skillwiki 0.0.1 → 0.2.0-beta.10

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 ADDED
@@ -0,0 +1,1937 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync } from "fs";
5
+ import { Command } from "commander";
6
+
7
+ // src/utils/output.ts
8
+ function printJson(r) {
9
+ process.stdout.write(JSON.stringify(r) + "\n");
10
+ }
11
+ function printHuman(r) {
12
+ if (r.ok) {
13
+ if (typeof r.data === "object" && r.data !== null && "humanHint" in r.data) {
14
+ process.stdout.write(`${r.data.humanHint}
15
+ `);
16
+ } else {
17
+ process.stdout.write(`OK
18
+ ${formatData(r.data)}
19
+ `);
20
+ }
21
+ } else {
22
+ process.stdout.write(`ERR ${r.error}
23
+ ${r.detail !== void 0 ? formatData(r.detail) + "\n" : ""}`);
24
+ }
25
+ }
26
+ function formatData(d) {
27
+ if (d == null) return "";
28
+ if (typeof d === "string") return d;
29
+ return JSON.stringify(d, null, 2);
30
+ }
31
+
32
+ // src/commands/hash.ts
33
+ import { readFile } from "fs/promises";
34
+ import { createHash } from "crypto";
35
+
36
+ // ../shared/src/exit-codes.ts
37
+ var ExitCode = {
38
+ OK: 0,
39
+ FILE_NOT_FOUND: 2,
40
+ MISSING_CLOSING_DELIMITER: 3,
41
+ SCHEME_REJECTED: 4,
42
+ HOST_BLOCKED: 5,
43
+ MALFORMED_URL: 6,
44
+ INVALID_FRONTMATTER: 7,
45
+ SCHEMA_NOT_DETECTED: 8,
46
+ VAULT_PATH_INVALID: 9,
47
+ WRITE_FAILED: 10,
48
+ UNRESOLVED_MARKERS: 11,
49
+ SOURCES_INCONSISTENT: 12,
50
+ PREFLIGHT_FAILED: 13,
51
+ ATOMIC_COPY_FAILED: 14,
52
+ INIT_TARGET_NOT_EMPTY: 15,
53
+ BROKEN_WIKILINKS: 16,
54
+ TAG_NOT_IN_TAXONOMY: 17,
55
+ INDEX_INCOMPLETE: 18,
56
+ STALE_PAGE: 19,
57
+ PAGE_TOO_LARGE: 20,
58
+ LOG_ROTATE_NEEDED: 21,
59
+ LINT_HAS_WARNINGS: 22,
60
+ LINT_HAS_ERRORS: 23,
61
+ ENV_WRITE_CONFLICT: 24,
62
+ NO_VAULT_CONFIGURED: 25,
63
+ INVALID_CONFIG_KEY: 26,
64
+ CONFIG_WRITE_FAILED: 27,
65
+ DOCTOR_HAS_WARNINGS: 28,
66
+ DOCTOR_HAS_ERRORS: 29,
67
+ ARCHIVE_TARGET_NOT_FOUND: 30,
68
+ ARCHIVE_ALREADY_ARCHIVED: 31,
69
+ DRIFT_DETECTED: 32,
70
+ RAW_DEDUP_DETECTED: 33
71
+ };
72
+
73
+ // ../shared/src/json-output.ts
74
+ function ok(data) {
75
+ return { ok: true, data };
76
+ }
77
+ function err(error, detail) {
78
+ return detail === void 0 ? { ok: false, error } : { ok: false, error, detail };
79
+ }
80
+
81
+ // ../shared/src/schemas.ts
82
+ import { z } from "zod";
83
+ var isoDate = z.string().refine((s) => {
84
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
85
+ const d = /* @__PURE__ */ new Date(s + "T00:00:00Z");
86
+ return !Number.isNaN(d.getTime()) && s === d.toISOString().slice(0, 10);
87
+ }, { message: "must be YYYY-MM-DD" });
88
+ var wikilink = z.string().regex(/^\[\[[^\[\]]+\]\]$/, 'must be "[[name]]"');
89
+ var TypedKnowledgeSchema = z.object({
90
+ title: z.string().min(1),
91
+ aliases: z.array(z.string()).optional(),
92
+ created: isoDate,
93
+ updated: isoDate,
94
+ type: z.enum(["entity", "concept", "comparison", "query", "summary"]),
95
+ tags: z.array(z.string()),
96
+ sources: z.array(z.string()).min(1),
97
+ confidence: z.enum(["high", "medium", "low"]).optional(),
98
+ contested: z.boolean().optional(),
99
+ contradictions: z.array(z.string()).optional(),
100
+ provenance: z.enum(["research", "project", "mixed"]).optional(),
101
+ provenance_projects: z.array(wikilink).optional(),
102
+ work_items: z.array(wikilink).optional()
103
+ }).superRefine((v, ctx) => {
104
+ if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
105
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
106
+ }
107
+ });
108
+ var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
109
+ var RawSourceSchema = z.object({
110
+ title: z.string().min(1),
111
+ source_url: z.string().url().nullable(),
112
+ ingested: isoDate,
113
+ ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]),
114
+ sha256: sha256Hex,
115
+ project: wikilink.optional(),
116
+ work_item: wikilink.optional(),
117
+ kind: z.enum(["postmortem", "session-log", "meeting-notes", "other"]).optional()
118
+ }).superRefine((v, ctx) => {
119
+ const projectFields = [v.project, v.work_item, v.kind];
120
+ const present = projectFields.filter((x) => x !== void 0).length;
121
+ if (present !== 0 && present !== 3) {
122
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "project, work_item, kind must all be set together" });
123
+ }
124
+ });
125
+ var WorkItemSchema = z.object({
126
+ title: z.string().min(1),
127
+ aliases: z.array(z.string()).optional(),
128
+ created: isoDate,
129
+ updated: isoDate,
130
+ started: isoDate,
131
+ completed: isoDate.optional(),
132
+ kind: z.enum(["feature", "issue", "refactor", "decision"]),
133
+ status: z.enum(["planned", "in-progress", "completed", "abandoned"]),
134
+ priority: z.enum(["high", "medium", "low"]),
135
+ project: wikilink,
136
+ owner: wikilink.optional(),
137
+ parent: wikilink.optional(),
138
+ related: z.array(wikilink).optional(),
139
+ sources: z.array(z.string()).optional()
140
+ }).superRefine((v, ctx) => {
141
+ if (v.status === "completed" && !v.completed) {
142
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["completed"], message: "required when status is completed" });
143
+ }
144
+ });
145
+ var CompoundSchema = z.object({
146
+ title: z.string().min(1),
147
+ aliases: z.array(z.string()).optional(),
148
+ created: isoDate,
149
+ updated: isoDate,
150
+ type: z.enum(["lesson", "pattern", "antipattern", "gotcha"]),
151
+ tags: z.array(z.string()),
152
+ confidence: z.enum(["high", "medium", "low"]),
153
+ contradicts: z.array(z.string()).optional(),
154
+ project: wikilink,
155
+ work_items: z.array(wikilink).min(1),
156
+ promoted_to: wikilink.optional(),
157
+ cssclasses: z.array(z.string()).optional()
158
+ });
159
+ function detectSchema(fm) {
160
+ const COMPOUND_TYPES = /* @__PURE__ */ new Set(["lesson", "pattern", "antipattern", "gotcha"]);
161
+ if (typeof fm.type === "string" && COMPOUND_TYPES.has(fm.type) && "project" in fm) return { schema: "compound" };
162
+ if ("type" in fm && "sources" in fm) return { schema: "typed-knowledge" };
163
+ if (typeof fm.sha256 === "string" && "ingested" in fm) return { schema: "raw" };
164
+ if ("kind" in fm && "status" in fm) return { schema: "work-item" };
165
+ return { schema: null };
166
+ }
167
+
168
+ // ../shared/src/blocked-hosts.ts
169
+ var METADATA_HOSTS = [
170
+ "metadata.google.internal",
171
+ "metadata"
172
+ ];
173
+ var METADATA_IPS = /* @__PURE__ */ new Set(["169.254.169.254"]);
174
+ function ipv4ToInt(ip) {
175
+ const parts = ip.split(".");
176
+ if (parts.length !== 4) return null;
177
+ let n = 0;
178
+ for (const p of parts) {
179
+ const v = Number(p);
180
+ if (!Number.isInteger(v) || v < 0 || v > 255) return null;
181
+ n = (n << 8) + v;
182
+ }
183
+ return n >>> 0;
184
+ }
185
+ function inRange(ip, baseStr, prefix) {
186
+ const ipN = ipv4ToInt(ip);
187
+ const baseN = ipv4ToInt(baseStr);
188
+ if (ipN === null || baseN === null) return false;
189
+ const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
190
+ return (ipN & mask) === (baseN & mask);
191
+ }
192
+ function isBlockedHost(host) {
193
+ const lower = host.toLowerCase();
194
+ if (METADATA_HOSTS.includes(lower)) return true;
195
+ if (METADATA_IPS.has(host)) return true;
196
+ if (lower === "::1") return true;
197
+ if (lower.startsWith("fe80:")) return true;
198
+ if (ipv4ToInt(host) === null) return false;
199
+ if (inRange(host, "10.0.0.0", 8)) return true;
200
+ if (inRange(host, "172.16.0.0", 12)) return true;
201
+ if (inRange(host, "192.168.0.0", 16)) return true;
202
+ if (inRange(host, "169.254.0.0", 16)) return true;
203
+ if (inRange(host, "127.0.0.0", 8)) return true;
204
+ return false;
205
+ }
206
+
207
+ // src/parsers/frontmatter.ts
208
+ import yaml from "js-yaml";
209
+ var FM_OPEN = /^---\r?\n/;
210
+ function splitFrontmatter(text) {
211
+ if (!FM_OPEN.test(text)) return ok({ rawFrontmatter: "", body: text, bodyStart: 0 });
212
+ const afterOpen = text.replace(FM_OPEN, "");
213
+ const closeIdx = afterOpen.search(/\r?\n---\r?\n/);
214
+ if (closeIdx === -1) return err("MISSING_CLOSING_DELIMITER");
215
+ const rawFrontmatter = afterOpen.slice(0, closeIdx);
216
+ const closeMatch = afterOpen.slice(closeIdx).match(/\r?\n---\r?\n/);
217
+ const bodyStart = text.length - (afterOpen.length - closeIdx - closeMatch[0].length);
218
+ const body = text.slice(bodyStart);
219
+ return ok({ rawFrontmatter, body, bodyStart });
220
+ }
221
+ function extractFrontmatter(text) {
222
+ const split = splitFrontmatter(text);
223
+ if (!split.ok) return split;
224
+ if (!split.data.rawFrontmatter) return ok({});
225
+ try {
226
+ const parsed = yaml.load(split.data.rawFrontmatter, { schema: yaml.JSON_SCHEMA });
227
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return ok({});
228
+ return ok(parsed);
229
+ } catch (e) {
230
+ return err("INVALID_FRONTMATTER", { message: e.message });
231
+ }
232
+ }
233
+
234
+ // src/commands/hash.ts
235
+ async function runHash(input) {
236
+ let text;
237
+ try {
238
+ text = await readFile(input.file, "utf8");
239
+ } catch {
240
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
241
+ }
242
+ const split = splitFrontmatter(text);
243
+ if (!split.ok) return { exitCode: ExitCode.MISSING_CLOSING_DELIMITER, result: split };
244
+ const bodyBytes = Buffer.from(split.data.body, "utf8");
245
+ const sha256 = createHash("sha256").update(bodyBytes).digest("hex");
246
+ return {
247
+ exitCode: ExitCode.OK,
248
+ result: ok({ path: input.file, sha256, byte_count: bodyBytes.byteLength, humanHint: sha256 })
249
+ };
250
+ }
251
+
252
+ // src/commands/fetch-guard.ts
253
+ var REDACT_PARAMS = /* @__PURE__ */ new Set(["api_key", "token", "key", "auth", "password", "secret", "access_token"]);
254
+ var PATH_TOKEN_RE = /[A-Fa-f0-9]{32,}|[A-Za-z0-9_\-]{40,}/g;
255
+ function runFetchGuard(input) {
256
+ return Promise.resolve(runFetchGuardSync(input));
257
+ }
258
+ function runFetchGuardSync(input) {
259
+ let u;
260
+ try {
261
+ u = new URL(input.url);
262
+ } catch {
263
+ return { exitCode: ExitCode.MALFORMED_URL, result: err("MALFORMED_URL", { url: input.url }) };
264
+ }
265
+ const sanitized = sanitizeUrl(u);
266
+ if (u.protocol !== "https:") {
267
+ return {
268
+ exitCode: ExitCode.SCHEME_REJECTED,
269
+ result: err("SCHEME_REJECTED", { sanitized_url: sanitized, scheme: u.protocol })
270
+ };
271
+ }
272
+ if (isBlockedHost(u.hostname)) {
273
+ return {
274
+ exitCode: ExitCode.HOST_BLOCKED,
275
+ result: err("HOST_BLOCKED", { sanitized_url: sanitized, host: u.hostname })
276
+ };
277
+ }
278
+ return { exitCode: ExitCode.OK, result: ok({ allowed: true, sanitized_url: sanitized, humanHint: `ALLOWED: ${sanitized}` }) };
279
+ }
280
+ function sanitizeUrl(u) {
281
+ const clone = new URL(u.toString());
282
+ if (clone.username || clone.password) {
283
+ clone.username = "";
284
+ clone.password = "";
285
+ }
286
+ for (const k of Array.from(clone.searchParams.keys())) {
287
+ if (REDACT_PARAMS.has(k.toLowerCase())) clone.searchParams.set(k, "REDACTED");
288
+ }
289
+ let s = clone.toString();
290
+ s = s.replace(PATH_TOKEN_RE, "REDACTED");
291
+ return s;
292
+ }
293
+
294
+ // src/commands/validate.ts
295
+ import { readFile as readFile2 } from "fs/promises";
296
+ var SCHEMAS = {
297
+ "typed-knowledge": TypedKnowledgeSchema,
298
+ "raw": RawSourceSchema,
299
+ "work-item": WorkItemSchema,
300
+ "compound": CompoundSchema
301
+ };
302
+ async function runValidate(input) {
303
+ let text;
304
+ try {
305
+ text = await readFile2(input.file, "utf8");
306
+ } catch {
307
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
308
+ }
309
+ const fm = extractFrontmatter(text);
310
+ if (!fm.ok) {
311
+ if (fm.error === "MISSING_CLOSING_DELIMITER") {
312
+ return { exitCode: ExitCode.MISSING_CLOSING_DELIMITER, result: fm };
313
+ }
314
+ return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
315
+ }
316
+ const det = detectSchema(fm.data);
317
+ if (!det.schema) {
318
+ return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], humanHint: "schema not detected" }) };
319
+ }
320
+ const parsed = SCHEMAS[det.schema].safeParse(fm.data);
321
+ if (!parsed.success) {
322
+ const errors = parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }));
323
+ return {
324
+ exitCode: ExitCode.INVALID_FRONTMATTER,
325
+ result: ok({ schema: det.schema, valid: false, errors, humanHint: `INVALID (${det.schema})
326
+ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
327
+ };
328
+ }
329
+ return { exitCode: ExitCode.OK, result: ok({ schema: det.schema, valid: true, errors: [], humanHint: `VALID (${det.schema})` }) };
330
+ }
331
+
332
+ // src/commands/graph.ts
333
+ import { writeFile, mkdir } from "fs/promises";
334
+ import { dirname } from "path";
335
+
336
+ // src/utils/vault.ts
337
+ import { readFile as readFile3, readdir, stat } from "fs/promises";
338
+ import { join, relative, sep } from "path";
339
+ var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries"];
340
+ async function scanVault(root) {
341
+ try {
342
+ await stat(join(root, "SCHEMA.md"));
343
+ } catch {
344
+ return err("VAULT_PATH_INVALID", { root, reason: "SCHEMA.md missing" });
345
+ }
346
+ const all = await walk(root);
347
+ const rels = all.map((p) => ({ absPath: p, relPath: relative(root, p).split(sep).join("/") }));
348
+ return ok({
349
+ root,
350
+ typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
351
+ raw: rels.filter((p) => p.relPath.startsWith("raw/")),
352
+ workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
353
+ compound: rels.filter((p) => /^projects\/[^/]+\/compound\//.test(p.relPath))
354
+ });
355
+ }
356
+ async function walk(dir) {
357
+ const entries = await readdir(dir, { withFileTypes: true });
358
+ const out = [];
359
+ for (const e of entries) {
360
+ const p = join(dir, e.name);
361
+ if (e.isDirectory()) out.push(...await walk(p));
362
+ else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
363
+ }
364
+ return out;
365
+ }
366
+ async function readPage(p) {
367
+ return readFile3(p.absPath, "utf8");
368
+ }
369
+
370
+ // src/parsers/wikilinks.ts
371
+ var FENCE = /`[^`]*`|```[\s\S]*?```/g;
372
+ function extractBodyWikilinks(body) {
373
+ const stripped = body.replace(FENCE, "");
374
+ const seen = /* @__PURE__ */ new Set();
375
+ const out = [];
376
+ const re = /\[\[([^\[\]|]+)(?:\|[^\[\]]*)?\]\]/g;
377
+ let m;
378
+ while ((m = re.exec(stripped)) !== null) {
379
+ const target = m[1].trim();
380
+ if (!seen.has(target)) {
381
+ seen.add(target);
382
+ out.push(target);
383
+ }
384
+ }
385
+ return out;
386
+ }
387
+
388
+ // src/commands/graph.ts
389
+ async function runGraphBuild(input) {
390
+ const scan = await scanVault(input.vault);
391
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
392
+ const adjacency = {};
393
+ const slugToPath = {};
394
+ for (const p of scan.data.typedKnowledge) {
395
+ const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
396
+ slugToPath[slug] = p.relPath;
397
+ }
398
+ for (const p of scan.data.typedKnowledge) {
399
+ const text = await readPage(p);
400
+ const split = splitFrontmatter(text);
401
+ const body = split.ok ? split.data.body : text;
402
+ const links = extractBodyWikilinks(body);
403
+ adjacency[p.relPath] = links.map((slug) => slugToPath[slug.split("/").pop()]).filter((x) => Boolean(x));
404
+ }
405
+ const adamicAdar = computeAdamicAdar(adjacency);
406
+ const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
407
+ try {
408
+ await mkdir(dirname(input.out), { recursive: true });
409
+ await writeFile(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
410
+ } catch (e) {
411
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
412
+ }
413
+ return {
414
+ exitCode: ExitCode.OK,
415
+ result: ok({ out_path: input.out, node_count: scan.data.typedKnowledge.length, edge_count, humanHint: `nodes: ${scan.data.typedKnowledge.length}, edges: ${edge_count}
416
+ written: ${input.out}` })
417
+ };
418
+ }
419
+ function computeAdamicAdar(adj) {
420
+ const undirected = {};
421
+ for (const [a, neighbors] of Object.entries(adj)) {
422
+ undirected[a] ??= /* @__PURE__ */ new Set();
423
+ for (const b of neighbors) {
424
+ undirected[a].add(b);
425
+ undirected[b] ??= /* @__PURE__ */ new Set();
426
+ undirected[b].add(a);
427
+ }
428
+ }
429
+ const nodes = Object.keys(undirected);
430
+ const out = {};
431
+ for (let i = 0; i < nodes.length; i++) {
432
+ for (let j = i + 1; j < nodes.length; j++) {
433
+ const a = nodes[i], b = nodes[j];
434
+ const common = [...undirected[a]].filter((x) => undirected[b].has(x));
435
+ let score = 0;
436
+ for (const c of common) {
437
+ const deg = undirected[c].size;
438
+ if (deg > 1) score += 1 / Math.log(deg);
439
+ }
440
+ if (score > 0) {
441
+ out[a] ??= {};
442
+ out[a][b] = score;
443
+ out[b] ??= {};
444
+ out[b][a] = score;
445
+ }
446
+ }
447
+ }
448
+ return out;
449
+ }
450
+
451
+ // src/commands/overlap.ts
452
+ async function runOverlap(input) {
453
+ const scan = await scanVault(input.vault);
454
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
455
+ const sourcesByPage = {};
456
+ for (const p of scan.data.typedKnowledge) {
457
+ const fm = extractFrontmatter(await readPage(p));
458
+ if (!fm.ok) continue;
459
+ const srcs = fm.data.sources ?? [];
460
+ sourcesByPage[p.relPath] = new Set(srcs);
461
+ }
462
+ const parent = {};
463
+ for (const k of Object.keys(sourcesByPage)) parent[k] = k;
464
+ const find = (x) => parent[x] === x ? x : parent[x] = find(parent[x]);
465
+ const union = (a, b) => {
466
+ const ra = find(a), rb = find(b);
467
+ if (ra !== rb) parent[ra] = rb;
468
+ };
469
+ const pages = Object.keys(sourcesByPage);
470
+ for (let i = 0; i < pages.length; i++) {
471
+ for (let j = i + 1; j < pages.length; j++) {
472
+ const sa = sourcesByPage[pages[i]], sb = sourcesByPage[pages[j]];
473
+ const shared = [...sa].filter((x) => sb.has(x)).length;
474
+ if (shared > 0) union(pages[i], pages[j]);
475
+ }
476
+ }
477
+ const groups = {};
478
+ for (const p of pages) {
479
+ const r = find(p);
480
+ (groups[r] ??= []).push(p);
481
+ }
482
+ const clusters = Object.entries(groups).filter(([, m]) => m.length > 1).map(([id, members]) => {
483
+ let score = 0;
484
+ for (let i = 0; i < members.length; i++)
485
+ for (let j = i + 1; j < members.length; j++) {
486
+ const sa = sourcesByPage[members[i]], sb = sourcesByPage[members[j]];
487
+ score += [...sa].filter((x) => sb.has(x)).length;
488
+ }
489
+ return { id, members, score };
490
+ });
491
+ const humanHint = clusters.length === 0 ? "no overlap clusters found" : clusters.map((c) => `cluster (${c.members.length} pages, score ${c.score}): ${c.members.join(", ")}`).join("\n");
492
+ return { exitCode: ExitCode.OK, result: ok({ clusters, humanHint }) };
493
+ }
494
+
495
+ // src/utils/wiki-path.ts
496
+ import { join as join2 } from "path";
497
+
498
+ // src/utils/dotenv.ts
499
+ import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
500
+ import { dirname as dirname2 } from "path";
501
+ var CONFIG_KEYS = ["WIKI_PATH", "WIKI_LANG"];
502
+ var _whitelist = new Set(CONFIG_KEYS);
503
+ function parseDotenvText(text) {
504
+ const out = {};
505
+ for (const rawLine of text.split(/\r?\n/)) {
506
+ const line = rawLine.trim();
507
+ if (line.length === 0 || line.startsWith("#")) continue;
508
+ const eq = line.indexOf("=");
509
+ if (eq <= 0) continue;
510
+ const key = line.slice(0, eq).trim();
511
+ const value = line.slice(eq + 1).trim();
512
+ if (!_whitelist.has(key)) continue;
513
+ if (value.length === 0) continue;
514
+ out[key] = value;
515
+ }
516
+ return out;
517
+ }
518
+ async function parseDotenvFile(path) {
519
+ let text;
520
+ try {
521
+ text = await readFile4(path, "utf8");
522
+ } catch {
523
+ return {};
524
+ }
525
+ return parseDotenvText(text);
526
+ }
527
+ async function writeDotenv(filePath, entries, originalContent) {
528
+ const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
529
+ await mkdir2(dirname2(filePath), { recursive: true });
530
+ await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
531
+ }
532
+ function freshLines(entries) {
533
+ const out = [];
534
+ for (const [key, value] of Object.entries(entries)) {
535
+ if (value !== void 0) out.push(`${key}=${value}`);
536
+ }
537
+ return out;
538
+ }
539
+ function updateLines(originalContent, entries) {
540
+ let rawLines = originalContent.split(/\r?\n/);
541
+ if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
542
+ rawLines = rawLines.slice(0, -1);
543
+ }
544
+ const keysToWrite = new Set(Object.keys(entries));
545
+ const out = [];
546
+ for (const line of rawLines) {
547
+ const trimmed = line.trim();
548
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
549
+ out.push(line);
550
+ continue;
551
+ }
552
+ const eq = trimmed.indexOf("=");
553
+ if (eq <= 0) {
554
+ out.push(line);
555
+ continue;
556
+ }
557
+ const key = trimmed.slice(0, eq).trim();
558
+ if (keysToWrite.has(key)) {
559
+ out.push(`${key}=${entries[key]}`);
560
+ keysToWrite.delete(key);
561
+ } else {
562
+ out.push(line);
563
+ }
564
+ }
565
+ for (const key of keysToWrite) {
566
+ const value = entries[key];
567
+ if (value !== void 0) out.push(`${key}=${value}`);
568
+ }
569
+ return out;
570
+ }
571
+
572
+ // src/utils/wiki-path.ts
573
+ async function resolveInitTimePath(input) {
574
+ const chain = [];
575
+ if (input.flag !== void 0 && input.flag.length > 0) {
576
+ if (input.explain) chain.push({ source: "flag", matched: true, value: input.flag });
577
+ return { path: input.flag, source: "flag", ...input.explain ? { chain } : {} };
578
+ }
579
+ if (input.explain) chain.push({ source: "flag", matched: false });
580
+ if (input.envValue !== void 0 && input.envValue.length > 0) {
581
+ if (input.explain) chain.push({ source: "env", matched: true, value: input.envValue });
582
+ return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
583
+ }
584
+ if (input.explain) chain.push({ source: "env", matched: false });
585
+ const sw = await parseDotenvFile(join2(input.home, ".skillwiki", ".env"));
586
+ if (sw.WIKI_PATH !== void 0) {
587
+ if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
588
+ return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
589
+ }
590
+ if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
591
+ const hermes = await parseDotenvFile(join2(input.home, ".hermes", ".env"));
592
+ if (hermes.WIKI_PATH !== void 0) {
593
+ if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
594
+ return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
595
+ }
596
+ if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
597
+ const fallback = join2(input.home, "wiki");
598
+ if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
599
+ return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
600
+ }
601
+ async function resolveRuntimePath(input) {
602
+ const chain = [];
603
+ if (input.flag !== void 0 && input.flag.length > 0) {
604
+ if (input.explain) chain.push({ source: "flag", matched: true, value: input.flag });
605
+ return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
606
+ }
607
+ if (input.explain) chain.push({ source: "flag", matched: false });
608
+ if (input.envValue !== void 0 && input.envValue.length > 0) {
609
+ if (input.explain) chain.push({ source: "env", matched: true, value: input.envValue });
610
+ return ok({ path: input.envValue, source: "env", ...input.explain ? { chain } : {} });
611
+ }
612
+ if (input.explain) chain.push({ source: "env", matched: false });
613
+ const sw = await parseDotenvFile(join2(input.home, ".skillwiki", ".env"));
614
+ if (sw.WIKI_PATH !== void 0) {
615
+ if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
616
+ return ok({ path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} });
617
+ }
618
+ if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
619
+ return err("NO_VAULT_CONFIGURED", {
620
+ message: "No vault configured. Run `skillwiki init` to bootstrap one, or pass `--vault <dir>`."
621
+ });
622
+ }
623
+
624
+ // src/commands/orphans.ts
625
+ async function runOrphans(input) {
626
+ let vault;
627
+ if (input.vault) {
628
+ vault = input.vault;
629
+ } else {
630
+ const r = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home ?? "" });
631
+ if (!r.ok) return { exitCode: ExitCode.NO_VAULT_CONFIGURED, result: r };
632
+ vault = r.data.path;
633
+ }
634
+ const scan = await scanVault(vault);
635
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
636
+ const slugToPath = {};
637
+ for (const p of scan.data.typedKnowledge) {
638
+ slugToPath[p.relPath.replace(/\.md$/, "").split("/").pop()] = p.relPath;
639
+ }
640
+ const adj = {};
641
+ for (const p of scan.data.typedKnowledge) adj[p.relPath] = /* @__PURE__ */ new Set();
642
+ for (const p of scan.data.typedKnowledge) {
643
+ const text = await readPage(p);
644
+ const split = splitFrontmatter(text);
645
+ const body = split.ok ? split.data.body : text;
646
+ for (const slug of extractBodyWikilinks(body)) {
647
+ const tgt = slugToPath[slug.split("/").pop()];
648
+ if (tgt) {
649
+ adj[p.relPath].add(tgt);
650
+ adj[tgt].add(p.relPath);
651
+ }
652
+ }
653
+ }
654
+ const orphans = Object.keys(adj).filter((k) => adj[k].size === 0);
655
+ const componentOf = {};
656
+ let cid = 0;
657
+ for (const node of Object.keys(adj)) {
658
+ if (componentOf[node] !== void 0) continue;
659
+ const stack = [node];
660
+ while (stack.length) {
661
+ const n = stack.pop();
662
+ if (componentOf[n] !== void 0) continue;
663
+ componentOf[n] = cid;
664
+ for (const nb of adj[n]) stack.push(nb);
665
+ }
666
+ cid++;
667
+ }
668
+ const bridges = [];
669
+ for (const node of Object.keys(adj)) {
670
+ const neighborComps = new Set([...adj[node]].map((n) => componentOf[n]));
671
+ if (adj[node].size >= 2 && neighborComps.size === 1) {
672
+ const without = simulateRemoval(adj, node);
673
+ if (without > Object.values(componentOf).filter((v, i, a) => a.indexOf(v) === i).length) {
674
+ bridges.push({ path: node, connects: [...adj[node]] });
675
+ }
676
+ }
677
+ }
678
+ const hintLines = [];
679
+ if (orphans.length > 0) hintLines.push(`orphans: ${orphans.length}`, ...orphans.map((o) => ` ${o}`));
680
+ if (bridges.length > 0) hintLines.push(`bridges: ${bridges.length}`, ...bridges.map((b) => ` ${b.path}`));
681
+ if (hintLines.length === 0) hintLines.push("no orphans or bridges");
682
+ return { exitCode: ExitCode.OK, result: ok({ orphans, bridges, humanHint: hintLines.join("\n") }) };
683
+ }
684
+ function simulateRemoval(adj, removed) {
685
+ const seen = /* @__PURE__ */ new Set();
686
+ let comps = 0;
687
+ for (const start of Object.keys(adj)) {
688
+ if (start === removed || seen.has(start)) continue;
689
+ comps++;
690
+ const stack = [start];
691
+ while (stack.length) {
692
+ const n = stack.pop();
693
+ if (seen.has(n) || n === removed) continue;
694
+ seen.add(n);
695
+ for (const nb of adj[n]) if (nb !== removed) stack.push(nb);
696
+ }
697
+ }
698
+ return comps;
699
+ }
700
+
701
+ // src/commands/audit.ts
702
+ import { readFile as readFile5, stat as stat2 } from "fs/promises";
703
+ import { dirname as dirname3, resolve, join as join3 } from "path";
704
+
705
+ // src/parsers/citations.ts
706
+ var FENCE2 = /```[\s\S]*?```/g;
707
+ function extractCitationMarkers(body) {
708
+ const stripped = body.replace(FENCE2, "");
709
+ const out = [];
710
+ const re = /\^\[(raw\/[^\]]+)\]/g;
711
+ let m;
712
+ while ((m = re.exec(stripped)) !== null) {
713
+ out.push({ marker: m[0], target: m[1] });
714
+ }
715
+ return out;
716
+ }
717
+
718
+ // src/commands/audit.ts
719
+ async function runAudit(input) {
720
+ let text;
721
+ try {
722
+ text = await readFile5(input.file, "utf8");
723
+ } catch {
724
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
725
+ }
726
+ const fm = extractFrontmatter(text);
727
+ if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
728
+ const split = splitFrontmatter(text);
729
+ const body = split.ok ? split.data.body : text;
730
+ const vault = await findVaultRoot(dirname3(resolve(input.file)));
731
+ if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
732
+ const markers = extractCitationMarkers(body);
733
+ const resolved = await Promise.all(markers.map(async (m) => {
734
+ try {
735
+ await stat2(join3(vault, m.target));
736
+ return { ...m, resolved: true };
737
+ } catch {
738
+ return { ...m, resolved: false };
739
+ }
740
+ }));
741
+ const sources = fm.data.sources ?? [];
742
+ const referenced = new Set(resolved.map((m) => m.target));
743
+ const unused_sources = sources.filter((s) => !referenced.has(s));
744
+ const missing_from_sources = [...referenced].filter((t) => !sources.includes(t));
745
+ const broken = resolved.filter((m) => !m.resolved);
746
+ const hintLines = [];
747
+ hintLines.push(`markers: ${resolved.length}, broken: ${broken.length}`);
748
+ if (unused_sources.length > 0) hintLines.push(`unused_sources: ${unused_sources.length}`);
749
+ if (missing_from_sources.length > 0) hintLines.push(`missing_from_sources: ${missing_from_sources.length}`);
750
+ if (broken.length === 0 && unused_sources.length === 0 && missing_from_sources.length === 0) hintLines.push("OK");
751
+ const humanHint = hintLines.join("\n");
752
+ if (resolved.some((m) => !m.resolved)) {
753
+ return { exitCode: ExitCode.UNRESOLVED_MARKERS, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
754
+ }
755
+ if (unused_sources.length > 0 || missing_from_sources.length > 0) {
756
+ return { exitCode: ExitCode.SOURCES_INCONSISTENT, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
757
+ }
758
+ return { exitCode: ExitCode.OK, result: ok({ markers: resolved, sources_consistency: { unused_sources, missing_from_sources }, humanHint }) };
759
+ }
760
+ async function findVaultRoot(start) {
761
+ let cur = start;
762
+ for (let i = 0; i < 20; i++) {
763
+ try {
764
+ await stat2(join3(cur, "SCHEMA.md"));
765
+ return cur;
766
+ } catch {
767
+ }
768
+ const parent = dirname3(cur);
769
+ if (parent === cur) return null;
770
+ cur = parent;
771
+ }
772
+ return null;
773
+ }
774
+
775
+ // src/commands/install.ts
776
+ import { readdir as readdir2, stat as stat4 } from "fs/promises";
777
+ import { join as join4 } from "path";
778
+
779
+ // src/utils/install-fs.ts
780
+ import { copyFile, mkdir as mkdir3, rename, writeFile as writeFile3, stat as stat3 } from "fs/promises";
781
+ import { dirname as dirname4 } from "path";
782
+ async function atomicCopyWithBackup(src, dst) {
783
+ await mkdir3(dirname4(dst), { recursive: true });
784
+ let backupPath = null;
785
+ try {
786
+ await stat3(dst);
787
+ backupPath = `${dst}.bak`;
788
+ await copyFile(dst, backupPath);
789
+ } catch {
790
+ }
791
+ const tmp = `${dst}.tmp.${process.pid}`;
792
+ try {
793
+ await copyFile(src, tmp);
794
+ await rename(tmp, dst);
795
+ } catch (e) {
796
+ return err("ATOMIC_COPY_FAILED", { message: String(e) });
797
+ }
798
+ return ok({ copied: true, backupPath });
799
+ }
800
+ async function writeManifest(path, m) {
801
+ await mkdir3(dirname4(path), { recursive: true });
802
+ const enriched = { installed_at: (/* @__PURE__ */ new Date()).toISOString(), ...m };
803
+ await writeFile3(path, JSON.stringify(enriched, null, 2));
804
+ }
805
+
806
+ // src/commands/install.ts
807
+ async function runInstall(input) {
808
+ let entries;
809
+ try {
810
+ entries = (await readdir2(input.skillsRoot, { withFileTypes: true })).filter((d) => d.isDirectory() && (d.name.startsWith("wiki-") || d.name.startsWith("proj-"))).map((d) => d.name);
811
+ } catch (e) {
812
+ return { exitCode: ExitCode.PREFLIGHT_FAILED, result: err("PREFLIGHT_FAILED", { message: String(e) }) };
813
+ }
814
+ if (entries.length === 0) {
815
+ return { exitCode: ExitCode.PREFLIGHT_FAILED, result: err("PREFLIGHT_FAILED", { reason: "no skills found" }) };
816
+ }
817
+ const installed = [];
818
+ const backed_up = [];
819
+ for (const name of entries) {
820
+ const src = join4(input.skillsRoot, name, "SKILL.md");
821
+ const dst = join4(input.target, name, "SKILL.md");
822
+ try {
823
+ await stat4(src);
824
+ } catch {
825
+ return { exitCode: ExitCode.PREFLIGHT_FAILED, result: err("PREFLIGHT_FAILED", { missing: src }) };
826
+ }
827
+ if (input.dryRun) {
828
+ installed.push(dst);
829
+ continue;
830
+ }
831
+ const r = await atomicCopyWithBackup(src, dst);
832
+ if (!r.ok) return { exitCode: ExitCode.ATOMIC_COPY_FAILED, result: r };
833
+ installed.push(dst);
834
+ if (r.data.backupPath) backed_up.push(r.data.backupPath);
835
+ }
836
+ const manifest_path = join4(input.target, "wiki-manifest.json");
837
+ if (!input.dryRun) await writeManifest(manifest_path, { installed, backed_up });
838
+ const hintLines = [
839
+ `installed: ${installed.length}`,
840
+ input.dryRun ? "(dry run)" : `backed up: ${backed_up.length}`,
841
+ `manifest: ${manifest_path}`
842
+ ];
843
+ return { exitCode: ExitCode.OK, result: ok({ installed, backed_up, manifest_path, humanHint: hintLines.join("\n") }) };
844
+ }
845
+
846
+ // src/commands/path.ts
847
+ async function runPath(input) {
848
+ if (input.initTime) {
849
+ const r2 = await resolveInitTimePath({
850
+ flag: input.flag,
851
+ envValue: input.envValue,
852
+ home: input.home,
853
+ explain: input.explain
854
+ });
855
+ return { exitCode: ExitCode.OK, result: ok({ path: r2.path, source: r2.source, ...r2.chain ? { chain: r2.chain } : {}, humanHint: `${r2.path} (via ${r2.source})` }) };
856
+ }
857
+ const r = await resolveRuntimePath({
858
+ flag: input.flag,
859
+ envValue: input.envValue,
860
+ home: input.home,
861
+ explain: input.explain
862
+ });
863
+ if (!r.ok) return { exitCode: ExitCode.NO_VAULT_CONFIGURED, result: r };
864
+ return { exitCode: ExitCode.OK, result: ok({ path: r.data.path, source: r.data.source, ...r.data.chain ? { chain: r.data.chain } : {}, humanHint: `${r.data.path} (via ${r.data.source})` }) };
865
+ }
866
+
867
+ // src/utils/lang.ts
868
+ import { join as join5 } from "path";
869
+ var ALIASES = {
870
+ english: "en",
871
+ en: "en",
872
+ "chinese-traditional": "zh-Hant",
873
+ "zh-hant": "zh-Hant",
874
+ "zh-tw": "zh-Hant",
875
+ "chinese-simplified": "zh-Hans",
876
+ "zh-hans": "zh-Hans",
877
+ "zh-cn": "zh-Hans"
878
+ };
879
+ function normalizeLang(input) {
880
+ const trimmed = input.trim();
881
+ const key = trimmed.toLowerCase();
882
+ return ALIASES[key] ?? trimmed;
883
+ }
884
+ async function resolveLang(input) {
885
+ if (input.flag !== void 0 && input.flag.length > 0) {
886
+ return { value: input.flag, source: "flag", canonical: normalizeLang(input.flag) };
887
+ }
888
+ if (input.envValue !== void 0 && input.envValue.length > 0) {
889
+ return { value: input.envValue, source: "env", canonical: normalizeLang(input.envValue) };
890
+ }
891
+ const dotenv = await parseDotenvFile(join5(input.home, ".skillwiki", ".env"));
892
+ if (dotenv.WIKI_LANG !== void 0) {
893
+ return { value: dotenv.WIKI_LANG, source: "skillwiki-dotenv", canonical: normalizeLang(dotenv.WIKI_LANG) };
894
+ }
895
+ return { value: "en", source: "default", canonical: "en" };
896
+ }
897
+
898
+ // src/commands/lang.ts
899
+ import { join as join6 } from "path";
900
+ async function runLang(input) {
901
+ const resolved = await resolveLang({ flag: input.flag, envValue: input.envValue, home: input.home });
902
+ let chain;
903
+ if (input.explain) {
904
+ chain = [
905
+ { source: "flag", matched: input.flag !== void 0 && input.flag.length > 0, value: input.flag },
906
+ { source: "env", matched: input.envValue !== void 0 && input.envValue.length > 0, value: input.envValue }
907
+ ];
908
+ const sw = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
909
+ chain.push({ source: "skillwiki-dotenv", matched: sw.WIKI_LANG !== void 0, value: sw.WIKI_LANG });
910
+ chain.push({ source: "default", matched: resolved.source === "default", value: "en" });
911
+ }
912
+ return {
913
+ exitCode: ExitCode.OK,
914
+ result: ok({
915
+ value: resolved.value,
916
+ source: resolved.source,
917
+ canonical: resolved.canonical,
918
+ ...chain ? { chain } : {},
919
+ humanHint: `${resolved.value} (via ${resolved.source})`
920
+ })
921
+ };
922
+ }
923
+
924
+ // src/commands/init.ts
925
+ import { mkdir as mkdir4, readFile as readFile6, readdir as readdir3, writeFile as writeFile4 } from "fs/promises";
926
+ import { join as join7 } from "path";
927
+
928
+ // src/parsers/taxonomy.ts
929
+ import yaml2 from "js-yaml";
930
+ var FENCE_RE = /^##\s+Tag Taxonomy\s*$[\s\S]*?```yaml\s*\n([\s\S]*?)\n```/m;
931
+ function extractTaxonomy(schemaText) {
932
+ const m = schemaText.match(FENCE_RE);
933
+ if (!m) return err("NO_TAXONOMY_BLOCK", { message: "No fenced YAML taxonomy block found in SCHEMA.md" });
934
+ let parsed;
935
+ try {
936
+ parsed = yaml2.load(m[1], { schema: yaml2.JSON_SCHEMA });
937
+ } catch (e) {
938
+ return err("INVALID_FRONTMATTER", { message: e.message });
939
+ }
940
+ if (parsed === null || typeof parsed !== "object") {
941
+ return err("INVALID_FRONTMATTER", { message: "taxonomy block is not an object" });
942
+ }
943
+ const tax = parsed.taxonomy;
944
+ if (!Array.isArray(tax)) {
945
+ return err("INVALID_FRONTMATTER", { message: "taxonomy key missing or not an array" });
946
+ }
947
+ if (!tax.every((x) => typeof x === "string")) {
948
+ return err("INVALID_FRONTMATTER", { message: "taxonomy must be a list of strings" });
949
+ }
950
+ return ok(tax);
951
+ }
952
+
953
+ // src/commands/init.ts
954
+ var DEFAULT_TAXONOMY = [
955
+ "research",
956
+ "comparison",
957
+ "timeline",
958
+ "summary",
959
+ "person",
960
+ "organization",
961
+ "concept",
962
+ "technique",
963
+ "tool",
964
+ "model"
965
+ ];
966
+ var VAULT_DIRS = [
967
+ "raw/articles",
968
+ "raw/papers",
969
+ "raw/transcripts",
970
+ "raw/assets",
971
+ "entities",
972
+ "concepts",
973
+ "comparisons",
974
+ "queries",
975
+ "meta",
976
+ "projects"
977
+ ];
978
+ function extractDomainFromSchema(text) {
979
+ const m = text.match(/^##\s+Domain\s*\n([\s\S]*?)(?=\n\n|\n##|\s*$)/m);
980
+ if (!m) return "";
981
+ const d = m[1].trim();
982
+ return d.startsWith("##") ? "" : d;
983
+ }
984
+ async function discoverTagsFromPages(target, knownSlugs) {
985
+ const knownSet = new Set(knownSlugs);
986
+ const discovered = /* @__PURE__ */ new Set();
987
+ for (const dir of ["entities", "concepts", "comparisons", "queries"]) {
988
+ let entries;
989
+ try {
990
+ entries = (await readdir3(join7(target, dir), { withFileTypes: true })).filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name);
991
+ } catch {
992
+ continue;
993
+ }
994
+ for (const file of entries) {
995
+ try {
996
+ const text = await readFile6(join7(target, dir, file), "utf8");
997
+ const fm = extractFrontmatter(text);
998
+ if (!fm.ok || !fm.data.tags || !Array.isArray(fm.data.tags)) continue;
999
+ for (const t of fm.data.tags) {
1000
+ if (typeof t === "string" && !knownSet.has(t)) discovered.add(t);
1001
+ }
1002
+ } catch {
1003
+ }
1004
+ }
1005
+ }
1006
+ return [...discovered].sort();
1007
+ }
1008
+ async function runInit(input) {
1009
+ const pathRes = await resolveInitTimePath({ flag: input.flag, envValue: input.envValue, home: input.home });
1010
+ const target = pathRes.path;
1011
+ const langRes = await resolveLang({ flag: input.lang, envValue: void 0, home: input.home });
1012
+ const canonicalLang = langRes.canonical;
1013
+ let oldSchemaText;
1014
+ try {
1015
+ oldSchemaText = await readFile6(join7(target, "SCHEMA.md"), "utf8");
1016
+ } catch {
1017
+ }
1018
+ if (oldSchemaText && !input.force) {
1019
+ return {
1020
+ exitCode: ExitCode.INIT_TARGET_NOT_EMPTY,
1021
+ result: err("INIT_TARGET_NOT_EMPTY", { target })
1022
+ };
1023
+ }
1024
+ const envPath = join7(input.home, ".skillwiki", ".env");
1025
+ let existingEnvRaw = "";
1026
+ try {
1027
+ existingEnvRaw = await readFile6(envPath, "utf8");
1028
+ } catch {
1029
+ }
1030
+ const existingEnv = parseDotenvText(existingEnvRaw);
1031
+ const swDotenvHadPath = existingEnv.WIKI_PATH !== void 0;
1032
+ if (existingEnv.WIKI_PATH !== void 0 && existingEnv.WIKI_PATH !== target && !input.force) {
1033
+ return {
1034
+ exitCode: ExitCode.ENV_WRITE_CONFLICT,
1035
+ result: err("ENV_WRITE_CONFLICT", { key: "WIKI_PATH", existing: existingEnv.WIKI_PATH, attempted: target })
1036
+ };
1037
+ }
1038
+ if (existingEnv.WIKI_LANG !== void 0 && existingEnv.WIKI_LANG !== canonicalLang && !input.force) {
1039
+ return {
1040
+ exitCode: ExitCode.ENV_WRITE_CONFLICT,
1041
+ result: err("ENV_WRITE_CONFLICT", { key: "WIKI_LANG", existing: existingEnv.WIKI_LANG, attempted: canonicalLang })
1042
+ };
1043
+ }
1044
+ const created = [];
1045
+ try {
1046
+ await mkdir4(target, { recursive: true });
1047
+ for (const d of VAULT_DIRS) {
1048
+ await mkdir4(join7(target, d), { recursive: true });
1049
+ created.push(d + "/");
1050
+ }
1051
+ } catch (e) {
1052
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
1053
+ }
1054
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1055
+ let taxonomy = input.taxonomy && input.taxonomy.length > 0 ? input.taxonomy : DEFAULT_TAXONOMY;
1056
+ let domain = input.domain;
1057
+ let oldTaxonomy = [];
1058
+ if (oldSchemaText) {
1059
+ if (!domain) {
1060
+ const oldDomain = extractDomainFromSchema(oldSchemaText);
1061
+ if (oldDomain) domain = oldDomain;
1062
+ }
1063
+ const oldTax = extractTaxonomy(oldSchemaText);
1064
+ if (oldTax.ok) oldTaxonomy = oldTax.data;
1065
+ }
1066
+ const taxonomySet = new Set(taxonomy);
1067
+ for (const t of oldTaxonomy) {
1068
+ if (!taxonomySet.has(t)) {
1069
+ taxonomy.push(t);
1070
+ taxonomySet.add(t);
1071
+ }
1072
+ }
1073
+ const discovered = await discoverTagsFromPages(target, taxonomy);
1074
+ const discovered_tags = discovered.length;
1075
+ const fullTaxonomyYaml = discovered.length > 0 ? taxonomy.map((t) => ` - ${t}`).join("\n") + "\n # --- Discovered from existing pages ---\n" + discovered.map((t) => ` - ${t}`).join("\n") : taxonomy.map((t) => ` - ${t}`).join("\n");
1076
+ try {
1077
+ const schemaTpl = await readFile6(join7(input.templates, "SCHEMA.md"), "utf8");
1078
+ const schema = schemaTpl.replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang).replace("{{TAXONOMY_YAML}}", fullTaxonomyYaml);
1079
+ await writeFile4(join7(target, "SCHEMA.md"), schema, "utf8");
1080
+ created.push("SCHEMA.md");
1081
+ } catch (e) {
1082
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: "SCHEMA.md", message: String(e) }) };
1083
+ }
1084
+ const preserved = [];
1085
+ async function writeOrPreserve(fileName, render) {
1086
+ try {
1087
+ const existing = await readFile6(join7(target, fileName), "utf8");
1088
+ if (existing.split("\n").length > 10) {
1089
+ preserved.push(fileName);
1090
+ return void 0;
1091
+ }
1092
+ } catch {
1093
+ }
1094
+ try {
1095
+ await writeFile4(join7(target, fileName), await render(), "utf8");
1096
+ created.push(fileName);
1097
+ return void 0;
1098
+ } catch (e) {
1099
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: fileName, message: String(e) }) };
1100
+ }
1101
+ }
1102
+ const err1 = await writeOrPreserve("index.md", async () => {
1103
+ const tpl = await readFile6(join7(input.templates, "index.md"), "utf8");
1104
+ return tpl.replace("{{INIT_DATE}}", today);
1105
+ });
1106
+ if (err1) return err1;
1107
+ const err22 = await writeOrPreserve("log.md", async () => {
1108
+ const tpl = await readFile6(join7(input.templates, "log.md"), "utf8");
1109
+ return tpl.replace(/\{\{INIT_DATE\}\}/g, today).replace("{{DOMAIN}}", domain).replace("{{WIKI_LANG}}", canonicalLang);
1110
+ });
1111
+ if (err22) return err22;
1112
+ const isTempPath = target.startsWith("/tmp/") || target === "/tmp" || target.startsWith("/var/tmp/") || target === "/var/tmp" || target.startsWith("/private/tmp/");
1113
+ const skipEnv = !!input.noEnv || isTempPath;
1114
+ let envWritten = "";
1115
+ if (!skipEnv) {
1116
+ try {
1117
+ await writeDotenv(envPath, { WIKI_PATH: target, WIKI_LANG: canonicalLang }, existingEnvRaw);
1118
+ envWritten = envPath;
1119
+ } catch (e) {
1120
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { file: envPath, message: String(e) }) };
1121
+ }
1122
+ }
1123
+ const importedFromHermes = pathRes.source === "hermes-dotenv" && !swDotenvHadPath;
1124
+ const humanHint = [
1125
+ `vault: ${target}`,
1126
+ `domain: ${domain}`,
1127
+ `lang: ${canonicalLang}`,
1128
+ `created: ${created.length}, preserved: ${preserved.length}`,
1129
+ `discovered tags: ${discovered_tags}`,
1130
+ skipEnv ? "env: skipped" : `env: ${envWritten}`
1131
+ ].join("\n");
1132
+ return {
1133
+ exitCode: ExitCode.OK,
1134
+ result: ok({
1135
+ vault: target,
1136
+ domain,
1137
+ taxonomy,
1138
+ lang: canonicalLang,
1139
+ created,
1140
+ preserved,
1141
+ env_written: envWritten,
1142
+ env_skipped: skipEnv,
1143
+ imported_from_hermes: importedFromHermes,
1144
+ discovered_tags,
1145
+ humanHint
1146
+ })
1147
+ };
1148
+ }
1149
+
1150
+ // src/utils/slug.ts
1151
+ function buildSlugMap(pages) {
1152
+ const map = /* @__PURE__ */ new Map();
1153
+ for (const p of pages) {
1154
+ const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
1155
+ map.set(slug.toLowerCase(), slug);
1156
+ }
1157
+ return map;
1158
+ }
1159
+
1160
+ // src/commands/links.ts
1161
+ async function runLinks(input) {
1162
+ const scan = await scanVault(input.vault);
1163
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1164
+ const slugs = buildSlugMap(scan.data.typedKnowledge);
1165
+ const broken = [];
1166
+ for (const p of scan.data.typedKnowledge) {
1167
+ const text = await readPage(p);
1168
+ const split = splitFrontmatter(text);
1169
+ const body = split.ok ? split.data.body : text;
1170
+ const lines = body.split("\n");
1171
+ for (const slug of extractBodyWikilinks(body)) {
1172
+ const tail = slug.split("/").pop();
1173
+ if (!slugs.has(tail.toLowerCase())) {
1174
+ const line = lines.findIndex((l) => l.includes(`[[${slug}`));
1175
+ broken.push({ page: p.relPath, slug, line: line >= 0 ? line + 1 : 0 });
1176
+ }
1177
+ }
1178
+ }
1179
+ if (broken.length > 0) {
1180
+ return { exitCode: ExitCode.BROKEN_WIKILINKS, result: ok({ broken, humanHint: `broken: ${broken.length}
1181
+ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }) };
1182
+ }
1183
+ return { exitCode: ExitCode.OK, result: ok({ broken, humanHint: "no broken wikilinks" }) };
1184
+ }
1185
+
1186
+ // src/commands/tag-audit.ts
1187
+ import { readFile as readFile7 } from "fs/promises";
1188
+ import { join as join8 } from "path";
1189
+ async function runTagAudit(input) {
1190
+ const scan = await scanVault(input.vault);
1191
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1192
+ const schemaText = await readFile7(join8(input.vault, "SCHEMA.md"), "utf8");
1193
+ const tax = extractTaxonomy(schemaText);
1194
+ if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
1195
+ const allowed = new Set(tax.data);
1196
+ const violations = [];
1197
+ for (const p of scan.data.typedKnowledge) {
1198
+ const text = await readPage(p);
1199
+ const fm = extractFrontmatter(text);
1200
+ if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
1201
+ const tags = fm.data.tags;
1202
+ if (!Array.isArray(tags)) continue;
1203
+ for (const t of tags) {
1204
+ if (typeof t === "string" && !allowed.has(t)) {
1205
+ violations.push({ page: p.relPath, tag: t });
1206
+ }
1207
+ }
1208
+ }
1209
+ if (violations.length > 0) {
1210
+ return { exitCode: ExitCode.TAG_NOT_IN_TAXONOMY, result: ok({ violations, taxonomy: tax.data, humanHint: violations.map((v) => `${v.page}: "${v.tag}" not in taxonomy`).join("\n") }) };
1211
+ }
1212
+ return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
1213
+ }
1214
+
1215
+ // src/commands/index-check.ts
1216
+ import { readFile as readFile8 } from "fs/promises";
1217
+ import { join as join9 } from "path";
1218
+ async function runIndexCheck(input) {
1219
+ const scan = await scanVault(input.vault);
1220
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1221
+ let indexText = "";
1222
+ try {
1223
+ indexText = await readFile8(join9(input.vault, "index.md"), "utf8");
1224
+ } catch {
1225
+ }
1226
+ const indexSlugsLower = /* @__PURE__ */ new Map();
1227
+ for (const s of extractBodyWikilinks(indexText)) {
1228
+ const tail = s.split("/").pop();
1229
+ indexSlugsLower.set(tail.toLowerCase(), tail);
1230
+ }
1231
+ const fileSlugs = /* @__PURE__ */ new Map();
1232
+ for (const p of scan.data.typedKnowledge) {
1233
+ const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
1234
+ fileSlugs.set(slug, p.relPath);
1235
+ }
1236
+ const missing_from_index = [];
1237
+ for (const [slug, relPath] of fileSlugs.entries()) {
1238
+ if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
1239
+ }
1240
+ const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
1241
+ const ghost_entries = [];
1242
+ for (const [lower, orig] of indexSlugsLower) {
1243
+ if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
1244
+ }
1245
+ const hintLines = [];
1246
+ if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
1247
+ if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
1248
+ if (hintLines.length === 0) hintLines.push("index OK");
1249
+ if (missing_from_index.length > 0 || ghost_entries.length > 0) {
1250
+ return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1251
+ }
1252
+ return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1253
+ }
1254
+
1255
+ // src/commands/stale.ts
1256
+ import { readFile as readFile9 } from "fs/promises";
1257
+ import { join as join10 } from "path";
1258
+ function dayDiff(a, b) {
1259
+ const da = Date.parse(a);
1260
+ const db = Date.parse(b);
1261
+ return Math.round((db - da) / 864e5);
1262
+ }
1263
+ async function runStale(input) {
1264
+ const scan = await scanVault(input.vault);
1265
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1266
+ const stale = [];
1267
+ for (const p of scan.data.typedKnowledge) {
1268
+ const fm = extractFrontmatter(await readPage(p));
1269
+ if (!fm.ok) continue;
1270
+ const updated = typeof fm.data.updated === "string" ? fm.data.updated : void 0;
1271
+ const sources = Array.isArray(fm.data.sources) ? fm.data.sources.filter((s) => typeof s === "string") : [];
1272
+ if (!updated || sources.length === 0) continue;
1273
+ let newest;
1274
+ for (const rel of sources) {
1275
+ let raw;
1276
+ try {
1277
+ raw = await readFile9(join10(input.vault, rel), "utf8");
1278
+ } catch {
1279
+ continue;
1280
+ }
1281
+ const rfm = extractFrontmatter(raw);
1282
+ if (!rfm.ok) continue;
1283
+ const ing = typeof rfm.data.ingested === "string" ? rfm.data.ingested : void 0;
1284
+ if (ing && (!newest || Date.parse(ing) > Date.parse(newest))) newest = ing;
1285
+ }
1286
+ if (!newest) continue;
1287
+ const gap = dayDiff(updated, newest);
1288
+ if (gap > input.days) {
1289
+ stale.push({ page: p.relPath, page_updated: updated, newest_source_ingested: newest, gap_days: gap });
1290
+ }
1291
+ }
1292
+ if (stale.length > 0) return { exitCode: ExitCode.STALE_PAGE, result: ok({ stale, humanHint: stale.map((s) => `${s.page} (${s.gap_days}d stale)`).join("\n") }) };
1293
+ return { exitCode: ExitCode.OK, result: ok({ stale, humanHint: "no stale pages" }) };
1294
+ }
1295
+
1296
+ // src/commands/pagesize.ts
1297
+ async function runPagesize(input) {
1298
+ const scan = await scanVault(input.vault);
1299
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1300
+ const oversized = [];
1301
+ for (const p of scan.data.typedKnowledge) {
1302
+ const text = await readPage(p);
1303
+ const split = splitFrontmatter(text);
1304
+ const body = split.ok ? split.data.body : text;
1305
+ const count = body.split("\n").length;
1306
+ if (count > input.lines) oversized.push({ page: p.relPath, lines: count });
1307
+ }
1308
+ if (oversized.length > 0) return { exitCode: ExitCode.PAGE_TOO_LARGE, result: ok({ oversized, humanHint: oversized.map((p) => `${p.page}: ${p.lines} lines`).join("\n") }) };
1309
+ return { exitCode: ExitCode.OK, result: ok({ oversized, humanHint: "all pages within size limit" }) };
1310
+ }
1311
+
1312
+ // src/commands/log-rotate.ts
1313
+ import { readFile as readFile10, rename as rename2, writeFile as writeFile5, stat as stat5 } from "fs/promises";
1314
+ import { join as join11 } from "path";
1315
+ var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
1316
+ async function runLogRotate(input) {
1317
+ try {
1318
+ await stat5(join11(input.vault, "SCHEMA.md"));
1319
+ } catch {
1320
+ return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
1321
+ }
1322
+ const logPath = join11(input.vault, "log.md");
1323
+ let logText;
1324
+ try {
1325
+ logText = await readFile10(logPath, "utf8");
1326
+ } catch {
1327
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
1328
+ }
1329
+ const matches = [...logText.matchAll(ENTRY_RE)];
1330
+ const entries = matches.length;
1331
+ if (entries < input.threshold) {
1332
+ return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 no rotation needed` }) };
1333
+ }
1334
+ if (!input.apply) {
1335
+ return {
1336
+ exitCode: ExitCode.LOG_ROTATE_NEEDED,
1337
+ result: ok({ entries, threshold: input.threshold, rotated: false, humanHint: `${entries}/${input.threshold} entries \u2014 rotation needed (use --apply)` })
1338
+ };
1339
+ }
1340
+ const newestYear = matches[matches.length - 1][1];
1341
+ const rotatedName = `log-${newestYear}.md`;
1342
+ const rotatedPath = join11(input.vault, rotatedName);
1343
+ try {
1344
+ await rename2(logPath, rotatedPath);
1345
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1346
+ const fresh = `# Vault Log
1347
+
1348
+ Chronological action log. Newest entries last. Skill writes append entries; lint may rotate.
1349
+
1350
+ ## [${today}] rotate | Log rotated from ${entries} entries
1351
+
1352
+ - Previous log moved to ${rotatedName}
1353
+ `;
1354
+ await writeFile5(logPath, fresh, "utf8");
1355
+ } catch (e) {
1356
+ return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
1357
+ }
1358
+ return { exitCode: ExitCode.OK, result: ok({ entries, threshold: input.threshold, rotated: true, rotated_to: rotatedName, humanHint: `rotated ${entries} entries to ${rotatedName}` }) };
1359
+ }
1360
+
1361
+ // src/commands/topic-map-check.ts
1362
+ var DEFAULT_THRESHOLD = 200;
1363
+ async function runTopicMapCheck(input) {
1364
+ const threshold = input.threshold ?? DEFAULT_THRESHOLD;
1365
+ const scan = await scanVault(input.vault);
1366
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1367
+ const page_count = scan.data.typedKnowledge.length;
1368
+ const recommended = page_count >= threshold;
1369
+ return {
1370
+ exitCode: ExitCode.OK,
1371
+ result: ok({
1372
+ recommended,
1373
+ page_count,
1374
+ threshold,
1375
+ humanHint: recommended ? `topic map recommended (${page_count} pages >= ${threshold} threshold)` : `topic map not needed (${page_count} pages < ${threshold} threshold)`
1376
+ })
1377
+ };
1378
+ }
1379
+
1380
+ // src/commands/index-link-format.ts
1381
+ import { readFile as readFile11 } from "fs/promises";
1382
+ import { join as join12 } from "path";
1383
+ var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
1384
+ async function runIndexLinkFormat(input) {
1385
+ let text = "";
1386
+ try {
1387
+ text = await readFile11(join12(input.vault, "index.md"), "utf8");
1388
+ } catch {
1389
+ }
1390
+ const markdown_links = [];
1391
+ for (const [i, line] of text.split("\n").entries()) {
1392
+ if (MD_LINK_RE.test(line)) markdown_links.push({ line: i + 1, text: line.trim() });
1393
+ }
1394
+ const humanHint = markdown_links.length === 0 ? "all index links use wikilink format" : `markdown links found: ${markdown_links.length}
1395
+ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
1396
+ return { exitCode: ExitCode.OK, result: ok({ markdown_links, humanHint }) };
1397
+ }
1398
+
1399
+ // src/commands/dedup.ts
1400
+ async function runDedup(input) {
1401
+ const scan = await scanVault(input.vault);
1402
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1403
+ const hashMap = /* @__PURE__ */ new Map();
1404
+ let totalFiles = 0;
1405
+ for (const raw of scan.data.raw) {
1406
+ const fm = extractFrontmatter(await readPage(raw));
1407
+ if (!fm.ok) continue;
1408
+ const sha = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
1409
+ if (!sha || sha.length !== 64) continue;
1410
+ totalFiles++;
1411
+ const existing = hashMap.get(sha);
1412
+ if (existing) existing.push(raw.relPath);
1413
+ else hashMap.set(sha, [raw.relPath]);
1414
+ }
1415
+ const duplicates = [...hashMap.entries()].filter(([, files]) => files.length > 1).map(([sha256, files]) => ({ sha256, files }));
1416
+ const exitCode = duplicates.length > 0 ? ExitCode.RAW_DEDUP_DETECTED : ExitCode.OK;
1417
+ const hintLines = [`scanned: ${totalFiles} raw files`];
1418
+ if (duplicates.length > 0) {
1419
+ hintLines.push(`duplicates: ${duplicates.length}`);
1420
+ for (const d of duplicates) hintLines.push(` ${d.sha256.slice(0, 12)}... \u2192 ${d.files.join(", ")}`);
1421
+ } else {
1422
+ hintLines.push("0 duplicates");
1423
+ }
1424
+ return {
1425
+ exitCode,
1426
+ result: ok({ scanned: totalFiles, duplicates, humanHint: hintLines.join("\n") })
1427
+ };
1428
+ }
1429
+
1430
+ // src/commands/lint.ts
1431
+ var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_dedup", "tag_not_in_taxonomy"];
1432
+ var WARNING_ORDER = ["index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "contested", "orphans"];
1433
+ var INFO_ORDER = ["bridges", "low_confidence_single_source", "topic_map_recommended"];
1434
+ async function runLint(input) {
1435
+ const buckets = {};
1436
+ const links = await runLinks({ vault: input.vault });
1437
+ if (links.result.ok && links.result.data.broken.length > 0) buckets.broken_wikilinks = links.result.data.broken;
1438
+ if (!links.result.ok && links.result.error === "INVALID_FRONTMATTER") {
1439
+ buckets.invalid_frontmatter = [links.result.detail ?? {}];
1440
+ }
1441
+ const tags = await runTagAudit({ vault: input.vault });
1442
+ if (tags.result.ok && tags.result.data.violations.length > 0) buckets.tag_not_in_taxonomy = tags.result.data.violations;
1443
+ if (!tags.result.ok && tags.result.error === "INVALID_FRONTMATTER") {
1444
+ buckets.invalid_frontmatter = [...buckets.invalid_frontmatter ?? [], tags.result.detail ?? {}];
1445
+ }
1446
+ const idx = await runIndexCheck({ vault: input.vault });
1447
+ if (idx.result.ok && (idx.result.data.missing_from_index.length > 0 || idx.result.data.ghost_entries.length > 0)) {
1448
+ buckets.index_incomplete = [{
1449
+ missing_from_index: idx.result.data.missing_from_index,
1450
+ ghost_entries: idx.result.data.ghost_entries
1451
+ }];
1452
+ }
1453
+ const linkFmt = await runIndexLinkFormat({ vault: input.vault });
1454
+ if (linkFmt.result.ok && linkFmt.result.data.markdown_links.length > 0) {
1455
+ buckets.index_link_format = linkFmt.result.data.markdown_links;
1456
+ }
1457
+ const stale = await runStale({ vault: input.vault, days: input.days });
1458
+ if (stale.result.ok && stale.result.data.stale.length > 0) buckets.stale_page = stale.result.data.stale;
1459
+ const pagesize = await runPagesize({ vault: input.vault, lines: input.lines });
1460
+ if (pagesize.result.ok && pagesize.result.data.oversized.length > 0) buckets.page_too_large = pagesize.result.data.oversized;
1461
+ const rotate = await runLogRotate({ vault: input.vault, threshold: input.logThreshold, apply: false });
1462
+ if (rotate.result.ok && rotate.exitCode === ExitCode.LOG_ROTATE_NEEDED) {
1463
+ buckets.log_rotate_needed = [{ entries: rotate.result.data.entries, threshold: rotate.result.data.threshold }];
1464
+ }
1465
+ const orphans = await runOrphans({ vault: input.vault });
1466
+ if (orphans.result.ok) {
1467
+ if (orphans.result.data.orphans.length > 0) buckets.orphans = orphans.result.data.orphans;
1468
+ if (orphans.result.data.bridges.length > 0) buckets.bridges = orphans.result.data.bridges;
1469
+ }
1470
+ const topicMap = await runTopicMapCheck({ vault: input.vault });
1471
+ if (topicMap.result.ok && topicMap.result.data.recommended) {
1472
+ buckets.topic_map_recommended = [{ page_count: topicMap.result.data.page_count, threshold: topicMap.result.data.threshold }];
1473
+ }
1474
+ const dedup = await runDedup({ vault: input.vault });
1475
+ if (dedup.result.ok && dedup.result.data.duplicates.length > 0) buckets.raw_dedup = dedup.result.data.duplicates;
1476
+ const errorOut = ERROR_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1477
+ const warningOut = WARNING_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1478
+ const infoOut = INFO_ORDER.flatMap((k) => buckets[k] ? [{ kind: k, items: buckets[k] }] : []);
1479
+ const summary = {
1480
+ errors: errorOut.reduce((n, b) => n + b.items.length, 0),
1481
+ warnings: warningOut.reduce((n, b) => n + b.items.length, 0),
1482
+ info: infoOut.reduce((n, b) => n + b.items.length, 0)
1483
+ };
1484
+ let exitCode = ExitCode.OK;
1485
+ if (summary.errors > 0) exitCode = ExitCode.LINT_HAS_ERRORS;
1486
+ else if (summary.warnings > 0 || summary.info > 0) exitCode = ExitCode.LINT_HAS_WARNINGS;
1487
+ const hintLines = [];
1488
+ if (summary.errors > 0) hintLines.push(`errors: ${summary.errors}`);
1489
+ if (summary.warnings > 0) hintLines.push(`warnings: ${summary.warnings}`);
1490
+ if (summary.info > 0) hintLines.push(`info: ${summary.info}`);
1491
+ const allBuckets = [...errorOut, ...warningOut, ...infoOut];
1492
+ for (const b of allBuckets) {
1493
+ hintLines.push(` ${b.kind}: ${b.items.length}`);
1494
+ }
1495
+ if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
1496
+ return {
1497
+ exitCode,
1498
+ result: ok({
1499
+ vault: { path: input.vault, source: input.source ?? "resolved" },
1500
+ summary,
1501
+ by_severity: { error: errorOut, warning: warningOut, info: infoOut },
1502
+ humanHint: hintLines.join("\n")
1503
+ })
1504
+ };
1505
+ }
1506
+
1507
+ // src/commands/config.ts
1508
+ import { readFile as readFile12 } from "fs/promises";
1509
+ import { existsSync } from "fs";
1510
+ import { join as join13 } from "path";
1511
+ function validateKey(key) {
1512
+ return CONFIG_KEYS.includes(key);
1513
+ }
1514
+ function configPath(home) {
1515
+ return join13(home, ".skillwiki", ".env");
1516
+ }
1517
+ async function runConfigGet(input) {
1518
+ if (!validateKey(input.key)) {
1519
+ return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
1520
+ }
1521
+ const map = await parseDotenvFile(configPath(input.home));
1522
+ const value = map[input.key] ?? "";
1523
+ return { exitCode: ExitCode.OK, result: ok({ key: input.key, value, humanHint: value }) };
1524
+ }
1525
+ async function runConfigSet(input) {
1526
+ if (!validateKey(input.key)) {
1527
+ return { exitCode: ExitCode.INVALID_CONFIG_KEY, result: err("INVALID_CONFIG_KEY", { key: input.key }) };
1528
+ }
1529
+ const filePath = configPath(input.home);
1530
+ try {
1531
+ let originalContent;
1532
+ try {
1533
+ originalContent = await readFile12(filePath, "utf8");
1534
+ } catch {
1535
+ }
1536
+ const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
1537
+ const merged = { ...existing, [input.key]: input.value };
1538
+ await writeDotenv(filePath, merged, originalContent);
1539
+ return { exitCode: ExitCode.OK, result: ok({ key: input.key, value: input.value, written: true, humanHint: `${input.key}=${input.value}` }) };
1540
+ } catch (e) {
1541
+ return { exitCode: ExitCode.CONFIG_WRITE_FAILED, result: err("CONFIG_WRITE_FAILED", { key: input.key, error: String(e) }) };
1542
+ }
1543
+ }
1544
+ async function runConfigList(input) {
1545
+ const map = await parseDotenvFile(configPath(input.home));
1546
+ const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
1547
+ return { exitCode: ExitCode.OK, result: ok({ entries, humanHint: entries.map((e) => `${e.key}=${e.value}`).join("\n") }) };
1548
+ }
1549
+ async function runConfigPath(input) {
1550
+ const filePath = configPath(input.home);
1551
+ return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync(filePath), humanHint: filePath }) };
1552
+ }
1553
+
1554
+ // src/commands/doctor.ts
1555
+ import { existsSync as existsSync2, readdirSync, statSync } from "fs";
1556
+ import { join as join14 } from "path";
1557
+ import { execSync } from "child_process";
1558
+ function check(status, id, label, detail) {
1559
+ return { id, label, status, detail };
1560
+ }
1561
+ function checkNodeVersion() {
1562
+ const major = parseInt(process.version.slice(1).split(".")[0], 10);
1563
+ if (major >= 20) {
1564
+ return check("pass", "node_version", "Node.js version", `v${major} >= 20`);
1565
+ }
1566
+ return check("error", "node_version", "Node.js version", `Node.js v${major} is below minimum v20`);
1567
+ }
1568
+ function checkCliOnPath(argv) {
1569
+ if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
1570
+ return check("warn", "cli_on_path", "skillwiki on PATH", "Running via node cli.js (dev mode) \u2014 PATH check skipped");
1571
+ }
1572
+ if (argv.length >= 2 && argv[1] === "skillwiki") {
1573
+ return check("pass", "cli_on_path", "skillwiki on PATH", "Running as skillwiki \u2014 already on PATH");
1574
+ }
1575
+ try {
1576
+ execSync("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
1577
+ return check("pass", "cli_on_path", "skillwiki on PATH", "skillwiki found on PATH");
1578
+ } catch {
1579
+ return check("warn", "cli_on_path", "skillwiki on PATH", "skillwiki not found on PATH");
1580
+ }
1581
+ }
1582
+ async function checkConfigFile(home) {
1583
+ const cfgPath = configPath(home);
1584
+ if (!existsSync2(cfgPath)) {
1585
+ return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
1586
+ }
1587
+ try {
1588
+ const map = await parseDotenvFile(cfgPath);
1589
+ const keys = Object.keys(map);
1590
+ return check("pass", "config_file", "Config file exists", `Found with keys: ${keys.length > 0 ? keys.join(", ") : "(none set)"}`);
1591
+ } catch (e) {
1592
+ return check("warn", "config_file", "Config file exists", `Failed to parse ${cfgPath}: ${String(e)}`);
1593
+ }
1594
+ }
1595
+ function checkWikiPathExists(resolvedPath) {
1596
+ if (resolvedPath === void 0) {
1597
+ return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
1598
+ }
1599
+ if (existsSync2(resolvedPath) && statSync(resolvedPath).isDirectory()) {
1600
+ return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
1601
+ }
1602
+ return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
1603
+ }
1604
+ function checkVaultStructure(resolvedPath) {
1605
+ if (resolvedPath === void 0) {
1606
+ return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
1607
+ }
1608
+ if (!existsSync2(resolvedPath)) {
1609
+ return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
1610
+ }
1611
+ const missing = [];
1612
+ if (!existsSync2(join14(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
1613
+ for (const dir of ["raw", "entities", "concepts", "meta"]) {
1614
+ if (!existsSync2(join14(resolvedPath, dir))) missing.push(dir + "/");
1615
+ }
1616
+ if (missing.length === 0) {
1617
+ return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
1618
+ }
1619
+ return check("error", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")}`);
1620
+ }
1621
+ function checkSkillsInstalled(home) {
1622
+ const skillsDir = join14(home, ".claude", "skills");
1623
+ if (!existsSync2(skillsDir)) {
1624
+ return check("warn", "skills_installed", "Skills installed", `${skillsDir} not found`);
1625
+ }
1626
+ const found = findSkillMd(skillsDir);
1627
+ if (found.length > 0) {
1628
+ return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found`);
1629
+ }
1630
+ return check("warn", "skills_installed", "Skills installed", "No SKILL.md files found in ~/.claude/skills/");
1631
+ }
1632
+ function findSkillMd(dir) {
1633
+ const results = [];
1634
+ let entries;
1635
+ try {
1636
+ entries = readdirSync(dir, { withFileTypes: true });
1637
+ } catch {
1638
+ return results;
1639
+ }
1640
+ for (const entry of entries) {
1641
+ if (entry.isFile() && entry.name === "SKILL.md") {
1642
+ results.push(join14(dir, entry.name));
1643
+ } else if (entry.isDirectory()) {
1644
+ results.push(...findSkillMd(join14(dir, entry.name)));
1645
+ }
1646
+ }
1647
+ return results;
1648
+ }
1649
+ async function runDoctor(input) {
1650
+ const checks = [];
1651
+ checks.push(checkNodeVersion());
1652
+ checks.push(checkCliOnPath(input.argv));
1653
+ checks.push(await checkConfigFile(input.home));
1654
+ const resolved = await resolveRuntimePath({ flag: void 0, envValue: input.envValue, home: input.home });
1655
+ if (resolved.ok) {
1656
+ checks.push(check("pass", "wiki_path_set", "WIKI_PATH configured", `Resolved via ${resolved.data.source}: ${resolved.data.path}`));
1657
+ } else {
1658
+ checks.push(check("error", "wiki_path_set", "WIKI_PATH configured", "No vault configured. Run `skillwiki init` or pass --vault."));
1659
+ }
1660
+ const resolvedPath = resolved.ok ? resolved.data.path : void 0;
1661
+ checks.push(checkWikiPathExists(resolvedPath));
1662
+ checks.push(checkVaultStructure(resolvedPath));
1663
+ checks.push(checkSkillsInstalled(input.home));
1664
+ const summary = {
1665
+ pass: checks.filter((c) => c.status === "pass").length,
1666
+ warn: checks.filter((c) => c.status === "warn").length,
1667
+ error: checks.filter((c) => c.status === "error").length
1668
+ };
1669
+ const exitCode = summary.error > 0 ? ExitCode.DOCTOR_HAS_ERRORS : summary.warn > 0 ? ExitCode.DOCTOR_HAS_WARNINGS : ExitCode.OK;
1670
+ const statusIcon = { pass: "\u2713", warn: "\u26A0", error: "\u2717" };
1671
+ const lines = checks.map((c) => {
1672
+ const icon = statusIcon[c.status];
1673
+ const padded = c.label.padEnd(24);
1674
+ return ` ${icon} ${padded} ${c.detail}`;
1675
+ });
1676
+ lines.push("");
1677
+ lines.push(`${summary.pass} pass \xB7 ${summary.warn} warn \xB7 ${summary.error} error`);
1678
+ const humanHint = lines.join("\n");
1679
+ return { exitCode, result: ok({ checks, summary, humanHint }) };
1680
+ }
1681
+
1682
+ // src/commands/archive.ts
1683
+ import { rename as rename3, mkdir as mkdir5, readFile as readFile13, writeFile as writeFile6 } from "fs/promises";
1684
+ import { join as join15, dirname as dirname6 } from "path";
1685
+ async function runArchive(input) {
1686
+ const scan = await scanVault(input.vault);
1687
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1688
+ let relPath;
1689
+ if (input.page.includes("/")) {
1690
+ relPath = scan.data.typedKnowledge.find((p) => p.relPath === input.page)?.relPath;
1691
+ } else {
1692
+ relPath = scan.data.typedKnowledge.find((p) => p.relPath.replace(/\.md$/, "").split("/").pop() === input.page)?.relPath;
1693
+ }
1694
+ if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
1695
+ if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
1696
+ const archivePath = join15("_archive", relPath);
1697
+ await mkdir5(dirname6(join15(input.vault, archivePath)), { recursive: true });
1698
+ let indexUpdated = false;
1699
+ const indexPath = join15(input.vault, "index.md");
1700
+ try {
1701
+ const idx = await readFile13(indexPath, "utf8");
1702
+ const slug = relPath.replace(/\.md$/, "").split("/").pop();
1703
+ const originalLines = idx.split("\n");
1704
+ const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
1705
+ if (filtered.length !== originalLines.length) {
1706
+ await writeFile6(indexPath, filtered.join("\n"), "utf8");
1707
+ indexUpdated = true;
1708
+ }
1709
+ } catch (e) {
1710
+ if (e?.code !== "ENOENT") throw e;
1711
+ }
1712
+ await rename3(join15(input.vault, relPath), join15(input.vault, archivePath));
1713
+ return { exitCode: ExitCode.OK, result: ok({ archived_from: relPath, archived_to: archivePath, index_updated: indexUpdated, humanHint: `${relPath} -> ${archivePath}${indexUpdated ? " (index updated)" : ""}` }) };
1714
+ }
1715
+
1716
+ // src/commands/drift.ts
1717
+ import { createHash as createHash2 } from "crypto";
1718
+
1719
+ // src/utils/fetch.ts
1720
+ async function controlledFetch(url, opts) {
1721
+ let current = url;
1722
+ for (let hop = 0; hop <= opts.maxRedirects; hop++) {
1723
+ const guard = runFetchGuardSync({ url: current });
1724
+ if (!guard.result.ok) return guard.result;
1725
+ const ctrl = new AbortController();
1726
+ const timer = setTimeout(() => ctrl.abort(), opts.timeoutMs);
1727
+ let res;
1728
+ try {
1729
+ res = await fetch(current, { redirect: "manual", signal: ctrl.signal });
1730
+ } catch (e) {
1731
+ clearTimeout(timer);
1732
+ if (e?.name === "AbortError") return err("FETCH_TIMEOUT", { url: current });
1733
+ return err("FETCH_FAILED", { message: String(e) });
1734
+ }
1735
+ clearTimeout(timer);
1736
+ if (res.status >= 300 && res.status < 400) {
1737
+ const loc = res.headers.get("location");
1738
+ if (!loc) return err("FETCH_FAILED", { reason: "redirect without Location" });
1739
+ current = new URL(loc, current).toString();
1740
+ continue;
1741
+ }
1742
+ const declared = Number(res.headers.get("content-length") ?? "0");
1743
+ if (declared > opts.maxBytes) return err("FETCH_TOO_LARGE", { declared, limit: opts.maxBytes });
1744
+ const buf = new Uint8Array(await res.arrayBuffer());
1745
+ if (buf.byteLength > opts.maxBytes) return err("FETCH_TOO_LARGE", { actual: buf.byteLength, limit: opts.maxBytes });
1746
+ return ok({ url: current, status: res.status, body: new TextDecoder().decode(buf), bytes: buf.byteLength });
1747
+ }
1748
+ return err("FETCH_FAILED", { reason: "too many redirects", limit: opts.maxRedirects });
1749
+ }
1750
+
1751
+ // src/commands/drift.ts
1752
+ var FETCH_OPTS = { timeoutMs: 1e4, maxBytes: 5e6, maxRedirects: 5 };
1753
+ async function runDrift(input) {
1754
+ const doFetch = input.fetchFn ?? controlledFetch;
1755
+ const scan = await scanVault(input.vault);
1756
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1757
+ const results = [];
1758
+ for (const raw of scan.data.raw) {
1759
+ const fm = extractFrontmatter(await readPage(raw));
1760
+ if (!fm.ok) continue;
1761
+ const sourceUrl = typeof fm.data.source_url === "string" ? fm.data.source_url : null;
1762
+ const storedHash = typeof fm.data.sha256 === "string" ? fm.data.sha256 : null;
1763
+ if (!sourceUrl || !storedHash) continue;
1764
+ const resp = await doFetch(sourceUrl, FETCH_OPTS);
1765
+ if (!resp.ok) {
1766
+ results.push({
1767
+ raw_path: raw.relPath,
1768
+ source_url: sourceUrl,
1769
+ stored_sha256: storedHash,
1770
+ current_sha256: null,
1771
+ status: "fetch_failed",
1772
+ fetch_error: resp.error
1773
+ });
1774
+ continue;
1775
+ }
1776
+ const currentHash = createHash2("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
1777
+ const drifted2 = currentHash !== storedHash;
1778
+ results.push({
1779
+ raw_path: raw.relPath,
1780
+ source_url: sourceUrl,
1781
+ stored_sha256: storedHash,
1782
+ current_sha256: currentHash,
1783
+ status: drifted2 ? "drifted" : "unchanged"
1784
+ });
1785
+ }
1786
+ const drifted = results.filter((r) => r.status === "drifted");
1787
+ const fetchFailed = results.filter((r) => r.status === "fetch_failed");
1788
+ const unchanged = results.filter((r) => r.status === "unchanged").length;
1789
+ const exitCode = drifted.length > 0 ? ExitCode.DRIFT_DETECTED : ExitCode.OK;
1790
+ const hintLines = [`scanned: ${results.length}, unchanged: ${unchanged}`];
1791
+ if (drifted.length > 0) hintLines.push(`drifted: ${drifted.length}`, ...drifted.map((d) => ` ${d.raw_path}`));
1792
+ if (fetchFailed.length > 0) hintLines.push(`fetch_failed: ${fetchFailed.length}`, ...fetchFailed.map((f) => ` ${f.raw_path}: ${f.fetch_error}`));
1793
+ return {
1794
+ exitCode,
1795
+ result: ok({ scanned: results.length, drifted, fetch_failed: fetchFailed, unchanged, humanHint: hintLines.join("\n") })
1796
+ };
1797
+ }
1798
+
1799
+ // src/cli.ts
1800
+ var pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
1801
+ var program = new Command();
1802
+ program.name("skillwiki").description("Deterministic helpers for CodeWiki skills").version(pkg.version);
1803
+ program.option("--human", "render terminal-readable output instead of JSON");
1804
+ function emit(r) {
1805
+ if (program.opts().human) printHuman(r.result);
1806
+ else printJson(r.result);
1807
+ process.exit(r.exitCode);
1808
+ }
1809
+ program.command("hash <file>").action(async (file) => emit(await runHash({ file })));
1810
+ program.command("fetch-guard <url>").action(async (url) => emit(await runFetchGuard({ url })));
1811
+ program.command("validate <file>").action(async (file) => emit(await runValidate({ file })));
1812
+ program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path", ".skillwiki/graph.json").action(async (vault, opts) => emit(await runGraphBuild({ vault, out: opts.out })));
1813
+ program.command("overlap <vault>").action(async (vault) => emit(await runOverlap({ vault })));
1814
+ program.command("orphans [vault]").action(async (vault) => emit(await runOrphans({
1815
+ vault,
1816
+ envValue: process.env.WIKI_PATH,
1817
+ home: process.env.HOME ?? ""
1818
+ })));
1819
+ program.command("audit <file>").action(async (file) => emit(await runAudit({ file })));
1820
+ program.command("install").option("--target <dir>", "target install directory", `${process.env.HOME ?? ""}/.claude/skills/`).option("--dry-run", "preview only", false).option("--skills-root <dir>", "source skills directory (defaults to packaged)").action(async (opts) => {
1821
+ const skillsRoot = opts.skillsRoot ?? new URL("../skills/", import.meta.url).pathname;
1822
+ emit(await runInstall({ skillsRoot, target: opts.target, dryRun: !!opts.dryRun }));
1823
+ });
1824
+ program.command("path").option("--vault <dir>", "explicit vault override (runtime)").option("--target <dir>", "explicit target override (init-time)").option("--init-time", "use init-time chain instead of runtime", false).option("--explain", "include resolution chain in output", false).action(async (opts) => {
1825
+ const initTime = !!opts.initTime;
1826
+ const flag = initTime ? opts.target : opts.vault;
1827
+ emit(await runPath({
1828
+ flag,
1829
+ envValue: process.env.WIKI_PATH,
1830
+ home: process.env.HOME ?? "",
1831
+ initTime,
1832
+ explain: !!opts.explain
1833
+ }));
1834
+ });
1835
+ program.command("lang").option("--lang <code>", "explicit language override").option("--explain", "include resolution chain in output", false).action(async (opts) => {
1836
+ emit(await runLang({
1837
+ flag: opts.lang,
1838
+ envValue: process.env.WIKI_LANG,
1839
+ home: process.env.HOME ?? "",
1840
+ explain: !!opts.explain
1841
+ }));
1842
+ });
1843
+ program.command("init").option("--target <dir>", "explicit target directory").requiredOption("--domain <text>", "knowledge domain seed").option("--taxonomy <csv>", "comma-separated tag list").option("--lang <code>", "output language (BCP 47 or alias)").option("--force", "override existing target / env conflict", false).option("--no-env", "skip writing ~/.skillwiki/.env").action(async (opts) => {
1844
+ const templates = new URL("../templates/", import.meta.url).pathname;
1845
+ const taxonomy = typeof opts.taxonomy === "string" ? opts.taxonomy.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0;
1846
+ emit(await runInit({
1847
+ flag: opts.target,
1848
+ envValue: process.env.WIKI_PATH,
1849
+ home: process.env.HOME ?? "",
1850
+ templates,
1851
+ domain: opts.domain,
1852
+ taxonomy,
1853
+ lang: opts.lang,
1854
+ force: !!opts.force,
1855
+ noEnv: opts.env === false
1856
+ }));
1857
+ });
1858
+ async function resolveVaultArg(arg) {
1859
+ if (arg) return { ok: true, vault: arg };
1860
+ const r = await resolveRuntimePath({
1861
+ flag: void 0,
1862
+ envValue: process.env.WIKI_PATH,
1863
+ home: process.env.HOME ?? ""
1864
+ });
1865
+ if (!r.ok) return { ok: false, exitCode: 25, payload: r };
1866
+ return { ok: true, vault: r.data.path };
1867
+ }
1868
+ program.command("links [vault]").action(async (vault) => {
1869
+ const v = await resolveVaultArg(vault);
1870
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1871
+ else emit(await runLinks({ vault: v.vault }));
1872
+ });
1873
+ program.command("tag-audit [vault]").action(async (vault) => {
1874
+ const v = await resolveVaultArg(vault);
1875
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1876
+ else emit(await runTagAudit({ vault: v.vault }));
1877
+ });
1878
+ program.command("index-check [vault]").action(async (vault) => {
1879
+ const v = await resolveVaultArg(vault);
1880
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1881
+ else emit(await runIndexCheck({ vault: v.vault }));
1882
+ });
1883
+ program.command("stale [vault]").option("--days <n>", "staleness threshold in days", (s) => parseInt(s, 10), 90).action(async (vault, opts) => {
1884
+ const v = await resolveVaultArg(vault);
1885
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1886
+ else emit(await runStale({ vault: v.vault, days: opts.days }));
1887
+ });
1888
+ program.command("pagesize [vault]").option("--lines <n>", "max body lines", (s) => parseInt(s, 10), 200).action(async (vault, opts) => {
1889
+ const v = await resolveVaultArg(vault);
1890
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1891
+ else emit(await runPagesize({ vault: v.vault, lines: opts.lines }));
1892
+ });
1893
+ program.command("log-rotate [vault]").option("--threshold <n>", "entry count threshold", (s) => parseInt(s, 10), 500).option("--apply", "actually rotate", false).action(async (vault, opts) => {
1894
+ const v = await resolveVaultArg(vault);
1895
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1896
+ else emit(await runLogRotate({ vault: v.vault, threshold: opts.threshold, apply: !!opts.apply }));
1897
+ });
1898
+ program.command("lint [vault]").option("--days <n>", "stale threshold", (s) => parseInt(s, 10), 90).option("--lines <n>", "pagesize threshold", (s) => parseInt(s, 10), 200).option("--log-threshold <n>", "log rotation threshold", (s) => parseInt(s, 10), 500).action(async (vault, opts) => {
1899
+ const v = await resolveVaultArg(vault);
1900
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1901
+ else emit(await runLint({
1902
+ vault: v.vault,
1903
+ source: vault ? "flag" : void 0,
1904
+ days: opts.days,
1905
+ lines: opts.lines,
1906
+ logThreshold: opts.logThreshold
1907
+ }));
1908
+ });
1909
+ var configCmd = program.command("config").description("manage skillwiki configuration");
1910
+ configCmd.command("get <key>").description("print the value of a config key").action(async (key) => emit(await runConfigGet({ key, home: process.env.HOME ?? "" })));
1911
+ configCmd.command("set <key> <value>").description("set a config key value").action(async (key, value) => emit(await runConfigSet({ key, value, home: process.env.HOME ?? "" })));
1912
+ configCmd.command("list").description("list all config key=value pairs").action(async () => emit(await runConfigList({ home: process.env.HOME ?? "" })));
1913
+ configCmd.command("path").description("print the config file path").action(async () => emit(await runConfigPath({ home: process.env.HOME ?? "" })));
1914
+ program.command("doctor").description("diagnose skillwiki setup issues").action(async () => emit(await runDoctor({
1915
+ home: process.env.HOME ?? "",
1916
+ envValue: process.env.WIKI_PATH,
1917
+ argv: process.argv
1918
+ })));
1919
+ program.command("archive <page> [vault]").description("archive a typed-knowledge page").action(async (page, vault) => {
1920
+ const v = await resolveVaultArg(vault);
1921
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1922
+ else emit(await runArchive({ vault: v.vault, page }));
1923
+ });
1924
+ program.command("drift [vault]").description("detect content drift in raw sources").action(async (vault) => {
1925
+ const v = await resolveVaultArg(vault);
1926
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1927
+ else emit(await runDrift({ vault: v.vault }));
1928
+ });
1929
+ program.command("dedup [vault]").description("detect duplicate raw sources by sha256").action(async (vault) => {
1930
+ const v = await resolveVaultArg(vault);
1931
+ if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
1932
+ else emit(await runDedup({ vault: v.vault }));
1933
+ });
1934
+ program.parseAsync(process.argv).catch((e) => {
1935
+ process.stdout.write(JSON.stringify({ ok: false, error: "INTERNAL", detail: { message: String(e) } }) + "\n");
1936
+ process.exit(1);
1937
+ });