@systemdox/mcp-server 0.1.49 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.js CHANGED
@@ -63,14 +63,6 @@ var SystemDoxApiClient = class {
63
63
  this.config = config;
64
64
  this.cache = new ResponseCache(config.cacheTtlMs);
65
65
  }
66
- async validate() {
67
- const res = await this.fetchRaw("/v1/health");
68
- if (!res.ok) {
69
- throw new Error(
70
- `SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
71
- );
72
- }
73
- }
74
66
  async get(path, params) {
75
67
  const url = this.buildUrl(path, params);
76
68
  const cacheKey = url.toString();
@@ -84,8 +76,8 @@ var SystemDoxApiClient = class {
84
76
  }
85
77
  /** A read that skips the cache: an install must take the version that is
86
78
  * latest now, not the one cached before someone published another. */
87
- async getFresh(path) {
88
- const res = await this.fetchRaw(path);
79
+ async getFresh(path, signal) {
80
+ const res = await this.fetchRaw(path, void 0, signal);
89
81
  if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
90
82
  return await res.json();
91
83
  }
@@ -115,8 +107,8 @@ var SystemDoxApiClient = class {
115
107
  // One method per operationId. Every read skips the cache: setup, sync and
116
108
  // plugin-path act on what the workspace holds now.
117
109
  /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
118
- getOwnTeamSetup() {
119
- return this.getFresh(TEAM_SETUP);
110
+ getOwnTeamSetup(signal) {
111
+ return this.getFresh(TEAM_SETUP, signal);
120
112
  }
121
113
  getTeamSetup(workspace) {
122
114
  return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
@@ -220,14 +212,15 @@ var SystemDoxApiClient = class {
220
212
  }
221
213
  return url;
222
214
  }
223
- async fetchRaw(path, params) {
215
+ async fetchRaw(path, params, signal) {
224
216
  const url = this.buildUrl(path, params);
225
217
  return fetch(url, {
226
218
  headers: {
227
219
  Authorization: `Bearer ${this.config.apiKey}`,
228
220
  "X-API-Key": this.config.apiKey,
229
221
  Accept: "application/json"
230
- }
222
+ },
223
+ signal
231
224
  });
232
225
  }
233
226
  };
@@ -286,149 +279,530 @@ function emitMcpToolCalls(tenant, rawBody, log = console.log) {
286
279
  }
287
280
  }
288
281
 
289
- // src/server.ts
290
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
291
-
292
- // src/tools/get-guardrails.ts
293
- import { z } from "zod";
282
+ // src/lib/session-brief.ts
283
+ import { existsSync as existsSync2 } from "fs";
284
+ import { readFile as readFile3 } from "fs/promises";
285
+ import { join as join3 } from "path";
294
286
 
295
- // src/lib/language.ts
296
- var EXTENSION_MAP = {
297
- ".py": { language: "python", categories: ["api", "backend", "data-access"] },
298
- ".ts": { language: "typescript", categories: ["frontend", "api"] },
299
- ".tsx": { language: "typescript", categories: ["frontend"] },
300
- ".js": { language: "javascript", categories: ["frontend"] },
301
- ".jsx": { language: "javascript", categories: ["frontend"] },
302
- ".yaml": {
303
- language: "cloudformation",
304
- categories: ["infrastructure", "architecture"]
305
- },
306
- ".yml": {
307
- language: "cloudformation",
308
- categories: ["infrastructure", "architecture"]
309
- },
310
- ".json": { language: "json", categories: ["configuration"] },
311
- ".css": { language: "css", categories: ["frontend"] },
312
- ".html": { language: "html", categories: ["frontend"] },
313
- ".sql": { language: "sql", categories: ["data-access"] },
314
- ".sh": { language: "shell", categories: ["infrastructure"] },
315
- ".bash": { language: "shell", categories: ["infrastructure"] },
316
- ".tf": { language: "terraform", categories: ["infrastructure"] },
317
- ".md": { language: "markdown", categories: ["documentation"] }
287
+ // src/lib/skills.ts
288
+ import { createHash } from "crypto";
289
+ import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
290
+ import { existsSync } from "fs";
291
+ import { homedir } from "os";
292
+ import { dirname, join, relative, resolve, sep } from "path";
293
+ var MANIFEST_FILE = ".systemdox-skill.json";
294
+ var ENTRY_FILE = "SKILL.md";
295
+ var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
296
+ var MAX_FILES = 100;
297
+ var MAX_FILE_BYTES = 256 * 1024;
298
+ var MAX_TOTAL_BYTES = 1024 * 1024;
299
+ var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
300
+ var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
301
+ var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
302
+ var SkillInstallRefused = class extends Error {
303
+ constructor(dest, edited) {
304
+ super(
305
+ `${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
306
+ );
307
+ this.dest = dest;
308
+ this.edited = edited;
309
+ }
310
+ dest;
311
+ edited;
318
312
  };
319
- var PATH_CATEGORY_RULES = [
320
- { pattern: /backend\/functions\//, categories: ["api", "backend"] },
321
- { pattern: /backend\/layers\//, categories: ["backend"] },
322
- { pattern: /backend\//, categories: ["backend"] },
323
- { pattern: /apps\/app\//, categories: ["frontend"] },
324
- { pattern: /apps\/website\//, categories: ["frontend"] },
325
- { pattern: /apps\/portal\//, categories: ["frontend"] },
326
- { pattern: /packages\/ui\//, categories: ["frontend"] },
327
- { pattern: /infra\//, categories: ["infrastructure"] },
328
- { pattern: /\.github\//, categories: ["infrastructure"] },
329
- { pattern: /tests?\//, categories: ["testing"] },
330
- { pattern: /spec/, categories: ["testing"] },
331
- { pattern: /docs\/adr/, categories: ["architecture"] },
332
- { pattern: /template\.yaml$/, categories: ["infrastructure", "architecture"] }
333
- ];
334
- function detectLanguage(filePath) {
335
- const ext = filePath.slice(filePath.lastIndexOf("."));
336
- return EXTENSION_MAP[ext];
313
+ function sha256(content) {
314
+ return createHash("sha256").update(content, "utf8").digest("hex");
337
315
  }
338
- function detectCategories(filePaths) {
339
- const categories = /* @__PURE__ */ new Set();
340
- for (const filePath of filePaths) {
341
- const langInfo = detectLanguage(filePath);
342
- if (langInfo) {
343
- for (const cat of langInfo.categories) {
344
- categories.add(cat);
345
- }
316
+ function isValidName(name) {
317
+ return name.length <= 64 && NAME_RE.test(name);
318
+ }
319
+ function isValidPath(path) {
320
+ return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
321
+ }
322
+ function frontmatterName(skillMd) {
323
+ const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
324
+ if (!block) return void 0;
325
+ const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
326
+ return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
327
+ }
328
+ function defaultSkillDir(name) {
329
+ return join(homedir(), ".claude", "skills", name);
330
+ }
331
+ async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
332
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
333
+ if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
334
+ const full = join(dir, entry.name);
335
+ const rel = relative(root, full).split(sep).join("/");
336
+ if (entry.isSymbolicLink()) out.links.push(rel);
337
+ else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
338
+ else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
339
+ else if (entry.isFile()) out.files.push(rel);
340
+ }
341
+ out.files.sort();
342
+ return out;
343
+ }
344
+ async function readTextFiles(root, paths, what) {
345
+ if (paths.length > MAX_FILES) {
346
+ throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
347
+ }
348
+ const decoder = new TextDecoder("utf-8", { fatal: true });
349
+ const files = [];
350
+ let total = 0;
351
+ for (const path of paths) {
352
+ if (!isValidPath(path)) {
353
+ throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
346
354
  }
347
- for (const rule of PATH_CATEGORY_RULES) {
348
- if (rule.pattern.test(filePath)) {
349
- for (const cat of rule.categories) {
350
- categories.add(cat);
351
- }
352
- }
355
+ const raw = await readFile(join(root, ...path.split("/")));
356
+ if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
357
+ if (raw.length > MAX_FILE_BYTES) {
358
+ throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
359
+ }
360
+ total += raw.length;
361
+ if (total > MAX_TOTAL_BYTES) {
362
+ throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
363
+ }
364
+ let content;
365
+ try {
366
+ content = decoder.decode(raw);
367
+ } catch {
368
+ throw new Error(`${path} is not UTF-8 text`);
353
369
  }
370
+ files.push({ path, content });
354
371
  }
355
- return Array.from(categories);
372
+ return files;
356
373
  }
357
- function detectLanguages(filePaths) {
358
- const languages = /* @__PURE__ */ new Set();
359
- for (const filePath of filePaths) {
360
- const info = detectLanguage(filePath);
361
- if (info) languages.add(info.language);
374
+ async function readSkillDirectory(directory) {
375
+ const root = resolve(directory);
376
+ if (!existsSync(join(root, ENTRY_FILE))) {
377
+ throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
362
378
  }
363
- return Array.from(languages);
379
+ const walked = await walk(root, { skipHidden: true });
380
+ if (walked.links.length) {
381
+ throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
382
+ }
383
+ const files = await readTextFiles(root, walked.files, "a skill");
384
+ const skillMd = files.find((f) => f.path === ENTRY_FILE);
385
+ const name = frontmatterName(skillMd.content);
386
+ if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
387
+ return { name, files, skipped: walked.hidden };
364
388
  }
365
-
366
- // src/lib/deprecation.ts
367
- var LEGACY_TOOL_REMOVAL_EARLIEST = "2027-01-31";
368
- function legacyToolDescription(canonical, description) {
369
- return `DEPRECATED \u2014 use \`${canonical}\` instead. This name still works and will not be removed before ${LEGACY_TOOL_REMOVAL_EARLIEST}. ${description}`;
389
+ function within(dest, path) {
390
+ if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
391
+ const full = resolve(dest, ...path.split("/"));
392
+ if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
393
+ return full;
370
394
  }
371
-
372
- // src/lib/config-links.ts
373
- async function workspaceBase(apiClient) {
395
+ async function readManifest(dest) {
374
396
  try {
375
- const access = await apiClient.get(
376
- "/v1/docs/portal-access"
377
- );
378
- const slug = (access.host ?? "").split(".")[0];
379
- return slug ? `https://${slug}.systemdox.com` : null;
397
+ return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
380
398
  } catch {
381
- return null;
399
+ return void 0;
382
400
  }
383
401
  }
384
- function configLink(path, base) {
385
- return base ? `${base}${path}` : `${path} in your SystemDox workspace`;
386
- }
387
-
388
- // src/tools/get-guardrails.ts
389
- var DESCRIPTION = "Retrieve SystemDox checks (banned patterns, required patterns, coding rules). Filter by language, category, or file path to get only relevant checks.";
390
- var inputSchema = () => ({
391
- language: z.string().optional().describe(
392
- "Filter by programming language (python, typescript, javascript, cloudformation)"
393
- ),
394
- category: z.string().optional().describe(
395
- "Filter by category (imports, auth, error-handling, architecture, testing, security, conventions, data-access)"
396
- ),
397
- file_path: z.string().optional().describe(
398
- "File path being edited \u2014 language is auto-detected from the extension"
399
- )
400
- });
401
- function registerGetGuardrails(server, apiClient) {
402
- const handler2 = async ({
403
- language,
404
- category,
405
- file_path
406
- }) => {
407
- const guardrails = await apiClient.get("/v1/guardrails");
408
- let filtered = guardrails;
409
- const lang = language ?? (file_path ? detectLanguage(file_path)?.language : void 0);
410
- if (lang) {
411
- filtered = filtered.filter(
412
- (g) => g.description.toLowerCase().includes(lang.toLowerCase()) || g.category.toLowerCase().includes(lang.toLowerCase()) || g.title.toLowerCase().includes(lang.toLowerCase())
413
- );
414
- }
415
- if (category) {
416
- filtered = filtered.filter(
417
- (g) => g.category.toLowerCase() === category.toLowerCase()
418
- );
419
- }
420
- if (filtered.length === 0) {
421
- const text4 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
422
- return {
423
- content: [{ type: "text", text: text4 }]
424
- };
402
+ async function installSkill(version, dest, { overwrite = false } = {}) {
403
+ if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
404
+ const incoming = new Map(version.files.map((f) => [f.path, f]));
405
+ for (const f of version.files) within(dest, f.path);
406
+ if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
407
+ throw new Error(`${dest} is a link: install into a real folder`);
408
+ }
409
+ const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
410
+ if (walked.links.length) {
411
+ throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
412
+ }
413
+ const present = walked.files;
414
+ const manifest = await readManifest(dest);
415
+ const known2 = Object.fromEntries(
416
+ Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
417
+ );
418
+ const edited = [];
419
+ const untracked = [];
420
+ for (const path of present) {
421
+ const content = await readFile(join(dest, ...path.split("/")), "utf8");
422
+ if (Object.hasOwn(known2, path)) {
423
+ if (sha256(content) !== known2[path]) edited.push(path);
424
+ } else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
425
+ if (incoming.has(path) || !manifest) edited.push(path);
426
+ else untracked.push(path);
425
427
  }
426
- const text3 = filtered.map(
427
- (g) => `## ${g.title} [${g.severity}]
428
-
429
- ${g.description}
430
-
431
- ` + (g.pattern ? `**Bad pattern:**
428
+ }
429
+ const blocking = edited.filter((p) => incoming.has(p) || !manifest);
430
+ if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
431
+ const written = [];
432
+ for (const f of version.files) {
433
+ const full = within(dest, f.path);
434
+ await mkdir(dirname(full), { recursive: true });
435
+ await writeFile(full, f.content, "utf8");
436
+ written.push(f.path);
437
+ }
438
+ const removed = [];
439
+ const keptEdited = [];
440
+ for (const path of Object.keys(known2)) {
441
+ if (incoming.has(path) || !present.includes(path)) continue;
442
+ if (edited.includes(path)) {
443
+ keptEdited.push(path);
444
+ continue;
445
+ }
446
+ await rm(within(dest, path));
447
+ removed.push(path);
448
+ }
449
+ const next = {
450
+ name: version.name,
451
+ version: version.version,
452
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
453
+ files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
454
+ };
455
+ await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
456
+ return {
457
+ dest,
458
+ version: version.version,
459
+ written,
460
+ removed,
461
+ keptUntracked: untracked.filter((p) => !incoming.has(p)),
462
+ keptEdited
463
+ };
464
+ }
465
+ function fenceFor(text3) {
466
+ const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
467
+ return "`".repeat(Math.max(3, longest + 1));
468
+ }
469
+ function describeInstall(name, r) {
470
+ const lines = [
471
+ `Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
472
+ ];
473
+ if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
474
+ if (r.keptEdited.length) {
475
+ lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
476
+ }
477
+ if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
478
+ lines.push("Start a new Claude Code session to load it.");
479
+ return lines.join("\n");
480
+ }
481
+
482
+ // src/lib/team-setup-files.ts
483
+ import { execFile } from "child_process";
484
+ import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
485
+ import { homedir as homedir2, userInfo } from "os";
486
+ import { dirname as dirname2, join as join2 } from "path";
487
+ import { promisify } from "util";
488
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
489
+ function isWorkspace(value) {
490
+ return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
491
+ }
492
+ function homeDir() {
493
+ return homedir2();
494
+ }
495
+ function systemdoxDir() {
496
+ return join2(homeDir(), ".claude", "systemdox");
497
+ }
498
+ function workspaceDir(workspace) {
499
+ if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
500
+ return join2(systemdoxDir(), workspace);
501
+ }
502
+ function statePath(workspace) {
503
+ return join2(workspaceDir(workspace), "state.json");
504
+ }
505
+ async function readJson(path, empty) {
506
+ let text3;
507
+ try {
508
+ text3 = await readFile2(path, "utf8");
509
+ } catch (e) {
510
+ if (e.code === "ENOENT") return empty;
511
+ throw e;
512
+ }
513
+ try {
514
+ return JSON.parse(text3);
515
+ } catch {
516
+ throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
517
+ }
518
+ }
519
+ async function writeJsonAtomically(path, value, mode) {
520
+ await mkdir2(dirname2(path), { recursive: true });
521
+ const tmp = `${path}.${process.pid}.tmp`;
522
+ await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
523
+ await rename(tmp, path);
524
+ }
525
+ var run = promisify(execFile);
526
+ async function readState(workspace) {
527
+ return readJson(statePath(workspace), {});
528
+ }
529
+ async function writeState(workspace, state) {
530
+ await writeJsonAtomically(statePath(workspace), state);
531
+ }
532
+ async function updateState(workspace, change) {
533
+ const state = await readState(workspace);
534
+ change(state);
535
+ await writeState(workspace, state);
536
+ return state;
537
+ }
538
+
539
+ // src/lib/session-brief.ts
540
+ var INSTRUCTIONS_MAX = 2e3;
541
+ var BRIEF_TIMEOUT_MS = 8e3;
542
+ var SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
543
+ var REFUSED = `SystemDox refused this computer's key, so the team's rules and skills cannot be read and every SystemDox tool fails. Tell the person once: a new key comes from the SystemDox console, Settings \u203A API keys, and \`${SETUP_COMMAND}\` in their own terminal stores it.`;
544
+ var NOT_CHECKED = "SystemDox could not be read when this session started, so the team's skills were not checked. Call get_team_setup to check again before relying on them.";
545
+ var CANNOT_SEE = "This server cannot see this computer's disk. For each skill listed under Team skills: if ~/.claude/skills/<name>/ is absent, or its .systemdox-skill.json names another version than get_team_setup lists, call install_skill with that version and write what it returns, unless the latest team plugin carries it. Leave a folder with no such file alone. If a file there differs from the hash that file holds for it, it was edited: ask the person before replacing it.";
546
+ function noKey(reason) {
547
+ 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.`;
548
+ }
549
+ function olderApi(setup) {
550
+ 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.`;
551
+ }
552
+ async function readJsonQuietly(path) {
553
+ try {
554
+ return JSON.parse(await readFile3(path, "utf8"));
555
+ } catch {
556
+ return void 0;
557
+ }
558
+ }
559
+ async function computerState(setup) {
560
+ const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
561
+ const installed = await readJsonQuietly(join3(homeDir(), ".claude", "plugins", "installed_plugins.json"));
562
+ const entries2 = installed?.plugins?.[id];
563
+ const entry = Array.isArray(entries2) ? entries2.find((e) => (e?.scope ?? "user") === "user") : void 0;
564
+ const state = {
565
+ missing: [],
566
+ behind: [],
567
+ notFromSystemDox: [],
568
+ pluginInstalled: !!entry,
569
+ pluginVersion: entry?.version
570
+ };
571
+ const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
572
+ for (const skill of setup.sessionBrief?.skills ?? []) {
573
+ const dir = defaultSkillDir(skill.name);
574
+ if (!existsSync2(dir)) {
575
+ if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
576
+ continue;
577
+ }
578
+ const manifest = await readJsonQuietly(join3(dir, MANIFEST_FILE));
579
+ if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
580
+ state.notFromSystemDox.push(skill.name);
581
+ } else if (manifest.version !== skill.latestVersion) {
582
+ state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
583
+ }
584
+ }
585
+ return state;
586
+ }
587
+ function hasPersonParts(setup) {
588
+ return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
589
+ }
590
+ var MORE = (n) => `${n} more (get_team_setup lists them)`;
591
+ var LIST_MIN = `all ${MORE(100)}`.length;
592
+ function listWithin(items, room) {
593
+ const whole2 = items.join(", ");
594
+ if (whole2.length <= room) return whole2;
595
+ const kept = [];
596
+ let used = 0;
597
+ for (let i = 0; i < items.length; i++) {
598
+ const rest = items.length - i - 1;
599
+ const cost = items[i].length + (kept.length ? 2 : 0);
600
+ if (used + cost + ` and ${MORE(rest)}`.length > room) break;
601
+ kept.push(items[i]);
602
+ used += cost;
603
+ }
604
+ return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
605
+ }
606
+ function localSection(setup, state, room) {
607
+ const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
608
+ const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
609
+ const lines = [];
610
+ if (state.missing.length) {
611
+ lines.push({
612
+ head: "On this computer, missing: ",
613
+ items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
614
+ 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.`
615
+ });
616
+ }
617
+ if (state.behind.length) {
618
+ lines.push({
619
+ head: "On this computer, behind: ",
620
+ items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
621
+ tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
622
+ });
623
+ }
624
+ if (state.notFromSystemDox.length) {
625
+ lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
626
+ }
627
+ const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
628
+ while (lines.length && fixed() > room) lines.pop();
629
+ if (!lines.length) return "";
630
+ const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
631
+ return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
632
+ }
633
+ function fit(text3, section) {
634
+ const whole2 = section ? `${text3}
635
+
636
+ ${section}` : text3;
637
+ if (whole2.length <= INSTRUCTIONS_MAX) return whole2;
638
+ return text3.length <= INSTRUCTIONS_MAX ? text3 : `${text3.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
639
+ }
640
+ async function readTeamSetup(apiClient, { keyProblem, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
641
+ if (keyProblem) return { problem: noKey(keyProblem) };
642
+ try {
643
+ const setup = await apiClient.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
644
+ return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
645
+ } catch (err) {
646
+ const status = err?.status;
647
+ return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
648
+ }
649
+ }
650
+ async function sessionInstructions(apiClient, options = {}) {
651
+ const read = await readTeamSetup(apiClient, options);
652
+ if ("problem" in read) return read.problem;
653
+ const text3 = read.setup.sessionBrief?.text ?? olderApi(read.setup);
654
+ try {
655
+ if (!options.localFiles) return fit(text3, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
656
+ const room = INSTRUCTIONS_MAX - text3.length - 2;
657
+ return fit(text3, localSection(read.setup, await computerState(read.setup), room));
658
+ } catch {
659
+ return fit(text3, "");
660
+ }
661
+ }
662
+
663
+ // src/server.ts
664
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
665
+
666
+ // src/tools/get-guardrails.ts
667
+ import { z } from "zod";
668
+
669
+ // src/lib/language.ts
670
+ var EXTENSION_MAP = {
671
+ ".py": { language: "python", categories: ["api", "backend", "data-access"] },
672
+ ".ts": { language: "typescript", categories: ["frontend", "api"] },
673
+ ".tsx": { language: "typescript", categories: ["frontend"] },
674
+ ".js": { language: "javascript", categories: ["frontend"] },
675
+ ".jsx": { language: "javascript", categories: ["frontend"] },
676
+ ".yaml": {
677
+ language: "cloudformation",
678
+ categories: ["infrastructure", "architecture"]
679
+ },
680
+ ".yml": {
681
+ language: "cloudformation",
682
+ categories: ["infrastructure", "architecture"]
683
+ },
684
+ ".json": { language: "json", categories: ["configuration"] },
685
+ ".css": { language: "css", categories: ["frontend"] },
686
+ ".html": { language: "html", categories: ["frontend"] },
687
+ ".sql": { language: "sql", categories: ["data-access"] },
688
+ ".sh": { language: "shell", categories: ["infrastructure"] },
689
+ ".bash": { language: "shell", categories: ["infrastructure"] },
690
+ ".tf": { language: "terraform", categories: ["infrastructure"] },
691
+ ".md": { language: "markdown", categories: ["documentation"] }
692
+ };
693
+ var PATH_CATEGORY_RULES = [
694
+ { pattern: /backend\/functions\//, categories: ["api", "backend"] },
695
+ { pattern: /backend\/layers\//, categories: ["backend"] },
696
+ { pattern: /backend\//, categories: ["backend"] },
697
+ { pattern: /apps\/app\//, categories: ["frontend"] },
698
+ { pattern: /apps\/website\//, categories: ["frontend"] },
699
+ { pattern: /apps\/portal\//, categories: ["frontend"] },
700
+ { pattern: /packages\/ui\//, categories: ["frontend"] },
701
+ { pattern: /infra\//, categories: ["infrastructure"] },
702
+ { pattern: /\.github\//, categories: ["infrastructure"] },
703
+ { pattern: /tests?\//, categories: ["testing"] },
704
+ { pattern: /spec/, categories: ["testing"] },
705
+ { pattern: /docs\/adr/, categories: ["architecture"] },
706
+ { pattern: /template\.yaml$/, categories: ["infrastructure", "architecture"] }
707
+ ];
708
+ function detectLanguage(filePath) {
709
+ const ext = filePath.slice(filePath.lastIndexOf("."));
710
+ return EXTENSION_MAP[ext];
711
+ }
712
+ function detectCategories(filePaths) {
713
+ const categories = /* @__PURE__ */ new Set();
714
+ for (const filePath of filePaths) {
715
+ const langInfo = detectLanguage(filePath);
716
+ if (langInfo) {
717
+ for (const cat of langInfo.categories) {
718
+ categories.add(cat);
719
+ }
720
+ }
721
+ for (const rule of PATH_CATEGORY_RULES) {
722
+ if (rule.pattern.test(filePath)) {
723
+ for (const cat of rule.categories) {
724
+ categories.add(cat);
725
+ }
726
+ }
727
+ }
728
+ }
729
+ return Array.from(categories);
730
+ }
731
+ function detectLanguages(filePaths) {
732
+ const languages = /* @__PURE__ */ new Set();
733
+ for (const filePath of filePaths) {
734
+ const info = detectLanguage(filePath);
735
+ if (info) languages.add(info.language);
736
+ }
737
+ return Array.from(languages);
738
+ }
739
+
740
+ // src/lib/deprecation.ts
741
+ var LEGACY_TOOL_REMOVAL_EARLIEST = "2027-01-31";
742
+ function legacyToolDescription(canonical, description) {
743
+ return `DEPRECATED \u2014 use \`${canonical}\` instead. This name still works and will not be removed before ${LEGACY_TOOL_REMOVAL_EARLIEST}. ${description}`;
744
+ }
745
+
746
+ // src/lib/config-links.ts
747
+ async function workspaceBase(apiClient) {
748
+ try {
749
+ const access = await apiClient.get(
750
+ "/v1/docs/portal-access"
751
+ );
752
+ const slug = (access.host ?? "").split(".")[0];
753
+ return slug ? `https://${slug}.systemdox.com` : null;
754
+ } catch {
755
+ return null;
756
+ }
757
+ }
758
+ function configLink(path, base) {
759
+ return base ? `${base}${path}` : `${path} in your SystemDox workspace`;
760
+ }
761
+
762
+ // src/tools/get-guardrails.ts
763
+ var DESCRIPTION = "Retrieve SystemDox checks (banned patterns, required patterns, coding rules). Filter by language, category, or file path to get only relevant checks.";
764
+ var inputSchema = () => ({
765
+ language: z.string().optional().describe(
766
+ "Filter by programming language (python, typescript, javascript, cloudformation)"
767
+ ),
768
+ category: z.string().optional().describe(
769
+ "Filter by category (imports, auth, error-handling, architecture, testing, security, conventions, data-access)"
770
+ ),
771
+ file_path: z.string().optional().describe(
772
+ "File path being edited \u2014 language is auto-detected from the extension"
773
+ )
774
+ });
775
+ function registerGetGuardrails(server, apiClient) {
776
+ const handler2 = async ({
777
+ language,
778
+ category,
779
+ file_path
780
+ }) => {
781
+ const guardrails = await apiClient.get("/v1/guardrails");
782
+ let filtered = guardrails;
783
+ const lang = language ?? (file_path ? detectLanguage(file_path)?.language : void 0);
784
+ if (lang) {
785
+ filtered = filtered.filter(
786
+ (g) => g.description.toLowerCase().includes(lang.toLowerCase()) || g.category.toLowerCase().includes(lang.toLowerCase()) || g.title.toLowerCase().includes(lang.toLowerCase())
787
+ );
788
+ }
789
+ if (category) {
790
+ filtered = filtered.filter(
791
+ (g) => g.category.toLowerCase() === category.toLowerCase()
792
+ );
793
+ }
794
+ if (filtered.length === 0) {
795
+ const text4 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
796
+ return {
797
+ content: [{ type: "text", text: text4 }]
798
+ };
799
+ }
800
+ const text3 = filtered.map(
801
+ (g) => `## ${g.title} [${g.severity}]
802
+
803
+ ${g.description}
804
+
805
+ ` + (g.pattern ? `**Bad pattern:**
432
806
  \`\`\`
433
807
  ${g.pattern}
434
808
  \`\`\`
@@ -744,7 +1118,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
744
1118
  ${repos}`);
745
1119
  }
746
1120
  if (ctx.dependencies.length > 0) {
747
- const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
1121
+ const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}${d.provenance === "inferred" ? " (inferred)" : ""}`).join("\n");
748
1122
  sections.push(`## Cross-Repo Dependencies
749
1123
 
750
1124
  ${deps}`);
@@ -972,7 +1346,7 @@ ${notes.join("\n")}` : "")
972
1346
  import { z as z6 } from "zod";
973
1347
  var GITHUB_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,99}";
974
1348
  var REPO_RE = new RegExp(`^${GITHUB_NAME}/${GITHUB_NAME}$`);
975
- var SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1349
+ var SLUG_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
976
1350
  var SLUG_MAX = 64;
977
1351
  var PARTS = ["decision", "contract", "behaviour", "plan", "release"];
978
1352
  var BRIEF = ["decision", "contract", "behaviour", "plan"];
@@ -1084,7 +1458,7 @@ function registerGetChange(server, apiClient) {
1084
1458
  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.`,
1085
1459
  inputSchema: {
1086
1460
  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"),
1087
- slug: z6.string().max(SLUG_MAX).regex(SLUG_RE, "a change folder's name: lower-case words joined by -, e.g. customer-reminders").describe("The change's folder name: specs/changes/<slug>/")
1461
+ 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>/")
1088
1462
  },
1089
1463
  annotations: { readOnlyHint: true }
1090
1464
  },
@@ -1444,16 +1818,22 @@ async function hydrate(api, item, meta) {
1444
1818
  }
1445
1819
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
1446
1820
  const budget = maxTokens ?? DEFAULT_BUDGET;
1821
+ const failures = [];
1822
+ const orEmpty = (empty) => (err) => {
1823
+ failures.push(err);
1824
+ return empty;
1825
+ };
1447
1826
  const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
1448
- api.get("/v1/guardrails").catch(() => []),
1449
- api.get("/v1/specs").catch(() => []),
1450
- api.get("/v1/docs").catch(() => ({ items: [] })),
1451
- api.get("/v1/ai/settings/stack").catch(() => []),
1452
- api.get("/v1/standards/rules").catch(() => []),
1827
+ api.get("/v1/guardrails").catch(orEmpty([])),
1828
+ api.get("/v1/specs").catch(orEmpty([])),
1829
+ api.get("/v1/docs").catch(orEmpty({ items: [] })),
1830
+ api.get("/v1/ai/settings/stack").catch(orEmpty([])),
1831
+ api.get("/v1/standards/rules").catch(orEmpty([])),
1453
1832
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
1454
1833
  // blip) gates nothing, it can never hide working context.
1455
1834
  api.get("/v1/doc-repos").catch(() => null)
1456
1835
  ]);
1836
+ if (failures.length === 5) throw failures[0];
1457
1837
  const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
1458
1838
  const guardrails = disabled("fitness") ? [] : rawGuardrails;
1459
1839
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
@@ -1660,47 +2040,280 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
1660
2040
  guardrails: selectedGuardrails.length
1661
2041
  };
1662
2042
  if (sections.length === 0) {
1663
- return { text: "No relevant context found for this task.", stats };
2043
+ 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.";
2044
+ return { text: text3, stats };
1664
2045
  }
1665
2046
  const footer = `
1666
2047
 
1667
2048
  ---
1668
- _Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
2049
+ _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` : "") + `_`;
1669
2050
  return { text: body + footer, stats };
1670
2051
  }
1671
-
1672
- // src/tools/get-context.ts
1673
- function registerGetContext(server, apiClient) {
2052
+
2053
+ // src/tools/get-context.ts
2054
+ function registerGetContext(server, apiClient) {
2055
+ server.registerTool(
2056
+ "get_context",
2057
+ {
2058
+ title: "Get Context",
2059
+ 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.",
2060
+ inputSchema: {
2061
+ task_description: z7.string().describe(
2062
+ "What you're working on (e.g., 'add a new REST endpoint for user preferences')"
2063
+ ),
2064
+ file_paths: z7.array(z7.string()).optional().describe(
2065
+ "File paths being edited \u2014 used for language/category detection"
2066
+ ),
2067
+ max_tokens: z7.number().optional().describe("Maximum token budget for the response (default: 4000)")
2068
+ }
2069
+ },
2070
+ async ({ task_description, file_paths, max_tokens }) => {
2071
+ const { text: text3 } = await buildContext(apiClient, {
2072
+ taskDescription: task_description,
2073
+ filePaths: file_paths,
2074
+ maxTokens: max_tokens
2075
+ });
2076
+ return {
2077
+ content: [{ type: "text", text: text3 }]
2078
+ };
2079
+ }
2080
+ );
2081
+ }
2082
+
2083
+ // src/tools/get-team-setup.ts
2084
+ var HELD_BECAUSE = {
2085
+ "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`,
2086
+ "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>`",
2087
+ "not-checked": "it could not be checked. Ask again later"
2088
+ };
2089
+ function describeSetup(setup) {
2090
+ const lines = [`# Team setup of ${setup.workspace}`, ""];
2091
+ const skills = setup.sessionBrief?.skills ?? [];
2092
+ const held = setup.sessionBrief?.heldSkills ?? [];
2093
+ lines.push(`## Skills a session installs by itself (${skills.length})`, "");
2094
+ if (skills.length) {
2095
+ for (const s of skills) lines.push(`- **${s.name}** version ${s.latestVersion}: ${s.description}`);
2096
+ } else if (!setup.sessionBrief && setup.skills.length) {
2097
+ lines.push(`- ${setup.skills.join(", ")}`);
2098
+ } else {
2099
+ lines.push("None.");
2100
+ }
2101
+ lines.push("");
2102
+ if (held.length) {
2103
+ lines.push(`## Skills that wait for the person (${held.length})`, "");
2104
+ for (const h of held) lines.push(`- **${h.name}**: ${HELD_BECAUSE[h.reason] ?? h.reason}`);
2105
+ lines.push("", "Do not install these yourself: each is the person's to decide.", "");
2106
+ }
2107
+ if (setup.plugin) {
2108
+ lines.push(`## Team plugin`, "", `\`${setup.workspace}-setup\` version ${setup.plugin.version}.`);
2109
+ for (const c of setup.plugin.hookCommands ?? []) lines.push(`- Hook: ${JSON.stringify(c)}`);
2110
+ for (const m of setup.plugin.mcpServers ?? []) {
2111
+ lines.push(`- MCP server ${JSON.stringify(m.name)}: ${JSON.stringify(m.url ?? [m.command, ...m.args ?? []].filter(Boolean).join(" "))}`);
2112
+ }
2113
+ lines.push("");
2114
+ }
2115
+ const parts = Object.entries(setup.parts ?? {});
2116
+ if (parts.length) {
2117
+ lines.push(`## Published parts`, "", ...parts.map(([p, v]) => `- ${p}: version ${v}`), "");
2118
+ }
2119
+ if (setup.memory?.length) {
2120
+ lines.push(`## Team memory`, "", ...setup.memory.map((m) => `- ${m.project}: ${m.fileCount} file(s)`), "");
2121
+ }
2122
+ return lines;
2123
+ }
2124
+ function describeComputer(setup, state) {
2125
+ const lines = ["## This computer", ""];
2126
+ if (!setup.sessionBrief) {
2127
+ return [...lines, "This SystemDox API is too old to compare this computer with the team setup."];
2128
+ }
2129
+ const skills = setup.sessionBrief.skills;
2130
+ const latest = new Map(skills.map((s) => [s.name, s.latestVersion]));
2131
+ lines.push(
2132
+ state.missing.length ? `- Missing: ${state.missing.map((name) => `${name} version ${latest.get(name)}`).join(", ")}` : `- Missing: none of ${skills.length}`
2133
+ );
2134
+ if (state.behind.length) {
2135
+ lines.push(`- Behind: ${state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`).join(", ")}`);
2136
+ }
2137
+ if (state.notFromSystemDox.length) lines.push(`- Not from SystemDox, left alone: ${state.notFromSystemDox.join(", ")}`);
2138
+ if (setup.plugin) {
2139
+ lines.push(
2140
+ state.pluginInstalled ? `- Team plugin: installed${state.pluginVersion ? `, ${state.pluginVersion}` : ""}; the latest is version ${setup.plugin.version}` : "- Team plugin: not installed"
2141
+ );
2142
+ }
2143
+ lines.push("", "## What to do", "");
2144
+ const steps = [];
2145
+ 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.");
2146
+ if (state.behind.length) {
2147
+ 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.");
2148
+ }
2149
+ if (hasPersonParts(setup) && !state.pluginInstalled) {
2150
+ steps.push(
2151
+ `Hooks, MCP servers, team instructions and memory need the person: \`${SETUP_COMMAND}\` in their own terminal. Never answer its prompts for them.`
2152
+ );
2153
+ }
2154
+ lines.push(...steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ["Nothing."]);
2155
+ return lines;
2156
+ }
2157
+ function registerGetTeamSetup(server, apiClient, { localFiles, keyProblem }) {
2158
+ server.registerTool(
2159
+ "get_team_setup",
2160
+ {
2161
+ title: "Get Team Setup",
2162
+ 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.",
2163
+ inputSchema: {},
2164
+ annotations: { readOnlyHint: true }
2165
+ },
2166
+ async () => {
2167
+ const read = await readTeamSetup(apiClient, { keyProblem, timeoutMs: 15e3 });
2168
+ if ("problem" in read) return { content: [{ type: "text", text: read.problem }] };
2169
+ const lines = describeSetup(read.setup);
2170
+ if (localFiles) lines.push(...describeComputer(read.setup, await computerState(read.setup)));
2171
+ else lines.push(CANNOT_SEE);
2172
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2173
+ }
2174
+ );
2175
+ }
2176
+
2177
+ // src/tools/document-graph.ts
2178
+ import { z as z8 } from "zod";
2179
+ var DEFAULT_LIMIT2 = 50;
2180
+ var MAX_LIMIT = 200;
2181
+ var DEFAULT_HOPS = 6;
2182
+ var MAX_HOPS = 8;
2183
+ function place(file, line) {
2184
+ return file ? `${file}:${line}` : "made by a person";
2185
+ }
2186
+ function known(provenance, confidence) {
2187
+ return `${provenance} ${confidence}`;
2188
+ }
2189
+ function formatNeighbours(answer) {
2190
+ const lines = [`# ${answer.doc.label}`, `\`${answer.doc.id}\``, ""];
2191
+ if (answer.links.length === 0) lines.push("No links.");
2192
+ for (const link of answer.links) {
2193
+ lines.push(
2194
+ `- ${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)}`
2195
+ );
2196
+ }
2197
+ if (answer.truncated) {
2198
+ lines.push("", `Showing ${answer.links.length} of ${answer.total}. Raise limit to see more.`);
2199
+ }
2200
+ if (answer.ambiguous.length > 0) {
2201
+ lines.push("", "## Ambiguous: not drawn, because each could mean several documents");
2202
+ for (const entry of answer.ambiguous) {
2203
+ const candidates = entry.candidates.map((c) => `${c.label} (${c.repo}, \`${c.id}\`)`);
2204
+ lines.push(`- ${entry.reference} at ${place(entry.file, entry.line)}: ${candidates.join("; ")}`);
2205
+ }
2206
+ }
2207
+ return lines.join("\n");
2208
+ }
2209
+ function formatPath(answer, maxHops) {
2210
+ if (!answer.found) return `No path within ${maxHops} hops.`;
2211
+ const lines = answer.steps.map((step, index) => {
2212
+ const arrow = step.reversed ? "<-" : "->";
2213
+ 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)}`;
2214
+ });
2215
+ return [...lines, "", `Path confidence: ${answer.confidence}`].join("\n");
2216
+ }
2217
+ function formatStats(stats, repoLinks) {
2218
+ const { declared, extracted, inferred, ambiguous } = stats.links;
2219
+ const lines = [
2220
+ `# The graph: ${stats.nodeCount} documents, ${stats.edgeCount} links`,
2221
+ "",
2222
+ `Links: ${declared} declared \xB7 ${extracted} extracted \xB7 ${inferred} inferred \xB7 ${ambiguous} ambiguous (not drawn)`,
2223
+ "",
2224
+ "## Repositories"
2225
+ ];
2226
+ for (const repo of stats.repos) {
2227
+ const state = repo.behind ? `BEHIND: pushed ${repo.headCommit || "unknown"} at ${repo.pushedAt}, index at ${repo.indexedCommit || "unknown"}` : `index at ${repo.indexedCommit || "unknown"}`;
2228
+ lines.push(`- ${repo.repo}: ${repo.documents} documents, ${repo.links} links, ${state}`);
2229
+ }
2230
+ if (repoLinks.length > 0) {
2231
+ lines.push("", "## Repository links");
2232
+ for (const link of repoLinks) {
2233
+ lines.push(
2234
+ `- ${link.source} ${link.relation} ${link.target} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${place(link.file, link.line)}`
2235
+ );
2236
+ }
2237
+ }
2238
+ return lines.join("\n");
2239
+ }
2240
+ function refused(err) {
2241
+ const status = err.status;
2242
+ if (status !== 404 && status !== 400) throw err;
2243
+ return {
2244
+ content: [{ type: "text", text: err.message }],
2245
+ isError: true
2246
+ };
2247
+ }
2248
+ function registerDocumentGraph(server, apiClient) {
1674
2249
  server.registerTool(
1675
- "get_context",
2250
+ "get_document_links",
1676
2251
  {
1677
- title: "Get Context",
1678
- 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.",
2252
+ 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.",
1679
2253
  inputSchema: {
1680
- task_description: z7.string().describe(
1681
- "What you're working on (e.g., 'add a new REST endpoint for user preferences')"
1682
- ),
1683
- file_paths: z7.array(z7.string()).optional().describe(
1684
- "File paths being edited \u2014 used for language/category detection"
1685
- ),
1686
- max_tokens: z7.number().optional().describe("Maximum token budget for the response (default: 4000)")
2254
+ doc_id: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2255
+ direction: z8.enum(["out", "in", "both"]).optional().describe("Default both"),
2256
+ relation: z8.enum(["links_to", "cites", "mentions", "satisfies", "references"]).optional().describe("Only links of this kind"),
2257
+ limit: z8.number().int().min(1).max(MAX_LIMIT).optional().describe(`Most links to show (default ${DEFAULT_LIMIT2})`)
2258
+ },
2259
+ annotations: { readOnlyHint: true }
2260
+ },
2261
+ async ({ doc_id, direction, relation, limit }) => {
2262
+ const params = { doc: doc_id, limit: String(limit ?? DEFAULT_LIMIT2) };
2263
+ if (direction) params.direction = direction;
2264
+ if (relation) params.relation = relation;
2265
+ try {
2266
+ const answer = await apiClient.get("/v1/graph/neighbours", params);
2267
+ return { content: [{ type: "text", text: formatNeighbours(answer) }] };
2268
+ } catch (err) {
2269
+ return refused(err);
1687
2270
  }
2271
+ }
2272
+ );
2273
+ server.registerTool(
2274
+ "trace_document_path",
2275
+ {
2276
+ 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.",
2277
+ inputSchema: {
2278
+ from: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2279
+ to: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2280
+ max_hops: z8.number().int().min(1).max(MAX_HOPS).optional().describe(`Longest path to look for (default ${DEFAULT_HOPS})`)
2281
+ },
2282
+ annotations: { readOnlyHint: true }
1688
2283
  },
1689
- async ({ task_description, file_paths, max_tokens }) => {
1690
- const { text: text3 } = await buildContext(apiClient, {
1691
- taskDescription: task_description,
1692
- filePaths: file_paths,
1693
- maxTokens: max_tokens
1694
- });
1695
- return {
1696
- content: [{ type: "text", text: text3 }]
1697
- };
2284
+ async ({ from, to, max_hops }) => {
2285
+ const hops = max_hops ?? DEFAULT_HOPS;
2286
+ try {
2287
+ const answer = await apiClient.get("/v1/graph/path", {
2288
+ from,
2289
+ to,
2290
+ maxHops: String(hops)
2291
+ });
2292
+ return { content: [{ type: "text", text: formatPath(answer, hops) }] };
2293
+ } catch (err) {
2294
+ return refused(err);
2295
+ }
2296
+ }
2297
+ );
2298
+ server.registerTool(
2299
+ "get_graph_stats",
2300
+ {
2301
+ 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.",
2302
+ inputSchema: {},
2303
+ annotations: { readOnlyHint: true }
2304
+ },
2305
+ async () => {
2306
+ const [stats, repos] = await Promise.all([
2307
+ apiClient.getFresh("/v1/graph/stats"),
2308
+ apiClient.getFresh("/v1/graph/repos")
2309
+ ]);
2310
+ return { content: [{ type: "text", text: formatStats(stats, repos.links) }] };
1698
2311
  }
1699
2312
  );
1700
2313
  }
1701
2314
 
1702
2315
  // src/tools/create-guardrail.ts
1703
- import { z as z8 } from "zod";
2316
+ import { z as z9 } from "zod";
1704
2317
  var DESCRIPTION2 = "Create a new check in SystemDox. Use this when you discover a pattern that should be enforced (banned pattern, required pattern, coding rule).";
1705
2318
  var CATEGORIES = [
1706
2319
  "imports",
@@ -1713,13 +2326,13 @@ var CATEGORIES = [
1713
2326
  "data-access"
1714
2327
  ];
1715
2328
  var inputSchema2 = () => ({
1716
- title: z8.string().describe("Short title for the check"),
1717
- description: z8.string().describe("Detailed description of what the check enforces and why"),
1718
- severity: z8.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
1719
- category: z8.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
1720
- pattern: z8.string().optional().describe("Bad code pattern that should be avoided"),
1721
- fix: z8.string().optional().describe("Correct code pattern to use instead"),
1722
- fitness_test: z8.string().optional().describe("Shell command to detect violations in CI")
2329
+ title: z9.string().describe("Short title for the check"),
2330
+ description: z9.string().describe("Detailed description of what the check enforces and why"),
2331
+ severity: z9.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
2332
+ category: z9.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
2333
+ pattern: z9.string().optional().describe("Bad code pattern that should be avoided"),
2334
+ fix: z9.string().optional().describe("Correct code pattern to use instead"),
2335
+ fitness_test: z9.string().optional().describe("Shell command to detect violations in CI")
1723
2336
  });
1724
2337
  function registerCreateGuardrail(server, apiClient) {
1725
2338
  const handler2 = async ({
@@ -1785,7 +2398,7 @@ function registerCreateGuardrail(server, apiClient) {
1785
2398
  }
1786
2399
 
1787
2400
  // src/tools/update-spec.ts
1788
- import { z as z9 } from "zod";
2401
+ import { z as z10 } from "zod";
1789
2402
  function registerUpdateSpec(server, apiClient) {
1790
2403
  server.registerTool(
1791
2404
  "update_spec",
@@ -1793,12 +2406,12 @@ function registerUpdateSpec(server, apiClient) {
1793
2406
  title: "Update Spec",
1794
2407
  description: "Update an existing SystemDox spec's proposal, tasks, or requirements. Only draft specs can be modified \u2014 published specs must be versioned first.",
1795
2408
  inputSchema: {
1796
- draft_id: z9.string().describe("ID of the spec to update"),
1797
- section: z9.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
1798
- content: z9.string().describe(
2409
+ draft_id: z10.string().describe("ID of the spec to update"),
2410
+ section: z10.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
2411
+ content: z10.string().describe(
1799
2412
  "New markdown content for the section. For requirements, provide the full requirement content."
1800
2413
  ),
1801
- requirement_name: z9.string().optional().describe(
2414
+ requirement_name: z10.string().optional().describe(
1802
2415
  "Name of the requirement (required when section is 'requirements')"
1803
2416
  )
1804
2417
  },
@@ -1824,235 +2437,22 @@ function registerUpdateSpec(server, apiClient) {
1824
2437
  body.proposalContent = content;
1825
2438
  } else if (section === "tasks") {
1826
2439
  body.taskContent = content;
1827
- } else if (section === "requirements") {
1828
- const existing = spec.requirements;
1829
- const newReq = {
1830
- name: requirement_name ?? "Untitled Requirement",
1831
- content
1832
- };
1833
- body.requirements = [...existing, newReq];
1834
- }
1835
- await apiClient.put(`/v1/specs/${draft_id}`, body);
1836
- apiClient.invalidateCache(`/v1/specs/${draft_id}`);
1837
- apiClient.invalidateCache("/v1/specs");
1838
- return {
1839
- content: [
1840
- {
1841
- type: "text",
1842
- text: `Updated spec "${spec.name}" \u2014 ${section} section modified.`
1843
- }
1844
- ]
1845
- };
1846
- }
1847
- );
1848
- }
1849
-
1850
- // src/tools/report-decision.ts
1851
- import { z as z10 } from "zod";
1852
- function registerReportDecision(server, apiClient) {
1853
- server.registerTool(
1854
- "report_decision",
1855
- {
1856
- title: "Report Decision",
1857
- description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
1858
- inputSchema: {
1859
- title: z10.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
1860
- context: z10.string().describe("What prompted this decision \u2014 the problem or constraint"),
1861
- decision: z10.string().describe("What was decided and why"),
1862
- consequences: z10.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
1863
- related_files: z10.array(z10.string()).optional().describe("File paths related to this decision")
1864
- },
1865
- annotations: {
1866
- readOnlyHint: false
1867
- }
1868
- },
1869
- async ({ title, context, decision, consequences, related_files }) => {
1870
- const adrTitle = `ADR: ${title}`;
1871
- const created = await apiClient.post("/v1/specs", {
1872
- name: adrTitle
1873
- });
1874
- const relatedSection = related_files && related_files.length > 0 ? `
1875
-
1876
- ## Related Files
1877
-
1878
- ${related_files.map((f) => `- \`${f}\``).join("\n")}` : "";
1879
- const adrContent = `# ${adrTitle}
1880
-
1881
- **Date:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
1882
- **Status:** Draft (pending review)
1883
-
1884
- ## Context
1885
-
1886
- ${context}
1887
-
1888
- ## Decision
1889
-
1890
- ${decision}
1891
-
1892
- ## Consequences
1893
-
1894
- ${consequences}` + relatedSection;
1895
- await apiClient.put(`/v1/specs/${created.draftId}`, {
1896
- proposalContent: adrContent
1897
- });
1898
- apiClient.invalidateCache("/v1/specs");
1899
- return {
1900
- content: [
1901
- {
1902
- type: "text",
1903
- text: `Architecture decision recorded: **${adrTitle}** (spec id: ${created.draftId})
1904
-
1905
- Status: draft \u2014 review in SystemDox before publishing.`
1906
- }
1907
- ]
1908
- };
1909
- }
1910
- );
1911
- }
1912
-
1913
- // src/tools/get-project-rules.ts
1914
- import { z as z11 } from "zod";
1915
- var CATEGORY_LABELS = {
1916
- deployment: "Deployment",
1917
- architecture: "Architecture",
1918
- security: "Security",
1919
- testing: "Testing",
1920
- process: "Process",
1921
- other: "Other"
1922
- };
1923
- function registerGetProjectRules(server, apiClient) {
1924
- server.registerTool(
1925
- "get_project_rules",
1926
- {
1927
- title: "Get Project Rules",
1928
- 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.",
1929
- inputSchema: {
1930
- category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
1931
- }
1932
- },
1933
- async ({ category }) => {
1934
- const rules = await apiClient.get(
1935
- "/v1/standards/rules"
1936
- );
1937
- let filtered = rules;
1938
- if (category) {
1939
- filtered = filtered.filter(
1940
- (r) => r.category.toLowerCase() === category.toLowerCase()
1941
- );
1942
- }
1943
- if (filtered.length === 0) {
1944
- const text4 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
1945
- return {
1946
- content: [{ type: "text", text: text4 }]
1947
- };
1948
- }
1949
- const grouped = /* @__PURE__ */ new Map();
1950
- for (const rule of filtered) {
1951
- const cat = rule.category || "other";
1952
- if (!grouped.has(cat)) grouped.set(cat, []);
1953
- grouped.get(cat).push(rule);
1954
- }
1955
- let text3 = `Found ${filtered.length} project rule(s):
1956
-
1957
- `;
1958
- for (const [cat, catRules] of grouped) {
1959
- const label = CATEGORY_LABELS[cat] ?? cat;
1960
- text3 += `## ${label}
1961
-
1962
- `;
1963
- for (const rule of catRules) {
1964
- text3 += `- ${rule.text}
1965
- `;
1966
- }
1967
- text3 += "\n";
1968
- }
1969
- return {
1970
- content: [
1971
- {
1972
- type: "text",
1973
- text: text3
1974
- }
1975
- ]
1976
- };
1977
- }
1978
- );
1979
- }
1980
-
1981
- // src/tools/create-project-rule.ts
1982
- import { z as z12 } from "zod";
1983
- function registerCreateProjectRule(server, apiClient) {
1984
- server.registerTool(
1985
- "create_project_rule",
1986
- {
1987
- title: "Create Project Rule",
1988
- 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.",
1989
- inputSchema: {
1990
- text: z12.string().describe(
1991
- "The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
1992
- ),
1993
- category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
1994
- },
1995
- annotations: {
1996
- readOnlyHint: false
1997
- }
1998
- },
1999
- async ({ text: text3, category }) => {
2000
- const saved = await apiClient.post(
2001
- "/v1/standards/rules",
2002
- { text: text3, category }
2003
- );
2004
- apiClient.invalidateCache("/v1/standards/rules");
2005
- return {
2006
- content: [
2007
- {
2008
- type: "text",
2009
- text: `Project rule created: **${saved.text}** (id: ${saved.id}, category: ${saved.category})`
2010
- }
2011
- ]
2012
- };
2013
- }
2014
- );
2015
- }
2016
-
2017
- // src/tools/get-prompt-templates.ts
2018
- function registerGetPromptTemplates(server, apiClient) {
2019
- server.registerTool(
2020
- "get_prompt_templates",
2021
- {
2022
- title: "Get Prompt Templates",
2023
- description: "Retrieve reusable prompt templates from SystemDox Standards. These are preconfigured prompts that Gen AI should use when generating content \u2014 e.g. code review checklists, documentation templates, or analysis frameworks.",
2024
- inputSchema: {}
2025
- },
2026
- async () => {
2027
- const templates = await apiClient.get(
2028
- "/v1/standards/prompt-templates"
2029
- );
2030
- if (templates.length === 0) {
2031
- return {
2032
- content: [
2033
- {
2034
- type: "text",
2035
- text: `No prompt templates found. Add templates at ${configLink("/docs/prompts", await workspaceBase(apiClient))}`
2036
- }
2037
- ]
2440
+ } else if (section === "requirements") {
2441
+ const existing = spec.requirements;
2442
+ const newReq = {
2443
+ name: requirement_name ?? "Untitled Requirement",
2444
+ content
2038
2445
  };
2446
+ body.requirements = [...existing, newReq];
2039
2447
  }
2040
- const text3 = templates.map(
2041
- (t) => `## ${t.name}
2042
-
2043
- ` + (t.description ? `*${t.description}*
2044
-
2045
- ` : "") + `\`\`\`
2046
- ${t.prompt}
2047
- \`\`\``
2048
- ).join("\n\n---\n\n");
2448
+ await apiClient.put(`/v1/specs/${draft_id}`, body);
2449
+ apiClient.invalidateCache(`/v1/specs/${draft_id}`);
2450
+ apiClient.invalidateCache("/v1/specs");
2049
2451
  return {
2050
2452
  content: [
2051
2453
  {
2052
2454
  type: "text",
2053
- text: `Found ${templates.length} prompt template(s):
2054
-
2055
- ${text3}`
2455
+ text: `Updated spec "${spec.name}" \u2014 ${section} section modified.`
2056
2456
  }
2057
2457
  ]
2058
2458
  };
@@ -2060,38 +2460,62 @@ ${text3}`
2060
2460
  );
2061
2461
  }
2062
2462
 
2063
- // src/tools/create-prompt-template.ts
2064
- import { z as z13 } from "zod";
2065
- function registerCreatePromptTemplate(server, apiClient) {
2463
+ // src/tools/report-decision.ts
2464
+ import { z as z11 } from "zod";
2465
+ function registerReportDecision(server, apiClient) {
2066
2466
  server.registerTool(
2067
- "create_prompt_template",
2467
+ "report_decision",
2068
2468
  {
2069
- title: "Create Prompt Template",
2070
- 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.",
2469
+ title: "Report Decision",
2470
+ description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
2071
2471
  inputSchema: {
2072
- name: z13.string().describe("Short name for the template"),
2073
- description: z13.string().optional().describe("Brief description of when to use this template"),
2074
- prompt: z13.string().describe("The full prompt text that Gen AI will use when generating content")
2472
+ title: z11.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
2473
+ context: z11.string().describe("What prompted this decision \u2014 the problem or constraint"),
2474
+ decision: z11.string().describe("What was decided and why"),
2475
+ consequences: z11.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
2476
+ related_files: z11.array(z11.string()).optional().describe("File paths related to this decision")
2075
2477
  },
2076
2478
  annotations: {
2077
2479
  readOnlyHint: false
2078
2480
  }
2079
2481
  },
2080
- async ({ name, description, prompt }) => {
2081
- const saved = await apiClient.post(
2082
- "/v1/standards/prompt-templates",
2083
- {
2084
- name,
2085
- description: description ?? "",
2086
- prompt
2087
- }
2088
- );
2089
- apiClient.invalidateCache("/v1/standards/prompt-templates");
2482
+ async ({ title, context, decision, consequences, related_files }) => {
2483
+ const adrTitle = `ADR: ${title}`;
2484
+ const created = await apiClient.post("/v1/specs", {
2485
+ name: adrTitle
2486
+ });
2487
+ const relatedSection = related_files && related_files.length > 0 ? `
2488
+
2489
+ ## Related Files
2490
+
2491
+ ${related_files.map((f) => `- \`${f}\``).join("\n")}` : "";
2492
+ const adrContent = `# ${adrTitle}
2493
+
2494
+ **Date:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
2495
+ **Status:** Draft (pending review)
2496
+
2497
+ ## Context
2498
+
2499
+ ${context}
2500
+
2501
+ ## Decision
2502
+
2503
+ ${decision}
2504
+
2505
+ ## Consequences
2506
+
2507
+ ${consequences}` + relatedSection;
2508
+ await apiClient.put(`/v1/specs/${created.draftId}`, {
2509
+ proposalContent: adrContent
2510
+ });
2511
+ apiClient.invalidateCache("/v1/specs");
2090
2512
  return {
2091
2513
  content: [
2092
2514
  {
2093
2515
  type: "text",
2094
- text: `Prompt template created: **${saved.name}** (id: ${saved.id})`
2516
+ text: `Architecture decision recorded: **${adrTitle}** (spec id: ${created.draftId})
2517
+
2518
+ Status: draft \u2014 review in SystemDox before publishing.`
2095
2519
  }
2096
2520
  ]
2097
2521
  };
@@ -2099,217 +2523,210 @@ function registerCreatePromptTemplate(server, apiClient) {
2099
2523
  );
2100
2524
  }
2101
2525
 
2102
- // src/tools/skills.ts
2103
- import { z as z14 } from "zod";
2104
- import { homedir as homedir2 } from "os";
2105
- import { basename, resolve as resolve2 } from "path";
2106
-
2107
- // src/lib/skills.ts
2108
- import { createHash } from "crypto";
2109
- import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
2110
- import { existsSync } from "fs";
2111
- import { homedir } from "os";
2112
- import { dirname, join, relative, resolve, sep } from "path";
2113
- var MANIFEST_FILE = ".systemdox-skill.json";
2114
- var ENTRY_FILE = "SKILL.md";
2115
- var MAX_FILES = 100;
2116
- var MAX_FILE_BYTES = 256 * 1024;
2117
- var MAX_TOTAL_BYTES = 1024 * 1024;
2118
- var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2119
- var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
2120
- var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
2121
- var SkillInstallRefused = class extends Error {
2122
- constructor(dest, edited) {
2123
- super(
2124
- `${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
2125
- );
2126
- this.dest = dest;
2127
- this.edited = edited;
2128
- }
2129
- dest;
2130
- edited;
2526
+ // src/tools/get-project-rules.ts
2527
+ import { z as z12 } from "zod";
2528
+ var CATEGORY_LABELS = {
2529
+ deployment: "Deployment",
2530
+ architecture: "Architecture",
2531
+ security: "Security",
2532
+ testing: "Testing",
2533
+ process: "Process",
2534
+ other: "Other"
2131
2535
  };
2132
- function sha256(content) {
2133
- return createHash("sha256").update(content, "utf8").digest("hex");
2134
- }
2135
- function isValidName(name) {
2136
- return name.length <= 64 && NAME_RE.test(name);
2137
- }
2138
- function isValidPath(path) {
2139
- return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
2140
- }
2141
- function frontmatterName(skillMd) {
2142
- const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
2143
- if (!block) return void 0;
2144
- const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
2145
- return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
2146
- }
2147
- function defaultSkillDir(name) {
2148
- return join(homedir(), ".claude", "skills", name);
2149
- }
2150
- async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
2151
- for (const entry of await readdir(dir, { withFileTypes: true })) {
2152
- if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
2153
- const full = join(dir, entry.name);
2154
- const rel = relative(root, full).split(sep).join("/");
2155
- if (entry.isSymbolicLink()) out.links.push(rel);
2156
- else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
2157
- else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
2158
- else if (entry.isFile()) out.files.push(rel);
2159
- }
2160
- out.files.sort();
2161
- return out;
2162
- }
2163
- async function readTextFiles(root, paths, what) {
2164
- if (paths.length > MAX_FILES) {
2165
- throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
2166
- }
2167
- const decoder = new TextDecoder("utf-8", { fatal: true });
2168
- const files = [];
2169
- let total = 0;
2170
- for (const path of paths) {
2171
- if (!isValidPath(path)) {
2172
- throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
2173
- }
2174
- const raw = await readFile(join(root, ...path.split("/")));
2175
- if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
2176
- if (raw.length > MAX_FILE_BYTES) {
2177
- throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
2178
- }
2179
- total += raw.length;
2180
- if (total > MAX_TOTAL_BYTES) {
2181
- throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
2182
- }
2183
- let content;
2184
- try {
2185
- content = decoder.decode(raw);
2186
- } catch {
2187
- throw new Error(`${path} is not UTF-8 text`);
2188
- }
2189
- files.push({ path, content });
2190
- }
2191
- return files;
2192
- }
2193
- async function readSkillDirectory(directory) {
2194
- const root = resolve(directory);
2195
- if (!existsSync(join(root, ENTRY_FILE))) {
2196
- throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
2197
- }
2198
- const walked = await walk(root, { skipHidden: true });
2199
- if (walked.links.length) {
2200
- throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
2201
- }
2202
- const files = await readTextFiles(root, walked.files, "a skill");
2203
- const skillMd = files.find((f) => f.path === ENTRY_FILE);
2204
- const name = frontmatterName(skillMd.content);
2205
- if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
2206
- return { name, files, skipped: walked.hidden };
2207
- }
2208
- function within(dest, path) {
2209
- if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
2210
- const full = resolve(dest, ...path.split("/"));
2211
- if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
2212
- return full;
2213
- }
2214
- async function readManifest(dest) {
2215
- try {
2216
- return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
2217
- } catch {
2218
- return void 0;
2219
- }
2220
- }
2221
- async function installSkill(version, dest, { overwrite = false } = {}) {
2222
- if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
2223
- const incoming = new Map(version.files.map((f) => [f.path, f]));
2224
- for (const f of version.files) within(dest, f.path);
2225
- if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
2226
- throw new Error(`${dest} is a link: install into a real folder`);
2227
- }
2228
- const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
2229
- if (walked.links.length) {
2230
- throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
2231
- }
2232
- const present = walked.files;
2233
- const manifest = await readManifest(dest);
2234
- const known = Object.fromEntries(
2235
- Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
2236
- );
2237
- const edited = [];
2238
- const untracked = [];
2239
- for (const path of present) {
2240
- const content = await readFile(join(dest, ...path.split("/")), "utf8");
2241
- if (Object.hasOwn(known, path)) {
2242
- if (sha256(content) !== known[path]) edited.push(path);
2243
- } else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
2244
- if (incoming.has(path) || !manifest) edited.push(path);
2245
- else untracked.push(path);
2536
+ function registerGetProjectRules(server, apiClient) {
2537
+ server.registerTool(
2538
+ "get_project_rules",
2539
+ {
2540
+ title: "Get Project Rules",
2541
+ 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.",
2542
+ inputSchema: {
2543
+ category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
2544
+ }
2545
+ },
2546
+ async ({ category }) => {
2547
+ const rules = await apiClient.get(
2548
+ "/v1/standards/rules"
2549
+ );
2550
+ let filtered = rules;
2551
+ if (category) {
2552
+ filtered = filtered.filter(
2553
+ (r) => r.category.toLowerCase() === category.toLowerCase()
2554
+ );
2555
+ }
2556
+ if (filtered.length === 0) {
2557
+ const text4 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
2558
+ return {
2559
+ content: [{ type: "text", text: text4 }]
2560
+ };
2561
+ }
2562
+ const grouped = /* @__PURE__ */ new Map();
2563
+ for (const rule of filtered) {
2564
+ const cat = rule.category || "other";
2565
+ if (!grouped.has(cat)) grouped.set(cat, []);
2566
+ grouped.get(cat).push(rule);
2567
+ }
2568
+ let text3 = `Found ${filtered.length} project rule(s):
2569
+
2570
+ `;
2571
+ for (const [cat, catRules] of grouped) {
2572
+ const label = CATEGORY_LABELS[cat] ?? cat;
2573
+ text3 += `## ${label}
2574
+
2575
+ `;
2576
+ for (const rule of catRules) {
2577
+ text3 += `- ${rule.text}
2578
+ `;
2579
+ }
2580
+ text3 += "\n";
2581
+ }
2582
+ return {
2583
+ content: [
2584
+ {
2585
+ type: "text",
2586
+ text: text3
2587
+ }
2588
+ ]
2589
+ };
2246
2590
  }
2247
- }
2248
- const blocking = edited.filter((p) => incoming.has(p) || !manifest);
2249
- if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
2250
- const written = [];
2251
- for (const f of version.files) {
2252
- const full = within(dest, f.path);
2253
- await mkdir(dirname(full), { recursive: true });
2254
- await writeFile(full, f.content, "utf8");
2255
- written.push(f.path);
2256
- }
2257
- const removed = [];
2258
- const keptEdited = [];
2259
- for (const path of Object.keys(known)) {
2260
- if (incoming.has(path) || !present.includes(path)) continue;
2261
- if (edited.includes(path)) {
2262
- keptEdited.push(path);
2263
- continue;
2591
+ );
2592
+ }
2593
+
2594
+ // src/tools/create-project-rule.ts
2595
+ import { z as z13 } from "zod";
2596
+ function registerCreateProjectRule(server, apiClient) {
2597
+ server.registerTool(
2598
+ "create_project_rule",
2599
+ {
2600
+ title: "Create Project Rule",
2601
+ 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.",
2602
+ inputSchema: {
2603
+ text: z13.string().describe(
2604
+ "The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
2605
+ ),
2606
+ category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
2607
+ },
2608
+ annotations: {
2609
+ readOnlyHint: false
2610
+ }
2611
+ },
2612
+ async ({ text: text3, category }) => {
2613
+ const saved = await apiClient.post(
2614
+ "/v1/standards/rules",
2615
+ { text: text3, category }
2616
+ );
2617
+ apiClient.invalidateCache("/v1/standards/rules");
2618
+ return {
2619
+ content: [
2620
+ {
2621
+ type: "text",
2622
+ text: `Project rule created: **${saved.text}** (id: ${saved.id}, category: ${saved.category})`
2623
+ }
2624
+ ]
2625
+ };
2264
2626
  }
2265
- await rm(within(dest, path));
2266
- removed.push(path);
2267
- }
2268
- const next = {
2269
- name: version.name,
2270
- version: version.version,
2271
- installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2272
- files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
2273
- };
2274
- await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
2275
- return {
2276
- dest,
2277
- version: version.version,
2278
- written,
2279
- removed,
2280
- keptUntracked: untracked.filter((p) => !incoming.has(p)),
2281
- keptEdited
2282
- };
2627
+ );
2283
2628
  }
2284
- function fenceFor(text3) {
2285
- const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
2286
- return "`".repeat(Math.max(3, longest + 1));
2629
+
2630
+ // src/tools/get-prompt-templates.ts
2631
+ function registerGetPromptTemplates(server, apiClient) {
2632
+ server.registerTool(
2633
+ "get_prompt_templates",
2634
+ {
2635
+ title: "Get Prompt Templates",
2636
+ description: "Retrieve reusable prompt templates from SystemDox Standards. These are preconfigured prompts that Gen AI should use when generating content \u2014 e.g. code review checklists, documentation templates, or analysis frameworks.",
2637
+ inputSchema: {}
2638
+ },
2639
+ async () => {
2640
+ const templates = await apiClient.get(
2641
+ "/v1/standards/prompt-templates"
2642
+ );
2643
+ if (templates.length === 0) {
2644
+ return {
2645
+ content: [
2646
+ {
2647
+ type: "text",
2648
+ text: `No prompt templates found. Add templates at ${configLink("/docs/prompts", await workspaceBase(apiClient))}`
2649
+ }
2650
+ ]
2651
+ };
2652
+ }
2653
+ const text3 = templates.map(
2654
+ (t) => `## ${t.name}
2655
+
2656
+ ` + (t.description ? `*${t.description}*
2657
+
2658
+ ` : "") + `\`\`\`
2659
+ ${t.prompt}
2660
+ \`\`\``
2661
+ ).join("\n\n---\n\n");
2662
+ return {
2663
+ content: [
2664
+ {
2665
+ type: "text",
2666
+ text: `Found ${templates.length} prompt template(s):
2667
+
2668
+ ${text3}`
2669
+ }
2670
+ ]
2671
+ };
2672
+ }
2673
+ );
2287
2674
  }
2288
- function describeInstall(name, r) {
2289
- const lines = [
2290
- `Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
2291
- ];
2292
- if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
2293
- if (r.keptEdited.length) {
2294
- lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
2295
- }
2296
- if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
2297
- lines.push("Start a new Claude Code session to load it.");
2298
- return lines.join("\n");
2675
+
2676
+ // src/tools/create-prompt-template.ts
2677
+ import { z as z14 } from "zod";
2678
+ function registerCreatePromptTemplate(server, apiClient) {
2679
+ server.registerTool(
2680
+ "create_prompt_template",
2681
+ {
2682
+ title: "Create Prompt Template",
2683
+ 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.",
2684
+ inputSchema: {
2685
+ name: z14.string().describe("Short name for the template"),
2686
+ description: z14.string().optional().describe("Brief description of when to use this template"),
2687
+ prompt: z14.string().describe("The full prompt text that Gen AI will use when generating content")
2688
+ },
2689
+ annotations: {
2690
+ readOnlyHint: false
2691
+ }
2692
+ },
2693
+ async ({ name, description, prompt }) => {
2694
+ const saved = await apiClient.post(
2695
+ "/v1/standards/prompt-templates",
2696
+ {
2697
+ name,
2698
+ description: description ?? "",
2699
+ prompt
2700
+ }
2701
+ );
2702
+ apiClient.invalidateCache("/v1/standards/prompt-templates");
2703
+ return {
2704
+ content: [
2705
+ {
2706
+ type: "text",
2707
+ text: `Prompt template created: **${saved.name}** (id: ${saved.id})`
2708
+ }
2709
+ ]
2710
+ };
2711
+ }
2712
+ );
2299
2713
  }
2300
2714
 
2301
2715
  // src/tools/skills.ts
2716
+ import { z as z15 } from "zod";
2717
+ import { homedir as homedir3 } from "os";
2718
+ import { basename, resolve as resolve2 } from "path";
2302
2719
  var SKILLS = "/v1/standards/skills";
2303
2720
  function expandHome(path) {
2304
- return path.replace(/^~(?=$|[\\/])/, homedir2());
2721
+ return path.replace(/^~(?=$|[\\/])/, homedir3());
2305
2722
  }
2306
2723
  function text(t) {
2307
2724
  return { content: [{ type: "text", text: t }] };
2308
2725
  }
2309
2726
  function registerSkillTools(server, apiClient, { localFiles }) {
2310
- const fileSchema = z14.object({
2311
- path: z14.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
2312
- content: z14.string().describe("The file's full text, exactly as it is")
2727
+ const fileSchema = z15.object({
2728
+ path: z15.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
2729
+ content: z15.string().describe("The file's full text, exactly as it is")
2313
2730
  });
2314
2731
  server.registerTool(
2315
2732
  "publish_skill",
@@ -2318,13 +2735,13 @@ function registerSkillTools(server, apiClient, { localFiles }) {
2318
2735
  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.",
2319
2736
  inputSchema: {
2320
2737
  ...localFiles ? {
2321
- directory: z14.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2738
+ directory: z15.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2322
2739
  } : {},
2323
- files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
2324
- name: z14.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
2325
- spaceId: z14.string().optional().describe("Space for a NEW skill (default: your default space)"),
2326
- note: z14.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
2327
- baseVersion: z14.number().int().positive().optional().describe(
2740
+ files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
2741
+ name: z15.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
2742
+ spaceId: z15.string().optional().describe("Space for a NEW skill (default: your default space)"),
2743
+ 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")`),
2744
+ baseVersion: z15.number().int().positive().optional().describe(
2328
2745
  "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"
2329
2746
  )
2330
2747
  },
@@ -2390,10 +2807,10 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2390
2807
  title: "Install Skill",
2391
2808
  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.",
2392
2809
  inputSchema: {
2393
- name: z14.string().describe("The skill's name"),
2394
- version: z14.number().int().positive().optional().describe("A version number (default: the latest)"),
2395
- target: z14.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
2396
- ...localFiles ? { overwrite: z14.boolean().optional().describe("Replace files edited since they were installed") } : {}
2810
+ name: z15.string().describe("The skill's name"),
2811
+ version: z15.number().int().positive().optional().describe("A version number (default: the latest)"),
2812
+ target: z15.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
2813
+ ...localFiles ? { overwrite: z15.boolean().optional().describe("Replace files edited since they were installed") } : {}
2397
2814
  },
2398
2815
  annotations: { readOnlyHint: !localFiles }
2399
2816
  },
@@ -2439,11 +2856,11 @@ ${body}`
2439
2856
  }
2440
2857
 
2441
2858
  // src/tools/team-setup.ts
2442
- import { z as z15 } from "zod";
2443
- import { existsSync as existsSync3 } from "fs";
2859
+ import { z as z16 } from "zod";
2860
+ import { existsSync as existsSync4 } from "fs";
2444
2861
  import { lstat as lstat2 } from "fs/promises";
2445
2862
  import { homedir as homedir4 } from "os";
2446
- import { join as join4, resolve as resolve4 } from "path";
2863
+ import { join as join5, resolve as resolve4 } from "path";
2447
2864
 
2448
2865
  // src/lib/person-values.ts
2449
2866
  var PHONE = "phone";
@@ -2524,9 +2941,9 @@ var PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
2524
2941
  var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2525
2942
 
2526
2943
  // src/sync-memory.ts
2527
- import { existsSync as existsSync2 } from "fs";
2528
- import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
2529
- import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
2944
+ import { existsSync as existsSync3 } from "fs";
2945
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
2946
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
2530
2947
 
2531
2948
  // src/lib/memory-sync.ts
2532
2949
  import { createHash as createHash2 } from "crypto";
@@ -2797,63 +3214,6 @@ function lineDiff(before, after) {
2797
3214
  return out;
2798
3215
  }
2799
3216
 
2800
- // src/lib/team-setup-files.ts
2801
- import { execFile } from "child_process";
2802
- import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2803
- import { homedir as homedir3, userInfo } from "os";
2804
- import { dirname as dirname2, join as join2 } from "path";
2805
- import { promisify } from "util";
2806
- var SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2807
- function isWorkspace(value) {
2808
- return typeof value === "string" && value.length <= 64 && SLUG_RE2.test(value);
2809
- }
2810
- function homeDir() {
2811
- return homedir3();
2812
- }
2813
- function systemdoxDir() {
2814
- return join2(homeDir(), ".claude", "systemdox");
2815
- }
2816
- function workspaceDir(workspace) {
2817
- if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
2818
- return join2(systemdoxDir(), workspace);
2819
- }
2820
- function statePath(workspace) {
2821
- return join2(workspaceDir(workspace), "state.json");
2822
- }
2823
- async function readJson(path, empty) {
2824
- let text3;
2825
- try {
2826
- text3 = await readFile2(path, "utf8");
2827
- } catch (e) {
2828
- if (e.code === "ENOENT") return empty;
2829
- throw e;
2830
- }
2831
- try {
2832
- return JSON.parse(text3);
2833
- } catch {
2834
- throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
2835
- }
2836
- }
2837
- async function writeJsonAtomically(path, value, mode) {
2838
- await mkdir2(dirname2(path), { recursive: true });
2839
- const tmp = `${path}.${process.pid}.tmp`;
2840
- await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
2841
- await rename(tmp, path);
2842
- }
2843
- var run = promisify(execFile);
2844
- async function readState(workspace) {
2845
- return readJson(statePath(workspace), {});
2846
- }
2847
- async function writeState(workspace, state) {
2848
- await writeJsonAtomically(statePath(workspace), state);
2849
- }
2850
- async function updateState(workspace, change) {
2851
- const state = await readState(workspace);
2852
- change(state);
2853
- await writeState(workspace, state);
2854
- return state;
2855
- }
2856
-
2857
3217
  // src/sync-memory.ts
2858
3218
  var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
2859
3219
  var SyncFailed = class extends Error {
@@ -2863,7 +3223,7 @@ function refusal(e) {
2863
3223
  const { status, code } = e;
2864
3224
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
2865
3225
  }
2866
- var toPath = (root, rel) => join3(root, ...rel.split("/"));
3226
+ var toPath = (root, rel) => join4(root, ...rel.split("/"));
2867
3227
  async function readFolder(root) {
2868
3228
  const files = {};
2869
3229
  const names = /* @__PURE__ */ new Set();
@@ -2871,14 +3231,14 @@ async function readFolder(root) {
2871
3231
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2872
3232
  if (e.name.startsWith(".")) continue;
2873
3233
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
2874
- if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
3234
+ if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
2875
3235
  else if (e.isFile()) {
2876
3236
  names.add(rel.toLowerCase());
2877
- if (e.name.endsWith(".md")) files[rel] = await readFile3(join3(dir, e.name), "utf8");
3237
+ if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
2878
3238
  }
2879
3239
  }
2880
3240
  }
2881
- if (existsSync2(root)) await walk2(root, "");
3241
+ if (existsSync3(root)) await walk2(root, "");
2882
3242
  return { files, names };
2883
3243
  }
2884
3244
  async function pullTeam(client, ws, project) {
@@ -3087,11 +3447,11 @@ async function people(opts, ws, files, report, hint) {
3087
3447
  report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
3088
3448
  break;
3089
3449
  }
3090
- const refused = result.refused[0];
3091
- if (refused?.code === "SYD-K021") {
3092
- out(` ${refused.detail}. Add them in Staff first, or answer p to file them.`);
3450
+ const refused2 = result.refused[0];
3451
+ if (refused2?.code === "SYD-K021") {
3452
+ out(` ${refused2.detail}. Add them in Staff first, or answer p to file them.`);
3093
3453
  choices = choices.filter((c) => c !== "m");
3094
- } else if (refused) out(` ${refused.detail}`);
3454
+ } else if (refused2) out(` ${refused2.detail}`);
3095
3455
  }
3096
3456
  }
3097
3457
  for (let i = 0; i < filed.length; i += 50) {
@@ -3235,7 +3595,7 @@ async function syncMemory(opts) {
3235
3595
  own.memory.length ? `${ws} has ${own.memory.length} team memories (${own.memory.map((m) => m.project).join(", ")}): name one with --project.` : `${ws} has no team memory yet: name the project to start with --project.`
3236
3596
  );
3237
3597
  }
3238
- const dir = resolve3(opts.dir ?? join3(workspaceDir(ws), "memory", project));
3598
+ const dir = resolve3(opts.dir ?? join4(workspaceDir(ws), "memory", project));
3239
3599
  const run2 = { ...opts, dir };
3240
3600
  const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
3241
3601
  const report = new Report();
@@ -3385,21 +3745,21 @@ async function readPartDirectory(part, directory) {
3385
3745
  let present = true;
3386
3746
  for (let i = 1; i <= segments.length; i++) {
3387
3747
  const rel = segments.slice(0, i).join("/");
3388
- if (await isLink(join4(root, ...segments.slice(0, i)))) {
3748
+ if (await isLink(join5(root, ...segments.slice(0, i)))) {
3389
3749
  links.push(rel);
3390
3750
  present = false;
3391
3751
  break;
3392
3752
  }
3393
3753
  }
3394
- if (present && existsSync3(join4(root, ...segments))) paths.push(path);
3754
+ if (present && existsSync4(join5(root, ...segments))) paths.push(path);
3395
3755
  }
3396
3756
  for (const { dir, md } of layout.folders) {
3397
- const full = join4(root, dir);
3757
+ const full = join5(root, dir);
3398
3758
  if (await isLink(full)) {
3399
3759
  links.push(dir);
3400
3760
  continue;
3401
3761
  }
3402
- if (!existsSync3(full)) continue;
3762
+ if (!existsSync4(full)) continue;
3403
3763
  const walked = await walk(full, { skipHidden: true });
3404
3764
  links.push(...walked.links.map((p) => `${dir}/${p}`));
3405
3765
  leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
@@ -3439,9 +3799,9 @@ function describePerson(id, person) {
3439
3799
  return lines.join("\n");
3440
3800
  }
3441
3801
  function registerTeamSetupTools(server, apiClient, { localFiles }) {
3442
- const fileSchema = z15.object({
3443
- path: z15.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
3444
- content: z15.string().describe("The file's full text, exactly as it is")
3802
+ const fileSchema = z16.object({
3803
+ path: z16.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
3804
+ content: z16.string().describe("The file's full text, exactly as it is")
3445
3805
  });
3446
3806
  server.registerTool(
3447
3807
  "publish_setup_part",
@@ -3449,13 +3809,13 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3449
3809
  title: "Publish Setup Part",
3450
3810
  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."),
3451
3811
  inputSchema: {
3452
- part: z15.enum(PARTS2).describe("Which part of the setup"),
3812
+ part: z16.enum(PARTS2).describe("Which part of the setup"),
3453
3813
  ...localFiles ? {
3454
- directory: z15.string().optional().describe(
3814
+ directory: z16.string().optional().describe(
3455
3815
  "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/"
3456
3816
  )
3457
3817
  } : {},
3458
- files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3818
+ files: z16.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3459
3819
  },
3460
3820
  annotations: { readOnlyHint: false }
3461
3821
  },
@@ -3491,7 +3851,7 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3491
3851
  title: "Resolve Person",
3492
3852
  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.",
3493
3853
  inputSchema: {
3494
- id: z15.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
3854
+ 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")
3495
3855
  },
3496
3856
  annotations: { readOnlyHint: true }
3497
3857
  },
@@ -3508,8 +3868,8 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3508
3868
  title: "Sync Memory",
3509
3869
  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.`,
3510
3870
  inputSchema: {
3511
- project: z15.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
3512
- dryRun: z15.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
3871
+ 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"),
3872
+ dryRun: z16.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
3513
3873
  },
3514
3874
  annotations: { readOnlyHint: false }
3515
3875
  },
@@ -3527,12 +3887,15 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3527
3887
  }
3528
3888
 
3529
3889
  // src/server.ts
3530
- var SERVER_VERSION = true ? "0.1.49" : "0.0.0-dev";
3531
- function createServer(apiClient, { localFiles = false } = {}) {
3532
- const server = new McpServer({
3533
- name: "systemdox-mcp-server",
3534
- version: SERVER_VERSION
3535
- });
3890
+ var SERVER_VERSION = true ? "0.1.50" : "0.0.0-dev";
3891
+ function createServer(apiClient, { localFiles = false, instructions, keyProblem } = {}) {
3892
+ const server = new McpServer(
3893
+ {
3894
+ name: "systemdox-mcp-server",
3895
+ version: SERVER_VERSION
3896
+ },
3897
+ instructions ? { instructions } : void 0
3898
+ );
3536
3899
  registerGetGuardrails(server, apiClient);
3537
3900
  registerGetSpecs(server, apiClient);
3538
3901
  registerGetDocs(server, apiClient);
@@ -3543,6 +3906,8 @@ function createServer(apiClient, { localFiles = false } = {}) {
3543
3906
  registerGetContext(server, apiClient);
3544
3907
  registerGetProjectRules(server, apiClient);
3545
3908
  registerGetPromptTemplates(server, apiClient);
3909
+ registerGetTeamSetup(server, apiClient, { localFiles, keyProblem });
3910
+ registerDocumentGraph(server, apiClient);
3546
3911
  registerCreateGuardrail(server, apiClient);
3547
3912
  registerUpdateSpec(server, apiClient);
3548
3913
  registerReportDecision(server, apiClient);
@@ -3562,6 +3927,14 @@ function lowercaseHeaders(headers) {
3562
3927
  }
3563
3928
  return out;
3564
3929
  }
3930
+ function opensSession(rawBody) {
3931
+ try {
3932
+ const parsed = JSON.parse(rawBody);
3933
+ return (Array.isArray(parsed) ? parsed : [parsed]).some((m) => m?.method === "initialize");
3934
+ } catch {
3935
+ return false;
3936
+ }
3937
+ }
3565
3938
  function jsonError(statusCode, message) {
3566
3939
  return {
3567
3940
  statusCode,
@@ -3615,7 +3988,8 @@ async function handler(event) {
3615
3988
  // per-request client; never cache across invocations
3616
3989
  defaultOrg: void 0
3617
3990
  });
3618
- const server = createServer(apiClient);
3991
+ const instructions = opensSession(rawBody) ? await sessionInstructions(apiClient) : void 0;
3992
+ const server = createServer(apiClient, { instructions });
3619
3993
  const transport = new WebStandardStreamableHTTPServerTransport({
3620
3994
  sessionIdGenerator: void 0,
3621
3995
  // stateless — every POST is independent