@systemdox/mcp-server 0.1.49 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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);
@@ -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>;
@@ -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(() => ({ items: [] })),
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
- return { text: "No relevant context found for this task.", stats };
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
+ };
@@ -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);
@@ -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>;
@@ -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;