@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/cli.js
CHANGED
|
@@ -51,6 +51,161 @@ var init_config = __esm({
|
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
// src/lib/team-setup-files.ts
|
|
55
|
+
import { execFile } from "child_process";
|
|
56
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
57
|
+
import { homedir, userInfo } from "os";
|
|
58
|
+
import { dirname, join } from "path";
|
|
59
|
+
import { promisify } from "util";
|
|
60
|
+
function isWorkspace(value) {
|
|
61
|
+
return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
|
|
62
|
+
}
|
|
63
|
+
function homeDir() {
|
|
64
|
+
return homedir();
|
|
65
|
+
}
|
|
66
|
+
function systemdoxDir() {
|
|
67
|
+
return join(homeDir(), ".claude", "systemdox");
|
|
68
|
+
}
|
|
69
|
+
function credentialsPath() {
|
|
70
|
+
return join(systemdoxDir(), "credentials.json");
|
|
71
|
+
}
|
|
72
|
+
function workspaceDir(workspace) {
|
|
73
|
+
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
74
|
+
return join(systemdoxDir(), workspace);
|
|
75
|
+
}
|
|
76
|
+
function statePath(workspace) {
|
|
77
|
+
return join(workspaceDir(workspace), "state.json");
|
|
78
|
+
}
|
|
79
|
+
function pluginRoot(workspace) {
|
|
80
|
+
return join(workspaceDir(workspace), "plugin");
|
|
81
|
+
}
|
|
82
|
+
function pluginDir(workspace, version) {
|
|
83
|
+
return join(pluginRoot(workspace), String(version));
|
|
84
|
+
}
|
|
85
|
+
async function readJson(path, empty) {
|
|
86
|
+
let text3;
|
|
87
|
+
try {
|
|
88
|
+
text3 = await readFile(path, "utf8");
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e.code === "ENOENT") return empty;
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(text3);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function writeJsonAtomically(path, value, mode) {
|
|
100
|
+
await mkdir(dirname(path), { recursive: true });
|
|
101
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
102
|
+
await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
|
|
103
|
+
await rename(tmp, path);
|
|
104
|
+
}
|
|
105
|
+
async function readCredentials() {
|
|
106
|
+
return readJson(credentialsPath(), {});
|
|
107
|
+
}
|
|
108
|
+
async function restrictToMember(path) {
|
|
109
|
+
await chmod(path, 384);
|
|
110
|
+
if (process.platform === "win32") {
|
|
111
|
+
await run("icacls", [path, "/inheritance:r", "/grant:r", `${userInfo().username}:F`], { windowsHide: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function writeCredentials(workspace, apiKey) {
|
|
115
|
+
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
116
|
+
if (!apiKey) throw new Error("an API key is required");
|
|
117
|
+
const path = credentialsPath();
|
|
118
|
+
const credentials = await readCredentials();
|
|
119
|
+
credentials[workspace] = { apiKey };
|
|
120
|
+
await writeJsonAtomically(path, credentials, 384);
|
|
121
|
+
await restrictToMember(path);
|
|
122
|
+
}
|
|
123
|
+
async function apiKeyFor(workspace, env = process.env) {
|
|
124
|
+
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
125
|
+
const entry = (await readCredentials())[workspace];
|
|
126
|
+
return typeof entry?.apiKey === "string" && entry.apiKey ? entry.apiKey : void 0;
|
|
127
|
+
}
|
|
128
|
+
function apiUrlFrom(env = process.env) {
|
|
129
|
+
return requireHttpsApiUrl(env.SYSTEMDOX_API_URL || API_URL_DEFAULT);
|
|
130
|
+
}
|
|
131
|
+
async function readState(workspace) {
|
|
132
|
+
return readJson(statePath(workspace), {});
|
|
133
|
+
}
|
|
134
|
+
async function writeState(workspace, state) {
|
|
135
|
+
await writeJsonAtomically(statePath(workspace), state);
|
|
136
|
+
}
|
|
137
|
+
async function updateState(workspace, change) {
|
|
138
|
+
const state = await readState(workspace);
|
|
139
|
+
change(state);
|
|
140
|
+
await writeState(workspace, state);
|
|
141
|
+
return state;
|
|
142
|
+
}
|
|
143
|
+
async function removeFolder(path) {
|
|
144
|
+
await rm(path, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
function escapeRegExp(s) {
|
|
147
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
148
|
+
}
|
|
149
|
+
function marketplaceWorkspace(marketplaceUrl, apiUrl) {
|
|
150
|
+
let url;
|
|
151
|
+
let api;
|
|
152
|
+
try {
|
|
153
|
+
url = new URL(marketplaceUrl);
|
|
154
|
+
api = new URL(requireHttpsApiUrl(apiUrl));
|
|
155
|
+
} catch {
|
|
156
|
+
return void 0;
|
|
157
|
+
}
|
|
158
|
+
if (url.protocol !== "https:" || url.origin !== api.origin || url.username || url.password) return void 0;
|
|
159
|
+
const base = api.pathname.replace(/\/+$/, "");
|
|
160
|
+
const m = new RegExp(`^${escapeRegExp(base)}/v1/team-setup/([^/]+)/marketplace\\.json$`).exec(url.pathname);
|
|
161
|
+
return m && isWorkspace(m[1]) ? m[1] : void 0;
|
|
162
|
+
}
|
|
163
|
+
async function runMarketplaceAuth(io, env = process.env) {
|
|
164
|
+
const url = env.CLAUDE_CODE_MARKETPLACE_URL;
|
|
165
|
+
if (!url) {
|
|
166
|
+
io.err("marketplace-auth: CLAUDE_CODE_MARKETPLACE_URL is not set; Claude Code sets it when it fetches a marketplace.");
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
let apiUrl;
|
|
170
|
+
try {
|
|
171
|
+
apiUrl = apiUrlFrom(env);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
io.err(`marketplace-auth: ${e.message}`);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
const workspace = marketplaceWorkspace(url, apiUrl);
|
|
177
|
+
if (!workspace) {
|
|
178
|
+
io.err(
|
|
179
|
+
`marketplace-auth: refusing to give a SystemDox key for ${url}: it is not a SystemDox team marketplace on ${apiUrl}`
|
|
180
|
+
);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
let key;
|
|
184
|
+
try {
|
|
185
|
+
key = await apiKeyFor(workspace, env);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
io.err(`marketplace-auth: ${e.message}`);
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
if (!key) {
|
|
191
|
+
io.err(
|
|
192
|
+
`marketplace-auth: no SystemDox key for ${workspace}. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.`
|
|
193
|
+
);
|
|
194
|
+
return 1;
|
|
195
|
+
}
|
|
196
|
+
io.out(JSON.stringify({ Authorization: `Bearer ${key}` }));
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
var SLUG_RE, run;
|
|
200
|
+
var init_team_setup_files = __esm({
|
|
201
|
+
"src/lib/team-setup-files.ts"() {
|
|
202
|
+
"use strict";
|
|
203
|
+
init_config();
|
|
204
|
+
SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
205
|
+
run = promisify(execFile);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
54
209
|
// src/lib/cache.ts
|
|
55
210
|
var ResponseCache;
|
|
56
211
|
var init_cache = __esm({
|
|
@@ -88,11 +243,33 @@ var init_cache = __esm({
|
|
|
88
243
|
});
|
|
89
244
|
|
|
90
245
|
// src/lib/api-client.ts
|
|
91
|
-
var SystemDoxApiClient;
|
|
246
|
+
var SystemDoxApiError, TEAM_SETUP, seg, SystemDoxApiClient;
|
|
92
247
|
var init_api_client = __esm({
|
|
93
248
|
"src/lib/api-client.ts"() {
|
|
94
249
|
"use strict";
|
|
95
250
|
init_cache();
|
|
251
|
+
SystemDoxApiError = class extends Error {
|
|
252
|
+
constructor(status, text3) {
|
|
253
|
+
super(`SystemDox API error ${status}: ${text3}`);
|
|
254
|
+
this.status = status;
|
|
255
|
+
this.text = text3;
|
|
256
|
+
let body;
|
|
257
|
+
try {
|
|
258
|
+
body = JSON.parse(text3);
|
|
259
|
+
} catch {
|
|
260
|
+
body = void 0;
|
|
261
|
+
}
|
|
262
|
+
this.body = body;
|
|
263
|
+
const code = body?.code;
|
|
264
|
+
this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text3)?.[1];
|
|
265
|
+
}
|
|
266
|
+
status;
|
|
267
|
+
text;
|
|
268
|
+
code;
|
|
269
|
+
body;
|
|
270
|
+
};
|
|
271
|
+
TEAM_SETUP = "/v1/team-setup";
|
|
272
|
+
seg = (s) => encodeURIComponent(String(s));
|
|
96
273
|
SystemDoxApiClient = class {
|
|
97
274
|
config;
|
|
98
275
|
cache;
|
|
@@ -114,88 +291,1535 @@ var init_api_client = __esm({
|
|
|
114
291
|
const cached = this.cache.get(cacheKey);
|
|
115
292
|
if (cached !== void 0) return cached;
|
|
116
293
|
const res = await this.fetchRaw(path, params);
|
|
117
|
-
if (!res.ok)
|
|
118
|
-
const body = await res.text();
|
|
119
|
-
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
120
|
-
}
|
|
294
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
121
295
|
const data = await res.json();
|
|
122
296
|
this.cache.set(cacheKey, data);
|
|
123
297
|
return data;
|
|
124
298
|
}
|
|
125
|
-
/** A read that skips the cache: an install must take the version that is
|
|
126
|
-
* latest now, not the one cached before someone published another. */
|
|
127
|
-
async getFresh(path) {
|
|
128
|
-
const res = await this.fetchRaw(path);
|
|
129
|
-
if (!res.ok)
|
|
130
|
-
|
|
131
|
-
|
|
299
|
+
/** A read that skips the cache: an install must take the version that is
|
|
300
|
+
* latest now, not the one cached before someone published another. */
|
|
301
|
+
async getFresh(path) {
|
|
302
|
+
const res = await this.fetchRaw(path);
|
|
303
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
304
|
+
return await res.json();
|
|
305
|
+
}
|
|
306
|
+
async post(path, body) {
|
|
307
|
+
return this.mutate("POST", path, body);
|
|
308
|
+
}
|
|
309
|
+
async put(path, body) {
|
|
310
|
+
return this.mutate("PUT", path, body);
|
|
311
|
+
}
|
|
312
|
+
async mutate(method, path, body) {
|
|
313
|
+
const url = this.buildUrl(path);
|
|
314
|
+
const res = await fetch(url, {
|
|
315
|
+
method,
|
|
316
|
+
headers: {
|
|
317
|
+
// The API Gateway authorizer only reads the Authorization header;
|
|
318
|
+
// X-API-Key is kept for backwards compatibility.
|
|
319
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
320
|
+
"X-API-Key": this.config.apiKey,
|
|
321
|
+
"Content-Type": "application/json"
|
|
322
|
+
},
|
|
323
|
+
body: JSON.stringify(body)
|
|
324
|
+
});
|
|
325
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
326
|
+
return await res.json();
|
|
327
|
+
}
|
|
328
|
+
// ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
|
|
329
|
+
// One method per operationId. Every read skips the cache: setup, sync and
|
|
330
|
+
// plugin-path act on what the workspace holds now.
|
|
331
|
+
/** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
|
|
332
|
+
getOwnTeamSetup() {
|
|
333
|
+
return this.getFresh(TEAM_SETUP);
|
|
334
|
+
}
|
|
335
|
+
getTeamSetup(workspace) {
|
|
336
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
|
|
337
|
+
}
|
|
338
|
+
getMarketplace(workspace) {
|
|
339
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
|
|
340
|
+
}
|
|
341
|
+
getPart(workspace, part) {
|
|
342
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
|
|
343
|
+
}
|
|
344
|
+
publishPartVersion(workspace, part, files) {
|
|
345
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
|
|
346
|
+
}
|
|
347
|
+
getPartVersion(workspace, part, version) {
|
|
348
|
+
return this.getFresh(
|
|
349
|
+
`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
getSetupSkills(workspace) {
|
|
353
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
|
|
354
|
+
}
|
|
355
|
+
setSetupSkills(workspace, skills) {
|
|
356
|
+
return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
|
|
357
|
+
}
|
|
358
|
+
getPlugin(workspace) {
|
|
359
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
|
|
360
|
+
}
|
|
361
|
+
/** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
|
|
362
|
+
* header. The caller checks the BYTES against the hash it was given. */
|
|
363
|
+
async getPluginZip(workspace, version) {
|
|
364
|
+
const res = await fetch(
|
|
365
|
+
this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
|
|
366
|
+
{
|
|
367
|
+
headers: {
|
|
368
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
369
|
+
"X-API-Key": this.config.apiKey,
|
|
370
|
+
Accept: "application/zip"
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
|
|
375
|
+
return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
|
|
376
|
+
}
|
|
377
|
+
listMemoryProjects(workspace) {
|
|
378
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
|
|
379
|
+
}
|
|
380
|
+
getMemory(workspace, project) {
|
|
381
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
|
|
382
|
+
}
|
|
383
|
+
/** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
|
|
384
|
+
* code SYD-K010 and `body.currentVersion`. */
|
|
385
|
+
pushMemoryVersion(workspace, project, body) {
|
|
386
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
|
|
387
|
+
}
|
|
388
|
+
getMemoryVersion(workspace, project, version) {
|
|
389
|
+
return this.getFresh(
|
|
390
|
+
`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
findMissingBlobs(workspace, project, hashes) {
|
|
394
|
+
return this.post(
|
|
395
|
+
`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
|
|
396
|
+
{ hashes }
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
uploadBlobs(workspace, project, blobs) {
|
|
400
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
|
|
401
|
+
}
|
|
402
|
+
readBlobs(workspace, project, hashes) {
|
|
403
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
|
|
404
|
+
}
|
|
405
|
+
listValueAnswers(workspace) {
|
|
406
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
|
|
407
|
+
}
|
|
408
|
+
lookupValues(workspace, values) {
|
|
409
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
|
|
410
|
+
}
|
|
411
|
+
answerValues(workspace, answers) {
|
|
412
|
+
return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
|
|
413
|
+
}
|
|
414
|
+
fileMemoryPeople(workspace, people2) {
|
|
415
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people: people2 });
|
|
416
|
+
}
|
|
417
|
+
resolvePerson(workspace, personId) {
|
|
418
|
+
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
|
|
419
|
+
}
|
|
420
|
+
invalidateCache(path) {
|
|
421
|
+
if (path) {
|
|
422
|
+
this.cache.invalidate(this.buildUrl(path).toString());
|
|
423
|
+
} else {
|
|
424
|
+
this.cache.clear();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
buildUrl(path, params) {
|
|
428
|
+
const base = this.config.apiUrl.replace(/\/+$/, "");
|
|
429
|
+
const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
|
|
430
|
+
if (params) {
|
|
431
|
+
for (const [key, value] of Object.entries(params)) {
|
|
432
|
+
url.searchParams.set(key, value);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return url;
|
|
436
|
+
}
|
|
437
|
+
async fetchRaw(path, params) {
|
|
438
|
+
const url = this.buildUrl(path, params);
|
|
439
|
+
return fetch(url, {
|
|
440
|
+
headers: {
|
|
441
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
442
|
+
"X-API-Key": this.config.apiKey,
|
|
443
|
+
Accept: "application/json"
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// src/lib/person-values.ts
|
|
452
|
+
function lineOf(text3, start) {
|
|
453
|
+
let line = 1;
|
|
454
|
+
for (let i = text3.indexOf("\n"); i !== -1 && i < start; i = text3.indexOf("\n", i + 1)) line++;
|
|
455
|
+
return line;
|
|
456
|
+
}
|
|
457
|
+
function phoneShape(value) {
|
|
458
|
+
const groups = value.replace(/^\++/, "").split(/[ -]/);
|
|
459
|
+
const digits = [...groups.join("")];
|
|
460
|
+
if (value.startsWith("+")) return digits.length >= 8 && digits.length <= 15 && digits[0] !== "0";
|
|
461
|
+
if (digits.length < 10 || digits.length > 11) return false;
|
|
462
|
+
return groups.length === 1 || groups.slice(1).every((g) => GROUP_SIZES.has([...g].length));
|
|
463
|
+
}
|
|
464
|
+
function normalise(kind, value) {
|
|
465
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
466
|
+
if (kind === PHONE) {
|
|
467
|
+
if (!(whole(E164).test(v) || whole(NATIONAL).test(v)) || !phoneShape(v)) {
|
|
468
|
+
throw new Error("not a phone number");
|
|
469
|
+
}
|
|
470
|
+
const digits = v.replace(/[ -]/g, "");
|
|
471
|
+
return digits.startsWith("+") ? digits : NATIONAL_PREFIX + digits.slice(1);
|
|
472
|
+
}
|
|
473
|
+
if (kind === EMAIL) {
|
|
474
|
+
if (!whole(EMAIL_RE).test(v)) throw new Error("not an email address");
|
|
475
|
+
return v.toLowerCase();
|
|
476
|
+
}
|
|
477
|
+
throw new Error(`not a value kind: ${JSON.stringify(kind)}`);
|
|
478
|
+
}
|
|
479
|
+
function findPersonValues(text3) {
|
|
480
|
+
if (typeof text3 !== "string" || !text3) return [];
|
|
481
|
+
const skipped = [...text3.matchAll(global(PERSON_TOKEN))].map((m) => [m.index, m.index + m[0].length]);
|
|
482
|
+
const found = [];
|
|
483
|
+
const add = (kind, m, normalised) => {
|
|
484
|
+
const start = m.index;
|
|
485
|
+
const end = start + m[0].length;
|
|
486
|
+
if (skipped.some(([s, e]) => s <= start && end <= e)) return;
|
|
487
|
+
if (found.some((f) => f.start < end && start < f.end)) return;
|
|
488
|
+
found.push({ kind, value: m[0], normalised, line: lineOf(text3, start), start, end });
|
|
489
|
+
};
|
|
490
|
+
for (const m of text3.matchAll(global(EMAIL_RE))) add(EMAIL, m, m[0].toLowerCase());
|
|
491
|
+
for (const source of [E164, NATIONAL]) {
|
|
492
|
+
for (const m of text3.matchAll(global(source))) {
|
|
493
|
+
if (phoneShape(m[0])) add(PHONE, m, normalise(PHONE, m[0]));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const m of text3.matchAll(global(HOME, "i"))) add(HOME_PATH, m, m[0]);
|
|
497
|
+
return found.sort((a, b) => a.start - b.start);
|
|
498
|
+
}
|
|
499
|
+
function findSecrets(text3) {
|
|
500
|
+
if (typeof text3 !== "string" || !text3) return [];
|
|
501
|
+
return [...text3.matchAll(global(KEY_PREFIX))].map((m) => ({ kind: "key-prefix", line: lineOf(text3, m.index) }));
|
|
502
|
+
}
|
|
503
|
+
var PHONE, EMAIL, HOME_PATH, NATIONAL_PREFIX, W, D, WORD_OR, PERSON_ID, PERSON_TOKEN, BEFORE, AFTER, E164, NATIONAL, GROUP_SIZES, ATEXT, EMAIL_RE, HOME_PREFIX, HOME, KEY_PREFIX, global, whole;
|
|
504
|
+
var init_person_values = __esm({
|
|
505
|
+
"src/lib/person-values.ts"() {
|
|
506
|
+
"use strict";
|
|
507
|
+
PHONE = "phone";
|
|
508
|
+
EMAIL = "email";
|
|
509
|
+
HOME_PATH = "home-path";
|
|
510
|
+
NATIONAL_PREFIX = "+44";
|
|
511
|
+
W = "[\\p{L}\\p{N}_]";
|
|
512
|
+
D = "\\p{Nd}";
|
|
513
|
+
WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
|
|
514
|
+
PERSON_ID = "(?:(?:con|ctc)_[0-9A-Za-z]{10,40}|usr_[0-9A-Za-z-]{10,40})";
|
|
515
|
+
PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
|
|
516
|
+
BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
|
|
517
|
+
AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
|
|
518
|
+
E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
|
|
519
|
+
NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
|
|
520
|
+
GROUP_SIZES = /* @__PURE__ */ new Set([2, 3, 4, 5, 6]);
|
|
521
|
+
ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
|
|
522
|
+
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])";
|
|
523
|
+
HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
|
|
524
|
+
HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
|
|
525
|
+
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,}`;
|
|
526
|
+
global = (source, flags = "") => new RegExp(source, `gu${flags}`);
|
|
527
|
+
whole = (source, flags = "") => new RegExp(`^(?:${source})$`, `u${flags}`);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// src/lib/memory-sync.ts
|
|
532
|
+
import { createHash } from "crypto";
|
|
533
|
+
function sha256(text3) {
|
|
534
|
+
return createHash("sha256").update(text3, "utf8").digest("hex");
|
|
535
|
+
}
|
|
536
|
+
function isFilePath(path) {
|
|
537
|
+
return path.length <= 512 && FILE_PATH_RE.test(path) && !path.split("/").some((s) => s === "." || s === "..");
|
|
538
|
+
}
|
|
539
|
+
function isConflictCopy(path) {
|
|
540
|
+
return CONFLICT_COPY_RE.test(path.split("/").pop() ?? path);
|
|
541
|
+
}
|
|
542
|
+
function readFrontmatter(text3) {
|
|
543
|
+
const m = BLOCK_RE.exec(text3);
|
|
544
|
+
if (!m) return void 0;
|
|
545
|
+
const front = { shareFalse: false };
|
|
546
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
547
|
+
const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
548
|
+
if (!kv) continue;
|
|
549
|
+
const raw = kv[2].replace(/[ \t]+#.*$/, "");
|
|
550
|
+
const quoted = /^(["'])(.*)\1$/.exec(raw);
|
|
551
|
+
const value = quoted ? quoted[2] : raw;
|
|
552
|
+
if (kv[1] === "type") front.type = value.trim().toLowerCase();
|
|
553
|
+
if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
|
|
554
|
+
}
|
|
555
|
+
return front;
|
|
556
|
+
}
|
|
557
|
+
function isShareable(path, text3) {
|
|
558
|
+
if (isConflictCopy(path) || !path.endsWith(".md")) return false;
|
|
559
|
+
const front = readFrontmatter(text3);
|
|
560
|
+
if (front?.shareFalse) return false;
|
|
561
|
+
if (path === INDEX_FILE) return true;
|
|
562
|
+
return !!front && SHAREABLE_TYPES.includes(front.type ?? "");
|
|
563
|
+
}
|
|
564
|
+
function addShareFalse(text3) {
|
|
565
|
+
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
566
|
+
const m = BLOCK_RE.exec(text3);
|
|
567
|
+
if (!m) return `---${eol}share: false${eol}---${eol}${text3}`;
|
|
568
|
+
const lines = m[1].split(/\r?\n/).filter((l) => !/^share[ \t]*:/.test(l));
|
|
569
|
+
lines.push("share: false");
|
|
570
|
+
const closing = m[0].slice(m[0].lastIndexOf("---"));
|
|
571
|
+
return `---${eol}${lines.join(eol)}${eol}${closing}${text3.slice(m[0].length)}`;
|
|
572
|
+
}
|
|
573
|
+
function localDate(now) {
|
|
574
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
575
|
+
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
|
576
|
+
}
|
|
577
|
+
function conflictCopyName(path, date, taken) {
|
|
578
|
+
const stem = path.replace(/\.md$/, "");
|
|
579
|
+
for (let n = 1; ; n++) {
|
|
580
|
+
const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
|
|
581
|
+
if (!taken.has(name.toLowerCase())) return name;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function decide(path, base, team, local) {
|
|
585
|
+
if (team === local) return "unchanged";
|
|
586
|
+
if (team === base) return local === void 0 ? "delete-team" : "local";
|
|
587
|
+
if (local === base) return team === void 0 ? "delete-local" : "team";
|
|
588
|
+
if (path === INDEX_FILE && team !== void 0 && local !== void 0) return "index";
|
|
589
|
+
if (team === void 0) return "local";
|
|
590
|
+
if (local === void 0) return "team";
|
|
591
|
+
return "conflict";
|
|
592
|
+
}
|
|
593
|
+
function decideAll(input) {
|
|
594
|
+
const localHashes = Object.fromEntries(Object.entries(input.local).map(([p, t]) => [p, sha256(t)]));
|
|
595
|
+
const paths = /* @__PURE__ */ new Set([...Object.keys(input.base), ...Object.keys(input.team), ...Object.keys(localHashes)]);
|
|
596
|
+
const rows = {};
|
|
597
|
+
const needed = /* @__PURE__ */ new Set();
|
|
598
|
+
for (const path of [...paths].sort()) {
|
|
599
|
+
if (input.excluded.has(path)) continue;
|
|
600
|
+
const row = decide(path, input.base[path], input.team[path], localHashes[path]);
|
|
601
|
+
rows[path] = row;
|
|
602
|
+
if (row === "team" || row === "conflict" || row === "index") needed.add(input.team[path]);
|
|
603
|
+
if (row === "index" && input.base[path]) needed.add(input.base[path]);
|
|
604
|
+
}
|
|
605
|
+
return { rows, needed: [...needed] };
|
|
606
|
+
}
|
|
607
|
+
function planMerge(input, rows, texts) {
|
|
608
|
+
const plan = {
|
|
609
|
+
manifest: {},
|
|
610
|
+
upload: {},
|
|
611
|
+
writes: {},
|
|
612
|
+
deletes: [],
|
|
613
|
+
pulled: [],
|
|
614
|
+
pushed: [],
|
|
615
|
+
deletedHere: [],
|
|
616
|
+
deletedFromTeam: [],
|
|
617
|
+
conflicts: [],
|
|
618
|
+
indexTeamLines: []
|
|
619
|
+
};
|
|
620
|
+
const taken = new Set(input.taken);
|
|
621
|
+
const teamText = (path) => {
|
|
622
|
+
const text3 = texts[input.team[path]];
|
|
623
|
+
if (text3 === void 0) throw new Error(`the team's ${path} could not be read`);
|
|
624
|
+
return text3;
|
|
625
|
+
};
|
|
626
|
+
for (const path of input.excluded) if (input.team[path]) plan.manifest[path] = input.team[path];
|
|
627
|
+
for (const [path, row] of Object.entries(rows)) {
|
|
628
|
+
const team = input.team[path];
|
|
629
|
+
const local = input.local[path];
|
|
630
|
+
switch (row) {
|
|
631
|
+
case "unchanged":
|
|
632
|
+
if (team) plan.manifest[path] = team;
|
|
633
|
+
break;
|
|
634
|
+
case "local":
|
|
635
|
+
plan.manifest[path] = sha256(local);
|
|
636
|
+
plan.upload[path] = local;
|
|
637
|
+
plan.pushed.push(path);
|
|
638
|
+
break;
|
|
639
|
+
case "delete-team":
|
|
640
|
+
plan.deletedFromTeam.push(path);
|
|
641
|
+
break;
|
|
642
|
+
case "team":
|
|
643
|
+
plan.manifest[path] = team;
|
|
644
|
+
plan.writes[path] = teamText(path);
|
|
645
|
+
plan.pulled.push(path);
|
|
646
|
+
break;
|
|
647
|
+
case "delete-local":
|
|
648
|
+
plan.deletes.push(path);
|
|
649
|
+
plan.deletedHere.push(path);
|
|
650
|
+
break;
|
|
651
|
+
case "conflict": {
|
|
652
|
+
const copy = conflictCopyName(path, input.date, taken);
|
|
653
|
+
taken.add(copy.toLowerCase());
|
|
654
|
+
plan.manifest[path] = team;
|
|
655
|
+
plan.writes[path] = teamText(path);
|
|
656
|
+
plan.writes[copy] = local;
|
|
657
|
+
plan.conflicts.push({ path, copy });
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case "index": {
|
|
661
|
+
const base = input.base[path] ? texts[input.base[path]] : void 0;
|
|
662
|
+
const merged = mergeIndex(base, teamText(path), local);
|
|
663
|
+
const hash = sha256(merged.text);
|
|
664
|
+
plan.manifest[path] = hash;
|
|
665
|
+
if (merged.text !== local) {
|
|
666
|
+
plan.writes[path] = merged.text;
|
|
667
|
+
plan.pulled.push(path);
|
|
668
|
+
}
|
|
669
|
+
if (hash !== team) {
|
|
670
|
+
plan.upload[path] = merged.text;
|
|
671
|
+
plan.pushed.push(path);
|
|
672
|
+
}
|
|
673
|
+
plan.indexTeamLines.push(...merged.teamLines);
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return plan;
|
|
679
|
+
}
|
|
680
|
+
function entries(lines) {
|
|
681
|
+
const seen = /* @__PURE__ */ new Map();
|
|
682
|
+
return lines.map((line) => {
|
|
683
|
+
const link = LINK_RE.exec(line);
|
|
684
|
+
const id = link ? `link:${link[1]}` : `text:${line}`;
|
|
685
|
+
const n = seen.get(id) ?? 0;
|
|
686
|
+
seen.set(id, n + 1);
|
|
687
|
+
return { key: n ? `${id}#${n}` : id, line };
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function mergeIndex(base, team, local) {
|
|
691
|
+
const eol = local.includes("\r\n") ? "\r\n" : "\n";
|
|
692
|
+
const b = new Map(entries(base === void 0 ? [] : splitLines(base)).map((e) => [e.key, e.line]));
|
|
693
|
+
const teamEntries = entries(splitLines(team));
|
|
694
|
+
const t = new Map(teamEntries.map((e) => [e.key, e.line]));
|
|
695
|
+
const localEntries = entries(splitLines(local));
|
|
696
|
+
const result = [];
|
|
697
|
+
const teamLines = [];
|
|
698
|
+
for (const e of localEntries) {
|
|
699
|
+
const was = b.get(e.key);
|
|
700
|
+
const theirs = t.get(e.key);
|
|
701
|
+
if (theirs === e.line || theirs === was) result.push(e);
|
|
702
|
+
else if (e.line === was) {
|
|
703
|
+
if (theirs !== void 0) result.push({ key: e.key, line: theirs });
|
|
704
|
+
} else {
|
|
705
|
+
result.push(e);
|
|
706
|
+
if (theirs !== void 0) teamLines.push(theirs);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const baseEntries = entries(base === void 0 ? [] : splitLines(base));
|
|
710
|
+
const localKeys = new Set(localEntries.map((e) => e.key));
|
|
711
|
+
const replacedHere = /* @__PURE__ */ new Map();
|
|
712
|
+
baseEntries.forEach((e, i) => {
|
|
713
|
+
if (e.key.startsWith("text:") && !t.has(e.key) && !localKeys.has(e.key)) {
|
|
714
|
+
replacedHere.set(e.key, i ? baseEntries[i - 1].key : "");
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
const inResult = (key) => result.findIndex((e) => e.key === key);
|
|
718
|
+
teamEntries.forEach((e, i) => {
|
|
719
|
+
if (inResult(e.key) !== -1) return;
|
|
720
|
+
const was = b.get(e.key);
|
|
721
|
+
const before = i ? teamEntries[i - 1].key : "";
|
|
722
|
+
const both = e.key.startsWith("text:") && [...replacedHere].find(([, prev]) => prev === before);
|
|
723
|
+
if (was === void 0 && both) {
|
|
724
|
+
replacedHere.delete(both[0]);
|
|
725
|
+
teamLines.push(e.line);
|
|
726
|
+
} else if (was === void 0) {
|
|
727
|
+
let at = -1;
|
|
728
|
+
for (let j = i - 1; j >= 0 && at === -1; j--) at = inResult(teamEntries[j].key);
|
|
729
|
+
result.splice(at + 1, 0, e);
|
|
730
|
+
} else if (e.line !== was) {
|
|
731
|
+
teamLines.push(e.line);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
return { text: result.map((e) => e.line).join(eol), teamLines };
|
|
735
|
+
}
|
|
736
|
+
function collectValues(files) {
|
|
737
|
+
const groups = /* @__PURE__ */ new Map();
|
|
738
|
+
const homePaths = [];
|
|
739
|
+
const findings = {};
|
|
740
|
+
for (const path of Object.keys(files).sort()) {
|
|
741
|
+
const found = findPersonValues(files[path]);
|
|
742
|
+
findings[path] = found;
|
|
743
|
+
for (const f of found) {
|
|
744
|
+
if (f.kind === HOME_PATH) {
|
|
745
|
+
if (!homePaths.includes(path)) homePaths.push(path);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
const key = groupKey(f.kind, f.normalised);
|
|
749
|
+
const group = groups.get(key);
|
|
750
|
+
if (group) {
|
|
751
|
+
if (!group.files.includes(path)) group.files.push(path);
|
|
752
|
+
} else {
|
|
753
|
+
groups.set(key, {
|
|
754
|
+
kind: f.kind,
|
|
755
|
+
value: f.value,
|
|
756
|
+
normalised: f.normalised,
|
|
757
|
+
files: [path],
|
|
758
|
+
first: { path, line: f.line, text: splitLines(files[path])[f.line - 1].trim() }
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return { groups: [...groups.values()], homePaths, findings };
|
|
764
|
+
}
|
|
765
|
+
function personToken(personId, label) {
|
|
766
|
+
return label ? `\`${personId}\` (${label})` : `\`${personId}\``;
|
|
767
|
+
}
|
|
768
|
+
function rewriteValues(text3, findings, replacement) {
|
|
769
|
+
let out = "";
|
|
770
|
+
let last = 0;
|
|
771
|
+
for (const f of [...findings].sort((a, b) => a.start - b.start)) {
|
|
772
|
+
const to = replacement(f);
|
|
773
|
+
if (to === void 0) continue;
|
|
774
|
+
out += text3.slice(last, f.start) + to;
|
|
775
|
+
last = f.end;
|
|
776
|
+
}
|
|
777
|
+
return out + text3.slice(last);
|
|
778
|
+
}
|
|
779
|
+
function lineDiff(before, after) {
|
|
780
|
+
const a = splitLines(before);
|
|
781
|
+
const b = splitLines(after);
|
|
782
|
+
const out = [];
|
|
783
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
784
|
+
if (a[i] === b[i]) continue;
|
|
785
|
+
if (a[i] !== void 0) out.push(`- ${a[i]}`);
|
|
786
|
+
if (b[i] !== void 0) out.push(`+ ${b[i]}`);
|
|
787
|
+
}
|
|
788
|
+
return out;
|
|
789
|
+
}
|
|
790
|
+
var INDEX_FILE, SHAREABLE_TYPES, FILE_PATH_RE, CONFLICT_COPY_RE, BLOCK_RE, YAML_FALSE, LINK_RE, splitLines, groupKey;
|
|
791
|
+
var init_memory_sync = __esm({
|
|
792
|
+
"src/lib/memory-sync.ts"() {
|
|
793
|
+
"use strict";
|
|
794
|
+
init_person_values();
|
|
795
|
+
INDEX_FILE = "MEMORY.md";
|
|
796
|
+
SHAREABLE_TYPES = ["feedback", "project", "reference"];
|
|
797
|
+
FILE_PATH_RE = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
|
|
798
|
+
CONFLICT_COPY_RE = /\.local-\d{4}-\d{2}-\d{2}(?:-\d+)?\.md$/;
|
|
799
|
+
BLOCK_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
800
|
+
YAML_FALSE = /* @__PURE__ */ new Set(["false", "False", "FALSE", "no", "No", "NO", "off", "Off", "OFF"]);
|
|
801
|
+
LINK_RE = /\]\(\s*<?([^)\s>]+)>?(?:\s+["'(][^)]*)?\)/;
|
|
802
|
+
splitLines = (text3) => text3.split(/\r?\n/);
|
|
803
|
+
groupKey = (kind, normalised) => `${kind}:${normalised}`;
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// src/sync-memory.ts
|
|
808
|
+
var sync_memory_exports = {};
|
|
809
|
+
__export(sync_memory_exports, {
|
|
810
|
+
SYNC_MEMORY_COMMAND: () => SYNC_MEMORY_COMMAND,
|
|
811
|
+
SYNC_MEMORY_USAGE: () => SYNC_MEMORY_USAGE,
|
|
812
|
+
parseSyncMemoryArgs: () => parseSyncMemoryArgs,
|
|
813
|
+
runSyncMemory: () => runSyncMemory,
|
|
814
|
+
syncMemory: () => syncMemory
|
|
815
|
+
});
|
|
816
|
+
import { existsSync } from "fs";
|
|
817
|
+
import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
818
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
819
|
+
function refusal(e) {
|
|
820
|
+
if (!(e instanceof Error)) return {};
|
|
821
|
+
const { status, code } = e;
|
|
822
|
+
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
823
|
+
}
|
|
824
|
+
function parseSyncMemoryArgs(argv) {
|
|
825
|
+
const args2 = { dryRun: false };
|
|
826
|
+
for (let i = 0; i < argv.length; i++) {
|
|
827
|
+
const a = argv[i];
|
|
828
|
+
if (a === "--dry-run") args2.dryRun = true;
|
|
829
|
+
else if (a === "--project" || a === "--dir" || a === "--import") {
|
|
830
|
+
const value = argv[++i];
|
|
831
|
+
if (!value || value.startsWith("--")) throw new SyncFailed(`${a} needs a value
|
|
832
|
+
${SYNC_MEMORY_USAGE}`);
|
|
833
|
+
if (a === "--project") {
|
|
834
|
+
if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a project name (lower case letters, digits and -)`);
|
|
835
|
+
args2.project = value;
|
|
836
|
+
} else if (a === "--dir") args2.dir = value;
|
|
837
|
+
else args2.importDir = value;
|
|
838
|
+
} else throw new SyncFailed(`Unexpected argument ${a}
|
|
839
|
+
${SYNC_MEMORY_USAGE}`);
|
|
840
|
+
}
|
|
841
|
+
return args2;
|
|
842
|
+
}
|
|
843
|
+
async function readFolder(root) {
|
|
844
|
+
const files = {};
|
|
845
|
+
const names = /* @__PURE__ */ new Set();
|
|
846
|
+
async function walk2(dir, prefix) {
|
|
847
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
848
|
+
if (e.name.startsWith(".")) continue;
|
|
849
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
850
|
+
if (e.isDirectory()) await walk2(join2(dir, e.name), rel);
|
|
851
|
+
else if (e.isFile()) {
|
|
852
|
+
names.add(rel.toLowerCase());
|
|
853
|
+
if (e.name.endsWith(".md")) files[rel] = await readFile2(join2(dir, e.name), "utf8");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (existsSync(root)) await walk2(root, "");
|
|
858
|
+
return { files, names };
|
|
859
|
+
}
|
|
860
|
+
async function pullTeam(client, ws, project) {
|
|
861
|
+
try {
|
|
862
|
+
const latest = await client.getMemory(ws, project);
|
|
863
|
+
return {
|
|
864
|
+
version: latest.version,
|
|
865
|
+
manifest: Object.fromEntries(Object.entries(latest.files).map(([p, f]) => [p, f.sha256])),
|
|
866
|
+
exists: true
|
|
867
|
+
};
|
|
868
|
+
} catch (e) {
|
|
869
|
+
if (refusal(e).status === 404) return { version: 0, manifest: {}, exists: false };
|
|
870
|
+
throw e;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async function readTexts(client, ws, project, hashes) {
|
|
874
|
+
const texts = {};
|
|
875
|
+
const one = async (batch) => {
|
|
876
|
+
let left = batch;
|
|
877
|
+
while (left.length) {
|
|
878
|
+
const got = await client.readBlobs(ws, project, left);
|
|
879
|
+
for (const b of got.blobs) texts[b.sha256] = b.content;
|
|
880
|
+
if (!got.blobs.length) break;
|
|
881
|
+
left = got.remaining;
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
for (let i = 0; i < hashes.length; i += 1e3) {
|
|
885
|
+
const batch = hashes.slice(i, i + 1e3);
|
|
886
|
+
try {
|
|
887
|
+
await one(batch);
|
|
888
|
+
} catch (e) {
|
|
889
|
+
if (!(refusal(e).status === 404)) throw e;
|
|
890
|
+
for (const h of batch) {
|
|
891
|
+
try {
|
|
892
|
+
await one([h]);
|
|
893
|
+
} catch (err) {
|
|
894
|
+
if (!(refusal(err).status === 404)) throw err;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return texts;
|
|
900
|
+
}
|
|
901
|
+
function labelProblem(label) {
|
|
902
|
+
if (!label) return "a label is needed";
|
|
903
|
+
if (label.length > 60) return "a label is at most 60 characters";
|
|
904
|
+
if (findPersonValues(label).length) return "a label is a role, never a phone number, an email address or a path";
|
|
905
|
+
return void 0;
|
|
906
|
+
}
|
|
907
|
+
async function askLabel(ask, out, question, optional) {
|
|
908
|
+
for (; ; ) {
|
|
909
|
+
const label = (await ask(question)).trim();
|
|
910
|
+
if (optional && !label) return void 0;
|
|
911
|
+
const problem = labelProblem(label);
|
|
912
|
+
if (!problem) return label;
|
|
913
|
+
out(` ${problem}.`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async function people(opts, ws, files, report, hint) {
|
|
917
|
+
const { client, out, ask, dryRun } = opts;
|
|
918
|
+
const terminal = !!ask && !dryRun;
|
|
919
|
+
const held = /* @__PURE__ */ new Set();
|
|
920
|
+
const leftOut = /* @__PURE__ */ new Set();
|
|
921
|
+
const current = { ...files };
|
|
922
|
+
for (const [path, text3] of Object.entries(current)) {
|
|
923
|
+
const secret = findSecrets(text3)[0];
|
|
924
|
+
if (secret) {
|
|
925
|
+
held.add(path);
|
|
926
|
+
report.skip(path, `a secret (${secret.kind}) on line ${secret.line}: remove it, then sync again`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const { groups, findings } = collectValues(current);
|
|
930
|
+
const answers = /* @__PURE__ */ new Map();
|
|
931
|
+
for (let i = 0; i < groups.length; i += 500) {
|
|
932
|
+
const batch = groups.slice(i, i + 500);
|
|
933
|
+
const got = await client.lookupValues(ws, batch.map((g) => ({ kind: g.kind, value: g.value })));
|
|
934
|
+
batch.forEach((g, j) => {
|
|
935
|
+
const a = got.answers[j];
|
|
936
|
+
if (a && a.answer !== "unanswered") answers.set(groupKey(g.kind, g.normalised), a);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
const vaultDown = /* @__PURE__ */ new Set();
|
|
940
|
+
if (terminal) {
|
|
941
|
+
const filed = [];
|
|
942
|
+
for (const g of groups) {
|
|
943
|
+
const key = groupKey(g.kind, g.normalised);
|
|
944
|
+
if (answers.has(key)) continue;
|
|
945
|
+
let choices = ["m", "p", "o", "x", "s"];
|
|
946
|
+
for (; ; ) {
|
|
947
|
+
const question = [
|
|
948
|
+
`${g.value} (${g.kind}) is in ${g.files.length} file${g.files.length === 1 ? "" : "s"}. First line it appears on, in ${g.first.path}:`,
|
|
949
|
+
` ${g.first.text}`,
|
|
950
|
+
[
|
|
951
|
+
choices.includes("m") && "[m] a member's contact",
|
|
952
|
+
"[p] someone else",
|
|
953
|
+
"[o] ours, not a person",
|
|
954
|
+
"[x] leave these files out",
|
|
955
|
+
"[s] skip for now"
|
|
956
|
+
].filter(Boolean).join(" "),
|
|
957
|
+
`Answer (${choices.join("/")}): `
|
|
958
|
+
].join("\n");
|
|
959
|
+
const answer = (await ask(question)).trim().toLowerCase();
|
|
960
|
+
if (!choices.includes(answer)) continue;
|
|
961
|
+
if (answer === "s") break;
|
|
962
|
+
if (answer === "x") {
|
|
963
|
+
for (const path of g.files) {
|
|
964
|
+
if (leftOut.has(path)) continue;
|
|
965
|
+
current[path] = addShareFalse(current[path]);
|
|
966
|
+
await writeFile2(toPath(opts.dir, path), current[path], "utf8");
|
|
967
|
+
leftOut.add(path);
|
|
968
|
+
}
|
|
969
|
+
report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
if (answer === "p") {
|
|
973
|
+
const label2 = await askLabel(ask, out, "Their role, as a label (e.g. reseller, sales; never a name): ", false);
|
|
974
|
+
let entry = filed.length ? await (async () => {
|
|
975
|
+
const list = filed.map((f, i) => ` ${i + 1}. ${f.label}: ${f.groups.map((x) => x.value).join(", ")}`);
|
|
976
|
+
for (; ; ) {
|
|
977
|
+
const same = (await ask(["The same person as one filed above?", ...list, "Number, or Enter for a new person: "].join("\n"))).trim();
|
|
978
|
+
if (!same) return void 0;
|
|
979
|
+
const n = Number(same);
|
|
980
|
+
if (Number.isInteger(n) && n >= 1 && n <= filed.length && filed[n - 1].groups.length < 4) return filed[n - 1];
|
|
981
|
+
}
|
|
982
|
+
})() : void 0;
|
|
983
|
+
if (!entry) {
|
|
984
|
+
entry = { groups: [], label: label2 };
|
|
985
|
+
filed.push(entry);
|
|
986
|
+
}
|
|
987
|
+
entry.groups.push(g);
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
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;
|
|
991
|
+
let result;
|
|
992
|
+
try {
|
|
993
|
+
result = await client.answerValues(ws, [
|
|
994
|
+
{ kind: g.kind, value: g.value, answer: answer === "m" ? "member" : "ours", ...label ? { label } : {} }
|
|
995
|
+
]);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
if (refusal(e).code === "SYD-K019") {
|
|
998
|
+
vaultDown.add(key);
|
|
999
|
+
out(" The vault did not answer (SYD-K019): the files holding it wait for the next sync.");
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
throw e;
|
|
1003
|
+
}
|
|
1004
|
+
if (result.stored[0]) {
|
|
1005
|
+
answers.set(key, result.stored[0]);
|
|
1006
|
+
report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
const refused = result.refused[0];
|
|
1010
|
+
if (refused?.code === "SYD-K021") {
|
|
1011
|
+
out(` ${refused.detail}. Add them in Staff first, or answer p to file them.`);
|
|
1012
|
+
choices = choices.filter((c) => c !== "m");
|
|
1013
|
+
} else if (refused) out(` ${refused.detail}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
for (let i = 0; i < filed.length; i += 50) {
|
|
1017
|
+
const batch = filed.slice(i, i + 50);
|
|
1018
|
+
let result;
|
|
1019
|
+
try {
|
|
1020
|
+
result = await client.fileMemoryPeople(
|
|
1021
|
+
ws,
|
|
1022
|
+
batch.map((f) => ({ values: f.groups.map((g) => ({ kind: g.kind, value: g.value })), label: f.label }))
|
|
1023
|
+
);
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
if (refusal(e).code === "SYD-K019") {
|
|
1026
|
+
out("Contacts did not answer (SYD-K019): the files holding those values wait for the next sync.");
|
|
1027
|
+
batch.forEach((f) => f.groups.forEach((g) => vaultDown.add(groupKey(g.kind, g.normalised))));
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
throw e;
|
|
1031
|
+
}
|
|
1032
|
+
for (const stored of result.stored) {
|
|
1033
|
+
let key;
|
|
1034
|
+
try {
|
|
1035
|
+
key = groupKey(stored.kind, normalise(stored.kind, stored.value));
|
|
1036
|
+
} catch {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
answers.set(key, stored);
|
|
1040
|
+
}
|
|
1041
|
+
for (const r of result.refused) {
|
|
1042
|
+
out(` ${batch[r.index]?.label ?? ""}: ${r.detail}`);
|
|
1043
|
+
}
|
|
1044
|
+
for (const f of batch) {
|
|
1045
|
+
const a = answers.get(groupKey(f.groups[0].kind, f.groups[0].normalised));
|
|
1046
|
+
if (a?.answer === "person") {
|
|
1047
|
+
for (const g of f.groups) report.people.push({ kind: g.kind, answer: ANSWER_WORDS.person, label: a.label, personId: a.personId });
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const rewritten = {};
|
|
1053
|
+
for (const [path, text3] of Object.entries(current)) {
|
|
1054
|
+
if (leftOut.has(path) || held.has(path)) continue;
|
|
1055
|
+
const next = rewriteValues(text3, findings[path] ?? [], (f) => {
|
|
1056
|
+
if (f.kind === HOME_PATH) return "~";
|
|
1057
|
+
const a = answers.get(groupKey(f.kind, f.normalised));
|
|
1058
|
+
return a && (a.answer === "member" || a.answer === "person") && a.personId ? personToken(a.personId, a.label) : void 0;
|
|
1059
|
+
});
|
|
1060
|
+
if (next !== text3) rewritten[path] = next;
|
|
1061
|
+
}
|
|
1062
|
+
const paths = Object.keys(rewritten).sort();
|
|
1063
|
+
if (paths.length && terminal) {
|
|
1064
|
+
out("", "The rewrite:");
|
|
1065
|
+
for (const path of paths) out(path, ...lineDiff(current[path], rewritten[path]).map((l) => ` ${l}`));
|
|
1066
|
+
const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
|
|
1067
|
+
if (yes) {
|
|
1068
|
+
for (const path of paths) {
|
|
1069
|
+
await writeFile2(toPath(opts.dir, path), rewritten[path], "utf8");
|
|
1070
|
+
current[path] = rewritten[path];
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const [path, text3] of Object.entries(current)) {
|
|
1075
|
+
if (leftOut.has(path) || held.has(path)) continue;
|
|
1076
|
+
const pending = findPersonValues(text3).filter((f) => {
|
|
1077
|
+
if (f.kind === HOME_PATH) return true;
|
|
1078
|
+
return answers.get(groupKey(f.kind, f.normalised))?.answer !== "ours";
|
|
1079
|
+
});
|
|
1080
|
+
if (!pending.length) continue;
|
|
1081
|
+
held.add(path);
|
|
1082
|
+
const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
|
|
1083
|
+
if (keys.some((k) => vaultDown.has(k))) {
|
|
1084
|
+
report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
|
|
1085
|
+
} else if (keys.some((k) => !answers.has(k))) {
|
|
1086
|
+
report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
|
|
1087
|
+
} else {
|
|
1088
|
+
report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
for (const g of groups) {
|
|
1092
|
+
const key = groupKey(g.kind, g.normalised);
|
|
1093
|
+
if (!answers.has(key) && g.files.some((p) => !leftOut.has(p))) {
|
|
1094
|
+
report.toAnswer.set(g.kind, (report.toAnswer.get(g.kind) ?? 0) + 1);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return { files: current, held, leftOut };
|
|
1098
|
+
}
|
|
1099
|
+
async function applyPlan(dir, plan) {
|
|
1100
|
+
for (const [path, text3] of Object.entries(plan.writes)) {
|
|
1101
|
+
const full = toPath(dir, path);
|
|
1102
|
+
await mkdir2(dirname2(full), { recursive: true });
|
|
1103
|
+
await writeFile2(full, text3, "utf8");
|
|
1104
|
+
}
|
|
1105
|
+
for (const path of plan.deletes) await rm2(toPath(dir, path), { force: true });
|
|
1106
|
+
}
|
|
1107
|
+
async function upload(client, ws, project, plan, team, report) {
|
|
1108
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
1109
|
+
for (const path of Object.keys(plan.upload)) {
|
|
1110
|
+
const h = plan.manifest[path];
|
|
1111
|
+
byHash.set(h, [...byHash.get(h) ?? [], path]);
|
|
1112
|
+
}
|
|
1113
|
+
const hashes = [...byHash.keys()];
|
|
1114
|
+
const missing = [];
|
|
1115
|
+
for (let i = 0; i < hashes.length; i += 1e4) {
|
|
1116
|
+
missing.push(...(await client.findMissingBlobs(ws, project, hashes.slice(i, i + 1e4))).missing);
|
|
1117
|
+
}
|
|
1118
|
+
const blobs = missing.map((h) => ({ sha256: h, content: plan.upload[byHash.get(h)[0]] }));
|
|
1119
|
+
let batch = [];
|
|
1120
|
+
let bytes = 0;
|
|
1121
|
+
const send = async () => {
|
|
1122
|
+
if (!batch.length) return;
|
|
1123
|
+
const result = await client.uploadBlobs(ws, project, batch);
|
|
1124
|
+
for (const r of result.refused) {
|
|
1125
|
+
for (const path of byHash.get(r.sha256) ?? []) {
|
|
1126
|
+
if (team[path]) plan.manifest[path] = team[path];
|
|
1127
|
+
else delete plan.manifest[path];
|
|
1128
|
+
plan.pushed = plan.pushed.filter((p) => p !== path);
|
|
1129
|
+
report.pushed.delete(path);
|
|
1130
|
+
report.skip(path, `refused by SystemDox, ${r.detail.startsWith(r.code) ? r.detail : `${r.code}: ${r.detail}`}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
batch = [];
|
|
1134
|
+
bytes = 0;
|
|
1135
|
+
};
|
|
1136
|
+
for (const blob of blobs) {
|
|
1137
|
+
const size = Buffer.byteLength(blob.content, "utf8");
|
|
1138
|
+
if (batch.length === 100 || batch.length && bytes + size > 5 * 1024 * 1024 - 64 * 1024) await send();
|
|
1139
|
+
batch.push(blob);
|
|
1140
|
+
bytes += size;
|
|
1141
|
+
}
|
|
1142
|
+
await send();
|
|
1143
|
+
}
|
|
1144
|
+
async function keyFromEnvironment(env) {
|
|
1145
|
+
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
1146
|
+
const entries2 = Object.entries(await readCredentials()).filter(([, v]) => typeof v?.apiKey === "string" && v.apiKey);
|
|
1147
|
+
if (entries2.length === 1) return entries2[0][1].apiKey;
|
|
1148
|
+
if (entries2.length > 1) {
|
|
1149
|
+
throw new SyncFailed(
|
|
1150
|
+
`~/.claude/systemdox/credentials.json holds keys for ${entries2.map(([w]) => w).join(", ")}: set SYSTEMDOX_API_KEY to the key of the workspace to sync.`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
throw new SyncFailed("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup, then run this again.");
|
|
1154
|
+
}
|
|
1155
|
+
async function syncMemory(opts) {
|
|
1156
|
+
const { client, out } = opts;
|
|
1157
|
+
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
1158
|
+
try {
|
|
1159
|
+
const own = await client.getOwnTeamSetup();
|
|
1160
|
+
const ws = own.workspace;
|
|
1161
|
+
const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
|
|
1162
|
+
if (!project) {
|
|
1163
|
+
throw new SyncFailed(
|
|
1164
|
+
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.`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
const dir = resolve(opts.dir ?? join2(workspaceDir(ws), "memory", project));
|
|
1168
|
+
const run3 = { ...opts, dir };
|
|
1169
|
+
const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
|
|
1170
|
+
const report = new Report();
|
|
1171
|
+
const date = localDate(now);
|
|
1172
|
+
const stored = (await readState(ws)).memory?.[project];
|
|
1173
|
+
let team = await pullTeam(client, ws, project);
|
|
1174
|
+
let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
|
|
1175
|
+
const folder = await readFolder(dir);
|
|
1176
|
+
const shared = {};
|
|
1177
|
+
const notShared = /* @__PURE__ */ new Set();
|
|
1178
|
+
for (const [path, text3] of Object.entries(folder.files)) {
|
|
1179
|
+
if (!isShareable(path, text3)) notShared.add(path);
|
|
1180
|
+
else if (!isFilePath(path)) {
|
|
1181
|
+
notShared.add(path);
|
|
1182
|
+
report.skip(path, "its name has characters a team memory path cannot hold (letters, digits, '.', '_', '-')");
|
|
1183
|
+
} else shared[path] = text3;
|
|
1184
|
+
}
|
|
1185
|
+
const importWrites = {};
|
|
1186
|
+
if (opts.importDir) {
|
|
1187
|
+
const other = await readFolder(resolve(opts.importDir));
|
|
1188
|
+
const taken = new Set(folder.names);
|
|
1189
|
+
for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
|
|
1190
|
+
if (!isShareable(path, text3) || !isFilePath(path)) continue;
|
|
1191
|
+
const here = folder.files[path];
|
|
1192
|
+
if (here === void 0) {
|
|
1193
|
+
shared[path] = text3;
|
|
1194
|
+
importWrites[path] = text3;
|
|
1195
|
+
taken.add(path.toLowerCase());
|
|
1196
|
+
} else if (here !== text3) {
|
|
1197
|
+
const copy = conflictCopyName(path, date, taken);
|
|
1198
|
+
taken.add(copy.toLowerCase());
|
|
1199
|
+
importWrites[copy] = text3;
|
|
1200
|
+
report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
folder.names = taken;
|
|
1204
|
+
if (!opts.dryRun) {
|
|
1205
|
+
for (const [path, text3] of Object.entries(importWrites)) {
|
|
1206
|
+
const full = toPath(dir, path);
|
|
1207
|
+
await mkdir2(dirname2(full), { recursive: true });
|
|
1208
|
+
await writeFile2(full, text3, "utf8");
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const found = await people(run3, ws, shared, report, hint);
|
|
1213
|
+
for (const path of found.leftOut) {
|
|
1214
|
+
delete found.files[path];
|
|
1215
|
+
notShared.add(path);
|
|
1216
|
+
}
|
|
1217
|
+
const excluded = /* @__PURE__ */ new Set([...found.held, ...notShared]);
|
|
1218
|
+
for (const path of found.held) delete found.files[path];
|
|
1219
|
+
for (let attempt = 1; ; attempt++) {
|
|
1220
|
+
const input = { base: base.manifest, team: team.manifest, local: found.files, excluded, taken: folder.names, date };
|
|
1221
|
+
const { rows, needed } = decideAll(input);
|
|
1222
|
+
const texts = needed.length ? await readTexts(client, ws, project, needed) : {};
|
|
1223
|
+
const plan = planMerge(input, rows, texts);
|
|
1224
|
+
report.add(plan);
|
|
1225
|
+
if (opts.dryRun) {
|
|
1226
|
+
const skipped = report.skipped.size;
|
|
1227
|
+
out(
|
|
1228
|
+
...report.lines(
|
|
1229
|
+
`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.`,
|
|
1230
|
+
true
|
|
1231
|
+
)
|
|
1232
|
+
);
|
|
1233
|
+
return 0;
|
|
1234
|
+
}
|
|
1235
|
+
await applyPlan(dir, plan);
|
|
1236
|
+
let version = team.version;
|
|
1237
|
+
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1238
|
+
await upload(client, ws, project, plan, team.manifest, report);
|
|
1239
|
+
}
|
|
1240
|
+
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1241
|
+
try {
|
|
1242
|
+
version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
if (!(refusal(e).status === 409 && refusal(e).code === "SYD-K010")) throw e;
|
|
1245
|
+
if (attempt === 2) {
|
|
1246
|
+
out(
|
|
1247
|
+
...report.lines(
|
|
1248
|
+
`Not pushed: ${ws}/${project} moved on twice while this sync merged. Your folder holds the merge; run sync-memory again.`,
|
|
1249
|
+
false
|
|
1250
|
+
)
|
|
1251
|
+
);
|
|
1252
|
+
return 2;
|
|
1253
|
+
}
|
|
1254
|
+
base = team;
|
|
1255
|
+
for (const p of plan.pushed) report.pushed.delete(p);
|
|
1256
|
+
const again = await readFolder(dir);
|
|
1257
|
+
folder.names = again.names;
|
|
1258
|
+
found.files = {};
|
|
1259
|
+
for (const [path, text3] of Object.entries(again.files)) {
|
|
1260
|
+
if (excluded.has(path) || !isShareable(path, text3) || !isFilePath(path)) continue;
|
|
1261
|
+
found.files[path] = text3;
|
|
1262
|
+
}
|
|
1263
|
+
team = await pullTeam(client, ws, project);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
await updateState(ws, (state) => {
|
|
1268
|
+
state.memory = { ...state.memory, [project]: { version, manifest: plan.manifest } };
|
|
1269
|
+
});
|
|
1270
|
+
out(
|
|
1271
|
+
...report.lines(
|
|
1272
|
+
`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.`,
|
|
1273
|
+
false
|
|
1274
|
+
)
|
|
1275
|
+
);
|
|
1276
|
+
return 0;
|
|
1277
|
+
}
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
out(e.message);
|
|
1280
|
+
return 1;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async function runSyncMemory(argv, io, env = process.env) {
|
|
1284
|
+
let args2;
|
|
1285
|
+
let client = io.client;
|
|
1286
|
+
try {
|
|
1287
|
+
args2 = parseSyncMemoryArgs(argv);
|
|
1288
|
+
if (!client) {
|
|
1289
|
+
const apiUrl = apiUrlFrom(env);
|
|
1290
|
+
const apiKey = await keyFromEnvironment(env);
|
|
1291
|
+
client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1292
|
+
}
|
|
1293
|
+
} catch (e) {
|
|
1294
|
+
io.err(e.message);
|
|
1295
|
+
return 1;
|
|
1296
|
+
}
|
|
1297
|
+
return syncMemory({ ...args2, client, out: (...lines) => lines.forEach((l) => io.out(l)), ask: io.ask, now: io.now });
|
|
1298
|
+
}
|
|
1299
|
+
var SYNC_MEMORY_USAGE, SYNC_MEMORY_COMMAND, SyncFailed, toPath, Report, ANSWER_WORDS, sameManifest;
|
|
1300
|
+
var init_sync_memory = __esm({
|
|
1301
|
+
"src/sync-memory.ts"() {
|
|
1302
|
+
"use strict";
|
|
1303
|
+
init_api_client();
|
|
1304
|
+
init_memory_sync();
|
|
1305
|
+
init_person_values();
|
|
1306
|
+
init_team_setup_files();
|
|
1307
|
+
SYNC_MEMORY_USAGE = "Usage: systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>] [--import <folder>] [--dry-run]";
|
|
1308
|
+
SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
|
|
1309
|
+
SyncFailed = class extends Error {
|
|
1310
|
+
};
|
|
1311
|
+
toPath = (root, rel) => join2(root, ...rel.split("/"));
|
|
1312
|
+
Report = class {
|
|
1313
|
+
pulled = /* @__PURE__ */ new Set();
|
|
1314
|
+
pushed = /* @__PURE__ */ new Set();
|
|
1315
|
+
deletedHere = /* @__PURE__ */ new Set();
|
|
1316
|
+
deletedFromTeam = /* @__PURE__ */ new Set();
|
|
1317
|
+
conflicts = [];
|
|
1318
|
+
skipped = /* @__PURE__ */ new Map();
|
|
1319
|
+
people = [];
|
|
1320
|
+
toAnswer = /* @__PURE__ */ new Map();
|
|
1321
|
+
add(plan) {
|
|
1322
|
+
plan.pulled.forEach((p) => this.pulled.add(p));
|
|
1323
|
+
plan.pushed.forEach((p) => this.pushed.add(p));
|
|
1324
|
+
plan.deletedHere.forEach((p) => this.deletedHere.add(p));
|
|
1325
|
+
plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
|
|
1326
|
+
for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
|
|
1327
|
+
for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
|
|
1328
|
+
}
|
|
1329
|
+
skip(path, reason) {
|
|
1330
|
+
if (!this.skipped.has(path)) this.skipped.set(path, reason);
|
|
1331
|
+
}
|
|
1332
|
+
lines(summary, dryRun) {
|
|
1333
|
+
const out = [summary];
|
|
1334
|
+
const section = (title, items) => {
|
|
1335
|
+
if (items.length) out.push("", `${title}:`, ...items.map((i) => `- ${i}`));
|
|
1336
|
+
};
|
|
1337
|
+
const sorted = (s) => [...s].sort();
|
|
1338
|
+
section(dryRun ? "Would pull" : "Pulled", sorted(this.pulled));
|
|
1339
|
+
section(dryRun ? "Would push" : "Pushed", sorted(this.pushed));
|
|
1340
|
+
section(dryRun ? "Would delete" : "Deleted", [
|
|
1341
|
+
...sorted(this.deletedHere).map((p) => `${p} (here: the team deleted it)`),
|
|
1342
|
+
...sorted(this.deletedFromTeam).map((p) => `${p} (from the team: you deleted it)`)
|
|
1343
|
+
]);
|
|
1344
|
+
section("Conflicts", this.conflicts);
|
|
1345
|
+
section(
|
|
1346
|
+
"Skipped",
|
|
1347
|
+
[...this.skipped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([p, why]) => `${p}: ${why}`)
|
|
1348
|
+
);
|
|
1349
|
+
section(
|
|
1350
|
+
"People",
|
|
1351
|
+
this.people.map(
|
|
1352
|
+
(a) => [a.kind, a.answer, a.label, a.personId && `\`${a.personId}\``, a.files !== void 0 && `${a.files} files`].filter(Boolean).join(", ")
|
|
1353
|
+
)
|
|
1354
|
+
);
|
|
1355
|
+
if (this.toAnswer.size) {
|
|
1356
|
+
out.push(
|
|
1357
|
+
"",
|
|
1358
|
+
`Values still to answer: ${[...this.toAnswer.entries()].map(([kind, n]) => `${n} ${kind}`).join(", ")}.`
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
return out;
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
ANSWER_WORDS = {
|
|
1365
|
+
member: "a member's contact",
|
|
1366
|
+
person: "someone else",
|
|
1367
|
+
ours: "ours, not a person"
|
|
1368
|
+
};
|
|
1369
|
+
sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
// src/setup-team.ts
|
|
1374
|
+
var setup_team_exports = {};
|
|
1375
|
+
__export(setup_team_exports, {
|
|
1376
|
+
HEADERS_HELPER: () => HEADERS_HELPER,
|
|
1377
|
+
SETUP_USAGE: () => SETUP_USAGE,
|
|
1378
|
+
claudeCodeVersion: () => claudeCodeVersion,
|
|
1379
|
+
importBlock: () => importBlock,
|
|
1380
|
+
parseSetupArgs: () => parseSetupArgs,
|
|
1381
|
+
runTeamSetup: () => runTeamSetup,
|
|
1382
|
+
runTeamSetupCommand: () => runTeamSetupCommand,
|
|
1383
|
+
setJsonMember: () => setJsonMember,
|
|
1384
|
+
withImportBlock: () => withImportBlock
|
|
1385
|
+
});
|
|
1386
|
+
import { execFile as execFile2 } from "child_process";
|
|
1387
|
+
import { createHash as createHash2 } from "crypto";
|
|
1388
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1389
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1390
|
+
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
1391
|
+
import { promisify as promisify2 } from "util";
|
|
1392
|
+
function parseSetupArgs(argv) {
|
|
1393
|
+
const args2 = { yes: false, overwrite: false };
|
|
1394
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1395
|
+
const a = argv[i];
|
|
1396
|
+
if (a === "--yes") args2.yes = true;
|
|
1397
|
+
else if (a === "--overwrite") args2.overwrite = true;
|
|
1398
|
+
else if (a === "--project" || a === "--dir") {
|
|
1399
|
+
const value = argv[++i];
|
|
1400
|
+
if (!value || value.startsWith("--")) throw new Error(`${a} needs a value
|
|
1401
|
+
${SETUP_USAGE}`);
|
|
1402
|
+
if (a === "--project") {
|
|
1403
|
+
if (!isWorkspace(value)) throw new Error(`${value} is not a project name (lower case letters, digits and -)`);
|
|
1404
|
+
args2.project = value;
|
|
1405
|
+
} else args2.dir = value;
|
|
1406
|
+
} else throw new Error(`Unexpected argument ${a}
|
|
1407
|
+
${SETUP_USAGE}`);
|
|
1408
|
+
}
|
|
1409
|
+
return args2;
|
|
1410
|
+
}
|
|
1411
|
+
async function claudeCodeVersion() {
|
|
1412
|
+
try {
|
|
1413
|
+
const { stdout } = await run2("claude", ["--version"], {
|
|
1414
|
+
shell: process.platform === "win32",
|
|
1415
|
+
timeout: 3e4,
|
|
1416
|
+
windowsHide: true
|
|
1417
|
+
});
|
|
1418
|
+
return stdout;
|
|
1419
|
+
} catch {
|
|
1420
|
+
return void 0;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function parseVersion(text3) {
|
|
1424
|
+
const m = text3?.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
1425
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : void 0;
|
|
1426
|
+
}
|
|
1427
|
+
function olderThan(have, need) {
|
|
1428
|
+
for (let i = 0; i < 3; i++) if (have[i] !== need[i]) return have[i] < need[i];
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
function skipSpace(t, i) {
|
|
1432
|
+
while (i < t.length && /\s/.test(t[i])) i++;
|
|
1433
|
+
return i;
|
|
1434
|
+
}
|
|
1435
|
+
function skipString(t, i) {
|
|
1436
|
+
for (i++; i < t.length; i++) {
|
|
1437
|
+
if (t[i] === "\\") i++;
|
|
1438
|
+
else if (t[i] === '"') return i + 1;
|
|
1439
|
+
}
|
|
1440
|
+
return i;
|
|
1441
|
+
}
|
|
1442
|
+
function skipValue(t, i) {
|
|
1443
|
+
if (t[i] === '"') return skipString(t, i);
|
|
1444
|
+
if (t[i] === "{" || t[i] === "[") {
|
|
1445
|
+
let depth = 0;
|
|
1446
|
+
while (i < t.length) {
|
|
1447
|
+
const c = t[i];
|
|
1448
|
+
if (c === '"') {
|
|
1449
|
+
i = skipString(t, i);
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (c === "{" || c === "[") depth++;
|
|
1453
|
+
else if (c === "}" || c === "]") {
|
|
1454
|
+
depth--;
|
|
1455
|
+
if (depth === 0) return i + 1;
|
|
1456
|
+
}
|
|
1457
|
+
i++;
|
|
1458
|
+
}
|
|
1459
|
+
return i;
|
|
1460
|
+
}
|
|
1461
|
+
while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
|
|
1462
|
+
return i;
|
|
1463
|
+
}
|
|
1464
|
+
function objectMembers(t, open) {
|
|
1465
|
+
const members = [];
|
|
1466
|
+
let i = skipSpace(t, open + 1);
|
|
1467
|
+
if (t[i] === "}") return { members, close: i };
|
|
1468
|
+
for (; ; ) {
|
|
1469
|
+
i = skipSpace(t, i);
|
|
1470
|
+
const keyStart = i;
|
|
1471
|
+
const keyEnd = skipString(t, i);
|
|
1472
|
+
const key = JSON.parse(t.slice(keyStart, keyEnd));
|
|
1473
|
+
i = skipSpace(t, skipSpace(t, keyEnd) + 1);
|
|
1474
|
+
const valueEnd = skipValue(t, i);
|
|
1475
|
+
members.push({ key, keyStart, valueStart: i, valueEnd });
|
|
1476
|
+
i = skipSpace(t, valueEnd);
|
|
1477
|
+
if (t[i] === ",") i++;
|
|
1478
|
+
else return { members, close: i };
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function lineIndent(t, pos) {
|
|
1482
|
+
const start = t.lastIndexOf("\n", pos - 1) + 1;
|
|
1483
|
+
return /^[ \t]*/.exec(t.slice(start))[0];
|
|
1484
|
+
}
|
|
1485
|
+
function setJsonMember(text3, keys, value) {
|
|
1486
|
+
const root = JSON.parse(text3);
|
|
1487
|
+
if (!root || typeof root !== "object" || Array.isArray(root)) throw new Error("it does not hold a JSON object");
|
|
1488
|
+
const compact = !text3.trimEnd().includes("\n");
|
|
1489
|
+
const unit = /^([ \t]+)"/m.exec(text3)?.[1] ?? " ";
|
|
1490
|
+
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
1491
|
+
const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
|
|
1492
|
+
const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
|
|
1493
|
+
let open = skipSpace(text3, 0);
|
|
1494
|
+
for (let k = 0; k < keys.length; k++) {
|
|
1495
|
+
const { members, close } = objectMembers(text3, open);
|
|
1496
|
+
const found = members.filter((m) => m.key === keys[k]).at(-1);
|
|
1497
|
+
if (found) {
|
|
1498
|
+
if (k < keys.length - 1 && text3[found.valueStart] === "{") {
|
|
1499
|
+
open = found.valueStart;
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
return text3.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text3, found.keyStart)) + text3.slice(found.valueEnd);
|
|
1503
|
+
}
|
|
1504
|
+
const name = JSON.stringify(keys[k]);
|
|
1505
|
+
if (compact) {
|
|
1506
|
+
const member = `${name}:${render(nest(k + 1), "")}`;
|
|
1507
|
+
if (members.length) return text3.slice(0, members.at(-1).valueEnd) + "," + member + text3.slice(members.at(-1).valueEnd);
|
|
1508
|
+
return text3.slice(0, open + 1) + member + text3.slice(close);
|
|
1509
|
+
}
|
|
1510
|
+
if (members.length) {
|
|
1511
|
+
const last = members.at(-1);
|
|
1512
|
+
const indent2 = lineIndent(text3, last.keyStart);
|
|
1513
|
+
return text3.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text3.slice(last.valueEnd);
|
|
1514
|
+
}
|
|
1515
|
+
const outer = lineIndent(text3, open);
|
|
1516
|
+
const indent = outer + unit;
|
|
1517
|
+
return text3.slice(0, open + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text3.slice(close);
|
|
1518
|
+
}
|
|
1519
|
+
return text3;
|
|
1520
|
+
}
|
|
1521
|
+
function jsonAt(value, keys) {
|
|
1522
|
+
let v = value;
|
|
1523
|
+
for (const k of keys) v = v && typeof v === "object" ? v[k] : void 0;
|
|
1524
|
+
return v;
|
|
1525
|
+
}
|
|
1526
|
+
async function setInJsonFile(path, keys, value) {
|
|
1527
|
+
let text3;
|
|
1528
|
+
try {
|
|
1529
|
+
text3 = await readFile3(path, "utf8");
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
if (e.code !== "ENOENT") throw e;
|
|
1532
|
+
}
|
|
1533
|
+
let next;
|
|
1534
|
+
if (text3 === void 0 || !text3.trim()) {
|
|
1535
|
+
next = JSON.stringify(keys.reduceRight((acc, k) => ({ [k]: acc }), value), null, 2) + "\n";
|
|
1536
|
+
} else {
|
|
1537
|
+
let current;
|
|
1538
|
+
try {
|
|
1539
|
+
current = JSON.parse(text3);
|
|
1540
|
+
} catch {
|
|
1541
|
+
throw new SetupFailed(`${path} is not valid JSON: fix it, then run setup again.`);
|
|
1542
|
+
}
|
|
1543
|
+
if (JSON.stringify(jsonAt(current, keys)) === JSON.stringify(value)) return;
|
|
1544
|
+
try {
|
|
1545
|
+
next = setJsonMember(text3, keys, value);
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
throw new SetupFailed(`${path}: ${e.message}; fix it, then run setup again.`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
1551
|
+
await writeFile3(path, next, "utf8");
|
|
1552
|
+
}
|
|
1553
|
+
function importBlock(workspace, eol = "\n") {
|
|
1554
|
+
return [
|
|
1555
|
+
`<!-- systemdox:${workspace} -->`,
|
|
1556
|
+
`@~/.claude/systemdox/${workspace}/CLAUDE.md`,
|
|
1557
|
+
`<!-- /systemdox:${workspace} -->`
|
|
1558
|
+
].join(eol);
|
|
1559
|
+
}
|
|
1560
|
+
function withImportBlock(text3, workspace) {
|
|
1561
|
+
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
1562
|
+
const block = importBlock(workspace, eol);
|
|
1563
|
+
const open = `<!-- systemdox:${workspace} -->`;
|
|
1564
|
+
const close = `<!-- /systemdox:${workspace} -->`;
|
|
1565
|
+
const start = text3.indexOf(open);
|
|
1566
|
+
const end = start >= 0 ? text3.indexOf(close, start) : -1;
|
|
1567
|
+
if (start >= 0 && end >= 0) return text3.slice(0, start) + block + text3.slice(end + close.length);
|
|
1568
|
+
if (!text3) return block + eol;
|
|
1569
|
+
return text3 + (text3.endsWith("\n") ? "" : eol) + block + eol;
|
|
1570
|
+
}
|
|
1571
|
+
async function readJsonQuietly(path) {
|
|
1572
|
+
try {
|
|
1573
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
1574
|
+
} catch {
|
|
1575
|
+
return void 0;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
async function chooseProject(own, opts, canAsk) {
|
|
1579
|
+
if (opts.project) return opts.project;
|
|
1580
|
+
if (own.memory.length <= 1) return own.memory[0]?.project;
|
|
1581
|
+
const names = own.memory.map((m) => m.project);
|
|
1582
|
+
const usage = `${own.workspace} has ${names.length} team memories (${names.join(", ")}): name this checkout's with --project.`;
|
|
1583
|
+
if (!canAsk) throw new SetupFailed(usage);
|
|
1584
|
+
const answer = (await opts.ask(`Which team memory is this checkout's? (${names.join(", ")}) `)).trim();
|
|
1585
|
+
if (!names.includes(answer)) throw new SetupFailed(usage);
|
|
1586
|
+
return answer;
|
|
1587
|
+
}
|
|
1588
|
+
async function runTeamSetup(opts) {
|
|
1589
|
+
const { client, out } = opts;
|
|
1590
|
+
try {
|
|
1591
|
+
const own = await client.getOwnTeamSetup();
|
|
1592
|
+
const ws = own.workspace;
|
|
1593
|
+
if (!isWorkspace(ws)) throw new SetupFailed(`SystemDox named the workspace ${JSON.stringify(ws)}, which is not a workspace name.`);
|
|
1594
|
+
const instructionsVersion = own.parts.instructions;
|
|
1595
|
+
if (!own.plugin && instructionsVersion === void 0 && !own.memory.length && !opts.project) return 0;
|
|
1596
|
+
const need = parseVersion(own.minClaudeCodeVersion);
|
|
1597
|
+
const have = parseVersion(await (opts.claudeVersion ?? claudeCodeVersion)());
|
|
1598
|
+
if (need && (!have || olderThan(have, need))) {
|
|
1599
|
+
out(
|
|
1600
|
+
have ? `Claude Code ${own.minClaudeCodeVersion} or later is needed for the team setup; this computer has ${have.join(".")}.` : `Claude Code ${own.minClaudeCodeVersion} or later is needed for the team setup; it was not found on this computer.`
|
|
1601
|
+
);
|
|
1602
|
+
out("Update it with: claude update");
|
|
1603
|
+
out("Then run setup again. Nothing was changed.");
|
|
1604
|
+
return 3;
|
|
1605
|
+
}
|
|
1606
|
+
const home = homeDir();
|
|
1607
|
+
const userSettings = join3(home, ".claude", "settings.json");
|
|
1608
|
+
const userClaudeMd = join3(home, ".claude", "CLAUDE.md");
|
|
1609
|
+
const teamClaudeMd = join3(workspaceDir(ws), "CLAUDE.md");
|
|
1610
|
+
const checkout = resolve2(opts.dir ?? opts.cwd ?? process.cwd());
|
|
1611
|
+
const localSettings = join3(checkout, ".claude", "settings.local.json");
|
|
1612
|
+
const state = await readState(ws);
|
|
1613
|
+
const accepted = new Set(state.accepted ?? []);
|
|
1614
|
+
const update = accepted.size > 0;
|
|
1615
|
+
const canAsk = !!opts.ask && !(opts.yes && !opts.terminal);
|
|
1616
|
+
const project = await chooseProject(own, opts, canAsk);
|
|
1617
|
+
const marketplaceName = `systemdox-${ws}`;
|
|
1618
|
+
const marketplace = { source: { source: "url", url: own.marketplaceUrl, headersHelper: HEADERS_HELPER } };
|
|
1619
|
+
const items = [];
|
|
1620
|
+
items.push(
|
|
1621
|
+
item(`the marketplace ${marketplaceName} in ${userSettings} (extraKnownMarketplaces)`, [
|
|
1622
|
+
`"${marketplaceName}": ${JSON.stringify(marketplace)}`
|
|
1623
|
+
])
|
|
1624
|
+
);
|
|
1625
|
+
const plugin = own.plugin;
|
|
1626
|
+
let previousPlugin;
|
|
1627
|
+
if (plugin) {
|
|
1628
|
+
const hooks = hookLines(plugin);
|
|
1629
|
+
const hooksName = `the hook commands of ${ws}-setup`;
|
|
1630
|
+
const hooksItem = item(hooksName, hooks);
|
|
1631
|
+
if (update && hooks.length && !accepted.has(hooksItem.sha256)) {
|
|
1632
|
+
const versions = (await client.getPlugin(ws)).versions.filter((v) => v.version !== plugin.version);
|
|
1633
|
+
previousPlugin = versions.sort((a, b) => b.version - a.version).find((v) => accepted.has(item(hooksName, hookLines(v)).sha256));
|
|
1634
|
+
}
|
|
1635
|
+
items.push(item(`the plugin ${ws}-setup, version ${plugin.version}`, [`${ws}-setup, version ${plugin.version}`]));
|
|
1636
|
+
if (hooks.length) {
|
|
1637
|
+
items.push(
|
|
1638
|
+
item(
|
|
1639
|
+
hooksName,
|
|
1640
|
+
hooks,
|
|
1641
|
+
previousPlugin ? lineDiff(hookLines(previousPlugin).join("\n"), hooks.join("\n")) : void 0
|
|
1642
|
+
)
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
const servers = mcpLines(plugin);
|
|
1646
|
+
if (servers.length) items.push(item(`the MCP servers of ${ws}-setup`, servers));
|
|
1647
|
+
}
|
|
1648
|
+
let instructions;
|
|
1649
|
+
if (instructionsVersion !== void 0) {
|
|
1650
|
+
const v = await client.getPartVersion(ws, "instructions", instructionsVersion);
|
|
1651
|
+
const file = v.files.find((f) => f.path === "CLAUDE.md") ?? v.files[0];
|
|
1652
|
+
instructions = { version: v.version, content: file?.content ?? "" };
|
|
1653
|
+
const name = `the team instructions, written to ${teamClaudeMd}`;
|
|
1654
|
+
const shown = item(name, instructions.content.split(/\r?\n/));
|
|
1655
|
+
let diff;
|
|
1656
|
+
const before = state.instructions;
|
|
1657
|
+
if (update && !accepted.has(shown.sha256) && before && before.version !== instructions.version) {
|
|
1658
|
+
const old = await client.getPartVersion(ws, "instructions", before.version);
|
|
1659
|
+
const oldFile = old.files.find((f) => f.path === "CLAUDE.md") ?? old.files[0];
|
|
1660
|
+
diff = lineDiff(oldFile?.content ?? "", instructions.content);
|
|
1661
|
+
}
|
|
1662
|
+
items.push({
|
|
1663
|
+
...shown,
|
|
1664
|
+
lines: diff ?? [`version ${instructions.version}, ${instructions.content.split(/\r?\n/).length} lines`]
|
|
1665
|
+
});
|
|
1666
|
+
items.push(item(`the import block in ${userClaudeMd}`, importBlock(ws).split("\n")));
|
|
1667
|
+
}
|
|
1668
|
+
let memoryFolder;
|
|
1669
|
+
if (project) {
|
|
1670
|
+
memoryFolder = join3(workspaceDir(ws), "memory", project);
|
|
1671
|
+
for (const m of own.memory) {
|
|
1672
|
+
const folder = join3(workspaceDir(ws), "memory", m.project);
|
|
1673
|
+
const count = `${m.fileCount} file${m.fileCount === 1 ? "" : "s"}`;
|
|
1674
|
+
items.push({ ...item(`the team memory ${m.project}`, [folder]), lines: [`${m.project}: ${count}, synced into ${folder}`] });
|
|
1675
|
+
}
|
|
1676
|
+
if (!own.memory.some((m) => m.project === project)) {
|
|
1677
|
+
items.push({ ...item(`the team memory ${project}`, [memoryFolder]), lines: [`${project}: new, synced into ${memoryFolder}`] });
|
|
1678
|
+
}
|
|
1679
|
+
items.push(item(`autoMemoryDirectory in ${localSettings}`, [memoryFolder]));
|
|
1680
|
+
}
|
|
1681
|
+
const pending = items.filter((i) => !accepted.has(i.sha256));
|
|
1682
|
+
if (pending.length) {
|
|
1683
|
+
if (!canAsk) {
|
|
1684
|
+
out(`Not set up: ${pending.length === 1 ? "this needs" : "these need"} a person at a terminal (run setup without --yes):`);
|
|
1685
|
+
for (const i of pending) {
|
|
1686
|
+
out(`- ${i.name}:`);
|
|
1687
|
+
for (const l of i.lines) out(` ${l}`);
|
|
132
1688
|
}
|
|
133
|
-
return
|
|
1689
|
+
return 4;
|
|
134
1690
|
}
|
|
135
|
-
|
|
136
|
-
|
|
1691
|
+
out(update ? `What changed in the team setup of ${ws}:` : `The team setup of ${ws} will add:`);
|
|
1692
|
+
for (const i of pending) {
|
|
1693
|
+
out(`- ${i.name}:`);
|
|
1694
|
+
for (const l of i.lines) out(` ${l}`);
|
|
137
1695
|
}
|
|
138
|
-
|
|
139
|
-
|
|
1696
|
+
const answer = (await opts.ask("Set this up? [y/N] ")).trim();
|
|
1697
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
1698
|
+
out("Nothing was changed.");
|
|
1699
|
+
return 4;
|
|
140
1700
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
body: JSON.stringify(body)
|
|
153
|
-
});
|
|
154
|
-
if (!res.ok) {
|
|
155
|
-
const text2 = await res.text();
|
|
156
|
-
throw new Error(`SystemDox API error ${res.status}: ${text2}`);
|
|
157
|
-
}
|
|
158
|
-
return await res.json();
|
|
1701
|
+
}
|
|
1702
|
+
if (opts.keySource !== "credentials" && (await readCredentials())[ws]?.apiKey !== opts.apiKey) {
|
|
1703
|
+
await writeCredentials(ws, opts.apiKey);
|
|
1704
|
+
}
|
|
1705
|
+
if (instructions) {
|
|
1706
|
+
const next = sha2562(instructions.content);
|
|
1707
|
+
let current;
|
|
1708
|
+
try {
|
|
1709
|
+
current = await readFile3(teamClaudeMd, "utf8");
|
|
1710
|
+
} catch (e) {
|
|
1711
|
+
if (e.code !== "ENOENT") throw e;
|
|
159
1712
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
1713
|
+
const currentSha = current === void 0 ? void 0 : sha2562(current);
|
|
1714
|
+
if (currentSha !== void 0 && currentSha !== next && currentSha !== state.instructions?.sha256 && !opts.overwrite) {
|
|
1715
|
+
out(`${teamClaudeMd} was edited since setup installed it, so it was not replaced by version ${instructions.version}.`);
|
|
1716
|
+
out("Keep your text elsewhere, then run setup --overwrite to replace it.");
|
|
1717
|
+
return 2;
|
|
166
1718
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (params) {
|
|
171
|
-
for (const [key, value] of Object.entries(params)) {
|
|
172
|
-
url.searchParams.set(key, value);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return url;
|
|
1719
|
+
if (currentSha !== next) {
|
|
1720
|
+
await mkdir3(dirname3(teamClaudeMd), { recursive: true });
|
|
1721
|
+
await writeFile3(teamClaudeMd, instructions.content, "utf8");
|
|
176
1722
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
1723
|
+
await updateState(ws, (s) => {
|
|
1724
|
+
s.instructions = { version: instructions.version, sha256: next };
|
|
1725
|
+
});
|
|
1726
|
+
let text3 = "";
|
|
1727
|
+
try {
|
|
1728
|
+
text3 = await readFile3(userClaudeMd, "utf8");
|
|
1729
|
+
} catch (e) {
|
|
1730
|
+
if (e.code !== "ENOENT") throw e;
|
|
1731
|
+
}
|
|
1732
|
+
const withBlock = withImportBlock(text3, ws);
|
|
1733
|
+
if (withBlock !== text3) {
|
|
1734
|
+
await mkdir3(dirname3(userClaudeMd), { recursive: true });
|
|
1735
|
+
await writeFile3(userClaudeMd, withBlock, "utf8");
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
await setInJsonFile(userSettings, ["extraKnownMarketplaces", marketplaceName], marketplace);
|
|
1739
|
+
await removeOldPlugins(ws);
|
|
1740
|
+
if (project && memoryFolder) {
|
|
1741
|
+
await setInJsonFile(localSettings, ["autoMemoryDirectory"], memoryFolder);
|
|
1742
|
+
}
|
|
1743
|
+
await updateState(ws, (s) => {
|
|
1744
|
+
s.accepted = [...new Set(items.map((i) => i.sha256))];
|
|
1745
|
+
});
|
|
1746
|
+
if (project) {
|
|
1747
|
+
const code = await syncMemory({
|
|
1748
|
+
project,
|
|
1749
|
+
dryRun: false,
|
|
1750
|
+
client,
|
|
1751
|
+
out: (...lines) => lines.forEach((l) => out(l)),
|
|
1752
|
+
ask: opts.terminal ? opts.ask : void 0,
|
|
1753
|
+
now: opts.now
|
|
1754
|
+
});
|
|
1755
|
+
if (code !== 0) return 1;
|
|
1756
|
+
}
|
|
1757
|
+
if (plugin) {
|
|
1758
|
+
const pluginsDir = join3(home, ".claude", "plugins");
|
|
1759
|
+
const id = `${ws}-setup@${marketplaceName}`;
|
|
1760
|
+
const installed = await readJsonQuietly(join3(pluginsDir, "installed_plugins.json"));
|
|
1761
|
+
if (!installed?.plugins?.[id]) {
|
|
1762
|
+
const known = await readJsonQuietly(join3(pluginsDir, "known_marketplaces.json"));
|
|
1763
|
+
const install = `Run \`claude plugin install ${id}\` in your own terminal and accept the command it shows.`;
|
|
1764
|
+
const steps = known && typeof known === "object" && known[marketplaceName] ? ["One step is left:", `1. ${install}`] : ["Two steps are left:", "1. Open Claude Code once in any folder, then close it.", `2. ${install}`];
|
|
1765
|
+
out("");
|
|
1766
|
+
steps.forEach((l) => out(l));
|
|
186
1767
|
}
|
|
1768
|
+
}
|
|
1769
|
+
return 0;
|
|
1770
|
+
} catch (e) {
|
|
1771
|
+
out(e.message);
|
|
1772
|
+
return 1;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
async function removeOldPlugins(workspace) {
|
|
1776
|
+
const root = pluginRoot(workspace);
|
|
1777
|
+
if (!existsSync2(root)) return;
|
|
1778
|
+
const versions = (await readdir2(root, { withFileTypes: true })).filter((e) => e.isDirectory() && /^\d+$/.test(e.name)).map((e) => Number(e.name)).sort((a, b) => b - a);
|
|
1779
|
+
const old = versions.slice(2);
|
|
1780
|
+
if (!old.length) return;
|
|
1781
|
+
for (const v of old) await removeFolder(join3(root, String(v)));
|
|
1782
|
+
await updateState(workspace, (s) => {
|
|
1783
|
+
for (const v of old) delete s.plugin?.[String(v)];
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
async function runTeamSetupCommand(args2, key, io) {
|
|
1787
|
+
const client = new SystemDoxApiClient({ apiKey: key.apiKey, apiUrl: key.apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1788
|
+
return runTeamSetup({ ...args2, client, apiKey: key.apiKey, keySource: "prompt", ...io });
|
|
1789
|
+
}
|
|
1790
|
+
var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item, hookLines, mcpLines;
|
|
1791
|
+
var init_setup_team = __esm({
|
|
1792
|
+
"src/setup-team.ts"() {
|
|
1793
|
+
"use strict";
|
|
1794
|
+
init_api_client();
|
|
1795
|
+
init_memory_sync();
|
|
1796
|
+
init_team_setup_files();
|
|
1797
|
+
init_sync_memory();
|
|
1798
|
+
SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]";
|
|
1799
|
+
HEADERS_HELPER = "npx -y @systemdox/mcp-server marketplace-auth";
|
|
1800
|
+
SetupFailed = class extends Error {
|
|
187
1801
|
};
|
|
1802
|
+
sha2562 = (text3) => createHash2("sha256").update(text3, "utf8").digest("hex");
|
|
1803
|
+
run2 = promisify2(execFile2);
|
|
1804
|
+
item = (name, lines, diff) => ({
|
|
1805
|
+
name,
|
|
1806
|
+
lines: diff ?? lines,
|
|
1807
|
+
sha256: sha2562(`${name}
|
|
1808
|
+
${lines.join("\n")}`)
|
|
1809
|
+
});
|
|
1810
|
+
hookLines = (p) => p.hookCommands ?? [];
|
|
1811
|
+
mcpLines = (p) => (p.mcpServers ?? []).map((s) => `${s.name}: ${s.url ?? [s.command, ...s.args ?? []].filter(Boolean).join(" ")}`);
|
|
188
1812
|
}
|
|
189
1813
|
});
|
|
190
1814
|
|
|
191
1815
|
// src/lib/skills.ts
|
|
192
|
-
import { createHash } from "crypto";
|
|
193
|
-
import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
|
|
194
|
-
import { existsSync } from "fs";
|
|
195
|
-
import { homedir } from "os";
|
|
196
|
-
import { dirname, join, relative, resolve, sep } from "path";
|
|
197
|
-
function
|
|
198
|
-
return
|
|
1816
|
+
import { createHash as createHash3 } from "crypto";
|
|
1817
|
+
import { lstat, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
1818
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1819
|
+
import { homedir as homedir2 } from "os";
|
|
1820
|
+
import { dirname as dirname4, join as join4, relative, resolve as resolve3, sep } from "path";
|
|
1821
|
+
function sha2563(content) {
|
|
1822
|
+
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
199
1823
|
}
|
|
200
1824
|
function isValidName(name) {
|
|
201
1825
|
return name.length <= 64 && NAME_RE.test(name);
|
|
@@ -210,12 +1834,12 @@ function frontmatterName(skillMd) {
|
|
|
210
1834
|
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
211
1835
|
}
|
|
212
1836
|
function defaultSkillDir(name) {
|
|
213
|
-
return
|
|
1837
|
+
return join4(homedir2(), ".claude", "skills", name);
|
|
214
1838
|
}
|
|
215
1839
|
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
216
|
-
for (const entry of await
|
|
1840
|
+
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
217
1841
|
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
218
|
-
const full =
|
|
1842
|
+
const full = join4(dir, entry.name);
|
|
219
1843
|
const rel = relative(root, full).split(sep).join("/");
|
|
220
1844
|
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
221
1845
|
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
@@ -225,18 +1849,9 @@ async function walk(root, { skipHidden }, dir = root, out = { files: [], links:
|
|
|
225
1849
|
out.files.sort();
|
|
226
1850
|
return out;
|
|
227
1851
|
}
|
|
228
|
-
async function
|
|
229
|
-
const root = resolve(directory);
|
|
230
|
-
if (!existsSync(join(root, ENTRY_FILE))) {
|
|
231
|
-
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
232
|
-
}
|
|
233
|
-
const walked = await walk(root, { skipHidden: true });
|
|
234
|
-
if (walked.links.length) {
|
|
235
|
-
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
236
|
-
}
|
|
237
|
-
const paths = walked.files;
|
|
1852
|
+
async function readTextFiles(root, paths, what) {
|
|
238
1853
|
if (paths.length > MAX_FILES) {
|
|
239
|
-
throw new Error(`${root} holds ${paths.length} files:
|
|
1854
|
+
throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
|
|
240
1855
|
}
|
|
241
1856
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
242
1857
|
const files = [];
|
|
@@ -245,8 +1860,8 @@ async function readSkillDirectory(directory) {
|
|
|
245
1860
|
if (!isValidPath(path)) {
|
|
246
1861
|
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
247
1862
|
}
|
|
248
|
-
const raw = await
|
|
249
|
-
if (raw.includes(0)) throw new Error(`${path} is binary:
|
|
1863
|
+
const raw = await readFile4(join4(root, ...path.split("/")));
|
|
1864
|
+
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
250
1865
|
if (raw.length > MAX_FILE_BYTES) {
|
|
251
1866
|
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
252
1867
|
}
|
|
@@ -262,6 +1877,18 @@ async function readSkillDirectory(directory) {
|
|
|
262
1877
|
}
|
|
263
1878
|
files.push({ path, content });
|
|
264
1879
|
}
|
|
1880
|
+
return files;
|
|
1881
|
+
}
|
|
1882
|
+
async function readSkillDirectory(directory) {
|
|
1883
|
+
const root = resolve3(directory);
|
|
1884
|
+
if (!existsSync3(join4(root, ENTRY_FILE))) {
|
|
1885
|
+
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
1886
|
+
}
|
|
1887
|
+
const walked = await walk(root, { skipHidden: true });
|
|
1888
|
+
if (walked.links.length) {
|
|
1889
|
+
throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
|
|
1890
|
+
}
|
|
1891
|
+
const files = await readTextFiles(root, walked.files, "a skill");
|
|
265
1892
|
const skillMd = files.find((f) => f.path === ENTRY_FILE);
|
|
266
1893
|
const name = frontmatterName(skillMd.content);
|
|
267
1894
|
if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
|
|
@@ -269,13 +1896,13 @@ async function readSkillDirectory(directory) {
|
|
|
269
1896
|
}
|
|
270
1897
|
function within(dest, path) {
|
|
271
1898
|
if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
|
|
272
|
-
const full =
|
|
273
|
-
if (!full.startsWith(
|
|
1899
|
+
const full = resolve3(dest, ...path.split("/"));
|
|
1900
|
+
if (!full.startsWith(resolve3(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
|
|
274
1901
|
return full;
|
|
275
1902
|
}
|
|
276
1903
|
async function readManifest(dest) {
|
|
277
1904
|
try {
|
|
278
|
-
return JSON.parse(await
|
|
1905
|
+
return JSON.parse(await readFile4(join4(dest, MANIFEST_FILE), "utf8"));
|
|
279
1906
|
} catch {
|
|
280
1907
|
return void 0;
|
|
281
1908
|
}
|
|
@@ -284,10 +1911,10 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
284
1911
|
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
285
1912
|
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
286
1913
|
for (const f of version.files) within(dest, f.path);
|
|
287
|
-
if (
|
|
1914
|
+
if (existsSync3(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
288
1915
|
throw new Error(`${dest} is a link: install into a real folder`);
|
|
289
1916
|
}
|
|
290
|
-
const walked =
|
|
1917
|
+
const walked = existsSync3(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
291
1918
|
if (walked.links.length) {
|
|
292
1919
|
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
293
1920
|
}
|
|
@@ -299,10 +1926,10 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
299
1926
|
const edited = [];
|
|
300
1927
|
const untracked = [];
|
|
301
1928
|
for (const path of present) {
|
|
302
|
-
const content = await
|
|
1929
|
+
const content = await readFile4(join4(dest, ...path.split("/")), "utf8");
|
|
303
1930
|
if (Object.hasOwn(known, path)) {
|
|
304
|
-
if (
|
|
305
|
-
} else if (!incoming.has(path) ||
|
|
1931
|
+
if (sha2563(content) !== known[path]) edited.push(path);
|
|
1932
|
+
} else if (!incoming.has(path) || sha2563(content) !== incoming.get(path).sha256) {
|
|
306
1933
|
if (incoming.has(path) || !manifest) edited.push(path);
|
|
307
1934
|
else untracked.push(path);
|
|
308
1935
|
}
|
|
@@ -312,8 +1939,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
312
1939
|
const written = [];
|
|
313
1940
|
for (const f of version.files) {
|
|
314
1941
|
const full = within(dest, f.path);
|
|
315
|
-
await
|
|
316
|
-
await
|
|
1942
|
+
await mkdir4(dirname4(full), { recursive: true });
|
|
1943
|
+
await writeFile4(full, f.content, "utf8");
|
|
317
1944
|
written.push(f.path);
|
|
318
1945
|
}
|
|
319
1946
|
const removed = [];
|
|
@@ -324,7 +1951,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
324
1951
|
keptEdited.push(path);
|
|
325
1952
|
continue;
|
|
326
1953
|
}
|
|
327
|
-
await
|
|
1954
|
+
await rm3(within(dest, path));
|
|
328
1955
|
removed.push(path);
|
|
329
1956
|
}
|
|
330
1957
|
const next = {
|
|
@@ -333,7 +1960,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
333
1960
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
334
1961
|
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
335
1962
|
};
|
|
336
|
-
await
|
|
1963
|
+
await writeFile4(join4(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
337
1964
|
return {
|
|
338
1965
|
dest,
|
|
339
1966
|
version: version.version,
|
|
@@ -343,8 +1970,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
343
1970
|
keptEdited
|
|
344
1971
|
};
|
|
345
1972
|
}
|
|
346
|
-
function fenceFor(
|
|
347
|
-
const longest = Math.max(0, ...(
|
|
1973
|
+
function fenceFor(text3) {
|
|
1974
|
+
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
348
1975
|
return "`".repeat(Math.max(3, longest + 1));
|
|
349
1976
|
}
|
|
350
1977
|
function describeInstall(name, r) {
|
|
@@ -392,7 +2019,7 @@ __export(install_skill_exports, {
|
|
|
392
2019
|
parseInstallArgs: () => parseInstallArgs,
|
|
393
2020
|
runInstallSkill: () => runInstallSkill
|
|
394
2021
|
});
|
|
395
|
-
import { basename, resolve as
|
|
2022
|
+
import { basename, resolve as resolve4 } from "path";
|
|
396
2023
|
function parseInstallArgs(argv) {
|
|
397
2024
|
const args2 = { overwrite: false };
|
|
398
2025
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -413,7 +2040,7 @@ ${INSTALL_USAGE}`);
|
|
|
413
2040
|
}
|
|
414
2041
|
if (!args2.name) throw new Error(INSTALL_USAGE);
|
|
415
2042
|
if (!isValidName(args2.name)) throw new Error(`${args2.name} is not a skill name`);
|
|
416
|
-
if (args2.target && basename(
|
|
2043
|
+
if (args2.target && basename(resolve4(args2.target)) !== args2.name) {
|
|
417
2044
|
throw new Error(`--target must be a folder named ${args2.name} (e.g. .claude/skills/${args2.name})`);
|
|
418
2045
|
}
|
|
419
2046
|
return args2;
|
|
@@ -469,6 +2096,226 @@ var init_install_skill = __esm({
|
|
|
469
2096
|
}
|
|
470
2097
|
});
|
|
471
2098
|
|
|
2099
|
+
// src/plugin-path.ts
|
|
2100
|
+
var plugin_path_exports = {};
|
|
2101
|
+
__export(plugin_path_exports, {
|
|
2102
|
+
MAX_UNPACKED_BYTES: () => MAX_UNPACKED_BYTES,
|
|
2103
|
+
PLUGIN_PATH_USAGE: () => PLUGIN_PATH_USAGE,
|
|
2104
|
+
PluginRefused: () => PluginRefused,
|
|
2105
|
+
parsePluginPathArgs: () => parsePluginPathArgs,
|
|
2106
|
+
readZipEntries: () => readZipEntries,
|
|
2107
|
+
runPluginPath: () => runPluginPath,
|
|
2108
|
+
unpackZip: () => unpackZip
|
|
2109
|
+
});
|
|
2110
|
+
import { createHash as createHash4, randomBytes } from "crypto";
|
|
2111
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2112
|
+
import { mkdir as mkdir5, rename as rename2, writeFile as writeFile5 } from "fs/promises";
|
|
2113
|
+
import { dirname as dirname5, join as join5, resolve as resolve5, sep as sep2 } from "path";
|
|
2114
|
+
import { inflateRawSync } from "zlib";
|
|
2115
|
+
function parsePluginPathArgs(argv) {
|
|
2116
|
+
const args2 = {};
|
|
2117
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2118
|
+
const a = argv[i];
|
|
2119
|
+
if (a === "--version" || a === "--sha256") {
|
|
2120
|
+
const value = argv[++i];
|
|
2121
|
+
if (!value) throw new Error(`${a} needs a value
|
|
2122
|
+
${PLUGIN_PATH_USAGE}`);
|
|
2123
|
+
if (a === "--sha256") {
|
|
2124
|
+
if (!SHA256_RE.test(value)) throw new Error(`--sha256 must be 64 lowercase hex characters, got ${value}`);
|
|
2125
|
+
args2.sha256 = value;
|
|
2126
|
+
} else if (/^[1-9]\d{0,8}$/.test(value)) args2.version = Number(value);
|
|
2127
|
+
else throw new Error(`--version must be a whole number, got ${value}`);
|
|
2128
|
+
} else if (a.startsWith("-")) throw new Error(`Unknown option ${a}
|
|
2129
|
+
${PLUGIN_PATH_USAGE}`);
|
|
2130
|
+
else if (!args2.workspace) args2.workspace = a;
|
|
2131
|
+
else throw new Error(`Unexpected argument ${a}
|
|
2132
|
+
${PLUGIN_PATH_USAGE}`);
|
|
2133
|
+
}
|
|
2134
|
+
if (!args2.workspace || !args2.version || !args2.sha256) throw new Error(PLUGIN_PATH_USAGE);
|
|
2135
|
+
if (!isWorkspace(args2.workspace)) throw new Error(`${args2.workspace} is not a workspace name`);
|
|
2136
|
+
return args2;
|
|
2137
|
+
}
|
|
2138
|
+
function refuse(name, why) {
|
|
2139
|
+
throw new PluginRefused(`refused the plugin entry ${JSON.stringify(name)}: it ${why}. Nothing was unpacked.`);
|
|
2140
|
+
}
|
|
2141
|
+
function readZipEntries(zip) {
|
|
2142
|
+
let eocd = -1;
|
|
2143
|
+
for (let i = zip.length - 22; i >= Math.max(0, zip.length - 22 - 65535); i--) {
|
|
2144
|
+
if (zip.readUInt32LE(i) === EOCD) {
|
|
2145
|
+
eocd = i;
|
|
2146
|
+
break;
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
if (eocd < 0) throw new PluginRefused("the download is not a zip. Nothing was unpacked.");
|
|
2150
|
+
const count = zip.readUInt16LE(eocd + 10);
|
|
2151
|
+
let at = zip.readUInt32LE(eocd + 16);
|
|
2152
|
+
if (count === 65535 || at === 4294967295) throw new PluginRefused("the zip is a ZIP64 archive, which a plugin never is. Nothing was unpacked.");
|
|
2153
|
+
const entries2 = [];
|
|
2154
|
+
let total = 0;
|
|
2155
|
+
for (let n = 0; n < count; n++) {
|
|
2156
|
+
if (at + 46 > zip.length || zip.readUInt32LE(at) !== CENTRAL) {
|
|
2157
|
+
throw new PluginRefused("the zip's directory is damaged. Nothing was unpacked.");
|
|
2158
|
+
}
|
|
2159
|
+
const madeBy = zip.readUInt16LE(at + 4);
|
|
2160
|
+
const flags = zip.readUInt16LE(at + 8);
|
|
2161
|
+
const method = zip.readUInt16LE(at + 10);
|
|
2162
|
+
const compressedSize = zip.readUInt32LE(at + 20);
|
|
2163
|
+
const size = zip.readUInt32LE(at + 24);
|
|
2164
|
+
const nameLength = zip.readUInt16LE(at + 28);
|
|
2165
|
+
const extraLength = zip.readUInt16LE(at + 30);
|
|
2166
|
+
const commentLength = zip.readUInt16LE(at + 32);
|
|
2167
|
+
const external = zip.readUInt32LE(at + 38);
|
|
2168
|
+
const offset = zip.readUInt32LE(at + 42);
|
|
2169
|
+
const name = zip.subarray(at + 46, at + 46 + nameLength).toString(flags & 2048 ? "utf8" : "latin1");
|
|
2170
|
+
at += 46 + nameLength + extraLength + commentLength;
|
|
2171
|
+
const unixMode = madeBy >> 8 === 3 ? external >>> 16 : 0;
|
|
2172
|
+
const segments = name.split(/[\\/]/);
|
|
2173
|
+
if (!name || /^[\\/]/.test(name) || /^[A-Za-z]:/.test(name)) refuse(name, "is absolute");
|
|
2174
|
+
if (segments.includes("..")) refuse(name, "climbs out of the plugin folder (..)");
|
|
2175
|
+
if (name.includes(":") || name.includes("\0")) refuse(name, "is not a plain relative path");
|
|
2176
|
+
if ((unixMode & S_IFMT) === S_IFLNK) refuse(name, "is a link");
|
|
2177
|
+
const directory = name.endsWith("/") || (unixMode & S_IFMT) === S_IFDIR;
|
|
2178
|
+
if (!directory && unixMode && (unixMode & S_IFMT) !== S_IFREG) refuse(name, "is not a regular file");
|
|
2179
|
+
if (flags & 1) refuse(name, "is encrypted");
|
|
2180
|
+
if (!directory && method !== 0 && method !== 8) refuse(name, `uses compression method ${method}`);
|
|
2181
|
+
total += size;
|
|
2182
|
+
if (total > MAX_UNPACKED_BYTES) refuse(name, `takes the unpacked size past ${MAX_UNPACKED_BYTES} bytes`);
|
|
2183
|
+
entries2.push({ name, directory, mode: unixMode ? unixMode & 511 : void 0, method, compressedSize, size, offset });
|
|
2184
|
+
}
|
|
2185
|
+
return entries2;
|
|
2186
|
+
}
|
|
2187
|
+
function entryData(zip, entry) {
|
|
2188
|
+
const at = entry.offset;
|
|
2189
|
+
if (at + 30 > zip.length || zip.readUInt32LE(at) !== LOCAL) refuse(entry.name, "has a damaged header");
|
|
2190
|
+
const start = at + 30 + zip.readUInt16LE(at + 26) + zip.readUInt16LE(at + 28);
|
|
2191
|
+
const raw = zip.subarray(start, start + entry.compressedSize);
|
|
2192
|
+
let data;
|
|
2193
|
+
try {
|
|
2194
|
+
data = entry.method === 0 ? raw : inflateRawSync(raw, { maxOutputLength: Math.max(1, entry.size) + 1 });
|
|
2195
|
+
} catch (e) {
|
|
2196
|
+
refuse(entry.name, `cannot be unpacked (${e.message})`);
|
|
2197
|
+
}
|
|
2198
|
+
if (data.length !== entry.size) refuse(entry.name, "unpacks to a size other than the one it declares");
|
|
2199
|
+
return data;
|
|
2200
|
+
}
|
|
2201
|
+
async function unpackZip(zip, dest) {
|
|
2202
|
+
const entries2 = readZipEntries(zip);
|
|
2203
|
+
const root = resolve5(dest);
|
|
2204
|
+
const targets = entries2.map((e) => {
|
|
2205
|
+
const full = resolve5(root, ...e.name.split(/[\\/]/).filter(Boolean));
|
|
2206
|
+
if (full !== root && !full.startsWith(root + sep2)) refuse(e.name, "climbs out of the plugin folder");
|
|
2207
|
+
return full;
|
|
2208
|
+
});
|
|
2209
|
+
const data = entries2.map((e) => e.directory ? void 0 : entryData(zip, e));
|
|
2210
|
+
await mkdir5(root, { recursive: true });
|
|
2211
|
+
for (const [i, entry] of entries2.entries()) {
|
|
2212
|
+
if (entry.directory) {
|
|
2213
|
+
await mkdir5(targets[i], { recursive: true });
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
await mkdir5(dirname5(targets[i]), { recursive: true });
|
|
2217
|
+
await writeFile5(targets[i], data[i], { mode: entry.mode ?? 420 });
|
|
2218
|
+
}
|
|
2219
|
+
return entries2.filter((e) => !e.directory).length;
|
|
2220
|
+
}
|
|
2221
|
+
async function runPluginPath(argv, io) {
|
|
2222
|
+
let args2;
|
|
2223
|
+
try {
|
|
2224
|
+
args2 = parsePluginPathArgs(argv);
|
|
2225
|
+
} catch (e) {
|
|
2226
|
+
io.err(e.message);
|
|
2227
|
+
return 1;
|
|
2228
|
+
}
|
|
2229
|
+
const { workspace, version, sha256: sha2564 } = args2;
|
|
2230
|
+
const folder = pluginDir(workspace, version);
|
|
2231
|
+
try {
|
|
2232
|
+
const state = await readState(workspace);
|
|
2233
|
+
if (existsSync4(folder) && state.plugin?.[String(version)]?.sha256 === sha2564) {
|
|
2234
|
+
io.out(folder);
|
|
2235
|
+
return 0;
|
|
2236
|
+
}
|
|
2237
|
+
} catch (e) {
|
|
2238
|
+
io.err(e.message);
|
|
2239
|
+
return 1;
|
|
2240
|
+
}
|
|
2241
|
+
let client = io.client;
|
|
2242
|
+
if (!client) {
|
|
2243
|
+
try {
|
|
2244
|
+
const apiUrl = apiUrlFrom();
|
|
2245
|
+
const apiKey = await apiKeyFor(workspace);
|
|
2246
|
+
if (!apiKey) {
|
|
2247
|
+
io.err(
|
|
2248
|
+
`No SystemDox key for ${workspace}. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup, then run this again.`
|
|
2249
|
+
);
|
|
2250
|
+
return 1;
|
|
2251
|
+
}
|
|
2252
|
+
client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
2253
|
+
} catch (e) {
|
|
2254
|
+
io.err(e.message);
|
|
2255
|
+
return 1;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
let zip;
|
|
2259
|
+
try {
|
|
2260
|
+
({ bytes: zip } = await client.getPluginZip(workspace, version));
|
|
2261
|
+
} catch (e) {
|
|
2262
|
+
io.err(e.message);
|
|
2263
|
+
return 1;
|
|
2264
|
+
}
|
|
2265
|
+
const got = createHash4("sha256").update(zip).digest("hex");
|
|
2266
|
+
if (got !== sha2564) {
|
|
2267
|
+
io.err(
|
|
2268
|
+
`The download of ${workspace}'s team plugin version ${version} has SHA-256 ${got}, not the ${sha2564} Claude Code named. Nothing was unpacked.`
|
|
2269
|
+
);
|
|
2270
|
+
return 2;
|
|
2271
|
+
}
|
|
2272
|
+
const tmp = join5(pluginRoot(workspace), `.${version}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
2273
|
+
try {
|
|
2274
|
+
await unpackZip(zip, tmp);
|
|
2275
|
+
if (existsSync4(folder) && (await readState(workspace)).plugin?.[String(version)]?.sha256 === sha2564) {
|
|
2276
|
+
await removeFolder(tmp);
|
|
2277
|
+
} else {
|
|
2278
|
+
if (existsSync4(folder)) await removeFolder(folder);
|
|
2279
|
+
try {
|
|
2280
|
+
await rename2(tmp, folder);
|
|
2281
|
+
} catch (e) {
|
|
2282
|
+
if (!existsSync4(folder)) throw e;
|
|
2283
|
+
await removeFolder(tmp);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
await updateState(workspace, (state) => {
|
|
2287
|
+
state.plugin = { ...state.plugin, [String(version)]: { sha256: sha2564, installedAt: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
2288
|
+
});
|
|
2289
|
+
} catch (e) {
|
|
2290
|
+
await removeFolder(tmp);
|
|
2291
|
+
io.err(e.message);
|
|
2292
|
+
return e instanceof PluginRefused ? 2 : 1;
|
|
2293
|
+
}
|
|
2294
|
+
io.err(`Unpacked ${workspace}'s team plugin version ${version}.`);
|
|
2295
|
+
io.out(folder);
|
|
2296
|
+
return 0;
|
|
2297
|
+
}
|
|
2298
|
+
var PLUGIN_PATH_USAGE, MAX_UNPACKED_BYTES, SHA256_RE, PluginRefused, EOCD, CENTRAL, LOCAL, S_IFMT, S_IFLNK, S_IFDIR, S_IFREG;
|
|
2299
|
+
var init_plugin_path = __esm({
|
|
2300
|
+
"src/plugin-path.ts"() {
|
|
2301
|
+
"use strict";
|
|
2302
|
+
init_api_client();
|
|
2303
|
+
init_team_setup_files();
|
|
2304
|
+
PLUGIN_PATH_USAGE = "Usage: systemdox-mcp-server plugin-path <workspace> --version <n> --sha256 <hash>";
|
|
2305
|
+
MAX_UNPACKED_BYTES = 20 * 1024 * 1024;
|
|
2306
|
+
SHA256_RE = /^[0-9a-f]{64}$/;
|
|
2307
|
+
PluginRefused = class extends Error {
|
|
2308
|
+
};
|
|
2309
|
+
EOCD = 101010256;
|
|
2310
|
+
CENTRAL = 33639248;
|
|
2311
|
+
LOCAL = 67324752;
|
|
2312
|
+
S_IFMT = 61440;
|
|
2313
|
+
S_IFLNK = 40960;
|
|
2314
|
+
S_IFDIR = 16384;
|
|
2315
|
+
S_IFREG = 32768;
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
|
|
472
2319
|
// src/lib/language.ts
|
|
473
2320
|
function detectLanguage(filePath) {
|
|
474
2321
|
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
@@ -601,12 +2448,12 @@ function registerGetGuardrails(server2, apiClient2) {
|
|
|
601
2448
|
);
|
|
602
2449
|
}
|
|
603
2450
|
if (filtered.length === 0) {
|
|
604
|
-
const
|
|
2451
|
+
const text4 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient2))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
|
|
605
2452
|
return {
|
|
606
|
-
content: [{ type: "text", text:
|
|
2453
|
+
content: [{ type: "text", text: text4 }]
|
|
607
2454
|
};
|
|
608
2455
|
}
|
|
609
|
-
const
|
|
2456
|
+
const text3 = filtered.map(
|
|
610
2457
|
(g) => `## ${g.title} [${g.severity}]
|
|
611
2458
|
|
|
612
2459
|
${g.description}
|
|
@@ -630,7 +2477,7 @@ ${g.fix}
|
|
|
630
2477
|
type: "text",
|
|
631
2478
|
text: `Found ${filtered.length} check(s):
|
|
632
2479
|
|
|
633
|
-
${
|
|
2480
|
+
${text3}`
|
|
634
2481
|
}
|
|
635
2482
|
]
|
|
636
2483
|
};
|
|
@@ -741,7 +2588,7 @@ ${spec.taskContent}`);
|
|
|
741
2588
|
]
|
|
742
2589
|
};
|
|
743
2590
|
}
|
|
744
|
-
const
|
|
2591
|
+
const text3 = filtered.map(
|
|
745
2592
|
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
746
2593
|
).join("\n");
|
|
747
2594
|
return {
|
|
@@ -750,7 +2597,7 @@ ${spec.taskContent}`);
|
|
|
750
2597
|
type: "text",
|
|
751
2598
|
text: `Found ${filtered.length} spec(s):
|
|
752
2599
|
|
|
753
|
-
${
|
|
2600
|
+
${text3}`
|
|
754
2601
|
}
|
|
755
2602
|
]
|
|
756
2603
|
};
|
|
@@ -808,18 +2655,18 @@ function registerGetDocs(server2, apiClient2) {
|
|
|
808
2655
|
isError: true
|
|
809
2656
|
};
|
|
810
2657
|
}
|
|
811
|
-
let
|
|
2658
|
+
let text4 = doc.content;
|
|
812
2659
|
if (anchor && doc.section_resolved === false) {
|
|
813
|
-
|
|
2660
|
+
text4 = `_Section "${anchor}" not found \u2014 returning the full document._
|
|
814
2661
|
|
|
815
|
-
` +
|
|
2662
|
+
` + text4;
|
|
816
2663
|
} else if (anchor && doc.heading) {
|
|
817
|
-
|
|
2664
|
+
text4 = `## ${doc.heading}
|
|
818
2665
|
|
|
819
2666
|
${doc.content}`;
|
|
820
2667
|
}
|
|
821
2668
|
return {
|
|
822
|
-
content: [{ type: "text", text:
|
|
2669
|
+
content: [{ type: "text", text: text4 }]
|
|
823
2670
|
};
|
|
824
2671
|
}
|
|
825
2672
|
const params = {};
|
|
@@ -831,7 +2678,7 @@ ${doc.content}`;
|
|
|
831
2678
|
);
|
|
832
2679
|
let items = response.items;
|
|
833
2680
|
if (path) {
|
|
834
|
-
items = items.filter((
|
|
2681
|
+
items = items.filter((item2) => matchGlob(item2.path, path));
|
|
835
2682
|
}
|
|
836
2683
|
if (items.length === 0) {
|
|
837
2684
|
return {
|
|
@@ -843,7 +2690,7 @@ ${doc.content}`;
|
|
|
843
2690
|
]
|
|
844
2691
|
};
|
|
845
2692
|
}
|
|
846
|
-
const
|
|
2693
|
+
const text3 = items.map((d) => {
|
|
847
2694
|
const sections = (d.outline ?? []).slice(0, 8);
|
|
848
2695
|
const sectionHint = sections.length > 0 ? `
|
|
849
2696
|
sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
|
|
@@ -857,7 +2704,7 @@ ${doc.content}`;
|
|
|
857
2704
|
type: "text",
|
|
858
2705
|
text: `Found ${items.length} document(s) (${response.total} total):
|
|
859
2706
|
|
|
860
|
-
${
|
|
2707
|
+
${text3}
|
|
861
2708
|
|
|
862
2709
|
Use doc_id for full content, or doc_id + section to fetch one section.`
|
|
863
2710
|
}
|
|
@@ -900,8 +2747,8 @@ function registerGetTechStack(server2, apiClient2) {
|
|
|
900
2747
|
list.push(entry);
|
|
901
2748
|
grouped.set(entry.category, list);
|
|
902
2749
|
}
|
|
903
|
-
const sections = Array.from(grouped.entries()).map(([category,
|
|
904
|
-
const items =
|
|
2750
|
+
const sections = Array.from(grouped.entries()).map(([category, entries2]) => {
|
|
2751
|
+
const items = entries2.map(
|
|
905
2752
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
906
2753
|
).join("\n");
|
|
907
2754
|
return `### ${category}
|
|
@@ -1011,8 +2858,8 @@ var init_get_planning_context = __esm({
|
|
|
1011
2858
|
|
|
1012
2859
|
// src/tools/get-plan-board.ts
|
|
1013
2860
|
import { z as z5 } from "zod";
|
|
1014
|
-
function bucketOf(
|
|
1015
|
-
const status = (
|
|
2861
|
+
function bucketOf(item2) {
|
|
2862
|
+
const status = (item2.fields?.Status ?? "").trim().toLowerCase();
|
|
1016
2863
|
if (status) {
|
|
1017
2864
|
if (status.includes("done") || status.includes("ship") || status.includes("complete")) {
|
|
1018
2865
|
return "done";
|
|
@@ -1022,7 +2869,7 @@ function bucketOf(item) {
|
|
|
1022
2869
|
}
|
|
1023
2870
|
return "todo";
|
|
1024
2871
|
}
|
|
1025
|
-
return
|
|
2872
|
+
return item2.state === "CLOSED" ? "done" : "todo";
|
|
1026
2873
|
}
|
|
1027
2874
|
function formatBoards(boards) {
|
|
1028
2875
|
const lines = boards.map((b) => {
|
|
@@ -1033,16 +2880,16 @@ function formatBoards(boards) {
|
|
|
1033
2880
|
});
|
|
1034
2881
|
return lines.join("\n");
|
|
1035
2882
|
}
|
|
1036
|
-
function formatItem(
|
|
2883
|
+
function formatItem(item2) {
|
|
1037
2884
|
const meta = [];
|
|
1038
|
-
if (
|
|
1039
|
-
if (
|
|
1040
|
-
meta.push(`assignees: ${
|
|
1041
|
-
const epic =
|
|
2885
|
+
if (item2.labels.length > 0) meta.push(`labels: ${item2.labels.join(", ")}`);
|
|
2886
|
+
if (item2.assignees.length > 0)
|
|
2887
|
+
meta.push(`assignees: ${item2.assignees.join(", ")}`);
|
|
2888
|
+
const epic = item2.fields?.Epic;
|
|
1042
2889
|
if (epic) meta.push(`epic: ${epic}`);
|
|
1043
|
-
return `- #${
|
|
1044
|
-
${meta.join(" \xB7 ")}` : "") + (
|
|
1045
|
-
${
|
|
2890
|
+
return `- #${item2.number} ${item2.title}${item2.repo ? ` \u2014 ${item2.repo}` : ""}` + (meta.length > 0 ? `
|
|
2891
|
+
${meta.join(" \xB7 ")}` : "") + (item2.url ? `
|
|
2892
|
+
${item2.url}` : "");
|
|
1046
2893
|
}
|
|
1047
2894
|
function registerGetPlanBoard(server2, apiClient2) {
|
|
1048
2895
|
server2.registerTool(
|
|
@@ -1124,7 +2971,7 @@ Call get_plan_board with project_number to list a board's issues.`
|
|
|
1124
2971
|
todo: [],
|
|
1125
2972
|
done: []
|
|
1126
2973
|
};
|
|
1127
|
-
for (const
|
|
2974
|
+
for (const item2 of allItems) grouped[bucketOf(item2)].push(item2);
|
|
1128
2975
|
const wanted = status ? [STATUS_ARG_TO_BUCKET[status]] : BUCKET_ORDER;
|
|
1129
2976
|
const shown = wanted.reduce((n, b) => n + grouped[b].length, 0);
|
|
1130
2977
|
if (shown === 0) {
|
|
@@ -1202,13 +3049,13 @@ var init_get_plan_board = __esm({
|
|
|
1202
3049
|
});
|
|
1203
3050
|
|
|
1204
3051
|
// src/lib/scoring.ts
|
|
1205
|
-
function estimateTokens(
|
|
1206
|
-
return Math.ceil(
|
|
3052
|
+
function estimateTokens(text3) {
|
|
3053
|
+
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
1207
3054
|
}
|
|
1208
|
-
function truncateToTokens(
|
|
3055
|
+
function truncateToTokens(text3, maxTokens) {
|
|
1209
3056
|
const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
|
|
1210
|
-
if (
|
|
1211
|
-
const head =
|
|
3057
|
+
if (text3.length <= maxChars) return { text: text3, truncated: false };
|
|
3058
|
+
const head = text3.slice(0, maxChars);
|
|
1212
3059
|
const floor = Math.floor(maxChars * 0.75);
|
|
1213
3060
|
const paragraph = head.lastIndexOf("\n\n");
|
|
1214
3061
|
const line = head.lastIndexOf("\n");
|
|
@@ -1217,11 +3064,11 @@ function truncateToTokens(text2, maxTokens) {
|
|
|
1217
3064
|
else if (line >= floor) cut = line;
|
|
1218
3065
|
return { text: head.slice(0, cut).trimEnd(), truncated: true };
|
|
1219
3066
|
}
|
|
1220
|
-
function scorePathMatch(
|
|
3067
|
+
function scorePathMatch(item2, languages, categories) {
|
|
1221
3068
|
if (languages.length === 0 && categories.length === 0) return 0.5;
|
|
1222
3069
|
let score = 0;
|
|
1223
|
-
const itemLang =
|
|
1224
|
-
const itemCat =
|
|
3070
|
+
const itemLang = item2.language?.toLowerCase() ?? "";
|
|
3071
|
+
const itemCat = item2.category?.toLowerCase() ?? "";
|
|
1225
3072
|
if (itemLang && languages.some((l) => itemLang.includes(l))) {
|
|
1226
3073
|
score += 0.6;
|
|
1227
3074
|
}
|
|
@@ -1230,30 +3077,30 @@ function scorePathMatch(item, languages, categories) {
|
|
|
1230
3077
|
}
|
|
1231
3078
|
return Math.min(score, 1);
|
|
1232
3079
|
}
|
|
1233
|
-
function scoreKeywordMatch(
|
|
3080
|
+
function scoreKeywordMatch(item2, keywords) {
|
|
1234
3081
|
if (keywords.length === 0) return 0;
|
|
1235
|
-
const searchable = `${
|
|
3082
|
+
const searchable = `${item2.title} ${item2.content}`.toLowerCase();
|
|
1236
3083
|
let matches = 0;
|
|
1237
3084
|
for (const kw of keywords) {
|
|
1238
3085
|
if (searchable.includes(kw)) matches++;
|
|
1239
3086
|
}
|
|
1240
3087
|
return Math.min(matches / keywords.length, 1);
|
|
1241
3088
|
}
|
|
1242
|
-
function scoreRecency(
|
|
1243
|
-
if (!
|
|
3089
|
+
function scoreRecency(item2) {
|
|
3090
|
+
if (!item2.updatedAt) return 0.5;
|
|
1244
3091
|
const now = Date.now();
|
|
1245
|
-
const updated = new Date(
|
|
3092
|
+
const updated = new Date(item2.updatedAt).getTime();
|
|
1246
3093
|
const daysSince = (now - updated) / (1e3 * 60 * 60 * 24);
|
|
1247
3094
|
if (daysSince <= 7) return 1;
|
|
1248
3095
|
if (daysSince <= 30) return 0.75;
|
|
1249
3096
|
if (daysSince <= 90) return 0.5;
|
|
1250
3097
|
return 0.25;
|
|
1251
3098
|
}
|
|
1252
|
-
function scoreSeverity(
|
|
1253
|
-
if (!
|
|
1254
|
-
return SEVERITY_SCORES[
|
|
3099
|
+
function scoreSeverity(item2) {
|
|
3100
|
+
if (!item2.severity) return 0.5;
|
|
3101
|
+
return SEVERITY_SCORES[item2.severity] ?? 0.5;
|
|
1255
3102
|
}
|
|
1256
|
-
function extractKeywords(
|
|
3103
|
+
function extractKeywords(text3) {
|
|
1257
3104
|
const stopWords = /* @__PURE__ */ new Set([
|
|
1258
3105
|
"the",
|
|
1259
3106
|
"a",
|
|
@@ -1338,35 +3185,35 @@ function extractKeywords(text2) {
|
|
|
1338
3185
|
"implement",
|
|
1339
3186
|
"create"
|
|
1340
3187
|
]);
|
|
1341
|
-
return
|
|
3188
|
+
return text3.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
1342
3189
|
}
|
|
1343
3190
|
function scoreItems(items, taskDescription, filePaths) {
|
|
1344
3191
|
const languages = detectLanguages(filePaths);
|
|
1345
3192
|
const categories = detectCategories(filePaths);
|
|
1346
3193
|
const keywords = extractKeywords(taskDescription);
|
|
1347
|
-
return items.map((
|
|
1348
|
-
const pathScore = scorePathMatch(
|
|
1349
|
-
const keywordScore = scoreKeywordMatch(
|
|
1350
|
-
const recencyScore = scoreRecency(
|
|
1351
|
-
const severityScore = scoreSeverity(
|
|
3194
|
+
return items.map((item2) => {
|
|
3195
|
+
const pathScore = scorePathMatch(item2, languages, categories);
|
|
3196
|
+
const keywordScore = scoreKeywordMatch(item2, keywords);
|
|
3197
|
+
const recencyScore = scoreRecency(item2);
|
|
3198
|
+
const severityScore = scoreSeverity(item2);
|
|
1352
3199
|
const score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
1353
3200
|
return {
|
|
1354
|
-
type:
|
|
1355
|
-
id:
|
|
1356
|
-
title:
|
|
1357
|
-
content:
|
|
3201
|
+
type: item2.type,
|
|
3202
|
+
id: item2.id,
|
|
3203
|
+
title: item2.title,
|
|
3204
|
+
content: item2.content,
|
|
1358
3205
|
score,
|
|
1359
|
-
tokenEstimate: estimateTokens(
|
|
3206
|
+
tokenEstimate: estimateTokens(item2.content)
|
|
1360
3207
|
};
|
|
1361
3208
|
}).sort((a, b) => b.score - a.score);
|
|
1362
3209
|
}
|
|
1363
3210
|
function selectWithinBudget(items, maxTokens) {
|
|
1364
3211
|
const selected = [];
|
|
1365
3212
|
let tokensUsed = 0;
|
|
1366
|
-
for (const
|
|
1367
|
-
if (tokensUsed +
|
|
1368
|
-
selected.push(
|
|
1369
|
-
tokensUsed +=
|
|
3213
|
+
for (const item2 of items) {
|
|
3214
|
+
if (tokensUsed + item2.tokenEstimate > maxTokens) continue;
|
|
3215
|
+
selected.push(item2);
|
|
3216
|
+
tokensUsed += item2.tokenEstimate;
|
|
1370
3217
|
}
|
|
1371
3218
|
return { selected, tokensUsed, totalItems: items.length };
|
|
1372
3219
|
}
|
|
@@ -1391,20 +3238,20 @@ var init_scoring = __esm({
|
|
|
1391
3238
|
|
|
1392
3239
|
// src/lib/context-builder.ts
|
|
1393
3240
|
function formatGuardrail(g) {
|
|
1394
|
-
let
|
|
3241
|
+
let text3 = `## ${g.title} [${g.severity}]
|
|
1395
3242
|
|
|
1396
3243
|
${g.description}`;
|
|
1397
|
-
if (g.pattern)
|
|
3244
|
+
if (g.pattern) text3 += `
|
|
1398
3245
|
|
|
1399
3246
|
**Bad:** \`\`\`
|
|
1400
3247
|
${g.pattern}
|
|
1401
3248
|
\`\`\``;
|
|
1402
|
-
if (g.fix)
|
|
3249
|
+
if (g.fix) text3 += `
|
|
1403
3250
|
|
|
1404
3251
|
**Good:** \`\`\`
|
|
1405
3252
|
${g.fix}
|
|
1406
3253
|
\`\`\``;
|
|
1407
|
-
return
|
|
3254
|
+
return text3;
|
|
1408
3255
|
}
|
|
1409
3256
|
function formatSpecHeadline(s) {
|
|
1410
3257
|
return `### ${s.name}
|
|
@@ -1414,8 +3261,8 @@ function formatDocHeadline(d) {
|
|
|
1414
3261
|
return `### ${d.title}
|
|
1415
3262
|
\`doc_id: ${d.id}\` \xB7 \`${d.repo_full_name}/${d.path}\` \xB7 ${d.doc_type}`;
|
|
1416
3263
|
}
|
|
1417
|
-
function formatStack(
|
|
1418
|
-
return
|
|
3264
|
+
function formatStack(entries2) {
|
|
3265
|
+
return entries2.map(
|
|
1419
3266
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
1420
3267
|
).join("\n");
|
|
1421
3268
|
}
|
|
@@ -1426,19 +3273,19 @@ function formatRules(rules) {
|
|
|
1426
3273
|
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
1427
3274
|
byCategory.get(cat).push(r);
|
|
1428
3275
|
}
|
|
1429
|
-
let
|
|
3276
|
+
let text3 = "";
|
|
1430
3277
|
for (const [cat, catRules] of byCategory) {
|
|
1431
|
-
|
|
3278
|
+
text3 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
1432
3279
|
`;
|
|
1433
3280
|
for (const r of catRules) {
|
|
1434
|
-
|
|
3281
|
+
text3 += `- ${r.text}
|
|
1435
3282
|
`;
|
|
1436
3283
|
}
|
|
1437
|
-
|
|
3284
|
+
text3 += "\n";
|
|
1438
3285
|
}
|
|
1439
3286
|
return `## Project Rules (${rules.length})
|
|
1440
3287
|
|
|
1441
|
-
${
|
|
3288
|
+
${text3.trim()}`;
|
|
1442
3289
|
}
|
|
1443
3290
|
async function fetchSpecBody(api, draftId) {
|
|
1444
3291
|
const spec = await api.get(
|
|
@@ -1496,13 +3343,13 @@ function selectSection(outline, taskDescription) {
|
|
|
1496
3343
|
}
|
|
1497
3344
|
return best;
|
|
1498
3345
|
}
|
|
1499
|
-
async function hydrate(api,
|
|
3346
|
+
async function hydrate(api, item2, meta) {
|
|
1500
3347
|
try {
|
|
1501
3348
|
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id, meta.section?.anchor);
|
|
1502
|
-
return { item, meta, body };
|
|
3349
|
+
return { item: item2, meta, body };
|
|
1503
3350
|
} catch (err) {
|
|
1504
3351
|
return {
|
|
1505
|
-
item,
|
|
3352
|
+
item: item2,
|
|
1506
3353
|
meta,
|
|
1507
3354
|
error: err instanceof Error ? err.message : String(err)
|
|
1508
3355
|
};
|
|
@@ -1609,13 +3456,13 @@ ${formatStack(stack)}`;
|
|
|
1609
3456
|
0,
|
|
1610
3457
|
Math.max(0, Math.min(MAX_HYDRATED, affordable))
|
|
1611
3458
|
);
|
|
1612
|
-
for (const
|
|
1613
|
-
remaining -=
|
|
3459
|
+
for (const item2 of shortlist) {
|
|
3460
|
+
remaining -= item2.tokenEstimate;
|
|
1614
3461
|
}
|
|
1615
3462
|
remaining = Math.max(0, remaining);
|
|
1616
3463
|
const hydrated = await Promise.all(
|
|
1617
3464
|
shortlist.map(
|
|
1618
|
-
(
|
|
3465
|
+
(item2) => hydrate(api, item2, refs.get(key(item2.type, item2.id)))
|
|
1619
3466
|
)
|
|
1620
3467
|
);
|
|
1621
3468
|
const tailCandidates = rankedRefs.slice(shortlist.length, shortlist.length + MAX_TAIL).map((i) => refs.get(key(i.type, i.id)));
|
|
@@ -1792,13 +3639,13 @@ function registerGetContext(server2, apiClient2) {
|
|
|
1792
3639
|
}
|
|
1793
3640
|
},
|
|
1794
3641
|
async ({ task_description, file_paths, max_tokens }) => {
|
|
1795
|
-
const { text:
|
|
3642
|
+
const { text: text3 } = await buildContext(apiClient2, {
|
|
1796
3643
|
taskDescription: task_description,
|
|
1797
3644
|
filePaths: file_paths,
|
|
1798
3645
|
maxTokens: max_tokens
|
|
1799
3646
|
});
|
|
1800
3647
|
return {
|
|
1801
|
-
content: [{ type: "text", text:
|
|
3648
|
+
content: [{ type: "text", text: text3 }]
|
|
1802
3649
|
};
|
|
1803
3650
|
}
|
|
1804
3651
|
);
|
|
@@ -2061,9 +3908,9 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
2061
3908
|
);
|
|
2062
3909
|
}
|
|
2063
3910
|
if (filtered.length === 0) {
|
|
2064
|
-
const
|
|
3911
|
+
const text4 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient2))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
|
|
2065
3912
|
return {
|
|
2066
|
-
content: [{ type: "text", text:
|
|
3913
|
+
content: [{ type: "text", text: text4 }]
|
|
2067
3914
|
};
|
|
2068
3915
|
}
|
|
2069
3916
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -2072,25 +3919,25 @@ function registerGetProjectRules(server2, apiClient2) {
|
|
|
2072
3919
|
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
2073
3920
|
grouped.get(cat).push(rule);
|
|
2074
3921
|
}
|
|
2075
|
-
let
|
|
3922
|
+
let text3 = `Found ${filtered.length} project rule(s):
|
|
2076
3923
|
|
|
2077
3924
|
`;
|
|
2078
3925
|
for (const [cat, catRules] of grouped) {
|
|
2079
3926
|
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
2080
|
-
|
|
3927
|
+
text3 += `## ${label}
|
|
2081
3928
|
|
|
2082
3929
|
`;
|
|
2083
3930
|
for (const rule of catRules) {
|
|
2084
|
-
|
|
3931
|
+
text3 += `- ${rule.text}
|
|
2085
3932
|
`;
|
|
2086
3933
|
}
|
|
2087
|
-
|
|
3934
|
+
text3 += "\n";
|
|
2088
3935
|
}
|
|
2089
3936
|
return {
|
|
2090
3937
|
content: [
|
|
2091
3938
|
{
|
|
2092
3939
|
type: "text",
|
|
2093
|
-
text:
|
|
3940
|
+
text: text3
|
|
2094
3941
|
}
|
|
2095
3942
|
]
|
|
2096
3943
|
};
|
|
@@ -2131,10 +3978,10 @@ function registerCreateProjectRule(server2, apiClient2) {
|
|
|
2131
3978
|
readOnlyHint: false
|
|
2132
3979
|
}
|
|
2133
3980
|
},
|
|
2134
|
-
async ({ text:
|
|
3981
|
+
async ({ text: text3, category }) => {
|
|
2135
3982
|
const saved = await apiClient2.post(
|
|
2136
3983
|
"/v1/standards/rules",
|
|
2137
|
-
{ text:
|
|
3984
|
+
{ text: text3, category }
|
|
2138
3985
|
);
|
|
2139
3986
|
apiClient2.invalidateCache("/v1/standards/rules");
|
|
2140
3987
|
return {
|
|
@@ -2177,7 +4024,7 @@ function registerGetPromptTemplates(server2, apiClient2) {
|
|
|
2177
4024
|
]
|
|
2178
4025
|
};
|
|
2179
4026
|
}
|
|
2180
|
-
const
|
|
4027
|
+
const text3 = templates.map(
|
|
2181
4028
|
(t) => `## ${t.name}
|
|
2182
4029
|
|
|
2183
4030
|
` + (t.description ? `*${t.description}*
|
|
@@ -2192,7 +4039,7 @@ ${t.prompt}
|
|
|
2192
4039
|
type: "text",
|
|
2193
4040
|
text: `Found ${templates.length} prompt template(s):
|
|
2194
4041
|
|
|
2195
|
-
${
|
|
4042
|
+
${text3}`
|
|
2196
4043
|
}
|
|
2197
4044
|
]
|
|
2198
4045
|
};
|
|
@@ -2252,10 +4099,10 @@ var init_create_prompt_template = __esm({
|
|
|
2252
4099
|
|
|
2253
4100
|
// src/tools/skills.ts
|
|
2254
4101
|
import { z as z13 } from "zod";
|
|
2255
|
-
import { homedir as
|
|
2256
|
-
import { basename as basename2, resolve as
|
|
4102
|
+
import { homedir as homedir3 } from "os";
|
|
4103
|
+
import { basename as basename2, resolve as resolve6 } from "path";
|
|
2257
4104
|
function expandHome(path) {
|
|
2258
|
-
return path.replace(/^~(?=$|[\\/])/,
|
|
4105
|
+
return path.replace(/^~(?=$|[\\/])/, homedir3());
|
|
2259
4106
|
}
|
|
2260
4107
|
function text(t) {
|
|
2261
4108
|
return { content: [{ type: "text", text: t }] };
|
|
@@ -2357,7 +4204,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2357
4204
|
);
|
|
2358
4205
|
if (localFiles) {
|
|
2359
4206
|
const dest2 = args2.target ? expandHome(args2.target) : defaultSkillDir(version.name);
|
|
2360
|
-
if (basename2(
|
|
4207
|
+
if (basename2(resolve6(dest2)) !== version.name) {
|
|
2361
4208
|
throw new Error(`target must be a folder named ${version.name} (e.g. .claude/skills/${version.name}); got ${dest2}`);
|
|
2362
4209
|
}
|
|
2363
4210
|
const result = await installSkill(version, dest2, { overwrite: args2.overwrite });
|
|
@@ -2401,6 +4248,205 @@ var init_skills2 = __esm({
|
|
|
2401
4248
|
}
|
|
2402
4249
|
});
|
|
2403
4250
|
|
|
4251
|
+
// src/lib/team-setup-types.ts
|
|
4252
|
+
var PARTS, PERSON_ID_RE;
|
|
4253
|
+
var init_team_setup_types = __esm({
|
|
4254
|
+
"src/lib/team-setup-types.ts"() {
|
|
4255
|
+
"use strict";
|
|
4256
|
+
init_person_values();
|
|
4257
|
+
PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
4258
|
+
PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
4259
|
+
}
|
|
4260
|
+
});
|
|
4261
|
+
|
|
4262
|
+
// src/tools/team-setup.ts
|
|
4263
|
+
import { z as z14 } from "zod";
|
|
4264
|
+
import { existsSync as existsSync5 } from "fs";
|
|
4265
|
+
import { lstat as lstat2 } from "fs/promises";
|
|
4266
|
+
import { homedir as homedir4 } from "os";
|
|
4267
|
+
import { join as join6, resolve as resolve7 } from "path";
|
|
4268
|
+
function expandHome2(path) {
|
|
4269
|
+
return path.replace(/^~(?=$|[\\/])/, homedir4());
|
|
4270
|
+
}
|
|
4271
|
+
function text2(t) {
|
|
4272
|
+
return { content: [{ type: "text", text: t }] };
|
|
4273
|
+
}
|
|
4274
|
+
async function isLink(path) {
|
|
4275
|
+
try {
|
|
4276
|
+
return (await lstat2(path)).isSymbolicLink();
|
|
4277
|
+
} catch (e) {
|
|
4278
|
+
if (e.code === "ENOENT") return false;
|
|
4279
|
+
throw e;
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
async function readPartDirectory(part, directory) {
|
|
4283
|
+
const root = resolve7(directory);
|
|
4284
|
+
const layout = LAYOUT[part];
|
|
4285
|
+
const paths = [];
|
|
4286
|
+
const leftOut = [];
|
|
4287
|
+
const links = [];
|
|
4288
|
+
for (const path of layout.files) {
|
|
4289
|
+
const segments = path.split("/");
|
|
4290
|
+
let present = true;
|
|
4291
|
+
for (let i = 1; i <= segments.length; i++) {
|
|
4292
|
+
const rel = segments.slice(0, i).join("/");
|
|
4293
|
+
if (await isLink(join6(root, ...segments.slice(0, i)))) {
|
|
4294
|
+
links.push(rel);
|
|
4295
|
+
present = false;
|
|
4296
|
+
break;
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
if (present && existsSync5(join6(root, ...segments))) paths.push(path);
|
|
4300
|
+
}
|
|
4301
|
+
for (const { dir, md } of layout.folders) {
|
|
4302
|
+
const full = join6(root, dir);
|
|
4303
|
+
if (await isLink(full)) {
|
|
4304
|
+
links.push(dir);
|
|
4305
|
+
continue;
|
|
4306
|
+
}
|
|
4307
|
+
if (!existsSync5(full)) continue;
|
|
4308
|
+
const walked = await walk(full, { skipHidden: true });
|
|
4309
|
+
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
4310
|
+
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
4311
|
+
for (const p of walked.files) {
|
|
4312
|
+
if (md && !p.endsWith(".md")) leftOut.push(`${dir}/${p}`);
|
|
4313
|
+
else paths.push(`${dir}/${p}`);
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
if (links.length) {
|
|
4317
|
+
throw new Error(
|
|
4318
|
+
`${links.join(", ")}: links are not followed; copy the file into the folder. Nothing was sent.`
|
|
4319
|
+
);
|
|
4320
|
+
}
|
|
4321
|
+
if (layout.required && !paths.includes(layout.required)) {
|
|
4322
|
+
throw new Error(`${root} has no ${layout.required}: the ${part} part needs it. Nothing was sent.`);
|
|
4323
|
+
}
|
|
4324
|
+
if (!paths.length) {
|
|
4325
|
+
throw new Error(
|
|
4326
|
+
`${root} has no ${layout.folders.map((f) => `${f.dir}/*.md`).join(" or ")}: nothing to publish for ${part}.`
|
|
4327
|
+
);
|
|
4328
|
+
}
|
|
4329
|
+
return { files: await readTextFiles(root, paths.sort(), `the ${part} part`), leftOut };
|
|
4330
|
+
}
|
|
4331
|
+
function describePerson(id, person) {
|
|
4332
|
+
const lines = [`**${person.label ?? "(no label)"}**, \`${id}\``];
|
|
4333
|
+
if (person.mergedInto) {
|
|
4334
|
+
lines.push(`\`${id}\` was merged into \`${person.mergedInto}\`: these are \`${person.mergedInto}\`'s details.`);
|
|
4335
|
+
}
|
|
4336
|
+
lines.push(`- Name: ${person.name ?? "(none held)"}`);
|
|
4337
|
+
lines.push(`- Phone number: ${person.phone ?? "(none held)"}`);
|
|
4338
|
+
lines.push(`- Email address: ${person.email ?? "(none held)"}`);
|
|
4339
|
+
const token = `\`${person.mergedInto ?? id}\`${person.label ? ` (${person.label})` : ""}`;
|
|
4340
|
+
lines.push(
|
|
4341
|
+
"",
|
|
4342
|
+
`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.`
|
|
4343
|
+
);
|
|
4344
|
+
return lines.join("\n");
|
|
4345
|
+
}
|
|
4346
|
+
function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
4347
|
+
const fileSchema = z14.object({
|
|
4348
|
+
path: z14.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
|
|
4349
|
+
content: z14.string().describe("The file's full text, exactly as it is")
|
|
4350
|
+
});
|
|
4351
|
+
server2.registerTool(
|
|
4352
|
+
"publish_setup_part",
|
|
4353
|
+
{
|
|
4354
|
+
title: "Publish Setup Part",
|
|
4355
|
+
description: "Publish one part of your team's Claude Code setup to SystemDox: `hooks` (hooks/hooks.json and the scripts/ it calls), `mcp` (.mcp.json), `agents` (agents/*.md), `commands` (commands/*.md) or `instructions` (CLAUDE.md). Every part but instructions makes a new version of the team plugin, which each member accepts at their next update. Publishing hooks, mcp, agents or commands needs the admin role. " + (localFiles ? "Pass `directory`: the server reads only the files the part accepts, byte for byte, and refuses a link, a binary file or a file too large before anything is sent. Never retype the files into `files` when the folder is on this computer." : "Pass every file of the part in `files`, exactly as it is."),
|
|
4356
|
+
inputSchema: {
|
|
4357
|
+
part: z14.enum(PARTS).describe("Which part of the setup"),
|
|
4358
|
+
...localFiles ? {
|
|
4359
|
+
directory: z14.string().optional().describe(
|
|
4360
|
+
"The folder on this computer: for hooks, the one holding hooks/hooks.json and scripts/; for mcp, the one holding .mcp.json; for instructions, the one holding CLAUDE.md; for agents or commands, the one holding agents/ or commands/"
|
|
4361
|
+
)
|
|
4362
|
+
} : {},
|
|
4363
|
+
files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
|
|
4364
|
+
},
|
|
4365
|
+
annotations: { readOnlyHint: false }
|
|
4366
|
+
},
|
|
4367
|
+
async (args2) => {
|
|
4368
|
+
let files;
|
|
4369
|
+
let leftOut = [];
|
|
4370
|
+
if (localFiles && args2.directory) {
|
|
4371
|
+
({ files, leftOut } = await readPartDirectory(args2.part, expandHome2(args2.directory)));
|
|
4372
|
+
} else if (args2.files?.length) {
|
|
4373
|
+
files = args2.files;
|
|
4374
|
+
} else {
|
|
4375
|
+
throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
|
|
4376
|
+
}
|
|
4377
|
+
const { workspace } = await apiClient2.getOwnTeamSetup();
|
|
4378
|
+
const saved = await apiClient2.publishPartVersion(workspace, args2.part, files);
|
|
4379
|
+
const lines = [`Published **${saved.part}** version ${saved.version}.`];
|
|
4380
|
+
if (saved.part !== "instructions" && saved.pluginVersion !== void 0) {
|
|
4381
|
+
lines.push(`Team plugin version ${saved.pluginVersion}; each member accepts it at their next update.`);
|
|
4382
|
+
}
|
|
4383
|
+
if (saved.replacedValues?.length) {
|
|
4384
|
+
lines.push(
|
|
4385
|
+
"Replaced by a reference to each member's own environment variable:",
|
|
4386
|
+
...saved.replacedValues.map((r) => `- ${r.server} ${r.field} ${r.name}: \${${r.name}}`)
|
|
4387
|
+
);
|
|
4388
|
+
}
|
|
4389
|
+
if (leftOut.length) lines.push(`Left out (not part of ${args2.part}): ${leftOut.join(", ")}`);
|
|
4390
|
+
return text2(lines.join("\n"));
|
|
4391
|
+
}
|
|
4392
|
+
);
|
|
4393
|
+
server2.registerTool(
|
|
4394
|
+
"resolve_person",
|
|
4395
|
+
{
|
|
4396
|
+
title: "Resolve Person",
|
|
4397
|
+
description: "Read back a person that a team memory names by identifier, written as `con_\u2026` (label), `ctc_\u2026` or `usr_\u2026`: their label, name, phone number and email address, from the vault that holds them. The details are for this session only: never write them into a memory file; the memory keeps the identifier. Writes nothing.",
|
|
4398
|
+
inputSchema: {
|
|
4399
|
+
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")
|
|
4400
|
+
},
|
|
4401
|
+
annotations: { readOnlyHint: true }
|
|
4402
|
+
},
|
|
4403
|
+
async (args2) => {
|
|
4404
|
+
const { workspace } = await apiClient2.getOwnTeamSetup();
|
|
4405
|
+
const person = await apiClient2.resolvePerson(workspace, args2.id);
|
|
4406
|
+
return text2(describePerson(args2.id, person));
|
|
4407
|
+
}
|
|
4408
|
+
);
|
|
4409
|
+
if (!localFiles) return;
|
|
4410
|
+
server2.registerTool(
|
|
4411
|
+
"sync_memory",
|
|
4412
|
+
{
|
|
4413
|
+
title: "Sync Memory",
|
|
4414
|
+
description: `Sync one project's team memory on this computer with SystemDox: pull the files the team changed, push the ones changed here, keep both texts of a file changed on both sides. It never answers for a person: a file that still holds a phone number, an email address or a home-folder path is skipped and listed, with the command for the member to run in their own terminal (\`${SYNC_MEMORY_COMMAND}\`). Pass dryRun to see what it would do without writing or sending anything.`,
|
|
4415
|
+
inputSchema: {
|
|
4416
|
+
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"),
|
|
4417
|
+
dryRun: z14.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
|
|
4418
|
+
},
|
|
4419
|
+
annotations: { readOnlyHint: false }
|
|
4420
|
+
},
|
|
4421
|
+
async (args2) => {
|
|
4422
|
+
const lines = [];
|
|
4423
|
+
const code = await syncMemory({
|
|
4424
|
+
project: args2.project,
|
|
4425
|
+
dryRun: !!args2.dryRun,
|
|
4426
|
+
client: apiClient2,
|
|
4427
|
+
out: (...l) => lines.push(...l)
|
|
4428
|
+
});
|
|
4429
|
+
return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
|
|
4430
|
+
}
|
|
4431
|
+
);
|
|
4432
|
+
}
|
|
4433
|
+
var LAYOUT;
|
|
4434
|
+
var init_team_setup = __esm({
|
|
4435
|
+
"src/tools/team-setup.ts"() {
|
|
4436
|
+
"use strict";
|
|
4437
|
+
init_skills();
|
|
4438
|
+
init_team_setup_types();
|
|
4439
|
+
init_sync_memory();
|
|
4440
|
+
LAYOUT = {
|
|
4441
|
+
hooks: { files: ["hooks/hooks.json"], folders: [{ dir: "scripts", md: false }], required: "hooks/hooks.json" },
|
|
4442
|
+
mcp: { files: [".mcp.json"], folders: [], required: ".mcp.json" },
|
|
4443
|
+
agents: { files: [], folders: [{ dir: "agents", md: true }] },
|
|
4444
|
+
commands: { files: [], folders: [{ dir: "commands", md: true }] },
|
|
4445
|
+
instructions: { files: ["CLAUDE.md"], folders: [], required: "CLAUDE.md" }
|
|
4446
|
+
};
|
|
4447
|
+
}
|
|
4448
|
+
});
|
|
4449
|
+
|
|
2404
4450
|
// src/server.ts
|
|
2405
4451
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2406
4452
|
function createServer(apiClient2, { localFiles = false } = {}) {
|
|
@@ -2423,6 +4469,7 @@ function createServer(apiClient2, { localFiles = false } = {}) {
|
|
|
2423
4469
|
registerCreateProjectRule(server2, apiClient2);
|
|
2424
4470
|
registerCreatePromptTemplate(server2, apiClient2);
|
|
2425
4471
|
registerSkillTools(server2, apiClient2, { localFiles });
|
|
4472
|
+
registerTeamSetupTools(server2, apiClient2, { localFiles });
|
|
2426
4473
|
return server2;
|
|
2427
4474
|
}
|
|
2428
4475
|
var SERVER_VERSION;
|
|
@@ -2444,7 +4491,8 @@ var init_server = __esm({
|
|
|
2444
4491
|
init_get_prompt_templates();
|
|
2445
4492
|
init_create_prompt_template();
|
|
2446
4493
|
init_skills2();
|
|
2447
|
-
|
|
4494
|
+
init_team_setup();
|
|
4495
|
+
SERVER_VERSION = true ? "0.1.47" : "0.0.0-dev";
|
|
2448
4496
|
}
|
|
2449
4497
|
});
|
|
2450
4498
|
|
|
@@ -2478,17 +4526,18 @@ var init_index = __esm({
|
|
|
2478
4526
|
|
|
2479
4527
|
// src/cli.ts
|
|
2480
4528
|
init_config();
|
|
2481
|
-
|
|
2482
|
-
import {
|
|
2483
|
-
import {
|
|
2484
|
-
import {
|
|
4529
|
+
init_team_setup_files();
|
|
4530
|
+
import { readFile as readFile5, writeFile as writeFile6, chmod as chmod2 } from "fs/promises";
|
|
4531
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4532
|
+
import { homedir as homedir5 } from "os";
|
|
4533
|
+
import { join as join7 } from "path";
|
|
2485
4534
|
import { createInterface } from "readline";
|
|
2486
4535
|
function prompt(question) {
|
|
2487
4536
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2488
|
-
return new Promise((
|
|
4537
|
+
return new Promise((resolve8) => {
|
|
2489
4538
|
rl.question(question, (answer) => {
|
|
2490
4539
|
rl.close();
|
|
2491
|
-
|
|
4540
|
+
resolve8(answer.trim());
|
|
2492
4541
|
});
|
|
2493
4542
|
});
|
|
2494
4543
|
}
|
|
@@ -2508,45 +4557,45 @@ async function validateApiKey(apiKey, apiUrl) {
|
|
|
2508
4557
|
}
|
|
2509
4558
|
function findConfigTargets() {
|
|
2510
4559
|
const targets = [];
|
|
2511
|
-
const projectMcp =
|
|
4560
|
+
const projectMcp = join7(process.cwd(), ".mcp.json");
|
|
2512
4561
|
targets.push({
|
|
2513
4562
|
name: "Claude Code (project)",
|
|
2514
4563
|
path: projectMcp,
|
|
2515
4564
|
detected: true
|
|
2516
4565
|
// Always available
|
|
2517
4566
|
});
|
|
2518
|
-
const claudeUser =
|
|
4567
|
+
const claudeUser = join7(homedir5(), ".claude.json");
|
|
2519
4568
|
targets.push({
|
|
2520
4569
|
name: "Claude Code (user \u2014 all projects)",
|
|
2521
4570
|
path: claudeUser,
|
|
2522
4571
|
detected: true
|
|
2523
4572
|
// Always available
|
|
2524
4573
|
});
|
|
2525
|
-
const cursorProject =
|
|
2526
|
-
const cursorDir =
|
|
4574
|
+
const cursorProject = join7(process.cwd(), ".cursor", "mcp.json");
|
|
4575
|
+
const cursorDir = join7(process.cwd(), ".cursor");
|
|
2527
4576
|
targets.push({
|
|
2528
4577
|
name: "Cursor (project)",
|
|
2529
4578
|
path: cursorProject,
|
|
2530
|
-
detected:
|
|
4579
|
+
detected: existsSync6(cursorDir)
|
|
2531
4580
|
});
|
|
2532
|
-
const vscodeProject =
|
|
2533
|
-
const vscodeDir =
|
|
4581
|
+
const vscodeProject = join7(process.cwd(), ".vscode", "mcp.json");
|
|
4582
|
+
const vscodeDir = join7(process.cwd(), ".vscode");
|
|
2534
4583
|
targets.push({
|
|
2535
4584
|
name: "VS Code (project)",
|
|
2536
4585
|
path: vscodeProject,
|
|
2537
|
-
detected:
|
|
4586
|
+
detected: existsSync6(vscodeDir)
|
|
2538
4587
|
});
|
|
2539
|
-
const cursorGlobal =
|
|
4588
|
+
const cursorGlobal = join7(homedir5(), ".cursor", "mcp.json");
|
|
2540
4589
|
targets.push({
|
|
2541
4590
|
name: "Cursor (global)",
|
|
2542
4591
|
path: cursorGlobal,
|
|
2543
|
-
detected:
|
|
4592
|
+
detected: existsSync6(join7(homedir5(), ".cursor"))
|
|
2544
4593
|
});
|
|
2545
4594
|
return targets;
|
|
2546
4595
|
}
|
|
2547
4596
|
async function readOrCreateConfig(path) {
|
|
2548
4597
|
try {
|
|
2549
|
-
const content = await
|
|
4598
|
+
const content = await readFile5(path, "utf-8");
|
|
2550
4599
|
return JSON.parse(content);
|
|
2551
4600
|
} catch {
|
|
2552
4601
|
return { mcpServers: {} };
|
|
@@ -2554,13 +4603,13 @@ async function readOrCreateConfig(path) {
|
|
|
2554
4603
|
}
|
|
2555
4604
|
async function writeConfig(path, config2) {
|
|
2556
4605
|
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
2557
|
-
if (!
|
|
2558
|
-
const { mkdir:
|
|
2559
|
-
await
|
|
4606
|
+
if (!existsSync6(dir)) {
|
|
4607
|
+
const { mkdir: mkdir6 } = await import("fs/promises");
|
|
4608
|
+
await mkdir6(dir, { recursive: true });
|
|
2560
4609
|
}
|
|
2561
|
-
await
|
|
4610
|
+
await writeFile6(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
|
|
2562
4611
|
try {
|
|
2563
|
-
await
|
|
4612
|
+
await chmod2(path, 384);
|
|
2564
4613
|
} catch {
|
|
2565
4614
|
}
|
|
2566
4615
|
}
|
|
@@ -2636,13 +4685,30 @@ async function setup() {
|
|
|
2636
4685
|
console.log(` Configured: ${target.name}`);
|
|
2637
4686
|
}
|
|
2638
4687
|
console.log(
|
|
2639
|
-
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides
|
|
4688
|
+
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides 20 tools: get_context, get_checks,\n get_project_rules, get_prompt_templates, get_specs, get_docs,\n get_tech_stack, get_planning_context, get_plan_board, create_check,\n create_project_rule, create_prompt_template, update_spec,\n report_decision, publish_skill, list_skills, install_skill,\n publish_setup_part, resolve_person, sync_memory.\n\n get_guardrails and create_guardrail remain registered as deprecated\n aliases of get_checks and create_check \u2014 existing configs keep working.\n"
|
|
2640
4689
|
);
|
|
4690
|
+
return { apiKey, apiUrl };
|
|
2641
4691
|
}
|
|
2642
4692
|
var args = process.argv.slice(2);
|
|
2643
4693
|
var command = args[0];
|
|
2644
4694
|
if (command === "setup") {
|
|
2645
|
-
|
|
4695
|
+
Promise.resolve().then(() => (init_setup_team(), setup_team_exports)).then(async ({ parseSetupArgs: parseSetupArgs2, runTeamSetupCommand: runTeamSetupCommand2 }) => {
|
|
4696
|
+
let teamArgs;
|
|
4697
|
+
try {
|
|
4698
|
+
teamArgs = parseSetupArgs2(args.slice(1));
|
|
4699
|
+
} catch (err) {
|
|
4700
|
+
console.error(`
|
|
4701
|
+
Error: ${err.message}
|
|
4702
|
+
`);
|
|
4703
|
+
process.exit(1);
|
|
4704
|
+
}
|
|
4705
|
+
const key = await setup();
|
|
4706
|
+
return runTeamSetupCommand2(teamArgs, key, {
|
|
4707
|
+
out: console.log,
|
|
4708
|
+
ask: (question) => prompt(question),
|
|
4709
|
+
terminal: !!process.stdin.isTTY
|
|
4710
|
+
});
|
|
4711
|
+
}).then((code) => process.exit(code)).catch((err) => {
|
|
2646
4712
|
console.error(`
|
|
2647
4713
|
Error: ${err.message}
|
|
2648
4714
|
`);
|
|
@@ -2652,20 +4718,54 @@ if (command === "setup") {
|
|
|
2652
4718
|
Promise.resolve().then(() => (init_install_skill(), install_skill_exports)).then(
|
|
2653
4719
|
({ runInstallSkill: runInstallSkill2 }) => runInstallSkill2(args.slice(1), { out: console.log, err: console.error })
|
|
2654
4720
|
).then((code) => process.exit(code));
|
|
4721
|
+
} else if (command === "marketplace-auth") {
|
|
4722
|
+
runMarketplaceAuth({ out: console.log, err: console.error }).then((code) => process.exit(code));
|
|
4723
|
+
} else if (command === "plugin-path") {
|
|
4724
|
+
Promise.resolve().then(() => (init_plugin_path(), plugin_path_exports)).then(
|
|
4725
|
+
({ runPluginPath: runPluginPath2 }) => runPluginPath2(args.slice(1), { out: console.log, err: console.error })
|
|
4726
|
+
).then((code) => process.exit(code));
|
|
4727
|
+
} else if (command === "sync-memory") {
|
|
4728
|
+
const terminal = !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
4729
|
+
Promise.resolve().then(() => (init_sync_memory(), sync_memory_exports)).then(
|
|
4730
|
+
({ runSyncMemory: runSyncMemory2 }) => runSyncMemory2(args.slice(1), {
|
|
4731
|
+
out: console.log,
|
|
4732
|
+
err: console.error,
|
|
4733
|
+
ask: terminal ? (question) => prompt(question) : void 0
|
|
4734
|
+
})
|
|
4735
|
+
).then((code) => process.exit(code));
|
|
2655
4736
|
} else if (command === "--help" || command === "-h") {
|
|
2656
4737
|
console.log(`
|
|
2657
4738
|
SystemDox MCP Server
|
|
2658
4739
|
|
|
2659
4740
|
Usage:
|
|
2660
4741
|
systemdox-mcp-server Start the MCP server (stdio transport)
|
|
2661
|
-
systemdox-mcp-server setup
|
|
4742
|
+
systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]
|
|
4743
|
+
Configure the MCP server in your AI coding tool,
|
|
4744
|
+
then your team's setup: the team instructions,
|
|
4745
|
+
the plugin's marketplace and the project's team
|
|
4746
|
+
memory (shown first; nothing changes until you
|
|
4747
|
+
answer y)
|
|
2662
4748
|
systemdox-mcp-server install-skill <name> [--version n] [--target dir] [--overwrite]
|
|
2663
4749
|
Install a skill published to SystemDox
|
|
2664
4750
|
(default folder: ~/.claude/skills/<name>)
|
|
4751
|
+
systemdox-mcp-server plugin-path <workspace> --version <n> --sha256 <hash>
|
|
4752
|
+
Print the folder of your team's plugin version,
|
|
4753
|
+
downloading and checking it first when needed
|
|
4754
|
+
(Claude Code runs it)
|
|
4755
|
+
systemdox-mcp-server marketplace-auth
|
|
4756
|
+
Print the header for your team's marketplace
|
|
4757
|
+
(Claude Code runs it)
|
|
4758
|
+
systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>]
|
|
4759
|
+
[--import <folder>] [--dry-run]
|
|
4760
|
+
Sync a project's team memory with SystemDox
|
|
4761
|
+
(default folder:
|
|
4762
|
+
~/.claude/systemdox/<workspace>/memory/<project>)
|
|
2665
4763
|
systemdox-mcp-server --help Show this help message
|
|
2666
4764
|
|
|
2667
4765
|
Environment variables:
|
|
2668
|
-
SYSTEMDOX_API_KEY Your SystemDox API key (required
|
|
4766
|
+
SYSTEMDOX_API_KEY Your SystemDox API key (required; plugin-path and
|
|
4767
|
+
marketplace-auth also read
|
|
4768
|
+
~/.claude/systemdox/credentials.json)
|
|
2669
4769
|
SYSTEMDOX_API_URL API URL (default: ${API_URL_DEFAULT})
|
|
2670
4770
|
SYSTEMDOX_CACHE_TTL Cache TTL in seconds (default: 300)
|
|
2671
4771
|
SYSTEMDOX_DEFAULT_ORG Default GitHub org
|