skillwiki 0.10.27 → 0.10.29

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.
@@ -4,9 +4,9 @@ import {
4
4
  UNMANAGED_START,
5
5
  renderRootIndex,
6
6
  writeRootIndexProjection
7
- } from "./chunk-QBZYEEBD.js";
8
- import "./chunk-6ZDKTNLA.js";
9
- import "./chunk-C2DKFJFA.js";
7
+ } from "./chunk-OJVIND5D.js";
8
+ import "./chunk-UI4SOWI2.js";
9
+ import "./chunk-RQARJ6HB.js";
10
10
  export {
11
11
  UNMANAGED_END,
12
12
  UNMANAGED_START,
@@ -2,9 +2,11 @@
2
2
  import {
3
3
  runManagedWritePreflight,
4
4
  runManagedWriteTransaction
5
- } from "./chunk-BASWDOQB.js";
6
- import "./chunk-PQG26AGJ.js";
7
- import "./chunk-C2DKFJFA.js";
5
+ } from "./chunk-NOXDRIHM.js";
6
+ import "./chunk-5XQWYC5K.js";
7
+ import "./chunk-UI4SOWI2.js";
8
+ import "./chunk-SGZPIGGX.js";
9
+ import "./chunk-RQARJ6HB.js";
8
10
  export {
9
11
  runManagedWritePreflight,
10
12
  runManagedWriteTransaction
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runSkillwikiMcpStdio
4
- } from "./chunk-VXMHHXVP.js";
4
+ } from "./chunk-6EEI6REK.js";
5
5
  import "./chunk-7I2TPIV5.js";
6
- import "./chunk-QBZYEEBD.js";
7
- import "./chunk-PQG26AGJ.js";
8
- import "./chunk-QNTBNNEL.js";
9
- import "./chunk-6ZDKTNLA.js";
10
- import "./chunk-C2DKFJFA.js";
6
+ import "./chunk-OJVIND5D.js";
7
+ import "./chunk-5XQWYC5K.js";
8
+ import "./chunk-UI4SOWI2.js";
9
+ import "./chunk-SGZPIGGX.js";
10
+ import "./chunk-RQARJ6HB.js";
11
11
 
12
12
  // src/mcp-entry.ts
