@systemdox/mcp-server 0.1.49 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/dist/cli.js +565 -161
- package/dist/http.js +1102 -728
- package/dist/index.d.ts +2 -2
- package/dist/index.js +747 -370
- package/dist/install-skill.js +9 -15
- package/dist/lib/api-client.d.ts +2 -3
- package/dist/lib/api-client.js +7 -14
- package/dist/lib/context-builder.js +14 -7
- package/dist/lib/session-brief.d.ts +59 -0
- package/dist/lib/session-brief.js +174 -0
- package/dist/lib/skills.js +2 -1
- package/dist/lib/team-setup-types.d.ts +18 -0
- package/dist/plugin-path.js +7 -14
- package/dist/server.d.ts +5 -1
- package/dist/server.js +3579 -0
- package/dist/setup-mcp.js +9 -15
- package/dist/setup-team.js +7 -14
- package/dist/sync-memory.js +7 -14
- package/dist/tools/document-graph.d.ts +87 -0
- package/dist/tools/get-team-setup.d.ts +6 -0
- package/dist/tools/skills.js +2 -1
- package/dist/tools/team-setup.js +2 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -63,14 +63,6 @@ var SystemDoxApiClient = class {
|
|
|
63
63
|
this.config = config2;
|
|
64
64
|
this.cache = new ResponseCache(config2.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,25 +212,19 @@ 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
|
};
|
|
234
227
|
|
|
235
|
-
// src/lib/team-setup-files.ts
|
|
236
|
-
import { execFile } from "child_process";
|
|
237
|
-
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
238
|
-
import { homedir, userInfo } from "os";
|
|
239
|
-
import { dirname, join } from "path";
|
|
240
|
-
import { promisify } from "util";
|
|
241
|
-
|
|
242
228
|
// src/lib/config.ts
|
|
243
229
|
var API_URL_DEFAULT = "https://api.puglieseweb.com/systemdox";
|
|
244
230
|
function requireHttpsApiUrl(apiUrl) {
|
|
@@ -274,31 +260,236 @@ function loadConfig(apiKey = process.env.SYSTEMDOX_API_KEY) {
|
|
|
274
260
|
};
|
|
275
261
|
}
|
|
276
262
|
|
|
263
|
+
// src/lib/session-brief.ts
|
|
264
|
+
import { existsSync as existsSync2 } from "fs";
|
|
265
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
266
|
+
import { join as join3 } from "path";
|
|
267
|
+
|
|
268
|
+
// src/lib/skills.ts
|
|
269
|
+
import { createHash } from "crypto";
|
|
270
|
+
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
271
|
+
import { existsSync } from "fs";
|
|
272
|
+
import { homedir } from "os";
|
|
273
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
274
|
+
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
275
|
+
var ENTRY_FILE = "SKILL.md";
|
|
276
|
+
var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
|
|
277
|
+
var MAX_FILES = 100;
|
|
278
|
+
var MAX_FILE_BYTES = 256 * 1024;
|
|
279
|
+
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
280
|
+
var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
281
|
+
var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
282
|
+
var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
|
|
283
|
+
var SkillInstallRefused = class extends Error {
|
|
284
|
+
constructor(dest, edited) {
|
|
285
|
+
super(
|
|
286
|
+
`${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
|
|
287
|
+
);
|
|
288
|
+
this.dest = dest;
|
|
289
|
+
this.edited = edited;
|
|
290
|
+
}
|
|
291
|
+
dest;
|
|
292
|
+
edited;
|
|
293
|
+
};
|
|
294
|
+
function sha256(content) {
|
|
295
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
296
|
+
}
|
|
297
|
+
function isValidName(name) {
|
|
298
|
+
return name.length <= 64 && NAME_RE.test(name);
|
|
299
|
+
}
|
|
300
|
+
function isValidPath(path) {
|
|
301
|
+
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
|
|
302
|
+
}
|
|
303
|
+
function frontmatterName(skillMd) {
|
|
304
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
305
|
+
if (!block) return void 0;
|
|
306
|
+
const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
|
|
307
|
+
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
308
|
+
}
|
|
309
|
+
function defaultSkillDir(name) {
|
|
310
|
+
return join(homedir(), ".claude", "skills", name);
|
|
311
|
+
}
|
|
312
|
+
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
313
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
314
|
+
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
315
|
+
const full = join(dir, entry.name);
|
|
316
|
+
const rel = relative(root, full).split(sep).join("/");
|
|
317
|
+
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
318
|
+
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
319
|
+
else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
|
|
320
|
+
else if (entry.isFile()) out.files.push(rel);
|
|
321
|
+
}
|
|
322
|
+
out.files.sort();
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
async function readTextFiles(root, paths, what) {
|
|
326
|
+
if (paths.length > MAX_FILES) {
|
|
327
|
+
throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
|
|
328
|
+
}
|
|
329
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
330
|
+
const files = [];
|
|
331
|
+
let total = 0;
|
|
332
|
+
for (const path of paths) {
|
|
333
|
+
if (!isValidPath(path)) {
|
|
334
|
+
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
335
|
+
}
|
|
336
|
+
const raw = await readFile(join(root, ...path.split("/")));
|
|
337
|
+
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
338
|
+
if (raw.length > MAX_FILE_BYTES) {
|
|
339
|
+
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
340
|
+
}
|
|
341
|
+
total += raw.length;
|
|
342
|
+
if (total > MAX_TOTAL_BYTES) {
|
|
343
|
+
throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
|
|
344
|
+
}
|
|
345
|
+
let content;
|
|
346
|
+
try {
|
|
347
|
+
content = decoder.decode(raw);
|
|
348
|
+
} catch {
|
|
349
|
+
throw new Error(`${path} is not UTF-8 text`);
|
|
350
|
+
}
|
|
351
|
+
files.push({ path, content });
|
|
352
|
+
}
|
|
353
|
+
return files;
|
|
354
|
+
}
|
|
355
|
+
async function readSkillDirectory(directory) {
|
|
356
|
+
const root = resolve(directory);
|
|
357
|
+
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
358
|
+
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
359
|
+
}
|
|
360
|
+
const walked = await walk(root, { skipHidden: true });
|
|
361
|
+
if (walked.links.length) {
|
|
362
|
+
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
363
|
+
}
|
|
364
|
+
const files = await readTextFiles(root, walked.files, "a skill");
|
|
365
|
+
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
366
|
+
const name = frontmatterName(skillMd.content);
|
|
367
|
+
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
368
|
+
return { name, files, skipped: walked.hidden };
|
|
369
|
+
}
|
|
370
|
+
function within(dest, path) {
|
|
371
|
+
if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
|
|
372
|
+
const full = resolve(dest, ...path.split("/"));
|
|
373
|
+
if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
|
|
374
|
+
return full;
|
|
375
|
+
}
|
|
376
|
+
async function readManifest(dest) {
|
|
377
|
+
try {
|
|
378
|
+
return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
|
|
379
|
+
} catch {
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
384
|
+
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
385
|
+
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
386
|
+
for (const f of version.files) within(dest, f.path);
|
|
387
|
+
if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
388
|
+
throw new Error(`${dest} is a link: install into a real folder`);
|
|
389
|
+
}
|
|
390
|
+
const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
391
|
+
if (walked.links.length) {
|
|
392
|
+
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
393
|
+
}
|
|
394
|
+
const present = walked.files;
|
|
395
|
+
const manifest = await readManifest(dest);
|
|
396
|
+
const known2 = Object.fromEntries(
|
|
397
|
+
Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
|
|
398
|
+
);
|
|
399
|
+
const edited = [];
|
|
400
|
+
const untracked = [];
|
|
401
|
+
for (const path of present) {
|
|
402
|
+
const content = await readFile(join(dest, ...path.split("/")), "utf8");
|
|
403
|
+
if (Object.hasOwn(known2, path)) {
|
|
404
|
+
if (sha256(content) !== known2[path]) edited.push(path);
|
|
405
|
+
} else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
|
|
406
|
+
if (incoming.has(path) || !manifest) edited.push(path);
|
|
407
|
+
else untracked.push(path);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const blocking = edited.filter((p) => incoming.has(p) || !manifest);
|
|
411
|
+
if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
|
|
412
|
+
const written = [];
|
|
413
|
+
for (const f of version.files) {
|
|
414
|
+
const full = within(dest, f.path);
|
|
415
|
+
await mkdir(dirname(full), { recursive: true });
|
|
416
|
+
await writeFile(full, f.content, "utf8");
|
|
417
|
+
written.push(f.path);
|
|
418
|
+
}
|
|
419
|
+
const removed = [];
|
|
420
|
+
const keptEdited = [];
|
|
421
|
+
for (const path of Object.keys(known2)) {
|
|
422
|
+
if (incoming.has(path) || !present.includes(path)) continue;
|
|
423
|
+
if (edited.includes(path)) {
|
|
424
|
+
keptEdited.push(path);
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
await rm(within(dest, path));
|
|
428
|
+
removed.push(path);
|
|
429
|
+
}
|
|
430
|
+
const next = {
|
|
431
|
+
name: version.name,
|
|
432
|
+
version: version.version,
|
|
433
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
434
|
+
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
435
|
+
};
|
|
436
|
+
await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
437
|
+
return {
|
|
438
|
+
dest,
|
|
439
|
+
version: version.version,
|
|
440
|
+
written,
|
|
441
|
+
removed,
|
|
442
|
+
keptUntracked: untracked.filter((p) => !incoming.has(p)),
|
|
443
|
+
keptEdited
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function fenceFor(text3) {
|
|
447
|
+
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
448
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
449
|
+
}
|
|
450
|
+
function describeInstall(name, r) {
|
|
451
|
+
const lines = [
|
|
452
|
+
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
453
|
+
];
|
|
454
|
+
if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
|
|
455
|
+
if (r.keptEdited.length) {
|
|
456
|
+
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
457
|
+
}
|
|
458
|
+
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
459
|
+
lines.push("Start a new Claude Code session to load it.");
|
|
460
|
+
return lines.join("\n");
|
|
461
|
+
}
|
|
462
|
+
|
|
277
463
|
// src/lib/team-setup-files.ts
|
|
464
|
+
import { execFile } from "child_process";
|
|
465
|
+
import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
466
|
+
import { homedir as homedir2, userInfo } from "os";
|
|
467
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
468
|
+
import { promisify } from "util";
|
|
278
469
|
var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
279
470
|
function isWorkspace(value) {
|
|
280
471
|
return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
|
|
281
472
|
}
|
|
282
473
|
function homeDir() {
|
|
283
|
-
return
|
|
474
|
+
return homedir2();
|
|
284
475
|
}
|
|
285
476
|
function systemdoxDir() {
|
|
286
|
-
return
|
|
477
|
+
return join2(homeDir(), ".claude", "systemdox");
|
|
287
478
|
}
|
|
288
479
|
function credentialsPath() {
|
|
289
|
-
return
|
|
480
|
+
return join2(systemdoxDir(), "credentials.json");
|
|
290
481
|
}
|
|
291
482
|
function workspaceDir(workspace) {
|
|
292
483
|
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
293
|
-
return
|
|
484
|
+
return join2(systemdoxDir(), workspace);
|
|
294
485
|
}
|
|
295
486
|
function statePath(workspace) {
|
|
296
|
-
return
|
|
487
|
+
return join2(workspaceDir(workspace), "state.json");
|
|
297
488
|
}
|
|
298
489
|
async function readJson(path, empty) {
|
|
299
490
|
let text3;
|
|
300
491
|
try {
|
|
301
|
-
text3 = await
|
|
492
|
+
text3 = await readFile2(path, "utf8");
|
|
302
493
|
} catch (e) {
|
|
303
494
|
if (e.code === "ENOENT") return empty;
|
|
304
495
|
throw e;
|
|
@@ -310,9 +501,9 @@ async function readJson(path, empty) {
|
|
|
310
501
|
}
|
|
311
502
|
}
|
|
312
503
|
async function writeJsonAtomically(path, value, mode) {
|
|
313
|
-
await
|
|
504
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
314
505
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
315
|
-
await
|
|
506
|
+
await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
|
|
316
507
|
await rename(tmp, path);
|
|
317
508
|
}
|
|
318
509
|
async function readCredentials() {
|
|
@@ -357,6 +548,137 @@ async function updateState(workspace, change) {
|
|
|
357
548
|
return state;
|
|
358
549
|
}
|
|
359
550
|
|
|
551
|
+
// src/lib/session-brief.ts
|
|
552
|
+
var INSTRUCTIONS_MAX = 2e3;
|
|
553
|
+
var BRIEF_TIMEOUT_MS = 8e3;
|
|
554
|
+
var NO_KEY = "no-key-on-this-computer";
|
|
555
|
+
var SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
|
|
556
|
+
var REFUSED = `SystemDox refused this computer's key, so the team's rules and skills cannot be read and every SystemDox tool fails. Tell the person once: a new key comes from the SystemDox console, Settings \u203A API keys, and \`${SETUP_COMMAND}\` in their own terminal stores it.`;
|
|
557
|
+
var NOT_CHECKED = "SystemDox could not be read when this session started, so the team's skills were not checked. Call get_team_setup to check again before relying on them.";
|
|
558
|
+
var CANNOT_SEE = "This server cannot see this computer's disk. For each skill listed under Team skills: if ~/.claude/skills/<name>/ is absent, or its .systemdox-skill.json names another version than get_team_setup lists, call install_skill with that version and write what it returns, unless the latest team plugin carries it. Leave a folder with no such file alone. If a file there differs from the hash that file holds for it, it was edited: ask the person before replacing it.";
|
|
559
|
+
function noKey(reason) {
|
|
560
|
+
return `This computer has no SystemDox key it can use (${reason}), so the team's rules and skills cannot be read and every SystemDox tool fails. Tell the person once: \`${SETUP_COMMAND}\` in their own terminal asks for one.`;
|
|
561
|
+
}
|
|
562
|
+
function keyReason(message) {
|
|
563
|
+
if (/holds keys for/.test(message)) return "keys for several workspaces are stored; set SYSTEMDOX_WORKSPACE to the one to use";
|
|
564
|
+
if (/holds no key for/.test(message)) return "SYSTEMDOX_WORKSPACE names a workspace with no stored key";
|
|
565
|
+
if (/No SystemDox key/.test(message)) return "none is stored";
|
|
566
|
+
return "the stored keys could not be read";
|
|
567
|
+
}
|
|
568
|
+
function olderApi(setup) {
|
|
569
|
+
return `SystemDox workspace ${setup.workspace}. Before writing code in one of its repositories, call get_context with the task and the files you expect to touch, and obey what it returns. get_team_setup lists the team setup.`;
|
|
570
|
+
}
|
|
571
|
+
async function readJsonQuietly(path) {
|
|
572
|
+
try {
|
|
573
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
574
|
+
} catch {
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async function computerState(setup) {
|
|
579
|
+
const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
|
|
580
|
+
const installed = await readJsonQuietly(join3(homeDir(), ".claude", "plugins", "installed_plugins.json"));
|
|
581
|
+
const entries2 = installed?.plugins?.[id];
|
|
582
|
+
const entry = Array.isArray(entries2) ? entries2.find((e) => (e?.scope ?? "user") === "user") : void 0;
|
|
583
|
+
const state = {
|
|
584
|
+
missing: [],
|
|
585
|
+
behind: [],
|
|
586
|
+
notFromSystemDox: [],
|
|
587
|
+
pluginInstalled: !!entry,
|
|
588
|
+
pluginVersion: entry?.version
|
|
589
|
+
};
|
|
590
|
+
const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
|
|
591
|
+
for (const skill of setup.sessionBrief?.skills ?? []) {
|
|
592
|
+
const dir = defaultSkillDir(skill.name);
|
|
593
|
+
if (!existsSync2(dir)) {
|
|
594
|
+
if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const manifest = await readJsonQuietly(join3(dir, MANIFEST_FILE));
|
|
598
|
+
if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
|
|
599
|
+
state.notFromSystemDox.push(skill.name);
|
|
600
|
+
} else if (manifest.version !== skill.latestVersion) {
|
|
601
|
+
state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return state;
|
|
605
|
+
}
|
|
606
|
+
function hasPersonParts(setup) {
|
|
607
|
+
return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
|
|
608
|
+
}
|
|
609
|
+
var MORE = (n) => `${n} more (get_team_setup lists them)`;
|
|
610
|
+
var LIST_MIN = `all ${MORE(100)}`.length;
|
|
611
|
+
function listWithin(items, room) {
|
|
612
|
+
const whole2 = items.join(", ");
|
|
613
|
+
if (whole2.length <= room) return whole2;
|
|
614
|
+
const kept = [];
|
|
615
|
+
let used = 0;
|
|
616
|
+
for (let i = 0; i < items.length; i++) {
|
|
617
|
+
const rest = items.length - i - 1;
|
|
618
|
+
const cost = items[i].length + (kept.length ? 2 : 0);
|
|
619
|
+
if (used + cost + ` and ${MORE(rest)}`.length > room) break;
|
|
620
|
+
kept.push(items[i]);
|
|
621
|
+
used += cost;
|
|
622
|
+
}
|
|
623
|
+
return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
|
|
624
|
+
}
|
|
625
|
+
function localSection(setup, state, room) {
|
|
626
|
+
const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
|
|
627
|
+
const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
|
|
628
|
+
const lines = [];
|
|
629
|
+
if (state.missing.length) {
|
|
630
|
+
lines.push({
|
|
631
|
+
head: "On this computer, missing: ",
|
|
632
|
+
items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
|
|
633
|
+
tail: `. Install each now with install_skill at that version, before your first answer. Then tell the person what you installed${rest}. When a skill's line matches the request and it is not loaded yet, read ~/.claude/skills/<name>/SKILL.md and follow it.`
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
if (state.behind.length) {
|
|
637
|
+
lines.push({
|
|
638
|
+
head: "On this computer, behind: ",
|
|
639
|
+
items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
|
|
640
|
+
tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
if (state.notFromSystemDox.length) {
|
|
644
|
+
lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
|
|
645
|
+
}
|
|
646
|
+
const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
|
|
647
|
+
while (lines.length && fixed() > room) lines.pop();
|
|
648
|
+
if (!lines.length) return "";
|
|
649
|
+
const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
|
|
650
|
+
return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
|
|
651
|
+
}
|
|
652
|
+
function fit(text3, section) {
|
|
653
|
+
const whole2 = section ? `${text3}
|
|
654
|
+
|
|
655
|
+
${section}` : text3;
|
|
656
|
+
if (whole2.length <= INSTRUCTIONS_MAX) return whole2;
|
|
657
|
+
return text3.length <= INSTRUCTIONS_MAX ? text3 : `${text3.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
|
|
658
|
+
}
|
|
659
|
+
async function readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
|
|
660
|
+
if (keyProblem2) return { problem: noKey(keyProblem2) };
|
|
661
|
+
try {
|
|
662
|
+
const setup = await apiClient2.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
|
|
663
|
+
return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
|
|
664
|
+
} catch (err) {
|
|
665
|
+
const status = err?.status;
|
|
666
|
+
return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async function sessionInstructions(apiClient2, options = {}) {
|
|
670
|
+
const read = await readTeamSetup(apiClient2, options);
|
|
671
|
+
if ("problem" in read) return read.problem;
|
|
672
|
+
const text3 = read.setup.sessionBrief?.text ?? olderApi(read.setup);
|
|
673
|
+
try {
|
|
674
|
+
if (!options.localFiles) return fit(text3, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
|
|
675
|
+
const room = INSTRUCTIONS_MAX - text3.length - 2;
|
|
676
|
+
return fit(text3, localSection(read.setup, await computerState(read.setup), room));
|
|
677
|
+
} catch {
|
|
678
|
+
return fit(text3, "");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
360
682
|
// src/server.ts
|
|
361
683
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
362
684
|
|
|
@@ -469,7 +791,7 @@ var inputSchema = () => ({
|
|
|
469
791
|
"File path being edited \u2014 language is auto-detected from the extension"
|
|
470
792
|
)
|
|
471
793
|
});
|
|
472
|
-
function registerGetGuardrails(
|
|
794
|
+
function registerGetGuardrails(server, apiClient2) {
|
|
473
795
|
const handler = async ({
|
|
474
796
|
language,
|
|
475
797
|
category,
|
|
@@ -523,7 +845,7 @@ ${text3}`
|
|
|
523
845
|
]
|
|
524
846
|
};
|
|
525
847
|
};
|
|
526
|
-
|
|
848
|
+
server.registerTool(
|
|
527
849
|
"get_checks",
|
|
528
850
|
{
|
|
529
851
|
title: "Get Checks",
|
|
@@ -532,7 +854,7 @@ ${text3}`
|
|
|
532
854
|
},
|
|
533
855
|
handler
|
|
534
856
|
);
|
|
535
|
-
|
|
857
|
+
server.registerTool(
|
|
536
858
|
"get_guardrails",
|
|
537
859
|
{
|
|
538
860
|
title: "Get Checks (legacy name)",
|
|
@@ -545,8 +867,8 @@ ${text3}`
|
|
|
545
867
|
|
|
546
868
|
// src/tools/get-specs.ts
|
|
547
869
|
import { z as z2 } from "zod";
|
|
548
|
-
function registerGetSpecs(
|
|
549
|
-
|
|
870
|
+
function registerGetSpecs(server, apiClient2) {
|
|
871
|
+
server.registerTool(
|
|
550
872
|
"get_specs",
|
|
551
873
|
{
|
|
552
874
|
title: "Get Specs",
|
|
@@ -631,8 +953,8 @@ function matchGlob(path, pattern) {
|
|
|
631
953
|
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
632
954
|
return new RegExp(`^${regex}$`).test(path);
|
|
633
955
|
}
|
|
634
|
-
function registerGetDocs(
|
|
635
|
-
|
|
956
|
+
function registerGetDocs(server, apiClient2) {
|
|
957
|
+
server.registerTool(
|
|
636
958
|
"get_docs",
|
|
637
959
|
{
|
|
638
960
|
title: "Get Docs",
|
|
@@ -730,8 +1052,8 @@ Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
|
730
1052
|
}
|
|
731
1053
|
|
|
732
1054
|
// src/tools/get-tech-stack.ts
|
|
733
|
-
function registerGetTechStack(
|
|
734
|
-
|
|
1055
|
+
function registerGetTechStack(server, apiClient2) {
|
|
1056
|
+
server.registerTool(
|
|
735
1057
|
"get_tech_stack",
|
|
736
1058
|
{
|
|
737
1059
|
title: "Get Tech Stack",
|
|
@@ -780,8 +1102,8 @@ ${sections}`
|
|
|
780
1102
|
|
|
781
1103
|
// src/tools/get-planning-context.ts
|
|
782
1104
|
import { z as z4 } from "zod";
|
|
783
|
-
function registerGetPlanningContext(
|
|
784
|
-
|
|
1105
|
+
function registerGetPlanningContext(server, apiClient2) {
|
|
1106
|
+
server.registerTool(
|
|
785
1107
|
"get_planning_context",
|
|
786
1108
|
{
|
|
787
1109
|
title: "Get Planning Context",
|
|
@@ -815,7 +1137,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
|
|
|
815
1137
|
${repos}`);
|
|
816
1138
|
}
|
|
817
1139
|
if (ctx.dependencies.length > 0) {
|
|
818
|
-
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
|
|
1140
|
+
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}${d.provenance === "inferred" ? " (inferred)" : ""}`).join("\n");
|
|
819
1141
|
sections.push(`## Cross-Repo Dependencies
|
|
820
1142
|
|
|
821
1143
|
${deps}`);
|
|
@@ -902,8 +1224,8 @@ function formatItem(item) {
|
|
|
902
1224
|
${meta.join(" \xB7 ")}` : "") + (item.url ? `
|
|
903
1225
|
${item.url}` : "");
|
|
904
1226
|
}
|
|
905
|
-
function registerGetPlanBoard(
|
|
906
|
-
|
|
1227
|
+
function registerGetPlanBoard(server, apiClient2) {
|
|
1228
|
+
server.registerTool(
|
|
907
1229
|
"get_plan_board",
|
|
908
1230
|
{
|
|
909
1231
|
title: "Get Plan Board",
|
|
@@ -1147,8 +1469,8 @@ function formatChange(change) {
|
|
|
1147
1469
|
briefVerdict(change.states)
|
|
1148
1470
|
].join("\n\n");
|
|
1149
1471
|
}
|
|
1150
|
-
function registerGetChange(
|
|
1151
|
-
|
|
1472
|
+
function registerGetChange(server, apiClient2) {
|
|
1473
|
+
server.registerTool(
|
|
1152
1474
|
"get_change",
|
|
1153
1475
|
{
|
|
1154
1476
|
title: "Get Change",
|
|
@@ -1515,16 +1837,22 @@ async function hydrate(api, item, meta) {
|
|
|
1515
1837
|
}
|
|
1516
1838
|
async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
|
|
1517
1839
|
const budget = maxTokens ?? DEFAULT_BUDGET;
|
|
1840
|
+
const failures = [];
|
|
1841
|
+
const orEmpty = (empty) => (err) => {
|
|
1842
|
+
failures.push(err);
|
|
1843
|
+
return empty;
|
|
1844
|
+
};
|
|
1518
1845
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
1519
|
-
api.get("/v1/guardrails").catch((
|
|
1520
|
-
api.get("/v1/specs").catch((
|
|
1521
|
-
api.get("/v1/docs").catch((
|
|
1522
|
-
api.get("/v1/ai/settings/stack").catch((
|
|
1523
|
-
api.get("/v1/standards/rules").catch((
|
|
1846
|
+
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
1847
|
+
api.get("/v1/specs").catch(orEmpty([])),
|
|
1848
|
+
api.get("/v1/docs").catch(orEmpty({ items: [] })),
|
|
1849
|
+
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
1850
|
+
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
1524
1851
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
1525
1852
|
// blip) gates nothing, it can never hide working context.
|
|
1526
1853
|
api.get("/v1/doc-repos").catch(() => null)
|
|
1527
1854
|
]);
|
|
1855
|
+
if (failures.length === 5) throw failures[0];
|
|
1528
1856
|
const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
|
|
1529
1857
|
const guardrails = disabled("fitness") ? [] : rawGuardrails;
|
|
1530
1858
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
@@ -1731,18 +2059,19 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
|
|
|
1731
2059
|
guardrails: selectedGuardrails.length
|
|
1732
2060
|
};
|
|
1733
2061
|
if (sections.length === 0) {
|
|
1734
|
-
|
|
2062
|
+
const text3 = failures.length ? `No context found for this task, but ${failures.length} of 5 reads failed, so this is not an all-clear: the team's rules may not have been read.` : "No relevant context found for this task.";
|
|
2063
|
+
return { text: text3, stats };
|
|
1735
2064
|
}
|
|
1736
2065
|
const footer = `
|
|
1737
2066
|
|
|
1738
2067
|
---
|
|
1739
|
-
_Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
|
|
2068
|
+
_Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + (failures.length > 0 ? ` \xB7 ${failures.length} of 5 reads failed` : "") + `_`;
|
|
1740
2069
|
return { text: body + footer, stats };
|
|
1741
2070
|
}
|
|
1742
2071
|
|
|
1743
2072
|
// src/tools/get-context.ts
|
|
1744
|
-
function registerGetContext(
|
|
1745
|
-
|
|
2073
|
+
function registerGetContext(server, apiClient2) {
|
|
2074
|
+
server.registerTool(
|
|
1746
2075
|
"get_context",
|
|
1747
2076
|
{
|
|
1748
2077
|
title: "Get Context",
|
|
@@ -1770,8 +2099,240 @@ function registerGetContext(server2, apiClient2) {
|
|
|
1770
2099
|
);
|
|
1771
2100
|
}
|
|
1772
2101
|
|
|
1773
|
-
// src/tools/
|
|
2102
|
+
// src/tools/get-team-setup.ts
|
|
2103
|
+
var HELD_BECAUSE = {
|
|
2104
|
+
"not-plain": `it is not a plain skill: it has frontmatter beyond a name and a description, files outside its own folders, or a command that runs as it loads. The team plugin carries it, and \`${SETUP_COMMAND}\` shows that before the person accepts`,
|
|
2105
|
+
"outside-default-space": "it is outside the default space, so the team plugin does not carry it either. The person can read it in the console, then install it with `npx -y @systemdox/mcp-server install-skill <name>`",
|
|
2106
|
+
"not-checked": "it could not be checked. Ask again later"
|
|
2107
|
+
};
|
|
2108
|
+
function describeSetup(setup) {
|
|
2109
|
+
const lines = [`# Team setup of ${setup.workspace}`, ""];
|
|
2110
|
+
const skills = setup.sessionBrief?.skills ?? [];
|
|
2111
|
+
const held = setup.sessionBrief?.heldSkills ?? [];
|
|
2112
|
+
lines.push(`## Skills a session installs by itself (${skills.length})`, "");
|
|
2113
|
+
if (skills.length) {
|
|
2114
|
+
for (const s of skills) lines.push(`- **${s.name}** version ${s.latestVersion}: ${s.description}`);
|
|
2115
|
+
} else if (!setup.sessionBrief && setup.skills.length) {
|
|
2116
|
+
lines.push(`- ${setup.skills.join(", ")}`);
|
|
2117
|
+
} else {
|
|
2118
|
+
lines.push("None.");
|
|
2119
|
+
}
|
|
2120
|
+
lines.push("");
|
|
2121
|
+
if (held.length) {
|
|
2122
|
+
lines.push(`## Skills that wait for the person (${held.length})`, "");
|
|
2123
|
+
for (const h of held) lines.push(`- **${h.name}**: ${HELD_BECAUSE[h.reason] ?? h.reason}`);
|
|
2124
|
+
lines.push("", "Do not install these yourself: each is the person's to decide.", "");
|
|
2125
|
+
}
|
|
2126
|
+
if (setup.plugin) {
|
|
2127
|
+
lines.push(`## Team plugin`, "", `\`${setup.workspace}-setup\` version ${setup.plugin.version}.`);
|
|
2128
|
+
for (const c of setup.plugin.hookCommands ?? []) lines.push(`- Hook: ${JSON.stringify(c)}`);
|
|
2129
|
+
for (const m of setup.plugin.mcpServers ?? []) {
|
|
2130
|
+
lines.push(`- MCP server ${JSON.stringify(m.name)}: ${JSON.stringify(m.url ?? [m.command, ...m.args ?? []].filter(Boolean).join(" "))}`);
|
|
2131
|
+
}
|
|
2132
|
+
lines.push("");
|
|
2133
|
+
}
|
|
2134
|
+
const parts = Object.entries(setup.parts ?? {});
|
|
2135
|
+
if (parts.length) {
|
|
2136
|
+
lines.push(`## Published parts`, "", ...parts.map(([p, v]) => `- ${p}: version ${v}`), "");
|
|
2137
|
+
}
|
|
2138
|
+
if (setup.memory?.length) {
|
|
2139
|
+
lines.push(`## Team memory`, "", ...setup.memory.map((m) => `- ${m.project}: ${m.fileCount} file(s)`), "");
|
|
2140
|
+
}
|
|
2141
|
+
return lines;
|
|
2142
|
+
}
|
|
2143
|
+
function describeComputer(setup, state) {
|
|
2144
|
+
const lines = ["## This computer", ""];
|
|
2145
|
+
if (!setup.sessionBrief) {
|
|
2146
|
+
return [...lines, "This SystemDox API is too old to compare this computer with the team setup."];
|
|
2147
|
+
}
|
|
2148
|
+
const skills = setup.sessionBrief.skills;
|
|
2149
|
+
const latest = new Map(skills.map((s) => [s.name, s.latestVersion]));
|
|
2150
|
+
lines.push(
|
|
2151
|
+
state.missing.length ? `- Missing: ${state.missing.map((name) => `${name} version ${latest.get(name)}`).join(", ")}` : `- Missing: none of ${skills.length}`
|
|
2152
|
+
);
|
|
2153
|
+
if (state.behind.length) {
|
|
2154
|
+
lines.push(`- Behind: ${state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`).join(", ")}`);
|
|
2155
|
+
}
|
|
2156
|
+
if (state.notFromSystemDox.length) lines.push(`- Not from SystemDox, left alone: ${state.notFromSystemDox.join(", ")}`);
|
|
2157
|
+
if (setup.plugin) {
|
|
2158
|
+
lines.push(
|
|
2159
|
+
state.pluginInstalled ? `- Team plugin: installed${state.pluginVersion ? `, ${state.pluginVersion}` : ""}; the latest is version ${setup.plugin.version}` : "- Team plugin: not installed"
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
lines.push("", "## What to do", "");
|
|
2163
|
+
const steps = [];
|
|
2164
|
+
if (state.missing.length) steps.push("Call install_skill for each missing skill, at the version named above. These are plain skills: installing writes files and runs nothing.");
|
|
2165
|
+
if (state.behind.length) {
|
|
2166
|
+
steps.push("Call install_skill for each skill behind, at the latest version named above. If it refuses because files were edited, tell the person and leave them.");
|
|
2167
|
+
}
|
|
2168
|
+
if (hasPersonParts(setup) && !state.pluginInstalled) {
|
|
2169
|
+
steps.push(
|
|
2170
|
+
`Hooks, MCP servers, team instructions and memory need the person: \`${SETUP_COMMAND}\` in their own terminal. Never answer its prompts for them.`
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
lines.push(...steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ["Nothing."]);
|
|
2174
|
+
return lines;
|
|
2175
|
+
}
|
|
2176
|
+
function registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 }) {
|
|
2177
|
+
server.registerTool(
|
|
2178
|
+
"get_team_setup",
|
|
2179
|
+
{
|
|
2180
|
+
title: "Get Team Setup",
|
|
2181
|
+
description: localFiles ? "What this workspace's team setup holds (skills, plugin, hooks, MCP servers, memory) and what this computer lacks, with what to do about it. Call it to check whether this computer is set up. Reads only." : "What this workspace's team setup holds (skills, plugin, hooks, MCP servers, memory), and how to check this computer against it. Reads only.",
|
|
2182
|
+
inputSchema: {},
|
|
2183
|
+
annotations: { readOnlyHint: true }
|
|
2184
|
+
},
|
|
2185
|
+
async () => {
|
|
2186
|
+
const read = await readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs: 15e3 });
|
|
2187
|
+
if ("problem" in read) return { content: [{ type: "text", text: read.problem }] };
|
|
2188
|
+
const lines = describeSetup(read.setup);
|
|
2189
|
+
if (localFiles) lines.push(...describeComputer(read.setup, await computerState(read.setup)));
|
|
2190
|
+
else lines.push(CANNOT_SEE);
|
|
2191
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2192
|
+
}
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
// src/tools/document-graph.ts
|
|
1774
2197
|
import { z as z8 } from "zod";
|
|
2198
|
+
var DEFAULT_LIMIT2 = 50;
|
|
2199
|
+
var MAX_LIMIT = 200;
|
|
2200
|
+
var DEFAULT_HOPS = 6;
|
|
2201
|
+
var MAX_HOPS = 8;
|
|
2202
|
+
function place(file, line) {
|
|
2203
|
+
return file ? `${file}:${line}` : "made by a person";
|
|
2204
|
+
}
|
|
2205
|
+
function known(provenance, confidence) {
|
|
2206
|
+
return `${provenance} ${confidence}`;
|
|
2207
|
+
}
|
|
2208
|
+
function formatNeighbours(answer) {
|
|
2209
|
+
const lines = [`# ${answer.doc.label}`, `\`${answer.doc.id}\``, ""];
|
|
2210
|
+
if (answer.links.length === 0) lines.push("No links.");
|
|
2211
|
+
for (const link of answer.links) {
|
|
2212
|
+
lines.push(
|
|
2213
|
+
`- ${link.direction} \xB7 ${link.relation} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${link.other.label} \xB7 \`${link.other.id}\` \xB7 ${place(link.file, link.line)}`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
if (answer.truncated) {
|
|
2217
|
+
lines.push("", `Showing ${answer.links.length} of ${answer.total}. Raise limit to see more.`);
|
|
2218
|
+
}
|
|
2219
|
+
if (answer.ambiguous.length > 0) {
|
|
2220
|
+
lines.push("", "## Ambiguous: not drawn, because each could mean several documents");
|
|
2221
|
+
for (const entry of answer.ambiguous) {
|
|
2222
|
+
const candidates = entry.candidates.map((c) => `${c.label} (${c.repo}, \`${c.id}\`)`);
|
|
2223
|
+
lines.push(`- ${entry.reference} at ${place(entry.file, entry.line)}: ${candidates.join("; ")}`);
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return lines.join("\n");
|
|
2227
|
+
}
|
|
2228
|
+
function formatPath(answer, maxHops) {
|
|
2229
|
+
if (!answer.found) return `No path within ${maxHops} hops.`;
|
|
2230
|
+
const lines = answer.steps.map((step, index) => {
|
|
2231
|
+
const arrow = step.reversed ? "<-" : "->";
|
|
2232
|
+
return `${index + 1}. ${step.from.label} ${arrow} ${step.relation} ${arrow} ${step.to.label} \xB7 ${known(step.provenance, step.confidence)} \xB7 ${place(step.file, step.line)}`;
|
|
2233
|
+
});
|
|
2234
|
+
return [...lines, "", `Path confidence: ${answer.confidence}`].join("\n");
|
|
2235
|
+
}
|
|
2236
|
+
function formatStats(stats, repoLinks) {
|
|
2237
|
+
const { declared, extracted, inferred, ambiguous } = stats.links;
|
|
2238
|
+
const lines = [
|
|
2239
|
+
`# The graph: ${stats.nodeCount} documents, ${stats.edgeCount} links`,
|
|
2240
|
+
"",
|
|
2241
|
+
`Links: ${declared} declared \xB7 ${extracted} extracted \xB7 ${inferred} inferred \xB7 ${ambiguous} ambiguous (not drawn)`,
|
|
2242
|
+
"",
|
|
2243
|
+
"## Repositories"
|
|
2244
|
+
];
|
|
2245
|
+
for (const repo of stats.repos) {
|
|
2246
|
+
const state = repo.behind ? `BEHIND: pushed ${repo.headCommit || "unknown"} at ${repo.pushedAt}, index at ${repo.indexedCommit || "unknown"}` : `index at ${repo.indexedCommit || "unknown"}`;
|
|
2247
|
+
lines.push(`- ${repo.repo}: ${repo.documents} documents, ${repo.links} links, ${state}`);
|
|
2248
|
+
}
|
|
2249
|
+
if (repoLinks.length > 0) {
|
|
2250
|
+
lines.push("", "## Repository links");
|
|
2251
|
+
for (const link of repoLinks) {
|
|
2252
|
+
lines.push(
|
|
2253
|
+
`- ${link.source} ${link.relation} ${link.target} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${place(link.file, link.line)}`
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
return lines.join("\n");
|
|
2258
|
+
}
|
|
2259
|
+
function refused(err) {
|
|
2260
|
+
const status = err.status;
|
|
2261
|
+
if (status !== 404 && status !== 400) throw err;
|
|
2262
|
+
return {
|
|
2263
|
+
content: [{ type: "text", text: err.message }],
|
|
2264
|
+
isError: true
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
function registerDocumentGraph(server, apiClient2) {
|
|
2268
|
+
server.registerTool(
|
|
2269
|
+
"get_document_links",
|
|
2270
|
+
{
|
|
2271
|
+
description: "What one document links to and from, strongest first. Each link says how it is known: declared (a person made it), extracted (the file says so), or inferred (a reading that could be wrong), with a confidence and the file and line it came from. A reference that could mean several documents is listed as ambiguous with its candidates, and is never drawn as a link. doc_id is the id get_docs returns. Read-only.",
|
|
2272
|
+
inputSchema: {
|
|
2273
|
+
doc_id: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
2274
|
+
direction: z8.enum(["out", "in", "both"]).optional().describe("Default both"),
|
|
2275
|
+
relation: z8.enum(["links_to", "cites", "mentions", "satisfies", "references"]).optional().describe("Only links of this kind"),
|
|
2276
|
+
limit: z8.number().int().min(1).max(MAX_LIMIT).optional().describe(`Most links to show (default ${DEFAULT_LIMIT2})`)
|
|
2277
|
+
},
|
|
2278
|
+
annotations: { readOnlyHint: true }
|
|
2279
|
+
},
|
|
2280
|
+
async ({ doc_id, direction, relation, limit }) => {
|
|
2281
|
+
const params = { doc: doc_id, limit: String(limit ?? DEFAULT_LIMIT2) };
|
|
2282
|
+
if (direction) params.direction = direction;
|
|
2283
|
+
if (relation) params.relation = relation;
|
|
2284
|
+
try {
|
|
2285
|
+
const answer = await apiClient2.get("/v1/graph/neighbours", params);
|
|
2286
|
+
return { content: [{ type: "text", text: formatNeighbours(answer) }] };
|
|
2287
|
+
} catch (err) {
|
|
2288
|
+
return refused(err);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
);
|
|
2292
|
+
server.registerTool(
|
|
2293
|
+
"trace_document_path",
|
|
2294
|
+
{
|
|
2295
|
+
description: "The shortest way two documents connect, walking links in either direction. Each step says how it is known, and the path is only as strong as its weakest step. Read-only.",
|
|
2296
|
+
inputSchema: {
|
|
2297
|
+
from: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
2298
|
+
to: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
2299
|
+
max_hops: z8.number().int().min(1).max(MAX_HOPS).optional().describe(`Longest path to look for (default ${DEFAULT_HOPS})`)
|
|
2300
|
+
},
|
|
2301
|
+
annotations: { readOnlyHint: true }
|
|
2302
|
+
},
|
|
2303
|
+
async ({ from, to, max_hops }) => {
|
|
2304
|
+
const hops = max_hops ?? DEFAULT_HOPS;
|
|
2305
|
+
try {
|
|
2306
|
+
const answer = await apiClient2.get("/v1/graph/path", {
|
|
2307
|
+
from,
|
|
2308
|
+
to,
|
|
2309
|
+
maxHops: String(hops)
|
|
2310
|
+
});
|
|
2311
|
+
return { content: [{ type: "text", text: formatPath(answer, hops) }] };
|
|
2312
|
+
} catch (err) {
|
|
2313
|
+
return refused(err);
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
);
|
|
2317
|
+
server.registerTool(
|
|
2318
|
+
"get_graph_stats",
|
|
2319
|
+
{
|
|
2320
|
+
description: "The state of the graph: links counted by how they are known, each repository's freshness (marked BEHIND when a push has not finished indexing), and how repositories depend on each other, from the files that declare it. Read-only.",
|
|
2321
|
+
inputSchema: {},
|
|
2322
|
+
annotations: { readOnlyHint: true }
|
|
2323
|
+
},
|
|
2324
|
+
async () => {
|
|
2325
|
+
const [stats, repos] = await Promise.all([
|
|
2326
|
+
apiClient2.getFresh("/v1/graph/stats"),
|
|
2327
|
+
apiClient2.getFresh("/v1/graph/repos")
|
|
2328
|
+
]);
|
|
2329
|
+
return { content: [{ type: "text", text: formatStats(stats, repos.links) }] };
|
|
2330
|
+
}
|
|
2331
|
+
);
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2334
|
+
// src/tools/create-guardrail.ts
|
|
2335
|
+
import { z as z9 } from "zod";
|
|
1775
2336
|
var DESCRIPTION2 = "Create a new check in SystemDox. Use this when you discover a pattern that should be enforced (banned pattern, required pattern, coding rule).";
|
|
1776
2337
|
var CATEGORIES = [
|
|
1777
2338
|
"imports",
|
|
@@ -1784,15 +2345,15 @@ var CATEGORIES = [
|
|
|
1784
2345
|
"data-access"
|
|
1785
2346
|
];
|
|
1786
2347
|
var inputSchema2 = () => ({
|
|
1787
|
-
title:
|
|
1788
|
-
description:
|
|
1789
|
-
severity:
|
|
1790
|
-
category:
|
|
1791
|
-
pattern:
|
|
1792
|
-
fix:
|
|
1793
|
-
fitness_test:
|
|
2348
|
+
title: z9.string().describe("Short title for the check"),
|
|
2349
|
+
description: z9.string().describe("Detailed description of what the check enforces and why"),
|
|
2350
|
+
severity: z9.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
|
|
2351
|
+
category: z9.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
|
|
2352
|
+
pattern: z9.string().optional().describe("Bad code pattern that should be avoided"),
|
|
2353
|
+
fix: z9.string().optional().describe("Correct code pattern to use instead"),
|
|
2354
|
+
fitness_test: z9.string().optional().describe("Shell command to detect violations in CI")
|
|
1794
2355
|
});
|
|
1795
|
-
function registerCreateGuardrail(
|
|
2356
|
+
function registerCreateGuardrail(server, apiClient2) {
|
|
1796
2357
|
const handler = async ({
|
|
1797
2358
|
title,
|
|
1798
2359
|
description,
|
|
@@ -1829,7 +2390,7 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
1829
2390
|
]
|
|
1830
2391
|
};
|
|
1831
2392
|
};
|
|
1832
|
-
|
|
2393
|
+
server.registerTool(
|
|
1833
2394
|
"create_check",
|
|
1834
2395
|
{
|
|
1835
2396
|
title: "Create Check",
|
|
@@ -1841,7 +2402,7 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
1841
2402
|
},
|
|
1842
2403
|
handler
|
|
1843
2404
|
);
|
|
1844
|
-
|
|
2405
|
+
server.registerTool(
|
|
1845
2406
|
"create_guardrail",
|
|
1846
2407
|
{
|
|
1847
2408
|
title: "Create Check (legacy name)",
|
|
@@ -1856,20 +2417,20 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
1856
2417
|
}
|
|
1857
2418
|
|
|
1858
2419
|
// src/tools/update-spec.ts
|
|
1859
|
-
import { z as
|
|
1860
|
-
function registerUpdateSpec(
|
|
1861
|
-
|
|
2420
|
+
import { z as z10 } from "zod";
|
|
2421
|
+
function registerUpdateSpec(server, apiClient2) {
|
|
2422
|
+
server.registerTool(
|
|
1862
2423
|
"update_spec",
|
|
1863
2424
|
{
|
|
1864
2425
|
title: "Update Spec",
|
|
1865
2426
|
description: "Update an existing SystemDox spec's proposal, tasks, or requirements. Only draft specs can be modified \u2014 published specs must be versioned first.",
|
|
1866
2427
|
inputSchema: {
|
|
1867
|
-
draft_id:
|
|
1868
|
-
section:
|
|
1869
|
-
content:
|
|
2428
|
+
draft_id: z10.string().describe("ID of the spec to update"),
|
|
2429
|
+
section: z10.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
|
|
2430
|
+
content: z10.string().describe(
|
|
1870
2431
|
"New markdown content for the section. For requirements, provide the full requirement content."
|
|
1871
2432
|
),
|
|
1872
|
-
requirement_name:
|
|
2433
|
+
requirement_name: z10.string().optional().describe(
|
|
1873
2434
|
"Name of the requirement (required when section is 'requirements')"
|
|
1874
2435
|
)
|
|
1875
2436
|
},
|
|
@@ -1919,19 +2480,19 @@ function registerUpdateSpec(server2, apiClient2) {
|
|
|
1919
2480
|
}
|
|
1920
2481
|
|
|
1921
2482
|
// src/tools/report-decision.ts
|
|
1922
|
-
import { z as
|
|
1923
|
-
function registerReportDecision(
|
|
1924
|
-
|
|
2483
|
+
import { z as z11 } from "zod";
|
|
2484
|
+
function registerReportDecision(server, apiClient2) {
|
|
2485
|
+
server.registerTool(
|
|
1925
2486
|
"report_decision",
|
|
1926
2487
|
{
|
|
1927
2488
|
title: "Report Decision",
|
|
1928
2489
|
description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
|
|
1929
2490
|
inputSchema: {
|
|
1930
|
-
title:
|
|
1931
|
-
context:
|
|
1932
|
-
decision:
|
|
1933
|
-
consequences:
|
|
1934
|
-
related_files:
|
|
2491
|
+
title: z11.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
|
|
2492
|
+
context: z11.string().describe("What prompted this decision \u2014 the problem or constraint"),
|
|
2493
|
+
decision: z11.string().describe("What was decided and why"),
|
|
2494
|
+
consequences: z11.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
|
|
2495
|
+
related_files: z11.array(z11.string()).optional().describe("File paths related to this decision")
|
|
1935
2496
|
},
|
|
1936
2497
|
annotations: {
|
|
1937
2498
|
readOnlyHint: false
|
|
@@ -1982,7 +2543,7 @@ Status: draft \u2014 review in SystemDox before publishing.`
|
|
|
1982
2543
|
}
|
|
1983
2544
|
|
|
1984
2545
|
// src/tools/get-project-rules.ts
|
|
1985
|
-
import { z as
|
|
2546
|
+
import { z as z12 } from "zod";
|
|
1986
2547
|
var CATEGORY_LABELS = {
|
|
1987
2548
|
deployment: "Deployment",
|
|
1988
2549
|
architecture: "Architecture",
|
|
@@ -1991,14 +2552,14 @@ var CATEGORY_LABELS = {
|
|
|
1991
2552
|
process: "Process",
|
|
1992
2553
|
other: "Other"
|
|
1993
2554
|
};
|
|
1994
|
-
function registerGetProjectRules(
|
|
1995
|
-
|
|
2555
|
+
function registerGetProjectRules(server, apiClient2) {
|
|
2556
|
+
server.registerTool(
|
|
1996
2557
|
"get_project_rules",
|
|
1997
2558
|
{
|
|
1998
2559
|
title: "Get Project Rules",
|
|
1999
2560
|
description: "Retrieve project rules defined in SystemDox Standards. These are team-defined principles covering deployment, architecture, security, testing, and process. Filter by category to get only relevant rules.",
|
|
2000
2561
|
inputSchema: {
|
|
2001
|
-
category:
|
|
2562
|
+
category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
|
|
2002
2563
|
}
|
|
2003
2564
|
},
|
|
2004
2565
|
async ({ category }) => {
|
|
@@ -2050,18 +2611,18 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
2050
2611
|
}
|
|
2051
2612
|
|
|
2052
2613
|
// src/tools/create-project-rule.ts
|
|
2053
|
-
import { z as
|
|
2054
|
-
function registerCreateProjectRule(
|
|
2055
|
-
|
|
2614
|
+
import { z as z13 } from "zod";
|
|
2615
|
+
function registerCreateProjectRule(server, apiClient2) {
|
|
2616
|
+
server.registerTool(
|
|
2056
2617
|
"create_project_rule",
|
|
2057
2618
|
{
|
|
2058
2619
|
title: "Create Project Rule",
|
|
2059
2620
|
description: "Create a new project rule in SystemDox Standards. Use this when you discover a team principle or convention that should be enforced across the project.",
|
|
2060
2621
|
inputSchema: {
|
|
2061
|
-
text:
|
|
2622
|
+
text: z13.string().describe(
|
|
2062
2623
|
"The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
|
|
2063
2624
|
),
|
|
2064
|
-
category:
|
|
2625
|
+
category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
|
|
2065
2626
|
},
|
|
2066
2627
|
annotations: {
|
|
2067
2628
|
readOnlyHint: false
|
|
@@ -2086,8 +2647,8 @@ function registerCreateProjectRule(server2, apiClient2) {
|
|
|
2086
2647
|
}
|
|
2087
2648
|
|
|
2088
2649
|
// src/tools/get-prompt-templates.ts
|
|
2089
|
-
function registerGetPromptTemplates(
|
|
2090
|
-
|
|
2650
|
+
function registerGetPromptTemplates(server, apiClient2) {
|
|
2651
|
+
server.registerTool(
|
|
2091
2652
|
"get_prompt_templates",
|
|
2092
2653
|
{
|
|
2093
2654
|
title: "Get Prompt Templates",
|
|
@@ -2132,17 +2693,17 @@ ${text3}`
|
|
|
2132
2693
|
}
|
|
2133
2694
|
|
|
2134
2695
|
// src/tools/create-prompt-template.ts
|
|
2135
|
-
import { z as
|
|
2136
|
-
function registerCreatePromptTemplate(
|
|
2137
|
-
|
|
2696
|
+
import { z as z14 } from "zod";
|
|
2697
|
+
function registerCreatePromptTemplate(server, apiClient2) {
|
|
2698
|
+
server.registerTool(
|
|
2138
2699
|
"create_prompt_template",
|
|
2139
2700
|
{
|
|
2140
2701
|
title: "Create Prompt Template",
|
|
2141
2702
|
description: "Create a new reusable prompt template in SystemDox Standards. Use this to store preconfigured prompts for common AI tasks like code reviews, documentation generation, or analysis frameworks.",
|
|
2142
2703
|
inputSchema: {
|
|
2143
|
-
name:
|
|
2144
|
-
description:
|
|
2145
|
-
prompt:
|
|
2704
|
+
name: z14.string().describe("Short name for the template"),
|
|
2705
|
+
description: z14.string().optional().describe("Brief description of when to use this template"),
|
|
2706
|
+
prompt: z14.string().describe("The full prompt text that Gen AI will use when generating content")
|
|
2146
2707
|
},
|
|
2147
2708
|
annotations: {
|
|
2148
2709
|
readOnlyHint: false
|
|
@@ -2171,205 +2732,9 @@ function registerCreatePromptTemplate(server2, apiClient2) {
|
|
|
2171
2732
|
}
|
|
2172
2733
|
|
|
2173
2734
|
// src/tools/skills.ts
|
|
2174
|
-
import { z as
|
|
2735
|
+
import { z as z15 } from "zod";
|
|
2175
2736
|
import { homedir as homedir3 } from "os";
|
|
2176
2737
|
import { basename, resolve as resolve2 } from "path";
|
|
2177
|
-
|
|
2178
|
-
// src/lib/skills.ts
|
|
2179
|
-
import { createHash } from "crypto";
|
|
2180
|
-
import { lstat, mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
2181
|
-
import { existsSync } from "fs";
|
|
2182
|
-
import { homedir as homedir2 } from "os";
|
|
2183
|
-
import { dirname as dirname2, join as join2, relative, resolve, sep } from "path";
|
|
2184
|
-
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
2185
|
-
var ENTRY_FILE = "SKILL.md";
|
|
2186
|
-
var MAX_FILES = 100;
|
|
2187
|
-
var MAX_FILE_BYTES = 256 * 1024;
|
|
2188
|
-
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
2189
|
-
var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2190
|
-
var SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
2191
|
-
var SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
|
|
2192
|
-
var SkillInstallRefused = class extends Error {
|
|
2193
|
-
constructor(dest, edited) {
|
|
2194
|
-
super(
|
|
2195
|
-
`${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
|
|
2196
|
-
);
|
|
2197
|
-
this.dest = dest;
|
|
2198
|
-
this.edited = edited;
|
|
2199
|
-
}
|
|
2200
|
-
dest;
|
|
2201
|
-
edited;
|
|
2202
|
-
};
|
|
2203
|
-
function sha256(content) {
|
|
2204
|
-
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
2205
|
-
}
|
|
2206
|
-
function isValidName(name) {
|
|
2207
|
-
return name.length <= 64 && NAME_RE.test(name);
|
|
2208
|
-
}
|
|
2209
|
-
function isValidPath(path) {
|
|
2210
|
-
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
|
|
2211
|
-
}
|
|
2212
|
-
function frontmatterName(skillMd) {
|
|
2213
|
-
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
2214
|
-
if (!block) return void 0;
|
|
2215
|
-
const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
|
|
2216
|
-
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
2217
|
-
}
|
|
2218
|
-
function defaultSkillDir(name) {
|
|
2219
|
-
return join2(homedir2(), ".claude", "skills", name);
|
|
2220
|
-
}
|
|
2221
|
-
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
2222
|
-
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
2223
|
-
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
2224
|
-
const full = join2(dir, entry.name);
|
|
2225
|
-
const rel = relative(root, full).split(sep).join("/");
|
|
2226
|
-
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
2227
|
-
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
2228
|
-
else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
|
|
2229
|
-
else if (entry.isFile()) out.files.push(rel);
|
|
2230
|
-
}
|
|
2231
|
-
out.files.sort();
|
|
2232
|
-
return out;
|
|
2233
|
-
}
|
|
2234
|
-
async function readTextFiles(root, paths, what) {
|
|
2235
|
-
if (paths.length > MAX_FILES) {
|
|
2236
|
-
throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
|
|
2237
|
-
}
|
|
2238
|
-
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2239
|
-
const files = [];
|
|
2240
|
-
let total = 0;
|
|
2241
|
-
for (const path of paths) {
|
|
2242
|
-
if (!isValidPath(path)) {
|
|
2243
|
-
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
2244
|
-
}
|
|
2245
|
-
const raw = await readFile2(join2(root, ...path.split("/")));
|
|
2246
|
-
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
2247
|
-
if (raw.length > MAX_FILE_BYTES) {
|
|
2248
|
-
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
2249
|
-
}
|
|
2250
|
-
total += raw.length;
|
|
2251
|
-
if (total > MAX_TOTAL_BYTES) {
|
|
2252
|
-
throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
|
|
2253
|
-
}
|
|
2254
|
-
let content;
|
|
2255
|
-
try {
|
|
2256
|
-
content = decoder.decode(raw);
|
|
2257
|
-
} catch {
|
|
2258
|
-
throw new Error(`${path} is not UTF-8 text`);
|
|
2259
|
-
}
|
|
2260
|
-
files.push({ path, content });
|
|
2261
|
-
}
|
|
2262
|
-
return files;
|
|
2263
|
-
}
|
|
2264
|
-
async function readSkillDirectory(directory) {
|
|
2265
|
-
const root = resolve(directory);
|
|
2266
|
-
if (!existsSync(join2(root, ENTRY_FILE))) {
|
|
2267
|
-
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
2268
|
-
}
|
|
2269
|
-
const walked = await walk(root, { skipHidden: true });
|
|
2270
|
-
if (walked.links.length) {
|
|
2271
|
-
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
2272
|
-
}
|
|
2273
|
-
const files = await readTextFiles(root, walked.files, "a skill");
|
|
2274
|
-
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
2275
|
-
const name = frontmatterName(skillMd.content);
|
|
2276
|
-
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
2277
|
-
return { name, files, skipped: walked.hidden };
|
|
2278
|
-
}
|
|
2279
|
-
function within(dest, path) {
|
|
2280
|
-
if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
|
|
2281
|
-
const full = resolve(dest, ...path.split("/"));
|
|
2282
|
-
if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
|
|
2283
|
-
return full;
|
|
2284
|
-
}
|
|
2285
|
-
async function readManifest(dest) {
|
|
2286
|
-
try {
|
|
2287
|
-
return JSON.parse(await readFile2(join2(dest, MANIFEST_FILE), "utf8"));
|
|
2288
|
-
} catch {
|
|
2289
|
-
return void 0;
|
|
2290
|
-
}
|
|
2291
|
-
}
|
|
2292
|
-
async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
2293
|
-
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
2294
|
-
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
2295
|
-
for (const f of version.files) within(dest, f.path);
|
|
2296
|
-
if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
2297
|
-
throw new Error(`${dest} is a link: install into a real folder`);
|
|
2298
|
-
}
|
|
2299
|
-
const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
2300
|
-
if (walked.links.length) {
|
|
2301
|
-
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
2302
|
-
}
|
|
2303
|
-
const present = walked.files;
|
|
2304
|
-
const manifest = await readManifest(dest);
|
|
2305
|
-
const known = Object.fromEntries(
|
|
2306
|
-
Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
|
|
2307
|
-
);
|
|
2308
|
-
const edited = [];
|
|
2309
|
-
const untracked = [];
|
|
2310
|
-
for (const path of present) {
|
|
2311
|
-
const content = await readFile2(join2(dest, ...path.split("/")), "utf8");
|
|
2312
|
-
if (Object.hasOwn(known, path)) {
|
|
2313
|
-
if (sha256(content) !== known[path]) edited.push(path);
|
|
2314
|
-
} else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
|
|
2315
|
-
if (incoming.has(path) || !manifest) edited.push(path);
|
|
2316
|
-
else untracked.push(path);
|
|
2317
|
-
}
|
|
2318
|
-
}
|
|
2319
|
-
const blocking = edited.filter((p) => incoming.has(p) || !manifest);
|
|
2320
|
-
if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
|
|
2321
|
-
const written = [];
|
|
2322
|
-
for (const f of version.files) {
|
|
2323
|
-
const full = within(dest, f.path);
|
|
2324
|
-
await mkdir2(dirname2(full), { recursive: true });
|
|
2325
|
-
await writeFile2(full, f.content, "utf8");
|
|
2326
|
-
written.push(f.path);
|
|
2327
|
-
}
|
|
2328
|
-
const removed = [];
|
|
2329
|
-
const keptEdited = [];
|
|
2330
|
-
for (const path of Object.keys(known)) {
|
|
2331
|
-
if (incoming.has(path) || !present.includes(path)) continue;
|
|
2332
|
-
if (edited.includes(path)) {
|
|
2333
|
-
keptEdited.push(path);
|
|
2334
|
-
continue;
|
|
2335
|
-
}
|
|
2336
|
-
await rm2(within(dest, path));
|
|
2337
|
-
removed.push(path);
|
|
2338
|
-
}
|
|
2339
|
-
const next = {
|
|
2340
|
-
name: version.name,
|
|
2341
|
-
version: version.version,
|
|
2342
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2343
|
-
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
2344
|
-
};
|
|
2345
|
-
await writeFile2(join2(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
2346
|
-
return {
|
|
2347
|
-
dest,
|
|
2348
|
-
version: version.version,
|
|
2349
|
-
written,
|
|
2350
|
-
removed,
|
|
2351
|
-
keptUntracked: untracked.filter((p) => !incoming.has(p)),
|
|
2352
|
-
keptEdited
|
|
2353
|
-
};
|
|
2354
|
-
}
|
|
2355
|
-
function fenceFor(text3) {
|
|
2356
|
-
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
2357
|
-
return "`".repeat(Math.max(3, longest + 1));
|
|
2358
|
-
}
|
|
2359
|
-
function describeInstall(name, r) {
|
|
2360
|
-
const lines = [
|
|
2361
|
-
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
2362
|
-
];
|
|
2363
|
-
if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
|
|
2364
|
-
if (r.keptEdited.length) {
|
|
2365
|
-
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
2366
|
-
}
|
|
2367
|
-
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
2368
|
-
lines.push("Start a new Claude Code session to load it.");
|
|
2369
|
-
return lines.join("\n");
|
|
2370
|
-
}
|
|
2371
|
-
|
|
2372
|
-
// src/tools/skills.ts
|
|
2373
2738
|
var SKILLS = "/v1/standards/skills";
|
|
2374
2739
|
function expandHome(path) {
|
|
2375
2740
|
return path.replace(/^~(?=$|[\\/])/, homedir3());
|
|
@@ -2377,25 +2742,25 @@ function expandHome(path) {
|
|
|
2377
2742
|
function text(t) {
|
|
2378
2743
|
return { content: [{ type: "text", text: t }] };
|
|
2379
2744
|
}
|
|
2380
|
-
function registerSkillTools(
|
|
2381
|
-
const fileSchema =
|
|
2382
|
-
path:
|
|
2383
|
-
content:
|
|
2745
|
+
function registerSkillTools(server, apiClient2, { localFiles }) {
|
|
2746
|
+
const fileSchema = z15.object({
|
|
2747
|
+
path: z15.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
|
|
2748
|
+
content: z15.string().describe("The file's full text, exactly as it is")
|
|
2384
2749
|
});
|
|
2385
|
-
|
|
2750
|
+
server.registerTool(
|
|
2386
2751
|
"publish_skill",
|
|
2387
2752
|
{
|
|
2388
2753
|
title: "Publish Skill",
|
|
2389
2754
|
description: localFiles ? "Publish a Claude Code skill folder (SKILL.md plus its references and scripts) to SystemDox so it can be installed on any computer. Pass `directory` (e.g. ~/.claude/skills/<name>): the server reads the files itself, byte for byte. Publishing a name again makes a new version. Never retype the files into `files` when the folder is on this computer." : "Publish a Claude Code skill (SKILL.md plus its references and scripts) to SystemDox so it can be installed on any computer. Pass every file of the folder in `files`, exactly as it is. Publishing a name again makes a new version.",
|
|
2390
2755
|
inputSchema: {
|
|
2391
2756
|
...localFiles ? {
|
|
2392
|
-
directory:
|
|
2757
|
+
directory: z15.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
|
|
2393
2758
|
} : {},
|
|
2394
|
-
files:
|
|
2395
|
-
name:
|
|
2396
|
-
spaceId:
|
|
2397
|
-
note:
|
|
2398
|
-
baseVersion:
|
|
2759
|
+
files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
|
|
2760
|
+
name: z15.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
|
|
2761
|
+
spaceId: z15.string().optional().describe("Space for a NEW skill (default: your default space)"),
|
|
2762
|
+
note: z15.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
|
|
2763
|
+
baseVersion: z15.number().int().positive().optional().describe(
|
|
2399
2764
|
"The version these files were edited from. When given, the publish is refused (SYD-K017) if someone published a newer version since, instead of replacing their changes"
|
|
2400
2765
|
)
|
|
2401
2766
|
},
|
|
@@ -2433,7 +2798,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2433
2798
|
);
|
|
2434
2799
|
}
|
|
2435
2800
|
);
|
|
2436
|
-
|
|
2801
|
+
server.registerTool(
|
|
2437
2802
|
"list_skills",
|
|
2438
2803
|
{
|
|
2439
2804
|
title: "List Skills",
|
|
@@ -2455,16 +2820,16 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2455
2820
|
);
|
|
2456
2821
|
}
|
|
2457
2822
|
);
|
|
2458
|
-
|
|
2823
|
+
server.registerTool(
|
|
2459
2824
|
"install_skill",
|
|
2460
2825
|
{
|
|
2461
2826
|
title: "Install Skill",
|
|
2462
2827
|
description: localFiles ? "Install a skill published to SystemDox onto this computer (default ~/.claude/skills/<name>/). Refuses, naming the files, when a file there was edited since it was installed, unless overwrite is true. Never runs anything." : "Fetch a skill published to SystemDox so you can install it: returns every file and the folder to write them into (default ~/.claude/skills/<name>/). Write each file exactly, then the manifest it shows.",
|
|
2463
2828
|
inputSchema: {
|
|
2464
|
-
name:
|
|
2465
|
-
version:
|
|
2466
|
-
target:
|
|
2467
|
-
...localFiles ? { overwrite:
|
|
2829
|
+
name: z15.string().describe("The skill's name"),
|
|
2830
|
+
version: z15.number().int().positive().optional().describe("A version number (default: the latest)"),
|
|
2831
|
+
target: z15.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
|
|
2832
|
+
...localFiles ? { overwrite: z15.boolean().optional().describe("Replace files edited since they were installed") } : {}
|
|
2468
2833
|
},
|
|
2469
2834
|
annotations: { readOnlyHint: !localFiles }
|
|
2470
2835
|
},
|
|
@@ -2510,11 +2875,11 @@ ${body}`
|
|
|
2510
2875
|
}
|
|
2511
2876
|
|
|
2512
2877
|
// src/tools/team-setup.ts
|
|
2513
|
-
import { z as
|
|
2514
|
-
import { existsSync as
|
|
2878
|
+
import { z as z16 } from "zod";
|
|
2879
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2515
2880
|
import { lstat as lstat2 } from "fs/promises";
|
|
2516
2881
|
import { homedir as homedir4 } from "os";
|
|
2517
|
-
import { join as
|
|
2882
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
2518
2883
|
|
|
2519
2884
|
// src/lib/person-values.ts
|
|
2520
2885
|
var PHONE = "phone";
|
|
@@ -2595,9 +2960,9 @@ var PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
|
2595
2960
|
var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
2596
2961
|
|
|
2597
2962
|
// src/sync-memory.ts
|
|
2598
|
-
import { existsSync as
|
|
2599
|
-
import { mkdir as mkdir3, readdir as readdir2, readFile as
|
|
2600
|
-
import { dirname as dirname3, join as
|
|
2963
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2964
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
2965
|
+
import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
|
|
2601
2966
|
|
|
2602
2967
|
// src/lib/memory-sync.ts
|
|
2603
2968
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -2877,7 +3242,7 @@ function refusal(e) {
|
|
|
2877
3242
|
const { status, code } = e;
|
|
2878
3243
|
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
2879
3244
|
}
|
|
2880
|
-
var toPath = (root, rel) =>
|
|
3245
|
+
var toPath = (root, rel) => join4(root, ...rel.split("/"));
|
|
2881
3246
|
async function readFolder(root) {
|
|
2882
3247
|
const files = {};
|
|
2883
3248
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -2885,14 +3250,14 @@ async function readFolder(root) {
|
|
|
2885
3250
|
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
2886
3251
|
if (e.name.startsWith(".")) continue;
|
|
2887
3252
|
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
2888
|
-
if (e.isDirectory()) await walk2(
|
|
3253
|
+
if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
|
|
2889
3254
|
else if (e.isFile()) {
|
|
2890
3255
|
names.add(rel.toLowerCase());
|
|
2891
|
-
if (e.name.endsWith(".md")) files[rel] = await
|
|
3256
|
+
if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
|
|
2892
3257
|
}
|
|
2893
3258
|
}
|
|
2894
3259
|
}
|
|
2895
|
-
if (
|
|
3260
|
+
if (existsSync3(root)) await walk2(root, "");
|
|
2896
3261
|
return { files, names };
|
|
2897
3262
|
}
|
|
2898
3263
|
async function pullTeam(client, ws, project) {
|
|
@@ -3101,11 +3466,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3101
3466
|
report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
|
|
3102
3467
|
break;
|
|
3103
3468
|
}
|
|
3104
|
-
const
|
|
3105
|
-
if (
|
|
3106
|
-
out(` ${
|
|
3469
|
+
const refused2 = result.refused[0];
|
|
3470
|
+
if (refused2?.code === "SYD-K021") {
|
|
3471
|
+
out(` ${refused2.detail}. Add them in Staff first, or answer p to file them.`);
|
|
3107
3472
|
choices = choices.filter((c) => c !== "m");
|
|
3108
|
-
} else if (
|
|
3473
|
+
} else if (refused2) out(` ${refused2.detail}`);
|
|
3109
3474
|
}
|
|
3110
3475
|
}
|
|
3111
3476
|
for (let i = 0; i < filed.length; i += 50) {
|
|
@@ -3249,7 +3614,7 @@ async function syncMemory(opts) {
|
|
|
3249
3614
|
own.memory.length ? `${ws} has ${own.memory.length} team memories (${own.memory.map((m) => m.project).join(", ")}): name one with --project.` : `${ws} has no team memory yet: name the project to start with --project.`
|
|
3250
3615
|
);
|
|
3251
3616
|
}
|
|
3252
|
-
const dir = resolve3(opts.dir ??
|
|
3617
|
+
const dir = resolve3(opts.dir ?? join4(workspaceDir(ws), "memory", project));
|
|
3253
3618
|
const run2 = { ...opts, dir };
|
|
3254
3619
|
const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
|
|
3255
3620
|
const report = new Report();
|
|
@@ -3399,21 +3764,21 @@ async function readPartDirectory(part, directory) {
|
|
|
3399
3764
|
let present = true;
|
|
3400
3765
|
for (let i = 1; i <= segments.length; i++) {
|
|
3401
3766
|
const rel = segments.slice(0, i).join("/");
|
|
3402
|
-
if (await isLink(
|
|
3767
|
+
if (await isLink(join5(root, ...segments.slice(0, i)))) {
|
|
3403
3768
|
links.push(rel);
|
|
3404
3769
|
present = false;
|
|
3405
3770
|
break;
|
|
3406
3771
|
}
|
|
3407
3772
|
}
|
|
3408
|
-
if (present &&
|
|
3773
|
+
if (present && existsSync4(join5(root, ...segments))) paths.push(path);
|
|
3409
3774
|
}
|
|
3410
3775
|
for (const { dir, md } of layout.folders) {
|
|
3411
|
-
const full =
|
|
3776
|
+
const full = join5(root, dir);
|
|
3412
3777
|
if (await isLink(full)) {
|
|
3413
3778
|
links.push(dir);
|
|
3414
3779
|
continue;
|
|
3415
3780
|
}
|
|
3416
|
-
if (!
|
|
3781
|
+
if (!existsSync4(full)) continue;
|
|
3417
3782
|
const walked = await walk(full, { skipHidden: true });
|
|
3418
3783
|
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
3419
3784
|
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
@@ -3452,24 +3817,24 @@ function describePerson(id, person) {
|
|
|
3452
3817
|
);
|
|
3453
3818
|
return lines.join("\n");
|
|
3454
3819
|
}
|
|
3455
|
-
function registerTeamSetupTools(
|
|
3456
|
-
const fileSchema =
|
|
3457
|
-
path:
|
|
3458
|
-
content:
|
|
3820
|
+
function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
3821
|
+
const fileSchema = z16.object({
|
|
3822
|
+
path: z16.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
|
|
3823
|
+
content: z16.string().describe("The file's full text, exactly as it is")
|
|
3459
3824
|
});
|
|
3460
|
-
|
|
3825
|
+
server.registerTool(
|
|
3461
3826
|
"publish_setup_part",
|
|
3462
3827
|
{
|
|
3463
3828
|
title: "Publish Setup Part",
|
|
3464
3829
|
description: "Publish one part of your team's Claude Code setup to SystemDox: `hooks` (hooks/hooks.json and the scripts/ it calls), `mcp` (.mcp.json), `agents` (agents/*.md), `commands` (commands/*.md) or `instructions` (CLAUDE.md). Every part but instructions makes a new version of the team plugin, which each member accepts at their next update. Publishing hooks, mcp, agents or commands needs the admin role. " + (localFiles ? "Pass `directory`: the server reads only the files the part accepts, byte for byte, and refuses a link, a binary file or a file too large before anything is sent. Never retype the files into `files` when the folder is on this computer." : "Pass every file of the part in `files`, exactly as it is."),
|
|
3465
3830
|
inputSchema: {
|
|
3466
|
-
part:
|
|
3831
|
+
part: z16.enum(PARTS2).describe("Which part of the setup"),
|
|
3467
3832
|
...localFiles ? {
|
|
3468
|
-
directory:
|
|
3833
|
+
directory: z16.string().optional().describe(
|
|
3469
3834
|
"The folder on this computer: for hooks, the one holding hooks/hooks.json and scripts/; for mcp, the one holding .mcp.json; for instructions, the one holding CLAUDE.md; for agents or commands, the one holding agents/ or commands/"
|
|
3470
3835
|
)
|
|
3471
3836
|
} : {},
|
|
3472
|
-
files:
|
|
3837
|
+
files: z16.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
|
|
3473
3838
|
},
|
|
3474
3839
|
annotations: { readOnlyHint: false }
|
|
3475
3840
|
},
|
|
@@ -3499,13 +3864,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3499
3864
|
return text2(lines.join("\n"));
|
|
3500
3865
|
}
|
|
3501
3866
|
);
|
|
3502
|
-
|
|
3867
|
+
server.registerTool(
|
|
3503
3868
|
"resolve_person",
|
|
3504
3869
|
{
|
|
3505
3870
|
title: "Resolve Person",
|
|
3506
3871
|
description: "Read back a person that a team memory names by identifier, written as `con_\u2026` (label), `ctc_\u2026` or `usr_\u2026`: their label, name, phone number and email address, from the vault that holds them. The details are for this session only: never write them into a memory file; the memory keeps the identifier. Writes nothing.",
|
|
3507
3872
|
inputSchema: {
|
|
3508
|
-
id:
|
|
3873
|
+
id: z16.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
|
|
3509
3874
|
},
|
|
3510
3875
|
annotations: { readOnlyHint: true }
|
|
3511
3876
|
},
|
|
@@ -3516,14 +3881,14 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3516
3881
|
}
|
|
3517
3882
|
);
|
|
3518
3883
|
if (!localFiles) return;
|
|
3519
|
-
|
|
3884
|
+
server.registerTool(
|
|
3520
3885
|
"sync_memory",
|
|
3521
3886
|
{
|
|
3522
3887
|
title: "Sync Memory",
|
|
3523
3888
|
description: `Sync one project's team memory on this computer with SystemDox: pull the files the team changed, push the ones changed here, keep both texts of a file changed on both sides. It never answers for a person: a file that still holds a phone number, an email address or a home-folder path is skipped and listed, with the command for the member to run in their own terminal (\`${SYNC_MEMORY_COMMAND}\`). Pass dryRun to see what it would do without writing or sending anything.`,
|
|
3524
3889
|
inputSchema: {
|
|
3525
|
-
project:
|
|
3526
|
-
dryRun:
|
|
3890
|
+
project: z16.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
|
|
3891
|
+
dryRun: z16.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
|
|
3527
3892
|
},
|
|
3528
3893
|
annotations: { readOnlyHint: false }
|
|
3529
3894
|
},
|
|
@@ -3541,44 +3906,56 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3541
3906
|
}
|
|
3542
3907
|
|
|
3543
3908
|
// src/server.ts
|
|
3544
|
-
var SERVER_VERSION = true ? "0.1.
|
|
3545
|
-
function createServer(apiClient2, { localFiles = false } = {}) {
|
|
3546
|
-
const
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3909
|
+
var SERVER_VERSION = true ? "0.1.50" : "0.0.0-dev";
|
|
3910
|
+
function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
|
|
3911
|
+
const server = new McpServer(
|
|
3912
|
+
{
|
|
3913
|
+
name: "systemdox-mcp-server",
|
|
3914
|
+
version: SERVER_VERSION
|
|
3915
|
+
},
|
|
3916
|
+
instructions ? { instructions } : void 0
|
|
3917
|
+
);
|
|
3918
|
+
registerGetGuardrails(server, apiClient2);
|
|
3919
|
+
registerGetSpecs(server, apiClient2);
|
|
3920
|
+
registerGetDocs(server, apiClient2);
|
|
3921
|
+
registerGetTechStack(server, apiClient2);
|
|
3922
|
+
registerGetPlanningContext(server, apiClient2);
|
|
3923
|
+
registerGetPlanBoard(server, apiClient2);
|
|
3924
|
+
registerGetChange(server, apiClient2);
|
|
3925
|
+
registerGetContext(server, apiClient2);
|
|
3926
|
+
registerGetProjectRules(server, apiClient2);
|
|
3927
|
+
registerGetPromptTemplates(server, apiClient2);
|
|
3928
|
+
registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 });
|
|
3929
|
+
registerDocumentGraph(server, apiClient2);
|
|
3930
|
+
registerCreateGuardrail(server, apiClient2);
|
|
3931
|
+
registerUpdateSpec(server, apiClient2);
|
|
3932
|
+
registerReportDecision(server, apiClient2);
|
|
3933
|
+
registerCreateProjectRule(server, apiClient2);
|
|
3934
|
+
registerCreatePromptTemplate(server, apiClient2);
|
|
3935
|
+
registerSkillTools(server, apiClient2, { localFiles });
|
|
3936
|
+
registerTeamSetupTools(server, apiClient2, { localFiles });
|
|
3937
|
+
return server;
|
|
3568
3938
|
}
|
|
3569
3939
|
|
|
3570
3940
|
// src/index.ts
|
|
3571
3941
|
var config;
|
|
3942
|
+
var keyProblem;
|
|
3572
3943
|
try {
|
|
3573
3944
|
config = await loadLocalConfig();
|
|
3574
3945
|
} catch (err) {
|
|
3575
3946
|
console.error(`SystemDox MCP server: ${err.message}`);
|
|
3576
|
-
|
|
3947
|
+
keyProblem = keyReason(err.message);
|
|
3948
|
+
try {
|
|
3949
|
+
config = loadConfig(NO_KEY);
|
|
3950
|
+
} catch (urlErr) {
|
|
3951
|
+
console.error(`SystemDox MCP server: ${urlErr.message}`);
|
|
3952
|
+
process.exit(1);
|
|
3953
|
+
}
|
|
3577
3954
|
}
|
|
3578
3955
|
var apiClient = new SystemDoxApiClient(config);
|
|
3579
|
-
var server = createServer(apiClient, { localFiles: true });
|
|
3580
3956
|
async function main() {
|
|
3581
|
-
await apiClient
|
|
3957
|
+
const instructions = await sessionInstructions(apiClient, { localFiles: true, keyProblem });
|
|
3958
|
+
const server = createServer(apiClient, { localFiles: true, instructions, keyProblem });
|
|
3582
3959
|
const transport = new StdioServerTransport();
|
|
3583
3960
|
await server.connect(transport);
|
|
3584
3961
|
}
|