@systemdox/mcp-server 0.1.48 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -11
- package/dist/cli.js +963 -357
- package/dist/http.js +1248 -732
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1341 -784
- package/dist/install-skill.js +10 -17
- package/dist/lib/api-client.d.ts +2 -3
- package/dist/lib/api-client.js +7 -14
- package/dist/lib/config.d.ts +4 -1
- package/dist/lib/config.js +1 -2
- package/dist/lib/context-builder.js +14 -7
- package/dist/lib/session-brief.d.ts +59 -0
- package/dist/lib/session-brief.js +174 -0
- package/dist/lib/skills.js +2 -1
- package/dist/lib/team-setup-files.d.ts +14 -1
- package/dist/lib/team-setup-files.js +43 -0
- package/dist/lib/team-setup-types.d.ts +18 -0
- package/dist/plugin-path.js +7 -14
- package/dist/server.d.ts +5 -1
- package/dist/server.js +3579 -0
- package/dist/setup-mcp.d.ts +66 -0
- package/dist/setup-mcp.js +630 -0
- package/dist/setup-team.d.ts +5 -1
- package/dist/setup-team.js +8 -14
- package/dist/sync-memory.js +29 -26
- package/dist/tools/document-graph.d.ts +87 -0
- package/dist/tools/get-change.d.ts +134 -0
- package/dist/tools/get-change.js +147 -0
- package/dist/tools/get-team-setup.d.ts +6 -0
- package/dist/tools/skills.js +3 -2
- package/dist/tools/team-setup.js +2 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -26,8 +26,7 @@ function requireHttpsApiUrl(apiUrl) {
|
|
|
26
26
|
}
|
|
27
27
|
return apiUrl;
|
|
28
28
|
}
|
|
29
|
-
function loadConfig() {
|
|
30
|
-
const apiKey = process.env.SYSTEMDOX_API_KEY;
|
|
29
|
+
function loadConfig(apiKey = process.env.SYSTEMDOX_API_KEY) {
|
|
31
30
|
if (!apiKey) {
|
|
32
31
|
throw new Error(
|
|
33
32
|
"SYSTEMDOX_API_KEY is required. Generate one at your SystemDox settings page."
|
|
@@ -125,6 +124,31 @@ async function apiKeyFor(workspace, env = process.env) {
|
|
|
125
124
|
const entry = (await readCredentials())[workspace];
|
|
126
125
|
return typeof entry?.apiKey === "string" && entry.apiKey ? entry.apiKey : void 0;
|
|
127
126
|
}
|
|
127
|
+
async function localApiKey(env = process.env) {
|
|
128
|
+
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
129
|
+
const keys = Object.entries(await readCredentials()).filter(
|
|
130
|
+
(e) => typeof e[1]?.apiKey === "string" && e[1].apiKey.length > 0
|
|
131
|
+
);
|
|
132
|
+
const wanted = env.SYSTEMDOX_WORKSPACE;
|
|
133
|
+
if (wanted) {
|
|
134
|
+
const key = keys.find(([ws]) => ws === wanted)?.[1].apiKey;
|
|
135
|
+
if (key) return key;
|
|
136
|
+
throw new Error(
|
|
137
|
+
`~/.claude/systemdox/credentials.json holds no key for ${wanted} (SYSTEMDOX_WORKSPACE). Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
if (keys.length === 1) return keys[0][1].apiKey;
|
|
141
|
+
if (keys.length > 1) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`~/.claude/systemdox/credentials.json holds keys for ${keys.map(([ws]) => ws).join(", ")}: set SYSTEMDOX_WORKSPACE to the workspace to use, or SYSTEMDOX_API_KEY to its key.`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
throw new Error("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.");
|
|
147
|
+
}
|
|
148
|
+
async function loadLocalConfig(env = process.env) {
|
|
149
|
+
const apiKey = await localApiKey(env);
|
|
150
|
+
return loadConfig(apiKey);
|
|
151
|
+
}
|
|
128
152
|
function apiUrlFrom(env = process.env) {
|
|
129
153
|
return requireHttpsApiUrl(env.SYSTEMDOX_API_URL || API_URL_DEFAULT);
|
|
130
154
|
}
|
|
@@ -277,14 +301,6 @@ var init_api_client = __esm({
|
|
|
277
301
|
this.config = config2;
|
|
278
302
|
this.cache = new ResponseCache(config2.cacheTtlMs);
|
|
279
303
|
}
|
|
280
|
-
async validate() {
|
|
281
|
-
const res = await this.fetchRaw("/v1/health");
|
|
282
|
-
if (!res.ok) {
|
|
283
|
-
throw new Error(
|
|
284
|
-
`SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
304
|
async get(path, params) {
|
|
289
305
|
const url = this.buildUrl(path, params);
|
|
290
306
|
const cacheKey = url.toString();
|
|
@@ -298,8 +314,8 @@ var init_api_client = __esm({
|
|
|
298
314
|
}
|
|
299
315
|
/** A read that skips the cache: an install must take the version that is
|
|
300
316
|
* latest now, not the one cached before someone published another. */
|
|
301
|
-
async getFresh(path) {
|
|
302
|
-
const res = await this.fetchRaw(path);
|
|
317
|
+
async getFresh(path, signal) {
|
|
318
|
+
const res = await this.fetchRaw(path, void 0, signal);
|
|
303
319
|
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
304
320
|
return await res.json();
|
|
305
321
|
}
|
|
@@ -329,8 +345,8 @@ var init_api_client = __esm({
|
|
|
329
345
|
// One method per operationId. Every read skips the cache: setup, sync and
|
|
330
346
|
// plugin-path act on what the workspace holds now.
|
|
331
347
|
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
332
|
-
getOwnTeamSetup() {
|
|
333
|
-
return this.getFresh(TEAM_SETUP);
|
|
348
|
+
getOwnTeamSetup(signal) {
|
|
349
|
+
return this.getFresh(TEAM_SETUP, signal);
|
|
334
350
|
}
|
|
335
351
|
getTeamSetup(workspace) {
|
|
336
352
|
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
@@ -434,14 +450,15 @@ var init_api_client = __esm({
|
|
|
434
450
|
}
|
|
435
451
|
return url;
|
|
436
452
|
}
|
|
437
|
-
async fetchRaw(path, params) {
|
|
453
|
+
async fetchRaw(path, params, signal) {
|
|
438
454
|
const url = this.buildUrl(path, params);
|
|
439
455
|
return fetch(url, {
|
|
440
456
|
headers: {
|
|
441
457
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
442
458
|
"X-API-Key": this.config.apiKey,
|
|
443
459
|
Accept: "application/json"
|
|
444
|
-
}
|
|
460
|
+
},
|
|
461
|
+
signal
|
|
445
462
|
});
|
|
446
463
|
}
|
|
447
464
|
};
|
|
@@ -1006,11 +1023,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
1006
1023
|
report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
|
|
1007
1024
|
break;
|
|
1008
1025
|
}
|
|
1009
|
-
const
|
|
1010
|
-
if (
|
|
1011
|
-
out(` ${
|
|
1026
|
+
const refused2 = result.refused[0];
|
|
1027
|
+
if (refused2?.code === "SYD-K021") {
|
|
1028
|
+
out(` ${refused2.detail}. Add them in Staff first, or answer p to file them.`);
|
|
1012
1029
|
choices = choices.filter((c) => c !== "m");
|
|
1013
|
-
} else if (
|
|
1030
|
+
} else if (refused2) out(` ${refused2.detail}`);
|
|
1014
1031
|
}
|
|
1015
1032
|
}
|
|
1016
1033
|
for (let i = 0; i < filed.length; i += 50) {
|
|
@@ -1141,17 +1158,6 @@ async function upload(client, ws, project, plan, team, report) {
|
|
|
1141
1158
|
}
|
|
1142
1159
|
await send();
|
|
1143
1160
|
}
|
|
1144
|
-
async function keyFromEnvironment(env) {
|
|
1145
|
-
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
1146
|
-
const entries2 = Object.entries(await readCredentials()).filter(([, v]) => typeof v?.apiKey === "string" && v.apiKey);
|
|
1147
|
-
if (entries2.length === 1) return entries2[0][1].apiKey;
|
|
1148
|
-
if (entries2.length > 1) {
|
|
1149
|
-
throw new SyncFailed(
|
|
1150
|
-
`~/.claude/systemdox/credentials.json holds keys for ${entries2.map(([w]) => w).join(", ")}: set SYSTEMDOX_API_KEY to the key of the workspace to sync.`
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
throw new SyncFailed("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup, then run this again.");
|
|
1154
|
-
}
|
|
1155
1161
|
async function syncMemory(opts) {
|
|
1156
1162
|
const { client, out } = opts;
|
|
1157
1163
|
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
@@ -1287,7 +1293,7 @@ async function runSyncMemory(argv, io, env = process.env) {
|
|
|
1287
1293
|
args2 = parseSyncMemoryArgs(argv);
|
|
1288
1294
|
if (!client) {
|
|
1289
1295
|
const apiUrl = apiUrlFrom(env);
|
|
1290
|
-
const apiKey = await
|
|
1296
|
+
const apiKey = await localApiKey(env);
|
|
1291
1297
|
client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1292
1298
|
}
|
|
1293
1299
|
} catch (e) {
|
|
@@ -1380,6 +1386,7 @@ __export(setup_team_exports, {
|
|
|
1380
1386
|
parseSetupArgs: () => parseSetupArgs,
|
|
1381
1387
|
runTeamSetup: () => runTeamSetup,
|
|
1382
1388
|
runTeamSetupCommand: () => runTeamSetupCommand,
|
|
1389
|
+
setInJsonFile: () => setInJsonFile,
|
|
1383
1390
|
setJsonMember: () => setJsonMember,
|
|
1384
1391
|
withImportBlock: () => withImportBlock
|
|
1385
1392
|
});
|
|
@@ -1759,9 +1766,9 @@ async function runTeamSetup(opts) {
|
|
|
1759
1766
|
const id = `${ws}-setup@${marketplaceName}`;
|
|
1760
1767
|
const installed = await readJsonQuietly(join3(pluginsDir, "installed_plugins.json"));
|
|
1761
1768
|
if (!installed?.plugins?.[id]) {
|
|
1762
|
-
const
|
|
1769
|
+
const known2 = await readJsonQuietly(join3(pluginsDir, "known_marketplaces.json"));
|
|
1763
1770
|
const install = `Run \`claude plugin install ${id}\` in your own terminal and accept the command it shows.`;
|
|
1764
|
-
const steps =
|
|
1771
|
+
const steps = known2 && typeof known2 === "object" && known2[marketplaceName] ? ["One step is left:", `1. ${install}`] : ["Two steps are left:", "1. Open Claude Code once in any folder, then close it.", `2. ${install}`];
|
|
1765
1772
|
out("");
|
|
1766
1773
|
steps.forEach((l) => out(l));
|
|
1767
1774
|
}
|
|
@@ -1812,12 +1819,191 @@ ${lines.join("\n")}`)
|
|
|
1812
1819
|
}
|
|
1813
1820
|
});
|
|
1814
1821
|
|
|
1822
|
+
// src/setup-mcp.ts
|
|
1823
|
+
var setup_mcp_exports = {};
|
|
1824
|
+
__export(setup_mcp_exports, {
|
|
1825
|
+
findConfigTargets: () => findConfigTargets,
|
|
1826
|
+
serverEntry: () => serverEntry,
|
|
1827
|
+
setupMcpServer: () => setupMcpServer,
|
|
1828
|
+
writeServerEntry: () => writeServerEntry
|
|
1829
|
+
});
|
|
1830
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
1831
|
+
import { join as join4 } from "path";
|
|
1832
|
+
async function validateApiKey(apiKey, apiUrl) {
|
|
1833
|
+
try {
|
|
1834
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/v1/health`, {
|
|
1835
|
+
headers: {
|
|
1836
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1837
|
+
"X-API-Key": apiKey,
|
|
1838
|
+
Accept: "application/json"
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
return res.ok;
|
|
1842
|
+
} catch {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
function findConfigTargets(cwd = process.cwd()) {
|
|
1847
|
+
const home = homeDir();
|
|
1848
|
+
return [
|
|
1849
|
+
// Claude Code: the project's .mcp.json, and ~/.claude.json (all projects)
|
|
1850
|
+
{ name: "Claude Code (project)", path: join4(cwd, ".mcp.json"), detected: true },
|
|
1851
|
+
{ name: "Claude Code (user \u2014 all projects)", path: join4(home, ".claude.json"), detected: true },
|
|
1852
|
+
{ name: "Cursor (project)", path: join4(cwd, ".cursor", "mcp.json"), detected: existsSync3(join4(cwd, ".cursor")) },
|
|
1853
|
+
{ name: "VS Code (project)", path: join4(cwd, ".vscode", "mcp.json"), detected: existsSync3(join4(cwd, ".vscode")) },
|
|
1854
|
+
{ name: "Cursor (global)", path: join4(home, ".cursor", "mcp.json"), detected: existsSync3(join4(home, ".cursor")) }
|
|
1855
|
+
];
|
|
1856
|
+
}
|
|
1857
|
+
function serverEntry(apiUrl, existing, workspace) {
|
|
1858
|
+
const env = {};
|
|
1859
|
+
const old = existing && typeof existing === "object" ? existing.env : void 0;
|
|
1860
|
+
if (old && typeof old === "object" && !Array.isArray(old)) {
|
|
1861
|
+
for (const [name, value] of Object.entries(old)) {
|
|
1862
|
+
if (typeof value !== "string" || name === "SYSTEMDOX_API_URL" || name === "SYSTEMDOX_WORKSPACE") continue;
|
|
1863
|
+
if (name === "SYSTEMDOX_API_KEY" && !isReference(value)) continue;
|
|
1864
|
+
env[name] = value;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (apiUrl !== API_URL_DEFAULT) env.SYSTEMDOX_API_URL = apiUrl;
|
|
1868
|
+
if (workspace) env.SYSTEMDOX_WORKSPACE = workspace;
|
|
1869
|
+
const entry = { command: "npx", args: ["@systemdox/mcp-server"] };
|
|
1870
|
+
if (Object.keys(env).length) entry.env = env;
|
|
1871
|
+
return entry;
|
|
1872
|
+
}
|
|
1873
|
+
function currentEntry(path) {
|
|
1874
|
+
let text3;
|
|
1875
|
+
try {
|
|
1876
|
+
text3 = readFileSync(path, "utf8");
|
|
1877
|
+
} catch (e) {
|
|
1878
|
+
if (e.code === "ENOENT") return void 0;
|
|
1879
|
+
throw e;
|
|
1880
|
+
}
|
|
1881
|
+
if (!text3.trim()) return void 0;
|
|
1882
|
+
let config2;
|
|
1883
|
+
try {
|
|
1884
|
+
config2 = JSON.parse(text3);
|
|
1885
|
+
} catch {
|
|
1886
|
+
throw new Error(`${path} is not valid JSON: fix it, then run setup again.`);
|
|
1887
|
+
}
|
|
1888
|
+
const servers = config2 && typeof config2 === "object" ? config2.mcpServers : void 0;
|
|
1889
|
+
return servers && typeof servers === "object" ? servers.systemdox : void 0;
|
|
1890
|
+
}
|
|
1891
|
+
async function writeServerEntry(path, apiUrl, workspace) {
|
|
1892
|
+
const existing = currentEntry(path);
|
|
1893
|
+
const env = existing && typeof existing === "object" ? existing.env : void 0;
|
|
1894
|
+
const heldKey = !!env && typeof env === "object" && "SYSTEMDOX_API_KEY" in env && !isReference(env.SYSTEMDOX_API_KEY);
|
|
1895
|
+
await setInJsonFile(path, ["mcpServers", "systemdox"], serverEntry(apiUrl, existing, workspace));
|
|
1896
|
+
return heldKey;
|
|
1897
|
+
}
|
|
1898
|
+
async function keyWorkspace(opts, apiKey, apiUrl) {
|
|
1899
|
+
const client = opts.client?.(apiKey, apiUrl) ?? new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1900
|
+
try {
|
|
1901
|
+
const { workspace } = await client.getOwnTeamSetup();
|
|
1902
|
+
if (isWorkspace(workspace)) return workspace;
|
|
1903
|
+
opts.out(` SystemDox named the workspace ${JSON.stringify(workspace)}, which is not a workspace name.`);
|
|
1904
|
+
} catch (e) {
|
|
1905
|
+
opts.out(` SystemDox did not name your key's workspace (${e.message}).`);
|
|
1906
|
+
}
|
|
1907
|
+
const fromEnv = (opts.env ?? process.env).SYSTEMDOX_WORKSPACE;
|
|
1908
|
+
if (isWorkspace(fromEnv)) return fromEnv;
|
|
1909
|
+
const typed = (await opts.ask(" Your workspace (acme in acme.systemdox.com): ")).trim().toLowerCase();
|
|
1910
|
+
return isWorkspace(typed) ? typed : void 0;
|
|
1911
|
+
}
|
|
1912
|
+
async function setupMcpServer(opts) {
|
|
1913
|
+
const { out, err, ask } = opts;
|
|
1914
|
+
out("\n SystemDox MCP Server Setup\n");
|
|
1915
|
+
const apiKey = (await ask(" Enter your SystemDox API key: ")).trim();
|
|
1916
|
+
if (!apiKey) {
|
|
1917
|
+
err("\n Error: API key is required.\n Generate one at your SystemDox settings page.\n");
|
|
1918
|
+
return { code: 1 };
|
|
1919
|
+
}
|
|
1920
|
+
const apiUrl = (await ask(` API URL [${API_URL_DEFAULT}]: `)).trim() || API_URL_DEFAULT;
|
|
1921
|
+
try {
|
|
1922
|
+
requireHttpsApiUrl(apiUrl);
|
|
1923
|
+
} catch (e) {
|
|
1924
|
+
err(`
|
|
1925
|
+
Error: ${e.message}
|
|
1926
|
+
`);
|
|
1927
|
+
return { code: 1 };
|
|
1928
|
+
}
|
|
1929
|
+
out("\n Validating API key...");
|
|
1930
|
+
if (!await (opts.validateKey ?? validateApiKey)(apiKey, apiUrl)) {
|
|
1931
|
+
err("\n Error: Invalid API key or cannot reach the SystemDox API.\n Check your key and try again.\n");
|
|
1932
|
+
return { code: 1 };
|
|
1933
|
+
}
|
|
1934
|
+
out(" API key validated successfully.\n");
|
|
1935
|
+
const workspace = await keyWorkspace(opts, apiKey, apiUrl);
|
|
1936
|
+
if (!workspace) {
|
|
1937
|
+
err("\n Error: that is not a workspace name (lower case letters, digits and -). Nothing was changed.\n");
|
|
1938
|
+
return { code: 1 };
|
|
1939
|
+
}
|
|
1940
|
+
const detected = findConfigTargets(opts.cwd).filter((t) => t.detected);
|
|
1941
|
+
out(" Available targets:");
|
|
1942
|
+
detected.forEach((t, i) => out(` ${i + 1}. ${t.name} (${t.path})`));
|
|
1943
|
+
const choice = (await ask(`
|
|
1944
|
+
Which target? [1-${detected.length}, or 'all']: `)).trim();
|
|
1945
|
+
let selected;
|
|
1946
|
+
if (choice.toLowerCase() === "all") {
|
|
1947
|
+
selected = detected;
|
|
1948
|
+
} else {
|
|
1949
|
+
const idx = parseInt(choice, 10) - 1;
|
|
1950
|
+
if (isNaN(idx) || idx < 0 || idx >= detected.length) {
|
|
1951
|
+
err("\n Invalid selection.\n");
|
|
1952
|
+
return { code: 1 };
|
|
1953
|
+
}
|
|
1954
|
+
selected = [detected[idx]];
|
|
1955
|
+
}
|
|
1956
|
+
try {
|
|
1957
|
+
if ((await readCredentials())[workspace]?.apiKey !== apiKey) await writeCredentials(workspace, apiKey);
|
|
1958
|
+
const several = Object.keys(await readCredentials()).length > 1;
|
|
1959
|
+
for (const target of selected) {
|
|
1960
|
+
const removed = await writeServerEntry(target.path, apiUrl, several ? workspace : void 0);
|
|
1961
|
+
out(` Configured: ${target.name}`);
|
|
1962
|
+
if (removed) out(` The key now lives in ~/.claude/systemdox/credentials.json and was removed from ${target.path}.`);
|
|
1963
|
+
}
|
|
1964
|
+
} catch (e) {
|
|
1965
|
+
err(`
|
|
1966
|
+
Error: ${e.message}
|
|
1967
|
+
`);
|
|
1968
|
+
return { code: 1 };
|
|
1969
|
+
}
|
|
1970
|
+
out(
|
|
1971
|
+
`
|
|
1972
|
+
Your key for ${workspace} is in ${credentialsPath()} (readable by you alone), not in these configs.
|
|
1973
|
+
|
|
1974
|
+
Done! Restart your AI coding tool to activate the SystemDox MCP server.
|
|
1975
|
+
The server provides 25 tools: get_context, get_team_setup, get_checks,
|
|
1976
|
+
get_project_rules, get_prompt_templates, get_specs, get_docs,
|
|
1977
|
+
get_tech_stack, get_planning_context, get_plan_board, get_change,
|
|
1978
|
+
get_document_links, trace_document_path, get_graph_stats,
|
|
1979
|
+
create_check, create_project_rule, create_prompt_template,
|
|
1980
|
+
update_spec, report_decision, publish_skill, list_skills,
|
|
1981
|
+
install_skill, publish_setup_part, resolve_person, sync_memory.
|
|
1982
|
+
|
|
1983
|
+
get_guardrails and create_guardrail remain registered as deprecated
|
|
1984
|
+
aliases of get_checks and create_check \u2014 existing configs keep working.
|
|
1985
|
+
`
|
|
1986
|
+
);
|
|
1987
|
+
return { code: 0, apiKey, apiUrl, workspace };
|
|
1988
|
+
}
|
|
1989
|
+
var isReference;
|
|
1990
|
+
var init_setup_mcp = __esm({
|
|
1991
|
+
"src/setup-mcp.ts"() {
|
|
1992
|
+
"use strict";
|
|
1993
|
+
init_api_client();
|
|
1994
|
+
init_config();
|
|
1995
|
+
init_team_setup_files();
|
|
1996
|
+
init_setup_team();
|
|
1997
|
+
isReference = (value) => typeof value === "string" && /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value);
|
|
1998
|
+
}
|
|
1999
|
+
});
|
|
2000
|
+
|
|
1815
2001
|
// src/lib/skills.ts
|
|
1816
2002
|
import { createHash as createHash3 } from "crypto";
|
|
1817
2003
|
import { lstat, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
1818
|
-
import { existsSync as
|
|
2004
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1819
2005
|
import { homedir as homedir2 } from "os";
|
|
1820
|
-
import { dirname as dirname4, join as
|
|
2006
|
+
import { dirname as dirname4, join as join5, relative, resolve as resolve3, sep } from "path";
|
|
1821
2007
|
function sha2563(content) {
|
|
1822
2008
|
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
1823
2009
|
}
|
|
@@ -1825,7 +2011,7 @@ function isValidName(name) {
|
|
|
1825
2011
|
return name.length <= 64 && NAME_RE.test(name);
|
|
1826
2012
|
}
|
|
1827
2013
|
function isValidPath(path) {
|
|
1828
|
-
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== "..");
|
|
2014
|
+
return path.length > 0 && path.length <= 512 && !path.startsWith("/") && path.split("/").every((s) => SEGMENT_RE.test(s) && s !== "." && s !== ".." && s !== PLUGIN_MANIFEST_FOLDER);
|
|
1829
2015
|
}
|
|
1830
2016
|
function frontmatterName(skillMd) {
|
|
1831
2017
|
const block = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
@@ -1834,12 +2020,12 @@ function frontmatterName(skillMd) {
|
|
|
1834
2020
|
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
1835
2021
|
}
|
|
1836
2022
|
function defaultSkillDir(name) {
|
|
1837
|
-
return
|
|
2023
|
+
return join5(homedir2(), ".claude", "skills", name);
|
|
1838
2024
|
}
|
|
1839
2025
|
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
1840
2026
|
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
1841
2027
|
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
1842
|
-
const full =
|
|
2028
|
+
const full = join5(dir, entry.name);
|
|
1843
2029
|
const rel = relative(root, full).split(sep).join("/");
|
|
1844
2030
|
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
1845
2031
|
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
@@ -1860,7 +2046,7 @@ async function readTextFiles(root, paths, what) {
|
|
|
1860
2046
|
if (!isValidPath(path)) {
|
|
1861
2047
|
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
1862
2048
|
}
|
|
1863
|
-
const raw = await readFile4(
|
|
2049
|
+
const raw = await readFile4(join5(root, ...path.split("/")));
|
|
1864
2050
|
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
1865
2051
|
if (raw.length > MAX_FILE_BYTES) {
|
|
1866
2052
|
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
@@ -1881,7 +2067,7 @@ async function readTextFiles(root, paths, what) {
|
|
|
1881
2067
|
}
|
|
1882
2068
|
async function readSkillDirectory(directory) {
|
|
1883
2069
|
const root = resolve3(directory);
|
|
1884
|
-
if (!
|
|
2070
|
+
if (!existsSync4(join5(root, ENTRY_FILE))) {
|
|
1885
2071
|
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
1886
2072
|
}
|
|
1887
2073
|
const walked = await walk(root, { skipHidden: true });
|
|
@@ -1902,7 +2088,7 @@ function within(dest, path) {
|
|
|
1902
2088
|
}
|
|
1903
2089
|
async function readManifest(dest) {
|
|
1904
2090
|
try {
|
|
1905
|
-
return JSON.parse(await readFile4(
|
|
2091
|
+
return JSON.parse(await readFile4(join5(dest, MANIFEST_FILE), "utf8"));
|
|
1906
2092
|
} catch {
|
|
1907
2093
|
return void 0;
|
|
1908
2094
|
}
|
|
@@ -1911,24 +2097,24 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
1911
2097
|
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
1912
2098
|
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
1913
2099
|
for (const f of version.files) within(dest, f.path);
|
|
1914
|
-
if (
|
|
2100
|
+
if (existsSync4(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
1915
2101
|
throw new Error(`${dest} is a link: install into a real folder`);
|
|
1916
2102
|
}
|
|
1917
|
-
const walked =
|
|
2103
|
+
const walked = existsSync4(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
1918
2104
|
if (walked.links.length) {
|
|
1919
2105
|
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
1920
2106
|
}
|
|
1921
2107
|
const present = walked.files;
|
|
1922
2108
|
const manifest = await readManifest(dest);
|
|
1923
|
-
const
|
|
2109
|
+
const known2 = Object.fromEntries(
|
|
1924
2110
|
Object.entries(manifest?.files ?? {}).filter(([p, h]) => isValidPath(p) && typeof h === "string")
|
|
1925
2111
|
);
|
|
1926
2112
|
const edited = [];
|
|
1927
2113
|
const untracked = [];
|
|
1928
2114
|
for (const path of present) {
|
|
1929
|
-
const content = await readFile4(
|
|
1930
|
-
if (Object.hasOwn(
|
|
1931
|
-
if (sha2563(content) !==
|
|
2115
|
+
const content = await readFile4(join5(dest, ...path.split("/")), "utf8");
|
|
2116
|
+
if (Object.hasOwn(known2, path)) {
|
|
2117
|
+
if (sha2563(content) !== known2[path]) edited.push(path);
|
|
1932
2118
|
} else if (!incoming.has(path) || sha2563(content) !== incoming.get(path).sha256) {
|
|
1933
2119
|
if (incoming.has(path) || !manifest) edited.push(path);
|
|
1934
2120
|
else untracked.push(path);
|
|
@@ -1945,7 +2131,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
1945
2131
|
}
|
|
1946
2132
|
const removed = [];
|
|
1947
2133
|
const keptEdited = [];
|
|
1948
|
-
for (const path of Object.keys(
|
|
2134
|
+
for (const path of Object.keys(known2)) {
|
|
1949
2135
|
if (incoming.has(path) || !present.includes(path)) continue;
|
|
1950
2136
|
if (edited.includes(path)) {
|
|
1951
2137
|
keptEdited.push(path);
|
|
@@ -1960,7 +2146,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
1960
2146
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1961
2147
|
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
1962
2148
|
};
|
|
1963
|
-
await writeFile4(
|
|
2149
|
+
await writeFile4(join5(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
1964
2150
|
return {
|
|
1965
2151
|
dest,
|
|
1966
2152
|
version: version.version,
|
|
@@ -1986,12 +2172,13 @@ function describeInstall(name, r) {
|
|
|
1986
2172
|
lines.push("Start a new Claude Code session to load it.");
|
|
1987
2173
|
return lines.join("\n");
|
|
1988
2174
|
}
|
|
1989
|
-
var MANIFEST_FILE, ENTRY_FILE, MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, NAME_RE, SEGMENT_RE, SKIPPED, SkillInstallRefused;
|
|
2175
|
+
var MANIFEST_FILE, ENTRY_FILE, PLUGIN_MANIFEST_FOLDER, MAX_FILES, MAX_FILE_BYTES, MAX_TOTAL_BYTES, NAME_RE, SEGMENT_RE, SKIPPED, SkillInstallRefused;
|
|
1990
2176
|
var init_skills = __esm({
|
|
1991
2177
|
"src/lib/skills.ts"() {
|
|
1992
2178
|
"use strict";
|
|
1993
2179
|
MANIFEST_FILE = ".systemdox-skill.json";
|
|
1994
2180
|
ENTRY_FILE = "SKILL.md";
|
|
2181
|
+
PLUGIN_MANIFEST_FOLDER = ".claude-plugin";
|
|
1995
2182
|
MAX_FILES = 100;
|
|
1996
2183
|
MAX_FILE_BYTES = 256 * 1024;
|
|
1997
2184
|
MAX_TOTAL_BYTES = 1024 * 1024;
|
|
@@ -2108,9 +2295,9 @@ __export(plugin_path_exports, {
|
|
|
2108
2295
|
unpackZip: () => unpackZip
|
|
2109
2296
|
});
|
|
2110
2297
|
import { createHash as createHash4, randomBytes } from "crypto";
|
|
2111
|
-
import { existsSync as
|
|
2298
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2112
2299
|
import { mkdir as mkdir5, rename as rename2, writeFile as writeFile5 } from "fs/promises";
|
|
2113
|
-
import { dirname as dirname5, join as
|
|
2300
|
+
import { dirname as dirname5, join as join6, resolve as resolve5, sep as sep2 } from "path";
|
|
2114
2301
|
import { inflateRawSync } from "zlib";
|
|
2115
2302
|
function parsePluginPathArgs(argv) {
|
|
2116
2303
|
const args2 = {};
|
|
@@ -2230,7 +2417,7 @@ async function runPluginPath(argv, io) {
|
|
|
2230
2417
|
const folder = pluginDir(workspace, version);
|
|
2231
2418
|
try {
|
|
2232
2419
|
const state = await readState(workspace);
|
|
2233
|
-
if (
|
|
2420
|
+
if (existsSync5(folder) && state.plugin?.[String(version)]?.sha256 === sha2564) {
|
|
2234
2421
|
io.out(folder);
|
|
2235
2422
|
return 0;
|
|
2236
2423
|
}
|
|
@@ -2269,17 +2456,17 @@ async function runPluginPath(argv, io) {
|
|
|
2269
2456
|
);
|
|
2270
2457
|
return 2;
|
|
2271
2458
|
}
|
|
2272
|
-
const tmp =
|
|
2459
|
+
const tmp = join6(pluginRoot(workspace), `.${version}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
2273
2460
|
try {
|
|
2274
2461
|
await unpackZip(zip, tmp);
|
|
2275
|
-
if (
|
|
2462
|
+
if (existsSync5(folder) && (await readState(workspace)).plugin?.[String(version)]?.sha256 === sha2564) {
|
|
2276
2463
|
await removeFolder(tmp);
|
|
2277
2464
|
} else {
|
|
2278
|
-
if (
|
|
2465
|
+
if (existsSync5(folder)) await removeFolder(folder);
|
|
2279
2466
|
try {
|
|
2280
2467
|
await rename2(tmp, folder);
|
|
2281
2468
|
} catch (e) {
|
|
2282
|
-
if (!
|
|
2469
|
+
if (!existsSync5(folder)) throw e;
|
|
2283
2470
|
await removeFolder(tmp);
|
|
2284
2471
|
}
|
|
2285
2472
|
}
|
|
@@ -2316,6 +2503,148 @@ var init_plugin_path = __esm({
|
|
|
2316
2503
|
}
|
|
2317
2504
|
});
|
|
2318
2505
|
|
|
2506
|
+
// src/lib/session-brief.ts
|
|
2507
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2508
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
2509
|
+
import { join as join7 } from "path";
|
|
2510
|
+
function noKey(reason) {
|
|
2511
|
+
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.`;
|
|
2512
|
+
}
|
|
2513
|
+
function keyReason(message) {
|
|
2514
|
+
if (/holds keys for/.test(message)) return "keys for several workspaces are stored; set SYSTEMDOX_WORKSPACE to the one to use";
|
|
2515
|
+
if (/holds no key for/.test(message)) return "SYSTEMDOX_WORKSPACE names a workspace with no stored key";
|
|
2516
|
+
if (/No SystemDox key/.test(message)) return "none is stored";
|
|
2517
|
+
return "the stored keys could not be read";
|
|
2518
|
+
}
|
|
2519
|
+
function olderApi(setup) {
|
|
2520
|
+
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.`;
|
|
2521
|
+
}
|
|
2522
|
+
async function readJsonQuietly2(path) {
|
|
2523
|
+
try {
|
|
2524
|
+
return JSON.parse(await readFile5(path, "utf8"));
|
|
2525
|
+
} catch {
|
|
2526
|
+
return void 0;
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
async function computerState(setup) {
|
|
2530
|
+
const id = `${setup.workspace}-setup@systemdox-${setup.workspace}`;
|
|
2531
|
+
const installed = await readJsonQuietly2(join7(homeDir(), ".claude", "plugins", "installed_plugins.json"));
|
|
2532
|
+
const entries2 = installed?.plugins?.[id];
|
|
2533
|
+
const entry = Array.isArray(entries2) ? entries2.find((e) => (e?.scope ?? "user") === "user") : void 0;
|
|
2534
|
+
const state = {
|
|
2535
|
+
missing: [],
|
|
2536
|
+
behind: [],
|
|
2537
|
+
notFromSystemDox: [],
|
|
2538
|
+
pluginInstalled: !!entry,
|
|
2539
|
+
pluginVersion: entry?.version
|
|
2540
|
+
};
|
|
2541
|
+
const pluginCurrent = !!entry && !!setup.plugin && entry.version === `${setup.plugin.version}.0.0`;
|
|
2542
|
+
for (const skill of setup.sessionBrief?.skills ?? []) {
|
|
2543
|
+
const dir = defaultSkillDir(skill.name);
|
|
2544
|
+
if (!existsSync6(dir)) {
|
|
2545
|
+
if (!(pluginCurrent && setup.plugin?.skills?.[skill.name] !== void 0)) state.missing.push(skill.name);
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
2548
|
+
const manifest = await readJsonQuietly2(join7(dir, MANIFEST_FILE));
|
|
2549
|
+
if (typeof manifest?.version !== "number" || !Number.isInteger(manifest.version)) {
|
|
2550
|
+
state.notFromSystemDox.push(skill.name);
|
|
2551
|
+
} else if (manifest.version !== skill.latestVersion) {
|
|
2552
|
+
state.behind.push({ name: skill.name, here: manifest.version, latest: skill.latestVersion });
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
return state;
|
|
2556
|
+
}
|
|
2557
|
+
function hasPersonParts(setup) {
|
|
2558
|
+
return Object.keys(setup.parts ?? {}).length > 0 || (setup.memory ?? []).length > 0 || (setup.sessionBrief?.heldSkills ?? []).some((h) => h.reason === "not-plain");
|
|
2559
|
+
}
|
|
2560
|
+
function listWithin(items, room) {
|
|
2561
|
+
const whole2 = items.join(", ");
|
|
2562
|
+
if (whole2.length <= room) return whole2;
|
|
2563
|
+
const kept = [];
|
|
2564
|
+
let used = 0;
|
|
2565
|
+
for (let i = 0; i < items.length; i++) {
|
|
2566
|
+
const rest = items.length - i - 1;
|
|
2567
|
+
const cost = items[i].length + (kept.length ? 2 : 0);
|
|
2568
|
+
if (used + cost + ` and ${MORE(rest)}`.length > room) break;
|
|
2569
|
+
kept.push(items[i]);
|
|
2570
|
+
used += cost;
|
|
2571
|
+
}
|
|
2572
|
+
return kept.length ? `${kept.join(", ")} and ${MORE(items.length - kept.length)}` : `all ${MORE(items.length)}`;
|
|
2573
|
+
}
|
|
2574
|
+
function localSection(setup, state, room) {
|
|
2575
|
+
const rest = hasPersonParts(setup) ? ` and that \`${SETUP_COMMAND}\` in their own terminal adds the rest of the team setup` : "";
|
|
2576
|
+
const latest = new Map((setup.sessionBrief?.skills ?? []).map((s) => [s.name, s.latestVersion]));
|
|
2577
|
+
const lines = [];
|
|
2578
|
+
if (state.missing.length) {
|
|
2579
|
+
lines.push({
|
|
2580
|
+
head: "On this computer, missing: ",
|
|
2581
|
+
items: state.missing.map((name) => `${name} version ${latest.get(name)}`),
|
|
2582
|
+
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.`
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
if (state.behind.length) {
|
|
2586
|
+
lines.push({
|
|
2587
|
+
head: "On this computer, behind: ",
|
|
2588
|
+
items: state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`),
|
|
2589
|
+
tail: ". install_skill updates each to that latest version; if it refuses because files were edited, tell the person and leave them."
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
if (state.notFromSystemDox.length) {
|
|
2593
|
+
lines.push({ head: "Not from SystemDox, left alone: ", items: state.notFromSystemDox, tail: "." });
|
|
2594
|
+
}
|
|
2595
|
+
const fixed = () => lines.reduce((n, l) => n + l.head.length + l.tail.length + LIST_MIN, 0) + Math.max(0, lines.length - 1);
|
|
2596
|
+
while (lines.length && fixed() > room) lines.pop();
|
|
2597
|
+
if (!lines.length) return "";
|
|
2598
|
+
const share = Math.floor((room - fixed()) / lines.length) + LIST_MIN;
|
|
2599
|
+
return lines.map((l) => `${l.head}${listWithin(l.items, share)}${l.tail}`).join("\n");
|
|
2600
|
+
}
|
|
2601
|
+
function fit(text3, section) {
|
|
2602
|
+
const whole2 = section ? `${text3}
|
|
2603
|
+
|
|
2604
|
+
${section}` : text3;
|
|
2605
|
+
if (whole2.length <= INSTRUCTIONS_MAX) return whole2;
|
|
2606
|
+
return text3.length <= INSTRUCTIONS_MAX ? text3 : `${text3.slice(0, INSTRUCTIONS_MAX - 1)}\u2026`;
|
|
2607
|
+
}
|
|
2608
|
+
async function readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs = BRIEF_TIMEOUT_MS } = {}) {
|
|
2609
|
+
if (keyProblem2) return { problem: noKey(keyProblem2) };
|
|
2610
|
+
try {
|
|
2611
|
+
const setup = await apiClient2.getOwnTeamSetup(AbortSignal.timeout(timeoutMs));
|
|
2612
|
+
return typeof setup?.workspace === "string" ? { setup } : { problem: NOT_CHECKED };
|
|
2613
|
+
} catch (err) {
|
|
2614
|
+
const status = err?.status;
|
|
2615
|
+
return { problem: status === 401 || status === 403 ? REFUSED : NOT_CHECKED };
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
async function sessionInstructions(apiClient2, options = {}) {
|
|
2619
|
+
const read = await readTeamSetup(apiClient2, options);
|
|
2620
|
+
if ("problem" in read) return read.problem;
|
|
2621
|
+
const text3 = read.setup.sessionBrief?.text ?? olderApi(read.setup);
|
|
2622
|
+
try {
|
|
2623
|
+
if (!options.localFiles) return fit(text3, read.setup.sessionBrief?.skills?.length ? CANNOT_SEE : "");
|
|
2624
|
+
const room = INSTRUCTIONS_MAX - text3.length - 2;
|
|
2625
|
+
return fit(text3, localSection(read.setup, await computerState(read.setup), room));
|
|
2626
|
+
} catch {
|
|
2627
|
+
return fit(text3, "");
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
var INSTRUCTIONS_MAX, BRIEF_TIMEOUT_MS, NO_KEY, SETUP_COMMAND, REFUSED, NOT_CHECKED, CANNOT_SEE, MORE, LIST_MIN;
|
|
2631
|
+
var init_session_brief = __esm({
|
|
2632
|
+
"src/lib/session-brief.ts"() {
|
|
2633
|
+
"use strict";
|
|
2634
|
+
init_skills();
|
|
2635
|
+
init_team_setup_files();
|
|
2636
|
+
INSTRUCTIONS_MAX = 2e3;
|
|
2637
|
+
BRIEF_TIMEOUT_MS = 8e3;
|
|
2638
|
+
NO_KEY = "no-key-on-this-computer";
|
|
2639
|
+
SETUP_COMMAND = "npx -y @systemdox/mcp-server setup";
|
|
2640
|
+
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.`;
|
|
2641
|
+
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.";
|
|
2642
|
+
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.";
|
|
2643
|
+
MORE = (n) => `${n} more (get_team_setup lists them)`;
|
|
2644
|
+
LIST_MIN = `all ${MORE(100)}`.length;
|
|
2645
|
+
}
|
|
2646
|
+
});
|
|
2647
|
+
|
|
2319
2648
|
// src/lib/language.ts
|
|
2320
2649
|
function detectLanguage(filePath) {
|
|
2321
2650
|
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
@@ -2428,7 +2757,7 @@ var init_config_links = __esm({
|
|
|
2428
2757
|
|
|
2429
2758
|
// src/tools/get-guardrails.ts
|
|
2430
2759
|
import { z } from "zod";
|
|
2431
|
-
function registerGetGuardrails(
|
|
2760
|
+
function registerGetGuardrails(server, apiClient2) {
|
|
2432
2761
|
const handler = async ({
|
|
2433
2762
|
language,
|
|
2434
2763
|
category,
|
|
@@ -2482,7 +2811,7 @@ ${text3}`
|
|
|
2482
2811
|
]
|
|
2483
2812
|
};
|
|
2484
2813
|
};
|
|
2485
|
-
|
|
2814
|
+
server.registerTool(
|
|
2486
2815
|
"get_checks",
|
|
2487
2816
|
{
|
|
2488
2817
|
title: "Get Checks",
|
|
@@ -2491,7 +2820,7 @@ ${text3}`
|
|
|
2491
2820
|
},
|
|
2492
2821
|
handler
|
|
2493
2822
|
);
|
|
2494
|
-
|
|
2823
|
+
server.registerTool(
|
|
2495
2824
|
"get_guardrails",
|
|
2496
2825
|
{
|
|
2497
2826
|
title: "Get Checks (legacy name)",
|
|
@@ -2525,8 +2854,8 @@ var init_get_guardrails = __esm({
|
|
|
2525
2854
|
|
|
2526
2855
|
// src/tools/get-specs.ts
|
|
2527
2856
|
import { z as z2 } from "zod";
|
|
2528
|
-
function registerGetSpecs(
|
|
2529
|
-
|
|
2857
|
+
function registerGetSpecs(server, apiClient2) {
|
|
2858
|
+
server.registerTool(
|
|
2530
2859
|
"get_specs",
|
|
2531
2860
|
{
|
|
2532
2861
|
title: "Get Specs",
|
|
@@ -2616,8 +2945,8 @@ function matchGlob(path, pattern) {
|
|
|
2616
2945
|
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
2617
2946
|
return new RegExp(`^${regex}$`).test(path);
|
|
2618
2947
|
}
|
|
2619
|
-
function registerGetDocs(
|
|
2620
|
-
|
|
2948
|
+
function registerGetDocs(server, apiClient2) {
|
|
2949
|
+
server.registerTool(
|
|
2621
2950
|
"get_docs",
|
|
2622
2951
|
{
|
|
2623
2952
|
title: "Get Docs",
|
|
@@ -2720,8 +3049,8 @@ var init_get_docs = __esm({
|
|
|
2720
3049
|
});
|
|
2721
3050
|
|
|
2722
3051
|
// src/tools/get-tech-stack.ts
|
|
2723
|
-
function registerGetTechStack(
|
|
2724
|
-
|
|
3052
|
+
function registerGetTechStack(server, apiClient2) {
|
|
3053
|
+
server.registerTool(
|
|
2725
3054
|
"get_tech_stack",
|
|
2726
3055
|
{
|
|
2727
3056
|
title: "Get Tech Stack",
|
|
@@ -2776,8 +3105,8 @@ var init_get_tech_stack = __esm({
|
|
|
2776
3105
|
|
|
2777
3106
|
// src/tools/get-planning-context.ts
|
|
2778
3107
|
import { z as z4 } from "zod";
|
|
2779
|
-
function registerGetPlanningContext(
|
|
2780
|
-
|
|
3108
|
+
function registerGetPlanningContext(server, apiClient2) {
|
|
3109
|
+
server.registerTool(
|
|
2781
3110
|
"get_planning_context",
|
|
2782
3111
|
{
|
|
2783
3112
|
title: "Get Planning Context",
|
|
@@ -2811,7 +3140,7 @@ ${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
|
|
|
2811
3140
|
${repos}`);
|
|
2812
3141
|
}
|
|
2813
3142
|
if (ctx.dependencies.length > 0) {
|
|
2814
|
-
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
|
|
3143
|
+
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}${d.provenance === "inferred" ? " (inferred)" : ""}`).join("\n");
|
|
2815
3144
|
sections.push(`## Cross-Repo Dependencies
|
|
2816
3145
|
|
|
2817
3146
|
${deps}`);
|
|
@@ -2891,8 +3220,8 @@ function formatItem(item2) {
|
|
|
2891
3220
|
${meta.join(" \xB7 ")}` : "") + (item2.url ? `
|
|
2892
3221
|
${item2.url}` : "");
|
|
2893
3222
|
}
|
|
2894
|
-
function registerGetPlanBoard(
|
|
2895
|
-
|
|
3223
|
+
function registerGetPlanBoard(server, apiClient2) {
|
|
3224
|
+
server.registerTool(
|
|
2896
3225
|
"get_plan_board",
|
|
2897
3226
|
{
|
|
2898
3227
|
title: "Get Plan Board",
|
|
@@ -3048,6 +3377,153 @@ var init_get_plan_board = __esm({
|
|
|
3048
3377
|
}
|
|
3049
3378
|
});
|
|
3050
3379
|
|
|
3380
|
+
// src/tools/get-change.ts
|
|
3381
|
+
import { z as z6 } from "zod";
|
|
3382
|
+
function briefVerdict(states) {
|
|
3383
|
+
const waiting = BRIEF.filter(
|
|
3384
|
+
(part) => !(part === "decision" && states.decision === null) && !PAST_REVIEW.has(String(states[part]))
|
|
3385
|
+
).map((part) => `${part} is ${states[part] ?? "unknown"}`);
|
|
3386
|
+
return waiting.length === 0 ? "The brief is passed" : `The brief is not passed: ${waiting.join(", ")}`;
|
|
3387
|
+
}
|
|
3388
|
+
function prLink(pr) {
|
|
3389
|
+
return `[${pr.repo}#${pr.number}](${pr.url}) ${pr.state}`;
|
|
3390
|
+
}
|
|
3391
|
+
function fileSource(file) {
|
|
3392
|
+
if (file.pullRequest) return prLink(file.pullRequest);
|
|
3393
|
+
return file.direct ? "committed directly" : "\u2014";
|
|
3394
|
+
}
|
|
3395
|
+
function partPullRequests(change, part) {
|
|
3396
|
+
const files = part === "contract" ? change.contract.files : part === "behaviour" ? change.behaviour.files : part === "plan" ? [change.plan.file] : [];
|
|
3397
|
+
const prs = part === "decision" ? [change.decision?.pullRequest ?? null] : part === "release" ? change.release.prs : files.map((file) => file?.pullRequest ?? null);
|
|
3398
|
+
const unique = /* @__PURE__ */ new Map();
|
|
3399
|
+
for (const pr of prs) {
|
|
3400
|
+
if (pr && !unique.has(`${pr.repo}#${pr.number}`)) unique.set(`${pr.repo}#${pr.number}`, pr);
|
|
3401
|
+
}
|
|
3402
|
+
return [...unique.values()].sort((a, b) => Number(b.state === "open") - Number(a.state === "open"));
|
|
3403
|
+
}
|
|
3404
|
+
function countsLine(counts) {
|
|
3405
|
+
const tags = ["proposed", "built", "defect", "unbuilt", "open", "untagged"].filter((tag) => counts[tag] > 0).map((tag) => `${counts[tag]} ${tag}`);
|
|
3406
|
+
return `${counts.total} scenario(s)` + (tags.length > 0 ? `: ${tags.join(", ")}` : "");
|
|
3407
|
+
}
|
|
3408
|
+
function partsTable(change) {
|
|
3409
|
+
const rows = PARTS.filter((part) => !(part === "decision" && change.states.decision === null)).map((part) => {
|
|
3410
|
+
const prs = partPullRequests(change, part);
|
|
3411
|
+
return `| ${part} | ${change.states[part]} | ${prs.length > 0 ? prs.map(prLink).join(", ") : "\u2014"} |`;
|
|
3412
|
+
});
|
|
3413
|
+
return ["| Part | State | Pull request |", "|---|---|---|", ...rows].join("\n");
|
|
3414
|
+
}
|
|
3415
|
+
function decisionSection(change) {
|
|
3416
|
+
const decision = change.decision;
|
|
3417
|
+
if (decision === null) return "## Decision\n\nNone: the proposal names no ADR (a small change).";
|
|
3418
|
+
const status = decision.status || "no record in a connected repository";
|
|
3419
|
+
return `## Decision
|
|
3420
|
+
|
|
3421
|
+
${decision.adr} \xB7 ${status}${decision.title ? ` \u2014 ${decision.title}` : ""}`;
|
|
3422
|
+
}
|
|
3423
|
+
function contractSection(change) {
|
|
3424
|
+
const files = change.contract.files;
|
|
3425
|
+
return "## Contract\n\n" + (files.length > 0 ? files.map((file) => `- ${file.path} \u2014 ${fileSource(file)}`).join("\n") : "No contract file yet (openapi.yaml, asyncapi.yaml or contract.md).");
|
|
3426
|
+
}
|
|
3427
|
+
function behaviourSection(change) {
|
|
3428
|
+
const { files, counts } = change.behaviour;
|
|
3429
|
+
if (files.length === 0) {
|
|
3430
|
+
return `## Behaviour
|
|
3431
|
+
|
|
3432
|
+
No feature file names this change. A feature file joins it when its path or its first ten lines name \`${change.slug}\`${change.decision ? ` or ${change.decision.adr}` : ""}.`;
|
|
3433
|
+
}
|
|
3434
|
+
const blocks = files.map(
|
|
3435
|
+
(file) => [
|
|
3436
|
+
`### ${file.path}`,
|
|
3437
|
+
"",
|
|
3438
|
+
[file.title, file.runner ? `runner ${file.runner}` : "", fileSource(file), countsLine(file.counts)].filter(Boolean).join(" \xB7 "),
|
|
3439
|
+
"",
|
|
3440
|
+
...file.scenarios.map((s) => `- ${s.id || "(no id)"} \xB7 ${s.state || "untagged"} \xB7 ${s.title}`)
|
|
3441
|
+
].join("\n")
|
|
3442
|
+
);
|
|
3443
|
+
return ["## Behaviour", countsLine(counts), ...blocks].join("\n\n");
|
|
3444
|
+
}
|
|
3445
|
+
function planSection(change) {
|
|
3446
|
+
const { file, draftId, board } = change.plan;
|
|
3447
|
+
const boardLine = board ? `[#${board.number} ${board.title}](${board.url}) \u2014 todo ${board.items.todo} \xB7 in progress ${board.items.inProgress} \xB7 done ${board.items.done}` + (board.items.truncated ? " (counted over its first 500 items)" : "") : "none";
|
|
3448
|
+
return [
|
|
3449
|
+
"## Plan",
|
|
3450
|
+
"",
|
|
3451
|
+
`- plan.md: ${file ? `${file.path} \u2014 ${fileSource(file)}` : "none"}`,
|
|
3452
|
+
`- Draft: ${draftId ?? "none"}`,
|
|
3453
|
+
`- Board: ${boardLine}`
|
|
3454
|
+
].join("\n");
|
|
3455
|
+
}
|
|
3456
|
+
function releaseSection(change) {
|
|
3457
|
+
const lines = change.release.prs.map(
|
|
3458
|
+
(pr) => `- ${prLink(pr)} \xB7 ${pr.title} \xB7 ` + (pr.approvingReview ? `approved by ${pr.approvingReview.by} at ${pr.approvingReview.at}` : "no approving review on its head yet")
|
|
3459
|
+
);
|
|
3460
|
+
return `## Release
|
|
3461
|
+
|
|
3462
|
+
${lines.length > 0 ? lines.join("\n") : "No release pull request is open."}`;
|
|
3463
|
+
}
|
|
3464
|
+
function formatChange(change) {
|
|
3465
|
+
const header = `# ${change.title}
|
|
3466
|
+
|
|
3467
|
+
\`${change.key}\` \xB7 size: ${change.size}` + (change.archived ? " \xB7 archived: read-only, done" : "") + ` \xB7 synced ${change.syncedAt}`;
|
|
3468
|
+
return [
|
|
3469
|
+
header,
|
|
3470
|
+
partsTable(change),
|
|
3471
|
+
decisionSection(change),
|
|
3472
|
+
contractSection(change),
|
|
3473
|
+
behaviourSection(change),
|
|
3474
|
+
planSection(change),
|
|
3475
|
+
releaseSection(change),
|
|
3476
|
+
// Compact: a change's every scenario with its steps rides in this block.
|
|
3477
|
+
"```json\n" + JSON.stringify(change) + "\n```",
|
|
3478
|
+
briefVerdict(change.states)
|
|
3479
|
+
].join("\n\n");
|
|
3480
|
+
}
|
|
3481
|
+
function registerGetChange(server, apiClient2) {
|
|
3482
|
+
server.registerTool(
|
|
3483
|
+
"get_change",
|
|
3484
|
+
{
|
|
3485
|
+
title: "Get Change",
|
|
3486
|
+
description: `Read one change (a specs/changes/<slug>/ folder of a connected repository) as SystemDox last synced it from GitHub: the state of each part of its brief (decision, contract, behaviour, plan) and of its release, each with its pull request; the decision's ADR and status; the contract files; every feature file that names the change, with its scenarios as id \xB7 state \xB7 title; the plan (plan.md, the draft, the board); the release pull requests; and the Change object as JSON. The last line says "The brief is passed" or "The brief is not passed: <part> is <state>". Call it before building a story, and stop if the brief is not passed or the story's scenarios are not proposed. Read-only.`,
|
|
3487
|
+
inputSchema: {
|
|
3488
|
+
repo: z6.string().regex(REPO_RE, "the repository as owner/name, e.g. acme/booking-api").describe("The repository that holds the change, as owner/name"),
|
|
3489
|
+
slug: z6.string().max(SLUG_MAX).regex(SLUG_RE2, "a change folder's name: lower-case words joined by -, e.g. customer-reminders").describe("The change's folder name: specs/changes/<slug>/")
|
|
3490
|
+
},
|
|
3491
|
+
annotations: { readOnlyHint: true }
|
|
3492
|
+
},
|
|
3493
|
+
async ({ repo, slug }) => {
|
|
3494
|
+
const [owner, name] = repo.split("/");
|
|
3495
|
+
let change;
|
|
3496
|
+
try {
|
|
3497
|
+
change = await apiClient2.getFresh(
|
|
3498
|
+
`/v1/changes/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/${encodeURIComponent(slug)}`
|
|
3499
|
+
);
|
|
3500
|
+
} catch (err) {
|
|
3501
|
+
if (err.status === 404) {
|
|
3502
|
+
return {
|
|
3503
|
+
content: [{ type: "text", text: `No such change ${repo}/${slug}` }],
|
|
3504
|
+
isError: true
|
|
3505
|
+
};
|
|
3506
|
+
}
|
|
3507
|
+
throw err;
|
|
3508
|
+
}
|
|
3509
|
+
return { content: [{ type: "text", text: formatChange(change) }] };
|
|
3510
|
+
}
|
|
3511
|
+
);
|
|
3512
|
+
}
|
|
3513
|
+
var GITHUB_NAME, REPO_RE, SLUG_RE2, SLUG_MAX, PARTS, BRIEF, PAST_REVIEW;
|
|
3514
|
+
var init_get_change = __esm({
|
|
3515
|
+
"src/tools/get-change.ts"() {
|
|
3516
|
+
"use strict";
|
|
3517
|
+
GITHUB_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,99}";
|
|
3518
|
+
REPO_RE = new RegExp(`^${GITHUB_NAME}/${GITHUB_NAME}$`);
|
|
3519
|
+
SLUG_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
3520
|
+
SLUG_MAX = 64;
|
|
3521
|
+
PARTS = ["decision", "contract", "behaviour", "plan", "release"];
|
|
3522
|
+
BRIEF = ["decision", "contract", "behaviour", "plan"];
|
|
3523
|
+
PAST_REVIEW = /* @__PURE__ */ new Set(["passed", "being built", "done"]);
|
|
3524
|
+
}
|
|
3525
|
+
});
|
|
3526
|
+
|
|
3051
3527
|
// src/lib/scoring.ts
|
|
3052
3528
|
function estimateTokens(text3) {
|
|
3053
3529
|
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
@@ -3357,16 +3833,22 @@ async function hydrate(api, item2, meta) {
|
|
|
3357
3833
|
}
|
|
3358
3834
|
async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
|
|
3359
3835
|
const budget = maxTokens ?? DEFAULT_BUDGET;
|
|
3836
|
+
const failures = [];
|
|
3837
|
+
const orEmpty = (empty) => (err) => {
|
|
3838
|
+
failures.push(err);
|
|
3839
|
+
return empty;
|
|
3840
|
+
};
|
|
3360
3841
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
3361
|
-
api.get("/v1/guardrails").catch((
|
|
3362
|
-
api.get("/v1/specs").catch((
|
|
3363
|
-
api.get("/v1/docs").catch((
|
|
3364
|
-
api.get("/v1/ai/settings/stack").catch((
|
|
3365
|
-
api.get("/v1/standards/rules").catch((
|
|
3842
|
+
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
3843
|
+
api.get("/v1/specs").catch(orEmpty([])),
|
|
3844
|
+
api.get("/v1/docs").catch(orEmpty({ items: [] })),
|
|
3845
|
+
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
3846
|
+
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
3366
3847
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
3367
3848
|
// blip) gates nothing, it can never hide working context.
|
|
3368
3849
|
api.get("/v1/doc-repos").catch(() => null)
|
|
3369
3850
|
]);
|
|
3851
|
+
if (failures.length === 5) throw failures[0];
|
|
3370
3852
|
const disabled = (key2) => docRepos?.capabilities?.[key2]?.status === "disabled";
|
|
3371
3853
|
const guardrails = disabled("fitness") ? [] : rawGuardrails;
|
|
3372
3854
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
@@ -3573,12 +4055,13 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
|
|
|
3573
4055
|
guardrails: selectedGuardrails.length
|
|
3574
4056
|
};
|
|
3575
4057
|
if (sections.length === 0) {
|
|
3576
|
-
|
|
4058
|
+
const text3 = failures.length ? `No context found for this task, but ${failures.length} of 5 reads failed, so this is not an all-clear: the team's rules may not have been read.` : "No relevant context found for this task.";
|
|
4059
|
+
return { text: text3, stats };
|
|
3577
4060
|
}
|
|
3578
4061
|
const footer = `
|
|
3579
4062
|
|
|
3580
4063
|
---
|
|
3581
|
-
_Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
|
|
4064
|
+
_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` : "") + `_`;
|
|
3582
4065
|
return { text: body + footer, stats };
|
|
3583
4066
|
}
|
|
3584
4067
|
var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS, SECTION_STOP_WORDS;
|
|
@@ -3621,21 +4104,21 @@ var init_context_builder = __esm({
|
|
|
3621
4104
|
});
|
|
3622
4105
|
|
|
3623
4106
|
// src/tools/get-context.ts
|
|
3624
|
-
import { z as
|
|
3625
|
-
function registerGetContext(
|
|
3626
|
-
|
|
4107
|
+
import { z as z7 } from "zod";
|
|
4108
|
+
function registerGetContext(server, apiClient2) {
|
|
4109
|
+
server.registerTool(
|
|
3627
4110
|
"get_context",
|
|
3628
4111
|
{
|
|
3629
4112
|
title: "Get Context",
|
|
3630
4113
|
description: "Intelligent context aggregator: fetches checks, specs, docs, and tech stack, ranks them by relevance to your current task, and returns the full text of the highest-ranked items within a token budget \u2014 each with the ID needed to fetch the rest. Use this instead of calling individual tools when you want curated, relevant context.",
|
|
3631
4114
|
inputSchema: {
|
|
3632
|
-
task_description:
|
|
4115
|
+
task_description: z7.string().describe(
|
|
3633
4116
|
"What you're working on (e.g., 'add a new REST endpoint for user preferences')"
|
|
3634
4117
|
),
|
|
3635
|
-
file_paths:
|
|
4118
|
+
file_paths: z7.array(z7.string()).optional().describe(
|
|
3636
4119
|
"File paths being edited \u2014 used for language/category detection"
|
|
3637
4120
|
),
|
|
3638
|
-
max_tokens:
|
|
4121
|
+
max_tokens: z7.number().optional().describe("Maximum token budget for the response (default: 4000)")
|
|
3639
4122
|
}
|
|
3640
4123
|
},
|
|
3641
4124
|
async ({ task_description, file_paths, max_tokens }) => {
|
|
@@ -3657,9 +4140,254 @@ var init_get_context = __esm({
|
|
|
3657
4140
|
}
|
|
3658
4141
|
});
|
|
3659
4142
|
|
|
4143
|
+
// src/tools/get-team-setup.ts
|
|
4144
|
+
function describeSetup(setup) {
|
|
4145
|
+
const lines = [`# Team setup of ${setup.workspace}`, ""];
|
|
4146
|
+
const skills = setup.sessionBrief?.skills ?? [];
|
|
4147
|
+
const held = setup.sessionBrief?.heldSkills ?? [];
|
|
4148
|
+
lines.push(`## Skills a session installs by itself (${skills.length})`, "");
|
|
4149
|
+
if (skills.length) {
|
|
4150
|
+
for (const s of skills) lines.push(`- **${s.name}** version ${s.latestVersion}: ${s.description}`);
|
|
4151
|
+
} else if (!setup.sessionBrief && setup.skills.length) {
|
|
4152
|
+
lines.push(`- ${setup.skills.join(", ")}`);
|
|
4153
|
+
} else {
|
|
4154
|
+
lines.push("None.");
|
|
4155
|
+
}
|
|
4156
|
+
lines.push("");
|
|
4157
|
+
if (held.length) {
|
|
4158
|
+
lines.push(`## Skills that wait for the person (${held.length})`, "");
|
|
4159
|
+
for (const h of held) lines.push(`- **${h.name}**: ${HELD_BECAUSE[h.reason] ?? h.reason}`);
|
|
4160
|
+
lines.push("", "Do not install these yourself: each is the person's to decide.", "");
|
|
4161
|
+
}
|
|
4162
|
+
if (setup.plugin) {
|
|
4163
|
+
lines.push(`## Team plugin`, "", `\`${setup.workspace}-setup\` version ${setup.plugin.version}.`);
|
|
4164
|
+
for (const c of setup.plugin.hookCommands ?? []) lines.push(`- Hook: ${JSON.stringify(c)}`);
|
|
4165
|
+
for (const m of setup.plugin.mcpServers ?? []) {
|
|
4166
|
+
lines.push(`- MCP server ${JSON.stringify(m.name)}: ${JSON.stringify(m.url ?? [m.command, ...m.args ?? []].filter(Boolean).join(" "))}`);
|
|
4167
|
+
}
|
|
4168
|
+
lines.push("");
|
|
4169
|
+
}
|
|
4170
|
+
const parts = Object.entries(setup.parts ?? {});
|
|
4171
|
+
if (parts.length) {
|
|
4172
|
+
lines.push(`## Published parts`, "", ...parts.map(([p, v]) => `- ${p}: version ${v}`), "");
|
|
4173
|
+
}
|
|
4174
|
+
if (setup.memory?.length) {
|
|
4175
|
+
lines.push(`## Team memory`, "", ...setup.memory.map((m) => `- ${m.project}: ${m.fileCount} file(s)`), "");
|
|
4176
|
+
}
|
|
4177
|
+
return lines;
|
|
4178
|
+
}
|
|
4179
|
+
function describeComputer(setup, state) {
|
|
4180
|
+
const lines = ["## This computer", ""];
|
|
4181
|
+
if (!setup.sessionBrief) {
|
|
4182
|
+
return [...lines, "This SystemDox API is too old to compare this computer with the team setup."];
|
|
4183
|
+
}
|
|
4184
|
+
const skills = setup.sessionBrief.skills;
|
|
4185
|
+
const latest = new Map(skills.map((s) => [s.name, s.latestVersion]));
|
|
4186
|
+
lines.push(
|
|
4187
|
+
state.missing.length ? `- Missing: ${state.missing.map((name) => `${name} version ${latest.get(name)}`).join(", ")}` : `- Missing: none of ${skills.length}`
|
|
4188
|
+
);
|
|
4189
|
+
if (state.behind.length) {
|
|
4190
|
+
lines.push(`- Behind: ${state.behind.map((b) => `${b.name} (version ${b.here} here, latest ${b.latest})`).join(", ")}`);
|
|
4191
|
+
}
|
|
4192
|
+
if (state.notFromSystemDox.length) lines.push(`- Not from SystemDox, left alone: ${state.notFromSystemDox.join(", ")}`);
|
|
4193
|
+
if (setup.plugin) {
|
|
4194
|
+
lines.push(
|
|
4195
|
+
state.pluginInstalled ? `- Team plugin: installed${state.pluginVersion ? `, ${state.pluginVersion}` : ""}; the latest is version ${setup.plugin.version}` : "- Team plugin: not installed"
|
|
4196
|
+
);
|
|
4197
|
+
}
|
|
4198
|
+
lines.push("", "## What to do", "");
|
|
4199
|
+
const steps = [];
|
|
4200
|
+
if (state.missing.length) steps.push("Call install_skill for each missing skill, at the version named above. These are plain skills: installing writes files and runs nothing.");
|
|
4201
|
+
if (state.behind.length) {
|
|
4202
|
+
steps.push("Call install_skill for each skill behind, at the latest version named above. If it refuses because files were edited, tell the person and leave them.");
|
|
4203
|
+
}
|
|
4204
|
+
if (hasPersonParts(setup) && !state.pluginInstalled) {
|
|
4205
|
+
steps.push(
|
|
4206
|
+
`Hooks, MCP servers, team instructions and memory need the person: \`${SETUP_COMMAND}\` in their own terminal. Never answer its prompts for them.`
|
|
4207
|
+
);
|
|
4208
|
+
}
|
|
4209
|
+
lines.push(...steps.length ? steps.map((s, i) => `${i + 1}. ${s}`) : ["Nothing."]);
|
|
4210
|
+
return lines;
|
|
4211
|
+
}
|
|
4212
|
+
function registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 }) {
|
|
4213
|
+
server.registerTool(
|
|
4214
|
+
"get_team_setup",
|
|
4215
|
+
{
|
|
4216
|
+
title: "Get Team Setup",
|
|
4217
|
+
description: localFiles ? "What this workspace's team setup holds (skills, plugin, hooks, MCP servers, memory) and what this computer lacks, with what to do about it. Call it to check whether this computer is set up. Reads only." : "What this workspace's team setup holds (skills, plugin, hooks, MCP servers, memory), and how to check this computer against it. Reads only.",
|
|
4218
|
+
inputSchema: {},
|
|
4219
|
+
annotations: { readOnlyHint: true }
|
|
4220
|
+
},
|
|
4221
|
+
async () => {
|
|
4222
|
+
const read = await readTeamSetup(apiClient2, { keyProblem: keyProblem2, timeoutMs: 15e3 });
|
|
4223
|
+
if ("problem" in read) return { content: [{ type: "text", text: read.problem }] };
|
|
4224
|
+
const lines = describeSetup(read.setup);
|
|
4225
|
+
if (localFiles) lines.push(...describeComputer(read.setup, await computerState(read.setup)));
|
|
4226
|
+
else lines.push(CANNOT_SEE);
|
|
4227
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
4228
|
+
}
|
|
4229
|
+
);
|
|
4230
|
+
}
|
|
4231
|
+
var HELD_BECAUSE;
|
|
4232
|
+
var init_get_team_setup = __esm({
|
|
4233
|
+
"src/tools/get-team-setup.ts"() {
|
|
4234
|
+
"use strict";
|
|
4235
|
+
init_session_brief();
|
|
4236
|
+
HELD_BECAUSE = {
|
|
4237
|
+
"not-plain": `it is not a plain skill: it has frontmatter beyond a name and a description, files outside its own folders, or a command that runs as it loads. The team plugin carries it, and \`${SETUP_COMMAND}\` shows that before the person accepts`,
|
|
4238
|
+
"outside-default-space": "it is outside the default space, so the team plugin does not carry it either. The person can read it in the console, then install it with `npx -y @systemdox/mcp-server install-skill <name>`",
|
|
4239
|
+
"not-checked": "it could not be checked. Ask again later"
|
|
4240
|
+
};
|
|
4241
|
+
}
|
|
4242
|
+
});
|
|
4243
|
+
|
|
4244
|
+
// src/tools/document-graph.ts
|
|
4245
|
+
import { z as z8 } from "zod";
|
|
4246
|
+
function place(file, line) {
|
|
4247
|
+
return file ? `${file}:${line}` : "made by a person";
|
|
4248
|
+
}
|
|
4249
|
+
function known(provenance, confidence) {
|
|
4250
|
+
return `${provenance} ${confidence}`;
|
|
4251
|
+
}
|
|
4252
|
+
function formatNeighbours(answer) {
|
|
4253
|
+
const lines = [`# ${answer.doc.label}`, `\`${answer.doc.id}\``, ""];
|
|
4254
|
+
if (answer.links.length === 0) lines.push("No links.");
|
|
4255
|
+
for (const link of answer.links) {
|
|
4256
|
+
lines.push(
|
|
4257
|
+
`- ${link.direction} \xB7 ${link.relation} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${link.other.label} \xB7 \`${link.other.id}\` \xB7 ${place(link.file, link.line)}`
|
|
4258
|
+
);
|
|
4259
|
+
}
|
|
4260
|
+
if (answer.truncated) {
|
|
4261
|
+
lines.push("", `Showing ${answer.links.length} of ${answer.total}. Raise limit to see more.`);
|
|
4262
|
+
}
|
|
4263
|
+
if (answer.ambiguous.length > 0) {
|
|
4264
|
+
lines.push("", "## Ambiguous: not drawn, because each could mean several documents");
|
|
4265
|
+
for (const entry of answer.ambiguous) {
|
|
4266
|
+
const candidates = entry.candidates.map((c) => `${c.label} (${c.repo}, \`${c.id}\`)`);
|
|
4267
|
+
lines.push(`- ${entry.reference} at ${place(entry.file, entry.line)}: ${candidates.join("; ")}`);
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
return lines.join("\n");
|
|
4271
|
+
}
|
|
4272
|
+
function formatPath(answer, maxHops) {
|
|
4273
|
+
if (!answer.found) return `No path within ${maxHops} hops.`;
|
|
4274
|
+
const lines = answer.steps.map((step, index) => {
|
|
4275
|
+
const arrow = step.reversed ? "<-" : "->";
|
|
4276
|
+
return `${index + 1}. ${step.from.label} ${arrow} ${step.relation} ${arrow} ${step.to.label} \xB7 ${known(step.provenance, step.confidence)} \xB7 ${place(step.file, step.line)}`;
|
|
4277
|
+
});
|
|
4278
|
+
return [...lines, "", `Path confidence: ${answer.confidence}`].join("\n");
|
|
4279
|
+
}
|
|
4280
|
+
function formatStats(stats, repoLinks) {
|
|
4281
|
+
const { declared, extracted, inferred, ambiguous } = stats.links;
|
|
4282
|
+
const lines = [
|
|
4283
|
+
`# The graph: ${stats.nodeCount} documents, ${stats.edgeCount} links`,
|
|
4284
|
+
"",
|
|
4285
|
+
`Links: ${declared} declared \xB7 ${extracted} extracted \xB7 ${inferred} inferred \xB7 ${ambiguous} ambiguous (not drawn)`,
|
|
4286
|
+
"",
|
|
4287
|
+
"## Repositories"
|
|
4288
|
+
];
|
|
4289
|
+
for (const repo of stats.repos) {
|
|
4290
|
+
const state = repo.behind ? `BEHIND: pushed ${repo.headCommit || "unknown"} at ${repo.pushedAt}, index at ${repo.indexedCommit || "unknown"}` : `index at ${repo.indexedCommit || "unknown"}`;
|
|
4291
|
+
lines.push(`- ${repo.repo}: ${repo.documents} documents, ${repo.links} links, ${state}`);
|
|
4292
|
+
}
|
|
4293
|
+
if (repoLinks.length > 0) {
|
|
4294
|
+
lines.push("", "## Repository links");
|
|
4295
|
+
for (const link of repoLinks) {
|
|
4296
|
+
lines.push(
|
|
4297
|
+
`- ${link.source} ${link.relation} ${link.target} \xB7 ${known(link.provenance, link.confidence)} \xB7 ${place(link.file, link.line)}`
|
|
4298
|
+
);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
return lines.join("\n");
|
|
4302
|
+
}
|
|
4303
|
+
function refused(err) {
|
|
4304
|
+
const status = err.status;
|
|
4305
|
+
if (status !== 404 && status !== 400) throw err;
|
|
4306
|
+
return {
|
|
4307
|
+
content: [{ type: "text", text: err.message }],
|
|
4308
|
+
isError: true
|
|
4309
|
+
};
|
|
4310
|
+
}
|
|
4311
|
+
function registerDocumentGraph(server, apiClient2) {
|
|
4312
|
+
server.registerTool(
|
|
4313
|
+
"get_document_links",
|
|
4314
|
+
{
|
|
4315
|
+
description: "What one document links to and from, strongest first. Each link says how it is known: declared (a person made it), extracted (the file says so), or inferred (a reading that could be wrong), with a confidence and the file and line it came from. A reference that could mean several documents is listed as ambiguous with its candidates, and is never drawn as a link. doc_id is the id get_docs returns. Read-only.",
|
|
4316
|
+
inputSchema: {
|
|
4317
|
+
doc_id: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
4318
|
+
direction: z8.enum(["out", "in", "both"]).optional().describe("Default both"),
|
|
4319
|
+
relation: z8.enum(["links_to", "cites", "mentions", "satisfies", "references"]).optional().describe("Only links of this kind"),
|
|
4320
|
+
limit: z8.number().int().min(1).max(MAX_LIMIT).optional().describe(`Most links to show (default ${DEFAULT_LIMIT2})`)
|
|
4321
|
+
},
|
|
4322
|
+
annotations: { readOnlyHint: true }
|
|
4323
|
+
},
|
|
4324
|
+
async ({ doc_id, direction, relation, limit }) => {
|
|
4325
|
+
const params = { doc: doc_id, limit: String(limit ?? DEFAULT_LIMIT2) };
|
|
4326
|
+
if (direction) params.direction = direction;
|
|
4327
|
+
if (relation) params.relation = relation;
|
|
4328
|
+
try {
|
|
4329
|
+
const answer = await apiClient2.get("/v1/graph/neighbours", params);
|
|
4330
|
+
return { content: [{ type: "text", text: formatNeighbours(answer) }] };
|
|
4331
|
+
} catch (err) {
|
|
4332
|
+
return refused(err);
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
);
|
|
4336
|
+
server.registerTool(
|
|
4337
|
+
"trace_document_path",
|
|
4338
|
+
{
|
|
4339
|
+
description: "The shortest way two documents connect, walking links in either direction. Each step says how it is known, and the path is only as strong as its weakest step. Read-only.",
|
|
4340
|
+
inputSchema: {
|
|
4341
|
+
from: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
4342
|
+
to: z8.string().min(1).max(400).describe("A document id, as get_docs returns it"),
|
|
4343
|
+
max_hops: z8.number().int().min(1).max(MAX_HOPS).optional().describe(`Longest path to look for (default ${DEFAULT_HOPS})`)
|
|
4344
|
+
},
|
|
4345
|
+
annotations: { readOnlyHint: true }
|
|
4346
|
+
},
|
|
4347
|
+
async ({ from, to, max_hops }) => {
|
|
4348
|
+
const hops = max_hops ?? DEFAULT_HOPS;
|
|
4349
|
+
try {
|
|
4350
|
+
const answer = await apiClient2.get("/v1/graph/path", {
|
|
4351
|
+
from,
|
|
4352
|
+
to,
|
|
4353
|
+
maxHops: String(hops)
|
|
4354
|
+
});
|
|
4355
|
+
return { content: [{ type: "text", text: formatPath(answer, hops) }] };
|
|
4356
|
+
} catch (err) {
|
|
4357
|
+
return refused(err);
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
);
|
|
4361
|
+
server.registerTool(
|
|
4362
|
+
"get_graph_stats",
|
|
4363
|
+
{
|
|
4364
|
+
description: "The state of the graph: links counted by how they are known, each repository's freshness (marked BEHIND when a push has not finished indexing), and how repositories depend on each other, from the files that declare it. Read-only.",
|
|
4365
|
+
inputSchema: {},
|
|
4366
|
+
annotations: { readOnlyHint: true }
|
|
4367
|
+
},
|
|
4368
|
+
async () => {
|
|
4369
|
+
const [stats, repos] = await Promise.all([
|
|
4370
|
+
apiClient2.getFresh("/v1/graph/stats"),
|
|
4371
|
+
apiClient2.getFresh("/v1/graph/repos")
|
|
4372
|
+
]);
|
|
4373
|
+
return { content: [{ type: "text", text: formatStats(stats, repos.links) }] };
|
|
4374
|
+
}
|
|
4375
|
+
);
|
|
4376
|
+
}
|
|
4377
|
+
var DEFAULT_LIMIT2, MAX_LIMIT, DEFAULT_HOPS, MAX_HOPS;
|
|
4378
|
+
var init_document_graph = __esm({
|
|
4379
|
+
"src/tools/document-graph.ts"() {
|
|
4380
|
+
"use strict";
|
|
4381
|
+
DEFAULT_LIMIT2 = 50;
|
|
4382
|
+
MAX_LIMIT = 200;
|
|
4383
|
+
DEFAULT_HOPS = 6;
|
|
4384
|
+
MAX_HOPS = 8;
|
|
4385
|
+
}
|
|
4386
|
+
});
|
|
4387
|
+
|
|
3660
4388
|
// src/tools/create-guardrail.ts
|
|
3661
|
-
import { z as
|
|
3662
|
-
function registerCreateGuardrail(
|
|
4389
|
+
import { z as z9 } from "zod";
|
|
4390
|
+
function registerCreateGuardrail(server, apiClient2) {
|
|
3663
4391
|
const handler = async ({
|
|
3664
4392
|
title,
|
|
3665
4393
|
description,
|
|
@@ -3696,7 +4424,7 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
3696
4424
|
]
|
|
3697
4425
|
};
|
|
3698
4426
|
};
|
|
3699
|
-
|
|
4427
|
+
server.registerTool(
|
|
3700
4428
|
"create_check",
|
|
3701
4429
|
{
|
|
3702
4430
|
title: "Create Check",
|
|
@@ -3708,7 +4436,7 @@ function registerCreateGuardrail(server2, apiClient2) {
|
|
|
3708
4436
|
},
|
|
3709
4437
|
handler
|
|
3710
4438
|
);
|
|
3711
|
-
|
|
4439
|
+
server.registerTool(
|
|
3712
4440
|
"create_guardrail",
|
|
3713
4441
|
{
|
|
3714
4442
|
title: "Create Check (legacy name)",
|
|
@@ -3738,32 +4466,32 @@ var init_create_guardrail = __esm({
|
|
|
3738
4466
|
"data-access"
|
|
3739
4467
|
];
|
|
3740
4468
|
inputSchema2 = () => ({
|
|
3741
|
-
title:
|
|
3742
|
-
description:
|
|
3743
|
-
severity:
|
|
3744
|
-
category:
|
|
3745
|
-
pattern:
|
|
3746
|
-
fix:
|
|
3747
|
-
fitness_test:
|
|
4469
|
+
title: z9.string().describe("Short title for the check"),
|
|
4470
|
+
description: z9.string().describe("Detailed description of what the check enforces and why"),
|
|
4471
|
+
severity: z9.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
|
|
4472
|
+
category: z9.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
|
|
4473
|
+
pattern: z9.string().optional().describe("Bad code pattern that should be avoided"),
|
|
4474
|
+
fix: z9.string().optional().describe("Correct code pattern to use instead"),
|
|
4475
|
+
fitness_test: z9.string().optional().describe("Shell command to detect violations in CI")
|
|
3748
4476
|
});
|
|
3749
4477
|
}
|
|
3750
4478
|
});
|
|
3751
4479
|
|
|
3752
4480
|
// src/tools/update-spec.ts
|
|
3753
|
-
import { z as
|
|
3754
|
-
function registerUpdateSpec(
|
|
3755
|
-
|
|
4481
|
+
import { z as z10 } from "zod";
|
|
4482
|
+
function registerUpdateSpec(server, apiClient2) {
|
|
4483
|
+
server.registerTool(
|
|
3756
4484
|
"update_spec",
|
|
3757
4485
|
{
|
|
3758
4486
|
title: "Update Spec",
|
|
3759
4487
|
description: "Update an existing SystemDox spec's proposal, tasks, or requirements. Only draft specs can be modified \u2014 published specs must be versioned first.",
|
|
3760
4488
|
inputSchema: {
|
|
3761
|
-
draft_id:
|
|
3762
|
-
section:
|
|
3763
|
-
content:
|
|
4489
|
+
draft_id: z10.string().describe("ID of the spec to update"),
|
|
4490
|
+
section: z10.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
|
|
4491
|
+
content: z10.string().describe(
|
|
3764
4492
|
"New markdown content for the section. For requirements, provide the full requirement content."
|
|
3765
4493
|
),
|
|
3766
|
-
requirement_name:
|
|
4494
|
+
requirement_name: z10.string().optional().describe(
|
|
3767
4495
|
"Name of the requirement (required when section is 'requirements')"
|
|
3768
4496
|
)
|
|
3769
4497
|
},
|
|
@@ -3818,19 +4546,19 @@ var init_update_spec = __esm({
|
|
|
3818
4546
|
});
|
|
3819
4547
|
|
|
3820
4548
|
// src/tools/report-decision.ts
|
|
3821
|
-
import { z as
|
|
3822
|
-
function registerReportDecision(
|
|
3823
|
-
|
|
4549
|
+
import { z as z11 } from "zod";
|
|
4550
|
+
function registerReportDecision(server, apiClient2) {
|
|
4551
|
+
server.registerTool(
|
|
3824
4552
|
"report_decision",
|
|
3825
4553
|
{
|
|
3826
4554
|
title: "Report Decision",
|
|
3827
4555
|
description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
|
|
3828
4556
|
inputSchema: {
|
|
3829
|
-
title:
|
|
3830
|
-
context:
|
|
3831
|
-
decision:
|
|
3832
|
-
consequences:
|
|
3833
|
-
related_files:
|
|
4557
|
+
title: z11.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
|
|
4558
|
+
context: z11.string().describe("What prompted this decision \u2014 the problem or constraint"),
|
|
4559
|
+
decision: z11.string().describe("What was decided and why"),
|
|
4560
|
+
consequences: z11.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
|
|
4561
|
+
related_files: z11.array(z11.string()).optional().describe("File paths related to this decision")
|
|
3834
4562
|
},
|
|
3835
4563
|
annotations: {
|
|
3836
4564
|
readOnlyHint: false
|
|
@@ -3886,15 +4614,15 @@ var init_report_decision = __esm({
|
|
|
3886
4614
|
});
|
|
3887
4615
|
|
|
3888
4616
|
// src/tools/get-project-rules.ts
|
|
3889
|
-
import { z as
|
|
3890
|
-
function registerGetProjectRules(
|
|
3891
|
-
|
|
4617
|
+
import { z as z12 } from "zod";
|
|
4618
|
+
function registerGetProjectRules(server, apiClient2) {
|
|
4619
|
+
server.registerTool(
|
|
3892
4620
|
"get_project_rules",
|
|
3893
4621
|
{
|
|
3894
4622
|
title: "Get Project Rules",
|
|
3895
4623
|
description: "Retrieve project rules defined in SystemDox Standards. These are team-defined principles covering deployment, architecture, security, testing, and process. Filter by category to get only relevant rules.",
|
|
3896
4624
|
inputSchema: {
|
|
3897
|
-
category:
|
|
4625
|
+
category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
|
|
3898
4626
|
}
|
|
3899
4627
|
},
|
|
3900
4628
|
async ({ category }) => {
|
|
@@ -3961,18 +4689,18 @@ var init_get_project_rules = __esm({
|
|
|
3961
4689
|
});
|
|
3962
4690
|
|
|
3963
4691
|
// src/tools/create-project-rule.ts
|
|
3964
|
-
import { z as
|
|
3965
|
-
function registerCreateProjectRule(
|
|
3966
|
-
|
|
4692
|
+
import { z as z13 } from "zod";
|
|
4693
|
+
function registerCreateProjectRule(server, apiClient2) {
|
|
4694
|
+
server.registerTool(
|
|
3967
4695
|
"create_project_rule",
|
|
3968
4696
|
{
|
|
3969
4697
|
title: "Create Project Rule",
|
|
3970
4698
|
description: "Create a new project rule in SystemDox Standards. Use this when you discover a team principle or convention that should be enforced across the project.",
|
|
3971
4699
|
inputSchema: {
|
|
3972
|
-
text:
|
|
4700
|
+
text: z13.string().describe(
|
|
3973
4701
|
"The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
|
|
3974
4702
|
),
|
|
3975
|
-
category:
|
|
4703
|
+
category: z13.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
|
|
3976
4704
|
},
|
|
3977
4705
|
annotations: {
|
|
3978
4706
|
readOnlyHint: false
|
|
@@ -4002,8 +4730,8 @@ var init_create_project_rule = __esm({
|
|
|
4002
4730
|
});
|
|
4003
4731
|
|
|
4004
4732
|
// src/tools/get-prompt-templates.ts
|
|
4005
|
-
function registerGetPromptTemplates(
|
|
4006
|
-
|
|
4733
|
+
function registerGetPromptTemplates(server, apiClient2) {
|
|
4734
|
+
server.registerTool(
|
|
4007
4735
|
"get_prompt_templates",
|
|
4008
4736
|
{
|
|
4009
4737
|
title: "Get Prompt Templates",
|
|
@@ -4054,17 +4782,17 @@ var init_get_prompt_templates = __esm({
|
|
|
4054
4782
|
});
|
|
4055
4783
|
|
|
4056
4784
|
// src/tools/create-prompt-template.ts
|
|
4057
|
-
import { z as
|
|
4058
|
-
function registerCreatePromptTemplate(
|
|
4059
|
-
|
|
4785
|
+
import { z as z14 } from "zod";
|
|
4786
|
+
function registerCreatePromptTemplate(server, apiClient2) {
|
|
4787
|
+
server.registerTool(
|
|
4060
4788
|
"create_prompt_template",
|
|
4061
4789
|
{
|
|
4062
4790
|
title: "Create Prompt Template",
|
|
4063
4791
|
description: "Create a new reusable prompt template in SystemDox Standards. Use this to store preconfigured prompts for common AI tasks like code reviews, documentation generation, or analysis frameworks.",
|
|
4064
4792
|
inputSchema: {
|
|
4065
|
-
name:
|
|
4066
|
-
description:
|
|
4067
|
-
prompt:
|
|
4793
|
+
name: z14.string().describe("Short name for the template"),
|
|
4794
|
+
description: z14.string().optional().describe("Brief description of when to use this template"),
|
|
4795
|
+
prompt: z14.string().describe("The full prompt text that Gen AI will use when generating content")
|
|
4068
4796
|
},
|
|
4069
4797
|
annotations: {
|
|
4070
4798
|
readOnlyHint: false
|
|
@@ -4098,7 +4826,7 @@ var init_create_prompt_template = __esm({
|
|
|
4098
4826
|
});
|
|
4099
4827
|
|
|
4100
4828
|
// src/tools/skills.ts
|
|
4101
|
-
import { z as
|
|
4829
|
+
import { z as z15 } from "zod";
|
|
4102
4830
|
import { homedir as homedir3 } from "os";
|
|
4103
4831
|
import { basename as basename2, resolve as resolve6 } from "path";
|
|
4104
4832
|
function expandHome(path) {
|
|
@@ -4107,25 +4835,25 @@ function expandHome(path) {
|
|
|
4107
4835
|
function text(t) {
|
|
4108
4836
|
return { content: [{ type: "text", text: t }] };
|
|
4109
4837
|
}
|
|
4110
|
-
function registerSkillTools(
|
|
4111
|
-
const fileSchema =
|
|
4112
|
-
path:
|
|
4113
|
-
content:
|
|
4838
|
+
function registerSkillTools(server, apiClient2, { localFiles }) {
|
|
4839
|
+
const fileSchema = z15.object({
|
|
4840
|
+
path: z15.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
|
|
4841
|
+
content: z15.string().describe("The file's full text, exactly as it is")
|
|
4114
4842
|
});
|
|
4115
|
-
|
|
4843
|
+
server.registerTool(
|
|
4116
4844
|
"publish_skill",
|
|
4117
4845
|
{
|
|
4118
4846
|
title: "Publish Skill",
|
|
4119
4847
|
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.",
|
|
4120
4848
|
inputSchema: {
|
|
4121
4849
|
...localFiles ? {
|
|
4122
|
-
directory:
|
|
4850
|
+
directory: z15.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
|
|
4123
4851
|
} : {},
|
|
4124
|
-
files:
|
|
4125
|
-
name:
|
|
4126
|
-
spaceId:
|
|
4127
|
-
note:
|
|
4128
|
-
baseVersion:
|
|
4852
|
+
files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
|
|
4853
|
+
name: z15.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
|
|
4854
|
+
spaceId: z15.string().optional().describe("Space for a NEW skill (default: your default space)"),
|
|
4855
|
+
note: z15.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
|
|
4856
|
+
baseVersion: z15.number().int().positive().optional().describe(
|
|
4129
4857
|
"The version these files were edited from. When given, the publish is refused (SYD-K017) if someone published a newer version since, instead of replacing their changes"
|
|
4130
4858
|
)
|
|
4131
4859
|
},
|
|
@@ -4163,7 +4891,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
4163
4891
|
);
|
|
4164
4892
|
}
|
|
4165
4893
|
);
|
|
4166
|
-
|
|
4894
|
+
server.registerTool(
|
|
4167
4895
|
"list_skills",
|
|
4168
4896
|
{
|
|
4169
4897
|
title: "List Skills",
|
|
@@ -4175,7 +4903,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
4175
4903
|
const { skills } = await apiClient2.get(SKILLS);
|
|
4176
4904
|
if (!skills.length) {
|
|
4177
4905
|
return text(
|
|
4178
|
-
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/skills", await workspaceBase(apiClient2))}`
|
|
4906
|
+
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/team-setup/skills", await workspaceBase(apiClient2))}`
|
|
4179
4907
|
);
|
|
4180
4908
|
}
|
|
4181
4909
|
return text(
|
|
@@ -4185,16 +4913,16 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
4185
4913
|
);
|
|
4186
4914
|
}
|
|
4187
4915
|
);
|
|
4188
|
-
|
|
4916
|
+
server.registerTool(
|
|
4189
4917
|
"install_skill",
|
|
4190
4918
|
{
|
|
4191
4919
|
title: "Install Skill",
|
|
4192
4920
|
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.",
|
|
4193
4921
|
inputSchema: {
|
|
4194
|
-
name:
|
|
4195
|
-
version:
|
|
4196
|
-
target:
|
|
4197
|
-
...localFiles ? { overwrite:
|
|
4922
|
+
name: z15.string().describe("The skill's name"),
|
|
4923
|
+
version: z15.number().int().positive().optional().describe("A version number (default: the latest)"),
|
|
4924
|
+
target: z15.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
|
|
4925
|
+
...localFiles ? { overwrite: z15.boolean().optional().describe("Replace files edited since they were installed") } : {}
|
|
4198
4926
|
},
|
|
4199
4927
|
annotations: { readOnlyHint: !localFiles }
|
|
4200
4928
|
},
|
|
@@ -4249,22 +4977,22 @@ var init_skills2 = __esm({
|
|
|
4249
4977
|
});
|
|
4250
4978
|
|
|
4251
4979
|
// src/lib/team-setup-types.ts
|
|
4252
|
-
var
|
|
4980
|
+
var PARTS2, PERSON_ID_RE;
|
|
4253
4981
|
var init_team_setup_types = __esm({
|
|
4254
4982
|
"src/lib/team-setup-types.ts"() {
|
|
4255
4983
|
"use strict";
|
|
4256
4984
|
init_person_values();
|
|
4257
|
-
|
|
4985
|
+
PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
4258
4986
|
PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
4259
4987
|
}
|
|
4260
4988
|
});
|
|
4261
4989
|
|
|
4262
4990
|
// src/tools/team-setup.ts
|
|
4263
|
-
import { z as
|
|
4264
|
-
import { existsSync as
|
|
4991
|
+
import { z as z16 } from "zod";
|
|
4992
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4265
4993
|
import { lstat as lstat2 } from "fs/promises";
|
|
4266
4994
|
import { homedir as homedir4 } from "os";
|
|
4267
|
-
import { join as
|
|
4995
|
+
import { join as join8, resolve as resolve7 } from "path";
|
|
4268
4996
|
function expandHome2(path) {
|
|
4269
4997
|
return path.replace(/^~(?=$|[\\/])/, homedir4());
|
|
4270
4998
|
}
|
|
@@ -4290,21 +5018,21 @@ async function readPartDirectory(part, directory) {
|
|
|
4290
5018
|
let present = true;
|
|
4291
5019
|
for (let i = 1; i <= segments.length; i++) {
|
|
4292
5020
|
const rel = segments.slice(0, i).join("/");
|
|
4293
|
-
if (await isLink(
|
|
5021
|
+
if (await isLink(join8(root, ...segments.slice(0, i)))) {
|
|
4294
5022
|
links.push(rel);
|
|
4295
5023
|
present = false;
|
|
4296
5024
|
break;
|
|
4297
5025
|
}
|
|
4298
5026
|
}
|
|
4299
|
-
if (present &&
|
|
5027
|
+
if (present && existsSync7(join8(root, ...segments))) paths.push(path);
|
|
4300
5028
|
}
|
|
4301
5029
|
for (const { dir, md } of layout.folders) {
|
|
4302
|
-
const full =
|
|
5030
|
+
const full = join8(root, dir);
|
|
4303
5031
|
if (await isLink(full)) {
|
|
4304
5032
|
links.push(dir);
|
|
4305
5033
|
continue;
|
|
4306
5034
|
}
|
|
4307
|
-
if (!
|
|
5035
|
+
if (!existsSync7(full)) continue;
|
|
4308
5036
|
const walked = await walk(full, { skipHidden: true });
|
|
4309
5037
|
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
4310
5038
|
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
@@ -4343,24 +5071,24 @@ function describePerson(id, person) {
|
|
|
4343
5071
|
);
|
|
4344
5072
|
return lines.join("\n");
|
|
4345
5073
|
}
|
|
4346
|
-
function registerTeamSetupTools(
|
|
4347
|
-
const fileSchema =
|
|
4348
|
-
path:
|
|
4349
|
-
content:
|
|
5074
|
+
function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
5075
|
+
const fileSchema = z16.object({
|
|
5076
|
+
path: z16.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
|
|
5077
|
+
content: z16.string().describe("The file's full text, exactly as it is")
|
|
4350
5078
|
});
|
|
4351
|
-
|
|
5079
|
+
server.registerTool(
|
|
4352
5080
|
"publish_setup_part",
|
|
4353
5081
|
{
|
|
4354
5082
|
title: "Publish Setup Part",
|
|
4355
5083
|
description: "Publish one part of your team's Claude Code setup to SystemDox: `hooks` (hooks/hooks.json and the scripts/ it calls), `mcp` (.mcp.json), `agents` (agents/*.md), `commands` (commands/*.md) or `instructions` (CLAUDE.md). Every part but instructions makes a new version of the team plugin, which each member accepts at their next update. Publishing hooks, mcp, agents or commands needs the admin role. " + (localFiles ? "Pass `directory`: the server reads only the files the part accepts, byte for byte, and refuses a link, a binary file or a file too large before anything is sent. Never retype the files into `files` when the folder is on this computer." : "Pass every file of the part in `files`, exactly as it is."),
|
|
4356
5084
|
inputSchema: {
|
|
4357
|
-
part:
|
|
5085
|
+
part: z16.enum(PARTS2).describe("Which part of the setup"),
|
|
4358
5086
|
...localFiles ? {
|
|
4359
|
-
directory:
|
|
5087
|
+
directory: z16.string().optional().describe(
|
|
4360
5088
|
"The folder on this computer: for hooks, the one holding hooks/hooks.json and scripts/; for mcp, the one holding .mcp.json; for instructions, the one holding CLAUDE.md; for agents or commands, the one holding agents/ or commands/"
|
|
4361
5089
|
)
|
|
4362
5090
|
} : {},
|
|
4363
|
-
files:
|
|
5091
|
+
files: z16.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
|
|
4364
5092
|
},
|
|
4365
5093
|
annotations: { readOnlyHint: false }
|
|
4366
5094
|
},
|
|
@@ -4390,13 +5118,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
4390
5118
|
return text2(lines.join("\n"));
|
|
4391
5119
|
}
|
|
4392
5120
|
);
|
|
4393
|
-
|
|
5121
|
+
server.registerTool(
|
|
4394
5122
|
"resolve_person",
|
|
4395
5123
|
{
|
|
4396
5124
|
title: "Resolve Person",
|
|
4397
5125
|
description: "Read back a person that a team memory names by identifier, written as `con_\u2026` (label), `ctc_\u2026` or `usr_\u2026`: their label, name, phone number and email address, from the vault that holds them. The details are for this session only: never write them into a memory file; the memory keeps the identifier. Writes nothing.",
|
|
4398
5126
|
inputSchema: {
|
|
4399
|
-
id:
|
|
5127
|
+
id: z16.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
|
|
4400
5128
|
},
|
|
4401
5129
|
annotations: { readOnlyHint: true }
|
|
4402
5130
|
},
|
|
@@ -4407,14 +5135,14 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
4407
5135
|
}
|
|
4408
5136
|
);
|
|
4409
5137
|
if (!localFiles) return;
|
|
4410
|
-
|
|
5138
|
+
server.registerTool(
|
|
4411
5139
|
"sync_memory",
|
|
4412
5140
|
{
|
|
4413
5141
|
title: "Sync Memory",
|
|
4414
5142
|
description: `Sync one project's team memory on this computer with SystemDox: pull the files the team changed, push the ones changed here, keep both texts of a file changed on both sides. It never answers for a person: a file that still holds a phone number, an email address or a home-folder path is skipped and listed, with the command for the member to run in their own terminal (\`${SYNC_MEMORY_COMMAND}\`). Pass dryRun to see what it would do without writing or sending anything.`,
|
|
4415
5143
|
inputSchema: {
|
|
4416
|
-
project:
|
|
4417
|
-
dryRun:
|
|
5144
|
+
project: z16.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
|
|
5145
|
+
dryRun: z16.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
|
|
4418
5146
|
},
|
|
4419
5147
|
annotations: { readOnlyHint: false }
|
|
4420
5148
|
},
|
|
@@ -4449,28 +5177,34 @@ var init_team_setup = __esm({
|
|
|
4449
5177
|
|
|
4450
5178
|
// src/server.ts
|
|
4451
5179
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4452
|
-
function createServer(apiClient2, { localFiles = false } = {}) {
|
|
4453
|
-
const
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
5180
|
+
function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
|
|
5181
|
+
const server = new McpServer(
|
|
5182
|
+
{
|
|
5183
|
+
name: "systemdox-mcp-server",
|
|
5184
|
+
version: SERVER_VERSION
|
|
5185
|
+
},
|
|
5186
|
+
instructions ? { instructions } : void 0
|
|
5187
|
+
);
|
|
5188
|
+
registerGetGuardrails(server, apiClient2);
|
|
5189
|
+
registerGetSpecs(server, apiClient2);
|
|
5190
|
+
registerGetDocs(server, apiClient2);
|
|
5191
|
+
registerGetTechStack(server, apiClient2);
|
|
5192
|
+
registerGetPlanningContext(server, apiClient2);
|
|
5193
|
+
registerGetPlanBoard(server, apiClient2);
|
|
5194
|
+
registerGetChange(server, apiClient2);
|
|
5195
|
+
registerGetContext(server, apiClient2);
|
|
5196
|
+
registerGetProjectRules(server, apiClient2);
|
|
5197
|
+
registerGetPromptTemplates(server, apiClient2);
|
|
5198
|
+
registerGetTeamSetup(server, apiClient2, { localFiles, keyProblem: keyProblem2 });
|
|
5199
|
+
registerDocumentGraph(server, apiClient2);
|
|
5200
|
+
registerCreateGuardrail(server, apiClient2);
|
|
5201
|
+
registerUpdateSpec(server, apiClient2);
|
|
5202
|
+
registerReportDecision(server, apiClient2);
|
|
5203
|
+
registerCreateProjectRule(server, apiClient2);
|
|
5204
|
+
registerCreatePromptTemplate(server, apiClient2);
|
|
5205
|
+
registerSkillTools(server, apiClient2, { localFiles });
|
|
5206
|
+
registerTeamSetupTools(server, apiClient2, { localFiles });
|
|
5207
|
+
return server;
|
|
4474
5208
|
}
|
|
4475
5209
|
var SERVER_VERSION;
|
|
4476
5210
|
var init_server = __esm({
|
|
@@ -4482,7 +5216,10 @@ var init_server = __esm({
|
|
|
4482
5216
|
init_get_tech_stack();
|
|
4483
5217
|
init_get_planning_context();
|
|
4484
5218
|
init_get_plan_board();
|
|
5219
|
+
init_get_change();
|
|
4485
5220
|
init_get_context();
|
|
5221
|
+
init_get_team_setup();
|
|
5222
|
+
init_document_graph();
|
|
4486
5223
|
init_create_guardrail();
|
|
4487
5224
|
init_update_spec();
|
|
4488
5225
|
init_report_decision();
|
|
@@ -4492,7 +5229,7 @@ var init_server = __esm({
|
|
|
4492
5229
|
init_create_prompt_template();
|
|
4493
5230
|
init_skills2();
|
|
4494
5231
|
init_team_setup();
|
|
4495
|
-
SERVER_VERSION = true ? "0.1.
|
|
5232
|
+
SERVER_VERSION = true ? "0.1.50" : "0.0.0-dev";
|
|
4496
5233
|
}
|
|
4497
5234
|
});
|
|
4498
5235
|
|
|
@@ -4505,21 +5242,34 @@ __export(index_exports, {
|
|
|
4505
5242
|
});
|
|
4506
5243
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4507
5244
|
async function main() {
|
|
4508
|
-
await apiClient
|
|
5245
|
+
const instructions = await sessionInstructions(apiClient, { localFiles: true, keyProblem });
|
|
5246
|
+
const server = createServer(apiClient, { localFiles: true, instructions, keyProblem });
|
|
4509
5247
|
const transport = new StdioServerTransport();
|
|
4510
5248
|
await server.connect(transport);
|
|
4511
5249
|
}
|
|
4512
|
-
var config,
|
|
5250
|
+
var config, keyProblem, apiClient;
|
|
4513
5251
|
var init_index = __esm({
|
|
4514
|
-
"src/index.ts"() {
|
|
5252
|
+
async "src/index.ts"() {
|
|
4515
5253
|
"use strict";
|
|
4516
|
-
init_config();
|
|
4517
5254
|
init_api_client();
|
|
5255
|
+
init_config();
|
|
5256
|
+
init_session_brief();
|
|
5257
|
+
init_team_setup_files();
|
|
4518
5258
|
init_server();
|
|
4519
5259
|
init_server();
|
|
4520
|
-
|
|
5260
|
+
try {
|
|
5261
|
+
config = await loadLocalConfig();
|
|
5262
|
+
} catch (err) {
|
|
5263
|
+
console.error(`SystemDox MCP server: ${err.message}`);
|
|
5264
|
+
keyProblem = keyReason(err.message);
|
|
5265
|
+
try {
|
|
5266
|
+
config = loadConfig(NO_KEY);
|
|
5267
|
+
} catch (urlErr) {
|
|
5268
|
+
console.error(`SystemDox MCP server: ${urlErr.message}`);
|
|
5269
|
+
process.exit(1);
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
4521
5272
|
apiClient = new SystemDoxApiClient(config);
|
|
4522
|
-
server = createServer(apiClient, { localFiles: true });
|
|
4523
5273
|
main();
|
|
4524
5274
|
}
|
|
4525
5275
|
});
|
|
@@ -4527,10 +5277,6 @@ var init_index = __esm({
|
|
|
4527
5277
|
// src/cli.ts
|
|
4528
5278
|
init_config();
|
|
4529
5279
|
init_team_setup_files();
|
|
4530
|
-
import { readFile as readFile5, writeFile as writeFile6, chmod as chmod2 } from "fs/promises";
|
|
4531
|
-
import { existsSync as existsSync6 } from "fs";
|
|
4532
|
-
import { homedir as homedir5 } from "os";
|
|
4533
|
-
import { join as join7 } from "path";
|
|
4534
5280
|
import { createInterface } from "readline";
|
|
4535
5281
|
function prompt(question) {
|
|
4536
5282
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -4541,158 +5287,10 @@ function prompt(question) {
|
|
|
4541
5287
|
});
|
|
4542
5288
|
});
|
|
4543
5289
|
}
|
|
4544
|
-
async function validateApiKey(apiKey, apiUrl) {
|
|
4545
|
-
try {
|
|
4546
|
-
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/v1/health`, {
|
|
4547
|
-
headers: {
|
|
4548
|
-
Authorization: `Bearer ${apiKey}`,
|
|
4549
|
-
"X-API-Key": apiKey,
|
|
4550
|
-
Accept: "application/json"
|
|
4551
|
-
}
|
|
4552
|
-
});
|
|
4553
|
-
return res.ok;
|
|
4554
|
-
} catch {
|
|
4555
|
-
return false;
|
|
4556
|
-
}
|
|
4557
|
-
}
|
|
4558
|
-
function findConfigTargets() {
|
|
4559
|
-
const targets = [];
|
|
4560
|
-
const projectMcp = join7(process.cwd(), ".mcp.json");
|
|
4561
|
-
targets.push({
|
|
4562
|
-
name: "Claude Code (project)",
|
|
4563
|
-
path: projectMcp,
|
|
4564
|
-
detected: true
|
|
4565
|
-
// Always available
|
|
4566
|
-
});
|
|
4567
|
-
const claudeUser = join7(homedir5(), ".claude.json");
|
|
4568
|
-
targets.push({
|
|
4569
|
-
name: "Claude Code (user \u2014 all projects)",
|
|
4570
|
-
path: claudeUser,
|
|
4571
|
-
detected: true
|
|
4572
|
-
// Always available
|
|
4573
|
-
});
|
|
4574
|
-
const cursorProject = join7(process.cwd(), ".cursor", "mcp.json");
|
|
4575
|
-
const cursorDir = join7(process.cwd(), ".cursor");
|
|
4576
|
-
targets.push({
|
|
4577
|
-
name: "Cursor (project)",
|
|
4578
|
-
path: cursorProject,
|
|
4579
|
-
detected: existsSync6(cursorDir)
|
|
4580
|
-
});
|
|
4581
|
-
const vscodeProject = join7(process.cwd(), ".vscode", "mcp.json");
|
|
4582
|
-
const vscodeDir = join7(process.cwd(), ".vscode");
|
|
4583
|
-
targets.push({
|
|
4584
|
-
name: "VS Code (project)",
|
|
4585
|
-
path: vscodeProject,
|
|
4586
|
-
detected: existsSync6(vscodeDir)
|
|
4587
|
-
});
|
|
4588
|
-
const cursorGlobal = join7(homedir5(), ".cursor", "mcp.json");
|
|
4589
|
-
targets.push({
|
|
4590
|
-
name: "Cursor (global)",
|
|
4591
|
-
path: cursorGlobal,
|
|
4592
|
-
detected: existsSync6(join7(homedir5(), ".cursor"))
|
|
4593
|
-
});
|
|
4594
|
-
return targets;
|
|
4595
|
-
}
|
|
4596
|
-
async function readOrCreateConfig(path) {
|
|
4597
|
-
try {
|
|
4598
|
-
const content = await readFile5(path, "utf-8");
|
|
4599
|
-
return JSON.parse(content);
|
|
4600
|
-
} catch {
|
|
4601
|
-
return { mcpServers: {} };
|
|
4602
|
-
}
|
|
4603
|
-
}
|
|
4604
|
-
async function writeConfig(path, config2) {
|
|
4605
|
-
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
4606
|
-
if (!existsSync6(dir)) {
|
|
4607
|
-
const { mkdir: mkdir6 } = await import("fs/promises");
|
|
4608
|
-
await mkdir6(dir, { recursive: true });
|
|
4609
|
-
}
|
|
4610
|
-
await writeFile6(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
|
|
4611
|
-
try {
|
|
4612
|
-
await chmod2(path, 384);
|
|
4613
|
-
} catch {
|
|
4614
|
-
}
|
|
4615
|
-
}
|
|
4616
|
-
function serverEntry(apiKey, apiUrl) {
|
|
4617
|
-
const entry = {
|
|
4618
|
-
command: "npx",
|
|
4619
|
-
args: ["@systemdox/mcp-server"],
|
|
4620
|
-
env: {
|
|
4621
|
-
SYSTEMDOX_API_KEY: apiKey
|
|
4622
|
-
}
|
|
4623
|
-
};
|
|
4624
|
-
if (apiUrl !== API_URL_DEFAULT) {
|
|
4625
|
-
entry.env.SYSTEMDOX_API_URL = apiUrl;
|
|
4626
|
-
}
|
|
4627
|
-
return entry;
|
|
4628
|
-
}
|
|
4629
|
-
async function setup() {
|
|
4630
|
-
console.log("\n SystemDox MCP Server Setup\n");
|
|
4631
|
-
const apiKey = await prompt(" Enter your SystemDox API key: ");
|
|
4632
|
-
if (!apiKey) {
|
|
4633
|
-
console.error(
|
|
4634
|
-
"\n Error: API key is required.\n Generate one at your SystemDox settings page.\n"
|
|
4635
|
-
);
|
|
4636
|
-
process.exit(1);
|
|
4637
|
-
}
|
|
4638
|
-
const apiUrlInput = await prompt(
|
|
4639
|
-
` API URL [${API_URL_DEFAULT}]: `
|
|
4640
|
-
);
|
|
4641
|
-
const apiUrl = apiUrlInput || API_URL_DEFAULT;
|
|
4642
|
-
try {
|
|
4643
|
-
requireHttpsApiUrl(apiUrl);
|
|
4644
|
-
} catch (err) {
|
|
4645
|
-
console.error(`
|
|
4646
|
-
Error: ${err.message}
|
|
4647
|
-
`);
|
|
4648
|
-
process.exit(1);
|
|
4649
|
-
}
|
|
4650
|
-
console.log("\n Validating API key...");
|
|
4651
|
-
const valid = await validateApiKey(apiKey, apiUrl);
|
|
4652
|
-
if (!valid) {
|
|
4653
|
-
console.error(
|
|
4654
|
-
"\n Error: Invalid API key or cannot reach the SystemDox API.\n Check your key and try again.\n"
|
|
4655
|
-
);
|
|
4656
|
-
process.exit(1);
|
|
4657
|
-
}
|
|
4658
|
-
console.log(" API key validated successfully.\n");
|
|
4659
|
-
const targets = findConfigTargets();
|
|
4660
|
-
const detected = targets.filter((t) => t.detected);
|
|
4661
|
-
console.log(" Available targets:");
|
|
4662
|
-
detected.forEach((t, i) => {
|
|
4663
|
-
console.log(` ${i + 1}. ${t.name} (${t.path})`);
|
|
4664
|
-
});
|
|
4665
|
-
const choiceInput = await prompt(
|
|
4666
|
-
`
|
|
4667
|
-
Which target? [1-${detected.length}, or 'all']: `
|
|
4668
|
-
);
|
|
4669
|
-
let selected;
|
|
4670
|
-
if (choiceInput.toLowerCase() === "all") {
|
|
4671
|
-
selected = detected;
|
|
4672
|
-
} else {
|
|
4673
|
-
const idx = parseInt(choiceInput, 10) - 1;
|
|
4674
|
-
if (isNaN(idx) || idx < 0 || idx >= detected.length) {
|
|
4675
|
-
console.error("\n Invalid selection.\n");
|
|
4676
|
-
process.exit(1);
|
|
4677
|
-
}
|
|
4678
|
-
selected = [detected[idx]];
|
|
4679
|
-
}
|
|
4680
|
-
const entry = serverEntry(apiKey, apiUrl);
|
|
4681
|
-
for (const target of selected) {
|
|
4682
|
-
const config2 = await readOrCreateConfig(target.path);
|
|
4683
|
-
config2.mcpServers["systemdox"] = entry;
|
|
4684
|
-
await writeConfig(target.path, config2);
|
|
4685
|
-
console.log(` Configured: ${target.name}`);
|
|
4686
|
-
}
|
|
4687
|
-
console.log(
|
|
4688
|
-
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides 20 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 publish_setup_part, resolve_person, sync_memory.\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"
|
|
4689
|
-
);
|
|
4690
|
-
return { apiKey, apiUrl };
|
|
4691
|
-
}
|
|
4692
5290
|
var args = process.argv.slice(2);
|
|
4693
5291
|
var command = args[0];
|
|
4694
5292
|
if (command === "setup") {
|
|
4695
|
-
Promise.resolve().then(() => (init_setup_team(), setup_team_exports)).then(async ({ parseSetupArgs: parseSetupArgs2, runTeamSetupCommand: runTeamSetupCommand2 }) => {
|
|
5293
|
+
Promise.all([Promise.resolve().then(() => (init_setup_team(), setup_team_exports)), Promise.resolve().then(() => (init_setup_mcp(), setup_mcp_exports))]).then(async ([{ parseSetupArgs: parseSetupArgs2, runTeamSetupCommand: runTeamSetupCommand2 }, { setupMcpServer: setupMcpServer2 }]) => {
|
|
4696
5294
|
let teamArgs;
|
|
4697
5295
|
try {
|
|
4698
5296
|
teamArgs = parseSetupArgs2(args.slice(1));
|
|
@@ -4702,8 +5300,13 @@ if (command === "setup") {
|
|
|
4702
5300
|
`);
|
|
4703
5301
|
process.exit(1);
|
|
4704
5302
|
}
|
|
4705
|
-
const
|
|
4706
|
-
|
|
5303
|
+
const step = await setupMcpServer2({
|
|
5304
|
+
out: console.log,
|
|
5305
|
+
err: console.error,
|
|
5306
|
+
ask: (question) => prompt(question)
|
|
5307
|
+
});
|
|
5308
|
+
if (step.code !== 0) return step.code;
|
|
5309
|
+
return runTeamSetupCommand2(teamArgs, step, {
|
|
4707
5310
|
out: console.log,
|
|
4708
5311
|
ask: (question) => prompt(question),
|
|
4709
5312
|
terminal: !!process.stdin.isTTY
|
|
@@ -4763,15 +5366,18 @@ if (command === "setup") {
|
|
|
4763
5366
|
systemdox-mcp-server --help Show this help message
|
|
4764
5367
|
|
|
4765
5368
|
Environment variables:
|
|
4766
|
-
SYSTEMDOX_API_KEY Your SystemDox API key
|
|
4767
|
-
|
|
4768
|
-
|
|
5369
|
+
SYSTEMDOX_API_KEY Your SystemDox API key. When it is not set, every
|
|
5370
|
+
command reads ~/.claude/systemdox/credentials.json,
|
|
5371
|
+
where setup stores the key (and nowhere else: not
|
|
5372
|
+
in your AI tool's MCP config)
|
|
5373
|
+
SYSTEMDOX_WORKSPACE The workspace whose key to use when
|
|
5374
|
+
credentials.json holds several
|
|
4769
5375
|
SYSTEMDOX_API_URL API URL (default: ${API_URL_DEFAULT})
|
|
4770
5376
|
SYSTEMDOX_CACHE_TTL Cache TTL in seconds (default: 300)
|
|
4771
5377
|
SYSTEMDOX_DEFAULT_ORG Default GitHub org
|
|
4772
5378
|
`);
|
|
4773
5379
|
} else if (!command) {
|
|
4774
|
-
|
|
5380
|
+
init_index();
|
|
4775
5381
|
} else {
|
|
4776
5382
|
console.error(` Unknown command: ${command}
|
|
4777
5383
|
Run 'systemdox-mcp-server --help' for usage.`);
|