@systemdox/mcp-server 0.1.48 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,42 +3,6 @@
3
3
  // src/index.ts
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
 
6
- // src/lib/config.ts
7
- var API_URL_DEFAULT = "https://api.puglieseweb.com/systemdox";
8
- function requireHttpsApiUrl(apiUrl) {
9
- let parsed;
10
- try {
11
- parsed = new URL(apiUrl);
12
- } catch {
13
- throw new Error(
14
- `SYSTEMDOX_API_URL is not a valid URL: ${apiUrl}`
15
- );
16
- }
17
- if (parsed.protocol !== "https:") {
18
- throw new Error(
19
- `SYSTEMDOX_API_URL must use https:// \u2014 refusing to send the API key over ${parsed.protocol}// in cleartext. Got: ${apiUrl}`
20
- );
21
- }
22
- return apiUrl;
23
- }
24
- function loadConfig() {
25
- const apiKey = process.env.SYSTEMDOX_API_KEY;
26
- if (!apiKey) {
27
- throw new Error(
28
- "SYSTEMDOX_API_KEY is required. Generate one at your SystemDox settings page."
29
- );
30
- }
31
- const apiUrl = requireHttpsApiUrl(
32
- process.env.SYSTEMDOX_API_URL ?? API_URL_DEFAULT
33
- );
34
- return {
35
- apiKey,
36
- apiUrl,
37
- cacheTtlMs: parseInt(process.env.SYSTEMDOX_CACHE_TTL ?? "300", 10) * 1e3,
38
- defaultOrg: process.env.SYSTEMDOX_DEFAULT_ORG
39
- };
40
- }
41
-
42
6
  // src/lib/cache.ts
43
7
  var ResponseCache = class {
44
8
  store = /* @__PURE__ */ new Map();
@@ -268,6 +232,131 @@ var SystemDoxApiClient = class {
268
232
  }
269
233
  };
270
234
 
235
+ // src/lib/team-setup-files.ts
236
+ import { execFile } from "child_process";
237
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
238
+ import { homedir, userInfo } from "os";
239
+ import { dirname, join } from "path";
240
+ import { promisify } from "util";
241
+
242
+ // src/lib/config.ts
243
+ var API_URL_DEFAULT = "https://api.puglieseweb.com/systemdox";
244
+ function requireHttpsApiUrl(apiUrl) {
245
+ let parsed;
246
+ try {
247
+ parsed = new URL(apiUrl);
248
+ } catch {
249
+ throw new Error(
250
+ `SYSTEMDOX_API_URL is not a valid URL: ${apiUrl}`
251
+ );
252
+ }
253
+ if (parsed.protocol !== "https:") {
254
+ throw new Error(
255
+ `SYSTEMDOX_API_URL must use https:// \u2014 refusing to send the API key over ${parsed.protocol}// in cleartext. Got: ${apiUrl}`
256
+ );
257
+ }
258
+ return apiUrl;
259
+ }
260
+ function loadConfig(apiKey = process.env.SYSTEMDOX_API_KEY) {
261
+ if (!apiKey) {
262
+ throw new Error(
263
+ "SYSTEMDOX_API_KEY is required. Generate one at your SystemDox settings page."
264
+ );
265
+ }
266
+ const apiUrl = requireHttpsApiUrl(
267
+ process.env.SYSTEMDOX_API_URL ?? API_URL_DEFAULT
268
+ );
269
+ return {
270
+ apiKey,
271
+ apiUrl,
272
+ cacheTtlMs: parseInt(process.env.SYSTEMDOX_CACHE_TTL ?? "300", 10) * 1e3,
273
+ defaultOrg: process.env.SYSTEMDOX_DEFAULT_ORG
274
+ };
275
+ }
276
+
277
+ // src/lib/team-setup-files.ts
278
+ var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
279
+ function isWorkspace(value) {
280
+ return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
281
+ }
282
+ function homeDir() {
283
+ return homedir();
284
+ }
285
+ function systemdoxDir() {
286
+ return join(homeDir(), ".claude", "systemdox");
287
+ }
288
+ function credentialsPath() {
289
+ return join(systemdoxDir(), "credentials.json");
290
+ }
291
+ function workspaceDir(workspace) {
292
+ if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
293
+ return join(systemdoxDir(), workspace);
294
+ }
295
+ function statePath(workspace) {
296
+ return join(workspaceDir(workspace), "state.json");
297
+ }
298
+ async function readJson(path, empty) {
299
+ let text3;
300
+ try {
301
+ text3 = await readFile(path, "utf8");
302
+ } catch (e) {
303
+ if (e.code === "ENOENT") return empty;
304
+ throw e;
305
+ }
306
+ try {
307
+ return JSON.parse(text3);
308
+ } catch {
309
+ throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
310
+ }
311
+ }
312
+ async function writeJsonAtomically(path, value, mode) {
313
+ await mkdir(dirname(path), { recursive: true });
314
+ const tmp = `${path}.${process.pid}.tmp`;
315
+ await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
316
+ await rename(tmp, path);
317
+ }
318
+ async function readCredentials() {
319
+ return readJson(credentialsPath(), {});
320
+ }
321
+ var run = promisify(execFile);
322
+ async function localApiKey(env = process.env) {
323
+ if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
324
+ const keys = Object.entries(await readCredentials()).filter(
325
+ (e) => typeof e[1]?.apiKey === "string" && e[1].apiKey.length > 0
326
+ );
327
+ const wanted = env.SYSTEMDOX_WORKSPACE;
328
+ if (wanted) {
329
+ const key = keys.find(([ws]) => ws === wanted)?.[1].apiKey;
330
+ if (key) return key;
331
+ throw new Error(
332
+ `~/.claude/systemdox/credentials.json holds no key for ${wanted} (SYSTEMDOX_WORKSPACE). Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.`
333
+ );
334
+ }
335
+ if (keys.length === 1) return keys[0][1].apiKey;
336
+ if (keys.length > 1) {
337
+ throw new Error(
338
+ `~/.claude/systemdox/credentials.json holds keys for ${keys.map(([ws]) => ws).join(", ")}: set SYSTEMDOX_WORKSPACE to the workspace to use, or SYSTEMDOX_API_KEY to its key.`
339
+ );
340
+ }
341
+ throw new Error("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.");
342
+ }
343
+ async function loadLocalConfig(env = process.env) {
344
+ const apiKey = await localApiKey(env);
345
+ return loadConfig(apiKey);
346
+ }
347
+ async function readState(workspace) {
348
+ return readJson(statePath(workspace), {});
349
+ }
350
+ async function writeState(workspace, state) {
351
+ await writeJsonAtomically(statePath(workspace), state);
352
+ }
353
+ async function updateState(workspace, change) {
354
+ const state = await readState(workspace);
355
+ change(state);
356
+ await writeState(workspace, state);
357
+ return state;
358
+ }
359
+
271
360
  // src/server.ts
