skillwiki 0.9.63 → 0.10.0

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.
Files changed (36) hide show
  1. package/dist/chunk-5TBIMLTZ.js +343 -0
  2. package/dist/chunk-C5OLZRRM.js +357 -0
  3. package/dist/{chunk-TUFQZ5K4.js → chunk-DR7KFHNH.js} +792 -1983
  4. package/dist/chunk-IZABIE44.js +647 -0
  5. package/dist/chunk-S5ABQCXQ.js +580 -0
  6. package/dist/cli.js +1540 -550
  7. package/dist/index-projection-ERFX76U5.js +10 -0
  8. package/dist/managed-write-preflight-SILEUQEV.js +11 -0
  9. package/dist/skillwiki-mcp.js +4 -1
  10. package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
  11. package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
  12. package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
  13. package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
  14. package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
  15. package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
  16. package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
  17. package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
  18. package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
  19. package/dist/vault-sync/scripts/lib/platform.sh +184 -0
  20. package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
  21. package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
  22. package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
  23. package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
  24. package/dist/vault-sync/scripts/wiki-push.sh +364 -0
  25. package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
  26. package/package.json +2 -2
  27. package/skills/.claude-plugin/plugin.json +1 -1
  28. package/skills/.codex-plugin/plugin.json +1 -1
  29. package/skills/README.md +13 -0
  30. package/skills/package.json +1 -1
  31. package/skills/proj-work/SKILL.md +3 -0
  32. package/skills/skills/proj-work/SKILL.md +3 -0
  33. package/skills/skills/using-skillwiki/SKILL.md +13 -0
  34. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  35. package/skills/using-skillwiki/SKILL.md +13 -0
  36. package/skills/wiki-crystallize/SKILL.md +3 -0
