@systemdox/mcp-server 0.1.57 → 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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/sync-memory.ts
4
4
  import { existsSync } from "fs";
5
- import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
5
+ import { appendFile, mkdir as mkdir2, open, readdir, readFile as readFile2, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
6
6
  import { dirname as dirname2, join as join2, resolve } from "path";
7
7
 
8
8
  // src/lib/cache.ts
@@ -349,15 +349,41 @@ function readFrontmatter(text) {
349
349
  const m = BLOCK_RE.exec(text);
350
350
  if (!m) return void 0;
351
351
  const front = { shareFalse: false };
352
- for (const line of m[1].split(/\r?\n/)) {
353
- const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
352
+ const lines = m[1].split(/\r?\n/);
353
+ if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
354
+ let topType;
355
+ let topShare;
356
+ let nestedType;
357
+ let nestedShare;
358
+ let inMetadata = false;
359
+ let nestedIndent;
360
+ for (const line of lines) {
361
+ if (!line.trim() || /^[ \t]*#/.test(line)) continue;
362
+ const indent = /^ */.exec(line)[0].length;
363
+ if (indent === 0) {
364
+ inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
365
+ nestedIndent = void 0;
366
+ }
367
+ const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
354
368
  if (!kv) continue;
355
369
  const raw = kv[2].replace(/[ \t]+#.*$/, "");
356
370
  const quoted = /^(["'])(.*)\1$/.exec(raw);
357
371
  const value = quoted ? quoted[2] : raw;
358
- if (kv[1] === "type") front.type = value.trim().toLowerCase();
359
- if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
360
- }
372
+ const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
373
+ if (indent === 0) {
374
+ if (kv[1] === "type") topType = value.trim().toLowerCase();
375
+ if (kv[1] === "share") topShare = isFalse;
376
+ continue;
377
+ }
378
+ if (!inMetadata) continue;
379
+ nestedIndent ??= indent;
380
+ if (indent !== nestedIndent) continue;
381
+ if (kv[1] === "type") nestedType = value.trim().toLowerCase();
382
+ if (kv[1] === "share") nestedShare = isFalse;
383
+ }
384
+ const type = topType ?? nestedType;
385
+ if (type !== void 0) front.type = type;
386
+ front.shareFalse = (topShare ?? nestedShare) === true;
361
387
  return front;
362
388
  }
363
389
  function isShareable(path, text) {
@@ -644,6 +670,12 @@ function workspaceDir(workspace) {
644
670
  function statePath(workspace) {
645
671
  return join(workspaceDir(workspace), "state.json");
646
672
  }
673
+ function syncLogPath(workspace) {
674
+ return join(workspaceDir(workspace), "sync-memory.log");
675
+ }
676
+ function syncLockPath(workspace) {
677
+ return join(workspaceDir(workspace), "sync.lock");
678
+ }
647
679
  async function readJson(path, empty) {
648
680
  let text;
649
681
  try {
@@ -668,6 +700,11 @@ async function readCredentials() {
668
700
  return readJson(credentialsPath(), {});
669
701
  }
670
702
  var run = promisify(execFile);
703
+ async function apiKeyFor(workspace, env = process.env) {
704
+ if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
705
+ const entry = (await readCredentials())[workspace];
706
+ return typeof entry?.apiKey === "string" && entry.apiKey ? entry.apiKey : void 0;
707
+ }
671
708
  async function localApiKey(env = process.env) {
672
709
  if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
673
710
  const keys = Object.entries(await readCredentials()).filter(
@@ -706,38 +743,79 @@ async function updateState(workspace, change) {
706
743
  }
707
744
 
708
745
  // src/sync-memory.ts
709
- var SYNC_MEMORY_USAGE = "Usage: systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>] [--import <folder>] [--dry-run]";
746
+ var 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>";
747
+ var HOOK_BUDGET_MS = 2e4;
748
+ var HOOK_LOCK_STALE_MS = 6e4;
749
+ var HOOK_LOG_MAX_BYTES = 1024 * 1024;
710
750
  var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
711
751
  var SyncFailed = class extends Error {
712
752
  };
753
+ var SyncStopped = class extends Error {
754
+ constructor(reason, message) {
755
+ super(message);
756
+ this.reason = reason;
757
+ }
758
+ reason;
759
+ };
760
+ function failureReason(e) {
761
+ if (e instanceof SyncStopped) return e.reason;
762
+ const { status, code } = refusal(e);
763
+ if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
764
+ const err = e;
765
+ const net = err?.cause?.code ?? err?.code ?? "";
766
+ if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
767
+ return "SystemDox could not be reached";
768
+ }
769
+ return "it could not finish";
770
+ }
771
+ async function writeWhole(path, text) {
772
+ await mkdir2(dirname2(path), { recursive: true });
773
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
774
+ try {
775
+ await writeFile2(tmp, text, "utf8");
776
+ await rename2(tmp, path);
777
+ } catch (e) {
778
+ await rm2(tmp, { force: true });
779
+ throw e;
780
+ }
781
+ }
713
782
  function refusal(e) {
714
783
  if (!(e instanceof Error)) return {};
715
784
  const { status, code } = e;
716
785
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
717
786
  }
718
787
  function parseSyncMemoryArgs(argv) {
719
- const args = { dryRun: false };
788
+ const args = { dryRun: false, importDirs: [] };
720
789
  for (let i = 0; i < argv.length; i++) {
721
790
  const a = argv[i];
722
791
  if (a === "--dry-run") args.dryRun = true;
723
- else if (a === "--project" || a === "--dir" || a === "--import") {
792
+ else if (a === "--project" || a === "--dir" || a === "--import" || a === "--hook") {
724
793
  const value = argv[++i];
725
794
  if (!value || value.startsWith("--")) throw new SyncFailed(`${a} needs a value
726
795
  ${SYNC_MEMORY_USAGE}`);
727
796
  if (a === "--project") {
728
797
  if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a project name (lower case letters, digits and -)`);
729
798
  args.project = value;
799
+ } else if (a === "--hook") {
800
+ if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a workspace name (lower case letters, digits and -)`);
801
+ args.hook = value;
730
802
  } else if (a === "--dir") args.dir = value;
731
- else args.importDir = value;
803
+ else args.importDirs.push(value);
732
804
  } else throw new SyncFailed(`Unexpected argument ${a}
805
+ ${SYNC_MEMORY_USAGE}`);
806
+ }
807
+ if (args.hook && (args.project || args.dir || args.importDirs.length || args.dryRun)) {
808
+ throw new SyncFailed(`--hook takes no other option
733
809
  ${SYNC_MEMORY_USAGE}`);
734
810
  }
735
811
  return args;
736
812
  }
737
813
  var toPath = (root, rel) => join2(root, ...rel.split("/"));
814
+ var LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
738
815
  async function readFolder(root) {
739
816
  const files = {};
740
817
  const names = /* @__PURE__ */ new Set();
818
+ const temps = [];
741
819
  async function walk(dir, prefix) {
742
820
  for (const e of await readdir(dir, { withFileTypes: true })) {
743
821
  if (e.name.startsWith(".")) continue;
@@ -745,12 +823,13 @@ async function readFolder(root) {
745
823
  if (e.isDirectory()) await walk(join2(dir, e.name), rel);
746
824
  else if (e.isFile()) {
747
825
  names.add(rel.toLowerCase());
826
+ if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
748
827
  if (e.name.endsWith(".md")) files[rel] = await readFile2(join2(dir, e.name), "utf8");
749
828
  }
750
829
  }
751
830
  }
752
831
  if (existsSync(root)) await walk(root, "");
753
- return { files, names };
832
+ return { files, names, temps };
754
833
  }
755
834
  async function pullTeam(client, ws, project) {
756
835
  try {
@@ -799,7 +878,11 @@ var Report = class {
799
878
  deletedHere = /* @__PURE__ */ new Set();
800
879
  deletedFromTeam = /* @__PURE__ */ new Set();
801
880
  conflicts = [];
881
+ /** The conflict copies written this run, by name. */
882
+ copies = /* @__PURE__ */ new Set();
802
883
  skipped = /* @__PURE__ */ new Map();
884
+ /** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
885
+ waiting = /* @__PURE__ */ new Set();
803
886
  people = [];
804
887
  toAnswer = /* @__PURE__ */ new Map();
805
888
  add(plan) {
@@ -807,12 +890,32 @@ var Report = class {
807
890
  plan.pushed.forEach((p) => this.pushed.add(p));
808
891
  plan.deletedHere.forEach((p) => this.deletedHere.add(p));
809
892
  plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
810
- for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
893
+ for (const c of plan.conflicts) {
894
+ this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
895
+ this.copies.add(c.copy);
896
+ }
811
897
  for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
812
898
  }
813
899
  skip(path, reason) {
814
900
  if (!this.skipped.has(path)) this.skipped.set(path, reason);
815
901
  }
902
+ /** Skipped, and waiting for the member at a terminal. */
903
+ wait(path, reason) {
904
+ this.skip(path, reason);
905
+ this.waiting.add(path);
906
+ }
907
+ summary(outcome, version, reason) {
908
+ return {
909
+ outcome,
910
+ ...reason ? { reason } : {},
911
+ version,
912
+ pulled: this.pulled.size,
913
+ pushed: this.pushed.size,
914
+ deleted: this.deletedHere.size + this.deletedFromTeam.size,
915
+ conflictCopies: [...this.copies].sort(),
916
+ waiting: [...this.waiting].sort()
917
+ };
918
+ }
816
919
  lines(summary, dryRun) {
817
920
  const out = [summary];
818
921
  const section = (title, items) => {
@@ -915,7 +1018,7 @@ async function people(opts, ws, files, report, hint) {
915
1018
  for (const path of g.files) {
916
1019
  if (leftOut.has(path)) continue;
917
1020
  current[path] = addShareFalse(current[path]);
918
- await writeFile2(toPath(opts.dir, path), current[path], "utf8");
1021
+ await writeWhole(toPath(opts.dir, path), current[path]);
919
1022
  leftOut.add(path);
920
1023
  }
921
1024
  report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
@@ -1018,7 +1121,7 @@ async function people(opts, ws, files, report, hint) {
1018
1121
  const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
1019
1122
  if (yes) {
1020
1123
  for (const path of paths) {
1021
- await writeFile2(toPath(opts.dir, path), rewritten[path], "utf8");
1124
+ await writeWhole(toPath(opts.dir, path), rewritten[path]);
1022
1125
  current[path] = rewritten[path];
1023
1126
  }
1024
1127
  }
@@ -1033,11 +1136,11 @@ async function people(opts, ws, files, report, hint) {
1033
1136
  held.add(path);
1034
1137
  const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
1035
1138
  if (keys.some((k) => vaultDown.has(k))) {
1036
- report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
1139
+ report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
1037
1140
  } else if (keys.some((k) => !answers.has(k))) {
1038
- report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
1141
+ report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
1039
1142
  } else {
1040
- report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
1143
+ report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
1041
1144
  }
1042
1145
  }
1043
1146
  for (const g of groups) {
@@ -1048,12 +1151,11 @@ async function people(opts, ws, files, report, hint) {
1048
1151
  }
1049
1152
  return { files: current, held, leftOut };
1050
1153
  }
1154
+ async function removeLeftTemps(dir, temps) {
1155
+ for (const rel of temps) await rm2(toPath(dir, rel), { force: true });
1156
+ }
1051
1157
  async function applyPlan(dir, plan) {
1052
- for (const [path, text] of Object.entries(plan.writes)) {
1053
- const full = toPath(dir, path);
1054
- await mkdir2(dirname2(full), { recursive: true });
1055
- await writeFile2(full, text, "utf8");
1056
- }
1158
+ for (const [path, text] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text);
1057
1159
  for (const path of plan.deletes) await rm2(toPath(dir, path), { force: true });
1058
1160
  }
1059
1161
  var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
@@ -1098,8 +1200,19 @@ async function syncMemory(opts) {
1098
1200
  const { client, out } = opts;
1099
1201
  const now = opts.now?.() ?? /* @__PURE__ */ new Date();
1100
1202
  try {
1203
+ const checkDeadline = () => {
1204
+ if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
1205
+ throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
1206
+ }
1207
+ };
1101
1208
  const own = await client.getOwnTeamSetup();
1102
1209
  const ws = own.workspace;
1210
+ if (opts.workspace && ws !== opts.workspace) {
1211
+ throw new SyncStopped(
1212
+ "the key is for another workspace",
1213
+ `The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
1214
+ );
1215
+ }
1103
1216
  const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
1104
1217
  if (!project) {
1105
1218
  throw new SyncFailed(
@@ -1113,6 +1226,12 @@ async function syncMemory(opts) {
1113
1226
  const date = localDate(now);
1114
1227
  const stored = (await readState(ws)).memory?.[project];
1115
1228
  let team = await pullTeam(client, ws, project);
1229
+ if (opts.unattended && stored && !team.exists) {
1230
+ throw new SyncStopped(
1231
+ "the team memory could not be read",
1232
+ `${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
1233
+ );
1234
+ }
1116
1235
  let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
1117
1236
  const folder = await readFolder(dir);
1118
1237
  const shared = {};
@@ -1125,14 +1244,16 @@ async function syncMemory(opts) {
1125
1244
  } else shared[path] = text;
1126
1245
  }
1127
1246
  const importWrites = {};
1128
- if (opts.importDir) {
1129
- const other = await readFolder(resolve(opts.importDir));
1130
- const taken = new Set(folder.names);
1247
+ const current = { ...folder.files };
1248
+ const taken = new Set(folder.names);
1249
+ for (const importDir of opts.importDirs ?? []) {
1250
+ const other = await readFolder(resolve(importDir));
1131
1251
  for (const [path, text] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
1132
1252
  if (!isShareable(path, text) || !isFilePath(path)) continue;
1133
- const here = folder.files[path];
1253
+ const here = current[path];
1134
1254
  if (here === void 0) {
1135
1255
  shared[path] = text;
1256
+ current[path] = text;
1136
1257
  importWrites[path] = text;
1137
1258
  taken.add(path.toLowerCase());
1138
1259
  } else if (here !== text) {
@@ -1140,16 +1261,14 @@ async function syncMemory(opts) {
1140
1261
  taken.add(copy.toLowerCase());
1141
1262
  importWrites[copy] = text;
1142
1263
  report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
1264
+ report.copies.add(copy);
1143
1265
  }
1144
1266
  }
1145
- folder.names = taken;
1146
- if (!opts.dryRun) {
1147
- for (const [path, text] of Object.entries(importWrites)) {
1148
- const full = toPath(dir, path);
1149
- await mkdir2(dirname2(full), { recursive: true });
1150
- await writeFile2(full, text, "utf8");
1151
- }
1152
- }
1267
+ }
1268
+ folder.names = taken;
1269
+ if (!opts.dryRun) {
1270
+ checkDeadline();
1271
+ for (const [path, text] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text);
1153
1272
  }
1154
1273
  const found = await people(run2, ws, shared, report, hint);
1155
1274
  for (const path of found.leftOut) {
@@ -1174,12 +1293,16 @@ async function syncMemory(opts) {
1174
1293
  );
1175
1294
  return 0;
1176
1295
  }
1296
+ checkDeadline();
1297
+ if (attempt === 1) await removeLeftTemps(dir, folder.temps);
1177
1298
  await applyPlan(dir, plan);
1178
1299
  let version = team.version;
1179
1300
  if (!sameManifest(plan.manifest, team.manifest)) {
1301
+ checkDeadline();
1180
1302
  await upload(client, ws, project, plan, team.manifest, report);
1181
1303
  }
1182
1304
  if (!sameManifest(plan.manifest, team.manifest)) {
1305
+ checkDeadline();
1183
1306
  try {
1184
1307
  version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
1185
1308
  } catch (e) {
@@ -1191,6 +1314,7 @@ async function syncMemory(opts) {
1191
1314
  false
1192
1315
  )
1193
1316
  );
1317
+ opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
1194
1318
  return 2;
1195
1319
  }
1196
1320
  base = team;
@@ -1215,18 +1339,198 @@ async function syncMemory(opts) {
1215
1339
  false
1216
1340
  )
1217
1341
  );
1342
+ opts.summary?.(report.summary("synced", version));
1218
1343
  return 0;
1219
1344
  }
1220
1345
  } catch (e) {
1221
1346
  out(e.message);
1347
+ opts.summary?.({
1348
+ outcome: "failed",
1349
+ reason: failureReason(e),
1350
+ version: 0,
1351
+ pulled: 0,
1352
+ pushed: 0,
1353
+ deleted: 0,
1354
+ conflictCopies: [],
1355
+ waiting: []
1356
+ });
1222
1357
  return 1;
1223
1358
  }
1224
1359
  }
1360
+ var plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
1361
+ function hookLines(ws, project, folder, s) {
1362
+ if (s.outcome !== "synced") {
1363
+ return [`Team memory ${ws}/${project}: not synced (${s.reason ?? "it did not finish"}); your files are unchanged and the next session tries again.`];
1364
+ }
1365
+ const lines = [`Team memory ${ws}/${project}: version ${s.version}, ${s.pulled} pulled, ${s.pushed} pushed, ${s.deleted} deleted.`];
1366
+ if (s.conflictCopies.length) {
1367
+ lines.push(` ${plural(s.conflictCopies.length, "conflict copy", "conflict copies")} to tidy in ${folder}: ${s.conflictCopies.join(", ")}`);
1368
+ }
1369
+ if (s.waiting.length) {
1370
+ lines.push(
1371
+ ` ${plural(s.waiting.length, "file waits", "files wait")} for you: run \`${SYNC_MEMORY_COMMAND} --project ${project}\` in your terminal.`
1372
+ );
1373
+ }
1374
+ return lines;
1375
+ }
1376
+ async function appendLog(ws, lines) {
1377
+ const path = syncLogPath(ws);
1378
+ await mkdir2(dirname2(path), { recursive: true });
1379
+ try {
1380
+ if ((await stat(path)).size > HOOK_LOG_MAX_BYTES) await writeFile2(path, "", "utf8");
1381
+ } catch {
1382
+ }
1383
+ await appendFile(path, lines.join("\n") + "\n", "utf8");
1384
+ }
1385
+ async function takeLock(lock, staleMs, now) {
1386
+ await mkdir2(dirname2(lock), { recursive: true });
1387
+ const body = JSON.stringify({ pid: process.pid, at: now.toISOString() }) + "\n";
1388
+ for (let attempt = 0; attempt < 2; attempt++) {
1389
+ try {
1390
+ const handle = await open(lock, "wx");
1391
+ try {
1392
+ await handle.writeFile(body, "utf8");
1393
+ } catch (err) {
1394
+ await handle.close();
1395
+ await rm2(lock, { force: true });
1396
+ throw err;
1397
+ }
1398
+ await handle.close();
1399
+ return true;
1400
+ } catch (e) {
1401
+ if (e.code !== "EEXIST") throw e;
1402
+ let seen;
1403
+ try {
1404
+ seen = await stat(lock);
1405
+ } catch {
1406
+ continue;
1407
+ }
1408
+ if (Date.now() - seen.mtimeMs < staleMs || attempt === 1) return false;
1409
+ try {
1410
+ const now2 = await stat(lock);
1411
+ if (now2.ino !== seen.ino || now2.mtimeMs !== seen.mtimeMs) return false;
1412
+ } catch {
1413
+ continue;
1414
+ }
1415
+ await rm2(lock, { force: true });
1416
+ }
1417
+ }
1418
+ return false;
1419
+ }
1420
+ async function releaseLock(lock) {
1421
+ try {
1422
+ const held = JSON.parse(await readFile2(lock, "utf8"));
1423
+ if (held.pid === process.pid) await rm2(lock, { force: true });
1424
+ } catch {
1425
+ }
1426
+ }
1427
+ async function runHookSync(workspace, io, env = process.env) {
1428
+ try {
1429
+ return await hookSync(workspace, io, env);
1430
+ } catch (e) {
1431
+ try {
1432
+ io.err(`sync-memory --hook: ${e.message}`);
1433
+ } catch {
1434
+ }
1435
+ return 0;
1436
+ }
1437
+ }
1438
+ async function hookSync(workspace, io, env) {
1439
+ if (!isWorkspace(workspace)) {
1440
+ io.err(`sync-memory --hook: ${JSON.stringify(workspace)} is not a workspace name`);
1441
+ return 0;
1442
+ }
1443
+ const now = io.now?.() ?? /* @__PURE__ */ new Date();
1444
+ const deadline = (io.startedAt ?? Date.now()) + (io.budgetMs ?? HOOK_BUDGET_MS);
1445
+ let client = io.client;
1446
+ if (!client) {
1447
+ let apiKey;
1448
+ let apiUrl;
1449
+ try {
1450
+ apiKey = await apiKeyFor(workspace, env);
1451
+ apiUrl = apiUrlFrom(env);
1452
+ } catch {
1453
+ return 0;
1454
+ }
1455
+ if (!apiKey) return 0;
1456
+ client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
1457
+ }
1458
+ let projects;
1459
+ try {
1460
+ projects = Object.keys((await readState(workspace)).memory ?? {}).sort();
1461
+ } catch {
1462
+ return 0;
1463
+ }
1464
+ if (!projects.length) return 0;
1465
+ const lock = syncLockPath(workspace);
1466
+ if (!await takeLock(lock, io.lockStaleMs ?? HOOK_LOCK_STALE_MS, now)) {
1467
+ io.out(`Team memory ${workspace}: a sync is already running.`);
1468
+ return 0;
1469
+ }
1470
+ try {
1471
+ for (const project of projects) {
1472
+ const folder = join2(workspaceDir(workspace), "memory", project);
1473
+ const report = [];
1474
+ let summary;
1475
+ const left = deadline - Date.now();
1476
+ if (left > 0) {
1477
+ let timer;
1478
+ const timeout = new Promise((resolveTimeout) => {
1479
+ timer = setTimeout(() => resolveTimeout("timeout"), left);
1480
+ });
1481
+ const run2 = syncMemory({
1482
+ project,
1483
+ dryRun: false,
1484
+ client,
1485
+ workspace,
1486
+ deadline,
1487
+ unattended: true,
1488
+ out: (...lines2) => report.push(...lines2),
1489
+ summary: (s) => summary = s,
1490
+ now: io.now
1491
+ });
1492
+ const won = await Promise.race([run2.then(() => "done"), timeout]);
1493
+ if (timer) clearTimeout(timer);
1494
+ if (won === "timeout") summary = void 0;
1495
+ }
1496
+ const result = summary ?? {
1497
+ outcome: "failed",
1498
+ reason: "took too long",
1499
+ version: 0,
1500
+ pulled: 0,
1501
+ pushed: 0,
1502
+ deleted: 0,
1503
+ conflictCopies: [],
1504
+ waiting: []
1505
+ };
1506
+ const lines = hookLines(workspace, project, folder, result);
1507
+ lines.forEach((l) => io.out(l));
1508
+ try {
1509
+ await appendLog(workspace, [`--- ${now.toISOString()} ${workspace}/${project}`, ...lines, ...report]);
1510
+ } catch (e) {
1511
+ io.err(`sync-memory --hook: the log could not be written: ${e.message}`);
1512
+ }
1513
+ }
1514
+ } finally {
1515
+ await releaseLock(lock);
1516
+ }
1517
+ return 0;
1518
+ }
1225
1519
  async function runSyncMemory(argv, io, env = process.env) {
1226
1520
  let args;
1227
1521
  let client = io.client;
1228
1522
  try {
1229
1523
  args = parseSyncMemoryArgs(argv);
1524
+ } catch (e) {
1525
+ if (argv.includes("--hook")) {
1526
+ io.err(`sync-memory --hook: ${e.message.split("\n")[0]}`);
1527
+ return 0;
1528
+ }
1529
+ io.err(e.message);
1530
+ return 1;
1531
+ }
1532
+ if (args.hook) return runHookSync(args.hook, io, env);
1533
+ try {
1230
1534
  if (!client) {
1231
1535
  const apiUrl = apiUrlFrom(env);
1232
1536
  const apiKey = await localApiKey(env);
@@ -1239,9 +1543,14 @@ async function runSyncMemory(argv, io, env = process.env) {
1239
1543
  return syncMemory({ ...args, client, out: (...lines) => lines.forEach((l) => io.out(l)), ask: io.ask, now: io.now });
1240
1544
  }
1241
1545
  export {
1546
+ HOOK_BUDGET_MS,
1547
+ HOOK_LOCK_STALE_MS,
1548
+ HOOK_LOG_MAX_BYTES,
1242
1549
  SYNC_MEMORY_COMMAND,
1243
1550
  SYNC_MEMORY_USAGE,
1551
+ hookLines,
1244
1552
  parseSyncMemoryArgs,
1553
+ runHookSync,
1245
1554
  runSyncMemory,
1246
1555
  syncMemory
1247
1556
  };