@systemdox/mcp-server 0.1.46 → 0.1.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.js CHANGED
@@ -197,8 +197,8 @@ var SystemDoxApiClient = class {
197
197
  answerValues(workspace, answers) {
198
198
  return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
199
199
  }
200
- fileMemoryPeople(workspace, people) {
201
- return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people });
200
+ fileMemoryPeople(workspace, people2) {
201
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people: people2 });
202
202
  }
203
203
  resolvePerson(workspace, personId) {
204
204
  return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
@@ -686,8 +686,8 @@ function registerGetTechStack(server, apiClient) {
686
686
  list.push(entry);
687
687
  grouped.set(entry.category, list);
688
688
  }
689
- const sections = Array.from(grouped.entries()).map(([category, entries]) => {
690
- const items = entries.map(
689
+ const sections = Array.from(grouped.entries()).map(([category, entries2]) => {
690
+ const items = entries2.map(
691
691
  (e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
692
692
  ).join("\n");
693
693
  return `### ${category}
@@ -1185,8 +1185,8 @@ function formatDocHeadline(d) {
1185
1185
  return `### ${d.title}
1186
1186
  \`doc_id: ${d.id}\` \xB7 \`${d.repo_full_name}/${d.path}\` \xB7 ${d.doc_type}`;
1187
1187
  }
1188
- function formatStack(entries) {
1189
- return entries.map(
1188
+ function formatStack(entries2) {
1189
+ return entries2.map(
1190
1190
  (e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
1191
1191
  ).join("\n");
1192
1192
  }
@@ -2299,31 +2299,918 @@ ${body}`
2299
2299
 
2300
2300
  // src/tools/team-setup.ts
2301
2301
  import { z as z14 } from "zod";
2302
- import { existsSync as existsSync2 } from "fs";
2302
+ import { existsSync as existsSync3 } from "fs";
2303
2303
  import { lstat as lstat2 } from "fs/promises";
2304
- import { homedir as homedir3 } from "os";
2305
- import { join as join2, resolve as resolve3 } from "path";
2304
+ import { homedir as homedir4 } from "os";
2305
+ import { join as join4, resolve as resolve4 } from "path";
2306
2306
 
2307
2307
  // src/lib/person-values.ts
2308
+ var PHONE = "phone";
2309
+ var EMAIL = "email";
2310
+ var HOME_PATH = "home-path";
2311
+ var NATIONAL_PREFIX = "+44";
2308
2312
  var W = "[\\p{L}\\p{N}_]";
2309
2313
  var D = "\\p{Nd}";
2310
2314
  var WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
2311
- var PERSON_ID = "(?:con|ctc|usr)_[0-9A-Za-z]{10,40}";
2315
+ var PERSON_ID = "(?:(?:con|ctc)_[0-9A-Za-z]{10,40}|usr_[0-9A-Za-z-]{10,40})";
2312
2316
  var PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
2313
2317
  var BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
2314
2318
  var AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
2315
2319
  var E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
2316
2320
  var NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
2321
+ var GROUP_SIZES = /* @__PURE__ */ new Set([2, 3, 4, 5, 6]);
2317
2322
  var ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
2318
2323
  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])";
2319
2324
  var HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
2320
2325
  var HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
2321
2326
  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,}`;
2327
+ var global = (source, flags = "") => new RegExp(source, `gu${flags}`);
2328
+ var whole = (source, flags = "") => new RegExp(`^(?:${source})$`, `u${flags}`);
2329
+ function lineOf(text3, start) {
2330
+ let line = 1;
2331
+ for (let i = text3.indexOf("\n"); i !== -1 && i < start; i = text3.indexOf("\n", i + 1)) line++;
2332
+ return line;
2333
+ }
2334
+ function phoneShape(value) {
2335
+ const groups = value.replace(/^\++/, "").split(/[ -]/);
2336
+ const digits = [...groups.join("")];
2337
+ if (value.startsWith("+")) return digits.length >= 8 && digits.length <= 15 && digits[0] !== "0";
2338
+ if (digits.length < 10 || digits.length > 11) return false;
2339
+ return groups.length === 1 || groups.slice(1).every((g) => GROUP_SIZES.has([...g].length));
2340
+ }
2341
+ function normalise(kind, value) {
2342
+ const v = typeof value === "string" ? value.trim() : "";
2343
+ if (kind === PHONE) {
2344
+ if (!(whole(E164).test(v) || whole(NATIONAL).test(v)) || !phoneShape(v)) {
2345
+ throw new Error("not a phone number");
2346
+ }
2347
+ const digits = v.replace(/[ -]/g, "");
2348
+ return digits.startsWith("+") ? digits : NATIONAL_PREFIX + digits.slice(1);
2349
+ }
2350
+ if (kind === EMAIL) {
2351
+ if (!whole(EMAIL_RE).test(v)) throw new Error("not an email address");
2352
+ return v.toLowerCase();
2353
+ }
2354
+ throw new Error(`not a value kind: ${JSON.stringify(kind)}`);
2355
+ }
2356
+ function findPersonValues(text3) {
2357
+ if (typeof text3 !== "string" || !text3) return [];
2358
+ const skipped = [...text3.matchAll(global(PERSON_TOKEN))].map((m) => [m.index, m.index + m[0].length]);
2359
+ const found = [];
2360
+ const add = (kind, m, normalised) => {
2361
+ const start = m.index;
2362
+ const end = start + m[0].length;
2363
+ if (skipped.some(([s, e]) => s <= start && end <= e)) return;
2364
+ if (found.some((f) => f.start < end && start < f.end)) return;
2365
+ found.push({ kind, value: m[0], normalised, line: lineOf(text3, start), start, end });
2366
+ };
2367
+ for (const m of text3.matchAll(global(EMAIL_RE))) add(EMAIL, m, m[0].toLowerCase());
2368
+ for (const source of [E164, NATIONAL]) {
2369
+ for (const m of text3.matchAll(global(source))) {
2370
+ if (phoneShape(m[0])) add(PHONE, m, normalise(PHONE, m[0]));
2371
+ }
2372
+ }
2373
+ for (const m of text3.matchAll(global(HOME, "i"))) add(HOME_PATH, m, m[0]);
2374
+ return found.sort((a, b) => a.start - b.start);
2375
+ }
2376
+ function findSecrets(text3) {
2377
+ if (typeof text3 !== "string" || !text3) return [];
2378
+ return [...text3.matchAll(global(KEY_PREFIX))].map((m) => ({ kind: "key-prefix", line: lineOf(text3, m.index) }));
2379
+ }
2322
2380
 
2323
2381
  // src/lib/team-setup-types.ts
2324
2382
  var PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
2325
2383
  var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2326
2384
 
2385
+ // src/sync-memory.ts
2386
+ import { existsSync as existsSync2 } from "fs";
2387
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
2388
+ import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
2389
+
2390
+ // src/lib/memory-sync.ts
2391
+ import { createHash as createHash2 } from "crypto";
2392
+ var INDEX_FILE = "MEMORY.md";
2393
+ var SHAREABLE_TYPES = ["feedback", "project", "reference"];
2394
+ var FILE_PATH_RE = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
2395
+ var CONFLICT_COPY_RE = /\.local-\d{4}-\d{2}-\d{2}(?:-\d+)?\.md$/;
2396
+ function sha2562(text3) {
2397
+ return createHash2("sha256").update(text3, "utf8").digest("hex");
2398
+ }
2399
+ function isFilePath(path) {
2400
+ return path.length <= 512 && FILE_PATH_RE.test(path) && !path.split("/").some((s) => s === "." || s === "..");
2401
+ }
2402
+ function isConflictCopy(path) {
2403
+ return CONFLICT_COPY_RE.test(path.split("/").pop() ?? path);
2404
+ }
2405
+ var BLOCK_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
2406
+ var YAML_FALSE = /* @__PURE__ */ new Set(["false", "False", "FALSE", "no", "No", "NO", "off", "Off", "OFF"]);
2407
+ function readFrontmatter(text3) {
2408
+ const m = BLOCK_RE.exec(text3);
2409
+ if (!m) return void 0;
2410
+ const front = { shareFalse: false };
2411
+ for (const line of m[1].split(/\r?\n/)) {
2412
+ const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
2413
+ if (!kv) continue;
2414
+ const raw = kv[2].replace(/[ \t]+#.*$/, "");
2415
+ const quoted = /^(["'])(.*)\1$/.exec(raw);
2416
+ const value = quoted ? quoted[2] : raw;
2417
+ if (kv[1] === "type") front.type = value.trim().toLowerCase();
2418
+ if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
2419
+ }
2420
+ return front;
2421
+ }
2422
+ function isShareable(path, text3) {
2423
+ if (isConflictCopy(path) || !path.endsWith(".md")) return false;
2424
+ const front = readFrontmatter(text3);
2425
+ if (front?.shareFalse) return false;
2426
+ if (path === INDEX_FILE) return true;
2427
+ return !!front && SHAREABLE_TYPES.includes(front.type ?? "");
2428
+ }
2429
+ function addShareFalse(text3) {
2430
+ const eol = text3.includes("\r\n") ? "\r\n" : "\n";
2431
+ const m = BLOCK_RE.exec(text3);
2432
+ if (!m) return `---${eol}share: false${eol}---${eol}${text3}`;
2433
+ const lines = m[1].split(/\r?\n/).filter((l) => !/^share[ \t]*:/.test(l));
2434
+ lines.push("share: false");
2435
+ const closing = m[0].slice(m[0].lastIndexOf("---"));
2436
+ return `---${eol}${lines.join(eol)}${eol}${closing}${text3.slice(m[0].length)}`;
2437
+ }
2438
+ function localDate(now) {
2439
+ const p = (n) => String(n).padStart(2, "0");
2440
+ return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
2441
+ }
2442
+ function conflictCopyName(path, date, taken) {
2443
+ const stem = path.replace(/\.md$/, "");
2444
+ for (let n = 1; ; n++) {
2445
+ const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
2446
+ if (!taken.has(name.toLowerCase())) return name;
2447
+ }
2448
+ }
2449
+ function decide(path, base, team, local) {
2450
+ if (team === local) return "unchanged";
2451
+ if (team === base) return local === void 0 ? "delete-team" : "local";
2452
+ if (local === base) return team === void 0 ? "delete-local" : "team";
2453
+ if (path === INDEX_FILE && team !== void 0 && local !== void 0) return "index";
2454
+ if (team === void 0) return "local";
2455
+ if (local === void 0) return "team";
2456
+ return "conflict";
2457
+ }
2458
+ function decideAll(input) {
2459
+ const localHashes = Object.fromEntries(Object.entries(input.local).map(([p, t]) => [p, sha2562(t)]));
2460
+ const paths = /* @__PURE__ */ new Set([...Object.keys(input.base), ...Object.keys(input.team), ...Object.keys(localHashes)]);
2461
+ const rows = {};
2462
+ const needed = /* @__PURE__ */ new Set();
2463
+ for (const path of [...paths].sort()) {
2464
+ if (input.excluded.has(path)) continue;
2465
+ const row = decide(path, input.base[path], input.team[path], localHashes[path]);
2466
+ rows[path] = row;
2467
+ if (row === "team" || row === "conflict" || row === "index") needed.add(input.team[path]);
2468
+ if (row === "index" && input.base[path]) needed.add(input.base[path]);
2469
+ }
2470
+ return { rows, needed: [...needed] };
2471
+ }
2472
+ function planMerge(input, rows, texts) {
2473
+ const plan = {
2474
+ manifest: {},
2475
+ upload: {},
2476
+ writes: {},
2477
+ deletes: [],
2478
+ pulled: [],
2479
+ pushed: [],
2480
+ deletedHere: [],
2481
+ deletedFromTeam: [],
2482
+ conflicts: [],
2483
+ indexTeamLines: []
2484
+ };
2485
+ const taken = new Set(input.taken);
2486
+ const teamText = (path) => {
2487
+ const text3 = texts[input.team[path]];
2488
+ if (text3 === void 0) throw new Error(`the team's ${path} could not be read`);
2489
+ return text3;
2490
+ };
2491
+ for (const path of input.excluded) if (input.team[path]) plan.manifest[path] = input.team[path];
2492
+ for (const [path, row] of Object.entries(rows)) {
2493
+ const team = input.team[path];
2494
+ const local = input.local[path];
2495
+ switch (row) {
2496
+ case "unchanged":
2497
+ if (team) plan.manifest[path] = team;
2498
+ break;
2499
+ case "local":
2500
+ plan.manifest[path] = sha2562(local);
2501
+ plan.upload[path] = local;
2502
+ plan.pushed.push(path);
2503
+ break;
2504
+ case "delete-team":
2505
+ plan.deletedFromTeam.push(path);
2506
+ break;
2507
+ case "team":
2508
+ plan.manifest[path] = team;
2509
+ plan.writes[path] = teamText(path);
2510
+ plan.pulled.push(path);
2511
+ break;
2512
+ case "delete-local":
2513
+ plan.deletes.push(path);
2514
+ plan.deletedHere.push(path);
2515
+ break;
2516
+ case "conflict": {
2517
+ const copy = conflictCopyName(path, input.date, taken);
2518
+ taken.add(copy.toLowerCase());
2519
+ plan.manifest[path] = team;
2520
+ plan.writes[path] = teamText(path);
2521
+ plan.writes[copy] = local;
2522
+ plan.conflicts.push({ path, copy });
2523
+ break;
2524
+ }
2525
+ case "index": {
2526
+ const base = input.base[path] ? texts[input.base[path]] : void 0;
2527
+ const merged = mergeIndex(base, teamText(path), local);
2528
+ const hash = sha2562(merged.text);
2529
+ plan.manifest[path] = hash;
2530
+ if (merged.text !== local) {
2531
+ plan.writes[path] = merged.text;
2532
+ plan.pulled.push(path);
2533
+ }
2534
+ if (hash !== team) {
2535
+ plan.upload[path] = merged.text;
2536
+ plan.pushed.push(path);
2537
+ }
2538
+ plan.indexTeamLines.push(...merged.teamLines);
2539
+ break;
2540
+ }
2541
+ }
2542
+ }
2543
+ return plan;
2544
+ }
2545
+ var LINK_RE = /\]\(\s*<?([^)\s>]+)>?(?:\s+["'(][^)]*)?\)/;
2546
+ function entries(lines) {
2547
+ const seen = /* @__PURE__ */ new Map();
2548
+ return lines.map((line) => {
2549
+ const link = LINK_RE.exec(line);
2550
+ const id = link ? `link:${link[1]}` : `text:${line}`;
2551
+ const n = seen.get(id) ?? 0;
2552
+ seen.set(id, n + 1);
2553
+ return { key: n ? `${id}#${n}` : id, line };
2554
+ });
2555
+ }
2556
+ var splitLines = (text3) => text3.split(/\r?\n/);
2557
+ function mergeIndex(base, team, local) {
2558
+ const eol = local.includes("\r\n") ? "\r\n" : "\n";
2559
+ const b = new Map(entries(base === void 0 ? [] : splitLines(base)).map((e) => [e.key, e.line]));
2560
+ const teamEntries = entries(splitLines(team));
2561
+ const t = new Map(teamEntries.map((e) => [e.key, e.line]));
2562
+ const localEntries = entries(splitLines(local));
2563
+ const result = [];
2564
+ const teamLines = [];
2565
+ for (const e of localEntries) {
2566
+ const was = b.get(e.key);
2567
+ const theirs = t.get(e.key);
2568
+ if (theirs === e.line || theirs === was) result.push(e);
2569
+ else if (e.line === was) {
2570
+ if (theirs !== void 0) result.push({ key: e.key, line: theirs });
2571
+ } else {
2572
+ result.push(e);
2573
+ if (theirs !== void 0) teamLines.push(theirs);
2574
+ }
2575
+ }
2576
+ const baseEntries = entries(base === void 0 ? [] : splitLines(base));
2577
+ const localKeys = new Set(localEntries.map((e) => e.key));
2578
+ const replacedHere = /* @__PURE__ */ new Map();
2579
+ baseEntries.forEach((e, i) => {
2580
+ if (e.key.startsWith("text:") && !t.has(e.key) && !localKeys.has(e.key)) {
2581
+ replacedHere.set(e.key, i ? baseEntries[i - 1].key : "");
2582
+ }
2583
+ });
2584
+ const inResult = (key) => result.findIndex((e) => e.key === key);
2585
+ teamEntries.forEach((e, i) => {
2586
+ if (inResult(e.key) !== -1) return;
2587
+ const was = b.get(e.key);
2588
+ const before = i ? teamEntries[i - 1].key : "";
2589
+ const both = e.key.startsWith("text:") && [...replacedHere].find(([, prev]) => prev === before);
2590
+ if (was === void 0 && both) {
2591
+ replacedHere.delete(both[0]);
2592
+ teamLines.push(e.line);
2593
+ } else if (was === void 0) {
2594
+ let at = -1;
2595
+ for (let j = i - 1; j >= 0 && at === -1; j--) at = inResult(teamEntries[j].key);
2596
+ result.splice(at + 1, 0, e);
2597
+ } else if (e.line !== was) {
2598
+ teamLines.push(e.line);
2599
+ }
2600
+ });
2601
+ return { text: result.map((e) => e.line).join(eol), teamLines };
2602
+ }
2603
+ var groupKey = (kind, normalised) => `${kind}:${normalised}`;
2604
+ function collectValues(files) {
2605
+ const groups = /* @__PURE__ */ new Map();
2606
+ const homePaths = [];
2607
+ const findings = {};
2608
+ for (const path of Object.keys(files).sort()) {
2609
+ const found = findPersonValues(files[path]);
2610
+ findings[path] = found;
2611
+ for (const f of found) {
2612
+ if (f.kind === HOME_PATH) {
2613
+ if (!homePaths.includes(path)) homePaths.push(path);
2614
+ continue;
2615
+ }
2616
+ const key = groupKey(f.kind, f.normalised);
2617
+ const group = groups.get(key);
2618
+ if (group) {
2619
+ if (!group.files.includes(path)) group.files.push(path);
2620
+ } else {
2621
+ groups.set(key, {
2622
+ kind: f.kind,
2623
+ value: f.value,
2624
+ normalised: f.normalised,
2625
+ files: [path],
2626
+ first: { path, line: f.line, text: splitLines(files[path])[f.line - 1].trim() }
2627
+ });
2628
+ }
2629
+ }
2630
+ }
2631
+ return { groups: [...groups.values()], homePaths, findings };
2632
+ }
2633
+ function personToken(personId, label) {
2634
+ return label ? `\`${personId}\` (${label})` : `\`${personId}\``;
2635
+ }
2636
+ function rewriteValues(text3, findings, replacement) {
2637
+ let out = "";
2638
+ let last = 0;
2639
+ for (const f of [...findings].sort((a, b) => a.start - b.start)) {
2640
+ const to = replacement(f);
2641
+ if (to === void 0) continue;
2642
+ out += text3.slice(last, f.start) + to;
2643
+ last = f.end;
2644
+ }
2645
+ return out + text3.slice(last);
2646
+ }
2647
+ function lineDiff(before, after) {
2648
+ const a = splitLines(before);
2649
+ const b = splitLines(after);
2650
+ const out = [];
2651
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
2652
+ if (a[i] === b[i]) continue;
2653
+ if (a[i] !== void 0) out.push(`- ${a[i]}`);
2654
+ if (b[i] !== void 0) out.push(`+ ${b[i]}`);
2655
+ }
2656
+ return out;
2657
+ }
2658
+
2659
+ // src/lib/team-setup-files.ts
2660
+ import { execFile } from "child_process";
2661
+ import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2662
+ import { homedir as homedir3, userInfo } from "os";
2663
+ import { dirname as dirname2, join as join2 } from "path";
2664
+ import { promisify } from "util";
2665
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2666
+ function isWorkspace(value) {
2667
+ return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
2668
+ }
2669
+ function homeDir() {
2670
+ return homedir3();
2671
+ }
2672
+ function systemdoxDir() {
2673
+ return join2(homeDir(), ".claude", "systemdox");
2674
+ }
2675
+ function workspaceDir(workspace) {
2676
+ if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
2677
+ return join2(systemdoxDir(), workspace);
2678
+ }
2679
+ function statePath(workspace) {
2680
+ return join2(workspaceDir(workspace), "state.json");
2681
+ }
2682
+ async function readJson(path, empty) {
2683
+ let text3;
2684
+ try {
2685
+ text3 = await readFile2(path, "utf8");
2686
+ } catch (e) {
2687
+ if (e.code === "ENOENT") return empty;
2688
+ throw e;
2689
+ }
2690
+ try {
2691
+ return JSON.parse(text3);
2692
+ } catch {
2693
+ throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
2694
+ }
2695
+ }
2696
+ async function writeJsonAtomically(path, value, mode) {
2697
+ await mkdir2(dirname2(path), { recursive: true });
2698
+ const tmp = `${path}.${process.pid}.tmp`;
2699
+ await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
2700
+ await rename(tmp, path);
2701
+ }
2702
+ var run = promisify(execFile);
2703
+ async function readState(workspace) {
2704
+ return readJson(statePath(workspace), {});
2705
+ }
2706
+ async function writeState(workspace, state) {
2707
+ await writeJsonAtomically(statePath(workspace), state);
2708
+ }
2709
+ async function updateState(workspace, change) {
2710
+ const state = await readState(workspace);
2711
+ change(state);
2712
+ await writeState(workspace, state);
2713
+ return state;
2714
+ }
2715
+
2716
+ // src/sync-memory.ts
2717
+ var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
2718
+ var SyncFailed = class extends Error {
2719
+ };
2720
+ function refusal(e) {
2721
+ if (!(e instanceof Error)) return {};
2722
+ const { status, code } = e;
2723
+ return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
2724
+ }
2725
+ var toPath = (root, rel) => join3(root, ...rel.split("/"));
2726
+ async function readFolder(root) {
2727
+ const files = {};
2728
+ const names = /* @__PURE__ */ new Set();
2729
+ async function walk2(dir, prefix) {
2730
+ for (const e of await readdir2(dir, { withFileTypes: true })) {
2731
+ if (e.name.startsWith(".")) continue;
2732
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
2733
+ if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
2734
+ else if (e.isFile()) {
2735
+ names.add(rel.toLowerCase());
2736
+ if (e.name.endsWith(".md")) files[rel] = await readFile3(join3(dir, e.name), "utf8");
2737
+ }
2738
+ }
2739
+ }
2740
+ if (existsSync2(root)) await walk2(root, "");
2741
+ return { files, names };
2742
+ }
2743
+ async function pullTeam(client, ws, project) {
2744
+ try {
2745
+ const latest = await client.getMemory(ws, project);
2746
+ return {
2747
+ version: latest.version,
2748
+ manifest: Object.fromEntries(Object.entries(latest.files).map(([p, f]) => [p, f.sha256])),
2749
+ exists: true
2750
+ };
2751
+ } catch (e) {
2752
+ if (refusal(e).status === 404) return { version: 0, manifest: {}, exists: false };
2753
+ throw e;
2754
+ }
2755
+ }
2756
+ async function readTexts(client, ws, project, hashes) {
2757
+ const texts = {};
2758
+ const one = async (batch) => {
2759
+ let left = batch;
2760
+ while (left.length) {
2761
+ const got = await client.readBlobs(ws, project, left);
2762
+ for (const b of got.blobs) texts[b.sha256] = b.content;
2763
+ if (!got.blobs.length) break;
2764
+ left = got.remaining;
2765
+ }
2766
+ };
2767
+ for (let i = 0; i < hashes.length; i += 1e3) {
2768
+ const batch = hashes.slice(i, i + 1e3);
2769
+ try {
2770
+ await one(batch);
2771
+ } catch (e) {
2772
+ if (!(refusal(e).status === 404)) throw e;
2773
+ for (const h of batch) {
2774
+ try {
2775
+ await one([h]);
2776
+ } catch (err) {
2777
+ if (!(refusal(err).status === 404)) throw err;
2778
+ }
2779
+ }
2780
+ }
2781
+ }
2782
+ return texts;
2783
+ }
2784
+ var Report = class {
2785
+ pulled = /* @__PURE__ */ new Set();
2786
+ pushed = /* @__PURE__ */ new Set();
2787
+ deletedHere = /* @__PURE__ */ new Set();
2788
+ deletedFromTeam = /* @__PURE__ */ new Set();
2789
+ conflicts = [];
2790
+ skipped = /* @__PURE__ */ new Map();
2791
+ people = [];
2792
+ toAnswer = /* @__PURE__ */ new Map();
2793
+ add(plan) {
2794
+ plan.pulled.forEach((p) => this.pulled.add(p));
2795
+ plan.pushed.forEach((p) => this.pushed.add(p));
2796
+ plan.deletedHere.forEach((p) => this.deletedHere.add(p));
2797
+ plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
2798
+ for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
2799
+ for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
2800
+ }
2801
+ skip(path, reason) {
2802
+ if (!this.skipped.has(path)) this.skipped.set(path, reason);
2803
+ }
2804
+ lines(summary, dryRun) {
2805
+ const out = [summary];
2806
+ const section = (title, items) => {
2807
+ if (items.length) out.push("", `${title}:`, ...items.map((i) => `- ${i}`));
2808
+ };
2809
+ const sorted = (s) => [...s].sort();
2810
+ section(dryRun ? "Would pull" : "Pulled", sorted(this.pulled));
2811
+ section(dryRun ? "Would push" : "Pushed", sorted(this.pushed));
2812
+ section(dryRun ? "Would delete" : "Deleted", [
2813
+ ...sorted(this.deletedHere).map((p) => `${p} (here: the team deleted it)`),
2814
+ ...sorted(this.deletedFromTeam).map((p) => `${p} (from the team: you deleted it)`)
2815
+ ]);
2816
+ section("Conflicts", this.conflicts);
2817
+ section(
2818
+ "Skipped",
2819
+ [...this.skipped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([p, why]) => `${p}: ${why}`)
2820
+ );
2821
+ section(
2822
+ "People",
2823
+ this.people.map(
2824
+ (a) => [a.kind, a.answer, a.label, a.personId && `\`${a.personId}\``, a.files !== void 0 && `${a.files} files`].filter(Boolean).join(", ")
2825
+ )
2826
+ );
2827
+ if (this.toAnswer.size) {
2828
+ out.push(
2829
+ "",
2830
+ `Values still to answer: ${[...this.toAnswer.entries()].map(([kind, n]) => `${n} ${kind}`).join(", ")}.`
2831
+ );
2832
+ }
2833
+ return out;
2834
+ }
2835
+ };
2836
+ var ANSWER_WORDS = {
2837
+ member: "a member's contact",
2838
+ person: "someone else",
2839
+ ours: "ours, not a person"
2840
+ };
2841
+ function labelProblem(label) {
2842
+ if (!label) return "a label is needed";
2843
+ if (label.length > 60) return "a label is at most 60 characters";
2844
+ if (findPersonValues(label).length) return "a label is a role, never a phone number, an email address or a path";
2845
+ return void 0;
2846
+ }
2847
+ async function askLabel(ask, out, question, optional) {
2848
+ for (; ; ) {
2849
+ const label = (await ask(question)).trim();
2850
+ if (optional && !label) return void 0;
2851
+ const problem = labelProblem(label);
2852
+ if (!problem) return label;
2853
+ out(` ${problem}.`);
2854
+ }
2855
+ }
2856
+ async function people(opts, ws, files, report, hint) {
2857
+ const { client, out, ask, dryRun } = opts;
2858
+ const terminal = !!ask && !dryRun;
2859
+ const held = /* @__PURE__ */ new Set();
2860
+ const leftOut = /* @__PURE__ */ new Set();
2861
+ const current = { ...files };
2862
+ for (const [path, text3] of Object.entries(current)) {
2863
+ const secret = findSecrets(text3)[0];
2864
+ if (secret) {
2865
+ held.add(path);
2866
+ report.skip(path, `a secret (${secret.kind}) on line ${secret.line}: remove it, then sync again`);
2867
+ }
2868
+ }
2869
+ const { groups, findings } = collectValues(current);
2870
+ const answers = /* @__PURE__ */ new Map();
2871
+ for (let i = 0; i < groups.length; i += 500) {
2872
+ const batch = groups.slice(i, i + 500);
2873
+ const got = await client.lookupValues(ws, batch.map((g) => ({ kind: g.kind, value: g.value })));
2874
+ batch.forEach((g, j) => {
2875
+ const a = got.answers[j];
2876
+ if (a && a.answer !== "unanswered") answers.set(groupKey(g.kind, g.normalised), a);
2877
+ });
2878
+ }
2879
+ const vaultDown = /* @__PURE__ */ new Set();
2880
+ if (terminal) {
2881
+ const filed = [];
2882
+ for (const g of groups) {
2883
+ const key = groupKey(g.kind, g.normalised);
2884
+ if (answers.has(key)) continue;
2885
+ let choices = ["m", "p", "o", "x", "s"];
2886
+ for (; ; ) {
2887
+ const question = [
2888
+ `${g.value} (${g.kind}) is in ${g.files.length} file${g.files.length === 1 ? "" : "s"}. First line it appears on, in ${g.first.path}:`,
2889
+ ` ${g.first.text}`,
2890
+ [
2891
+ choices.includes("m") && "[m] a member's contact",
2892
+ "[p] someone else",
2893
+ "[o] ours, not a person",
2894
+ "[x] leave these files out",
2895
+ "[s] skip for now"
2896
+ ].filter(Boolean).join(" "),
2897
+ `Answer (${choices.join("/")}): `
2898
+ ].join("\n");
2899
+ const answer = (await ask(question)).trim().toLowerCase();
2900
+ if (!choices.includes(answer)) continue;
2901
+ if (answer === "s") break;
2902
+ if (answer === "x") {
2903
+ for (const path of g.files) {
2904
+ if (leftOut.has(path)) continue;
2905
+ current[path] = addShareFalse(current[path]);
2906
+ await writeFile3(toPath(opts.dir, path), current[path], "utf8");
2907
+ leftOut.add(path);
2908
+ }
2909
+ report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
2910
+ break;
2911
+ }
2912
+ if (answer === "p") {
2913
+ const label2 = await askLabel(ask, out, "Their role, as a label (e.g. reseller, sales; never a name): ", false);
2914
+ let entry = filed.length ? await (async () => {
2915
+ const list = filed.map((f, i) => ` ${i + 1}. ${f.label}: ${f.groups.map((x) => x.value).join(", ")}`);
2916
+ for (; ; ) {
2917
+ const same = (await ask(["The same person as one filed above?", ...list, "Number, or Enter for a new person: "].join("\n"))).trim();
2918
+ if (!same) return void 0;
2919
+ const n = Number(same);
2920
+ if (Number.isInteger(n) && n >= 1 && n <= filed.length && filed[n - 1].groups.length < 4) return filed[n - 1];
2921
+ }
2922
+ })() : void 0;
2923
+ if (!entry) {
2924
+ entry = { groups: [], label: label2 };
2925
+ filed.push(entry);
2926
+ }
2927
+ entry.groups.push(g);
2928
+ break;
2929
+ }
2930
+ 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;
2931
+ let result;
2932
+ try {
2933
+ result = await client.answerValues(ws, [
2934
+ { kind: g.kind, value: g.value, answer: answer === "m" ? "member" : "ours", ...label ? { label } : {} }
2935
+ ]);
2936
+ } catch (e) {
2937
+ if (refusal(e).code === "SYD-K019") {
2938
+ vaultDown.add(key);
2939
+ out(" The vault did not answer (SYD-K019): the files holding it wait for the next sync.");
2940
+ break;
2941
+ }
2942
+ throw e;
2943
+ }
2944
+ if (result.stored[0]) {
2945
+ answers.set(key, result.stored[0]);
2946
+ report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
2947
+ break;
2948
+ }
2949
+ const refused = result.refused[0];
2950
+ if (refused?.code === "SYD-K021") {
2951
+ out(` ${refused.detail}. Add them in Staff first, or answer p to file them.`);
2952
+ choices = choices.filter((c) => c !== "m");
2953
+ } else if (refused) out(` ${refused.detail}`);
2954
+ }
2955
+ }
2956
+ for (let i = 0; i < filed.length; i += 50) {
2957
+ const batch = filed.slice(i, i + 50);
2958
+ let result;
2959
+ try {
2960
+ result = await client.fileMemoryPeople(
2961
+ ws,
2962
+ batch.map((f) => ({ values: f.groups.map((g) => ({ kind: g.kind, value: g.value })), label: f.label }))
2963
+ );
2964
+ } catch (e) {
2965
+ if (refusal(e).code === "SYD-K019") {
2966
+ out("Contacts did not answer (SYD-K019): the files holding those values wait for the next sync.");
2967
+ batch.forEach((f) => f.groups.forEach((g) => vaultDown.add(groupKey(g.kind, g.normalised))));
2968
+ continue;
2969
+ }
2970
+ throw e;
2971
+ }
2972
+ for (const stored of result.stored) {
2973
+ let key;
2974
+ try {
2975
+ key = groupKey(stored.kind, normalise(stored.kind, stored.value));
2976
+ } catch {
2977
+ continue;
2978
+ }
2979
+ answers.set(key, stored);
2980
+ }
2981
+ for (const r of result.refused) {
2982
+ out(` ${batch[r.index]?.label ?? ""}: ${r.detail}`);
2983
+ }
2984
+ for (const f of batch) {
2985
+ const a = answers.get(groupKey(f.groups[0].kind, f.groups[0].normalised));
2986
+ if (a?.answer === "person") {
2987
+ for (const g of f.groups) report.people.push({ kind: g.kind, answer: ANSWER_WORDS.person, label: a.label, personId: a.personId });
2988
+ }
2989
+ }
2990
+ }
2991
+ }
2992
+ const rewritten = {};
2993
+ for (const [path, text3] of Object.entries(current)) {
2994
+ if (leftOut.has(path) || held.has(path)) continue;
2995
+ const next = rewriteValues(text3, findings[path] ?? [], (f) => {
2996
+ if (f.kind === HOME_PATH) return "~";
2997
+ const a = answers.get(groupKey(f.kind, f.normalised));
2998
+ return a && (a.answer === "member" || a.answer === "person") && a.personId ? personToken(a.personId, a.label) : void 0;
2999
+ });
3000
+ if (next !== text3) rewritten[path] = next;
3001
+ }
3002
+ const paths = Object.keys(rewritten).sort();
3003
+ if (paths.length && terminal) {
3004
+ out("", "The rewrite:");
3005
+ for (const path of paths) out(path, ...lineDiff(current[path], rewritten[path]).map((l) => ` ${l}`));
3006
+ const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
3007
+ if (yes) {
3008
+ for (const path of paths) {
3009
+ await writeFile3(toPath(opts.dir, path), rewritten[path], "utf8");
3010
+ current[path] = rewritten[path];
3011
+ }
3012
+ }
3013
+ }
3014
+ for (const [path, text3] of Object.entries(current)) {
3015
+ if (leftOut.has(path) || held.has(path)) continue;
3016
+ const pending = findPersonValues(text3).filter((f) => {
3017
+ if (f.kind === HOME_PATH) return true;
3018
+ return answers.get(groupKey(f.kind, f.normalised))?.answer !== "ours";
3019
+ });
3020
+ if (!pending.length) continue;
3021
+ held.add(path);
3022
+ const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
3023
+ if (keys.some((k) => vaultDown.has(k))) {
3024
+ report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
3025
+ } else if (keys.some((k) => !answers.has(k))) {
3026
+ report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
3027
+ } else {
3028
+ report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
3029
+ }
3030
+ }
3031
+ for (const g of groups) {
3032
+ const key = groupKey(g.kind, g.normalised);
3033
+ if (!answers.has(key) && g.files.some((p) => !leftOut.has(p))) {
3034
+ report.toAnswer.set(g.kind, (report.toAnswer.get(g.kind) ?? 0) + 1);
3035
+ }
3036
+ }
3037
+ return { files: current, held, leftOut };
3038
+ }
3039
+ async function applyPlan(dir, plan) {
3040
+ for (const [path, text3] of Object.entries(plan.writes)) {
3041
+ const full = toPath(dir, path);
3042
+ await mkdir3(dirname3(full), { recursive: true });
3043
+ await writeFile3(full, text3, "utf8");
3044
+ }
3045
+ for (const path of plan.deletes) await rm3(toPath(dir, path), { force: true });
3046
+ }
3047
+ var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
3048
+ async function upload(client, ws, project, plan, team, report) {
3049
+ const byHash = /* @__PURE__ */ new Map();
3050
+ for (const path of Object.keys(plan.upload)) {
3051
+ const h = plan.manifest[path];
3052
+ byHash.set(h, [...byHash.get(h) ?? [], path]);
3053
+ }
3054
+ const hashes = [...byHash.keys()];
3055
+ const missing = [];
3056
+ for (let i = 0; i < hashes.length; i += 1e4) {
3057
+ missing.push(...(await client.findMissingBlobs(ws, project, hashes.slice(i, i + 1e4))).missing);
3058
+ }
3059
+ const blobs = missing.map((h) => ({ sha256: h, content: plan.upload[byHash.get(h)[0]] }));
3060
+ let batch = [];
3061
+ let bytes = 0;
3062
+ const send = async () => {
3063
+ if (!batch.length) return;
3064
+ const result = await client.uploadBlobs(ws, project, batch);
3065
+ for (const r of result.refused) {
3066
+ for (const path of byHash.get(r.sha256) ?? []) {
3067
+ if (team[path]) plan.manifest[path] = team[path];
3068
+ else delete plan.manifest[path];
3069
+ plan.pushed = plan.pushed.filter((p) => p !== path);
3070
+ report.pushed.delete(path);
3071
+ report.skip(path, `refused by SystemDox, ${r.detail.startsWith(r.code) ? r.detail : `${r.code}: ${r.detail}`}`);
3072
+ }
3073
+ }
3074
+ batch = [];
3075
+ bytes = 0;
3076
+ };
3077
+ for (const blob of blobs) {
3078
+ const size = Buffer.byteLength(blob.content, "utf8");
3079
+ if (batch.length === 100 || batch.length && bytes + size > 5 * 1024 * 1024 - 64 * 1024) await send();
3080
+ batch.push(blob);
3081
+ bytes += size;
3082
+ }
3083
+ await send();
3084
+ }
3085
+ async function syncMemory(opts) {
3086
+ const { client, out } = opts;
3087
+ const now = opts.now?.() ?? /* @__PURE__ */ new Date();
3088
+ try {
3089
+ const own = await client.getOwnTeamSetup();
3090
+ const ws = own.workspace;
3091
+ const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
3092
+ if (!project) {
3093
+ throw new SyncFailed(
3094
+ 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.`
3095
+ );
3096
+ }
3097
+ const dir = resolve3(opts.dir ?? join3(workspaceDir(ws), "memory", project));
3098
+ const run2 = { ...opts, dir };
3099
+ const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
3100
+ const report = new Report();
3101
+ const date = localDate(now);
3102
+ const stored = (await readState(ws)).memory?.[project];
3103
+ let team = await pullTeam(client, ws, project);
3104
+ let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
3105
+ const folder = await readFolder(dir);
3106
+ const shared = {};
3107
+ const notShared = /* @__PURE__ */ new Set();
3108
+ for (const [path, text3] of Object.entries(folder.files)) {
3109
+ if (!isShareable(path, text3)) notShared.add(path);
3110
+ else if (!isFilePath(path)) {
3111
+ notShared.add(path);
3112
+ report.skip(path, "its name has characters a team memory path cannot hold (letters, digits, '.', '_', '-')");
3113
+ } else shared[path] = text3;
3114
+ }
3115
+ const importWrites = {};
3116
+ if (opts.importDir) {
3117
+ const other = await readFolder(resolve3(opts.importDir));
3118
+ const taken = new Set(folder.names);
3119
+ for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
3120
+ if (!isShareable(path, text3) || !isFilePath(path)) continue;
3121
+ const here = folder.files[path];
3122
+ if (here === void 0) {
3123
+ shared[path] = text3;
3124
+ importWrites[path] = text3;
3125
+ taken.add(path.toLowerCase());
3126
+ } else if (here !== text3) {
3127
+ const copy = conflictCopyName(path, date, taken);
3128
+ taken.add(copy.toLowerCase());
3129
+ importWrites[copy] = text3;
3130
+ report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
3131
+ }
3132
+ }
3133
+ folder.names = taken;
3134
+ if (!opts.dryRun) {
3135
+ for (const [path, text3] of Object.entries(importWrites)) {
3136
+ const full = toPath(dir, path);
3137
+ await mkdir3(dirname3(full), { recursive: true });
3138
+ await writeFile3(full, text3, "utf8");
3139
+ }
3140
+ }
3141
+ }
3142
+ const found = await people(run2, ws, shared, report, hint);
3143
+ for (const path of found.leftOut) {
3144
+ delete found.files[path];
3145
+ notShared.add(path);
3146
+ }
3147
+ const excluded = /* @__PURE__ */ new Set([...found.held, ...notShared]);
3148
+ for (const path of found.held) delete found.files[path];
3149
+ for (let attempt = 1; ; attempt++) {
3150
+ const input = { base: base.manifest, team: team.manifest, local: found.files, excluded, taken: folder.names, date };
3151
+ const { rows, needed } = decideAll(input);
3152
+ const texts = needed.length ? await readTexts(client, ws, project, needed) : {};
3153
+ const plan = planMerge(input, rows, texts);
3154
+ report.add(plan);
3155
+ if (opts.dryRun) {
3156
+ const skipped = report.skipped.size;
3157
+ out(
3158
+ ...report.lines(
3159
+ `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.`,
3160
+ true
3161
+ )
3162
+ );
3163
+ return 0;
3164
+ }
3165
+ await applyPlan(dir, plan);
3166
+ let version = team.version;
3167
+ if (!sameManifest(plan.manifest, team.manifest)) {
3168
+ await upload(client, ws, project, plan, team.manifest, report);
3169
+ }
3170
+ if (!sameManifest(plan.manifest, team.manifest)) {
3171
+ try {
3172
+ version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
3173
+ } catch (e) {
3174
+ if (!(refusal(e).status === 409 && refusal(e).code === "SYD-K010")) throw e;
3175
+ if (attempt === 2) {
3176
+ out(
3177
+ ...report.lines(
3178
+ `Not pushed: ${ws}/${project} moved on twice while this sync merged. Your folder holds the merge; run sync-memory again.`,
3179
+ false
3180
+ )
3181
+ );
3182
+ return 2;
3183
+ }
3184
+ base = team;
3185
+ for (const p of plan.pushed) report.pushed.delete(p);
3186
+ const again = await readFolder(dir);
3187
+ folder.names = again.names;
3188
+ found.files = {};
3189
+ for (const [path, text3] of Object.entries(again.files)) {
3190
+ if (excluded.has(path) || !isShareable(path, text3) || !isFilePath(path)) continue;
3191
+ found.files[path] = text3;
3192
+ }
3193
+ team = await pullTeam(client, ws, project);
3194
+ continue;
3195
+ }
3196
+ }
3197
+ await updateState(ws, (state) => {
3198
+ state.memory = { ...state.memory, [project]: { version, manifest: plan.manifest } };
3199
+ });
3200
+ out(
3201
+ ...report.lines(
3202
+ `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.`,
3203
+ false
3204
+ )
3205
+ );
3206
+ return 0;
3207
+ }
3208
+ } catch (e) {
3209
+ out(e.message);
3210
+ return 1;
3211
+ }
3212
+ }
3213
+
2327
3214
  // src/tools/team-setup.ts
2328
3215
  var LAYOUT = {
2329
3216
  hooks: { files: ["hooks/hooks.json"], folders: [{ dir: "scripts", md: false }], required: "hooks/hooks.json" },
@@ -2333,7 +3220,7 @@ var LAYOUT = {
2333
3220
  instructions: { files: ["CLAUDE.md"], folders: [], required: "CLAUDE.md" }
2334
3221
  };
2335
3222
  function expandHome2(path) {
2336
- return path.replace(/^~(?=$|[\\/])/, homedir3());
3223
+ return path.replace(/^~(?=$|[\\/])/, homedir4());
2337
3224
  }
2338
3225
  function text2(t) {
2339
3226
  return { content: [{ type: "text", text: t }] };
@@ -2347,7 +3234,7 @@ async function isLink(path) {
2347
3234
  }
2348
3235
  }
2349
3236
  async function readPartDirectory(part, directory) {
2350
- const root = resolve3(directory);
3237
+ const root = resolve4(directory);
2351
3238
  const layout = LAYOUT[part];
2352
3239
  const paths = [];
2353
3240
  const leftOut = [];
@@ -2357,21 +3244,21 @@ async function readPartDirectory(part, directory) {
2357
3244
  let present = true;
2358
3245
  for (let i = 1; i <= segments.length; i++) {
2359
3246
  const rel = segments.slice(0, i).join("/");
2360
- if (await isLink(join2(root, ...segments.slice(0, i)))) {
3247
+ if (await isLink(join4(root, ...segments.slice(0, i)))) {
2361
3248
  links.push(rel);
2362
3249
  present = false;
2363
3250
  break;
2364
3251
  }
2365
3252
  }
2366
- if (present && existsSync2(join2(root, ...segments))) paths.push(path);
3253
+ if (present && existsSync3(join4(root, ...segments))) paths.push(path);
2367
3254
  }
2368
3255
  for (const { dir, md } of layout.folders) {
2369
- const full = join2(root, dir);
3256
+ const full = join4(root, dir);
2370
3257
  if (await isLink(full)) {
2371
3258
  links.push(dir);
2372
3259
  continue;
2373
3260
  }
2374
- if (!existsSync2(full)) continue;
3261
+ if (!existsSync3(full)) continue;
2375
3262
  const walked = await walk(full, { skipHidden: true });
2376
3263
  links.push(...walked.links.map((p) => `${dir}/${p}`));
2377
3264
  leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
@@ -2473,10 +3360,33 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
2473
3360
  return text2(describePerson(args.id, person));
2474
3361
  }
2475
3362
  );
3363
+ if (!localFiles) return;
3364
+ server.registerTool(
3365
+ "sync_memory",
3366
+ {
3367
+ title: "Sync Memory",
3368
+ description: `Sync one project's team memory on this computer with SystemDox: pull the files the team changed, push the ones changed here, keep both texts of a file changed on both sides. It never answers for a person: a file that still holds a phone number, an email address or a home-folder path is skipped and listed, with the command for the member to run in their own terminal (\`${SYNC_MEMORY_COMMAND}\`). Pass dryRun to see what it would do without writing or sending anything.`,
3369
+ inputSchema: {
3370
+ project: z14.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
3371
+ dryRun: z14.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
3372
+ },
3373
+ annotations: { readOnlyHint: false }
3374
+ },
3375
+ async (args) => {
3376
+ const lines = [];
3377
+ const code = await syncMemory({
3378
+ project: args.project,
3379
+ dryRun: !!args.dryRun,
3380
+ client: apiClient,
3381
+ out: (...l) => lines.push(...l)
3382
+ });
3383
+ return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
3384
+ }
3385
+ );
2476
3386
  }
2477
3387
 
2478
3388
  // src/server.ts
2479
- var SERVER_VERSION = true ? "0.1.46" : "0.0.0-dev";
3389
+ var SERVER_VERSION = true ? "0.1.47" : "0.0.0-dev";
2480
3390
  function createServer(apiClient, { localFiles = false } = {}) {
2481
3391
  const server = new McpServer({
2482
3392
  name: "systemdox-mcp-server",