@systemdox/mcp-server 0.1.57 → 0.1.59

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,70 @@ 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,
887
+ HOOK_WAITING_NAMED: () => HOOK_WAITING_NAMED,
852
888
  SYNC_MEMORY_COMMAND: () => SYNC_MEMORY_COMMAND,
853
889
  SYNC_MEMORY_USAGE: () => SYNC_MEMORY_USAGE,
890
+ hookLines: () => hookLines,
854
891
  parseSyncMemoryArgs: () => parseSyncMemoryArgs,
892
+ runHookSync: () => runHookSync,
855
893
  runSyncMemory: () => runSyncMemory,
856
894
  syncMemory: () => syncMemory
857
895
  });
858
896
  import { existsSync } from "fs";
859
- import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
897
+ import { appendFile, mkdir as mkdir2, open, readdir, readFile as readFile2, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
860
898
  import { dirname as dirname2, join as join2, resolve } from "path";
899
+ function failureReason(e) {
900
+ if (e instanceof SyncStopped) return e.reason;
901
+ const { status, code } = refusal(e);
902
+ if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
903
+ const err = e;
904
+ const net = err?.cause?.code ?? err?.code ?? "";
905
+ if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
906
+ return "SystemDox could not be reached";
907
+ }
908
+ return "it could not finish";
909
+ }
910
+ async function writeWhole(path, text3) {
911
+ await mkdir2(dirname2(path), { recursive: true });
912
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
913
+ try {
914
+ await writeFile2(tmp, text3, "utf8");
915
+ await rename2(tmp, path);
916
+ } catch (e) {
917
+ await rm2(tmp, { force: true });
918
+ throw e;
919
+ }
920
+ }
861
921
  function refusal(e) {
862
922
  if (!(e instanceof Error)) return {};
863
923
  const { status, code } = e;
864
924
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
865
925
  }
866
926
  function parseSyncMemoryArgs(argv) {
867
- const args2 = { dryRun: false };
927
+ const args2 = { dryRun: false, importDirs: [] };
868
928
  for (let i = 0; i < argv.length; i++) {
869
929
  const a = argv[i];
870
930
  if (a === "--dry-run") args2.dryRun = true;
871
- else if (a === "--project" || a === "--dir" || a === "--import") {
931
+ else if (a === "--project" || a === "--dir" || a === "--import" || a === "--hook") {
872
932
  const value = argv[++i];
873
933
  if (!value || value.startsWith("--")) throw new SyncFailed(`${a} needs a value
874
934
  ${SYNC_MEMORY_USAGE}`);
875
935
  if (a === "--project") {
876
936
  if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a project name (lower case letters, digits and -)`);
877
937
  args2.project = value;
938
+ } else if (a === "--hook") {
939
+ if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a workspace name (lower case letters, digits and -)`);
940
+ args2.hook = value;
878
941
  } else if (a === "--dir") args2.dir = value;
879
- else args2.importDir = value;
942
+ else args2.importDirs.push(value);
880
943
  } else throw new SyncFailed(`Unexpected argument ${a}
944
+ ${SYNC_MEMORY_USAGE}`);
945
+ }
946
+ if (args2.hook && (args2.project || args2.dir || args2.importDirs.length || args2.dryRun)) {
947
+ throw new SyncFailed(`--hook takes no other option
881
948
  ${SYNC_MEMORY_USAGE}`);
882
949
  }
883
950
  return args2;
@@ -885,6 +952,7 @@ ${SYNC_MEMORY_USAGE}`);
885
952
  async function readFolder(root) {
886
953
  const files = {};
887
954
  const names = /* @__PURE__ */ new Set();
955
+ const temps = [];
888
956
  async function walk2(dir, prefix) {
889
957
  for (const e of await readdir(dir, { withFileTypes: true })) {
890
958
  if (e.name.startsWith(".")) continue;
@@ -892,12 +960,13 @@ async function readFolder(root) {
892
960
  if (e.isDirectory()) await walk2(join2(dir, e.name), rel);
893
961
  else if (e.isFile()) {
894
962
  names.add(rel.toLowerCase());
963
+ if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
895
964
  if (e.name.endsWith(".md")) files[rel] = await readFile2(join2(dir, e.name), "utf8");
896
965
  }
897
966
  }
898
967
  }
899
968
  if (existsSync(root)) await walk2(root, "");
900
- return { files, names };
969
+ return { files, names, temps };
901
970
  }
902
971
  async function pullTeam(client, ws, project) {
903
972
  try {
@@ -1005,7 +1074,7 @@ async function people(opts, ws, files, report, hint) {
1005
1074
  for (const path of g.files) {
1006
1075
  if (leftOut.has(path)) continue;
1007
1076
  current[path] = addShareFalse(current[path]);
1008
- await writeFile2(toPath(opts.dir, path), current[path], "utf8");
1077
+ await writeWhole(toPath(opts.dir, path), current[path]);
1009
1078
  leftOut.add(path);
1010
1079
  }
1011
1080
  report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
@@ -1108,7 +1177,7 @@ async function people(opts, ws, files, report, hint) {
1108
1177
  const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
1109
1178
  if (yes) {
1110
1179
  for (const path of paths) {
1111
- await writeFile2(toPath(opts.dir, path), rewritten[path], "utf8");
1180
+ await writeWhole(toPath(opts.dir, path), rewritten[path]);
1112
1181
  current[path] = rewritten[path];
1113
1182
  }
1114
1183
  }
@@ -1123,11 +1192,11 @@ async function people(opts, ws, files, report, hint) {
1123
1192
  held.add(path);
1124
1193
  const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
1125
1194
  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");
1195
+ report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
1127
1196
  } 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`);
