@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/cli.js CHANGED
@@ -301,14 +301,6 @@ var init_api_client = __esm({
301
301
  this.config = config2;
302
302
  this.cache = new ResponseCache(config2.cacheTtlMs);
303
303
  }
304
- async validate() {
305
- const res = await this.fetchRaw("/v1/health");
306
- if (!res.ok) {
307
- throw new Error(
308
- `SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
309
- );
310
- }
311
- }
312
304
  async get(path, params) {
313
305
  const url = this.buildUrl(path, params);
314
306
  const cacheKey = url.toString();
@@ -322,8 +314,8 @@ var init_api_client = __esm({
322
314
  }
323
315
  /** A read that skips the cache: an install must take the version that is
324
316
  * latest now, not the one cached before someone published another. */
325
- async getFresh(path) {
326
- const res = await this.fetchRaw(path);
317
+ async getFresh(path, signal) {
318
+ const res = await this.fetchRaw(path, void 0, signal);
327
319
  if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
328
320
  return await res.json();
329
321
  }
@@ -353,8 +345,8 @@ var init_api_client = __esm({
353
345
  // One method per operationId. Every read skips the cache: setup, sync and
354
346
  // plugin-path act on what the workspace holds now.
355
347
  /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
356
- getOwnTeamSetup() {
357
- return this.getFresh(TEAM_SETUP);
348
+ getOwnTeamSetup(signal) {
349
+ return this.getFresh(TEAM_SETUP, signal);
358
350
  }
359
351
  getTeamSetup(workspace) {
360
352
  return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
@@ -458,14 +450,15 @@ var init_api_client = __esm({
458
450
  }
459
451
  return url;
460
452
  }
461
- async fetchRaw(path, params) {
453
+ async fetchRaw(path, params, signal) {
462
454
  const url = this.buildUrl(path, params);
463
455
  return fetch(url, {
464
456
  headers: {
465
457
  Authorization: `Bearer ${this.config.apiKey}`,
466
458
  "X-API-Key": this.config.apiKey,
467
459
  Accept: "application/json"
468
- }
460
+ },
461
+ signal
469
462
  });
470
463
  }
471
464
  };
@@ -1030,11 +1023,11 @@ async function people(opts, ws, files, report, hint) {
1030
1023
  report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
1031
1024
  break;
1032
1025
  }
1033
- const refused = result.refused[0];
1034
- if (refused?.code === "SYD-K021") {
1035
- out(` ${refused.detail}. Add them in Staff first, or answer p to file them.`);
1026
+ const refused2 = result.refused[0];
1027
+ if (refused2?.code === "SYD-K021") {
1028
+ out(` ${refused2.detail}. Add them in Staff first, or answer p to file them.`);
1036
1029
  choices = choices.filter((c) => c !== "m");
1037
- } else if (refused) out(` ${refused.detail}`);
1030
+ } else if (refused2) out(` ${refused2.detail}`);
1038
1031
  }
1039
1032
  }
1040
1033
  for (let i = 0; i < filed.length; i += 50) {
@@ -1773,9 +1766,9 @@ async function runTeamSetup(opts) {
1773
1766
  const id = `${ws}-setup@${marketplaceName}`;
1774
1767
  const installed = await readJsonQuietly(join3(pluginsDir, "installed_plugins.json"));
1775
1768
  if (!installed?.plugins?.[id]) {
1776
- const known = await readJsonQuietly(join3(pluginsDir, "known_marketplaces.json"));
1769
+ const known2 = await readJsonQuietly(join3(pluginsDir, "known_marketplaces.json"));
1777
1770
  const install = `Run \`claude plugin install ${id}\` in your own terminal and accept the command it shows.`;
1778
- const steps = known && typeof known === "object" && known[marketplaceName] ? ["One step is left:", `1. ${install}`] : ["Two steps are left:", "1. Open Claude Code once in any folder, then close it.", `2. ${install}`];
1771
+ const steps = known2 && typeof known2 === "object" && known2[marketplaceName] ? ["One step is left:", `1. ${install}`] : ["Two steps are left:", "1. Open Claude Code once in any folder, then close it.", `2. ${install}`];
1779
1772
  out("");
1780
1773
  steps.forEach((l) => out(l));
1781
1774
  }
@@ -1979,9 +1972,10 @@ async function setupMcpServer(opts) {
1979
1972
  Your key for ${workspace} is in ${credentialsPath()} (readable by you alone), not in these configs.
1980
1973
 
1981
1974
  Done! Restart your AI coding tool to activate the SystemDox MCP server.
1982
- The server provides 21 tools: get_context, get_checks,
1975
+ The server provides 25 tools: get_context, get_team_setup, get_checks,
1983
1976
  get_project_rules, get_prompt_templates, get_specs, get_docs,
1984
1977
  get_tech_stack, get_planning_context, get_plan_board, get_change,
1978
+ get_document_links, trace_document_path, get_graph_stats,
1985
1979
  create_check, create_project_rule, create_prompt_template,
1986
1980
  update_spec, report_decision, publish_skill, list_skills,
1987
1981
  install_skill, publish_setup_part, resolve_person, sync_memory.
@@ -2017,7 +2011,7 @@ function isValidName(name) {
2017
2011
  return name.length <= 64 && NAME_RE.test(name);
2018
2012
  }
2019
2013
  function isValidPath(path) {
2020
- return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
2014
+ return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
2021
2015
  }
2022
2016
  function frontmatterName(skillMd) {
2023
2017
  const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
@@ -2112,15 +2106,15 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
2112
2106
  }
2113
2107
  const present = walked.files;
2114
2108
  const manifest = await readManifest(dest);
2115
- const known = Object.fromEntries(
2109
+ const known2 = Object.fromEntries(
2116
2110
  Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
2117
2111
  );
2118
2112
  const edited = [];
2119
2113
  const untracked = [];
2120
2114
  for (const path of present) {
2121
2115
  const content = await readFile4(join5(dest, ...path.split("/")), "utf8");
2122
- if (Object.hasOwn(known, path)) {
2123
- if (sha2563(content) !== known[path]) edited.push(path);
2116
+ if (Object.hasOwn(known2, path)) {
2117
+ if (sha2563(content) !== known2[path]) edited.push(path);
2124
2118
  } else if (!incoming.has(path) || sha2563(content) !== incoming.get(path).sha256) {
2125
2119
  if (incoming.has(path) || !manifest) edited.push(path);
2126
2120
  else untracked.push(path);
@@ -2137,7 +2131,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
2137
2131
  }
2138
2132
  const removed = [];
2139
2133
  const keptEdited = [];
2140
- for (const path of Object.keys(known)) {
2134
+ for (const path of Object.keys(known2)) {
2141
2135
  if (incoming.has(path) || !present.includes(path)) continue;
2142
2136
  if (edited.includes(path)) {
2143
2137
  keptEdited.push(path);
@@ -2178,12 +2172,13 @@ function describeInstall(name, r) {
2178
2172
  lines.push("Start a new Claude Code session to load it.");
2179
2173
  return lines.join("\n");
2180
2174
  }
2181
- var MANIFEST_FILE, ENTRY_FILE, MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, NAME_RE, SEGMENT_RE, SKIPPED, SkillInstallRefused;
2175
+ var MANIFEST_FILE, ENTRY_FILE, PLUGIN_MANIFEST_FOLDER, MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, NAME_RE, SEGMENT_RE, SKIPPED, SkillInstallRefused;
2182
2176
  var init_skills = __esm({
2183
2177
  "src/lib/skills.ts"() {
2184
2178
  "use strict";
2185
2179
  MANIFEST_FILE = ".systemdox-skill.json";
2186
2180
  ENTRY_FILE = "SKILL.md";
2181
+ PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
2187
2182
  MAX_FILES = 100;
2188
2183
  MAX_FILE_BYTES = 256 * 1024;
2189
2184
  MAX_TOTAL_BYTES = 1024 * 1024;
@@ -2508,6 +2503,148 @@ var init_plugin_path = __esm({
2508
2503
  }
2509
2504
  });
2510
2505
 
2506
+ // src/lib/session-brief.ts
2507
+ import { existsSync as existsSync6 } from "fs";
2508
+ import { readFile as readFile5 } from "fs/promises";
2509
+ import { join as join7 } from "path";
2510
+ function noKey(reason) {
2511
+ 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.`;
2512
+ }
2513
+ function keyReason(message) {
2514
+ if (/holds keys for/.test(message)) return "keys for several workspaces are stored; set SYSTEMDOX_WORKSPACE to the one to use";
2515
+ if (/holds no key for/.test(message)) return "SYSTEMDOX_WORKSPACE names a workspace with no stored key";
2516
+ if (/No SystemDox key/.test(message)) return "none is stored";
2517
+ return "the stored keys could not be read";
2518
+ }
2519
+ function olderApi(setup) {
2520
+ 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.`;
2521
+ }
2522
+ async function readJsonQuietly2(path) {
2523
+ try {
2524
+ return JSON.parse(await readFile5(path, "utf8"));
2525
+ } catch {
2526
+ return void 0;
2527
+ }
2528
+ }
2529
+ async function computerState(setup) {
2530
+ const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
2531
+ const installed = await readJsonQuietly2(join7(homeDir(), ".claude", "plugins", "installed_plugins.json"));
2532
+ const entries2 = installed?.plugins?.[id];
2533
+ const entry = Array.isArray(entries2) ? entries2.find((e) => (e?.scope ?? "user") === "user") : void 0;
2534
+ const state = {
2535
+ missing: [],
2536
+ behind: [],
2537
+ notFromSystemDox: [],
2538
+ pluginInstalled: !!entry,
2539
+ pluginVersion: entry?.version
2540
+ };
2541
+ const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
2542
+ for (const skill of setup.sessionBrief?.skills ?? []) {
2543
+ const dir = defaultSkillDir(skill.name);
2544
+ if (!existsSync6(dir)) {
2545
+ if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
2546
+ continue;
2547
+ }
2548
+ const manifest = await readJsonQuietly2(join7(dir, MANIFEST_FILE));
2549
+ if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
2550
+ state.notFromSystemDox.push(skill.name);
2551
+ } else if (manifest.version !== skill.latestVersion) {
2552
+ state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
2553
+ }
2554
+ }
2555
+ return state;
2556
+ }
2557
+ function hasPersonParts(setup) {
2558
+ return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
2559
+ }
2560
+ function listWithin(items, room) {
2561
+ const whole2 = items.join(", ");
2562
+ if (whole2.length <= room) return whole2;
2563
+ const kept = [];
2564
+ let used = 0;
2565
+ for (let i = 0; i < items.length; i++) {
2566
+ const rest = items.length - i - 1;
2567
+ const cost = items[i].length + (kept.length ? 2 : 0);
2568
+ if (used + cost + ` and ${MORE(rest)}`.length > room) break;
2569
+ kept.push(items[i]);
2570
+ used += cost;
2571
+ }
2572
+ return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
2573
+ }
2574
+ function localSection(setup, state, room) {
2575
+ const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
2576
+ const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
2577
+ const lines = [];
2578
+ if (state.missing.length) {
2579
+ lines.push({
2580
+ head: "On this computer, missing: ",
2581
+ items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
2582
+ 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.`
2583
+ });
2584
+ }
2585
+ if (state.behind.length) {
2586
+ lines.push({
2587
+ head: "On this computer, behind: ",
2588
+ items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
2589
+ tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
2590
+ });
2591
+ }
2592
+ if (state.notFromSystemDox.length) {
2593
+ lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
2594
+ }
2595
+ const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
2596
+ while (lines.length && fixed() > room) lines.pop();
2597
+ if (!lines.length) return "";
2598
+ const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
2599
+ return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
2600
+ }
2601
+ function fit(text3, section) {
2602
+ const whole2 = section ? `${text3}
2603
+
2604
+ ${section}` : text3;
2605
+ if (whole2.length <= INSTRUCTIONS_MAX) return whole2;
2606
+ return text3.length <= INSTRUCTIONS_MAX ? text3 : `${text3.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
2607
+ }
2608
+ async function readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
2609
+ if (keyProblem2) return { problem: noKey(keyProblem2) };
2610
+ try {
2611
+ const setup = await apiClient2.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
2612
+ return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
2613
+ } catch (err) {
2614
+ const status = err?.status;
2615
+ return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
2616
+ }
2617
+ }
2618
+ async function sessionInstructions(apiClient2, options = {}) {
2619
+ const read = await readTeamSetup(apiClient2, options);
2620
+ if ("problem" in read) return read.problem;
2621
+ const text3 = read.setup.sessionBrief?.text ?? olderApi(read.setup);
2622
+ try {
2623
+ if (!options.localFiles) return fit(text3, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
2624
+ const room = INSTRUCTIONS_MAX - text3.length - 2;
2625
+ return fit(text3, localSection(read.setup, await computerState(read.setup), room));
2626
+ } catch {
2627
+ return fit(text3, "");
2628
+ }
2629
+ }
2630
+ var INSTRUCTIONS_MAX, BRIEF_TIMEOUT_MS, NO_KEY, SETUP_COMMAND, REFUSED, NOT_CHECKED, CANNOT_SEE, MORE, LIST_MIN;
2631
+ var init_session_brief = __esm({
2632
+ "src/lib/session-brief.ts"() {
2633
+ "use strict";
2634
+ init_skills();
2635
+ init_team_setup_files();
2636
+ INSTRUCTIONS_MAX = 2e3;
2637
+ BRIEF_TIMEOUT_MS = 8e3;
2638
+ NO_KEY = "no-key-on-this-computer";
2639
+ SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
2640
+ 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.`;
2641
+ 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.";
2642
+ 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.";
2643
+ MORE = (n) => `${n} more (get_team_setup lists them)`;
2644
+ LIST_MIN = `all ${MORE(100)}`.length;
2645
+ }
2646
+ });
2647
+
2511
2648
  // src/lib/language.ts
2512
2649
  function detectLanguage(filePath) {
2513
2650
  const ext = filePath.slice(filePath.lastIndexOf("."));
@@ -2620,7 +2757,7 @@ var init_config_links = __esm({
2620
2757
 
2621
2758
  // src/tools/get-guardrails.ts
2622
2759
  import { z } from "zod";
2623
- function registerGetGuardrails(server2, apiClient2) {
2760
+ function registerGetGuardrails(server, apiClient2) {
2624
2761
  const handler = async ({
2625
2762
  language,
2626
2763
  category,
@@ -2674,7 +2811,7 @@ ${text3}`
2674
2811
  ]
2675
2812
  };
2676
2813
  };
2677
- server2.registerTool(
2814
+ server.registerTool(
2678
2815
  "get_checks",
2679
2816
  {
2680
2817
  title: "Get Checks",
@@ -2683,7 +2820,7 @@ ${text3}`
2683
2820
  },
2684
2821
  handler
2685
2822
  );
2686
- server2.registerTool(
2823
+ server.registerTool(
2687
2824
  "get_guardrails",
2688
2825
  {
2689
2826
  title: "Get Checks (legacy name)",
@@ -2717,8 +2854,8 @@ var init_get_guardrails = __esm({
2717
2854
 
2718
2855
  // src/tools/get-specs.ts
2719
2856
  import { z as z2 } from "zod";
2720
- function registerGetSpecs(server2, apiClient2) {
2721
- server2.registerTool(
2857
+ function registerGetSpecs(server, apiClient2) {
2858
+ server.registerTool(
2722
2859
  "get_specs",
2723
2860
  {
2724
2861
  title: "Get Specs",
@@ -2808,8 +2945,8 @@ function matchGlob(path, pattern) {
2808
2945
  const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
2809
2946
  return new RegExp(`^${regex}$`).test(path);
2810
2947
  }
2811
- function registerGetDocs(server2, apiClient2) {
2812
- server2.registerTool(
2948
+ function registerGetDocs(server, apiClient2) {
2949
+ server.registerTool(
2813
2950
  "get_docs",
2814
2951
  {
2815
2952
  title: "Get Docs",
@@ -2912,8 +3049,8 @@ var init_get_docs = __esm({
2912
3049
  });
2913
3050
 
2914
3051
  // src/tools/get-tech-stack.ts
2915
- function registerGetTechStack(server2, apiClient2) {
2916
- server2.registerTool(
3052
+ function registerGetTechStack(server, apiClient2) {
3053
+ server.registerTool(
2917
3054
  "get_tech_stack",
2918
3055
  {
2919
3056
  title: "Get Tech Stack",
@@ -2968,8 +3105,8 @@ var init_get_tech_stack = __esm({
2968
3105
 
2969
3106
  // src/tools/get-planning-context.ts
2970
3107
  import { z as z4 } from "zod";
2971
- function registerGetPlanningContext(server2, apiClient2) {
2972
- server2.registerTool(
3108
+ function registerGetPlanningContext(server, apiClient2) {
3109
+ server.registerTool(
2973
3110
  "get_planning_context",
2974
3111
  {
2975
3112
  title: "Get Planning Context",
@@ -3003,7 +3140,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
3003
3140
  ${repos}`);
3004
3141
  }
3005
3142
  if (ctx.dependencies.length > 0) {
3006
- const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
3143
+ const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}${d.provenance === "inferred" ? " (inferred)" : ""}`).join("\n");
3007
3144
  sections.push(`## Cross-Repo Dependencies
3008
3145
 
3009
3146
  ${deps}`);
@@ -3083,8 +3220,8 @@ function formatItem(item2) {
3083
3220
  ${meta.join(" \xB7 ")}` : "") + (item2.url ? `
3084
3221
  ${item2.url}` : "");
3085
3222
  }
3086
- function registerGetPlanBoard(server2, apiClient2) {
3087
- server2.registerTool(
3223
+ function registerGetPlanBoard(server, apiClient2) {
3224
+ server.registerTool(
3088
3225
  "get_plan_board",
3089
3226
  {
3090
3227
  title: "Get Plan Board",
@@ -3341,8 +3478,8 @@ function formatChange(change) {
3341
3478
  briefVerdict(change.states)
3342
3479
  ].join("\n\n");
3343
3480
  }
3344
- function registerGetChange(server2, apiClient2) {
3345
- server2.registerTool(
3481
+ function registerGetChange(server, apiClient2) {
3482
+ server.registerTool(
3346
3483
  "get_change",
3347
3484
  {
3348
3485
  title: "Get Change",
@@ -3696,16 +3833,22 @@ async function hydrate(api, item2, meta) {
3696
3833
  }
3697
3834
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
3698
3835
  const budget = maxTokens ?? DEFAULT_BUDGET;
3836
+ const failures = [];
3837
+ const orEmpty = (empty) => (err) => {
3838
+ failures.push(err);
3839
+ return empty;
3840
+ };
3699
3841
  const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
3700
- api.get("/v1/guardrails").catch(() => []),
3701
- api.get("/v1/specs").catch(() => []),
3702
- api.get("/v1/docs").catch(() => ({ items: [] })),
3703
- api.get("/v1/ai/settings/stack").catch(() => []),
3704
- api.get("/v1/standards/rules").catch(() => []),
3842
+ api.get("/v1/guardrails").catch(orEmpty([])),
3843
+ api.get("/v1/specs").catch(orEmpty([])),
3844
+ api.get("/v1/docs").catch(orEmpty({ items: [] })),
3845
+ api.get("/v1/ai/settings/stack").catch(orEmpty([])),
3846
+ api.get("/v1/standards/rules").catch(orEmpty([])),
3705
3847
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
3706
3848
  // blip) gates nothing, it can never hide working context.
3707
3849
  api.get("/v1/doc-repos").catch(() => null)
3708
3850
  ]);
3851
+ if (failures.length === 5) throw failures[0];
3709
3852
  const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
3710
3853
  const guardrails = disabled("fitness") ? [] : rawGuardrails;
3711
3854
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
@@ -3912,12 +4055,13 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
3912
4055
  guardrails: selectedGuardrails.length
3913
4056
  };
3914
4057
  if (sections.length === 0) {
3915
- return { text: "No relevant context found for this task.", stats };
4058
+ 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.";
4059
+ return { text: text3, stats };
3916
4060
  }
3917
4061
  const footer = `
3918
4062
 
3919
4063
  ---
3920
- _Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
4064
+ _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` : "") + `_`;
3921
4065
  return { text: body + footer, stats };
3922
4066
  }
3923
4067
  var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS, SECTION_STOP_WORDS;
@@ -3961,8 +4105,8 @@ var init_context_builder = __esm({
3961
4105
 
3962
4106
  // src/tools/get-context.ts
3963
4107
  import { z as z7 } from "zod";
3964
- function registerGetContext(server2, apiClient2) {
3965
- server2.registerTool(
4108
+ function registerGetContext(server, apiClient2) {
4109
+ server.registerTool(
3966
4110
  "get_context",
3967
4111
  {
3968
4112
  title: "Get Context",
@@ -3996,9 +4140,254 @@ var init_get_context = __esm({
3996
4140
  }
3997
4141
  });
3998
4142
 
3999
- // src/tools/create-guardrail.ts
4143
+ // src/tools/get-team-setup.ts
4144
+ function describeSetup(setup) {
4145
+ const lines = [`# Team setup of ${setup.workspace}`, ""];
4146
+ const skills = setup.sessionBrief?.skills ?? [];
4147
+ const held = setup.sessionBrief?.heldSkills ?? [];
4148
+ lines.push(`## Skills a session installs by itself (${skills.length})`, "");
4149
+ if (skills.length) {
4150
+ for (const s of skills) lines.push(`- **${s.name}** version ${s.latestVersion}: ${s.description}`);
4151
+ } else if (!setup.sessionBrief && setup.skills.length) {
4152
+ lines.push(`- ${setup.skills.join(", ")}`);
4153
+ } else {
4154
+ lines.push("None.");
4155
+ }
4156
+ lines.push("");
4157
+ if (held.length) {
4158
+ lines.push(`## Skills that wait for the person (${held.length})`, "");
4159
+ for (const h of held) lines.push(`- **${h.name}**: ${HELD_BECAUSE[h.reason] ?? h.reason}`);
4160
+ lines.push("", "Do not install these yourself: each is the person's to decide.", "");
4161
+ }
4162
+ if (setup.plugin) {
4163
+ lines.push(`## Team plugin`, "", `\`${setup.workspace}-setup\` version ${setup.plugin.version}.`);
4164
+ for (const c of setup.plugin.hookCommands ?? []) lines.push(`- Hook: ${JSON.stringify(c)}`);
4165
+ for (const m of setup.plugin.mcpServers ?? []) {
4166
+ lines.push(`- MCP server ${JSON.stringify(m.name)}: ${JSON.stringify(m.url ?? [m.command, ...m.args ?? []].filter(Boolean).join(" "))}`);
4167
+ }
4168
+ lines.push("");
4169
+ }
4170
+ const parts = Object.entries(setup.parts ?? {});
4171
+ if (parts.length) {
4172
+ lines.push(`## Published parts`, "", ...parts.map(([p, v]) => `- ${p}: version ${v}`), "");
4173
+ }
4174
+ if (setup.memory?.length) {
4175
+ lines.push(`## Team memory`, "", ...setup.memory.map((m) => `- ${m.project}: ${m.fileCount} file(s)`), "");
4176
+ }
4177
+ return lines;
4178
+ }
4179
+ function describeComputer(setup, state) {
4180
+ const lines = ["## This computer", ""];
4181
+ if (!setup.sessionBrief) {
4182
+ return [...lines, "This SystemDox API is too old to compare this computer with the team setup."];
4183
+ }
4184
+ const skills = setup.sessionBrief.skills;
4185
+ const latest = new Map(skills.map((s) => [s.name, s.latestVersion]));
4186
+ lines.push(
4187
+ state.missing.length ? `- Missing: ${state.missing.map((name) => `${name} version ${latest.get(name)}`).join(", ")}` : `- Missing: none of ${skills.length}`
4188
+ );
4189
+ if (state.behind.length) {
4190
+ lines.push(`- Behind: ${state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`).join(", ")}`);
4191
+ }
4192
+ if (state.notFromSystemDox.length) lines.push(`- Not from SystemDox, left alone: ${state.notFromSystemDox.join(", ")}`);
4193
+ if (setup.plugin) {
4194
+ lines.push(
4195
+ state.pluginInstalled ? `- Team plugin: installed${state.pluginVersion ? `, ${state.pluginVersion}` : ""}; the latest is version ${setup.plugin.version}` : "- Team plugin: not installed"
4196
+ );
4197
+ }
4198
+ lines.push("", "## What to do", "");
4199
+ const steps = [];
4200
+ 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.");
4201
+ if (state.behind.length) {
4202
+ 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.");
4203
+ }
4204
+ if (hasPersonParts(setup) && !state.pluginInstalled) {
4205
+ steps.push(
4206
+ `Hooks, MCP servers, team instructions and memory need the person: \`${SETUP_COMMAND}\` in their own terminal. Never answer its prompts for them.`
4207
+ );
4208
+ }
4209
+ lines.push(...steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ["Nothing."]);
4210
+ return lines;
4211
+ }
4212
+ function registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 }) {
4213
+ server.registerTool(
4214
+ "get_team_setup",
4215
+ {
4216
+ title: "Get Team Setup",
4217
+ 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.",
4218
+ inputSchema: {},
4219
+ annotations: { readOnlyHint: true }
4220
+ },
4221
+ async () => {
4222
+ const read = await readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs: 15e3 });
4223
+ if ("problem" in read) return { content: [{ type: "text", text: read.problem }] };
4224
+ const lines = describeSetup(read.setup);
4225
+ if (localFiles) lines.push(...describeComputer(read.setup, await computerState(read.setup)));
4226
+ else lines.push(CANNOT_SEE);
4227
+ return { content: [{ type: "text", text: lines.join("\n") }] };
4228
+ }
4229
+ );
4230
+ }
4231
+ var HELD_BECAUSE;
4232
+ var init_get_team_setup = __esm({
4233
+ "src/tools/get-team-setup.ts"() {
4234
+ "use strict";
4235
+ init_session_brief();
4236
+ HELD_BECAUSE = {
4237
+ "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`,
4238
+ "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>`",
4239
+ "not-checked": "it could not be checked. Ask again later"
4240
+ };
4241
+ }
4242
+ });
4243
+
4244
+ // src/tools/document-graph.ts
4000
4245
  import { z as z8 } from "zod";
4001
- function registerCreateGuardrail(server2, apiClient2) {
4246
+ function place(file, line) {
4247
+ return file ? `${file}:${line}` : "made by a person";
4248
+ }
4249
+ function known(provenance, confidence) {
4250
+ return `${provenance} ${confidence}`;
4251
+ }
4252
+ function formatNeighbours(answer) {
4253
+ const lines = [`# ${answer.doc.label}`, `\`${answer.doc.id}\``, ""];
4254
+ if (answer.links.length === 0) lines.push("No links.");
4255
+ for (const link of answer.links) {
4256
+ lines.push(
4257
+ `- ${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)}`
4258
+ );
4259
+ }
4260
+ if (answer.truncated) {
4261
+ lines.push("", `Showing ${answer.links.length} of ${answer.total}. Raise limit to see more.`);
4262
+ }
4263
+ if (answer.ambiguous.length > 0) {
4264
+ lines.push("", "## Ambiguous: not drawn, because each could mean several documents");
4265
+ for (const entry of answer.ambiguous) {
4266
+ const candidates = entry.candidates.map((c) => `${c.label} (${c.repo}, \`${c.id}\`)`);
4267
+ lines.push(`- ${entry.reference} at ${place(entry.file, entry.line)}: ${candidates.join("; ")}`);
4268
+ }
4269
+ }
4270
+ return lines.join("\n");
4271
+ }
4272
+ function formatPath(answer, maxHops) {
4273
+ if (!answer.found) return `No path within ${maxHops} hops.`;
4274
+ const lines = answer.steps.map((step, index) => {
4275
+ const arrow = step.reversed ? "<-" : "->";
4276
+ 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)}`;
4277
+ });
4278
+ return [...lines, "", `Path confidence: ${answer.confidence}`].join("\n");
4279
+ }
4280
+ function formatStats(stats, repoLinks) {
4281
+ const { declared, extracted, inferred, ambiguous } = stats.links;
4282
+ const lines = [
4283
+ `# The graph: ${stats.nodeCount} documents, ${stats.edgeCount} links`,
4284
+ "",
4285
+ `Links: ${declared} declared \xB7 ${extracted} extracted \xB7 ${inferred} inferred \xB7 ${ambiguous} ambiguous (not drawn)`,
4286
+ "",
4287
+ "## Repositories"
4288
+ ];
4289
+ for (const repo of stats.repos) {
4290
+ const state = repo.behind ? `BEHIND: pushed ${repo.headCommit || "unknown"} at ${repo.pushedAt}, index at ${repo.indexedCommit || "unknown"}` : `index at ${repo.indexedCommit || "unknown"}`;
4291
+ lines.push(`- ${repo.repo}: ${repo.documents} documents, ${repo.links} links, ${state}`);
4292
+ }
4293
+ if (repoLinks.length > 0) {
4294
+ lines.push("", "## Repository links");
4295
+ for (const link of repoLinks) {
4296
+ lines.push(
4297
+ `- ${link.source} ${link.relation} ${link.target} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${place(link.file, link.line)}`
4298
+ );
4299
+ }
4300
+ }
4301
+ return lines.join("\n");
4302
+ }
4303
+ function refused(err) {
4304
+ const status = err.status;
4305
+ if (status !== 404 && status !== 400) throw err;
4306
+ return {
4307
+ content: [{ type: "text", text: err.message }],
4308
+ isError: true
4309
+ };
4310
+ }
4311
+ function registerDocumentGraph(server, apiClient2) {
4312
+ server.registerTool(
4313
+ "get_document_links",
4314
+ {
4315
+ 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.",
4316
+ inputSchema: {
4317
+ doc_id: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
4318
+ direction: z8.enum(["out", "in", "both"]).optional().describe("Default both"),
4319
+ relation: z8.enum(["links_to", "cites", "mentions", "satisfies", "references"]).optional().describe("Only links of this kind"),
4320
+ limit: z8.number().int().min(1).max(MAX_LIMIT).optional().describe(`Most links to show (default ${DEFAULT_LIMIT2})`)
4321
+ },
4322
+ annotations: { readOnlyHint: true }
4323
+ },
4324
+ async ({ doc_id, direction, relation, limit }) => {
4325
+ const params = { doc: doc_id, limit: String(limit ?? DEFAULT_LIMIT2) };
4326
+ if (direction) params.direction = direction;
4327
+ if (relation) params.relation = relation;
4328
+ try {
4329
+ const answer = await apiClient2.get("/v1/graph/neighbours", params);
4330
+ return { content: [{ type: "text", text: formatNeighbours(answer) }] };
4331
+ } catch (err) {
4332
+ return refused(err);
4333
+ }
4334
+ }
4335
+ );
4336
+ server.registerTool(
4337
+ "trace_document_path",
4338
+ {
4339
+ 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.",
4340
+ inputSchema: {
4341
+ from: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
4342
+ to: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
4343
+ max_hops: z8.number().int().min(1).max(MAX_HOPS).optional().describe(`Longest path to look for (default ${DEFAULT_HOPS})`)
4344
+ },
4345
+ annotations: { readOnlyHint: true }
4346
+ },
4347
+ async ({ from, to, max_hops }) => {
4348
+ const hops = max_hops ?? DEFAULT_HOPS;
4349
+ try {
4350
+ const answer = await apiClient2.get("/v1/graph/path", {
4351
+ from,
4352
+ to,
4353
+ maxHops: String(hops)
4354
+ });
4355
+ return { content: [{ type: "text", text: formatPath(answer, hops) }] };
4356
+ } catch (err) {
4357
+ return refused(err);
4358
+ }
4359
+ }
4360
+ );
4361
+ server.registerTool(
4362
+ "get_graph_stats",
4363
+ {
4364
+ 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.",
4365
+ inputSchema: {},
4366
+ annotations: { readOnlyHint: true }
4367
+ },
4368
+ async () => {
4369
+ const [stats, repos] = await Promise.all([
4370
+ apiClient2.getFresh("/v1/graph/stats"),
4371
+ apiClient2.getFresh("/v1/graph/repos")
4372
+ ]);
4373
+ return { content: [{ type: "text", text: formatStats(stats, repos.links) }] };
4374
+ }
4375
+ );
4376
+ }
4377
+ var DEFAULT_LIMIT2, MAX_LIMIT, DEFAULT_HOPS, MAX_HOPS;
4378
+ var init_document_graph = __esm({
4379
+ "src/tools/document-graph.ts"() {
4380
+ "use strict";
4381
+ DEFAULT_LIMIT2 = 50;
4382
+ MAX_LIMIT = 200;
4383
+ DEFAULT_HOPS = 6;
4384
+ MAX_HOPS = 8;
4385
+ }
4386
+ });
4387
+
4388
+ // src/tools/create-guardrail.ts
4389
+ import { z as z9 } from "zod";
4390
+ function registerCreateGuardrail(server, apiClient2) {
4002
4391
  const handler = async ({
4003
4392
  title,
4004
4393
  description,
@@ -4035,7 +4424,7 @@ function registerCreateGuardrail(server2, apiClient2) {
4035
4424
  ]
4036
4425
  };
4037
4426
  };
4038
- server2.registerTool(
4427
+ server.registerTool(
4039
4428
  "create_check",
4040
4429
  {
4041
4430
  title: "Create Check",
@@ -4047,7 +4436,7 @@ function registerCreateGuardrail(server2, apiClient2) {
4047
4436
  },
4048
4437
  handler
4049
4438
  );
4050
- server2.registerTool(
4439
+ server.registerTool(
4051
4440
  "create_guardrail",
4052
4441
  {
4053
4442
  title: "Create Check (legacy name)",
@@ -4077,32 +4466,32 @@ var init_create_guardrail = __esm({
4077
4466
  "data-access"
4078
4467
  ];
4079
4468
  inputSchema2 = () => ({
4080
- title: z8.string().describe("Short title for the check"),
4081
- description: z8.string().describe("Detailed description of what the check enforces and why"),
4082
- severity: z8.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
4083
- category: z8.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
4084
- pattern: z8.string().optional().describe("Bad code pattern that should be avoided"),
4085
- fix: z8.string().optional().describe("Correct code pattern to use instead"),
4086
- fitness_test: z8.string().optional().describe("Shell command to detect violations in CI")
4469
+ title: z9.string().describe("Short title for the check"),
4470
+ description: z9.string().describe("Detailed description of what the check enforces and why"),
4471
+ severity: z9.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
4472
+ category: z9.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
4473
+ pattern: z9.string().optional().describe("Bad code pattern that should be avoided"),
4474
+ fix: z9.string().optional().describe("Correct code pattern to use instead"),
4475
+ fitness_test: z9.string().optional().describe("Shell command to detect violations in CI")
4087
4476
  });
4088
4477
  }
4089
4478
  });
4090
4479
 
4091
4480
  // src/tools/update-spec.ts
4092
- import { z as z9 } from "zod";
4093
- function registerUpdateSpec(server2, apiClient2) {
4094
- server2.registerTool(
4481
+ import { z as z10 } from "zod";
4482
+ function registerUpdateSpec(server, apiClient2) {
4483
+ server.registerTool(
4095
4484
  "update_spec",
4096
4485
  {
4097
4486
  title: "Update Spec",
4098
4487
  description: "Update an existing SystemDox spec's proposal, tasks, or requirements. Only draft specs can be modified \u2014 published specs must be versioned first.",
4099
4488
  inputSchema: {
4100
- draft_id: z9.string().describe("ID of the spec to update"),
4101
- section: z9.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
4102
- content: z9.string().describe(
4489
+ draft_id: z10.string().describe("ID of the spec to update"),
4490
+ section: z10.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
4491
+ content: z10.string().describe(
4103
4492
  "New markdown content for the section. For requirements, provide the full requirement content."
4104
4493
  ),
4105
- requirement_name: z9.string().optional().describe(
4494
+ requirement_name: z10.string().optional().describe(
4106
4495
  "Name of the requirement (required when section is 'requirements')"
4107
4496
  )
4108
4497
  },
@@ -4157,19 +4546,19 @@ var init_update_spec = __esm({
4157
4546
  });
4158
4547
 
4159
4548
  // src/tools/report-decision.ts
4160
- import { z as z10 } from "zod";
4161
- function registerReportDecision(server2, apiClient2) {
4162
- server2.registerTool(
4549
+ import { z as z11 } from "zod";
4550
+ function registerReportDecision(server, apiClient2) {
4551
+ server.registerTool(
4163
4552
  "report_decision",
4164
4553
  {
4165
4554
  title: "Report Decision",
4166
4555
  description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
4167
4556
  inputSchema: {
4168
- title: z10.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
4169
- context: z10.string().describe("What prompted this decision \u2014 the problem or constraint"),
4170
- decision: z10.string().describe("What was decided and why"),
4171
- consequences: z10.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
4172
- related_files: z10.array(z10.string()).optional().describe("File paths related to this decision")
4557
+ title: z11.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
4558
+ context: z11.string().describe("What prompted this decision \u2014 the problem or constraint"),
4559
+ decision: z11.string().describe("What was decided and why"),
4560
+ consequences: z11.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
4561
+ related_files: z11.array(z11.string()).optional().describe("File paths related to this decision")
4173
4562
  },
4174
4563
  annotations: {
4175
4564
  readOnlyHint: false
@@ -4225,15 +4614,15 @@ var init_report_decision = __esm({
4225
4614
  });
4226
4615
 
4227
4616
  // src/tools/get-project-rules.ts
4228
- import { z as z11 } from "zod";
4229
- function registerGetProjectRules(server2, apiClient2) {
4230
- server2.registerTool(
4617
+ import { z as z12 } from "zod";
4618
+ function registerGetProjectRules(server, apiClient2) {
4619
+ server.registerTool(
4231
4620
  "get_project_rules",
4232
4621
  {
4233
4622
  title: "Get Project Rules",
4234
4623
  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.",
4235
4624
  inputSchema: {
4236
- category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
4625
+ category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
4237
4626
  }
4238
4627
  },
4239
4628
  async ({ category }) => {
@@ -4300,18 +4689,18 @@ var init_get_project_rules = __esm({
4300
4689
  });
4301
4690
 
4302
4691
  // src/tools/create-project-rule.ts
4303
- import { z as z12 } from "zod";
4304
- function registerCreateProjectRule(server2, apiClient2) {
4305
- server2.registerTool(
4692
+ import { z as z13 } from "zod";
4693
+ function registerCreateProjectRule(server, apiClient2) {
4694
+ server.registerTool(
4306
4695
  "create_project_rule",
4307
4696
  {
4308
4697
  title: "Create Project Rule",
4309
4698
  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.",
4310
4699
  inputSchema: {
4311
- text: z12.string().describe(
4700
+ text: z13.string().describe(
4312
4701
  "The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
4313
4702
  ),
4314
- category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
4703
+ category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
4315
4704
  },
4316
4705
  annotations: {
4317
4706
  readOnlyHint: false
@@ -4341,8 +4730,8 @@ var init_create_project_rule = __esm({
4341
4730
  });
4342
4731
 
4343
4732
  // src/tools/get-prompt-templates.ts
4344
- function registerGetPromptTemplates(server2, apiClient2) {
4345
- server2.registerTool(
4733
+ function registerGetPromptTemplates(server, apiClient2) {
4734
+ server.registerTool(
4346
4735
  "get_prompt_templates",
4347
4736
  {
4348
4737
  title: "Get Prompt Templates",
@@ -4393,17 +4782,17 @@ var init_get_prompt_templates = __esm({
4393
4782
  });
4394
4783
 
4395
4784
  // src/tools/create-prompt-template.ts
4396
- import { z as z13 } from "zod";
4397
- function registerCreatePromptTemplate(server2, apiClient2) {
4398
- server2.registerTool(
4785
+ import { z as z14 } from "zod";
4786
+ function registerCreatePromptTemplate(server, apiClient2) {
4787
+ server.registerTool(
4399
4788
  "create_prompt_template",
4400
4789
  {
4401
4790
  title: "Create Prompt Template",
4402
4791
  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.",
4403
4792
  inputSchema: {
4404
- name: z13.string().describe("Short name for the template"),
4405
- description: z13.string().optional().describe("Brief description of when to use this template"),
4406
- prompt: z13.string().describe("The full prompt text that Gen AI will use when generating content")
4793
+ name: z14.string().describe("Short name for the template"),
4794
+ description: z14.string().optional().describe("Brief description of when to use this template"),
4795
+ prompt: z14.string().describe("The full prompt text that Gen AI will use when generating content")
4407
4796
  },
4408
4797
  annotations: {
4409
4798
  readOnlyHint: false
@@ -4437,7 +4826,7 @@ var init_create_prompt_template = __esm({
4437
4826
  });
4438
4827
 
4439
4828
  // src/tools/skills.ts
4440
- import { z as z14 } from "zod";
4829
+ import { z as z15 } from "zod";
4441
4830
  import { homedir as homedir3 } from "os";
4442
4831
  import { basename as basename2, resolve as resolve6 } from "path";
4443
4832
  function expandHome(path) {
@@ -4446,25 +4835,25 @@ function expandHome(path) {
4446
4835
  function text(t) {
4447
4836
  return { content: [{ type: "text", text: t }] };
4448
4837
  }
4449
- function registerSkillTools(server2, apiClient2, { localFiles }) {
4450
- const fileSchema = z14.object({
4451
- path: z14.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
4452
- content: z14.string().describe("The file's full text, exactly as it is")
4838
+ function registerSkillTools(server, apiClient2, { localFiles }) {
4839
+ const fileSchema = z15.object({
4840
+ path: z15.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
4841
+ content: z15.string().describe("The file's full text, exactly as it is")
4453
4842
  });
4454
- server2.registerTool(
4843
+ server.registerTool(
4455
4844
  "publish_skill",
4456
4845
  {
4457
4846
  title: "Publish Skill",
4458
4847
  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.",
4459
4848
  inputSchema: {
4460
4849
  ...localFiles ? {
4461
- directory: z14.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
4850
+ directory: z15.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
4462
4851
  } : {},
4463
- files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
4464
- name: z14.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
4465
- spaceId: z14.string().optional().describe("Space for a NEW skill (default: your default space)"),
4466
- 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")`),
4467
- baseVersion: z14.number().int().positive().optional().describe(
4852
+ files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
4853
+ name: z15.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
4854
+ spaceId: z15.string().optional().describe("Space for a NEW skill (default: your default space)"),
4855
+ 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")`),
4856
+ baseVersion: z15.number().int().positive().optional().describe(
4468
4857
  "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"
4469
4858
  )
4470
4859
  },
@@ -4502,7 +4891,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
4502
4891
  );
4503
4892
  }
4504
4893
  );
4505
- server2.registerTool(
4894
+ server.registerTool(
4506
4895
  "list_skills",
4507
4896
  {
4508
4897
  title: "List Skills",
@@ -4524,16 +4913,16 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
4524
4913
  );
4525
4914
  }
4526
4915
  );
4527
- server2.registerTool(
4916
+ server.registerTool(
4528
4917
  "install_skill",
4529
4918
  {
4530
4919
  title: "Install Skill",
4531
4920
  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.",
4532
4921
  inputSchema: {
4533
- name: z14.string().describe("The skill's name"),
4534
- version: z14.number().int().positive().optional().describe("A version number (default: the latest)"),
4535
- target: z14.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
4536
- ...localFiles ? { overwrite: z14.boolean().optional().describe("Replace files edited since they were installed") } : {}
4922
+ name: z15.string().describe("The skill's name"),
4923
+ version: z15.number().int().positive().optional().describe("A version number (default: the latest)"),
4924
+ target: z15.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
4925
+ ...localFiles ? { overwrite: z15.boolean().optional().describe("Replace files edited since they were installed") } : {}
4537
4926
  },
4538
4927
  annotations: { readOnlyHint: !localFiles }
4539
4928
  },
@@ -4599,11 +4988,11 @@ var init_team_setup_types = __esm({
4599
4988
  });
4600
4989
 
4601
4990
  // src/tools/team-setup.ts
4602
- import { z as z15 } from "zod";
4603
- import { existsSync as existsSync6 } from "fs";
4991
+ import { z as z16 } from "zod";
4992
+ import { existsSync as existsSync7 } from "fs";
4604
4993
  import { lstat as lstat2 } from "fs/promises";
4605
4994
  import { homedir as homedir4 } from "os";
4606
- import { join as join7, resolve as resolve7 } from "path";
4995
+ import { join as join8, resolve as resolve7 } from "path";
4607
4996
  function expandHome2(path) {
4608
4997
  return path.replace(/^~(?=$|[\\/])/, homedir4());
4609
4998
  }
@@ -4629,21 +5018,21 @@ async function readPartDirectory(part, directory) {
4629
5018
  let present = true;
4630
5019
  for (let i = 1; i <= segments.length; i++) {
4631
5020
  const rel = segments.slice(0, i).join("/");
4632
- if (await isLink(join7(root, ...segments.slice(0, i)))) {
5021
+ if (await isLink(join8(root, ...segments.slice(0, i)))) {
4633
5022
  links.push(rel);
4634
5023
  present = false;
4635
5024
  break;
4636
5025
  }
4637
5026
  }
4638
- if (present && existsSync6(join7(root, ...segments))) paths.push(path);
5027
+ if (present && existsSync7(join8(root, ...segments))) paths.push(path);
4639
5028
  }
4640
5029
  for (const { dir, md } of layout.folders) {
4641
- const full = join7(root, dir);
5030
+ const full = join8(root, dir);
4642
5031
  if (await isLink(full)) {
4643
5032
  links.push(dir);
4644
5033
  continue;
4645
5034
  }
4646
- if (!existsSync6(full)) continue;
5035
+ if (!existsSync7(full)) continue;
4647
5036
  const walked = await walk(full, { skipHidden: true });
4648
5037
  links.push(...walked.links.map((p) => `${dir}/${p}`));
4649
5038
  leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
@@ -4682,24 +5071,24 @@ function describePerson(id, person) {
4682
5071
  );
4683
5072
  return lines.join("\n");
4684
5073
  }
4685
- function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
4686
- const fileSchema = z15.object({
4687
- path: z15.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
4688
- content: z15.string().describe("The file's full text, exactly as it is")
5074
+ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
5075
+ const fileSchema = z16.object({
5076
+ path: z16.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
5077
+ content: z16.string().describe("The file's full text, exactly as it is")
4689
5078
  });
4690
- server2.registerTool(
5079
+ server.registerTool(
4691
5080
  "publish_setup_part",
4692
5081
  {
4693
5082
  title: "Publish Setup Part",
4694
5083
  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."),
4695
5084
  inputSchema: {
4696
- part: z15.enum(PARTS2).describe("Which part of the setup"),
5085
+ part: z16.enum(PARTS2).describe("Which part of the setup"),
4697
5086
  ...localFiles ? {
4698
- directory: z15.string().optional().describe(
5087
+ directory: z16.string().optional().describe(
4699
5088
  "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/"
4700
5089
  )
4701
5090
  } : {},
4702
- files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
5091
+ files: z16.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
4703
5092
  },
4704
5093
  annotations: { readOnlyHint: false }
4705
5094
  },
@@ -4729,13 +5118,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
4729
5118
  return text2(lines.join("\n"));
4730
5119
  }
4731
5120
  );
4732
- server2.registerTool(
5121
+ server.registerTool(
4733
5122
  "resolve_person",
4734
5123
  {
4735
5124
  title: "Resolve Person",
4736
5125
  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.",
4737
5126
  inputSchema: {
4738
- 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")
5127
+ 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")
4739
5128
  },
4740
5129
  annotations: { readOnlyHint: true }
4741
5130
  },
@@ -4746,14 +5135,14 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
4746
5135
  }
4747
5136
  );
4748
5137
  if (!localFiles) return;
4749
- server2.registerTool(
5138
+ server.registerTool(
4750
5139
  "sync_memory",
4751
5140
  {
4752
5141
  title: "Sync Memory",
4753
5142
  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.`,
4754
5143
  inputSchema: {
4755
- 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"),
4756
- dryRun: z15.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
5144
+ 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"),
5145
+ dryRun: z16.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
4757
5146
  },
4758
5147
  annotations: { readOnlyHint: false }
4759
5148
  },
@@ -4788,29 +5177,34 @@ var init_team_setup = __esm({
4788
5177
 
4789
5178
  // src/server.ts
4790
5179
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4791
- function createServer(apiClient2, { localFiles = false } = {}) {
4792
- const server2 = new McpServer({
4793
- name: "systemdox-mcp-server",
4794
- version: SERVER_VERSION
4795
- });
4796
- registerGetGuardrails(server2, apiClient2);
4797
- registerGetSpecs(server2, apiClient2);
4798
- registerGetDocs(server2, apiClient2);
4799
- registerGetTechStack(server2, apiClient2);
4800
- registerGetPlanningContext(server2, apiClient2);
4801
- registerGetPlanBoard(server2, apiClient2);
4802
- registerGetChange(server2, apiClient2);
4803
- registerGetContext(server2, apiClient2);
4804
- registerGetProjectRules(server2, apiClient2);
4805
- registerGetPromptTemplates(server2, apiClient2);
4806
- registerCreateGuardrail(server2, apiClient2);
4807
- registerUpdateSpec(server2, apiClient2);
4808
- registerReportDecision(server2, apiClient2);
4809
- registerCreateProjectRule(server2, apiClient2);
4810
- registerCreatePromptTemplate(server2, apiClient2);
4811
- registerSkillTools(server2, apiClient2, { localFiles });
4812
- registerTeamSetupTools(server2, apiClient2, { localFiles });
4813
- return server2;
5180
+ function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
5181
+ const server = new McpServer(
5182
+ {
5183
+ name: "systemdox-mcp-server",
5184
+ version: SERVER_VERSION
5185
+ },
5186
+ instructions ? { instructions } : void 0
5187
+ );
5188
+ registerGetGuardrails(server, apiClient2);
5189
+ registerGetSpecs(server, apiClient2);
5190
+ registerGetDocs(server, apiClient2);
5191
+ registerGetTechStack(server, apiClient2);
5192
+ registerGetPlanningContext(server, apiClient2);
5193
+ registerGetPlanBoard(server, apiClient2);
5194
+ registerGetChange(server, apiClient2);
5195
+ registerGetContext(server, apiClient2);
5196
+ registerGetProjectRules(server, apiClient2);
5197
+ registerGetPromptTemplates(server, apiClient2);
5198
+ registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 });
5199
+ registerDocumentGraph(server, apiClient2);
5200
+ registerCreateGuardrail(server, apiClient2);
5201
+ registerUpdateSpec(server, apiClient2);
5202
+ registerReportDecision(server, apiClient2);
5203
+ registerCreateProjectRule(server, apiClient2);
5204
+ registerCreatePromptTemplate(server, apiClient2);
5205
+ registerSkillTools(server, apiClient2, { localFiles });
5206
+ registerTeamSetupTools(server, apiClient2, { localFiles });
5207
+ return server;
4814
5208
  }
4815
5209
  var SERVER_VERSION;
4816
5210
  var init_server = __esm({
@@ -4824,6 +5218,8 @@ var init_server = __esm({
4824
5218
  init_get_plan_board();
4825
5219
  init_get_change();
4826
5220
  init_get_context();
5221
+ init_get_team_setup();
5222
+ init_document_graph();
4827
5223
  init_create_guardrail();
4828
5224
  init_update_spec();
4829
5225
  init_report_decision();
@@ -4833,7 +5229,7 @@ var init_server = __esm({
4833
5229
  init_create_prompt_template();
4834
5230
  init_skills2();
4835
5231
  init_team_setup();
4836
- SERVER_VERSION = true ? "0.1.49" : "0.0.0-dev";
5232
+ SERVER_VERSION = true ? "0.1.50" : "0.0.0-dev";
4837
5233
  }
4838
5234
  });
4839
5235
 
@@ -4846,15 +5242,18 @@ __export(index_exports, {
4846
5242
  });
4847
5243
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4848
5244
  async function main() {
4849
- await apiClient.validate();
5245
+ const instructions = await sessionInstructions(apiClient, { localFiles: true, keyProblem });
5246
+ const server = createServer(apiClient, { localFiles: true, instructions, keyProblem });
4850
5247
  const transport = new StdioServerTransport();
4851
5248
  await server.connect(transport);
4852
5249
  }
4853
- var config, apiClient, server;
5250
+ var config, keyProblem, apiClient;
4854
5251
  var init_index = __esm({
4855
5252
  async "src/index.ts"() {
4856
5253
  "use strict";
4857
5254
  init_api_client();
5255
+ init_config();
5256
+ init_session_brief();
4858
5257
  init_team_setup_files();
4859
5258
  init_server();
4860
5259
  init_server();
@@ -4862,10 +5261,15 @@ var init_index = __esm({
4862
5261
  config = await loadLocalConfig();
4863
5262
  } catch (err) {
4864
5263
  console.error(`SystemDox MCP server: ${err.message}`);
4865
- process.exit(1);
5264
+ keyProblem = keyReason(err.message);
5265
+ try {
5266
+ config = loadConfig(NO_KEY);
5267
+ } catch (urlErr) {
5268
+ console.error(`SystemDox MCP server: ${urlErr.message}`);
5269
+ process.exit(1);
5270
+ }
4866
5271
  }
4867
5272
  apiClient = new SystemDoxApiClient(config);
4868
- server = createServer(apiClient, { localFiles: true });
4869
5273
  main();
4870
5274
  }
4871
5275
  });