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
@@ -3,334 +3,49 @@ import {
3
3
  latestFromCache,
4
4
  semverGt
5
5
  } from "./chunk-7I2TPIV5.js";
6
-
7
- // ../shared/src/exit-codes.ts
8
- var ExitCode = {
9
- OK: 0,
10
- INTERNAL_ERROR: 1,
11
- FILE_NOT_FOUND: 2,
12
- MISSING_CLOSING_DELIMITER: 3,
13
- SCHEME_REJECTED: 4,
14
- HOST_BLOCKED: 5,
15
- MALFORMED_URL: 6,
16
- INVALID_FRONTMATTER: 7,
17
- SCHEMA_NOT_DETECTED: 8,
18
- VAULT_PATH_INVALID: 9,
19
- WRITE_FAILED: 10,
20
- UNRESOLVED_MARKERS: 11,
21
- SOURCES_INCONSISTENT: 12,
22
- PREFLIGHT_FAILED: 13,
23
- ATOMIC_COPY_FAILED: 14,
24
- INIT_TARGET_NOT_EMPTY: 15,
25
- BROKEN_WIKILINKS: 16,
26
- TAG_NOT_IN_TAXONOMY: 17,
27
- INDEX_INCOMPLETE: 18,
28
- STALE_PAGE: 19,
29
- PAGE_TOO_LARGE: 20,
30
- LOG_ROTATE_NEEDED: 21,
31
- LINT_HAS_WARNINGS: 22,
32
- LINT_HAS_ERRORS: 23,
33
- ENV_WRITE_CONFLICT: 24,
34
- NO_VAULT_CONFIGURED: 25,
35
- INVALID_CONFIG_KEY: 26,
36
- CONFIG_WRITE_FAILED: 27,
37
- DOCTOR_HAS_WARNINGS: 28,
38
- DOCTOR_HAS_ERRORS: 29,
39
- ARCHIVE_TARGET_NOT_FOUND: 30,
40
- ARCHIVE_ALREADY_ARCHIVED: 31,
41
- DRIFT_DETECTED: 32,
42
- RAW_DEDUP_DETECTED: 33,
43
- MIGRATION_APPLIED: 34,
44
- UNKNOWN_WIKI_PROFILE: 35,
45
- DEDUP_APPLIED: 36,
46
- PROJECT_NOT_FOUND: 37,
47
- SYMLINK_FAILED: 38,
48
- COMPOUND_PROMOTED: 39,
49
- SKILL_VERSION_MISMATCH: 40,
50
- INGEST_VALIDATION_FAILED: 41,
51
- SYNC_PUSH_FAILED: 42,
52
- SYNC_PULL_FAILED: 43,
53
- BACKUP_SYNC_FAILED: 44,
54
- BACKUP_RESTORE_CONFLICTS: 45,
55
- USAGE: 46,
56
- BODY_TRUNCATION_GUARD: 47,
57
- SYNC_LOCK_HELD: 48,
58
- LOG_APPEND_LOCK_HELD: 49,
59
- FLEET_MANIFEST_INVALID: 50,
60
- SENSITIVE_CONTENT_DETECTED: 51,
61
- FLEET_SATELLITE_HEALTH_FAILED: 52,
62
- PROTECTED_SNAPSHOTTER_WRITE_BLOCKED: 53
63
- };
64
-
65
- // ../shared/src/json-output.ts
66
- function ok(data) {
67
- return { ok: true, data };
68
- }
69
- function err(error, detail) {
70
- return detail === void 0 ? { ok: false, error } : { ok: false, error, detail };
71
- }
72
-
73
- // ../shared/src/schemas.ts
74
- import { z } from "zod";
75
- var isoDate = z.string().refine((s) => {
76
- if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) return false;
77
- const d = /* @__PURE__ */ new Date(s + "T00:00:00Z");
78
- return !Number.isNaN(d.getTime()) && s === d.toISOString().slice(0, 10);
79
- }, { message: "must be YYYY-MM-DD" });
80
- var wikilink = z.string().regex(/^\[\[[^\[\]]+\]\]$/, 'must be "[[name]]"');
81
- var TypedKnowledgeSchema = z.object({
82
- title: z.string().min(1),
83
- aliases: z.array(z.string()).optional(),
84
- created: isoDate,
85
- updated: isoDate,
86
- type: z.enum(["entity", "concept", "comparison", "query"]),
87
- tags: z.array(z.string()),
88
- sources: z.array(z.string()).min(1),
89
- confidence: z.enum(["high", "medium", "low"]).optional(),
90
- contested: z.boolean().optional(),
91
- contradictions: z.array(z.string()).optional(),
92
- provenance: z.enum(["research", "project", "mixed"]).optional(),
93
- provenance_projects: z.array(wikilink).optional(),
94
- work_items: z.array(wikilink).optional(),
95
- stale_ttl: z.number().int().positive().optional()
96
- }).superRefine((v, ctx) => {
97
- if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
98
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
99
- }
100
- });
101
- var sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
102
- var RawSourceSchema = z.object({
103
- title: z.string().min(1).optional(),
104
- source_url: z.string().nullable(),
105
- created: isoDate.optional(),
106
- ingested: isoDate,
107
- ingested_by: z.enum(["wiki-ingest", "proj-work", "manual"]).optional(),
108
- sha256: sha256Hex.optional(),
109
- project: wikilink.optional(),
110
- work_item: wikilink.optional(),
111
- kind: z.enum(["postmortem", "session-log", "meeting-notes", "other", "idea", "bug", "task", "note"]).optional()
112
- }).superRefine((v, ctx) => {
113
- if (v.work_item !== void 0 && (v.project === void 0 || v.kind === void 0)) {
114
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: "project and kind are required when work_item is set" });
115
- }
116
- });
117
- var WorkItemSchema = z.object({
118
- title: z.string().min(1),
119
- aliases: z.array(z.string()).optional(),
120
- created: isoDate,
121
- updated: isoDate,
122
- started: isoDate,
123
- completed: isoDate.optional(),
124
- kind: z.enum(["feature", "issue", "refactor", "decision"]),
125
- status: z.enum(["planned", "in-progress", "completed", "abandoned"]),
126
- priority: z.enum(["high", "medium", "low"]),
127
- project: wikilink,
128
- owner: wikilink.optional(),
129
- parent: wikilink.optional(),
130
- related: z.array(wikilink).optional(),
131
- sources: z.array(z.string()).optional()
132
- }).superRefine((v, ctx) => {
133
- if (v.status === "completed" && !v.completed) {
134
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["completed"], message: "required when status is completed" });
135
- }
136
- });
137
- var CompoundSchema = z.object({
138
- title: z.string().min(1),
139
- aliases: z.array(z.string()).optional(),
140
- created: isoDate,
141
- updated: isoDate,
142
- type: z.enum(["lesson", "pattern", "antipattern", "gotcha"]),
143
- tags: z.array(z.string()),
144
- confidence: z.enum(["high", "medium", "low"]),
145
- contradicts: z.array(z.string()).optional(),
146
- project: wikilink,
147
- work_items: z.array(wikilink).min(1),
148
- promoted_to: wikilink.optional(),
149
- cssclasses: z.array(z.string()).optional()
150
- });
151
- var sessionPinPath = z.string().regex(
152
- /^(entities|concepts|comparisons|queries|meta)\/.+\.md$/,
153
- "must reference a typed-knowledge markdown page"
154
- );
155
- var SessionPinSchema = z.object({
156
- title: z.string().min(1),
157
- path: sessionPinPath,
158
- scope: z.enum(["global", "project"]),
159
- project: wikilink.optional(),
160
- summary: z.string().min(1).optional(),
161
- updated: isoDate.optional()
162
- }).superRefine((v, ctx) => {
163
- if (v.scope === "project" && v.project === void 0) {
164
- ctx.addIssue({
165
- code: z.ZodIssueCode.custom,
166
- path: ["project"],
167
- message: "project is required when scope is project"
168
- });
169
- }
170
- });
171
- var MetaSchema = z.object({
172
- title: z.string().min(1),
173
- aliases: z.array(z.string()).optional(),
174
- created: isoDate,
175
- updated: isoDate,
176
- type: z.literal("meta"),
177
- tags: z.array(z.string()),
178
- confidence: z.enum(["high", "medium", "low"]).optional(),
179
- provenance: z.enum(["research", "project", "mixed"]).optional(),
180
- provenance_projects: z.array(wikilink).optional(),
181
- generated_by: z.string().min(1).optional(),
182
- generated_at: z.string().datetime().optional(),
183
- generated_kind: z.enum(["session-brief"]).optional(),
184
- meta_kind: z.enum(["session-pins"]).optional(),
185
- stale_ttl: z.number().int().positive().optional(),
186
- pins: z.array(SessionPinSchema).optional()
187
- }).superRefine((v, ctx) => {
188
- const isGeneratedSessionBrief = v.generated_kind === "session-brief";
189
- const isSessionPins = v.meta_kind === "session-pins";
190
- if (isSessionPins && (!v.pins || v.pins.length === 0)) {
191
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["pins"], message: "required when meta_kind is session-pins" });
192
- }
193
- if (!isGeneratedSessionBrief && !isSessionPins && (!v.provenance_projects || v.provenance_projects.length < 2)) {
194
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "meta pages must reference \u22652 projects" });
195
- }
196
- if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
197
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
198
- }
199
- });
200
- var hostId = z.string().regex(/^[a-z0-9][a-z0-9_-]*$/, "must be a lowercase host id");
201
- var endpointName = z.string().min(1).regex(/^[A-Za-z0-9_.-]+$/, "must be a hostname-like token");
202
- var ipAddress = z.string().min(1).regex(/^[0-9a-fA-F:.]+$/, "must be an IP address token");
203
- var sshAlias = z.string().min(1).regex(/^[A-Za-z0-9_.@-]+$/, "must be an SSH alias token");
204
- var sshUser = z.string().min(1).regex(/^[A-Za-z0-9_.-]+$/, "must be an SSH user token");
205
- var absolutePath = z.string().min(1).regex(/^\//, "must be an absolute path");
206
- var FleetAccessProfileSchema = z.object({
207
- status: z.enum(["local", "configured", "planned", "absent", "unknown"]),
208
- ssh_aliases: z.array(sshAlias).optional(),
209
- users: z.array(sshUser).optional(),
210
- transports: z.array(z.enum(["local", "public-ip", "tailscale", "private-lan"])).min(1)
211
- }).strict();
212
- var FleetHostIdentitySchema = z.object({
213
- hostnames: z.array(endpointName).min(1),
214
- public_addresses: z.array(ipAddress).optional(),
215
- private_addresses: z.array(ipAddress).optional(),
216
- tailscale: z.object({
217
- node_names: z.array(endpointName).optional(),
218
- magicdns_names: z.array(endpointName).optional(),
219
- addresses: z.array(ipAddress).optional()
220
- }).strict().optional()
221
- }).strict();
222
- var FleetSkillwikiSatelliteSchema = z.object({
223
- enabled: z.boolean(),
224
- user: sshUser,
225
- vault_path: absolutePath,
226
- repo_path: absolutePath,
227
- ssh_alias: sshAlias,
228
- scheduler: z.enum(["systemd"]),
229
- timezone: z.string().min(1).optional(),
230
- jobs: z.array(z.enum([
231
- "self-update-check",
232
- "vault-sync-preflight",
233
- "agent-memory-trends-daily",
234
- "session-brief-refresh",
235
- "health-summary"
236
- ])).min(1),
237
- cadence: z.object({
238
- self_update_check: z.literal("every-4-hours").optional(),
239
- daily_window: z.string().min(1).optional()
240
- }).strict().optional()
241
- }).strict();
242
- var FleetHostSchema = z.object({
243
- class: z.enum(["dev-macos", "dev-linux", "prod-linux", "unknown"]),
244
- role: z.enum(["leaf", "snapshotter"]),
245
- writes_to: z.array(z.enum(["s3", "github"])).min(1),
246
- protected: z.boolean().optional(),
247
- identity: FleetHostIdentitySchema,
248
- access: z.object({
249
- from: z.record(hostId, FleetAccessProfileSchema).optional()
250
- }).strict().optional(),
251
- maintenance: z.object({
252
- skillwiki_satellite: FleetSkillwikiSatelliteSchema.optional()
253
- }).strict().optional()
254
- }).strict();
255
- var FleetManifestSchema = z.object({
256
- "$schema": z.string().url().optional(),
257
- schema_version: z.literal(1),
258
- vault_remote: z.string().min(1),
259
- s3_remote: z.string().min(1).optional(),
260
- hosts: z.record(hostId, FleetHostSchema)
261
- }).strict().superRefine((v, ctx) => {
262
- const entries = Object.entries(v.hosts);
263
- if (entries.length === 0) {
264
- ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["hosts"], message: "must contain at least one host" });
265
- }
266
- const snapshotters = entries.filter(([, host]) => host.role === "snapshotter").map(([id]) => id);
267
- if (snapshotters.length !== 1) {
268
- ctx.addIssue({
269
- code: z.ZodIssueCode.custom,
270
- path: ["hosts"],
271
- message: `must contain exactly one snapshotter host, found ${snapshotters.length}`
272
- });
273
- }
274
- });
275
- function detectSchema(fm) {
276
- const COMPOUND_TYPES = /* @__PURE__ */ new Set(["lesson", "pattern", "antipattern", "gotcha"]);
277
- if (typeof fm.type === "string" && COMPOUND_TYPES.has(fm.type) && "project" in fm) return { schema: "compound" };
278
- if (fm.type === "meta") return { schema: "meta" };
279
- if ("type" in fm && "sources" in fm) return { schema: "typed-knowledge" };
280
- if ("ingested" in fm && ("source_url" in fm || "sha256" in fm)) return { schema: "raw" };
281
- const RAW_KINDS = /* @__PURE__ */ new Set(["postmortem", "session-log", "meeting-notes", "other", "idea", "bug", "task", "note"]);
282
- if ("ingested" in fm && typeof fm.kind === "string" && RAW_KINDS.has(fm.kind)) return { schema: "raw" };
283
- if ("kind" in fm && "status" in fm) return { schema: "work-item" };
284
- return { schema: null };
285
- }
286
-
287
- // ../shared/src/blocked-hosts.ts
288
- var METADATA_HOSTS = /* @__PURE__ */ new Set([
289
- "metadata.google.internal",
290
- "metadata"
291
- ]);
292
- var METADATA_IPS = /* @__PURE__ */ new Set(["169.254.169.254"]);
293
- function ipv4ToInt(ip) {
294
- const parts = ip.split(".");
295
- if (parts.length !== 4) return null;
296
- let n = 0;
297
- for (const p of parts) {
298
- const v = Number(p);
299
- if (!Number.isInteger(v) || v < 0 || v > 255) return null;
300
- n = (n << 8) + v;
301
- }
302
- return n >>> 0;
303
- }
304
- function inRange(ip, baseStr, prefix) {
305
- const ipN = ipv4ToInt(ip);
306
- const baseN = ipv4ToInt(baseStr);
307
- if (ipN === null || baseN === null) return false;
308
- const mask = prefix === 0 ? 0 : ~0 << 32 - prefix >>> 0;
309
- return (ipN & mask) === (baseN & mask);
310
- }
311
- function isBlockedHost(host) {
312
- const lower = host.toLowerCase();
313
- if (METADATA_HOSTS.has(lower)) return true;
314
- if (METADATA_IPS.has(host)) return true;
315
- if (lower === "::1") return true;
316
- if (lower.startsWith("fe80:")) return true;
317
- if (ipv4ToInt(host) === null) return false;
318
- if (inRange(host, "10.0.0.0", 8)) return true;
319
- if (inRange(host, "172.16.0.0", 12)) return true;
320
- if (inRange(host, "192.168.0.0", 16)) return true;
321
- if (inRange(host, "169.254.0.0", 16)) return true;
322
- if (inRange(host, "127.0.0.0", 8)) return true;
323
- return false;
324
- }
325
-
326
- // ../shared/src/error-message.ts
327
- function getErrorMessage(e) {
328
- return e instanceof Error ? e.message : String(e);
329
- }
6
+ import {
7
+ atomicWriteText,
8
+ extractFrontmatter,
9
+ mapWithConcurrency,
10
+ prepareTypedPage,
11
+ readPage,
12
+ readPageCached,
13
+ redactSensitiveContent,
14
+ renderRootIndex,
15
+ resolveReadOnlyVaultRoot,
16
+ scanSensitiveContent,
17
+ scanVault,
18
+ splitFrontmatter,
19
+ vaultIoConcurrency,
20
+ writeRootIndexProjection
21
+ } from "./chunk-IZABIE44.js";
22
+ import {
23
+ CONFIG_KEYS,
24
+ isValidWikiProfileKey,
25
+ loadFleetManifestAndHost,
26
+ parseDotenvFile,
27
+ parseDotenvText,
28
+ profileKey,
29
+ satelliteGateFromFleetLoad,
30
+ snapshotterAliasForLocalHost,
31
+ writeDotenv
32
+ } from "./chunk-S5ABQCXQ.js";
33
+ import {
34
+ CompoundSchema,
35
+ ExitCode,
36
+ MetaSchema,
37
+ RawSourceSchema,
38
+ TypedKnowledgeSchema,
39
+ WorkItemSchema,
40
+ detectSchema,
41
+ err,
42
+ getErrorMessage,
43
+ ok
44
+ } from "./chunk-C5OLZRRM.js";
330
45
 
331
46
  // src/commands/log-append.ts
332
- import { readFile as readFile2, stat } from "fs/promises";
333
- import { join as join4 } from "path";
47
+ import { readFile, stat } from "fs/promises";
48
+ import { join as join3 } from "path";
334
49
 
335
50
  // src/utils/last-op.ts
336
51
  import { readFileSync, writeFileSync, mkdirSync, unlinkSync, existsSync } from "fs";
@@ -374,58 +89,12 @@ function clearLastOp(vault) {
374
89
  }
375
90
  }
376
91
 
377
- // src/utils/atomic-write.ts
378
- import { randomBytes } from "crypto";
379
- import { open, readFile, rename, unlink } from "fs/promises";
380
- import { basename, dirname, join as join2 } from "path";
381
- async function readExisting(path) {
382
- try {
383
- return await readFile(path, "utf8");
384
- } catch (error) {
385
- if (error.code === "ENOENT") return null;
386
- throw error;
387
- }
388
- }
389
- async function atomicWriteText(path, text) {
390
- let existing;
391
- try {
392
- existing = await readExisting(path);
393
- } catch (error) {
394
- return err("WRITE_FAILED", { path, phase: "read-existing", message: String(error) });
395
- }
396
- if (existing === text) return ok({ changed: false, existed: true });
397
- const tmp = join2(
398
- dirname(path),
399
- `.${basename(path)}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`
400
- );
401
- try {
402
- const handle = await open(tmp, "wx");
403
- try {
404
- await handle.writeFile(text, "utf8");
405
- try {
406
- await handle.sync();
407
- } catch {
408
- }
409
- } finally {
410
- await handle.close();
411
- }
412
- await rename(tmp, path);
413
- return ok({ changed: true, existed: existing !== null });
414
- } catch (error) {
415
- try {
416
- await unlink(tmp);
417
- } catch {
418
- }
419
- return err("WRITE_FAILED", { path, phase: "atomic-write", message: String(error) });
420
- }
421
- }
422
-
423
92
  // src/utils/log-lock.ts
424
- import { randomBytes as randomBytes2 } from "crypto";
93
+ import { randomBytes } from "crypto";
425
94
  import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
426
- import { join as join3 } from "path";
95
+ import { join as join2 } from "path";
427
96
  function logLockPath(vault) {
428
- return join3(vault, ".skillwiki", "log-append.lock");
97
+ return join2(vault, ".skillwiki", "log-append.lock");
429
98
  }
430
99
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
431
100
  function readLogLock(path) {
@@ -441,10 +110,10 @@ async function acquireLogLock(vault, opts = {}) {
441
110
  const staleMs = opts.staleMs ?? 1e4;
442
111
  const reclaimStale = opts.reclaimStale ?? true;
443
112
  const path = logLockPath(vault);
444
- const dir = join3(vault, ".skillwiki");
113
+ const dir = join2(vault, ".skillwiki");
445
114
  if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
446
115
  const deadline = Date.now() + retryMs;
447
- const ownerToken = randomBytes2(16).toString("hex");
116
+ const ownerToken = randomBytes(16).toString("hex");
448
117
  const acquired = (/* @__PURE__ */ new Date()).toISOString();
449
118
  const content = JSON.stringify({ pid: process.pid, owner_token: ownerToken, acquired }) + "\n";
450
119
  for (; ; ) {
@@ -487,146 +156,6 @@ function releaseLogLock(handle) {
487
156
  }
488
157
  }
489
158
 
490
- // src/utils/sensitive-content.ts
491
- import { createHash } from "crypto";
492
- var REDACTED_RE = /\[REDACTED:[^\]]+\]/i;
493
- var SYNTHETIC_RE = /^(?:<[^>]+>|\$\{[^}]+\}|REPLACE_WITH_[A-Z0-9_]+|YOUR_[A-Z0-9_]+|EXAMPLE_[A-Z0-9_]+)$/i;
494
- var MATCHERS = [
495
- {
496
- kind: "private_key",
497
- re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g
498
- },
499
- {
500
- kind: "authorization_header",
501
- re: /\bAuthorization["']?\s*:\s*["']?(Bearer\s+[A-Za-z0-9._~+/-]{20,})["']?/gi,
502
- valueGroup: 1
503
- },
504
- {
505
- kind: "cookie",
506
- re: /\b(?:Cookie|Set-Cookie)\s*:\s*([^\n]{20,})/gi,
507
- valueGroup: 1
508
- },
509
- {
510
- kind: "jwt",
511
- re: /\b([A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,})\b/g,
512
- valueGroup: 1
513
- },
514
- {
515
- kind: "provider_key",
516
- re: /\b(sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|gh[pousr]_[A-Za-z0-9_=-]{20,})\b/g,
517
- valueGroup: 1
518
- },
519
- {
520
- kind: "access_key",
521
- re: /\b((?:AKIA|ASIA)[A-Z0-9]{16})\b/g,
522
- valueGroup: 1
523
- },
524
- {
525
- kind: "access_key",
526
- re: /\b(?:access[-_ ]?key|credential)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
527
- valueGroup: 1
528
- },
529
- {
530
- kind: "api_key",
531
- re: /\b(?:api[-_ ]?key)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
532
- valueGroup: 1
533
- },
534
- {
535
- kind: "password",
536
- re: /\b(?:pass(?:word|wd)?)["']?\s*[:=]\s*["']?([^\s`"']{8,})["']?/gi,
537
- valueGroup: 1
538
- },
539
- {
540
- kind: "secret",
541
- re: /\b(?:secret|client[-_ ]?secret)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
542
- valueGroup: 1
543
- },
544
- {
545
- kind: "token",
546
- re: /\b(?:token|session)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
547
- valueGroup: 1
548
- }
549
- ];
550
- function fingerprint(value) {
551
- return createHash("sha256").update(value).digest("hex").slice(0, 12);
552
- }
553
- function lineFor(text, offset) {
554
- return text.slice(0, offset).split(/\r?\n/).length;
555
- }
556
- function redactMarker(kind, value) {
557
- return `[REDACTED:${kind}:${fingerprint(value)}]`;
558
- }
559
- function isSyntheticPlaceholder(value) {
560
- return REDACTED_RE.test(value) || SYNTHETIC_RE.test(value.trim());
561
- }
562
- function collectMatches(text) {
563
- const matches = [];
564
- for (const matcher of MATCHERS) {
565
- matcher.re.lastIndex = 0;
566
- for (const m of text.matchAll(matcher.re)) {
567
- const whole = m[0];
568
- const start = m.index ?? 0;
569
- if (REDACTED_RE.test(whole)) continue;
570
- const value = matcher.valueGroup ? m[matcher.valueGroup] : whole;
571
- if (isSyntheticPlaceholder(value)) continue;
572
- const valueOffset = whole.lastIndexOf(value);
573
- const valueStart = start + Math.max(0, valueOffset);
574
- matches.push({
575
- start,
576
- end: start + whole.length,
577
- valueStart,
578
- valueEnd: valueStart + value.length,
579
- kind: matcher.kind
580
- });
581
- }
582
- }
583
- return matches.sort((a, b) => {
584
- if (a.valueStart !== b.valueStart) return a.valueStart - b.valueStart;
585
- return b.valueEnd - b.valueStart - (a.valueEnd - a.valueStart);
586
- });
587
- }
588
- function collapseOverlaps(matches) {
589
- const kept = [];
590
- for (const match of matches) {
591
- const overlaps = kept.some((k) => match.valueStart < k.valueEnd && match.valueEnd > k.valueStart);
592
- if (!overlaps) kept.push(match);
593
- }
594
- return kept;
595
- }
596
- function scanSensitiveContent(text, opts = {}) {
597
- return collapseOverlaps(collectMatches(text)).map((match) => {
598
- const value = text.slice(match.valueStart, match.valueEnd);
599
- const marker = redactMarker(match.kind, value);
600
- const rawPreview = text.slice(Math.max(0, match.start - 24), Math.min(text.length, match.end + 24));
601
- const preview = rawPreview.replace(value, marker);
602
- return {
603
- file: opts.file,
604
- line: lineFor(text, match.valueStart),
605
- kind: match.kind,
606
- preview,
607
- fingerprint: fingerprint(value)
608
- };
609
- });
610
- }
611
- function redactSensitiveContent(text, opts = {}) {
612
- const matches = collapseOverlaps(collectMatches(text));
613
- if (matches.length === 0) return { text, changed: false, findings: [] };
614
- let out = "";
615
- let cursor = 0;
616
- for (const match of matches) {
617
- const value = text.slice(match.valueStart, match.valueEnd);
618
- out += text.slice(cursor, match.valueStart);
619
- out += redactMarker(match.kind, value);
620
- cursor = match.valueEnd;
621
- }
622
- out += text.slice(cursor);
623
- return {
624
- text: out,
625
- changed: out !== text,
626
- findings: scanSensitiveContent(text, opts)
627
- };
628
- }
629
-
630
159
  // src/commands/log-append.ts
631
160
  var ENTRY_RE = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
