@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.
package/dist/http.js CHANGED
@@ -873,8 +873,8 @@ function registerGetPlanBoard(server, apiClient) {
873
873
  ]
874
874
  };
875
875
  }
876
- const open = boards.filter((b) => !b.closed);
877
- if (open.length === 0) {
876
+ const open2 = boards.filter((b) => !b.closed);
877
+ if (open2.length === 0) {
878
878
  return {
879
879
  content: [
880
880
  {
@@ -884,16 +884,16 @@ function registerGetPlanBoard(server, apiClient) {
884
884
  ]
885
885
  };
886
886
  }
887
- const closedNote = boards.length > open.length ? `
887
+ const closedNote = boards.length > open2.length ? `
888
888
 
889
- _${boards.length - open.length} closed board(s) not listed._` : "";
889
+ _${boards.length - open2.length} closed board(s) not listed._` : "";
890
890
  return {
891
891
  content: [
892
892
  {
893
893
  type: "text",
894
- text: `${open.length} open project board(s):
894
+ text: `${open2.length} open project board(s):
895
895
 
896
- ${formatBoards(open)}` + closedNote + `
896
+ ${formatBoards(open2)}` + closedNote + `
897
897
 
898
898
  Call get_plan_board with project_number to list a board's issues.`
899
899
  }
@@ -3040,7 +3040,7 @@ var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
3040
3040
 
3041
3041
  // src/sync-memory.ts
3042
3042
  import { existsSync as existsSync3 } from "fs";
3043
- import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
3043
+ 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";
3044
3044
  import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
3045
3045
 
3046
3046
  // src/lib/memory-sync.ts
@@ -3064,15 +3064,41 @@ function readFrontmatter(text3) {
3064
3064
  const m = BLOCK_RE.exec(text3);
3065
3065
  if (!m) return void 0;
3066
3066
  const front = { shareFalse: false };
3067
- for (const line of m[1].split(/\r?\n/)) {
3068
- const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
3067
+ const lines = m[1].split(/\r?\n/);
3068
+ if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
3069
+ let topType;
3070
+ let topShare;
3071
+ let nestedType;
3072
+ let nestedShare;
3073
+ let inMetadata = false;
3074
+ let nestedIndent;
3075
+ for (const line of lines) {
3076
+ if (!line.trim() || /^[ \t]*#/.test(line)) continue;
3077
+ const indent = /^ */.exec(line)[0].length;
3078
+ if (indent === 0) {
3079
+ inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
3080
+ nestedIndent = void 0;
3081
+ }
3082
+ const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
3069
3083
  if (!kv) continue;
3070
3084
  const raw = kv[2].replace(/[ \t]+#.*$/, "");
3071
3085
  const quoted = /^(["'])(.*)\1$/.exec(raw);
3072
3086
  const value = quoted ? quoted[2] : raw;
3073
- if (kv[1] === "type") front.type = value.trim().toLowerCase();
3074
- if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
3075
- }
3087
+ const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
3088
+ if (indent === 0) {
3089
+ if (kv[1] === "type") topType = value.trim().toLowerCase();
3090
+ if (kv[1] === "share") topShare = isFalse;
3091
+ continue;
3092
+ }
3093
+ if (!inMetadata) continue;
3094
+ nestedIndent ??= indent;
3095
+ if (indent !== nestedIndent) continue;
3096
+ if (kv[1] === "type") nestedType = value.trim().toLowerCase();
3097
+ if (kv[1] === "share") nestedShare = isFalse;
3098
+ }
3099
+ const type = topType ?? nestedType;
3100
+ if (type !== void 0) front.type = type;
3101
+ front.shareFalse = (topShare ?? nestedShare) === true;
3076
3102
  return front;
3077
3103
  }
3078
3104
  function isShareable(path, text3) {
@@ -3313,18 +3339,50 @@ function lineDiff(before, after) {
3313
3339
  }
3314
3340
 
3315
3341
  // src/sync-memory.ts
3342
+ var HOOK_LOG_MAX_BYTES = 1024 * 1024;
3316
3343
  var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
3317
3344
  var SyncFailed = class extends Error {
3318
3345
  };
3346
+ var SyncStopped = class extends Error {
3347
+ constructor(reason, message) {
3348
+ super(message);
3349
+ this.reason = reason;
3350
+ }
3351
+ reason;
3352
+ };
3353
+ function failureReason(e) {
3354
+ if (e instanceof SyncStopped) return e.reason;
3355
+ const { status, code } = refusal(e);
3356
+ if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
3357
+ const err = e;
3358
+ const net = err?.cause?.code ?? err?.code ?? "";
3359
+ if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
3360
+ return "SystemDox could not be reached";
3361
+ }
3362
+ return "it could not finish";
3363
+ }
3364
+ async function writeWhole(path, text3) {
3365
+ await mkdir3(dirname3(path), { recursive: true });
3366
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3367
+ try {
3368
+ await writeFile3(tmp, text3, "utf8");
3369
+ await rename2(tmp, path);
3370
+ } catch (e) {
3371
+ await rm3(tmp, { force: true });
3372
+ throw e;
3373
+ }
3374
+ }
3319
3375
  function refusal(e) {
3320
3376
  if (!(e instanceof Error)) return {};
3321
3377
  const { status, code } = e;
3322
3378
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
3323
3379
  }
3324
3380
  var toPath = (root, rel) => join4(root, ...rel.split("/"));
3381
+ var LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
3325
3382
  async function readFolder(root) {
3326
3383
  const files = {};
3327
3384
  const names = /* @__PURE__ */ new Set();
3385
+ const temps = [];
3328
3386
  async function walk2(dir, prefix) {
3329
3387
  for (const e of await readdir2(dir, { withFileTypes: true })) {
3330
3388
  if (e.name.startsWith(".")) continue;
@@ -3332,12 +3390,13 @@ async function readFolder(root) {
3332
3390
  if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
3333
3391
  else if (e.isFile()) {
3334
3392
  names.add(rel.toLowerCase());
3393
+ if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
3335
3394
  if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
3336
3395
  }
3337
3396
  }
3338
3397
  }
3339
3398
  if (existsSync3(root)) await walk2(root, "");
3340
- return { files, names };
3399
+ return { files, names, temps };
3341
3400
  }
3342
3401
  async function pullTeam(client, ws, project) {
3343
3402
  try {
@@ -3386,7 +3445,11 @@ var Report = class {
3386
3445
  deletedHere = /* @__PURE__ */ new Set();
3387
3446
  deletedFromTeam = /* @__PURE__ */ new Set();
3388
3447
  conflicts = [];
3448
+ /** The conflict copies written this run, by name. */
3449
+ copies = /* @__PURE__ */ new Set();
3389
3450
  skipped = /* @__PURE__ */ new Map();
3451
+ /** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
3452
+ waiting = /* @__PURE__ */ new Set();
3390
3453
  people = [];
3391
3454
  toAnswer = /* @__PURE__ */ new Map();
3392
3455
  add(plan) {
@@ -3394,12 +3457,32 @@ var Report = class {
3394
3457
  plan.pushed.forEach((p) => this.pushed.add(p));
3395
3458
  plan.deletedHere.forEach((p) => this.deletedHere.add(p));
3396
3459
  plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
3397
- for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
3460
+ for (const c of plan.conflicts) {
3461
+ this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
3462
+ this.copies.add(c.copy);
3463
+ }
3398
3464
  for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
3399
3465
  }
3400
3466
  skip(path, reason) {
3401
3467
  if (!this.skipped.has(path)) this.skipped.set(path, reason);
3402
3468
  }
3469
+ /** Skipped, and waiting for the member at a terminal. */
3470
+ wait(path, reason) {
3471
+ this.skip(path, reason);
3472
+ this.waiting.add(path);
3473
+ }
3474
+ summary(outcome, version, reason) {
3475
+ return {
3476
+ outcome,
3477
+ ...reason ? { reason } : {},
3478
+ version,
3479
+ pulled: this.pulled.size,
3480
+ pushed: this.pushed.size,
3481
+ deleted: this.deletedHere.size + this.deletedFromTeam.size,
3482
+ conflictCopies: [...this.copies].sort(),
3483
+ waiting: [...this.waiting].sort()
3484
+ };
3485
+ }
3403
3486
  lines(summary, dryRun) {
3404
3487
  const out = [summary];
3405
3488
  const section = (title, items) => {
@@ -3502,7 +3585,7 @@ async function people(opts, ws, files, report, hint) {
3502
3585
  for (const path of g.files) {
3503
3586
  if (leftOut.has(path)) continue;
3504
3587
  current[path] = addShareFalse(current[path]);
3505
- await writeFile3(toPath(opts.dir, path), current[path], "utf8");
3588
+ await writeWhole(toPath(opts.dir, path), current[path]);
3506
3589
  leftOut.add(path);
3507
3590
  }
3508
3591
  report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
@@ -3605,7 +3688,7 @@ async function people(opts, ws, files, report, hint) {
3605
3688
  const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
3606
3689
  if (yes) {
3607
3690
  for (const path of paths) {
3608
- await writeFile3(toPath(opts.dir, path), rewritten[path], "utf8");
3691
+ await writeWhole(toPath(opts.dir, path), rewritten[path]);
3609
3692
  current[path] = rewritten[path];
3610
3693
  }
3611
3694
  }
@@ -3620,11 +3703,11 @@ async function people(opts, ws, files, report, hint) {
3620
3703
  held.add(path);
3621
3704
  const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
3622
3705
  if (keys.some((k) => vaultDown.has(k))) {
3623
- report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
3706
+ report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
3624
3707
  } else if (keys.some((k) => !answers.has(k))) {
3625
- report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
3708
+ report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
3626
3709
  } else {
3627
- report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
3710
+ report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
3628
3711
  }
3629
3712
  }
3630
3713
  for (const g of groups) {
@@ -3635,12 +3718,11 @@ async function people(opts, ws, files, report, hint) {
3635
3718
  }
3636
3719
  return { files: current, held, leftOut };
3637
3720
  }
3721
+ async function removeLeftTemps(dir, temps) {
3722
+ for (const rel of temps) await rm3(toPath(dir, rel), { force: true });
3723
+ }
3638
3724
  async function applyPlan(dir, plan) {
3639
- for (const [path, text3] of Object.entries(plan.writes)) {
3640
- const full = toPath(dir, path);
3641
- await mkdir3(dirname3(full), { recursive: true });
3642
- await writeFile3(full, text3, "utf8");
3643
- }
3725
+ for (const [path, text3] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text3);
3644
3726
  for (const path of plan.deletes) await rm3(toPath(dir, path), { force: true });
3645
3727
  }
3646
3728
  var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
@@ -3685,8 +3767,19 @@ async function syncMemory(opts) {
3685
3767
  const { client, out } = opts;
3686
3768
  const now = opts.now?.() ?? /* @__PURE__ */ new Date();
3687
3769
  try {
3770
+ const checkDeadline = () => {
3771
+ if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
3772
+ throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
3773
+ }
3774
+ };
3688
3775
  const own = await client.getOwnTeamSetup();
3689
3776
  const ws = own.workspace;
3777
+ if (opts.workspace && ws !== opts.workspace) {
3778
+ throw new SyncStopped(
3779
+ "the key is for another workspace",
3780
+ `The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
3781
+ );
3782
+ }
3690
3783
  const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
3691
3784
  if (!project) {
3692
3785
  throw new SyncFailed(
@@ -3700,6 +3793,12 @@ async function syncMemory(opts) {
3700
3793
  const date = localDate(now);
3701
3794
  const stored = (await readState(ws)).memory?.[project];
3702
3795
  let team = await pullTeam(client, ws, project);
3796
+ if (opts.unattended && stored && !team.exists) {
3797
+ throw new SyncStopped(
3798
+ "the team memory could not be read",
3799
+ `${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
3800
+ );
3801
+ }
3703
3802
  let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
3704
3803
  const folder = await readFolder(dir);
3705
3804
  const shared = {};
@@ -3712,14 +3811,16 @@ async function syncMemory(opts) {
3712
3811
  } else shared[path] = text3;
3713
3812
  }
3714
3813
  const importWrites = {};
3715
- if (opts.importDir) {
3716
- const other = await readFolder(resolve3(opts.importDir));
3717
- const taken = new Set(folder.names);
3814
+ const current = { ...folder.files };
3815
+ const taken = new Set(folder.names);
3816
+ for (const importDir of opts.importDirs ?? []) {
3817
+ const other = await readFolder(resolve3(importDir));
3718
3818
  for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
3719
3819
  if (!isShareable(path, text3) || !isFilePath(path)) continue;
3720
- const here = folder.files[path];
3820
+ const here = current[path];
3721
3821
  if (here === void 0) {
3722
3822
  shared[path] = text3;
3823
+ current[path] = text3;
3723
3824
  importWrites[path] = text3;
3724
3825
  taken.add(path.toLowerCase());
3725
3826
  } else if (here !== text3) {
@@ -3727,16 +3828,14 @@ async function syncMemory(opts) {
3727
3828
  taken.add(copy.toLowerCase());
3728
3829
  importWrites[copy] = text3;
3729
3830
  report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
3831
+ report.copies.add(copy);
3730
3832
  }
3731
3833
  }
3732
- folder.names = taken;
3733
- if (!opts.dryRun) {
3734
- for (const [path, text3] of Object.entries(importWrites)) {
3735
- const full = toPath(dir, path);
3736
- await mkdir3(dirname3(full), { recursive: true });
3737
- await writeFile3(full, text3, "utf8");
3738
- }
3739
- }
3834
+ }
3835
+ folder.names = taken;
3836
+ if (!opts.dryRun) {
3837
+ checkDeadline();
3838
+ for (const [path, text3] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text3);
3740
3839
  }
3741
3840
  const found = await people(run2, ws, shared, report, hint);
3742
3841
  for (const path of found.leftOut) {
@@ -3761,12 +3860,16 @@ async function syncMemory(opts) {
3761
3860
  );
3762
3861
  return 0;
3763
3862
  }
3863
+ checkDeadline();
3864
+ if (attempt === 1) await removeLeftTemps(dir, folder.temps);
3764
3865
  await applyPlan(dir, plan);
3765
3866
  let version = team.version;
3766
3867
  if (!sameManifest(plan.manifest, team.manifest)) {
3868
+ checkDeadline();
3767
3869
  await upload(client, ws, project, plan, team.manifest, report);
3768
3870
  }
3769
3871
  if (!sameManifest(plan.manifest, team.manifest)) {
3872
+ checkDeadline();
3770
3873
  try {
3771
3874
  version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
3772
3875
  } catch (e) {
@@ -3778,6 +3881,7 @@ async function syncMemory(opts) {
3778
3881
  false
3779
3882
  )
3780
3883
  );
3884
+ opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
3781
3885
  return 2;
3782
3886
  }
3783
3887
  base = team;
@@ -3802,10 +3906,21 @@ async function syncMemory(opts) {
3802
3906
  false
3803
3907
  )
3804
3908
  );
3909
+ opts.summary?.(report.summary("synced", version));
3805
3910
  return 0;
3806
3911
  }
3807
3912
  } catch (e) {
3808
3913
  out(e.message);
3914
+ opts.summary?.({
3915
+ outcome: "failed",
3916
+ reason: failureReason(e),
3917
+ version: 0,
3918
+ pulled: 0,
3919
+ pushed: 0,
3920
+ deleted: 0,
3921
+ conflictCopies: [],
3922
+ waiting: []
3923
+ });
3809
3924
  return 1;
3810
3925
  }
3811
3926
  }
@@ -3977,7 +4092,9 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3977
4092
  project: args.project,
3978
4093
  dryRun: !!args.dryRun,
3979
4094
  client: apiClient,
3980
- out: (...l) => lines.push(...l)
4095
+ out: (...l) => lines.push(...l),
4096
+ // Nobody is at a terminal: a project the team no longer serves is left as it is.
4097
+ unattended: true
3981
4098
  });
3982
4099
  return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
3983
4100
  }
@@ -3985,7 +4102,7 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3985
4102
  }
3986
4103
 
3987
4104
  // src/server.ts
3988
- var SERVER_VERSION = true ? "0.1.57" : "0.0.0-dev";
4105
+ var SERVER_VERSION = true ? "0.1.58" : "0.0.0-dev";
3989
4106
  var READ_TOOLS = [
3990
4107
  "get_context",
3991
4108
  "get_checks",