@systemdox/mcp-server 0.1.56 → 0.1.58

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/cli.js CHANGED
@@ -75,6 +75,12 @@ function workspaceDir(workspace) {
75
75
  function statePath(workspace) {
76
76
  return join(workspaceDir(workspace), "state.json");
77
77
  }
78
+ function syncLogPath(workspace) {
79
+ return join(workspaceDir(workspace), "sync-memory.log");
80
+ }
81
+ function syncLockPath(workspace) {
82
+ return join(workspaceDir(workspace), "sync.lock");
83
+ }
78
84
  function pluginRoot(workspace) {
79
85
  return join(workspaceDir(workspace), "plugin");
80
86
  }
@@ -585,15 +591,41 @@ function readFrontmatter(text3) {
585
591
  const m = BLOCK_RE.exec(text3);
586
592
  if (!m) return void 0;
587
593
  const front = { shareFalse: false };
588
- for (const line of m[1].split(/\r?\n/)) {
589
- const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
594
+ const lines = m[1].split(/\r?\n/);
595
+ if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
596
+ let topType;
597
+ let topShare;
598
+ let nestedType;
599
+ let nestedShare;
600
+ let inMetadata = false;
601
+ let nestedIndent;
602
+ for (const line of lines) {
603
+ if (!line.trim() || /^[ \t]*#/.test(line)) continue;
604
+ const indent = /^ */.exec(line)[0].length;
605
+ if (indent === 0) {
606
+ inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
607
+ nestedIndent = void 0;
608
+ }
609
+ const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
590
610
  if (!kv) continue;
591
611
  const raw = kv[2].replace(/[ \t]+#.*$/, "");
592
612
  const quoted = /^(["'])(.*)\1$/.exec(raw);
593
613
  const value = quoted ? quoted[2] : raw;
594
- if (kv[1] === "type") front.type = value.trim().toLowerCase();
595
- if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
596
- }
614
+ const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
615
+ if (indent === 0) {
616
+ if (kv[1] === "type") topType = value.trim().toLowerCase();
617
+ if (kv[1] === "share") topShare = isFalse;
618
+ continue;
619
+ }
620
+ if (!inMetadata) continue;
621
+ nestedIndent ??= indent;
622
+ if (indent !== nestedIndent) continue;
623
+ if (kv[1] === "type") nestedType = value.trim().toLowerCase();
624
+ if (kv[1] === "share") nestedShare = isFalse;
625
+ }
626
+ const type = topType ?? nestedType;
627
+ if (type !== void 0) front.type = type;
628
+ front.shareFalse = (topShare ?? nestedShare) === true;
597
629
  return front;
598
630
  }
599
631
  function isShareable(path, text3) {
@@ -849,35 +881,69 @@ var init_memory_sync = __esm({
849
881
  // src/sync-memory.ts
850
882
  var sync_memory_exports = {};
851
883
  __export(sync_memory_exports, {
884
+ HOOK_BUDGET_MS: () => HOOK_BUDGET_MS,
885
+ HOOK_LOCK_STALE_MS: () => HOOK_LOCK_STALE_MS,
886
+ HOOK_LOG_MAX_BYTES: () => HOOK_LOG_MAX_BYTES,
852
887
  SYNC_MEMORY_COMMAND: () => SYNC_MEMORY_COMMAND,
853
888
  SYNC_MEMORY_USAGE: () => SYNC_MEMORY_USAGE,
889
+ hookLines: () => hookLines,
854
890
  parseSyncMemoryArgs: () => parseSyncMemoryArgs,
891
+ runHookSync: () => runHookSync,
855
892
  runSyncMemory: () => runSyncMemory,
856
893
  syncMemory: () => syncMemory
857
894
  });
858
895
  import { existsSync } from "fs";
859
- import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
896
+ import { appendFile, mkdir as mkdir2, open, readdir, readFile as readFile2, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
860
897
  import { dirname as dirname2, join as join2, resolve } from "path";
898
+ function failureReason(e) {
899
+ if (e instanceof SyncStopped) return e.reason;
900
+ const { status, code } = refusal(e);
901
+ if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
902
+ const err = e;
903
+ const net = err?.cause?.code ?? err?.code ?? "";
904
+ if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
905
+ return "SystemDox could not be reached";
906
+ }
907
+ return "it could not finish";
908
+ }
909
+ async function writeWhole(path, text3) {
910
+ await mkdir2(dirname2(path), { recursive: true });
911
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
912
+ try {
913
+ await writeFile2(tmp, text3, "utf8");
914
+ await rename2(tmp, path);
915
+ } catch (e) {
916
+ await rm2(tmp, { force: true });
917
+ throw e;
918
+ }
919
+ }
861
920
  function refusal(e) {
862
921
  if (!(e instanceof Error)) return {};
863
922
  const { status, code } = e;
864
923
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
865
924
  }
866
925
  function parseSyncMemoryArgs(argv) {
867
- const args2 = { dryRun: false };
926
+ const args2 = { dryRun: false, importDirs: [] };
868
927
  for (let i = 0; i < argv.length; i++) {
869
928
  const a = argv[i];
870
929
  if (a === "--dry-run") args2.dryRun = true;
871
- else if (a === "--project" || a === "--dir" || a === "--import") {
930
+ else if (a === "--project" || a === "--dir" || a === "--import" || a === "--hook") {
872
931
  const value = argv[++i];
873
932
  if (!value || value.startsWith("--")) throw new SyncFailed(`${a} needs a value
874
933
  ${SYNC_MEMORY_USAGE}`);
875
934
  if (a === "--project") {
876
935
  if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a project name (lower case letters, digits and -)`);
877
936
  args2.project = value;
937
+ } else if (a === "--hook") {
938
+ if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a workspace name (lower case letters, digits and -)`);
939
+ args2.hook = value;
878
940
  } else if (a === "--dir") args2.dir = value;
879
- else args2.importDir = value;
941
+ else args2.importDirs.push(value);
880
942
  } else throw new SyncFailed(`Unexpected argument ${a}
943
+ ${SYNC_MEMORY_USAGE}`);
944
+ }
945
+ if (args2.hook && (args2.project || args2.dir || args2.importDirs.length || args2.dryRun)) {
946
+ throw new SyncFailed(`--hook takes no other option
881
947
  ${SYNC_MEMORY_USAGE}`);
882
948
  }
883
949
  return args2;
@@ -885,6 +951,7 @@ ${SYNC_MEMORY_USAGE}`);
885
951
  async function readFolder(root) {
886
952
  const files = {};
887
953
  const names = /* @__PURE__ */ new Set();
954
+ const temps = [];
888
955
  async function walk2(dir, prefix) {
889
956
  for (const e of await readdir(dir, { withFileTypes: true })) {
890
957
  if (e.name.startsWith(".")) continue;
@@ -892,12 +959,13 @@ async function readFolder(root) {
892
959
  if (e.isDirectory()) await walk2(join2(dir, e.name), rel);
893
960
  else if (e.isFile()) {
894
961
  names.add(rel.toLowerCase());
962
+ if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
895
963
  if (e.name.endsWith(".md")) files[rel] = await readFile2(join2(dir, e.name), "utf8");
896
964
  }
897
965
  }
898
966
  }
899
967
  if (existsSync(root)) await walk2(root, "");
900
- return { files, names };
968
+ return { files, names, temps };
901
969
  }
902
970
  async function pullTeam(client, ws, project) {
903
971
  try {
@@ -1005,7 +1073,7 @@ async function people(opts, ws, files, report, hint) {
1005
1073
  for (const path of g.files) {
1006
1074
  if (leftOut.has(path)) continue;
1007
1075
  current[path] = addShareFalse(current[path]);
1008
- await writeFile2(toPath(opts.dir, path), current[path], "utf8");
1076
+ await writeWhole(toPath(opts.dir, path), current[path]);
1009
1077
  leftOut.add(path);
1010
1078
  }
1011
1079
  report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
@@ -1108,7 +1176,7 @@ async function people(opts, ws, files, report, hint) {
1108
1176
  const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
1109
1177
  if (yes) {
1110
1178
  for (const path of paths) {
1111
- await writeFile2(toPath(opts.dir, path), rewritten[path], "utf8");
1179
+ await writeWhole(toPath(opts.dir, path), rewritten[path]);
1112
1180
  current[path] = rewritten[path];
1113
1181
  }
1114
1182
  }
@@ -1123,11 +1191,11 @@ async function people(opts, ws, files, report, hint) {
1123
1191
  held.add(path);
1124
1192
  const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
1125
1193
  if (keys.some((k) => vaultDown.has(k))) {
1126
- report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
1194
+ report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
1127
1195
  } else if (keys.some((k) => !answers.has(k))) {
1128
- report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
1196
+ report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
1129
1197
  } else {
1130
- report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
1198
+ report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
1131
1199
  }
1132
1200
  }
1133
1201
  for (const g of groups) {
@@ -1138,12 +1206,11 @@ async function people(opts, ws, files, report, hint) {
1138
1206
  }
1139
1207
  return { files: current, held, leftOut };
1140
1208
  }
1209
+ async function removeLeftTemps(dir, temps) {
1210
+ for (const rel of temps) await rm2(toPath(dir, rel), { force: true });
1211
+ }
1141
1212
  async function applyPlan(dir, plan) {
1142
- for (const [path, text3] of Object.entries(plan.writes)) {
1143
- const full = toPath(dir, path);
1144
- await mkdir2(dirname2(full), { recursive: true });
1145
- await writeFile2(full, text3, "utf8");
1146
- }
1213
+ for (const [path, text3] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text3);
1147
1214
  for (const path of plan.deletes) await rm2(toPath(dir, path), { force: true });
1148
1215
  }
1149
1216
  async function upload(client, ws, project, plan, team, report) {
@@ -1187,8 +1254,19 @@ async function syncMemory(opts) {
1187
1254
  const { client, out } = opts;
1188
1255
  const now = opts.now?.() ?? /* @__PURE__ */ new Date();
1189
1256
  try {
1257
+ const checkDeadline = () => {
1258
+ if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
1259
+ throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
1260
+ }
1261
+ };
1190
1262
  const own = await client.getOwnTeamSetup();
1191
1263
  const ws = own.workspace;
1264
+ if (opts.workspace && ws !== opts.workspace) {
1265
+ throw new SyncStopped(
1266
+ "the key is for another workspace",
1267
+ `The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
1268
+ );
1269
+ }
1192
1270
  const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
1193
1271
  if (!project) {
1194
1272
  throw new SyncFailed(
@@ -1202,6 +1280,12 @@ async function syncMemory(opts) {
1202
1280
  const date = localDate(now);
1203
1281
  const stored = (await readState(ws)).memory?.[project];
1204
1282
  let team = await pullTeam(client, ws, project);
1283
+ if (opts.unattended && stored && !team.exists) {
1284
+ throw new SyncStopped(
1285
+ "the team memory could not be read",
1286
+ `${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
1287
+ );
1288
+ }
1205
1289
  let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
1206
1290
  const folder = await readFolder(dir);
1207
1291
  const shared = {};
@@ -1214,14 +1298,16 @@ async function syncMemory(opts) {
1214
1298
  } else shared[path] = text3;
1215
1299
  }
1216
1300
  const importWrites = {};
1217
- if (opts.importDir) {
1218
- const other = await readFolder(resolve(opts.importDir));
1219
- const taken = new Set(folder.names);
1301
+ const current = { ...folder.files };
1302
+ const taken = new Set(folder.names);
1303
+ for (const importDir of opts.importDirs ?? []) {
1304
+ const other = await readFolder(resolve(importDir));
1220
1305
  for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
1221
1306
  if (!isShareable(path, text3) || !isFilePath(path)) continue;
1222
- const here = folder.files[path];
1307
+ const here = current[path];
1223
1308
  if (here === void 0) {
1224
1309
  shared[path] = text3;
1310
+ current[path] = text3;
1225
1311
  importWrites[path] = text3;
1226
1312
  taken.add(path.toLowerCase());
1227
1313
  } else if (here !== text3) {
@@ -1229,17 +1315,15 @@ async function syncMemory(opts) {
1229
1315
  taken.add(copy.toLowerCase());
1230
1316
  importWrites[copy] = text3;
1231
1317
  report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
1232
- }
1233
- }
1234
- folder.names = taken;
1235
- if (!opts.dryRun) {
1236
- for (const [path, text3] of Object.entries(importWrites)) {
1237
- const full = toPath(dir, path);
1238
- await mkdir2(dirname2(full), { recursive: true });
1239
- await writeFile2(full, text3, "utf8");
1318
+ report.copies.add(copy);
1240
1319
  }
1241
1320
  }
1242
1321
  }
1322
+ folder.names = taken;
1323
+ if (!opts.dryRun) {
1324
+ checkDeadline();
1325
+ for (const [path, text3] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text3);
1326
+ }
1243
1327
  const found = await people(run3, ws, shared, report, hint);
1244
1328
  for (const path of found.leftOut) {
1245
1329
  delete found.files[path];
@@ -1263,12 +1347,16 @@ async function syncMemory(opts) {
1263
1347
  );
1264
1348
  return 0;
1265
1349
  }
1350
+ checkDeadline();
1351
+ if (attempt === 1) await removeLeftTemps(dir, folder.temps);
1266
1352
  await applyPlan(dir, plan);
1267
1353
  let version = team.version;
1268
1354
  if (!sameManifest(plan.manifest, team.manifest)) {
1355
+ checkDeadline();
1269
1356
  await upload(client, ws, project, plan, team.manifest, report);
1270
1357
  }
1271
1358
  if (!sameManifest(plan.manifest, team.manifest)) {
1359
+ checkDeadline();
1272
1360
  try {
1273
1361
  version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
1274
1362
  } catch (e) {
@@ -1280,6 +1368,7 @@ async function syncMemory(opts) {
1280
1368
  false
1281
1369
  )
1282
1370
  );
1371
+ opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
1283
1372
  return 2;
1284
1373
  }
1285
1374
  base = team;
@@ -1304,18 +1393,197 @@ async function syncMemory(opts) {
1304
1393
  false
1305
1394
  )
1306
1395
  );
1396
+ opts.summary?.(report.summary("synced", version));
1307
1397
  return 0;
1308
1398
  }
1309
1399
  } catch (e) {
1310
1400
  out(e.message);
1401
+ opts.summary?.({
1402
+ outcome: "failed",
1403
+ reason: failureReason(e),
1404
+ version: 0,
1405
+ pulled: 0,
1406
+ pushed: 0,
1407
+ deleted: 0,
1408
+ conflictCopies: [],
1409
+ waiting: []
1410
+ });
1311
1411
  return 1;
1312
1412
  }
1313
1413
  }
1414
+ function hookLines(ws, project, folder, s) {
1415
+ if (s.outcome !== "synced") {
1416
+ return [`Team memory ${ws}/${project}: not synced (${s.reason ?? "it did not finish"}); your files are unchanged and the next session tries again.`];
1417
+ }
1418
+ const lines = [`Team memory ${ws}/${project}: version ${s.version}, ${s.pulled} pulled, ${s.pushed} pushed, ${s.deleted} deleted.`];
1419
+ if (s.conflictCopies.length) {
1420
+ lines.push(` ${plural(s.conflictCopies.length, "conflict copy", "conflict copies")} to tidy in ${folder}: ${s.conflictCopies.join(", ")}`);
1421
+ }
1422
+ if (s.waiting.length) {
1423
+ lines.push(
1424
+ ` ${plural(s.waiting.length, "file waits", "files wait")} for you: run \`${SYNC_MEMORY_COMMAND} --project ${project}\` in your terminal.`
1425
+ );
1426
+ }
1427
+ return lines;
1428
+ }
1429
+ async function appendLog(ws, lines) {
1430
+ const path = syncLogPath(ws);
1431
+ await mkdir2(dirname2(path), { recursive: true });
1432
+ try {
1433
+ if ((await stat(path)).size > HOOK_LOG_MAX_BYTES) await writeFile2(path, "", "utf8");
1434
+ } catch {
1435
+ }
1436
+ await appendFile(path, lines.join("\n") + "\n", "utf8");
1437
+ }
1438
+ async function takeLock(lock, staleMs, now) {
1439
+ await mkdir2(dirname2(lock), { recursive: true });
1440
+ const body = JSON.stringify({ pid: process.pid, at: now.toISOString() }) + "\n";
1441
+ for (let attempt = 0; attempt < 2; attempt++) {
1442
+ try {
1443
+ const handle = await open(lock, "wx");
1444
+ try {
1445
+ await handle.writeFile(body, "utf8");
1446
+ } catch (err) {
1447
+ await handle.close();
1448
+ await rm2(lock, { force: true });
1449
+ throw err;
1450
+ }
1451
+ await handle.close();
1452
+ return true;
1453
+ } catch (e) {
1454
+ if (e.code !== "EEXIST") throw e;
1455
+ let seen;
1456
+ try {
1457
+ seen = await stat(lock);
1458
+ } catch {
1459
+ continue;
1460
+ }
1461
+ if (Date.now() - seen.mtimeMs < staleMs || attempt === 1) return false;
1462
+ try {
1463
+ const now2 = await stat(lock);
1464
+ if (now2.ino !== seen.ino || now2.mtimeMs !== seen.mtimeMs) return false;
1465
+ } catch {
1466
+ continue;
1467
+ }
1468
+ await rm2(lock, { force: true });
1469
+ }
1470
+ }
1471
+ return false;
1472
+ }
1473
+ async function releaseLock(lock) {
1474
+ try {
1475
+ const held = JSON.parse(await readFile2(lock, "utf8"));
1476
+ if (held.pid === process.pid) await rm2(lock, { force: true });
1477
+ } catch {
1478
+ }
1479
+ }
1480
+ async function runHookSync(workspace, io, env = process.env) {
1481
+ try {
1482
+ return await hookSync(workspace, io, env);
1483
+ } catch (e) {
1484
+ try {
1485
+ io.err(`sync-memory --hook: ${e.message}`);
1486
+ } catch {
1487
+ }
1488
+ return 0;
1489
+ }
1490
+ }
1491
+ async function hookSync(workspace, io, env) {
1492
+ if (!isWorkspace(workspace)) {
1493
+ io.err(`sync-memory --hook: ${JSON.stringify(workspace)} is not a workspace name`);
1494
+ return 0;
1495
+ }
1496
+ const now = io.now?.() ?? /* @__PURE__ */ new Date();
1497
+ const deadline = (io.startedAt ?? Date.now()) + (io.budgetMs ?? HOOK_BUDGET_MS);
1498
+ let client = io.client;
1499
+ if (!client) {
1500
+ let apiKey;
1501
+ let apiUrl;
1502
+ try {
1503
+ apiKey = await apiKeyFor(workspace, env);
1504
+ apiUrl = apiUrlFrom(env);
1505
+ } catch {
1506
+ return 0;
1507
+ }
1508
+ if (!apiKey) return 0;
1509
+ client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
1510
+ }
1511
+ let projects;
1512
+ try {
1513
+ projects = Object.keys((await readState(workspace)).memory ?? {}).sort();
1514
+ } catch {
1515
+ return 0;
1516
+ }
1517
+ if (!projects.length) return 0;
1518
+ const lock = syncLockPath(workspace);
1519
+ if (!await takeLock(lock, io.lockStaleMs ?? HOOK_LOCK_STALE_MS, now)) {
1520
+ io.out(`Team memory ${workspace}: a sync is already running.`);
1521
+ return 0;
1522
+ }
1523
+ try {
1524
+ for (const project of projects) {
1525
+ const folder = join2(workspaceDir(workspace), "memory", project);
1526
+ const report = [];
1527
+ let summary;
1528
+ const left = deadline - Date.now();
1529
+ if (left > 0) {
1530
+ let timer;
1531
+ const timeout = new Promise((resolveTimeout) => {
1532
+ timer = setTimeout(() => resolveTimeout("timeout"), left);
1533
+ });
1534
+ const run3 = syncMemory({
1535
+ project,
1536
+ dryRun: false,
1537
+ client,
1538
+ workspace,
1539
+ deadline,
1540
+ unattended: true,
1541
+ out: (...lines2) => report.push(...lines2),
1542
+ summary: (s) => summary = s,
1543
+ now: io.now
1544
+ });
1545
+ const won = await Promise.race([run3.then(() => "done"), timeout]);
1546
+ if (timer) clearTimeout(timer);
1547
+ if (won === "timeout") summary = void 0;
1548
+ }
1549
+ const result = summary ?? {
1550
+ outcome: "failed",
1551
+ reason: "took too long",
1552
+ version: 0,
1553
+ pulled: 0,
1554
+ pushed: 0,
1555
+ deleted: 0,
1556
+ conflictCopies: [],
1557
+ waiting: []
1558
+ };
1559
+ const lines = hookLines(workspace, project, folder, result);
1560
+ lines.forEach((l) => io.out(l));
1561
+ try {
1562
+ await appendLog(workspace, [`--- ${now.toISOString()} ${workspace}/${project}`, ...lines, ...report]);
1563
+ } catch (e) {
1564
+ io.err(`sync-memory --hook: the log could not be written: ${e.message}`);
1565
+ }
1566
+ }
1567
+ } finally {
1568
+ await releaseLock(lock);
1569
+ }
1570
+ return 0;
1571
+ }
1314
1572
  async function runSyncMemory(argv, io, env = process.env) {
1315
1573
  let args2;
1316
1574
  let client = io.client;
1317
1575
  try {
1318
1576
  args2 = parseSyncMemoryArgs(argv);
1577
+ } catch (e) {
1578
+ if (argv.includes("--hook")) {
1579
+ io.err(`sync-memory --hook: ${e.message.split("\n")[0]}`);
1580
+ return 0;
1581
+ }
1582
+ io.err(e.message);
1583
+ return 1;
1584
+ }
1585
+ if (args2.hook) return runHookSync(args2.hook, io, env);
1586
+ try {
1319
1587
  if (!client) {
1320
1588
  const apiUrl = apiUrlFrom(env);
1321
1589
  const apiKey = await localApiKey(env);
@@ -1327,7 +1595,7 @@ async function runSyncMemory(argv, io, env = process.env) {
1327
1595
  }
1328
1596
  return syncMemory({ ...args2, client, out: (...lines) => lines.forEach((l) => io.out(l)), ask: io.ask, now: io.now });
1329
1597
  }
1330
- var SYNC_MEMORY_USAGE, SYNC_MEMORY_COMMAND, SyncFailed, toPath, Report, ANSWER_WORDS, sameManifest;
1598
+ var SYNC_MEMORY_USAGE, HOOK_BUDGET_MS, HOOK_LOCK_STALE_MS, HOOK_LOG_MAX_BYTES, SYNC_MEMORY_COMMAND, SyncFailed, SyncStopped, toPath, LEFT_TEMP_RE, Report, ANSWER_WORDS, sameManifest, plural;
1331
1599
  var init_sync_memory = __esm({
1332
1600
  "src/sync-memory.ts"() {
1333
1601
  "use strict";
@@ -1335,18 +1603,33 @@ var init_sync_memory = __esm({
1335
1603
  init_memory_sync();
1336
1604
  init_person_values();
1337
1605
  init_team_setup_files();
1338
- SYNC_MEMORY_USAGE = "Usage: systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>] [--import <folder>] [--dry-run]";
1606
+ SYNC_MEMORY_USAGE = "Usage: systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>] [--import <folder>]... [--dry-run]\n systemdox-mcp-server sync-memory --hook <workspace>";
1607
+ HOOK_BUDGET_MS = 2e4;
1608
+ HOOK_LOCK_STALE_MS = 6e4;
1609
+ HOOK_LOG_MAX_BYTES = 1024 * 1024;
1339
1610
  SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
1340
1611
  SyncFailed = class extends Error {
1341
1612
  };
1613
+ SyncStopped = class extends Error {
1614
+ constructor(reason, message) {
1615
+ super(message);
1616
+ this.reason = reason;
1617
+ }
1618
+ reason;
1619
+ };
1342
1620
  toPath = (root, rel) => join2(root, ...rel.split("/"));
1621
+ LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
1343
1622
  Report = class {
1344
1623
  pulled = /* @__PURE__ */ new Set();
1345
1624
  pushed = /* @__PURE__ */ new Set();
1346
1625
  deletedHere = /* @__PURE__ */ new Set();
1347
1626
  deletedFromTeam = /* @__PURE__ */ new Set();
1348
1627
  conflicts = [];
1628
+ /** The conflict copies written this run, by name. */
1629
+ copies = /* @__PURE__ */ new Set();
1349
1630
  skipped = /* @__PURE__ */ new Map();
1631
+ /** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
1632
+ waiting = /* @__PURE__ */ new Set();
1350
1633
  people = [];
1351
1634
  toAnswer = /* @__PURE__ */ new Map();
1352
1635
  add(plan) {
@@ -1354,12 +1637,32 @@ var init_sync_memory = __esm({
1354
1637
  plan.pushed.forEach((p) => this.pushed.add(p));
1355
1638
  plan.deletedHere.forEach((p) => this.deletedHere.add(p));
1356
1639
  plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
1357
- for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
1640
+ for (const c of plan.conflicts) {
1641
+ this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
1642
+ this.copies.add(c.copy);
1643
+ }
1358
1644
  for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
1359
1645
  }
1360
1646
  skip(path, reason) {
1361
1647
  if (!this.skipped.has(path)) this.skipped.set(path, reason);
1362
1648
  }
1649
+ /** Skipped, and waiting for the member at a terminal. */
1650
+ wait(path, reason) {
1651
+ this.skip(path, reason);
1652
+ this.waiting.add(path);
1653
+ }
1654
+ summary(outcome, version, reason) {
1655
+ return {
1656
+ outcome,
1657
+ ...reason ? { reason } : {},
1658
+ version,
1659
+ pulled: this.pulled.size,
1660
+ pushed: this.pushed.size,
1661
+ deleted: this.deletedHere.size + this.deletedFromTeam.size,
1662
+ conflictCopies: [...this.copies].sort(),
1663
+ waiting: [...this.waiting].sort()
1664
+ };
1665
+ }
1363
1666
  lines(summary, dryRun) {
1364
1667
  const out = [summary];
1365
1668
  const section = (title, items) => {
@@ -1398,6 +1701,7 @@ var init_sync_memory = __esm({
1398
1701
  ours: "ours, not a person"
1399
1702
  };
1400
1703
  sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
1704
+ plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
1401
1705
  }
1402
1706
  });
1403
1707
 
@@ -1407,7 +1711,11 @@ __export(setup_team_exports, {
1407
1711
  HEADERS_HELPER: () => HEADERS_HELPER,
1408
1712
  SETUP_USAGE: () => SETUP_USAGE,
1409
1713
  claudeCodeVersion: () => claudeCodeVersion,
1714
+ claudeMemoryFolder: () => claudeMemoryFolder,
1715
+ countMemoryFiles: () => countMemoryFiles,
1716
+ gitTopFolder: () => gitTopFolder,
1410
1717
  importBlock: () => importBlock,
1718
+ memoryImports: () => memoryImports,
1411
1719
  parseSetupArgs: () => parseSetupArgs,
1412
1720
  runTeamSetup: () => runTeamSetup,
1413
1721
  runTeamSetupCommand: () => runTeamSetupCommand,
@@ -1422,19 +1730,20 @@ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, writeFile
1422
1730
  import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
1423
1731
  import { promisify as promisify2 } from "util";
1424
1732
  function parseSetupArgs(argv) {
1425
- const args2 = { yes: false, overwrite: false };
1733
+ const args2 = { yes: false, overwrite: false, importDirs: [] };
1426
1734
  for (let i = 0; i < argv.length; i++) {
1427
1735
  const a = argv[i];
1428
1736
  if (a === "--yes") args2.yes = true;
1429
1737
  else if (a === "--overwrite") args2.overwrite = true;
1430
- else if (a === "--project" || a === "--dir") {
1738
+ else if (a === "--project" || a === "--dir" || a === "--import") {
1431
1739
  const value = argv[++i];
1432
1740
  if (!value || value.startsWith("--")) throw new Error(`${a} needs a value
1433
1741
  ${SETUP_USAGE}`);
1434
1742
  if (a === "--project") {
1435
1743
  if (!isWorkspace(value)) throw new Error(`${value} is not a project name (lower case letters, digits and -)`);
1436
1744
  args2.project = value;
1437
- } else args2.dir = value;
1745
+ } else if (a === "--dir") args2.dir = value;
1746
+ else args2.importDirs.push(value);
1438
1747
  } else throw new Error(`Unexpected argument ${a}
1439
1748
  ${SETUP_USAGE}`);
1440
1749
  }
@@ -1452,6 +1761,60 @@ async function claudeCodeVersion() {
1452
1761
  return void 0;
1453
1762
  }
1454
1763
  }
1764
+ async function gitTopFolder(dir) {
1765
+ try {
1766
+ const { stdout } = await run2("git", ["-C", dir, "rev-parse", "--show-toplevel"], { timeout: 1e4, windowsHide: true });
1767
+ const top = stdout.trim();
1768
+ return top ? resolve2(top) : void 0;
1769
+ } catch {
1770
+ return void 0;
1771
+ }
1772
+ }
1773
+ function claudeMemoryFolder(path) {
1774
+ return join3(homeDir(), ".claude", "projects", path.replace(/[^A-Za-z0-9]/g, "-"), "memory");
1775
+ }
1776
+ async function countMemoryFiles(folder) {
1777
+ let count = 0;
1778
+ async function walk2(dir, prefix) {
1779
+ let entries2;
1780
+ try {
1781
+ entries2 = await readdir2(dir, { withFileTypes: true });
1782
+ } catch {
1783
+ return;
1784
+ }
1785
+ for (const e of entries2) {
1786
+ if (e.name.startsWith(".")) continue;
1787
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
1788
+ if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
1789
+ else if (e.isFile() && e.name.endsWith(".md")) {
1790
+ try {
1791
+ if (isShareable(rel, await readFile3(join3(dir, e.name), "utf8"))) count++;
1792
+ } catch {
1793
+ }
1794
+ }
1795
+ }
1796
+ }
1797
+ await walk2(folder, "");
1798
+ return count;
1799
+ }
1800
+ async function memoryImports(checkout, teamFolder, named, gitTop = gitTopFolder) {
1801
+ const candidates = [resolve2(checkout)];
1802
+ const top = await gitTop(checkout);
1803
+ if (top && !candidates.includes(top)) candidates.push(top);
1804
+ const out = [];
1805
+ for (const path of candidates) {
1806
+ const folder = claudeMemoryFolder(path);
1807
+ if (resolve2(folder) === resolve2(teamFolder)) continue;
1808
+ const files = await countMemoryFiles(folder);
1809
+ if (files) out.push({ folder, files, named: false });
1810
+ }
1811
+ for (const dir of named) {
1812
+ const folder = resolve2(dir);
1813
+ if (out.some((i) => i.folder === folder)) continue;
1814
+ out.push({ folder, files: await countMemoryFiles(folder), named: true });
1815
+ }
1816
+ return out;
1817
+ }
1455
1818
  function parseVersion(text3) {
1456
1819
  const m = text3?.match(/(\d+)\.(\d+)\.(\d+)/);
1457
1820
  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : void 0;
@@ -1493,9 +1856,9 @@ function skipValue(t, i) {
1493
1856
  while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
1494
1857
  return i;
1495
1858
  }
1496
- function objectMembers(t, open) {
1859
+ function objectMembers(t, open2) {
1497
1860
  const members = [];
1498
- let i = skipSpace(t, open + 1);
1861
+ let i = skipSpace(t, open2 + 1);
1499
1862
  if (t[i] === "}") return { members, close: i };
1500
1863
  for (; ; ) {
1501
1864
  i = skipSpace(t, i);
@@ -1522,13 +1885,13 @@ function setJsonMember(text3, keys, value) {
1522
1885
  const eol = text3.includes("\r\n") ? "\r\n" : "\n";
1523
1886
  const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
1524
1887
  const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
1525
- let open = skipSpace(text3, 0);
1888
+ let open2 = skipSpace(text3, 0);
1526
1889
  for (let k = 0; k < keys.length; k++) {
1527
- const { members, close } = objectMembers(text3, open);
1890
+ const { members, close } = objectMembers(text3, open2);
1528
1891
  const found = members.filter((m) => m.key === keys[k]).at(-1);
1529
1892
  if (found) {
1530
1893
  if (k < keys.length - 1 && text3[found.valueStart] === "{") {
1531
- open = found.valueStart;
1894
+ open2 = found.valueStart;
1532
1895
  continue;
1533
1896
  }
1534
1897
  return text3.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text3, found.keyStart)) + text3.slice(found.valueEnd);
@@ -1537,16 +1900,16 @@ function setJsonMember(text3, keys, value) {
1537
1900
  if (compact) {
1538
1901
  const member = `${name}:${render(nest(k + 1), "")}`;
1539
1902
  if (members.length) return text3.slice(0, members.at(-1).valueEnd) + "," + member + text3.slice(members.at(-1).valueEnd);
1540
- return text3.slice(0, open + 1) + member + text3.slice(close);
1903
+ return text3.slice(0, open2 + 1) + member + text3.slice(close);
1541
1904
  }
1542
1905
  if (members.length) {
1543
1906
  const last = members.at(-1);
1544
1907
  const indent2 = lineIndent(text3, last.keyStart);
1545
1908
  return text3.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text3.slice(last.valueEnd);
1546
1909
  }
1547
- const outer = lineIndent(text3, open);
1910
+ const outer = lineIndent(text3, open2);
1548
1911
  const indent = outer + unit;
1549
- return text3.slice(0, open + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text3.slice(close);
1912
+ return text3.slice(0, open2 + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text3.slice(close);
1550
1913
  }
1551
1914
  return text3;
1552
1915
  }
@@ -1592,9 +1955,9 @@ function importBlock(workspace, eol = "\n") {
1592
1955
  function withImportBlock(text3, workspace) {
1593
1956
  const eol = text3.includes("\r\n") ? "\r\n" : "\n";
1594
1957
  const block = importBlock(workspace, eol);
1595
- const open = `<!-- systemdox:${workspace} -->`;
1958
+ const open2 = `<!-- systemdox:${workspace} -->`;
1596
1959
  const close = `<!-- /systemdox:${workspace} -->`;
1597
- const start = text3.indexOf(open);
1960
+ const start = text3.indexOf(open2);
1598
1961
  const end = start >= 0 ? text3.indexOf(close, start) : -1;
1599
1962
  if (start >= 0 && end >= 0) return text3.slice(0, start) + block + text3.slice(end + close.length);
1600
1963
  if (!text3) return block + eol;
@@ -1657,12 +2020,12 @@ async function runTeamSetup(opts) {
1657
2020
  const plugin = own.plugin;
1658
2021
  let previousPlugin;
1659
2022
  if (plugin) {
1660
- const hooks = hookLines(plugin);
2023
+ const hooks = hookLines2(plugin);
1661
2024
  const hooksName = `the hook commands of ${ws}-setup`;
1662
2025
  const hooksItem = item(hooksName, hooks);
1663
2026
  if (update && hooks.length && !accepted.has(hooksItem.sha256)) {
1664
2027
  const versions = (await client.getPlugin(ws)).versions.filter((v) => v.version !== plugin.version);
1665
- previousPlugin = versions.sort((a, b) => b.version - a.version).find((v) => accepted.has(item(hooksName, hookLines(v)).sha256));
2028
+ previousPlugin = versions.sort((a, b) => b.version - a.version).find((v) => accepted.has(item(hooksName, hookLines2(v)).sha256));
1666
2029
  }
1667
2030
  items.push(item(`the plugin ${ws}-setup, version ${plugin.version}`, [`${ws}-setup, version ${plugin.version}`]));
1668
2031
  if (hooks.length) {
@@ -1670,7 +2033,7 @@ async function runTeamSetup(opts) {
1670
2033
  item(
1671
2034
  hooksName,
1672
2035
  hooks,
1673
- previousPlugin ? lineDiff(hookLines(previousPlugin).join("\n"), hooks.join("\n")) : void 0
2036
+ previousPlugin ? lineDiff(hookLines2(previousPlugin).join("\n"), hooks.join("\n")) : void 0
1674
2037
  )
1675
2038
  );
1676
2039
  }
@@ -1710,6 +2073,15 @@ async function runTeamSetup(opts) {
1710
2073
  }
1711
2074
  items.push(item(`autoMemoryDirectory in ${localSettings}`, [memoryFolder]));
1712
2075
  }
2076
+ const imports = project && memoryFolder && !state.memory?.[project] ? await memoryImports(checkout, memoryFolder, opts.importDirs ?? [], opts.gitTop) : [];
2077
+ for (const i of imports) {
2078
+ items.push(
2079
+ item(
2080
+ `${i.named ? "the folder --import names" : "the memories Claude Code kept for this checkout"}, imported into ${project} on its first sync`,
2081
+ [`${i.folder}: ${i.files} file${i.files === 1 ? "" : "s"} to share`]
2082
+ )
2083
+ );
2084
+ }
1713
2085
  const pending = items.filter((i) => !accepted.has(i.sha256));
1714
2086
  if (pending.length) {
1715
2087
  if (!canAsk) {
@@ -1779,6 +2151,7 @@ async function runTeamSetup(opts) {
1779
2151
  const code = await syncMemory({
1780
2152
  project,
1781
2153
  dryRun: false,
2154
+ importDirs: imports.map((i) => i.folder),
1782
2155
  client,
1783
2156
  out: (...lines) => lines.forEach((l) => out(l)),
1784
2157
  ask: opts.terminal ? opts.ask : void 0,
@@ -1819,7 +2192,7 @@ async function runTeamSetupCommand(args2, key, io) {
1819
2192
  const client = new SystemDoxApiClient({ apiKey: key.apiKey, apiUrl: key.apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
1820
2193
  return runTeamSetup({ ...args2, client, apiKey: key.apiKey, keySource: "prompt", ...io });
1821
2194
  }
1822
- var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item, hookLines, mcpLines;
2195
+ var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item, hookLines2, mcpLines;
1823
2196
  var init_setup_team = __esm({
1824
2197
  "src/setup-team.ts"() {
1825
2198
  "use strict";
@@ -1827,7 +2200,7 @@ var init_setup_team = __esm({
1827
2200
  init_memory_sync();
1828
2201
  init_team_setup_files();
1829
2202
  init_sync_memory();
1830
- SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]";
2203
+ SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]";
1831
2204
  HEADERS_HELPER = "npx -y @systemdox/mcp-server marketplace-auth";
1832
2205
  SetupFailed = class extends Error {
1833
2206
  };
@@ -1839,7 +2212,7 @@ var init_setup_team = __esm({
1839
2212
  sha256: sha2562(`${name}
1840
2213
  ${lines.join("\n")}`)
1841
2214
  });
1842
- hookLines = (p) => p.hookCommands ?? [];
2215
+ hookLines2 = (p) => p.hookCommands ?? [];
1843
2216
  mcpLines = (p) => (p.mcpServers ?? []).map((s) => `${s.name}: ${s.url ?? [s.command, ...s.args ?? []].filter(Boolean).join(" ")}`);
1844
2217
  }
1845
2218
  });
@@ -2326,7 +2699,7 @@ __export(plugin_path_exports, {
2326
2699
  });
2327
2700
  import { createHash as createHash4, randomBytes } from "crypto";
2328
2701
  import { existsSync as existsSync5 } from "fs";
2329
- import { mkdir as mkdir6, rename as rename2, writeFile as writeFile5 } from "fs/promises";
2702
+ import { mkdir as mkdir6, rename as rename3, writeFile as writeFile5 } from "fs/promises";
2330
2703
  import { dirname as dirname5, join as join6, resolve as resolve5, sep as sep2 } from "path";
2331
2704
  import { inflateRawSync } from "zlib";
2332
2705
  function parsePluginPathArgs(argv) {
@@ -2494,7 +2867,7 @@ async function runPluginPath(argv, io) {
2494
2867
  } else {
2495
2868
  if (existsSync5(folder)) await removeFolder(folder);
2496
2869
  try {
2497
- await rename2(tmp, folder);
2870
+ await rename3(tmp, folder);
2498
2871
  } catch (e) {
2499
2872
  if (!existsSync5(folder)) throw e;
2500
2873
  await removeFolder(tmp);
@@ -3315,8 +3688,8 @@ function registerGetPlanBoard(server, apiClient2) {
3315
3688
  ]
3316
3689
  };
3317
3690
  }
3318
- const open = boards.filter((b) => !b.closed);
3319
- if (open.length === 0) {
3691
+ const open2 = boards.filter((b) => !b.closed);
3692
+ if (open2.length === 0) {
3320
3693
  return {
3321
3694
  content: [
3322
3695
  {
@@ -3326,16 +3699,16 @@ function registerGetPlanBoard(server, apiClient2) {
3326
3699
  ]
3327
3700
  };
3328
3701
  }
3329
- const closedNote = boards.length > open.length ? `
3702
+ const closedNote = boards.length > open2.length ? `
3330
3703
 
3331
- _${boards.length - open.length} closed board(s) not listed._` : "";
3704
+ _${boards.length - open2.length} closed board(s) not listed._` : "";
3332
3705
  return {
3333
3706
  content: [
3334
3707
  {
3335
3708
  type: "text",
3336
- text: `${open.length} open project board(s):
3709
+ text: `${open2.length} open project board(s):
3337
3710
 
3338
- ${formatBoards(open)}` + closedNote + `
3711
+ ${formatBoards(open2)}` + closedNote + `
3339
3712
 
3340
3713
  Call get_plan_board with project_number to list a board's issues.`
3341
3714
  }
@@ -5295,7 +5668,9 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
5295
5668
  project: args2.project,
5296
5669
  dryRun: !!args2.dryRun,
5297
5670
  client: apiClient2,
5298
- out: (...l) => lines.push(...l)
5671
+ out: (...l) => lines.push(...l),
5672
+ // Nobody is at a terminal: a project the team no longer serves is left as it is.
5673
+ unattended: true
5299
5674
  });
5300
5675
  return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
5301
5676
  }
@@ -5383,7 +5758,7 @@ var init_server = __esm({
5383
5758
  init_create_prompt_template();
5384
5759
  init_skills2();
5385
5760
  init_team_setup();
5386
- SERVER_VERSION = true ? "0.1.56" : "0.0.0-dev";
5761
+ SERVER_VERSION = true ? "0.1.58" : "0.0.0-dev";
5387
5762
  READ_TOOLS = [
5388
5763
  "get_context",
5389
5764
  "get_checks",
@@ -5512,21 +5887,26 @@ if (command === "setup") {
5512
5887
  ({ runPluginPath: runPluginPath2 }) => runPluginPath2(args.slice(1), { out: console.log, err: console.error })
5513
5888
  ).then((code) => process.exit(code));
5514
5889
  } else if (command === "sync-memory") {
5515
- const terminal = !!process.stdin.isTTY && !!process.stdout.isTTY;
5890
+ const hook = args.includes("--hook");
5891
+ const terminal = !hook && !!process.stdin.isTTY && !!process.stdout.isTTY;
5516
5892
  Promise.resolve().then(() => (init_sync_memory(), sync_memory_exports)).then(
5517
5893
  ({ runSyncMemory: runSyncMemory2 }) => runSyncMemory2(args.slice(1), {
5518
5894
  out: console.log,
5519
5895
  err: console.error,
5520
- ask: terminal ? (question) => prompt(question) : void 0
5896
+ ask: terminal ? (question) => prompt(question) : void 0,
5897
+ startedAt: performance.timeOrigin
5521
5898
  })
5522
- ).then((code) => process.exit(code));
5899
+ ).then((code) => process.exit(code)).catch((err) => {
5900
+ console.error(` Error: ${err.message}`);
5901
+ process.exit(hook ? 0 : 1);
5902
+ });
5523
5903
  } else if (command === "--help" || command === "-h") {
5524
5904
  console.log(`
5525
5905
  SystemDox MCP Server
5526
5906
 
5527
5907
  Usage:
5528
5908
  systemdox-mcp-server Start the MCP server (stdio transport)
5529
- systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]
5909
+ systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]
5530
5910
  Configure the MCP server in your AI coding tool,
5531
5911
  then your team's setup: the team instructions,
5532
5912
  the plugin's marketplace and the project's team
@@ -5543,10 +5923,15 @@ if (command === "setup") {
5543
5923
  Print the header for your team's marketplace
5544
5924
  (Claude Code runs it)
5545
5925
  systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>]
5546
- [--import <folder>] [--dry-run]
5926
+ [--import <folder>]... [--dry-run]
5547
5927
  Sync a project's team memory with SystemDox
5548
5928
  (default folder:
5549
5929
  ~/.claude/systemdox/<workspace>/memory/<project>)
5930
+ systemdox-mcp-server sync-memory --hook <workspace>
5931
+ The run the team plugin makes at the start and
5932
+ the end of a session: the memories this computer
5933
+ synced before, one line each, never a question,
5934
+ never a held-up session (Claude Code runs it)
5550
5935
  systemdox-mcp-server --help Show this help message
5551
5936
 
5552
5937
  Environment variables: