@systemdox/mcp-server 0.1.46 → 0.1.48

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,1593 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/setup-team.ts
4
+ import { execFile as execFile2 } from "child_process";
5
+ import { createHash as createHash2 } from "crypto";
6
+ import { existsSync as existsSync2 } from "fs";
7
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
8
+ import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
9
+ import { promisify as promisify2 } from "util";
10
+
11
+ // src/lib/cache.ts
12
+ var ResponseCache = class {
13
+ store = /* @__PURE__ */ new Map();
14
+ ttlMs;
15
+ constructor(ttlMs) {
16
+ this.ttlMs = ttlMs;
17
+ }
18
+ get(key) {
19
+ const entry = this.store.get(key);
20
+ if (!entry) return void 0;
21
+ if (Date.now() > entry.expiresAt) {
22
+ this.store.delete(key);
23
+ return void 0;
24
+ }
25
+ return entry.data;
26
+ }
27
+ set(key, data) {
28
+ this.store.set(key, {
29
+ data,
30
+ expiresAt: Date.now() + this.ttlMs
31
+ });
32
+ }
33
+ invalidate(key) {
34
+ this.store.delete(key);
35
+ }
36
+ clear() {
37
+ this.store.clear();
38
+ }
39
+ };
40
+
41
+ // src/lib/api-client.ts
42
+ var SystemDoxApiError = class extends Error {
43
+ constructor(status, text) {
44
+ super(`SystemDox API error ${status}: ${text}`);
45
+ this.status = status;
46
+ this.text = text;
47
+ let body;
48
+ try {
49
+ body = JSON.parse(text);
50
+ } catch {
51
+ body = void 0;
52
+ }
53
+ this.body = body;
54
+ const code = body?.code;
55
+ this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text)?.[1];
56
+ }
57
+ status;
58
+ text;
59
+ code;
60
+ body;
61
+ };
62
+ var TEAM_SETUP = "/v1/team-setup";
63
+ var seg = (s) => encodeURIComponent(String(s));
64
+ var SystemDoxApiClient = class {
65
+ config;
66
+ cache;
67
+ constructor(config) {
68
+ this.config = config;
69
+ this.cache = new ResponseCache(config.cacheTtlMs);
70
+ }
71
+ async validate() {
72
+ const res = await this.fetchRaw("/v1/health");
73
+ if (!res.ok) {
74
+ throw new Error(
75
+ `SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
76
+ );
77
+ }
78
+ }
79
+ async get(path, params) {
80
+ const url = this.buildUrl(path, params);
81
+ const cacheKey = url.toString();
82
+ const cached = this.cache.get(cacheKey);
83
+ if (cached !== void 0) return cached;
84
+ const res = await this.fetchRaw(path, params);
85
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
86
+ const data = await res.json();
87
+ this.cache.set(cacheKey, data);
88
+ return data;
89
+ }
90
+ /** A read that skips the cache: an install must take the version that is
91
+ * latest now, not the one cached before someone published another. */
92
+ async getFresh(path) {
93
+ const res = await this.fetchRaw(path);
94
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
95
+ return await res.json();
96
+ }
97
+ async post(path, body) {
98
+ return this.mutate("POST", path, body);
99
+ }
100
+ async put(path, body) {
101
+ return this.mutate("PUT", path, body);
102
+ }
103
+ async mutate(method, path, body) {
104
+ const url = this.buildUrl(path);
105
+ const res = await fetch(url, {
106
+ method,
107
+ headers: {
108
+ // The API Gateway authorizer only reads the Authorization header;
109
+ // X-API-Key is kept for backwards compatibility.
110
+ Authorization: `Bearer ${this.config.apiKey}`,
111
+ "X-API-Key": this.config.apiKey,
112
+ "Content-Type": "application/json"
113
+ },
114
+ body: JSON.stringify(body)
115
+ });
116
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
117
+ return await res.json();
118
+ }
119
+ // ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
120
+ // One method per operationId. Every read skips the cache: setup, sync and
121
+ // plugin-path act on what the workspace holds now.
122
+ /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
123
+ getOwnTeamSetup() {
124
+ return this.getFresh(TEAM_SETUP);
125
+ }
126
+ getTeamSetup(workspace) {
127
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
128
+ }
129
+ getMarketplace(workspace) {
130
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
131
+ }
132
+ getPart(workspace, part) {
133
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
134
+ }
135
+ publishPartVersion(workspace, part, files) {
136
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
137
+ }
138
+ getPartVersion(workspace, part, version) {
139
+ return this.getFresh(
140
+ `${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
141
+ );
142
+ }
143
+ getSetupSkills(workspace) {
144
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
145
+ }
146
+ setSetupSkills(workspace, skills) {
147
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
148
+ }
149
+ getPlugin(workspace) {
150
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
151
+ }
152
+ /** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
153
+ * header. The caller checks the BYTES against the hash it was given. */
154
+ async getPluginZip(workspace, version) {
155
+ const res = await fetch(
156
+ this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
157
+ {
158
+ headers: {
159
+ Authorization: `Bearer ${this.config.apiKey}`,
160
+ "X-API-Key": this.config.apiKey,
161
+ Accept: "application/zip"
162
+ }
163
+ }
164
+ );
165
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
166
+ return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
167
+ }
168
+ listMemoryProjects(workspace) {
169
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
170
+ }
171
+ getMemory(workspace, project) {
172
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
173
+ }
174
+ /** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
175
+ * code SYD-K010 and `body.currentVersion`. */
176
+ pushMemoryVersion(workspace, project, body) {
177
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
178
+ }
179
+ getMemoryVersion(workspace, project, version) {
180
+ return this.getFresh(
181
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
182
+ );
183
+ }
184
+ findMissingBlobs(workspace, project, hashes) {
185
+ return this.post(
186
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
187
+ { hashes }
188
+ );
189
+ }
190
+ uploadBlobs(workspace, project, blobs) {
191
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
192
+ }
193
+ readBlobs(workspace, project, hashes) {
194
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
195
+ }
196
+ listValueAnswers(workspace) {
197
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
198
+ }
199
+ lookupValues(workspace, values) {
200
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
201
+ }
202
+ answerValues(workspace, answers) {
203
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
204
+ }
205
+ fileMemoryPeople(workspace, people2) {
206
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people: people2 });
207
+ }
208
+ resolvePerson(workspace, personId) {
209
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
210
+ }
211
+ invalidateCache(path) {
212
+ if (path) {
213
+ this.cache.invalidate(this.buildUrl(path).toString());
214
+ } else {
215
+ this.cache.clear();
216
+ }
217
+ }
218
+ buildUrl(path, params) {
219
+ const base = this.config.apiUrl.replace(/\/+$/, "");
220
+ const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
221
+ if (params) {
222
+ for (const [key, value] of Object.entries(params)) {
223
+ url.searchParams.set(key, value);
224
+ }
225
+ }
226
+ return url;
227
+ }
228
+ async fetchRaw(path, params) {
229
+ const url = this.buildUrl(path, params);
230
+ return fetch(url, {
231
+ headers: {
232
+ Authorization: `Bearer ${this.config.apiKey}`,
233
+ "X-API-Key": this.config.apiKey,
234
+ Accept: "application/json"
235
+ }
236
+ });
237
+ }
238
+ };
239
+
240
+ // src/lib/memory-sync.ts
241
+ import { createHash } from "crypto";
242
+
243
+ // src/lib/person-values.ts
244
+ var PHONE = "phone";
245
+ var EMAIL = "email";
246
+ var HOME_PATH = "home-path";
247
+ var NATIONAL_PREFIX = "+44";
248
+ var W = "[\\p{L}\\p{N}_]";
249
+ var D = "\\p{Nd}";
250
+ var WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
251
+ var PERSON_ID = "(?:(?:con|ctc)_[0-9A-Za-z]{10,40}|usr_[0-9A-Za-z-]{10,40})";
252
+ var PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
253
+ var BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
254
+ var AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
255
+ var E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
256
+ var NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
257
+ var GROUP_SIZES = /* @__PURE__ */ new Set([2, 3, 4, 5, 6]);
258
+ var ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
259
+ 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])";
260
+ var HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
261
+ var HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
262
+ 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,}`;
263
+ var global = (source, flags = "") => new RegExp(source, `gu${flags}`);
264
+ var whole = (source, flags = "") => new RegExp(`^(?:${source})$`, `u${flags}`);
265
+ function lineOf(text, start) {
266
+ let line = 1;
267
+ for (let i = text.indexOf("\n"); i !== -1 && i < start; i = text.indexOf("\n", i + 1)) line++;
268
+ return line;
269
+ }
270
+ function phoneShape(value) {
271
+ const groups = value.replace(/^\++/, "").split(/[ -]/);
272
+ const digits = [...groups.join("")];
273
+ if (value.startsWith("+")) return digits.length >= 8 && digits.length <= 15 && digits[0] !== "0";
274
+ if (digits.length < 10 || digits.length > 11) return false;
275
+ return groups.length === 1 || groups.slice(1).every((g) => GROUP_SIZES.has([...g].length));
276
+ }
277
+ function normalise(kind, value) {
278
+ const v = typeof value === "string" ? value.trim() : "";
279
+ if (kind === PHONE) {
280
+ if (!(whole(E164).test(v) || whole(NATIONAL).test(v)) || !phoneShape(v)) {
281
+ throw new Error("not a phone number");
282
+ }
283
+ const digits = v.replace(/[ -]/g, "");
284
+ return digits.startsWith("+") ? digits : NATIONAL_PREFIX + digits.slice(1);
285
+ }
286
+ if (kind === EMAIL) {
287
+ if (!whole(EMAIL_RE).test(v)) throw new Error("not an email address");
288
+ return v.toLowerCase();
289
+ }
290
+ throw new Error(`not a value kind: ${JSON.stringify(kind)}`);
291
+ }
292
+ function findPersonValues(text) {
293
+ if (typeof text !== "string" || !text) return [];
294
+ const skipped = [...text.matchAll(global(PERSON_TOKEN))].map((m) => [m.index, m.index + m[0].length]);
295
+ const found = [];
296
+ const add = (kind, m, normalised) => {
297
+ const start = m.index;
298
+ const end = start + m[0].length;
299
+ if (skipped.some(([s, e]) => s <= start && end <= e)) return;
300
+ if (found.some((f) => f.start < end && start < f.end)) return;
301
+ found.push({ kind, value: m[0], normalised, line: lineOf(text, start), start, end });
302
+ };
303
+ for (const m of text.matchAll(global(EMAIL_RE))) add(EMAIL, m, m[0].toLowerCase());
304
+ for (const source of [E164, NATIONAL]) {
305
+ for (const m of text.matchAll(global(source))) {
306
+ if (phoneShape(m[0])) add(PHONE, m, normalise(PHONE, m[0]));
307
+ }
308
+ }
309
+ for (const m of text.matchAll(global(HOME, "i"))) add(HOME_PATH, m, m[0]);
310
+ return found.sort((a, b) => a.start - b.start);
311
+ }
312
+ function findSecrets(text) {
313
+ if (typeof text !== "string" || !text) return [];
314
+ return [...text.matchAll(global(KEY_PREFIX))].map((m) => ({ kind: "key-prefix", line: lineOf(text, m.index) }));
315
+ }
316
+
317
+ // src/lib/memory-sync.ts
318
+ var INDEX_FILE = "MEMORY.md";
319
+ var SHAREABLE_TYPES = ["feedback", "project", "reference"];
320
+ var FILE_PATH_RE = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
321
+ var CONFLICT_COPY_RE = /\.local-\d{4}-\d{2}-\d{2}(?:-\d+)?\.md$/;
322
+ function sha256(text) {
323
+ return createHash("sha256").update(text, "utf8").digest("hex");
324
+ }
325
+ function isFilePath(path) {
326
+ return path.length <= 512 && FILE_PATH_RE.test(path) && !path.split("/").some((s) => s === "." || s === "..");
327
+ }
328
+ function isConflictCopy(path) {
329
+ return CONFLICT_COPY_RE.test(path.split("/").pop() ?? path);
330
+ }
331
+ var BLOCK_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
332
+ var YAML_FALSE = /* @__PURE__ */ new Set(["false", "False", "FALSE", "no", "No", "NO", "off", "Off", "OFF"]);
333
+ function readFrontmatter(text) {
334
+ const m = BLOCK_RE.exec(text);
335
+ if (!m) return void 0;
336
+ const front = { shareFalse: false };
337
+ for (const line of m[1].split(/\r?\n/)) {
338
+ const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
339
+ if (!kv) continue;
340
+ const raw = kv[2].replace(/[ \t]+#.*$/, "");
341
+ const quoted = /^(["'])(.*)\1$/.exec(raw);
342
+ const value = quoted ? quoted[2] : raw;
343
+ if (kv[1] === "type") front.type = value.trim().toLowerCase();
344
+ if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
345
+ }
346
+ return front;
347
+ }
348
+ function isShareable(path, text) {
349
+ if (isConflictCopy(path) || !path.endsWith(".md")) return false;
350
+ const front = readFrontmatter(text);
351
+ if (front?.shareFalse) return false;
352
+ if (path === INDEX_FILE) return true;
353
+ return !!front && SHAREABLE_TYPES.includes(front.type ?? "");
354
+ }
355
+ function addShareFalse(text) {
356
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
357
+ const m = BLOCK_RE.exec(text);
358
+ if (!m) return `---${eol}share: false${eol}---${eol}${text}`;
359
+ const lines = m[1].split(/\r?\n/).filter((l) => !/^share[ \t]*:/.test(l));
360
+ lines.push("share: false");
361
+ const closing = m[0].slice(m[0].lastIndexOf("---"));
362
+ return `---${eol}${lines.join(eol)}${eol}${closing}${text.slice(m[0].length)}`;
363
+ }
364
+ function localDate(now) {
365
+ const p = (n) => String(n).padStart(2, "0");
366
+ return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
367
+ }
368
+ function conflictCopyName(path, date, taken) {
369
+ const stem = path.replace(/\.md$/, "");
370
+ for (let n = 1; ; n++) {
371
+ const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
372
+ if (!taken.has(name.toLowerCase())) return name;
373
+ }
374
+ }
375
+ function decide(path, base, team, local) {
376
+ if (team === local) return "unchanged";
377
+ if (team === base) return local === void 0 ? "delete-team" : "local";
378
+ if (local === base) return team === void 0 ? "delete-local" : "team";
379
+ if (path === INDEX_FILE && team !== void 0 && local !== void 0) return "index";
380
+ if (team === void 0) return "local";
381
+ if (local === void 0) return "team";
382
+ return "conflict";
383
+ }
384
+ function decideAll(input) {
385
+ const localHashes = Object.fromEntries(Object.entries(input.local).map(([p, t]) => [p, sha256(t)]));
386
+ const paths = /* @__PURE__ */ new Set([...Object.keys(input.base), ...Object.keys(input.team), ...Object.keys(localHashes)]);
387
+ const rows = {};
388
+ const needed = /* @__PURE__ */ new Set();
389
+ for (const path of [...paths].sort()) {
390
+ if (input.excluded.has(path)) continue;
391
+ const row = decide(path, input.base[path], input.team[path], localHashes[path]);
392
+ rows[path] = row;
393
+ if (row === "team" || row === "conflict" || row === "index") needed.add(input.team[path]);
394
+ if (row === "index" && input.base[path]) needed.add(input.base[path]);
395
+ }
396
+ return { rows, needed: [...needed] };
397
+ }
398
+ function planMerge(input, rows, texts) {
399
+ const plan = {
400
+ manifest: {},
401
+ upload: {},
402
+ writes: {},
403
+ deletes: [],
404
+ pulled: [],
405
+ pushed: [],
406
+ deletedHere: [],
407
+ deletedFromTeam: [],
408
+ conflicts: [],
409
+ indexTeamLines: []
410
+ };
411
+ const taken = new Set(input.taken);
412
+ const teamText = (path) => {
413
+ const text = texts[input.team[path]];
414
+ if (text === void 0) throw new Error(`the team's ${path} could not be read`);
415
+ return text;
416
+ };
417
+ for (const path of input.excluded) if (input.team[path]) plan.manifest[path] = input.team[path];
418
+ for (const [path, row] of Object.entries(rows)) {
419
+ const team = input.team[path];
420
+ const local = input.local[path];
421
+ switch (row) {
422
+ case "unchanged":
423
+ if (team) plan.manifest[path] = team;
424
+ break;
425
+ case "local":
426
+ plan.manifest[path] = sha256(local);
427
+ plan.upload[path] = local;
428
+ plan.pushed.push(path);
429
+ break;
430
+ case "delete-team":
431
+ plan.deletedFromTeam.push(path);
432
+ break;
433
+ case "team":
434
+ plan.manifest[path] = team;
435
+ plan.writes[path] = teamText(path);
436
+ plan.pulled.push(path);
437
+ break;
438
+ case "delete-local":
439
+ plan.deletes.push(path);
440
+ plan.deletedHere.push(path);
441
+ break;
442
+ case "conflict": {
443
+ const copy = conflictCopyName(path, input.date, taken);
444
+ taken.add(copy.toLowerCase());
445
+ plan.manifest[path] = team;
446
+ plan.writes[path] = teamText(path);
447
+ plan.writes[copy] = local;
448
+ plan.conflicts.push({ path, copy });
449
+ break;
450
+ }
451
+ case "index": {
452
+ const base = input.base[path] ? texts[input.base[path]] : void 0;
453
+ const merged = mergeIndex(base, teamText(path), local);
454
+ const hash = sha256(merged.text);
455
+ plan.manifest[path] = hash;
456
+ if (merged.text !== local) {
457
+ plan.writes[path] = merged.text;
458
+ plan.pulled.push(path);
459
+ }
460
+ if (hash !== team) {
461
+ plan.upload[path] = merged.text;
462
+ plan.pushed.push(path);
463
+ }
464
+ plan.indexTeamLines.push(...merged.teamLines);
465
+ break;
466
+ }
467
+ }
468
+ }
469
+ return plan;
470
+ }
471
+ var LINK_RE = /\]\(\s*<?([^)\s>]+)>?(?:\s+["'(][^)]*)?\)/;
472
+ function entries(lines) {
473
+ const seen = /* @__PURE__ */ new Map();
474
+ return lines.map((line) => {
475
+ const link = LINK_RE.exec(line);
476
+ const id = link ? `link:${link[1]}` : `text:${line}`;
477
+ const n = seen.get(id) ?? 0;
478
+ seen.set(id, n + 1);
479
+ return { key: n ? `${id}#${n}` : id, line };
480
+ });
481
+ }
482
+ var splitLines = (text) => text.split(/\r?\n/);
483
+ function mergeIndex(base, team, local) {
484
+ const eol = local.includes("\r\n") ? "\r\n" : "\n";
485
+ const b = new Map(entries(base === void 0 ? [] : splitLines(base)).map((e) => [e.key, e.line]));
486
+ const teamEntries = entries(splitLines(team));
487
+ const t = new Map(teamEntries.map((e) => [e.key, e.line]));
488
+ const localEntries = entries(splitLines(local));
489
+ const result = [];
490
+ const teamLines = [];
491
+ for (const e of localEntries) {
492
+ const was = b.get(e.key);
493
+ const theirs = t.get(e.key);
494
+ if (theirs === e.line || theirs === was) result.push(e);
495
+ else if (e.line === was) {
496
+ if (theirs !== void 0) result.push({ key: e.key, line: theirs });
497
+ } else {
498
+ result.push(e);
499
+ if (theirs !== void 0) teamLines.push(theirs);
500
+ }
501
+ }
502
+ const baseEntries = entries(base === void 0 ? [] : splitLines(base));
503
+ const localKeys = new Set(localEntries.map((e) => e.key));
504
+ const replacedHere = /* @__PURE__ */ new Map();
505
+ baseEntries.forEach((e, i) => {
506
+ if (e.key.startsWith("text:") && !t.has(e.key) && !localKeys.has(e.key)) {
507
+ replacedHere.set(e.key, i ? baseEntries[i - 1].key : "");
508
+ }
509
+ });
510
+ const inResult = (key) => result.findIndex((e) => e.key === key);
511
+ teamEntries.forEach((e, i) => {
512
+ if (inResult(e.key) !== -1) return;
513
+ const was = b.get(e.key);
514
+ const before = i ? teamEntries[i - 1].key : "";
515
+ const both = e.key.startsWith("text:") && [...replacedHere].find(([, prev]) => prev === before);
516
+ if (was === void 0 && both) {
517
+ replacedHere.delete(both[0]);
518
+ teamLines.push(e.line);
519
+ } else if (was === void 0) {
520
+ let at = -1;
521
+ for (let j = i - 1; j >= 0 && at === -1; j--) at = inResult(teamEntries[j].key);
522
+ result.splice(at + 1, 0, e);
523
+ } else if (e.line !== was) {
524
+ teamLines.push(e.line);
525
+ }
526
+ });
527
+ return { text: result.map((e) => e.line).join(eol), teamLines };
528
+ }
529
+ var groupKey = (kind, normalised) => `${kind}:${normalised}`;
530
+ function collectValues(files) {
531
+ const groups = /* @__PURE__ */ new Map();
532
+ const homePaths = [];
533
+ const findings = {};
534
+ for (const path of Object.keys(files).sort()) {
535
+ const found = findPersonValues(files[path]);
536
+ findings[path] = found;
537
+ for (const f of found) {
538
+ if (f.kind === HOME_PATH) {
539
+ if (!homePaths.includes(path)) homePaths.push(path);
540
+ continue;
541
+ }
542
+ const key = groupKey(f.kind, f.normalised);
543
+ const group = groups.get(key);
544
+ if (group) {
545
+ if (!group.files.includes(path)) group.files.push(path);
546
+ } else {
547
+ groups.set(key, {
548
+ kind: f.kind,
549
+ value: f.value,
550
+ normalised: f.normalised,
551
+ files: [path],
552
+ first: { path, line: f.line, text: splitLines(files[path])[f.line - 1].trim() }
553
+ });
554
+ }
555
+ }
556
+ }
557
+ return { groups: [...groups.values()], homePaths, findings };
558
+ }
559
+ function personToken(personId, label) {
560
+ return label ? `\`${personId}\` (${label})` : `\`${personId}\``;
561
+ }
562
+ function rewriteValues(text, findings, replacement) {
563
+ let out = "";
564
+ let last = 0;
565
+ for (const f of [...findings].sort((a, b) => a.start - b.start)) {
566
+ const to = replacement(f);
567
+ if (to === void 0) continue;
568
+ out += text.slice(last, f.start) + to;
569
+ last = f.end;
570
+ }
571
+ return out + text.slice(last);
572
+ }
573
+ function lineDiff(before, after) {
574
+ const a = splitLines(before);
575
+ const b = splitLines(after);
576
+ const out = [];
577
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
578
+ if (a[i] === b[i]) continue;
579
+ if (a[i] !== void 0) out.push(`- ${a[i]}`);
580
+ if (b[i] !== void 0) out.push(`+ ${b[i]}`);
581
+ }
582
+ return out;
583
+ }
584
+
585
+ // src/lib/team-setup-files.ts
586
+ import { execFile } from "child_process";
587
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
588
+ import { homedir, userInfo } from "os";
589
+ import { dirname, join } from "path";
590
+ import { promisify } from "util";
591
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
592
+ function isWorkspace(value) {
593
+ return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
594
+ }
595
+ function homeDir() {
596
+ return homedir();
597
+ }
598
+ function systemdoxDir() {
599
+ return join(homeDir(), ".claude", "systemdox");
600
+ }
601
+ function credentialsPath() {
602
+ return join(systemdoxDir(), "credentials.json");
603
+ }
604
+ function workspaceDir(workspace) {
605
+ if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
606
+ return join(systemdoxDir(), workspace);
607
+ }
608
+ function statePath(workspace) {
609
+ return join(workspaceDir(workspace), "state.json");
610
+ }
611
+ function pluginRoot(workspace) {
612
+ return join(workspaceDir(workspace), "plugin");
613
+ }
614
+ async function readJson(path, empty) {
615
+ let text;
616
+ try {
617
+ text = await readFile(path, "utf8");
618
+ } catch (e) {
619
+ if (e.code === "ENOENT") return empty;
620
+ throw e;
621
+ }
622
+ try {
623
+ return JSON.parse(text);
624
+ } catch {
625
+ throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
626
+ }
627
+ }
628
+ async function writeJsonAtomically(path, value, mode) {
629
+ await mkdir(dirname(path), { recursive: true });
630
+ const tmp = `${path}.${process.pid}.tmp`;
631
+ await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
632
+ await rename(tmp, path);
633
+ }
634
+ async function readCredentials() {
635
+ return readJson(credentialsPath(), {});
636
+ }
637
+ var run = promisify(execFile);
638
+ async function restrictToMember(path) {
639
+ await chmod(path, 384);
640
+ if (process.platform === "win32") {
641
+ await run("icacls", [path, "/inheritance:r", "/grant:r", `${userInfo().username}:F`], { windowsHide: true });
642
+ }
643
+ }
644
+ async function writeCredentials(workspace, apiKey) {
645
+ if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
646
+ if (!apiKey) throw new Error("an API key is required");
647
+ const path = credentialsPath();
648
+ const credentials = await readCredentials();
649
+ credentials[workspace] = { apiKey };
650
+ await writeJsonAtomically(path, credentials, 384);
651
+ await restrictToMember(path);
652
+ }
653
+ async function readState(workspace) {
654
+ return readJson(statePath(workspace), {});
655
+ }
656
+ async function writeState(workspace, state) {
657
+ await writeJsonAtomically(statePath(workspace), state);
658
+ }
659
+ async function updateState(workspace, change) {
660
+ const state = await readState(workspace);
661
+ change(state);
662
+ await writeState(workspace, state);
663
+ return state;
664
+ }
665
+ async function removeFolder(path) {
666
+ await rm(path, { recursive: true, force: true });
667
+ }
668
+
669
+ // src/sync-memory.ts
670
+ import { existsSync } from "fs";
671
+ import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
672
+ import { dirname as dirname2, join as join2, resolve } from "path";
673
+ var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
674
+ var SyncFailed = class extends Error {
675
+ };
676
+ function refusal(e) {
677
+ if (!(e instanceof Error)) return {};
678
+ const { status, code } = e;
679
+ return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
680
+ }
681
+ var toPath = (root, rel) => join2(root, ...rel.split("/"));
682
+ async function readFolder(root) {
683
+ const files = {};
684
+ const names = /* @__PURE__ */ new Set();
685
+ async function walk(dir, prefix) {
686
+ for (const e of await readdir(dir, { withFileTypes: true })) {
687
+ if (e.name.startsWith(".")) continue;
688
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
689
+ if (e.isDirectory()) await walk(join2(dir, e.name), rel);
690
+ else if (e.isFile()) {
691
+ names.add(rel.toLowerCase());
692
+ if (e.name.endsWith(".md")) files[rel] = await readFile2(join2(dir, e.name), "utf8");
693
+ }
694
+ }
695
+ }
696
+ if (existsSync(root)) await walk(root, "");
697
+ return { files, names };
698
+ }
699
+ async function pullTeam(client, ws, project) {
700
+ try {
701
+ const latest = await client.getMemory(ws, project);
702
+ return {
703
+ version: latest.version,
704
+ manifest: Object.fromEntries(Object.entries(latest.files).map(([p, f]) => [p, f.sha256])),
705
+ exists: true
706
+ };
707
+ } catch (e) {
708
+ if (refusal(e).status === 404) return { version: 0, manifest: {}, exists: false };
709
+ throw e;
710
+ }
711
+ }
712
+ async function readTexts(client, ws, project, hashes) {
713
+ const texts = {};
714
+ const one = async (batch) => {
715
+ let left = batch;
716
+ while (left.length) {
717
+ const got = await client.readBlobs(ws, project, left);
718
+ for (const b of got.blobs) texts[b.sha256] = b.content;
719
+ if (!got.blobs.length) break;
720
+ left = got.remaining;
721
+ }
722
+ };
723
+ for (let i = 0; i < hashes.length; i += 1e3) {
724
+ const batch = hashes.slice(i, i + 1e3);
725
+ try {
726
+ await one(batch);
727
+ } catch (e) {
728
+ if (!(refusal(e).status === 404)) throw e;
729
+ for (const h of batch) {
730
+ try {
731
+ await one([h]);
732
+ } catch (err) {
733
+ if (!(refusal(err).status === 404)) throw err;
734
+ }
735
+ }
736
+ }
737
+ }
738
+ return texts;
739
+ }
740
+ var Report = class {
741
+ pulled = /* @__PURE__ */ new Set();
742
+ pushed = /* @__PURE__ */ new Set();
743
+ deletedHere = /* @__PURE__ */ new Set();
744
+ deletedFromTeam = /* @__PURE__ */ new Set();
745
+ conflicts = [];
746
+ skipped = /* @__PURE__ */ new Map();
747
+ people = [];
748
+ toAnswer = /* @__PURE__ */ new Map();
749
+ add(plan) {
750
+ plan.pulled.forEach((p) => this.pulled.add(p));
751
+ plan.pushed.forEach((p) => this.pushed.add(p));
752
+ plan.deletedHere.forEach((p) => this.deletedHere.add(p));
753
+ plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
754
+ for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
755
+ for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
756
+ }
757
+ skip(path, reason) {
758
+ if (!this.skipped.has(path)) this.skipped.set(path, reason);
759
+ }
760
+ lines(summary, dryRun) {
761
+ const out = [summary];
762
+ const section = (title, items) => {
763
+ if (items.length) out.push("", `${title}:`, ...items.map((i) => `- ${i}`));
764
+ };
765
+ const sorted = (s) => [...s].sort();
766
+ section(dryRun ? "Would pull" : "Pulled", sorted(this.pulled));
767
+ section(dryRun ? "Would push" : "Pushed", sorted(this.pushed));
768
+ section(dryRun ? "Would delete" : "Deleted", [
769
+ ...sorted(this.deletedHere).map((p) => `${p} (here: the team deleted it)`),
770
+ ...sorted(this.deletedFromTeam).map((p) => `${p} (from the team: you deleted it)`)
771
+ ]);
772
+ section("Conflicts", this.conflicts);
773
+ section(
774
+ "Skipped",
775
+ [...this.skipped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([p, why]) => `${p}: ${why}`)
776
+ );
777
+ section(
778
+ "People",
779
+ this.people.map(
780
+ (a) => [a.kind, a.answer, a.label, a.personId && `\`${a.personId}\``, a.files !== void 0 && `${a.files} files`].filter(Boolean).join(", ")
781
+ )
782
+ );
783
+ if (this.toAnswer.size) {
784
+ out.push(
785
+ "",
786
+ `Values still to answer: ${[...this.toAnswer.entries()].map(([kind, n]) => `${n} ${kind}`).join(", ")}.`
787
+ );
788
+ }
789
+ return out;
790
+ }
791
+ };
792
+ var ANSWER_WORDS = {
793
+ member: "a member's contact",
794
+ person: "someone else",
795
+ ours: "ours, not a person"
796
+ };
797
+ function labelProblem(label) {
798
+ if (!label) return "a label is needed";
799
+ if (label.length > 60) return "a label is at most 60 characters";
800
+ if (findPersonValues(label).length) return "a label is a role, never a phone number, an email address or a path";
801
+ return void 0;
802
+ }
803
+ async function askLabel(ask, out, question, optional) {
804
+ for (; ; ) {
805
+ const label = (await ask(question)).trim();
806
+ if (optional && !label) return void 0;
807
+ const problem = labelProblem(label);
808
+ if (!problem) return label;
809
+ out(` ${problem}.`);
810
+ }
811
+ }
812
+ async function people(opts, ws, files, report, hint) {
813
+ const { client, out, ask, dryRun } = opts;
814
+ const terminal = !!ask && !dryRun;
815
+ const held = /* @__PURE__ */ new Set();
816
+ const leftOut = /* @__PURE__ */ new Set();
817
+ const current = { ...files };
818
+ for (const [path, text] of Object.entries(current)) {
819
+ const secret = findSecrets(text)[0];
820
+ if (secret) {
821
+ held.add(path);
822
+ report.skip(path, `a secret (${secret.kind}) on line ${secret.line}: remove it, then sync again`);
823
+ }
824
+ }
825
+ const { groups, findings } = collectValues(current);
826
+ const answers = /* @__PURE__ */ new Map();
827
+ for (let i = 0; i < groups.length; i += 500) {
828
+ const batch = groups.slice(i, i + 500);
829
+ const got = await client.lookupValues(ws, batch.map((g) => ({ kind: g.kind, value: g.value })));
830
+ batch.forEach((g, j) => {
831
+ const a = got.answers[j];
832
+ if (a && a.answer !== "unanswered") answers.set(groupKey(g.kind, g.normalised), a);
833
+ });
834
+ }
835
+ const vaultDown = /* @__PURE__ */ new Set();
836
+ if (terminal) {
837
+ const filed = [];
838
+ for (const g of groups) {
839
+ const key = groupKey(g.kind, g.normalised);
840
+ if (answers.has(key)) continue;
841
+ let choices = ["m", "p", "o", "x", "s"];
842
+ for (; ; ) {
843
+ const question = [
844
+ `${g.value} (${g.kind}) is in ${g.files.length} file${g.files.length === 1 ? "" : "s"}. First line it appears on, in ${g.first.path}:`,
845
+ ` ${g.first.text}`,
846
+ [
847
+ choices.includes("m") && "[m] a member's contact",
848
+ "[p] someone else",
849
+ "[o] ours, not a person",
850
+ "[x] leave these files out",
851
+ "[s] skip for now"
852
+ ].filter(Boolean).join(" "),
853
+ `Answer (${choices.join("/")}): `
854
+ ].join("\n");
855
+ const answer = (await ask(question)).trim().toLowerCase();
856
+ if (!choices.includes(answer)) continue;
857
+ if (answer === "s") break;
858
+ if (answer === "x") {
859
+ for (const path of g.files) {
860
+ if (leftOut.has(path)) continue;
861
+ current[path] = addShareFalse(current[path]);
862
+ await writeFile2(toPath(opts.dir, path), current[path], "utf8");
863
+ leftOut.add(path);
864
+ }
865
+ report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
866
+ break;
867
+ }
868
+ if (answer === "p") {
869
+ const label2 = await askLabel(ask, out, "Their role, as a label (e.g. reseller, sales; never a name): ", false);
870
+ let entry = filed.length ? await (async () => {
871
+ const list = filed.map((f, i) => ` ${i + 1}. ${f.label}: ${f.groups.map((x) => x.value).join(", ")}`);
872
+ for (; ; ) {
873
+ const same = (await ask(["The same person as one filed above?", ...list, "Number, or Enter for a new person: "].join("\n"))).trim();
874
+ if (!same) return void 0;
875
+ const n = Number(same);
876
+ if (Number.isInteger(n) && n >= 1 && n <= filed.length && filed[n - 1].groups.length < 4) return filed[n - 1];
877
+ }
878
+ })() : void 0;
879
+ if (!entry) {
880
+ entry = { groups: [], label: label2 };
881
+ filed.push(entry);
882
+ }
883
+ entry.groups.push(g);
884
+ break;
885
+ }
886
+ const label = answer === "m" ? await askLabel(ask, out, "A role label for it (e.g. owner, business mobile), or Enter for none: ", true) : void 0;
887
+ let result;
888
+ try {
889
+ result = await client.answerValues(ws, [
890
+ { kind: g.kind, value: g.value, answer: answer === "m" ? "member" : "ours", ...label ? { label } : {} }
891
+ ]);
892
+ } catch (e) {
893
+ if (refusal(e).code === "SYD-K019") {
894
+ vaultDown.add(key);
895
+ out(" The vault did not answer (SYD-K019): the files holding it wait for the next sync.");
896
+ break;
897
+ }
898
+ throw e;
899
+ }
900
+ if (result.stored[0]) {
901
+ answers.set(key, result.stored[0]);
902
+ report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
903
+ break;
904
+ }
905
+ const refused = result.refused[0];
906
+ if (refused?.code === "SYD-K021") {
907
+ out(` ${refused.detail}. Add them in Staff first, or answer p to file them.`);
908
+ choices = choices.filter((c) => c !== "m");
909
+ } else if (refused) out(` ${refused.detail}`);
910
+ }
911
+ }
912
+ for (let i = 0; i < filed.length; i += 50) {
913
+ const batch = filed.slice(i, i + 50);
914
+ let result;
915
+ try {
916
+ result = await client.fileMemoryPeople(
917
+ ws,
918
+ batch.map((f) => ({ values: f.groups.map((g) => ({ kind: g.kind, value: g.value })), label: f.label }))
919
+ );
920
+ } catch (e) {
921
+ if (refusal(e).code === "SYD-K019") {
922
+ out("Contacts did not answer (SYD-K019): the files holding those values wait for the next sync.");
923
+ batch.forEach((f) => f.groups.forEach((g) => vaultDown.add(groupKey(g.kind, g.normalised))));
924
+ continue;
925
+ }
926
+ throw e;
927
+ }
928
+ for (const stored of result.stored) {
929
+ let key;
930
+ try {
931
+ key = groupKey(stored.kind, normalise(stored.kind, stored.value));
932
+ } catch {
933
+ continue;
934
+ }
935
+ answers.set(key, stored);
936
+ }
937
+ for (const r of result.refused) {
938
+ out(` ${batch[r.index]?.label ?? ""}: ${r.detail}`);
939
+ }
940
+ for (const f of batch) {
941
+ const a = answers.get(groupKey(f.groups[0].kind, f.groups[0].normalised));
942
+ if (a?.answer === "person") {
943
+ for (const g of f.groups) report.people.push({ kind: g.kind, answer: ANSWER_WORDS.person, label: a.label, personId: a.personId });
944
+ }
945
+ }
946
+ }
947
+ }
948
+ const rewritten = {};
949
+ for (const [path, text] of Object.entries(current)) {
950
+ if (leftOut.has(path) || held.has(path)) continue;
951
+ const next = rewriteValues(text, findings[path] ?? [], (f) => {
952
+ if (f.kind === HOME_PATH) return "~";
953
+ const a = answers.get(groupKey(f.kind, f.normalised));
954
+ return a && (a.answer === "member" || a.answer === "person") && a.personId ? personToken(a.personId, a.label) : void 0;
955
+ });
956
+ if (next !== text) rewritten[path] = next;
957
+ }
958
+ const paths = Object.keys(rewritten).sort();
959
+ if (paths.length && terminal) {
960
+ out("", "The rewrite:");
961
+ for (const path of paths) out(path, ...lineDiff(current[path], rewritten[path]).map((l) => ` ${l}`));
962
+ const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
963
+ if (yes) {
964
+ for (const path of paths) {
965
+ await writeFile2(toPath(opts.dir, path), rewritten[path], "utf8");
966
+ current[path] = rewritten[path];
967
+ }
968
+ }
969
+ }
970
+ for (const [path, text] of Object.entries(current)) {
971
+ if (leftOut.has(path) || held.has(path)) continue;
972
+ const pending = findPersonValues(text).filter((f) => {
973
+ if (f.kind === HOME_PATH) return true;
974
+ return answers.get(groupKey(f.kind, f.normalised))?.answer !== "ours";
975
+ });
976
+ if (!pending.length) continue;
977
+ held.add(path);
978
+ const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
979
+ if (keys.some((k) => vaultDown.has(k))) {
980
+ report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
981
+ } else if (keys.some((k) => !answers.has(k))) {
982
+ report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
983
+ } else {
984
+ report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
985
+ }
986
+ }
987
+ for (const g of groups) {
988
+ const key = groupKey(g.kind, g.normalised);
989
+ if (!answers.has(key) && g.files.some((p) => !leftOut.has(p))) {
990
+ report.toAnswer.set(g.kind, (report.toAnswer.get(g.kind) ?? 0) + 1);
991
+ }
992
+ }
993
+ return { files: current, held, leftOut };
994
+ }
995
+ async function applyPlan(dir, plan) {
996
+ for (const [path, text] of Object.entries(plan.writes)) {
997
+ const full = toPath(dir, path);
998
+ await mkdir2(dirname2(full), { recursive: true });
999
+ await writeFile2(full, text, "utf8");
1000
+ }
1001
+ for (const path of plan.deletes) await rm2(toPath(dir, path), { force: true });
1002
+ }
1003
+ var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
1004
+ async function upload(client, ws, project, plan, team, report) {
1005
+ const byHash = /* @__PURE__ */ new Map();
1006
+ for (const path of Object.keys(plan.upload)) {
1007
+ const h = plan.manifest[path];
1008
+ byHash.set(h, [...byHash.get(h) ?? [], path]);
1009
+ }
1010
+ const hashes = [...byHash.keys()];
1011
+ const missing = [];
1012
+ for (let i = 0; i < hashes.length; i += 1e4) {
1013
+ missing.push(...(await client.findMissingBlobs(ws, project, hashes.slice(i, i + 1e4))).missing);
1014
+ }
1015
+ const blobs = missing.map((h) => ({ sha256: h, content: plan.upload[byHash.get(h)[0]] }));
1016
+ let batch = [];
1017
+ let bytes = 0;
1018
+ const send = async () => {
1019
+ if (!batch.length) return;
1020
+ const result = await client.uploadBlobs(ws, project, batch);
1021
+ for (const r of result.refused) {
1022
+ for (const path of byHash.get(r.sha256) ?? []) {
1023
+ if (team[path]) plan.manifest[path] = team[path];
1024
+ else delete plan.manifest[path];
1025
+ plan.pushed = plan.pushed.filter((p) => p !== path);
1026
+ report.pushed.delete(path);
1027
+ report.skip(path, `refused by SystemDox, ${r.detail.startsWith(r.code) ? r.detail : `${r.code}: ${r.detail}`}`);
1028
+ }
1029
+ }
1030
+ batch = [];
1031
+ bytes = 0;
1032
+ };
1033
+ for (const blob of blobs) {
1034
+ const size = Buffer.byteLength(blob.content, "utf8");
1035
+ if (batch.length === 100 || batch.length && bytes + size > 5 * 1024 * 1024 - 64 * 1024) await send();
1036
+ batch.push(blob);
1037
+ bytes += size;
1038
+ }
1039
+ await send();
1040
+ }
1041
+ async function syncMemory(opts) {
1042
+ const { client, out } = opts;
1043
+ const now = opts.now?.() ?? /* @__PURE__ */ new Date();
1044
+ try {
1045
+ const own = await client.getOwnTeamSetup();
1046
+ const ws = own.workspace;
1047
+ const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
1048
+ if (!project) {
1049
+ throw new SyncFailed(
1050
+ own.memory.length ? `${ws} has ${own.memory.length} team memories (${own.memory.map((m) => m.project).join(", ")}): name one with --project.` : `${ws} has no team memory yet: name the project to start with --project.`
1051
+ );
1052
+ }
1053
+ const dir = resolve(opts.dir ?? join2(workspaceDir(ws), "memory", project));
1054
+ const run3 = { ...opts, dir };
1055
+ const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
1056
+ const report = new Report();
1057
+ const date = localDate(now);
1058
+ const stored = (await readState(ws)).memory?.[project];
1059
+ let team = await pullTeam(client, ws, project);
1060
+ let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
1061
+ const folder = await readFolder(dir);
1062
+ const shared = {};
1063
+ const notShared = /* @__PURE__ */ new Set();
1064
+ for (const [path, text] of Object.entries(folder.files)) {
1065
+ if (!isShareable(path, text)) notShared.add(path);
1066
+ else if (!isFilePath(path)) {
1067
+ notShared.add(path);
1068
+ report.skip(path, "its name has characters a team memory path cannot hold (letters, digits, '.', '_', '-')");
1069
+ } else shared[path] = text;
1070
+ }
1071
+ const importWrites = {};
1072
+ if (opts.importDir) {
1073
+ const other = await readFolder(resolve(opts.importDir));
1074
+ const taken = new Set(folder.names);
1075
+ for (const [path, text] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
1076
+ if (!isShareable(path, text) || !isFilePath(path)) continue;
1077
+ const here = folder.files[path];
1078
+ if (here === void 0) {
1079
+ shared[path] = text;
1080
+ importWrites[path] = text;
1081
+ taken.add(path.toLowerCase());
1082
+ } else if (here !== text) {
1083
+ const copy = conflictCopyName(path, date, taken);
1084
+ taken.add(copy.toLowerCase());
1085
+ importWrites[copy] = text;
1086
+ report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
1087
+ }
1088
+ }
1089
+ folder.names = taken;
1090
+ if (!opts.dryRun) {
1091
+ for (const [path, text] of Object.entries(importWrites)) {
1092
+ const full = toPath(dir, path);
1093
+ await mkdir2(dirname2(full), { recursive: true });
1094
+ await writeFile2(full, text, "utf8");
1095
+ }
1096
+ }
1097
+ }
1098
+ const found = await people(run3, ws, shared, report, hint);
1099
+ for (const path of found.leftOut) {
1100
+ delete found.files[path];
1101
+ notShared.add(path);
1102
+ }
1103
+ const excluded = /* @__PURE__ */ new Set([...found.held, ...notShared]);
1104
+ for (const path of found.held) delete found.files[path];
1105
+ for (let attempt = 1; ; attempt++) {
1106
+ const input = { base: base.manifest, team: team.manifest, local: found.files, excluded, taken: folder.names, date };
1107
+ const { rows, needed } = decideAll(input);
1108
+ const texts = needed.length ? await readTexts(client, ws, project, needed) : {};
1109
+ const plan = planMerge(input, rows, texts);
1110
+ report.add(plan);
1111
+ if (opts.dryRun) {
1112
+ const skipped = report.skipped.size;
1113
+ out(
1114
+ ...report.lines(
1115
+ `Dry run of ${ws}/${project} against version ${team.version}: would pull ${report.pulled.size}, push ${report.pushed.size} and delete ${report.deletedHere.size + report.deletedFromTeam.size}; ${report.conflicts.length} conflicts, ${skipped} skipped. Nothing was asked, written or sent.`,
1116
+ true
1117
+ )
1118
+ );
1119
+ return 0;
1120
+ }
1121
+ await applyPlan(dir, plan);
1122
+ let version = team.version;
1123
+ if (!sameManifest(plan.manifest, team.manifest)) {
1124
+ await upload(client, ws, project, plan, team.manifest, report);
1125
+ }
1126
+ if (!sameManifest(plan.manifest, team.manifest)) {
1127
+ try {
1128
+ version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
1129
+ } catch (e) {
1130
+ if (!(refusal(e).status === 409 && refusal(e).code === "SYD-K010")) throw e;
1131
+ if (attempt === 2) {
1132
+ out(
1133
+ ...report.lines(
1134
+ `Not pushed: ${ws}/${project} moved on twice while this sync merged. Your folder holds the merge; run sync-memory again.`,
1135
+ false
1136
+ )
1137
+ );
1138
+ return 2;
1139
+ }
1140
+ base = team;
1141
+ for (const p of plan.pushed) report.pushed.delete(p);
1142
+ const again = await readFolder(dir);
1143
+ folder.names = again.names;
1144
+ found.files = {};
1145
+ for (const [path, text] of Object.entries(again.files)) {
1146
+ if (excluded.has(path) || !isShareable(path, text) || !isFilePath(path)) continue;
1147
+ found.files[path] = text;
1148
+ }
1149
+ team = await pullTeam(client, ws, project);
1150
+ continue;
1151
+ }
1152
+ }
1153
+ await updateState(ws, (state) => {
1154
+ state.memory = { ...state.memory, [project]: { version, manifest: plan.manifest } };
1155
+ });
1156
+ out(
1157
+ ...report.lines(
1158
+ `Synced ${ws}/${project} at version ${version}: ${report.pulled.size} pulled, ${report.pushed.size} pushed, ${report.deletedHere.size + report.deletedFromTeam.size} deleted, ${report.conflicts.length} conflicts, ${report.skipped.size} skipped.`,
1159
+ false
1160
+ )
1161
+ );
1162
+ return 0;
1163
+ }
1164
+ } catch (e) {
1165
+ out(e.message);
1166
+ return 1;
1167
+ }
1168
+ }
1169
+
1170
+ // src/setup-team.ts
1171
+ var SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]";
1172
+ var HEADERS_HELPER = "npx -y @systemdox/mcp-server marketplace-auth";
1173
+ var SetupFailed = class extends Error {
1174
+ };
1175
+ function parseSetupArgs(argv) {
1176
+ const args = { yes: false, overwrite: false };
1177
+ for (let i = 0; i < argv.length; i++) {
1178
+ const a = argv[i];
1179
+ if (a === "--yes") args.yes = true;
1180
+ else if (a === "--overwrite") args.overwrite = true;
1181
+ else if (a === "--project" || a === "--dir") {
1182
+ const value = argv[++i];
1183
+ if (!value || value.startsWith("--")) throw new Error(`${a} needs a value
1184
+ ${SETUP_USAGE}`);
1185
+ if (a === "--project") {
1186
+ if (!isWorkspace(value)) throw new Error(`${value} is not a project name (lower case letters, digits and -)`);
1187
+ args.project = value;
1188
+ } else args.dir = value;
1189
+ } else throw new Error(`Unexpected argument ${a}
1190
+ ${SETUP_USAGE}`);
1191
+ }
1192
+ return args;
1193
+ }
1194
+ var sha2562 = (text) => createHash2("sha256").update(text, "utf8").digest("hex");
1195
+ var run2 = promisify2(execFile2);
1196
+ async function claudeCodeVersion() {
1197
+ try {
1198
+ const { stdout } = await run2("claude", ["--version"], {
1199
+ shell: process.platform === "win32",
1200
+ timeout: 3e4,
1201
+ windowsHide: true
1202
+ });
1203
+ return stdout;
1204
+ } catch {
1205
+ return void 0;
1206
+ }
1207
+ }
1208
+ function parseVersion(text) {
1209
+ const m = text?.match(/(\d+)\.(\d+)\.(\d+)/);
1210
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : void 0;
1211
+ }
1212
+ function olderThan(have, need) {
1213
+ for (let i = 0; i < 3; i++) if (have[i] !== need[i]) return have[i] < need[i];
1214
+ return false;
1215
+ }
1216
+ function skipSpace(t, i) {
1217
+ while (i < t.length && /\s/.test(t[i])) i++;
1218
+ return i;
1219
+ }
1220
+ function skipString(t, i) {
1221
+ for (i++; i < t.length; i++) {
1222
+ if (t[i] === "\\") i++;
1223
+ else if (t[i] === '"') return i + 1;
1224
+ }
1225
+ return i;
1226
+ }
1227
+ function skipValue(t, i) {
1228
+ if (t[i] === '"') return skipString(t, i);
1229
+ if (t[i] === "{" || t[i] === "[") {
1230
+ let depth = 0;
1231
+ while (i < t.length) {
1232
+ const c = t[i];
1233
+ if (c === '"') {
1234
+ i = skipString(t, i);
1235
+ continue;
1236
+ }
1237
+ if (c === "{" || c === "[") depth++;
1238
+ else if (c === "}" || c === "]") {
1239
+ depth--;
1240
+ if (depth === 0) return i + 1;
1241
+ }
1242
+ i++;
1243
+ }
1244
+ return i;
1245
+ }
1246
+ while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
1247
+ return i;
1248
+ }
1249
+ function objectMembers(t, open) {
1250
+ const members = [];
1251
+ let i = skipSpace(t, open + 1);
1252
+ if (t[i] === "}") return { members, close: i };
1253
+ for (; ; ) {
1254
+ i = skipSpace(t, i);
1255
+ const keyStart = i;
1256
+ const keyEnd = skipString(t, i);
1257
+ const key = JSON.parse(t.slice(keyStart, keyEnd));
1258
+ i = skipSpace(t, skipSpace(t, keyEnd) + 1);
1259
+ const valueEnd = skipValue(t, i);
1260
+ members.push({ key, keyStart, valueStart: i, valueEnd });
1261
+ i = skipSpace(t, valueEnd);
1262
+ if (t[i] === ",") i++;
1263
+ else return { members, close: i };
1264
+ }
1265
+ }
1266
+ function lineIndent(t, pos) {
1267
+ const start = t.lastIndexOf("\n", pos - 1) + 1;
1268
+ return /^[ \t]*/.exec(t.slice(start))[0];
1269
+ }
1270
+ function setJsonMember(text, keys, value) {
1271
+ const root = JSON.parse(text);
1272
+ if (!root || typeof root !== "object" || Array.isArray(root)) throw new Error("it does not hold a JSON object");
1273
+ const compact = !text.trimEnd().includes("\n");
1274
+ const unit = /^([ \t]+)"/m.exec(text)?.[1] ?? " ";
1275
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
1276
+ const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
1277
+ const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
1278
+ let open = skipSpace(text, 0);
1279
+ for (let k = 0; k < keys.length; k++) {
1280
+ const { members, close } = objectMembers(text, open);
1281
+ const found = members.filter((m) => m.key === keys[k]).at(-1);
1282
+ if (found) {
1283
+ if (k < keys.length - 1 && text[found.valueStart] === "{") {
1284
+ open = found.valueStart;
1285
+ continue;
1286
+ }
1287
+ return text.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text, found.keyStart)) + text.slice(found.valueEnd);
1288
+ }
1289
+ const name = JSON.stringify(keys[k]);
1290
+ if (compact) {
1291
+ const member = `${name}:${render(nest(k + 1), "")}`;
1292
+ if (members.length) return text.slice(0, members.at(-1).valueEnd) + "," + member + text.slice(members.at(-1).valueEnd);
1293
+ return text.slice(0, open + 1) + member + text.slice(close);
1294
+ }
1295
+ if (members.length) {
1296
+ const last = members.at(-1);
1297
+ const indent2 = lineIndent(text, last.keyStart);
1298
+ return text.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text.slice(last.valueEnd);
1299
+ }
1300
+ const outer = lineIndent(text, open);
1301
+ const indent = outer + unit;
1302
+ return text.slice(0, open + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text.slice(close);
1303
+ }
1304
+ return text;
1305
+ }
1306
+ function jsonAt(value, keys) {
1307
+ let v = value;
1308
+ for (const k of keys) v = v && typeof v === "object" ? v[k] : void 0;
1309
+ return v;
1310
+ }
1311
+ async function setInJsonFile(path, keys, value) {
1312
+ let text;
1313
+ try {
1314
+ text = await readFile3(path, "utf8");
1315
+ } catch (e) {
1316
+ if (e.code !== "ENOENT") throw e;
1317
+ }
1318
+ let next;
1319
+ if (text === void 0 || !text.trim()) {
1320
+ next = JSON.stringify(keys.reduceRight((acc, k) => ({ [k]: acc }), value), null, 2) + "\n";
1321
+ } else {
1322
+ let current;
1323
+ try {
1324
+ current = JSON.parse(text);
1325
+ } catch {
1326
+ throw new SetupFailed(`${path} is not valid JSON: fix it, then run setup again.`);
1327
+ }
1328
+ if (JSON.stringify(jsonAt(current, keys)) === JSON.stringify(value)) return;
1329
+ try {
1330
+ next = setJsonMember(text, keys, value);
1331
+ } catch (e) {
1332
+ throw new SetupFailed(`${path}: ${e.message}; fix it, then run setup again.`);
1333
+ }
1334
+ }
1335
+ await mkdir3(dirname3(path), { recursive: true });
1336
+ await writeFile3(path, next, "utf8");
1337
+ }
1338
+ function importBlock(workspace, eol = "\n") {
1339
+ return [
1340
+ `<!-- systemdox:${workspace} -->`,
1341
+ `@~/.claude/systemdox/${workspace}/CLAUDE.md`,
1342
+ `<!-- /systemdox:${workspace} -->`
1343
+ ].join(eol);
1344
+ }
1345
+ function withImportBlock(text, workspace) {
1346
+ const eol = text.includes("\r\n") ? "\r\n" : "\n";
1347
+ const block = importBlock(workspace, eol);
1348
+ const open = `<!-- systemdox:${workspace} -->`;
1349
+ const close = `<!-- /systemdox:${workspace} -->`;
1350
+ const start = text.indexOf(open);
1351
+ const end = start >= 0 ? text.indexOf(close, start) : -1;
1352
+ if (start >= 0 && end >= 0) return text.slice(0, start) + block + text.slice(end + close.length);
1353
+ if (!text) return block + eol;
1354
+ return text + (text.endsWith("\n") ? "" : eol) + block + eol;
1355
+ }
1356
+ async function readJsonQuietly(path) {
1357
+ try {
1358
+ return JSON.parse(await readFile3(path, "utf8"));
1359
+ } catch {
1360
+ return void 0;
1361
+ }
1362
+ }
1363
+ var item = (name, lines, diff) => ({
1364
+ name,
1365
+ lines: diff ?? lines,
1366
+ sha256: sha2562(`${name}
1367
+ ${lines.join("\n")}`)
1368
+ });
1369
+ var hookLines = (p) => p.hookCommands ?? [];
1370
+ var mcpLines = (p) => (p.mcpServers ?? []).map((s) => `${s.name}: ${s.url ?? [s.command, ...s.args ?? []].filter(Boolean).join(" ")}`);
1371
+ async function chooseProject(own, opts, canAsk) {
1372
+ if (opts.project) return opts.project;
1373
+ if (own.memory.length <= 1) return own.memory[0]?.project;
1374
+ const names = own.memory.map((m) => m.project);
1375
+ const usage = `${own.workspace} has ${names.length} team memories (${names.join(", ")}): name this checkout's with --project.`;
1376
+ if (!canAsk) throw new SetupFailed(usage);
1377
+ const answer = (await opts.ask(`Which team memory is this checkout's? (${names.join(", ")}) `)).trim();
1378
+ if (!names.includes(answer)) throw new SetupFailed(usage);
1379
+ return answer;
1380
+ }
1381
+ async function runTeamSetup(opts) {
1382
+ const { client, out } = opts;
1383
+ try {
1384
+ const own = await client.getOwnTeamSetup();
1385
+ const ws = own.workspace;
1386
+ if (!isWorkspace(ws)) throw new SetupFailed(`SystemDox named the workspace ${JSON.stringify(ws)}, which is not a workspace name.`);
1387
+ const instructionsVersion = own.parts.instructions;
1388
+ if (!own.plugin && instructionsVersion === void 0 && !own.memory.length && !opts.project) return 0;
1389
+ const need = parseVersion(own.minClaudeCodeVersion);
1390
+ const have = parseVersion(await (opts.claudeVersion ?? claudeCodeVersion)());
1391
+ if (need && (!have || olderThan(have, need))) {
1392
+ out(
1393
+ have ? `Claude Code ${own.minClaudeCodeVersion} or later is needed for the team setup; this computer has ${have.join(".")}.` : `Claude Code ${own.minClaudeCodeVersion} or later is needed for the team setup; it was not found on this computer.`
1394
+ );
1395
+ out("Update it with: claude update");
1396
+ out("Then run setup again. Nothing was changed.");
1397
+ return 3;
1398
+ }
1399
+ const home = homeDir();
1400
+ const userSettings = join3(home, ".claude", "settings.json");
1401
+ const userClaudeMd = join3(home, ".claude", "CLAUDE.md");
1402
+ const teamClaudeMd = join3(workspaceDir(ws), "CLAUDE.md");
1403
+ const checkout = resolve2(opts.dir ?? opts.cwd ?? process.cwd());
1404
+ const localSettings = join3(checkout, ".claude", "settings.local.json");
1405
+ const state = await readState(ws);
1406
+ const accepted = new Set(state.accepted ?? []);
1407
+ const update = accepted.size > 0;
1408
+ const canAsk = !!opts.ask && !(opts.yes && !opts.terminal);
1409
+ const project = await chooseProject(own, opts, canAsk);
1410
+ const marketplaceName = `systemdox-${ws}`;
1411
+ const marketplace = { source: { source: "url", url: own.marketplaceUrl, headersHelper: HEADERS_HELPER } };
1412
+ const items = [];
1413
+ items.push(
1414
+ item(`the marketplace ${marketplaceName} in ${userSettings} (extraKnownMarketplaces)`, [
1415
+ `"${marketplaceName}": ${JSON.stringify(marketplace)}`
1416
+ ])
1417
+ );
1418
+ const plugin = own.plugin;
1419
+ let previousPlugin;
1420
+ if (plugin) {
1421
+ const hooks = hookLines(plugin);
1422
+ const hooksName = `the hook commands of ${ws}-setup`;
1423
+ const hooksItem = item(hooksName, hooks);
1424
+ if (update && hooks.length && !accepted.has(hooksItem.sha256)) {
1425
+ const versions = (await client.getPlugin(ws)).versions.filter((v) => v.version !== plugin.version);
1426
+ previousPlugin = versions.sort((a, b) => b.version - a.version).find((v) => accepted.has(item(hooksName, hookLines(v)).sha256));
1427
+ }
1428
+ items.push(item(`the plugin ${ws}-setup, version ${plugin.version}`, [`${ws}-setup, version ${plugin.version}`]));
1429
+ if (hooks.length) {
1430
+ items.push(
1431
+ item(
1432
+ hooksName,
1433
+ hooks,
1434
+ previousPlugin ? lineDiff(hookLines(previousPlugin).join("\n"), hooks.join("\n")) : void 0
1435
+ )
1436
+ );
1437
+ }
1438
+ const servers = mcpLines(plugin);
1439
+ if (servers.length) items.push(item(`the MCP servers of ${ws}-setup`, servers));
1440
+ }
1441
+ let instructions;
1442
+ if (instructionsVersion !== void 0) {
1443
+ const v = await client.getPartVersion(ws, "instructions", instructionsVersion);
1444
+ const file = v.files.find((f) => f.path === "CLAUDE.md") ?? v.files[0];
1445
+ instructions = { version: v.version, content: file?.content ?? "" };
1446
+ const name = `the team instructions, written to ${teamClaudeMd}`;
1447
+ const shown = item(name, instructions.content.split(/\r?\n/));
1448
+ let diff;
1449
+ const before = state.instructions;
1450
+ if (update && !accepted.has(shown.sha256) && before && before.version !== instructions.version) {
1451
+ const old = await client.getPartVersion(ws, "instructions", before.version);
1452
+ const oldFile = old.files.find((f) => f.path === "CLAUDE.md") ?? old.files[0];
1453
+ diff = lineDiff(oldFile?.content ?? "", instructions.content);
1454
+ }
1455
+ items.push({
1456
+ ...shown,
1457
+ lines: diff ?? [`version ${instructions.version}, ${instructions.content.split(/\r?\n/).length} lines`]
1458
+ });
1459
+ items.push(item(`the import block in ${userClaudeMd}`, importBlock(ws).split("\n")));
1460
+ }
1461
+ let memoryFolder;
1462
+ if (project) {
1463
+ memoryFolder = join3(workspaceDir(ws), "memory", project);
1464
+ for (const m of own.memory) {
1465
+ const folder = join3(workspaceDir(ws), "memory", m.project);
1466
+ const count = `${m.fileCount} file${m.fileCount === 1 ? "" : "s"}`;
1467
+ items.push({ ...item(`the team memory ${m.project}`, [folder]), lines: [`${m.project}: ${count}, synced into ${folder}`] });
1468
+ }
1469
+ if (!own.memory.some((m) => m.project === project)) {
1470
+ items.push({ ...item(`the team memory ${project}`, [memoryFolder]), lines: [`${project}: new, synced into ${memoryFolder}`] });
1471
+ }
1472
+ items.push(item(`autoMemoryDirectory in ${localSettings}`, [memoryFolder]));
1473
+ }
1474
+ const pending = items.filter((i) => !accepted.has(i.sha256));
1475
+ if (pending.length) {
1476
+ if (!canAsk) {
1477
+ out(`Not set up: ${pending.length === 1 ? "this needs" : "these need"} a person at a terminal (run setup without --yes):`);
1478
+ for (const i of pending) {
1479
+ out(`- ${i.name}:`);
1480
+ for (const l of i.lines) out(` ${l}`);
1481
+ }
1482
+ return 4;
1483
+ }
1484
+ out(update ? `What changed in the team setup of ${ws}:` : `The team setup of ${ws} will add:`);
1485
+ for (const i of pending) {
1486
+ out(`- ${i.name}:`);
1487
+ for (const l of i.lines) out(` ${l}`);
1488
+ }
1489
+ const answer = (await opts.ask("Set this up? [y/N] ")).trim();
1490
+ if (!/^y(es)?$/i.test(answer)) {
1491
+ out("Nothing was changed.");
1492
+ return 4;
1493
+ }
1494
+ }
1495
+ if (opts.keySource !== "credentials" && (await readCredentials())[ws]?.apiKey !== opts.apiKey) {
1496
+ await writeCredentials(ws, opts.apiKey);
1497
+ }
1498
+ if (instructions) {
1499
+ const next = sha2562(instructions.content);
1500
+ let current;
1501
+ try {
1502
+ current = await readFile3(teamClaudeMd, "utf8");
1503
+ } catch (e) {
1504
+ if (e.code !== "ENOENT") throw e;
1505
+ }
1506
+ const currentSha = current === void 0 ? void 0 : sha2562(current);
1507
+ if (currentSha !== void 0 && currentSha !== next && currentSha !== state.instructions?.sha256 && !opts.overwrite) {
1508
+ out(`${teamClaudeMd} was edited since setup installed it, so it was not replaced by version ${instructions.version}.`);
1509
+ out("Keep your text elsewhere, then run setup --overwrite to replace it.");
1510
+ return 2;
1511
+ }
1512
+ if (currentSha !== next) {
1513
+ await mkdir3(dirname3(teamClaudeMd), { recursive: true });
1514
+ await writeFile3(teamClaudeMd, instructions.content, "utf8");
1515
+ }
1516
+ await updateState(ws, (s) => {
1517
+ s.instructions = { version: instructions.version, sha256: next };
1518
+ });
1519
+ let text = "";
1520
+ try {
1521
+ text = await readFile3(userClaudeMd, "utf8");
1522
+ } catch (e) {
1523
+ if (e.code !== "ENOENT") throw e;
1524
+ }
1525
+ const withBlock = withImportBlock(text, ws);
1526
+ if (withBlock !== text) {
1527
+ await mkdir3(dirname3(userClaudeMd), { recursive: true });
1528
+ await writeFile3(userClaudeMd, withBlock, "utf8");
1529
+ }
1530
+ }
1531
+ await setInJsonFile(userSettings, ["extraKnownMarketplaces", marketplaceName], marketplace);
1532
+ await removeOldPlugins(ws);
1533
+ if (project && memoryFolder) {
1534
+ await setInJsonFile(localSettings, ["autoMemoryDirectory"], memoryFolder);
1535
+ }
1536
+ await updateState(ws, (s) => {
1537
+ s.accepted = [...new Set(items.map((i) => i.sha256))];
1538
+ });
1539
+ if (project) {
1540
+ const code = await syncMemory({
1541
+ project,
1542
+ dryRun: false,
1543
+ client,
1544
+ out: (...lines) => lines.forEach((l) => out(l)),
1545
+ ask: opts.terminal ? opts.ask : void 0,
1546
+ now: opts.now
1547
+ });
1548
+ if (code !== 0) return 1;
1549
+ }
1550
+ if (plugin) {
1551
+ const pluginsDir = join3(home, ".claude", "plugins");
1552
+ const id = `${ws}-setup@${marketplaceName}`;
1553
+ const installed = await readJsonQuietly(join3(pluginsDir, "installed_plugins.json"));
1554
+ if (!installed?.plugins?.[id]) {
1555
+ const known = await readJsonQuietly(join3(pluginsDir, "known_marketplaces.json"));
1556
+ const install = `Run \`claude plugin install ${id}\` in your own terminal and accept the command it shows.`;
1557
+ const steps = known && typeof known === "object" && known[marketplaceName] ? ["One step is left:", `1. ${install}`] : ["Two steps are left:", "1. Open Claude Code once in any folder, then close it.", `2. ${install}`];
1558
+ out("");
1559
+ steps.forEach((l) => out(l));
1560
+ }
1561
+ }
1562
+ return 0;
1563
+ } catch (e) {
1564
+ out(e.message);
1565
+ return 1;
1566
+ }
1567
+ }
1568
+ async function removeOldPlugins(workspace) {
1569
+ const root = pluginRoot(workspace);
1570
+ if (!existsSync2(root)) return;
1571
+ const versions = (await readdir2(root, { withFileTypes: true })).filter((e) => e.isDirectory() && /^\d+$/.test(e.name)).map((e) => Number(e.name)).sort((a, b) => b - a);
1572
+ const old = versions.slice(2);
1573
+ if (!old.length) return;
1574
+ for (const v of old) await removeFolder(join3(root, String(v)));
1575
+ await updateState(workspace, (s) => {
1576
+ for (const v of old) delete s.plugin?.[String(v)];
1577
+ });
1578
+ }
1579
+ async function runTeamSetupCommand(args, key, io) {
1580
+ const client = new SystemDoxApiClient({ apiKey: key.apiKey, apiUrl: key.apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
1581
+ return runTeamSetup({ ...args, client, apiKey: key.apiKey, keySource: "prompt", ...io });
1582
+ }
1583
+ export {
1584
+ HEADERS_HELPER,
1585
+ SETUP_USAGE,
1586
+ claudeCodeVersion,
1587
+ importBlock,
1588
+ parseSetupArgs,
1589
+ runTeamSetup,
1590
+ runTeamSetupCommand,
1591
+ setJsonMember,
1592
+ withImportBlock
1593
+ };