@systemdox/mcp-server 0.1.47 → 0.1.49

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.
@@ -0,0 +1,636 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/setup-mcp.ts
4
+ import { existsSync as existsSync3, readFileSync } from "fs";
5
+ import { join as join4 } from "path";
6
+
7
+ // src/lib/cache.ts
8
+ var ResponseCache = class {
9
+ store = /* @__PURE__ */ new Map();
10
+ ttlMs;
11
+ constructor(ttlMs) {
12
+ this.ttlMs = ttlMs;
13
+ }
14
+ get(key) {
15
+ const entry = this.store.get(key);
16
+ if (!entry) return void 0;
17
+ if (Date.now() > entry.expiresAt) {
18
+ this.store.delete(key);
19
+ return void 0;
20
+ }
21
+ return entry.data;
22
+ }
23
+ set(key, data) {
24
+ this.store.set(key, {
25
+ data,
26
+ expiresAt: Date.now() + this.ttlMs
27
+ });
28
+ }
29
+ invalidate(key) {
30
+ this.store.delete(key);
31
+ }
32
+ clear() {
33
+ this.store.clear();
34
+ }
35
+ };
36
+
37
+ // src/lib/api-client.ts
38
+ var SystemDoxApiError = class extends Error {
39
+ constructor(status, text) {
40
+ super(`SystemDox API error ${status}: ${text}`);
41
+ this.status = status;
42
+ this.text = text;
43
+ let body;
44
+ try {
45
+ body = JSON.parse(text);
46
+ } catch {
47
+ body = void 0;
48
+ }
49
+ this.body = body;
50
+ const code = body?.code;
51
+ this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text)?.[1];
52
+ }
53
+ status;
54
+ text;
55
+ code;
56
+ body;
57
+ };
58
+ var TEAM_SETUP = "/v1/team-setup";
59
+ var seg = (s) => encodeURIComponent(String(s));
60
+ var SystemDoxApiClient = class {
61
+ config;
62
+ cache;
63
+ constructor(config) {
64
+ this.config = config;
65
+ this.cache = new ResponseCache(config.cacheTtlMs);
66
+ }
67
+ async validate() {
68
+ const res = await this.fetchRaw("/v1/health");
69
+ if (!res.ok) {
70
+ throw new Error(
71
+ `SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
72
+ );
73
+ }
74
+ }
75
+ async get(path, params) {
76
+ const url = this.buildUrl(path, params);
77
+ const cacheKey = url.toString();
78
+ const cached = this.cache.get(cacheKey);
79
+ if (cached !== void 0) return cached;
80
+ const res = await this.fetchRaw(path, params);
81
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
82
+ const data = await res.json();
83
+ this.cache.set(cacheKey, data);
84
+ return data;
85
+ }
86
+ /** A read that skips the cache: an install must take the version that is
87
+ * latest now, not the one cached before someone published another. */
88
+ async getFresh(path) {
89
+ const res = await this.fetchRaw(path);
90
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
91
+ return await res.json();
92
+ }
93
+ async post(path, body) {
94
+ return this.mutate("POST", path, body);
95
+ }
96
+ async put(path, body) {
97
+ return this.mutate("PUT", path, body);
98
+ }
99
+ async mutate(method, path, body) {
100
+ const url = this.buildUrl(path);
101
+ const res = await fetch(url, {
102
+ method,
103
+ headers: {
104
+ // The API Gateway authorizer only reads the Authorization header;
105
+ // X-API-Key is kept for backwards compatibility.
106
+ Authorization: `Bearer ${this.config.apiKey}`,
107
+ "X-API-Key": this.config.apiKey,
108
+ "Content-Type": "application/json"
109
+ },
110
+ body: JSON.stringify(body)
111
+ });
112
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
113
+ return await res.json();
114
+ }
115
+ // ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
116
+ // One method per operationId. Every read skips the cache: setup, sync and
117
+ // plugin-path act on what the workspace holds now.
118
+ /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
119
+ getOwnTeamSetup() {
120
+ return this.getFresh(TEAM_SETUP);
121
+ }
122
+ getTeamSetup(workspace) {
123
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
124
+ }
125
+ getMarketplace(workspace) {
126
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
127
+ }
128
+ getPart(workspace, part) {
129
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
130
+ }
131
+ publishPartVersion(workspace, part, files) {
132
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
133
+ }
134
+ getPartVersion(workspace, part, version) {
135
+ return this.getFresh(
136
+ `${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
137
+ );
138
+ }
139
+ getSetupSkills(workspace) {
140
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
141
+ }
142
+ setSetupSkills(workspace, skills) {
143
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
144
+ }
145
+ getPlugin(workspace) {
146
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
147
+ }
148
+ /** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
149
+ * header. The caller checks the BYTES against the hash it was given. */
150
+ async getPluginZip(workspace, version) {
151
+ const res = await fetch(
152
+ this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
153
+ {
154
+ headers: {
155
+ Authorization: `Bearer ${this.config.apiKey}`,
156
+ "X-API-Key": this.config.apiKey,
157
+ Accept: "application/zip"
158
+ }
159
+ }
160
+ );
161
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
162
+ return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
163
+ }
164
+ listMemoryProjects(workspace) {
165
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
166
+ }
167
+ getMemory(workspace, project) {
168
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
169
+ }
170
+ /** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
171
+ * code SYD-K010 and `body.currentVersion`. */
172
+ pushMemoryVersion(workspace, project, body) {
173
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
174
+ }
175
+ getMemoryVersion(workspace, project, version) {
176
+ return this.getFresh(
177
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
178
+ );
179
+ }
180
+ findMissingBlobs(workspace, project, hashes) {
181
+ return this.post(
182
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
183
+ { hashes }
184
+ );
185
+ }
186
+ uploadBlobs(workspace, project, blobs) {
187
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
188
+ }
189
+ readBlobs(workspace, project, hashes) {
190
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
191
+ }
192
+ listValueAnswers(workspace) {
193
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
194
+ }
195
+ lookupValues(workspace, values) {
196
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
197
+ }
198
+ answerValues(workspace, answers) {
199
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
200
+ }
201
+ fileMemoryPeople(workspace, people) {
202
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people });
203
+ }
204
+ resolvePerson(workspace, personId) {
205
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
206
+ }
207
+ invalidateCache(path) {
208
+ if (path) {
209
+ this.cache.invalidate(this.buildUrl(path).toString());
210
+ } else {
211
+ this.cache.clear();
212
+ }
213
+ }
214
+ buildUrl(path, params) {
215
+ const base = this.config.apiUrl.replace(/\/+$/, "");
216
+ const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
217
+ if (params) {
218
+ for (const [key, value] of Object.entries(params)) {
219
+ url.searchParams.set(key, value);
220
+ }
221
+ }
222
+ return url;
223
+ }
224
+ async fetchRaw(path, params) {
225
+ const url = this.buildUrl(path, params);
226
+ return fetch(url, {
227
+ headers: {
228
+ Authorization: `Bearer ${this.config.apiKey}`,
229
+ "X-API-Key": this.config.apiKey,
230
+ Accept: "application/json"
231
+ }
232
+ });
233
+ }
234
+ };
235
+
236
+ // src/lib/config.ts
237
+ var API_URL_DEFAULT = "https://api.puglieseweb.com/systemdox";
238
+ function requireHttpsApiUrl(apiUrl) {
239
+ let parsed;
240
+ try {
241
+ parsed = new URL(apiUrl);
242
+ } catch {
243
+ throw new Error(
244
+ `SYSTEMDOX_API_URL is not a valid URL: ${apiUrl}`
245
+ );
246
+ }
247
+ if (parsed.protocol !== "https:") {
248
+ throw new Error(
249
+ `SYSTEMDOX_API_URL must use https:// \u2014 refusing to send the API key over ${parsed.protocol}// in cleartext. Got: ${apiUrl}`
250
+ );
251
+ }
252
+ return apiUrl;
253
+ }
254
+
255
+ // src/lib/team-setup-files.ts
256
+ import { execFile } from "child_process";
257
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
258
+ import { homedir, userInfo } from "os";
259
+ import { dirname, join } from "path";
260
+ import { promisify } from "util";
261
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
262
+ function isWorkspace(value) {
263
+ return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
264
+ }
265
+ function homeDir() {
266
+ return homedir();
267
+ }
268
+ function systemdoxDir() {
269
+ return join(homeDir(), ".claude", "systemdox");
270
+ }
271
+ function credentialsPath() {
272
+ return join(systemdoxDir(), "credentials.json");
273
+ }
274
+ async function readJson(path, empty) {
275
+ let text;
276
+ try {
277
+ text = await readFile(path, "utf8");
278
+ } catch (e) {
279
+ if (e.code === "ENOENT") return empty;
280
+ throw e;
281
+ }
282
+ try {
283
+ return JSON.parse(text);
284
+ } catch {
285
+ throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
286
+ }
287
+ }
288
+ async function writeJsonAtomically(path, value, mode) {
289
+ await mkdir(dirname(path), { recursive: true });
290
+ const tmp = `${path}.${process.pid}.tmp`;
291
+ await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
292
+ await rename(tmp, path);
293
+ }
294
+ async function readCredentials() {
295
+ return readJson(credentialsPath(), {});
296
+ }
297
+ var run = promisify(execFile);
298
+ async function restrictToMember(path) {
299
+ await chmod(path, 384);
300
+ if (process.platform === "win32") {
301
+ await run("icacls", [path, "/inheritance:r", "/grant:r", `${userInfo().username}:F`], { windowsHide: true });
302
+ }
303
+ }
304
+ async function writeCredentials(workspace, apiKey) {
305
+ if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
306
+ if (!apiKey) throw new Error("an API key is required");
307
+ const path = credentialsPath();
308
+ const credentials = await readCredentials();
309
+ credentials[workspace] = { apiKey };
310
+ await writeJsonAtomically(path, credentials, 384);
311
+ await restrictToMember(path);
312
+ }
313
+
314
+ // src/setup-team.ts
315
+ import { execFile as execFile2 } from "child_process";
316
+ import { createHash as createHash2 } from "crypto";
317
+ import { existsSync as existsSync2 } from "fs";
318
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
319
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
320
+ import { promisify as promisify2 } from "util";
321
+
322
+ // src/lib/memory-sync.ts
323
+ import { createHash } from "crypto";
324
+
325
+ // src/lib/person-values.ts
326
+ var W = "[\\p{L}\\p{N}_]";
327
+ var D = "\\p{Nd}";
328
+ var WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
329
+ var PERSON_ID = "(?:(?:con|ctc)_[0-9A-Za-z]{10,40}|usr_[0-9A-Za-z-]{10,40})";
330
+ var PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
331
+ var BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
332
+ var AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
333
+ var E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
334
+ var NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
335
+ var ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
336
+ var EMAIL_RE = "(?<![A-Za-z0-9!#$%&*+/=?^_{|}~.@\\-])[A-Za-z0-9_]" + ATEXT + "*(?:\\." + ATEXT + "+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}(?![A-Za-z0-9\\-]|\\.[A-Za-z0-9])";
337
+ var HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
338
+ var HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
339
+ var KEY_PREFIX = `(?<!${W})(?:pweb_[a-z]+_key_|sk-ant-|sk_live_|rk_live_|github_pat_|gh[pousr]_|sntrys_|xox[baprs]-)[A-Za-z0-9_\\-]{4,}`;
340
+
341
+ // src/sync-memory.ts
342
+ import { existsSync } from "fs";
343
+ import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
344
+ import { dirname as dirname2, join as join2, resolve } from "path";
345
+
346
+ // src/setup-team.ts
347
+ var SetupFailed = class extends Error {
348
+ };
349
+ var run2 = promisify2(execFile2);
350
+ function skipSpace(t, i) {
351
+ while (i < t.length && /\s/.test(t[i])) i++;
352
+ return i;
353
+ }
354
+ function skipString(t, i) {
355
+ for (i++; i < t.length; i++) {
356
+ if (t[i] === "\\") i++;
357
+ else if (t[i] === '"') return i + 1;
358
+ }
359
+ return i;
360
+ }
361
+ function skipValue(t, i) {
362
+ if (t[i] === '"') return skipString(t, i);
363
+ if (t[i] === "{" || t[i] === "[") {
364
+ let depth = 0;
365
+ while (i < t.length) {
366
+ const c = t[i];
367
+ if (c === '"') {
368
+ i = skipString(t, i);
369
+ continue;
370
+ }
371
+ if (c === "{" || c === "[") depth++;
372
+ else if (c === "}" || c === "]") {
373
+ depth--;
374
+ if (depth === 0) return i + 1;
375
+ }
376
+ i++;
377
+ }
378
+ return i;
379
+ }
380
+ while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
381
+ return i;
382
+ }
383
+ function objectMembers(t, open) {
384
+ const members = [];
385
+ let i = skipSpace(t, open + 1);
386
+ if (t[i] === "}") return { members, close: i };
387
+ for (; ; ) {
388
+ i = skipSpace(t, i);
389
+ const keyStart = i;
390
+ const keyEnd = skipString(t, i);
391
+ const key = JSON.parse(t.slice(keyStart, keyEnd));
392
+ i = skipSpace(t, skipSpace(t, keyEnd) + 1);
393
+ const valueEnd = skipValue(t, i);
394
+ members.push({ key, keyStart, valueStart: i, valueEnd });
395
+ i = skipSpace(t, valueEnd);
396
+ if (t[i] === ",") i++;
397
+ else return { members, close: i };
398
+ }
399
+ }
400
+ function lineIndent(t, pos) {
401
+ const start = t.lastIndexOf("\n", pos - 1) + 1;
402
+ return /^[ \t]*/.exec(t.slice(start))[0];
403
+ }
404
+ function setJsonMember(text, keys, value) {
405
+ const root = JSON.parse(text);
406
+ if (!root || typeof root !== "object" || Array.isArray(root)) throw new Error("it does not hold a JSON object");
407
+ const compact = !text.trimEnd().includes("\n");
408
+ const unit = /^([ \t]+)"/m.exec(text)?.[1] ?? " ";
409
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
410
+ const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
411
+ const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
412
+ let open = skipSpace(text, 0);
413
+ for (let k = 0; k < keys.length; k++) {
414
+ const { members, close } = objectMembers(text, open);
415
+ const found = members.filter((m) => m.key === keys[k]).at(-1);
416
+ if (found) {
417
+ if (k < keys.length - 1 && text[found.valueStart] === "{") {
418
+ open = found.valueStart;
419
+ continue;
420
+ }
421
+ return text.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text, found.keyStart)) + text.slice(found.valueEnd);
422
+ }
423
+ const name = JSON.stringify(keys[k]);
424
+ if (compact) {
425
+ const member = `${name}:${render(nest(k + 1), "")}`;
426
+ if (members.length) return text.slice(0, members.at(-1).valueEnd) + "," + member + text.slice(members.at(-1).valueEnd);
427
+ return text.slice(0, open + 1) + member + text.slice(close);
428
+ }
429
+ if (members.length) {
430
+ const last = members.at(-1);
431
+ const indent2 = lineIndent(text, last.keyStart);
432
+ return text.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text.slice(last.valueEnd);
433
+ }
434
+ const outer = lineIndent(text, open);
435
+ const indent = outer + unit;
436
+ return text.slice(0, open + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text.slice(close);
437
+ }
438
+ return text;
439
+ }
440
+ function jsonAt(value, keys) {
441
+ let v = value;
442
+ for (const k of keys) v = v && typeof v === "object" ? v[k] : void 0;
443
+ return v;
444
+ }
445
+ async function setInJsonFile(path, keys, value) {
446
+ let text;
447
+ try {
448
+ text = await readFile3(path, "utf8");
449
+ } catch (e) {
450
+ if (e.code !== "ENOENT") throw e;
451
+ }
452
+ let next;
453
+ if (text === void 0 || !text.trim()) {
454
+ next = JSON.stringify(keys.reduceRight((acc, k) => ({ [k]: acc }), value), null, 2) + "\n";
455
+ } else {
456
+ let current;
457
+ try {
458
+ current = JSON.parse(text);
459
+ } catch {
460
+ throw new SetupFailed(`${path} is not valid JSON: fix it, then run setup again.`);
461
+ }
462
+ if (JSON.stringify(jsonAt(current, keys)) === JSON.stringify(value)) return;
463
+ try {
464
+ next = setJsonMember(text, keys, value);
465
+ } catch (e) {
466
+ throw new SetupFailed(`${path}: ${e.message}; fix it, then run setup again.`);
467
+ }
468
+ }
469
+ await mkdir3(dirname3(path), { recursive: true });
470
+ await writeFile3(path, next, "utf8");
471
+ }
472
+
473
+ // src/setup-mcp.ts
474
+ var isReference = (value) => typeof value === "string" && /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value);
475
+ async function validateApiKey(apiKey, apiUrl) {
476
+ try {
477
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/v1/health`, {
478
+ headers: {
479
+ Authorization: `Bearer ${apiKey}`,
480
+ "X-API-Key": apiKey,
481
+ Accept: "application/json"
482
+ }
483
+ });
484
+ return res.ok;
485
+ } catch {
486
+ return false;
487
+ }
488
+ }
489
+ function findConfigTargets(cwd = process.cwd()) {
490
+ const home = homeDir();
491
+ return [
492
+ // Claude Code: the project's .mcp.json, and ~/.claude.json (all projects)
493
+ { name: "Claude Code (project)", path: join4(cwd, ".mcp.json"), detected: true },
494
+ { name: "Claude Code (user \u2014 all projects)", path: join4(home, ".claude.json"), detected: true },
495
+ { name: "Cursor (project)", path: join4(cwd, ".cursor", "mcp.json"), detected: existsSync3(join4(cwd, ".cursor")) },
496
+ { name: "VS Code (project)", path: join4(cwd, ".vscode", "mcp.json"), detected: existsSync3(join4(cwd, ".vscode")) },
497
+ { name: "Cursor (global)", path: join4(home, ".cursor", "mcp.json"), detected: existsSync3(join4(home, ".cursor")) }
498
+ ];
499
+ }
500
+ function serverEntry(apiUrl, existing, workspace) {
501
+ const env = {};
502
+ const old = existing && typeof existing === "object" ? existing.env : void 0;
503
+ if (old && typeof old === "object" && !Array.isArray(old)) {
504
+ for (const [name, value] of Object.entries(old)) {
505
+ if (typeof value !== "string" || name === "SYSTEMDOX_API_URL" || name === "SYSTEMDOX_WORKSPACE") continue;
506
+ if (name === "SYSTEMDOX_API_KEY" && !isReference(value)) continue;
507
+ env[name] = value;
508
+ }
509
+ }
510
+ if (apiUrl !== API_URL_DEFAULT) env.SYSTEMDOX_API_URL = apiUrl;
511
+ if (workspace) env.SYSTEMDOX_WORKSPACE = workspace;
512
+ const entry = { command: "npx", args: ["@systemdox/mcp-server"] };
513
+ if (Object.keys(env).length) entry.env = env;
514
+ return entry;
515
+ }
516
+ function currentEntry(path) {
517
+ let text;
518
+ try {
519
+ text = readFileSync(path, "utf8");
520
+ } catch (e) {
521
+ if (e.code === "ENOENT") return void 0;
522
+ throw e;
523
+ }
524
+ if (!text.trim()) return void 0;
525
+ let config;
526
+ try {
527
+ config = JSON.parse(text);
528
+ } catch {
529
+ throw new Error(`${path} is not valid JSON: fix it, then run setup again.`);
530
+ }
531
+ const servers = config && typeof config === "object" ? config.mcpServers : void 0;
532
+ return servers && typeof servers === "object" ? servers.systemdox : void 0;
533
+ }
534
+ async function writeServerEntry(path, apiUrl, workspace) {
535
+ const existing = currentEntry(path);
536
+ const env = existing && typeof existing === "object" ? existing.env : void 0;
537
+ const heldKey = !!env && typeof env === "object" && "SYSTEMDOX_API_KEY" in env && !isReference(env.SYSTEMDOX_API_KEY);
538
+ await setInJsonFile(path, ["mcpServers", "systemdox"], serverEntry(apiUrl, existing, workspace));
539
+ return heldKey;
540
+ }
541
+ async function keyWorkspace(opts, apiKey, apiUrl) {
542
+ const client = opts.client?.(apiKey, apiUrl) ?? new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
543
+ try {
544
+ const { workspace } = await client.getOwnTeamSetup();
545
+ if (isWorkspace(workspace)) return workspace;
546
+ opts.out(` SystemDox named the workspace ${JSON.stringify(workspace)}, which is not a workspace name.`);
547
+ } catch (e) {
548
+ opts.out(` SystemDox did not name your key's workspace (${e.message}).`);
549
+ }
550
+ const fromEnv = (opts.env ?? process.env).SYSTEMDOX_WORKSPACE;
551
+ if (isWorkspace(fromEnv)) return fromEnv;
552
+ const typed = (await opts.ask(" Your workspace (acme in acme.systemdox.com): ")).trim().toLowerCase();
553
+ return isWorkspace(typed) ? typed : void 0;
554
+ }
555
+ async function setupMcpServer(opts) {
556
+ const { out, err, ask } = opts;
557
+ out("\n SystemDox MCP Server Setup\n");
558
+ const apiKey = (await ask(" Enter your SystemDox API key: ")).trim();
559
+ if (!apiKey) {
560
+ err("\n Error: API key is required.\n Generate one at your SystemDox settings page.\n");
561
+ return { code: 1 };
562
+ }
563
+ const apiUrl = (await ask(` API URL [${API_URL_DEFAULT}]: `)).trim() || API_URL_DEFAULT;
564
+ try {
565
+ requireHttpsApiUrl(apiUrl);
566
+ } catch (e) {
567
+ err(`
568
+ Error: ${e.message}
569
+ `);
570
+ return { code: 1 };
571
+ }
572
+ out("\n Validating API key...");
573
+ if (!await (opts.validateKey ?? validateApiKey)(apiKey, apiUrl)) {
574
+ err("\n Error: Invalid API key or cannot reach the SystemDox API.\n Check your key and try again.\n");
575
+ return { code: 1 };
576
+ }
577
+ out(" API key validated successfully.\n");
578
+ const workspace = await keyWorkspace(opts, apiKey, apiUrl);
579
+ if (!workspace) {
580
+ err("\n Error: that is not a workspace name (lower case letters, digits and -). Nothing was changed.\n");
581
+ return { code: 1 };
582
+ }
583
+ const detected = findConfigTargets(opts.cwd).filter((t) => t.detected);
584
+ out(" Available targets:");
585
+ detected.forEach((t, i) => out(` ${i + 1}. ${t.name} (${t.path})`));
586
+ const choice = (await ask(`
587
+ Which target? [1-${detected.length}, or 'all']: `)).trim();
588
+ let selected;
589
+ if (choice.toLowerCase() === "all") {
590
+ selected = detected;
591
+ } else {
592
+ const idx = parseInt(choice, 10) - 1;
593
+ if (isNaN(idx) || idx < 0 || idx >= detected.length) {
594
+ err("\n Invalid selection.\n");
595
+ return { code: 1 };
596
+ }
597
+ selected = [detected[idx]];
598
+ }
599
+ try {
600
+ if ((await readCredentials())[workspace]?.apiKey !== apiKey) await writeCredentials(workspace, apiKey);
601
+ const several = Object.keys(await readCredentials()).length > 1;
602
+ for (const target of selected) {
603
+ const removed = await writeServerEntry(target.path, apiUrl, several ? workspace : void 0);
604
+ out(` Configured: ${target.name}`);
605
+ if (removed) out(` The key now lives in ~/.claude/systemdox/credentials.json and was removed from ${target.path}.`);
606
+ }
607
+ } catch (e) {
608
+ err(`
609
+ Error: ${e.message}
610
+ `);
611
+ return { code: 1 };
612
+ }
613
+ out(
614
+ `
615
+ Your key for ${workspace} is in ${credentialsPath()} (readable by you alone), not in these configs.
616
+
617
+ Done! Restart your AI coding tool to activate the SystemDox MCP server.
618
+ The server provides 21 tools: get_context, get_checks,
619
+ get_project_rules, get_prompt_templates, get_specs, get_docs,
620
+ get_tech_stack, get_planning_context, get_plan_board, get_change,
621
+ create_check, create_project_rule, create_prompt_template,
622
+ update_spec, report_decision, publish_skill, list_skills,
623
+ install_skill, publish_setup_part, resolve_person, sync_memory.
624
+
625
+ get_guardrails and create_guardrail remain registered as deprecated
626
+ aliases of get_checks and create_check \u2014 existing configs keep working.
627
+ `
628
+ );
629
+ return { code: 0, apiKey, apiUrl, workspace };
630
+ }
631
+ export {
632
+ findConfigTargets,
633
+ serverEntry,
634
+ setupMcpServer,
635
+ writeServerEntry
636
+ };
@@ -17,7 +17,8 @@
17
17
  * 4. Finish: the two steps a person must take in Claude Code.
18
18
  *
19
19
  * It never runs `claude plugin marketplace add` and never sets
20
- * `enabledPlugins`. The key is written to credentials.json only.
20
+ * `enabledPlugins`. The key is written to credentials.json only (the MCP
21
+ * server step, setup-mcp.ts, has stored it there already).
21
22
  *
22
23
  * Exit codes: 0 done; 1 usage, no key or the API refused; 2 an edited team
23
24
  * CLAUDE.md would be replaced; 3 Claude Code missing or too old; 4 not
@@ -60,6 +61,9 @@ export declare function claudeCodeVersion(): Promise<string | undefined>;
60
61
  * last one in the file's own indentation and line ending.
61
62
  */
62
63
  export declare function setJsonMember(text: string, keys: string[], value: unknown): string;
64
+ /** Set `keys` to `value` in the JSON file at `path`, creating it when absent;
65
+ * nothing is written when it already holds that value. */
66
+ export declare function setInJsonFile(path: string, keys: string[], value: unknown): Promise<void>;
63
67
  export declare function importBlock(workspace: string, eol?: string): string;
64
68
  /** `text` with the workspace's marked block added at the end, or replaced
65
69
  * where it is; nothing outside the block changes. */
@@ -1588,6 +1588,7 @@ export {
1588
1588
  parseSetupArgs,
1589
1589
  runTeamSetup,
1590
1590
  runTeamSetupCommand,
1591
+ setInJsonFile,
1591
1592
  setJsonMember,
1592
1593
  withImportBlock
1593
1594
  };