1197
+ report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
1129
1198
  } else {
1130
- report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
1199
+ report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
1131
1200
  }
1132
1201
  }
1133
1202
  for (const g of groups) {
@@ -1138,12 +1207,11 @@ async function people(opts, ws, files, report, hint) {
1138
1207
  }
1139
1208
  return { files: current, held, leftOut };
1140
1209
  }
1210
+ async function removeLeftTemps(dir, temps) {
1211
+ for (const rel of temps) await rm2(toPath(dir, rel), { force: true });
1212
+ }
1141
1213
  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
- }
1214
+ for (const [path, text3] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text3);
1147
1215
  for (const path of plan.deletes) await rm2(toPath(dir, path), { force: true });
1148
1216
  }
1149
1217
  async function upload(client, ws, project, plan, team, report) {
@@ -1187,8 +1255,19 @@ async function syncMemory(opts) {
1187
1255
  const { client, out } = opts;
1188
1256
  const now = opts.now?.() ?? /* @__PURE__ */ new Date();
1189
1257
  try {
1258
+ const checkDeadline = () => {
1259
+ if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
1260
+ throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
1261
+ }
1262
+ };
1190
1263
  const own = await client.getOwnTeamSetup();
1191
1264
  const ws = own.workspace;
1265
+ if (opts.workspace && ws !== opts.workspace) {
1266
+ throw new SyncStopped(
1267
+ "the key is for another workspace",
1268
+ `The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
1269
+ );
1270
+ }
1192
1271
  const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
1193
1272
  if (!project) {
1194
1273
  throw new SyncFailed(
@@ -1202,6 +1281,12 @@ async function syncMemory(opts) {
1202
1281
  const date = localDate(now);
1203
1282
  const stored = (await readState(ws)).memory?.[project];
1204
1283
  let team = await pullTeam(client, ws, project);
1284
+ if (opts.unattended && stored && !team.exists) {
1285
+ throw new SyncStopped(
1286
+ "the team memory could not be read",
1287
+ `${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
1288
+ );
1289
+ }
1205
1290
  let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
1206
1291
  const folder = await readFolder(dir);
1207
1292
  const shared = {};
@@ -1214,14 +1299,16 @@ async function syncMemory(opts) {
1214
1299
  } else shared[path] = text3;
1215
1300
  }
1216
1301
  const importWrites = {};
1217
- if (opts.importDir) {
1218
- const other = await readFolder(resolve(opts.importDir));
1219
- const taken = new Set(folder.names);
1302
+ const current = { ...folder.files };
1303
+ const taken = new Set(folder.names);
1304
+ for (const importDir of opts.importDirs ?? []) {
1305
+ const other = await readFolder(resolve(importDir));
1220
1306
  for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
1221
1307
  if (!isShareable(path, text3) || !isFilePath(path)) continue;
1222
- const here = folder.files[path];
1308
+ const here = current[path];
1223
1309
  if (here === void 0) {
1224
1310
  shared[path] = text3;
1311
+ current[path] = text3;
1225
1312
  importWrites[path] = text3;
1226
1313
  taken.add(path.toLowerCase());
1227
1314
  } else if (here !== text3) {
@@ -1229,16 +1316,14 @@ async function syncMemory(opts) {
1229
1316
  taken.add(copy.toLowerCase());
1230
1317
  importWrites[copy] = text3;
1231
1318
  report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
1319
+ report.copies.add(copy);
1232
1320
  }
1233
1321
  }
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");
1240
- }
1241
- }
1322
+ }
1323
+ folder.names = taken;
1324
+ if (!opts.dryRun) {
1325
+ checkDeadline();
1326
+ for (const [path, text3] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text3);
1242
1327
  }
1243
1328
  const found = await people(run3, ws, shared, report, hint);
1244
1329
  for (const path of found.leftOut) {
@@ -1263,12 +1348,16 @@ async function syncMemory(opts) {
1263
1348
  );
1264
1349
  return 0;
1265
1350
  }
1351
+ checkDeadline();
1352
+ if (attempt === 1) await removeLeftTemps(dir, folder.temps);
1266
1353
  await applyPlan(dir, plan);
1267
1354
  let version = team.version;
1268
1355
  if (!sameManifest(plan.manifest, team.manifest)) {
1356
+ checkDeadline();
1269
1357
  await upload(client, ws, project, plan, team.manifest, report);
1270
1358
  }
1271
1359
  if (!sameManifest(plan.manifest, team.manifest)) {
1360
+ checkDeadline();
1272
1361
  try {
1273
1362
  version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
1274
1363
  } catch (e) {
@@ -1280,6 +1369,7 @@ async function syncMemory(opts) {
1280
1369
  false
1281
1370
  )
1282
1371
  );
1372
+ opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
1283
1373
  return 2;
1284
1374
  }
1285
1375
  base = team;
@@ -1304,18 +1394,202 @@ async function syncMemory(opts) {
1304
1394
  false
1305
1395
  )
1306
1396
  );
