code-ai-installer 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/mcp_setup.d.ts +79 -48
- package/dist/mcp_setup.js +215 -107
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ Every domain also carries a meta **Auditor** beside the pipeline. Each skill dec
|
|
|
48
48
|
|
|
49
49
|
## 🚪 The MCP Gate Pipeline
|
|
50
50
|
|
|
51
|
-
When you install for **Claude**, the installer registers `code-ai-mcp` in your global (user-scope) Claude config
|
|
51
|
+
When you install for **Claude**, the installer registers `code-ai-mcp` in your global (user-scope) Claude config — it adds an entry to the `mcpServers` object of `~/.claude.json` (idempotent — a server already present is left untouched, so an existing global `mempalace` is never duplicated). From then on, the agents drive the work *through the server*, not by free-form prompting:
|
|
52
52
|
|
|
53
53
|
1. **`current_gate` / `classify_gate`** — the conductor reads where the run is and classifies the gate outcome (`auto_resolve` / `fork` / `exception`).
|
|
54
54
|
2. **`get_skill`** — the active role fetches the skills it owns at this gate (the fetch is logged as telemetry for the Auditor).
|
|
@@ -151,7 +151,7 @@ Depending on `--target`, `code-ai` restructures your project:
|
|
|
151
151
|
1. **Orchestration entry** — `AGENTS.md` (plus tool aliases like `CLAUDE.md`, `CODEX.md`, `GEMINI.md`, `QWEN.md`, `KIMI.md`).
|
|
152
152
|
2. **Agents** — roles copied into the target's layout (e.g. `.claude/agents/<role>.md`, `.github/copilot-instructions.md`).
|
|
153
153
|
3. **Skills & workflows** — the execution skillsets, with per-tool metadata sidecars.
|
|
154
|
-
4. **MCP wiring (Claude only)** — registers `code-ai-mcp` (run as `npx -p code-ai-installer code-ai-mcp`) in your global (user-scope) Claude config
|
|
154
|
+
4. **MCP wiring (Claude only)** — registers `code-ai-mcp` (run as `npx -p code-ai-installer code-ai-mcp`) in your global (user-scope) Claude config by adding it to the `mcpServers` object of `~/.claude.json` (backed up first, written atomically, idempotent), plus an optional [MemPalace](https://www.npmjs.com/package/mempalace) memory server when present, and a project-local `.code-ai/config.json` that records the active domain and decision-store backend. If `~/.claude.json` can't be read or written, the installer prints the exact JSON entries to add by hand instead of guessing.
|
|
155
155
|
|
|
156
156
|
---
|
|
157
157
|
|
package/dist/mcp_setup.d.ts
CHANGED
|
@@ -5,23 +5,33 @@ import type { DomainId } from "./shared/index.js";
|
|
|
5
5
|
* Responsibilities:
|
|
6
6
|
* 1. Detect / install MemPalace as an opt-in mirror for decision storage.
|
|
7
7
|
* 2. Register `code-ai-mcp` (always) and `mempalace` (when accepted) in
|
|
8
|
-
* Claude Code's USER (global)
|
|
9
|
-
*
|
|
8
|
+
* Claude Code's USER (global) config so the servers are available across
|
|
9
|
+
* all the user's projects.
|
|
10
10
|
* 3. Write `.code-ai/config.json` so `code-ai-mcp` knows which backend +
|
|
11
11
|
* domain to use. This stays PROJECT-local — the global server reads it
|
|
12
12
|
* from the project cwd at runtime.
|
|
13
13
|
*
|
|
14
|
+
* Why direct edit of `~/.claude.json` (not `claude mcp add --scope user`):
|
|
15
|
+
* earlier versions registered via the CLI, but `claude mcp add` in current
|
|
16
|
+
* Claude Code rejects the `-s/--scope` flag whenever a stdio passthrough
|
|
17
|
+
* (`-- <command> ...`) is present — a commander parsing quirk — so a stdio
|
|
18
|
+
* server like `code-ai-mcp` can never be added to user scope through the CLI.
|
|
19
|
+
* The CLI is also version-unstable here. User-scope servers live as plain
|
|
20
|
+
* entries in the top-level `mcpServers` object of `~/.claude.json` (exactly
|
|
21
|
+
* where `mempalace`, `figma`, etc. already sit), so we merge there directly.
|
|
22
|
+
*
|
|
14
23
|
* Why user scope (not a project `.mcp.json`): MCP servers are tools the user
|
|
15
|
-
* wants everywhere, not per-project copies.
|
|
24
|
+
* wants everywhere, not per-project copies. A project `.mcp.json` both
|
|
16
25
|
* scattered `code-ai-mcp` per-project and duplicated an already-global
|
|
17
|
-
* `mempalace
|
|
18
|
-
*
|
|
26
|
+
* `mempalace`. Registration is idempotent — a server already present in the
|
|
27
|
+
* user config is left untouched (so a pre-existing global `mempalace` is never
|
|
28
|
+
* duplicated or rewritten).
|
|
19
29
|
*
|
|
20
30
|
* Non-Claude targets skip this whole flow — MCP is Claude-specific.
|
|
21
31
|
*
|
|
22
|
-
* Graceful degradation: every step is best-effort. If
|
|
23
|
-
*
|
|
24
|
-
*
|
|
32
|
+
* Graceful degradation: every step is best-effort. If `~/.claude.json` cannot
|
|
33
|
+
* be read or written we never guess — we print the exact JSON entries to add by
|
|
34
|
+
* hand. Writes are atomic (temp file + rename) and back up the original first.
|
|
25
35
|
*/
|
|
26
36
|
export type PythonRuntimeTool = "uv" | "pipx" | "pip";
|
|
27
37
|
export interface PythonRuntime {
|
|
@@ -35,17 +45,6 @@ export interface McpServerEntry {
|
|
|
35
45
|
args: string[];
|
|
36
46
|
env?: Record<string, string>;
|
|
37
47
|
}
|
|
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
|
-
}>;
|
|
48
|
-
}
|
|
49
48
|
export interface McpSetupOptions {
|
|
50
49
|
destinationDir: string;
|
|
51
50
|
/** User's answer to the MemPalace prompt (true = wants it). */
|
|
@@ -64,15 +63,15 @@ export interface McpSetupReport {
|
|
|
64
63
|
mempalaceInstallAttempted: boolean;
|
|
65
64
|
mempalaceInstallSucceeded: boolean;
|
|
66
65
|
pythonRuntime: PythonRuntime | null;
|
|
67
|
-
/**
|
|
68
|
-
|
|
66
|
+
/** Absolute path of the Claude user config we register servers into. */
|
|
67
|
+
userConfigPath: string;
|
|
69
68
|
/** How servers were registered. */
|
|
70
69
|
registration: "user-scope" | "manual-fallback";
|
|
71
|
-
/** Server names freshly added to user
|
|
70
|
+
/** Server names freshly added to the user config this run. */
|
|
72
71
|
serversRegistered: string[];
|
|
73
|
-
/** Server names already present in user
|
|
72
|
+
/** Server names already present in the user config (left untouched). */
|
|
74
73
|
serversAlreadyPresent: string[];
|
|
75
|
-
/** Server names whose registration failed. */
|
|
74
|
+
/** Server names whose registration failed (config unwritable). */
|
|
76
75
|
serversFailed: string[];
|
|
77
76
|
configPath: string;
|
|
78
77
|
notices: string[];
|
|
@@ -85,8 +84,6 @@ export interface McpSetupReport {
|
|
|
85
84
|
* instead of registering a config that can't launch.
|
|
86
85
|
*/
|
|
87
86
|
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>;
|
|
90
87
|
/**
|
|
91
88
|
* Find the first available Python install runtime, in preference order:
|
|
92
89
|
* uv → pipx → pip. Returns null if none available.
|
|
@@ -102,20 +99,54 @@ export declare function installMemPalace(runtime: PythonRuntime): Promise<{
|
|
|
102
99
|
output: string;
|
|
103
100
|
}>;
|
|
104
101
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
102
|
+
* The Claude user config holds user-scope MCP servers in a top-level
|
|
103
|
+
* `mcpServers` object. We only ever read it, merge our entries, and write it
|
|
104
|
+
* back — preserving every other key.
|
|
105
|
+
*/
|
|
106
|
+
export interface UserConfigIO {
|
|
107
|
+
/** Absolute path of the config file. */
|
|
108
|
+
readonly configPath: string;
|
|
109
|
+
/** Read + parse the config. `ok:false` when missing or not a JSON object. */
|
|
110
|
+
read(): Promise<{
|
|
111
|
+
ok: boolean;
|
|
112
|
+
data?: Record<string, unknown>;
|
|
113
|
+
error?: string;
|
|
114
|
+
}>;
|
|
115
|
+
/** Back up the original then atomically write `data`. */
|
|
116
|
+
writeWithBackup(data: Record<string, unknown>): Promise<{
|
|
117
|
+
ok: boolean;
|
|
118
|
+
backupPath?: string;
|
|
119
|
+
error?: string;
|
|
120
|
+
}>;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the Claude user config path. Honours `CLAUDE_CONFIG_DIR` when set,
|
|
124
|
+
* else `~/.claude.json`.
|
|
125
|
+
*/
|
|
126
|
+
export declare function userConfigPath(): string;
|
|
127
|
+
/** Real filesystem-backed UserConfigIO. Factory so tests can target a temp file. */
|
|
128
|
+
export declare function createUserConfigIO(configPath: string): UserConfigIO;
|
|
129
|
+
/**
|
|
130
|
+
* Idempotently merge `servers` into the config's top-level `mcpServers`. Pure —
|
|
131
|
+
* returns a new config object plus which names were freshly added vs already
|
|
132
|
+
* present. An existing key is NEVER overwritten (so a pre-existing global
|
|
133
|
+
* `mempalace` is preserved untouched). Exported for unit testing.
|
|
108
134
|
*/
|
|
109
|
-
export declare function
|
|
110
|
-
|
|
111
|
-
|
|
135
|
+
export declare function mergeUserScopeServers(config: Record<string, unknown>, servers: Record<string, McpServerEntry>): {
|
|
136
|
+
config: Record<string, unknown>;
|
|
137
|
+
registered: string[];
|
|
138
|
+
alreadyPresent: string[];
|
|
139
|
+
};
|
|
112
140
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* user scope.
|
|
141
|
+
* Idempotently remove `names` from the config's `mcpServers`. Pure — returns a
|
|
142
|
+
* new config object plus which names were removed vs were not present. Exported
|
|
143
|
+
* for unit testing.
|
|
117
144
|
*/
|
|
118
|
-
export declare function
|
|
145
|
+
export declare function removeUserScopeServers(config: Record<string, unknown>, names: string[]): {
|
|
146
|
+
config: Record<string, unknown>;
|
|
147
|
+
removed: string[];
|
|
148
|
+
notPresent: string[];
|
|
149
|
+
};
|
|
119
150
|
/**
|
|
120
151
|
* Write `<destinationDir>/.code-ai/config.json` with the chosen backend.
|
|
121
152
|
* Idempotent — re-running overwrites with the same content if backend unchanged.
|
|
@@ -133,17 +164,17 @@ export declare function writeCodeAiConfig(destinationDir: string, config: {
|
|
|
133
164
|
* Order:
|
|
134
165
|
* 1. If user wants MemPalace: detect → install if absent → fall back if install fails.
|
|
135
166
|
* 2. Build server entries (code-ai-mcp always; mempalace only when usable).
|
|
136
|
-
* 3.
|
|
137
|
-
*
|
|
138
|
-
*
|
|
167
|
+
* 3. Merge them into the Claude user config's `mcpServers` (idempotent; skip
|
|
168
|
+
* already-present). If the config can't be read/written, print the exact
|
|
169
|
+
* JSON to add by hand instead of guessing.
|
|
139
170
|
* 4. Write project-local `.code-ai/config.json` with the backend choice.
|
|
140
171
|
*
|
|
141
172
|
* Reports all decisions in `McpSetupReport.notices` so callers can surface
|
|
142
|
-
* them to the user verbatim. `
|
|
173
|
+
* them to the user verbatim. `io` is injectable for testing.
|
|
143
174
|
*/
|
|
144
|
-
export declare function setupMcp(opts: McpSetupOptions,
|
|
175
|
+
export declare function setupMcp(opts: McpSetupOptions, io?: UserConfigIO): Promise<McpSetupReport>;
|
|
145
176
|
export interface McpTeardownReport {
|
|
146
|
-
|
|
177
|
+
userConfigPath: string;
|
|
147
178
|
removal: "user-scope" | "manual-fallback";
|
|
148
179
|
serversRemoved: string[];
|
|
149
180
|
serversNotPresent: string[];
|
|
@@ -151,10 +182,10 @@ export interface McpTeardownReport {
|
|
|
151
182
|
notices: string[];
|
|
152
183
|
}
|
|
153
184
|
/**
|
|
154
|
-
* Remove the installer-owned MCP server(s) from Claude's
|
|
155
|
-
* `
|
|
156
|
-
*
|
|
157
|
-
*
|
|
185
|
+
* Remove the installer-owned MCP server(s) from the Claude user config's
|
|
186
|
+
* `mcpServers`. Idempotent — a server that isn't present is reported as
|
|
187
|
+
* "nothing to remove". If the config can't be read/written, prints guidance
|
|
188
|
+
* instead of guessing. `mempalace` is never touched. `io` is injectable.
|
|
158
189
|
*
|
|
159
190
|
* NOTE: the registration is global (shared across all projects), so this removes
|
|
160
191
|
* code-ai-mcp for every project. That is the chosen behaviour (symmetric with
|
|
@@ -162,4 +193,4 @@ export interface McpTeardownReport {
|
|
|
162
193
|
*/
|
|
163
194
|
export declare function teardownMcp(opts: {
|
|
164
195
|
dryRun: boolean;
|
|
165
|
-
},
|
|
196
|
+
}, io?: UserConfigIO): Promise<McpTeardownReport>;
|
package/dist/mcp_setup.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { copyFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
|
-
const defaultClaudeCli = {
|
|
5
|
-
run: (args) => spawnCapture("claude", args),
|
|
6
|
-
};
|
|
7
5
|
/**
|
|
8
6
|
* Try `mempalace-mcp --help` — the dedicated MCP-server bin, which is exactly
|
|
9
7
|
* what we register. Resolves true on exit code 0, false otherwise (including
|
|
@@ -14,11 +12,6 @@ const defaultClaudeCli = {
|
|
|
14
12
|
export async function detectMemPalace() {
|
|
15
13
|
return spawnExitZero("mempalace-mcp", ["--help"]);
|
|
16
14
|
}
|
|
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
|
-
}
|
|
22
15
|
/**
|
|
23
16
|
* Find the first available Python install runtime, in preference order:
|
|
24
17
|
* uv → pipx → pip. Returns null if none available.
|
|
@@ -56,55 +49,118 @@ function installCommandArgs(runtime) {
|
|
|
56
49
|
}
|
|
57
50
|
}
|
|
58
51
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* before the server name (as `claude mcp add` expects options first).
|
|
52
|
+
* Resolve the Claude user config path. Honours `CLAUDE_CONFIG_DIR` when set,
|
|
53
|
+
* else `~/.claude.json`.
|
|
62
54
|
*/
|
|
63
|
-
export function
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
export function userConfigPath() {
|
|
56
|
+
const dir = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
57
|
+
return dir ? join(dir, ".claude.json") : join(homedir(), ".claude.json");
|
|
58
|
+
}
|
|
59
|
+
/** Real filesystem-backed UserConfigIO. Factory so tests can target a temp file. */
|
|
60
|
+
export function createUserConfigIO(configPath) {
|
|
61
|
+
return {
|
|
62
|
+
configPath,
|
|
63
|
+
async read() {
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = await readFile(configPath, "utf8");
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const e = err;
|
|
70
|
+
return { ok: false, error: e.code === "ENOENT" ? "file not found" : e.message };
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const data = JSON.parse(raw);
|
|
74
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
75
|
+
return { ok: false, error: "config is not a JSON object" };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, data: data };
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
return { ok: false, error: `JSON parse error: ${err.message}` };
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
async writeWithBackup(data) {
|
|
84
|
+
const backupPath = `${configPath}.bak`;
|
|
85
|
+
try {
|
|
86
|
+
try {
|
|
87
|
+
await copyFile(configPath, backupPath);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// Only tolerate "original did not exist"; surface anything else.
|
|
91
|
+
if (err.code !== "ENOENT")
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
const tmpPath = `${configPath}.code-ai.tmp`;
|
|
95
|
+
await writeFile(tmpPath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
96
|
+
await rename(tmpPath, configPath);
|
|
97
|
+
return { ok: true, backupPath };
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
return { ok: false, error: err.message };
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
68
104
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
const defaultUserConfigIO = createUserConfigIO(userConfigPath());
|
|
106
|
+
/** Shape a server entry the way Claude stores user-scope stdio servers. */
|
|
107
|
+
function toUserScopeEntry(entry) {
|
|
108
|
+
return {
|
|
109
|
+
type: "stdio",
|
|
110
|
+
command: entry.command,
|
|
111
|
+
args: [...entry.args],
|
|
112
|
+
env: entry.env ?? {},
|
|
113
|
+
};
|
|
72
114
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
115
|
+
function readMcpServers(config) {
|
|
116
|
+
const existing = config.mcpServers;
|
|
117
|
+
return existing && typeof existing === "object" && !Array.isArray(existing)
|
|
118
|
+
? { ...existing }
|
|
119
|
+
: {};
|
|
76
120
|
}
|
|
77
121
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
122
|
+
* Idempotently merge `servers` into the config's top-level `mcpServers`. Pure —
|
|
123
|
+
* returns a new config object plus which names were freshly added vs already
|
|
124
|
+
* present. An existing key is NEVER overwritten (so a pre-existing global
|
|
125
|
+
* `mempalace` is preserved untouched). Exported for unit testing.
|
|
82
126
|
*/
|
|
83
|
-
export
|
|
84
|
-
const
|
|
85
|
-
|
|
127
|
+
export function mergeUserScopeServers(config, servers) {
|
|
128
|
+
const next = { ...config };
|
|
129
|
+
const mcpServers = readMcpServers(config);
|
|
130
|
+
const registered = [];
|
|
131
|
+
const alreadyPresent = [];
|
|
132
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
133
|
+
if (Object.prototype.hasOwnProperty.call(mcpServers, name)) {
|
|
134
|
+
alreadyPresent.push(name);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
mcpServers[name] = toUserScopeEntry(entry);
|
|
138
|
+
registered.push(name);
|
|
139
|
+
}
|
|
140
|
+
next.mcpServers = mcpServers;
|
|
141
|
+
return { config: next, registered, alreadyPresent };
|
|
86
142
|
}
|
|
87
143
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
144
|
+
* Idempotently remove `names` from the config's `mcpServers`. Pure — returns a
|
|
145
|
+
* new config object plus which names were removed vs were not present. Exported
|
|
146
|
+
* for unit testing.
|
|
90
147
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
148
|
+
export function removeUserScopeServers(config, names) {
|
|
149
|
+
const next = { ...config };
|
|
150
|
+
const mcpServers = readMcpServers(config);
|
|
151
|
+
const removed = [];
|
|
152
|
+
const notPresent = [];
|
|
153
|
+
for (const name of names) {
|
|
154
|
+
if (Object.prototype.hasOwnProperty.call(mcpServers, name)) {
|
|
155
|
+
delete mcpServers[name];
|
|
156
|
+
removed.push(name);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
notPresent.push(name);
|
|
160
|
+
}
|
|
101
161
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
notice: `Failed to register '${name}' in user scope. Run it manually:\n` +
|
|
105
|
-
` ${manualAddCommand(name, entry)}\n` +
|
|
106
|
-
` Output:\n${res.output}`,
|
|
107
|
-
};
|
|
162
|
+
next.mcpServers = mcpServers;
|
|
163
|
+
return { config: next, removed, notPresent };
|
|
108
164
|
}
|
|
109
165
|
/**
|
|
110
166
|
* Write `<destinationDir>/.code-ai/config.json` with the chosen backend.
|
|
@@ -142,21 +198,25 @@ function buildServerEntries(mempalaceUsed) {
|
|
|
142
198
|
}
|
|
143
199
|
return servers;
|
|
144
200
|
}
|
|
201
|
+
/** Format a single "name: entry-json" line for the manual-fallback notice. */
|
|
202
|
+
function manualEntryLine(name, entry) {
|
|
203
|
+
return ` "${name}": ${JSON.stringify(toUserScopeEntry(entry))}`;
|
|
204
|
+
}
|
|
145
205
|
/**
|
|
146
206
|
* End-to-end orchestrator. Called from the install flow when `target=claude`.
|
|
147
207
|
*
|
|
148
208
|
* Order:
|
|
149
209
|
* 1. If user wants MemPalace: detect → install if absent → fall back if install fails.
|
|
150
210
|
* 2. Build server entries (code-ai-mcp always; mempalace only when usable).
|
|
151
|
-
* 3.
|
|
152
|
-
*
|
|
153
|
-
*
|
|
211
|
+
* 3. Merge them into the Claude user config's `mcpServers` (idempotent; skip
|
|
212
|
+
* already-present). If the config can't be read/written, print the exact
|
|
213
|
+
* JSON to add by hand instead of guessing.
|
|
154
214
|
* 4. Write project-local `.code-ai/config.json` with the backend choice.
|
|
155
215
|
*
|
|
156
216
|
* Reports all decisions in `McpSetupReport.notices` so callers can surface
|
|
157
|
-
* them to the user verbatim. `
|
|
217
|
+
* them to the user verbatim. `io` is injectable for testing.
|
|
158
218
|
*/
|
|
159
|
-
export async function setupMcp(opts,
|
|
219
|
+
export async function setupMcp(opts, io = defaultUserConfigIO) {
|
|
160
220
|
const notices = [];
|
|
161
221
|
let mempalaceUsed = false;
|
|
162
222
|
let mempalaceInstallAttempted = false;
|
|
@@ -191,33 +251,49 @@ export async function setupMcp(opts, cli = defaultClaudeCli) {
|
|
|
191
251
|
const serversRegistered = [];
|
|
192
252
|
const serversAlreadyPresent = [];
|
|
193
253
|
const serversFailed = [];
|
|
194
|
-
const claudeCliAvailable = !opts.dryRun && (await detectClaudeCli(cli));
|
|
195
254
|
let registration;
|
|
196
255
|
if (opts.dryRun) {
|
|
197
256
|
registration = "user-scope";
|
|
198
257
|
for (const [name, entry] of Object.entries(servers)) {
|
|
199
|
-
notices.push(`Would register MCP server '${name}' in user
|
|
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);
|
|
258
|
+
notices.push(`Would register MCP server '${name}' in Claude user config (${io.configPath}): ${JSON.stringify(toUserScopeEntry(entry))}`);
|
|
213
259
|
}
|
|
214
260
|
}
|
|
215
261
|
else {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
262
|
+
const read = await io.read();
|
|
263
|
+
if (!read.ok || !read.data) {
|
|
264
|
+
registration = "manual-fallback";
|
|
265
|
+
notices.push(`Could not read Claude user config at ${io.configPath} (${read.error ?? "unknown error"}). ` +
|
|
266
|
+
`Not modifying it automatically — add these entries to its "mcpServers" object by hand:`);
|
|
267
|
+
for (const [name, entry] of Object.entries(servers))
|
|
268
|
+
notices.push(manualEntryLine(name, entry));
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
const merged = mergeUserScopeServers(read.data, servers);
|
|
272
|
+
serversAlreadyPresent.push(...merged.alreadyPresent);
|
|
273
|
+
for (const name of merged.alreadyPresent) {
|
|
274
|
+
notices.push(`MCP server '${name}' already in user config — left untouched.`);
|
|
275
|
+
}
|
|
276
|
+
if (merged.registered.length === 0) {
|
|
277
|
+
registration = "user-scope";
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
const written = await io.writeWithBackup(merged.config);
|
|
281
|
+
if (written.ok) {
|
|
282
|
+
registration = "user-scope";
|
|
283
|
+
serversRegistered.push(...merged.registered);
|
|
284
|
+
notices.push(`Registered MCP server(s) [${merged.registered.join(", ")}] in Claude user config ${io.configPath}` +
|
|
285
|
+
(written.backupPath ? ` (backup: ${written.backupPath})` : "") +
|
|
286
|
+
". Restart your Claude Code session to load them.");
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
registration = "manual-fallback";
|
|
290
|
+
serversFailed.push(...merged.registered);
|
|
291
|
+
notices.push(`Failed to write Claude user config at ${io.configPath} (${written.error ?? "unknown error"}). ` +
|
|
292
|
+
`No changes made — add these entries to its "mcpServers" object by hand:`);
|
|
293
|
+
for (const name of merged.registered)
|
|
294
|
+
notices.push(manualEntryLine(name, servers[name]));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
221
297
|
}
|
|
222
298
|
}
|
|
223
299
|
const cfg = await writeCodeAiConfig(opts.destinationDir, {
|
|
@@ -229,7 +305,7 @@ export async function setupMcp(opts, cli = defaultClaudeCli) {
|
|
|
229
305
|
mempalaceInstallAttempted,
|
|
230
306
|
mempalaceInstallSucceeded,
|
|
231
307
|
pythonRuntime,
|
|
232
|
-
|
|
308
|
+
userConfigPath: io.configPath,
|
|
233
309
|
registration,
|
|
234
310
|
serversRegistered,
|
|
235
311
|
serversAlreadyPresent,
|
|
@@ -246,54 +322,86 @@ export async function setupMcp(opts, cli = defaultClaudeCli) {
|
|
|
246
322
|
*/
|
|
247
323
|
const INSTALLER_OWNED_SERVERS = ["code-ai-mcp"];
|
|
248
324
|
/**
|
|
249
|
-
* Remove the installer-owned MCP server(s) from Claude's
|
|
250
|
-
* `
|
|
251
|
-
*
|
|
252
|
-
*
|
|
325
|
+
* Remove the installer-owned MCP server(s) from the Claude user config's
|
|
326
|
+
* `mcpServers`. Idempotent — a server that isn't present is reported as
|
|
327
|
+
* "nothing to remove". If the config can't be read/written, prints guidance
|
|
328
|
+
* instead of guessing. `mempalace` is never touched. `io` is injectable.
|
|
253
329
|
*
|
|
254
330
|
* NOTE: the registration is global (shared across all projects), so this removes
|
|
255
331
|
* code-ai-mcp for every project. That is the chosen behaviour (symmetric with
|
|
256
332
|
* install); a multi-project user re-runs the installer to restore it.
|
|
257
333
|
*/
|
|
258
|
-
export async function teardownMcp(opts,
|
|
334
|
+
export async function teardownMcp(opts, io = defaultUserConfigIO) {
|
|
259
335
|
const notices = [];
|
|
260
336
|
const serversRemoved = [];
|
|
261
337
|
const serversNotPresent = [];
|
|
262
338
|
const serversFailed = [];
|
|
263
339
|
if (opts.dryRun) {
|
|
264
340
|
for (const name of INSTALLER_OWNED_SERVERS) {
|
|
265
|
-
notices.push(`Would remove MCP server '${name}' from user
|
|
341
|
+
notices.push(`Would remove MCP server '${name}' from Claude user config (${io.configPath}).`);
|
|
266
342
|
}
|
|
267
|
-
return {
|
|
343
|
+
return {
|
|
344
|
+
userConfigPath: io.configPath,
|
|
345
|
+
removal: "user-scope",
|
|
346
|
+
serversRemoved,
|
|
347
|
+
serversNotPresent,
|
|
348
|
+
serversFailed,
|
|
349
|
+
notices,
|
|
350
|
+
};
|
|
268
351
|
}
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
271
|
-
notices.push(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
352
|
+
const read = await io.read();
|
|
353
|
+
if (!read.ok || !read.data) {
|
|
354
|
+
notices.push(`Could not read Claude user config at ${io.configPath} (${read.error ?? "unknown error"}). ` +
|
|
355
|
+
`Remove the server(s) [${INSTALLER_OWNED_SERVERS.join(", ")}] from its "mcpServers" object by hand.`);
|
|
356
|
+
return {
|
|
357
|
+
userConfigPath: io.configPath,
|
|
358
|
+
removal: "manual-fallback",
|
|
359
|
+
serversRemoved,
|
|
360
|
+
serversNotPresent,
|
|
361
|
+
serversFailed,
|
|
362
|
+
notices,
|
|
363
|
+
};
|
|
277
364
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
365
|
+
const result = removeUserScopeServers(read.data, INSTALLER_OWNED_SERVERS);
|
|
366
|
+
serversNotPresent.push(...result.notPresent);
|
|
367
|
+
for (const name of result.notPresent) {
|
|
368
|
+
notices.push(`MCP server '${name}' is not in user config — nothing to remove.`);
|
|
369
|
+
}
|
|
370
|
+
if (result.removed.length === 0) {
|
|
371
|
+
return {
|
|
372
|
+
userConfigPath: io.configPath,
|
|
373
|
+
removal: "user-scope",
|
|
374
|
+
serversRemoved,
|
|
375
|
+
serversNotPresent,
|
|
376
|
+
serversFailed,
|
|
377
|
+
notices,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const written = await io.writeWithBackup(result.config);
|
|
381
|
+
if (written.ok) {
|
|
382
|
+
serversRemoved.push(...result.removed);
|
|
383
|
+
notices.push(`Removed MCP server(s) [${result.removed.join(", ")}] from Claude user config ${io.configPath}` +
|
|
384
|
+
(written.backupPath ? ` (backup: ${written.backupPath})` : "") +
|
|
385
|
+
".");
|
|
386
|
+
return {
|
|
387
|
+
userConfigPath: io.configPath,
|
|
388
|
+
removal: "user-scope",
|
|
389
|
+
serversRemoved,
|
|
390
|
+
serversNotPresent,
|
|
391
|
+
serversFailed,
|
|
392
|
+
notices,
|
|
393
|
+
};
|
|
295
394
|
}
|
|
296
|
-
|
|
395
|
+
serversFailed.push(...result.removed);
|
|
396
|
+
notices.push(`Failed to write Claude user config at ${io.configPath} (${written.error ?? "unknown error"}). No changes made.`);
|
|
397
|
+
return {
|
|
398
|
+
userConfigPath: io.configPath,
|
|
399
|
+
removal: "manual-fallback",
|
|
400
|
+
serversRemoved,
|
|
401
|
+
serversNotPresent,
|
|
402
|
+
serversFailed,
|
|
403
|
+
notices,
|
|
404
|
+
};
|
|
297
405
|
}
|
|
298
406
|
// ─── Subprocess helpers ─────────────────────────────────────────────────────
|
|
299
407
|
async function spawnExitZero(command, args) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-ai-installer",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Production-ready CLI to install code-ai agents and skills for multiple AI coding assistants. Bundles the code-ai-mcp MCP server for Claude Code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Denish1209",
|