@systemdox/mcp-server 0.1.56 → 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/cli.js
CHANGED
|
@@ -75,6 +75,12 @@ function workspaceDir(workspace) {
|
|
|
75
75
|
function statePath(workspace) {
|
|
76
76
|
return join(workspaceDir(workspace), "state.json");
|
|
77
77
|
}
|
|
78
|
+
function syncLogPath(workspace) {
|
|
79
|
+
return join(workspaceDir(workspace), "sync-memory.log");
|
|
80
|
+
}
|
|
81
|
+
function syncLockPath(workspace) {
|
|
82
|
+
return join(workspaceDir(workspace), "sync.lock");
|
|
83
|
+
}
|
|
78
84
|
function pluginRoot(workspace) {
|
|
79
85
|
return join(workspaceDir(workspace), "plugin");
|
|
80
86
|
}
|
|
@@ -585,15 +591,41 @@ function readFrontmatter(text3) {
|
|
|
585
591
|
const m = BLOCK_RE.exec(text3);
|
|
586
592
|
if (!m) return void 0;
|
|
587
593
|
const front = { shareFalse: false };
|
|
588
|
-
|
|
589
|
-
|
|
594
|
+
const lines = m[1].split(/\r?\n/);
|
|
595
|
+
if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
|
|
596
|
+
let topType;
|
|
597
|
+
let topShare;
|
|
598
|
+
let nestedType;
|
|
599
|
+
let nestedShare;
|
|
600
|
+
let inMetadata = false;
|
|
601
|
+
let nestedIndent;
|
|
602
|
+
for (const line of lines) {
|
|
603
|
+
if (!line.trim() || /^[ \t]*#/.test(line)) continue;
|
|
604
|
+
const indent = /^ */.exec(line)[0].length;
|
|
605
|
+
if (indent === 0) {
|
|
606
|
+
inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
|
|
607
|
+
nestedIndent = void 0;
|
|
608
|
+
}
|
|
609
|
+
const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
590
610
|
if (!kv) continue;
|
|
591
611
|
const raw = kv[2].replace(/[ \t]+#.*$/, "");
|
|
592
612
|
const quoted = /^(["'])(.*)\1$/.exec(raw);
|
|
593
613
|
const value = quoted ? quoted[2] : raw;
|
|
594
|
-
|
|
595
|
-
if (
|
|
596
|
-
|
|
614
|
+
const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
|
|
615
|
+
if (indent === 0) {
|
|
616
|
+
if (kv[1] === "type") topType = value.trim().toLowerCase();
|
|
617
|
+
if (kv[1] === "share") topShare = isFalse;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (!inMetadata) continue;
|
|
621
|
+
nestedIndent ??= indent;
|
|
622
|
+
if (indent !== nestedIndent) continue;
|
|
623
|
+
if (kv[1] === "type") nestedType = value.trim().toLowerCase();
|
|
624
|
+
if (kv[1] === "share") nestedShare = isFalse;
|
|
625
|
+
}
|
|
626
|
+
const type = topType ?? nestedType;
|
|
627
|
+
if (type !== void 0) front.type = type;
|
|
628
|
+
front.shareFalse = (topShare ?? nestedShare) === true;
|
|
597
629
|
return front;
|
|
598
630
|
}
|
|
599
631
|
function isShareable(path, text3) {
|
|
@@ -849,35 +881,69 @@ var init_memory_sync = __esm({
|
|
|
849
881
|
// src/sync-memory.ts
|
|
850
882
|
var sync_memory_exports = {};
|
|
851
883
|
__export(sync_memory_exports, {
|
|
884
|
+
HOOK_BUDGET_MS: () => HOOK_BUDGET_MS,
|
|
885
|
+
HOOK_LOCK_STALE_MS: () => HOOK_LOCK_STALE_MS,
|
|
886
|
+
HOOK_LOG_MAX_BYTES: () => HOOK_LOG_MAX_BYTES,
|
|
852
887
|
SYNC_MEMORY_COMMAND: () => SYNC_MEMORY_COMMAND,
|
|
853
888
|
SYNC_MEMORY_USAGE: () => SYNC_MEMORY_USAGE,
|
|
889
|
+
hookLines: () => hookLines,
|
|
854
890
|
parseSyncMemoryArgs: () => parseSyncMemoryArgs,
|
|
891
|
+
runHookSync: () => runHookSync,
|
|
855
892
|
runSyncMemory: () => runSyncMemory,
|
|
856
893
|
syncMemory: () => syncMemory
|
|
857
894
|
});
|
|
858
895
|
import { existsSync } from "fs";
|
|
859
|
-
import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
896
|
+
import { appendFile, mkdir as mkdir2, open, readdir, readFile as readFile2, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
860
897
|
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
898
|
+
function failureReason(e) {
|
|
899
|
+
if (e instanceof SyncStopped) return e.reason;
|
|
900
|
+
const { status, code } = refusal(e);
|
|
901
|
+
if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
|
|
902
|
+
const err = e;
|
|
903
|
+
const net = err?.cause?.code ?? err?.code ?? "";
|
|
904
|
+
if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
|
|
905
|
+
return "SystemDox could not be reached";
|
|
906
|
+
}
|
|
907
|
+
return "it could not finish";
|
|
908
|
+
}
|
|
909
|
+
async function writeWhole(path, text3) {
|
|
910
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
911
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
912
|
+
try {
|
|
913
|
+
await writeFile2(tmp, text3, "utf8");
|
|
914
|
+
await rename2(tmp, path);
|
|
915
|
+
} catch (e) {
|
|
916
|
+
await rm2(tmp, { force: true });
|
|
917
|
+
throw e;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
861
920
|
function refusal(e) {
|
|
862
921
|
if (!(e instanceof Error)) return {};
|
|
863
922
|
const { status, code } = e;
|
|
864
923
|
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
865
924
|
}
|
|
866
925
|
function parseSyncMemoryArgs(argv) {
|
|
867
|
-
const args2 = { dryRun: false };
|
|
926
|
+
const args2 = { dryRun: false, importDirs: [] };
|
|
868
927
|
for (let i = 0; i < argv.length; i++) {
|
|
869
928
|
const a = argv[i];
|
|
870
929
|
if (a === "--dry-run") args2.dryRun = true;
|
|
871
|
-
else if (a === "--project" || a === "--dir" || a === "--import") {
|
|
930
|
+
else if (a === "--project" || a === "--dir" || a === "--import" || a === "--hook") {
|
|
872
931
|
const value = argv[++i];
|
|
873
932
|
if (!value || value.startsWith("--")) throw new SyncFailed(`${a} needs a value
|
|
874
933
|
${SYNC_MEMORY_USAGE}`);
|
|
875
934
|
if (a === "--project") {
|
|
876
935
|
if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a project name (lower case letters, digits and -)`);
|
|
877
936
|
args2.project = value;
|
|
937
|
+
} else if (a === "--hook") {
|
|
938
|
+
if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a workspace name (lower case letters, digits and -)`);
|
|
939
|
+
args2.hook = value;
|
|
878
940
|
} else if (a === "--dir") args2.dir = value;
|
|
879
|
-
else args2.
|
|
941
|
+
else args2.importDirs.push(value);
|
|
880
942
|
} else throw new SyncFailed(`Unexpected argument ${a}
|
|
943
|
+
${SYNC_MEMORY_USAGE}`);
|
|
944
|
+
}
|
|
945
|
+
if (args2.hook && (args2.project || args2.dir || args2.importDirs.length || args2.dryRun)) {
|
|
946
|
+
throw new SyncFailed(`--hook takes no other option
|
|
881
947
|
${SYNC_MEMORY_USAGE}`);
|
|
882
948
|
}
|
|
883
949
|
return args2;
|
|
@@ -885,6 +951,7 @@ ${SYNC_MEMORY_USAGE}`);
|
|
|
885
951
|
async function readFolder(root) {
|
|
886
952
|
const files = {};
|
|
887
953
|
const names = /* @__PURE__ */ new Set();
|
|
954
|
+
const temps = [];
|
|
888
955
|
async function walk2(dir, prefix) {
|
|
889
956
|
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
890
957
|
if (e.name.startsWith(".")) continue;
|
|
@@ -892,12 +959,13 @@ async function readFolder(root) {
|
|
|
892
959
|
if (e.isDirectory()) await walk2(join2(dir, e.name), rel);
|
|
893
960
|
else if (e.isFile()) {
|
|
894
961
|
names.add(rel.toLowerCase());
|
|
962
|
+
if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
|
|
895
963
|
if (e.name.endsWith(".md")) files[rel] = await readFile2(join2(dir, e.name), "utf8");
|
|
896
964
|
}
|
|
897
965
|
}
|
|
898
966
|
}
|
|
899
967
|
if (existsSync(root)) await walk2(root, "");
|
|
900
|
-
return { files, names };
|
|
968
|
+
return { files, names, temps };
|
|
901
969
|
}
|
|
902
970
|
async function pullTeam(client, ws, project) {
|
|
903
971
|
try {
|
|
@@ -1005,7 +1073,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
1005
1073
|
for (const path of g.files) {
|
|
1006
1074
|
if (leftOut.has(path)) continue;
|
|
1007
1075
|
current[path] = addShareFalse(current[path]);
|
|
1008
|
-
await
|
|
1076
|
+
await writeWhole(toPath(opts.dir, path), current[path]);
|
|
1009
1077
|
leftOut.add(path);
|
|
1010
1078
|
}
|
|
1011
1079
|
report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
|
|
@@ -1108,7 +1176,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
1108
1176
|
const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
|
|
1109
1177
|
if (yes) {
|
|
1110
1178
|
for (const path of paths) {
|
|
1111
|
-
await
|
|
1179
|
+
await writeWhole(toPath(opts.dir, path), rewritten[path]);
|
|
1112
1180
|
current[path] = rewritten[path];
|
|
1113
1181
|
}
|
|
1114
1182
|
}
|
|
@@ -1123,11 +1191,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
1123
1191
|
held.add(path);
|
|
1124
1192
|
const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
|
|
1125
1193
|
if (keys.some((k) => vaultDown.has(k))) {
|
|
1126
|
-
report.
|
|
1194
|
+
report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
|
|
1127
1195
|
} else if (keys.some((k) => !answers.has(k))) {
|
|
1128
|
-
report.
|
|
1196
|
+
report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
|
|
1129
1197
|
} else {
|
|
1130
|
-
report.
|
|
1198
|
+
report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
|
|
1131
1199
|
}
|
|
1132
1200
|
}
|
|
1133
1201
|
for (const g of groups) {
|
|
@@ -1138,12 +1206,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
1138
1206
|
}
|
|
1139
1207
|
return { files: current, held, leftOut };
|
|
1140
1208
|
}
|
|
1209
|
+
async function removeLeftTemps(dir, temps) {
|
|
1210
|
+
for (const rel of temps) await rm2(toPath(dir, rel), { force: true });
|
|
1211
|
+
}
|
|
1141
1212
|
async function applyPlan(dir, plan) {
|
|
1142
|
-
for (const [path, text3] of Object.entries(plan.writes))
|
|
1143
|
-
const full = toPath(dir, path);
|
|
1144
|
-
await mkdir2(dirname2(full), { recursive: true });
|
|
1145
|
-
await writeFile2(full, text3, "utf8");
|
|
1146
|
-
}
|
|
1213
|
+
for (const [path, text3] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text3);
|
|
1147
1214
|
for (const path of plan.deletes) await rm2(toPath(dir, path), { force: true });
|
|
1148
1215
|
}
|
|
1149
1216
|
async function upload(client, ws, project, plan, team, report) {
|
|
@@ -1187,8 +1254,19 @@ async function syncMemory(opts) {
|
|
|
1187
1254
|
const { client, out } = opts;
|
|
1188
1255
|
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
1189
1256
|
try {
|
|
1257
|
+
const checkDeadline = () => {
|
|
1258
|
+
if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
|
|
1259
|
+
throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1190
1262
|
const own = await client.getOwnTeamSetup();
|
|
1191
1263
|
const ws = own.workspace;
|
|
1264
|
+
if (opts.workspace && ws !== opts.workspace) {
|
|
1265
|
+
throw new SyncStopped(
|
|
1266
|
+
"the key is for another workspace",
|
|
1267
|
+
`The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1192
1270
|
const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
|
|
1193
1271
|
if (!project) {
|
|
1194
1272
|
throw new SyncFailed(
|
|
@@ -1202,6 +1280,12 @@ async function syncMemory(opts) {
|
|
|
1202
1280
|
const date = localDate(now);
|
|
1203
1281
|
const stored = (await readState(ws)).memory?.[project];
|
|
1204
1282
|
let team = await pullTeam(client, ws, project);
|
|
1283
|
+
if (opts.unattended && stored && !team.exists) {
|
|
1284
|
+
throw new SyncStopped(
|
|
1285
|
+
"the team memory could not be read",
|
|
1286
|
+
`${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1205
1289
|
let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
|
|
1206
1290
|
const folder = await readFolder(dir);
|
|
1207
1291
|
const shared = {};
|
|
@@ -1214,14 +1298,16 @@ async function syncMemory(opts) {
|
|
|
1214
1298
|
} else shared[path] = text3;
|
|
1215
1299
|
}
|
|
1216
1300
|
const importWrites = {};
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1301
|
+
const current = { ...folder.files };
|
|
1302
|
+
const taken = new Set(folder.names);
|
|
1303
|
+
for (const importDir of opts.importDirs ?? []) {
|
|
1304
|
+
const other = await readFolder(resolve(importDir));
|
|
1220
1305
|
for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
|
|
1221
1306
|
if (!isShareable(path, text3) || !isFilePath(path)) continue;
|
|
1222
|
-
const here =
|
|
1307
|
+
const here = current[path];
|
|
1223
1308
|
if (here === void 0) {
|
|
1224
1309
|
shared[path] = text3;
|
|
1310
|
+
current[path] = text3;
|
|
1225
1311
|
importWrites[path] = text3;
|
|
1226
1312
|
taken.add(path.toLowerCase());
|
|
1227
1313
|
} else if (here !== text3) {
|
|
@@ -1229,17 +1315,15 @@ async function syncMemory(opts) {
|
|
|
1229
1315
|
taken.add(copy.toLowerCase());
|
|
1230
1316
|
importWrites[copy] = text3;
|
|
1231
1317
|
report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
folder.names = taken;
|
|
1235
|
-
if (!opts.dryRun) {
|
|
1236
|
-
for (const [path, text3] of Object.entries(importWrites)) {
|
|
1237
|
-
const full = toPath(dir, path);
|
|
1238
|
-
await mkdir2(dirname2(full), { recursive: true });
|
|
1239
|
-
await writeFile2(full, text3, "utf8");
|
|
1318
|
+
report.copies.add(copy);
|
|
1240
1319
|
}
|
|
1241
1320
|
}
|
|
1242
1321
|
}
|
|
1322
|
+
folder.names = taken;
|
|
1323
|
+
if (!opts.dryRun) {
|
|
1324
|
+
checkDeadline();
|
|
1325
|
+
for (const [path, text3] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text3);
|
|
1326
|
+
}
|
|
1243
1327
|
const found = await people(run3, ws, shared, report, hint);
|
|
1244
1328
|
for (const path of found.leftOut) {
|
|
1245
1329
|
delete found.files[path];
|
|
@@ -1263,12 +1347,16 @@ async function syncMemory(opts) {
|
|
|
1263
1347
|
);
|
|
1264
1348
|
return 0;
|
|
1265
1349
|
}
|
|
1350
|
+
checkDeadline();
|
|
1351
|
+
if (attempt === 1) await removeLeftTemps(dir, folder.temps);
|
|
1266
1352
|
await applyPlan(dir, plan);
|
|
1267
1353
|
let version = team.version;
|
|
1268
1354
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1355
|
+
checkDeadline();
|
|
1269
1356
|
await upload(client, ws, project, plan, team.manifest, report);
|
|
1270
1357
|
}
|
|
1271
1358
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1359
|
+
checkDeadline();
|
|
1272
1360
|
try {
|
|
1273
1361
|
version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
|
|
1274
1362
|
} catch (e) {
|
|
@@ -1280,6 +1368,7 @@ async function syncMemory(opts) {
|
|
|
1280
1368
|
false
|
|
1281
1369
|
)
|
|
1282
1370
|
);
|
|
1371
|
+
opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
|
|
1283
1372
|
return 2;
|
|
1284
1373
|
}
|
|
1285
1374
|
base = team;
|
|
@@ -1304,18 +1393,197 @@ async function syncMemory(opts) {
|
|
|
1304
1393
|
false
|
|
1305
1394
|
)
|
|
1306
1395
|
);
|
|
1396
|
+
opts.summary?.(report.summary("synced", version));
|
|
1307
1397
|
return 0;
|
|
1308
1398
|
}
|
|
1309
1399
|
} catch (e) {
|
|
1310
1400
|
out(e.message);
|
|
1401
|
+
opts.summary?.({
|
|
1402
|
+
outcome: "failed",
|
|
1403
|
+
reason: failureReason(e),
|
|
1404
|
+
version: 0,
|
|
1405
|
+
pulled: 0,
|
|
1406
|
+
pushed: 0,
|
|
1407
|
+
deleted: 0,
|
|
1408
|
+
conflictCopies: [],
|
|
1409
|
+
waiting: []
|
|
1410
|
+
});
|
|
1311
1411
|
return 1;
|
|
1312
1412
|
}
|
|
1313
1413
|
}
|
|
1414
|
+
function hookLines(ws, project, folder, s) {
|
|
1415
|
+
if (s.outcome !== "synced") {
|
|
1416
|
+
return [`Team memory ${ws}/${project}: not synced (${s.reason ?? "it did not finish"}); your files are unchanged and the next session tries again.`];
|
|
1417
|
+
}
|
|
1418
|
+
const lines = [`Team memory ${ws}/${project}: version ${s.version}, ${s.pulled} pulled, ${s.pushed} pushed, ${s.deleted} deleted.`];
|
|
1419
|
+
if (s.conflictCopies.length) {
|
|
1420
|
+
lines.push(` ${plural(s.conflictCopies.length, "conflict copy", "conflict copies")} to tidy in ${folder}: ${s.conflictCopies.join(", ")}`);
|
|
1421
|
+
}
|
|
1422
|
+
if (s.waiting.length) {
|
|
1423
|
+
lines.push(
|
|
1424
|
+
` ${plural(s.waiting.length, "file waits", "files wait")} for you: run \`${SYNC_MEMORY_COMMAND} --project ${project}\` in your terminal.`
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
return lines;
|
|
1428
|
+
}
|
|
1429
|
+
async function appendLog(ws, lines) {
|
|
1430
|
+
const path = syncLogPath(ws);
|
|
1431
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
1432
|
+
try {
|
|
1433
|
+
if ((await stat(path)).size > HOOK_LOG_MAX_BYTES) await writeFile2(path, "", "utf8");
|
|
1434
|
+
} catch {
|
|
1435
|
+
}
|
|
1436
|
+
await appendFile(path, lines.join("\n") + "\n", "utf8");
|
|
1437
|
+
}
|
|
1438
|
+
async function takeLock(lock, staleMs, now) {
|
|
1439
|
+
await mkdir2(dirname2(lock), { recursive: true });
|
|
1440
|
+
const body = JSON.stringify({ pid: process.pid, at: now.toISOString() }) + "\n";
|
|
1441
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1442
|
+
try {
|
|
1443
|
+
const handle = await open(lock, "wx");
|
|
1444
|
+
try {
|
|
1445
|
+
await handle.writeFile(body, "utf8");
|
|
1446
|
+
} catch (err) {
|
|
1447
|
+
await handle.close();
|
|
1448
|
+
await rm2(lock, { force: true });
|
|
1449
|
+
throw err;
|
|
1450
|
+
}
|
|
1451
|
+
await handle.close();
|
|
1452
|
+
return true;
|
|
1453
|
+
} catch (e) {
|
|
1454
|
+
if (e.code !== "EEXIST") throw e;
|
|
1455
|
+
let seen;
|
|
1456
|
+
try {
|
|
1457
|
+
seen = await stat(lock);
|
|
1458
|
+
} catch {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
if (Date.now() - seen.mtimeMs < staleMs || attempt === 1) return false;
|
|
1462
|
+
try {
|
|
1463
|
+
const now2 = await stat(lock);
|
|
1464
|
+
if (now2.ino !== seen.ino || now2.mtimeMs !== seen.mtimeMs) return false;
|
|
1465
|
+
} catch {
|
|
1466
|
+
continue;
|
|
1467
|
+
}
|
|
1468
|
+
await rm2(lock, { force: true });
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
return false;
|
|
1472
|
+
}
|
|
1473
|
+
async function releaseLock(lock) {
|
|
1474
|
+
try {
|
|
1475
|
+
const held = JSON.parse(await readFile2(lock, "utf8"));
|
|
1476
|
+
if (held.pid === process.pid) await rm2(lock, { force: true });
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
async function runHookSync(workspace, io, env = process.env) {
|
|
1481
|
+
try {
|
|
1482
|
+
return await hookSync(workspace, io, env);
|
|
1483
|
+
} catch (e) {
|
|
1484
|
+
try {
|
|
1485
|
+
io.err(`sync-memory --hook: ${e.message}`);
|
|
1486
|
+
} catch {
|
|
1487
|
+
}
|
|
1488
|
+
return 0;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
async function hookSync(workspace, io, env) {
|
|
1492
|
+
if (!isWorkspace(workspace)) {
|
|
1493
|
+
io.err(`sync-memory --hook: ${JSON.stringify(workspace)} is not a workspace name`);
|
|
1494
|
+
return 0;
|
|
1495
|
+
}
|
|
1496
|
+
const now = io.now?.() ?? /* @__PURE__ */ new Date();
|
|
1497
|
+
const deadline = (io.startedAt ?? Date.now()) + (io.budgetMs ?? HOOK_BUDGET_MS);
|
|
1498
|
+
let client = io.client;
|
|
1499
|
+
if (!client) {
|
|
1500
|
+
let apiKey;
|
|
1501
|
+
let apiUrl;
|
|
1502
|
+
try {
|
|
1503
|
+
apiKey = await apiKeyFor(workspace, env);
|
|
1504
|
+
apiUrl = apiUrlFrom(env);
|
|
1505
|
+
} catch {
|
|
1506
|
+
return 0;
|
|
1507
|
+
}
|
|
1508
|
+
if (!apiKey) return 0;
|
|
1509
|
+
client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1510
|
+
}
|
|
1511
|
+
let projects;
|
|
1512
|
+
try {
|
|
1513
|
+
projects = Object.keys((await readState(workspace)).memory ?? {}).sort();
|
|
1514
|
+
} catch {
|
|
1515
|
+
return 0;
|
|
1516
|
+
}
|
|
1517
|
+
if (!projects.length) return 0;
|
|
1518
|
+
const lock = syncLockPath(workspace);
|
|
1519
|
+
if (!await takeLock(lock, io.lockStaleMs ?? HOOK_LOCK_STALE_MS, now)) {
|
|
1520
|
+
io.out(`Team memory ${workspace}: a sync is already running.`);
|
|
1521
|
+
return 0;
|
|
1522
|
+
}
|
|
1523
|
+
try {
|
|
1524
|
+
for (const project of projects) {
|
|
1525
|
+
const folder = join2(workspaceDir(workspace), "memory", project);
|
|
1526
|
+
const report = [];
|
|
1527
|
+
let summary;
|
|
1528
|
+
const left = deadline - Date.now();
|
|
1529
|
+
if (left > 0) {
|
|
1530
|
+
let timer;
|
|
1531
|
+
const timeout = new Promise((resolveTimeout) => {
|
|
1532
|
+
timer = setTimeout(() => resolveTimeout("timeout"), left);
|
|
1533
|
+
});
|
|
1534
|
+
const run3 = syncMemory({
|
|
1535
|
+
project,
|
|
1536
|
+
dryRun: false,
|
|
1537
|
+
client,
|
|
1538
|
+
workspace,
|
|
1539
|
+
deadline,
|
|
1540
|
+
unattended: true,
|
|
1541
|
+
out: (...lines2) => report.push(...lines2),
|
|
1542
|
+
summary: (s) => summary = s,
|
|
1543
|
+
now: io.now
|
|
1544
|
+
});
|
|
1545
|
+
const won = await Promise.race([run3.then(() => "done"), timeout]);
|
|
1546
|
+
if (timer) clearTimeout(timer);
|
|
1547
|
+
if (won === "timeout") summary = void 0;
|
|
1548
|
+
}
|
|
1549
|
+
const result = summary ?? {
|
|
1550
|
+
outcome: "failed",
|
|
1551
|
+
reason: "took too long",
|
|
1552
|
+
version: 0,
|
|
1553
|
+
pulled: 0,
|
|
1554
|
+
pushed: 0,
|
|
1555
|
+
deleted: 0,
|
|
1556
|
+
conflictCopies: [],
|
|
1557
|
+
waiting: []
|
|
1558
|
+
};
|
|
1559
|
+
const lines = hookLines(workspace, project, folder, result);
|
|
1560
|
+
lines.forEach((l) => io.out(l));
|
|
1561
|
+
try {
|
|
1562
|
+
await appendLog(workspace, [`--- ${now.toISOString()} ${workspace}/${project}`, ...lines, ...report]);
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
io.err(`sync-memory --hook: the log could not be written: ${e.message}`);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
} finally {
|
|
1568
|
+
await releaseLock(lock);
|
|
1569
|
+
}
|
|
1570
|
+
return 0;
|
|
1571
|
+
}
|
|
1314
1572
|
async function runSyncMemory(argv, io, env = process.env) {
|
|
1315
1573
|
let args2;
|
|
1316
1574
|
let client = io.client;
|
|
1317
1575
|
try {
|
|
1318
1576
|
args2 = parseSyncMemoryArgs(argv);
|
|
1577
|
+
} catch (e) {
|
|
1578
|
+
if (argv.includes("--hook")) {
|
|
1579
|
+
io.err(`sync-memory --hook: ${e.message.split("\n")[0]}`);
|
|
1580
|
+
return 0;
|
|
1581
|
+
}
|
|
1582
|
+
io.err(e.message);
|
|
1583
|
+
return 1;
|
|
1584
|
+
}
|
|
1585
|
+
if (args2.hook) return runHookSync(args2.hook, io, env);
|
|
1586
|
+
try {
|
|
1319
1587
|
if (!client) {
|
|
1320
1588
|
const apiUrl = apiUrlFrom(env);
|
|
1321
1589
|
const apiKey = await localApiKey(env);
|
|
@@ -1327,7 +1595,7 @@ async function runSyncMemory(argv, io, env = process.env) {
|
|
|
1327
1595
|
}
|
|
1328
1596
|
return syncMemory({ ...args2, client, out: (...lines) => lines.forEach((l) => io.out(l)), ask: io.ask, now: io.now });
|
|
1329
1597
|
}
|
|
1330
|
-
var SYNC_MEMORY_USAGE, SYNC_MEMORY_COMMAND, SyncFailed, toPath, Report, ANSWER_WORDS, sameManifest;
|
|
1598
|
+
var SYNC_MEMORY_USAGE, HOOK_BUDGET_MS, HOOK_LOCK_STALE_MS, HOOK_LOG_MAX_BYTES, SYNC_MEMORY_COMMAND, SyncFailed, SyncStopped, toPath, LEFT_TEMP_RE, Report, ANSWER_WORDS, sameManifest, plural;
|
|
1331
1599
|
var init_sync_memory = __esm({
|
|
1332
1600
|
"src/sync-memory.ts"() {
|
|
1333
1601
|
"use strict";
|
|
@@ -1335,18 +1603,33 @@ var init_sync_memory = __esm({
|
|
|
1335
1603
|
init_memory_sync();
|
|
1336
1604
|
init_person_values();
|
|
1337
1605
|
init_team_setup_files();
|
|
1338
|
-
SYNC_MEMORY_USAGE = "Usage: systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>] [--import <folder>] [--dry-run]";
|
|
1606
|
+
SYNC_MEMORY_USAGE = "Usage: systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>] [--import <folder>]... [--dry-run]\n systemdox-mcp-server sync-memory --hook <workspace>";
|
|
1607
|
+
HOOK_BUDGET_MS = 2e4;
|
|
1608
|
+
HOOK_LOCK_STALE_MS = 6e4;
|
|
1609
|
+
HOOK_LOG_MAX_BYTES = 1024 * 1024;
|
|
1339
1610
|
SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
|
|
1340
1611
|
SyncFailed = class extends Error {
|
|
1341
1612
|
};
|
|
1613
|
+
SyncStopped = class extends Error {
|
|
1614
|
+
constructor(reason, message) {
|
|
1615
|
+
super(message);
|
|
1616
|
+
this.reason = reason;
|
|
1617
|
+
}
|
|
1618
|
+
reason;
|
|
1619
|
+
};
|
|
1342
1620
|
toPath = (root, rel) => join2(root, ...rel.split("/"));
|
|
1621
|
+
LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
|
|
1343
1622
|
Report = class {
|
|
1344
1623
|
pulled = /* @__PURE__ */ new Set();
|
|
1345
1624
|
pushed = /* @__PURE__ */ new Set();
|
|
1346
1625
|
deletedHere = /* @__PURE__ */ new Set();
|
|
1347
1626
|
deletedFromTeam = /* @__PURE__ */ new Set();
|
|
1348
1627
|
conflicts = [];
|
|
1628
|
+
/** The conflict copies written this run, by name. */
|
|
1629
|
+
copies = /* @__PURE__ */ new Set();
|
|
1349
1630
|
skipped = /* @__PURE__ */ new Map();
|
|
1631
|
+
/** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
|
|
1632
|
+
waiting = /* @__PURE__ */ new Set();
|
|
1350
1633
|
people = [];
|
|
1351
1634
|
toAnswer = /* @__PURE__ */ new Map();
|
|
1352
1635
|
add(plan) {
|
|
@@ -1354,12 +1637,32 @@ var init_sync_memory = __esm({
|
|
|
1354
1637
|
plan.pushed.forEach((p) => this.pushed.add(p));
|
|
1355
1638
|
plan.deletedHere.forEach((p) => this.deletedHere.add(p));
|
|
1356
1639
|
plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
|
|
1357
|
-
for (const c of plan.conflicts)
|
|
1640
|
+
for (const c of plan.conflicts) {
|
|
1641
|
+
this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
|
|
1642
|
+
this.copies.add(c.copy);
|
|
1643
|
+
}
|
|
1358
1644
|
for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
|
|
1359
1645
|
}
|
|
1360
1646
|
skip(path, reason) {
|
|
1361
1647
|
if (!this.skipped.has(path)) this.skipped.set(path, reason);
|
|
1362
1648
|
}
|
|
1649
|
+
/** Skipped, and waiting for the member at a terminal. */
|
|
1650
|
+
wait(path, reason) {
|
|
1651
|
+
this.skip(path, reason);
|
|
1652
|
+
this.waiting.add(path);
|
|
1653
|
+
}
|
|
1654
|
+
summary(outcome, version, reason) {
|
|
1655
|
+
return {
|
|
1656
|
+
outcome,
|
|
1657
|
+
...reason ? { reason } : {},
|
|
1658
|
+
version,
|
|
1659
|
+
pulled: this.pulled.size,
|
|
1660
|
+
pushed: this.pushed.size,
|
|
1661
|
+
deleted: this.deletedHere.size + this.deletedFromTeam.size,
|
|
1662
|
+
conflictCopies: [...this.copies].sort(),
|
|
1663
|
+
waiting: [...this.waiting].sort()
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1363
1666
|
lines(summary, dryRun) {
|
|
1364
1667
|
const out = [summary];
|
|
1365
1668
|
const section = (title, items) => {
|
|
@@ -1398,6 +1701,7 @@ var init_sync_memory = __esm({
|
|
|
1398
1701
|
ours: "ours, not a person"
|
|
1399
1702
|
};
|
|
1400
1703
|
sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
|
|
1704
|
+
plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
1401
1705
|
}
|
|
1402
1706
|
});
|
|
1403
1707
|
|
|
@@ -1407,7 +1711,11 @@ __export(setup_team_exports, {
|
|
|
1407
1711
|
HEADERS_HELPER: () => HEADERS_HELPER,
|
|
1408
1712
|
SETUP_USAGE: () => SETUP_USAGE,
|
|
1409
1713
|
claudeCodeVersion: () => claudeCodeVersion,
|
|
1714
|
+
claudeMemoryFolder: () => claudeMemoryFolder,
|
|
1715
|
+
countMemoryFiles: () => countMemoryFiles,
|
|
1716
|
+
gitTopFolder: () => gitTopFolder,
|
|
1410
1717
|
importBlock: () => importBlock,
|
|
1718
|
+
memoryImports: () => memoryImports,
|
|
1411
1719
|
parseSetupArgs: () => parseSetupArgs,
|
|
1412
1720
|
runTeamSetup: () => runTeamSetup,
|
|
1413
1721
|
runTeamSetupCommand: () => runTeamSetupCommand,
|
|
@@ -1422,19 +1730,20 @@ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, writeFile
|
|
|
1422
1730
|
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
1423
1731
|
import { promisify as promisify2 } from "util";
|
|
1424
1732
|
function parseSetupArgs(argv) {
|
|
1425
|
-
const args2 = { yes: false, overwrite: false };
|
|
1733
|
+
const args2 = { yes: false, overwrite: false, importDirs: [] };
|
|
1426
1734
|
for (let i = 0; i < argv.length; i++) {
|
|
1427
1735
|
const a = argv[i];
|
|
1428
1736
|
if (a === "--yes") args2.yes = true;
|
|
1429
1737
|
else if (a === "--overwrite") args2.overwrite = true;
|
|
1430
|
-
else if (a === "--project" || a === "--dir") {
|
|
1738
|
+
else if (a === "--project" || a === "--dir" || a === "--import") {
|
|
1431
1739
|
const value = argv[++i];
|
|
1432
1740
|
if (!value || value.startsWith("--")) throw new Error(`${a} needs a value
|
|
1433
1741
|
${SETUP_USAGE}`);
|
|
1434
1742
|
if (a === "--project") {
|
|
1435
1743
|
if (!isWorkspace(value)) throw new Error(`${value} is not a project name (lower case letters, digits and -)`);
|
|
1436
1744
|
args2.project = value;
|
|
1437
|
-
} else args2.dir = value;
|
|
1745
|
+
} else if (a === "--dir") args2.dir = value;
|
|
1746
|
+
else args2.importDirs.push(value);
|
|
1438
1747
|
} else throw new Error(`Unexpected argument ${a}
|
|
1439
1748
|
${SETUP_USAGE}`);
|
|
1440
1749
|
}
|
|
@@ -1452,6 +1761,60 @@ async function claudeCodeVersion() {
|
|
|
1452
1761
|
return void 0;
|
|
1453
1762
|
}
|
|
1454
1763
|
}
|
|
1764
|
+
async function gitTopFolder(dir) {
|
|
1765
|
+
try {
|
|
1766
|
+
const { stdout } = await run2("git", ["-C", dir, "rev-parse", "--show-toplevel"], { timeout: 1e4, windowsHide: true });
|
|
1767
|
+
const top = stdout.trim();
|
|
1768
|
+
return top ? resolve2(top) : void 0;
|
|
1769
|
+
} catch {
|
|
1770
|
+
return void 0;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function claudeMemoryFolder(path) {
|
|
1774
|
+
return join3(homeDir(), ".claude", "projects", path.replace(/[^A-Za-z0-9]/g, "-"), "memory");
|
|
1775
|
+
}
|
|
1776
|
+
async function countMemoryFiles(folder) {
|
|
1777
|
+
let count = 0;
|
|
1778
|
+
async function walk2(dir, prefix) {
|
|
1779
|
+
let entries2;
|
|
1780
|
+
try {
|
|
1781
|
+
entries2 = await readdir2(dir, { withFileTypes: true });
|
|
1782
|
+
} catch {
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
for (const e of entries2) {
|
|
1786
|
+
if (e.name.startsWith(".")) continue;
|
|
1787
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
1788
|
+
if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
|
|
1789
|
+
else if (e.isFile() && e.name.endsWith(".md")) {
|
|
1790
|
+
try {
|
|
1791
|
+
if (isShareable(rel, await readFile3(join3(dir, e.name), "utf8"))) count++;
|
|
1792
|
+
} catch {
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
await walk2(folder, "");
|
|
1798
|
+
return count;
|
|
1799
|
+
}
|
|
1800
|
+
async function memoryImports(checkout, teamFolder, named, gitTop = gitTopFolder) {
|
|
1801
|
+
const candidates = [resolve2(checkout)];
|
|
1802
|
+
const top = await gitTop(checkout);
|
|
1803
|
+
if (top && !candidates.includes(top)) candidates.push(top);
|
|
1804
|
+
const out = [];
|
|
1805
|
+
for (const path of candidates) {
|
|
1806
|
+
const folder = claudeMemoryFolder(path);
|
|
1807
|
+
if (resolve2(folder) === resolve2(teamFolder)) continue;
|
|
1808
|
+
const files = await countMemoryFiles(folder);
|
|
1809
|
+
if (files) out.push({ folder, files, named: false });
|
|
1810
|
+
}
|
|
1811
|
+
for (const dir of named) {
|
|
1812
|
+
const folder = resolve2(dir);
|
|
1813
|
+
if (out.some((i) => i.folder === folder)) continue;
|
|
1814
|
+
out.push({ folder, files: await countMemoryFiles(folder), named: true });
|
|
1815
|
+
}
|
|
1816
|
+
return out;
|
|
1817
|
+
}
|
|
1455
1818
|
function parseVersion(text3) {
|
|
1456
1819
|
const m = text3?.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
1457
1820
|
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : void 0;
|
|
@@ -1493,9 +1856,9 @@ function skipValue(t, i) {
|
|
|
1493
1856
|
while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
|
|
1494
1857
|
return i;
|
|
1495
1858
|
}
|
|
1496
|
-
function objectMembers(t,
|
|
1859
|
+
function objectMembers(t, open2) {
|
|
1497
1860
|
const members = [];
|
|
1498
|
-
let i = skipSpace(t,
|
|
1861
|
+
let i = skipSpace(t, open2 + 1);
|
|
1499
1862
|
if (t[i] === "}") return { members, close: i };
|
|
1500
1863
|
for (; ; ) {
|
|
1501
1864
|
i = skipSpace(t, i);
|
|
@@ -1522,13 +1885,13 @@ function setJsonMember(text3, keys, value) {
|
|
|
1522
1885
|
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
1523
1886
|
const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
|
|
1524
1887
|
const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
|
|
1525
|
-
let
|
|
1888
|
+
let open2 = skipSpace(text3, 0);
|
|
1526
1889
|
for (let k = 0; k < keys.length; k++) {
|
|
1527
|
-
const { members, close } = objectMembers(text3,
|
|
1890
|
+
const { members, close } = objectMembers(text3, open2);
|
|
1528
1891
|
const found = members.filter((m) => m.key === keys[k]).at(-1);
|
|
1529
1892
|
if (found) {
|
|
1530
1893
|
if (k < keys.length - 1 && text3[found.valueStart] === "{") {
|
|
1531
|
-
|
|
1894
|
+
open2 = found.valueStart;
|
|
1532
1895
|
continue;
|
|
1533
1896
|
}
|
|
1534
1897
|
return text3.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text3, found.keyStart)) + text3.slice(found.valueEnd);
|
|
@@ -1537,16 +1900,16 @@ function setJsonMember(text3, keys, value) {
|
|
|
1537
1900
|
if (compact) {
|
|
1538
1901
|
const member = `${name}:${render(nest(k + 1), "")}`;
|
|
1539
1902
|
if (members.length) return text3.slice(0, members.at(-1).valueEnd) + "," + member + text3.slice(members.at(-1).valueEnd);
|
|
1540
|
-
return text3.slice(0,
|
|
1903
|
+
return text3.slice(0, open2 + 1) + member + text3.slice(close);
|
|
1541
1904
|
}
|
|
1542
1905
|
if (members.length) {
|
|
1543
1906
|
const last = members.at(-1);
|
|
1544
1907
|
const indent2 = lineIndent(text3, last.keyStart);
|
|
1545
1908
|
return text3.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text3.slice(last.valueEnd);
|
|
1546
1909
|
}
|
|
1547
|
-
const outer = lineIndent(text3,
|
|
1910
|
+
const outer = lineIndent(text3, open2);
|
|
1548
1911
|
const indent = outer + unit;
|
|
1549
|
-
return text3.slice(0,
|
|
1912
|
+
return text3.slice(0, open2 + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text3.slice(close);
|
|
1550
1913
|
}
|
|
1551
1914
|
return text3;
|
|
1552
1915
|
}
|
|
@@ -1592,9 +1955,9 @@ function importBlock(workspace, eol = "\n") {
|
|
|
1592
1955
|
function withImportBlock(text3, workspace) {
|
|
1593
1956
|
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
1594
1957
|
const block = importBlock(workspace, eol);
|
|
1595
|
-
const
|
|
1958
|
+
const open2 = `<!-- systemdox:${workspace} -->`;
|
|
1596
1959
|
const close = `<!-- /systemdox:${workspace} -->`;
|
|
1597
|
-
const start = text3.indexOf(
|
|
1960
|
+
const start = text3.indexOf(open2);
|
|
1598
1961
|
const end = start >= 0 ? text3.indexOf(close, start) : -1;
|
|
1599
1962
|
if (start >= 0 && end >= 0) return text3.slice(0, start) + block + text3.slice(end + close.length);
|
|
1600
1963
|
if (!text3) return block + eol;
|
|
@@ -1657,12 +2020,12 @@ async function runTeamSetup(opts) {
|
|
|
1657
2020
|
const plugin = own.plugin;
|
|
1658
2021
|
let previousPlugin;
|
|
1659
2022
|
if (plugin) {
|
|
1660
|
-
const hooks =
|
|
2023
|
+
const hooks = hookLines2(plugin);
|
|
1661
2024
|
const hooksName = `the hook commands of ${ws}-setup`;
|
|
1662
2025
|
const hooksItem = item(hooksName, hooks);
|
|
1663
2026
|
if (update && hooks.length && !accepted.has(hooksItem.sha256)) {
|
|
1664
2027
|
const versions = (await client.getPlugin(ws)).versions.filter((v) => v.version !== plugin.version);
|
|
1665
|
-
previousPlugin = versions.sort((a, b) => b.version - a.version).find((v) => accepted.has(item(hooksName,
|
|
2028
|
+
previousPlugin = versions.sort((a, b) => b.version - a.version).find((v) => accepted.has(item(hooksName, hookLines2(v)).sha256));
|
|
1666
2029
|
}
|
|
1667
2030
|
items.push(item(`the plugin ${ws}-setup, version ${plugin.version}`, [`${ws}-setup, version ${plugin.version}`]));
|
|
1668
2031
|
if (hooks.length) {
|
|
@@ -1670,7 +2033,7 @@ async function runTeamSetup(opts) {
|
|
|
1670
2033
|
item(
|
|
1671
2034
|
hooksName,
|
|
1672
2035
|
hooks,
|
|
1673
|
-
previousPlugin ? lineDiff(
|
|
2036
|
+
previousPlugin ? lineDiff(hookLines2(previousPlugin).join("\n"), hooks.join("\n")) : void 0
|
|
1674
2037
|
)
|
|
1675
2038
|
);
|
|
1676
2039
|
}
|
|
@@ -1710,6 +2073,15 @@ async function runTeamSetup(opts) {
|
|
|
1710
2073
|
}
|
|
1711
2074
|
items.push(item(`autoMemoryDirectory in ${localSettings}`, [memoryFolder]));
|
|
1712
2075
|
}
|
|
2076
|
+
const imports = project && memoryFolder && !state.memory?.[project] ? await memoryImports(checkout, memoryFolder, opts.importDirs ?? [], opts.gitTop) : [];
|
|
2077
|
+
for (const i of imports) {
|
|
2078
|
+
items.push(
|
|
2079
|
+
item(
|
|
2080
|
+
`${i.named ? "the folder --import names" : "the memories Claude Code kept for this checkout"}, imported into ${project} on its first sync`,
|
|
2081
|
+
[`${i.folder}: ${i.files} file${i.files === 1 ? "" : "s"} to share`]
|
|
2082
|
+
)
|
|
2083
|
+
);
|
|
2084
|
+
}
|
|
1713
2085
|
const pending = items.filter((i) => !accepted.has(i.sha256));
|
|
1714
2086
|
if (pending.length) {
|
|
1715
2087
|
if (!canAsk) {
|
|
@@ -1779,6 +2151,7 @@ async function runTeamSetup(opts) {
|
|
|
1779
2151
|
const code = await syncMemory({
|
|
1780
2152
|
project,
|
|
1781
2153
|
dryRun: false,
|
|
2154
|
+
importDirs: imports.map((i) => i.folder),
|
|
1782
2155
|
client,
|
|
1783
2156
|
out: (...lines) => lines.forEach((l) => out(l)),
|
|
1784
2157
|
ask: opts.terminal ? opts.ask : void 0,
|
|
@@ -1819,7 +2192,7 @@ async function runTeamSetupCommand(args2, key, io) {
|
|
|
1819
2192
|
const client = new SystemDoxApiClient({ apiKey: key.apiKey, apiUrl: key.apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1820
2193
|
return runTeamSetup({ ...args2, client, apiKey: key.apiKey, keySource: "prompt", ...io });
|
|
1821
2194
|
}
|
|
1822
|
-
var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item,
|
|
2195
|
+
var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item, hookLines2, mcpLines;
|
|
1823
2196
|
var init_setup_team = __esm({
|
|
1824
2197
|
"src/setup-team.ts"() {
|
|
1825
2198
|
"use strict";
|
|
@@ -1827,7 +2200,7 @@ var init_setup_team = __esm({
|
|
|
1827
2200
|
init_memory_sync();
|
|
1828
2201
|
init_team_setup_files();
|
|
1829
2202
|
init_sync_memory();
|
|
1830
|
-
SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]";
|
|
2203
|
+
SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]";
|
|
1831
2204
|
HEADERS_HELPER = "npx -y @systemdox/mcp-server marketplace-auth";
|
|
1832
2205
|
SetupFailed = class extends Error {
|
|
1833
2206
|
};
|
|
@@ -1839,7 +2212,7 @@ var init_setup_team = __esm({
|
|
|
1839
2212
|
sha256: sha2562(`${name}
|
|
1840
2213
|
${lines.join("\n")}`)
|
|
1841
2214
|
});
|
|
1842
|
-
|
|
2215
|
+
hookLines2 = (p) => p.hookCommands ?? [];
|
|
1843
2216
|
mcpLines = (p) => (p.mcpServers ?? []).map((s) => `${s.name}: ${s.url ?? [s.command, ...s.args ?? []].filter(Boolean).join(" ")}`);
|
|
1844
2217
|
}
|
|
1845
2218
|
});
|
|
@@ -2326,7 +2699,7 @@ __export(plugin_path_exports, {
|
|
|
2326
2699
|
});
|
|
2327
2700
|
import { createHash as createHash4, randomBytes } from "crypto";
|
|
2328
2701
|
import { existsSync as existsSync5 } from "fs";
|
|
2329
|
-
import { mkdir as mkdir6, rename as
|
|
2702
|
+
import { mkdir as mkdir6, rename as rename3, writeFile as writeFile5 } from "fs/promises";
|
|
2330
2703
|
import { dirname as dirname5, join as join6, resolve as resolve5, sep as sep2 } from "path";
|
|
2331
2704
|
import { inflateRawSync } from "zlib";
|
|
2332
2705
|
function parsePluginPathArgs(argv) {
|
|
@@ -2494,7 +2867,7 @@ async function runPluginPath(argv, io) {
|
|
|
2494
2867
|
} else {
|
|
2495
2868
|
if (existsSync5(folder)) await removeFolder(folder);
|
|
2496
2869
|
try {
|
|
2497
|
-
await
|
|
2870
|
+
await rename3(tmp, folder);
|
|
2498
2871
|
} catch (e) {
|
|
2499
2872
|
if (!existsSync5(folder)) throw e;
|
|
2500
2873
|
await removeFolder(tmp);
|
|
@@ -3315,8 +3688,8 @@ function registerGetPlanBoard(server, apiClient2) {
|
|
|
3315
3688
|
]
|
|
3316
3689
|
};
|
|
3317
3690
|
}
|
|
3318
|
-
const
|
|
3319
|
-
if (
|
|
3691
|
+
const open2 = boards.filter((b) => !b.closed);
|
|
3692
|
+
if (open2.length === 0) {
|
|
3320
3693
|
return {
|
|
3321
3694
|
content: [
|
|
3322
3695
|
{
|
|
@@ -3326,16 +3699,16 @@ function registerGetPlanBoard(server, apiClient2) {
|
|
|
3326
3699
|
]
|
|
3327
3700
|
};
|
|
3328
3701
|
}
|
|
3329
|
-
const closedNote = boards.length >
|
|
3702
|
+
const closedNote = boards.length > open2.length ? `
|
|
3330
3703
|
|
|
3331
|
-
_${boards.length -
|
|
3704
|
+
_${boards.length - open2.length} closed board(s) not listed._` : "";
|
|
3332
3705
|
return {
|
|
3333
3706
|
content: [
|
|
3334
3707
|
{
|
|
3335
3708
|
type: "text",
|
|
3336
|
-
text: `${
|
|
3709
|
+
text: `${open2.length} open project board(s):
|
|
3337
3710
|
|
|
3338
|
-
${formatBoards(
|
|
3711
|
+
${formatBoards(open2)}` + closedNote + `
|
|
3339
3712
|
|
|
3340
3713
|
Call get_plan_board with project_number to list a board's issues.`
|
|
3341
3714
|
}
|
|
@@ -5295,7 +5668,9 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
|
5295
5668
|
project: args2.project,
|
|
5296
5669
|
dryRun: !!args2.dryRun,
|
|
5297
5670
|
client: apiClient2,
|
|
5298
|
-
out: (...l) => lines.push(...l)
|
|
5671
|
+
out: (...l) => lines.push(...l),
|
|
5672
|
+
// Nobody is at a terminal: a project the team no longer serves is left as it is.
|
|
5673
|
+
unattended: true
|
|
5299
5674
|
});
|
|
5300
5675
|
return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
|
|
5301
5676
|
}
|
|
@@ -5383,7 +5758,7 @@ var init_server = __esm({
|
|
|
5383
5758
|
init_create_prompt_template();
|
|
5384
5759
|
init_skills2();
|
|
5385
5760
|
init_team_setup();
|
|
5386
|
-
SERVER_VERSION = true ? "0.1.
|
|
5761
|
+
SERVER_VERSION = true ? "0.1.58" : "0.0.0-dev";
|
|
5387
5762
|
READ_TOOLS = [
|
|
5388
5763
|
"get_context",
|
|
5389
5764
|
"get_checks",
|
|
@@ -5512,21 +5887,26 @@ if (command === "setup") {
|
|
|
5512
5887
|
({ runPluginPath: runPluginPath2 }) => runPluginPath2(args.slice(1), { out: console.log, err: console.error })
|
|
5513
5888
|
).then((code) => process.exit(code));
|
|
5514
5889
|
} else if (command === "sync-memory") {
|
|
5515
|
-
const
|
|
5890
|
+
const hook = args.includes("--hook");
|
|
5891
|
+
const terminal = !hook && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
5516
5892
|
Promise.resolve().then(() => (init_sync_memory(), sync_memory_exports)).then(
|
|
5517
5893
|
({ runSyncMemory: runSyncMemory2 }) => runSyncMemory2(args.slice(1), {
|
|
5518
5894
|
out: console.log,
|
|
5519
5895
|
err: console.error,
|
|
5520
|
-
ask: terminal ? (question) => prompt(question) : void 0
|
|
5896
|
+
ask: terminal ? (question) => prompt(question) : void 0,
|
|
5897
|
+
startedAt: performance.timeOrigin
|
|
5521
5898
|
})
|
|
5522
|
-
).then((code) => process.exit(code))
|
|
5899
|
+
).then((code) => process.exit(code)).catch((err) => {
|
|
5900
|
+
console.error(` Error: ${err.message}`);
|
|
5901
|
+
process.exit(hook ? 0 : 1);
|
|
5902
|
+
});
|
|
5523
5903
|
} else if (command === "--help" || command === "-h") {
|
|
5524
5904
|
console.log(`
|
|
5525
5905
|
SystemDox MCP Server
|
|
5526
5906
|
|
|
5527
5907
|
Usage:
|
|
5528
5908
|
systemdox-mcp-server Start the MCP server (stdio transport)
|
|
5529
|
-
systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]
|
|
5909
|
+
systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]
|
|
5530
5910
|
Configure the MCP server in your AI coding tool,
|
|
5531
5911
|
then your team's setup: the team instructions,
|
|
5532
5912
|
the plugin's marketplace and the project's team
|
|
@@ -5543,10 +5923,15 @@ if (command === "setup") {
|
|
|
5543
5923
|
Print the header for your team's marketplace
|
|
5544
5924
|
(Claude Code runs it)
|
|
5545
5925
|
systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>]
|
|
5546
|
-
[--import <folder>] [--dry-run]
|
|
5926
|
+
[--import <folder>]... [--dry-run]
|
|
5547
5927
|
Sync a project's team memory with SystemDox
|
|
5548
5928
|
(default folder:
|
|
5549
5929
|
~/.claude/systemdox/<workspace>/memory/<project>)
|
|
5930
|
+
systemdox-mcp-server sync-memory --hook <workspace>
|
|
5931
|
+
The run the team plugin makes at the start and
|
|
5932
|
+
the end of a session: the memories this computer
|
|
5933
|
+
synced before, one line each, never a question,
|
|
5934
|
+
never a held-up session (Claude Code runs it)
|
|
5550
5935
|
systemdox-mcp-server --help Show this help message
|
|
5551
5936
|
|
|
5552
5937
|
Environment variables:
|