code-ai-installer 4.0.1 → 4.3.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 +5 -3
- package/dist/mcp/audit_ledger.d.ts +10 -0
- package/dist/mcp/audit_ledger.js +15 -0
- package/dist/mcp/cli.js +1 -0
- package/dist/mcp/tools/render_diff.d.ts +25 -0
- package/dist/mcp/tools/render_diff.js +138 -0
- package/dist/mcp/tools/sign_off.js +10 -5
- package/dist/mcp/tools/stubs.js +2 -0
- package/dist/mcp_setup.d.ts +79 -48
- package/dist/mcp_setup.js +215 -107
- package/dist/shared/tools.d.ts +38 -0
- package/dist/shared/tools.js +24 -0
- package/domains/analytics/agents/conductor.md +15 -1
- package/domains/analytics/locales/en/agents/conductor.md +15 -1
- package/domains/content/agents/conductor.md +15 -1
- package/domains/content/locales/en/agents/conductor.md +15 -1
- package/domains/development/.agents/skills/mcp-integration/SKILL.md +3 -1
- package/domains/development/.agents/workflows/audit.md +25 -0
- package/domains/development/.agents/workflows/pipeline-rules.md +1 -0
- package/domains/development/AGENTS.md +1 -0
- package/domains/development/agents/architect.md +1 -1
- package/domains/development/agents/auditor.md +4 -3
- package/domains/development/agents/conductor.md +4 -1
- package/domains/development/agents/devops.md +1 -1
- package/domains/development/agents/reviewer.md +2 -1
- package/domains/development/agents/senior_full_stack.md +1 -1
- package/domains/development/agents/tester.md +1 -1
- package/domains/development/locales/en/.agents/skills/mcp-integration/SKILL.md +3 -1
- package/domains/development/locales/en/.agents/workflows/audit.md +25 -0
- package/domains/development/locales/en/.agents/workflows/pipeline-rules.md +1 -0
- package/domains/development/locales/en/AGENTS.md +2 -0
- package/domains/development/locales/en/agents/architect.md +1 -1
- package/domains/development/locales/en/agents/auditor.md +4 -3
- package/domains/development/locales/en/agents/conductor.md +4 -1
- package/domains/development/locales/en/agents/devops.md +1 -1
- package/domains/development/locales/en/agents/reviewer.md +2 -1
- package/domains/development/locales/en/agents/senior_full_stack.md +1 -1
- package/domains/development/locales/en/agents/tester.md +1 -1
- package/domains/product/agents/conductor.md +15 -1
- package/domains/product/locales/en/agents/conductor.md +15 -1
- 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,14 +151,16 @@ 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
|
|
|
158
158
|
## 🧬 Versions & migration
|
|
159
159
|
|
|
160
|
-
`code-ai-installer` is on **v4.
|
|
160
|
+
`code-ai-installer` is on **v4.3.0**.
|
|
161
161
|
|
|
162
|
+
- **v4.3.0** — `render_diff` MCP tool (unified diff → a standalone HTML review page); MCP gate-flow + stop-at-user-gate sections added to the content / analytics / product conductors; Auditor trigger — a `/audit` command plus a Release-Gate nudge that surfaces after every 3rd completed run (development pilot).
|
|
163
|
+
- **v4.1.0** — MCP servers now register in your **global (user-scope)** config via a direct, idempotent `~/.claude.json` merge (no dependency on the `claude` CLI); the conductor halts at each user gate — one at a time, no batching, no auto-pass on green.
|
|
162
164
|
- **v4.0.0** — consolidated the previously separate `code-ai-mcp` and types packages into this single package with **two bins** (`code-ai` + `code-ai-mcp`). Installing the CLI now also delivers the MCP server; for Claude it is auto-registered in your global (user-scope) MCP config. Existing 3.x CLI behavior is unchanged.
|
|
163
165
|
- **v3.0.0 (breaking)** — removed the legacy flat root layout (`AGENTS.md` + `agents/` + `.agents/` at package root); the CLI now reads exclusively from `domains/<id>/`. **Migrating from v2.x:** add `--domain <development|content|analytics|product>` to every CLI call. If you used a custom `--project-dir`, restructure it to `domains/<id>/` instead of a flat layout.
|
|
164
166
|
|
|
@@ -10,3 +10,13 @@ export declare function readLedger(): Promise<RunScorecard[]>;
|
|
|
10
10
|
* break a sign-off (telemetry is never load-bearing for the gate).
|
|
11
11
|
*/
|
|
12
12
|
export declare function recordRunScorecard(state: TaskState): Promise<void>;
|
|
13
|
+
/** Number of COMPLETED (RG-signed) runs in the ledger. */
|
|
14
|
+
export declare function countCompletedRuns(): Promise<number>;
|
|
15
|
+
/**
|
|
16
|
+
* A1 cadence (Variant A1): surface an Auditor nudge on every 3rd completed run
|
|
17
|
+
* (3, 6, 9, …). Stateless — no last-audit marker — so it re-reminds until the
|
|
18
|
+
* user runs /audit. Surfacing only; returns undefined when no nudge is due.
|
|
19
|
+
*/
|
|
20
|
+
export declare function auditNudgeFor(completedRuns: number): {
|
|
21
|
+
runs_total: number;
|
|
22
|
+
} | undefined;
|
package/dist/mcp/audit_ledger.js
CHANGED
|
@@ -80,3 +80,18 @@ export async function recordRunScorecard(state) {
|
|
|
80
80
|
const extras = await readSideCounts(state.task_id);
|
|
81
81
|
await appendScorecard(buildScorecard(state, extras));
|
|
82
82
|
}
|
|
83
|
+
/** Number of COMPLETED (RG-signed) runs in the ledger. */
|
|
84
|
+
export async function countCompletedRuns() {
|
|
85
|
+
const cards = await readLedger();
|
|
86
|
+
return cards.filter((c) => c.completed).length;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* A1 cadence (Variant A1): surface an Auditor nudge on every 3rd completed run
|
|
90
|
+
* (3, 6, 9, …). Stateless — no last-audit marker — so it re-reminds until the
|
|
91
|
+
* user runs /audit. Surfacing only; returns undefined when no nudge is due.
|
|
92
|
+
*/
|
|
93
|
+
export function auditNudgeFor(completedRuns) {
|
|
94
|
+
return completedRuns >= 3 && completedRuns % 3 === 0
|
|
95
|
+
? { runs_total: completedRuns }
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
package/dist/mcp/cli.js
CHANGED
|
@@ -56,6 +56,7 @@ const TOOL_DESCRIPTIONS = {
|
|
|
56
56
|
dependency_supply_chain: "[stub] Check supply chain (npm audit, Socket integration, license check).",
|
|
57
57
|
docker_compose: "[stub] Run docker compose up/down for a target.",
|
|
58
58
|
e2e_playwright: "[stub] Run Playwright end-to-end suite.",
|
|
59
|
+
render_diff: "Render a unified diff (e.g. `git diff`) into a colored, per-file, line-numbered HTML review page written to the OS temp dir. Returns the path + a file:// URL the user opens in a browser. Use at the REV gate to present code changes for review. Informational only — the file lives in temp (cleared on reboot), never in the project.",
|
|
59
60
|
};
|
|
60
61
|
function buildServer() {
|
|
61
62
|
const dispatch = new CodeAiMcpServer();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type RenderDiffInput, type RenderDiffOutput } from "../../shared/index.js";
|
|
2
|
+
type RowKind = "hunk" | "add" | "del" | "ctx";
|
|
3
|
+
interface DiffRow {
|
|
4
|
+
kind: RowKind;
|
|
5
|
+
text: string;
|
|
6
|
+
o: number | "";
|
|
7
|
+
n: number | "";
|
|
8
|
+
}
|
|
9
|
+
export interface DiffFile {
|
|
10
|
+
name: string;
|
|
11
|
+
added: number;
|
|
12
|
+
removed: number;
|
|
13
|
+
rows: DiffRow[];
|
|
14
|
+
}
|
|
15
|
+
/** Parse a unified diff into per-file rows with old/new line numbers. Pure. */
|
|
16
|
+
export declare function parseUnifiedDiff(raw: string): DiffFile[];
|
|
17
|
+
/** Render parsed diff files into a self-contained HTML page. Pure. */
|
|
18
|
+
export declare function renderDiffHtml(diff: string, title: string): {
|
|
19
|
+
html: string;
|
|
20
|
+
files: number;
|
|
21
|
+
added: number;
|
|
22
|
+
removed: number;
|
|
23
|
+
};
|
|
24
|
+
export declare function renderDiff(input: RenderDiffInput): Promise<RenderDiffOutput>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
/**
|
|
7
|
+
* render_diff — turn a unified diff into a colored, per-file, line-numbered HTML
|
|
8
|
+
* review page and write it to the OS temp dir.
|
|
9
|
+
*
|
|
10
|
+
* Why a tool (not a loose script): code reviews in the pipeline are presented as
|
|
11
|
+
* an HTML diff the user opens via a file:// link. Shipping it as an MCP tool keeps
|
|
12
|
+
* it version-controlled, testable, and reachable wherever the server runs.
|
|
13
|
+
*
|
|
14
|
+
* The output is INFORMATIONAL only — it lives in the system temp dir (cleared on
|
|
15
|
+
* reboot), never in the project, so it pollutes nothing and needs no cleanup.
|
|
16
|
+
* Self-contained: no deps, no network.
|
|
17
|
+
*/
|
|
18
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
19
|
+
/** Parse a unified diff into per-file rows with old/new line numbers. Pure. */
|
|
20
|
+
export function parseUnifiedDiff(raw) {
|
|
21
|
+
const blocks = raw.split(/^diff --git /m).filter(Boolean);
|
|
22
|
+
const files = [];
|
|
23
|
+
for (const b of blocks) {
|
|
24
|
+
const lines = ("diff --git " + b).split("\n");
|
|
25
|
+
const header = lines[0];
|
|
26
|
+
const m = header.match(/a\/(.+?) b\/(.+)$/);
|
|
27
|
+
const name = m ? m[2] : header;
|
|
28
|
+
let added = 0;
|
|
29
|
+
let removed = 0;
|
|
30
|
+
const rows = [];
|
|
31
|
+
let oldLn = 0;
|
|
32
|
+
let newLn = 0;
|
|
33
|
+
for (const ln of lines) {
|
|
34
|
+
if (ln.startsWith("diff --git") ||
|
|
35
|
+
ln.startsWith("index ") ||
|
|
36
|
+
ln.startsWith("--- ") ||
|
|
37
|
+
ln.startsWith("+++ "))
|
|
38
|
+
continue;
|
|
39
|
+
const hunk = ln.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
40
|
+
if (hunk) {
|
|
41
|
+
oldLn = Number(hunk[1]);
|
|
42
|
+
newLn = Number(hunk[2]);
|
|
43
|
+
rows.push({ kind: "hunk", text: ln, o: "", n: "" });
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ln.startsWith("+")) {
|
|
47
|
+
added++;
|
|
48
|
+
rows.push({ kind: "add", text: ln.slice(1), o: "", n: newLn++ });
|
|
49
|
+
}
|
|
50
|
+
else if (ln.startsWith("-")) {
|
|
51
|
+
removed++;
|
|
52
|
+
rows.push({ kind: "del", text: ln.slice(1), o: oldLn++, n: "" });
|
|
53
|
+
}
|
|
54
|
+
else if (ln.startsWith("\\")) {
|
|
55
|
+
// "" — skip
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
rows.push({ kind: "ctx", text: ln.slice(1), o: oldLn++, n: newLn++ });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
files.push({ name, added, removed, rows });
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
/** Render parsed diff files into a self-contained HTML page. Pure. */
|
|
66
|
+
export function renderDiffHtml(diff, title) {
|
|
67
|
+
const files = parseUnifiedDiff(diff);
|
|
68
|
+
const fileSections = files
|
|
69
|
+
.map((f, i) => {
|
|
70
|
+
const rowsHtml = f.rows
|
|
71
|
+
.map((r) => {
|
|
72
|
+
if (r.kind === "hunk")
|
|
73
|
+
return `<tr class="hunk"><td class="ln"></td><td class="ln"></td><td class="code">${esc(r.text)}</td></tr>`;
|
|
74
|
+
const sign = r.kind === "add" ? "+" : r.kind === "del" ? "-" : " ";
|
|
75
|
+
return `<tr class="${r.kind}"><td class="ln">${r.o}</td><td class="ln">${r.n}</td><td class="code"><span class="sign">${sign}</span>${esc(r.text)}</td></tr>`;
|
|
76
|
+
})
|
|
77
|
+
.join("\n");
|
|
78
|
+
return `<section id="f${i}">
|
|
79
|
+
<h2>${esc(f.name)} <span class="stat"><span class="plus">+${f.added}</span> <span class="minus">-${f.removed}</span></span></h2>
|
|
80
|
+
<table>${rowsHtml}</table>
|
|
81
|
+
</section>`;
|
|
82
|
+
})
|
|
83
|
+
.join("\n");
|
|
84
|
+
const added = files.reduce((s, f) => s + f.added, 0);
|
|
85
|
+
const removed = files.reduce((s, f) => s + f.removed, 0);
|
|
86
|
+
const nav = files
|
|
87
|
+
.map((f, i) => `<a href="#f${i}">${esc(f.name.split("/").pop() ?? f.name)}</a>`)
|
|
88
|
+
.join("");
|
|
89
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
90
|
+
<title>${esc(title)}</title>
|
|
91
|
+
<style>
|
|
92
|
+
:root { color-scheme: light dark; }
|
|
93
|
+
body { font: 13px/1.5 ui-monospace, "Cascadia Code", Consolas, monospace; margin: 0; background:#0d1117; color:#c9d1d9; }
|
|
94
|
+
header { padding: 16px 24px; border-bottom: 1px solid #30363d; position: sticky; top:0; background:#0d1117; }
|
|
95
|
+
header h1 { font: 600 16px system-ui, sans-serif; margin: 0 0 4px; }
|
|
96
|
+
header .summary { font: 13px system-ui, sans-serif; color:#8b949e; }
|
|
97
|
+
.plus { color:#3fb950; } .minus { color:#f85149; }
|
|
98
|
+
section { margin: 20px 24px; border: 1px solid #30363d; border-radius: 8px; overflow:hidden; }
|
|
99
|
+
section h2 { font: 600 13px ui-monospace, monospace; margin:0; padding:10px 14px; background:#161b22; border-bottom:1px solid #30363d; }
|
|
100
|
+
.stat { float:right; font-weight:400; }
|
|
101
|
+
table { border-collapse: collapse; width:100%; }
|
|
102
|
+
td { padding: 0 6px; white-space: pre-wrap; word-break: break-word; vertical-align: top; }
|
|
103
|
+
td.ln { width:1%; text-align:right; color:#6e7681; user-select:none; border-right:1px solid #21262d; padding:0 8px; }
|
|
104
|
+
td.code { width:100%; }
|
|
105
|
+
.sign { display:inline-block; width:1ch; color:#6e7681; }
|
|
106
|
+
tr.add { background: rgba(63,185,80,.15); } tr.add .sign { color:#3fb950; }
|
|
107
|
+
tr.del { background: rgba(248,81,73,.15); } tr.del .sign { color:#f85149; }
|
|
108
|
+
tr.hunk td { background:#161b22; color:#8b949e; padding:4px 14px; }
|
|
109
|
+
nav { padding: 0 24px; font: 13px system-ui, sans-serif; }
|
|
110
|
+
nav a { color:#58a6ff; text-decoration:none; display:inline-block; margin:2px 12px 2px 0; }
|
|
111
|
+
</style></head><body>
|
|
112
|
+
<header>
|
|
113
|
+
<h1>${esc(title)}</h1>
|
|
114
|
+
<div class="summary">${files.length} files · <span class="plus">+${added}</span> <span class="minus">-${removed}</span></div>
|
|
115
|
+
</header>
|
|
116
|
+
<nav>${nav}</nav>
|
|
117
|
+
${fileSections}
|
|
118
|
+
</body></html>`;
|
|
119
|
+
return { html, files: files.length, added, removed };
|
|
120
|
+
}
|
|
121
|
+
/** Slugify a label for use in a temp filename. */
|
|
122
|
+
function slug(s) {
|
|
123
|
+
return (s
|
|
124
|
+
.toLowerCase()
|
|
125
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
126
|
+
.replace(/^-+|-+$/g, "")
|
|
127
|
+
.slice(0, 40) || "review");
|
|
128
|
+
}
|
|
129
|
+
export async function renderDiff(input) {
|
|
130
|
+
const title = input.title ?? "Diff review";
|
|
131
|
+
const { html, files, added, removed } = renderDiffHtml(input.diff, title);
|
|
132
|
+
// Content-hashed name → same diff reuses one file; lands in the OS temp dir
|
|
133
|
+
// (cleared on reboot), never in the project.
|
|
134
|
+
const hash = createHash("sha1").update(input.diff).digest("hex").slice(0, 8);
|
|
135
|
+
const path = join(tmpdir(), `code-ai-diff-${slug(input.task_id ?? title)}-${hash}.html`);
|
|
136
|
+
await writeFile(path, html, "utf8");
|
|
137
|
+
return { path, file_url: pathToFileURL(path).href, files, added, removed };
|
|
138
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getGateConfig, loadPipeline } from "../pipeline.js";
|
|
2
2
|
import { readTaskState, writeTaskState } from "../task_state.js";
|
|
3
|
-
import { recordRunScorecard } from "../audit_ledger.js";
|
|
3
|
+
import { recordRunScorecard, countCompletedRuns, auditNudgeFor } from "../audit_ledger.js";
|
|
4
4
|
import { resolveActiveDomain } from "../config.js";
|
|
5
5
|
/**
|
|
6
6
|
* Records a sign-off event for the task's current gate. Validates that:
|
|
@@ -27,7 +27,9 @@ export async function signOff(input) {
|
|
|
27
27
|
switch (gateCfg.sign_off_policy) {
|
|
28
28
|
case "user":
|
|
29
29
|
if (input.signer !== "user") {
|
|
30
|
-
throw new Error(`sign_off: gate ${input.gate} requires signer 'user' (policy=user), got '${input.signer}'`
|
|
30
|
+
throw new Error(`sign_off: gate ${input.gate} requires signer 'user' (policy=user), got '${input.signer}'. ` +
|
|
31
|
+
`This is a USER gate — HALT: present the ${input.gate} artifact and request the user's sign_off(signer='user') for THIS gate. ` +
|
|
32
|
+
`Do not auto-pass it, do not batch it with other gates, and do not begin any downstream-gate work until it is signed.`);
|
|
31
33
|
}
|
|
32
34
|
break;
|
|
33
35
|
case "mcp_auto_pass":
|
|
@@ -56,15 +58,18 @@ export async function signOff(input) {
|
|
|
56
58
|
});
|
|
57
59
|
await writeTaskState(state);
|
|
58
60
|
// Auditor telemetry: when the terminal gate is signed, the run is complete —
|
|
59
|
-
// append a scorecard to the local ledger
|
|
60
|
-
//
|
|
61
|
+
// append a scorecard to the local ledger, then compute the /audit nudge.
|
|
62
|
+
// Best-effort: a ledger failure (or nudge failure) must NEVER break a
|
|
63
|
+
// sign-off (telemetry is not load-bearing for the gate).
|
|
64
|
+
let audit_nudge;
|
|
61
65
|
if (input.gate === "RG") {
|
|
62
66
|
try {
|
|
63
67
|
await recordRunScorecard(state);
|
|
68
|
+
audit_nudge = auditNudgeFor(await countCompletedRuns());
|
|
64
69
|
}
|
|
65
70
|
catch {
|
|
66
71
|
/* swallow — see contract in audit_ledger.recordRunScorecard */
|
|
67
72
|
}
|
|
68
73
|
}
|
|
69
|
-
return { signed: true, signer: input.signer, timestamp };
|
|
74
|
+
return { signed: true, signer: input.signer, timestamp, ...(audit_nudge ? { audit_nudge } : {}) };
|
|
70
75
|
}
|
package/dist/mcp/tools/stubs.js
CHANGED
|
@@ -31,6 +31,7 @@ import { reviewProposal } from "./review_proposal.js";
|
|
|
31
31
|
import { e2ePlaywright } from "./e2e_playwright.js";
|
|
32
32
|
import { dockerCompose } from "./docker_compose.js";
|
|
33
33
|
import { dependencySupplyChain } from "./dependency_supply_chain.js";
|
|
34
|
+
import { renderDiff } from "./render_diff.js";
|
|
34
35
|
/** Thrown by every stub until the real implementation lands. */
|
|
35
36
|
export class NotImplementedError extends Error {
|
|
36
37
|
constructor(tool) {
|
|
@@ -83,4 +84,5 @@ export const DEFAULT_HANDLERS = {
|
|
|
83
84
|
dependency_supply_chain: dependencySupplyChain,
|
|
84
85
|
docker_compose: dockerCompose,
|
|
85
86
|
e2e_playwright: e2ePlaywright,
|
|
87
|
+
render_diff: renderDiff,
|
|
86
88
|
};
|
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>;
|