@systemdox/mcp-server 0.1.45 → 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/http.js
CHANGED
|
@@ -34,6 +34,28 @@ var ResponseCache = class {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
// src/lib/api-client.ts
|
|
37
|
+
var SystemDoxApiError = class extends Error {
|
|
38
|
+
constructor(status, text3) {
|
|
39
|
+
super(`SystemDox API error ${status}: ${text3}`);
|
|
40
|
+
this.status = status;
|
|
41
|
+
this.text = text3;
|
|
42
|
+
let body;
|
|
43
|
+
try {
|
|
44
|
+
body = JSON.parse(text3);
|
|
45
|
+
} catch {
|
|
46
|
+
body = void 0;
|
|
47
|
+
}
|
|
48
|
+
this.body = body;
|
|
49
|
+
const code = body?.code;
|
|
50
|
+
this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text3)?.[1];
|
|
51
|
+
}
|
|
52
|
+
status;
|
|
53
|
+
text;
|
|
54
|
+
code;
|
|
55
|
+
body;
|
|
56
|
+
};
|
|
57
|
+
var TEAM_SETUP = "/v1/team-setup";
|
|
58
|
+
var seg = (s) => encodeURIComponent(String(s));
|
|
37
59
|
var SystemDoxApiClient = class {
|
|
38
60
|
config;
|
|
39
61
|
cache;
|
|
@@ -55,10 +77,7 @@ var SystemDoxApiClient = class {
|
|
|
55
77
|
const cached = this.cache.get(cacheKey);
|
|
56
78
|
if (cached !== void 0) return cached;
|
|
57
79
|
const res = await this.fetchRaw(path, params);
|
|
58
|
-
if (!res.ok)
|
|
59
|
-
const body = await res.text();
|
|
60
|
-
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
61
|
-
}
|
|
80
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
62
81
|
const data = await res.json();
|
|
63
82
|
this.cache.set(cacheKey, data);
|
|
64
83
|
return data;
|
|
@@ -67,10 +86,7 @@ var SystemDoxApiClient = class {
|
|
|
67
86
|
* latest now, not the one cached before someone published another. */
|
|
68
87
|
async getFresh(path) {
|
|
69
88
|
const res = await this.fetchRaw(path);
|
|
70
|
-
if (!res.ok)
|
|
71
|
-
const body = await res.text();
|
|
72
|
-
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
73
|
-
}
|
|
89
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
74
90
|
return await res.json();
|
|
75
91
|
}
|
|
76
92
|
async post(path, body) {
|
|
@@ -92,12 +108,101 @@ var SystemDoxApiClient = class {
|
|
|
92
108
|
},
|
|
93
109
|
body: JSON.stringify(body)
|
|
94
110
|
});
|
|
95
|
-
if (!res.ok)
|
|
96
|
-
const text2 = await res.text();
|
|
97
|
-
throw new Error(`SystemDox API error ${res.status}: ${text2}`);
|
|
98
|
-
}
|
|
111
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
99
112
|
return await res.json();
|
|
100
113
|
}
|
|
114
|
+
// ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
|
|
115
|
+
// One method per operationId. Every read skips the cache: setup, sync and
|
|
116
|
+
// plugin-path act on what the workspace holds now.
|
|
117
|
+
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
118
|
+
getOwnTeamSetup() {
|
|
119
|
+
return this.getFresh(TEAM_SETUP);
|
|
120
|
+
}
|
|
121
|
+
getTeamSetup(workspace) {
|
|
122
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
123
|
+
}
|
|
124
|
+
getMarketplace(workspace) {
|
|
125
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
|
|
126
|
+
}
|
|
127
|
+
getPart(workspace, part) {
|
|
128
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
|
|
129
|
+
}
|
|
130
|
+
publishPartVersion(workspace, part, files) {
|
|
131
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
|
|
132
|
+
}
|
|
133
|
+
getPartVersion(workspace, part, version) {
|
|
134
|
+
return this.getFresh(
|
|
135
|
+
`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
getSetupSkills(workspace) {
|
|
139
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
|
|
140
|
+
}
|
|
141
|
+
setSetupSkills(workspace, skills) {
|
|
142
|
+
return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
|
|
143
|
+
}
|
|
144
|
+
getPlugin(workspace) {
|
|
145
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
|
|
146
|
+
}
|
|
147
|
+
/** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
|
|
148
|
+
* header. The caller checks the BYTES against the hash it was given. */
|
|
149
|
+
async getPluginZip(workspace, version) {
|
|
150
|
+
const res = await fetch(
|
|
151
|
+
this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
|
|
152
|
+
{
|
|
153
|
+
headers: {
|
|
154
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
155
|
+
"X-API-Key": this.config.apiKey,
|
|
156
|
+
Accept: "application/zip"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
161
|
+
return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
|
|
162
|
+
}
|
|
163
|
+
listMemoryProjects(workspace) {
|
|
164
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
|
|
165
|
+
}
|
|
166
|
+
getMemory(workspace, project) {
|
|
167
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
|
|
168
|
+
}
|
|
169
|
+
/** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
|
|
170
|
+
* code SYD-K010 and `body.currentVersion`. */
|
|
171
|
+
pushMemoryVersion(workspace, project, body) {
|
|
172
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
|
|
173
|
+
}
|
|
174
|
+
getMemoryVersion(workspace, project, version) {
|
|
175
|
+
return this.getFresh(
|
|
176
|
+
`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
findMissingBlobs(workspace, project, hashes) {
|
|
180
|
+
return this.post(
|
|
181
|
+
`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
|
|
182
|
+
{ hashes }
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
uploadBlobs(workspace, project, blobs) {
|
|
186
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
|
|
187
|
+
}
|
|
188
|
+
readBlobs(workspace, project, hashes) {
|
|
189
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
|
|
190
|
+
}
|
|
191
|
+
listValueAnswers(workspace) {
|
|
192
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
|
|
193
|
+
}
|
|
194
|
+
lookupValues(workspace, values) {
|
|
195
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
|
|
196
|
+
}
|
|
197
|
+
answerValues(workspace, answers) {
|
|
198
|
+
return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
|
|
199
|
+
}
|
|
200
|
+
fileMemoryPeople(workspace, people) {
|
|
201
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people });
|
|
202
|
+
}
|
|
203
|
+
resolvePerson(workspace, personId) {
|
|
204
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
|
|
205
|
+
}
|
|
101
206
|
invalidateCache(path) {
|
|
102
207
|
if (path) {
|
|
103
208
|
this.cache.invalidate(this.buildUrl(path).toString());
|
|
@@ -313,12 +418,12 @@ function registerGetGuardrails(server, apiClient) {
|
|
|
313
418
|
);
|
|
314
419
|
}
|
|
315
420
|
if (filtered.length === 0) {
|
|
316
|
-
const
|
|
421
|
+
const text4 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
|
|
317
422
|
return {
|
|
318
|
-
content: [{ type: "text", text:
|
|
423
|
+
content: [{ type: "text", text: text4 }]
|
|
319
424
|
};
|
|
320
425
|
}
|
|
321
|
-
const
|
|
426
|
+
const text3 = filtered.map(
|
|
322
427
|
(g) => `## ${g.title} [${g.severity}]
|
|
323
428
|
|
|
324
429
|
${g.description}
|
|
@@ -342,7 +447,7 @@ ${g.fix}
|
|
|
342
447
|
type: "text",
|
|
343
448
|
text: `Found ${filtered.length} check(s):
|
|
344
449
|
|
|
345
|
-
${
|
|
450
|
+
${text3}`
|
|
346
451
|
}
|
|
347
452
|
]
|
|
348
453
|
};
|
|
@@ -432,7 +537,7 @@ ${spec.taskContent}`);
|
|
|
432
537
|
]
|
|
433
538
|
};
|
|
434
539
|
}
|
|
435
|
-
const
|
|
540
|
+
const text3 = filtered.map(
|
|
436
541
|
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
437
542
|
).join("\n");
|
|
438
543
|
return {
|
|
@@ -441,7 +546,7 @@ ${spec.taskContent}`);
|
|
|
441
546
|
type: "text",
|
|
442
547
|
text: `Found ${filtered.length} spec(s):
|
|
443
548
|
|
|
444
|
-
${
|
|
549
|
+
${text3}`
|
|
445
550
|
}
|
|
446
551
|
]
|
|
447
552
|
};
|
|
@@ -494,18 +599,18 @@ function registerGetDocs(server, apiClient) {
|
|
|
494
599
|
isError: true
|
|
495
600
|
};
|
|
496
601
|
}
|
|
497
|
-
let
|
|
602
|
+
let text4 = doc.content;
|
|
498
603
|
if (anchor && doc.section_resolved === false) {
|
|
499
|
-
|
|
604
|
+
text4 = `_Section "${anchor}" not found \u2014 returning the full document._
|
|
500
605
|
|
|
501
|
-
` +
|
|
606
|
+
` + text4;
|
|
502
607
|
} else if (anchor && doc.heading) {
|
|
503
|
-
|
|
608
|
+
text4 = `## ${doc.heading}
|
|
504
609
|
|
|
505
610
|
${doc.content}`;
|
|
506
611
|
}
|
|
507
612
|
return {
|
|
508
|
-
content: [{ type: "text", text:
|
|
613
|
+
content: [{ type: "text", text: text4 }]
|
|
509
614
|
};
|
|
510
615
|
}
|
|
511
616
|
const params = {};
|
|
@@ -529,7 +634,7 @@ ${doc.content}`;
|
|
|
529
634
|
]
|
|
530
635
|
};
|
|
531
636
|
}
|
|
532
|
-
const
|
|
637
|
+
const text3 = items.map((d) => {
|
|
533
638
|
const sections = (d.outline ?? []).slice(0, 8);
|
|
534
639
|
const sectionHint = sections.length > 0 ? `
|
|
535
640
|
sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
|
|
@@ -543,7 +648,7 @@ ${doc.content}`;
|
|
|
543
648
|
type: "text",
|
|
544
649
|
text: `Found ${items.length} document(s) (${response.total} total):
|
|
545
650
|
|
|
546
|
-
${
|
|
651
|
+
${text3}
|
|
547
652
|
|
|
548
653
|
Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
549
654
|
}
|
|
@@ -878,13 +983,13 @@ var SEVERITY_SCORES = {
|
|
|
878
983
|
low: 0.25
|
|
879
984
|
};
|
|
880
985
|
var CHARS_PER_TOKEN = 4;
|
|
881
|
-
function estimateTokens(
|
|
882
|
-
return Math.ceil(
|
|
986
|
+
function estimateTokens(text3) {
|
|
987
|
+
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
883
988
|
}
|
|
884
|
-
function truncateToTokens(
|
|
989
|
+
function truncateToTokens(text3, maxTokens) {
|
|
885
990
|
const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
|
|
886
|
-
if (
|
|
887
|
-
const head =
|
|
991
|
+
if (text3.length <= maxChars) return { text: text3, truncated: false };
|
|
992
|
+
const head = text3.slice(0, maxChars);
|
|
888
993
|
const floor = Math.floor(maxChars * 0.75);
|
|
889
994
|
const paragraph = head.lastIndexOf("\n\n");
|
|
890
995
|
const line = head.lastIndexOf("\n");
|
|
@@ -929,7 +1034,7 @@ function scoreSeverity(item) {
|
|
|
929
1034
|
if (!item.severity) return 0.5;
|
|
930
1035
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
931
1036
|
}
|
|
932
|
-
function extractKeywords(
|
|
1037
|
+
function extractKeywords(text3) {
|
|
933
1038
|
const stopWords = /* @__PURE__ */ new Set([
|
|
934
1039
|
"the",
|
|
935
1040
|
"a",
|
|
@@ -1014,7 +1119,7 @@ function extractKeywords(text2) {
|
|
|
1014
1119
|
"implement",
|
|
1015
1120
|
"create"
|
|
1016
1121
|
]);
|
|
1017
|
-
return
|
|
1122
|
+
return text3.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
1018
1123
|
}
|
|
1019
1124
|
function scoreItems(items, taskDescription, filePaths) {
|
|
1020
1125
|
const languages = detectLanguages(filePaths);
|
|
@@ -1057,20 +1162,20 @@ var MAX_TAIL = 10;
|
|
|
1057
1162
|
var TAIL_SHARE = 0.15;
|
|
1058
1163
|
var TRUNCATION_NOTICE_TOKENS = 30;
|
|
1059
1164
|
function formatGuardrail(g) {
|
|
1060
|
-
let
|
|
1165
|
+
let text3 = `## ${g.title} [${g.severity}]
|
|
1061
1166
|
|
|
1062
1167
|
${g.description}`;
|
|
1063
|
-
if (g.pattern)
|
|
1168
|
+
if (g.pattern) text3 += `
|
|
1064
1169
|
|
|
1065
1170
|
**Bad:** \`\`\`
|
|
1066
1171
|
${g.pattern}
|
|
1067
1172
|
\`\`\``;
|
|
1068
|
-
if (g.fix)
|
|
1173
|
+
if (g.fix) text3 += `
|
|
1069
1174
|
|
|
1070
1175
|
**Good:** \`\`\`
|
|
1071
1176
|
${g.fix}
|
|
1072
1177
|
\`\`\``;
|
|
1073
|
-
return
|
|
1178
|
+
return text3;
|
|
1074
1179
|
}
|
|
1075
1180
|
function formatSpecHeadline(s) {
|
|
1076
1181
|
return `### ${s.name}
|
|
@@ -1092,19 +1197,19 @@ function formatRules(rules) {
|
|
|
1092
1197
|
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
1093
1198
|
byCategory.get(cat).push(r);
|
|
1094
1199
|
}
|
|
1095
|
-
let
|
|
1200
|
+
let text3 = "";
|
|
1096
1201
|
for (const [cat, catRules] of byCategory) {
|
|
1097
|
-
|
|
1202
|
+
text3 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
1098
1203
|
`;
|
|
1099
1204
|
for (const r of catRules) {
|
|
1100
|
-
|
|
1205
|
+
text3 += `- ${r.text}
|
|
1101
1206
|
`;
|
|
1102
1207
|
}
|
|
1103
|
-
|
|
1208
|
+
text3 += "\n";
|
|
1104
1209
|
}
|
|
1105
1210
|
return `## Project Rules (${rules.length})
|
|
1106
1211
|
|
|
1107
|
-
${
|
|
1212
|
+
${text3.trim()}`;
|
|
1108
1213
|
}
|
|
1109
1214
|
async function fetchSpecBody(api, draftId) {
|
|
1110
1215
|
const spec = await api.get(
|
|
@@ -1441,13 +1546,13 @@ function registerGetContext(server, apiClient) {
|
|
|
1441
1546
|
}
|
|
1442
1547
|
},
|
|
1443
1548
|
async ({ task_description, file_paths, max_tokens }) => {
|
|
1444
|
-
const { text:
|
|
1549
|
+
const { text: text3 } = await buildContext(apiClient, {
|
|
1445
1550
|
taskDescription: task_description,
|
|
1446
1551
|
filePaths: file_paths,
|
|
1447
1552
|
maxTokens: max_tokens
|
|
1448
1553
|
});
|
|
1449
1554
|
return {
|
|
1450
|
-
content: [{ type: "text", text:
|
|
1555
|
+
content: [{ type: "text", text: text3 }]
|
|
1451
1556
|
};
|
|
1452
1557
|
}
|
|
1453
1558
|
);
|
|
@@ -1695,9 +1800,9 @@ function registerGetProjectRules(server, apiClient) {
|
|
|
1695
1800
|
);
|
|
1696
1801
|
}
|
|
1697
1802
|
if (filtered.length === 0) {
|
|
1698
|
-
const
|
|
1803
|
+
const text4 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
|
|
1699
1804
|
return {
|
|
1700
|
-
content: [{ type: "text", text:
|
|
1805
|
+
content: [{ type: "text", text: text4 }]
|
|
1701
1806
|
};
|
|
1702
1807
|
}
|
|
1703
1808
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -1706,25 +1811,25 @@ function registerGetProjectRules(server, apiClient) {
|
|
|
1706
1811
|
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
1707
1812
|
grouped.get(cat).push(rule);
|
|
1708
1813
|
}
|
|
1709
|
-
let
|
|
1814
|
+
let text3 = `Found ${filtered.length} project rule(s):
|
|
1710
1815
|
|
|
1711
1816
|
`;
|
|
1712
1817
|
for (const [cat, catRules] of grouped) {
|
|
1713
1818
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
1714
|
-
|
|
1819
|
+
text3 += `## ${label}
|
|
1715
1820
|
|
|
1716
1821
|
`;
|
|
1717
1822
|
for (const rule of catRules) {
|
|
1718
|
-
|
|
1823
|
+
text3 += `- ${rule.text}
|
|
1719
1824
|
`;
|
|
1720
1825
|
}
|
|
1721
|
-
|
|
1826
|
+
text3 += "\n";
|
|
1722
1827
|
}
|
|
1723
1828
|
return {
|
|
1724
1829
|
content: [
|
|
1725
1830
|
{
|
|
1726
1831
|
type: "text",
|
|
1727
|
-
text:
|
|
1832
|
+
text: text3
|
|
1728
1833
|
}
|
|
1729
1834
|
]
|
|
1730
1835
|
};
|
|
@@ -1750,10 +1855,10 @@ function registerCreateProjectRule(server, apiClient) {
|
|
|
1750
1855
|
readOnlyHint: false
|
|
1751
1856
|
}
|
|
1752
1857
|
},
|
|
1753
|
-
async ({ text:
|
|
1858
|
+
async ({ text: text3, category }) => {
|
|
1754
1859
|
const saved = await apiClient.post(
|
|
1755
1860
|
"/v1/standards/rules",
|
|
1756
|
-
{ text:
|
|
1861
|
+
{ text: text3, category }
|
|
1757
1862
|
);
|
|
1758
1863
|
apiClient.invalidateCache("/v1/standards/rules");
|
|
1759
1864
|
return {
|
|
@@ -1791,7 +1896,7 @@ function registerGetPromptTemplates(server, apiClient) {
|
|
|
1791
1896
|
]
|
|
1792
1897
|
};
|
|
1793
1898
|
}
|
|
1794
|
-
const
|
|
1899
|
+
const text3 = templates.map(
|
|
1795
1900
|
(t) => `## ${t.name}
|
|
1796
1901
|
|
|
1797
1902
|
` + (t.description ? `*${t.description}*
|
|
@@ -1806,7 +1911,7 @@ ${t.prompt}
|
|
|
1806
1911
|
type: "text",
|
|
1807
1912
|
text: `Found ${templates.length} prompt template(s):
|
|
1808
1913
|
|
|
1809
|
-
${
|
|
1914
|
+
${text3}`
|
|
1810
1915
|
}
|
|
1811
1916
|
]
|
|
1812
1917
|
};
|
|
@@ -1914,18 +2019,9 @@ async function walk(root, { skipHidden }, dir = root, out = { files: [], links:
|
|
|
1914
2019
|
out.files.sort();
|
|
1915
2020
|
return out;
|
|
1916
2021
|
}
|
|
1917
|
-
async function
|
|
1918
|
-
const root = resolve(directory);
|
|
1919
|
-
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
1920
|
-
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
1921
|
-
}
|
|
1922
|
-
const walked = await walk(root, { skipHidden: true });
|
|
1923
|
-
if (walked.links.length) {
|
|
1924
|
-
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
1925
|
-
}
|
|
1926
|
-
const paths = walked.files;
|
|
2022
|
+
async function readTextFiles(root, paths, what) {
|
|
1927
2023
|
if (paths.length > MAX_FILES) {
|
|
1928
|
-
throw new Error(`${root} holds ${paths.length} files:
|
|
2024
|
+
throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
|
|
1929
2025
|
}
|
|
1930
2026
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1931
2027
|
const files = [];
|
|
@@ -1935,7 +2031,7 @@ async function readSkillDirectory(directory) {
|
|
|
1935
2031
|
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
1936
2032
|
}
|
|
1937
2033
|
const raw = await readFile(join(root, ...path.split("/")));
|
|
1938
|
-
if (raw.includes(0)) throw new Error(`${path} is binary:
|
|
2034
|
+
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
1939
2035
|
if (raw.length > MAX_FILE_BYTES) {
|
|
1940
2036
|
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
1941
2037
|
}
|
|
@@ -1951,6 +2047,18 @@ async function readSkillDirectory(directory) {
|
|
|
1951
2047
|
}
|
|
1952
2048
|
files.push({ path, content });
|
|
1953
2049
|
}
|
|
2050
|
+
return files;
|
|
2051
|
+
}
|
|
2052
|
+
async function readSkillDirectory(directory) {
|
|
2053
|
+
const root = resolve(directory);
|
|
2054
|
+
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
2055
|
+
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
2056
|
+
}
|
|
2057
|
+
const walked = await walk(root, { skipHidden: true });
|
|
2058
|
+
if (walked.links.length) {
|
|
2059
|
+
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
2060
|
+
}
|
|
2061
|
+
const files = await readTextFiles(root, walked.files, "a skill");
|
|
1954
2062
|
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
1955
2063
|
const name = frontmatterName(skillMd.content);
|
|
1956
2064
|
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
@@ -2032,8 +2140,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
2032
2140
|
keptEdited
|
|
2033
2141
|
};
|
|
2034
2142
|
}
|
|
2035
|
-
function fenceFor(
|
|
2036
|
-
const longest = Math.max(0, ...(
|
|
2143
|
+
function fenceFor(text3) {
|
|
2144
|
+
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
2037
2145
|
return "`".repeat(Math.max(3, longest + 1));
|
|
2038
2146
|
}
|
|
2039
2147
|
function describeInstall(name, r) {
|
|
@@ -2189,8 +2297,186 @@ ${body}`
|
|
|
2189
2297
|
);
|
|
2190
2298
|
}
|
|
2191
2299
|
|
|
2300
|
+
// src/tools/team-setup.ts
|
|
2301
|
+
import { z as z14 } from "zod";
|
|
2302
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2303
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
2304
|
+
import { homedir as homedir3 } from "os";
|
|
2305
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
2306
|
+
|
|
2307
|
+
// src/lib/person-values.ts
|
|
2308
|
+
var W = "[\\p{L}\\p{N}_]";
|
|
2309
|
+
var D = "\\p{Nd}";
|
|
2310
|
+
var WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
|
|
2311
|
+
var PERSON_ID = "(?:con|ctc|usr)_[0-9A-Za-z]{10,40}";
|
|
2312
|
+
var PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
|
|
2313
|
+
var BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
|
|
2314
|
+
var AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
|
|
2315
|
+
var E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
|
|
2316
|
+
var NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
|
|
2317
|
+
var ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
|
|
2318
|
+
var 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])";
|
|
2319
|
+
var HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
|
|
2320
|
+
var HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
|
|
2321
|
+
var 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,}`;
|
|
2322
|
+
|
|
2323
|
+
// src/lib/team-setup-types.ts
|
|
2324
|
+
var PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
2325
|
+
var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
2326
|
+
|
|
2327
|
+
// src/tools/team-setup.ts
|
|
2328
|
+
var LAYOUT = {
|
|
2329
|
+
hooks: { files: ["hooks/hooks.json"], folders: [{ dir: "scripts", md: false }], required: "hooks/hooks.json" },
|
|
2330
|
+
mcp: { files: [".mcp.json"], folders: [], required: ".mcp.json" },
|
|
2331
|
+
agents: { files: [], folders: [{ dir: "agents", md: true }] },
|
|
2332
|
+
commands: { files: [], folders: [{ dir: "commands", md: true }] },
|
|
2333
|
+
instructions: { files: ["CLAUDE.md"], folders: [], required: "CLAUDE.md" }
|
|
2334
|
+
};
|
|
2335
|
+
function expandHome2(path) {
|
|
2336
|
+
return path.replace(/^~(?=$|[\\/])/, homedir3());
|
|
2337
|
+
}
|
|
2338
|
+
function text2(t) {
|
|
2339
|
+
return { content: [{ type: "text", text: t }] };
|
|
2340
|
+
}
|
|
2341
|
+
async function isLink(path) {
|
|
2342
|
+
try {
|
|
2343
|
+
return (await lstat2(path)).isSymbolicLink();
|
|
2344
|
+
} catch (e) {
|
|
2345
|
+
if (e.code === "ENOENT") return false;
|
|
2346
|
+
throw e;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
async function readPartDirectory(part, directory) {
|
|
2350
|
+
const root = resolve3(directory);
|
|
2351
|
+
const layout = LAYOUT[part];
|
|
2352
|
+
const paths = [];
|
|
2353
|
+
const leftOut = [];
|
|
2354
|
+
const links = [];
|
|
2355
|
+
for (const path of layout.files) {
|
|
2356
|
+
const segments = path.split("/");
|
|
2357
|
+
let present = true;
|
|
2358
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
2359
|
+
const rel = segments.slice(0, i).join("/");
|
|
2360
|
+
if (await isLink(join2(root, ...segments.slice(0, i)))) {
|
|
2361
|
+
links.push(rel);
|
|
2362
|
+
present = false;
|
|
2363
|
+
break;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (present && existsSync2(join2(root, ...segments))) paths.push(path);
|
|
2367
|
+
}
|
|
2368
|
+
for (const { dir, md } of layout.folders) {
|
|
2369
|
+
const full = join2(root, dir);
|
|
2370
|
+
if (await isLink(full)) {
|
|
2371
|
+
links.push(dir);
|
|
2372
|
+
continue;
|
|
2373
|
+
}
|
|
2374
|
+
if (!existsSync2(full)) continue;
|
|
2375
|
+
const walked = await walk(full, { skipHidden: true });
|
|
2376
|
+
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
2377
|
+
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
2378
|
+
for (const p of walked.files) {
|
|
2379
|
+
if (md && !p.endsWith(".md")) leftOut.push(`${dir}/${p}`);
|
|
2380
|
+
else paths.push(`${dir}/${p}`);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
if (links.length) {
|
|
2384
|
+
throw new Error(
|
|
2385
|
+
`${links.join(", ")}: links are not followed; copy the file into the folder. Nothing was sent.`
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
if (layout.required && !paths.includes(layout.required)) {
|
|
2389
|
+
throw new Error(`${root} has no ${layout.required}: the ${part} part needs it. Nothing was sent.`);
|
|
2390
|
+
}
|
|
2391
|
+
if (!paths.length) {
|
|
2392
|
+
throw new Error(
|
|
2393
|
+
`${root} has no ${layout.folders.map((f) => `${f.dir}/*.md`).join(" or ")}: nothing to publish for ${part}.`
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
return { files: await readTextFiles(root, paths.sort(), `the ${part} part`), leftOut };
|
|
2397
|
+
}
|
|
2398
|
+
function describePerson(id, person) {
|
|
2399
|
+
const lines = [`**${person.label ?? "(no label)"}**, \`${id}\``];
|
|
2400
|
+
if (person.mergedInto) {
|
|
2401
|
+
lines.push(`\`${id}\` was merged into \`${person.mergedInto}\`: these are \`${person.mergedInto}\`'s details.`);
|
|
2402
|
+
}
|
|
2403
|
+
lines.push(`- Name: ${person.name ?? "(none held)"}`);
|
|
2404
|
+
lines.push(`- Phone number: ${person.phone ?? "(none held)"}`);
|
|
2405
|
+
lines.push(`- Email address: ${person.email ?? "(none held)"}`);
|
|
2406
|
+
const token = `\`${person.mergedInto ?? id}\`${person.label ? ` (${person.label})` : ""}`;
|
|
2407
|
+
lines.push(
|
|
2408
|
+
"",
|
|
2409
|
+
`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.`
|
|
2410
|
+
);
|
|
2411
|
+
return lines.join("\n");
|
|
2412
|
+
}
|
|
2413
|
+
function registerTeamSetupTools(server, apiClient, { localFiles }) {
|
|
2414
|
+
const fileSchema = z14.object({
|
|
2415
|
+
path: z14.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
|
|
2416
|
+
content: z14.string().describe("The file's full text, exactly as it is")
|
|
2417
|
+
});
|
|
2418
|
+
server.registerTool(
|
|
2419
|
+
"publish_setup_part",
|
|
2420
|
+
{
|
|
2421
|
+
title: "Publish Setup Part",
|
|
2422
|
+
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."),
|
|
2423
|
+
inputSchema: {
|
|
2424
|
+
part: z14.enum(PARTS).describe("Which part of the setup"),
|
|
2425
|
+
...localFiles ? {
|
|
2426
|
+
directory: z14.string().optional().describe(
|
|
2427
|
+
"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/"
|
|
2428
|
+
)
|
|
2429
|
+
} : {},
|
|
2430
|
+
files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
|
|
2431
|
+
},
|
|
2432
|
+
annotations: { readOnlyHint: false }
|
|
2433
|
+
},
|
|
2434
|
+
async (args) => {
|
|
2435
|
+
let files;
|
|
2436
|
+
let leftOut = [];
|
|
2437
|
+
if (localFiles && args.directory) {
|
|
2438
|
+
({ files, leftOut } = await readPartDirectory(args.part, expandHome2(args.directory)));
|
|
2439
|
+
} else if (args.files?.length) {
|
|
2440
|
+
files = args.files;
|
|
2441
|
+
} else {
|
|
2442
|
+
throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
|
|
2443
|
+
}
|
|
2444
|
+
const { workspace } = await apiClient.getOwnTeamSetup();
|
|
2445
|
+
const saved = await apiClient.publishPartVersion(workspace, args.part, files);
|
|
2446
|
+
const lines = [`Published **${saved.part}** version ${saved.version}.`];
|
|
2447
|
+
if (saved.part !== "instructions" && saved.pluginVersion !== void 0) {
|
|
2448
|
+
lines.push(`Team plugin version ${saved.pluginVersion}; each member accepts it at their next update.`);
|
|
2449
|
+
}
|
|
2450
|
+
if (saved.replacedValues?.length) {
|
|
2451
|
+
lines.push(
|
|
2452
|
+
"Replaced by a reference to each member's own environment variable:",
|
|
2453
|
+
...saved.replacedValues.map((r) => `- ${r.server} ${r.field} ${r.name}: \${${r.name}}`)
|
|
2454
|
+
);
|
|
2455
|
+
}
|
|
2456
|
+
if (leftOut.length) lines.push(`Left out (not part of ${args.part}): ${leftOut.join(", ")}`);
|
|
2457
|
+
return text2(lines.join("\n"));
|
|
2458
|
+
}
|
|
2459
|
+
);
|
|
2460
|
+
server.registerTool(
|
|
2461
|
+
"resolve_person",
|
|
2462
|
+
{
|
|
2463
|
+
title: "Resolve Person",
|
|
2464
|
+
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.",
|
|
2465
|
+
inputSchema: {
|
|
2466
|
+
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")
|
|
2467
|
+
},
|
|
2468
|
+
annotations: { readOnlyHint: true }
|
|
2469
|
+
},
|
|
2470
|
+
async (args) => {
|
|
2471
|
+
const { workspace } = await apiClient.getOwnTeamSetup();
|
|
2472
|
+
const person = await apiClient.resolvePerson(workspace, args.id);
|
|
2473
|
+
return text2(describePerson(args.id, person));
|
|
2474
|
+
}
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2192
2478
|
// src/server.ts
|
|
2193
|
-
var SERVER_VERSION = true ? "0.1.
|
|
2479
|
+
var SERVER_VERSION = true ? "0.1.46" : "0.0.0-dev";
|
|
2194
2480
|
function createServer(apiClient, { localFiles = false } = {}) {
|
|
2195
2481
|
const server = new McpServer({
|
|
2196
2482
|
name: "systemdox-mcp-server",
|
|
@@ -2211,6 +2497,7 @@ function createServer(apiClient, { localFiles = false } = {}) {
|
|
|
2211
2497
|
registerCreateProjectRule(server, apiClient);
|
|
2212
2498
|
registerCreatePromptTemplate(server, apiClient);
|
|
2213
2499
|
registerSkillTools(server, apiClient, { localFiles });
|
|
2500
|
+
registerTeamSetupTools(server, apiClient, { localFiles });
|
|
2214
2501
|
return server;
|
|
2215
2502
|
}
|
|
2216
2503
|
|