@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/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,239 @@ 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 afterInstall(skillMd) {
|
|
451
|
+
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.`;
|
|
452
|
+
}
|
|
453
|
+
function describeInstall(name, r) {
|
|
454
|
+
const lines = [
|
|
455
|
+
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
456
|
+
];
|
|
457
|
+
if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
|
|
458
|
+
if (r.keptEdited.length) {
|
|
459
|
+
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
460
|
+
}
|
|
461
|
+
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
462
|
+
lines.push(afterInstall(join(r.dest, "SKILL.md")));
|
|
463
|
+
return lines.join("\n");
|
|
464
|
+
}
|
|
465
|
+
|
|
277
466
|
// src/lib/team-setup-files.ts
|
|
467
|
+
import { execFile } from "child_process";
|
|
468
|
+
import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
469
|
+
import { homedir as homedir2, userInfo } from "os";
|
|
470
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
471
|
+
import { promisify } from "util";
|
|
278
472
|
var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
279
473
|
function isWorkspace(value) {
|
|
280
474
|
return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
|
|
281
475
|
}
|
|
282
476
|
function homeDir() {
|
|
283
|
-
return
|
|
477
|
+
return homedir2();
|
|
284
478
|
}
|
|
285
479
|
function systemdoxDir() {
|
|
286
|
-
return
|
|
480
|
+
return join2(homeDir(), ".claude", "systemdox");
|
|
287
481
|
}
|
|
288
482
|
function credentialsPath() {
|
|
289
|
-
return
|
|
483
|
+
return join2(systemdoxDir(), "credentials.json");
|
|
290
484
|
}
|
|
291
485
|
function workspaceDir(workspace) {
|
|
292
486
|
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
293
|
-
return
|
|
487
|
+
return join2(systemdoxDir(), workspace);
|
|
294
488
|
}
|
|
295
489
|
function statePath(workspace) {
|
|
296
|
-
return
|
|
490
|
+
return join2(workspaceDir(workspace), "state.json");
|
|
297
491
|
}
|
|
298
492
|
async function readJson(path, empty) {
|
|
299
493
|
let text3;
|
|
300
494
|
try {
|
|
301
|
-
text3 = await
|
|
495
|
+
text3 = await readFile2(path, "utf8");
|
|
302
496
|
} catch (e) {
|
|
303
497
|
if (e.code === "ENOENT") return empty;
|
|
304
498
|
throw e;
|
|
@@ -310,9 +504,9 @@ async function readJson(path, empty) {
|
|
|
310
504
|
}
|
|
311
505
|
}
|
|
312
506
|
async function writeJsonAtomically(path, value, mode) {
|
|
313
|
-
await
|
|
507
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
314
508
|
const tmp = `${path}.${process.pid}.tmp`;
|
|
315
|
-
await
|
|
509
|
+
await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
|
|
316
510
|
await rename(tmp, path);
|
|
317
511
|
}
|
|
318
512
|
async function readCredentials() {
|
|
@@ -357,6 +551,137 @@ async function updateState(workspace, change) {
|
|
|
357
551
|
return state;
|
|
358
552
|
}
|
|
359
553
|
|
|
554
|
+
// src/lib/session-brief.ts
|
|
555
|
+
var INSTRUCTIONS_MAX = 2e3;
|
|
556
|
+
var BRIEF_TIMEOUT_MS = 8e3;
|
|
557
|
+
var NO_KEY = "no-key-on-this-computer";
|
|
558
|
+
var SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
|
|
559
|
+
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.`;
|
|
560
|
+
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.";
|
|
561
|
+
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.";
|
|
562
|
+
function noKey(reason) {
|
|
563
|
+
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.`;
|
|
564
|
+
}
|
|
565
|
+
function keyReason(message) {
|
|
566
|
+
if (/holds keys for/.test(message)) return "keys for several workspaces are stored; set SYSTEMDOX_WORKSPACE to the one to use";
|
|
567
|
+
if (/holds no key for/.test(message)) return "SYSTEMDOX_WORKSPACE names a workspace with no stored key";
|
|
568
|
+
if (/No SystemDox key/.test(message)) return "none is stored";
|
|
569
|
+
return "the stored keys could not be read";
|
|
570
|
+
}
|
|
571
|
+
function olderApi(setup) {
|
|
572
|
+
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.`;
|
|
573
|
+
}
|
|
574
|
+
async function readJsonQuietly(path) {
|
|
575
|
+
try {
|
|
576
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
577
|
+
} catch {
|
|
578
|
+
return void 0;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function computerState(setup) {
|
|
582
|
+
const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
|
|
583
|
+
const installed = await readJsonQuietly(join3(homeDir(), ".claude", "plugins", "installed_plugins.json"));
|
|
584
|
+
const entries2 = installed?.plugins?.[id];
|
|
585
|
+
const entry = Array.isArray(entries2) ? entries2.find((e) => (e?.scope ?? "user") === "user") : void 0;
|
|
586
|
+
const state = {
|
|
587
|
+
missing: [],
|
|
588
|
+
behind: [],
|
|
589
|
+
notFromSystemDox: [],
|
|
590
|
+
pluginInstalled: !!entry,
|
|
591
|
+
pluginVersion: entry?.version
|
|
592
|
+
};
|
|
593
|
+
const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
|
|
594
|
+
for (const skill of setup.sessionBrief?.skills ?? []) {
|
|
595
|
+
const dir = defaultSkillDir(skill.name);
|
|
596
|
+
if (!existsSync2(dir)) {
|
|
597
|
+
if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const manifest = await readJsonQuietly(join3(dir, MANIFEST_FILE));
|
|
601
|
+
if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
|
|
602
|
+
state.notFromSystemDox.push(skill.name);
|
|
603
|
+
} else if (manifest.version !== skill.latestVersion) {
|
|
604
|
+
state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return state;
|
|
608
|
+
}
|
|
609
|
+
function hasPersonParts(setup) {
|
|
610
|
+
return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
|
|
611
|
+
}
|
|
612
|
+
var MORE = (n) => `${n} more (get_team_setup lists them)`;
|
|
613
|
+
var LIST_MIN = `all ${MORE(100)}`.length;
|
|
614
|
+
function listWithin(items, room) {
|
|
615
|
+
const whole2 = items.join(", ");
|
|
616
|
+
if (whole2.length <= room) return whole2;
|
|
617
|
+
const kept = [];
|
|
618
|
+
let used = 0;
|
|
619
|
+
for (let i = 0; i < items.length; i++) {
|
|
620
|
+
const rest = items.length - i - 1;
|
|
621
|
+
const cost = items[i].length + (kept.length ? 2 : 0);
|
|
622
|
+
if (used + cost + ` and ${MORE(rest)}`.length > room) break;
|
|
623
|
+
kept.push(items[i]);
|
|
624
|
+
used += cost;
|
|
625
|
+
}
|
|
626
|
+
return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
|
|
627
|
+
}
|
|
628
|
+
function localSection(setup, state, room) {
|
|
629
|
+
const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
|
|
630
|
+
const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
|
|
631
|
+
const lines = [];
|
|
632
|
+
if (state.missing.length) {
|
|
633
|
+
lines.push({
|
|
634
|
+
head: "On this computer, missing: ",
|
|
635
|
+
items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
|
|
636
|
+
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.`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
if (state.behind.length) {
|
|
640
|
+
lines.push({
|
|
641
|
+
head: "On this computer, behind: ",
|
|
642
|
+
items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
|
|
643
|
+
tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
if (state.notFromSystemDox.length) {
|
|
647
|
+
lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
|
|
648
|
+
}
|
|
649
|
+
const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
|
|
650
|
+
while (lines.length && fixed() > room) lines.pop();
|
|
651
|
+
if (!lines.length) return "";
|
|
652
|
+
const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
|
|
653
|
+
return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
|
|
654
|
+
}
|
|
655
|
+
function fit(text3, section) {
|
|
656
|
+
const whole2 = section ? `${text3}
|
|
657
|
+
|
|
658
|
+
${section}` : text3;
|
|
659
|
+
if (whole2.length <= INSTRUCTIONS_MAX) return whole2;
|
|
660
|
+
return text3.length <= INSTRUCTIONS_MAX ? text3 : `${text3.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
|
|
661
|
+
}
|
|
662
|
+
async function readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
|
|
663
|
+
if (keyProblem2) return { problem: noKey(keyProblem2) };
|
|
664
|
+
try {
|
|
665
|
+
const setup = await apiClient2.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
|
|
666
|
+
return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
|
|
667
|
+
} catch (err) {
|
|
668
|
+
const status = err?.status;
|
|
669
|
+
return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
async function sessionInstructions(apiClient2, options = {}) {
|
|
673
|
+
const read = await readTeamSetup(apiClient2, options);
|
|
674
|
+
if ("problem" in read) return read.problem;
|
|
675
|
+
const text3 = read.setup.sessionBrief?.text ?? olderApi(read.setup);
|
|
676
|
+
try {
|
|
677
|
+
if (!options.localFiles) return fit(text3, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
|
|
678
|
+
const room = INSTRUCTIONS_MAX - text3.length - 2;
|
|
679
|
+
return fit(text3, localSection(read.setup, await computerState(read.setup), room));
|
|
680
|
+
} catch {
|
|
681
|
+
return fit(text3, "");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
360
685
|
// src/server.ts
|
|
361
686
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
362
687
|
|
|
@@ -469,7 +794,7 @@ var inputSchema = () => ({
|
|
|
469
794
|
"File path being edited \u2014 language is auto-detected from the extension"
|
|
470
795
|
)
|
|
471
796
|
});
|
|
472
|
-
function registerGetGuardrails(
|
|
797
|
+
function registerGetGuardrails(server, apiClient2) {
|
|
473
798
|
const handler = async ({
|
|
474
799
|
language,
|
|
475
800
|
category,
|
|
@@ -523,7 +848,7 @@ ${text3}`
|
|
|
523
848
|
]
|
|
524
849
|
};
|
|
525
850
|
};
|
|
526
|
-
|
|
851
|
+
server.registerTool(
|
|
527
852
|
"get_checks",
|
|
528
853
|
{
|
|
529
854
|
title: "Get Checks",
|
|
@@ -532,7 +857,7 @@ ${text3}`
|
|
|
532
857
|
},
|
|
533
858
|
handler
|
|
534
859
|
);
|
|
535
|
-
|
|
860
|
+
server.registerTool(
|
|
536
861
|
"get_guardrails",
|
|
537
862
|
{
|
|
538
863
|
title: "Get Checks (legacy name)",
|
|
@@ -545,8 +870,8 @@ ${text3}`
|
|
|
545
870
|
|
|
546
871
|
// src/tools/get-specs.ts
|
|
547
872
|
import { z as z2 } from "zod";
|
|
548
|
-
function registerGetSpecs(
|
|
549
|
-
|
|
873
|
+
function registerGetSpecs(server, apiClient2) {
|
|
874
|
+
server.registerTool(
|
|
550
875
|
"get_specs",
|
|
551
876
|
{
|
|
552
877
|
title: "Get Specs",
|
|
@@ -631,8 +956,8 @@ function matchGlob(path, pattern) {
|
|
|
631
956
|
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
632
957
|
return new RegExp(`^${regex}$`).test(path);
|
|
633
958
|
}
|
|
634
|
-
function registerGetDocs(
|
|
635
|
-
|
|
959
|
+
function registerGetDocs(server, apiClient2) {
|
|
960
|
+
server.registerTool(
|
|
636
961
|
"get_docs",
|
|
637
962
|
{
|
|
638
963
|
title: "Get Docs",
|
|
@@ -730,8 +1055,8 @@ Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
|
730
1055
|
}
|
|
731
1056
|
|
|
732
1057
|
// src/tools/get-tech-stack.ts
|
|
733
|
-
function registerGetTechStack(
|
|
734
|
-
|
|
1058
|
+
function registerGetTechStack(server, apiClient2) {
|
|
1059
|
+
server.registerTool(
|
|
735
1060
|
"get_tech_stack",
|
|
736
1061
|
{
|
|
737
1062
|
title: "Get Tech Stack",
|
|
@@ -780,8 +1105,8 @@ ${sections}`
|
|
|
780
1105
|
|
|
781
1106
|
// src/tools/get-planning-context.ts
|
|
782
1107
|
import { z as z4 } from "zod";
|
|
783
|
-
function registerGetPlanningContext(
|
|
784
|
-
|
|
1108
|
+
function registerGetPlanningContext(server, apiClient2) {
|
|
1109
|
+
server.registerTool(
|
|
785
1110
|
"get_planning_context",
|
|
786
1111
|
{
|
|
787
1112
|
title: "Get Planning Context",
|
|
@@ -815,7 +1140,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
|
|
|
815
1140
|
${repos}`);
|
|
816
1141
|
}
|
|
817
1142
|
if (ctx.dependencies.length > 0) {
|
|
818
|
-
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
|
|
1143
|
+
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}${d.provenance === "inferred" ? " (inferred)" : ""}`).join("\n");
|
|
819
1144
|
sections.push(`## Cross-Repo Dependencies
|
|
820
1145
|
|
|
821
1146
|
${deps}`);
|
|
@@ -902,8 +1227,8 @@ function formatItem(item) {
|
|
|
902
1227
|
${meta.join(" \xB7 ")}` : "") + (item.url ? `
|
|
903
1228
|
${item.url}` : "");
|
|
904
1229
|
}
|
|
905
|
-
function registerGetPlanBoard(
|
|
906
|
-
|
|
1230
|
+
function registerGetPlanBoard(server, apiClient2) {
|
|
1231
|
+
server.registerTool(
|
|
907
1232
|
"get_plan_board",
|
|
908
1233
|
{
|
|
909
1234
|
title: "Get Plan Board",
|
|
@@ -1147,8 +1472,8 @@ function formatChange(change) {
|
|
|
1147
1472
|
briefVerdict(change.states)
|
|
1148
1473
|
].join("\n\n");
|
|
1149
1474
|
}
|
|
1150
|
-
function registerGetChange(
|
|
1151
|
-
|
|
1475
|
+
function registerGetChange(server, apiClient2) {
|
|
1476
|
+
server.registerTool(
|
|
1152
1477
|
"get_change",
|
|
1153
1478
|
{
|
|
1154
1479
|
title: "Get Change",
|
|
@@ -1515,16 +1840,22 @@ async function hydrate(api, item, meta) {
|
|
|
1515
1840
|
}
|
|
1516
1841
|
async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
|
|
1517
1842
|
const budget = maxTokens ?? DEFAULT_BUDGET;
|
|
1843
|
+
const failures = [];
|
|
1844
|
+
const orEmpty = (empty) => (err) => {
|
|
1845
|
+
failures.push(err);
|
|
1846
|
+
return empty;
|
|
1847
|
+
};
|
|
1518
1848
|
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((
|
|
1849
|
+
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
1850
|
+
api.get("/v1/specs").catch(orEmpty([])),
|
|
1851
|
+
api.get("/v1/docs").catch(orEmpty({ items: [] })),
|
|
1852
|
+
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
1853
|
+
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
1524
1854
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
1525
1855
|
// blip) gates nothing, it can never hide working context.
|
|
1526
1856
|
api.get("/v1/doc-repos").catch(() => null)
|
|
1527
1857
|
]);
|
|
1858
|
+
if (failures.length === 5) throw failures[0];
|
|
1528
1859
|
const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
|
|
1529
1860
|
const guardrails = disabled("fitness") ? [] : rawGuardrails;
|
|
1530
1861
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
@@ -1731,18 +2062,19 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
|
|
|
1731
2062
|
guardrails: selectedGuardrails.length
|
|
1732
2063
|
};
|
|
1733
2064
|
if (sections.length === 0) {
|
|
1734
|
-
|
|
2065
|
+
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.";
|
|
2066
|
+
return { text: text3, stats };
|
|
1735
2067
|
}
|
|
1736
2068
|
const footer = `
|
|
1737
2069
|
|
|
1738
2070
|
---
|
|
1739
|
-
_Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
|
|
2071
|
+
_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
2072
|
return { text: body + footer, stats };
|
|
1741
2073
|
}
|
|
1742
2074
|
|
|
1743
2075
|
// src/tools/get-context.ts
|
|
1744
|
-
function registerGetContext(
|
|
1745
|
-
|
|
2076
|
+
function registerGetContext(server, apiClient2) {
|
|
2077
|
+
server.registerTool(
|
|
1746
2078
|
"get_context",
|
|
1747
2079
|
{
|
|
1748
2080
|
title: "Get Context",
|
|
@@ -1770,8 +2102,240 @@ function registerGetContext(server2, apiClient2) {
|
|
|
1770
2102
|
);
|
|
1771
2103
|
}
|
|
1772
2104
|
|
|
1773
|
-
// src/tools/
|
|
2105
|
+
// src/tools/get-team-setup.ts
|
|
2106
|
+
var HELD_BECAUSE = {
|
|
2107
|
+
"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`,
|
|
2108
|
+
"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>`",
|
|
2109
|
+
"not-checked": "it could not be checked. Ask again later"
|
|
2110
|
+
};
|
|
2111
|
+
function describeSetup(setup) {
|
|
2112
|
+
const lines = [`# Team setup of ${setup.workspace}`, ""];
|
|
2113
|
+
const skills = setup.sessionBrief?.skills ?? [];
|
|
2114
|
+
const held = setup.sessionBrief?.heldSkills ?? [];
|
|
2115
|
+
lines.push(`## Skills a session installs by itself (${skills.length})`, "");
|
|
2116
|
+
if (skills.length) {
|
|
2117
|
+
for (const s of skills) lines.push(`- **${s.name}** version ${s.latestVersion}: ${s.description}`);
|
|
2118
|
+
} else if (!setup.sessionBrief && setup.skills.length) {
|
|
2119
|
+
lines.push(`- ${setup.skills.join(", ")}`);
|
|
2120
|
+
} else {
|
|
2121
|
+
lines.push("None.");
|
|
2122
|
+
}
|
|
2123
|
+
lines.push("");
|
|
2124
|
+
if (held.length) {
|
|
2125
|
+
lines.push(`## Skills that wait for the person (${held.length})`, "");
|
|
2126
|
+
for (const h of held) lines.push(`- **${h.name}**: ${HELD_BECAUSE[h.reason] ?? h.reason}`);
|
|
2127
|
+
lines.push("", "Do not install these yourself: each is the person's to decide.", "");
|
|
2128
|
+
}
|
|
2129
|
+
if (setup.plugin) {
|
|
2130
|
+
lines.push(`## Team plugin`, "", `\`${setup.workspace}-setup\` version ${setup.plugin.version}.`);
|
|
2131
|
+
for (const c of setup.plugin.hookCommands ?? []) lines.push(`- Hook: ${JSON.stringify(c)}`);
|
|
2132
|
+
for (const m of setup.plugin.mcpServers ?? []) {
|
|
2133
|
+
lines.push(`- MCP server ${JSON.stringify(m.name)}: ${JSON.stringify(m.url ?? [m.command, ...m.args ?? []].filter(Boolean).join(" "))}`);
|
|
2134
|
+
}
|
|
2135
|
+
lines.push("");
|
|
2136
|
+
}
|
|
2137
|
+
const parts = Object.entries(setup.parts ?? {});
|
|
2138
|
+
if (parts.length) {
|
|
2139
|
+
lines.push(`## Published parts`, "", ...parts.map(([p, v]) => `- ${p}: version ${v}`), "");
|
|
2140
|
+
}
|
|
2141
|
+
if (setup.memory?.length) {
|
|
2142
|
+
lines.push(`## Team memory`, "", ...setup.memory.map((m) => `- ${m.project}: ${m.fileCount} file(s)`), "");
|
|
2143
|
+
}
|
|
2144
|
+
return lines;
|
|
2145
|
+
}
|
|
2146
|
+
function describeComputer(setup, state) {
|
|
2147
|
+
const lines = ["## This computer", ""];
|
|
2148
|
+
if (!setup.sessionBrief) {
|
|
2149
|
+
return [...lines, "This SystemDox API is too old to compare this computer with the team setup."];
|
|
2150
|
+
}
|
|
2151
|
+
const skills = setup.sessionBrief.skills;
|
|
2152
|
+
const latest = new Map(skills.map((s) => [s.name, s.latestVersion]));
|
|
2153
|
+
lines.push(
|
|
2154
|
+
state.missing.length ? `- Missing: ${state.missing.map((name) => `${name} version ${latest.get(name)}`).join(", ")}` : `- Missing: none of ${skills.length}`
|
|
2155
|
+
);
|
|
2156
|
+
if (state.behind.length) {
|
|
2157
|
+
lines.push(`- Behind: ${state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`).join(", ")}`);
|
|
2158
|
+
}
|
|
2159
|
+
if (state.notFromSystemDox.length) lines.push(`- Not from SystemDox, left alone: ${state.notFromSystemDox.join(", ")}`);
|
|
2160
|
+
if (setup.plugin) {
|
|
2161
|
+
lines.push(
|
|
2162
|
+
state.pluginInstalled ? `- Team plugin: installed${state.pluginVersion ? `, ${state.pluginVersion}` : ""}; the latest is version ${setup.plugin.version}` : "- Team plugin: not installed"
|
|
2163
|
+
);
|
|
2164
|
+
}
|
|
2165
|
+
lines.push("", "## What to do", "");
|
|
2166
|
+
const steps = [];
|
|
2167
|
+
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.");
|
|
2168
|
+
if (state.behind.length) {
|
|
2169
|
+
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.");
|
|
2170
|
+
}
|
|
2171
|
+
if (hasPersonParts(setup) && !state.pluginInstalled) {
|
|
2172
|
+
steps.push(
|
|
2173
|
+
`Hooks, MCP servers, team instructions and memory need the person: \`${SETUP_COMMAND}\` in their own terminal. Never answer its prompts for them.`
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
lines.push(...steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ["Nothing."]);
|
|
2177
|
+
return lines;
|
|
2178
|
+
}
|
|
2179
|
+
function registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 }) {
|
|
2180
|
+
server.registerTool(
|
|
2181
|
+
"get_team_setup",
|
|
2182
|
+
{
|
|
2183
|
+
title: "Get Team Setup",
|
|
2184
|
+
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.",
|
|
2185
|
+
inputSchema: {},
|
|
2186
|
+
annotations: { readOnlyHint: true }
|
|
2187
|
+
},
|
|
2188
|
+
async () => {
|
|
2189
|
+
const read = await readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs: 15e3 });
|
|
2190
|
+
if ("problem" in read) return { content: [{ type: "text", text: read.problem }] };
|
|
2191
|
+
const lines = describeSetup(read.setup);
|
|
2192
|
+
if (localFiles) lines.push(...describeComputer(read.setup, await computerState(read.setup)));
|
|
2193
|
+
else lines.push(CANNOT_SEE);
|
|
2194
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
2195
|
+
}
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// src/tools/document-graph.ts
|
|
1774
2200
|
import { z as z8 } from "zod";
|
|
2201
|
+
var DEFAULT_LIMIT2 = 50;
|
|
2202
|
+
var MAX_LIMIT = 200;
|
|
2203
|
+
var DEFAULT_HOPS = 6;
|
|
2204
|
+
var MAX_HOPS = 8;
|
|
2205
|
+
function place(file, line) {
|
|
2206
|
+
return file ? `${file}:${line}` : "made by a person";
|
|
2207
|
+
}
|
|
2208
|
+
function known(provenance, confidence) {
|
|
2209
|
+
return `${provenance} ${confidence}`;
|
|
2210
|
+
}
|
|
2211
|
+
function formatNeighbours(answer) {
|
|
2212
|
+
const lines = [`# ${answer.doc.label}`, `\`${answer.doc.id}\``, ""];
|
|
2213
|
+
if (answer.links.length === 0) lines.push("No links.");
|
|
2214
|
+
for (const link of answer.links) {
|
|
2215
|
+
lines.push(
|
|
2216
|
+
`- ${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)}`
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
if (answer.truncated) {
|
|
2220
|
+
lines.push("", `Showing ${answer.links.length} of ${answer.total}. Raise limit to see more.`);
|
|
2221
|
+
}
|
|
2222
|
+
if (answer.ambiguous.length > 0) {
|
|
2223
|
+
lines.push("", "## Ambiguous: not drawn, because each could mean several documents");
|
|
2224
|
+
for (const entry of answer.ambiguous) {
|
|
2225
|
+
const candidates = entry.candidates.map((c) => `${c.label} (${c.repo}, \`${c.id}\`)`);
|
|
2226
|
+
lines.push(`- ${entry.reference} at ${place(entry.file, entry.line)}: ${candidates.join("; ")}`);
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
return lines.join("\n");
|
|
2230
|
+
}
|
|
2231
|
+
function formatPath(answer, maxHops) {
|
|
2232
|
+
if (!answer.found) return `No path within ${maxHops} hops.`;
|
|
2233
|
+
const lines = answer.steps.map((step, index) => {
|
|
2234
|
+
const arrow = step.reversed ? "<-" : "->";
|
|
2235
|
+
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)}`;
|
|
2236
|
+
});
|
|
2237
|
+
return [...lines, "", `Path confidence: ${answer.confidence}`].join("\n");
|
|
2238
|
+
}
|
|
2239
|
+
function formatStats(stats, repoLinks) {
|
|
2240
|
+
const { declared, extracted, inferred, ambiguous } = stats.links;
|
|
2241
|
+
const lines = [
|
|
2242
|
+
`# The graph: ${stats.nodeCount} documents, ${stats.edgeCount} links`,
|
|
2243
|
+
"",
|
|
2244
|
+
`Links: ${declared} declared \xB7 ${extracted} extracted \xB7 ${inferred} inferred \xB7 ${ambiguous} ambiguous (not drawn)`,
|
|
2245
|
+
"",
|
|
2246
|
+
"## Repositories"
|
|
2247
|
+
];
|
|
2248
|
+
for (const repo of stats.repos) {
|
|
2249
|
+
const state = repo.behind ? `BEHIND: pushed ${repo.headCommit || "unknown"} at ${repo.pushedAt}, index at ${repo.indexedCommit || "unknown"}` : `index at ${repo.indexedCommit || "unknown"}`;
|
|
2250
|
+
lines.push(`- ${repo.repo}: ${repo.documents} documents, ${repo.links} links, ${state}`);
|
|
2251
|
+
}
|
|
2252
|
+
if (repoLinks.length > 0) {
|
|
2253
|
+
lines.push("", "## Repository links");
|
|
2254
|
+
for (const link of repoLinks) {
|
|
2255
|
+
lines.push(
|
|
2256
|
+
`- ${link.source} ${link.relation} ${link.target} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${place(link.file, link.line)}`
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
return lines.join("\n");
|
|
2261
|
+
}
|
|
2262
|
+
function refused(err) {
|
|
2263
|
+
const status = err.status;
|
|
2264
|
+
if (status !== 404 && status !== 400) throw err;
|
|
2265
|
+
return {
|
|
2266
|
+
content: [{ type: "text", text: err.message }],
|
|
2267
|
+
isError: true
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
function registerDocumentGraph(server, apiClient2) {
|
|
2271
|
+
server.registerTool(
|
|
2272
|
+
"get_document_links",
|
|
2273
|
+
{
|
|
2274
|
+
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.",
|
|
2275
|
+
inputSchema: {
|
|
2276
|
+
doc_id: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
2277
|
+
direction: z8.enum(["out", "in", "both"]).optional().describe("Default both"),
|
|
2278
|
+
relation: z8.enum(["links_to", "cites", "mentions", "satisfies", "references"]).optional().describe("Only links of this kind"),
|
|
2279
|
+
limit: z8.number().int().min(1).max(MAX_LIMIT).optional().describe(`Most links to show (default ${DEFAULT_LIMIT2})`)
|
|
2280
|
+
},
|
|
2281
|
+
annotations: { readOnlyHint: true }
|
|
2282
|
+
},
|
|
2283
|
+
async ({ doc_id, direction, relation, limit }) => {
|
|
2284
|
+
const params = { doc: doc_id, limit: String(limit ?? DEFAULT_LIMIT2) };
|
|
2285
|
+
if (direction) params.direction = direction;
|
|
2286
|
+
if (relation) params.relation = relation;
|
|
2287
|
+
try {
|
|
2288
|
+
const answer = await apiClient2.get("/v1/graph/neighbours", params);
|
|
2289
|
+
return { content: [{ type: "text", text: formatNeighbours(answer) }] };
|
|
2290
|
+
} catch (err) {
|
|
2291
|
+
return refused(err);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
);
|
|
2295
|
+
server.registerTool(
|
|
2296
|
+
"trace_document_path",
|
|
2297
|
+
{
|
|
2298
|
+
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.",
|
|
2299
|
+
inputSchema: {
|
|
2300
|
+
from: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
2301
|
+
to: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
2302
|
+
max_hops: z8.number().int().min(1).max(MAX_HOPS).optional().describe(`Longest path to look for (default ${DEFAULT_HOPS})`)
|
|
2303
|
+
},
|
|
2304
|
+
annotations: { readOnlyHint: true }
|
|
2305
|
+
},
|
|
2306
|
+
async ({ from, to, max_hops }) => {
|
|
2307
|
+
const hops = max_hops ?? DEFAULT_HOPS;
|
|
2308
|
+
try {
|
|
2309
|
+
const answer = await apiClient2.get("/v1/graph/path", {
|
|
2310
|
+
from,
|
|
2311
|
+
to,
|
|
2312
|
+
maxHops: String(hops)
|
|
2313
|
+
});
|
|
2314
|
+
return { content: [{ type: "text", text: formatPath(answer, hops) }] };
|
|
2315
|
+
} catch (err) {
|
|
2316
|
+
return refused(err);
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
);
|
|
2320
|
+
server.registerTool(
|
|
2321
|
+
"get_graph_stats",
|
|
2322
|
+
{
|
|
2323
|
+
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.",
|
|
2324
|
+
inputSchema: {},
|
|
2325
|
+
annotations: { readOnlyHint: true }
|
|
2326
|
+
},
|
|
2327
|
+
async () => {
|
|
2328
|
+
const [stats, repos] = await Promise.all([
|
|
2329
|
+
apiClient2.getFresh("/v1/graph/stats"),
|
|
2330
|
+
apiClient2.getFresh("/v1/graph/repos")
|
|
2331
|
+
]);
|
|
2332
|
+
return { content: [{ type: "text", text: formatStats(stats, repos.links) }] };
|
|
2333
|
+
}
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
// src/tools/create-guardrail.ts
|
|
2338
|
+
import { z as z9 } from "zod";
|
|
1775
2339
|
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
2340
|
var CATEGORIES = [
|
|
1777
2341
|
"imports",
|
|
@@ -1784,15 +2348,15 @@ var CATEGORIES = [
|
|
|
1784
2348
|
"data-access"
|
|
1785
2349
|
];
|
|
1786
2350
|
var inputSchema2 = () => ({
|
|
1787
|
-
title:
|
|
1788
|
-
description:
|
|
1789
|
-
severity:
|
|
1790
|
-
category:
|
|
1791
|
-
pattern:
|
|
1792
|
-
fix:
|
|
1793
|
-
fitness_test:
|
|
2351
|
+
title: z9.string().describe("Short title for the check"),
|
|
2352
|
+
description: z9.string().describe("Detailed description of what the check enforces and why"),
|
|
2353
|
+
severity: z9.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
|
|
2354
|
+
category: z9.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
|
|
2355
|
+
pattern: z9.string().optional().describe("Bad code pattern that should be avoided"),
|
|
2356
|
+
fix: z9.string().optional().describe("Correct code pattern to use instead"),
|
|
2357
|
+
fitness_test: z9.string().optional().describe("Shell command to detect violations in CI")
|
|
1794
2358
|
});
|
|
1795
|
-
function registerCreateGuardrail(
|
|
2359
|
+
function registerCreateGuardrail(server, apiClient2) {
|
|
1796
2360
|
const handler = async ({
|
|
1797
2361
|
title,
|
|
1798
2362
|
description,
|
|
@@ -1829,7 +2393,7 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
1829
2393
|
]
|
|
1830
2394
|
};
|
|
1831
2395
|
};
|
|
1832
|
-
|
|
2396
|
+
server.registerTool(
|
|
1833
2397
|
"create_check",
|
|
1834
2398
|
{
|
|
1835
2399
|
title: "Create Check",
|
|
@@ -1841,7 +2405,7 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
1841
2405
|
},
|
|
1842
2406
|
handler
|
|
1843
2407
|
);
|
|
1844
|
-
|
|
2408
|
+
server.registerTool(
|
|
1845
2409
|
"create_guardrail",
|
|
1846
2410
|
{
|
|
1847
2411
|
title: "Create Check (legacy name)",
|
|
@@ -1856,20 +2420,20 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
1856
2420
|
}
|
|
1857
2421
|
|
|
1858
2422
|
// src/tools/update-spec.ts
|
|
1859
|
-
import { z as
|
|
1860
|
-
function registerUpdateSpec(
|
|
1861
|
-
|
|
2423
|
+
import { z as z10 } from "zod";
|
|
2424
|
+
function registerUpdateSpec(server, apiClient2) {
|
|
2425
|
+
server.registerTool(
|
|
1862
2426
|
"update_spec",
|
|
1863
2427
|
{
|
|
1864
2428
|
title: "Update Spec",
|
|
1865
2429
|
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
2430
|
inputSchema: {
|
|
1867
|
-
draft_id:
|
|
1868
|
-
section:
|
|
1869
|
-
content:
|
|
2431
|
+
draft_id: z10.string().describe("ID of the spec to update"),
|
|
2432
|
+
section: z10.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
|
|
2433
|
+
content: z10.string().describe(
|
|
1870
2434
|
"New markdown content for the section. For requirements, provide the full requirement content."
|
|
1871
2435
|
),
|
|
1872
|
-
requirement_name:
|
|
2436
|
+
requirement_name: z10.string().optional().describe(
|
|
1873
2437
|
"Name of the requirement (required when section is 'requirements')"
|
|
1874
2438
|
)
|
|
1875
2439
|
},
|
|
@@ -1919,19 +2483,19 @@ function registerUpdateSpec(server2, apiClient2) {
|
|
|
1919
2483
|
}
|
|
1920
2484
|
|
|
1921
2485
|
// src/tools/report-decision.ts
|
|
1922
|
-
import { z as
|
|
1923
|
-
function registerReportDecision(
|
|
1924
|
-
|
|
2486
|
+
import { z as z11 } from "zod";
|
|
2487
|
+
function registerReportDecision(server, apiClient2) {
|
|
2488
|
+
server.registerTool(
|
|
1925
2489
|
"report_decision",
|
|
1926
2490
|
{
|
|
1927
2491
|
title: "Report Decision",
|
|
1928
2492
|
description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
|
|
1929
2493
|
inputSchema: {
|
|
1930
|
-
title:
|
|
1931
|
-
context:
|
|
1932
|
-
decision:
|
|
1933
|
-
consequences:
|
|
1934
|
-
related_files:
|
|
2494
|
+
title: z11.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
|
|
2495
|
+
context: z11.string().describe("What prompted this decision \u2014 the problem or constraint"),
|
|
2496
|
+
decision: z11.string().describe("What was decided and why"),
|
|
2497
|
+
consequences: z11.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
|
|
2498
|
+
related_files: z11.array(z11.string()).optional().describe("File paths related to this decision")
|
|
1935
2499
|
},
|
|
1936
2500
|
annotations: {
|
|
1937
2501
|
readOnlyHint: false
|
|
@@ -1982,7 +2546,7 @@ Status: draft \u2014 review in SystemDox before publishing.`
|
|
|
1982
2546
|
}
|
|
1983
2547
|
|
|
1984
2548
|
// src/tools/get-project-rules.ts
|
|
1985
|
-
import { z as
|
|
2549
|
+
import { z as z12 } from "zod";
|
|
1986
2550
|
var CATEGORY_LABELS = {
|
|
1987
2551
|
deployment: "Deployment",
|
|
1988
2552
|
architecture: "Architecture",
|
|
@@ -1991,14 +2555,14 @@ var CATEGORY_LABELS = {
|
|
|
1991
2555
|
process: "Process",
|
|
1992
2556
|
other: "Other"
|
|
1993
2557
|
};
|
|
1994
|
-
function registerGetProjectRules(
|
|
1995
|
-
|
|
2558
|
+
function registerGetProjectRules(server, apiClient2) {
|
|
2559
|
+
server.registerTool(
|
|
1996
2560
|
"get_project_rules",
|
|
1997
2561
|
{
|
|
1998
2562
|
title: "Get Project Rules",
|
|
1999
2563
|
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
2564
|
inputSchema: {
|
|
2001
|
-
category:
|
|
2565
|
+
category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
|
|
2002
2566
|
}
|
|
2003
2567
|
},
|
|
2004
2568
|
async ({ category }) => {
|
|
@@ -2050,18 +2614,18 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
2050
2614
|
}
|
|
2051
2615
|
|
|
2052
2616
|
// src/tools/create-project-rule.ts
|
|
2053
|
-
import { z as
|
|
2054
|
-
function registerCreateProjectRule(
|
|
2055
|
-
|
|
2617
|
+
import { z as z13 } from "zod";
|
|
2618
|
+
function registerCreateProjectRule(server, apiClient2) {
|
|
2619
|
+
server.registerTool(
|
|
2056
2620
|
"create_project_rule",
|
|
2057
2621
|
{
|
|
2058
2622
|
title: "Create Project Rule",
|
|
2059
2623
|
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
2624
|
inputSchema: {
|
|
2061
|
-
text:
|
|
2625
|
+
text: z13.string().describe(
|
|
2062
2626
|
"The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
|
|
2063
2627
|
),
|
|
2064
|
-
category:
|
|
2628
|
+
category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
|
|
2065
2629
|
},
|
|
2066
2630
|
annotations: {
|
|
2067
2631
|
readOnlyHint: false
|
|
@@ -2086,8 +2650,8 @@ function registerCreateProjectRule(server2, apiClient2) {
|
|
|
2086
2650
|
}
|
|
2087
2651
|
|
|
2088
2652
|
// src/tools/get-prompt-templates.ts
|
|
2089
|
-
function registerGetPromptTemplates(
|
|
2090
|
-
|
|
2653
|
+
function registerGetPromptTemplates(server, apiClient2) {
|
|
2654
|
+
server.registerTool(
|
|
2091
2655
|
"get_prompt_templates",
|
|
2092
2656
|
{
|
|
2093
2657
|
title: "Get Prompt Templates",
|
|
@@ -2132,17 +2696,17 @@ ${text3}`
|
|
|
2132
2696
|
}
|
|
2133
2697
|
|
|
2134
2698
|
// src/tools/create-prompt-template.ts
|
|
2135
|
-
import { z as
|
|
2136
|
-
function registerCreatePromptTemplate(
|
|
2137
|
-
|
|
2699
|
+
import { z as z14 } from "zod";
|
|
2700
|
+
function registerCreatePromptTemplate(server, apiClient2) {
|
|
2701
|
+
server.registerTool(
|
|
2138
2702
|
"create_prompt_template",
|
|
2139
2703
|
{
|
|
2140
2704
|
title: "Create Prompt Template",
|
|
2141
2705
|
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
2706
|
inputSchema: {
|
|
2143
|
-
name:
|
|
2144
|
-
description:
|
|
2145
|
-
prompt:
|
|
2707
|
+
name: z14.string().describe("Short name for the template"),
|
|
2708
|
+
description: z14.string().optional().describe("Brief description of when to use this template"),
|
|
2709
|
+
prompt: z14.string().describe("The full prompt text that Gen AI will use when generating content")
|
|
2146
2710
|
},
|
|
2147
2711
|
annotations: {
|
|
2148
2712
|
readOnlyHint: false
|
|
@@ -2171,205 +2735,9 @@ function registerCreatePromptTemplate(server2, apiClient2) {
|
|
|
2171
2735
|
}
|
|
2172
2736
|
|
|
2173
2737
|
// src/tools/skills.ts
|
|
2174
|
-
import { z as
|
|
2738
|
+
import { z as z15 } from "zod";
|
|
2175
2739
|
import { homedir as homedir3 } from "os";
|
|
2176
2740
|
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
2741
|
var SKILLS = "/v1/standards/skills";
|
|
2374
2742
|
function expandHome(path) {
|
|
2375
2743
|
return path.replace(/^~(?=$|[\\/])/, homedir3());
|
|
@@ -2377,25 +2745,25 @@ function expandHome(path) {
|
|
|
2377
2745
|
function text(t) {
|
|
2378
2746
|
return { content: [{ type: "text", text: t }] };
|
|
2379
2747
|
}
|
|
2380
|
-
function registerSkillTools(
|
|
2381
|
-
const fileSchema =
|
|
2382
|
-
path:
|
|
2383
|
-
content:
|
|
2748
|
+
function registerSkillTools(server, apiClient2, { localFiles }) {
|
|
2749
|
+
const fileSchema = z15.object({
|
|
2750
|
+
path: z15.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
|
|
2751
|
+
content: z15.string().describe("The file's full text, exactly as it is")
|
|
2384
2752
|
});
|
|
2385
|
-
|
|
2753
|
+
server.registerTool(
|
|
2386
2754
|
"publish_skill",
|
|
2387
2755
|
{
|
|
2388
2756
|
title: "Publish Skill",
|
|
2389
2757
|
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
2758
|
inputSchema: {
|
|
2391
2759
|
...localFiles ? {
|
|
2392
|
-
directory:
|
|
2760
|
+
directory: z15.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
|
|
2393
2761
|
} : {},
|
|
2394
|
-
files:
|
|
2395
|
-
name:
|
|
2396
|
-
spaceId:
|
|
2397
|
-
note:
|
|
2398
|
-
baseVersion:
|
|
2762
|
+
files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
|
|
2763
|
+
name: z15.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
|
|
2764
|
+
spaceId: z15.string().optional().describe("Space for a NEW skill (default: your default space)"),
|
|
2765
|
+
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")`),
|
|
2766
|
+
baseVersion: z15.number().int().positive().optional().describe(
|
|
2399
2767
|
"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
2768
|
)
|
|
2401
2769
|
},
|
|
@@ -2433,7 +2801,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2433
2801
|
);
|
|
2434
2802
|
}
|
|
2435
2803
|
);
|
|
2436
|
-
|
|
2804
|
+
server.registerTool(
|
|
2437
2805
|
"list_skills",
|
|
2438
2806
|
{
|
|
2439
2807
|
title: "List Skills",
|
|
@@ -2455,16 +2823,16 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2455
2823
|
);
|
|
2456
2824
|
}
|
|
2457
2825
|
);
|
|
2458
|
-
|
|
2826
|
+
server.registerTool(
|
|
2459
2827
|
"install_skill",
|
|
2460
2828
|
{
|
|
2461
2829
|
title: "Install Skill",
|
|
2462
2830
|
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
2831
|
inputSchema: {
|
|
2464
|
-
name:
|
|
2465
|
-
version:
|
|
2466
|
-
target:
|
|
2467
|
-
...localFiles ? { overwrite:
|
|
2832
|
+
name: z15.string().describe("The skill's name"),
|
|
2833
|
+
version: z15.number().int().positive().optional().describe("A version number (default: the latest)"),
|
|
2834
|
+
target: z15.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
|
|
2835
|
+
...localFiles ? { overwrite: z15.boolean().optional().describe("Replace files edited since they were installed") } : {}
|
|
2468
2836
|
},
|
|
2469
2837
|
annotations: { readOnlyHint: !localFiles }
|
|
2470
2838
|
},
|
|
@@ -2495,7 +2863,7 @@ ${fence}
|
|
|
2495
2863
|
${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
|
|
2496
2864
|
}).join("\n\n");
|
|
2497
2865
|
return text(
|
|
2498
|
-
`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
|
|
2866
|
+
`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`)}
|
|
2499
2867
|
|
|
2500
2868
|
### ${MANIFEST_FILE}
|
|
2501
2869
|
|
|
@@ -2510,11 +2878,11 @@ ${body}`
|
|
|
2510
2878
|
}
|
|
2511
2879
|
|
|
2512
2880
|
// src/tools/team-setup.ts
|
|
2513
|
-
import { z as
|
|
2514
|
-
import { existsSync as
|
|
2881
|
+
import { z as z16 } from "zod";
|
|
2882
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2515
2883
|
import { lstat as lstat2 } from "fs/promises";
|
|
2516
2884
|
import { homedir as homedir4 } from "os";
|
|
2517
|
-
import { join as
|
|
2885
|
+
import { join as join5, resolve as resolve4 } from "path";
|
|
2518
2886
|
|
|
2519
2887
|
// src/lib/person-values.ts
|
|
2520
2888
|
var PHONE = "phone";
|
|
@@ -2595,9 +2963,9 @@ var PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
|
2595
2963
|
var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
2596
2964
|
|
|
2597
2965
|
// 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
|
|
2966
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2967
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
2968
|
+
import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
|
|
2601
2969
|
|
|
2602
2970
|
// src/lib/memory-sync.ts
|
|
2603
2971
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -2877,7 +3245,7 @@ function refusal(e) {
|
|
|
2877
3245
|
const { status, code } = e;
|
|
2878
3246
|
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
2879
3247
|
}
|
|
2880
|
-
var toPath = (root, rel) =>
|
|
3248
|
+
var toPath = (root, rel) => join4(root, ...rel.split("/"));
|
|
2881
3249
|
async function readFolder(root) {
|
|
2882
3250
|
const files = {};
|
|
2883
3251
|
const names = /* @__PURE__ */ new Set();
|
|
@@ -2885,14 +3253,14 @@ async function readFolder(root) {
|
|
|
2885
3253
|
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
2886
3254
|
if (e.name.startsWith(".")) continue;
|
|
2887
3255
|
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
2888
|
-
if (e.isDirectory()) await walk2(
|
|
3256
|
+
if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
|
|
2889
3257
|
else if (e.isFile()) {
|
|
2890
3258
|
names.add(rel.toLowerCase());
|
|
2891
|
-
if (e.name.endsWith(".md")) files[rel] = await
|
|
3259
|
+
if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
|
|
2892
3260
|
}
|
|
2893
3261
|
}
|
|
2894
3262
|
}
|
|
2895
|
-
if (
|
|
3263
|
+
if (existsSync3(root)) await walk2(root, "");
|
|
2896
3264
|
return { files, names };
|
|
2897
3265
|
}
|
|
2898
3266
|
async function pullTeam(client, ws, project) {
|
|
@@ -3101,11 +3469,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3101
3469
|
report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
|
|
3102
3470
|
break;
|
|
3103
3471
|
}
|
|
3104
|
-
const
|
|
3105
|
-
if (
|
|
3106
|
-
out(` ${
|
|
3472
|
+
const refused2 = result.refused[0];
|
|
3473
|
+
if (refused2?.code === "SYD-K021") {
|
|
3474
|
+
out(` ${refused2.detail}. Add them in Staff first, or answer p to file them.`);
|
|
3107
3475
|
choices = choices.filter((c) => c !== "m");
|
|
3108
|
-
} else if (
|
|
3476
|
+
} else if (refused2) out(` ${refused2.detail}`);
|
|
3109
3477
|
}
|
|
3110
3478
|
}
|
|
3111
3479
|
for (let i = 0; i < filed.length; i += 50) {
|
|
@@ -3249,7 +3617,7 @@ async function syncMemory(opts) {
|
|
|
3249
3617
|
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
3618
|
);
|
|
3251
3619
|
}
|
|
3252
|
-
const dir = resolve3(opts.dir ??
|
|
3620
|
+
const dir = resolve3(opts.dir ?? join4(workspaceDir(ws), "memory", project));
|
|
3253
3621
|
const run2 = { ...opts, dir };
|
|
3254
3622
|
const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
|
|
3255
3623
|
const report = new Report();
|
|
@@ -3399,21 +3767,21 @@ async function readPartDirectory(part, directory) {
|
|
|
3399
3767
|
let present = true;
|
|
3400
3768
|
for (let i = 1; i <= segments.length; i++) {
|
|
3401
3769
|
const rel = segments.slice(0, i).join("/");
|
|
3402
|
-
if (await isLink(
|
|
3770
|
+
if (await isLink(join5(root, ...segments.slice(0, i)))) {
|
|
3403
3771
|
links.push(rel);
|
|
3404
3772
|
present = false;
|
|
3405
3773
|
break;
|
|
3406
3774
|
}
|
|
3407
3775
|
}
|
|
3408
|
-
if (present &&
|
|
3776
|
+
if (present && existsSync4(join5(root, ...segments))) paths.push(path);
|
|
3409
3777
|
}
|
|
3410
3778
|
for (const { dir, md } of layout.folders) {
|
|
3411
|
-
const full =
|
|
3779
|
+
const full = join5(root, dir);
|
|
3412
3780
|
if (await isLink(full)) {
|
|
3413
3781
|
links.push(dir);
|
|
3414
3782
|
continue;
|
|
3415
3783
|
}
|
|
3416
|
-
if (!
|
|
3784
|
+
if (!existsSync4(full)) continue;
|
|
3417
3785
|
const walked = await walk(full, { skipHidden: true });
|
|
3418
3786
|
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
3419
3787
|
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
@@ -3452,24 +3820,24 @@ function describePerson(id, person) {
|
|
|
3452
3820
|
);
|
|
3453
3821
|
return lines.join("\n");
|
|
3454
3822
|
}
|
|
3455
|
-
function registerTeamSetupTools(
|
|
3456
|
-
const fileSchema =
|
|
3457
|
-
path:
|
|
3458
|
-
content:
|
|
3823
|
+
function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
3824
|
+
const fileSchema = z16.object({
|
|
3825
|
+
path: z16.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
|
|
3826
|
+
content: z16.string().describe("The file's full text, exactly as it is")
|
|
3459
3827
|
});
|
|
3460
|
-
|
|
3828
|
+
server.registerTool(
|
|
3461
3829
|
"publish_setup_part",
|
|
3462
3830
|
{
|
|
3463
3831
|
title: "Publish Setup Part",
|
|
3464
3832
|
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
3833
|
inputSchema: {
|
|
3466
|
-
part:
|
|
3834
|
+
part: z16.enum(PARTS2).describe("Which part of the setup"),
|
|
3467
3835
|
...localFiles ? {
|
|
3468
|
-
directory:
|
|
3836
|
+
directory: z16.string().optional().describe(
|
|
3469
3837
|
"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
3838
|
)
|
|
3471
3839
|
} : {},
|
|
3472
|
-
files:
|
|
3840
|
+
files: z16.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
|
|
3473
3841
|
},
|
|
3474
3842
|
annotations: { readOnlyHint: false }
|
|
3475
3843
|
},
|
|
@@ -3499,13 +3867,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3499
3867
|
return text2(lines.join("\n"));
|
|
3500
3868
|
}
|
|
3501
3869
|
);
|
|
3502
|
-
|
|
3870
|
+
server.registerTool(
|
|
3503
3871
|
"resolve_person",
|
|
3504
3872
|
{
|
|
3505
3873
|
title: "Resolve Person",
|
|
3506
3874
|
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
3875
|
inputSchema: {
|
|
3508
|
-
id:
|
|
3876
|
+
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
3877
|
},
|
|
3510
3878
|
annotations: { readOnlyHint: true }
|
|
3511
3879
|
},
|
|
@@ -3516,14 +3884,14 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3516
3884
|
}
|
|
3517
3885
|
);
|
|
3518
3886
|
if (!localFiles) return;
|
|
3519
|
-
|
|
3887
|
+
server.registerTool(
|
|
3520
3888
|
"sync_memory",
|
|
3521
3889
|
{
|
|
3522
3890
|
title: "Sync Memory",
|
|
3523
3891
|
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
3892
|
inputSchema: {
|
|
3525
|
-
project:
|
|
3526
|
-
dryRun:
|
|
3893
|
+
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"),
|
|
3894
|
+
dryRun: z16.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
|
|
3527
3895
|
},
|
|
3528
3896
|
annotations: { readOnlyHint: false }
|
|
3529
3897
|
},
|
|
@@ -3541,44 +3909,56 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3541
3909
|
}
|
|
3542
3910
|
|
|
3543
3911
|
// 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
|
-
|
|
3912
|
+
var SERVER_VERSION = true ? "0.1.51" : "0.0.0-dev";
|
|
3913
|
+
function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
|
|
3914
|
+
const server = new McpServer(
|
|
3915
|
+
{
|
|
3916
|
+
name: "systemdox-mcp-server",
|
|
3917
|
+
version: SERVER_VERSION
|
|
3918
|
+
},
|
|
3919
|
+
instructions ? { instructions } : void 0
|
|
3920
|
+
);
|
|
3921
|
+
registerGetGuardrails(server, apiClient2);
|
|
3922
|
+
registerGetSpecs(server, apiClient2);
|
|
3923
|
+
registerGetDocs(server, apiClient2);
|
|
3924
|
+
registerGetTechStack(server, apiClient2);
|
|
3925
|
+
registerGetPlanningContext(server, apiClient2);
|
|
3926
|
+
registerGetPlanBoard(server, apiClient2);
|
|
3927
|
+
registerGetChange(server, apiClient2);
|
|
3928
|
+
registerGetContext(server, apiClient2);
|
|
3929
|
+
registerGetProjectRules(server, apiClient2);
|
|
3930
|
+
registerGetPromptTemplates(server, apiClient2);
|
|
3931
|
+
registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 });
|
|
3932
|
+
registerDocumentGraph(server, apiClient2);
|
|
3933
|
+
registerCreateGuardrail(server, apiClient2);
|
|
3934
|
+
registerUpdateSpec(server, apiClient2);
|
|
3935
|
+
registerReportDecision(server, apiClient2);
|
|
3936
|
+
registerCreateProjectRule(server, apiClient2);
|
|
3937
|
+
registerCreatePromptTemplate(server, apiClient2);
|
|
3938
|
+
registerSkillTools(server, apiClient2, { localFiles });
|
|
3939
|
+
registerTeamSetupTools(server, apiClient2, { localFiles });
|
|
3940
|
+
return server;
|
|
3568
3941
|
}
|
|
3569
3942
|
|
|
3570
3943
|
// src/index.ts
|
|
3571
3944
|
var config;
|
|
3945
|
+
var keyProblem;
|
|
3572
3946
|
try {
|
|
3573
3947
|
config = await loadLocalConfig();
|
|
3574
3948
|
} catch (err) {
|
|
3575
3949
|
console.error(`SystemDox MCP server: ${err.message}`);
|
|
3576
|
-
|
|
3950
|
+
keyProblem = keyReason(err.message);
|
|
3951
|
+
try {
|
|
3952
|
+
config = loadConfig(NO_KEY);
|
|
3953
|
+
} catch (urlErr) {
|
|
3954
|
+
console.error(`SystemDox MCP server: ${urlErr.message}`);
|
|
3955
|
+
process.exit(1);
|
|
3956
|
+
}
|
|
3577
3957
|
}
|
|
3578
3958
|
var apiClient = new SystemDoxApiClient(config);
|
|
3579
|
-
var server = createServer(apiClient, { localFiles: true });
|
|
3580
3959
|
async function main() {
|
|
3581
|
-
await apiClient
|
|
3960
|
+
const instructions = await sessionInstructions(apiClient, { localFiles: true, keyProblem });
|
|
3961
|
+
const server = createServer(apiClient, { localFiles: true, instructions, keyProblem });
|
|
3582
3962
|
const transport = new StdioServerTransport();
|
|
3583
3963
|
await server.connect(transport);
|
|
3584
3964
|
}
|