skillwiki 0.10.26 → 0.10.28

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-W7NPTDIU.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
+ };
@@ -147,6 +147,270 @@ platform_job_status() {
147
147
  printf '{"enabled": %s, "running": %s, "last_exit": %d}\n' "$_enabled" "$_running" "$_last_exit"
148
148
  }
149
149
 
150
+ # macOS LaunchAgent plist validation. Callers inspect the non-empty
151
+ # PLATFORM_LAUNCHD_PLIST_REASON when this returns non-zero. The generated
152
+ # vault-sync units are XML plists, but use plutil to canonicalize a valid
153
+ # binary plist before applying the same structural checks. The validated first
154
+ # argument is exported for installer-only executable preflight warnings.
155
+ PLATFORM_LAUNCHD_PLIST_REASON=""
156
+ PLATFORM_LAUNCHD_PLIST_PROGRAM=""
157
+
158
+ platform_launchd_plist_validate() {
159
+ _platform_expected_label="${1:-}"
160
+ _platform_plist="${2:-}"
161
+ _platform_xml=""
162
+ _platform_fields=""
163
+ _platform_document_kind=""
164
+ _platform_label_kind=""
165
+ _platform_program_kind=""
166
+ _platform_actual_label=""
167
+ _platform_program=""
168
+ PLATFORM_LAUNCHD_PLIST_REASON=""
169
+ PLATFORM_LAUNCHD_PLIST_PROGRAM=""
170
+
171
+ if [ -z "$_platform_expected_label" ]; then
172
+ PLATFORM_LAUNCHD_PLIST_REASON="expected Label is empty"
173
+ return 1
174
+ fi
175
+ if [ -z "$_platform_plist" ] || [ ! -f "$_platform_plist" ]; then
176
+ PLATFORM_LAUNCHD_PLIST_REASON="plist file missing: ${_platform_plist:-unknown}"
177
+ return 1
178
+ fi
179
+ if [ ! -r "$_platform_plist" ]; then
180
+ PLATFORM_LAUNCHD_PLIST_REASON="plist file is not readable: $_platform_plist"
181
+ return 1
182
+ fi
183
+
184
+ if command -v plutil >/dev/null 2>&1; then
185
+ if ! plutil -lint "$_platform_plist" >/dev/null 2>&1; then
186
+ PLATFORM_LAUNCHD_PLIST_REASON="plutil -lint failed"
187
+ return 1
188
+ fi
189
+ if ! _platform_xml="$(plutil -convert xml1 -o - "$_platform_plist" 2>/dev/null)"; then
190
+ PLATFORM_LAUNCHD_PLIST_REASON="plutil could not render plist as XML"
191
+ return 1
192
+ fi
193
+ else
194
+ # Keep status useful on non-macOS CI hosts that lack plutil. This fallback
195
+ # intentionally supports structural XML only; macOS uses plutil above for
196
+ # syntactic validation and binary-plist conversion.
197
+ _platform_xml="$(cat "$_platform_plist")"
198
+ fi
199
+
200
+ # Parse the root plist dictionary structurally rather than searching for
201
+ # text. In particular, a Label-like string inside ProgramArguments must not
202
+ # satisfy Label validation, and ProgramArguments[0] must actually be a
203
+ # non-empty <string>, not merely a scalar which plutil can render as raw.
204
+ _platform_fields="$(
205
+ printf '%s\n' "$_platform_xml" | awk '
206
+ BEGIN {
207
+ label_kind = "missing"
208
+ program_kind = "missing"
209
+ }
210
+ function trim(value) {
211
+ sub(/^[[:space:]]+/, "", value)
212
+ sub(/[[:space:]]+$/, "", value)
213
+ return value
214
+ }
215
+ function string_value(value) {
216
+ sub(/^[[:space:]]*<string>/, "", value)
217
+ sub(/<\/string>[[:space:]]*$/, "", value)
218
+ return value
219
+ }
220
+ function invalid() {
221
+ invalid_token = 1
222
+ }
223
+ function open_container(kind) {
224
+ if (!plist_seen || plist_closed) {
225
+ invalid()
226
+ return
227
+ }
228
+ if (container_depth == 0) {
229
+ if (kind != "dict" || root_dict_seen) {
230
+ invalid()
231
+ return
232
+ }
233
+ root_dict_seen = 1
234
+ }
235
+ container_depth++
236
+ container_stack[container_depth] = kind
237
+ if (kind == "dict") dict_depth++
238
+ }
239
+ function close_container(kind) {
240
+ if (container_depth == 0 || container_stack[container_depth] != kind) {
241
+ invalid()
242
+ return
243
+ }
244
+ if (kind == "dict") {
245
+ if (dict_depth == 1) root_dict_closed = 1
246
+ dict_depth--
247
+ }
248
+ delete container_stack[container_depth]
249
+ container_depth--
250
+ }
251
+ function process(line) {
252
+ line = trim(line)
253
+ if (line == "") return
254
+ if (line ~ /^<!--.*-->$/) return
255
+ if (line ~ /^<\?xml[[:space:]][^>]*\?>$/) {
256
+ if (plist_seen || xml_declaration_seen) invalid()
257
+ xml_declaration_seen = 1
258
+ return
259
+ }
260
+ if (line ~ /^<!DOCTYPE[[:space:]].*>$/) {
261
+ if (plist_seen || doctype_seen) invalid()
262
+ doctype_seen = 1
263
+ return
264
+ }
265
+ if (line ~ /^<plist([[:space:]][^>]*)?>$/) {
266
+ if (plist_seen || plist_closed || container_depth != 0 || root_dict_seen) {
267
+ invalid()
268
+ } else {
269
+ plist_seen = 1
270
+ }
271
+ return
272
+ }
273
+ if (line == "</plist>") {
274
+ if (!plist_seen || plist_closed || !root_dict_closed || container_depth != 0) {
275
+ invalid()
276
+ } else {
277
+ plist_closed = 1
278
+ }
279
+ return
280
+ }
281
+
282
+ if (label_wait) {
283
+ if (line ~ /^<string>[^<]*<\/string>$/) {
284
+ label_value = string_value(line)
285
+ label_kind = "string"
286
+ label_wait = 0
287
+ return
288
+ }
289
+ label_kind = "not-string"
290
+ label_wait = 0
291
+ }
292
+
293
+ if (program_wait_array) {
294
+ if (line == "<array>") {
295
+ program_wait_array = 0
296
+ program_wait_first = 1
297
+ open_container("array")
298
+ return
299
+ }
300
+ program_kind = "not-array"
301
+ program_wait_array = 0
302
+ }
303
+
304
+ if (program_wait_first) {
305
+ if (line ~ /^<string>[^<]*<\/string>$/) {
306
+ program_value = string_value(line)
307
+ program_kind = (program_value == "" ? "empty" : "string")
308
+ program_wait_first = 0
309
+ return
310
+ }
311
+ program_kind = (line == "</array>" ? "missing" : "not-string")
312
+ program_wait_first = 0
313
+ }
314
+
315
+ if (line == "<dict>") {
316
+ open_container("dict")
317
+ return
318
+ }
319
+ if (line == "</dict>") {
320
+ close_container("dict")
321
+ return
322
+ }
323
+ if (line == "<array>") {
324
+ open_container("array")
325
+ return
326
+ }
327
+ if (line == "</array>") {
328
+ close_container("array")
329
+ return
330
+ }
331
+ if (line ~ /^<key>Label<\/key>$/ && dict_depth == 1 && container_depth == 1 && label_kind == "missing") {
332
+ label_wait = 1
333
+ return
334
+ }
335
+ if (line ~ /^<key>ProgramArguments<\/key>$/ && dict_depth == 1 && container_depth == 1 && program_kind == "missing") {
336
+ program_wait_array = 1
337
+ return
338
+ }
339
+ if (line ~ /^<key>[^<]*<\/key>$/ ||
340
+ line ~ /^<string>[^<]*<\/string>$/ ||
341
+ line ~ /^<integer>[^<]*<\/integer>$/ ||
342
+ line ~ /^<real>[^<]*<\/real>$/ ||
343
+ line ~ /^<date>[^<]*<\/date>$/ ||
344
+ line ~ /^<data>[^<]*<\/data>$/ ||
345
+ line == "<true/>" || line == "<false/>") {
346
+ if (!plist_seen || plist_closed || container_depth == 0) invalid()
347
+ return
348
+ }
349
+ invalid()
350
+ }
351
+ {
352
+ content = $0
353
+ gsub(/></, ">\n<", content)
354
+ count = split(content, chunks, "\n")
355
+ for (chunk_index = 1; chunk_index <= count; chunk_index++) process(chunks[chunk_index])
356
+ }
357
+ END {
358
+ if (label_wait && label_kind == "missing") label_kind = "not-string"
359
+ if (program_wait_array) program_kind = "not-array"
360
+ if (program_wait_first) program_kind = "missing"
361
+ document_kind = (plist_seen && plist_closed && root_dict_seen && root_dict_closed &&
362
+ dict_depth == 0 && container_depth == 0 && !invalid_token ? "valid" : "invalid")
363
+ printf "DOCUMENT_KIND=%s\n", document_kind
364
+ printf "LABEL_KIND=%s\n", label_kind
365
+ printf "LABEL_VALUE=%s\n", label_value
366
+ printf "PROGRAM_KIND=%s\n", program_kind
367
+ printf "PROGRAM_VALUE=%s\n", program_value
368
+ }
369
+ '
370
+ )"
371
+ _platform_document_kind="$(printf '%s\n' "$_platform_fields" | awk 'sub(/^DOCUMENT_KIND=/, "") { print; exit }')"
372
+ _platform_label_kind="$(printf '%s\n' "$_platform_fields" | awk 'sub(/^LABEL_KIND=/, "") { print; exit }')"
373
+ _platform_actual_label="$(printf '%s\n' "$_platform_fields" | awk 'sub(/^LABEL_VALUE=/, "") { print; exit }')"
374
+ _platform_program_kind="$(printf '%s\n' "$_platform_fields" | awk 'sub(/^PROGRAM_KIND=/, "") { print; exit }')"
375
+ _platform_program="$(printf '%s\n' "$_platform_fields" | awk 'sub(/^PROGRAM_VALUE=/, "") { print; exit }')"
376
+
377
+ if [ "$_platform_document_kind" != "valid" ]; then
378
+ PLATFORM_LAUNCHD_PLIST_REASON="plist XML structure is invalid"
379
+ return 1
380
+ fi
381
+ if [ "$_platform_label_kind" != "string" ]; then
382
+ PLATFORM_LAUNCHD_PLIST_REASON="Label missing or is not a string"
383
+ return 1
384
+ fi
385
+
386
+ if [ "$_platform_actual_label" != "$_platform_expected_label" ]; then
387
+ PLATFORM_LAUNCHD_PLIST_REASON="Label is '${_platform_actual_label:-missing}', expected '$_platform_expected_label'"
388
+ return 1
389
+ fi
390
+ case "$_platform_program_kind" in
391
+ missing)
392
+ PLATFORM_LAUNCHD_PLIST_REASON="ProgramArguments[0] missing"
393
+ return 1
394
+ ;;
395
+ empty)
396
+ PLATFORM_LAUNCHD_PLIST_REASON="ProgramArguments[0] is empty"
397
+ return 1
398
+ ;;
399
+ string)
400
+ ;;
401
+ *)
402
+ PLATFORM_LAUNCHD_PLIST_REASON="ProgramArguments[0] must be a string"
403
+ return 1
404
+ ;;
405
+ esac
406
+ if [ -z "$_platform_program" ]; then
407
+ PLATFORM_LAUNCHD_PLIST_REASON="ProgramArguments[0] missing"
408
+ return 1
409
+ fi
410
+ PLATFORM_LAUNCHD_PLIST_PROGRAM="$_platform_program"
411
+ return 0
412
+ }
413
+
150
414
  # Feature prerequisite check:
151
415
  # exit 1 with message if not available
152
416
  platform_require() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.10.26",
3
+ "version": "0.10.28",
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.26",
3
+ "version": "0.10.28",
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.26",
3
+ "version": "0.10.28",
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.26",
3
+ "version": "0.10.28",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",
@@ -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
- };