@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/index.js CHANGED
@@ -1311,8 +1311,8 @@ function registerGetPlanBoard(server, apiClient2) {
1311
1311
  ]
1312
1312
  };
1313
1313
  }
1314
- const open = boards.filter((b) => !b.closed);
1315
- if (open.length === 0) {
1314
+ const open2 = boards.filter((b) => !b.closed);
1315
+ if (open2.length === 0) {
1316
1316
  return {
1317
1317
  content: [
1318
1318
  {
@@ -1322,16 +1322,16 @@ function registerGetPlanBoard(server, apiClient2) {
1322
1322
  ]
1323
1323
  };
1324
1324
  }
1325
- const closedNote = boards.length > open.length ? `
1325
+ const closedNote = boards.length > open2.length ? `
1326
1326
 
1327
- _${boards.length - open.length} closed board(s) not listed._` : "";
1327
+ _${boards.length - open2.length} closed board(s) not listed._` : "";
1328
1328
  return {
1329
1329
  content: [
1330
1330
  {
1331
1331
  type: "text",
1332
- text: `${open.length} open project board(s):
1332
+ text: `${open2.length} open project board(s):
1333
1333
 
1334
- ${formatBoards(open)}` + closedNote + `
1334
+ ${formatBoards(open2)}` + closedNote + `
1335
1335
 
1336
1336
  Call get_plan_board with project_number to list a board's issues.`
1337
1337
  }
@@ -3094,7 +3094,7 @@ var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
3094
3094
 
3095
3095
  // src/sync-memory.ts
3096
3096
  import { existsSync as existsSync3 } from "fs";
3097
- import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3097
+ import { appendFile, mkdir as mkdir3, open, readdir as readdir2, readFile as readFile4, rename as rename2, rm as rm3, stat, writeFile as writeFile3 } from "fs/promises";
3098
3098
  import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
3099
3099
 
3100
3100
  // src/lib/memory-sync.ts
@@ -3118,15 +3118,41 @@ function readFrontmatter(text3) {
3118
3118
  const m = BLOCK_RE.exec(text3);
3119
3119
  if (!m) return void 0;
3120
3120
  const front = { shareFalse: false };
3121
- for (const line of m[1].split(/\r?\n/)) {
3122
- const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
3121
+ const lines = m[1].split(/\r?\n/);
3122
+ if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
3123
+ let topType;
3124
+ let topShare;
3125
+ let nestedType;
3126
+ let nestedShare;
3127
+ let inMetadata = false;
3128
+ let nestedIndent;
3129
+ for (const line of lines) {
3130
+ if (!line.trim() || /^[ \t]*#/.test(line)) continue;
3131
+ const indent = /^ */.exec(line)[0].length;
3132
+ if (indent === 0) {
3133
+ inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
3134
+ nestedIndent = void 0;
3135
+ }
3136
+ const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
3123
3137
  if (!kv) continue;
3124
3138
  const raw = kv[2].replace(/[ \t]+#.*$/, "");
3125
3139
  const quoted = /^(["'])(.*)\1$/.exec(raw);
3126
3140
  const value = quoted ? quoted[2] : raw;
3127
- if (kv[1] === "type") front.type = value.trim().toLowerCase();
3128
- if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
3129
- }
3141
+ const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
3142
+ if (indent === 0) {
3143
+ if (kv[1] === "type") topType = value.trim().toLowerCase();
3144
+ if (kv[1] === "share") topShare = isFalse;
3145
+ continue;
3146
+ }
3147
+ if (!inMetadata) continue;
3148
+ nestedIndent ??= indent;
3149
+ if (indent !== nestedIndent) continue;
3150
+ if (kv[1] === "type") nestedType = value.trim().toLowerCase();
3151
+ if (kv[1] === "share") nestedShare = isFalse;
3152
+ }
3153
+ const type = topType ?? nestedType;
3154
+ if (type !== void 0) front.type = type;
3155
+ front.shareFalse = (topShare ?? nestedShare) === true;
3130
3156
  return front;
3131
3157
  }
3132
3158
  function isShareable(path, text3) {
@@ -3367,18 +3393,50 @@ function lineDiff(before, after) {
3367
3393
  }
3368
3394
 
3369
3395
  // src/sync-memory.ts
3396
+ var HOOK_LOG_MAX_BYTES = 1024 * 1024;
3370
3397
  var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
3371
3398
  var SyncFailed = class extends Error {
3372
3399
  };
3400
+ var SyncStopped = class extends Error {
3401
+ constructor(reason, message) {
3402
+ super(message);
3403
+ this.reason = reason;
3404
+ }
3405
+ reason;
3406
+ };
3407
+ function failureReason(e) {
3408
+ if (e instanceof SyncStopped) return e.reason;
3409
+ const { status, code } = refusal(e);
3410
+ if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
3411
+ const err = e;
3412
+ const net = err?.cause?.code ?? err?.code ?? "";
3413
+ if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
3414
+ return "SystemDox could not be reached";
3415
+ }
3416
+ return "it could not finish";
3417
+ }
3418
+ async function writeWhole(path, text3) {
3419
+ await mkdir3(dirname3(path), { recursive: true });
3420
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3421
+ try {
3422
+ await writeFile3(tmp, text3, "utf8");
3423
+ await rename2(tmp, path);
3424
+ } catch (e) {
3425
+ await rm3(tmp, { force: true });
3426
+ throw e;
3427
+ }
3428
+ }
3373
3429
  function refusal(e) {
3374
3430
  if (!(e instanceof Error)) return {};
3375
3431
  const { status, code } = e;
3376
3432
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
3377
3433
  }
3378
3434
  var toPath = (root, rel) => join4(root, ...rel.split("/"));
3435
+ var LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
3379
3436
  async function readFolder(root) {
3380
3437
  const files = {};
3381
3438
  const names = /* @__PURE__ */ new Set();
3439
+ const temps = [];
3382
3440
  async function walk2(dir, prefix) {
3383
3441
  for (const e of await readdir2(dir, { withFileTypes: true })) {
3384
3442
  if (e.name.startsWith(".")) continue;
@@ -3386,12 +3444,13 @@ async function readFolder(root) {
3386
3444
  if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
3387
3445
  else if (e.isFile()) {
3388
3446
  names.add(rel.toLowerCase());
3447
+ if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
3389
3448
  if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
3390
3449
  }
3391
3450
  }
3392
3451
  }
3393
3452
  if (existsSync3(root)) await walk2(root, "");
3394
- return { files, names };
3453
+ return { files, names, temps };
3395
3454
  }
3396
3455
  async function pullTeam(client, ws, project) {
3397
3456
  try {
@@ -3440,7 +3499,11 @@ var Report = class {
3440
3499
  deletedHere = /* @__PURE__ */ new Set();
3441
3500
  deletedFromTeam = /* @__PURE__ */ new Set();
3442
3501
  conflicts = [];
3502
+ /** The conflict copies written this run, by name. */
3503
+ copies = /* @__PURE__ */ new Set();
3443
3504
  skipped = /* @__PURE__ */ new Map();
3505
+ /** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
3506
+ waiting = /* @__PURE__ */ new Set();
3444
3507
  people = [];
3445
3508
  toAnswer = /* @__PURE__ */ new Map();
3446
3509
  add(plan) {
@@ -3448,12 +3511,32 @@ var Report = class {
3448
3511
  plan.pushed.forEach((p) => this.pushed.add(p));
3449
3512
  plan.deletedHere.forEach((p) => this.deletedHere.add(p));
3450
3513
  plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
3451
- for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
3514
+ for (const c of plan.conflicts) {
3515
+ this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
3516
+ this.copies.add(c.copy);
3517
+ }
3452
3518
  for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
3453
3519
  }
3454
3520
  skip(path, reason) {
3455
3521
  if (!this.skipped.has(path)) this.skipped.set(path, reason);
3456
3522
  }
3523
+ /** Skipped, and waiting for the member at a terminal. */
3524
+ wait(path, reason) {
3525
+ this.skip(path, reason);
3526
+ this.waiting.add(path);
3527
+ }
3528
+ summary(outcome, version, reason) {
3529
+ return {
3530
+ outcome,
3531
+ ...reason ? { reason } : {},
3532
+ version,
3533
+ pulled: this.pulled.size,
3534
+ pushed: this.pushed.size,
3535
+ deleted: this.deletedHere.size + this.deletedFromTeam.size,
3536
+ conflictCopies: [...this.copies].sort(),
3537
+ waiting: [...this.waiting].sort()
3538
+ };
3539
+ }
3457
3540
  lines(summary, dryRun) {
3458
3541
  const out = [summary];
3459
3542
  const section = (title, items) => {
@@ -3556,7 +3639,7 @@ async function people(opts, ws, files, report, hint) {
3556
3639
  for (const path of g.files) {
3557
3640
  if (leftOut.has(path)) continue;
3558
3641
  current[path] = addShareFalse(current[path]);
3559
- await writeFile3(toPath(opts.dir, path), current[path], "utf8");
3642
+ await writeWhole(toPath(opts.dir, path), current[path]);
3560
3643
  leftOut.add(path);
3561
3644
  }
3562
3645
  report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
@@ -3659,7 +3742,7 @@ async function people(opts, ws, files, report, hint) {
3659
3742
  const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
3660
3743
  if (yes) {
3661
3744
  for (const path of paths) {
3662
- await writeFile3(toPath(opts.dir, path), rewritten[path], "utf8");
3745
+ await writeWhole(toPath(opts.dir, path), rewritten[path]);
3663
3746
  current[path] = rewritten[path];
3664
3747
  }
3665
3748
  }
@@ -3674,11 +3757,11 @@ async function people(opts, ws, files, report, hint) {
3674
3757
  held.add(path);
3675
3758
  const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
3676
3759
  if (keys.some((k) => vaultDown.has(k))) {
3677
- report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
3760
+ report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
3678
3761
  } else if (keys.some((k) => !answers.has(k))) {
3679
- report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
3762
+ report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
3680
3763
  } else {
3681
- report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
3764
+ report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
3682
3765
  }
3683
3766
  }
3684
3767
  for (const g of groups) {
@@ -3689,12 +3772,11 @@ async function people(opts, ws, files, report, hint) {
3689
3772
  }
3690
3773
  return { files: current, held, leftOut };
3691
3774
  }
3775
+ async function removeLeftTemps(dir, temps) {
3776
+ for (const rel of temps) await rm3(toPath(dir, rel), { force: true });
3777
+ }
3692
3778
  async function applyPlan(dir, plan) {
3693
- for (const [path, text3] of Object.entries(plan.writes)) {
3694
- const full = toPath(dir, path);
3695
- await mkdir3(dirname3(full), { recursive: true });
3696
- await writeFile3(full, text3, "utf8");
3697
- }
3779
+ for (const [path, text3] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text3);
3698
3780
  for (const path of plan.deletes) await rm3(toPath(dir, path), { force: true });
3699
3781
  }
3700
3782
  var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
@@ -3739,8 +3821,19 @@ async function syncMemory(opts) {
3739
3821
  const { client, out } = opts;
3740
3822
  const now = opts.now?.() ?? /* @__PURE__ */ new Date();
3741
3823
  try {
3824
+ const checkDeadline = () => {
3825
+ if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
3826
+ throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
3827
+ }
3828
+ };
3742
3829
  const own = await client.getOwnTeamSetup();
3743
3830
  const ws = own.workspace;
3831
+ if (opts.workspace && ws !== opts.workspace) {
3832
+ throw new SyncStopped(
3833
+ "the key is for another workspace",
3834
+ `The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
3835
+ );
3836
+ }
3744
3837
  const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
3745
3838
  if (!project) {
3746
3839
  throw new SyncFailed(
@@ -3754,6 +3847,12 @@ async function syncMemory(opts) {
3754
3847
  const date = localDate(now);
3755
3848
  const stored = (await readState(ws)).memory?.[project];
3756
3849
  let team = await pullTeam(client, ws, project);
3850
+ if (opts.unattended && stored && !team.exists) {
3851
+ throw new SyncStopped(
3852
+ "the team memory could not be read",
3853
+ `${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
3854
+ );
3855
+ }
3757
3856
  let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
3758
3857
  const folder = await readFolder(dir);
3759
3858
  const shared = {};
@@ -3766,14 +3865,16 @@ async function syncMemory(opts) {
3766
3865
  } else shared[path] = text3;
3767
3866
  }
3768
3867
  const importWrites = {};
3769
- if (opts.importDir) {
3770
- const other = await readFolder(resolve3(opts.importDir));
3771
- const taken = new Set(folder.names);
3868
+ const current = { ...folder.files };
3869
+ const taken = new Set(folder.names);
3870
+ for (const importDir of opts.importDirs ?? []) {
3871
+ const other = await readFolder(resolve3(importDir));
3772
3872
  for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
3773
3873
  if (!isShareable(path, text3) || !isFilePath(path)) continue;
3774
- const here = folder.files[path];
3874
+ const here = current[path];
3775
3875
  if (here === void 0) {
3776
3876
  shared[path] = text3;
3877
+ current[path] = text3;
3777
3878
  importWrites[path] = text3;
3778
3879
  taken.add(path.toLowerCase());
3779
3880
  } else if (here !== text3) {
@@ -3781,16 +3882,14 @@ async function syncMemory(opts) {
3781
3882
  taken.add(copy.toLowerCase());
3782
3883
  importWrites[copy] = text3;
3783
3884
  report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
3885
+ report.copies.add(copy);
3784
3886
  }
3785
3887
  }
3786
- folder.names = taken;
3787
- if (!opts.dryRun) {
3788
- for (const [path, text3] of Object.entries(importWrites)) {
3789
- const full = toPath(dir, path);
3790
- await mkdir3(dirname3(full), { recursive: true });
3791
- await writeFile3(full, text3, "utf8");
3792
- }
3793
- }
3888
+ }
3889
+ folder.names = taken;
3890
+ if (!opts.dryRun) {
3891
+ checkDeadline();
3892
+ for (const [path, text3] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text3);
3794
3893
  }
3795
3894
  const found = await people(run2, ws, shared, report, hint);
3796
3895
  for (const path of found.leftOut) {
@@ -3815,12 +3914,16 @@ async function syncMemory(opts) {
3815
3914
  );
3816
3915
  return 0;
3817
3916
  }
3917
+ checkDeadline();
3918
+ if (attempt === 1) await removeLeftTemps(dir, folder.temps);
3818
3919
  await applyPlan(dir, plan);
3819
3920
  let version = team.version;
3820
3921
  if (!sameManifest(plan.manifest, team.manifest)) {
3922
+ checkDeadline();
3821
3923
  await upload(client, ws, project, plan, team.manifest, report);
3822
3924
  }
3823
3925
  if (!sameManifest(plan.manifest, team.manifest)) {
3926
+ checkDeadline();
3824
3927
  try {
3825
3928
  version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
3826
3929
  } catch (e) {
@@ -3832,6 +3935,7 @@ async function syncMemory(opts) {
3832
3935
  false
3833
3936
  )
3834
3937
  );
3938
+ opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
3835
3939
  return 2;
3836
3940
  }
3837
3941
  base = team;
@@ -3856,10 +3960,21 @@ async function syncMemory(opts) {
3856
3960
  false
3857
3961
  )
3858
3962
  );
3963
+ opts.summary?.(report.summary("synced", version));
3859
3964
  return 0;
3860
3965
  }
3861
3966
  } catch (e) {
3862
3967
  out(e.message);
3968
+ opts.summary?.({
3969
+ outcome: "failed",
3970
+ reason: failureReason(e),
3971
+ version: 0,
3972
+ pulled: 0,
3973
+ pushed: 0,
3974
+ deleted: 0,
3975
+ conflictCopies: [],
3976
+ waiting: []
3977
+ });
3863
3978
  return 1;
3864
3979
  }
3865
3980
  }
@@ -4031,7 +4146,9 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
4031
4146
  project: args.project,
4032
4147
  dryRun: !!args.dryRun,
4033
4148
  client: apiClient2,
4034
- out: (...l) => lines.push(...l)
4149
+ out: (...l) => lines.push(...l),
4150
+ // Nobody is at a terminal: a project the team no longer serves is left as it is.
4151
+ unattended: true
4035
4152
  });
4036
4153
  return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
4037
4154
  }
@@ -4039,7 +4156,7 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
4039
4156
  }
4040
4157
 
4041
4158
  // src/server.ts
4042
- var SERVER_VERSION = true ? "0.1.56" : "0.0.0-dev";
4159
+ var SERVER_VERSION = true ? "0.1.58" : "0.0.0-dev";
4043
4160
  var READ_TOOLS = [
4044
4161
  "get_context",
4045
4162
  "get_checks",
@@ -4,8 +4,9 @@
4
4
  * `src/sync-memory.ts` reads the folders and calls the API around it.
5
5
  *
6
6
  * - **What is shared** (`isShareable`): a `*.md` file whose frontmatter `type`
7
- * is `feedback`, `project` or `reference`, and `MEMORY.md`; never a file
8
- * with `share: false` or a `<name>.local-<date>.md` conflict copy.
7
+ * (at the top level or under `metadata:`, Amendment 3) is `feedback`,
8
+ * `project` or `reference`, and `MEMORY.md`; never a file with
9
+ * `share: false` or a `<name>.local-<date>.md` conflict copy.
9
10
  * - **The merge** (`decide`, `planMerge`): three-way per path, with the base
10
11
  * (the manifest of the last sync), the team's latest version and the local
11
12
  * folder, row by row as the contract's table says.
@@ -29,7 +30,13 @@ export interface Frontmatter {
29
30
  shareFalse: boolean;
30
31
  }
31
32
  /** The frontmatter's `type` and `share`, or undefined when the file has no
32
- * leading block. Only top-level `key: value` lines are read. */
33
+ * leading block. They are read at the top level, or one level under
34
+ * `metadata:`, the form Claude Code writes (`metadata:` on one line,
35
+ * ` type: project` under it); a top-level key wins over the nested one
36
+ * (ADR-217 Amendment 3). Read as the server's YAML reads them:
37
+ * - a block with a tab in a line's indentation is not YAML, so the server sees
38
+ * no front matter: nothing is read here either (no type, so not shared);
39
+ * - only keys at the first indent under `metadata:` count, never deeper ones. */
33
40
  export declare function readFrontmatter(text: string): Frontmatter | undefined;
34
41
  /**
35
42
  * Shared with the team: `MEMORY.md` at the folder's root, or a `*.md` file
@@ -93,15 +93,41 @@ function readFrontmatter(text) {
93
93
  const m = BLOCK_RE.exec(text);
94
94
  if (!m) return void 0;
95
95
  const front = { shareFalse: false };
96
- for (const line of m[1].split(/\r?\n/)) {
97
- const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
96
+ const lines = m[1].split(/\r?\n/);
97
+ if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
98
+ let topType;
99
+ let topShare;
100
+ let nestedType;
101
+ let nestedShare;
102
+ let inMetadata = false;
103
+ let nestedIndent;
104
+ for (const line of lines) {
105
+ if (!line.trim() || /^[ \t]*#/.test(line)) continue;
106
+ const indent = /^ */.exec(line)[0].length;
107
+ if (indent === 0) {
108
+ inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
109
+ nestedIndent = void 0;
110
+ }
111
+ const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
98
112
  if (!kv) continue;
99
113
  const raw = kv[2].replace(/[ \t]+#.*$/, "");
100
114
  const quoted = /^(["'])(.*)\1$/.exec(raw);
101
115
  const value = quoted ? quoted[2] : raw;
102
- if (kv[1] === "type") front.type = value.trim().toLowerCase();
103
- if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
116
+ const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
117
+ if (indent === 0) {
118
+ if (kv[1] === "type") topType = value.trim().toLowerCase();
119
+ if (kv[1] === "share") topShare = isFalse;
120
+ continue;
121
+ }
122
+ if (!inMetadata) continue;
123
+ nestedIndent ??= indent;
124
+ if (indent !== nestedIndent) continue;
125
+ if (kv[1] === "type") nestedType = value.trim().toLowerCase();
126
+ if (kv[1] === "share") nestedShare = isFalse;
104
127
  }
128
+ const type = topType ?? nestedType;
129
+ if (type !== void 0) front.type = type;
130
+ front.shareFalse = (topShare ?? nestedShare) === true;
105
131
  return front;
106
132
  }
107
133
  function isShareable(path, text) {
@@ -8,7 +8,9 @@
8
8
  * - `~/.claude/systemdox/<workspace>/state.json`: the installed versions and
9
9
  * hashes, each memory project's base manifest and version, the accepted
10
10
  * SHA-256s. Never a value and never an answer about a person;
11
- * - `~/.claude/systemdox/<workspace>/plugin/<n>/`: plugin version n, unpacked.
11
+ * - `~/.claude/systemdox/<workspace>/plugin/<n>/`: plugin version n, unpacked;
12
+ * - `~/.claude/systemdox/<workspace>/sync-memory.log` and `sync.lock`: the hook
13
+ * run's full report and its lock (ADR-217 Amendment 3).
12
14
  *
13
15
  * Every path starts at `homeDir()`, the one place the home folder is read, so
14
16
  * a test points HOME (USERPROFILE on Windows) at a temporary folder.
@@ -29,6 +31,11 @@ export declare function systemdoxDir(): string;
29
31
  export declare function credentialsPath(): string;
30
32
  export declare function workspaceDir(workspace: string): string;
31
33
  export declare function statePath(workspace: string): string;
34
+ /** `~/.claude/systemdox/<workspace>/sync-memory.log`: the full report of every
35
+ * `sync-memory --hook` run, newest last (ADR-217 Amendment 3). */
36
+ export declare function syncLogPath(workspace: string): string;
37
+ /** `~/.claude/systemdox/<workspace>/sync.lock`: held while a hook run syncs. */
38
+ export declare function syncLockPath(workspace: string): string;
32
39
  export declare function pluginRoot(workspace: string): string;
33
40
  export declare function pluginDir(workspace: string, version: number): string;
34
41
  export type Credentials = Record<string, {
@@ -63,6 +63,12 @@ function workspaceDir(workspace) {
63
63
  function statePath(workspace) {
64
64
  return join(workspaceDir(workspace), "state.json");
65
65
  }
66
+ function syncLogPath(workspace) {
67
+ return join(workspaceDir(workspace), "sync-memory.log");
68
+ }
69
+ function syncLockPath(workspace) {
70
+ return join(workspaceDir(workspace), "sync.lock");
71
+ }
66
72
  function pluginRoot(workspace) {
67
73
  return join(workspaceDir(workspace), "plugin");
68
74
  }
@@ -226,6 +232,8 @@ export {
226
232
  restrictToMember,
227
233
  runMarketplaceAuth,
228
234
  statePath,
235
+ syncLockPath,
236
+ syncLogPath,
229
237
  systemdoxDir,
230
238
  updateState,
231
239
  workspaceDir,
@@ -56,6 +56,8 @@ export interface PluginVersion {
56
56
  args?: string[];
57
57
  url?: string;
58
58
  }[];
59
+ /** This version carries SystemDox's memory sync hooks (ADR-217 Amendment 3); absent before it. */
60
+ memoryHooks?: boolean;
59
61
  command: string;
60
62
  }
61
63
  export interface Marketplace {