@systemdox/mcp-server 0.1.42 → 0.1.43
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 +15 -0
- package/dist/cli.js +511 -69
- package/dist/http.js +384 -46
- package/dist/index.js +385 -47
- package/dist/install-skill.d.ts +25 -0
- package/dist/install-skill.js +374 -0
- package/dist/lib/api-client.d.ts +3 -0
- package/dist/lib/api-client.js +10 -0
- package/dist/lib/skills.d.ts +87 -0
- package/dist/lib/skills.js +209 -0
- package/dist/server.d.ts +7 -1
- package/dist/tools/skills.d.ts +5 -0
- package/dist/tools/skills.js +347 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -122,6 +122,16 @@ var init_api_client = __esm({
|
|
|
122
122
|
this.cache.set(cacheKey, data);
|
|
123
123
|
return data;
|
|
124
124
|
}
|
|
125
|
+
/** A read that skips the cache: an install must take the version that is
|
|
126
|
+
* latest now, not the one cached before someone published another. */
|
|
127
|
+
async getFresh(path) {
|
|
128
|
+
const res = await this.fetchRaw(path);
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
const body = await res.text();
|
|
131
|
+
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
132
|
+
}
|
|
133
|
+
return await res.json();
|
|
134
|
+
}
|
|
125
135
|
async post(path, body) {
|
|
126
136
|
return this.mutate("POST", path, body);
|
|
127
137
|
}
|
|
@@ -142,8 +152,8 @@ var init_api_client = __esm({
|
|
|
142
152
|
body: JSON.stringify(body)
|
|
143
153
|
});
|
|
144
154
|
if (!res.ok) {
|
|
145
|
-
const
|
|
146
|
-
throw new Error(`SystemDox API error ${res.status}: ${
|
|
155
|
+
const text2 = await res.text();
|
|
156
|
+
throw new Error(`SystemDox API error ${res.status}: ${text2}`);
|
|
147
157
|
}
|
|
148
158
|
return await res.json();
|
|
149
159
|
}
|
|
@@ -178,6 +188,287 @@ var init_api_client = __esm({
|
|
|
178
188
|
}
|
|
179
189
|
});
|
|
180
190
|
|
|
191
|
+
// src/lib/skills.ts
|
|
192
|
+
import { createHash } from "crypto";
|
|
193
|
+
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
194
|
+
import { existsSync } from "fs";
|
|
195
|
+
import { homedir } from "os";
|
|
196
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
197
|
+
function sha256(content) {
|
|
198
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
199
|
+
}
|
|
200
|
+
function isValidName(name) {
|
|
201
|
+
return name.length <= 64 && NAME_RE.test(name);
|
|
202
|
+
}
|
|
203
|
+
function isValidPath(path) {
|
|
204
|
+
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
|
|
205
|
+
}
|
|
206
|
+
function frontmatterName(skillMd) {
|
|
207
|
+
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
208
|
+
if (!block) return void 0;
|
|
209
|
+
const line = /^name:[ \t]*(.+?)[ \t]*$/m.exec(block[1]);
|
|
210
|
+
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
211
|
+
}
|
|
212
|
+
function defaultSkillDir(name) {
|
|
213
|
+
return join(homedir(), ".claude", "skills", name);
|
|
214
|
+
}
|
|
215
|
+
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
216
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
217
|
+
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
218
|
+
const full = join(dir, entry.name);
|
|
219
|
+
const rel = relative(root, full).split(sep).join("/");
|
|
220
|
+
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
221
|
+
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
222
|
+
else if (entry.isDirectory()) await walk(root, { skipHidden }, full, out);
|
|
223
|
+
else if (entry.isFile()) out.files.push(rel);
|
|
224
|
+
}
|
|
225
|
+
out.files.sort();
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
async function readSkillDirectory(directory) {
|
|
229
|
+
const root = resolve(directory);
|
|
230
|
+
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
231
|
+
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
232
|
+
}
|
|
233
|
+
const walked = await walk(root, { skipHidden: true });
|
|
234
|
+
if (walked.links.length) {
|
|
235
|
+
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
236
|
+
}
|
|
237
|
+
const paths = walked.files;
|
|
238
|
+
if (paths.length > MAX_FILES) {
|
|
239
|
+
throw new Error(`${root} holds ${paths.length} files: a skill holds at most ${MAX_FILES}`);
|
|
240
|
+
}
|
|
241
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
242
|
+
const files = [];
|
|
243
|
+
let total = 0;
|
|
244
|
+
for (const path of paths) {
|
|
245
|
+
if (!isValidPath(path)) {
|
|
246
|
+
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
247
|
+
}
|
|
248
|
+
const raw = await readFile(join(root, ...path.split("/")));
|
|
249
|
+
if (raw.includes(0)) throw new Error(`${path} is binary: a skill holds text files only`);
|
|
250
|
+
if (raw.length > MAX_FILE_BYTES) {
|
|
251
|
+
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
252
|
+
}
|
|
253
|
+
total += raw.length;
|
|
254
|
+
if (total > MAX_TOTAL_BYTES) {
|
|
255
|
+
throw new Error(`the files pass ${MAX_TOTAL_BYTES} bytes at ${path}`);
|
|
256
|
+
}
|
|
257
|
+
let content;
|
|
258
|
+
try {
|
|
259
|
+
content = decoder.decode(raw);
|
|
260
|
+
} catch {
|
|
261
|
+
throw new Error(`${path} is not UTF-8 text`);
|
|
262
|
+
}
|
|
263
|
+
files.push({ path, content });
|
|
264
|
+
}
|
|
265
|
+
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
266
|
+
const name = frontmatterName(skillMd.content);
|
|
267
|
+
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
268
|
+
return { name, files, skipped: walked.hidden };
|
|
269
|
+
}
|
|
270
|
+
function within(dest, path) {
|
|
271
|
+
if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
|
|
272
|
+
const full = resolve(dest, ...path.split("/"));
|
|
273
|
+
if (!full.startsWith(resolve(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
|
|
274
|
+
return full;
|
|
275
|
+
}
|
|
276
|
+
async function readManifest(dest) {
|
|
277
|
+
try {
|
|
278
|
+
return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
|
|
279
|
+
} catch {
|
|
280
|
+
return void 0;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
284
|
+
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
285
|
+
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
286
|
+
for (const f of version.files) within(dest, f.path);
|
|
287
|
+
if (existsSync(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
288
|
+
throw new Error(`${dest} is a link: install into a real folder`);
|
|
289
|
+
}
|
|
290
|
+
const walked = existsSync(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
291
|
+
if (walked.links.length) {
|
|
292
|
+
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
293
|
+
}
|
|
294
|
+
const present = walked.files;
|
|
295
|
+
const manifest = await readManifest(dest);
|
|
296
|
+
const known = Object.fromEntries(
|
|
297
|
+
Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
|
|
298
|
+
);
|
|
299
|
+
const edited = [];
|
|
300
|
+
const untracked = [];
|
|
301
|
+
for (const path of present) {
|
|
302
|
+
const content = await readFile(join(dest, ...path.split("/")), "utf8");
|
|
303
|
+
if (Object.hasOwn(known, path)) {
|
|
304
|
+
if (sha256(content) !== known[path]) edited.push(path);
|
|
305
|
+
} else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
|
|
306
|
+
if (incoming.has(path) || !manifest) edited.push(path);
|
|
307
|
+
else untracked.push(path);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const blocking = edited.filter((p) => incoming.has(p) || !manifest);
|
|
311
|
+
if (blocking.length && !overwrite) throw new SkillInstallRefused(dest, blocking);
|
|
312
|
+
const written = [];
|
|
313
|
+
for (const f of version.files) {
|
|
314
|
+
const full = within(dest, f.path);
|
|
315
|
+
await mkdir(dirname(full), { recursive: true });
|
|
316
|
+
await writeFile(full, f.content, "utf8");
|
|
317
|
+
written.push(f.path);
|
|
318
|
+
}
|
|
319
|
+
const removed = [];
|
|
320
|
+
const keptEdited = [];
|
|
321
|
+
for (const path of Object.keys(known)) {
|
|
322
|
+
if (incoming.has(path) || !present.includes(path)) continue;
|
|
323
|
+
if (edited.includes(path)) {
|
|
324
|
+
keptEdited.push(path);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
await rm(within(dest, path));
|
|
328
|
+
removed.push(path);
|
|
329
|
+
}
|
|
330
|
+
const next = {
|
|
331
|
+
name: version.name,
|
|
332
|
+
version: version.version,
|
|
333
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
334
|
+
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
335
|
+
};
|
|
336
|
+
await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
337
|
+
return {
|
|
338
|
+
dest,
|
|
339
|
+
version: version.version,
|
|
340
|
+
written,
|
|
341
|
+
removed,
|
|
342
|
+
keptUntracked: untracked.filter((p) => !incoming.has(p)),
|
|
343
|
+
keptEdited
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function fenceFor(text2) {
|
|
347
|
+
const longest = Math.max(0, ...(text2.match(/`+/g) ?? []).map((r) => r.length));
|
|
348
|
+
return "`".repeat(Math.max(3, longest + 1));
|
|
349
|
+
}
|
|
350
|
+
function describeInstall(name, r) {
|
|
351
|
+
const lines = [
|
|
352
|
+
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
353
|
+
];
|
|
354
|
+
if (r.removed.length) lines.push(`Removed (no longer in the skill): ${r.removed.join(", ")}`);
|
|
355
|
+
if (r.keptEdited.length) {
|
|
356
|
+
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
357
|
+
}
|
|
358
|
+
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
359
|
+
lines.push("Start a new Claude Code session to load it.");
|
|
360
|
+
return lines.join("\n");
|
|
361
|
+
}
|
|
362
|
+
var MANIFEST_FILE, ENTRY_FILE, MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, NAME_RE, SEGMENT_RE, SKIPPED, SkillInstallRefused;
|
|
363
|
+
var init_skills = __esm({
|
|
364
|
+
"src/lib/skills.ts"() {
|
|
365
|
+
"use strict";
|
|
366
|
+
MANIFEST_FILE = ".systemdox-skill.json";
|
|
367
|
+
ENTRY_FILE = "SKILL.md";
|
|
368
|
+
MAX_FILES = 100;
|
|
369
|
+
MAX_FILE_BYTES = 256 * 1024;
|
|
370
|
+
MAX_TOTAL_BYTES = 1024 * 1024;
|
|
371
|
+
NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
372
|
+
SEGMENT_RE = /^[A-Za-z0-9._-]+$/;
|
|
373
|
+
SKIPPED = /* @__PURE__ */ new Set([MANIFEST_FILE, ".git", "node_modules", "__pycache__", ".DS_Store"]);
|
|
374
|
+
SkillInstallRefused = class extends Error {
|
|
375
|
+
constructor(dest, edited) {
|
|
376
|
+
super(
|
|
377
|
+
`${dest} has local changes in ${edited.length} file(s): ${edited.join(", ")}. Nothing was written. Move your changes aside, or install again with overwrite.`
|
|
378
|
+
);
|
|
379
|
+
this.dest = dest;
|
|
380
|
+
this.edited = edited;
|
|
381
|
+
}
|
|
382
|
+
dest;
|
|
383
|
+
edited;
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
// src/install-skill.ts
|
|
389
|
+
var install_skill_exports = {};
|
|
390
|
+
__export(install_skill_exports, {
|
|
391
|
+
INSTALL_USAGE: () => INSTALL_USAGE,
|
|
392
|
+
parseInstallArgs: () => parseInstallArgs,
|
|
393
|
+
runInstallSkill: () => runInstallSkill
|
|
394
|
+
});
|
|
395
|
+
import { basename, resolve as resolve2 } from "path";
|
|
396
|
+
function parseInstallArgs(argv) {
|
|
397
|
+
const args2 = { overwrite: false };
|
|
398
|
+
for (let i = 0; i < argv.length; i++) {
|
|
399
|
+
const a = argv[i];
|
|
400
|
+
if (a === "--overwrite") args2.overwrite = true;
|
|
401
|
+
else if (a === "--version" || a === "--target") {
|
|
402
|
+
const value = argv[++i];
|
|
403
|
+
if (!value) throw new Error(`${a} needs a value
|
|
404
|
+
${INSTALL_USAGE}`);
|
|
405
|
+
if (a === "--target") args2.target = value;
|
|
406
|
+
else if (/^[1-9]\d*$/.test(value)) args2.version = Number(value);
|
|
407
|
+
else throw new Error(`--version must be a whole number, got ${value}`);
|
|
408
|
+
} else if (a.startsWith("-")) throw new Error(`Unknown option ${a}
|
|
409
|
+
${INSTALL_USAGE}`);
|
|
410
|
+
else if (!args2.name) args2.name = a;
|
|
411
|
+
else throw new Error(`Unexpected argument ${a}
|
|
412
|
+
${INSTALL_USAGE}`);
|
|
413
|
+
}
|
|
414
|
+
if (!args2.name) throw new Error(INSTALL_USAGE);
|
|
415
|
+
if (!isValidName(args2.name)) throw new Error(`${args2.name} is not a skill name`);
|
|
416
|
+
if (args2.target && basename(resolve2(args2.target)) !== args2.name) {
|
|
417
|
+
throw new Error(`--target must be a folder named ${args2.name} (e.g. .claude/skills/${args2.name})`);
|
|
418
|
+
}
|
|
419
|
+
return args2;
|
|
420
|
+
}
|
|
421
|
+
async function runInstallSkill(argv, io) {
|
|
422
|
+
let args2;
|
|
423
|
+
try {
|
|
424
|
+
args2 = parseInstallArgs(argv);
|
|
425
|
+
} catch (e) {
|
|
426
|
+
io.err(e.message);
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
let client = io.client;
|
|
430
|
+
if (!client) {
|
|
431
|
+
try {
|
|
432
|
+
client = new SystemDoxApiClient(loadConfig());
|
|
433
|
+
} catch (e) {
|
|
434
|
+
io.err(
|
|
435
|
+
`${e.message}
|
|
436
|
+
Set SYSTEMDOX_API_KEY in your environment (create a key under Settings \u203A API keys in SystemDox), then run this again.`
|
|
437
|
+
);
|
|
438
|
+
return 1;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
let version;
|
|
442
|
+
try {
|
|
443
|
+
version = await client.getFresh(
|
|
444
|
+
`/v1/standards/skills/${encodeURIComponent(args2.name)}/versions/${args2.version ?? "latest"}`
|
|
445
|
+
);
|
|
446
|
+
} catch (e) {
|
|
447
|
+
io.err(e.message);
|
|
448
|
+
return 1;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
const result = await installSkill(version, args2.target ?? defaultSkillDir(version.name), {
|
|
452
|
+
overwrite: args2.overwrite
|
|
453
|
+
});
|
|
454
|
+
io.out(describeInstall(version.name, result));
|
|
455
|
+
return 0;
|
|
456
|
+
} catch (e) {
|
|
457
|
+
io.err(e.message);
|
|
458
|
+
return e instanceof SkillInstallRefused ? 2 : 1;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
var INSTALL_USAGE;
|
|
462
|
+
var init_install_skill = __esm({
|
|
463
|
+
"src/install-skill.ts"() {
|
|
464
|
+
"use strict";
|
|
465
|
+
init_api_client();
|
|
466
|
+
init_config();
|
|
467
|
+
init_skills();
|
|
468
|
+
INSTALL_USAGE = "Usage: systemdox-mcp-server install-skill <name> [--version <n>] [--target <dir>] [--overwrite]";
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
181
472
|
// src/lib/language.ts
|
|
182
473
|
function detectLanguage(filePath) {
|
|
183
474
|
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
@@ -310,12 +601,12 @@ function registerGetGuardrails(server2, apiClient2) {
|
|
|
310
601
|
);
|
|
311
602
|
}
|
|
312
603
|
if (filtered.length === 0) {
|
|
313
|
-
const
|
|
604
|
+
const text3 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient2))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
|
|
314
605
|
return {
|
|
315
|
-
content: [{ type: "text", text:
|
|
606
|
+
content: [{ type: "text", text: text3 }]
|
|
316
607
|
};
|
|
317
608
|
}
|
|
318
|
-
const
|
|
609
|
+
const text2 = filtered.map(
|
|
319
610
|
(g) => `## ${g.title} [${g.severity}]
|
|
320
611
|
|
|
321
612
|
${g.description}
|
|
@@ -339,7 +630,7 @@ ${g.fix}
|
|
|
339
630
|
type: "text",
|
|
340
631
|
text: `Found ${filtered.length} check(s):
|
|
341
632
|
|
|
342
|
-
${
|
|
633
|
+
${text2}`
|
|
343
634
|
}
|
|
344
635
|
]
|
|
345
636
|
};
|
|
@@ -450,7 +741,7 @@ ${spec.taskContent}`);
|
|
|
450
741
|
]
|
|
451
742
|
};
|
|
452
743
|
}
|
|
453
|
-
const
|
|
744
|
+
const text2 = filtered.map(
|
|
454
745
|
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
455
746
|
).join("\n");
|
|
456
747
|
return {
|
|
@@ -459,7 +750,7 @@ ${spec.taskContent}`);
|
|
|
459
750
|
type: "text",
|
|
460
751
|
text: `Found ${filtered.length} spec(s):
|
|
461
752
|
|
|
462
|
-
${
|
|
753
|
+
${text2}`
|
|
463
754
|
}
|
|
464
755
|
]
|
|
465
756
|
};
|
|
@@ -517,18 +808,18 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
517
808
|
isError: true
|
|
518
809
|
};
|
|
519
810
|
}
|
|
520
|
-
let
|
|
811
|
+
let text3 = doc.content;
|
|
521
812
|
if (anchor && doc.section_resolved === false) {
|
|
522
|
-
|
|
813
|
+
text3 = `_Section "${anchor}" not found \u2014 returning the full document._
|
|
523
814
|
|
|
524
|
-
` +
|
|
815
|
+
` + text3;
|
|
525
816
|
} else if (anchor && doc.heading) {
|
|
526
|
-
|
|
817
|
+
text3 = `## ${doc.heading}
|
|
527
818
|
|
|
528
819
|
${doc.content}`;
|
|
529
820
|
}
|
|
530
821
|
return {
|
|
531
|
-
content: [{ type: "text", text:
|
|
822
|
+
content: [{ type: "text", text: text3 }]
|
|
532
823
|
};
|
|
533
824
|
}
|
|
534
825
|
const params = {};
|
|
@@ -552,7 +843,7 @@ ${doc.content}`;
|
|
|
552
843
|
]
|
|
553
844
|
};
|
|
554
845
|
}
|
|
555
|
-
const
|
|
846
|
+
const text2 = items.map((d) => {
|
|
556
847
|
const sections = (d.outline ?? []).slice(0, 8);
|
|
557
848
|
const sectionHint = sections.length > 0 ? `
|
|
558
849
|
sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
|
|
@@ -566,7 +857,7 @@ ${doc.content}`;
|
|
|
566
857
|
type: "text",
|
|
567
858
|
text: `Found ${items.length} document(s) (${response.total} total):
|
|
568
859
|
|
|
569
|
-
${
|
|
860
|
+
${text2}
|
|
570
861
|
|
|
571
862
|
Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
572
863
|
}
|
|
@@ -911,13 +1202,13 @@ var init_get_plan_board = __esm({
|
|
|
911
1202
|
});
|
|
912
1203
|
|
|
913
1204
|
// src/lib/scoring.ts
|
|
914
|
-
function estimateTokens(
|
|
915
|
-
return Math.ceil(
|
|
1205
|
+
function estimateTokens(text2) {
|
|
1206
|
+
return Math.ceil(text2.length / CHARS_PER_TOKEN);
|
|
916
1207
|
}
|
|
917
|
-
function truncateToTokens(
|
|
1208
|
+
function truncateToTokens(text2, maxTokens) {
|
|
918
1209
|
const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
|
|
919
|
-
if (
|
|
920
|
-
const head =
|
|
1210
|
+
if (text2.length <= maxChars) return { text: text2, truncated: false };
|
|
1211
|
+
const head = text2.slice(0, maxChars);
|
|
921
1212
|
const floor = Math.floor(maxChars * 0.75);
|
|
922
1213
|
const paragraph = head.lastIndexOf("\n\n");
|
|
923
1214
|
const line = head.lastIndexOf("\n");
|
|
@@ -962,7 +1253,7 @@ function scoreSeverity(item) {
|
|
|
962
1253
|
if (!item.severity) return 0.5;
|
|
963
1254
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
964
1255
|
}
|
|
965
|
-
function extractKeywords(
|
|
1256
|
+
function extractKeywords(text2) {
|
|
966
1257
|
const stopWords = /* @__PURE__ */ new Set([
|
|
967
1258
|
"the",
|
|
968
1259
|
"a",
|
|
@@ -1047,7 +1338,7 @@ function extractKeywords(text) {
|
|
|
1047
1338
|
"implement",
|
|
1048
1339
|
"create"
|
|
1049
1340
|
]);
|
|
1050
|
-
return
|
|
1341
|
+
return text2.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
1051
1342
|
}
|
|
1052
1343
|
function scoreItems(items, taskDescription, filePaths) {
|
|
1053
1344
|
const languages = detectLanguages(filePaths);
|
|
@@ -1100,20 +1391,20 @@ var init_scoring = __esm({
|
|
|
1100
1391
|
|
|
1101
1392
|
// src/lib/context-builder.ts
|
|
1102
1393
|
function formatGuardrail(g) {
|
|
1103
|
-
let
|
|
1394
|
+
let text2 = `## ${g.title} [${g.severity}]
|
|
1104
1395
|
|
|
1105
1396
|
${g.description}`;
|
|
1106
|
-
if (g.pattern)
|
|
1397
|
+
if (g.pattern) text2 += `
|
|
1107
1398
|
|
|
1108
1399
|
**Bad:** \`\`\`
|
|
1109
1400
|
${g.pattern}
|
|
1110
1401
|
\`\`\``;
|
|
1111
|
-
if (g.fix)
|
|
1402
|
+
if (g.fix) text2 += `
|
|
1112
1403
|
|
|
1113
1404
|
**Good:** \`\`\`
|
|
1114
1405
|
${g.fix}
|
|
1115
1406
|
\`\`\``;
|
|
1116
|
-
return
|
|
1407
|
+
return text2;
|
|
1117
1408
|
}
|
|
1118
1409
|
function formatSpecHeadline(s) {
|
|
1119
1410
|
return `### ${s.name}
|
|
@@ -1135,19 +1426,19 @@ function formatRules(rules) {
|
|
|
1135
1426
|
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
1136
1427
|
byCategory.get(cat).push(r);
|
|
1137
1428
|
}
|
|
1138
|
-
let
|
|
1429
|
+
let text2 = "";
|
|
1139
1430
|
for (const [cat, catRules] of byCategory) {
|
|
1140
|
-
|
|
1431
|
+
text2 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
1141
1432
|
`;
|
|
1142
1433
|
for (const r of catRules) {
|
|
1143
|
-
|
|
1434
|
+
text2 += `- ${r.text}
|
|
1144
1435
|
`;
|
|
1145
1436
|
}
|
|
1146
|
-
|
|
1437
|
+
text2 += "\n";
|
|
1147
1438
|
}
|
|
1148
1439
|
return `## Project Rules (${rules.length})
|
|
1149
1440
|
|
|
1150
|
-
${
|
|
1441
|
+
${text2.trim()}`;
|
|
1151
1442
|
}
|
|
1152
1443
|
async function fetchSpecBody(api, draftId) {
|
|
1153
1444
|
const spec = await api.get(
|
|
@@ -1501,13 +1792,13 @@ function registerGetContext(server2, apiClient2) {
|
|
|
1501
1792
|
}
|
|
1502
1793
|
},
|
|
1503
1794
|
async ({ task_description, file_paths, max_tokens }) => {
|
|
1504
|
-
const { text } = await buildContext(apiClient2, {
|
|
1795
|
+
const { text: text2 } = await buildContext(apiClient2, {
|
|
1505
1796
|
taskDescription: task_description,
|
|
1506
1797
|
filePaths: file_paths,
|
|
1507
1798
|
maxTokens: max_tokens
|
|
1508
1799
|
});
|
|
1509
1800
|
return {
|
|
1510
|
-
content: [{ type: "text", text }]
|
|
1801
|
+
content: [{ type: "text", text: text2 }]
|
|
1511
1802
|
};
|
|
1512
1803
|
}
|
|
1513
1804
|
);
|
|
@@ -1770,9 +2061,9 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
1770
2061
|
);
|
|
1771
2062
|
}
|
|
1772
2063
|
if (filtered.length === 0) {
|
|
1773
|
-
const
|
|
2064
|
+
const text3 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient2))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
|
|
1774
2065
|
return {
|
|
1775
|
-
content: [{ type: "text", text:
|
|
2066
|
+
content: [{ type: "text", text: text3 }]
|
|
1776
2067
|
};
|
|
1777
2068
|
}
|
|
1778
2069
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -1781,25 +2072,25 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
1781
2072
|
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
1782
2073
|
grouped.get(cat).push(rule);
|
|
1783
2074
|
}
|
|
1784
|
-
let
|
|
2075
|
+
let text2 = `Found ${filtered.length} project rule(s):
|
|
1785
2076
|
|
|
1786
2077
|
`;
|
|
1787
2078
|
for (const [cat, catRules] of grouped) {
|
|
1788
2079
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
1789
|
-
|
|
2080
|
+
text2 += `## ${label}
|
|
1790
2081
|
|
|
1791
2082
|
`;
|
|
1792
2083
|
for (const rule of catRules) {
|
|
1793
|
-
|
|
2084
|
+
text2 += `- ${rule.text}
|
|
1794
2085
|
`;
|
|
1795
2086
|
}
|
|
1796
|
-
|
|
2087
|
+
text2 += "\n";
|
|
1797
2088
|
}
|
|
1798
2089
|
return {
|
|
1799
2090
|
content: [
|
|
1800
2091
|
{
|
|
1801
2092
|
type: "text",
|
|
1802
|
-
text
|
|
2093
|
+
text: text2
|
|
1803
2094
|
}
|
|
1804
2095
|
]
|
|
1805
2096
|
};
|
|
@@ -1840,10 +2131,10 @@ function registerCreateProjectRule(server2, apiClient2) {
|
|
|
1840
2131
|
readOnlyHint: false
|
|
1841
2132
|
}
|
|
1842
2133
|
},
|
|
1843
|
-
async ({ text, category }) => {
|
|
2134
|
+
async ({ text: text2, category }) => {
|
|
1844
2135
|
const saved = await apiClient2.post(
|
|
1845
2136
|
"/v1/standards/rules",
|
|
1846
|
-
{ text, category }
|
|
2137
|
+
{ text: text2, category }
|
|
1847
2138
|
);
|
|
1848
2139
|
apiClient2.invalidateCache("/v1/standards/rules");
|
|
1849
2140
|
return {
|
|
@@ -1886,7 +2177,7 @@ function registerGetPromptTemplates(server2, apiClient2) {
|
|
|
1886
2177
|
]
|
|
1887
2178
|
};
|
|
1888
2179
|
}
|
|
1889
|
-
const
|
|
2180
|
+
const text2 = templates.map(
|
|
1890
2181
|
(t) => `## ${t.name}
|
|
1891
2182
|
|
|
1892
2183
|
` + (t.description ? `*${t.description}*
|
|
@@ -1901,7 +2192,7 @@ ${t.prompt}
|
|
|
1901
2192
|
type: "text",
|
|
1902
2193
|
text: `Found ${templates.length} prompt template(s):
|
|
1903
2194
|
|
|
1904
|
-
${
|
|
2195
|
+
${text2}`
|
|
1905
2196
|
}
|
|
1906
2197
|
]
|
|
1907
2198
|
};
|
|
@@ -1959,9 +2250,151 @@ var init_create_prompt_template = __esm({
|
|
|
1959
2250
|
}
|
|
1960
2251
|
});
|
|
1961
2252
|
|
|
2253
|
+
// src/tools/skills.ts
|
|
2254
|
+
import { z as z13 } from "zod";
|
|
2255
|
+
import { homedir as homedir2 } from "os";
|
|
2256
|
+
import { basename as basename2, resolve as resolve3 } from "path";
|
|
2257
|
+
function expandHome(path) {
|
|
2258
|
+
return path.replace(/^~(?=$|[\\/])/, homedir2());
|
|
2259
|
+
}
|
|
2260
|
+
function text(t) {
|
|
2261
|
+
return { content: [{ type: "text", text: t }] };
|
|
2262
|
+
}
|
|
2263
|
+
function registerSkillTools(server2, apiClient2, { localFiles }) {
|
|
2264
|
+
const fileSchema = z13.object({
|
|
2265
|
+
path: z13.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
|
|
2266
|
+
content: z13.string().describe("The file's full text, exactly as it is")
|
|
2267
|
+
});
|
|
2268
|
+
server2.registerTool(
|
|
2269
|
+
"publish_skill",
|
|
2270
|
+
{
|
|
2271
|
+
title: "Publish Skill",
|
|
2272
|
+
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.",
|
|
2273
|
+
inputSchema: {
|
|
2274
|
+
...localFiles ? {
|
|
2275
|
+
directory: z13.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
|
|
2276
|
+
} : {},
|
|
2277
|
+
files: z13.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
|
|
2278
|
+
name: z13.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
|
|
2279
|
+
spaceId: z13.string().optional().describe("Space for a NEW skill (default: your default space)")
|
|
2280
|
+
},
|
|
2281
|
+
annotations: { readOnlyHint: false }
|
|
2282
|
+
},
|
|
2283
|
+
async (args2) => {
|
|
2284
|
+
let files;
|
|
2285
|
+
let name;
|
|
2286
|
+
let skipped = [];
|
|
2287
|
+
if (localFiles && args2.directory) {
|
|
2288
|
+
({ name, files, skipped } = await readSkillDirectory(expandHome(args2.directory)));
|
|
2289
|
+
} else if (args2.files?.length) {
|
|
2290
|
+
files = args2.files;
|
|
2291
|
+
const entry = files.find((f) => f.path === ENTRY_FILE);
|
|
2292
|
+
name = entry ? frontmatterName(entry.content) : void 0;
|
|
2293
|
+
} else {
|
|
2294
|
+
throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
|
|
2295
|
+
}
|
|
2296
|
+
name = args2.name ?? name;
|
|
2297
|
+
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name; pass \`name\`.`);
|
|
2298
|
+
const saved = await apiClient2.post(
|
|
2299
|
+
`${SKILLS}/${encodeURIComponent(name)}/versions`,
|
|
2300
|
+
{ files, ...args2.spaceId ? { spaceId: args2.spaceId } : {} }
|
|
2301
|
+
);
|
|
2302
|
+
apiClient2.invalidateCache(SKILLS);
|
|
2303
|
+
return text(
|
|
2304
|
+
`Published **${saved.name}** version ${saved.version} (${saved.fileCount} file(s), ${saved.totalBytes} bytes).
|
|
2305
|
+
Install it anywhere with: npx -y @systemdox/mcp-server install-skill ${saved.name}` + (skipped.length ? `
|
|
2306
|
+
Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
2307
|
+
);
|
|
2308
|
+
}
|
|
2309
|
+
);
|
|
2310
|
+
server2.registerTool(
|
|
2311
|
+
"list_skills",
|
|
2312
|
+
{
|
|
2313
|
+
title: "List Skills",
|
|
2314
|
+
description: "List the Claude Code skills published to SystemDox that you can install: name, description, latest version. Use install_skill to install one.",
|
|
2315
|
+
inputSchema: {},
|
|
2316
|
+
annotations: { readOnlyHint: true }
|
|
2317
|
+
},
|
|
2318
|
+
async () => {
|
|
2319
|
+
const { skills } = await apiClient2.get(SKILLS);
|
|
2320
|
+
if (!skills.length) {
|
|
2321
|
+
return text(
|
|
2322
|
+
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/skills", await workspaceBase(apiClient2))}`
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
return text(
|
|
2326
|
+
`${skills.length} skill(s):
|
|
2327
|
+
|
|
2328
|
+
` + skills.map((s) => `- **${s.name}** (version ${s.latestVersion}, updated ${s.updatedAt.slice(0, 10)}): ${s.description}`).join("\n")
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
);
|
|
2332
|
+
server2.registerTool(
|
|
2333
|
+
"install_skill",
|
|
2334
|
+
{
|
|
2335
|
+
title: "Install Skill",
|
|
2336
|
+
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.",
|
|
2337
|
+
inputSchema: {
|
|
2338
|
+
name: z13.string().describe("The skill's name"),
|
|
2339
|
+
version: z13.number().int().positive().optional().describe("A version number (default: the latest)"),
|
|
2340
|
+
target: z13.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
|
|
2341
|
+
...localFiles ? { overwrite: z13.boolean().optional().describe("Replace files edited since they were installed") } : {}
|
|
2342
|
+
},
|
|
2343
|
+
annotations: { readOnlyHint: !localFiles }
|
|
2344
|
+
},
|
|
2345
|
+
async (args2) => {
|
|
2346
|
+
const version = await apiClient2.getFresh(
|
|
2347
|
+
`${SKILLS}/${encodeURIComponent(args2.name)}/versions/${args2.version ?? "latest"}`
|
|
2348
|
+
);
|
|
2349
|
+
if (localFiles) {
|
|
2350
|
+
const dest2 = args2.target ? expandHome(args2.target) : defaultSkillDir(version.name);
|
|
2351
|
+
if (basename2(resolve3(dest2)) !== version.name) {
|
|
2352
|
+
throw new Error(`target must be a folder named ${version.name} (e.g. .claude/skills/${version.name}); got ${dest2}`);
|
|
2353
|
+
}
|
|
2354
|
+
const result = await installSkill(version, dest2, { overwrite: args2.overwrite });
|
|
2355
|
+
return text(describeInstall(version.name, result));
|
|
2356
|
+
}
|
|
2357
|
+
const dest = args2.target ?? `~/.claude/skills/${version.name}`;
|
|
2358
|
+
const manifest = {
|
|
2359
|
+
name: version.name,
|
|
2360
|
+
version: version.version,
|
|
2361
|
+
installedAt: "<now, ISO 8601>",
|
|
2362
|
+
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
2363
|
+
};
|
|
2364
|
+
const body = version.files.map((f) => {
|
|
2365
|
+
const fence = fenceFor(f.content);
|
|
2366
|
+
return `### ${f.path}
|
|
2367
|
+
|
|
2368
|
+
${fence}
|
|
2369
|
+
${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
|
|
2370
|
+
}).join("\n\n");
|
|
2371
|
+
return text(
|
|
2372
|
+
`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, and tell the user to start a new Claude Code session.
|
|
2373
|
+
|
|
2374
|
+
### ${MANIFEST_FILE}
|
|
2375
|
+
|
|
2376
|
+
\`\`\`json
|
|
2377
|
+
${JSON.stringify(manifest, null, 2)}
|
|
2378
|
+
\`\`\`
|
|
2379
|
+
|
|
2380
|
+
${body}`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
var SKILLS;
|
|
2386
|
+
var init_skills2 = __esm({
|
|
2387
|
+
"src/tools/skills.ts"() {
|
|
2388
|
+
"use strict";
|
|
2389
|
+
init_config_links();
|
|
2390
|
+
init_skills();
|
|
2391
|
+
SKILLS = "/v1/standards/skills";
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
|
|
1962
2395
|
// src/server.ts
|
|
1963
2396
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1964
|
-
function createServer(apiClient2) {
|
|
2397
|
+
function createServer(apiClient2, { localFiles = false } = {}) {
|
|
1965
2398
|
const server2 = new McpServer({
|
|
1966
2399
|
name: "systemdox-mcp-server",
|
|
1967
2400
|
version: SERVER_VERSION
|
|
@@ -1980,6 +2413,7 @@ function createServer(apiClient2) {
|
|
|
1980
2413
|
registerReportDecision(server2, apiClient2);
|
|
1981
2414
|
registerCreateProjectRule(server2, apiClient2);
|
|
1982
2415
|
registerCreatePromptTemplate(server2, apiClient2);
|
|
2416
|
+
registerSkillTools(server2, apiClient2, { localFiles });
|
|
1983
2417
|
return server2;
|
|
1984
2418
|
}
|
|
1985
2419
|
var SERVER_VERSION;
|
|
@@ -2000,7 +2434,8 @@ var init_server = __esm({
|
|
|
2000
2434
|
init_create_project_rule();
|
|
2001
2435
|
init_get_prompt_templates();
|
|
2002
2436
|
init_create_prompt_template();
|
|
2003
|
-
|
|
2437
|
+
init_skills2();
|
|
2438
|
+
SERVER_VERSION = true ? "0.1.43" : "0.0.0-dev";
|
|
2004
2439
|
}
|
|
2005
2440
|
});
|
|
2006
2441
|
|
|
@@ -2027,24 +2462,24 @@ var init_index = __esm({
|
|
|
2027
2462
|
init_server();
|
|
2028
2463
|
config = loadConfig();
|
|
2029
2464
|
apiClient = new SystemDoxApiClient(config);
|
|
2030
|
-
server = createServer(apiClient);
|
|
2465
|
+
server = createServer(apiClient, { localFiles: true });
|
|
2031
2466
|
main();
|
|
2032
2467
|
}
|
|
2033
2468
|
});
|
|
2034
2469
|
|
|
2035
2470
|
// src/cli.ts
|
|
2036
2471
|
init_config();
|
|
2037
|
-
import { readFile, writeFile, chmod } from "fs/promises";
|
|
2038
|
-
import { existsSync } from "fs";
|
|
2039
|
-
import { homedir } from "os";
|
|
2040
|
-
import { join } from "path";
|
|
2472
|
+
import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
|
|
2473
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2474
|
+
import { homedir as homedir3 } from "os";
|
|
2475
|
+
import { join as join2 } from "path";
|
|
2041
2476
|
import { createInterface } from "readline";
|
|
2042
2477
|
function prompt(question) {
|
|
2043
2478
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2044
|
-
return new Promise((
|
|
2479
|
+
return new Promise((resolve4) => {
|
|
2045
2480
|
rl.question(question, (answer) => {
|
|
2046
2481
|
rl.close();
|
|
2047
|
-
|
|
2482
|
+
resolve4(answer.trim());
|
|
2048
2483
|
});
|
|
2049
2484
|
});
|
|
2050
2485
|
}
|
|
@@ -2064,45 +2499,45 @@ async function validateApiKey(apiKey, apiUrl) {
|
|
|
2064
2499
|
}
|
|
2065
2500
|
function findConfigTargets() {
|
|
2066
2501
|
const targets = [];
|
|
2067
|
-
const projectMcp =
|
|
2502
|
+
const projectMcp = join2(process.cwd(), ".mcp.json");
|
|
2068
2503
|
targets.push({
|
|
2069
2504
|
name: "Claude Code (project)",
|
|
2070
2505
|
path: projectMcp,
|
|
2071
2506
|
detected: true
|
|
2072
2507
|
// Always available
|
|
2073
2508
|
});
|
|
2074
|
-
const claudeUser =
|
|
2509
|
+
const claudeUser = join2(homedir3(), ".claude.json");
|
|
2075
2510
|
targets.push({
|
|
2076
2511
|
name: "Claude Code (user \u2014 all projects)",
|
|
2077
2512
|
path: claudeUser,
|
|
2078
2513
|
detected: true
|
|
2079
2514
|
// Always available
|
|
2080
2515
|
});
|
|
2081
|
-
const cursorProject =
|
|
2082
|
-
const cursorDir =
|
|
2516
|
+
const cursorProject = join2(process.cwd(), ".cursor", "mcp.json");
|
|
2517
|
+
const cursorDir = join2(process.cwd(), ".cursor");
|
|
2083
2518
|
targets.push({
|
|
2084
2519
|
name: "Cursor (project)",
|
|
2085
2520
|
path: cursorProject,
|
|
2086
|
-
detected:
|
|
2521
|
+
detected: existsSync2(cursorDir)
|
|
2087
2522
|
});
|
|
2088
|
-
const vscodeProject =
|
|
2089
|
-
const vscodeDir =
|
|
2523
|
+
const vscodeProject = join2(process.cwd(), ".vscode", "mcp.json");
|
|
2524
|
+
const vscodeDir = join2(process.cwd(), ".vscode");
|
|
2090
2525
|
targets.push({
|
|
2091
2526
|
name: "VS Code (project)",
|
|
2092
2527
|
path: vscodeProject,
|
|
2093
|
-
detected:
|
|
2528
|
+
detected: existsSync2(vscodeDir)
|
|
2094
2529
|
});
|
|
2095
|
-
const cursorGlobal =
|
|
2530
|
+
const cursorGlobal = join2(homedir3(), ".cursor", "mcp.json");
|
|
2096
2531
|
targets.push({
|
|
2097
2532
|
name: "Cursor (global)",
|
|
2098
2533
|
path: cursorGlobal,
|
|
2099
|
-
detected:
|
|
2534
|
+
detected: existsSync2(join2(homedir3(), ".cursor"))
|
|
2100
2535
|
});
|
|
2101
2536
|
return targets;
|
|
2102
2537
|
}
|
|
2103
2538
|
async function readOrCreateConfig(path) {
|
|
2104
2539
|
try {
|
|
2105
|
-
const content = await
|
|
2540
|
+
const content = await readFile2(path, "utf-8");
|
|
2106
2541
|
return JSON.parse(content);
|
|
2107
2542
|
} catch {
|
|
2108
2543
|
return { mcpServers: {} };
|
|
@@ -2110,11 +2545,11 @@ async function readOrCreateConfig(path) {
|
|
|
2110
2545
|
}
|
|
2111
2546
|
async function writeConfig(path, config2) {
|
|
2112
2547
|
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
2113
|
-
if (!
|
|
2114
|
-
const { mkdir } = await import("fs/promises");
|
|
2115
|
-
await
|
|
2548
|
+
if (!existsSync2(dir)) {
|
|
2549
|
+
const { mkdir: mkdir2 } = await import("fs/promises");
|
|
2550
|
+
await mkdir2(dir, { recursive: true });
|
|
2116
2551
|
}
|
|
2117
|
-
await
|
|
2552
|
+
await writeFile2(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
|
|
2118
2553
|
try {
|
|
2119
2554
|
await chmod(path, 384);
|
|
2120
2555
|
} catch {
|
|
@@ -2192,7 +2627,7 @@ async function setup() {
|
|
|
2192
2627
|
console.log(` Configured: ${target.name}`);
|
|
2193
2628
|
}
|
|
2194
2629
|
console.log(
|
|
2195
|
-
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides
|
|
2630
|
+
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides 17 tools: get_context, get_checks,\n get_project_rules, get_prompt_templates, get_specs, get_docs,\n get_tech_stack, get_planning_context, get_plan_board, create_check,\n create_project_rule, create_prompt_template, update_spec,\n report_decision, publish_skill, list_skills, install_skill.\n\n get_guardrails and create_guardrail remain registered as deprecated\n aliases of get_checks and create_check \u2014 existing configs keep working.\n"
|
|
2196
2631
|
);
|
|
2197
2632
|
}
|
|
2198
2633
|
var args = process.argv.slice(2);
|
|
@@ -2204,6 +2639,10 @@ if (command === "setup") {
|
|
|
2204
2639
|
`);
|
|
2205
2640
|
process.exit(1);
|
|
2206
2641
|
});
|
|
2642
|
+
} else if (command === "install-skill") {
|
|
2643
|
+
Promise.resolve().then(() => (init_install_skill(), install_skill_exports)).then(
|
|
2644
|
+
({ runInstallSkill: runInstallSkill2 }) => runInstallSkill2(args.slice(1), { out: console.log, err: console.error })
|
|
2645
|
+
).then((code) => process.exit(code));
|
|
2207
2646
|
} else if (command === "--help" || command === "-h") {
|
|
2208
2647
|
console.log(`
|
|
2209
2648
|
SystemDox MCP Server
|
|
@@ -2211,6 +2650,9 @@ if (command === "setup") {
|
|
|
2211
2650
|
Usage:
|
|
2212
2651
|
systemdox-mcp-server Start the MCP server (stdio transport)
|
|
2213
2652
|
systemdox-mcp-server setup Configure the MCP server in your AI coding tool
|
|
2653
|
+
systemdox-mcp-server install-skill <name> [--version n] [--target dir] [--overwrite]
|
|
2654
|
+
Install a skill published to SystemDox
|
|
2655
|
+
(default folder: ~/.claude/skills/<name>)
|
|
2214
2656
|
systemdox-mcp-server --help Show this help message
|
|
2215
2657
|
|
|
2216
2658
|
Environment variables:
|