@systemdox/mcp-server 0.1.57 → 0.1.59
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 +41 -9
- package/dist/cli.js +461 -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 +72 -9
- package/dist/sync-memory.js +350 -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,70 @@ 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,
|
|
887
|
+
HOOK_WAITING_NAMED: () => HOOK_WAITING_NAMED,
|
|
852
888
|
SYNC_MEMORY_COMMAND: () => SYNC_MEMORY_COMMAND,
|
|
853
889
|
SYNC_MEMORY_USAGE: () => SYNC_MEMORY_USAGE,
|
|
890
|
+
hookLines: () => hookLines,
|
|
854
891
|
parseSyncMemoryArgs: () => parseSyncMemoryArgs,
|
|
892
|
+
runHookSync: () => runHookSync,
|
|
855
893
|
runSyncMemory: () => runSyncMemory,
|
|
856
894
|
syncMemory: () => syncMemory
|
|
857
895
|
});
|
|
858
896
|
import { existsSync } from "fs";
|
|
859
|
-
import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
897
|
+
import { appendFile, mkdir as mkdir2, open, readdir, readFile as readFile2, rename as rename2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
860
898
|
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
899
|
+
function failureReason(e) {
|
|
900
|
+
if (e instanceof SyncStopped) return e.reason;
|
|
901
|
+
const { status, code } = refusal(e);
|
|
902
|
+
if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
|
|
903
|
+
const err = e;
|
|
904
|
+
const net = err?.cause?.code ?? err?.code ?? "";
|
|
905
|
+
if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
|
|
906
|
+
return "SystemDox could not be reached";
|
|
907
|
+
}
|
|
908
|
+
return "it could not finish";
|
|
909
|
+
}
|
|
910
|
+
async function writeWhole(path, text3) {
|
|
911
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
912
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
913
|
+
try {
|
|
914
|
+
await writeFile2(tmp, text3, "utf8");
|
|
915
|
+
await rename2(tmp, path);
|
|
916
|
+
} catch (e) {
|
|
917
|
+
await rm2(tmp, { force: true });
|
|
918
|
+
throw e;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
861
921
|
function refusal(e) {
|
|
862
922
|
if (!(e instanceof Error)) return {};
|
|
863
923
|
const { status, code } = e;
|
|
864
924
|
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
865
925
|
}
|
|
866
926
|
function parseSyncMemoryArgs(argv) {
|
|
867
|
-
const args2 = { dryRun: false };
|
|
927
|
+
const args2 = { dryRun: false, importDirs: [] };
|
|
868
928
|
for (let i = 0; i < argv.length; i++) {
|
|
869
929
|
const a = argv[i];
|
|
870
930
|
if (a === "--dry-run") args2.dryRun = true;
|
|
871
|
-
else if (a === "--project" || a === "--dir" || a === "--import") {
|
|
931
|
+
else if (a === "--project" || a === "--dir" || a === "--import" || a === "--hook") {
|
|
872
932
|
const value = argv[++i];
|
|
873
933
|
if (!value || value.startsWith("--")) throw new SyncFailed(`${a} needs a value
|
|
874
934
|
${SYNC_MEMORY_USAGE}`);
|
|
875
935
|
if (a === "--project") {
|
|
876
936
|
if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a project name (lower case letters, digits and -)`);
|
|
877
937
|
args2.project = value;
|
|
938
|
+
} else if (a === "--hook") {
|
|
939
|
+
if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a workspace name (lower case letters, digits and -)`);
|
|
940
|
+
args2.hook = value;
|
|
878
941
|
} else if (a === "--dir") args2.dir = value;
|
|
879
|
-
else args2.
|
|
942
|
+
else args2.importDirs.push(value);
|
|
880
943
|
} else throw new SyncFailed(`Unexpected argument ${a}
|
|
944
|
+
${SYNC_MEMORY_USAGE}`);
|
|
945
|
+
}
|
|
946
|
+
if (args2.hook && (args2.project || args2.dir || args2.importDirs.length || args2.dryRun)) {
|
|
947
|
+
throw new SyncFailed(`--hook takes no other option
|
|
881
948
|
${SYNC_MEMORY_USAGE}`);
|
|
882
949
|
}
|
|
883
950
|
return args2;
|
|
@@ -885,6 +952,7 @@ ${SYNC_MEMORY_USAGE}`);
|
|
|
885
952
|
async function readFolder(root) {
|
|
886
953
|
const files = {};
|
|
887
954
|
const names = /* @__PURE__ */ new Set();
|
|
955
|
+
const temps = [];
|
|
888
956
|
async function walk2(dir, prefix) {
|
|
889
957
|
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
890
958
|
if (e.name.startsWith(".")) continue;
|
|
@@ -892,12 +960,13 @@ async function readFolder(root) {
|
|
|
892
960
|
if (e.isDirectory()) await walk2(join2(dir, e.name), rel);
|
|
893
961
|
else if (e.isFile()) {
|
|
894
962
|
names.add(rel.toLowerCase());
|
|
963
|
+
if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
|
|
895
964
|
if (e.name.endsWith(".md")) files[rel] = await readFile2(join2(dir, e.name), "utf8");
|
|
896
965
|
}
|
|
897
966
|
}
|
|
898
967
|
}
|
|
899
968
|
if (existsSync(root)) await walk2(root, "");
|
|
900
|
-
return { files, names };
|
|
969
|
+
return { files, names, temps };
|
|
901
970
|
}
|
|
902
971
|
async function pullTeam(client, ws, project) {
|
|
903
972
|
try {
|
|
@@ -1005,7 +1074,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
1005
1074
|
for (const path of g.files) {
|
|
1006
1075
|
if (leftOut.has(path)) continue;
|
|
1007
1076
|
current[path] = addShareFalse(current[path]);
|
|
1008
|
-
await
|
|
1077
|
+
await writeWhole(toPath(opts.dir, path), current[path]);
|
|
1009
1078
|
leftOut.add(path);
|
|
1010
1079
|
}
|
|
1011
1080
|
report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
|
|
@@ -1108,7 +1177,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
1108
1177
|
const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
|
|
1109
1178
|
if (yes) {
|
|
1110
1179
|
for (const path of paths) {
|
|
1111
|
-
await
|
|
1180
|
+
await writeWhole(toPath(opts.dir, path), rewritten[path]);
|
|
1112
1181
|
current[path] = rewritten[path];
|
|
1113
1182
|
}
|
|
1114
1183
|
}
|
|
@@ -1123,11 +1192,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
1123
1192
|
held.add(path);
|
|
1124
1193
|
const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
|
|
1125
1194
|
if (keys.some((k) => vaultDown.has(k))) {
|
|
1126
|
-
report.
|
|
1195
|
+
report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
|
|
1127
1196
|
} else if (keys.some((k) => !answers.has(k))) {
|
|
1128
|
-
report.
|
|
1197
|
+
report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
|
|
1129
1198
|
} else {
|
|
1130
|
-
report.
|
|
1199
|
+
report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
|
|
1131
1200
|
}
|
|
1132
1201
|
}
|
|
1133
1202
|
for (const g of groups) {
|
|
@@ -1138,12 +1207,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
1138
1207
|
}
|
|
1139
1208
|
return { files: current, held, leftOut };
|
|
1140
1209
|
}
|
|
1210
|
+
async function removeLeftTemps(dir, temps) {
|
|
1211
|
+
for (const rel of temps) await rm2(toPath(dir, rel), { force: true });
|
|
1212
|
+
}
|
|
1141
1213
|
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
|
-
}
|
|
1214
|
+
for (const [path, text3] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text3);
|
|
1147
1215
|
for (const path of plan.deletes) await rm2(toPath(dir, path), { force: true });
|
|
1148
1216
|
}
|
|
1149
1217
|
async function upload(client, ws, project, plan, team, report) {
|
|
@@ -1187,8 +1255,19 @@ async function syncMemory(opts) {
|
|
|
1187
1255
|
const { client, out } = opts;
|
|
1188
1256
|
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
1189
1257
|
try {
|
|
1258
|
+
const checkDeadline = () => {
|
|
1259
|
+
if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
|
|
1260
|
+
throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1190
1263
|
const own = await client.getOwnTeamSetup();
|
|
1191
1264
|
const ws = own.workspace;
|
|
1265
|
+
if (opts.workspace && ws !== opts.workspace) {
|
|
1266
|
+
throw new SyncStopped(
|
|
1267
|
+
"the key is for another workspace",
|
|
1268
|
+
`The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1192
1271
|
const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
|
|
1193
1272
|
if (!project) {
|
|
1194
1273
|
throw new SyncFailed(
|
|
@@ -1202,6 +1281,12 @@ async function syncMemory(opts) {
|
|
|
1202
1281
|
const date = localDate(now);
|
|
1203
1282
|
const stored = (await readState(ws)).memory?.[project];
|
|
1204
1283
|
let team = await pullTeam(client, ws, project);
|
|
1284
|
+
if (opts.unattended && stored && !team.exists) {
|
|
1285
|
+
throw new SyncStopped(
|
|
1286
|
+
"the team memory could not be read",
|
|
1287
|
+
`${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1205
1290
|
let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
|
|
1206
1291
|
const folder = await readFolder(dir);
|
|
1207
1292
|
const shared = {};
|
|
@@ -1214,14 +1299,16 @@ async function syncMemory(opts) {
|
|
|
1214
1299
|
} else shared[path] = text3;
|
|
1215
1300
|
}
|
|
1216
1301
|
const importWrites = {};
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1302
|
+
const current = { ...folder.files };
|
|
1303
|
+
const taken = new Set(folder.names);
|
|
1304
|
+
for (const importDir of opts.importDirs ?? []) {
|
|
1305
|
+
const other = await readFolder(resolve(importDir));
|
|
1220
1306
|
for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
|
|
1221
1307
|
if (!isShareable(path, text3) || !isFilePath(path)) continue;
|
|
1222
|
-
const here =
|
|
1308
|
+
const here = current[path];
|
|
1223
1309
|
if (here === void 0) {
|
|
1224
1310
|
shared[path] = text3;
|
|
1311
|
+
current[path] = text3;
|
|
1225
1312
|
importWrites[path] = text3;
|
|
1226
1313
|
taken.add(path.toLowerCase());
|
|
1227
1314
|
} else if (here !== text3) {
|
|
@@ -1229,16 +1316,14 @@ async function syncMemory(opts) {
|
|
|
1229
1316
|
taken.add(copy.toLowerCase());
|
|
1230
1317
|
importWrites[copy] = text3;
|
|
1231
1318
|
report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
|
|
1319
|
+
report.copies.add(copy);
|
|
1232
1320
|
}
|
|
1233
1321
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
await writeFile2(full, text3, "utf8");
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1322
|
+
}
|
|
1323
|
+
folder.names = taken;
|
|
1324
|
+
if (!opts.dryRun) {
|
|
1325
|
+
checkDeadline();
|
|
1326
|
+
for (const [path, text3] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text3);
|
|
1242
1327
|
}
|
|
1243
1328
|
const found = await people(run3, ws, shared, report, hint);
|
|
1244
1329
|
for (const path of found.leftOut) {
|
|
@@ -1263,12 +1348,16 @@ async function syncMemory(opts) {
|
|
|
1263
1348
|
);
|
|
1264
1349
|
return 0;
|
|
1265
1350
|
}
|
|
1351
|
+
checkDeadline();
|
|
1352
|
+
if (attempt === 1) await removeLeftTemps(dir, folder.temps);
|
|
1266
1353
|
await applyPlan(dir, plan);
|
|
1267
1354
|
let version = team.version;
|
|
1268
1355
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1356
|
+
checkDeadline();
|
|
1269
1357
|
await upload(client, ws, project, plan, team.manifest, report);
|
|
1270
1358
|
}
|
|
1271
1359
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1360
|
+
checkDeadline();
|
|
1272
1361
|
try {
|
|
1273
1362
|
version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
|
|
1274
1363
|
} catch (e) {
|
|
@@ -1280,6 +1369,7 @@ async function syncMemory(opts) {
|
|
|
1280
1369
|
false
|
|
1281
1370
|
)
|
|
1282
1371
|
);
|
|
1372
|
+
opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
|
|
1283
1373
|
return 2;
|
|
1284
1374
|
}
|
|
1285
1375
|
base = team;
|
|
@@ -1304,18 +1394,202 @@ async function syncMemory(opts) {
|
|
|
1304
1394
|
false
|
|
1305
1395
|
)
|
|
1306
1396
|
);
|
|
1397
|
+
opts.summary?.(report.summary("synced", version));
|
|
1307
1398
|
return 0;
|
|
1308
1399
|
}
|
|
1309
1400
|
} catch (e) {
|
|
1310
1401
|
out(e.message);
|
|
1402
|
+
opts.summary?.({
|
|
1403
|
+
outcome: "failed",
|
|
1404
|
+
reason: failureReason(e),
|
|
1405
|
+
version: 0,
|
|
1406
|
+
pulled: 0,
|
|
1407
|
+
pushed: 0,
|
|
1408
|
+
deleted: 0,
|
|
1409
|
+
conflictCopies: [],
|
|
1410
|
+
waiting: []
|
|
1411
|
+
});
|
|
1311
1412
|
return 1;
|
|
1312
1413
|
}
|
|
1313
1414
|
}
|
|
1415
|
+
function firstNames(names) {
|
|
1416
|
+
const shown = names.slice(0, HOOK_WAITING_NAMED).join(", ");
|
|
1417
|
+
const rest = names.length - HOOK_WAITING_NAMED;
|
|
1418
|
+
return rest > 0 ? `${shown} and ${rest} more` : shown;
|
|
1419
|
+
}
|
|
1420
|
+
function hookLines(ws, project, folder, s) {
|
|
1421
|
+
if (s.outcome !== "synced") {
|
|
1422
|
+
return [`Team memory ${ws}/${project}: not synced (${s.reason ?? "it did not finish"}); the next session tries again.`];
|
|
1423
|
+
}
|
|
1424
|
+
const lines = [`Team memory ${ws}/${project}: version ${s.version}, ${s.pulled} pulled, ${s.pushed} pushed, ${s.deleted} deleted.`];
|
|
1425
|
+
if (s.conflictCopies.length) {
|
|
1426
|
+
lines.push(` ${plural(s.conflictCopies.length, "conflict copy", "conflict copies")} to tidy in ${folder}: ${s.conflictCopies.join(", ")}`);
|
|
1427
|
+
}
|
|
1428
|
+
if (s.waiting.length) {
|
|
1429
|
+
lines.push(
|
|
1430
|
+
` ${plural(s.waiting.length, "file waits", "files wait")} for you (${firstNames(s.waiting)}): run \`${SYNC_MEMORY_COMMAND} --project ${project}\` in your terminal.`
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
return lines;
|
|
1434
|
+
}
|
|
1435
|
+
async function appendLog(ws, lines) {
|
|
1436
|
+
const path = syncLogPath(ws);
|
|
1437
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
1438
|
+
try {
|
|
1439
|
+
if ((await stat(path)).size > HOOK_LOG_MAX_BYTES) await writeFile2(path, "", "utf8");
|
|
1440
|
+
} catch {
|
|
1441
|
+
}
|
|
1442
|
+
await appendFile(path, lines.join("\n") + "\n", "utf8");
|
|
1443
|
+
}
|
|
1444
|
+
async function takeLock(lock, staleMs, now) {
|
|
1445
|
+
await mkdir2(dirname2(lock), { recursive: true });
|
|
1446
|
+
const body = JSON.stringify({ pid: process.pid, at: now.toISOString() }) + "\n";
|
|
1447
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1448
|
+
try {
|
|
1449
|
+
const handle = await open(lock, "wx");
|
|
1450
|
+
try {
|
|
1451
|
+
await handle.writeFile(body, "utf8");
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
await handle.close();
|
|
1454
|
+
await rm2(lock, { force: true });
|
|
1455
|
+
throw err;
|
|
1456
|
+
}
|
|
1457
|
+
await handle.close();
|
|
1458
|
+
return true;
|
|
1459
|
+
} catch (e) {
|
|
1460
|
+
if (e.code !== "EEXIST") throw e;
|
|
1461
|
+
let seen;
|
|
1462
|
+
try {
|
|
1463
|
+
seen = await stat(lock);
|
|
1464
|
+
} catch {
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
if (Date.now() - seen.mtimeMs < staleMs || attempt === 1) return false;
|
|
1468
|
+
try {
|
|
1469
|
+
const now2 = await stat(lock);
|
|
1470
|
+
if (now2.ino !== seen.ino || now2.mtimeMs !== seen.mtimeMs) return false;
|
|
1471
|
+
} catch {
|
|
1472
|
+
continue;
|
|
1473
|
+
}
|
|
1474
|
+
await rm2(lock, { force: true });
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
return false;
|
|
1478
|
+
}
|
|
1479
|
+
async function releaseLock(lock) {
|
|
1480
|
+
try {
|
|
1481
|
+
const held = JSON.parse(await readFile2(lock, "utf8"));
|
|
1482
|
+
if (held.pid === process.pid) await rm2(lock, { force: true });
|
|
1483
|
+
} catch {
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
async function runHookSync(workspace, io, env = process.env) {
|
|
1487
|
+
try {
|
|
1488
|
+
return await hookSync(workspace, io, env);
|
|
1489
|
+
} catch (e) {
|
|
1490
|
+
try {
|
|
1491
|
+
io.err(`sync-memory --hook: ${e.message}`);
|
|
1492
|
+
} catch {
|
|
1493
|
+
}
|
|
1494
|
+
return 0;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
async function hookSync(workspace, io, env) {
|
|
1498
|
+
if (!isWorkspace(workspace)) {
|
|
1499
|
+
io.err(`sync-memory --hook: ${JSON.stringify(workspace)} is not a workspace name`);
|
|
1500
|
+
return 0;
|
|
1501
|
+
}
|
|
1502
|
+
const now = io.now?.() ?? /* @__PURE__ */ new Date();
|
|
1503
|
+
const deadline = (io.startedAt ?? Date.now()) + (io.budgetMs ?? HOOK_BUDGET_MS);
|
|
1504
|
+
let client = io.client;
|
|
1505
|
+
if (!client) {
|
|
1506
|
+
let apiKey;
|
|
1507
|
+
let apiUrl;
|
|
1508
|
+
try {
|
|
1509
|
+
apiKey = await apiKeyFor(workspace, env);
|
|
1510
|
+
apiUrl = apiUrlFrom(env);
|
|
1511
|
+
} catch {
|
|
1512
|
+
return 0;
|
|
1513
|
+
}
|
|
1514
|
+
if (!apiKey) return 0;
|
|
1515
|
+
client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1516
|
+
}
|
|
1517
|
+
let projects;
|
|
1518
|
+
try {
|
|
1519
|
+
projects = Object.keys((await readState(workspace)).memory ?? {}).sort();
|
|
1520
|
+
} catch {
|
|
1521
|
+
return 0;
|
|
1522
|
+
}
|
|
1523
|
+
if (!projects.length) return 0;
|
|
1524
|
+
const lock = syncLockPath(workspace);
|
|
1525
|
+
if (!await takeLock(lock, io.lockStaleMs ?? HOOK_LOCK_STALE_MS, now)) {
|
|
1526
|
+
io.out(`Team memory ${workspace}: a sync is already running.`);
|
|
1527
|
+
return 0;
|
|
1528
|
+
}
|
|
1529
|
+
try {
|
|
1530
|
+
for (const project of projects) {
|
|
1531
|
+
const folder = join2(workspaceDir(workspace), "memory", project);
|
|
1532
|
+
const report = [];
|
|
1533
|
+
let summary;
|
|
1534
|
+
const left = deadline - Date.now();
|
|
1535
|
+
if (left > 0) {
|
|
1536
|
+
let timer;
|
|
1537
|
+
const timeout = new Promise((resolveTimeout) => {
|
|
1538
|
+
timer = setTimeout(() => resolveTimeout("timeout"), left);
|
|
1539
|
+
});
|
|
1540
|
+
const run3 = syncMemory({
|
|
1541
|
+
project,
|
|
1542
|
+
dryRun: false,
|
|
1543
|
+
client,
|
|
1544
|
+
workspace,
|
|
1545
|
+
deadline,
|
|
1546
|
+
unattended: true,
|
|
1547
|
+
out: (...lines2) => report.push(...lines2),
|
|
1548
|
+
summary: (s) => summary = s,
|
|
1549
|
+
now: io.now
|
|
1550
|
+
});
|
|
1551
|
+
const won = await Promise.race([run3.then(() => "done"), timeout]);
|
|
1552
|
+
if (timer) clearTimeout(timer);
|
|
1553
|
+
if (won === "timeout") summary = void 0;
|
|
1554
|
+
}
|
|
1555
|
+
const result = summary ?? {
|
|
1556
|
+
outcome: "failed",
|
|
1557
|
+
reason: "took too long",
|
|
1558
|
+
version: 0,
|
|
1559
|
+
pulled: 0,
|
|
1560
|
+
pushed: 0,
|
|
1561
|
+
deleted: 0,
|
|
1562
|
+
conflictCopies: [],
|
|
1563
|
+
waiting: []
|
|
1564
|
+
};
|
|
1565
|
+
const lines = hookLines(workspace, project, folder, result);
|
|
1566
|
+
lines.forEach((l) => io.out(l));
|
|
1567
|
+
try {
|
|
1568
|
+
await appendLog(workspace, [`--- ${now.toISOString()} ${workspace}/${project}`, ...lines, ...report]);
|
|
1569
|
+
} catch (e) {
|
|
1570
|
+
io.err(`sync-memory --hook: the log could not be written: ${e.message}`);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
} finally {
|
|
1574
|
+
await releaseLock(lock);
|
|
1575
|
+
}
|
|
1576
|
+
return 0;
|
|
1577
|
+
}
|
|
1314
1578
|
async function runSyncMemory(argv, io, env = process.env) {
|
|
1315
1579
|
let args2;
|
|
1316
1580
|
let client = io.client;
|
|
1317
1581
|
try {
|
|
1318
1582
|
args2 = parseSyncMemoryArgs(argv);
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
if (argv.includes("--hook")) {
|
|
1585
|
+
io.err(`sync-memory --hook: ${e.message.split("\n")[0]}`);
|
|
1586
|
+
return 0;
|
|
1587
|
+
}
|
|
1588
|
+
io.err(e.message);
|
|
1589
|
+
return 1;
|
|
1590
|
+
}
|
|
1591
|
+
if (args2.hook) return runHookSync(args2.hook, io, env);
|
|
1592
|
+
try {
|
|
1319
1593
|
if (!client) {
|
|
1320
1594
|
const apiUrl = apiUrlFrom(env);
|
|
1321
1595
|
const apiKey = await localApiKey(env);
|
|
@@ -1327,7 +1601,7 @@ async function runSyncMemory(argv, io, env = process.env) {
|
|
|
1327
1601
|
}
|
|
1328
1602
|
return syncMemory({ ...args2, client, out: (...lines) => lines.forEach((l) => io.out(l)), ask: io.ask, now: io.now });
|
|
1329
1603
|
}
|
|
1330
|
-
var SYNC_MEMORY_USAGE, SYNC_MEMORY_COMMAND, SyncFailed, toPath, Report, ANSWER_WORDS, sameManifest;
|
|
1604
|
+
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, HOOK_WAITING_NAMED;
|
|
1331
1605
|
var init_sync_memory = __esm({
|
|
1332
1606
|
"src/sync-memory.ts"() {
|
|
1333
1607
|
"use strict";
|
|
@@ -1335,18 +1609,33 @@ var init_sync_memory = __esm({
|
|
|
1335
1609
|
init_memory_sync();
|
|
1336
1610
|
init_person_values();
|
|
1337
1611
|
init_team_setup_files();
|
|
1338
|
-
SYNC_MEMORY_USAGE = "Usage: systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>] [--import <folder>] [--dry-run]";
|
|
1612
|
+
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>";
|
|
1613
|
+
HOOK_BUDGET_MS = 2e4;
|
|
1614
|
+
HOOK_LOCK_STALE_MS = 6e4;
|
|
1615
|
+
HOOK_LOG_MAX_BYTES = 1024 * 1024;
|
|
1339
1616
|
SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
|
|
1340
1617
|
SyncFailed = class extends Error {
|
|
1341
1618
|
};
|
|
1619
|
+
SyncStopped = class extends Error {
|
|
1620
|
+
constructor(reason, message) {
|
|
1621
|
+
super(message);
|
|
1622
|
+
this.reason = reason;
|
|
1623
|
+
}
|
|
1624
|
+
reason;
|
|
1625
|
+
};
|
|
1342
1626
|
toPath = (root, rel) => join2(root, ...rel.split("/"));
|
|
1627
|
+
LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
|
|
1343
1628
|
Report = class {
|
|
1344
1629
|
pulled = /* @__PURE__ */ new Set();
|
|
1345
1630
|
pushed = /* @__PURE__ */ new Set();
|
|
1346
1631
|
deletedHere = /* @__PURE__ */ new Set();
|
|
1347
1632
|
deletedFromTeam = /* @__PURE__ */ new Set();
|
|
1348
1633
|
conflicts = [];
|
|
1634
|
+
/** The conflict copies written this run, by name. */
|
|
1635
|
+
copies = /* @__PURE__ */ new Set();
|
|
1349
1636
|
skipped = /* @__PURE__ */ new Map();
|
|
1637
|
+
/** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
|
|
1638
|
+
waiting = /* @__PURE__ */ new Set();
|
|
1350
1639
|
people = [];
|
|
1351
1640
|
toAnswer = /* @__PURE__ */ new Map();
|
|
1352
1641
|
add(plan) {
|
|
@@ -1354,12 +1643,32 @@ var init_sync_memory = __esm({
|
|
|
1354
1643
|
plan.pushed.forEach((p) => this.pushed.add(p));
|
|
1355
1644
|
plan.deletedHere.forEach((p) => this.deletedHere.add(p));
|
|
1356
1645
|
plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
|
|
1357
|
-
for (const c of plan.conflicts)
|
|
1646
|
+
for (const c of plan.conflicts) {
|
|
1647
|
+
this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
|
|
1648
|
+
this.copies.add(c.copy);
|
|
1649
|
+
}
|
|
1358
1650
|
for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
|
|
1359
1651
|
}
|
|
1360
1652
|
skip(path, reason) {
|
|
1361
1653
|
if (!this.skipped.has(path)) this.skipped.set(path, reason);
|
|
1362
1654
|
}
|
|
1655
|
+
/** Skipped, and waiting for the member at a terminal. */
|
|
1656
|
+
wait(path, reason) {
|
|
1657
|
+
this.skip(path, reason);
|
|
1658
|
+
this.waiting.add(path);
|
|
1659
|
+
}
|
|
1660
|
+
summary(outcome, version, reason) {
|
|
1661
|
+
return {
|
|
1662
|
+
outcome,
|
|
1663
|
+
...reason ? { reason } : {},
|
|
1664
|
+
version,
|
|
1665
|
+
pulled: this.pulled.size,
|
|
1666
|
+
pushed: this.pushed.size,
|
|
1667
|
+
deleted: this.deletedHere.size + this.deletedFromTeam.size,
|
|
1668
|
+
conflictCopies: [...this.copies].sort(),
|
|
1669
|
+
waiting: [...this.waiting].sort()
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1363
1672
|
lines(summary, dryRun) {
|
|
1364
1673
|
const out = [summary];
|
|
1365
1674
|
const section = (title, items) => {
|
|
@@ -1398,6 +1707,8 @@ var init_sync_memory = __esm({
|
|
|
1398
1707
|
ours: "ours, not a person"
|
|
1399
1708
|
};
|
|
1400
1709
|
sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
|
|
1710
|
+
plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
1711
|
+
HOOK_WAITING_NAMED = 5;
|
|
1401
1712
|
}
|
|
1402
1713
|
});
|
|
1403
1714
|
|
|
@@ -1407,7 +1718,11 @@ __export(setup_team_exports, {
|
|
|
1407
1718
|
HEADERS_HELPER: () => HEADERS_HELPER,
|
|
1408
1719
|
SETUP_USAGE: () => SETUP_USAGE,
|
|
1409
1720
|
claudeCodeVersion: () => claudeCodeVersion,
|
|
1721
|
+
claudeMemoryFolder: () => claudeMemoryFolder,
|
|
1722
|
+
countMemoryFiles: () => countMemoryFiles,
|
|
1723
|
+
gitTopFolder: () => gitTopFolder,
|
|
1410
1724
|
importBlock: () => importBlock,
|
|
1725
|
+
memoryImports: () => memoryImports,
|
|
1411
1726
|
parseSetupArgs: () => parseSetupArgs,
|
|
1412
1727
|
runTeamSetup: () => runTeamSetup,
|
|
1413
1728
|
runTeamSetupCommand: () => runTeamSetupCommand,
|
|
@@ -1422,19 +1737,20 @@ import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, writeFile
|
|
|
1422
1737
|
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
1423
1738
|
import { promisify as promisify2 } from "util";
|
|
1424
1739
|
function parseSetupArgs(argv) {
|
|
1425
|
-
const args2 = { yes: false, overwrite: false };
|
|
1740
|
+
const args2 = { yes: false, overwrite: false, importDirs: [] };
|
|
1426
1741
|
for (let i = 0; i < argv.length; i++) {
|
|
1427
1742
|
const a = argv[i];
|
|
1428
1743
|
if (a === "--yes") args2.yes = true;
|
|
1429
1744
|
else if (a === "--overwrite") args2.overwrite = true;
|
|
1430
|
-
else if (a === "--project" || a === "--dir") {
|
|
1745
|
+
else if (a === "--project" || a === "--dir" || a === "--import") {
|
|
1431
1746
|
const value = argv[++i];
|
|
1432
1747
|
if (!value || value.startsWith("--")) throw new Error(`${a} needs a value
|
|
1433
1748
|
${SETUP_USAGE}`);
|
|
1434
1749
|
if (a === "--project") {
|
|
1435
1750
|
if (!isWorkspace(value)) throw new Error(`${value} is not a project name (lower case letters, digits and -)`);
|
|
1436
1751
|
args2.project = value;
|
|
1437
|
-
} else args2.dir = value;
|
|
1752
|
+
} else if (a === "--dir") args2.dir = value;
|
|
1753
|
+
else args2.importDirs.push(value);
|
|
1438
1754
|
} else throw new Error(`Unexpected argument ${a}
|
|
1439
1755
|
${SETUP_USAGE}`);
|
|
1440
1756
|
}
|
|
@@ -1452,6 +1768,60 @@ async function claudeCodeVersion() {
|
|
|
1452
1768
|
return void 0;
|
|
1453
1769
|
}
|
|
1454
1770
|
}
|
|
1771
|
+
async function gitTopFolder(dir) {
|
|
1772
|
+
try {
|
|
1773
|
+
const { stdout } = await run2("git", ["-C", dir, "rev-parse", "--show-toplevel"], { timeout: 1e4, windowsHide: true });
|
|
1774
|
+
const top = stdout.trim();
|
|
1775
|
+
return top ? resolve2(top) : void 0;
|
|
1776
|
+
} catch {
|
|
1777
|
+
return void 0;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function claudeMemoryFolder(path) {
|
|
1781
|
+
return join3(homeDir(), ".claude", "projects", path.replace(/[^A-Za-z0-9]/g, "-"), "memory");
|
|
1782
|
+
}
|
|
1783
|
+
async function countMemoryFiles(folder) {
|
|
1784
|
+
let count = 0;
|
|
1785
|
+
async function walk2(dir, prefix) {
|
|
1786
|
+
let entries2;
|
|
1787
|
+
try {
|
|
1788
|
+
entries2 = await readdir2(dir, { withFileTypes: true });
|
|
1789
|
+
} catch {
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
for (const e of entries2) {
|
|
1793
|
+
if (e.name.startsWith(".")) continue;
|
|
1794
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
1795
|
+
if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
|
|
1796
|
+
else if (e.isFile() && e.name.endsWith(".md")) {
|
|
1797
|
+
try {
|
|
1798
|
+
if (isShareable(rel, await readFile3(join3(dir, e.name), "utf8"))) count++;
|
|
1799
|
+
} catch {
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
await walk2(folder, "");
|
|
1805
|
+
return count;
|
|
1806
|
+
}
|
|
1807
|
+
async function memoryImports(checkout, teamFolder, named, gitTop = gitTopFolder) {
|
|
1808
|
+
const candidates = [resolve2(checkout)];
|
|
1809
|
+
const top = await gitTop(checkout);
|
|
1810
|
+
if (top && !candidates.includes(top)) candidates.push(top);
|
|
1811
|
+
const out = [];
|
|
1812
|
+
for (const path of candidates) {
|
|
1813
|
+
const folder = claudeMemoryFolder(path);
|
|
1814
|
+
if (resolve2(folder) === resolve2(teamFolder)) continue;
|
|
1815
|
+
const files = await countMemoryFiles(folder);
|
|
1816
|
+
if (files) out.push({ folder, files, named: false });
|
|
1817
|
+
}
|
|
1818
|
+
for (const dir of named) {
|
|
1819
|
+
const folder = resolve2(dir);
|
|
1820
|
+
if (out.some((i) => i.folder === folder)) continue;
|
|
1821
|
+
out.push({ folder, files: await countMemoryFiles(folder), named: true });
|
|
1822
|
+
}
|
|
1823
|
+
return out;
|
|
1824
|
+
}
|
|
1455
1825
|
function parseVersion(text3) {
|
|
1456
1826
|
const m = text3?.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
1457
1827
|
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : void 0;
|
|
@@ -1493,9 +1863,9 @@ function skipValue(t, i) {
|
|
|
1493
1863
|
while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
|
|
1494
1864
|
return i;
|
|
1495
1865
|
}
|
|
1496
|
-
function objectMembers(t,
|
|
1866
|
+
function objectMembers(t, open2) {
|
|
1497
1867
|
const members = [];
|
|
1498
|
-
let i = skipSpace(t,
|
|
1868
|
+
let i = skipSpace(t, open2 + 1);
|
|
1499
1869
|
if (t[i] === "}") return { members, close: i };
|
|
1500
1870
|
for (; ; ) {
|
|
1501
1871
|
i = skipSpace(t, i);
|
|
@@ -1522,13 +1892,13 @@ function setJsonMember(text3, keys, value) {
|
|
|
1522
1892
|
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
1523
1893
|
const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
|
|
1524
1894
|
const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
|
|
1525
|
-
let
|
|
1895
|
+
let open2 = skipSpace(text3, 0);
|
|
1526
1896
|
for (let k = 0; k < keys.length; k++) {
|
|
1527
|
-
const { members, close } = objectMembers(text3,
|
|
1897
|
+
const { members, close } = objectMembers(text3, open2);
|
|
1528
1898
|
const found = members.filter((m) => m.key === keys[k]).at(-1);
|
|
1529
1899
|
if (found) {
|
|
1530
1900
|
if (k < keys.length - 1 && text3[found.valueStart] === "{") {
|
|
1531
|
-
|
|
1901
|
+
open2 = found.valueStart;
|
|
1532
1902
|
continue;
|
|
1533
1903
|
}
|
|
1534
1904
|
return text3.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text3, found.keyStart)) + text3.slice(found.valueEnd);
|
|
@@ -1537,16 +1907,16 @@ function setJsonMember(text3, keys, value) {
|
|
|
1537
1907
|
if (compact) {
|
|
1538
1908
|
const member = `${name}:${render(nest(k + 1), "")}`;
|
|
1539
1909
|
if (members.length) return text3.slice(0, members.at(-1).valueEnd) + "," + member + text3.slice(members.at(-1).valueEnd);
|
|
1540
|
-
return text3.slice(0,
|
|
1910
|
+
return text3.slice(0, open2 + 1) + member + text3.slice(close);
|
|
1541
1911
|
}
|
|
1542
1912
|
if (members.length) {
|
|
1543
1913
|
const last = members.at(-1);
|
|
1544
1914
|
const indent2 = lineIndent(text3, last.keyStart);
|
|
1545
1915
|
return text3.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text3.slice(last.valueEnd);
|
|
1546
1916
|
}
|
|
1547
|
-
const outer = lineIndent(text3,
|
|
1917
|
+
const outer = lineIndent(text3, open2);
|
|
1548
1918
|
const indent = outer + unit;
|
|
1549
|
-
return text3.slice(0,
|
|
1919
|
+
return text3.slice(0, open2 + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text3.slice(close);
|
|
1550
1920
|
}
|
|
1551
1921
|
return text3;
|
|
1552
1922
|
}
|
|
@@ -1592,9 +1962,9 @@ function importBlock(workspace, eol = "\n") {
|
|
|
1592
1962
|
function withImportBlock(text3, workspace) {
|
|
1593
1963
|
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
1594
1964
|
const block = importBlock(workspace, eol);
|
|
1595
|
-
const
|
|
1965
|
+
const open2 = `<!-- systemdox:${workspace} -->`;
|
|
1596
1966
|
const close = `<!-- /systemdox:${workspace} -->`;
|
|
1597
|
-
const start = text3.indexOf(
|
|
1967
|
+
const start = text3.indexOf(open2);
|
|
1598
1968
|
const end = start >= 0 ? text3.indexOf(close, start) : -1;
|
|
1599
1969
|
if (start >= 0 && end >= 0) return text3.slice(0, start) + block + text3.slice(end + close.length);
|
|
1600
1970
|
if (!text3) return block + eol;
|
|
@@ -1657,12 +2027,12 @@ async function runTeamSetup(opts) {
|
|
|
1657
2027
|
const plugin = own.plugin;
|
|
1658
2028
|
let previousPlugin;
|
|
1659
2029
|
if (plugin) {
|
|
1660
|
-
const hooks =
|
|
2030
|
+
const hooks = hookLines2(plugin);
|
|
1661
2031
|
const hooksName = `the hook commands of ${ws}-setup`;
|
|
1662
2032
|
const hooksItem = item(hooksName, hooks);
|
|
1663
2033
|
if (update && hooks.length && !accepted.has(hooksItem.sha256)) {
|
|
1664
2034
|
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,
|
|
2035
|
+
previousPlugin = versions.sort((a, b) => b.version - a.version).find((v) => accepted.has(item(hooksName, hookLines2(v)).sha256));
|
|
1666
2036
|
}
|
|
1667
2037
|
items.push(item(`the plugin ${ws}-setup, version ${plugin.version}`, [`${ws}-setup, version ${plugin.version}`]));
|
|
1668
2038
|
if (hooks.length) {
|
|
@@ -1670,7 +2040,7 @@ async function runTeamSetup(opts) {
|
|
|
1670
2040
|
item(
|
|
1671
2041
|
hooksName,
|
|
1672
2042
|
hooks,
|
|
1673
|
-
previousPlugin ? lineDiff(
|
|
2043
|
+
previousPlugin ? lineDiff(hookLines2(previousPlugin).join("\n"), hooks.join("\n")) : void 0
|
|
1674
2044
|
)
|
|
1675
2045
|
);
|
|
1676
2046
|
}
|
|
@@ -1710,6 +2080,15 @@ async function runTeamSetup(opts) {
|
|
|
1710
2080
|
}
|
|
1711
2081
|
items.push(item(`autoMemoryDirectory in ${localSettings}`, [memoryFolder]));
|
|
1712
2082
|
}
|
|
2083
|
+
const imports = project && memoryFolder && !state.memory?.[project] ? await memoryImports(checkout, memoryFolder, opts.importDirs ?? [], opts.gitTop) : [];
|
|
2084
|
+
for (const i of imports) {
|
|
2085
|
+
items.push(
|
|
2086
|
+
item(
|
|
2087
|
+
`${i.named ? "the folder --import names" : "the memories Claude Code kept for this checkout"}, imported into ${project} on its first sync`,
|
|
2088
|
+
[`${i.folder}: ${i.files} file${i.files === 1 ? "" : "s"} to share`]
|
|
2089
|
+
)
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
1713
2092
|
const pending = items.filter((i) => !accepted.has(i.sha256));
|
|
1714
2093
|
if (pending.length) {
|
|
1715
2094
|
if (!canAsk) {
|
|
@@ -1779,6 +2158,7 @@ async function runTeamSetup(opts) {
|
|
|
1779
2158
|
const code = await syncMemory({
|
|
1780
2159
|
project,
|
|
1781
2160
|
dryRun: false,
|
|
2161
|
+
importDirs: imports.map((i) => i.folder),
|
|
1782
2162
|
client,
|
|
1783
2163
|
out: (...lines) => lines.forEach((l) => out(l)),
|
|
1784
2164
|
ask: opts.terminal ? opts.ask : void 0,
|
|
@@ -1819,7 +2199,7 @@ async function runTeamSetupCommand(args2, key, io) {
|
|
|
1819
2199
|
const client = new SystemDoxApiClient({ apiKey: key.apiKey, apiUrl: key.apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1820
2200
|
return runTeamSetup({ ...args2, client, apiKey: key.apiKey, keySource: "prompt", ...io });
|
|
1821
2201
|
}
|
|
1822
|
-
var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item,
|
|
2202
|
+
var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item, hookLines2, mcpLines;
|
|
1823
2203
|
var init_setup_team = __esm({
|
|
1824
2204
|
"src/setup-team.ts"() {
|
|
1825
2205
|
"use strict";
|
|
@@ -1827,7 +2207,7 @@ var init_setup_team = __esm({
|
|
|
1827
2207
|
init_memory_sync();
|
|
1828
2208
|
init_team_setup_files();
|
|
1829
2209
|
init_sync_memory();
|
|
1830
|
-
SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]";
|
|
2210
|
+
SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]";
|
|
1831
2211
|
HEADERS_HELPER = "npx -y @systemdox/mcp-server marketplace-auth";
|
|
1832
2212
|
SetupFailed = class extends Error {
|
|
1833
2213
|
};
|
|
@@ -1839,7 +2219,7 @@ var init_setup_team = __esm({
|
|
|
1839
2219
|
sha256: sha2562(`${name}
|
|
1840
2220
|
${lines.join("\n")}`)
|
|
1841
2221
|
});
|
|
1842
|
-
|
|
2222
|
+
hookLines2 = (p) => p.hookCommands ?? [];
|
|
1843
2223
|
mcpLines = (p) => (p.mcpServers ?? []).map((s) => `${s.name}: ${s.url ?? [s.command, ...s.args ?? []].filter(Boolean).join(" ")}`);
|
|
1844
2224
|
}
|
|
1845
2225
|
});
|
|
@@ -2326,7 +2706,7 @@ __export(plugin_path_exports, {
|
|
|
2326
2706
|
});
|
|
2327
2707
|
import { createHash as createHash4, randomBytes } from "crypto";
|
|
2328
2708
|
import { existsSync as existsSync5 } from "fs";
|
|
2329
|
-
import { mkdir as mkdir6, rename as
|
|
2709
|
+
import { mkdir as mkdir6, rename as rename3, writeFile as writeFile5 } from "fs/promises";
|
|
2330
2710
|
import { dirname as dirname5, join as join6, resolve as resolve5, sep as sep2 } from "path";
|
|
2331
2711
|
import { inflateRawSync } from "zlib";
|
|
2332
2712
|
function parsePluginPathArgs(argv) {
|
|
@@ -2494,7 +2874,7 @@ async function runPluginPath(argv, io) {
|
|
|
2494
2874
|
} else {
|
|
2495
2875
|
if (existsSync5(folder)) await removeFolder(folder);
|
|
2496
2876
|
try {
|
|
2497
|
-
await
|
|
2877
|
+
await rename3(tmp, folder);
|
|
2498
2878
|
} catch (e) {
|
|
2499
2879
|
if (!existsSync5(folder)) throw e;
|
|
2500
2880
|
await removeFolder(tmp);
|
|
@@ -3315,8 +3695,8 @@ function registerGetPlanBoard(server, apiClient2) {
|
|
|
3315
3695
|
]
|
|
3316
3696
|
};
|
|
3317
3697
|
}
|
|
3318
|
-
const
|
|
3319
|
-
if (
|
|
3698
|
+
const open2 = boards.filter((b) => !b.closed);
|
|
3699
|
+
if (open2.length === 0) {
|
|
3320
3700
|
return {
|
|
3321
3701
|
content: [
|
|
3322
3702
|
{
|
|
@@ -3326,16 +3706,16 @@ function registerGetPlanBoard(server, apiClient2) {
|
|
|
3326
3706
|
]
|
|
3327
3707
|
};
|
|
3328
3708
|
}
|
|
3329
|
-
const closedNote = boards.length >
|
|
3709
|
+
const closedNote = boards.length > open2.length ? `
|
|
3330
3710
|
|
|
3331
|
-
_${boards.length -
|
|
3711
|
+
_${boards.length - open2.length} closed board(s) not listed._` : "";
|
|
3332
3712
|
return {
|
|
3333
3713
|
content: [
|
|
3334
3714
|
{
|
|
3335
3715
|
type: "text",
|
|
3336
|
-
text: `${
|
|
3716
|
+
text: `${open2.length} open project board(s):
|
|
3337
3717
|
|
|
3338
|
-
${formatBoards(
|
|
3718
|
+
${formatBoards(open2)}` + closedNote + `
|
|
3339
3719
|
|
|
3340
3720
|
Call get_plan_board with project_number to list a board's issues.`
|
|
3341
3721
|
}
|
|
@@ -5295,7 +5675,9 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
|
5295
5675
|
project: args2.project,
|
|
5296
5676
|
dryRun: !!args2.dryRun,
|
|
5297
5677
|
client: apiClient2,
|
|
5298
|
-
out: (...l) => lines.push(...l)
|
|
5678
|
+
out: (...l) => lines.push(...l),
|
|
5679
|
+
// Nobody is at a terminal: a project the team no longer serves is left as it is.
|
|
5680
|
+
unattended: true
|
|
5299
5681
|
});
|
|
5300
5682
|
return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
|
|
5301
5683
|
}
|
|
@@ -5383,7 +5765,7 @@ var init_server = __esm({
|
|
|
5383
5765
|
init_create_prompt_template();
|
|
5384
5766
|
init_skills2();
|
|
5385
5767
|
init_team_setup();
|
|
5386
|
-
SERVER_VERSION = true ? "0.1.
|
|
5768
|
+
SERVER_VERSION = true ? "0.1.59" : "0.0.0-dev";
|
|
5387
5769
|
READ_TOOLS = [
|
|
5388
5770
|
"get_context",
|
|
5389
5771
|
"get_checks",
|
|
@@ -5512,21 +5894,26 @@ if (command === "setup") {
|
|
|
5512
5894
|
({ runPluginPath: runPluginPath2 }) => runPluginPath2(args.slice(1), { out: console.log, err: console.error })
|
|
5513
5895
|
).then((code) => process.exit(code));
|
|
5514
5896
|
} else if (command === "sync-memory") {
|
|
5515
|
-
const
|
|
5897
|
+
const hook = args.includes("--hook");
|
|
5898
|
+
const terminal = !hook && !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
5516
5899
|
Promise.resolve().then(() => (init_sync_memory(), sync_memory_exports)).then(
|
|
5517
5900
|
({ runSyncMemory: runSyncMemory2 }) => runSyncMemory2(args.slice(1), {
|
|
5518
5901
|
out: console.log,
|
|
5519
5902
|
err: console.error,
|
|
5520
|
-
ask: terminal ? (question) => prompt(question) : void 0
|
|
5903
|
+
ask: terminal ? (question) => prompt(question) : void 0,
|
|
5904
|
+
startedAt: performance.timeOrigin
|
|
5521
5905
|
})
|
|
5522
|
-
).then((code) => process.exit(code))
|
|
5906
|
+
).then((code) => process.exit(code)).catch((err) => {
|
|
5907
|
+
console.error(` Error: ${err.message}`);
|
|
5908
|
+
process.exit(hook ? 0 : 1);
|
|
5909
|
+
});
|
|
5523
5910
|
} else if (command === "--help" || command === "-h") {
|
|
5524
5911
|
console.log(`
|
|
5525
5912
|
SystemDox MCP Server
|
|
5526
5913
|
|
|
5527
5914
|
Usage:
|
|
5528
5915
|
systemdox-mcp-server Start the MCP server (stdio transport)
|
|
5529
|
-
systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]
|
|
5916
|
+
systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--import <folder>]... [--yes] [--overwrite]
|
|
5530
5917
|
Configure the MCP server in your AI coding tool,
|
|
5531
5918
|
then your team's setup: the team instructions,
|
|
5532
5919
|
the plugin's marketplace and the project's team
|
|
@@ -5543,10 +5930,15 @@ if (command === "setup") {
|
|
|
5543
5930
|
Print the header for your team's marketplace
|
|
5544
5931
|
(Claude Code runs it)
|
|
5545
5932
|
systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>]
|
|
5546
|
-
[--import <folder>] [--dry-run]
|
|
5933
|
+
[--import <folder>]... [--dry-run]
|
|
5547
5934
|
Sync a project's team memory with SystemDox
|
|
5548
5935
|
(default folder:
|
|
5549
5936
|
~/.claude/systemdox/<workspace>/memory/<project>)
|
|
5937
|
+
systemdox-mcp-server sync-memory --hook <workspace>
|
|
5938
|
+
The run the team plugin makes at the start and
|
|
5939
|
+
the end of a session: the memories this computer
|
|
5940
|
+
synced before, one line each, never a question,
|
|
5941
|
+
never a held-up session (Claude Code runs it)
|
|
5550
5942
|
systemdox-mcp-server --help Show this help message
|
|
5551
5943
|
|
|
5552
5944
|
Environment variables:
|