@systemdox/mcp-server 0.1.46 → 0.1.48
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 +69 -0
- package/dist/cli.js +1575 -158
- package/dist/http.js +927 -17
- package/dist/index.js +927 -17
- package/dist/lib/memory-sync.d.ts +150 -0
- package/dist/lib/memory-sync.js +363 -0
- package/dist/lib/person-values.d.ts +6 -3
- package/dist/lib/person-values.js +1 -1
- package/dist/setup-team.d.ts +80 -0
- package/dist/setup-team.js +1593 -0
- package/dist/sync-memory.d.ts +53 -0
- package/dist/sync-memory.js +1219 -0
- package/dist/tools/team-setup.d.ts +5 -1
- package/dist/tools/team-setup.js +920 -10
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -105,6 +105,21 @@ async function writeJsonAtomically(path, value, mode) {
|
|
|
105
105
|
async function readCredentials() {
|
|
106
106
|
return readJson(credentialsPath(), {});
|
|
107
107
|
}
|
|
108
|
+
async function restrictToMember(path) {
|
|
109
|
+
await chmod(path, 384);
|
|
110
|
+
if (process.platform === "win32") {
|
|
111
|
+
await run("icacls", [path, "/inheritance:r", "/grant:r", `${userInfo().username}:F`], { windowsHide: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function writeCredentials(workspace, apiKey) {
|
|
115
|
+
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
116
|
+
if (!apiKey) throw new Error("an API key is required");
|
|
117
|
+
const path = credentialsPath();
|
|
118
|
+
const credentials = await readCredentials();
|
|
119
|
+
credentials[workspace] = { apiKey };
|
|
120
|
+
await writeJsonAtomically(path, credentials, 384);
|
|
121
|
+
await restrictToMember(path);
|
|
122
|
+
}
|
|
108
123
|
async function apiKeyFor(workspace, env = process.env) {
|
|
109
124
|
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
110
125
|
const entry = (await readCredentials())[workspace];
|
|
@@ -396,8 +411,8 @@ var init_api_client = __esm({
|
|
|
396
411
|
answerValues(workspace, answers) {
|
|
397
412
|
return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
|
|
398
413
|
}
|
|
399
|
-
fileMemoryPeople(workspace,
|
|
400
|
-
return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people });
|
|
414
|
+
fileMemoryPeople(workspace, people2) {
|
|
415
|
+
return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people: people2 });
|
|
401
416
|
}
|
|
402
417
|
resolvePerson(workspace, personId) {
|
|
403
418
|
return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
|
|
@@ -433,14 +448,1378 @@ var init_api_client = __esm({
|
|
|
433
448
|
}
|
|
434
449
|
});
|
|
435
450
|
|
|
436
|
-
// src/lib/
|
|
451
|
+
// src/lib/person-values.ts
|
|
452
|
+
function lineOf(text3, start) {
|
|
453
|
+
let line = 1;
|
|
454
|
+
for (let i = text3.indexOf("\n"); i !== -1 && i < start; i = text3.indexOf("\n", i + 1)) line++;
|
|
455
|
+
return line;
|
|
456
|
+
}
|
|
457
|
+
function phoneShape(value) {
|
|
458
|
+
const groups = value.replace(/^\++/, "").split(/[ -]/);
|
|
459
|
+
const digits = [...groups.join("")];
|
|
460
|
+
if (value.startsWith("+")) return digits.length >= 8 && digits.length <= 15 && digits[0] !== "0";
|
|
461
|
+
if (digits.length < 10 || digits.length > 11) return false;
|
|
462
|
+
return groups.length === 1 || groups.slice(1).every((g) => GROUP_SIZES.has([...g].length));
|
|
463
|
+
}
|
|
464
|
+
function normalise(kind, value) {
|
|
465
|
+
const v = typeof value === "string" ? value.trim() : "";
|
|
466
|
+
if (kind === PHONE) {
|
|
467
|
+
if (!(whole(E164).test(v) || whole(NATIONAL).test(v)) || !phoneShape(v)) {
|
|
468
|
+
throw new Error("not a phone number");
|
|
469
|
+
}
|
|
470
|
+
const digits = v.replace(/[ -]/g, "");
|
|
471
|
+
return digits.startsWith("+") ? digits : NATIONAL_PREFIX + digits.slice(1);
|
|
472
|
+
}
|
|
473
|
+
if (kind === EMAIL) {
|
|
474
|
+
if (!whole(EMAIL_RE).test(v)) throw new Error("not an email address");
|
|
475
|
+
return v.toLowerCase();
|
|
476
|
+
}
|
|
477
|
+
throw new Error(`not a value kind: ${JSON.stringify(kind)}`);
|
|
478
|
+
}
|
|
479
|
+
function findPersonValues(text3) {
|
|
480
|
+
if (typeof text3 !== "string" || !text3) return [];
|
|
481
|
+
const skipped = [...text3.matchAll(global(PERSON_TOKEN))].map((m) => [m.index, m.index + m[0].length]);
|
|
482
|
+
const found = [];
|
|
483
|
+
const add = (kind, m, normalised) => {
|
|
484
|
+
const start = m.index;
|
|
485
|
+
const end = start + m[0].length;
|
|
486
|
+
if (skipped.some(([s, e]) => s <= start && end <= e)) return;
|
|
487
|
+
if (found.some((f) => f.start < end && start < f.end)) return;
|
|
488
|
+
found.push({ kind, value: m[0], normalised, line: lineOf(text3, start), start, end });
|
|
489
|
+
};
|
|
490
|
+
for (const m of text3.matchAll(global(EMAIL_RE))) add(EMAIL, m, m[0].toLowerCase());
|
|
491
|
+
for (const source of [E164, NATIONAL]) {
|
|
492
|
+
for (const m of text3.matchAll(global(source))) {
|
|
493
|
+
if (phoneShape(m[0])) add(PHONE, m, normalise(PHONE, m[0]));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const m of text3.matchAll(global(HOME, "i"))) add(HOME_PATH, m, m[0]);
|
|
497
|
+
return found.sort((a, b) => a.start - b.start);
|
|
498
|
+
}
|
|
499
|
+
function findSecrets(text3) {
|
|
500
|
+
if (typeof text3 !== "string" || !text3) return [];
|
|
501
|
+
return [...text3.matchAll(global(KEY_PREFIX))].map((m) => ({ kind: "key-prefix", line: lineOf(text3, m.index) }));
|
|
502
|
+
}
|
|
503
|
+
var PHONE, EMAIL, HOME_PATH, NATIONAL_PREFIX, W, D, WORD_OR, PERSON_ID, PERSON_TOKEN, BEFORE, AFTER, E164, NATIONAL, GROUP_SIZES, ATEXT, EMAIL_RE, HOME_PREFIX, HOME, KEY_PREFIX, global, whole;
|
|
504
|
+
var init_person_values = __esm({
|
|
505
|
+
"src/lib/person-values.ts"() {
|
|
506
|
+
"use strict";
|
|
507
|
+
PHONE = "phone";
|
|
508
|
+
EMAIL = "email";
|
|
509
|
+
HOME_PATH = "home-path";
|
|
510
|
+
NATIONAL_PREFIX = "+44";
|
|
511
|
+
W = "[\\p{L}\\p{N}_]";
|
|
512
|
+
D = "\\p{Nd}";
|
|
513
|
+
WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
|
|
514
|
+
PERSON_ID = "(?:(?:con|ctc)_[0-9A-Za-z]{10,40}|usr_[0-9A-Za-z-]{10,40})";
|
|
515
|
+
PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
|
|
516
|
+
BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
|
|
517
|
+
AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
|
|
518
|
+
E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
|
|
519
|
+
NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
|
|
520
|
+
GROUP_SIZES = /* @__PURE__ */ new Set([2, 3, 4, 5, 6]);
|
|
521
|
+
ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
|
|
522
|
+
EMAIL_RE = "(?<![A-Za-z0-9!#$%&*+/=?^_{|}~.@\\-])[A-Za-z0-9_]" + ATEXT + "*(?:\\." + ATEXT + "+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}(?![A-Za-z0-9\\-]|\\.[A-Za-z0-9])";
|
|
523
|
+
HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
|
|
524
|
+
HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
|
|
525
|
+
KEY_PREFIX = `(?<!${W})(?:pweb_[a-z]+_key_|sk-ant-|sk_live_|rk_live_|github_pat_|gh[pousr]_|sntrys_|xox[baprs]-)[A-Za-z0-9_\\-]{4,}`;
|
|
526
|
+
global = (source, flags = "") => new RegExp(source, `gu${flags}`);
|
|
527
|
+
whole = (source, flags = "") => new RegExp(`^(?:${source})$`, `u${flags}`);
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// src/lib/memory-sync.ts
|
|
437
532
|
import { createHash } from "crypto";
|
|
438
|
-
|
|
533
|
+
function sha256(text3) {
|
|
534
|
+
return createHash("sha256").update(text3, "utf8").digest("hex");
|
|
535
|
+
}
|
|
536
|
+
function isFilePath(path) {
|
|
537
|
+
return path.length <= 512 && FILE_PATH_RE.test(path) && !path.split("/").some((s) => s === "." || s === "..");
|
|
538
|
+
}
|
|
539
|
+
function isConflictCopy(path) {
|
|
540
|
+
return CONFLICT_COPY_RE.test(path.split("/").pop() ?? path);
|
|
541
|
+
}
|
|
542
|
+
function readFrontmatter(text3) {
|
|
543
|
+
const m = BLOCK_RE.exec(text3);
|
|
544
|
+
if (!m) return void 0;
|
|
545
|
+
const front = { shareFalse: false };
|
|
546
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
547
|
+
const kv = /^([A-Za-z_][\w-]*)[ \t]*:[ \t]*(.*?)[ \t]*$/.exec(line);
|
|
548
|
+
if (!kv) continue;
|
|
549
|
+
const raw = kv[2].replace(/[ \t]+#.*$/, "");
|
|
550
|
+
const quoted = /^(["'])(.*)\1$/.exec(raw);
|
|
551
|
+
const value = quoted ? quoted[2] : raw;
|
|
552
|
+
if (kv[1] === "type") front.type = value.trim().toLowerCase();
|
|
553
|
+
if (kv[1] === "share") front.shareFalse = quoted ? value.trim().toLowerCase() === "false" : YAML_FALSE.has(value);
|
|
554
|
+
}
|
|
555
|
+
return front;
|
|
556
|
+
}
|
|
557
|
+
function isShareable(path, text3) {
|
|
558
|
+
if (isConflictCopy(path) || !path.endsWith(".md")) return false;
|
|
559
|
+
const front = readFrontmatter(text3);
|
|
560
|
+
if (front?.shareFalse) return false;
|
|
561
|
+
if (path === INDEX_FILE) return true;
|
|
562
|
+
return !!front && SHAREABLE_TYPES.includes(front.type ?? "");
|
|
563
|
+
}
|
|
564
|
+
function addShareFalse(text3) {
|
|
565
|
+
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
566
|
+
const m = BLOCK_RE.exec(text3);
|
|
567
|
+
if (!m) return `---${eol}share: false${eol}---${eol}${text3}`;
|
|
568
|
+
const lines = m[1].split(/\r?\n/).filter((l) => !/^share[ \t]*:/.test(l));
|
|
569
|
+
lines.push("share: false");
|
|
570
|
+
const closing = m[0].slice(m[0].lastIndexOf("---"));
|
|
571
|
+
return `---${eol}${lines.join(eol)}${eol}${closing}${text3.slice(m[0].length)}`;
|
|
572
|
+
}
|
|
573
|
+
function localDate(now) {
|
|
574
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
575
|
+
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
|
576
|
+
}
|
|
577
|
+
function conflictCopyName(path, date, taken) {
|
|
578
|
+
const stem = path.replace(/\.md$/, "");
|
|
579
|
+
for (let n = 1; ; n++) {
|
|
580
|
+
const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
|
|
581
|
+
if (!taken.has(name.toLowerCase())) return name;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function decide(path, base, team, local) {
|
|
585
|
+
if (team === local) return "unchanged";
|
|
586
|
+
if (team === base) return local === void 0 ? "delete-team" : "local";
|
|
587
|
+
if (local === base) return team === void 0 ? "delete-local" : "team";
|
|
588
|
+
if (path === INDEX_FILE && team !== void 0 && local !== void 0) return "index";
|
|
589
|
+
if (team === void 0) return "local";
|
|
590
|
+
if (local === void 0) return "team";
|
|
591
|
+
return "conflict";
|
|
592
|
+
}
|
|
593
|
+
function decideAll(input) {
|
|
594
|
+
const localHashes = Object.fromEntries(Object.entries(input.local).map(([p, t]) => [p, sha256(t)]));
|
|
595
|
+
const paths = /* @__PURE__ */ new Set([...Object.keys(input.base), ...Object.keys(input.team), ...Object.keys(localHashes)]);
|
|
596
|
+
const rows = {};
|
|
597
|
+
const needed = /* @__PURE__ */ new Set();
|
|
598
|
+
for (const path of [...paths].sort()) {
|
|
599
|
+
if (input.excluded.has(path)) continue;
|
|
600
|
+
const row = decide(path, input.base[path], input.team[path], localHashes[path]);
|
|
601
|
+
rows[path] = row;
|
|
602
|
+
if (row === "team" || row === "conflict" || row === "index") needed.add(input.team[path]);
|
|
603
|
+
if (row === "index" && input.base[path]) needed.add(input.base[path]);
|
|
604
|
+
}
|
|
605
|
+
return { rows, needed: [...needed] };
|
|
606
|
+
}
|
|
607
|
+
function planMerge(input, rows, texts) {
|
|
608
|
+
const plan = {
|
|
609
|
+
manifest: {},
|
|
610
|
+
upload: {},
|
|
611
|
+
writes: {},
|
|
612
|
+
deletes: [],
|
|
613
|
+
pulled: [],
|
|
614
|
+
pushed: [],
|
|
615
|
+
deletedHere: [],
|
|
616
|
+
deletedFromTeam: [],
|
|
617
|
+
conflicts: [],
|
|
618
|
+
indexTeamLines: []
|
|
619
|
+
};
|
|
620
|
+
const taken = new Set(input.taken);
|
|
621
|
+
const teamText = (path) => {
|
|
622
|
+
const text3 = texts[input.team[path]];
|
|
623
|
+
if (text3 === void 0) throw new Error(`the team's ${path} could not be read`);
|
|
624
|
+
return text3;
|
|
625
|
+
};
|
|
626
|
+
for (const path of input.excluded) if (input.team[path]) plan.manifest[path] = input.team[path];
|
|
627
|
+
for (const [path, row] of Object.entries(rows)) {
|
|
628
|
+
const team = input.team[path];
|
|
629
|
+
const local = input.local[path];
|
|
630
|
+
switch (row) {
|
|
631
|
+
case "unchanged":
|
|
632
|
+
if (team) plan.manifest[path] = team;
|
|
633
|
+
break;
|
|
634
|
+
case "local":
|
|
635
|
+
plan.manifest[path] = sha256(local);
|
|
636
|
+
plan.upload[path] = local;
|
|
637
|
+
plan.pushed.push(path);
|
|
638
|
+
break;
|
|
639
|
+
case "delete-team":
|
|
640
|
+
plan.deletedFromTeam.push(path);
|
|
641
|
+
break;
|
|
642
|
+
case "team":
|
|
643
|
+
plan.manifest[path] = team;
|
|
644
|
+
plan.writes[path] = teamText(path);
|
|
645
|
+
plan.pulled.push(path);
|
|
646
|
+
break;
|
|
647
|
+
case "delete-local":
|
|
648
|
+
plan.deletes.push(path);
|
|
649
|
+
plan.deletedHere.push(path);
|
|
650
|
+
break;
|
|
651
|
+
case "conflict": {
|
|
652
|
+
const copy = conflictCopyName(path, input.date, taken);
|
|
653
|
+
taken.add(copy.toLowerCase());
|
|
654
|
+
plan.manifest[path] = team;
|
|
655
|
+
plan.writes[path] = teamText(path);
|
|
656
|
+
plan.writes[copy] = local;
|
|
657
|
+
plan.conflicts.push({ path, copy });
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case "index": {
|
|
661
|
+
const base = input.base[path] ? texts[input.base[path]] : void 0;
|
|
662
|
+
const merged = mergeIndex(base, teamText(path), local);
|
|
663
|
+
const hash = sha256(merged.text);
|
|
664
|
+
plan.manifest[path] = hash;
|
|
665
|
+
if (merged.text !== local) {
|
|
666
|
+
plan.writes[path] = merged.text;
|
|
667
|
+
plan.pulled.push(path);
|
|
668
|
+
}
|
|
669
|
+
if (hash !== team) {
|
|
670
|
+
plan.upload[path] = merged.text;
|
|
671
|
+
plan.pushed.push(path);
|
|
672
|
+
}
|
|
673
|
+
plan.indexTeamLines.push(...merged.teamLines);
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return plan;
|
|
679
|
+
}
|
|
680
|
+
function entries(lines) {
|
|
681
|
+
const seen = /* @__PURE__ */ new Map();
|
|
682
|
+
return lines.map((line) => {
|
|
683
|
+
const link = LINK_RE.exec(line);
|
|
684
|
+
const id = link ? `link:${link[1]}` : `text:${line}`;
|
|
685
|
+
const n = seen.get(id) ?? 0;
|
|
686
|
+
seen.set(id, n + 1);
|
|
687
|
+
return { key: n ? `${id}#${n}` : id, line };
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
function mergeIndex(base, team, local) {
|
|
691
|
+
const eol = local.includes("\r\n") ? "\r\n" : "\n";
|
|
692
|
+
const b = new Map(entries(base === void 0 ? [] : splitLines(base)).map((e) => [e.key, e.line]));
|
|
693
|
+
const teamEntries = entries(splitLines(team));
|
|
694
|
+
const t = new Map(teamEntries.map((e) => [e.key, e.line]));
|
|
695
|
+
const localEntries = entries(splitLines(local));
|
|
696
|
+
const result = [];
|
|
697
|
+
const teamLines = [];
|
|
698
|
+
for (const e of localEntries) {
|
|
699
|
+
const was = b.get(e.key);
|
|
700
|
+
const theirs = t.get(e.key);
|
|
701
|
+
if (theirs === e.line || theirs === was) result.push(e);
|
|
702
|
+
else if (e.line === was) {
|
|
703
|
+
if (theirs !== void 0) result.push({ key: e.key, line: theirs });
|
|
704
|
+
} else {
|
|
705
|
+
result.push(e);
|
|
706
|
+
if (theirs !== void 0) teamLines.push(theirs);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const baseEntries = entries(base === void 0 ? [] : splitLines(base));
|
|
710
|
+
const localKeys = new Set(localEntries.map((e) => e.key));
|
|
711
|
+
const replacedHere = /* @__PURE__ */ new Map();
|
|
712
|
+
baseEntries.forEach((e, i) => {
|
|
713
|
+
if (e.key.startsWith("text:") && !t.has(e.key) && !localKeys.has(e.key)) {
|
|
714
|
+
replacedHere.set(e.key, i ? baseEntries[i - 1].key : "");
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
const inResult = (key) => result.findIndex((e) => e.key === key);
|
|
718
|
+
teamEntries.forEach((e, i) => {
|
|
719
|
+
if (inResult(e.key) !== -1) return;
|
|
720
|
+
const was = b.get(e.key);
|
|
721
|
+
const before = i ? teamEntries[i - 1].key : "";
|
|
722
|
+
const both = e.key.startsWith("text:") && [...replacedHere].find(([, prev]) => prev === before);
|
|
723
|
+
if (was === void 0 && both) {
|
|
724
|
+
replacedHere.delete(both[0]);
|
|
725
|
+
teamLines.push(e.line);
|
|
726
|
+
} else if (was === void 0) {
|
|
727
|
+
let at = -1;
|
|
728
|
+
for (let j = i - 1; j >= 0 && at === -1; j--) at = inResult(teamEntries[j].key);
|
|
729
|
+
result.splice(at + 1, 0, e);
|
|
730
|
+
} else if (e.line !== was) {
|
|
731
|
+
teamLines.push(e.line);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
return { text: result.map((e) => e.line).join(eol), teamLines };
|
|
735
|
+
}
|
|
736
|
+
function collectValues(files) {
|
|
737
|
+
const groups = /* @__PURE__ */ new Map();
|
|
738
|
+
const homePaths = [];
|
|
739
|
+
const findings = {};
|
|
740
|
+
for (const path of Object.keys(files).sort()) {
|
|
741
|
+
const found = findPersonValues(files[path]);
|
|
742
|
+
findings[path] = found;
|
|
743
|
+
for (const f of found) {
|
|
744
|
+
if (f.kind === HOME_PATH) {
|
|
745
|
+
if (!homePaths.includes(path)) homePaths.push(path);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
const key = groupKey(f.kind, f.normalised);
|
|
749
|
+
const group = groups.get(key);
|
|
750
|
+
if (group) {
|
|
751
|
+
if (!group.files.includes(path)) group.files.push(path);
|
|
752
|
+
} else {
|
|
753
|
+
groups.set(key, {
|
|
754
|
+
kind: f.kind,
|
|
755
|
+
value: f.value,
|
|
756
|
+
normalised: f.normalised,
|
|
757
|
+
files: [path],
|
|
758
|
+
first: { path, line: f.line, text: splitLines(files[path])[f.line - 1].trim() }
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return { groups: [...groups.values()], homePaths, findings };
|
|
764
|
+
}
|
|
765
|
+
function personToken(personId, label) {
|
|
766
|
+
return label ? `\`${personId}\` (${label})` : `\`${personId}\``;
|
|
767
|
+
}
|
|
768
|
+
function rewriteValues(text3, findings, replacement) {
|
|
769
|
+
let out = "";
|
|
770
|
+
let last = 0;
|
|
771
|
+
for (const f of [...findings].sort((a, b) => a.start - b.start)) {
|
|
772
|
+
const to = replacement(f);
|
|
773
|
+
if (to === void 0) continue;
|
|
774
|
+
out += text3.slice(last, f.start) + to;
|
|
775
|
+
last = f.end;
|
|
776
|
+
}
|
|
777
|
+
return out + text3.slice(last);
|
|
778
|
+
}
|
|
779
|
+
function lineDiff(before, after) {
|
|
780
|
+
const a = splitLines(before);
|
|
781
|
+
const b = splitLines(after);
|
|
782
|
+
const out = [];
|
|
783
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
784
|
+
if (a[i] === b[i]) continue;
|
|
785
|
+
if (a[i] !== void 0) out.push(`- ${a[i]}`);
|
|
786
|
+
if (b[i] !== void 0) out.push(`+ ${b[i]}`);
|
|
787
|
+
}
|
|
788
|
+
return out;
|
|
789
|
+
}
|
|
790
|
+
var INDEX_FILE, SHAREABLE_TYPES, FILE_PATH_RE, CONFLICT_COPY_RE, BLOCK_RE, YAML_FALSE, LINK_RE, splitLines, groupKey;
|
|
791
|
+
var init_memory_sync = __esm({
|
|
792
|
+
"src/lib/memory-sync.ts"() {
|
|
793
|
+
"use strict";
|
|
794
|
+
init_person_values();
|
|
795
|
+
INDEX_FILE = "MEMORY.md";
|
|
796
|
+
SHAREABLE_TYPES = ["feedback", "project", "reference"];
|
|
797
|
+
FILE_PATH_RE = /^[A-Za-z0-9._-]+(\/[A-Za-z0-9._-]+)*$/;
|
|
798
|
+
CONFLICT_COPY_RE = /\.local-\d{4}-\d{2}-\d{2}(?:-\d+)?\.md$/;
|
|
799
|
+
BLOCK_RE = /^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/;
|
|
800
|
+
YAML_FALSE = /* @__PURE__ */ new Set(["false", "False", "FALSE", "no", "No", "NO", "off", "Off", "OFF"]);
|
|
801
|
+
LINK_RE = /\]\(\s*<?([^)\s>]+)>?(?:\s+["'(][^)]*)?\)/;
|
|
802
|
+
splitLines = (text3) => text3.split(/\r?\n/);
|
|
803
|
+
groupKey = (kind, normalised) => `${kind}:${normalised}`;
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
// src/sync-memory.ts
|
|
808
|
+
var sync_memory_exports = {};
|
|
809
|
+
__export(sync_memory_exports, {
|
|
810
|
+
SYNC_MEMORY_COMMAND: () => SYNC_MEMORY_COMMAND,
|
|
811
|
+
SYNC_MEMORY_USAGE: () => SYNC_MEMORY_USAGE,
|
|
812
|
+
parseSyncMemoryArgs: () => parseSyncMemoryArgs,
|
|
813
|
+
runSyncMemory: () => runSyncMemory,
|
|
814
|
+
syncMemory: () => syncMemory
|
|
815
|
+
});
|
|
439
816
|
import { existsSync } from "fs";
|
|
817
|
+
import { mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
818
|
+
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
819
|
+
function refusal(e) {
|
|
820
|
+
if (!(e instanceof Error)) return {};
|
|
821
|
+
const { status, code } = e;
|
|
822
|
+
return { status: typeof status === "number" ? status : void 0, code: typeof code === "string" ? code : void 0 };
|
|
823
|
+
}
|
|
824
|
+
function parseSyncMemoryArgs(argv) {
|
|
825
|
+
const args2 = { dryRun: false };
|
|
826
|
+
for (let i = 0; i < argv.length; i++) {
|
|
827
|
+
const a = argv[i];
|
|
828
|
+
if (a === "--dry-run") args2.dryRun = true;
|
|
829
|
+
else if (a === "--project" || a === "--dir" || a === "--import") {
|
|
830
|
+
const value = argv[++i];
|
|
831
|
+
if (!value || value.startsWith("--")) throw new SyncFailed(`${a} needs a value
|
|
832
|
+
${SYNC_MEMORY_USAGE}`);
|
|
833
|
+
if (a === "--project") {
|
|
834
|
+
if (!isWorkspace(value)) throw new SyncFailed(`${value} is not a project name (lower case letters, digits and -)`);
|
|
835
|
+
args2.project = value;
|
|
836
|
+
} else if (a === "--dir") args2.dir = value;
|
|
837
|
+
else args2.importDir = value;
|
|
838
|
+
} else throw new SyncFailed(`Unexpected argument ${a}
|
|
839
|
+
${SYNC_MEMORY_USAGE}`);
|
|
840
|
+
}
|
|
841
|
+
return args2;
|
|
842
|
+
}
|
|
843
|
+
async function readFolder(root) {
|
|
844
|
+
const files = {};
|
|
845
|
+
const names = /* @__PURE__ */ new Set();
|
|
846
|
+
async function walk2(dir, prefix) {
|
|
847
|
+
for (const e of await readdir(dir, { withFileTypes: true })) {
|
|
848
|
+
if (e.name.startsWith(".")) continue;
|
|
849
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
850
|
+
if (e.isDirectory()) await walk2(join2(dir, e.name), rel);
|
|
851
|
+
else if (e.isFile()) {
|
|
852
|
+
names.add(rel.toLowerCase());
|
|
853
|
+
if (e.name.endsWith(".md")) files[rel] = await readFile2(join2(dir, e.name), "utf8");
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
if (existsSync(root)) await walk2(root, "");
|
|
858
|
+
return { files, names };
|
|
859
|
+
}
|
|
860
|
+
async function pullTeam(client, ws, project) {
|
|
861
|
+
try {
|
|
862
|
+
const latest = await client.getMemory(ws, project);
|
|
863
|
+
return {
|
|
864
|
+
version: latest.version,
|
|
865
|
+
manifest: Object.fromEntries(Object.entries(latest.files).map(([p, f]) => [p, f.sha256])),
|
|
866
|
+
exists: true
|
|
867
|
+
};
|
|
868
|
+
} catch (e) {
|
|
869
|
+
if (refusal(e).status === 404) return { version: 0, manifest: {}, exists: false };
|
|
870
|
+
throw e;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
async function readTexts(client, ws, project, hashes) {
|
|
874
|
+
const texts = {};
|
|
875
|
+
const one = async (batch) => {
|
|
876
|
+
let left = batch;
|
|
877
|
+
while (left.length) {
|
|
878
|
+
const got = await client.readBlobs(ws, project, left);
|
|
879
|
+
for (const b of got.blobs) texts[b.sha256] = b.content;
|
|
880
|
+
if (!got.blobs.length) break;
|
|
881
|
+
left = got.remaining;
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
for (let i = 0; i < hashes.length; i += 1e3) {
|
|
885
|
+
const batch = hashes.slice(i, i + 1e3);
|
|
886
|
+
try {
|
|
887
|
+
await one(batch);
|
|
888
|
+
} catch (e) {
|
|
889
|
+
if (!(refusal(e).status === 404)) throw e;
|
|
890
|
+
for (const h of batch) {
|
|
891
|
+
try {
|
|
892
|
+
await one([h]);
|
|
893
|
+
} catch (err) {
|
|
894
|
+
if (!(refusal(err).status === 404)) throw err;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return texts;
|
|
900
|
+
}
|
|
901
|
+
function labelProblem(label) {
|
|
902
|
+
if (!label) return "a label is needed";
|
|
903
|
+
if (label.length > 60) return "a label is at most 60 characters";
|
|
904
|
+
if (findPersonValues(label).length) return "a label is a role, never a phone number, an email address or a path";
|
|
905
|
+
return void 0;
|
|
906
|
+
}
|
|
907
|
+
async function askLabel(ask, out, question, optional) {
|
|
908
|
+
for (; ; ) {
|
|
909
|
+
const label = (await ask(question)).trim();
|
|
910
|
+
if (optional && !label) return void 0;
|
|
911
|
+
const problem = labelProblem(label);
|
|
912
|
+
if (!problem) return label;
|
|
913
|
+
out(` ${problem}.`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
async function people(opts, ws, files, report, hint) {
|
|
917
|
+
const { client, out, ask, dryRun } = opts;
|
|
918
|
+
const terminal = !!ask && !dryRun;
|
|
919
|
+
const held = /* @__PURE__ */ new Set();
|
|
920
|
+
const leftOut = /* @__PURE__ */ new Set();
|
|
921
|
+
const current = { ...files };
|
|
922
|
+
for (const [path, text3] of Object.entries(current)) {
|
|
923
|
+
const secret = findSecrets(text3)[0];
|
|
924
|
+
if (secret) {
|
|
925
|
+
held.add(path);
|
|
926
|
+
report.skip(path, `a secret (${secret.kind}) on line ${secret.line}: remove it, then sync again`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const { groups, findings } = collectValues(current);
|
|
930
|
+
const answers = /* @__PURE__ */ new Map();
|
|
931
|
+
for (let i = 0; i < groups.length; i += 500) {
|
|
932
|
+
const batch = groups.slice(i, i + 500);
|
|
933
|
+
const got = await client.lookupValues(ws, batch.map((g) => ({ kind: g.kind, value: g.value })));
|
|
934
|
+
batch.forEach((g, j) => {
|
|
935
|
+
const a = got.answers[j];
|
|
936
|
+
if (a && a.answer !== "unanswered") answers.set(groupKey(g.kind, g.normalised), a);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
const vaultDown = /* @__PURE__ */ new Set();
|
|
940
|
+
if (terminal) {
|
|
941
|
+
const filed = [];
|
|
942
|
+
for (const g of groups) {
|
|
943
|
+
const key = groupKey(g.kind, g.normalised);
|
|
944
|
+
if (answers.has(key)) continue;
|
|
945
|
+
let choices = ["m", "p", "o", "x", "s"];
|
|
946
|
+
for (; ; ) {
|
|
947
|
+
const question = [
|
|
948
|
+
`${g.value} (${g.kind}) is in ${g.files.length} file${g.files.length === 1 ? "" : "s"}. First line it appears on, in ${g.first.path}:`,
|
|
949
|
+
` ${g.first.text}`,
|
|
950
|
+
[
|
|
951
|
+
choices.includes("m") && "[m] a member's contact",
|
|
952
|
+
"[p] someone else",
|
|
953
|
+
"[o] ours, not a person",
|
|
954
|
+
"[x] leave these files out",
|
|
955
|
+
"[s] skip for now"
|
|
956
|
+
].filter(Boolean).join(" "),
|
|
957
|
+
`Answer (${choices.join("/")}): `
|
|
958
|
+
].join("\n");
|
|
959
|
+
const answer = (await ask(question)).trim().toLowerCase();
|
|
960
|
+
if (!choices.includes(answer)) continue;
|
|
961
|
+
if (answer === "s") break;
|
|
962
|
+
if (answer === "x") {
|
|
963
|
+
for (const path of g.files) {
|
|
964
|
+
if (leftOut.has(path)) continue;
|
|
965
|
+
current[path] = addShareFalse(current[path]);
|
|
966
|
+
await writeFile2(toPath(opts.dir, path), current[path], "utf8");
|
|
967
|
+
leftOut.add(path);
|
|
968
|
+
}
|
|
969
|
+
report.people.push({ kind: g.kind, answer: "left out (share: false)", files: g.files.length });
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
if (answer === "p") {
|
|
973
|
+
const label2 = await askLabel(ask, out, "Their role, as a label (e.g. reseller, sales; never a name): ", false);
|
|
974
|
+
let entry = filed.length ? await (async () => {
|
|
975
|
+
const list = filed.map((f, i) => ` ${i + 1}. ${f.label}: ${f.groups.map((x) => x.value).join(", ")}`);
|
|
976
|
+
for (; ; ) {
|
|
977
|
+
const same = (await ask(["The same person as one filed above?", ...list, "Number, or Enter for a new person: "].join("\n"))).trim();
|
|
978
|
+
if (!same) return void 0;
|
|
979
|
+
const n = Number(same);
|
|
980
|
+
if (Number.isInteger(n) && n >= 1 && n <= filed.length && filed[n - 1].groups.length < 4) return filed[n - 1];
|
|
981
|
+
}
|
|
982
|
+
})() : void 0;
|
|
983
|
+
if (!entry) {
|
|
984
|
+
entry = { groups: [], label: label2 };
|
|
985
|
+
filed.push(entry);
|
|
986
|
+
}
|
|
987
|
+
entry.groups.push(g);
|
|
988
|
+
break;
|
|
989
|
+
}
|
|
990
|
+
const label = answer === "m" ? await askLabel(ask, out, "A role label for it (e.g. owner, business mobile), or Enter for none: ", true) : void 0;
|
|
991
|
+
let result;
|
|
992
|
+
try {
|
|
993
|
+
result = await client.answerValues(ws, [
|
|
994
|
+
{ kind: g.kind, value: g.value, answer: answer === "m" ? "member" : "ours", ...label ? { label } : {} }
|
|
995
|
+
]);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
if (refusal(e).code === "SYD-K019") {
|
|
998
|
+
vaultDown.add(key);
|
|
999
|
+
out(" The vault did not answer (SYD-K019): the files holding it wait for the next sync.");
|
|
1000
|
+
break;
|
|
1001
|
+
}
|
|
1002
|
+
throw e;
|
|
1003
|
+
}
|
|
1004
|
+
if (result.stored[0]) {
|
|
1005
|
+
answers.set(key, result.stored[0]);
|
|
1006
|
+
report.people.push({ kind: g.kind, answer: ANSWER_WORDS[result.stored[0].answer], label: result.stored[0].label, personId: result.stored[0].personId });
|
|
1007
|
+
break;
|
|
1008
|
+
}
|
|
1009
|
+
const refused = result.refused[0];
|
|
1010
|
+
if (refused?.code === "SYD-K021") {
|
|
1011
|
+
out(` ${refused.detail}. Add them in Staff first, or answer p to file them.`);
|
|
1012
|
+
choices = choices.filter((c) => c !== "m");
|
|
1013
|
+
} else if (refused) out(` ${refused.detail}`);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
for (let i = 0; i < filed.length; i += 50) {
|
|
1017
|
+
const batch = filed.slice(i, i + 50);
|
|
1018
|
+
let result;
|
|
1019
|
+
try {
|
|
1020
|
+
result = await client.fileMemoryPeople(
|
|
1021
|
+
ws,
|
|
1022
|
+
batch.map((f) => ({ values: f.groups.map((g) => ({ kind: g.kind, value: g.value })), label: f.label }))
|
|
1023
|
+
);
|
|
1024
|
+
} catch (e) {
|
|
1025
|
+
if (refusal(e).code === "SYD-K019") {
|
|
1026
|
+
out("Contacts did not answer (SYD-K019): the files holding those values wait for the next sync.");
|
|
1027
|
+
batch.forEach((f) => f.groups.forEach((g) => vaultDown.add(groupKey(g.kind, g.normalised))));
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
throw e;
|
|
1031
|
+
}
|
|
1032
|
+
for (const stored of result.stored) {
|
|
1033
|
+
let key;
|
|
1034
|
+
try {
|
|
1035
|
+
key = groupKey(stored.kind, normalise(stored.kind, stored.value));
|
|
1036
|
+
} catch {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
answers.set(key, stored);
|
|
1040
|
+
}
|
|
1041
|
+
for (const r of result.refused) {
|
|
1042
|
+
out(` ${batch[r.index]?.label ?? ""}: ${r.detail}`);
|
|
1043
|
+
}
|
|
1044
|
+
for (const f of batch) {
|
|
1045
|
+
const a = answers.get(groupKey(f.groups[0].kind, f.groups[0].normalised));
|
|
1046
|
+
if (a?.answer === "person") {
|
|
1047
|
+
for (const g of f.groups) report.people.push({ kind: g.kind, answer: ANSWER_WORDS.person, label: a.label, personId: a.personId });
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const rewritten = {};
|
|
1053
|
+
for (const [path, text3] of Object.entries(current)) {
|
|
1054
|
+
if (leftOut.has(path) || held.has(path)) continue;
|
|
1055
|
+
const next = rewriteValues(text3, findings[path] ?? [], (f) => {
|
|
1056
|
+
if (f.kind === HOME_PATH) return "~";
|
|
1057
|
+
const a = answers.get(groupKey(f.kind, f.normalised));
|
|
1058
|
+
return a && (a.answer === "member" || a.answer === "person") && a.personId ? personToken(a.personId, a.label) : void 0;
|
|
1059
|
+
});
|
|
1060
|
+
if (next !== text3) rewritten[path] = next;
|
|
1061
|
+
}
|
|
1062
|
+
const paths = Object.keys(rewritten).sort();
|
|
1063
|
+
if (paths.length && terminal) {
|
|
1064
|
+
out("", "The rewrite:");
|
|
1065
|
+
for (const path of paths) out(path, ...lineDiff(current[path], rewritten[path]).map((l) => ` ${l}`));
|
|
1066
|
+
const yes = /^y(es)?$/i.test((await ask(`Rewrite ${paths.length} file${paths.length === 1 ? "" : "s"} as shown? [y/N] `)).trim());
|
|
1067
|
+
if (yes) {
|
|
1068
|
+
for (const path of paths) {
|
|
1069
|
+
await writeFile2(toPath(opts.dir, path), rewritten[path], "utf8");
|
|
1070
|
+
current[path] = rewritten[path];
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
for (const [path, text3] of Object.entries(current)) {
|
|
1075
|
+
if (leftOut.has(path) || held.has(path)) continue;
|
|
1076
|
+
const pending = findPersonValues(text3).filter((f) => {
|
|
1077
|
+
if (f.kind === HOME_PATH) return true;
|
|
1078
|
+
return answers.get(groupKey(f.kind, f.normalised))?.answer !== "ours";
|
|
1079
|
+
});
|
|
1080
|
+
if (!pending.length) continue;
|
|
1081
|
+
held.add(path);
|
|
1082
|
+
const keys = pending.filter((f) => f.kind !== HOME_PATH).map((f) => groupKey(f.kind, f.normalised));
|
|
1083
|
+
if (keys.some((k) => vaultDown.has(k))) {
|
|
1084
|
+
report.skip(path, "the vault did not answer (SYD-K019) for a value in it; it syncs once the value is filed");
|
|
1085
|
+
} else if (keys.some((k) => !answers.has(k))) {
|
|
1086
|
+
report.skip(path, `it holds a value nobody has answered for: run \`${hint}\` in a terminal`);
|
|
1087
|
+
} else {
|
|
1088
|
+
report.skip(path, `it holds a value to rewrite as its identifier or ~: run \`${hint}\` in a terminal`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
for (const g of groups) {
|
|
1092
|
+
const key = groupKey(g.kind, g.normalised);
|
|
1093
|
+
if (!answers.has(key) && g.files.some((p) => !leftOut.has(p))) {
|
|
1094
|
+
report.toAnswer.set(g.kind, (report.toAnswer.get(g.kind) ?? 0) + 1);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return { files: current, held, leftOut };
|
|
1098
|
+
}
|
|
1099
|
+
async function applyPlan(dir, plan) {
|
|
1100
|
+
for (const [path, text3] of Object.entries(plan.writes)) {
|
|
1101
|
+
const full = toPath(dir, path);
|
|
1102
|
+
await mkdir2(dirname2(full), { recursive: true });
|
|
1103
|
+
await writeFile2(full, text3, "utf8");
|
|
1104
|
+
}
|
|
1105
|
+
for (const path of plan.deletes) await rm2(toPath(dir, path), { force: true });
|
|
1106
|
+
}
|
|
1107
|
+
async function upload(client, ws, project, plan, team, report) {
|
|
1108
|
+
const byHash = /* @__PURE__ */ new Map();
|
|
1109
|
+
for (const path of Object.keys(plan.upload)) {
|
|
1110
|
+
const h = plan.manifest[path];
|
|
1111
|
+
byHash.set(h, [...byHash.get(h) ?? [], path]);
|
|
1112
|
+
}
|
|
1113
|
+
const hashes = [...byHash.keys()];
|
|
1114
|
+
const missing = [];
|
|
1115
|
+
for (let i = 0; i < hashes.length; i += 1e4) {
|
|
1116
|
+
missing.push(...(await client.findMissingBlobs(ws, project, hashes.slice(i, i + 1e4))).missing);
|
|
1117
|
+
}
|
|
1118
|
+
const blobs = missing.map((h) => ({ sha256: h, content: plan.upload[byHash.get(h)[0]] }));
|
|
1119
|
+
let batch = [];
|
|
1120
|
+
let bytes = 0;
|
|
1121
|
+
const send = async () => {
|
|
1122
|
+
if (!batch.length) return;
|
|
1123
|
+
const result = await client.uploadBlobs(ws, project, batch);
|
|
1124
|
+
for (const r of result.refused) {
|
|
1125
|
+
for (const path of byHash.get(r.sha256) ?? []) {
|
|
1126
|
+
if (team[path]) plan.manifest[path] = team[path];
|
|
1127
|
+
else delete plan.manifest[path];
|
|
1128
|
+
plan.pushed = plan.pushed.filter((p) => p !== path);
|
|
1129
|
+
report.pushed.delete(path);
|
|
1130
|
+
report.skip(path, `refused by SystemDox, ${r.detail.startsWith(r.code) ? r.detail : `${r.code}: ${r.detail}`}`);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
batch = [];
|
|
1134
|
+
bytes = 0;
|
|
1135
|
+
};
|
|
1136
|
+
for (const blob of blobs) {
|
|
1137
|
+
const size = Buffer.byteLength(blob.content, "utf8");
|
|
1138
|
+
if (batch.length === 100 || batch.length && bytes + size > 5 * 1024 * 1024 - 64 * 1024) await send();
|
|
1139
|
+
batch.push(blob);
|
|
1140
|
+
bytes += size;
|
|
1141
|
+
}
|
|
1142
|
+
await send();
|
|
1143
|
+
}
|
|
1144
|
+
async function keyFromEnvironment(env) {
|
|
1145
|
+
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
1146
|
+
const entries2 = Object.entries(await readCredentials()).filter(([, v]) => typeof v?.apiKey === "string" && v.apiKey);
|
|
1147
|
+
if (entries2.length === 1) return entries2[0][1].apiKey;
|
|
1148
|
+
if (entries2.length > 1) {
|
|
1149
|
+
throw new SyncFailed(
|
|
1150
|
+
`~/.claude/systemdox/credentials.json holds keys for ${entries2.map(([w]) => w).join(", ")}: set SYSTEMDOX_API_KEY to the key of the workspace to sync.`
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
throw new SyncFailed("No SystemDox key. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup, then run this again.");
|
|
1154
|
+
}
|
|
1155
|
+
async function syncMemory(opts) {
|
|
1156
|
+
const { client, out } = opts;
|
|
1157
|
+
const now = opts.now?.() ?? /* @__PURE__ */ new Date();
|
|
1158
|
+
try {
|
|
1159
|
+
const own = await client.getOwnTeamSetup();
|
|
1160
|
+
const ws = own.workspace;
|
|
1161
|
+
const project = opts.project ?? (own.memory.length === 1 ? own.memory[0].project : void 0);
|
|
1162
|
+
if (!project) {
|
|
1163
|
+
throw new SyncFailed(
|
|
1164
|
+
own.memory.length ? `${ws} has ${own.memory.length} team memories (${own.memory.map((m) => m.project).join(", ")}): name one with --project.` : `${ws} has no team memory yet: name the project to start with --project.`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
const dir = resolve(opts.dir ?? join2(workspaceDir(ws), "memory", project));
|
|
1168
|
+
const run3 = { ...opts, dir };
|
|
1169
|
+
const hint = [SYNC_MEMORY_COMMAND, `--project ${project}`, opts.dir ? `--dir "${opts.dir}"` : ""].filter(Boolean).join(" ");
|
|
1170
|
+
const report = new Report();
|
|
1171
|
+
const date = localDate(now);
|
|
1172
|
+
const stored = (await readState(ws)).memory?.[project];
|
|
1173
|
+
let team = await pullTeam(client, ws, project);
|
|
1174
|
+
let base = stored ? { version: stored.version, manifest: stored.manifest, exists: true } : { version: 0, manifest: {}, exists: false };
|
|
1175
|
+
const folder = await readFolder(dir);
|
|
1176
|
+
const shared = {};
|
|
1177
|
+
const notShared = /* @__PURE__ */ new Set();
|
|
1178
|
+
for (const [path, text3] of Object.entries(folder.files)) {
|
|
1179
|
+
if (!isShareable(path, text3)) notShared.add(path);
|
|
1180
|
+
else if (!isFilePath(path)) {
|
|
1181
|
+
notShared.add(path);
|
|
1182
|
+
report.skip(path, "its name has characters a team memory path cannot hold (letters, digits, '.', '_', '-')");
|
|
1183
|
+
} else shared[path] = text3;
|
|
1184
|
+
}
|
|
1185
|
+
const importWrites = {};
|
|
1186
|
+
if (opts.importDir) {
|
|
1187
|
+
const other = await readFolder(resolve(opts.importDir));
|
|
1188
|
+
const taken = new Set(folder.names);
|
|
1189
|
+
for (const [path, text3] of Object.entries(other.files).sort(([a], [b]) => a.localeCompare(b))) {
|
|
1190
|
+
if (!isShareable(path, text3) || !isFilePath(path)) continue;
|
|
1191
|
+
const here = folder.files[path];
|
|
1192
|
+
if (here === void 0) {
|
|
1193
|
+
shared[path] = text3;
|
|
1194
|
+
importWrites[path] = text3;
|
|
1195
|
+
taken.add(path.toLowerCase());
|
|
1196
|
+
} else if (here !== text3) {
|
|
1197
|
+
const copy = conflictCopyName(path, date, taken);
|
|
1198
|
+
taken.add(copy.toLowerCase());
|
|
1199
|
+
importWrites[copy] = text3;
|
|
1200
|
+
report.conflicts.push(`${path}: this folder's text; the imported one is beside it as ${copy}`);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
folder.names = taken;
|
|
1204
|
+
if (!opts.dryRun) {
|
|
1205
|
+
for (const [path, text3] of Object.entries(importWrites)) {
|
|
1206
|
+
const full = toPath(dir, path);
|
|
1207
|
+
await mkdir2(dirname2(full), { recursive: true });
|
|
1208
|
+
await writeFile2(full, text3, "utf8");
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
const found = await people(run3, ws, shared, report, hint);
|
|
1213
|
+
for (const path of found.leftOut) {
|
|
1214
|
+
delete found.files[path];
|
|
1215
|
+
notShared.add(path);
|
|
1216
|
+
}
|
|
1217
|
+
const excluded = /* @__PURE__ */ new Set([...found.held, ...notShared]);
|
|
1218
|
+
for (const path of found.held) delete found.files[path];
|
|
1219
|
+
for (let attempt = 1; ; attempt++) {
|
|
1220
|
+
const input = { base: base.manifest, team: team.manifest, local: found.files, excluded, taken: folder.names, date };
|
|
1221
|
+
const { rows, needed } = decideAll(input);
|
|
1222
|
+
const texts = needed.length ? await readTexts(client, ws, project, needed) : {};
|
|
1223
|
+
const plan = planMerge(input, rows, texts);
|
|
1224
|
+
report.add(plan);
|
|
1225
|
+
if (opts.dryRun) {
|
|
1226
|
+
const skipped = report.skipped.size;
|
|
1227
|
+
out(
|
|
1228
|
+
...report.lines(
|
|
1229
|
+
`Dry run of ${ws}/${project} against version ${team.version}: would pull ${report.pulled.size}, push ${report.pushed.size} and delete ${report.deletedHere.size + report.deletedFromTeam.size}; ${report.conflicts.length} conflicts, ${skipped} skipped. Nothing was asked, written or sent.`,
|
|
1230
|
+
true
|
|
1231
|
+
)
|
|
1232
|
+
);
|
|
1233
|
+
return 0;
|
|
1234
|
+
}
|
|
1235
|
+
await applyPlan(dir, plan);
|
|
1236
|
+
let version = team.version;
|
|
1237
|
+
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1238
|
+
await upload(client, ws, project, plan, team.manifest, report);
|
|
1239
|
+
}
|
|
1240
|
+
if (!sameManifest(plan.manifest, team.manifest)) {
|
|
1241
|
+
try {
|
|
1242
|
+
version = (await client.pushMemoryVersion(ws, project, { baseVersion: team.version, manifest: plan.manifest })).version;
|
|
1243
|
+
} catch (e) {
|
|
1244
|
+
if (!(refusal(e).status === 409 && refusal(e).code === "SYD-K010")) throw e;
|
|
1245
|
+
if (attempt === 2) {
|
|
1246
|
+
out(
|
|
1247
|
+
...report.lines(
|
|
1248
|
+
`Not pushed: ${ws}/${project} moved on twice while this sync merged. Your folder holds the merge; run sync-memory again.`,
|
|
1249
|
+
false
|
|
1250
|
+
)
|
|
1251
|
+
);
|
|
1252
|
+
return 2;
|
|
1253
|
+
}
|
|
1254
|
+
base = team;
|
|
1255
|
+
for (const p of plan.pushed) report.pushed.delete(p);
|
|
1256
|
+
const again = await readFolder(dir);
|
|
1257
|
+
folder.names = again.names;
|
|
1258
|
+
found.files = {};
|
|
1259
|
+
for (const [path, text3] of Object.entries(again.files)) {
|
|
1260
|
+
if (excluded.has(path) || !isShareable(path, text3) || !isFilePath(path)) continue;
|
|
1261
|
+
found.files[path] = text3;
|
|
1262
|
+
}
|
|
1263
|
+
team = await pullTeam(client, ws, project);
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
await updateState(ws, (state) => {
|
|
1268
|
+
state.memory = { ...state.memory, [project]: { version, manifest: plan.manifest } };
|
|
1269
|
+
});
|
|
1270
|
+
out(
|
|
1271
|
+
...report.lines(
|
|
1272
|
+
`Synced ${ws}/${project} at version ${version}: ${report.pulled.size} pulled, ${report.pushed.size} pushed, ${report.deletedHere.size + report.deletedFromTeam.size} deleted, ${report.conflicts.length} conflicts, ${report.skipped.size} skipped.`,
|
|
1273
|
+
false
|
|
1274
|
+
)
|
|
1275
|
+
);
|
|
1276
|
+
return 0;
|
|
1277
|
+
}
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
out(e.message);
|
|
1280
|
+
return 1;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async function runSyncMemory(argv, io, env = process.env) {
|
|
1284
|
+
let args2;
|
|
1285
|
+
let client = io.client;
|
|
1286
|
+
try {
|
|
1287
|
+
args2 = parseSyncMemoryArgs(argv);
|
|
1288
|
+
if (!client) {
|
|
1289
|
+
const apiUrl = apiUrlFrom(env);
|
|
1290
|
+
const apiKey = await keyFromEnvironment(env);
|
|
1291
|
+
client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1292
|
+
}
|
|
1293
|
+
} catch (e) {
|
|
1294
|
+
io.err(e.message);
|
|
1295
|
+
return 1;
|
|
1296
|
+
}
|
|
1297
|
+
return syncMemory({ ...args2, client, out: (...lines) => lines.forEach((l) => io.out(l)), ask: io.ask, now: io.now });
|
|
1298
|
+
}
|
|
1299
|
+
var SYNC_MEMORY_USAGE, SYNC_MEMORY_COMMAND, SyncFailed, toPath, Report, ANSWER_WORDS, sameManifest;
|
|
1300
|
+
var init_sync_memory = __esm({
|
|
1301
|
+
"src/sync-memory.ts"() {
|
|
1302
|
+
"use strict";
|
|
1303
|
+
init_api_client();
|
|
1304
|
+
init_memory_sync();
|
|
1305
|
+
init_person_values();
|
|
1306
|
+
init_team_setup_files();
|
|
1307
|
+
SYNC_MEMORY_USAGE = "Usage: systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>] [--import <folder>] [--dry-run]";
|
|
1308
|
+
SYNC_MEMORY_COMMAND = "npx -y @systemdox/mcp-server sync-memory";
|
|
1309
|
+
SyncFailed = class extends Error {
|
|
1310
|
+
};
|
|
1311
|
+
toPath = (root, rel) => join2(root, ...rel.split("/"));
|
|
1312
|
+
Report = class {
|
|
1313
|
+
pulled = /* @__PURE__ */ new Set();
|
|
1314
|
+
pushed = /* @__PURE__ */ new Set();
|
|
1315
|
+
deletedHere = /* @__PURE__ */ new Set();
|
|
1316
|
+
deletedFromTeam = /* @__PURE__ */ new Set();
|
|
1317
|
+
conflicts = [];
|
|
1318
|
+
skipped = /* @__PURE__ */ new Map();
|
|
1319
|
+
people = [];
|
|
1320
|
+
toAnswer = /* @__PURE__ */ new Map();
|
|
1321
|
+
add(plan) {
|
|
1322
|
+
plan.pulled.forEach((p) => this.pulled.add(p));
|
|
1323
|
+
plan.pushed.forEach((p) => this.pushed.add(p));
|
|
1324
|
+
plan.deletedHere.forEach((p) => this.deletedHere.add(p));
|
|
1325
|
+
plan.deletedFromTeam.forEach((p) => this.deletedFromTeam.add(p));
|
|
1326
|
+
for (const c of plan.conflicts) this.conflicts.push(`${c.path}: the team's text; yours is beside it as ${c.copy}`);
|
|
1327
|
+
for (const line of plan.indexTeamLines) this.conflicts.push(`${INDEX_FILE}: your line was kept; the team's was: ${line}`);
|
|
1328
|
+
}
|
|
1329
|
+
skip(path, reason) {
|
|
1330
|
+
if (!this.skipped.has(path)) this.skipped.set(path, reason);
|
|
1331
|
+
}
|
|
1332
|
+
lines(summary, dryRun) {
|
|
1333
|
+
const out = [summary];
|
|
1334
|
+
const section = (title, items) => {
|
|
1335
|
+
if (items.length) out.push("", `${title}:`, ...items.map((i) => `- ${i}`));
|
|
1336
|
+
};
|
|
1337
|
+
const sorted = (s) => [...s].sort();
|
|
1338
|
+
section(dryRun ? "Would pull" : "Pulled", sorted(this.pulled));
|
|
1339
|
+
section(dryRun ? "Would push" : "Pushed", sorted(this.pushed));
|
|
1340
|
+
section(dryRun ? "Would delete" : "Deleted", [
|
|
1341
|
+
...sorted(this.deletedHere).map((p) => `${p} (here: the team deleted it)`),
|
|
1342
|
+
...sorted(this.deletedFromTeam).map((p) => `${p} (from the team: you deleted it)`)
|
|
1343
|
+
]);
|
|
1344
|
+
section("Conflicts", this.conflicts);
|
|
1345
|
+
section(
|
|
1346
|
+
"Skipped",
|
|
1347
|
+
[...this.skipped.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([p, why]) => `${p}: ${why}`)
|
|
1348
|
+
);
|
|
1349
|
+
section(
|
|
1350
|
+
"People",
|
|
1351
|
+
this.people.map(
|
|
1352
|
+
(a) => [a.kind, a.answer, a.label, a.personId && `\`${a.personId}\``, a.files !== void 0 && `${a.files} files`].filter(Boolean).join(", ")
|
|
1353
|
+
)
|
|
1354
|
+
);
|
|
1355
|
+
if (this.toAnswer.size) {
|
|
1356
|
+
out.push(
|
|
1357
|
+
"",
|
|
1358
|
+
`Values still to answer: ${[...this.toAnswer.entries()].map(([kind, n]) => `${n} ${kind}`).join(", ")}.`
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
return out;
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
ANSWER_WORDS = {
|
|
1365
|
+
member: "a member's contact",
|
|
1366
|
+
person: "someone else",
|
|
1367
|
+
ours: "ours, not a person"
|
|
1368
|
+
};
|
|
1369
|
+
sameManifest = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([p, h]) => b[p] === h);
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
// src/setup-team.ts
|
|
1374
|
+
var setup_team_exports = {};
|
|
1375
|
+
__export(setup_team_exports, {
|
|
1376
|
+
HEADERS_HELPER: () => HEADERS_HELPER,
|
|
1377
|
+
SETUP_USAGE: () => SETUP_USAGE,
|
|
1378
|
+
claudeCodeVersion: () => claudeCodeVersion,
|
|
1379
|
+
importBlock: () => importBlock,
|
|
1380
|
+
parseSetupArgs: () => parseSetupArgs,
|
|
1381
|
+
runTeamSetup: () => runTeamSetup,
|
|
1382
|
+
runTeamSetupCommand: () => runTeamSetupCommand,
|
|
1383
|
+
setJsonMember: () => setJsonMember,
|
|
1384
|
+
withImportBlock: () => withImportBlock
|
|
1385
|
+
});
|
|
1386
|
+
import { execFile as execFile2 } from "child_process";
|
|
1387
|
+
import { createHash as createHash2 } from "crypto";
|
|
1388
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1389
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
1390
|
+
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
1391
|
+
import { promisify as promisify2 } from "util";
|
|
1392
|
+
function parseSetupArgs(argv) {
|
|
1393
|
+
const args2 = { yes: false, overwrite: false };
|
|
1394
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1395
|
+
const a = argv[i];
|
|
1396
|
+
if (a === "--yes") args2.yes = true;
|
|
1397
|
+
else if (a === "--overwrite") args2.overwrite = true;
|
|
1398
|
+
else if (a === "--project" || a === "--dir") {
|
|
1399
|
+
const value = argv[++i];
|
|
1400
|
+
if (!value || value.startsWith("--")) throw new Error(`${a} needs a value
|
|
1401
|
+
${SETUP_USAGE}`);
|
|
1402
|
+
if (a === "--project") {
|
|
1403
|
+
if (!isWorkspace(value)) throw new Error(`${value} is not a project name (lower case letters, digits and -)`);
|
|
1404
|
+
args2.project = value;
|
|
1405
|
+
} else args2.dir = value;
|
|
1406
|
+
} else throw new Error(`Unexpected argument ${a}
|
|
1407
|
+
${SETUP_USAGE}`);
|
|
1408
|
+
}
|
|
1409
|
+
return args2;
|
|
1410
|
+
}
|
|
1411
|
+
async function claudeCodeVersion() {
|
|
1412
|
+
try {
|
|
1413
|
+
const { stdout } = await run2("claude", ["--version"], {
|
|
1414
|
+
shell: process.platform === "win32",
|
|
1415
|
+
timeout: 3e4,
|
|
1416
|
+
windowsHide: true
|
|
1417
|
+
});
|
|
1418
|
+
return stdout;
|
|
1419
|
+
} catch {
|
|
1420
|
+
return void 0;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function parseVersion(text3) {
|
|
1424
|
+
const m = text3?.match(/(\d+)\.(\d+)\.(\d+)/);
|
|
1425
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : void 0;
|
|
1426
|
+
}
|
|
1427
|
+
function olderThan(have, need) {
|
|
1428
|
+
for (let i = 0; i < 3; i++) if (have[i] !== need[i]) return have[i] < need[i];
|
|
1429
|
+
return false;
|
|
1430
|
+
}
|
|
1431
|
+
function skipSpace(t, i) {
|
|
1432
|
+
while (i < t.length && /\s/.test(t[i])) i++;
|
|
1433
|
+
return i;
|
|
1434
|
+
}
|
|
1435
|
+
function skipString(t, i) {
|
|
1436
|
+
for (i++; i < t.length; i++) {
|
|
1437
|
+
if (t[i] === "\\") i++;
|
|
1438
|
+
else if (t[i] === '"') return i + 1;
|
|
1439
|
+
}
|
|
1440
|
+
return i;
|
|
1441
|
+
}
|
|
1442
|
+
function skipValue(t, i) {
|
|
1443
|
+
if (t[i] === '"') return skipString(t, i);
|
|
1444
|
+
if (t[i] === "{" || t[i] === "[") {
|
|
1445
|
+
let depth = 0;
|
|
1446
|
+
while (i < t.length) {
|
|
1447
|
+
const c = t[i];
|
|
1448
|
+
if (c === '"') {
|
|
1449
|
+
i = skipString(t, i);
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (c === "{" || c === "[") depth++;
|
|
1453
|
+
else if (c === "}" || c === "]") {
|
|
1454
|
+
depth--;
|
|
1455
|
+
if (depth === 0) return i + 1;
|
|
1456
|
+
}
|
|
1457
|
+
i++;
|
|
1458
|
+
}
|
|
1459
|
+
return i;
|
|
1460
|
+
}
|
|
1461
|
+
while (i < t.length && !/[\s,}\]]/.test(t[i])) i++;
|
|
1462
|
+
return i;
|
|
1463
|
+
}
|
|
1464
|
+
function objectMembers(t, open) {
|
|
1465
|
+
const members = [];
|
|
1466
|
+
let i = skipSpace(t, open + 1);
|
|
1467
|
+
if (t[i] === "}") return { members, close: i };
|
|
1468
|
+
for (; ; ) {
|
|
1469
|
+
i = skipSpace(t, i);
|
|
1470
|
+
const keyStart = i;
|
|
1471
|
+
const keyEnd = skipString(t, i);
|
|
1472
|
+
const key = JSON.parse(t.slice(keyStart, keyEnd));
|
|
1473
|
+
i = skipSpace(t, skipSpace(t, keyEnd) + 1);
|
|
1474
|
+
const valueEnd = skipValue(t, i);
|
|
1475
|
+
members.push({ key, keyStart, valueStart: i, valueEnd });
|
|
1476
|
+
i = skipSpace(t, valueEnd);
|
|
1477
|
+
if (t[i] === ",") i++;
|
|
1478
|
+
else return { members, close: i };
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function lineIndent(t, pos) {
|
|
1482
|
+
const start = t.lastIndexOf("\n", pos - 1) + 1;
|
|
1483
|
+
return /^[ \t]*/.exec(t.slice(start))[0];
|
|
1484
|
+
}
|
|
1485
|
+
function setJsonMember(text3, keys, value) {
|
|
1486
|
+
const root = JSON.parse(text3);
|
|
1487
|
+
if (!root || typeof root !== "object" || Array.isArray(root)) throw new Error("it does not hold a JSON object");
|
|
1488
|
+
const compact = !text3.trimEnd().includes("\n");
|
|
1489
|
+
const unit = /^([ \t]+)"/m.exec(text3)?.[1] ?? " ";
|
|
1490
|
+
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
1491
|
+
const render = (v, indent) => compact ? JSON.stringify(v) : JSON.stringify(v, null, unit).split("\n").join(eol + indent);
|
|
1492
|
+
const nest = (from) => keys.slice(from).reduceRight((acc, k) => ({ [k]: acc }), value);
|
|
1493
|
+
let open = skipSpace(text3, 0);
|
|
1494
|
+
for (let k = 0; k < keys.length; k++) {
|
|
1495
|
+
const { members, close } = objectMembers(text3, open);
|
|
1496
|
+
const found = members.filter((m) => m.key === keys[k]).at(-1);
|
|
1497
|
+
if (found) {
|
|
1498
|
+
if (k < keys.length - 1 && text3[found.valueStart] === "{") {
|
|
1499
|
+
open = found.valueStart;
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
return text3.slice(0, found.valueStart) + render(nest(k + 1), lineIndent(text3, found.keyStart)) + text3.slice(found.valueEnd);
|
|
1503
|
+
}
|
|
1504
|
+
const name = JSON.stringify(keys[k]);
|
|
1505
|
+
if (compact) {
|
|
1506
|
+
const member = `${name}:${render(nest(k + 1), "")}`;
|
|
1507
|
+
if (members.length) return text3.slice(0, members.at(-1).valueEnd) + "," + member + text3.slice(members.at(-1).valueEnd);
|
|
1508
|
+
return text3.slice(0, open + 1) + member + text3.slice(close);
|
|
1509
|
+
}
|
|
1510
|
+
if (members.length) {
|
|
1511
|
+
const last = members.at(-1);
|
|
1512
|
+
const indent2 = lineIndent(text3, last.keyStart);
|
|
1513
|
+
return text3.slice(0, last.valueEnd) + `,${eol}${indent2}${name}: ${render(nest(k + 1), indent2)}` + text3.slice(last.valueEnd);
|
|
1514
|
+
}
|
|
1515
|
+
const outer = lineIndent(text3, open);
|
|
1516
|
+
const indent = outer + unit;
|
|
1517
|
+
return text3.slice(0, open + 1) + `${eol}${indent}${name}: ${render(nest(k + 1), indent)}${eol}${outer}` + text3.slice(close);
|
|
1518
|
+
}
|
|
1519
|
+
return text3;
|
|
1520
|
+
}
|
|
1521
|
+
function jsonAt(value, keys) {
|
|
1522
|
+
let v = value;
|
|
1523
|
+
for (const k of keys) v = v && typeof v === "object" ? v[k] : void 0;
|
|
1524
|
+
return v;
|
|
1525
|
+
}
|
|
1526
|
+
async function setInJsonFile(path, keys, value) {
|
|
1527
|
+
let text3;
|
|
1528
|
+
try {
|
|
1529
|
+
text3 = await readFile3(path, "utf8");
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
if (e.code !== "ENOENT") throw e;
|
|
1532
|
+
}
|
|
1533
|
+
let next;
|
|
1534
|
+
if (text3 === void 0 || !text3.trim()) {
|
|
1535
|
+
next = JSON.stringify(keys.reduceRight((acc, k) => ({ [k]: acc }), value), null, 2) + "\n";
|
|
1536
|
+
} else {
|
|
1537
|
+
let current;
|
|
1538
|
+
try {
|
|
1539
|
+
current = JSON.parse(text3);
|
|
1540
|
+
} catch {
|
|
1541
|
+
throw new SetupFailed(`${path} is not valid JSON: fix it, then run setup again.`);
|
|
1542
|
+
}
|
|
1543
|
+
if (JSON.stringify(jsonAt(current, keys)) === JSON.stringify(value)) return;
|
|
1544
|
+
try {
|
|
1545
|
+
next = setJsonMember(text3, keys, value);
|
|
1546
|
+
} catch (e) {
|
|
1547
|
+
throw new SetupFailed(`${path}: ${e.message}; fix it, then run setup again.`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
1551
|
+
await writeFile3(path, next, "utf8");
|
|
1552
|
+
}
|
|
1553
|
+
function importBlock(workspace, eol = "\n") {
|
|
1554
|
+
return [
|
|
1555
|
+
`<!-- systemdox:${workspace} -->`,
|
|
1556
|
+
`@~/.claude/systemdox/${workspace}/CLAUDE.md`,
|
|
1557
|
+
`<!-- /systemdox:${workspace} -->`
|
|
1558
|
+
].join(eol);
|
|
1559
|
+
}
|
|
1560
|
+
function withImportBlock(text3, workspace) {
|
|
1561
|
+
const eol = text3.includes("\r\n") ? "\r\n" : "\n";
|
|
1562
|
+
const block = importBlock(workspace, eol);
|
|
1563
|
+
const open = `<!-- systemdox:${workspace} -->`;
|
|
1564
|
+
const close = `<!-- /systemdox:${workspace} -->`;
|
|
1565
|
+
const start = text3.indexOf(open);
|
|
1566
|
+
const end = start >= 0 ? text3.indexOf(close, start) : -1;
|
|
1567
|
+
if (start >= 0 && end >= 0) return text3.slice(0, start) + block + text3.slice(end + close.length);
|
|
1568
|
+
if (!text3) return block + eol;
|
|
1569
|
+
return text3 + (text3.endsWith("\n") ? "" : eol) + block + eol;
|
|
1570
|
+
}
|
|
1571
|
+
async function readJsonQuietly(path) {
|
|
1572
|
+
try {
|
|
1573
|
+
return JSON.parse(await readFile3(path, "utf8"));
|
|
1574
|
+
} catch {
|
|
1575
|
+
return void 0;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
async function chooseProject(own, opts, canAsk) {
|
|
1579
|
+
if (opts.project) return opts.project;
|
|
1580
|
+
if (own.memory.length <= 1) return own.memory[0]?.project;
|
|
1581
|
+
const names = own.memory.map((m) => m.project);
|
|
1582
|
+
const usage = `${own.workspace} has ${names.length} team memories (${names.join(", ")}): name this checkout's with --project.`;
|
|
1583
|
+
if (!canAsk) throw new SetupFailed(usage);
|
|
1584
|
+
const answer = (await opts.ask(`Which team memory is this checkout's? (${names.join(", ")}) `)).trim();
|
|
1585
|
+
if (!names.includes(answer)) throw new SetupFailed(usage);
|
|
1586
|
+
return answer;
|
|
1587
|
+
}
|
|
1588
|
+
async function runTeamSetup(opts) {
|
|
1589
|
+
const { client, out } = opts;
|
|
1590
|
+
try {
|
|
1591
|
+
const own = await client.getOwnTeamSetup();
|
|
1592
|
+
const ws = own.workspace;
|
|
1593
|
+
if (!isWorkspace(ws)) throw new SetupFailed(`SystemDox named the workspace ${JSON.stringify(ws)}, which is not a workspace name.`);
|
|
1594
|
+
const instructionsVersion = own.parts.instructions;
|
|
1595
|
+
if (!own.plugin && instructionsVersion === void 0 && !own.memory.length && !opts.project) return 0;
|
|
1596
|
+
const need = parseVersion(own.minClaudeCodeVersion);
|
|
1597
|
+
const have = parseVersion(await (opts.claudeVersion ?? claudeCodeVersion)());
|
|
1598
|
+
if (need && (!have || olderThan(have, need))) {
|
|
1599
|
+
out(
|
|
1600
|
+
have ? `Claude Code ${own.minClaudeCodeVersion} or later is needed for the team setup; this computer has ${have.join(".")}.` : `Claude Code ${own.minClaudeCodeVersion} or later is needed for the team setup; it was not found on this computer.`
|
|
1601
|
+
);
|
|
1602
|
+
out("Update it with: claude update");
|
|
1603
|
+
out("Then run setup again. Nothing was changed.");
|
|
1604
|
+
return 3;
|
|
1605
|
+
}
|
|
1606
|
+
const home = homeDir();
|
|
1607
|
+
const userSettings = join3(home, ".claude", "settings.json");
|
|
1608
|
+
const userClaudeMd = join3(home, ".claude", "CLAUDE.md");
|
|
1609
|
+
const teamClaudeMd = join3(workspaceDir(ws), "CLAUDE.md");
|
|
1610
|
+
const checkout = resolve2(opts.dir ?? opts.cwd ?? process.cwd());
|
|
1611
|
+
const localSettings = join3(checkout, ".claude", "settings.local.json");
|
|
1612
|
+
const state = await readState(ws);
|
|
1613
|
+
const accepted = new Set(state.accepted ?? []);
|
|
1614
|
+
const update = accepted.size > 0;
|
|
1615
|
+
const canAsk = !!opts.ask && !(opts.yes && !opts.terminal);
|
|
1616
|
+
const project = await chooseProject(own, opts, canAsk);
|
|
1617
|
+
const marketplaceName = `systemdox-${ws}`;
|
|
1618
|
+
const marketplace = { source: { source: "url", url: own.marketplaceUrl, headersHelper: HEADERS_HELPER } };
|
|
1619
|
+
const items = [];
|
|
1620
|
+
items.push(
|
|
1621
|
+
item(`the marketplace ${marketplaceName} in ${userSettings} (extraKnownMarketplaces)`, [
|
|
1622
|
+
`"${marketplaceName}": ${JSON.stringify(marketplace)}`
|
|
1623
|
+
])
|
|
1624
|
+
);
|
|
1625
|
+
const plugin = own.plugin;
|
|
1626
|
+
let previousPlugin;
|
|
1627
|
+
if (plugin) {
|
|
1628
|
+
const hooks = hookLines(plugin);
|
|
1629
|
+
const hooksName = `the hook commands of ${ws}-setup`;
|
|
1630
|
+
const hooksItem = item(hooksName, hooks);
|
|
1631
|
+
if (update && hooks.length && !accepted.has(hooksItem.sha256)) {
|
|
1632
|
+
const versions = (await client.getPlugin(ws)).versions.filter((v) => v.version !== plugin.version);
|
|
1633
|
+
previousPlugin = versions.sort((a, b) => b.version - a.version).find((v) => accepted.has(item(hooksName, hookLines(v)).sha256));
|
|
1634
|
+
}
|
|
1635
|
+
items.push(item(`the plugin ${ws}-setup, version ${plugin.version}`, [`${ws}-setup, version ${plugin.version}`]));
|
|
1636
|
+
if (hooks.length) {
|
|
1637
|
+
items.push(
|
|
1638
|
+
item(
|
|
1639
|
+
hooksName,
|
|
1640
|
+
hooks,
|
|
1641
|
+
previousPlugin ? lineDiff(hookLines(previousPlugin).join("\n"), hooks.join("\n")) : void 0
|
|
1642
|
+
)
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
const servers = mcpLines(plugin);
|
|
1646
|
+
if (servers.length) items.push(item(`the MCP servers of ${ws}-setup`, servers));
|
|
1647
|
+
}
|
|
1648
|
+
let instructions;
|
|
1649
|
+
if (instructionsVersion !== void 0) {
|
|
1650
|
+
const v = await client.getPartVersion(ws, "instructions", instructionsVersion);
|
|
1651
|
+
const file = v.files.find((f) => f.path === "CLAUDE.md") ?? v.files[0];
|
|
1652
|
+
instructions = { version: v.version, content: file?.content ?? "" };
|
|
1653
|
+
const name = `the team instructions, written to ${teamClaudeMd}`;
|
|
1654
|
+
const shown = item(name, instructions.content.split(/\r?\n/));
|
|
1655
|
+
let diff;
|
|
1656
|
+
const before = state.instructions;
|
|
1657
|
+
if (update && !accepted.has(shown.sha256) && before && before.version !== instructions.version) {
|
|
1658
|
+
const old = await client.getPartVersion(ws, "instructions", before.version);
|
|
1659
|
+
const oldFile = old.files.find((f) => f.path === "CLAUDE.md") ?? old.files[0];
|
|
1660
|
+
diff = lineDiff(oldFile?.content ?? "", instructions.content);
|
|
1661
|
+
}
|
|
1662
|
+
items.push({
|
|
1663
|
+
...shown,
|
|
1664
|
+
lines: diff ?? [`version ${instructions.version}, ${instructions.content.split(/\r?\n/).length} lines`]
|
|
1665
|
+
});
|
|
1666
|
+
items.push(item(`the import block in ${userClaudeMd}`, importBlock(ws).split("\n")));
|
|
1667
|
+
}
|
|
1668
|
+
let memoryFolder;
|
|
1669
|
+
if (project) {
|
|
1670
|
+
memoryFolder = join3(workspaceDir(ws), "memory", project);
|
|
1671
|
+
for (const m of own.memory) {
|
|
1672
|
+
const folder = join3(workspaceDir(ws), "memory", m.project);
|
|
1673
|
+
const count = `${m.fileCount} file${m.fileCount === 1 ? "" : "s"}`;
|
|
1674
|
+
items.push({ ...item(`the team memory ${m.project}`, [folder]), lines: [`${m.project}: ${count}, synced into ${folder}`] });
|
|
1675
|
+
}
|
|
1676
|
+
if (!own.memory.some((m) => m.project === project)) {
|
|
1677
|
+
items.push({ ...item(`the team memory ${project}`, [memoryFolder]), lines: [`${project}: new, synced into ${memoryFolder}`] });
|
|
1678
|
+
}
|
|
1679
|
+
items.push(item(`autoMemoryDirectory in ${localSettings}`, [memoryFolder]));
|
|
1680
|
+
}
|
|
1681
|
+
const pending = items.filter((i) => !accepted.has(i.sha256));
|
|
1682
|
+
if (pending.length) {
|
|
1683
|
+
if (!canAsk) {
|
|
1684
|
+
out(`Not set up: ${pending.length === 1 ? "this needs" : "these need"} a person at a terminal (run setup without --yes):`);
|
|
1685
|
+
for (const i of pending) {
|
|
1686
|
+
out(`- ${i.name}:`);
|
|
1687
|
+
for (const l of i.lines) out(` ${l}`);
|
|
1688
|
+
}
|
|
1689
|
+
return 4;
|
|
1690
|
+
}
|
|
1691
|
+
out(update ? `What changed in the team setup of ${ws}:` : `The team setup of ${ws} will add:`);
|
|
1692
|
+
for (const i of pending) {
|
|
1693
|
+
out(`- ${i.name}:`);
|
|
1694
|
+
for (const l of i.lines) out(` ${l}`);
|
|
1695
|
+
}
|
|
1696
|
+
const answer = (await opts.ask("Set this up? [y/N] ")).trim();
|
|
1697
|
+
if (!/^y(es)?$/i.test(answer)) {
|
|
1698
|
+
out("Nothing was changed.");
|
|
1699
|
+
return 4;
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
if (opts.keySource !== "credentials" && (await readCredentials())[ws]?.apiKey !== opts.apiKey) {
|
|
1703
|
+
await writeCredentials(ws, opts.apiKey);
|
|
1704
|
+
}
|
|
1705
|
+
if (instructions) {
|
|
1706
|
+
const next = sha2562(instructions.content);
|
|
1707
|
+
let current;
|
|
1708
|
+
try {
|
|
1709
|
+
current = await readFile3(teamClaudeMd, "utf8");
|
|
1710
|
+
} catch (e) {
|
|
1711
|
+
if (e.code !== "ENOENT") throw e;
|
|
1712
|
+
}
|
|
1713
|
+
const currentSha = current === void 0 ? void 0 : sha2562(current);
|
|
1714
|
+
if (currentSha !== void 0 && currentSha !== next && currentSha !== state.instructions?.sha256 && !opts.overwrite) {
|
|
1715
|
+
out(`${teamClaudeMd} was edited since setup installed it, so it was not replaced by version ${instructions.version}.`);
|
|
1716
|
+
out("Keep your text elsewhere, then run setup --overwrite to replace it.");
|
|
1717
|
+
return 2;
|
|
1718
|
+
}
|
|
1719
|
+
if (currentSha !== next) {
|
|
1720
|
+
await mkdir3(dirname3(teamClaudeMd), { recursive: true });
|
|
1721
|
+
await writeFile3(teamClaudeMd, instructions.content, "utf8");
|
|
1722
|
+
}
|
|
1723
|
+
await updateState(ws, (s) => {
|
|
1724
|
+
s.instructions = { version: instructions.version, sha256: next };
|
|
1725
|
+
});
|
|
1726
|
+
let text3 = "";
|
|
1727
|
+
try {
|
|
1728
|
+
text3 = await readFile3(userClaudeMd, "utf8");
|
|
1729
|
+
} catch (e) {
|
|
1730
|
+
if (e.code !== "ENOENT") throw e;
|
|
1731
|
+
}
|
|
1732
|
+
const withBlock = withImportBlock(text3, ws);
|
|
1733
|
+
if (withBlock !== text3) {
|
|
1734
|
+
await mkdir3(dirname3(userClaudeMd), { recursive: true });
|
|
1735
|
+
await writeFile3(userClaudeMd, withBlock, "utf8");
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
await setInJsonFile(userSettings, ["extraKnownMarketplaces", marketplaceName], marketplace);
|
|
1739
|
+
await removeOldPlugins(ws);
|
|
1740
|
+
if (project && memoryFolder) {
|
|
1741
|
+
await setInJsonFile(localSettings, ["autoMemoryDirectory"], memoryFolder);
|
|
1742
|
+
}
|
|
1743
|
+
await updateState(ws, (s) => {
|
|
1744
|
+
s.accepted = [...new Set(items.map((i) => i.sha256))];
|
|
1745
|
+
});
|
|
1746
|
+
if (project) {
|
|
1747
|
+
const code = await syncMemory({
|
|
1748
|
+
project,
|
|
1749
|
+
dryRun: false,
|
|
1750
|
+
client,
|
|
1751
|
+
out: (...lines) => lines.forEach((l) => out(l)),
|
|
1752
|
+
ask: opts.terminal ? opts.ask : void 0,
|
|
1753
|
+
now: opts.now
|
|
1754
|
+
});
|
|
1755
|
+
if (code !== 0) return 1;
|
|
1756
|
+
}
|
|
1757
|
+
if (plugin) {
|
|
1758
|
+
const pluginsDir = join3(home, ".claude", "plugins");
|
|
1759
|
+
const id = `${ws}-setup@${marketplaceName}`;
|
|
1760
|
+
const installed = await readJsonQuietly(join3(pluginsDir, "installed_plugins.json"));
|
|
1761
|
+
if (!installed?.plugins?.[id]) {
|
|
1762
|
+
const known = await readJsonQuietly(join3(pluginsDir, "known_marketplaces.json"));
|
|
1763
|
+
const install = `Run \`claude plugin install ${id}\` in your own terminal and accept the command it shows.`;
|
|
1764
|
+
const steps = known && typeof known === "object" && known[marketplaceName] ? ["One step is left:", `1. ${install}`] : ["Two steps are left:", "1. Open Claude Code once in any folder, then close it.", `2. ${install}`];
|
|
1765
|
+
out("");
|
|
1766
|
+
steps.forEach((l) => out(l));
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return 0;
|
|
1770
|
+
} catch (e) {
|
|
1771
|
+
out(e.message);
|
|
1772
|
+
return 1;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
async function removeOldPlugins(workspace) {
|
|
1776
|
+
const root = pluginRoot(workspace);
|
|
1777
|
+
if (!existsSync2(root)) return;
|
|
1778
|
+
const versions = (await readdir2(root, { withFileTypes: true })).filter((e) => e.isDirectory() && /^\d+$/.test(e.name)).map((e) => Number(e.name)).sort((a, b) => b - a);
|
|
1779
|
+
const old = versions.slice(2);
|
|
1780
|
+
if (!old.length) return;
|
|
1781
|
+
for (const v of old) await removeFolder(join3(root, String(v)));
|
|
1782
|
+
await updateState(workspace, (s) => {
|
|
1783
|
+
for (const v of old) delete s.plugin?.[String(v)];
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
async function runTeamSetupCommand(args2, key, io) {
|
|
1787
|
+
const client = new SystemDoxApiClient({ apiKey: key.apiKey, apiUrl: key.apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
|
|
1788
|
+
return runTeamSetup({ ...args2, client, apiKey: key.apiKey, keySource: "prompt", ...io });
|
|
1789
|
+
}
|
|
1790
|
+
var SETUP_USAGE, HEADERS_HELPER, SetupFailed, sha2562, run2, item, hookLines, mcpLines;
|
|
1791
|
+
var init_setup_team = __esm({
|
|
1792
|
+
"src/setup-team.ts"() {
|
|
1793
|
+
"use strict";
|
|
1794
|
+
init_api_client();
|
|
1795
|
+
init_memory_sync();
|
|
1796
|
+
init_team_setup_files();
|
|
1797
|
+
init_sync_memory();
|
|
1798
|
+
SETUP_USAGE = "Usage: systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]";
|
|
1799
|
+
HEADERS_HELPER = "npx -y @systemdox/mcp-server marketplace-auth";
|
|
1800
|
+
SetupFailed = class extends Error {
|
|
1801
|
+
};
|
|
1802
|
+
sha2562 = (text3) => createHash2("sha256").update(text3, "utf8").digest("hex");
|
|
1803
|
+
run2 = promisify2(execFile2);
|
|
1804
|
+
item = (name, lines, diff) => ({
|
|
1805
|
+
name,
|
|
1806
|
+
lines: diff ?? lines,
|
|
1807
|
+
sha256: sha2562(`${name}
|
|
1808
|
+
${lines.join("\n")}`)
|
|
1809
|
+
});
|
|
1810
|
+
hookLines = (p) => p.hookCommands ?? [];
|
|
1811
|
+
mcpLines = (p) => (p.mcpServers ?? []).map((s) => `${s.name}: ${s.url ?? [s.command, ...s.args ?? []].filter(Boolean).join(" ")}`);
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
// src/lib/skills.ts
|
|
1816
|
+
import { createHash as createHash3 } from "crypto";
|
|
1817
|
+
import { lstat, mkdir as mkdir4, readdir as readdir3, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
1818
|
+
import { existsSync as existsSync3 } from "fs";
|
|
440
1819
|
import { homedir as homedir2 } from "os";
|
|
441
|
-
import { dirname as
|
|
442
|
-
function
|
|
443
|
-
return
|
|
1820
|
+
import { dirname as dirname4, join as join4, relative, resolve as resolve3, sep } from "path";
|
|
1821
|
+
function sha2563(content) {
|
|
1822
|
+
return createHash3("sha256").update(content, "utf8").digest("hex");
|
|
444
1823
|
}
|
|
445
1824
|
function isValidName(name) {
|
|
446
1825
|
return name.length <= 64 && NAME_RE.test(name);
|
|
@@ -455,12 +1834,12 @@ function frontmatterName(skillMd) {
|
|
|
455
1834
|
return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
|
|
456
1835
|
}
|
|
457
1836
|
function defaultSkillDir(name) {
|
|
458
|
-
return
|
|
1837
|
+
return join4(homedir2(), ".claude", "skills", name);
|
|
459
1838
|
}
|
|
460
1839
|
async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
|
|
461
|
-
for (const entry of await
|
|
1840
|
+
for (const entry of await readdir3(dir, { withFileTypes: true })) {
|
|
462
1841
|
if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
|
|
463
|
-
const full =
|
|
1842
|
+
const full = join4(dir, entry.name);
|
|
464
1843
|
const rel = relative(root, full).split(sep).join("/");
|
|
465
1844
|
if (entry.isSymbolicLink()) out.links.push(rel);
|
|
466
1845
|
else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
|
|
@@ -481,7 +1860,7 @@ async function readTextFiles(root, paths, what) {
|
|
|
481
1860
|
if (!isValidPath(path)) {
|
|
482
1861
|
throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
|
|
483
1862
|
}
|
|
484
|
-
const raw = await
|
|
1863
|
+
const raw = await readFile4(join4(root, ...path.split("/")));
|
|
485
1864
|
if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
|
|
486
1865
|
if (raw.length > MAX_FILE_BYTES) {
|
|
487
1866
|
throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
|
|
@@ -501,8 +1880,8 @@ async function readTextFiles(root, paths, what) {
|
|
|
501
1880
|
return files;
|
|
502
1881
|
}
|
|
503
1882
|
async function readSkillDirectory(directory) {
|
|
504
|
-
const root =
|
|
505
|
-
if (!
|
|
1883
|
+
const root = resolve3(directory);
|
|
1884
|
+
if (!existsSync3(join4(root, ENTRY_FILE))) {
|
|
506
1885
|
throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
|
|
507
1886
|
}
|
|
508
1887
|
const walked = await walk(root, { skipHidden: true });
|
|
@@ -517,13 +1896,13 @@ async function readSkillDirectory(directory) {
|
|
|
517
1896
|
}
|
|
518
1897
|
function within(dest, path) {
|
|
519
1898
|
if (!isValidPath(path)) throw new Error(`refusing to write ${JSON.stringify(path)}: not a skill path`);
|
|
520
|
-
const full =
|
|
521
|
-
if (!full.startsWith(
|
|
1899
|
+
const full = resolve3(dest, ...path.split("/"));
|
|
1900
|
+
if (!full.startsWith(resolve3(dest) + sep)) throw new Error(`refusing to write outside ${dest}: ${path}`);
|
|
522
1901
|
return full;
|
|
523
1902
|
}
|
|
524
1903
|
async function readManifest(dest) {
|
|
525
1904
|
try {
|
|
526
|
-
return JSON.parse(await
|
|
1905
|
+
return JSON.parse(await readFile4(join4(dest, MANIFEST_FILE), "utf8"));
|
|
527
1906
|
} catch {
|
|
528
1907
|
return void 0;
|
|
529
1908
|
}
|
|
@@ -532,10 +1911,10 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
532
1911
|
if (!isValidName(version.name)) throw new Error(`refusing to install ${JSON.stringify(version.name)}: not a skill name`);
|
|
533
1912
|
const incoming = new Map(version.files.map((f) => [f.path, f]));
|
|
534
1913
|
for (const f of version.files) within(dest, f.path);
|
|
535
|
-
if (
|
|
1914
|
+
if (existsSync3(dest) && (await lstat(dest)).isSymbolicLink()) {
|
|
536
1915
|
throw new Error(`${dest} is a link: install into a real folder`);
|
|
537
1916
|
}
|
|
538
|
-
const walked =
|
|
1917
|
+
const walked = existsSync3(dest) ? await walk(dest, { skipHidden: false }) : { files: [], links: [], hidden: [] };
|
|
539
1918
|
if (walked.links.length) {
|
|
540
1919
|
throw new Error(`${dest} holds links (${walked.links.join(", ")}): remove them, nothing was written`);
|
|
541
1920
|
}
|
|
@@ -547,10 +1926,10 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
547
1926
|
const edited = [];
|
|
548
1927
|
const untracked = [];
|
|
549
1928
|
for (const path of present) {
|
|
550
|
-
const content = await
|
|
1929
|
+
const content = await readFile4(join4(dest, ...path.split("/")), "utf8");
|
|
551
1930
|
if (Object.hasOwn(known, path)) {
|
|
552
|
-
if (
|
|
553
|
-
} else if (!incoming.has(path) ||
|
|
1931
|
+
if (sha2563(content) !== known[path]) edited.push(path);
|
|
1932
|
+
} else if (!incoming.has(path) || sha2563(content) !== incoming.get(path).sha256) {
|
|
554
1933
|
if (incoming.has(path) || !manifest) edited.push(path);
|
|
555
1934
|
else untracked.push(path);
|
|
556
1935
|
}
|
|
@@ -560,8 +1939,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
560
1939
|
const written = [];
|
|
561
1940
|
for (const f of version.files) {
|
|
562
1941
|
const full = within(dest, f.path);
|
|
563
|
-
await
|
|
564
|
-
await
|
|
1942
|
+
await mkdir4(dirname4(full), { recursive: true });
|
|
1943
|
+
await writeFile4(full, f.content, "utf8");
|
|
565
1944
|
written.push(f.path);
|
|
566
1945
|
}
|
|
567
1946
|
const removed = [];
|
|
@@ -572,7 +1951,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
572
1951
|
keptEdited.push(path);
|
|
573
1952
|
continue;
|
|
574
1953
|
}
|
|
575
|
-
await
|
|
1954
|
+
await rm3(within(dest, path));
|
|
576
1955
|
removed.push(path);
|
|
577
1956
|
}
|
|
578
1957
|
const next = {
|
|
@@ -581,7 +1960,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
581
1960
|
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
582
1961
|
files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
|
|
583
1962
|
};
|
|
584
|
-
await
|
|
1963
|
+
await writeFile4(join4(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
585
1964
|
return {
|
|
586
1965
|
dest,
|
|
587
1966
|
version: version.version,
|
|
@@ -640,7 +2019,7 @@ __export(install_skill_exports, {
|
|
|
640
2019
|
parseInstallArgs: () => parseInstallArgs,
|
|
641
2020
|
runInstallSkill: () => runInstallSkill
|
|
642
2021
|
});
|
|
643
|
-
import { basename, resolve as
|
|
2022
|
+
import { basename, resolve as resolve4 } from "path";
|
|
644
2023
|
function parseInstallArgs(argv) {
|
|
645
2024
|
const args2 = { overwrite: false };
|
|
646
2025
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -661,7 +2040,7 @@ ${INSTALL_USAGE}`);
|
|
|
661
2040
|
}
|
|
662
2041
|
if (!args2.name) throw new Error(INSTALL_USAGE);
|
|
663
2042
|
if (!isValidName(args2.name)) throw new Error(`${args2.name} is not a skill name`);
|
|
664
|
-
if (args2.target && basename(
|
|
2043
|
+
if (args2.target && basename(resolve4(args2.target)) !== args2.name) {
|
|
665
2044
|
throw new Error(`--target must be a folder named ${args2.name} (e.g. .claude/skills/${args2.name})`);
|
|
666
2045
|
}
|
|
667
2046
|
return args2;
|
|
@@ -728,10 +2107,10 @@ __export(plugin_path_exports, {
|
|
|
728
2107
|
runPluginPath: () => runPluginPath,
|
|
729
2108
|
unpackZip: () => unpackZip
|
|
730
2109
|
});
|
|
731
|
-
import { createHash as
|
|
732
|
-
import { existsSync as
|
|
733
|
-
import { mkdir as
|
|
734
|
-
import { dirname as
|
|
2110
|
+
import { createHash as createHash4, randomBytes } from "crypto";
|
|
2111
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2112
|
+
import { mkdir as mkdir5, rename as rename2, writeFile as writeFile5 } from "fs/promises";
|
|
2113
|
+
import { dirname as dirname5, join as join5, resolve as resolve5, sep as sep2 } from "path";
|
|
735
2114
|
import { inflateRawSync } from "zlib";
|
|
736
2115
|
function parsePluginPathArgs(argv) {
|
|
737
2116
|
const args2 = {};
|
|
@@ -771,7 +2150,7 @@ function readZipEntries(zip) {
|
|
|
771
2150
|
const count = zip.readUInt16LE(eocd + 10);
|
|
772
2151
|
let at = zip.readUInt32LE(eocd + 16);
|
|
773
2152
|
if (count === 65535 || at === 4294967295) throw new PluginRefused("the zip is a ZIP64 archive, which a plugin never is. Nothing was unpacked.");
|
|
774
|
-
const
|
|
2153
|
+
const entries2 = [];
|
|
775
2154
|
let total = 0;
|
|
776
2155
|
for (let n = 0; n < count; n++) {
|
|
777
2156
|
if (at + 46 > zip.length || zip.readUInt32LE(at) !== CENTRAL) {
|
|
@@ -801,9 +2180,9 @@ function readZipEntries(zip) {
|
|
|
801
2180
|
if (!directory && method !== 0 && method !== 8) refuse(name, `uses compression method ${method}`);
|
|
802
2181
|
total += size;
|
|
803
2182
|
if (total > MAX_UNPACKED_BYTES) refuse(name, `takes the unpacked size past ${MAX_UNPACKED_BYTES} bytes`);
|
|
804
|
-
|
|
2183
|
+
entries2.push({ name, directory, mode: unixMode ? unixMode & 511 : void 0, method, compressedSize, size, offset });
|
|
805
2184
|
}
|
|
806
|
-
return
|
|
2185
|
+
return entries2;
|
|
807
2186
|
}
|
|
808
2187
|
function entryData(zip, entry) {
|
|
809
2188
|
const at = entry.offset;
|
|
@@ -820,24 +2199,24 @@ function entryData(zip, entry) {
|
|
|
820
2199
|
return data;
|
|
821
2200
|
}
|
|
822
2201
|
async function unpackZip(zip, dest) {
|
|
823
|
-
const
|
|
824
|
-
const root =
|
|
825
|
-
const targets =
|
|
826
|
-
const full =
|
|
2202
|
+
const entries2 = readZipEntries(zip);
|
|
2203
|
+
const root = resolve5(dest);
|
|
2204
|
+
const targets = entries2.map((e) => {
|
|
2205
|
+
const full = resolve5(root, ...e.name.split(/[\\/]/).filter(Boolean));
|
|
827
2206
|
if (full !== root && !full.startsWith(root + sep2)) refuse(e.name, "climbs out of the plugin folder");
|
|
828
2207
|
return full;
|
|
829
2208
|
});
|
|
830
|
-
const data =
|
|
831
|
-
await
|
|
832
|
-
for (const [i, entry] of
|
|
2209
|
+
const data = entries2.map((e) => e.directory ? void 0 : entryData(zip, e));
|
|
2210
|
+
await mkdir5(root, { recursive: true });
|
|
2211
|
+
for (const [i, entry] of entries2.entries()) {
|
|
833
2212
|
if (entry.directory) {
|
|
834
|
-
await
|
|
2213
|
+
await mkdir5(targets[i], { recursive: true });
|
|
835
2214
|
continue;
|
|
836
2215
|
}
|
|
837
|
-
await
|
|
838
|
-
await
|
|
2216
|
+
await mkdir5(dirname5(targets[i]), { recursive: true });
|
|
2217
|
+
await writeFile5(targets[i], data[i], { mode: entry.mode ?? 420 });
|
|
839
2218
|
}
|
|
840
|
-
return
|
|
2219
|
+
return entries2.filter((e) => !e.directory).length;
|
|
841
2220
|
}
|
|
842
2221
|
async function runPluginPath(argv, io) {
|
|
843
2222
|
let args2;
|
|
@@ -847,11 +2226,11 @@ async function runPluginPath(argv, io) {
|
|
|
847
2226
|
io.err(e.message);
|
|
848
2227
|
return 1;
|
|
849
2228
|
}
|
|
850
|
-
const { workspace, version, sha256:
|
|
2229
|
+
const { workspace, version, sha256: sha2564 } = args2;
|
|
851
2230
|
const folder = pluginDir(workspace, version);
|
|
852
2231
|
try {
|
|
853
2232
|
const state = await readState(workspace);
|
|
854
|
-
if (
|
|
2233
|
+
if (existsSync4(folder) && state.plugin?.[String(version)]?.sha256 === sha2564) {
|
|
855
2234
|
io.out(folder);
|
|
856
2235
|
return 0;
|
|
857
2236
|
}
|
|
@@ -883,29 +2262,29 @@ async function runPluginPath(argv, io) {
|
|
|
883
2262
|
io.err(e.message);
|
|
884
2263
|
return 1;
|
|
885
2264
|
}
|
|
886
|
-
const got =
|
|
887
|
-
if (got !==
|
|
2265
|
+
const got = createHash4("sha256").update(zip).digest("hex");
|
|
2266
|
+
if (got !== sha2564) {
|
|
888
2267
|
io.err(
|
|
889
|
-
`The download of ${workspace}'s team plugin version ${version} has SHA-256 ${got}, not the ${
|
|
2268
|
+
`The download of ${workspace}'s team plugin version ${version} has SHA-256 ${got}, not the ${sha2564} Claude Code named. Nothing was unpacked.`
|
|
890
2269
|
);
|
|
891
2270
|
return 2;
|
|
892
2271
|
}
|
|
893
|
-
const tmp =
|
|
2272
|
+
const tmp = join5(pluginRoot(workspace), `.${version}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
|
|
894
2273
|
try {
|
|
895
2274
|
await unpackZip(zip, tmp);
|
|
896
|
-
if (
|
|
2275
|
+
if (existsSync4(folder) && (await readState(workspace)).plugin?.[String(version)]?.sha256 === sha2564) {
|
|
897
2276
|
await removeFolder(tmp);
|
|
898
2277
|
} else {
|
|
899
|
-
if (
|
|
2278
|
+
if (existsSync4(folder)) await removeFolder(folder);
|
|
900
2279
|
try {
|
|
901
2280
|
await rename2(tmp, folder);
|
|
902
2281
|
} catch (e) {
|
|
903
|
-
if (!
|
|
2282
|
+
if (!existsSync4(folder)) throw e;
|
|
904
2283
|
await removeFolder(tmp);
|
|
905
2284
|
}
|
|
906
2285
|
}
|
|
907
2286
|
await updateState(workspace, (state) => {
|
|
908
|
-
state.plugin = { ...state.plugin, [String(version)]: { sha256:
|
|
2287
|
+
state.plugin = { ...state.plugin, [String(version)]: { sha256: sha2564, installedAt: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
909
2288
|
});
|
|
910
2289
|
} catch (e) {
|
|
911
2290
|
await removeFolder(tmp);
|
|
@@ -1299,7 +2678,7 @@ ${doc.content}`;
|
|
|
1299
2678
|
);
|
|
1300
2679
|
let items = response.items;
|
|
1301
2680
|
if (path) {
|
|
1302
|
-
items = items.filter((
|
|
2681
|
+
items = items.filter((item2) => matchGlob(item2.path, path));
|
|
1303
2682
|
}
|
|
1304
2683
|
if (items.length === 0) {
|
|
1305
2684
|
return {
|
|
@@ -1368,8 +2747,8 @@ function registerGetTechStack(server2, apiClient2) {
|
|
|
1368
2747
|
list.push(entry);
|
|
1369
2748
|
grouped.set(entry.category, list);
|
|
1370
2749
|
}
|
|
1371
|
-
const sections = Array.from(grouped.entries()).map(([category,
|
|
1372
|
-
const items =
|
|
2750
|
+
const sections = Array.from(grouped.entries()).map(([category, entries2]) => {
|
|
2751
|
+
const items = entries2.map(
|
|
1373
2752
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
1374
2753
|
).join("\n");
|
|
1375
2754
|
return `### ${category}
|
|
@@ -1479,8 +2858,8 @@ var init_get_planning_context = __esm({
|
|
|
1479
2858
|
|
|
1480
2859
|
// src/tools/get-plan-board.ts
|
|
1481
2860
|
import { z as z5 } from "zod";
|
|
1482
|
-
function bucketOf(
|
|
1483
|
-
const status = (
|
|
2861
|
+
function bucketOf(item2) {
|
|
2862
|
+
const status = (item2.fields?.Status ?? "").trim().toLowerCase();
|
|
1484
2863
|
if (status) {
|
|
1485
2864
|
if (status.includes("done") || status.includes("ship") || status.includes("complete")) {
|
|
1486
2865
|
return "done";
|
|
@@ -1490,7 +2869,7 @@ function bucketOf(item) {
|
|
|
1490
2869
|
}
|
|
1491
2870
|
return "todo";
|
|
1492
2871
|
}
|
|
1493
|
-
return
|
|
2872
|
+
return item2.state === "CLOSED" ? "done" : "todo";
|
|
1494
2873
|
}
|
|
1495
2874
|
function formatBoards(boards) {
|
|
1496
2875
|
const lines = boards.map((b) => {
|
|
@@ -1501,16 +2880,16 @@ function formatBoards(boards) {
|
|
|
1501
2880
|
});
|
|
1502
2881
|
return lines.join("\n");
|
|
1503
2882
|
}
|
|
1504
|
-
function formatItem(
|
|
2883
|
+
function formatItem(item2) {
|
|
1505
2884
|
const meta = [];
|
|
1506
|
-
if (
|
|
1507
|
-
if (
|
|
1508
|
-
meta.push(`assignees: ${
|
|
1509
|
-
const epic =
|
|
2885
|
+
if (item2.labels.length > 0) meta.push(`labels: ${item2.labels.join(", ")}`);
|
|
2886
|
+
if (item2.assignees.length > 0)
|
|
2887
|
+
meta.push(`assignees: ${item2.assignees.join(", ")}`);
|
|
2888
|
+
const epic = item2.fields?.Epic;
|
|
1510
2889
|
if (epic) meta.push(`epic: ${epic}`);
|
|
1511
|
-
return `- #${
|
|
1512
|
-
${meta.join(" \xB7 ")}` : "") + (
|
|
1513
|
-
${
|
|
2890
|
+
return `- #${item2.number} ${item2.title}${item2.repo ? ` \u2014 ${item2.repo}` : ""}` + (meta.length > 0 ? `
|
|
2891
|
+
${meta.join(" \xB7 ")}` : "") + (item2.url ? `
|
|
2892
|
+
${item2.url}` : "");
|
|
1514
2893
|
}
|
|
1515
2894
|
function registerGetPlanBoard(server2, apiClient2) {
|
|
1516
2895
|
server2.registerTool(
|
|
@@ -1592,7 +2971,7 @@ Call get_plan_board with project_number to list a board's issues.`
|
|
|
1592
2971
|
todo: [],
|
|
1593
2972
|
done: []
|
|
1594
2973
|
};
|
|
1595
|
-
for (const
|
|
2974
|
+
for (const item2 of allItems) grouped[bucketOf(item2)].push(item2);
|
|
1596
2975
|
const wanted = status ? [STATUS_ARG_TO_BUCKET[status]] : BUCKET_ORDER;
|
|
1597
2976
|
const shown = wanted.reduce((n, b) => n + grouped[b].length, 0);
|
|
1598
2977
|
if (shown === 0) {
|
|
@@ -1685,11 +3064,11 @@ function truncateToTokens(text3, maxTokens) {
|
|
|
1685
3064
|
else if (line >= floor) cut = line;
|
|
1686
3065
|
return { text: head.slice(0, cut).trimEnd(), truncated: true };
|
|
1687
3066
|
}
|
|
1688
|
-
function scorePathMatch(
|
|
3067
|
+
function scorePathMatch(item2, languages, categories) {
|
|
1689
3068
|
if (languages.length === 0 && categories.length === 0) return 0.5;
|
|
1690
3069
|
let score = 0;
|
|
1691
|
-
const itemLang =
|
|
1692
|
-
const itemCat =
|
|
3070
|
+
const itemLang = item2.language?.toLowerCase() ?? "";
|
|
3071
|
+
const itemCat = item2.category?.toLowerCase() ?? "";
|
|
1693
3072
|
if (itemLang && languages.some((l) => itemLang.includes(l))) {
|
|
1694
3073
|
score += 0.6;
|
|
1695
3074
|
}
|
|
@@ -1698,28 +3077,28 @@ function scorePathMatch(item, languages, categories) {
|
|
|
1698
3077
|
}
|
|
1699
3078
|
return Math.min(score, 1);
|
|
1700
3079
|
}
|
|
1701
|
-
function scoreKeywordMatch(
|
|
3080
|
+
function scoreKeywordMatch(item2, keywords) {
|
|
1702
3081
|
if (keywords.length === 0) return 0;
|
|
1703
|
-
const searchable = `${
|
|
3082
|
+
const searchable = `${item2.title} ${item2.content}`.toLowerCase();
|
|
1704
3083
|
let matches = 0;
|
|
1705
3084
|
for (const kw of keywords) {
|
|
1706
3085
|
if (searchable.includes(kw)) matches++;
|
|
1707
3086
|
}
|
|
1708
3087
|
return Math.min(matches / keywords.length, 1);
|
|
1709
3088
|
}
|
|
1710
|
-
function scoreRecency(
|
|
1711
|
-
if (!
|
|
3089
|
+
function scoreRecency(item2) {
|
|
3090
|
+
if (!item2.updatedAt) return 0.5;
|
|
1712
3091
|
const now = Date.now();
|
|
1713
|
-
const updated = new Date(
|
|
3092
|
+
const updated = new Date(item2.updatedAt).getTime();
|
|
1714
3093
|
const daysSince = (now - updated) / (1e3 * 60 * 60 * 24);
|
|
1715
3094
|
if (daysSince <= 7) return 1;
|
|
1716
3095
|
if (daysSince <= 30) return 0.75;
|
|
1717
3096
|
if (daysSince <= 90) return 0.5;
|
|
1718
3097
|
return 0.25;
|
|
1719
3098
|
}
|
|
1720
|
-
function scoreSeverity(
|
|
1721
|
-
if (!
|
|
1722
|
-
return SEVERITY_SCORES[
|
|
3099
|
+
function scoreSeverity(item2) {
|
|
3100
|
+
if (!item2.severity) return 0.5;
|
|
3101
|
+
return SEVERITY_SCORES[item2.severity] ?? 0.5;
|
|
1723
3102
|
}
|
|
1724
3103
|
function extractKeywords(text3) {
|
|
1725
3104
|
const stopWords = /* @__PURE__ */ new Set([
|
|
@@ -1812,29 +3191,29 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
1812
3191
|
const languages = detectLanguages(filePaths);
|
|
1813
3192
|
const categories = detectCategories(filePaths);
|
|
1814
3193
|
const keywords = extractKeywords(taskDescription);
|
|
1815
|
-
return items.map((
|
|
1816
|
-
const pathScore = scorePathMatch(
|
|
1817
|
-
const keywordScore = scoreKeywordMatch(
|
|
1818
|
-
const recencyScore = scoreRecency(
|
|
1819
|
-
const severityScore = scoreSeverity(
|
|
3194
|
+
return items.map((item2) => {
|
|
3195
|
+
const pathScore = scorePathMatch(item2, languages, categories);
|
|
3196
|
+
const keywordScore = scoreKeywordMatch(item2, keywords);
|
|
3197
|
+
const recencyScore = scoreRecency(item2);
|
|
3198
|
+
const severityScore = scoreSeverity(item2);
|
|
1820
3199
|
const score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
1821
3200
|
return {
|
|
1822
|
-
type:
|
|
1823
|
-
id:
|
|
1824
|
-
title:
|
|
1825
|
-
content:
|
|
3201
|
+
type: item2.type,
|
|
3202
|
+
id: item2.id,
|
|
3203
|
+
title: item2.title,
|
|
3204
|
+
content: item2.content,
|
|
1826
3205
|
score,
|
|
1827
|
-
tokenEstimate: estimateTokens(
|
|
3206
|
+
tokenEstimate: estimateTokens(item2.content)
|
|
1828
3207
|
};
|
|
1829
3208
|
}).sort((a, b) => b.score - a.score);
|
|
1830
3209
|
}
|
|
1831
3210
|
function selectWithinBudget(items, maxTokens) {
|
|
1832
3211
|
const selected = [];
|
|
1833
3212
|
let tokensUsed = 0;
|
|
1834
|
-
for (const
|
|
1835
|
-
if (tokensUsed +
|
|
1836
|
-
selected.push(
|
|
1837
|
-
tokensUsed +=
|
|
3213
|
+
for (const item2 of items) {
|
|
3214
|
+
if (tokensUsed + item2.tokenEstimate > maxTokens) continue;
|
|
3215
|
+
selected.push(item2);
|
|
3216
|
+
tokensUsed += item2.tokenEstimate;
|
|
1838
3217
|
}
|
|
1839
3218
|
return { selected, tokensUsed, totalItems: items.length };
|
|
1840
3219
|
}
|
|
@@ -1882,8 +3261,8 @@ function formatDocHeadline(d) {
|
|
|
1882
3261
|
return `### ${d.title}
|
|
1883
3262
|
\`doc_id: ${d.id}\` \xB7 \`${d.repo_full_name}/${d.path}\` \xB7 ${d.doc_type}`;
|
|
1884
3263
|
}
|
|
1885
|
-
function formatStack(
|
|
1886
|
-
return
|
|
3264
|
+
function formatStack(entries2) {
|
|
3265
|
+
return entries2.map(
|
|
1887
3266
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
1888
3267
|
).join("\n");
|
|
1889
3268
|
}
|
|
@@ -1964,13 +3343,13 @@ function selectSection(outline, taskDescription) {
|
|
|
1964
3343
|
}
|
|
1965
3344
|
return best;
|
|
1966
3345
|
}
|
|
1967
|
-
async function hydrate(api,
|
|
3346
|
+
async function hydrate(api, item2, meta) {
|
|
1968
3347
|
try {
|
|
1969
3348
|
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id, meta.section?.anchor);
|
|
1970
|
-
return { item, meta, body };
|
|
3349
|
+
return { item: item2, meta, body };
|
|
1971
3350
|
} catch (err) {
|
|
1972
3351
|
return {
|
|
1973
|
-
item,
|
|
3352
|
+
item: item2,
|
|
1974
3353
|
meta,
|
|
1975
3354
|
error: err instanceof Error ? err.message : String(err)
|
|
1976
3355
|
};
|
|
@@ -2077,13 +3456,13 @@ ${formatStack(stack)}`;
|
|
|
2077
3456
|
0,
|
|
2078
3457
|
Math.max(0, Math.min(MAX_HYDRATED, affordable))
|
|
2079
3458
|
);
|
|
2080
|
-
for (const
|
|
2081
|
-
remaining -=
|
|
3459
|
+
for (const item2 of shortlist) {
|
|
3460
|
+
remaining -= item2.tokenEstimate;
|
|
2082
3461
|
}
|
|
2083
3462
|
remaining = Math.max(0, remaining);
|
|
2084
3463
|
const hydrated = await Promise.all(
|
|
2085
3464
|
shortlist.map(
|
|
2086
|
-
(
|
|
3465
|
+
(item2) => hydrate(api, item2, refs.get(key(item2.type, item2.id)))
|
|
2087
3466
|
)
|
|
2088
3467
|
);
|
|
2089
3468
|
const tailCandidates = rankedRefs.slice(shortlist.length, shortlist.length + MAX_TAIL).map((i) => refs.get(key(i.type, i.id)));
|
|
@@ -2721,7 +4100,7 @@ var init_create_prompt_template = __esm({
|
|
|
2721
4100
|
// src/tools/skills.ts
|
|
2722
4101
|
import { z as z13 } from "zod";
|
|
2723
4102
|
import { homedir as homedir3 } from "os";
|
|
2724
|
-
import { basename as basename2, resolve as
|
|
4103
|
+
import { basename as basename2, resolve as resolve6 } from "path";
|
|
2725
4104
|
function expandHome(path) {
|
|
2726
4105
|
return path.replace(/^~(?=$|[\\/])/, homedir3());
|
|
2727
4106
|
}
|
|
@@ -2825,7 +4204,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
|
|
|
2825
4204
|
);
|
|
2826
4205
|
if (localFiles) {
|
|
2827
4206
|
const dest2 = args2.target ? expandHome(args2.target) : defaultSkillDir(version.name);
|
|
2828
|
-
if (basename2(
|
|
4207
|
+
if (basename2(resolve6(dest2)) !== version.name) {
|
|
2829
4208
|
throw new Error(`target must be a folder named ${version.name} (e.g. .claude/skills/${version.name}); got ${dest2}`);
|
|
2830
4209
|
}
|
|
2831
4210
|
const result = await installSkill(version, dest2, { overwrite: args2.overwrite });
|
|
@@ -2869,28 +4248,6 @@ var init_skills2 = __esm({
|
|
|
2869
4248
|
}
|
|
2870
4249
|
});
|
|
2871
4250
|
|
|
2872
|
-
// src/lib/person-values.ts
|
|
2873
|
-
var W, D, WORD_OR, PERSON_ID, PERSON_TOKEN, BEFORE, AFTER, E164, NATIONAL, ATEXT, EMAIL_RE, HOME_PREFIX, HOME, KEY_PREFIX;
|
|
2874
|
-
var init_person_values = __esm({
|
|
2875
|
-
"src/lib/person-values.ts"() {
|
|
2876
|
-
"use strict";
|
|
2877
|
-
W = "[\\p{L}\\p{N}_]";
|
|
2878
|
-
D = "\\p{Nd}";
|
|
2879
|
-
WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
|
|
2880
|
-
PERSON_ID = "(?:con|ctc|usr)_[0-9A-Za-z]{10,40}";
|
|
2881
|
-
PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
|
|
2882
|
-
BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
|
|
2883
|
-
AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
|
|
2884
|
-
E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
|
|
2885
|
-
NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
|
|
2886
|
-
ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
|
|
2887
|
-
EMAIL_RE = "(?<![A-Za-z0-9!#$%&*+/=?^_{|}~.@\\-])[A-Za-z0-9_]" + ATEXT + "*(?:\\." + ATEXT + "+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}(?![A-Za-z0-9\\-]|\\.[A-Za-z0-9])";
|
|
2888
|
-
HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
|
|
2889
|
-
HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
|
|
2890
|
-
KEY_PREFIX = `(?<!${W})(?:pweb_[a-z]+_key_|sk-ant-|sk_live_|rk_live_|github_pat_|gh[pousr]_|sntrys_|xox[baprs]-)[A-Za-z0-9_\\-]{4,}`;
|
|
2891
|
-
}
|
|
2892
|
-
});
|
|
2893
|
-
|
|
2894
4251
|
// src/lib/team-setup-types.ts
|
|
2895
4252
|
var PARTS, PERSON_ID_RE;
|
|
2896
4253
|
var init_team_setup_types = __esm({
|
|
@@ -2904,10 +4261,10 @@ var init_team_setup_types = __esm({
|
|
|
2904
4261
|
|
|
2905
4262
|
// src/tools/team-setup.ts
|
|
2906
4263
|
import { z as z14 } from "zod";
|
|
2907
|
-
import { existsSync as
|
|
4264
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2908
4265
|
import { lstat as lstat2 } from "fs/promises";
|
|
2909
4266
|
import { homedir as homedir4 } from "os";
|
|
2910
|
-
import { join as
|
|
4267
|
+
import { join as join6, resolve as resolve7 } from "path";
|
|
2911
4268
|
function expandHome2(path) {
|
|
2912
4269
|
return path.replace(/^~(?=$|[\\/])/, homedir4());
|
|
2913
4270
|
}
|
|
@@ -2923,7 +4280,7 @@ async function isLink(path) {
|
|
|
2923
4280
|
}
|
|
2924
4281
|
}
|
|
2925
4282
|
async function readPartDirectory(part, directory) {
|
|
2926
|
-
const root =
|
|
4283
|
+
const root = resolve7(directory);
|
|
2927
4284
|
const layout = LAYOUT[part];
|
|
2928
4285
|
const paths = [];
|
|
2929
4286
|
const leftOut = [];
|
|
@@ -2933,21 +4290,21 @@ async function readPartDirectory(part, directory) {
|
|
|
2933
4290
|
let present = true;
|
|
2934
4291
|
for (let i = 1; i <= segments.length; i++) {
|
|
2935
4292
|
const rel = segments.slice(0, i).join("/");
|
|
2936
|
-
if (await isLink(
|
|
4293
|
+
if (await isLink(join6(root, ...segments.slice(0, i)))) {
|
|
2937
4294
|
links.push(rel);
|
|
2938
4295
|
present = false;
|
|
2939
4296
|
break;
|
|
2940
4297
|
}
|
|
2941
4298
|
}
|
|
2942
|
-
if (present &&
|
|
4299
|
+
if (present && existsSync5(join6(root, ...segments))) paths.push(path);
|
|
2943
4300
|
}
|
|
2944
4301
|
for (const { dir, md } of layout.folders) {
|
|
2945
|
-
const full =
|
|
4302
|
+
const full = join6(root, dir);
|
|
2946
4303
|
if (await isLink(full)) {
|
|
2947
4304
|
links.push(dir);
|
|
2948
4305
|
continue;
|
|
2949
4306
|
}
|
|
2950
|
-
if (!
|
|
4307
|
+
if (!existsSync5(full)) continue;
|
|
2951
4308
|
const walked = await walk(full, { skipHidden: true });
|
|
2952
4309
|
links.push(...walked.links.map((p) => `${dir}/${p}`));
|
|
2953
4310
|
leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
|
|
@@ -3049,6 +4406,29 @@ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
|
|
|
3049
4406
|
return text2(describePerson(args2.id, person));
|
|
3050
4407
|
}
|
|
3051
4408
|
);
|
|
4409
|
+
if (!localFiles) return;
|
|
4410
|
+
server2.registerTool(
|
|
4411
|
+
"sync_memory",
|
|
4412
|
+
{
|
|
4413
|
+
title: "Sync Memory",
|
|
4414
|
+
description: `Sync one project's team memory on this computer with SystemDox: pull the files the team changed, push the ones changed here, keep both texts of a file changed on both sides. It never answers for a person: a file that still holds a phone number, an email address or a home-folder path is skipped and listed, with the command for the member to run in their own terminal (\`${SYNC_MEMORY_COMMAND}\`). Pass dryRun to see what it would do without writing or sending anything.`,
|
|
4415
|
+
inputSchema: {
|
|
4416
|
+
project: z14.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "a project name: lower case letters, digits and -").optional().describe("The project's team memory, e.g. puglieseweb-ltd; needed when the workspace has more than one"),
|
|
4417
|
+
dryRun: z14.boolean().optional().describe("Show what would be pulled, pushed, deleted, conflicted and skipped, and change nothing")
|
|
4418
|
+
},
|
|
4419
|
+
annotations: { readOnlyHint: false }
|
|
4420
|
+
},
|
|
4421
|
+
async (args2) => {
|
|
4422
|
+
const lines = [];
|
|
4423
|
+
const code = await syncMemory({
|
|
4424
|
+
project: args2.project,
|
|
4425
|
+
dryRun: !!args2.dryRun,
|
|
4426
|
+
client: apiClient2,
|
|
4427
|
+
out: (...l) => lines.push(...l)
|
|
4428
|
+
});
|
|
4429
|
+
return { ...text2(lines.join("\n")), ...code ? { isError: true } : {} };
|
|
4430
|
+
}
|
|
4431
|
+
);
|
|
3052
4432
|
}
|
|
3053
4433
|
var LAYOUT;
|
|
3054
4434
|
var init_team_setup = __esm({
|
|
@@ -3056,6 +4436,7 @@ var init_team_setup = __esm({
|
|
|
3056
4436
|
"use strict";
|
|
3057
4437
|
init_skills();
|
|
3058
4438
|
init_team_setup_types();
|
|
4439
|
+
init_sync_memory();
|
|
3059
4440
|
LAYOUT = {
|
|
3060
4441
|
hooks: { files: ["hooks/hooks.json"], folders: [{ dir: "scripts", md: false }], required: "hooks/hooks.json" },
|
|
3061
4442
|
mcp: { files: [".mcp.json"], folders: [], required: ".mcp.json" },
|
|
@@ -3111,7 +4492,7 @@ var init_server = __esm({
|
|
|
3111
4492
|
init_create_prompt_template();
|
|
3112
4493
|
init_skills2();
|
|
3113
4494
|
init_team_setup();
|
|
3114
|
-
SERVER_VERSION = true ? "0.1.
|
|
4495
|
+
SERVER_VERSION = true ? "0.1.48" : "0.0.0-dev";
|
|
3115
4496
|
}
|
|
3116
4497
|
});
|
|
3117
4498
|
|
|
@@ -3146,17 +4527,17 @@ var init_index = __esm({
|
|
|
3146
4527
|
// src/cli.ts
|
|
3147
4528
|
init_config();
|
|
3148
4529
|
init_team_setup_files();
|
|
3149
|
-
import { readFile as
|
|
3150
|
-
import { existsSync as
|
|
4530
|
+
import { readFile as readFile5, writeFile as writeFile6, chmod as chmod2 } from "fs/promises";
|
|
4531
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3151
4532
|
import { homedir as homedir5 } from "os";
|
|
3152
|
-
import { join as
|
|
4533
|
+
import { join as join7 } from "path";
|
|
3153
4534
|
import { createInterface } from "readline";
|
|
3154
4535
|
function prompt(question) {
|
|
3155
4536
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3156
|
-
return new Promise((
|
|
4537
|
+
return new Promise((resolve8) => {
|
|
3157
4538
|
rl.question(question, (answer) => {
|
|
3158
4539
|
rl.close();
|
|
3159
|
-
|
|
4540
|
+
resolve8(answer.trim());
|
|
3160
4541
|
});
|
|
3161
4542
|
});
|
|
3162
4543
|
}
|
|
@@ -3176,45 +4557,45 @@ async function validateApiKey(apiKey, apiUrl) {
|
|
|
3176
4557
|
}
|
|
3177
4558
|
function findConfigTargets() {
|
|
3178
4559
|
const targets = [];
|
|
3179
|
-
const projectMcp =
|
|
4560
|
+
const projectMcp = join7(process.cwd(), ".mcp.json");
|
|
3180
4561
|
targets.push({
|
|
3181
4562
|
name: "Claude Code (project)",
|
|
3182
4563
|
path: projectMcp,
|
|
3183
4564
|
detected: true
|
|
3184
4565
|
// Always available
|
|
3185
4566
|
});
|
|
3186
|
-
const claudeUser =
|
|
4567
|
+
const claudeUser = join7(homedir5(), ".claude.json");
|
|
3187
4568
|
targets.push({
|
|
3188
4569
|
name: "Claude Code (user \u2014 all projects)",
|
|
3189
4570
|
path: claudeUser,
|
|
3190
4571
|
detected: true
|
|
3191
4572
|
// Always available
|
|
3192
4573
|
});
|
|
3193
|
-
const cursorProject =
|
|
3194
|
-
const cursorDir =
|
|
4574
|
+
const cursorProject = join7(process.cwd(), ".cursor", "mcp.json");
|
|
4575
|
+
const cursorDir = join7(process.cwd(), ".cursor");
|
|
3195
4576
|
targets.push({
|
|
3196
4577
|
name: "Cursor (project)",
|
|
3197
4578
|
path: cursorProject,
|
|
3198
|
-
detected:
|
|
4579
|
+
detected: existsSync6(cursorDir)
|
|
3199
4580
|
});
|
|
3200
|
-
const vscodeProject =
|
|
3201
|
-
const vscodeDir =
|
|
4581
|
+
const vscodeProject = join7(process.cwd(), ".vscode", "mcp.json");
|
|
4582
|
+
const vscodeDir = join7(process.cwd(), ".vscode");
|
|
3202
4583
|
targets.push({
|
|
3203
4584
|
name: "VS Code (project)",
|
|
3204
4585
|
path: vscodeProject,
|
|
3205
|
-
detected:
|
|
4586
|
+
detected: existsSync6(vscodeDir)
|
|
3206
4587
|
});
|
|
3207
|
-
const cursorGlobal =
|
|
4588
|
+
const cursorGlobal = join7(homedir5(), ".cursor", "mcp.json");
|
|
3208
4589
|
targets.push({
|
|
3209
4590
|
name: "Cursor (global)",
|
|
3210
4591
|
path: cursorGlobal,
|
|
3211
|
-
detected:
|
|
4592
|
+
detected: existsSync6(join7(homedir5(), ".cursor"))
|
|
3212
4593
|
});
|
|
3213
4594
|
return targets;
|
|
3214
4595
|
}
|
|
3215
4596
|
async function readOrCreateConfig(path) {
|
|
3216
4597
|
try {
|
|
3217
|
-
const content = await
|
|
4598
|
+
const content = await readFile5(path, "utf-8");
|
|
3218
4599
|
return JSON.parse(content);
|
|
3219
4600
|
} catch {
|
|
3220
4601
|
return { mcpServers: {} };
|
|
@@ -3222,11 +4603,11 @@ async function readOrCreateConfig(path) {
|
|
|
3222
4603
|
}
|
|
3223
4604
|
async function writeConfig(path, config2) {
|
|
3224
4605
|
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
3225
|
-
if (!
|
|
3226
|
-
const { mkdir:
|
|
3227
|
-
await
|
|
4606
|
+
if (!existsSync6(dir)) {
|
|
4607
|
+
const { mkdir: mkdir6 } = await import("fs/promises");
|
|
4608
|
+
await mkdir6(dir, { recursive: true });
|
|
3228
4609
|
}
|
|
3229
|
-
await
|
|
4610
|
+
await writeFile6(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
|
|
3230
4611
|
try {
|
|
3231
4612
|
await chmod2(path, 384);
|
|
3232
4613
|
} catch {
|
|
@@ -3304,13 +4685,30 @@ async function setup() {
|
|
|
3304
4685
|
console.log(` Configured: ${target.name}`);
|
|
3305
4686
|
}
|
|
3306
4687
|
console.log(
|
|
3307
|
-
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides
|
|
4688
|
+
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides 20 tools: get_context, get_checks,\n get_project_rules, get_prompt_templates, get_specs, get_docs,\n get_tech_stack, get_planning_context, get_plan_board, create_check,\n create_project_rule, create_prompt_template, update_spec,\n report_decision, publish_skill, list_skills, install_skill,\n publish_setup_part, resolve_person, sync_memory.\n\n get_guardrails and create_guardrail remain registered as deprecated\n aliases of get_checks and create_check \u2014 existing configs keep working.\n"
|
|
3308
4689
|
);
|
|
4690
|
+
return { apiKey, apiUrl };
|
|
3309
4691
|
}
|
|
3310
4692
|
var args = process.argv.slice(2);
|
|
3311
4693
|
var command = args[0];
|
|
3312
4694
|
if (command === "setup") {
|
|
3313
|
-
|
|
4695
|
+
Promise.resolve().then(() => (init_setup_team(), setup_team_exports)).then(async ({ parseSetupArgs: parseSetupArgs2, runTeamSetupCommand: runTeamSetupCommand2 }) => {
|
|
4696
|
+
let teamArgs;
|
|
4697
|
+
try {
|
|
4698
|
+
teamArgs = parseSetupArgs2(args.slice(1));
|
|
4699
|
+
} catch (err) {
|
|
4700
|
+
console.error(`
|
|
4701
|
+
Error: ${err.message}
|
|
4702
|
+
`);
|
|
4703
|
+
process.exit(1);
|
|
4704
|
+
}
|
|
4705
|
+
const key = await setup();
|
|
4706
|
+
return runTeamSetupCommand2(teamArgs, key, {
|
|
4707
|
+
out: console.log,
|
|
4708
|
+
ask: (question) => prompt(question),
|
|
4709
|
+
terminal: !!process.stdin.isTTY
|
|
4710
|
+
});
|
|
4711
|
+
}).then((code) => process.exit(code)).catch((err) => {
|
|
3314
4712
|
console.error(`
|
|
3315
4713
|
Error: ${err.message}
|
|
3316
4714
|
`);
|
|
@@ -3326,13 +4724,27 @@ if (command === "setup") {
|
|
|
3326
4724
|
Promise.resolve().then(() => (init_plugin_path(), plugin_path_exports)).then(
|
|
3327
4725
|
({ runPluginPath: runPluginPath2 }) => runPluginPath2(args.slice(1), { out: console.log, err: console.error })
|
|
3328
4726
|
).then((code) => process.exit(code));
|
|
4727
|
+
} else if (command === "sync-memory") {
|
|
4728
|
+
const terminal = !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
4729
|
+
Promise.resolve().then(() => (init_sync_memory(), sync_memory_exports)).then(
|
|
4730
|
+
({ runSyncMemory: runSyncMemory2 }) => runSyncMemory2(args.slice(1), {
|
|
4731
|
+
out: console.log,
|
|
4732
|
+
err: console.error,
|
|
4733
|
+
ask: terminal ? (question) => prompt(question) : void 0
|
|
4734
|
+
})
|
|
4735
|
+
).then((code) => process.exit(code));
|
|
3329
4736
|
} else if (command === "--help" || command === "-h") {
|
|
3330
4737
|
console.log(`
|
|
3331
4738
|
SystemDox MCP Server
|
|
3332
4739
|
|
|
3333
4740
|
Usage:
|
|
3334
4741
|
systemdox-mcp-server Start the MCP server (stdio transport)
|
|
3335
|
-
systemdox-mcp-server setup
|
|
4742
|
+
systemdox-mcp-server setup [--project <name>] [--dir <checkout>] [--yes] [--overwrite]
|
|
4743
|
+
Configure the MCP server in your AI coding tool,
|
|
4744
|
+
then your team's setup: the team instructions,
|
|
4745
|
+
the plugin's marketplace and the project's team
|
|
4746
|
+
memory (shown first; nothing changes until you
|
|
4747
|
+
answer y)
|
|
3336
4748
|
systemdox-mcp-server install-skill <name> [--version n] [--target dir] [--overwrite]
|
|
3337
4749
|
Install a skill published to SystemDox
|
|
3338
4750
|
(default folder: ~/.claude/skills/<name>)
|
|
@@ -3343,6 +4755,11 @@ if (command === "setup") {
|
|
|
3343
4755
|
systemdox-mcp-server marketplace-auth
|
|
3344
4756
|
Print the header for your team's marketplace
|
|
3345
4757
|
(Claude Code runs it)
|
|
4758
|
+
systemdox-mcp-server sync-memory [--project <name>] [--dir <folder>]
|
|
4759
|
+
[--import <folder>] [--dry-run]
|
|
4760
|
+
Sync a project's team memory with SystemDox
|
|
4761
|
+
(default folder:
|
|
4762
|
+
~/.claude/systemdox/<workspace>/memory/<project>)
|
|
3346
4763
|
systemdox-mcp-server --help Show this help message
|
|
3347
4764
|
|
|
3348
4765
|
Environment variables:
|