632
161
  function operationMarker(operationId) {
@@ -638,7 +167,7 @@ function operationMarker(operationId) {
638
167
  async function appendWhileLocked(logPath, content, marker) {
639
168
  let logText;
640
169
  try {
641
- logText = await readFile2(logPath, "utf8");
170
+ logText = await readFile(logPath, "utf8");
642
171
  } catch {
643
172
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
644
173
  }
@@ -677,7 +206,7 @@ ${appendedContent}
677
206
  }
678
207
  async function runLogAppend(input) {
679
208
  try {
680
- await stat(join4(input.vault, "SCHEMA.md"));
209
+ await stat(join3(input.vault, "SCHEMA.md"));
681
210
  } catch {
682
211
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
683
212
  }
@@ -706,7 +235,7 @@ async function runLogAppend(input) {
706
235
  return { exitCode: ExitCode.LOG_APPEND_LOCK_HELD, result: err("LOG_APPEND_LOCK_HELD", { vault: input.vault }) };
707
236
  }
708
237
  const lockHandle = acquired.data;
709
- const logPath = join4(input.vault, "log.md");
238
+ const logPath = join3(input.vault, "log.md");
710
239
  let outcome;
711
240
  let released;
712
241
  try {
@@ -751,8 +280,8 @@ async function runLogAppend(input) {
751
280
 
752
281
  // src/utils/sync-lock.ts
753
282
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
754
- import { join as join5 } from "path";
755
- import { createHash as createHash2, randomBytes as randomBytes3 } from "crypto";
283
+ import { join as join4 } from "path";
284
+ import { createHash, randomBytes as randomBytes2 } from "crypto";
756
285
  function getEnvSessionId() {
757
286
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
758
287
  if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
@@ -765,7 +294,7 @@ function getSessionId() {
765
294
  }
766
295
  function getCwdHash(cwd) {
767
296
  const path = cwd || process.cwd();
768
- const hash = createHash2("sha256").update(path).digest("hex");
297
+ const hash = createHash("sha256").update(path).digest("hex");
769
298
  return hash.slice(0, 8);
770
299
  }
771
300
  function getCliSessionId(cwd) {
@@ -774,7 +303,7 @@ function getCliSessionId(cwd) {
774
303
  return `cli-${getCwdHash(cwd)}`;
775
304
  }
776
305
  function lockPath(vault) {
777
- return join5(vault, ".skillwiki", "sync.lock");
306
+ return join4(vault, ".skillwiki", "sync.lock");
778
307
  }
779
308
  function readLock(vault) {
780
309
  const path = lockPath(vault);
@@ -793,7 +322,7 @@ function isStale(lock, now) {
793
322
  }
794
323
  function acquireLock(vault, opts = {}) {
795
324
  const path = lockPath(vault);
796
- const dir = join5(vault, ".skillwiki");
325
+ const dir = join4(vault, ".skillwiki");
797
326
  if (!existsSync3(dir)) {
798
327
  mkdirSync3(dir, { recursive: true });
799
328
  }
@@ -864,7 +393,7 @@ function releaseLock(vault, opts = {}) {
864
393
  }
865
394
  }
866
395
  function acquireOwnedSyncLock(vault, opts) {
867
- const ownerToken = randomBytes3(16).toString("hex");
396
+ const ownerToken = randomBytes2(16).toString("hex");
868
397
  const sessionId = `publish-${process.pid}-${ownerToken.slice(0, 12)}`;
869
398
  const now = /* @__PURE__ */ new Date();
870
399
  const lock = {
@@ -878,7 +407,7 @@ function acquireOwnedSyncLock(vault, opts) {
878
407
  };
879
408
  const path = lockPath(vault);
880
409
  try {
881
- mkdirSync3(join5(vault, ".skillwiki"), { recursive: true });
410
+ mkdirSync3(join4(vault, ".skillwiki"), { recursive: true });
882
411
  } catch (error) {
883
412
  return err("WRITE_FAILED", { path, message: String(error) });
884
413
  }
@@ -909,40 +438,13 @@ function releaseOwnedSyncLock(handle) {
909
438
  }
910
439
 
911
440
  // src/commands/validate.ts
912
- import { createHash as createHash3 } from "crypto";
913
- import { readFile as readFile4 } from "fs/promises";
914
- import { resolve as resolve2, relative as relative2, sep as sep2 } from "path";
915
-
916
- // src/parsers/frontmatter.ts
917
- import yaml from "js-yaml";
918
- var FM_OPEN = /^---\r?\n/;
919
- function splitFrontmatter(text) {
920
- if (!FM_OPEN.test(text)) return ok({ rawFrontmatter: "", body: text, bodyStart: 0 });
921
- const afterOpen = text.replace(FM_OPEN, "");
922
- const closeIdx = afterOpen.search(/\r?\n---\r?\n/);
923
- if (closeIdx === -1) return err("MISSING_CLOSING_DELIMITER");
924
- const rawFrontmatter = afterOpen.slice(0, closeIdx);
925
- const closeMatch = afterOpen.slice(closeIdx).match(/\r?\n---\r?\n/);
926
- const bodyStart = text.length - (afterOpen.length - closeIdx - closeMatch[0].length);
927
- const body = text.slice(bodyStart);
928
- return ok({ rawFrontmatter, body, bodyStart });
929
- }
930
- function extractFrontmatter(text) {
931
- const split = splitFrontmatter(text);
932
- if (!split.ok) return split;
933
- if (!split.data.rawFrontmatter) return ok({});
934
- try {
935
- const parsed = yaml.load(split.data.rawFrontmatter, { schema: yaml.JSON_SCHEMA });
936
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return ok({});
937
- return ok(parsed);
938
- } catch (e) {
939
- return err("INVALID_FRONTMATTER", { message: getErrorMessage(e) });
940
- }
941
- }
441
+ import { createHash as createHash2 } from "crypto";
442
+ import { readFile as readFile3 } from "fs/promises";
443
+ import { resolve, relative, sep } from "path";
942
444
 
943
445
  // src/utils/index-entry.ts
944
- import { readFile as readFile3 } from "fs/promises";
945
- import { join as join6 } from "path";
446
+ import { readFile as readFile2 } from "fs/promises";
447
+ import { join as join5 } from "path";
946
448
  var TYPE_SECTION = {
947
449
  entity: "Entities",
948
450
  concept: "Concepts",
@@ -986,128 +488,31 @@ function renderIndexUpsert(text, input) {
986
488
  });
987
489
  }
988
490
  async function upsertIndexEntry(input) {
989
- const path = join6(input.vault, "index.md");
990
- let current;
991
- try {
992
- current = await readFile3(path, "utf8");
993
- } catch (error) {
994
- return err("FILE_NOT_FOUND", { path, message: String(error) });
995
- }
996
- const rendered = renderIndexUpsert(current, input);
997
- if (!rendered.ok) return rendered;
998
- if (!rendered.data.changed) return ok({ changed: false });
999
- const written = await atomicWriteText(path, rendered.data.text);
1000
- return written.ok ? ok({ changed: written.data.changed }) : written;
1001
- }
1002
-
1003
- // src/utils/typed-page.ts
1004
- import { lstatSync, realpathSync } from "fs";
1005
- import { dirname as dirname2, posix, relative, resolve, sep } from "path";
1006
- var TYPE_DIRECTORY = {
1007
- entity: "entities",
1008
- concept: "concepts",
1009
- comparison: "comparisons",
1010
- query: "queries",
1011
- meta: "meta"
1012
- };
1013
- function validateTypedTarget(target) {
1014
- const segments = target.split("/");
1015
- 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)) {
1016
- return err("VAULT_PATH_INVALID", { target, message: "unsafe typed-page target" });
1017
- }
1018
- return ok(target);
1019
- }
1020
- function assertTargetInsideVault(vault, target) {
1021
- const validated = validateTypedTarget(target);
1022
- if (!validated.ok) return validated;
1023
- let vaultReal;
491
+ const path = join5(input.vault, "index.md");
492
+ let before = "";
1024
493
  try {
1025
- vaultReal = realpathSync(vault);
494
+ before = await readFile2(path, "utf8");
1026
495
  } catch {
1027
- return err("VAULT_PATH_INVALID", { target, message: "vault realpath failed" });
496
+ before = "";
1028
497
  }
1029
- const absolutePath2 = resolve(vaultReal, target);
1030
- const parent = dirname2(absolutePath2);
1031
- let parentReal;
1032
- try {
1033
- parentReal = realpathSync(parent);
1034
- } catch {
1035
- return err("VAULT_PATH_INVALID", { target, message: "target parent realpath failed" });
1036
- }
1037
- const parentRelative = relative(vaultReal, parentReal).split(sep).join("/");
1038
- if (parentRelative === ".." || parentRelative.startsWith("../")) {
1039
- return err("VAULT_PATH_INVALID", { target, message: "target parent escapes vault" });
1040
- }
1041
- if (parentReal !== parent) {
1042
- return err("VAULT_PATH_INVALID", { target, message: "target parent may not be a symlink alias" });
1043
- }
1044
- let existingRealPath;
1045
- try {
1046
- const targetStat = lstatSync(absolutePath2);
1047
- if (targetStat.isSymbolicLink()) {
1048
- return err("VAULT_PATH_INVALID", { target, message: "target may not be a symlink" });
1049
- }
1050
- if (!targetStat.isFile()) {
1051
- return err("VAULT_PATH_INVALID", { target, message: "existing target must be a regular file" });
1052
- }
1053
- try {
1054
- existingRealPath = realpathSync(absolutePath2);
1055
- } catch {
1056
- return err("VAULT_PATH_INVALID", { target, message: "target realpath failed" });
1057
- }
1058
- } catch (error) {
1059
- if (error.code !== "ENOENT") {
1060
- return err("VAULT_PATH_INVALID", { target, message: "target lstat failed" });
1061
- }
498
+ const projection = await renderRootIndex({ vault: input.vault, currentText: before });
499
+ if (projection.ok) {
500
+ if (projection.data.text === before) return ok({ changed: false });
501
+ const written2 = await writeRootIndexProjection(input.vault, projection.data);
502
+ if (!written2.ok) return written2;
503
+ return ok({ changed: written2.data.changed });
1062
504
  }
1063
- return ok({ absolutePath: absolutePath2, existingRealPath });
1064
- }
1065
- function invalidFrontmatter(target, issues) {
1066
- return err("INVALID_FRONTMATTER", {
1067
- target,
1068
- errors: issues.map((issue) => ({ path: issue.path.join("."), message: issue.message }))
505
+ const rendered = renderIndexUpsert(before, input);
506
+ if (!rendered.ok) return rendered;
507
+ if (!rendered.data.changed) return ok({ changed: false });
508
+ const written = await writeRootIndexProjection(input.vault, {
509
+ text: rendered.data.text,
510
+ entries: [],
511
+ duplicates_removed: 0,
512
+ ghosts_removed: []
1069
513
  });
1070
- }
1071
- function prepareTypedPage(content, target) {
1072
- const safeTarget = validateTypedTarget(target);
1073
- if (!safeTarget.ok) return safeTarget;
1074
- const sensitive = scanSensitiveContent(content, { file: target });
1075
- if (sensitive.length > 0) {
1076
- return err("SENSITIVE_CONTENT_DETECTED", { file: target, findings: sensitive });
1077
- }
1078
- const frontmatter = extractFrontmatter(content);
1079
- if (!frontmatter.ok) return frontmatter;
1080
- const detected = detectSchema(frontmatter.data);
1081
- if (detected.schema === "typed-knowledge") {
1082
- const parsed = TypedKnowledgeSchema.safeParse(frontmatter.data);
1083
- if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
1084
- const expectedDirectory = TYPE_DIRECTORY[parsed.data.type];
1085
- if (!expectedDirectory || !target.startsWith(`${expectedDirectory}/`)) {
1086
- return err("SCHEME_REJECTED", { target, type: parsed.data.type, message: "frontmatter type does not match target directory" });
1087
- }
1088
- return ok({
1089
- target,
1090
- title: parsed.data.title,
1091
- type: parsed.data.type,
1092
- tags: [...parsed.data.tags],
1093
- content
1094
- });
1095
- }
1096
- if (detected.schema === "meta") {
1097
- const parsed = MetaSchema.safeParse(frontmatter.data);
1098
- if (!parsed.success) return invalidFrontmatter(target, parsed.error.issues);
1099
- if (!target.startsWith("meta/")) {
1100
- return err("SCHEME_REJECTED", { target, type: "meta", message: "frontmatter type does not match target directory" });
1101
- }
1102
- return ok({
1103
- target,
1104
- title: parsed.data.title,
1105
- type: "meta",
1106
- tags: [...parsed.data.tags],
1107
- content
1108
- });
1109
- }
1110
- return invalidFrontmatter(target, []);
514
+ if (!written.ok) return written;
515
+ return ok({ changed: written.data.changed });
1111
516
  }
1112
517
 
1113
518
  // src/commands/validate.ts
@@ -1121,7 +526,7 @@ var SCHEMAS = {
1121
526
  async function runValidate(input) {
1122
527
  let text;
1123
528
  try {
1124
- text = await readFile4(input.file, "utf8");
529
+ text = await readFile3(input.file, "utf8");
1125
530
  } catch {
1126
531
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
1127
532
  }
@@ -1172,13 +577,13 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
1172
577
  let logUpdated = false;
1173
578
  let applyHint = "";
1174
579
  if (input.apply && input.vault) {
1175
- const absFile = resolve2(input.file);
1176
- const absVault = resolve2(input.vault);
1177
- const relPath = relative2(absVault, absFile).split(sep2).join("/");
580
+ const absFile = resolve(input.file);
581
+ const absVault = resolve(input.vault);
582
+ const relPath = relative(absVault, absFile).split(sep).join("/");
1178
583
  if (relPath.startsWith("..")) {
1179
584
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { reason: `file ${input.file} is not inside vault ${input.vault}` }) };
1180
585
  }
1181
- const operationId = createHash3("sha256").update("skillwiki-validate-apply-v1\0").update(relPath).update("\0").update(text).digest("hex");
586
+ const operationId = createHash2("sha256").update("skillwiki-validate-apply-v1\0").update(relPath).update("\0").update(text).digest("hex");
1182
587
  if (det.schema === "typed-knowledge" || det.schema === "meta") {
1183
588
  const prepared = prepareTypedPage(text, relPath);
1184
589
  if (!prepared.ok) {
@@ -1262,109 +667,7 @@ ${errors.map((e) => ` ${e.path}: ${e.message}`).join("\n")}` })
1262
667
 
1263
668
  // src/commands/graph.ts
1264
669
  import { writeFile, mkdir } from "fs/promises";
1265
- import { dirname as dirname3 } from "path";
1266
-
1267
- // src/utils/vault.ts
1268
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
1269
- import { readFile as readFile5, readdir, stat as stat2 } from "fs/promises";
1270
- import { join as join7, relative as relative3, sep as sep3 } from "path";
1271
- var TYPED_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
1272
- var SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules"]);
1273
- var DEFAULT_IO_CONCURRENCY = 1;
1274
- function vaultIoConcurrency() {
1275
- const raw = Number.parseInt(process.env.SKILLWIKI_VAULT_IO_CONCURRENCY ?? "", 10);
1276
- return Number.isFinite(raw) && raw > 0 ? Math.min(raw, 64) : DEFAULT_IO_CONCURRENCY;
1277
- }
1278
- function decodeProcMountPath(value) {
1279
- return value.replace(/\\040/g, " ");
1280
- }
1281
- function isRcloneFuseVaultFromMounts(root, mounts) {
1282
- return mounts.split(/\r?\n/).some((line) => {
1283
- const parts = line.split(" ");
1284
- if (parts.length < 3) return false;
1285
- const mountPoint = decodeProcMountPath(parts[1]);
1286
- const fsType = parts[2];
1287
- return fsType === "fuse.rclone" && (root === mountPoint || root.startsWith(`${mountPoint}/`));
1288
- });
1289
- }
1290
- function resolveReadOnlyVaultRootWithMounts(root, mounts) {
1291
- if (/^(1|true|yes)$/i.test(process.env.SKILLWIKI_DISABLE_VAULT_READ_MIRROR ?? "")) {
1292
- return { root, mirrored: false };
1293
- }
1294
- const explicitMirror = process.env.SKILLWIKI_VAULT_READ_MIRROR;
1295
- if (explicitMirror && existsSync4(join7(explicitMirror, "SCHEMA.md"))) {
1296
- return { root: explicitMirror, mirrored: explicitMirror !== root };
1297
- }
1298
- const siblingMirror = `${root}-git`;
1299
- if (isRcloneFuseVaultFromMounts(root, mounts) && existsSync4(join7(siblingMirror, "SCHEMA.md"))) {
1300
- return { root: siblingMirror, mirrored: true };
1301
- }
1302
- return { root, mirrored: false };
1303
- }
1304
- function resolveReadOnlyVaultRoot(root) {
1305
- let mounts = "";
1306
- try {
1307
- mounts = readFileSync4("/proc/mounts", "utf8");
1308
- } catch {
1309
- }
1310
- return resolveReadOnlyVaultRootWithMounts(root, mounts);
1311
- }
1312
- async function mapWithConcurrency(items, limit, mapper) {
1313
- const out = new Array(items.length);
1314
- let next = 0;
1315
- const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
1316
- for (; ; ) {
1317
- const index = next++;
1318
- if (index >= items.length) return;
1319
- out[index] = await mapper(items[index], index);
1320
- }
1321
- });
1322
- await Promise.all(workers);
1323
- return out;
1324
- }
1325
- async function scanVault(root) {
1326
- try {
1327
- await stat2(join7(root, "SCHEMA.md"));
1328
- } catch {
1329
- return err("VAULT_PATH_INVALID", { root, reason: "SCHEMA.md missing" });
1330
- }
1331
- const all = await walk(root);
1332
- const rels = all.map((p) => ({ absPath: p, relPath: relative3(root, p).split(sep3).join("/") }));
1333
- return ok({
1334
- root,
1335
- allMarkdown: rels,
1336
- typedKnowledge: rels.filter((p) => TYPED_DIRS.some((d) => p.relPath.startsWith(d + "/"))),
1337
- raw: rels.filter((p) => p.relPath.startsWith("raw/")),
1338
- workItems: rels.filter((p) => /^projects\/[^/]+\/work\/[^/]+\/(spec|plan|log)\.md$/.test(p.relPath)),
1339
- compound: rels.filter((p) => /^projects\/[^/]+\/compound\//.test(p.relPath))
1340
- });
1341
- }
1342
- async function walk(dir) {
1343
- const entries = await readdir(dir, { withFileTypes: true });
1344
- const out = [];
1345
- const subdirs = [];
1346
- for (const e of entries) {
1347
- const p = join7(dir, e.name);
1348
- if (e.isDirectory()) {
1349
- if (SKIP_DIRS.has(e.name)) continue;
1350
- subdirs.push(p);
1351
- } else if (e.isFile() && e.name.endsWith(".md")) out.push(p);
1352
- }
1353
- const nested = await mapWithConcurrency(subdirs, Math.min(8, vaultIoConcurrency()), walk);
1354
- for (const files of nested) out.push(...files);
1355
- return out;
1356
- }
1357
- async function readPage(p) {
1358
- return readFile5(p.absPath, "utf8");
1359
- }
1360
- async function readPageCached(p, cache) {
1361
- if (!cache) return readPage(p);
1362
- const existing = cache.get(p.absPath);
1363
- if (existing) return existing;
1364
- const pending = readPage(p);
1365
- cache.set(p.absPath, pending);
1366
- return pending;
1367
- }
670
+ import { dirname } from "path";
1368
671
 
1369
672
  // src/parsers/wikilinks.ts
1370
673
  var FENCE = /```[\s\S]*?```|`[^`\n]*`/g;
@@ -1518,7 +821,7 @@ async function runGraphBuild(input) {
1518
821
  const adamicAdar = computeAdamicAdar(adjacency);
1519
822
  const edge_count = Object.values(adjacency).reduce((acc, arr) => acc + arr.length, 0);
1520
823
  try {
1521
- await mkdir(dirname3(input.out), { recursive: true });
824
+ await mkdir(dirname(input.out), { recursive: true });
1522
825
  await writeFile(input.out, JSON.stringify({ adjacency, adamicAdar }, null, 2));
1523
826
  } catch (e) {
1524
827
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
@@ -1561,102 +864,8 @@ function computeAdamicAdar(adj) {
1561
864
  return out;
1562
865
  }
1563
866
 
1564
- // src/utils/dotenv.ts
1565
- import { readFile as readFile6, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1566
- import { dirname as dirname4 } from "path";
1567
- var CONFIG_KEYS = [
1568
- "WIKI_PATH",
1569
- "WIKI_LANG",
1570
- "SKILLWIKI_HOST_ID",
1571
- "AUTO_COMMIT",
1572
- "BACKUP_ENDPOINT",
1573
- "BACKUP_BUCKET",
1574
- "BACKUP_REGION",
1575
- "BACKUP_ACCESS_KEY_ID",
1576
- "BACKUP_SECRET_ACCESS_KEY"
1577
- ];
1578
- var _whitelist = new Set(CONFIG_KEYS);
1579
- var PROFILE_PATH_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/;
1580
- var PROFILE_LANG_RE = /^WIKI_([A-Z][A-Z0-9_]{0,31})_LANG$/;
1581
- var PROFILE_DEFAULT_RE = /^WIKI_DEFAULT$/;
1582
- function isValidWikiProfileKey(key) {
1583
- if (key === "WIKI_PATH" || key === "WIKI_LANG") return false;
1584
- return PROFILE_PATH_RE.test(key) || PROFILE_LANG_RE.test(key) || PROFILE_DEFAULT_RE.test(key);
1585
- }
1586
- function profileKey(name, suffix) {
1587
- return `WIKI_${name.toUpperCase().replace(/-/g, "_").replace(/[^A-Z0-9_]/g, "")}_${suffix}`;
1588
- }
1589
- function parseDotenvText(text) {
1590
- const out = {};
1591
- for (const rawLine of text.split(/\r?\n/)) {
1592
- const line = rawLine.trim();
1593
- if (line.length === 0 || line.startsWith("#")) continue;
1594
- const eq = line.indexOf("=");
1595
- if (eq <= 0) continue;
1596
- const key = line.slice(0, eq).trim();
1597
- const value = line.slice(eq + 1).trim();
1598
- if (!_whitelist.has(key) && !isValidWikiProfileKey(key)) continue;
1599
- if (value.length === 0) continue;
1600
- out[key] = value;
1601
- }
1602
- return out;
1603
- }
1604
- async function parseDotenvFile(path) {
1605
- let text;
1606
- try {
1607
- text = await readFile6(path, "utf8");
1608
- } catch {
1609
- return {};
1610
- }
1611
- return parseDotenvText(text);
1612
- }
1613
- async function writeDotenv(filePath, entries, originalContent) {
1614
- const lines = originalContent !== void 0 ? updateLines(originalContent, entries) : freshLines(entries);
1615
- await mkdir2(dirname4(filePath), { recursive: true });
1616
- await writeFile2(filePath, lines.join("\n") + "\n", "utf8");
1617
- }
1618
- function freshLines(entries) {
1619
- const out = [];
1620
- for (const [key, value] of Object.entries(entries)) {
1621
- if (value !== void 0) out.push(`${key}=${value}`);
1622
- }
1623
- return out;
1624
- }
1625
- function updateLines(originalContent, entries) {
1626
- let rawLines = originalContent.split(/\r?\n/);
1627
- if (rawLines.length > 0 && rawLines[rawLines.length - 1] === "") {
1628
- rawLines = rawLines.slice(0, -1);
1629
- }
1630
- const keysToWrite = new Set(Object.keys(entries));
1631
- const out = [];
1632
- for (const line of rawLines) {
1633
- const trimmed = line.trim();
1634
- if (trimmed.length === 0 || trimmed.startsWith("#")) {
1635
- out.push(line);
1636
- continue;
1637
- }
1638
- const eq = trimmed.indexOf("=");
1639
- if (eq <= 0) {
1640
- out.push(line);
1641
- continue;
1642
- }
1643
- const key = trimmed.slice(0, eq).trim();
1644
- if (keysToWrite.has(key)) {
1645
- out.push(`${key}=${entries[key]}`);
1646
- keysToWrite.delete(key);
1647
- } else {
1648
- out.push(line);
1649
- }
1650
- }
1651
- for (const key of keysToWrite) {
1652
- const value = entries[key];
1653
- if (value !== void 0) out.push(`${key}=${value}`);
1654
- }
1655
- return out;
1656
- }
1657
-
1658
867
  // src/utils/wiki-path.ts
1659
- import { join as join8 } from "path";
868
+ import { join as join6 } from "path";
1660
869
  async function resolveInitTimePath(input) {
1661
870
  const chain = [];
1662
871
  if (input.flag !== void 0 && input.flag.length > 0) {
@@ -1669,27 +878,27 @@ async function resolveInitTimePath(input) {
1669
878
  return { path: input.envValue, source: "env", ...input.explain ? { chain } : {} };
1670
879
  }
1671
880
  if (input.explain) chain.push({ source: "env", matched: false });
1672
- const sw = await parseDotenvFile(join8(input.home, ".skillwiki", ".env"));
881
+ const sw = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
1673
882
  if (sw.WIKI_PATH !== void 0) {
1674
883
  if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: true, value: sw.WIKI_PATH });
1675
884
  return { path: sw.WIKI_PATH, source: "skillwiki-dotenv", ...input.explain ? { chain } : {} };
1676
885
  }
1677
886
  if (input.explain) chain.push({ source: "skillwiki-dotenv", matched: false });
1678
- const hermes = await parseDotenvFile(join8(input.home, ".hermes", ".env"));
887
+ const hermes = await parseDotenvFile(join6(input.home, ".hermes", ".env"));
1679
888
  if (hermes.WIKI_PATH !== void 0) {
1680
889
  if (input.explain) chain.push({ source: "hermes-dotenv", matched: true, value: hermes.WIKI_PATH });
1681
890
  return { path: hermes.WIKI_PATH, source: "hermes-dotenv", ...input.explain ? { chain } : {} };
1682
891
  }
1683
892
  if (input.explain) chain.push({ source: "hermes-dotenv", matched: false });
1684
893
  if (input.cwd) {
1685
- const projCfg = await parseDotenvFile(join8(input.cwd, ".skillwiki", ".env"));
894
+ const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
1686
895
  if (projCfg.WIKI_PATH !== void 0) {
1687
896
  if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
1688
897
  return { path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} };
1689
898
  }
1690
899
  }
1691
900
  if (input.explain) chain.push({ source: "project-dotenv", matched: false });
1692
- const fallback = join8(input.home, "wiki");
901
+ const fallback = join6(input.home, "wiki");
1693
902
  if (input.explain) chain.push({ source: "default", matched: true, value: fallback });
1694
903
  return { path: fallback, source: "default", ...input.explain ? { chain } : {} };
1695
904
  }
@@ -1700,7 +909,7 @@ async function resolveRuntimePath(input) {
1700
909
  return ok({ path: input.flag, source: "flag", ...input.explain ? { chain } : {} });
1701
910
  }
1702
911
  if (input.explain) chain.push({ source: "flag", matched: false });
1703
- const swGlobal = await parseDotenvFile(join8(input.home, ".skillwiki", ".env"));
912
+ const swGlobal = await parseDotenvFile(join6(input.home, ".skillwiki", ".env"));
1704
913
  const wikiName = input.wiki;
1705
914
  if (wikiName !== void 0 && wikiName.length > 0) {
1706
915
  if (wikiName.toLowerCase() === "default") {
@@ -1744,7 +953,7 @@ async function resolveRuntimePath(input) {
1744
953
  }
1745
954
  if (input.explain) chain.push({ source: "env", matched: false });
1746
955
  if (input.cwd) {
1747
- const projCfg = await parseDotenvFile(join8(input.cwd, ".skillwiki", ".env"));
956
+ const projCfg = await parseDotenvFile(join6(input.cwd, ".skillwiki", ".env"));
1748
957
  if (projCfg.WIKI_PATH !== void 0) {
1749
958
  if (input.explain) chain.push({ source: "project-dotenv", matched: true, value: projCfg.WIKI_PATH });
1750
959
  return ok({ path: projCfg.WIKI_PATH, source: "project-dotenv", ...input.explain ? { chain } : {} });
@@ -1864,8 +1073,8 @@ function simulateRemoval(adj, removed) {
1864
1073
  }
1865
1074
 
1866
1075
  // src/commands/audit.ts
1867
- import { readFile as readFile7, stat as stat4 } from "fs/promises";
1868
- import { dirname as dirname5, resolve as resolve3, join as join10 } from "path";
1076
+ import { readFile as readFile4, stat as stat3 } from "fs/promises";
1077
+ import { dirname as dirname2, resolve as resolve2, join as join8 } from "path";
1869
1078
 
1870
1079
  // src/parsers/citations.ts
1871
1080
  var FENCE2 = /```[\s\S]*?```/g;
@@ -1975,9 +1184,9 @@ function hasWikilinkCitations(body) {
1975
1184
  }
1976
1185
 
1977
1186
  // src/utils/raw-source.ts
1978
- import { existsSync as existsSync5 } from "fs";
1979
- import { stat as stat3 } from "fs/promises";
1980
- import { join as join9 } from "path";
1187
+ import { existsSync as existsSync4 } from "fs";
1188
+ import { stat as stat2 } from "fs/promises";
1189
+ import { join as join7 } from "path";
1981
1190
  function normalizeRawSourceTarget(entry) {
1982
1191
  let target = entry.trim().replace(/^"/, "").replace(/"$/, "").replace(/^'/, "").replace(/'$/, "");
1983
1192
  target = target.replace(/^\^\[/, "").replace(/\]$/, "");
@@ -1987,21 +1196,21 @@ function normalizeRawSourceTarget(entry) {
1987
1196
  function rawSourceTargetCandidates(vault, target) {
1988
1197
  const normalized = normalizeRawSourceTarget(target);
1989
1198
  if (!normalized) return [];
1990
- const candidates = [join9(vault, normalized)];
1991
- if (!normalized.endsWith(".md")) candidates.push(join9(vault, `${normalized}.md`));
1199
+ const candidates = [join7(vault, normalized)];
1200
+ if (!normalized.endsWith(".md")) candidates.push(join7(vault, `${normalized}.md`));
1992
1201
  if (normalized.startsWith("raw/")) {
1993
- candidates.push(join9(vault, "_archive", normalized));
1994
- if (!normalized.endsWith(".md")) candidates.push(join9(vault, "_archive", `${normalized}.md`));
1202
+ candidates.push(join7(vault, "_archive", normalized));
1203
+ if (!normalized.endsWith(".md")) candidates.push(join7(vault, "_archive", `${normalized}.md`));
1995
1204
  }
1996
1205
  return [...new Set(candidates)];
1997
1206
  }
1998
1207
  function rawSourceTargetExistsSync(vault, target) {
1999
- return rawSourceTargetCandidates(vault, target).some((candidate) => existsSync5(candidate));
1208
+ return rawSourceTargetCandidates(vault, target).some((candidate) => existsSync4(candidate));
2000
1209
  }
2001
1210
  async function rawSourceTargetExists(vault, target) {
2002
1211
  for (const candidate of rawSourceTargetCandidates(vault, target)) {
2003
1212
  try {
2004
- await stat3(candidate);
1213
+ await stat2(candidate);
2005
1214
  return true;
2006
1215
  } catch {
2007
1216
  }
@@ -2013,7 +1222,7 @@ async function rawSourceTargetExists(vault, target) {
2013
1222
  async function runAudit(input) {
2014
1223
  let text;
2015
1224
  try {
2016
- text = await readFile7(input.file, "utf8");
1225
+ text = await readFile4(input.file, "utf8");
2017
1226
  } catch {
2018
1227
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
2019
1228
  }
@@ -2021,7 +1230,7 @@ async function runAudit(input) {
2021
1230
  if (!fm.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
2022
1231
  const split = splitFrontmatter(text);
2023
1232
  const body = split.ok ? split.data.body : text;
2024
- const vault = await findVaultRoot(dirname5(resolve3(input.file)));
1233
+ const vault = await findVaultRoot(dirname2(resolve2(input.file)));
2025
1234
  if (!vault) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID") };
2026
1235
  const markers = extractCitationMarkers(body);
2027
1236
  const resolved = await Promise.all(markers.map(async (m) => {
@@ -2066,11 +1275,11 @@ async function findVaultRoot(start) {
2066
1275
  let cur = start;
2067
1276
  for (let i = 0; i < 20; i++) {
2068
1277
  try {
2069
- await stat4(join10(cur, "SCHEMA.md"));
1278
+ await stat3(join8(cur, "SCHEMA.md"));
2070
1279
  return cur;
2071
1280
  } catch {
2072
1281
  }
2073
- const parent = dirname5(cur);
1282
+ const parent = dirname2(cur);
2074
1283
  if (parent === cur) return null;
2075
1284
  cur = parent;
2076
1285
  }
@@ -2158,11 +1367,11 @@ ${broken.map((b) => ` ${b.page}:[[${b.slug}]] (line ${b.line})`).join("\n")}` }
2158
1367
  }
2159
1368
 
2160
1369
  // src/commands/tag-audit.ts
2161
- import { readFile as readFile8 } from "fs/promises";
2162
- import { join as join11 } from "path";
1370
+ import { readFile as readFile5 } from "fs/promises";
1371
+ import { join as join9 } from "path";
2163
1372
 
2164
1373
  // src/parsers/taxonomy.ts
2165
- import yaml2 from "js-yaml";
1374
+ import yaml from "js-yaml";
2166
1375
  var TAG_SLUG_RE = /^[a-z0-9][a-z0-9_./-]*$/;
2167
1376
  function taxonomyItemIndent(yamlText) {
2168
1377
  const lines = yamlText.split(/\r?\n/);
@@ -2184,12 +1393,12 @@ function parseTaxonomyDocument(schemaText) {
2184
1393
  const nextHeading = /^#{1,2}[ \t]+/m.exec(unboundedTail);
2185
1394
  const sectionEnd = nextHeading?.index === void 0 ? schemaText.length : afterHeading + nextHeading.index;
2186
1395
  const sectionText = schemaText.slice(afterHeading, sectionEnd);
2187
- const open2 = /^```yaml[ \t]*\r?$/m.exec(sectionText);
2188
- if (!open2 || open2.index === void 0) {
1396
+ const open = /^```yaml[ \t]*\r?$/m.exec(sectionText);
1397
+ if (!open || open.index === void 0) {
2189
1398
  return err("NO_TAXONOMY_BLOCK", { message: "Fenced YAML taxonomy block not found" });
2190
1399
  }
2191
- const openStart = afterHeading + open2.index;
2192
- const yamlStart = openStart + open2[0].length + 1;
1400
+ const openStart = afterHeading + open.index;
1401
+ const yamlStart = openStart + open[0].length + 1;
2193
1402
  const afterOpen = schemaText.slice(yamlStart, sectionEnd);
2194
1403
  const close = /^```[ \t]*\r?$/m.exec(afterOpen);
2195
1404
  if (!close || close.index === void 0) {
@@ -2201,7 +1410,7 @@ function parseTaxonomyDocument(schemaText) {
2201
1410
  const yamlText = schemaText.slice(yamlStart, yamlEnd);
2202
1411
  let parsed;
2203
1412
  try {
2204
- parsed = yaml2.load(yamlText, { schema: yaml2.JSON_SCHEMA });
1413
+ parsed = yaml.load(yamlText, { schema: yaml.JSON_SCHEMA });
2205
1414
  } catch (error) {
2206
1415
  return err("INVALID_FRONTMATTER", { message: getErrorMessage(error) });
2207
1416
  }
@@ -2221,8 +1430,8 @@ function extractTaxonomy(schemaText) {
2221
1430
  return ok(parsed.data.tags);
2222
1431
  }
2223
1432
  function renderTag(tag) {
2224
- const roundTrip = yaml2.load(`value: ${tag}
2225
- `, { schema: yaml2.JSON_SCHEMA });
1433
+ const roundTrip = yaml.load(`value: ${tag}
1434
+ `, { schema: yaml.JSON_SCHEMA });
2226
1435
  return typeof roundTrip.value === "string" && roundTrip.value === tag ? tag : JSON.stringify(tag);
2227
1436
  }
2228
1437
  function taxonomyCommentForPage(page, date, reason) {
@@ -2276,13 +1485,43 @@ function reconcileTaxonomyDocument(schemaText, input) {
2276
1485
  changed: true
2277
1486
  });
2278
1487
  }
1488
+ function taxonomyEnvelope(text, doc) {
1489
+ return text.slice(0, doc.yamlStart) + "<taxonomy-yaml>" + text.slice(doc.closingFenceStart);
1490
+ }
1491
+ function mergeTaxonomyConflict(baseText, oursText, theirsText) {
1492
+ const base = parseTaxonomyDocument(baseText);
1493
+ const ours = parseTaxonomyDocument(oursText);
1494
+ const theirs = parseTaxonomyDocument(theirsText);
1495
+ if (!base.ok) return base;
1496
+ if (!ours.ok) return ours;
1497
+ if (!theirs.ok) return theirs;
1498
+ const envelope = taxonomyEnvelope(baseText, base.data);
1499
+ if (taxonomyEnvelope(oursText, ours.data) !== envelope || taxonomyEnvelope(theirsText, theirs.data) !== envelope) {
1500
+ return err("SCHEME_REJECTED", { reason: "non-taxonomy-change" });
1501
+ }
1502
+ const baseTags = new Set(base.data.tags);
1503
+ const addedFromOurs = ours.data.tags.filter((tag) => !baseTags.has(tag)).sort();
1504
+ const addedFromTheirs = theirs.data.tags.filter((tag) => !baseTags.has(tag)).sort();
1505
+ const tags = [...base.data.tags, .../* @__PURE__ */ new Set([...addedFromOurs, ...addedFromTheirs])];
1506
+ const rendered = reconcileTaxonomyDocument(baseText, {
1507
+ tags,
1508
+ comment: "# -- reconciled: taxonomy-only three-stage merge --"
1509
+ });
1510
+ if (!rendered.ok) return rendered;
1511
+ return ok({
1512
+ text: rendered.data.text,
1513
+ tags,
1514
+ added_from_ours: addedFromOurs,
1515
+ added_from_theirs: addedFromTheirs
1516
+ });
1517
+ }
2279
1518
 
2280
1519
  // src/commands/tag-audit.ts
2281
1520
  async function runTagAudit(input) {
2282
1521
  const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
2283
1522
  if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
2284
1523
  const scan = scanResult.data;
2285
- const schemaText = await readFile8(join11(input.vault, "SCHEMA.md"), "utf8");
1524
+ const schemaText = await readFile5(join9(input.vault, "SCHEMA.md"), "utf8");
2286
1525
  const tax = extractTaxonomy(schemaText);
2287
1526
  if (!tax.ok) return { exitCode: ExitCode.INVALID_FRONTMATTER, result: tax };
2288
1527
  const allowed = new Set(tax.data);
@@ -2299,69 +1538,328 @@ async function runTagAudit(input) {
2299
1538
  pageViolations.push({ page: p.relPath, tag: t });
2300
1539
  }
2301
1540
  }
2302
- return pageViolations;
1541
+ return pageViolations;
1542
+ });
1543
+ for (const result of perPage) {
1544
+ if (!Array.isArray(result)) {
1545
+ return { exitCode: ExitCode.INVALID_FRONTMATTER, result };
1546
+ }
1547
+ violations.push(...result);
1548
+ }
1549
+ if (violations.length > 0) {
1550
+ 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") }) };
1551
+ }
1552
+ return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
1553
+ }
1554
+
1555
+ // src/commands/index-check.ts
1556
+ import { readFile as readFile6 } from "fs/promises";
1557
+ import { join as join10 } from "path";
1558
+ function normalizeIndexTarget(raw) {
1559
+ return raw.replace(/\.md$/, "").replace(/^\.?\//, "");
1560
+ }
1561
+ async function runIndexCheck(input) {
1562
+ const scan = input.scan ? ok(input.scan) : await scanVault(input.vault);
1563
+ if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
1564
+ let indexText = "";
1565
+ try {
1566
+ indexText = await readFile6(join10(input.vault, "index.md"), "utf8");
1567
+ } catch {
1568
+ }
1569
+ const indexTargets = /* @__PURE__ */ new Set();
1570
+ const indexBare = /* @__PURE__ */ new Map();
1571
+ for (const s of extractBodyWikilinks(indexText)) {
1572
+ const target = normalizeIndexTarget(s);
1573
+ indexTargets.add(target);
1574
+ const bare = target.split("/").pop().toLowerCase();
1575
+ const list = indexBare.get(bare) ?? [];
1576
+ list.push(target);
1577
+ indexBare.set(bare, list);
1578
+ }
1579
+ const required = /* @__PURE__ */ new Map();
1580
+ const known = /* @__PURE__ */ new Set();
1581
+ for (const p of scan.data.typedKnowledge) {
1582
+ const target = p.relPath.replace(/\.md$/, "");
1583
+ required.set(target, p.relPath);
1584
+ known.add(target);
1585
+ }
1586
+ for (const p of scan.data.compound) {
1587
+ known.add(p.relPath.replace(/\.md$/, ""));
1588
+ }
1589
+ const missing_from_index = [];
1590
+ for (const [target, relPath] of required.entries()) {
1591
+ if (indexTargets.has(target)) continue;
1592
+ const bare = target.split("/").pop().toLowerCase();
1593
+ const bareHits = indexBare.get(bare) ?? [];
1594
+ const basenameOnly = bareHits.filter((t) => !t.includes("/"));
1595
+ const sameNameRequired = [...required.keys()].filter(
1596
+ (t) => t.split("/").pop().toLowerCase() === bare
1597
+ );
1598
+ if (basenameOnly.length === 1 && sameNameRequired.length === 1) continue;
1599
+ missing_from_index.push(relPath);
1600
+ }
1601
+ const ghost_entries = [];
1602
+ for (const target of indexTargets) {
1603
+ if (known.has(target)) continue;
1604
+ if (!target.includes("/")) {
1605
+ const bare = target.toLowerCase();
1606
+ const matches = [...known].filter((k) => k.split("/").pop().toLowerCase() === bare);
1607
+ if (matches.length === 0) ghost_entries.push(target);
1608
+ continue;
1609
+ }
1610
+ ghost_entries.push(target);
1611
+ }
1612
+ const hintLines = [];
1613
+ if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
1614
+ if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
1615
+ if (hintLines.length === 0) hintLines.push("index OK");
1616
+ if (missing_from_index.length > 0 || ghost_entries.length > 0) {
1617
+ return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1618
+ }
1619
+ return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1620
+ }
1621
+
1622
+ // src/commands/project-index.ts
1623
+ import { readdir, readFile as readFile7, mkdir as mkdir2 } from "fs/promises";
1624
+ import { join as join11, dirname as dirname3, basename } from "path";
1625
+ var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
1626
+ var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
1627
+ async function scanMarkdownTree(rootAbs, rootRel) {
1628
+ const found = [];
1629
+ let entries;
1630
+ try {
1631
+ entries = await readdir(rootAbs, { withFileTypes: true });
1632
+ } catch {
1633
+ return found;
1634
+ }
1635
+ for (const entry of entries) {
1636
+ const abs = join11(rootAbs, entry.name);
1637
+ const rel = `${rootRel}/${entry.name}`;
1638
+ if (entry.isDirectory()) {
1639
+ found.push(...await scanMarkdownTree(abs, rel));
1640
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
1641
+ found.push(rel);
1642
+ }
1643
+ }
1644
+ return found;
1645
+ }
1646
+ function projectLocalType(slug, page, data) {
1647
+ if (page.startsWith(`projects/${slug}/requirements/`)) return "requirement";
1648
+ if (page.startsWith(`projects/${slug}/work/`)) {
1649
+ if (typeof data.kind === "string") return data.kind;
1650
+ const name = basename(page, ".md");
1651
+ if (name === "spec" || name === "plan" || name === "retro") return name;
1652
+ return "work";
1653
+ }
1654
+ if (page.startsWith(`projects/${slug}/architecture/`)) {
1655
+ return typeof data.type === "string" ? data.type : "architecture";
1656
+ }
1657
+ if (page.startsWith(`projects/${slug}/history/`)) {
1658
+ if (typeof data.kind === "string") return data.kind;
1659
+ if (typeof data.type === "string") return data.type;
1660
+ return "history";
1661
+ }
1662
+ return typeof data.type === "string" ? data.type : "project";
1663
+ }
1664
+ async function renderProjectIndex(vault, slug, opts = {}) {
1665
+ const projectDir = join11(vault, "projects", slug);
1666
+ try {
1667
+ await readdir(projectDir);
1668
+ } catch {
1669
+ return err("PROJECT_NOT_FOUND", { slug, path: projectDir });
1670
+ }
1671
+ const wikilinkPattern = `[[${slug}]]`;
1672
+ const entries = [];
1673
+ const compoundDir = join11(vault, "projects", slug, "compound");
1674
+ try {
1675
+ const compoundFiles = await readdir(compoundDir, { withFileTypes: true });
1676
+ for (const entry of compoundFiles) {
1677
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
1678
+ const filePath = join11(compoundDir, entry.name);
1679
+ let text;
1680
+ try {
1681
+ text = await readFile7(filePath, "utf8");
1682
+ } catch {
1683
+ continue;
1684
+ }
1685
+ const fm = extractFrontmatter(text);
1686
+ if (!fm.ok) continue;
1687
+ entries.push({
1688
+ page: `projects/${slug}/compound/${entry.name}`,
1689
+ type: typeof fm.data.type === "string" ? fm.data.type : "compound",
1690
+ title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
1691
+ });
1692
+ }
1693
+ } catch {
1694
+ }
1695
+ for (const dir of LAYER2_DIRS) {
1696
+ let files;
1697
+ try {
1698
+ files = await readdir(join11(vault, dir), { withFileTypes: true });
1699
+ } catch {
1700
+ continue;
1701
+ }
1702
+ for (const entry of files) {
1703
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
1704
+ const filePath = join11(vault, dir, entry.name);
1705
+ let text;
1706
+ try {
1707
+ text = await readFile7(filePath, "utf8");
1708
+ } catch {
1709
+ continue;
1710
+ }
1711
+ const fm = extractFrontmatter(text);
1712
+ if (!fm.ok) continue;
1713
+ const pp = fm.data.provenance_projects;
1714
+ if (!Array.isArray(pp) || !pp.some((p) => String(p) === wikilinkPattern)) continue;
1715
+ entries.push({
1716
+ page: `${dir}/${entry.name}`,
1717
+ type: typeof fm.data.type === "string" ? fm.data.type : dir.slice(0, -1),
1718
+ title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
1719
+ });
1720
+ }
1721
+ }
1722
+ for (const dir of PROJECT_LOCAL_DIRS) {
1723
+ const rootAbs = join11(projectDir, dir);
1724
+ const rootRel = `projects/${slug}/${dir}`;
1725
+ const pages = await scanMarkdownTree(rootAbs, rootRel);
1726
+ for (const page of pages) {
1727
+ const filePath = join11(vault, page);
1728
+ let text;
1729
+ try {
1730
+ text = await readFile7(filePath, "utf8");
1731
+ } catch {
1732
+ continue;
1733
+ }
1734
+ const fm = extractFrontmatter(text);
1735
+ if (!fm.ok) continue;
1736
+ entries.push({
1737
+ page,
1738
+ type: projectLocalType(slug, page, fm.data),
1739
+ title: typeof fm.data.title === "string" ? fm.data.title : basename(page, ".md")
1740
+ });
1741
+ }
1742
+ }
1743
+ const typeOrder = {
1744
+ entity: 0,
1745
+ concept: 1,
1746
+ comparison: 2,
1747
+ query: 3,
1748
+ summary: 4,
1749
+ meta: 5,
1750
+ requirement: 6,
1751
+ spec: 7,
1752
+ plan: 8,
1753
+ retro: 9,
1754
+ architecture: 10,
1755
+ pattern: 11,
1756
+ gotcha: 12,
1757
+ lesson: 13,
1758
+ antipattern: 14,
1759
+ compound: 15,
1760
+ work: 16,
1761
+ history: 17
1762
+ };
1763
+ entries.sort((a, b) => {
1764
+ const ta = typeOrder[a.type] ?? 99;
1765
+ const tb = typeOrder[b.type] ?? 99;
1766
+ return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
2303
1767
  });
2304
- for (const result of perPage) {
2305
- if (!Array.isArray(result)) {
2306
- return { exitCode: ExitCode.INVALID_FRONTMATTER, result };
1768
+ const today = opts.today ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1769
+ const grouped = /* @__PURE__ */ new Map();
1770
+ for (const e of entries) {
1771
+ const group = e.type;
1772
+ if (!grouped.has(group)) grouped.set(group, []);
1773
+ grouped.get(group).push(e);
1774
+ }
1775
+ let body = `# Knowledge Index: ${slug}
1776
+
1777
+ Autogenerated by \`skillwiki project-index\` on ${today}.
1778
+
1779
+ `;
1780
+ for (const [type, items] of grouped) {
1781
+ body += `## ${type}
1782
+
1783
+ `;
1784
+ for (const item of items) {
1785
+ const pageRef = item.page.replace(/\.md$/, "");
1786
+ body += `- [[${pageRef}]] \u2014 ${item.title}
1787
+ `;
2307
1788
  }
2308
- violations.push(...result);
1789
+ body += "\n";
2309
1790
  }
2310
- if (violations.length > 0) {
2311
- 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") }) };
1791
+ if (entries.length === 0) {
1792
+ body += `No Layer 2 pages reference \`[[${slug}]]\` in provenance_projects.
1793
+ `;
2312
1794
  }
2313
- return { exitCode: ExitCode.OK, result: ok({ violations, taxonomy: tax.data, humanHint: "all tags valid" }) };
1795
+ return ok({
1796
+ text: body,
1797
+ entries,
1798
+ index_path: `projects/${slug}/knowledge.md`
1799
+ });
2314
1800
  }
2315
-
2316
- // src/commands/index-check.ts
2317
- import { readFile as readFile9 } from "fs/promises";
2318
- import { join as join12 } from "path";
2319
- async function runIndexCheck(input) {
2320
- const scan = input.scan ? ok(input.scan) : await scanVault(input.vault);
2321
- if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
2322
- let indexText = "";
1801
+ async function runProjectIndex(input) {
1802
+ const slug = input.slug;
1803
+ const projectDir = join11(input.vault, "projects", slug);
1804
+ const rendered = await renderProjectIndex(input.vault, slug);
1805
+ if (!rendered.ok) {
1806
+ return {
1807
+ exitCode: rendered.error === "PROJECT_NOT_FOUND" ? ExitCode.PROJECT_NOT_FOUND : ExitCode.WRITE_FAILED,
1808
+ result: rendered
1809
+ };
1810
+ }
1811
+ const indexPath = join11(projectDir, "knowledge.md");
1812
+ const entries = rendered.data.entries;
1813
+ let existing = false;
1814
+ let stale = false;
2323
1815
  try {
2324
- indexText = await readFile9(join12(input.vault, "index.md"), "utf8");
1816
+ const existingText = await readFile7(indexPath, "utf8");
1817
+ existing = true;
1818
+ const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
1819
+ const existingPages = new Set(existingEntries.map((l) => {
1820
+ const m = l.match(/\[\[([^\]]+)\]\]/);
1821
+ return m ? m[1] : "";
1822
+ }));
1823
+ const currentPages = new Set(entries.map((e) => e.page.replace(/\.md$/, "")));
1824
+ stale = existingPages.size !== currentPages.size || [...currentPages].some((p) => !existingPages.has(p));
2325
1825
  } catch {
2326
1826
  }
2327
- const indexSlugsLower = /* @__PURE__ */ new Map();
2328
- for (const s of extractBodyWikilinks(indexText)) {
2329
- const tail = s.split("/").pop();
2330
- indexSlugsLower.set(tail.toLowerCase(), tail);
2331
- }
2332
- const fileSlugs = /* @__PURE__ */ new Map();
2333
- const requiredSlugs = /* @__PURE__ */ new Map();
2334
- for (const p of scan.data.typedKnowledge) {
2335
- const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
2336
- fileSlugs.set(slug, p.relPath);
2337
- requiredSlugs.set(slug, p.relPath);
2338
- }
2339
- for (const p of scan.data.compound) {
2340
- const slug = p.relPath.replace(/\.md$/, "").split("/").pop();
2341
- fileSlugs.set(slug, p.relPath);
2342
- }
2343
- const missing_from_index = [];
2344
- for (const [slug, relPath] of requiredSlugs.entries()) {
2345
- if (!indexSlugsLower.has(slug.toLowerCase())) missing_from_index.push(relPath);
2346
- }
2347
- const fileSlugsLower = new Set([...fileSlugs.keys()].map((s) => s.toLowerCase()));
2348
- const ghost_entries = [];
2349
- for (const [lower, orig] of indexSlugsLower) {
2350
- if (!fileSlugsLower.has(lower)) ghost_entries.push(orig);
2351
- }
2352
- const hintLines = [];
2353
- if (missing_from_index.length > 0) hintLines.push(`missing from index: ${missing_from_index.length}`, ...missing_from_index.map((p) => ` ${p}`));
2354
- if (ghost_entries.length > 0) hintLines.push(`ghost entries: ${ghost_entries.length}`, ...ghost_entries.map((g) => ` ${g}`));
2355
- if (hintLines.length === 0) hintLines.push("index OK");
2356
- if (missing_from_index.length > 0 || ghost_entries.length > 0) {
2357
- return { exitCode: ExitCode.INDEX_INCOMPLETE, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1827
+ if (input.apply) {
1828
+ try {
1829
+ await mkdir2(dirname3(indexPath), { recursive: true });
1830
+ } catch (e) {
1831
+ return {
1832
+ exitCode: ExitCode.WRITE_FAILED,
1833
+ result: err("WRITE_FAILED", { file: indexPath, message: String(e) })
1834
+ };
1835
+ }
1836
+ const written = await atomicWriteText(indexPath, rendered.data.text);
1837
+ if (!written.ok) {
1838
+ return { exitCode: ExitCode.WRITE_FAILED, result: written };
1839
+ }
2358
1840
  }
2359
- return { exitCode: ExitCode.OK, result: ok({ missing_from_index, ghost_entries, humanHint: hintLines.join("\n") }) };
1841
+ const action = input.apply ? `written ${entries.length} entries to ${indexPath}` : `${entries.length} entries found (use --apply to write)`;
1842
+ const staleHint = stale ? " (STALE \u2014 existing index outdated)" : existing ? " (up to date)" : "";
1843
+ return {
1844
+ exitCode: ExitCode.OK,
1845
+ result: ok({
1846
+ slug,
1847
+ entries,
1848
+ existing,
1849
+ stale,
1850
+ index_path: rendered.data.index_path,
1851
+ humanHint: `project: ${slug}
1852
+ entries: ${entries.length}${staleHint}
1853
+ ${action}
1854
+
1855
+ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e.title}`).join("\n")}`
1856
+ })
1857
+ };
2360
1858
  }
2361
1859
 
2362
1860
  // src/commands/stale.ts
2363
- import { readdir as readdir2, rename as rename2, mkdir as mkdir3, readFile as readFile10 } from "fs/promises";
2364
- import { join as join13 } from "path";
1861
+ import { readdir as readdir2, rename, mkdir as mkdir3, readFile as readFile8 } from "fs/promises";
1862
+ import { join as join12 } from "path";
2365
1863
 
2366
1864
  // src/parsers/expiry-annotations.ts
2367
1865
  var HEADING_RE = /^#{1,6}\s+(.+)$/;
@@ -2398,8 +1896,8 @@ function parseExpiryAnnotations(content, pagePath) {
2398
1896
  }
2399
1897
 
2400
1898
  // src/commands/stale.ts
2401
- function daysSince(isoDate2) {
2402
- return Math.floor((Date.now() - Date.parse(isoDate2)) / 864e5);
1899
+ function daysSince(isoDate) {
1900
+ return Math.floor((Date.now() - Date.parse(isoDate)) / 864e5);
2403
1901
  }
2404
1902
  async function runStale(input) {
2405
1903
  const scanResult = input.scan ? ok(input.scan) : await scanVault(input.vault);
@@ -2410,7 +1908,7 @@ async function runStale(input) {
2410
1908
  const archived = [];
2411
1909
  const workDirs = /* @__PURE__ */ new Map();
2412
1910
  const workDirsBySlug = /* @__PURE__ */ new Map();
2413
- const projectsDir = join13(input.vault, "projects");
1911
+ const projectsDir = join12(input.vault, "projects");
2414
1912
  let projectSlugs = [];
2415
1913
  try {
2416
1914
  projectSlugs = (await readdir2(projectsDir, { withFileTypes: true })).filter((d) => d.isDirectory()).map((d) => d.name);
@@ -2423,7 +1921,7 @@ async function runStale(input) {
2423
1921
  projectSlugs = [input.project];
2424
1922
  }
2425
1923
  for (const slug of projectSlugs) {
2426
- const workPath = join13(projectsDir, slug, "work");
1924
+ const workPath = join12(projectsDir, slug, "work");
2427
1925
  let entries;
2428
1926
  try {
2429
1927
  entries = await readdir2(workPath, { withFileTypes: true });
@@ -2434,7 +1932,7 @@ async function runStale(input) {
2434
1932
  for (const e of entries) {
2435
1933
  if (!e.isDirectory()) continue;
2436
1934
  const relDir = `projects/${slug}/work/${e.name}`;
2437
- const absDir = join13(workPath, e.name);
1935
+ const absDir = join12(workPath, e.name);
2438
1936
  let status = "";
2439
1937
  let files;
2440
1938
  try {
@@ -2447,7 +1945,7 @@ async function runStale(input) {
2447
1945
  for (const f of files) {
2448
1946
  if (!f.endsWith(".md")) continue;
2449
1947
  try {
2450
- const fm = extractFrontmatter(await readFile10(join13(absDir, f), "utf8"));
1948
+ const fm = extractFrontmatter(await readFile8(join12(absDir, f), "utf8"));
2451
1949
  if (fm.ok && typeof fm.data.status === "string") {
2452
1950
  status = fm.data.status;
2453
1951
  break;
@@ -2478,9 +1976,9 @@ async function runStale(input) {
2478
1976
  if (input.project && !project.includes(input.project)) return null;
2479
1977
  let inferred = false;
2480
1978
  if (input.forceScan && !kind) {
2481
- const basename4 = t.relPath.split("/").pop();
2482
- if (!LOOP_CYCLE_PATTERN.test(basename4)) {
2483
- const m = basename4.match(KIND_FROM_FILENAME);
1979
+ const basename3 = t.relPath.split("/").pop();
1980
+ if (!LOOP_CYCLE_PATTERN.test(basename3)) {
1981
+ const m = basename3.match(KIND_FROM_FILENAME);
2484
1982
  if (m) {
2485
1983
  kind = m[1];
2486
1984
  inferred = true;
@@ -2491,9 +1989,9 @@ async function runStale(input) {
2491
1989
  const bodyStart = content.indexOf("---", 4);
2492
1990
  if (bodyStart > 0) {
2493
1991
  const body = content.slice(bodyStart);
2494
- const wikilink2 = body.match(/\[\[([a-z0-9-]+)\]\]/);
2495
- if (wikilink2) {
2496
- const candidate = wikilink2[1];
1992
+ const wikilink = body.match(/\[\[([a-z0-9-]+)\]\]/);
1993
+ if (wikilink) {
1994
+ const candidate = wikilink[1];
2497
1995
  if (workDirsBySlug.has(candidate)) {
2498
1996
  project = `[[${candidate}]]`;
2499
1997
  inferred = true;
@@ -2543,9 +2041,9 @@ async function runStale(input) {
2543
2041
  }
2544
2042
  }
2545
2043
  await mapWithConcurrency([...workDirs.keys()], vaultIoConcurrency(), async (relDir) => {
2546
- const specPath = join13(input.vault, relDir, "spec.md");
2044
+ const specPath = join12(input.vault, relDir, "spec.md");
2547
2045
  try {
2548
- const specContent = await readFile10(specPath, "utf8");
2046
+ const specContent = await readFile8(specPath, "utf8");
2549
2047
  const specFm = extractFrontmatter(specContent);
2550
2048
  if (specFm.ok && typeof specFm.data.source === "string") {
2551
2049
  const sourcePath = specFm.data.source;
@@ -2574,7 +2072,7 @@ async function runStale(input) {
2574
2072
  if (daysSince(dateStr) < input.days) continue;
2575
2073
  let files;
2576
2074
  try {
2577
- files = await readdir2(join13(input.vault, relDir));
2075
+ files = await readdir2(join12(input.vault, relDir));
2578
2076
  } catch {
2579
2077
  continue;
2580
2078
  }
@@ -2644,7 +2142,7 @@ async function runStale(input) {
2644
2142
  staleSections.push(...staleSectionResults.flat());
2645
2143
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2646
2144
  if (input.archive) {
2647
- const archiveDir = join13(input.vault, "_archive", today);
2145
+ const archiveDir = join12(input.vault, "_archive", today);
2648
2146
  await mkdir3(archiveDir, { recursive: true });
2649
2147
  const citedRawPaths = /* @__PURE__ */ new Set();
2650
2148
  for (const page of scan.typedKnowledge) {
@@ -2660,9 +2158,9 @@ async function runStale(input) {
2660
2158
  }
2661
2159
  for (const t of staleTranscripts) {
2662
2160
  if (citedRawPaths.has(t.path) || citedRawPaths.has(t.path.replace(/\.md$/, ""))) continue;
2663
- const dest = join13(archiveDir, t.path.split("/").pop());
2161
+ const dest = join12(archiveDir, t.path.split("/").pop());
2664
2162
  try {
2665
- await rename2(join13(input.vault, t.path), dest);
2163
+ await rename(join12(input.vault, t.path), dest);
2666
2164
  archived.push(t.path);
2667
2165
  } catch {
2668
2166
  }
@@ -2672,18 +2170,18 @@ async function runStale(input) {
2672
2170
  if (parts.length >= 4 && parts[0] === "projects") {
2673
2171
  const slug = parts[1];
2674
2172
  const itemName = parts[3];
2675
- const histDir = join13(input.vault, "projects", slug, "history", "archived-work");
2173
+ const histDir = join12(input.vault, "projects", slug, "history", "archived-work");
2676
2174
  await mkdir3(histDir, { recursive: true });
2677
- const dest = join13(histDir, itemName);
2175
+ const dest = join12(histDir, itemName);
2678
2176
  try {
2679
- await rename2(join13(input.vault, w.path), dest);
2177
+ await rename(join12(input.vault, w.path), dest);
2680
2178
  archived.push(w.path);
2681
2179
  } catch {
2682
2180
  }
2683
2181
  } else {
2684
- const dest = join13(archiveDir, w.path.replace(/\//g, "_"));
2182
+ const dest = join12(archiveDir, w.path.replace(/\//g, "_"));
2685
2183
  try {
2686
- await rename2(join13(input.vault, w.path), dest);
2184
+ await rename(join12(input.vault, w.path), dest);
2687
2185
  archived.push(w.path);
2688
2186
  } catch {
2689
2187
  }
@@ -2738,19 +2236,19 @@ async function runPagesize(input) {
2738
2236
  }
2739
2237
 
2740
2238
  // src/commands/log-rotate.ts
2741
- import { readFile as readFile11, rename as rename3, writeFile as writeFile3, stat as stat5 } from "fs/promises";
2742
- import { join as join14 } from "path";
2239
+ import { readFile as readFile9, rename as rename2, writeFile as writeFile2, stat as stat4 } from "fs/promises";
2240
+ import { join as join13 } from "path";
2743
2241
  var ENTRY_RE2 = /^## \[(\d{4})-\d{2}-\d{2}\]/gm;
2744
2242
  async function runLogRotate(input) {
2745
2243
  try {
2746
- await stat5(join14(input.vault, "SCHEMA.md"));
2244
+ await stat4(join13(input.vault, "SCHEMA.md"));
2747
2245
  } catch {
2748
2246
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
2749
2247
  }
2750
- const logPath = join14(input.vault, "log.md");
2248
+ const logPath = join13(input.vault, "log.md");
2751
2249
  let logText;
2752
2250
  try {
2753
- logText = await readFile11(logPath, "utf8");
2251
+ logText = await readFile9(logPath, "utf8");
2754
2252
  } catch {
2755
2253
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
2756
2254
  }
@@ -2767,9 +2265,9 @@ async function runLogRotate(input) {
2767
2265
  }
2768
2266
  const newestYear = matches[matches.length - 1][1];
2769
2267
  const rotatedName = `log-${newestYear}.md`;
2770
- const rotatedPath = join14(input.vault, rotatedName);
2268
+ const rotatedPath = join13(input.vault, rotatedName);
2771
2269
  try {
2772
- await rename3(logPath, rotatedPath);
2270
+ await rename2(logPath, rotatedPath);
2773
2271
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2774
2272
  const fresh = `# Vault Log
2775
2273
 
@@ -2779,7 +2277,7 @@ Chronological action log. Newest entries last. Skill writes append entries; lint
2779
2277
 
2780
2278
  - Previous log moved to ${rotatedName}
2781
2279
  `;
2782
- await writeFile3(logPath, fresh, "utf8");
2280
+ await writeFile2(logPath, fresh, "utf8");
2783
2281
  } catch (e) {
2784
2282
  return { exitCode: ExitCode.WRITE_FAILED, result: err("WRITE_FAILED", { message: String(e) }) };
2785
2283
  }
@@ -2812,13 +2310,13 @@ async function runTopicMapCheck(input) {
2812
2310
  }
2813
2311
 
2814
2312
  // src/commands/index-link-format.ts
2815
- import { readFile as readFile12 } from "fs/promises";
2816
- import { join as join15 } from "path";
2313
+ import { readFile as readFile10 } from "fs/promises";
2314
+ import { join as join14 } from "path";
2817
2315
  var MD_LINK_RE = /\[[^\[\]]+\]\([^)]+\.md\)/;
2818
2316
  async function runIndexLinkFormat(input) {
2819
2317
  let text = "";
2820
2318
  try {
2821
- text = await readFile12(join15(input.vault, "index.md"), "utf8");
2319
+ text = await readFile10(join14(input.vault, "index.md"), "utf8");
2822
2320
  } catch {
2823
2321
  }
2824
2322
  const markdown_links = [];
@@ -2831,9 +2329,9 @@ ${markdown_links.map((l) => ` line ${l.line}: ${l.text}`).join("\n")}`;
2831
2329
  }
2832
2330
 
2833
2331
  // src/commands/dedup.ts
2834
- import { createHash as createHash4 } from "crypto";
2835
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
2836
- import { dirname as dirname6, join as join16, resolve as resolve4 } from "path";
2332
+ import { createHash as createHash3 } from "crypto";
2333
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
2334
+ import { dirname as dirname4, join as join15, resolve as resolve3 } from "path";
2837
2335
 
2838
2336
  // src/utils/rclone.ts
2839
2337
  import { execFile } from "child_process";
@@ -2925,7 +2423,7 @@ async function runDedup(input) {
2925
2423
  const manifest = safeEntries.length > 0 ? {
2926
2424
  version: 1,
2927
2425
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
2928
- vault: resolve4(input.vault),
2426
+ vault: resolve3(input.vault),
2929
2427
  entries: safeEntries
2930
2428
  } : void 0;
2931
2429
  const remote = await planAndMaybePruneRemote(input, safeEntries);
@@ -2937,7 +2435,7 @@ async function runDedup(input) {
2937
2435
  }
2938
2436
  if (input.manifestOut && manifest) {
2939
2437
  try {
2940
- mkdirSync4(dirname6(input.manifestOut), { recursive: true });
2438
+ mkdirSync4(dirname4(input.manifestOut), { recursive: true });
2941
2439
  writeFileSync4(input.manifestOut, `${JSON.stringify(manifest, null, 2)}
2942
2440
  `, "utf-8");
2943
2441
  } catch (e) {
@@ -2952,7 +2450,7 @@ async function runDedup(input) {
2952
2450
  }
2953
2451
  }
2954
2452
  for (const page of scan.allMarkdown.filter((p) => !p.relPath.startsWith("raw/"))) {
2955
- const text = readFileSync5(join16(input.vault, page.relPath), "utf-8");
2453
+ const text = readFileSync4(join15(input.vault, page.relPath), "utf-8");
2956
2454
  let updated = text;
2957
2455
  let changed = false;
2958
2456
  for (const [oldPath, newPath] of replacements) {
@@ -2970,12 +2468,12 @@ async function runDedup(input) {
2970
2468
  }
2971
2469
  }
2972
2470
  if (changed) {
2973
- writeFileSync4(join16(input.vault, page.relPath), updated);
2471
+ writeFileSync4(join15(input.vault, page.relPath), updated);
2974
2472
  rewired.push(page.relPath);
2975
2473
  }
2976
2474
  }
2977
2475
  for (const oldPath of replacements.keys()) {
2978
- const fullPath = join16(input.vault, oldPath);
2476
+ const fullPath = join15(input.vault, oldPath);
2979
2477
  try {
2980
2478
  unlinkSync4(fullPath);
2981
2479
  removed.push(oldPath);
@@ -3069,14 +2567,14 @@ function buildSafeEntries(vault, duplicates, unsafe) {
3069
2567
  return entries;
3070
2568
  }
3071
2569
  function hashRawBody(vault, relPath) {
3072
- const text = readFileSync5(join16(vault, relPath), "utf-8");
2570
+ const text = readFileSync4(join15(vault, relPath), "utf-8");
3073
2571
  const split = splitFrontmatter(text);
3074
2572
  const body = split.ok ? split.data.body : text;
3075
- return createHash4("sha256").update(body).digest("hex");
2573
+ return createHash3("sha256").update(body).digest("hex");
3076
2574
  }
3077
2575
  function readManifest(path) {
3078
2576
  try {
3079
- const parsed = JSON.parse(readFileSync5(path, "utf-8"));
2577
+ const parsed = JSON.parse(readFileSync4(path, "utf-8"));
3080
2578
  if (parsed.version !== 1 || !Array.isArray(parsed.entries)) {
3081
2579
  return err("INVALID_FRONTMATTER", { message: "dedup manifest must have version 1 and entries[]" });
3082
2580
  }
@@ -3092,7 +2590,7 @@ async function planAndMaybePruneRemote(input, entries) {
3092
2590
  }
3093
2591
 
3094
2592
  // src/utils/safe-write.ts
3095
- import { readFile as readFile13, writeFile as writeFile4 } from "fs/promises";
2593
+ import { readFile as readFile11, writeFile as writeFile3 } from "fs/promises";
3096
2594
  var DEFAULT_MIN_BODY_RATIO = 0.5;
3097
2595
  var DEFAULT_MIN_OLD_BODY_BYTES = 200;
3098
2596
  function bodyBytes(text) {
@@ -3102,7 +2600,7 @@ function bodyBytes(text) {
3102
2600
  }
3103
2601
  async function readIfExists(absPath) {
3104
2602
  try {
3105
- return await readFile13(absPath, "utf8");
2603
+ return await readFile11(absPath, "utf8");
3106
2604
  } catch (e) {
3107
2605
  if (e.code === "ENOENT") return null;
3108
2606
  throw e;
@@ -3225,10 +2723,10 @@ ${newBody}`;
3225
2723
  }
3226
2724
 
3227
2725
  // src/commands/lint.ts
3228
- import { existsSync as existsSync7 } from "fs";
3229
- import { readFile as readFile15, readdir as readdir3 } from "fs/promises";
3230
- import { createHash as createHash6 } from "crypto";
3231
- import { join as join18, relative as relative4, sep as sep4 } from "path";
2726
+ import { existsSync as existsSync6 } from "fs";
2727
+ import { readFile as readFile13, readdir as readdir3 } from "fs/promises";
2728
+ import { createHash as createHash5 } from "crypto";
2729
+ import { join as join17, relative as relative2, sep as sep2 } from "path";
3232
2730
 
3233
2731
  // src/commands/sparse-community.ts
3234
2732
  async function runSparseCommunity(input) {
@@ -3244,7 +2742,7 @@ async function runSparseCommunity(input) {
3244
2742
  }
3245
2743
 
3246
2744
  // src/commands/raw-body-dedup.ts
3247
- import { createHash as createHash5 } from "crypto";
2745
+ import { createHash as createHash4 } from "crypto";
3248
2746
  async function runRawBodyDedup(vault, scan, pageTextCache) {
3249
2747
  const scanResult = scan ? ok(scan) : await scanVault(vault);
3250
2748
  if (!scanResult.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scanResult };
@@ -3253,7 +2751,7 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
3253
2751
  const text = await readPageCached(raw, pageTextCache);
3254
2752
  const split = splitFrontmatter(text);
3255
2753
  if (!split.ok) return null;
3256
- const bodyHash = createHash5("sha256").update(split.data.body).digest("hex");
2754
+ const bodyHash = createHash4("sha256").update(split.data.body).digest("hex");
3257
2755
  const fm = extractFrontmatter(text);
3258
2756
  let fmSha256 = null;
3259
2757
  if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
@@ -3285,9 +2783,9 @@ async function runRawBodyDedup(vault, scan, pageTextCache) {
3285
2783
  }
3286
2784
 
3287
2785
  // src/commands/path-too-long.ts
3288
- import { existsSync as existsSync6 } from "fs";
3289
- import { mkdir as mkdir4, readFile as readFile14, rename as rename4, unlink as unlink2 } from "fs/promises";
3290
- import { dirname as dirname7, join as join17, posix as posix2, resolve as resolve5 } from "path";
2786
+ import { existsSync as existsSync5 } from "fs";
2787
+ import { mkdir as mkdir4, readFile as readFile12, rename as rename3, unlink } from "fs/promises";
2788
+ import { dirname as dirname5, join as join16, posix, resolve as resolve4 } from "path";
3291
2789
  var MAX_PATH_LENGTH = 240;
3292
2790
  var WINDOWS_ABSOLUTE_PATH_LIMIT = 259;
3293
2791
  async function runPathTooLong(input) {
@@ -3320,10 +2818,10 @@ async function fixPathTooLong(input) {
3320
2818
  }
3321
2819
  try {
3322
2820
  if (target.mode === "dedupe") {
3323
- await unlink2(join17(input.vault, violation.relPath));
2821
+ await unlink(join16(input.vault, violation.relPath));
3324
2822
  } else {
3325
- await mkdir4(dirname7(join17(input.vault, target.relPath)), { recursive: true });
3326
- await rename4(join17(input.vault, violation.relPath), join17(input.vault, target.relPath));
2823
+ await mkdir4(dirname5(join16(input.vault, target.relPath)), { recursive: true });
2824
+ await rename3(join16(input.vault, violation.relPath), join16(input.vault, target.relPath));
3327
2825
  }
3328
2826
  fixed.push({ from: violation.relPath, to: target.relPath });
3329
2827
  } catch {
@@ -3337,7 +2835,7 @@ async function fixPathTooLong(input) {
3337
2835
  for (const page of afterScan.data.allMarkdown) {
3338
2836
  if (!shouldRewriteReferences(page.relPath)) continue;
3339
2837
  try {
3340
- const original = await readFile14(page.absPath, "utf8");
2838
+ const original = await readFile12(page.absPath, "utf8");
3341
2839
  let updated = original;
3342
2840
  for (const fix of fixed) {
3343
2841
  updated = replacePathReferences(updated, fix.from, fix.to);
@@ -3374,7 +2872,7 @@ function findPathTooLongViolations(pages, maxLength) {
3374
2872
  }
3375
2873
  function maxFixPathLength(vault) {
3376
2874
  if (process.platform !== "win32") return MAX_PATH_LENGTH;
3377
- const root = resolve5(vault);
2875
+ const root = resolve4(vault);
3378
2876
  const separatorBudget = root.endsWith("\\") || root.endsWith("/") ? 0 : 1;
3379
2877
  const absoluteSafeRelLength = WINDOWS_ABSOLUTE_PATH_LIMIT - root.length - separatorBudget;
3380
2878
  return Math.max(1, Math.min(MAX_PATH_LENGTH, absoluteSafeRelLength));
@@ -3400,9 +2898,9 @@ function truncateFilename(relPath, maxLength = MAX_PATH_LENGTH) {
3400
2898
  async function resolveFixTarget(vault, original, preferred, maxLength) {
3401
2899
  for (const candidate of candidateRelPaths(preferred, maxLength)) {
3402
2900
  if (candidate === original || candidate.length > maxLength) continue;
3403
- const candidatePath = join17(vault, candidate);
3404
- if (!existsSync6(candidatePath)) return { relPath: candidate, mode: "rename" };
3405
- if (await hasSameContent(join17(vault, original), candidatePath)) {
2901
+ const candidatePath = join16(vault, candidate);
2902
+ if (!existsSync5(candidatePath)) return { relPath: candidate, mode: "rename" };
2903
+ if (await hasSameContent(join16(vault, original), candidatePath)) {
3406
2904
  return { relPath: candidate, mode: "dedupe" };
3407
2905
  }
3408
2906
  }
@@ -3411,8 +2909,8 @@ async function resolveFixTarget(vault, original, preferred, maxLength) {
3411
2909
  function candidateRelPaths(preferred, maxLength) {
3412
2910
  const candidates = [preferred];
3413
2911
  if (preferred.length > maxLength) return candidates;
3414
- const dir = posix2.dirname(preferred) === "." ? "" : posix2.dirname(preferred);
3415
- const filename = posix2.basename(preferred);
2912
+ const dir = posix.dirname(preferred) === "." ? "" : posix.dirname(preferred);
2913
+ const filename = posix.basename(preferred);
3416
2914
  const ext = filename.endsWith(".md") ? ".md" : "";
3417
2915
  const base = ext ? filename.slice(0, -3) : filename;
3418
2916
  const dirPrefix = dir ? `${dir}/` : "";
@@ -3426,7 +2924,7 @@ function candidateRelPaths(preferred, maxLength) {
3426
2924
  }
3427
2925
  async function hasSameContent(a, b) {
3428
2926
  try {
3429
- const [left, right] = await Promise.all([readFile14(a), readFile14(b)]);
2927
+ const [left, right] = await Promise.all([readFile12(a), readFile12(b)]);
3430
2928
  return left.equals(right);
3431
2929
  } catch {
3432
2930
  return false;
@@ -3439,8 +2937,8 @@ function shouldRewriteReferences(relPath) {
3439
2937
  }
3440
2938
  function replacePathReferences(content, oldRelPath, newRelPath) {
3441
2939
  let updated = content.replaceAll(oldRelPath, newRelPath);
3442
- const oldStem = posix2.basename(oldRelPath).replace(/\.md$/, "");
3443
- const newStem = posix2.basename(newRelPath).replace(/\.md$/, "");
2940
+ const oldStem = posix.basename(oldRelPath).replace(/\.md$/, "");
2941
+ const newStem = posix.basename(newRelPath).replace(/\.md$/, "");
3444
2942
  if (oldStem !== newStem) {
3445
2943
  const oldStemEscaped = oldStem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3446
2944
  const stemWikilinkRe = new RegExp(`\\[\\[${oldStemEscaped}(\\|[^\\]]*)?\\]\\]`, "g");
@@ -3553,16 +3051,16 @@ function buildCliSurface() {
3553
3051
  fleetCmd.command("health").option("--file <path>").option("--host-id <id>").option("--json");
3554
3052
  const surface = /* @__PURE__ */ new Map();
3555
3053
  const rootFlags = new Set(program.options.map((o) => o.long ?? o.short).filter((f) => f != null));
3556
- function walk2(cmd, prefix, parentFlags) {
3054
+ function walk(cmd, prefix, parentFlags) {
3557
3055
  const key = prefix ? `${prefix}.${cmd.name()}` : cmd.name();
3558
3056
  const flags = /* @__PURE__ */ new Set([...parentFlags, ...cmd.options.map((o) => o.long ?? o.short).filter((f) => f != null)]);
3559
3057
  surface.set(key, flags);
3560
3058
  for (const sub of cmd.commands) {
3561
- walk2(sub, key, flags);
3059
+ walk(sub, key, flags);
3562
3060
  }
3563
3061
  }
3564
3062
  for (const cmd of program.commands) {
3565
- walk2(cmd, "", rootFlags);
3063
+ walk(cmd, "", rootFlags);
3566
3064
  }
3567
3065
  return surface;
3568
3066
  }
@@ -3838,7 +3336,7 @@ function recomputeRawSha256IfPresent(content) {
3838
3336
  const split = splitFrontmatter(content);
3839
3337
  if (!split.ok) return content;
3840
3338
  if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
3841
- const sha256 = createHash6("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
3339
+ const sha256 = createHash5("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
3842
3340
  const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
3843
3341
  return `---
3844
3342
  ${rawFrontmatter}
@@ -3886,24 +3384,24 @@ async function walkMarkdownFiles(absDir, vaultRoot) {
3886
3384
  const entries = await readdir3(absDir, { withFileTypes: true });
3887
3385
  const pages = [];
3888
3386
  for (const entry of entries) {
3889
- const absPath = join18(absDir, entry.name);
3387
+ const absPath = join17(absDir, entry.name);
3890
3388
  if (entry.isDirectory()) {
3891
3389
  if (entry.name === ".git" || entry.name === "node_modules") continue;
3892
3390
  pages.push(...await walkMarkdownFiles(absPath, vaultRoot));
3893
3391
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
3894
- pages.push({ absPath, relPath: relative4(vaultRoot, absPath).split(sep4).join("/") });
3392
+ pages.push({ absPath, relPath: relative2(vaultRoot, absPath).split(sep2).join("/") });
3895
3393
  }
3896
3394
  }
3897
3395
  return pages;
3898
3396
  }
3899
3397
  async function collectCliRefsPages(vault) {
3900
- if (!existsSync7(join18(vault, "SCHEMA.md"))) {
3398
+ if (!existsSync6(join17(vault, "SCHEMA.md"))) {
3901
3399
  return err("VAULT_PATH_INVALID", { root: vault, reason: "SCHEMA.md missing" });
3902
3400
  }
3903
3401
  const pages = [];
3904
3402
  for (const dir of CLI_REFS_TYPED_DIRS) {
3905
- const absDir = join18(vault, dir);
3906
- if (!existsSync7(absDir)) continue;
3403
+ const absDir = join17(vault, dir);
3404
+ if (!existsSync6(absDir)) continue;
3907
3405
  pages.push(...await walkMarkdownFiles(absDir, vault));
3908
3406
  }
3909
3407
  return ok(pages);
@@ -4024,7 +3522,7 @@ async function applyFileSourceUrlFix(input, scan, fileSourceUrlFlags, fileSource
4024
3522
  for (const relPath of fileSourceUrlFrontmatterFlags) {
4025
3523
  try {
4026
3524
  const absPath = `${input.vault}/${relPath}`;
4027
- const raw = await readFile15(absPath, "utf8");
3525
+ const raw = await readFile13(absPath, "utf8");
4028
3526
  const parts = raw.split("---", 3);
4029
3527
  if (parts.length < 3) {
4030
3528
  unresolved.push(relPath);
@@ -4417,8 +3915,8 @@ async function runLint(input) {
4417
3915
  const readKnowledgeContent = (slug) => {
4418
3916
  const existing = knowledgeContentCache.get(slug);
4419
3917
  if (existing) return existing;
4420
- const knowledgePath = join18(lintVault, "projects", slug, "knowledge.md");
4421
- const pending = existsSync7(knowledgePath) ? readFile15(knowledgePath, "utf8").catch(() => null) : Promise.resolve(null);
3918
+ const knowledgePath = join17(lintVault, "projects", slug, "knowledge.md");
3919
+ const pending = existsSync6(knowledgePath) ? readFile13(knowledgePath, "utf8").catch(() => null) : Promise.resolve(null);
4422
3920
  knowledgeContentCache.set(slug, pending);
4423
3921
  return pending;
4424
3922
  };
@@ -4524,7 +4022,7 @@ async function runLint(input) {
4524
4022
  for (const relPath of legacyPages) {
4525
4023
  try {
4526
4024
  const absPath = `${input.vault}/${relPath}`;
4527
- const raw = await readFile15(absPath, "utf8");
4025
+ const raw = await readFile13(absPath, "utf8");
4528
4026
  const split = splitFrontmatter(raw);
4529
4027
  if (!split.ok) {
4530
4028
  unresolved.push(relPath);
@@ -4623,7 +4121,7 @@ ${newBody}`;
4623
4121
  for (const relPath of noOverview) {
4624
4122
  try {
4625
4123
  const absPath = `${input.vault}/${relPath}`;
4626
- const raw = await readFile15(absPath, "utf8");
4124
+ const raw = await readFile13(absPath, "utf8");
4627
4125
  const split = splitFrontmatter(raw);
4628
4126
  if (!split.ok) {
4629
4127
  unresolved.push(relPath);
@@ -4664,7 +4162,7 @@ ${trimmedBody}`;
4664
4162
  for (const relPath of missingTldrFlags) {
4665
4163
  try {
4666
4164
  const absPath = `${input.vault}/${relPath}`;
4667
- const raw = await readFile15(absPath, "utf8");
4165
+ const raw = await readFile13(absPath, "utf8");
4668
4166
  const split = splitFrontmatter(raw);
4669
4167
  if (!split.ok) {
4670
4168
  unresolved.push(relPath);
@@ -4714,7 +4212,7 @@ ${lines.join("\n")}`;
4714
4212
  for (const relPath of wikilinkCitationFlags) {
4715
4213
  try {
4716
4214
  const absPath = `${input.vault}/${relPath}`;
4717
- const raw = await readFile15(absPath, "utf8");
4215
+ const raw = await readFile13(absPath, "utf8");
4718
4216
  const split = splitFrontmatter(raw);
4719
4217
  if (!split.ok) {
4720
4218
  unresolved.push(relPath);
@@ -5055,14 +4553,14 @@ async function runSyncLintDelta(input) {
5055
4553
  }
5056
4554
 
5057
4555
  // src/commands/config.ts
5058
- import { readFile as readFile16 } from "fs/promises";
5059
- import { existsSync as existsSync8 } from "fs";
5060
- import { join as join19 } from "path";
4556
+ import { readFile as readFile14 } from "fs/promises";
4557
+ import { existsSync as existsSync7 } from "fs";
4558
+ import { join as join18 } from "path";
5061
4559
  function validateKey(key) {
5062
4560
  return CONFIG_KEYS.includes(key) || isValidWikiProfileKey(key);
5063
4561
  }
5064
4562
  function configPath(home) {
5065
- return join19(home, ".skillwiki", ".env");
4563
+ return join18(home, ".skillwiki", ".env");
5066
4564
  }
5067
4565
  async function runConfigGet(input) {
5068
4566
  if (!validateKey(input.key)) {
@@ -5080,7 +4578,7 @@ async function runConfigSet(input) {
5080
4578
  try {
5081
4579
  let originalContent;
5082
4580
  try {
5083
- originalContent = await readFile16(filePath, "utf8");
4581
+ originalContent = await readFile14(filePath, "utf8");
5084
4582
  } catch {
5085
4583
  }
5086
4584
  const existing = originalContent !== void 0 ? parseDotenvText(originalContent) : {};
@@ -5093,505 +4591,43 @@ async function runConfigSet(input) {
5093
4591
  }
5094
4592
  async function runConfigList(input) {
5095
4593
  const map = await parseDotenvFile(configPath(input.home));
5096
- const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
5097
- let profiles;
5098
- if (input.profiles) {
5099
- const defaultProfile = map["WIKI_DEFAULT"];
5100
- profiles = [];
5101
- for (const key of Object.keys(map)) {
5102
- const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
5103
- if (m && key !== "WIKI_PATH") {
5104
- const name = m[1].toLowerCase().replace(/_/g, "-");
5105
- profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
5106
- }
5107
- }
5108
- profiles.sort((a, b) => a.name.localeCompare(b.name));
5109
- }
5110
- const hint = profiles ? profiles.map((p) => `${p.isDefault ? "* " : " "}${p.name} \u2192 ${p.path}`).join("\n") || "(no profiles)" : entries.map((e) => `${e.key}=${e.value}`).join("\n");
5111
- return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
5112
- }
5113
- async function runConfigPath(input) {
5114
- const filePath = configPath(input.home);
5115
- return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync8(filePath), humanHint: filePath }) };
5116
- }
5117
-
5118
- // src/commands/fleet.ts
5119
- import { readFile as readFile17 } from "fs/promises";
5120
- import { hostname as nodeHostname, userInfo } from "os";
5121
- import { join as join20 } from "path";
5122
- import yaml3 from "js-yaml";
5123
- var FLEET_REL_PATH = join20("projects", "llm-wiki", "architecture", "fleet.yaml");
5124
- async function runFleetValidate(input) {
5125
- const loaded = await loadFleetManifest(input.file);
5126
- if (!loaded.ok) {
5127
- if (loaded.error === "FILE_NOT_FOUND") {
5128
- return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
5129
- }
5130
- const errors = fleetLoadErrors(loaded);
5131
- return invalidFleet(errors);
5132
- }
5133
- const warnings = fleetWarnings(loaded.manifest);
5134
- const snapshotter = findSnapshotter(loaded.manifest);
5135
- return {
5136
- exitCode: ExitCode.OK,
5137
- result: ok({
5138
- valid: true,
5139
- errors: [],
5140
- warnings,
5141
- host_count: Object.keys(loaded.manifest.hosts).length,
5142
- snapshotter,
5143
- humanHint: `VALID fleet manifest (${Object.keys(loaded.manifest.hosts).length} hosts; snapshotter: ${snapshotter ?? "none"})`
5144
- })
5145
- };
5146
- }
5147
- async function runFleetContext(input) {
5148
- const env = input.env ?? process.env;
5149
- const home = input.home ?? env.HOME ?? "";
5150
- const cwd = input.cwd ?? process.cwd();
5151
- const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
5152
- const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
5153
- const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
5154
- const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
5155
- const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
5156
- const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
5157
- if (!loaded.ok) {
5158
- const warnings = ["fleet manifest unavailable or invalid"];
5159
- const markdown2 = formatUnknownContext({
5160
- generatedAt,
5161
- osHostname,
5162
- user,
5163
- cwd,
5164
- vault,
5165
- reason: warnings[0],
5166
- trace: [],
5167
- warnings
5168
- });
5169
- return {
5170
- exitCode: ExitCode.OK,
5171
- result: ok({
5172
- manifest_loaded: false,
5173
- generated_at: generatedAt,
5174
- identity_status: "unknown",
5175
- resolver_trace: [],
5176
- warnings,
5177
- markdown: markdown2,
5178
- humanHint: markdown2
5179
- })
5180
- };
5181
- }
5182
- const resolved = await resolveFleetHostId({
5183
- manifest: loaded.manifest,
5184
- hostId: input.hostId,
5185
- env,
5186
- home,
5187
- osHostname
5188
- });
5189
- if (resolved.hostId && !loaded.manifest.hosts[resolved.hostId]) {
5190
- const source = resolved.source ?? "unknown";
5191
- const warnings = [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`];
5192
- const markdown2 = formatInvalidContext({
5193
- generatedAt,
5194
- hostId: resolved.hostId,
5195
- source,
5196
- osHostname,
5197
- user,
5198
- cwd,
5199
- vault,
5200
- trace: resolved.trace,
5201
- warnings
5202
- });
5203
- return {
5204
- exitCode: ExitCode.OK,
5205
- result: ok({
5206
- manifest_loaded: true,
5207
- host_id: resolved.hostId,
5208
- source: resolved.source,
5209
- generated_at: generatedAt,
5210
- identity_status: "invalid",
5211
- resolver_trace: resolved.trace,
5212
- warnings,
5213
- markdown: markdown2,
5214
- humanHint: markdown2
5215
- })
5216
- };
5217
- }
5218
- if (!resolved.hostId) {
5219
- const warnings = ["host identity is unresolved"];
5220
- const markdown2 = formatUnknownContext({
5221
- generatedAt,
5222
- osHostname,
5223
- user,
5224
- cwd,
5225
- vault,
5226
- reason: warnings[0],
5227
- trace: resolved.trace,
5228
- warnings
5229
- });
5230
- return {
5231
- exitCode: ExitCode.OK,
5232
- result: ok({
5233
- manifest_loaded: true,
5234
- generated_at: generatedAt,
5235
- identity_status: "unknown",
5236
- resolver_trace: resolved.trace,
5237
- warnings,
5238
- markdown: markdown2,
5239
- humanHint: markdown2
5240
- })
5241
- };
5242
- }
5243
- const markdown = formatKnownContext({
5244
- manifest: loaded.manifest,
5245
- hostId: resolved.hostId,
5246
- source: resolved.source,
5247
- generatedAt,
5248
- osHostname,
5249
- user,
5250
- cwd,
5251
- vault,
5252
- trace: resolved.trace
5253
- });
5254
- return {
5255
- exitCode: ExitCode.OK,
5256
- result: ok({
5257
- manifest_loaded: true,
5258
- host_id: resolved.hostId,
5259
- source: resolved.source,
5260
- generated_at: generatedAt,
5261
- identity_status: "known",
5262
- resolver_trace: resolved.trace,
5263
- warnings: [],
5264
- markdown,
5265
- humanHint: markdown
5266
- })
5267
- };
5268
- }
5269
- function fleetContextEnv(input) {
5270
- const env = input.env ?? process.env;
5271
- const home = input.home ?? env.HOME ?? "";
5272
- const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
5273
- const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
5274
- const file = input.file ?? (vault ? join20(vault, FLEET_REL_PATH) : void 0);
5275
- return { env, home, osHostname, vault, file };
5276
- }
5277
- async function loadFleetManifestAndHost(input) {
5278
- const { env, home, osHostname, file } = fleetContextEnv(input);
5279
- if (!file) return null;
5280
- const loaded = await loadFleetManifest(file);
5281
- if (!loaded.ok) return null;
5282
- const resolved = await resolveFleetHostId({
5283
- manifest: loaded.manifest,
5284
- hostId: input.hostId,
5285
- env,
5286
- home,
5287
- osHostname
5288
- });
5289
- if (!resolved.hostId) {
5290
- return {
5291
- manifest: loaded.manifest,
5292
- hostId: void 0,
5293
- source: resolved.source,
5294
- warnings: ["host identity is unresolved"],
5295
- identityStatus: "unknown"
5296
- };
5297
- }
5298
- if (!loaded.manifest.hosts[resolved.hostId]) {
5299
- const source = resolved.source ?? "unknown";
5300
- return {
5301
- manifest: loaded.manifest,
5302
- hostId: resolved.hostId,
5303
- source: resolved.source,
5304
- warnings: [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`],
5305
- identityStatus: "invalid"
5306
- };
5307
- }
5308
- return {
5309
- manifest: loaded.manifest,
5310
- hostId: resolved.hostId,
5311
- source: resolved.source,
5312
- warnings: [],
5313
- identityStatus: "known"
5314
- };
5315
- }
5316
- function snapshotterAliasForLocalHost(fleetLoad) {
5317
- if (!fleetLoad?.manifest || !fleetLoad.hostId) return void 0;
5318
- const snapshotterId = Object.entries(fleetLoad.manifest.hosts).find(([, h]) => h.role === "snapshotter")?.[0];
5319
- if (!snapshotterId) return void 0;
5320
- const profile = fleetLoad.manifest.hosts[snapshotterId]?.access?.from?.[fleetLoad.hostId];
5321
- if (!profile || profile.status !== "configured" && profile.status !== "local") return void 0;
5322
- const aliases = profile.ssh_aliases ?? [];
5323
- return aliases.length > 0 ? aliases[0] : void 0;
5324
- }
5325
- function satelliteGateFromFleetLoad(load) {
5326
- if (!load?.hostId) return { satelliteExpected: false };
5327
- const host = load.manifest.hosts[load.hostId];
5328
- if (!host) return { satelliteExpected: false };
5329
- return { satelliteExpected: host.maintenance?.skillwiki_satellite?.enabled === true };
5330
- }
5331
- async function loadFleetManifest(file) {
5332
- let text;
5333
- try {
5334
- text = await readFile17(file, "utf8");
5335
- } catch {
5336
- return { ok: false, error: "FILE_NOT_FOUND" };
5337
- }
5338
- let parsed;
5339
- try {
5340
- parsed = yaml3.load(text, { schema: yaml3.JSON_SCHEMA });
5341
- } catch (error) {
5342
- return { ok: false, error: "INVALID_YAML", detail: error instanceof Error ? error.message : String(error) };
5343
- }
5344
- const result = FleetManifestSchema.safeParse(parsed);
5345
- if (!result.success) {
5346
- return { ok: false, error: "INVALID_FLEET_MANIFEST", detail: result.error.issues };
5347
- }
5348
- return { ok: true, manifest: result.data };
5349
- }
5350
- function invalidFleet(errors) {
5351
- return {
5352
- exitCode: ExitCode.FLEET_MANIFEST_INVALID,
5353
- result: ok({
5354
- valid: false,
5355
- errors,
5356
- warnings: [],
5357
- host_count: 0,
5358
- humanHint: `INVALID fleet manifest
5359
- ${errors.map((e) => ` ${e.path || "(root)"}: ${e.message}`).join("\n")}`
5360
- })
5361
- };
5362
- }
5363
- function fleetLoadErrors(loaded) {
5364
- if (loaded.error === "INVALID_YAML") {
5365
- return [{ path: "", message: `invalid YAML: ${String(loaded.detail ?? "parse failed")}` }];
5366
- }
5367
- if (loaded.error === "INVALID_FLEET_MANIFEST" && Array.isArray(loaded.detail)) {
5368
- return loaded.detail.map((issue) => {
5369
- const zodIssue = issue;
5370
- return {
5371
- path: (zodIssue.path ?? []).join("."),
5372
- message: zodIssue.message ?? "invalid value"
5373
- };
5374
- });
5375
- }
5376
- return [{ path: "", message: loaded.error }];
5377
- }
5378
- function fleetWarnings(manifest) {
5379
- const warnings = [];
5380
- for (const [id, host] of Object.entries(manifest.hosts)) {
5381
- if (host.role === "snapshotter" && host.protected !== true) {
5382
- warnings.push(`snapshotter host '${id}' is not protected=true`);
5383
- }
5384
- }
5385
- return warnings;
5386
- }
5387
- function findSnapshotter(manifest) {
5388
- return Object.entries(manifest.hosts).find(([, host]) => host.role === "snapshotter")?.[0];
5389
- }
5390
- async function resolveFleetHostId(input) {
5391
- const trace = [];
5392
- if (input.hostId) {
5393
- trace.push({ source: "--host-id", status: "matched", value: input.hostId });
5394
- return { hostId: input.hostId, source: "host-id", trace };
5395
- }
5396
- trace.push({ source: "--host-id", status: "unset" });
5397
- if (input.env.SKILLWIKI_HOST_ID) {
5398
- trace.push({ source: "SKILLWIKI_HOST_ID", status: "matched", value: input.env.SKILLWIKI_HOST_ID });
5399
- return { hostId: input.env.SKILLWIKI_HOST_ID, source: "SKILLWIKI_HOST_ID", trace };
5400
- }
5401
- trace.push({ source: "SKILLWIKI_HOST_ID", status: "unset" });
5402
- if (input.env.AGENT_HOST_ID) {
5403
- trace.push({ source: "AGENT_HOST_ID", status: "matched", value: input.env.AGENT_HOST_ID });
5404
- return { hostId: input.env.AGENT_HOST_ID, source: "AGENT_HOST_ID", trace };
5405
- }
5406
- trace.push({ source: "AGENT_HOST_ID", status: "unset" });
5407
- if (input.home) {
5408
- const dotenv = await parseDotenvFile(join20(input.home, ".skillwiki", ".env"));
5409
- if (dotenv.SKILLWIKI_HOST_ID) {
5410
- trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "matched", value: dotenv.SKILLWIKI_HOST_ID });
5411
- return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", trace };
5412
- }
5413
- trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "unset" });
5414
- } else {
5415
- trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "skipped" });
5416
- }
5417
- if (input.env.VS_HOSTNAME) {
5418
- trace.push({ source: "VS_HOSTNAME", status: "matched", value: input.env.VS_HOSTNAME });
5419
- return { hostId: input.env.VS_HOSTNAME, source: "VS_HOSTNAME", trace };
5420
- }
5421
- trace.push({ source: "VS_HOSTNAME", status: "unset" });
5422
- const hostname = input.osHostname.trim();
5423
- if (hostname) {
5424
- if (input.manifest.hosts[hostname]) {
5425
- trace.push({ source: "hostname", status: "matched", value: hostname });
5426
- return { hostId: hostname, source: "hostname", trace };
5427
- }
5428
- const byHostname = Object.entries(input.manifest.hosts).find(([, host]) => host.identity.hostnames.includes(hostname));
5429
- if (byHostname) {
5430
- trace.push({ source: "hostname", status: "matched", value: hostname });
5431
- return { hostId: byHostname[0], source: "hostname", trace };
5432
- }
5433
- trace.push({ source: "hostname", status: "unmatched", value: hostname });
5434
- } else {
5435
- trace.push({ source: "hostname", status: "unset" });
5436
- }
5437
- return { trace };
5438
- }
5439
- function formatKnownContext(input) {
5440
- const host = input.manifest.hosts[input.hostId];
5441
- const protectedValue = host.protected === true ? "true" : "false";
5442
- const writesTo = host.writes_to.join(", ");
5443
- const selfAliases = collectSelfAliases(input.manifest, input.hostId);
5444
- const outbound = collectOutboundAccess(input.manifest, input.hostId);
5445
- const maintenanceLines = formatMaintenanceLines(host);
5446
- const guidance = host.role === "snapshotter" && host.protected === true ? `this session is already on \`${input.hostId}\`; this is a protected snapshotter host. Live-vault authoring at the resolved \`skillwiki path\` is allowed here. Do not mutate snapshot worktrees or repo-local project workspaces from this session except explicitly approved snapshot maintenance. Keep release-validation workflows read-only when they are documented as such.` : input.hostId === "macos-dev" ? "use declared SSH aliases for remote work when needed; do not assume undeclared hosts have reciprocal SSH access." : `this session is already on \`${input.hostId}\`; do not SSH to self aliases unless the user explicitly asks. Do not assume outbound SSH to other fleet hosts is configured.`;
5447
- return [
5448
- "## Runtime Host Context",
5449
- "",
5450
- `- Context generated: \`${input.generatedAt}\``,
5451
- `- Current machine: \`${input.hostId}\`${input.source ? ` (source: \`${input.source}\`)` : ""}`,
5452
- "- Identity status: `known`",
5453
- `- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
5454
- `- Resolver trace: ${formatTrace(input.trace)}`,
5455
- `- OS hostname: ${formatMaybe(input.osHostname)}`,
5456
- `- User: ${formatMaybe(input.user)}`,
5457
- `- Workspace: ${formatMaybe(input.cwd)}`,
5458
- `- Vault: ${formatMaybe(input.vault)}`,
5459
- "- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
5460
- `- Fleet role: \`${host.role}\`; protected: \`${protectedValue}\`; writes_to: \`${writesTo}\``,
5461
- ...maintenanceLines,
5462
- `- Self SSH aliases known in fleet: ${formatList(selfAliases)}`,
5463
- `- Declared outbound SSH from this source: ${formatOutboundAccess(outbound)}`,
5464
- `- Guidance: ${guidance}`
5465
- ].join("\n");
5466
- }
5467
- function formatUnknownContext(input) {
5468
- return [
5469
- "## Runtime Host Context",
5470
- "",
5471
- `- Context generated: \`${input.generatedAt}\``,
5472
- "- Current machine: unknown",
5473
- "- Identity status: `unknown`",
5474
- `- Resolver trace: ${formatTrace(input.trace)}`,
5475
- `- Warnings: ${formatWarnings(input.warnings)}`,
5476
- `- OS hostname: ${formatMaybe(input.osHostname)}`,
5477
- `- User: ${formatMaybe(input.user)}`,
5478
- `- Workspace: ${formatMaybe(input.cwd)}`,
5479
- `- Vault: ${formatMaybe(input.vault)}`,
5480
- "- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
5481
- "- Fleet role: unknown",
5482
- "- Self SSH aliases known in fleet: unknown",
5483
- "- Declared outbound SSH from this source: unknown",
5484
- `- Guidance: ${input.reason}; do not assume local vs remote role. Inspect runtime or ask before SSH/deploy/sync work.`
5485
- ].join("\n");
5486
- }
5487
- function formatInvalidContext(input) {
5488
- return [
5489
- "## Runtime Host Context",
5490
- "",
5491
- `- Context generated: \`${input.generatedAt}\``,
5492
- "- Current machine: unknown",
5493
- "- Identity status: `invalid`",
5494
- `- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
5495
- `- Resolver trace: ${formatTrace(input.trace)}`,
5496
- `- Warnings: ${formatWarnings(input.warnings)}`,
5497
- `- OS hostname: ${formatMaybe(input.osHostname)}`,
5498
- `- User: ${formatMaybe(input.user)}`,
5499
- `- Workspace: ${formatMaybe(input.cwd)}`,
5500
- `- Vault: ${formatMaybe(input.vault)}`,
5501
- "- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
5502
- "- Fleet role: unknown",
5503
- "- Self SSH aliases known in fleet: unknown",
5504
- "- Declared outbound SSH from this source: unknown",
5505
- `- Guidance: do not trust this identity; rerun with \`--host-id\` only if the user confirms \`${input.hostId}\` is the current fleet host id.`
5506
- ].join("\n");
5507
- }
5508
- function collectSelfAliases(manifest, hostId2) {
5509
- const aliases = [];
5510
- const host = manifest.hosts[hostId2];
5511
- const access = host?.access?.from ?? {};
5512
- for (const profile of Object.values(access)) {
5513
- for (const alias of profile.ssh_aliases ?? []) aliases.push(alias);
5514
- }
5515
- return [...new Set(aliases)];
5516
- }
5517
- function collectOutboundAccess(manifest, sourceHostId) {
5518
- const hosts = [];
5519
- for (const [targetId, target] of Object.entries(manifest.hosts)) {
5520
- if (targetId === sourceHostId) continue;
5521
- const profile = target.access?.from?.[sourceHostId];
5522
- if (profile && (profile.status === "configured" || profile.status === "local")) {
5523
- hosts.push({
5524
- hostId: targetId,
5525
- sshAliases: [...new Set(profile.ssh_aliases ?? [])],
5526
- users: [...new Set(profile.users ?? [])]
5527
- });
4594
+ const entries = Object.entries(map).map(([key, value]) => ({ key, value: value ?? "" }));
4595
+ let profiles;
4596
+ if (input.profiles) {
4597
+ const defaultProfile = map["WIKI_DEFAULT"];
4598
+ profiles = [];
4599
+ for (const key of Object.keys(map)) {
4600
+ const m = key.match(/^WIKI_([A-Z][A-Z0-9_]{0,31})_PATH$/);
4601
+ if (m && key !== "WIKI_PATH") {
4602
+ const name = m[1].toLowerCase().replace(/_/g, "-");
4603
+ profiles.push({ name, path: map[key] ?? "", isDefault: name === defaultProfile });
4604
+ }
5528
4605
  }
4606
+ profiles.sort((a, b) => a.name.localeCompare(b.name));
5529
4607
  }
5530
- return hosts.sort((left, right) => left.hostId.localeCompare(right.hostId));
5531
- }
5532
- function formatMaintenanceLines(host) {
5533
- const satellite = host.maintenance?.skillwiki_satellite;
5534
- if (!satellite?.enabled) return [];
5535
- return [
5536
- `- Maintenance role: \`skillwiki satellite\`; user: \`${satellite.user}\`; ssh: \`${satellite.ssh_alias}\``,
5537
- `- Maintenance paths: maintenance vault: \`${satellite.vault_path}\`; repo: \`${satellite.repo_path}\`; scheduler: \`${satellite.scheduler}\`; jobs: ${formatList(satellite.jobs)}`
5538
- ];
5539
- }
5540
- function formatOutboundAccess(values) {
5541
- if (values.length === 0) return "none";
5542
- return values.map((value) => {
5543
- const aliasPart = value.sshAliases.length > 0 ? ` via ${formatList(value.sshAliases)}` : " (no SSH aliases)";
5544
- const usersPart = value.users.length > 0 ? ` (users: ${formatList(value.users)})` : "";
5545
- return `\`${value.hostId}\`${aliasPart}${usersPart}`;
5546
- }).join("; ");
5547
- }
5548
- function formatResolution(source, hostId2) {
5549
- return source ? `\`${source === "host-id" ? "--host-id" : source}\` -> \`${hostId2}\`` : `unknown -> \`${hostId2}\``;
5550
- }
5551
- function formatTrace(values) {
5552
- if (values.length === 0) return "not available";
5553
- return values.map((value) => {
5554
- const source = `\`${value.source}\``;
5555
- if (value.status === "matched") return `${source} matched \`${value.value ?? ""}\``;
5556
- if (value.status === "unmatched") return `${source} unmatched \`${value.value ?? ""}\``;
5557
- return `${source} ${value.status}`;
5558
- }).join("; ");
5559
- }
5560
- function formatWarnings(values) {
5561
- return values.length > 0 ? values.join("; ") : "none";
5562
- }
5563
- function formatList(values) {
5564
- return values.length > 0 ? values.map((v) => `\`${v}\``).join(", ") : "none";
5565
- }
5566
- function formatMaybe(value) {
5567
- return value && value.trim().length > 0 ? `\`${value}\`` : "unknown";
5568
- }
5569
- function safeEnvValue(value) {
5570
- return value && value.trim().length > 0 ? value : void 0;
4608
+ const hint = profiles ? profiles.map((p) => `${p.isDefault ? "* " : " "}${p.name} \u2192 ${p.path}`).join("\n") || "(no profiles)" : entries.map((e) => `${e.key}=${e.value}`).join("\n");
4609
+ return { exitCode: ExitCode.OK, result: ok({ entries, profiles, humanHint: hint }) };
5571
4610
  }
5572
- function safeUserName() {
5573
- try {
5574
- return userInfo().username;
5575
- } catch {
5576
- return "";
5577
- }
4611
+ async function runConfigPath(input) {
4612
+ const filePath = configPath(input.home);
4613
+ return { exitCode: ExitCode.OK, result: ok({ path: filePath, exists: existsSync7(filePath), humanHint: filePath }) };
5578
4614
  }
5579
4615
 
5580
4616
  // src/commands/doctor.ts
5581
- import { existsSync as existsSync14, lstatSync as lstatSync2, readlinkSync, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync11 } from "fs";
5582
- import { join as join26, resolve as resolve6 } from "path";
4617
+ import { existsSync as existsSync13, lstatSync, readlinkSync, readdirSync as readdirSync3, statSync as statSync2, readFileSync as readFileSync10 } from "fs";
4618
+ import { join as join24, resolve as resolve5 } from "path";
5583
4619
  import { execSync as execSync2 } from "child_process";
5584
4620
  import { platform as platform2 } from "os";
5585
4621
 
5586
4622
  // src/utils/plugin-registry.ts
5587
- import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync6 } from "fs";
5588
- import { join as join21 } from "path";
5589
- var REGISTRY_PATH = join21(".claude", "plugins", "installed_plugins.json");
5590
- var CODEX_CONFIG_PATH = join21(".codex", "config.toml");
4623
+ import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync5 } from "fs";
4624
+ import { join as join19 } from "path";
4625
+ var REGISTRY_PATH = join19(".claude", "plugins", "installed_plugins.json");
4626
+ var CODEX_CONFIG_PATH = join19(".codex", "config.toml");
5591
4627
  var PLUGIN_KEY = "skillwiki@llm-wiki";
5592
4628
  function readInstalledPlugins(home) {
5593
4629
  try {
5594
- const raw = readFileSync6(join21(home, REGISTRY_PATH), "utf8");
4630
+ const raw = readFileSync5(join19(home, REGISTRY_PATH), "utf8");
5595
4631
  return JSON.parse(raw);
5596
4632
  } catch {
5597
4633
  return null;
@@ -5627,8 +4663,8 @@ function findPluginInstallations(home, key = PLUGIN_KEY) {
5627
4663
  function findCodexPlugin(home, key, pluginName, marketplace) {
5628
4664
  const config = readCodexPluginConfig(home, key, marketplace);
5629
4665
  if (!config?.enabled) return null;
5630
- const cacheRoot = join21(home, ".codex", "plugins", "cache", marketplace, pluginName);
5631
- if (!existsSync9(cacheRoot)) return null;
4666
+ const cacheRoot = join19(home, ".codex", "plugins", "cache", marketplace, pluginName);
4667
+ if (!existsSync8(cacheRoot)) return null;
5632
4668
  let versions;
5633
4669
  try {
5634
4670
  versions = readdirSync(cacheRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
@@ -5643,7 +4679,7 @@ function findCodexPlugin(home, key, pluginName, marketplace) {
5643
4679
  key,
5644
4680
  pluginName,
5645
4681
  marketplace,
5646
- installPath: join21(cacheRoot, version),
4682
+ installPath: join19(cacheRoot, version),
5647
4683
  version,
5648
4684
  sourceType: config.sourceType,
5649
4685
  source: config.source
@@ -5660,7 +4696,7 @@ function parsePluginKey(key) {
5660
4696
  function readCodexPluginConfig(home, key, marketplace) {
5661
4697
  let raw;
5662
4698
  try {
5663
- raw = readFileSync6(join21(home, CODEX_CONFIG_PATH), "utf8");
4699
+ raw = readFileSync5(join19(home, CODEX_CONFIG_PATH), "utf8");
5664
4700
  } catch {
5665
4701
  return null;
5666
4702
  }
@@ -5702,8 +4738,8 @@ function parseTomlScalar(rawValue) {
5702
4738
  }
5703
4739
 
5704
4740
  // src/utils/conflict-markers.ts
5705
- import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync7 } from "fs";
5706
- import { join as join22 } from "path";
4741
+ import { existsSync as existsSync9, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
4742
+ import { join as join20 } from "path";
5707
4743
  function scanConflictMarkerBlocksInText(relPath, text) {
5708
4744
  const findings = [];
5709
4745
  const lines = text.split(/\r?\n/);
@@ -5754,21 +4790,21 @@ function walkMarkdownFiles2(root, dir, rel, out) {
5754
4790
  for (const entry of entries) {
5755
4791
  if (entry.isDirectory()) {
5756
4792
  if (PRUNE_DIRS.has(entry.name)) continue;
5757
- walkMarkdownFiles2(root, join22(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
4793
+ walkMarkdownFiles2(root, join20(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name, out);
5758
4794
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
5759
4795
  out.push(rel ? `${rel}/${entry.name}` : entry.name);
5760
4796
  }
5761
4797
  }
5762
4798
  }
5763
4799
  function scanVaultConflictMarkers(vaultRoot) {
5764
- if (!existsSync10(vaultRoot)) return [];
4800
+ if (!existsSync9(vaultRoot)) return [];
5765
4801
  const relPaths = [];
5766
4802
  walkMarkdownFiles2(vaultRoot, vaultRoot, "", relPaths);
5767
4803
  const all = [];
5768
4804
  for (const rel of relPaths) {
5769
4805
  let text;
5770
4806
  try {
5771
- text = readFileSync7(join22(vaultRoot, rel), "utf8");
4807
+ text = readFileSync6(join20(vaultRoot, rel), "utf8");
5772
4808
  } catch {
5773
4809
  continue;
5774
4810
  }
@@ -5778,8 +4814,8 @@ function scanVaultConflictMarkers(vaultRoot) {
5778
4814
  }
5779
4815
 
5780
4816
  // src/utils/remote-health.ts
5781
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
5782
- import { join as join23 } from "path";
4817
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
4818
+ import { join as join21 } from "path";
5783
4819
  import { execFileSync } from "child_process";
5784
4820
  var REMOTE_PROBE_TIMEOUT_MS = 3e3;
5785
4821
  var defaultExec = (file, args, cwd) => execFileSync(file, args, {
@@ -5790,7 +4826,7 @@ var defaultExec = (file, args, cwd) => execFileSync(file, args, {
5790
4826
  }).trim();
5791
4827
  function readWikiS3RemoteConfigured(home) {
5792
4828
  try {
5793
- const content = readFileSync8(join23(home, ".skillwiki", ".env"), "utf8");
4829
+ const content = readFileSync7(join21(home, ".skillwiki", ".env"), "utf8");
5794
4830
  for (const line of content.split(/\r?\n/)) {
5795
4831
  const trimmed = line.trim();
5796
4832
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -5815,7 +4851,7 @@ function resolveWikiS3Remote(input) {
5815
4851
  return readWikiS3RemoteConfigured(input.home);
5816
4852
  }
5817
4853
  function probeGithubReachability(vaultPath, exec = defaultExec) {
5818
- if (!existsSync11(join23(vaultPath, ".git"))) return "unknown";
4854
+ if (!existsSync10(join21(vaultPath, ".git"))) return "unknown";
5819
4855
  try {
5820
4856
  exec("git", ["remote", "get-url", "origin"], vaultPath);
5821
4857
  } catch {
@@ -5838,8 +4874,8 @@ function probeS3Reachability(remote, exec = defaultExec) {
5838
4874
  return "unreachable";
5839
4875
  }
5840
4876
  }
5841
- function probeSnapshotterSsh(sshAlias2, exec = defaultExec) {
5842
- if (!sshAlias2) return "unknown";
4877
+ function probeSnapshotterSsh(sshAlias, exec = defaultExec) {
4878
+ if (!sshAlias) return "unknown";
5843
4879
  try {
5844
4880
  exec("ssh", [
5845
4881
  "-o",
@@ -5848,7 +4884,7 @@ function probeSnapshotterSsh(sshAlias2, exec = defaultExec) {
5848
4884
  "ConnectTimeout=3",
5849
4885
  "-o",
5850
4886
  "StrictHostKeyChecking=accept-new",
5851
- sshAlias2,
4887
+ sshAlias,
5852
4888
  "true"
5853
4889
  ]);
5854
4890
  return "ok";
@@ -5880,11 +4916,11 @@ function probeRemoteHealth(input) {
5880
4916
  }
5881
4917
 
5882
4918
  // src/utils/satellite-run-health.ts
5883
- import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
5884
- import { join as join24 } from "path";
4919
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
4920
+ import { join as join22 } from "path";
5885
4921
  var SATELLITE_STALE_MS = 26 * 60 * 60 * 1e3;
5886
4922
  function satelliteLatestRunPath(vault) {
5887
- return join24(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
4923
+ return join22(vault, ".skillwiki", "agent-memory-trends", "latest-run.json");
5888
4924
  }
5889
4925
  function isFailedRunStatus(status) {
5890
4926
  return status === "fail" || status === "failure";
@@ -5906,9 +4942,9 @@ function readSatelliteLatestRunFromText(text) {
5906
4942
  }
5907
4943
  function readSatelliteLatestRun(vault) {
5908
4944
  const latestPath = satelliteLatestRunPath(vault);
5909
- if (!existsSync12(latestPath)) return null;
4945
+ if (!existsSync11(latestPath)) return null;
5910
4946
  try {
5911
- return parseLatestRunFile(readFileSync9(latestPath, "utf8"));
4947
+ return parseLatestRunFile(readFileSync8(latestPath, "utf8"));
5912
4948
  } catch {
5913
4949
  return null;
5914
4950
  }
@@ -5937,8 +4973,8 @@ function evaluateSatelliteRunHealth(vault, now) {
5937
4973
  // src/utils/s3-mount-health.ts
5938
4974
  import { execSync } from "child_process";
5939
4975
  import { platform } from "os";
5940
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5, readFileSync as readFile18 } from "fs";
5941
- import { join as join25 } from "path";
4976
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, unlinkSync as unlinkSync5, readFileSync as readFile15 } from "fs";
4977
+ import { join as join23 } from "path";
5942
4978
  var OS = platform();
5943
4979
  function findRcloneMountPid() {
5944
4980
  try {
@@ -6022,7 +5058,7 @@ function extractRcloneFs(args) {
6022
5058
  function getRcloneArgs(pid) {
6023
5059
  try {
6024
5060
  if (OS === "linux") {
6025
- const raw = readFileSync10(`/proc/${pid}/cmdline`);
5061
+ const raw = readFileSync9(`/proc/${pid}/cmdline`);
6026
5062
  return new TextDecoder().decode(raw).split("\0").filter(Boolean);
6027
5063
  } else {
6028
5064
  const out = execSync(`ps -o args= -p ${pid}`, {
@@ -6065,7 +5101,7 @@ function queryRcloneRC(rcAddr, fs) {
6065
5101
  function detectFuseMount(vaultPath) {
6066
5102
  try {
6067
5103
  if (OS === "linux") {
6068
- const mounts = readFileSync10("/proc/mounts", "utf8");
5104
+ const mounts = readFileSync9("/proc/mounts", "utf8");
6069
5105
  let best = null;
6070
5106
  for (const line of mounts.split("\n")) {
6071
5107
  const parts = line.split(" ");
@@ -6096,7 +5132,7 @@ function detectFuseMount(vaultPath) {
6096
5132
  return null;
6097
5133
  }
6098
5134
  function writeTest(dir) {
6099
- const testFile = join25(dir, `.doctor-write-test-${process.pid}.tmp`);
5135
+ const testFile = join23(dir, `.doctor-write-test-${process.pid}.tmp`);
6100
5136
  const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
6101
5137
  const start = Date.now();
6102
5138
  try {
@@ -6107,7 +5143,7 @@ function writeTest(dir) {
6107
5143
  const writeMs = Date.now() - start;
6108
5144
  const readStart = Date.now();
6109
5145
  try {
6110
- const back = readFile18(testFile, "utf8");
5146
+ const back = readFile15(testFile, "utf8");
6111
5147
  const readMs = Date.now() - readStart;
6112
5148
  if (back !== payload) {
6113
5149
  try {
@@ -6181,14 +5217,14 @@ function checkNodeVersion() {
6181
5217
  function detectCliChannels(argv, home) {
6182
5218
  const channels = [];
6183
5219
  if (argv.length >= 2 && argv[1].endsWith("cli.js")) {
6184
- const devPath = resolve6(argv[1]);
5220
+ const devPath = resolve5(argv[1]);
6185
5221
  channels.push({ name: "dev", path: devPath, isDevLink: true });
6186
5222
  }
6187
5223
  try {
6188
5224
  const whichOut = execSync2("which skillwiki 2>/dev/null", { encoding: "utf8" }).trim();
6189
5225
  if (whichOut) {
6190
5226
  const isDev = isDevSymlink(whichOut);
6191
- if (!channels.some((c) => c.path === resolve6(whichOut))) {
5227
+ if (!channels.some((c) => c.path === resolve5(whichOut))) {
6192
5228
  channels.push({ name: "npm", path: whichOut, isDevLink: isDev });
6193
5229
  }
6194
5230
  }
@@ -6196,22 +5232,22 @@ function detectCliChannels(argv, home) {
6196
5232
  }
6197
5233
  const plugin = findPlugin(home);
6198
5234
  if (plugin) {
6199
- const pluginBin = join26(plugin.installPath, "bin", "skillwiki");
6200
- if (existsSync14(pluginBin)) {
5235
+ const pluginBin = join24(plugin.installPath, "bin", "skillwiki");
5236
+ if (existsSync13(pluginBin)) {
6201
5237
  channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
6202
5238
  }
6203
5239
  }
6204
- const installBin = join26(home, ".claude", "skills", "bin", "skillwiki");
6205
- if (existsSync14(installBin)) {
5240
+ const installBin = join24(home, ".claude", "skills", "bin", "skillwiki");
5241
+ if (existsSync13(installBin)) {
6206
5242
  channels.push({ name: "install", path: installBin, isDevLink: false });
6207
5243
  }
6208
5244
  return channels;
6209
5245
  }
6210
5246
  function isDevSymlink(binPath) {
6211
5247
  try {
6212
- const st = lstatSync2(binPath);
5248
+ const st = lstatSync(binPath);
6213
5249
  if (st.isSymbolicLink()) {
6214
- const target = resolve6(binPath, "..", readlinkSync(binPath));
5250
+ const target = resolve5(binPath, "..", readlinkSync(binPath));
6215
5251
  return target.includes("packages/cli") || target.includes("packages\\cli");
6216
5252
  }
6217
5253
  } catch {
@@ -6263,7 +5299,7 @@ function isDevSourceRun(argv) {
6263
5299
  }
6264
5300
  async function checkConfigFile(home) {
6265
5301
  const cfgPath = configPath(home);
6266
- if (!existsSync14(cfgPath)) {
5302
+ if (!existsSync13(cfgPath)) {
6267
5303
  return check("warn", "config_file", "Config file exists", `${cfgPath} not found`);
6268
5304
  }
6269
5305
  try {
@@ -6278,7 +5314,7 @@ function checkWikiPathExists(resolvedPath) {
6278
5314
  if (resolvedPath === void 0) {
6279
5315
  return check("error", "wiki_path_exists", "Vault directory exists", "Cannot check \u2014 WIKI_PATH not resolved");
6280
5316
  }
6281
- if (existsSync14(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
5317
+ if (existsSync13(resolvedPath) && statSync2(resolvedPath).isDirectory()) {
6282
5318
  return check("pass", "wiki_path_exists", "Vault directory exists", resolvedPath);
6283
5319
  }
6284
5320
  return check("error", "wiki_path_exists", "Vault directory exists", `${resolvedPath} does not exist or is not a directory`);
@@ -6287,13 +5323,13 @@ function checkVaultStructure(resolvedPath) {
6287
5323
  if (resolvedPath === void 0) {
6288
5324
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 WIKI_PATH not resolved");
6289
5325
  }
6290
- if (!existsSync14(resolvedPath)) {
5326
+ if (!existsSync13(resolvedPath)) {
6291
5327
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
6292
5328
  }
6293
5329
  const missing = [];
6294
- if (!existsSync14(join26(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
5330
+ if (!existsSync13(join24(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
6295
5331
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
6296
- if (!existsSync14(join26(resolvedPath, dir))) missing.push(dir + "/");
5332
+ if (!existsSync13(join24(resolvedPath, dir))) missing.push(dir + "/");
6297
5333
  }
6298
5334
  if (missing.length === 0) {
6299
5335
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -6301,8 +5337,8 @@ function checkVaultStructure(resolvedPath) {
6301
5337
  return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
6302
5338
  }
6303
5339
  function checkSkillsInstalled(home, cwd) {
6304
- const srcDir = cwd ? join26(cwd, "packages", "skills") : void 0;
6305
- if (srcDir && existsSync14(srcDir)) {
5340
+ const srcDir = cwd ? join24(cwd, "packages", "skills") : void 0;
5341
+ if (srcDir && existsSync13(srcDir)) {
6306
5342
  const found = findInstalledSkillMd(srcDir);
6307
5343
  if (found.length > 0) {
6308
5344
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (source)`);
@@ -6315,8 +5351,8 @@ function checkSkillsInstalled(home, cwd) {
6315
5351
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
6316
5352
  }
6317
5353
  }
6318
- const skillsDir = join26(home, ".claude", "skills");
6319
- if (existsSync14(skillsDir)) {
5354
+ const skillsDir = join24(home, ".claude", "skills");
5355
+ if (existsSync13(skillsDir)) {
6320
5356
  const found = findInstalledSkillMd(skillsDir);
6321
5357
  if (found.length > 0) {
6322
5358
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (CLI install)`);
@@ -6326,10 +5362,10 @@ function checkSkillsInstalled(home, cwd) {
6326
5362
  }
6327
5363
  function checkDuplicateSkills(home) {
6328
5364
  const plugin = findPlugin(home);
6329
- const skillsDir = join26(home, ".claude", "skills");
5365
+ const skillsDir = join24(home, ".claude", "skills");
6330
5366
  const agentSkillDirs = [
6331
- { label: "~/.codex/skills/", path: join26(home, ".codex", "skills") },
6332
- { label: "~/.agents/skills/", path: join26(home, ".agents", "skills") }
5367
+ { label: "~/.codex/skills/", path: join24(home, ".codex", "skills") },
5368
+ { label: "~/.agents/skills/", path: join24(home, ".agents", "skills") }
6333
5369
  ];
6334
5370
  if (!plugin) {
6335
5371
  return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
@@ -6432,8 +5468,8 @@ async function checkProfiles(home) {
6432
5468
  }
6433
5469
  async function checkProjectLocalOverride(cwd) {
6434
5470
  const dir = cwd ?? process.cwd();
6435
- const envPath = join26(dir, ".skillwiki", ".env");
6436
- if (existsSync14(envPath)) {
5471
+ const envPath = join24(dir, ".skillwiki", ".env");
5472
+ if (existsSync13(envPath)) {
6437
5473
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
6438
5474
  }
6439
5475
  return check("pass", "project_local", "Project-local config", "None");
@@ -6442,7 +5478,7 @@ function checkVaultGitRemote(resolvedPath) {
6442
5478
  if (resolvedPath === void 0) {
6443
5479
  return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
6444
5480
  }
6445
- if (!existsSync14(join26(resolvedPath, ".git"))) {
5481
+ if (!existsSync13(join24(resolvedPath, ".git"))) {
6446
5482
  return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
6447
5483
  }
6448
5484
  try {
@@ -6465,9 +5501,9 @@ function checkObsidianTemplates(resolvedPath) {
6465
5501
  return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
6466
5502
  }
6467
5503
  const missing = [];
6468
- if (!existsSync14(join26(resolvedPath, "_Templates"))) missing.push("_Templates/");
6469
- if (!existsSync14(join26(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
6470
- if (!existsSync14(join26(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
5504
+ if (!existsSync13(join24(resolvedPath, "_Templates"))) missing.push("_Templates/");
5505
+ if (!existsSync13(join24(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
5506
+ if (!existsSync13(join24(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
6471
5507
  if (missing.length === 0) {
6472
5508
  return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
6473
5509
  }
@@ -6477,12 +5513,12 @@ function checkDotStoreClean(resolvedPath) {
6477
5513
  if (resolvedPath === void 0) {
6478
5514
  return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
6479
5515
  }
6480
- const rawDir = join26(resolvedPath, "raw");
6481
- if (!existsSync14(rawDir)) {
5516
+ const rawDir = join24(resolvedPath, "raw");
5517
+ if (!existsSync13(rawDir)) {
6482
5518
  return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
6483
5519
  }
6484
5520
  const found = [];
6485
- (function walk2(dir, rel) {
5521
+ (function walk(dir, rel) {
6486
5522
  let entries;
6487
5523
  try {
6488
5524
  entries = readdirSync3(dir, { withFileTypes: true });
@@ -6493,7 +5529,7 @@ function checkDotStoreClean(resolvedPath) {
6493
5529
  if (entry.name === ".DS_Store") {
6494
5530
  found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
6495
5531
  } else if (entry.isDirectory()) {
6496
- walk2(join26(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
5532
+ walk(join24(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
6497
5533
  }
6498
5534
  }
6499
5535
  })(rawDir, "");
@@ -6524,7 +5560,7 @@ function checkSyncLastPush(resolvedPath) {
6524
5560
  if (resolvedPath === void 0) {
6525
5561
  return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
6526
5562
  }
6527
- if (!existsSync14(join26(resolvedPath, ".git"))) {
5563
+ if (!existsSync13(join24(resolvedPath, ".git"))) {
6528
5564
  return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
6529
5565
  }
6530
5566
  let timestamp;
@@ -6572,7 +5608,7 @@ function checkVaultGitDirty(resolvedPath) {
6572
5608
  if (resolvedPath === void 0) {
6573
5609
  return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
6574
5610
  }
6575
- if (!existsSync14(join26(resolvedPath, ".git"))) {
5611
+ if (!existsSync13(join24(resolvedPath, ".git"))) {
6576
5612
  return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
6577
5613
  }
6578
5614
  try {
@@ -6640,7 +5676,7 @@ function remoteMainHash(resolvedPath) {
6640
5676
  }
6641
5677
  function checkStaleRemoteMain(resolvedPath) {
6642
5678
  if (resolvedPath === void 0) return void 0;
6643
- if (!existsSync14(join26(resolvedPath, ".git"))) return void 0;
5679
+ if (!existsSync13(join24(resolvedPath, ".git"))) return void 0;
6644
5680
  const localOrigin = gitRefHash(resolvedPath, "origin/main");
6645
5681
  if (!localOrigin) return void 0;
6646
5682
  const remoteMain = remoteMainHash(resolvedPath);
@@ -6656,7 +5692,7 @@ function checkVaultLocalGit(resolvedPath) {
6656
5692
  if (resolvedPath === void 0) {
6657
5693
  return check("warn", "vault_local_git", "Vault local git", "Cannot check \u2014 WIKI_PATH not resolved");
6658
5694
  }
6659
- if (!existsSync14(join26(resolvedPath, ".git"))) {
5695
+ if (!existsSync13(join24(resolvedPath, ".git"))) {
6660
5696
  return check("warn", "vault_local_git", "Vault local git", "Not a git repository - sync features unavailable");
6661
5697
  }
6662
5698
  try {
@@ -6675,7 +5711,7 @@ function checkVaultGithubRemote(resolvedPath, exec) {
6675
5711
  if (resolvedPath === void 0) {
6676
5712
  return check("pass", "vault_github_remote", "Vault GitHub remote", "No vault path \u2014 check skipped");
6677
5713
  }
6678
- if (!existsSync14(join26(resolvedPath, ".git"))) {
5714
+ if (!existsSync13(join24(resolvedPath, ".git"))) {
6679
5715
  return check("pass", "vault_github_remote", "Vault GitHub remote", "No git repo \u2014 check skipped");
6680
5716
  }
6681
5717
  const state = probeGithubReachability(resolvedPath, exec);
@@ -6719,7 +5755,7 @@ function checkVaultPromotionLag(resolvedPath) {
6719
5755
  if (resolvedPath === void 0) {
6720
5756
  return check("pass", "vault_promotion_lag", "Vault promotion lag", "No vault path \u2014 check skipped");
6721
5757
  }
6722
- if (!existsSync14(join26(resolvedPath, ".git"))) {
5758
+ if (!existsSync13(join24(resolvedPath, ".git"))) {
6723
5759
  return check("pass", "vault_promotion_lag", "Vault promotion lag", "No git repo \u2014 check skipped");
6724
5760
  }
6725
5761
  try {
@@ -6746,7 +5782,7 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
6746
5782
  if (resolvedPath === void 0) {
6747
5783
  return check("pass", id, label, "No vault path \u2014 check skipped");
6748
5784
  }
6749
- if (!existsSync14(join26(resolvedPath, ".git"))) {
5785
+ if (!existsSync13(join24(resolvedPath, ".git"))) {
6750
5786
  return check("pass", id, label, "No git repo \u2014 check skipped");
6751
5787
  }
6752
5788
  if (!hasOriginMain(resolvedPath)) {
@@ -6774,7 +5810,7 @@ function checkSatelliteLastRun(vaultPath, satelliteExpected) {
6774
5810
  return check("pass", "satellite_job_last_run", "Satellite job last run", "No vault path \u2014 check skipped");
6775
5811
  }
6776
5812
  const latestPath = satelliteLatestRunPath(vaultPath);
6777
- if (!existsSync14(latestPath)) {
5813
+ if (!existsSync13(latestPath)) {
6778
5814
  return check("pass", "satellite_job_last_run", "Satellite job last run", "No latest-run.json \u2014 satellite has not run yet");
6779
5815
  }
6780
5816
  try {
@@ -6862,11 +5898,11 @@ async function checkFleetIdentity(input) {
6862
5898
  }
6863
5899
  function pullLogPaths(home) {
6864
5900
  const paths = platform2() === "darwin" ? [
6865
- join26(home, "Library", "Logs", "wiki-pull.log"),
6866
- join26(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
5901
+ join24(home, "Library", "Logs", "wiki-pull.log"),
5902
+ join24(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
6867
5903
  ] : [
6868
- join26(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
6869
- join26(home, "Library", "Logs", "wiki-pull.log")
5904
+ join24(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
5905
+ join24(home, "Library", "Logs", "wiki-pull.log")
6870
5906
  ];
6871
5907
  return [...new Set(paths)];
6872
5908
  }
@@ -6878,12 +5914,12 @@ function isRecentLogLine(line, nowMs) {
6878
5914
  return nowMs - ts <= 24 * 60 * 60 * 1e3;
6879
5915
  }
6880
5916
  function checkVaultGitPullFailures(home) {
6881
- const path = pullLogPaths(home).find((p) => existsSync14(p));
5917
+ const path = pullLogPaths(home).find((p) => existsSync13(p));
6882
5918
  if (!path) {
6883
5919
  return check("pass", "vault_git_pull_failures", "Vault pull failures", "No wiki-pull.log found \u2014 check skipped");
6884
5920
  }
6885
5921
  try {
6886
- const lines = readFileSync11(path, "utf8").split(/\r?\n/).filter(Boolean);
5922
+ const lines = readFileSync10(path, "utf8").split(/\r?\n/).filter(Boolean);
6887
5923
  const now = Date.now();
6888
5924
  const failures = lines.filter(
6889
5925
  (line) => isRecentLogLine(line, now) && /(pre-push pull failed|FAIL .*pull|FAIL .*rebase|cannot pull with rebase|unstaged changes)/i.test(line)
@@ -6906,8 +5942,8 @@ function checkS3MountPerf(resolvedPath) {
6906
5942
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
6907
5943
  }
6908
5944
  const mountPoint = fuse.mountPoint;
6909
- const conceptsDir = join26(resolvedPath, "concepts");
6910
- if (!existsSync14(conceptsDir)) {
5945
+ const conceptsDir = join24(resolvedPath, "concepts");
5946
+ if (!existsSync13(conceptsDir)) {
6911
5947
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
6912
5948
  }
6913
5949
  const start = Date.now();
@@ -7089,8 +6125,8 @@ function checkWriteTest(resolvedPath) {
7089
6125
  if (!fuse) {
7090
6126
  return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
7091
6127
  }
7092
- const conceptsDir = join26(resolvedPath, "concepts");
7093
- if (!existsSync14(conceptsDir)) {
6128
+ const conceptsDir = join24(resolvedPath, "concepts");
6129
+ if (!existsSync13(conceptsDir)) {
7094
6130
  return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
7095
6131
  }
7096
6132
  const result = writeTest(conceptsDir);
@@ -7176,7 +6212,7 @@ function checkVfsCacheHealth(resolvedPath) {
7176
6212
  }
7177
6213
  function readVaultSyncConfig(home) {
7178
6214
  try {
7179
- const content = readFileSync11(join26(home, ".skillwiki", ".env"), "utf8");
6215
+ const content = readFileSync10(join24(home, ".skillwiki", ".env"), "utf8");
7180
6216
  let installed = false;
7181
6217
  let role;
7182
6218
  let serviceScope;
@@ -7205,7 +6241,7 @@ function readVaultSyncConfig(home) {
7205
6241
  }
7206
6242
  function readKeyFromEnvFile(path, keys) {
7207
6243
  try {
7208
- const content = readFileSync11(path, "utf8");
6244
+ const content = readFileSync10(path, "utf8");
7209
6245
  for (const line of content.split(/\r?\n/)) {
7210
6246
  const trimmed = line.trim();
7211
6247
  if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
@@ -7227,7 +6263,7 @@ function resolveSnapshotGitWorktree(config) {
7227
6263
  if (fromProfile) return fromProfile;
7228
6264
  }
7229
6265
  const defaultPath = "/root/wiki-git";
7230
- return existsSync14(defaultPath) ? defaultPath : void 0;
6266
+ return existsSync13(defaultPath) ? defaultPath : void 0;
7231
6267
  }
7232
6268
  function vaultSyncChecks(input) {
7233
6269
  const os = input.os ?? platform2();
@@ -7244,16 +6280,16 @@ function vaultSyncChecks(input) {
7244
6280
  ];
7245
6281
  }
7246
6282
  const isMac = os === "darwin";
7247
- const logDir = input.logDir ?? (isMac ? join26(home, "Library", "Logs") : join26(home, ".local", "state", "vault-sync", "log"));
7248
- const shareDir = input.shareDir ?? (isMac ? join26(home, "Library", "Application Support", "vault-sync", "bin") : join26(home, ".local", "share", "vault-sync", "bin"));
7249
- const filterPath = input.filterPath ?? join26(home, ".config", "rclone", "wiki-push-filters.txt");
7250
- const packagedSnapshotPath = join26(shareDir, "wiki-snapshot.sh");
6283
+ const logDir = input.logDir ?? (isMac ? join24(home, "Library", "Logs") : join24(home, ".local", "state", "vault-sync", "log"));
6284
+ const shareDir = input.shareDir ?? (isMac ? join24(home, "Library", "Application Support", "vault-sync", "bin") : join24(home, ".local", "share", "vault-sync", "bin"));
6285
+ const filterPath = input.filterPath ?? join24(home, ".config", "rclone", "wiki-push-filters.txt");
6286
+ const packagedSnapshotPath = join24(shareDir, "wiki-snapshot.sh");
7251
6287
  const legacySnapshotPath = "/root/.hermes/scripts/wiki-snapshot-v3.sh";
7252
- const snapshotPath = input.snapshotScriptPath ?? (existsSync14(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
6288
+ const snapshotPath = input.snapshotScriptPath ?? (existsSync13(packagedSnapshotPath) ? packagedSnapshotPath : legacySnapshotPath);
7253
6289
  function snapshotLastStatusCheck() {
7254
- const snapshotLog = join26(logDir, "wiki-snapshot.log");
6290
+ const snapshotLog = join24(logDir, "wiki-snapshot.log");
7255
6291
  try {
7256
- const logContent = readFileSync11(snapshotLog, "utf8");
6292
+ const logContent = readFileSync10(snapshotLog, "utf8");
7257
6293
  const lines = logContent.trim().split("\n").filter(Boolean);
7258
6294
  if (lines.length === 0) {
7259
6295
  return check(
@@ -7298,14 +6334,14 @@ function vaultSyncChecks(input) {
7298
6334
  }
7299
6335
  }
7300
6336
  if (input.vaultSyncRole === "snapshotter") {
7301
- const c12 = existsSync14(snapshotPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found snapshot script: ${snapshotPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Snapshot script not found at ${snapshotPath}`);
6337
+ const c12 = existsSync13(snapshotPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found snapshot script: ${snapshotPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Snapshot script not found at ${snapshotPath}`);
7302
6338
  const serviceScope = input.vaultSyncServiceScope ?? "user";
7303
- const userTimerPath = join26(home, ".config", "systemd", "user", "wiki-snapshot.timer");
6339
+ const userTimerPath = join24(home, ".config", "systemd", "user", "wiki-snapshot.timer");
7304
6340
  const systemTimerPath = "/etc/systemd/system/wiki-snapshot.timer";
7305
6341
  let c22;
7306
- if (serviceScope === "user" && existsSync14(userTimerPath)) {
6342
+ if (serviceScope === "user" && existsSync13(userTimerPath)) {
7307
6343
  c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${userTimerPath}`);
7308
- } else if (serviceScope === "system" && existsSync14(systemTimerPath)) {
6344
+ } else if (serviceScope === "system" && existsSync13(systemTimerPath)) {
7309
6345
  c22 = check("pass", "vault_sync_jobs_enabled", "Vault sync jobs enabled", `Found: ${systemTimerPath}`);
7310
6346
  } else if (os !== "linux") {
7311
6347
  c22 = check("warn", "vault_sync_jobs_enabled", "Vault sync jobs enabled", "Snapshotter scheduler is Linux-only and no wiki-snapshot.timer file was found");
@@ -7337,7 +6373,7 @@ function vaultSyncChecks(input) {
7337
6373
  );
7338
6374
  let c52;
7339
6375
  try {
7340
- if (!existsSync14(snapshotPath)) {
6376
+ if (!existsSync13(snapshotPath)) {
7341
6377
  c52 = check(
7342
6378
  "error",
7343
6379
  "vault_sync_snapshot_guard",
@@ -7345,7 +6381,7 @@ function vaultSyncChecks(input) {
7345
6381
  `Snapshot script not found at ${snapshotPath}`
7346
6382
  );
7347
6383
  } else {
7348
- const content = readFileSync11(snapshotPath, "utf8");
6384
+ const content = readFileSync10(snapshotPath, "utf8");
7349
6385
  if (!content.includes("--max-delete")) {
7350
6386
  c52 = check(
7351
6387
  "error",
@@ -7372,8 +6408,8 @@ function vaultSyncChecks(input) {
7372
6408
  }
7373
6409
  return [c12, c22, c32, cFetch2, c42, c52];
7374
6410
  }
7375
- const pushScriptPath = join26(shareDir, "wiki-push.sh");
7376
- const c1 = existsSync14(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
6411
+ const pushScriptPath = join24(shareDir, "wiki-push.sh");
6412
+ const c1 = existsSync13(pushScriptPath) ? check("pass", "vault_sync_installed", "Vault sync installed", `Found: ${pushScriptPath}`) : check("error", "vault_sync_installed", "Vault sync installed", `Script not found at ${pushScriptPath} \u2014 run vault-sync-install`);
7377
6413
  let c2;
7378
6414
  try {
7379
6415
  if (isMac) {
@@ -7424,10 +6460,10 @@ function vaultSyncChecks(input) {
7424
6460
  "Scheduler check failed \u2014 run vault-sync-install"
7425
6461
  );
7426
6462
  }
7427
- const logFile = join26(logDir, "wiki-push.log");
6463
+ const logFile = join24(logDir, "wiki-push.log");
7428
6464
  let c3;
7429
6465
  try {
7430
- const logContent = readFileSync11(logFile, "utf8");
6466
+ const logContent = readFileSync10(logFile, "utf8");
7431
6467
  const lines = logContent.trim().split("\n").filter(Boolean);
7432
6468
  if (lines.length === 0) {
7433
6469
  c3 = check(
@@ -7483,7 +6519,7 @@ function vaultSyncChecks(input) {
7483
6519
  }
7484
6520
  }
7485
6521
  } catch {
7486
- c3 = existsSync14(logDir) ? check(
6522
+ c3 = existsSync13(logDir) ? check(
7487
6523
  "warn",
7488
6524
  "vault_sync_last_push_age",
7489
6525
  "Vault sync last push recency",
@@ -7495,10 +6531,10 @@ function vaultSyncChecks(input) {
7495
6531
  `Log directory not found at ${logDir}`
7496
6532
  );
7497
6533
  }
7498
- const fetchLogFile = join26(logDir, "wiki-fetch.log");
6534
+ const fetchLogFile = join24(logDir, "wiki-fetch.log");
7499
6535
  let cFetch;
7500
6536
  try {
7501
- const logContent = readFileSync11(fetchLogFile, "utf8");
6537
+ const logContent = readFileSync10(fetchLogFile, "utf8");
7502
6538
  const lines = logContent.trim().split("\n").filter(Boolean);
7503
6539
  if (lines.length === 0) {
7504
6540
  cFetch = check(
@@ -7542,7 +6578,7 @@ function vaultSyncChecks(input) {
7542
6578
  }
7543
6579
  let c4;
7544
6580
  try {
7545
- if (!existsSync14(filterPath)) {
6581
+ if (!existsSync13(filterPath)) {
7546
6582
  c4 = check(
7547
6583
  "error",
7548
6584
  "vault_sync_filter_present",
@@ -7550,7 +6586,7 @@ function vaultSyncChecks(input) {
7550
6586
  `Filter file not found at ${filterPath}`
7551
6587
  );
7552
6588
  } else {
7553
- const content = readFileSync11(filterPath, "utf8");
6589
+ const content = readFileSync10(filterPath, "utf8");
7554
6590
  const requiredExcludes = [
7555
6591
  "remotely-save/data.json",
7556
6592
  ".skillwiki/sync.lock",
@@ -7593,7 +6629,7 @@ function vaultSyncChecks(input) {
7593
6629
  );
7594
6630
  } else {
7595
6631
  try {
7596
- if (!existsSync14(snapshotPath)) {
6632
+ if (!existsSync13(snapshotPath)) {
7597
6633
  c5 = check(
7598
6634
  "error",
7599
6635
  "vault_sync_snapshot_guard",
@@ -7601,7 +6637,7 @@ function vaultSyncChecks(input) {
7601
6637
  `Snapshot script not found at ${snapshotPath}`
7602
6638
  );
7603
6639
  } else {
7604
- const content = readFileSync11(snapshotPath, "utf8");
6640
+ const content = readFileSync10(snapshotPath, "utf8");
7605
6641
  if (!content.includes("--max-delete")) {
7606
6642
  c5 = check(
7607
6643
  "error",
@@ -7639,15 +6675,15 @@ function findSkillMd(dir) {
7639
6675
  }
7640
6676
  for (const entry of entries) {
7641
6677
  if (entry.isFile() && entry.name === "SKILL.md") {
7642
- results.push(join26(dir, entry.name));
6678
+ results.push(join24(dir, entry.name));
7643
6679
  } else if (entry.isDirectory()) {
7644
- results.push(...findSkillMd(join26(dir, entry.name)));
6680
+ results.push(...findSkillMd(join24(dir, entry.name)));
7645
6681
  }
7646
6682
  }
7647
6683
  return results;
7648
6684
  }
7649
6685
  function findInstalledSkillMd(dir) {
7650
- const directSkills = findSkillNames(dir).map((name) => join26(dir, name, "SKILL.md"));
6686
+ const directSkills = findSkillNames(dir).map((name) => join24(dir, name, "SKILL.md"));
7651
6687
  return directSkills.length > 0 ? directSkills : findSkillMd(dir);
7652
6688
  }
7653
6689
  function findSkillNames(dir) {
@@ -7659,7 +6695,7 @@ function findSkillNames(dir) {
7659
6695
  return results;
7660
6696
  }
7661
6697
  for (const entry of entries) {
7662
- if (entry.isDirectory() && existsSync14(join26(dir, entry.name, "SKILL.md"))) {
6698
+ if (entry.isDirectory() && existsSync13(join24(dir, entry.name, "SKILL.md"))) {
7663
6699
  results.push(entry.name);
7664
6700
  }
7665
6701
  }
@@ -7703,7 +6739,7 @@ async function vaultMetrics(resolvedPath) {
7703
6739
  }
7704
6740
  let logLines = 0;
7705
6741
  try {
7706
- logLines = readFileSync11(join26(resolvedPath, "log.md"), "utf8").split("\n").length;
6742
+ logLines = readFileSync10(join24(resolvedPath, "log.md"), "utf8").split("\n").length;
7707
6743
  } catch {
7708
6744
  }
7709
6745
  return [
@@ -7811,7 +6847,7 @@ async function runDoctor(input) {
7811
6847
  }
7812
6848
 
7813
6849
  // src/utils/package-info.ts
7814
- import { readFileSync as readFileSync12 } from "fs";
6850
+ import { readFileSync as readFileSync11 } from "fs";
7815
6851
  function packageJsonCandidateUrls(baseUrl = import.meta.url) {
7816
6852
  return [
7817
6853
  new URL("../package.json", baseUrl),
@@ -7821,7 +6857,7 @@ function packageJsonCandidateUrls(baseUrl = import.meta.url) {
7821
6857
  function readCliPackageJson(baseUrl = import.meta.url) {
7822
6858
  for (const url of packageJsonCandidateUrls(baseUrl)) {
7823
6859
  try {
7824
- const pkg = JSON.parse(readFileSync12(url, "utf8"));
6860
+ const pkg = JSON.parse(readFileSync11(url, "utf8"));
7825
6861
  if (typeof pkg.version === "string") {
7826
6862
  return { ...pkg, version: pkg.version };
7827
6863
  }
@@ -7831,214 +6867,11 @@ function readCliPackageJson(baseUrl = import.meta.url) {
7831
6867
  throw new Error(`Could not locate skillwiki package.json from ${baseUrl}`);
7832
6868
  }
7833
6869
 
7834
- // src/commands/project-index.ts
7835
- import { readdir as readdir4, readFile as readFile19, writeFile as writeFile5, mkdir as mkdir5 } from "fs/promises";
7836
- import { join as join27, dirname as dirname8, basename as basename2 } from "path";
7837
- var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
7838
- var PROJECT_LOCAL_DIRS = ["requirements", "work", "architecture", "history"];
7839
- async function scanMarkdownTree(rootAbs, rootRel) {
7840
- const found = [];
7841
- let entries;
7842
- try {
7843
- entries = await readdir4(rootAbs, { withFileTypes: true });
7844
- } catch {
7845
- return found;
7846
- }
7847
- for (const entry of entries) {
7848
- const abs = join27(rootAbs, entry.name);
7849
- const rel = `${rootRel}/${entry.name}`;
7850
- if (entry.isDirectory()) {
7851
- found.push(...await scanMarkdownTree(abs, rel));
7852
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
7853
- found.push(rel);
7854
- }
7855
- }
7856
- return found;
7857
- }
7858
- function projectLocalType(slug, page, data) {
7859
- if (page.startsWith(`projects/${slug}/requirements/`)) return "requirement";
7860
- if (page.startsWith(`projects/${slug}/work/`)) {
7861
- if (typeof data.kind === "string") return data.kind;
7862
- const name = basename2(page, ".md");
7863
- if (name === "spec" || name === "plan" || name === "retro") return name;
7864
- return "work";
7865
- }
7866
- if (page.startsWith(`projects/${slug}/architecture/`)) {
7867
- return typeof data.type === "string" ? data.type : "architecture";
7868
- }
7869
- if (page.startsWith(`projects/${slug}/history/`)) {
7870
- if (typeof data.kind === "string") return data.kind;
7871
- if (typeof data.type === "string") return data.type;
7872
- return "history";
7873
- }
7874
- return typeof data.type === "string" ? data.type : "project";
7875
- }
7876
- async function runProjectIndex(input) {
7877
- const slug = input.slug;
7878
- const projectDir = join27(input.vault, "projects", slug);
7879
- try {
7880
- await readdir4(projectDir);
7881
- } catch {
7882
- return {
7883
- exitCode: ExitCode.PROJECT_NOT_FOUND,
7884
- result: err("PROJECT_NOT_FOUND", { slug, path: projectDir })
7885
- };
7886
- }
7887
- const wikilinkPattern = `[[${slug}]]`;
7888
- const entries = [];
7889
- const compoundDir = join27(input.vault, "projects", slug, "compound");
7890
- try {
7891
- const compoundFiles = await readdir4(compoundDir, { withFileTypes: true });
7892
- for (const entry of compoundFiles) {
7893
- if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
7894
- const filePath = join27(compoundDir, entry.name);
7895
- let text;
7896
- try {
7897
- text = await readFile19(filePath, "utf8");
7898
- } catch {
7899
- continue;
7900
- }
7901
- const fm = extractFrontmatter(text);
7902
- if (!fm.ok) continue;
7903
- entries.push({
7904
- page: `projects/${slug}/compound/${entry.name}`,
7905
- type: typeof fm.data.type === "string" ? fm.data.type : "compound",
7906
- title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
7907
- });
7908
- }
7909
- } catch {
7910
- }
7911
- for (const dir of LAYER2_DIRS) {
7912
- let files;
7913
- try {
7914
- files = await readdir4(join27(input.vault, dir), { withFileTypes: true });
7915
- } catch {
7916
- continue;
7917
- }
7918
- for (const entry of files) {
7919
- if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
7920
- const filePath = join27(input.vault, dir, entry.name);
7921
- let text;
7922
- try {
7923
- text = await readFile19(filePath, "utf8");
7924
- } catch {
7925
- continue;
7926
- }
7927
- const fm = extractFrontmatter(text);
7928
- if (!fm.ok) continue;
7929
- const pp = fm.data.provenance_projects;
7930
- if (!Array.isArray(pp) || !pp.some((p) => String(p) === wikilinkPattern)) continue;
7931
- entries.push({
7932
- page: `${dir}/${entry.name}`,
7933
- type: typeof fm.data.type === "string" ? fm.data.type : dir.slice(0, -1),
7934
- title: typeof fm.data.title === "string" ? fm.data.title : entry.name.replace(/\.md$/, "")
7935
- });
7936
- }
7937
- }
7938
- for (const dir of PROJECT_LOCAL_DIRS) {
7939
- const rootAbs = join27(projectDir, dir);
7940
- const rootRel = `projects/${slug}/${dir}`;
7941
- const pages = await scanMarkdownTree(rootAbs, rootRel);
7942
- for (const page of pages) {
7943
- const filePath = join27(input.vault, page);
7944
- let text;
7945
- try {
7946
- text = await readFile19(filePath, "utf8");
7947
- } catch {
7948
- continue;
7949
- }
7950
- const fm = extractFrontmatter(text);
7951
- if (!fm.ok) continue;
7952
- entries.push({
7953
- page,
7954
- type: projectLocalType(slug, page, fm.data),
7955
- title: typeof fm.data.title === "string" ? fm.data.title : basename2(page, ".md")
7956
- });
7957
- }
7958
- }
7959
- const typeOrder = { entity: 0, concept: 1, comparison: 2, query: 3, summary: 4, meta: 5, requirement: 6, spec: 7, plan: 8, retro: 9, architecture: 10, pattern: 11, gotcha: 12, lesson: 13, antipattern: 14, compound: 15, work: 16, history: 17 };
7960
- entries.sort((a, b) => {
7961
- const ta = typeOrder[a.type] ?? 99;
7962
- const tb = typeOrder[b.type] ?? 99;
7963
- return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
7964
- });
7965
- const indexPath = join27(projectDir, "knowledge.md");
7966
- let existing = false;
7967
- let stale = false;
7968
- try {
7969
- const existingText = await readFile19(indexPath, "utf8");
7970
- existing = true;
7971
- const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
7972
- const existingPages = new Set(existingEntries.map((l) => {
7973
- const m = l.match(/\[\[([^\]]+)\]\]/);
7974
- return m ? m[1] : "";
7975
- }));
7976
- const currentPages = new Set(entries.map((e) => e.page.replace(/\.md$/, "")));
7977
- stale = existingPages.size !== currentPages.size || [...currentPages].some((p) => !existingPages.has(p));
7978
- } catch {
7979
- }
7980
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7981
- const grouped = /* @__PURE__ */ new Map();
7982
- for (const e of entries) {
7983
- const group = e.type;
7984
- if (!grouped.has(group)) grouped.set(group, []);
7985
- grouped.get(group).push(e);
7986
- }
7987
- let body = `# Knowledge Index: ${slug}
7988
-
7989
- Autogenerated by \`skillwiki project-index\` on ${today}.
7990
-
7991
- `;
7992
- for (const [type, items] of grouped) {
7993
- body += `## ${type}
7994
-
7995
- `;
7996
- for (const item of items) {
7997
- const pageRef = item.page.replace(/\.md$/, "");
7998
- body += `- [[${pageRef}]] \u2014 ${item.title}
7999
- `;
8000
- }
8001
- body += "\n";
8002
- }
8003
- if (entries.length === 0) {
8004
- body += `No Layer 2 pages reference \`[[${slug}]]\` in provenance_projects.
8005
- `;
8006
- }
8007
- if (input.apply) {
8008
- try {
8009
- await mkdir5(dirname8(indexPath), { recursive: true });
8010
- await writeFile5(indexPath, body, "utf8");
8011
- } catch (e) {
8012
- return {
8013
- exitCode: ExitCode.WRITE_FAILED,
8014
- result: err("WRITE_FAILED", { file: indexPath, message: String(e) })
8015
- };
8016
- }
8017
- }
8018
- const action = input.apply ? `written ${entries.length} entries to ${indexPath}` : `${entries.length} entries found (use --apply to write)`;
8019
- const staleHint = stale ? " (STALE \u2014 existing index outdated)" : existing ? " (up to date)" : "";
8020
- return {
8021
- exitCode: ExitCode.OK,
8022
- result: ok({
8023
- slug,
8024
- entries,
8025
- existing,
8026
- stale,
8027
- index_path: `projects/${slug}/knowledge.md`,
8028
- humanHint: `project: ${slug}
8029
- entries: ${entries.length}${staleHint}
8030
- ${action}
8031
-
8032
- ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e.title}`).join("\n")}`
8033
- })
8034
- };
8035
- }
8036
-
8037
6870
  // src/commands/observe.ts
8038
- import { mkdir as mkdir6, writeFile as writeFile6 } from "fs/promises";
8039
- import { existsSync as existsSync15, statSync as statSync3 } from "fs";
8040
- import { join as join28 } from "path";
8041
- import { createHash as createHash7 } from "crypto";
6871
+ import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
6872
+ import { existsSync as existsSync14, statSync as statSync3 } from "fs";
6873
+ import { join as join25 } from "path";
6874
+ import { createHash as createHash6 } from "crypto";
8042
6875
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
8043
6876
  function slugify(text) {
8044
6877
  const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -8060,15 +6893,15 @@ async function runObserve(input) {
8060
6893
  result: err("SCHEME_REJECTED", { message: "Text must not be empty" })
8061
6894
  };
8062
6895
  }
8063
- if (!existsSync15(input.vault) || !statSync3(input.vault).isDirectory()) {
6896
+ if (!existsSync14(input.vault) || !statSync3(input.vault).isDirectory()) {
8064
6897
  return {
8065
6898
  exitCode: ExitCode.VAULT_PATH_INVALID,
8066
6899
  result: err("VAULT_PATH_INVALID", { path: input.vault })
8067
6900
  };
8068
6901
  }
8069
- const transcriptsDir = join28(input.vault, "raw", "transcripts");
6902
+ const transcriptsDir = join25(input.vault, "raw", "transcripts");
8070
6903
  try {
8071
- await mkdir6(transcriptsDir, { recursive: true });
6904
+ await mkdir5(transcriptsDir, { recursive: true });
8072
6905
  } catch {
8073
6906
  return {
8074
6907
  exitCode: ExitCode.VAULT_PATH_INVALID,
@@ -8078,11 +6911,11 @@ async function runObserve(input) {
8078
6911
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
8079
6912
  const slug = slugify(input.text);
8080
6913
  const fileName = `${today}-observation-${slug}.md`;
8081
- const filePath = join28(transcriptsDir, fileName);
6914
+ const filePath = join25(transcriptsDir, fileName);
8082
6915
  const body = `
8083
6916
  ${input.text.trim()}
8084
6917
  `;
8085
- const sha256 = createHash7("sha256").update(Buffer.from(body, "utf8")).digest("hex");
6918
+ const sha256 = createHash6("sha256").update(Buffer.from(body, "utf8")).digest("hex");
8086
6919
  const frontmatterLines = [
8087
6920
  "---",
8088
6921
  "source_url:",
@@ -8096,7 +6929,7 @@ ${input.text.trim()}
8096
6929
  frontmatterLines.push("---");
8097
6930
  const content = frontmatterLines.join("\n") + body;
8098
6931
  try {
8099
- await writeFile6(filePath, content, "utf8");
6932
+ await writeFile4(filePath, content, "utf8");
8100
6933
  } catch (e) {
8101
6934
  return {
8102
6935
  exitCode: ExitCode.WRITE_FAILED,
@@ -8118,9 +6951,9 @@ ${input.text.trim()}
8118
6951
  }
8119
6952
 
8120
6953
  // src/commands/memory.ts
8121
- import { createHash as createHash8 } from "crypto";
8122
- import { mkdir as mkdir7, readFile as readFile20, readdir as readdir5, stat as stat6, writeFile as writeFile7 } from "fs/promises";
8123
- import { basename as basename3, extname, join as join29, relative as relative5, sep as sep5 } from "path";
6954
+ import { createHash as createHash7 } from "crypto";
6955
+ import { mkdir as mkdir6, readFile as readFile16, readdir as readdir4, stat as stat5, writeFile as writeFile5 } from "fs/promises";
6956
+ import { basename as basename2, extname, join as join26, relative as relative3, sep as sep3 } from "path";
8124
6957
  async function runMemoryTopics(input) {
8125
6958
  const scan = await scanVault(input.vault);
8126
6959
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -8184,9 +7017,9 @@ async function runMemoryIndex(input) {
8184
7017
  }
8185
7018
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
8186
7019
  const relCachePath = memoryCacheRelPath(input.project);
8187
- const absCachePath = join29(input.vault, relCachePath);
8188
- await mkdir7(join29(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
8189
- await writeFile7(absCachePath, `${JSON.stringify({
7020
+ const absCachePath = join26(input.vault, relCachePath);
7021
+ await mkdir6(join26(input.vault, ".skillwiki", "memory", input.project), { recursive: true });
7022
+ await writeFile5(absCachePath, `${JSON.stringify({
8190
7023
  generated_at: generatedAt,
8191
7024
  project: input.project,
8192
7025
  topics: state.topics,
@@ -8377,7 +7210,7 @@ async function buildMemoryIndexState(pages, project) {
8377
7210
  }
8378
7211
  async function checkMemoryIndex(vault, project, current) {
8379
7212
  const relCachePath = memoryCacheRelPath(project);
8380
- const cacheText = await readIfExists2(join29(vault, relCachePath));
7213
+ const cacheText = await readIfExists2(join26(vault, relCachePath));
8381
7214
  if (!cacheText) {
8382
7215
  return {
8383
7216
  ok: true,
@@ -8494,7 +7327,7 @@ async function readMemoryPage(page, project, warnings) {
8494
7327
  title,
8495
7328
  summary: summarize(body),
8496
7329
  updated,
8497
- hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
7330
+ hash: createHash7("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
8498
7331
  topics,
8499
7332
  project,
8500
7333
  ...stringField(fm.data.memory_kind) ? { memory_kind: stringField(fm.data.memory_kind) } : {},
@@ -8770,23 +7603,23 @@ function renderMemoryIndexStatusHint(status) {
8770
7603
  }
8771
7604
  async function readIfExists2(path) {
8772
7605
  try {
8773
- return await readFile20(path, "utf8");
7606
+ return await readFile16(path, "utf8");
8774
7607
  } catch {
8775
7608
  return "";
8776
7609
  }
8777
7610
  }
8778
7611
  async function collectImportFiles(source) {
8779
- const st = await stat6(source);
7612
+ const st = await stat5(source);
8780
7613
  if (st.isFile()) return isImportCandidate(source) ? [source] : [];
8781
7614
  const files = [];
8782
7615
  await walkImportFiles(source, files);
8783
7616
  return files.sort((a, b) => a.localeCompare(b));
8784
7617
  }
8785
7618
  async function walkImportFiles(dir, out) {
8786
- const entries = await readdir5(dir, { withFileTypes: true });
7619
+ const entries = await readdir4(dir, { withFileTypes: true });
8787
7620
  for (const entry of entries) {
8788
7621
  if (entry.name === ".git" || entry.name === "node_modules") continue;
8789
- const path = join29(dir, entry.name);
7622
+ const path = join26(dir, entry.name);
8790
7623
  if (entry.isDirectory()) {
8791
7624
  await walkImportFiles(path, out);
8792
7625
  } else if (entry.isFile() && isImportCandidate(path)) {
@@ -8799,9 +7632,9 @@ function isImportCandidate(path) {
8799
7632
  return ext === ".md" || ext === ".txt";
8800
7633
  }
8801
7634
  async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
8802
- const st = await stat6(file);
7635
+ const st = await stat5(file);
8803
7636
  const sourceKind = classifyImportSource(file);
8804
- const hash = createHash8("sha256").update(await readFile20(file)).digest("hex");
7637
+ const hash = createHash7("sha256").update(await readFile16(file)).digest("hex");
8805
7638
  const baseEntry = {
8806
7639
  source_path: file,
8807
7640
  source_kind: sourceKind,
@@ -8824,7 +7657,7 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
8824
7657
  reason: "policy_source_not_imported"
8825
7658
  };
8826
7659
  }
8827
- const text = await readFile20(file, "utf8");
7660
+ const text = await readFile16(file, "utf8");
8828
7661
  const extracted = extractImportText(text, sourceKind);
8829
7662
  if (!extracted) {
8830
7663
  return {
@@ -8835,8 +7668,8 @@ async function buildImportEntry(file, sourceRoot, project, today, maxBytes) {
8835
7668
  }
8836
7669
  const redacted = redactSensitiveContent(extracted, { file });
8837
7670
  const privacy = redacted.findings.length > 0 ? "sensitive" : "local";
8838
- const sourceSlug = slugify2(basename3(file, extname(file)));
8839
- const relSource = relative5(sourceRoot, file).split(sep5).join("/");
7671
+ const sourceSlug = slugify2(basename2(file, extname(file)));
7672
+ const relSource = relative3(sourceRoot, file).split(sep3).join("/");
8840
7673
  const entry = {
8841
7674
  ...baseEntry,
8842
7675
  status: "ready",
@@ -8853,9 +7686,9 @@ async function writeImportCapture(vault, entry, today) {
8853
7686
  const content = hiddenString(entry, "__content");
8854
7687
  const project = hiddenString(entry, "__project");
8855
7688
  const relPath = await availableImportPath(vault, entry.proposed_path);
8856
- const absPath = join29(vault, relPath);
8857
- await mkdir7(join29(vault, "raw", "transcripts"), { recursive: true });
8858
- await writeFile7(absPath, renderImportCapture(entry, content, project, today), "utf8");
7689
+ const absPath = join26(vault, relPath);
7690
+ await mkdir6(join26(vault, "raw", "transcripts"), { recursive: true });
7691
+ await writeFile5(absPath, renderImportCapture(entry, content, project, today), "utf8");
8859
7692
  const validation = await runValidate({ file: absPath });
8860
7693
  return {
8861
7694
  relPath,
@@ -8870,7 +7703,7 @@ async function availableImportPath(vault, proposed) {
8870
7703
  const stem = proposed.slice(0, -ext.length);
8871
7704
  let candidate = proposed;
8872
7705
  let i = 2;
8873
- while (await readIfExists2(join29(vault, candidate))) {
7706
+ while (await readIfExists2(join26(vault, candidate))) {
8874
7707
  candidate = `${stem}-${i}${ext}`;
8875
7708
  i++;
8876
7709
  }
@@ -8894,7 +7727,7 @@ function renderImportCapture(entry, content, project, today) {
8894
7727
  `source_paths: ["${entry.source_path.replaceAll('"', '\\"')}"]`,
8895
7728
  "---",
8896
7729
  "",
8897
- `# Imported Memory: ${basename3(entry.source_path, extname(entry.source_path))}`,
7730
+ `# Imported Memory: ${basename2(entry.source_path, extname(entry.source_path))}`,
8898
7731
  "",
8899
7732
  `Source kind: ${entry.source_kind}`,
8900
7733
  "",
@@ -8908,8 +7741,8 @@ function hiddenString(entry, key) {
8908
7741
  return entry[key] ?? "";
8909
7742
  }
8910
7743
  function classifyImportSource(file) {
8911
- const rel = file.split(sep5).join("/");
8912
- const name = basename3(file);
7744
+ const rel = file.split(sep3).join("/");
7745
+ const name = basename2(file);
8913
7746
  if (rel.includes("/.codex/memories/")) return "codex-memory";
8914
7747
  if (rel.includes("/.codex/rules/")) return "codex-rule";
8915
7748
  if (rel.includes("/.claude/") && name === "napkin.md") return "napkin";
@@ -9042,10 +7875,10 @@ function memoryCacheRelPath(project) {
9042
7875
  }
9043
7876
  async function readMemoryCache(vault, project) {
9044
7877
  if (project) {
9045
- const projectCache = await readIfExists2(join29(vault, memoryCacheRelPath(project)));
7878
+ const projectCache = await readIfExists2(join26(vault, memoryCacheRelPath(project)));
9046
7879
  if (projectCache) return projectCache;
9047
7880
  }
9048
- return readIfExists2(join29(vault, ".skillwiki", "memory-topics.json"));
7881
+ return readIfExists2(join26(vault, ".skillwiki", "memory-topics.json"));
9049
7882
  }
9050
7883
  function dedupePages(pages) {
9051
7884
  const seen = /* @__PURE__ */ new Set();
@@ -9131,8 +7964,8 @@ function slugify2(value) {
9131
7964
  }
9132
7965
 
9133
7966
  // src/commands/query.ts
9134
- import { readFile as readFile21, stat as stat7 } from "fs/promises";
9135
- import { join as join30 } from "path";
7967
+ import { readFile as readFile17, stat as stat6 } from "fs/promises";
7968
+ import { join as join27 } from "path";
9136
7969
  var W_KEYWORD = 2;
9137
7970
  var W_SOURCE_OVERLAP = 4;
9138
7971
  var W_WIKILINK = 3;
@@ -9185,11 +8018,11 @@ async function runQuery(input) {
9185
8018
  );
9186
8019
  const results = pages.map((page) => {
9187
8020
  const sourceOverlap = scoreSourceOverlap(page, pages, seedPaths);
9188
- const wikilink2 = scoreWikilink(page.relPath, seedPaths, graph);
8021
+ const wikilink = scoreWikilink(page.relPath, seedPaths, graph);
9189
8022
  const aa = scoreAdamicAdar(page.relPath, seedPaths, graph);
9190
8023
  const typeAffinity = scoreTypeAffinity(page.type, queryTerms);
9191
8024
  const isSeed = page.keywordScore > 0;
9192
- const structuralBoost = sourceOverlap * W_SOURCE_OVERLAP + wikilink2 * W_WIKILINK + aa * W_ADAMIC_ADAR;
8025
+ const structuralBoost = sourceOverlap * W_SOURCE_OVERLAP + wikilink * W_WIKILINK + aa * W_ADAMIC_ADAR;
9193
8026
  const composite = isSeed ? page.keywordScore * W_KEYWORD + structuralBoost + typeAffinity * W_TYPE_AFFINITY : structuralBoost * NON_SEED_FACTOR + typeAffinity * W_TYPE_AFFINITY;
9194
8027
  return {
9195
8028
  path: page.relPath,
@@ -9253,10 +8086,10 @@ function computeKeywordScore(terms, title, tags, body) {
9253
8086
  return score;
9254
8087
  }
9255
8088
  async function loadOrBuildGraph(vault) {
9256
- const graphPath = join30(vault, ".skillwiki", "graph.json");
8089
+ const graphPath = join27(vault, ".skillwiki", "graph.json");
9257
8090
  let needsBuild = false;
9258
8091
  try {
9259
- const fileStat = await stat7(graphPath);
8092
+ const fileStat = await stat6(graphPath);
9260
8093
  const ageHours = (Date.now() - fileStat.mtimeMs) / (1e3 * 60 * 60);
9261
8094
  if (ageHours > 24) needsBuild = true;
9262
8095
  } catch {
@@ -9267,7 +8100,7 @@ async function loadOrBuildGraph(vault) {
9267
8100
  if (buildResult.exitCode !== 0) return null;
9268
8101
  }
9269
8102
  try {
9270
- const raw = await readFile21(graphPath, "utf8");
8103
+ const raw = await readFile17(graphPath, "utf8");
9271
8104
  return JSON.parse(raw);
9272
8105
  } catch {
9273
8106
  return null;
@@ -9279,30 +8112,30 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9279
8112
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9280
8113
 
9281
8114
  // src/mcp/tools.ts
9282
- import { z as z2 } from "zod";
8115
+ import { z } from "zod";
9283
8116
 
9284
8117
  // src/mcp/vault-resolve.ts
9285
- import { join as join31, resolve as resolve8 } from "path";
8118
+ import { join as join28, resolve as resolve7 } from "path";
9286
8119
 
9287
8120
  // src/mcp/allowlist.ts
9288
- import { resolve as resolve7, sep as sep6 } from "path";
9289
- import { realpathSync as realpathSync2 } from "fs";
8121
+ import { resolve as resolve6, sep as sep4 } from "path";
8122
+ import { realpathSync } from "fs";
9290
8123
  function parseVaultAllowlist(envValue) {
9291
8124
  if (envValue === void 0 || envValue.trim() === "") return null;
9292
- return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) => resolve7(p));
8125
+ return envValue.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map((p) => resolve6(p));
9293
8126
  }
9294
8127
  function vaultAllowedByList(vaultPath, allowlist) {
9295
8128
  if (!allowlist || allowlist.length === 0) return true;
9296
8129
  let canonical = vaultPath;
9297
8130
  try {
9298
- canonical = realpathSync2(vaultPath);
8131
+ canonical = realpathSync(vaultPath);
9299
8132
  } catch {
9300
- canonical = resolve7(vaultPath);
8133
+ canonical = resolve6(vaultPath);
9301
8134
  }
9302
- const resolved = resolve7(canonical);
8135
+ const resolved = resolve6(canonical);
9303
8136
  return allowlist.some((root) => {
9304
- const r = resolve7(root);
9305
- return resolved === r || resolved.startsWith(r + sep6);
8137
+ const r = resolve6(root);
8138
+ return resolved === r || resolved.startsWith(r + sep4);
9306
8139
  });
9307
8140
  }
9308
8141
  function getVaultAllowlistFromEnv() {
@@ -9315,7 +8148,7 @@ async function resolveMcpVault(input) {
9315
8148
  let vaultPath;
9316
8149
  let source = "resolved";
9317
8150
  if (input.vault !== void 0 && input.vault.length > 0) {
9318
- vaultPath = resolve8(input.vault);
8151
+ vaultPath = resolve7(input.vault);
9319
8152
  source = "flag";
9320
8153
  } else {
9321
8154
  const r = await resolveRuntimePath({
@@ -9327,7 +8160,7 @@ async function resolveMcpVault(input) {
9327
8160
  cwd: input.cwd ?? process.cwd()
9328
8161
  });
9329
8162
  if (!r.ok) return r;
9330
- vaultPath = resolve8(r.data.path);
8163
+ vaultPath = resolve7(r.data.path);
9331
8164
  source = r.data.source;
9332
8165
  }
9333
8166
  const scan = await scanVault(vaultPath);
@@ -9344,7 +8177,7 @@ async function resolveMcpVault(input) {
9344
8177
  return ok({ vault: vaultPath, source });
9345
8178
  }
9346
8179
  function defaultGraphOut(vault) {
9347
- return join31(vault, ".skillwiki", "graph.json");
8180
+ return join28(vault, ".skillwiki", "graph.json");
9348
8181
  }
9349
8182
 
9350
8183
  // src/mcp/result-format.ts
@@ -9361,7 +8194,7 @@ function formatToolResult(payload) {
9361
8194
  // src/mcp/audit-log.ts
9362
8195
  import { appendFileSync, mkdirSync as mkdirSync5 } from "fs";
9363
8196
  import { homedir } from "os";
9364
- import { join as join32 } from "path";
8197
+ import { join as join29 } from "path";
9365
8198
  function auditEnabled() {
9366
8199
  const v = process.env.SKILLWIKI_MCP_AUDIT;
9367
8200
  if (v === "0" || v === "false") return false;
@@ -9373,7 +8206,7 @@ function auditSink() {
9373
8206
  function auditFilePath() {
9374
8207
  const custom = process.env.SKILLWIKI_MCP_AUDIT_FILE;
9375
8208
  if (custom && custom.length > 0) return custom;
9376
- return join32(homedir(), ".skillwiki", "mcp-audit.jsonl");
8209
+ return join29(homedir(), ".skillwiki", "mcp-audit.jsonl");
9377
8210
  }
9378
8211
  function auditMcpToolCall(entry) {
9379
8212
  if (!auditEnabled()) return;
@@ -9383,7 +8216,7 @@ function auditMcpToolCall(entry) {
9383
8216
  return;
9384
8217
  }
9385
8218
  const path = auditFilePath();
9386
- mkdirSync5(join32(path, ".."), { recursive: true });
8219
+ mkdirSync5(join29(path, ".."), { recursive: true });
9387
8220
  appendFileSync(path, line, "utf8");
9388
8221
  }
9389
8222
  async function runMcpToolHandler(tool, input, fn) {
@@ -9412,18 +8245,18 @@ async function runMcpToolHandler(tool, input, fn) {
9412
8245
 
9413
8246
  // src/mcp/tools.ts
9414
8247
  var vaultFields = {
9415
- vault: z2.string().optional().describe("Vault root directory; omitted = resolve WIKI_PATH / default ~/wiki"),
9416
- wiki: z2.string().optional().describe("Wiki profile name for vault resolution")
8248
+ vault: z.string().optional().describe("Vault root directory; omitted = resolve WIKI_PATH / default ~/wiki"),
8249
+ wiki: z.string().optional().describe("Wiki profile name for vault resolution")
9417
8250
  };
9418
8251
  function registerMcpTools(server) {
9419
8252
  server.registerTool(
9420
8253
  "skillwiki.query",
9421
8254
  {
9422
8255
  description: "Ranked vault query over typed knowledge (read-only). Returns Result envelope JSON.",
9423
- inputSchema: z2.object({
8256
+ inputSchema: z.object({
9424
8257
  ...vaultFields,
9425
- text: z2.string().min(1).describe("Query text"),
9426
- limit: z2.number().int().positive().optional().describe("Max results (default 10)")
8258
+ text: z.string().min(1).describe("Query text"),
8259
+ limit: z.number().int().positive().optional().describe("Max results (default 10)")
9427
8260
  })
9428
8261
  },
9429
8262
  async ({ vault, wiki, text, limit }) => runMcpToolHandler("skillwiki.query", { vault, wiki }, async () => {
@@ -9437,13 +8270,13 @@ function registerMcpTools(server) {
9437
8270
  "skillwiki.lint_summary",
9438
8271
  {
9439
8272
  description: "Vault lint bucket summary (read-only, no --fix). Returns Result envelope JSON.",
9440
- inputSchema: z2.object({
8273
+ inputSchema: z.object({
9441
8274
  ...vaultFields,
9442
- only: z2.string().optional().describe("Run a single lint bucket"),
9443
- examplesLimit: z2.number().int().nonnegative().optional().describe("Examples per bucket in summary (default 3)"),
9444
- days: z2.number().int().positive().optional().describe("Stale threshold days (default 90)"),
9445
- lines: z2.number().int().positive().optional().describe("Pagesize threshold lines (default 200)"),
9446
- logThreshold: z2.number().int().positive().optional().describe("Log rotation threshold (default 500)")
8275
+ only: z.string().optional().describe("Run a single lint bucket"),
8276
+ examplesLimit: z.number().int().nonnegative().optional().describe("Examples per bucket in summary (default 3)"),
8277
+ days: z.number().int().positive().optional().describe("Stale threshold days (default 90)"),
8278
+ lines: z.number().int().positive().optional().describe("Pagesize threshold lines (default 200)"),
8279
+ logThreshold: z.number().int().positive().optional().describe("Log rotation threshold (default 500)")
9447
8280
  })
9448
8281
  },
9449
8282
  async (args) => runMcpToolHandler("skillwiki.lint_summary", { vault: args.vault, wiki: args.wiki }, async () => {
@@ -9467,7 +8300,7 @@ function registerMcpTools(server) {
9467
8300
  "skillwiki.doctor",
9468
8301
  {
9469
8302
  description: "Diagnose skillwiki setup, vault path, sync, and plugin channels (read-only).",
9470
- inputSchema: z2.object({
8303
+ inputSchema: z.object({
9471
8304
  ...vaultFields
9472
8305
  })
9473
8306
  },
@@ -9488,9 +8321,9 @@ function registerMcpTools(server) {
9488
8321
  "skillwiki.graph_build",
9489
8322
  {
9490
8323
  description: "Build wikilink graph JSON under .skillwiki/graph.json (writes graph file only).",
9491
- inputSchema: z2.object({
8324
+ inputSchema: z.object({
9492
8325
  ...vaultFields,
9493
- out: z2.string().optional().describe("Output path (default <vault>/.skillwiki/graph.json)")
8326
+ out: z.string().optional().describe("Output path (default <vault>/.skillwiki/graph.json)")
9494
8327
  })
9495
8328
  },
9496
8329
  async ({ vault, wiki, out }) => runMcpToolHandler("skillwiki.graph_build", { vault, wiki }, async () => {
@@ -9505,9 +8338,9 @@ function registerMcpTools(server) {
9505
8338
  "skillwiki.project_index",
9506
8339
  {
9507
8340
  description: "List project index entries for a slug (read-only, apply=false).",
9508
- inputSchema: z2.object({
8341
+ inputSchema: z.object({
9509
8342
  ...vaultFields,
9510
- slug: z2.string().min(1).describe("Project slug under projects/{slug}/")
8343
+ slug: z.string().min(1).describe("Project slug under projects/{slug}/")
9511
8344
  })
9512
8345
  },
9513
8346
  async ({ vault, wiki, slug }) => runMcpToolHandler("skillwiki.project_index", { vault, wiki }, async () => {
@@ -9521,10 +8354,10 @@ function registerMcpTools(server) {
9521
8354
  "skillwiki.stale",
9522
8355
  {
9523
8356
  description: "List stale pages, transcripts, and incomplete work items (read-only).",
9524
- inputSchema: z2.object({
8357
+ inputSchema: z.object({
9525
8358
  ...vaultFields,
9526
- days: z2.number().int().positive().optional().describe("Stale age threshold (default 90)"),
9527
- project: z2.string().optional().describe("Scope to one project slug")
8359
+ days: z.number().int().positive().optional().describe("Stale age threshold (default 90)"),
8360
+ project: z.string().optional().describe("Scope to one project slug")
9528
8361
  })
9529
8362
  },
9530
8363
  async ({ vault, wiki, days, project }) => runMcpToolHandler("skillwiki.stale", { vault, wiki }, async () => {
@@ -9543,8 +8376,8 @@ function registerMcpTools(server) {
9543
8376
  "skillwiki.config_get",
9544
8377
  {
9545
8378
  description: "Read a single skillwiki config key from ~/.skillwiki/.env (read-only).",
9546
- inputSchema: z2.object({
9547
- key: z2.string().min(1).describe("Config key (e.g. WIKI_PATH or profile key)")
8379
+ inputSchema: z.object({
8380
+ key: z.string().min(1).describe("Config key (e.g. WIKI_PATH or profile key)")
9548
8381
  })
9549
8382
  },
9550
8383
  async ({ key }) => runMcpToolHandler("skillwiki.config_get", {}, async () => {
@@ -9555,7 +8388,7 @@ function registerMcpTools(server) {
9555
8388
  }
9556
8389
 
9557
8390
  // src/mcp/mutating-tools.ts
9558
- import { z as z3 } from "zod";
8391
+ import { z as z2 } from "zod";
9559
8392
 
9560
8393
  // src/mcp/mutation-gate.ts
9561
8394
  function mcpMutationsEnabled() {
@@ -9566,20 +8399,20 @@ var MCP_MUTATION_DISABLED_MESSAGE = "Mutating MCP tools are disabled. Set SKILLW
9566
8399
 
9567
8400
  // src/mcp/mutating-tools.ts
9568
8401
  var vaultFields2 = {
9569
- vault: z3.string().optional(),
9570
- wiki: z3.string().optional()
8402
+ vault: z2.string().optional(),
8403
+ wiki: z2.string().optional()
9571
8404
  };
9572
8405
  function registerMcpMutatingTools(server) {
9573
8406
  server.registerTool(
9574
8407
  "skillwiki.observe",
9575
8408
  {
9576
8409
  description: "Create a new raw/transcripts capture file (mutating). Requires SKILLWIKI_MCP_ALLOW_MUTATIONS=true.",
9577
- inputSchema: z3.object({
8410
+ inputSchema: z2.object({
9578
8411
  ...vaultFields2,
9579
- text: z3.string().min(1).describe("Capture body text"),
9580
- kind: z3.enum(["note", "bug", "task", "idea", "session-log"]).optional(),
9581
- project: z3.string().optional().describe("Project slug for frontmatter"),
9582
- confirm_mutation: z3.literal(true).describe("Must be true to acknowledge vault write")
8412
+ text: z2.string().min(1).describe("Capture body text"),
8413
+ kind: z2.enum(["note", "bug", "task", "idea", "session-log"]).optional(),
8414
+ project: z2.string().optional().describe("Project slug for frontmatter"),
8415
+ confirm_mutation: z2.literal(true).describe("Must be true to acknowledge vault write")
9583
8416
  })
9584
8417
  },
9585
8418
  async (args) => runMcpToolHandler("skillwiki.observe", { vault: args.vault, wiki: args.wiki }, async () => {
@@ -9603,8 +8436,8 @@ function registerMcpMutatingTools(server) {
9603
8436
  }
9604
8437
 
9605
8438
  // src/mcp/resources.ts
9606
- import { readFile as readFile23 } from "fs/promises";
9607
- import { join as join34 } from "path";
8439
+ import { readFile as readFile19 } from "fs/promises";
8440
+ import { join as join31 } from "path";
9608
8441
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
9609
8442
 
9610
8443
  // src/mcp/lint-bucket.ts
@@ -9732,9 +8565,9 @@ async function fetchQueryPreview(input) {
9732
8565
  }
9733
8566
 
9734
8567
  // src/mcp/graph-html.ts
9735
- import { readFile as readFile22 } from "fs/promises";
9736
- import { join as join33 } from "path";
9737
- import { existsSync as existsSync16 } from "fs";
8568
+ import { readFile as readFile18 } from "fs/promises";
8569
+ import { join as join30 } from "path";
8570
+ import { existsSync as existsSync15 } from "fs";
9738
8571
  var TYPE_COLORS = {
9739
8572
  entities: "#e74c3c",
9740
8573
  concepts: "#27ae60",
@@ -9802,9 +8635,9 @@ ${nodeSvg}
9802
8635
  return { html, node_count: nodes.length, edge_count: edges.length, truncated };
9803
8636
  }
9804
8637
  async function fetchGraphHtmlReport(input) {
9805
- const graphPath = input.graphPath ?? join33(input.vault, ".skillwiki", "graph.json");
8638
+ const graphPath = input.graphPath ?? join30(input.vault, ".skillwiki", "graph.json");
9806
8639
  const maxNodes = Math.min(Math.max(10, input.maxNodes ?? 120), 500);
9807
- if (!existsSync16(graphPath)) {
8640
+ if (!existsSync15(graphPath)) {
9808
8641
  return {
9809
8642
  exitCode: ExitCode.FILE_NOT_FOUND,
9810
8643
  result: err("GRAPH_MISSING", { path: graphPath, hint: "Run skillwiki.graph_build first." })
@@ -9812,7 +8645,7 @@ async function fetchGraphHtmlReport(input) {
9812
8645
  }
9813
8646
  let raw;
9814
8647
  try {
9815
- raw = await readFile22(graphPath, "utf8");
8648
+ raw = await readFile18(graphPath, "utf8");
9816
8649
  } catch (e) {
9817
8650
  return {
9818
8651
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -9876,7 +8709,7 @@ async function fetchStaleSummary(input) {
9876
8709
 
9877
8710
  // src/mcp/resources.ts
9878
8711
  async function readVaultFile(vault, rel) {
9879
- return readFile23(join34(vault, rel), "utf8");
8712
+ return readFile19(join31(vault, rel), "utf8");
9880
8713
  }
9881
8714
  async function tailLines(text, lines) {
9882
8715
  const parts = text.split(/\r?\n/);
@@ -9962,9 +8795,9 @@ function registerMcpResources(server) {
9962
8795
  if (!v.ok) {
9963
8796
  return { contents: [{ uri: uri.href, mimeType: "text/plain", text: JSON.stringify(v) }] };
9964
8797
  }
9965
- const path = join34(v.data.vault, ".skillwiki", "graph.json");
8798
+ const path = join31(v.data.vault, ".skillwiki", "graph.json");
9966
8799
  try {
9967
- const raw = await readFile23(path, "utf8");
8800
+ const raw = await readFile19(path, "utf8");
9968
8801
  const graph = JSON.parse(raw);
9969
8802
  const adjacency = graph.adjacency ?? {};
9970
8803
  const nodes = Object.keys(adjacency);
@@ -10138,14 +8971,14 @@ function registerMcpResources(server) {
10138
8971
  }
10139
8972
 
10140
8973
  // src/mcp/prompts.ts
10141
- import { z as z4 } from "zod";
8974
+ import { z as z3 } from "zod";
10142
8975
  function registerMcpPrompts(server) {
10143
8976
  server.registerPrompt(
10144
8977
  "skillwiki-research-query",
10145
8978
  {
10146
8979
  description: "Structured vault research using skillwiki.query and typed pages",
10147
8980
  argsSchema: {
10148
- topic: z4.string().describe("Research topic or question")
8981
+ topic: z3.string().describe("Research topic or question")
10149
8982
  }
10150
8983
  },
10151
8984
  ({ topic }) => ({
@@ -10176,8 +9009,8 @@ function registerMcpPrompts(server) {
10176
9009
  {
10177
9010
  description: "Plan a project work item using vault project workspace conventions",
10178
9011
  argsSchema: {
10179
- slug: z4.string().describe("Project slug"),
10180
- idea: z4.string().describe("Work item idea or bug/feature summary")
9012
+ slug: z3.string().describe("Project slug"),
9013
+ idea: z3.string().describe("Work item idea or bug/feature summary")
10181
9014
  }
10182
9015
  },
10183
9016
  ({ slug, idea }) => ({
@@ -10205,7 +9038,7 @@ function registerMcpPrompts(server) {
10205
9038
  {
10206
9039
  description: "Review vault health via lint summary, doctor, and stale tools",
10207
9040
  argsSchema: {
10208
- vault: z4.string().optional().describe("Optional vault path")
9041
+ vault: z3.string().optional().describe("Optional vault path")
10209
9042
  }
10210
9043
  },
10211
9044
  ({ vault }) => ({
@@ -10231,7 +9064,7 @@ function registerMcpPrompts(server) {
10231
9064
  {
10232
9065
  description: "Audit citation health using query + lint buckets",
10233
9066
  argsSchema: {
10234
- focus: z4.string().optional().describe("Page or topic focus")
9067
+ focus: z3.string().optional().describe("Page or topic focus")
10235
9068
  }
10236
9069
  },
10237
9070
  ({ focus }) => ({
@@ -10274,20 +9107,9 @@ async function runSkillwikiMcpStdio() {
10274
9107
  }
10275
9108
 
10276
9109
  export {
10277
- ExitCode,
10278
- ok,
10279
- err,
10280
- RawSourceSchema,
10281
- MetaSchema,
10282
- isBlockedHost,
10283
- splitFrontmatter,
10284
- extractFrontmatter,
10285
- scanSensitiveContent,
10286
- redactSensitiveContent,
10287
9110
  readLastOp,
10288
9111
  appendLastOp,
10289
9112
  clearLastOp,
10290
- atomicWriteText,
10291
9113
  runLogAppend,
10292
9114
  renderIndexUpsert,
10293
9115
  upsertIndexEntry,
@@ -10298,16 +9120,8 @@ export {
10298
9120
  releaseLock,
10299
9121
  acquireOwnedSyncLock,
10300
9122
  releaseOwnedSyncLock,
10301
- assertTargetInsideVault,
10302
- prepareTypedPage,
10303
9123
  runValidate,
10304
- scanVault,
10305
- readPage,
10306
9124
  runGraphBuild,
10307
- profileKey,
10308
- parseDotenvText,
10309
- parseDotenvFile,
10310
- writeDotenv,
10311
9125
  resolveInitTimePath,
10312
9126
  resolveRuntimePath,
10313
9127
  runOrphans,
@@ -10317,9 +9131,12 @@ export {
10317
9131
  extractTaxonomy,
10318
9132
  taxonomyCommentForPage,
10319
9133
  reconcileTaxonomyDocument,
9134
+ mergeTaxonomyConflict,
10320
9135
  runLinks,
10321
9136
  runTagAudit,
10322
9137
  runIndexCheck,
9138
+ renderProjectIndex,
9139
+ runProjectIndex,
10323
9140
  runStale,
10324
9141
  runPagesize,
10325
9142
  runLogRotate,
@@ -10343,13 +9160,6 @@ export {
10343
9160
  runConfigPath,
10344
9161
  buildDegradedReasons,
10345
9162
  probeRemoteHealth,
10346
- FLEET_REL_PATH,
10347
- runFleetValidate,
10348
- runFleetContext,
10349
- loadFleetManifestAndHost,
10350
- snapshotterAliasForLocalHost,
10351
- loadFleetManifest,
10352
- resolveFleetHostId,
10353
9163
  SATELLITE_STALE_MS,
10354
9164
  satelliteLatestRunPath,
10355
9165
  isFailedRunStatus,
@@ -10357,7 +9167,6 @@ export {
10357
9167
  evaluateSatelliteRunHealth,
10358
9168
  runDoctor,
10359
9169
  readCliPackageJson,
10360
- runProjectIndex,
10361
9170
  runObserve,
10362
9171
  runMemoryTopics,
10363
9172
  runMemoryIndex,