@systemdox/mcp-server 0.1.48 → 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
@@ -26,8 +26,7 @@ function requireHttpsApiUrl(apiUrl) {
26
26
  }
27
27
  return apiUrl;
28
28
  }
29
- function loadConfig() {
30
- const apiKey = process.env.SYSTEMDOX_API_KEY;
29
+ function loadConfig(apiKey = process.env.SYSTEMDOX_API_KEY) {
31
30
  if (!apiKey) {
32
31
  throw new Error(
33
32
  "SYSTEMDOX_API_KEY is required. Generate one at your SystemDox settings page."
@@ -125,6 +124,31 @@ async function apiKeyFor(workspace, env = process.env) {
125
124
  const entry = (await readCredentials())[workspace];
126
125
  return typeof entry?.apiKey === "string" && entry.apiKey ? entry.apiKey : void 0;
127
126
  }
127
+ async function localApiKey(env = process.env) {
128
+ if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
129
+ const keys = Object.entries(await readCredentials()).filter(
130
+ (e) => typeof e[1]?.apiKey === "string" && e[1].apiKey.length > 0
131
+ );
132
+ const wanted = env.SYSTEMDOX_WORKSPACE;
133
+ if (wanted) {
134
+ const key = keys.find(([ws]) => ws === wanted)?.[1].apiKey;
135
+ if (key) return key;
136
+ throw new Error(
137
+ `~/.claude/systemdox/credentials.json holds no key for ${wanted} (SYSTEMDOX_WORKSPACE). Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.`
138
+ );
139
+ }
140
+ if (keys.length === 1) return keys[0][1].apiKey;
141
+ if (keys.length > 1) {
142
+ throw new Error(
143
+ `~/.claude/systemdox/credentials.json holds keys for ${keys.map(([ws]) => ws).join(", ")}: set SYSTEMDOX_WORKSPACE to the workspace to use, or SYSTEMDOX_API_KEY to its key.`
144
+ );
145
+ }
146
+ throw new Error("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.");
147
+ }
148
+ async function loadLocalConfig(env = process.env) {
149
+ const apiKey = await localApiKey(env);
150
+ return loadConfig(apiKey);
151
+ }
128
152
  function apiUrlFrom(env = process.env) {
129
153
  return requireHttpsApiUrl(env.SYSTEMDOX_API_URL || API_URL_DEFAULT);
130
154
  }
@@ -277,14 +301,6 @@ var init_api_client = __esm({
277
301
  this.config = config2;
278
302
  this.cache = new ResponseCache(config2.cacheTtlMs);
279
303
  }
280
- async validate() {
281
- const res = await this.fetchRaw("/v1/health");
282
- if (!res.ok) {
283
- throw new Error(
284
- `SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
285
- );
286
- }
287
- }
288
304
  async get(path, params) {
289
305
  const url = this.buildUrl(path, params);
290
306
  const cacheKey = url.toString();
@@ -298,8 +314,8 @@ var init_api_client = __esm({
298
314
  }
299
315
  /** A read that skips the cache: an install must take the version that is
300
316
  * latest now, not the one cached before someone published another. */
301
- async getFresh(path) {
302
- const res = await this.fetchRaw(path);
317
+ async getFresh(path, signal) {
318
+ const res = await this.fetchRaw(path, void 0, signal);
303
319
  if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
304
320
  return await res.json();
305
321
  }
@@ -329,8 +345,8 @@ var init_api_client = __esm({
329
345
  // One method per operationId. Every read skips the cache: setup, sync and
330
346
  // plugin-path act on what the workspace holds now.
331
347
  /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
332
- getOwnTeamSetup() {
333
- return this.getFresh(TEAM_SETUP);
348
+ getOwnTeamSetup(signal) {
349
+ return this.getFresh(TEAM_SETUP, signal);
334
350
  }
335
351
  getTeamSetup(workspace) {
336
352
  return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
@@ -434,14 +450,15 @@ var init_api_client = __esm({
434
450
  }
435
451
  return url;
436
452
  }
437
- async fetchRaw(path, params) {
453
+ async fetchRaw(path, params, signal) {
438
454
  const url = this.buildUrl(path, params);
439
455
  return fetch(url, {
440
456
  headers: {
441
457
  Authorization: `Bearer ${this.config.apiKey}`,
442
458
  "X-API-Key": this.config.apiKey,
443
459
  Accept: "application/json"
444
- }
460
+ },
461
+ signal
445
462
  });
446
463
  }
447
464
  };
@@ -1006,11 +1023,11 @@ async function people(opts, ws, files, report, hint) {
1006
1023
  report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
1007
1024
  break;
1008
1025
  }
1009
- const refused = result.refused[0];
1010
- if (refused?.code === "SYD-K021") {
1011
- 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.`);
1012
1029
  choices = choices.filter((c) => c !== "m");
1013
- } else if (refused) out(` ${refused.detail}`);
1030
+ } else if (refused2) out(` ${refused2.detail}`);
1014
1031
  }
1015
1032
  }
1016
1033
  for (let i = 0; i < filed.length; i += 50) {
@@ -1141,17 +1158,6 @@ async function upload(client, ws, project, plan, team, report) {
1141
1158
  }
1142
1159
  await send();
1143
1160
  }
1144
- async function keyFromEnvironment(env) {
1145
- if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
1146
- const entries2 = Object.entries(await readCredentials()).filter(([, v]) => typeof v?.apiKey === "string" && v.apiKey);
1147
- if (entries2.length === 1) return entries2[0][1].apiKey;
1148
- if (entries2.length > 1) {
1149
- throw new SyncFailed(
1150
- `~/.claude/systemdox/credentials.json holds keys for ${entries2.map(([w]) => w).join(", ")}: set SYSTEMDOX_API_KEY to the key of the workspace to sync.`
1151
- );
1152
- }
1153
- throw new SyncFailed("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup, then run this again.");
1154
- }
1155
1161
  async function syncMemory(opts) {
1156
1162
  const { client, out } = opts;
1157
1163
  const now = opts.now?.() ?? /* @__PURE__ */ new Date();
@@ -1287,7 +1293,7 @@ async function runSyncMemory(argv, io, env = process.env) {
1287
1293
  args2 = parseSyncMemoryArgs(argv);
1288
1294
  if (!client) {
1289
1295
  const apiUrl = apiUrlFrom(env);
1290
- const apiKey = await keyFromEnvironment(env);
1296
+ const apiKey = await localApiKey(env);
1291
1297
  client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
1292
1298
  }
1293
1299
  } catch (e) {
@@ -1380,6 +1386,7 @@ __export(setup_team_exports, {
1380
1386
  parseSetupArgs: () => parseSetupArgs,
1381
1387
  runTeamSetup: () => runTeamSetup,
1382
1388
  runTeamSetupCommand: () => runTeamSetupCommand,
1389
+ setInJsonFile: () => setInJsonFile,
1383
1390
  setJsonMember: () => setJsonMember,
1384
1391
  withImportBlock: () => withImportBlock
1385
1392
  });
@@ -1759,9 +1766,9 @@ async function runTeamSetup(opts) {
1759
1766
  const id = `${ws}-setup@${marketplaceName}`;
1760
1767
  const installed = await readJsonQuietly(join3(pluginsDir, "installed_plugins.json"));
1761
1768
  if (!installed?.plugins?.[id]) {
1762
- const known = await readJsonQuietly(join3(pluginsDir, "known_marketplaces.json"));
1769
+ const known2 = await readJsonQuietly(join3(pluginsDir, "known_marketplaces.json"));
1763
1770
  const install = `Run \`claude plugin install ${id}\` in your own terminal and accept the command it shows.`;
1764
- 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}`];
1765
1772
  out("");
1766
1773
  steps.forEach((l) => out(l));
1767
1774
  }
@@ -1812,12 +1819,191 @@ ${lines.join("\n")}`)
1812
1819
  }
1813
1820
  });
1814
1821
 
1822
+ // src/setup-mcp.ts
1823
+ var setup_mcp_exports = {};
1824
+ __export(setup_mcp_exports, {
1825
+ findConfigTargets: () => findConfigTargets,
1826
+ serverEntry: () => serverEntry,
1827
+ setupMcpServer: () => setupMcpServer,
1828
+ writeServerEntry: () => writeServerEntry
1829
+ });
1830
+ import { existsSync as existsSync3, readFileSync } from "fs";
1831
+ import { join as join4 } from "path";
1832
+ async function validateApiKey(apiKey, apiUrl) {
1833
+ try {
1834
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/v1/health`, {
1835
+ headers: {
1836
+ Authorization: `Bearer ${apiKey}`,
1837
+ "X-API-Key": apiKey,
1838
+ Accept: "application/json"
1839
+ }
1840
+ });
1841
+ return res.ok;
1842
+ } catch {
1843
+ return false;
1844
+ }
1845
+ }
1846
+ function findConfigTargets(cwd = process.cwd()) {
1847
+ const home = homeDir();
1848
+ return [
1849
+ // Claude Code: the project's .mcp.json, and ~/.claude.json (all projects)
1850
+ { name: "Claude Code (project)", path: join4(cwd, ".mcp.json"), detected: true },
1851
+ { name: "Claude Code (user \u2014 all projects)", path: join4(home, ".claude.json"), detected: true },
1852
+ { name: "Cursor (project)", path: join4(cwd, ".cursor", "mcp.json"), detected: existsSync3(join4(cwd, ".cursor")) },
1853
+ { name: "VS Code (project)", path: join4(cwd, ".vscode", "mcp.json"), detected: existsSync3(join4(cwd, ".vscode")) },
1854
+ { name: "Cursor (global)", path: join4(home, ".cursor", "mcp.json"), detected: existsSync3(join4(home, ".cursor")) }
1855
+ ];
1856
+ }
1857
+ function serverEntry(apiUrl, existing, workspace) {
1858
+ const env = {};
1859
+ const old = existing && typeof existing === "object" ? existing.env : void 0;
1860
+ if (old && typeof old === "object" && !Array.isArray(old)) {
1861
+ for (const [name, value] of Object.entries(old)) {
1862
+ if (typeof value !== "string" || name === "SYSTEMDOX_API_URL" || name === "SYSTEMDOX_WORKSPACE") continue;
1863
+ if (name === "SYSTEMDOX_API_KEY" && !isReference(value)) continue;
1864
+ env[name] = value;
1865
+ }
1866
+ }
1867
+ if (apiUrl !== API_URL_DEFAULT) env.SYSTEMDOX_API_URL = apiUrl;
1868
+ if (workspace) env.SYSTEMDOX_WORKSPACE = workspace;
1869
+ const entry = { command: "npx", args: ["@systemdox/mcp-server"] };
1870
+ if (Object.keys(env).length) entry.env = env;
1871
+ return entry;
1872
+ }
1873
+ function currentEntry(path) {
1874
+ let text3;
1875
+ try {
1876
+ text3 = readFileSync(path, "utf8");
1877
+ } catch (e) {
1878
+ if (e.code === "ENOENT") return void 0;
1879
+ throw e;
1880
+ }
1881
+ if (!text3.trim()) return void 0;
1882
+ let config2;
1883
+ try {
1884
+ config2 = JSON.parse(text3);
1885
+ } catch {
1886
+ throw new Error(`${path} is not valid JSON: fix it, then run setup again.`);
1887
+ }
1888
+ const servers = config2 && typeof config2 === "object" ? config2.mcpServers : void 0;
1889
+ return servers && typeof servers === "object" ? servers.systemdox : void 0;
1890
+ }
1891
+ async function writeServerEntry(path, apiUrl, workspace) {
1892
+ const existing = currentEntry(path);
1893
+ const env = existing && typeof existing === "object" ? existing.env : void 0;
1894
+ const heldKey = !!env && typeof env === "object" && "SYSTEMDOX_API_KEY" in env && !isReference(env.SYSTEMDOX_API_KEY);
1895
+ await setInJsonFile(path, ["mcpServers", "systemdox"], serverEntry(apiUrl, existing, workspace));
1896
+ return heldKey;
1897
+ }
1898
+ async function keyWorkspace(opts, apiKey, apiUrl) {
1899
+ const client = opts.client?.(apiKey, apiUrl) ?? new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
1900
+ try {
1901
+ const { workspace } = await client.getOwnTeamSetup();
1902
+ if (isWorkspace(workspace)) return workspace;
1903
+ opts.out(` SystemDox named the workspace ${JSON.stringify(workspace)}, which is not a workspace name.`);
1904
+ } catch (e) {
1905
+ opts.out(` SystemDox did not name your key's workspace (${e.message}).`);
1906
+ }
1907
+ const fromEnv = (opts.env ?? process.env).SYSTEMDOX_WORKSPACE;
1908
+ if (isWorkspace(fromEnv)) return fromEnv;
1909
+ const typed = (await opts.ask(" Your workspace (acme in acme.systemdox.com): ")).trim().toLowerCase();
1910
+ return isWorkspace(typed) ? typed : void 0;
1911
+ }
1912
+ async function setupMcpServer(opts) {
1913
+ const { out, err, ask } = opts;
1914
+ out("\n SystemDox MCP Server Setup\n");
1915
+ const apiKey = (await ask(" Enter your SystemDox API key: ")).trim();
1916
+ if (!apiKey) {
1917
+ err("\n Error: API key is required.\n Generate one at your SystemDox settings page.\n");
1918
+ return { code: 1 };
1919
+ }
1920
+ const apiUrl = (await ask(` API URL [${API_URL_DEFAULT}]: `)).trim() || API_URL_DEFAULT;
1921
+ try {
1922
+ requireHttpsApiUrl(apiUrl);
1923
+ } catch (e) {
1924
+ err(`
1925
+ Error: ${e.message}
1926
+ `);
1927
+ return { code: 1 };
1928
+ }
1929
+ out("\n Validating API key...");
1930
+ if (!await (opts.validateKey ?? validateApiKey)(apiKey, apiUrl)) {
1931
+ err("\n Error: Invalid API key or cannot reach the SystemDox API.\n Check your key and try again.\n");
1932
+ return { code: 1 };
1933
+ }
1934
+ out(" API key validated successfully.\n");
1935
+ const workspace = await keyWorkspace(opts, apiKey, apiUrl);
1936
+ if (!workspace) {
1937
+ err("\n Error: that is not a workspace name (lower case letters, digits and -). Nothing was changed.\n");
1938
+ return { code: 1 };
1939
+ }
1940
+ const detected = findConfigTargets(opts.cwd).filter((t) => t.detected);
1941
+ out(" Available targets:");
1942
+ detected.forEach((t, i) => out(` ${i + 1}. ${t.name} (${t.path})`));
1943
+ const choice = (await ask(`
1944
+ Which target? [1-${detected.length}, or 'all']: `)).trim();
1945
+ let selected;
1946
+ if (choice.toLowerCase() === "all") {
1947
+ selected = detected;
1948
+ } else {
1949
+ const idx = parseInt(choice, 10) - 1;
1950
+ if (isNaN(idx) || idx < 0 || idx >= detected.length) {
1951
+ err("\n Invalid selection.\n");
1952
+ return { code: 1 };
1953
+ }
1954
+ selected = [detected[idx]];
1955
+ }
1956
+ try {
1957
+ if ((await readCredentials())[workspace]?.apiKey !== apiKey) await writeCredentials(workspace, apiKey);
1958
+ const several = Object.keys(await readCredentials()).length > 1;
1959
+ for (const target of selected) {
1960
+ const removed = await writeServerEntry(target.path, apiUrl, several ? workspace : void 0);
1961
+ out(` Configured: ${target.name}`);
1962
+ if (removed) out(` The key now lives in ~/.claude/systemdox/credentials.json and was removed from ${target.path}.`);
1963
+ }
1964
+ } catch (e) {
1965
+ err(`
1966
+ Error: ${e.message}
1967
+ `);
1968
+ return { code: 1 };
1969
+ }
1970
+ out(
1971
+ `
1972
+ Your key for ${workspace} is in ${credentialsPath()} (readable by you alone), not in these configs.
1973
+
1974
+ Done! Restart your AI coding tool to activate the SystemDox MCP server.
1975
+ The server provides 25 tools: get_context, get_team_setup, get_checks,
1976
+ get_project_rules, get_prompt_templates, get_specs, get_docs,
1977
+ get_tech_stack, get_planning_context, get_plan_board, get_change,
1978
+ get_document_links, trace_document_path, get_graph_stats,
1979
+ create_check, create_project_rule, create_prompt_template,
1980
+ update_spec, report_decision, publish_skill, list_skills,
1981
+ install_skill, publish_setup_part, resolve_person, sync_memory.
1982
+
1983
+ get_guardrails and create_guardrail remain registered as deprecated
1984
+ aliases of get_checks and create_check \u2014 existing configs keep working.
1985
+ `
1986
+ );
1987
+ return { code: 0, apiKey, apiUrl, workspace };
1988
+ }
1989
+ var isReference;
1990
+ var init_setup_mcp = __esm({
1991
+ "src/setup-mcp.ts"() {
1992
+ "use strict";
1993
+ init_api_client();
1994
+ init_config();
1995
+ init_team_setup_files();
1996
+ init_setup_team();
1997
+ isReference = (value) => typeof value === "string" && /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value);
1998
+ }
1999
+ });
2000
+
1815
2001
  // src/lib/skills.ts
1816
2002
  import { createHash as createHash3 } from "crypto";
1817
2003
  import { lstat, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
1818
- import { existsSync as existsSync3 } from "fs";
2004
+ import { existsSync as existsSync4 } from "fs";
1819
2005
  import { homedir as homedir2 } from "os";
1820
- import { dirname as dirname4, join as join4, relative, resolve as resolve3, sep } from "path";
2006
+ import { dirname as dirname4, join as join5, relative, resolve as resolve3, sep } from "path";
1821
2007
  function sha2563(content) {
1822
2008
  return createHash3("sha256").update(content, "utf8").digest("hex");
1823
2009
  }
@@ -1825,7 +2011,7 @@ function isValidName(name) {
1825
2011
  return name.length <= 64 && NAME_RE.test(name);
1826
2012
  }
1827
2013
  function isValidPath(path) {
1828
- 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);
1829
2015
  }
1830
2016
  function frontmatterName(skillMd) {
1831
2017
  const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
@@ -1834,12 +2020,12 @@ function frontmatterName(skillMd) {
1834
2020
  return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
1835
2021
  }
1836
2022
  function defaultSkillDir(name) {
1837
- return join4(homedir2(), ".claude", "skills", name);
2023
+ return join5(homedir2(), ".claude", "skills", name);
1838
2024
  }
1839
2025
  async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
1840
2026
  for (const entry of await readdir3(dir, { withFileTypes: true })) {
1841
2027
  if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
1842
- const full = join4(dir, entry.name);
2028
+ const full = join5(dir, entry.name);
1843
2029
  const rel = relative(root, full).split(sep).join("/");
1844
2030
  if (entry.isSymbolicLink()) out.links.push(rel);
1845
2031
  else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
@@ -1860,7 +2046,7 @@ async function readTextFiles(root, paths, what) {
1860
2046
  if (!isValidPath(path)) {
1861
2047
  throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
1862
2048
  }
1863
- const raw = await readFile4(join4(root, ...path.split("/")));
2049
+ const raw = await readFile4(join5(root, ...path.split("/")));
1864
2050
  if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
1865
2051
  if (raw.length > MAX_FILE_BYTES) {
1866
2052
  throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
@@ -1881,7 +2067,7 @@ async function readTextFiles(root, paths, what) {
1881
2067
  }
1882
2068
  async function readSkillDirectory(directory) {
1883
2069
  const root = resolve3(directory);
1884
- if (!existsSync3(join4(root, ENTRY_FILE))) {
2070
+ if (!existsSync4(join5(root, ENTRY_FILE))) {
1885
2071
  throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
1886
2072
  }
1887
2073
  const walked = await walk(root, { skipHidden: true });
@@ -1902,7 +2088,7 @@ function within(dest, path) {
1902
2088
  }
1903
2089
  async function readManifest(dest) {
1904
2090
  try {
1905
- return JSON.parse(await readFile4(join4(dest, MANIFEST_FILE), "utf8"));
2091
+ return JSON.parse(await readFile4(join5(dest, MANIFEST_FILE), "utf8"));
1906
2092
  } catch {
1907
2093
  return void 0;
1908
2094
  }
@@ -1911,24 +2097,24 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
1911
2097
  if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
1912
2098
  const incoming = new Map(version.files.map((f) => [f.path, f]));
1913
2099
  for (const f of version.files) within(dest, f.path);
1914
- if (existsSync3(dest) && (await lstat(dest)).isSymbolicLink()) {
2100
+ if (existsSync4(dest) && (await lstat(dest)).isSymbolicLink()) {
1915
2101
  throw new Error(`${dest} is a link: install into a real folder`);
1916
2102
  }
1917
- const walked = existsSync3(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
2103
+ const walked = existsSync4(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
1918
2104
  if (walked.links.length) {
1919
2105
  throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
1920
2106
  }
1921
2107
  const present = walked.files;
1922
2108
  const manifest = await readManifest(dest);
1923
- const known = Object.fromEntries(
2109
+ const known2 = Object.fromEntries(
1924
2110
  Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
1925
2111
  );
1926
2112
  const edited = [];
1927
2113
  const untracked = [];
1928
2114
  for (const path of present) {
1929
- const content = await readFile4(join4(dest, ...path.split("/")), "utf8");
1930
- if (Object.hasOwn(known, path)) {
1931
- if (sha2563(content) !== known[path]) edited.push(path);
2115
+ const content = await readFile4(join5(dest, ...path.split("/")), "utf8");
2116
+ if (Object.hasOwn(known2, path)) {
2117
+ if (sha2563(content) !== known2[path]) edited.push(path);
1932
2118
  } else if (!incoming.has(path) || sha2563(content) !== incoming.get(path).sha256) {
1933
2119
  if (incoming.has(path) || !manifest) edited.push(path);
1934
2120
  else untracked.push(path);
@@ -1945,7 +2131,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
1945
2131
  }
1946
2132
  const removed = [];
1947
2133
  const keptEdited = [];
1948
- for (const path of Object.keys(known)) {
2134
+ for (const path of Object.keys(known2)) {
1949
2135
  if (incoming.has(path) || !present.includes(path)) continue;
1950
2136
  if (edited.includes(path)) {
1951
2137
  keptEdited.push(path);
@@ -1960,7 +2146,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
1960
2146
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
1961
2147
  files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
1962
2148
  };
1963
- await writeFile4(join4(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
2149
+ await writeFile4(join5(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
1964
2150
  return {
1965
2151
  dest,
1966
2152
  version: version.version,
@@ -1986,12 +2172,13 @@ function describeInstall(name, r) {
1986
2172
  lines.push("Start a new Claude Code session to load it.");
1987
2173
  return lines.join("\n");
1988
2174
  }
1989
- 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;
1990
2176
  var init_skills = __esm({
1991
2177
  "src/lib/skills.ts"() {
1992
2178
  "use strict";
1993
2179
  MANIFEST_FILE = ".systemdox-skill.json";
1994
2180
  ENTRY_FILE = "SKILL.md";
2181
+ PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
1995
2182
  MAX_FILES = 100;
1996
2183
  MAX_FILE_BYTES = 256 * 1024;
1997
2184
  MAX_TOTAL_BYTES = 1024 * 1024;
@@ -2108,9 +2295,9 @@ __export(plugin_path_exports, {
2108
2295
  unpackZip: () => unpackZip
2109
2296
  });
2110
2297
  import { createHash as createHash4, randomBytes } from "crypto";
2111
- import { existsSync as existsSync4 } from "fs";
2298
+ import { existsSync as existsSync5 } from "fs";
2112
2299
  import { mkdir as mkdir5, rename as rename2, writeFile as writeFile5 } from "fs/promises";
2113
- import { dirname as dirname5, join as join5, resolve as resolve5, sep as sep2 } from "path";
2300
+ import { dirname as dirname5, join as join6, resolve as resolve5, sep as sep2 } from "path";
2114
2301
  import { inflateRawSync } from "zlib";
2115
2302
  function parsePluginPathArgs(argv) {
2116
2303
  const args2 = {};
@@ -2230,7 +2417,7 @@ async function runPluginPath(argv, io) {
2230
2417
  const folder = pluginDir(workspace, version);
2231
2418
  try {
2232
2419
  const state = await readState(workspace);
2233
- if (existsSync4(folder) && state.plugin?.[String(version)]?.sha256 === sha2564) {
2420
+ if (existsSync5(folder) && state.plugin?.[String(version)]?.sha256 === sha2564) {
2234
2421
  io.out(folder);
2235
2422
  return 0;
2236
2423
  }
@@ -2269,17 +2456,17 @@ async function runPluginPath(argv, io) {
2269
2456
  );
2270
2457
  return 2;
2271
2458
  }
2272
- const tmp = join5(pluginRoot(workspace), `.${version}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
2459
+ const tmp = join6(pluginRoot(workspace), `.${version}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
2273
2460
  try {
2274
2461
  await unpackZip(zip, tmp);
2275
- if (existsSync4(folder) && (await readState(workspace)).plugin?.[String(version)]?.sha256 === sha2564) {
2462
+ if (existsSync5(folder) && (await readState(workspace)).plugin?.[String(version)]?.sha256 === sha2564) {
2276
2463
  await removeFolder(tmp);
2277
2464
  } else {
2278
- if (existsSync4(folder)) await removeFolder(folder);
2465
+ if (existsSync5(folder)) await removeFolder(folder);
2279
2466
  try {
2280
2467
  await rename2(tmp, folder);
2281
2468
  } catch (e) {
2282
- if (!existsSync4(folder)) throw e;
2469
+ if (!existsSync5(folder)) throw e;
2283
2470
  await removeFolder(tmp);
2284
2471
  }
2285
2472
  }
@@ -2316,6 +2503,148 @@ var init_plugin_path = __esm({
2316
2503
  }
2317
2504
  });
2318
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
+
2319
2648
  // src/lib/language.ts
2320
2649
  function detectLanguage(filePath) {
2321
2650
  const ext = filePath.slice(filePath.lastIndexOf("."));
@@ -2428,7 +2757,7 @@ var init_config_links = __esm({
2428
2757
 
2429
2758
  // src/tools/get-guardrails.ts
2430
2759
  import { z } from "zod";
2431
- function registerGetGuardrails(server2, apiClient2) {
2760
+ function registerGetGuardrails(server, apiClient2) {
2432
2761
  const handler = async ({
2433
2762
  language,
2434
2763
  category,
@@ -2482,7 +2811,7 @@ ${text3}`
2482
2811
  ]
2483
2812
  };
2484
2813
  };
2485
- server2.registerTool(
2814
+ server.registerTool(
2486
2815
  "get_checks",
2487
2816
  {
2488
2817
  title: "Get Checks",
@@ -2491,7 +2820,7 @@ ${text3}`
2491
2820
  },
2492
2821
  handler
2493
2822
  );
2494
- server2.registerTool(
2823
+ server.registerTool(
2495
2824
  "get_guardrails",
2496
2825
  {
2497
2826
  title: "Get Checks (legacy name)",
@@ -2525,8 +2854,8 @@ var init_get_guardrails = __esm({
2525
2854
 
2526
2855
  // src/tools/get-specs.ts
2527
2856
  import { z as z2 } from "zod";
2528
- function registerGetSpecs(server2, apiClient2) {
2529
- server2.registerTool(
2857
+ function registerGetSpecs(server, apiClient2) {
2858
+ server.registerTool(
2530
2859
  "get_specs",
2531
2860
  {
2532
2861
  title: "Get Specs",
@@ -2616,8 +2945,8 @@ function matchGlob(path, pattern) {
2616
2945
  const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
2617
2946
  return new RegExp(`^${regex}$`).test(path);
2618
2947
  }
2619
- function registerGetDocs(server2, apiClient2) {
2620
- server2.registerTool(
2948
+ function registerGetDocs(server, apiClient2) {
2949
+ server.registerTool(
2621
2950
  "get_docs",
2622
2951
  {
2623
2952
  title: "Get Docs",
@@ -2720,8 +3049,8 @@ var init_get_docs = __esm({
2720
3049
  });
2721
3050
 
2722
3051
  // src/tools/get-tech-stack.ts
2723
- function registerGetTechStack(server2, apiClient2) {
2724
- server2.registerTool(
3052
+ function registerGetTechStack(server, apiClient2) {
3053
+ server.registerTool(
2725
3054
  "get_tech_stack",
2726
3055
  {
2727
3056
  title: "Get Tech Stack",
@@ -2776,8 +3105,8 @@ var init_get_tech_stack = __esm({
2776
3105
 
2777
3106
  // src/tools/get-planning-context.ts
2778
3107
  import { z as z4 } from "zod";
2779
- function registerGetPlanningContext(server2, apiClient2) {
2780
- server2.registerTool(
3108
+ function registerGetPlanningContext(server, apiClient2) {
3109
+ server.registerTool(
2781
3110
  "get_planning_context",
2782
3111
  {
2783
3112
  title: "Get Planning Context",
@@ -2811,7 +3140,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
2811
3140
  ${repos}`);
2812
3141
  }
2813
3142
  if (ctx.dependencies.length > 0) {
2814
- 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");
2815
3144
  sections.push(`## Cross-Repo Dependencies
2816
3145
 
2817
3146
  ${deps}`);
@@ -2891,8 +3220,8 @@ function formatItem(item2) {
2891
3220
  ${meta.join(" \xB7 ")}` : "") + (item2.url ? `
2892
3221
  ${item2.url}` : "");
2893
3222
  }
2894
- function registerGetPlanBoard(server2, apiClient2) {
2895
- server2.registerTool(
3223
+ function registerGetPlanBoard(server, apiClient2) {
3224
+ server.registerTool(
2896
3225
  "get_plan_board",
2897
3226
  {
2898
3227
  title: "Get Plan Board",
@@ -3048,6 +3377,153 @@ var init_get_plan_board = __esm({
3048
3377
  }
3049
3378
  });
3050
3379
 
3380
+ // src/tools/get-change.ts
3381
+ import { z as z6 } from "zod";
3382
+ function briefVerdict(states) {
3383
+ const waiting = BRIEF.filter(
3384
+ (part) => !(part === "decision" && states.decision === null) && !PAST_REVIEW.has(String(states[part]))
3385
+ ).map((part) => `${part} is ${states[part] ?? "unknown"}`);
3386
+ return waiting.length === 0 ? "The brief is passed" : `The brief is not passed: ${waiting.join(", ")}`;
3387
+ }
3388
+ function prLink(pr) {
3389
+ return `[${pr.repo}#${pr.number}](${pr.url}) ${pr.state}`;
3390
+ }
3391
+ function fileSource(file) {
3392
+ if (file.pullRequest) return prLink(file.pullRequest);
3393
+ return file.direct ? "committed directly" : "\u2014";
3394
+ }
3395
+ function partPullRequests(change, part) {
3396
+ const files = part === "contract" ? change.contract.files : part === "behaviour" ? change.behaviour.files : part === "plan" ? [change.plan.file] : [];
3397
+ const prs = part === "decision" ? [change.decision?.pullRequest ?? null] : part === "release" ? change.release.prs : files.map((file) => file?.pullRequest ?? null);
3398
+ const unique = /* @__PURE__ */ new Map();
3399
+ for (const pr of prs) {
3400
+ if (pr && !unique.has(`${pr.repo}#${pr.number}`)) unique.set(`${pr.repo}#${pr.number}`, pr);
3401
+ }
3402
+ return [...unique.values()].sort((a, b) => Number(b.state === "open") - Number(a.state === "open"));
3403
+ }
3404
+ function countsLine(counts) {
3405
+ const tags = ["proposed", "built", "defect", "unbuilt", "open", "untagged"].filter((tag) => counts[tag] > 0).map((tag) => `${counts[tag]} ${tag}`);
3406
+ return `${counts.total} scenario(s)` + (tags.length > 0 ? `: ${tags.join(", ")}` : "");
3407
+ }
3408
+ function partsTable(change) {
3409
+ const rows = PARTS.filter((part) => !(part === "decision" && change.states.decision === null)).map((part) => {
3410
+ const prs = partPullRequests(change, part);
3411
+ return `| ${part} | ${change.states[part]} | ${prs.length > 0 ? prs.map(prLink).join(", ") : "\u2014"} |`;
3412
+ });
3413
+ return ["| Part | State | Pull request |", "|---|---|---|", ...rows].join("\n");
3414
+ }
3415
+ function decisionSection(change) {
3416
+ const decision = change.decision;
3417
+ if (decision === null) return "## Decision\n\nNone: the proposal names no ADR (a small change).";
3418
+ const status = decision.status || "no record in a connected repository";
3419
+ return `## Decision
3420
+
3421
+ ${decision.adr} \xB7 ${status}${decision.title ? ` \u2014 ${decision.title}` : ""}`;
3422
+ }
3423
+ function contractSection(change) {
3424
+ const files = change.contract.files;
3425
+ return "## Contract\n\n" + (files.length > 0 ? files.map((file) => `- ${file.path} \u2014 ${fileSource(file)}`).join("\n") : "No contract file yet (openapi.yaml, asyncapi.yaml or contract.md).");
3426
+ }
3427
+ function behaviourSection(change) {
3428
+ const { files, counts } = change.behaviour;
3429
+ if (files.length === 0) {
3430
+ return `## Behaviour
3431
+
3432
+ No feature file names this change. A feature file joins it when its path or its first ten lines name \`${change.slug}\`${change.decision ? ` or ${change.decision.adr}` : ""}.`;
3433
+ }
3434
+ const blocks = files.map(
3435
+ (file) => [
3436
+ `### ${file.path}`,
3437
+ "",
3438
+ [file.title, file.runner ? `runner ${file.runner}` : "", fileSource(file), countsLine(file.counts)].filter(Boolean).join(" \xB7 "),
3439
+ "",
3440
+ ...file.scenarios.map((s) => `- ${s.id || "(no id)"} \xB7 ${s.state || "untagged"} \xB7 ${s.title}`)
3441
+ ].join("\n")
3442
+ );
3443
+ return ["## Behaviour", countsLine(counts), ...blocks].join("\n\n");
3444
+ }
3445
+ function planSection(change) {
3446
+ const { file, draftId, board } = change.plan;
3447
+ const boardLine = board ? `[#${board.number} ${board.title}](${board.url}) \u2014 todo ${board.items.todo} \xB7 in progress ${board.items.inProgress} \xB7 done ${board.items.done}` + (board.items.truncated ? " (counted over its first 500 items)" : "") : "none";
3448
+ return [
3449
+ "## Plan",
3450
+ "",
3451
+ `- plan.md: ${file ? `${file.path} \u2014 ${fileSource(file)}` : "none"}`,
3452
+ `- Draft: ${draftId ?? "none"}`,
3453
+ `- Board: ${boardLine}`
3454
+ ].join("\n");
3455
+ }
3456
+ function releaseSection(change) {
3457
+ const lines = change.release.prs.map(
3458
+ (pr) => `- ${prLink(pr)} \xB7 ${pr.title} \xB7 ` + (pr.approvingReview ? `approved by ${pr.approvingReview.by} at ${pr.approvingReview.at}` : "no approving review on its head yet")
3459
+ );
3460
+ return `## Release
3461
+
3462
+ ${lines.length > 0 ? lines.join("\n") : "No release pull request is open."}`;
3463
+ }
3464
+ function formatChange(change) {
3465
+ const header = `# ${change.title}
3466
+
3467
+ \`${change.key}\` \xB7 size: ${change.size}` + (change.archived ? " \xB7 archived: read-only, done" : "") + ` \xB7 synced ${change.syncedAt}`;
3468
+ return [
3469
+ header,
3470
+ partsTable(change),
3471
+ decisionSection(change),
3472
+ contractSection(change),
3473
+ behaviourSection(change),
3474
+ planSection(change),
3475
+ releaseSection(change),
3476
+ // Compact: a change's every scenario with its steps rides in this block.
3477
+ "```json\n" + JSON.stringify(change) + "\n```",
3478
+ briefVerdict(change.states)
3479
+ ].join("\n\n");
3480
+ }
3481
+ function registerGetChange(server, apiClient2) {
3482
+ server.registerTool(
3483
+ "get_change",
3484
+ {
3485
+ title: "Get Change",
3486
+ description: `Read one change (a specs/changes/<slug>/ folder of a connected repository) as SystemDox last synced it from GitHub: the state of each part of its brief (decision, contract, behaviour, plan) and of its release, each with its pull request; the decision's ADR and status; the contract files; every feature file that names the change, with its scenarios as id \xB7 state \xB7 title; the plan (plan.md, the draft, the board); the release pull requests; and the Change object as JSON. The last line says "The brief is passed" or "The brief is not passed: <part> is <state>". Call it before building a story, and stop if the brief is not passed or the story's scenarios are not proposed. Read-only.`,
3487
+ inputSchema: {
3488
+ repo: z6.string().regex(REPO_RE, "the repository as owner/name, e.g. acme/booking-api").describe("The repository that holds the change, as owner/name"),
3489
+ slug: z6.string().max(SLUG_MAX).regex(SLUG_RE2, "a change folder's name: lower-case words joined by -, e.g. customer-reminders").describe("The change's folder name: specs/changes/<slug>/")
3490
+ },
3491
+ annotations: { readOnlyHint: true }
3492
+ },
3493
+ async ({ repo, slug }) => {
3494
+ const [owner, name] = repo.split("/");
3495
+ let change;
3496
+ try {
3497
+ change = await apiClient2.getFresh(
3498
+ `/v1/changes/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/${encodeURIComponent(slug)}`
3499
+ );
3500
+ } catch (err) {
3501
+ if (err.status === 404) {
3502
+ return {
3503
+ content: [{ type: "text", text: `No such change ${repo}/${slug}` }],
3504
+ isError: true
3505
+ };
3506
+ }
3507
+ throw err;
3508
+ }
3509
+ return { content: [{ type: "text", text: formatChange(change) }] };
3510
+ }
3511
+ );
3512
+ }
3513
+ var GITHUB_NAME, REPO_RE, SLUG_RE2, SLUG_MAX, PARTS, BRIEF, PAST_REVIEW;
3514
+ var init_get_change = __esm({
3515
+ "src/tools/get-change.ts"() {
3516
+ "use strict";
3517
+ GITHUB_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,99}";
3518
+ REPO_RE = new RegExp(`^${GITHUB_NAME}/${GITHUB_NAME}$`);
3519
+ SLUG_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
3520
+ SLUG_MAX = 64;
3521
+ PARTS = ["decision", "contract", "behaviour", "plan", "release"];
3522
+ BRIEF = ["decision", "contract", "behaviour", "plan"];
3523
+ PAST_REVIEW = /* @__PURE__ */ new Set(["passed", "being built", "done"]);
3524
+ }
3525
+ });
3526
+
3051
3527
  // src/lib/scoring.ts
3052
3528
  function estimateTokens(text3) {
3053
3529
  return Math.ceil(text3.length / CHARS_PER_TOKEN);
@@ -3357,16 +3833,22 @@ async function hydrate(api, item2, meta) {
3357
3833
  }
3358
3834
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
3359
3835
  const budget = maxTokens ?? DEFAULT_BUDGET;
3836
+ const failures = [];
3837
+ const orEmpty = (empty) => (err) => {
3838
+ failures.push(err);
3839
+ return empty;
3840
+ };
3360
3841
  const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
3361
- api.get("/v1/guardrails").catch(() => []),
3362
- api.get("/v1/specs").catch(() => []),
3363
- api.get("/v1/docs").catch(() => ({ items: [] })),
3364
- api.get("/v1/ai/settings/stack").catch(() => []),
3365
- 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([])),
3366
3847
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
3367
3848
  // blip) gates nothing, it can never hide working context.
3368
3849
  api.get("/v1/doc-repos").catch(() => null)
3369
3850
  ]);
3851
+ if (failures.length === 5) throw failures[0];
3370
3852
  const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
3371
3853
  const guardrails = disabled("fitness") ? [] : rawGuardrails;
3372
3854
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
@@ -3573,12 +4055,13 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
3573
4055
  guardrails: selectedGuardrails.length
3574
4056
  };
3575
4057
  if (sections.length === 0) {
3576
- 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 };
3577
4060
  }
3578
4061
  const footer = `
3579
4062
 
3580
4063
  ---
3581
- _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` : "") + `_`;
3582
4065
  return { text: body + footer, stats };
3583
4066
  }
3584
4067
  var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS, SECTION_STOP_WORDS;
@@ -3621,21 +4104,21 @@ var init_context_builder = __esm({
3621
4104
  });
3622
4105
 
3623
4106
  // src/tools/get-context.ts
3624
- import { z as z6 } from "zod";
3625
- function registerGetContext(server2, apiClient2) {
3626
- server2.registerTool(
4107
+ import { z as z7 } from "zod";
4108
+ function registerGetContext(server, apiClient2) {
4109
+ server.registerTool(
3627
4110
  "get_context",
3628
4111
  {
3629
4112
  title: "Get Context",
3630
4113
  description: "Intelligent context aggregator: fetches checks, specs, docs, and tech stack, ranks them by relevance to your current task, and returns the full text of the highest-ranked items within a token budget \u2014 each with the ID needed to fetch the rest. Use this instead of calling individual tools when you want curated, relevant context.",
3631
4114
  inputSchema: {
3632
- task_description: z6.string().describe(
4115
+ task_description: z7.string().describe(
3633
4116
  "What you're working on (e.g., 'add a new REST endpoint for user preferences')"
3634
4117
  ),
3635
- file_paths: z6.array(z6.string()).optional().describe(
4118
+ file_paths: z7.array(z7.string()).optional().describe(
3636
4119
  "File paths being edited \u2014 used for language/category detection"
3637
4120
  ),
3638
- max_tokens: z6.number().optional().describe("Maximum token budget for the response (default: 4000)")
4121
+ max_tokens: z7.number().optional().describe("Maximum token budget for the response (default: 4000)")
3639
4122
  }
3640
4123
  },
3641
4124
  async ({ task_description, file_paths, max_tokens }) => {
@@ -3657,9 +4140,254 @@ var init_get_context = __esm({
3657
4140
  }
3658
4141
  });
3659
4142
 
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
4245
+ import { z as z8 } from "zod";
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
+
3660
4388
  // src/tools/create-guardrail.ts
3661
- import { z as z7 } from "zod";
3662
- function registerCreateGuardrail(server2, apiClient2) {
4389
+ import { z as z9 } from "zod";
4390
+ function registerCreateGuardrail(server, apiClient2) {
3663
4391
  const handler = async ({
3664
4392
  title,
3665
4393
  description,
@@ -3696,7 +4424,7 @@ function registerCreateGuardrail(server2, apiClient2) {
3696
4424
  ]
3697
4425
  };
3698
4426
  };
3699
- server2.registerTool(
4427
+ server.registerTool(
3700
4428
  "create_check",
3701
4429
  {
3702
4430
  title: "Create Check",
@@ -3708,7 +4436,7 @@ function registerCreateGuardrail(server2, apiClient2) {
3708
4436
  },
3709
4437
  handler
3710
4438
  );
3711
- server2.registerTool(
4439
+ server.registerTool(
3712
4440
  "create_guardrail",
3713
4441
  {
3714
4442
  title: "Create Check (legacy name)",
@@ -3738,32 +4466,32 @@ var init_create_guardrail = __esm({
3738
4466
  "data-access"
3739
4467
  ];
3740
4468
  inputSchema2 = () => ({
3741
- title: z7.string().describe("Short title for the check"),
3742
- description: z7.string().describe("Detailed description of what the check enforces and why"),
3743
- severity: z7.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
3744
- category: z7.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
3745
- pattern: z7.string().optional().describe("Bad code pattern that should be avoided"),
3746
- fix: z7.string().optional().describe("Correct code pattern to use instead"),
3747
- fitness_test: z7.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")
3748
4476
  });
3749
4477
  }
3750
4478
  });
3751
4479
 
3752
4480
  // src/tools/update-spec.ts
3753
- import { z as z8 } from "zod";
3754
- function registerUpdateSpec(server2, apiClient2) {
3755
- server2.registerTool(
4481
+ import { z as z10 } from "zod";
4482
+ function registerUpdateSpec(server, apiClient2) {
4483
+ server.registerTool(
3756
4484
  "update_spec",
3757
4485
  {
3758
4486
  title: "Update Spec",
3759
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.",
3760
4488
  inputSchema: {
3761
- draft_id: z8.string().describe("ID of the spec to update"),
3762
- section: z8.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
3763
- content: z8.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(
3764
4492
  "New markdown content for the section. For requirements, provide the full requirement content."
3765
4493
  ),
3766
- requirement_name: z8.string().optional().describe(
4494
+ requirement_name: z10.string().optional().describe(
3767
4495
  "Name of the requirement (required when section is 'requirements')"
3768
4496
  )
3769
4497
  },
@@ -3818,19 +4546,19 @@ var init_update_spec = __esm({
3818
4546
  });
3819
4547
 
3820
4548
  // src/tools/report-decision.ts
3821
- import { z as z9 } from "zod";
3822
- function registerReportDecision(server2, apiClient2) {
3823
- server2.registerTool(
4549
+ import { z as z11 } from "zod";
4550
+ function registerReportDecision(server, apiClient2) {
4551
+ server.registerTool(
3824
4552
  "report_decision",
3825
4553
  {
3826
4554
  title: "Report Decision",
3827
4555
  description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
3828
4556
  inputSchema: {
3829
- title: z9.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
3830
- context: z9.string().describe("What prompted this decision \u2014 the problem or constraint"),
3831
- decision: z9.string().describe("What was decided and why"),
3832
- consequences: z9.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
3833
- related_files: z9.array(z9.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")
3834
4562
  },
3835
4563
  annotations: {
3836
4564
  readOnlyHint: false
@@ -3886,15 +4614,15 @@ var init_report_decision = __esm({
3886
4614
  });
3887
4615
 
3888
4616
  // src/tools/get-project-rules.ts
3889
- import { z as z10 } from "zod";
3890
- function registerGetProjectRules(server2, apiClient2) {
3891
- server2.registerTool(
4617
+ import { z as z12 } from "zod";
4618
+ function registerGetProjectRules(server, apiClient2) {
4619
+ server.registerTool(
3892
4620
  "get_project_rules",
3893
4621
  {
3894
4622
  title: "Get Project Rules",
3895
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.",
3896
4624
  inputSchema: {
3897
- category: z10.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")
3898
4626
  }
3899
4627
  },
3900
4628
  async ({ category }) => {
@@ -3961,18 +4689,18 @@ var init_get_project_rules = __esm({
3961
4689
  });
3962
4690
 
3963
4691
  // src/tools/create-project-rule.ts
3964
- import { z as z11 } from "zod";
3965
- function registerCreateProjectRule(server2, apiClient2) {
3966
- server2.registerTool(
4692
+ import { z as z13 } from "zod";
4693
+ function registerCreateProjectRule(server, apiClient2) {
4694
+ server.registerTool(
3967
4695
  "create_project_rule",
3968
4696
  {
3969
4697
  title: "Create Project Rule",
3970
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.",
3971
4699
  inputSchema: {
3972
- text: z11.string().describe(
4700
+ text: z13.string().describe(
3973
4701
  "The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
3974
4702
  ),
3975
- category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
4703
+ category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
3976
4704
  },
3977
4705
  annotations: {
3978
4706
  readOnlyHint: false
@@ -4002,8 +4730,8 @@ var init_create_project_rule = __esm({
4002
4730
  });
4003
4731
 
4004
4732
  // src/tools/get-prompt-templates.ts
4005
- function registerGetPromptTemplates(server2, apiClient2) {
4006
- server2.registerTool(
4733
+ function registerGetPromptTemplates(server, apiClient2) {
4734
+ server.registerTool(
4007
4735
  "get_prompt_templates",
4008
4736
  {
4009
4737
  title: "Get Prompt Templates",
@@ -4054,17 +4782,17 @@ var init_get_prompt_templates = __esm({
4054
4782
  });
4055
4783
 
4056
4784
  // src/tools/create-prompt-template.ts
4057
- import { z as z12 } from "zod";
4058
- function registerCreatePromptTemplate(server2, apiClient2) {
4059
- server2.registerTool(
4785
+ import { z as z14 } from "zod";
4786
+ function registerCreatePromptTemplate(server, apiClient2) {
4787
+ server.registerTool(
4060
4788
  "create_prompt_template",
4061
4789
  {
4062
4790
  title: "Create Prompt Template",
4063
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.",
4064
4792
  inputSchema: {
4065
- name: z12.string().describe("Short name for the template"),
4066
- description: z12.string().optional().describe("Brief description of when to use this template"),
4067
- prompt: z12.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")
4068
4796
  },
4069
4797
  annotations: {
4070
4798
  readOnlyHint: false
@@ -4098,7 +4826,7 @@ var init_create_prompt_template = __esm({
4098
4826
  });
4099
4827
 
4100
4828
  // src/tools/skills.ts
4101
- import { z as z13 } from "zod";
4829
+ import { z as z15 } from "zod";
4102
4830
  import { homedir as homedir3 } from "os";
4103
4831
  import { basename as basename2, resolve as resolve6 } from "path";
4104
4832
  function expandHome(path) {
@@ -4107,25 +4835,25 @@ function expandHome(path) {
4107
4835
  function text(t) {
4108
4836
  return { content: [{ type: "text", text: t }] };
4109
4837
  }
4110
- function registerSkillTools(server2, apiClient2, { localFiles }) {
4111
- const fileSchema = z13.object({
4112
- path: z13.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
4113
- content: z13.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")
4114
4842
  });
4115
- server2.registerTool(
4843
+ server.registerTool(
4116
4844
  "publish_skill",
4117
4845
  {
4118
4846
  title: "Publish Skill",
4119
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.",
4120
4848
  inputSchema: {
4121
4849
  ...localFiles ? {
4122
- directory: z13.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")
4123
4851
  } : {},
4124
- files: z13.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
4125
- name: z13.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
4126
- spaceId: z13.string().optional().describe("Space for a NEW skill (default: your default space)"),
4127
- note: z13.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
4128
- baseVersion: z13.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(
4129
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"
4130
4858
  )
4131
4859
  },
@@ -4163,7 +4891,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
4163
4891
  );
4164
4892
  }
4165
4893
  );
4166
- server2.registerTool(
4894
+ server.registerTool(
4167
4895
  "list_skills",
4168
4896
  {
4169
4897
  title: "List Skills",
@@ -4175,7 +4903,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
4175
4903
  const { skills } = await apiClient2.get(SKILLS);
4176
4904
  if (!skills.length) {
4177
4905
  return text(
4178
- `No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/skills", await workspaceBase(apiClient2))}`
4906
+ `No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/team-setup/skills", await workspaceBase(apiClient2))}`
4179
4907
  );
4180
4908
  }
4181
4909
  return text(
@@ -4185,16 +4913,16 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
4185
4913
  );
4186
4914
  }
4187
4915
  );
4188
- server2.registerTool(
4916
+ server.registerTool(
4189
4917
  "install_skill",
4190
4918
  {
4191
4919
  title: "Install Skill",
4192
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.",
4193
4921
  inputSchema: {
4194
- name: z13.string().describe("The skill's name"),
4195
- version: z13.number().int().positive().optional().describe("A version number (default: the latest)"),
4196
- target: z13.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
4197
- ...localFiles ? { overwrite: z13.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") } : {}
4198
4926
  },
4199
4927
  annotations: { readOnlyHint: !localFiles }
4200
4928
  },
@@ -4249,22 +4977,22 @@ var init_skills2 = __esm({
4249
4977
  });
4250
4978
 
4251
4979
  // src/lib/team-setup-types.ts
4252
- var PARTS, PERSON_ID_RE;
4980
+ var PARTS2, PERSON_ID_RE;
4253
4981
  var init_team_setup_types = __esm({
4254
4982
  "src/lib/team-setup-types.ts"() {
4255
4983
  "use strict";
4256
4984
  init_person_values();
4257
- PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
4985
+ PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
4258
4986
  PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
4259
4987
  }
4260
4988
  });
4261
4989
 
4262
4990
  // src/tools/team-setup.ts
4263
- import { z as z14 } from "zod";
4264
- import { existsSync as existsSync5 } from "fs";
4991
+ import { z as z16 } from "zod";
4992
+ import { existsSync as existsSync7 } from "fs";
4265
4993
  import { lstat as lstat2 } from "fs/promises";
4266
4994
  import { homedir as homedir4 } from "os";
4267
- import { join as join6, resolve as resolve7 } from "path";
4995
+ import { join as join8, resolve as resolve7 } from "path";
4268
4996
  function expandHome2(path) {
4269
4997
  return path.replace(/^~(?=$|[\\/])/, homedir4());
4270
4998
  }
@@ -4290,21 +5018,21 @@ async function readPartDirectory(part, directory) {
4290
5018
  let present = true;
4291
5019
  for (let i = 1; i <= segments.length; i++) {
4292
5020
  const rel = segments.slice(0, i).join("/");
4293
- if (await isLink(join6(root, ...segments.slice(0, i)))) {
5021
+ if (await isLink(join8(root, ...segments.slice(0, i)))) {
4294
5022
  links.push(rel);
4295
5023
  present = false;
4296
5024
  break;
4297
5025
  }
4298
5026
  }
4299
- if (present && existsSync5(join6(root, ...segments))) paths.push(path);
5027
+ if (present && existsSync7(join8(root, ...segments))) paths.push(path);
4300
5028
  }
4301
5029
  for (const { dir, md } of layout.folders) {
4302
- const full = join6(root, dir);
5030
+ const full = join8(root, dir);
4303
5031
  if (await isLink(full)) {
4304
5032
  links.push(dir);
4305
5033
  continue;
4306
5034
  }
4307
- if (!existsSync5(full)) continue;
5035
+ if (!existsSync7(full)) continue;
4308
5036
  const walked = await walk(full, { skipHidden: true });
4309
5037
  links.push(...walked.links.map((p) => `${dir}/${p}`));
4310
5038
  leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
@@ -4343,24 +5071,24 @@ function describePerson(id, person) {
4343
5071
  );
4344
5072
  return lines.join("\n");
4345
5073
  }
4346
- function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
4347
- const fileSchema = z14.object({
4348
- path: z14.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
4349
- content: z14.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")
4350
5078
  });
4351
- server2.registerTool(
5079
+ server.registerTool(
4352
5080
  "publish_setup_part",
4353
5081
  {
4354
5082
  title: "Publish Setup Part",
4355
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."),
4356
5084
  inputSchema: {
4357
- part: z14.enum(PARTS).describe("Which part of the setup"),
5085
+ part: z16.enum(PARTS2).describe("Which part of the setup"),
4358
5086
  ...localFiles ? {
4359
- directory: z14.string().optional().describe(
5087
+ directory: z16.string().optional().describe(
4360
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/"
4361
5089
  )
4362
5090
  } : {},
4363
- files: z14.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")
4364
5092
  },
4365
5093
  annotations: { readOnlyHint: false }
4366
5094
  },
@@ -4390,13 +5118,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
4390
5118
  return text2(lines.join("\n"));
4391
5119
  }
4392
5120
  );
4393
- server2.registerTool(
5121
+ server.registerTool(
4394
5122
  "resolve_person",
4395
5123
  {
4396
5124
  title: "Resolve Person",
4397
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.",
4398
5126
  inputSchema: {
4399
- id: z14.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")
4400
5128
  },
4401
5129
  annotations: { readOnlyHint: true }
4402
5130
  },
@@ -4407,14 +5135,14 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
4407
5135
  }
4408
5136
  );
4409
5137
  if (!localFiles) return;
4410
- server2.registerTool(
5138
+ server.registerTool(
4411
5139
  "sync_memory",
4412
5140
  {
4413
5141
  title: "Sync Memory",
4414
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.`,
4415
5143
  inputSchema: {
4416
- project: z14.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"),
4417
- dryRun: z14.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")
4418
5146
  },
4419
5147
  annotations: { readOnlyHint: false }
4420
5148
  },
@@ -4449,28 +5177,34 @@ var init_team_setup = __esm({
4449
5177
 
4450
5178
  // src/server.ts
4451
5179
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4452
- function createServer(apiClient2, { localFiles = false } = {}) {
4453
- const server2 = new McpServer({
4454
- name: "systemdox-mcp-server",
4455
- version: SERVER_VERSION
4456
- });
4457
- registerGetGuardrails(server2, apiClient2);
4458
- registerGetSpecs(server2, apiClient2);
4459
- registerGetDocs(server2, apiClient2);
4460
- registerGetTechStack(server2, apiClient2);
4461
- registerGetPlanningContext(server2, apiClient2);
4462
- registerGetPlanBoard(server2, apiClient2);
4463
- registerGetContext(server2, apiClient2);
4464
- registerGetProjectRules(server2, apiClient2);
4465
- registerGetPromptTemplates(server2, apiClient2);
4466
- registerCreateGuardrail(server2, apiClient2);
4467
- registerUpdateSpec(server2, apiClient2);
4468
- registerReportDecision(server2, apiClient2);
4469
- registerCreateProjectRule(server2, apiClient2);
4470
- registerCreatePromptTemplate(server2, apiClient2);
4471
- registerSkillTools(server2, apiClient2, { localFiles });
4472
- registerTeamSetupTools(server2, apiClient2, { localFiles });
4473
- 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;
4474
5208
  }
4475
5209
  var SERVER_VERSION;
4476
5210
  var init_server = __esm({
@@ -4482,7 +5216,10 @@ var init_server = __esm({
4482
5216
  init_get_tech_stack();
4483
5217
  init_get_planning_context();
4484
5218
  init_get_plan_board();
5219
+ init_get_change();
4485
5220
  init_get_context();
5221
+ init_get_team_setup();
5222
+ init_document_graph();
4486
5223
  init_create_guardrail();
4487
5224
  init_update_spec();
4488
5225
  init_report_decision();
@@ -4492,7 +5229,7 @@ var init_server = __esm({
4492
5229
  init_create_prompt_template();
4493
5230
  init_skills2();
4494
5231
  init_team_setup();
4495
- SERVER_VERSION = true ? "0.1.48" : "0.0.0-dev";
5232
+ SERVER_VERSION = true ? "0.1.50" : "0.0.0-dev";
4496
5233
  }
4497
5234
  });
4498
5235
 
@@ -4505,21 +5242,34 @@ __export(index_exports, {
4505
5242
  });
4506
5243
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4507
5244
  async function main() {
4508
- await apiClient.validate();
5245
+ const instructions = await sessionInstructions(apiClient, { localFiles: true, keyProblem });
5246
+ const server = createServer(apiClient, { localFiles: true, instructions, keyProblem });
4509
5247
  const transport = new StdioServerTransport();
4510
5248
  await server.connect(transport);
4511
5249
  }
4512
- var config, apiClient, server;
5250
+ var config, keyProblem, apiClient;
4513
5251
  var init_index = __esm({
4514
- "src/index.ts"() {
5252
+ async "src/index.ts"() {
4515
5253
  "use strict";
4516
- init_config();
4517
5254
  init_api_client();
5255
+ init_config();
5256
+ init_session_brief();
5257
+ init_team_setup_files();
4518
5258
  init_server();
4519
5259
  init_server();
4520
- config = loadConfig();
5260
+ try {
5261
+ config = await loadLocalConfig();
5262
+ } catch (err) {
5263
+ console.error(`SystemDox MCP server: ${err.message}`);
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
+ }
5271
+ }
4521
5272
  apiClient = new SystemDoxApiClient(config);
4522
- server = createServer(apiClient, { localFiles: true });
4523
5273
  main();
4524
5274
  }
4525
5275
  });
@@ -4527,10 +5277,6 @@ var init_index = __esm({
4527
5277
  // src/cli.ts
4528
5278
  init_config();
4529
5279
  init_team_setup_files();
4530
- import { readFile as readFile5, writeFile as writeFile6, chmod as chmod2 } from "fs/promises";
4531
- import { existsSync as existsSync6 } from "fs";
4532
- import { homedir as homedir5 } from "os";
4533
- import { join as join7 } from "path";
4534
5280
  import { createInterface } from "readline";
4535
5281
  function prompt(question) {
4536
5282
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -4541,158 +5287,10 @@ function prompt(question) {
4541
5287
  });
4542
5288
  });
4543
5289
  }
4544
- async function validateApiKey(apiKey, apiUrl) {
4545
- try {
4546
- const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/v1/health`, {
4547
- headers: {
4548
- Authorization: `Bearer ${apiKey}`,
4549
- "X-API-Key": apiKey,
4550
- Accept: "application/json"
4551
- }
4552
- });
4553
- return res.ok;
4554
- } catch {
4555
- return false;
4556
- }
4557
- }
4558
- function findConfigTargets() {
4559
- const targets = [];
4560
- const projectMcp = join7(process.cwd(), ".mcp.json");
4561
- targets.push({
4562
- name: "Claude Code (project)",
4563
- path: projectMcp,
4564
- detected: true
4565
- // Always available
4566
- });
4567
- const claudeUser = join7(homedir5(), ".claude.json");
4568
- targets.push({
4569
- name: "Claude Code (user \u2014 all projects)",
4570
- path: claudeUser,
4571
- detected: true
4572
- // Always available
4573
- });
4574
- const cursorProject = join7(process.cwd(), ".cursor", "mcp.json");
4575
- const cursorDir = join7(process.cwd(), ".cursor");
4576
- targets.push({
4577
- name: "Cursor (project)",
4578
- path: cursorProject,
4579
- detected: existsSync6(cursorDir)
4580
- });
4581
- const vscodeProject = join7(process.cwd(), ".vscode", "mcp.json");
4582
- const vscodeDir = join7(process.cwd(), ".vscode");
4583
- targets.push({
4584
- name: "VS Code (project)",
4585
- path: vscodeProject,
4586
- detected: existsSync6(vscodeDir)
4587
- });
4588
- const cursorGlobal = join7(homedir5(), ".cursor", "mcp.json");
4589
- targets.push({
4590
- name: "Cursor (global)",
4591
- path: cursorGlobal,
4592
- detected: existsSync6(join7(homedir5(), ".cursor"))
4593
- });
4594
- return targets;
4595
- }
4596
- async function readOrCreateConfig(path) {
4597
- try {
4598
- const content = await readFile5(path, "utf-8");
4599
- return JSON.parse(content);
4600
- } catch {
4601
- return { mcpServers: {} };
4602
- }
4603
- }
4604
- async function writeConfig(path, config2) {
4605
- const dir = path.substring(0, path.lastIndexOf("/"));
4606
- if (!existsSync6(dir)) {
4607
- const { mkdir: mkdir6 } = await import("fs/promises");
4608
- await mkdir6(dir, { recursive: true });
4609
- }
4610
- await writeFile6(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
4611
- try {
4612
- await chmod2(path, 384);
4613
- } catch {
4614
- }
4615
- }
4616
- function serverEntry(apiKey, apiUrl) {
4617
- const entry = {
4618
- command: "npx",
4619
- args: ["@systemdox/mcp-server"],
4620
- env: {
4621
- SYSTEMDOX_API_KEY: apiKey
4622
- }
4623
- };
4624
- if (apiUrl !== API_URL_DEFAULT) {
4625
- entry.env.SYSTEMDOX_API_URL = apiUrl;
4626
- }
4627
- return entry;
4628
- }
4629
- async function setup() {
4630
- console.log("\n SystemDox MCP Server Setup\n");
4631
- const apiKey = await prompt(" Enter your SystemDox API key: ");
4632
- if (!apiKey) {
4633
- console.error(
4634
- "\n Error: API key is required.\n Generate one at your SystemDox settings page.\n"
4635
- );
4636
- process.exit(1);
4637
- }
4638
- const apiUrlInput = await prompt(
4639
- ` API URL [${API_URL_DEFAULT}]: `
4640
- );
4641
- const apiUrl = apiUrlInput || API_URL_DEFAULT;
4642
- try {
4643
- requireHttpsApiUrl(apiUrl);
4644
- } catch (err) {
4645
- console.error(`
4646
- Error: ${err.message}
4647
- `);
4648
- process.exit(1);
4649
- }
4650
- console.log("\n Validating API key...");
4651
- const valid = await validateApiKey(apiKey, apiUrl);
4652
- if (!valid) {
4653
- console.error(
4654
- "\n Error: Invalid API key or cannot reach the SystemDox API.\n Check your key and try again.\n"
4655
- );
4656
- process.exit(1);
4657
- }
4658
- console.log(" API key validated successfully.\n");
4659
- const targets = findConfigTargets();
4660
- const detected = targets.filter((t) => t.detected);
4661
- console.log(" Available targets:");
4662
- detected.forEach((t, i) => {
4663
- console.log(` ${i + 1}. ${t.name} (${t.path})`);
4664
- });
4665
- const choiceInput = await prompt(
4666
- `
4667
- Which target? [1-${detected.length}, or 'all']: `
4668
- );
4669
- let selected;
4670
- if (choiceInput.toLowerCase() === "all") {
4671
- selected = detected;
4672
- } else {
4673
- const idx = parseInt(choiceInput, 10) - 1;
4674
- if (isNaN(idx) || idx < 0 || idx >= detected.length) {
4675
- console.error("\n Invalid selection.\n");
4676
- process.exit(1);
4677
- }
4678
- selected = [detected[idx]];
4679
- }
4680
- const entry = serverEntry(apiKey, apiUrl);
4681
- for (const target of selected) {
4682
- const config2 = await readOrCreateConfig(target.path);
4683
- config2.mcpServers["systemdox"] = entry;
4684
- await writeConfig(target.path, config2);
4685
- console.log(` Configured: ${target.name}`);
4686
- }
4687
- console.log(
4688
- "\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides 20 tools: get_context, get_checks,\n get_project_rules, get_prompt_templates, get_specs, get_docs,\n get_tech_stack, get_planning_context, get_plan_board, create_check,\n create_project_rule, create_prompt_template, update_spec,\n report_decision, publish_skill, list_skills, install_skill,\n publish_setup_part, resolve_person, sync_memory.\n\n get_guardrails and create_guardrail remain registered as deprecated\n aliases of get_checks and create_check \u2014 existing configs keep working.\n"
4689
- );
4690
- return { apiKey, apiUrl };
4691
- }
4692
5290
  var args = process.argv.slice(2);
4693
5291
  var command = args[0];
4694
5292
  if (command === "setup") {
4695
- Promise.resolve().then(() => (init_setup_team(), setup_team_exports)).then(async ({ parseSetupArgs: parseSetupArgs2, runTeamSetupCommand: runTeamSetupCommand2 }) => {
5293
+ Promise.all([Promise.resolve().then(() => (init_setup_team(), setup_team_exports)), Promise.resolve().then(() => (init_setup_mcp(), setup_mcp_exports))]).then(async ([{ parseSetupArgs: parseSetupArgs2, runTeamSetupCommand: runTeamSetupCommand2 }, { setupMcpServer: setupMcpServer2 }]) => {
4696
5294
  let teamArgs;
4697
5295
  try {
4698
5296
  teamArgs = parseSetupArgs2(args.slice(1));
@@ -4702,8 +5300,13 @@ if (command === "setup") {
4702
5300
  `);
4703
5301
  process.exit(1);
4704
5302
  }
4705
- const key = await setup();
4706
- return runTeamSetupCommand2(teamArgs, key, {
5303
+ const step = await setupMcpServer2({
5304
+ out: console.log,
5305
+ err: console.error,
5306
+ ask: (question) => prompt(question)
5307
+ });
5308
+ if (step.code !== 0) return step.code;
5309
+ return runTeamSetupCommand2(teamArgs, step, {
4707
5310
  out: console.log,
4708
5311
  ask: (question) => prompt(question),
4709
5312
  terminal: !!process.stdin.isTTY
@@ -4763,15 +5366,18 @@ if (command === "setup") {
4763
5366
  systemdox-mcp-server --help Show this help message
4764
5367
 
4765
5368
  Environment variables:
4766
- SYSTEMDOX_API_KEY Your SystemDox API key (required; plugin-path and
4767
- marketplace-auth also read
4768
- ~/.claude/systemdox/credentials.json)
5369
+ SYSTEMDOX_API_KEY Your SystemDox API key. When it is not set, every
5370
+ command reads ~/.claude/systemdox/credentials.json,
5371
+ where setup stores the key (and nowhere else: not
5372
+ in your AI tool's MCP config)
5373
+ SYSTEMDOX_WORKSPACE The workspace whose key to use when
5374
+ credentials.json holds several
4769
5375
  SYSTEMDOX_API_URL API URL (default: ${API_URL_DEFAULT})
4770
5376
  SYSTEMDOX_CACHE_TTL Cache TTL in seconds (default: 300)
4771
5377
  SYSTEMDOX_DEFAULT_ORG Default GitHub org
4772
5378
  `);
4773
5379
  } else if (!command) {
4774
- Promise.resolve().then(() => init_index());
5380
+ init_index();
4775
5381
  } else {
4776
5382
  console.error(` Unknown command: ${command}
4777
5383
  Run 'systemdox-mcp-server --help' for usage.`);