@systemdox/mcp-server 0.1.48 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -11
- package/dist/cli.js +963 -357
- package/dist/http.js +1248 -732
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1341 -784
- package/dist/install-skill.js +10 -17
- package/dist/lib/api-client.d.ts +2 -3
- package/dist/lib/api-client.js +7 -14
- package/dist/lib/config.d.ts +4 -1
- package/dist/lib/config.js +1 -2
- package/dist/lib/context-builder.js +14 -7
- package/dist/lib/session-brief.d.ts +59 -0
- package/dist/lib/session-brief.js +174 -0
- package/dist/lib/skills.js +2 -1
- package/dist/lib/team-setup-files.d.ts +14 -1
- package/dist/lib/team-setup-files.js +43 -0
- package/dist/lib/team-setup-types.d.ts +18 -0
- package/dist/plugin-path.js +7 -14
- package/dist/server.d.ts +5 -1
- package/dist/server.js +3579 -0
- package/dist/setup-mcp.d.ts +66 -0
- package/dist/setup-mcp.js +630 -0
- package/dist/setup-team.d.ts +5 -1
- package/dist/setup-team.js +8 -14
- package/dist/sync-memory.js +29 -26
- package/dist/tools/document-graph.d.ts +87 -0
- package/dist/tools/get-change.d.ts +134 -0
- package/dist/tools/get-change.js +147 -0
- package/dist/tools/get-team-setup.d.ts +6 -0
- package/dist/tools/skills.js +3 -2
- package/dist/tools/team-setup.js +2 -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();
|
|
@@ -99,14 +63,6 @@ var SystemDoxApiClient = class {
|
|
|
99
63
|
this.config = config2;
|
|
100
64
|
this.cache = new ResponseCache(config2.cacheTtlMs);
|
|
101
65
|
}
|
|
102
|
-
async validate() {
|
|
103
|
-
const res = await this.fetchRaw("/v1/health");
|
|
104
|
-
if (!res.ok) {
|
|
105
|
-
throw new Error(
|
|
106
|
-
`SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
66
|
async get(path, params) {
|
|
111
67
|
const url = this.buildUrl(path, params);
|
|
112
68
|
const cacheKey = url.toString();
|
|
@@ -120,8 +76,8 @@ var SystemDoxApiClient = class {
|
|
|
120
76
|
}
|
|
121
77
|
/** A read that skips the cache: an install must take the version that is
|
|
122
78
|
* latest now, not the one cached before someone published another. */
|
|
123
|
-
async getFresh(path) {
|
|
124
|
-
const res = await this.fetchRaw(path);
|
|
79
|
+
async getFresh(path, signal) {
|
|
80
|
+
const res = await this.fetchRaw(path, void 0, signal);
|
|
125
81
|
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
126
82
|
return await res.json();
|
|
127
83
|
}
|
|
@@ -151,8 +107,8 @@ var SystemDoxApiClient = class {
|
|
|
151
107
|
// One method per operationId. Every read skips the cache: setup, sync and
|
|
152
108
|
// plugin-path act on what the workspace holds now.
|
|
153
109
|
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
154
|
-
getOwnTeamSetup() {
|
|
155
|
-
return this.getFresh(TEAM_SETUP);
|
|
110
|
+
getOwnTeamSetup(signal) {
|
|
111
|
+
return this.getFresh(TEAM_SETUP, signal);
|
|
156
112
|
}
|
|
157
113
|
getTeamSetup(workspace) {
|
|
158
114
|
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
@@ -256,116 +212,571 @@ var SystemDoxApiClient = class {
|
|
|
256
212
|
}
|
|
257
213
|
return url;
|
|
258
214
|
}
|
|
259
|
-
async fetchRaw(path, params) {
|
|
215
|
+
async fetchRaw(path, params, signal) {
|
|
260
216
|
const url = this.buildUrl(path, params);
|
|
261
217
|
return fetch(url, {
|
|
262
218
|
headers: {
|
|
263
219
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
264
220
|
"X-API-Key": this.config.apiKey,
|
|
265
221
|
Accept: "application/json"
|
|
266
|
-
}
|
|
222
|
+
},
|
|
223
|
+
signal
|
|
267
224
|
});
|
|
268
225
|
}
|
|
269
226
|
};
|
|
270
227
|
|
|
271
|
-
// src/
|
|
272
|
-
|
|
228
|
+
// src/lib/config.ts
|
|
229
|
+
var API_URL_DEFAULT = "https://api.puglieseweb.com/systemdox";
|
|
230
|
+
function requireHttpsApiUrl(apiUrl) {
|
|
231
|
+
let parsed;
|
|
232
|
+
try {
|
|
233
|
+
parsed = new URL(apiUrl);
|
|
234
|
+
} catch {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`SYSTEMDOX_API_URL is not a valid URL: ${apiUrl}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
if (parsed.protocol !== "https:") {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`SYSTEMDOX_API_URL must use https:// \u2014 refusing to send the API key over ${parsed.protocol}// in cleartext. Got: ${apiUrl}`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return apiUrl;
|
|
245
|
+
}
|
|
246
|
+
function loadConfig(apiKey = process.env.SYSTEMDOX_API_KEY) {
|
|
247
|
+
if (!apiKey) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
"SYSTEMDOX_API_KEY is required. Generate one at your SystemDox settings page."
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const apiUrl = requireHttpsApiUrl(
|
|
253
|
+
process.env.SYSTEMDOX_API_URL ?? API_URL_DEFAULT
|
|
254
|
+
);
|
|
255
|
+
return {
|
|
256
|
+
apiKey,
|
|
257
|
+
apiUrl,
|
|
258
|
+
cacheTtlMs: parseInt(process.env.SYSTEMDOX_CACHE_TTL ?? "300", 10) * 1e3,
|
|
259
|
+
defaultOrg: process.env.SYSTEMDOX_DEFAULT_ORG
|
|
260
|
+
};
|
|
261
|
+
}
|
|
273
262
|
|
|
274
|
-
// src/
|
|
275
|
-
import {
|
|
263
|
+
// src/lib/session-brief.ts
|
|
264
|
+
import { existsSync as existsSync2 } from "fs";
|
|
265
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
266
|
+
import { join as join3 } from "path";
|
|
276
267
|
|
|
277
|
-
// src/lib/
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
268
|
+
// src/lib/skills.ts
|
|
269
|
+
import { createHash } from "crypto";
|
|
270
|
+
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
271
|
+
import { existsSync } from "fs";
|
|
272
|
+
import { homedir } from "os";
|
|
273
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
274
|
+
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
275
|
+
var ENTRY_FILE = "SKILL.md";
|
|
276
|
+
var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
|
|
277
|
+
var MAX_FILES = 100;
|
|
278
|
+
var MAX_FILE_BYTES = 256 * 1024;
|
|
279
|
+
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
280
|
+
var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
281
|
+
var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
282
|
+
var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
|
|
283
|
+
var SkillInstallRefused = class extends Error {
|
|
284
|
+
constructor(dest, edited) {
|
|
285
|
+
super(
|
|
286
|
+
`${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
|
|
287
|
+
);
|
|
288
|
+
this.dest = dest;
|
|
289
|
+
this.edited = edited;
|
|
290
|
+
}
|
|
291
|
+
dest;
|
|
292
|
+
edited;
|
|
300
293
|
};
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
{ pattern: /backend\/layers\//, categories: ["backend"] },
|
|
304
|
-
{ pattern: /backend\//, categories: ["backend"] },
|
|
305
|
-
{ pattern: /apps\/app\//, categories: ["frontend"] },
|
|
306
|
-
{ pattern: /apps\/website\//, categories: ["frontend"] },
|
|
307
|
-
{ pattern: /apps\/portal\//, categories: ["frontend"] },
|
|
308
|
-
{ pattern: /packages\/ui\//, categories: ["frontend"] },
|
|
309
|
-
{ pattern: /infra\//, categories: ["infrastructure"] },
|
|
310
|
-
{ pattern: /\.github\//, categories: ["infrastructure"] },
|
|
311
|
-
{ pattern: /tests?\//, categories: ["testing"] },
|
|
312
|
-
{ pattern: /spec/, categories: ["testing"] },
|
|
313
|
-
{ pattern: /docs\/adr/, categories: ["architecture"] },
|
|
314
|
-
{ pattern: /template\.yaml$/, categories: ["infrastructure", "architecture"] }
|
|
315
|
-
];
|
|
316
|
-
function detectLanguage(filePath) {
|
|
317
|
-
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
318
|
-
return EXTENSION_MAP[ext];
|
|
294
|
+
function sha256(content) {
|
|
295
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
319
296
|
}
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
297
|
+
function isValidName(name) {
|
|
298
|
+
return name.length <= 64 && NAME_RE.test(name);
|
|
299
|
+
}
|
|
300
|
+
function isValidPath(path) {
|
|
301
|
+
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
|
|
302
|
+
}
|
|
303
|
+
function frontmatterName(skillMd) {
|
|
304
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
305
|
+
if (!block) return void 0;
|
|
306
|
+
const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
|
|
307
|
+
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
308
|
+
}
|
|
309
|
+
function defaultSkillDir(name) {
|
|
310
|
+
return join(homedir(), ".claude", "skills", name);
|
|
311
|
+
}
|
|
312
|
+
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
313
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
314
|
+
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
315
|
+
const full = join(dir, entry.name);
|
|
316
|
+
const rel = relative(root, full).split(sep).join("/");
|
|
317
|
+
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
318
|
+
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
319
|
+
else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
|
|
320
|
+
else if (entry.isFile()) out.files.push(rel);
|
|
321
|
+
}
|
|
322
|
+
out.files.sort();
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
async function readTextFiles(root, paths, what) {
|
|
326
|
+
if (paths.length > MAX_FILES) {
|
|
327
|
+
throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
|
|
328
|
+
}
|
|
329
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
330
|
+
const files = [];
|
|
331
|
+
let total = 0;
|
|
332
|
+
for (const path of paths) {
|
|
333
|
+
if (!isValidPath(path)) {
|
|
334
|
+
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
328
335
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
336
|
+
const raw = await readFile(join(root, ...path.split("/")));
|
|
337
|
+
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
338
|
+
if (raw.length > MAX_FILE_BYTES) {
|
|
339
|
+
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
340
|
+
}
|
|
341
|
+
total += raw.length;
|
|
342
|
+
if (total > MAX_TOTAL_BYTES) {
|
|
343
|
+
throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
|
|
344
|
+
}
|
|
345
|
+
let content;
|
|
346
|
+
try {
|
|
347
|
+
content = decoder.decode(raw);
|
|
348
|
+
} catch {
|
|
349
|
+
throw new Error(`${path} is not UTF-8 text`);
|
|
335
350
|
}
|
|
351
|
+
files.push({ path, content });
|
|
336
352
|
}
|
|
337
|
-
return
|
|
353
|
+
return files;
|
|
338
354
|
}
|
|
339
|
-
function
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (info) languages.add(info.language);
|
|
355
|
+
async function readSkillDirectory(directory) {
|
|
356
|
+
const root = resolve(directory);
|
|
357
|
+
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
358
|
+
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
344
359
|
}
|
|
345
|
-
|
|
360
|
+
const walked = await walk(root, { skipHidden: true });
|
|
361
|
+
if (walked.links.length) {
|
|
362
|
+
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
363
|
+
}
|
|
364
|
+
const files = await readTextFiles(root, walked.files, "a skill");
|
|
365
|
+
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
366
|
+
const name = frontmatterName(skillMd.content);
|
|
367
|
+
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
368
|
+
return { name, files, skipped: walked.hidden };
|
|
346
369
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return
|
|
370
|
+
function within(dest, path) {
|
|
371
|
+
if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
|
|
372
|
+
const full = resolve(dest, ...path.split("/"));
|
|
373
|
+
if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
|
|
374
|
+
return full;
|
|
352
375
|
}
|
|
353
|
-
|
|
354
|
-
// src/lib/config-links.ts
|
|
355
|
-
async function workspaceBase(apiClient2) {
|
|
376
|
+
async function readManifest(dest) {
|
|
356
377
|
try {
|
|
357
|
-
|
|
358
|
-
"/v1/docs/portal-access"
|
|
359
|
-
);
|
|
360
|
-
const slug = (access.host ?? "").split(".")[0];
|
|
361
|
-
return slug ? `https://${slug}.systemdox.com` : null;
|
|
378
|
+
return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
|
|
362
379
|
} catch {
|
|
363
|
-
return
|
|
380
|
+
return void 0;
|
|
364
381
|
}
|
|
365
382
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
383
|
+
async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
384
|
+
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
385
|
+
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
386
|
+
for (const f of version.files) within(dest, f.path);
|
|
387
|
+
if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
388
|
+
throw new Error(`${dest} is a link: install into a real folder`);
|
|
389
|
+
}
|
|
390
|
+
const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
391
|
+
if (walked.links.length) {
|
|
392
|
+
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
393
|
+
}
|
|
394
|
+
const present = walked.files;
|
|
395
|
+
const manifest = await readManifest(dest);
|
|
396
|
+
const known2 = Object.fromEntries(
|
|
397
|
+
Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
|
|
398
|
+
);
|
|
399
|
+
const edited = [];
|
|
400
|
+
const untracked = [];
|
|
401
|
+
for (const path of present) {
|
|
402
|
+
const content = await readFile(join(dest, ...path.split("/")), "utf8");
|
|
403
|
+
if (Object.hasOwn(known2, path)) {
|
|
404
|
+
if (sha256(content) !== known2[path]) edited.push(path);
|
|
405
|
+
} else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
|
|
406
|
+
if (incoming.has(path) || !manifest) edited.push(path);
|
|
407
|
+
else untracked.push(path);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const blocking = edited.filter((p) => incoming.has(p) || !manifest);
|
|
411
|
+
if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
|
|
412
|
+
const written = [];
|
|
413
|
+
for (const f of version.files) {
|
|
414
|
+
const full = within(dest, f.path);
|
|
415
|
+
await mkdir(dirname(full), { recursive: true });
|
|
416
|
+
await writeFile(full, f.content, "utf8");
|
|
417
|
+
written.push(f.path);
|
|
418
|
+
}
|
|
419
|
+
const removed = [];
|
|
420
|
+
const keptEdited = [];
|
|
421
|
+
for (const path of Object.keys(known2)) {
|
|
422
|
+
if (incoming.has(path) || !present.includes(path)) continue;
|
|
423
|
+
if (edited.includes(path)) {
|
|
424
|
+
keptEdited.push(path);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
await rm(within(dest, path));
|
|
428
|
+
removed.push(path);
|
|
429
|
+
}
|
|
430
|
+
const next = {
|
|
431
|
+
name: version.name,
|
|
432
|
+
version: version.version,
|
|
433
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
434
|
+
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
435
|
+
};
|
|
436
|
+
await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
437
|
+
return {
|
|
438
|
+
dest,
|
|
439
|
+
version: version.version,
|
|
440
|
+
written,
|
|
441
|
+
removed,
|
|
442
|
+
keptUntracked: untracked.filter((p) => !incoming.has(p)),
|
|
443
|
+
keptEdited
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function fenceFor(text3) {
|
|
447
|
+
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
448
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
449
|
+
}
|
|
450
|
+
function describeInstall(name, r) {
|
|
451
|
+
const lines = [
|
|
452
|
+
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
453
|
+
];
|
|
454
|
+
if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
|
|
455
|
+
if (r.keptEdited.length) {
|
|
456
|
+
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
457
|
+
}
|
|
458
|
+
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
459
|
+
lines.push("Start a new Claude Code session to load it.");
|
|
460
|
+
return lines.join("\n");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/lib/team-setup-files.ts
|
|
464
|
+
import { execFile } from "child_process";
|
|
465
|
+
import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
466
|
+
import { homedir as homedir2, userInfo } from "os";
|
|
467
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
468
|
+
import { promisify } from "util";
|
|
469
|
+
var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
470
|
+
function isWorkspace(value) {
|
|
471
|
+
return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
|
|
472
|
+
}
|
|
473
|
+
function homeDir() {
|
|
474
|
+
return homedir2();
|
|
475
|
+
}
|
|
476
|
+
function systemdoxDir() {
|
|
477
|
+
return join2(homeDir(), ".claude", "systemdox");
|
|
478
|
+
}
|
|
479
|
+
function credentialsPath() {
|
|
480
|
+
return join2(systemdoxDir(), "credentials.json");
|
|
481
|
+
}
|
|
482
|
+
function workspaceDir(workspace) {
|
|
483
|
+
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
484
|
+
return join2(systemdoxDir(), workspace);
|
|
485
|
+
}
|
|
486
|
+
function statePath(workspace) {
|
|
487
|
+
return join2(workspaceDir(workspace), "state.json");
|
|
488
|
+
}
|
|
489
|
+
async function readJson(path, empty) {
|
|
490
|
+
let text3;
|
|
491
|
+
try {
|
|
492
|
+
text3 = await readFile2(path, "utf8");
|
|
493
|
+
} catch (e) {
|
|
494
|
+
if (e.code === "ENOENT") return empty;
|
|
495
|
+
throw e;
|
|
496
|
+
}
|
|
497
|
+
try {
|
|
498
|
+
return JSON.parse(text3);
|
|
499
|
+
} catch {
|
|
500
|
+
throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async function writeJsonAtomically(path, value, mode) {
|
|
504
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
505
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
506
|
+
await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
|
|
507
|
+
await rename(tmp, path);
|
|
508
|
+
}
|
|
509
|
+
async function readCredentials() {
|
|
510
|
+
return readJson(credentialsPath(), {});
|
|
511
|
+
}
|
|
512
|
+
var run = promisify(execFile);
|
|
513
|
+
async function localApiKey(env = process.env) {
|
|
514
|
+
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
515
|
+
const keys = Object.entries(await readCredentials()).filter(
|
|
516
|
+
(e) => typeof e[1]?.apiKey === "string" && e[1].apiKey.length > 0
|
|
517
|
+
);
|
|
518
|
+
const wanted = env.SYSTEMDOX_WORKSPACE;
|
|
519
|
+
if (wanted) {
|
|
520
|
+
const key = keys.find(([ws]) => ws === wanted)?.[1].apiKey;
|
|
521
|
+
if (key) return key;
|
|
522
|
+
throw new Error(
|
|
523
|
+
`~/.claude/systemdox/credentials.json holds no key for ${wanted} (SYSTEMDOX_WORKSPACE). Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.`
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
if (keys.length === 1) return keys[0][1].apiKey;
|
|
527
|
+
if (keys.length > 1) {
|
|
528
|
+
throw new Error(
|
|
529
|
+
`~/.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.`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
throw new Error("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.");
|
|
533
|
+
}
|
|
534
|
+
async function loadLocalConfig(env = process.env) {
|
|
535
|
+
const apiKey = await localApiKey(env);
|
|
536
|
+
return loadConfig(apiKey);
|
|
537
|
+
}
|
|
538
|
+
async function readState(workspace) {
|
|
539
|
+
return readJson(statePath(workspace), {});
|
|
540
|
+
}
|
|
541
|
+
async function writeState(workspace, state) {
|
|
542
|
+
await writeJsonAtomically(statePath(workspace), state);
|
|
543
|
+
}
|
|
544
|
+
async function updateState(workspace, change) {
|
|
545
|
+
const state = await readState(workspace);
|
|
546
|
+
change(state);
|
|
547
|
+
await writeState(workspace, state);
|
|
548
|
+
return state;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/lib/session-brief.ts
|
|
552
|
+
var INSTRUCTIONS_MAX = 2e3;
|
|
553
|
+
var BRIEF_TIMEOUT_MS = 8e3;
|
|
554
|
+
var NO_KEY = "no-key-on-this-computer";
|
|
555
|
+
var SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
|
|
556
|
+
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.`;
|
|
557
|
+
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.";
|
|
558
|
+
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.";
|
|
559
|
+
function noKey(reason) {
|
|
560
|
+
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.`;
|
|
561
|
+
}
|
|
562
|
+
function keyReason(message) {
|
|
563
|
+
if (/holds keys for/.test(message)) return "keys for several workspaces are stored; set SYSTEMDOX_WORKSPACE to the one to use";
|
|
564
|
+
if (/holds no key for/.test(message)) return "SYSTEMDOX_WORKSPACE names a workspace with no stored key";
|
|
565
|
+
if (/No SystemDox key/.test(message)) return "none is stored";
|
|
566
|
+
return "the stored keys could not be read";
|
|
567
|
+
}
|
|
568
|
+
function olderApi(setup) {
|
|
569
|
+
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.`;
|
|
570
|
+
}
|
|
571
|
+
async function readJsonQuietly(path) {
|
|
572
|
+
try {
|
|
573
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
574
|
+
} catch {
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async function computerState(setup) {
|
|
579
|
+
const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
|
|
580
|
+
const installed = await readJsonQuietly(join3(homeDir(), ".claude", "plugins", "installed_plugins.json"));
|
|
581
|
+
const entries2 = installed?.plugins?.[id];
|
|
582
|
+
const entry = Array.isArray(entries2) ? entries2.find((e) => (e?.scope ?? "user") === "user") : void 0;
|
|
583
|
+
const state = {
|
|
584
|
+
missing: [],
|
|
585
|
+
behind: [],
|
|
586
|
+
notFromSystemDox: [],
|
|
587
|
+
pluginInstalled: !!entry,
|
|
588
|
+
pluginVersion: entry?.version
|
|
589
|
+
};
|
|
590
|
+
const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
|
|
591
|
+
for (const skill of setup.sessionBrief?.skills ?? []) {
|
|
592
|
+
const dir = defaultSkillDir(skill.name);
|
|
593
|
+
if (!existsSync2(dir)) {
|
|
594
|
+
if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const manifest = await readJsonQuietly(join3(dir, MANIFEST_FILE));
|
|
598
|
+
if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
|
|
599
|
+
state.notFromSystemDox.push(skill.name);
|
|
600
|
+
} else if (manifest.version !== skill.latestVersion) {
|
|
601
|
+
state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return state;
|
|
605
|
+
}
|
|
606
|
+
function hasPersonParts(setup) {
|
|
607
|
+
return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
|
|
608
|
+
}
|
|
609
|
+
var MORE = (n) => `${n} more (get_team_setup lists them)`;
|
|
610
|
+
var LIST_MIN = `all ${MORE(100)}`.length;
|
|
611
|
+
function listWithin(items, room) {
|
|
612
|
+
const whole2 = items.join(", ");
|
|
613
|
+
if (whole2.length <= room) return whole2;
|
|
614
|
+
const kept = [];
|
|
615
|
+
let used = 0;
|
|
616
|
+
for (let i = 0; i < items.length; i++) {
|
|
617
|
+
const rest = items.length - i - 1;
|
|
618
|
+
const cost = items[i].length + (kept.length ? 2 : 0);
|
|
619
|
+
if (used + cost + ` and ${MORE(rest)}`.length > room) break;
|
|
620
|
+
kept.push(items[i]);
|
|
621
|
+
used += cost;
|
|
622
|
+
}
|
|
623
|
+
return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
|
|
624
|
+
}
|
|
625
|
+
function localSection(setup, state, room) {
|
|
626
|
+
const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
|
|
627
|
+
const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
|
|
628
|
+
const lines = [];
|
|
629
|
+
if (state.missing.length) {
|
|
630
|
+
lines.push({
|
|
631
|
+
head: "On this computer, missing: ",
|
|
632
|
+
items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
|
|
633
|
+
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.`
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (state.behind.length) {
|
|
637
|
+
lines.push({
|
|
638
|
+
head: "On this computer, behind: ",
|
|
639
|
+
items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
|
|
640
|
+
tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
if (state.notFromSystemDox.length) {
|
|
644
|
+
lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
|
|
645
|
+
}
|
|
646
|
+
const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
|
|
647
|
+
while (lines.length && fixed() > room) lines.pop();
|
|
648
|
+
if (!lines.length) return "";
|
|
649
|
+
const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
|
|
650
|
+
return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
|
|
651
|
+
}
|
|
652
|
+
function fit(text3, section) {
|
|
653
|
+
const whole2 = section ? `${text3}
|
|
654
|
+
|
|
655
|
+
${section}` : text3;
|
|
656
|
+
if (whole2.length <= INSTRUCTIONS_MAX) return whole2;
|
|
657
|
+
return text3.length <= INSTRUCTIONS_MAX ? text3 : `${text3.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
|
|
658
|
+
}
|
|
659
|
+
async function readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
|
|
660
|
+
if (keyProblem2) return { problem: noKey(keyProblem2) };
|
|
661
|
+
try {
|
|
662
|
+
const setup = await apiClient2.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
|
|
663
|
+
return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
|
|
664
|
+
} catch (err) {
|
|
665
|
+
const status = err?.status;
|
|
666
|
+
return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async function sessionInstructions(apiClient2, options = {}) {
|
|
670
|
+
const read = await readTeamSetup(apiClient2, options);
|
|
671
|
+
if ("problem" in read) return read.problem;
|
|
672
|
+
const text3 = read.setup.sessionBrief?.text ?? olderApi(read.setup);
|
|
673
|
+
try {
|
|
674
|
+
if (!options.localFiles) return fit(text3, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
|
|
675
|
+
const room = INSTRUCTIONS_MAX - text3.length - 2;
|
|
676
|
+
return fit(text3, localSection(read.setup, await computerState(read.setup), room));
|
|
677
|
+
} catch {
|
|
678
|
+
return fit(text3, "");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/server.ts
|
|
683
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
684
|
+
|
|
685
|
+
// src/tools/get-guardrails.ts
|
|
686
|
+
import { z } from "zod";
|
|
687
|
+
|
|
688
|
+
// src/lib/language.ts
|
|
689
|
+
var EXTENSION_MAP = {
|
|
690
|
+
".py": { language: "python", categories: ["api", "backend", "data-access"] },
|
|
691
|
+
".ts": { language: "typescript", categories: ["frontend", "api"] },
|
|
692
|
+
".tsx": { language: "typescript", categories: ["frontend"] },
|
|
693
|
+
".js": { language: "javascript", categories: ["frontend"] },
|
|
694
|
+
".jsx": { language: "javascript", categories: ["frontend"] },
|
|
695
|
+
".yaml": {
|
|
696
|
+
language: "cloudformation",
|
|
697
|
+
categories: ["infrastructure", "architecture"]
|
|
698
|
+
},
|
|
699
|
+
".yml": {
|
|
700
|
+
language: "cloudformation",
|
|
701
|
+
categories: ["infrastructure", "architecture"]
|
|
702
|
+
},
|
|
703
|
+
".json": { language: "json", categories: ["configuration"] },
|
|
704
|
+
".css": { language: "css", categories: ["frontend"] },
|
|
705
|
+
".html": { language: "html", categories: ["frontend"] },
|
|
706
|
+
".sql": { language: "sql", categories: ["data-access"] },
|
|
707
|
+
".sh": { language: "shell", categories: ["infrastructure"] },
|
|
708
|
+
".bash": { language: "shell", categories: ["infrastructure"] },
|
|
709
|
+
".tf": { language: "terraform", categories: ["infrastructure"] },
|
|
710
|
+
".md": { language: "markdown", categories: ["documentation"] }
|
|
711
|
+
};
|
|
712
|
+
var PATH_CATEGORY_RULES = [
|
|
713
|
+
{ pattern: /backend\/functions\//, categories: ["api", "backend"] },
|
|
714
|
+
{ pattern: /backend\/layers\//, categories: ["backend"] },
|
|
715
|
+
{ pattern: /backend\//, categories: ["backend"] },
|
|
716
|
+
{ pattern: /apps\/app\//, categories: ["frontend"] },
|
|
717
|
+
{ pattern: /apps\/website\//, categories: ["frontend"] },
|
|
718
|
+
{ pattern: /apps\/portal\//, categories: ["frontend"] },
|
|
719
|
+
{ pattern: /packages\/ui\//, categories: ["frontend"] },
|
|
720
|
+
{ pattern: /infra\//, categories: ["infrastructure"] },
|
|
721
|
+
{ pattern: /\.github\//, categories: ["infrastructure"] },
|
|
722
|
+
{ pattern: /tests?\//, categories: ["testing"] },
|
|
723
|
+
{ pattern: /spec/, categories: ["testing"] },
|
|
724
|
+
{ pattern: /docs\/adr/, categories: ["architecture"] },
|
|
725
|
+
{ pattern: /template\.yaml$/, categories: ["infrastructure", "architecture"] }
|
|
726
|
+
];
|
|
727
|
+
function detectLanguage(filePath) {
|
|
728
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
729
|
+
return EXTENSION_MAP[ext];
|
|
730
|
+
}
|
|
731
|
+
function detectCategories(filePaths) {
|
|
732
|
+
const categories = /* @__PURE__ */ new Set();
|
|
733
|
+
for (const filePath of filePaths) {
|
|
734
|
+
const langInfo = detectLanguage(filePath);
|
|
735
|
+
if (langInfo) {
|
|
736
|
+
for (const cat of langInfo.categories) {
|
|
737
|
+
categories.add(cat);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
for (const rule of PATH_CATEGORY_RULES) {
|
|
741
|
+
if (rule.pattern.test(filePath)) {
|
|
742
|
+
for (const cat of rule.categories) {
|
|
743
|
+
categories.add(cat);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return Array.from(categories);
|
|
749
|
+
}
|
|
750
|
+
function detectLanguages(filePaths) {
|
|
751
|
+
const languages = /* @__PURE__ */ new Set();
|
|
752
|
+
for (const filePath of filePaths) {
|
|
753
|
+
const info = detectLanguage(filePath);
|
|
754
|
+
if (info) languages.add(info.language);
|
|
755
|
+
}
|
|
756
|
+
return Array.from(languages);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/lib/deprecation.ts
|
|
760
|
+
var LEGACY_TOOL_REMOVAL_EARLIEST = "2027-01-31";
|
|
761
|
+
function legacyToolDescription(canonical, description) {
|
|
762
|
+
return `DEPRECATED \u2014 use \`${canonical}\` instead. This name still works and will not be removed before ${LEGACY_TOOL_REMOVAL_EARLIEST}. ${description}`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/lib/config-links.ts
|
|
766
|
+
async function workspaceBase(apiClient2) {
|
|
767
|
+
try {
|
|
768
|
+
const access = await apiClient2.get(
|
|
769
|
+
"/v1/docs/portal-access"
|
|
770
|
+
);
|
|
771
|
+
const slug = (access.host ?? "").split(".")[0];
|
|
772
|
+
return slug ? `https://${slug}.systemdox.com` : null;
|
|
773
|
+
} catch {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function configLink(path, base) {
|
|
778
|
+
return base ? `${base}${path}` : `${path} in your SystemDox workspace`;
|
|
779
|
+
}
|
|
369
780
|
|
|
370
781
|
// src/tools/get-guardrails.ts
|
|
371
782
|
var DESCRIPTION = "Retrieve SystemDox checks (banned patterns, required patterns, coding rules). Filter by language, category, or file path to get only relevant checks.";
|
|
@@ -380,7 +791,7 @@ var inputSchema = () => ({
|
|
|
380
791
|
"File path being edited \u2014 language is auto-detected from the extension"
|
|
381
792
|
)
|
|
382
793
|
});
|
|
383
|
-
function registerGetGuardrails(
|
|
794
|
+
function registerGetGuardrails(server, apiClient2) {
|
|
384
795
|
const handler = async ({
|
|
385
796
|
language,
|
|
386
797
|
category,
|
|
@@ -434,7 +845,7 @@ ${text3}`
|
|
|
434
845
|
]
|
|
435
846
|
};
|
|
436
847
|
};
|
|
437
|
-
|
|
848
|
+
server.registerTool(
|
|
438
849
|
"get_checks",
|
|
439
850
|
{
|
|
440
851
|
title: "Get Checks",
|
|
@@ -443,7 +854,7 @@ ${text3}`
|
|
|
443
854
|
},
|
|
444
855
|
handler
|
|
445
856
|
);
|
|
446
|
-
|
|
857
|
+
server.registerTool(
|
|
447
858
|
"get_guardrails",
|
|
448
859
|
{
|
|
449
860
|
title: "Get Checks (legacy name)",
|
|
@@ -456,8 +867,8 @@ ${text3}`
|
|
|
456
867
|
|
|
457
868
|
// src/tools/get-specs.ts
|
|
458
869
|
import { z as z2 } from "zod";
|
|
459
|
-
function registerGetSpecs(
|
|
460
|
-
|
|
870
|
+
function registerGetSpecs(server, apiClient2) {
|
|
871
|
+
server.registerTool(
|
|
461
872
|
"get_specs",
|
|
462
873
|
{
|
|
463
874
|
title: "Get Specs",
|
|
@@ -542,8 +953,8 @@ function matchGlob(path, pattern) {
|
|
|
542
953
|
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
543
954
|
return new RegExp(`^${regex}$`).test(path);
|
|
544
955
|
}
|
|
545
|
-
function registerGetDocs(
|
|
546
|
-
|
|
956
|
+
function registerGetDocs(server, apiClient2) {
|
|
957
|
+
server.registerTool(
|
|
547
958
|
"get_docs",
|
|
548
959
|
{
|
|
549
960
|
title: "Get Docs",
|
|
@@ -641,8 +1052,8 @@ Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
|
641
1052
|
}
|
|
642
1053
|
|
|
643
1054
|
// src/tools/get-tech-stack.ts
|
|
644
|
-
function registerGetTechStack(
|
|
645
|
-
|
|
1055
|
+
function registerGetTechStack(server, apiClient2) {
|
|
1056
|
+
server.registerTool(
|
|
646
1057
|
"get_tech_stack",
|
|
647
1058
|
{
|
|
648
1059
|
title: "Get Tech Stack",
|
|
@@ -691,8 +1102,8 @@ ${sections}`
|
|
|
691
1102
|
|
|
692
1103
|
// src/tools/get-planning-context.ts
|
|
693
1104
|
import { z as z4 } from "zod";
|
|
694
|
-
function registerGetPlanningContext(
|
|
695
|
-
|
|
1105
|
+
function registerGetPlanningContext(server, apiClient2) {
|
|
1106
|
+
server.registerTool(
|
|
696
1107
|
"get_planning_context",
|
|
697
1108
|
{
|
|
698
1109
|
title: "Get Planning Context",
|
|
@@ -726,7 +1137,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
|
|
|
726
1137
|
${repos}`);
|
|
727
1138
|
}
|
|
728
1139
|
if (ctx.dependencies.length > 0) {
|
|
729
|
-
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
|
|
1140
|
+
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}${d.provenance === "inferred" ? " (inferred)" : ""}`).join("\n");
|
|
730
1141
|
sections.push(`## Cross-Repo Dependencies
|
|
731
1142
|
|
|
732
1143
|
${deps}`);
|
|
@@ -813,8 +1224,8 @@ function formatItem(item) {
|
|
|
813
1224
|
${meta.join(" \xB7 ")}` : "") + (item.url ? `
|
|
814
1225
|
${item.url}` : "");
|
|
815
1226
|
}
|
|
816
|
-
function registerGetPlanBoard(
|
|
817
|
-
|
|
1227
|
+
function registerGetPlanBoard(server, apiClient2) {
|
|
1228
|
+
server.registerTool(
|
|
818
1229
|
"get_plan_board",
|
|
819
1230
|
{
|
|
820
1231
|
title: "Get Plan Board",
|
|
@@ -950,8 +1361,149 @@ ${notes.join("\n")}` : "")
|
|
|
950
1361
|
);
|
|
951
1362
|
}
|
|
952
1363
|
|
|
953
|
-
// src/tools/get-
|
|
1364
|
+
// src/tools/get-change.ts
|
|
954
1365
|
import { z as z6 } from "zod";
|
|
1366
|
+
var GITHUB_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,99}";
|
|
1367
|
+
var REPO_RE = new RegExp(`^${GITHUB_NAME}/${GITHUB_NAME}$`);
|
|
1368
|
+
var SLUG_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
1369
|
+
var SLUG_MAX = 64;
|
|
1370
|
+
var PARTS = ["decision", "contract", "behaviour", "plan", "release"];
|
|
1371
|
+
var BRIEF = ["decision", "contract", "behaviour", "plan"];
|
|
1372
|
+
var PAST_REVIEW = /* @__PURE__ */ new Set(["passed", "being built", "done"]);
|
|
1373
|
+
function briefVerdict(states) {
|
|
1374
|
+
const waiting = BRIEF.filter(
|
|
1375
|
+
(part) => !(part === "decision" && states.decision === null) && !PAST_REVIEW.has(String(states[part]))
|
|
1376
|
+
).map((part) => `${part} is ${states[part] ?? "unknown"}`);
|
|
1377
|
+
return waiting.length === 0 ? "The brief is passed" : `The brief is not passed: ${waiting.join(", ")}`;
|
|
1378
|
+
}
|
|
1379
|
+
function prLink(pr) {
|
|
1380
|
+
return `[${pr.repo}#${pr.number}](${pr.url}) ${pr.state}`;
|
|
1381
|
+
}
|
|
1382
|
+
function fileSource(file) {
|
|
1383
|
+
if (file.pullRequest) return prLink(file.pullRequest);
|
|
1384
|
+
return file.direct ? "committed directly" : "\u2014";
|
|
1385
|
+
}
|
|
1386
|
+
function partPullRequests(change, part) {
|
|
1387
|
+
const files = part === "contract" ? change.contract.files : part === "behaviour" ? change.behaviour.files : part === "plan" ? [change.plan.file] : [];
|
|
1388
|
+
const prs = part === "decision" ? [change.decision?.pullRequest ?? null] : part === "release" ? change.release.prs : files.map((file) => file?.pullRequest ?? null);
|
|
1389
|
+
const unique = /* @__PURE__ */ new Map();
|
|
1390
|
+
for (const pr of prs) {
|
|
1391
|
+
if (pr && !unique.has(`${pr.repo}#${pr.number}`)) unique.set(`${pr.repo}#${pr.number}`, pr);
|
|
1392
|
+
}
|
|
1393
|
+
return [...unique.values()].sort((a, b) => Number(b.state === "open") - Number(a.state === "open"));
|
|
1394
|
+
}
|
|
1395
|
+
function countsLine(counts) {
|
|
1396
|
+
const tags = ["proposed", "built", "defect", "unbuilt", "open", "untagged"].filter((tag) => counts[tag] > 0).map((tag) => `${counts[tag]} ${tag}`);
|
|
1397
|
+
return `${counts.total} scenario(s)` + (tags.length > 0 ? `: ${tags.join(", ")}` : "");
|
|
1398
|
+
}
|
|
1399
|
+
function partsTable(change) {
|
|
1400
|
+
const rows = PARTS.filter((part) => !(part === "decision" && change.states.decision === null)).map((part) => {
|
|
1401
|
+
const prs = partPullRequests(change, part);
|
|
1402
|
+
return `| ${part} | ${change.states[part]} | ${prs.length > 0 ? prs.map(prLink).join(", ") : "\u2014"} |`;
|
|
1403
|
+
});
|
|
1404
|
+
return ["| Part | State | Pull request |", "|---|---|---|", ...rows].join("\n");
|
|
1405
|
+
}
|
|
1406
|
+
function decisionSection(change) {
|
|
1407
|
+
const decision = change.decision;
|
|
1408
|
+
if (decision === null) return "## Decision\n\nNone: the proposal names no ADR (a small change).";
|
|
1409
|
+
const status = decision.status || "no record in a connected repository";
|
|
1410
|
+
return `## Decision
|
|
1411
|
+
|
|
1412
|
+
${decision.adr} \xB7 ${status}${decision.title ? ` \u2014 ${decision.title}` : ""}`;
|
|
1413
|
+
}
|
|
1414
|
+
function contractSection(change) {
|
|
1415
|
+
const files = change.contract.files;
|
|
1416
|
+
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).");
|
|
1417
|
+
}
|
|
1418
|
+
function behaviourSection(change) {
|
|
1419
|
+
const { files, counts } = change.behaviour;
|
|
1420
|
+
if (files.length === 0) {
|
|
1421
|
+
return `## Behaviour
|
|
1422
|
+
|
|
1423
|
+
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}` : ""}.`;
|
|
1424
|
+
}
|
|
1425
|
+
const blocks = files.map(
|
|
1426
|
+
(file) => [
|
|
1427
|
+
`### ${file.path}`,
|
|
1428
|
+
"",
|
|
1429
|
+
[file.title, file.runner ? `runner ${file.runner}` : "", fileSource(file), countsLine(file.counts)].filter(Boolean).join(" \xB7 "),
|
|
1430
|
+
"",
|
|
1431
|
+
...file.scenarios.map((s) => `- ${s.id || "(no id)"} \xB7 ${s.state || "untagged"} \xB7 ${s.title}`)
|
|
1432
|
+
].join("\n")
|
|
1433
|
+
);
|
|
1434
|
+
return ["## Behaviour", countsLine(counts), ...blocks].join("\n\n");
|
|
1435
|
+
}
|
|
1436
|
+
function planSection(change) {
|
|
1437
|
+
const { file, draftId, board } = change.plan;
|
|
1438
|
+
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";
|
|
1439
|
+
return [
|
|
1440
|
+
"## Plan",
|
|
1441
|
+
"",
|
|
1442
|
+
`- plan.md: ${file ? `${file.path} \u2014 ${fileSource(file)}` : "none"}`,
|
|
1443
|
+
`- Draft: ${draftId ?? "none"}`,
|
|
1444
|
+
`- Board: ${boardLine}`
|
|
1445
|
+
].join("\n");
|
|
1446
|
+
}
|
|
1447
|
+
function releaseSection(change) {
|
|
1448
|
+
const lines = change.release.prs.map(
|
|
1449
|
+
(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")
|
|
1450
|
+
);
|
|
1451
|
+
return `## Release
|
|
1452
|
+
|
|
1453
|
+
${lines.length > 0 ? lines.join("\n") : "No release pull request is open."}`;
|
|
1454
|
+
}
|
|
1455
|
+
function formatChange(change) {
|
|
1456
|
+
const header = `# ${change.title}
|
|
1457
|
+
|
|
1458
|
+
\`${change.key}\` \xB7 size: ${change.size}` + (change.archived ? " \xB7 archived: read-only, done" : "") + ` \xB7 synced ${change.syncedAt}`;
|
|
1459
|
+
return [
|
|
1460
|
+
header,
|
|
1461
|
+
partsTable(change),
|
|
1462
|
+
decisionSection(change),
|
|
1463
|
+
contractSection(change),
|
|
1464
|
+
behaviourSection(change),
|
|
1465
|
+
planSection(change),
|
|
1466
|
+
releaseSection(change),
|
|
1467
|
+
// Compact: a change's every scenario with its steps rides in this block.
|
|
1468
|
+
"```json\n" + JSON.stringify(change) + "\n```",
|
|
1469
|
+
briefVerdict(change.states)
|
|
1470
|
+
].join("\n\n");
|
|
1471
|
+
}
|
|
1472
|
+
function registerGetChange(server, apiClient2) {
|
|
1473
|
+
server.registerTool(
|
|
1474
|
+
"get_change",
|
|
1475
|
+
{
|
|
1476
|
+
title: "Get Change",
|
|
1477
|
+
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.`,
|
|
1478
|
+
inputSchema: {
|
|
1479
|
+
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"),
|
|
1480
|
+
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>/")
|
|
1481
|
+
},
|
|
1482
|
+
annotations: { readOnlyHint: true }
|
|
1483
|
+
},
|
|
1484
|
+
async ({ repo, slug }) => {
|
|
1485
|
+
const [owner, name] = repo.split("/");
|
|
1486
|
+
let change;
|
|
1487
|
+
try {
|
|
1488
|
+
change = await apiClient2.getFresh(
|
|
1489
|
+
`/v1/changes/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/${encodeURIComponent(slug)}`
|
|
1490
|
+
);
|
|
1491
|
+
} catch (err) {
|
|
1492
|
+
if (err.status === 404) {
|
|
1493
|
+
return {
|
|
1494
|
+
content: [{ type: "text", text: `No such change ${repo}/${slug}` }],
|
|
1495
|
+
isError: true
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
throw err;
|
|
1499
|
+
}
|
|
1500
|
+
return { content: [{ type: "text", text: formatChange(change) }] };
|
|
1501
|
+
}
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// src/tools/get-context.ts
|
|
1506
|
+
import { z as z7 } from "zod";
|
|
955
1507
|
|
|
956
1508
|
// src/lib/scoring.ts
|
|
957
1509
|
var WEIGHT_PATH = 0.4;
|
|
@@ -1285,16 +1837,22 @@ async function hydrate(api, item, meta) {
|
|
|
1285
1837
|
}
|
|
1286
1838
|
async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
|
|
1287
1839
|
const budget = maxTokens ?? DEFAULT_BUDGET;
|
|
1840
|
+
const failures = [];
|
|
1841
|
+
const orEmpty = (empty) => (err) => {
|
|
1842
|
+
failures.push(err);
|
|
1843
|
+
return empty;
|
|
1844
|
+
};
|
|
1288
1845
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
1289
|
-
api.get("/v1/guardrails").catch((
|
|
1290
|
-
api.get("/v1/specs").catch((
|
|
1291
|
-
api.get("/v1/docs").catch((
|
|
1292
|
-
api.get("/v1/ai/settings/stack").catch((
|
|
1293
|
-
api.get("/v1/standards/rules").catch((
|
|
1846
|
+
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
1847
|
+
api.get("/v1/specs").catch(orEmpty([])),
|
|
1848
|
+
api.get("/v1/docs").catch(orEmpty({ items: [] })),
|
|
1849
|
+
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
1850
|
+
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
1294
1851
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
1295
1852
|
// blip) gates nothing, it can never hide working context.
|
|
1296
1853
|
api.get("/v1/doc-repos").catch(() => null)
|
|
1297
1854
|
]);
|
|
1855
|
+
if (failures.length === 5) throw failures[0];
|
|
1298
1856
|
const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
|
|
1299
1857
|
const guardrails = disabled("fitness") ? [] : rawGuardrails;
|
|
1300
1858
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
@@ -1501,47 +2059,280 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
|
|
|
1501
2059
|
guardrails: selectedGuardrails.length
|
|
1502
2060
|
};
|
|
1503
2061
|
if (sections.length === 0) {
|
|
1504
|
-
|
|
2062
|
+
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.";
|
|
2063
|
+
return { text: text3, stats };
|
|
2064
|
+
}
|
|
2065
|
+
const footer = `
|
|
2066
|
+
|
|
2067
|
+
---
|
|
2068
|
+
_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` : "") + `_`;
|
|
2069
|
+
return { text: body + footer, stats };
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
// src/tools/get-context.ts
|
|
2073
|
+
function registerGetContext(server, apiClient2) {
|
|
2074
|
+
server.registerTool(
|
|
2075
|
+
"get_context",
|
|
2076
|
+
{
|
|
2077
|
+
title: "Get Context",
|
|
2078
|
+
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.",
|
|
2079
|
+
inputSchema: {
|
|
2080
|
+
task_description: z7.string().describe(
|
|
2081
|
+
"What you're working on (e.g., 'add a new REST endpoint for user preferences')"
|
|
2082
|
+
),
|
|
2083
|
+
file_paths: z7.array(z7.string()).optional().describe(
|
|
2084
|
+
"File paths being edited \u2014 used for language/category detection"
|
|
2085
|
+
),
|
|
2086
|
+
max_tokens: z7.number().optional().describe("Maximum token budget for the response (default: 4000)")
|
|
2087
|
+
}
|
|
2088
|
+
},
|
|
2089
|
+
async ({ task_description, file_paths, max_tokens }) => {
|
|
2090
|
+
const { text: text3 } = await buildContext(apiClient2, {
|
|
2091
|
+
taskDescription: task_description,
|
|
2092
|
+
filePaths: file_paths,
|
|
2093
|
+
maxTokens: max_tokens
|
|
2094
|
+
});
|
|
2095
|
+
return {
|
|
2096
|
+
content: [{ type: "text", text: text3 }]
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// src/tools/get-team-setup.ts
|
|
2103
|
+
var HELD_BECAUSE = {
|
|
2104
|
+
"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`,
|
|
2105
|
+
"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>`",
|
|
2106
|
+
"not-checked": "it could not be checked. Ask again later"
|
|
2107
|
+
};
|
|
2108
|
+
function describeSetup(setup) {
|
|
2109
|
+
const lines = [`# Team setup of ${setup.workspace}`, ""];
|
|
2110
|
+
const skills = setup.sessionBrief?.skills ?? [];
|
|
2111
|
+
const held = setup.sessionBrief?.heldSkills ?? [];
|
|
2112
|
+
lines.push(`## Skills a session installs by itself (${skills.length})`, "");
|
|
2113
|
+
if (skills.length) {
|
|
2114
|
+
for (const s of skills) lines.push(`- **${s.name}** version ${s.latestVersion}: ${s.description}`);
|
|
2115
|
+
} else if (!setup.sessionBrief && setup.skills.length) {
|
|
2116
|
+
lines.push(`- ${setup.skills.join(", ")}`);
|
|
2117
|
+
} else {
|
|
2118
|
+
lines.push("None.");
|
|
2119
|
+
}
|
|
2120
|
+
lines.push("");
|
|
2121
|
+
if (held.length) {
|
|
2122
|
+
lines.push(`## Skills that wait for the person (${held.length})`, "");
|
|
2123
|
+
for (const h of held) lines.push(`- **${h.name}**: ${HELD_BECAUSE[h.reason] ?? h.reason}`);
|
|
2124
|
+
lines.push("", "Do not install these yourself: each is the person's to decide.", "");
|
|
2125
|
+
}
|
|
2126
|
+
if (setup.plugin) {
|
|
2127
|
+
lines.push(`## Team plugin`, "", `\`${setup.workspace}-setup\` version ${setup.plugin.version}.`);
|
|
2128
|
+
for (const c of setup.plugin.hookCommands ?? []) lines.push(`- Hook: ${JSON.stringify(c)}`);
|
|
2129
|
+
for (const m of setup.plugin.mcpServers ?? []) {
|
|
2130
|
+
lines.push(`- MCP server ${JSON.stringify(m.name)}: ${JSON.stringify(m.url ?? [m.command, ...m.args ?? []].filter(Boolean).join(" "))}`);
|
|
2131
|
+
}
|
|
2132
|
+
lines.push("");
|
|
2133
|
+
}
|
|
2134
|
+
const parts = Object.entries(setup.parts ?? {});
|
|
2135
|
+
if (parts.length) {
|
|
2136
|
+
lines.push(`## Published parts`, "", ...parts.map(([p, v]) => `- ${p}: version ${v}`), "");
|
|
2137
|
+
}
|
|
2138
|
+
if (setup.memory?.length) {
|
|
2139
|
+
lines.push(`## Team memory`, "", ...setup.memory.map((m) => `- ${m.project}: ${m.fileCount} file(s)`), "");
|
|
2140
|
+
}
|
|
2141
|
+
return lines;
|
|
2142
|
+
}
|
|
2143
|
+
function describeComputer(setup, state) {
|
|
2144
|
+
const lines = ["## This computer", ""];
|
|
2145
|
+
if (!setup.sessionBrief) {
|
|
2146
|
+
return [...lines, "This SystemDox API is too old to compare this computer with the team setup."];
|
|
2147
|
+
}
|
|
2148
|
+
const skills = setup.sessionBrief.skills;
|
|
2149
|
+
const latest = new Map(skills.map((s) => [s.name, s.latestVersion]));
|
|
2150
|
+
lines.push(
|
|
2151
|
+
state.missing.length ? `- Missing: ${state.missing.map((name) => `${name} version ${latest.get(name)}`).join(", ")}` : `- Missing: none of ${skills.length}`
|
|
2152
|
+
);
|
|
2153
|
+
if (state.behind.length) {
|
|
2154
|
+
lines.push(`- Behind: ${state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`).join(", ")}`);
|
|
2155
|
+
}
|
|
2156
|
+
if (state.notFromSystemDox.length) lines.push(`- Not from SystemDox, left alone: ${state.notFromSystemDox.join(", ")}`);
|
|
2157
|
+
if (setup.plugin) {
|
|
2158
|
+
lines.push(
|
|
2159
|
+
state.pluginInstalled ? `- Team plugin: installed${state.pluginVersion ? `, ${state.pluginVersion}` : ""}; the latest is version ${setup.plugin.version}` : "- Team plugin: not installed"
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
lines.push("", "## What to do", "");
|
|
2163
|
+
const steps = [];
|
|
2164
|
+
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.");
|
|
2165
|
+
if (state.behind.length) {
|
|
2166
|
+
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.");
|
|
2167
|
+
}
|
|
2168
|
+
if (hasPersonParts(setup) && !state.pluginInstalled) {
|
|
2169
|
+
steps.push(
|
|
2170
|
+
`Hooks, MCP servers, team instructions and memory need the person: \`${SETUP_COMMAND}\` in their own terminal. Never answer its prompts for them.`
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
lines.push(...steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ["Nothing."]);
|
|
2174
|
+
return lines;
|
|
2175
|
+
}
|
|
2176
|
+
function registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 }) {
|
|
2177
|
+
server.registerTool(
|
|
2178
|
+
"get_team_setup",
|
|
2179
|
+
{
|
|
2180
|
+
title: "Get Team Setup",
|
|
2181
|
+
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.",
|
|
2182
|
+
inputSchema: {},
|
|
2183
|
+
annotations: { readOnlyHint: true }
|
|
2184
|
+
},
|
|
2185
|
+
async () => {
|
|
2186
|
+
const read = await readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs: 15e3 });
|
|
2187
|
+
if ("problem" in read) return { content: [{ type: "text", text: read.problem }] };
|
|
2188
|
+
const lines = describeSetup(read.setup);
|
|
2189
|
+
if (localFiles) lines.push(...describeComputer(read.setup, await computerState(read.setup)));
|
|
2190
|
+
else lines.push(CANNOT_SEE);
|
|
2191
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2192
|
+
}
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// src/tools/document-graph.ts
|
|
2197
|
+
import { z as z8 } from "zod";
|
|
2198
|
+
var DEFAULT_LIMIT2 = 50;
|
|
2199
|
+
var MAX_LIMIT = 200;
|
|
2200
|
+
var DEFAULT_HOPS = 6;
|
|
2201
|
+
var MAX_HOPS = 8;
|
|
2202
|
+
function place(file, line) {
|
|
2203
|
+
return file ? `${file}:${line}` : "made by a person";
|
|
2204
|
+
}
|
|
2205
|
+
function known(provenance, confidence) {
|
|
2206
|
+
return `${provenance} ${confidence}`;
|
|
2207
|
+
}
|
|
2208
|
+
function formatNeighbours(answer) {
|
|
2209
|
+
const lines = [`# ${answer.doc.label}`, `\`${answer.doc.id}\``, ""];
|
|
2210
|
+
if (answer.links.length === 0) lines.push("No links.");
|
|
2211
|
+
for (const link of answer.links) {
|
|
2212
|
+
lines.push(
|
|
2213
|
+
`- ${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)}`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
if (answer.truncated) {
|
|
2217
|
+
lines.push("", `Showing ${answer.links.length} of ${answer.total}. Raise limit to see more.`);
|
|
2218
|
+
}
|
|
2219
|
+
if (answer.ambiguous.length > 0) {
|
|
2220
|
+
lines.push("", "## Ambiguous: not drawn, because each could mean several documents");
|
|
2221
|
+
for (const entry of answer.ambiguous) {
|
|
2222
|
+
const candidates = entry.candidates.map((c) => `${c.label} (${c.repo}, \`${c.id}\`)`);
|
|
2223
|
+
lines.push(`- ${entry.reference} at ${place(entry.file, entry.line)}: ${candidates.join("; ")}`);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return lines.join("\n");
|
|
2227
|
+
}
|
|
2228
|
+
function formatPath(answer, maxHops) {
|
|
2229
|
+
if (!answer.found) return `No path within ${maxHops} hops.`;
|
|
2230
|
+
const lines = answer.steps.map((step, index) => {
|
|
2231
|
+
const arrow = step.reversed ? "<-" : "->";
|
|
2232
|
+
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)}`;
|
|
2233
|
+
});
|
|
2234
|
+
return [...lines, "", `Path confidence: ${answer.confidence}`].join("\n");
|
|
2235
|
+
}
|
|
2236
|
+
function formatStats(stats, repoLinks) {
|
|
2237
|
+
const { declared, extracted, inferred, ambiguous } = stats.links;
|
|
2238
|
+
const lines = [
|
|
2239
|
+
`# The graph: ${stats.nodeCount} documents, ${stats.edgeCount} links`,
|
|
2240
|
+
"",
|
|
2241
|
+
`Links: ${declared} declared \xB7 ${extracted} extracted \xB7 ${inferred} inferred \xB7 ${ambiguous} ambiguous (not drawn)`,
|
|
2242
|
+
"",
|
|
2243
|
+
"## Repositories"
|
|
2244
|
+
];
|
|
2245
|
+
for (const repo of stats.repos) {
|
|
2246
|
+
const state = repo.behind ? `BEHIND: pushed ${repo.headCommit || "unknown"} at ${repo.pushedAt}, index at ${repo.indexedCommit || "unknown"}` : `index at ${repo.indexedCommit || "unknown"}`;
|
|
2247
|
+
lines.push(`- ${repo.repo}: ${repo.documents} documents, ${repo.links} links, ${state}`);
|
|
2248
|
+
}
|
|
2249
|
+
if (repoLinks.length > 0) {
|
|
2250
|
+
lines.push("", "## Repository links");
|
|
2251
|
+
for (const link of repoLinks) {
|
|
2252
|
+
lines.push(
|
|
2253
|
+
`- ${link.source} ${link.relation} ${link.target} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${place(link.file, link.line)}`
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
1505
2256
|
}
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
---
|
|
1509
|
-
_Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
|
|
1510
|
-
return { text: body + footer, stats };
|
|
2257
|
+
return lines.join("\n");
|
|
1511
2258
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
"
|
|
2259
|
+
function refused(err) {
|
|
2260
|
+
const status = err.status;
|
|
2261
|
+
if (status !== 404 && status !== 400) throw err;
|
|
2262
|
+
return {
|
|
2263
|
+
content: [{ type: "text", text: err.message }],
|
|
2264
|
+
isError: true
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
function registerDocumentGraph(server, apiClient2) {
|
|
2268
|
+
server.registerTool(
|
|
2269
|
+
"get_document_links",
|
|
1517
2270
|
{
|
|
1518
|
-
|
|
1519
|
-
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.",
|
|
2271
|
+
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.",
|
|
1520
2272
|
inputSchema: {
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
),
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
2273
|
+
doc_id: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
2274
|
+
direction: z8.enum(["out", "in", "both"]).optional().describe("Default both"),
|
|
2275
|
+
relation: z8.enum(["links_to", "cites", "mentions", "satisfies", "references"]).optional().describe("Only links of this kind"),
|
|
2276
|
+
limit: z8.number().int().min(1).max(MAX_LIMIT).optional().describe(`Most links to show (default ${DEFAULT_LIMIT2})`)
|
|
2277
|
+
},
|
|
2278
|
+
annotations: { readOnlyHint: true }
|
|
2279
|
+
},
|
|
2280
|
+
async ({ doc_id, direction, relation, limit }) => {
|
|
2281
|
+
const params = { doc: doc_id, limit: String(limit ?? DEFAULT_LIMIT2) };
|
|
2282
|
+
if (direction) params.direction = direction;
|
|
2283
|
+
if (relation) params.relation = relation;
|
|
2284
|
+
try {
|
|
2285
|
+
const answer = await apiClient2.get("/v1/graph/neighbours", params);
|
|
2286
|
+
return { content: [{ type: "text", text: formatNeighbours(answer) }] };
|
|
2287
|
+
} catch (err) {
|
|
2288
|
+
return refused(err);
|
|
1528
2289
|
}
|
|
2290
|
+
}
|
|
2291
|
+
);
|
|
2292
|
+
server.registerTool(
|
|
2293
|
+
"trace_document_path",
|
|
2294
|
+
{
|
|
2295
|
+
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.",
|
|
2296
|
+
inputSchema: {
|
|
2297
|
+
from: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
2298
|
+
to: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
2299
|
+
max_hops: z8.number().int().min(1).max(MAX_HOPS).optional().describe(`Longest path to look for (default ${DEFAULT_HOPS})`)
|
|
2300
|
+
},
|
|
2301
|
+
annotations: { readOnlyHint: true }
|
|
1529
2302
|
},
|
|
1530
|
-
async ({
|
|
1531
|
-
const
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
2303
|
+
async ({ from, to, max_hops }) => {
|
|
2304
|
+
const hops = max_hops ?? DEFAULT_HOPS;
|
|
2305
|
+
try {
|
|
2306
|
+
const answer = await apiClient2.get("/v1/graph/path", {
|
|
2307
|
+
from,
|
|
2308
|
+
to,
|
|
2309
|
+
maxHops: String(hops)
|
|
2310
|
+
});
|
|
2311
|
+
return { content: [{ type: "text", text: formatPath(answer, hops) }] };
|
|
2312
|
+
} catch (err) {
|
|
2313
|
+
return refused(err);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
);
|
|
2317
|
+
server.registerTool(
|
|
2318
|
+
"get_graph_stats",
|
|
2319
|
+
{
|
|
2320
|
+
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.",
|
|
2321
|
+
inputSchema: {},
|
|
2322
|
+
annotations: { readOnlyHint: true }
|
|
2323
|
+
},
|
|
2324
|
+
async () => {
|
|
2325
|
+
const [stats, repos] = await Promise.all([
|
|
2326
|
+
apiClient2.getFresh("/v1/graph/stats"),
|
|
2327
|
+
apiClient2.getFresh("/v1/graph/repos")
|
|
2328
|
+
]);
|
|
2329
|
+
return { content: [{ type: "text", text: formatStats(stats, repos.links) }] };
|
|
1539
2330
|
}
|
|
1540
2331
|
);
|
|
1541
2332
|
}
|
|
1542
2333
|
|
|
1543
2334
|
// src/tools/create-guardrail.ts
|
|
1544
|
-
import { z as
|
|
2335
|
+
import { z as z9 } from "zod";
|
|
1545
2336
|
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
2337
|
var CATEGORIES = [
|
|
1547
2338
|
"imports",
|
|
@@ -1554,15 +2345,15 @@ var CATEGORIES = [
|
|
|
1554
2345
|
"data-access"
|
|
1555
2346
|
];
|
|
1556
2347
|
var inputSchema2 = () => ({
|
|
1557
|
-
title:
|
|
1558
|
-
description:
|
|
1559
|
-
severity:
|
|
1560
|
-
category:
|
|
1561
|
-
pattern:
|
|
1562
|
-
fix:
|
|
1563
|
-
fitness_test:
|
|
2348
|
+
title: z9.string().describe("Short title for the check"),
|
|
2349
|
+
description: z9.string().describe("Detailed description of what the check enforces and why"),
|
|
2350
|
+
severity: z9.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
|
|
2351
|
+
category: z9.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
|
|
2352
|
+
pattern: z9.string().optional().describe("Bad code pattern that should be avoided"),
|
|
2353
|
+
fix: z9.string().optional().describe("Correct code pattern to use instead"),
|
|
2354
|
+
fitness_test: z9.string().optional().describe("Shell command to detect violations in CI")
|
|
1564
2355
|
});
|
|
1565
|
-
function registerCreateGuardrail(
|
|
2356
|
+
function registerCreateGuardrail(server, apiClient2) {
|
|
1566
2357
|
const handler = async ({
|
|
1567
2358
|
title,
|
|
1568
2359
|
description,
|
|
@@ -1599,7 +2390,7 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
1599
2390
|
]
|
|
1600
2391
|
};
|
|
1601
2392
|
};
|
|
1602
|
-
|
|
2393
|
+
server.registerTool(
|
|
1603
2394
|
"create_check",
|
|
1604
2395
|
{
|
|
1605
2396
|
title: "Create Check",
|
|
@@ -1611,7 +2402,7 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
1611
2402
|
},
|
|
1612
2403
|
handler
|
|
1613
2404
|
);
|
|
1614
|
-
|
|
2405
|
+
server.registerTool(
|
|
1615
2406
|
"create_guardrail",
|
|
1616
2407
|
{
|
|
1617
2408
|
title: "Create Check (legacy name)",
|
|
@@ -1626,20 +2417,20 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
1626
2417
|
}
|
|
1627
2418
|
|
|
1628
2419
|
// src/tools/update-spec.ts
|
|
1629
|
-
import { z as
|
|
1630
|
-
function registerUpdateSpec(
|
|
1631
|
-
|
|
2420
|
+
import { z as z10 } from "zod";
|
|
2421
|
+
function registerUpdateSpec(server, apiClient2) {
|
|
2422
|
+
server.registerTool(
|
|
1632
2423
|
"update_spec",
|
|
1633
2424
|
{
|
|
1634
2425
|
title: "Update Spec",
|
|
1635
2426
|
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
2427
|
inputSchema: {
|
|
1637
|
-
draft_id:
|
|
1638
|
-
section:
|
|
1639
|
-
content:
|
|
2428
|
+
draft_id: z10.string().describe("ID of the spec to update"),
|
|
2429
|
+
section: z10.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
|
|
2430
|
+
content: z10.string().describe(
|
|
1640
2431
|
"New markdown content for the section. For requirements, provide the full requirement content."
|
|
1641
2432
|
),
|
|
1642
|
-
requirement_name:
|
|
2433
|
+
requirement_name: z10.string().optional().describe(
|
|
1643
2434
|
"Name of the requirement (required when section is 'requirements')"
|
|
1644
2435
|
)
|
|
1645
2436
|
},
|
|
@@ -1647,253 +2438,40 @@ function registerUpdateSpec(server2, apiClient2) {
|
|
|
1647
2438
|
readOnlyHint: false
|
|
1648
2439
|
}
|
|
1649
2440
|
},
|
|
1650
|
-
async ({ draft_id, section, content, requirement_name }) => {
|
|
1651
|
-
const spec = await apiClient2.get(`/v1/specs/${draft_id}`);
|
|
1652
|
-
if (spec.status !== "draft") {
|
|
1653
|
-
return {
|
|
1654
|
-
content: [
|
|
1655
|
-
{
|
|
1656
|
-
type: "text",
|
|
1657
|
-
text: `Cannot modify spec "${spec.name}" \u2014 status is "${spec.status}". Only draft specs can be updated.`
|
|
1658
|
-
}
|
|
1659
|
-
],
|
|
1660
|
-
isError: true
|
|
1661
|
-
};
|
|
1662
|
-
}
|
|
1663
|
-
const body = {};
|
|
1664
|
-
if (section === "proposal") {
|
|
1665
|
-
body.proposalContent = content;
|
|
1666
|
-
} else if (section === "tasks") {
|
|
1667
|
-
body.taskContent = content;
|
|
1668
|
-
} else if (section === "requirements") {
|
|
1669
|
-
const existing = spec.requirements;
|
|
1670
|
-
const newReq = {
|
|
1671
|
-
name: requirement_name ?? "Untitled Requirement",
|
|
1672
|
-
content
|
|
1673
|
-
};
|
|
1674
|
-
body.requirements = [...existing, newReq];
|
|
1675
|
-
}
|
|
1676
|
-
await apiClient2.put(`/v1/specs/${draft_id}`, body);
|
|
1677
|
-
apiClient2.invalidateCache(`/v1/specs/${draft_id}`);
|
|
1678
|
-
apiClient2.invalidateCache("/v1/specs");
|
|
1679
|
-
return {
|
|
1680
|
-
content: [
|
|
1681
|
-
{
|
|
1682
|
-
type: "text",
|
|
1683
|
-
text: `Updated spec "${spec.name}" \u2014 ${section} section modified.`
|
|
1684
|
-
}
|
|
1685
|
-
]
|
|
1686
|
-
};
|
|
1687
|
-
}
|
|
1688
|
-
);
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
// src/tools/report-decision.ts
|
|
1692
|
-
import { z as z9 } from "zod";
|
|
1693
|
-
function registerReportDecision(server2, apiClient2) {
|
|
1694
|
-
server2.registerTool(
|
|
1695
|
-
"report_decision",
|
|
1696
|
-
{
|
|
1697
|
-
title: "Report Decision",
|
|
1698
|
-
description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
|
|
1699
|
-
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")
|
|
1705
|
-
},
|
|
1706
|
-
annotations: {
|
|
1707
|
-
readOnlyHint: false
|
|
1708
|
-
}
|
|
1709
|
-
},
|
|
1710
|
-
async ({ title, context, decision, consequences, related_files }) => {
|
|
1711
|
-
const adrTitle = `ADR: ${title}`;
|
|
1712
|
-
const created = await apiClient2.post("/v1/specs", {
|
|
1713
|
-
name: adrTitle
|
|
1714
|
-
});
|
|
1715
|
-
const relatedSection = related_files && related_files.length > 0 ? `
|
|
1716
|
-
|
|
1717
|
-
## Related Files
|
|
1718
|
-
|
|
1719
|
-
${related_files.map((f) => `- \`${f}\``).join("\n")}` : "";
|
|
1720
|
-
const adrContent = `# ${adrTitle}
|
|
1721
|
-
|
|
1722
|
-
**Date:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
|
|
1723
|
-
**Status:** Draft (pending review)
|
|
1724
|
-
|
|
1725
|
-
## Context
|
|
1726
|
-
|
|
1727
|
-
${context}
|
|
1728
|
-
|
|
1729
|
-
## Decision
|
|
1730
|
-
|
|
1731
|
-
${decision}
|
|
1732
|
-
|
|
1733
|
-
## Consequences
|
|
1734
|
-
|
|
1735
|
-
${consequences}` + relatedSection;
|
|
1736
|
-
await apiClient2.put(`/v1/specs/${created.draftId}`, {
|
|
1737
|
-
proposalContent: adrContent
|
|
1738
|
-
});
|
|
1739
|
-
apiClient2.invalidateCache("/v1/specs");
|
|
1740
|
-
return {
|
|
1741
|
-
content: [
|
|
1742
|
-
{
|
|
1743
|
-
type: "text",
|
|
1744
|
-
text: `Architecture decision recorded: **${adrTitle}** (spec id: ${created.draftId})
|
|
1745
|
-
|
|
1746
|
-
Status: draft \u2014 review in SystemDox before publishing.`
|
|
1747
|
-
}
|
|
1748
|
-
]
|
|
1749
|
-
};
|
|
1750
|
-
}
|
|
1751
|
-
);
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
// src/tools/get-project-rules.ts
|
|
1755
|
-
import { z as z10 } from "zod";
|
|
1756
|
-
var CATEGORY_LABELS = {
|
|
1757
|
-
deployment: "Deployment",
|
|
1758
|
-
architecture: "Architecture",
|
|
1759
|
-
security: "Security",
|
|
1760
|
-
testing: "Testing",
|
|
1761
|
-
process: "Process",
|
|
1762
|
-
other: "Other"
|
|
1763
|
-
};
|
|
1764
|
-
function registerGetProjectRules(server2, apiClient2) {
|
|
1765
|
-
server2.registerTool(
|
|
1766
|
-
"get_project_rules",
|
|
1767
|
-
{
|
|
1768
|
-
title: "Get Project Rules",
|
|
1769
|
-
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
|
-
inputSchema: {
|
|
1771
|
-
category: z10.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
|
|
1772
|
-
}
|
|
1773
|
-
},
|
|
1774
|
-
async ({ category }) => {
|
|
1775
|
-
const rules = await apiClient2.get(
|
|
1776
|
-
"/v1/standards/rules"
|
|
1777
|
-
);
|
|
1778
|
-
let filtered = rules;
|
|
1779
|
-
if (category) {
|
|
1780
|
-
filtered = filtered.filter(
|
|
1781
|
-
(r) => r.category.toLowerCase() === category.toLowerCase()
|
|
1782
|
-
);
|
|
1783
|
-
}
|
|
1784
|
-
if (filtered.length === 0) {
|
|
1785
|
-
const text4 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient2))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
|
|
1786
|
-
return {
|
|
1787
|
-
content: [{ type: "text", text: text4 }]
|
|
1788
|
-
};
|
|
1789
|
-
}
|
|
1790
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
1791
|
-
for (const rule of filtered) {
|
|
1792
|
-
const cat = rule.category || "other";
|
|
1793
|
-
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
1794
|
-
grouped.get(cat).push(rule);
|
|
1795
|
-
}
|
|
1796
|
-
let text3 = `Found ${filtered.length} project rule(s):
|
|
1797
|
-
|
|
1798
|
-
`;
|
|
1799
|
-
for (const [cat, catRules] of grouped) {
|
|
1800
|
-
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
1801
|
-
text3 += `## ${label}
|
|
1802
|
-
|
|
1803
|
-
`;
|
|
1804
|
-
for (const rule of catRules) {
|
|
1805
|
-
text3 += `- ${rule.text}
|
|
1806
|
-
`;
|
|
1807
|
-
}
|
|
1808
|
-
text3 += "\n";
|
|
1809
|
-
}
|
|
1810
|
-
return {
|
|
1811
|
-
content: [
|
|
1812
|
-
{
|
|
1813
|
-
type: "text",
|
|
1814
|
-
text: text3
|
|
1815
|
-
}
|
|
1816
|
-
]
|
|
1817
|
-
};
|
|
1818
|
-
}
|
|
1819
|
-
);
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// src/tools/create-project-rule.ts
|
|
1823
|
-
import { z as z11 } from "zod";
|
|
1824
|
-
function registerCreateProjectRule(server2, apiClient2) {
|
|
1825
|
-
server2.registerTool(
|
|
1826
|
-
"create_project_rule",
|
|
1827
|
-
{
|
|
1828
|
-
title: "Create Project Rule",
|
|
1829
|
-
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
|
-
inputSchema: {
|
|
1831
|
-
text: z11.string().describe(
|
|
1832
|
-
"The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
|
|
1833
|
-
),
|
|
1834
|
-
category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
|
|
1835
|
-
},
|
|
1836
|
-
annotations: {
|
|
1837
|
-
readOnlyHint: false
|
|
1838
|
-
}
|
|
1839
|
-
},
|
|
1840
|
-
async ({ text: text3, category }) => {
|
|
1841
|
-
const saved = await apiClient2.post(
|
|
1842
|
-
"/v1/standards/rules",
|
|
1843
|
-
{ text: text3, category }
|
|
1844
|
-
);
|
|
1845
|
-
apiClient2.invalidateCache("/v1/standards/rules");
|
|
1846
|
-
return {
|
|
1847
|
-
content: [
|
|
1848
|
-
{
|
|
1849
|
-
type: "text",
|
|
1850
|
-
text: `Project rule created: **${saved.text}** (id: ${saved.id}, category: ${saved.category})`
|
|
1851
|
-
}
|
|
1852
|
-
]
|
|
1853
|
-
};
|
|
1854
|
-
}
|
|
1855
|
-
);
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
// src/tools/get-prompt-templates.ts
|
|
1859
|
-
function registerGetPromptTemplates(server2, apiClient2) {
|
|
1860
|
-
server2.registerTool(
|
|
1861
|
-
"get_prompt_templates",
|
|
1862
|
-
{
|
|
1863
|
-
title: "Get Prompt Templates",
|
|
1864
|
-
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.",
|
|
1865
|
-
inputSchema: {}
|
|
1866
|
-
},
|
|
1867
|
-
async () => {
|
|
1868
|
-
const templates = await apiClient2.get(
|
|
1869
|
-
"/v1/standards/prompt-templates"
|
|
1870
|
-
);
|
|
1871
|
-
if (templates.length === 0) {
|
|
2441
|
+
async ({ draft_id, section, content, requirement_name }) => {
|
|
2442
|
+
const spec = await apiClient2.get(`/v1/specs/${draft_id}`);
|
|
2443
|
+
if (spec.status !== "draft") {
|
|
1872
2444
|
return {
|
|
1873
2445
|
content: [
|
|
1874
2446
|
{
|
|
1875
2447
|
type: "text",
|
|
1876
|
-
text: `
|
|
2448
|
+
text: `Cannot modify spec "${spec.name}" \u2014 status is "${spec.status}". Only draft specs can be updated.`
|
|
1877
2449
|
}
|
|
1878
|
-
]
|
|
2450
|
+
],
|
|
2451
|
+
isError: true
|
|
1879
2452
|
};
|
|
1880
2453
|
}
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2454
|
+
const body = {};
|
|
2455
|
+
if (section === "proposal") {
|
|
2456
|
+
body.proposalContent = content;
|
|
2457
|
+
} else if (section === "tasks") {
|
|
2458
|
+
body.taskContent = content;
|
|
2459
|
+
} else if (section === "requirements") {
|
|
2460
|
+
const existing = spec.requirements;
|
|
2461
|
+
const newReq = {
|
|
2462
|
+
name: requirement_name ?? "Untitled Requirement",
|
|
2463
|
+
content
|
|
2464
|
+
};
|
|
2465
|
+
body.requirements = [...existing, newReq];
|
|
2466
|
+
}
|
|
2467
|
+
await apiClient2.put(`/v1/specs/${draft_id}`, body);
|
|
2468
|
+
apiClient2.invalidateCache(`/v1/specs/${draft_id}`);
|
|
2469
|
+
apiClient2.invalidateCache("/v1/specs");
|
|
1890
2470
|
return {
|
|
1891
2471
|
content: [
|
|
1892
2472
|
{
|
|
1893
2473
|
type: "text",
|
|
1894
|
-
text: `
|
|
1895
|
-
|
|
1896
|
-
${text3}`
|
|
2474
|
+
text: `Updated spec "${spec.name}" \u2014 ${section} section modified.`
|
|
1897
2475
|
}
|
|
1898
2476
|
]
|
|
1899
2477
|
};
|
|
@@ -1901,38 +2479,62 @@ ${text3}`
|
|
|
1901
2479
|
);
|
|
1902
2480
|
}
|
|
1903
2481
|
|
|
1904
|
-
// src/tools/
|
|
1905
|
-
import { z as
|
|
1906
|
-
function
|
|
1907
|
-
|
|
1908
|
-
"
|
|
2482
|
+
// src/tools/report-decision.ts
|
|
2483
|
+
import { z as z11 } from "zod";
|
|
2484
|
+
function registerReportDecision(server, apiClient2) {
|
|
2485
|
+
server.registerTool(
|
|
2486
|
+
"report_decision",
|
|
1909
2487
|
{
|
|
1910
|
-
title: "
|
|
1911
|
-
description: "
|
|
2488
|
+
title: "Report Decision",
|
|
2489
|
+
description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
|
|
1912
2490
|
inputSchema: {
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2491
|
+
title: z11.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
|
|
2492
|
+
context: z11.string().describe("What prompted this decision \u2014 the problem or constraint"),
|
|
2493
|
+
decision: z11.string().describe("What was decided and why"),
|
|
2494
|
+
consequences: z11.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
|
|
2495
|
+
related_files: z11.array(z11.string()).optional().describe("File paths related to this decision")
|
|
1916
2496
|
},
|
|
1917
2497
|
annotations: {
|
|
1918
2498
|
readOnlyHint: false
|
|
1919
2499
|
}
|
|
1920
2500
|
},
|
|
1921
|
-
async ({
|
|
1922
|
-
const
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2501
|
+
async ({ title, context, decision, consequences, related_files }) => {
|
|
2502
|
+
const adrTitle = `ADR: ${title}`;
|
|
2503
|
+
const created = await apiClient2.post("/v1/specs", {
|
|
2504
|
+
name: adrTitle
|
|
2505
|
+
});
|
|
2506
|
+
const relatedSection = related_files && related_files.length > 0 ? `
|
|
2507
|
+
|
|
2508
|
+
## Related Files
|
|
2509
|
+
|
|
2510
|
+
${related_files.map((f) => `- \`${f}\``).join("\n")}` : "";
|
|
2511
|
+
const adrContent = `# ${adrTitle}
|
|
2512
|
+
|
|
2513
|
+
**Date:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
|
|
2514
|
+
**Status:** Draft (pending review)
|
|
2515
|
+
|
|
2516
|
+
## Context
|
|
2517
|
+
|
|
2518
|
+
${context}
|
|
2519
|
+
|
|
2520
|
+
## Decision
|
|
2521
|
+
|
|
2522
|
+
${decision}
|
|
2523
|
+
|
|
2524
|
+
## Consequences
|
|
2525
|
+
|
|
2526
|
+
${consequences}` + relatedSection;
|
|
2527
|
+
await apiClient2.put(`/v1/specs/${created.draftId}`, {
|
|
2528
|
+
proposalContent: adrContent
|
|
2529
|
+
});
|
|
2530
|
+
apiClient2.invalidateCache("/v1/specs");
|
|
1931
2531
|
return {
|
|
1932
2532
|
content: [
|
|
1933
2533
|
{
|
|
1934
2534
|
type: "text",
|
|
1935
|
-
text: `
|
|
2535
|
+
text: `Architecture decision recorded: **${adrTitle}** (spec id: ${created.draftId})
|
|
2536
|
+
|
|
2537
|
+
Status: draft \u2014 review in SystemDox before publishing.`
|
|
1936
2538
|
}
|
|
1937
2539
|
]
|
|
1938
2540
|
};
|
|
@@ -1940,232 +2542,225 @@ function registerCreatePromptTemplate(server2, apiClient2) {
|
|
|
1940
2542
|
);
|
|
1941
2543
|
}
|
|
1942
2544
|
|
|
1943
|
-
// src/tools/
|
|
1944
|
-
import { z as
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
import { homedir } from "os";
|
|
1953
|
-
import { dirname, join, relative, resolve, sep } from "path";
|
|
1954
|
-
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
1955
|
-
var ENTRY_FILE = "SKILL.md";
|
|
1956
|
-
var MAX_FILES = 100;
|
|
1957
|
-
var MAX_FILE_BYTES = 256 * 1024;
|
|
1958
|
-
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
1959
|
-
var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1960
|
-
var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
1961
|
-
var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
|
|
1962
|
-
var SkillInstallRefused = class extends Error {
|
|
1963
|
-
constructor(dest, edited) {
|
|
1964
|
-
super(
|
|
1965
|
-
`${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
|
|
1966
|
-
);
|
|
1967
|
-
this.dest = dest;
|
|
1968
|
-
this.edited = edited;
|
|
1969
|
-
}
|
|
1970
|
-
dest;
|
|
1971
|
-
edited;
|
|
2545
|
+
// src/tools/get-project-rules.ts
|
|
2546
|
+
import { z as z12 } from "zod";
|
|
2547
|
+
var CATEGORY_LABELS = {
|
|
2548
|
+
deployment: "Deployment",
|
|
2549
|
+
architecture: "Architecture",
|
|
2550
|
+
security: "Security",
|
|
2551
|
+
testing: "Testing",
|
|
2552
|
+
process: "Process",
|
|
2553
|
+
other: "Other"
|
|
1972
2554
|
};
|
|
1973
|
-
function
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
}
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
} catch {
|
|
2028
|
-
throw new Error(`${path} is not UTF-8 text`);
|
|
2029
|
-
}
|
|
2030
|
-
files.push({ path, content });
|
|
2031
|
-
}
|
|
2032
|
-
return files;
|
|
2033
|
-
}
|
|
2034
|
-
async function readSkillDirectory(directory) {
|
|
2035
|
-
const root = resolve(directory);
|
|
2036
|
-
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
2037
|
-
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
2038
|
-
}
|
|
2039
|
-
const walked = await walk(root, { skipHidden: true });
|
|
2040
|
-
if (walked.links.length) {
|
|
2041
|
-
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
2042
|
-
}
|
|
2043
|
-
const files = await readTextFiles(root, walked.files, "a skill");
|
|
2044
|
-
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
2045
|
-
const name = frontmatterName(skillMd.content);
|
|
2046
|
-
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
2047
|
-
return { name, files, skipped: walked.hidden };
|
|
2048
|
-
}
|
|
2049
|
-
function within(dest, path) {
|
|
2050
|
-
if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
|
|
2051
|
-
const full = resolve(dest, ...path.split("/"));
|
|
2052
|
-
if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
|
|
2053
|
-
return full;
|
|
2054
|
-
}
|
|
2055
|
-
async function readManifest(dest) {
|
|
2056
|
-
try {
|
|
2057
|
-
return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
|
|
2058
|
-
} catch {
|
|
2059
|
-
return void 0;
|
|
2060
|
-
}
|
|
2061
|
-
}
|
|
2062
|
-
async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
2063
|
-
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
2064
|
-
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
2065
|
-
for (const f of version.files) within(dest, f.path);
|
|
2066
|
-
if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
2067
|
-
throw new Error(`${dest} is a link: install into a real folder`);
|
|
2068
|
-
}
|
|
2069
|
-
const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
2070
|
-
if (walked.links.length) {
|
|
2071
|
-
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
2072
|
-
}
|
|
2073
|
-
const present = walked.files;
|
|
2074
|
-
const manifest = await readManifest(dest);
|
|
2075
|
-
const known = Object.fromEntries(
|
|
2076
|
-
Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
|
|
2077
|
-
);
|
|
2078
|
-
const edited = [];
|
|
2079
|
-
const untracked = [];
|
|
2080
|
-
for (const path of present) {
|
|
2081
|
-
const content = await readFile(join(dest, ...path.split("/")), "utf8");
|
|
2082
|
-
if (Object.hasOwn(known, path)) {
|
|
2083
|
-
if (sha256(content) !== known[path]) edited.push(path);
|
|
2084
|
-
} else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
|
|
2085
|
-
if (incoming.has(path) || !manifest) edited.push(path);
|
|
2086
|
-
else untracked.push(path);
|
|
2555
|
+
function registerGetProjectRules(server, apiClient2) {
|
|
2556
|
+
server.registerTool(
|
|
2557
|
+
"get_project_rules",
|
|
2558
|
+
{
|
|
2559
|
+
title: "Get Project Rules",
|
|
2560
|
+
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.",
|
|
2561
|
+
inputSchema: {
|
|
2562
|
+
category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
|
|
2563
|
+
}
|
|
2564
|
+
},
|
|
2565
|
+
async ({ category }) => {
|
|
2566
|
+
const rules = await apiClient2.get(
|
|
2567
|
+
"/v1/standards/rules"
|
|
2568
|
+
);
|
|
2569
|
+
let filtered = rules;
|
|
2570
|
+
if (category) {
|
|
2571
|
+
filtered = filtered.filter(
|
|
2572
|
+
(r) => r.category.toLowerCase() === category.toLowerCase()
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
if (filtered.length === 0) {
|
|
2576
|
+
const text4 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient2))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
|
|
2577
|
+
return {
|
|
2578
|
+
content: [{ type: "text", text: text4 }]
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2582
|
+
for (const rule of filtered) {
|
|
2583
|
+
const cat = rule.category || "other";
|
|
2584
|
+
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
2585
|
+
grouped.get(cat).push(rule);
|
|
2586
|
+
}
|
|
2587
|
+
let text3 = `Found ${filtered.length} project rule(s):
|
|
2588
|
+
|
|
2589
|
+
`;
|
|
2590
|
+
for (const [cat, catRules] of grouped) {
|
|
2591
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2592
|
+
text3 += `## ${label}
|
|
2593
|
+
|
|
2594
|
+
`;
|
|
2595
|
+
for (const rule of catRules) {
|
|
2596
|
+
text3 += `- ${rule.text}
|
|
2597
|
+
`;
|
|
2598
|
+
}
|
|
2599
|
+
text3 += "\n";
|
|
2600
|
+
}
|
|
2601
|
+
return {
|
|
2602
|
+
content: [
|
|
2603
|
+
{
|
|
2604
|
+
type: "text",
|
|
2605
|
+
text: text3
|
|
2606
|
+
}
|
|
2607
|
+
]
|
|
2608
|
+
};
|
|
2087
2609
|
}
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// src/tools/create-project-rule.ts
|
|
2614
|
+
import { z as z13 } from "zod";
|
|
2615
|
+
function registerCreateProjectRule(server, apiClient2) {
|
|
2616
|
+
server.registerTool(
|
|
2617
|
+
"create_project_rule",
|
|
2618
|
+
{
|
|
2619
|
+
title: "Create Project Rule",
|
|
2620
|
+
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.",
|
|
2621
|
+
inputSchema: {
|
|
2622
|
+
text: z13.string().describe(
|
|
2623
|
+
"The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
|
|
2624
|
+
),
|
|
2625
|
+
category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
|
|
2626
|
+
},
|
|
2627
|
+
annotations: {
|
|
2628
|
+
readOnlyHint: false
|
|
2629
|
+
}
|
|
2630
|
+
},
|
|
2631
|
+
async ({ text: text3, category }) => {
|
|
2632
|
+
const saved = await apiClient2.post(
|
|
2633
|
+
"/v1/standards/rules",
|
|
2634
|
+
{ text: text3, category }
|
|
2635
|
+
);
|
|
2636
|
+
apiClient2.invalidateCache("/v1/standards/rules");
|
|
2637
|
+
return {
|
|
2638
|
+
content: [
|
|
2639
|
+
{
|
|
2640
|
+
type: "text",
|
|
2641
|
+
text: `Project rule created: **${saved.text}** (id: ${saved.id}, category: ${saved.category})`
|
|
2642
|
+
}
|
|
2643
|
+
]
|
|
2644
|
+
};
|
|
2105
2645
|
}
|
|
2106
|
-
|
|
2107
|
-
removed.push(path);
|
|
2108
|
-
}
|
|
2109
|
-
const next = {
|
|
2110
|
-
name: version.name,
|
|
2111
|
-
version: version.version,
|
|
2112
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2113
|
-
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
2114
|
-
};
|
|
2115
|
-
await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
2116
|
-
return {
|
|
2117
|
-
dest,
|
|
2118
|
-
version: version.version,
|
|
2119
|
-
written,
|
|
2120
|
-
removed,
|
|
2121
|
-
keptUntracked: untracked.filter((p) => !incoming.has(p)),
|
|
2122
|
-
keptEdited
|
|
2123
|
-
};
|
|
2646
|
+
);
|
|
2124
2647
|
}
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2648
|
+
|
|
2649
|
+
// src/tools/get-prompt-templates.ts
|
|
2650
|
+
function registerGetPromptTemplates(server, apiClient2) {
|
|
2651
|
+
server.registerTool(
|
|
2652
|
+
"get_prompt_templates",
|
|
2653
|
+
{
|
|
2654
|
+
title: "Get Prompt Templates",
|
|
2655
|
+
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.",
|
|
2656
|
+
inputSchema: {}
|
|
2657
|
+
},
|
|
2658
|
+
async () => {
|
|
2659
|
+
const templates = await apiClient2.get(
|
|
2660
|
+
"/v1/standards/prompt-templates"
|
|
2661
|
+
);
|
|
2662
|
+
if (templates.length === 0) {
|
|
2663
|
+
return {
|
|
2664
|
+
content: [
|
|
2665
|
+
{
|
|
2666
|
+
type: "text",
|
|
2667
|
+
text: `No prompt templates found. Add templates at ${configLink("/docs/prompts", await workspaceBase(apiClient2))}`
|
|
2668
|
+
}
|
|
2669
|
+
]
|
|
2670
|
+
};
|
|
2671
|
+
}
|
|
2672
|
+
const text3 = templates.map(
|
|
2673
|
+
(t) => `## ${t.name}
|
|
2674
|
+
|
|
2675
|
+
` + (t.description ? `*${t.description}*
|
|
2676
|
+
|
|
2677
|
+
` : "") + `\`\`\`
|
|
2678
|
+
${t.prompt}
|
|
2679
|
+
\`\`\``
|
|
2680
|
+
).join("\n\n---\n\n");
|
|
2681
|
+
return {
|
|
2682
|
+
content: [
|
|
2683
|
+
{
|
|
2684
|
+
type: "text",
|
|
2685
|
+
text: `Found ${templates.length} prompt template(s):
|
|
2686
|
+
|
|
2687
|
+
${text3}`
|
|
2688
|
+
}
|
|
2689
|
+
]
|
|
2690
|
+
};
|
|
2691
|
+
}
|
|
2692
|
+
);
|
|
2128
2693
|
}
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2694
|
+
|
|
2695
|
+
// src/tools/create-prompt-template.ts
|
|
2696
|
+
import { z as z14 } from "zod";
|
|
2697
|
+
function registerCreatePromptTemplate(server, apiClient2) {
|
|
2698
|
+
server.registerTool(
|
|
2699
|
+
"create_prompt_template",
|
|
2700
|
+
{
|
|
2701
|
+
title: "Create Prompt Template",
|
|
2702
|
+
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.",
|
|
2703
|
+
inputSchema: {
|
|
2704
|
+
name: z14.string().describe("Short name for the template"),
|
|
2705
|
+
description: z14.string().optional().describe("Brief description of when to use this template"),
|
|
2706
|
+
prompt: z14.string().describe("The full prompt text that Gen AI will use when generating content")
|
|
2707
|
+
},
|
|
2708
|
+
annotations: {
|
|
2709
|
+
readOnlyHint: false
|
|
2710
|
+
}
|
|
2711
|
+
},
|
|
2712
|
+
async ({ name, description, prompt }) => {
|
|
2713
|
+
const saved = await apiClient2.post(
|
|
2714
|
+
"/v1/standards/prompt-templates",
|
|
2715
|
+
{
|
|
2716
|
+
name,
|
|
2717
|
+
description: description ?? "",
|
|
2718
|
+
prompt
|
|
2719
|
+
}
|
|
2720
|
+
);
|
|
2721
|
+
apiClient2.invalidateCache("/v1/standards/prompt-templates");
|
|
2722
|
+
return {
|
|
2723
|
+
content: [
|
|
2724
|
+
{
|
|
2725
|
+
type: "text",
|
|
2726
|
+
text: `Prompt template created: **${saved.name}** (id: ${saved.id})`
|
|
2727
|
+
}
|
|
2728
|
+
]
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
);
|
|
2140
2732
|
}
|
|
2141
2733
|
|
|
2142
2734
|
// src/tools/skills.ts
|
|
2735
|
+
import { z as z15 } from "zod";
|
|
2736
|
+
import { homedir as homedir3 } from "os";
|
|
2737
|
+
import { basename, resolve as resolve2 } from "path";
|
|
2143
2738
|
var SKILLS = "/v1/standards/skills";
|
|
2144
2739
|
function expandHome(path) {
|
|
2145
|
-
return path.replace(/^~(?=$|[\\/])/,
|
|
2740
|
+
return path.replace(/^~(?=$|[\\/])/, homedir3());
|
|
2146
2741
|
}
|
|
2147
2742
|
function text(t) {
|
|
2148
2743
|
return { content: [{ type: "text", text: t }] };
|
|
2149
2744
|
}
|
|
2150
|
-
function registerSkillTools(
|
|
2151
|
-
const fileSchema =
|
|
2152
|
-
path:
|
|
2153
|
-
content:
|
|
2745
|
+
function registerSkillTools(server, apiClient2, { localFiles }) {
|
|
2746
|
+
const fileSchema = z15.object({
|
|
2747
|
+
path: z15.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
|
|
2748
|
+
content: z15.string().describe("The file's full text, exactly as it is")
|
|
2154
2749
|
});
|
|
2155
|
-
|
|
2750
|
+
server.registerTool(
|
|
2156
2751
|
"publish_skill",
|
|
2157
2752
|
{
|
|
2158
2753
|
title: "Publish Skill",
|
|
2159
2754
|
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
2755
|
inputSchema: {
|
|
2161
2756
|
...localFiles ? {
|
|
2162
|
-
directory:
|
|
2757
|
+
directory: z15.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
|
|
2163
2758
|
} : {},
|
|
2164
|
-
files:
|
|
2165
|
-
name:
|
|
2166
|
-
spaceId:
|
|
2167
|
-
note:
|
|
2168
|
-
baseVersion:
|
|
2759
|
+
files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
|
|
2760
|
+
name: z15.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
|
|
2761
|
+
spaceId: z15.string().optional().describe("Space for a NEW skill (default: your default space)"),
|
|
2762
|
+
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")`),
|
|
2763
|
+
baseVersion: z15.number().int().positive().optional().describe(
|
|
2169
2764
|
"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
2765
|
)
|
|
2171
2766
|
},
|
|
@@ -2203,7 +2798,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2203
2798
|
);
|
|
2204
2799
|
}
|
|
2205
2800
|
);
|
|
2206
|
-
|
|
2801
|
+
server.registerTool(
|
|
2207
2802
|
"list_skills",
|
|
2208
2803
|
{
|
|
2209
2804
|
title: "List Skills",
|
|
@@ -2215,7 +2810,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2215
2810
|
const { skills } = await apiClient2.get(SKILLS);
|
|
2216
2811
|
if (!skills.length) {
|
|
2217
2812
|
return text(
|
|
2218
|
-
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/skills", await workspaceBase(apiClient2))}`
|
|
2813
|
+
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/team-setup/skills", await workspaceBase(apiClient2))}`
|
|
2219
2814
|
);
|
|
2220
2815
|
}
|
|
2221
2816
|
return text(
|
|
@@ -2225,16 +2820,16 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2225
2820
|
);
|
|
2226
2821
|
}
|
|
2227
2822
|
);
|
|
2228
|
-
|
|
2823
|
+
server.registerTool(
|
|
2229
2824
|
"install_skill",
|
|
2230
2825
|
{
|
|
2231
2826
|
title: "Install Skill",
|
|
2232
2827
|
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
2828
|
inputSchema: {
|
|
2234
|
-
name:
|
|
2235
|
-
version:
|
|
2236
|
-
target:
|
|
2237
|
-
...localFiles ? { overwrite:
|
|
2829
|
+
name: z15.string().describe("The skill's name"),
|
|
2830
|
+
version: z15.number().int().positive().optional().describe("A version number (default: the latest)"),
|
|
2831
|
+
target: z15.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
|
|
2832
|
+
...localFiles ? { overwrite: z15.boolean().optional().describe("Replace files edited since they were installed") } : {}
|
|
2238
2833
|
},
|
|
2239
2834
|
annotations: { readOnlyHint: !localFiles }
|
|
2240
2835
|
},
|
|
@@ -2280,11 +2875,11 @@ ${body}`
|
|
|
2280
2875
|
}
|
|
2281
2876
|
|
|
2282
2877
|
// src/tools/team-setup.ts
|
|
2283
|
-
import { z as
|
|
2284
|
-
import { existsSync as
|
|
2878
|
+
import { z as z16 } from "zod";
|
|
2879
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2285
2880
|
import { lstat as lstat2 } from "fs/promises";
|
|
2286
2881
|
import { homedir as homedir4 } from "os";
|
|
2287
|
-
import { join as
|
|
2882
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
2288
2883
|
|
|
2289
2884
|
// src/lib/person-values.ts
|
|
2290
2885
|
var PHONE = "phone";
|
|
@@ -2361,13 +2956,13 @@ function findSecrets(text3) {
|
|
|
2361
2956
|
}
|
|
2362
2957
|
|
|
2363
2958
|
// src/lib/team-setup-types.ts
|
|
2364
|
-
var
|
|
2959
|
+
var PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
2365
2960
|
var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
2366
2961
|
|
|
2367
2962
|
// src/sync-memory.ts
|
|
2368
|
-
import { existsSync as
|
|
2369
|
-
import { mkdir as mkdir3, readdir as readdir2, readFile as
|
|
2370
|
-
import { dirname as dirname3, join as
|
|
2963
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2964
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
2965
|
+
import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
|
|
2371
2966
|
|
|
2372
2967
|
// src/lib/memory-sync.ts
|
|
2373
2968
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -2638,63 +3233,6 @@ function lineDiff(before, after) {
|
|
|
2638
3233
|
return out;
|
|
2639
3234
|
}
|
|
2640
3235
|
|
|
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
3236
|
// src/sync-memory.ts
|
|
2699
3237
|
var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
|
|
2700
3238
|
var SyncFailed = class extends Error {
|
|
@@ -2704,7 +3242,7 @@ function refusal(e) {
|
|
|
2704
3242
|
const { status, code } = e;
|
|
2705
3243
|
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
2706
3244
|
}
|
|
2707
|
-
var toPath = (root, rel) =>
|
|
3245
|
+
var toPath = (root, rel) => join4(root, ...rel.split("/"));
|
|
2708
3246
|
async function readFolder(root) {
|
|
2709
3247
|
const files = {};
|
|
2710
3248
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -2712,14 +3250,14 @@ async function readFolder(root) {
|
|
|
2712
3250
|
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
2713
3251
|
if (e.name.startsWith(".")) continue;
|
|
2714
3252
|
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
2715
|
-
if (e.isDirectory()) await walk2(
|
|
3253
|
+
if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
|
|
2716
3254
|
else if (e.isFile()) {
|
|
2717
3255
|
names.add(rel.toLowerCase());
|
|
2718
|
-
if (e.name.endsWith(".md")) files[rel] = await
|
|
3256
|
+
if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
|
|
2719
3257
|
}
|
|
2720
3258
|
}
|
|
2721
3259
|
}
|
|
2722
|
-
if (
|
|
3260
|
+
if (existsSync3(root)) await walk2(root, "");
|
|
2723
3261
|
return { files, names };
|
|
2724
3262
|
}
|
|
2725
3263
|
async function pullTeam(client, ws, project) {
|
|
@@ -2928,11 +3466,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
2928
3466
|
report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
|
|
2929
3467
|
break;
|
|
2930
3468
|
}
|
|
2931
|
-
const
|
|
2932
|
-
if (
|
|
2933
|
-
out(` ${
|
|
3469
|
+
const refused2 = result.refused[0];
|
|
3470
|
+
if (refused2?.code === "SYD-K021") {
|
|
3471
|
+
out(` ${refused2.detail}. Add them in Staff first, or answer p to file them.`);
|
|
2934
3472
|
choices = choices.filter((c) => c !== "m");
|
|
2935
|
-
} else if (
|
|
3473
|
+
} else if (refused2) out(` ${refused2.detail}`);
|
|
2936
3474
|
}
|
|
2937
3475
|
}
|
|
2938
3476
|
for (let i = 0; i < filed.length; i += 50) {
|
|
@@ -3076,7 +3614,7 @@ async function syncMemory(opts) {
|
|
|
3076
3614
|
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.`
|
|
3077
3615
|
);
|
|
3078
3616
|
}
|
|
3079
|
-
const dir = resolve3(opts.dir ??
|
|
3617
|
+
const dir = resolve3(opts.dir ?? join4(workspaceDir(ws), "memory", project));
|
|
3080
3618
|
const run2 = { ...opts, dir };
|
|
3081
3619
|
const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
|
|
3082
3620
|
const report = new Report();
|
|
@@ -3226,21 +3764,21 @@ async function readPartDirectory(part, directory) {
|
|
|
3226
3764
|
let present = true;
|
|
3227
3765
|
for (let i = 1; i <= segments.length; i++) {
|
|
3228
3766
|
const rel = segments.slice(0, i).join("/");
|
|
3229
|
-
if (await isLink(
|
|
3767
|
+
if (await isLink(join5(root, ...segments.slice(0, i)))) {
|
|
3230
3768
|
links.push(rel);
|
|
3231
3769
|
present = false;
|
|
3232
3770
|
break;
|
|
3233
3771
|
}
|
|
3234
3772
|
}
|
|
3235
|
-
if (present &&
|
|
3773
|
+
if (present && existsSync4(join5(root, ...segments))) paths.push(path);
|
|
3236
3774
|
}
|
|
3237
3775
|
for (const { dir, md } of layout.folders) {
|
|
3238
|
-
const full =
|
|
3776
|
+
const full = join5(root, dir);
|
|
3239
3777
|
if (await isLink(full)) {
|
|
3240
3778
|
links.push(dir);
|
|
3241
3779
|
continue;
|
|
3242
3780
|
}
|
|
3243
|
-
if (!
|
|
3781
|
+
if (!existsSync4(full)) continue;
|
|
3244
3782
|
const walked = await walk(full, { skipHidden: true });
|
|
3245
3783
|
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
3246
3784
|
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
@@ -3279,24 +3817,24 @@ function describePerson(id, person) {
|
|
|
3279
3817
|
);
|
|
3280
3818
|
return lines.join("\n");
|
|
3281
3819
|
}
|
|
3282
|
-
function registerTeamSetupTools(
|
|
3283
|
-
const fileSchema =
|
|
3284
|
-
path:
|
|
3285
|
-
content:
|
|
3820
|
+
function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
3821
|
+
const fileSchema = z16.object({
|
|
3822
|
+
path: z16.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
|
|
3823
|
+
content: z16.string().describe("The file's full text, exactly as it is")
|
|
3286
3824
|
});
|
|
3287
|
-
|
|
3825
|
+
server.registerTool(
|
|
3288
3826
|
"publish_setup_part",
|
|
3289
3827
|
{
|
|
3290
3828
|
title: "Publish Setup Part",
|
|
3291
3829
|
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
3830
|
inputSchema: {
|
|
3293
|
-
part:
|
|
3831
|
+
part: z16.enum(PARTS2).describe("Which part of the setup"),
|
|
3294
3832
|
...localFiles ? {
|
|
3295
|
-
directory:
|
|
3833
|
+
directory: z16.string().optional().describe(
|
|
3296
3834
|
"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
3835
|
)
|
|
3298
3836
|
} : {},
|
|
3299
|
-
files:
|
|
3837
|
+
files: z16.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
|
|
3300
3838
|
},
|
|
3301
3839
|
annotations: { readOnlyHint: false }
|
|
3302
3840
|
},
|
|
@@ -3326,13 +3864,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3326
3864
|
return text2(lines.join("\n"));
|
|
3327
3865
|
}
|
|
3328
3866
|
);
|
|
3329
|
-
|
|
3867
|
+
server.registerTool(
|
|
3330
3868
|
"resolve_person",
|
|
3331
3869
|
{
|
|
3332
3870
|
title: "Resolve Person",
|
|
3333
3871
|
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
3872
|
inputSchema: {
|
|
3335
|
-
id:
|
|
3873
|
+
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")
|
|
3336
3874
|
},
|
|
3337
3875
|
annotations: { readOnlyHint: true }
|
|
3338
3876
|
},
|
|
@@ -3343,14 +3881,14 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3343
3881
|
}
|
|
3344
3882
|
);
|
|
3345
3883
|
if (!localFiles) return;
|
|
3346
|
-
|
|
3884
|
+
server.registerTool(
|
|
3347
3885
|
"sync_memory",
|
|
3348
3886
|
{
|
|
3349
3887
|
title: "Sync Memory",
|
|
3350
3888
|
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
3889
|
inputSchema: {
|
|
3352
|
-
project:
|
|
3353
|
-
dryRun:
|
|
3890
|
+
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"),
|
|
3891
|
+
dryRun: z16.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
|
|
3354
3892
|
},
|
|
3355
3893
|
annotations: { readOnlyHint: false }
|
|
3356
3894
|
},
|
|
@@ -3368,37 +3906,56 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3368
3906
|
}
|
|
3369
3907
|
|
|
3370
3908
|
// src/server.ts
|
|
3371
|
-
var SERVER_VERSION = true ? "0.1.
|
|
3372
|
-
function createServer(apiClient2, { localFiles = false } = {}) {
|
|
3373
|
-
const
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3909
|
+
var SERVER_VERSION = true ? "0.1.50" : "0.0.0-dev";
|
|
3910
|
+
function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
|
|
3911
|
+
const server = new McpServer(
|
|
3912
|
+
{
|
|
3913
|
+
name: "systemdox-mcp-server",
|
|
3914
|
+
version: SERVER_VERSION
|
|
3915
|
+
},
|
|
3916
|
+
instructions ? { instructions } : void 0
|
|
3917
|
+
);
|
|
3918
|
+
registerGetGuardrails(server, apiClient2);
|
|
3919
|
+
registerGetSpecs(server, apiClient2);
|
|
3920
|
+
registerGetDocs(server, apiClient2);
|
|
3921
|
+
registerGetTechStack(server, apiClient2);
|
|
3922
|
+
registerGetPlanningContext(server, apiClient2);
|
|
3923
|
+
registerGetPlanBoard(server, apiClient2);
|
|
3924
|
+
registerGetChange(server, apiClient2);
|
|
3925
|
+
registerGetContext(server, apiClient2);
|
|
3926
|
+
registerGetProjectRules(server, apiClient2);
|
|
3927
|
+
registerGetPromptTemplates(server, apiClient2);
|
|
3928
|
+
registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 });
|
|
3929
|
+
registerDocumentGraph(server, apiClient2);
|
|
3930
|
+
registerCreateGuardrail(server, apiClient2);
|
|
3931
|
+
registerUpdateSpec(server, apiClient2);
|
|
3932
|
+
registerReportDecision(server, apiClient2);
|
|
3933
|
+
registerCreateProjectRule(server, apiClient2);
|
|
3934
|
+
registerCreatePromptTemplate(server, apiClient2);
|
|
3935
|
+
registerSkillTools(server, apiClient2, { localFiles });
|
|
3936
|
+
registerTeamSetupTools(server, apiClient2, { localFiles });
|
|
3937
|
+
return server;
|
|
3394
3938
|
}
|
|
3395
3939
|
|
|
3396
3940
|
// src/index.ts
|
|
3397
|
-
var config
|
|
3941
|
+
var config;
|
|
3942
|
+
var keyProblem;
|
|
3943
|
+
try {
|
|
3944
|
+
config = await loadLocalConfig();
|
|
3945
|
+
} catch (err) {
|
|
3946
|
+
console.error(`SystemDox MCP server: ${err.message}`);
|
|
3947
|
+
keyProblem = keyReason(err.message);
|
|
3948
|
+
try {
|
|
3949
|
+
config = loadConfig(NO_KEY);
|
|
3950
|
+
} catch (urlErr) {
|
|
3951
|
+
console.error(`SystemDox MCP server: ${urlErr.message}`);
|
|
3952
|
+
process.exit(1);
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3398
3955
|
var apiClient = new SystemDoxApiClient(config);
|
|
3399
|
-
var server = createServer(apiClient, { localFiles: true });
|
|
3400
3956
|
async function main() {
|
|
3401
|
-
await apiClient
|
|
3957
|
+
const instructions = await sessionInstructions(apiClient, { localFiles: true, keyProblem });
|
|
3958
|
+
const server = createServer(apiClient, { localFiles: true, instructions, keyProblem });
|
|
3402
3959
|
const transport = new StdioServerTransport();
|
|
3403
3960
|
await server.connect(transport);
|
|
3404
3961
|
}
|