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