code-ai-installer 4.0.1-b → 4.0.1-c

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 (128) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +5 -5
  3. package/dist/catalog.js +1 -1
  4. package/dist/contentTransformer.d.ts +1 -1
  5. package/dist/contentTransformer.js +39 -0
  6. package/dist/index.js +10 -5
  7. package/dist/mcp/cli.js +4 -4
  8. package/dist/mcp/scorecard.d.ts +2 -2
  9. package/dist/mcp/task_state.d.ts +2 -2
  10. package/dist/mcp/tools/advance_gate.js +1 -1
  11. package/dist/mcp/tools/classify_gate.d.ts +2 -2
  12. package/dist/mcp/tools/classify_gate.js +2 -2
  13. package/dist/mcp/tools/load_role.d.ts +2 -2
  14. package/dist/mcp/tools/load_role.js +2 -2
  15. package/dist/mcp/tools/report_exception.d.ts +3 -3
  16. package/dist/mcp/tools/report_exception.js +4 -4
  17. package/dist/mcp/tools/request_decision.d.ts +3 -3
  18. package/dist/mcp/tools/request_decision.js +5 -5
  19. package/dist/mcp/tools/review_proposal.d.ts +1 -1
  20. package/dist/mcp/tools/review_proposal.js +6 -6
  21. package/dist/mcp/tools/sign_off.d.ts +2 -2
  22. package/dist/mcp/tools/sign_off.js +7 -7
  23. package/dist/mcp/tools/verify_claim.d.ts +1 -1
  24. package/dist/mcp/tools/verify_claim.js +1 -1
  25. package/dist/mcp_setup.d.ts +84 -31
  26. package/dist/mcp_setup.js +182 -66
  27. package/dist/platforms/adapters.js +54 -19
  28. package/dist/shared/frontmatter.js +1 -1
  29. package/dist/shared/persona.d.ts +1 -1
  30. package/dist/shared/persona.js +1 -1
  31. package/dist/shared/pipeline.d.ts +10 -10
  32. package/dist/shared/pipeline.js +7 -7
  33. package/dist/shared/tools.d.ts +15 -15
  34. package/dist/shared/tools.js +3 -3
  35. package/dist/shared/vocabulary.d.ts +4 -4
  36. package/dist/shared/vocabulary.js +4 -4
  37. package/dist/types.d.ts +1 -1
  38. package/domains/analytics/.agents/workflows/analytics-pipeline-rules.md +13 -3
  39. package/domains/analytics/.agents/workflows/analyze.md +1 -0
  40. package/domains/analytics/.agents/workflows/quick-insight.md +1 -0
  41. package/domains/analytics/locales/en/.agents/workflows/analytics-pipeline-rules.md +13 -3
  42. package/domains/analytics/locales/en/.agents/workflows/analyze.md +1 -0
  43. package/domains/analytics/locales/en/.agents/workflows/quick-insight.md +1 -0
  44. package/domains/analytics/locales/en/agents/interviewer.md +2 -1
  45. package/domains/analytics/locales/en/agents/layouter.md +2 -1
  46. package/domains/analytics/locales/en/agents/mediator.md +2 -1
  47. package/domains/analytics/locales/en/agents/researcher.md +2 -1
  48. package/domains/analytics/locales/en/agents/strategist.md +2 -1
  49. package/domains/analytics/pipeline.yaml +10 -10
  50. package/domains/content/.agents/skills/content-release-gate/SKILL.md +3 -5
  51. package/domains/content/.agents/workflows/content-pipeline-rules.md +14 -11
  52. package/domains/content/.agents/workflows/edit-content.md +0 -1
  53. package/domains/content/.agents/workflows/quick-post.md +0 -1
  54. package/domains/content/.agents/workflows/start-content.md +0 -1
  55. package/domains/content/agents/conductor.md +1 -2
  56. package/domains/content/locales/en/.agents/skills/content-release-gate/SKILL.md +3 -5
  57. package/domains/content/locales/en/.agents/workflows/content-pipeline-rules.md +14 -11
  58. package/domains/content/locales/en/.agents/workflows/edit-content.md +0 -1
  59. package/domains/content/locales/en/.agents/workflows/quick-post.md +0 -1
  60. package/domains/content/locales/en/.agents/workflows/start-content.md +0 -1
  61. package/domains/content/locales/en/agents/conductor.md +1 -2
  62. package/domains/content/pipeline.yaml +8 -8
  63. package/domains/development/.agents/skills/handoff/SKILL.md +276 -276
  64. package/domains/development/.agents/skills/lava-flow-legacy-detection/SKILL.md +197 -197
  65. package/domains/development/.agents/skills/mcp-integration/SKILL.md +211 -211
  66. package/domains/development/.agents/skills/qa-test-data-management/SKILL.md +250 -250
  67. package/domains/development/.agents/workflows/bugfix.md +16 -82
  68. package/domains/development/.agents/workflows/hotfix.md +16 -66
  69. package/domains/development/.agents/workflows/pipeline-rules.md +49 -132
  70. package/domains/development/.agents/workflows/start-task.md +17 -121
  71. package/domains/development/AGENTS.md +8 -3
  72. package/domains/development/agents/architect.md +247 -247
  73. package/domains/development/agents/conductor.md +363 -363
  74. package/domains/development/agents/devops.md +297 -297
  75. package/domains/development/agents/reviewer.md +293 -293
  76. package/domains/development/agents/senior_full_stack.md +295 -295
  77. package/domains/development/agents/tester.md +395 -395
  78. package/domains/development/locales/en/.agents/skills/handoff/SKILL.md +276 -276
  79. package/domains/development/locales/en/.agents/skills/lava-flow-legacy-detection/SKILL.md +197 -197
  80. package/domains/development/locales/en/.agents/skills/mcp-integration/SKILL.md +211 -211
  81. package/domains/development/locales/en/.agents/skills/qa-test-data-management/SKILL.md +250 -250
  82. package/domains/development/locales/en/.agents/workflows/bugfix.md +16 -82
  83. package/domains/development/locales/en/.agents/workflows/hotfix.md +15 -65
  84. package/domains/development/locales/en/.agents/workflows/pipeline-rules.md +48 -131
  85. package/domains/development/locales/en/.agents/workflows/start-task.md +17 -121
  86. package/domains/development/locales/en/AGENTS.md +15 -0
  87. package/domains/development/locales/en/agents/architect.md +247 -247
  88. package/domains/development/locales/en/agents/conductor.md +363 -363
  89. package/domains/development/locales/en/agents/devops.md +297 -297
  90. package/domains/development/locales/en/agents/reviewer.md +293 -293
  91. package/domains/development/locales/en/agents/senior_full_stack.md +295 -295
  92. package/domains/development/locales/en/agents/tester.md +395 -395
  93. package/domains/development/locales/en/prompt-examples.md +34 -120
  94. package/domains/development/pipeline.yaml +150 -135
  95. package/domains/development/prompt-examples.md +33 -119
  96. package/domains/product/.agents/workflows/product-pipeline-rules.md +13 -2
  97. package/domains/product/.agents/workflows/quick-pm.md +1 -1
  98. package/domains/product/.agents/workflows/shape-prioritize.md +1 -0
  99. package/domains/product/.agents/workflows/ship-right-thing.md +1 -0
  100. package/domains/product/.agents/workflows/spec.md +1 -0
  101. package/domains/product/agents/tech_lead.md +1 -1
  102. package/domains/product/locales/en/.agents/workflows/product-pipeline-rules.md +13 -2
  103. package/domains/product/locales/en/.agents/workflows/quick-pm.md +1 -1
  104. package/domains/product/locales/en/.agents/workflows/shape-prioritize.md +1 -0
  105. package/domains/product/locales/en/.agents/workflows/ship-right-thing.md +1 -0
  106. package/domains/product/locales/en/.agents/workflows/spec.md +1 -0
  107. package/domains/product/locales/en/agents/conductor.md +2 -2
  108. package/domains/product/locales/en/agents/data_analyst.md +2 -1
  109. package/domains/product/locales/en/agents/designer.md +2 -1
  110. package/domains/product/locales/en/agents/discovery.md +2 -1
  111. package/domains/product/locales/en/agents/layouter.md +2 -1
  112. package/domains/product/locales/en/agents/mediator.md +2 -1
  113. package/domains/product/locales/en/agents/pm.md +2 -1
  114. package/domains/product/locales/en/agents/product_strategist.md +2 -1
  115. package/domains/product/locales/en/agents/tech_lead.md +3 -2
  116. package/domains/product/locales/en/agents/ux_designer.md +2 -1
  117. package/domains/product/pipeline.yaml +12 -12
  118. package/package.json +5 -5
  119. package/domains/analytics/CONTEXT.md +0 -25
  120. package/domains/analytics/locales/en/CONTEXT.md +0 -25
  121. package/domains/content/CONTEXT.md +0 -19
  122. package/domains/content/locales/en/CONTEXT.md +0 -19
  123. package/domains/development/.agents/workflows/auto-restart-containers.md +0 -56
  124. package/domains/development/CONTEXT.md +0 -62
  125. package/domains/development/locales/en/.agents/workflows/auto-restart-containers.md +0 -24
  126. package/domains/development/locales/en/CONTEXT.md +0 -62
  127. package/domains/product/CONTEXT.md +0 -40
  128. package/domains/product/locales/en/CONTEXT.md +0 -40
