@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/index.js
CHANGED
|
@@ -1311,8 +1311,8 @@ function registerGetPlanBoard(server, apiClient2) {
|
|
|
1311
1311
|
]
|
|
1312
1312
|
};
|
|
1313
1313
|
}
|
|
1314
|
-
const
|
|
1315
|
-
if (
|
|
1314
|
+
const open2 = boards.filter((b) => !b.closed);
|
|
1315
|
+
if (open2.length === 0) {
|
|
1316
1316
|
return {
|
|
1317
1317
|
content: [
|
|
1318
1318
|
{
|
|
@@ -1322,16 +1322,16 @@ function registerGetPlanBoard(server, apiClient2) {
|
|
|
1322
1322
|
]
|
|
1323
1323
|
};
|
|
1324
1324
|
}
|
|
1325
|
-
const closedNote = boards.length >
|
|
1325
|
+
const closedNote = boards.length > open2.length ? `
|
|
1326
1326
|
|
|
1327
|
-
_${boards.length -
|
|
1327
|
+
_${boards.length - open2.length} closed board(s) not listed._` : "";
|
|
1328
1328
|
return {
|
|
1329
1329
|
content: [
|
|
1330
1330
|
{
|
|
1331
1331
|
type: "text",
|
|
1332
|
-
text: `${
|
|
1332
|
+
text: `${open2.length} open project board(s):
|
|
1333
1333
|
|
|
1334
|
-
${formatBoards(
|
|
1334
|
+
${formatBoards(open2)}` + closedNote + `
|
|
1335
1335
|
|
|
1336
1336
|
Call get_plan_board with project_number to list a board's issues.`
|
|
1337
1337
|
}
|
|
@@ -3094,7 +3094,7 @@ var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
|
3094
3094
|
|
|
3095
3095
|
// src/sync-memory.ts
|
|
3096
3096
|
import { existsSync as existsSync3 } from "fs";
|
|
3097
|
-
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile4, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
3097
|
+
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";
|
|
3098
3098
|
import { dirname as dirname3, join as join4, resolve as resolve3 } from "path";
|
|
3099
3099
|
|
|
3100
3100
|
// src/lib/memory-sync.ts
|
|
@@ -3118,15 +3118,41 @@ function readFrontmatter(text3) {
|
|
|
3118
3118
|
const m = BLOCK_RE.exec(text3);
|
|
3119
3119
|
if (!m) return void 0;
|
|
3120
3120
|
const front = { shareFalse: false };
|
|
3121
|
-
|
|
3122
|
-
|
|
3121
|
+
const lines = m[1].split(/\r?\n/);
|
|
3122
|
+
if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
|
|
3123
|
+
let topType;
|
|
3124
|
+
let topShare;
|
|
3125
|
+
let nestedType;
|
|
3126
|
+
let nestedShare;
|
|
3127
|
+
let inMetadata = false;
|
|
3128
|
+
let nestedIndent;
|
|
3129
|
+
for (const line of lines) {
|
|
3130
|
+
if (!line.trim() || /^[ \t]*#/.test(line)) continue;
|
|
3131
|
+
const indent = /^ */.exec(line)[0].length;
|
|
3132
|
+
if (indent === 0) {
|
|
3133
|
+
inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
|
|
3134
|
+
nestedIndent = void 0;
|
|
3135
|
+
}
|
|
3136
|
+
const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
3123
3137
|
if (!kv) continue;
|
|
3124
3138
|
const raw = kv[2].replace(/[ \t]+#.*$/, "");
|
|
3125
3139
|
const quoted = /^(["'])(.*)\1$/.exec(raw);
|
|
3126
3140
|
const value = quoted ? quoted[2] : raw;
|
|
3127
|
-
|
|
3128
|
-
if (
|
|
3129
|
-
|
|
3141
|
+
const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
|
|
3142
|
+
if (indent === 0) {
|
|
3143
|
+
if (kv[1] === "type") topType = value.trim().toLowerCase();
|
|
3144
|
+
if (kv[1] === "share") topShare = isFalse;
|
|
3145
|
+
continue;
|
|
3146
|
+
}
|
|
3147
|
+
if (!inMetadata) continue;
|
|
3148
|
+
nestedIndent ??= indent;
|
|
3149
|
+
if (indent !== nestedIndent) continue;
|
|
3150
|
+
if (kv[1] === "type") nestedType = value.trim().toLowerCase();
|
|
3151
|
+
if (kv[1] === "share") nestedShare = isFalse;
|
|
3152
|
+
}
|
|
3153
|
+
const type = topType ?? nestedType;
|
|
3154
|
+
if (type !== void 0) front.type = type;
|
|
3155
|
+
front.shareFalse = (topShare ?? nestedShare) === true;
|
|
3130
3156
|
return front;
|
|
3131
3157
|
}
|
|
3132
3158
|
function isShareable(path, text3) {
|
|
@@ -3367,18 +3393,50 @@ function lineDiff(before, after) {
|
|
|
3367
3393
|
}
|
|
3368
3394
|
|
|
3369
3395
|
// src/sync-memory.ts
|
|
3396
|
+
var HOOK_LOG_MAX_BYTES = 1024 * 1024;
|
|
3370
3397
|
var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
|
|
3371
3398
|
var SyncFailed = class extends Error {
|
|
3372
3399
|
};
|
|
3400
|
+
var SyncStopped = class extends Error {
|
|
3401
|
+
constructor(reason, message) {
|
|
3402
|
+
super(message);
|
|
3403
|
+
this.reason = reason;
|
|
3404
|
+
}
|
|
3405
|
+
reason;
|
|
3406
|
+
};
|
|
3407
|
+
function failureReason(e) {
|
|
3408
|
+
if (e instanceof SyncStopped) return e.reason;
|
|
3409
|
+
const { status, code } = refusal(e);
|
|
3410
|
+
if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
|
|
3411
|
+
const err = e;
|
|
3412
|
+
const net = err?.cause?.code ?? err?.code ?? "";
|
|
3413
|
+
if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
|
|
3414
|
+
return "SystemDox could not be reached";
|
|
3415
|
+
}
|
|
3416
|
+
return "it could not finish";
|
|
3417
|
+
}
|
|
3418
|
+
async function writeWhole(path, text3) {
|
|
3419
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
3420
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3421
|
+
try {
|
|
3422
|
+
await writeFile3(tmp, text3, "utf8");
|
|
3423
|
+
await rename2(tmp, path);
|
|
3424
|
+
} catch (e) {
|
|
3425
|
+
await rm3(tmp, { force: true });
|
|
3426
|
+
throw e;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3373
3429
|
function refusal(e) {
|
|
3374
3430
|
if (!(e instanceof Error)) return {};
|
|
3375
3431
|
const { status, code } = e;
|
|
3376
3432
|
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
3377
3433
|
}
|
|
3378
3434
|
var toPath = (root, rel) => join4(root, ...rel.split("/"));
|
|
3435
|
+
var LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
|
|
3379
3436
|
async function readFolder(root) {
|
|
3380
3437
|
const files = {};
|
|
3381
3438
|
const names = /* @__PURE__ */ new Set();
|
|
3439
|
+
const temps = [];
|
|
3382
3440
|
async function walk2(dir, prefix) {
|
|
3383
3441
|
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
3384
3442
|
if (e.name.startsWith(".")) continue;
|
|
@@ -3386,12 +3444,13 @@ async function readFolder(root) {
|
|
|
3386
3444
|
if (e.isDirectory()) await walk2(join4(dir, e.name), rel);
|
|
3387
3445
|
else if (e.isFile()) {
|
|
3388
3446
|
names.add(rel.toLowerCase());
|
|
3447
|
+
if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
|
|
3389
3448
|
if (e.name.endsWith(".md")) files[rel] = await readFile4(join4(dir, e.name), "utf8");
|
|
3390
3449
|
}
|
|
3391
3450
|
}
|
|
3392
3451
|
}
|
|
3393
3452
|
if (existsSync3(root)) await walk2(root, "");
|
|
3394
|
-
return { files, names };
|
|
3453
|
+
return { files, names, temps };
|
|
3395
3454
|
}
|
|
3396
3455
|
async function pullTeam(client, ws, project) {
|
|
3397
3456
|
try {
|
|
@@ -3440,7 +3499,11 @@ var Report = class {
|
|
|
3440
3499
|
deletedHere = /* @__PURE__ */ new Set();
|
|
3441
3500
|
deletedFromTeam = /* @__PURE__ */ new Set();
|
|
3442
3501
|
conflicts = [];
|
|
3502
|
+
/** The conflict copies written this run, by name. */
|
|
3503
|
+
copies = /* @__PURE__ */ new Set();
|
|
3443
3504
|
skipped = /* @__PURE__ */ new Map();
|
|
3505
|
+
/** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
|
|
3506
|
+
waiting = /* @__PURE__ */ new Set();
|
|
3444
3507
|
people = [];
|
|
3445
3508
|
toAnswer = /* @__PURE__ */ new Map();
|
|
3446
3509
|
add(plan) {
|
|
@@ -3448,12 +3511,32 @@ var Report = class {
|
|
|
3448
3511
|
plan.pushed.forEach((p) => this.pushed.add(p));
|
|
3449
3512
|
plan.deletedHere.forEach((p) => this.deletedHere.add(p));
|
|
3450
3513
|
plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
|
|
3451
|
-
for (const c of plan.conflicts)
|
|
3514
|
+
for (const c of plan.conflicts) {
|
|
3515
|
+
this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
|
|
3516
|
+
this.copies.add(c.copy);
|
|
3517
|
+
}
|
|
3452
3518
|
for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
|
|
3453
3519
|
}
|
|
3454
3520
|
skip(path, reason) {
|
|
3455
3521
|
if (!this.skipped.has(path)) this.skipped.set(path, reason);
|
|
3456
3522
|
}
|
|
3523
|
+
/** Skipped, and waiting for the member at a terminal. */
|
|
3524
|
+
wait(path, reason) {
|
|
3525
|
+
this.skip(path, reason);
|
|
3526
|
+
this.waiting.add(path);
|
|
3527
|
+
}
|
|
3528
|
+
summary(outcome, version, reason) {
|
|
3529
|
+
return {
|
|
3530
|
+
outcome,
|
|
3531
|
+
...reason ? { reason } : {},
|
|
3532
|
+
version,
|
|
3533
|
+
pulled: this.pulled.size,
|
|
3534
|
+
pushed: this.pushed.size,
|
|
3535
|
+
deleted: this.deletedHere.size + this.deletedFromTeam.size,
|
|
3536
|
+
conflictCopies: [...this.copies].sort(),
|
|
3537
|
+
waiting: [...this.waiting].sort()
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3457
3540
|
lines(summary, dryRun) {
|
|
3458
3541
|
const out = [summary];
|
|
3459
3542
|
const section = (title, items) => {
|
|
@@ -3556,7 +3639,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3556
3639
|
for (const path of g.files) {
|
|
3557
3640
|
if (leftOut.has(path)) continue;
|
|
3558
3641
|
current[path] = addShareFalse(current[path]);
|
|
3559
|
-
await
|
|
3642
|
+
await writeWhole(toPath(opts.dir, path), current[path]);
|
|
3560
3643
|
leftOut.add(path);
|
|
3561
3644
|
}
|
|
3562
3645
|
report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
|
|
@@ -3659,7 +3742,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3659
3742
|
const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
|
|
3660
3743
|
if (yes) {
|
|
3661
3744
|
for (const path of paths) {
|
|
3662
|
-
await
|
|
3745
|
+
await writeWhole(toPath(opts.dir, path), rewritten[path]);
|
|
3663
3746
|
current[path] = rewritten[path];
|
|
3664
3747
|
}
|
|
3665
3748
|
}
|
|
@@ -3674,11 +3757,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3674
3757
|
held.add(path);
|
|
3675
3758
|
const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
|
|
3676
3759
|
if (keys.some((k) => vaultDown.has(k))) {
|
|
3677
|
-
report.
|
|
3760
|
+
report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
|
|
3678
3761
|
} else if (keys.some((k) => !answers.has(k))) {
|
|
3679
|
-
report.
|
|
3762
|
+
report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
|
|
3680
3763
|
} else {
|
|
3681
|
-
report.
|
|
3764
|
+
report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
|
|
3682
3765
|
}
|
|
3683
3766
|
}
|
|
3684
3767
|
for (const g of groups) {
|
|
@@ -3689,12 +3772,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
3689
3772
|
}
|
|
3690
3773
|
return { files: current, held, leftOut };
|
|
3691
3774
|
}
|
|
3775
|
+
async function removeLeftTemps(dir, temps) {
|
|
3776
|
+
for (const rel of temps) await rm3(toPath(dir, rel), { force: true });
|
|
3777
|
+
}
|
|
3692
3778
|
async function applyPlan(dir, plan) {
|
|
3693
|
-
for (const [path, text3] of Object.entries(plan.writes))
|
|
3694
|
-
const full = toPath(dir, path);
|
|
3695
|
-
await mkdir3(dirname3(full), { recursive: true });
|
|
3696
|
-
await writeFile3(full, text3, "utf8");
|
|
3697
|
-
}
|
|
3779
|
+
for (const [path, text3] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text3);
|
|
3698
3780
|
for (const path of plan.deletes) await rm3(toPath(dir, path), { force: true });
|
|
3699
3781
|
}
|
|
3700
3782
|
var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
|
|
@@ -3739,8 +3821,19 @@ async function syncMemory(opts) {
|
|
|
3739
3821
|
const { client, out } = opts;
|
|
3740
3822
|
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
3741
3823
|
try {
|
|
3824
|
+
const checkDeadline = () => {
|
|
3825
|
+
if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
|
|
3826
|
+
throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
|
|
3827
|
+
}
|
|
3828
|
+
};
|
|
3742
3829
|
const own = await client.getOwnTeamSetup();
|
|
3743
3830
|
const ws = own.workspace;
|
|
3831
|
+
if (opts.workspace && ws !== opts.workspace) {
|
|
3832
|
+
throw new SyncStopped(
|
|
3833
|
+
"the key is for another workspace",
|
|
3834
|
+
`The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
|
|
3835
|
+
);
|
|
3836
|
+
}
|
|
3744
3837
|
const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
|
|
3745
3838
|
if (!project) {
|
|
3746
3839
|
throw new SyncFailed(
|
|
@@ -3754,6 +3847,12 @@ async function syncMemory(opts) {
|
|
|
3754
3847
|
const date = localDate(now);
|
|
3755
3848
|
const stored = (await readState(ws)).memory?.[project];
|
|
3756
3849
|
let team = await pullTeam(client, ws, project);
|
|
3850
|
+
if (opts.unattended && stored && !team.exists) {
|
|
3851
|
+
throw new SyncStopped(
|
|
3852
|
+
"the team memory could not be read",
|
|
3853
|
+
`${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
|
|
3854
|
+
);
|
|
3855
|
+
}
|
|
3757
3856
|
let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
|
|
3758
3857
|
const folder = await readFolder(dir);
|
|
3759
3858
|
const shared = {};
|
|
@@ -3766,14 +3865,16 @@ async function syncMemory(opts) {
|
|
|
3766
3865
|
} else shared[path] = text3;
|
|
3767
3866
|
}
|
|
3768
3867
|
const importWrites = {};
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3868
|
+
const current = { ...folder.files };
|
|
3869
|
+
const taken = new Set(folder.names);
|
|
3870
|
+
for (const importDir of opts.importDirs ?? []) {
|
|
3871
|
+
const other = await readFolder(resolve3(importDir));
|
|
3772
3872
|
for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
|
|
3773
3873
|
if (!isShareable(path, text3) || !isFilePath(path)) continue;
|
|
3774
|
-
const here =
|
|
3874
|
+
const here = current[path];
|
|
3775
3875
|
if (here === void 0) {
|
|
3776
3876
|
shared[path] = text3;
|
|
3877
|
+
current[path] = text3;
|
|
3777
3878
|
importWrites[path] = text3;
|
|
3778
3879
|
taken.add(path.toLowerCase());
|
|
3779
3880
|
} else if (here !== text3) {
|
|
@@ -3781,16 +3882,14 @@ async function syncMemory(opts) {
|
|
|
3781
3882
|
taken.add(copy.toLowerCase());
|
|
3782
3883
|
importWrites[copy] = text3;
|
|
3783
3884
|
report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
|
|
3885
|
+
report.copies.add(copy);
|
|
3784
3886
|
}
|
|
3785
3887
|
}
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
await writeFile3(full, text3, "utf8");
|
|
3792
|
-
}
|
|
3793
|
-
}
|
|
3888
|
+
}
|
|
3889
|
+
folder.names = taken;
|
|
3890
|
+
if (!opts.dryRun) {
|
|
3891
|
+
checkDeadline();
|
|
3892
|
+
for (const [path, text3] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text3);
|
|
3794
3893
|
}
|
|
3795
3894
|
const found = await people(run2, ws, shared, report, hint);
|
|
3796
3895
|
for (const path of found.leftOut) {
|
|
@@ -3815,12 +3914,16 @@ async function syncMemory(opts) {
|
|
|
3815
3914
|
);
|
|
3816
3915
|
return 0;
|
|
3817
3916
|
}
|
|
3917
|
+
checkDeadline();
|
|
3918
|
+
if (attempt === 1) await removeLeftTemps(dir, folder.temps);
|
|
3818
3919
|
await applyPlan(dir, plan);
|
|
3819
3920
|
let version = team.version;
|
|
3820
3921
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
3922
|
+
checkDeadline();
|
|
3821
3923
|
await upload(client, ws, project, plan, team.manifest, report);
|
|
3822
3924
|
}
|
|
3823
3925
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
3926
|
+
checkDeadline();
|
|
3824
3927
|
try {
|
|
3825
3928
|
version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
|
|
3826
3929
|
} catch (e) {
|
|
@@ -3832,6 +3935,7 @@ async function syncMemory(opts) {
|
|
|
3832
3935
|
false
|
|
3833
3936
|
)
|
|
3834
3937
|
);
|
|
3938
|
+
opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
|
|
3835
3939
|
return 2;
|
|
3836
3940
|
}
|
|
3837
3941
|
base = team;
|
|
@@ -3856,10 +3960,21 @@ async function syncMemory(opts) {
|
|
|
3856
3960
|
false
|
|
3857
3961
|
)
|
|
3858
3962
|
);
|
|
3963
|
+
opts.summary?.(report.summary("synced", version));
|
|
3859
3964
|
return 0;
|
|
3860
3965
|
}
|
|
3861
3966
|
} catch (e) {
|
|
3862
3967
|
out(e.message);
|
|
3968
|
+
opts.summary?.({
|
|
3969
|
+
outcome: "failed",
|
|
3970
|
+
reason: failureReason(e),
|
|
3971
|
+
version: 0,
|
|
3972
|
+
pulled: 0,
|
|
3973
|
+
pushed: 0,
|
|
3974
|
+
deleted: 0,
|
|
3975
|
+
conflictCopies: [],
|
|
3976
|
+
waiting: []
|
|
3977
|
+
});
|
|
3863
3978
|
return 1;
|
|
3864
3979
|
}
|
|
3865
3980
|
}
|
|
@@ -4031,7 +4146,9 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
|
4031
4146
|
project: args.project,
|
|
4032
4147
|
dryRun: !!args.dryRun,
|
|
4033
4148
|
client: apiClient2,
|
|
4034
|
-
out: (...l) => lines.push(...l)
|
|
4149
|
+
out: (...l) => lines.push(...l),
|
|
4150
|
+
// Nobody is at a terminal: a project the team no longer serves is left as it is.
|
|
4151
|
+
unattended: true
|
|
4035
4152
|
});
|
|
4036
4153
|
return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
|
|
4037
4154
|
}
|
|
@@ -4039,7 +4156,7 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
|
4039
4156
|
}
|
|
4040
4157
|
|
|
4041
4158
|
// src/server.ts
|
|
4042
|
-
var SERVER_VERSION = true ? "0.1.
|
|
4159
|
+
var SERVER_VERSION = true ? "0.1.58" : "0.0.0-dev";
|
|
4043
4160
|
var READ_TOOLS = [
|
|
4044
4161
|
"get_context",
|
|
4045
4162
|
"get_checks",
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* `src/sync-memory.ts` reads the folders and calls the API around it.
|
|
5
5
|
*
|
|
6
6
|
* - **What is shared** (`isShareable`): a `*.md` file whose frontmatter `type`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* (at the top level or under `metadata:`, Amendment 3) is `feedback`,
|
|
8
|
+
* `project` or `reference`, and `MEMORY.md`; never a file with
|
|
9
|
+
* `share: false` or a `<name>.local-<date>.md` conflict copy.
|
|
9
10
|
* - **The merge** (`decide`, `planMerge`): three-way per path, with the base
|
|
10
11
|
* (the manifest of the last sync), the team's latest version and the local
|
|
11
12
|
* folder, row by row as the contract's table says.
|
|
@@ -29,7 +30,13 @@ export interface Frontmatter {
|
|
|
29
30
|
shareFalse: boolean;
|
|
30
31
|
}
|
|
31
32
|
/** The frontmatter's `type` and `share`, or undefined when the file has no
|
|
32
|
-
* leading block.
|
|
33
|
+
* leading block. They are read at the top level, or one level under
|
|
34
|
+
* `metadata:`, the form Claude Code writes (`metadata:` on one line,
|
|
35
|
+
* ` type: project` under it); a top-level key wins over the nested one
|
|
36
|
+
* (ADR-217 Amendment 3). Read as the server's YAML reads them:
|
|
37
|
+
* - a block with a tab in a line's indentation is not YAML, so the server sees
|
|
38
|
+
* no front matter: nothing is read here either (no type, so not shared);
|
|
39
|
+
* - only keys at the first indent under `metadata:` count, never deeper ones. */
|
|
33
40
|
export declare function readFrontmatter(text: string): Frontmatter | undefined;
|
|
34
41
|
/**
|
|
35
42
|
* Shared with the team: `MEMORY.md` at the folder's root, or a `*.md` file
|
package/dist/lib/memory-sync.js
CHANGED
|
@@ -93,15 +93,41 @@ function readFrontmatter(text) {
|
|
|
93
93
|
const m = BLOCK_RE.exec(text);
|
|
94
94
|
if (!m) return void 0;
|
|
95
95
|
const front = { shareFalse: false };
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
const lines = m[1].split(/\r?\n/);
|
|
97
|
+
if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
|
|
98
|
+
let topType;
|
|
99
|
+
let topShare;
|
|
100
|
+
let nestedType;
|
|
101
|
+
let nestedShare;
|
|
102
|
+
let inMetadata = false;
|
|
103
|
+
let nestedIndent;
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
if (!line.trim() || /^[ \t]*#/.test(line)) continue;
|
|
106
|
+
const indent = /^ */.exec(line)[0].length;
|
|
107
|
+
if (indent === 0) {
|
|
108
|
+
inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
|
|
109
|
+
nestedIndent = void 0;
|
|
110
|
+
}
|
|
111
|
+
const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
98
112
|
if (!kv) continue;
|
|
99
113
|
const raw = kv[2].replace(/[ \t]+#.*$/, "");
|
|
100
114
|
const quoted = /^(["'])(.*)\1$/.exec(raw);
|
|
101
115
|
const value = quoted ? quoted[2] : raw;
|
|
102
|
-
|
|
103
|
-
if (
|
|
116
|
+
const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
|
|
117
|
+
if (indent === 0) {
|
|
118
|
+
if (kv[1] === "type") topType = value.trim().toLowerCase();
|
|
119
|
+
if (kv[1] === "share") topShare = isFalse;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!inMetadata) continue;
|
|
123
|
+
nestedIndent ??= indent;
|
|
124
|
+
if (indent !== nestedIndent) continue;
|
|
125
|
+
if (kv[1] === "type") nestedType = value.trim().toLowerCase();
|
|
126
|
+
if (kv[1] === "share") nestedShare = isFalse;
|
|
104
127
|
}
|
|
128
|
+
const type = topType ?? nestedType;
|
|
129
|
+
if (type !== void 0) front.type = type;
|
|
130
|
+
front.shareFalse = (topShare ?? nestedShare) === true;
|
|
105
131
|
return front;
|
|
106
132
|
}
|
|
107
133
|
function isShareable(path, text) {
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
* - `~/.claude/systemdox/<workspace>/state.json`: the installed versions and
|
|
9
9
|
* hashes, each memory project's base manifest and version, the accepted
|
|
10
10
|
* SHA-256s. Never a value and never an answer about a person;
|
|
11
|
-
* - `~/.claude/systemdox/<workspace>/plugin/<n>/`: plugin version n, unpacked
|
|
11
|
+
* - `~/.claude/systemdox/<workspace>/plugin/<n>/`: plugin version n, unpacked;
|
|
12
|
+
* - `~/.claude/systemdox/<workspace>/sync-memory.log` and `sync.lock`: the hook
|
|
13
|
+
* run's full report and its lock (ADR-217 Amendment 3).
|
|
12
14
|
*
|
|
13
15
|
* Every path starts at `homeDir()`, the one place the home folder is read, so
|
|
14
16
|
* a test points HOME (USERPROFILE on Windows) at a temporary folder.
|
|
@@ -29,6 +31,11 @@ export declare function systemdoxDir(): string;
|
|
|
29
31
|
export declare function credentialsPath(): string;
|
|
30
32
|
export declare function workspaceDir(workspace: string): string;
|
|
31
33
|
export declare function statePath(workspace: string): string;
|
|
34
|
+
/** `~/.claude/systemdox/<workspace>/sync-memory.log`: the full report of every
|
|
35
|
+
* `sync-memory --hook` run, newest last (ADR-217 Amendment 3). */
|
|
36
|
+
export declare function syncLogPath(workspace: string): string;
|
|
37
|
+
/** `~/.claude/systemdox/<workspace>/sync.lock`: held while a hook run syncs. */
|
|
38
|
+
export declare function syncLockPath(workspace: string): string;
|
|
32
39
|
export declare function pluginRoot(workspace: string): string;
|
|
33
40
|
export declare function pluginDir(workspace: string, version: number): string;
|
|
34
41
|
export type Credentials = Record<string, {
|
|
@@ -63,6 +63,12 @@ function workspaceDir(workspace) {
|
|
|
63
63
|
function statePath(workspace) {
|
|
64
64
|
return join(workspaceDir(workspace), "state.json");
|
|
65
65
|
}
|
|
66
|
+
function syncLogPath(workspace) {
|
|
67
|
+
return join(workspaceDir(workspace), "sync-memory.log");
|
|
68
|
+
}
|
|
69
|
+
function syncLockPath(workspace) {
|
|
70
|
+
return join(workspaceDir(workspace), "sync.lock");
|
|
71
|
+
}
|
|
66
72
|
function pluginRoot(workspace) {
|
|
67
73
|
return join(workspaceDir(workspace), "plugin");
|
|
68
74
|
}
|
|
@@ -226,6 +232,8 @@ export {
|
|
|
226
232
|
restrictToMember,
|
|
227
233
|
runMarketplaceAuth,
|
|
228
234
|
statePath,
|
|
235
|
+
syncLockPath,
|
|
236
|
+
syncLogPath,
|
|
229
237
|
systemdoxDir,
|
|
230
238
|
updateState,
|
|
231
239
|
workspaceDir,
|
|
@@ -56,6 +56,8 @@ export interface PluginVersion {
|
|
|
56
56
|
args?: string[];
|
|
57
57
|
url?: string;
|
|
58
58
|
}[];
|
|
59
|
+
/** This version carries SystemDox's memory sync hooks (ADR-217 Amendment 3); absent before it. */
|
|
60
|
+
memoryHooks?: boolean;
|
|
59
61
|
command: string;
|
|
60
62
|
}
|
|
61
63
|
export interface Marketplace {
|