@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/tools/team-setup.js
CHANGED
|
@@ -147,7 +147,7 @@ var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
|
|
|
147
147
|
|
|
148
148
|
// src/sync-memory.ts
|
|
149
149
|
import { existsSync as existsSync2 } from "fs";
|
|
150
|
-
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
150
|
+
import { appendFile, mkdir as mkdir3, open, readdir as readdir2, readFile as readFile3, rename as rename2, rm as rm3, stat, writeFile as writeFile3 } from "fs/promises";
|
|
151
151
|
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
152
152
|
|
|
153
153
|
// src/lib/memory-sync.ts
|
|
@@ -171,15 +171,41 @@ function readFrontmatter(text2) {
|
|
|
171
171
|
const m = BLOCK_RE.exec(text2);
|
|
172
172
|
if (!m) return void 0;
|
|
173
173
|
const front = { shareFalse: false };
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
const lines = m[1].split(/\r?\n/);
|
|
175
|
+
if (lines.some((l) => /^[ \t]*\t/.test(l) && l.trim())) return front;
|
|
176
|
+
let topType;
|
|
177
|
+
let topShare;
|
|
178
|
+
let nestedType;
|
|
179
|
+
let nestedShare;
|
|
180
|
+
let inMetadata = false;
|
|
181
|
+
let nestedIndent;
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
if (!line.trim() || /^[ \t]*#/.test(line)) continue;
|
|
184
|
+
const indent = /^ */.exec(line)[0].length;
|
|
185
|
+
if (indent === 0) {
|
|
186
|
+
inMetadata = /^metadata[ \t]*:[ \t]*(#.*)?$/.test(line);
|
|
187
|
+
nestedIndent = void 0;
|
|
188
|
+
}
|
|
189
|
+
const kv = /^ *([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
176
190
|
if (!kv) continue;
|
|
177
191
|
const raw = kv[2].replace(/[ \t]+#.*$/, "");
|
|
178
192
|
const quoted = /^(["'])(.*)\1$/.exec(raw);
|
|
179
193
|
const value = quoted ? quoted[2] : raw;
|
|
180
|
-
|
|
181
|
-
if (
|
|
194
|
+
const isFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
|
|
195
|
+
if (indent === 0) {
|
|
196
|
+
if (kv[1] === "type") topType = value.trim().toLowerCase();
|
|
197
|
+
if (kv[1] === "share") topShare = isFalse;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!inMetadata) continue;
|
|
201
|
+
nestedIndent ??= indent;
|
|
202
|
+
if (indent !== nestedIndent) continue;
|
|
203
|
+
if (kv[1] === "type") nestedType = value.trim().toLowerCase();
|
|
204
|
+
if (kv[1] === "share") nestedShare = isFalse;
|
|
182
205
|
}
|
|
206
|
+
const type = topType ?? nestedType;
|
|
207
|
+
if (type !== void 0) front.type = type;
|
|
208
|
+
front.shareFalse = (topShare ?? nestedShare) === true;
|
|
183
209
|
return front;
|
|
184
210
|
}
|
|
185
211
|
function isShareable(path, text2) {
|
|
@@ -477,18 +503,50 @@ async function updateState(workspace, change) {
|
|
|
477
503
|
}
|
|
478
504
|
|
|
479
505
|
// src/sync-memory.ts
|
|
506
|
+
var HOOK_LOG_MAX_BYTES = 1024 * 1024;
|
|
480
507
|
var SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
|
|
481
508
|
var SyncFailed = class extends Error {
|
|
482
509
|
};
|
|
510
|
+
var SyncStopped = class extends Error {
|
|
511
|
+
constructor(reason, message) {
|
|
512
|
+
super(message);
|
|
513
|
+
this.reason = reason;
|
|
514
|
+
}
|
|
515
|
+
reason;
|
|
516
|
+
};
|
|
517
|
+
function failureReason(e) {
|
|
518
|
+
if (e instanceof SyncStopped) return e.reason;
|
|
519
|
+
const { status, code } = refusal(e);
|
|
520
|
+
if (status !== void 0) return code ? `SystemDox refused it, ${code}` : "SystemDox refused it";
|
|
521
|
+
const err = e;
|
|
522
|
+
const net = err?.cause?.code ?? err?.code ?? "";
|
|
523
|
+
if (e instanceof TypeError || /^(ECONN|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR)/.test(net)) {
|
|
524
|
+
return "SystemDox could not be reached";
|
|
525
|
+
}
|
|
526
|
+
return "it could not finish";
|
|
527
|
+
}
|
|
528
|
+
async function writeWhole(path, text2) {
|
|
529
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
530
|
+
const tmp = `${path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
531
|
+
try {
|
|
532
|
+
await writeFile3(tmp, text2, "utf8");
|
|
533
|
+
await rename2(tmp, path);
|
|
534
|
+
} catch (e) {
|
|
535
|
+
await rm3(tmp, { force: true });
|
|
536
|
+
throw e;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
483
539
|
function refusal(e) {
|
|
484
540
|
if (!(e instanceof Error)) return {};
|
|
485
541
|
const { status, code } = e;
|
|
486
542
|
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
487
543
|
}
|
|
488
544
|
var toPath = (root, rel) => join3(root, ...rel.split("/"));
|
|
545
|
+
var LEFT_TEMP_RE = /\.\d+\.[a-z0-9]+\.tmp$/;
|
|
489
546
|
async function readFolder(root) {
|
|
490
547
|
const files = {};
|
|
491
548
|
const names = /* @__PURE__ */ new Set();
|
|
549
|
+
const temps = [];
|
|
492
550
|
async function walk2(dir, prefix) {
|
|
493
551
|
for (const e of await readdir2(dir, { withFileTypes: true })) {
|
|
494
552
|
if (e.name.startsWith(".")) continue;
|
|
@@ -496,12 +554,13 @@ async function readFolder(root) {
|
|
|
496
554
|
if (e.isDirectory()) await walk2(join3(dir, e.name), rel);
|
|
497
555
|
else if (e.isFile()) {
|
|
498
556
|
names.add(rel.toLowerCase());
|
|
557
|
+
if (LEFT_TEMP_RE.test(e.name)) temps.push(rel);
|
|
499
558
|
if (e.name.endsWith(".md")) files[rel] = await readFile3(join3(dir, e.name), "utf8");
|
|
500
559
|
}
|
|
501
560
|
}
|
|
502
561
|
}
|
|
503
562
|
if (existsSync2(root)) await walk2(root, "");
|
|
504
|
-
return { files, names };
|
|
563
|
+
return { files, names, temps };
|
|
505
564
|
}
|
|
506
565
|
async function pullTeam(client, ws, project) {
|
|
507
566
|
try {
|
|
@@ -550,7 +609,11 @@ var Report = class {
|
|
|
550
609
|
deletedHere = /* @__PURE__ */ new Set();
|
|
551
610
|
deletedFromTeam = /* @__PURE__ */ new Set();
|
|
552
611
|
conflicts = [];
|
|
612
|
+
/** The conflict copies written this run, by name. */
|
|
613
|
+
copies = /* @__PURE__ */ new Set();
|
|
553
614
|
skipped = /* @__PURE__ */ new Map();
|
|
615
|
+
/** Skipped files a sync in the member's terminal can send: held for a value, or for the vault. */
|
|
616
|
+
waiting = /* @__PURE__ */ new Set();
|
|
554
617
|
people = [];
|
|
555
618
|
toAnswer = /* @__PURE__ */ new Map();
|
|
556
619
|
add(plan) {
|
|
@@ -558,12 +621,32 @@ var Report = class {
|
|
|
558
621
|
plan.pushed.forEach((p) => this.pushed.add(p));
|
|
559
622
|
plan.deletedHere.forEach((p) => this.deletedHere.add(p));
|
|
560
623
|
plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
|
|
561
|
-
for (const c of plan.conflicts)
|
|
624
|
+
for (const c of plan.conflicts) {
|
|
625
|
+
this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
|
|
626
|
+
this.copies.add(c.copy);
|
|
627
|
+
}
|
|
562
628
|
for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
|
|
563
629
|
}
|
|
564
630
|
skip(path, reason) {
|
|
565
631
|
if (!this.skipped.has(path)) this.skipped.set(path, reason);
|
|
566
632
|
}
|
|
633
|
+
/** Skipped, and waiting for the member at a terminal. */
|
|
634
|
+
wait(path, reason) {
|
|
635
|
+
this.skip(path, reason);
|
|
636
|
+
this.waiting.add(path);
|
|
637
|
+
}
|
|
638
|
+
summary(outcome, version, reason) {
|
|
639
|
+
return {
|
|
640
|
+
outcome,
|
|
641
|
+
...reason ? { reason } : {},
|
|
642
|
+
version,
|
|
643
|
+
pulled: this.pulled.size,
|
|
644
|
+
pushed: this.pushed.size,
|
|
645
|
+
deleted: this.deletedHere.size + this.deletedFromTeam.size,
|
|
646
|
+
conflictCopies: [...this.copies].sort(),
|
|
647
|
+
waiting: [...this.waiting].sort()
|
|
648
|
+
};
|
|
649
|
+
}
|
|
567
650
|
lines(summary, dryRun) {
|
|
568
651
|
const out = [summary];
|
|
569
652
|
const section = (title, items) => {
|
|
@@ -666,7 +749,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
666
749
|
for (const path of g.files) {
|
|
667
750
|
if (leftOut.has(path)) continue;
|
|
668
751
|
current[path] = addShareFalse(current[path]);
|
|
669
|
-
await
|
|
752
|
+
await writeWhole(toPath(opts.dir, path), current[path]);
|
|
670
753
|
leftOut.add(path);
|
|
671
754
|
}
|
|
672
755
|
report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
|
|
@@ -769,7 +852,7 @@ async function people(opts, ws, files, report, hint) {
|
|
|
769
852
|
const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
|
|
770
853
|
if (yes) {
|
|
771
854
|
for (const path of paths) {
|
|
772
|
-
await
|
|
855
|
+
await writeWhole(toPath(opts.dir, path), rewritten[path]);
|
|
773
856
|
current[path] = rewritten[path];
|
|
774
857
|
}
|
|
775
858
|
}
|
|
@@ -784,11 +867,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
784
867
|
held.add(path);
|
|
785
868
|
const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
|
|
786
869
|
if (keys.some((k) => vaultDown.has(k))) {
|
|
787
|
-
report.
|
|
870
|
+
report.wait(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
|
|
788
871
|
} else if (keys.some((k) => !answers.has(k))) {
|
|
789
|
-
report.
|
|
872
|
+
report.wait(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
|
|
790
873
|
} else {
|
|
791
|
-
report.
|
|
874
|
+
report.wait(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
|
|
792
875
|
}
|
|
793
876
|
}
|
|
794
877
|
for (const g of groups) {
|
|
@@ -799,12 +882,11 @@ async function people(opts, ws, files, report, hint) {
|
|
|
799
882
|
}
|
|
800
883
|
return { files: current, held, leftOut };
|
|
801
884
|
}
|
|
885
|
+
async function removeLeftTemps(dir, temps) {
|
|
886
|
+
for (const rel of temps) await rm3(toPath(dir, rel), { force: true });
|
|
887
|
+
}
|
|
802
888
|
async function applyPlan(dir, plan) {
|
|
803
|
-
for (const [path, text2] of Object.entries(plan.writes))
|
|
804
|
-
const full = toPath(dir, path);
|
|
805
|
-
await mkdir3(dirname3(full), { recursive: true });
|
|
806
|
-
await writeFile3(full, text2, "utf8");
|
|
807
|
-
}
|
|
889
|
+
for (const [path, text2] of Object.entries(plan.writes)) await writeWhole(toPath(dir, path), text2);
|
|
808
890
|
for (const path of plan.deletes) await rm3(toPath(dir, path), { force: true });
|
|
809
891
|
}
|
|
810
892
|
var sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
|
|
@@ -849,8 +931,19 @@ async function syncMemory(opts) {
|
|
|
849
931
|
const { client, out } = opts;
|
|
850
932
|
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
851
933
|
try {
|
|
934
|
+
const checkDeadline = () => {
|
|
935
|
+
if (opts.deadline !== void 0 && Date.now() > opts.deadline) {
|
|
936
|
+
throw new SyncStopped("took too long", "Stopped: the sync's time ran out before anything more was written or sent.");
|
|
937
|
+
}
|
|
938
|
+
};
|
|
852
939
|
const own = await client.getOwnTeamSetup();
|
|
853
940
|
const ws = own.workspace;
|
|
941
|
+
if (opts.workspace && ws !== opts.workspace) {
|
|
942
|
+
throw new SyncStopped(
|
|
943
|
+
"the key is for another workspace",
|
|
944
|
+
`The key is for ${ws}, not ${opts.workspace}: nothing was synced. Set SYSTEMDOX_API_KEY to a key of ${opts.workspace}, or unset it.`
|
|
945
|
+
);
|
|
946
|
+
}
|
|
854
947
|
const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
|
|
855
948
|
if (!project) {
|
|
856
949
|
throw new SyncFailed(
|
|
@@ -864,6 +957,12 @@ async function syncMemory(opts) {
|
|
|
864
957
|
const date = localDate(now);
|
|
865
958
|
const stored = (await readState(ws)).memory?.[project];
|
|
866
959
|
let team = await pullTeam(client, ws, project);
|
|
960
|
+
if (opts.unattended && stored && !team.exists) {
|
|
961
|
+
throw new SyncStopped(
|
|
962
|
+
"the team memory could not be read",
|
|
963
|
+
`${ws}/${project} could not be read from SystemDox: nothing was changed. Run sync-memory in your terminal to see why.`
|
|
964
|
+
);
|
|
965
|
+
}
|
|
867
966
|
let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
|
|
868
967
|
const folder = await readFolder(dir);
|
|
869
968
|
const shared = {};
|
|
@@ -876,14 +975,16 @@ async function syncMemory(opts) {
|
|
|
876
975
|
} else shared[path] = text2;
|
|
877
976
|
}
|
|
878
977
|
const importWrites = {};
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
978
|
+
const current = { ...folder.files };
|
|
979
|
+
const taken = new Set(folder.names);
|
|
980
|
+
for (const importDir of opts.importDirs ?? []) {
|
|
981
|
+
const other = await readFolder(resolve2(importDir));
|
|
882
982
|
for (const [path, text2] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
|
|
883
983
|
if (!isShareable(path, text2) || !isFilePath(path)) continue;
|
|
884
|
-
const here =
|
|
984
|
+
const here = current[path];
|
|
885
985
|
if (here === void 0) {
|
|
886
986
|
shared[path] = text2;
|
|
987
|
+
current[path] = text2;
|
|
887
988
|
importWrites[path] = text2;
|
|
888
989
|
taken.add(path.toLowerCase());
|
|
889
990
|
} else if (here !== text2) {
|
|
@@ -891,16 +992,14 @@ async function syncMemory(opts) {
|
|
|
891
992
|
taken.add(copy.toLowerCase());
|
|
892
993
|
importWrites[copy] = text2;
|
|
893
994
|
report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
|
|
995
|
+
report.copies.add(copy);
|
|
894
996
|
}
|
|
895
997
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
await writeFile3(full, text2, "utf8");
|
|
902
|
-
}
|
|
903
|
-
}
|
|
998
|
+
}
|
|
999
|
+
folder.names = taken;
|
|
1000
|
+
if (!opts.dryRun) {
|
|
1001
|
+
checkDeadline();
|
|
1002
|
+
for (const [path, text2] of Object.entries(importWrites)) await writeWhole(toPath(dir, path), text2);
|
|
904
1003
|
}
|
|
905
1004
|
const found = await people(run2, ws, shared, report, hint);
|
|
906
1005
|
for (const path of found.leftOut) {
|
|
@@ -925,12 +1024,16 @@ async function syncMemory(opts) {
|
|
|
925
1024
|
);
|
|
926
1025
|
return 0;
|
|
927
1026
|
}
|
|
1027
|
+
checkDeadline();
|
|
1028
|
+
if (attempt === 1) await removeLeftTemps(dir, folder.temps);
|
|
928
1029
|
await applyPlan(dir, plan);
|
|
929
1030
|
let version = team.version;
|
|
930
1031
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1032
|
+
checkDeadline();
|
|
931
1033
|
await upload(client, ws, project, plan, team.manifest, report);
|
|
932
1034
|
}
|
|
933
1035
|
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1036
|
+
checkDeadline();
|
|
934
1037
|
try {
|
|
935
1038
|
version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
|
|
936
1039
|
} catch (e) {
|
|
@@ -942,6 +1045,7 @@ async function syncMemory(opts) {
|
|
|
942
1045
|
false
|
|
943
1046
|
)
|
|
944
1047
|
);
|
|
1048
|
+
opts.summary?.(report.summary("moved-on", team.version, "moved on twice"));
|
|
945
1049
|
return 2;
|
|
946
1050
|
}
|
|
947
1051
|
base = team;
|
|
@@ -966,10 +1070,21 @@ async function syncMemory(opts) {
|
|
|
966
1070
|
false
|
|
967
1071
|
)
|
|
968
1072
|
);
|
|
1073
|
+
opts.summary?.(report.summary("synced", version));
|
|
969
1074
|
return 0;
|
|
970
1075
|
}
|
|
971
1076
|
} catch (e) {
|
|
972
1077
|
out(e.message);
|
|
1078
|
+
opts.summary?.({
|
|
1079
|
+
outcome: "failed",
|
|
1080
|
+
reason: failureReason(e),
|
|
1081
|
+
version: 0,
|
|
1082
|
+
pulled: 0,
|
|
1083
|
+
pushed: 0,
|
|
1084
|
+
deleted: 0,
|
|
1085
|
+
conflictCopies: [],
|
|
1086
|
+
waiting: []
|
|
1087
|
+
});
|
|
973
1088
|
return 1;
|
|
974
1089
|
}
|
|
975
1090
|
}
|
|
@@ -1141,7 +1256,9 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
|
|
|
1141
1256
|
project: args.project,
|
|
1142
1257
|
dryRun: !!args.dryRun,
|
|
1143
1258
|
client: apiClient,
|
|
1144
|
-
out: (...l) => lines.push(...l)
|
|
1259
|
+
out: (...l) => lines.push(...l),
|
|
1260
|
+
// Nobody is at a terminal: a project the team no longer serves is left as it is.
|
|
1261
|
+
unattended: true
|
|
1145
1262
|
});
|
|
1146
1263
|
return { ...text(lines.join("\n")), ...code ? { isError: true } : {} };
|
|
1147
1264
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@systemdox/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.58",
|
|
4
4
|
"mcpName": "io.github.puglieseweb/systemdox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Give AI coding agents your architecture context over MCP — checks, specs, ADRs, project rules, and docs from SystemDox, delivered at write time",
|