@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,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,54 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readJson, writeJson } from '../utils/fileUtils.js';
4
+ import { resolvePath } from '../utils/pathResolver.js';
5
+ import { log } from '../utils/logger.js';
6
+
7
+ /**
8
+ * Build the MCP block for a tool config file from an MCP definition.
9
+ * Strips internal fields (id, transport). Does NOT expand ${env:VAR} in env values.
10
+ * @param {object} mcp
11
+ * @returns {object}
12
+ */
13
+ export function buildMcpBlock(mcp) {
14
+ const block = {
15
+ command: mcp.command,
16
+ };
17
+ if (mcp.args && mcp.args.length > 0) {
18
+ block.args = mcp.args;
19
+ }
20
+ if (mcp.env && Object.keys(mcp.env).length > 0) {
21
+ block.env = mcp.env; // passthrough — NOT expanded
22
+ }
23
+ return block;
24
+ }
25
+
26
+ /**
27
+ * Apply MCPs to a JSON config file (merge-safe).
28
+ * Reads existing file, merges into [rootObject] key, writes back (or dry-runs).
29
+ * @param {string} targetFile may contain ~ or env vars
30
+ * @param {string} rootObject e.g. "mcpServers"
31
+ * @param {object[]} mcps canonical MCP definitions
32
+ * @param {boolean} dryRun
33
+ */
34
+ export async function applyMcpJson(targetFile, rootObject, mcps, dryRun = false) {
35
+ const resolved = resolvePath(targetFile);
36
+ const existing = readJson(resolved) ?? {};
37
+
38
+ if (!existing[rootObject]) {
39
+ existing[rootObject] = {};
40
+ }
41
+
42
+ for (const mcp of mcps) {
43
+ existing[rootObject][mcp.id] = buildMcpBlock(mcp);
44
+ }
45
+
46
+ if (dryRun) {
47
+ log.dryRun(`Would write ${mcps.length} MCP(s) to ${resolved} under "${rootObject}"`);
48
+ log.dryRun(JSON.stringify(existing, null, 2));
49
+ return;
50
+ }
51
+
52
+ writeJson(resolved, existing);
53
+ log.success(`Wrote ${mcps.length} MCP(s) to ${resolved}`);
54
+ }
@@ -0,0 +1,35 @@
1
+ import path from 'node:path';
2
+ import { getSkillsDir } from '../registry/paths.js';
3
+ import { fileExists, copyDir } from '../utils/fileUtils.js';
4
+ import { resolvePath } from '../utils/pathResolver.js';
5
+ import { log } from '../utils/logger.js';
6
+
7
+ /**
8
+ * Apply skills to a target folder.
9
+ * For each skill: validates SKILL.md exists, then copies the skill directory.
10
+ * @param {string[]} skillNames
11
+ * @param {string} targetFolder may contain ~ or env vars
12
+ * @param {boolean} dryRun
13
+ */
14
+ export async function applySkills(skillNames, targetFolder, dryRun = false) {
15
+ const resolvedTarget = resolvePath(targetFolder);
16
+
17
+ for (const name of skillNames) {
18
+ const skillDir = path.join(getSkillsDir(), name);
19
+ const skillMd = path.join(skillDir, 'SKILL.md');
20
+
21
+ if (!fileExists(skillMd)) {
22
+ throw new Error(`Skill "${name}" is missing SKILL.md at ${skillMd}`);
23
+ }
24
+
25
+ const dest = path.join(resolvedTarget, name);
26
+
27
+ if (dryRun) {
28
+ log.dryRun(`Would copy skill "${name}" → ${dest}`);
29
+ continue;
30
+ }
31
+
32
+ await copyDir(skillDir, dest);
33
+ log.success(`Copied skill "${name}" → ${dest}`);
34
+ }
35
+ }
@@ -0,0 +1,49 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import TOML from '@iarna/toml';
4
+ import { fileExists } from '../utils/fileUtils.js';
5
+ import { resolvePath } from '../utils/pathResolver.js';
6
+ import { log } from '../utils/logger.js';
7
+ import { buildMcpBlock } from './jsonAdapter.js';
8
+
9
+ /**
10
+ * Apply MCPs to a TOML config file (merge-safe).
11
+ * Reads existing file, merges into [rootObject] key, writes back (or dry-runs).
12
+ * @param {string} targetFile may contain ~ or env vars
13
+ * @param {string} rootObject e.g. "mcpServers"
14
+ * @param {object[]} mcps canonical MCP definitions
15
+ * @param {boolean} dryRun
16
+ */
17
+ export async function applyMcpToml(targetFile, rootObject, mcps, dryRun = false) {
18
+ const resolved = resolvePath(targetFile);
19
+ let existing = {};
20
+
21
+ if (fileExists(resolved)) {
22
+ try {
23
+ const raw = fs.readFileSync(resolved, 'utf8');
24
+ existing = TOML.parse(raw);
25
+ } catch (e) {
26
+ throw new Error(`Failed to parse TOML at ${resolved}: ${e.message}`);
27
+ }
28
+ }
29
+
30
+ if (!existing[rootObject]) {
31
+ existing[rootObject] = {};
32
+ }
33
+
34
+ for (const mcp of mcps) {
35
+ existing[rootObject][mcp.id] = buildMcpBlock(mcp);
36
+ }
37
+
38
+ const tomlStr = TOML.stringify(existing);
39
+
40
+ if (dryRun) {
41
+ log.dryRun(`Would write ${mcps.length} MCP(s) to ${resolved} under "${rootObject}"`);
42
+ log.dryRun(tomlStr);
43
+ return;
44
+ }
45
+
46
+ fs.mkdirSync(path.dirname(resolved), { recursive: true });
47
+ fs.writeFileSync(resolved, tomlStr, 'utf8');
48
+ log.success(`Wrote ${mcps.length} MCP(s) to ${resolved}`);
49
+ }
@@ -0,0 +1,100 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Command } from 'commander';
4
+ import { log } from '../utils/logger.js';
5
+ import { getMcpsDir, getSkillsDir } from '../registry/paths.js';
6
+ import { listFiles, listDirs, fileExists } from '../utils/fileUtils.js';
7
+ import { validateMcp, validateToolDef } from '../utils/validator.js';
8
+ import { loadTool, listTools } from '../registry/toolRegistry.js';
9
+ import { readJson } from '../utils/fileUtils.js';
10
+ import { getWorkspacePath } from '../workspace/workspaceConfig.js';
11
+
12
+ export function makeDoctorCommand() {
13
+ return new Command('doctor')
14
+ .description('Check registry integrity and validate workspace config')
15
+ .action(() => {
16
+ const issues = [];
17
+
18
+ // Check registry dirs exist
19
+ for (const [label, dir] of [
20
+ ['mcps', getMcpsDir()],
21
+ ['skills', getSkillsDir()],
22
+ ]) {
23
+ if (!fs.existsSync(dir)) {
24
+ issues.push(`Registry dir missing: ${dir}`);
25
+ }
26
+ }
27
+
28
+ // Validate all MCP JSON files in registry
29
+ const mcpsDir = getMcpsDir();
30
+ const mcpFiles = listFiles(mcpsDir).filter(f => f.endsWith('.json'));
31
+ for (const file of mcpFiles) {
32
+ const data = readJson(path.join(mcpsDir, file));
33
+ const { valid, errors } = validateMcp(data);
34
+ if (!valid) {
35
+ issues.push(`MCP file "${file}": ${errors.join('; ')}`);
36
+ }
37
+ }
38
+
39
+ // Validate all tool definitions
40
+ const knownToolIds = listTools();
41
+ for (const toolId of knownToolIds) {
42
+ const toolDef = loadTool(toolId);
43
+ const { valid, errors } = validateToolDef(toolDef);
44
+ if (!valid) {
45
+ issues.push(`Tool "${toolId}": ${errors.join('; ')}`);
46
+ }
47
+ }
48
+
49
+ // Validate .awm.json if present in cwd
50
+ const wsPath = getWorkspacePath();
51
+ if (fs.existsSync(wsPath)) {
52
+ const ws = readJson(wsPath);
53
+ if (!ws) {
54
+ issues.push('.awm.json: invalid JSON');
55
+ } else {
56
+ if (!Array.isArray(ws.tools)) issues.push('.awm.json: "tools" must be an array');
57
+ if (!Array.isArray(ws.mcps)) issues.push('.awm.json: "mcps" must be an array');
58
+ if (!Array.isArray(ws.skills)) issues.push('.awm.json: "skills" must be an array');
59
+
60
+ if (Array.isArray(ws.tools)) {
61
+ for (const toolId of ws.tools) {
62
+ if (!knownToolIds.includes(toolId)) {
63
+ issues.push(`.awm.json: unknown tool "${toolId}"`);
64
+ }
65
+ }
66
+ }
67
+
68
+ const knownMcpIds = new Set(mcpFiles.map(f => f.replace(/\.json$/, '')));
69
+ if (Array.isArray(ws.mcps)) {
70
+ for (const mcpId of ws.mcps) {
71
+ if (!knownMcpIds.has(mcpId)) {
72
+ issues.push(`.awm.json: MCP "${mcpId}" not found in registry`);
73
+ }
74
+ }
75
+ }
76
+
77
+ const knownSkills = new Set(listDirs(getSkillsDir()));
78
+ if (Array.isArray(ws.skills)) {
79
+ for (const skillName of ws.skills) {
80
+ if (!knownSkills.has(skillName)) {
81
+ issues.push(`.awm.json: skill "${skillName}" not found in registry`);
82
+ } else {
83
+ const skillMd = path.join(getSkillsDir(), skillName, 'SKILL.md');
84
+ if (!fileExists(skillMd)) {
85
+ issues.push(`.awm.json: skill "${skillName}" missing SKILL.md`);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ if (issues.length > 0) {
94
+ for (const issue of issues) log.error(issue);
95
+ process.exit(1);
96
+ } else {
97
+ log.success('No issues found');
98
+ }
99
+ });
100
+ }
@@ -0,0 +1,34 @@
1
+ import { Command } from 'commander';
2
+ import inquirer from 'inquirer';
3
+ import fs from 'node:fs';
4
+ import { log } from '../utils/logger.js';
5
+ import { listTools } from '../registry/toolRegistry.js';
6
+ import { getWorkspacePath, writeWorkspace } from '../workspace/workspaceConfig.js';
7
+
8
+ export function makeInitCommand() {
9
+ return new Command('init')
10
+ .description('Initialize a workspace config (.awm.json) in the current directory')
11
+ .option('--force', 'Overwrite existing .awm.json')
12
+ .action(async (opts) => {
13
+ const wsPath = getWorkspacePath();
14
+
15
+ if (fs.existsSync(wsPath) && !opts.force) {
16
+ throw new Error(`.awm.json already exists. Use --force to overwrite.`);
17
+ }
18
+
19
+ const tools = listTools();
20
+ if (tools.length === 0) throw new Error('No tool definitions found');
21
+
22
+ const { selectedTools } = await inquirer.prompt([{
23
+ type: 'checkbox',
24
+ name: 'selectedTools',
25
+ message: 'Select tools to support in this workspace:',
26
+ choices: tools,
27
+ validate: v => v.length > 0 || 'Select at least one tool',
28
+ }]);
29
+
30
+ writeWorkspace({ tools: selectedTools, mcps: [], skills: [] });
31
+ log.success(`Workspace initialized with tools: ${selectedTools.join(', ')}`);
32
+ log.info(` Run "awm mcp add" to add MCPs, "awm skill add" to add skills.`);
33
+ });
34
+ }