@systemdox/mcp-server 0.1.57 → 0.1.58
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 +37 -9
- package/dist/cli.js +454 -69
- package/dist/http.js +155 -38
- package/dist/index.js +155 -38
- package/dist/lib/memory-sync.d.ts +10 -3
- package/dist/lib/memory-sync.js +30 -4
- package/dist/lib/team-setup-files.d.ts +8 -1
- package/dist/lib/team-setup-files.js +8 -0
- package/dist/lib/team-setup-types.d.ts +2 -0
- package/dist/server.js +154 -37
- package/dist/setup-mcp.js +10 -9
- package/dist/setup-team.d.ts +38 -5
- package/dist/setup-team.js +228 -44
- package/dist/sync-memory.d.ts +61 -9
- package/dist/sync-memory.js +343 -34
- package/dist/tools/team-setup.js +147 -30
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -604,8 +604,8 @@ function registerGetPlanBoard(server, apiClient) {
|
|
|
604
604
|
]
|
|
605
605
|
};
|
|
606
606
|
}
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
607
|
+
const open2 = boards.filter((b) => !b.closed);
|
|
608
|
+
if (open2.length === 0) {
|
|
609
609
|
return {
|
|
610
610
|
content: [
|
|
611
611
|
{
|
|
@@ -615,16 +615,16 @@ function registerGetPlanBoard(server, apiClient) {
|
|
|
615
615
|
]
|
|
616
616
|
};
|
|
617
617
|
}
|
|
618
|
-
const closedNote = boards.length >
|
|
618
|
+
const closedNote = boards.length > open2.length ? `
|
|
619
619
|
|
|
620
|
-
_${boards.length -
|
|
620
|
+
_${boards.length - open2.length} closed board(s) not listed._` : "";
|
|
621
621
|
return {
|
|
622
622
|
content: [
|
|
623
623
|
{
|
|
624
624
|
type: "text",
|
|
625
|
-
text: `${
|
|
625
|
+
text: `${open2.length} open project board(s):
|
|
626
626
|
|
|
627
|
-
${formatBoards(
|
|
627
|
+
${formatBoards(open2)}` + closedNote + `
|
|
628
628
|
|
|
629
629
|
Call get_plan_board with project_number to list a board's issues.`
|
|
630
630
|
}
|
|
@@ -2707,7 +2707,7 @@ var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
|
2707
2707
|
|
|
2708
2708
|
// src/sync-memory.ts
|
|
2709
2709
|
import { existsSync as existsSync3 } from "fs";
|
|
2710
|
-
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
2710
|
+
import { appendFile, mkdir as mkdir3, open, readdir as readdir2, readFile as readFile4, rename as rename2, rm as rm3, stat, writeFile as writeFile3 } from "fs/promises";
|
|
2711
2711
|
import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
|
|
2712
2712
|
|
|
2713
2713
|
// src/lib/memory-sync.ts
|
|
@@ -2731,15 +2731,41 @@ function readFrontmatter(text3) {
|
|
|
2731
2731
|
const m = BLOCK_RE.exec(text3);
|
|
2732
2732
|
if (!m) return void 0;
|
|
2733
2733
|
const front = { shareFalse: false };
|
|
2734
|
-
|
|
2735
|
-
|
|
2734
|
+
const lines = m[1].split(/\r?\n/);
|
|
2735
|
+
if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
|
|
2736
|
+
let topType;
|
|
2737
|
+
let topShare;
|
|
2738
|
+
let nestedType;
|
|
2739
|
+
let nestedShare;
|
|
2740
|
+
let inMetadata = false;
|
|
2741
|
+
let nestedIndent;
|
|
2742
|
+
for (const line of lines) {
|
|
2743
|
+
if (!line.trim() || /^[ \t]*#/.test(line)) continue;
|
|
2744
|
+
const indent = /^ */.exec(line)[0].length;
|
|
2745
|
+
if (indent === 0) {
|
|
2746
|
+
inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
|
|
2747
|
+
nestedIndent = void 0;
|
|
2748
|
+
}
|
|
2749
|
+
const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
2736
2750
|
if (!kv) continue;
|
|
2737
2751
|
const raw = kv[2].replace(/[ \t]+#.*$/, "");
|
|
2738
2752
|
const quoted = /^(["'])(.*)\1$/.exec(raw);
|
|
2739
2753
|
const value = quoted ? quoted[2] : raw;
|
|
2740
|
-
|
|
2741
|
-
if (
|
|
2754
|
+
const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
|
|
2755
|
+
if (indent === 0) {
|
|
2756
|
+
if (kv[1] === "type") topType = value.trim().toLowerCase();
|
|
2757
|
+
if (kv[1] === "share") topShare = isFalse;
|
|
2758
|
+
continue;
|
|
2759
|
+
}
|
|
2760
|
+
if (!inMetadata) continue;
|
|
2761
|
+
nestedIndent ??= indent;
|
|
2762
|
+
if (indent !== nestedIndent) continue;
|
|
2763
|
+
if (kv[1] === "type") nestedType = value.trim().toLowerCase();
|
|
2764
|
+
if (kv[1] === "share") nestedShare = isFalse;
|
|
2742
2765
|
}
|
|
2766
|
+
const type = topType ?? nestedType;
|
|
2767
|
+
if (type !== void 0) front.type = type;
|
|
2768
|
+
front.shareFalse = (topShare ?? nestedShare) === true;
|
|
2743
2769
|
return front;
|
|
2744
2770
|
}
|
|
2745
2771
|
function isShareable(path, text3) {
|
|
@@ -2980,18 +3006,50 @@ function lineDiff(before, after) {
|
|
|
2980
3006
|
}
|
|
2981
3007
|
|
|
2982
3008
|
// src/sync-memory.ts
|
|
3009
|
+
var HOOK_LOG_MAX_BYTES = 1024 * 1024;
|
|
2983
3010
|
var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
|
|
2984
3011
|
var SyncFailed = class extends Error {
|
|
2985
3012
|
};
|
|
3013
|
+
var SyncStopped = class extends Error {
|
|
3014
|
+
constructor(reason, message) {
|
|
3015
|
+
super(message);
|
|
3016
|
+
this.reason = reason;
|
|
3017
|
+
}
|
|
3018
|
+
reason;
|
|
3019
|
+
};
|
|
3020
|
+
function failureReason(e) {
|
|
3021
|
+
if (e instanceof SyncStopped) return e.reason;
|
|
3022
|
+
const { status, code } = refusal(e);
|
|
3023
|
+
if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
|
|
3024
|
+
const err = e;
|
|
3025
|
+
const net = err?.cause?.code ?? err?.code ?? "";
|
|
3026
|
+
if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
|
|
3027
|
+
return "SystemDox could not be reached";
|
|
3028
|
+
}
|
|
3029
|
+
return "it could not finish";
|
|
3030
|
+
}
|
|
3031
|
+
async function writeWhole(path, text3) {
|
|
3032
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
3033
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3034
|
+
try {
|
|
3035
|
+
await writeFile3(tmp, text3, "utf8");
|
|
3036
|
+
await rename2(tmp, path);
|
|
3037
|
+
} catch (e) {
|
|
3038
|
+
await rm3(tmp, { force: true });
|
|
3039
|
+
throw e;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
2986
3042
|
function refusal(e) {
|
|
2987
3043
|
if (!(e instanceof Error)) return {};
|
|
2988
3044
|
const { status, code } = e;
|
|
2989
3045
|
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
2990
3046
|
}
|
|
2991
3047
|
var toPath = (root, rel) => join4(root, ...rel.split("/"));
|
|
3048
|
+
var LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
|
|
2992
3049
|
async function readFolder(root) {
|
|
2993
3050
|
const files = {};
|
|
2994
3051
|
const names = /* @__PURE__ */ new Set();
|
|
3052
|
+
const temps = [];
|
|
2995
3053
|
async function walk2(dir, prefix) {
|
|
2996
3054
|
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
2997
3055
|
if (e.name.startsWith(".")) continue;
|
|
@@ -2999,12 +3057,13 @@ async function readFolder(root) {
|
|
|
2999
3057
|
if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
|
|
3000
3058
|
else if (e.isFile()) {
|
|
3001
3059
|
names.add(rel.toLowerCase());
|
|
3060
|
+
if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
|
|
3002
3061
|
if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
|
|
3003
3062
|
}
|
|
3004
3063
|
}
|
|
3005
3064
|
}
|
|
3006
3065
|
if (existsSync3(root)) await walk2(root, "");
|
|
3007
|
-
return { files, names };
|
|
3066
|
+
return { files, names, temps };
|
|
3008
3067
|
}
|
|
3009
3068
|
async function pullTeam(client, ws, project) {
|
|
3010
3069
|
try {
|
|
@@ -3053,7 +3112,11 @@ var Report = class {
|
|
|
3053
3112
|
deletedHere = /* @__PURE__ */ new Set();
|
|
3054
3113
|
deletedFromTeam = /* @__PURE__ */ new Set();
|
|
3055
3114
|
conflicts = [];
|
|
3115
|
+
/** The conflict copies written this run, by name. */
|
|
3116
|
+
copies = /* @__PURE__ */ new Set();
|
|
3056
3117
|
skipped = /* @__PURE__ */ new Map();
|
|
3118
|
+
/** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
|
|
3119
|
+
waiting = /* @__PURE__ */ new Set();
|
|
3057
3120
|
people = [];
|
|
3058
3121
|
toAnswer = /* @__PURE__ */ new Map();
|
|
3059
3122
|
add(plan) {
|
|
@@ -3061,12 +3124,32 @@ var Report = class {
|
|
|
3061
3124
|
plan.pushed.forEach((p) => this.pushed.add(p));
|
|
3062
3125
|
plan.deletedHere.forEach((p) => this.deletedHere.add(p));
|
|
3063
3126
|
plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
|
|
3064
|
-
for (const c of plan.conflicts)
|
|
3127
|
+
for (const c of plan.conflicts) {
|
|
3128
|
+
this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
|
|
3129
|
+
this.copies.add(c.copy);
|
|
3130
|
+
}
|
|
3065
3131
|
for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
|
|
3066
3132
|
}
|
|
3067
3133
|
skip(path, reason) {
|
|
3068
3134
|
if (!this.skipped.has(path)) this.skipped.set(path, reason);
|
|
3069
3135
|
}
|
|
3136
|
+
/** Skipped, and waiting for the member at a terminal. */
|
|
3137
|
+
wait(path, reason) {
|
|
3138
|
+
this.skip(path, reason);
|
|
3139
|
+
this.waiting.add(path);
|
|
3140
|
+
}
|
|
3141
|
+
summary(outcome, version, reason) {
|
|
3142
|
+
return {
|
|
3143
|
+
outcome,
|
|
3144
|
+
...reason ? { reason } : {},
|
|
3145
|
+
version,
|
|
3146
|
+
pulled: this.pulled.size,
|
|
3147
|
+
pushed: this.pushed.size,
|
|
3148
|
+
deleted: this.deletedHere.size + this.deletedFromTeam.size,
|
|
3149
|
+
conflictCopies: [...this.copies].sort(),
|
|
3150
|
+
waiting: [...this.waiting].sort()
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3070
3153
|
lines(summary, dryRun) {
|
|
3071
3154
|
const out = [summary];
|
|
3072
3155
|
const section = (title, items) => {
|
|
@@ -3169,7 +3252,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3169
3252
|
for (const path of g.files) {
|
|
3170
3253
|
if (leftOut.has(path)) continue;
|
|
3171
3254
|
current[path] = addShareFalse(current[path]);
|
|
3172
|
-
await
|
|
3255
|
+
await writeWhole(toPath(opts.dir, path), current[path]);
|
|
3173
3256
|
leftOut.add(path);
|
|
3174
3257
|
}
|
|
3175
3258
|
report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
|
|
@@ -3272,7 +3355,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3272
3355
|
const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
|
|
3273
3356
|
if (yes) {
|
|
3274
3357
|
for (const path of paths) {
|
|
3275
|
-
await
|
|
3358
|
+
await writeWhole(toPath(opts.dir, path), rewritten[path]);
|
|
3276
3359
|
current[path] = rewritten[path];
|
|
3277
3360
|
}
|
|
3278
3361
|
}
|
|
@@ -3287,11 +3370,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3287
3370
|
held.add(path);
|
|
3288
3371
|
const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
|
|
3289
3372
|
if (keys.some((k) => vaultDown.has(k))) {
|
|
3290
|
-
report.
|
|
3373
|
+
report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
|
|
3291
3374
|
} else if (keys.some((k) => !answers.has(k))) {
|
|
3292
|
-
report.
|
|
3375
|
+
report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
|
|
3293
3376
|
} else {
|
|
3294
|
-
report.
|
|
3377
|
+
report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
|
|
3295
3378
|
}
|
|
3296
3379
|
}
|
|
3297
3380
|
for (const g of groups) {
|
|
@@ -3302,12 +3385,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3302
3385
|
}
|
|
3303
3386
|
return { files: current, held, leftOut };
|
|
3304
3387
|
}
|
|
3388
|
+
async function removeLeftTemps(dir, temps) {
|
|
3389
|
+
for (const rel of temps) await rm3(toPath(dir, rel), { force: true });
|
|
3390
|
+
}
|
|
3305
3391
|
async function applyPlan(dir, plan) {
|
|
3306
|
-
for (const [path, text3] of Object.entries(plan.writes))
|
|
3307
|
-
const full = toPath(dir, path);
|
|
3308
|
-
await mkdir3(dirname3(full), { recursive: true });
|
|
3309
|
-
await writeFile3(full, text3, "utf8");
|
|
3310
|
-
}
|
|
3392
|
+
for (const [path, text3] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text3);
|
|
3311
3393
|
for (const path of plan.deletes) await rm3(toPath(dir, path), { force: true });
|
|
3312
3394
|
}
|
|
3313
3395
|
var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
|
|
@@ -3352,8 +3434,19 @@ async function syncMemory(opts) {
|
|
|
3352
3434
|
const { client, out } = opts;
|
|
3353
3435
|
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
3354
3436
|
try {
|
|
3437
|
+
const checkDeadline = () => {
|
|
3438
|
+
if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
|
|
3439
|
+
throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
|
|
3440
|
+
}
|
|
3441
|
+
};
|
|
3355
3442
|
const own = await client.getOwnTeamSetup();
|
|
3356
3443
|
const ws = own.workspace;
|
|
3444
|
+
if (opts.workspace && ws !== opts.workspace) {
|
|
3445
|
+
throw new SyncStopped(
|
|
3446
|
+
"the key is for another workspace",
|
|
3447
|
+
`The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
|
|
3448
|
+
);
|
|
3449
|
+
}
|
|
3357
3450
|
const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
|
|
3358
3451
|
if (!project) {
|
|
3359
3452
|
throw new SyncFailed(
|
|
@@ -3367,6 +3460,12 @@ async function syncMemory(opts) {
|
|
|
3367
3460
|
const date = localDate(now);
|
|
3368
3461
|
const stored = (await readState(ws)).memory?.[project];
|
|
3369
3462
|
let team = await pullTeam(client, ws, project);
|
|
3463
|
+
if (opts.unattended && stored && !team.exists) {
|
|
3464
|
+
throw new SyncStopped(
|
|
3465
|
+
"the team memory could not be read",
|
|
3466
|
+
`${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
|
|
3467
|
+
);
|
|
3468
|
+
}
|
|
3370
3469
|
let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
|
|
3371
3470
|
const folder = await readFolder(dir);
|
|
3372
3471
|
const shared = {};
|
|
@@ -3379,14 +3478,16 @@ async function syncMemory(opts) {
|
|
|
3379
3478
|
} else shared[path] = text3;
|
|
3380
3479
|
}
|
|
3381
3480
|
const importWrites = {};
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3481
|
+
const current = { ...folder.files };
|
|
3482
|
+
const taken = new Set(folder.names);
|
|
3483
|
+
for (const importDir of opts.importDirs ?? []) {
|
|
3484
|
+
const other = await readFolder(resolve3(importDir));
|
|
3385
3485
|
for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
|
|
3386
3486
|
if (!isShareable(path, text3) || !isFilePath(path)) continue;
|
|
3387
|
-
const here =
|
|
3487
|
+
const here = current[path];
|
|
3388
3488
|
if (here === void 0) {
|
|
3389
3489
|
shared[path] = text3;
|
|
3490
|
+
current[path] = text3;
|
|
3390
3491
|
importWrites[path] = text3;
|
|
3391
3492
|
taken.add(path.toLowerCase());
|
|
3392
3493
|
} else if (here !== text3) {
|
|
@@ -3394,16 +3495,14 @@ async function syncMemory(opts) {
|
|
|
3394
3495
|
taken.add(copy.toLowerCase());
|
|
3395
3496
|
importWrites[copy] = text3;
|
|
3396
3497
|
report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
|
|
3498
|
+
report.copies.add(copy);
|
|
3397
3499
|
}
|
|
3398
3500
|
}
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
await writeFile3(full, text3, "utf8");
|
|
3405
|
-
}
|
|
3406
|
-
}
|
|
3501
|
+
}
|
|
3502
|
+
folder.names = taken;
|
|
3503
|
+
if (!opts.dryRun) {
|
|
3504
|
+
checkDeadline();
|
|
3505
|
+
for (const [path, text3] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text3);
|
|
3407
3506
|
}
|
|
3408
3507
|
const found = await people(run2, ws, shared, report, hint);
|
|
3409
3508
|
for (const path of found.leftOut) {
|
|
@@ -3428,12 +3527,16 @@ async function syncMemory(opts) {
|
|
|
3428
3527
|
);
|
|
3429
3528
|
return 0;
|
|
3430
3529
|
}
|
|
3530
|
+
checkDeadline();
|
|
3531
|
+
if (attempt === 1) await removeLeftTemps(dir, folder.temps);
|
|
3431
3532
|
await applyPlan(dir, plan);
|
|
3432
3533
|
let version = team.version;
|
|
3433
3534
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
3535
|
+
checkDeadline();
|
|
3434
3536
|
await upload(client, ws, project, plan, team.manifest, report);
|
|
3435
3537
|
}
|
|
3436
3538
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
3539
|
+
checkDeadline();
|
|
3437
3540
|
try {
|
|
3438
3541
|
version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
|
|
3439
3542
|
} catch (e) {
|
|
@@ -3445,6 +3548,7 @@ async function syncMemory(opts) {
|
|
|
3445
3548
|
false
|
|
3446
3549
|
)
|
|
3447
3550
|
);
|
|
3551
|
+
opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
|
|
3448
3552
|
return 2;
|
|
3449
3553
|
}
|
|
3450
3554
|
base = team;
|
|
@@ -3469,10 +3573,21 @@ async function syncMemory(opts) {
|
|
|
3469
3573
|
false
|
|
3470
3574
|
)
|
|
3471
3575
|
);
|
|
3576
|
+
opts.summary?.(report.summary("synced", version));
|
|
3472
3577
|
return 0;
|
|
3473
3578
|
}
|
|
3474
3579
|
} catch (e) {
|
|
3475
3580
|
out(e.message);
|
|
3581
|
+
opts.summary?.({
|
|
3582
|
+
outcome: "failed",
|
|
3583
|
+
reason: failureReason(e),
|
|
3584
|
+
version: 0,
|
|
3585
|
+
pulled: 0,
|
|
3586
|
+
pushed: 0,
|
|
3587
|
+
deleted: 0,
|
|
3588
|
+
conflictCopies: [],
|
|
3589
|
+
waiting: []
|
|
3590
|
+
});
|
|
3476
3591
|
return 1;
|
|
3477
3592
|
}
|
|
3478
3593
|
}
|
|
@@ -3644,7 +3759,9 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
|
|
|
3644
3759
|
project: args.project,
|
|
3645
3760
|
dryRun: !!args.dryRun,
|
|
3646
3761
|
client: apiClient,
|
|
3647
|
-
out: (...l) => lines.push(...l)
|
|
3762
|
+
out: (...l) => lines.push(...l),
|
|
3763
|
+
// Nobody is at a terminal: a project the team no longer serves is left as it is.
|
|
3764
|
+
unattended: true
|
|
3648
3765
|
});
|
|
3649
3766
|
return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
|
|
3650
3767
|
}
|
|
@@ -3652,7 +3769,7 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
|
|
|
3652
3769
|
}
|
|
3653
3770
|
|
|
3654
3771
|
// src/server.ts
|
|
3655
|
-
var SERVER_VERSION = true ? "0.1.
|
|
3772
|
+
var SERVER_VERSION = true ? "0.1.58" : "0.0.0-dev";
|
|
3656
3773
|
var READ_TOOLS = [
|
|
3657
3774
|
"get_context",
|
|
3658
3775
|
"get_checks",
|
package/dist/setup-mcp.js
CHANGED
|
@@ -359,8 +359,9 @@ var KEY_PREFIX = `(?<!${W})(?:pweb_[a-z]+_key_|sk-ant-|sk_live_|rk_live_|github_
|
|
|
359
359
|
|
|
360
360
|
// src/sync-memory.ts
|
|
361
361
|
import { existsSync } from "fs";
|
|
362
|
-
import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
362
|
+
import { appendFile, mkdir as mkdir2, open, readdir, readFile as readFile2, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
363
363
|
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
364
|
+
var HOOK_LOG_MAX_BYTES = 1024 * 1024;
|
|
364
365
|
|
|
365
366
|
// src/setup-team.ts
|
|
366
367
|
var SetupFailed = class extends Error {
|
|
@@ -399,9 +400,9 @@ function skipValue(t, i) {
|
|
|
399
400
|
while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
|
|
400
401
|
return i;
|
|
401
402
|
}
|
|
402
|
-
function objectMembers(t,
|
|
403
|
+
function objectMembers(t, open2) {
|
|
403
404
|
const members = [];
|
|
404
|
-
let i = skipSpace(t,
|
|
405
|
+
let i = skipSpace(t, open2 + 1);
|
|
405
406
|
if (t[i] === "}") return { members, close: i };
|
|
406
407
|
for (; ; ) {
|
|
407
408
|
i = skipSpace(t, i);
|
|
@@ -428,13 +429,13 @@ function setJsonMember(text, keys, value) {
|
|
|
428
429
|
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
429
430
|
const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
|
|
430
431
|
const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
|
|
431
|
-
let
|
|
432
|
+
let open2 = skipSpace(text, 0);
|
|
432
433
|
for (let k = 0; k < keys.length; k++) {
|
|
433
|
-
const { members, close } = objectMembers(text,
|
|
434
|
+
const { members, close } = objectMembers(text, open2);
|
|
434
435
|
const found = members.filter((m) => m.key === keys[k]).at(-1);
|
|
435
436
|
if (found) {
|
|
436
437
|
if (k < keys.length - 1 && text[found.valueStart] === "{") {
|
|
437
|
-
|
|
438
|
+
open2 = found.valueStart;
|
|
438
439
|
continue;
|
|
439
440
|
}
|
|
440
441
|
return text.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text, found.keyStart)) + text.slice(found.valueEnd);
|
|
@@ -443,16 +444,16 @@ function setJsonMember(text, keys, value) {
|
|
|
443
444
|
if (compact) {
|
|
444
445
|
const member = `${name}:${render(nest(k + 1), "")}`;
|
|
445
446
|
if (members.length) return text.slice(0, members.at(-1).valueEnd) + "," + member + text.slice(members.at(-1).valueEnd);
|
|
446
|
-
return text.slice(0,
|
|
447
|
+
return text.slice(0, open2 + 1) + member + text.slice(close);
|
|
447
448
|
}
|
|
448
449
|
if (members.length) {
|
|
449
450
|
const last = members.at(-1);
|
|
450
451
|
const indent2 = lineIndent(text, last.keyStart);
|
|
451
452
|
return text.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text.slice(last.valueEnd);
|
|
452
453
|
}
|
|
453
|
-
const outer = lineIndent(text,
|
|
454
|
+
const outer = lineIndent(text, open2);
|
|
454
455
|
const indent = outer + unit;
|
|
455
|
-
return text.slice(0,
|
|
456
|
+
return text.slice(0, open2 + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text.slice(close);
|
|
456
457
|
}
|
|
457
458
|
return text;
|
|
458
459
|
}
|
package/dist/setup-team.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The team setup that `systemdox-mcp-server setup [--project <name>]
|
|
3
|
-
* [--dir <checkout>] [--yes] [--overwrite]` runs after
|
|
4
|
-
* server into an AI tool's config (ADR-217 with
|
|
5
|
-
* specs/changes/team-setup/contract.md "setup").
|
|
3
|
+
* [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]` runs after
|
|
4
|
+
* it has written the MCP server into an AI tool's config (ADR-217 with
|
|
5
|
+
* Amendments 1 to 3, specs/changes/team-setup/contract.md "setup").
|
|
6
6
|
*
|
|
7
7
|
* 1. Check: `GET /team-setup` names the workspace; `claude --version` must be
|
|
8
8
|
* at least `minClaudeCodeVersion` (exit 3 otherwise, nothing changed).
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
* instructions (exit 2 if the member edited them, unless --overwrite); the
|
|
14
14
|
* import block in ~/.claude/CLAUDE.md; the marketplace in
|
|
15
15
|
* ~/.claude/settings.json; the project's `autoMemoryDirectory` in
|
|
16
|
-
* <checkout>/.claude/settings.local.json, then its sync.
|
|
16
|
+
* <checkout>/.claude/settings.local.json, then its sync. On the project's
|
|
17
|
+
* first sync on this computer, the sync imports the memory Claude Code kept
|
|
18
|
+
* for the checkout (`~/.claude/projects/<slug>/memory/`, for the checkout's
|
|
19
|
+
* path and, inside a Git repository, the repository's top folder) and each
|
|
20
|
+
* folder `--import` names, every one shown with its file count first
|
|
21
|
+
* (Amendment 3).
|
|
17
22
|
* 4. Finish: the two steps a person must take in Claude Code.
|
|
18
23
|
*
|
|
19
24
|
* It never runs `claude plugin marketplace add` and never sets
|
|
@@ -26,11 +31,13 @@
|
|
|
26
31
|
*/
|
|
27
32
|
import { SystemDoxApiClient } from "./lib/api-client.js";
|
|
28
33
|
import { type SyncClient } from "./sync-memory.js";
|
|
29
|
-
export declare const SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]";
|
|
34
|
+
export declare const SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]";
|
|
30
35
|
export declare const HEADERS_HELPER = "npx -y @systemdox/mcp-server marketplace-auth";
|
|
31
36
|
export interface TeamSetupArgs {
|
|
32
37
|
project?: string;
|
|
33
38
|
dir?: string;
|
|
39
|
+
/** Folders to import on the project's first sync on this computer (`--import`, repeatable). */
|
|
40
|
+
importDirs?: string[];
|
|
34
41
|
yes: boolean;
|
|
35
42
|
overwrite: boolean;
|
|
36
43
|
}
|
|
@@ -49,12 +56,38 @@ export interface TeamSetupOptions extends TeamSetupArgs {
|
|
|
49
56
|
cwd?: string;
|
|
50
57
|
/** `claude --version`'s output, or undefined when Claude Code is not found. */
|
|
51
58
|
claudeVersion?: () => Promise<string | undefined>;
|
|
59
|
+
/** The top folder of the Git repository holding `dir`, or undefined outside one. */
|
|
60
|
+
gitTop?: (dir: string) => Promise<string | undefined>;
|
|
52
61
|
now?: () => Date;
|
|
53
62
|
}
|
|
54
63
|
export declare function parseSetupArgs(argv: string[]): TeamSetupArgs;
|
|
55
64
|
/** `claude --version`'s output, or undefined when it cannot be run. On
|
|
56
65
|
* Windows through the shell, so `claude.cmd` (npm) and `claude.exe` both resolve. */
|
|
57
66
|
export declare function claudeCodeVersion(): Promise<string | undefined>;
|
|
67
|
+
/** The top folder of the Git repository holding `dir`, or undefined outside one. */
|
|
68
|
+
export declare function gitTopFolder(dir: string): Promise<string | undefined>;
|
|
69
|
+
/** `~/.claude/projects/<slug>/memory/` for an absolute path: Claude Code's slug
|
|
70
|
+
* replaces every character that is not a letter or a digit with `-`
|
|
71
|
+
* (`/home/jo/git` → `-home-jo-git`; `C:\Users\jo\git` → `C--Users-jo-git`). */
|
|
72
|
+
export declare function claudeMemoryFolder(path: string): string;
|
|
73
|
+
/** How many files of a folder a sync would share (hidden entries left out):
|
|
74
|
+
* the `feedback`, `project` and `reference` memories and `MEMORY.md`. A `user`
|
|
75
|
+
* memory, a `share: false` file and a conflict copy stay where they are, so
|
|
76
|
+
* they are not counted. 0 when the folder is missing. */
|
|
77
|
+
export declare function countMemoryFiles(folder: string): Promise<number>;
|
|
78
|
+
export interface MemoryImport {
|
|
79
|
+
folder: string;
|
|
80
|
+
files: number;
|
|
81
|
+
/** Named by `--import`, as opposed to found for the checkout. */
|
|
82
|
+
named: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The folders a project's first sync imports (contract.md "setup", step 3.5):
|
|
86
|
+
* the memory Claude Code kept for the checkout's own path and, inside a Git
|
|
87
|
+
* repository, for the repository's top folder, each when it holds a file and
|
|
88
|
+
* is not the team folder itself; then each folder `--import` names.
|
|
89
|
+
*/
|
|
90
|
+
export declare function memoryImports(checkout: string, teamFolder: string, named: string[], gitTop?: (dir: string) => Promise<string | undefined>): Promise<MemoryImport[]>;
|
|
58
91
|
/**
|
|
59
92
|
* `text` (a JSON object) with `keys` set to `value`, every other byte kept:
|
|
60
93
|
* an existing value is replaced in place, a missing member is added after the
|