@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/README.md +24 -11
- package/dist/cli.js +462 -260
- package/dist/http.js +203 -61
- package/dist/index.d.ts +2 -1
- package/dist/index.js +348 -168
- package/dist/install-skill.js +1 -2
- package/dist/lib/config.d.ts +4 -1
- package/dist/lib/config.js +1 -2
- package/dist/lib/team-setup-files.d.ts +14 -1
- package/dist/lib/team-setup-files.js +43 -0
- package/dist/setup-mcp.d.ts +66 -0
- package/dist/setup-mcp.js +636 -0
- package/dist/setup-team.d.ts +5 -1
- package/dist/setup-team.js +1 -0
- package/dist/sync-memory.js +22 -12
- package/dist/tools/get-change.d.ts +134 -0
- package/dist/tools/get-change.js +147 -0
- package/dist/tools/skills.js +1 -1
- package/package.json +1 -1
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-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
1558
|
-
description:
|
|
1559
|
-
severity:
|
|
1560
|
-
category:
|
|
1561
|
-
pattern:
|
|
1562
|
-
fix:
|
|
1563
|
-
fitness_test:
|
|
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
|
|
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:
|
|
1638
|
-
section:
|
|
1639
|
-
content:
|
|
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:
|
|
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
|
|
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:
|
|
1701
|
-
context:
|
|
1702
|
-
decision:
|
|
1703
|
-
consequences:
|
|
1704
|
-
related_files:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
1914
|
-
description:
|
|
1915
|
-
prompt:
|
|
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
|
|
1945
|
-
import { homedir as
|
|
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
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
2095
|
-
await
|
|
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
|
|
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
|
|
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(/^~(?=$|[\\/])/,
|
|
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 =
|
|
2152
|
-
path:
|
|
2153
|
-
content:
|
|
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:
|
|
2392
|
+
directory: z14.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
|
|
2163
2393
|
} : {},
|
|
2164
|
-
files:
|
|
2165
|
-
name:
|
|
2166
|
-
spaceId:
|
|
2167
|
-
note:
|
|
2168
|
-
baseVersion:
|
|
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:
|
|
2235
|
-
version:
|
|
2236
|
-
target:
|
|
2237
|
-
...localFiles ? { overwrite:
|
|
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
|
|
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
|
|
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 =
|
|
3284
|
-
path:
|
|
3285
|
-
content:
|
|
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:
|
|
3466
|
+
part: z15.enum(PARTS2).describe("Which part of the setup"),
|
|
3294
3467
|
...localFiles ? {
|
|
3295
|
-
directory:
|
|
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:
|
|
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:
|
|
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:
|
|
3353
|
-
dryRun:
|
|
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.
|
|
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
|
|
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() {
|