@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/install-skill.js
CHANGED
|
@@ -63,14 +63,6 @@ var SystemDoxApiClient = class {
|
|
|
63
63
|
this.config = config;
|
|
64
64
|
this.cache = new ResponseCache(config.cacheTtlMs);
|
|
65
65
|
}
|
|
66
|
-
async validate() {
|
|
67
|
-
const res = await this.fetchRaw("/v1/health");
|
|
68
|
-
if (!res.ok) {
|
|
69
|
-
throw new Error(
|
|
70
|
-
`SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
66
|
async get(path, params) {
|
|
75
67
|
const url = this.buildUrl(path, params);
|
|
76
68
|
const cacheKey = url.toString();
|
|
@@ -84,8 +76,8 @@ var SystemDoxApiClient = class {
|
|
|
84
76
|
}
|
|
85
77
|
/** A read that skips the cache: an install must take the version that is
|
|
86
78
|
* latest now, not the one cached before someone published another. */
|
|
87
|
-
async getFresh(path) {
|
|
88
|
-
const res = await this.fetchRaw(path);
|
|
79
|
+
async getFresh(path, signal) {
|
|
80
|
+
const res = await this.fetchRaw(path, void 0, signal);
|
|
89
81
|
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
90
82
|
return await res.json();
|
|
91
83
|
}
|
|
@@ -115,8 +107,8 @@ var SystemDoxApiClient = class {
|
|
|
115
107
|
// One method per operationId. Every read skips the cache: setup, sync and
|
|
116
108
|
// plugin-path act on what the workspace holds now.
|
|
117
109
|
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
118
|
-
getOwnTeamSetup() {
|
|
119
|
-
return this.getFresh(TEAM_SETUP);
|
|
110
|
+
getOwnTeamSetup(signal) {
|
|
111
|
+
return this.getFresh(TEAM_SETUP, signal);
|
|
120
112
|
}
|
|
121
113
|
getTeamSetup(workspace) {
|
|
122
114
|
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
@@ -220,14 +212,15 @@ var SystemDoxApiClient = class {
|
|
|
220
212
|
}
|
|
221
213
|
return url;
|
|
222
214
|
}
|
|
223
|
-
async fetchRaw(path, params) {
|
|
215
|
+
async fetchRaw(path, params, signal) {
|
|
224
216
|
const url = this.buildUrl(path, params);
|
|
225
217
|
return fetch(url, {
|
|
226
218
|
headers: {
|
|
227
219
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
228
220
|
"X-API-Key": this.config.apiKey,
|
|
229
221
|
Accept: "application/json"
|
|
230
|
-
}
|
|
222
|
+
},
|
|
223
|
+
signal
|
|
231
224
|
});
|
|
232
225
|
}
|
|
233
226
|
};
|
|
@@ -274,6 +267,7 @@ import { existsSync } from "fs";
|
|
|
274
267
|
import { homedir } from "os";
|
|
275
268
|
import { dirname, join, relative, resolve, sep } from "path";
|
|
276
269
|
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
270
|
+
var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
|
|
277
271
|
var MAX_FILE_BYTES = 256 * 1024;
|
|
278
272
|
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
279
273
|
var NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -297,7 +291,7 @@ function isValidName(name) {
|
|
|
297
291
|
return name.length <= 64 && NAME_RE.test(name);
|
|
298
292
|
}
|
|
299
293
|
function isValidPath(path) {
|
|
300
|
-
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
|
|
294
|
+
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
|
|
301
295
|
}
|
|
302
296
|
function defaultSkillDir(name) {
|
|
303
297
|
return join(homedir(), ".claude", "skills", name);
|
|
@@ -391,6 +385,9 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
391
385
|
keptEdited
|
|
392
386
|
};
|
|
393
387
|
}
|
|
388
|
+
function afterInstall(skillMd) {
|
|
389
|
+
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.`;
|
|
390
|
+
}
|
|
394
391
|
function describeInstall(name, r) {
|
|
395
392
|
const lines = [
|
|
396
393
|
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
@@ -400,7 +397,7 @@ function describeInstall(name, r) {
|
|
|
400
397
|
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
401
398
|
}
|
|
402
399
|
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
403
|
-
lines.push("
|
|
400
|
+
lines.push(afterInstall(join(r.dest, "SKILL.md")));
|
|
404
401
|
return lines.join("\n");
|
|
405
402
|
}
|
|
406
403
|
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -17,16 +17,15 @@ export declare class SystemDoxApiClient {
|
|
|
17
17
|
private config;
|
|
18
18
|
private cache;
|
|
19
19
|
constructor(config: ServerConfig);
|
|
20
|
-
validate(): Promise<void>;
|
|
21
20
|
get<T>(path: string, params?: Record<string, string>): Promise<T>;
|
|
22
21
|
/** A read that skips the cache: an install must take the version that is
|
|
23
22
|
* latest now, not the one cached before someone published another. */
|
|
24
|
-
getFresh<T>(path: string): Promise<T>;
|
|
23
|
+
getFresh<T>(path: string, signal?: AbortSignal): Promise<T>;
|
|
25
24
|
post<T>(path: string, body: unknown): Promise<T>;
|
|
26
25
|
put<T>(path: string, body: unknown): Promise<T>;
|
|
27
26
|
private mutate;
|
|
28
27
|
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
29
|
-
getOwnTeamSetup(): Promise<TeamSetup>;
|
|
28
|
+
getOwnTeamSetup(signal?: AbortSignal): Promise<TeamSetup>;
|
|
30
29
|
getTeamSetup(workspace: string): Promise<TeamSetup>;
|
|
31
30
|
getMarketplace(workspace: string): Promise<Marketplace>;
|
|
32
31
|
getPart(workspace: string, part: PartName): Promise<PartDetail>;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -60,14 +60,6 @@ var SystemDoxApiClient = class {
|
|
|
60
60
|
this.config = config;
|
|
61
61
|
this.cache = new ResponseCache(config.cacheTtlMs);
|
|
62
62
|
}
|
|
63
|
-
async validate() {
|
|
64
|
-
const res = await this.fetchRaw("/v1/health");
|
|
65
|
-
if (!res.ok) {
|
|
66
|
-
throw new Error(
|
|
67
|
-
`SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
63
|
async get(path, params) {
|
|
72
64
|
const url = this.buildUrl(path, params);
|
|
73
65
|
const cacheKey = url.toString();
|
|
@@ -81,8 +73,8 @@ var SystemDoxApiClient = class {
|
|
|
81
73
|
}
|
|
82
74
|
/** A read that skips the cache: an install must take the version that is
|
|
83
75
|
* latest now, not the one cached before someone published another. */
|
|
84
|
-
async getFresh(path) {
|
|
85
|
-
const res = await this.fetchRaw(path);
|
|
76
|
+
async getFresh(path, signal) {
|
|
77
|
+
const res = await this.fetchRaw(path, void 0, signal);
|
|
86
78
|
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
87
79
|
return await res.json();
|
|
88
80
|
}
|
|
@@ -112,8 +104,8 @@ var SystemDoxApiClient = class {
|
|
|
112
104
|
// One method per operationId. Every read skips the cache: setup, sync and
|
|
113
105
|
// plugin-path act on what the workspace holds now.
|
|
114
106
|
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
115
|
-
getOwnTeamSetup() {
|
|
116
|
-
return this.getFresh(TEAM_SETUP);
|
|
107
|
+
getOwnTeamSetup(signal) {
|
|
108
|
+
return this.getFresh(TEAM_SETUP, signal);
|
|
117
109
|
}
|
|
118
110
|
getTeamSetup(workspace) {
|
|
119
111
|
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
@@ -217,14 +209,15 @@ var SystemDoxApiClient = class {
|
|
|
217
209
|
}
|
|
218
210
|
return url;
|
|
219
211
|
}
|
|
220
|
-
async fetchRaw(path, params) {
|
|
212
|
+
async fetchRaw(path, params, signal) {
|
|
221
213
|
const url = this.buildUrl(path, params);
|
|
222
214
|
return fetch(url, {
|
|
223
215
|
headers: {
|
|
224
216
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
225
217
|
"X-API-Key": this.config.apiKey,
|
|
226
218
|
Accept: "application/json"
|
|
227
|
-
}
|
|
219
|
+
},
|
|
220
|
+
signal
|
|
228
221
|
});
|
|
229
222
|
}
|
|
230
223
|
};
|
|
@@ -403,16 +403,22 @@ async function hydrate(api, item, meta) {
|
|
|
403
403
|
}
|
|
404
404
|
async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
|
|
405
405
|
const budget = maxTokens ?? DEFAULT_BUDGET;
|
|
406
|
+
const failures = [];
|
|
407
|
+
const orEmpty = (empty) => (err) => {
|
|
408
|
+
failures.push(err);
|
|
409
|
+
return empty;
|
|
410
|
+
};
|
|
406
411
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
407
|
-
api.get("/v1/guardrails").catch((
|
|
408
|
-
api.get("/v1/specs").catch((
|
|
409
|
-
api.get("/v1/docs").catch((
|
|
410
|
-
api.get("/v1/ai/settings/stack").catch((
|
|
411
|
-
api.get("/v1/standards/rules").catch((
|
|
412
|
+
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
413
|
+
api.get("/v1/specs").catch(orEmpty([])),
|
|
414
|
+
api.get("/v1/docs").catch(orEmpty({ items: [] })),
|
|
415
|
+
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
416
|
+
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
412
417
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
413
418
|
// blip) gates nothing, it can never hide working context.
|
|
414
419
|
api.get("/v1/doc-repos").catch(() => null)
|
|
415
420
|
]);
|
|
421
|
+
if (failures.length === 5) throw failures[0];
|
|
416
422
|
const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
|
|
417
423
|
const guardrails = disabled("fitness") ? [] : rawGuardrails;
|
|
418
424
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
@@ -619,12 +625,13 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
|
|
|
619
625
|
guardrails: selectedGuardrails.length
|
|
620
626
|
};
|
|
621
627
|
if (sections.length === 0) {
|
|
622
|
-
|
|
628
|
+
const text = 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.";
|
|
629
|
+
return { text, stats };
|
|
623
630
|
}
|
|
624
631
|
const footer = `
|
|
625
632
|
|
|
626
633
|
---
|
|
627
|
-
_Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
|
|
634
|
+
_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` : "") + `_`;
|
|
628
635
|
return { text: body + footer, stats };
|
|
629
636
|
}
|
|
630
637
|
export {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { SystemDoxApiClient } from "./api-client.js";
|
|
2
|
+
import type { TeamSetup } from "./team-setup-types.js";
|
|
3
|
+
/** Claude Code cuts a server's instructions at 2,048 characters in older versions and 4,096 now: this fits both. */
|
|
4
|
+
export declare const INSTRUCTIONS_MAX = 2000;
|
|
5
|
+
/**
|
|
6
|
+
* The read that a session's start waits for, at most. Sessions start rarely, so
|
|
7
|
+
* the read is usually a cold one: in prod on 2026-11-04 the team setup function's
|
|
8
|
+
* cold start alone took 2 to 2.9 s, before the authoriser and the network, while a
|
|
9
|
+
* warm read took 0.65 s. A bound that fits only a warm read would fail the common
|
|
10
|
+
* case.
|
|
11
|
+
*/
|
|
12
|
+
export declare const BRIEF_TIMEOUT_MS = 8000;
|
|
13
|
+
/** What the local server sends when this computer has no key: the API refuses it. */
|
|
14
|
+
export declare const NO_KEY = "no-key-on-this-computer";
|
|
15
|
+
declare const SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
|
|
16
|
+
declare const CANNOT_SEE: string;
|
|
17
|
+
/**
|
|
18
|
+
* The reason a key could not be loaded, as words of our own. The error's message
|
|
19
|
+
* goes to the server's log: it can hold a path on this computer, a long list of
|
|
20
|
+
* workspaces or a value from the environment, and none of that belongs in a system
|
|
21
|
+
* prompt.
|
|
22
|
+
*/
|
|
23
|
+
export declare function keyReason(message: string): string;
|
|
24
|
+
export interface ComputerState {
|
|
25
|
+
missing: string[];
|
|
26
|
+
behind: {
|
|
27
|
+
name: string;
|
|
28
|
+
here: number;
|
|
29
|
+
latest: number;
|
|
30
|
+
}[];
|
|
31
|
+
notFromSystemDox: string[];
|
|
32
|
+
pluginInstalled: boolean;
|
|
33
|
+
pluginVersion?: string;
|
|
34
|
+
}
|
|
35
|
+
/** The team skills against this computer's folders. Reads only. */
|
|
36
|
+
export declare function computerState(setup: TeamSetup): Promise<ComputerState>;
|
|
37
|
+
/** Whether `setup` would add anything: a skill outside the default space is in no plugin. */
|
|
38
|
+
export declare function hasPersonParts(setup: TeamSetup): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* What the local server adds, within `room`: nothing when this computer lacks
|
|
41
|
+
* nothing. The sentences are fixed and the lists take what is left, so a sentence
|
|
42
|
+
* is never cut. If the fixed sentences do not fit, the least urgent line goes whole.
|
|
43
|
+
*/
|
|
44
|
+
export declare function localSection(setup: TeamSetup, state: ComputerState, room: number): string;
|
|
45
|
+
export interface BriefOptions {
|
|
46
|
+
localFiles?: boolean;
|
|
47
|
+
/** Why the local server has no key, when it has none. */
|
|
48
|
+
keyProblem?: string;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}
|
|
51
|
+
/** The team setup, or what to tell a session about why it could not be read. */
|
|
52
|
+
export declare function readTeamSetup(apiClient: Pick<SystemDoxApiClient, "getOwnTeamSetup">, { keyProblem, timeoutMs }?: BriefOptions): Promise<{
|
|
53
|
+
setup: TeamSetup;
|
|
54
|
+
} | {
|
|
55
|
+
problem: string;
|
|
56
|
+
}>;
|
|
57
|
+
/** The `instructions` of an `initialize` answer. Never throws. */
|
|
58
|
+
export declare function sessionInstructions(apiClient: Pick<SystemDoxApiClient, "getOwnTeamSetup">, options?: BriefOptions): Promise<string>;
|
|
59
|
+
export { CANNOT_SEE, SETUP_COMMAND };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/session-brief.ts
|
|
4
|
+
import { existsSync as existsSync2 } from "fs";
|
|
5
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
6
|
+
import { join as join3 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/lib/skills.ts
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
11
|
+
import { existsSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
14
|
+
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
15
|
+
var MAX_FILE_BYTES = 256 * 1024;
|
|
16
|
+
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
17
|
+
function defaultSkillDir(name) {
|
|
18
|
+
return join(homedir(), ".claude", "skills", name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/lib/team-setup-files.ts
|
|
22
|
+
import { execFile } from "child_process";
|
|
23
|
+
import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
24
|
+
import { homedir as homedir2, userInfo } from "os";
|
|
25
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
26
|
+
import { promisify } from "util";
|
|
27
|
+
function homeDir() {
|
|
28
|
+
return homedir2();
|
|
29
|
+
}
|
|
30
|
+
var run = promisify(execFile);
|
|
31
|
+
|
|
32
|
+
// src/lib/session-brief.ts
|
|
33
|
+
var INSTRUCTIONS_MAX = 2e3;
|
|
34
|
+
var BRIEF_TIMEOUT_MS = 8e3;
|
|
35
|
+
var NO_KEY = "no-key-on-this-computer";
|
|
36
|
+
var SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
|
|
37
|
+
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.`;
|
|
38
|
+
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.";
|
|
39
|
+
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.";
|
|
40
|
+
function noKey(reason) {
|
|
41
|
+
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.`;
|
|
42
|
+
}
|
|
43
|
+
function keyReason(message) {
|
|
44
|
+
if (/holds keys for/.test(message)) return "keys for several workspaces are stored; set SYSTEMDOX_WORKSPACE to the one to use";
|
|
45
|
+
if (/holds no key for/.test(message)) return "SYSTEMDOX_WORKSPACE names a workspace with no stored key";
|
|
46
|
+
if (/No SystemDox key/.test(message)) return "none is stored";
|
|
47
|
+
return "the stored keys could not be read";
|
|
48
|
+
}
|
|
49
|
+
function olderApi(setup) {
|
|
50
|
+
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.`;
|
|
51
|
+
}
|
|
52
|
+
async function readJsonQuietly(path) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
55
|
+
} catch {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function computerState(setup) {
|
|
60
|
+
const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
|
|
61
|
+
const installed = await readJsonQuietly(join3(homeDir(), ".claude", "plugins", "installed_plugins.json"));
|
|
62
|
+
const entries = installed?.plugins?.[id];
|
|
63
|
+
const entry = Array.isArray(entries) ? entries.find((e) => (e?.scope ?? "user") === "user") : void 0;
|
|
64
|
+
const state = {
|
|
65
|
+
missing: [],
|
|
66
|
+
behind: [],
|
|
67
|
+
notFromSystemDox: [],
|
|
68
|
+
pluginInstalled: !!entry,
|
|
69
|
+
pluginVersion: entry?.version
|
|
70
|
+
};
|
|
71
|
+
const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
|
|
72
|
+
for (const skill of setup.sessionBrief?.skills ?? []) {
|
|
73
|
+
const dir = defaultSkillDir(skill.name);
|
|
74
|
+
if (!existsSync2(dir)) {
|
|
75
|
+
if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const manifest = await readJsonQuietly(join3(dir, MANIFEST_FILE));
|
|
79
|
+
if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
|
|
80
|
+
state.notFromSystemDox.push(skill.name);
|
|
81
|
+
} else if (manifest.version !== skill.latestVersion) {
|
|
82
|
+
state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return state;
|
|
86
|
+
}
|
|
87
|
+
function hasPersonParts(setup) {
|
|
88
|
+
return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
|
|
89
|
+
}
|
|
90
|
+
var MORE = (n) => `${n} more (get_team_setup lists them)`;
|
|
91
|
+
var LIST_MIN = `all ${MORE(100)}`.length;
|
|
92
|
+
function listWithin(items, room) {
|
|
93
|
+
const whole = items.join(", ");
|
|
94
|
+
if (whole.length <= room) return whole;
|
|
95
|
+
const kept = [];
|
|
96
|
+
let used = 0;
|
|
97
|
+
for (let i = 0; i < items.length; i++) {
|
|
98
|
+
const rest = items.length - i - 1;
|
|
99
|
+
const cost = items[i].length + (kept.length ? 2 : 0);
|
|
100
|
+
if (used + cost + ` and ${MORE(rest)}`.length > room) break;
|
|
101
|
+
kept.push(items[i]);
|
|
102
|
+
used += cost;
|
|
103
|
+
}
|
|
104
|
+
return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
|
|
105
|
+
}
|
|
106
|
+
function localSection(setup, state, room) {
|
|
107
|
+
const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
|
|
108
|
+
const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
|
|
109
|
+
const lines = [];
|
|
110
|
+
if (state.missing.length) {
|
|
111
|
+
lines.push({
|
|
112
|
+
head: "On this computer, missing: ",
|
|
113
|
+
items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
|
|
114
|
+
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.`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (state.behind.length) {
|
|
118
|
+
lines.push({
|
|
119
|
+
head: "On this computer, behind: ",
|
|
120
|
+
items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
|
|
121
|
+
tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (state.notFromSystemDox.length) {
|
|
125
|
+
lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
|
|
126
|
+
}
|
|
127
|
+
const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
|
|
128
|
+
while (lines.length && fixed() > room) lines.pop();
|
|
129
|
+
if (!lines.length) return "";
|
|
130
|
+
const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
|
|
131
|
+
return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
|
|
132
|
+
}
|
|
133
|
+
function fit(text, section) {
|
|
134
|
+
const whole = section ? `${text}
|
|
135
|
+
|
|
136
|
+
${section}` : text;
|
|
137
|
+
if (whole.length <= INSTRUCTIONS_MAX) return whole;
|
|
138
|
+
return text.length <= INSTRUCTIONS_MAX ? text : `${text.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
|
|
139
|
+
}
|
|
140
|
+
async function readTeamSetup(apiClient, { keyProblem, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
|
|
141
|
+
if (keyProblem) return { problem: noKey(keyProblem) };
|
|
142
|
+
try {
|
|
143
|
+
const setup = await apiClient.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
|
|
144
|
+
return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
|
|
145
|
+
} catch (err) {
|
|
146
|
+
const status = err?.status;
|
|
147
|
+
return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function sessionInstructions(apiClient, options = {}) {
|
|
151
|
+
const read = await readTeamSetup(apiClient, options);
|
|
152
|
+
if ("problem" in read) return read.problem;
|
|
153
|
+
const text = read.setup.sessionBrief?.text ?? olderApi(read.setup);
|
|
154
|
+
try {
|
|
155
|
+
if (!options.localFiles) return fit(text, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
|
|
156
|
+
const room = INSTRUCTIONS_MAX - text.length - 2;
|
|
157
|
+
return fit(text, localSection(read.setup, await computerState(read.setup), room));
|
|
158
|
+
} catch {
|
|
159
|
+
return fit(text, "");
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export {
|
|
163
|
+
BRIEF_TIMEOUT_MS,
|
|
164
|
+
CANNOT_SEE,
|
|
165
|
+
INSTRUCTIONS_MAX,
|
|
166
|
+
NO_KEY,
|
|
167
|
+
SETUP_COMMAND,
|
|
168
|
+
computerState,
|
|
169
|
+
hasPersonParts,
|
|
170
|
+
keyReason,
|
|
171
|
+
localSection,
|
|
172
|
+
readTeamSetup,
|
|
173
|
+
sessionInstructions
|
|
174
|
+
};
|
package/dist/lib/skills.d.ts
CHANGED
|
@@ -103,5 +103,12 @@ export declare function installSkill(version: SkillVersionFiles, dest: string, {
|
|
|
103
103
|
}): Promise<InstallResult>;
|
|
104
104
|
/** A fence longer than any backtick run in `text`, so content cannot close it. */
|
|
105
105
|
export declare function fenceFor(text: string): string;
|
|
106
|
+
/**
|
|
107
|
+
* What follows an install. Claude Code watches its skills folders, so a running
|
|
108
|
+
* session needs no restart, unless the top-level folder did not exist when the
|
|
109
|
+
* session started (code.claude.com/docs/en/skills, "Edit a skill during a
|
|
110
|
+
* session"): `/reload-skills` covers that, and reading the file works anywhere.
|
|
111
|
+
*/
|
|
112
|
+
export declare function afterInstall(skillMd: string): string;
|
|
106
113
|
/** Human summary of an install, shared by the tool and the CLI. */
|
|
107
114
|
export declare function describeInstall(name: string, r: InstallResult): string;
|
package/dist/lib/skills.js
CHANGED
|
@@ -8,6 +8,7 @@ import { homedir } from "os";
|
|
|
8
8
|
import { dirname, join, relative, resolve, sep } from "path";
|
|
9
9
|
var MANIFEST_FILE = ".systemdox-skill.json";
|
|
10
10
|
var ENTRY_FILE = "SKILL.md";
|
|
11
|
+
var PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
|
|
11
12
|
var MAX_FILES = 100;
|
|
12
13
|
var MAX_FILE_BYTES = 256 * 1024;
|
|
13
14
|
var MAX_TOTAL_BYTES = 1024 * 1024;
|
|
@@ -32,7 +33,7 @@ function isValidName(name) {
|
|
|
32
33
|
return name.length <= 64 && NAME_RE.test(name);
|
|
33
34
|
}
|
|
34
35
|
function isValidPath(path) {
|
|
35
|
-
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
|
|
36
|
+
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
|
|
36
37
|
}
|
|
37
38
|
function frontmatterName(skillMd) {
|
|
38
39
|
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
@@ -181,6 +182,9 @@ function fenceFor(text) {
|
|
|
181
182
|
const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((r) => r.length));
|
|
182
183
|
return "`".repeat(Math.max(3, longest + 1));
|
|
183
184
|
}
|
|
185
|
+
function afterInstall(skillMd) {
|
|
186
|
+
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.`;
|
|
187
|
+
}
|
|
184
188
|
function describeInstall(name, r) {
|
|
185
189
|
const lines = [
|
|
186
190
|
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
@@ -190,7 +194,7 @@ function describeInstall(name, r) {
|
|
|
190
194
|
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
191
195
|
}
|
|
192
196
|
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
193
|
-
lines.push("
|
|
197
|
+
lines.push(afterInstall(join(r.dest, "SKILL.md")));
|
|
194
198
|
return lines.join("\n");
|
|
195
199
|
}
|
|
196
200
|
export {
|
|
@@ -200,6 +204,7 @@ export {
|
|
|
200
204
|
MAX_FILE_BYTES,
|
|
201
205
|
MAX_TOTAL_BYTES,
|
|
202
206
|
SkillInstallRefused,
|
|
207
|
+
afterInstall,
|
|
203
208
|
defaultSkillDir,
|
|
204
209
|
describeInstall,
|
|
205
210
|
fenceFor,
|
|
@@ -90,6 +90,24 @@ export interface TeamSetup {
|
|
|
90
90
|
parts: Record<string, number>;
|
|
91
91
|
skills: string[];
|
|
92
92
|
memory: MemoryProjectSummary[];
|
|
93
|
+
/** Absent on an API older than ADR-223. */
|
|
94
|
+
sessionBrief?: SessionBrief;
|
|
95
|
+
}
|
|
96
|
+
/** What every session is told (ADR-223, specs/changes/session-brief/contract.md). */
|
|
97
|
+
export interface SessionBrief {
|
|
98
|
+
text: string;
|
|
99
|
+
/** What a session may install by itself: in the default space, and unable to run anything by itself. */
|
|
100
|
+
skills: {
|
|
101
|
+
name: string;
|
|
102
|
+
description: string;
|
|
103
|
+
latestVersion: number;
|
|
104
|
+
}[];
|
|
105
|
+
/** The rest: they reach a computer only through the person. Only the name is repeated. */
|
|
106
|
+
heldSkills: {
|
|
107
|
+
name: string;
|
|
108
|
+
reason: string;
|
|
109
|
+
}[];
|
|
110
|
+
omittedSkills: number;
|
|
93
111
|
}
|
|
94
112
|
/** `{path: sha256}`. */
|
|
95
113
|
export type Manifest = Record<string, string>;
|
package/dist/plugin-path.js
CHANGED
|
@@ -67,14 +67,6 @@ var SystemDoxApiClient = class {
|
|
|
67
67
|
this.config = config;
|
|
68
68
|
this.cache = new ResponseCache(config.cacheTtlMs);
|
|
69
69
|
}
|
|
70
|
-
async validate() {
|
|
71
|
-
const res = await this.fetchRaw("/v1/health");
|
|
72
|
-
if (!res.ok) {
|
|
73
|
-
throw new Error(
|
|
74
|
-
`SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
70
|
async get(path, params) {
|
|
79
71
|
const url = this.buildUrl(path, params);
|
|
80
72
|
const cacheKey = url.toString();
|
|
@@ -88,8 +80,8 @@ var SystemDoxApiClient = class {
|
|
|
88
80
|
}
|
|
89
81
|
/** A read that skips the cache: an install must take the version that is
|
|
90
82
|
* latest now, not the one cached before someone published another. */
|
|
91
|
-
async getFresh(path) {
|
|
92
|
-
const res = await this.fetchRaw(path);
|
|
83
|
+
async getFresh(path, signal) {
|
|
84
|
+
const res = await this.fetchRaw(path, void 0, signal);
|
|
93
85
|
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
94
86
|
return await res.json();
|
|
95
87
|
}
|
|
@@ -119,8 +111,8 @@ var SystemDoxApiClient = class {
|
|
|
119
111
|
// One method per operationId. Every read skips the cache: setup, sync and
|
|
120
112
|
// plugin-path act on what the workspace holds now.
|
|
121
113
|
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
122
|
-
getOwnTeamSetup() {
|
|
123
|
-
return this.getFresh(TEAM_SETUP);
|
|
114
|
+
getOwnTeamSetup(signal) {
|
|
115
|
+
return this.getFresh(TEAM_SETUP, signal);
|
|
124
116
|
}
|
|
125
117
|
getTeamSetup(workspace) {
|
|
126
118
|
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
@@ -224,14 +216,15 @@ var SystemDoxApiClient = class {
|
|
|
224
216
|
}
|
|
225
217
|
return url;
|
|
226
218
|
}
|
|
227
|
-
async fetchRaw(path, params) {
|
|
219
|
+
async fetchRaw(path, params, signal) {
|
|
228
220
|
const url = this.buildUrl(path, params);
|
|
229
221
|
return fetch(url, {
|
|
230
222
|
headers: {
|
|
231
223
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
232
224
|
"X-API-Key": this.config.apiKey,
|
|
233
225
|
Accept: "application/json"
|
|
234
|
-
}
|
|
226
|
+
},
|
|
227
|
+
signal
|
|
235
228
|
});
|
|
236
229
|
}
|
|
237
230
|
};
|
package/dist/server.d.ts
CHANGED
|
@@ -5,10 +5,14 @@ export interface ServerOptions {
|
|
|
5
5
|
* and write the skill folder there. The hosted server never touches a disk
|
|
6
6
|
* (ADR-216 §2.2). */
|
|
7
7
|
localFiles?: boolean;
|
|
8
|
+
/** What the server tells every session in its `initialize` answer (ADR-223). */
|
|
9
|
+
instructions?: string;
|
|
10
|
+
/** Why the local server has no key, when it has none. */
|
|
11
|
+
keyProblem?: string;
|
|
8
12
|
}
|
|
9
13
|
/**
|
|
10
14
|
* Build a SystemDox MCP server with all tools registered against the given
|
|
11
15
|
* API client. Shared by the stdio entry point (index.ts) and the hosted
|
|
12
16
|
* Streamable HTTP entry point (http.ts).
|
|
13
17
|
*/
|
|
14
|
-
export declare function createServer(apiClient: SystemDoxApiClient, { localFiles }?: ServerOptions): McpServer;
|
|
18
|
+
export declare function createServer(apiClient: SystemDoxApiClient, { localFiles, instructions, keyProblem }?: ServerOptions): McpServer;
|