@systemdox/mcp-server 0.1.44 → 0.1.46
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 +46 -0
- package/dist/cli.js +793 -110
- package/dist/http.js +356 -69
- package/dist/index.js +356 -69
- package/dist/install-skill.js +117 -12
- package/dist/lib/api-client.d.ts +81 -0
- package/dist/lib/api-client.js +119 -13
- package/dist/lib/person-values.d.ts +78 -0
- package/dist/lib/person-values.js +99 -0
- package/dist/lib/skills.d.ts +20 -0
- package/dist/lib/skills.js +18 -13
- package/dist/lib/team-setup-files.d.ts +88 -0
- package/dist/lib/team-setup-files.js +191 -0
- package/dist/lib/team-setup-types.d.ts +153 -0
- package/dist/plugin-path.d.ts +55 -0
- package/dist/plugin-path.js +544 -0
- package/dist/tools/skills.js +15 -12
- package/dist/tools/team-setup.d.ts +33 -0
- package/dist/tools/team-setup.js +242 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -51,6 +51,146 @@ var init_config = __esm({
|
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
// src/lib/team-setup-files.ts
|
|
55
|
+
import { execFile } from "child_process";
|
|
56
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
57
|
+
import { homedir, userInfo } from "os";
|
|
58
|
+
import { dirname, join } from "path";
|
|
59
|
+
import { promisify } from "util";
|
|
60
|
+
function isWorkspace(value) {
|
|
61
|
+
return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
|
|
62
|
+
}
|
|
63
|
+
function homeDir() {
|
|
64
|
+
return homedir();
|
|
65
|
+
}
|
|
66
|
+
function systemdoxDir() {
|
|
67
|
+
return join(homeDir(), ".claude", "systemdox");
|
|
68
|
+
}
|
|
69
|
+
function credentialsPath() {
|
|
70
|
+
return join(systemdoxDir(), "credentials.json");
|
|
71
|
+
}
|
|
72
|
+
function workspaceDir(workspace) {
|
|
73
|
+
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
74
|
+
return join(systemdoxDir(), workspace);
|
|
75
|
+
}
|
|
76
|
+
function statePath(workspace) {
|
|
77
|
+
return join(workspaceDir(workspace), "state.json");
|
|
78
|
+
}
|
|
79
|
+
function pluginRoot(workspace) {
|
|
80
|
+
return join(workspaceDir(workspace), "plugin");
|
|
81
|
+
}
|
|
82
|
+
function pluginDir(workspace, version) {
|
|
83
|
+
return join(pluginRoot(workspace), String(version));
|
|
84
|
+
}
|
|
85
|
+
async function readJson(path, empty) {
|
|
86
|
+
let text3;
|
|
87
|
+
try {
|
|
88
|
+
text3 = await readFile(path, "utf8");
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e.code === "ENOENT") return empty;
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(text3);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function writeJsonAtomically(path, value, mode) {
|
|
100
|
+
await mkdir(dirname(path), { recursive: true });
|
|
101
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
102
|
+
await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
|
|
103
|
+
await rename(tmp, path);
|
|
104
|
+
}
|
|
105
|
+
async function readCredentials() {
|
|
106
|
+
return readJson(credentialsPath(), {});
|
|
107
|
+
}
|
|
108
|
+
async function apiKeyFor(workspace, env = process.env) {
|
|
109
|
+
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
110
|
+
const entry = (await readCredentials())[workspace];
|
|
111
|
+
return typeof entry?.apiKey === "string" && entry.apiKey ? entry.apiKey : void 0;
|
|
112
|
+
}
|
|
113
|
+
function apiUrlFrom(env = process.env) {
|
|
114
|
+
return requireHttpsApiUrl(env.SYSTEMDOX_API_URL || API_URL_DEFAULT);
|
|
115
|
+
}
|
|
116
|
+
async function readState(workspace) {
|
|
117
|
+
return readJson(statePath(workspace), {});
|
|
118
|
+
}
|
|
119
|
+
async function writeState(workspace, state) {
|
|
120
|
+
await writeJsonAtomically(statePath(workspace), state);
|
|
121
|
+
}
|
|
122
|
+
async function updateState(workspace, change) {
|
|
123
|
+
const state = await readState(workspace);
|
|
124
|
+
change(state);
|
|
125
|
+
await writeState(workspace, state);
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
async function removeFolder(path) {
|
|
129
|
+
await rm(path, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
function escapeRegExp(s) {
|
|
132
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
133
|
+
}
|
|
134
|
+
function marketplaceWorkspace(marketplaceUrl, apiUrl) {
|
|
135
|
+
let url;
|
|
136
|
+
let api;
|
|
137
|
+
try {
|
|
138
|
+
url = new URL(marketplaceUrl);
|
|
139
|
+
api = new URL(requireHttpsApiUrl(apiUrl));
|
|
140
|
+
} catch {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
if (url.protocol !== "https:" || url.origin !== api.origin || url.username || url.password) return void 0;
|
|
144
|
+
const base = api.pathname.replace(/\/+$/, "");
|
|
145
|
+
const m = new RegExp(`^${escapeRegExp(base)}/v1/team-setup/([^/]+)/marketplace\\.json$`).exec(url.pathname);
|
|
146
|
+
return m && isWorkspace(m[1]) ? m[1] : void 0;
|
|
147
|
+
}
|
|
148
|
+
async function runMarketplaceAuth(io, env = process.env) {
|
|
149
|
+
const url = env.CLAUDE_CODE_MARKETPLACE_URL;
|
|
150
|
+
if (!url) {
|
|
151
|
+
io.err("marketplace-auth: CLAUDE_CODE_MARKETPLACE_URL is not set; Claude Code sets it when it fetches a marketplace.");
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
let apiUrl;
|
|
155
|
+
try {
|
|
156
|
+
apiUrl = apiUrlFrom(env);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
io.err(`marketplace-auth: ${e.message}`);
|
|
159
|
+
return 1;
|
|
160
|
+
}
|
|
161
|
+
const workspace = marketplaceWorkspace(url, apiUrl);
|
|
162
|
+
if (!workspace) {
|
|
163
|
+
io.err(
|
|
164
|
+
`marketplace-auth: refusing to give a SystemDox key for ${url}: it is not a SystemDox team marketplace on ${apiUrl}`
|
|
165
|
+
);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
let key;
|
|
169
|
+
try {
|
|
170
|
+
key = await apiKeyFor(workspace, env);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
io.err(`marketplace-auth: ${e.message}`);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
if (!key) {
|
|
176
|
+
io.err(
|
|
177
|
+
`marketplace-auth: no SystemDox key for ${workspace}. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.`
|
|
178
|
+
);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
io.out(JSON.stringify({ Authorization: `Bearer ${key}` }));
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
var SLUG_RE, run;
|
|
185
|
+
var init_team_setup_files = __esm({
|
|
186
|
+
"src/lib/team-setup-files.ts"() {
|
|
187
|
+
"use strict";
|
|
188
|
+
init_config();
|
|
189
|
+
SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
190
|
+
run = promisify(execFile);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
54
194
|
// src/lib/cache.ts
|
|
55
195
|
var ResponseCache;
|
|
56
196
|
var init_cache = __esm({
|
|
@@ -88,11 +228,33 @@ var init_cache = __esm({
|
|
|
88
228
|
});
|
|
89
229
|
|
|
90
230
|
// src/lib/api-client.ts
|
|
91
|
-
var SystemDoxApiClient;
|
|
231
|
+
var SystemDoxApiError, TEAM_SETUP, seg, SystemDoxApiClient;
|
|
92
232
|
var init_api_client = __esm({
|
|
93
233
|
"src/lib/api-client.ts"() {
|
|
94
234
|
"use strict";
|
|
95
235
|
init_cache();
|
|
236
|
+
SystemDoxApiError = class extends Error {
|
|
237
|
+
constructor(status, text3) {
|
|
238
|
+
super(`SystemDox API error ${status}: ${text3}`);
|
|
239
|
+
this.status = status;
|
|
240
|
+
this.text = text3;
|
|
241
|
+
let body;
|
|
242
|
+
try {
|
|
243
|
+
body = JSON.parse(text3);
|
|
244
|
+
} catch {
|
|
245
|
+
body = void 0;
|
|
246
|
+
}
|
|
247
|
+
this.body = body;
|
|
248
|
+
const code = body?.code;
|
|
249
|
+
this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text3)?.[1];
|
|
250
|
+
}
|
|
251
|
+
status;
|
|
252
|
+
text;
|
|
253
|
+
code;
|
|
254
|
+
body;
|
|
255
|
+
};
|
|
256
|
+
TEAM_SETUP = "/v1/team-setup";
|
|
257
|
+
seg = (s) => encodeURIComponent(String(s));
|
|
96
258
|
SystemDoxApiClient = class {
|
|
97
259
|
config;
|
|
98
260
|
cache;
|
|
@@ -114,10 +276,7 @@ var init_api_client = __esm({
|
|
|
114
276
|
const cached = this.cache.get(cacheKey);
|
|
115
277
|
if (cached !== void 0) return cached;
|
|
116
278
|
const res = await this.fetchRaw(path, params);
|
|
117
|
-
if (!res.ok)
|
|
118
|
-
const body = await res.text();
|
|
119
|
-
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
120
|
-
}
|
|
279
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
121
280
|
const data = await res.json();
|
|
122
281
|
this.cache.set(cacheKey, data);
|
|
123
282
|
return data;
|
|
@@ -126,10 +285,7 @@ var init_api_client = __esm({
|
|
|
126
285
|
* latest now, not the one cached before someone published another. */
|
|
127
286
|
async getFresh(path) {
|
|
128
287
|
const res = await this.fetchRaw(path);
|
|
129
|
-
if (!res.ok)
|
|
130
|
-
const body = await res.text();
|
|
131
|
-
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
132
|
-
}
|
|
288
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
133
289
|
return await res.json();
|
|
134
290
|
}
|
|
135
291
|
async post(path, body) {
|
|
@@ -151,12 +307,101 @@ var init_api_client = __esm({
|
|
|
151
307
|
},
|
|
152
308
|
body: JSON.stringify(body)
|
|
153
309
|
});
|
|
154
|
-
if (!res.ok)
|
|
155
|
-
const text2 = await res.text();
|
|
156
|
-
throw new Error(`SystemDox API error ${res.status}: ${text2}`);
|
|
157
|
-
}
|
|
310
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
158
311
|
return await res.json();
|
|
159
312
|
}
|
|
313
|
+
// ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
|
|
314
|
+
// One method per operationId. Every read skips the cache: setup, sync and
|
|
315
|
+
// plugin-path act on what the workspace holds now.
|
|
316
|
+
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
317
|
+
getOwnTeamSetup() {
|
|
318
|
+
return this.getFresh(TEAM_SETUP);
|
|
319
|
+
}
|
|
320
|
+
getTeamSetup(workspace) {
|
|
321
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
322
|
+
}
|
|
323
|
+
getMarketplace(workspace) {
|
|
324
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
|
|
325
|
+
}
|
|
326
|
+
getPart(workspace, part) {
|
|
327
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
|
|
328
|
+
}
|
|
329
|
+
publishPartVersion(workspace, part, files) {
|
|
330
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
|
|
331
|
+
}
|
|
332
|
+
getPartVersion(workspace, part, version) {
|
|
333
|
+
return this.getFresh(
|
|
334
|
+
`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
getSetupSkills(workspace) {
|
|
338
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
|
|
339
|
+
}
|
|
340
|
+
setSetupSkills(workspace, skills) {
|
|
341
|
+
return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
|
|
342
|
+
}
|
|
343
|
+
getPlugin(workspace) {
|
|
344
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
|
|
345
|
+
}
|
|
346
|
+
/** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
|
|
347
|
+
* header. The caller checks the BYTES against the hash it was given. */
|
|
348
|
+
async getPluginZip(workspace, version) {
|
|
349
|
+
const res = await fetch(
|
|
350
|
+
this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
|
|
351
|
+
{
|
|
352
|
+
headers: {
|
|
353
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
354
|
+
"X-API-Key": this.config.apiKey,
|
|
355
|
+
Accept: "application/zip"
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
);
|
|
359
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
360
|
+
return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
|
|
361
|
+
}
|
|
362
|
+
listMemoryProjects(workspace) {
|
|
363
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
|
|
364
|
+
}
|
|
365
|
+
getMemory(workspace, project) {
|
|
366
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
|
|
367
|
+
}
|
|
368
|
+
/** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
|
|
369
|
+
* code SYD-K010 and `body.currentVersion`. */
|
|
370
|
+
pushMemoryVersion(workspace, project, body) {
|
|
371
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
|
|
372
|
+
}
|
|
373
|
+
getMemoryVersion(workspace, project, version) {
|
|
374
|
+
return this.getFresh(
|
|
375
|
+
`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
findMissingBlobs(workspace, project, hashes) {
|
|
379
|
+
return this.post(
|
|
380
|
+
`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
|
|
381
|
+
{ hashes }
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
uploadBlobs(workspace, project, blobs) {
|
|
385
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
|
|
386
|
+
}
|
|
387
|
+
readBlobs(workspace, project, hashes) {
|
|
388
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
|
|
389
|
+
}
|
|
390
|
+
listValueAnswers(workspace) {
|
|
391
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
|
|
392
|
+
}
|
|
393
|
+
lookupValues(workspace, values) {
|
|
394
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
|
|
395
|
+
}
|
|
396
|
+
answerValues(workspace, answers) {
|
|
397
|
+
return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
|
|
398
|
+
}
|
|
399
|
+
fileMemoryPeople(workspace, people) {
|
|
400
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people });
|
|
401
|
+
}
|
|
402
|
+
resolvePerson(workspace, personId) {
|
|
403
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
|
|
404
|
+
}
|
|
160
405
|
invalidateCache(path) {
|
|
161
406
|
if (path) {
|
|
162
407
|
this.cache.invalidate(this.buildUrl(path).toString());
|
|
@@ -190,10 +435,10 @@ var init_api_client = __esm({
|
|
|
190
435
|
|
|
191
436
|
// src/lib/skills.ts
|
|
192
437
|
import { createHash } from "crypto";
|
|
193
|
-
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
438
|
+
import { lstat, mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
194
439
|
import { existsSync } from "fs";
|
|
195
|
-
import { homedir } from "os";
|
|
196
|
-
import { dirname, join, relative, resolve, sep } from "path";
|
|
440
|
+
import { homedir as homedir2 } from "os";
|
|
441
|
+
import { dirname as dirname2, join as join2, relative, resolve, sep } from "path";
|
|
197
442
|
function sha256(content) {
|
|
198
443
|
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
199
444
|
}
|
|
@@ -210,12 +455,12 @@ function frontmatterName(skillMd) {
|
|
|
210
455
|
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
211
456
|
}
|
|
212
457
|
function defaultSkillDir(name) {
|
|
213
|
-
return
|
|
458
|
+
return join2(homedir2(), ".claude", "skills", name);
|
|
214
459
|
}
|
|
215
460
|
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
216
461
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
217
462
|
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
218
|
-
const full =
|
|
463
|
+
const full = join2(dir, entry.name);
|
|
219
464
|
const rel = relative(root, full).split(sep).join("/");
|
|
220
465
|
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
221
466
|
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
@@ -225,18 +470,9 @@ async function walk(root, { skipHidden }, dir = root, out = { files: [], links:
|
|
|
225
470
|
out.files.sort();
|
|
226
471
|
return out;
|
|
227
472
|
}
|
|
228
|
-
async function
|
|
229
|
-
const root = resolve(directory);
|
|
230
|
-
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
231
|
-
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
232
|
-
}
|
|
233
|
-
const walked = await walk(root, { skipHidden: true });
|
|
234
|
-
if (walked.links.length) {
|
|
235
|
-
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
236
|
-
}
|
|
237
|
-
const paths = walked.files;
|
|
473
|
+
async function readTextFiles(root, paths, what) {
|
|
238
474
|
if (paths.length > MAX_FILES) {
|
|
239
|
-
throw new Error(`${root} holds ${paths.length} files:
|
|
475
|
+
throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
|
|
240
476
|
}
|
|
241
477
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
242
478
|
const files = [];
|
|
@@ -245,8 +481,8 @@ async function readSkillDirectory(directory) {
|
|
|
245
481
|
if (!isValidPath(path)) {
|
|
246
482
|
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
247
483
|
}
|
|
248
|
-
const raw = await
|
|
249
|
-
if (raw.includes(0)) throw new Error(`${path} is binary:
|
|
484
|
+
const raw = await readFile2(join2(root, ...path.split("/")));
|
|
485
|
+
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
250
486
|
if (raw.length > MAX_FILE_BYTES) {
|
|
251
487
|
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
252
488
|
}
|
|
@@ -262,6 +498,18 @@ async function readSkillDirectory(directory) {
|
|
|
262
498
|
}
|
|
263
499
|
files.push({ path, content });
|
|
264
500
|
}
|
|
501
|
+
return files;
|
|
502
|
+
}
|
|
503
|
+
async function readSkillDirectory(directory) {
|
|
504
|
+
const root = resolve(directory);
|
|
505
|
+
if (!existsSync(join2(root, ENTRY_FILE))) {
|
|
506
|
+
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
507
|
+
}
|
|
508
|
+
const walked = await walk(root, { skipHidden: true });
|
|
509
|
+
if (walked.links.length) {
|
|
510
|
+
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
511
|
+
}
|
|
512
|
+
const files = await readTextFiles(root, walked.files, "a skill");
|
|
265
513
|
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
266
514
|
const name = frontmatterName(skillMd.content);
|
|
267
515
|
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
@@ -275,7 +523,7 @@ function within(dest, path) {
|
|
|
275
523
|
}
|
|
276
524
|
async function readManifest(dest) {
|
|
277
525
|
try {
|
|
278
|
-
return JSON.parse(await
|
|
526
|
+
return JSON.parse(await readFile2(join2(dest, MANIFEST_FILE), "utf8"));
|
|
279
527
|
} catch {
|
|
280
528
|
return void 0;
|
|
281
529
|
}
|
|
@@ -299,7 +547,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
299
547
|
const edited = [];
|
|
300
548
|
const untracked = [];
|
|
301
549
|
for (const path of present) {
|
|
302
|
-
const content = await
|
|
550
|
+
const content = await readFile2(join2(dest, ...path.split("/")), "utf8");
|
|
303
551
|
if (Object.hasOwn(known, path)) {
|
|
304
552
|
if (sha256(content) !== known[path]) edited.push(path);
|
|
305
553
|
} else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
|
|
@@ -312,8 +560,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
312
560
|
const written = [];
|
|
313
561
|
for (const f of version.files) {
|
|
314
562
|
const full = within(dest, f.path);
|
|
315
|
-
await
|
|
316
|
-
await
|
|
563
|
+
await mkdir2(dirname2(full), { recursive: true });
|
|
564
|
+
await writeFile2(full, f.content, "utf8");
|
|
317
565
|
written.push(f.path);
|
|
318
566
|
}
|
|
319
567
|
const removed = [];
|
|
@@ -324,7 +572,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
324
572
|
keptEdited.push(path);
|
|
325
573
|
continue;
|
|
326
574
|
}
|
|
327
|
-
await
|
|
575
|
+
await rm2(within(dest, path));
|
|
328
576
|
removed.push(path);
|
|
329
577
|
}
|
|
330
578
|
const next = {
|
|
@@ -333,7 +581,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
333
581
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
334
582
|
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
335
583
|
};
|
|
336
|
-
await
|
|
584
|
+
await writeFile2(join2(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
337
585
|
return {
|
|
338
586
|
dest,
|
|
339
587
|
version: version.version,
|
|
@@ -343,8 +591,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
343
591
|
keptEdited
|
|
344
592
|
};
|
|
345
593
|
}
|
|
346
|
-
function fenceFor(
|
|
347
|
-
const longest = Math.max(0, ...(
|
|
594
|
+
function fenceFor(text3) {
|
|
595
|
+
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
348
596
|
return "`".repeat(Math.max(3, longest + 1));
|
|
349
597
|
}
|
|
350
598
|
function describeInstall(name, r) {
|
|
@@ -469,6 +717,226 @@ var init_install_skill = __esm({
|
|
|
469
717
|
}
|
|
470
718
|
});
|
|
471
719
|
|
|
720
|
+
// src/plugin-path.ts
|
|
721
|
+
var plugin_path_exports = {};
|
|
722
|
+
__export(plugin_path_exports, {
|
|
723
|
+
MAX_UNPACKED_BYTES: () => MAX_UNPACKED_BYTES,
|
|
724
|
+
PLUGIN_PATH_USAGE: () => PLUGIN_PATH_USAGE,
|
|
725
|
+
PluginRefused: () => PluginRefused,
|
|
726
|
+
parsePluginPathArgs: () => parsePluginPathArgs,
|
|
727
|
+
readZipEntries: () => readZipEntries,
|
|
728
|
+
runPluginPath: () => runPluginPath,
|
|
729
|
+
unpackZip: () => unpackZip
|
|
730
|
+
});
|
|
731
|
+
import { createHash as createHash2, randomBytes } from "crypto";
|
|
732
|
+
import { existsSync as existsSync2 } from "fs";
|
|
733
|
+
import { mkdir as mkdir3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
|
|
734
|
+
import { dirname as dirname3, join as join3, resolve as resolve3, sep as sep2 } from "path";
|
|
735
|
+
import { inflateRawSync } from "zlib";
|
|
736
|
+
function parsePluginPathArgs(argv) {
|
|
737
|
+
const args2 = {};
|
|
738
|
+
for (let i = 0; i < argv.length; i++) {
|
|
739
|
+
const a = argv[i];
|
|
740
|
+
if (a === "--version" || a === "--sha256") {
|
|
741
|
+
const value = argv[++i];
|
|
742
|
+
if (!value) throw new Error(`${a} needs a value
|
|
743
|
+
${PLUGIN_PATH_USAGE}`);
|
|
744
|
+
if (a === "--sha256") {
|
|
745
|
+
if (!SHA256_RE.test(value)) throw new Error(`--sha256 must be 64 lowercase hex characters, got ${value}`);
|
|
746
|
+
args2.sha256 = value;
|
|
747
|
+
} else if (/^[1-9]\d{0,8}$/.test(value)) args2.version = Number(value);
|
|
748
|
+
else throw new Error(`--version must be a whole number, got ${value}`);
|
|
749
|
+
} else if (a.startsWith("-")) throw new Error(`Unknown option ${a}
|
|
750
|
+
${PLUGIN_PATH_USAGE}`);
|
|
751
|
+
else if (!args2.workspace) args2.workspace = a;
|
|
752
|
+
else throw new Error(`Unexpected argument ${a}
|
|
753
|
+
${PLUGIN_PATH_USAGE}`);
|
|
754
|
+
}
|
|
755
|
+
if (!args2.workspace || !args2.version || !args2.sha256) throw new Error(PLUGIN_PATH_USAGE);
|
|
756
|
+
if (!isWorkspace(args2.workspace)) throw new Error(`${args2.workspace} is not a workspace name`);
|
|
757
|
+
return args2;
|
|
758
|
+
}
|
|
759
|
+
function refuse(name, why) {
|
|
760
|
+
throw new PluginRefused(`refused the plugin entry ${JSON.stringify(name)}: it ${why}. Nothing was unpacked.`);
|
|
761
|
+
}
|
|
762
|
+
function readZipEntries(zip) {
|
|
763
|
+
let eocd = -1;
|
|
764
|
+
for (let i = zip.length - 22; i >= Math.max(0, zip.length - 22 - 65535); i--) {
|
|
765
|
+
if (zip.readUInt32LE(i) === EOCD) {
|
|
766
|
+
eocd = i;
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (eocd < 0) throw new PluginRefused("the download is not a zip. Nothing was unpacked.");
|
|
771
|
+
const count = zip.readUInt16LE(eocd + 10);
|
|
772
|
+
let at = zip.readUInt32LE(eocd + 16);
|
|
773
|
+
if (count === 65535 || at === 4294967295) throw new PluginRefused("the zip is a ZIP64 archive, which a plugin never is. Nothing was unpacked.");
|
|
774
|
+
const entries = [];
|
|
775
|
+
let total = 0;
|
|
776
|
+
for (let n = 0; n < count; n++) {
|
|
777
|
+
if (at + 46 > zip.length || zip.readUInt32LE(at) !== CENTRAL) {
|
|
778
|
+
throw new PluginRefused("the zip's directory is damaged. Nothing was unpacked.");
|
|
779
|
+
}
|
|
780
|
+
const madeBy = zip.readUInt16LE(at + 4);
|
|
781
|
+
const flags = zip.readUInt16LE(at + 8);
|
|
782
|
+
const method = zip.readUInt16LE(at + 10);
|
|
783
|
+
const compressedSize = zip.readUInt32LE(at + 20);
|
|
784
|
+
const size = zip.readUInt32LE(at + 24);
|
|
785
|
+
const nameLength = zip.readUInt16LE(at + 28);
|
|
786
|
+
const extraLength = zip.readUInt16LE(at + 30);
|
|
787
|
+
const commentLength = zip.readUInt16LE(at + 32);
|
|
788
|
+
const external = zip.readUInt32LE(at + 38);
|
|
789
|
+
const offset = zip.readUInt32LE(at + 42);
|
|
790
|
+
const name = zip.subarray(at + 46, at + 46 + nameLength).toString(flags & 2048 ? "utf8" : "latin1");
|
|
791
|
+
at += 46 + nameLength + extraLength + commentLength;
|
|
792
|
+
const unixMode = madeBy >> 8 === 3 ? external >>> 16 : 0;
|
|
793
|
+
const segments = name.split(/[\\/]/);
|
|
794
|
+
if (!name || /^[\\/]/.test(name) || /^[A-Za-z]:/.test(name)) refuse(name, "is absolute");
|
|
795
|
+
if (segments.includes("..")) refuse(name, "climbs out of the plugin folder (..)");
|
|
796
|
+
if (name.includes(":") || name.includes("\0")) refuse(name, "is not a plain relative path");
|
|
797
|
+
if ((unixMode & S_IFMT) === S_IFLNK) refuse(name, "is a link");
|
|
798
|
+
const directory = name.endsWith("/") || (unixMode & S_IFMT) === S_IFDIR;
|
|
799
|
+
if (!directory && unixMode && (unixMode & S_IFMT) !== S_IFREG) refuse(name, "is not a regular file");
|
|
800
|
+
if (flags & 1) refuse(name, "is encrypted");
|
|
801
|
+
if (!directory && method !== 0 && method !== 8) refuse(name, `uses compression method ${method}`);
|
|
802
|
+
total += size;
|
|
803
|
+
if (total > MAX_UNPACKED_BYTES) refuse(name, `takes the unpacked size past ${MAX_UNPACKED_BYTES} bytes`);
|
|
804
|
+
entries.push({ name, directory, mode: unixMode ? unixMode & 511 : void 0, method, compressedSize, size, offset });
|
|
805
|
+
}
|
|
806
|
+
return entries;
|
|
807
|
+
}
|
|
808
|
+
function entryData(zip, entry) {
|
|
809
|
+
const at = entry.offset;
|
|
810
|
+
if (at + 30 > zip.length || zip.readUInt32LE(at) !== LOCAL) refuse(entry.name, "has a damaged header");
|
|
811
|
+
const start = at + 30 + zip.readUInt16LE(at + 26) + zip.readUInt16LE(at + 28);
|
|
812
|
+
const raw = zip.subarray(start, start + entry.compressedSize);
|
|
813
|
+
let data;
|
|
814
|
+
try {
|
|
815
|
+
data = entry.method === 0 ? raw : inflateRawSync(raw, { maxOutputLength: Math.max(1, entry.size) + 1 });
|
|
816
|
+
} catch (e) {
|
|
817
|
+
refuse(entry.name, `cannot be unpacked (${e.message})`);
|
|
818
|
+
}
|
|
819
|
+
if (data.length !== entry.size) refuse(entry.name, "unpacks to a size other than the one it declares");
|
|
820
|
+
return data;
|
|
821
|
+
}
|
|
822
|
+
async function unpackZip(zip, dest) {
|
|
823
|
+
const entries = readZipEntries(zip);
|
|
824
|
+
const root = resolve3(dest);
|
|
825
|
+
const targets = entries.map((e) => {
|
|
826
|
+
const full = resolve3(root, ...e.name.split(/[\\/]/).filter(Boolean));
|
|
827
|
+
if (full !== root && !full.startsWith(root + sep2)) refuse(e.name, "climbs out of the plugin folder");
|
|
828
|
+
return full;
|
|
829
|
+
});
|
|
830
|
+
const data = entries.map((e) => e.directory ? void 0 : entryData(zip, e));
|
|
831
|
+
await mkdir3(root, { recursive: true });
|
|
832
|
+
for (const [i, entry] of entries.entries()) {
|
|
833
|
+
if (entry.directory) {
|
|
834
|
+
await mkdir3(targets[i], { recursive: true });
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
await mkdir3(dirname3(targets[i]), { recursive: true });
|
|
838
|
+
await writeFile3(targets[i], data[i], { mode: entry.mode ?? 420 });
|
|
839
|
+
}
|
|
840
|
+
return entries.filter((e) => !e.directory).length;
|
|
841
|
+
}
|
|
842
|
+
async function runPluginPath(argv, io) {
|
|
843
|
+
let args2;
|
|
844
|
+
try {
|
|
845
|
+
args2 = parsePluginPathArgs(argv);
|
|
846
|
+
} catch (e) {
|
|
847
|
+
io.err(e.message);
|
|
848
|
+
return 1;
|
|
849
|
+
}
|
|
850
|
+
const { workspace, version, sha256: sha2562 } = args2;
|
|
851
|
+
const folder = pluginDir(workspace, version);
|
|
852
|
+
try {
|
|
853
|
+
const state = await readState(workspace);
|
|
854
|
+
if (existsSync2(folder) && state.plugin?.[String(version)]?.sha256 === sha2562) {
|
|
855
|
+
io.out(folder);
|
|
856
|
+
return 0;
|
|
857
|
+
}
|
|
858
|
+
} catch (e) {
|
|
859
|
+
io.err(e.message);
|
|
860
|
+
return 1;
|
|
861
|
+
}
|
|
862
|
+
let client = io.client;
|
|
863
|
+
if (!client) {
|
|
864
|
+
try {
|
|
865
|
+
const apiUrl = apiUrlFrom();
|
|
866
|
+
const apiKey = await apiKeyFor(workspace);
|
|
867
|
+
if (!apiKey) {
|
|
868
|
+
io.err(
|
|
869
|
+
`No SystemDox key for ${workspace}. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup, then run this again.`
|
|
870
|
+
);
|
|
871
|
+
return 1;
|
|
872
|
+
}
|
|
873
|
+
client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
874
|
+
} catch (e) {
|
|
875
|
+
io.err(e.message);
|
|
876
|
+
return 1;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
let zip;
|
|
880
|
+
try {
|
|
881
|
+
({ bytes: zip } = await client.getPluginZip(workspace, version));
|
|
882
|
+
} catch (e) {
|
|
883
|
+
io.err(e.message);
|
|
884
|
+
return 1;
|
|
885
|
+
}
|
|
886
|
+
const got = createHash2("sha256").update(zip).digest("hex");
|
|
887
|
+
if (got !== sha2562) {
|
|
888
|
+
io.err(
|
|
889
|
+
`The download of ${workspace}'s team plugin version ${version} has SHA-256 ${got}, not the ${sha2562} Claude Code named. Nothing was unpacked.`
|
|
890
|
+
);
|
|
891
|
+
return 2;
|
|
892
|
+
}
|
|
893
|
+
const tmp = join3(pluginRoot(workspace), `.${version}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
894
|
+
try {
|
|
895
|
+
await unpackZip(zip, tmp);
|
|
896
|
+
if (existsSync2(folder) && (await readState(workspace)).plugin?.[String(version)]?.sha256 === sha2562) {
|
|
897
|
+
await removeFolder(tmp);
|
|
898
|
+
} else {
|
|
899
|
+
if (existsSync2(folder)) await removeFolder(folder);
|
|
900
|
+
try {
|
|
901
|
+
await rename2(tmp, folder);
|
|
902
|
+
} catch (e) {
|
|
903
|
+
if (!existsSync2(folder)) throw e;
|
|
904
|
+
await removeFolder(tmp);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
await updateState(workspace, (state) => {
|
|
908
|
+
state.plugin = { ...state.plugin, [String(version)]: { sha256: sha2562, installedAt: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
909
|
+
});
|
|
910
|
+
} catch (e) {
|
|
911
|
+
await removeFolder(tmp);
|
|
912
|
+
io.err(e.message);
|
|
913
|
+
return e instanceof PluginRefused ? 2 : 1;
|
|
914
|
+
}
|
|
915
|
+
io.err(`Unpacked ${workspace}'s team plugin version ${version}.`);
|
|
916
|
+
io.out(folder);
|
|
917
|
+
return 0;
|
|
918
|
+
}
|
|
919
|
+
var PLUGIN_PATH_USAGE, MAX_UNPACKED_BYTES, SHA256_RE, PluginRefused, EOCD, CENTRAL, LOCAL, S_IFMT, S_IFLNK, S_IFDIR, S_IFREG;
|
|
920
|
+
var init_plugin_path = __esm({
|
|
921
|
+
"src/plugin-path.ts"() {
|
|
922
|
+
"use strict";
|
|
923
|
+
init_api_client();
|
|
924
|
+
init_team_setup_files();
|
|
925
|
+
PLUGIN_PATH_USAGE = "Usage: systemdox-mcp-server plugin-path <workspace> --version <n> --sha256 <hash>";
|
|
926
|
+
MAX_UNPACKED_BYTES = 20 * 1024 * 1024;
|
|
927
|
+
SHA256_RE = /^[0-9a-f]{64}$/;
|
|
928
|
+
PluginRefused = class extends Error {
|
|
929
|
+
};
|
|
930
|
+
EOCD = 101010256;
|
|
931
|
+
CENTRAL = 33639248;
|
|
932
|
+
LOCAL = 67324752;
|
|
933
|
+
S_IFMT = 61440;
|
|
934
|
+
S_IFLNK = 40960;
|
|
935
|
+
S_IFDIR = 16384;
|
|
936
|
+
S_IFREG = 32768;
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
|
|
472
940
|
// src/lib/language.ts
|
|
473
941
|
function detectLanguage(filePath) {
|
|
474
942
|
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
@@ -601,12 +1069,12 @@ function registerGetGuardrails(server2, apiClient2) {
|
|
|
601
1069
|
);
|
|
602
1070
|
}
|
|
603
1071
|
if (filtered.length === 0) {
|
|
604
|
-
const
|
|
1072
|
+
const text4 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient2))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
|
|
605
1073
|
return {
|
|
606
|
-
content: [{ type: "text", text:
|
|
1074
|
+
content: [{ type: "text", text: text4 }]
|
|
607
1075
|
};
|
|
608
1076
|
}
|
|
609
|
-
const
|
|
1077
|
+
const text3 = filtered.map(
|
|
610
1078
|
(g) => `## ${g.title} [${g.severity}]
|
|
611
1079
|
|
|
612
1080
|
${g.description}
|
|
@@ -630,7 +1098,7 @@ ${g.fix}
|
|
|
630
1098
|
type: "text",
|
|
631
1099
|
text: `Found ${filtered.length} check(s):
|
|
632
1100
|
|
|
633
|
-
${
|
|
1101
|
+
${text3}`
|
|
634
1102
|
}
|
|
635
1103
|
]
|
|
636
1104
|
};
|
|
@@ -741,7 +1209,7 @@ ${spec.taskContent}`);
|
|
|
741
1209
|
]
|
|
742
1210
|
};
|
|
743
1211
|
}
|
|
744
|
-
const
|
|
1212
|
+
const text3 = filtered.map(
|
|
745
1213
|
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
746
1214
|
).join("\n");
|
|
747
1215
|
return {
|
|
@@ -750,7 +1218,7 @@ ${spec.taskContent}`);
|
|
|
750
1218
|
type: "text",
|
|
751
1219
|
text: `Found ${filtered.length} spec(s):
|
|
752
1220
|
|
|
753
|
-
${
|
|
1221
|
+
${text3}`
|
|
754
1222
|
}
|
|
755
1223
|
]
|
|
756
1224
|
};
|
|
@@ -808,18 +1276,18 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
808
1276
|
isError: true
|
|
809
1277
|
};
|
|
810
1278
|
}
|
|
811
|
-
let
|
|
1279
|
+
let text4 = doc.content;
|
|
812
1280
|
if (anchor && doc.section_resolved === false) {
|
|
813
|
-
|
|
1281
|
+
text4 = `_Section "${anchor}" not found \u2014 returning the full document._
|
|
814
1282
|
|
|
815
|
-
` +
|
|
1283
|
+
` + text4;
|
|
816
1284
|
} else if (anchor && doc.heading) {
|
|
817
|
-
|
|
1285
|
+
text4 = `## ${doc.heading}
|
|
818
1286
|
|
|
819
1287
|
${doc.content}`;
|
|
820
1288
|
}
|
|
821
1289
|
return {
|
|
822
|
-
content: [{ type: "text", text:
|
|
1290
|
+
content: [{ type: "text", text: text4 }]
|
|
823
1291
|
};
|
|
824
1292
|
}
|
|
825
1293
|
const params = {};
|
|
@@ -843,7 +1311,7 @@ ${doc.content}`;
|
|
|
843
1311
|
]
|
|
844
1312
|
};
|
|
845
1313
|
}
|
|
846
|
-
const
|
|
1314
|
+
const text3 = items.map((d) => {
|
|
847
1315
|
const sections = (d.outline ?? []).slice(0, 8);
|
|
848
1316
|
const sectionHint = sections.length > 0 ? `
|
|
849
1317
|
sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
|
|
@@ -857,7 +1325,7 @@ ${doc.content}`;
|
|
|
857
1325
|
type: "text",
|
|
858
1326
|
text: `Found ${items.length} document(s) (${response.total} total):
|
|
859
1327
|
|
|
860
|
-
${
|
|
1328
|
+
${text3}
|
|
861
1329
|
|
|
862
1330
|
Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
863
1331
|
}
|
|
@@ -1202,13 +1670,13 @@ var init_get_plan_board = __esm({
|
|
|
1202
1670
|
});
|
|
1203
1671
|
|
|
1204
1672
|
// src/lib/scoring.ts
|
|
1205
|
-
function estimateTokens(
|
|
1206
|
-
return Math.ceil(
|
|
1673
|
+
function estimateTokens(text3) {
|
|
1674
|
+
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
1207
1675
|
}
|
|
1208
|
-
function truncateToTokens(
|
|
1676
|
+
function truncateToTokens(text3, maxTokens) {
|
|
1209
1677
|
const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
|
|
1210
|
-
if (
|
|
1211
|
-
const head =
|
|
1678
|
+
if (text3.length <= maxChars) return { text: text3, truncated: false };
|
|
1679
|
+
const head = text3.slice(0, maxChars);
|
|
1212
1680
|
const floor = Math.floor(maxChars * 0.75);
|
|
1213
1681
|
const paragraph = head.lastIndexOf("\n\n");
|
|
1214
1682
|
const line = head.lastIndexOf("\n");
|
|
@@ -1253,7 +1721,7 @@ function scoreSeverity(item) {
|
|
|
1253
1721
|
if (!item.severity) return 0.5;
|
|
1254
1722
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
1255
1723
|
}
|
|
1256
|
-
function extractKeywords(
|
|
1724
|
+
function extractKeywords(text3) {
|
|
1257
1725
|
const stopWords = /* @__PURE__ */ new Set([
|
|
1258
1726
|
"the",
|
|
1259
1727
|
"a",
|
|
@@ -1338,7 +1806,7 @@ function extractKeywords(text2) {
|
|
|
1338
1806
|
"implement",
|
|
1339
1807
|
"create"
|
|
1340
1808
|
]);
|
|
1341
|
-
return
|
|
1809
|
+
return text3.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
1342
1810
|
}
|
|
1343
1811
|
function scoreItems(items, taskDescription, filePaths) {
|
|
1344
1812
|
const languages = detectLanguages(filePaths);
|
|
@@ -1391,20 +1859,20 @@ var init_scoring = __esm({
|
|
|
1391
1859
|
|
|
1392
1860
|
// src/lib/context-builder.ts
|
|
1393
1861
|
function formatGuardrail(g) {
|
|
1394
|
-
let
|
|
1862
|
+
let text3 = `## ${g.title} [${g.severity}]
|
|
1395
1863
|
|
|
1396
1864
|
${g.description}`;
|
|
1397
|
-
if (g.pattern)
|
|
1865
|
+
if (g.pattern) text3 += `
|
|
1398
1866
|
|
|
1399
1867
|
**Bad:** \`\`\`
|
|
1400
1868
|
${g.pattern}
|
|
1401
1869
|
\`\`\``;
|
|
1402
|
-
if (g.fix)
|
|
1870
|
+
if (g.fix) text3 += `
|
|
1403
1871
|
|
|
1404
1872
|
**Good:** \`\`\`
|
|
1405
1873
|
${g.fix}
|
|
1406
1874
|
\`\`\``;
|
|
1407
|
-
return
|
|
1875
|
+
return text3;
|
|
1408
1876
|
}
|
|
1409
1877
|
function formatSpecHeadline(s) {
|
|
1410
1878
|
return `### ${s.name}
|
|
@@ -1426,19 +1894,19 @@ function formatRules(rules) {
|
|
|
1426
1894
|
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
1427
1895
|
byCategory.get(cat).push(r);
|
|
1428
1896
|
}
|
|
1429
|
-
let
|
|
1897
|
+
let text3 = "";
|
|
1430
1898
|
for (const [cat, catRules] of byCategory) {
|
|
1431
|
-
|
|
1899
|
+
text3 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
1432
1900
|
`;
|
|
1433
1901
|
for (const r of catRules) {
|
|
1434
|
-
|
|
1902
|
+
text3 += `- ${r.text}
|
|
1435
1903
|
`;
|
|
1436
1904
|
}
|
|
1437
|
-
|
|
1905
|
+
text3 += "\n";
|
|
1438
1906
|
}
|
|
1439
1907
|
return `## Project Rules (${rules.length})
|
|
1440
1908
|
|
|
1441
|
-
${
|
|
1909
|
+
${text3.trim()}`;
|
|
1442
1910
|
}
|
|
1443
1911
|
async function fetchSpecBody(api, draftId) {
|
|
1444
1912
|
const spec = await api.get(
|
|
@@ -1792,13 +2260,13 @@ function registerGetContext(server2, apiClient2) {
|
|
|
1792
2260
|
}
|
|
1793
2261
|
},
|
|
1794
2262
|
async ({ task_description, file_paths, max_tokens }) => {
|
|
1795
|
-
const { text:
|
|
2263
|
+
const { text: text3 } = await buildContext(apiClient2, {
|
|
1796
2264
|
taskDescription: task_description,
|
|
1797
2265
|
filePaths: file_paths,
|
|
1798
2266
|
maxTokens: max_tokens
|
|
1799
2267
|
});
|
|
1800
2268
|
return {
|
|
1801
|
-
content: [{ type: "text", text:
|
|
2269
|
+
content: [{ type: "text", text: text3 }]
|
|
1802
2270
|
};
|
|
1803
2271
|
}
|
|
1804
2272
|
);
|
|
@@ -2061,9 +2529,9 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
2061
2529
|
);
|
|
2062
2530
|
}
|
|
2063
2531
|
if (filtered.length === 0) {
|
|
2064
|
-
const
|
|
2532
|
+
const text4 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient2))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
|
|
2065
2533
|
return {
|
|
2066
|
-
content: [{ type: "text", text:
|
|
2534
|
+
content: [{ type: "text", text: text4 }]
|
|
2067
2535
|
};
|
|
2068
2536
|
}
|
|
2069
2537
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -2072,25 +2540,25 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
2072
2540
|
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
2073
2541
|
grouped.get(cat).push(rule);
|
|
2074
2542
|
}
|
|
2075
|
-
let
|
|
2543
|
+
let text3 = `Found ${filtered.length} project rule(s):
|
|
2076
2544
|
|
|
2077
2545
|
`;
|
|
2078
2546
|
for (const [cat, catRules] of grouped) {
|
|
2079
2547
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2080
|
-
|
|
2548
|
+
text3 += `## ${label}
|
|
2081
2549
|
|
|
2082
2550
|
`;
|
|
2083
2551
|
for (const rule of catRules) {
|
|
2084
|
-
|
|
2552
|
+
text3 += `- ${rule.text}
|
|
2085
2553
|
`;
|
|
2086
2554
|
}
|
|
2087
|
-
|
|
2555
|
+
text3 += "\n";
|
|
2088
2556
|
}
|
|
2089
2557
|
return {
|
|
2090
2558
|
content: [
|
|
2091
2559
|
{
|
|
2092
2560
|
type: "text",
|
|
2093
|
-
text:
|
|
2561
|
+
text: text3
|
|
2094
2562
|
}
|
|
2095
2563
|
]
|
|
2096
2564
|
};
|
|
@@ -2131,10 +2599,10 @@ function registerCreateProjectRule(server2, apiClient2) {
|
|
|
2131
2599
|
readOnlyHint: false
|
|
2132
2600
|
}
|
|
2133
2601
|
},
|
|
2134
|
-
async ({ text:
|
|
2602
|
+
async ({ text: text3, category }) => {
|
|
2135
2603
|
const saved = await apiClient2.post(
|
|
2136
2604
|
"/v1/standards/rules",
|
|
2137
|
-
{ text:
|
|
2605
|
+
{ text: text3, category }
|
|
2138
2606
|
);
|
|
2139
2607
|
apiClient2.invalidateCache("/v1/standards/rules");
|
|
2140
2608
|
return {
|
|
@@ -2177,7 +2645,7 @@ function registerGetPromptTemplates(server2, apiClient2) {
|
|
|
2177
2645
|
]
|
|
2178
2646
|
};
|
|
2179
2647
|
}
|
|
2180
|
-
const
|
|
2648
|
+
const text3 = templates.map(
|
|
2181
2649
|
(t) => `## ${t.name}
|
|
2182
2650
|
|
|
2183
2651
|
` + (t.description ? `*${t.description}*
|
|
@@ -2192,7 +2660,7 @@ ${t.prompt}
|
|
|
2192
2660
|
type: "text",
|
|
2193
2661
|
text: `Found ${templates.length} prompt template(s):
|
|
2194
2662
|
|
|
2195
|
-
${
|
|
2663
|
+
${text3}`
|
|
2196
2664
|
}
|
|
2197
2665
|
]
|
|
2198
2666
|
};
|
|
@@ -2252,10 +2720,10 @@ var init_create_prompt_template = __esm({
|
|
|
2252
2720
|
|
|
2253
2721
|
// src/tools/skills.ts
|
|
2254
2722
|
import { z as z13 } from "zod";
|
|
2255
|
-
import { homedir as
|
|
2256
|
-
import { basename as basename2, resolve as
|
|
2723
|
+
import { homedir as homedir3 } from "os";
|
|
2724
|
+
import { basename as basename2, resolve as resolve4 } from "path";
|
|
2257
2725
|
function expandHome(path) {
|
|
2258
|
-
return path.replace(/^~(?=$|[\\/])/,
|
|
2726
|
+
return path.replace(/^~(?=$|[\\/])/, homedir3());
|
|
2259
2727
|
}
|
|
2260
2728
|
function text(t) {
|
|
2261
2729
|
return { content: [{ type: "text", text: t }] };
|
|
@@ -2357,7 +2825,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2357
2825
|
);
|
|
2358
2826
|
if (localFiles) {
|
|
2359
2827
|
const dest2 = args2.target ? expandHome(args2.target) : defaultSkillDir(version.name);
|
|
2360
|
-
if (basename2(
|
|
2828
|
+
if (basename2(resolve4(dest2)) !== version.name) {
|
|
2361
2829
|
throw new Error(`target must be a folder named ${version.name} (e.g. .claude/skills/${version.name}); got ${dest2}`);
|
|
2362
2830
|
}
|
|
2363
2831
|
const result = await installSkill(version, dest2, { overwrite: args2.overwrite });
|
|
@@ -2401,6 +2869,203 @@ var init_skills2 = __esm({
|
|
|
2401
2869
|
}
|
|
2402
2870
|
});
|
|
2403
2871
|
|
|
2872
|
+
// src/lib/person-values.ts
|
|
2873
|
+
var W, D, WORD_OR, PERSON_ID, PERSON_TOKEN, BEFORE, AFTER, E164, NATIONAL, ATEXT, EMAIL_RE, HOME_PREFIX, HOME, KEY_PREFIX;
|
|
2874
|
+
var init_person_values = __esm({
|
|
2875
|
+
"src/lib/person-values.ts"() {
|
|
2876
|
+
"use strict";
|
|
2877
|
+
W = "[\\p{L}\\p{N}_]";
|
|
2878
|
+
D = "\\p{Nd}";
|
|
2879
|
+
WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
|
|
2880
|
+
PERSON_ID = "(?:con|ctc|usr)_[0-9A-Za-z]{10,40}";
|
|
2881
|
+
PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
|
|
2882
|
+
BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
|
|
2883
|
+
AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
|
|
2884
|
+
E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
|
|
2885
|
+
NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
|
|
2886
|
+
ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
|
|
2887
|
+
EMAIL_RE = "(?<![A-Za-z0-9!#$%&*+/=?^_{|}~.@\\-])[A-Za-z0-9_]" + ATEXT + "*(?:\\." + ATEXT + "+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}(?![A-Za-z0-9\\-]|\\.[A-Za-z0-9])";
|
|
2888
|
+
HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
|
|
2889
|
+
HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
|
|
2890
|
+
KEY_PREFIX = `(?<!${W})(?:pweb_[a-z]+_key_|sk-ant-|sk_live_|rk_live_|github_pat_|gh[pousr]_|sntrys_|xox[baprs]-)[A-Za-z0-9_\\-]{4,}`;
|
|
2891
|
+
}
|
|
2892
|
+
});
|
|
2893
|
+
|
|
2894
|
+
// src/lib/team-setup-types.ts
|
|
2895
|
+
var PARTS, PERSON_ID_RE;
|
|
2896
|
+
var init_team_setup_types = __esm({
|
|
2897
|
+
"src/lib/team-setup-types.ts"() {
|
|
2898
|
+
"use strict";
|
|
2899
|
+
init_person_values();
|
|
2900
|
+
PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
2901
|
+
PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
2902
|
+
}
|
|
2903
|
+
});
|
|
2904
|
+
|
|
2905
|
+
// src/tools/team-setup.ts
|
|
2906
|
+
import { z as z14 } from "zod";
|
|
2907
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2908
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
2909
|
+
import { homedir as homedir4 } from "os";
|
|
2910
|
+
import { join as join4, resolve as resolve5 } from "path";
|
|
2911
|
+
function expandHome2(path) {
|
|
2912
|
+
return path.replace(/^~(?=$|[\\/])/, homedir4());
|
|
2913
|
+
}
|
|
2914
|
+
function text2(t) {
|
|
2915
|
+
return { content: [{ type: "text", text: t }] };
|
|
2916
|
+
}
|
|
2917
|
+
async function isLink(path) {
|
|
2918
|
+
try {
|
|
2919
|
+
return (await lstat2(path)).isSymbolicLink();
|
|
2920
|
+
} catch (e) {
|
|
2921
|
+
if (e.code === "ENOENT") return false;
|
|
2922
|
+
throw e;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
async function readPartDirectory(part, directory) {
|
|
2926
|
+
const root = resolve5(directory);
|
|
2927
|
+
const layout = LAYOUT[part];
|
|
2928
|
+
const paths = [];
|
|
2929
|
+
const leftOut = [];
|
|
2930
|
+
const links = [];
|
|
2931
|
+
for (const path of layout.files) {
|
|
2932
|
+
const segments = path.split("/");
|
|
2933
|
+
let present = true;
|
|
2934
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
2935
|
+
const rel = segments.slice(0, i).join("/");
|
|
2936
|
+
if (await isLink(join4(root, ...segments.slice(0, i)))) {
|
|
2937
|
+
links.push(rel);
|
|
2938
|
+
present = false;
|
|
2939
|
+
break;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
if (present && existsSync3(join4(root, ...segments))) paths.push(path);
|
|
2943
|
+
}
|
|
2944
|
+
for (const { dir, md } of layout.folders) {
|
|
2945
|
+
const full = join4(root, dir);
|
|
2946
|
+
if (await isLink(full)) {
|
|
2947
|
+
links.push(dir);
|
|
2948
|
+
continue;
|
|
2949
|
+
}
|
|
2950
|
+
if (!existsSync3(full)) continue;
|
|
2951
|
+
const walked = await walk(full, { skipHidden: true });
|
|
2952
|
+
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
2953
|
+
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
2954
|
+
for (const p of walked.files) {
|
|
2955
|
+
if (md && !p.endsWith(".md")) leftOut.push(`${dir}/${p}`);
|
|
2956
|
+
else paths.push(`${dir}/${p}`);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
if (links.length) {
|
|
2960
|
+
throw new Error(
|
|
2961
|
+
`${links.join(", ")}: links are not followed; copy the file into the folder. Nothing was sent.`
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
if (layout.required && !paths.includes(layout.required)) {
|
|
2965
|
+
throw new Error(`${root} has no ${layout.required}: the ${part} part needs it. Nothing was sent.`);
|
|
2966
|
+
}
|
|
2967
|
+
if (!paths.length) {
|
|
2968
|
+
throw new Error(
|
|
2969
|
+
`${root} has no ${layout.folders.map((f) => `${f.dir}/*.md`).join(" or ")}: nothing to publish for ${part}.`
|
|
2970
|
+
);
|
|
2971
|
+
}
|
|
2972
|
+
return { files: await readTextFiles(root, paths.sort(), `the ${part} part`), leftOut };
|
|
2973
|
+
}
|
|
2974
|
+
function describePerson(id, person) {
|
|
2975
|
+
const lines = [`**${person.label ?? "(no label)"}**, \`${id}\``];
|
|
2976
|
+
if (person.mergedInto) {
|
|
2977
|
+
lines.push(`\`${id}\` was merged into \`${person.mergedInto}\`: these are \`${person.mergedInto}\`'s details.`);
|
|
2978
|
+
}
|
|
2979
|
+
lines.push(`- Name: ${person.name ?? "(none held)"}`);
|
|
2980
|
+
lines.push(`- Phone number: ${person.phone ?? "(none held)"}`);
|
|
2981
|
+
lines.push(`- Email address: ${person.email ?? "(none held)"}`);
|
|
2982
|
+
const token = `\`${person.mergedInto ?? id}\`${person.label ? ` (${person.label})` : ""}`;
|
|
2983
|
+
lines.push(
|
|
2984
|
+
"",
|
|
2985
|
+
`These details are for this session only. Do not write them into a memory file, a note or a commit: a memory names this person by the identifier, as ${token}. A memory file that holds the details is offered for rewriting again at the next sync.`
|
|
2986
|
+
);
|
|
2987
|
+
return lines.join("\n");
|
|
2988
|
+
}
|
|
2989
|
+
function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
2990
|
+
const fileSchema = z14.object({
|
|
2991
|
+
path: z14.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
|
|
2992
|
+
content: z14.string().describe("The file's full text, exactly as it is")
|
|
2993
|
+
});
|
|
2994
|
+
server2.registerTool(
|
|
2995
|
+
"publish_setup_part",
|
|
2996
|
+
{
|
|
2997
|
+
title: "Publish Setup Part",
|
|
2998
|
+
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."),
|
|
2999
|
+
inputSchema: {
|
|
3000
|
+
part: z14.enum(PARTS).describe("Which part of the setup"),
|
|
3001
|
+
...localFiles ? {
|
|
3002
|
+
directory: z14.string().optional().describe(
|
|
3003
|
+
"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/"
|
|
3004
|
+
)
|
|
3005
|
+
} : {},
|
|
3006
|
+
files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
|
|
3007
|
+
},
|
|
3008
|
+
annotations: { readOnlyHint: false }
|
|
3009
|
+
},
|
|
3010
|
+
async (args2) => {
|
|
3011
|
+
let files;
|
|
3012
|
+
let leftOut = [];
|
|
3013
|
+
if (localFiles && args2.directory) {
|
|
3014
|
+
({ files, leftOut } = await readPartDirectory(args2.part, expandHome2(args2.directory)));
|
|
3015
|
+
} else if (args2.files?.length) {
|
|
3016
|
+
files = args2.files;
|
|
3017
|
+
} else {
|
|
3018
|
+
throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
|
|
3019
|
+
}
|
|
3020
|
+
const { workspace } = await apiClient2.getOwnTeamSetup();
|
|
3021
|
+
const saved = await apiClient2.publishPartVersion(workspace, args2.part, files);
|
|
3022
|
+
const lines = [`Published **${saved.part}** version ${saved.version}.`];
|
|
3023
|
+
if (saved.part !== "instructions" && saved.pluginVersion !== void 0) {
|
|
3024
|
+
lines.push(`Team plugin version ${saved.pluginVersion}; each member accepts it at their next update.`);
|
|
3025
|
+
}
|
|
3026
|
+
if (saved.replacedValues?.length) {
|
|
3027
|
+
lines.push(
|
|
3028
|
+
"Replaced by a reference to each member's own environment variable:",
|
|
3029
|
+
...saved.replacedValues.map((r) => `- ${r.server} ${r.field} ${r.name}: \${${r.name}}`)
|
|
3030
|
+
);
|
|
3031
|
+
}
|
|
3032
|
+
if (leftOut.length) lines.push(`Left out (not part of ${args2.part}): ${leftOut.join(", ")}`);
|
|
3033
|
+
return text2(lines.join("\n"));
|
|
3034
|
+
}
|
|
3035
|
+
);
|
|
3036
|
+
server2.registerTool(
|
|
3037
|
+
"resolve_person",
|
|
3038
|
+
{
|
|
3039
|
+
title: "Resolve Person",
|
|
3040
|
+
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.",
|
|
3041
|
+
inputSchema: {
|
|
3042
|
+
id: z14.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
|
|
3043
|
+
},
|
|
3044
|
+
annotations: { readOnlyHint: true }
|
|
3045
|
+
},
|
|
3046
|
+
async (args2) => {
|
|
3047
|
+
const { workspace } = await apiClient2.getOwnTeamSetup();
|
|
3048
|
+
const person = await apiClient2.resolvePerson(workspace, args2.id);
|
|
3049
|
+
return text2(describePerson(args2.id, person));
|
|
3050
|
+
}
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
var LAYOUT;
|
|
3054
|
+
var init_team_setup = __esm({
|
|
3055
|
+
"src/tools/team-setup.ts"() {
|
|
3056
|
+
"use strict";
|
|
3057
|
+
init_skills();
|
|
3058
|
+
init_team_setup_types();
|
|
3059
|
+
LAYOUT = {
|
|
3060
|
+
hooks: { files: ["hooks/hooks.json"], folders: [{ dir: "scripts", md: false }], required: "hooks/hooks.json" },
|
|
3061
|
+
mcp: { files: [".mcp.json"], folders: [], required: ".mcp.json" },
|
|
3062
|
+
agents: { files: [], folders: [{ dir: "agents", md: true }] },
|
|
3063
|
+
commands: { files: [], folders: [{ dir: "commands", md: true }] },
|
|
3064
|
+
instructions: { files: ["CLAUDE.md"], folders: [], required: "CLAUDE.md" }
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
|
|
2404
3069
|
// src/server.ts
|
|
2405
3070
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2406
3071
|
function createServer(apiClient2, { localFiles = false } = {}) {
|
|
@@ -2423,6 +3088,7 @@ function createServer(apiClient2, { localFiles = false } = {}) {
|
|
|
2423
3088
|
registerCreateProjectRule(server2, apiClient2);
|
|
2424
3089
|
registerCreatePromptTemplate(server2, apiClient2);
|
|
2425
3090
|
registerSkillTools(server2, apiClient2, { localFiles });
|
|
3091
|
+
registerTeamSetupTools(server2, apiClient2, { localFiles });
|
|
2426
3092
|
return server2;
|
|
2427
3093
|
}
|
|
2428
3094
|
var SERVER_VERSION;
|
|
@@ -2444,7 +3110,8 @@ var init_server = __esm({
|
|
|
2444
3110
|
init_get_prompt_templates();
|
|
2445
3111
|
init_create_prompt_template();
|
|
2446
3112
|
init_skills2();
|
|
2447
|
-
|
|
3113
|
+
init_team_setup();
|
|
3114
|
+
SERVER_VERSION = true ? "0.1.46" : "0.0.0-dev";
|
|
2448
3115
|
}
|
|
2449
3116
|
});
|
|
2450
3117
|
|
|
@@ -2478,17 +3145,18 @@ var init_index = __esm({
|
|
|
2478
3145
|
|
|
2479
3146
|
// src/cli.ts
|
|
2480
3147
|
init_config();
|
|
2481
|
-
|
|
2482
|
-
import {
|
|
2483
|
-
import {
|
|
2484
|
-
import {
|
|
3148
|
+
init_team_setup_files();
|
|
3149
|
+
import { readFile as readFile3, writeFile as writeFile4, chmod as chmod2 } from "fs/promises";
|
|
3150
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3151
|
+
import { homedir as homedir5 } from "os";
|
|
3152
|
+
import { join as join5 } from "path";
|
|
2485
3153
|
import { createInterface } from "readline";
|
|
2486
3154
|
function prompt(question) {
|
|
2487
3155
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2488
|
-
return new Promise((
|
|
3156
|
+
return new Promise((resolve6) => {
|
|
2489
3157
|
rl.question(question, (answer) => {
|
|
2490
3158
|
rl.close();
|
|
2491
|
-
|
|
3159
|
+
resolve6(answer.trim());
|
|
2492
3160
|
});
|
|
2493
3161
|
});
|
|
2494
3162
|
}
|
|
@@ -2508,45 +3176,45 @@ async function validateApiKey(apiKey, apiUrl) {
|
|
|
2508
3176
|
}
|
|
2509
3177
|
function findConfigTargets() {
|
|
2510
3178
|
const targets = [];
|
|
2511
|
-
const projectMcp =
|
|
3179
|
+
const projectMcp = join5(process.cwd(), ".mcp.json");
|
|
2512
3180
|
targets.push({
|
|
2513
3181
|
name: "Claude Code (project)",
|
|
2514
3182
|
path: projectMcp,
|
|
2515
3183
|
detected: true
|
|
2516
3184
|
// Always available
|
|
2517
3185
|
});
|
|
2518
|
-
const claudeUser =
|
|
3186
|
+
const claudeUser = join5(homedir5(), ".claude.json");
|
|
2519
3187
|
targets.push({
|
|
2520
3188
|
name: "Claude Code (user \u2014 all projects)",
|
|
2521
3189
|
path: claudeUser,
|
|
2522
3190
|
detected: true
|
|
2523
3191
|
// Always available
|
|
2524
3192
|
});
|
|
2525
|
-
const cursorProject =
|
|
2526
|
-
const cursorDir =
|
|
3193
|
+
const cursorProject = join5(process.cwd(), ".cursor", "mcp.json");
|
|
3194
|
+
const cursorDir = join5(process.cwd(), ".cursor");
|
|
2527
3195
|
targets.push({
|
|
2528
3196
|
name: "Cursor (project)",
|
|
2529
3197
|
path: cursorProject,
|
|
2530
|
-
detected:
|
|
3198
|
+
detected: existsSync4(cursorDir)
|
|
2531
3199
|
});
|
|
2532
|
-
const vscodeProject =
|
|
2533
|
-
const vscodeDir =
|
|
3200
|
+
const vscodeProject = join5(process.cwd(), ".vscode", "mcp.json");
|
|
3201
|
+
const vscodeDir = join5(process.cwd(), ".vscode");
|
|
2534
3202
|
targets.push({
|
|
2535
3203
|
name: "VS Code (project)",
|
|
2536
3204
|
path: vscodeProject,
|
|
2537
|
-
detected:
|
|
3205
|
+
detected: existsSync4(vscodeDir)
|
|
2538
3206
|
});
|
|
2539
|
-
const cursorGlobal =
|
|
3207
|
+
const cursorGlobal = join5(homedir5(), ".cursor", "mcp.json");
|
|
2540
3208
|
targets.push({
|
|
2541
3209
|
name: "Cursor (global)",
|
|
2542
3210
|
path: cursorGlobal,
|
|
2543
|
-
detected:
|
|
3211
|
+
detected: existsSync4(join5(homedir5(), ".cursor"))
|
|
2544
3212
|
});
|
|
2545
3213
|
return targets;
|
|
2546
3214
|
}
|
|
2547
3215
|
async function readOrCreateConfig(path) {
|
|
2548
3216
|
try {
|
|
2549
|
-
const content = await
|
|
3217
|
+
const content = await readFile3(path, "utf-8");
|
|
2550
3218
|
return JSON.parse(content);
|
|
2551
3219
|
} catch {
|
|
2552
3220
|
return { mcpServers: {} };
|
|
@@ -2554,13 +3222,13 @@ async function readOrCreateConfig(path) {
|
|
|
2554
3222
|
}
|
|
2555
3223
|
async function writeConfig(path, config2) {
|
|
2556
3224
|
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
2557
|
-
if (!
|
|
2558
|
-
const { mkdir:
|
|
2559
|
-
await
|
|
3225
|
+
if (!existsSync4(dir)) {
|
|
3226
|
+
const { mkdir: mkdir4 } = await import("fs/promises");
|
|
3227
|
+
await mkdir4(dir, { recursive: true });
|
|
2560
3228
|
}
|
|
2561
|
-
await
|
|
3229
|
+
await writeFile4(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
|
|
2562
3230
|
try {
|
|
2563
|
-
await
|
|
3231
|
+
await chmod2(path, 384);
|
|
2564
3232
|
} catch {
|
|
2565
3233
|
}
|
|
2566
3234
|
}
|
|
@@ -2636,7 +3304,7 @@ async function setup() {
|
|
|
2636
3304
|
console.log(` Configured: ${target.name}`);
|
|
2637
3305
|
}
|
|
2638
3306
|
console.log(
|
|
2639
|
-
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides
|
|
3307
|
+
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides 19 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.\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"
|
|
2640
3308
|
);
|
|
2641
3309
|
}
|
|
2642
3310
|
var args = process.argv.slice(2);
|
|
@@ -2652,6 +3320,12 @@ if (command === "setup") {
|
|
|
2652
3320
|
Promise.resolve().then(() => (init_install_skill(), install_skill_exports)).then(
|
|
2653
3321
|
({ runInstallSkill: runInstallSkill2 }) => runInstallSkill2(args.slice(1), { out: console.log, err: console.error })
|
|
2654
3322
|
).then((code) => process.exit(code));
|
|
3323
|
+
} else if (command === "marketplace-auth") {
|
|
3324
|
+
runMarketplaceAuth({ out: console.log, err: console.error }).then((code) => process.exit(code));
|
|
3325
|
+
} else if (command === "plugin-path") {
|
|
3326
|
+
Promise.resolve().then(() => (init_plugin_path(), plugin_path_exports)).then(
|
|
3327
|
+
({ runPluginPath: runPluginPath2 }) => runPluginPath2(args.slice(1), { out: console.log, err: console.error })
|
|
3328
|
+
).then((code) => process.exit(code));
|
|
2655
3329
|
} else if (command === "--help" || command === "-h") {
|
|
2656
3330
|
console.log(`
|
|
2657
3331
|
SystemDox MCP Server
|
|
@@ -2662,10 +3336,19 @@ if (command === "setup") {
|
|
|
2662
3336
|
systemdox-mcp-server install-skill <name> [--version n] [--target dir] [--overwrite]
|
|
2663
3337
|
Install a skill published to SystemDox
|
|
2664
3338
|
(default folder: ~/.claude/skills/<name>)
|
|
3339
|
+
systemdox-mcp-server plugin-path <workspace> --version <n> --sha256 <hash>
|
|
3340
|
+
Print the folder of your team's plugin version,
|
|
3341
|
+
downloading and checking it first when needed
|
|
3342
|
+
(Claude Code runs it)
|
|
3343
|
+
systemdox-mcp-server marketplace-auth
|
|
3344
|
+
Print the header for your team's marketplace
|
|
3345
|
+
(Claude Code runs it)
|
|
2665
3346
|
systemdox-mcp-server --help Show this help message
|
|
2666
3347
|
|
|
2667
3348
|
Environment variables:
|
|
2668
|
-
SYSTEMDOX_API_KEY Your SystemDox API key (required
|
|
3349
|
+
SYSTEMDOX_API_KEY Your SystemDox API key (required; plugin-path and
|
|
3350
|
+
marketplace-auth also read
|
|
3351
|
+
~/.claude/systemdox/credentials.json)
|
|
2669
3352
|
SYSTEMDOX_API_URL API URL (default: ${API_URL_DEFAULT})
|
|
2670
3353
|
SYSTEMDOX_CACHE_TTL Cache TTL in seconds (default: 300)
|
|
2671
3354
|
SYSTEMDOX_DEFAULT_ORG Default GitHub org
|