@joehom/awm-cli 0.0.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.
Files changed (46) hide show
  1. package/README.md +666 -0
  2. package/bin/awm.js +2 -0
  3. package/package.json +25 -0
  4. package/skills/awm-cli/SKILL.md +189 -0
  5. package/src/adapters/jsonAdapter.js +54 -0
  6. package/src/adapters/skillApplier.js +35 -0
  7. package/src/adapters/tomlAdapter.js +49 -0
  8. package/src/commands/doctor.js +100 -0
  9. package/src/commands/init.js +34 -0
  10. package/src/commands/mcp.js +253 -0
  11. package/src/commands/pull.js +168 -0
  12. package/src/commands/setup.js +31 -0
  13. package/src/commands/skill.js +187 -0
  14. package/src/commands/status.js +17 -0
  15. package/src/commands/tool.js +45 -0
  16. package/src/defaults/mcps/fetch.json +6 -0
  17. package/src/defaults/mcps/filesystem.json +6 -0
  18. package/src/defaults/mcps/github.json +9 -0
  19. package/src/defaults/mcps/memory.json +6 -0
  20. package/src/defaults/skills/awm-cli/SKILL.md +189 -0
  21. package/src/defaults/tools/claude-code.json +27 -0
  22. package/src/defaults/tools/codex.json +27 -0
  23. package/src/defaults/tools/copilot-cli.json +18 -0
  24. package/src/defaults/tools/cursor.json +27 -0
  25. package/src/defaults/tools/gemini-cli.json +27 -0
  26. package/src/defaults/tools/github-copilot.json +23 -0
  27. package/src/defaults/tools/windsurf.json +23 -0
  28. package/src/index.js +35 -0
  29. package/src/registry/mcpRegistry.js +68 -0
  30. package/src/registry/paths.js +21 -0
  31. package/src/registry/skillRegistry.js +61 -0
  32. package/src/registry/toolRegistry.js +43 -0
  33. package/src/seed.js +131 -0
  34. package/src/tools/claude-code.json +27 -0
  35. package/src/tools/codex.json +27 -0
  36. package/src/tools/copilot-cli.json +15 -0
  37. package/src/tools/cursor.json +27 -0
  38. package/src/tools/gemini-cli.json +27 -0
  39. package/src/tools/github-copilot.json +23 -0
  40. package/src/tools/windsurf.json +23 -0
  41. package/src/utils/fileUtils.js +76 -0
  42. package/src/utils/logger.js +17 -0
  43. package/src/utils/pathResolver.js +40 -0
  44. package/src/utils/validator.js +68 -0
  45. package/src/workspace/applyWorkspace.js +81 -0
  46. package/src/workspace/workspaceConfig.js +34 -0
