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