@systemdox/mcp-server 0.1.47 → 0.1.49
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 +24 -11
- package/dist/cli.js +462 -260
- package/dist/http.js +203 -61
- package/dist/index.d.ts +2 -1
- package/dist/index.js +348 -168
- package/dist/install-skill.js +1 -2
- package/dist/lib/config.d.ts +4 -1
- package/dist/lib/config.js +1 -2
- package/dist/lib/person-values.d.ts +4 -1
- package/dist/lib/team-setup-files.d.ts +14 -1
- package/dist/lib/team-setup-files.js +43 -0
- package/dist/setup-mcp.d.ts +66 -0
- package/dist/setup-mcp.js +636 -0
- package/dist/setup-team.d.ts +5 -1
- package/dist/setup-team.js +1 -0
- package/dist/sync-memory.js +22 -12
- package/dist/tools/get-change.d.ts +134 -0
- package/dist/tools/get-change.js +147 -0
- package/dist/tools/skills.js +1 -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
|
}
|
|
@@ -1141,17 +1165,6 @@ async function upload(client, ws, project, plan, team, report) {
|
|
|
1141
1165
|
}
|
|
1142
1166
|
await send();
|
|
1143
1167
|
}
|
|
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
1168
|
async function syncMemory(opts) {
|
|
1156
1169
|
const { client, out } = opts;
|
|
1157
1170
|
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
@@ -1287,7 +1300,7 @@ async function runSyncMemory(argv, io, env = process.env) {
|
|
|
1287
1300
|
args2 = parseSyncMemoryArgs(argv);
|
|
1288
1301
|
if (!client) {
|
|
1289
1302
|
const apiUrl = apiUrlFrom(env);
|
|
1290
|
-
const apiKey = await
|
|
1303
|
+
const apiKey = await localApiKey(env);
|
|
1291
1304
|
client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1292
1305
|
}
|
|
1293
1306
|
} catch (e) {
|
|
@@ -1380,6 +1393,7 @@ __export(setup_team_exports, {
|
|
|
1380
1393
|
parseSetupArgs: () => parseSetupArgs,
|
|
1381
1394
|
runTeamSetup: () => runTeamSetup,
|
|
1382
1395
|
runTeamSetupCommand: () => runTeamSetupCommand,
|
|
1396
|
+
setInJsonFile: () => setInJsonFile,
|
|
1383
1397
|
setJsonMember: () => setJsonMember,
|
|
1384
1398
|
withImportBlock: () => withImportBlock
|
|
1385
1399
|
});
|
|
@@ -1812,12 +1826,190 @@ ${lines.join("\n")}`)
|
|
|
1812
1826
|
}
|
|
1813
1827
|
});
|
|
1814
1828
|
|
|
1829
|
+
// src/setup-mcp.ts
|
|
1830
|
+
var setup_mcp_exports = {};
|
|
1831
|
+
__export(setup_mcp_exports, {
|
|
1832
|
+
findConfigTargets: () => findConfigTargets,
|
|
1833
|
+
serverEntry: () => serverEntry,
|
|
1834
|
+
setupMcpServer: () => setupMcpServer,
|
|
1835
|
+
writeServerEntry: () => writeServerEntry
|
|
1836
|
+
});
|
|
1837
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
1838
|
+
import { join as join4 } from "path";
|
|
1839
|
+
async function validateApiKey(apiKey, apiUrl) {
|
|
1840
|
+
try {
|
|
1841
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/v1/health`, {
|
|
1842
|
+
headers: {
|
|
1843
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1844
|
+
"X-API-Key": apiKey,
|
|
1845
|
+
Accept: "application/json"
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
return res.ok;
|
|
1849
|
+
} catch {
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
function findConfigTargets(cwd = process.cwd()) {
|
|
1854
|
+
const home = homeDir();
|
|
1855
|
+
return [
|
|
1856
|
+
// Claude Code: the project's .mcp.json, and ~/.claude.json (all projects)
|
|
1857
|
+
{ name: "Claude Code (project)", path: join4(cwd, ".mcp.json"), detected: true },
|
|
1858
|
+
{ name: "Claude Code (user \u2014 all projects)", path: join4(home, ".claude.json"), detected: true },
|
|
1859
|
+
{ name: "Cursor (project)", path: join4(cwd, ".cursor", "mcp.json"), detected: existsSync3(join4(cwd, ".cursor")) },
|
|
1860
|
+
{ name: "VS Code (project)", path: join4(cwd, ".vscode", "mcp.json"), detected: existsSync3(join4(cwd, ".vscode")) },
|
|
1861
|
+
{ name: "Cursor (global)", path: join4(home, ".cursor", "mcp.json"), detected: existsSync3(join4(home, ".cursor")) }
|
|
1862
|
+
];
|
|
1863
|
+
}
|
|
1864
|
+
function serverEntry(apiUrl, existing, workspace) {
|
|
1865
|
+
const env = {};
|
|
1866
|
+
const old = existing && typeof existing === "object" ? existing.env : void 0;
|
|
1867
|
+
if (old && typeof old === "object" && !Array.isArray(old)) {
|
|
1868
|
+
for (const [name, value] of Object.entries(old)) {
|
|
1869
|
+
if (typeof value !== "string" || name === "SYSTEMDOX_API_URL" || name === "SYSTEMDOX_WORKSPACE") continue;
|
|
1870
|
+
if (name === "SYSTEMDOX_API_KEY" && !isReference(value)) continue;
|
|
1871
|
+
env[name] = value;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
if (apiUrl !== API_URL_DEFAULT) env.SYSTEMDOX_API_URL = apiUrl;
|
|
1875
|
+
if (workspace) env.SYSTEMDOX_WORKSPACE = workspace;
|
|
1876
|
+
const entry = { command: "npx", args: ["@systemdox/mcp-server"] };
|
|
1877
|
+
if (Object.keys(env).length) entry.env = env;
|
|
1878
|
+
return entry;
|
|
1879
|
+
}
|
|
1880
|
+
function currentEntry(path) {
|
|
1881
|
+
let text3;
|
|
1882
|
+
try {
|
|
1883
|
+
text3 = readFileSync(path, "utf8");
|
|
1884
|
+
} catch (e) {
|
|
1885
|
+
if (e.code === "ENOENT") return void 0;
|
|
1886
|
+
throw e;
|
|
1887
|
+
}
|
|
1888
|
+
if (!text3.trim()) return void 0;
|
|
1889
|
+
let config2;
|
|
1890
|
+
try {
|
|
1891
|
+
config2 = JSON.parse(text3);
|
|
1892
|
+
} catch {
|
|
1893
|
+
throw new Error(`${path} is not valid JSON: fix it, then run setup again.`);
|
|
1894
|
+
}
|
|
1895
|
+
const servers = config2 && typeof config2 === "object" ? config2.mcpServers : void 0;
|
|
1896
|
+
return servers && typeof servers === "object" ? servers.systemdox : void 0;
|
|
1897
|
+
}
|
|
1898
|
+
async function writeServerEntry(path, apiUrl, workspace) {
|
|
1899
|
+
const existing = currentEntry(path);
|
|
1900
|
+
const env = existing && typeof existing === "object" ? existing.env : void 0;
|
|
1901
|
+
const heldKey = !!env && typeof env === "object" && "SYSTEMDOX_API_KEY" in env && !isReference(env.SYSTEMDOX_API_KEY);
|
|
1902
|
+
await setInJsonFile(path, ["mcpServers", "systemdox"], serverEntry(apiUrl, existing, workspace));
|
|
1903
|
+
return heldKey;
|
|
1904
|
+
}
|
|
1905
|
+
async function keyWorkspace(opts, apiKey, apiUrl) {
|
|
1906
|
+
const client = opts.client?.(apiKey, apiUrl) ?? new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1907
|
+
try {
|
|
1908
|
+
const { workspace } = await client.getOwnTeamSetup();
|
|
1909
|
+
if (isWorkspace(workspace)) return workspace;
|
|
1910
|
+
opts.out(` SystemDox named the workspace ${JSON.stringify(workspace)}, which is not a workspace name.`);
|
|
1911
|
+
} catch (e) {
|
|
1912
|
+
opts.out(` SystemDox did not name your key's workspace (${e.message}).`);
|
|
1913
|
+
}
|
|
1914
|
+
const fromEnv = (opts.env ?? process.env).SYSTEMDOX_WORKSPACE;
|
|
1915
|
+
if (isWorkspace(fromEnv)) return fromEnv;
|
|
1916
|
+
const typed = (await opts.ask(" Your workspace (acme in acme.systemdox.com): ")).trim().toLowerCase();
|
|
1917
|
+
return isWorkspace(typed) ? typed : void 0;
|
|
1918
|
+
}
|
|
1919
|
+
async function setupMcpServer(opts) {
|
|
1920
|
+
const { out, err, ask } = opts;
|
|
1921
|
+
out("\n SystemDox MCP Server Setup\n");
|
|
1922
|
+
const apiKey = (await ask(" Enter your SystemDox API key: ")).trim();
|
|
1923
|
+
if (!apiKey) {
|
|
1924
|
+
err("\n Error: API key is required.\n Generate one at your SystemDox settings page.\n");
|
|
1925
|
+
return { code: 1 };
|
|
1926
|
+
}
|
|
1927
|
+
const apiUrl = (await ask(` API URL [${API_URL_DEFAULT}]: `)).trim() || API_URL_DEFAULT;
|
|
1928
|
+
try {
|
|
1929
|
+
requireHttpsApiUrl(apiUrl);
|
|
1930
|
+
} catch (e) {
|
|
1931
|
+
err(`
|
|
1932
|
+
Error: ${e.message}
|
|
1933
|
+
`);
|
|
1934
|
+
return { code: 1 };
|
|
1935
|
+
}
|
|
1936
|
+
out("\n Validating API key...");
|
|
1937
|
+
if (!await (opts.validateKey ?? validateApiKey)(apiKey, apiUrl)) {
|
|
1938
|
+
err("\n Error: Invalid API key or cannot reach the SystemDox API.\n Check your key and try again.\n");
|
|
1939
|
+
return { code: 1 };
|
|
1940
|
+
}
|
|
1941
|
+
out(" API key validated successfully.\n");
|
|
1942
|
+
const workspace = await keyWorkspace(opts, apiKey, apiUrl);
|
|
1943
|
+
if (!workspace) {
|
|
1944
|
+
err("\n Error: that is not a workspace name (lower case letters, digits and -). Nothing was changed.\n");
|
|
1945
|
+
return { code: 1 };
|
|
1946
|
+
}
|
|
1947
|
+
const detected = findConfigTargets(opts.cwd).filter((t) => t.detected);
|
|
1948
|
+
out(" Available targets:");
|
|
1949
|
+
detected.forEach((t, i) => out(` ${i + 1}. ${t.name} (${t.path})`));
|
|
1950
|
+
const choice = (await ask(`
|
|
1951
|
+
Which target? [1-${detected.length}, or 'all']: `)).trim();
|
|
1952
|
+
let selected;
|
|
1953
|
+
if (choice.toLowerCase() === "all") {
|
|
1954
|
+
selected = detected;
|
|
1955
|
+
} else {
|
|
1956
|
+
const idx = parseInt(choice, 10) - 1;
|
|
1957
|
+
if (isNaN(idx) || idx < 0 || idx >= detected.length) {
|
|
1958
|
+
err("\n Invalid selection.\n");
|
|
1959
|
+
return { code: 1 };
|
|
1960
|
+
}
|
|
1961
|
+
selected = [detected[idx]];
|
|
1962
|
+
}
|
|
1963
|
+
try {
|
|
1964
|
+
if ((await readCredentials())[workspace]?.apiKey !== apiKey) await writeCredentials(workspace, apiKey);
|
|
1965
|
+
const several = Object.keys(await readCredentials()).length > 1;
|
|
1966
|
+
for (const target of selected) {
|
|
1967
|
+
const removed = await writeServerEntry(target.path, apiUrl, several ? workspace : void 0);
|
|
1968
|
+
out(` Configured: ${target.name}`);
|
|
1969
|
+
if (removed) out(` The key now lives in ~/.claude/systemdox/credentials.json and was removed from ${target.path}.`);
|
|
1970
|
+
}
|
|
1971
|
+
} catch (e) {
|
|
1972
|
+
err(`
|
|
1973
|
+
Error: ${e.message}
|
|
1974
|
+
`);
|
|
1975
|
+
return { code: 1 };
|
|
1976
|
+
}
|
|
1977
|
+
out(
|
|
1978
|
+
`
|
|
1979
|
+
Your key for ${workspace} is in ${credentialsPath()} (readable by you alone), not in these configs.
|
|
1980
|
+
|
|
1981
|
+
Done! Restart your AI coding tool to activate the SystemDox MCP server.
|
|
1982
|
+
The server provides 21 tools: get_context, get_checks,
|
|
1983
|
+
get_project_rules, get_prompt_templates, get_specs, get_docs,
|
|
1984
|
+
get_tech_stack, get_planning_context, get_plan_board, get_change,
|
|
1985
|
+
create_check, create_project_rule, create_prompt_template,
|
|
1986
|
+
update_spec, report_decision, publish_skill, list_skills,
|
|
1987
|
+
install_skill, publish_setup_part, resolve_person, sync_memory.
|
|
1988
|
+
|
|
1989
|
+
get_guardrails and create_guardrail remain registered as deprecated
|
|
1990
|
+
aliases of get_checks and create_check \u2014 existing configs keep working.
|
|
1991
|
+
`
|
|
1992
|
+
);
|
|
1993
|
+
return { code: 0, apiKey, apiUrl, workspace };
|
|
1994
|
+
}
|
|
1995
|
+
var isReference;
|
|
1996
|
+
var init_setup_mcp = __esm({
|
|
1997
|
+
"src/setup-mcp.ts"() {
|
|
1998
|
+
"use strict";
|
|
1999
|
+
init_api_client();
|
|
2000
|
+
init_config();
|
|
2001
|
+
init_team_setup_files();
|
|
2002
|
+
init_setup_team();
|
|
2003
|
+
isReference = (value) => typeof value === "string" && /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value);
|
|
2004
|
+
}
|
|
2005
|
+
});
|
|
2006
|
+
|
|
1815
2007
|
// src/lib/skills.ts
|
|
1816
2008
|
import { createHash as createHash3 } from "crypto";
|
|
1817
2009
|
import { lstat, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
1818
|
-
import { existsSync as
|
|
2010
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1819
2011
|
import { homedir as homedir2 } from "os";
|
|
1820
|
-
import { dirname as dirname4, join as
|
|
2012
|
+
import { dirname as dirname4, join as join5, relative, resolve as resolve3, sep } from "path";
|
|
1821
2013
|
function sha2563(content) {
|
|
1822
2014
|
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
1823
2015
|
}
|
|
@@ -1834,12 +2026,12 @@ function frontmatterName(skillMd) {
|
|
|
1834
2026
|
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
1835
2027
|
}
|
|
1836
2028
|
function defaultSkillDir(name) {
|
|
1837
|
-
return
|
|
2029
|
+
return join5(homedir2(), ".claude", "skills", name);
|
|
1838
2030
|
}
|
|
1839
2031
|
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
1840
2032
|
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
1841
2033
|
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
1842
|
-
const full =
|
|
2034
|
+
const full = join5(dir, entry.name);
|
|
1843
2035
|
const rel = relative(root, full).split(sep).join("/");
|
|
1844
2036
|
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
1845
2037
|
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
@@ -1860,7 +2052,7 @@ async function readTextFiles(root, paths, what) {
|
|
|
1860
2052
|
if (!isValidPath(path)) {
|
|
1861
2053
|
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
1862
2054
|
}
|
|
1863
|
-
const raw = await readFile4(
|
|
2055
|
+
const raw = await readFile4(join5(root, ...path.split("/")));
|
|
1864
2056
|
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
1865
2057
|
if (raw.length > MAX_FILE_BYTES) {
|
|
1866
2058
|
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
@@ -1881,7 +2073,7 @@ async function readTextFiles(root, paths, what) {
|
|
|
1881
2073
|
}
|
|
1882
2074
|
async function readSkillDirectory(directory) {
|
|
1883
2075
|
const root = resolve3(directory);
|
|
1884
|
-
if (!
|
|
2076
|
+
if (!existsSync4(join5(root, ENTRY_FILE))) {
|
|
1885
2077
|
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
1886
2078
|
}
|
|
1887
2079
|
const walked = await walk(root, { skipHidden: true });
|
|
@@ -1902,7 +2094,7 @@ function within(dest, path) {
|
|
|
1902
2094
|
}
|
|
1903
2095
|
async function readManifest(dest) {
|
|
1904
2096
|
try {
|
|
1905
|
-
return JSON.parse(await readFile4(
|
|
2097
|
+
return JSON.parse(await readFile4(join5(dest, MANIFEST_FILE), "utf8"));
|
|
1906
2098
|
} catch {
|
|
1907
2099
|
return void 0;
|
|
1908
2100
|
}
|
|
@@ -1911,10 +2103,10 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
1911
2103
|
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
1912
2104
|
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
1913
2105
|
for (const f of version.files) within(dest, f.path);
|
|
1914
|
-
if (
|
|
2106
|
+
if (existsSync4(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
1915
2107
|
throw new Error(`${dest} is a link: install into a real folder`);
|
|
1916
2108
|
}
|
|
1917
|
-
const walked =
|
|
2109
|
+
const walked = existsSync4(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
1918
2110
|
if (walked.links.length) {
|
|
1919
2111
|
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
1920
2112
|
}
|
|
@@ -1926,7 +2118,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
1926
2118
|
const edited = [];
|
|
1927
2119
|
const untracked = [];
|
|
1928
2120
|
for (const path of present) {
|
|
1929
|
-
const content = await readFile4(
|
|
2121
|
+
const content = await readFile4(join5(dest, ...path.split("/")), "utf8");
|
|
1930
2122
|
if (Object.hasOwn(known, path)) {
|
|
1931
2123
|
if (sha2563(content) !== known[path]) edited.push(path);
|
|
1932
2124
|
} else if (!incoming.has(path) || sha2563(content) !== incoming.get(path).sha256) {
|
|
@@ -1960,7 +2152,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
1960
2152
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1961
2153
|
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
1962
2154
|
};
|
|
1963
|
-
await writeFile4(
|
|
2155
|
+
await writeFile4(join5(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
1964
2156
|
return {
|
|
1965
2157
|
dest,
|
|
1966
2158
|
version: version.version,
|
|
@@ -2108,9 +2300,9 @@ __export(plugin_path_exports, {
|
|
|
2108
2300
|
unpackZip: () => unpackZip
|
|
2109
2301
|
});
|
|
2110
2302
|
import { createHash as createHash4, randomBytes } from "crypto";
|
|
2111
|
-
import { existsSync as
|
|
2303
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2112
2304
|
import { mkdir as mkdir5, rename as rename2, writeFile as writeFile5 } from "fs/promises";
|
|
2113
|
-
import { dirname as dirname5, join as
|
|
2305
|
+
import { dirname as dirname5, join as join6, resolve as resolve5, sep as sep2 } from "path";
|
|
2114
2306
|
import { inflateRawSync } from "zlib";
|
|
2115
2307
|
function parsePluginPathArgs(argv) {
|
|
2116
2308
|
const args2 = {};
|
|
@@ -2230,7 +2422,7 @@ async function runPluginPath(argv, io) {
|
|
|
2230
2422
|
const folder = pluginDir(workspace, version);
|
|
2231
2423
|
try {
|
|
2232
2424
|
const state = await readState(workspace);
|
|
2233
|
-
if (
|
|
2425
|
+
if (existsSync5(folder) && state.plugin?.[String(version)]?.sha256 === sha2564) {
|
|
2234
2426
|
io.out(folder);
|
|
2235
2427
|
return 0;
|
|
2236
2428
|
}
|
|
@@ -2269,17 +2461,17 @@ async function runPluginPath(argv, io) {
|
|
|
2269
2461
|
);
|
|
2270
2462
|
return 2;
|
|
2271
2463
|
}
|
|
2272
|
-
const tmp =
|
|
2464
|
+
const tmp = join6(pluginRoot(workspace), `.${version}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
2273
2465
|
try {
|
|
2274
2466
|
await unpackZip(zip, tmp);
|
|
2275
|
-
if (
|
|
2467
|
+
if (existsSync5(folder) && (await readState(workspace)).plugin?.[String(version)]?.sha256 === sha2564) {
|
|
2276
2468
|
await removeFolder(tmp);
|
|
2277
2469
|
} else {
|
|
2278
|
-
if (
|
|
2470
|
+
if (existsSync5(folder)) await removeFolder(folder);
|
|
2279
2471
|
try {
|
|
2280
2472
|
await rename2(tmp, folder);
|
|
2281
2473
|
} catch (e) {
|
|
2282
|
-
if (!
|
|
2474
|
+
if (!existsSync5(folder)) throw e;
|
|
2283
2475
|
await removeFolder(tmp);
|
|
2284
2476
|
}
|
|
2285
2477
|
}
|
|
@@ -3048,6 +3240,153 @@ var init_get_plan_board = __esm({
|
|
|
3048
3240
|
}
|
|
3049
3241
|
});
|
|
3050
3242
|
|
|
3243
|
+
// src/tools/get-change.ts
|
|
3244
|
+
import { z as z6 } from "zod";
|
|
3245
|
+
function briefVerdict(states) {
|
|
3246
|
+
const waiting = BRIEF.filter(
|
|
3247
|
+
(part) => !(part === "decision" && states.decision === null) && !PAST_REVIEW.has(String(states[part]))
|
|
3248
|
+
).map((part) => `${part} is ${states[part] ?? "unknown"}`);
|
|
3249
|
+
return waiting.length === 0 ? "The brief is passed" : `The brief is not passed: ${waiting.join(", ")}`;
|
|
3250
|
+
}
|
|
3251
|
+
function prLink(pr) {
|
|
3252
|
+
return `[${pr.repo}#${pr.number}](${pr.url}) ${pr.state}`;
|
|
3253
|
+
}
|
|
3254
|
+
function fileSource(file) {
|
|
3255
|
+
if (file.pullRequest) return prLink(file.pullRequest);
|
|
3256
|
+
return file.direct ? "committed directly" : "\u2014";
|
|
3257
|
+
}
|
|
3258
|
+
function partPullRequests(change, part) {
|
|
3259
|
+
const files = part === "contract" ? change.contract.files : part === "behaviour" ? change.behaviour.files : part === "plan" ? [change.plan.file] : [];
|
|
3260
|
+
const prs = part === "decision" ? [change.decision?.pullRequest ?? null] : part === "release" ? change.release.prs : files.map((file) => file?.pullRequest ?? null);
|
|
3261
|
+
const unique = /* @__PURE__ */ new Map();
|
|
3262
|
+
for (const pr of prs) {
|
|
3263
|
+
if (pr && !unique.has(`${pr.repo}#${pr.number}`)) unique.set(`${pr.repo}#${pr.number}`, pr);
|
|
3264
|
+
}
|
|
3265
|
+
return [...unique.values()].sort((a, b) => Number(b.state === "open") - Number(a.state === "open"));
|
|
3266
|
+
}
|
|
3267
|
+
function countsLine(counts) {
|
|
3268
|
+
const tags = ["proposed", "built", "defect", "unbuilt", "open", "untagged"].filter((tag) => counts[tag] > 0).map((tag) => `${counts[tag]} ${tag}`);
|
|
3269
|
+
return `${counts.total} scenario(s)` + (tags.length > 0 ? `: ${tags.join(", ")}` : "");
|
|
3270
|
+
}
|
|
3271
|
+
function partsTable(change) {
|
|
3272
|
+
const rows = PARTS.filter((part) => !(part === "decision" && change.states.decision === null)).map((part) => {
|
|
3273
|
+
const prs = partPullRequests(change, part);
|
|
3274
|
+
return `| ${part} | ${change.states[part]} | ${prs.length > 0 ? prs.map(prLink).join(", ") : "\u2014"} |`;
|
|
3275
|
+
});
|
|
3276
|
+
return ["| Part | State | Pull request |", "|---|---|---|", ...rows].join("\n");
|
|
3277
|
+
}
|
|
3278
|
+
function decisionSection(change) {
|
|
3279
|
+
const decision = change.decision;
|
|
3280
|
+
if (decision === null) return "## Decision\n\nNone: the proposal names no ADR (a small change).";
|
|
3281
|
+
const status = decision.status || "no record in a connected repository";
|
|
3282
|
+
return `## Decision
|
|
3283
|
+
|
|
3284
|
+
${decision.adr} \xB7 ${status}${decision.title ? ` \u2014 ${decision.title}` : ""}`;
|
|
3285
|
+
}
|
|
3286
|
+
function contractSection(change) {
|
|
3287
|
+
const files = change.contract.files;
|
|
3288
|
+
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).");
|
|
3289
|
+
}
|
|
3290
|
+
function behaviourSection(change) {
|
|
3291
|
+
const { files, counts } = change.behaviour;
|
|
3292
|
+
if (files.length === 0) {
|
|
3293
|
+
return `## Behaviour
|
|
3294
|
+
|
|
3295
|
+
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}` : ""}.`;
|
|
3296
|
+
}
|
|
3297
|
+
const blocks = files.map(
|
|
3298
|
+
(file) => [
|
|
3299
|
+
`### ${file.path}`,
|
|
3300
|
+
"",
|
|
3301
|
+
[file.title, file.runner ? `runner ${file.runner}` : "", fileSource(file), countsLine(file.counts)].filter(Boolean).join(" \xB7 "),
|
|
3302
|
+
"",
|
|
3303
|
+
...file.scenarios.map((s) => `- ${s.id || "(no id)"} \xB7 ${s.state || "untagged"} \xB7 ${s.title}`)
|
|
3304
|
+
].join("\n")
|
|
3305
|
+
);
|
|
3306
|
+
return ["## Behaviour", countsLine(counts), ...blocks].join("\n\n");
|
|
3307
|
+
}
|
|
3308
|
+
function planSection(change) {
|
|
3309
|
+
const { file, draftId, board } = change.plan;
|
|
3310
|
+
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";
|
|
3311
|
+
return [
|
|
3312
|
+
"## Plan",
|
|
3313
|
+
"",
|
|
3314
|
+
`- plan.md: ${file ? `${file.path} \u2014 ${fileSource(file)}` : "none"}`,
|
|
3315
|
+
`- Draft: ${draftId ?? "none"}`,
|
|
3316
|
+
`- Board: ${boardLine}`
|
|
3317
|
+
].join("\n");
|
|
3318
|
+
}
|
|
3319
|
+
function releaseSection(change) {
|
|
3320
|
+
const lines = change.release.prs.map(
|
|
3321
|
+
(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")
|
|
3322
|
+
);
|
|
3323
|
+
return `## Release
|
|
3324
|
+
|
|
3325
|
+
${lines.length > 0 ? lines.join("\n") : "No release pull request is open."}`;
|
|
3326
|
+
}
|
|
3327
|
+
function formatChange(change) {
|
|
3328
|
+
const header = `# ${change.title}
|
|
3329
|
+
|
|
3330
|
+
\`${change.key}\` \xB7 size: ${change.size}` + (change.archived ? " \xB7 archived: read-only, done" : "") + ` \xB7 synced ${change.syncedAt}`;
|
|
3331
|
+
return [
|
|
3332
|
+
header,
|
|
3333
|
+
partsTable(change),
|
|
3334
|
+
decisionSection(change),
|
|
3335
|
+
contractSection(change),
|
|
3336
|
+
behaviourSection(change),
|
|
3337
|
+
planSection(change),
|
|
3338
|
+
releaseSection(change),
|
|
3339
|
+
// Compact: a change's every scenario with its steps rides in this block.
|
|
3340
|
+
"```json\n" + JSON.stringify(change) + "\n```",
|
|
3341
|
+
briefVerdict(change.states)
|
|
3342
|
+
].join("\n\n");
|
|
3343
|
+
}
|
|
3344
|
+
function registerGetChange(server2, apiClient2) {
|
|
3345
|
+
server2.registerTool(
|
|
3346
|
+
"get_change",
|
|
3347
|
+
{
|
|
3348
|
+
title: "Get Change",
|
|
3349
|
+
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.`,
|
|
3350
|
+
inputSchema: {
|
|
3351
|
+
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"),
|
|
3352
|
+
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>/")
|
|
3353
|
+
},
|
|
3354
|
+
annotations: { readOnlyHint: true }
|
|
3355
|
+
},
|
|
3356
|
+
async ({ repo, slug }) => {
|
|
3357
|
+
const [owner, name] = repo.split("/");
|
|
3358
|
+
let change;
|
|
3359
|
+
try {
|
|
3360
|
+
change = await apiClient2.getFresh(
|
|
3361
|
+
`/v1/changes/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/${encodeURIComponent(slug)}`
|
|
3362
|
+
);
|
|
3363
|
+
} catch (err) {
|
|
3364
|
+
if (err.status === 404) {
|
|
3365
|
+
return {
|
|
3366
|
+
content: [{ type: "text", text: `No such change ${repo}/${slug}` }],
|
|
3367
|
+
isError: true
|
|
3368
|
+
};
|
|
3369
|
+
}
|
|
3370
|
+
throw err;
|
|
3371
|
+
}
|
|
3372
|
+
return { content: [{ type: "text", text: formatChange(change) }] };
|
|
3373
|
+
}
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3376
|
+
var GITHUB_NAME, REPO_RE, SLUG_RE2, SLUG_MAX, PARTS, BRIEF, PAST_REVIEW;
|
|
3377
|
+
var init_get_change = __esm({
|
|
3378
|
+
"src/tools/get-change.ts"() {
|
|
3379
|
+
"use strict";
|
|
3380
|
+
GITHUB_NAME = "[A-Za-z0-9][A-Za-z0-9._-]{0,99}";
|
|
3381
|
+
REPO_RE = new RegExp(`^${GITHUB_NAME}/${GITHUB_NAME}$`);
|
|
3382
|
+
SLUG_RE2 = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
3383
|
+
SLUG_MAX = 64;
|
|
3384
|
+
PARTS = ["decision", "contract", "behaviour", "plan", "release"];
|
|
3385
|
+
BRIEF = ["decision", "contract", "behaviour", "plan"];
|
|
3386
|
+
PAST_REVIEW = /* @__PURE__ */ new Set(["passed", "being built", "done"]);
|
|
3387
|
+
}
|
|
3388
|
+
});
|
|
3389
|
+
|
|
3051
3390
|
// src/lib/scoring.ts
|
|
3052
3391
|
function estimateTokens(text3) {
|
|
3053
3392
|
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
@@ -3621,7 +3960,7 @@ var init_context_builder = __esm({
|
|
|
3621
3960
|
});
|
|
3622
3961
|
|
|
3623
3962
|
// src/tools/get-context.ts
|
|
3624
|
-
import { z as
|
|
3963
|
+
import { z as z7 } from "zod";
|
|
3625
3964
|
function registerGetContext(server2, apiClient2) {
|
|
3626
3965
|
server2.registerTool(
|
|
3627
3966
|
"get_context",
|
|
@@ -3629,13 +3968,13 @@ function registerGetContext(server2, apiClient2) {
|
|
|
3629
3968
|
title: "Get Context",
|
|
3630
3969
|
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
3970
|
inputSchema: {
|
|
3632
|
-
task_description:
|
|
3971
|
+
task_description: z7.string().describe(
|
|
3633
3972
|
"What you're working on (e.g., 'add a new REST endpoint for user preferences')"
|
|
3634
3973
|
),
|
|
3635
|
-
file_paths:
|
|
3974
|
+
file_paths: z7.array(z7.string()).optional().describe(
|
|
3636
3975
|
"File paths being edited \u2014 used for language/category detection"
|
|
3637
3976
|
),
|
|
3638
|
-
max_tokens:
|
|
3977
|
+
max_tokens: z7.number().optional().describe("Maximum token budget for the response (default: 4000)")
|
|
3639
3978
|
}
|
|
3640
3979
|
},
|
|
3641
3980
|
async ({ task_description, file_paths, max_tokens }) => {
|
|
@@ -3658,7 +3997,7 @@ var init_get_context = __esm({
|
|
|
3658
3997
|
});
|
|
3659
3998
|
|
|
3660
3999
|
// src/tools/create-guardrail.ts
|
|
3661
|
-
import { z as
|
|
4000
|
+
import { z as z8 } from "zod";
|
|
3662
4001
|
function registerCreateGuardrail(server2, apiClient2) {
|
|
3663
4002
|
const handler = async ({
|
|
3664
4003
|
title,
|
|
@@ -3738,19 +4077,19 @@ var init_create_guardrail = __esm({
|
|
|
3738
4077
|
"data-access"
|
|
3739
4078
|
];
|
|
3740
4079
|
inputSchema2 = () => ({
|
|
3741
|
-
title:
|
|
3742
|
-
description:
|
|
3743
|
-
severity:
|
|
3744
|
-
category:
|
|
3745
|
-
pattern:
|
|
3746
|
-
fix:
|
|
3747
|
-
fitness_test:
|
|
4080
|
+
title: z8.string().describe("Short title for the check"),
|
|
4081
|
+
description: z8.string().describe("Detailed description of what the check enforces and why"),
|
|
4082
|
+
severity: z8.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
|
|
4083
|
+
category: z8.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
|
|
4084
|
+
pattern: z8.string().optional().describe("Bad code pattern that should be avoided"),
|
|
4085
|
+
fix: z8.string().optional().describe("Correct code pattern to use instead"),
|
|
4086
|
+
fitness_test: z8.string().optional().describe("Shell command to detect violations in CI")
|
|
3748
4087
|
});
|
|
3749
4088
|
}
|
|
3750
4089
|
});
|
|
3751
4090
|
|
|
3752
4091
|
// src/tools/update-spec.ts
|
|
3753
|
-
import { z as
|
|
4092
|
+
import { z as z9 } from "zod";
|
|
3754
4093
|
function registerUpdateSpec(server2, apiClient2) {
|
|
3755
4094
|
server2.registerTool(
|
|
3756
4095
|
"update_spec",
|
|
@@ -3758,12 +4097,12 @@ function registerUpdateSpec(server2, apiClient2) {
|
|
|
3758
4097
|
title: "Update Spec",
|
|
3759
4098
|
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
4099
|
inputSchema: {
|
|
3761
|
-
draft_id:
|
|
3762
|
-
section:
|
|
3763
|
-
content:
|
|
4100
|
+
draft_id: z9.string().describe("ID of the spec to update"),
|
|
4101
|
+
section: z9.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
|
|
4102
|
+
content: z9.string().describe(
|
|
3764
4103
|
"New markdown content for the section. For requirements, provide the full requirement content."
|
|
3765
4104
|
),
|
|
3766
|
-
requirement_name:
|
|
4105
|
+
requirement_name: z9.string().optional().describe(
|
|
3767
4106
|
"Name of the requirement (required when section is 'requirements')"
|
|
3768
4107
|
)
|
|
3769
4108
|
},
|
|
@@ -3818,7 +4157,7 @@ var init_update_spec = __esm({
|
|
|
3818
4157
|
});
|
|
3819
4158
|
|
|
3820
4159
|
// src/tools/report-decision.ts
|
|
3821
|
-
import { z as
|
|
4160
|
+
import { z as z10 } from "zod";
|
|
3822
4161
|
function registerReportDecision(server2, apiClient2) {
|
|
3823
4162
|
server2.registerTool(
|
|
3824
4163
|
"report_decision",
|
|
@@ -3826,11 +4165,11 @@ function registerReportDecision(server2, apiClient2) {
|
|
|
3826
4165
|
title: "Report Decision",
|
|
3827
4166
|
description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
|
|
3828
4167
|
inputSchema: {
|
|
3829
|
-
title:
|
|
3830
|
-
context:
|
|
3831
|
-
decision:
|
|
3832
|
-
consequences:
|
|
3833
|
-
related_files:
|
|
4168
|
+
title: z10.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
|
|
4169
|
+
context: z10.string().describe("What prompted this decision \u2014 the problem or constraint"),
|
|
4170
|
+
decision: z10.string().describe("What was decided and why"),
|
|
4171
|
+
consequences: z10.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
|
|
4172
|
+
related_files: z10.array(z10.string()).optional().describe("File paths related to this decision")
|
|
3834
4173
|
},
|
|
3835
4174
|
annotations: {
|
|
3836
4175
|
readOnlyHint: false
|
|
@@ -3886,7 +4225,7 @@ var init_report_decision = __esm({
|
|
|
3886
4225
|
});
|
|
3887
4226
|
|
|
3888
4227
|
// src/tools/get-project-rules.ts
|
|
3889
|
-
import { z as
|
|
4228
|
+
import { z as z11 } from "zod";
|
|
3890
4229
|
function registerGetProjectRules(server2, apiClient2) {
|
|
3891
4230
|
server2.registerTool(
|
|
3892
4231
|
"get_project_rules",
|
|
@@ -3894,7 +4233,7 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
3894
4233
|
title: "Get Project Rules",
|
|
3895
4234
|
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
4235
|
inputSchema: {
|
|
3897
|
-
category:
|
|
4236
|
+
category: z11.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
|
|
3898
4237
|
}
|
|
3899
4238
|
},
|
|
3900
4239
|
async ({ category }) => {
|
|
@@ -3961,7 +4300,7 @@ var init_get_project_rules = __esm({
|
|
|
3961
4300
|
});
|
|
3962
4301
|
|
|
3963
4302
|
// src/tools/create-project-rule.ts
|
|
3964
|
-
import { z as
|
|
4303
|
+
import { z as z12 } from "zod";
|
|
3965
4304
|
function registerCreateProjectRule(server2, apiClient2) {
|
|
3966
4305
|
server2.registerTool(
|
|
3967
4306
|
"create_project_rule",
|
|
@@ -3969,10 +4308,10 @@ function registerCreateProjectRule(server2, apiClient2) {
|
|
|
3969
4308
|
title: "Create Project Rule",
|
|
3970
4309
|
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
4310
|
inputSchema: {
|
|
3972
|
-
text:
|
|
4311
|
+
text: z12.string().describe(
|
|
3973
4312
|
"The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
|
|
3974
4313
|
),
|
|
3975
|
-
category:
|
|
4314
|
+
category: z12.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
|
|
3976
4315
|
},
|
|
3977
4316
|
annotations: {
|
|
3978
4317
|
readOnlyHint: false
|
|
@@ -4054,7 +4393,7 @@ var init_get_prompt_templates = __esm({
|
|
|
4054
4393
|
});
|
|
4055
4394
|
|
|
4056
4395
|
// src/tools/create-prompt-template.ts
|
|
4057
|
-
import { z as
|
|
4396
|
+
import { z as z13 } from "zod";
|
|
4058
4397
|
function registerCreatePromptTemplate(server2, apiClient2) {
|
|
4059
4398
|
server2.registerTool(
|
|
4060
4399
|
"create_prompt_template",
|
|
@@ -4062,9 +4401,9 @@ function registerCreatePromptTemplate(server2, apiClient2) {
|
|
|
4062
4401
|
title: "Create Prompt Template",
|
|
4063
4402
|
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
4403
|
inputSchema: {
|
|
4065
|
-
name:
|
|
4066
|
-
description:
|
|
4067
|
-
prompt:
|
|
4404
|
+
name: z13.string().describe("Short name for the template"),
|
|
4405
|
+
description: z13.string().optional().describe("Brief description of when to use this template"),
|
|
4406
|
+
prompt: z13.string().describe("The full prompt text that Gen AI will use when generating content")
|
|
4068
4407
|
},
|
|
4069
4408
|
annotations: {
|
|
4070
4409
|
readOnlyHint: false
|
|
@@ -4098,7 +4437,7 @@ var init_create_prompt_template = __esm({
|
|
|
4098
4437
|
});
|
|
4099
4438
|
|
|
4100
4439
|
// src/tools/skills.ts
|
|
4101
|
-
import { z as
|
|
4440
|
+
import { z as z14 } from "zod";
|
|
4102
4441
|
import { homedir as homedir3 } from "os";
|
|
4103
4442
|
import { basename as basename2, resolve as resolve6 } from "path";
|
|
4104
4443
|
function expandHome(path) {
|
|
@@ -4108,9 +4447,9 @@ function text(t) {
|
|
|
4108
4447
|
return { content: [{ type: "text", text: t }] };
|
|
4109
4448
|
}
|
|
4110
4449
|
function registerSkillTools(server2, apiClient2, { localFiles }) {
|
|
4111
|
-
const fileSchema =
|
|
4112
|
-
path:
|
|
4113
|
-
content:
|
|
4450
|
+
const fileSchema = z14.object({
|
|
4451
|
+
path: z14.string().describe('Path inside the skill folder, forward slashes, e.g. "references/guide.md"'),
|
|
4452
|
+
content: z14.string().describe("The file's full text, exactly as it is")
|
|
4114
4453
|
});
|
|
4115
4454
|
server2.registerTool(
|
|
4116
4455
|
"publish_skill",
|
|
@@ -4119,13 +4458,13 @@ function registerSkillTools(server2, apiClient2, { localFiles }) {
|
|
|
4119
4458
|
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
4459
|
inputSchema: {
|
|
4121
4460
|
...localFiles ? {
|
|
4122
|
-
directory:
|
|
4461
|
+
directory: z14.string().optional().describe("The skill folder on this computer, holding SKILL.md at its root")
|
|
4123
4462
|
} : {},
|
|
4124
|
-
files:
|
|
4125
|
-
name:
|
|
4126
|
-
spaceId:
|
|
4127
|
-
note:
|
|
4128
|
-
baseVersion:
|
|
4463
|
+
files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the skill folder"),
|
|
4464
|
+
name: z14.string().optional().describe("The skill's name; defaults to SKILL.md's frontmatter name and must match it"),
|
|
4465
|
+
spaceId: z14.string().optional().describe("Space for a NEW skill (default: your default space)"),
|
|
4466
|
+
note: z14.string().max(500).optional().describe(`A short change note for this version, shown in the console's history (e.g. "Fix the GA4 step")`),
|
|
4467
|
+
baseVersion: z14.number().int().positive().optional().describe(
|
|
4129
4468
|
"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
4469
|
)
|
|
4131
4470
|
},
|
|
@@ -4175,7 +4514,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
4175
4514
|
const { skills } = await apiClient2.get(SKILLS);
|
|
4176
4515
|
if (!skills.length) {
|
|
4177
4516
|
return text(
|
|
4178
|
-
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/skills", await workspaceBase(apiClient2))}`
|
|
4517
|
+
`No skills published yet. Publish one with publish_skill; they appear at ${configLink("/docs/team-setup/skills", await workspaceBase(apiClient2))}`
|
|
4179
4518
|
);
|
|
4180
4519
|
}
|
|
4181
4520
|
return text(
|
|
@@ -4191,10 +4530,10 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
4191
4530
|
title: "Install Skill",
|
|
4192
4531
|
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
4532
|
inputSchema: {
|
|
4194
|
-
name:
|
|
4195
|
-
version:
|
|
4196
|
-
target:
|
|
4197
|
-
...localFiles ? { overwrite:
|
|
4533
|
+
name: z14.string().describe("The skill's name"),
|
|
4534
|
+
version: z14.number().int().positive().optional().describe("A version number (default: the latest)"),
|
|
4535
|
+
target: z14.string().optional().describe("Folder to install into, e.g. a repo's .claude/skills/<name>"),
|
|
4536
|
+
...localFiles ? { overwrite: z14.boolean().optional().describe("Replace files edited since they were installed") } : {}
|
|
4198
4537
|
},
|
|
4199
4538
|
annotations: { readOnlyHint: !localFiles }
|
|
4200
4539
|
},
|
|
@@ -4249,22 +4588,22 @@ var init_skills2 = __esm({
|
|
|
4249
4588
|
});
|
|
4250
4589
|
|
|
4251
4590
|
// src/lib/team-setup-types.ts
|
|
4252
|
-
var
|
|
4591
|
+
var PARTS2, PERSON_ID_RE;
|
|
4253
4592
|
var init_team_setup_types = __esm({
|
|
4254
4593
|
"src/lib/team-setup-types.ts"() {
|
|
4255
4594
|
"use strict";
|
|
4256
4595
|
init_person_values();
|
|
4257
|
-
|
|
4596
|
+
PARTS2 = ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
4258
4597
|
PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
4259
4598
|
}
|
|
4260
4599
|
});
|
|
4261
4600
|
|
|
4262
4601
|
// src/tools/team-setup.ts
|
|
4263
|
-
import { z as
|
|
4264
|
-
import { existsSync as
|
|
4602
|
+
import { z as z15 } from "zod";
|
|
4603
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4265
4604
|
import { lstat as lstat2 } from "fs/promises";
|
|
4266
4605
|
import { homedir as homedir4 } from "os";
|
|
4267
|
-
import { join as
|
|
4606
|
+
import { join as join7, resolve as resolve7 } from "path";
|
|
4268
4607
|
function expandHome2(path) {
|
|
4269
4608
|
return path.replace(/^~(?=$|[\\/])/, homedir4());
|
|
4270
4609
|
}
|
|
@@ -4290,21 +4629,21 @@ async function readPartDirectory(part, directory) {
|
|
|
4290
4629
|
let present = true;
|
|
4291
4630
|
for (let i = 1; i <= segments.length; i++) {
|
|
4292
4631
|
const rel = segments.slice(0, i).join("/");
|
|
4293
|
-
if (await isLink(
|
|
4632
|
+
if (await isLink(join7(root, ...segments.slice(0, i)))) {
|
|
4294
4633
|
links.push(rel);
|
|
4295
4634
|
present = false;
|
|
4296
4635
|
break;
|
|
4297
4636
|
}
|
|
4298
4637
|
}
|
|
4299
|
-
if (present &&
|
|
4638
|
+
if (present && existsSync6(join7(root, ...segments))) paths.push(path);
|
|
4300
4639
|
}
|
|
4301
4640
|
for (const { dir, md } of layout.folders) {
|
|
4302
|
-
const full =
|
|
4641
|
+
const full = join7(root, dir);
|
|
4303
4642
|
if (await isLink(full)) {
|
|
4304
4643
|
links.push(dir);
|
|
4305
4644
|
continue;
|
|
4306
4645
|
}
|
|
4307
|
-
if (!
|
|
4646
|
+
if (!existsSync6(full)) continue;
|
|
4308
4647
|
const walked = await walk(full, { skipHidden: true });
|
|
4309
4648
|
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
4310
4649
|
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
@@ -4344,9 +4683,9 @@ function describePerson(id, person) {
|
|
|
4344
4683
|
return lines.join("\n");
|
|
4345
4684
|
}
|
|
4346
4685
|
function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
4347
|
-
const fileSchema =
|
|
4348
|
-
path:
|
|
4349
|
-
content:
|
|
4686
|
+
const fileSchema = z15.object({
|
|
4687
|
+
path: z15.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
|
|
4688
|
+
content: z15.string().describe("The file's full text, exactly as it is")
|
|
4350
4689
|
});
|
|
4351
4690
|
server2.registerTool(
|
|
4352
4691
|
"publish_setup_part",
|
|
@@ -4354,13 +4693,13 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
4354
4693
|
title: "Publish Setup Part",
|
|
4355
4694
|
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
4695
|
inputSchema: {
|
|
4357
|
-
part:
|
|
4696
|
+
part: z15.enum(PARTS2).describe("Which part of the setup"),
|
|
4358
4697
|
...localFiles ? {
|
|
4359
|
-
directory:
|
|
4698
|
+
directory: z15.string().optional().describe(
|
|
4360
4699
|
"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
4700
|
)
|
|
4362
4701
|
} : {},
|
|
4363
|
-
files:
|
|
4702
|
+
files: z15.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
|
|
4364
4703
|
},
|
|
4365
4704
|
annotations: { readOnlyHint: false }
|
|
4366
4705
|
},
|
|
@@ -4396,7 +4735,7 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
4396
4735
|
title: "Resolve Person",
|
|
4397
4736
|
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
4737
|
inputSchema: {
|
|
4399
|
-
id:
|
|
4738
|
+
id: z15.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
|
|
4400
4739
|
},
|
|
4401
4740
|
annotations: { readOnlyHint: true }
|
|
4402
4741
|
},
|
|
@@ -4413,8 +4752,8 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
4413
4752
|
title: "Sync Memory",
|
|
4414
4753
|
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
4754
|
inputSchema: {
|
|
4416
|
-
project:
|
|
4417
|
-
dryRun:
|
|
4755
|
+
project: z15.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"),
|
|
4756
|
+
dryRun: z15.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
|
|
4418
4757
|
},
|
|
4419
4758
|
annotations: { readOnlyHint: false }
|
|
4420
4759
|
},
|
|
@@ -4460,6 +4799,7 @@ function createServer(apiClient2, { localFiles = false } = {}) {
|
|
|
4460
4799
|
registerGetTechStack(server2, apiClient2);
|
|
4461
4800
|
registerGetPlanningContext(server2, apiClient2);
|
|
4462
4801
|
registerGetPlanBoard(server2, apiClient2);
|
|
4802
|
+
registerGetChange(server2, apiClient2);
|
|
4463
4803
|
registerGetContext(server2, apiClient2);
|
|
4464
4804
|
registerGetProjectRules(server2, apiClient2);
|
|
4465
4805
|
registerGetPromptTemplates(server2, apiClient2);
|
|
@@ -4482,6 +4822,7 @@ var init_server = __esm({
|
|
|
4482
4822
|
init_get_tech_stack();
|
|
4483
4823
|
init_get_planning_context();
|
|
4484
4824
|
init_get_plan_board();
|
|
4825
|
+
init_get_change();
|
|
4485
4826
|
init_get_context();
|
|
4486
4827
|
init_create_guardrail();
|
|
4487
4828
|
init_update_spec();
|
|
@@ -4492,7 +4833,7 @@ var init_server = __esm({
|
|
|
4492
4833
|
init_create_prompt_template();
|
|
4493
4834
|
init_skills2();
|
|
4494
4835
|
init_team_setup();
|
|
4495
|
-
SERVER_VERSION = true ? "0.1.
|
|
4836
|
+
SERVER_VERSION = true ? "0.1.49" : "0.0.0-dev";
|
|
4496
4837
|
}
|
|
4497
4838
|
});
|
|
4498
4839
|
|
|
@@ -4511,13 +4852,18 @@ async function main() {
|
|
|
4511
4852
|
}
|
|
4512
4853
|
var config, apiClient, server;
|
|
4513
4854
|
var init_index = __esm({
|
|
4514
|
-
"src/index.ts"() {
|
|
4855
|
+
async "src/index.ts"() {
|
|
4515
4856
|
"use strict";
|
|
4516
|
-
init_config();
|
|
4517
4857
|
init_api_client();
|
|
4858
|
+
init_team_setup_files();
|
|
4518
4859
|
init_server();
|
|
4519
4860
|
init_server();
|
|
4520
|
-
|
|
4861
|
+
try {
|
|
4862
|
+
config = await loadLocalConfig();
|
|
4863
|
+
} catch (err) {
|
|
4864
|
+
console.error(`SystemDox MCP server: ${err.message}`);
|
|
4865
|
+
process.exit(1);
|
|
4866
|
+
}
|
|
4521
4867
|
apiClient = new SystemDoxApiClient(config);
|
|
4522
4868
|
server = createServer(apiClient, { localFiles: true });
|
|
4523
4869
|
main();
|
|
@@ -4527,10 +4873,6 @@ var init_index = __esm({
|
|
|
4527
4873
|
// src/cli.ts
|
|
4528
4874
|
init_config();
|
|
4529
4875
|
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
4876
|
import { createInterface } from "readline";
|
|
4535
4877
|
function prompt(question) {
|
|
4536
4878
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -4541,158 +4883,10 @@ function prompt(question) {
|
|
|
4541
4883
|
});
|
|
4542
4884
|
});
|
|
4543
4885
|
}
|
|
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
4886
|
var args = process.argv.slice(2);
|
|
4693
4887
|
var command = args[0];
|
|
4694
4888
|
if (command === "setup") {
|
|
4695
|
-
Promise.resolve().then(() => (init_setup_team(), setup_team_exports)).then(async ({ parseSetupArgs: parseSetupArgs2, runTeamSetupCommand: runTeamSetupCommand2 }) => {
|
|
4889
|
+
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
4890
|
let teamArgs;
|
|
4697
4891
|
try {
|
|
4698
4892
|
teamArgs = parseSetupArgs2(args.slice(1));
|
|
@@ -4702,8 +4896,13 @@ if (command === "setup") {
|
|
|
4702
4896
|
`);
|
|
4703
4897
|
process.exit(1);
|
|
4704
4898
|
}
|
|
4705
|
-
const
|
|
4706
|
-
|
|
4899
|
+
const step = await setupMcpServer2({
|
|
4900
|
+
out: console.log,
|
|
4901
|
+
err: console.error,
|
|
4902
|
+
ask: (question) => prompt(question)
|
|
4903
|
+
});
|
|
4904
|
+
if (step.code !== 0) return step.code;
|
|
4905
|
+
return runTeamSetupCommand2(teamArgs, step, {
|
|
4707
4906
|
out: console.log,
|
|
4708
4907
|
ask: (question) => prompt(question),
|
|
4709
4908
|
terminal: !!process.stdin.isTTY
|
|
@@ -4763,15 +4962,18 @@ if (command === "setup") {
|
|
|
4763
4962
|
systemdox-mcp-server --help Show this help message
|
|
4764
4963
|
|
|
4765
4964
|
Environment variables:
|
|
4766
|
-
SYSTEMDOX_API_KEY Your SystemDox API key
|
|
4767
|
-
|
|
4768
|
-
|
|
4965
|
+
SYSTEMDOX_API_KEY Your SystemDox API key. When it is not set, every
|
|
4966
|
+
command reads ~/.claude/systemdox/credentials.json,
|
|
4967
|
+
where setup stores the key (and nowhere else: not
|
|
4968
|
+
in your AI tool's MCP config)
|
|
4969
|
+
SYSTEMDOX_WORKSPACE The workspace whose key to use when
|
|
4970
|
+
credentials.json holds several
|
|
4769
4971
|
SYSTEMDOX_API_URL API URL (default: ${API_URL_DEFAULT})
|
|
4770
4972
|
SYSTEMDOX_CACHE_TTL Cache TTL in seconds (default: 300)
|
|
4771
4973
|
SYSTEMDOX_DEFAULT_ORG Default GitHub org
|
|
4772
4974
|
`);
|
|
4773
4975
|
} else if (!command) {
|
|
4774
|
-
|
|
4976
|
+
init_index();
|
|
4775
4977
|
} else {
|
|
4776
4978
|
console.error(` Unknown command: ${command}
|
|
4777
4979
|
Run 'systemdox-mcp-server --help' for usage.`);
|