272
361
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
273
362
 
@@ -950,8 +1039,149 @@ ${notes.join("\n")}` : "")
950
1039
  );
951
1040
  }
952
1041
 
953
- // src/tools/get-context.ts
1042
+ // src/tools/get-change.ts
954
1043
  import { z as z6 } from "zod";
1044
+ var GITHUB_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,99}";
1045
+ var REPO_RE = new RegExp(`^${GITHUB_NAME}/${GITHUB_NAME}$`);
1046
+ var SLUG_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
1047
+ var SLUG_MAX = 64;
1048
+ var PARTS = ["decision", "contract", "behaviour", "plan", "release"];
1049
+ var BRIEF = ["decision", "contract", "behaviour", "plan"];
1050
+ var PAST_REVIEW = /* @__PURE__ */ new Set(["passed", "being built", "done"]);
1051
+ function briefVerdict(states) {
1052
+ const waiting = BRIEF.filter(
1053
+ (part) => !(part === "decision" && states.decision === null) && !PAST_REVIEW.has(String(states[part]))
1054
+ ).map((part) => `${part} is ${states[part] ?? "unknown"}`);
1055
+ return waiting.length === 0 ? "The brief is passed" : `The brief is not passed: ${waiting.join(", ")}`;
1056
+ }
1057
+ function prLink(pr) {
1058
+ return `[${pr.repo}#${pr.number}](${pr.url}) ${pr.state}`;
1059
+ }
1060
+ function fileSource(file) {
1061
+ if (file.pullRequest) return prLink(file.pullRequest);
1062
+ return file.direct ? "committed directly" : "\u2014";
1063
+ }
1064
+ function partPullRequests(change, part) {
1065
+ const files = part === "contract" ? change.contract.files : part === "behaviour" ? change.behaviour.files : part === "plan" ? [change.plan.file] : [];
1066
+ const prs = part === "decision" ? [change.decision?.pullRequest ?? null] : part === "release" ? change.release.prs : files.map((file) => file?.pullRequest ?? null);
1067
+ const unique = /* @__PURE__ */ new Map();
1068
+ for (const pr of prs) {
1069
+ if (pr && !unique.has(`${pr.repo}#${pr.number}`)) unique.set(`${pr.repo}#${pr.number}`, pr);
1070
+ }
1071
+ return [...unique.values()].sort((a, b) => Number(b.state === "open") - Number(a.state === "open"));
1072
+ }
1073
+ function countsLine(counts) {
1074
+ const tags = ["proposed", "built", "defect", "unbuilt", "open", "untagged"].filter((tag) => counts[tag] > 0).map((tag) => `${counts[tag]} ${tag}`);
1075
+ return `${counts.total} scenario(s)` + (tags.length > 0 ? `: ${tags.join(", ")}` : "");
1076
+ }
1077
+ function partsTable(change) {
1078
+ const rows = PARTS.filter((part) => !(part === "decision" && change.states.decision === null)).map((part) => {
1079
+ const prs = partPullRequests(change, part);
1080
+ return `| ${part} | ${change.states[part]} | ${prs.length > 0 ? prs.map(prLink).join(", ") : "\u2014"} |`;
1081
+ });
1082
+ return ["| Part | State | Pull request |", "|---|---|---|", ...rows].join("\n");
1083
+ }
1084
+ function decisionSection(change) {
1085
+ const decision = change.decision;
1086
+ if (decision === null) return "## Decision\n\nNone: the proposal names no ADR (a small change).";
1087
+ const status = decision.status || "no record in a connected repository";
1088
+ return `## Decision
1089
+
1090
+ ${decision.adr} \xB7 ${status}${decision.title ? ` \u2014 ${decision.title}` : ""}`;
1091
+ }
1092
+ function contractSection(change) {
1093
+ const files = change.contract.files;
1094
+ return "## Contract\n\n" + (files.length > 0 ? files.map((file) => `- ${file.path} \u2014 ${fileSource(file)}`).join("\n") : "No contract file yet (openapi.yaml, asyncapi.yaml or contract.md).");
1095
+ }
1096
+ function behaviourSection(change) {
1097
+ const { files, counts } = change.behaviour;
1098
+ if (files.length === 0) {
1099
+ return `## Behaviour
1100
+
1101
+ No feature file names this change. A feature file joins it when its path or its first ten lines name \`${change.slug}\`${change.decision ? ` or ${change.decision.adr}` : ""}.`;
1102
+ }
1103
+ const blocks = files.map(
1104
+ (file) => [
1105
+ `### ${file.path}`,
1106
+ "",
1107
+ [file.title, file.runner ? `runner ${file.runner}` : "", fileSource(file), countsLine(file.counts)].filter(Boolean).join(" \xB7 "),
1108
+ "",
1109
+ ...file.scenarios.map((s) => `- ${s.id || "(no id)"} \xB7 ${s.state || "untagged"} \xB7 ${s.title}`)
1110
+ ].join("\n")
1111
+ );
1112
+ return ["## Behaviour", countsLine(counts), ...blocks].join("\n\n");
1113
+ }
1114
+ function planSection(change) {
1115
+ const { file, draftId, board } = change.plan;
1116
+ const boardLine = board ? `[#${board.number} ${board.title}](${board.url}) \u2014 todo ${board.items.todo} \xB7 in progress ${board.items.inProgress} \xB7 done ${board.items.done}` + (board.items.truncated ? " (counted over its first 500 items)" : "") : "none";
1117
+ return [
1118
+ "## Plan",
1119
+ "",
1120
+ `- plan.md: ${file ? `${file.path} \u2014 ${fileSource(file)}` : "none"}`,
1121
+ `- Draft: ${draftId ?? "none"}`,
1122
+ `- Board: ${boardLine}`
1123
+ ].join("\n");
1124
+ }
1125
+ function releaseSection(change) {
1126
+ const lines = change.release.prs.map(
1127
+ (pr) => `- ${prLink(pr)} \xB7 ${pr.title} \xB7 ` + (pr.approvingReview ? `approved by ${pr.approvingReview.by} at ${pr.approvingReview.at}` : "no approving review on its head yet")
1128
+ );
1129
+ return `## Release
1130
+
1131
+ ${lines.length > 0 ? lines.join("\n") : "No release pull request is open."}`;
1132
+ }
1133
+ function formatChange(change) {
1134
+ const header = `# ${change.title}
1135
+
1136
+ \`${change.key}\` \xB7 size: ${change.size}` + (change.archived ? " \xB7 archived: read-only, done" : "") + ` \xB7 synced ${change.syncedAt}`;
1137
+ return [
1138
+ header,
1139
+ partsTable(change),
1140
+ decisionSection(change),
1141
+ contractSection(change),
1142
+ behaviourSection(change),
1143
+ planSection(change),
1144
+ releaseSection(change),
1145
+ // Compact: a change's every scenario with its steps rides in this block.
1146
+ "```json\n" + JSON.stringify(change) + "\n```",
1147
+ briefVerdict(change.states)
1148
+ ].join("\n\n");
1149
+ }
1150
+ function registerGetChange(server2, apiClient2) {
1151
+ server2.registerTool(
1152
+ "get_change",
1153
+ {
1154
+ title: "Get Change",
1155
+ 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.`,
1156
+ inputSchema: {
1157
+ 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"),
1158
+ 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>/")
1159
+ },
1160
+ annotations: { readOnlyHint: true }
1161
+ },
1162
+ async ({ repo, slug }) => {
1163
+ const [owner, name] = repo.split("/");
1164
+ let change;
1165
+ try {
1166
+ change = await apiClient2.getFresh(
1167
+ `/v1/changes/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/${encodeURIComponent(slug)}`
1168
+ );
1169
+ } catch (err) {
1170
+ if (err.status === 404) {
1171
+ return {
1172
+ content: [{ type: "text", text: `No such change ${repo}/${slug}` }],
1173
+ isError: true
1174
+ };
1175
+ }
1176
+ throw err;
1177
+ }
1178
+ return { content: [{ type: "text", text: formatChange(change) }] };
1179
+ }
1180
+ );
1181
+ }
1182
+
1183
+ // src/tools/get-context.ts
1184
+ import { z as z7 } from "zod";
955
1185
 
956
1186
  // src/lib/scoring.ts
957
1187
  var WEIGHT_PATH = 0.4;
@@ -1518,13 +1748,13 @@ function registerGetContext(server2, apiClient2) {
1518
1748
  title: "Get Context",
1519
1749
  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.",
1520
1750
  inputSchema: {
1521
- task_description: z6.string().describe(
1751
+ task_description: z7.string().describe(
1522
1752
  "What you're working on (e.g., 'add a new REST endpoint for user preferences')"
1523
1753
  ),
1524
- file_paths: z6.array(z6.string()).optional().describe(
1754
+ file_paths: z7.array(z7.string()).optional().describe(
1525
1755
  "File paths being edited \u2014 used for language/category detection"
1526
1756
  ),
1527
- max_tokens: z6.number().optional().describe("Maximum token budget for the response (default: 4000)")
1757
+ max_tokens: z7.number().optional().describe("Maximum token budget for the response (default: 4000)")
1528
1758
  }
1529
1759
  },
1530
1760
  async ({ task_description, file_paths, max_tokens }) => {
@@ -1541,7 +1771,7 @@ function registerGetContext(server2, apiClient2) {
1541
1771
  }
1542
1772
 
1543
1773
  // src/tools/create-guardrail.ts
1544
- import { z as z7 } from "zod";
1774
+ import { z as z8 } from "zod";
1545
1775
  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).";
1546
1776
  var CATEGORIES = [
1547
1777
  "imports",
@@ -1554,13 +1784,13 @@ var CATEGORIES = [
1554
1784
  "data-access"
1555
1785
  ];
1556
1786
  var inputSchema2 = () => ({
1557
- title: z7.string().describe("Short title for the check"),
1558
- description: z7.string().describe("Detailed description of what the check enforces and why"),
1559
- severity: z7.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
1560
- category: z7.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
1561
- pattern: z7.string().optional().describe("Bad code pattern that should be avoided"),
1562
- fix: z7.string().optional().describe("Correct code pattern to use instead"),
1563
- fitness_test: z7.string().optional().describe("Shell command to detect violations in CI")
1787
+ title: z8.string().describe("Short title for the check"),
1788
+ description: z8.string().describe("Detailed description of what the check enforces and why"),
1789
+ severity: z8.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
1790
+ category: z8.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
1791
+ pattern: z8.string().optional().describe("Bad code pattern that should be avoided"),
1792
+ fix: z8.string().optional().describe("Correct code pattern to use instead"),
1793
+ fitness_test: z8.string().optional().describe("Shell command to detect violations in CI")
1564
1794
  });
1565
1795
  function registerCreateGuardrail(server2, apiClient2) {
1566
1796
  const handler = async ({
@@ -1626,7 +1856,7 @@ function registerCreateGuardrail(server2, apiClient2) {
1626
1856
  }
1627
1857
 
1628
1858
  // src/tools/update-spec.ts
1629
- import { z as z8 } from "zod";
1859
+ import { z as z9 } from "zod";
1630
1860
  function registerUpdateSpec(server2, apiClient2) {
1631
1861
  server2.registerTool(
1632
1862
  "update_spec",
@@ -1634,12 +1864,12 @@ function registerUpdateSpec(server2, apiClient2) {
1634
1864
  title: "Update Spec",
1635
1865
  description: "Update an existing SystemDox spec's proposal, tasks, or requirements. Only draft specs can be modified \u2014 published specs must be versioned first.",
1636
1866
  inputSchema: {
1637
- draft_id: z8.string().describe("ID of the spec to update"),
1638
- section: z8.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
1639
- content: z8.string().describe(
1867
+ draft_id: z9.string().describe("ID of the spec to update"),
1868
+ section: z9.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
1869
+ content: z9.string().describe(
1640
1870
  "New markdown content for the section. For requirements, provide the full requirement content."
1641
1871
  ),
1642
- requirement_name: z8.string().optional().describe(
1872
+ requirement_name: z9.string().optional().describe(
1643
1873
  "Name of the requirement (required when section is 'requirements')"
1644
1874
  )
1645
1875
  },
@@ -1689,7 +1919,7 @@ function registerUpdateSpec(server2, apiClient2) {
1689
1919
  }
1690
1920
 
1691
1921
  // src/tools/report-decision.ts
1692
- import { z as z9 } from "zod";
1922
+ import { z as z10 } from "zod";
1693
1923
  function registerReportDecision(server2, apiClient2) {
1694
1924
  server2.registerTool(
1695
1925
  "report_decision",
@@ -1697,11 +1927,11 @@ function registerReportDecision(server2, apiClient2) {
1697
1927
  title: "Report Decision",
1698
1928
  description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
1699
1929
  inputSchema: {
1700
- title: z9.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
1701
- context: z9.string().describe("What prompted this decision \u2014 the problem or constraint"),
1702
- decision: z9.string().describe("What was decided and why"),
1703
- consequences: z9.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
1704
- related_files: z9.array(z9.string()).optional().describe("File paths related to this decision")
1930
+ title: z10.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
1931
+ context: z10.string().describe("What prompted this decision \u2014 the problem or constraint"),
1932
+ decision: z10.string().describe("What was decided and why"),
1933
+ consequences: z10.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
1934
+ related_files: z10.array(z10.string()).optional().describe("File paths related to this decision")
1705
1935
  },
1706
1936
  annotations: {
1707
1937
  readOnlyHint: false
@@ -1752,7 +1982,7 @@ Status: draft \u2014 review in SystemDox before publishing.`
1752
1982
  }
1753
1983
 
1754
1984
  // src/tools/get-project-rules.ts
1755
- import { z as z10 } from "zod";
1985
+ import { z as z11 } from "zod";
1756
1986
  var CATEGORY_LABELS = {
1757
1987
  deployment: "Deployment",
1758
1988
  architecture: "Architecture",
@@ -1768,7 +1998,7 @@ function registerGetProjectRules(server2, apiClient2) {
1768
1998
  title: "Get Project Rules",
1769
1999
  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.",
1770
2000
  inputSchema: {
1771
- category: z10.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
2001
+ category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
1772
2002
  }
1773
2003
  },
1774
2004
  async ({ category }) => {
@@ -1820,7 +2050,7 @@ function registerGetProjectRules(server2, apiClient2) {
1820
2050
  }
1821
2051
 
1822
2052
  // src/tools/create-project-rule.ts
1823
- import { z as z11 } from "zod";
2053
+ import { z as z12 } from "zod";
1824
2054
  function registerCreateProjectRule(server2, apiClient2) {
1825
2055
  server2.registerTool(
1826
2056
  "create_project_rule",
@@ -1828,10 +2058,10 @@ function registerCreateProjectRule(server2, apiClient2) {
1828
2058
  title: "Create Project Rule",
1829
2059
  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.",
1830
2060
  inputSchema: {
1831
- text: z11.string().describe(
2061
+ text: z12.string().describe(
1832
2062
  "The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
1833
2063
  ),
1834
- category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
2064
+ category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
1835
2065
  },
1836
2066
  annotations: {
1837
2067
  readOnlyHint: false
@@ -1902,7 +2132,7 @@ ${text3}`
1902
2132
  }
1903
2133
 
1904
2134
  // src/tools/create-prompt-template.ts
1905
- import { z as z12 } from "zod";
2135
+ import { z as z13 } from "zod";
1906
2136
  function registerCreatePromptTemplate(server2, apiClient2) {
1907
2137
  server2.registerTool(
1908
2138
  "create_prompt_template",
@@ -1910,9 +2140,9 @@ function registerCreatePromptTemplate(server2, apiClient2) {
1910
2140
  title: "Create Prompt Template",
1911
2141
  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.",
1912
2142
  inputSchema: {
1913
- name: z12.string().describe("Short name for the template"),
1914
- description: z12.string().optional().describe("Brief description of when to use this template"),
1915
- prompt: z12.string().describe("The full prompt text that Gen AI will use when generating content")
2143
+ name: z13.string().describe("Short name for the template"),
2144
+ description: z13.string().optional().describe("Brief description of when to use this template"),
2145
+ prompt: z13.string().describe("The full prompt text that Gen AI will use when generating content")
1916
2146
  },
1917
2147
  annotations: {
1918
2148
  readOnlyHint: false
@@ -1941,16 +2171,16 @@ function registerCreatePromptTemplate(server2, apiClient2) {
1941
2171
  }
1942
2172
 
1943
2173
  // src/tools/skills.ts
1944
- import { z as z13 } from "zod";
1945
- import { homedir as homedir2 } from "os";
2174
+ import { z as z14 } from "zod";
2175
+ import { homedir as homedir3 } from "os";
1946
2176
  import { basename, resolve as resolve2 } from "path";
1947
2177
 
1948
2178
  // src/lib/skills.ts
1949
2179
  import { createHash } from "crypto";
1950
- import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
2180
+ import { lstat, mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1951
2181
  import { existsSync } from "fs";
1952
- import { homedir } from "os";
1953
- import { dirname, join, relative, resolve, sep } from "path";
2182
+ import { homedir as homedir2 } from "os";
2183
+ import { dirname as dirname2, join as join2, relative, resolve, sep } from "path";
1954
2184
  var MANIFEST_FILE = ".systemdox-skill.json";
1955
2185
  var ENTRY_FILE = "SKILL.md";
1956
2186
  var MAX_FILES = 100;
@@ -1986,12 +2216,12 @@ function frontmatterName(skillMd) {
1986
2216
  return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
1987
2217
  }
1988
2218
  function defaultSkillDir(name) {
1989
- return join(homedir(), ".claude", "skills", name);
2219
+ return join2(homedir2(), ".claude", "skills", name);
1990
2220
  }
1991
2221
  async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
1992
2222
  for (const entry of await readdir(dir, { withFileTypes: true })) {
1993
2223
  if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
1994
- const full = join(dir, entry.name);
2224
+ const full = join2(dir, entry.name);
1995
2225
  const rel = relative(root, full).split(sep).join("/");
1996
2226
  if (entry.isSymbolicLink()) out.links.push(rel);
1997
2227
  else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
@@ -2012,7 +2242,7 @@ async function readTextFiles(root, paths, what) {
2012
2242
  if (!isValidPath(path)) {
2013
2243
  throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
2014
2244
  }
2015
- const raw = await readFile(join(root, ...path.split("/")));
2245
+ const raw = await readFile2(join2(root, ...path.split("/")));
2016
2246
  if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
2017
2247
  if (raw.length > MAX_FILE_BYTES) {
2018
2248
  throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
@@ -2033,7 +2263,7 @@ async function readTextFiles(root, paths, what) {
2033
2263
  }
2034
2264
  async function readSkillDirectory(directory) {
2035
2265
  const root = resolve(directory);
2036
- if (!existsSync(join(root, ENTRY_FILE))) {
2266
+ if (!existsSync(join2(root, ENTRY_FILE))) {
2037
2267
  throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
2038
2268
  }
2039
2269
  const walked = await walk(root, { skipHidden: true });
@@ -2054,7 +2284,7 @@ function within(dest, path) {
2054
2284
  }
2055
2285
  async function readManifest(dest) {
2056
2286
  try {
2057
- return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
2287
+ return JSON.parse(await readFile2(join2(dest, MANIFEST_FILE), "utf8"));
2058
2288
  } catch {
2059
2289
  return void 0;
2060
2290
  }
@@ -2078,7 +2308,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
2078
2308
  const edited = [];
2079
2309
  const untracked = [];
2080
2310
  for (const path of present) {
2081
- const content = await readFile(join(dest, ...path.split("/")), "utf8");
2311
+ const content = await readFile2(join2(dest, ...path.split("/")), "utf8");
2082
2312
  if (Object.hasOwn(known, path)) {
2083
2313
  if (sha256(content) !== known[path]) edited.push(path);
2084
2314
  } else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
@@ -2091,8 +2321,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
2091
2321
  const written = [];
2092
2322
  for (const f of version.files) {
2093
2323
  const full = within(dest, f.path);
2094
- await mkdir(dirname(full), { recursive: true });
2095
- await writeFile(full, f.content, "utf8");
2324
+ await mkdir2(dirname2(full), { recursive: true });
2325
+ await writeFile2(full, f.content, "utf8");
2096
2326
  written.push(f.path);
2097
2327
  }
2098
2328
  const removed = [];
@@ -2103,7 +2333,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
2103
2333
  keptEdited.push(path);
2104
2334
  continue;
2105
2335
  }
2106
- await rm(within(dest, path));
2336
+ await rm2(within(dest, path));
2107
2337
  removed.push(path);
2108
2338
  }
2109
2339
  const next = {
@@ -2112,7 +2342,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
2112
2342
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
2113
2343
  files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
2114
2344
  };
2115
- await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
2345
+ await writeFile2(join2(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
2116
2346
  return {
2117
2347
  dest,
2118
2348
  version: version.version,
@@ -2142,15 +2372,15 @@ function describeInstall(name, r) {
2142
2372
  // src/tools/skills.ts
2143
2373
  var SKILLS = "/v1/standards/skills";
2144
2374
  function expandHome(path) {
2145
- return path.replace(/^~(?=$|[\\/])/, homedir2());
2375
+ return path.replace(/^~(?=$|[\\/])/, homedir3());
2146
2376
  }
2147
2377
  function text(t) {
2148
2378
  return { content: [{ type: "text", text: t }] };
2149
2379
  }
2150
2380
  function registerSkillTools(server2, apiClient2, { localFiles }) {
2151
- const fileSchema = z13.object({
2152
- path: z13.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
2153
- content: z13.string().describe("The file's full text, exactly as it is")
2381
+ const fileSchema = z14.object({
2382
+ path: z14.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
2383
+ content: z14.string().describe("The file's full text, exactly as it is")
2154
2384
  });
2155
2385
  server2.registerTool(
2156
2386
  "publish_skill",
@@ -2159,13 +2389,13 @@ function registerSkillTools(server2, apiClient2, { localFiles }) {
2159
2389
  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.",
2160
2390
  inputSchema: {
2161
2391
  ...localFiles ? {
2162
- directory: z13.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2392
+ directory: z14.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
2163
2393
  } : {},
2164
- files: z13.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
2165
- name: z13.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
2166
- spaceId: z13.string().optional().describe("Space for a NEW skill (default: your default space)"),
2167
- note: z13.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
2168
- baseVersion: z13.number().int().positive().optional().describe(
2394
+ files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
2395
+ name: z14.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
2396
+ spaceId: z14.string().optional().describe("Space for a NEW skill (default: your default space)"),
2397
+ note: z14.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
2398
+ baseVersion: z14.number().int().positive().optional().describe(
2169
2399
  "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"
2170
2400
  )
2171
2401
  },
@@ -2215,7 +2445,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2215
2445
  const { skills } = await apiClient2.get(SKILLS);
2216
2446
  if (!skills.length) {
2217
2447
  return text(
2218
- `No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/skills", await workspaceBase(apiClient2))}`
2448
+ `No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/team-setup/skills", await workspaceBase(apiClient2))}`
2219
2449
  );
2220
2450
  }
2221
2451
  return text(
@@ -2231,10 +2461,10 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2231
2461
  title: "Install Skill",
2232
2462
  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.",
2233
2463
  inputSchema: {
2234
- name: z13.string().describe("The skill's name"),
2235
- version: z13.number().int().positive().optional().describe("A version number (default: the latest)"),
2236
- target: z13.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
2237
- ...localFiles ? { overwrite: z13.boolean().optional().describe("Replace files edited since they were installed") } : {}
2464
+ name: z14.string().describe("The skill's name"),
2465
+ version: z14.number().int().positive().optional().describe("A version number (default: the latest)"),
2466
+ target: z14.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
2467
+ ...localFiles ? { overwrite: z14.boolean().optional().describe("Replace files edited since they were installed") } : {}
2238
2468
  },
2239
2469
  annotations: { readOnlyHint: !localFiles }
2240
2470
  },
@@ -2280,7 +2510,7 @@ ${body}`
2280
2510
  }
2281
2511
 
2282
2512
  // src/tools/team-setup.ts
2283
- import { z as z14 } from "zod";
2513
+ import { z as z15 } from "zod";
2284
2514
  import { existsSync as existsSync3 } from "fs";
2285
2515
  import { lstat as lstat2 } from "fs/promises";
2286
2516
  import { homedir as homedir4 } from "os";
@@ -2361,7 +2591,7 @@ function findSecrets(text3) {
2361
2591
  }
2362
2592
 
2363
2593
  // src/lib/team-setup-types.ts
2364
- var PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
2594
+ var PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
2365
2595
  var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2366
2596
 
2367
2597
  // src/sync-memory.ts
@@ -2638,63 +2868,6 @@ function lineDiff(before, after) {
2638
2868
  return out;
2639
2869
  }
2640
2870
 
2641
- // src/lib/team-setup-files.ts
2642
- import { execFile } from "child_process";
2643
- import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2644
- import { homedir as homedir3, userInfo } from "os";
2645
- import { dirname as dirname2, join as join2 } from "path";
2646
- import { promisify } from "util";
2647
- var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2648
- function isWorkspace(value) {
2649
- return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
2650
- }
2651
- function homeDir() {
2652
- return homedir3();
2653
- }
2654
- function systemdoxDir() {
2655
- return join2(homeDir(), ".claude", "systemdox");
2656
- }
2657
- function workspaceDir(workspace) {
2658
- if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
2659
- return join2(systemdoxDir(), workspace);
2660
- }
2661
- function statePath(workspace) {
2662
- return join2(workspaceDir(workspace), "state.json");
2663
- }
2664
- async function readJson(path, empty) {
2665
- let text3;
2666
- try {
2667
- text3 = await readFile2(path, "utf8");
2668
- } catch (e) {
2669
- if (e.code === "ENOENT") return empty;
2670
- throw e;
2671
- }
2672
- try {
2673
- return JSON.parse(text3);
2674
- } catch {
2675
- throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
2676
- }
2677
- }
2678
- async function writeJsonAtomically(path, value, mode) {
2679
- await mkdir2(dirname2(path), { recursive: true });
2680
- const tmp = `${path}.${process.pid}.tmp`;
2681
- await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
2682
- await rename(tmp, path);
2683
- }
2684
- var run = promisify(execFile);
2685
- async function readState(workspace) {
2686
- return readJson(statePath(workspace), {});
2687
- }
2688
- async function writeState(workspace, state) {
2689
- await writeJsonAtomically(statePath(workspace), state);
2690
- }
2691
- async function updateState(workspace, change) {
2692
- const state = await readState(workspace);
2693
- change(state);
2694
- await writeState(workspace, state);
2695
- return state;
2696
- }
2697
-
2698
2871
  // src/sync-memory.ts
2699
2872
  var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
2700
2873
  var SyncFailed = class extends Error {
@@ -3280,9 +3453,9 @@ function describePerson(id, person) {
3280
3453
  return lines.join("\n");
3281
3454
  }
3282
3455
  function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3283
- const fileSchema = z14.object({
3284
- path: z14.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
3285
- content: z14.string().describe("The file's full text, exactly as it is")
3456
+ const fileSchema = z15.object({
3457
+ path: z15.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
3458
+ content: z15.string().describe("The file's full text, exactly as it is")
3286
3459
  });
3287
3460
  server2.registerTool(
3288
3461
  "publish_setup_part",
@@ -3290,13 +3463,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3290
3463
  title: "Publish Setup Part",
3291
3464
  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."),
3292
3465
  inputSchema: {
3293
- part: z14.enum(PARTS).describe("Which part of the setup"),
3466
+ part: z15.enum(PARTS2).describe("Which part of the setup"),
3294
3467
  ...localFiles ? {
3295
- directory: z14.string().optional().describe(
3468
+ directory: z15.string().optional().describe(
3296
3469
  "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/"
3297
3470
  )
3298
3471
  } : {},
3299
- files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3472
+ files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3300
3473
  },
3301
3474
  annotations: { readOnlyHint: false }
3302
3475
  },
@@ -3332,7 +3505,7 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3332
3505
  title: "Resolve Person",
3333
3506
  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.",
3334
3507
  inputSchema: {
3335
- id: z14.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
3508
+ id: z15.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
3336
3509
  },
3337
3510
  annotations: { readOnlyHint: true }
3338
3511
  },
@@ -3349,8 +3522,8 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3349
3522
  title: "Sync Memory",
3350
3523
  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.`,
3351
3524
  inputSchema: {
3352
- project: z14.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
3353
- dryRun: z14.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
3525
+ project: z15.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
3526
+ dryRun: z15.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
3354
3527
  },
3355
3528
  annotations: { readOnlyHint: false }
3356
3529
  },
@@ -3368,7 +3541,7 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
3368
3541
  }
3369
3542
 
3370
3543
  // src/server.ts
3371
- var SERVER_VERSION = true ? "0.1.48" : "0.0.0-dev";
3544
+ var SERVER_VERSION = true ? "0.1.49" : "0.0.0-dev";
3372
3545
  function createServer(apiClient2, { localFiles = false } = {}) {
3373
3546
  const server2 = new McpServer({
3374
3547
  name: "systemdox-mcp-server",
@@ -3380,6 +3553,7 @@ function createServer(apiClient2, { localFiles = false } = {}) {
3380
3553
  registerGetTechStack(server2, apiClient2);
3381
3554
  registerGetPlanningContext(server2, apiClient2);
3382
3555
  registerGetPlanBoard(server2, apiClient2);
3556
+ registerGetChange(server2, apiClient2);
3383
3557
  registerGetContext(server2, apiClient2);
3384
3558
  registerGetProjectRules(server2, apiClient2);
3385
3559
  registerGetPromptTemplates(server2, apiClient2);
@@ -3394,7 +3568,13 @@ function createServer(apiClient2, { localFiles = false } = {}) {
3394
3568
  }
3395
3569
 
3396
3570
  // src/index.ts
3397
- var config = loadConfig();
3571
+ var config;
3572
+ try {
3573
+ config = await loadLocalConfig();
3574
+ } catch (err) {
3575
+ console.error(`SystemDox MCP server: ${err.message}`);
3576
+ process.exit(1);
3577
+ }
3398
3578
  var apiClient = new SystemDoxApiClient(config);
3399
3579
  var server = createServer(apiClient, { localFiles: true });
3400
3580
  async function main() {