@systemdox/mcp-server 0.1.49 → 0.1.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,533 @@ 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 afterInstall(skillMd) {
470
+ return `No restart is needed: a running Claude Code session picks it up. If one does not list it yet, read ${skillMd} and follow it, or run /reload-skills.`;
471
+ }
472
+ function describeInstall(name, r) {
473
+ const lines = [
474
+ `Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
475
+ ];
476
+ if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
477
+ if (r.keptEdited.length) {
478
+ lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
479
+ }
480
+ if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
481
+ lines.push(afterInstall(join(r.dest, "SKILL.md")));
482
+ return lines.join("\n");
483
+ }
484
+
485
+ // src/lib/team-setup-files.ts
486
+ import { execFile } from "child_process";
487
+ import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
488
+ import { homedir as homedir2, userInfo } from "os";
489
+ import { dirname as dirname2, join as join2 } from "path";
490
+ import { promisify } from "util";
491
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
492
+ function isWorkspace(value) {
493
+ return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
494
+ }
495
+ function homeDir() {
496
+ return homedir2();
497
+ }
498
+ function systemdoxDir() {
499
+ return join2(homeDir(), ".claude", "systemdox");
500
+ }
501
+ function workspaceDir(workspace) {
502
+ if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
503
+ return join2(systemdoxDir(), workspace);
504
+ }
505
+ function statePath(workspace) {
506
+ return join2(workspaceDir(workspace), "state.json");
507
+ }
508
+ async function readJson(path, empty) {
509
+ let text3;
510
+ try {
511
+ text3 = await readFile2(path, "utf8");
512
+ } catch (e) {
513
+ if (e.code === "ENOENT") return empty;
514
+ throw e;
515
+ }
516
+ try {
517
+ return JSON.parse(text3);
518
+ } catch {
519
+ throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
520
+ }
521
+ }
522
+ async function writeJsonAtomically(path, value, mode) {
523
+ await mkdir2(dirname2(path), { recursive: true });
524
+ const tmp = `${path}.${process.pid}.tmp`;
525
+ await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
526
+ await rename(tmp, path);
527
+ }
528
+ var run = promisify(execFile);
529
+ async function readState(workspace) {
530
+ return readJson(statePath(workspace), {});
531
+ }
532
+ async function writeState(workspace, state) {
533
+ await writeJsonAtomically(statePath(workspace), state);
534
+ }
535
+ async function updateState(workspace, change) {
536
+ const state = await readState(workspace);
537
+ change(state);
538
+ await writeState(workspace, state);
539
+ return state;
540
+ }
541
+
542
+ // src/lib/session-brief.ts
543
+ var INSTRUCTIONS_MAX = 2e3;
544
+ var BRIEF_TIMEOUT_MS = 8e3;
545
+ var SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
546
+ 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.`;
547
+ 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.";
548
+ 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.";
549
+ function noKey(reason) {
550
+ 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.`;
551
+ }
552
+ function olderApi(setup) {
553
+ 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.`;
554
+ }
555
+ async function readJsonQuietly(path) {
556
+ try {
557
+ return JSON.parse(await readFile3(path, "utf8"));
558
+ } catch {
559
+ return void 0;
560
+ }
561
+ }
562
+ async function computerState(setup) {
563
+ const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
564
+ const installed = await readJsonQuietly(join3(homeDir(), ".claude", "plugins", "installed_plugins.json"));
565
+ const entries2 = installed?.plugins?.[id];
566
+ const entry = Array.isArray(entries2) ? entries2.find((e) => (e?.scope ?? "user") === "user") : void 0;
567
+ const state = {
568
+ missing: [],
569
+ behind: [],
570
+ notFromSystemDox: [],
571
+ pluginInstalled: !!entry,
572
+ pluginVersion: entry?.version
573
+ };
574
+ const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
575
+ for (const skill of setup.sessionBrief?.skills ?? []) {
576
+ const dir = defaultSkillDir(skill.name);
577
+ if (!existsSync2(dir)) {
578
+ if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
579
+ continue;
580
+ }
581
+ const manifest = await readJsonQuietly(join3(dir, MANIFEST_FILE));
582
+ if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
583
+ state.notFromSystemDox.push(skill.name);
584
+ } else if (manifest.version !== skill.latestVersion) {
585
+ state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
586
+ }
587
+ }
588
+ return state;
589
+ }
590
+ function hasPersonParts(setup) {
591
+ return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
592
+ }
593
+ var MORE = (n) => `${n} more (get_team_setup lists them)`;
594
+ var LIST_MIN = `all ${MORE(100)}`.length;
595
+ function listWithin(items, room) {
596
+ const whole2 = items.join(", ");
597
+ if (whole2.length <= room) return whole2;
598
+ const kept = [];
599
+ let used = 0;
600
+ for (let i = 0; i < items.length; i++) {
601
+ const rest = items.length - i - 1;
602
+ const cost = items[i].length + (kept.length ? 2 : 0);
603
+ if (used + cost + ` and ${MORE(rest)}`.length > room) break;
604
+ kept.push(items[i]);
605
+ used += cost;
606
+ }
607
+ return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
608
+ }
609
+ function localSection(setup, state, room) {
610
+ const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
611
+ const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
612
+ const lines = [];
613
+ if (state.missing.length) {
614
+ lines.push({
615
+ head: "On this computer, missing: ",
616
+ items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
617
+ 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.`
618
+ });
619
+ }
620
+ if (state.behind.length) {
621
+ lines.push({
622
+ head: "On this computer, behind: ",
623
+ items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
624
+ tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
625
+ });
626
+ }
627
+ if (state.notFromSystemDox.length) {
628
+ lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
629
+ }
630
+ const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
631
+ while (lines.length && fixed() > room) lines.pop();
632
+ if (!lines.length) return "";
633
+ const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
634
+ return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
635
+ }
636
+ function fit(text3, section) {
637
+ const whole2 = section ? `${text3}
638
+
639
+ ${section}` : text3;
640
+ if (whole2.length <= INSTRUCTIONS_MAX) return whole2;
641
+ return text3.length <= INSTRUCTIONS_MAX ? text3 : `${text3.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
642
+ }
643
+ async function readTeamSetup(apiClient, { keyProblem, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
644
+ if (keyProblem) return { problem: noKey(keyProblem) };
645
+ try {
646
+ const setup = await apiClient.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
647
+ return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
648
+ } catch (err) {
649
+ const status = err?.status;
650
+ return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
651
+ }
652
+ }
653
+ async function sessionInstructions(apiClient, options = {}) {
654
+ const read = await readTeamSetup(apiClient, options);
655
+ if ("problem" in read) return read.problem;
656
+ const text3 = read.setup.sessionBrief?.text ?? olderApi(read.setup);
657
+ try {
658
+ if (!options.localFiles) return fit(text3, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
659
+ const room = INSTRUCTIONS_MAX - text3.length - 2;
660
+ return fit(text3, localSection(read.setup, await computerState(read.setup), room));
661
+ } catch {
662
+ return fit(text3, "");
663
+ }
664
+ }
665
+
666
+ // src/server.ts
667
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
668
+
669
+ // src/tools/get-guardrails.ts
670
+ import { z } from "zod";
671
+
672
+ // src/lib/language.ts
673
+ var EXTENSION_MAP = {
674
+ ".py": { language: "python", categories: ["api", "backend", "data-access"] },
675
+ ".ts": { language: "typescript", categories: ["frontend", "api"] },
676
+ ".tsx": { language: "typescript", categories: ["frontend"] },
677
+ ".js": { language: "javascript", categories: ["frontend"] },
678
+ ".jsx": { language: "javascript", categories: ["frontend"] },
679
+ ".yaml": {
680
+ language: "cloudformation",
681
+ categories: ["infrastructure", "architecture"]
682
+ },
683
+ ".yml": {
684
+ language: "cloudformation",
685
+ categories: ["infrastructure", "architecture"]
686
+ },
687
+ ".json": { language: "json", categories: ["configuration"] },
688
+ ".css": { language: "css", categories: ["frontend"] },
689
+ ".html": { language: "html", categories: ["frontend"] },
690
+ ".sql": { language: "sql", categories: ["data-access"] },
691
+ ".sh": { language: "shell", categories: ["infrastructure"] },
692
+ ".bash": { language: "shell", categories: ["infrastructure"] },
693
+ ".tf": { language: "terraform", categories: ["infrastructure"] },
694
+ ".md": { language: "markdown", categories: ["documentation"] }
695
+ };
696
+ var PATH_CATEGORY_RULES = [
697
+ { pattern: /backend\/functions\//, categories: ["api", "backend"] },
698
+ { pattern: /backend\/layers\//, categories: ["backend"] },
699
+ { pattern: /backend\//, categories: ["backend"] },
700
+ { pattern: /apps\/app\//, categories: ["frontend"] },
701
+ { pattern: /apps\/website\//, categories: ["frontend"] },
702
+ { pattern: /apps\/portal\//, categories: ["frontend"] },
703
+ { pattern: /packages\/ui\//, categories: ["frontend"] },
704
+ { pattern: /infra\//, categories: ["infrastructure"] },
705
+ { pattern: /\.github\//, categories: ["infrastructure"] },
706
+ { pattern: /tests?\//, categories: ["testing"] },
707
+ { pattern: /spec/, categories: ["testing"] },
708
+ { pattern: /docs\/adr/, categories: ["architecture"] },
709
+ { pattern: /template\.yaml$/, categories: ["infrastructure", "architecture"] }
710
+ ];
711
+ function detectLanguage(filePath) {
712
+ const ext = filePath.slice(filePath.lastIndexOf("."));
713
+ return EXTENSION_MAP[ext];
714
+ }
715
+ function detectCategories(filePaths) {
716
+ const categories = /* @__PURE__ */ new Set();
717
+ for (const filePath of filePaths) {
718
+ const langInfo = detectLanguage(filePath);
719
+ if (langInfo) {
720
+ for (const cat of langInfo.categories) {
721
+ categories.add(cat);
722
+ }
723
+ }
724
+ for (const rule of PATH_CATEGORY_RULES) {
725
+ if (rule.pattern.test(filePath)) {
726
+ for (const cat of rule.categories) {
727
+ categories.add(cat);
728
+ }
729
+ }
730
+ }
731
+ }
732
+ return Array.from(categories);
733
+ }
734
+ function detectLanguages(filePaths) {
735
+ const languages = /* @__PURE__ */ new Set();
736
+ for (const filePath of filePaths) {
737
+ const info = detectLanguage(filePath);
738
+ if (info) languages.add(info.language);
739
+ }
740
+ return Array.from(languages);
741
+ }
742
+
743
+ // src/lib/deprecation.ts
744
+ var LEGACY_TOOL_REMOVAL_EARLIEST = "2027-01-31";
745
+ function legacyToolDescription(canonical, description) {
746
+ return `DEPRECATED \u2014 use \`${canonical}\` instead. This name still works and will not be removed before ${LEGACY_TOOL_REMOVAL_EARLIEST}. ${description}`;
747
+ }
748
+
749
+ // src/lib/config-links.ts
750
+ async function workspaceBase(apiClient) {
751
+ try {
752
+ const access = await apiClient.get(
753
+ "/v1/docs/portal-access"
754
+ );
755
+ const slug = (access.host ?? "").split(".")[0];
756
+ return slug ? `https://${slug}.systemdox.com` : null;
757
+ } catch {
758
+ return null;
759
+ }
760
+ }
761
+ function configLink(path, base) {
762
+ return base ? `${base}${path}` : `${path} in your SystemDox workspace`;
763
+ }
764
+
765
+ // src/tools/get-guardrails.ts
766
+ var DESCRIPTION = "Retrieve SystemDox checks (banned patterns, required patterns, coding rules). Filter by language, category, or file path to get only relevant checks.";
767
+ var inputSchema = () => ({
768
+ language: z.string().optional().describe(
769
+ "Filter by programming language (python, typescript, javascript, cloudformation)"
770
+ ),
771
+ category: z.string().optional().describe(
772
+ "Filter by category (imports, auth, error-handling, architecture, testing, security, conventions, data-access)"
773
+ ),
774
+ file_path: z.string().optional().describe(
775
+ "File path being edited \u2014 language is auto-detected from the extension"
776
+ )
777
+ });
778
+ function registerGetGuardrails(server, apiClient) {
779
+ const handler2 = async ({
780
+ language,
781
+ category,
782
+ file_path
783
+ }) => {
784
+ const guardrails = await apiClient.get("/v1/guardrails");
785
+ let filtered = guardrails;
786
+ const lang = language ?? (file_path ? detectLanguage(file_path)?.language : void 0);
787
+ if (lang) {
788
+ filtered = filtered.filter(
789
+ (g) => g.description.toLowerCase().includes(lang.toLowerCase()) || g.category.toLowerCase().includes(lang.toLowerCase()) || g.title.toLowerCase().includes(lang.toLowerCase())
790
+ );
791
+ }
792
+ if (category) {
793
+ filtered = filtered.filter(
794
+ (g) => g.category.toLowerCase() === category.toLowerCase()
795
+ );
796
+ }
797
+ if (filtered.length === 0) {
798
+ 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}"` : ""}.`;
799
+ return {
800
+ content: [{ type: "text", text: text4 }]
801
+ };
802
+ }
803
+ const text3 = filtered.map(
804
+ (g) => `## ${g.title} [${g.severity}]
805
+
806
+ ${g.description}
807
+
808
+ ` + (g.pattern ? `**Bad pattern:**
432
809
  \`\`\`
433
810
  ${g.pattern}
434
811
  \`\`\`
@@ -744,7 +1121,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
744
1121
  ${repos}`);
745
1122
  }
746
1123
  if (ctx.dependencies.length > 0) {
747
- const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
1124
+ const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}${d.provenance === "inferred" ? " (inferred)" : ""}`).join("\n");
748
1125
  sections.push(`## Cross-Repo Dependencies
749
1126
 
750
1127
  ${deps}`);
@@ -972,7 +1349,7 @@ ${notes.join("\n")}` : "")
972
1349
  import { z as z6 } from "zod";
973
1350
  var GITHUB_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,99}";
974
1351
  var REPO_RE = new RegExp(`^${GITHUB_NAME}/${GITHUB_NAME}$`);
975
- var SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1352
+ var SLUG_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
976
1353
  var SLUG_MAX = 64;
977
1354
  var PARTS = ["decision", "contract", "behaviour", "plan", "release"];
978
1355
  var BRIEF = ["decision", "contract", "behaviour", "plan"];
@@ -1084,7 +1461,7 @@ function registerGetChange(server, apiClient) {
1084
1461
  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
1462
  inputSchema: {
1086
1463
  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>/")
1464
+ 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
1465
  },
1089
1466
  annotations: { readOnlyHint: true }
1090
1467
  },
@@ -1444,16 +1821,22 @@ async function hydrate(api, item, meta) {
1444
1821
  }
1445
1822
  async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
1446
1823
  const budget = maxTokens ?? DEFAULT_BUDGET;
1824
+ const failures = [];
1825
+ const orEmpty = (empty) => (err) => {
1826
+ failures.push(err);
1827
+ return empty;
1828
+ };
1447
1829
  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(() => []),
1830
+ api.get("/v1/guardrails").catch(orEmpty([])),
1831
+ api.get("/v1/specs").catch(orEmpty([])),
1832
+ api.get("/v1/docs").catch(orEmpty({ items: [] })),
1833
+ api.get("/v1/ai/settings/stack").catch(orEmpty([])),
1834
+ api.get("/v1/standards/rules").catch(orEmpty([])),
1453
1835
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
1454
1836
  // blip) gates nothing, it can never hide working context.
1455
1837
  api.get("/v1/doc-repos").catch(() => null)
1456
1838
  ]);
1839
+ if (failures.length === 5) throw failures[0];
1457
1840
  const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
1458
1841
  const guardrails = disabled("fitness") ? [] : rawGuardrails;
1459
1842
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
@@ -1660,47 +2043,280 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
1660
2043
  guardrails: selectedGuardrails.length
1661
2044
  };
1662
2045
  if (sections.length === 0) {
1663
- return { text: "No relevant context found for this task.", stats };
2046
+ 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.";
2047
+ return { text: text3, stats };
1664
2048
  }
1665
2049
  const footer = `
1666
2050
 
1667
2051
  ---
1668
- _Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
2052
+ _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
2053
  return { text: body + footer, stats };
1670
2054
  }
1671
-
1672
- // src/tools/get-context.ts
1673
- function registerGetContext(server, apiClient) {
2055
+
2056
+ // src/tools/get-context.ts
2057
+ function registerGetContext(server, apiClient) {
2058
+ server.registerTool(
2059
+ "get_context",
2060
+ {
2061
+ title: "Get Context",
2062
+ 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.",
2063
+ inputSchema: {
2064
+ task_description: z7.string().describe(
2065
+ "What you're working on (e.g., 'add a new REST endpoint for user preferences')"
2066
+ ),
2067
+ file_paths: z7.array(z7.string()).optional().describe(
2068
+ "File paths being edited \u2014 used for language/category detection"
2069
+ ),
2070
+ max_tokens: z7.number().optional().describe("Maximum token budget for the response (default: 4000)")
2071
+ }
2072
+ },
2073
+ async ({ task_description, file_paths, max_tokens }) => {
2074
+ const { text: text3 } = await buildContext(apiClient, {
2075
+ taskDescription: task_description,
2076
+ filePaths: file_paths,
2077
+ maxTokens: max_tokens
2078
+ });
2079
+ return {
2080
+ content: [{ type: "text", text: text3 }]
2081
+ };
2082
+ }
2083
+ );
2084
+ }
2085
+
2086
+ // src/tools/get-team-setup.ts
2087
+ var HELD_BECAUSE = {
2088
+ "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`,
2089
+ "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>`",
2090
+ "not-checked": "it could not be checked. Ask again later"
2091
+ };
2092
+ function describeSetup(setup) {
2093
+ const lines = [`# Team setup of ${setup.workspace}`, ""];
2094
+ const skills = setup.sessionBrief?.skills ?? [];
2095
+ const held = setup.sessionBrief?.heldSkills ?? [];
2096
+ lines.push(`## Skills a session installs by itself (${skills.length})`, "");
2097
+ if (skills.length) {
2098
+ for (const s of skills) lines.push(`- **${s.name}** version ${s.latestVersion}: ${s.description}`);
2099
+ } else if (!setup.sessionBrief && setup.skills.length) {
2100
+ lines.push(`- ${setup.skills.join(", ")}`);
2101
+ } else {
2102
+ lines.push("None.");
2103
+ }
2104
+ lines.push("");
2105
+ if (held.length) {
2106
+ lines.push(`## Skills that wait for the person (${held.length})`, "");
2107
+ for (const h of held) lines.push(`- **${h.name}**: ${HELD_BECAUSE[h.reason] ?? h.reason}`);
2108
+ lines.push("", "Do not install these yourself: each is the person's to decide.", "");
2109
+ }
2110
+ if (setup.plugin) {
2111
+ lines.push(`## Team plugin`, "", `\`${setup.workspace}-setup\` version ${setup.plugin.version}.`);
2112
+ for (const c of setup.plugin.hookCommands ?? []) lines.push(`- Hook: ${JSON.stringify(c)}`);
2113
+ for (const m of setup.plugin.mcpServers ?? []) {
2114
+ lines.push(`- MCP server ${JSON.stringify(m.name)}: ${JSON.stringify(m.url ?? [m.command, ...m.args ?? []].filter(Boolean).join(" "))}`);
2115
+ }
2116
+ lines.push("");
2117
+ }
2118
+ const parts = Object.entries(setup.parts ?? {});
2119
+ if (parts.length) {
2120
+ lines.push(`## Published parts`, "", ...parts.map(([p, v]) => `- ${p}: version ${v}`), "");
2121
+ }
2122
+ if (setup.memory?.length) {
2123
+ lines.push(`## Team memory`, "", ...setup.memory.map((m) => `- ${m.project}: ${m.fileCount} file(s)`), "");
2124
+ }
2125
+ return lines;
2126
+ }
2127
+ function describeComputer(setup, state) {
2128
+ const lines = ["## This computer", ""];
2129
+ if (!setup.sessionBrief) {
2130
+ return [...lines, "This SystemDox API is too old to compare this computer with the team setup."];
2131
+ }
2132
+ const skills = setup.sessionBrief.skills;
2133
+ const latest = new Map(skills.map((s) => [s.name, s.latestVersion]));
2134
+ lines.push(
2135
+ state.missing.length ? `- Missing: ${state.missing.map((name) => `${name} version ${latest.get(name)}`).join(", ")}` : `- Missing: none of ${skills.length}`
2136
+ );
2137
+ if (state.behind.length) {
2138
+ lines.push(`- Behind: ${state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`).join(", ")}`);
2139
+ }
2140
+ if (state.notFromSystemDox.length) lines.push(`- Not from SystemDox, left alone: ${state.notFromSystemDox.join(", ")}`);
2141
+ if (setup.plugin) {
2142
+ lines.push(
2143
+ state.pluginInstalled ? `- Team plugin: installed${state.pluginVersion ? `, ${state.pluginVersion}` : ""}; the latest is version ${setup.plugin.version}` : "- Team plugin: not installed"
2144
+ );
2145
+ }
2146
+ lines.push("", "## What to do", "");
2147
+ const steps = [];
2148
+ 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.");
2149
+ if (state.behind.length) {
2150
+ 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.");
2151
+ }
2152
+ if (hasPersonParts(setup) && !state.pluginInstalled) {
2153
+ steps.push(
2154
+ `Hooks, MCP servers, team instructions and memory need the person: \`${SETUP_COMMAND}\` in their own terminal. Never answer its prompts for them.`
2155
+ );
2156
+ }
2157
+ lines.push(...steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ["Nothing."]);
2158
+ return lines;
2159
+ }
2160
+ function registerGetTeamSetup(server, apiClient, { localFiles, keyProblem }) {
2161
+ server.registerTool(
2162
+ "get_team_setup",
2163
+ {
2164
+ title: "Get Team Setup",
2165
+ 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.",
2166
+ inputSchema: {},
2167
+ annotations: { readOnlyHint: true }
2168
+ },
2169
+ async () => {
2170
+ const read = await readTeamSetup(apiClient, { keyProblem, timeoutMs: 15e3 });
2171
+ if ("problem" in read) return { content: [{ type: "text", text: read.problem }] };
2172
+ const lines = describeSetup(read.setup);
2173
+ if (localFiles) lines.push(...describeComputer(read.setup, await computerState(read.setup)));
2174
+ else lines.push(CANNOT_SEE);
2175
+ return { content: [{ type: "text", text: lines.join("\n") }] };
2176
+ }
2177
+ );
2178
+ }
2179
+
2180
+ // src/tools/document-graph.ts
2181
+ import { z as z8 } from "zod";
2182
+ var DEFAULT_LIMIT2 = 50;
2183
+ var MAX_LIMIT = 200;
2184
+ var DEFAULT_HOPS = 6;
2185
+ var MAX_HOPS = 8;
2186
+ function place(file, line) {
2187
+ return file ? `${file}:${line}` : "made by a person";
2188
+ }
2189
+ function known(provenance, confidence) {
2190
+ return `${provenance} ${confidence}`;
2191
+ }
2192
+ function formatNeighbours(answer) {
2193
+ const lines = [`# ${answer.doc.label}`, `\`${answer.doc.id}\``, ""];
2194
+ if (answer.links.length === 0) lines.push("No links.");
2195
+ for (const link of answer.links) {
2196
+ lines.push(
2197
+ `- ${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)}`
2198
+ );
2199
+ }
2200
+ if (answer.truncated) {
2201
+ lines.push("", `Showing ${answer.links.length} of ${answer.total}. Raise limit to see more.`);
2202
+ }
2203
+ if (answer.ambiguous.length > 0) {
2204
+ lines.push("", "## Ambiguous: not drawn, because each could mean several documents");
2205
+ for (const entry of answer.ambiguous) {
2206
+ const candidates = entry.candidates.map((c) => `${c.label} (${c.repo}, \`${c.id}\`)`);
2207
+ lines.push(`- ${entry.reference} at ${place(entry.file, entry.line)}: ${candidates.join("; ")}`);
2208
+ }
2209
+ }
2210
+ return lines.join("\n");
2211
+ }
2212
+ function formatPath(answer, maxHops) {
2213
+ if (!answer.found) return `No path within ${maxHops} hops.`;
2214
+ const lines = answer.steps.map((step, index) => {
2215
+ const arrow = step.reversed ? "<-" : "->";
2216
+ 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)}`;
2217
+ });
2218
+ return [...lines, "", `Path confidence: ${answer.confidence}`].join("\n");
2219
+ }
2220
+ function formatStats(stats, repoLinks) {
2221
+ const { declared, extracted, inferred, ambiguous } = stats.links;
2222
+ const lines = [
2223
+ `# The graph: ${stats.nodeCount} documents, ${stats.edgeCount} links`,
2224
+ "",
2225
+ `Links: ${declared} declared \xB7 ${extracted} extracted \xB7 ${inferred} inferred \xB7 ${ambiguous} ambiguous (not drawn)`,
2226
+ "",
2227
+ "## Repositories"
2228
+ ];
2229
+ for (const repo of stats.repos) {
2230
+ const state = repo.behind ? `BEHIND: pushed ${repo.headCommit || "unknown"} at ${repo.pushedAt}, index at ${repo.indexedCommit || "unknown"}` : `index at ${repo.indexedCommit || "unknown"}`;
2231
+ lines.push(`- ${repo.repo}: ${repo.documents} documents, ${repo.links} links, ${state}`);
2232
+ }
2233
+ if (repoLinks.length > 0) {
2234
+ lines.push("", "## Repository links");
2235
+ for (const link of repoLinks) {
2236
+ lines.push(
2237
+ `- ${link.source} ${link.relation} ${link.target} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${place(link.file, link.line)}`
2238
+ );
2239
+ }
2240
+ }
2241
+ return lines.join("\n");
2242
+ }
2243
+ function refused(err) {
2244
+ const status = err.status;
2245
+ if (status !== 404 && status !== 400) throw err;
2246
+ return {
2247
+ content: [{ type: "text", text: err.message }],
2248
+ isError: true
2249
+ };
2250
+ }
2251
+ function registerDocumentGraph(server, apiClient) {
1674
2252
  server.registerTool(
1675
- "get_context",
2253
+ "get_document_links",
1676
2254
  {
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.",
2255
+ 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
2256
  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)")
2257
+ doc_id: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2258
+ direction: z8.enum(["out", "in", "both"]).optional().describe("Default both"),
2259
+ relation: z8.enum(["links_to", "cites", "mentions", "satisfies", "references"]).optional().describe("Only links of this kind"),
2260
+ limit: z8.number().int().min(1).max(MAX_LIMIT).optional().describe(`Most links to show (default ${DEFAULT_LIMIT2})`)
2261
+ },
2262
+ annotations: { readOnlyHint: true }
2263
+ },
2264
+ async ({ doc_id, direction, relation, limit }) => {
2265
+ const params = { doc: doc_id, limit: String(limit ?? DEFAULT_LIMIT2) };
2266
+ if (direction) params.direction = direction;
2267
+ if (relation) params.relation = relation;
2268
+ try {
2269
+ const answer = await apiClient.get("/v1/graph/neighbours", params);
2270
+ return { content: [{ type: "text", text: formatNeighbours(answer) }] };
2271
+ } catch (err) {
2272
+ return refused(err);
1687
2273
  }
2274
+ }
2275
+ );
2276
+ server.registerTool(
2277
+ "trace_document_path",
2278
+ {
2279
+ 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.",
2280
+ inputSchema: {
2281
+ from: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2282
+ to: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
2283
+ max_hops: z8.number().int().min(1).max(MAX_HOPS).optional().describe(`Longest path to look for (default ${DEFAULT_HOPS})`)
2284
+ },
2285
+ annotations: { readOnlyHint: true }
1688
2286
  },
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
- };
2287
+ async ({ from, to, max_hops }) => {
2288
+ const hops = max_hops ?? DEFAULT_HOPS;
2289
+ try {
2290
+ const answer = await apiClient.get("/v1/graph/path", {
2291
+ from,
2292
+ to,
2293
+ maxHops: String(hops)
2294
+ });
2295
+ return { content: [{ type: "text", text: formatPath(answer, hops) }] };
2296
+ } catch (err) {
2297
+ return refused(err);
2298
+ }
2299
+ }
2300
+ );
2301
+ server.registerTool(
2302
+ "get_graph_stats",
2303
+ {
2304
+ 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.",
2305
+ inputSchema: {},
2306
+ annotations: { readOnlyHint: true }
2307
+ },
2308
+ async () => {
2309
+ const [stats, repos] = await Promise.all([
2310
+ apiClient.getFresh("/v1/graph/stats"),
2311
+ apiClient.getFresh("/v1/graph/repos")
2312
+ ]);
2313
+ return { content: [{ type: "text", text: formatStats(stats, repos.links) }] };
1698
2314
  }
1699
2315
  );
1700
2316
  }
1701
2317
 
1702
2318
  // src/tools/create-guardrail.ts
1703
- import { z as z8 } from "zod";
2319
+ import { z as z9 } from "zod";
1704
2320
  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
2321
  var CATEGORIES = [
1706
2322
  "imports",
@@ -1713,13 +2329,13 @@ var CATEGORIES = [
1713
2329
  "data-access"
1714
2330
  ];
1715
2331
  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")
2332
+ title: z9.string().describe("Short title for the check"),
2333
+ description: z9.string().describe("Detailed description of what the check enforces and why"),
2334
+ severity: z9.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
2335
+ category: z9.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
2336
+ pattern: z9.string().optional().describe("Bad code pattern that should be avoided"),
2337
+ fix: z9.string().optional().describe("Correct code pattern to use instead"),
2338
+ fitness_test: z9.string().optional().describe("Shell command to detect violations in CI")
1723
2339
  });
1724
2340
  function registerCreateGuardrail(server, apiClient) {
1725
2341
  const handler2 = async ({
@@ -1785,7 +2401,7 @@ function registerCreateGuardrail(server, apiClient) {
1785
2401
  }
1786
2402
 
1787
2403
  // src/tools/update-spec.ts
1788
- import { z as z9 } from "zod";
2404
+ import { z as z10 } from "zod";
1789
2405
  function registerUpdateSpec(server, apiClient) {
1790
2406
  server.registerTool(
1791
2407
  "update_spec",
@@ -1793,12 +2409,12 @@ function registerUpdateSpec(server, apiClient) {
1793
2409
  title: "Update Spec",
1794
2410
  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
2411
  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(
2412
+ draft_id: z10.string().describe("ID of the spec to update"),
2413
+ section: z10.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
2414
+ content: z10.string().describe(
1799
2415
  "New markdown content for the section. For requirements, provide the full requirement content."
1800
2416
  ),
1801
- requirement_name: z9.string().optional().describe(
2417
+ requirement_name: z10.string().optional().describe(
1802
2418
  "Name of the requirement (required when section is 'requirements')"
1803
2419
  )
1804
2420
  },
@@ -1823,236 +2439,23 @@ function registerUpdateSpec(server, apiClient) {
1823
2439
  if (section === "proposal") {
1824
2440
  body.proposalContent = content;
1825
2441
  } else if (section === "tasks") {
1826
- 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
- ]
2442
+ body.taskContent = content;
2443
+ } else if (section === "requirements") {
2444
+ const existing = spec.requirements;
2445
+ const newReq = {
2446
+ name: requirement_name ?? "Untitled Requirement",
2447
+ content
2038
2448
  };
2449
+ body.requirements = [...existing, newReq];
2039
2450
  }
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");
2451
+ await apiClient.put(`/v1/specs/${draft_id}`, body);
2452
+ apiClient.invalidateCache(`/v1/specs/${draft_id}`);
2453
+ apiClient.invalidateCache("/v1/specs");
2049
2454
  return {
2050
2455
  content: [
2051
2456
  {
2052
2457
  type: "text",
2053
- text: `Found ${templates.length} prompt template(s):
2054
-
2055
- ${text3}`
2458
+ text: `Updated spec "${spec.name}" \u2014 ${section} section modified.`
2056
2459
  }
2057
2460
  ]
2058
2461
  };
@@ -2060,38 +2463,62 @@ ${text3}`
2060
2463
  );
2061
2464
  }
2062
2465
 
2063
- // src/tools/create-prompt-template.ts
2064
- import { z as z13 } from "zod";
2065
- function registerCreatePromptTemplate(server, apiClient) {
2466
+ // src/tools/report-decision.ts
2467
+ import { z as z11 } from "zod";
2468
+ function registerReportDecision(server, apiClient) {
2066
2469
  server.registerTool(
2067
- "create_prompt_template",
2470
+ "report_decision",
2068
2471
  {
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.",
2472
+ title: "Report Decision",
2473
+ description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
2071
2474
  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")
2475
+ title: z11.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
2476
+ context: z11.string().describe("What prompted this decision \u2014 the problem or constraint"),
2477
+ decision: z11.string().describe("What was decided and why"),
2478
+ consequences: z11.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
2479
+ related_files: z11.array(z11.string()).optional().describe("File paths related to this decision")
2075
2480
  },
2076
2481
  annotations: {
2077
2482
  readOnlyHint: false
2078
2483
  }
2079
2484
  },
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");
2485
+ async ({ title, context, decision, consequences, related_files }) => {
2486
+ const adrTitle = `ADR: ${title}`;
2487
+ const created = await apiClient.post("/v1/specs", {
2488
+ name: adrTitle
2489
+ });
2490
+ const relatedSection = related_files && related_files.length > 0 ? `
2491
+
2492
+ ## Related Files
2493
+
2494
+ ${related_files.map((f) => `- \`${f}\``).join("\n")}` : "";
2495
+ const adrContent = `# ${adrTitle}
2496
+
2497
+ **Date:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
2498
+ **Status:** Draft (pending review)
2499
+
2500
+ ## Context
2501
+
2502
+ ${context}
2503
+
2504
+ ## Decision
2505
+
2506
+ ${decision}
2507
+
2508
+ ## Consequences
2509
+
2510
+ ${consequences}` + relatedSection;
2511
+ await apiClient.put(`/v1/specs/${created.draftId}`, {
2512
+ proposalContent: adrContent
2513
+ });
2514
+ apiClient.invalidateCache("/v1/specs");
2090
2515
  return {
2091
2516
  content: [
2092
2517
  {
2093
2518
  type: "text",
2094
- text: `Prompt template created: **${saved.name}** (id: ${saved.id})`
2519
+ text: `Architecture decision recorded: **${adrTitle}** (spec id: ${created.draftId})
2520
+
2521
+ Status: draft \u2014 review in SystemDox before publishing.`
2095
2522
  }
2096
2523
  ]
2097
2524
  };
@@ -2099,217 +2526,210 @@ function registerCreatePromptTemplate(server, apiClient) {
2099
2526
  );
2100
2527
  }
2101
2528
 
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;
2529
+ // src/tools/get-project-rules.ts
2530
+ import { z as z12 } from "zod";
2531
+ var CATEGORY_LABELS = {
2532
+ deployment: "Deployment",
2533
+ architecture: "Architecture",
2534
+ security: "Security",
2535
+ testing: "Testing",
2536
+ process: "Process",
2537
+ other: "Other"
2131
2538
  };
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);
2539
+ function registerGetProjectRules(server, apiClient) {
2540
+ server.registerTool(
2541
+ "get_project_rules",
2542
+ {
2543
+ title: "Get Project Rules",
2544
+ 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.",
2545
+ inputSchema: {
2546
+ category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
2547
+ }
2548
+ },
2549
+ async ({ category }) => {
2550
+ const rules = await apiClient.get(
2551
+ "/v1/standards/rules"
2552
+ );
2553
+ let filtered = rules;
2554
+ if (category) {
2555
+ filtered = filtered.filter(
2556
+ (r) => r.category.toLowerCase() === category.toLowerCase()
2557
+ );
2558
+ }
2559
+ if (filtered.length === 0) {
2560
+ 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}"` : ""}.`;
2561
+ return {
2562
+ content: [{ type: "text", text: text4 }]
2563
+ };
2564
+ }
2565
+ const grouped = /* @__PURE__ */ new Map();
2566
+ for (const rule of filtered) {
2567
+ const cat = rule.category || "other";
2568
+ if (!grouped.has(cat)) grouped.set(cat, []);
2569
+ grouped.get(cat).push(rule);
2570
+ }
2571
+ let text3 = `Found ${filtered.length} project rule(s):
2572
+
2573
+ `;
2574
+ for (const [cat, catRules] of grouped) {
2575
+ const label = CATEGORY_LABELS[cat] ?? cat;
2576
+ text3 += `## ${label}
2577
+
2578
+ `;
2579
+ for (const rule of catRules) {
2580
+ text3 += `- ${rule.text}
2581
+ `;
2582
+ }
2583
+ text3 += "\n";
2584
+ }
2585
+ return {
2586
+ content: [
2587
+ {
2588
+ type: "text",
2589
+ text: text3
2590
+ }
2591
+ ]
2592
+ };
2246
2593
  }
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;
2594
+ );
2595
+ }
2596
+
2597
+ // src/tools/create-project-rule.ts
2598
+ import { z as z13 } from "zod";
2599
+ function registerCreateProjectRule(server, apiClient) {
2600
+ server.registerTool(
2601
+ "create_project_rule",
2602
+ {
2603
+ title: "Create Project Rule",
2604
+ 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.",
2605
+ inputSchema: {
2606
+ text: z13.string().describe(
2607
+ "The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
2608
+ ),
2609
+ category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
2610
+ },
2611
+ annotations: {
2612
+ readOnlyHint: false
2613
+ }
2614
+ },
2615
+ async ({ text: text3, category }) => {
2616
+ const saved = await apiClient.post(
2617
+ "/v1/standards/rules",
2618
+ { text: text3, category }
2619
+ );
2620
+ apiClient.invalidateCache("/v1/standards/rules");
2621
+ return {
2622
+ content: [
2623
+ {
2624
+ type: "text",
2625
+ text: `Project rule created: **${saved.text}** (id: ${saved.id}, category: ${saved.category})`
2626
+ }
2627
+ ]
2628
+ };
2264
2629
  }
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
- };
2630
+ );
2283
2631
  }
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));
2632
+
2633
+ // src/tools/get-prompt-templates.ts
2634
+ function registerGetPromptTemplates(server, apiClient) {
2635
+ server.registerTool(
2636
+ "get_prompt_templates",
2637
+ {
2638
+ title: "Get Prompt Templates",
2639
+ 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.",
2640
+ inputSchema: {}
2641
+ },
2642
+ async () => {
2643
+ const templates = await apiClient.get(
2644
+ "/v1/standards/prompt-templates"
2645
+ );
2646
+ if (templates.length === 0) {
2647
+ return {
2648
+ content: [
2649
+ {
2650
+ type: "text",
2651
+ text: `No prompt templates found. Add templates at ${configLink("/docs/prompts", await workspaceBase(apiClient))}`
2652
+ }
2653
+ ]
2654
+ };
2655
+ }
2656
+ const text3 = templates.map(
2657
+ (t) => `## ${t.name}
2658
+
2659
+ ` + (t.description ? `*${t.description}*
2660
+
2661
+ ` : "") + `\`\`\`
2662
+ ${t.prompt}
2663
+ \`\`\``
2664
+ ).join("\n\n---\n\n");
2665
+ return {
2666
+ content: [
2667
+ {
2668
+ type: "text",
2669
+ text: `Found ${templates.length} prompt template(s):
2670
+
2671
+ ${text3}`
2672
+ }
2673
+ ]
2674
+ };
2675
+ }
2676
+ );
2287
2677
  }
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");
2678
+
2679
+ // src/tools/create-prompt-template.ts
2680
+ import { z as z14 } from "zod";
2681
+ function registerCreatePromptTemplate(server, apiClient) {
2682
+ server.registerTool(
2683
+ "create_prompt_template",
2684
+ {
2685
+ title: "Create Prompt Template",
2686
+ 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.",
2687
+ inputSchema: {
2688
+ name: z14.string().describe("Short name for the template"),
2689
+ description: z14.string().optional().describe("Brief description of when to use this template"),
2690
+ prompt: z14.string().describe("The full prompt text that Gen AI will use when generating content")
2691
+ },
2692
+ annotations: {
2693
+ readOnlyHint: false
2694
+ }
2695
+ },
2696
+ async ({ name, description, prompt }) => {
2697
+ const saved = await apiClient.post(
2698
+ "/v1/standards/prompt-templates",
2699
+ {
2700
+ name,
2701
+ description: description ?? "",
2702
+ prompt
2703
+ }
2704
+ );
2705
+ apiClient.invalidateCache("/v1/standards/prompt-templates");
2706
+ return {
2707
+ content: [
2708
+ {
2709
+ type: "text",
2710
+ text: `Prompt template created: **${saved.name}** (id: ${saved.id})`
2711
+ }
2712
+ ]
2713
+ };
2714
+ }
2715
+ );
2299
2716
  }
2300
2717
 
2301
2718
  // src/tools/skills.ts
2719
+ import { z as z15 } from "zod";
2720
+ import { homedir as homedir3 } from "os";
2721
+ import { basename, resolve as resolve2 } from "path";
2302
2722
  var SKILLS = "/v1/standards/skills";
2303
2723
  function expandHome(path) {
2304
- return path.replace(/^~(?=$|[\\/])/, homedir2());
2724
+ return path.replace(/^~(?=$|[\\/])/, homedir3());
2305
2725
  }
2306
2726
  function text(t) {
2307
2727
  return { content: [{ type: "text", text: t }] };
2308
2728
  }
2309
2729
  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")
2730
+ const fileSchema = z15.object({
2731
+ path: z15.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
2732
+ content: z15.string().describe("The file's full text, exactly as it is")
2313
2733
  });
2314
2734
  server.registerTool(
2315
2735
  "publish_skill",
@@ -2318,13 +2738,13 @@ function registerSkillTools(server, apiClient, { localFiles }) {
2318
2738
  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
2739
  inputSchema: {
2320
2740
  ...localFiles ? {
2321
- directory: z14.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2741
+ directory: z15.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2322
2742
  } : {},
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(
2743
+ files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
2744
+ name: z15.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
2745
+ spaceId: z15.string().optional().describe("Space for a NEW skill (default: your default space)"),
2746
+ 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")`),
2747
+ baseVersion: z15.number().int().positive().optional().describe(
2328
2748
  "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
2749
  )
2330
2750
  },
@@ -2390,10 +2810,10 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2390
2810
  title: "Install Skill",
2391
2811
  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
2812
  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") } : {}
2813
+ name: z15.string().describe("The skill's name"),
2814
+ version: z15.number().int().positive().optional().describe("A version number (default: the latest)"),
2815
+ target: z15.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
2816
+ ...localFiles ? { overwrite: z15.boolean().optional().describe("Replace files edited since they were installed") } : {}
2397
2817
  },
2398
2818
  annotations: { readOnlyHint: !localFiles }
2399
2819
  },
@@ -2424,7 +2844,7 @@ ${fence}
2424
2844
  ${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
2425
2845
  }).join("\n\n");
2426
2846
  return text(
2427
- `Skill **${version.name}** version ${version.version}: write these ${version.files.length} file(s) into ${dest}/ exactly as shown (the content between the fences), creating folders as needed. If a file there differs from ${MANIFEST_FILE}'s hash for it, it was edited: ask before replacing it. Then write ${MANIFEST_FILE} with this content, and tell the user to start a new Claude Code session.
2847
+ `Skill **${version.name}** version ${version.version}: write these ${version.files.length} file(s) into ${dest}/ exactly as shown (the content between the fences), creating folders as needed. If a file there differs from ${MANIFEST_FILE}'s hash for it, it was edited: ask before replacing it. Then write ${MANIFEST_FILE} with this content. ${afterInstall(`${dest}/SKILL.md`)}
2428
2848
 
2429
2849
  ### ${MANIFEST_FILE}
2430
2850
 
@@ -2439,11 +2859,11 @@ ${body}`
2439
2859
  }
2440
2860
 
2441
2861
  // src/tools/team-setup.ts
2442
- import { z as z15 } from "zod";
2443
- import { existsSync as existsSync3 } from "fs";
2862
+ import { z as z16 } from "zod";
2863
+ import { existsSync as existsSync4 } from "fs";
2444
2864
  import { lstat as lstat2 } from "fs/promises";
2445
2865
  import { homedir as homedir4 } from "os";
2446
- import { join as join4, resolve as resolve4 } from "path";
2866
+ import { join as join5, resolve as resolve4 } from "path";
2447
2867
 
2448
2868
  // src/lib/person-values.ts
2449
2869
  var PHONE = "phone";
@@ -2524,9 +2944,9 @@ var PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
2524
2944
  var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2525
2945
 
2526
2946
  // 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";
2947
+ import { existsSync as existsSync3 } from "fs";
2948
+ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
2949
+ import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
2530
2950
 
2531
2951
  // src/lib/memory-sync.ts
2532
2952
  import { createHash as createHash2 } from "crypto";
@@ -2797,63 +3217,6 @@ function lineDiff(before, after) {
2797
3217
  return out;
2798
3218
  }
2799
3219
 
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
3220
  // src/sync-memory.ts
2858
3221
  var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
2859
3222
  var SyncFailed = class extends Error {
@@ -2863,7 +3226,7 @@ function refusal(e) {
2863
3226
  const { status, code } = e;
2864
3227
  return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
2865
3228
  }
2866
- var toPath = (root, rel) => join3(root, ...rel.split("/"));
3229
+ var toPath = (root, rel) => join4(root, ...rel.split("/"));
2867
3230
  async function readFolder(root) {
2868
3231
  const files = {};
2869
3232
  const names = /* @__PURE__ */ new Set();
@@ -2871,14 +3234,14 @@ async function readFolder(root) {
2871
3234
  for (const e of await readdir2(dir, { withFileTypes: true })) {
2872
3235
  if (e.name.startsWith(".")) continue;
2873
3236
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
2874
- if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
3237
+ if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
2875
3238
  else if (e.isFile()) {
2876
3239
  names.add(rel.toLowerCase());
2877
- if (e.name.endsWith(".md")) files[rel] = await readFile3(join3(dir, e.name), "utf8");
3240
+ if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
2878
3241
  }
2879
3242
  }
2880
3243
  }
2881
- if (existsSync2(root)) await walk2(root, "");
3244
+ if (existsSync3(root)) await walk2(root, "");
2882
3245
  return { files, names };
2883
3246
  }
2884
3247
  async function pullTeam(client, ws, project) {
@@ -3087,11 +3450,11 @@ async function people(opts, ws, files, report, hint) {
3087
3450
  report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
3088
3451
  break;
3089
3452
  }
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.`);
3453
+ const refused2 = result.refused[0];
3454
+ if (refused2?.code === "SYD-K021") {
3455
+ out(` ${refused2.detail}. Add them in Staff first, or answer p to file them.`);
3093
3456
  choices = choices.filter((c) => c !== "m");
3094
- } else if (refused) out(` ${refused.detail}`);
3457
+ } else if (refused2) out(` ${refused2.detail}`);
3095
3458
  }
3096
3459
  }
3097
3460
  for (let i = 0; i < filed.length; i += 50) {
@@ -3235,7 +3598,7 @@ async function syncMemory(opts) {
3235
3598
  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
3599
  );
3237
3600
  }
3238
- const dir = resolve3(opts.dir ?? join3(workspaceDir(ws), "memory", project));
3601
+ const dir = resolve3(opts.dir ?? join4(workspaceDir(ws), "memory", project));
3239
3602
  const run2 = { ...opts, dir };
3240
3603
  const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
3241
3604
  const report = new Report();
@@ -3385,21 +3748,21 @@ async function readPartDirectory(part, directory) {
3385
3748
  let present = true;
3386
3749
  for (let i = 1; i <= segments.length; i++) {
3387
3750
  const rel = segments.slice(0, i).join("/");
3388
- if (await isLink(join4(root, ...segments.slice(0, i)))) {
3751
+ if (await isLink(join5(root, ...segments.slice(0, i)))) {
3389
3752
  links.push(rel);
3390
3753
  present = false;
3391
3754
  break;
3392
3755
  }
3393
3756
  }
3394
- if (present && existsSync3(join4(root, ...segments))) paths.push(path);
3757
+ if (present && existsSync4(join5(root, ...segments))) paths.push(path);
3395
3758
  }
3396
3759
  for (const { dir, md } of layout.folders) {
3397
- const full = join4(root, dir);
3760
+ const full = join5(root, dir);
3398
3761
  if (await isLink(full)) {
3399
3762
  links.push(dir);
3400
3763
  continue;
3401
3764
  }
3402
- if (!existsSync3(full)) continue;
3765
+ if (!existsSync4(full)) continue;
3403
3766
  const walked = await walk(full, { skipHidden: true });
3404
3767
  links.push(...walked.links.map((p) => `${dir}/${p}`));
3405
3768
  leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
@@ -3439,9 +3802,9 @@ function describePerson(id, person) {
3439
3802
  return lines.join("\n");
3440
3803
  }
3441
3804
  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")
3805
+ const fileSchema = z16.object({
3806
+ path: z16.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
3807
+ content: z16.string().describe("The file's full text, exactly as it is")
3445
3808
  });
3446
3809
  server.registerTool(
3447
3810
  "publish_setup_part",
@@ -3449,13 +3812,13 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3449
3812
  title: "Publish Setup Part",
3450
3813
  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
3814
  inputSchema: {
3452
- part: z15.enum(PARTS2).describe("Which part of the setup"),
3815
+ part: z16.enum(PARTS2).describe("Which part of the setup"),
3453
3816
  ...localFiles ? {
3454
- directory: z15.string().optional().describe(
3817
+ directory: z16.string().optional().describe(
3455
3818
  "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
3819
  )
3457
3820
  } : {},
3458
- files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3821
+ files: z16.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3459
3822
  },
3460
3823
  annotations: { readOnlyHint: false }
3461
3824
  },
@@ -3491,7 +3854,7 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3491
3854
  title: "Resolve Person",
3492
3855
  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
3856
  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")
3857
+ 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
3858
  },
3496
3859
  annotations: { readOnlyHint: true }
3497
3860
  },
@@ -3508,8 +3871,8 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3508
3871
  title: "Sync Memory",
3509
3872
  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
3873
  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")
3874
+ 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"),
3875
+ dryRun: z16.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
3513
3876
  },
3514
3877
  annotations: { readOnlyHint: false }
3515
3878
  },
@@ -3527,12 +3890,15 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3527
3890
  }
3528
3891
 
3529
3892
  // 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
- });
3893
+ var SERVER_VERSION = true ? "0.1.51" : "0.0.0-dev";
3894
+ function createServer(apiClient, { localFiles = false, instructions, keyProblem } = {}) {
3895
+ const server = new McpServer(
3896
+ {
3897
+ name: "systemdox-mcp-server",
3898
+ version: SERVER_VERSION
3899
+ },
3900
+ instructions ? { instructions } : void 0
3901
+ );
3536
3902
  registerGetGuardrails(server, apiClient);
3537
3903
  registerGetSpecs(server, apiClient);
3538
3904
  registerGetDocs(server, apiClient);
@@ -3543,6 +3909,8 @@ function createServer(apiClient, { localFiles = false } = {}) {
3543
3909
  registerGetContext(server, apiClient);
3544
3910
  registerGetProjectRules(server, apiClient);
3545
3911
  registerGetPromptTemplates(server, apiClient);
3912
+ registerGetTeamSetup(server, apiClient, { localFiles, keyProblem });
3913
+ registerDocumentGraph(server, apiClient);
3546
3914
  registerCreateGuardrail(server, apiClient);
3547
3915
  registerUpdateSpec(server, apiClient);
3548
3916
  registerReportDecision(server, apiClient);
@@ -3562,6 +3930,14 @@ function lowercaseHeaders(headers) {
3562
3930
  }
3563
3931
  return out;
3564
3932
  }
3933
+ function opensSession(rawBody) {
3934
+ try {
3935
+ const parsed = JSON.parse(rawBody);
3936
+ return (Array.isArray(parsed) ? parsed : [parsed]).some((m) => m?.method === "initialize");
3937
+ } catch {
3938
+ return false;
3939
+ }
3940
+ }
3565
3941
  function jsonError(statusCode, message) {
3566
3942
  return {
3567
3943
  statusCode,
@@ -3615,7 +3991,8 @@ async function handler(event) {
3615
3991
  // per-request client; never cache across invocations
3616
3992
  defaultOrg: void 0
3617
3993
  });
3618
- const server = createServer(apiClient);
3994
+ const instructions = opensSession(rawBody) ? await sessionInstructions(apiClient) : void 0;
3995
+ const server = createServer(apiClient, { instructions });
3619
3996
  const transport = new WebStandardStreamableHTTPServerTransport({
3620
3997
  sessionIdGenerator: void 0,
3621
3998
  // stateless — every POST is independent