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 +156 -0
- package/bin/skillforge.js +72 -0
- package/package.json +43 -0
- package/skillforge.yaml +4 -0
- package/skills/code-review.md +40 -0
- package/skills/explain-code.md +31 -0
- package/skills/generate-tests.md +35 -0
- package/src/commands/config.js +66 -0
- package/src/commands/info.js +51 -0
- package/src/commands/init.js +62 -0
- package/src/commands/install.js +58 -0
- package/src/commands/list.js +64 -0
- package/src/commands/run.js +69 -0
- package/src/commands/search.js +44 -0
- package/src/commands/update.js +46 -0
- package/src/config/store.js +57 -0
- package/src/engine/parser.js +43 -0
- package/src/engine/renderer.js +35 -0
- package/src/engine/runner.js +54 -0
- package/src/utils/scanner.js +145 -0
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
|
+
}
|
package/skillforge.yaml
ADDED
|
@@ -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
|
+
}
|