@systemdox/mcp-server 0.1.49 → 0.1.51

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,239 @@ 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 afterInstall(skillMd) {
451
+ return `No restart is needed: a running Claude Code session picks it up. If one does not list it yet, read ${skillMd} and follow it, or run /reload-skills.`;
452
+ }
453
+ function describeInstall(name, r) {
454
+ const lines = [
455
+ `Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
456
+ ];
457
+ if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
458
+ if (r.keptEdited.length) {
459
+ lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
460
+ }
461
+ if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
462
+ lines.push(afterInstall(join(r.dest, "SKILL.md")));
463
+ return lines.join("\n");
464
+ }
465
+
277
466
  // src/lib/team-setup-files.ts
467
+ import { execFile } from "child_process";
468
+ import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
469
+ import { homedir as homedir2, userInfo } from "os";
470
+ import { dirname as dirname2, join as join2 } from "path";
471
+ import { promisify } from "util";
278
472
  var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
279
473
  function isWorkspace(value) {
280
474
  return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
281
475
  }
282
476
  function homeDir() {
283
- return homedir();
477
+ return homedir2();
284
478
  }
285
479
  function systemdoxDir() {
286
- return join(homeDir(), ".claude", "systemdox");
480
+ return join2(homeDir(), ".claude", "systemdox");
287
481
  }
288
482
  function credentialsPath() {
289
- return join(systemdoxDir(), "credentials.json");
483
+ return join2(systemdoxDir(), "credentials.json");
290
484
  }
291
485
  function workspaceDir(workspace) {
292
486
  if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
293
- return join(systemdoxDir(), workspace);
487
+ return join2(systemdoxDir(), workspace);
294
488
  }
295
489
  function statePath(workspace) {
296
- return join(workspaceDir(workspace), "state.json");
490
+ return join2(workspaceDir(workspace), "state.json");
297
491
  }
298
492
  async function readJson(path, empty) {
299
493
  let text3;
300
494
  try {
301
- text3 = await readFile(path, "utf8");
495
+ text3 = await readFile2(path, "utf8");
302
496
  } catch (e) {
303
497
  if (e.code === "ENOENT") return empty;
304
498
  throw e;
@@ -310,9 +504,9 @@ async function readJson(path, empty) {
310
504
  }
311
505
  }
312
506
  async function writeJsonAtomically(path, value, mode) {
313
- await mkdir(dirname(path), { recursive: true });
507
+ await mkdir2(dirname2(path), { recursive: true });
314
508
  const tmp = `${path}.${process.pid}.tmp`;
315
- await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
509
+ await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
316
510
  await rename(tmp, path);
317
511
  }
318
512
  async function readCredentials() {
@@ -357,6 +551,137 @@ async function updateState(workspace, change) {
357
551
  return state;
358
552
  }
359
553
 
554
+ // src/lib/session-brief.ts
555
+ var INSTRUCTIONS_MAX = 2e3;
556
+ var BRIEF_TIMEOUT_MS = 8e3;
557
+ var NO_KEY = "no-key-on-this-computer";
558
+ var SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
559
+ 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.`;
560
+ 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.";
561
+ 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.";
562
+ function noKey(reason) {
563
+ 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.`;
564
+ }
565
+ function keyReason(message) {
566
+ if (/holds keys for/.test(message)) return "keys for several workspaces are stored; set SYSTEMDOX_WORKSPACE to the one to use";
567
+ if (/holds no key for/.test(message)) return "SYSTEMDOX_WORKSPACE names a workspace with no stored key";
568
+ if (/No SystemDox key/.test(message)) return "none is stored";
569
+ return "the stored keys could not be read";
570
+ }
571
+ function olderApi(setup) {
572
+ 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.`;
573
+ }
574
+ async function readJsonQuietly(path) {
575
+ try {
576
+ return JSON.parse(await readFile3(path, "utf8"));
577
+ } catch {
578
+ return void 0;
579
+ }
580
+ }
581
+ async function computerState(setup) {
582
+ const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
583
+ const installed = await readJsonQuietly(join3(homeDir(), ".claude", "plugins", "installed_plugins.json"));
584
+ const entries2 = installed?.plugins?.[id];
585
+ const entry = Array.isArray(entries2) ? entries2.find((e) => (e?.scope ?? "user") === "user") : void 0;
586
+ const state = {
587
+ missing: [],
588
+ behind: [],
589
+ notFromSystemDox: [],
590
+ pluginInstalled: !!entry,
591
+ pluginVersion: entry?.version
592
+ };
593
+ const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
594
+ for (const skill of setup.sessionBrief?.skills ?? []) {
595
+ const dir = defaultSkillDir(skill.name);
596
+ if (!existsSync2(dir)) {
597
+ if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
598
+ continue;
599
+ }
600
+ const manifest = await readJsonQuietly(join3(dir, MANIFEST_FILE));
601
+ if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
602
+ state.notFromSystemDox.push(skill.name);
603
+ } else if (manifest.version !== skill.latestVersion) {
604
+ state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
605
+ }
606
+ }
607
+ return state;
608
+ }
609
+ function hasPersonParts(setup) {
610
+ return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
611
+ }
612
+ var MORE = (n) => `${n} more (get_team_setup lists them)`;
613
+ var LIST_MIN = `all ${MORE(100)}`.length;
614
+ function listWithin(items, room) {
615
+ const whole2 = items.join(", ");
616
+ if (whole2.length <= room) return whole2;
617
+ const kept = [];
618
+ let used = 0;
619
+ for (let i = 0; i < items.length; i++) {
620
+ const rest = items.length - i - 1;
621
+ const cost = items[i].length + (kept.length ? 2 : 0);
622
+ if (used + cost + ` and ${MORE(rest)}`.length > room) break;
623
+ kept.push(items[i]);
624
+ used += cost;
625
+ }
626
+ return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
627
+ }
628
+ function localSection(setup, state, room) {
629
+ const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
630
+ const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
631
+ const lines = [];
632
+ if (state.missing.length) {
633
+ lines.push({
634
+ head: "On this computer, missing: ",
635
+ items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
636
+ 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.`
637
+ });
638
+ }
639
+ if (state.behind.length) {
640
+ lines.push({
641
+ head: "On this computer, behind: ",
642
+ items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
643
+ tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
644
+ });
645
+ }
646
+ if (state.notFromSystemDox.length) {
647
+ lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
648
+ }
649
+ const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
650
+ while (lines.length && fixed() > room) lines.pop();
651
+ if (!lines.length) return "";
652
+ const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
653
+ return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
654
+ }
655
+ function fit(text3, section) {
656
+ const whole2 = section ? `${text3}
657
+
658
+ ${section}` : text3;
659
+ if (whole2.length <= INSTRUCTIONS_MAX) return whole2;
660
+ return text3.length <= INSTRUCTIONS_MAX ? text3 : `${text3.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
661
+ }
662
+ async function readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
663
+ if (keyProblem2) return { problem: noKey(keyProblem2) };
664
+ try {
665
+ const setup = await apiClient2.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
666
+ return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
667
+ } catch (err) {
668
+ const status = err?.status;
669
+ return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
670
+ }
671
+ }
672
+ async function sessionInstructions(apiClient2, options = {}) {
673
+ const read = await readTeamSetup(apiClient2, options);
674
+ if ("problem" in read) return read.problem;
675
+ const text3 = read.setup.sessionBrief?.text ?? olderApi(read.setup);
676
+ try {
677
+ if (!options.localFiles) return fit(text3, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
678
+ const room = INSTRUCTIONS_MAX - text3.length - 2;
679
+ return fit(text3, localSection(read.setup, await computerState(read.setup), room));
680
+ } catch {
681
+ return fit(text3, "");
682
+ }
683
+ }
684
+
360
685
  // src/server.ts
361
686
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
362
687
 
@@ -469,7 +794,7 @@ var inputSchema = () => ({
469
794
  "File path being edited \u2014 language is auto-detected from the extension"
470
795
  )
471
796
  });
472
- function registerGetGuardrails(server2, apiClient2) {
797
+ function registerGetGuardrails(server, apiClient2) {
473
798
  const handler = async ({
474
799
  language,
475
800
  category,
@@ -523,7 +848,7 @@ ${text3}`
523
848
  ]
524
849
  };
525
850
  };
526
- server2.registerTool(
851
+ server.registerTool(
527
852
  "get_checks",
528
853
  {
529
854
  title: "Get Checks",
@@ -532,7 +857,7 @@ ${text3}`
532
857
  },
533
858
  handler
534
859
  );
535
- server2.registerTool(
860
+ server.registerTool(
536
861
  "get_guardrails",
537
862
  {
538
863
  title: "Get Checks (legacy name)",
@@ -545,8 +870,8 @@ ${text3}`
545
870
 
546
871
  // src/tools/get-specs.ts
547
872
  import { z as z2 } from "zod";
548
- function registerGetSpecs(server2, apiClient2) {
549
- server2.registerTool(
873
+ function registerGetSpecs(server, apiClient2) {
874
+ server.registerTool(
550
875
  "get_specs",
551
876
  {
552
877
  title: "Get Specs",
@@ -631,8 +956,8 @@ function matchGlob(path, pattern) {
631
956
  const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
632
957
  return new RegExp(`^${regex}$`).test(path);
633
958
  }
634
- function registerGetDocs(server2, apiClient2) {
635
- server2.registerTool(
959
+ function registerGetDocs(server, apiClient2) {
960
+ server.registerTool(
636
961
  "get_docs",
637
962
  {
638
963
  title: "Get Docs",
@@ -730,8 +1055,8 @@ Use doc_id for full content, or doc_id + section to fetch one section.`
730
1055
  }
731
1056
 
732
1057
  // src/tools/get-tech-stack.ts
733
- function registerGetTechStack(server2, apiClient2) {
734
- server2.registerTool(
1058
+ function registerGetTechStack(server, apiClient2) {
1059
+ server.registerTool(
735
1060
  "get_tech_stack",
736
1061
  {
737
1062
  title: "Get Tech Stack",
@@ -780,8 +1105,8 @@ ${sections}`
780
1105
 
781
1106
  // src/tools/get-planning-context.ts
782
1107
  import { z as z4 } from "zod";
783
- function registerGetPlanningContext(server2, apiClient2) {
784
- server2.registerTool(
1108
+ function registerGetPlanningContext(server, apiClient2) {
1109
+ server.registerTool(
785
1110
  "get_planning_context",
786
1111
  {
787
1112
  title: "Get Planning Context",
@@ -815,7 +1140,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
815
1140
  ${repos}`);
816
1141
  }
817
1142
  if (ctx.dependencies.length > 0) {
818
- const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
1143
+ const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}${d.provenance === "inferred" ? " (inferred)" : ""}`).join("\n");
819
1144
  sections.push(`## Cross-Repo Dependencies
820
1145
 
821
1146
  ${deps}`);
@@ -902,8 +1227,8 @@ function formatItem(item) {
902
1227
  ${meta.join(" \xB7 ")}` : "") + (item.url ? `
903
1228
  ${item.url}` : "");
904
1229
  }
905
- function registerGetPlanBoard(server2, apiClient2) {
906
- server2.registerTool(
1230
+ function registerGetPlanBoard(server, apiClient2) {
1231
+ server.registerTool(
907
1232
  "get_plan_board",
908
1233
  {
909
1234
  title: "Get Plan Board",
@@ -1147,8 +1472,8 @@ function formatChange(change) {
1147
1472
  briefVerdict(change.states)
1148
1473
  ].join("\n\n");
1149
1474
  }
1150
- function registerGetChange(server2, apiClient2) {
1151
- server2.registerTool(
1475
+ function registerGetChange(server, apiClient2) {
1476
+ server.registerTool(
1152
1477
  "get_change",
1153
1478
  {
1154
1479
  title: "Get Change",
@@ -1515,16 +1840,22 @@ async function hydrate(api, item, meta) {
1515
1840
  }
1516
1841
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
1517
1842
  const budget = maxTokens ?? DEFAULT_BUDGET;
1843
+ const failures = [];
1844
+ const orEmpty = (empty) => (err) => {
1845
+ failures.push(err);
1846
+ return empty;
1847
+ };
1518
1848
  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(() => []),
1849
+ api.get("/v1/guardrails").catch(orEmpty([])),
1850
+ api.get("/v1/specs").catch(orEmpty([])),
1851
+ api.get("/v1/docs").catch(orEmpty({ items: [] })),
1852
+ api.get("/v1/ai/settings/stack").catch(orEmpty([])),
1853
+ api.get("/v1/standards/rules").catch(orEmpty([])),
1524
1854
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
1525
1855
  // blip) gates nothing, it can never hide working context.
1526
1856
  api.get("/v1/doc-repos").catch(() => null)
1527
1857
  ]);
1858
+ if (failures.length === 5) throw failures[0];
1528
1859
  const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
1529
1860
  const guardrails = disabled("fitness") ? [] : rawGuardrails;
1530
1861
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
@@ -1731,18 +2062,19 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
1731
2062
  guardrails: selectedGuardrails.length
1732
2063
  };
1733
2064
  if (sections.length === 0) {
1734
- return { text: "No relevant context found for this task.", stats };
2065
+ 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.";
2066
+ return { text: text3, stats };
1735
2067
  }
1736
2068
  const footer = `
1737
2069
 
1738
2070
  ---
1739
- _Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
2071
+ _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
2072
  return { text: body + footer, stats };
1741
2073
  }
1742
2074
 
1743
2075
  // src/tools/get-context.ts
1744
- function registerGetContext(server2, apiClient2) {
1745
- server2.registerTool(
2076
+ function registerGetContext(server, apiClient2) {
2077
+ server.registerTool(
1746
2078
  "get_context",
1747
2079
  {
1748
2080
  title: "Get Context",
@@ -1770,8 +2102,240 @@ function registerGetContext(server2, apiClient2) {
1770
2102
  );
1771
2103
  }
1772
2104
 
1773
- // src/tools/create-guardrail.ts
2105
+ // src/tools/get-team-setup.ts
2106
+ var HELD_BECAUSE = {
2107
+ "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`,
2108
+ "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>`",
2109
+ "not-checked": "it could not be checked. Ask again later"
2110
+ };
2111
+ function describeSetup(setup) {
2112
+ const lines = [`# Team setup of ${setup.workspace}`, ""];
2113
+ const skills = setup.sessionBrief?.skills ?? [];
2114
+ const held = setup.sessionBrief?.heldSkills ?? [];
2115
+ lines.push(`## Skills a session installs by itself (${skills.length})`, "");
2116
+ if (skills.length) {
2117
+ for (const s of skills) lines.push(`- **${s.name}** version ${s.latestVersion}: ${s.description}`);
2118
+ } else if (!setup.sessionBrief && setup.skills.length) {
2119
+ lines.push(`- ${setup.skills.join(", ")}`);
2120
+ } else {
2121
+ lines.push("None.");
2122
+ }
2123
+ lines.push("");
2124
+ if (held.length) {
2125
+ lines.push(`## Skills that wait for the person (${held.length})`, "");
2126
+ for (const h of held) lines.push(`- **${h.name}**: ${HELD_BECAUSE[h.reason] ?? h.reason}`);
2127
+ lines.push("", "Do not install these yourself: each is the person's to decide.", "");
2128
+ }
2129
+ if (setup.plugin) {
2130
+ lines.push(`## Team plugin`, "", `\`${setup.workspace}-setup\` version ${setup.plugin.version}.`);
2131
+ for (const c of setup.plugin.hookCommands ?? []) lines.push(`- Hook: ${JSON.stringify(c)}`);
2132
+ for (const m of setup.plugin.mcpServers ?? []) {
2133
+ lines.push(`- MCP server ${JSON.stringify(m.name)}: ${JSON.stringify(m.url ?? [m.command, ...m.args ?? []].filter(Boolean).join(" "))}`);
2134
+ }
2135
+ lines.push("");
2136
+ }
2137
+ const parts = Object.entries(setup.parts ?? {});
2138
+ if (parts.length) {
2139
+ lines.push(`## Published parts`, "", ...parts.map(([p, v]) => `- ${p}: version ${v}`), "");
2140
+ }
2141
+ if (setup.memory?.length) {
2142
+ lines.push(`## Team memory`, "", ...setup.memory.map((m) => `- ${m.project}: ${m.fileCount} file(s)`), "");
2143
+ }
2144
+ return lines;
2145
+ }
2146
+ function describeComputer(setup, state) {
2147
+ const lines = ["## This computer", ""];
2148
+ if (!setup.sessionBrief) {
2149
+ return [...lines, "This SystemDox API is too old to compare this computer with the team setup."];
2150
+ }
2151
+ const skills = setup.sessionBrief.skills;
2152
+ const latest = new Map(skills.map((s) => [s.name, s.latestVersion]));
2153
+ lines.push(
2154
+ state.missing.length ? `- Missing: ${state.missing.map((name) => `${name} version ${latest.get(name)}`).join(", ")}` : `- Missing: none of ${skills.length}`
2155
+ );
2156
+ if (state.behind.length) {
2157
+ lines.push(`- Behind: ${state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`).join(", ")}`);
2158
+ }
2159
+ if (state.notFromSystemDox.length) lines.push(`- Not from SystemDox, left alone: ${state.notFromSystemDox.join(", ")}`);
2160
+ if (setup.plugin) {
2161
+ lines.push(
2162
+ state.pluginInstalled ? `- Team plugin: installed${state.pluginVersion ? `, ${state.pluginVersion}` : ""}; the latest is version ${setup.plugin.version}` : "- Team plugin: not installed"
2163
+ );
2164
+ }
2165
+ lines.push("", "## What to do", "");
2166
+ const steps = [];
2167
+ 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.");
2168
+ if (state.behind.length) {
2169
+ 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.");
2170
+ }
2171
+ if (hasPersonParts(setup) && !state.pluginInstalled) {
2172
+ steps.push(
2173
+ `Hooks, MCP servers, team instructions and memory need the person: \`${SETUP_COMMAND}\` in their own terminal. Never answer its prompts for them.`
2174
+ );
2175
+ }
2176
+ lines.push(...steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ["Nothing."]);
2177
+ return lines;
2178
+ }
2179
+ function registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 }) {
2180
+ server.registerTool(
2181
+ "get_team_setup",
2182
+ {
2183
+ title: "Get Team Setup",
2184
+ 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.",
2185
+ inputSchema: {},
2186
+ annotations: { readOnlyHint: true }
2187
+ },
2188
+ async () => {
2189
+ const read = await readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs: 15e3 });
2190
+ if ("problem" in read) return { content: [{ type: "text", text: read.problem }] };
2191
+ const lines = describeSetup(read.setup);
2192
+ if (localFiles) lines.push(...describeComputer(read.setup, await computerState(read.setup)));
2193
+ else lines.push(CANNOT_SEE);
2194
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2195
+ }
2196
+ );
2197
+ }
2198
+
2199
+ // src/tools/document-graph.ts
1774
2200
  import { z as z8 } from "zod";
2201
+ var DEFAULT_LIMIT2 = 50;
2202
+ var MAX_LIMIT = 200;
2203
+ var DEFAULT_HOPS = 6;
2204
+ var MAX_HOPS = 8;
2205
+ function place(file, line) {
2206
+ return file ? `${file}:${line}` : "made by a person";
2207
+ }
2208
+ function known(provenance, confidence) {
2209
+ return `${provenance} ${confidence}`;
2210
+ }
2211
+ function formatNeighbours(answer) {
2212
+ const lines = [`# ${answer.doc.label}`, `\`${answer.doc.id}\``, ""];
2213
+ if (answer.links.length === 0) lines.push("No links.");
2214
+ for (const link of answer.links) {
2215
+ lines.push(
2216
+ `- ${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)}`
2217
+ );
2218
+ }
2219
+ if (answer.truncated) {
2220
+ lines.push("", `Showing ${answer.links.length} of ${answer.total}. Raise limit to see more.`);
2221
+ }
2222
+ if (answer.ambiguous.length > 0) {
2223
+ lines.push("", "## Ambiguous: not drawn, because each could mean several documents");
2224
+ for (const entry of answer.ambiguous) {
2225
+ const candidates = entry.candidates.map((c) => `${c.label} (${c.repo}, \`${c.id}\`)`);
2226
+ lines.push(`- ${entry.reference} at ${place(entry.file, entry.line)}: ${candidates.join("; ")}`);
2227
+ }
2228
+ }
2229
+ return lines.join("\n");
2230
+ }
2231
+ function formatPath(answer, maxHops) {
2232
+ if (!answer.found) return `No path within ${maxHops} hops.`;
2233
+ const lines = answer.steps.map((step, index) => {
2234
+ const arrow = step.reversed ? "<-" : "->";
2235
+ 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)}`;
2236
+ });
2237
+ return [...lines, "", `Path confidence: ${answer.confidence}`].join("\n");
2238
+ }
2239
+ function formatStats(stats, repoLinks) {
2240
+ const { declared, extracted, inferred, ambiguous } = stats.links;
2241
+ const lines = [
2242
+ `# The graph: ${stats.nodeCount} documents, ${stats.edgeCount} links`,
2243
+ "",
2244
+ `Links: ${declared} declared \xB7 ${extracted} extracted \xB7 ${inferred} inferred \xB7 ${ambiguous} ambiguous (not drawn)`,
2245
+ "",
2246
+ "## Repositories"
2247
+ ];
2248
+ for (const repo of stats.repos) {
2249
+ const state = repo.behind ? `BEHIND: pushed ${repo.headCommit || "unknown"} at ${repo.pushedAt}, index at ${repo.indexedCommit || "unknown"}` : `index at ${repo.indexedCommit || "unknown"}`;
2250
+ lines.push(`- ${repo.repo}: ${repo.documents} documents, ${repo.links} links, ${state}`);
2251
+ }
2252
+ if (repoLinks.length > 0) {
2253
+ lines.push("", "## Repository links");
2254
+ for (const link of repoLinks) {
2255
+ lines.push(
2256
+ `- ${link.source} ${link.relation} ${link.target} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${place(link.file, link.line)}`
2257
+ );
2258
+ }
2259
+ }
2260
+ return lines.join("\n");
2261
+ }
2262
+ function refused(err) {
2263
+ const status = err.status;
2264
+ if (status !== 404 && status !== 400) throw err;
2265
+ return {
2266
+ content: [{ type: "text", text: err.message }],
2267
+ isError: true
2268
+ };
2269
+ }
2270
+ function registerDocumentGraph(server, apiClient2) {
2271
+ server.registerTool(
2272
+ "get_document_links",
2273
+ {
2274
+ 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.",
2275
+ inputSchema: {
2276
+ doc_id: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2277
+ direction: z8.enum(["out", "in", "both"]).optional().describe("Default both"),
2278
+ relation: z8.enum(["links_to", "cites", "mentions", "satisfies", "references"]).optional().describe("Only links of this kind"),
2279
+ limit: z8.number().int().min(1).max(MAX_LIMIT).optional().describe(`Most links to show (default ${DEFAULT_LIMIT2})`)
2280
+ },
2281
+ annotations: { readOnlyHint: true }
2282
+ },
2283
+ async ({ doc_id, direction, relation, limit }) => {
2284
+ const params = { doc: doc_id, limit: String(limit ?? DEFAULT_LIMIT2) };
2285
+ if (direction) params.direction = direction;
2286
+ if (relation) params.relation = relation;
2287
+ try {
2288
+ const answer = await apiClient2.get("/v1/graph/neighbours", params);
2289
+ return { content: [{ type: "text", text: formatNeighbours(answer) }] };
2290
+ } catch (err) {
2291
+ return refused(err);
2292
+ }
2293
+ }
2294
+ );
2295
+ server.registerTool(
2296
+ "trace_document_path",
2297
+ {
2298
+ 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.",
2299
+ inputSchema: {
2300
+ from: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2301
+ to: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2302
+ max_hops: z8.number().int().min(1).max(MAX_HOPS).optional().describe(`Longest path to look for (default ${DEFAULT_HOPS})`)
2303
+ },
2304
+ annotations: { readOnlyHint: true }
2305
+ },
2306
+ async ({ from, to, max_hops }) => {
2307
+ const hops = max_hops ?? DEFAULT_HOPS;
2308
+ try {
2309
+ const answer = await apiClient2.get("/v1/graph/path", {
2310
+ from,
2311
+ to,
2312
+ maxHops: String(hops)
2313
+ });
2314
+ return { content: [{ type: "text", text: formatPath(answer, hops) }] };
2315
+ } catch (err) {
2316
+ return refused(err);
2317
+ }
2318
+ }
2319
+ );
2320
+ server.registerTool(
2321
+ "get_graph_stats",
2322
+ {
2323
+ 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.",
2324
+ inputSchema: {},
2325
+ annotations: { readOnlyHint: true }
2326
+ },
2327
+ async () => {
2328
+ const [stats, repos] = await Promise.all([
2329
+ apiClient2.getFresh("/v1/graph/stats"),
2330
+ apiClient2.getFresh("/v1/graph/repos")
2331
+ ]);
2332
+ return { content: [{ type: "text", text: formatStats(stats, repos.links) }] };
2333
+ }
2334
+ );
2335
+ }
2336
+
2337
+ // src/tools/create-guardrail.ts
2338
+ import { z as z9 } from "zod";
1775
2339
  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
2340
  var CATEGORIES = [
1777
2341
  "imports",
@@ -1784,15 +2348,15 @@ var CATEGORIES = [
1784
2348
  "data-access"
1785
2349
  ];
1786
2350
  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")
2351
+ title: z9.string().describe("Short title for the check"),
2352
+ description: z9.string().describe("Detailed description of what the check enforces and why"),
2353
+ severity: z9.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
2354
+ category: z9.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
2355
+ pattern: z9.string().optional().describe("Bad code pattern that should be avoided"),
2356
+ fix: z9.string().optional().describe("Correct code pattern to use instead"),
2357
+ fitness_test: z9.string().optional().describe("Shell command to detect violations in CI")
1794
2358
  });
1795
- function registerCreateGuardrail(server2, apiClient2) {
2359
+ function registerCreateGuardrail(server, apiClient2) {
1796
2360
  const handler = async ({
1797
2361
  title,
1798
2362
  description,
@@ -1829,7 +2393,7 @@ function registerCreateGuardrail(server2, apiClient2) {
1829
2393
  ]
1830
2394
  };
1831
2395
  };
1832
- server2.registerTool(
2396
+ server.registerTool(
1833
2397
  "create_check",
1834
2398
  {
1835
2399
  title: "Create Check",
@@ -1841,7 +2405,7 @@ function registerCreateGuardrail(server2, apiClient2) {
1841
2405
  },
1842
2406
  handler
1843
2407
  );
1844
- server2.registerTool(
2408
+ server.registerTool(
1845
2409
  "create_guardrail",
1846
2410
  {
1847
2411
  title: "Create Check (legacy name)",
@@ -1856,20 +2420,20 @@ function registerCreateGuardrail(server2, apiClient2) {
1856
2420
  }
1857
2421
 
1858
2422
  // src/tools/update-spec.ts
1859
- import { z as z9 } from "zod";
1860
- function registerUpdateSpec(server2, apiClient2) {
1861
- server2.registerTool(
2423
+ import { z as z10 } from "zod";
2424
+ function registerUpdateSpec(server, apiClient2) {
2425
+ server.registerTool(
1862
2426
  "update_spec",
1863
2427
  {
1864
2428
  title: "Update Spec",
1865
2429
  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
2430
  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(
2431
+ draft_id: z10.string().describe("ID of the spec to update"),
2432
+ section: z10.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
2433
+ content: z10.string().describe(
1870
2434
  "New markdown content for the section. For requirements, provide the full requirement content."
1871
2435
  ),
1872
- requirement_name: z9.string().optional().describe(
2436
+ requirement_name: z10.string().optional().describe(
1873
2437
  "Name of the requirement (required when section is 'requirements')"
1874
2438
  )
1875
2439
  },
@@ -1919,19 +2483,19 @@ function registerUpdateSpec(server2, apiClient2) {
1919
2483
  }
1920
2484
 
1921
2485
  // src/tools/report-decision.ts
1922
- import { z as z10 } from "zod";
1923
- function registerReportDecision(server2, apiClient2) {
1924
- server2.registerTool(
2486
+ import { z as z11 } from "zod";
2487
+ function registerReportDecision(server, apiClient2) {
2488
+ server.registerTool(
1925
2489
  "report_decision",
1926
2490
  {
1927
2491
  title: "Report Decision",
1928
2492
  description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
1929
2493
  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")
2494
+ title: z11.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
2495
+ context: z11.string().describe("What prompted this decision \u2014 the problem or constraint"),
2496
+ decision: z11.string().describe("What was decided and why"),
2497
+ consequences: z11.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
2498
+ related_files: z11.array(z11.string()).optional().describe("File paths related to this decision")
1935
2499
  },
1936
2500
  annotations: {
1937
2501
  readOnlyHint: false
@@ -1982,7 +2546,7 @@ Status: draft \u2014 review in SystemDox before publishing.`
1982
2546
  }
1983
2547
 
1984
2548
  // src/tools/get-project-rules.ts
1985
- import { z as z11 } from "zod";
2549
+ import { z as z12 } from "zod";
1986
2550
  var CATEGORY_LABELS = {
1987
2551
  deployment: "Deployment",
1988
2552
  architecture: "Architecture",
@@ -1991,14 +2555,14 @@ var CATEGORY_LABELS = {
1991
2555
  process: "Process",
1992
2556
  other: "Other"
1993
2557
  };
1994
- function registerGetProjectRules(server2, apiClient2) {
1995
- server2.registerTool(
2558
+ function registerGetProjectRules(server, apiClient2) {
2559
+ server.registerTool(
1996
2560
  "get_project_rules",
1997
2561
  {
1998
2562
  title: "Get Project Rules",
1999
2563
  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
2564
  inputSchema: {
2001
- category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
2565
+ category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
2002
2566
  }
2003
2567
  },
2004
2568
  async ({ category }) => {
@@ -2050,18 +2614,18 @@ function registerGetProjectRules(server2, apiClient2) {
2050
2614
  }
2051
2615
 
2052
2616
  // src/tools/create-project-rule.ts
2053
- import { z as z12 } from "zod";
2054
- function registerCreateProjectRule(server2, apiClient2) {
2055
- server2.registerTool(
2617
+ import { z as z13 } from "zod";
2618
+ function registerCreateProjectRule(server, apiClient2) {
2619
+ server.registerTool(
2056
2620
  "create_project_rule",
2057
2621
  {
2058
2622
  title: "Create Project Rule",
2059
2623
  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
2624
  inputSchema: {
2061
- text: z12.string().describe(
2625
+ text: z13.string().describe(
2062
2626
  "The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
2063
2627
  ),
2064
- category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
2628
+ category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
2065
2629
  },
2066
2630
  annotations: {
2067
2631
  readOnlyHint: false
@@ -2086,8 +2650,8 @@ function registerCreateProjectRule(server2, apiClient2) {
2086
2650
  }
2087
2651
 
2088
2652
  // src/tools/get-prompt-templates.ts
2089
- function registerGetPromptTemplates(server2, apiClient2) {
2090
- server2.registerTool(
2653
+ function registerGetPromptTemplates(server, apiClient2) {
2654
+ server.registerTool(
2091
2655
  "get_prompt_templates",
2092
2656
  {
2093
2657
  title: "Get Prompt Templates",
@@ -2132,17 +2696,17 @@ ${text3}`
2132
2696
  }
2133
2697
 
2134
2698
  // src/tools/create-prompt-template.ts
2135
- import { z as z13 } from "zod";
2136
- function registerCreatePromptTemplate(server2, apiClient2) {
2137
- server2.registerTool(
2699
+ import { z as z14 } from "zod";
2700
+ function registerCreatePromptTemplate(server, apiClient2) {
2701
+ server.registerTool(
2138
2702
  "create_prompt_template",
2139
2703
  {
2140
2704
  title: "Create Prompt Template",
2141
2705
  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
2706
  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")
2707
+ name: z14.string().describe("Short name for the template"),
2708
+ description: z14.string().optional().describe("Brief description of when to use this template"),
2709
+ prompt: z14.string().describe("The full prompt text that Gen AI will use when generating content")
2146
2710
  },
2147
2711
  annotations: {
2148
2712
  readOnlyHint: false
@@ -2171,205 +2735,9 @@ function registerCreatePromptTemplate(server2, apiClient2) {
2171
2735
  }
2172
2736
 
2173
2737
  // src/tools/skills.ts
2174
- import { z as z14 } from "zod";
2738
+ import { z as z15 } from "zod";
2175
2739
  import { homedir as homedir3 } from "os";
2176
2740
  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
2741
  var SKILLS = "/v1/standards/skills";
2374
2742
  function expandHome(path) {
2375
2743
  return path.replace(/^~(?=$|[\\/])/, homedir3());
@@ -2377,25 +2745,25 @@ function expandHome(path) {
2377
2745
  function text(t) {
2378
2746
  return { content: [{ type: "text", text: t }] };
2379
2747
  }
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")
2748
+ function registerSkillTools(server, apiClient2, { localFiles }) {
2749
+ const fileSchema = z15.object({
2750
+ path: z15.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
2751
+ content: z15.string().describe("The file's full text, exactly as it is")
2384
2752
  });
2385
- server2.registerTool(
2753
+ server.registerTool(
2386
2754
  "publish_skill",
2387
2755
  {
2388
2756
  title: "Publish Skill",
2389
2757
  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
2758
  inputSchema: {
2391
2759
  ...localFiles ? {
2392
- directory: z14.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2760
+ directory: z15.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2393
2761
  } : {},
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(
2762
+ files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
2763
+ name: z15.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
2764
+ spaceId: z15.string().optional().describe("Space for a NEW skill (default: your default space)"),
2765
+ 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")`),
2766
+ baseVersion: z15.number().int().positive().optional().describe(
2399
2767
  "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
2768
  )
2401
2769
  },
@@ -2433,7 +2801,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2433
2801
  );
2434
2802
  }
2435
2803
  );
2436
- server2.registerTool(
2804
+ server.registerTool(
2437
2805
  "list_skills",
2438
2806
  {
2439
2807
  title: "List Skills",
@@ -2455,16 +2823,16 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2455
2823
  );
2456
2824
  }
2457
2825
  );
2458
- server2.registerTool(
2826
+ server.registerTool(
2459
2827
  "install_skill",
2460
2828
  {
2461
2829
  title: "Install Skill",
2462
2830
  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
2831
  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") } : {}
2832
+ name: z15.string().describe("The skill's name"),
2833
+ version: z15.number().int().positive().optional().describe("A version number (default: the latest)"),
2834
+ target: z15.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
2835
+ ...localFiles ? { overwrite: z15.boolean().optional().describe("Replace files edited since they were installed") } : {}
2468
2836
  },
2469
2837
  annotations: { readOnlyHint: !localFiles }
2470
2838
  },
@@ -2495,7 +2863,7 @@ ${fence}
2495
2863
  ${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
2496
2864
  }).join("\n\n");
2497
2865
  return text(
2498
- `Skill **${version.name}** version ${version.version}: write these ${version.files.length} file(s) into ${dest}/ exactly as shown (the content between the fences), creating folders as needed. If a file there differs from ${MANIFEST_FILE}'s hash for it, it was edited: ask before replacing it. Then write ${MANIFEST_FILE} with this content, and tell the user to start a new Claude Code session.
2866
+ `Skill **${version.name}** version ${version.version}: write these ${version.files.length} file(s) into ${dest}/ exactly as shown (the content between the fences), creating folders as needed. If a file there differs from ${MANIFEST_FILE}'s hash for it, it was edited: ask before replacing it. Then write ${MANIFEST_FILE} with this content. ${afterInstall(`${dest}/SKILL.md`)}
2499
2867
 
2500
2868
  ### ${MANIFEST_FILE}
2501
2869
 
@@ -2510,11 +2878,11 @@ ${body}`
2510
2878
  }
2511
2879
 
2512
2880
  // src/tools/team-setup.ts
2513
- import { z as z15 } from "zod";
2514
- import { existsSync as existsSync3 } from "fs";
2881
+ import { z as z16 } from "zod";
2882
+ import { existsSync as existsSync4 } from "fs";
2515
2883
  import { lstat as lstat2 } from "fs/promises";
2516
2884
  import { homedir as homedir4 } from "os";
2517
- import { join as join4, resolve as resolve4 } from "path";
2885
+ import { join as join5, resolve as resolve4 } from "path";
2518
2886
 
2519
2887
  // src/lib/person-values.ts
2520
2888
  var PHONE = "phone";
@@ -2595,9 +2963,9 @@ var PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
2595
2963
  var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2596
2964
 
2597
2965
  // 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";
2966
+ import { existsSync as existsSync3 } from "fs";
2967
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
2968
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
2601
2969
 
2602
2970
  // src/lib/memory-sync.ts
2603
2971
  import { createHash as createHash2 } from "crypto";
@@ -2877,7 +3245,7 @@ function refusal(e) {
2877
3245
  const { status, code } = e;
2878
3246
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
2879
3247
  }
2880
- var toPath = (root, rel) => join3(root, ...rel.split("/"));
3248
+ var toPath = (root, rel) => join4(root, ...rel.split("/"));
2881
3249
  async function readFolder(root) {
2882
3250
  const files = {};
2883
3251
  const names = /* @__PURE__ */ new Set();
@@ -2885,14 +3253,14 @@ async function readFolder(root) {
2885
3253
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2886
3254
  if (e.name.startsWith(".")) continue;
2887
3255
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
2888
- if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
3256
+ if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
2889
3257
  else if (e.isFile()) {
2890
3258
  names.add(rel.toLowerCase());
2891
- if (e.name.endsWith(".md")) files[rel] = await readFile3(join3(dir, e.name), "utf8");
3259
+ if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
2892
3260
  }
2893
3261
  }
2894
3262
  }
2895
- if (existsSync2(root)) await walk2(root, "");
3263
+ if (existsSync3(root)) await walk2(root, "");
2896
3264
  return { files, names };
2897
3265
  }
2898
3266
  async function pullTeam(client, ws, project) {
@@ -3101,11 +3469,11 @@ async function people(opts, ws, files, report, hint) {
3101
3469
  report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
3102
3470
  break;
3103
3471
  }
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.`);
3472
+ const refused2 = result.refused[0];
3473
+ if (refused2?.code === "SYD-K021") {
3474
+ out(` ${refused2.detail}. Add them in Staff first, or answer p to file them.`);
3107
3475
  choices = choices.filter((c) => c !== "m");
3108
- } else if (refused) out(` ${refused.detail}`);
3476
+ } else if (refused2) out(` ${refused2.detail}`);
3109
3477
  }
3110
3478
  }
3111
3479
  for (let i = 0; i < filed.length; i += 50) {
@@ -3249,7 +3617,7 @@ async function syncMemory(opts) {
3249
3617
  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
3618
  );
3251
3619
  }
3252
- const dir = resolve3(opts.dir ?? join3(workspaceDir(ws), "memory", project));
3620
+ const dir = resolve3(opts.dir ?? join4(workspaceDir(ws), "memory", project));
3253
3621
  const run2 = { ...opts, dir };
3254
3622
  const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
3255
3623
  const report = new Report();
@@ -3399,21 +3767,21 @@ async function readPartDirectory(part, directory) {
3399
3767
  let present = true;
3400
3768
  for (let i = 1; i <= segments.length; i++) {
3401
3769
  const rel = segments.slice(0, i).join("/");
3402
- if (await isLink(join4(root, ...segments.slice(0, i)))) {
3770
+ if (await isLink(join5(root, ...segments.slice(0, i)))) {
3403
3771
  links.push(rel);
3404
3772
  present = false;
3405
3773
  break;
3406
3774
  }
3407
3775
  }
3408
- if (present && existsSync3(join4(root, ...segments))) paths.push(path);
3776
+ if (present && existsSync4(join5(root, ...segments))) paths.push(path);
3409
3777
  }
3410
3778
  for (const { dir, md } of layout.folders) {
3411
- const full = join4(root, dir);
3779
+ const full = join5(root, dir);
3412
3780
  if (await isLink(full)) {
3413
3781
  links.push(dir);
3414
3782
  continue;
3415
3783
  }
3416
- if (!existsSync3(full)) continue;
3784
+ if (!existsSync4(full)) continue;
3417
3785
  const walked = await walk(full, { skipHidden: true });
3418
3786
  links.push(...walked.links.map((p) => `${dir}/${p}`));
3419
3787
  leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
@@ -3452,24 +3820,24 @@ function describePerson(id, person) {
3452
3820
  );
3453
3821
  return lines.join("\n");
3454
3822
  }
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")
3823
+ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
3824
+ const fileSchema = z16.object({
3825
+ path: z16.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
3826
+ content: z16.string().describe("The file's full text, exactly as it is")
3459
3827
  });
3460
- server2.registerTool(
3828
+ server.registerTool(
3461
3829
  "publish_setup_part",
3462
3830
  {
3463
3831
  title: "Publish Setup Part",
3464
3832
  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
3833
  inputSchema: {
3466
- part: z15.enum(PARTS2).describe("Which part of the setup"),
3834
+ part: z16.enum(PARTS2).describe("Which part of the setup"),
3467
3835
  ...localFiles ? {
3468
- directory: z15.string().optional().describe(
3836
+ directory: z16.string().optional().describe(
3469
3837
  "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
3838
  )
3471
3839
  } : {},
3472
- files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3840
+ files: z16.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3473
3841
  },
3474
3842
  annotations: { readOnlyHint: false }
3475
3843
  },
@@ -3499,13 +3867,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3499
3867
  return text2(lines.join("\n"));
3500
3868
  }
3501
3869
  );
3502
- server2.registerTool(
3870
+ server.registerTool(
3503
3871
  "resolve_person",
3504
3872
  {
3505
3873
  title: "Resolve Person",
3506
3874
  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
3875
  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")
3876
+ 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
3877
  },
3510
3878
  annotations: { readOnlyHint: true }
3511
3879
  },
@@ -3516,14 +3884,14 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3516
3884
  }
3517
3885
  );
3518
3886
  if (!localFiles) return;
3519
- server2.registerTool(
3887
+ server.registerTool(
3520
3888
  "sync_memory",
3521
3889
  {
3522
3890
  title: "Sync Memory",
3523
3891
  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
3892
  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")
3893
+ 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"),
3894
+ dryRun: z16.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
3527
3895
  },
3528
3896
  annotations: { readOnlyHint: false }
3529
3897
  },
@@ -3541,44 +3909,56 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3541
3909
  }
3542
3910
 
3543
3911
  // 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;
3912
+ var SERVER_VERSION = true ? "0.1.51" : "0.0.0-dev";
3913
+ function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
3914
+ const server = new McpServer(
3915
+ {
3916
+ name: "systemdox-mcp-server",
3917
+ version: SERVER_VERSION
3918
+ },
3919
+ instructions ? { instructions } : void 0
3920
+ );
3921
+ registerGetGuardrails(server, apiClient2);
3922
+ registerGetSpecs(server, apiClient2);
3923
+ registerGetDocs(server, apiClient2);
3924
+ registerGetTechStack(server, apiClient2);
3925
+ registerGetPlanningContext(server, apiClient2);
3926
+ registerGetPlanBoard(server, apiClient2);
3927
+ registerGetChange(server, apiClient2);
3928
+ registerGetContext(server, apiClient2);
3929
+ registerGetProjectRules(server, apiClient2);
3930
+ registerGetPromptTemplates(server, apiClient2);
3931
+ registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 });
3932
+ registerDocumentGraph(server, apiClient2);
3933
+ registerCreateGuardrail(server, apiClient2);
3934
+ registerUpdateSpec(server, apiClient2);
3935
+ registerReportDecision(server, apiClient2);
3936
+ registerCreateProjectRule(server, apiClient2);
3937
+ registerCreatePromptTemplate(server, apiClient2);
3938
+ registerSkillTools(server, apiClient2, { localFiles });
3939
+ registerTeamSetupTools(server, apiClient2, { localFiles });
3940
+ return server;
3568
3941
  }
3569
3942
 
3570
3943
  // src/index.ts
3571
3944
  var config;
3945
+ var keyProblem;
3572
3946
  try {
3573
3947
  config = await loadLocalConfig();
3574
3948
  } catch (err) {
3575
3949
  console.error(`SystemDox MCP server: ${err.message}`);
3576
- process.exit(1);
3950
+ keyProblem = keyReason(err.message);
3951
+ try {
3952
+ config = loadConfig(NO_KEY);
3953
+ } catch (urlErr) {
3954
+ console.error(`SystemDox MCP server: ${urlErr.message}`);
3955
+ process.exit(1);
3956
+ }
3577
3957
  }
3578
3958
  var apiClient = new SystemDoxApiClient(config);
3579
- var server = createServer(apiClient, { localFiles: true });
3580
3959
  async function main() {
3581
- await apiClient.validate();
3960
+ const instructions = await sessionInstructions(apiClient, { localFiles: true, keyProblem });
3961
+ const server = createServer(apiClient, { localFiles: true, instructions, keyProblem });
3582
3962
  const transport = new StdioServerTransport();
3583
3963
  await server.connect(transport);
3584
3964
  }