@arkhera30/cli 0.3.16 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command11 } from "commander";
5
- import chalk11 from "chalk";
4
+ import { Command as Command14 } from "commander";
5
+ import chalk14 from "chalk";
6
6
 
7
7
  // src/commands/setup.ts
8
8
  import { Command as Command2 } from "commander";
@@ -139,6 +139,7 @@ function buildConfigFromParsed(parsed) {
139
139
  vault_rest: parsedPorts?.vault_rest ?? defaults.ports.vault_rest,
140
140
  vault_mcp: parsedPorts?.vault_mcp ?? defaults.ports.vault_mcp,
141
141
  vault_router: parsedPorts?.vault_router ?? defaults.ports.vault_router,
142
+ ui: parsedPorts?.ui ?? defaults.ports.ui,
142
143
  forge: parsedPorts?.forge ?? defaults.ports.forge,
143
144
  typesense: parsedPorts?.typesense ?? defaults.ports.typesense,
144
145
  neo4j_http: parsedPorts?.neo4j_http ?? defaults.ports.neo4j_http,
@@ -1088,6 +1089,25 @@ function mergeAndWriteConfig(configPath, mcpServers) {
1088
1089
  mkdirSync2(dir, { recursive: true });
1089
1090
  writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
1090
1091
  }
1092
+ function detectNpxPath() {
1093
+ const candidates = ["/opt/homebrew/bin/npx", "/usr/local/bin/npx"];
1094
+ for (const candidate of candidates) {
1095
+ if (existsSync4(candidate)) {
1096
+ return candidate;
1097
+ }
1098
+ }
1099
+ return "npx";
1100
+ }
1101
+ function buildClaudeDesktopServers(config, host) {
1102
+ const npxPath = detectNpxPath();
1103
+ const npxDir = npxPath === "npx" ? "/usr/local/bin" : npxPath.substring(0, npxPath.lastIndexOf("/"));
1104
+ const envPath = `${npxDir}:/usr/local/bin:/usr/bin:/bin`;
1105
+ return {
1106
+ anvil: { command: npxPath, args: ["mcp-remote", `http://${host}:${config.ports.anvil}/mcp`], env: { PATH: envPath } },
1107
+ vault: { command: npxPath, args: ["mcp-remote", `http://${host}:${config.ports.vault_mcp}/mcp`], env: { PATH: envPath } },
1108
+ forge: { command: npxPath, args: ["mcp-remote", `http://${host}:${config.ports.forge}/mcp`], env: { PATH: envPath } }
1109
+ };
1110
+ }
1091
1111
  async function isClaudeCliAvailable() {
1092
1112
  try {
1093
1113
  const result = await execa2("claude", ["--version"], { reject: false });
@@ -1118,12 +1138,14 @@ async function registerWithClaudeCode(mcpServers) {
1118
1138
  async function syncSkills(runtime) {
1119
1139
  const home = homedir3();
1120
1140
  const skillsBase = join2(home, ".claude", "skills");
1121
- const skills = ["horus-anvil", "horus-vault", "horus-forge"];
1141
+ const skills = ["horus-anvil", "horus-vault", "horus-forge", "horus-context", "capture", "triage"];
1122
1142
  const forgeContainer = "horus-forge-1";
1143
+ const homeResult = await runtime.exec(forgeContainer, "sh", "-c", "echo $HOME");
1144
+ const containerHome = homeResult.stdout.trim();
1123
1145
  for (const skill of skills) {
1124
1146
  const destDir = join2(skillsBase, skill);
1125
1147
  mkdirSync2(destDir, { recursive: true });
1126
- const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
1148
+ const src = `${containerHome}/.claude/skills/${skill}/SKILL.md`;
1127
1149
  const dest = join2(destDir, "SKILL.md");
1128
1150
  const result = await runtime.exec(forgeContainer, "cat", src);
1129
1151
  if (result.exitCode === 0 && result.stdout.trim()) {
@@ -1135,11 +1157,13 @@ async function syncSkillsForCursor(runtime) {
1135
1157
  const home = homedir3();
1136
1158
  const rulesDir = join2(home, ".cursor", "rules");
1137
1159
  const skillsBase = join2(home, ".cursor", "skills-cursor");
1138
- const skills = ["horus-anvil", "horus-vault", "horus-forge"];
1160
+ const skills = ["horus-anvil", "horus-vault", "horus-forge", "horus-context", "capture", "triage"];
1139
1161
  const forgeContainer = "horus-forge-1";
1140
1162
  mkdirSync2(rulesDir, { recursive: true });
1163
+ const homeResult = await runtime.exec(forgeContainer, "sh", "-c", "echo $HOME");
1164
+ const containerHome = homeResult.stdout.trim();
1141
1165
  for (const skill of skills) {
1142
- const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
1166
+ const src = `${containerHome}/.claude/skills/${skill}/SKILL.md`;
1143
1167
  const result = await runtime.exec(forgeContainer, "cat", src);
1144
1168
  if (result.exitCode === 0 && result.stdout.trim()) {
1145
1169
  const ruleDest = join2(rulesDir, `${skill}.mdc`);
@@ -1186,7 +1210,8 @@ async function runConnect(config, runtime, targets, host = "localhost") {
1186
1210
  const desktopSpinner = ora(`Configuring ${chalk.cyan("claude-desktop")}...`).start();
1187
1211
  try {
1188
1212
  const configPath = getConfigPath(target);
1189
- mergeAndWriteConfig(configPath, httpServers);
1213
+ const desktopServers = buildClaudeDesktopServers(config, host);
1214
+ mergeAndWriteConfig(configPath, desktopServers);
1190
1215
  desktopSpinner.succeed(`Configured ${chalk.cyan("claude-desktop")} \u2014 ${chalk.dim(configPath)}`);
1191
1216
  configured.push(target);
1192
1217
  } catch (error) {
@@ -1338,7 +1363,7 @@ function extractHostname(url) {
1338
1363
  return "github.com";
1339
1364
  }
1340
1365
  }
1341
- var setupCommand = new Command2("setup").description("Interactive first-run setup for Horus").option("-y, --yes", "Non-interactive mode (use defaults + env vars)").option("--runtime <runtime>", "Container runtime to use: docker or podman (non-interactive only)").option("--data-dir <path>", "Data directory path").option("--repos-path <path>", "Host repos path for Forge scanning").option("--anvil-repo <url>", "Anvil notes repository URL").option("--vault-name <name>", "Vault name (can be specified multiple times)").option("--vault-repo <url>", "Vault knowledge-base repository URL (matches positionally with --vault-name)").option("--forge-repo <url>", "Forge registry repository URL").option("--github-token <token>", "GitHub personal access token for private repos (primary host)").action(async (opts) => {
1366
+ var setupCommand = new Command2("setup").description("Interactive first-run setup for Horus").option("-y, --yes", "Non-interactive mode (use defaults + env vars)").option("--runtime <runtime>", "Container runtime to use: docker or podman (non-interactive only)").option("--data-dir <path>", "Data directory path").option("--repos-path <path>", "Host repos path for Forge scanning").option("--anvil-repo <url>", "Anvil notes repository URL").option("--vault-name <name>", "Vault name (can be specified multiple times)").option("--vault-repo <url>", "Vault knowledge-base repository URL (matches positionally with --vault-name)").option("--forge-repo <url>", "Forge registry repository URL").option("--github-token <token>", "GitHub personal access token for private repos (primary host)").option("--claude-desktop", "Configure Claude Desktop MCP servers during setup (non-interactive opt-in)").action(async (opts) => {
1342
1367
  console.log("");
1343
1368
  console.log(chalk2.bold("Horus Setup"));
1344
1369
  console.log(chalk2.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
@@ -1483,6 +1508,7 @@ var setupCommand = new Command2("setup").description("Interactive first-run setu
1483
1508
  vault_rest: vault_rest ?? DEFAULT_PORTS.vault_rest,
1484
1509
  vault_mcp: vault_mcp ?? DEFAULT_PORTS.vault_mcp,
1485
1510
  vault_router: vault_router ?? DEFAULT_PORTS.vault_router,
1511
+ ui: DEFAULT_PORTS.ui,
1486
1512
  forge: forge ?? DEFAULT_PORTS.forge,
1487
1513
  typesense: DEFAULT_PORTS.typesense,
1488
1514
  neo4j_http: DEFAULT_PORTS.neo4j_http,
@@ -1736,11 +1762,28 @@ ${example(`${vaultName.trim()}-knowledge`)}
1736
1762
  const detectedClients = detectInstalledClients();
1737
1763
  if (detectedClients.length > 0) {
1738
1764
  console.log(chalk2.bold("Configuring AI clients..."));
1739
- try {
1740
- await runConnect(config, runtime, detectedClients, "localhost");
1741
- } catch (error) {
1742
- console.log(chalk2.yellow("Could not configure AI clients automatically."));
1743
- console.log(chalk2.dim(`Run ${chalk2.cyan("horus connect")} to configure them manually.`));
1765
+ let clientsToConnect = [...detectedClients];
1766
+ if (clientsToConnect.includes("claude-desktop")) {
1767
+ let configureDesktop;
1768
+ if (opts.yes) {
1769
+ configureDesktop = opts.claudeDesktop === true;
1770
+ if (!configureDesktop) {
1771
+ console.log(chalk2.dim("Skipping Claude Desktop (pass --claude-desktop to configure it)."));
1772
+ }
1773
+ } else {
1774
+ configureDesktop = opts.claudeDesktop === false ? false : await confirm({ message: "Setup for Claude Desktop?", default: true });
1775
+ }
1776
+ if (!configureDesktop) {
1777
+ clientsToConnect = clientsToConnect.filter((c) => c !== "claude-desktop");
1778
+ }
1779
+ }
1780
+ if (clientsToConnect.length > 0) {
1781
+ try {
1782
+ await runConnect(config, runtime, clientsToConnect, "localhost");
1783
+ } catch (error) {
1784
+ console.log(chalk2.yellow("Could not configure AI clients automatically."));
1785
+ console.log(chalk2.dim(`Run ${chalk2.cyan("horus connect")} to configure them manually.`));
1786
+ }
1744
1787
  }
1745
1788
  } else {
1746
1789
  console.log(chalk2.dim(`No AI clients detected. Run ${chalk2.cyan("horus connect")} after installing Claude Desktop, Claude Code, or Cursor.`));
@@ -3376,8 +3419,428 @@ testEnvCommand.command("seed").description("Populate a slot with test fixtures (
3376
3419
  console.log(chalk10.dim("Services will re-index automatically. Allow ~10s before running tests."));
3377
3420
  });
3378
3421
 
3422
+ // src/commands/help.ts
3423
+ import { Command as Command11 } from "commander";
3424
+ import chalk11 from "chalk";
3425
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
3426
+ import { join as join9, dirname as dirname3 } from "path";
3427
+ import { fileURLToPath as fileURLToPath3 } from "url";
3428
+
3429
+ // src/lib/guide-retrieval.ts
3430
+ var STOP_WORDS = /* @__PURE__ */ new Set([
3431
+ "the",
3432
+ "a",
3433
+ "an",
3434
+ "and",
3435
+ "or",
3436
+ "but",
3437
+ "if",
3438
+ "then",
3439
+ "else",
3440
+ "when",
3441
+ "while",
3442
+ "of",
3443
+ "in",
3444
+ "on",
3445
+ "at",
3446
+ "to",
3447
+ "from",
3448
+ "for",
3449
+ "with",
3450
+ "by",
3451
+ "as",
3452
+ "about",
3453
+ "this",
3454
+ "that",
3455
+ "these",
3456
+ "those",
3457
+ "it",
3458
+ "its",
3459
+ "they",
3460
+ "them",
3461
+ "their",
3462
+ "he",
3463
+ "she",
3464
+ "we",
3465
+ "you",
3466
+ "your",
3467
+ "our",
3468
+ "my",
3469
+ "me",
3470
+ "us",
3471
+ "his",
3472
+ "her",
3473
+ "him",
3474
+ "is",
3475
+ "are",
3476
+ "was",
3477
+ "were",
3478
+ "be",
3479
+ "been",
3480
+ "being",
3481
+ "am",
3482
+ "have",
3483
+ "has",
3484
+ "had",
3485
+ "having",
3486
+ "do",
3487
+ "does",
3488
+ "did",
3489
+ "doing",
3490
+ "will",
3491
+ "would",
3492
+ "could",
3493
+ "should",
3494
+ "can",
3495
+ "may",
3496
+ "might",
3497
+ "must",
3498
+ "shall",
3499
+ "not",
3500
+ "no",
3501
+ "yes",
3502
+ "so",
3503
+ "too",
3504
+ "very",
3505
+ "just",
3506
+ "only",
3507
+ "also",
3508
+ "all",
3509
+ "any",
3510
+ "some",
3511
+ "each",
3512
+ "every",
3513
+ "more",
3514
+ "most",
3515
+ "much",
3516
+ "many",
3517
+ "one",
3518
+ "two",
3519
+ "three",
3520
+ "here",
3521
+ "there",
3522
+ "where",
3523
+ "how",
3524
+ "why",
3525
+ "what",
3526
+ "who",
3527
+ "which"
3528
+ ]);
3529
+ function tokenizeQuery(text) {
3530
+ return text.toLowerCase().replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((t) => t.length >= 2 && !STOP_WORDS.has(t));
3531
+ }
3532
+ var K1 = 1.5;
3533
+ var B = 0.75;
3534
+ function termFreq(token, tokens) {
3535
+ let n = 0;
3536
+ for (const t of tokens) if (t === token) n++;
3537
+ return n;
3538
+ }
3539
+ function docFreq(token, guides) {
3540
+ let n = 0;
3541
+ for (const g of guides) {
3542
+ if (g.tokens.includes(token)) n++;
3543
+ }
3544
+ return n;
3545
+ }
3546
+ function bm25Score(queryTokens, doc, avgDocLen, n, guides) {
3547
+ let score = 0;
3548
+ const docLen = doc.tokens.length;
3549
+ for (const q of queryTokens) {
3550
+ const f = termFreq(q, doc.tokens);
3551
+ if (f === 0) continue;
3552
+ const df = docFreq(q, guides);
3553
+ const idf = Math.log((n - df + 0.5) / (df + 0.5) + 1);
3554
+ const norm = f * (K1 + 1) / (f + K1 * (1 - B + B * docLen / avgDocLen));
3555
+ score += idf * norm;
3556
+ }
3557
+ return score;
3558
+ }
3559
+ function retrieve(query, index, maxAlternates = 3) {
3560
+ const queryTokens = tokenizeQuery(query);
3561
+ if (queryTokens.length === 0 || index.guides.length === 0) {
3562
+ return { primary: null, alternates: [] };
3563
+ }
3564
+ const n = index.guides.length;
3565
+ const totalLen = index.guides.reduce((s, g) => s + g.tokens.length, 0);
3566
+ const avgDocLen = totalLen / n;
3567
+ const scored = index.guides.map((g) => ({ guide: g, score: bm25Score(queryTokens, g, avgDocLen, n, index.guides) })).filter((s) => s.score > 0).sort((a, b) => b.score - a.score);
3568
+ if (scored.length === 0) {
3569
+ return { primary: null, alternates: [] };
3570
+ }
3571
+ const primary = scored[0].guide;
3572
+ const alternates = scored.slice(1, 1 + maxAlternates).map((s) => s.guide);
3573
+ return { primary, alternates };
3574
+ }
3575
+
3576
+ // src/commands/help.ts
3577
+ function findGuidesDir() {
3578
+ let dir = dirname3(fileURLToPath3(import.meta.url));
3579
+ const seen = /* @__PURE__ */ new Set();
3580
+ while (dir !== dirname3(dir) && !seen.has(dir)) {
3581
+ seen.add(dir);
3582
+ const candidate = join9(dir, "guides");
3583
+ if (existsSync10(candidate) && existsSync10(join9(candidate, "index.json"))) {
3584
+ return candidate;
3585
+ }
3586
+ dir = dirname3(dir);
3587
+ }
3588
+ throw new Error(
3589
+ "Could not find the bundled Horus guides directory. Try reinstalling: npm install -g @arkhera30/cli"
3590
+ );
3591
+ }
3592
+ function loadIndex(guidesDir) {
3593
+ const indexPath = join9(guidesDir, "index.json");
3594
+ return JSON.parse(readFileSync7(indexPath, "utf8"));
3595
+ }
3596
+ function stripFrontMatter(content) {
3597
+ return content.replace(/^---\n[\s\S]*?\n---\n?/, "");
3598
+ }
3599
+ function printTopicIndex(index) {
3600
+ console.log("");
3601
+ console.log(chalk11.bold("Horus Help \u2014 Available Guides"));
3602
+ console.log(chalk11.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3603
+ console.log("");
3604
+ for (const g of index.guides) {
3605
+ console.log(` ${chalk11.cyan(g.slug.padEnd(20))} ${g.title}`);
3606
+ console.log(` ${" ".repeat(20)} ${chalk11.dim(g.description)}`);
3607
+ console.log("");
3608
+ }
3609
+ console.log(chalk11.dim("Example queries:"));
3610
+ console.log(chalk11.dim(" horus help how do I start"));
3611
+ console.log(chalk11.dim(" horus help what is a forge workspace"));
3612
+ console.log(chalk11.dim(" horus help create my first anvil note"));
3613
+ console.log("");
3614
+ console.log(chalk11.dim("To print a specific guide directly:"));
3615
+ console.log(chalk11.dim(" horus guide <slug>"));
3616
+ console.log("");
3617
+ }
3618
+ function printGuideBody(guidesDir, file) {
3619
+ const path = join9(guidesDir, file);
3620
+ const content = readFileSync7(path, "utf8");
3621
+ console.log(stripFrontMatter(content));
3622
+ }
3623
+ function printSeeAlso(alternates, guidesDir) {
3624
+ if (alternates.length === 0) return;
3625
+ console.log(chalk11.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3626
+ console.log(chalk11.bold("See also:"));
3627
+ for (const a of alternates) {
3628
+ console.log(` ${chalk11.cyan(a.slug.padEnd(20))} ${a.title}`);
3629
+ console.log(` ${" ".repeat(20)} ${chalk11.dim(join9(guidesDir, a.file))}`);
3630
+ }
3631
+ console.log("");
3632
+ }
3633
+ function printFooter() {
3634
+ console.log(
3635
+ chalk11.dim(
3636
+ "Run `horus guide <slug>` to print a specific guide without retrieval, or `horus guide` to list all."
3637
+ )
3638
+ );
3639
+ console.log("");
3640
+ }
3641
+ var helpCommand = new Command11("help").description("Search and print bundled Horus getting-started guides").argument("[query...]", "Natural-language query. Omit to see the topic index.").action((query) => {
3642
+ const guidesDir = findGuidesDir();
3643
+ const index = loadIndex(guidesDir);
3644
+ if (!query || query.length === 0) {
3645
+ printTopicIndex(index);
3646
+ return;
3647
+ }
3648
+ const queryStr = query.join(" ");
3649
+ const result = retrieve(queryStr, index, 3);
3650
+ if (!result.primary) {
3651
+ console.log("");
3652
+ console.log(chalk11.yellow(`No guide matched "${queryStr}".`));
3653
+ console.log("");
3654
+ console.log(chalk11.dim("Try `horus help` with no arguments to see the full topic index,"));
3655
+ console.log(chalk11.dim("or pick a slug directly with `horus guide <slug>`."));
3656
+ console.log("");
3657
+ return;
3658
+ }
3659
+ console.log("");
3660
+ console.log(chalk11.dim(`# ${result.primary.title} (${result.primary.slug})`));
3661
+ console.log("");
3662
+ printGuideBody(guidesDir, result.primary.file);
3663
+ console.log("");
3664
+ printSeeAlso(result.alternates, guidesDir);
3665
+ printFooter();
3666
+ });
3667
+
3668
+ // src/commands/guide.ts
3669
+ import { Command as Command12 } from "commander";
3670
+ import chalk12 from "chalk";
3671
+ import { readFileSync as readFileSync8, existsSync as existsSync11 } from "fs";
3672
+ import { join as join10, dirname as dirname4 } from "path";
3673
+ import { fileURLToPath as fileURLToPath4 } from "url";
3674
+ function findGuidesDir2() {
3675
+ let dir = dirname4(fileURLToPath4(import.meta.url));
3676
+ const seen = /* @__PURE__ */ new Set();
3677
+ while (dir !== dirname4(dir) && !seen.has(dir)) {
3678
+ seen.add(dir);
3679
+ const candidate = join10(dir, "guides");
3680
+ if (existsSync11(candidate) && existsSync11(join10(candidate, "index.json"))) {
3681
+ return candidate;
3682
+ }
3683
+ dir = dirname4(dir);
3684
+ }
3685
+ throw new Error(
3686
+ "Could not find the bundled Horus guides directory. Try reinstalling: npm install -g @arkhera30/cli"
3687
+ );
3688
+ }
3689
+ function loadIndex2(guidesDir) {
3690
+ return JSON.parse(readFileSync8(join10(guidesDir, "index.json"), "utf8"));
3691
+ }
3692
+ function lookupTopic(topic, index) {
3693
+ const t = topic.trim().toLowerCase();
3694
+ if (!t) return { tier: "none", matches: [] };
3695
+ const exact = index.guides.filter((g) => g.slug === t);
3696
+ if (exact.length > 0) return { tier: "exact-slug", matches: exact };
3697
+ const prefix = index.guides.filter((g) => g.slug.startsWith(t));
3698
+ if (prefix.length > 0) return { tier: "slug-prefix", matches: prefix };
3699
+ const titleHits = index.guides.filter((g) => g.title.toLowerCase().includes(t));
3700
+ if (titleHits.length > 0) return { tier: "title-fuzzy", matches: titleHits };
3701
+ const kwHits = index.guides.filter(
3702
+ (g) => g.keywords.some((k) => k.toLowerCase() === t)
3703
+ );
3704
+ if (kwHits.length > 0) return { tier: "keyword", matches: kwHits };
3705
+ return { tier: "none", matches: [] };
3706
+ }
3707
+ function printGuideList(index) {
3708
+ console.log("");
3709
+ console.log(chalk12.bold("Bundled Horus Guides"));
3710
+ console.log(chalk12.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
3711
+ console.log("");
3712
+ for (const g of index.guides) {
3713
+ console.log(` ${chalk12.cyan(g.slug.padEnd(20))} ${g.title}`);
3714
+ console.log(` ${" ".repeat(20)} ${chalk12.dim(g.description)}`);
3715
+ console.log("");
3716
+ }
3717
+ console.log(chalk12.dim("Print a guide: horus guide <slug>"));
3718
+ console.log(chalk12.dim("Print a guide file path: horus guide <slug> --path"));
3719
+ console.log(chalk12.dim("Print the guides root: horus guide --path"));
3720
+ console.log("");
3721
+ }
3722
+ function printGuideBody2(guidesDir, file) {
3723
+ const content = readFileSync8(join10(guidesDir, file), "utf8");
3724
+ console.log(content.replace(/^---\n[\s\S]*?\n---\n?/, ""));
3725
+ }
3726
+ function printDisambiguation(tier, matches) {
3727
+ console.log("");
3728
+ console.log(chalk12.yellow(`Multiple guides matched (tier: ${tier}):`));
3729
+ console.log("");
3730
+ for (const m of matches) {
3731
+ console.log(` ${chalk12.cyan(m.slug.padEnd(20))} ${m.title}`);
3732
+ }
3733
+ console.log("");
3734
+ console.log(chalk12.dim("Pick one: horus guide <slug>"));
3735
+ console.log("");
3736
+ }
3737
+ var guideCommand = new Command12("guide").description("Print a bundled Horus guide, or list all guides").argument("[topic]", "Slug, slug prefix, or search term. Omit to list all guides.").option("--path", "Print the file path instead of the body (or the guides dir root if no topic)").action((topic, opts) => {
3738
+ const guidesDir = findGuidesDir2();
3739
+ const index = loadIndex2(guidesDir);
3740
+ if (!topic) {
3741
+ if (opts.path) {
3742
+ console.log(guidesDir);
3743
+ return;
3744
+ }
3745
+ printGuideList(index);
3746
+ return;
3747
+ }
3748
+ const result = lookupTopic(topic, index);
3749
+ if (result.matches.length === 0) {
3750
+ console.log("");
3751
+ console.log(chalk12.yellow(`No guide matched "${topic}".`));
3752
+ console.log("");
3753
+ console.log(chalk12.dim("Run `horus guide` to see all available guides."));
3754
+ console.log("");
3755
+ process.exitCode = 1;
3756
+ return;
3757
+ }
3758
+ if (result.matches.length > 1) {
3759
+ printDisambiguation(result.tier, result.matches);
3760
+ process.exitCode = 1;
3761
+ return;
3762
+ }
3763
+ const match = result.matches[0];
3764
+ if (opts.path) {
3765
+ console.log(join10(guidesDir, match.file));
3766
+ return;
3767
+ }
3768
+ printGuideBody2(guidesDir, match.file);
3769
+ });
3770
+
3771
+ // src/commands/repo.ts
3772
+ import { Command as Command13 } from "commander";
3773
+ import chalk13 from "chalk";
3774
+ import ora9 from "ora";
3775
+ var repoCommand = new Command13("repo").description("Manage the Forge repository index");
3776
+ repoCommand.command("rindex").alias("scan").description("Trigger a full repository index rescan via Forge").action(async () => {
3777
+ if (!configExists()) {
3778
+ console.log(chalk13.red("Horus is not set up yet."));
3779
+ console.log(chalk13.dim("Run `horus setup` first."));
3780
+ process.exit(1);
3781
+ }
3782
+ const config = loadConfig();
3783
+ const forgePort = config.ports.forge ?? 8200;
3784
+ const forgeUrl = `http://localhost:${forgePort}/mcp`;
3785
+ const spinner = ora9("Scanning repositories...").start();
3786
+ let body;
3787
+ try {
3788
+ const res = await fetch(forgeUrl, {
3789
+ method: "POST",
3790
+ headers: { "Content-Type": "application/json" },
3791
+ body: JSON.stringify({
3792
+ jsonrpc: "2.0",
3793
+ id: 1,
3794
+ method: "tools/call",
3795
+ params: { name: "forge_repo_scan", arguments: {} }
3796
+ })
3797
+ });
3798
+ body = await res.text();
3799
+ if (!res.ok) {
3800
+ spinner.fail(`Forge returned HTTP ${res.status}`);
3801
+ console.error(chalk13.red(body));
3802
+ process.exit(1);
3803
+ }
3804
+ } catch (err) {
3805
+ spinner.fail("Could not reach Forge");
3806
+ console.error(chalk13.red(`Is Horus running? (horus up)`));
3807
+ console.error(chalk13.dim(err.message));
3808
+ process.exit(1);
3809
+ }
3810
+ let parsed;
3811
+ try {
3812
+ parsed = JSON.parse(body);
3813
+ } catch {
3814
+ spinner.fail("Unexpected response from Forge");
3815
+ console.error(body);
3816
+ process.exit(1);
3817
+ }
3818
+ if (parsed.error) {
3819
+ spinner.fail("Scan failed");
3820
+ console.error(chalk13.red(parsed.error.message ?? JSON.stringify(parsed.error)));
3821
+ process.exit(1);
3822
+ }
3823
+ let result = {};
3824
+ try {
3825
+ const text = parsed.result?.content?.[0]?.text ?? "{}";
3826
+ result = JSON.parse(text);
3827
+ } catch {
3828
+ }
3829
+ spinner.succeed("Repository scan complete");
3830
+ console.log("");
3831
+ console.log(` ${chalk13.bold("Scan paths:")} ${(result.scanPaths ?? []).length}`);
3832
+ console.log(` ${chalk13.bold("Repos found:")} ${result.reposFound ?? 0}`);
3833
+ if (result.repos && result.repos.length > 0) {
3834
+ console.log("");
3835
+ for (const repo of result.repos) {
3836
+ console.log(` ${chalk13.green("\u2713")} ${chalk13.bold(repo.name)} ${chalk13.dim(repo.localPath)}`);
3837
+ }
3838
+ }
3839
+ console.log("");
3840
+ });
3841
+
3379
3842
  // src/index.ts
3380
- var program = new Command11();
3843
+ var program = new Command14();
3381
3844
  program.name("horus").description("CLI for managing the Horus Docker Compose stack").version(CLI_VERSION);
3382
3845
  program.addCommand(setupCommand);
3383
3846
  program.addCommand(upCommand);
@@ -3389,6 +3852,9 @@ program.addCommand(updateCommand);
3389
3852
  program.addCommand(doctorCommand);
3390
3853
  program.addCommand(backupCommand);
3391
3854
  program.addCommand(testEnvCommand);
3855
+ program.addCommand(helpCommand);
3856
+ program.addCommand(guideCommand);
3857
+ program.addCommand(repoCommand);
3392
3858
  program.exitOverride();
3393
3859
  try {
3394
3860
  await program.parseAsync(process.argv);
@@ -3397,9 +3863,9 @@ try {
3397
3863
  process.exit(0);
3398
3864
  }
3399
3865
  if (error instanceof Error) {
3400
- console.error(chalk11.red(`Error: ${error.message}`));
3866
+ console.error(chalk14.red(`Error: ${error.message}`));
3401
3867
  } else {
3402
- console.error(chalk11.red("An unexpected error occurred."));
3868
+ console.error(chalk14.red("An unexpected error occurred."));
3403
3869
  }
3404
3870
  process.exit(1);
3405
3871
  }