skillbox 0.1.1
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/LICENSE +21 -0
- package/README.md +172 -0
- package/dist/cli.js +25 -0
- package/dist/commands/add.js +105 -0
- package/dist/commands/agent.js +33 -0
- package/dist/commands/config.js +55 -0
- package/dist/commands/convert.js +64 -0
- package/dist/commands/import.js +50 -0
- package/dist/commands/list.js +95 -0
- package/dist/commands/meta.js +60 -0
- package/dist/commands/project.js +174 -0
- package/dist/commands/status.js +129 -0
- package/dist/commands/update.js +84 -0
- package/dist/lib/agents.js +44 -0
- package/dist/lib/command.js +9 -0
- package/dist/lib/config.js +27 -0
- package/dist/lib/fetcher.js +8 -0
- package/dist/lib/grouping.js +34 -0
- package/dist/lib/index.js +42 -0
- package/dist/lib/installs.js +36 -0
- package/dist/lib/options.js +21 -0
- package/dist/lib/output.js +26 -0
- package/dist/lib/paths.js +6 -0
- package/dist/lib/project-paths.js +18 -0
- package/dist/lib/project-root.js +25 -0
- package/dist/lib/projects.js +31 -0
- package/dist/lib/runtime.js +24 -0
- package/dist/lib/skill-parser.js +61 -0
- package/dist/lib/skill-store.js +28 -0
- package/dist/lib/sync.js +43 -0
- package/dist/lib/types.js +1 -0
- package/package.json +51 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Christian Anagnostou
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# skillbox
|
|
2
|
+
|
|
3
|
+
Local-first, agent-agnostic skills manager. Track, update, and sync skills across popular AI coding agents with one CLI.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### npm (recommended)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g skillbox
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
TODO: Publish initial npm release
|
|
14
|
+
|
|
15
|
+
### From Source
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/christiananagnostou/skillbox
|
|
19
|
+
cd skillbox
|
|
20
|
+
npm install
|
|
21
|
+
npm run build
|
|
22
|
+
npm link --global
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Homebrew
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
brew tap christiananagnostou/skillbox
|
|
29
|
+
brew install skillbox
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
TODO: Homebrew formula publishing
|
|
33
|
+
|
|
34
|
+
## CI
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm run lint:ci
|
|
38
|
+
npm run format:check
|
|
39
|
+
npm run build
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
TODO: Publish GitHub Actions badge
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
skillbox add https://example.com/skills/linting/SKILL.md
|
|
48
|
+
skillbox list
|
|
49
|
+
skillbox status
|
|
50
|
+
skillbox update linting
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
### Core Commands
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
skillbox add <url> [--name <name>] [--global] [--agents ...]
|
|
59
|
+
skillbox convert <url> [--name <name>] [--output <dir>] [--agent]
|
|
60
|
+
skillbox list [--group=category|namespace|source|project] [--json]
|
|
61
|
+
skillbox status [--group=project|source] [--json]
|
|
62
|
+
skillbox update [name] [--system] [--project <path>]
|
|
63
|
+
skillbox import <path>
|
|
64
|
+
skillbox meta set <name> --category foo --tag bar --namespace baz
|
|
65
|
+
skillbox agent
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Project Commands
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
skillbox project add <path> [--agent-path agent=path]
|
|
72
|
+
skillbox project list
|
|
73
|
+
skillbox project inspect <path>
|
|
74
|
+
skillbox project sync <path>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Config
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
skillbox config get
|
|
81
|
+
skillbox config set --default-scope user
|
|
82
|
+
skillbox config set --default-agent claude --default-agent cursor
|
|
83
|
+
skillbox config set --manage-system
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
TODO: Config option reference (defaults, values, validation)
|
|
87
|
+
|
|
88
|
+
## Agent Mode
|
|
89
|
+
|
|
90
|
+
Use `--json` for machine-readable output.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
skillbox list --json
|
|
94
|
+
skillbox status --json
|
|
95
|
+
skillbox update linting --json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Agent Usage Snippet
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
Use skillbox for skill management.
|
|
102
|
+
|
|
103
|
+
Common workflow:
|
|
104
|
+
1) skillbox list --json
|
|
105
|
+
2) skillbox status --json
|
|
106
|
+
3) skillbox update <name> --json
|
|
107
|
+
|
|
108
|
+
If you need to install a new skill from a URL, run:
|
|
109
|
+
skillbox add <url> [--name <name>]
|
|
110
|
+
|
|
111
|
+
If a URL is not a valid skill, run:
|
|
112
|
+
skillbox convert <url> --agent
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Skill Locations
|
|
116
|
+
|
|
117
|
+
Skillbox maintains a canonical store and syncs into agent-native folders.
|
|
118
|
+
|
|
119
|
+
Canonical store:
|
|
120
|
+
|
|
121
|
+
- `~/.config/skillbox/skills/<name>/`
|
|
122
|
+
|
|
123
|
+
Index + config:
|
|
124
|
+
|
|
125
|
+
- `~/.config/skillbox/index.json`
|
|
126
|
+
- `~/.config/skillbox/projects.json`
|
|
127
|
+
- `~/.config/skillbox/config.json`
|
|
128
|
+
|
|
129
|
+
Agent paths (default):
|
|
130
|
+
|
|
131
|
+
- OpenCode: `.opencode/skills/`, `~/.config/opencode/skills/` (Claude-compatible `.claude/skills/` also supported)
|
|
132
|
+
- Claude: `.claude/skills/`, `~/.claude/skills/`
|
|
133
|
+
- Cursor: `.cursor/skills/`, `.claude/skills/`, `~/.cursor/skills/`, `~/.claude/skills/`
|
|
134
|
+
- Codex: `$REPO_ROOT/.codex/skills/`, `~/.codex/skills/`, `/etc/codex/skills` (system)
|
|
135
|
+
- Amp: `.agents/skills/`, `~/.config/agents/skills/` (Claude-compatible `.claude/skills/` also supported)
|
|
136
|
+
- Antigravity: `.agent/skills/`, `~/.gemini/antigravity/skills/`
|
|
137
|
+
|
|
138
|
+
TODO: Validate agent path list against upstream docs before release
|
|
139
|
+
|
|
140
|
+
## Usage with AI Agents
|
|
141
|
+
|
|
142
|
+
### Just ask the agent
|
|
143
|
+
|
|
144
|
+
The simplest approach is to instruct your agent to use Skillbox:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
Use skillbox to manage skills for this repo. Run skillbox --help for all commands.
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### AGENTS.md / CLAUDE.md
|
|
151
|
+
|
|
152
|
+
Add this to your project instructions for more consistent results:
|
|
153
|
+
|
|
154
|
+
```markdown
|
|
155
|
+
## Skills
|
|
156
|
+
|
|
157
|
+
Use `skillbox` to manage skills. Run `skillbox --help` for all commands.
|
|
158
|
+
|
|
159
|
+
Core workflow:
|
|
160
|
+
|
|
161
|
+
1. `skillbox list --json`
|
|
162
|
+
2. `skillbox status --json`
|
|
163
|
+
3. `skillbox update <name> --json`
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Claude Code Skill
|
|
167
|
+
|
|
168
|
+
TODO: Provide a Skillbox skill for Claude Code (SKILL.md)
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerAdd } from "./commands/add.js";
|
|
4
|
+
import { registerConvert } from "./commands/convert.js";
|
|
5
|
+
import { registerList } from "./commands/list.js";
|
|
6
|
+
import { registerStatus } from "./commands/status.js";
|
|
7
|
+
import { registerUpdate } from "./commands/update.js";
|
|
8
|
+
import { registerImport } from "./commands/import.js";
|
|
9
|
+
import { registerMeta } from "./commands/meta.js";
|
|
10
|
+
import { registerProject } from "./commands/project.js";
|
|
11
|
+
import { registerAgent } from "./commands/agent.js";
|
|
12
|
+
import { registerConfig } from "./commands/config.js";
|
|
13
|
+
const program = new Command();
|
|
14
|
+
program.name("skillbox").description("Local-first, agent-agnostic skills manager").version("0.1.0");
|
|
15
|
+
registerAdd(program);
|
|
16
|
+
registerConvert(program);
|
|
17
|
+
registerList(program);
|
|
18
|
+
registerStatus(program);
|
|
19
|
+
registerUpdate(program);
|
|
20
|
+
registerImport(program);
|
|
21
|
+
registerMeta(program);
|
|
22
|
+
registerProject(program);
|
|
23
|
+
registerAgent(program);
|
|
24
|
+
registerConfig(program);
|
|
25
|
+
program.parse();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
2
|
+
import { fetchText } from "../lib/fetcher.js";
|
|
3
|
+
import { parseSkillMarkdown, inferNameFromUrl, buildMetadata } from "../lib/skill-parser.js";
|
|
4
|
+
import { handleCommandError } from "../lib/command.js";
|
|
5
|
+
import { ensureSkillsDir, writeSkillFiles } from "../lib/skill-store.js";
|
|
6
|
+
import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
|
|
7
|
+
import { buildTargets, copySkillToTargets } from "../lib/sync.js";
|
|
8
|
+
import { buildProjectAgentPaths } from "../lib/project-paths.js";
|
|
9
|
+
import { resolveRuntime, ensureProjectRegistered } from "../lib/runtime.js";
|
|
10
|
+
export const registerAdd = (program) => {
|
|
11
|
+
program
|
|
12
|
+
.command("add")
|
|
13
|
+
.argument("<url>", "Skill URL")
|
|
14
|
+
.option("--name <name>", "Override skill name")
|
|
15
|
+
.option("--global", "Install to user scope")
|
|
16
|
+
.option("--agents <list>", "Comma-separated agent list")
|
|
17
|
+
.option("--json", "JSON output")
|
|
18
|
+
.action(async (url, options) => {
|
|
19
|
+
try {
|
|
20
|
+
const skillMarkdown = await fetchText(url);
|
|
21
|
+
const parsed = parseSkillMarkdown(skillMarkdown);
|
|
22
|
+
const inferred = inferNameFromUrl(url);
|
|
23
|
+
const skillName = options.name ?? inferred ?? parsed.name;
|
|
24
|
+
if (!skillName) {
|
|
25
|
+
throw new Error("Unable to infer skill name. Use --name to specify it.");
|
|
26
|
+
}
|
|
27
|
+
if (!parsed.name && !options.name) {
|
|
28
|
+
throw new Error("Skill frontmatter missing name. Provide --name to continue.");
|
|
29
|
+
}
|
|
30
|
+
const metadata = buildMetadata(parsed, { type: "url", url }, skillName);
|
|
31
|
+
if (!parsed.description) {
|
|
32
|
+
throw new Error("Skill frontmatter missing description. Convert the source into a valid skill.");
|
|
33
|
+
}
|
|
34
|
+
await ensureSkillsDir();
|
|
35
|
+
await writeSkillFiles(skillName, skillMarkdown, metadata);
|
|
36
|
+
const index = await loadIndex();
|
|
37
|
+
const updated = upsertSkill(index, {
|
|
38
|
+
name: skillName,
|
|
39
|
+
source: { type: "url", url },
|
|
40
|
+
checksum: parsed.checksum,
|
|
41
|
+
updatedAt: metadata.updatedAt,
|
|
42
|
+
});
|
|
43
|
+
const { projectRoot, scope, agentList } = await resolveRuntime({
|
|
44
|
+
global: options.global,
|
|
45
|
+
agents: options.agents,
|
|
46
|
+
});
|
|
47
|
+
const projectEntry = await ensureProjectRegistered(projectRoot, scope);
|
|
48
|
+
const paths = buildProjectAgentPaths(projectRoot, projectEntry);
|
|
49
|
+
const installed = [];
|
|
50
|
+
const installs = [];
|
|
51
|
+
for (const agent of agentList) {
|
|
52
|
+
const map = paths[agent];
|
|
53
|
+
if (!map) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const targets = buildTargets(agent, map, scope).map((target) => target.path);
|
|
57
|
+
const written = await copySkillToTargets(skillName, targets);
|
|
58
|
+
installed.push({ agent, scope, targets: written });
|
|
59
|
+
for (const target of written) {
|
|
60
|
+
installs.push({
|
|
61
|
+
scope,
|
|
62
|
+
agent,
|
|
63
|
+
path: target,
|
|
64
|
+
projectRoot: scope === "project" ? projectRoot : undefined,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const nextIndex = upsertSkill(updated, {
|
|
69
|
+
name: skillName,
|
|
70
|
+
source: { type: "url", url },
|
|
71
|
+
checksum: parsed.checksum,
|
|
72
|
+
updatedAt: metadata.updatedAt,
|
|
73
|
+
installs,
|
|
74
|
+
});
|
|
75
|
+
await saveIndex(sortIndex(nextIndex));
|
|
76
|
+
if (isJsonEnabled(options)) {
|
|
77
|
+
printJson({
|
|
78
|
+
ok: true,
|
|
79
|
+
command: "add",
|
|
80
|
+
data: {
|
|
81
|
+
name: skillName,
|
|
82
|
+
url,
|
|
83
|
+
scope,
|
|
84
|
+
agents: installed,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
printInfo(`Installed skill: ${skillName}`);
|
|
90
|
+
printInfo(`Source: ${url}`);
|
|
91
|
+
printInfo(`Scope: ${scope}`);
|
|
92
|
+
if (installed.length === 0) {
|
|
93
|
+
printInfo("No agent targets were updated.");
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
for (const entry of installed) {
|
|
97
|
+
printInfo(`Updated ${entry.agent}: ${entry.targets.join(", ")}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
handleCommandError(options, "add", error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
2
|
+
const agentSnippet = `Use skillbox for skill management.
|
|
3
|
+
|
|
4
|
+
Common workflow:
|
|
5
|
+
1) skillbox list --json
|
|
6
|
+
2) skillbox status --json
|
|
7
|
+
3) skillbox update <name> --json
|
|
8
|
+
|
|
9
|
+
If you need to install a new skill from a URL, run:
|
|
10
|
+
skillbox add <url> [--name <name>]
|
|
11
|
+
|
|
12
|
+
If a URL is not a valid skill, run:
|
|
13
|
+
skillbox convert <url> --agent
|
|
14
|
+
`;
|
|
15
|
+
export const registerAgent = (program) => {
|
|
16
|
+
program
|
|
17
|
+
.command("agent")
|
|
18
|
+
.description("Print agent-friendly usage")
|
|
19
|
+
.option("--json", "JSON output")
|
|
20
|
+
.action((options) => {
|
|
21
|
+
if (isJsonEnabled(options)) {
|
|
22
|
+
printJson({
|
|
23
|
+
ok: true,
|
|
24
|
+
command: "agent",
|
|
25
|
+
data: {
|
|
26
|
+
snippet: agentSnippet,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
printInfo(agentSnippet);
|
|
32
|
+
});
|
|
33
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
2
|
+
import { loadConfig, saveConfig } from "../lib/config.js";
|
|
3
|
+
import { handleCommandError } from "../lib/command.js";
|
|
4
|
+
const collect = (value, previous = []) => {
|
|
5
|
+
return [...previous, value];
|
|
6
|
+
};
|
|
7
|
+
export const registerConfig = (program) => {
|
|
8
|
+
const config = program.command("config").description("View or edit skillbox config");
|
|
9
|
+
config
|
|
10
|
+
.command("get")
|
|
11
|
+
.option("--json", "JSON output")
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
try {
|
|
14
|
+
const current = await loadConfig();
|
|
15
|
+
if (isJsonEnabled(options)) {
|
|
16
|
+
printJson({ ok: true, command: "config get", data: current });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
printInfo(JSON.stringify(current, null, 2));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
handleCommandError(options, "config get", error);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
config
|
|
26
|
+
.command("set")
|
|
27
|
+
.option("--default-agent <agent>", "Default agent", collect)
|
|
28
|
+
.option("--default-scope <scope>", "Default scope: project or user")
|
|
29
|
+
.option("--manage-system", "Enable system scope operations")
|
|
30
|
+
.option("--json", "JSON output")
|
|
31
|
+
.action(async (options) => {
|
|
32
|
+
try {
|
|
33
|
+
const current = await loadConfig();
|
|
34
|
+
const nextScope = options.defaultScope ?? current.defaultScope;
|
|
35
|
+
if (nextScope !== "project" && nextScope !== "user") {
|
|
36
|
+
throw new Error("defaultScope must be 'project' or 'user'.");
|
|
37
|
+
}
|
|
38
|
+
const next = {
|
|
39
|
+
...current,
|
|
40
|
+
defaultAgents: options.defaultAgent ?? current.defaultAgents,
|
|
41
|
+
defaultScope: nextScope,
|
|
42
|
+
manageSystem: options.manageSystem ?? current.manageSystem,
|
|
43
|
+
};
|
|
44
|
+
await saveConfig(next);
|
|
45
|
+
if (isJsonEnabled(options)) {
|
|
46
|
+
printJson({ ok: true, command: "config set", data: next });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
printInfo("Config updated.");
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
handleCommandError(options, "config set", error);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
4
|
+
import { fetchText } from "../lib/fetcher.js";
|
|
5
|
+
import { inferNameFromUrl } from "../lib/skill-parser.js";
|
|
6
|
+
import { handleCommandError } from "../lib/command.js";
|
|
7
|
+
export const registerConvert = (program) => {
|
|
8
|
+
program
|
|
9
|
+
.command("convert")
|
|
10
|
+
.argument("<url>", "Source URL to convert")
|
|
11
|
+
.option("--name <name>", "Override skill name")
|
|
12
|
+
.option("--output <dir>", "Output directory")
|
|
13
|
+
.option("--agent", "Delegate conversion to agent")
|
|
14
|
+
.option("--json", "JSON output")
|
|
15
|
+
.action(async (url, options) => {
|
|
16
|
+
try {
|
|
17
|
+
const sourceText = await fetchText(url);
|
|
18
|
+
const inferred = inferNameFromUrl(url);
|
|
19
|
+
const skillName = options.name ?? inferred;
|
|
20
|
+
if (!skillName) {
|
|
21
|
+
throw new Error("Unable to infer skill name. Use --name to specify it.");
|
|
22
|
+
}
|
|
23
|
+
const outputDir = options.output
|
|
24
|
+
? path.resolve(options.output)
|
|
25
|
+
: path.join(process.cwd(), "skillbox-convert", skillName);
|
|
26
|
+
const description = "Draft skill generated from source content.";
|
|
27
|
+
const draftMarkdown = `---\nname: ${skillName}\ndescription: ${description}\n---\n\n# ${skillName}\n\n## Source\n- See source.txt for the raw content.\n\n## When to use\n- TODO\n\n## Instructions\n- TODO\n`;
|
|
28
|
+
const metadata = {
|
|
29
|
+
name: skillName,
|
|
30
|
+
version: "0.1.0",
|
|
31
|
+
description,
|
|
32
|
+
entry: "SKILL.md",
|
|
33
|
+
source: { type: "url", url },
|
|
34
|
+
checksum: "draft",
|
|
35
|
+
updatedAt: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
38
|
+
await fs.writeFile(path.join(outputDir, "source.txt"), sourceText, "utf8");
|
|
39
|
+
await fs.writeFile(path.join(outputDir, "SKILL.md"), draftMarkdown, "utf8");
|
|
40
|
+
await fs.writeFile(path.join(outputDir, "skill.json"), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
41
|
+
if (isJsonEnabled(options)) {
|
|
42
|
+
printJson({
|
|
43
|
+
ok: true,
|
|
44
|
+
command: "convert",
|
|
45
|
+
data: {
|
|
46
|
+
url,
|
|
47
|
+
name: skillName,
|
|
48
|
+
outputDir,
|
|
49
|
+
agent: Boolean(options.agent),
|
|
50
|
+
sourceLength: sourceText.length,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
printInfo(`Draft created: ${outputDir}`);
|
|
56
|
+
if (options.agent) {
|
|
57
|
+
printInfo("Agent mode enabled. Use the source.txt content to refine SKILL.md.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
handleCommandError(options, "convert", error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
4
|
+
import { parseSkillMarkdown, buildMetadata } from "../lib/skill-parser.js";
|
|
5
|
+
import { ensureSkillsDir, writeSkillFiles } from "../lib/skill-store.js";
|
|
6
|
+
import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
|
|
7
|
+
import { handleCommandError } from "../lib/command.js";
|
|
8
|
+
export const registerImport = (program) => {
|
|
9
|
+
program
|
|
10
|
+
.command("import")
|
|
11
|
+
.argument("<path>", "Path to skill directory")
|
|
12
|
+
.option("--json", "JSON output")
|
|
13
|
+
.action(async (inputPath, options) => {
|
|
14
|
+
try {
|
|
15
|
+
const resolved = path.resolve(inputPath);
|
|
16
|
+
const skillPath = path.join(resolved, "SKILL.md");
|
|
17
|
+
const markdown = await fs.readFile(skillPath, "utf8");
|
|
18
|
+
const parsed = parseSkillMarkdown(markdown);
|
|
19
|
+
const metadata = buildMetadata(parsed, { type: "local" });
|
|
20
|
+
if (!parsed.description) {
|
|
21
|
+
throw new Error("Skill frontmatter missing description.");
|
|
22
|
+
}
|
|
23
|
+
await ensureSkillsDir();
|
|
24
|
+
await writeSkillFiles(metadata.name, markdown, metadata);
|
|
25
|
+
const index = await loadIndex();
|
|
26
|
+
const updated = upsertSkill(index, {
|
|
27
|
+
name: metadata.name,
|
|
28
|
+
source: { type: "local" },
|
|
29
|
+
checksum: parsed.checksum,
|
|
30
|
+
updatedAt: metadata.updatedAt,
|
|
31
|
+
});
|
|
32
|
+
await saveIndex(sortIndex(updated));
|
|
33
|
+
if (isJsonEnabled(options)) {
|
|
34
|
+
printJson({
|
|
35
|
+
ok: true,
|
|
36
|
+
command: "import",
|
|
37
|
+
data: {
|
|
38
|
+
name: metadata.name,
|
|
39
|
+
path: resolved,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
printInfo(`Imported skill: ${metadata.name}`);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
handleCommandError(options, "import", error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { isJsonEnabled, printInfo, printJson, printGroupList } from "../lib/output.js";
|
|
2
|
+
import { loadIndex } from "../lib/index.js";
|
|
3
|
+
import { groupNamesByKey } from "../lib/grouping.js";
|
|
4
|
+
export const registerList = (program) => {
|
|
5
|
+
program
|
|
6
|
+
.command("list")
|
|
7
|
+
.option("--group <group>", "Group by category, namespace, source, project")
|
|
8
|
+
.option("--json", "JSON output")
|
|
9
|
+
.action(async (options) => {
|
|
10
|
+
const index = await loadIndex();
|
|
11
|
+
const skills = index.skills;
|
|
12
|
+
const groupedProjects = groupByProject(skills);
|
|
13
|
+
const groupedSources = groupBySource(skills);
|
|
14
|
+
const groupedNamespaces = groupByNamespace(skills);
|
|
15
|
+
const groupedCategories = groupByCategory(skills);
|
|
16
|
+
if (isJsonEnabled(options)) {
|
|
17
|
+
printJson({
|
|
18
|
+
ok: true,
|
|
19
|
+
command: "list",
|
|
20
|
+
data: {
|
|
21
|
+
group: options.group ?? null,
|
|
22
|
+
skills,
|
|
23
|
+
projects: options.group === "project" ? groupedProjects : undefined,
|
|
24
|
+
sources: options.group === "source" ? groupedSources : undefined,
|
|
25
|
+
namespaces: options.group === "namespace" ? groupedNamespaces : undefined,
|
|
26
|
+
categories: options.group === "category" ? groupedCategories : undefined,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (options.group === "project") {
|
|
32
|
+
printGroupList("Projects", groupedProjects.map((project) => ({ key: project.root, items: project.skills })));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (options.group === "source") {
|
|
36
|
+
printGroupList("Sources", groupedSources.map((source) => ({ key: source.source, items: source.skills })));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (options.group === "namespace") {
|
|
40
|
+
printGroupList("Namespaces", groupedNamespaces.map((namespace) => ({
|
|
41
|
+
key: namespace.namespace,
|
|
42
|
+
items: namespace.skills,
|
|
43
|
+
})));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (options.group === "category") {
|
|
47
|
+
printGroupList("Categories", groupedCategories.map((category) => ({ key: category.category, items: category.skills })));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
printInfo(`Skills: ${skills.length}`);
|
|
51
|
+
for (const skill of skills) {
|
|
52
|
+
const source = skill.source.type;
|
|
53
|
+
const namespace = skill.namespace ? ` (${skill.namespace})` : "";
|
|
54
|
+
printInfo(`- ${skill.name}${namespace} [${source}]`);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const groupByProject = (skills) => {
|
|
59
|
+
const grouped = groupNamesByKey(skills, (skill) => skill.name, (skill) => (skill.installs ?? [])
|
|
60
|
+
.filter((install) => install.scope === "project" && install.projectRoot)
|
|
61
|
+
.map((install) => install.projectRoot));
|
|
62
|
+
return grouped.map((group) => ({ root: group.key, skills: group.skills }));
|
|
63
|
+
};
|
|
64
|
+
const groupBySource = (skills) => {
|
|
65
|
+
const grouped = groupNamesByKey(skills, (skill) => skill.name, (skill) => [skill.source.type]);
|
|
66
|
+
return grouped.map((group) => ({ source: group.key, skills: group.skills }));
|
|
67
|
+
};
|
|
68
|
+
const groupByNamespace = (skills) => {
|
|
69
|
+
const grouped = groupNamesByKey(skills, (skill) => skill.name, (skill) => [skill.namespace ?? "(none)"]);
|
|
70
|
+
return grouped
|
|
71
|
+
.map((group) => ({ namespace: group.key, skills: group.skills }))
|
|
72
|
+
.sort((a, b) => {
|
|
73
|
+
if (a.namespace === "(none)") {
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (b.namespace === "(none)") {
|
|
77
|
+
return -1;
|
|
78
|
+
}
|
|
79
|
+
return a.namespace.localeCompare(b.namespace);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const groupByCategory = (skills) => {
|
|
83
|
+
const grouped = groupNamesByKey(skills, (skill) => skill.name, (skill) => skill.categories ?? ["(uncategorized)"]);
|
|
84
|
+
return grouped
|
|
85
|
+
.map((group) => ({ category: group.key, skills: group.skills }))
|
|
86
|
+
.sort((a, b) => {
|
|
87
|
+
if (a.category === "(uncategorized)") {
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
if (b.category === "(uncategorized)") {
|
|
91
|
+
return -1;
|
|
92
|
+
}
|
|
93
|
+
return a.category.localeCompare(b.category);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isJsonEnabled, printInfo, printJson } from "../lib/output.js";
|
|
2
|
+
import { readSkillMetadata, writeSkillMetadata } from "../lib/skill-store.js";
|
|
3
|
+
import { loadIndex, saveIndex, sortIndex, upsertSkill } from "../lib/index.js";
|
|
4
|
+
import { handleCommandError } from "../lib/command.js";
|
|
5
|
+
export const registerMeta = (program) => {
|
|
6
|
+
const meta = program.command("meta").description("Manage skill metadata");
|
|
7
|
+
meta
|
|
8
|
+
.command("set")
|
|
9
|
+
.argument("<name>", "Skill name")
|
|
10
|
+
.option("--category <category>", "Category", collect)
|
|
11
|
+
.option("--tag <tag>", "Tag", collect)
|
|
12
|
+
.option("--namespace <namespace>", "Namespace")
|
|
13
|
+
.option("--json", "JSON output")
|
|
14
|
+
.action(async (name, options) => {
|
|
15
|
+
try {
|
|
16
|
+
const metadata = await readSkillMetadata(name);
|
|
17
|
+
const categories = options.category ?? metadata.categories ?? [];
|
|
18
|
+
const tags = options.tag ?? metadata.tags ?? [];
|
|
19
|
+
const namespace = options.namespace ?? metadata.namespace;
|
|
20
|
+
const nextMetadata = {
|
|
21
|
+
...metadata,
|
|
22
|
+
categories: categories.length > 0 ? categories : undefined,
|
|
23
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
24
|
+
namespace,
|
|
25
|
+
};
|
|
26
|
+
await writeSkillMetadata(name, nextMetadata);
|
|
27
|
+
const index = await loadIndex();
|
|
28
|
+
const updated = upsertSkill(index, {
|
|
29
|
+
name,
|
|
30
|
+
source: metadata.source,
|
|
31
|
+
checksum: metadata.checksum,
|
|
32
|
+
updatedAt: metadata.updatedAt,
|
|
33
|
+
categories: nextMetadata.categories,
|
|
34
|
+
tags: nextMetadata.tags,
|
|
35
|
+
namespace: nextMetadata.namespace,
|
|
36
|
+
});
|
|
37
|
+
await saveIndex(sortIndex(updated));
|
|
38
|
+
if (isJsonEnabled(options)) {
|
|
39
|
+
printJson({
|
|
40
|
+
ok: true,
|
|
41
|
+
command: "meta set",
|
|
42
|
+
data: {
|
|
43
|
+
name,
|
|
44
|
+
categories: nextMetadata.categories ?? [],
|
|
45
|
+
tags: nextMetadata.tags ?? [],
|
|
46
|
+
namespace: nextMetadata.namespace ?? null,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
printInfo(`Updated metadata for ${name}`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
handleCommandError(options, "meta set", error);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const collect = (value, previous = []) => {
|
|
59
|
+
return [...previous, value];
|
|
60
|
+
};
|