skillwiki 0.9.2 → 0.9.4

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 (34) hide show
  1. package/dist/cli.js +938 -531
  2. package/package.json +1 -1
  3. package/skills/.claude-plugin/plugin.json +1 -1
  4. package/skills/.codex-plugin/plugin.json +1 -1
  5. package/skills/agents/proj-decide.md +1 -0
  6. package/skills/agents/proj-distill.md +1 -0
  7. package/skills/agents/proj-init.md +1 -0
  8. package/skills/agents/proj-work.md +1 -0
  9. package/skills/agents/wiki-adapter-prd.md +8 -5
  10. package/skills/agents/wiki-add-task.md +8 -5
  11. package/skills/agents/wiki-archive.md +1 -0
  12. package/skills/agents/wiki-audit.md +1 -0
  13. package/skills/agents/wiki-canvas.md +1 -0
  14. package/skills/agents/wiki-crystallize.md +5 -2
  15. package/skills/agents/wiki-ingest.md +8 -5
  16. package/skills/agents/wiki-lint.md +5 -3
  17. package/skills/agents/wiki-query.md +4 -1
  18. package/skills/agents/wiki-reingest.md +6 -3
  19. package/skills/agents/wiki-sync.md +1 -0
  20. package/skills/package.json +1 -1
  21. package/skills/skills/using-skillwiki/SKILL.md +26 -1
  22. package/skills/skills/wiki-adapter-prd/SKILL.md +8 -5
  23. package/skills/skills/wiki-add-task/SKILL.md +9 -6
  24. package/skills/skills/wiki-crystallize/SKILL.md +5 -2
  25. package/skills/skills/wiki-ingest/SKILL.md +8 -6
  26. package/skills/skills/wiki-lint/SKILL.md +5 -3
  27. package/skills/skills/wiki-query/SKILL.md +4 -1
  28. package/skills/using-skillwiki/SKILL.md +26 -1
  29. package/skills/wiki-adapter-prd/SKILL.md +8 -5
  30. package/skills/wiki-add-task/SKILL.md +9 -6
  31. package/skills/wiki-crystallize/SKILL.md +5 -2
  32. package/skills/wiki-ingest/SKILL.md +8 -6
  33. package/skills/wiki-lint/SKILL.md +5 -3
  34. package/skills/wiki-query/SKILL.md +4 -1
package/dist/cli.js CHANGED
@@ -69,7 +69,8 @@ var ExitCode = {
69
69
  BODY_TRUNCATION_GUARD: 47,
70
70
  SYNC_LOCK_HELD: 48,
71
71
  LOG_APPEND_LOCK_HELD: 49,
72
- FLEET_MANIFEST_INVALID: 50
72
+ FLEET_MANIFEST_INVALID: 50,
73
+ SENSITIVE_CONTENT_DETECTED: 51
73
74
  };
74
75
 
75
76
  // ../shared/src/json-output.ts
