@systemdox/mcp-server 0.1.49 → 0.1.50

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
@@ -63,14 +63,6 @@ var SystemDoxApiClient = class {
63
63
  this.config = config2;
64
64
  this.cache = new ResponseCache(config2.cacheTtlMs);
65
65
  }
66
- async validate() {
67
- const res = await this.fetchRaw("/v1/health");
68
- if (!res.ok) {
69
- throw new Error(
70
- `SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
71
- );
72
- }
73
- }
74
66
  async get(path, params) {
75
67
  const url = this.buildUrl(path, params);
76
68
  const cacheKey = url.toString();
@@ -84,8 +76,8 @@ var SystemDoxApiClient = class {
84
76
  }
85
77
  /** A read that skips the cache: an install must take the version that is
86
78
  * latest now, not the one cached before someone published another. */
87
- async getFresh(path) {
88
- const res = await this.fetchRaw(path);
79
+ async getFresh(path, signal) {
80
+ const res = await this.fetchRaw(path, void 0, signal);
89
81
  if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
90
82
  return await res.json();
91
83
  }
@@ -115,8 +107,8 @@ var SystemDoxApiClient = class {
115
107
  // One method per operationId. Every read skips the cache: setup, sync and
116
108
  // plugin-path act on what the workspace holds now.
117
109
  /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
118
- getOwnTeamSetup() {
119
- return this.getFresh(TEAM_SETUP);
110
+ getOwnTeamSetup(signal) {
111
+ return this.getFresh(TEAM_SETUP, signal);
120
112
  }
121
113
  getTeamSetup(workspace) {
122
114
  return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
@@ -220,25 +212,19 @@ var SystemDoxApiClient = class {
220
212
  }
221
213
  return url;
222
214
  }
223
- async fetchRaw(path, params) {
215
+ async fetchRaw(path, params, signal) {
224
216
  const url = this.buildUrl(path, params);
225
217
  return fetch(url, {
226
218
  headers: {
227
219
  Authorization: `Bearer ${this.config.apiKey}`,
228
220
  "X-API-Key": this.config.apiKey,
229
221
  Accept: "application/json"
230
- }
222
+ },
223
+ signal
231
224
  });
232
225
  }
233
226
  };
234
227
 
235
- // src/lib/team-setup-files.ts
236
- import { execFile } from "child_process";
237
- import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
238
- import { homedir, userInfo } from "os";
239
- import { dirname, join } from "path";
240
- import { promisify } from "util";
241
-
242
228
  // src/lib/config.ts
243
229
  var API_URL_DEFAULT = "https://api.puglieseweb.com/systemdox";
244
230
  function requireHttpsApiUrl(apiUrl) {
@@ -274,31 +260,236 @@ function loadConfig(apiKey = process.env.SYSTEMDOX_API_KEY) {
274
260
  };
275
261
  }
276
262
 
263
+ // src/lib/session-brief.ts
264
+ import { existsSync as existsSync2 } from "fs";
265
+ import { readFile as readFile3 } from "fs/promises";
266
+ import { join as join3 } from "path";
267
+
268
+ // src/lib/skills.ts
269
+ import { createHash } from "crypto";
270
+ import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
271
+ import { existsSync } from "fs";
272
+ import { homedir } from "os";
273
+ import { dirname, join, relative, resolve, sep } from "path";
274
+ var MANIFEST_FILE = ".systemdox-skill.json";
275
+ var ENTRY_FILE = "SKILL.md";
276
+ var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
277
+ var MAX_FILES = 100;
278
+ var MAX_FILE_BYTES = 256 * 1024;
279
+ var MAX_TOTAL_BYTES = 1024 * 1024;
280
+ var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
281
+ var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
282
+ var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
283
+ var SkillInstallRefused = class extends Error {
284
+ constructor(dest, edited) {
285
+ super(
286
+ `${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
287
+ );
288
+ this.dest = dest;
289
+ this.edited = edited;
290
+ }
291
+ dest;
292
+ edited;
293
+ };
294
+ function sha256(content) {
295
+ return createHash("sha256").update(content, "utf8").digest("hex");
296
+ }
297
+ function isValidName(name) {
298
+ return name.length <= 64 && NAME_RE.test(name);
299
+ }
300
+ function isValidPath(path) {
301
+ return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
302
+ }
303
+ function frontmatterName(skillMd) {
304
+ const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
305
+ if (!block) return void 0;
306
+ const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
307
+ return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
308
+ }
309
+ function defaultSkillDir(name) {
310
+ return join(homedir(), ".claude", "skills", name);
311
+ }
312
+ async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
313
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
314
+ if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
315
+ const full = join(dir, entry.name);
316
+ const rel = relative(root, full).split(sep).join("/");
317
+ if (entry.isSymbolicLink()) out.links.push(rel);
318
+ else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
319
+ else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
320
+ else if (entry.isFile()) out.files.push(rel);
321
+ }
322
+ out.files.sort();
323
+ return out;
324
+ }
325
+ async function readTextFiles(root, paths, what) {
326
+ if (paths.length > MAX_FILES) {
327
+ throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
328
+ }
329
+ const decoder = new TextDecoder("utf-8", { fatal: true });
330
+ const files = [];
331
+ let total = 0;
332
+ for (const path of paths) {
333
+ if (!isValidPath(path)) {
334
+ throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
335
+ }
336
+ const raw = await readFile(join(root, ...path.split("/")));
337
+ if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
338
+ if (raw.length > MAX_FILE_BYTES) {
339
+ throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
340
+ }
341
+ total += raw.length;
342
+ if (total > MAX_TOTAL_BYTES) {
343
+ throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
344
+ }
345
+ let content;
346
+ try {
347
+ content = decoder.decode(raw);
348
+ } catch {
349
+ throw new Error(`${path} is not UTF-8 text`);
350
+ }
351
+ files.push({ path, content });
352
+ }
353
+ return files;
354
+ }
355
+ async function readSkillDirectory(directory) {
356
+ const root = resolve(directory);
357
+ if (!existsSync(join(root, ENTRY_FILE))) {
358
+ throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
359
+ }
360
+ const walked = await walk(root, { skipHidden: true });
361
+ if (walked.links.length) {
362
+ throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
363
+ }
364
+ const files = await readTextFiles(root, walked.files, "a skill");
365
+ const skillMd = files.find((f) => f.path === ENTRY_FILE);
366
+ const name = frontmatterName(skillMd.content);
367
+ if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
368
+ return { name, files, skipped: walked.hidden };
369
+ }
370
+ function within(dest, path) {
371
+ if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
372
+ const full = resolve(dest, ...path.split("/"));
373
+ if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
374
+ return full;
375
+ }
376
+ async function readManifest(dest) {
377
+ try {
378
+ return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
379
+ } catch {
380
+ return void 0;
381
+ }
382
+ }
383
+ async function installSkill(version, dest, { overwrite = false } = {}) {
384
+ if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
385
+ const incoming = new Map(version.files.map((f) => [f.path, f]));
386
+ for (const f of version.files) within(dest, f.path);
387
+ if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
388
+ throw new Error(`${dest} is a link: install into a real folder`);
389
+ }
390
+ const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
391
+ if (walked.links.length) {
392
+ throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
393
+ }
394
+ const present = walked.files;
395
+ const manifest = await readManifest(dest);
396
+ const known2 = Object.fromEntries(
397
+ Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
398
+ );
399
+ const edited = [];
400
+ const untracked = [];
401
+ for (const path of present) {
402
+ const content = await readFile(join(dest, ...path.split("/")), "utf8");
403
+ if (Object.hasOwn(known2, path)) {
404
+ if (sha256(content) !== known2[path]) edited.push(path);
405
+ } else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
406
+ if (incoming.has(path) || !manifest) edited.push(path);
407
+ else untracked.push(path);
408
+ }
409
+ }
410
+ const blocking = edited.filter((p) => incoming.has(p) || !manifest);
411
+ if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
412
+ const written = [];
413
+ for (const f of version.files) {
414
+ const full = within(dest, f.path);
415
+ await mkdir(dirname(full), { recursive: true });
416
+ await writeFile(full, f.content, "utf8");
417
+ written.push(f.path);
418
+ }
419
+ const removed = [];
420
+ const keptEdited = [];
421
+ for (const path of Object.keys(known2)) {
422
+ if (incoming.has(path) || !present.includes(path)) continue;
423
+ if (edited.includes(path)) {
424
+ keptEdited.push(path);
425
+ continue;
426
+ }
427
+ await rm(within(dest, path));
428
+ removed.push(path);
429
+ }
430
+ const next = {
431
+ name: version.name,
432
+ version: version.version,
433
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
434
+ files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
435
+ };
436
+ await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
437
+ return {
438
+ dest,
439
+ version: version.version,
440
+ written,
441
+ removed,
442
+ keptUntracked: untracked.filter((p) => !incoming.has(p)),
443
+ keptEdited
444
+ };
445
+ }
446
+ function fenceFor(text3) {
447
+ const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
448
+ return "`".repeat(Math.max(3, longest + 1));
449
+ }
450
+ function describeInstall(name, r) {
451
+ const lines = [
452
+ `Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
453
+ ];
454
+ if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
455
+ if (r.keptEdited.length) {
456
+ lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
457
+ }
458
+ if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
459
+ lines.push("Start a new Claude Code session to load it.");
460
+ return lines.join("\n");
461
+ }
462
+
277
463
  // src/lib/team-setup-files.ts
464
+ import { execFile } from "child_process";
465
+ import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
466
+ import { homedir as homedir2, userInfo } from "os";
467
+ import { dirname as dirname2, join as join2 } from "path";
468
+ import { promisify } from "util";
278
469
  var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
279
470
  function isWorkspace(value) {
280
471
  return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
281
472
  }
282
473
  function homeDir() {
283
- return homedir();
474
+ return homedir2();
284
475
  }
285
476
  function systemdoxDir() {
286
- return join(homeDir(), ".claude", "systemdox");
477
+ return join2(homeDir(), ".claude", "systemdox");
287
478
  }
288
479
  function credentialsPath() {
289
- return join(systemdoxDir(), "credentials.json");
480
+ return join2(systemdoxDir(), "credentials.json");
290
481
  }
291
482
  function workspaceDir(workspace) {
292
483
  if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
293
- return join(systemdoxDir(), workspace);
484
+ return join2(systemdoxDir(), workspace);
294
485
  }
295
486
  function statePath(workspace) {
296
- return join(workspaceDir(workspace), "state.json");
487
+ return join2(workspaceDir(workspace), "state.json");
297
488
  }
298
489
  async function readJson(path, empty) {
299
490
  let text3;
300
491
  try {
301
- text3 = await readFile(path, "utf8");
492
+ text3 = await readFile2(path, "utf8");
302
493
  } catch (e) {
303
494
  if (e.code === "ENOENT") return empty;
304
495
  throw e;
@@ -310,9 +501,9 @@ async function readJson(path, empty) {
310
501
  }
311
502
  }
312
503
  async function writeJsonAtomically(path, value, mode) {
313
- await mkdir(dirname(path), { recursive: true });
504
+ await mkdir2(dirname2(path), { recursive: true });
314
505
  const tmp = `${path}.${process.pid}.tmp`;
315
- await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
506
+ await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
316
507
  await rename(tmp, path);
317
508
  }
318
509
  async function readCredentials() {
@@ -357,6 +548,137 @@ async function updateState(workspace, change) {
357
548
  return state;
358
549
  }
359
550
 
551
+ // src/lib/session-brief.ts
552
+ var INSTRUCTIONS_MAX = 2e3;
553
+ var BRIEF_TIMEOUT_MS = 8e3;
554
+ var NO_KEY = "no-key-on-this-computer";
555
+ var SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
556
+ var REFUSED = `SystemDox refused this computer's key, so the team's rules and skills cannot be read and every SystemDox tool fails. Tell the person once: a new key comes from the SystemDox console, Settings \u203A API keys, and \`${SETUP_COMMAND}\` in their own terminal stores it.`;
557
+ var NOT_CHECKED = "SystemDox could not be read when this session started, so the team's skills were not checked. Call get_team_setup to check again before relying on them.";
558
+ var CANNOT_SEE = "This server cannot see this computer's disk. For each skill listed under Team skills: if ~/.claude/skills/<name>/ is absent, or its .systemdox-skill.json names another version than get_team_setup lists, call install_skill with that version and write what it returns, unless the latest team plugin carries it. Leave a folder with no such file alone. If a file there differs from the hash that file holds for it, it was edited: ask the person before replacing it.";
559
+ function noKey(reason) {
560
+ return `This computer has no SystemDox key it can use (${reason}), so the team's rules and skills cannot be read and every SystemDox tool fails. Tell the person once: \`${SETUP_COMMAND}\` in their own terminal asks for one.`;
561
+ }
562
+ function keyReason(message) {
563
+ if (/holds keys for/.test(message)) return "keys for several workspaces are stored; set SYSTEMDOX_WORKSPACE to the one to use";
564
+ if (/holds no key for/.test(message)) return "SYSTEMDOX_WORKSPACE names a workspace with no stored key";
565
+ if (/No SystemDox key/.test(message)) return "none is stored";
566
+ return "the stored keys could not be read";
567
+ }
568
+ function olderApi(setup) {
569
+ return `SystemDox workspace ${setup.workspace}. Before writing code in one of its repositories, call get_context with the task and the files you expect to touch, and obey what it returns. get_team_setup lists the team setup.`;
570
+ }
571
+ async function readJsonQuietly(path) {
572
+ try {
573
+ return JSON.parse(await readFile3(path, "utf8"));
574
+ } catch {
575
+ return void 0;
576
+ }
577
+ }
578
+ async function computerState(setup) {
579
+ const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
580
+ const installed = await readJsonQuietly(join3(homeDir(), ".claude", "plugins", "installed_plugins.json"));
581
+ const entries2 = installed?.plugins?.[id];
582
+ const entry = Array.isArray(entries2) ? entries2.find((e) => (e?.scope ?? "user") === "user") : void 0;
583
+ const state = {
584
+ missing: [],
585
+ behind: [],
586
+ notFromSystemDox: [],
587
+ pluginInstalled: !!entry,
588
+ pluginVersion: entry?.version
589
+ };
590
+ const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
591
+ for (const skill of setup.sessionBrief?.skills ?? []) {
592
+ const dir = defaultSkillDir(skill.name);
593
+ if (!existsSync2(dir)) {
594
+ if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
595
+ continue;
596
+ }
597
+ const manifest = await readJsonQuietly(join3(dir, MANIFEST_FILE));
598
+ if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
599
+ state.notFromSystemDox.push(skill.name);
600
+ } else if (manifest.version !== skill.latestVersion) {
601
+ state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
602
+ }
603
+ }
604
+ return state;
605
+ }
606
+ function hasPersonParts(setup) {
607
+ return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
608
+ }
609
+ var MORE = (n) => `${n} more (get_team_setup lists them)`;
610
+ var LIST_MIN = `all ${MORE(100)}`.length;
611
+ function listWithin(items, room) {
612
+ const whole2 = items.join(", ");
613
+ if (whole2.length <= room) return whole2;
614
+ const kept = [];
615
+ let used = 0;
616
+ for (let i = 0; i < items.length; i++) {
617
+ const rest = items.length - i - 1;
618
+ const cost = items[i].length + (kept.length ? 2 : 0);
619
+ if (used + cost + ` and ${MORE(rest)}`.length > room) break;
620
+ kept.push(items[i]);
621
+ used += cost;
622
+ }
623
+ return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
624
+ }
625
+ function localSection(setup, state, room) {
626
+ const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
627
+ const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
628
+ const lines = [];
629
+ if (state.missing.length) {
630
+ lines.push({
631
+ head: "On this computer, missing: ",
632
+ items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
633
+ tail: `. Install each now with install_skill at that version, before your first answer. Then tell the person what you installed${rest}. When a skill's line matches the request and it is not loaded yet, read ~/.claude/skills/<name>/SKILL.md and follow it.`
634
+ });
635
+ }
636
+ if (state.behind.length) {
637
+ lines.push({
638
+ head: "On this computer, behind: ",
639
+ items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
640
+ tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
641
+ });
642
+ }
643
+ if (state.notFromSystemDox.length) {
644
+ lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
645
+ }
646
+ const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
647
+ while (lines.length && fixed() > room) lines.pop();
648
+ if (!lines.length) return "";
649
+ const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
650
+ return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
651
+ }
652
+ function fit(text3, section) {
653
+ const whole2 = section ? `${text3}
654
+
655
+ ${section}` : text3;
656
+ if (whole2.length <= INSTRUCTIONS_MAX) return whole2;
657
+ return text3.length <= INSTRUCTIONS_MAX ? text3 : `${text3.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
658
+ }
659
+ async function readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
660
+ if (keyProblem2) return { problem: noKey(keyProblem2) };
661
+ try {
662
+ const setup = await apiClient2.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
663
+ return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
664
+ } catch (err) {
665
+ const status = err?.status;
666
+ return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
667
+ }
668
+ }
669
+ async function sessionInstructions(apiClient2, options = {}) {
670
+ const read = await readTeamSetup(apiClient2, options);
671
+ if ("problem" in read) return read.problem;
672
+ const text3 = read.setup.sessionBrief?.text ?? olderApi(read.setup);
673
+ try {
674
+ if (!options.localFiles) return fit(text3, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
675
+ const room = INSTRUCTIONS_MAX - text3.length - 2;
676
+ return fit(text3, localSection(read.setup, await computerState(read.setup), room));
677
+ } catch {
678
+ return fit(text3, "");
679
+ }
680
+ }
681
+
360
682
  // src/server.ts
361
683
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
362
684
 
@@ -469,7 +791,7 @@ var inputSchema = () => ({
469
791
  "File path being edited \u2014 language is auto-detected from the extension"
470
792
  )
471
793
  });
472
- function registerGetGuardrails(server2, apiClient2) {
794
+ function registerGetGuardrails(server, apiClient2) {
473
795
  const handler = async ({
474
796
  language,
475
797
  category,
@@ -523,7 +845,7 @@ ${text3}`
523
845
  ]
524
846
  };
