diff --git a/nx.json b/nx.json index d2d2a188..e1dc0102 100644 --- a/nx.json +++ b/nx.json @@ -34,7 +34,8 @@ "exclude": [ "packages/commands/agnostic/*", "packages/agents/agnostic/*", - "packages/commands/typescript/*" + "packages/commands/typescript/*", + "packages/skills/agnostic/*" ] }, { @@ -42,7 +43,8 @@ "include": [ "packages/commands/agnostic/*", "packages/agents/agnostic/*", - "packages/commands/typescript/*" + "packages/commands/typescript/*", + "packages/skills/agnostic/*" ], "options": { "typecheck": { diff --git a/packages/ai-toolkit-nx-claude/generators.json b/packages/ai-toolkit-nx-claude/generators.json index 4d97f74f..bed5833b 100644 --- a/packages/ai-toolkit-nx-claude/generators.json +++ b/packages/ai-toolkit-nx-claude/generators.json @@ -15,6 +15,16 @@ "schema": "./dist/generators/add-agent/schema.json", "description": "Add a new Claude Code agent to existing or new packages" }, + "add-skill": { + "factory": "./dist/generators/add-skill/generator", + "schema": "./dist/generators/add-skill/schema.json", + "description": "Add a new Claude Code skill compatible with the plugin system" + }, + "init-plugin": { + "factory": "./dist/generators/init-plugin/generator", + "schema": "./dist/generators/init-plugin/schema.json", + "description": "Initialize a Claude Code plugin structure for distributing skills, commands, and agents" + }, "hooks": { "factory": "./dist/generators/hooks/generator", "schema": "./dist/generators/hooks/schema.json", diff --git a/packages/ai-toolkit-nx-claude/package.json b/packages/ai-toolkit-nx-claude/package.json index 2ca14f10..1baa91ed 100644 --- a/packages/ai-toolkit-nx-claude/package.json +++ b/packages/ai-toolkit-nx-claude/package.json @@ -49,8 +49,10 @@ "packages/ai-toolkit-nx-claude/src/scripts/claude-plus/index.ts", "packages/ai-toolkit-nx-claude/src/generators/add-agent/generator.ts", "packages/ai-toolkit-nx-claude/src/generators/add-command/generator.ts", + "packages/ai-toolkit-nx-claude/src/generators/add-skill/generator.ts", "packages/ai-toolkit-nx-claude/src/generators/addons/generator.ts", "packages/ai-toolkit-nx-claude/src/generators/hooks/generator.ts", + "packages/ai-toolkit-nx-claude/src/generators/init-plugin/generator.ts", "packages/ai-toolkit-nx-claude/src/generators/init/generator.ts" ], "tsConfig": "packages/ai-toolkit-nx-claude/tsconfig.lib.json", diff --git a/packages/ai-toolkit-nx-claude/src/generators/add-skill/files/__name__.md.template b/packages/ai-toolkit-nx-claude/src/generators/add-skill/files/__name__.md.template new file mode 100644 index 00000000..fe81a10a --- /dev/null +++ b/packages/ai-toolkit-nx-claude/src/generators/add-skill/files/__name__.md.template @@ -0,0 +1,45 @@ +--- +name: <%= name %> +description: <%= description %> +<% if (allowedTools) { %>allowed-tools: <%= allowedTools %> +<% } %>--- + +# <%= name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') %> Skill + + + +## When to Use + +Activate this skill when: + +- +- + +## Instructions + + + +### Step 1: [First Action] + + + +### Step 2: [Second Action] + + + +## Output Format + + + +```markdown +## Summary +[Summary of findings/actions] + +## Details +[Detailed information] +``` + +## Best Practices + +- +- diff --git a/packages/ai-toolkit-nx-claude/src/generators/add-skill/generator.ts b/packages/ai-toolkit-nx-claude/src/generators/add-skill/generator.ts new file mode 100644 index 00000000..d07d5c4e --- /dev/null +++ b/packages/ai-toolkit-nx-claude/src/generators/add-skill/generator.ts @@ -0,0 +1,381 @@ +import type { Tree } from '@nx/devkit'; +import { formatFiles, generateFiles, joinPathFragments } from '@nx/devkit'; +import * as path from 'path'; +import type { AddSkillGeneratorSchema } from './schema'; +import { promptForMissingOptions } from '../../utils/prompt-utils'; +import { libraryGenerator } from '@nx/js'; + +export async function addSkillGenerator( + tree: Tree, + options: AddSkillGeneratorSchema +) { + console.log('⚔ Add Skill Generator'); + console.log(' Creates Claude Code skills compatible with the plugin system'); + + // Discover existing skill packages + const skillPackages = discoverSkillPackages(tree); + + // Add option for creating new package + const packageChoices = [...skillPackages, '__create_new__']; + + // Get the schema path + const schemaPath = path.join(__dirname, 'schema.json'); + + // Prepare context for prompts with dynamic packages + const context: any = { + availablePackages: packageChoices, + }; + + // For non-interactive mode, default to first package if not specified + if ((options as any)['no-interactive'] || (options as any).noInteractive) { + if (!options.package && skillPackages.length > 0) { + options.package = skillPackages[0]; + } + } + + // Prompt for missing options (dynamic packages handled via context) + const normalizedOptions = await promptForMissingOptions( + options, + schemaPath, + context + ); + + // Handle package creation or selection + let targetPackage: string; + + if ( + normalizedOptions.package === '__create_new__' || + normalizedOptions.createNewPackage + ) { + // Create new package + if (!normalizedOptions.newPackageName) { + throw new Error('New package name is required'); + } + + targetPackage = normalizedOptions.newPackageName; + + console.log(`\nšŸ“¦ Creating new skills package: skills-${targetPackage}`); + + // Create the new package using Nx library generator + await libraryGenerator(tree, { + name: `skills-${targetPackage}`, + directory: `packages/skills/${targetPackage}`, + bundler: 'tsc', + linter: 'eslint', + unitTestRunner: 'jest', + testEnvironment: 'node', + skipFormat: true, + skipTsConfig: false, + strict: true, + publishable: true, + importPath: `@ai-toolkit/skills-${targetPackage}`, + }); + + // Create package.json content with nx targets + const packageJson = { + name: `@ai-toolkit/skills-${targetPackage}`, + version: '0.1.0', + private: true, + type: 'module', + main: './dist/src/index.js', + types: './dist/src/index.d.ts', + exports: { + '.': { + types: './dist/src/index.d.ts', + import: './dist/src/index.js', + default: './dist/src/index.js', + }, + './package.json': './package.json', + }, + nx: { + targets: { + 'generate-index': { + executor: 'nx:run-commands', + options: { + command: 'npx tsx scripts/generate.ts', + cwd: `packages/skills/${targetPackage}`, + }, + dependsOn: ['@ai-toolkit/utils:build'], + inputs: [ + '{projectRoot}/src/**/*.md', + '{projectRoot}/scripts/generate.ts', + ], + outputs: ['{projectRoot}/src/index.ts'], + }, + build: { + executor: '@nx/js:tsc', + outputs: ['{options.outputPath}'], + options: { + outputPath: `packages/skills/${targetPackage}/dist`, + main: `packages/skills/${targetPackage}/src/index.ts`, + tsConfig: `packages/skills/${targetPackage}/tsconfig.lib.json`, + assets: [ + { + input: `packages/skills/${targetPackage}/src`, + glob: '**/*.md', + output: '.', + }, + ], + }, + dependsOn: ['generate-index'], + }, + }, + }, + dependencies: { + '@ai-toolkit/utils': '*', + }, + }; + + tree.write( + joinPathFragments('packages', 'skills', targetPackage, 'package.json'), + JSON.stringify(packageJson, null, 2) + ); + + // Create generate script + const generateScript = `#!/usr/bin/env node + +import { generateIndex } from '@ai-toolkit/utils'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function main() { + const srcPath = join(__dirname, '..', 'src'); + const outputPath = join(srcPath, 'index.ts'); + + await generateIndex({ + srcPath, + outputPath, + typeName: 'SkillName', + exportName: 'skills', + typeNamePlural: 'Skills', + regenerateCommand: + 'npx nx run @ai-toolkit/skills-${targetPackage}:generate-index', + }); +} + +main().catch((error) => { + console.error('Failed to generate index:', error); + process.exit(1); +});`; + + tree.write( + joinPathFragments( + 'packages', + 'skills', + targetPackage, + 'scripts', + 'generate.ts' + ), + generateScript + ); + + // Create tsconfig.lib.json + const tsConfig = { + extends: '../../../tsconfig.base.json', + compilerOptions: { + baseUrl: '.', + rootDir: 'src', + outDir: 'dist', + tsBuildInfoFile: 'dist/tsconfig.lib.tsbuildinfo', + emitDeclarationOnly: false, + module: 'NodeNext', + moduleResolution: 'NodeNext', + target: 'es2022', + forceConsistentCasingInFileNames: true, + types: ['node'], + }, + include: ['src/**/*.ts'], + references: [ + { + path: '../../utils/tsconfig.lib.json', + }, + ], + }; + + tree.write( + joinPathFragments( + 'packages', + 'skills', + targetPackage, + 'tsconfig.lib.json' + ), + JSON.stringify(tsConfig, null, 2) + ); + + // Create README + const readme = `# @ai-toolkit/skills-${targetPackage} + +${normalizedOptions.newPackageDescription || `Claude Code skills for ${targetPackage}`} + +## Overview + +This package contains Claude Code skills specific to ${targetPackage}. Skills are model-invoked +capabilities that Claude autonomously decides when to use based on the task context. + +## Skills vs Commands + +- **Commands** (like \`/explore\`) are user-invoked via slash commands +- **Skills** are model-invoked - Claude decides when to use them based on the task + +## Usage + +Install this package to access ${targetPackage}-specific skills in Claude Code: + +\`\`\`bash +npx nx generate @uniswap/ai-toolkit-nx-claude:init +\`\`\` + +Then select skills from this package during installation, or install as a plugin. + +## Available Skills + +- \`${normalizedOptions.name}\`: ${normalizedOptions.description || 'Description pending'} + +## Claude Code Plugin Integration + +Skills from this package can be bundled into a Claude Code plugin. See the +[Claude Code plugins documentation](https://docs.anthropic.com/claude-code/plugins) for details. + +## Development + +To add new skills to this package: + +\`\`\`bash +npx nx generate @uniswap/ai-toolkit-nx-claude:add-skill +\`\`\` + +After adding or modifying skills, regenerate the index: + +\`\`\`bash +npx nx run @ai-toolkit/skills-${targetPackage}:generate-index +\`\`\` +`; + + tree.write( + joinPathFragments('packages', 'skills', targetPackage, 'README.md'), + readme + ); + } else { + targetPackage = normalizedOptions.package!.replace('skills-', ''); + } + + // Generate the skill file + const skillFileName = `${normalizedOptions.name}.md`; + const skillPath = joinPathFragments('packages', 'skills', targetPackage, 'src'); + + // Check if skill already exists + if (tree.exists(joinPathFragments(skillPath, skillFileName))) { + const overwrite = await promptForOverwrite(normalizedOptions.name!); + if (!overwrite) { + console.log('āŒ Skill creation cancelled'); + return; + } + } + + // Generate files from template + generateFiles(tree, path.join(__dirname, 'files'), skillPath, { + ...normalizedOptions, + name: normalizedOptions.name, + description: normalizedOptions.description || `${normalizedOptions.name} skill`, + allowedTools: normalizedOptions.allowedTools || '', + template: '', + }); + + // Format files + await formatFiles(tree); + + console.log(`\nāœ… Skill file created at: ${skillPath}/${skillFileName}`); + console.log('\nšŸ“ Next steps:'); + console.log(`1. Edit ${skillPath}/${skillFileName} and add your skill instructions`); + console.log('\nšŸ’” The skill file has TODO markers for you to fill in.'); + console.log('\nšŸ”Œ Plugin Integration:'); + console.log(' Skills are automatically available when installed via the init generator'); + console.log(' or can be bundled into a Claude Code plugin for distribution.'); + + // Schedule generate-index to run after the generator completes + const { spawn } = await import('child_process'); + + // Create a custom task that runs after file generation + const runGenerateIndexTask = (tree: Tree) => { + return () => { + console.log(`\nšŸ”„ Updating package index...`); + return new Promise((resolve) => { + const child = spawn( + 'npx', + ['nx', 'run', `@ai-toolkit/skills-${targetPackage}:generate-index`], + { + stdio: 'inherit', + shell: true, + cwd: tree.root, + } + ); + + child.on('close', (code) => { + if (code === 0) { + console.log(`āœ… Package index updated successfully`); + resolve(); + } else { + console.warn( + `āš ļø Failed to update package index. You may need to run manually:` + ); + console.warn( + ` npx nx run @ai-toolkit/skills-${targetPackage}:generate-index` + ); + // Don't reject - let the generator complete successfully + resolve(); + } + }); + + child.on('error', (error) => { + console.warn(`āš ļø Failed to update package index: ${error.message}`); + console.warn( + ` Run manually: npx nx run @ai-toolkit/skills-${targetPackage}:generate-index` + ); + // Don't reject - let the generator complete successfully + resolve(); + }); + }); + }; + }; + + // Return the task to be executed after files are written + return runGenerateIndexTask(tree); +} + +function discoverSkillPackages(tree: Tree): string[] { + const packages: string[] = []; + const skillsDir = joinPathFragments('packages', 'skills'); + + if (!tree.exists(skillsDir)) { + return packages; + } + + // Get all subdirectories under packages/skills + const children = tree.children(skillsDir); + + for (const child of children) { + const packagePath = joinPathFragments(skillsDir, child); + // Check if it's a valid package (has package.json) + if (tree.exists(joinPathFragments(packagePath, 'package.json'))) { + packages.push(`skills-${child}`); + } + } + + return packages; +} + +async function promptForOverwrite(name: string): Promise { + const { prompt } = await import('enquirer'); + const response = await prompt<{ overwrite: boolean }>({ + type: 'confirm', + name: 'overwrite', + message: `āš ļø Skill "${name}" already exists. Overwrite?`, + initial: false, + }); + return response.overwrite; +} + +export default addSkillGenerator; diff --git a/packages/ai-toolkit-nx-claude/src/generators/add-skill/schema.d.ts b/packages/ai-toolkit-nx-claude/src/generators/add-skill/schema.d.ts new file mode 100644 index 00000000..05f98100 --- /dev/null +++ b/packages/ai-toolkit-nx-claude/src/generators/add-skill/schema.d.ts @@ -0,0 +1,9 @@ +export interface AddSkillGeneratorSchema { + package?: string; + createNewPackage?: boolean; + newPackageName?: string; + newPackageDescription?: string; + name?: string; + description?: string; + allowedTools?: string; +} diff --git a/packages/ai-toolkit-nx-claude/src/generators/add-skill/schema.json b/packages/ai-toolkit-nx-claude/src/generators/add-skill/schema.json new file mode 100644 index 00000000..ab27ff04 --- /dev/null +++ b/packages/ai-toolkit-nx-claude/src/generators/add-skill/schema.json @@ -0,0 +1,58 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "AddSkill", + "title": "Add Skill Generator", + "description": "Creates a new Claude Code skill in the skills package", + "type": "object", + "properties": { + "package": { + "type": "string", + "description": "Target package for the skill", + "prompt-message": "šŸ“¦ Which package should this skill be added to?" + }, + "createNewPackage": { + "type": "boolean", + "description": "Create a new package for this skill", + "default": false, + "prompt-message": "āž• Would you like to create a new package instead?", + "prompt-type": "confirm", + "prompt-when": "package === '__create_new__'" + }, + "newPackageName": { + "type": "string", + "description": "Name of the new package (e.g., backend, frontend, typescript)", + "prompt-message": "šŸ“ Enter the name for the new package (e.g., backend, frontend, typescript):", + "prompt-when": "createNewPackage === true", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "newPackageDescription": { + "type": "string", + "description": "Description of the new package", + "prompt-message": "šŸ“„ Enter a description for the new package:", + "prompt-when": "createNewPackage === true" + }, + "name": { + "type": "string", + "description": "Name of the skill (lowercase, hyphens allowed)", + "$default": { + "$source": "argv", + "index": 0 + }, + "prompt-message": "⚔ What should the skill be called? (kebab-case, e.g., code-review):", + "pattern": "^[a-z][a-z0-9-]*$", + "maxLength": 64 + }, + "description": { + "type": "string", + "description": "Description of what the skill does and when Claude should use it (max 1024 chars)", + "prompt-message": "šŸ’¬ Describe what this skill does and when Claude should use it:", + "maxLength": 1024 + }, + "allowedTools": { + "type": "string", + "description": "Comma-separated list of tools this skill can use (optional)", + "prompt-message": "šŸ”§ What tools should this skill have access to? (e.g., Read, Grep, Glob - leave empty for all):" + } + }, + "required": [] +} diff --git a/packages/ai-toolkit-nx-claude/src/generators/init-plugin/generator.ts b/packages/ai-toolkit-nx-claude/src/generators/init-plugin/generator.ts new file mode 100644 index 00000000..de293926 --- /dev/null +++ b/packages/ai-toolkit-nx-claude/src/generators/init-plugin/generator.ts @@ -0,0 +1,206 @@ +import type { Tree } from '@nx/devkit'; +import { formatFiles, joinPathFragments, logger } from '@nx/devkit'; +import * as path from 'path'; +import type { InitPluginGeneratorSchema } from './schema'; +import { promptForMissingOptions } from '../../utils/prompt-utils'; + +export async function initPluginGenerator( + tree: Tree, + options: InitPluginGeneratorSchema +) { + logger.info('šŸ”Œ Initialize Claude Code Plugin'); + logger.info(' Creates a plugin structure compatible with Claude Code\'s plugin system'); + + // Get the schema path + const schemaPath = path.join(__dirname, 'schema.json'); + + // Prompt for missing options + const normalizedOptions = await promptForMissingOptions( + options, + schemaPath, + {} + ); + + const pluginName = normalizedOptions.name!; + const baseDir = normalizedOptions.directory || 'plugins'; + const pluginDir = joinPathFragments(baseDir, pluginName); + + // Create plugin manifest + const manifest = { + name: pluginName, + description: normalizedOptions.description || `${pluginName} plugin for Claude Code`, + version: normalizedOptions.version || '1.0.0', + author: normalizedOptions.author || '', + }; + + // Create .claude-plugin/plugin.json + tree.write( + joinPathFragments(pluginDir, '.claude-plugin', 'plugin.json'), + JSON.stringify(manifest, null, 2) + ); + + // Create directory structure based on options + if (normalizedOptions.includeSkills !== false) { + // Create skills directory with a placeholder SKILL.md + const skillContent = `--- +name: example-skill +description: Example skill - replace with your skill description. Claude uses this to decide when to activate the skill. +--- + +# Example Skill + +This is a placeholder skill. Replace this with your actual skill content. + +## When to Use + +Describe when Claude should activate this skill. + +## Instructions + +Provide step-by-step guidance for Claude to follow. +`; + tree.write( + joinPathFragments(pluginDir, 'skills', 'example', 'SKILL.md'), + skillContent + ); + } + + if (normalizedOptions.includeCommands !== false) { + // Create commands directory with a placeholder command + const commandContent = `--- +description: Example command - replace with your command description +argument-hint: +--- + +# Example Command + +This is a placeholder command. Replace this with your actual command content. + +## Inputs + +Describe expected inputs. + +## Task + +Describe what the command does. + +## Output + +Describe expected output format. +`; + tree.write( + joinPathFragments(pluginDir, 'commands', 'example.md'), + commandContent + ); + } + + if (normalizedOptions.includeAgents !== false) { + // Create agents directory with a placeholder agent + const agentContent = `--- +name: example-agent +description: Example agent - replace with your agent description +--- + +# Example Agent + +This is a placeholder agent. Replace this with your actual agent content. + +## Mission + +Describe the agent's specialized purpose. + +## Inputs + +Describe expected inputs. + +## Process + +Describe the agent's methodology. + +## Output + +Describe expected output format. +`; + tree.write( + joinPathFragments(pluginDir, 'agents', 'example.md'), + agentContent + ); + } + + // Create README for the plugin + const readme = `# ${pluginName} + +${normalizedOptions.description || `A Claude Code plugin`} + +## Installation + +### Via Plugin Marketplace + +1. Add this plugin's marketplace to Claude Code: + \`\`\` + /plugin marketplace add + \`\`\` + +2. Install the plugin: + \`\`\` + /plugin install ${pluginName} + \`\`\` + +### Via Direct Installation + +Copy this directory to your Claude Code plugins location or use the \`/plugin\` command. + +## Contents + +${normalizedOptions.includeSkills !== false ? '### Skills\n\nModel-invoked capabilities that Claude uses autonomously.\n\n- See `skills/` directory\n' : ''} +${normalizedOptions.includeCommands !== false ? '### Commands\n\nUser-invoked slash commands.\n\n- See `commands/` directory\n' : ''} +${normalizedOptions.includeAgents !== false ? '### Agents\n\nSpecialized agents for complex tasks.\n\n- See `agents/` directory\n' : ''} + +## Development + +### Adding Skills + +1. Create a new directory under \`skills/\` +2. Add a \`SKILL.md\` file with: + - \`name\`: Lowercase, hyphens allowed (max 64 chars) + - \`description\`: When Claude should use this skill (max 1024 chars) + +### Adding Commands + +1. Create a new \`.md\` file under \`commands/\` +2. Include \`description\` in the frontmatter + +### Adding Agents + +1. Create a new \`.md\` file under \`agents/\` +2. Include \`name\` and \`description\` in the frontmatter + +## Plugin Structure + +\`\`\` +${pluginName}/ +ā”œā”€ā”€ .claude-plugin/ +│ └── plugin.json # Plugin manifest +${normalizedOptions.includeSkills !== false ? 'ā”œā”€ā”€ skills/ # Model-invoked skills\n│ └── example/\n│ └── SKILL.md\n' : ''}${normalizedOptions.includeCommands !== false ? 'ā”œā”€ā”€ commands/ # User-invoked commands\n│ └── example.md\n' : ''}${normalizedOptions.includeAgents !== false ? 'ā”œā”€ā”€ agents/ # Specialized agents\n│ └── example.md\n' : ''}└── README.md +\`\`\` + +## Resources + +- [Claude Code Plugins Documentation](https://docs.anthropic.com/claude-code/plugins) +- [AI Toolkit Repository](https://github.com/Uniswap/ai-toolkit) +`; + + tree.write(joinPathFragments(pluginDir, 'README.md'), readme); + + await formatFiles(tree); + + logger.info(`\nāœ… Plugin created at: ${pluginDir}`); + logger.info('\nšŸ“ Next steps:'); + logger.info('1. Review and customize the example files'); + logger.info('2. Add your skills, commands, and agents'); + logger.info('3. Test locally by installing the plugin'); + logger.info(`\nšŸ”Œ To install locally: /plugin install ${pluginDir}`); + logger.info('\nšŸ“š Documentation: https://docs.anthropic.com/claude-code/plugins'); +} + +export default initPluginGenerator; diff --git a/packages/ai-toolkit-nx-claude/src/generators/init-plugin/schema.d.ts b/packages/ai-toolkit-nx-claude/src/generators/init-plugin/schema.d.ts new file mode 100644 index 00000000..ca8eee0f --- /dev/null +++ b/packages/ai-toolkit-nx-claude/src/generators/init-plugin/schema.d.ts @@ -0,0 +1,10 @@ +export interface InitPluginGeneratorSchema { + name?: string; + description?: string; + version?: string; + author?: string; + directory?: string; + includeSkills?: boolean; + includeCommands?: boolean; + includeAgents?: boolean; +} diff --git a/packages/ai-toolkit-nx-claude/src/generators/init-plugin/schema.json b/packages/ai-toolkit-nx-claude/src/generators/init-plugin/schema.json new file mode 100644 index 00000000..0a9f79c3 --- /dev/null +++ b/packages/ai-toolkit-nx-claude/src/generators/init-plugin/schema.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "InitPlugin", + "title": "Initialize Claude Code Plugin", + "description": "Creates a Claude Code plugin structure for distributing skills, commands, and agents", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Plugin name (lowercase, hyphens allowed)", + "$default": { + "$source": "argv", + "index": 0 + }, + "prompt-message": "šŸ“¦ What should the plugin be called? (e.g., my-team-toolkit):", + "pattern": "^[a-z][a-z0-9-]*$" + }, + "description": { + "type": "string", + "description": "Plugin description", + "prompt-message": "šŸ’¬ Brief description of this plugin:" + }, + "version": { + "type": "string", + "description": "Plugin version", + "default": "1.0.0", + "x-prompt": { + "message": "šŸ“Œ Plugin version:", + "type": "input" + } + }, + "author": { + "type": "string", + "description": "Plugin author or organization", + "prompt-message": "šŸ‘¤ Author or organization name:" + }, + "directory": { + "type": "string", + "description": "Directory to create the plugin in", + "default": "plugins", + "prompt-message": "šŸ“ Directory for the plugin (relative to workspace root):" + }, + "includeSkills": { + "type": "boolean", + "description": "Include skills directory", + "default": true, + "x-prompt": { + "message": "šŸŽÆ Include skills directory?", + "type": "confirm" + } + }, + "includeCommands": { + "type": "boolean", + "description": "Include commands directory", + "default": true, + "x-prompt": { + "message": "⚔ Include commands directory?", + "type": "confirm" + } + }, + "includeAgents": { + "type": "boolean", + "description": "Include agents directory", + "default": true, + "x-prompt": { + "message": "šŸ¤– Include agents directory?", + "type": "confirm" + } + } + }, + "required": [] +} diff --git a/packages/ai-toolkit-nx-claude/src/generators/init/generator.ts b/packages/ai-toolkit-nx-claude/src/generators/init/generator.ts index 97c98b39..5bdfb6b0 100644 --- a/packages/ai-toolkit-nx-claude/src/generators/init/generator.ts +++ b/packages/ai-toolkit-nx-claude/src/generators/init/generator.ts @@ -23,6 +23,21 @@ import { commands as agnosticCommands } from '@ai-toolkit/commands-agnostic'; import { agents as agnosticAgents } from '@ai-toolkit/agents-agnostic'; import { addonsGenerator, hooksGenerator } from '../../index'; +// Type for skills - skills package may be added later +type SkillsMap = Record; + +// Helper to dynamically load skills (avoids hard compile-time dependency) +function getSkills(): SkillsMap { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const skillsModule = require('@ai-toolkit/skills-agnostic'); + return skillsModule.skills || {}; + } catch { + // Skills package not available - return empty object + return {}; + } +} + // Recommended default commands for most users const DEFAULT_COMMANDS = ['explore', 'plan', 'review-plan', 'execute-plan', 'address-pr-issues']; @@ -36,17 +51,21 @@ const DEFAULT_AGENTS = [ 'pr-reviewer', ]; +// Recommended default skills for most users (model-invoked capabilities) +const DEFAULT_SKILLS = ['codebase-exploration']; + interface Manifest { version: string; installedAt: string; commands: string[]; agents: string[]; + skills: string[]; files: string[]; } function checkExistingFiles( targetDir: string, - subDir: 'commands' | 'agents', + subDir: 'commands' | 'agents' | 'skills', items: string[] ): Set { const existing = new Set(); @@ -114,8 +133,10 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { options.installationType = 'global'; options.commands = DEFAULT_COMMANDS; options.agents = DEFAULT_AGENTS; + options.skills = DEFAULT_SKILLS; options.installCommands = true; options.installAgents = true; + options.installSkills = true; options.installHooks = true; // Auto-install hooks in default mode options.hooksMode = 'sound'; // Use sound notifications options.installAddons = true; // Install all MCPs in default mode @@ -124,6 +145,7 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { logger.info('šŸ“ Location: Global (~/.claude)'); logger.info(`šŸ“ Commands: ${DEFAULT_COMMANDS.length} pre-selected`); logger.info(`šŸ¤– Agents: ${DEFAULT_AGENTS.length} pre-selected`); + logger.info(`šŸŽÆ Skills: ${DEFAULT_SKILLS.length} pre-selected`); logger.info(`šŸ”Œ MCPs: All recommended servers will be installed\n`); // Mark all default mode options as explicitly provided to skip prompts @@ -132,24 +154,32 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { explicitlyProvided.set('installationType', 'global'); explicitlyProvided.set('installCommands', true); explicitlyProvided.set('installAgents', true); + explicitlyProvided.set('installSkills', true); explicitlyProvided.set('installHooks', true); explicitlyProvided.set('hooksMode', 'sound'); explicitlyProvided.set('installAddons', true); explicitlyProvided.set('dry', false); explicitlyProvided.set('commands', DEFAULT_COMMANDS); explicitlyProvided.set('agents', DEFAULT_AGENTS); + explicitlyProvided.set('skills', DEFAULT_SKILLS); } // Handle interactive mode with schema-driven prompts const schemaPath = path.join(__dirname, 'schema.json'); - // Extract command and agent descriptions from the new structure + // Load skills dynamically to avoid hard compile-time dependency + const agnosticSkills = getSkills(); + + // Extract command, agent, and skill descriptions from the new structure const commandDescriptions = Object.fromEntries( Object.entries(agnosticCommands).map(([key, value]) => [key, value.description]) ); const agentDescriptions = Object.fromEntries( Object.entries(agnosticAgents).map(([key, value]) => [key, value.description]) ); + const skillDescriptions = Object.fromEntries( + Object.entries(agnosticSkills).map(([key, value]) => [key, value.description]) + ); // Define directory paths const homeDir = os.homedir(); @@ -164,12 +194,14 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { Object.keys(agnosticCommands) ); const globalExistingAgents = checkExistingFiles(globalDir, 'agents', Object.keys(agnosticAgents)); + const globalExistingSkills = checkExistingFiles(globalDir, 'skills', Object.keys(agnosticSkills)); const localExistingCommands = checkExistingFiles( localDir, 'commands', Object.keys(agnosticCommands) ); const localExistingAgents = checkExistingFiles(localDir, 'agents', Object.keys(agnosticAgents)); + const localExistingSkills = checkExistingFiles(localDir, 'skills', Object.keys(agnosticSkills)); // Pass the no-interactive flag to prompt-utils via options const optionsWithNoInteractive = { @@ -185,14 +217,19 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { { availableCommands: Object.keys(agnosticCommands), availableAgents: Object.keys(agnosticAgents), + availableSkills: Object.keys(agnosticSkills), commandDescriptions, agentDescriptions, + skillDescriptions, globalExistingCommands, globalExistingAgents, + globalExistingSkills, localExistingCommands, localExistingAgents, + localExistingSkills, defaultCommands: DEFAULT_COMMANDS, defaultAgents: DEFAULT_AGENTS, + defaultSkills: DEFAULT_SKILLS, }, explicitlyProvided ); @@ -221,14 +258,21 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { normalizedOptions.agents = Object.keys(agnosticAgents); logger.info(`šŸ¤– All agents selected (${normalizedOptions.agents.length} total)`); } + if (normalizedOptions.skillSelectionMode === 'all') { + normalizedOptions.skills = Object.keys(agnosticSkills); + logger.info(`šŸŽÆ All skills selected (${normalizedOptions.skills.length} total)`); + } - // Skip command/agent arrays if install flags are false + // Skip command/agent/skill arrays if install flags are false if (normalizedOptions.installCommands === false) { normalizedOptions.commands = []; } if (normalizedOptions.installAgents === false) { normalizedOptions.agents = []; } + if (normalizedOptions.installSkills === false) { + normalizedOptions.skills = []; + } // Determine target directory based on installation type const isGlobalInstall = normalizedOptions.installationType === 'global'; @@ -261,14 +305,17 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { // Create directory structure const commandsDir = path.join(targetDir, 'commands'); const agentsDir = path.join(targetDir, 'agents'); + const skillsDir = path.join(targetDir, 'skills'); // Relative paths for tree.write() const relativeCommandsDir = path.join(relativeTargetDir, 'commands'); const relativeAgentsDir = path.join(relativeTargetDir, 'agents'); + const relativeSkillsDir = path.join(relativeTargetDir, 'skills'); // Collect files to install const installedCommands: string[] = []; const installedAgents: string[] = []; + const installedSkills: string[] = []; const installedFiles: string[] = []; // Install selected commands @@ -405,6 +452,76 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { } } + // Install selected skills (Claude Code plugin-compatible format) + const skillsToInstall = normalizedOptions.skills || []; + + for (const skillName of skillsToInstall) { + let sourcePath: string | null = null; + + // First check for bundled content (when running as standalone package) + const bundledContentDir = path.join(__dirname, '..', '..', 'content', 'skills'); + if (fs.existsSync(bundledContentDir)) { + // Check in bundled content subdirectories + const contentSubDirs = fs.readdirSync(bundledContentDir).filter((item) => { + const itemPath = path.join(bundledContentDir, item); + return fs.statSync(itemPath).isDirectory(); + }); + + for (const subDir of contentSubDirs) { + const potentialPath = path.join(bundledContentDir, subDir, `${skillName}.md`); + if (fs.existsSync(potentialPath)) { + sourcePath = potentialPath; + break; + } + } + } + + // Fall back to workspace lookup if not found in bundled content + if (!sourcePath) { + // Search through all subdirectories under packages/skills/ + const skillsBaseDir = path.join(workspaceRoot, 'packages/skills'); + + // Check if skills directory exists + if (fs.existsSync(skillsBaseDir)) { + // Get all subdirectories (agnostic, mobile, web, etc.) + const skillSubDirs = fs.readdirSync(skillsBaseDir).filter((item) => { + const itemPath = path.join(skillsBaseDir, item); + return fs.statSync(itemPath).isDirectory(); + }); + + // Search for the skill file in each subdirectory's src folder + for (const subDir of skillSubDirs) { + const potentialPath = path.join(skillsBaseDir, subDir, 'src', `${skillName}.md`); + if (fs.existsSync(potentialPath)) { + sourcePath = potentialPath; + break; + } + } + } + } + + // Skills are installed in a subdirectory structure for Claude Code plugin compatibility + // Format: skills//SKILL.md + const skillSubDir = path.join(skillsDir, skillName); + const destPath = path.join(skillSubDir, 'SKILL.md'); + const relativeDestPath = path.join(relativeSkillsDir, skillName, 'SKILL.md'); + + try { + if (sourcePath && fs.existsSync(sourcePath)) { + const content = fs.readFileSync(sourcePath, 'utf-8'); + if (!isDryRun) { + tree.write(relativeDestPath, content); + } + installedSkills.push(skillName); + installedFiles.push(path.relative(targetDir, destPath)); + } else { + logger.warn(`Skill file not found: ${skillName}`); + } + } catch (error) { + logger.warn(`Error reading skill ${skillName}: ${error}`); + } + } + // Display installation plan logger.info('šŸ“¦ Installation Plan:'); logger.info( @@ -416,6 +533,7 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { ); logger.info(` Commands: ${installedCommands.length} selected`); logger.info(` Agents: ${installedAgents.length} selected`); + logger.info(` Skills: ${installedSkills.length} selected`); if (isDryRun) { logger.info('\nšŸ“‹ Would install:'); @@ -441,6 +559,7 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { installedAt: new Date().toISOString(), commands: installedCommands, agents: installedAgents, + skills: installedSkills, files: installedFiles, }; @@ -535,6 +654,9 @@ export async function initGenerator(tree: Tree, options: InitGeneratorSchema) { if (installedAgents.length > 0) { logger.info(` Agents: ${installedAgents.join(', ')}`); } + if (installedSkills.length > 0) { + logger.info(` Skills: ${installedSkills.join(', ')}`); + } if (normalizedOptions.installHooks) { logger.info(' Hooks: āœ… Installed'); } diff --git a/packages/ai-toolkit-nx-claude/src/generators/init/schema.d.ts b/packages/ai-toolkit-nx-claude/src/generators/init/schema.d.ts index 5b5e2393..587671bc 100644 --- a/packages/ai-toolkit-nx-claude/src/generators/init/schema.d.ts +++ b/packages/ai-toolkit-nx-claude/src/generators/init/schema.d.ts @@ -6,12 +6,15 @@ export interface InitGeneratorSchema { commandSelectionMode?: 'all' | 'specific'; installAgents?: boolean; agentSelectionMode?: 'all' | 'specific'; + installSkills?: boolean; + skillSelectionMode?: 'all' | 'specific'; installHooks?: boolean; hooksMode?: 'sound' | 'speech' | 'both'; installAddons?: boolean; addonSelectionMode?: 'all' | 'specific'; commands?: string[]; agents?: string[]; + skills?: string[]; dry?: boolean; nonInteractive?: boolean; force?: boolean; diff --git a/packages/ai-toolkit-nx-claude/src/generators/init/schema.json b/packages/ai-toolkit-nx-claude/src/generators/init/schema.json index a1caab58..57e454cf 100644 --- a/packages/ai-toolkit-nx-claude/src/generators/init/schema.json +++ b/packages/ai-toolkit-nx-claude/src/generators/init/schema.json @@ -99,6 +99,32 @@ } ] }, + "installSkills": { + "type": "boolean", + "description": "Whether to install skills (model-invoked capabilities)", + "default": true, + "prompt-when": "installMode === 'custom'", + "prompt-message": "šŸŽÆ Install skills? (model-invoked capabilities Claude uses autonomously)", + "prompt-type": "confirm" + }, + "skillSelectionMode": { + "type": "string", + "description": "Whether to install all skills or select specific ones", + "enum": ["all", "specific"], + "prompt-when": "installMode === 'custom' && installSkills === true", + "prompt-message": "šŸŽÆ Select skills to install:", + "prompt-type": "list", + "prompt-items": [ + { + "value": "all", + "label": "All skills" + }, + { + "value": "specific", + "label": "Select specific skills" + } + ] + }, "commands": { "type": "array", "description": "Specific commands to install", @@ -115,6 +141,14 @@ }, "prompt-when": "installMode === 'custom' && installAgents === true && agentSelectionMode === 'specific'" }, + "skills": { + "type": "array", + "description": "Specific skills to install", + "items": { + "type": "string" + }, + "prompt-when": "installMode === 'custom' && installSkills === true && skillSelectionMode === 'specific'" + }, "installHooks": { "type": "boolean", "description": "Install notification hooks (plays sound when Claude needs input)", diff --git a/packages/ai-toolkit-nx-claude/src/index.ts b/packages/ai-toolkit-nx-claude/src/index.ts index 7231f768..3759aab3 100644 --- a/packages/ai-toolkit-nx-claude/src/index.ts +++ b/packages/ai-toolkit-nx-claude/src/index.ts @@ -2,4 +2,8 @@ // Works in both dev (exports.development -> src) and dist (exports.import/default -> dist) export { default as hooksGenerator } from './generators/hooks/generator'; export { default as addonsGenerator } from './generators/addons/generator'; +export { default as addSkillGenerator } from './generators/add-skill/generator'; +export { default as initPluginGenerator } from './generators/init-plugin/generator'; export type { InitGeneratorSchema } from './generators/init/schema'; +export type { AddSkillGeneratorSchema } from './generators/add-skill/schema'; +export type { InitPluginGeneratorSchema } from './generators/init-plugin/schema'; diff --git a/packages/ai-toolkit-nx-claude/src/utils/prompt-utils.ts b/packages/ai-toolkit-nx-claude/src/utils/prompt-utils.ts index cbbd29a2..2cf6062c 100644 --- a/packages/ai-toolkit-nx-claude/src/utils/prompt-utils.ts +++ b/packages/ai-toolkit-nx-claude/src/utils/prompt-utils.ts @@ -33,20 +33,25 @@ export async function promptForMissingOptions>( context: { availableCommands?: string[]; availableAgents?: string[]; + availableSkills?: string[]; availableAddons?: string[]; commandDescriptions?: Record; agentDescriptions?: Record; + skillDescriptions?: Record; addonDescriptions?: Record; // Explicit global/local format globalExistingCommands?: Set; globalExistingAgents?: Set; + globalExistingSkills?: Set; localExistingCommands?: Set; localExistingAgents?: Set; + localExistingSkills?: Set; // Dynamic packages support availablePackages?: string[]; // Default selections for default mode defaultCommands?: string[]; defaultAgents?: string[]; + defaultSkills?: string[]; } = {}, explicitlyProvidedOptions?: Map | Set ): Promise { diff --git a/packages/ai-toolkit-nx-claude/tsconfig.lib.json b/packages/ai-toolkit-nx-claude/tsconfig.lib.json index b8ce4d65..1057ca22 100644 --- a/packages/ai-toolkit-nx-claude/tsconfig.lib.json +++ b/packages/ai-toolkit-nx-claude/tsconfig.lib.json @@ -16,6 +16,9 @@ }, { "path": "../commands/agnostic/tsconfig.lib.json" + }, + { + "path": "../skills/agnostic/tsconfig.lib.json" } ] } diff --git a/packages/claude-mcp-helper/jest.config.ts b/packages/claude-mcp-helper/jest.config.ts index 4d0e683d..9574e7c1 100644 --- a/packages/claude-mcp-helper/jest.config.ts +++ b/packages/claude-mcp-helper/jest.config.ts @@ -1,9 +1,7 @@ import { readFileSync } from 'fs'; // Reading the SWC compilation config for the spec files -const swcJestConfig = JSON.parse( - readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8') -); +const swcJestConfig = JSON.parse(readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8')); // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves swcJestConfig.swcrc = false; diff --git a/packages/linear-task-utils/jest.config.ts b/packages/linear-task-utils/jest.config.ts index 0d9f24b4..25062eee 100644 --- a/packages/linear-task-utils/jest.config.ts +++ b/packages/linear-task-utils/jest.config.ts @@ -1,9 +1,7 @@ import { readFileSync } from 'fs'; // Reading the SWC compilation config for the spec files -const swcJestConfig = JSON.parse( - readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8') -); +const swcJestConfig = JSON.parse(readFileSync(`${__dirname}/.spec.swcrc`, 'utf-8')); // Disable .swcrc look-up by SWC core because we're passing in swcJestConfig ourselves swcJestConfig.swcrc = false; diff --git a/packages/skills/agnostic/package.json b/packages/skills/agnostic/package.json new file mode 100644 index 00000000..6094791e --- /dev/null +++ b/packages/skills/agnostic/package.json @@ -0,0 +1,61 @@ +{ + "name": "@ai-toolkit/skills-agnostic", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "exports": { + ".": { + "types": "./dist/src/index.d.ts", + "import": "./dist/src/index.js", + "default": "./dist/src/index.js" + }, + "./package.json": "./package.json" + }, + "nx": { + "targets": { + "generate-index": { + "executor": "nx:run-commands", + "options": { + "command": "npx tsx scripts/generate.ts", + "cwd": "packages/skills/agnostic" + }, + "dependsOn": [ + "@ai-toolkit/utils:build" + ], + "inputs": [ + "{projectRoot}/src/**/*.md", + "{projectRoot}/scripts/generate.ts" + ], + "outputs": [ + "{projectRoot}/src/index.ts" + ] + }, + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "packages/skills/agnostic/dist", + "main": "packages/skills/agnostic/src/index.ts", + "tsConfig": "packages/skills/agnostic/tsconfig.lib.json", + "assets": [ + { + "input": "packages/skills/agnostic/src", + "glob": "**/*.md", + "output": "." + } + ] + }, + "dependsOn": [ + "generate-index" + ] + } + } + }, + "dependencies": { + "@ai-toolkit/utils": "0.2.12-next.7" + } +} diff --git a/packages/skills/agnostic/project.json b/packages/skills/agnostic/project.json new file mode 100644 index 00000000..64c939f1 --- /dev/null +++ b/packages/skills/agnostic/project.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "name": "@ai-toolkit/skills-agnostic", + "sourceRoot": "packages/skills/agnostic/src", + "projectType": "library" +} diff --git a/packages/skills/agnostic/scripts/generate.ts b/packages/skills/agnostic/scripts/generate.ts new file mode 100644 index 00000000..04b8dfb5 --- /dev/null +++ b/packages/skills/agnostic/scripts/generate.ts @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { generateIndex } from '@ai-toolkit/utils'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function main() { + const srcPath = join(__dirname, '..', 'src'); + const outputPath = join(srcPath, 'index.ts'); + + await generateIndex({ + srcPath, + outputPath, + typeName: 'SkillName', + exportName: 'skills', + typeNamePlural: 'Skills', + regenerateCommand: + 'npx nx run @ai-toolkit/skills-agnostic:generate-index', + }); +} + +main().catch((error) => { + console.error('Failed to generate index:', error); + process.exit(1); +}); diff --git a/packages/skills/agnostic/src/codebase-exploration.md b/packages/skills/agnostic/src/codebase-exploration.md new file mode 100644 index 00000000..2f0466d7 --- /dev/null +++ b/packages/skills/agnostic/src/codebase-exploration.md @@ -0,0 +1,95 @@ +--- +name: codebase-exploration +description: Use this skill when exploring or analyzing a codebase structure, understanding architecture, or mapping out dependencies between components. +allowed-tools: Read, Grep, Glob, Bash(git ls-files:*), Bash(find:*) +--- + +# Codebase Exploration Skill + +This skill provides structured guidance for comprehensive codebase exploration and analysis. + +## When to Use + +Activate this skill when: + +- Exploring an unfamiliar codebase or project structure +- Mapping out architecture and component relationships +- Understanding dependency graphs and integration points +- Preparing context for implementation tasks +- Answering questions about "how does X work" or "where is Y implemented" + +## Exploration Methodology + +### Phase 1: High-Level Structure + +1. **Project Root Analysis** + - Check for package.json, cargo.toml, go.mod, or other manifest files + - Identify the build system (nx, make, gradle, etc.) + - Look for configuration files (.env.example, docker-compose.yml) + - Read README.md and CONTRIBUTING.md if present + +2. **Directory Structure** + - Map top-level directories and their purposes + - Identify src/, lib/, pkg/, internal/, cmd/ patterns + - Note test directories and their organization + - Find documentation locations + +### Phase 2: Dependency Mapping + +1. **Internal Dependencies** + - Trace imports and module relationships + - Build a mental model of the dependency graph + - Identify core modules vs. peripheral utilities + +2. **External Dependencies** + - Review package manifests for third-party deps + - Note which external services are integrated + - Check for API clients or SDK usage + +### Phase 3: Pattern Recognition + +1. **Code Patterns** + - Identify naming conventions (camelCase, snake_case) + - Note architectural patterns (MVC, hexagonal, etc.) + - Find common utilities and shared code + +2. **Data Flow** + - Trace how data enters and exits the system + - Map transformation pipelines + - Identify persistence layers + +## Output Format + +When using this skill, structure findings as: + +```markdown +## Summary +[1-2 sentence overview] + +## Key Components +- component-name: purpose and responsibility +- ... + +## Patterns Observed +- Pattern: description + +## Dependencies +- Internal: list of core internal dependencies +- External: list of key external dependencies + +## Data Flow +[Description of how data moves through the system] + +## Gotchas +- Non-obvious behaviors or potential pitfalls + +## Suggested Next Steps +- Recommended areas for deeper investigation +``` + +## Best Practices + +- Start broad, then narrow focus based on findings +- Document assumptions and verify them +- Cross-reference multiple files to confirm patterns +- Note areas that seem inconsistent or unusual diff --git a/packages/skills/agnostic/src/index.ts b/packages/skills/agnostic/src/index.ts new file mode 100644 index 00000000..3961da7e --- /dev/null +++ b/packages/skills/agnostic/src/index.ts @@ -0,0 +1,24 @@ +// Auto-generated file - DO NOT EDIT +// Generated by @ai-toolkit/utils generate-index +// To regenerate, run: npx nx run @ai-toolkit/skills-agnostic:generate-index + +// Skill types - compatible with Claude Code plugin system +type SkillName = 'codebase-exploration'; + +export type Skills = { + [key in SkillName]: { + description: string; + filePath: string; + }; +}; + +// Export skills with descriptions and file paths +export const skills: Skills = { + 'codebase-exploration': { + description: + 'Use this skill when exploring or analyzing a codebase structure, understanding architecture, or mapping out dependencies between components.', + filePath: './codebase-exploration.md', + }, +} as const; + +export type { SkillName }; diff --git a/packages/skills/agnostic/tsconfig.lib.json b/packages/skills/agnostic/tsconfig.lib.json new file mode 100644 index 00000000..d0fefbca --- /dev/null +++ b/packages/skills/agnostic/tsconfig.lib.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo", + "emitDeclarationOnly": false, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [ + { + "path": "../../utils/tsconfig.lib.json" + } + ] +} diff --git a/packages/utils/src/lib/generate-index.ts b/packages/utils/src/lib/generate-index.ts index d720dd5b..dbdc4fea 100644 --- a/packages/utils/src/lib/generate-index.ts +++ b/packages/utils/src/lib/generate-index.ts @@ -13,9 +13,9 @@ interface ItemInfo { export interface GenerateIndexOptions { srcPath: string; outputPath: string; - typeName: string; // 'Agent' or 'CommandName' - exportName: string; // 'agents' or 'commands' - typeNamePlural?: string; // 'Agents' or 'Commands' + typeName: string; // 'Agent', 'CommandName', or 'SkillName' + exportName: string; // 'agents', 'commands', or 'skills' + typeNamePlural?: string; // 'Agents', 'Commands', or 'Skills' regenerateCommand: string; // The nx command to regenerate } @@ -86,6 +86,7 @@ export async function generateIndex( const items: ItemInfo[] = []; // Determine if we should use filename as name (for commands) + // Skills and agents use 'name' field in frontmatter const useFilenameAsName = typeName === 'CommandName'; for (const file of markdownFiles) { @@ -118,9 +119,10 @@ export async function generateIndex( }) .join(',\n'); - const indexContent = - typeName === 'Agent' - ? `// Auto-generated file - DO NOT EDIT + let indexContent: string; + + if (typeName === 'Agent') { + indexContent = `// Auto-generated file - DO NOT EDIT // Generated by @ai-toolkit/utils generate-index // To regenerate, run: ${regenerateCommand} @@ -140,8 +142,32 @@ export const agents: Agents = { ${itemsObject}, } as const; -export type AgentName = keyof typeof agents;` - : `// Auto-generated file - DO NOT EDIT +export type AgentName = keyof typeof agents;`; + } else if (typeName === 'SkillName') { + indexContent = `// Auto-generated file - DO NOT EDIT +// Generated by @ai-toolkit/utils generate-index +// To regenerate, run: ${regenerateCommand} + +// Skill types - compatible with Claude Code plugin system +type SkillName = +${typeUnion}; + +export type Skills = { + [key in SkillName]: { + description: string; + filePath: string; + }; +}; + +// Export skills with descriptions and file paths +export const skills: Skills = { +${itemsObject}, +} as const; + +export type { SkillName };`; + } else { + // CommandName + indexContent = `// Auto-generated file - DO NOT EDIT // Generated by @ai-toolkit/utils generate-index // To regenerate, run: ${regenerateCommand} @@ -158,6 +184,7 @@ export type Commands = { export const commands: Commands = { ${itemsObject}, } as const;`; + } // Write the generated index.ts await writeFile(outputPath, indexContent, 'utf-8'); @@ -185,8 +212,8 @@ if (import.meta.url === `file://${process.argv[1]}`) { console.error( 'Usage: generate-index [regenerateCommand]' ); - console.error(' type: "Agent" or "CommandName"'); - console.error(' exportName: "agents" or "commands"'); + console.error(' type: "Agent", "CommandName", or "SkillName"'); + console.error(' exportName: "agents", "commands", or "skills"'); process.exit(1); } diff --git a/tsconfig.base.json b/tsconfig.base.json index 96d60ced..1c5a406e 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -20,6 +20,7 @@ "paths": { "@ai-toolkit/agents-agnostic": ["packages/agents/agnostic/src/index.ts"], "@ai-toolkit/commands-agnostic": ["packages/commands/agnostic/src/index.ts"], + "@ai-toolkit/skills-agnostic": ["packages/skills/agnostic/src/index.ts"], "@ai-toolkit/utils": ["packages/utils/src/index.ts"] }, "customConditions": ["@ai-toolkit/source"]