@@ -450,6 +451,148 @@ function sanitizeUrl(u) {
450
451
  // src/commands/validate.ts
451
452
  import { readFile as readFile2, writeFile } from "fs/promises";
452
453
  import { join as join2, resolve, relative, sep } from "path";
454
+
455
+ // src/utils/sensitive-content.ts
456
+ import { createHash as createHash2 } from "crypto";
457
+ var REDACTED_RE = /\[REDACTED:[^\]]+\]/i;
458
+ var SYNTHETIC_RE = /^(?:<[^>]+>|\$\{[^}]+\}|REPLACE_WITH_[A-Z0-9_]+|YOUR_[A-Z0-9_]+|EXAMPLE_[A-Z0-9_]+)$/i;
459
+ var MATCHERS = [
460
+ {
461
+ kind: "private_key",
462
+ re: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g
463
+ },
464
+ {
465
+ kind: "authorization_header",
466
+ re: /\bAuthorization["']?\s*:\s*["']?(Bearer\s+[A-Za-z0-9._~+/-]{20,})["']?/gi,
467
+ valueGroup: 1
468
+ },
469
+ {
470
+ kind: "cookie",
471
+ re: /\b(?:Cookie|Set-Cookie)\s*:\s*([^\n]{20,})/gi,
472
+ valueGroup: 1
473
+ },
474
+ {
475
+ kind: "jwt",
476
+ re: /\b([A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,})\b/g,
477
+ valueGroup: 1
478
+ },
479
+ {
480
+ kind: "provider_key",
481
+ re: /\b(sk-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{20,}|gh[pousr]_[A-Za-z0-9_=-]{20,})\b/g,
482
+ valueGroup: 1
483
+ },
484
+ {
485
+ kind: "access_key",
486
+ re: /\b((?:AKIA|ASIA)[A-Z0-9]{16})\b/g,
487
+ valueGroup: 1
488
+ },
489
+ {
490
+ kind: "access_key",
491
+ re: /\b(?:access[-_ ]?key|credential)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
492
+ valueGroup: 1
493
+ },
494
+ {
495
+ kind: "api_key",
496
+ re: /\b(?:api[-_ ]?key)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{20,})["']?/gi,
497
+ valueGroup: 1
498
+ },
499
+ {
500
+ kind: "password",
501
+ re: /\b(?:pass(?:word|wd)?)["']?\s*[:=]\s*["']?([^\s`"']{8,})["']?/gi,
502
+ valueGroup: 1
503
+ },
504
+ {
505
+ kind: "secret",
506
+ re: /\b(?:secret|client[-_ ]?secret)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
507
+ valueGroup: 1
508
+ },
509
+ {
510
+ kind: "token",
511
+ re: /\b(?:token|session)["']?\s*[:=]\s*["']?([A-Za-z0-9._~+/-]{16,})["']?/gi,
512
+ valueGroup: 1
513
+ }
514
+ ];
515
+ function fingerprint(value) {
516
+ return createHash2("sha256").update(value).digest("hex").slice(0, 12);
517
+ }
518
+ function lineFor(text, offset) {
519
+ return text.slice(0, offset).split(/\r?\n/).length;
520
+ }
521
+ function redactMarker(kind, value) {
522
+ return `[REDACTED:${kind}:${fingerprint(value)}]`;
523
+ }
524
+ function isSyntheticPlaceholder(value) {
525
+ return REDACTED_RE.test(value) || SYNTHETIC_RE.test(value.trim());
526
+ }
527
+ function collectMatches(text) {
528
+ const matches = [];
529
+ for (const matcher of MATCHERS) {
530
+ matcher.re.lastIndex = 0;
531
+ for (const m of text.matchAll(matcher.re)) {
532
+ const whole = m[0];
533
+ const start = m.index ?? 0;
534
+ if (REDACTED_RE.test(whole)) continue;
535
+ const value = matcher.valueGroup ? m[matcher.valueGroup] : whole;
536
+ if (isSyntheticPlaceholder(value)) continue;
537
+ const valueOffset = whole.lastIndexOf(value);
538
+ const valueStart = start + Math.max(0, valueOffset);
539
+ matches.push({
540
+ start,
541
+ end: start + whole.length,
542
+ valueStart,
543
+ valueEnd: valueStart + value.length,
544
+ kind: matcher.kind
545
+ });
546
+ }
547
+ }
548
+ return matches.sort((a, b) => {
549
+ if (a.valueStart !== b.valueStart) return a.valueStart - b.valueStart;
550
+ return b.valueEnd - b.valueStart - (a.valueEnd - a.valueStart);
551
+ });
552
+ }
553
+ function collapseOverlaps(matches) {
554
+ const kept = [];
555
+ for (const match of matches) {
556
+ const overlaps = kept.some((k) => match.valueStart < k.valueEnd && match.valueEnd > k.valueStart);
557
+ if (!overlaps) kept.push(match);
558
+ }
559
+ return kept;
560
+ }
561
+ function scanSensitiveContent(text, opts = {}) {
562
+ return collapseOverlaps(collectMatches(text)).map((match) => {
563
+ const value = text.slice(match.valueStart, match.valueEnd);
564
+ const marker = redactMarker(match.kind, value);
565
+ const rawPreview = text.slice(Math.max(0, match.start - 24), Math.min(text.length, match.end + 24));
566
+ const preview = rawPreview.replace(value, marker);
567
+ return {
568
+ file: opts.file,
569
+ line: lineFor(text, match.valueStart),
570
+ kind: match.kind,
571
+ preview,
572
+ fingerprint: fingerprint(value)
573
+ };
574
+ });
575
+ }
576
+ function redactSensitiveContent(text, opts = {}) {
577
+ const matches = collapseOverlaps(collectMatches(text));
578
+ if (matches.length === 0) return { text, changed: false, findings: [] };
579
+ let out = "";
580
+ let cursor = 0;
581
+ for (const match of matches) {
582
+ const value = text.slice(match.valueStart, match.valueEnd);
583
+ out += text.slice(cursor, match.valueStart);
584
+ out += redactMarker(match.kind, value);
585
+ cursor = match.valueEnd;
586
+ }
587
+ out += text.slice(cursor);
588
+ return {
589
+ text: out,
590
+ changed: out !== text,
591
+ findings: scanSensitiveContent(text, opts)
592
+ };
593
+ }
594
+
595
+ // src/commands/validate.ts
453
596
  var TYPE_TO_SECTION = {
454
597
  entity: "Entities",
455
598
  concept: "Concepts",
@@ -480,6 +623,26 @@ async function runValidate(input) {
480
623
  return { exitCode: ExitCode.INVALID_FRONTMATTER, result: fm };
481
624
  }
482
625
  const det = detectSchema(fm.data);
626
+ const sensitiveFindings = scanSensitiveContent(text, { file: input.file });
627
+ if (sensitiveFindings.length > 0) {
628
+ const errors = sensitiveFindings.map((f) => ({
629
+ path: "sensitive_content",
630
+ message: `${f.kind} at line ${f.line}: ${f.preview}`
631
+ }));
632
+ return {
633
+ exitCode: ExitCode.SENSITIVE_CONTENT_DETECTED,
634
+ result: ok({
635
+ schema: det.schema,
636
+ valid: false,
637
+ errors,
638
+ index_updated: false,
639
+ log_updated: false,
640
+ humanHint: `SENSITIVE CONTENT (${sensitiveFindings.length})
641
+ ${errors.map((e) => ` ${e.message}`).join("\n")}`,
642
+ sensitive_findings: sensitiveFindings
643
+ })
644
+ };
645
+ }
483
646
  if (!det.schema) {
484
647
  return { exitCode: ExitCode.SCHEMA_NOT_DETECTED, result: ok({ schema: null, valid: false, errors: [], index_updated: false, log_updated: false, humanHint: "schema not detected" }) };
485
648
  }
@@ -2704,6 +2867,7 @@ ${content}
2704
2867
  // src/commands/lint.ts
2705
2868
  import { existsSync as existsSync6 } from "fs";
2706
2869
  import { readFile as readFile17 } from "fs/promises";
2870
+ import { createHash as createHash4 } from "crypto";
2707
2871
  import { join as join23 } from "path";
2708
2872
 
2709
2873
  // src/commands/sparse-community.ts
@@ -2920,7 +3084,7 @@ async function safeWritePage(absPath, newContent, opts = {}) {
2920
3084
  }
2921
3085
 
2922
3086
  // src/commands/raw-body-dedup.ts
2923
- import { createHash as createHash2 } from "crypto";
3087
+ import { createHash as createHash3 } from "crypto";
2924
3088
  async function runRawBodyDedup(vault) {
2925
3089
  const scan = await scanVault(vault);
2926
3090
  if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
@@ -2931,7 +3095,7 @@ async function runRawBodyDedup(vault) {
2931
3095
  const split = splitFrontmatter(text);
2932
3096
  if (!split.ok) continue;
2933
3097
  totalFiles++;
2934
- const bodyHash = createHash2("sha256").update(split.data.body).digest("hex");
3098
+ const bodyHash = createHash3("sha256").update(split.data.body).digest("hex");
2935
3099
  const fm = extractFrontmatter(text);
2936
3100
  let fmSha256 = null;
2937
3101
  if (fm.ok && typeof fm.data.sha256 === "string" && fm.data.sha256.length === 64) {
@@ -3387,7 +3551,7 @@ function extractSourceEntries(rawFm) {
3387
3551
  }
3388
3552
  return entries;
3389
3553
  }
3390
- var ERROR_ORDER = ["broken_wikilinks", "invalid_frontmatter", "raw_source_identity_conflict", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
3554
+ var ERROR_ORDER = ["sensitive_content", "broken_wikilinks", "invalid_frontmatter", "raw_source_identity_conflict", "raw_dedup", "broken_sources", "tag_not_in_taxonomy", "path_too_long"];
3391
3555
  var WARNING_ORDER = ["raw_body_duplicate", "raw_subdirectory_duplicate", "file_source_url", "index_incomplete", "index_link_format", "stale_page", "page_too_large", "log_rotate_needed", "orphans", "compound_refs", "legacy_citation_style", "orphaned_citations", "duplicate_frontmatter", "work_item_health", "orphaned_project_pages", "missing_overview", "missing_diagram"];
3392
3556
  var INFO_ORDER = ["bridges", "sparse_community", "page_structure", "topic_map_recommended", "frontmatter_wikilink", "wikilink_citation", "missing_tldr", "stale_sections", "cli_refs"];
3393
3557
  var KNOWN_BUCKETS = [...ERROR_ORDER, ...WARNING_ORDER, ...INFO_ORDER];
@@ -3416,6 +3580,26 @@ function summarizeBucket(bucket, severity, vaultPath, examplesLimit) {
3416
3580
  details_command: `skillwiki lint ${shellQuote(vaultPath)} --only ${bucket.kind}`
3417
3581
  };
3418
3582
  }
3583
+ function recomputeRawSha256IfPresent(content) {
3584
+ const split = splitFrontmatter(content);
3585
+ if (!split.ok) return content;
3586
+ if (!/^sha256:\s*[0-9a-f]{64}$/m.test(split.data.rawFrontmatter)) return content;
3587
+ const sha256 = createHash4("sha256").update(Buffer.from(split.data.body, "utf8")).digest("hex");
3588
+ const rawFrontmatter = split.data.rawFrontmatter.replace(/^sha256:\s*[0-9a-f]{64}$/m, `sha256: ${sha256}`);
3589
+ return `---
3590
+ ${rawFrontmatter}
3591
+ ---
3592
+ ${split.data.body}`;
3593
+ }
3594
+ function appendLintFixLastOp(vault, fixed) {
3595
+ if (fixed.length === 0) return;
3596
+ appendLastOp(vault, {
3597
+ operation: "lint-fix",
3598
+ summary: `fixed ${fixed.length} page(s)`,
3599
+ files: fixed,
3600
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3601
+ });
3602
+ }
3419
3603
  function summarizeLintOutput(output, examplesLimit = 3) {
3420
3604
  const buckets = [
3421
3605
  ...output.by_severity.error.map((bucket) => summarizeBucket(bucket, "error", output.vault.path, examplesLimit)),
@@ -3517,6 +3701,16 @@ async function runLint(input) {
3517
3701
  const allPages = scan.ok ? [...scan.data.typedKnowledge, ...scan.data.raw, ...scan.data.workItems, ...scan.data.compound] : [];
3518
3702
  const slugs = scan.ok ? buildSlugMap(allPages) : /* @__PURE__ */ new Map();
3519
3703
  if (scan.ok) {
3704
+ const sensitiveFlags = [];
3705
+ for (const page of scan.data.allMarkdown) {
3706
+ try {
3707
+ const text = await readPage(page);
3708
+ const findings = scanSensitiveContent(text, { file: page.relPath });
3709
+ sensitiveFlags.push(...findings);
3710
+ } catch {
3711
+ }
3712
+ }
3713
+ if (sensitiveFlags.length > 0) buckets.sensitive_content = sensitiveFlags;
3520
3714
  const subDirDupes = [];
3521
3715
  const flatStems = /* @__PURE__ */ new Map();
3522
3716
  const deepFiles = [];
@@ -3732,6 +3926,36 @@ async function runLint(input) {
3732
3926
  }
3733
3927
  }
3734
3928
  if (staleSectionFlags.length > 0) buckets.stale_sections = staleSectionFlags;
3929
+ if (shouldFix("sensitive_content") && buckets.sensitive_content) {
3930
+ const sensitiveFixed = [];
3931
+ for (const page of scan.data.allMarkdown) {
3932
+ try {
3933
+ const raw = await readPage(page);
3934
+ const redacted = redactSensitiveContent(raw, { file: page.relPath });
3935
+ if (!redacted.changed) continue;
3936
+ const next = recomputeRawSha256IfPresent(redacted.text);
3937
+ const w = await safeWritePage(page.absPath, next, { minBodyRatio: null });
3938
+ if (!w.ok) {
3939
+ unresolved.push(page.relPath);
3940
+ continue;
3941
+ }
3942
+ sensitiveFixed.push(page.relPath);
3943
+ } catch {
3944
+ unresolved.push(page.relPath);
3945
+ }
3946
+ }
3947
+ fixed.push(...sensitiveFixed);
3948
+ const remainingSensitiveFlags = [];
3949
+ for (const page of scan.data.allMarkdown) {
3950
+ try {
3951
+ const text = await readPage(page);
3952
+ remainingSensitiveFlags.push(...scanSensitiveContent(text, { file: page.relPath }));
3953
+ } catch {
3954
+ }
3955
+ }
3956
+ if (remainingSensitiveFlags.length > 0) buckets.sensitive_content = remainingSensitiveFlags;
3957
+ else delete buckets.sensitive_content;
3958
+ }
3735
3959
  if (shouldFix("legacy_citation_style") && legacyPages.length > 0) {
3736
3960
  const FENCE_RE2 = /```[\s\S]*?```/g;
3737
3961
  const INLINE_MARKER = /\^\[raw\/[^\]]+\]/g;
@@ -4088,6 +4312,7 @@ ${newBody}`;
4088
4312
  humanHint: `--only ${input.only}
4089
4313
  ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items.length}`).join("\n")}`
4090
4314
  };
4315
+ if (input.fix) appendLintFixLastOp(input.vault, fixed);
4091
4316
  return {
4092
4317
  exitCode: fExit,
4093
4318
  result: ok(input.summary ? summarizeLintOutput(output2, input.examplesLimit) : output2)
@@ -4110,14 +4335,7 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
4110
4335
  hintLines.push(` ${b.kind}: ${b.items.length}`);
4111
4336
  }
4112
4337
  if (hintLines.length === 0) hintLines.push("0 errors, 0 warnings, 0 info");
4113
- if (input.fix && fixed.length > 0) {
4114
- appendLastOp(input.vault, {
4115
- operation: "lint-fix",
4116
- summary: `fixed ${fixed.length} page(s)`,
4117
- files: fixed,
4118
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4119
- });
4120
- }
4338
+ if (input.fix) appendLintFixLastOp(input.vault, fixed);
4121
4339
  const output = {
4122
4340
  vault: { path: input.vault, source: input.source ?? "resolved" },
4123
4341
  summary,
@@ -4134,12 +4352,12 @@ ${match.length === 0 ? "0 violations" : match.map((b) => ` ${b.kind}: ${b.items
4134
4352
 
4135
4353
  // src/commands/health.ts
4136
4354
  import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync8, renameSync, writeFileSync as writeFileSync6 } from "fs";
4137
- import { dirname as dirname10, join as join29, resolve as resolve6 } from "path";
4355
+ import { dirname as dirname10, join as join30, resolve as resolve6 } from "path";
4138
4356
  import { platform as platform3 } from "os";
4139
4357
 
4140
4358
  // src/commands/doctor.ts
4141
4359
  import { existsSync as existsSync11, lstatSync, readlinkSync, readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync7 } from "fs";
4142
- import { join as join28, resolve as resolve5 } from "path";
4360
+ import { join as join29, resolve as resolve5 } from "path";
4143
4361
  import { execSync as execSync2 } from "child_process";
4144
4362
  import { platform as platform2 } from "os";
4145
4363
 
@@ -4381,135 +4599,535 @@ function parseTomlScalar(rawValue) {
4381
4599
  return value;
4382
4600
  }
4383
4601
 
4384
- // src/utils/s3-mount-health.ts
4385
- import { execSync } from "child_process";
4386
- import { platform } from "os";
4387
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as readFile19 } from "fs";
4602
+ // src/commands/fleet.ts
4603
+ import { readFile as readFile19 } from "fs/promises";
4604
+ import { hostname as nodeHostname, userInfo } from "os";
4388
4605
  import { join as join27 } from "path";
4389
- var OS = platform();
4390
- function findRcloneMountPid() {
4391
- try {
4392
- const out = execSync("pgrep -f 'rclone.*mount'", {
4393
- encoding: "utf8",
4394
- timeout: 2e3,
4395
- stdio: ["pipe", "pipe", "pipe"]
4396
- }).trim();
4397
- const pids = out.split("\n").filter(Boolean);
4398
- if (pids.length === 0) return null;
4399
- return parseInt(pids[0], 10);
4400
- } catch {
4401
- try {
4402
- const out = execSync("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
4403
- for (const line of out.split("\n")) {
4404
- if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
4405
- const parts = line.trim().split(/\s+/);
4406
- if (parts.length >= 2) return parseInt(parts[1], 10);
4407
- }
4408
- }
4409
- } catch {
4606
+ import yaml3 from "js-yaml";
4607
+ var FLEET_REL_PATH = join27("projects", "llm-wiki", "architecture", "fleet.yaml");
4608
+ async function runFleetValidate(input) {
4609
+ const loaded = await loadFleetManifest(input.file);
4610
+ if (!loaded.ok) {
4611
+ if (loaded.error === "FILE_NOT_FOUND") {
4612
+ return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
4410
4613
  }
4411
- return null;
4614
+ const errors = fleetLoadErrors(loaded);
4615
+ return invalidFleet(errors);
4412
4616
  }
4617
+ const warnings = fleetWarnings(loaded.manifest);
4618
+ const snapshotter = findSnapshotter(loaded.manifest);
4619
+ return {
4620
+ exitCode: ExitCode.OK,
4621
+ result: ok({
4622
+ valid: true,
4623
+ errors: [],
4624
+ warnings,
4625
+ host_count: Object.keys(loaded.manifest.hosts).length,
4626
+ snapshotter,
4627
+ humanHint: `VALID fleet manifest (${Object.keys(loaded.manifest.hosts).length} hosts; snapshotter: ${snapshotter ?? "none"})`
4628
+ })
4629
+ };
4413
4630
  }
4414
- function parseRcloneFlags(pid) {
4415
- const flags = /* @__PURE__ */ new Map();
4416
- try {
4417
- const args = getRcloneArgs(pid);
4418
- for (let i = 0; i < args.length; i++) {
4419
- const arg = args[i];
4420
- if (arg.startsWith("--") && arg.includes("=")) {
4421
- const eq = arg.indexOf("=");
4422
- flags.set(arg.slice(0, eq), arg.slice(eq + 1));
4423
- } else if (arg.startsWith("--")) {
4424
- const next = args[i + 1];
4425
- if (next && !next.startsWith("-")) {
4426
- flags.set(arg, next);
4427
- i++;
4428
- } else {
4429
- flags.set(arg, "");
4430
- }
4431
- }
4432
- }
4433
- } catch {
4631
+ async function runFleetContext(input) {
4632
+ const env = input.env ?? process.env;
4633
+ const home = input.home ?? env.HOME ?? "";
4634
+ const cwd = input.cwd ?? process.cwd();
4635
+ const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
4636
+ const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
4637
+ const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
4638
+ const file = input.file ?? (vault ? join27(vault, FLEET_REL_PATH) : void 0);
4639
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
4640
+ const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
4641
+ if (!loaded.ok) {
4642
+ const warnings = ["fleet manifest unavailable or invalid"];
4643
+ const markdown2 = formatUnknownContext({
4644
+ generatedAt,
4645
+ osHostname,
4646
+ user,
4647
+ cwd,
4648
+ vault,
4649
+ reason: warnings[0],
4650
+ trace: [],
4651
+ warnings
4652
+ });
4653
+ return {
4654
+ exitCode: ExitCode.OK,
4655
+ result: ok({
4656
+ manifest_loaded: false,
4657
+ generated_at: generatedAt,
4658
+ identity_status: "unknown",
4659
+ resolver_trace: [],
4660
+ warnings,
4661
+ markdown: markdown2,
4662
+ humanHint: markdown2
4663
+ })
4664
+ };
4434
4665
  }
4435
- return flags;
4436
- }
4437
- function getRcloneVersion() {
4438
- try {
4439
- const out = execSync("rclone version", {
4440
- encoding: "utf8",
4441
- timeout: 3e3,
4442
- stdio: ["pipe", "pipe", "pipe"]
4666
+ const resolved = await resolveHostId({
4667
+ manifest: loaded.manifest,
4668
+ hostId: input.hostId,
4669
+ env,
4670
+ home,
4671
+ osHostname
4672
+ });
4673
+ if (resolved.hostId && !loaded.manifest.hosts[resolved.hostId]) {
4674
+ const source = resolved.source ?? "unknown";
4675
+ const warnings = [`resolved host id \`${resolved.hostId}\` from ${source} is not in fleet.yaml`];
4676
+ const markdown2 = formatInvalidContext({
4677
+ generatedAt,
4678
+ hostId: resolved.hostId,
4679
+ source,
4680
+ osHostname,
4681
+ user,
4682
+ cwd,
4683
+ vault,
4684
+ trace: resolved.trace,
4685
+ warnings
4443
4686
  });
4444
- const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
4445
- if (!match) return null;
4446
4687
  return {
4447
- major: parseInt(match[1], 10),
4448
- minor: parseInt(match[2], 10),
4449
- patch: parseInt(match[3], 10),
4450
- raw: out.split("\n")[0].trim()
4688
+ exitCode: ExitCode.OK,
4689
+ result: ok({
4690
+ manifest_loaded: true,
4691
+ host_id: resolved.hostId,
4692
+ source: resolved.source,
4693
+ generated_at: generatedAt,
4694
+ identity_status: "invalid",
4695
+ resolver_trace: resolved.trace,
4696
+ warnings,
4697
+ markdown: markdown2,
4698
+ humanHint: markdown2
4699
+ })
4451
4700
  };
4452
- } catch {
4453
- return null;
4454
4701
  }
4455
- }
4456
- function extractRcloneFs(args) {
4457
- let foundMount = false;
4458
- for (const arg of args) {
4459
- if (arg === "mount") {
4460
- foundMount = true;
4461
- continue;
4462
- }
4463
- if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
4464
- return arg;
4465
- }
4702
+ if (!resolved.hostId) {
4703
+ const warnings = ["host identity is unresolved"];
4704
+ const markdown2 = formatUnknownContext({
4705
+ generatedAt,
4706
+ osHostname,
4707
+ user,
4708
+ cwd,
4709
+ vault,
4710
+ reason: warnings[0],
4711
+ trace: resolved.trace,
4712
+ warnings
4713
+ });
4714
+ return {
4715
+ exitCode: ExitCode.OK,
4716
+ result: ok({
4717
+ manifest_loaded: true,
4718
+ generated_at: generatedAt,
4719
+ identity_status: "unknown",
4720
+ resolver_trace: resolved.trace,
4721
+ warnings,
4722
+ markdown: markdown2,
4723
+ humanHint: markdown2
4724
+ })
4725
+ };
4466
4726
  }
4467
- return null;
4727
+ const markdown = formatKnownContext({
4728
+ manifest: loaded.manifest,
4729
+ hostId: resolved.hostId,
4730
+ source: resolved.source,
4731
+ generatedAt,
4732
+ osHostname,
4733
+ user,
4734
+ cwd,
4735
+ vault,
4736
+ trace: resolved.trace
4737
+ });
4738
+ return {
4739
+ exitCode: ExitCode.OK,
4740
+ result: ok({
4741
+ manifest_loaded: true,
4742
+ host_id: resolved.hostId,
4743
+ source: resolved.source,
4744
+ generated_at: generatedAt,
4745
+ identity_status: "known",
4746
+ resolver_trace: resolved.trace,
4747
+ warnings: [],
4748
+ markdown,
4749
+ humanHint: markdown
4750
+ })
4751
+ };
4468
4752
  }
4469
- function getRcloneArgs(pid) {
4753
+ async function loadFleetManifest(file) {
4754
+ let text;
4470
4755
  try {
4471
- if (OS === "linux") {
4472
- const raw = readFileSync6(`/proc/${pid}/cmdline`);
4473
- return new TextDecoder().decode(raw).split("\0").filter(Boolean);
4474
- } else {
4475
- const out = execSync(`ps -o args= -p ${pid}`, {
4476
- encoding: "utf8",
4477
- timeout: 2e3,
4478
- stdio: ["pipe", "pipe", "pipe"]
4479
- }).trim();
4480
- return out.split(/\s+/);
4481
- }
4756
+ text = await readFile19(file, "utf8");
4482
4757
  } catch {
4483
- return [];
4758
+ return { ok: false, error: "FILE_NOT_FOUND" };
4484
4759
  }
4485
- }
4486
- function queryRcloneRC(rcAddr, fs) {
4760
+ let parsed;
4487
4761
  try {
4488
- const payload = JSON.stringify({ fs });
4489
- const out = execSync(
4490
- `curl -s --max-time 3 -X POST "http://${rcAddr}/vfs/stats" -H "Content-Type: application/json" -d '${payload}' 2>/dev/null`,
4491
- { encoding: "utf8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
4492
- );
4493
- if (!out.trim()) return null;
4494
- const data = JSON.parse(out);
4495
- if (data.status && data.status >= 400) {
4496
- return { error: data.error || `RC error (status ${data.status})`, erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
4497
- }
4498
- const dc = data.diskCache || {};
4499
- return {
4500
- erroredFiles: dc.erroredFiles ?? 0,
4501
- uploadsInProgress: dc.uploadsInProgress ?? 0,
4502
- uploadsQueued: dc.uploadsQueued ?? 0,
4503
- outOfSpace: dc.outOfSpace ?? false,
4504
- bytesUsed: dc.bytesUsed ?? 0,
4505
- files: dc.files ?? 0,
4506
- totalSize: data.totalSize || "unknown"
4507
- };
4508
- } catch {
4509
- return { error: "RC endpoint unreachable", erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
4762
+ parsed = yaml3.load(text, { schema: yaml3.JSON_SCHEMA });
4763
+ } catch (error) {
4764
+ return { ok: false, error: "INVALID_YAML", detail: error instanceof Error ? error.message : String(error) };
4510
4765
  }
4511
- }
4512
- function detectFuseMount(vaultPath) {
4766
+ const result = FleetManifestSchema.safeParse(parsed);
4767
+ if (!result.success) {
4768
+ return { ok: false, error: "INVALID_FLEET_MANIFEST", detail: result.error.issues };
4769
+ }
4770
+ return { ok: true, manifest: result.data };
4771
+ }
4772
+ function invalidFleet(errors) {
4773
+ return {
4774
+ exitCode: ExitCode.FLEET_MANIFEST_INVALID,
4775
+ result: ok({
4776
+ valid: false,
4777
+ errors,
4778
+ warnings: [],
4779
+ host_count: 0,
4780
+ humanHint: `INVALID fleet manifest
4781
+ ${errors.map((e) => ` ${e.path || "(root)"}: ${e.message}`).join("\n")}`
4782
+ })
4783
+ };
4784
+ }
4785
+ function fleetLoadErrors(loaded) {
4786
+ if (loaded.error === "INVALID_YAML") {
4787
+ return [{ path: "", message: `invalid YAML: ${String(loaded.detail ?? "parse failed")}` }];
4788
+ }
4789
+ if (loaded.error === "INVALID_FLEET_MANIFEST" && Array.isArray(loaded.detail)) {
4790
+ return loaded.detail.map((issue) => {
4791
+ const zodIssue = issue;
4792
+ return {
4793
+ path: (zodIssue.path ?? []).join("."),
4794
+ message: zodIssue.message ?? "invalid value"
4795
+ };
4796
+ });
4797
+ }
4798
+ return [{ path: "", message: loaded.error }];
4799
+ }
4800
+ function fleetWarnings(manifest) {
4801
+ const warnings = [];
4802
+ for (const [id, host] of Object.entries(manifest.hosts)) {
4803
+ if (host.role === "snapshotter" && host.protected !== true) {
4804
+ warnings.push(`snapshotter host '${id}' is not protected=true`);
4805
+ }
4806
+ }
4807
+ return warnings;
4808
+ }
4809
+ function findSnapshotter(manifest) {
4810
+ return Object.entries(manifest.hosts).find(([, host]) => host.role === "snapshotter")?.[0];
4811
+ }
4812
+ async function resolveHostId(input) {
4813
+ const trace = [];
4814
+ if (input.hostId) {
4815
+ trace.push({ source: "--host-id", status: "matched", value: input.hostId });
4816
+ return { hostId: input.hostId, source: "host-id", trace };
4817
+ }
4818
+ trace.push({ source: "--host-id", status: "unset" });
4819
+ if (input.env.SKILLWIKI_HOST_ID) {
4820
+ trace.push({ source: "SKILLWIKI_HOST_ID", status: "matched", value: input.env.SKILLWIKI_HOST_ID });
4821
+ return { hostId: input.env.SKILLWIKI_HOST_ID, source: "SKILLWIKI_HOST_ID", trace };
4822
+ }
4823
+ trace.push({ source: "SKILLWIKI_HOST_ID", status: "unset" });
4824
+ if (input.env.AGENT_HOST_ID) {
4825
+ trace.push({ source: "AGENT_HOST_ID", status: "matched", value: input.env.AGENT_HOST_ID });
4826
+ return { hostId: input.env.AGENT_HOST_ID, source: "AGENT_HOST_ID", trace };
4827
+ }
4828
+ trace.push({ source: "AGENT_HOST_ID", status: "unset" });
4829
+ if (input.home) {
4830
+ const dotenv = await parseDotenvFile(join27(input.home, ".skillwiki", ".env"));
4831
+ if (dotenv.SKILLWIKI_HOST_ID) {
4832
+ trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "matched", value: dotenv.SKILLWIKI_HOST_ID });
4833
+ return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", trace };
4834
+ }
4835
+ trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "unset" });
4836
+ } else {
4837
+ trace.push({ source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID", status: "skipped" });
4838
+ }
4839
+ if (input.env.VS_HOSTNAME) {
4840
+ trace.push({ source: "VS_HOSTNAME", status: "matched", value: input.env.VS_HOSTNAME });
4841
+ return { hostId: input.env.VS_HOSTNAME, source: "VS_HOSTNAME", trace };
4842
+ }
4843
+ trace.push({ source: "VS_HOSTNAME", status: "unset" });
4844
+ const hostname = input.osHostname.trim();
4845
+ if (hostname) {
4846
+ if (input.manifest.hosts[hostname]) {
4847
+ trace.push({ source: "hostname", status: "matched", value: hostname });
4848
+ return { hostId: hostname, source: "hostname", trace };
4849
+ }
4850
+ const byHostname = Object.entries(input.manifest.hosts).find(([, host]) => host.identity.hostnames.includes(hostname));
4851
+ if (byHostname) {
4852
+ trace.push({ source: "hostname", status: "matched", value: hostname });
4853
+ return { hostId: byHostname[0], source: "hostname", trace };
4854
+ }
4855
+ trace.push({ source: "hostname", status: "unmatched", value: hostname });
4856
+ } else {
4857
+ trace.push({ source: "hostname", status: "unset" });
4858
+ }
4859
+ return { trace };
4860
+ }
4861
+ function formatKnownContext(input) {
4862
+ const host = input.manifest.hosts[input.hostId];
4863
+ const protectedValue = host.protected === true ? "true" : "false";
4864
+ const writesTo = host.writes_to.join(", ");
4865
+ const selfAliases = collectSelfAliases(input.manifest, input.hostId);
4866
+ const outbound = collectOutboundAccess(input.manifest, input.hostId);
4867
+ const maintenanceLines = formatMaintenanceLines(host);
4868
+ const guidance = 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.`;
4869
+ return [
4870
+ "## Runtime Host Context",
4871
+ "",
4872
+ `- Context generated: \`${input.generatedAt}\``,
4873
+ `- Current machine: \`${input.hostId}\`${input.source ? ` (source: \`${input.source}\`)` : ""}`,
4874
+ "- Identity status: `known`",
4875
+ `- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
4876
+ `- Resolver trace: ${formatTrace(input.trace)}`,
4877
+ `- OS hostname: ${formatMaybe(input.osHostname)}`,
4878
+ `- User: ${formatMaybe(input.user)}`,
4879
+ `- Workspace: ${formatMaybe(input.cwd)}`,
4880
+ `- Vault: ${formatMaybe(input.vault)}`,
4881
+ "- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
4882
+ `- Fleet role: \`${host.role}\`; protected: \`${protectedValue}\`; writes_to: \`${writesTo}\``,
4883
+ ...maintenanceLines,
4884
+ `- Self SSH aliases known in fleet: ${formatList(selfAliases)}`,
4885
+ `- Declared outbound SSH from this source: ${formatOutboundAccess(outbound)}`,
4886
+ `- Guidance: ${guidance}`
4887
+ ].join("\n");
4888
+ }
4889
+ function formatUnknownContext(input) {
4890
+ return [
4891
+ "## Runtime Host Context",
4892
+ "",
4893
+ `- Context generated: \`${input.generatedAt}\``,
4894
+ "- Current machine: unknown",
4895
+ "- Identity status: `unknown`",
4896
+ `- Resolver trace: ${formatTrace(input.trace)}`,
4897
+ `- Warnings: ${formatWarnings(input.warnings)}`,
4898
+ `- OS hostname: ${formatMaybe(input.osHostname)}`,
4899
+ `- User: ${formatMaybe(input.user)}`,
4900
+ `- Workspace: ${formatMaybe(input.cwd)}`,
4901
+ `- Vault: ${formatMaybe(input.vault)}`,
4902
+ "- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
4903
+ "- Fleet role: unknown",
4904
+ "- Self SSH aliases known in fleet: unknown",
4905
+ "- Declared outbound SSH from this source: unknown",
4906
+ `- Guidance: ${input.reason}; do not assume local vs remote role. Inspect runtime or ask before SSH/deploy/sync work.`
4907
+ ].join("\n");
4908
+ }
4909
+ function formatInvalidContext(input) {
4910
+ return [
4911
+ "## Runtime Host Context",
4912
+ "",
4913
+ `- Context generated: \`${input.generatedAt}\``,
4914
+ "- Current machine: unknown",
4915
+ "- Identity status: `invalid`",
4916
+ `- Identity resolution: ${formatResolution(input.source, input.hostId)}`,
4917
+ `- Resolver trace: ${formatTrace(input.trace)}`,
4918
+ `- Warnings: ${formatWarnings(input.warnings)}`,
4919
+ `- OS hostname: ${formatMaybe(input.osHostname)}`,
4920
+ `- User: ${formatMaybe(input.user)}`,
4921
+ `- Workspace: ${formatMaybe(input.cwd)}`,
4922
+ `- Vault: ${formatMaybe(input.vault)}`,
4923
+ "- Remote freshness: not checked by `fleet context`; run `sync status` or presync before host-sensitive work.",
4924
+ "- Fleet role: unknown",
4925
+ "- Self SSH aliases known in fleet: unknown",
4926
+ "- Declared outbound SSH from this source: unknown",
4927
+ `- Guidance: do not trust this identity; rerun with \`--host-id\` only if the user confirms \`${input.hostId}\` is the current fleet host id.`
4928
+ ].join("\n");
4929
+ }
4930
+ function collectSelfAliases(manifest, hostId2) {
4931
+ const aliases = [];
4932
+ const host = manifest.hosts[hostId2];
4933
+ const access = host?.access?.from ?? {};
4934
+ for (const profile of Object.values(access)) {
4935
+ for (const alias of profile.ssh_aliases ?? []) aliases.push(alias);
4936
+ }
4937
+ return [...new Set(aliases)];
4938
+ }
4939
+ function collectOutboundAccess(manifest, sourceHostId) {
4940
+ const hosts = [];
4941
+ for (const [targetId, target] of Object.entries(manifest.hosts)) {
4942
+ if (targetId === sourceHostId) continue;
4943
+ const profile = target.access?.from?.[sourceHostId];
4944
+ if (profile && (profile.status === "configured" || profile.status === "local")) {
4945
+ hosts.push({
4946
+ hostId: targetId,
4947
+ sshAliases: [...new Set(profile.ssh_aliases ?? [])],
4948
+ users: [...new Set(profile.users ?? [])]
4949
+ });
4950
+ }
4951
+ }
4952
+ return hosts.sort((left, right) => left.hostId.localeCompare(right.hostId));
4953
+ }
4954
+ function formatMaintenanceLines(host) {
4955
+ const satellite = host.maintenance?.skillwiki_satellite;
4956
+ if (!satellite?.enabled) return [];
4957
+ return [
4958
+ `- Maintenance role: \`skillwiki satellite\`; user: \`${satellite.user}\`; ssh: \`${satellite.ssh_alias}\``,
4959
+ `- Maintenance paths: maintenance vault: \`${satellite.vault_path}\`; repo: \`${satellite.repo_path}\`; scheduler: \`${satellite.scheduler}\`; jobs: ${formatList(satellite.jobs)}`
4960
+ ];
4961
+ }
4962
+ function formatOutboundAccess(values) {
4963
+ if (values.length === 0) return "none";
4964
+ return values.map((value) => {
4965
+ const aliasPart = value.sshAliases.length > 0 ? ` via ${formatList(value.sshAliases)}` : " (no SSH aliases)";
4966
+ const usersPart = value.users.length > 0 ? ` (users: ${formatList(value.users)})` : "";
4967
+ return `\`${value.hostId}\`${aliasPart}${usersPart}`;
4968
+ }).join("; ");
4969
+ }
4970
+ function formatResolution(source, hostId2) {
4971
+ return source ? `\`${source === "host-id" ? "--host-id" : source}\` -> \`${hostId2}\`` : `unknown -> \`${hostId2}\``;
4972
+ }
4973
+ function formatTrace(values) {
4974
+ if (values.length === 0) return "not available";
4975
+ return values.map((value) => {
4976
+ const source = `\`${value.source}\``;
4977
+ if (value.status === "matched") return `${source} matched \`${value.value ?? ""}\``;
4978
+ if (value.status === "unmatched") return `${source} unmatched \`${value.value ?? ""}\``;
4979
+ return `${source} ${value.status}`;
4980
+ }).join("; ");
4981
+ }
4982
+ function formatWarnings(values) {
4983
+ return values.length > 0 ? values.join("; ") : "none";
4984
+ }
4985
+ function formatList(values) {
4986
+ return values.length > 0 ? values.map((v) => `\`${v}\``).join(", ") : "none";
4987
+ }
4988
+ function formatMaybe(value) {
4989
+ return value && value.trim().length > 0 ? `\`${value}\`` : "unknown";
4990
+ }
4991
+ function safeEnvValue(value) {
4992
+ return value && value.trim().length > 0 ? value : void 0;
4993
+ }
4994
+ function safeUserName() {
4995
+ try {
4996
+ return userInfo().username;
4997
+ } catch {
4998
+ return "";
4999
+ }
5000
+ }
5001
+
5002
+ // src/utils/s3-mount-health.ts
5003
+ import { execSync } from "child_process";
5004
+ import { platform } from "os";
5005
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, unlinkSync as unlinkSync4, readFileSync as readFile20 } from "fs";
5006
+ import { join as join28 } from "path";
5007
+ var OS = platform();
5008
+ function findRcloneMountPid() {
5009
+ try {
5010
+ const out = execSync("pgrep -f 'rclone.*mount'", {
5011
+ encoding: "utf8",
5012
+ timeout: 2e3,
5013
+ stdio: ["pipe", "pipe", "pipe"]
5014
+ }).trim();
5015
+ const pids = out.split("\n").filter(Boolean);
5016
+ if (pids.length === 0) return null;
5017
+ return parseInt(pids[0], 10);
5018
+ } catch {
5019
+ try {
5020
+ const out = execSync("ps aux", { encoding: "utf8", timeout: 2e3, stdio: ["pipe", "pipe", "pipe"] });
5021
+ for (const line of out.split("\n")) {
5022
+ if (line.includes("rclone") && line.includes("mount") && !line.includes("grep")) {
5023
+ const parts = line.trim().split(/\s+/);
5024
+ if (parts.length >= 2) return parseInt(parts[1], 10);
5025
+ }
5026
+ }
5027
+ } catch {
5028
+ }
5029
+ return null;
5030
+ }
5031
+ }
5032
+ function parseRcloneFlags(pid) {
5033
+ const flags = /* @__PURE__ */ new Map();
5034
+ try {
5035
+ const args = getRcloneArgs(pid);
5036
+ for (let i = 0; i < args.length; i++) {
5037
+ const arg = args[i];
5038
+ if (arg.startsWith("--") && arg.includes("=")) {
5039
+ const eq = arg.indexOf("=");
5040
+ flags.set(arg.slice(0, eq), arg.slice(eq + 1));
5041
+ } else if (arg.startsWith("--")) {
5042
+ const next = args[i + 1];
5043
+ if (next && !next.startsWith("-")) {
5044
+ flags.set(arg, next);
5045
+ i++;
5046
+ } else {
5047
+ flags.set(arg, "");
5048
+ }
5049
+ }
5050
+ }
5051
+ } catch {
5052
+ }
5053
+ return flags;
5054
+ }
5055
+ function getRcloneVersion() {
5056
+ try {
5057
+ const out = execSync("rclone version", {
5058
+ encoding: "utf8",
5059
+ timeout: 3e3,
5060
+ stdio: ["pipe", "pipe", "pipe"]
5061
+ });
5062
+ const match = out.match(/rclone\s+v(\d+)\.(\d+)\.(\d+)/i);
5063
+ if (!match) return null;
5064
+ return {
5065
+ major: parseInt(match[1], 10),
5066
+ minor: parseInt(match[2], 10),
5067
+ patch: parseInt(match[3], 10),
5068
+ raw: out.split("\n")[0].trim()
5069
+ };
5070
+ } catch {
5071
+ return null;
5072
+ }
5073
+ }
5074
+ function extractRcloneFs(args) {
5075
+ let foundMount = false;
5076
+ for (const arg of args) {
5077
+ if (arg === "mount") {
5078
+ foundMount = true;
5079
+ continue;
5080
+ }
5081
+ if (foundMount && arg.includes(":") && !arg.startsWith("-") && !arg.startsWith("/")) {
5082
+ return arg;
5083
+ }
5084
+ }
5085
+ return null;
5086
+ }
5087
+ function getRcloneArgs(pid) {
5088
+ try {
5089
+ if (OS === "linux") {
5090
+ const raw = readFileSync6(`/proc/${pid}/cmdline`);
5091
+ return new TextDecoder().decode(raw).split("\0").filter(Boolean);
5092
+ } else {
5093
+ const out = execSync(`ps -o args= -p ${pid}`, {
5094
+ encoding: "utf8",
5095
+ timeout: 2e3,
5096
+ stdio: ["pipe", "pipe", "pipe"]
5097
+ }).trim();
5098
+ return out.split(/\s+/);
5099
+ }
5100
+ } catch {
5101
+ return [];
5102
+ }
5103
+ }
5104
+ function queryRcloneRC(rcAddr, fs) {
5105
+ try {
5106
+ const payload = JSON.stringify({ fs });
5107
+ const out = execSync(
5108
+ `curl -s --max-time 3 -X POST "http://${rcAddr}/vfs/stats" -H "Content-Type: application/json" -d '${payload}' 2>/dev/null`,
5109
+ { encoding: "utf8", timeout: 5e3, stdio: ["pipe", "pipe", "pipe"] }
5110
+ );
5111
+ if (!out.trim()) return null;
5112
+ const data = JSON.parse(out);
5113
+ if (data.status && data.status >= 400) {
5114
+ return { error: data.error || `RC error (status ${data.status})`, erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
5115
+ }
5116
+ const dc = data.diskCache || {};
5117
+ return {
5118
+ erroredFiles: dc.erroredFiles ?? 0,
5119
+ uploadsInProgress: dc.uploadsInProgress ?? 0,
5120
+ uploadsQueued: dc.uploadsQueued ?? 0,
5121
+ outOfSpace: dc.outOfSpace ?? false,
5122
+ bytesUsed: dc.bytesUsed ?? 0,
5123
+ files: dc.files ?? 0,
5124
+ totalSize: data.totalSize || "unknown"
5125
+ };
5126
+ } catch {
5127
+ return { error: "RC endpoint unreachable", erroredFiles: 0, uploadsInProgress: 0, uploadsQueued: 0, outOfSpace: false, bytesUsed: 0, files: 0, totalSize: "unknown" };
5128
+ }
5129
+ }
5130
+ function detectFuseMount(vaultPath) {
4513
5131
  try {
4514
5132
  if (OS === "linux") {
4515
5133
  const mounts = readFileSync6("/proc/mounts", "utf8");
@@ -4543,7 +5161,7 @@ function detectFuseMount(vaultPath) {
4543
5161
  return null;
4544
5162
  }
4545
5163
  function writeTest(dir) {
4546
- const testFile = join27(dir, `.doctor-write-test-${process.pid}.tmp`);
5164
+ const testFile = join28(dir, `.doctor-write-test-${process.pid}.tmp`);
4547
5165
  const payload = `skillwiki doctor write test \u2014 ${Date.now()} \u2014 ${Math.random().toString(36).slice(2)}`;
4548
5166
  const start = Date.now();
4549
5167
  try {
@@ -4554,7 +5172,7 @@ function writeTest(dir) {
4554
5172
  const writeMs = Date.now() - start;
4555
5173
  const readStart = Date.now();
4556
5174
  try {
4557
- const back = readFile19(testFile, "utf8");
5175
+ const back = readFile20(testFile, "utf8");
4558
5176
  const readMs = Date.now() - readStart;
4559
5177
  if (back !== payload) {
4560
5178
  try {
@@ -4643,12 +5261,12 @@ function detectCliChannels(argv, home) {
4643
5261
  }
4644
5262
  const plugin = findPlugin(home);
4645
5263
  if (plugin) {
4646
- const pluginBin = join28(plugin.installPath, "bin", "skillwiki");
5264
+ const pluginBin = join29(plugin.installPath, "bin", "skillwiki");
4647
5265
  if (existsSync11(pluginBin)) {
4648
5266
  channels.push({ name: "plugin", path: pluginBin, isDevLink: false });
4649
5267
  }
4650
5268
  }
4651
- const installBin = join28(home, ".claude", "skills", "bin", "skillwiki");
5269
+ const installBin = join29(home, ".claude", "skills", "bin", "skillwiki");
4652
5270
  if (existsSync11(installBin)) {
4653
5271
  channels.push({ name: "install", path: installBin, isDevLink: false });
4654
5272
  }
@@ -4729,9 +5347,9 @@ function checkVaultStructure(resolvedPath) {
4729
5347
  return check("error", "vault_structure", "Vault structure valid", "Cannot check \u2014 vault directory does not exist");
4730
5348
  }
4731
5349
  const missing = [];
4732
- if (!existsSync11(join28(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
5350
+ if (!existsSync11(join29(resolvedPath, "SCHEMA.md"))) missing.push("SCHEMA.md");
4733
5351
  for (const dir of ["raw", "entities", "concepts", "meta"]) {
4734
- if (!existsSync11(join28(resolvedPath, dir))) missing.push(dir + "/");
5352
+ if (!existsSync11(join29(resolvedPath, dir))) missing.push(dir + "/");
4735
5353
  }
4736
5354
  if (missing.length === 0) {
4737
5355
  return check("pass", "vault_structure", "Vault structure valid", "All required files and directories present");
@@ -4739,7 +5357,7 @@ function checkVaultStructure(resolvedPath) {
4739
5357
  return check("warn", "vault_structure", "Vault structure valid", `Missing: ${missing.join(", ")} \u2014 run \`skillwiki init\` to add CodeWiki structure`);
4740
5358
  }
4741
5359
  function checkSkillsInstalled(home, cwd) {
4742
- const srcDir = cwd ? join28(cwd, "packages", "skills") : void 0;
5360
+ const srcDir = cwd ? join29(cwd, "packages", "skills") : void 0;
4743
5361
  if (srcDir && existsSync11(srcDir)) {
4744
5362
  const found = findInstalledSkillMd(srcDir);
4745
5363
  if (found.length > 0) {
@@ -4753,7 +5371,7 @@ function checkSkillsInstalled(home, cwd) {
4753
5371
  return check("pass", "skills_installed", "Skills installed", `${found.length} SKILL.md file(s) found (plugin v${plugin.version})`);
4754
5372
  }
4755
5373
  }
4756
- const skillsDir = join28(home, ".claude", "skills");
5374
+ const skillsDir = join29(home, ".claude", "skills");
4757
5375
  if (existsSync11(skillsDir)) {
4758
5376
  const found = findInstalledSkillMd(skillsDir);
4759
5377
  if (found.length > 0) {
@@ -4764,10 +5382,10 @@ function checkSkillsInstalled(home, cwd) {
4764
5382
  }
4765
5383
  function checkDuplicateSkills(home) {
4766
5384
  const plugin = findPlugin(home);
4767
- const skillsDir = join28(home, ".claude", "skills");
5385
+ const skillsDir = join29(home, ".claude", "skills");
4768
5386
  const agentSkillDirs = [
4769
- { label: "~/.codex/skills/", path: join28(home, ".codex", "skills") },
4770
- { label: "~/.agents/skills/", path: join28(home, ".agents", "skills") }
5387
+ { label: "~/.codex/skills/", path: join29(home, ".codex", "skills") },
5388
+ { label: "~/.agents/skills/", path: join29(home, ".agents", "skills") }
4771
5389
  ];
4772
5390
  if (!plugin) {
4773
5391
  return check("pass", "skills_duplicate", "Skills not duplicated", "Single install channel");
@@ -4866,7 +5484,7 @@ async function checkProfiles(home) {
4866
5484
  }
4867
5485
  async function checkProjectLocalOverride(cwd) {
4868
5486
  const dir = cwd ?? process.cwd();
4869
- const envPath = join28(dir, ".skillwiki", ".env");
5487
+ const envPath = join29(dir, ".skillwiki", ".env");
4870
5488
  if (existsSync11(envPath)) {
4871
5489
  return check("pass", "project_local", "Project-local config", `Found: ${envPath}`);
4872
5490
  }
@@ -4876,7 +5494,7 @@ function checkVaultGitRemote(resolvedPath) {
4876
5494
  if (resolvedPath === void 0) {
4877
5495
  return check("error", "vault_git_remote", "Vault git remote", "Cannot check \u2014 WIKI_PATH not resolved");
4878
5496
  }
4879
- if (!existsSync11(join28(resolvedPath, ".git"))) {
5497
+ if (!existsSync11(join29(resolvedPath, ".git"))) {
4880
5498
  return check("warn", "vault_git_remote", "Vault git remote", "Vault is not a git repository \u2014 sync features unavailable");
4881
5499
  }
4882
5500
  try {
@@ -4899,9 +5517,9 @@ function checkObsidianTemplates(resolvedPath) {
4899
5517
  return check("error", "obsidian_templates", "Obsidian templates", "Cannot check \u2014 WIKI_PATH not resolved");
4900
5518
  }
4901
5519
  const missing = [];
4902
- if (!existsSync11(join28(resolvedPath, "_Templates"))) missing.push("_Templates/");
4903
- if (!existsSync11(join28(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
4904
- if (!existsSync11(join28(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
5520
+ if (!existsSync11(join29(resolvedPath, "_Templates"))) missing.push("_Templates/");
5521
+ if (!existsSync11(join29(resolvedPath, ".obsidian", "templates.json"))) missing.push(".obsidian/templates.json");
5522
+ if (!existsSync11(join29(resolvedPath, ".obsidian", "app.json"))) missing.push(".obsidian/app.json");
4905
5523
  if (missing.length === 0) {
4906
5524
  return check("pass", "obsidian_templates", "Obsidian templates", "Template folder and config present");
4907
5525
  }
@@ -4911,7 +5529,7 @@ function checkDotStoreClean(resolvedPath) {
4911
5529
  if (resolvedPath === void 0) {
4912
5530
  return check("error", "dsstore_clean", "No .DS_Store in raw/", "Cannot check \u2014 WIKI_PATH not resolved");
4913
5531
  }
4914
- const rawDir = join28(resolvedPath, "raw");
5532
+ const rawDir = join29(resolvedPath, "raw");
4915
5533
  if (!existsSync11(rawDir)) {
4916
5534
  return check("pass", "dsstore_clean", "No .DS_Store in raw/", "raw/ directory not found \u2014 check skipped");
4917
5535
  }
@@ -4927,7 +5545,7 @@ function checkDotStoreClean(resolvedPath) {
4927
5545
  if (entry.name === ".DS_Store") {
4928
5546
  found.push(rel ? `${rel}/.DS_Store` : ".DS_Store");
4929
5547
  } else if (entry.isDirectory()) {
4930
- walk2(join28(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
5548
+ walk2(join29(dir, entry.name), rel ? `${rel}/${entry.name}` : entry.name);
4931
5549
  }
4932
5550
  }
4933
5551
  })(rawDir, "");
@@ -4940,7 +5558,7 @@ function checkSyncLastPush(resolvedPath) {
4940
5558
  if (resolvedPath === void 0) {
4941
5559
  return check("error", "sync_last_push", "Vault sync recency", "Cannot check \u2014 WIKI_PATH not resolved");
4942
5560
  }
4943
- if (!existsSync11(join28(resolvedPath, ".git"))) {
5561
+ if (!existsSync11(join29(resolvedPath, ".git"))) {
4944
5562
  return check("pass", "sync_last_push", "Vault sync recency", "No git repo \u2014 sync check skipped");
4945
5563
  }
4946
5564
  let timestamp;
@@ -4988,7 +5606,7 @@ function checkVaultGitDirty(resolvedPath) {
4988
5606
  if (resolvedPath === void 0) {
4989
5607
  return check("pass", "vault_git_dirty", "Vault git dirty state", "No vault path \u2014 check skipped");
4990
5608
  }
4991
- if (!existsSync11(join28(resolvedPath, ".git"))) {
5609
+ if (!existsSync11(join29(resolvedPath, ".git"))) {
4992
5610
  return check("pass", "vault_git_dirty", "Vault git dirty state", "No git repo \u2014 check skipped");
4993
5611
  }
4994
5612
  try {
@@ -5029,7 +5647,7 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
5029
5647
  if (resolvedPath === void 0) {
5030
5648
  return check("pass", id, label, "No vault path \u2014 check skipped");
5031
5649
  }
5032
- if (!existsSync11(join28(resolvedPath, ".git"))) {
5650
+ if (!existsSync11(join29(resolvedPath, ".git"))) {
5033
5651
  return check("pass", id, label, "No git repo \u2014 check skipped");
5034
5652
  }
5035
5653
  if (!hasOriginMain(resolvedPath)) {
@@ -5049,13 +5667,38 @@ function checkVaultGitComparison(resolvedPath, id, label, range, nonZeroSuffix,
5049
5667
  return check("warn", id, label, "Could not compare HEAD with origin/main");
5050
5668
  }
5051
5669
  }
5670
+ async function checkFleetIdentity(input) {
5671
+ if (!input.vaultPath) {
5672
+ return check("pass", "fleet_identity", "Fleet identity", "No vault path \u2014 check skipped");
5673
+ }
5674
+ const r = await runFleetContext({
5675
+ vault: input.vaultPath,
5676
+ env: { ...process.env, WIKI_PATH: input.envValue ?? input.vaultPath },
5677
+ home: input.home,
5678
+ cwd: input.cwd ?? process.cwd(),
5679
+ osHostname: process.env.HOSTNAME,
5680
+ user: process.env.USER
5681
+ });
5682
+ if (!r.result.ok) {
5683
+ return check("warn", "fleet_identity", "Fleet identity", "Could not evaluate fleet identity");
5684
+ }
5685
+ const data = r.result.data;
5686
+ if (!data.manifest_loaded) {
5687
+ return check("pass", "fleet_identity", "Fleet identity", "Fleet manifest unavailable \u2014 check skipped");
5688
+ }
5689
+ if (data.identity_status === "known") {
5690
+ return check("pass", "fleet_identity", "Fleet identity", `Resolved ${data.host_id ?? "unknown"} via ${data.source ?? "unknown"}`);
5691
+ }
5692
+ const detail = data.warnings.length > 0 ? data.warnings.join("; ") : "Fleet identity is unresolved";
5693
+ return check("warn", "fleet_identity", "Fleet identity", detail);
5694
+ }
5052
5695
  function pullLogPaths(home) {
5053
5696
  const paths = platform2() === "darwin" ? [
5054
- join28(home, "Library", "Logs", "wiki-pull.log"),
5055
- join28(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
5697
+ join29(home, "Library", "Logs", "wiki-pull.log"),
5698
+ join29(home, ".local", "state", "vault-sync", "log", "wiki-pull.log")
5056
5699
  ] : [
5057
- join28(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
5058
- join28(home, "Library", "Logs", "wiki-pull.log")
5700
+ join29(home, ".local", "state", "vault-sync", "log", "wiki-pull.log"),
5701
+ join29(home, "Library", "Logs", "wiki-pull.log")
5059
5702
  ];
5060
5703
  return [...new Set(paths)];
5061
5704
  }
@@ -5095,7 +5738,7 @@ function checkS3MountPerf(resolvedPath) {
5095
5738
  return check("pass", "s3_mount_perf", "S3 mount performance", "local disk");
5096
5739
  }
5097
5740
  const mountPoint = fuse.mountPoint;
5098
- const conceptsDir = join28(resolvedPath, "concepts");
5741
+ const conceptsDir = join29(resolvedPath, "concepts");
5099
5742
  if (!existsSync11(conceptsDir)) {
5100
5743
  return check("pass", "s3_mount_perf", "S3 mount performance", `S3 FUSE mount (${mountPoint}), no concepts/ to benchmark`);
5101
5744
  }
@@ -5278,7 +5921,7 @@ function checkWriteTest(resolvedPath) {
5278
5921
  if (!fuse) {
5279
5922
  return check("pass", "s3_write_test", "S3 write test", "local disk \u2014 check skipped");
5280
5923
  }
5281
- const conceptsDir = join28(resolvedPath, "concepts");
5924
+ const conceptsDir = join29(resolvedPath, "concepts");
5282
5925
  if (!existsSync11(conceptsDir)) {
5283
5926
  return check("pass", "s3_write_test", "S3 write test", "no concepts/ dir to test \u2014 check skipped");
5284
5927
  }
@@ -5365,7 +6008,7 @@ function checkVfsCacheHealth(resolvedPath) {
5365
6008
  }
5366
6009
  function readVaultSyncConfig(home) {
5367
6010
  try {
5368
- const content = readFileSync7(join28(home, ".skillwiki", ".env"), "utf8");
6011
+ const content = readFileSync7(join29(home, ".skillwiki", ".env"), "utf8");
5369
6012
  let installed = false;
5370
6013
  let role;
5371
6014
  for (const line of content.split(/\r?\n/)) {
@@ -5399,11 +6042,11 @@ function vaultSyncChecks(input) {
5399
6042
  ];
5400
6043
  }
5401
6044
  const isMac = os === "darwin";
5402
- const logDir = input.logDir ?? (isMac ? join28(home, "Library", "Logs") : join28(home, ".local", "state", "vault-sync", "log"));
5403
- const shareDir = input.shareDir ?? (isMac ? join28(home, "Library", "Application Support", "vault-sync", "bin") : join28(home, ".local", "share", "vault-sync", "bin"));
5404
- const filterPath = input.filterPath ?? join28(home, ".config", "rclone", "wiki-push-filters.txt");
6045
+ const logDir = input.logDir ?? (isMac ? join29(home, "Library", "Logs") : join29(home, ".local", "state", "vault-sync", "log"));
6046
+ const shareDir = input.shareDir ?? (isMac ? join29(home, "Library", "Application Support", "vault-sync", "bin") : join29(home, ".local", "share", "vault-sync", "bin"));
6047
+ const filterPath = input.filterPath ?? join29(home, ".config", "rclone", "wiki-push-filters.txt");
5405
6048
  const snapshotPath = input.snapshotScriptPath ?? "/root/.hermes/scripts/wiki-snapshot-v3.sh";
5406
- const pushScriptPath = join28(shareDir, "wiki-push.sh");
6049
+ const pushScriptPath = join29(shareDir, "wiki-push.sh");
5407
6050
  const c1 = existsSync11(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`);
5408
6051
  let c2;
5409
6052
  try {
@@ -5455,7 +6098,7 @@ function vaultSyncChecks(input) {
5455
6098
  "Scheduler check failed \u2014 run vault-sync-install"
5456
6099
  );
5457
6100
  }
5458
- const logFile = join28(logDir, "wiki-push.log");
6101
+ const logFile = join29(logDir, "wiki-push.log");
5459
6102
  let c3;
5460
6103
  try {
5461
6104
  const logContent = readFileSync7(logFile, "utf8");
@@ -5526,7 +6169,7 @@ function vaultSyncChecks(input) {
5526
6169
  `Log directory not found at ${logDir}`
5527
6170
  );
5528
6171
  }
5529
- const fetchLogFile = join28(logDir, "wiki-fetch.log");
6172
+ const fetchLogFile = join29(logDir, "wiki-fetch.log");
5530
6173
  let cFetch;
5531
6174
  try {
5532
6175
  const logContent = readFileSync7(fetchLogFile, "utf8");
@@ -5668,15 +6311,15 @@ function findSkillMd(dir) {
5668
6311
  }
5669
6312
  for (const entry of entries) {
5670
6313
  if (entry.isFile() && entry.name === "SKILL.md") {
5671
- results.push(join28(dir, entry.name));
6314
+ results.push(join29(dir, entry.name));
5672
6315
  } else if (entry.isDirectory()) {
5673
- results.push(...findSkillMd(join28(dir, entry.name)));
6316
+ results.push(...findSkillMd(join29(dir, entry.name)));
5674
6317
  }
5675
6318
  }
5676
6319
  return results;
5677
6320
  }
5678
6321
  function findInstalledSkillMd(dir) {
5679
- const directSkills = findSkillNames(dir).map((name) => join28(dir, name, "SKILL.md"));
6322
+ const directSkills = findSkillNames(dir).map((name) => join29(dir, name, "SKILL.md"));
5680
6323
  return directSkills.length > 0 ? directSkills : findSkillMd(dir);
5681
6324
  }
5682
6325
  function findSkillNames(dir) {
@@ -5688,7 +6331,7 @@ function findSkillNames(dir) {
5688
6331
  return results;
5689
6332
  }
5690
6333
  for (const entry of entries) {
5691
- if (entry.isDirectory() && existsSync11(join28(dir, entry.name, "SKILL.md"))) {
6334
+ if (entry.isDirectory() && existsSync11(join29(dir, entry.name, "SKILL.md"))) {
5692
6335
  results.push(entry.name);
5693
6336
  }
5694
6337
  }
@@ -5732,7 +6375,7 @@ async function vaultMetrics(resolvedPath) {
5732
6375
  }
5733
6376
  let logLines = 0;
5734
6377
  try {
5735
- logLines = readFileSync7(join28(resolvedPath, "log.md"), "utf8").split("\n").length;
6378
+ logLines = readFileSync7(join29(resolvedPath, "log.md"), "utf8").split("\n").length;
5736
6379
  } catch {
5737
6380
  }
5738
6381
  return [
@@ -5762,6 +6405,12 @@ async function runDoctor(input) {
5762
6405
  checks.push(checkVaultStructure(resolvedPath));
5763
6406
  checks.push(checkObsidianTemplates(resolvedPath));
5764
6407
  checks.push(checkVaultGitRemote(resolvedPath));
6408
+ checks.push(await checkFleetIdentity({
6409
+ vaultPath: resolvedPath,
6410
+ home: input.home,
6411
+ cwd: input.cwd,
6412
+ envValue: input.envValue
6413
+ }));
5765
6414
  checks.push(checkSyncLastPush(resolvedPath));
5766
6415
  checks.push(checkVaultGitDirty(resolvedPath));
5767
6416
  checks.push(checkVaultGitAhead(resolvedPath));
@@ -5935,27 +6584,27 @@ function runVaultSyncHealth(home, syncMode) {
5935
6584
  };
5936
6585
  }
5937
6586
  const isMac = platform3() === "darwin";
5938
- const shareDir = isMac ? join29(home, "Library", "Application Support", "vault-sync", "bin") : join29(home, ".local", "share", "vault-sync", "bin");
5939
- const logDir = isMac ? join29(home, "Library", "Logs") : join29(home, ".local", "state", "vault-sync", "log");
5940
- const filterPath = join29(home, ".config", "rclone", "wiki-push-filters.txt");
6587
+ const shareDir = isMac ? join30(home, "Library", "Application Support", "vault-sync", "bin") : join30(home, ".local", "share", "vault-sync", "bin");
6588
+ const logDir = isMac ? join30(home, "Library", "Logs") : join30(home, ".local", "state", "vault-sync", "log");
6589
+ const filterPath = join30(home, ".config", "rclone", "wiki-push-filters.txt");
5941
6590
  const checks = [];
5942
- const pushScript = join29(shareDir, "wiki-push.sh");
6591
+ const pushScript = join30(shareDir, "wiki-push.sh");
5943
6592
  checks.push(existsSync12(pushScript) ? { id: "vault_sync_installed", label: "Vault sync installed", status: "pass", detail: `Found: ${pushScript}` } : { id: "vault_sync_installed", label: "Vault sync installed", status: "error", detail: `Script missing: ${pushScript}` });
5944
6593
  if (isMac) {
5945
- const pushPlist = join29(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
5946
- const fetchPlist = join29(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
6594
+ const pushPlist = join30(home, "Library", "LaunchAgents", "com.karlchow.wiki-push.plist");
6595
+ const fetchPlist = join30(home, "Library", "LaunchAgents", "com.karlchow.wiki-fetch.plist");
5947
6596
  checks.push(existsSync12(pushPlist) && existsSync12(fetchPlist) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "launchd unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "launchd unit files missing (read-only mode)" });
5948
6597
  checks.push({ id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "macOS host \u2014 check skipped" });
5949
6598
  } else {
5950
- const pushTimer = join29(home, ".config", "systemd", "user", "wiki-push.timer");
5951
- const fetchTimer = join29(home, ".config", "systemd", "user", "wiki-fetch.timer");
5952
- const fuseTimer = join29(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
5953
- const fuseService = join29(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
6599
+ const pushTimer = join30(home, ".config", "systemd", "user", "wiki-push.timer");
6600
+ const fetchTimer = join30(home, ".config", "systemd", "user", "wiki-fetch.timer");
6601
+ const fuseTimer = join30(home, ".config", "systemd", "user", "wiki-fuse-refresh.timer");
6602
+ const fuseService = join30(home, ".config", "systemd", "user", "wiki-fuse-refresh.service");
5954
6603
  checks.push(existsSync12(pushTimer) && existsSync12(fetchTimer) ? { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "pass", detail: "systemd timer unit files present (read-only mode)" } : { id: "vault_sync_jobs_enabled", label: "Vault sync jobs enabled", status: "warn", detail: "systemd timer unit files missing (read-only mode)" });
5955
6604
  checks.push(existsSync12(fuseTimer) && existsSync12(fuseService) ? { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "pass", detail: "wiki-fuse-refresh unit files present (read-only mode)" } : { id: "vault_sync_fuse_refresh_job", label: "Vault sync fuse refresh job", status: "warn", detail: "wiki-fuse-refresh unit files missing (read-only mode)" });
5956
6605
  }
5957
- checks.push(classifyLog(join29(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
5958
- checks.push(classifyLog(join29(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
6606
+ checks.push(classifyLog(join30(logDir, "wiki-push.log"), "vault_sync_last_push_age", "Vault sync last push recency", /OK push/));
6607
+ checks.push(classifyLog(join30(logDir, "wiki-fetch.log"), "vault_sync_last_fetch_status", "Vault sync last fetch status", /NOTIFY|OK behind|OK/));
5959
6608
  if (!existsSync12(filterPath)) {
5960
6609
  checks.push({ id: "vault_sync_filter_present", label: "Vault sync filter file present", status: "error", detail: `Filter missing: ${filterPath}` });
5961
6610
  } else {
@@ -6181,8 +6830,8 @@ async function runHealth(input) {
6181
6830
  }
6182
6831
 
6183
6832
  // src/commands/archive.ts
6184
- import { rename as rename7, mkdir as mkdir9, readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
6185
- import { join as join30, dirname as dirname11 } from "path";
6833
+ import { rename as rename7, mkdir as mkdir9, readFile as readFile21, writeFile as writeFile10 } from "fs/promises";
6834
+ import { join as join31, dirname as dirname11 } from "path";
6186
6835
  function countWikilinks(body, slug) {
6187
6836
  const escaped = slug.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6188
6837
  const re = new RegExp(`\\[\\[${escaped}(?:[|#][^\\]]*)?\\]\\]`, "g");
@@ -6210,7 +6859,7 @@ async function runArchive(input) {
6210
6859
  if (!relPath) return { exitCode: ExitCode.ARCHIVE_TARGET_NOT_FOUND, result: err("ARCHIVE_TARGET_NOT_FOUND", { page: input.page }) };
6211
6860
  if (relPath.startsWith("_archive/")) return { exitCode: ExitCode.ARCHIVE_ALREADY_ARCHIVED, result: err("ARCHIVE_ALREADY_ARCHIVED", { page: relPath }) };
6212
6861
  const slug = relPath.replace(/\.md$/, "").split("/").pop();
6213
- const archivePath = join30("_archive", relPath).replace(/\\/g, "/");
6862
+ const archivePath = join31("_archive", relPath).replace(/\\/g, "/");
6214
6863
  let cascade;
6215
6864
  if (input.cascade) {
6216
6865
  const wikilinkRefs = [];
@@ -6234,7 +6883,7 @@ async function runArchive(input) {
6234
6883
  const indexRefs = [];
6235
6884
  if (!isRaw) {
6236
6885
  try {
6237
- const idx = await readFile20(join30(input.vault, "index.md"), "utf8");
6886
+ const idx = await readFile21(join31(input.vault, "index.md"), "utf8");
6238
6887
  idx.split("\n").forEach((line, i) => {
6239
6888
  if (line.includes(`[[${slug}]]`)) indexRefs.push({ line: i + 1, text: line });
6240
6889
  });
@@ -6260,8 +6909,8 @@ async function runArchive(input) {
6260
6909
  }
6261
6910
  if (input.cascade && input.apply && cascade) {
6262
6911
  for (const ref of cascade.source_array_refs) {
6263
- const absPath = join30(input.vault, ref.page);
6264
- const text = await readFile20(absPath, "utf8");
6912
+ const absPath = join31(input.vault, ref.page);
6913
+ const text = await readFile21(absPath, "utf8");
6265
6914
  const split = splitFrontmatter(text);
6266
6915
  if (!split.ok) continue;
6267
6916
  const before = split.data.rawFrontmatter;
@@ -6278,12 +6927,12 @@ ${fmRewritten}
6278
6927
  }
6279
6928
  }
6280
6929
  }
6281
- await mkdir9(dirname11(join30(input.vault, archivePath)), { recursive: true });
6930
+ await mkdir9(dirname11(join31(input.vault, archivePath)), { recursive: true });
6282
6931
  let indexUpdated = false;
6283
6932
  if (!isRaw) {
6284
- const indexPath = join30(input.vault, "index.md");
6933
+ const indexPath = join31(input.vault, "index.md");
6285
6934
  try {
6286
- const idx = await readFile20(indexPath, "utf8");
6935
+ const idx = await readFile21(indexPath, "utf8");
6287
6936
  const originalLines = idx.split("\n");
6288
6937
  const filtered = originalLines.filter((l) => !l.includes(`[[${slug}]]`));
6289
6938
  if (filtered.length !== originalLines.length) {
@@ -6294,7 +6943,7 @@ ${fmRewritten}
6294
6943
  if (e instanceof Error && "code" in e && e.code !== "ENOENT") throw e;
6295
6944
  }
6296
6945
  }
6297
- await rename7(join30(input.vault, relPath), join30(input.vault, archivePath));
6946
+ await rename7(join31(input.vault, relPath), join31(input.vault, archivePath));
6298
6947
  appendLastOp(input.vault, {
6299
6948
  operation: input.cascade ? "archive-cascade" : "archive",
6300
6949
  summary: `moved ${relPath} to ${archivePath}${input.cascade ? ` (cascade: ${cascade?.source_array_refs.length ?? 0} source arrays updated)` : ""}`,
@@ -6317,7 +6966,7 @@ ${fmRewritten}
6317
6966
  }
6318
6967
 
6319
6968
  // src/commands/drift.ts
6320
- import { createHash as createHash3 } from "crypto";
6969
+ import { createHash as createHash5 } from "crypto";
6321
6970
 
6322
6971
  // src/utils/fetch.ts
6323
6972
  async function controlledFetch(url, opts) {
@@ -6396,7 +7045,7 @@ async function runDrift(input) {
6396
7045
  });
6397
7046
  continue;
6398
7047
  }
6399
- const currentHash = createHash3("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
7048
+ const currentHash = createHash5("sha256").update(Buffer.from(resp.data.body, "utf8")).digest("hex");
6400
7049
  const identity = assessSourceIdentity({
6401
7050
  rawPath: raw.relPath,
6402
7051
  sourceUrl,
@@ -6703,7 +7352,7 @@ ${newBody}`;
6703
7352
 
6704
7353
  // src/commands/update.ts
6705
7354
  import { execSync as execSync3 } from "child_process";
6706
- import { join as join31 } from "path";
7355
+ import { join as join32 } from "path";
6707
7356
 
6708
7357
  // src/utils/package-info.ts
6709
7358
  import { readFileSync as readFileSync9 } from "fs";
@@ -6733,7 +7382,7 @@ function resolveGlobalSkillsRoot() {
6733
7382
  encoding: "utf8",
6734
7383
  timeout: 5e3
6735
7384
  }).trim();
6736
- return join31(globalRoot, "skillwiki", "skills");
7385
+ return join32(globalRoot, "skillwiki", "skills");
6737
7386
  } catch {
6738
7387
  return null;
6739
7388
  }
@@ -6757,7 +7406,7 @@ async function runUpdate(input) {
6757
7406
  const pkg2 = readCliPackageJson();
6758
7407
  const currentVersion = pkg2.version;
6759
7408
  const tag = normalizeDistTag(input.distTag);
6760
- const target = join31(input.home, ".claude", "skills");
7409
+ const target = join32(input.home, ".claude", "skills");
6761
7410
  let latest;
6762
7411
  try {
6763
7412
  latest = execSync3(`npm view skillwiki@${tag} version`, {
@@ -6829,13 +7478,13 @@ async function runUpdate(input) {
6829
7478
  // src/commands/self-update.ts
6830
7479
  import { execSync as execSync4 } from "child_process";
6831
7480
  import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
6832
- import { join as join32 } from "path";
7481
+ import { join as join33 } from "path";
6833
7482
  var DEFAULT_SOURCE_ROOT_SUFFIX = "/Desktop/code/llm-wiki";
6834
7483
  async function runSelfUpdate(input) {
6835
7484
  const currentVersion = readCliPackageJson().version;
6836
7485
  const sourceRoot = input.sourceRoot ?? `${input.home}${DEFAULT_SOURCE_ROOT_SUFFIX}`;
6837
7486
  const distTag = normalizeDistTag(input.distTag);
6838
- const localPkgPath = join32(sourceRoot, "packages", "cli", "package.json");
7487
+ const localPkgPath = join33(sourceRoot, "packages", "cli", "package.json");
6839
7488
  const hasLocalSource = existsSync13(localPkgPath);
6840
7489
  if (input.check) {
6841
7490
  let availableVersion = null;
@@ -6967,10 +7616,10 @@ async function runSelfUpdate(input) {
6967
7616
  }
6968
7617
 
6969
7618
  // src/commands/transcripts.ts
6970
- import { readdir as readdir5, stat as stat8, readFile as readFile21 } from "fs/promises";
6971
- import { join as join33 } from "path";
7619
+ import { readdir as readdir5, stat as stat8, readFile as readFile22 } from "fs/promises";
7620
+ import { join as join34 } from "path";
6972
7621
  async function runTranscripts(input) {
6973
- const dir = join33(input.vault, "raw", "transcripts");
7622
+ const dir = join34(input.vault, "raw", "transcripts");
6974
7623
  let entries;
6975
7624
  try {
6976
7625
  entries = await readdir5(dir, { withFileTypes: true });
@@ -6980,8 +7629,8 @@ async function runTranscripts(input) {
6980
7629
  const transcripts = [];
6981
7630
  for (const entry of entries) {
6982
7631
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
6983
- const filePath = join33(dir, entry.name);
6984
- const content = await readFile21(filePath, "utf8");
7632
+ const filePath = join34(dir, entry.name);
7633
+ const content = await readFile22(filePath, "utf8");
6985
7634
  const fm = extractFrontmatter(content);
6986
7635
  if (!fm.ok) continue;
6987
7636
  const ingested = typeof fm.data.ingested === "string" ? fm.data.ingested : "";
@@ -6998,12 +7647,12 @@ async function runTranscripts(input) {
6998
7647
  }
6999
7648
 
7000
7649
  // src/commands/project-index.ts
7001
- import { readdir as readdir6, readFile as readFile22, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
7002
- import { join as join34, dirname as dirname12 } from "path";
7650
+ import { readdir as readdir6, readFile as readFile23, writeFile as writeFile11, mkdir as mkdir10 } from "fs/promises";
7651
+ import { join as join35, dirname as dirname12 } from "path";
7003
7652
  var LAYER2_DIRS = ["entities", "concepts", "comparisons", "queries", "meta"];
7004
7653
  async function runProjectIndex(input) {
7005
7654
  const slug = input.slug;
7006
- const projectDir = join34(input.vault, "projects", slug);
7655
+ const projectDir = join35(input.vault, "projects", slug);
7007
7656
  try {
7008
7657
  await readdir6(projectDir);
7009
7658
  } catch {
@@ -7014,15 +7663,15 @@ async function runProjectIndex(input) {
7014
7663
  }
7015
7664
  const wikilinkPattern = `[[${slug}]]`;
7016
7665
  const entries = [];
7017
- const compoundDir = join34(input.vault, "projects", slug, "compound");
7666
+ const compoundDir = join35(input.vault, "projects", slug, "compound");
7018
7667
  try {
7019
7668
  const compoundFiles = await readdir6(compoundDir, { withFileTypes: true });
7020
7669
  for (const entry of compoundFiles) {
7021
7670
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
7022
- const filePath = join34(compoundDir, entry.name);
7671
+ const filePath = join35(compoundDir, entry.name);
7023
7672
  let text;
7024
7673
  try {
7025
- text = await readFile22(filePath, "utf8");
7674
+ text = await readFile23(filePath, "utf8");
7026
7675
  } catch {
7027
7676
  continue;
7028
7677
  }
@@ -7039,16 +7688,16 @@ async function runProjectIndex(input) {
7039
7688
  for (const dir of LAYER2_DIRS) {
7040
7689
  let files;
7041
7690
  try {
7042
- files = await readdir6(join34(input.vault, dir), { withFileTypes: true });
7691
+ files = await readdir6(join35(input.vault, dir), { withFileTypes: true });
7043
7692
  } catch {
7044
7693
  continue;
7045
7694
  }
7046
7695
  for (const entry of files) {
7047
7696
  if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
7048
- const filePath = join34(input.vault, dir, entry.name);
7697
+ const filePath = join35(input.vault, dir, entry.name);
7049
7698
  let text;
7050
7699
  try {
7051
- text = await readFile22(filePath, "utf8");
7700
+ text = await readFile23(filePath, "utf8");
7052
7701
  } catch {
7053
7702
  continue;
7054
7703
  }
@@ -7069,11 +7718,11 @@ async function runProjectIndex(input) {
7069
7718
  const tb = typeOrder[b.type] ?? 99;
7070
7719
  return ta !== tb ? ta - tb : a.title.localeCompare(b.title);
7071
7720
  });
7072
- const indexPath = join34(projectDir, "knowledge.md");
7721
+ const indexPath = join35(projectDir, "knowledge.md");
7073
7722
  let existing = false;
7074
7723
  let stale = false;
7075
7724
  try {
7076
- const existingText = await readFile22(indexPath, "utf8");
7725
+ const existingText = await readFile23(indexPath, "utf8");
7077
7726
  existing = true;
7078
7727
  const existingEntries = existingText.split("\n").filter((l) => l.startsWith("- [["));
7079
7728
  const existingPages = new Set(existingEntries.map((l) => {
@@ -7143,9 +7792,9 @@ ${entries.map((e) => ` ${e.type}: [[${e.page.replace(/\.md$/, "")}]] \u2014 ${e
7143
7792
 
7144
7793
  // src/commands/compound.ts
7145
7794
  import { writeFile as writeFile12, mkdir as mkdir11, readdir as readdir7, unlink as unlink4 } from "fs/promises";
7146
- import { join as join35 } from "path";
7795
+ import { join as join36 } from "path";
7147
7796
  import { existsSync as existsSync14 } from "fs";
7148
- import { readFile as readFile23 } from "fs/promises";
7797
+ import { readFile as readFile24 } from "fs/promises";
7149
7798
  var RETRO_HEADING_RE = /^## \[(\d{4}-\d{2}-\d{2})(?:\s+[^\]]+)?\] retro \| loop cycle(?: (\d+))?: (.+)$/;
7150
7799
  var FIELD_RE = {
7151
7800
  improve: /^-\s+\*?\*?Improve:?\*?\*?\s*(.+)$/m,
@@ -7243,17 +7892,17 @@ function extractRetroFields(date, cycleName, block) {
7243
7892
  };
7244
7893
  }
7245
7894
  async function runCompound(input) {
7246
- const logPath = join35(input.vault, "log.md");
7895
+ const logPath = join36(input.vault, "log.md");
7247
7896
  let logText;
7248
7897
  try {
7249
- logText = await readFile23(logPath, "utf8");
7898
+ logText = await readFile24(logPath, "utf8");
7250
7899
  } catch {
7251
7900
  return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: logPath }) };
7252
7901
  }
7253
7902
  const entries = parseRetroEntries(logText);
7254
7903
  const promoted = [];
7255
7904
  const skipped = [];
7256
- const compoundDir = join35(input.vault, "projects", input.project, "compound");
7905
+ const compoundDir = join36(input.vault, "projects", input.project, "compound");
7257
7906
  for (const entry of entries) {
7258
7907
  const generalizeValue = entry.generalize.trim();
7259
7908
  if (!/^yes/i.test(generalizeValue)) {
@@ -7261,7 +7910,7 @@ async function runCompound(input) {
7261
7910
  continue;
7262
7911
  }
7263
7912
  const slug = slugify(entry.cycleName);
7264
- const compoundPath = join35(compoundDir, `${slug}.md`);
7913
+ const compoundPath = join36(compoundDir, `${slug}.md`);
7265
7914
  if (existsSync14(compoundPath)) {
7266
7915
  skipped.push(entry.date);
7267
7916
  continue;
@@ -7322,7 +7971,7 @@ async function runCompound(input) {
7322
7971
  };
7323
7972
  }
7324
7973
  async function runCompoundDelete(input) {
7325
- const projectDir = join35(input.vault, "projects", input.project);
7974
+ const projectDir = join36(input.vault, "projects", input.project);
7326
7975
  if (!existsSync14(projectDir)) {
7327
7976
  return {
7328
7977
  exitCode: ExitCode.PROJECT_NOT_FOUND,
@@ -7330,7 +7979,7 @@ async function runCompoundDelete(input) {
7330
7979
  };
7331
7980
  }
7332
7981
  const entryName = input.entry.replace(/\.md$/, "");
7333
- const compoundPath = join35(projectDir, "compound", `${entryName}.md`);
7982
+ const compoundPath = join36(projectDir, "compound", `${entryName}.md`);
7334
7983
  if (!existsSync14(compoundPath)) {
7335
7984
  return {
7336
7985
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -7364,7 +8013,7 @@ knowledge.md regenerated`
7364
8013
  };
7365
8014
  }
7366
8015
  async function runCompoundList(input) {
7367
- const compoundDir = join35(input.vault, "projects", input.project, "compound");
8016
+ const compoundDir = join36(input.vault, "projects", input.project, "compound");
7368
8017
  if (!existsSync14(compoundDir)) {
7369
8018
  return {
7370
8019
  exitCode: ExitCode.OK,
@@ -7395,10 +8044,10 @@ could not read compound directory`
7395
8044
  const entries = [];
7396
8045
  for (const dirent of dirents) {
7397
8046
  if (!dirent.isFile() || !dirent.name.endsWith(".md")) continue;
7398
- const filePath = join35(compoundDir, dirent.name);
8047
+ const filePath = join36(compoundDir, dirent.name);
7399
8048
  let text;
7400
8049
  try {
7401
- text = await readFile23(filePath, "utf8");
8050
+ text = await readFile24(filePath, "utf8");
7402
8051
  } catch {
7403
8052
  continue;
7404
8053
  }
@@ -7429,8 +8078,8 @@ no compound entries found`;
7429
8078
  // src/commands/observe.ts
7430
8079
  import { mkdir as mkdir12, writeFile as writeFile13 } from "fs/promises";
7431
8080
  import { existsSync as existsSync15, statSync as statSync4 } from "fs";
7432
- import { join as join36 } from "path";
7433
- import { createHash as createHash4 } from "crypto";
8081
+ import { join as join37 } from "path";
8082
+ import { createHash as createHash6 } from "crypto";
7434
8083
  var ALLOWED_KINDS = /* @__PURE__ */ new Set(["note", "bug", "task", "idea", "session-log"]);
7435
8084
  function slugify2(text) {
7436
8085
  const words = text.trim().split(/\s+/).slice(0, 6).join("-").toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
@@ -7458,7 +8107,7 @@ async function runObserve(input) {
7458
8107
  result: err("VAULT_PATH_INVALID", { path: input.vault })
7459
8108
  };
7460
8109
  }
7461
- const transcriptsDir = join36(input.vault, "raw", "transcripts");
8110
+ const transcriptsDir = join37(input.vault, "raw", "transcripts");
7462
8111
  try {
7463
8112
  await mkdir12(transcriptsDir, { recursive: true });
7464
8113
  } catch {
@@ -7470,11 +8119,11 @@ async function runObserve(input) {
7470
8119
  const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
7471
8120
  const slug = slugify2(input.text);
7472
8121
  const fileName = `${today}-observation-${slug}.md`;
7473
- const filePath = join36(transcriptsDir, fileName);
8122
+ const filePath = join37(transcriptsDir, fileName);
7474
8123
  const body = `
7475
8124
  ${input.text.trim()}
7476
8125
  `;
7477
- const sha256 = createHash4("sha256").update(Buffer.from(body, "utf8")).digest("hex");
8126
+ const sha256 = createHash6("sha256").update(Buffer.from(body, "utf8")).digest("hex");
7478
8127
  const frontmatterLines = [
7479
8128
  "---",
7480
8129
  "source_url:",
@@ -7510,8 +8159,8 @@ ${input.text.trim()}
7510
8159
  }
7511
8160
 
7512
8161
  // src/commands/session-brief.ts
7513
- import { mkdir as mkdir13, readFile as readFile24, writeFile as writeFile14 } from "fs/promises";
7514
- import { join as join37, relative as relative3, sep as sep3 } from "path";
8162
+ import { mkdir as mkdir13, readFile as readFile25, writeFile as writeFile14 } from "fs/promises";
8163
+ import { join as join38, relative as relative3, sep as sep3 } from "path";
7515
8164
  var MAX_WORDS = 900;
7516
8165
  async function runSessionBrief(input) {
7517
8166
  const scan = await scanVault(input.vault);
@@ -7589,7 +8238,7 @@ async function resolveProject(input) {
7589
8238
  const envProject = input.env?.SKILLWIKI_PROJECT;
7590
8239
  if (envProject) return envProject;
7591
8240
  const cwd = input.cwd ?? process.cwd();
7592
- const projectDotenv = await readProjectSlug(join37(cwd, ".skillwiki", ".env"));
8241
+ const projectDotenv = await readProjectSlug(join38(cwd, ".skillwiki", ".env"));
7593
8242
  if (projectDotenv) return projectDotenv;
7594
8243
  const inferred = inferProjectFromPath(input.vault, cwd);
7595
8244
  if (inferred) return inferred;
@@ -7598,7 +8247,7 @@ async function resolveProject(input) {
7598
8247
  async function readProjectSlug(file) {
7599
8248
  let text;
7600
8249
  try {
7601
- text = await readFile24(file, "utf8");
8250
+ text = await readFile25(file, "utf8");
7602
8251
  } catch {
7603
8252
  return void 0;
7604
8253
  }
@@ -7723,7 +8372,7 @@ function appendTextSection(lines, title, items, empty) {
7723
8372
  lines.push("");
7724
8373
  }
7725
8374
  async function loadHealthWarnings(vault) {
7726
- const text = await readIfExists2(join37(vault, ".skillwiki", "health.json"));
8375
+ const text = await readIfExists2(join38(vault, ".skillwiki", "health.json"));
7727
8376
  if (!text) return [];
7728
8377
  try {
7729
8378
  const parsed = JSON.parse(text);
@@ -7736,11 +8385,11 @@ async function loadHealthWarnings(vault) {
7736
8385
  }
7737
8386
  }
7738
8387
  async function writeBriefArtifacts(vault, input) {
7739
- const metaPath = join37(vault, "meta", "latest-session-brief.md");
7740
- const cacheMdPath = join37(vault, ".skillwiki", "session-brief.md");
7741
- const cacheJsonPath = join37(vault, ".skillwiki", "session-brief.json");
7742
- await mkdir13(join37(vault, "meta"), { recursive: true });
7743
- await mkdir13(join37(vault, ".skillwiki"), { recursive: true });
8388
+ const metaPath = join38(vault, "meta", "latest-session-brief.md");
8389
+ const cacheMdPath = join38(vault, ".skillwiki", "session-brief.md");
8390
+ const cacheJsonPath = join38(vault, ".skillwiki", "session-brief.json");
8391
+ await mkdir13(join38(vault, "meta"), { recursive: true });
8392
+ await mkdir13(join38(vault, ".skillwiki"), { recursive: true });
7744
8393
  const committed = renderCommittedBrief(input);
7745
8394
  const previousComparable = comparableBrief(await readIfExists2(metaPath));
7746
8395
  const nextComparable = comparableBrief(committed);
@@ -7800,7 +8449,7 @@ function renderCommittedBrief(input) {
7800
8449
  ].filter((line) => line !== "").join("\n");
7801
8450
  }
7802
8451
  async function ensureIndexEntry(vault) {
7803
- const indexPath = join37(vault, "index.md");
8452
+ const indexPath = join38(vault, "index.md");
7804
8453
  let text = await readIfExists2(indexPath);
7805
8454
  if (!text) return false;
7806
8455
  if (text.includes("[[meta/latest-session-brief]]")) return false;
@@ -7819,7 +8468,7 @@ async function ensureIndexEntry(vault) {
7819
8468
  return true;
7820
8469
  }
7821
8470
  async function appendMaterialLog(vault, today) {
7822
- const logPath = join37(vault, "log.md");
8471
+ const logPath = join38(vault, "log.md");
7823
8472
  const text = await readIfExists2(logPath);
7824
8473
  if (!text) return false;
7825
8474
  const entry = `
@@ -7829,7 +8478,7 @@ async function appendMaterialLog(vault, today) {
7829
8478
  }
7830
8479
  async function readIfExists2(path) {
7831
8480
  try {
7832
- return await readFile24(path, "utf8");
8481
+ return await readFile25(path, "utf8");
7833
8482
  } catch {
7834
8483
  return "";
7835
8484
  }
@@ -7871,9 +8520,9 @@ function dateFromPath(path) {
7871
8520
  }
7872
8521
 
7873
8522
  // src/commands/ingest.ts
7874
- import { readFile as readFile25, writeFile as writeFile15, mkdir as mkdir14 } from "fs/promises";
7875
- import { join as join38 } from "path";
7876
- import { createHash as createHash5 } from "crypto";
8523
+ import { readFile as readFile26, writeFile as writeFile15, mkdir as mkdir14 } from "fs/promises";
8524
+ import { join as join39 } from "path";
8525
+ import { createHash as createHash7 } from "crypto";
7877
8526
  var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
7878
8527
  var TYPE_DIR = {
7879
8528
  entity: "entities",
@@ -8031,7 +8680,7 @@ async function runIngest(input) {
8031
8680
  sourceContent = fetchResult.data.body;
8032
8681
  } else {
8033
8682
  try {
8034
- sourceContent = await readFile25(input.source, "utf8");
8683
+ sourceContent = await readFile26(input.source, "utf8");
8035
8684
  } catch {
8036
8685
  return {
8037
8686
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -8039,15 +8688,25 @@ async function runIngest(input) {
8039
8688
  };
8040
8689
  }
8041
8690
  }
8042
- const sha256 = createHash5("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
8691
+ const sensitiveFindings = scanSensitiveContent(sourceContent, { file: input.source });
8692
+ if (sensitiveFindings.length > 0) {
8693
+ return {
8694
+ exitCode: ExitCode.SENSITIVE_CONTENT_DETECTED,
8695
+ result: err("SENSITIVE_CONTENT_DETECTED", {
8696
+ message: "source content contains sensitive authentication material; provide a redacted source before ingesting",
8697
+ findings: sensitiveFindings
8698
+ })
8699
+ };
8700
+ }
8701
+ const sha256 = createHash7("sha256").update(Buffer.from(sourceContent, "utf8")).digest("hex");
8043
8702
  const today = todayIso();
8044
8703
  const slug = slugify3(input.title);
8045
8704
  const tags = input.tags && input.tags.length > 0 ? input.tags : [];
8046
8705
  const rawRelPath = `raw/articles/${slug}.md`;
8047
8706
  const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
8048
8707
  const typedRelPath = `${typedDir}/${slug}.md`;
8049
- const rawAbsPath = join38(input.vault, rawRelPath);
8050
- const typedAbsPath = join38(input.vault, typedRelPath);
8708
+ const rawAbsPath = join39(input.vault, rawRelPath);
8709
+ const typedAbsPath = join39(input.vault, typedRelPath);
8051
8710
  const identity = assessSourceIdentity({
8052
8711
  rawPath: rawRelPath,
8053
8712
  sourceUrl: sourceUrl ?? void 0,
@@ -8129,7 +8788,7 @@ async function runIngest(input) {
8129
8788
  };
8130
8789
  }
8131
8790
  try {
8132
- await mkdir14(join38(input.vault, "raw", "articles"), { recursive: true });
8791
+ await mkdir14(join39(input.vault, "raw", "articles"), { recursive: true });
8133
8792
  await writeFile15(rawAbsPath, rawContent, "utf8");
8134
8793
  } catch (e) {
8135
8794
  return {
@@ -8138,7 +8797,7 @@ async function runIngest(input) {
8138
8797
  };
8139
8798
  }
8140
8799
  try {
8141
- await mkdir14(join38(input.vault, typedDir), { recursive: true });
8800
+ await mkdir14(join39(input.vault, typedDir), { recursive: true });
8142
8801
  await writeFile15(typedAbsPath, typedContent, "utf8");
8143
8802
  } catch (e) {
8144
8803
  return {
@@ -8318,19 +8977,19 @@ ${body}`;
8318
8977
 
8319
8978
  // src/commands/sync.ts
8320
8979
  import { existsSync as existsSync17 } from "fs";
8321
- import { join as join40 } from "path";
8980
+ import { join as join41 } from "path";
8322
8981
 
8323
8982
  // src/utils/sync-lock.ts
8324
8983
  import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
8325
- import { join as join39 } from "path";
8326
- import { createHash as createHash6 } from "crypto";
8984
+ import { join as join40 } from "path";
8985
+ import { createHash as createHash8 } from "crypto";
8327
8986
  function getSessionId() {
8328
8987
  if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
8329
8988
  if (process.env.SKILLWIKI_SESSION_ID) return process.env.SKILLWIKI_SESSION_ID;
8330
8989
  return process.pid.toString();
8331
8990
  }
8332
8991
  function lockPath(vault) {
8333
- return join39(vault, ".skillwiki", "sync.lock");
8992
+ return join40(vault, ".skillwiki", "sync.lock");
8334
8993
  }
8335
8994
  function readLock(vault) {
8336
8995
  const path = lockPath(vault);
@@ -8349,7 +9008,7 @@ function isStale(lock, now) {
8349
9008
  }
8350
9009
  function acquireLock(vault, opts = {}) {
8351
9010
  const path = lockPath(vault);
8352
- const dir = join39(vault, ".skillwiki");
9011
+ const dir = join40(vault, ".skillwiki");
8353
9012
  if (!existsSync16(dir)) {
8354
9013
  mkdirSync5(dir, { recursive: true });
8355
9014
  }
@@ -8424,7 +9083,7 @@ function releaseLock(vault, opts = {}) {
8424
9083
  function runSyncStatus(input) {
8425
9084
  const vault = input.vault;
8426
9085
  const includeStashes = input.includeStashes ?? false;
8427
- if (!existsSync17(join40(vault, ".git"))) {
9086
+ if (!existsSync17(join41(vault, ".git"))) {
8428
9087
  return {
8429
9088
  exitCode: ExitCode.VAULT_PATH_INVALID,
8430
9089
  result: ok({
@@ -8502,7 +9161,7 @@ function runSyncStatus(input) {
8502
9161
  }
8503
9162
  async function runSyncPush(input) {
8504
9163
  const vault = input.vault;
8505
- if (!existsSync17(join40(vault, ".git"))) {
9164
+ if (!existsSync17(join41(vault, ".git"))) {
8506
9165
  return {
8507
9166
  exitCode: ExitCode.VAULT_PATH_INVALID,
8508
9167
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -8625,7 +9284,7 @@ function enableGitLongPathsOnWindows(vault) {
8625
9284
  }
8626
9285
  async function runSyncPull(input) {
8627
9286
  const vault = input.vault;
8628
- if (!existsSync17(join40(vault, ".git"))) {
9287
+ if (!existsSync17(join41(vault, ".git"))) {
8629
9288
  return {
8630
9289
  exitCode: ExitCode.VAULT_PATH_INVALID,
8631
9290
  result: err("NOT_A_GIT_REPO", { path: vault })
@@ -8866,7 +9525,7 @@ function runSyncUnlock(input) {
8866
9525
 
8867
9526
  // src/commands/backup.ts
8868
9527
  import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync12, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
8869
- import { join as join41, relative as relative4, dirname as dirname13 } from "path";
9528
+ import { join as join42, relative as relative4, dirname as dirname13 } from "path";
8870
9529
  import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
8871
9530
 
8872
9531
  // src/utils/s3-client.ts
@@ -8890,7 +9549,7 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node
8890
9549
  function* walkMarkdown(dir, base) {
8891
9550
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
8892
9551
  if (SKIP_DIRS2.has(entry.name)) continue;
8893
- const full = join41(dir, entry.name);
9552
+ const full = join42(dir, entry.name);
8894
9553
  if (entry.isDirectory()) {
8895
9554
  yield* walkMarkdown(full, base);
8896
9555
  } else if (entry.name.endsWith(".md")) {
@@ -8913,7 +9572,7 @@ async function runBackupSync(input) {
8913
9572
  let failed = 0;
8914
9573
  const files = [...walkMarkdown(input.vault, input.vault)];
8915
9574
  for (const relPath of files) {
8916
- const absPath = join41(input.vault, relPath);
9575
+ const absPath = join42(input.vault, relPath);
8917
9576
  const localStat = statSync5(absPath);
8918
9577
  let needsUpload = true;
8919
9578
  try {
@@ -8989,7 +9648,7 @@ async function runBackupRestore(input) {
8989
9648
  const objects = list.Contents ?? [];
8990
9649
  for (const obj of objects) {
8991
9650
  if (!obj.Key) continue;
8992
- const localPath = join41(target, obj.Key);
9651
+ const localPath = join42(target, obj.Key);
8993
9652
  try {
8994
9653
  const localStat = statSync5(localPath);
8995
9654
  if (obj.LastModified && localStat.mtime > obj.LastModified) {
@@ -9036,8 +9695,8 @@ async function runBackupRestore(input) {
9036
9695
 
9037
9696
  // src/commands/status.ts
9038
9697
  import { existsSync as existsSync18, statSync as statSync6 } from "fs";
9039
- import { readFile as readFile26 } from "fs/promises";
9040
- import { join as join42 } from "path";
9698
+ import { readFile as readFile27 } from "fs/promises";
9699
+ import { join as join43 } from "path";
9041
9700
  async function runStatus(input) {
9042
9701
  if (!existsSync18(input.vault)) {
9043
9702
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
@@ -9064,7 +9723,7 @@ async function runStatus(input) {
9064
9723
  const compound = scan.data.compound.length;
9065
9724
  let schemaVersion = "v1";
9066
9725
  try {
9067
- const schemaContent = await readFile26(join42(input.vault, "SCHEMA.md"), "utf8");
9726
+ const schemaContent = await readFile27(join43(input.vault, "SCHEMA.md"), "utf8");
9068
9727
  const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
9069
9728
  if (versionMatch) schemaVersion = versionMatch[1];
9070
9729
  } catch {
@@ -9125,7 +9784,7 @@ async function runStatus(input) {
9125
9784
 
9126
9785
  // src/commands/seed.ts
9127
9786
  import { mkdir as mkdir15, writeFile as writeFile16, stat as stat9 } from "fs/promises";
9128
- import { join as join43 } from "path";
9787
+ import { join as join44 } from "path";
9129
9788
  var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
9130
9789
  var EXAMPLE_PAGES = {
9131
9790
  "entities/example-project.md": `---
@@ -9194,29 +9853,29 @@ Real sources are immutable after ingestion \u2014 never edit them.
9194
9853
  `;
9195
9854
  async function runSeed(input) {
9196
9855
  try {
9197
- await stat9(join43(input.vault, "SCHEMA.md"));
9856
+ await stat9(join44(input.vault, "SCHEMA.md"));
9198
9857
  } catch {
9199
9858
  return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
9200
9859
  }
9201
9860
  const created = [];
9202
9861
  const skipped = [];
9203
9862
  for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
9204
- const absPath = join43(input.vault, relPath);
9863
+ const absPath = join44(input.vault, relPath);
9205
9864
  try {
9206
9865
  await stat9(absPath);
9207
9866
  skipped.push(relPath);
9208
9867
  } catch {
9209
- await mkdir15(join43(absPath, ".."), { recursive: true });
9868
+ await mkdir15(join44(absPath, ".."), { recursive: true });
9210
9869
  await writeFile16(absPath, content, "utf8");
9211
9870
  created.push(relPath);
9212
9871
  }
9213
9872
  }
9214
- const rawPath = join43(input.vault, "raw", "articles", "example-source.md");
9873
+ const rawPath = join44(input.vault, "raw", "articles", "example-source.md");
9215
9874
  try {
9216
9875
  await stat9(rawPath);
9217
9876
  skipped.push("raw/articles/example-source.md");
9218
9877
  } catch {
9219
- await mkdir15(join43(rawPath, ".."), { recursive: true });
9878
+ await mkdir15(join44(rawPath, ".."), { recursive: true });
9220
9879
  await writeFile16(rawPath, EXAMPLE_RAW, "utf8");
9221
9880
  created.push("raw/articles/example-source.md");
9222
9881
  }
@@ -9239,9 +9898,9 @@ async function runSeed(input) {
9239
9898
  }
9240
9899
 
9241
9900
  // src/commands/canvas.ts
9242
- import { readFile as readFile27, writeFile as writeFile17 } from "fs/promises";
9901
+ import { readFile as readFile28, writeFile as writeFile17 } from "fs/promises";
9243
9902
  import { existsSync as existsSync19 } from "fs";
9244
- import { join as join44 } from "path";
9903
+ import { join as join45 } from "path";
9245
9904
  var NODE_WIDTH = 240;
9246
9905
  var NODE_HEIGHT = 60;
9247
9906
  var COLUMN_SPACING = 400;
@@ -9319,7 +9978,7 @@ function buildCanvasEdges(adjacency) {
9319
9978
  return edges;
9320
9979
  }
9321
9980
  async function runCanvasGenerate(input) {
9322
- const graphPath = input.graphPath ?? join44(input.vault, ".skillwiki", "graph.json");
9981
+ const graphPath = input.graphPath ?? join45(input.vault, ".skillwiki", "graph.json");
9323
9982
  if (!existsSync19(graphPath)) {
9324
9983
  return {
9325
9984
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -9331,7 +9990,7 @@ async function runCanvasGenerate(input) {
9331
9990
  }
9332
9991
  let raw;
9333
9992
  try {
9334
- raw = await readFile27(graphPath, "utf8");
9993
+ raw = await readFile28(graphPath, "utf8");
9335
9994
  } catch (e) {
9336
9995
  return {
9337
9996
  exitCode: ExitCode.FILE_NOT_FOUND,
@@ -9357,7 +10016,7 @@ async function runCanvasGenerate(input) {
9357
10016
  const nodes = buildCanvasNodes(paths);
9358
10017
  const edges = buildCanvasEdges(graph.adjacency);
9359
10018
  const canvas = { nodes, edges };
9360
- const outPath = join44(input.vault, "vault-graph.canvas");
10019
+ const outPath = join45(input.vault, "vault-graph.canvas");
9361
10020
  try {
9362
10021
  await writeFile17(outPath, JSON.stringify(canvas, null, 2));
9363
10022
  } catch (e) {
@@ -9379,8 +10038,8 @@ written: ${outPath}`
9379
10038
  }
9380
10039
 
9381
10040
  // src/commands/query.ts
9382
- import { readFile as readFile28, stat as stat10 } from "fs/promises";
9383
- import { join as join45 } from "path";
10041
+ import { readFile as readFile29, stat as stat10 } from "fs/promises";
10042
+ import { join as join46 } from "path";
9384
10043
  var W_KEYWORD = 2;
9385
10044
  var W_SOURCE_OVERLAP = 4;
9386
10045
  var W_WIKILINK = 3;
@@ -9501,7 +10160,7 @@ function computeKeywordScore(terms, title, tags, body) {
9501
10160
  return score;
9502
10161
  }
9503
10162
  async function loadOrBuildGraph(vault) {
9504
- const graphPath = join45(vault, ".skillwiki", "graph.json");
10163
+ const graphPath = join46(vault, ".skillwiki", "graph.json");
9505
10164
  let needsBuild = false;
9506
10165
  try {
9507
10166
  const fileStat = await stat10(graphPath);
@@ -9515,265 +10174,13 @@ async function loadOrBuildGraph(vault) {
9515
10174
  if (buildResult.exitCode !== 0) return null;
9516
10175
  }
9517
10176
  try {
9518
- const raw = await readFile28(graphPath, "utf8");
10177
+ const raw = await readFile29(graphPath, "utf8");
9519
10178
  return JSON.parse(raw);
9520
10179
  } catch {
9521
10180
  return null;
9522
10181
  }
9523
10182
  }
9524
10183
 
9525
- // src/commands/fleet.ts
9526
- import { readFile as readFile29 } from "fs/promises";
9527
- import { hostname as nodeHostname, userInfo } from "os";
9528
- import { join as join46 } from "path";
9529
- import yaml3 from "js-yaml";
9530
- var FLEET_REL_PATH = join46("projects", "llm-wiki", "architecture", "fleet.yaml");
9531
- async function runFleetValidate(input) {
9532
- const loaded = await loadFleetManifest(input.file);
9533
- if (!loaded.ok) {
9534
- if (loaded.error === "FILE_NOT_FOUND") {
9535
- return { exitCode: ExitCode.FILE_NOT_FOUND, result: err("FILE_NOT_FOUND", { path: input.file }) };
9536
- }
9537
- const errors = fleetLoadErrors(loaded);
9538
- return invalidFleet(errors);
9539
- }
9540
- const warnings = fleetWarnings(loaded.manifest);
9541
- const snapshotter = findSnapshotter(loaded.manifest);
9542
- return {
9543
- exitCode: ExitCode.OK,
9544
- result: ok({
9545
- valid: true,
9546
- errors: [],
9547
- warnings,
9548
- host_count: Object.keys(loaded.manifest.hosts).length,
9549
- snapshotter,
9550
- humanHint: `VALID fleet manifest (${Object.keys(loaded.manifest.hosts).length} hosts; snapshotter: ${snapshotter ?? "none"})`
9551
- })
9552
- };
9553
- }
9554
- async function runFleetContext(input) {
9555
- const env = input.env ?? process.env;
9556
- const home = input.home ?? env.HOME ?? "";
9557
- const cwd = input.cwd ?? process.cwd();
9558
- const osHostname = input.osHostname ?? safeEnvValue(env.HOSTNAME) ?? nodeHostname();
9559
- const user = input.user ?? safeEnvValue(env.USER) ?? safeUserName();
9560
- const vault = input.vault ?? safeEnvValue(env.WIKI_PATH);
9561
- const file = input.file ?? (vault ? join46(vault, FLEET_REL_PATH) : void 0);
9562
- const loaded = file ? await loadFleetManifest(file) : { ok: false, error: "FILE_NOT_FOUND" };
9563
- if (!loaded.ok) {
9564
- const markdown2 = formatUnknownContext({ osHostname, user, cwd, vault, reason: "fleet manifest unavailable or invalid" });
9565
- return {
9566
- exitCode: ExitCode.OK,
9567
- result: ok({ manifest_loaded: false, markdown: markdown2, humanHint: markdown2 })
9568
- };
9569
- }
9570
- const resolved = await resolveHostId({
9571
- manifest: loaded.manifest,
9572
- hostId: input.hostId,
9573
- env,
9574
- home,
9575
- osHostname
9576
- });
9577
- if (!resolved.hostId || !loaded.manifest.hosts[resolved.hostId]) {
9578
- const markdown2 = formatUnknownContext({ osHostname, user, cwd, vault, reason: "host identity is unresolved" });
9579
- return {
9580
- exitCode: ExitCode.OK,
9581
- result: ok({ manifest_loaded: true, markdown: markdown2, humanHint: markdown2 })
9582
- };
9583
- }
9584
- const markdown = formatKnownContext({
9585
- manifest: loaded.manifest,
9586
- hostId: resolved.hostId,
9587
- source: resolved.source,
9588
- osHostname,
9589
- user,
9590
- cwd,
9591
- vault
9592
- });
9593
- return {
9594
- exitCode: ExitCode.OK,
9595
- result: ok({
9596
- manifest_loaded: true,
9597
- host_id: resolved.hostId,
9598
- source: resolved.source,
9599
- markdown,
9600
- humanHint: markdown
9601
- })
9602
- };
9603
- }
9604
- async function loadFleetManifest(file) {
9605
- let text;
9606
- try {
9607
- text = await readFile29(file, "utf8");
9608
- } catch {
9609
- return { ok: false, error: "FILE_NOT_FOUND" };
9610
- }
9611
- let parsed;
9612
- try {
9613
- parsed = yaml3.load(text, { schema: yaml3.JSON_SCHEMA });
9614
- } catch (error) {
9615
- return { ok: false, error: "INVALID_YAML", detail: error instanceof Error ? error.message : String(error) };
9616
- }
9617
- const result = FleetManifestSchema.safeParse(parsed);
9618
- if (!result.success) {
9619
- return { ok: false, error: "INVALID_FLEET_MANIFEST", detail: result.error.issues };
9620
- }
9621
- return { ok: true, manifest: result.data };
9622
- }
9623
- function invalidFleet(errors) {
9624
- return {
9625
- exitCode: ExitCode.FLEET_MANIFEST_INVALID,
9626
- result: ok({
9627
- valid: false,
9628
- errors,
9629
- warnings: [],
9630
- host_count: 0,
9631
- humanHint: `INVALID fleet manifest
9632
- ${errors.map((e) => ` ${e.path || "(root)"}: ${e.message}`).join("\n")}`
9633
- })
9634
- };
9635
- }
9636
- function fleetLoadErrors(loaded) {
9637
- if (loaded.error === "INVALID_YAML") {
9638
- return [{ path: "", message: `invalid YAML: ${String(loaded.detail ?? "parse failed")}` }];
9639
- }
9640
- if (loaded.error === "INVALID_FLEET_MANIFEST" && Array.isArray(loaded.detail)) {
9641
- return loaded.detail.map((issue) => {
9642
- const zodIssue = issue;
9643
- return {
9644
- path: (zodIssue.path ?? []).join("."),
9645
- message: zodIssue.message ?? "invalid value"
9646
- };
9647
- });
9648
- }
9649
- return [{ path: "", message: loaded.error }];
9650
- }
9651
- function fleetWarnings(manifest) {
9652
- const warnings = [];
9653
- for (const [id, host] of Object.entries(manifest.hosts)) {
9654
- if (host.role === "snapshotter" && host.protected !== true) {
9655
- warnings.push(`snapshotter host '${id}' is not protected=true`);
9656
- }
9657
- }
9658
- return warnings;
9659
- }
9660
- function findSnapshotter(manifest) {
9661
- return Object.entries(manifest.hosts).find(([, host]) => host.role === "snapshotter")?.[0];
9662
- }
9663
- async function resolveHostId(input) {
9664
- if (input.hostId) return { hostId: input.hostId, source: "host-id" };
9665
- if (input.env.SKILLWIKI_HOST_ID) return { hostId: input.env.SKILLWIKI_HOST_ID, source: "SKILLWIKI_HOST_ID" };
9666
- if (input.env.AGENT_HOST_ID) return { hostId: input.env.AGENT_HOST_ID, source: "AGENT_HOST_ID" };
9667
- if (input.home) {
9668
- const dotenv = await parseDotenvFile(join46(input.home, ".skillwiki", ".env"));
9669
- if (dotenv.SKILLWIKI_HOST_ID) {
9670
- return { hostId: dotenv.SKILLWIKI_HOST_ID, source: "~/.skillwiki/.env:SKILLWIKI_HOST_ID" };
9671
- }
9672
- }
9673
- if (input.env.VS_HOSTNAME) return { hostId: input.env.VS_HOSTNAME, source: "VS_HOSTNAME" };
9674
- const hostname = input.osHostname.trim();
9675
- if (hostname) {
9676
- if (input.manifest.hosts[hostname]) return { hostId: hostname, source: "hostname" };
9677
- const byHostname = Object.entries(input.manifest.hosts).find(([, host]) => host.identity.hostnames.includes(hostname));
9678
- if (byHostname) return { hostId: byHostname[0], source: "hostname" };
9679
- }
9680
- return {};
9681
- }
9682
- function formatKnownContext(input) {
9683
- const host = input.manifest.hosts[input.hostId];
9684
- const protectedValue = host.protected === true ? "true" : "false";
9685
- const writesTo = host.writes_to.join(", ");
9686
- const selfAliases = collectSelfAliases(input.manifest, input.hostId);
9687
- const outbound = collectOutboundAccess(input.manifest, input.hostId);
9688
- const maintenanceLines = formatMaintenanceLines(host);
9689
- const guidance = 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.`;
9690
- return [
9691
- "## Runtime Host Context",
9692
- "",
9693
- `- Current machine: \`${input.hostId}\`${input.source ? ` (source: \`${input.source}\`)` : ""}`,
9694
- `- OS hostname: ${formatMaybe(input.osHostname)}`,
9695
- `- User: ${formatMaybe(input.user)}`,
9696
- `- Workspace: ${formatMaybe(input.cwd)}`,
9697
- `- Vault: ${formatMaybe(input.vault)}`,
9698
- `- Fleet role: \`${host.role}\`; protected: \`${protectedValue}\`; writes_to: \`${writesTo}\``,
9699
- ...maintenanceLines,
9700
- `- Self SSH aliases known in fleet: ${formatList(selfAliases)}`,
9701
- `- Declared outbound SSH from this source: ${formatOutboundAccess(outbound)}`,
9702
- `- Guidance: ${guidance}`
9703
- ].join("\n");
9704
- }
9705
- function formatUnknownContext(input) {
9706
- return [
9707
- "## Runtime Host Context",
9708
- "",
9709
- "- Current machine: unknown",
9710
- `- OS hostname: ${formatMaybe(input.osHostname)}`,
9711
- `- User: ${formatMaybe(input.user)}`,
9712
- `- Workspace: ${formatMaybe(input.cwd)}`,
9713
- `- Vault: ${formatMaybe(input.vault)}`,
9714
- "- Fleet role: unknown",
9715
- "- Self SSH aliases known in fleet: unknown",
9716
- "- Declared outbound SSH from this source: unknown",
9717
- `- Guidance: ${input.reason}; do not assume local vs remote role. Inspect runtime or ask before SSH/deploy/sync work.`
9718
- ].join("\n");
9719
- }
9720
- function collectSelfAliases(manifest, hostId2) {
9721
- const aliases = [];
9722
- const host = manifest.hosts[hostId2];
9723
- const access = host?.access?.from ?? {};
9724
- for (const profile of Object.values(access)) {
9725
- for (const alias of profile.ssh_aliases ?? []) aliases.push(alias);
9726
- }
9727
- return [...new Set(aliases)];
9728
- }
9729
- function collectOutboundAccess(manifest, sourceHostId) {
9730
- const hosts = [];
9731
- for (const [targetId, target] of Object.entries(manifest.hosts)) {
9732
- if (targetId === sourceHostId) continue;
9733
- const profile = target.access?.from?.[sourceHostId];
9734
- if (profile && (profile.status === "configured" || profile.status === "local")) {
9735
- hosts.push({
9736
- hostId: targetId,
9737
- sshAliases: [...new Set(profile.ssh_aliases ?? [])],
9738
- users: [...new Set(profile.users ?? [])]
9739
- });
9740
- }
9741
- }
9742
- return hosts.sort((left, right) => left.hostId.localeCompare(right.hostId));
9743
- }
9744
- function formatMaintenanceLines(host) {
9745
- const satellite = host.maintenance?.skillwiki_satellite;
9746
- if (!satellite?.enabled) return [];
9747
- return [
9748
- `- Maintenance role: \`skillwiki satellite\`; user: \`${satellite.user}\`; ssh: \`${satellite.ssh_alias}\``,
9749
- `- Maintenance paths: maintenance vault: \`${satellite.vault_path}\`; repo: \`${satellite.repo_path}\`; scheduler: \`${satellite.scheduler}\`; jobs: ${formatList(satellite.jobs)}`
9750
- ];
9751
- }
9752
- function formatOutboundAccess(values) {
9753
- if (values.length === 0) return "none";
9754
- return values.map((value) => {
9755
- const aliasPart = value.sshAliases.length > 0 ? ` via ${formatList(value.sshAliases)}` : " (no SSH aliases)";
9756
- const usersPart = value.users.length > 0 ? ` (users: ${formatList(value.users)})` : "";
9757
- return `\`${value.hostId}\`${aliasPart}${usersPart}`;
9758
- }).join("; ");
9759
- }
9760
- function formatList(values) {
9761
- return values.length > 0 ? values.map((v) => `\`${v}\``).join(", ") : "none";
9762
- }
9763
- function formatMaybe(value) {
9764
- return value && value.trim().length > 0 ? `\`${value}\`` : "unknown";
9765
- }
9766
- function safeEnvValue(value) {
9767
- return value && value.trim().length > 0 ? value : void 0;
9768
- }
9769
- function safeUserName() {
9770
- try {
9771
- return userInfo().username;
9772
- } catch {
9773
- return "";
9774
- }
9775
- }
9776
-
9777
10184
  // src/utils/auto-commit.ts
9778
10185
  import { existsSync as existsSync20 } from "fs";
9779
10186
  import { join as join47 } from "path";