@systemdox/mcp-server 0.1.45 → 0.1.47
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 +115 -0
- package/dist/cli.js +2319 -219
- package/dist/http.js +1270 -73
- package/dist/index.js +1270 -73
- 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/memory-sync.d.ts +150 -0
- package/dist/lib/memory-sync.js +363 -0
- 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/setup-team.d.ts +80 -0
- package/dist/setup-team.js +1593 -0
- package/dist/sync-memory.d.ts +53 -0
- package/dist/sync-memory.js +1219 -0
- package/dist/tools/skills.js +15 -12
- package/dist/tools/team-setup.d.ts +37 -0
- package/dist/tools/team-setup.js +1152 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -70,6 +70,28 @@ var ResponseCache = class {
|
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
// src/lib/api-client.ts
|
|
73
|
+
var SystemDoxApiError = class extends Error {
|
|
74
|
+
constructor(status, text3) {
|
|
75
|
+
super(`SystemDox API error ${status}: ${text3}`);
|
|
76
|
+
this.status = status;
|
|
77
|
+
this.text = text3;
|
|
78
|
+
let body;
|
|
79
|
+
try {
|
|
80
|
+
body = JSON.parse(text3);
|
|
81
|
+
} catch {
|
|
82
|
+
body = void 0;
|
|
83
|
+
}
|
|
84
|
+
this.body = body;
|
|
85
|
+
const code = body?.code;
|
|
86
|
+
this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text3)?.[1];
|
|
87
|
+
}
|
|
88
|
+
status;
|
|
89
|
+
text;
|
|
90
|
+
code;
|
|
91
|
+
body;
|
|
92
|
+
};
|
|
93
|
+
var TEAM_SETUP = "/v1/team-setup";
|
|
94
|
+
var seg = (s) => encodeURIComponent(String(s));
|
|
73
95
|
var SystemDoxApiClient = class {
|
|
74
96
|
config;
|
|
75
97
|
cache;
|
|
@@ -91,10 +113,7 @@ var SystemDoxApiClient = class {
|
|
|
91
113
|
const cached = this.cache.get(cacheKey);
|
|
92
114
|
if (cached !== void 0) return cached;
|
|
93
115
|
const res = await this.fetchRaw(path, params);
|
|
94
|
-
if (!res.ok)
|
|
95
|
-
const body = await res.text();
|
|
96
|
-
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
97
|
-
}
|
|
116
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
98
117
|
const data = await res.json();
|
|
99
118
|
this.cache.set(cacheKey, data);
|
|
100
119
|
return data;
|
|
@@ -103,10 +122,7 @@ var SystemDoxApiClient = class {
|
|
|
103
122
|
* latest now, not the one cached before someone published another. */
|
|
104
123
|
async getFresh(path) {
|
|
105
124
|
const res = await this.fetchRaw(path);
|
|
106
|
-
if (!res.ok)
|
|
107
|
-
const body = await res.text();
|
|
108
|
-
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
109
|
-
}
|
|
125
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
110
126
|
return await res.json();
|
|
111
127
|
}
|
|
112
128
|
async post(path, body) {
|
|
@@ -128,12 +144,101 @@ var SystemDoxApiClient = class {
|
|
|
128
144
|
},
|
|
129
145
|
body: JSON.stringify(body)
|
|
130
146
|
});
|
|
131
|
-
if (!res.ok)
|
|
132
|
-
const text2 = await res.text();
|
|
133
|
-
throw new Error(`SystemDox API error ${res.status}: ${text2}`);
|
|
134
|
-
}
|
|
147
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
135
148
|
return await res.json();
|
|
136
149
|
}
|
|
150
|
+
// ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
|
|
151
|
+
// One method per operationId. Every read skips the cache: setup, sync and
|
|
152
|
+
// plugin-path act on what the workspace holds now.
|
|
153
|
+
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
154
|
+
getOwnTeamSetup() {
|
|
155
|
+
return this.getFresh(TEAM_SETUP);
|
|
156
|
+
}
|
|
157
|
+
getTeamSetup(workspace) {
|
|
158
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
159
|
+
}
|
|
160
|
+
getMarketplace(workspace) {
|
|
161
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
|
|
162
|
+
}
|
|
163
|
+
getPart(workspace, part) {
|
|
164
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
|
|
165
|
+
}
|
|
166
|
+
publishPartVersion(workspace, part, files) {
|
|
167
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
|
|
168
|
+
}
|
|
169
|
+
getPartVersion(workspace, part, version) {
|
|
170
|
+
return this.getFresh(
|
|
171
|
+
`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
getSetupSkills(workspace) {
|
|
175
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
|
|
176
|
+
}
|
|
177
|
+
setSetupSkills(workspace, skills) {
|
|
178
|
+
return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
|
|
179
|
+
}
|
|
180
|
+
getPlugin(workspace) {
|
|
181
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
|
|
182
|
+
}
|
|
183
|
+
/** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
|
|
184
|
+
* header. The caller checks the BYTES against the hash it was given. */
|
|
185
|
+
async getPluginZip(workspace, version) {
|
|
186
|
+
const res = await fetch(
|
|
187
|
+
this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
|
|
188
|
+
{
|
|
189
|
+
headers: {
|
|
190
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
191
|
+
"X-API-Key": this.config.apiKey,
|
|
192
|
+
Accept: "application/zip"
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
197
|
+
return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
|
|
198
|
+
}
|
|
199
|
+
listMemoryProjects(workspace) {
|
|
200
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
|
|
201
|
+
}
|
|
202
|
+
getMemory(workspace, project) {
|
|
203
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
|
|
204
|
+
}
|
|
205
|
+
/** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
|
|
206
|
+
* code SYD-K010 and `body.currentVersion`. */
|
|
207
|
+
pushMemoryVersion(workspace, project, body) {
|
|
208
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
|
|
209
|
+
}
|
|
210
|
+
getMemoryVersion(workspace, project, version) {
|
|
211
|
+
return this.getFresh(
|
|
212
|
+
`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
findMissingBlobs(workspace, project, hashes) {
|
|
216
|
+
return this.post(
|
|
217
|
+
`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
|
|
218
|
+
{ hashes }
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
uploadBlobs(workspace, project, blobs) {
|
|
222
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
|
|
223
|
+
}
|
|
224
|
+
readBlobs(workspace, project, hashes) {
|
|
225
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
|
|
226
|
+
}
|
|
227
|
+
listValueAnswers(workspace) {
|
|
228
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
|
|
229
|
+
}
|
|
230
|
+
lookupValues(workspace, values) {
|
|
231
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
|
|
232
|
+
}
|
|
233
|
+
answerValues(workspace, answers) {
|
|
234
|
+
return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
|
|
235
|
+
}
|
|
236
|
+
fileMemoryPeople(workspace, people2) {
|
|
237
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people: people2 });
|
|
238
|
+
}
|
|
239
|
+
resolvePerson(workspace, personId) {
|
|
240
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
|
|
241
|
+
}
|
|
137
242
|
invalidateCache(path) {
|
|
138
243
|
if (path) {
|
|
139
244
|
this.cache.invalidate(this.buildUrl(path).toString());
|
|
@@ -295,12 +400,12 @@ function registerGetGuardrails(server2, apiClient2) {
|
|
|
295
400
|
);
|
|
296
401
|
}
|
|
297
402
|
if (filtered.length === 0) {
|
|
298
|
-
const
|
|
403
|
+
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}"` : ""}.`;
|
|
299
404
|
return {
|
|
300
|
-
content: [{ type: "text", text:
|
|
405
|
+
content: [{ type: "text", text: text4 }]
|
|
301
406
|
};
|
|
302
407
|
}
|
|
303
|
-
const
|
|
408
|
+
const text3 = filtered.map(
|
|
304
409
|
(g) => `## ${g.title} [${g.severity}]
|
|
305
410
|
|
|
306
411
|
${g.description}
|
|
@@ -324,7 +429,7 @@ ${g.fix}
|
|
|
324
429
|
type: "text",
|
|
325
430
|
text: `Found ${filtered.length} check(s):
|
|
326
431
|
|
|
327
|
-
${
|
|
432
|
+
${text3}`
|
|
328
433
|
}
|
|
329
434
|
]
|
|
330
435
|
};
|
|
@@ -414,7 +519,7 @@ ${spec.taskContent}`);
|
|
|
414
519
|
]
|
|
415
520
|
};
|
|
416
521
|
}
|
|
417
|
-
const
|
|
522
|
+
const text3 = filtered.map(
|
|
418
523
|
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
419
524
|
).join("\n");
|
|
420
525
|
return {
|
|
@@ -423,7 +528,7 @@ ${spec.taskContent}`);
|
|
|
423
528
|
type: "text",
|
|
424
529
|
text: `Found ${filtered.length} spec(s):
|
|
425
530
|
|
|
426
|
-
${
|
|
531
|
+
${text3}`
|
|
427
532
|
}
|
|
428
533
|
]
|
|
429
534
|
};
|
|
@@ -476,18 +581,18 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
476
581
|
isError: true
|
|
477
582
|
};
|
|
478
583
|
}
|
|
479
|
-
let
|
|
584
|
+
let text4 = doc.content;
|
|
480
585
|
if (anchor && doc.section_resolved === false) {
|
|
481
|
-
|
|
586
|
+
text4 = `_Section "${anchor}" not found \u2014 returning the full document._
|
|
482
587
|
|
|
483
|
-
` +
|
|
588
|
+
` + text4;
|
|
484
589
|
} else if (anchor && doc.heading) {
|
|
485
|
-
|
|
590
|
+
text4 = `## ${doc.heading}
|
|
486
591
|
|
|
487
592
|
${doc.content}`;
|
|
488
593
|
}
|
|
489
594
|
return {
|
|
490
|
-
content: [{ type: "text", text:
|
|
595
|
+
content: [{ type: "text", text: text4 }]
|
|
491
596
|
};
|
|
492
597
|
}
|
|
493
598
|
const params = {};
|
|
@@ -511,7 +616,7 @@ ${doc.content}`;
|
|
|
511
616
|
]
|
|
512
617
|
};
|
|
513
618
|
}
|
|
514
|
-
const
|
|
619
|
+
const text3 = items.map((d) => {
|
|
515
620
|
const sections = (d.outline ?? []).slice(0, 8);
|
|
516
621
|
const sectionHint = sections.length > 0 ? `
|
|
517
622
|
sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
|
|
@@ -525,7 +630,7 @@ ${doc.content}`;
|
|
|
525
630
|
type: "text",
|
|
526
631
|
text: `Found ${items.length} document(s) (${response.total} total):
|
|
527
632
|
|
|
528
|
-
${
|
|
633
|
+
${text3}
|
|
529
634
|
|
|
530
635
|
Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
531
636
|
}
|
|
@@ -563,8 +668,8 @@ function registerGetTechStack(server2, apiClient2) {
|
|
|
563
668
|
list.push(entry);
|
|
564
669
|
grouped.set(entry.category, list);
|
|
565
670
|
}
|
|
566
|
-
const sections = Array.from(grouped.entries()).map(([category,
|
|
567
|
-
const items =
|
|
671
|
+
const sections = Array.from(grouped.entries()).map(([category, entries2]) => {
|
|
672
|
+
const items = entries2.map(
|
|
568
673
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
569
674
|
).join("\n");
|
|
570
675
|
return `### ${category}
|
|
@@ -860,13 +965,13 @@ var SEVERITY_SCORES = {
|
|
|
860
965
|
low: 0.25
|
|
861
966
|
};
|
|
862
967
|
var CHARS_PER_TOKEN = 4;
|
|
863
|
-
function estimateTokens(
|
|
864
|
-
return Math.ceil(
|
|
968
|
+
function estimateTokens(text3) {
|
|
969
|
+
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
865
970
|
}
|
|
866
|
-
function truncateToTokens(
|
|
971
|
+
function truncateToTokens(text3, maxTokens) {
|
|
867
972
|
const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
|
|
868
|
-
if (
|
|
869
|
-
const head =
|
|
973
|
+
if (text3.length <= maxChars) return { text: text3, truncated: false };
|
|
974
|
+
const head = text3.slice(0, maxChars);
|
|
870
975
|
const floor = Math.floor(maxChars * 0.75);
|
|
871
976
|
const paragraph = head.lastIndexOf("\n\n");
|
|
872
977
|
const line = head.lastIndexOf("\n");
|
|
@@ -911,7 +1016,7 @@ function scoreSeverity(item) {
|
|
|
911
1016
|
if (!item.severity) return 0.5;
|
|
912
1017
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
913
1018
|
}
|
|
914
|
-
function extractKeywords(
|
|
1019
|
+
function extractKeywords(text3) {
|
|
915
1020
|
const stopWords = /* @__PURE__ */ new Set([
|
|
916
1021
|
"the",
|
|
917
1022
|
"a",
|
|
@@ -996,7 +1101,7 @@ function extractKeywords(text2) {
|
|
|
996
1101
|
"implement",
|
|
997
1102
|
"create"
|
|
998
1103
|
]);
|
|
999
|
-
return
|
|
1104
|
+
return text3.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
1000
1105
|
}
|
|
1001
1106
|
function scoreItems(items, taskDescription, filePaths) {
|
|
1002
1107
|
const languages = detectLanguages(filePaths);
|
|
@@ -1039,20 +1144,20 @@ var MAX_TAIL = 10;
|
|
|
1039
1144
|
var TAIL_SHARE = 0.15;
|
|
1040
1145
|
var TRUNCATION_NOTICE_TOKENS = 30;
|
|
1041
1146
|
function formatGuardrail(g) {
|
|
1042
|
-
let
|
|
1147
|
+
let text3 = `## ${g.title} [${g.severity}]
|
|
1043
1148
|
|
|
1044
1149
|
${g.description}`;
|
|
1045
|
-
if (g.pattern)
|
|
1150
|
+
if (g.pattern) text3 += `
|
|
1046
1151
|
|
|
1047
1152
|
**Bad:** \`\`\`
|
|
1048
1153
|
${g.pattern}
|
|
1049
1154
|
\`\`\``;
|
|
1050
|
-
if (g.fix)
|
|
1155
|
+
if (g.fix) text3 += `
|
|
1051
1156
|
|
|
1052
1157
|
**Good:** \`\`\`
|
|
1053
1158
|
${g.fix}
|
|
1054
1159
|
\`\`\``;
|
|
1055
|
-
return
|
|
1160
|
+
return text3;
|
|
1056
1161
|
}
|
|
1057
1162
|
function formatSpecHeadline(s) {
|
|
1058
1163
|
return `### ${s.name}
|
|
@@ -1062,8 +1167,8 @@ function formatDocHeadline(d) {
|
|
|
1062
1167
|
return `### ${d.title}
|
|
1063
1168
|
\`doc_id: ${d.id}\` \xB7 \`${d.repo_full_name}/${d.path}\` \xB7 ${d.doc_type}`;
|
|
1064
1169
|
}
|
|
1065
|
-
function formatStack(
|
|
1066
|
-
return
|
|
1170
|
+
function formatStack(entries2) {
|
|
1171
|
+
return entries2.map(
|
|
1067
1172
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
1068
1173
|
).join("\n");
|
|
1069
1174
|
}
|
|
@@ -1074,19 +1179,19 @@ function formatRules(rules) {
|
|
|
1074
1179
|
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
1075
1180
|
byCategory.get(cat).push(r);
|
|
1076
1181
|
}
|
|
1077
|
-
let
|
|
1182
|
+
let text3 = "";
|
|
1078
1183
|
for (const [cat, catRules] of byCategory) {
|
|
1079
|
-
|
|
1184
|
+
text3 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
1080
1185
|
`;
|
|
1081
1186
|
for (const r of catRules) {
|
|
1082
|
-
|
|
1187
|
+
text3 += `- ${r.text}
|
|
1083
1188
|
`;
|
|
1084
1189
|
}
|
|
1085
|
-
|
|
1190
|
+
text3 += "\n";
|
|
1086
1191
|
}
|
|
1087
1192
|
return `## Project Rules (${rules.length})
|
|
1088
1193
|
|
|
1089
|
-
${
|
|
1194
|
+
${text3.trim()}`;
|
|
1090
1195
|
}
|
|
1091
1196
|
async function fetchSpecBody(api, draftId) {
|
|
1092
1197
|
const spec = await api.get(
|
|
@@ -1423,13 +1528,13 @@ function registerGetContext(server2, apiClient2) {
|
|
|
1423
1528
|
}
|
|
1424
1529
|
},
|
|
1425
1530
|
async ({ task_description, file_paths, max_tokens }) => {
|
|
1426
|
-
const { text:
|
|
1531
|
+
const { text: text3 } = await buildContext(apiClient2, {
|
|
1427
1532
|
taskDescription: task_description,
|
|
1428
1533
|
filePaths: file_paths,
|
|
1429
1534
|
maxTokens: max_tokens
|
|
1430
1535
|
});
|
|
1431
1536
|
return {
|
|
1432
|
-
content: [{ type: "text", text:
|
|
1537
|
+
content: [{ type: "text", text: text3 }]
|
|
1433
1538
|
};
|
|
1434
1539
|
}
|
|
1435
1540
|
);
|
|
@@ -1677,9 +1782,9 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
1677
1782
|
);
|
|
1678
1783
|
}
|
|
1679
1784
|
if (filtered.length === 0) {
|
|
1680
|
-
const
|
|
1785
|
+
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}"` : ""}.`;
|
|
1681
1786
|
return {
|
|
1682
|
-
content: [{ type: "text", text:
|
|
1787
|
+
content: [{ type: "text", text: text4 }]
|
|
1683
1788
|
};
|
|
1684
1789
|
}
|
|
1685
1790
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -1688,25 +1793,25 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
1688
1793
|
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
1689
1794
|
grouped.get(cat).push(rule);
|
|
1690
1795
|
}
|
|
1691
|
-
let
|
|
1796
|
+
let text3 = `Found ${filtered.length} project rule(s):
|
|
1692
1797
|
|
|
1693
1798
|
`;
|
|
1694
1799
|
for (const [cat, catRules] of grouped) {
|
|
1695
1800
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
1696
|
-
|
|
1801
|
+
text3 += `## ${label}
|
|
1697
1802
|
|
|
1698
1803
|
`;
|
|
1699
1804
|
for (const rule of catRules) {
|
|
1700
|
-
|
|
1805
|
+
text3 += `- ${rule.text}
|
|
1701
1806
|
`;
|
|
1702
1807
|
}
|
|
1703
|
-
|
|
1808
|
+
text3 += "\n";
|
|
1704
1809
|
}
|
|
1705
1810
|
return {
|
|
1706
1811
|
content: [
|
|
1707
1812
|
{
|
|
1708
1813
|
type: "text",
|
|
1709
|
-
text:
|
|
1814
|
+
text: text3
|
|
1710
1815
|
}
|
|
1711
1816
|
]
|
|
1712
1817
|
};
|
|
@@ -1732,10 +1837,10 @@ function registerCreateProjectRule(server2, apiClient2) {
|
|
|
1732
1837
|
readOnlyHint: false
|
|
1733
1838
|
}
|
|
1734
1839
|
},
|
|
1735
|
-
async ({ text:
|
|
1840
|
+
async ({ text: text3, category }) => {
|
|
1736
1841
|
const saved = await apiClient2.post(
|
|
1737
1842
|
"/v1/standards/rules",
|
|
1738
|
-
{ text:
|
|
1843
|
+
{ text: text3, category }
|
|
1739
1844
|
);
|
|
1740
1845
|
apiClient2.invalidateCache("/v1/standards/rules");
|
|
1741
1846
|
return {
|
|
@@ -1773,7 +1878,7 @@ function registerGetPromptTemplates(server2, apiClient2) {
|
|
|
1773
1878
|
]
|
|
1774
1879
|
};
|
|
1775
1880
|
}
|
|
1776
|
-
const
|
|
1881
|
+
const text3 = templates.map(
|
|
1777
1882
|
(t) => `## ${t.name}
|
|
1778
1883
|
|
|
1779
1884
|
` + (t.description ? `*${t.description}*
|
|
@@ -1788,7 +1893,7 @@ ${t.prompt}
|
|
|
1788
1893
|
type: "text",
|
|
1789
1894
|
text: `Found ${templates.length} prompt template(s):
|
|
1790
1895
|
|
|
1791
|
-
${
|
|
1896
|
+
${text3}`
|
|
1792
1897
|
}
|
|
1793
1898
|
]
|
|
1794
1899
|
};
|
|
@@ -1896,18 +2001,9 @@ async function walk(root, { skipHidden }, dir = root, out = { files: [], links:
|
|
|
1896
2001
|
out.files.sort();
|
|
1897
2002
|
return out;
|
|
1898
2003
|
}
|
|
1899
|
-
async function
|
|
1900
|
-
const root = resolve(directory);
|
|
1901
|
-
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
1902
|
-
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
1903
|
-
}
|
|
1904
|
-
const walked = await walk(root, { skipHidden: true });
|
|
1905
|
-
if (walked.links.length) {
|
|
1906
|
-
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
1907
|
-
}
|
|
1908
|
-
const paths = walked.files;
|
|
2004
|
+
async function readTextFiles(root, paths, what) {
|
|
1909
2005
|
if (paths.length > MAX_FILES) {
|
|
1910
|
-
throw new Error(`${root} holds ${paths.length} files:
|
|
2006
|
+
throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
|
|
1911
2007
|
}
|
|
1912
2008
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
1913
2009
|
const files = [];
|
|
@@ -1917,7 +2013,7 @@ async function readSkillDirectory(directory) {
|
|
|
1917
2013
|
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
1918
2014
|
}
|
|
1919
2015
|
const raw = await readFile(join(root, ...path.split("/")));
|
|
1920
|
-
if (raw.includes(0)) throw new Error(`${path} is binary:
|
|
2016
|
+
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
1921
2017
|
if (raw.length > MAX_FILE_BYTES) {
|
|
1922
2018
|
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
1923
2019
|
}
|
|
@@ -1933,6 +2029,18 @@ async function readSkillDirectory(directory) {
|
|
|
1933
2029
|
}
|
|
1934
2030
|
files.push({ path, content });
|
|
1935
2031
|
}
|
|
2032
|
+
return files;
|
|
2033
|
+
}
|
|
2034
|
+
async function readSkillDirectory(directory) {
|
|
2035
|
+
const root = resolve(directory);
|
|
2036
|
+
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
2037
|
+
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
2038
|
+
}
|
|
2039
|
+
const walked = await walk(root, { skipHidden: true });
|
|
2040
|
+
if (walked.links.length) {
|
|
2041
|
+
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
2042
|
+
}
|
|
2043
|
+
const files = await readTextFiles(root, walked.files, "a skill");
|
|
1936
2044
|
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
1937
2045
|
const name = frontmatterName(skillMd.content);
|
|
1938
2046
|
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
@@ -2014,8 +2122,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
2014
2122
|
keptEdited
|
|
2015
2123
|
};
|
|
2016
2124
|
}
|
|
2017
|
-
function fenceFor(
|
|
2018
|
-
const longest = Math.max(0, ...(
|
|
2125
|
+
function fenceFor(text3) {
|
|
2126
|
+
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
2019
2127
|
return "`".repeat(Math.max(3, longest + 1));
|
|
2020
2128
|
}
|
|
2021
2129
|
function describeInstall(name, r) {
|
|
@@ -2171,8 +2279,1096 @@ ${body}`
|
|
|
2171
2279
|
);
|
|
2172
2280
|
}
|
|
2173
2281
|
|
|
2282
|
+
// src/tools/team-setup.ts
|
|
2283
|
+
import { z as z14 } from "zod";
|
|
2284
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2285
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
2286
|
+
import { homedir as homedir4 } from "os";
|
|
2287
|
+
import { join as join4, resolve as resolve4 } from "path";
|
|
2288
|
+
|
|
2289
|
+
// src/lib/person-values.ts
|
|
2290
|
+
var PHONE = "phone";
|
|
2291
|
+
var EMAIL = "email";
|
|
2292
|
+
var HOME_PATH = "home-path";
|
|
2293
|
+
var NATIONAL_PREFIX = "+44";
|
|
2294
|
+
var W = "[\\p{L}\\p{N}_]";
|
|
2295
|
+
var D = "\\p{Nd}";
|
|
2296
|
+
var WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
|
|
2297
|
+
var PERSON_ID = "(?:(?:con|ctc)_[0-9A-Za-z]{10,40}|usr_[0-9A-Za-z-]{10,40})";
|
|
2298
|
+
var PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
|
|
2299
|
+
var BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
|
|
2300
|
+
var AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
|
|
2301
|
+
var E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
|
|
2302
|
+
var NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
|
|
2303
|
+
var GROUP_SIZES = /* @__PURE__ */ new Set([2, 3, 4, 5, 6]);
|
|
2304
|
+
var ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
|
|
2305
|
+
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])";
|
|
2306
|
+
var HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
|
|
2307
|
+
var HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
|
|
2308
|
+
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,}`;
|
|
2309
|
+
var global = (source, flags = "") => new RegExp(source, `gu${flags}`);
|
|
2310
|
+
var whole = (source, flags = "") => new RegExp(`^(?:${source})$`, `u${flags}`);
|
|
2311
|
+
function lineOf(text3, start) {
|
|
2312
|
+
let line = 1;
|
|
2313
|
+
for (let i = text3.indexOf("\n"); i !== -1 && i < start; i = text3.indexOf("\n", i + 1)) line++;
|
|
2314
|
+
return line;
|
|
2315
|
+
}
|
|
2316
|
+
function phoneShape(value) {
|
|
2317
|
+
const groups = value.replace(/^\++/, "").split(/[ -]/);
|
|
2318
|
+
const digits = [...groups.join("")];
|
|
2319
|
+
if (value.startsWith("+")) return digits.length >= 8 && digits.length <= 15 && digits[0] !== "0";
|
|
2320
|
+
if (digits.length < 10 || digits.length > 11) return false;
|
|
2321
|
+
return groups.length === 1 || groups.slice(1).every((g) => GROUP_SIZES.has([...g].length));
|
|
2322
|
+
}
|
|
2323
|
+
function normalise(kind, value) {
|
|
2324
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
2325
|
+
if (kind === PHONE) {
|
|
2326
|
+
if (!(whole(E164).test(v) || whole(NATIONAL).test(v)) || !phoneShape(v)) {
|
|
2327
|
+
throw new Error("not a phone number");
|
|
2328
|
+
}
|
|
2329
|
+
const digits = v.replace(/[ -]/g, "");
|
|
2330
|
+
return digits.startsWith("+") ? digits : NATIONAL_PREFIX + digits.slice(1);
|
|
2331
|
+
}
|
|
2332
|
+
if (kind === EMAIL) {
|
|
2333
|
+
if (!whole(EMAIL_RE).test(v)) throw new Error("not an email address");
|
|
2334
|
+
return v.toLowerCase();
|
|
2335
|
+
}
|
|
2336
|
+
throw new Error(`not a value kind: ${JSON.stringify(kind)}`);
|
|
2337
|
+
}
|
|
2338
|
+
function findPersonValues(text3) {
|
|
2339
|
+
if (typeof text3 !== "string" || !text3) return [];
|
|
2340
|
+
const skipped = [...text3.matchAll(global(PERSON_TOKEN))].map((m) => [m.index, m.index + m[0].length]);
|
|
2341
|
+
const found = [];
|
|
2342
|
+
const add = (kind, m, normalised) => {
|
|
2343
|
+
const start = m.index;
|
|
2344
|
+
const end = start + m[0].length;
|
|
2345
|
+
if (skipped.some(([s, e]) => s <= start && end <= e)) return;
|
|
2346
|
+
if (found.some((f) => f.start < end && start < f.end)) return;
|
|
2347
|
+
found.push({ kind, value: m[0], normalised, line: lineOf(text3, start), start, end });
|
|
2348
|
+
};
|
|
2349
|
+
for (const m of text3.matchAll(global(EMAIL_RE))) add(EMAIL, m, m[0].toLowerCase());
|
|
2350
|
+
for (const source of [E164, NATIONAL]) {
|
|
2351
|
+
for (const m of text3.matchAll(global(source))) {
|
|
2352
|
+
if (phoneShape(m[0])) add(PHONE, m, normalise(PHONE, m[0]));
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
for (const m of text3.matchAll(global(HOME, "i"))) add(HOME_PATH, m, m[0]);
|
|
2356
|
+
return found.sort((a, b) => a.start - b.start);
|
|
2357
|
+
}
|
|
2358
|
+
function findSecrets(text3) {
|
|
2359
|
+
if (typeof text3 !== "string" || !text3) return [];
|
|
2360
|
+
return [...text3.matchAll(global(KEY_PREFIX))].map((m) => ({ kind: "key-prefix", line: lineOf(text3, m.index) }));
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// src/lib/team-setup-types.ts
|
|
2364
|
+
var PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
2365
|
+
var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
2366
|
+
|
|
2367
|
+
// src/sync-memory.ts
|
|
2368
|
+
import { existsSync as existsSync2 } from "fs";
|
|
2369
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
2370
|
+
import { dirname as dirname3, join as join3, resolve as resolve3 } from "path";
|
|
2371
|
+
|
|
2372
|
+
// src/lib/memory-sync.ts
|
|
2373
|
+
import { createHash as createHash2 } from "crypto";
|
|
2374
|
+
var INDEX_FILE = "MEMORY.md";
|
|
2375
|
+
var SHAREABLE_TYPES = ["feedback", "project", "reference"];
|
|
2376
|
+
var FILE_PATH_RE = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
|
|
2377
|
+
var CONFLICT_COPY_RE = /\.local-\d{4}-\d{2}-\d{2}(?:-\d+)?\.md$/;
|
|
2378
|
+
function sha2562(text3) {
|
|
2379
|
+
return createHash2("sha256").update(text3, "utf8").digest("hex");
|
|
2380
|
+
}
|
|
2381
|
+
function isFilePath(path) {
|
|
2382
|
+
return path.length <= 512 && FILE_PATH_RE.test(path) && !path.split("/").some((s) => s === "." || s === "..");
|
|
2383
|
+
}
|
|
2384
|
+
function isConflictCopy(path) {
|
|
2385
|
+
return CONFLICT_COPY_RE.test(path.split("/").pop() ?? path);
|
|
2386
|
+
}
|
|
2387
|
+
var BLOCK_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
2388
|
+
var YAML_FALSE = /* @__PURE__ */ new Set(["false", "False", "FALSE", "no", "No", "NO", "off", "Off", "OFF"]);
|
|
2389
|
+
function readFrontmatter(text3) {
|
|
2390
|
+
const m = BLOCK_RE.exec(text3);
|
|
2391
|
+
if (!m) return void 0;
|
|
2392
|
+
const front = { shareFalse: false };
|
|
2393
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
2394
|
+
const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
2395
|
+
if (!kv) continue;
|
|
2396
|
+
const raw = kv[2].replace(/[ \t]+#.*$/, "");
|
|
2397
|
+
const quoted = /^(["'])(.*)\1$/.exec(raw);
|
|
2398
|
+
const value = quoted ? quoted[2] : raw;
|
|
2399
|
+
if (kv[1] === "type") front.type = value.trim().toLowerCase();
|
|
2400
|
+
if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
|
|
2401
|
+
}
|
|
2402
|
+
return front;
|
|
2403
|
+
}
|
|
2404
|
+
function isShareable(path, text3) {
|
|
2405
|
+
if (isConflictCopy(path) || !path.endsWith(".md")) return false;
|
|
2406
|
+
const front = readFrontmatter(text3);
|
|
2407
|
+
if (front?.shareFalse) return false;
|
|
2408
|
+
if (path === INDEX_FILE) return true;
|
|
2409
|
+
return !!front && SHAREABLE_TYPES.includes(front.type ?? "");
|
|
2410
|
+
}
|
|
2411
|
+
function addShareFalse(text3) {
|
|
2412
|
+
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
2413
|
+
const m = BLOCK_RE.exec(text3);
|
|
2414
|
+
if (!m) return `---${eol}share: false${eol}---${eol}${text3}`;
|
|
2415
|
+
const lines = m[1].split(/\r?\n/).filter((l) => !/^share[ \t]*:/.test(l));
|
|
2416
|
+
lines.push("share: false");
|
|
2417
|
+
const closing = m[0].slice(m[0].lastIndexOf("---"));
|
|
2418
|
+
return `---${eol}${lines.join(eol)}${eol}${closing}${text3.slice(m[0].length)}`;
|
|
2419
|
+
}
|
|
2420
|
+
function localDate(now) {
|
|
2421
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
2422
|
+
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
|
2423
|
+
}
|
|
2424
|
+
function conflictCopyName(path, date, taken) {
|
|
2425
|
+
const stem = path.replace(/\.md$/, "");
|
|
2426
|
+
for (let n = 1; ; n++) {
|
|
2427
|
+
const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
|
|
2428
|
+
if (!taken.has(name.toLowerCase())) return name;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
function decide(path, base, team, local) {
|
|
2432
|
+
if (team === local) return "unchanged";
|
|
2433
|
+
if (team === base) return local === void 0 ? "delete-team" : "local";
|
|
2434
|
+
if (local === base) return team === void 0 ? "delete-local" : "team";
|
|
2435
|
+
if (path === INDEX_FILE && team !== void 0 && local !== void 0) return "index";
|
|
2436
|
+
if (team === void 0) return "local";
|
|
2437
|
+
if (local === void 0) return "team";
|
|
2438
|
+
return "conflict";
|
|
2439
|
+
}
|
|
2440
|
+
function decideAll(input) {
|
|
2441
|
+
const localHashes = Object.fromEntries(Object.entries(input.local).map(([p, t]) => [p, sha2562(t)]));
|
|
2442
|
+
const paths = /* @__PURE__ */ new Set([...Object.keys(input.base), ...Object.keys(input.team), ...Object.keys(localHashes)]);
|
|
2443
|
+
const rows = {};
|
|
2444
|
+
const needed = /* @__PURE__ */ new Set();
|
|
2445
|
+
for (const path of [...paths].sort()) {
|
|
2446
|
+
if (input.excluded.has(path)) continue;
|
|
2447
|
+
const row = decide(path, input.base[path], input.team[path], localHashes[path]);
|
|
2448
|
+
rows[path] = row;
|
|
2449
|
+
if (row === "team" || row === "conflict" || row === "index") needed.add(input.team[path]);
|
|
2450
|
+
if (row === "index" && input.base[path]) needed.add(input.base[path]);
|
|
2451
|
+
}
|
|
2452
|
+
return { rows, needed: [...needed] };
|
|
2453
|
+
}
|
|
2454
|
+
function planMerge(input, rows, texts) {
|
|
2455
|
+
const plan = {
|
|
2456
|
+
manifest: {},
|
|
2457
|
+
upload: {},
|
|
2458
|
+
writes: {},
|
|
2459
|
+
deletes: [],
|
|
2460
|
+
pulled: [],
|
|
2461
|
+
pushed: [],
|
|
2462
|
+
deletedHere: [],
|
|
2463
|
+
deletedFromTeam: [],
|
|
2464
|
+
conflicts: [],
|
|
2465
|
+
indexTeamLines: []
|
|
2466
|
+
};
|
|
2467
|
+
const taken = new Set(input.taken);
|
|
2468
|
+
const teamText = (path) => {
|
|
2469
|
+
const text3 = texts[input.team[path]];
|
|
2470
|
+
if (text3 === void 0) throw new Error(`the team's ${path} could not be read`);
|
|
2471
|
+
return text3;
|
|
2472
|
+
};
|
|
2473
|
+
for (const path of input.excluded) if (input.team[path]) plan.manifest[path] = input.team[path];
|
|
2474
|
+
for (const [path, row] of Object.entries(rows)) {
|
|
2475
|
+
const team = input.team[path];
|
|
2476
|
+
const local = input.local[path];
|
|
2477
|
+
switch (row) {
|
|
2478
|
+
case "unchanged":
|
|
2479
|
+
if (team) plan.manifest[path] = team;
|
|
2480
|
+
break;
|
|
2481
|
+
case "local":
|
|
2482
|
+
plan.manifest[path] = sha2562(local);
|
|
2483
|
+
plan.upload[path] = local;
|
|
2484
|
+
plan.pushed.push(path);
|
|
2485
|
+
break;
|
|
2486
|
+
case "delete-team":
|
|
2487
|
+
plan.deletedFromTeam.push(path);
|
|
2488
|
+
break;
|
|
2489
|
+
case "team":
|
|
2490
|
+
plan.manifest[path] = team;
|
|
2491
|
+
plan.writes[path] = teamText(path);
|
|
2492
|
+
plan.pulled.push(path);
|
|
2493
|
+
break;
|
|
2494
|
+
case "delete-local":
|
|
2495
|
+
plan.deletes.push(path);
|
|
2496
|
+
plan.deletedHere.push(path);
|
|
2497
|
+
break;
|
|
2498
|
+
case "conflict": {
|
|
2499
|
+
const copy = conflictCopyName(path, input.date, taken);
|
|
2500
|
+
taken.add(copy.toLowerCase());
|
|
2501
|
+
plan.manifest[path] = team;
|
|
2502
|
+
plan.writes[path] = teamText(path);
|
|
2503
|
+
plan.writes[copy] = local;
|
|
2504
|
+
plan.conflicts.push({ path, copy });
|
|
2505
|
+
break;
|
|
2506
|
+
}
|
|
2507
|
+
case "index": {
|
|
2508
|
+
const base = input.base[path] ? texts[input.base[path]] : void 0;
|
|
2509
|
+
const merged = mergeIndex(base, teamText(path), local);
|
|
2510
|
+
const hash = sha2562(merged.text);
|
|
2511
|
+
plan.manifest[path] = hash;
|
|
2512
|
+
if (merged.text !== local) {
|
|
2513
|
+
plan.writes[path] = merged.text;
|
|
2514
|
+
plan.pulled.push(path);
|
|
2515
|
+
}
|
|
2516
|
+
if (hash !== team) {
|
|
2517
|
+
plan.upload[path] = merged.text;
|
|
2518
|
+
plan.pushed.push(path);
|
|
2519
|
+
}
|
|
2520
|
+
plan.indexTeamLines.push(...merged.teamLines);
|
|
2521
|
+
break;
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
return plan;
|
|
2526
|
+
}
|
|
2527
|
+
var LINK_RE = /\]\(\s*<?([^)\s>]+)>?(?:\s+["'(][^)]*)?\)/;
|
|
2528
|
+
function entries(lines) {
|
|
2529
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2530
|
+
return lines.map((line) => {
|
|
2531
|
+
const link = LINK_RE.exec(line);
|
|
2532
|
+
const id = link ? `link:${link[1]}` : `text:${line}`;
|
|
2533
|
+
const n = seen.get(id) ?? 0;
|
|
2534
|
+
seen.set(id, n + 1);
|
|
2535
|
+
return { key: n ? `${id}#${n}` : id, line };
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
var splitLines = (text3) => text3.split(/\r?\n/);
|
|
2539
|
+
function mergeIndex(base, team, local) {
|
|
2540
|
+
const eol = local.includes("\r\n") ? "\r\n" : "\n";
|
|
2541
|
+
const b = new Map(entries(base === void 0 ? [] : splitLines(base)).map((e) => [e.key, e.line]));
|
|
2542
|
+
const teamEntries = entries(splitLines(team));
|
|
2543
|
+
const t = new Map(teamEntries.map((e) => [e.key, e.line]));
|
|
2544
|
+
const localEntries = entries(splitLines(local));
|
|
2545
|
+
const result = [];
|
|
2546
|
+
const teamLines = [];
|
|
2547
|
+
for (const e of localEntries) {
|
|
2548
|
+
const was = b.get(e.key);
|
|
2549
|
+
const theirs = t.get(e.key);
|
|
2550
|
+
if (theirs === e.line || theirs === was) result.push(e);
|
|
2551
|
+
else if (e.line === was) {
|
|
2552
|
+
if (theirs !== void 0) result.push({ key: e.key, line: theirs });
|
|
2553
|
+
} else {
|
|
2554
|
+
result.push(e);
|
|
2555
|
+
if (theirs !== void 0) teamLines.push(theirs);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
const baseEntries = entries(base === void 0 ? [] : splitLines(base));
|
|
2559
|
+
const localKeys = new Set(localEntries.map((e) => e.key));
|
|
2560
|
+
const replacedHere = /* @__PURE__ */ new Map();
|
|
2561
|
+
baseEntries.forEach((e, i) => {
|
|
2562
|
+
if (e.key.startsWith("text:") && !t.has(e.key) && !localKeys.has(e.key)) {
|
|
2563
|
+
replacedHere.set(e.key, i ? baseEntries[i - 1].key : "");
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
const inResult = (key) => result.findIndex((e) => e.key === key);
|
|
2567
|
+
teamEntries.forEach((e, i) => {
|
|
2568
|
+
if (inResult(e.key) !== -1) return;
|
|
2569
|
+
const was = b.get(e.key);
|
|
2570
|
+
const before = i ? teamEntries[i - 1].key : "";
|
|
2571
|
+
const both = e.key.startsWith("text:") && [...replacedHere].find(([, prev]) => prev === before);
|
|
2572
|
+
if (was === void 0 && both) {
|
|
2573
|
+
replacedHere.delete(both[0]);
|
|
2574
|
+
teamLines.push(e.line);
|
|
2575
|
+
} else if (was === void 0) {
|
|
2576
|
+
let at = -1;
|
|
2577
|
+
for (let j = i - 1; j >= 0 && at === -1; j--) at = inResult(teamEntries[j].key);
|
|
2578
|
+
result.splice(at + 1, 0, e);
|
|
2579
|
+
} else if (e.line !== was) {
|
|
2580
|
+
teamLines.push(e.line);
|
|
2581
|
+
}
|
|
2582
|
+
});
|
|
2583
|
+
return { text: result.map((e) => e.line).join(eol), teamLines };
|
|
2584
|
+
}
|
|
2585
|
+
var groupKey = (kind, normalised) => `${kind}:${normalised}`;
|
|
2586
|
+
function collectValues(files) {
|
|
2587
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2588
|
+
const homePaths = [];
|
|
2589
|
+
const findings = {};
|
|
2590
|
+
for (const path of Object.keys(files).sort()) {
|
|
2591
|
+
const found = findPersonValues(files[path]);
|
|
2592
|
+
findings[path] = found;
|
|
2593
|
+
for (const f of found) {
|
|
2594
|
+
if (f.kind === HOME_PATH) {
|
|
2595
|
+
if (!homePaths.includes(path)) homePaths.push(path);
|
|
2596
|
+
continue;
|
|
2597
|
+
}
|
|
2598
|
+
const key = groupKey(f.kind, f.normalised);
|
|
2599
|
+
const group = groups.get(key);
|
|
2600
|
+
if (group) {
|
|
2601
|
+
if (!group.files.includes(path)) group.files.push(path);
|
|
2602
|
+
} else {
|
|
2603
|
+
groups.set(key, {
|
|
2604
|
+
kind: f.kind,
|
|
2605
|
+
value: f.value,
|
|
2606
|
+
normalised: f.normalised,
|
|
2607
|
+
files: [path],
|
|
2608
|
+
first: { path, line: f.line, text: splitLines(files[path])[f.line - 1].trim() }
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
return { groups: [...groups.values()], homePaths, findings };
|
|
2614
|
+
}
|
|
2615
|
+
function personToken(personId, label) {
|
|
2616
|
+
return label ? `\`${personId}\` (${label})` : `\`${personId}\``;
|
|
2617
|
+
}
|
|
2618
|
+
function rewriteValues(text3, findings, replacement) {
|
|
2619
|
+
let out = "";
|
|
2620
|
+
let last = 0;
|
|
2621
|
+
for (const f of [...findings].sort((a, b) => a.start - b.start)) {
|
|
2622
|
+
const to = replacement(f);
|
|
2623
|
+
if (to === void 0) continue;
|
|
2624
|
+
out += text3.slice(last, f.start) + to;
|
|
2625
|
+
last = f.end;
|
|
2626
|
+
}
|
|
2627
|
+
return out + text3.slice(last);
|
|
2628
|
+
}
|
|
2629
|
+
function lineDiff(before, after) {
|
|
2630
|
+
const a = splitLines(before);
|
|
2631
|
+
const b = splitLines(after);
|
|
2632
|
+
const out = [];
|
|
2633
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
2634
|
+
if (a[i] === b[i]) continue;
|
|
2635
|
+
if (a[i] !== void 0) out.push(`- ${a[i]}`);
|
|
2636
|
+
if (b[i] !== void 0) out.push(`+ ${b[i]}`);
|
|
2637
|
+
}
|
|
2638
|
+
return out;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
// src/lib/team-setup-files.ts
|
|
2642
|
+
import { execFile } from "child_process";
|
|
2643
|
+
import { chmod, mkdir as mkdir2, readFile as readFile2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
2644
|
+
import { homedir as homedir3, userInfo } from "os";
|
|
2645
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
2646
|
+
import { promisify } from "util";
|
|
2647
|
+
var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2648
|
+
function isWorkspace(value) {
|
|
2649
|
+
return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
|
|
2650
|
+
}
|
|
2651
|
+
function homeDir() {
|
|
2652
|
+
return homedir3();
|
|
2653
|
+
}
|
|
2654
|
+
function systemdoxDir() {
|
|
2655
|
+
return join2(homeDir(), ".claude", "systemdox");
|
|
2656
|
+
}
|
|
2657
|
+
function workspaceDir(workspace) {
|
|
2658
|
+
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
2659
|
+
return join2(systemdoxDir(), workspace);
|
|
2660
|
+
}
|
|
2661
|
+
function statePath(workspace) {
|
|
2662
|
+
return join2(workspaceDir(workspace), "state.json");
|
|
2663
|
+
}
|
|
2664
|
+
async function readJson(path, empty) {
|
|
2665
|
+
let text3;
|
|
2666
|
+
try {
|
|
2667
|
+
text3 = await readFile2(path, "utf8");
|
|
2668
|
+
} catch (e) {
|
|
2669
|
+
if (e.code === "ENOENT") return empty;
|
|
2670
|
+
throw e;
|
|
2671
|
+
}
|
|
2672
|
+
try {
|
|
2673
|
+
return JSON.parse(text3);
|
|
2674
|
+
} catch {
|
|
2675
|
+
throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
async function writeJsonAtomically(path, value, mode) {
|
|
2679
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
2680
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
2681
|
+
await writeFile2(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
|
|
2682
|
+
await rename(tmp, path);
|
|
2683
|
+
}
|
|
2684
|
+
var run = promisify(execFile);
|
|
2685
|
+
async function readState(workspace) {
|
|
2686
|
+
return readJson(statePath(workspace), {});
|
|
2687
|
+
}
|
|
2688
|
+
async function writeState(workspace, state) {
|
|
2689
|
+
await writeJsonAtomically(statePath(workspace), state);
|
|
2690
|
+
}
|
|
2691
|
+
async function updateState(workspace, change) {
|
|
2692
|
+
const state = await readState(workspace);
|
|
2693
|
+
change(state);
|
|
2694
|
+
await writeState(workspace, state);
|
|
2695
|
+
return state;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
// src/sync-memory.ts
|
|
2699
|
+
var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
|
|
2700
|
+
var SyncFailed = class extends Error {
|
|
2701
|
+
};
|
|
2702
|
+
function refusal(e) {
|
|
2703
|
+
if (!(e instanceof Error)) return {};
|
|
2704
|
+
const { status, code } = e;
|
|
2705
|
+
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
2706
|
+
}
|
|
2707
|
+
var toPath = (root, rel) => join3(root, ...rel.split("/"));
|
|
2708
|
+
async function readFolder(root) {
|
|
2709
|
+
const files = {};
|
|
2710
|
+
const names = /* @__PURE__ */ new Set();
|
|
2711
|
+
async function walk2(dir, prefix) {
|
|
2712
|
+
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
2713
|
+
if (e.name.startsWith(".")) continue;
|
|
2714
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
2715
|
+
if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
|
|
2716
|
+
else if (e.isFile()) {
|
|
2717
|
+
names.add(rel.toLowerCase());
|
|
2718
|
+
if (e.name.endsWith(".md")) files[rel] = await readFile3(join3(dir, e.name), "utf8");
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
if (existsSync2(root)) await walk2(root, "");
|
|
2723
|
+
return { files, names };
|
|
2724
|
+
}
|
|
2725
|
+
async function pullTeam(client, ws, project) {
|
|
2726
|
+
try {
|
|
2727
|
+
const latest = await client.getMemory(ws, project);
|
|
2728
|
+
return {
|
|
2729
|
+
version: latest.version,
|
|
2730
|
+
manifest: Object.fromEntries(Object.entries(latest.files).map(([p, f]) => [p, f.sha256])),
|
|
2731
|
+
exists: true
|
|
2732
|
+
};
|
|
2733
|
+
} catch (e) {
|
|
2734
|
+
if (refusal(e).status === 404) return { version: 0, manifest: {}, exists: false };
|
|
2735
|
+
throw e;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
async function readTexts(client, ws, project, hashes) {
|
|
2739
|
+
const texts = {};
|
|
2740
|
+
const one = async (batch) => {
|
|
2741
|
+
let left = batch;
|
|
2742
|
+
while (left.length) {
|
|
2743
|
+
const got = await client.readBlobs(ws, project, left);
|
|
2744
|
+
for (const b of got.blobs) texts[b.sha256] = b.content;
|
|
2745
|
+
if (!got.blobs.length) break;
|
|
2746
|
+
left = got.remaining;
|
|
2747
|
+
}
|
|
2748
|
+
};
|
|
2749
|
+
for (let i = 0; i < hashes.length; i += 1e3) {
|
|
2750
|
+
const batch = hashes.slice(i, i + 1e3);
|
|
2751
|
+
try {
|
|
2752
|
+
await one(batch);
|
|
2753
|
+
} catch (e) {
|
|
2754
|
+
if (!(refusal(e).status === 404)) throw e;
|
|
2755
|
+
for (const h of batch) {
|
|
2756
|
+
try {
|
|
2757
|
+
await one([h]);
|
|
2758
|
+
} catch (err) {
|
|
2759
|
+
if (!(refusal(err).status === 404)) throw err;
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2764
|
+
return texts;
|
|
2765
|
+
}
|
|
2766
|
+
var Report = class {
|
|
2767
|
+
pulled = /* @__PURE__ */ new Set();
|
|
2768
|
+
pushed = /* @__PURE__ */ new Set();
|
|
2769
|
+
deletedHere = /* @__PURE__ */ new Set();
|
|
2770
|
+
deletedFromTeam = /* @__PURE__ */ new Set();
|
|
2771
|
+
conflicts = [];
|
|
2772
|
+
skipped = /* @__PURE__ */ new Map();
|
|
2773
|
+
people = [];
|
|
2774
|
+
toAnswer = /* @__PURE__ */ new Map();
|
|
2775
|
+
add(plan) {
|
|
2776
|
+
plan.pulled.forEach((p) => this.pulled.add(p));
|
|
2777
|
+
plan.pushed.forEach((p) => this.pushed.add(p));
|
|
2778
|
+
plan.deletedHere.forEach((p) => this.deletedHere.add(p));
|
|
2779
|
+
plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
|
|
2780
|
+
for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
|
|
2781
|
+
for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
|
|
2782
|
+
}
|
|
2783
|
+
skip(path, reason) {
|
|
2784
|
+
if (!this.skipped.has(path)) this.skipped.set(path, reason);
|
|
2785
|
+
}
|
|
2786
|
+
lines(summary, dryRun) {
|
|
2787
|
+
const out = [summary];
|
|
2788
|
+
const section = (title, items) => {
|
|
2789
|
+
if (items.length) out.push("", `${title}:`, ...items.map((i) => `- ${i}`));
|
|
2790
|
+
};
|
|
2791
|
+
const sorted = (s) => [...s].sort();
|
|
2792
|
+
section(dryRun ? "Would pull" : "Pulled", sorted(this.pulled));
|
|
2793
|
+
section(dryRun ? "Would push" : "Pushed", sorted(this.pushed));
|
|
2794
|
+
section(dryRun ? "Would delete" : "Deleted", [
|
|
2795
|
+
...sorted(this.deletedHere).map((p) => `${p} (here: the team deleted it)`),
|
|
2796
|
+
...sorted(this.deletedFromTeam).map((p) => `${p} (from the team: you deleted it)`)
|
|
2797
|
+
]);
|
|
2798
|
+
section("Conflicts", this.conflicts);
|
|
2799
|
+
section(
|
|
2800
|
+
"Skipped",
|
|
2801
|
+
[...this.skipped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([p, why]) => `${p}: ${why}`)
|
|
2802
|
+
);
|
|
2803
|
+
section(
|
|
2804
|
+
"People",
|
|
2805
|
+
this.people.map(
|
|
2806
|
+
(a) => [a.kind, a.answer, a.label, a.personId && `\`${a.personId}\``, a.files !== void 0 && `${a.files} files`].filter(Boolean).join(", ")
|
|
2807
|
+
)
|
|
2808
|
+
);
|
|
2809
|
+
if (this.toAnswer.size) {
|
|
2810
|
+
out.push(
|
|
2811
|
+
"",
|
|
2812
|
+
`Values still to answer: ${[...this.toAnswer.entries()].map(([kind, n]) => `${n} ${kind}`).join(", ")}.`
|
|
2813
|
+
);
|
|
2814
|
+
}
|
|
2815
|
+
return out;
|
|
2816
|
+
}
|
|
2817
|
+
};
|
|
2818
|
+
var ANSWER_WORDS = {
|
|
2819
|
+
member: "a member's contact",
|
|
2820
|
+
person: "someone else",
|
|
2821
|
+
ours: "ours, not a person"
|
|
2822
|
+
};
|
|
2823
|
+
function labelProblem(label) {
|
|
2824
|
+
if (!label) return "a label is needed";
|
|
2825
|
+
if (label.length > 60) return "a label is at most 60 characters";
|
|
2826
|
+
if (findPersonValues(label).length) return "a label is a role, never a phone number, an email address or a path";
|
|
2827
|
+
return void 0;
|
|
2828
|
+
}
|
|
2829
|
+
async function askLabel(ask, out, question, optional) {
|
|
2830
|
+
for (; ; ) {
|
|
2831
|
+
const label = (await ask(question)).trim();
|
|
2832
|
+
if (optional && !label) return void 0;
|
|
2833
|
+
const problem = labelProblem(label);
|
|
2834
|
+
if (!problem) return label;
|
|
2835
|
+
out(` ${problem}.`);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
async function people(opts, ws, files, report, hint) {
|
|
2839
|
+
const { client, out, ask, dryRun } = opts;
|
|
2840
|
+
const terminal = !!ask && !dryRun;
|
|
2841
|
+
const held = /* @__PURE__ */ new Set();
|
|
2842
|
+
const leftOut = /* @__PURE__ */ new Set();
|
|
2843
|
+
const current = { ...files };
|
|
2844
|
+
for (const [path, text3] of Object.entries(current)) {
|
|
2845
|
+
const secret = findSecrets(text3)[0];
|
|
2846
|
+
if (secret) {
|
|
2847
|
+
held.add(path);
|
|
2848
|
+
report.skip(path, `a secret (${secret.kind}) on line ${secret.line}: remove it, then sync again`);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
const { groups, findings } = collectValues(current);
|
|
2852
|
+
const answers = /* @__PURE__ */ new Map();
|
|
2853
|
+
for (let i = 0; i < groups.length; i += 500) {
|
|
2854
|
+
const batch = groups.slice(i, i + 500);
|
|
2855
|
+
const got = await client.lookupValues(ws, batch.map((g) => ({ kind: g.kind, value: g.value })));
|
|
2856
|
+
batch.forEach((g, j) => {
|
|
2857
|
+
const a = got.answers[j];
|
|
2858
|
+
if (a && a.answer !== "unanswered") answers.set(groupKey(g.kind, g.normalised), a);
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
const vaultDown = /* @__PURE__ */ new Set();
|
|
2862
|
+
if (terminal) {
|
|
2863
|
+
const filed = [];
|
|
2864
|
+
for (const g of groups) {
|
|
2865
|
+
const key = groupKey(g.kind, g.normalised);
|
|
2866
|
+
if (answers.has(key)) continue;
|
|
2867
|
+
let choices = ["m", "p", "o", "x", "s"];
|
|
2868
|
+
for (; ; ) {
|
|
2869
|
+
const question = [
|
|
2870
|
+
`${g.value} (${g.kind}) is in ${g.files.length} file${g.files.length === 1 ? "" : "s"}. First line it appears on, in ${g.first.path}:`,
|
|
2871
|
+
` ${g.first.text}`,
|
|
2872
|
+
[
|
|
2873
|
+
choices.includes("m") && "[m] a member's contact",
|
|
2874
|
+
"[p] someone else",
|
|
2875
|
+
"[o] ours, not a person",
|
|
2876
|
+
"[x] leave these files out",
|
|
2877
|
+
"[s] skip for now"
|
|
2878
|
+
].filter(Boolean).join(" "),
|
|
2879
|
+
`Answer (${choices.join("/")}): `
|
|
2880
|
+
].join("\n");
|
|
2881
|
+
const answer = (await ask(question)).trim().toLowerCase();
|
|
2882
|
+
if (!choices.includes(answer)) continue;
|
|
2883
|
+
if (answer === "s") break;
|
|
2884
|
+
if (answer === "x") {
|
|
2885
|
+
for (const path of g.files) {
|
|
2886
|
+
if (leftOut.has(path)) continue;
|
|
2887
|
+
current[path] = addShareFalse(current[path]);
|
|
2888
|
+
await writeFile3(toPath(opts.dir, path), current[path], "utf8");
|
|
2889
|
+
leftOut.add(path);
|
|
2890
|
+
}
|
|
2891
|
+
report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
|
|
2892
|
+
break;
|
|
2893
|
+
}
|
|
2894
|
+
if (answer === "p") {
|
|
2895
|
+
const label2 = await askLabel(ask, out, "Their role, as a label (e.g. reseller, sales; never a name): ", false);
|
|
2896
|
+
let entry = filed.length ? await (async () => {
|
|
2897
|
+
const list = filed.map((f, i) => ` ${i + 1}. ${f.label}: ${f.groups.map((x) => x.value).join(", ")}`);
|
|
2898
|
+
for (; ; ) {
|
|
2899
|
+
const same = (await ask(["The same person as one filed above?", ...list, "Number, or Enter for a new person: "].join("\n"))).trim();
|
|
2900
|
+
if (!same) return void 0;
|
|
2901
|
+
const n = Number(same);
|
|
2902
|
+
if (Number.isInteger(n) && n >= 1 && n <= filed.length && filed[n - 1].groups.length < 4) return filed[n - 1];
|
|
2903
|
+
}
|
|
2904
|
+
})() : void 0;
|
|
2905
|
+
if (!entry) {
|
|
2906
|
+
entry = { groups: [], label: label2 };
|
|
2907
|
+
filed.push(entry);
|
|
2908
|
+
}
|
|
2909
|
+
entry.groups.push(g);
|
|
2910
|
+
break;
|
|
2911
|
+
}
|
|
2912
|
+
const label = answer === "m" ? await askLabel(ask, out, "A role label for it (e.g. owner, business mobile), or Enter for none: ", true) : void 0;
|
|
2913
|
+
let result;
|
|
2914
|
+
try {
|
|
2915
|
+
result = await client.answerValues(ws, [
|
|
2916
|
+
{ kind: g.kind, value: g.value, answer: answer === "m" ? "member" : "ours", ...label ? { label } : {} }
|
|
2917
|
+
]);
|
|
2918
|
+
} catch (e) {
|
|
2919
|
+
if (refusal(e).code === "SYD-K019") {
|
|
2920
|
+
vaultDown.add(key);
|
|
2921
|
+
out(" The vault did not answer (SYD-K019): the files holding it wait for the next sync.");
|
|
2922
|
+
break;
|
|
2923
|
+
}
|
|
2924
|
+
throw e;
|
|
2925
|
+
}
|
|
2926
|
+
if (result.stored[0]) {
|
|
2927
|
+
answers.set(key, result.stored[0]);
|
|
2928
|
+
report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
|
|
2929
|
+
break;
|
|
2930
|
+
}
|
|
2931
|
+
const refused = result.refused[0];
|
|
2932
|
+
if (refused?.code === "SYD-K021") {
|
|
2933
|
+
out(` ${refused.detail}. Add them in Staff first, or answer p to file them.`);
|
|
2934
|
+
choices = choices.filter((c) => c !== "m");
|
|
2935
|
+
} else if (refused) out(` ${refused.detail}`);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
for (let i = 0; i < filed.length; i += 50) {
|
|
2939
|
+
const batch = filed.slice(i, i + 50);
|
|
2940
|
+
let result;
|
|
2941
|
+
try {
|
|
2942
|
+
result = await client.fileMemoryPeople(
|
|
2943
|
+
ws,
|
|
2944
|
+
batch.map((f) => ({ values: f.groups.map((g) => ({ kind: g.kind, value: g.value })), label: f.label }))
|
|
2945
|
+
);
|
|
2946
|
+
} catch (e) {
|
|
2947
|
+
if (refusal(e).code === "SYD-K019") {
|
|
2948
|
+
out("Contacts did not answer (SYD-K019): the files holding those values wait for the next sync.");
|
|
2949
|
+
batch.forEach((f) => f.groups.forEach((g) => vaultDown.add(groupKey(g.kind, g.normalised))));
|
|
2950
|
+
continue;
|
|
2951
|
+
}
|
|
2952
|
+
throw e;
|
|
2953
|
+
}
|
|
2954
|
+
for (const stored of result.stored) {
|
|
2955
|
+
let key;
|
|
2956
|
+
try {
|
|
2957
|
+
key = groupKey(stored.kind, normalise(stored.kind, stored.value));
|
|
2958
|
+
} catch {
|
|
2959
|
+
continue;
|
|
2960
|
+
}
|
|
2961
|
+
answers.set(key, stored);
|
|
2962
|
+
}
|
|
2963
|
+
for (const r of result.refused) {
|
|
2964
|
+
out(` ${batch[r.index]?.label ?? ""}: ${r.detail}`);
|
|
2965
|
+
}
|
|
2966
|
+
for (const f of batch) {
|
|
2967
|
+
const a = answers.get(groupKey(f.groups[0].kind, f.groups[0].normalised));
|
|
2968
|
+
if (a?.answer === "person") {
|
|
2969
|
+
for (const g of f.groups) report.people.push({ kind: g.kind, answer: ANSWER_WORDS.person, label: a.label, personId: a.personId });
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
const rewritten = {};
|
|
2975
|
+
for (const [path, text3] of Object.entries(current)) {
|
|
2976
|
+
if (leftOut.has(path) || held.has(path)) continue;
|
|
2977
|
+
const next = rewriteValues(text3, findings[path] ?? [], (f) => {
|
|
2978
|
+
if (f.kind === HOME_PATH) return "~";
|
|
2979
|
+
const a = answers.get(groupKey(f.kind, f.normalised));
|
|
2980
|
+
return a && (a.answer === "member" || a.answer === "person") && a.personId ? personToken(a.personId, a.label) : void 0;
|
|
2981
|
+
});
|
|
2982
|
+
if (next !== text3) rewritten[path] = next;
|
|
2983
|
+
}
|
|
2984
|
+
const paths = Object.keys(rewritten).sort();
|
|
2985
|
+
if (paths.length && terminal) {
|
|
2986
|
+
out("", "The rewrite:");
|
|
2987
|
+
for (const path of paths) out(path, ...lineDiff(current[path], rewritten[path]).map((l) => ` ${l}`));
|
|
2988
|
+
const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
|
|
2989
|
+
if (yes) {
|
|
2990
|
+
for (const path of paths) {
|
|
2991
|
+
await writeFile3(toPath(opts.dir, path), rewritten[path], "utf8");
|
|
2992
|
+
current[path] = rewritten[path];
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
for (const [path, text3] of Object.entries(current)) {
|
|
2997
|
+
if (leftOut.has(path) || held.has(path)) continue;
|
|
2998
|
+
const pending = findPersonValues(text3).filter((f) => {
|
|
2999
|
+
if (f.kind === HOME_PATH) return true;
|
|
3000
|
+
return answers.get(groupKey(f.kind, f.normalised))?.answer !== "ours";
|
|
3001
|
+
});
|
|
3002
|
+
if (!pending.length) continue;
|
|
3003
|
+
held.add(path);
|
|
3004
|
+
const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
|
|
3005
|
+
if (keys.some((k) => vaultDown.has(k))) {
|
|
3006
|
+
report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
|
|
3007
|
+
} else if (keys.some((k) => !answers.has(k))) {
|
|
3008
|
+
report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
|
|
3009
|
+
} else {
|
|
3010
|
+
report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
for (const g of groups) {
|
|
3014
|
+
const key = groupKey(g.kind, g.normalised);
|
|
3015
|
+
if (!answers.has(key) && g.files.some((p) => !leftOut.has(p))) {
|
|
3016
|
+
report.toAnswer.set(g.kind, (report.toAnswer.get(g.kind) ?? 0) + 1);
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
return { files: current, held, leftOut };
|
|
3020
|
+
}
|
|
3021
|
+
async function applyPlan(dir, plan) {
|
|
3022
|
+
for (const [path, text3] of Object.entries(plan.writes)) {
|
|
3023
|
+
const full = toPath(dir, path);
|
|
3024
|
+
await mkdir3(dirname3(full), { recursive: true });
|
|
3025
|
+
await writeFile3(full, text3, "utf8");
|
|
3026
|
+
}
|
|
3027
|
+
for (const path of plan.deletes) await rm3(toPath(dir, path), { force: true });
|
|
3028
|
+
}
|
|
3029
|
+
var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
|
|
3030
|
+
async function upload(client, ws, project, plan, team, report) {
|
|
3031
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
3032
|
+
for (const path of Object.keys(plan.upload)) {
|
|
3033
|
+
const h = plan.manifest[path];
|
|
3034
|
+
byHash.set(h, [...byHash.get(h) ?? [], path]);
|
|
3035
|
+
}
|
|
3036
|
+
const hashes = [...byHash.keys()];
|
|
3037
|
+
const missing = [];
|
|
3038
|
+
for (let i = 0; i < hashes.length; i += 1e4) {
|
|
3039
|
+
missing.push(...(await client.findMissingBlobs(ws, project, hashes.slice(i, i + 1e4))).missing);
|
|
3040
|
+
}
|
|
3041
|
+
const blobs = missing.map((h) => ({ sha256: h, content: plan.upload[byHash.get(h)[0]] }));
|
|
3042
|
+
let batch = [];
|
|
3043
|
+
let bytes = 0;
|
|
3044
|
+
const send = async () => {
|
|
3045
|
+
if (!batch.length) return;
|
|
3046
|
+
const result = await client.uploadBlobs(ws, project, batch);
|
|
3047
|
+
for (const r of result.refused) {
|
|
3048
|
+
for (const path of byHash.get(r.sha256) ?? []) {
|
|
3049
|
+
if (team[path]) plan.manifest[path] = team[path];
|
|
3050
|
+
else delete plan.manifest[path];
|
|
3051
|
+
plan.pushed = plan.pushed.filter((p) => p !== path);
|
|
3052
|
+
report.pushed.delete(path);
|
|
3053
|
+
report.skip(path, `refused by SystemDox, ${r.detail.startsWith(r.code) ? r.detail : `${r.code}: ${r.detail}`}`);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
batch = [];
|
|
3057
|
+
bytes = 0;
|
|
3058
|
+
};
|
|
3059
|
+
for (const blob of blobs) {
|
|
3060
|
+
const size = Buffer.byteLength(blob.content, "utf8");
|
|
3061
|
+
if (batch.length === 100 || batch.length && bytes + size > 5 * 1024 * 1024 - 64 * 1024) await send();
|
|
3062
|
+
batch.push(blob);
|
|
3063
|
+
bytes += size;
|
|
3064
|
+
}
|
|
3065
|
+
await send();
|
|
3066
|
+
}
|
|
3067
|
+
async function syncMemory(opts) {
|
|
3068
|
+
const { client, out } = opts;
|
|
3069
|
+
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
3070
|
+
try {
|
|
3071
|
+
const own = await client.getOwnTeamSetup();
|
|
3072
|
+
const ws = own.workspace;
|
|
3073
|
+
const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
|
|
3074
|
+
if (!project) {
|
|
3075
|
+
throw new SyncFailed(
|
|
3076
|
+
own.memory.length ? `${ws} has ${own.memory.length} team memories (${own.memory.map((m) => m.project).join(", ")}): name one with --project.` : `${ws} has no team memory yet: name the project to start with --project.`
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
const dir = resolve3(opts.dir ?? join3(workspaceDir(ws), "memory", project));
|
|
3080
|
+
const run2 = { ...opts, dir };
|
|
3081
|
+
const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
|
|
3082
|
+
const report = new Report();
|
|
3083
|
+
const date = localDate(now);
|
|
3084
|
+
const stored = (await readState(ws)).memory?.[project];
|
|
3085
|
+
let team = await pullTeam(client, ws, project);
|
|
3086
|
+
let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
|
|
3087
|
+
const folder = await readFolder(dir);
|
|
3088
|
+
const shared = {};
|
|
3089
|
+
const notShared = /* @__PURE__ */ new Set();
|
|
3090
|
+
for (const [path, text3] of Object.entries(folder.files)) {
|
|
3091
|
+
if (!isShareable(path, text3)) notShared.add(path);
|
|
3092
|
+
else if (!isFilePath(path)) {
|
|
3093
|
+
notShared.add(path);
|
|
3094
|
+
report.skip(path, "its name has characters a team memory path cannot hold (letters, digits, '.', '_', '-')");
|
|
3095
|
+
} else shared[path] = text3;
|
|
3096
|
+
}
|
|
3097
|
+
const importWrites = {};
|
|
3098
|
+
if (opts.importDir) {
|
|
3099
|
+
const other = await readFolder(resolve3(opts.importDir));
|
|
3100
|
+
const taken = new Set(folder.names);
|
|
3101
|
+
for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
|
|
3102
|
+
if (!isShareable(path, text3) || !isFilePath(path)) continue;
|
|
3103
|
+
const here = folder.files[path];
|
|
3104
|
+
if (here === void 0) {
|
|
3105
|
+
shared[path] = text3;
|
|
3106
|
+
importWrites[path] = text3;
|
|
3107
|
+
taken.add(path.toLowerCase());
|
|
3108
|
+
} else if (here !== text3) {
|
|
3109
|
+
const copy = conflictCopyName(path, date, taken);
|
|
3110
|
+
taken.add(copy.toLowerCase());
|
|
3111
|
+
importWrites[copy] = text3;
|
|
3112
|
+
report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
folder.names = taken;
|
|
3116
|
+
if (!opts.dryRun) {
|
|
3117
|
+
for (const [path, text3] of Object.entries(importWrites)) {
|
|
3118
|
+
const full = toPath(dir, path);
|
|
3119
|
+
await mkdir3(dirname3(full), { recursive: true });
|
|
3120
|
+
await writeFile3(full, text3, "utf8");
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
const found = await people(run2, ws, shared, report, hint);
|
|
3125
|
+
for (const path of found.leftOut) {
|
|
3126
|
+
delete found.files[path];
|
|
3127
|
+
notShared.add(path);
|
|
3128
|
+
}
|
|
3129
|
+
const excluded = /* @__PURE__ */ new Set([...found.held, ...notShared]);
|
|
3130
|
+
for (const path of found.held) delete found.files[path];
|
|
3131
|
+
for (let attempt = 1; ; attempt++) {
|
|
3132
|
+
const input = { base: base.manifest, team: team.manifest, local: found.files, excluded, taken: folder.names, date };
|
|
3133
|
+
const { rows, needed } = decideAll(input);
|
|
3134
|
+
const texts = needed.length ? await readTexts(client, ws, project, needed) : {};
|
|
3135
|
+
const plan = planMerge(input, rows, texts);
|
|
3136
|
+
report.add(plan);
|
|
3137
|
+
if (opts.dryRun) {
|
|
3138
|
+
const skipped = report.skipped.size;
|
|
3139
|
+
out(
|
|
3140
|
+
...report.lines(
|
|
3141
|
+
`Dry run of ${ws}/${project} against version ${team.version}: would pull ${report.pulled.size}, push ${report.pushed.size} and delete ${report.deletedHere.size + report.deletedFromTeam.size}; ${report.conflicts.length} conflicts, ${skipped} skipped. Nothing was asked, written or sent.`,
|
|
3142
|
+
true
|
|
3143
|
+
)
|
|
3144
|
+
);
|
|
3145
|
+
return 0;
|
|
3146
|
+
}
|
|
3147
|
+
await applyPlan(dir, plan);
|
|
3148
|
+
let version = team.version;
|
|
3149
|
+
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
3150
|
+
await upload(client, ws, project, plan, team.manifest, report);
|
|
3151
|
+
}
|
|
3152
|
+
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
3153
|
+
try {
|
|
3154
|
+
version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
|
|
3155
|
+
} catch (e) {
|
|
3156
|
+
if (!(refusal(e).status === 409 && refusal(e).code === "SYD-K010")) throw e;
|
|
3157
|
+
if (attempt === 2) {
|
|
3158
|
+
out(
|
|
3159
|
+
...report.lines(
|
|
3160
|
+
`Not pushed: ${ws}/${project} moved on twice while this sync merged. Your folder holds the merge; run sync-memory again.`,
|
|
3161
|
+
false
|
|
3162
|
+
)
|
|
3163
|
+
);
|
|
3164
|
+
return 2;
|
|
3165
|
+
}
|
|
3166
|
+
base = team;
|
|
3167
|
+
for (const p of plan.pushed) report.pushed.delete(p);
|
|
3168
|
+
const again = await readFolder(dir);
|
|
3169
|
+
folder.names = again.names;
|
|
3170
|
+
found.files = {};
|
|
3171
|
+
for (const [path, text3] of Object.entries(again.files)) {
|
|
3172
|
+
if (excluded.has(path) || !isShareable(path, text3) || !isFilePath(path)) continue;
|
|
3173
|
+
found.files[path] = text3;
|
|
3174
|
+
}
|
|
3175
|
+
team = await pullTeam(client, ws, project);
|
|
3176
|
+
continue;
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
await updateState(ws, (state) => {
|
|
3180
|
+
state.memory = { ...state.memory, [project]: { version, manifest: plan.manifest } };
|
|
3181
|
+
});
|
|
3182
|
+
out(
|
|
3183
|
+
...report.lines(
|
|
3184
|
+
`Synced ${ws}/${project} at version ${version}: ${report.pulled.size} pulled, ${report.pushed.size} pushed, ${report.deletedHere.size + report.deletedFromTeam.size} deleted, ${report.conflicts.length} conflicts, ${report.skipped.size} skipped.`,
|
|
3185
|
+
false
|
|
3186
|
+
)
|
|
3187
|
+
);
|
|
3188
|
+
return 0;
|
|
3189
|
+
}
|
|
3190
|
+
} catch (e) {
|
|
3191
|
+
out(e.message);
|
|
3192
|
+
return 1;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
// src/tools/team-setup.ts
|
|
3197
|
+
var LAYOUT = {
|
|
3198
|
+
hooks: { files: ["hooks/hooks.json"], folders: [{ dir: "scripts", md: false }], required: "hooks/hooks.json" },
|
|
3199
|
+
mcp: { files: [".mcp.json"], folders: [], required: ".mcp.json" },
|
|
3200
|
+
agents: { files: [], folders: [{ dir: "agents", md: true }] },
|
|
3201
|
+
commands: { files: [], folders: [{ dir: "commands", md: true }] },
|
|
3202
|
+
instructions: { files: ["CLAUDE.md"], folders: [], required: "CLAUDE.md" }
|
|
3203
|
+
};
|
|
3204
|
+
function expandHome2(path) {
|
|
3205
|
+
return path.replace(/^~(?=$|[\\/])/, homedir4());
|
|
3206
|
+
}
|
|
3207
|
+
function text2(t) {
|
|
3208
|
+
return { content: [{ type: "text", text: t }] };
|
|
3209
|
+
}
|
|
3210
|
+
async function isLink(path) {
|
|
3211
|
+
try {
|
|
3212
|
+
return (await lstat2(path)).isSymbolicLink();
|
|
3213
|
+
} catch (e) {
|
|
3214
|
+
if (e.code === "ENOENT") return false;
|
|
3215
|
+
throw e;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
async function readPartDirectory(part, directory) {
|
|
3219
|
+
const root = resolve4(directory);
|
|
3220
|
+
const layout = LAYOUT[part];
|
|
3221
|
+
const paths = [];
|
|
3222
|
+
const leftOut = [];
|
|
3223
|
+
const links = [];
|
|
3224
|
+
for (const path of layout.files) {
|
|
3225
|
+
const segments = path.split("/");
|
|
3226
|
+
let present = true;
|
|
3227
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
3228
|
+
const rel = segments.slice(0, i).join("/");
|
|
3229
|
+
if (await isLink(join4(root, ...segments.slice(0, i)))) {
|
|
3230
|
+
links.push(rel);
|
|
3231
|
+
present = false;
|
|
3232
|
+
break;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
if (present && existsSync3(join4(root, ...segments))) paths.push(path);
|
|
3236
|
+
}
|
|
3237
|
+
for (const { dir, md } of layout.folders) {
|
|
3238
|
+
const full = join4(root, dir);
|
|
3239
|
+
if (await isLink(full)) {
|
|
3240
|
+
links.push(dir);
|
|
3241
|
+
continue;
|
|
3242
|
+
}
|
|
3243
|
+
if (!existsSync3(full)) continue;
|
|
3244
|
+
const walked = await walk(full, { skipHidden: true });
|
|
3245
|
+
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
3246
|
+
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
3247
|
+
for (const p of walked.files) {
|
|
3248
|
+
if (md && !p.endsWith(".md")) leftOut.push(`${dir}/${p}`);
|
|
3249
|
+
else paths.push(`${dir}/${p}`);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
if (links.length) {
|
|
3253
|
+
throw new Error(
|
|
3254
|
+
`${links.join(", ")}: links are not followed; copy the file into the folder. Nothing was sent.`
|
|
3255
|
+
);
|
|
3256
|
+
}
|
|
3257
|
+
if (layout.required && !paths.includes(layout.required)) {
|
|
3258
|
+
throw new Error(`${root} has no ${layout.required}: the ${part} part needs it. Nothing was sent.`);
|
|
3259
|
+
}
|
|
3260
|
+
if (!paths.length) {
|
|
3261
|
+
throw new Error(
|
|
3262
|
+
`${root} has no ${layout.folders.map((f) => `${f.dir}/*.md`).join(" or ")}: nothing to publish for ${part}.`
|
|
3263
|
+
);
|
|
3264
|
+
}
|
|
3265
|
+
return { files: await readTextFiles(root, paths.sort(), `the ${part} part`), leftOut };
|
|
3266
|
+
}
|
|
3267
|
+
function describePerson(id, person) {
|
|
3268
|
+
const lines = [`**${person.label ?? "(no label)"}**, \`${id}\``];
|
|
3269
|
+
if (person.mergedInto) {
|
|
3270
|
+
lines.push(`\`${id}\` was merged into \`${person.mergedInto}\`: these are \`${person.mergedInto}\`'s details.`);
|
|
3271
|
+
}
|
|
3272
|
+
lines.push(`- Name: ${person.name ?? "(none held)"}`);
|
|
3273
|
+
lines.push(`- Phone number: ${person.phone ?? "(none held)"}`);
|
|
3274
|
+
lines.push(`- Email address: ${person.email ?? "(none held)"}`);
|
|
3275
|
+
const token = `\`${person.mergedInto ?? id}\`${person.label ? ` (${person.label})` : ""}`;
|
|
3276
|
+
lines.push(
|
|
3277
|
+
"",
|
|
3278
|
+
`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.`
|
|
3279
|
+
);
|
|
3280
|
+
return lines.join("\n");
|
|
3281
|
+
}
|
|
3282
|
+
function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
3283
|
+
const fileSchema = z14.object({
|
|
3284
|
+
path: z14.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
|
|
3285
|
+
content: z14.string().describe("The file's full text, exactly as it is")
|
|
3286
|
+
});
|
|
3287
|
+
server2.registerTool(
|
|
3288
|
+
"publish_setup_part",
|
|
3289
|
+
{
|
|
3290
|
+
title: "Publish Setup Part",
|
|
3291
|
+
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."),
|
|
3292
|
+
inputSchema: {
|
|
3293
|
+
part: z14.enum(PARTS).describe("Which part of the setup"),
|
|
3294
|
+
...localFiles ? {
|
|
3295
|
+
directory: z14.string().optional().describe(
|
|
3296
|
+
"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/"
|
|
3297
|
+
)
|
|
3298
|
+
} : {},
|
|
3299
|
+
files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
|
|
3300
|
+
},
|
|
3301
|
+
annotations: { readOnlyHint: false }
|
|
3302
|
+
},
|
|
3303
|
+
async (args) => {
|
|
3304
|
+
let files;
|
|
3305
|
+
let leftOut = [];
|
|
3306
|
+
if (localFiles && args.directory) {
|
|
3307
|
+
({ files, leftOut } = await readPartDirectory(args.part, expandHome2(args.directory)));
|
|
3308
|
+
} else if (args.files?.length) {
|
|
3309
|
+
files = args.files;
|
|
3310
|
+
} else {
|
|
3311
|
+
throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
|
|
3312
|
+
}
|
|
3313
|
+
const { workspace } = await apiClient2.getOwnTeamSetup();
|
|
3314
|
+
const saved = await apiClient2.publishPartVersion(workspace, args.part, files);
|
|
3315
|
+
const lines = [`Published **${saved.part}** version ${saved.version}.`];
|
|
3316
|
+
if (saved.part !== "instructions" && saved.pluginVersion !== void 0) {
|
|
3317
|
+
lines.push(`Team plugin version ${saved.pluginVersion}; each member accepts it at their next update.`);
|
|
3318
|
+
}
|
|
3319
|
+
if (saved.replacedValues?.length) {
|
|
3320
|
+
lines.push(
|
|
3321
|
+
"Replaced by a reference to each member's own environment variable:",
|
|
3322
|
+
...saved.replacedValues.map((r) => `- ${r.server} ${r.field} ${r.name}: \${${r.name}}`)
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
if (leftOut.length) lines.push(`Left out (not part of ${args.part}): ${leftOut.join(", ")}`);
|
|
3326
|
+
return text2(lines.join("\n"));
|
|
3327
|
+
}
|
|
3328
|
+
);
|
|
3329
|
+
server2.registerTool(
|
|
3330
|
+
"resolve_person",
|
|
3331
|
+
{
|
|
3332
|
+
title: "Resolve Person",
|
|
3333
|
+
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.",
|
|
3334
|
+
inputSchema: {
|
|
3335
|
+
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")
|
|
3336
|
+
},
|
|
3337
|
+
annotations: { readOnlyHint: true }
|
|
3338
|
+
},
|
|
3339
|
+
async (args) => {
|
|
3340
|
+
const { workspace } = await apiClient2.getOwnTeamSetup();
|
|
3341
|
+
const person = await apiClient2.resolvePerson(workspace, args.id);
|
|
3342
|
+
return text2(describePerson(args.id, person));
|
|
3343
|
+
}
|
|
3344
|
+
);
|
|
3345
|
+
if (!localFiles) return;
|
|
3346
|
+
server2.registerTool(
|
|
3347
|
+
"sync_memory",
|
|
3348
|
+
{
|
|
3349
|
+
title: "Sync Memory",
|
|
3350
|
+
description: `Sync one project's team memory on this computer with SystemDox: pull the files the team changed, push the ones changed here, keep both texts of a file changed on both sides. It never answers for a person: a file that still holds a phone number, an email address or a home-folder path is skipped and listed, with the command for the member to run in their own terminal (\`${SYNC_MEMORY_COMMAND}\`). Pass dryRun to see what it would do without writing or sending anything.`,
|
|
3351
|
+
inputSchema: {
|
|
3352
|
+
project: z14.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
|
|
3353
|
+
dryRun: z14.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
|
|
3354
|
+
},
|
|
3355
|
+
annotations: { readOnlyHint: false }
|
|
3356
|
+
},
|
|
3357
|
+
async (args) => {
|
|
3358
|
+
const lines = [];
|
|
3359
|
+
const code = await syncMemory({
|
|
3360
|
+
project: args.project,
|
|
3361
|
+
dryRun: !!args.dryRun,
|
|
3362
|
+
client: apiClient2,
|
|
3363
|
+
out: (...l) => lines.push(...l)
|
|
3364
|
+
});
|
|
3365
|
+
return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
|
|
3366
|
+
}
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
2174
3370
|
// src/server.ts
|
|
2175
|
-
var SERVER_VERSION = true ? "0.1.
|
|
3371
|
+
var SERVER_VERSION = true ? "0.1.47" : "0.0.0-dev";
|
|
2176
3372
|
function createServer(apiClient2, { localFiles = false } = {}) {
|
|
2177
3373
|
const server2 = new McpServer({
|
|
2178
3374
|
name: "systemdox-mcp-server",
|
|
@@ -2193,6 +3389,7 @@ function createServer(apiClient2, { localFiles = false } = {}) {
|
|
|
2193
3389
|
registerCreateProjectRule(server2, apiClient2);
|
|
2194
3390
|
registerCreatePromptTemplate(server2, apiClient2);
|
|
2195
3391
|
registerSkillTools(server2, apiClient2, { localFiles });
|
|
3392
|
+
registerTeamSetupTools(server2, apiClient2, { localFiles });
|
|
2196
3393
|
return server2;
|
|
2197
3394
|
}
|
|
2198
3395
|
|