@@ -0,0 +1,45 @@
1
+ import { Command } from 'commander';
2
+ import inquirer from 'inquirer';
3
+ import { log } from '../utils/logger.js';
4
+ import { listTools } from '../registry/toolRegistry.js';
5
+ import { readWorkspace, writeWorkspace } from '../workspace/workspaceConfig.js';
6
+ import { applyAll } from '../workspace/applyWorkspace.js';
7
+
8
+ export function makeToolCommand() {
9
+ return new Command('tool')
10
+ .description('Select which tools this workspace targets, then resync MCPs and skills')
11
+ .action(async () => {
12
+ const allTools = listTools();
13
+ if (allTools.length === 0) throw new Error('No tool definitions found');
14
+
15
+ const ws = readWorkspace();
16
+ const currentTools = ws?.tools ?? [];
17
+
18
+ let answer;
19
+ try {
20
+ answer = await inquirer.prompt([{
21
+ type: 'checkbox',
22
+ name: 'tools',
23
+ message: 'Select tools for this workspace (space to toggle, enter to confirm):',
24
+ choices: allTools.map(id => ({ name: id, value: id, checked: currentTools.includes(id) })),
25
+ validate: v => v.length > 0 || 'Select at least one tool',
26
+ }]);
27
+ } catch (err) {
28
+ if (err.name === 'ExitPromptError') {
29
+ log.info('Cancelled.');
30
+ return;
31
+ }
32
+ throw err;
33
+ }
34
+
35
+ const updated = { ...(ws ?? { mcps: [], skills: [] }), tools: answer.tools };
36
+ writeWorkspace(updated);
37
+
38
+ if (updated.mcps.length > 0 || updated.skills.length > 0) {
39
+ log.info('Resyncing MCPs and skills to updated tool list...');
40
+ await applyAll(updated);
41
+ }
42
+
43
+ log.success(`Tools updated: ${answer.tools.join(', ')}`);
44
+ });
45
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "fetch",
3
+ "transport": "stdio",
4
+ "command": "uvx",
5
+ "args": ["mcp-server-fetch"]
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "filesystem",
3
+ "transport": "stdio",
4
+ "command": "npx",
5
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "${env:HOME}"]
6
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "github",
3
+ "transport": "stdio",
4
+ "command": "npx",
5
+ "args": ["-y", "@modelcontextprotocol/server-github"],
6
+ "env": {
7
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "${env:GITHUB_TOKEN}"
8
+ }
9
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "memory",
3
+ "transport": "stdio",
4
+ "command": "npx",
5
+ "args": ["-y", "@modelcontextprotocol/server-memory"]
6
+ }
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: awm-cli
3
+ description: Use when the user asks to manage MCP servers, skills, or workspace configs for AI coding tools (Claude Code, Cursor, Codex, Copilot CLI, Gemini CLI, Windsurf, GitHub Copilot) using the awm CLI. Triggers on requests to register MCPs, add MCPs/skills to a project, import configs, or check registry health.
4
+ ---
5
+
6
+ # AWM — Agent Workspace Manager CLI
7
+
8
+ ## Overview
9
+
10
+ AWM is a local-first CLI that keeps a central registry of MCP servers and skills at `~/.agent-workspace/registry/`, then writes the correct config files into projects for supported AI tools.
11
+
12
+ **Core pattern:** Register once → add to workspace → auto-applied to all tools.
13
+
14
+ ```
15
+ awm init # create .awm.json in project root
16
+ awm mcp add # pick MCPs from registry → apply to all tools
17
+ awm skill add # pick skills from registry → apply to all tools
18
+ ```
19
+
20
+ ---
21
+
22
+ ## RULES — Follow These Before Running Any Command
23
+
24
+ **ALWAYS ask before acting. Never assume.**
25
+
26
+ 1. **Check existing state first** — Run `awm status` if `.awm.json` exists, and `awm mcp list -g` / `awm skill list -g` to see what's in the registry. Do not recreate things that already exist.
27
+
28
+ 2. **Only add what the user explicitly requested** — If the user says "add fetch and memory", add ONLY fetch and memory. Do not add other MCPs or skills, even if they exist in the registry.
29
+
30
+ 3. **Confirm the plan before running** — Before executing any sequence of commands, show the user what you are about to do:
31
+ ```
32
+ I will run:
33
+ awm init (creates .awm.json, you pick tools)
34
+ awm mcp add (you pick: fetch, memory)
35
+ Proceed?
36
+ ```
37
+
38
+ 4. **Run `awm init` only once per project** — It creates `.awm.json`. If it already exists, use `awm status` instead.
39
+
40
+ 5. **`mcp add` and `skill add` auto-apply** — After selection, MCPs and skills are immediately written to all tool config files in the project.
41
+
42
+ ---
43
+
44
+ ## Quick Reference
45
+
46
+ ### Workspace commands
47
+
48
+ | Command | What it does |
49
+ |---------|-------------|
50
+ | `awm init` | Interactive: pick tools → create `.awm.json` in cwd |
51
+ | `awm status` | Show current project tools, MCPs, skills, lastSync |
52
+ | `awm doctor` | Validate registry + `.awm.json` health |
53
+
54
+ ### MCP commands
55
+
56
+ | Command | What it does |
57
+ |---------|-------------|
58
+ | `awm mcp register [id]` | Interactive prompt → save MCP to global registry |
59
+ | `awm mcp unregister <id>` | Remove MCP from global registry |
60
+ | `awm mcp import` | Scan cwd tool config files → register found MCPs |
61
+ | `awm mcp add` | Checkbox from registry → add to `.awm.json` + apply to tools |
62
+ | `awm mcp delete <id>` | Remove MCP from `.awm.json` + re-apply tools |
63
+ | `awm mcp list` | List MCPs in current workspace |
64
+ | `awm mcp list -g` | List all MCPs in global registry |
65
+ | `awm mcp show <id>` | Print full MCP definition |
66
+
67
+ ### Skill commands
68
+
69
+ | Command | What it does |
70
+ |---------|-------------|
71
+ | `awm skill register <name> --from <path>` | Copy dir/file into global registry |
72
+ | `awm skill unregister <name>` | Remove skill from global registry |
73
+ | `awm skill import` | Scan `.claude/skills/`, `.agents/skills/` → register found skills |
74
+ | `awm skill add` | Checkbox from registry → add to `.awm.json` + apply to tools |
75
+ | `awm skill delete <name>` | Remove skill from `.awm.json` |
76
+ | `awm skill list` | List skills in current workspace |
77
+ | `awm skill list -g` | List all skills in global registry |
78
+ | `awm skill show <name>` | Print SKILL.md |
79
+
80
+ ### Other commands
81
+
82
+ | Command | What it does |
83
+ |---------|-------------|
84
+ | `awm tool list` | List known tool IDs |
85
+ | `awm tool show <id>` | Print tool definition JSON |
86
+ | `awm setup [--force]` | Re-seed default MCPs and skills into registry |
87
+
88
+ ---
89
+
90
+ ## Common Workflows
91
+
92
+ ### Set up a new project
93
+
94
+ ```bash
95
+ # 1. Check registry state
96
+ awm mcp list -g
97
+ awm skill list -g
98
+
99
+ # 2. Initialize workspace (interactive: pick tools)
100
+ awm init
101
+
102
+ # 3. Add ONLY the MCPs the user requested
103
+ awm mcp add # checkbox → select fetch, memory (if user asked)
104
+
105
+ # 4. Add ONLY the skills the user requested
106
+ awm skill add # checkbox → select awm-cli (if user asked)
107
+
108
+ # 5. Verify
109
+ awm status
110
+ cat .awm.json
111
+ ```
112
+
113
+ ### Register a new MCP from a file
114
+
115
+ To register an MCP defined in a JSON file, use `mcp register` interactively, or import existing tool configs:
116
+
117
+ ```bash
118
+ awm mcp import # scans .mcp.json, .cursor/mcp.json, .gemini/settings.json, etc.
119
+ ```
120
+
121
+ ### Register a skill
122
+
123
+ ```bash
124
+ # From a directory (must contain SKILL.md)
125
+ awm skill register clean-arch --from ./skills/clean-arch/
126
+
127
+ # From a single markdown file
128
+ awm skill register tdd --from ./TDD.md
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Workspace Config — `.awm.json`
134
+
135
+ Created by `awm init` at the project root:
136
+
137
+ ```json
138
+ {
139
+ "tools": ["claude-code", "cursor", "windsurf"],
140
+ "mcps": ["fetch", "memory"],
141
+ "skills": ["awm-cli"],
142
+ "lastSync": "2026-03-08T12:00:00Z"
143
+ }
144
+ ```
145
+
146
+ - **`tools`** — which AI tools to write configs for in this project
147
+ - **`mcps`** — MCP IDs from the global registry active in this project
148
+ - **`skills`** — skill names from the global registry active in this project
149
+ - **`lastSync`** — timestamp of last `mcp add` or `skill add` apply
150
+
151
+ ---
152
+
153
+ ## Apply Output by Tool
154
+
155
+ | Tool | MCP file | Format | Skills |
156
+ |------|----------|--------|--------|
157
+ | `claude-code` | `.mcp.json` | JSON | `.claude/skills/<name>/` |
158
+ | `codex` | `.codex/config.toml` | TOML | — |
159
+ | `cursor` | `.cursor/mcp.json` | JSON | — |
160
+ | `gemini-cli` | `.gemini/settings.json` | JSON | — |
161
+ | `github-copilot` | `.vscode/mcp.json` | JSON (`servers`) | — |
162
+ | `windsurf` | — | — | — |
163
+ | `copilot-cli` | — | — | — |
164
+
165
+ Notes:
166
+ - **claude-code** is the only tool that supports skills.
167
+ - **Merge-safe writes** — `mcp add` only touches the MCP root key. All other keys in tool config files are preserved.
168
+ - **Env passthrough** — `${env:VAR}` values are written verbatim; the target tool resolves them at runtime.
169
+
170
+ ---
171
+
172
+ ## Default MCPs (pre-installed)
173
+
174
+ | ID | Command | Notes |
175
+ |----|---------|-------|
176
+ | `github` | `npx -y @modelcontextprotocol/server-github` | Needs `GITHUB_TOKEN` env var |
177
+ | `filesystem` | `npx -y @modelcontextprotocol/server-filesystem` | Local file access |
178
+ | `memory` | `npx -y @modelcontextprotocol/server-memory` | Persistent knowledge graph |
179
+ | `fetch` | `uvx mcp-server-fetch` | Web fetching (requires `uv`) |
180
+
181
+ ---
182
+
183
+ ## Environment Variable Override
184
+
185
+ ```bash
186
+ AWM_REGISTRY=/path/to/registry awm mcp list -g
187
+ ```
188
+
189
+ Default registry: `~/.agent-workspace/registry/`
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "claude-code",
3
+ "name": "Claude Code",
4
+ "supports": {
5
+ "mcp": true,
6
+ "skills": true
7
+ },
8
+ "mcp": {
9
+ "format": "json",
10
+ "project": {
11
+ "targetFile": ".mcp.json",
12
+ "rootObject": "mcpServers"
13
+ },
14
+ "global": {
15
+ "targetFile": "~/.claude.json",
16
+ "rootObject": "mcpServers"
17
+ }
18
+ },
19
+ "skills": {
20
+ "project": {
21
+ "targetFolder": ".claude/skills"
22
+ },
23
+ "global": {
24
+ "targetFolder": "~/.claude/skills"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "codex",
3
+ "name": "Codex CLI",
4
+ "supports": {
5
+ "mcp": true,
6
+ "skills": true
7
+ },
8
+ "mcp": {
9
+ "format": "toml",
10
+ "project": {
11
+ "targetFile": ".codex/config.toml",
12
+ "rootObject": "mcp_servers"
13
+ },
14
+ "global": {
15
+ "targetFile": "~/.codex/config.toml",
16
+ "rootObject": "mcp_servers"
17
+ }
18
+ },
19
+ "skills": {
20
+ "project": {
21
+ "targetFolder": ".agents/skills"
22
+ },
23
+ "global": {
24
+ "targetFolder": "~/.agents/skills"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "copilot-cli",
3
+ "name": "Copilot CLI",
4
+ "supports": {
5
+ "mcp": true,
6
+ "skills": true
7
+ },
8
+ "mcp": {
9
+ "format": "json",
10
+ "global": {
11
+ "targetFile": "~/.copilot/mcp-config.json",
12
+ "rootObject": "servers"
13
+ }
14
+ },
15
+ "skills": {
16
+ "global": "~/.copilot/skills"
17
+ }
18
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "cursor",
3
+ "name": "Cursor",
4
+ "supports": {
5
+ "mcp": true,
6
+ "skills": true
7
+ },
8
+ "mcp": {
9
+ "format": "json",
10
+ "project": {
11
+ "targetFile": ".cursor/mcp.json",
12
+ "rootObject": "mcpServers"
13
+ },
14
+ "global": {
15
+ "targetFile": "~/.cursor/mcp.json",
16
+ "rootObject": "mcpServers"
17
+ }
18
+ },
19
+ "skills": {
20
+ "project": {
21
+ "targetFolder": ".cursor/rules"
22
+ },
23
+ "global": {
24
+ "targetFolder": "~/.cursor/rules"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "gemini-cli",
3
+ "name": "Gemini CLI",
4
+ "supports": {
5
+ "mcp": true,
6
+ "skills": true
7
+ },
8
+ "mcp": {
9
+ "format": "json",
10
+ "project": {
11
+ "targetFile": ".gemini/settings.json",
12
+ "rootObject": "mcpServers"
13
+ },
14
+ "global": {
15
+ "targetFile": "~/.gemini/settings.json",
16
+ "rootObject": "mcpServers"
17
+ }
18
+ },
19
+ "skills": {
20
+ "project": {
21
+ "targetFolder": ".gemini/skills"
22
+ },
23
+ "global": {
24
+ "targetFolder": "~/.gemini/skills"
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "github-copilot",
3
+ "name": "GitHub Copilot (VS Code)",
4
+ "supports": {
5
+ "mcp": true,
6
+ "skills": true
7
+ },
8
+ "mcp": {
9
+ "format": "json",
10
+ "project": {
11
+ "targetFile": ".vscode/mcp.json",
12
+ "rootObject": "servers"
13
+ }
14
+ },
15
+ "skills": {
16
+ "project": {
17
+ "targetFolder": ".github/skills"
18
+ },
19
+ "global": {
20
+ "targetFolder": "~/.copilot/skills"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "id": "windsurf",
3
+ "name": "Windsurf",
4
+ "supports": {
5
+ "mcp": true,
6
+ "skills": true
7
+ },
8
+ "mcp": {
9
+ "format": "json",
10
+ "global": {
11
+ "targetFile": "~/.codeium/windsurf/mcp_config.json",
12
+ "rootObject": "mcpServers"
13
+ }
14
+ },
15
+ "skills": {
16
+ "project": {
17
+ "targetFolder": ".windsurf/skills"
18
+ },
19
+ "global": {
20
+ "targetFolder": "~/.codeium/windsurf/skills"
21
+ }
22
+ }
23
+ }
package/src/index.js ADDED
@@ -0,0 +1,35 @@
1
+ import { Command } from 'commander';
2
+ import { ensureRegistryDirs } from './registry/paths.js';
3
+ import { log } from './utils/logger.js';
4
+ import { makeMcpCommand } from './commands/mcp.js';
5
+ import { makeSkillCommand } from './commands/skill.js';
6
+ import { makeInitCommand } from './commands/init.js';
7
+ import { makePullCommand } from './commands/pull.js';
8
+ import { makeStatusCommand } from './commands/status.js';
9
+ import { makeDoctorCommand } from './commands/doctor.js';
10
+ import { makeToolCommand } from './commands/tool.js';
11
+ import { makeSetupCommand } from './commands/setup.js';
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('awm')
17
+ .description('Agent Workspace Manager — manage MCP servers, skills, and workspace configs for AI coding tools')
18
+ .version('1.0.0');
19
+
20
+ program.addCommand(makeInitCommand());
21
+ program.addCommand(makePullCommand());
22
+ program.addCommand(makeStatusCommand());
23
+ program.addCommand(makeMcpCommand());
24
+ program.addCommand(makeSkillCommand());
25
+ program.addCommand(makeDoctorCommand());
26
+ program.addCommand(makeToolCommand());
27
+ program.addCommand(makeSetupCommand());
28
+
29
+ try {
30
+ ensureRegistryDirs();
31
+ await program.parseAsync(process.argv);
32
+ } catch (err) {
33
+ log.error(err.message);
34
+ process.exit(1);
35
+ }
@@ -0,0 +1,68 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getMcpsDir } from './paths.js';
4
+ import { readJson, writeJson, listFiles } from '../utils/fileUtils.js';
5
+ import { validateMcp } from '../utils/validator.js';
6
+
7
+ function mcpFile(id) {
8
+ return path.join(getMcpsDir(), `${id}.json`);
9
+ }
10
+
11
+ /**
12
+ * List all registered MCP IDs.
13
+ * @returns {string[]}
14
+ */
15
+ export function listMcps() {
16
+ return listFiles(getMcpsDir())
17
+ .filter(f => f.endsWith('.json'))
18
+ .map(f => f.replace(/\.json$/, ''));
19
+ }
20
+
21
+ /**
22
+ * Get an MCP definition by ID. Returns null if not found.
23
+ * @param {string} id
24
+ * @returns {object|null}
25
+ */
26
+ export function getMcp(id) {
27
+ return readJson(mcpFile(id));
28
+ }
29
+
30
+ /**
31
+ * Save an MCP definition (validates first).
32
+ * Throws if validation fails.
33
+ * @param {object} mcp
34
+ */
35
+ export function saveMcp(mcp) {
36
+ const { valid, errors } = validateMcp(mcp);
37
+ if (!valid) {
38
+ throw new Error(`Invalid MCP definition: ${errors.join('; ')}`);
39
+ }
40
+ writeJson(mcpFile(mcp.id), mcp);
41
+ }
42
+
43
+ /**
44
+ * Remove an MCP by ID. Throws if not found.
45
+ * @param {string} id
46
+ */
47
+ export function removeMcp(id) {
48
+ const file = mcpFile(id);
49
+ if (!fs.existsSync(file)) {
50
+ throw new Error(`MCP "${id}" not found in registry`);
51
+ }
52
+ fs.rmSync(file);
53
+ }
54
+
55
+ /**
56
+ * Import one or more MCPs from a JSON value (single object or array).
57
+ * @param {object|object[]} data
58
+ * @returns {string[]} imported IDs
59
+ */
60
+ export function importMcps(data) {
61
+ const items = Array.isArray(data) ? data : [data];
62
+ const imported = [];
63
+ for (const item of items) {
64
+ saveMcp(item);
65
+ imported.push(item.id);
66
+ }
67
+ return imported;
68
+ }
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getRegistryRoot } from '../utils/pathResolver.js';
4
+
5
+ export function getMcpsDir() {
6
+ return path.join(getRegistryRoot(), 'mcps');
7
+ }
8
+
9
+ export function getSkillsDir() {
10
+ return path.join(getRegistryRoot(), 'skills');
11
+ }
12
+
13
+ export function getToolsDir() {
14
+ return path.join(getRegistryRoot(), 'tools');
15
+ }
16
+
17
+ export function ensureRegistryDirs() {
18
+ for (const dir of [getMcpsDir(), getSkillsDir(), getToolsDir()]) {
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ }
21
+ }
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { getSkillsDir } from './paths.js';
4
+ import { listDirs, fileExists, copyDir } from '../utils/fileUtils.js';
5
+
6
+ function skillDir(name) {
7
+ return path.join(getSkillsDir(), name);
8
+ }
9
+
10
+ /**
11
+ * List all registered skill names.
12
+ * @returns {string[]}
13
+ */
14
+ export function listSkills() {
15
+ return listDirs(getSkillsDir());
16
+ }
17
+
18
+ /**
19
+ * Add a skill from a source path (directory or single .md file).
20
+ * If src is a directory, it is copied as-is.
21
+ * If src is a .md file, it is placed into a new folder named after the skill,
22
+ * saved as SKILL.md.
23
+ * @param {string} name
24
+ * @param {string} src absolute path to source
25
+ */
26
+ export async function addSkill(name, src) {
27
+ const dest = skillDir(name);
28
+ const stat = fs.statSync(src);
29
+ if (stat.isDirectory()) {
30
+ await copyDir(src, dest);
31
+ } else if (src.endsWith('.md')) {
32
+ fs.mkdirSync(dest, { recursive: true });
33
+ fs.copyFileSync(src, path.join(dest, 'SKILL.md'));
34
+ } else {
35
+ throw new Error(`Skill source must be a directory or .md file: ${src}`);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Show the SKILL.md contents for a skill.
41
+ * Returns null if SKILL.md doesn't exist.
42
+ * @param {string} name
43
+ * @returns {string|null}
44
+ */
45
+ export function showSkill(name) {
46
+ const skillMd = path.join(skillDir(name), 'SKILL.md');
47
+ if (!fileExists(skillMd)) return null;
48
+ return fs.readFileSync(skillMd, 'utf8');
49
+ }
50
+
51
+ /**
52
+ * Remove a skill by name. Throws if not found.
53
+ * @param {string} name
54
+ */
55
+ export function removeSkill(name) {
56
+ const dir = skillDir(name);
57
+ if (!fs.existsSync(dir)) {
58
+ throw new Error(`Skill "${name}" not found in registry`);
59
+ }
60
+ fs.rmSync(dir, { recursive: true });
61
+ }
@@ -0,0 +1,43 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { getToolsDir } from './paths.js';
5
+ import { readJson, listFiles } from '../utils/fileUtils.js';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const BUILTIN_TOOLS_DIR = path.join(__dirname, '..', 'tools');
9
+
10
+ /**
11
+ * Load a tool definition by ID.
12
+ * Checks user registry tools/ first, then falls back to builtin src/tools/.
13
+ * Returns null if not found.
14
+ * @param {string} id
15
+ * @returns {object|null}
16
+ */
17
+ export function loadTool(id) {
18
+ // User override
19
+ const userFile = path.join(getToolsDir(), `${id}.json`);
20
+ if (fs.existsSync(userFile)) return readJson(userFile);
21
+
22
+ // Builtin
23
+ const builtinFile = path.join(BUILTIN_TOOLS_DIR, `${id}.json`);
24
+ if (fs.existsSync(builtinFile)) return readJson(builtinFile);
25
+
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * List all known tool IDs (union of user overrides and builtins).
31
+ * @returns {string[]}
32
+ */
33
+ export function listTools() {
34
+ const userTools = listFiles(getToolsDir())
35
+ .filter(f => f.endsWith('.json'))
36
+ .map(f => f.replace(/\.json$/, ''));
37
+
38
+ const builtinTools = listFiles(BUILTIN_TOOLS_DIR)
39
+ .filter(f => f.endsWith('.json'))
40
+ .map(f => f.replace(/\.json$/, ''));
41
+
42
+ return [...new Set([...userTools, ...builtinTools])];
43
+ }