1397
+ opts.summary?.(report.summary("synced", version));
1307
1398
  return 0;
1308
1399
  }
1309
1400
  } catch (e) {
1310
1401
  out(e.message);
1402
+ opts.summary?.({
1403
+ outcome: "failed",
1404
+ reason: failureReason(e),
1405
+ version: 0,
1406
+ pulled: 0,
1407
+ pushed: 0,
1408
+ deleted: 0,
1409
+ conflictCopies: [],
1410
+ waiting: []
1411
+ });
1311
1412
  return 1;
1312
1413
  }
1313
1414
  }
1415
+ function firstNames(names) {
1416
+ const shown = names.slice(0, HOOK_WAITING_NAMED).join(", ");
1417
+ const rest = names.length - HOOK_WAITING_NAMED;
1418
+ return rest > 0 ? `${shown} and ${rest} more` : shown;
1419
+ }
1420
+ function hookLines(ws, project, folder, s) {
1421
+ if (s.outcome !== "synced") {
1422
+ return [`Team memory ${ws}/${project}: not synced (${s.reason ?? "it did not finish"}); the next session tries again.`];
1423
+ }
1424
+ const lines = [`Team memory ${ws}/${project}: version ${s.version}, ${s.pulled} pulled, ${s.pushed} pushed, ${s.deleted} deleted.`];
1425
+ if (s.conflictCopies.length) {
1426
+ lines.push(` ${plural(s.conflictCopies.length, "conflict copy", "conflict copies")} to tidy in ${folder}: ${s.conflictCopies.join(", ")}`);
1427
+ }
1428
+ if (s.waiting.length) {
1429
+ lines.push(
1430
+ ` ${plural(s.waiting.length, "file waits", "files wait")} for you (${firstNames(s.waiting)}): run \`${SYNC_MEMORY_COMMAND} --project ${project}\` in your terminal.`
1431
+ );
1432
+ }
1433
+ return lines;
1434
+ }
1435
+ async function appendLog(ws, lines) {
1436
+ const path = syncLogPath(ws);
1437
+ await mkdir2(dirname2(path), { recursive: true });
1438
+ try {
1439
+ if ((await stat(path)).size > HOOK_LOG_MAX_BYTES) await writeFile2(path, "", "utf8");
1440
+ } catch {
1441
+ }
1442
+ await appendFile(path, lines.join("\n") + "\n", "utf8");
1443
+ }
1444
+ async function takeLock(lock, staleMs, now) {
1445
+ await mkdir2(dirname2(lock), { recursive: true });
1446
+ const body = JSON.stringify({ pid: process.pid, at: now.toISOString() }) + "\n";
1447
+ for (let attempt = 0; attempt < 2; attempt++) {
1448
+ try {
1449
+ const handle = await open(lock, "wx");
1450
+ try {
1451
+ await handle.writeFile(body, "utf8");
1452
+ } catch (err) {
1453
+ await handle.close();
1454
+ await rm2(lock, { force: true });
1455
+ throw err;
1456
+ }
1457
+ await handle.close();
1458
+ return true;
1459
+ } catch (e) {
1460
+ if (e.code !== "EEXIST") throw e;
1461
+ let seen;
1462
+ try {
1463
+ seen = await stat(lock);
1464
+ } catch {
1465
+ continue;
1466
+ }
1467
+ if (Date.now() - seen.mtimeMs < staleMs || attempt === 1) return false;
1468
+ try {
1469
+ const now2 = await stat(lock);
1470
+ if (now2.ino !== seen.ino || now2.mtimeMs !== seen.mtimeMs) return false;
1471
+ } catch {
1472
+ continue;
1473
+ }
1474
+ await rm2(lock, { force: true });
1475
+ }
1476
+ }
1477
+ return false;
1478
+ }
1479
+ async function releaseLock(lock) {
1480
+ try {
1481
+ const held = JSON.parse(await readFile2(lock, "utf8"));
1482
+ if (held.pid === process.pid) await rm2(lock, { force: true });
1483
+ } catch {
1484
+ }
1485
+ }
1486
+ async function runHookSync(workspace, io, env = process.env) {
1487
+ try {
1488
+ return await hookSync(workspace, io, env);
1489
+ } catch (e) {
1490
+ try {
1491
+ io.err(`sync-memory --hook: ${e.message}`);
1492
+ } catch {
1493
+ }
1494
+ return 0;
1495
+ }
1496
+ }
1497
+ async function hookSync(workspace, io, env) {
1498
+ if (!isWorkspace(workspace)) {
1499
+ io.err(`sync-memory --hook: ${JSON.stringify(workspace)} is not a workspace name`);
1500
+ return 0;
1501
+ }
1502
+ const now = io.now?.() ?? /* @__PURE__ */ new Date();
1503
+ const deadline = (io.startedAt ?? Date.now()) + (io.budgetMs ?? HOOK_BUDGET_MS);
1504
+ let client = io.client;
1505
+ if (!client) {
1506
+ let apiKey;
1507
+ let apiUrl;
1508
+ try {
1509
+ apiKey = await apiKeyFor(workspace, env);
1510
+ apiUrl = apiUrlFrom(env);
1511
+ } catch {
1512
+ return 0;
1513
+ }
1514
+ if (!apiKey) return 0;
1515
+ client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
1516
+ }
1517
+ let projects;
1518
+ try {
1519
+ projects = Object.keys((await readState(workspace)).memory ?? {}).sort();
1520
+ } catch {
1521
+ return 0;
1522
+ }
1523
+ if (!projects.length) return 0;
1524
+ const lock = syncLockPath(workspace);
1525
+ if (!await takeLock(lock, io.lockStaleMs ?? HOOK_LOCK_STALE_MS, now)) {
1526
+ io.out(`Team memory ${workspace}: a sync is already running.`);
1527
+ return 0;
1528
+ }
1529
+ try {
1530
+ for (const project of projects) {
1531
+ const folder = join2(workspaceDir(workspace), "memory", project);
1532
+ const report = [];
1533
+ let summary;
1534
+ const left = deadline - Date.now();
1535
+ if (left > 0) {
1536
+ let timer;
1537
+ const timeout = new Promise((resolveTimeout) => {
1538
+ timer = setTimeout(() => resolveTimeout("timeout"), left);
1539
+ });
1540
+ const run3 = syncMemory({
1541
+ project,
1542
+ dryRun: false,
1543
+ client,
1544
+ workspace,
1545
+ deadline,
1546
+ unattended: true,
1547
+ out: (...lines2) => report.push(...lines2),
1548
+ summary: (s) => summary = s,
1549
+ now: io.now
1550
+ });
1551
+ const won = await Promise.race([run3.then(() => "done"), timeout]);
1552
+ if (timer) clearTimeout(timer);
1553
+ if (won === "timeout") summary = void 0;
1554
+ }
1555
+ const result = summary ?? {
1556
+ outcome: "failed",
1557
+ reason: "took too long",
1558
+ version: 0,
1559
+ pulled: 0,
1560
+ pushed: 0,
1561
+ deleted: 0,
1562
+ conflictCopies: [],
1563
+ waiting: []
1564
+ };
1565
+ const lines = hookLines(workspace, project, folder, result);
1566
+ lines.forEach((l) => io.out(l));
1567
+ try {
1568
+ await appendLog(workspace, [`--- ${now.toISOString()} ${workspace}/${project}`, ...lines, ...report]);
1569
+ } catch (e) {
1570
+ io.err(`sync-memory --hook: the log could not be written: ${e.message}`);
1571
+ }
1572
+ }
1573
+ } finally {
1574
+ await releaseLock(lock);
1575
+ }
1576
+ return 0;
1577
+ }
1314
1578
  async function runSyncMemory(argv, io, env = process.env) {
1315
1579
  let args2;
1316
1580
  let client = io.client;
1317
1581
  try {
1318
1582
  args2 = parseSyncMemoryArgs(argv);
1583
+ } catch (e) {
1584
+ if (argv.includes("--hook")) {
1585
+ io.err(`sync-memory --hook: ${e.message.split("\n")[0]}`);
1586
+ return 0;
1587
+ }
1588
+ io.err(e.message);
1589
+ return 1;
1590
+ }
1591
+ if (args2.hook) return runHookSync(args2.hook, io, env);
1592
+ try {
1319
1593
  if (!client) {
1320
1594
  const apiUrl = apiUrlFrom(env);
1321
1595
  const apiKey = await localApiKey(env);
@@ -1327,7 +1601,7 @@ async function runSyncMemory(argv, io, env = process.env) {
1327
1601
  }
1328
1602
  return syncMemory({ ...args2, client, out: (...lines) => lines.forEach((l) => io.out(l)), ask: io.ask, now: io.now });
1329
1603
  }
1330
- var SYNC_MEMORY_USAGE, SYNC_MEMORY_COMMAND, SyncFailed, toPath, Report, ANSWER_WORDS, sameManifest;
1604
+ 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, HOOK_WAITING_NAMED;
1331
1605
  var init_sync_memory = __esm({
1332
1606
  "src/sync-memory.ts"() {
1333
1607
  "use strict";
@@ -1335,18 +1609,33 @@ var init_sync_memory = __esm({
1335
1609
  init_memory_sync();
1336
1610
  init_person_values();
1337
1611
  init_team_setup_files();
1338
- SYNC_MEMORY_USAGE = "Usage: systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>] [--import <folder>] [--dry-run]";
1612
+ 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>";
1613
+ HOOK_BUDGET_MS = 2e4;
1614
+ HOOK_LOCK_STALE_MS = 6e4;
1615
+ HOOK_LOG_MAX_BYTES = 1024 * 1024;
1339
1616
  SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
1340
1617
  SyncFailed = class extends Error {
1341
1618
  };
1619
+ SyncStopped = class extends Error {
1620
+ constructor(reason, message) {
1621
+ super(message);
1622
+ this.reason = reason;
1623
+ }
1624
+ reason;
1625
+ };
1342
1626
  toPath = (root, rel) => join2(root, ...rel.split("/"));
1627
+ LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
1343
1628
  Report = class {
1344
1629
  pulled = /* @__PURE__ */ new Set();
1345
1630
  pushed = /* @__PURE__ */ new Set();
1346
1631
  deletedHere = /* @__PURE__ */ new Set();
1347
1632
  deletedFromTeam = /* @__PURE__ */ new Set();
1348
1633
  conflicts = [];
1634
+ /** The conflict copies written this run, by name. */
1635
+ copies = /* @__PURE__ */ new Set();
1349
1636
  skipped = /* @__PURE__ */ new Map();
1637
+ /** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
1638
+ waiting = /* @__PURE__ */ new Set();
1350
1639
  people = [];
1351
1640
  toAnswer = /* @__PURE__ */ new Map();
1352
1641
  add(plan) {
@@ -1354,12 +1643,32 @@ var init_sync_memory = __esm({
1354
1643
  plan.pushed.forEach((p) => this.pushed.add(p));
1355
1644
  plan.deletedHere.forEach((p) => this.deletedHere.add(p));
1356
1645
  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}`);
1646
+ for (const c of plan.conflicts) {
1647
+ this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
1648
+ this.copies.add(c.copy);
1649
+ }
1358
1650
  for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
1359
1651
  }
1360
1652
  skip(path, reason) {
1361
1653
  if (!this.skipped.has(path)) this.skipped.set(path, reason);
1362
1654
  }
1655
+ /** Skipped, and waiting for the member at a terminal. */
1656
+ wait(path, reason) {
1657
+ this.skip(path, reason);
1658
+ this.waiting.add(path);
1659
+ }
1660
+ summary(outcome, version, reason) {
1661
+ return {
1662
+ outcome,
1663
+ ...reason ? { reason } : {},
1664
+ version,
1665
+ pulled: this.pulled.size,
1666
+ pushed: this.pushed.size,
1667
+ deleted: this.deletedHere.size + this.deletedFromTeam.size,
1668
+ conflictCopies: [...this.copies].sort(),
1669
+ waiting: [...this.waiting].sort()
1670
+ };
1671
+ }
1363
1672
  lines(summary, dryRun) {
1364
1673
  const out = [summary];
1365
1674
  const section = (title, items) => {
@@ -1398,6 +1707,8 @@ var init_sync_memory = __esm({
1398
1707
  ours: "ours, not a person"
1399
1708
  };
1400
1709
  sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
1710
+ plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
1711
+ HOOK_WAITING_NAMED = 5;
1401
1712
  }
1402
1713
  });
1403
1714
 
@@ -1407,7 +1718,11 @@ __export(setup_team_exports, {
1407
1718
  HEADERS_HELPER: () => HEADERS_HELPER,
1408
1719
  SETUP_USAGE: () => SETUP_USAGE,
1409
1720
  claudeCodeVersion: () => claudeCodeVersion,
1721
+ claudeMemoryFolder: () => claudeMemoryFolder,
1722
+ countMemoryFiles: () => countMemoryFiles,
1723
+ gitTopFolder: () => gitTopFolder,
1410
1724
  importBlock: () => importBlock,
1725
+ memoryImports: () => memoryImports,
1411
1726
  parseSetupArgs: () => parseSetupArgs,
1412
1727
  runTeamSetup: () => runTeamSetup,
1413
1728
  runTeamSetupCommand: () => runTeamSetupCommand,
@@ -1422,19 +1737,20 @@ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, writeFile
1422
1737
  import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
1423
1738
  import { promisify as promisify2 } from "util";
1424
1739
  function parseSetupArgs(argv) {
1425
- const args2 = { yes: false, overwrite: false };
1740
+ const args2 = { yes: false, overwrite: false, importDirs: [] };
1426
1741
  for (let i = 0; i < argv.length; i++) {
1427
1742
  const a = argv[i];
1428
1743
  if (a === "--yes") args2.yes = true;
1429
1744
  else if (a === "--overwrite") args2.overwrite = true;
1430
- else if (a === "--project" || a === "--dir") {
1745
+ else if (a === "--project" || a === "--dir" || a === "--import") {
1431
1746
  const value = argv[++i];
1432
1747
  if (!value || value.startsWith("--")) throw new Error(`${a} needs a value
1433
1748
  ${SETUP_USAGE}`);
1434
1749
  if (a === "--project") {
1435
1750
  if (!isWorkspace(value)) throw new Error(`${value} is not a project name (lower case letters, digits and -)`);
1436
1751
  args2.project = value;
1437
- } else args2.dir = value;
1752
+ } else if (a === "--dir") args2.dir = value;
1753
+ else args2.importDirs.push(value);
1438
1754
  } else throw new Error(`Unexpected argument ${a}
1439
1755
  ${SETUP_USAGE}`);
1440
1756
  }
@@ -1452,6 +1768,60 @@ async function claudeCodeVersion() {
1452
1768
  return void 0;
1453
1769
  }
1454
1770
  }
1771
+ async function gitTopFolder(dir) {
1772
+ try {
1773
+ const { stdout } = await run2("git", ["-C", dir, "rev-parse", "--show-toplevel"], { timeout: 1e4, windowsHide: true });
1774
+ const top = stdout.trim();
1775
+ return top ? resolve2(top) : void 0;
1776
+ } catch {
1777
+ return void 0;
1778
+ }
1779
+ }
1780
+ function claudeMemoryFolder(path) {
1781
+ return join3(homeDir(), ".claude", "projects", path.replace(/[^A-Za-z0-9]/g, "-"), "memory");
1782
+ }
1783
+ async function countMemoryFiles(folder) {
1784
+ let count = 0;
1785
+ async function walk2(dir, prefix) {
1786
+ let entries2;
1787
+ try {
1788
+ entries2 = await readdir2(dir, { withFileTypes: true });
1789
+ } catch {
1790
+ return;
1791
+ }
1792
+ for (const e of entries2) {
1793
+ if (e.name.startsWith(".")) continue;
1794
+ const rel = prefix ? `${prefix}/${e.name}` : e.name;
1795
+ if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
1796
+ else if (e.isFile() && e.name.endsWith(".md")) {
1797
+ try {
1798
+ if (isShareable(rel, await readFile3(join3(dir, e.name), "utf8"))) count++;
1799
+ } catch {
1800
+ }
1801
+ }
1802
+ }
1803
+ }
1804
+ await walk2(folder, "");
1805
+ return count;
1806
+ }
1807
+ async function memoryImports(checkout, teamFolder, named, gitTop = gitTopFolder) {
1808
+ const candidates = [resolve2(checkout)];
1809
+ const top = await gitTop(checkout);
1810
+ if (top && !candidates.includes(top)) candidates.push(top);
1811
+ const out = [];
1812
+ for (const path of candidates) {
1813
+ const folder = claudeMemoryFolder(path);
1814
+ if (resolve2(folder) === resolve2(teamFolder)) continue;
1815
+ const files = await countMemoryFiles(folder);
1816
+ if (files) out.push({ folder, files, named: false });
1817
+ }
1818
+ for (const dir of named) {
1819
+ const folder = resolve2(dir);
1820
+ if (out.some((i) => i.folder === folder)) continue;
1821
+ out.push({ folder, files: await countMemoryFiles(folder), named: true });
1822
+ }
1823
+ return out;
1824
+ }
1455
1825
  function parseVersion(text3) {
1456
1826
  const m = text3?.match(/(\d+)\.(\d+)\.(\d+)/);
1457
1827
  return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : void 0;
@@ -1493,9 +1863,9 @@ function skipValue(t, i) {
1493
1863
  while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
1494
1864
  return i;
1495
1865
  }
1496
- function objectMembers(t, open) {
1866
+ function objectMembers(t, open2) {
1497
1867
  const members = [];
1498
- let i = skipSpace(t, open + 1);
1868
+ let i = skipSpace(t, open2 + 1);
1499
1869
  if (t[i] === "}") return { members, close: i };
1500
1870
  for (; ; ) {
1501
1871
  i = skipSpace(t, i);
@@ -1522,13 +1892,13 @@ function setJsonMember(text3, keys, value) {
1522
1892
  const eol = text3.includes("\r\n") ? "\r\n" : "\n";
1523
1893
  const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
1524
1894
  const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
1525
- let open = skipSpace(text3, 0);
1895
+ let open2 = skipSpace(text3, 0);
1526
1896
  for (let k = 0; k < keys.length; k++) {
1527
- const { members, close } = objectMembers(text3, open);
1897
+ const { members, close } = objectMembers(text3, open2);
1528
1898
  const found = members.filter((m) => m.key === keys[k]).at(-1);
1529
1899
  if (found) {
1530
1900
  if (k < keys.length - 1 && text3[found.valueStart] === "{") {
1531
- open = found.valueStart;
1901
+ open2 = found.valueStart;
1532
1902
  continue;
1533
1903
  }
1534
1904
  return text3.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text3, found.keyStart)) + text3.slice(found.valueEnd);
@@ -1537,16 +1907,16 @@ function setJsonMember(text3, keys, value) {
1537
1907
  if (compact) {
1538
1908
  const member = `${name}:${render(nest(k + 1), "")}`;
1539
1909
  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);
1910
+ return text3.slice(0, open2 + 1) + member + text3.slice(close);
1541
1911
  }
1542
1912
  if (members.length) {
1543
1913
  const last = members.at(-1);
1544
1914
  const indent2 = lineIndent(text3, last.keyStart);
1545
1915
  return text3.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text3.slice(last.valueEnd);
1546
1916
  }
1547
- const outer = lineIndent(text3, open);
1917
+ const outer = lineIndent(text3, open2);
1548
1918
  const indent = outer + unit;
1549
- return text3.slice(0, open + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text3.slice(close);
1919
+ return text3.slice(0, open2 + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text3.slice(close);
1550
1920
  }
1551
1921
  return text3;
1552
1922
  }
@@ -1592,9 +1962,9 @@ function importBlock(workspace, eol = "\n") {
1592
1962
  function withImportBlock(text3, workspace) {
1593
1963
  const eol = text3.includes("\r\n") ? "\r\n" : "\n";
1594
1964
  const block = importBlock(workspace, eol);
1595
- const open = `<!-- systemdox:${workspace} -->`;
1965
+ const open2 = `<!-- systemdox:${workspace} -->`;
1596
1966
  const close = `<!-- /systemdox:${workspace} -->`;
1597
- const start = text3.indexOf(open);
1967
+ const start = text3.indexOf(open2);
1598
1968
  const end = start >= 0 ? text3.indexOf(close, start) : -1;
1599
1969
  if (start >= 0 && end >= 0) return text3.slice(0, start) + block + text3.slice(end + close.length);
1600
1970
  if (!text3) return block + eol;
@@ -1657,12 +2027,12 @@ async function runTeamSetup(opts) {
1657
2027
  const plugin = own.plugin;
1658
2028
  let previousPlugin;
1659
2029
  if (plugin) {
1660
- const hooks = hookLines(plugin);
2030
+ const hooks = hookLines2(plugin);
1661
2031
  const hooksName = `the hook commands of ${ws}-setup`;
1662
2032
  const hooksItem = item(hooksName, hooks);
1663
2033
  if (update && hooks.length && !accepted.has(hooksItem.sha256)) {
1664
2034
  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));
2035
+ previousPlugin = versions.sort((a, b) => b.version - a.version).find((v) => accepted.has(item(hooksName, hookLines2(v)).sha256));
1666
2036
  }
1667
2037
  items.push(item(`the plugin ${ws}-setup, version ${plugin.version}`, [`${ws}-setup, version ${plugin.version}`]));
1668
2038
  if (hooks.length) {
@@ -1670,7 +2040,7 @@ async function runTeamSetup(opts) {
1670
2040
  item(
1671
2041
  hooksName,
1672
2042
  hooks,
1673
- previousPlugin ? lineDiff(hookLines(previousPlugin).join("\n"), hooks.join("\n")) : void 0
2043
+ previousPlugin ? lineDiff(hookLines2(previousPlugin).join("\n"), hooks.join("\n")) : void 0
1674
2044
  )
1675
2045
  );
1676
2046
  }
@@ -1710,6 +2080,15 @@ async function runTeamSetup(opts) {
1710
2080
  }
1711
2081
  items.push(item(`autoMemoryDirectory in ${localSettings}`, [memoryFolder]));
1712
2082
  }
2083
+ const imports = project && memoryFolder && !state.memory?.[project] ? await memoryImports(checkout, memoryFolder, opts.importDirs ?? [], opts.gitTop) : [];
2084
+ for (const i of imports) {
2085
+ items.push(
2086
+ item(
2087
+ `${i.named ? "the folder --import names" : "the memories Claude Code kept for this checkout"}, imported into ${project} on its first sync`,
2088
+ [`${i.folder}: ${i.files} file${i.files === 1 ? "" : "s"} to share`]
2089
+ )
2090
+ );
2091
+ }
1713
2092
  const pending = items.filter((i) => !accepted.has(i.sha256));
1714
2093
  if (pending.length) {
1715
2094
  if (!canAsk) {
@@ -1779,6 +2158,7 @@ async function runTeamSetup(opts) {
1779
2158
  const code = await syncMemory({
1780
2159
  project,
1781
2160
  dryRun: false,
2161
+ importDirs: imports.map((i) => i.folder),
1782
2162
  client,
1783
2163
  out: (...lines) => lines.forEach((l) => out(l)),
1784
2164
  ask: opts.terminal ? opts.ask : void 0,
@@ -1819,7 +2199,7 @@ async function runTeamSetupCommand(args2, key, io) {
1819
2199
  const client = new SystemDoxApiClient({ apiKey: key.apiKey, apiUrl: key.apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
1820
2200
  return runTeamSetup({ ...args2, client, apiKey: key.apiKey, keySource: "prompt", ...io });
1821
2201
  }
1822
- var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item, hookLines, mcpLines;
2202
+ var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item, hookLines2, mcpLines;
1823
2203
  var init_setup_team = __esm({
1824
2204
  "src/setup-team.ts"() {
1825
2205
  "use strict";
@@ -1827,7 +2207,7 @@ var init_setup_team = __esm({
1827
2207
  init_memory_sync();
1828
2208
  init_team_setup_files();
1829
2209
  init_sync_memory();
1830
- SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]";
2210
+ SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]";
1831
2211
  HEADERS_HELPER = "npx -y @systemdox/mcp-server marketplace-auth";
1832
2212
  SetupFailed = class extends Error {
1833
2213
  };
@@ -1839,7 +2219,7 @@ var init_setup_team = __esm({
1839
2219
  sha256: sha2562(`${name}
1840
2220
  ${lines.join("\n")}`)
1841
2221
  });
1842
- hookLines = (p) => p.hookCommands ?? [];
2222
+ hookLines2 = (p) => p.hookCommands ?? [];
1843
2223
  mcpLines = (p) => (p.mcpServers ?? []).map((s) => `${s.name}: ${s.url ?? [s.command, ...s.args ?? []].filter(Boolean).join(" ")}`);
1844
2224
  }
1845
2225
  });
@@ -2326,7 +2706,7 @@ __export(plugin_path_exports, {
2326
2706
  });
2327
2707
  import { createHash as createHash4, randomBytes } from "crypto";
2328
2708
  import { existsSync as existsSync5 } from "fs";
2329
- import { mkdir as mkdir6, rename as rename2, writeFile as writeFile5 } from "fs/promises";
2709
+ import { mkdir as mkdir6, rename as rename3, writeFile as writeFile5 } from "fs/promises";
2330
2710
  import { dirname as dirname5, join as join6, resolve as resolve5, sep as sep2 } from "path";
2331
2711
  import { inflateRawSync } from "zlib";
2332
2712
  function parsePluginPathArgs(argv) {
@@ -2494,7 +2874,7 @@ async function runPluginPath(argv, io) {
2494
2874
  } else {
2495
2875
  if (existsSync5(folder)) await removeFolder(folder);
2496
2876
  try {
2497
- await rename2(tmp, folder);
2877
+ await rename3(tmp, folder);
2498
2878
  } catch (e) {
2499
2879
  if (!existsSync5(folder)) throw e;
2500
2880
  await removeFolder(tmp);
@@ -3315,8 +3695,8 @@ function registerGetPlanBoard(server, apiClient2) {
3315
3695
  ]
3316
3696
  };
3317
3697
  }
3318
- const open = boards.filter((b) => !b.closed);
3319
- if (open.length === 0) {
3698
+ const open2 = boards.filter((b) => !b.closed);
3699
+ if (open2.length === 0) {
3320
3700
  return {
3321
3701
  content: [
3322
3702
  {
@@ -3326,16 +3706,16 @@ function registerGetPlanBoard(server, apiClient2) {
3326
3706
  ]
3327
3707
  };
3328
3708
  }
3329
- const closedNote = boards.length > open.length ? `
3709
+ const closedNote = boards.length > open2.length ? `
3330
3710
 
3331
- _${boards.length - open.length} closed board(s) not listed._` : "";
3711
+ _${boards.length - open2.length} closed board(s) not listed._` : "";
3332
3712
  return {
3333
3713
  content: [
3334
3714
  {
3335
3715
  type: "text",
3336
- text: `${open.length} open project board(s):
3716
+ text: `${open2.length} open project board(s):
3337
3717
 
3338
- ${formatBoards(open)}` + closedNote + `
3718
+ ${formatBoards(open2)}` + closedNote + `
3339
3719
 
3340
3720
  Call get_plan_board with project_number to list a board's issues.`
3341
3721
  }
@@ -5295,7 +5675,9 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
5295
5675
  project: args2.project,
5296
5676
  dryRun: !!args2.dryRun,
5297
5677
  client: apiClient2,
5298
- out: (...l) => lines.push(...l)
5678
+ out: (...l) => lines.push(...l),
5679
+ // Nobody is at a terminal: a project the team no longer serves is left as it is.
5680
+ unattended: true
5299
5681
  });
5300
5682
  return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
5301
5683
  }
@@ -5383,7 +5765,7 @@ var init_server = __esm({
5383
5765
  init_create_prompt_template();
5384
5766
  init_skills2();
5385
5767
  init_team_setup();
5386
- SERVER_VERSION = true ? "0.1.57" : "0.0.0-dev";
5768
+ SERVER_VERSION = true ? "0.1.59" : "0.0.0-dev";
5387
5769
  READ_TOOLS = [
5388
5770
  "get_context",
5389
5771
  "get_checks",
@@ -5512,21 +5894,26 @@ if (command === "setup") {
5512
5894
  ({ runPluginPath: runPluginPath2 }) => runPluginPath2(args.slice(1), { out: console.log, err: console.error })
5513
5895
  ).then((code) => process.exit(code));
5514
5896
  } else if (command === "sync-memory") {
5515
- const terminal = !!process.stdin.isTTY && !!process.stdout.isTTY;
5897
+ const hook = args.includes("--hook");
5898
+ const terminal = !hook && !!process.stdin.isTTY && !!process.stdout.isTTY;
5516
5899
  Promise.resolve().then(() => (init_sync_memory(), sync_memory_exports)).then(
5517
5900
  ({ runSyncMemory: runSyncMemory2 }) => runSyncMemory2(args.slice(1), {
5518
5901
  out: console.log,
5519
5902
  err: console.error,
5520
- ask: terminal ? (question) => prompt(question) : void 0
5903
+ ask: terminal ? (question) => prompt(question) : void 0,
5904
+ startedAt: performance.timeOrigin
5521
5905
  })
5522
- ).then((code) => process.exit(code));
5906
+ ).then((code) => process.exit(code)).catch((err) => {
5907
+ console.error(` Error: ${err.message}`);
5908
+ process.exit(hook ? 0 : 1);
5909
+ });
5523
5910
  } else if (command === "--help" || command === "-h") {
5524
5911
  console.log(`
5525
5912
  SystemDox MCP Server
5526
5913
 
5527
5914
  Usage:
5528
5915
  systemdox-mcp-server Start the MCP server (stdio transport)
5529
- systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]
5916
+ systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]
5530
5917
  Configure the MCP server in your AI coding tool,
5531
5918
  then your team's setup: the team instructions,
5532
5919
  the plugin's marketplace and the project's team
@@ -5543,10 +5930,15 @@ if (command === "setup") {
5543
5930
  Print the header for your team's marketplace
5544
5931
  (Claude Code runs it)
5545
5932
  systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>]
5546
- [--import <folder>] [--dry-run]
5933
+ [--import <folder>]... [--dry-run]
5547
5934
  Sync a project's team memory with SystemDox
5548
5935
  (default folder:
5549
5936
  ~/.claude/systemdox/<workspace>/memory/<project>)
5937
+ systemdox-mcp-server sync-memory --hook <workspace>
5938
+ The run the team plugin makes at the start and
5939
+ the end of a session: the memories this computer
5940
+ synced before, one line each, never a question,
5941
+ never a held-up session (Claude Code runs it)
5550
5942
  systemdox-mcp-server --help Show this help message
5551
5943
 
5552
5944
  Environment variables: