skillwiki 0.8.1-beta.1 → 0.8.1-beta.11
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/dist/cli.js +675 -270
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +3 -2
- package/skills/README.md +9 -1
- package/skills/hooks/hooks-codex.json +16 -0
- package/skills/hooks/session-context +95 -0
- package/skills/hooks/session-start +6 -20
- package/skills/hooks/session-start-codex +15 -0
- package/skills/package.json +2 -1
- package/skills/skills/proj-decide/SKILL.md +25 -0
- package/skills/skills/proj-distill/SKILL.md +55 -0
- package/skills/skills/proj-init/SKILL.md +30 -0
- package/skills/skills/proj-work/SKILL.md +69 -0
- package/skills/skills/using-skillwiki/SKILL.md +157 -0
- package/skills/skills/wiki-adapter-prd/SKILL.md +88 -0
- package/skills/skills/wiki-add-task/SKILL.md +102 -0
- package/skills/skills/wiki-archive/SKILL.md +46 -0
- package/skills/skills/wiki-audit/SKILL.md +34 -0
- package/skills/skills/wiki-canvas/SKILL.md +57 -0
- package/skills/skills/wiki-crystallize/SKILL.md +29 -0
- package/skills/skills/wiki-gate-plan-mode/SKILL.md +80 -0
- package/skills/skills/wiki-ingest/SKILL.md +55 -0
- package/skills/skills/wiki-init/SKILL.md +37 -0
- package/skills/skills/wiki-lint/SKILL.md +25 -0
- package/skills/skills/wiki-query/SKILL.md +36 -0
- package/skills/skills/wiki-reingest/SKILL.md +55 -0
- package/skills/skills/wiki-sync/SKILL.md +240 -0
- package/skills/using-skillwiki/SKILL.md +4 -5
- package/skills/wiki-query/SKILL.md +1 -1
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
|
|
10
10
|
// src/cli.ts
|
|
11
11
|
import { readFileSync as readFileSync12 } from "fs";
|
|
12
|
-
import { join as
|
|
12
|
+
import { join as join44 } from "path";
|
|
13
13
|
import { Command as Command2 } from "commander";
|
|
14
14
|
|
|
15
15
|
// ../shared/src/exit-codes.ts
|
|
@@ -500,6 +500,7 @@ import { dirname } from "path";
|
|
|
500
500
|
import { readFile as readFile3, readdir, stat } from "fs/promises";
|
|
501
501
|
import { join as join3, relative as relative2, sep as sep2 } from "path";
|
|
502
502
|
var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
503
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
503
504
|
async function scanVault(root) {
|
|
504
505
|
try {
|
|
505
506
|
await stat(join3(root, "SCHEMA.md"));
|
|
@@ -510,6 +511,7 @@ async function scanVault(root) {
|
|
|
510
511
|
const rels = all.map((p) => ({ absPath: p, relPath: relative2(root, p).split(sep2).join("/") }));
|
|
511
512
|
return ok({
|
|
512
513
|
root,
|
|
514
|
+
allMarkdown: rels,
|
|
513
515
|
typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
|
|
514
516
|
raw: rels.filter((p) => p.relPath.startsWith("raw/")),
|
|
515
517
|
workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
|
|
@@ -521,8 +523,10 @@ async function walk(dir) {
|
|
|
521
523
|
const out = [];
|
|
522
524
|
for (const e of entries) {
|
|
523
525
|
const p = join3(dir, e.name);
|
|
524
|
-
if (e.isDirectory())
|
|
525
|
-
|
|
526
|
+
if (e.isDirectory()) {
|
|
527
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
528
|
+
out.push(...await walk(p));
|
|
529
|
+
} else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
|
|
526
530
|
}
|
|
527
531
|
return out;
|
|
528
532
|
}
|
|
@@ -548,23 +552,137 @@ function extractBodyWikilinks(body) {
|
|
|
548
552
|
return out;
|
|
549
553
|
}
|
|
550
554
|
|
|
551
|
-
// src/
|
|
552
|
-
async function
|
|
553
|
-
const scan = await scanVault(input.vault);
|
|
554
|
-
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
555
|
+
// src/utils/community.ts
|
|
556
|
+
async function buildWikilinkAdjacency(typedKnowledge) {
|
|
555
557
|
const adjacency = {};
|
|
556
558
|
const slugToPath = {};
|
|
557
|
-
for (const p of
|
|
559
|
+
for (const p of typedKnowledge) {
|
|
558
560
|
const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
|
|
559
561
|
slugToPath[slug] = p.relPath;
|
|
560
562
|
}
|
|
561
|
-
for (const p of
|
|
563
|
+
for (const p of typedKnowledge) {
|
|
562
564
|
const text = await readPage(p);
|
|
563
565
|
const split = splitFrontmatter(text);
|
|
564
566
|
const body = split.ok ? split.data.body : text;
|
|
565
567
|
const links = extractBodyWikilinks(body);
|
|
566
568
|
adjacency[p.relPath] = links.map((slug) => slugToPath[slug.split("/").pop()]).filter((x) => Boolean(x));
|
|
567
569
|
}
|
|
570
|
+
return adjacency;
|
|
571
|
+
}
|
|
572
|
+
function toUndirectedWeighted(adj) {
|
|
573
|
+
const g = /* @__PURE__ */ new Map();
|
|
574
|
+
const ensure = (n) => {
|
|
575
|
+
let m = g.get(n);
|
|
576
|
+
if (!m) {
|
|
577
|
+
m = /* @__PURE__ */ new Map();
|
|
578
|
+
g.set(n, m);
|
|
579
|
+
}
|
|
580
|
+
return m;
|
|
581
|
+
};
|
|
582
|
+
for (const node of Object.keys(adj)) ensure(node);
|
|
583
|
+
for (const [a, nbrs] of Object.entries(adj)) {
|
|
584
|
+
for (const b of nbrs) {
|
|
585
|
+
if (a === b) continue;
|
|
586
|
+
ensure(a).set(b, 1);
|
|
587
|
+
ensure(b).set(a, 1);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return g;
|
|
591
|
+
}
|
|
592
|
+
function louvain(g) {
|
|
593
|
+
const nodes = [...g.keys()].sort();
|
|
594
|
+
const comm = /* @__PURE__ */ new Map();
|
|
595
|
+
nodes.forEach((n, i) => comm.set(n, i));
|
|
596
|
+
const k = /* @__PURE__ */ new Map();
|
|
597
|
+
let m2 = 0;
|
|
598
|
+
for (const n of nodes) {
|
|
599
|
+
let deg = 0;
|
|
600
|
+
for (const w of g.get(n).values()) deg += w;
|
|
601
|
+
k.set(n, deg);
|
|
602
|
+
m2 += deg;
|
|
603
|
+
}
|
|
604
|
+
if (m2 === 0) return comm;
|
|
605
|
+
const sumTot = /* @__PURE__ */ new Map();
|
|
606
|
+
for (const n of nodes) {
|
|
607
|
+
const c = comm.get(n);
|
|
608
|
+
sumTot.set(c, (sumTot.get(c) ?? 0) + k.get(n));
|
|
609
|
+
}
|
|
610
|
+
let improved = true;
|
|
611
|
+
while (improved) {
|
|
612
|
+
improved = false;
|
|
613
|
+
for (const n of nodes) {
|
|
614
|
+
const cur = comm.get(n);
|
|
615
|
+
const kn = k.get(n);
|
|
616
|
+
sumTot.set(cur, sumTot.get(cur) - kn);
|
|
617
|
+
const wToComm = /* @__PURE__ */ new Map();
|
|
618
|
+
for (const [nb, w] of g.get(n)) {
|
|
619
|
+
if (nb === n) continue;
|
|
620
|
+
const c = comm.get(nb);
|
|
621
|
+
wToComm.set(c, (wToComm.get(c) ?? 0) + w);
|
|
622
|
+
}
|
|
623
|
+
const gainFor = (c) => (wToComm.get(c) ?? 0) - (sumTot.get(c) ?? 0) * kn / m2;
|
|
624
|
+
const curGain = gainFor(cur);
|
|
625
|
+
let bestComm = cur;
|
|
626
|
+
let bestDelta = 0;
|
|
627
|
+
for (const c of wToComm.keys()) {
|
|
628
|
+
const delta = gainFor(c) - curGain;
|
|
629
|
+
if (delta > bestDelta) {
|
|
630
|
+
bestDelta = delta;
|
|
631
|
+
bestComm = c;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
comm.set(n, bestComm);
|
|
635
|
+
sumTot.set(bestComm, (sumTot.get(bestComm) ?? 0) + kn);
|
|
636
|
+
if (bestComm !== cur) improved = true;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return comm;
|
|
640
|
+
}
|
|
641
|
+
function communityCohesion(members, g) {
|
|
642
|
+
const n = members.length;
|
|
643
|
+
if (n < 2) return 1;
|
|
644
|
+
const set = new Set(members);
|
|
645
|
+
let internal = 0;
|
|
646
|
+
for (const a of members) {
|
|
647
|
+
for (const [b, w] of g.get(a) ?? /* @__PURE__ */ new Map()) {
|
|
648
|
+
if (a < b && set.has(b)) internal += w;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return internal / (n * (n - 1) / 2);
|
|
652
|
+
}
|
|
653
|
+
function findSparseCommunities(adj, opts = {}) {
|
|
654
|
+
const minSize = opts.minSize ?? 3;
|
|
655
|
+
const maxCohesion = opts.maxCohesion ?? 0.15;
|
|
656
|
+
const g = toUndirectedWeighted(adj);
|
|
657
|
+
const comm = louvain(g);
|
|
658
|
+
const groups = /* @__PURE__ */ new Map();
|
|
659
|
+
for (const [node, c] of comm) {
|
|
660
|
+
const arr = groups.get(c);
|
|
661
|
+
if (arr) arr.push(node);
|
|
662
|
+
else groups.set(c, [node]);
|
|
663
|
+
}
|
|
664
|
+
const out = [];
|
|
665
|
+
for (const members of groups.values()) {
|
|
666
|
+
if (members.length < minSize) continue;
|
|
667
|
+
const cohesion = communityCohesion(members, g);
|
|
668
|
+
if (cohesion < maxCohesion) {
|
|
669
|
+
out.push({
|
|
670
|
+
members: [...members].sort(),
|
|
671
|
+
size: members.length,
|
|
672
|
+
cohesion: Math.round(cohesion * 1e3) / 1e3,
|
|
673
|
+
action: members.length <= 5 ? "merge into adjacent community" : "split into smaller topics"
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
out.sort((a, b) => a.cohesion - b.cohesion);
|
|
678
|
+
return out;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/commands/graph.ts
|
|
682
|
+
async function runGraphBuild(input) {
|
|
683
|
+
const scan = await scanVault(input.vault);
|
|
684
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
685
|
+
const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge);
|
|
568
686
|
const adamicAdar = computeAdamicAdar(adjacency);
|
|
569
687
|
const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
|
|
570
688
|
try {
|
|
@@ -2465,9 +2583,22 @@ ${content}
|
|
|
2465
2583
|
}
|
|
2466
2584
|
|
|
2467
2585
|
// src/commands/lint.ts
|
|
2468
|
-
import { existsSync as
|
|
2469
|
-
import { readFile as
|
|
2470
|
-
import { join as
|
|
2586
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2587
|
+
import { readFile as readFile17 } from "fs/promises";
|
|
2588
|
+
import { join as join22 } from "path";
|
|
2589
|
+
|
|
2590
|
+
// src/commands/sparse-community.ts
|
|
2591
|
+
async function runSparseCommunity(input) {
|
|
2592
|
+
const scan = await scanVault(input.vault);
|
|
2593
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2594
|
+
const adjacency = await buildWikilinkAdjacency(scan.data.typedKnowledge);
|
|
2595
|
+
const communities = findSparseCommunities(adjacency, {
|
|
2596
|
+
minSize: input.minSize,
|
|
2597
|
+
maxCohesion: input.maxCohesion
|
|
2598
|
+
});
|
|
2599
|
+
const humanHint = communities.length === 0 ? "no sparse communities" : communities.map((c) => ` cohesion ${c.cohesion} (${c.size} pages): ${c.action}`).join("\n");
|
|
2600
|
+
return { exitCode: ExitCode.OK, result: ok({ communities, humanHint }) };
|
|
2601
|
+
}
|
|
2471
2602
|
|
|
2472
2603
|
// src/commands/topic-map-check.ts
|
|
2473
2604
|
var DEFAULT_THRESHOLD = 200;
|
|
@@ -2710,28 +2841,100 @@ async function runRawBodyDedup(vault) {
|
|
|
2710
2841
|
}
|
|
2711
2842
|
|
|
2712
2843
|
// src/commands/path-too-long.ts
|
|
2844
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2845
|
+
import { mkdir as mkdir8, readFile as readFile16, rename as rename6, unlink as unlink3 } from "fs/promises";
|
|
2846
|
+
import { dirname as dirname8, join as join21, posix, resolve as resolve4 } from "path";
|
|
2713
2847
|
var MAX_PATH_LENGTH = 240;
|
|
2848
|
+
var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
|
|
2714
2849
|
async function runPathTooLong(input) {
|
|
2715
2850
|
const scan = await scanVault(input.vault);
|
|
2716
2851
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2717
|
-
const
|
|
2718
|
-
const violations = [];
|
|
2719
|
-
for (const page of allPages) {
|
|
2720
|
-
if (page.relPath.length > MAX_PATH_LENGTH) {
|
|
2721
|
-
violations.push({ relPath: page.relPath, length: page.relPath.length });
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2852
|
+
const violations = findPathTooLongViolations(scan.data.allMarkdown, MAX_PATH_LENGTH);
|
|
2724
2853
|
if (violations.length > 0) {
|
|
2725
2854
|
return {
|
|
2726
2855
|
exitCode: ExitCode.LINT_HAS_ERRORS,
|
|
2727
2856
|
result: ok({
|
|
2728
2857
|
violations,
|
|
2729
|
-
humanHint: violations.map((v) => `${v.relPath}: ${v.length} chars (max ${MAX_PATH_LENGTH})`).join("\n")
|
|
2858
|
+
humanHint: violations.map((v) => `${v.relPath}: ${v.length} chars (max ${MAX_PATH_LENGTH}) -> ${v.suggestedRelPath}`).join("\n")
|
|
2730
2859
|
})
|
|
2731
2860
|
};
|
|
2732
2861
|
}
|
|
2733
2862
|
return { exitCode: ExitCode.OK, result: ok({ violations, humanHint: "all paths within length limit" }) };
|
|
2734
2863
|
}
|
|
2864
|
+
async function fixPathTooLong(input) {
|
|
2865
|
+
const scan = await scanVault(input.vault);
|
|
2866
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
2867
|
+
const maxFixLength = maxFixPathLength(input.vault);
|
|
2868
|
+
const violations = findPathTooLongViolations(scan.data.allMarkdown, maxFixLength);
|
|
2869
|
+
const fixed = [];
|
|
2870
|
+
const unresolved = [];
|
|
2871
|
+
for (const violation of violations) {
|
|
2872
|
+
const target = await resolveFixTarget(input.vault, violation.relPath, violation.suggestedRelPath, maxFixLength);
|
|
2873
|
+
if (!target || target.relPath === violation.relPath || target.relPath.length > maxFixLength) {
|
|
2874
|
+
unresolved.push(violation.relPath);
|
|
2875
|
+
continue;
|
|
2876
|
+
}
|
|
2877
|
+
try {
|
|
2878
|
+
if (target.mode === "dedupe") {
|
|
2879
|
+
await unlink3(join21(input.vault, violation.relPath));
|
|
2880
|
+
} else {
|
|
2881
|
+
await mkdir8(dirname8(join21(input.vault, target.relPath)), { recursive: true });
|
|
2882
|
+
await rename6(join21(input.vault, violation.relPath), join21(input.vault, target.relPath));
|
|
2883
|
+
}
|
|
2884
|
+
fixed.push({ from: violation.relPath, to: target.relPath });
|
|
2885
|
+
} catch {
|
|
2886
|
+
unresolved.push(violation.relPath);
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
const rewired = [];
|
|
2890
|
+
if (fixed.length > 0) {
|
|
2891
|
+
const afterScan = await scanVault(input.vault);
|
|
2892
|
+
if (afterScan.ok) {
|
|
2893
|
+
for (const page of afterScan.data.allMarkdown) {
|
|
2894
|
+
if (!shouldRewriteReferences(page.relPath)) continue;
|
|
2895
|
+
try {
|
|
2896
|
+
const original = await readFile16(page.absPath, "utf8");
|
|
2897
|
+
let updated = original;
|
|
2898
|
+
for (const fix of fixed) {
|
|
2899
|
+
updated = replacePathReferences(updated, fix.from, fix.to);
|
|
2900
|
+
}
|
|
2901
|
+
if (updated !== original) {
|
|
2902
|
+
const write = await safeWritePage(page.absPath, updated);
|
|
2903
|
+
if (write.ok) rewired.push(page.relPath);
|
|
2904
|
+
else unresolved.push(`${page.relPath} (rewire)`);
|
|
2905
|
+
}
|
|
2906
|
+
} catch {
|
|
2907
|
+
unresolved.push(`${page.relPath} (rewire)`);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
const hintLines = [
|
|
2913
|
+
`fixed: ${fixed.length}`,
|
|
2914
|
+
`rewired: ${rewired.length}`,
|
|
2915
|
+
`unresolved: ${unresolved.length}`
|
|
2916
|
+
];
|
|
2917
|
+
for (const f of fixed) hintLines.push(` ${f.from} -> ${f.to}`);
|
|
2918
|
+
for (const u of unresolved) hintLines.push(` unresolved: ${u}`);
|
|
2919
|
+
return {
|
|
2920
|
+
exitCode: unresolved.length > 0 ? ExitCode.LINT_HAS_ERRORS : ExitCode.OK,
|
|
2921
|
+
result: ok({ fixed, unresolved, rewired, humanHint: hintLines.join("\n") })
|
|
2922
|
+
};
|
|
2923
|
+
}
|
|
2924
|
+
function findPathTooLongViolations(pages, maxLength) {
|
|
2925
|
+
return pages.filter((page) => page.relPath.length > maxLength).map((page) => ({
|
|
2926
|
+
relPath: page.relPath,
|
|
2927
|
+
length: page.relPath.length,
|
|
2928
|
+
suggestedRelPath: truncateFilename(page.relPath, maxLength)
|
|
2929
|
+
}));
|
|
2930
|
+
}
|
|
2931
|
+
function maxFixPathLength(vault) {
|
|
2932
|
+
if (process.platform !== "win32") return MAX_PATH_LENGTH;
|
|
2933
|
+
const root = resolve4(vault);
|
|
2934
|
+
const separatorBudget = root.endsWith("\\") || root.endsWith("/") ? 0 : 1;
|
|
2935
|
+
const absoluteSafeRelLength = WINDOWS_ABSOLUTE_PATH_LIMIT - root.length - separatorBudget;
|
|
2936
|
+
return Math.max(1, Math.min(MAX_PATH_LENGTH, absoluteSafeRelLength));
|
|
2937
|
+
}
|
|
2735
2938
|
function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
|
|
2736
2939
|
if (relPath.length <= maxLength) return relPath;
|
|
2737
2940
|
const lastSlash = relPath.lastIndexOf("/");
|
|
@@ -2745,15 +2948,62 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
|
|
|
2745
2948
|
const maxPrefixLen = maxLength - dirPrefix.length - suffix.length;
|
|
2746
2949
|
if (maxPrefixLen <= 0) {
|
|
2747
2950
|
const fallback = dirPrefix + hash + ext;
|
|
2748
|
-
|
|
2749
|
-
const dirBudget = maxLength - suffix.length;
|
|
2750
|
-
return dirPrefix.slice(0, Math.max(0, dirBudget)) + suffix;
|
|
2751
|
-
}
|
|
2752
|
-
return fallback;
|
|
2951
|
+
return fallback.length <= maxLength ? fallback : relPath;
|
|
2753
2952
|
}
|
|
2754
2953
|
const prefix = base.slice(0, maxPrefixLen).replace(/[-_\s]+$/, "");
|
|
2755
2954
|
return dirPrefix + prefix + suffix;
|
|
2756
2955
|
}
|
|
2956
|
+
async function resolveFixTarget(vault, original, preferred, maxLength) {
|
|
2957
|
+
for (const candidate of candidateRelPaths(preferred, maxLength)) {
|
|
2958
|
+
if (candidate === original || candidate.length > maxLength) continue;
|
|
2959
|
+
const candidatePath = join21(vault, candidate);
|
|
2960
|
+
if (!existsSync4(candidatePath)) return { relPath: candidate, mode: "rename" };
|
|
2961
|
+
if (await hasSameContent(join21(vault, original), candidatePath)) {
|
|
2962
|
+
return { relPath: candidate, mode: "dedupe" };
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
function candidateRelPaths(preferred, maxLength) {
|
|
2968
|
+
const candidates = [preferred];
|
|
2969
|
+
if (preferred.length > maxLength) return candidates;
|
|
2970
|
+
const dir = posix.dirname(preferred) === "." ? "" : posix.dirname(preferred);
|
|
2971
|
+
const filename = posix.basename(preferred);
|
|
2972
|
+
const ext = filename.endsWith(".md") ? ".md" : "";
|
|
2973
|
+
const base = ext ? filename.slice(0, -3) : filename;
|
|
2974
|
+
const dirPrefix = dir ? `${dir}/` : "";
|
|
2975
|
+
for (let i = 2; i < 100; i++) {
|
|
2976
|
+
const suffix = `-${i}${ext}`;
|
|
2977
|
+
const prefixBudget = maxLength - dirPrefix.length - suffix.length;
|
|
2978
|
+
if (prefixBudget <= 0) break;
|
|
2979
|
+
candidates.push(`${dirPrefix}${base.slice(0, prefixBudget).replace(/[-_\s]+$/, "")}${suffix}`);
|
|
2980
|
+
}
|
|
2981
|
+
return candidates;
|
|
2982
|
+
}
|
|
2983
|
+
async function hasSameContent(a, b) {
|
|
2984
|
+
try {
|
|
2985
|
+
const [left, right] = await Promise.all([readFile16(a), readFile16(b)]);
|
|
2986
|
+
return left.equals(right);
|
|
2987
|
+
} catch {
|
|
2988
|
+
return false;
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
function shouldRewriteReferences(relPath) {
|
|
2992
|
+
if (relPath.startsWith("raw/")) return false;
|
|
2993
|
+
if (relPath.startsWith("_archive/")) return false;
|
|
2994
|
+
return true;
|
|
2995
|
+
}
|
|
2996
|
+
function replacePathReferences(content, oldRelPath, newRelPath) {
|
|
2997
|
+
let updated = content.replaceAll(oldRelPath, newRelPath);
|
|
2998
|
+
const oldStem = posix.basename(oldRelPath).replace(/\.md$/, "");
|
|
2999
|
+
const newStem = posix.basename(newRelPath).replace(/\.md$/, "");
|
|
3000
|
+
if (oldStem !== newStem) {
|
|
3001
|
+
const oldStemEscaped = oldStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3002
|
+
const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
|
|
3003
|
+
updated = updated.replace(stemWikilinkRe, (_match, alias) => `[[${newStem}${alias ?? ""}]]`);
|
|
3004
|
+
}
|
|
3005
|
+
return updated;
|
|
3006
|
+
}
|
|
2757
3007
|
function computeShortHash(input) {
|
|
2758
3008
|
let hash = 2166136261;
|
|
2759
3009
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -2791,6 +3041,7 @@ function buildCliSurface() {
|
|
|
2791
3041
|
program2.command("claim").option("--project <slug>").option("--slug <slug>").option("--wiki <name>");
|
|
2792
3042
|
program2.command("pagesize").option("--lines <n>").option("--wiki <name>");
|
|
2793
3043
|
program2.command("log-rotate").option("--threshold <n>").option("--apply").option("--wiki <name>");
|
|
3044
|
+
program2.command("log-append").requiredOption("--content <text>").option("--wiki <name>");
|
|
2794
3045
|
program2.command("lint").option("--days <n>").option("--lines <n>").option("--log-threshold <n>").option("--fix").option("--only <bucket>").option("--wiki <name>");
|
|
2795
3046
|
program2.command("config");
|
|
2796
3047
|
program2.command("doctor");
|
|
@@ -2940,8 +3191,16 @@ function extractSourceEntries(rawFm) {
|
|
|
2940
3191
|
}
|
|
2941
3192
|
var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
|
|
2942
3193
|
var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "file_source_url", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
|
|
2943
|
-
var INFO_ORDER = ["bridges", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
|
|
3194
|
+
var INFO_ORDER = ["bridges", "sparse_community", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
|
|
3195
|
+
var KNOWN_BUCKETS = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
|
|
2944
3196
|
async function runLint(input) {
|
|
3197
|
+
if (input.only && !KNOWN_BUCKETS.includes(input.only)) {
|
|
3198
|
+
return {
|
|
3199
|
+
exitCode: ExitCode.USAGE,
|
|
3200
|
+
result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${KNOWN_BUCKETS.join(", ")}` }
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
const shouldFix = (bucket) => !!input.fix && (!input.only || input.only === bucket);
|
|
2945
3204
|
const buckets = {};
|
|
2946
3205
|
const fixed = [];
|
|
2947
3206
|
const unresolved = [];
|
|
@@ -2983,6 +3242,10 @@ async function runLint(input) {
|
|
|
2983
3242
|
if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
|
|
2984
3243
|
if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
|
|
2985
3244
|
}
|
|
3245
|
+
const sparse = await runSparseCommunity({ vault: input.vault });
|
|
3246
|
+
if (sparse.result.ok && sparse.result.data.communities.length > 0) {
|
|
3247
|
+
buckets.sparse_community = sparse.result.data.communities;
|
|
3248
|
+
}
|
|
2986
3249
|
const topicMap = await runTopicMapCheck({ vault: input.vault });
|
|
2987
3250
|
if (topicMap.result.ok && topicMap.result.data.recommended) {
|
|
2988
3251
|
buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
|
|
@@ -3061,7 +3324,7 @@ async function runLint(input) {
|
|
|
3061
3324
|
let rawPath = entry.replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
|
|
3062
3325
|
rawPath = rawPath.replace(/^\^\[/, "").replace(/\]$/, "");
|
|
3063
3326
|
if (!rawPath.startsWith("raw/") && !rawPath.startsWith("_archive/raw/")) continue;
|
|
3064
|
-
if (!
|
|
3327
|
+
if (!existsSync5(join22(input.vault, rawPath)) && !existsSync5(join22(input.vault, rawPath + ".md")) && !rawPath.startsWith("_archive/") && !existsSync5(join22(input.vault, "_archive", rawPath)) && !existsSync5(join22(input.vault, "_archive", rawPath + ".md"))) {
|
|
3065
3328
|
brokenSourceFlags.push(`${page.relPath}: ${rawPath}`);
|
|
3066
3329
|
}
|
|
3067
3330
|
}
|
|
@@ -3152,11 +3415,11 @@ async function runLint(input) {
|
|
|
3152
3415
|
const slugMatch = String(entry).match(/\[\[([^\]]+)\]\]/);
|
|
3153
3416
|
if (!slugMatch) continue;
|
|
3154
3417
|
const slug = slugMatch[1];
|
|
3155
|
-
const knowledgePath =
|
|
3156
|
-
if (!
|
|
3418
|
+
const knowledgePath = join22(input.vault, "projects", slug, "knowledge.md");
|
|
3419
|
+
if (!existsSync5(knowledgePath)) continue;
|
|
3157
3420
|
const pageRef = page.relPath.replace(/\.md$/, "");
|
|
3158
3421
|
try {
|
|
3159
|
-
const knowledgeContent = await
|
|
3422
|
+
const knowledgeContent = await readFile17(knowledgePath, "utf8");
|
|
3160
3423
|
if (!knowledgeContent.includes(`[[${pageRef}]]`)) {
|
|
3161
3424
|
orphanedProjectPages.push(`${page.relPath}: not in projects/${slug}/knowledge.md`);
|
|
3162
3425
|
}
|
|
@@ -3197,13 +3460,13 @@ async function runLint(input) {
|
|
|
3197
3460
|
}
|
|
3198
3461
|
}
|
|
3199
3462
|
if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
|
|
3200
|
-
if (
|
|
3463
|
+
if (shouldFix("legacy_citation_style") && legacyPages.length > 0) {
|
|
3201
3464
|
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
3202
3465
|
const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
|
|
3203
3466
|
for (const relPath of legacyPages) {
|
|
3204
3467
|
try {
|
|
3205
3468
|
const absPath = `${input.vault}/${relPath}`;
|
|
3206
|
-
const raw = await
|
|
3469
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3207
3470
|
const split = splitFrontmatter(raw);
|
|
3208
3471
|
if (!split.ok) {
|
|
3209
3472
|
unresolved.push(relPath);
|
|
@@ -3298,11 +3561,11 @@ ${newBody}`;
|
|
|
3298
3561
|
else delete buckets.legacy_citation_style;
|
|
3299
3562
|
}
|
|
3300
3563
|
}
|
|
3301
|
-
if (
|
|
3564
|
+
if (shouldFix("missing_overview") && noOverview.length > 0) {
|
|
3302
3565
|
for (const relPath of noOverview) {
|
|
3303
3566
|
try {
|
|
3304
3567
|
const absPath = `${input.vault}/${relPath}`;
|
|
3305
|
-
const raw = await
|
|
3568
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3306
3569
|
const split = splitFrontmatter(raw);
|
|
3307
3570
|
if (!split.ok) {
|
|
3308
3571
|
unresolved.push(relPath);
|
|
@@ -3339,11 +3602,11 @@ ${trimmedBody}`;
|
|
|
3339
3602
|
if (remaining.length > 0) buckets.missing_overview = remaining;
|
|
3340
3603
|
else delete buckets.missing_overview;
|
|
3341
3604
|
}
|
|
3342
|
-
if (
|
|
3605
|
+
if (shouldFix("missing_tldr") && missingTldrFlags.length > 0) {
|
|
3343
3606
|
for (const relPath of missingTldrFlags) {
|
|
3344
3607
|
try {
|
|
3345
3608
|
const absPath = `${input.vault}/${relPath}`;
|
|
3346
|
-
const raw = await
|
|
3609
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3347
3610
|
const split = splitFrontmatter(raw);
|
|
3348
3611
|
if (!split.ok) {
|
|
3349
3612
|
unresolved.push(relPath);
|
|
@@ -3386,14 +3649,14 @@ ${lines.join("\n")}`;
|
|
|
3386
3649
|
if (remaining.length > 0) buckets.missing_tldr = remaining;
|
|
3387
3650
|
else delete buckets.missing_tldr;
|
|
3388
3651
|
}
|
|
3389
|
-
if (
|
|
3652
|
+
if (shouldFix("wikilink_citation") && wikilinkCitationFlags.length > 0) {
|
|
3390
3653
|
const WIKILINK_RE = /\[\[raw\/([^\]|]+)(?:\|[^\]]*)?\]\]/g;
|
|
3391
3654
|
const FENCE_RE2 = /```[\s\S]*?```/g;
|
|
3392
3655
|
const wikilinkFixed = [];
|
|
3393
3656
|
for (const relPath of wikilinkCitationFlags) {
|
|
3394
3657
|
try {
|
|
3395
3658
|
const absPath = `${input.vault}/${relPath}`;
|
|
3396
|
-
const raw = await
|
|
3659
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3397
3660
|
const split = splitFrontmatter(raw);
|
|
3398
3661
|
if (!split.ok) {
|
|
3399
3662
|
unresolved.push(relPath);
|
|
@@ -3475,12 +3738,12 @@ ${newBody}`;
|
|
|
3475
3738
|
else delete buckets.wikilink_citation;
|
|
3476
3739
|
}
|
|
3477
3740
|
}
|
|
3478
|
-
if (
|
|
3741
|
+
if (shouldFix("file_source_url") && fileSourceUrlFlags.length > 0) {
|
|
3479
3742
|
const FILE_FIXED = [];
|
|
3480
3743
|
for (const relPath of fileSourceUrlFlags) {
|
|
3481
3744
|
try {
|
|
3482
3745
|
const absPath = `${input.vault}/${relPath}`;
|
|
3483
|
-
const raw = await
|
|
3746
|
+
const raw = await readFile17(absPath, "utf8");
|
|
3484
3747
|
const parts = raw.split("---", 3);
|
|
3485
3748
|
if (parts.length < 3) {
|
|
3486
3749
|
unresolved.push(relPath);
|
|
@@ -3515,36 +3778,11 @@ ${newBody}`;
|
|
|
3515
3778
|
}
|
|
3516
3779
|
}
|
|
3517
3780
|
const pathViolations = buckets.path_too_long;
|
|
3518
|
-
if (
|
|
3519
|
-
const
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
const newRelPath = truncateFilename(v.relPath);
|
|
3524
|
-
const newAbsPath = `${input.vault}/${newRelPath}`;
|
|
3525
|
-
await rename6(absPath, newAbsPath);
|
|
3526
|
-
const oldPathEscaped = v.relPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3527
|
-
for (const page of allPages) {
|
|
3528
|
-
if (page.relPath === v.relPath) continue;
|
|
3529
|
-
const content = await readFile16(page.absPath, "utf8");
|
|
3530
|
-
if (!content.includes(v.relPath)) continue;
|
|
3531
|
-
let updated = content;
|
|
3532
|
-
const citationRe = new RegExp(`\\^\\[${oldPathEscaped}\\]`, "g");
|
|
3533
|
-
updated = updated.replace(citationRe, `^[${newRelPath}]`);
|
|
3534
|
-
const wikilinkRe = new RegExp(`\\[\\[${oldPathEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
|
|
3535
|
-
updated = updated.replace(wikilinkRe, (_m, alias) => `[[${newRelPath}${alias ?? ""}]]`);
|
|
3536
|
-
if (updated !== content) {
|
|
3537
|
-
const w = await safeWritePage(page.absPath, updated);
|
|
3538
|
-
if (!w.ok) {
|
|
3539
|
-
unresolved.push(`${page.relPath} (rewire)`);
|
|
3540
|
-
}
|
|
3541
|
-
}
|
|
3542
|
-
}
|
|
3543
|
-
pathFixed.push(v.relPath);
|
|
3544
|
-
} catch {
|
|
3545
|
-
unresolved.push(v.relPath);
|
|
3546
|
-
}
|
|
3547
|
-
}
|
|
3781
|
+
if (shouldFix("path_too_long") && pathViolations && pathViolations.length > 0) {
|
|
3782
|
+
const pathFix = await fixPathTooLong({ vault: input.vault });
|
|
3783
|
+
const pathFixed = pathFix.result.ok ? pathFix.result.data.fixed.map((f) => f.from) : [];
|
|
3784
|
+
if (pathFix.result.ok) unresolved.push(...pathFix.result.data.unresolved);
|
|
3785
|
+
else unresolved.push(...pathViolations.map((v) => v.relPath));
|
|
3548
3786
|
fixed.push(...pathFixed);
|
|
3549
3787
|
if (pathFixed.length > 0) {
|
|
3550
3788
|
const fixedSet = new Set(pathFixed);
|
|
@@ -3558,13 +3796,6 @@ ${newBody}`;
|
|
|
3558
3796
|
const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3559
3797
|
const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
|
|
3560
3798
|
if (input.only) {
|
|
3561
|
-
const allKnown = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
|
|
3562
|
-
if (!allKnown.includes(input.only)) {
|
|
3563
|
-
return {
|
|
3564
|
-
exitCode: ExitCode.USAGE,
|
|
3565
|
-
result: { ok: false, error: "UNKNOWN_BUCKET", detail: `Unknown bucket "${input.only}". Valid: ${allKnown.join(", ")}` }
|
|
3566
|
-
};
|
|
3567
|
-
}
|
|
3568
3799
|
const match = [...errorOut, ...warningOut, ...infoOut].filter((b) => b.kind === input.only);
|
|
3569
3800
|
const severity = ERROR_ORDER.includes(input.only) ? "error" : WARNING_ORDER.includes(input.only) ? "warning" : "info";
|
|
3570
3801
|
const filtered = severity === "error" ? { error: match, warning: [], info: [] } : severity === "warning" ? { error: [], warning: match, info: [] } : { error: [], warning: [], info: match };
|
|
@@ -3628,14 +3859,14 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
|
|
|
3628
3859
|
}
|
|
3629
3860
|
|
|
3630
3861
|
// src/commands/config.ts
|
|
3631
|
-
import { readFile as
|
|
3632
|
-
import { existsSync as
|
|
3633
|
-
import { join as
|
|
3862
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
3863
|
+
import { existsSync as existsSync6 } from "fs";
|
|
3864
|
+
import { join as join23 } from "path";
|
|
3634
3865
|
function validateKey(key) {
|
|
3635
3866
|
return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
|
|
3636
3867
|
}
|
|
3637
3868
|
function configPath(home) {
|
|
3638
|
-
return
|
|
3869
|
+
return join23(home, ".skillwiki", ".env");
|
|
3639
3870
|
}
|
|
3640
3871
|
async function runConfigGet(input) {
|
|
3641
3872
|
if (!validateKey(input.key)) {
|
|
@@ -3653,7 +3884,7 @@ async function runConfigSet(input) {
|
|
|
3653
3884
|
try {
|
|
3654
3885
|
let originalContent;
|
|
3655
3886
|
try {
|
|
3656
|
-
originalContent = await
|
|
3887
|
+
originalContent = await readFile18(filePath, "utf8");
|
|
3657
3888
|
} catch {
|
|
3658
3889
|
}
|
|
3659
3890
|
const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
|
|
@@ -3685,18 +3916,18 @@ async function runConfigList(input) {
|
|
|
3685
3916
|
}
|
|
3686
3917
|
async function runConfigPath(input) {
|
|
3687
3918
|
const filePath = configPath(input.home);
|
|
3688
|
-
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists:
|
|
3919
|
+
return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync6(filePath), humanHint: filePath }) };
|
|
3689
3920
|
}
|
|
3690
3921
|
|
|
3691
3922
|
// src/commands/doctor.ts
|
|
3692
|
-
import { existsSync as
|
|
3693
|
-
import { join as
|
|
3923
|
+
import { existsSync as existsSync9, lstatSync, readlinkSync, readdirSync, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
|
|
3924
|
+
import { join as join27, resolve as resolve5 } from "path";
|
|
3694
3925
|
import { execSync as execSync2 } from "child_process";
|
|
3695
3926
|
import { platform as platform2 } from "os";
|
|
3696
3927
|
|
|
3697
3928
|
// src/utils/auto-update.ts
|
|
3698
|
-
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as
|
|
3699
|
-
import { join as
|
|
3929
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
|
|
3930
|
+
import { join as join24, dirname as dirname9 } from "path";
|
|
3700
3931
|
import { spawn } from "child_process";
|
|
3701
3932
|
|
|
3702
3933
|
// src/utils/update-consts.ts
|
|
@@ -3707,7 +3938,7 @@ var CLI_DISABLE_FLAG = "--no-update-notifier";
|
|
|
3707
3938
|
|
|
3708
3939
|
// src/utils/auto-update.ts
|
|
3709
3940
|
function cachePath(home) {
|
|
3710
|
-
return
|
|
3941
|
+
return join24(home, ".skillwiki", CACHE_FILENAME);
|
|
3711
3942
|
}
|
|
3712
3943
|
function readCacheRaw(home) {
|
|
3713
3944
|
try {
|
|
@@ -3726,7 +3957,7 @@ function readCache(home) {
|
|
|
3726
3957
|
}
|
|
3727
3958
|
function writeCache(home, cache) {
|
|
3728
3959
|
const p = cachePath(home);
|
|
3729
|
-
mkdirSync3(
|
|
3960
|
+
mkdirSync3(dirname9(p), { recursive: true });
|
|
3730
3961
|
writeFileSync4(p, JSON.stringify(cache, null, 2));
|
|
3731
3962
|
}
|
|
3732
3963
|
function latestFromCache(home, currentVersion) {
|
|
@@ -3745,7 +3976,7 @@ function triggerAutoUpdate(home, currentVersion) {
|
|
|
3745
3976
|
const { isStale: isStale2 } = readCache(home);
|
|
3746
3977
|
if (!isStale2) return;
|
|
3747
3978
|
const bgScript = new URL("../auto-update-bg.js", import.meta.url).pathname;
|
|
3748
|
-
if (!
|
|
3979
|
+
if (!existsSync7(bgScript)) return;
|
|
3749
3980
|
const child = spawn(process.execPath, [bgScript, home, currentVersion], {
|
|
3750
3981
|
detached: true,
|
|
3751
3982
|
stdio: "ignore"
|
|
@@ -3757,12 +3988,12 @@ function triggerAutoUpdate(home, currentVersion) {
|
|
|
3757
3988
|
|
|
3758
3989
|
// src/utils/plugin-registry.ts
|
|
3759
3990
|
import { readFileSync as readFileSync5 } from "fs";
|
|
3760
|
-
import { join as
|
|
3761
|
-
var REGISTRY_PATH =
|
|
3991
|
+
import { join as join25 } from "path";
|
|
3992
|
+
var REGISTRY_PATH = join25(".claude", "plugins", "installed_plugins.json");
|
|
3762
3993
|
var PLUGIN_KEY = "skillwiki@llm-wiki";
|
|
3763
3994
|
function readInstalledPlugins(home) {
|
|
3764
3995
|
try {
|
|
3765
|
-
const raw = readFileSync5(
|
|
3996
|
+
const raw = readFileSync5(join25(home, REGISTRY_PATH), "utf8");
|
|
3766
3997
|
return JSON.parse(raw);
|
|
3767
3998
|
} catch {
|
|
3768
3999
|
return null;
|
|
@@ -3779,8 +4010,8 @@ function findPlugin(home, key = PLUGIN_KEY) {
|
|
|
3779
4010
|
// src/utils/s3-mount-health.ts
|
|
3780
4011
|
import { execSync } from "child_process";
|
|
3781
4012
|
import { platform } from "os";
|
|
3782
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as
|
|
3783
|
-
import { join as
|
|
4013
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as readFile19 } from "fs";
|
|
4014
|
+
import { join as join26 } from "path";
|
|
3784
4015
|
var OS = platform();
|
|
3785
4016
|
function findRcloneMountPid() {
|
|
3786
4017
|
try {
|
|
@@ -3938,7 +4169,7 @@ function detectFuseMount(vaultPath) {
|
|
|
3938
4169
|
return null;
|
|
3939
4170
|
}
|
|
3940
4171
|
function writeTest(dir) {
|
|
3941
|
-
const testFile =
|
|
4172
|
+
const testFile = join26(dir, `.doctor-write-test-${process.pid}.tmp`);
|
|
3942
4173
|
const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
|
|
3943
4174
|
const start = Date.now();
|
|
3944
4175
|
try {
|
|
@@ -3949,7 +4180,7 @@ function writeTest(dir) {
|
|
|
3949
4180
|
const writeMs = Date.now() - start;
|
|
3950
4181
|
const readStart = Date.now();
|
|
3951
4182
|
try {
|
|
3952
|
-
const back =
|
|
4183
|
+
const back = readFile19(testFile, "utf8");
|
|
3953
4184
|
const readMs = Date.now() - readStart;
|
|
3954
4185
|
if (back !== payload) {
|
|
3955
4186
|
try {
|
|
@@ -3971,6 +4202,37 @@ function writeTest(dir) {
|
|
|
3971
4202
|
}
|
|
3972
4203
|
return { success: true, writeMs, readMs: Date.now() - readStart, size: Buffer.byteLength(payload, "utf8") };
|
|
3973
4204
|
}
|
|
4205
|
+
var DURATION_UNIT_SECONDS = {
|
|
4206
|
+
ms: 1 / 1e3,
|
|
4207
|
+
s: 1,
|
|
4208
|
+
m: 60,
|
|
4209
|
+
h: 3600,
|
|
4210
|
+
d: 86400,
|
|
4211
|
+
w: 604800
|
|
4212
|
+
};
|
|
4213
|
+
function parseDurationSeconds(raw) {
|
|
4214
|
+
const input = raw.trim().toLowerCase();
|
|
4215
|
+
if (!input) return null;
|
|
4216
|
+
if (/^\d+(?:\.\d+)?$/.test(input)) {
|
|
4217
|
+
const num = parseFloat(input);
|
|
4218
|
+
return Number.isFinite(num) ? num : null;
|
|
4219
|
+
}
|
|
4220
|
+
const re = /(\d+(?:\.\d+)?)(ms|s|m|h|d|w)/g;
|
|
4221
|
+
let total = 0;
|
|
4222
|
+
let consumed = 0;
|
|
4223
|
+
for (const match of input.matchAll(re)) {
|
|
4224
|
+
const full = match[0];
|
|
4225
|
+
const value = parseFloat(match[1]);
|
|
4226
|
+
const unit = match[2];
|
|
4227
|
+
if (!Number.isFinite(value)) return null;
|
|
4228
|
+
const factor = DURATION_UNIT_SECONDS[unit];
|
|
4229
|
+
if (factor === void 0) return null;
|
|
4230
|
+
total += value * factor;
|
|
4231
|
+
consumed += full.length;
|
|
4232
|
+
}
|
|
4233
|
+
if (consumed !== input.length) return null;
|
|
4234
|
+
return total;
|
|
4235
|
+
}
|
|
3974
4236
|
var FLAG_THRESHOLDS = {
|
|
3975
4237
|
"--vfs-write-back": { min: 15, unit: "s", label: "VFS write-back window" },
|
|
3976
4238
|
"--vfs-write-wait": { min: 10, unit: "s", label: "VFS write-wait" },
|
|
@@ -3992,14 +4254,14 @@ function checkNodeVersion() {
|
|
|
3992
4254
|
function detectCliChannels(argv, home) {
|
|
3993
4255
|
const channels = [];
|
|
3994
4256
|
if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
|
|
3995
|
-
const devPath =
|
|
4257
|
+
const devPath = resolve5(argv[1]);
|
|
3996
4258
|
channels.push({ name: "dev", path: devPath, isDevLink: true });
|
|
3997
4259
|
}
|
|
3998
4260
|
try {
|
|
3999
4261
|
const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
|
|
4000
4262
|
if (whichOut) {
|
|
4001
4263
|
const isDev = isDevSymlink(whichOut);
|
|
4002
|
-
if (!channels.some((c) => c.path ===
|
|
4264
|
+
if (!channels.some((c) => c.path === resolve5(whichOut))) {
|
|
4003
4265
|
channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
|
|
4004
4266
|
}
|
|
4005
4267
|
}
|
|
@@ -4007,13 +4269,13 @@ function detectCliChannels(argv, home) {
|
|
|
4007
4269
|
}
|
|
4008
4270
|
const plugin = findPlugin(home);
|
|
4009
4271
|
if (plugin) {
|
|
4010
|
-
const pluginBin =
|
|
4011
|
-
if (
|
|
4272
|
+
const pluginBin = join27(plugin.installPath, "bin", "skillwiki");
|
|
4273
|
+
if (existsSync9(pluginBin)) {
|
|
4012
4274
|
channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
|
|
4013
4275
|
}
|
|
4014
4276
|
}
|
|
4015
|
-
const installBin =
|
|
4016
|
-
if (
|
|
4277
|
+
const installBin = join27(home, ".claude", "skills", "bin", "skillwiki");
|
|
4278
|
+
if (existsSync9(installBin)) {
|
|
4017
4279
|
channels.push({ name: "install", path: installBin, isDevLink: false });
|
|
4018
4280
|
}
|
|
4019
4281
|
return channels;
|
|
@@ -4022,7 +4284,7 @@ function isDevSymlink(binPath) {
|
|
|
4022
4284
|
try {
|
|
4023
4285
|
const st = lstatSync(binPath);
|
|
4024
4286
|
if (st.isSymbolicLink()) {
|
|
4025
|
-
const target =
|
|
4287
|
+
const target = resolve5(binPath, "..", readlinkSync(binPath));
|
|
4026
4288
|
return target.includes("packages/cli") || target.includes("packages\\cli");
|
|
4027
4289
|
}
|
|
4028
4290
|
} catch {
|
|
@@ -4065,7 +4327,7 @@ function checkCliChannels(argv, home) {
|
|
|
4065
4327
|
}
|
|
4066
4328
|
async function checkConfigFile(home) {
|
|
4067
4329
|
const cfgPath = configPath(home);
|
|
4068
|
-
if (!
|
|
4330
|
+
if (!existsSync9(cfgPath)) {
|
|
4069
4331
|
return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
|
|
4070
4332
|
}
|
|
4071
4333
|
try {
|
|
@@ -4080,7 +4342,7 @@ function checkWikiPathExists(resolvedPath) {
|
|
|
4080
4342
|
if (resolvedPath === void 0) {
|
|
4081
4343
|
return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4082
4344
|
}
|
|
4083
|
-
if (
|
|
4345
|
+
if (existsSync9(resolvedPath) && statSync3(resolvedPath).isDirectory()) {
|
|
4084
4346
|
return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
|
|
4085
4347
|
}
|
|
4086
4348
|
return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
|
|
@@ -4089,13 +4351,13 @@ function checkVaultStructure(resolvedPath) {
|
|
|
4089
4351
|
if (resolvedPath === void 0) {
|
|
4090
4352
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4091
4353
|
}
|
|
4092
|
-
if (!
|
|
4354
|
+
if (!existsSync9(resolvedPath)) {
|
|
4093
4355
|
return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
|
|
4094
4356
|
}
|
|
4095
4357
|
const missing = [];
|
|
4096
|
-
if (!
|
|
4358
|
+
if (!existsSync9(join27(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
|
|
4097
4359
|
for (const dir of ["raw", "entities", "concepts", "meta"]) {
|
|
4098
|
-
if (!
|
|
4360
|
+
if (!existsSync9(join27(resolvedPath, dir))) missing.push(dir + "/");
|
|
4099
4361
|
}
|
|
4100
4362
|
if (missing.length === 0) {
|
|
4101
4363
|
return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
|
|
@@ -4103,23 +4365,23 @@ function checkVaultStructure(resolvedPath) {
|
|
|
4103
4365
|
return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
|
|
4104
4366
|
}
|
|
4105
4367
|
function checkSkillsInstalled(home, cwd) {
|
|
4106
|
-
const srcDir = cwd ?
|
|
4107
|
-
if (srcDir &&
|
|
4108
|
-
const found =
|
|
4368
|
+
const srcDir = cwd ? join27(cwd, "packages", "skills") : void 0;
|
|
4369
|
+
if (srcDir && existsSync9(srcDir)) {
|
|
4370
|
+
const found = findInstalledSkillMd(srcDir);
|
|
4109
4371
|
if (found.length > 0) {
|
|
4110
4372
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
|
|
4111
4373
|
}
|
|
4112
4374
|
}
|
|
4113
4375
|
const plugin = findPlugin(home);
|
|
4114
4376
|
if (plugin) {
|
|
4115
|
-
const found =
|
|
4377
|
+
const found = findInstalledSkillMd(plugin.installPath);
|
|
4116
4378
|
if (found.length > 0) {
|
|
4117
4379
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
|
|
4118
4380
|
}
|
|
4119
4381
|
}
|
|
4120
|
-
const skillsDir =
|
|
4121
|
-
if (
|
|
4122
|
-
const found =
|
|
4382
|
+
const skillsDir = join27(home, ".claude", "skills");
|
|
4383
|
+
if (existsSync9(skillsDir)) {
|
|
4384
|
+
const found = findInstalledSkillMd(skillsDir);
|
|
4123
4385
|
if (found.length > 0) {
|
|
4124
4386
|
return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
|
|
4125
4387
|
}
|
|
@@ -4128,10 +4390,10 @@ function checkSkillsInstalled(home, cwd) {
|
|
|
4128
4390
|
}
|
|
4129
4391
|
function checkDuplicateSkills(home) {
|
|
4130
4392
|
const plugin = findPlugin(home);
|
|
4131
|
-
const skillsDir =
|
|
4393
|
+
const skillsDir = join27(home, ".claude", "skills");
|
|
4132
4394
|
const agentSkillDirs = [
|
|
4133
|
-
{ label: "~/.codex/skills/", path:
|
|
4134
|
-
{ label: "~/.agents/skills/", path:
|
|
4395
|
+
{ label: "~/.codex/skills/", path: join27(home, ".codex", "skills") },
|
|
4396
|
+
{ label: "~/.agents/skills/", path: join27(home, ".agents", "skills") }
|
|
4135
4397
|
];
|
|
4136
4398
|
if (!plugin) {
|
|
4137
4399
|
return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
|
|
@@ -4208,8 +4470,8 @@ async function checkProfiles(home) {
|
|
|
4208
4470
|
}
|
|
4209
4471
|
async function checkProjectLocalOverride(cwd) {
|
|
4210
4472
|
const dir = cwd ?? process.cwd();
|
|
4211
|
-
const envPath =
|
|
4212
|
-
if (
|
|
4473
|
+
const envPath = join27(dir, ".skillwiki", ".env");
|
|
4474
|
+
if (existsSync9(envPath)) {
|
|
4213
4475
|
return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
|
|
4214
4476
|
}
|
|
4215
4477
|
return check("pass", "project_local", "Project-local config", "None");
|
|
@@ -4218,7 +4480,7 @@ function checkVaultGitRemote(resolvedPath) {
|
|
|
4218
4480
|
if (resolvedPath === void 0) {
|
|
4219
4481
|
return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4220
4482
|
}
|
|
4221
|
-
if (!
|
|
4483
|
+
if (!existsSync9(join27(resolvedPath, ".git"))) {
|
|
4222
4484
|
return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
|
|
4223
4485
|
}
|
|
4224
4486
|
try {
|
|
@@ -4241,9 +4503,9 @@ function checkObsidianTemplates(resolvedPath) {
|
|
|
4241
4503
|
return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4242
4504
|
}
|
|
4243
4505
|
const missing = [];
|
|
4244
|
-
if (!
|
|
4245
|
-
if (!
|
|
4246
|
-
if (!
|
|
4506
|
+
if (!existsSync9(join27(resolvedPath, "_Templates"))) missing.push("_Templates/");
|
|
4507
|
+
if (!existsSync9(join27(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
|
|
4508
|
+
if (!existsSync9(join27(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
|
|
4247
4509
|
if (missing.length === 0) {
|
|
4248
4510
|
return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
|
|
4249
4511
|
}
|
|
@@ -4253,8 +4515,8 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
4253
4515
|
if (resolvedPath === void 0) {
|
|
4254
4516
|
return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4255
4517
|
}
|
|
4256
|
-
const rawDir =
|
|
4257
|
-
if (!
|
|
4518
|
+
const rawDir = join27(resolvedPath, "raw");
|
|
4519
|
+
if (!existsSync9(rawDir)) {
|
|
4258
4520
|
return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
|
|
4259
4521
|
}
|
|
4260
4522
|
const found = [];
|
|
@@ -4269,7 +4531,7 @@ function checkDotStoreClean(resolvedPath) {
|
|
|
4269
4531
|
if (entry.name === ".DS_Store") {
|
|
4270
4532
|
found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
|
|
4271
4533
|
} else if (entry.isDirectory()) {
|
|
4272
|
-
walk2(
|
|
4534
|
+
walk2(join27(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
|
|
4273
4535
|
}
|
|
4274
4536
|
}
|
|
4275
4537
|
})(rawDir, "");
|
|
@@ -4282,7 +4544,7 @@ function checkSyncLastPush(resolvedPath) {
|
|
|
4282
4544
|
if (resolvedPath === void 0) {
|
|
4283
4545
|
return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
|
|
4284
4546
|
}
|
|
4285
|
-
if (!
|
|
4547
|
+
if (!existsSync9(join27(resolvedPath, ".git"))) {
|
|
4286
4548
|
return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
|
|
4287
4549
|
}
|
|
4288
4550
|
let timestamp;
|
|
@@ -4323,8 +4585,8 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
4323
4585
|
return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
|
|
4324
4586
|
}
|
|
4325
4587
|
const mountPoint = fuse.mountPoint;
|
|
4326
|
-
const conceptsDir =
|
|
4327
|
-
if (!
|
|
4588
|
+
const conceptsDir = join27(resolvedPath, "concepts");
|
|
4589
|
+
if (!existsSync9(conceptsDir)) {
|
|
4328
4590
|
return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
|
|
4329
4591
|
}
|
|
4330
4592
|
const start = Date.now();
|
|
@@ -4363,6 +4625,73 @@ function checkS3MountPerf(resolvedPath) {
|
|
|
4363
4625
|
`S3 FUSE mount, cache warm (rg scan: ${elapsed.toFixed(3)}s)`
|
|
4364
4626
|
);
|
|
4365
4627
|
}
|
|
4628
|
+
var MAX_DIR_CACHE_TIME_SECONDS = 15 * 60;
|
|
4629
|
+
function formatDurationForHumans(seconds) {
|
|
4630
|
+
if (!Number.isFinite(seconds)) return `${seconds}s`;
|
|
4631
|
+
if (seconds >= 3600) return `${(seconds / 3600).toFixed(1)}h`;
|
|
4632
|
+
if (seconds >= 60) return `${(seconds / 60).toFixed(1)}m`;
|
|
4633
|
+
if (seconds >= 1) return `${seconds.toFixed(1)}s`;
|
|
4634
|
+
return `${Math.round(seconds * 1e3)}ms`;
|
|
4635
|
+
}
|
|
4636
|
+
function checkS3MountFreshness(resolvedPath) {
|
|
4637
|
+
if (!resolvedPath) {
|
|
4638
|
+
return check("pass", "s3_mount_freshness", "S3 visibility freshness", "No vault path \u2014 check skipped");
|
|
4639
|
+
}
|
|
4640
|
+
const fuse = detectFuseMount(resolvedPath);
|
|
4641
|
+
if (!fuse) {
|
|
4642
|
+
return check("pass", "s3_mount_freshness", "S3 visibility freshness", "local disk \u2014 check skipped");
|
|
4643
|
+
}
|
|
4644
|
+
const pid = findRcloneMountPid();
|
|
4645
|
+
if (pid === null) {
|
|
4646
|
+
return check(
|
|
4647
|
+
"warn",
|
|
4648
|
+
"s3_mount_freshness",
|
|
4649
|
+
"S3 visibility freshness",
|
|
4650
|
+
`S3 FUSE mount (${fuse.mountPoint}) but no rclone process found \u2014 cannot audit --dir-cache-time`
|
|
4651
|
+
);
|
|
4652
|
+
}
|
|
4653
|
+
const flags = parseRcloneFlags(pid);
|
|
4654
|
+
if (flags.size === 0) {
|
|
4655
|
+
return check(
|
|
4656
|
+
"warn",
|
|
4657
|
+
"s3_mount_freshness",
|
|
4658
|
+
"S3 visibility freshness",
|
|
4659
|
+
`rclone PID ${pid} found but could not parse flags`
|
|
4660
|
+
);
|
|
4661
|
+
}
|
|
4662
|
+
const raw = flags.get("--dir-cache-time");
|
|
4663
|
+
if (!raw) {
|
|
4664
|
+
return check(
|
|
4665
|
+
"pass",
|
|
4666
|
+
"s3_mount_freshness",
|
|
4667
|
+
"S3 visibility freshness",
|
|
4668
|
+
"PID " + pid + ": --dir-cache-time not set (rclone default 5m, within <=15m SLA)"
|
|
4669
|
+
);
|
|
4670
|
+
}
|
|
4671
|
+
const seconds = parseDurationSeconds(raw);
|
|
4672
|
+
if (seconds === null) {
|
|
4673
|
+
return check(
|
|
4674
|
+
"warn",
|
|
4675
|
+
"s3_mount_freshness",
|
|
4676
|
+
"S3 visibility freshness",
|
|
4677
|
+
`PID ${pid}: could not parse --dir-cache-time=${raw}`
|
|
4678
|
+
);
|
|
4679
|
+
}
|
|
4680
|
+
if (seconds > MAX_DIR_CACHE_TIME_SECONDS) {
|
|
4681
|
+
return check(
|
|
4682
|
+
"warn",
|
|
4683
|
+
"s3_mount_freshness",
|
|
4684
|
+
"S3 visibility freshness",
|
|
4685
|
+
`PID ${pid}: --dir-cache-time=${raw} (${formatDurationForHumans(seconds)}) exceeds 15m SLA \u2014 external S3 changes may remain invisible`
|
|
4686
|
+
);
|
|
4687
|
+
}
|
|
4688
|
+
return check(
|
|
4689
|
+
"pass",
|
|
4690
|
+
"s3_mount_freshness",
|
|
4691
|
+
"S3 visibility freshness",
|
|
4692
|
+
`PID ${pid}: --dir-cache-time=${raw} (${formatDurationForHumans(seconds)}), within <=15m SLA`
|
|
4693
|
+
);
|
|
4694
|
+
}
|
|
4366
4695
|
function checkRcloneFlagAudit(resolvedPath) {
|
|
4367
4696
|
if (!resolvedPath) {
|
|
4368
4697
|
return check("pass", "rclone_flags", "rclone VFS flags", "No vault path \u2014 check skipped");
|
|
@@ -4386,11 +4715,8 @@ function checkRcloneFlagAudit(resolvedPath) {
|
|
|
4386
4715
|
warnings.push(`${flag} not set (default may be unsafe)`);
|
|
4387
4716
|
continue;
|
|
4388
4717
|
}
|
|
4389
|
-
const
|
|
4390
|
-
if (
|
|
4391
|
-
let inSeconds = value;
|
|
4392
|
-
if (raw.endsWith("h")) inSeconds = value * 3600;
|
|
4393
|
-
else if (raw.endsWith("m")) inSeconds = value * 60;
|
|
4718
|
+
const inSeconds = parseDurationSeconds(raw);
|
|
4719
|
+
if (inSeconds === null) continue;
|
|
4394
4720
|
const thresholdSec = threshold.unit === "h" ? threshold.min * 3600 : threshold.unit === "m" ? threshold.min * 60 : threshold.min;
|
|
4395
4721
|
if (inSeconds < thresholdSec) {
|
|
4396
4722
|
warnings.push(`${flag}=${raw} (recommended \u2265${threshold.min}${threshold.unit})`);
|
|
@@ -4442,8 +4768,8 @@ function checkWriteTest(resolvedPath) {
|
|
|
4442
4768
|
if (!fuse) {
|
|
4443
4769
|
return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
|
|
4444
4770
|
}
|
|
4445
|
-
const conceptsDir =
|
|
4446
|
-
if (!
|
|
4771
|
+
const conceptsDir = join27(resolvedPath, "concepts");
|
|
4772
|
+
if (!existsSync9(conceptsDir)) {
|
|
4447
4773
|
return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
|
|
4448
4774
|
}
|
|
4449
4775
|
const result = writeTest(conceptsDir);
|
|
@@ -4529,7 +4855,7 @@ function checkVfsCacheHealth(resolvedPath) {
|
|
|
4529
4855
|
}
|
|
4530
4856
|
function readVaultSyncConfig(home) {
|
|
4531
4857
|
try {
|
|
4532
|
-
const content = readFileSync7(
|
|
4858
|
+
const content = readFileSync7(join27(home, ".skillwiki", ".env"), "utf8");
|
|
4533
4859
|
let installed = false;
|
|
4534
4860
|
let role;
|
|
4535
4861
|
for (const line of content.split(/\r?\n/)) {
|
|
@@ -4563,12 +4889,12 @@ function vaultSyncChecks(input) {
|
|
|
4563
4889
|
];
|
|
4564
4890
|
}
|
|
4565
4891
|
const isMac = os === "darwin";
|
|
4566
|
-
const logDir = input.logDir ?? (isMac ?
|
|
4567
|
-
const shareDir = input.shareDir ?? (isMac ?
|
|
4568
|
-
const filterPath = input.filterPath ??
|
|
4892
|
+
const logDir = input.logDir ?? (isMac ? join27(home, "Library", "Logs") : join27(home, ".local", "state", "vault-sync", "log"));
|
|
4893
|
+
const shareDir = input.shareDir ?? (isMac ? join27(home, "Library", "Application Support", "vault-sync", "bin") : join27(home, ".local", "share", "vault-sync", "bin"));
|
|
4894
|
+
const filterPath = input.filterPath ?? join27(home, ".config", "rclone", "wiki-push-filters.txt");
|
|
4569
4895
|
const snapshotPath = input.snapshotScriptPath ?? "/root/.hermes/scripts/wiki-snapshot-v3.sh";
|
|
4570
|
-
const pushScriptPath =
|
|
4571
|
-
const c1 =
|
|
4896
|
+
const pushScriptPath = join27(shareDir, "wiki-push.sh");
|
|
4897
|
+
const c1 = existsSync9(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
|
|
4572
4898
|
let c2;
|
|
4573
4899
|
try {
|
|
4574
4900
|
if (isMac) {
|
|
@@ -4619,7 +4945,7 @@ function vaultSyncChecks(input) {
|
|
|
4619
4945
|
"Scheduler check failed \u2014 run vault-sync-install"
|
|
4620
4946
|
);
|
|
4621
4947
|
}
|
|
4622
|
-
const logFile =
|
|
4948
|
+
const logFile = join27(logDir, "wiki-push.log");
|
|
4623
4949
|
let c3;
|
|
4624
4950
|
try {
|
|
4625
4951
|
const logContent = readFileSync7(logFile, "utf8");
|
|
@@ -4678,7 +5004,7 @@ function vaultSyncChecks(input) {
|
|
|
4678
5004
|
}
|
|
4679
5005
|
}
|
|
4680
5006
|
} catch {
|
|
4681
|
-
c3 =
|
|
5007
|
+
c3 = existsSync9(logDir) ? check(
|
|
4682
5008
|
"warn",
|
|
4683
5009
|
"vault_sync_last_push_age",
|
|
4684
5010
|
"Vault sync last push recency",
|
|
@@ -4690,7 +5016,7 @@ function vaultSyncChecks(input) {
|
|
|
4690
5016
|
`Log directory not found at ${logDir}`
|
|
4691
5017
|
);
|
|
4692
5018
|
}
|
|
4693
|
-
const fetchLogFile =
|
|
5019
|
+
const fetchLogFile = join27(logDir, "wiki-fetch.log");
|
|
4694
5020
|
let cFetch;
|
|
4695
5021
|
try {
|
|
4696
5022
|
const logContent = readFileSync7(fetchLogFile, "utf8");
|
|
@@ -4737,7 +5063,7 @@ function vaultSyncChecks(input) {
|
|
|
4737
5063
|
}
|
|
4738
5064
|
let c4;
|
|
4739
5065
|
try {
|
|
4740
|
-
if (!
|
|
5066
|
+
if (!existsSync9(filterPath)) {
|
|
4741
5067
|
c4 = check(
|
|
4742
5068
|
"error",
|
|
4743
5069
|
"vault_sync_filter_present",
|
|
@@ -4786,7 +5112,7 @@ function vaultSyncChecks(input) {
|
|
|
4786
5112
|
);
|
|
4787
5113
|
} else {
|
|
4788
5114
|
try {
|
|
4789
|
-
if (!
|
|
5115
|
+
if (!existsSync9(snapshotPath)) {
|
|
4790
5116
|
c5 = check(
|
|
4791
5117
|
"error",
|
|
4792
5118
|
"vault_sync_snapshot_guard",
|
|
@@ -4832,13 +5158,17 @@ function findSkillMd(dir) {
|
|
|
4832
5158
|
}
|
|
4833
5159
|
for (const entry of entries) {
|
|
4834
5160
|
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
4835
|
-
results.push(
|
|
5161
|
+
results.push(join27(dir, entry.name));
|
|
4836
5162
|
} else if (entry.isDirectory()) {
|
|
4837
|
-
results.push(...findSkillMd(
|
|
5163
|
+
results.push(...findSkillMd(join27(dir, entry.name)));
|
|
4838
5164
|
}
|
|
4839
5165
|
}
|
|
4840
5166
|
return results;
|
|
4841
5167
|
}
|
|
5168
|
+
function findInstalledSkillMd(dir) {
|
|
5169
|
+
const directSkills = findSkillNames(dir).map((name) => join27(dir, name, "SKILL.md"));
|
|
5170
|
+
return directSkills.length > 0 ? directSkills : findSkillMd(dir);
|
|
5171
|
+
}
|
|
4842
5172
|
function findSkillNames(dir) {
|
|
4843
5173
|
const results = [];
|
|
4844
5174
|
let entries;
|
|
@@ -4848,12 +5178,61 @@ function findSkillNames(dir) {
|
|
|
4848
5178
|
return results;
|
|
4849
5179
|
}
|
|
4850
5180
|
for (const entry of entries) {
|
|
4851
|
-
if (entry.isDirectory() &&
|
|
5181
|
+
if (entry.isDirectory() && existsSync9(join27(dir, entry.name, "SKILL.md"))) {
|
|
4852
5182
|
results.push(entry.name);
|
|
4853
5183
|
}
|
|
4854
5184
|
}
|
|
4855
5185
|
return results;
|
|
4856
5186
|
}
|
|
5187
|
+
var METRIC_TYPES = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
5188
|
+
async function vaultMetrics(resolvedPath) {
|
|
5189
|
+
const ids = [
|
|
5190
|
+
["vault_metric_pages", "Vault pages by type"],
|
|
5191
|
+
["vault_metric_orphans", "Vault orphan rate"],
|
|
5192
|
+
["vault_metric_bridges", "Vault bridge count"],
|
|
5193
|
+
["vault_metric_cohesion", "Mean community cohesion"],
|
|
5194
|
+
["vault_metric_log_size", "Vault log size"]
|
|
5195
|
+
];
|
|
5196
|
+
const noVault = () => ids.map(([id, label]) => check("info", id, label, "no vault configured"));
|
|
5197
|
+
if (!resolvedPath) return noVault();
|
|
5198
|
+
const scan = await scanVault(resolvedPath);
|
|
5199
|
+
if (!scan.ok) return noVault();
|
|
5200
|
+
const tk = scan.data.typedKnowledge;
|
|
5201
|
+
const perType = METRIC_TYPES.map((d) => `${d} ${tk.filter((p) => p.relPath.startsWith(d + "/")).length}`).join(", ");
|
|
5202
|
+
const adj = await buildWikilinkAdjacency(tk);
|
|
5203
|
+
const g = toUndirectedWeighted(adj);
|
|
5204
|
+
const nodes = [...g.keys()];
|
|
5205
|
+
const total = nodes.length;
|
|
5206
|
+
const orphanCount = nodes.filter((n) => g.get(n).size === 0).length;
|
|
5207
|
+
const orphanRate = total > 0 ? Math.round(orphanCount / total * 1e3) / 10 : 0;
|
|
5208
|
+
const comm = louvain(g);
|
|
5209
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5210
|
+
for (const [node, c] of comm) {
|
|
5211
|
+
const arr = groups.get(c);
|
|
5212
|
+
if (arr) arr.push(node);
|
|
5213
|
+
else groups.set(c, [node]);
|
|
5214
|
+
}
|
|
5215
|
+
const cohesions = [...groups.values()].filter((m) => m.length >= 2).map((m) => communityCohesion(m, g));
|
|
5216
|
+
const meanCohesion = cohesions.length > 0 ? Math.round(cohesions.reduce((a, b) => a + b, 0) / cohesions.length * 1e3) / 1e3 : 0;
|
|
5217
|
+
let bridges = 0;
|
|
5218
|
+
for (const n of nodes) {
|
|
5219
|
+
const nbrComms = /* @__PURE__ */ new Set();
|
|
5220
|
+
for (const nb of g.get(n).keys()) nbrComms.add(comm.get(nb));
|
|
5221
|
+
if (nbrComms.size >= 3) bridges++;
|
|
5222
|
+
}
|
|
5223
|
+
let logLines = 0;
|
|
5224
|
+
try {
|
|
5225
|
+
logLines = readFileSync7(join27(resolvedPath, "log.md"), "utf8").split("\n").length;
|
|
5226
|
+
} catch {
|
|
5227
|
+
}
|
|
5228
|
+
return [
|
|
5229
|
+
check("info", "vault_metric_pages", "Vault pages by type", `${total} typed (${perType})`),
|
|
5230
|
+
check("info", "vault_metric_orphans", "Vault orphan rate", `${orphanRate}% (${orphanCount}/${total} degree-0)`),
|
|
5231
|
+
check("info", "vault_metric_bridges", "Vault bridge count", `${bridges} page(s) link >= 3 communities`),
|
|
5232
|
+
check("info", "vault_metric_cohesion", "Mean community cohesion", `${meanCohesion} across ${cohesions.length} communities (size >= 2)`),
|
|
5233
|
+
check("info", "vault_metric_log_size", "Vault log size", `${logLines} lines`)
|
|
5234
|
+
];
|
|
5235
|
+
}
|
|
4857
5236
|
async function runDoctor(input) {
|
|
4858
5237
|
const checks = [];
|
|
4859
5238
|
const vsConfig = readVaultSyncConfig(input.home);
|
|
@@ -4876,6 +5255,7 @@ async function runDoctor(input) {
|
|
|
4876
5255
|
checks.push(checkSyncLastPush(resolvedPath));
|
|
4877
5256
|
checks.push(checkDotStoreClean(resolvedPath));
|
|
4878
5257
|
checks.push(checkS3MountPerf(resolvedPath));
|
|
5258
|
+
checks.push(checkS3MountFreshness(resolvedPath));
|
|
4879
5259
|
checks.push(checkRcloneFlagAudit(resolvedPath));
|
|
4880
5260
|
checks.push(checkRcloneVersion(resolvedPath, vsConfig.installed));
|
|
4881
5261
|
checks.push(checkWriteTest(resolvedPath));
|
|
@@ -4889,6 +5269,7 @@ async function runDoctor(input) {
|
|
|
4889
5269
|
vaultSyncInstalled: vsConfig.installed,
|
|
4890
5270
|
vaultSyncRole: vsConfig.role
|
|
4891
5271
|
}));
|
|
5272
|
+
checks.push(...await vaultMetrics(resolvedPath));
|
|
4892
5273
|
const summary = {
|
|
4893
5274
|
pass: checks.filter((c) => c.status === "pass").length,
|
|
4894
5275
|
info: checks.filter((c) => c.status === "info").length,
|
|
@@ -4912,8 +5293,8 @@ async function runDoctor(input) {
|
|
|
4912
5293
|
}
|
|
4913
5294
|
|
|
4914
5295
|
// src/commands/archive.ts
|
|
4915
|
-
import { rename as rename7, mkdir as
|
|
4916
|
-
import { join as
|
|
5296
|
+
import { rename as rename7, mkdir as mkdir9, readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
|
|
5297
|
+
import { join as join28, dirname as dirname10 } from "path";
|
|
4917
5298
|
function countWikilinks(body, slug) {
|
|
4918
5299
|
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4919
5300
|
const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
|
|
@@ -4941,7 +5322,7 @@ async function runArchive(input) {
|
|
|
4941
5322
|
if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
|
|
4942
5323
|
if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
|
|
4943
5324
|
const slug = relPath.replace(/\.md$/, "").split("/").pop();
|
|
4944
|
-
const archivePath =
|
|
5325
|
+
const archivePath = join28("_archive", relPath).replace(/\\/g, "/");
|
|
4945
5326
|
let cascade;
|
|
4946
5327
|
if (input.cascade) {
|
|
4947
5328
|
const wikilinkRefs = [];
|
|
@@ -4965,7 +5346,7 @@ async function runArchive(input) {
|
|
|
4965
5346
|
const indexRefs = [];
|
|
4966
5347
|
if (!isRaw) {
|
|
4967
5348
|
try {
|
|
4968
|
-
const idx = await
|
|
5349
|
+
const idx = await readFile20(join28(input.vault, "index.md"), "utf8");
|
|
4969
5350
|
idx.split("\n").forEach((line, i) => {
|
|
4970
5351
|
if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
|
|
4971
5352
|
});
|
|
@@ -4991,8 +5372,8 @@ async function runArchive(input) {
|
|
|
4991
5372
|
}
|
|
4992
5373
|
if (input.cascade && input.apply && cascade) {
|
|
4993
5374
|
for (const ref of cascade.source_array_refs) {
|
|
4994
|
-
const absPath =
|
|
4995
|
-
const text = await
|
|
5375
|
+
const absPath = join28(input.vault, ref.page);
|
|
5376
|
+
const text = await readFile20(absPath, "utf8");
|
|
4996
5377
|
const split = splitFrontmatter(text);
|
|
4997
5378
|
if (!split.ok) continue;
|
|
4998
5379
|
const before = split.data.rawFrontmatter;
|
|
@@ -5009,12 +5390,12 @@ ${fmRewritten}
|
|
|
5009
5390
|
}
|
|
5010
5391
|
}
|
|
5011
5392
|
}
|
|
5012
|
-
await
|
|
5393
|
+
await mkdir9(dirname10(join28(input.vault, archivePath)), { recursive: true });
|
|
5013
5394
|
let indexUpdated = false;
|
|
5014
5395
|
if (!isRaw) {
|
|
5015
|
-
const indexPath =
|
|
5396
|
+
const indexPath = join28(input.vault, "index.md");
|
|
5016
5397
|
try {
|
|
5017
|
-
const idx = await
|
|
5398
|
+
const idx = await readFile20(indexPath, "utf8");
|
|
5018
5399
|
const originalLines = idx.split("\n");
|
|
5019
5400
|
const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
|
|
5020
5401
|
if (filtered.length !== originalLines.length) {
|
|
@@ -5025,7 +5406,7 @@ ${fmRewritten}
|
|
|
5025
5406
|
if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
|
|
5026
5407
|
}
|
|
5027
5408
|
}
|
|
5028
|
-
await rename7(
|
|
5409
|
+
await rename7(join28(input.vault, relPath), join28(input.vault, archivePath));
|
|
5029
5410
|
appendLastOp(input.vault, {
|
|
5030
5411
|
operation: input.cascade ? "archive-cascade" : "archive",
|
|
5031
5412
|
summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
|
|
@@ -5412,14 +5793,14 @@ ${newBody}`;
|
|
|
5412
5793
|
// src/commands/update.ts
|
|
5413
5794
|
import { execSync as execSync3 } from "child_process";
|
|
5414
5795
|
import { readFileSync as readFileSync8 } from "fs";
|
|
5415
|
-
import { join as
|
|
5796
|
+
import { join as join29 } from "path";
|
|
5416
5797
|
function resolveGlobalSkillsRoot() {
|
|
5417
5798
|
try {
|
|
5418
5799
|
const globalRoot = execSync3("npm root -g", {
|
|
5419
5800
|
encoding: "utf8",
|
|
5420
5801
|
timeout: 5e3
|
|
5421
5802
|
}).trim();
|
|
5422
|
-
return
|
|
5803
|
+
return join29(globalRoot, "skillwiki", "skills");
|
|
5423
5804
|
} catch {
|
|
5424
5805
|
return null;
|
|
5425
5806
|
}
|
|
@@ -5445,7 +5826,7 @@ async function runUpdate(input) {
|
|
|
5445
5826
|
);
|
|
5446
5827
|
const currentVersion = pkg2.version;
|
|
5447
5828
|
const tag = input.distTag ?? "latest";
|
|
5448
|
-
const target =
|
|
5829
|
+
const target = join29(input.home, ".claude", "skills");
|
|
5449
5830
|
let latest;
|
|
5450
5831
|
try {
|
|
5451
5832
|
latest = execSync3(`npm view skillwiki@${tag} version`, {
|
|
@@ -5515,16 +5896,16 @@ async function runUpdate(input) {
|
|
|
5515
5896
|
|
|
5516
5897
|
// src/commands/self-update.ts
|
|
5517
5898
|
import { execSync as execSync4 } from "child_process";
|
|
5518
|
-
import { existsSync as
|
|
5519
|
-
import { join as
|
|
5899
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
|
|
5900
|
+
import { join as join30 } from "path";
|
|
5520
5901
|
var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
|
|
5521
5902
|
async function runSelfUpdate(input) {
|
|
5522
5903
|
const currentVersion = JSON.parse(
|
|
5523
5904
|
readFileSync9(new URL("../../package.json", import.meta.url), "utf8")
|
|
5524
5905
|
).version;
|
|
5525
5906
|
const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
|
|
5526
|
-
const localPkgPath =
|
|
5527
|
-
const hasLocalSource =
|
|
5907
|
+
const localPkgPath = join30(sourceRoot, "packages", "cli", "package.json");
|
|
5908
|
+
const hasLocalSource = existsSync10(localPkgPath);
|
|
5528
5909
|
if (input.check) {
|
|
5529
5910
|
let availableVersion = null;
|
|
5530
5911
|
let source;
|
|
@@ -5655,10 +6036,10 @@ async function runSelfUpdate(input) {
|
|
|
5655
6036
|
}
|
|
5656
6037
|
|
|
5657
6038
|
// src/commands/transcripts.ts
|
|
5658
|
-
import { readdir as readdir5, stat as stat7, readFile as
|
|
5659
|
-
import { join as
|
|
6039
|
+
import { readdir as readdir5, stat as stat7, readFile as readFile21 } from "fs/promises";
|
|
6040
|
+
import { join as join31 } from "path";
|
|
5660
6041
|
async function runTranscripts(input) {
|
|
5661
|
-
const dir =
|
|
6042
|
+
const dir = join31(input.vault, "raw", "transcripts");
|
|
5662
6043
|
let entries;
|
|
5663
6044
|
try {
|
|
5664
6045
|
entries = await readdir5(dir, { withFileTypes: true });
|
|
@@ -5668,8 +6049,8 @@ async function runTranscripts(input) {
|
|
|
5668
6049
|
const transcripts = [];
|
|
5669
6050
|
for (const entry of entries) {
|
|
5670
6051
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
5671
|
-
const filePath =
|
|
5672
|
-
const content = await
|
|
6052
|
+
const filePath = join31(dir, entry.name);
|
|
6053
|
+
const content = await readFile21(filePath, "utf8");
|
|
5673
6054
|
const fm = extractFrontmatter(content);
|
|
5674
6055
|
if (!fm.ok) continue;
|
|
5675
6056
|
const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
|
|
@@ -5686,12 +6067,12 @@ async function runTranscripts(input) {
|
|
|
5686
6067
|
}
|
|
5687
6068
|
|
|
5688
6069
|
// src/commands/project-index.ts
|
|
5689
|
-
import { readdir as readdir6, readFile as
|
|
5690
|
-
import { join as
|
|
6070
|
+
import { readdir as readdir6, readFile as readFile22, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
|
|
6071
|
+
import { join as join32, dirname as dirname11 } from "path";
|
|
5691
6072
|
var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
|
|
5692
6073
|
async function runProjectIndex(input) {
|
|
5693
6074
|
const slug = input.slug;
|
|
5694
|
-
const projectDir =
|
|
6075
|
+
const projectDir = join32(input.vault, "projects", slug);
|
|
5695
6076
|
try {
|
|
5696
6077
|
await readdir6(projectDir);
|
|
5697
6078
|
} catch {
|
|
@@ -5702,15 +6083,15 @@ async function runProjectIndex(input) {
|
|
|
5702
6083
|
}
|
|
5703
6084
|
const wikilinkPattern = `[[${slug}]]`;
|
|
5704
6085
|
const entries = [];
|
|
5705
|
-
const compoundDir =
|
|
6086
|
+
const compoundDir = join32(input.vault, "projects", slug, "compound");
|
|
5706
6087
|
try {
|
|
5707
6088
|
const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
|
|
5708
6089
|
for (const entry of compoundFiles) {
|
|
5709
6090
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
5710
|
-
const filePath =
|
|
6091
|
+
const filePath = join32(compoundDir, entry.name);
|
|
5711
6092
|
let text;
|
|
5712
6093
|
try {
|
|
5713
|
-
text = await
|
|
6094
|
+
text = await readFile22(filePath, "utf8");
|
|
5714
6095
|
} catch {
|
|
5715
6096
|
continue;
|
|
5716
6097
|
}
|
|
@@ -5727,16 +6108,16 @@ async function runProjectIndex(input) {
|
|
|
5727
6108
|
for (const dir of LAYER2_DIRS) {
|
|
5728
6109
|
let files;
|
|
5729
6110
|
try {
|
|
5730
|
-
files = await readdir6(
|
|
6111
|
+
files = await readdir6(join32(input.vault, dir), { withFileTypes: true });
|
|
5731
6112
|
} catch {
|
|
5732
6113
|
continue;
|
|
5733
6114
|
}
|
|
5734
6115
|
for (const entry of files) {
|
|
5735
6116
|
if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
|
|
5736
|
-
const filePath =
|
|
6117
|
+
const filePath = join32(input.vault, dir, entry.name);
|
|
5737
6118
|
let text;
|
|
5738
6119
|
try {
|
|
5739
|
-
text = await
|
|
6120
|
+
text = await readFile22(filePath, "utf8");
|
|
5740
6121
|
} catch {
|
|
5741
6122
|
continue;
|
|
5742
6123
|
}
|
|
@@ -5757,11 +6138,11 @@ async function runProjectIndex(input) {
|
|
|
5757
6138
|
const tb = typeOrder[b.type] ?? 99;
|
|
5758
6139
|
return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
|
|
5759
6140
|
});
|
|
5760
|
-
const indexPath =
|
|
6141
|
+
const indexPath = join32(projectDir, "knowledge.md");
|
|
5761
6142
|
let existing = false;
|
|
5762
6143
|
let stale = false;
|
|
5763
6144
|
try {
|
|
5764
|
-
const existingText = await
|
|
6145
|
+
const existingText = await readFile22(indexPath, "utf8");
|
|
5765
6146
|
existing = true;
|
|
5766
6147
|
const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
|
|
5767
6148
|
const existingPages = new Set(existingEntries.map((l) => {
|
|
@@ -5801,7 +6182,7 @@ Autogenerated by \`skillwiki project-index\` on ${today}.
|
|
|
5801
6182
|
}
|
|
5802
6183
|
if (input.apply) {
|
|
5803
6184
|
try {
|
|
5804
|
-
await
|
|
6185
|
+
await mkdir10(dirname11(indexPath), { recursive: true });
|
|
5805
6186
|
await writeFile11(indexPath, body, "utf8");
|
|
5806
6187
|
} catch (e) {
|
|
5807
6188
|
return {
|
|
@@ -5830,10 +6211,10 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
|
|
|
5830
6211
|
}
|
|
5831
6212
|
|
|
5832
6213
|
// src/commands/compound.ts
|
|
5833
|
-
import { writeFile as writeFile12, mkdir as
|
|
5834
|
-
import { join as
|
|
5835
|
-
import { existsSync as
|
|
5836
|
-
import { readFile as
|
|
6214
|
+
import { writeFile as writeFile12, mkdir as mkdir11, readdir as readdir7, unlink as unlink4 } from "fs/promises";
|
|
6215
|
+
import { join as join33 } from "path";
|
|
6216
|
+
import { existsSync as existsSync11 } from "fs";
|
|
6217
|
+
import { readFile as readFile23 } from "fs/promises";
|
|
5837
6218
|
var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
|
|
5838
6219
|
var FIELD_RE = {
|
|
5839
6220
|
improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
|
|
@@ -5931,17 +6312,17 @@ function extractRetroFields(date, cycleName, block) {
|
|
|
5931
6312
|
};
|
|
5932
6313
|
}
|
|
5933
6314
|
async function runCompound(input) {
|
|
5934
|
-
const logPath =
|
|
6315
|
+
const logPath = join33(input.vault, "log.md");
|
|
5935
6316
|
let logText;
|
|
5936
6317
|
try {
|
|
5937
|
-
logText = await
|
|
6318
|
+
logText = await readFile23(logPath, "utf8");
|
|
5938
6319
|
} catch {
|
|
5939
6320
|
return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
|
|
5940
6321
|
}
|
|
5941
6322
|
const entries = parseRetroEntries(logText);
|
|
5942
6323
|
const promoted = [];
|
|
5943
6324
|
const skipped = [];
|
|
5944
|
-
const compoundDir =
|
|
6325
|
+
const compoundDir = join33(input.vault, "projects", input.project, "compound");
|
|
5945
6326
|
for (const entry of entries) {
|
|
5946
6327
|
const generalizeValue = entry.generalize.trim();
|
|
5947
6328
|
if (!/^yes/i.test(generalizeValue)) {
|
|
@@ -5949,8 +6330,8 @@ async function runCompound(input) {
|
|
|
5949
6330
|
continue;
|
|
5950
6331
|
}
|
|
5951
6332
|
const slug = slugify(entry.cycleName);
|
|
5952
|
-
const compoundPath =
|
|
5953
|
-
if (
|
|
6333
|
+
const compoundPath = join33(compoundDir, `${slug}.md`);
|
|
6334
|
+
if (existsSync11(compoundPath)) {
|
|
5954
6335
|
skipped.push(entry.date);
|
|
5955
6336
|
continue;
|
|
5956
6337
|
}
|
|
@@ -5988,8 +6369,8 @@ async function runCompound(input) {
|
|
|
5988
6369
|
].join("\n");
|
|
5989
6370
|
const content = frontmatter + "\n" + body;
|
|
5990
6371
|
if (!input.dryRun) {
|
|
5991
|
-
if (!
|
|
5992
|
-
await
|
|
6372
|
+
if (!existsSync11(compoundDir)) {
|
|
6373
|
+
await mkdir11(compoundDir, { recursive: true });
|
|
5993
6374
|
}
|
|
5994
6375
|
await writeFile12(compoundPath, content, "utf8");
|
|
5995
6376
|
}
|
|
@@ -6010,23 +6391,23 @@ async function runCompound(input) {
|
|
|
6010
6391
|
};
|
|
6011
6392
|
}
|
|
6012
6393
|
async function runCompoundDelete(input) {
|
|
6013
|
-
const projectDir =
|
|
6014
|
-
if (!
|
|
6394
|
+
const projectDir = join33(input.vault, "projects", input.project);
|
|
6395
|
+
if (!existsSync11(projectDir)) {
|
|
6015
6396
|
return {
|
|
6016
6397
|
exitCode: ExitCode.PROJECT_NOT_FOUND,
|
|
6017
6398
|
result: err("PROJECT_NOT_FOUND", { slug: input.project, path: projectDir })
|
|
6018
6399
|
};
|
|
6019
6400
|
}
|
|
6020
6401
|
const entryName = input.entry.replace(/\.md$/, "");
|
|
6021
|
-
const compoundPath =
|
|
6022
|
-
if (!
|
|
6402
|
+
const compoundPath = join33(projectDir, "compound", `${entryName}.md`);
|
|
6403
|
+
if (!existsSync11(compoundPath)) {
|
|
6023
6404
|
return {
|
|
6024
6405
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
6025
6406
|
result: err("FILE_NOT_FOUND", { path: compoundPath })
|
|
6026
6407
|
};
|
|
6027
6408
|
}
|
|
6028
6409
|
try {
|
|
6029
|
-
await
|
|
6410
|
+
await unlink4(compoundPath);
|
|
6030
6411
|
} catch (e) {
|
|
6031
6412
|
return {
|
|
6032
6413
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -6052,8 +6433,8 @@ knowledge.md regenerated`
|
|
|
6052
6433
|
};
|
|
6053
6434
|
}
|
|
6054
6435
|
async function runCompoundList(input) {
|
|
6055
|
-
const compoundDir =
|
|
6056
|
-
if (!
|
|
6436
|
+
const compoundDir = join33(input.vault, "projects", input.project, "compound");
|
|
6437
|
+
if (!existsSync11(compoundDir)) {
|
|
6057
6438
|
return {
|
|
6058
6439
|
exitCode: ExitCode.OK,
|
|
6059
6440
|
result: ok({
|
|
@@ -6083,10 +6464,10 @@ could not read compound directory`
|
|
|
6083
6464
|
const entries = [];
|
|
6084
6465
|
for (const dirent of dirents) {
|
|
6085
6466
|
if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
|
|
6086
|
-
const filePath =
|
|
6467
|
+
const filePath = join33(compoundDir, dirent.name);
|
|
6087
6468
|
let text;
|
|
6088
6469
|
try {
|
|
6089
|
-
text = await
|
|
6470
|
+
text = await readFile23(filePath, "utf8");
|
|
6090
6471
|
} catch {
|
|
6091
6472
|
continue;
|
|
6092
6473
|
}
|
|
@@ -6115,9 +6496,9 @@ no compound entries found`;
|
|
|
6115
6496
|
}
|
|
6116
6497
|
|
|
6117
6498
|
// src/commands/observe.ts
|
|
6118
|
-
import { mkdir as
|
|
6119
|
-
import { existsSync as
|
|
6120
|
-
import { join as
|
|
6499
|
+
import { mkdir as mkdir12, writeFile as writeFile13 } from "fs/promises";
|
|
6500
|
+
import { existsSync as existsSync12, statSync as statSync4 } from "fs";
|
|
6501
|
+
import { join as join34 } from "path";
|
|
6121
6502
|
import { createHash as createHash4 } from "crypto";
|
|
6122
6503
|
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
|
|
6123
6504
|
function slugify2(text) {
|
|
@@ -6140,15 +6521,15 @@ async function runObserve(input) {
|
|
|
6140
6521
|
result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
|
|
6141
6522
|
};
|
|
6142
6523
|
}
|
|
6143
|
-
if (!
|
|
6524
|
+
if (!existsSync12(input.vault) || !statSync4(input.vault).isDirectory()) {
|
|
6144
6525
|
return {
|
|
6145
6526
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6146
6527
|
result: err("VAULT_PATH_INVALID", { path: input.vault })
|
|
6147
6528
|
};
|
|
6148
6529
|
}
|
|
6149
|
-
const transcriptsDir =
|
|
6530
|
+
const transcriptsDir = join34(input.vault, "raw", "transcripts");
|
|
6150
6531
|
try {
|
|
6151
|
-
await
|
|
6532
|
+
await mkdir12(transcriptsDir, { recursive: true });
|
|
6152
6533
|
} catch {
|
|
6153
6534
|
return {
|
|
6154
6535
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
@@ -6158,7 +6539,7 @@ async function runObserve(input) {
|
|
|
6158
6539
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6159
6540
|
const slug = slugify2(input.text);
|
|
6160
6541
|
const fileName = `${today}-observation-${slug}.md`;
|
|
6161
|
-
const filePath =
|
|
6542
|
+
const filePath = join34(transcriptsDir, fileName);
|
|
6162
6543
|
const body = `
|
|
6163
6544
|
${input.text.trim()}
|
|
6164
6545
|
`;
|
|
@@ -6198,8 +6579,8 @@ ${input.text.trim()}
|
|
|
6198
6579
|
}
|
|
6199
6580
|
|
|
6200
6581
|
// src/commands/ingest.ts
|
|
6201
|
-
import { readFile as
|
|
6202
|
-
import { join as
|
|
6582
|
+
import { readFile as readFile24, writeFile as writeFile14, mkdir as mkdir13 } from "fs/promises";
|
|
6583
|
+
import { join as join35 } from "path";
|
|
6203
6584
|
import { createHash as createHash5 } from "crypto";
|
|
6204
6585
|
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
|
|
6205
6586
|
var TYPE_DIR = {
|
|
@@ -6358,7 +6739,7 @@ async function runIngest(input) {
|
|
|
6358
6739
|
sourceContent = fetchResult.data.body;
|
|
6359
6740
|
} else {
|
|
6360
6741
|
try {
|
|
6361
|
-
sourceContent = await
|
|
6742
|
+
sourceContent = await readFile24(input.source, "utf8");
|
|
6362
6743
|
} catch {
|
|
6363
6744
|
return {
|
|
6364
6745
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -6373,8 +6754,8 @@ async function runIngest(input) {
|
|
|
6373
6754
|
const rawRelPath = `raw/articles/${slug}.md`;
|
|
6374
6755
|
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
6375
6756
|
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
6376
|
-
const rawAbsPath =
|
|
6377
|
-
const typedAbsPath =
|
|
6757
|
+
const rawAbsPath = join35(input.vault, rawRelPath);
|
|
6758
|
+
const typedAbsPath = join35(input.vault, typedRelPath);
|
|
6378
6759
|
const rawContent = buildRawContent(sourceUrl, today, sha256, sourceContent);
|
|
6379
6760
|
const typedContent = buildTypedContent(
|
|
6380
6761
|
input.title,
|
|
@@ -6437,7 +6818,7 @@ async function runIngest(input) {
|
|
|
6437
6818
|
};
|
|
6438
6819
|
}
|
|
6439
6820
|
try {
|
|
6440
|
-
await
|
|
6821
|
+
await mkdir13(join35(input.vault, "raw", "articles"), { recursive: true });
|
|
6441
6822
|
await writeFile14(rawAbsPath, rawContent, "utf8");
|
|
6442
6823
|
} catch (e) {
|
|
6443
6824
|
return {
|
|
@@ -6446,7 +6827,7 @@ async function runIngest(input) {
|
|
|
6446
6827
|
};
|
|
6447
6828
|
}
|
|
6448
6829
|
try {
|
|
6449
|
-
await
|
|
6830
|
+
await mkdir13(join35(input.vault, typedDir), { recursive: true });
|
|
6450
6831
|
await writeFile14(typedAbsPath, typedContent, "utf8");
|
|
6451
6832
|
} catch (e) {
|
|
6452
6833
|
return {
|
|
@@ -6625,12 +7006,12 @@ ${body}`;
|
|
|
6625
7006
|
}
|
|
6626
7007
|
|
|
6627
7008
|
// src/commands/sync.ts
|
|
6628
|
-
import { existsSync as
|
|
6629
|
-
import { join as
|
|
7009
|
+
import { existsSync as existsSync14 } from "fs";
|
|
7010
|
+
import { join as join37 } from "path";
|
|
6630
7011
|
|
|
6631
7012
|
// src/utils/sync-lock.ts
|
|
6632
|
-
import { existsSync as
|
|
6633
|
-
import { join as
|
|
7013
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync10, renameSync, unlinkSync as unlinkSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
7014
|
+
import { join as join36 } from "path";
|
|
6634
7015
|
import { createHash as createHash6 } from "crypto";
|
|
6635
7016
|
function getSessionId() {
|
|
6636
7017
|
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
@@ -6638,11 +7019,11 @@ function getSessionId() {
|
|
|
6638
7019
|
return process.pid.toString();
|
|
6639
7020
|
}
|
|
6640
7021
|
function lockPath(vault) {
|
|
6641
|
-
return
|
|
7022
|
+
return join36(vault, ".skillwiki", "sync.lock");
|
|
6642
7023
|
}
|
|
6643
7024
|
function readLock(vault) {
|
|
6644
7025
|
const path = lockPath(vault);
|
|
6645
|
-
if (!
|
|
7026
|
+
if (!existsSync13(path)) return null;
|
|
6646
7027
|
try {
|
|
6647
7028
|
const raw = readFileSync10(path, "utf8");
|
|
6648
7029
|
return JSON.parse(raw);
|
|
@@ -6657,8 +7038,8 @@ function isStale(lock, now) {
|
|
|
6657
7038
|
}
|
|
6658
7039
|
function acquireLock(vault, opts = {}) {
|
|
6659
7040
|
const path = lockPath(vault);
|
|
6660
|
-
const dir =
|
|
6661
|
-
if (!
|
|
7041
|
+
const dir = join36(vault, ".skillwiki");
|
|
7042
|
+
if (!existsSync13(dir)) {
|
|
6662
7043
|
mkdirSync4(dir, { recursive: true });
|
|
6663
7044
|
}
|
|
6664
7045
|
const sessionId = opts.sessionId ?? getSessionId();
|
|
@@ -6703,7 +7084,7 @@ function writeLockedFile(path, lock) {
|
|
|
6703
7084
|
}
|
|
6704
7085
|
function releaseLock(vault, opts = {}) {
|
|
6705
7086
|
const path = lockPath(vault);
|
|
6706
|
-
if (!
|
|
7087
|
+
if (!existsSync13(path)) {
|
|
6707
7088
|
return { released: false };
|
|
6708
7089
|
}
|
|
6709
7090
|
const sessionId = opts.sessionId ?? getSessionId();
|
|
@@ -6732,7 +7113,7 @@ function releaseLock(vault, opts = {}) {
|
|
|
6732
7113
|
function runSyncStatus(input) {
|
|
6733
7114
|
const vault = input.vault;
|
|
6734
7115
|
const includeStashes = input.includeStashes ?? false;
|
|
6735
|
-
if (!
|
|
7116
|
+
if (!existsSync14(join37(vault, ".git"))) {
|
|
6736
7117
|
return {
|
|
6737
7118
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6738
7119
|
result: ok({
|
|
@@ -6746,6 +7127,7 @@ function runSyncStatus(input) {
|
|
|
6746
7127
|
})
|
|
6747
7128
|
};
|
|
6748
7129
|
}
|
|
7130
|
+
enableGitLongPathsOnWindows(vault);
|
|
6749
7131
|
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
6750
7132
|
const dirty = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0).length : 0;
|
|
6751
7133
|
const revOutput = git(vault, ["rev-list", "--left-right", "--count", "origin/HEAD...HEAD"]);
|
|
@@ -6809,12 +7191,24 @@ function runSyncStatus(input) {
|
|
|
6809
7191
|
}
|
|
6810
7192
|
async function runSyncPush(input) {
|
|
6811
7193
|
const vault = input.vault;
|
|
6812
|
-
if (!
|
|
7194
|
+
if (!existsSync14(join37(vault, ".git"))) {
|
|
6813
7195
|
return {
|
|
6814
7196
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6815
7197
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
6816
7198
|
};
|
|
6817
7199
|
}
|
|
7200
|
+
enableGitLongPathsOnWindows(vault);
|
|
7201
|
+
let pathFixes = 0;
|
|
7202
|
+
const pathFix = await fixPathTooLong({ vault });
|
|
7203
|
+
if (pathFix.result.ok && pathFix.result.data.fixed.length > 0) {
|
|
7204
|
+
pathFixes = pathFix.result.data.fixed.length;
|
|
7205
|
+
appendLastOp(vault, {
|
|
7206
|
+
operation: "lint-fix",
|
|
7207
|
+
summary: `fixed ${pathFixes} long path(s)`,
|
|
7208
|
+
files: pathFix.result.data.fixed.flatMap((f) => [f.from, f.to]),
|
|
7209
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
7210
|
+
});
|
|
7211
|
+
}
|
|
6818
7212
|
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
6819
7213
|
const dirtyFiles = porcelain ? porcelain.split("\n").filter((l) => l.trim().length > 0) : [];
|
|
6820
7214
|
if (dirtyFiles.length === 0) {
|
|
@@ -6824,6 +7218,7 @@ async function runSyncPush(input) {
|
|
|
6824
7218
|
files_committed: 0,
|
|
6825
7219
|
commit_message: "",
|
|
6826
7220
|
pushed: false,
|
|
7221
|
+
path_fixes: pathFixes,
|
|
6827
7222
|
humanHint: "nothing to commit, working tree clean"
|
|
6828
7223
|
})
|
|
6829
7224
|
};
|
|
@@ -6878,7 +7273,8 @@ async function runSyncPush(input) {
|
|
|
6878
7273
|
files_committed: dirtyFiles.length,
|
|
6879
7274
|
commit_message: commitMessage,
|
|
6880
7275
|
pushed: false,
|
|
6881
|
-
|
|
7276
|
+
path_fixes: pathFixes,
|
|
7277
|
+
humanHint: `committed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""} but push failed: ${String(e)}`
|
|
6882
7278
|
})
|
|
6883
7279
|
};
|
|
6884
7280
|
}
|
|
@@ -6888,7 +7284,8 @@ async function runSyncPush(input) {
|
|
|
6888
7284
|
files_committed: dirtyFiles.length,
|
|
6889
7285
|
commit_message: commitMessage,
|
|
6890
7286
|
pushed,
|
|
6891
|
-
|
|
7287
|
+
path_fixes: pathFixes,
|
|
7288
|
+
humanHint: `committed and pushed ${dirtyFiles.length} file(s)${pathFixes > 0 ? ` after ${pathFixes} long-path fix(es)` : ""}`
|
|
6892
7289
|
})
|
|
6893
7290
|
};
|
|
6894
7291
|
}
|
|
@@ -6911,14 +7308,19 @@ function enumerateStashes(vault) {
|
|
|
6911
7308
|
}
|
|
6912
7309
|
return stashes;
|
|
6913
7310
|
}
|
|
7311
|
+
function enableGitLongPathsOnWindows(vault) {
|
|
7312
|
+
if (process.platform !== "win32") return;
|
|
7313
|
+
git(vault, ["config", "core.longpaths", "true"]);
|
|
7314
|
+
}
|
|
6914
7315
|
async function runSyncPull(input) {
|
|
6915
7316
|
const vault = input.vault;
|
|
6916
|
-
if (!
|
|
7317
|
+
if (!existsSync14(join37(vault, ".git"))) {
|
|
6917
7318
|
return {
|
|
6918
7319
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
6919
7320
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
6920
7321
|
};
|
|
6921
7322
|
}
|
|
7323
|
+
enableGitLongPathsOnWindows(vault);
|
|
6922
7324
|
let fetched = false;
|
|
6923
7325
|
try {
|
|
6924
7326
|
gitStrict(vault, ["fetch", "origin"]);
|
|
@@ -7007,6 +7409,8 @@ async function runSyncPull(input) {
|
|
|
7007
7409
|
};
|
|
7008
7410
|
}
|
|
7009
7411
|
}
|
|
7412
|
+
const pathFix = await fixPathTooLong({ vault });
|
|
7413
|
+
const pathFixCount = pathFix.result.ok ? pathFix.result.data.fixed.length : 0;
|
|
7010
7414
|
let lintErrors = 0;
|
|
7011
7415
|
let lintWarnings = 0;
|
|
7012
7416
|
const lintResult = await runLint({ vault, days: 90, lines: 200, logThreshold: 500 });
|
|
@@ -7018,6 +7422,7 @@ async function runSyncPull(input) {
|
|
|
7018
7422
|
if (filesUpdated > 0) hintParts.push(`updated ${filesUpdated} file(s)`);
|
|
7019
7423
|
else hintParts.push("already up to date");
|
|
7020
7424
|
if (autoResolved > 0) hintParts.push(`${autoResolved} conflict(s) auto-resolved`);
|
|
7425
|
+
if (pathFixCount > 0) hintParts.push(`${pathFixCount} long path(s) fixed`);
|
|
7021
7426
|
if (lintErrors > 0) hintParts.push(`${lintErrors} lint error(s)`);
|
|
7022
7427
|
if (lintWarnings > 0) hintParts.push(`${lintWarnings} lint warning(s)`);
|
|
7023
7428
|
const exitCode = lintErrors > 0 ? ExitCode.LINT_HAS_ERRORS : lintWarnings > 0 ? ExitCode.LINT_HAS_WARNINGS : ExitCode.OK;
|
|
@@ -7081,7 +7486,7 @@ function runSyncPeers(input) {
|
|
|
7081
7486
|
}
|
|
7082
7487
|
function runSyncLock(input) {
|
|
7083
7488
|
const vault = input.vault;
|
|
7084
|
-
if (!
|
|
7489
|
+
if (!existsSync14(vault)) {
|
|
7085
7490
|
return {
|
|
7086
7491
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
7087
7492
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -7116,7 +7521,7 @@ function runSyncLock(input) {
|
|
|
7116
7521
|
}
|
|
7117
7522
|
function runSyncUnlock(input) {
|
|
7118
7523
|
const vault = input.vault;
|
|
7119
|
-
if (!
|
|
7524
|
+
if (!existsSync14(vault)) {
|
|
7120
7525
|
return {
|
|
7121
7526
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
7122
7527
|
result: err("VAULT_PATH_INVALID", { path: vault })
|
|
@@ -7150,7 +7555,7 @@ function runSyncUnlock(input) {
|
|
|
7150
7555
|
|
|
7151
7556
|
// src/commands/backup.ts
|
|
7152
7557
|
import { statSync as statSync5, readdirSync as readdirSync2, readFileSync as readFileSync11, mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
7153
|
-
import { join as
|
|
7558
|
+
import { join as join38, relative as relative3, dirname as dirname12 } from "path";
|
|
7154
7559
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
7155
7560
|
|
|
7156
7561
|
// src/utils/s3-client.ts
|
|
@@ -7170,11 +7575,11 @@ function createS3Client(config) {
|
|
|
7170
7575
|
}
|
|
7171
7576
|
|
|
7172
7577
|
// src/commands/backup.ts
|
|
7173
|
-
var
|
|
7578
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node_modules", ".skillwiki"]);
|
|
7174
7579
|
function* walkMarkdown(dir, base) {
|
|
7175
7580
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
7176
|
-
if (
|
|
7177
|
-
const full =
|
|
7581
|
+
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
7582
|
+
const full = join38(dir, entry.name);
|
|
7178
7583
|
if (entry.isDirectory()) {
|
|
7179
7584
|
yield* walkMarkdown(full, base);
|
|
7180
7585
|
} else if (entry.name.endsWith(".md")) {
|
|
@@ -7197,7 +7602,7 @@ async function runBackupSync(input) {
|
|
|
7197
7602
|
let failed = 0;
|
|
7198
7603
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
7199
7604
|
for (const relPath of files) {
|
|
7200
|
-
const absPath =
|
|
7605
|
+
const absPath = join38(input.vault, relPath);
|
|
7201
7606
|
const localStat = statSync5(absPath);
|
|
7202
7607
|
let needsUpload = true;
|
|
7203
7608
|
try {
|
|
@@ -7273,7 +7678,7 @@ async function runBackupRestore(input) {
|
|
|
7273
7678
|
const objects = list.Contents ?? [];
|
|
7274
7679
|
for (const obj of objects) {
|
|
7275
7680
|
if (!obj.Key) continue;
|
|
7276
|
-
const localPath =
|
|
7681
|
+
const localPath = join38(target, obj.Key);
|
|
7277
7682
|
try {
|
|
7278
7683
|
const localStat = statSync5(localPath);
|
|
7279
7684
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
@@ -7286,7 +7691,7 @@ async function runBackupRestore(input) {
|
|
|
7286
7691
|
const resp = await client.send(new GetObjectCommand({ Bucket: input.bucket, Key: obj.Key }));
|
|
7287
7692
|
const body = await resp.Body?.transformToByteArray();
|
|
7288
7693
|
if (body) {
|
|
7289
|
-
mkdirSync5(
|
|
7694
|
+
mkdirSync5(dirname12(localPath), { recursive: true });
|
|
7290
7695
|
writeFileSync7(localPath, Buffer.from(body));
|
|
7291
7696
|
downloaded++;
|
|
7292
7697
|
}
|
|
@@ -7319,11 +7724,11 @@ async function runBackupRestore(input) {
|
|
|
7319
7724
|
}
|
|
7320
7725
|
|
|
7321
7726
|
// src/commands/status.ts
|
|
7322
|
-
import { existsSync as
|
|
7323
|
-
import { readFile as
|
|
7324
|
-
import { join as
|
|
7727
|
+
import { existsSync as existsSync15, statSync as statSync6 } from "fs";
|
|
7728
|
+
import { readFile as readFile25 } from "fs/promises";
|
|
7729
|
+
import { join as join39 } from "path";
|
|
7325
7730
|
async function runStatus(input) {
|
|
7326
|
-
if (!
|
|
7731
|
+
if (!existsSync15(input.vault)) {
|
|
7327
7732
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
7328
7733
|
}
|
|
7329
7734
|
const scan = await scanVault(input.vault);
|
|
@@ -7348,7 +7753,7 @@ async function runStatus(input) {
|
|
|
7348
7753
|
const compound = scan.data.compound.length;
|
|
7349
7754
|
let schemaVersion = "v1";
|
|
7350
7755
|
try {
|
|
7351
|
-
const schemaContent = await
|
|
7756
|
+
const schemaContent = await readFile25(join39(input.vault, "SCHEMA.md"), "utf8");
|
|
7352
7757
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
7353
7758
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
7354
7759
|
} catch {
|
|
@@ -7408,8 +7813,8 @@ async function runStatus(input) {
|
|
|
7408
7813
|
}
|
|
7409
7814
|
|
|
7410
7815
|
// src/commands/seed.ts
|
|
7411
|
-
import { mkdir as
|
|
7412
|
-
import { join as
|
|
7816
|
+
import { mkdir as mkdir14, writeFile as writeFile15, stat as stat8 } from "fs/promises";
|
|
7817
|
+
import { join as join40 } from "path";
|
|
7413
7818
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
7414
7819
|
var EXAMPLE_PAGES = {
|
|
7415
7820
|
"entities/example-project.md": `---
|
|
@@ -7478,29 +7883,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
7478
7883
|
`;
|
|
7479
7884
|
async function runSeed(input) {
|
|
7480
7885
|
try {
|
|
7481
|
-
await stat8(
|
|
7886
|
+
await stat8(join40(input.vault, "SCHEMA.md"));
|
|
7482
7887
|
} catch {
|
|
7483
7888
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
7484
7889
|
}
|
|
7485
7890
|
const created = [];
|
|
7486
7891
|
const skipped = [];
|
|
7487
7892
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
7488
|
-
const absPath =
|
|
7893
|
+
const absPath = join40(input.vault, relPath);
|
|
7489
7894
|
try {
|
|
7490
7895
|
await stat8(absPath);
|
|
7491
7896
|
skipped.push(relPath);
|
|
7492
7897
|
} catch {
|
|
7493
|
-
await
|
|
7898
|
+
await mkdir14(join40(absPath, ".."), { recursive: true });
|
|
7494
7899
|
await writeFile15(absPath, content, "utf8");
|
|
7495
7900
|
created.push(relPath);
|
|
7496
7901
|
}
|
|
7497
7902
|
}
|
|
7498
|
-
const rawPath =
|
|
7903
|
+
const rawPath = join40(input.vault, "raw", "articles", "example-source.md");
|
|
7499
7904
|
try {
|
|
7500
7905
|
await stat8(rawPath);
|
|
7501
7906
|
skipped.push("raw/articles/example-source.md");
|
|
7502
7907
|
} catch {
|
|
7503
|
-
await
|
|
7908
|
+
await mkdir14(join40(rawPath, ".."), { recursive: true });
|
|
7504
7909
|
await writeFile15(rawPath, EXAMPLE_RAW, "utf8");
|
|
7505
7910
|
created.push("raw/articles/example-source.md");
|
|
7506
7911
|
}
|
|
@@ -7523,9 +7928,9 @@ async function runSeed(input) {
|
|
|
7523
7928
|
}
|
|
7524
7929
|
|
|
7525
7930
|
// src/commands/canvas.ts
|
|
7526
|
-
import { readFile as
|
|
7527
|
-
import { existsSync as
|
|
7528
|
-
import { join as
|
|
7931
|
+
import { readFile as readFile26, writeFile as writeFile16 } from "fs/promises";
|
|
7932
|
+
import { existsSync as existsSync16 } from "fs";
|
|
7933
|
+
import { join as join41 } from "path";
|
|
7529
7934
|
var NODE_WIDTH = 240;
|
|
7530
7935
|
var NODE_HEIGHT = 60;
|
|
7531
7936
|
var COLUMN_SPACING = 400;
|
|
@@ -7603,8 +8008,8 @@ function buildCanvasEdges(adjacency) {
|
|
|
7603
8008
|
return edges;
|
|
7604
8009
|
}
|
|
7605
8010
|
async function runCanvasGenerate(input) {
|
|
7606
|
-
const graphPath = input.graphPath ??
|
|
7607
|
-
if (!
|
|
8011
|
+
const graphPath = input.graphPath ?? join41(input.vault, ".skillwiki", "graph.json");
|
|
8012
|
+
if (!existsSync16(graphPath)) {
|
|
7608
8013
|
return {
|
|
7609
8014
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
7610
8015
|
result: err("FILE_NOT_FOUND", {
|
|
@@ -7615,7 +8020,7 @@ async function runCanvasGenerate(input) {
|
|
|
7615
8020
|
}
|
|
7616
8021
|
let raw;
|
|
7617
8022
|
try {
|
|
7618
|
-
raw = await
|
|
8023
|
+
raw = await readFile26(graphPath, "utf8");
|
|
7619
8024
|
} catch (e) {
|
|
7620
8025
|
return {
|
|
7621
8026
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -7641,7 +8046,7 @@ async function runCanvasGenerate(input) {
|
|
|
7641
8046
|
const nodes = buildCanvasNodes(paths);
|
|
7642
8047
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
7643
8048
|
const canvas = { nodes, edges };
|
|
7644
|
-
const outPath =
|
|
8049
|
+
const outPath = join41(input.vault, "vault-graph.canvas");
|
|
7645
8050
|
try {
|
|
7646
8051
|
await writeFile16(outPath, JSON.stringify(canvas, null, 2));
|
|
7647
8052
|
} catch (e) {
|
|
@@ -7663,8 +8068,8 @@ written: ${outPath}`
|
|
|
7663
8068
|
}
|
|
7664
8069
|
|
|
7665
8070
|
// src/commands/query.ts
|
|
7666
|
-
import { readFile as
|
|
7667
|
-
import { join as
|
|
8071
|
+
import { readFile as readFile27, stat as stat9 } from "fs/promises";
|
|
8072
|
+
import { join as join42 } from "path";
|
|
7668
8073
|
var W_KEYWORD = 2;
|
|
7669
8074
|
var W_SOURCE_OVERLAP = 4;
|
|
7670
8075
|
var W_WIKILINK = 3;
|
|
@@ -7785,7 +8190,7 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
7785
8190
|
return score;
|
|
7786
8191
|
}
|
|
7787
8192
|
async function loadOrBuildGraph(vault) {
|
|
7788
|
-
const graphPath =
|
|
8193
|
+
const graphPath = join42(vault, ".skillwiki", "graph.json");
|
|
7789
8194
|
let needsBuild = false;
|
|
7790
8195
|
try {
|
|
7791
8196
|
const fileStat = await stat9(graphPath);
|
|
@@ -7799,7 +8204,7 @@ async function loadOrBuildGraph(vault) {
|
|
|
7799
8204
|
if (buildResult.exitCode !== 0) return null;
|
|
7800
8205
|
}
|
|
7801
8206
|
try {
|
|
7802
|
-
const raw = await
|
|
8207
|
+
const raw = await readFile27(graphPath, "utf8");
|
|
7803
8208
|
return JSON.parse(raw);
|
|
7804
8209
|
} catch {
|
|
7805
8210
|
return null;
|
|
@@ -7807,14 +8212,14 @@ async function loadOrBuildGraph(vault) {
|
|
|
7807
8212
|
}
|
|
7808
8213
|
|
|
7809
8214
|
// src/utils/auto-commit.ts
|
|
7810
|
-
import { existsSync as
|
|
7811
|
-
import { join as
|
|
8215
|
+
import { existsSync as existsSync17 } from "fs";
|
|
8216
|
+
import { join as join43 } from "path";
|
|
7812
8217
|
async function postCommit(vault, exitCode) {
|
|
7813
8218
|
if (exitCode !== 0) return;
|
|
7814
8219
|
const home = process.env.HOME ?? "";
|
|
7815
8220
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
7816
8221
|
if (dotenv["AUTO_COMMIT"] === "false") return;
|
|
7817
|
-
if (!
|
|
8222
|
+
if (!existsSync17(join43(vault, ".git"))) return;
|
|
7818
8223
|
const lastOps = readLastOp(vault);
|
|
7819
8224
|
if (lastOps.length === 0) return;
|
|
7820
8225
|
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
@@ -7865,7 +8270,7 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
7865
8270
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
7866
8271
|
});
|
|
7867
8272
|
program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
7868
|
-
const out = opts.out ??
|
|
8273
|
+
const out = opts.out ?? join44(vault, ".skillwiki", "graph.json");
|
|
7869
8274
|
emit(await runGraphBuild({ vault, out }), vault);
|
|
7870
8275
|
});
|
|
7871
8276
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|