13
13
  runSkillwikiMcpStdio().catch((error) => {
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runSourcesPending
4
+ } from "./chunk-23CR4YPL.js";
5
+ import "./chunk-SGZPIGGX.js";
6
+ import "./chunk-RQARJ6HB.js";
7
+ export {
8
+ runSourcesPending
9
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.27",
3
+ "version": "0.10.29",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.27",
3
+ "version": "0.10.29",
4
4
  "skills": "./",
5
5
  "description": "Project-aware Karpathy-style knowledge base for Claude Code: 19 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
6
6
  "author": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.27",
3
+ "version": "0.10.29",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 19 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.10.27",
3
+ "version": "0.10.29",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -17,6 +17,17 @@ Use this section as procedural planning guidelines:
17
17
  3. If `prd_layer` is `manual` or `none`, do not force the gate; follow project policy.
18
18
  4. Always apply the PRD bridge: spec/plan outputs go to vault work-item paths, never `docs/superpowers/`.
19
19
 
20
+ ## Activation File (Cross-Harness Alternative to SessionStart)
21
+
22
+ On harnesses where the SessionStart hook does not fire (Grok, Codex), SkillWiki activation context is delivered via a compact file installed at session start. Run `npm run install:activation` from the repo to install:
23
+
24
+ - `~/.grok/skillwiki.md` and `~/.claude/skillwiki.md` - compact derivative (~31 lines) covering identity, CLI probe, skill map, PRD bridge, fail-closed boundary, and drift warning.
25
+ - `~/.grok/AGENTS.md` and `~/.claude/CLAUDE.md` - prepended with a marker-wrapped reference: `Read @skillwiki.md for SkillWiki activation context.`
26
+
27
+ The activation file is self-contained literal text (ADR-3). On harnesses with `@file` import support (Claude Code, Cursor, Gemini), the reference is inlined automatically. On harnesses without it (Grok, Codex), the agent reads the file manually.
28
+
29
+ The template lives at `packages/skills/using-skillwiki/activation.md`. Run `npm run install:activation:check` to verify the installed copy matches the template.
30
+
20
31
  ## When to Use These Skills
21
32
  Invoke a skillwiki skill when the user:
22
33
  - Wants to create, build, or start a vault/wiki/knowledge base
@@ -0,0 +1,57 @@
1
+ # SkillWiki Activation
2
+
3
+ You have SkillWiki - a project-aware knowledge-base CLI + skill suite for agent harnesses.
4
+ This file is loaded at session start. For full operational detail, invoke `/using-skillwiki`.
5
+
6
+ ## CLI Probe
7
+
8
+ If `skillwiki --help` fails, the CLI is unavailable. Degrade to manual file ops (grep/find) for read-only queries. Fail closed for managed mutations - never write typed pages, index, or log directly.
9
+
10
+ ## When to Route
11
+
12
+ Invoke a SkillWiki skill when the user: wants vault/wiki/knowledge-base operations, ingests sources or URLs, searches/queries vault content, runs health checks or lint, crystallizes a session, works with project workspaces/ADRs, captures ideas/bugs/tasks, archives pages, removes paths, detects source drift, ingests foreign PRD formats, syncs vault git, or visualizes the vault graph.
13
+
14
+ ## Skill Map
15
+
16
+ | Skill | When to Invoke |
17
+ |-------|----------------|
18
+ | `wiki-init` | Bootstrap a vault |
19
+ | `wiki-ingest` | Convert URLs/files/text into typed-knowledge pages |
20
+ | `wiki-query` | Search typed knowledge |
21
+ | `wiki-lint` | Vault health and lint checks |
22
+ | `wiki-crystallize` | Distill current session into a typed page |
23
+ | `wiki-audit` | Verify raw provenance and source integrity |
24
+ | `wiki-archive` | Archive typed pages or preserve-move raw sources |
25
+ | `wiki-remove` | Hard-delete vault paths without snapshot resurrection |
26
+ | `wiki-reingest` | Detect source drift and re-ingest updated content |
27
+ | `wiki-add-task` | Quick-capture ideas, bugs, tasks, notes |
28
+ | `wiki-adapter-prd` | Map foreign PRD formats (CodeStable, RFC, AIDE, Hermes) |
29
+ | `wiki-sync` | Safely sync vault git repository |
30
+ | `wiki-canvas` | Generate Obsidian Canvas visualization |
31
+ | `wiki-gate-plan-mode` | Toggle EnterPlanMode gating for superpowers planning |
32
+ | `proj-init` | Bootstrap a project workspace |
33
+ | `proj-work` | Open or run a work item |
34
+ | `proj-distill` | Distill project compound entries into concept pages |
35
+ | `proj-decide` | Write an Architectural Decision Record (ADR) |
36
+ | `dev-loop:research` | Research scan of repo + vault health |
37
+
38
+ ## PRD Bridge
39
+
40
+ Route PRD/spec/plan work to `wiki-adapter-prd` and `proj-work`, not `docs/superpowers/`. Spec and plan outputs must land in vault work-item paths. Never create `docs/superpowers/` in any repo.
41
+
42
+ ## Fail-Closed Boundary
43
+
44
+ Never write typed pages, `index.md`, or `log.md` directly. Never bare `rm` or `git rm` as a fleet delete (snapshot resurrects from S3). Never auto `npm install -g skillwiki` in headless/goal/satellite sessions. If `skillwiki page publish --help` is unavailable, fail closed.
45
+
46
+ ## Sensitive Content
47
+
48
+ Never commit secrets, credentials, API keys, tokens, passwords, or PII to the vault. Redact using `[REDACTED:<kind>]` before filing.
49
+
50
+ ## Drift Warning
51
+
52
+ If `skillwiki doctor` mentions a version newer than this file, re-read the full `/using-skillwiki` skill for updated content.
53
+
54
+ ## Canonical Paths
55
+
56
+ - Full skill: `packages/skills/using-skillwiki/SKILL.md`
57
+ - Vault schema: `SCHEMA.md` at the vault root (run `skillwiki path` to resolve)
@@ -17,6 +17,17 @@ Use this section as procedural planning guidelines:
17
17
  3. If `prd_layer` is `manual` or `none`, do not force the gate; follow project policy.
18
18
  4. Always apply the PRD bridge: spec/plan outputs go to vault work-item paths, never `docs/superpowers/`.
19
19
 
20
+ ## Activation File (Cross-Harness Alternative to SessionStart)
21
+
22
+ On harnesses where the SessionStart hook does not fire (Grok, Codex), SkillWiki activation context is delivered via a compact file installed at session start. Run `npm run install:activation` from the repo to install:
23
+
24
+ - `~/.grok/skillwiki.md` and `~/.claude/skillwiki.md` - compact derivative (~31 lines) covering identity, CLI probe, skill map, PRD bridge, fail-closed boundary, and drift warning.
25
+ - `~/.grok/AGENTS.md` and `~/.claude/CLAUDE.md` - prepended with a marker-wrapped reference: `Read @skillwiki.md for SkillWiki activation context.`
26
+
27
+ The activation file is self-contained literal text (ADR-3). On harnesses with `@file` import support (Claude Code, Cursor, Gemini), the reference is inlined automatically. On harnesses without it (Grok, Codex), the agent reads the file manually.
28
+
29
+ The template lives at `packages/skills/using-skillwiki/activation.md`. Run `npm run install:activation:check` to verify the installed copy matches the template.
30
+
20
31
  ## When to Use These Skills
21
32
  Invoke a skillwiki skill when the user:
22
33
  - Wants to create, build, or start a vault/wiki/knowledge base
@@ -0,0 +1,57 @@
1
+ # SkillWiki Activation
2
+
3
+ You have SkillWiki - a project-aware knowledge-base CLI + skill suite for agent harnesses.
4
+ This file is loaded at session start. For full operational detail, invoke `/using-skillwiki`.
5
+
6
+ ## CLI Probe
7
+
8
+ If `skillwiki --help` fails, the CLI is unavailable. Degrade to manual file ops (grep/find) for read-only queries. Fail closed for managed mutations - never write typed pages, index, or log directly.
9
+
10
+ ## When to Route
11
+
12
+ Invoke a SkillWiki skill when the user: wants vault/wiki/knowledge-base operations, ingests sources or URLs, searches/queries vault content, runs health checks or lint, crystallizes a session, works with project workspaces/ADRs, captures ideas/bugs/tasks, archives pages, removes paths, detects source drift, ingests foreign PRD formats, syncs vault git, or visualizes the vault graph.
13
+
14
+ ## Skill Map
15
+
16
+ | Skill | When to Invoke |
17
+ |-------|----------------|
18
+ | `wiki-init` | Bootstrap a vault |
19
+ | `wiki-ingest` | Convert URLs/files/text into typed-knowledge pages |
20
+ | `wiki-query` | Search typed knowledge |
21
+ | `wiki-lint` | Vault health and lint checks |
22
+ | `wiki-crystallize` | Distill current session into a typed page |
23
+ | `wiki-audit` | Verify raw provenance and source integrity |
24
+ | `wiki-archive` | Archive typed pages or preserve-move raw sources |
25
+ | `wiki-remove` | Hard-delete vault paths without snapshot resurrection |
26
+ | `wiki-reingest` | Detect source drift and re-ingest updated content |
27
+ | `wiki-add-task` | Quick-capture ideas, bugs, tasks, notes |
28
+ | `wiki-adapter-prd` | Map foreign PRD formats (CodeStable, RFC, AIDE, Hermes) |
29
+ | `wiki-sync` | Safely sync vault git repository |
30
+ | `wiki-canvas` | Generate Obsidian Canvas visualization |
31
+ | `wiki-gate-plan-mode` | Toggle EnterPlanMode gating for superpowers planning |
32
+ | `proj-init` | Bootstrap a project workspace |
33
+ | `proj-work` | Open or run a work item |
34
+ | `proj-distill` | Distill project compound entries into concept pages |
35
+ | `proj-decide` | Write an Architectural Decision Record (ADR) |
36
+ | `dev-loop:research` | Research scan of repo + vault health |
37
+
38
+ ## PRD Bridge
39
+
40
+ Route PRD/spec/plan work to `wiki-adapter-prd` and `proj-work`, not `docs/superpowers/`. Spec and plan outputs must land in vault work-item paths. Never create `docs/superpowers/` in any repo.
41
+
42
+ ## Fail-Closed Boundary
43
+
44
+ Never write typed pages, `index.md`, or `log.md` directly. Never bare `rm` or `git rm` as a fleet delete (snapshot resurrects from S3). Never auto `npm install -g skillwiki` in headless/goal/satellite sessions. If `skillwiki page publish --help` is unavailable, fail closed.
45
+
46
+ ## Sensitive Content
47
+
48
+ Never commit secrets, credentials, API keys, tokens, passwords, or PII to the vault. Redact using `[REDACTED:<kind>]` before filing.
49
+
50
+ ## Drift Warning
51
+
52
+ If `skillwiki doctor` mentions a version newer than this file, re-read the full `/using-skillwiki` skill for updated content.
53
+
54
+ ## Canonical Paths
55
+
56
+ - Full skill: `packages/skills/using-skillwiki/SKILL.md`
57
+ - Vault schema: `SCHEMA.md` at the vault root (run `skillwiki path` to resolve)
@@ -1,294 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- err,
4
- getErrorMessage,
5
- ok
6
- } from "./chunk-C2DKFJFA.js";
7
-
8
- // src/parsers/frontmatter.ts
9
- import yaml from "js-yaml";
10
- var FM_OPEN = /^---\r?\n/;
11
- function splitFrontmatter(text) {
12
- if (!FM_OPEN.test(text)) return ok({ rawFrontmatter: "", body: text, bodyStart: 0 });
13
- const afterOpen = text.replace(FM_OPEN, "");
14
- const closeIdx = afterOpen.search(/\r?\n---\r?\n/);
15
- if (closeIdx === -1) return err("MISSING_CLOSING_DELIMITER");
16
- const rawFrontmatter = afterOpen.slice(0, closeIdx);
17
- const closeMatch = afterOpen.slice(closeIdx).match(/\r?\n---\r?\n/);
18
- const bodyStart = text.length - (afterOpen.length - closeIdx - closeMatch[0].length);
19
- const body = text.slice(bodyStart);
20
- return ok({ rawFrontmatter, body, bodyStart });
21
- }
22
- function extractFrontmatter(text) {
23
- const split = splitFrontmatter(text);
24
- if (!split.ok) return split;
25
- if (!split.data.rawFrontmatter) return ok({});
26
- try {
27
- const parsed = yaml.load(split.data.rawFrontmatter, { schema: yaml.JSON_SCHEMA });
28
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return ok({});
29
- return ok(parsed);
30
- } catch (e) {
31
- return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
32
- }
33
- }
34
-
35
- // src/utils/sensitive-content.ts
36
- import { createHash } from "crypto";
37
- var REDACTED_RE = /\[REDACTED:[^\]]+\]/i;
38
- var SYNTHETIC_RE = /^(?:<[^>]+>|\$\{[^}]+\}|REPLACE_WITH_[A-Z0-9_]+|YOUR_[A-Z0-9_]+|EXAMPLE_[A-Z0-9_]+)$/i;
39
- var MATCHERS = [
40
- {
41
- kind: "private_key",
42
- re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g
43
- },
44
- {
45
- kind: "authorization_header",
46
- re: /\bAuthorization["']?\s*:\s*["']?(Bearer\s+[A-Za-z0-9._~+/-]{20,})["']?/gi,
47
- valueGroup: 1
48
- },
49
- {
50
- kind: "cookie",
51
- re: /\b(?:Cookie|Set-Cookie)\s*:\s*([^\n]{20,})/gi,
52
- valueGroup: 1
53
- },
54
- {
55
- kind: "jwt",
56
- re: /\b([A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,})\b/g,
57
- valueGroup: 1
58
- },
59
- {
60
- kind: "provider_key",
61
- re: /\b(sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|gh[pousr]_[A-Za-z0-9_=-]{20,})\b/g,
62
- valueGroup: 1
63
- },
64
- {
65
- kind: "access_key",
66
- re: /\b((?:AKIA|ASIA)[A-Z0-9]{16})\b/g,
67
- valueGroup: 1
68
- },
69
- {
70
- kind: "access_key",
71
- re: /\b(?:access[-_ ]?key|credential)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
72
- valueGroup: 1
73
- },
74
- {
75
- kind: "api_key",
76
- re: /\b(?:api[-_ ]?key)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
77
- valueGroup: 1
78
- },
79
- {
80
- kind: "password",
81
- re: /\b(?:pass(?:word|wd)?)["']?\s*[:=]\s*["']?([^\s`"']{8,})["']?/gi,
82
- valueGroup: 1
83
- },
84
- {
85
- kind: "secret",
86
- re: /\b(?:secret|client[-_ ]?secret)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
87
- valueGroup: 1
88
- },
89
- {
90
- kind: "token",
91
- re: /\b(?:token|session)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
92
- valueGroup: 1
93
- }
94
- ];
95
- function fingerprint(value) {
96
- return createHash("sha256").update(value).digest("hex").slice(0, 12);
97
- }
98
- function lineFor(text, offset) {
99
- return text.slice(0, offset).split(/\r?\n/).length;
100
- }
101
- function redactMarker(kind, value) {
102
- return `[REDACTED:${kind}:${fingerprint(value)}]`;
103
- }
104
- function isSyntheticPlaceholder(value) {
105
- return REDACTED_RE.test(value) || SYNTHETIC_RE.test(value.trim());
106
- }
107
- function isNonSecretTokenCapture(value) {
108
- if (value.startsWith("//")) return true;
109
- if (/^[a-z]{2,}(?:-[a-z]{2,})+$/.test(value)) return true;
110
- return false;
111
- }
112
- function collectMatches(text) {
113
- const matches = [];
114
- for (const matcher of MATCHERS) {
115
- matcher.re.lastIndex = 0;
116
- for (const m of text.matchAll(matcher.re)) {
117
- const whole = m[0];
118
- const start = m.index ?? 0;
119
- if (REDACTED_RE.test(whole)) continue;
120
- const value = matcher.valueGroup ? m[matcher.valueGroup] : whole;
121
- if (isSyntheticPlaceholder(value)) continue;
122
- if (matcher.kind === "token" && isNonSecretTokenCapture(value)) continue;
123
- const valueOffset = whole.lastIndexOf(value);
124
- const valueStart = start + Math.max(0, valueOffset);
125
- matches.push({
126
- start,
127
- end: start + whole.length,
128
- valueStart,
129
- valueEnd: valueStart + value.length,
130
- kind: matcher.kind
131
- });
132
- }
133
- }
134
- return matches.sort((a, b) => {
135
- if (a.valueStart !== b.valueStart) return a.valueStart - b.valueStart;
136
- return b.valueEnd - b.valueStart - (a.valueEnd - a.valueStart);
137
- });
138
- }
139
- function collapseOverlaps(matches) {
140
- const kept = [];
141
- for (const match of matches) {
142
- const overlaps = kept.some((k) => match.valueStart < k.valueEnd && match.valueEnd > k.valueStart);
143
- if (!overlaps) kept.push(match);
144
- }
145
- return kept;
146
- }
147
- function scanSensitiveContent(text, opts = {}) {
148
- return collapseOverlaps(collectMatches(text)).map((match) => {
149
- const value = text.slice(match.valueStart, match.valueEnd);
150
- const marker = redactMarker(match.kind, value);
151
- const rawPreview = text.slice(Math.max(0, match.start - 24), Math.min(text.length, match.end + 24));
152
- const preview = rawPreview.replace(value, marker);
153
- return {
154
- file: opts.file,
155
- line: lineFor(text, match.valueStart),
156
- kind: match.kind,
157
- preview,
158
- fingerprint: fingerprint(value)
159
- };
160
- });
161
- }
162
- function redactSensitiveContent(text, opts = {}) {
163
- const matches = collapseOverlaps(collectMatches(text));
164
- if (matches.length === 0) return { text, changed: false, findings: [] };
165
- let out = "";
166
- let cursor = 0;
167
- for (const match of matches) {
168
- const value = text.slice(match.valueStart, match.valueEnd);
169
- out += text.slice(cursor, match.valueStart);
170
- out += redactMarker(match.kind, value);
171
- cursor = match.valueEnd;
172
- }
173
- out += text.slice(cursor);
174
- return {
175
- text: out,
176
- changed: out !== text,
177
- findings: scanSensitiveContent(text, opts)
178
- };
179
- }
180
-
181
- // src/utils/vault.ts
182
- import { existsSync, readFileSync } from "fs";
183
- import { readFile, readdir, stat } from "fs/promises";
184
- import { join, relative, sep } from "path";
185
- var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
186
- var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
187
- var DEFAULT_IO_CONCURRENCY = 1;
188
- function vaultIoConcurrency() {
189
- const raw = Number.parseInt(process.env.SKILLWIKI_VAULT_IO_CONCURRENCY ?? "", 10);
190
- return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 64) : DEFAULT_IO_CONCURRENCY;
191
- }
192
- function decodeProcMountPath(value) {
193
- return value.replace(/\\040/g, " ");
194
- }
195
- function isRcloneFuseVaultFromMounts(root, mounts) {
196
- return mounts.split(/\r?\n/).some((line) => {
197
- const parts = line.split(" ");
198
- if (parts.length < 3) return false;
199
- const mountPoint = decodeProcMountPath(parts[1]);
200
- const fsType = parts[2];
201
- return fsType === "fuse.rclone" && (root === mountPoint || root.startsWith(`${mountPoint}/`));
202
- });
203
- }
204
- function resolveReadOnlyVaultRootWithMounts(root, mounts) {
205
- if (/^(1|true|yes)$/i.test(process.env.SKILLWIKI_DISABLE_VAULT_READ_MIRROR ?? "")) {
206
- return { root, mirrored: false };
207
- }
208
- const explicitMirror = process.env.SKILLWIKI_VAULT_READ_MIRROR;
209
- if (explicitMirror && existsSync(join(explicitMirror, "SCHEMA.md"))) {
210
- return { root: explicitMirror, mirrored: explicitMirror !== root };
211
- }
212
- const siblingMirror = `${root}-git`;
213
- if (isRcloneFuseVaultFromMounts(root, mounts) && existsSync(join(siblingMirror, "SCHEMA.md"))) {
214
- return { root: siblingMirror, mirrored: true };
215
- }
216
- return { root, mirrored: false };
217
- }
218
- function resolveReadOnlyVaultRoot(root) {
219
- let mounts = "";
220
- try {
221
- mounts = readFileSync("/proc/mounts", "utf8");
222
- } catch {
223
- }
224
- return resolveReadOnlyVaultRootWithMounts(root, mounts);
225
- }
226
- async function mapWithConcurrency(items, limit, mapper) {
227
- const out = new Array(items.length);
228
- let next = 0;
229
- const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
230
- for (; ; ) {
231
- const index = next++;
232
- if (index >= items.length) return;
233
- out[index] = await mapper(items[index], index);
234
- }
235
- });
236
- await Promise.all(workers);
237
- return out;
238
- }
239
- async function scanVault(root) {
240
- try {
241
- await stat(join(root, "SCHEMA.md"));
242
- } catch {
243
- return err("VAULT_PATH_INVALID", { root, reason: "SCHEMA.md missing" });
244
- }
245
- const all = await walk(root);
246
- const rels = all.map((p) => ({ absPath: p, relPath: relative(root, p).split(sep).join("/") }));
247
- return ok({
248
- root,
249
- allMarkdown: rels,
250
- typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
251
- raw: rels.filter((p) => p.relPath.startsWith("raw/")),
252
- workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
253
- compound: rels.filter((p) => /^projects\/[^/]+\/compound\//.test(p.relPath))
254
- });
255
- }
256
- async function walk(dir) {
257
- const entries = await readdir(dir, { withFileTypes: true });
258
- const out = [];
259
- const subdirs = [];
260
- for (const e of entries) {
261
- const p = join(dir, e.name);
262
- if (e.isDirectory()) {
263
- if (SKIP_DIRS.has(e.name)) continue;
264
- subdirs.push(p);
265
- } else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
266
- }
267
- const nested = await mapWithConcurrency(subdirs, Math.min(8, vaultIoConcurrency()), walk);
268
- for (const files of nested) out.push(...files);
269
- return out;
270
- }
271
- async function readPage(p) {
272
- return readFile(p.absPath, "utf8");
273
- }
274
- async function readPageCached(p, cache) {
275
- if (!cache) return readPage(p);
276
- const existing = cache.get(p.absPath);
277
- if (existing) return existing;
278
- const pending = readPage(p);
279
- cache.set(p.absPath, pending);
280
- return pending;
281
- }
282
-
283
- export {
284
- splitFrontmatter,
285
- extractFrontmatter,
286
- scanSensitiveContent,
287
- redactSensitiveContent,
288
- vaultIoConcurrency,
289
- resolveReadOnlyVaultRoot,
290
- mapWithConcurrency,
291
- scanVault,
292
- readPage,
293
- readPageCached
294
- };