windsurf-skillforge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # 🔨 Skillforge
2
+
3
+ A Windsurf-native CLI tool to install and run AI agent skills from Git repos — like `brew` for AI prompts. All skills and config live inside `.windsurf/` directories.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install globally
9
+ npm install -g skillforge
10
+
11
+ # Or link locally for development
12
+ npm link
13
+
14
+ # Configure your OpenAI API key
15
+ skillforge config set openai-key sk-your-key-here
16
+
17
+ # Install a skills repo (global — available everywhere)
18
+ skillforge install https://github.com/your-org/ai-skills.git
19
+
20
+ # Install a skills repo (local — only this project)
21
+ skillforge install https://github.com/your-org/ai-skills.git --local
22
+
23
+ # Create a skill in current project
24
+ skillforge init my-skill
25
+
26
+ # Create a global skill
27
+ skillforge init my-skill --global
28
+
29
+ # List available skills (merges local + global)
30
+ skillforge list
31
+
32
+ # Run a skill
33
+ skillforge run code-review --file "$(cat myfile.js)"
34
+
35
+ # Search for skills
36
+ skillforge search testing
37
+ ```
38
+
39
+ ## Storage (Windsurf-native)
40
+
41
+ Skillforge stores everything inside `.windsurf/` directories:
42
+
43
+ | Scope | Path | When to use |
44
+ |---|---|---|
45
+ | **Global** | `~/.windsurf/skillforge/` | Skills available across all projects |
46
+ | **Local** | `.windsurf/skillforge/` (in project root) | Skills scoped to this project only |
47
+
48
+ ```
49
+ ~/.windsurf/skillforge/ # Global
50
+ ├── config.json # API keys, default model
51
+ ├── repos/ # Cloned skill repos
52
+ │ └── my-team-skills/
53
+ │ └── skills/*.md
54
+ └── skills/ # Standalone global skills
55
+ └── my-global-skill.md
56
+
57
+ your-project/.windsurf/skillforge/ # Local (per-project)
58
+ ├── repos/ # Project-scoped repos
59
+ └── skills/ # Project-scoped skills
60
+ └── my-local-skill.md
61
+ ```
62
+
63
+ **Local skills take precedence** over global ones with the same name.
64
+
65
+ ### Example Output
66
+
67
+ ```
68
+ $ skillforge list
69
+
70
+ 📋 Installed Skills (2 total)
71
+
72
+ ┌──────────────────┬──────────────────────────────┬──────────┬──────────────────┬────────┬──────────────┐
73
+ │ Skill │ Description │ Model │ Tags │ Scope │ Source │
74
+ ├──────────────────┼──────────────────────────────┼──────────┼──────────────────┼────────┼──────────────┤
75
+ │ test-global-sk… │ Describe what this skill │ gpt-4 │ custom │ global │ global-skil… │
76
+ │ │ does │ │ │ │ │
77
+ ├──────────────────┼──────────────────────────────┼──────────┼──────────────────┼────────┼──────────────┤
78
+ │ test-local-skill │ Describe what this skill │ gpt-4 │ custom │ local │ local-skills │
79
+ │ │ does │ │ │ │ │
80
+ └──────────────────┴──────────────────────────────┴──────────┴──────────────────┴────────┴──────────────┘
81
+ ```
82
+
83
+ ## Commands
84
+
85
+ | Command | Description |
86
+ |---|---|
87
+ | `skillforge install <url>` | Clone a skills repo globally (or `--local`) |
88
+ | `skillforge list` | List all skills (local + global), filter with `--scope local` |
89
+ | `skillforge search <keyword>` | Search skills by name, description, or tags |
90
+ | `skillforge run <skill> [--args]` | Execute a skill (sends prompt to LLM) |
91
+ | `skillforge update` | Pull latest changes from all repos |
92
+ | `skillforge info <skill-name>` | Show details about a skill |
93
+ | `skillforge init [name]` | Create a skill in local `.windsurf/` (or `--global`) |
94
+ | `skillforge config <action> [key] [value]` | Manage configuration |
95
+
96
+ ## Skill Format
97
+
98
+ Skills are Markdown files with YAML frontmatter:
99
+
100
+ ```md
101
+ ---
102
+ name: my-skill
103
+ description: What this skill does
104
+ args:
105
+ - name: input
106
+ description: The primary input
107
+ required: true
108
+ - name: language
109
+ default: auto-detect
110
+ tags: [coding, review]
111
+ model: gpt-4
112
+ ---
113
+
114
+ Your prompt template here. Use {{input}} and {{language}} placeholders.
115
+ ```
116
+
117
+ ## Creating Skills
118
+
119
+ ```bash
120
+ # Create a local skill (in .windsurf/skillforge/skills/)
121
+ skillforge init my-awesome-skill
122
+
123
+ # Create a global skill (in ~/.windsurf/skillforge/skills/)
124
+ skillforge init my-awesome-skill --global
125
+ ```
126
+
127
+ ## Sharing Skills via Git
128
+
129
+ Create a repo with a `skills/` directory containing `.md` files, then anyone can install:
130
+ ```bash
131
+ skillforge install <your-repo-url>
132
+ skillforge install <your-repo-url> --local # project-only
133
+ ```
134
+
135
+ ## Configuration
136
+
137
+ ```bash
138
+ # Set OpenAI API key
139
+ skillforge config set openai-key sk-...
140
+
141
+ # Change default model
142
+ skillforge config set default-model gpt-4o
143
+
144
+ # View all config
145
+ skillforge config list
146
+
147
+ # Get specific value
148
+ skillforge config get default-model
149
+ ```
150
+
151
+ Config is stored at `~/.windsurf/skillforge/config.json`.
152
+
153
+ ## Requirements
154
+
155
+ - Node.js 18+
156
+ - An OpenAI API key (for running skills)
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'module';
4
+ import { program } from 'commander';
5
+ import chalk from 'chalk';
6
+
7
+ import { installCommand } from '../src/commands/install.js';
8
+ import { listCommand } from '../src/commands/list.js';
9
+ import { searchCommand } from '../src/commands/search.js';
10
+ import { runCommand } from '../src/commands/run.js';
11
+ import { updateCommand } from '../src/commands/update.js';
12
+ import { infoCommand } from '../src/commands/info.js';
13
+ import { initCommand } from '../src/commands/init.js';
14
+ import { configCommand } from '../src/commands/config.js';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const pkg = require('../package.json');
18
+
19
+ program
20
+ .name('skillforge')
21
+ .description(chalk.bold('🔨 Skillforge') + ' — Install and run AI agent skills from Git repos')
22
+ .version(pkg.version, '-v, --version');
23
+
24
+ program
25
+ .command('install <repo-url>')
26
+ .description('Install a skills repo from a Git URL')
27
+ .option('-n, --name <name>', 'Custom name for the repo')
28
+ .option('-l, --local', 'Install to local .windsurf/ (project scope)')
29
+ .action(installCommand);
30
+
31
+ program
32
+ .command('list')
33
+ .description('List all installed skills (global + local)')
34
+ .option('-r, --repo <repo>', 'Filter by repo name')
35
+ .option('-s, --scope <scope>', 'Filter by scope: local or global')
36
+ .action(listCommand);
37
+
38
+ program
39
+ .command('search <keyword>')
40
+ .description('Search skills by name, description, or tags')
41
+ .action(searchCommand);
42
+
43
+ program
44
+ .command('run <skill-name>')
45
+ .description('Run an AI skill')
46
+ .option('-m, --model <model>', 'Override the LLM model')
47
+ .allowUnknownOption(true)
48
+ .action(runCommand);
49
+
50
+ program
51
+ .command('update')
52
+ .description('Update all installed skill repos (git pull)')
53
+ .option('-r, --repo <repo>', 'Update a specific repo only')
54
+ .action(updateCommand);
55
+
56
+ program
57
+ .command('info <skill-name>')
58
+ .description('Show details of a specific skill')
59
+ .action(infoCommand);
60
+
61
+ program
62
+ .command('init [name]')
63
+ .description('Create a new skill template (defaults to local .windsurf/)')
64
+ .option('-g, --global', 'Create in global ~/.windsurf/ instead of local')
65
+ .action(initCommand);
66
+
67
+ program
68
+ .command('config <action> [key] [value]')
69
+ .description('Manage configuration (set, get, list, delete)')
70
+ .action(configCommand);
71
+
72
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "windsurf-skillforge",
3
+ "version": "1.0.0",
4
+ "description": "A CLI tool to install and run AI agent skills from Git repos — like brew for AI prompts",
5
+ "type": "module",
6
+ "bin": {
7
+ "skillforge": "bin/skillforge.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "skills/",
13
+ "skillforge.yaml",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "start": "node bin/skillforge.js",
18
+ "test": "node --test test/**/*.test.js"
19
+ },
20
+ "keywords": [
21
+ "cli",
22
+ "ai",
23
+ "skills",
24
+ "prompts",
25
+ "llm",
26
+ "agent"
27
+ ],
28
+ "author": "",
29
+ "license": "MIT",
30
+ "dependencies": {
31
+ "chalk": "^5.3.0",
32
+ "commander": "^12.1.0",
33
+ "conf": "^13.0.1",
34
+ "gray-matter": "^4.0.3",
35
+ "openai": "^4.68.0",
36
+ "ora": "^8.1.0",
37
+ "simple-git": "^3.27.0",
38
+ "cli-table3": "^0.6.5"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ }
43
+ }
@@ -0,0 +1,4 @@
1
+ # Skillforge Skills Repository
2
+ name: skillforge-default-skills
3
+ description: Default AI agent skills bundled with Skillforge
4
+ version: 1.0.0
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: code-review
3
+ description: Reviews code for best practices, bugs, and performance issues
4
+ args:
5
+ - name: file
6
+ description: Path to the file to review
7
+ required: true
8
+ - name: language
9
+ description: Programming language
10
+ default: auto-detect
11
+ - name: focus
12
+ description: What to focus on (bugs, performance, style, security)
13
+ default: all
14
+ tags: [review, quality, bugs]
15
+ model: gpt-4
16
+ ---
17
+
18
+ You are an expert code reviewer with deep knowledge of software engineering best practices.
19
+
20
+ Review the following code thoroughly for:
21
+ - **Bugs and edge cases** — logic errors, off-by-one, null checks
22
+ - **Performance issues** — unnecessary loops, memory leaks, inefficient patterns
23
+ - **Best practices violations** — naming, structure, SOLID principles
24
+ - **Security concerns** — injection, XSS, sensitive data exposure
25
+
26
+ Focus area: {{focus}}
27
+ Language: {{language}}
28
+
29
+ Code to review:
30
+ ```
31
+ {{file}}
32
+ ```
33
+
34
+ Provide your review in this format:
35
+ 1. **Summary** — overall assessment (1-2 sentences)
36
+ 2. **Critical Issues** — bugs or security problems that must be fixed
37
+ 3. **Improvements** — recommended changes for better code quality
38
+ 4. **Positive Highlights** — things done well
39
+
40
+ Be specific with line references and provide corrected code snippets where applicable.
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: explain-code
3
+ description: Explains code in plain English with visual diagrams
4
+ args:
5
+ - name: code
6
+ description: The code to explain
7
+ required: true
8
+ - name: level
9
+ description: Explanation level (beginner, intermediate, expert)
10
+ default: intermediate
11
+ tags: [learning, documentation, explain]
12
+ model: gpt-4
13
+ ---
14
+
15
+ You are a patient and clear technical educator.
16
+
17
+ Explain the following code at a {{level}} level:
18
+
19
+ ```
20
+ {{code}}
21
+ ```
22
+
23
+ Structure your explanation as:
24
+
25
+ 1. **Overview** — What does this code do in one sentence?
26
+ 2. **Step-by-step breakdown** — Walk through the logic line by line
27
+ 3. **Key concepts** — Explain any patterns, algorithms, or techniques used
28
+ 4. **Flow diagram** — Show the execution flow using ASCII art
29
+ 5. **Potential gotchas** — What could trip someone up?
30
+
31
+ Use analogies and simple language where possible.
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: generate-tests
3
+ description: Generates unit tests for a given function or module
4
+ args:
5
+ - name: code
6
+ description: The code to generate tests for
7
+ required: true
8
+ - name: framework
9
+ description: Test framework to use (jest, mocha, vitest, pytest)
10
+ default: jest
11
+ - name: style
12
+ description: Test style (unit, integration, e2e)
13
+ default: unit
14
+ tags: [testing, automation, quality]
15
+ model: gpt-4
16
+ ---
17
+
18
+ You are an expert test engineer. Generate comprehensive {{style}} tests for the following code.
19
+
20
+ Test framework: {{framework}}
21
+
22
+ Code to test:
23
+ ```
24
+ {{code}}
25
+ ```
26
+
27
+ Requirements:
28
+ 1. Cover all public functions/methods
29
+ 2. Include edge cases (empty input, null, boundary values)
30
+ 3. Include both positive and negative test cases
31
+ 4. Use descriptive test names that explain the expected behavior
32
+ 5. Mock external dependencies appropriately
33
+ 6. Aim for high code coverage
34
+
35
+ Output the complete test file ready to run.
@@ -0,0 +1,66 @@
1
+ import chalk from 'chalk';
2
+ import { config } from '../config/store.js';
3
+
4
+ export async function configCommand(action, key, value) {
5
+ switch (action) {
6
+ case 'set':
7
+ if (!key || value === undefined) {
8
+ console.error(chalk.red('\n✗ Usage: skillforge config set <key> <value>\n'));
9
+ console.error(chalk.dim(' Examples:'));
10
+ console.error(chalk.dim(' skillforge config set openai-key sk-...'));
11
+ console.error(chalk.dim(' skillforge config set default-model gpt-4o\n'));
12
+ process.exit(1);
13
+ }
14
+ config.set(key, value);
15
+ // Mask API keys in output
16
+ const displayValue = key.includes('key') ? value.slice(0, 6) + '...' + value.slice(-4) : value;
17
+ console.log(chalk.green(`\n✓ Set "${key}" = "${displayValue}"\n`));
18
+ break;
19
+
20
+ case 'get':
21
+ if (!key) {
22
+ console.error(chalk.red('\n✗ Usage: skillforge config get <key>\n'));
23
+ process.exit(1);
24
+ }
25
+ const val = config.get(key);
26
+ if (val === undefined) {
27
+ console.log(chalk.yellow(`\n"${key}" is not set.\n`));
28
+ } else {
29
+ const display = key.includes('key') && typeof val === 'string'
30
+ ? val.slice(0, 6) + '...' + val.slice(-4)
31
+ : (typeof val === 'object' ? JSON.stringify(val, null, 2) : val);
32
+ console.log(`\n${chalk.bold(key)}: ${display}\n`);
33
+ }
34
+ break;
35
+
36
+ case 'list':
37
+ console.log(chalk.bold('\n⚙️ Configuration\n'));
38
+ const store = config.store;
39
+ for (const [k, v] of Object.entries(store)) {
40
+ if (k === 'repos') {
41
+ const repoNames = Object.keys(v);
42
+ console.log(` ${chalk.cyan(k)}: ${repoNames.length > 0 ? repoNames.join(', ') : chalk.dim('(none)')}`);
43
+ } else if (k.includes('key') && typeof v === 'string' && v.length > 10) {
44
+ console.log(` ${chalk.cyan(k)}: ${v.slice(0, 6)}...${v.slice(-4)}`);
45
+ } else {
46
+ console.log(` ${chalk.cyan(k)}: ${v || chalk.dim('(not set)')}`);
47
+ }
48
+ }
49
+ console.log('');
50
+ break;
51
+
52
+ case 'delete':
53
+ if (!key) {
54
+ console.error(chalk.red('\n✗ Usage: skillforge config delete <key>\n'));
55
+ process.exit(1);
56
+ }
57
+ config.delete(key);
58
+ console.log(chalk.green(`\n✓ Deleted "${key}"\n`));
59
+ break;
60
+
61
+ default:
62
+ console.error(chalk.red(`\n✗ Unknown action "${action}".`));
63
+ console.error(chalk.dim(' Available: set, get, list, delete\n'));
64
+ process.exit(1);
65
+ }
66
+ }
@@ -0,0 +1,51 @@
1
+ import chalk from 'chalk';
2
+ import { findSkill } from '../utils/scanner.js';
3
+
4
+ export async function infoCommand(skillName) {
5
+ const skill = findSkill(skillName);
6
+
7
+ if (!skill) {
8
+ console.error(chalk.red(`\n✗ Skill "${skillName}" not found.`));
9
+ console.error(chalk.dim(' Run: skillforge list to see available skills\n'));
10
+ process.exit(1);
11
+ }
12
+
13
+ console.log('');
14
+ console.log(chalk.bold(`🔨 ${skill.skillName}`));
15
+ console.log(chalk.dim('─'.repeat(50)));
16
+
17
+ if (skill.meta.description) {
18
+ console.log(chalk.white(` Description: ${skill.meta.description}`));
19
+ }
20
+
21
+ console.log(chalk.white(` Model: ${skill.meta.model || 'gpt-4'}`));
22
+ console.log(chalk.white(` Repo: ${skill.repoName}`));
23
+ console.log(chalk.white(` File: ${skill.filePath}`));
24
+
25
+ if (skill.meta.tags && skill.meta.tags.length > 0) {
26
+ console.log(chalk.white(` Tags: ${skill.meta.tags.join(', ')}`));
27
+ }
28
+
29
+ if (skill.meta.args && skill.meta.args.length > 0) {
30
+ console.log(chalk.bold('\n Arguments:'));
31
+ for (const arg of skill.meta.args) {
32
+ const required = arg.required ? chalk.red('(required)') : chalk.dim('(optional)');
33
+ const defaultVal = arg.default ? chalk.dim(` [default: ${arg.default}]`) : '';
34
+ console.log(` --${arg.name} ${arg.description || ''} ${required}${defaultVal}`);
35
+ }
36
+ }
37
+
38
+ console.log(chalk.bold('\n Prompt Preview:'));
39
+ console.log(chalk.dim(' ┌' + '─'.repeat(48) + '┐'));
40
+ const previewLines = skill.prompt.split('\n').slice(0, 8);
41
+ for (const line of previewLines) {
42
+ const truncated = line.length > 46 ? line.substring(0, 43) + '...' : line;
43
+ console.log(chalk.dim(' │ ') + truncated);
44
+ }
45
+ if (skill.prompt.split('\n').length > 8) {
46
+ console.log(chalk.dim(' │ ...'));
47
+ }
48
+ console.log(chalk.dim(' └' + '─'.repeat(48) + '┘'));
49
+
50
+ console.log('');
51
+ }
@@ -0,0 +1,62 @@
1
+ import { writeFileSync, existsSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import chalk from 'chalk';
4
+ import { GLOBAL_SKILLS_DIR, ensureLocalDirs, getLocalDirs } from '../config/store.js';
5
+
6
+ const SKILL_TEMPLATE = `---
7
+ name: {{SKILL_NAME}}
8
+ description: Describe what this skill does
9
+ args:
10
+ - name: input
11
+ description: The primary input for this skill
12
+ required: true
13
+ - name: language
14
+ description: Programming language context
15
+ default: auto-detect
16
+ tags: [custom]
17
+ model: gpt-4
18
+ ---
19
+
20
+ You are an expert assistant. Follow these instructions carefully.
21
+
22
+ Input: {{input}}
23
+ Language: {{language}}
24
+
25
+ Please provide a thorough and helpful response.
26
+ `;
27
+
28
+ export async function initCommand(name, options) {
29
+ const skillName = name || 'my-skill';
30
+ const isLocal = !options.global; // default to local (in project .windsurf/)
31
+
32
+ let skillsDir;
33
+ if (isLocal) {
34
+ const local = ensureLocalDirs();
35
+ skillsDir = local.SKILLS_DIR;
36
+ } else {
37
+ skillsDir = GLOBAL_SKILLS_DIR;
38
+ }
39
+
40
+ if (!existsSync(skillsDir)) {
41
+ mkdirSync(skillsDir, { recursive: true });
42
+ }
43
+
44
+ const filePath = join(skillsDir, `${skillName}.md`);
45
+ const scope = isLocal ? 'local' : 'global';
46
+
47
+ if (existsSync(filePath)) {
48
+ console.error(chalk.red(`\n✗ File already exists: ${filePath}\n`));
49
+ process.exit(1);
50
+ }
51
+
52
+ const content = SKILL_TEMPLATE.replace('{{SKILL_NAME}}', skillName);
53
+ writeFileSync(filePath, content, 'utf-8');
54
+
55
+ console.log(chalk.green(`\n✓ Created skill template (${scope}): ${filePath}`));
56
+ console.log(chalk.dim(' Edit the file to customize your skill.'));
57
+ if (isLocal) {
58
+ console.log(chalk.dim(' This skill is available in the current project.\n'));
59
+ } else {
60
+ console.log(chalk.dim(' This skill is available globally across all projects.\n'));
61
+ }
62
+ }
@@ -0,0 +1,58 @@
1
+ import { join, basename } from 'path';
2
+ import { existsSync } from 'fs';
3
+ import simpleGit from 'simple-git';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { config, GLOBAL_REPOS_DIR, ensureLocalDirs, getLocalDirs } from '../config/store.js';
7
+
8
+ export async function installCommand(repoUrl, options) {
9
+ // Derive repo name from URL or use custom name
10
+ let repoName = options.name;
11
+ if (!repoName) {
12
+ repoName = basename(repoUrl, '.git').replace(/[^a-zA-Z0-9_-]/g, '-');
13
+ }
14
+
15
+ // Determine scope: --local installs to .windsurf/skillforge/repos/ in cwd
16
+ const isLocal = options.local || false;
17
+ let reposDir;
18
+
19
+ if (isLocal) {
20
+ const local = ensureLocalDirs();
21
+ reposDir = local.REPOS_DIR;
22
+ } else {
23
+ reposDir = GLOBAL_REPOS_DIR;
24
+ }
25
+
26
+ const repoPath = join(reposDir, repoName);
27
+ const scope = isLocal ? 'local' : 'global';
28
+
29
+ if (existsSync(repoPath)) {
30
+ console.log(chalk.yellow(`\n⚠ Repo "${repoName}" already installed (${scope}) at ${repoPath}`));
31
+ console.log(chalk.dim(' Run: skillforge update to pull latest changes\n'));
32
+ return;
33
+ }
34
+
35
+ const spinner = ora(`Cloning ${repoUrl} (${scope})...`).start();
36
+
37
+ try {
38
+ const git = simpleGit();
39
+ await git.clone(repoUrl, repoPath);
40
+
41
+ // Register in config
42
+ const repos = config.get('repos') || {};
43
+ repos[repoName] = {
44
+ url: repoUrl,
45
+ path: repoPath,
46
+ scope,
47
+ installedAt: new Date().toISOString()
48
+ };
49
+ config.set('repos', repos);
50
+
51
+ spinner.succeed(chalk.green(`Installed "${repoName}" (${scope}) successfully`));
52
+ console.log(chalk.dim(` Path: ${repoPath}`));
53
+ console.log(chalk.dim(' Run: skillforge list to see available skills\n'));
54
+ } catch (err) {
55
+ spinner.fail(chalk.red(`Failed to clone repo: ${err.message}`));
56
+ process.exit(1);
57
+ }
58
+ }
@@ -0,0 +1,64 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { scanAllSkills, scanRepoSkills } from '../utils/scanner.js';
4
+ import { config } from '../config/store.js';
5
+
6
+ export async function listCommand(options) {
7
+ let skills;
8
+
9
+ if (options.repo) {
10
+ const repos = config.get('repos') || {};
11
+ const repoInfo = repos[options.repo];
12
+ if (!repoInfo) {
13
+ console.log(chalk.red(`\n✗ Repo "${options.repo}" not found.\n`));
14
+ return;
15
+ }
16
+ skills = scanRepoSkills(options.repo, repoInfo.path, repoInfo.scope || 'global');
17
+ } else {
18
+ skills = scanAllSkills();
19
+ }
20
+
21
+ // Filter by scope if requested
22
+ if (options.scope) {
23
+ skills = skills.filter(s => s.scope === options.scope);
24
+ }
25
+
26
+ if (skills.length === 0) {
27
+ console.log(chalk.yellow('\nNo skills found.'));
28
+ console.log(chalk.dim(' Install a skills repo: skillforge install <repo-url>'));
29
+ console.log(chalk.dim(' Or create one: skillforge init my-skill\n'));
30
+ return;
31
+ }
32
+
33
+ const table = new Table({
34
+ head: [
35
+ chalk.cyan('Skill'),
36
+ chalk.cyan('Description'),
37
+ chalk.cyan('Model'),
38
+ chalk.cyan('Tags'),
39
+ chalk.cyan('Scope'),
40
+ chalk.cyan('Source')
41
+ ],
42
+ colWidths: [18, 30, 10, 18, 8, 14],
43
+ wordWrap: true
44
+ });
45
+
46
+ for (const skill of skills) {
47
+ const scopeLabel = skill.scope === 'local'
48
+ ? chalk.green('local')
49
+ : chalk.blue('global');
50
+
51
+ table.push([
52
+ chalk.white.bold(skill.skillName),
53
+ skill.meta.description || chalk.dim('—'),
54
+ chalk.dim(skill.meta.model || 'gpt-4'),
55
+ (skill.meta.tags || []).join(', ') || chalk.dim('—'),
56
+ scopeLabel,
57
+ chalk.dim(skill.repoName)
58
+ ]);
59
+ }
60
+
61
+ console.log(`\n${chalk.bold('📋 Installed Skills')} (${skills.length} total)\n`);
62
+ console.log(table.toString());
63
+ console.log('');
64
+ }
@@ -0,0 +1,69 @@
1
+ import chalk from 'chalk';
2
+ import { findSkill } from '../utils/scanner.js';
3
+ import { renderPrompt } from '../engine/renderer.js';
4
+ import { executeSkill } from '../engine/runner.js';
5
+ import { config } from '../config/store.js';
6
+
7
+ export async function runCommand(skillName, options, command) {
8
+ const skill = findSkill(skillName);
9
+
10
+ if (!skill) {
11
+ console.error(chalk.red(`\n✗ Skill "${skillName}" not found.`));
12
+ console.error(chalk.dim(' Run: skillforge list to see available skills\n'));
13
+ process.exit(1);
14
+ }
15
+
16
+ // Parse extra args from command line (--file=test.js or --file test.js)
17
+ const userArgs = {};
18
+ const rawArgs = command.parent.args.slice(1); // skip skill name
19
+ for (let i = 0; i < rawArgs.length; i++) {
20
+ const arg = rawArgs[i];
21
+ if (arg.startsWith('--')) {
22
+ const key = arg.replace(/^--/, '');
23
+ if (key.includes('=')) {
24
+ const [k, v] = key.split('=');
25
+ userArgs[k] = v;
26
+ } else if (i + 1 < rawArgs.length && !rawArgs[i + 1].startsWith('--')) {
27
+ userArgs[key] = rawArgs[i + 1];
28
+ i++;
29
+ } else {
30
+ userArgs[key] = 'true';
31
+ }
32
+ }
33
+ }
34
+
35
+ // Check required args
36
+ const requiredArgs = (skill.meta.args || []).filter(a => a.required);
37
+ const missingArgs = requiredArgs.filter(a => !userArgs[a.name]);
38
+
39
+ if (missingArgs.length > 0) {
40
+ console.error(chalk.red(`\n✗ Missing required arguments:`));
41
+ for (const arg of missingArgs) {
42
+ console.error(chalk.yellow(` --${arg.name} ${arg.description || ''}`));
43
+ }
44
+ console.error('');
45
+ process.exit(1);
46
+ }
47
+
48
+ // Fill defaults
49
+ for (const argDef of skill.meta.args || []) {
50
+ if (userArgs[argDef.name] === undefined && argDef.default !== undefined) {
51
+ userArgs[argDef.name] = argDef.default;
52
+ }
53
+ }
54
+
55
+ // Render prompt
56
+ const renderedPrompt = renderPrompt(skill.prompt, userArgs);
57
+
58
+ // Determine model
59
+ const model = options.model || skill.meta.model || config.get('default-model') || 'gpt-4';
60
+
61
+ console.log(chalk.bold(`\n🔨 Running skill: ${skill.skillName}`));
62
+ console.log(chalk.dim(` From repo: ${skill.repoName}`));
63
+
64
+ if (Object.keys(userArgs).length > 0) {
65
+ console.log(chalk.dim(` Args: ${JSON.stringify(userArgs)}`));
66
+ }
67
+
68
+ await executeSkill(renderedPrompt, model, skill.meta);
69
+ }
@@ -0,0 +1,44 @@
1
+ import chalk from 'chalk';
2
+ import Table from 'cli-table3';
3
+ import { scanAllSkills } from '../utils/scanner.js';
4
+
5
+ export async function searchCommand(keyword) {
6
+ const allSkills = scanAllSkills();
7
+ const query = keyword.toLowerCase();
8
+
9
+ const results = allSkills.filter(skill => {
10
+ const nameMatch = skill.skillName.toLowerCase().includes(query);
11
+ const descMatch = (skill.meta.description || '').toLowerCase().includes(query);
12
+ const tagMatch = (skill.meta.tags || []).some(t => t.toLowerCase().includes(query));
13
+ return nameMatch || descMatch || tagMatch;
14
+ });
15
+
16
+ if (results.length === 0) {
17
+ console.log(chalk.yellow(`\nNo skills matching "${keyword}".\n`));
18
+ return;
19
+ }
20
+
21
+ const table = new Table({
22
+ head: [
23
+ chalk.cyan('Skill'),
24
+ chalk.cyan('Description'),
25
+ chalk.cyan('Tags'),
26
+ chalk.cyan('Repo')
27
+ ],
28
+ colWidths: [20, 40, 20, 15],
29
+ wordWrap: true
30
+ });
31
+
32
+ for (const skill of results) {
33
+ table.push([
34
+ chalk.white.bold(skill.skillName),
35
+ skill.meta.description || chalk.dim('—'),
36
+ (skill.meta.tags || []).join(', ') || chalk.dim('—'),
37
+ chalk.dim(skill.repoName)
38
+ ]);
39
+ }
40
+
41
+ console.log(`\n${chalk.bold('🔍 Search Results')} for "${keyword}" (${results.length} found)\n`);
42
+ console.log(table.toString());
43
+ console.log('');
44
+ }
@@ -0,0 +1,46 @@
1
+ import simpleGit from 'simple-git';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { config } from '../config/store.js';
5
+
6
+ export async function updateCommand(options) {
7
+ const repos = config.get('repos') || {};
8
+ const repoEntries = Object.entries(repos);
9
+
10
+ if (repoEntries.length === 0) {
11
+ console.log(chalk.yellow('\nNo repos installed.'));
12
+ console.log(chalk.dim(' Run: skillforge install <repo-url>\n'));
13
+ return;
14
+ }
15
+
16
+ // Filter to specific repo if requested
17
+ const toUpdate = options.repo
18
+ ? repoEntries.filter(([name]) => name === options.repo)
19
+ : repoEntries;
20
+
21
+ if (toUpdate.length === 0) {
22
+ console.log(chalk.red(`\n✗ Repo "${options.repo}" not found.\n`));
23
+ return;
24
+ }
25
+
26
+ console.log(chalk.bold(`\n🔄 Updating ${toUpdate.length} repo(s)...\n`));
27
+
28
+ for (const [repoName, repoInfo] of toUpdate) {
29
+ const spinner = ora(`Updating "${repoName}"...`).start();
30
+
31
+ try {
32
+ const git = simpleGit(repoInfo.path);
33
+ const result = await git.pull();
34
+
35
+ if (result.summary.changes > 0) {
36
+ spinner.succeed(chalk.green(`"${repoName}" updated (${result.summary.changes} changes)`));
37
+ } else {
38
+ spinner.succeed(chalk.dim(`"${repoName}" already up to date`));
39
+ }
40
+ } catch (err) {
41
+ spinner.fail(chalk.red(`"${repoName}" failed: ${err.message}`));
42
+ }
43
+ }
44
+
45
+ console.log('');
46
+ }
@@ -0,0 +1,57 @@
1
+ import Conf from 'conf';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import { mkdirSync, existsSync } from 'fs';
5
+
6
+ // Global: ~/.windsurf/skillforge/
7
+ const GLOBAL_WINDSURF_DIR = join(homedir(), '.windsurf');
8
+ const GLOBAL_SKILLFORGE_DIR = join(GLOBAL_WINDSURF_DIR, 'skillforge');
9
+ const GLOBAL_REPOS_DIR = join(GLOBAL_SKILLFORGE_DIR, 'repos');
10
+ const GLOBAL_SKILLS_DIR = join(GLOBAL_SKILLFORGE_DIR, 'skills');
11
+
12
+ // Ensure global directories exist
13
+ for (const dir of [GLOBAL_WINDSURF_DIR, GLOBAL_SKILLFORGE_DIR, GLOBAL_REPOS_DIR, GLOBAL_SKILLS_DIR]) {
14
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
15
+ }
16
+
17
+ // Local: .windsurf/skillforge/ (relative to cwd)
18
+ function getLocalDirs() {
19
+ const localWindsurf = join(process.cwd(), '.windsurf');
20
+ const localSkillforge = join(localWindsurf, 'skillforge');
21
+ const localRepos = join(localSkillforge, 'repos');
22
+ const localSkills = join(localSkillforge, 'skills');
23
+ return {
24
+ WINDSURF_DIR: localWindsurf,
25
+ SKILLFORGE_DIR: localSkillforge,
26
+ REPOS_DIR: localRepos,
27
+ SKILLS_DIR: localSkills
28
+ };
29
+ }
30
+
31
+ function ensureLocalDirs() {
32
+ const local = getLocalDirs();
33
+ for (const dir of [local.WINDSURF_DIR, local.SKILLFORGE_DIR, local.REPOS_DIR, local.SKILLS_DIR]) {
34
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
35
+ }
36
+ return local;
37
+ }
38
+
39
+ const config = new Conf({
40
+ projectName: 'skillforge',
41
+ cwd: GLOBAL_SKILLFORGE_DIR,
42
+ defaults: {
43
+ 'openai-key': '',
44
+ 'default-model': 'gpt-4',
45
+ 'repos': {}
46
+ }
47
+ });
48
+
49
+ export {
50
+ config,
51
+ GLOBAL_WINDSURF_DIR,
52
+ GLOBAL_SKILLFORGE_DIR,
53
+ GLOBAL_REPOS_DIR,
54
+ GLOBAL_SKILLS_DIR,
55
+ getLocalDirs,
56
+ ensureLocalDirs
57
+ };
@@ -0,0 +1,43 @@
1
+ import matter from 'gray-matter';
2
+ import { readFileSync } from 'fs';
3
+
4
+ /**
5
+ * Parse a skill markdown file into structured data.
6
+ * Returns { meta, prompt } where meta is the YAML frontmatter
7
+ * and prompt is the markdown body.
8
+ */
9
+ export function parseSkillFile(filePath) {
10
+ const raw = readFileSync(filePath, 'utf-8');
11
+ const { data, content } = matter(raw);
12
+
13
+ return {
14
+ meta: {
15
+ name: data.name || '',
16
+ description: data.description || '',
17
+ args: data.args || [],
18
+ tags: data.tags || [],
19
+ model: data.model || 'gpt-4',
20
+ ...data
21
+ },
22
+ prompt: content.trim()
23
+ };
24
+ }
25
+
26
+ /**
27
+ * Parse skill content from a string (not file).
28
+ */
29
+ export function parseSkillContent(rawContent) {
30
+ const { data, content } = matter(rawContent);
31
+
32
+ return {
33
+ meta: {
34
+ name: data.name || '',
35
+ description: data.description || '',
36
+ args: data.args || [],
37
+ tags: data.tags || [],
38
+ model: data.model || 'gpt-4',
39
+ ...data
40
+ },
41
+ prompt: content.trim()
42
+ };
43
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Replace {{arg}} placeholders in the prompt with user-provided values.
3
+ * Also supports {{arg | default_value}} syntax.
4
+ */
5
+ export function renderPrompt(promptTemplate, args) {
6
+ let rendered = promptTemplate;
7
+
8
+ // Replace {{argName}} and {{ argName | default }} patterns
9
+ rendered = rendered.replace(/\{\{\s*(\w+)\s*(?:\|\s*([^}]*))?\s*\}\}/g, (match, argName, defaultVal) => {
10
+ if (args[argName] !== undefined && args[argName] !== null) {
11
+ return String(args[argName]);
12
+ }
13
+ if (defaultVal !== undefined) {
14
+ return defaultVal.trim();
15
+ }
16
+ return match; // Leave unresolved placeholders as-is
17
+ });
18
+
19
+ return rendered;
20
+ }
21
+
22
+ /**
23
+ * Extract all placeholder names from a prompt template.
24
+ */
25
+ export function extractPlaceholders(promptTemplate) {
26
+ const placeholders = [];
27
+ const regex = /\{\{\s*(\w+)\s*(?:\|\s*[^}]*)?\s*\}\}/g;
28
+ let match;
29
+ while ((match = regex.exec(promptTemplate)) !== null) {
30
+ if (!placeholders.includes(match[1])) {
31
+ placeholders.push(match[1]);
32
+ }
33
+ }
34
+ return placeholders;
35
+ }
@@ -0,0 +1,54 @@
1
+ import OpenAI from 'openai';
2
+ import chalk from 'chalk';
3
+ import { config } from '../config/store.js';
4
+
5
+ /**
6
+ * Send a rendered prompt to the LLM and stream the response to stdout.
7
+ */
8
+ export async function executeSkill(renderedPrompt, model, skillMeta) {
9
+ const apiKey = config.get('openai-key');
10
+
11
+ if (!apiKey) {
12
+ console.error(chalk.red('\n✗ OpenAI API key not configured.'));
13
+ console.error(chalk.yellow(' Run: skillforge config set openai-key <your-key>\n'));
14
+ process.exit(1);
15
+ }
16
+
17
+ const openai = new OpenAI({ apiKey });
18
+
19
+ const systemMessage = skillMeta.system
20
+ ? skillMeta.system
21
+ : 'You are a helpful AI assistant executing a skill. Follow the instructions precisely.';
22
+
23
+ console.log(chalk.dim(`\nUsing model: ${model}\n`));
24
+ console.log(chalk.dim('─'.repeat(60)));
25
+
26
+ try {
27
+ const stream = await openai.chat.completions.create({
28
+ model: model,
29
+ messages: [
30
+ { role: 'system', content: systemMessage },
31
+ { role: 'user', content: renderedPrompt }
32
+ ],
33
+ stream: true
34
+ });
35
+
36
+ for await (const chunk of stream) {
37
+ const content = chunk.choices[0]?.delta?.content;
38
+ if (content) {
39
+ process.stdout.write(content);
40
+ }
41
+ }
42
+
43
+ console.log('\n' + chalk.dim('─'.repeat(60)));
44
+ console.log(chalk.green('\n✓ Skill execution complete.\n'));
45
+ } catch (err) {
46
+ if (err.code === 'invalid_api_key') {
47
+ console.error(chalk.red('\n✗ Invalid OpenAI API key.'));
48
+ console.error(chalk.yellow(' Run: skillforge config set openai-key <your-key>\n'));
49
+ } else {
50
+ console.error(chalk.red(`\n✗ LLM Error: ${err.message}\n`));
51
+ }
52
+ process.exit(1);
53
+ }
54
+ }
@@ -0,0 +1,145 @@
1
+ import { readdirSync, statSync, existsSync } from 'fs';
2
+ import { join, extname, basename } from 'path';
3
+ import { config, GLOBAL_SKILLS_DIR, GLOBAL_REPOS_DIR, getLocalDirs } from '../config/store.js';
4
+ import { parseSkillFile } from '../engine/parser.js';
5
+
6
+ /**
7
+ * Scan all installed repos + standalone skill directories (global & local).
8
+ * Returns an array of { repoName, filePath, meta, prompt, scope }
9
+ */
10
+ export function scanAllSkills() {
11
+ const skills = [];
12
+
13
+ // 1. Scan registered repos from config
14
+ const repos = config.get('repos') || {};
15
+ for (const [repoName, repoInfo] of Object.entries(repos)) {
16
+ const repoSkills = scanRepoSkills(repoName, repoInfo.path, repoInfo.scope || 'global');
17
+ skills.push(...repoSkills);
18
+ }
19
+
20
+ // 2. Scan global standalone skills (~/.windsurf/skillforge/skills/)
21
+ const globalStandalone = scanDirectory(GLOBAL_SKILLS_DIR, 'global-skills', 'global');
22
+ skills.push(...globalStandalone);
23
+
24
+ // 3. Scan local standalone skills (.windsurf/skillforge/skills/ in cwd)
25
+ const local = getLocalDirs();
26
+ if (existsSync(local.SKILLS_DIR)) {
27
+ const localStandalone = scanDirectory(local.SKILLS_DIR, 'local-skills', 'local');
28
+ skills.push(...localStandalone);
29
+ }
30
+
31
+ // 4. Scan local repos (.windsurf/skillforge/repos/ in cwd)
32
+ if (existsSync(local.REPOS_DIR)) {
33
+ try {
34
+ const localRepoDirs = readdirSync(local.REPOS_DIR);
35
+ for (const dir of localRepoDirs) {
36
+ const dirPath = join(local.REPOS_DIR, dir);
37
+ if (statSync(dirPath).isDirectory()) {
38
+ const repoSkills = scanRepoSkills(dir, dirPath, 'local');
39
+ skills.push(...repoSkills);
40
+ }
41
+ }
42
+ } catch {
43
+ // skip
44
+ }
45
+ }
46
+
47
+ return skills;
48
+ }
49
+
50
+ /**
51
+ * Scan a single repo for skill files.
52
+ */
53
+ export function scanRepoSkills(repoName, repoPath, scope = 'global') {
54
+ const skills = [];
55
+ const skillsDir = join(repoPath, 'skills');
56
+
57
+ // Look for .md files in skills/ directory, or root
58
+ const searchDirs = [skillsDir, repoPath];
59
+
60
+ for (const dir of searchDirs) {
61
+ try {
62
+ const files = readdirSync(dir);
63
+ for (const file of files) {
64
+ if (extname(file) !== '.md') continue;
65
+ if (file.toLowerCase() === 'readme.md') continue;
66
+
67
+ const filePath = join(dir, file);
68
+ if (!statSync(filePath).isFile()) continue;
69
+
70
+ try {
71
+ const { meta, prompt } = parseSkillFile(filePath);
72
+ if (meta.name || meta.description) {
73
+ skills.push({
74
+ repoName,
75
+ filePath,
76
+ skillName: meta.name || basename(file, '.md'),
77
+ meta,
78
+ prompt,
79
+ scope
80
+ });
81
+ }
82
+ } catch {
83
+ // Skip files that can't be parsed
84
+ }
85
+ }
86
+ } catch {
87
+ // Directory doesn't exist, skip
88
+ }
89
+ }
90
+
91
+ return skills;
92
+ }
93
+
94
+ /**
95
+ * Scan a standalone skills directory for .md files.
96
+ */
97
+ function scanDirectory(dirPath, sourceName, scope) {
98
+ const skills = [];
99
+ try {
100
+ const files = readdirSync(dirPath);
101
+ for (const file of files) {
102
+ if (extname(file) !== '.md') continue;
103
+ if (file.toLowerCase() === 'readme.md') continue;
104
+
105
+ const filePath = join(dirPath, file);
106
+ if (!statSync(filePath).isFile()) continue;
107
+
108
+ try {
109
+ const { meta, prompt } = parseSkillFile(filePath);
110
+ if (meta.name || meta.description) {
111
+ skills.push({
112
+ repoName: sourceName,
113
+ filePath,
114
+ skillName: meta.name || basename(file, '.md'),
115
+ meta,
116
+ prompt,
117
+ scope
118
+ });
119
+ }
120
+ } catch {
121
+ // Skip
122
+ }
123
+ }
124
+ } catch {
125
+ // Directory doesn't exist
126
+ }
127
+ return skills;
128
+ }
129
+
130
+ /**
131
+ * Find a specific skill by name across all repos.
132
+ * Local skills take precedence over global ones.
133
+ */
134
+ export function findSkill(skillName) {
135
+ const allSkills = scanAllSkills();
136
+ // Prefer local over global
137
+ const local = allSkills.find(
138
+ s => (s.skillName === skillName || s.meta.name === skillName) && s.scope === 'local'
139
+ );
140
+ if (local) return local;
141
+
142
+ return allSkills.find(
143
+ s => s.skillName === skillName || s.meta.name === skillName
144
+ );
145
+ }