@@ -0,0 +1,647 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ MetaSchema,
4
+ TypedKnowledgeSchema,
5
+ detectSchema,
6
+ err,
7
+ getErrorMessage,
8
+ ok
9
+ } from "./chunk-C5OLZRRM.js";
10
+
11
+ // src/utils/index-projection.ts
12
+ import { readdirSync, readFileSync as readFileSync2 } from "fs";
13
+ import { readFile as readFile3 } from "fs/promises";
14
+ import { join as join3 } from "path";
15
+
16
+ // src/utils/atomic-write.ts
17
+ import { randomBytes } from "crypto";
18
+ import { open, readFile, rename, unlink } from "fs/promises";
19
+ import { basename, dirname, join } from "path";
20
+ async function readExisting(path) {
21
+ try {
22
+ return await readFile(path, "utf8");
23
+ } catch (error) {
24
+ if (error.code === "ENOENT") return null;
25
+ throw error;
26
+ }
27
+ }
28
+ async function atomicWriteText(path, text) {
29
+ let existing;
30
+ try {
31
+ existing = await readExisting(path);
32
+ } catch (error) {
33
+ return err("WRITE_FAILED", { path, phase: "read-existing", message: String(error) });
34
+ }
35
+ if (existing === text) return ok({ changed: false, existed: true });
36
+ const tmp = join(
37
+ dirname(path),
38
+ `.${basename(path)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
39
+ );
40
+ try {
41
+ const handle = await open(tmp, "wx");
42
+ try {
43
+ await handle.writeFile(text, "utf8");
44
+ try {
45
+ await handle.sync();
46
+ } catch {
47
+ }
48
+ } finally {
49
+ await handle.close();
50
+ }
51
+ await rename(tmp, path);
52
+ return ok({ changed: true, existed: existing !== null });
53
+ } catch (error) {
54
+ try {
55
+ await unlink(tmp);
56
+ } catch {
57
+ }
58
+ return err("WRITE_FAILED", { path, phase: "atomic-write", message: String(error) });
59
+ }
60
+ }
61
+
62
+ // src/utils/typed-page.ts
63
+ import { lstatSync, realpathSync } from "fs";
64
+ import { dirname as dirname2, posix, relative, resolve, sep } from "path";
65
+
66
+ // src/parsers/frontmatter.ts
67
+ import yaml from "js-yaml";
68
+ var FM_OPEN = /^---\r?\n/;
69
+ function splitFrontmatter(text) {
70
+ if (!FM_OPEN.test(text)) return ok({ rawFrontmatter: "", body: text, bodyStart: 0 });
71
+ const afterOpen = text.replace(FM_OPEN, "");
72
+ const closeIdx = afterOpen.search(/\r?\n---\r?\n/);
73
+ if (closeIdx === -1) return err("MISSING_CLOSING_DELIMITER");
74
+ const rawFrontmatter = afterOpen.slice(0, closeIdx);
75
+ const closeMatch = afterOpen.slice(closeIdx).match(/\r?\n---\r?\n/);
76
+ const bodyStart = text.length - (afterOpen.length - closeIdx - closeMatch[0].length);
77
+ const body = text.slice(bodyStart);
78
+ return ok({ rawFrontmatter, body, bodyStart });
79
+ }
80
+ function extractFrontmatter(text) {
81
+ const split = splitFrontmatter(text);
82
+ if (!split.ok) return split;
83
+ if (!split.data.rawFrontmatter) return ok({});
84
+ try {
85
+ const parsed = yaml.load(split.data.rawFrontmatter, { schema: yaml.JSON_SCHEMA });
86
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return ok({});
87
+ return ok(parsed);
88
+ } catch (e) {
89
+ return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
90
+ }
91
+ }
92
+
93
+ // src/utils/sensitive-content.ts
94
+ import { createHash } from "crypto";
95
+ var REDACTED_RE = /\[REDACTED:[^\]]+\]/i;
96
+ var SYNTHETIC_RE = /^(?:<[^>]+>|\$\{[^}]+\}|REPLACE_WITH_[A-Z0-9_]+|YOUR_[A-Z0-9_]+|EXAMPLE_[A-Z0-9_]+)$/i;
97
+ var MATCHERS = [
98
+ {
99
+ kind: "private_key",
100
+ re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g
101
+ },
102
+ {
103
+ kind: "authorization_header",
104
+ re: /\bAuthorization["']?\s*:\s*["']?(Bearer\s+[A-Za-z0-9._~+/-]{20,})["']?/gi,
105
+ valueGroup: 1
106
+ },
107
+ {
108
+ kind: "cookie",
109
+ re: /\b(?:Cookie|Set-Cookie)\s*:\s*([^\n]{20,})/gi,
110
+ valueGroup: 1
111
+ },
112
+ {
113
+ kind: "jwt",
114
+ re: /\b([A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,})\b/g,
115
+ valueGroup: 1
116
+ },
117
+ {
118
+ kind: "provider_key",
119
+ re: /\b(sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|gh[pousr]_[A-Za-z0-9_=-]{20,})\b/g,
120
+ valueGroup: 1
121
+ },
122
+ {
123
+ kind: "access_key",
124
+ re: /\b((?:AKIA|ASIA)[A-Z0-9]{16})\b/g,
125
+ valueGroup: 1
126
+ },
127
+ {
128
+ kind: "access_key",
129
+ re: /\b(?:access[-_ ]?key|credential)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
130
+ valueGroup: 1
131
+ },
132
+ {
133
+ kind: "api_key",
134
+ re: /\b(?:api[-_ ]?key)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
135
+ valueGroup: 1
136
+ },
137
+ {
138
+ kind: "password",
139
+ re: /\b(?:pass(?:word|wd)?)["']?\s*[:=]\s*["']?([^\s`"']{8,})["']?/gi,
140
+ valueGroup: 1
141
+ },
142
+ {
143
+ kind: "secret",
144
+ re: /\b(?:secret|client[-_ ]?secret)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
145
+ valueGroup: 1
146
+ },
147
+ {
148
+ kind: "token",
149
+ re: /\b(?:token|session)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
150
+ valueGroup: 1
151
+ }
152
+ ];
153
+ function fingerprint(value) {
154
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
155
+ }
156
+ function lineFor(text, offset) {
157
+ return text.slice(0, offset).split(/\r?\n/).length;
158
+ }
159
+ function redactMarker(kind, value) {
160
+ return `[REDACTED:${kind}:${fingerprint(value)}]`;
161
+ }
162
+ function isSyntheticPlaceholder(value) {
163
+ return REDACTED_RE.test(value) || SYNTHETIC_RE.test(value.trim());
164
+ }
165
+ function collectMatches(text) {
166
+ const matches = [];
167
+ for (const matcher of MATCHERS) {
168
+ matcher.re.lastIndex = 0;
169
+ for (const m of text.matchAll(matcher.re)) {
170
+ const whole = m[0];
171
+ const start = m.index ?? 0;
172
+ if (REDACTED_RE.test(whole)) continue;
173
+ const value = matcher.valueGroup ? m[matcher.valueGroup] : whole;
174
+ if (isSyntheticPlaceholder(value)) continue;
175
+ const valueOffset = whole.lastIndexOf(value);
176
+ const valueStart = start + Math.max(0, valueOffset);
177
+ matches.push({
178
+ start,
179
+ end: start + whole.length,
180
+ valueStart,
181
+ valueEnd: valueStart + value.length,
182
+ kind: matcher.kind
183
+ });
184
+ }
185
+ }
186
+ return matches.sort((a, b) => {
187
+ if (a.valueStart !== b.valueStart) return a.valueStart - b.valueStart;
188
+ return b.valueEnd - b.valueStart - (a.valueEnd - a.valueStart);
189
+ });
190
+ }
191
+ function collapseOverlaps(matches) {
192
+ const kept = [];
193
+ for (const match of matches) {
194
+ const overlaps = kept.some((k) => match.valueStart < k.valueEnd && match.valueEnd > k.valueStart);
195
+ if (!overlaps) kept.push(match);
196
+ }
197
+ return kept;
198
+ }
199
+ function scanSensitiveContent(text, opts = {}) {
200
+ return collapseOverlaps(collectMatches(text)).map((match) => {
201
+ const value = text.slice(match.valueStart, match.valueEnd);
202
+ const marker = redactMarker(match.kind, value);
203
+ const rawPreview = text.slice(Math.max(0, match.start - 24), Math.min(text.length, match.end + 24));
204
+ const preview = rawPreview.replace(value, marker);
205
+ return {
206
+ file: opts.file,
207
+ line: lineFor(text, match.valueStart),
208
+ kind: match.kind,
209
+ preview,
210
+ fingerprint: fingerprint(value)
211
+ };
212
+ });
213
+ }
214
+ function redactSensitiveContent(text, opts = {}) {
215
+ const matches = collapseOverlaps(collectMatches(text));
216
+ if (matches.length === 0) return { text, changed: false, findings: [] };
217
+ let out = "";
218
+ let cursor = 0;
219
+ for (const match of matches) {
220
+ const value = text.slice(match.valueStart, match.valueEnd);
221
+ out += text.slice(cursor, match.valueStart);
222
+ out += redactMarker(match.kind, value);
223
+ cursor = match.valueEnd;
224
+ }
225
+ out += text.slice(cursor);
226
+ return {
227
+ text: out,
228
+ changed: out !== text,
229
+ findings: scanSensitiveContent(text, opts)
230
+ };
231
+ }
232
+
233
+ // src/utils/typed-page.ts
234
+ var TYPE_DIRECTORY = {
235
+ entity: "entities",
236
+ concept: "concepts",
237
+ comparison: "comparisons",
238
+ query: "queries",
239
+ meta: "meta"
240
+ };
241
+ function validateTypedTarget(target) {
242
+ const segments = target.split("/");
243
+ if (target.length === 0 || posix.isAbsolute(target) || target.includes("\\") || posix.normalize(target) !== target || segments.some((segment) => segment === "" || segment === "." || segment === "..") || !/^(entities|concepts|comparisons|queries|meta)\/[a-z0-9][a-z0-9._/-]*\.md$/.test(target)) {
244
+ return err("VAULT_PATH_INVALID", { target, message: "unsafe typed-page target" });
245
+ }
246
+ return ok(target);
247
+ }
248
+ function assertTargetInsideVault(vault, target) {
249
+ const validated = validateTypedTarget(target);
250
+ if (!validated.ok) return validated;
251
+ let vaultReal;
252
+ try {
253
+ vaultReal = realpathSync(vault);
254
+ } catch {
255
+ return err("VAULT_PATH_INVALID", { target, message: "vault realpath failed" });
256
+ }
257
+ const absolutePath = resolve(vaultReal, target);
258
+ const parent = dirname2(absolutePath);
259
+ let parentReal;
260
+ try {
261
+ parentReal = realpathSync(parent);
262
+ } catch {
263
+ return err("VAULT_PATH_INVALID", { target, message: "target parent realpath failed" });
264
+ }
265
+ const parentRelative = relative(vaultReal, parentReal).split(sep).join("/");
266
+ if (parentRelative === ".." || parentRelative.startsWith("../")) {
267
+ return err("VAULT_PATH_INVALID", { target, message: "target parent escapes vault" });
268
+ }
269
+ if (parentReal !== parent) {
270
+ return err("VAULT_PATH_INVALID", { target, message: "target parent may not be a symlink alias" });
271
+ }
272
+ let existingRealPath;
273
+ try {
274
+ const targetStat = lstatSync(absolutePath);
275
+ if (targetStat.isSymbolicLink()) {
276
+ return err("VAULT_PATH_INVALID", { target, message: "target may not be a symlink" });
277
+ }
278
+ if (!targetStat.isFile()) {
279
+ return err("VAULT_PATH_INVALID", { target, message: "existing target must be a regular file" });
280
+ }
281
+ try {
282
+ existingRealPath = realpathSync(absolutePath);
283
+ } catch {
284
+ return err("VAULT_PATH_INVALID", { target, message: "target realpath failed" });
285
+ }
286
+ } catch (error) {
287
+ if (error.code !== "ENOENT") {
288
+ return err("VAULT_PATH_INVALID", { target, message: "target lstat failed" });
289
+ }
290
+ }
291
+ return ok({ absolutePath, existingRealPath });
292
+ }
293
+ function invalidFrontmatter(target, issues) {
294
+ return err("INVALID_FRONTMATTER", {
295
+ target,
296
+ errors: issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }))
297
+ });
298
+ }
299
+ function prepareTypedPage(content, target) {
300
+ const safeTarget = validateTypedTarget(target);
301
+ if (!safeTarget.ok) return safeTarget;
302
+ const sensitive = scanSensitiveContent(content, { file: target });
303
+ if (sensitive.length > 0) {
304
+ return err("SENSITIVE_CONTENT_DETECTED", { file: target, findings: sensitive });
305
+ }
306
+ const frontmatter = extractFrontmatter(content);
307
+ if (!frontmatter.ok) return frontmatter;
308
+ const detected = detectSchema(frontmatter.data);
309
+ if (detected.schema === "typed-knowledge") {
310
+ const parsed = TypedKnowledgeSchema.safeParse(frontmatter.data);
311
+ if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
312
+ const expectedDirectory = TYPE_DIRECTORY[parsed.data.type];
313
+ if (!expectedDirectory || !target.startsWith(`${expectedDirectory}/`)) {
314
+ return err("SCHEME_REJECTED", { target, type: parsed.data.type, message: "frontmatter type does not match target directory" });
315
+ }
316
+ return ok({
317
+ target,
318
+ title: parsed.data.title,
319
+ type: parsed.data.type,
320
+ tags: [...parsed.data.tags],
321
+ content
322
+ });
323
+ }
324
+ if (detected.schema === "meta") {
325
+ const parsed = MetaSchema.safeParse(frontmatter.data);
326
+ if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
327
+ if (!target.startsWith("meta/")) {
328
+ return err("SCHEME_REJECTED", { target, type: "meta", message: "frontmatter type does not match target directory" });
329
+ }
330
+ return ok({
331
+ target,
332
+ title: parsed.data.title,
333
+ type: "meta",
334
+ tags: [...parsed.data.tags],
335
+ content
336
+ });
337
+ }
338
+ return invalidFrontmatter(target, []);
339
+ }
340
+
341
+ // src/utils/vault.ts
342
+ import { existsSync, readFileSync } from "fs";
343
+ import { readFile as readFile2, readdir, stat } from "fs/promises";
344
+ import { join as join2, relative as relative2, sep as sep2 } from "path";
345
+ var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
346
+ var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
347
+ var DEFAULT_IO_CONCURRENCY = 1;
348
+ function vaultIoConcurrency() {
349
+ const raw = Number.parseInt(process.env.SKILLWIKI_VAULT_IO_CONCURRENCY ?? "", 10);
350
+ return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 64) : DEFAULT_IO_CONCURRENCY;
351
+ }
352
+ function decodeProcMountPath(value) {
353
+ return value.replace(/\\040/g, " ");
354
+ }
355
+ function isRcloneFuseVaultFromMounts(root, mounts) {
356
+ return mounts.split(/\r?\n/).some((line) => {
357
+ const parts = line.split(" ");
358
+ if (parts.length < 3) return false;
359
+ const mountPoint = decodeProcMountPath(parts[1]);
360
+ const fsType = parts[2];
361
+ return fsType === "fuse.rclone" && (root === mountPoint || root.startsWith(`${mountPoint}/`));
362
+ });
363
+ }
364
+ function resolveReadOnlyVaultRootWithMounts(root, mounts) {
365
+ if (/^(1|true|yes)$/i.test(process.env.SKILLWIKI_DISABLE_VAULT_READ_MIRROR ?? "")) {
366
+ return { root, mirrored: false };
367
+ }
368
+ const explicitMirror = process.env.SKILLWIKI_VAULT_READ_MIRROR;
369
+ if (explicitMirror && existsSync(join2(explicitMirror, "SCHEMA.md"))) {
370
+ return { root: explicitMirror, mirrored: explicitMirror !== root };
371
+ }
372
+ const siblingMirror = `${root}-git`;
373
+ if (isRcloneFuseVaultFromMounts(root, mounts) && existsSync(join2(siblingMirror, "SCHEMA.md"))) {
374
+ return { root: siblingMirror, mirrored: true };
375
+ }
376
+ return { root, mirrored: false };
377
+ }
378
+ function resolveReadOnlyVaultRoot(root) {
379
+ let mounts = "";
380
+ try {
381
+ mounts = readFileSync("/proc/mounts", "utf8");
382
+ } catch {
383
+ }
384
+ return resolveReadOnlyVaultRootWithMounts(root, mounts);
385
+ }
386
+ async function mapWithConcurrency(items, limit, mapper) {
387
+ const out = new Array(items.length);
388
+ let next = 0;
389
+ const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
390
+ for (; ; ) {
391
+ const index = next++;
392
+ if (index >= items.length) return;
393
+ out[index] = await mapper(items[index], index);
394
+ }
395
+ });
396
+ await Promise.all(workers);
397
+ return out;
398
+ }
399
+ async function scanVault(root) {
400
+ try {
401
+ await stat(join2(root, "SCHEMA.md"));
402
+ } catch {
403
+ return err("VAULT_PATH_INVALID", { root, reason: "SCHEMA.md missing" });
404
+ }
405
+ const all = await walk(root);
406
+ const rels = all.map((p) => ({ absPath: p, relPath: relative2(root, p).split(sep2).join("/") }));
407
+ return ok({
408
+ root,
409
+ allMarkdown: rels,
410
+ typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
411
+ raw: rels.filter((p) => p.relPath.startsWith("raw/")),
412
+ workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
413
+ compound: rels.filter((p) => /^projects\/[^/]+\/compound\//.test(p.relPath))
414
+ });
415
+ }
416
+ async function walk(dir) {
417
+ const entries = await readdir(dir, { withFileTypes: true });
418
+ const out = [];
419
+ const subdirs = [];
420
+ for (const e of entries) {
421
+ const p = join2(dir, e.name);
422
+ if (e.isDirectory()) {
423
+ if (SKIP_DIRS.has(e.name)) continue;
424
+ subdirs.push(p);
425
+ } else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
426
+ }
427
+ const nested = await mapWithConcurrency(subdirs, Math.min(8, vaultIoConcurrency()), walk);
428
+ for (const files of nested) out.push(...files);
429
+ return out;
430
+ }
431
+ async function readPage(p) {
432
+ return readFile2(p.absPath, "utf8");
433
+ }
434
+ async function readPageCached(p, cache) {
435
+ if (!cache) return readPage(p);
436
+ const existing = cache.get(p.absPath);
437
+ if (existing) return existing;
438
+ const pending = readPage(p);
439
+ cache.set(p.absPath, pending);
440
+ return pending;
441
+ }
442
+
443
+ // src/utils/index-projection.ts
444
+ var SECTION_ORDER = ["Entities", "Concepts", "Comparisons", "Queries", "Meta", "Projects"];
445
+ var TYPE_SECTION = {
446
+ entity: "Entities",
447
+ concept: "Concepts",
448
+ comparison: "Comparisons",
449
+ query: "Queries",
450
+ meta: "Meta"
451
+ };
452
+ var compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
453
+ var UNMANAGED_START = "<!-- skillwiki:index-unmanaged:start -->";
454
+ var UNMANAGED_END = "<!-- skillwiki:index-unmanaged:end -->";
455
+ function extractUnmanaged(currentText) {
456
+ const startCount = currentText.split(UNMANAGED_START).length - 1;
457
+ const endCount = currentText.split(UNMANAGED_END).length - 1;
458
+ if (startCount === 0 && endCount === 0) return ok("");
459
+ if (startCount !== 1 || endCount !== 1) {
460
+ return err("SCHEME_REJECTED", {
461
+ message: "index.md must contain exactly one complete unmanaged marker pair"
462
+ });
463
+ }
464
+ const start = currentText.indexOf(UNMANAGED_START);
465
+ const end = currentText.indexOf(UNMANAGED_END);
466
+ if (start < 0 || end < 0 || end < start) {
467
+ return err("SCHEME_REJECTED", {
468
+ message: "unmanaged marker pair is reversed or incomplete"
469
+ });
470
+ }
471
+ const body = currentText.slice(start + UNMANAGED_START.length, end).replace(/^\n/, "").replace(/\n$/, "");
472
+ return ok(body);
473
+ }
474
+ function priorWikilinkTargets(text) {
475
+ const out = [];
476
+ const re = /\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]/g;
477
+ let m;
478
+ while ((m = re.exec(text)) !== null) {
479
+ out.push(m[1].trim().replace(/\.md$/, ""));
480
+ }
481
+ return out;
482
+ }
483
+ function projectTitleFromReadme(text, slug) {
484
+ const m = text.match(/^#\s+Project:\s+(.+)$/m);
485
+ return m?.[1]?.trim() || slug;
486
+ }
487
+ async function renderRootIndex(input) {
488
+ const vault = input.vault;
489
+ let currentText = input.currentText;
490
+ if (currentText === void 0) {
491
+ try {
492
+ currentText = await readFile3(join3(vault, "index.md"), "utf8");
493
+ } catch {
494
+ currentText = "";
495
+ }
496
+ }
497
+ const unmanaged = extractUnmanaged(currentText);
498
+ if (!unmanaged.ok) return unmanaged;
499
+ const scan = await scanVault(vault);
500
+ if (!scan.ok) return scan;
501
+ const entries = [];
502
+ const seen = /* @__PURE__ */ new Map();
503
+ let duplicatesRemoved = 0;
504
+ for (const page of scan.data.typedKnowledge) {
505
+ let text;
506
+ try {
507
+ text = await readFile3(join3(vault, page.relPath), "utf8");
508
+ } catch {
509
+ continue;
510
+ }
511
+ const prepared = prepareTypedPage(text, page.relPath);
512
+ if (!prepared.ok) continue;
513
+ const section = TYPE_SECTION[prepared.data.type];
514
+ if (!section) continue;
515
+ const target = prepared.data.target.replace(/\.md$/, "");
516
+ const prev = seen.get(target);
517
+ if (prev !== void 0) {
518
+ if (prev !== prepared.data.title) {
519
+ return err("SCHEME_REJECTED", {
520
+ message: `duplicate index target ${target} with differing titles`,
521
+ titles: [prev, prepared.data.title]
522
+ });
523
+ }
524
+ duplicatesRemoved += 1;
525
+ continue;
526
+ }
527
+ seen.set(target, prepared.data.title);
528
+ entries.push({
529
+ section,
530
+ target,
531
+ title: prepared.data.title,
532
+ source: "typed"
533
+ });
534
+ }
535
+ try {
536
+ const projectsRoot = join3(vault, "projects");
537
+ for (const slug of readdirSync(projectsRoot, { withFileTypes: true })) {
538
+ if (!slug.isDirectory()) continue;
539
+ const readmePath = join3(projectsRoot, slug.name, "README.md");
540
+ let text;
541
+ try {
542
+ text = readFileSync2(readmePath, "utf8");
543
+ } catch {
544
+ continue;
545
+ }
546
+ const target = `projects/${slug.name}/README`;
547
+ const title = projectTitleFromReadme(text, slug.name);
548
+ const prev = seen.get(target);
549
+ if (prev !== void 0) {
550
+ if (prev !== title) {
551
+ return err("SCHEME_REJECTED", {
552
+ message: `duplicate index target ${target} with differing titles`
553
+ });
554
+ }
555
+ duplicatesRemoved += 1;
556
+ continue;
557
+ }
558
+ seen.set(target, title);
559
+ entries.push({ section: "Projects", target, title, source: "project" });
560
+ }
561
+ } catch {
562
+ }
563
+ entries.sort((a, b) => {
564
+ const sa = SECTION_ORDER.indexOf(a.section);
565
+ const sb = SECTION_ORDER.indexOf(b.section);
566
+ if (sa !== sb) return sa - sb;
567
+ return compareText(a.target, b.target);
568
+ });
569
+ const prior = priorWikilinkTargets(currentText);
570
+ const generatedTargets = new Set(entries.map((e) => e.target));
571
+ const priorLower = /* @__PURE__ */ new Map();
572
+ for (const t of prior) {
573
+ const k = t.toLowerCase();
574
+ priorLower.set(k, (priorLower.get(k) ?? 0) + 1);
575
+ }
576
+ for (const count of priorLower.values()) {
577
+ if (count > 1) duplicatesRemoved += count - 1;
578
+ }
579
+ const headingDupes = (currentText.match(/^##\s+(entities|concepts|comparisons|queries|meta|projects)\s*$/gim) ?? []).map((h) => h.replace(/^##\s+/i, "").trim().toLowerCase());
580
+ const headingCounts = /* @__PURE__ */ new Map();
581
+ for (const h of headingDupes) headingCounts.set(h, (headingCounts.get(h) ?? 0) + 1);
582
+ for (const c of headingCounts.values()) {
583
+ if (c > 1) duplicatesRemoved += c - 1;
584
+ }
585
+ const ghostsRemoved = [
586
+ ...new Set(
587
+ prior.filter((t) => {
588
+ const bare = t.includes("/") ? t : null;
589
+ if (bare && !generatedTargets.has(bare) && !generatedTargets.has(t)) {
590
+ if (!seen.has(t)) return true;
591
+ }
592
+ if (!t.includes("/")) {
593
+ const matches = [...generatedTargets].filter((g) => g.split("/").pop() === t);
594
+ return matches.length === 0;
595
+ }
596
+ return !generatedTargets.has(t);
597
+ })
598
+ )
599
+ ];
600
+ const lines = [
601
+ "# Vault Index",
602
+ "",
603
+ "Generated by `skillwiki index rebuild`. Generated sections are derived from typed-page frontmatter and project manifests.",
604
+ ""
605
+ ];
606
+ for (const section of SECTION_ORDER) {
607
+ lines.push(`## ${section}`, "");
608
+ const sectionEntries = entries.filter((e) => e.section === section);
609
+ for (const e of sectionEntries) {
610
+ lines.push(`- [[${e.target}]] \u2014 ${e.title}`);
611
+ }
612
+ if (sectionEntries.length > 0) lines.push("");
613
+ }
614
+ lines.push(UNMANAGED_START);
615
+ if (unmanaged.data) {
616
+ lines.push(unmanaged.data);
617
+ }
618
+ lines.push(UNMANAGED_END);
619
+ lines.push("");
620
+ return ok({
621
+ text: lines.join("\n"),
622
+ entries,
623
+ duplicates_removed: duplicatesRemoved,
624
+ ghosts_removed: ghostsRemoved
625
+ });
626
+ }
627
+ async function writeRootIndexProjection(vault, projection) {
628
+ return atomicWriteText(join3(vault, "index.md"), projection.text);
629
+ }
630
+
631
+ export {
632
+ splitFrontmatter,
633
+ extractFrontmatter,
634
+ scanSensitiveContent,
635
+ redactSensitiveContent,
636
+ atomicWriteText,
637
+ assertTargetInsideVault,
638
+ prepareTypedPage,
639
+ vaultIoConcurrency,
640
+ resolveReadOnlyVaultRoot,
641
+ mapWithConcurrency,
642
+ scanVault,
643
+ readPage,
644
+ readPageCached,
645
+ renderRootIndex,
646
+ writeRootIndexProjection
647
+ };