525
847
  };
526
- server2.registerTool(
848
+ server.registerTool(
527
849
  "get_checks",
528
850
  {
529
851
  title: "Get Checks",
@@ -532,7 +854,7 @@ ${text3}`
532
854
  },
533
855
  handler
534
856
  );
535
- server2.registerTool(
857
+ server.registerTool(
536
858
  "get_guardrails",
537
859
  {
538
860
  title: "Get Checks (legacy name)",
@@ -545,8 +867,8 @@ ${text3}`
545
867
 
546
868
  // src/tools/get-specs.ts
547
869
  import { z as z2 } from "zod";
548
- function registerGetSpecs(server2, apiClient2) {
549
- server2.registerTool(
870
+ function registerGetSpecs(server, apiClient2) {
871
+ server.registerTool(
550
872
  "get_specs",
551
873
  {
552
874
  title: "Get Specs",
@@ -631,8 +953,8 @@ function matchGlob(path, pattern) {
631
953
  const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
632
954
  return new RegExp(`^${regex}$`).test(path);
633
955
  }
634
- function registerGetDocs(server2, apiClient2) {
635
- server2.registerTool(
956
+ function registerGetDocs(server, apiClient2) {
957
+ server.registerTool(
636
958
  "get_docs",
637
959
  {
638
960
  title: "Get Docs",
@@ -730,8 +1052,8 @@ Use doc_id for full content, or doc_id + section to fetch one section.`
730
1052
  }
731
1053
 
732
1054
  // src/tools/get-tech-stack.ts
733
- function registerGetTechStack(server2, apiClient2) {
734
- server2.registerTool(
1055
+ function registerGetTechStack(server, apiClient2) {
1056
+ server.registerTool(
735
1057
  "get_tech_stack",
736
1058
  {
737
1059
  title: "Get Tech Stack",
@@ -780,8 +1102,8 @@ ${sections}`
780
1102
 
781
1103
  // src/tools/get-planning-context.ts
782
1104
  import { z as z4 } from "zod";
783
- function registerGetPlanningContext(server2, apiClient2) {
784
- server2.registerTool(
1105
+ function registerGetPlanningContext(server, apiClient2) {
1106
+ server.registerTool(
785
1107
  "get_planning_context",
786
1108
  {
787
1109
  title: "Get Planning Context",
@@ -815,7 +1137,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
815
1137
  ${repos}`);
816
1138
  }
817
1139
  if (ctx.dependencies.length > 0) {
818
- const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
1140
+ const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}${d.provenance === "inferred" ? " (inferred)" : ""}`).join("\n");
819
1141
  sections.push(`## Cross-Repo Dependencies
820
1142
 
821
1143
  ${deps}`);
@@ -902,8 +1224,8 @@ function formatItem(item) {
902
1224
  ${meta.join(" \xB7 ")}` : "") + (item.url ? `
903
1225
  ${item.url}` : "");
904
1226
  }
905
- function registerGetPlanBoard(server2, apiClient2) {
906
- server2.registerTool(
1227
+ function registerGetPlanBoard(server, apiClient2) {
1228
+ server.registerTool(
907
1229
  "get_plan_board",
908
1230
  {
909
1231
  title: "Get Plan Board",
@@ -1147,8 +1469,8 @@ function formatChange(change) {
1147
1469
  briefVerdict(change.states)
1148
1470
  ].join("\n\n");
1149
1471
  }
1150
- function registerGetChange(server2, apiClient2) {
1151
- server2.registerTool(
1472
+ function registerGetChange(server, apiClient2) {
1473
+ server.registerTool(
1152
1474
  "get_change",
1153
1475
  {
1154
1476
  title: "Get Change",
@@ -1515,16 +1837,22 @@ async function hydrate(api, item, meta) {
1515
1837
  }
1516
1838
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
1517
1839
  const budget = maxTokens ?? DEFAULT_BUDGET;
1840
+ const failures = [];
1841
+ const orEmpty = (empty) => (err) => {
1842
+ failures.push(err);
1843
+ return empty;
1844
+ };
1518
1845
  const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
1519
- api.get("/v1/guardrails").catch(() => []),
1520
- api.get("/v1/specs").catch(() => []),
1521
- api.get("/v1/docs").catch(() => ({ items: [] })),
1522
- api.get("/v1/ai/settings/stack").catch(() => []),
1523
- api.get("/v1/standards/rules").catch(() => []),
1846
+ api.get("/v1/guardrails").catch(orEmpty([])),
1847
+ api.get("/v1/specs").catch(orEmpty([])),
1848
+ api.get("/v1/docs").catch(orEmpty({ items: [] })),
1849
+ api.get("/v1/ai/settings/stack").catch(orEmpty([])),
1850
+ api.get("/v1/standards/rules").catch(orEmpty([])),
1524
1851
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
1525
1852
  // blip) gates nothing, it can never hide working context.
1526
1853
  api.get("/v1/doc-repos").catch(() => null)
1527
1854
  ]);
1855
+ if (failures.length === 5) throw failures[0];
1528
1856
  const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
1529
1857
  const guardrails = disabled("fitness") ? [] : rawGuardrails;
1530
1858
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
@@ -1731,18 +2059,19 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
1731
2059
  guardrails: selectedGuardrails.length
1732
2060
  };
1733
2061
  if (sections.length === 0) {
1734
- return { text: "No relevant context found for this task.", stats };
2062
+ const text3 = failures.length ? `No context found for this task, but ${failures.length} of 5 reads failed, so this is not an all-clear: the team's rules may not have been read.` : "No relevant context found for this task.";
2063
+ return { text: text3, stats };
1735
2064
  }
1736
2065
  const footer = `
1737
2066
 
1738
2067
  ---
1739
- _Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
2068
+ _Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + (failures.length > 0 ? ` \xB7 ${failures.length} of 5 reads failed` : "") + `_`;
1740
2069
  return { text: body + footer, stats };
1741
2070
  }
1742
2071
 
1743
2072
  // src/tools/get-context.ts
1744
- function registerGetContext(server2, apiClient2) {
1745
- server2.registerTool(
2073
+ function registerGetContext(server, apiClient2) {
2074
+ server.registerTool(
1746
2075
  "get_context",
1747
2076
  {
1748
2077
  title: "Get Context",
@@ -1770,8 +2099,240 @@ function registerGetContext(server2, apiClient2) {
1770
2099
  );
1771
2100
  }
1772
2101
 
1773
- // src/tools/create-guardrail.ts
2102
+ // src/tools/get-team-setup.ts
2103
+ var HELD_BECAUSE = {
2104
+ "not-plain": `it is not a plain skill: it has frontmatter beyond a name and a description, files outside its own folders, or a command that runs as it loads. The team plugin carries it, and \`${SETUP_COMMAND}\` shows that before the person accepts`,
2105
+ "outside-default-space": "it is outside the default space, so the team plugin does not carry it either. The person can read it in the console, then install it with `npx -y @systemdox/mcp-server install-skill <name>`",
2106
+ "not-checked": "it could not be checked. Ask again later"
2107
+ };
2108
+ function describeSetup(setup) {
2109
+ const lines = [`# Team setup of ${setup.workspace}`, ""];
2110
+ const skills = setup.sessionBrief?.skills ?? [];
2111
+ const held = setup.sessionBrief?.heldSkills ?? [];
2112
+ lines.push(`## Skills a session installs by itself (${skills.length})`, "");
2113
+ if (skills.length) {
2114
+ for (const s of skills) lines.push(`- **${s.name}** version ${s.latestVersion}: ${s.description}`);
2115
+ } else if (!setup.sessionBrief && setup.skills.length) {
2116
+ lines.push(`- ${setup.skills.join(", ")}`);
2117
+ } else {
2118
+ lines.push("None.");
2119
+ }
2120
+ lines.push("");
2121
+ if (held.length) {
2122
+ lines.push(`## Skills that wait for the person (${held.length})`, "");
2123
+ for (const h of held) lines.push(`- **${h.name}**: ${HELD_BECAUSE[h.reason] ?? h.reason}`);
2124
+ lines.push("", "Do not install these yourself: each is the person's to decide.", "");
2125
+ }
2126
+ if (setup.plugin) {
2127
+ lines.push(`## Team plugin`, "", `\`${setup.workspace}-setup\` version ${setup.plugin.version}.`);
2128
+ for (const c of setup.plugin.hookCommands ?? []) lines.push(`- Hook: ${JSON.stringify(c)}`);
2129
+ for (const m of setup.plugin.mcpServers ?? []) {
2130
+ lines.push(`- MCP server ${JSON.stringify(m.name)}: ${JSON.stringify(m.url ?? [m.command, ...m.args ?? []].filter(Boolean).join(" "))}`);
2131
+ }
2132
+ lines.push("");
2133
+ }
2134
+ const parts = Object.entries(setup.parts ?? {});
2135
+ if (parts.length) {
2136
+ lines.push(`## Published parts`, "", ...parts.map(([p, v]) => `- ${p}: version ${v}`), "");
2137
+ }
2138
+ if (setup.memory?.length) {
2139
+ lines.push(`## Team memory`, "", ...setup.memory.map((m) => `- ${m.project}: ${m.fileCount} file(s)`), "");
2140
+ }
2141
+ return lines;
2142
+ }
2143
+ function describeComputer(setup, state) {
2144
+ const lines = ["## This computer", ""];
2145
+ if (!setup.sessionBrief) {
2146
+ return [...lines, "This SystemDox API is too old to compare this computer with the team setup."];
2147
+ }
2148
+ const skills = setup.sessionBrief.skills;
2149
+ const latest = new Map(skills.map((s) => [s.name, s.latestVersion]));
2150
+ lines.push(
2151
+ state.missing.length ? `- Missing: ${state.missing.map((name) => `${name} version ${latest.get(name)}`).join(", ")}` : `- Missing: none of ${skills.length}`
2152
+ );
2153
+ if (state.behind.length) {
2154
+ lines.push(`- Behind: ${state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`).join(", ")}`);
2155
+ }
2156
+ if (state.notFromSystemDox.length) lines.push(`- Not from SystemDox, left alone: ${state.notFromSystemDox.join(", ")}`);
2157
+ if (setup.plugin) {
2158
+ lines.push(
2159
+ state.pluginInstalled ? `- Team plugin: installed${state.pluginVersion ? `, ${state.pluginVersion}` : ""}; the latest is version ${setup.plugin.version}` : "- Team plugin: not installed"
2160
+ );
2161
+ }
2162
+ lines.push("", "## What to do", "");
2163
+ const steps = [];
2164
+ if (state.missing.length) steps.push("Call install_skill for each missing skill, at the version named above. These are plain skills: installing writes files and runs nothing.");
2165
+ if (state.behind.length) {
2166
+ steps.push("Call install_skill for each skill behind, at the latest version named above. If it refuses because files were edited, tell the person and leave them.");
2167
+ }
2168
+ if (hasPersonParts(setup) && !state.pluginInstalled) {
2169
+ steps.push(
2170
+ `Hooks, MCP servers, team instructions and memory need the person: \`${SETUP_COMMAND}\` in their own terminal. Never answer its prompts for them.`
2171
+ );
2172
+ }
2173
+ lines.push(...steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ["Nothing."]);
2174
+ return lines;
2175
+ }
2176
+ function registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 }) {
2177
+ server.registerTool(
2178
+ "get_team_setup",
2179
+ {
2180
+ title: "Get Team Setup",
2181
+ description: localFiles ? "What this workspace's team setup holds (skills, plugin, hooks, MCP servers, memory) and what this computer lacks, with what to do about it. Call it to check whether this computer is set up. Reads only." : "What this workspace's team setup holds (skills, plugin, hooks, MCP servers, memory), and how to check this computer against it. Reads only.",
2182
+ inputSchema: {},
2183
+ annotations: { readOnlyHint: true }
2184
+ },
2185
+ async () => {
2186
+ const read = await readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs: 15e3 });
2187
+ if ("problem" in read) return { content: [{ type: "text", text: read.problem }] };
2188
+ const lines = describeSetup(read.setup);
2189
+ if (localFiles) lines.push(...describeComputer(read.setup, await computerState(read.setup)));
2190
+ else lines.push(CANNOT_SEE);
2191
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2192
+ }
2193
+ );
2194
+ }
2195
+
2196
+ // src/tools/document-graph.ts
1774
2197
  import { z as z8 } from "zod";
2198
+ var DEFAULT_LIMIT2 = 50;
2199
+ var MAX_LIMIT = 200;
2200
+ var DEFAULT_HOPS = 6;
2201
+ var MAX_HOPS = 8;
2202
+ function place(file, line) {
2203
+ return file ? `${file}:${line}` : "made by a person";
2204
+ }
2205
+ function known(provenance, confidence) {
2206
+ return `${provenance} ${confidence}`;
2207
+ }
2208
+ function formatNeighbours(answer) {
2209
+ const lines = [`# ${answer.doc.label}`, `\`${answer.doc.id}\``, ""];
2210
+ if (answer.links.length === 0) lines.push("No links.");
2211
+ for (const link of answer.links) {
2212
+ lines.push(
2213
+ `- ${link.direction} \xB7 ${link.relation} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${link.other.label} \xB7 \`${link.other.id}\` \xB7 ${place(link.file, link.line)}`
2214
+ );
2215
+ }
2216
+ if (answer.truncated) {
2217
+ lines.push("", `Showing ${answer.links.length} of ${answer.total}. Raise limit to see more.`);
2218
+ }
2219
+ if (answer.ambiguous.length > 0) {
2220
+ lines.push("", "## Ambiguous: not drawn, because each could mean several documents");
2221
+ for (const entry of answer.ambiguous) {
2222
+ const candidates = entry.candidates.map((c) => `${c.label} (${c.repo}, \`${c.id}\`)`);
2223
+ lines.push(`- ${entry.reference} at ${place(entry.file, entry.line)}: ${candidates.join("; ")}`);
2224
+ }
2225
+ }
2226
+ return lines.join("\n");
2227
+ }
2228
+ function formatPath(answer, maxHops) {
2229
+ if (!answer.found) return `No path within ${maxHops} hops.`;
2230
+ const lines = answer.steps.map((step, index) => {
2231
+ const arrow = step.reversed ? "<-" : "->";
2232
+ return `${index + 1}. ${step.from.label} ${arrow} ${step.relation} ${arrow} ${step.to.label} \xB7 ${known(step.provenance, step.confidence)} \xB7 ${place(step.file, step.line)}`;
2233
+ });
2234
+ return [...lines, "", `Path confidence: ${answer.confidence}`].join("\n");
2235
+ }
2236
+ function formatStats(stats, repoLinks) {
2237
+ const { declared, extracted, inferred, ambiguous } = stats.links;
2238
+ const lines = [
2239
+ `# The graph: ${stats.nodeCount} documents, ${stats.edgeCount} links`,
2240
+ "",
2241
+ `Links: ${declared} declared \xB7 ${extracted} extracted \xB7 ${inferred} inferred \xB7 ${ambiguous} ambiguous (not drawn)`,
2242
+ "",
2243
+ "## Repositories"
2244
+ ];
2245
+ for (const repo of stats.repos) {
2246
+ const state = repo.behind ? `BEHIND: pushed ${repo.headCommit || "unknown"} at ${repo.pushedAt}, index at ${repo.indexedCommit || "unknown"}` : `index at ${repo.indexedCommit || "unknown"}`;
2247
+ lines.push(`- ${repo.repo}: ${repo.documents} documents, ${repo.links} links, ${state}`);
2248
+ }
2249
+ if (repoLinks.length > 0) {
2250
+ lines.push("", "## Repository links");
2251
+ for (const link of repoLinks) {
2252
+ lines.push(
2253
+ `- ${link.source} ${link.relation} ${link.target} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${place(link.file, link.line)}`
2254
+ );
2255
+ }
2256
+ }
2257
+ return lines.join("\n");
2258
+ }
2259
+ function refused(err) {
2260
+ const status = err.status;
2261
+ if (status !== 404 && status !== 400) throw err;
2262
+ return {
2263
+ content: [{ type: "text", text: err.message }],
2264
+ isError: true
2265
+ };
2266
+ }
2267
+ function registerDocumentGraph(server, apiClient2) {
2268
+ server.registerTool(
2269
+ "get_document_links",
2270
+ {
2271
+ description: "What one document links to and from, strongest first. Each link says how it is known: declared (a person made it), extracted (the file says so), or inferred (a reading that could be wrong), with a confidence and the file and line it came from. A reference that could mean several documents is listed as ambiguous with its candidates, and is never drawn as a link. doc_id is the id get_docs returns. Read-only.",
2272
+ inputSchema: {
2273
+ doc_id: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2274
+ direction: z8.enum(["out", "in", "both"]).optional().describe("Default both"),
2275
+ relation: z8.enum(["links_to", "cites", "mentions", "satisfies", "references"]).optional().describe("Only links of this kind"),
2276
+ limit: z8.number().int().min(1).max(MAX_LIMIT).optional().describe(`Most links to show (default ${DEFAULT_LIMIT2})`)
2277
+ },
2278
+ annotations: { readOnlyHint: true }
2279
+ },
2280
+ async ({ doc_id, direction, relation, limit }) => {
2281
+ const params = { doc: doc_id, limit: String(limit ?? DEFAULT_LIMIT2) };
2282
+ if (direction) params.direction = direction;
2283
+ if (relation) params.relation = relation;
2284
+ try {
2285
+ const answer = await apiClient2.get("/v1/graph/neighbours", params);
2286
+ return { content: [{ type: "text", text: formatNeighbours(answer) }] };
2287
+ } catch (err) {
2288
+ return refused(err);
2289
+ }
2290
+ }
2291
+ );
2292
+ server.registerTool(
2293
+ "trace_document_path",
2294
+ {
2295
+ description: "The shortest way two documents connect, walking links in either direction. Each step says how it is known, and the path is only as strong as its weakest step. Read-only.",
2296
+ inputSchema: {
2297
+ from: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2298
+ to: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2299
+ max_hops: z8.number().int().min(1).max(MAX_HOPS).optional().describe(`Longest path to look for (default ${DEFAULT_HOPS})`)
2300
+ },
2301
+ annotations: { readOnlyHint: true }
2302
+ },
2303
+ async ({ from, to, max_hops }) => {
2304
+ const hops = max_hops ?? DEFAULT_HOPS;
2305
+ try {
2306
+ const answer = await apiClient2.get("/v1/graph/path", {
2307
+ from,
2308
+ to,
2309
+ maxHops: String(hops)
2310
+ });
2311
+ return { content: [{ type: "text", text: formatPath(answer, hops) }] };
2312
+ } catch (err) {
2313
+ return refused(err);
2314
+ }
2315
+ }
2316
+ );
2317
+ server.registerTool(
2318
+ "get_graph_stats",
2319
+ {
2320
+ description: "The state of the graph: links counted by how they are known, each repository's freshness (marked BEHIND when a push has not finished indexing), and how repositories depend on each other, from the files that declare it. Read-only.",
2321
+ inputSchema: {},
2322
+ annotations: { readOnlyHint: true }
2323
+ },
2324
+ async () => {
2325
+ const [stats, repos] = await Promise.all([
2326
+ apiClient2.getFresh("/v1/graph/stats"),
2327
+ apiClient2.getFresh("/v1/graph/repos")
2328
+ ]);
2329
+ return { content: [{ type: "text", text: formatStats(stats, repos.links) }] };
2330
+ }
2331
+ );
2332
+ }
2333
+
2334
+ // src/tools/create-guardrail.ts
2335
+ import { z as z9 } from "zod";
1775
2336
  var DESCRIPTION2 = "Create a new check in SystemDox. Use this when you discover a pattern that should be enforced (banned pattern, required pattern, coding rule).";
1776
2337
  var CATEGORIES = [
1777
2338
  "imports",
@@ -1784,15 +2345,15 @@ var CATEGORIES = [
1784
2345
  "data-access"
1785
2346
  ];
1786
2347
  var inputSchema2 = () => ({
1787
- title: z8.string().describe("Short title for the check"),
1788
- description: z8.string().describe("Detailed description of what the check enforces and why"),
1789
- severity: z8.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
1790
- category: z8.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
1791
- pattern: z8.string().optional().describe("Bad code pattern that should be avoided"),
1792
- fix: z8.string().optional().describe("Correct code pattern to use instead"),
1793
- fitness_test: z8.string().optional().describe("Shell command to detect violations in CI")
2348
+ title: z9.string().describe("Short title for the check"),
2349
+ description: z9.string().describe("Detailed description of what the check enforces and why"),
2350
+ severity: z9.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
2351
+ category: z9.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
2352
+ pattern: z9.string().optional().describe("Bad code pattern that should be avoided"),
2353
+ fix: z9.string().optional().describe("Correct code pattern to use instead"),
2354
+ fitness_test: z9.string().optional().describe("Shell command to detect violations in CI")
1794
2355
  });
1795
- function registerCreateGuardrail(server2, apiClient2) {
2356
+ function registerCreateGuardrail(server, apiClient2) {
1796
2357
  const handler = async ({
1797
2358
  title,
1798
2359
  description,
@@ -1829,7 +2390,7 @@ function registerCreateGuardrail(server2, apiClient2) {
1829
2390
  ]
1830
2391
  };
1831
2392
  };
1832
- server2.registerTool(
2393
+ server.registerTool(
1833
2394
  "create_check",
1834
2395
  {
1835
2396
  title: "Create Check",
@@ -1841,7 +2402,7 @@ function registerCreateGuardrail(server2, apiClient2) {
1841
2402
  },
1842
2403
  handler
1843
2404
  );
1844
- server2.registerTool(
2405
+ server.registerTool(
1845
2406
  "create_guardrail",
1846
2407
  {
1847
2408
  title: "Create Check (legacy name)",
@@ -1856,20 +2417,20 @@ function registerCreateGuardrail(server2, apiClient2) {
1856
2417
  }
1857
2418
 
1858
2419
  // src/tools/update-spec.ts
1859
- import { z as z9 } from "zod";
1860
- function registerUpdateSpec(server2, apiClient2) {
1861
- server2.registerTool(
2420
+ import { z as z10 } from "zod";
2421
+ function registerUpdateSpec(server, apiClient2) {
2422
+ server.registerTool(
1862
2423
  "update_spec",
1863
2424
  {
1864
2425
  title: "Update Spec",
1865
2426
  description: "Update an existing SystemDox spec's proposal, tasks, or requirements. Only draft specs can be modified \u2014 published specs must be versioned first.",
1866
2427
  inputSchema: {
1867
- draft_id: z9.string().describe("ID of the spec to update"),
1868
- section: z9.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
1869
- content: z9.string().describe(
2428
+ draft_id: z10.string().describe("ID of the spec to update"),
2429
+ section: z10.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
2430
+ content: z10.string().describe(
1870
2431
  "New markdown content for the section. For requirements, provide the full requirement content."
1871
2432
  ),
1872
- requirement_name: z9.string().optional().describe(
2433
+ requirement_name: z10.string().optional().describe(
1873
2434
  "Name of the requirement (required when section is 'requirements')"
1874
2435
  )
1875
2436
  },
@@ -1919,19 +2480,19 @@ function registerUpdateSpec(server2, apiClient2) {
1919
2480
  }
1920
2481
 
1921
2482
  // src/tools/report-decision.ts
1922
- import { z as z10 } from "zod";
1923
- function registerReportDecision(server2, apiClient2) {
1924
- server2.registerTool(
2483
+ import { z as z11 } from "zod";
2484
+ function registerReportDecision(server, apiClient2) {
2485
+ server.registerTool(
1925
2486
  "report_decision",
1926
2487
  {
1927
2488
  title: "Report Decision",
1928
2489
  description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
1929
2490
  inputSchema: {
1930
- title: z10.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
1931
- context: z10.string().describe("What prompted this decision \u2014 the problem or constraint"),
1932
- decision: z10.string().describe("What was decided and why"),
1933
- consequences: z10.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
1934
- related_files: z10.array(z10.string()).optional().describe("File paths related to this decision")
2491
+ title: z11.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
2492
+ context: z11.string().describe("What prompted this decision \u2014 the problem or constraint"),
2493
+ decision: z11.string().describe("What was decided and why"),
2494
+ consequences: z11.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
2495
+ related_files: z11.array(z11.string()).optional().describe("File paths related to this decision")
1935
2496
  },
1936
2497
  annotations: {
1937
2498
  readOnlyHint: false
@@ -1982,7 +2543,7 @@ Status: draft \u2014 review in SystemDox before publishing.`
1982
2543
  }
1983
2544
 
1984
2545
  // src/tools/get-project-rules.ts
1985
- import { z as z11 } from "zod";
2546
+ import { z as z12 } from "zod";
1986
2547
  var CATEGORY_LABELS = {
1987
2548
  deployment: "Deployment",
1988
2549
  architecture: "Architecture",
@@ -1991,14 +2552,14 @@ var CATEGORY_LABELS = {
1991
2552
  process: "Process",
1992
2553
  other: "Other"
1993
2554
  };
1994
- function registerGetProjectRules(server2, apiClient2) {
1995
- server2.registerTool(
2555
+ function registerGetProjectRules(server, apiClient2) {
2556
+ server.registerTool(
1996
2557
  "get_project_rules",
1997
2558
  {
1998
2559
  title: "Get Project Rules",
1999
2560
  description: "Retrieve project rules defined in SystemDox Standards. These are team-defined principles covering deployment, architecture, security, testing, and process. Filter by category to get only relevant rules.",
2000
2561
  inputSchema: {
2001
- category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
2562
+ category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
2002
2563
  }
2003
2564
  },
2004
2565
  async ({ category }) => {
@@ -2050,18 +2611,18 @@ function registerGetProjectRules(server2, apiClient2) {
2050
2611
  }
2051
2612
 
2052
2613
  // src/tools/create-project-rule.ts
2053
- import { z as z12 } from "zod";
2054
- function registerCreateProjectRule(server2, apiClient2) {
2055
- server2.registerTool(
2614
+ import { z as z13 } from "zod";
2615
+ function registerCreateProjectRule(server, apiClient2) {
2616
+ server.registerTool(
2056
2617
  "create_project_rule",
2057
2618
  {
2058
2619
  title: "Create Project Rule",
2059
2620
  description: "Create a new project rule in SystemDox Standards. Use this when you discover a team principle or convention that should be enforced across the project.",
2060
2621
  inputSchema: {
2061
- text: z12.string().describe(
2622
+ text: z13.string().describe(
2062
2623
  "The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
2063
2624
  ),
2064
- category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
2625
+ category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
2065
2626
  },
2066
2627
  annotations: {
2067
2628
  readOnlyHint: false
@@ -2086,8 +2647,8 @@ function registerCreateProjectRule(server2, apiClient2) {
2086
2647
  }
2087
2648
 
2088
2649
  // src/tools/get-prompt-templates.ts
2089
- function registerGetPromptTemplates(server2, apiClient2) {
2090
- server2.registerTool(
2650
+ function registerGetPromptTemplates(server, apiClient2) {
2651
+ server.registerTool(
2091
2652
  "get_prompt_templates",
2092
2653
  {
2093
2654
  title: "Get Prompt Templates",
@@ -2132,17 +2693,17 @@ ${text3}`
2132
2693
  }
2133
2694
 
2134
2695
  // src/tools/create-prompt-template.ts
2135
- import { z as z13 } from "zod";
2136
- function registerCreatePromptTemplate(server2, apiClient2) {
2137
- server2.registerTool(
2696
+ import { z as z14 } from "zod";
2697
+ function registerCreatePromptTemplate(server, apiClient2) {
2698
+ server.registerTool(
2138
2699
  "create_prompt_template",
2139
2700
  {
2140
2701
  title: "Create Prompt Template",
2141
2702
  description: "Create a new reusable prompt template in SystemDox Standards. Use this to store preconfigured prompts for common AI tasks like code reviews, documentation generation, or analysis frameworks.",
2142
2703
  inputSchema: {
2143
- name: z13.string().describe("Short name for the template"),
2144
- description: z13.string().optional().describe("Brief description of when to use this template"),
2145
- prompt: z13.string().describe("The full prompt text that Gen AI will use when generating content")
2704
+ name: z14.string().describe("Short name for the template"),
2705
+ description: z14.string().optional().describe("Brief description of when to use this template"),
2706
+ prompt: z14.string().describe("The full prompt text that Gen AI will use when generating content")
2146
2707
  },
2147
2708
  annotations: {
2148
2709
  readOnlyHint: false
@@ -2171,205 +2732,9 @@ function registerCreatePromptTemplate(server2, apiClient2) {
2171
2732
  }
2172
2733
 
2173
2734
  // src/tools/skills.ts
2174
- import { z as z14 } from "zod";
2735
+ import { z as z15 } from "zod";
2175
2736
  import { homedir as homedir3 } from "os";
2176
2737
  import { basename, resolve as resolve2 } from "path";
2177
-
2178
- // src/lib/skills.ts
2179
- import { createHash } from "crypto";
2180
- import { lstat, mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2181
- import { existsSync } from "fs";
2182
- import { homedir as homedir2 } from "os";
2183
- import { dirname as dirname2, join as join2, relative, resolve, sep } from "path";
2184
- var MANIFEST_FILE = ".systemdox-skill.json";
2185
- var ENTRY_FILE = "SKILL.md";
2186
- var MAX_FILES = 100;
2187
- var MAX_FILE_BYTES = 256 * 1024;
2188
- var MAX_TOTAL_BYTES = 1024 * 1024;
2189
- var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2190
- var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
2191
- var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
2192
- var SkillInstallRefused = class extends Error {
2193
- constructor(dest, edited) {
2194
- super(
2195
- `${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
2196
- );
2197
- this.dest = dest;
2198
- this.edited = edited;
2199
- }
2200
- dest;
2201
- edited;
2202
- };
2203
- function sha256(content) {
2204
- return createHash("sha256").update(content, "utf8").digest("hex");
2205
- }
2206
- function isValidName(name) {
2207
- return name.length <= 64 && NAME_RE.test(name);
2208
- }
2209
- function isValidPath(path) {
2210
- return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
2211
- }
2212
- function frontmatterName(skillMd) {
2213
- const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
2214
- if (!block) return void 0;
2215
- const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
2216
- return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
2217
- }
2218
- function defaultSkillDir(name) {
2219
- return join2(homedir2(), ".claude", "skills", name);
2220
- }
2221
- async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
2222
- for (const entry of await readdir(dir, { withFileTypes: true })) {
2223
- if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
2224
- const full = join2(dir, entry.name);
2225
- const rel = relative(root, full).split(sep).join("/");
2226
- if (entry.isSymbolicLink()) out.links.push(rel);
2227
- else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
2228
- else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
2229
- else if (entry.isFile()) out.files.push(rel);
2230
- }
2231
- out.files.sort();
2232
- return out;
2233
- }
2234
- async function readTextFiles(root, paths, what) {
2235
- if (paths.length > MAX_FILES) {
2236
- throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
2237
- }
2238
- const decoder = new TextDecoder("utf-8", { fatal: true });
2239
- const files = [];
2240
- let total = 0;
2241
- for (const path of paths) {
2242
- if (!isValidPath(path)) {
2243
- throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
2244
- }
2245
- const raw = await readFile2(join2(root, ...path.split("/")));
2246
- if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
2247
- if (raw.length > MAX_FILE_BYTES) {
2248
- throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
2249
- }
2250
- total += raw.length;
2251
- if (total > MAX_TOTAL_BYTES) {
2252
- throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
2253
- }
2254
- let content;
2255
- try {
2256
- content = decoder.decode(raw);
2257
- } catch {
2258
- throw new Error(`${path} is not UTF-8 text`);
2259
- }
2260
- files.push({ path, content });
2261
- }
2262
- return files;
2263
- }
2264
- async function readSkillDirectory(directory) {
2265
- const root = resolve(directory);
2266
- if (!existsSync(join2(root, ENTRY_FILE))) {
2267
- throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
2268
- }
2269
- const walked = await walk(root, { skipHidden: true });
2270
- if (walked.links.length) {
2271
- throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
2272
- }
2273
- const files = await readTextFiles(root, walked.files, "a skill");
2274
- const skillMd = files.find((f) => f.path === ENTRY_FILE);
2275
- const name = frontmatterName(skillMd.content);
2276
- if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
2277
- return { name, files, skipped: walked.hidden };
2278
- }
2279
- function within(dest, path) {
2280
- if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
2281
- const full = resolve(dest, ...path.split("/"));
2282
- if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
2283
- return full;
2284
- }
2285
- async function readManifest(dest) {
2286
- try {
2287
- return JSON.parse(await readFile2(join2(dest, MANIFEST_FILE), "utf8"));
2288
- } catch {
2289
- return void 0;
2290
- }
2291
- }
2292
- async function installSkill(version, dest, { overwrite = false } = {}) {
2293
- if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
2294
- const incoming = new Map(version.files.map((f) => [f.path, f]));
2295
- for (const f of version.files) within(dest, f.path);
2296
- if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
2297
- throw new Error(`${dest} is a link: install into a real folder`);
2298
- }
2299
- const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
2300
- if (walked.links.length) {
2301
- throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
2302
- }
2303
- const present = walked.files;
2304
- const manifest = await readManifest(dest);
2305
- const known = Object.fromEntries(
2306
- Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
2307
- );
2308
- const edited = [];
2309
- const untracked = [];
2310
- for (const path of present) {
2311
- const content = await readFile2(join2(dest, ...path.split("/")), "utf8");
2312
- if (Object.hasOwn(known, path)) {
2313
- if (sha256(content) !== known[path]) edited.push(path);
2314
- } else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
2315
- if (incoming.has(path) || !manifest) edited.push(path);
2316
- else untracked.push(path);
2317
- }
2318
- }
2319
- const blocking = edited.filter((p) => incoming.has(p) || !manifest);
2320
- if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
2321
- const written = [];
2322
- for (const f of version.files) {
2323
- const full = within(dest, f.path);
2324
- await mkdir2(dirname2(full), { recursive: true });
2325
- await writeFile2(full, f.content, "utf8");
2326
- written.push(f.path);
2327
- }
2328
- const removed = [];
2329
- const keptEdited = [];
2330
- for (const path of Object.keys(known)) {
2331
- if (incoming.has(path) || !present.includes(path)) continue;
2332
- if (edited.includes(path)) {
2333
- keptEdited.push(path);
2334
- continue;
2335
- }
2336
- await rm2(within(dest, path));
2337
- removed.push(path);
2338
- }
2339
- const next = {
2340
- name: version.name,
2341
- version: version.version,
2342
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2343
- files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
2344
- };
2345
- await writeFile2(join2(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
2346
- return {
2347
- dest,
2348
- version: version.version,
2349
- written,
2350
- removed,
2351
- keptUntracked: untracked.filter((p) => !incoming.has(p)),
2352
- keptEdited
2353
- };
2354
- }
2355
- function fenceFor(text3) {
2356
- const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
2357
- return "`".repeat(Math.max(3, longest + 1));
2358
- }
2359
- function describeInstall(name, r) {
2360
- const lines = [
2361
- `Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
2362
- ];
2363
- if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
2364
- if (r.keptEdited.length) {
2365
- lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
2366
- }
2367
- if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
2368
- lines.push("Start a new Claude Code session to load it.");
2369
- return lines.join("\n");
2370
- }
2371
-
2372
- // src/tools/skills.ts
2373
2738
  var SKILLS = "/v1/standards/skills";
2374
2739
  function expandHome(path) {
2375
2740
  return path.replace(/^~(?=$|[\\/])/, homedir3());
@@ -2377,25 +2742,25 @@ function expandHome(path) {
2377
2742
  function text(t) {
2378
2743
  return { content: [{ type: "text", text: t }] };
2379
2744
  }
2380
- function registerSkillTools(server2, apiClient2, { localFiles }) {
2381
- const fileSchema = z14.object({
2382
- path: z14.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
2383
- content: z14.string().describe("The file's full text, exactly as it is")
2745
+ function registerSkillTools(server, apiClient2, { localFiles }) {
2746
+ const fileSchema = z15.object({
2747
+ path: z15.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
2748
+ content: z15.string().describe("The file's full text, exactly as it is")
2384
2749
  });
2385
- server2.registerTool(
2750
+ server.registerTool(
2386
2751
  "publish_skill",
2387
2752
  {
2388
2753
  title: "Publish Skill",
2389
2754
  description: localFiles ? "Publish a Claude Code skill folder (SKILL.md plus its references and scripts) to SystemDox so it can be installed on any computer. Pass `directory` (e.g. ~/.claude/skills/<name>): the server reads the files itself, byte for byte. Publishing a name again makes a new version. Never retype the files into `files` when the folder is on this computer." : "Publish a Claude Code skill (SKILL.md plus its references and scripts) to SystemDox so it can be installed on any computer. Pass every file of the folder in `files`, exactly as it is. Publishing a name again makes a new version.",
2390
2755
  inputSchema: {
2391
2756
  ...localFiles ? {
2392
- directory: z14.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2757
+ directory: z15.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2393
2758
  } : {},
2394
- files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
2395
- name: z14.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
2396
- spaceId: z14.string().optional().describe("Space for a NEW skill (default: your default space)"),
2397
- note: z14.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
2398
- baseVersion: z14.number().int().positive().optional().describe(
2759
+ files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
2760
+ name: z15.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
2761
+ spaceId: z15.string().optional().describe("Space for a NEW skill (default: your default space)"),
2762
+ note: z15.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
2763
+ baseVersion: z15.number().int().positive().optional().describe(
2399
2764
  "The version these files were edited from. When given, the publish is refused (SYD-K017) if someone published a newer version since, instead of replacing their changes"
2400
2765
  )
2401
2766
  },
@@ -2433,7 +2798,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2433
2798
  );
2434
2799
  }
2435
2800
  );
2436
- server2.registerTool(
2801
+ server.registerTool(
2437
2802
  "list_skills",
2438
2803
  {
2439
2804
  title: "List Skills",
@@ -2455,16 +2820,16 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2455
2820
  );
2456
2821
  }
2457
2822
  );
2458
- server2.registerTool(
2823
+ server.registerTool(
2459
2824
  "install_skill",
2460
2825
  {
2461
2826
  title: "Install Skill",
2462
2827
  description: localFiles ? "Install a skill published to SystemDox onto this computer (default ~/.claude/skills/<name>/). Refuses, naming the files, when a file there was edited since it was installed, unless overwrite is true. Never runs anything." : "Fetch a skill published to SystemDox so you can install it: returns every file and the folder to write them into (default ~/.claude/skills/<name>/). Write each file exactly, then the manifest it shows.",
2463
2828
  inputSchema: {
2464
- name: z14.string().describe("The skill's name"),
2465
- version: z14.number().int().positive().optional().describe("A version number (default: the latest)"),
2466
- target: z14.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
2467
- ...localFiles ? { overwrite: z14.boolean().optional().describe("Replace files edited since they were installed") } : {}
2829
+ name: z15.string().describe("The skill's name"),
2830
+ version: z15.number().int().positive().optional().describe("A version number (default: the latest)"),
2831
+ target: z15.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
2832
+ ...localFiles ? { overwrite: z15.boolean().optional().describe("Replace files edited since they were installed") } : {}
2468
2833
  },
2469
2834
  annotations: { readOnlyHint: !localFiles }
2470
2835
  },
@@ -2510,11 +2875,11 @@ ${body}`
2510
2875
  }
2511
2876
 
2512
2877
  // src/tools/team-setup.ts
2513
- import { z as z15 } from "zod";
2514
- import { existsSync as existsSync3 } from "fs";
2878
+ import { z as z16 } from "zod";
2879
+ import { existsSync as existsSync4 } from "fs";
2515
2880
  import { lstat as lstat2 } from "fs/promises";
2516
2881
  import { homedir as homedir4 } from "os";
2517
- import { join as join4, resolve as resolve4 } from "path";
2882
+ import { join as join5, resolve as resolve4 } from "path";
2518
2883
 
2519
2884
  // src/lib/person-values.ts
2520
2885
  var PHONE = "phone";
@@ -2595,9 +2960,9 @@ var PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
2595
2960
  var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2596
2961
 
2597
2962
  // src/sync-memory.ts
2598
- import { existsSync as existsSync2 } from "fs";
2599
- import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
2600
- import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
2963
+ import { existsSync as existsSync3 } from "fs";
2964
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
2965
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
2601
2966
 
2602
2967
  // src/lib/memory-sync.ts
2603
2968
  import { createHash as createHash2 } from "crypto";
@@ -2877,7 +3242,7 @@ function refusal(e) {
2877
3242
  const { status, code } = e;
2878
3243
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
2879
3244
  }
2880
- var toPath = (root, rel) => join3(root, ...rel.split("/"));
3245
+ var toPath = (root, rel) => join4(root, ...rel.split("/"));
2881
3246
  async function readFolder(root) {
2882
3247
  const files = {};
2883
3248
  const names = /* @__PURE__ */ new Set();
@@ -2885,14 +3250,14 @@ async function readFolder(root) {
2885
3250
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2886
3251
  if (e.name.startsWith(".")) continue;
2887
3252
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
2888
- if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
3253
+ if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
2889
3254
  else if (e.isFile()) {
2890
3255
  names.add(rel.toLowerCase());
2891
- if (e.name.endsWith(".md")) files[rel] = await readFile3(join3(dir, e.name), "utf8");
3256
+ if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
2892
3257
  }
2893
3258
  }
2894
3259
  }
2895
- if (existsSync2(root)) await walk2(root, "");
3260
+ if (existsSync3(root)) await walk2(root, "");
2896
3261
  return { files, names };
2897
3262
  }
2898
3263
  async function pullTeam(client, ws, project) {
@@ -3101,11 +3466,11 @@ async function people(opts, ws, files, report, hint) {
3101
3466
  report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
3102
3467
  break;
3103
3468
  }
3104
- const refused = result.refused[0];
3105
- if (refused?.code === "SYD-K021") {
3106
- out(` ${refused.detail}. Add them in Staff first, or answer p to file them.`);
3469
+ const refused2 = result.refused[0];
3470
+ if (refused2?.code === "SYD-K021") {
3471
+ out(` ${refused2.detail}. Add them in Staff first, or answer p to file them.`);
3107
3472
  choices = choices.filter((c) => c !== "m");
3108
- } else if (refused) out(` ${refused.detail}`);
3473
+ } else if (refused2) out(` ${refused2.detail}`);
3109
3474
  }
3110
3475
  }
3111
3476
  for (let i = 0; i < filed.length; i += 50) {
@@ -3249,7 +3614,7 @@ async function syncMemory(opts) {
3249
3614
  own.memory.length ? `${ws} has ${own.memory.length} team memories (${own.memory.map((m) => m.project).join(", ")}): name one with --project.` : `${ws} has no team memory yet: name the project to start with --project.`
3250
3615
  );
3251
3616
  }
3252
- const dir = resolve3(opts.dir ?? join3(workspaceDir(ws), "memory", project));
3617
+ const dir = resolve3(opts.dir ?? join4(workspaceDir(ws), "memory", project));
3253
3618
  const run2 = { ...opts, dir };
3254
3619
  const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
3255
3620
  const report = new Report();
@@ -3399,21 +3764,21 @@ async function readPartDirectory(part, directory) {
3399
3764
  let present = true;
3400
3765
  for (let i = 1; i <= segments.length; i++) {
3401
3766
  const rel = segments.slice(0, i).join("/");
3402
- if (await isLink(join4(root, ...segments.slice(0, i)))) {
3767
+ if (await isLink(join5(root, ...segments.slice(0, i)))) {
3403
3768
  links.push(rel);
3404
3769
  present = false;
3405
3770
  break;
3406
3771
  }
3407
3772
  }
3408
- if (present && existsSync3(join4(root, ...segments))) paths.push(path);
3773
+ if (present && existsSync4(join5(root, ...segments))) paths.push(path);
3409
3774
  }
3410
3775
  for (const { dir, md } of layout.folders) {
3411
- const full = join4(root, dir);
3776
+ const full = join5(root, dir);
3412
3777
  if (await isLink(full)) {
3413
3778
  links.push(dir);
3414
3779
  continue;
3415
3780
  }
3416
- if (!existsSync3(full)) continue;
3781
+ if (!existsSync4(full)) continue;
3417
3782
  const walked = await walk(full, { skipHidden: true });
3418
3783
  links.push(...walked.links.map((p) => `${dir}/${p}`));
3419
3784
  leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
@@ -3452,24 +3817,24 @@ function describePerson(id, person) {
3452
3817
  );
3453
3818
  return lines.join("\n");
3454
3819
  }
3455
- function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3456
- const fileSchema = z15.object({
3457
- path: z15.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
3458
- content: z15.string().describe("The file's full text, exactly as it is")
3820
+ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
3821
+ const fileSchema = z16.object({
3822
+ path: z16.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
3823
+ content: z16.string().describe("The file's full text, exactly as it is")
3459
3824
  });
3460
- server2.registerTool(
3825
+ server.registerTool(
3461
3826
  "publish_setup_part",
3462
3827
  {
3463
3828
  title: "Publish Setup Part",
3464
3829
  description: "Publish one part of your team's Claude Code setup to SystemDox: `hooks` (hooks/hooks.json and the scripts/ it calls), `mcp` (.mcp.json), `agents` (agents/*.md), `commands` (commands/*.md) or `instructions` (CLAUDE.md). Every part but instructions makes a new version of the team plugin, which each member accepts at their next update. Publishing hooks, mcp, agents or commands needs the admin role. " + (localFiles ? "Pass `directory`: the server reads only the files the part accepts, byte for byte, and refuses a link, a binary file or a file too large before anything is sent. Never retype the files into `files` when the folder is on this computer." : "Pass every file of the part in `files`, exactly as it is."),
3465
3830
  inputSchema: {
3466
- part: z15.enum(PARTS2).describe("Which part of the setup"),
3831
+ part: z16.enum(PARTS2).describe("Which part of the setup"),
3467
3832
  ...localFiles ? {
3468
- directory: z15.string().optional().describe(
3833
+ directory: z16.string().optional().describe(
3469
3834
  "The folder on this computer: for hooks, the one holding hooks/hooks.json and scripts/; for mcp, the one holding .mcp.json; for instructions, the one holding CLAUDE.md; for agents or commands, the one holding agents/ or commands/"
3470
3835
  )
3471
3836
  } : {},
3472
- files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3837
+ files: z16.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3473
3838
  },
3474
3839
  annotations: { readOnlyHint: false }
3475
3840
  },
@@ -3499,13 +3864,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3499
3864
  return text2(lines.join("\n"));
3500
3865
  }
3501
3866
  );
3502
- server2.registerTool(
3867
+ server.registerTool(
3503
3868
  "resolve_person",
3504
3869
  {
3505
3870
  title: "Resolve Person",
3506
3871
  description: "Read back a person that a team memory names by identifier, written as `con_\u2026` (label), `ctc_\u2026` or `usr_\u2026`: their label, name, phone number and email address, from the vault that holds them. The details are for this session only: never write them into a memory file; the memory keeps the identifier. Writes nothing.",
3507
3872
  inputSchema: {
3508
- id: z15.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
3873
+ id: z16.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
3509
3874
  },
3510
3875
  annotations: { readOnlyHint: true }
3511
3876
  },
@@ -3516,14 +3881,14 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3516
3881
  }
3517
3882
  );
3518
3883
  if (!localFiles) return;
3519
- server2.registerTool(
3884
+ server.registerTool(
3520
3885
  "sync_memory",
3521
3886
  {
3522
3887
  title: "Sync Memory",
3523
3888
  description: `Sync one project's team memory on this computer with SystemDox: pull the files the team changed, push the ones changed here, keep both texts of a file changed on both sides. It never answers for a person: a file that still holds a phone number, an email address or a home-folder path is skipped and listed, with the command for the member to run in their own terminal (\`${SYNC_MEMORY_COMMAND}\`). Pass dryRun to see what it would do without writing or sending anything.`,
3524
3889
  inputSchema: {
3525
- project: z15.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
3526
- dryRun: z15.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
3890
+ project: z16.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
3891
+ dryRun: z16.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
3527
3892
  },
3528
3893
  annotations: { readOnlyHint: false }
3529
3894
  },
@@ -3541,44 +3906,56 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3541
3906
  }
3542
3907
 
3543
3908
  // src/server.ts
3544
- var SERVER_VERSION = true ? "0.1.49" : "0.0.0-dev";
3545
- function createServer(apiClient2, { localFiles = false } = {}) {
3546
- const server2 = new McpServer({
3547
- name: "systemdox-mcp-server",
3548
- version: SERVER_VERSION
3549
- });
3550
- registerGetGuardrails(server2, apiClient2);
3551
- registerGetSpecs(server2, apiClient2);
3552
- registerGetDocs(server2, apiClient2);
3553
- registerGetTechStack(server2, apiClient2);
3554
- registerGetPlanningContext(server2, apiClient2);
3555
- registerGetPlanBoard(server2, apiClient2);
3556
- registerGetChange(server2, apiClient2);
3557
- registerGetContext(server2, apiClient2);
3558
- registerGetProjectRules(server2, apiClient2);
3559
- registerGetPromptTemplates(server2, apiClient2);
3560
- registerCreateGuardrail(server2, apiClient2);
3561
- registerUpdateSpec(server2, apiClient2);
3562
- registerReportDecision(server2, apiClient2);
3563
- registerCreateProjectRule(server2, apiClient2);
3564
- registerCreatePromptTemplate(server2, apiClient2);
3565
- registerSkillTools(server2, apiClient2, { localFiles });
3566
- registerTeamSetupTools(server2, apiClient2, { localFiles });
3567
- return server2;
3909
+ var SERVER_VERSION = true ? "0.1.50" : "0.0.0-dev";
3910
+ function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
3911
+ const server = new McpServer(
3912
+ {
3913
+ name: "systemdox-mcp-server",
3914
+ version: SERVER_VERSION
3915
+ },
3916
+ instructions ? { instructions } : void 0
3917
+ );
3918
+ registerGetGuardrails(server, apiClient2);
3919
+ registerGetSpecs(server, apiClient2);
3920
+ registerGetDocs(server, apiClient2);
3921
+ registerGetTechStack(server, apiClient2);
3922
+ registerGetPlanningContext(server, apiClient2);
3923
+ registerGetPlanBoard(server, apiClient2);
3924
+ registerGetChange(server, apiClient2);
3925
+ registerGetContext(server, apiClient2);
3926
+ registerGetProjectRules(server, apiClient2);
3927
+ registerGetPromptTemplates(server, apiClient2);
3928
+ registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 });
3929
+ registerDocumentGraph(server, apiClient2);
3930
+ registerCreateGuardrail(server, apiClient2);
3931
+ registerUpdateSpec(server, apiClient2);
3932
+ registerReportDecision(server, apiClient2);
3933
+ registerCreateProjectRule(server, apiClient2);
3934
+ registerCreatePromptTemplate(server, apiClient2);
3935
+ registerSkillTools(server, apiClient2, { localFiles });
3936
+ registerTeamSetupTools(server, apiClient2, { localFiles });
3937
+ return server;
3568
3938
  }
3569
3939
 
3570
3940
  // src/index.ts
3571
3941
  var config;
3942
+ var keyProblem;
3572
3943
  try {
3573
3944
  config = await loadLocalConfig();
3574
3945
  } catch (err) {
3575
3946
  console.error(`SystemDox MCP server: ${err.message}`);
3576
- process.exit(1);
3947
+ keyProblem = keyReason(err.message);
3948
+ try {
3949
+ config = loadConfig(NO_KEY);
3950
+ } catch (urlErr) {
3951
+ console.error(`SystemDox MCP server: ${urlErr.message}`);
3952
+ process.exit(1);
3953
+ }
3577
3954
  }
3578
3955
  var apiClient = new SystemDoxApiClient(config);
3579
- var server = createServer(apiClient, { localFiles: true });
3580
3956
  async function main() {
3581
- await apiClient.validate();
3957
+ const instructions = await sessionInstructions(apiClient, { localFiles: true, keyProblem });
3958
+ const server = createServer(apiClient, { localFiles: true, instructions, keyProblem });
3582
3959
  const transport = new StdioServerTransport();
3583
3960
  await server.connect(transport);
3584
3961
  }