@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/server.js CHANGED
@@ -604,8 +604,8 @@ function registerGetPlanBoard(server, apiClient) {
604
604
  ]
605
605
  };
606
606
  }
607
- const open = boards.filter((b) => !b.closed);
608
- if (open.length === 0) {
607
+ const open2 = boards.filter((b) => !b.closed);
608
+ if (open2.length === 0) {
609
609
  return {
610
610
  content: [
611
611
  {
@@ -615,16 +615,16 @@ function registerGetPlanBoard(server, apiClient) {
615
615
  ]
616
616
  };
617
617
  }
618
- const closedNote = boards.length > open.length ? `
618
+ const closedNote = boards.length > open2.length ? `
619
619
 
620
- _${boards.length - open.length} closed board(s) not listed._` : "";
620
+ _${boards.length - open2.length} closed board(s) not listed._` : "";
621
621
  return {
622
622
  content: [
623
623
  {
624
624
  type: "text",
625
- text: `${open.length} open project board(s):
625
+ text: `${open2.length} open project board(s):
626
626
 
627
- ${formatBoards(open)}` + closedNote + `
627
+ ${formatBoards(open2)}` + closedNote + `
628
628
 
629
629
  Call get_plan_board with project_number to list a board's issues.`
630
630
  }
@@ -2707,7 +2707,7 @@ var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2707
2707
 
2708
2708
  // src/sync-memory.ts
2709
2709
  import { existsSync as existsSync3 } from "fs";
2710
- import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
2710
+ 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";
2711
2711
  import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
2712
2712
 
2713
2713
  // src/lib/memory-sync.ts
@@ -2731,15 +2731,41 @@ function readFrontmatter(text3) {
2731
2731
  const m = BLOCK_RE.exec(text3);
2732
2732
  if (!m) return void 0;
2733
2733
  const front = { shareFalse: false };
2734
- for (const line of m[1].split(/\r?\n/)) {
2735
- const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
2734
+ const lines = m[1].split(/\r?\n/);
2735
+ if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
2736
+ let topType;
2737
+ let topShare;
2738
+ let nestedType;
2739
+ let nestedShare;
2740
+ let inMetadata = false;
2741
+ let nestedIndent;
2742
+ for (const line of lines) {
2743
+ if (!line.trim() || /^[ \t]*#/.test(line)) continue;
2744
+ const indent = /^ */.exec(line)[0].length;
2745
+ if (indent === 0) {
2746
+ inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
2747
+ nestedIndent = void 0;
2748
+ }
2749
+ const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
2736
2750
  if (!kv) continue;
2737
2751
  const raw = kv[2].replace(/[ \t]+#.*$/, "");
2738
2752
  const quoted = /^(["'])(.*)\1$/.exec(raw);
2739
2753
  const value = quoted ? quoted[2] : raw;
2740
- if (kv[1] === "type") front.type = value.trim().toLowerCase();
2741
- if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
2754
+ const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
2755
+ if (indent === 0) {
2756
+ if (kv[1] === "type") topType = value.trim().toLowerCase();
2757
+ if (kv[1] === "share") topShare = isFalse;
2758
+ continue;
2759
+ }
2760
+ if (!inMetadata) continue;
2761
+ nestedIndent ??= indent;
2762
+ if (indent !== nestedIndent) continue;
2763
+ if (kv[1] === "type") nestedType = value.trim().toLowerCase();
2764
+ if (kv[1] === "share") nestedShare = isFalse;
2742
2765
  }
2766
+ const type = topType ?? nestedType;
2767
+ if (type !== void 0) front.type = type;
2768
+ front.shareFalse = (topShare ?? nestedShare) === true;
2743
2769
  return front;
2744
2770
  }
2745
2771
  function isShareable(path, text3) {
@@ -2980,18 +3006,50 @@ function lineDiff(before, after) {
2980
3006
  }
2981
3007
 
2982
3008
  // src/sync-memory.ts
3009
+ var HOOK_LOG_MAX_BYTES = 1024 * 1024;
2983
3010
  var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
2984
3011
  var SyncFailed = class extends Error {
2985
3012
  };
3013
+ var SyncStopped = class extends Error {
3014
+ constructor(reason, message) {
3015
+ super(message);
3016
+ this.reason = reason;
3017
+ }
3018
+ reason;
3019
+ };
3020
+ function failureReason(e) {
3021
+ if (e instanceof SyncStopped) return e.reason;
3022
+ const { status, code } = refusal(e);
3023
+ if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
3024
+ const err = e;
3025
+ const net = err?.cause?.code ?? err?.code ?? "";
3026
+ if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
3027
+ return "SystemDox could not be reached";
3028
+ }
3029
+ return "it could not finish";
3030
+ }
3031
+ async function writeWhole(path, text3) {
3032
+ await mkdir3(dirname3(path), { recursive: true });
3033
+ const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
3034
+ try {
3035
+ await writeFile3(tmp, text3, "utf8");
3036
+ await rename2(tmp, path);
3037
+ } catch (e) {
3038
+ await rm3(tmp, { force: true });
3039
+ throw e;
3040
+ }
3041
+ }
2986
3042
  function refusal(e) {
2987
3043
  if (!(e instanceof Error)) return {};
2988
3044
  const { status, code } = e;
2989
3045
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
2990
3046
  }
2991
3047
  var toPath = (root, rel) => join4(root, ...rel.split("/"));
3048
+ var LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
2992
3049
  async function readFolder(root) {
2993
3050
  const files = {};
2994
3051
  const names = /* @__PURE__ */ new Set();
3052
+ const temps = [];
2995
3053
  async function walk2(dir, prefix) {
2996
3054
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2997
3055
  if (e.name.startsWith(".")) continue;
@@ -2999,12 +3057,13 @@ async function readFolder(root) {
2999
3057
  if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
3000
3058
  else if (e.isFile()) {
3001
3059
  names.add(rel.toLowerCase());
3060
+ if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
3002
3061
  if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
3003
3062
  }
3004
3063
  }
3005
3064
  }
3006
3065
  if (existsSync3(root)) await walk2(root, "");
3007
- return { files, names };
3066
+ return { files, names, temps };
3008
3067
  }
3009
3068
  async function pullTeam(client, ws, project) {
3010
3069
  try {
@@ -3053,7 +3112,11 @@ var Report = class {
3053
3112
  deletedHere = /* @__PURE__ */ new Set();
3054
3113
  deletedFromTeam = /* @__PURE__ */ new Set();
3055
3114
  conflicts = [];
3115
+ /** The conflict copies written this run, by name. */
3116
+ copies = /* @__PURE__ */ new Set();
3056
3117
  skipped = /* @__PURE__ */ new Map();
3118
+ /** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
3119
+ waiting = /* @__PURE__ */ new Set();
3057
3120
  people = [];
3058
3121
  toAnswer = /* @__PURE__ */ new Map();
3059
3122
  add(plan) {
@@ -3061,12 +3124,32 @@ var Report = class {
3061
3124
  plan.pushed.forEach((p) => this.pushed.add(p));
3062
3125
  plan.deletedHere.forEach((p) => this.deletedHere.add(p));
3063
3126
  plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
3064
- for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
3127
+ for (const c of plan.conflicts) {
3128
+ this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
3129
+ this.copies.add(c.copy);
3130
+ }
3065
3131
  for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
3066
3132
  }
3067
3133
  skip(path, reason) {
3068
3134
  if (!this.skipped.has(path)) this.skipped.set(path, reason);
3069
3135
  }
3136
+ /** Skipped, and waiting for the member at a terminal. */
3137
+ wait(path, reason) {
3138
+ this.skip(path, reason);
3139
+ this.waiting.add(path);
3140
+ }
3141
+ summary(outcome, version, reason) {
3142
+ return {
3143
+ outcome,
3144
+ ...reason ? { reason } : {},
3145
+ version,
3146
+ pulled: this.pulled.size,
3147
+ pushed: this.pushed.size,
3148
+ deleted: this.deletedHere.size + this.deletedFromTeam.size,
3149
+ conflictCopies: [...this.copies].sort(),
3150
+ waiting: [...this.waiting].sort()
3151
+ };
3152
+ }
3070
3153
  lines(summary, dryRun) {
3071
3154
  const out = [summary];
3072
3155
  const section = (title, items) => {
@@ -3169,7 +3252,7 @@ async function people(opts, ws, files, report, hint) {
3169
3252
  for (const path of g.files) {
3170
3253
  if (leftOut.has(path)) continue;
3171
3254
  current[path] = addShareFalse(current[path]);
3172
- await writeFile3(toPath(opts.dir, path), current[path], "utf8");
3255
+ await writeWhole(toPath(opts.dir, path), current[path]);
3173
3256
  leftOut.add(path);
3174
3257
  }
3175
3258
  report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
@@ -3272,7 +3355,7 @@ async function people(opts, ws, files, report, hint) {
3272
3355
  const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
3273
3356
  if (yes) {
3274
3357
  for (const path of paths) {
3275
- await writeFile3(toPath(opts.dir, path), rewritten[path], "utf8");
3358
+ await writeWhole(toPath(opts.dir, path), rewritten[path]);
3276
3359
  current[path] = rewritten[path];
3277
3360
  }
3278
3361
  }
@@ -3287,11 +3370,11 @@ async function people(opts, ws, files, report, hint) {
3287
3370
  held.add(path);
3288
3371
  const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
3289
3372
  if (keys.some((k) => vaultDown.has(k))) {
3290
- report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
3373
+ report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
3291
3374
  } else if (keys.some((k) => !answers.has(k))) {
3292
- report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
3375
+ report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
3293
3376
  } else {
3294
- report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
3377
+ report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
3295
3378
  }
3296
3379
  }
3297
3380
  for (const g of groups) {
@@ -3302,12 +3385,11 @@ async function people(opts, ws, files, report, hint) {
3302
3385
  }
3303
3386
  return { files: current, held, leftOut };
3304
3387
  }
3388
+ async function removeLeftTemps(dir, temps) {
3389
+ for (const rel of temps) await rm3(toPath(dir, rel), { force: true });
3390
+ }
3305
3391
  async function applyPlan(dir, plan) {
3306
- for (const [path, text3] of Object.entries(plan.writes)) {
3307
- const full = toPath(dir, path);
3308
- await mkdir3(dirname3(full), { recursive: true });
3309
- await writeFile3(full, text3, "utf8");
3310
- }
3392
+ for (const [path, text3] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text3);
3311
3393
  for (const path of plan.deletes) await rm3(toPath(dir, path), { force: true });
3312
3394
  }
3313
3395
  var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
@@ -3352,8 +3434,19 @@ async function syncMemory(opts) {
3352
3434
  const { client, out } = opts;
3353
3435
  const now = opts.now?.() ?? /* @__PURE__ */ new Date();
3354
3436
  try {
3437
+ const checkDeadline = () => {
3438
+ if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
3439
+ throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
3440
+ }
3441
+ };
3355
3442
  const own = await client.getOwnTeamSetup();
3356
3443
  const ws = own.workspace;
3444
+ if (opts.workspace && ws !== opts.workspace) {
3445
+ throw new SyncStopped(
3446
+ "the key is for another workspace",
3447
+ `The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
3448
+ );
3449
+ }
3357
3450
  const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
3358
3451
  if (!project) {
3359
3452
  throw new SyncFailed(
@@ -3367,6 +3460,12 @@ async function syncMemory(opts) {
3367
3460
  const date = localDate(now);
3368
3461
  const stored = (await readState(ws)).memory?.[project];
3369
3462
  let team = await pullTeam(client, ws, project);
3463
+ if (opts.unattended && stored && !team.exists) {
3464
+ throw new SyncStopped(
3465
+ "the team memory could not be read",
3466
+ `${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
3467
+ );
3468
+ }
3370
3469
  let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
3371
3470
  const folder = await readFolder(dir);
3372
3471
  const shared = {};
@@ -3379,14 +3478,16 @@ async function syncMemory(opts) {
3379
3478
  } else shared[path] = text3;
3380
3479
  }
3381
3480
  const importWrites = {};
3382
- if (opts.importDir) {
3383
- const other = await readFolder(resolve3(opts.importDir));
3384
- const taken = new Set(folder.names);
3481
+ const current = { ...folder.files };
3482
+ const taken = new Set(folder.names);
3483
+ for (const importDir of opts.importDirs ?? []) {
3484
+ const other = await readFolder(resolve3(importDir));
3385
3485
  for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
3386
3486
  if (!isShareable(path, text3) || !isFilePath(path)) continue;
3387
- const here = folder.files[path];
3487
+ const here = current[path];
3388
3488
  if (here === void 0) {
3389
3489
  shared[path] = text3;
3490
+ current[path] = text3;
3390
3491
  importWrites[path] = text3;
3391
3492
  taken.add(path.toLowerCase());
3392
3493
  } else if (here !== text3) {
@@ -3394,16 +3495,14 @@ async function syncMemory(opts) {
3394
3495
  taken.add(copy.toLowerCase());
3395
3496
  importWrites[copy] = text3;
3396
3497
  report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
3498
+ report.copies.add(copy);
3397
3499
  }
3398
3500
  }
3399
- folder.names = taken;
3400
- if (!opts.dryRun) {
3401
- for (const [path, text3] of Object.entries(importWrites)) {
3402
- const full = toPath(dir, path);
3403
- await mkdir3(dirname3(full), { recursive: true });
3404
- await writeFile3(full, text3, "utf8");
3405
- }
3406
- }
3501
+ }
3502
+ folder.names = taken;
3503
+ if (!opts.dryRun) {
3504
+ checkDeadline();
3505
+ for (const [path, text3] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text3);
3407
3506
  }
3408
3507
  const found = await people(run2, ws, shared, report, hint);
3409
3508
  for (const path of found.leftOut) {
@@ -3428,12 +3527,16 @@ async function syncMemory(opts) {
3428
3527
  );
3429
3528
  return 0;
3430
3529
  }
3530
+ checkDeadline();
3531
+ if (attempt === 1) await removeLeftTemps(dir, folder.temps);
3431
3532
  await applyPlan(dir, plan);
3432
3533
  let version = team.version;
3433
3534
  if (!sameManifest(plan.manifest, team.manifest)) {
3535
+ checkDeadline();
3434
3536
  await upload(client, ws, project, plan, team.manifest, report);
3435
3537
  }
3436
3538
  if (!sameManifest(plan.manifest, team.manifest)) {
3539
+ checkDeadline();
3437
3540
  try {
3438
3541
  version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
3439
3542
  } catch (e) {
@@ -3445,6 +3548,7 @@ async function syncMemory(opts) {
3445
3548
  false
3446
3549
  )
3447
3550
  );
3551
+ opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
3448
3552
  return 2;
3449
3553
  }
3450
3554
  base = team;
@@ -3469,10 +3573,21 @@ async function syncMemory(opts) {
3469
3573
  false
3470
3574
  )
3471
3575
  );
3576
+ opts.summary?.(report.summary("synced", version));
3472
3577
  return 0;
3473
3578
  }
3474
3579
  } catch (e) {
3475
3580
  out(e.message);
3581
+ opts.summary?.({
3582
+ outcome: "failed",
3583
+ reason: failureReason(e),
3584
+ version: 0,
3585
+ pulled: 0,
3586
+ pushed: 0,
3587
+ deleted: 0,
3588
+ conflictCopies: [],
3589
+ waiting: []
3590
+ });
3476
3591
  return 1;
3477
3592
  }
3478
3593
  }
@@ -3644,7 +3759,9 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3644
3759
  project: args.project,
3645
3760
  dryRun: !!args.dryRun,
3646
3761
  client: apiClient,
3647
- out: (...l) => lines.push(...l)
3762
+ out: (...l) => lines.push(...l),
3763
+ // Nobody is at a terminal: a project the team no longer serves is left as it is.
3764
+ unattended: true
3648
3765
  });
3649
3766
  return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
3650
3767
  }
@@ -3652,7 +3769,7 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3652
3769
  }
3653
3770
 
3654
3771
  // src/server.ts
3655
- var SERVER_VERSION = true ? "0.1.57" : "0.0.0-dev";
3772
+ var SERVER_VERSION = true ? "0.1.59" : "0.0.0-dev";
3656
3773
  var READ_TOOLS = [
3657
3774
  "get_context",
3658
3775
  "get_checks",
package/dist/setup-mcp.js CHANGED
@@ -359,8 +359,9 @@ var KEY_PREFIX = `(?<!${W})(?:pweb_[a-z]+_key_|sk-ant-|sk_live_|rk_live_|github_
359
359
 
360
360
  // src/sync-memory.ts
361
361
  import { existsSync } from "fs";
362
- import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
362
+ import { appendFile, mkdir as mkdir2, open, readdir, readFile as readFile2, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
363
363
  import { dirname as dirname2, join as join2, resolve } from "path";
364
+ var HOOK_LOG_MAX_BYTES = 1024 * 1024;
364
365
 
365
366
  // src/setup-team.ts
366
367
  var SetupFailed = class extends Error {
@@ -399,9 +400,9 @@ function skipValue(t, i) {
399
400
  while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
400
401
  return i;
401
402
  }
402
- function objectMembers(t, open) {
403
+ function objectMembers(t, open2) {
403
404
  const members = [];
404
- let i = skipSpace(t, open + 1);
405
+ let i = skipSpace(t, open2 + 1);
405
406
  if (t[i] === "}") return { members, close: i };
406
407
  for (; ; ) {
407
408
  i = skipSpace(t, i);
@@ -428,13 +429,13 @@ function setJsonMember(text, keys, value) {
428
429
  const eol = text.includes("\r\n") ? "\r\n" : "\n";
429
430
  const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
430
431
  const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
431
- let open = skipSpace(text, 0);
432
+ let open2 = skipSpace(text, 0);
432
433
  for (let k = 0; k < keys.length; k++) {
433
- const { members, close } = objectMembers(text, open);
434
+ const { members, close } = objectMembers(text, open2);
434
435
  const found = members.filter((m) => m.key === keys[k]).at(-1);
435
436
  if (found) {
436
437
  if (k < keys.length - 1 && text[found.valueStart] === "{") {
437
- open = found.valueStart;
438
+ open2 = found.valueStart;
438
439
  continue;
439
440
  }
440
441
  return text.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text, found.keyStart)) + text.slice(found.valueEnd);
@@ -443,16 +444,16 @@ function setJsonMember(text, keys, value) {
443
444
  if (compact) {
444
445
  const member = `${name}:${render(nest(k + 1), "")}`;
445
446
  if (members.length) return text.slice(0, members.at(-1).valueEnd) + "," + member + text.slice(members.at(-1).valueEnd);
446
- return text.slice(0, open + 1) + member + text.slice(close);
447
+ return text.slice(0, open2 + 1) + member + text.slice(close);
447
448
  }
448
449
  if (members.length) {
449
450
  const last = members.at(-1);
450
451
  const indent2 = lineIndent(text, last.keyStart);
451
452
  return text.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text.slice(last.valueEnd);
452
453
  }
453
- const outer = lineIndent(text, open);
454
+ const outer = lineIndent(text, open2);
454
455
  const indent = outer + unit;
455
- return text.slice(0, open + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text.slice(close);
456
+ return text.slice(0, open2 + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text.slice(close);
456
457
  }
457
458
  return text;
458
459
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The team setup that `systemdox-mcp-server setup [--project <name>]
3
- * [--dir <checkout>] [--yes] [--overwrite]` runs after it has written the MCP
4
- * server into an AI tool's config (ADR-217 with Amendments 1 and 2,
5
- * specs/changes/team-setup/contract.md "setup").
3
+ * [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]` runs after
4
+ * it has written the MCP server into an AI tool's config (ADR-217 with
5
+ * Amendments 1 to 3, specs/changes/team-setup/contract.md "setup").
6
6
  *
7
7
  * 1. Check: `GET /team-setup` names the workspace; `claude --version` must be
8
8
  * at least `minClaudeCodeVersion` (exit 3 otherwise, nothing changed).
@@ -13,7 +13,12 @@
13
13
  * instructions (exit 2 if the member edited them, unless --overwrite); the
14
14
  * import block in ~/.claude/CLAUDE.md; the marketplace in
15
15
  * ~/.claude/settings.json; the project's `autoMemoryDirectory` in
16
- * <checkout>/.claude/settings.local.json, then its sync.
16
+ * <checkout>/.claude/settings.local.json, then its sync. On the project's
17
+ * first sync on this computer, the sync imports the memory Claude Code kept
18
+ * for the checkout (`~/.claude/projects/<slug>/memory/`, for the checkout's
19
+ * path and, inside a Git repository, the repository's top folder) and each
20
+ * folder `--import` names, every one shown with its file count first
21
+ * (Amendment 3).
17
22
  * 4. Finish: the two steps a person must take in Claude Code.
18
23
  *
19
24
  * It never runs `claude plugin marketplace add` and never sets
@@ -26,11 +31,13 @@
26
31
  */
27
32
  import { SystemDoxApiClient } from "./lib/api-client.js";
28
33
  import { type SyncClient } from "./sync-memory.js";
29
- export declare const SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]";
34
+ export declare const SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]";
30
35
  export declare const HEADERS_HELPER = "npx -y @systemdox/mcp-server marketplace-auth";
31
36
  export interface TeamSetupArgs {
32
37
  project?: string;
33
38
  dir?: string;
39
+ /** Folders to import on the project's first sync on this computer (`--import`, repeatable). */
40
+ importDirs?: string[];
34
41
  yes: boolean;
35
42
  overwrite: boolean;
36
43
  }
@@ -49,12 +56,38 @@ export interface TeamSetupOptions extends TeamSetupArgs {
49
56
  cwd?: string;
50
57
  /** `claude --version`'s output, or undefined when Claude Code is not found. */
51
58
  claudeVersion?: () => Promise<string | undefined>;
59
+ /** The top folder of the Git repository holding `dir`, or undefined outside one. */
60
+ gitTop?: (dir: string) => Promise<string | undefined>;
52
61
  now?: () => Date;
53
62
  }
54
63
  export declare function parseSetupArgs(argv: string[]): TeamSetupArgs;
55
64
  /** `claude --version`'s output, or undefined when it cannot be run. On
56
65
  * Windows through the shell, so `claude.cmd` (npm) and `claude.exe` both resolve. */
57
66
  export declare function claudeCodeVersion(): Promise<string | undefined>;
67
+ /** The top folder of the Git repository holding `dir`, or undefined outside one. */
68
+ export declare function gitTopFolder(dir: string): Promise<string | undefined>;
69
+ /** `~/.claude/projects/<slug>/memory/` for an absolute path: Claude Code's slug
70
+ * replaces every character that is not a letter or a digit with `-`
71
+ * (`/home/jo/git` → `-home-jo-git`; `C:\Users\jo\git` → `C--Users-jo-git`). */
72
+ export declare function claudeMemoryFolder(path: string): string;
73
+ /** How many files of a folder a sync would share (hidden entries left out):
74
+ * the `feedback`, `project` and `reference` memories and `MEMORY.md`. A `user`
75
+ * memory, a `share: false` file and a conflict copy stay where they are, so
76
+ * they are not counted. 0 when the folder is missing. */
77
+ export declare function countMemoryFiles(folder: string): Promise<number>;
78
+ export interface MemoryImport {
79
+ folder: string;
80
+ files: number;
81
+ /** Named by `--import`, as opposed to found for the checkout. */
82
+ named: boolean;
83
+ }
84
+ /**
85
+ * The folders a project's first sync imports (contract.md "setup", step 3.5):
86
+ * the memory Claude Code kept for the checkout's own path and, inside a Git
87
+ * repository, for the repository's top folder, each when it holds a file and
88
+ * is not the team folder itself; then each folder `--import` names.
89
+ */
90
+ export declare function memoryImports(checkout: string, teamFolder: string, named: string[], gitTop?: (dir: string) => Promise<string | undefined>): Promise<MemoryImport[]>;
58
91
  /**
59
92
  * `text` (a JSON object) with `keys` set to `value`, every other byte kept:
60
93
  * an existing value is replaced in place, a missing member is added after the