@@ -2,16 +2,26 @@ import type { DomainId } from "./shared/index.js";
2
2
  /**
3
3
  * MCP auto-setup for `--target=claude` installs.
4
4
  *
5
- * Two responsibilities (ADR-DEV-099):
5
+ * Responsibilities:
6
6
  * 1. Detect / install MemPalace as an opt-in mirror for decision storage.
7
- * 2. Write project-level `.mcp.json` registering `code-ai-mcp` (always) and
8
- * `mempalace-mcp` (when accepted) so Claude Code picks them up automatically.
9
- * 3. Write `.code-ai/config.json` so `code-ai-mcp` knows which backend to use.
7
+ * 2. Register `code-ai-mcp` (always) and `mempalace` (when accepted) in
8
+ * Claude Code's USER (global) scope via `claude mcp add --scope user`,
9
+ * so the servers are available across all the user's projects.
10
+ * 3. Write `.code-ai/config.json` so `code-ai-mcp` knows which backend +
11
+ * domain to use. This stays PROJECT-local — the global server reads it
12
+ * from the project cwd at runtime.
13
+ *
14
+ * Why user scope (not a project `.mcp.json`): MCP servers are tools the user
15
+ * wants everywhere, not per-project copies. Writing a project `.mcp.json` both
16
+ * scattered `code-ai-mcp` per-project and duplicated an already-global
17
+ * `mempalace` into the project. Registration is idempotent — a server already
18
+ * present in user scope is left untouched.
10
19
  *
11
20
  * Non-Claude targets skip this whole flow — MCP is Claude-specific.
12
21
  *
13
- * Graceful degradation: every step is best-effort. Failures fall back to
14
- * "JsonlStore only" without breaking the rest of the install.
22
+ * Graceful degradation: every step is best-effort. If the `claude` CLI is not
23
+ * on PATH we never touch the user's config by hand — we print the exact
24
+ * `claude mcp add` commands to run manually instead.
15
25
  */
16
26
  export type PythonRuntimeTool = "uv" | "pipx" | "pip";
17
27
  export interface PythonRuntime {
@@ -25,15 +35,22 @@ export interface McpServerEntry {
25
35
  args: string[];
26
36
  env?: Record<string, string>;
27
37
  }
28
- export interface McpJsonShape {
29
- mcpServers?: Record<string, McpServerEntry>;
30
- [key: string]: unknown;
38
+ /**
39
+ * Minimal `claude` CLI surface this module needs. Injectable so tests can
40
+ * assert the constructed argv without spawning the real CLI.
41
+ */
42
+ export interface ClaudeCli {
43
+ /** Run `claude <args>`; resolve { ok, output } (combined stdout+stderr). */
44
+ run(args: string[]): Promise<{
45
+ ok: boolean;
46
+ output: string;
47
+ }>;
31
48
  }
32
49
  export interface McpSetupOptions {
33
50
  destinationDir: string;
34
51
  /** User's answer to the MemPalace prompt (true = wants it). */
35
52
  wantMempalace: boolean;
36
- /** When true, no files are written; the report still reflects planned actions. */
53
+ /** When true, no commands run and no files are written; report reflects plan. */
37
54
  dryRun: boolean;
38
55
  /**
39
56
  * The selected domain, written into `.code-ai/config.json` so the gate tools
@@ -47,19 +64,29 @@ export interface McpSetupReport {
47
64
  mempalaceInstallAttempted: boolean;
48
65
  mempalaceInstallSucceeded: boolean;
49
66
  pythonRuntime: PythonRuntime | null;
50
- mcpJsonPath: string;
51
- mcpJsonAction: "created" | "merged" | "skipped";
67
+ /** Was the `claude` CLI found on PATH? When false, registration falls back to manual instructions. */
68
+ claudeCliAvailable: boolean;
69
+ /** How servers were registered. */
70
+ registration: "user-scope" | "manual-fallback";
71
+ /** Server names freshly added to user scope this run. */
72
+ serversRegistered: string[];
73
+ /** Server names already present in user scope (left untouched). */
74
+ serversAlreadyPresent: string[];
75
+ /** Server names whose registration failed. */
76
+ serversFailed: string[];
52
77
  configPath: string;
53
78
  notices: string[];
54
79
  }
55
80
  /**
56
81
  * Try `mempalace-mcp --help` — the dedicated MCP-server bin, which is exactly
57
- * what we register in `.mcp.json`. Resolves true on exit code 0, false otherwise
58
- * (including ENOENT — bin not on PATH). Probing the actual server bin (not the
59
- * `mempalace` CLI) means a stale MemPalace without `mempalace-mcp` triggers a
60
- * (re)install instead of writing a config that can't launch.
82
+ * what we register. Resolves true on exit code 0, false otherwise (including
83
+ * ENOENT — bin not on PATH). Probing the actual server bin (not the `mempalace`
84
+ * CLI) means a stale MemPalace without `mempalace-mcp` triggers a (re)install
85
+ * instead of registering a config that can't launch.
61
86
  */
62
87
  export declare function detectMemPalace(): Promise<boolean>;
88
+ /** True when the `claude` CLI is on PATH (probed via `claude --version`). */
89
+ export declare function detectClaudeCli(cli?: ClaudeCli): Promise<boolean>;
63
90
  /**
64
91
  * Find the first available Python install runtime, in preference order:
65
92
  * uv → pipx → pip. Returns null if none available.
@@ -75,21 +102,24 @@ export declare function installMemPalace(runtime: PythonRuntime): Promise<{
75
102
  output: string;
76
103
  }>;
77
104
  /**
78
- * Merge MCP server entries into `<destinationDir>/.mcp.json`.
79
- * Behaviour:
80
- * - Missing file: create with `{ "mcpServers": <new> }`.
81
- * - Existing valid JSON: merge `mcpServers`; if a server name already exists,
82
- * keep the existing entry untouched and add a notice.
83
- * - Malformed JSON: abort (return action="skipped") with notice — never clobber.
105
+ * Build the argv for `claude mcp add --scope user <name> -- <command> [args]`.
106
+ * Pure — exported for testing. Env vars become `-e KEY=VALUE` flags placed
107
+ * before the server name (as `claude mcp add` expects options first).
84
108
  */
85
- export declare function mergeMcpJson(destinationDir: string, servers: Record<string, McpServerEntry>, dryRun?: boolean): Promise<{
86
- action: McpSetupReport["mcpJsonAction"];
87
- path: string;
88
- notices: string[];
89
- }>;
109
+ export declare function buildClaudeAddArgs(name: string, entry: McpServerEntry): string[];
110
+ /** Build the argv for `claude mcp remove --scope user <name>`. Pure — exported for testing. */
111
+ export declare function buildClaudeRemoveArgs(name: string): string[];
112
+ /**
113
+ * Is `name` already registered in Claude's USER scope? Uses `claude mcp get`,
114
+ * whose output prints `Scope: User config` for user-scope servers. A server in
115
+ * a different scope (local/project) returns false here — we still want it in
116
+ * user scope.
117
+ */
118
+ export declare function isRegisteredInUserScope(cli: ClaudeCli, name: string): Promise<boolean>;
90
119
  /**
91
120
  * Write `<destinationDir>/.code-ai/config.json` with the chosen backend.
92
121
  * Idempotent — re-running overwrites with the same content if backend unchanged.
122
+ * Stays project-local: the user-scope `code-ai-mcp` reads it from the cwd.
93
123
  */
94
124
  export declare function writeCodeAiConfig(destinationDir: string, config: {
95
125
  decision_store: "jsonl" | "mempalace";
@@ -103,10 +133,33 @@ export declare function writeCodeAiConfig(destinationDir: string, config: {
103
133
  * Order:
104
134
  * 1. If user wants MemPalace: detect → install if absent → fall back if install fails.
105
135
  * 2. Build server entries (code-ai-mcp always; mempalace only when usable).
106
- * 3. Merge into .mcp.json.
107
- * 4. Write .code-ai/config.json with backend choice.
136
+ * 3. Register them in Claude USER scope via `claude mcp add --scope user`
137
+ * (idempotent; skip if already present). If `claude` CLI is absent, emit
138
+ * manual commands instead of touching the config by hand.
139
+ * 4. Write project-local `.code-ai/config.json` with the backend choice.
108
140
  *
109
141
  * Reports all decisions in `McpSetupReport.notices` so callers can surface
110
- * them to the user verbatim.
142
+ * them to the user verbatim. `cli` is injectable for testing.
111
143
  */
112
- export declare function setupMcp(opts: McpSetupOptions): Promise<McpSetupReport>;
144
+ export declare function setupMcp(opts: McpSetupOptions, cli?: ClaudeCli): Promise<McpSetupReport>;
145
+ export interface McpTeardownReport {
146
+ claudeCliAvailable: boolean;
147
+ removal: "user-scope" | "manual-fallback";
148
+ serversRemoved: string[];
149
+ serversNotPresent: string[];
150
+ serversFailed: string[];
151
+ notices: string[];
152
+ }
153
+ /**
154
+ * Remove the installer-owned MCP server(s) from Claude's USER scope via
155
+ * `claude mcp remove --scope user`. Idempotent — a server that isn't registered
156
+ * is reported as "nothing to remove". If the `claude` CLI is absent, prints the
157
+ * manual commands instead of touching the config. `cli` is injectable for tests.
158
+ *
159
+ * NOTE: the registration is global (shared across all projects), so this removes
160
+ * code-ai-mcp for every project. That is the chosen behaviour (symmetric with
161
+ * install); a multi-project user re-runs the installer to restore it.
162
+ */
163
+ export declare function teardownMcp(opts: {
164
+ dryRun: boolean;
165
+ }, cli?: ClaudeCli): Promise<McpTeardownReport>;
package/dist/mcp_setup.js CHANGED
@@ -1,16 +1,24 @@
1
1
  import { spawn } from "node:child_process";
2
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
+ const defaultClaudeCli = {
5
+ run: (args) => spawnCapture("claude", args),
6
+ };
4
7
  /**
5
8
  * Try `mempalace-mcp --help` — the dedicated MCP-server bin, which is exactly
6
- * what we register in `.mcp.json`. Resolves true on exit code 0, false otherwise
7
- * (including ENOENT — bin not on PATH). Probing the actual server bin (not the
8
- * `mempalace` CLI) means a stale MemPalace without `mempalace-mcp` triggers a
9
- * (re)install instead of writing a config that can't launch.
9
+ * what we register. Resolves true on exit code 0, false otherwise (including
10
+ * ENOENT — bin not on PATH). Probing the actual server bin (not the `mempalace`
11
+ * CLI) means a stale MemPalace without `mempalace-mcp` triggers a (re)install
12
+ * instead of registering a config that can't launch.
10
13
  */
11
14
  export async function detectMemPalace() {
12
15
  return spawnExitZero("mempalace-mcp", ["--help"]);
13
16
  }
17
+ /** True when the `claude` CLI is on PATH (probed via `claude --version`). */
18
+ export async function detectClaudeCli(cli = defaultClaudeCli) {
19
+ const res = await cli.run(["--version"]);
20
+ return res.ok;
21
+ }
14
22
  /**
15
23
  * Find the first available Python install runtime, in preference order:
16
24
  * uv → pipx → pip. Returns null if none available.
@@ -48,49 +56,60 @@ function installCommandArgs(runtime) {
48
56
  }
49
57
  }
50
58
  /**
51
- * Merge MCP server entries into `<destinationDir>/.mcp.json`.
52
- * Behaviour:
53
- * - Missing file: create with `{ "mcpServers": <new> }`.
54
- * - Existing valid JSON: merge `mcpServers`; if a server name already exists,
55
- * keep the existing entry untouched and add a notice.
56
- * - Malformed JSON: abort (return action="skipped") with notice — never clobber.
59
+ * Build the argv for `claude mcp add --scope user <name> -- <command> [args]`.
60
+ * Pure — exported for testing. Env vars become `-e KEY=VALUE` flags placed
61
+ * before the server name (as `claude mcp add` expects options first).
57
62
  */
58
- export async function mergeMcpJson(destinationDir, servers, dryRun = false) {
59
- const path = join(destinationDir, ".mcp.json");
60
- const notices = [];
61
- const existingRaw = await readFile(path, "utf8").catch(() => null);
62
- if (existingRaw === null) {
63
- if (!dryRun) {
64
- await mkdir(destinationDir, { recursive: true });
65
- await writeFile(path, JSON.stringify({ mcpServers: servers }, null, 2) + "\n", "utf8");
66
- }
67
- return { action: "created", path, notices };
68
- }
69
- let parsed;
70
- try {
71
- parsed = JSON.parse(existingRaw);
72
- }
73
- catch {
74
- notices.push(`Existing .mcp.json is not valid JSON skipped to avoid clobbering it. Path: ${path}`);
75
- return { action: "skipped", path, notices };
76
- }
77
- const merged = { ...(parsed.mcpServers ?? {}) };
78
- for (const [name, entry] of Object.entries(servers)) {
79
- if (name in merged) {
80
- notices.push(`.mcp.json already has server '${name}' — kept existing entry.`);
81
- continue;
82
- }
83
- merged[name] = entry;
63
+ export function buildClaudeAddArgs(name, entry) {
64
+ const envFlags = entry.env
65
+ ? Object.entries(entry.env).flatMap(([k, v]) => ["-e", `${k}=${v}`])
66
+ : [];
67
+ return ["mcp", "add", ...envFlags, "--scope", "user", name, "--", entry.command, ...entry.args];
68
+ }
69
+ /** Build the argv for `claude mcp remove --scope user <name>`. Pure — exported for testing. */
70
+ export function buildClaudeRemoveArgs(name) {
71
+ return ["mcp", "remove", "--scope", "user", name];
72
+ }
73
+ /** Human-readable manual command, for the no-CLI fallback notice. */
74
+ function manualAddCommand(name, entry) {
75
+ return `claude ${buildClaudeAddArgs(name, entry).join(" ")}`;
76
+ }
77
+ /**
78
+ * Is `name` already registered in Claude's USER scope? Uses `claude mcp get`,
79
+ * whose output prints `Scope: User config` for user-scope servers. A server in
80
+ * a different scope (local/project) returns false here — we still want it in
81
+ * user scope.
82
+ */
83
+ export async function isRegisteredInUserScope(cli, name) {
84
+ const res = await cli.run(["mcp", "get", name]);
85
+ return res.ok && /Scope:\s*User config/i.test(res.output);
86
+ }
87
+ /**
88
+ * Register one server in user scope, idempotently. Skips if already present in
89
+ * user scope. Never throws — returns the outcome + a notice for the report.
90
+ */
91
+ async function addServerToUserScope(cli, name, entry) {
92
+ if (await isRegisteredInUserScope(cli, name)) {
93
+ return {
94
+ outcome: "already-present",
95
+ notice: `MCP server '${name}' already registered in user scope — left untouched.`,
96
+ };
84
97
  }
85
- parsed.mcpServers = merged;
86
- if (!dryRun) {
87
- await writeFile(path, JSON.stringify(parsed, null, 2) + "\n", "utf8");
98
+ const res = await cli.run(buildClaudeAddArgs(name, entry));
99
+ if (res.ok) {
100
+ return { outcome: "registered", notice: `Registered MCP server '${name}' in user scope (global).` };
88
101
  }
89
- return { action: "merged", path, notices };
102
+ return {
103
+ outcome: "failed",
104
+ notice: `Failed to register '${name}' in user scope. Run it manually:\n` +
105
+ ` ${manualAddCommand(name, entry)}\n` +
106
+ ` Output:\n${res.output}`,
107
+ };
90
108
  }
91
109
  /**
92
110
  * Write `<destinationDir>/.code-ai/config.json` with the chosen backend.
93
111
  * Idempotent — re-running overwrites with the same content if backend unchanged.
112
+ * Stays project-local: the user-scope `code-ai-mcp` reads it from the cwd.
94
113
  */
95
114
  export async function writeCodeAiConfig(destinationDir, config, dryRun = false) {
96
115
  const dir = join(destinationDir, ".code-ai");
@@ -101,19 +120,43 @@ export async function writeCodeAiConfig(destinationDir, config, dryRun = false)
101
120
  }
102
121
  return { path };
103
122
  }
123
+ /**
124
+ * Build the server entries to register. `code-ai-mcp` always; `mempalace` only
125
+ * when usable.
126
+ */
127
+ function buildServerEntries(mempalaceUsed) {
128
+ // DEV-100 consolidation: the code-ai-mcp bin lives inside the
129
+ // `code-ai-installer` npm package. `npx -p <package> <bin>` tells npm to
130
+ // install that package and run the named bin from it.
131
+ const servers = {
132
+ "code-ai-mcp": {
133
+ command: "npx",
134
+ args: ["-y", "-p", "code-ai-installer", "code-ai-mcp"],
135
+ },
136
+ };
137
+ if (mempalaceUsed) {
138
+ // Use the dedicated `mempalace-mcp` stdio server bin (NOT `mempalace mcp`):
139
+ // the full CLI subcommand can emit to stdout before the MCP handshake and
140
+ // corrupt the JSON-RPC stream, so Claude Code fails to launch it.
141
+ servers["mempalace"] = { command: "mempalace-mcp", args: [] };
142
+ }
143
+ return servers;
144
+ }
104
145
  /**
105
146
  * End-to-end orchestrator. Called from the install flow when `target=claude`.
106
147
  *
107
148
  * Order:
108
149
  * 1. If user wants MemPalace: detect → install if absent → fall back if install fails.
109
150
  * 2. Build server entries (code-ai-mcp always; mempalace only when usable).
110
- * 3. Merge into .mcp.json.
111
- * 4. Write .code-ai/config.json with backend choice.
151
+ * 3. Register them in Claude USER scope via `claude mcp add --scope user`
152
+ * (idempotent; skip if already present). If `claude` CLI is absent, emit
153
+ * manual commands instead of touching the config by hand.
154
+ * 4. Write project-local `.code-ai/config.json` with the backend choice.
112
155
  *
113
156
  * Reports all decisions in `McpSetupReport.notices` so callers can surface
114
- * them to the user verbatim.
157
+ * them to the user verbatim. `cli` is injectable for testing.
115
158
  */
116
- export async function setupMcp(opts) {
159
+ export async function setupMcp(opts, cli = defaultClaudeCli) {
117
160
  const notices = [];
118
161
  let mempalaceUsed = false;
119
162
  let mempalaceInstallAttempted = false;
@@ -144,26 +187,39 @@ export async function setupMcp(opts) {
144
187
  }
145
188
  }
146
189
  }
147
- // DEV-100 consolidation: the code-ai-mcp bin lives inside the
148
- // `code-ai-installer` npm package. `npx -p <package> <bin>` tells npm to
149
- // install that package and run the named bin from it.
150
- const servers = {
151
- "code-ai-mcp": {
152
- command: "npx",
153
- args: ["-y", "-p", "code-ai-installer", "code-ai-mcp"],
154
- },
155
- };
156
- if (mempalaceUsed) {
157
- // Use the dedicated `mempalace-mcp` stdio server bin (NOT `mempalace mcp`):
158
- // the full CLI subcommand can emit to stdout before the MCP handshake and
159
- // corrupt the JSON-RPC stream, so Claude Code fails to launch it.
160
- servers["mempalace"] = {
161
- command: "mempalace-mcp",
162
- args: [],
163
- };
190
+ const servers = buildServerEntries(mempalaceUsed);
191
+ const serversRegistered = [];
192
+ const serversAlreadyPresent = [];
193
+ const serversFailed = [];
194
+ const claudeCliAvailable = !opts.dryRun && (await detectClaudeCli(cli));
195
+ let registration;
196
+ if (opts.dryRun) {
197
+ registration = "user-scope";
198
+ for (const [name, entry] of Object.entries(servers)) {
199
+ notices.push(`Would register MCP server '${name}' in user scope: ${manualAddCommand(name, entry)}`);
200
+ }
201
+ }
202
+ else if (claudeCliAvailable) {
203
+ registration = "user-scope";
204
+ for (const [name, entry] of Object.entries(servers)) {
205
+ const { outcome, notice } = await addServerToUserScope(cli, name, entry);
206
+ notices.push(notice);
207
+ if (outcome === "registered")
208
+ serversRegistered.push(name);
209
+ else if (outcome === "already-present")
210
+ serversAlreadyPresent.push(name);
211
+ else
212
+ serversFailed.push(name);
213
+ }
214
+ }
215
+ else {
216
+ registration = "manual-fallback";
217
+ notices.push("The `claude` CLI was not found on PATH — not modifying your global config automatically. " +
218
+ "Register the MCP server(s) in user scope by running:");
219
+ for (const [name, entry] of Object.entries(servers)) {
220
+ notices.push(` ${manualAddCommand(name, entry)}`);
221
+ }
164
222
  }
165
- const mcp = await mergeMcpJson(opts.destinationDir, servers, opts.dryRun);
166
- notices.push(...mcp.notices);
167
223
  const cfg = await writeCodeAiConfig(opts.destinationDir, {
168
224
  decision_store: mempalaceUsed ? "mempalace" : "jsonl",
169
225
  ...(opts.domain ? { domain: opts.domain } : {}),
@@ -173,12 +229,72 @@ export async function setupMcp(opts) {
173
229
  mempalaceInstallAttempted,
174
230
  mempalaceInstallSucceeded,
175
231
  pythonRuntime,
176
- mcpJsonPath: mcp.path,
177
- mcpJsonAction: mcp.action,
232
+ claudeCliAvailable,
233
+ registration,
234
+ serversRegistered,
235
+ serversAlreadyPresent,
236
+ serversFailed,
178
237
  configPath: cfg.path,
179
238
  notices,
180
239
  };
181
240
  }
241
+ // ─── Teardown (uninstall) ───────────────────────────────────────────────────
242
+ /**
243
+ * Servers the installer registers and is therefore responsible for removing on
244
+ * uninstall. `mempalace` is intentionally absent — it is the user's own memory
245
+ * server and may predate code-ai, so uninstall never touches it.
246
+ */
247
+ const INSTALLER_OWNED_SERVERS = ["code-ai-mcp"];
248
+ /**
249
+ * Remove the installer-owned MCP server(s) from Claude's USER scope via
250
+ * `claude mcp remove --scope user`. Idempotent — a server that isn't registered
251
+ * is reported as "nothing to remove". If the `claude` CLI is absent, prints the
252
+ * manual commands instead of touching the config. `cli` is injectable for tests.
253
+ *
254
+ * NOTE: the registration is global (shared across all projects), so this removes
255
+ * code-ai-mcp for every project. That is the chosen behaviour (symmetric with
256
+ * install); a multi-project user re-runs the installer to restore it.
257
+ */
258
+ export async function teardownMcp(opts, cli = defaultClaudeCli) {
259
+ const notices = [];
260
+ const serversRemoved = [];
261
+ const serversNotPresent = [];
262
+ const serversFailed = [];
263
+ if (opts.dryRun) {
264
+ for (const name of INSTALLER_OWNED_SERVERS) {
265
+ notices.push(`Would remove MCP server '${name}' from user scope: claude ${buildClaudeRemoveArgs(name).join(" ")}`);
266
+ }
267
+ return { claudeCliAvailable: false, removal: "user-scope", serversRemoved, serversNotPresent, serversFailed, notices };
268
+ }
269
+ const claudeCliAvailable = await detectClaudeCli(cli);
270
+ if (!claudeCliAvailable) {
271
+ notices.push("The `claude` CLI was not found on PATH — not modifying your global config automatically. " +
272
+ "Remove the MCP server(s) from user scope by running:");
273
+ for (const name of INSTALLER_OWNED_SERVERS) {
274
+ notices.push(` claude ${buildClaudeRemoveArgs(name).join(" ")}`);
275
+ }
276
+ return { claudeCliAvailable, removal: "manual-fallback", serversRemoved, serversNotPresent, serversFailed, notices };
277
+ }
278
+ for (const name of INSTALLER_OWNED_SERVERS) {
279
+ if (!(await isRegisteredInUserScope(cli, name))) {
280
+ serversNotPresent.push(name);
281
+ notices.push(`MCP server '${name}' is not registered in user scope — nothing to remove.`);
282
+ continue;
283
+ }
284
+ const res = await cli.run(buildClaudeRemoveArgs(name));
285
+ if (res.ok) {
286
+ serversRemoved.push(name);
287
+ notices.push(`Removed MCP server '${name}' from user scope.`);
288
+ }
289
+ else {
290
+ serversFailed.push(name);
291
+ notices.push(`Failed to remove '${name}' from user scope. Run it manually:\n` +
292
+ ` claude ${buildClaudeRemoveArgs(name).join(" ")}\n` +
293
+ ` Output:\n${res.output}`);
294
+ }
295
+ }
296
+ return { claudeCliAvailable, removal: "user-scope", serversRemoved, serversNotPresent, serversFailed, notices };
297
+ }
182
298
  // ─── Subprocess helpers ─────────────────────────────────────────────────────
183
299
  async function spawnExitZero(command, args) {
184
300
  return new Promise((resolve) => {
@@ -15,7 +15,8 @@ const targetLayouts = {
15
15
  agentsDir: ".claude/agents",
16
16
  skillsDir: ".claude/skills",
17
17
  workflowsDir: ".claude/workflows",
18
- notes: "Uses CLAUDE.md and local .claude folder for role/skill docs.",
18
+ commandsDir: ".claude/commands",
19
+ notes: "Uses CLAUDE.md and local .claude folder for role/skill docs; command-type workflows install as slash commands under .claude/commands.",
19
20
  },
20
21
  "qwen-3.5": {
21
22
  instructionFile: "QWEN.md",
@@ -192,15 +193,7 @@ function planForLayout(layout, catalog, destinationDir, selectedAgents, selected
192
193
  target,
193
194
  });
194
195
  }
195
- if (layout.workflowsDir) {
196
- for (const workflowPath of Object.values(catalog.workflowFiles)) {
197
- operations.push({
198
- sourcePath: workflowPath,
199
- destinationPath: path.join(destinationDir, layout.workflowsDir, path.basename(workflowPath)),
200
- generated: false,
201
- });
202
- }
203
- }
196
+ appendWorkflowOperations(operations, layout, catalog, destinationDir, target);
204
197
  appendExtraFileOperations(operations, catalog, destinationDir);
205
198
  return operations;
206
199
  }
@@ -288,18 +281,60 @@ function planForGeminiLayout(layout, catalog, destinationDir, selectedAgents, se
288
281
  target,
289
282
  });
290
283
  }
291
- if (layout.workflowsDir) {
292
- for (const workflowPath of Object.values(catalog.workflowFiles)) {
293
- operations.push({
294
- sourcePath: workflowPath,
295
- destinationPath: path.join(destinationDir, layout.workflowsDir, path.basename(workflowPath)),
296
- generated: false,
297
- });
298
- }
299
- }
284
+ appendWorkflowOperations(operations, layout, catalog, destinationDir, target);
300
285
  appendExtraFileOperations(operations, catalog, destinationDir);
301
286
  return operations;
302
287
  }
288
+ /**
289
+ * Returns true when a workflow file is a user-invokable command (installed as a
290
+ * slash command on platforms that expose a commands directory), false for
291
+ * pipeline rule / reference docs that stay in the workflows directory.
292
+ *
293
+ * Convention-based classifier (no source-file metadata): files ending in
294
+ * `pipeline-rules.md` and the implicit `auto-restart-containers.md` rule are
295
+ * NOT commands; everything else is. Fail-safe — an unrecognised name defaults
296
+ * to a command so it stays visible under commands/ rather than being hidden.
297
+ * @param fileName Workflow file base name.
298
+ * @returns True when the file is a command, false for a rule/reference doc.
299
+ */
300
+ function isCommandWorkflow(fileName) {
301
+ const lower = fileName.toLowerCase();
302
+ if (lower.endsWith("pipeline-rules.md")) {
303
+ return false;
304
+ }
305
+ if (lower === "auto-restart-containers.md") {
306
+ return false;
307
+ }
308
+ return true;
309
+ }
310
+ /**
311
+ * Appends workflow copy operations, routing command-type files to the platform
312
+ * commands directory (when the layout defines one) and rule / reference docs to
313
+ * the workflows directory. Platforms without a commands directory keep all
314
+ * workflow files in the workflows directory (unchanged behaviour).
315
+ * @param operations Mutable operations list.
316
+ * @param layout Platform layout.
317
+ * @param catalog Source catalog with workflowFiles.
318
+ * @param destinationDir Destination root.
319
+ */
320
+ function appendWorkflowOperations(operations, layout, catalog, destinationDir, target) {
321
+ for (const workflowPath of Object.values(catalog.workflowFiles)) {
322
+ const fileName = path.basename(workflowPath);
323
+ const targetDir = layout.commandsDir && isCommandWorkflow(fileName) ? layout.commandsDir : layout.workflowsDir;
324
+ if (!targetDir) {
325
+ continue;
326
+ }
327
+ operations.push({
328
+ sourcePath: workflowPath,
329
+ destinationPath: path.join(destinationDir, targetDir, fileName),
330
+ generated: false,
331
+ transform: {
332
+ target,
333
+ assetType: "workflow",
334
+ },
335
+ });
336
+ }
337
+ }
303
338
  /**
304
339
  * Appends copy operations for extra root-level files (e.g. prompt-examples.md).
305
340
  * @param operations Mutable operations list.
@@ -61,7 +61,7 @@ export const SkillFrontmatter = z.object({
61
61
  * n8n-pinecone-qdrant-supabase-reference). Workflows may use tactical per-skill raises
62
62
  * up to 400 (precedent ADR-DEV-021 raised k8s-manifests-conventions workflow to 350).
63
63
  * Raised to 800 in ADR-DEV-034 for state-zustand-beast-practices-reference (750)
64
- * — densest examples skill in codebase, DEN-locked rule prevents trim. */
64
+ * — densest examples skill in codebase, the user-mandated cap prevents trim. */
65
65
  budget_lines: z.number().int().positive().max(SKILL_BUDGET_LINES_MAX).default(300),
66
66
  /** Frontmatter version. Bumped when SkillFrontmatter shape changes. */
67
67
  schema_version: z.literal(1).default(1),
@@ -7,7 +7,7 @@ import { z } from "zod";
7
7
  * - persona-user-{handle}.md is local-only (gitignored, frontmatter flag, files-whitelist excluded)
8
8
  * - the merged persona is what `load_role` returns to SessionStart
9
9
  *
10
- * Provenance per property is mandatory (DEN: "must be visible where each rule came from").
10
+ * Provenance per property is mandatory (the user: "must be visible where each rule came from").
11
11
  */
12
12
  /**
13
13
  * Provenance pointer — where a persona property was derived from.
@@ -7,7 +7,7 @@ import { z } from "zod";
7
7
  * - persona-user-{handle}.md is local-only (gitignored, frontmatter flag, files-whitelist excluded)
8
8
  * - the merged persona is what `load_role` returns to SessionStart
9
9
  *
10
- * Provenance per property is mandatory (DEN: "must be visible where each rule came from").
10
+ * Provenance per property is mandatory (the user: "must be visible where each rule came from").
11
11
  */
12
12
  /**
13
13
  * Provenance pointer — where a persona property was derived from.