@arkhera30/cli 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -198,8 +198,9 @@ services:
198
198
  - FORGE_HOST_ANVIL_URL=http://localhost:${ANVIL_PORT:-8100}
199
199
  - FORGE_HOST_VAULT_URL=http://localhost:${VAULT_MCP_PORT:-8300}
200
200
  - FORGE_HOST_FORGE_URL=http://localhost:${FORGE_PORT:-8200}
201
- # Colon-separated list of in-container paths to scan for git repos
202
- - FORGE_SCAN_PATHS=/data/repos
201
+ # Colon-separated list of in-container paths to scan for git repos.
202
+ # Generated by the CLI from host_repos_extra_scan_dirs config — do not edit manually.
203
+ - FORGE_SCAN_PATHS=${FORGE_SCAN_PATHS:-/data/repos}
203
204
  - GITHUB_TOKEN=${GITHUB_TOKEN:-}
204
205
  depends_on:
205
206
  anvil:
package/dist/index.js CHANGED
@@ -6,12 +6,12 @@ import chalk10 from "chalk";
6
6
  import { createRequire } from "module";
7
7
 
8
8
  // src/commands/setup.ts
9
- import { Command } from "commander";
10
- import chalk from "chalk";
11
- import ora from "ora";
9
+ import { Command as Command2 } from "commander";
10
+ import chalk2 from "chalk";
11
+ import ora2 from "ora";
12
12
  import { execSync } from "child_process";
13
- import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
14
- import { join as join3 } from "path";
13
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
14
+ import { join as join4 } from "path";
15
15
  import { input, confirm, number, select, password } from "@inquirer/prompts";
16
16
 
17
17
  // src/lib/config.ts
@@ -58,6 +58,7 @@ function defaultConfig() {
58
58
  git_host: "github.com",
59
59
  repos: { ...DEFAULT_REPOS },
60
60
  host_repos_path: "",
61
+ host_repos_extra_scan_dirs: [],
61
62
  github_token: ""
62
63
  };
63
64
  }
@@ -91,6 +92,7 @@ function loadConfig() {
91
92
  forge_registry: parsed.repos?.forge_registry ?? defaults.repos.forge_registry
92
93
  },
93
94
  host_repos_path: parsed.host_repos_path ?? defaults.host_repos_path,
95
+ host_repos_extra_scan_dirs: parsed.host_repos_extra_scan_dirs ?? defaults.host_repos_extra_scan_dirs,
94
96
  github_token: parsed.github_token ?? defaults.github_token
95
97
  };
96
98
  }
@@ -108,6 +110,9 @@ function resolvePath(p) {
108
110
  function generateEnv(config) {
109
111
  const dataDir = resolvePath(config.data_dir);
110
112
  const hostReposPath = config.host_repos_path ? resolvePath(config.host_repos_path) : "";
113
+ const baseScanPath = "/data/repos";
114
+ const extraScanPaths = (config.host_repos_extra_scan_dirs ?? []).map((d) => d.trim()).filter(Boolean).map((d) => `${baseScanPath}/${d}`);
115
+ const forgeScanPaths = [baseScanPath, ...extraScanPaths].join(":");
111
116
  const lines = [
112
117
  "# \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\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\u2500",
113
118
  "# Horus \u2014 Generated .env file",
@@ -116,6 +121,7 @@ function generateEnv(config) {
116
121
  "",
117
122
  `HORUS_DATA_PATH=${dataDir}`,
118
123
  `HOST_REPOS_PATH=${hostReposPath}`,
124
+ `FORGE_SCAN_PATHS=${forgeScanPaths}`,
119
125
  "",
120
126
  "# Ports",
121
127
  `ANVIL_PORT=${config.ports.anvil}`,
@@ -142,6 +148,7 @@ function writeEnvFile(config) {
142
148
  var CONFIG_KEYS = [
143
149
  "data-dir",
144
150
  "host-repos-path",
151
+ "host-repos-extra-scan-dirs",
145
152
  "runtime",
146
153
  "port.anvil",
147
154
  "port.vault-rest",
@@ -159,6 +166,8 @@ function getConfigValue(config, key) {
159
166
  return config.data_dir;
160
167
  case "host-repos-path":
161
168
  return config.host_repos_path;
169
+ case "host-repos-extra-scan-dirs":
170
+ return (config.host_repos_extra_scan_dirs ?? []).join(", ");
162
171
  case "runtime":
163
172
  return config.runtime;
164
173
  case "port.anvil":
@@ -190,6 +199,9 @@ function setConfigValue(config, key, value) {
190
199
  case "host-repos-path":
191
200
  updated.host_repos_path = value;
192
201
  break;
202
+ case "host-repos-extra-scan-dirs":
203
+ updated.host_repos_extra_scan_dirs = value.split(",").map((d) => d.trim()).filter(Boolean);
204
+ break;
193
205
  case "runtime":
194
206
  if (value !== "docker" && value !== "podman") {
195
207
  throw new Error(`Invalid runtime: ${value}. Must be "docker" or "podman".`);
@@ -258,11 +270,13 @@ async function commandExists(command) {
258
270
  }
259
271
  function createRuntime(name) {
260
272
  const bin = name;
273
+ const composeEnv = { ...process.env, HORUS_RUNTIME: name };
261
274
  return {
262
275
  name,
263
276
  async compose(...args) {
264
277
  const result = await execa(bin, ["compose", ...args], {
265
278
  cwd: HORUS_DIR,
279
+ env: composeEnv,
266
280
  reject: false
267
281
  });
268
282
  if (result.exitCode !== 0) {
@@ -290,6 +304,7 @@ function createRuntime(name) {
290
304
  try {
291
305
  const result = await execa(bin, ["compose", "ps", "--format", "json"], {
292
306
  cwd: HORUS_DIR,
307
+ env: composeEnv,
293
308
  reject: false
294
309
  });
295
310
  return result.exitCode === 0 && result.stdout.toString().trim().length > 0;
@@ -331,6 +346,7 @@ async function composeStreaming(runtime, args) {
331
346
  const bin = runtime.name;
332
347
  const result = await execa(bin, ["compose", ...args], {
333
348
  cwd: HORUS_DIR,
349
+ env: { ...process.env, HORUS_RUNTIME: runtime.name },
334
350
  stdio: "inherit",
335
351
  reject: false
336
352
  });
@@ -420,16 +436,294 @@ function getBundledComposePath() {
420
436
  Searched: ${candidates.join(", ")}`
421
437
  );
422
438
  }
439
+ function applyPodmanUserOverride(compose) {
440
+ return compose.replace(
441
+ /^( image: .+)$/gm,
442
+ '$1\n user: "0:0"'
443
+ );
444
+ }
423
445
  function composeFileExists() {
424
446
  return existsSync2(COMPOSE_PATH);
425
447
  }
426
- function installComposeFile() {
448
+ function installComposeFile(runtime) {
427
449
  ensureHorusDir();
428
450
  const bundledPath = getBundledComposePath();
429
- const content = readFileSync2(bundledPath, "utf-8");
451
+ let content = readFileSync2(bundledPath, "utf-8");
452
+ if (runtime === "podman") {
453
+ content = applyPodmanUserOverride(content);
454
+ }
430
455
  writeFileSync2(COMPOSE_PATH, content, "utf-8");
431
456
  }
432
457
 
458
+ // src/commands/connect.ts
459
+ import { Command } from "commander";
460
+ import chalk from "chalk";
461
+ import ora from "ora";
462
+ import { checkbox } from "@inquirer/prompts";
463
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
464
+ import { join as join3 } from "path";
465
+ import { homedir as homedir3 } from "os";
466
+ import { execa as execa2 } from "execa";
467
+ function detectInstalledClients() {
468
+ const detected = [];
469
+ const home = homedir3();
470
+ const claudeDesktopDir = join3(home, "Library", "Application Support", "Claude");
471
+ if (existsSync3(claudeDesktopDir)) {
472
+ detected.push("claude-desktop");
473
+ }
474
+ const claudeCodeDir = join3(home, ".claude");
475
+ if (existsSync3(claudeCodeDir)) {
476
+ detected.push("claude-code");
477
+ }
478
+ const cursorDir = join3(home, ".cursor");
479
+ const cursorAppDir = join3(home, "Library", "Application Support", "Cursor");
480
+ if (existsSync3(cursorDir) || existsSync3(cursorAppDir)) {
481
+ detected.push("cursor");
482
+ }
483
+ return detected;
484
+ }
485
+ function getConfigPath(target) {
486
+ const home = homedir3();
487
+ switch (target) {
488
+ case "claude-desktop":
489
+ return join3(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
490
+ case "claude-code":
491
+ return join3(home, ".claude", "settings.json");
492
+ case "cursor":
493
+ return join3(home, ".cursor", "mcp.json");
494
+ }
495
+ }
496
+ function mergeAndWriteConfig(configPath, mcpServers) {
497
+ let existing = {};
498
+ if (existsSync3(configPath)) {
499
+ try {
500
+ const raw = readFileSync3(configPath, "utf-8");
501
+ existing = JSON.parse(raw);
502
+ } catch {
503
+ existing = {};
504
+ }
505
+ }
506
+ const existingServers = existing.mcpServers ?? {};
507
+ existing.mcpServers = { ...existingServers, ...mcpServers };
508
+ const dir = configPath.substring(0, configPath.lastIndexOf("/"));
509
+ mkdirSync2(dir, { recursive: true });
510
+ writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
511
+ }
512
+ async function isClaudeCliAvailable() {
513
+ try {
514
+ const result = await execa2("claude", ["--version"], { reject: false });
515
+ return result.exitCode === 0;
516
+ } catch {
517
+ return false;
518
+ }
519
+ }
520
+ async function registerWithClaudeCode(mcpServers) {
521
+ const registered = [];
522
+ const failed = [];
523
+ for (const [name, entry] of Object.entries(mcpServers)) {
524
+ const baseUrl = entry.url.replace(/\/sse$/, "");
525
+ const result = await execa2(
526
+ "claude",
527
+ ["mcp", "add", "--transport", "http", "--scope", "user", name, baseUrl],
528
+ { reject: false }
529
+ );
530
+ if (result.exitCode === 0) {
531
+ registered.push(name);
532
+ } else {
533
+ failed.push(name);
534
+ }
535
+ }
536
+ return { registered, failed };
537
+ }
538
+ async function syncSkills(runtime) {
539
+ const home = homedir3();
540
+ const skillsBase = join3(home, ".claude", "skills");
541
+ const skills = ["horus-anvil", "horus-vault", "horus-forge"];
542
+ const forgeContainer = "horus-forge-1";
543
+ for (const skill of skills) {
544
+ const destDir = join3(skillsBase, skill);
545
+ mkdirSync2(destDir, { recursive: true });
546
+ const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
547
+ const dest = join3(destDir, "SKILL.md");
548
+ const result = await runtime.exec(forgeContainer, "cat", src);
549
+ if (result.exitCode === 0 && result.stdout.trim()) {
550
+ writeFileSync3(dest, result.stdout, "utf-8");
551
+ }
552
+ }
553
+ }
554
+ async function syncSkillsForCursor(runtime) {
555
+ const home = homedir3();
556
+ const rulesDir = join3(home, ".cursor", "rules");
557
+ const skills = ["horus-anvil", "horus-vault", "horus-forge"];
558
+ const forgeContainer = "horus-forge-1";
559
+ mkdirSync2(rulesDir, { recursive: true });
560
+ for (const skill of skills) {
561
+ const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
562
+ const dest = join3(rulesDir, `${skill}.mdc`);
563
+ const result = await runtime.exec(forgeContainer, "cat", src);
564
+ if (result.exitCode === 0 && result.stdout.trim()) {
565
+ const frontmatter = `---
566
+ description: Horus ${skill} reference
567
+ alwaysApply: true
568
+ ---
569
+
570
+ `;
571
+ writeFileSync3(dest, frontmatter + result.stdout, "utf-8");
572
+ }
573
+ }
574
+ }
575
+ function printNextSteps(targets) {
576
+ console.log("");
577
+ console.log(chalk.bold("Next steps:"));
578
+ for (const target of targets) {
579
+ switch (target) {
580
+ case "claude-desktop":
581
+ console.log(` ${chalk.cyan("Claude Desktop")} Restart Claude Desktop to pick up the new MCP configuration`);
582
+ break;
583
+ case "claude-code":
584
+ console.log(` ${chalk.cyan("Claude Code")} Start a new Claude Code session`);
585
+ break;
586
+ case "cursor":
587
+ console.log(` ${chalk.cyan("Cursor")} Restart Cursor to pick up the new MCP configuration and rules`);
588
+ break;
589
+ }
590
+ }
591
+ console.log("");
592
+ }
593
+ async function runConnect(config, runtime, targets, host = "localhost") {
594
+ const mcpServers = {
595
+ anvil: { url: `http://${host}:${config.ports.anvil}/sse` },
596
+ vault: { url: `http://${host}:${config.ports.vault_mcp}/sse` },
597
+ forge: { url: `http://${host}:${config.ports.forge}/sse` }
598
+ };
599
+ const configured = [];
600
+ for (const target of targets) {
601
+ if (target === "claude-code") {
602
+ const cliSpinner = ora("Registering MCP servers with Claude Code CLI...").start();
603
+ const cliAvailable = await isClaudeCliAvailable();
604
+ if (cliAvailable) {
605
+ const { registered, failed } = await registerWithClaudeCode(mcpServers);
606
+ if (failed.length === 0) {
607
+ cliSpinner.succeed(
608
+ `Registered with Claude Code: ${registered.map((n) => chalk.cyan(n)).join(", ")}`
609
+ );
610
+ configured.push(target);
611
+ } else if (registered.length > 0) {
612
+ cliSpinner.warn(
613
+ `Partially registered \u2014 ok: ${registered.join(", ")}, failed: ${failed.join(", ")}`
614
+ );
615
+ configured.push(target);
616
+ } else {
617
+ cliSpinner.fail("Failed to register MCP servers with Claude Code CLI");
618
+ }
619
+ } else {
620
+ cliSpinner.warn("claude CLI not found on PATH \u2014 register manually:");
621
+ for (const [name, entry] of Object.entries(mcpServers)) {
622
+ const baseUrl = entry.url.replace(/\/sse$/, "");
623
+ console.log(
624
+ chalk.dim(` claude mcp add --transport http --scope user ${name} ${baseUrl}`)
625
+ );
626
+ }
627
+ }
628
+ } else {
629
+ const configPath = getConfigPath(target);
630
+ const writeSpinner = ora(`Configuring ${chalk.cyan(target)}...`).start();
631
+ try {
632
+ mergeAndWriteConfig(configPath, mcpServers);
633
+ writeSpinner.succeed(`Configured ${chalk.cyan(target)} \u2014 ${chalk.dim(configPath)}`);
634
+ configured.push(target);
635
+ } catch (error) {
636
+ writeSpinner.fail(`Failed to configure ${target}`);
637
+ console.log(chalk.dim(error.message));
638
+ }
639
+ }
640
+ }
641
+ if (targets.includes("claude-code")) {
642
+ const skillsSpinner = ora("Syncing horus-core skills...").start();
643
+ try {
644
+ await syncSkills(runtime);
645
+ skillsSpinner.succeed("horus-core skills synced to ~/.claude/skills/");
646
+ } catch (error) {
647
+ skillsSpinner.warn("Could not sync skills (Forge container may not be running)");
648
+ console.log(chalk.dim(error.message));
649
+ }
650
+ }
651
+ if (targets.includes("cursor")) {
652
+ const cursorRulesSpinner = ora("Syncing horus-core rules for Cursor...").start();
653
+ try {
654
+ await syncSkillsForCursor(runtime);
655
+ cursorRulesSpinner.succeed("horus-core rules synced to ~/.cursor/rules/");
656
+ } catch (error) {
657
+ cursorRulesSpinner.warn("Could not sync Cursor rules (Forge container may not be running)");
658
+ console.log(chalk.dim(error.message));
659
+ }
660
+ }
661
+ if (configured.length > 0) {
662
+ printNextSteps(configured);
663
+ }
664
+ return configured;
665
+ }
666
+ var connectCommand = new Command("connect").description("Configure Claude/Cursor MCP integration").option("--target <client>", "Client to configure: claude-desktop, claude-code, cursor, all (default: auto-detect)").option("--host <host>", "MCP host (default: localhost)", "localhost").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
667
+ console.log("");
668
+ console.log(chalk.bold("Horus Connect"));
669
+ console.log(chalk.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"));
670
+ console.log("");
671
+ const config = loadConfig();
672
+ const runtimeSpinner = ora("Detecting runtime...").start();
673
+ let runtime;
674
+ try {
675
+ runtime = await detectRuntime(config.runtime);
676
+ runtimeSpinner.succeed(`Using ${chalk.cyan(runtime.name)}`);
677
+ } catch (error) {
678
+ runtimeSpinner.fail("No container runtime found");
679
+ console.log(error.message);
680
+ process.exit(1);
681
+ }
682
+ const runningSpinner = ora("Checking Horus status...").start();
683
+ const running = await runtime.isRunning();
684
+ if (!running) {
685
+ runningSpinner.fail("Horus is not running");
686
+ console.log(chalk.dim("Run `horus up` first, then re-run `horus connect`."));
687
+ process.exit(1);
688
+ }
689
+ runningSpinner.succeed("Horus is running");
690
+ let targets = [];
691
+ if (opts.target === "all") {
692
+ targets = ["claude-desktop", "claude-code", "cursor"];
693
+ } else if (opts.target) {
694
+ const valid = ["claude-desktop", "claude-code", "cursor"];
695
+ if (!valid.includes(opts.target)) {
696
+ console.log(chalk.red(`Invalid target: ${opts.target}`));
697
+ console.log(chalk.dim("Valid targets: claude-desktop, claude-code, cursor, all"));
698
+ process.exit(1);
699
+ }
700
+ targets = [opts.target];
701
+ } else {
702
+ const detected = detectInstalledClients();
703
+ if (detected.length === 0) {
704
+ console.log(chalk.yellow("No supported clients detected (Claude Desktop, Claude Code, or Cursor)."));
705
+ console.log(chalk.dim("Use --target to specify a client manually."));
706
+ process.exit(1);
707
+ }
708
+ if (opts.yes) {
709
+ targets = detected;
710
+ console.log(`Detected clients: ${detected.map((t) => chalk.cyan(t)).join(", ")}`);
711
+ } else {
712
+ const chosen = await checkbox({
713
+ message: "Select clients to configure:",
714
+ choices: detected.map((t) => ({ name: t, value: t, checked: true })),
715
+ validate: (input2) => input2.length > 0 ? true : "Select at least one client."
716
+ });
717
+ targets = chosen;
718
+ }
719
+ }
720
+ if (targets.length === 0) {
721
+ console.log(chalk.yellow("No clients selected. Exiting."));
722
+ return;
723
+ }
724
+ await runConnect(config, runtime, targets, opts.host);
725
+ });
726
+
433
727
  // src/commands/setup.ts
434
728
  function injectToken(url, token) {
435
729
  if (!token) return url;
@@ -442,26 +736,26 @@ function injectToken(url, token) {
442
736
  return url;
443
737
  }
444
738
  }
445
- var setupCommand = new Command("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("--git-host <host>", "Git server hostname (e.g., github.com, gitlab.corp.com)").option("--anvil-repo <url>", "Anvil notes repository URL").option("--vault-repo <url>", "Vault knowledge-base repository URL").option("--forge-repo <url>", "Forge registry repository URL").option("--github-token <token>", "GitHub personal access token for private repos").action(async (opts) => {
739
+ 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("--git-host <host>", "Git server hostname (e.g., github.com, gitlab.corp.com)").option("--anvil-repo <url>", "Anvil notes repository URL").option("--vault-repo <url>", "Vault knowledge-base repository URL").option("--forge-repo <url>", "Forge registry repository URL").option("--github-token <token>", "GitHub personal access token for private repos").action(async (opts) => {
446
740
  console.log("");
447
- console.log(chalk.bold("Horus Setup"));
448
- console.log(chalk.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"));
741
+ console.log(chalk2.bold("Horus Setup"));
742
+ 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"));
449
743
  console.log("");
450
744
  if (configExists()) {
451
745
  if (opts.yes) {
452
- console.log(chalk.yellow("Existing configuration found. Overwriting in non-interactive mode."));
746
+ console.log(chalk2.yellow("Existing configuration found. Overwriting in non-interactive mode."));
453
747
  } else {
454
748
  const proceed = await confirm({
455
749
  message: "Horus is already configured. Reconfigure?",
456
750
  default: false
457
751
  });
458
752
  if (!proceed) {
459
- console.log(chalk.dim("Setup cancelled."));
753
+ console.log(chalk2.dim("Setup cancelled."));
460
754
  return;
461
755
  }
462
756
  }
463
757
  }
464
- const checkSpinner = ora("Checking for container runtimes...").start();
758
+ const checkSpinner = ora2("Checking for container runtimes...").start();
465
759
  const [hasDocker, hasPodman] = await Promise.all([
466
760
  checkRuntime("docker"),
467
761
  checkRuntime("podman")
@@ -472,7 +766,7 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
472
766
  ...hasPodman ? ["podman"] : []
473
767
  ];
474
768
  if (available.length === 0) {
475
- console.log(chalk.red("No container runtime found."));
769
+ console.log(chalk2.red("No container runtime found."));
476
770
  console.log("");
477
771
  console.log("Horus requires Docker or Podman with the Compose plugin.");
478
772
  console.log("");
@@ -485,12 +779,12 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
485
779
  if (opts.yes) {
486
780
  const requested = opts.runtime;
487
781
  if (requested && !available.includes(requested)) {
488
- console.log(chalk.red(`Requested runtime "${requested}" is not installed.`));
489
- console.log(chalk.dim(`Available: ${available.join(", ")}`));
782
+ console.log(chalk2.red(`Requested runtime "${requested}" is not installed.`));
783
+ console.log(chalk2.dim(`Available: ${available.join(", ")}`));
490
784
  process.exit(1);
491
785
  }
492
786
  selectedRuntime = requested ?? available[0];
493
- console.log(`Using ${chalk.cyan(selectedRuntime)}`);
787
+ console.log(`Using ${chalk2.cyan(selectedRuntime)}`);
494
788
  } else {
495
789
  selectedRuntime = await select({
496
790
  message: "Which container runtime would you like to use?",
@@ -526,6 +820,11 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
526
820
  message: "Host repos path (for Forge repo scanning, leave empty to skip):",
527
821
  default: ""
528
822
  });
823
+ const extra_scan_dirs_raw = await input({
824
+ message: "Extra subdirectories to scan within repos path (comma-separated, e.g. ArjunKhera \u2014 leave empty to skip):",
825
+ default: ""
826
+ });
827
+ const host_repos_extra_scan_dirs = extra_scan_dirs_raw.split(",").map((d) => d.trim()).filter(Boolean);
529
828
  const customize_ports = await confirm({
530
829
  message: "Customize port assignments?",
531
830
  default: false
@@ -556,19 +855,19 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
556
855
  };
557
856
  }
558
857
  console.log("");
559
- console.log(chalk.bold("Repository Configuration"));
560
- console.log(chalk.dim("Horus stores notes and knowledge in Git repos you own."));
561
- console.log(chalk.dim("Create empty repos on your Git server, then paste the URLs below."));
858
+ console.log(chalk2.bold("Repository Configuration"));
859
+ console.log(chalk2.dim("Horus stores notes and knowledge in Git repos you own."));
860
+ console.log(chalk2.dim("Create empty repos on your Git server, then paste the URLs below."));
562
861
  console.log("");
563
- console.log(chalk.yellow(" Use HTTPS URLs \u2014 container services do not have SSH keys."));
564
- console.log(chalk.dim(" SSH URLs (git@github.com:...) will fail at runtime inside Docker/Podman."));
862
+ console.log(chalk2.yellow(" Use HTTPS URLs \u2014 container services do not have SSH keys."));
863
+ console.log(chalk2.dim(" SSH URLs (git@github.com:...) will fail at runtime inside Docker/Podman."));
565
864
  console.log("");
566
865
  const git_host = await input({
567
866
  message: "Git server hostname:",
568
867
  default: "github.com"
569
868
  });
570
869
  const host = git_host.trim();
571
- const example = (repo) => chalk.dim(` e.g., https://${host}/<owner>/${repo}`);
870
+ const example = (repo) => chalk2.dim(` e.g., https://${host}/<owner>/${repo}`);
572
871
  console.log("");
573
872
  const anvil_notes = await input({
574
873
  message: `Anvil notes repo URL:
@@ -589,8 +888,8 @@ ${example("forge-registry")}
589
888
  validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
590
889
  });
591
890
  console.log("");
592
- console.log(chalk.bold("Authentication"));
593
- console.log(chalk.dim("A personal access token is required for private repositories."));
891
+ console.log(chalk2.bold("Authentication"));
892
+ console.log(chalk2.dim("A personal access token is required for private repositories."));
594
893
  console.log("");
595
894
  const github_token = await password({
596
895
  message: "GitHub personal access token (leave empty to skip):",
@@ -600,6 +899,7 @@ ${example("forge-registry")}
600
899
  ...defaultConfig(),
601
900
  data_dir,
602
901
  host_repos_path,
902
+ host_repos_extra_scan_dirs,
603
903
  runtime: runtime.name,
604
904
  ports,
605
905
  git_host: git_host.trim(),
@@ -611,7 +911,7 @@ ${example("forge-registry")}
611
911
  github_token: github_token.trim()
612
912
  };
613
913
  }
614
- const configSpinner = ora("Saving configuration...").start();
914
+ const configSpinner = ora2("Saving configuration...").start();
615
915
  try {
616
916
  saveConfig(config);
617
917
  configSpinner.succeed("Configuration saved to ~/.horus/config.yaml");
@@ -620,7 +920,7 @@ ${example("forge-registry")}
620
920
  console.error(error.message);
621
921
  process.exit(1);
622
922
  }
623
- const envSpinner = ora("Generating .env file...").start();
923
+ const envSpinner = ora2("Generating .env file...").start();
624
924
  try {
625
925
  writeEnvFile(config);
626
926
  envSpinner.succeed("Environment file written to ~/.horus/.env");
@@ -629,33 +929,33 @@ ${example("forge-registry")}
629
929
  console.error(error.message);
630
930
  process.exit(1);
631
931
  }
632
- const composeSpinner = ora("Installing docker-compose.yml...").start();
932
+ const composeSpinner = ora2("Installing docker-compose.yml...").start();
633
933
  try {
634
- installComposeFile();
934
+ installComposeFile(runtime.name);
635
935
  composeSpinner.succeed("Compose file installed to ~/.horus/docker-compose.yml");
636
936
  } catch (error) {
637
937
  composeSpinner.fail("Failed to install compose file");
638
938
  console.error(error.message);
639
939
  process.exit(1);
640
940
  }
641
- const dataDir = config.data_dir.startsWith("~") ? join3(process.env.HOME || "", config.data_dir.slice(1)) : config.data_dir;
941
+ const dataDir = config.data_dir.startsWith("~") ? join4(process.env.HOME || "", config.data_dir.slice(1)) : config.data_dir;
642
942
  const reposToClone = [
643
- { url: config.repos.anvil_notes, dest: join3(dataDir, "notes"), label: "Anvil notes" },
644
- { url: config.repos.vault_knowledge, dest: join3(dataDir, "knowledge-base"), label: "Vault knowledge-base" },
645
- { url: config.repos.forge_registry, dest: join3(dataDir, "registry"), label: "Forge registry" }
943
+ { url: config.repos.anvil_notes, dest: join4(dataDir, "notes"), label: "Anvil notes" },
944
+ { url: config.repos.vault_knowledge, dest: join4(dataDir, "knowledge-base"), label: "Vault knowledge-base" },
945
+ { url: config.repos.forge_registry, dest: join4(dataDir, "registry"), label: "Forge registry" }
646
946
  ].filter((r) => r.url);
647
947
  if (reposToClone.length > 0) {
648
948
  console.log("");
649
- console.log(chalk.bold("Cloning repositories..."));
650
- mkdirSync2(dataDir, { recursive: true });
949
+ console.log(chalk2.bold("Cloning repositories..."));
950
+ mkdirSync3(dataDir, { recursive: true });
651
951
  for (const repo of reposToClone) {
652
- const spinner = ora(`Cloning ${repo.label}...`).start();
653
- if (existsSync3(join3(repo.dest, ".git"))) {
952
+ const spinner = ora2(`Cloning ${repo.label}...`).start();
953
+ if (existsSync4(join4(repo.dest, ".git"))) {
654
954
  spinner.succeed(`${repo.label} already cloned`);
655
955
  continue;
656
956
  }
657
957
  try {
658
- mkdirSync2(repo.dest, { recursive: true });
958
+ mkdirSync3(repo.dest, { recursive: true });
659
959
  const cloneUrl = injectToken(repo.url, config.github_token);
660
960
  execSync(`git clone "${cloneUrl}" "${repo.dest}"`, {
661
961
  stdio: "pipe",
@@ -666,37 +966,37 @@ ${example("forge-registry")}
666
966
  spinner.fail(`Failed to clone ${repo.label}`);
667
967
  const msg = error.message || "";
668
968
  if (msg.includes("already exists and is not an empty directory")) {
669
- console.log(chalk.dim(" Directory exists but has no .git \u2014 check the path."));
969
+ console.log(chalk2.dim(" Directory exists but has no .git \u2014 check the path."));
670
970
  } else {
671
- console.log(chalk.dim(` ${msg.split("\n")[0]}`));
971
+ console.log(chalk2.dim(` ${msg.split("\n")[0]}`));
672
972
  }
673
- console.log(chalk.dim(` URL: ${repo.url}`));
973
+ console.log(chalk2.dim(` URL: ${repo.url}`));
674
974
  if (!config.github_token) {
675
- console.log(chalk.dim(" Tip: Re-run setup and provide a GitHub token if the repo is private."));
975
+ console.log(chalk2.dim(" Tip: Re-run setup and provide a GitHub token if the repo is private."));
676
976
  }
677
977
  process.exit(1);
678
978
  }
679
979
  }
680
980
  }
681
981
  console.log("");
682
- console.log(chalk.bold("Pulling container images..."));
982
+ console.log(chalk2.bold("Pulling container images..."));
683
983
  try {
684
984
  await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
685
985
  } catch {
686
- console.log(chalk.yellow("Some images could not be pulled."));
687
- console.log(chalk.dim("Continuing \u2014 services will be built from source if build contexts are available."));
986
+ console.log(chalk2.yellow("Some images could not be pulled."));
987
+ console.log(chalk2.dim("Continuing \u2014 services will be built from source if build contexts are available."));
688
988
  }
689
989
  console.log("");
690
- console.log(chalk.bold("Starting Horus services..."));
990
+ console.log(chalk2.bold("Starting Horus services..."));
691
991
  try {
692
992
  await composeStreaming(runtime, ["up", "-d"]);
693
993
  } catch (error) {
694
- console.log(chalk.red("Failed to start services."));
695
- console.log(chalk.dim(error.message));
994
+ console.log(chalk2.red("Failed to start services."));
995
+ console.log(chalk2.dim(error.message));
696
996
  process.exit(1);
697
997
  }
698
998
  console.log("");
699
- const healthSpinner = ora("Waiting for services to become healthy...").start();
999
+ const healthSpinner = ora2("Waiting for services to become healthy...").start();
700
1000
  let lastStates = [];
701
1001
  try {
702
1002
  const states = await pollUntilHealthy(
@@ -704,7 +1004,7 @@ ${example("forge-registry")}
704
1004
  (current) => {
705
1005
  lastStates = current;
706
1006
  const summary = current.map((s) => {
707
- const icon = s.status === "healthy" ? chalk.green("*") : s.status === "starting" ? chalk.yellow("~") : chalk.red("x");
1007
+ const icon = s.status === "healthy" ? chalk2.green("*") : s.status === "starting" ? chalk2.yellow("~") : chalk2.red("x");
708
1008
  return `${icon} ${s.name}`;
709
1009
  }).join(" ");
710
1010
  healthSpinner.text = `Waiting for services... ${summary}`;
@@ -716,20 +1016,33 @@ ${example("forge-registry")}
716
1016
  lastStates = states;
717
1017
  } catch (error) {
718
1018
  healthSpinner.fail("Some services did not become healthy");
719
- console.log(chalk.dim(error.message));
1019
+ console.log(chalk2.dim(error.message));
720
1020
  console.log("");
721
- console.log(chalk.dim("Tip: Check logs with `docker compose logs` from ~/.horus/"));
1021
+ console.log(chalk2.dim("Tip: Check logs with `docker compose logs` from ~/.horus/"));
722
1022
  process.exit(1);
723
1023
  }
724
1024
  console.log("");
725
- console.log(chalk.bold.green("Setup complete!"));
726
- console.log(chalk.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"));
1025
+ const detectedClients = detectInstalledClients();
1026
+ if (detectedClients.length > 0) {
1027
+ console.log(chalk2.bold("Configuring AI clients..."));
1028
+ try {
1029
+ await runConnect(config, runtime, detectedClients, "localhost");
1030
+ } catch (error) {
1031
+ console.log(chalk2.yellow("Could not configure AI clients automatically."));
1032
+ console.log(chalk2.dim(`Run ${chalk2.cyan("horus connect")} to configure them manually.`));
1033
+ }
1034
+ } else {
1035
+ console.log(chalk2.dim(`No AI clients detected. Run ${chalk2.cyan("horus connect")} after installing Claude Desktop, Claude Code, or Cursor.`));
1036
+ }
727
1037
  console.log("");
728
- console.log(` ${chalk.bold("Runtime:")} ${runtime.name}`);
729
- console.log(` ${chalk.bold("Config:")} ~/.horus/config.yaml`);
730
- console.log(` ${chalk.bold("Data:")} ${config.data_dir}`);
1038
+ console.log(chalk2.bold.green("Setup complete!"));
1039
+ 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"));
731
1040
  console.log("");
732
- console.log(chalk.bold(" Service URLs:"));
1041
+ console.log(` ${chalk2.bold("Runtime:")} ${runtime.name}`);
1042
+ console.log(` ${chalk2.bold("Config:")} ~/.horus/config.yaml`);
1043
+ console.log(` ${chalk2.bold("Data:")} ${config.data_dir}`);
1044
+ console.log("");
1045
+ console.log(chalk2.bold(" Service URLs:"));
733
1046
  console.log(` Anvil: http://localhost:${config.ports.anvil}`);
734
1047
  console.log(` Vault REST: http://localhost:${config.ports.vault_rest}`);
735
1048
  console.log(` Vault MCP: http://localhost:${config.ports.vault_mcp}`);
@@ -738,50 +1051,59 @@ ${example("forge-registry")}
738
1051
  });
739
1052
 
740
1053
  // src/commands/up.ts
741
- import { Command as Command2 } from "commander";
742
- import chalk2 from "chalk";
743
- import ora2 from "ora";
744
- var upCommand = new Command2("up").description("Start the Horus stack").action(async () => {
1054
+ import { Command as Command3 } from "commander";
1055
+ import chalk3 from "chalk";
1056
+ import ora3 from "ora";
1057
+ var upCommand = new Command3("up").description("Start the Horus stack").option("--no-pull", "Skip pulling latest images before starting").action(async (opts) => {
745
1058
  if (!configExists() || !composeFileExists()) {
746
- console.log(chalk2.red("Horus is not set up yet."));
747
- console.log(chalk2.dim("Run `horus setup` first."));
1059
+ console.log(chalk3.red("Horus is not set up yet."));
1060
+ console.log(chalk3.dim("Run `horus setup` first."));
748
1061
  process.exit(1);
749
1062
  }
750
1063
  const config = loadConfig();
751
- const spinner = ora2("Detecting runtime...").start();
1064
+ const spinner = ora3("Detecting runtime...").start();
752
1065
  let runtime;
753
1066
  try {
754
1067
  runtime = await detectRuntime(config.runtime);
755
- spinner.succeed(`Using ${chalk2.cyan(runtime.name)}`);
1068
+ spinner.succeed(`Using ${chalk3.cyan(runtime.name)}`);
756
1069
  } catch (error) {
757
1070
  spinner.fail("No container runtime found");
758
1071
  console.log(error.message);
759
1072
  process.exit(1);
760
1073
  }
1074
+ if (opts.pull) {
1075
+ const pullSpinner = ora3("Pulling latest images...").start();
1076
+ try {
1077
+ await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
1078
+ pullSpinner.succeed("Images up to date");
1079
+ } catch {
1080
+ pullSpinner.warn("Could not pull images, using cached");
1081
+ }
1082
+ }
761
1083
  console.log("");
762
- console.log(chalk2.bold("Starting Horus services..."));
1084
+ console.log(chalk3.bold("Starting Horus services..."));
763
1085
  try {
764
1086
  await composeStreaming(runtime, ["up", "-d"]);
765
1087
  } catch (error) {
766
- console.log(chalk2.red("Failed to start services."));
767
- console.log(chalk2.dim(error.message));
1088
+ console.log(chalk3.red("Failed to start services."));
1089
+ console.log(chalk3.dim(error.message));
768
1090
  process.exit(1);
769
1091
  }
770
1092
  console.log("");
771
- const statusSpinner = ora2("Checking service status...").start();
1093
+ const statusSpinner = ora3("Checking service status...").start();
772
1094
  try {
773
1095
  const states = await checkAllHealth(runtime);
774
1096
  statusSpinner.stop();
775
- console.log(chalk2.bold("Service Status:"));
1097
+ console.log(chalk3.bold("Service Status:"));
776
1098
  for (const s of states) {
777
- const color = s.status === "healthy" ? chalk2.green : s.status === "starting" ? chalk2.yellow : chalk2.red;
1099
+ const color = s.status === "healthy" ? chalk3.green : s.status === "starting" ? chalk3.yellow : chalk3.red;
778
1100
  console.log(` ${color(s.status.padEnd(10))} ${s.name}`);
779
1101
  }
780
1102
  const allHealthy = states.every((s) => s.status === "healthy");
781
1103
  if (!allHealthy) {
782
1104
  console.log("");
783
1105
  console.log(
784
- chalk2.yellow("Some services are still starting. Run `horus status` to check progress.")
1106
+ chalk3.yellow("Some services are still starting. Run `horus status` to check progress.")
785
1107
  );
786
1108
  }
787
1109
  } catch {
@@ -791,53 +1113,53 @@ var upCommand = new Command2("up").description("Start the Horus stack").action(a
791
1113
  });
792
1114
 
793
1115
  // src/commands/down.ts
794
- import { Command as Command3 } from "commander";
795
- import chalk3 from "chalk";
796
- import ora3 from "ora";
797
- var downCommand = new Command3("down").description("Stop the Horus stack").action(async () => {
1116
+ import { Command as Command4 } from "commander";
1117
+ import chalk4 from "chalk";
1118
+ import ora4 from "ora";
1119
+ var downCommand = new Command4("down").description("Stop the Horus stack").action(async () => {
798
1120
  if (!configExists() || !composeFileExists()) {
799
- console.log(chalk3.red("Horus is not set up yet."));
800
- console.log(chalk3.dim("Run `horus setup` first."));
1121
+ console.log(chalk4.red("Horus is not set up yet."));
1122
+ console.log(chalk4.dim("Run `horus setup` first."));
801
1123
  process.exit(1);
802
1124
  }
803
1125
  const config = loadConfig();
804
- const spinner = ora3("Detecting runtime...").start();
1126
+ const spinner = ora4("Detecting runtime...").start();
805
1127
  let runtime;
806
1128
  try {
807
1129
  runtime = await detectRuntime(config.runtime);
808
- spinner.succeed(`Using ${chalk3.cyan(runtime.name)}`);
1130
+ spinner.succeed(`Using ${chalk4.cyan(runtime.name)}`);
809
1131
  } catch (error) {
810
1132
  spinner.fail("No container runtime found");
811
1133
  console.log(error.message);
812
1134
  process.exit(1);
813
1135
  }
814
1136
  console.log("");
815
- console.log(chalk3.bold("Stopping Horus services..."));
1137
+ console.log(chalk4.bold("Stopping Horus services..."));
816
1138
  try {
817
1139
  await composeStreaming(runtime, ["down"]);
818
1140
  } catch (error) {
819
- console.log(chalk3.red("Failed to stop services."));
820
- console.log(chalk3.dim(error.message));
1141
+ console.log(chalk4.red("Failed to stop services."));
1142
+ console.log(chalk4.dim(error.message));
821
1143
  process.exit(1);
822
1144
  }
823
1145
  console.log("");
824
- console.log(chalk3.green("All services stopped."));
825
- console.log(chalk3.dim("Data volumes have been preserved. Run `horus up` to restart."));
1146
+ console.log(chalk4.green("All services stopped."));
1147
+ console.log(chalk4.dim("Data volumes have been preserved. Run `horus up` to restart."));
826
1148
  console.log("");
827
1149
  });
828
1150
 
829
1151
  // src/commands/status.ts
830
- import { Command as Command4 } from "commander";
831
- import chalk4 from "chalk";
832
- import ora4 from "ora";
833
- var statusCommand = new Command4("status").description("Show status of Horus services").action(async () => {
1152
+ import { Command as Command5 } from "commander";
1153
+ import chalk5 from "chalk";
1154
+ import ora5 from "ora";
1155
+ var statusCommand = new Command5("status").description("Show status of Horus services").action(async () => {
834
1156
  if (!configExists() || !composeFileExists()) {
835
- console.log(chalk4.red("Horus is not set up yet."));
836
- console.log(chalk4.dim("Run `horus setup` first."));
1157
+ console.log(chalk5.red("Horus is not set up yet."));
1158
+ console.log(chalk5.dim("Run `horus setup` first."));
837
1159
  process.exit(1);
838
1160
  }
839
1161
  const config = loadConfig();
840
- const spinner = ora4("Checking services...").start();
1162
+ const spinner = ora5("Checking services...").start();
841
1163
  let runtime;
842
1164
  try {
843
1165
  runtime = await detectRuntime(config.runtime);
@@ -858,28 +1180,28 @@ var statusCommand = new Command4("status").description("Show status of Horus ser
858
1180
  }
859
1181
  spinner.stop();
860
1182
  console.log("");
861
- console.log(chalk4.bold("Horus Status"));
862
- console.log(chalk4.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"));
863
- console.log(` ${chalk4.bold("Version:")} ${config.version}`);
864
- console.log(` ${chalk4.bold("Runtime:")} ${runtime.name}`);
865
- console.log(` ${chalk4.bold("Config:")} ~/.horus/config.yaml`);
1183
+ console.log(chalk5.bold("Horus Status"));
1184
+ console.log(chalk5.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"));
1185
+ console.log(` ${chalk5.bold("Version:")} ${config.version}`);
1186
+ console.log(` ${chalk5.bold("Runtime:")} ${runtime.name}`);
1187
+ console.log(` ${chalk5.bold("Config:")} ~/.horus/config.yaml`);
866
1188
  console.log("");
867
1189
  if (containers.length === 0) {
868
- console.log(chalk4.yellow(" No services are running."));
869
- console.log(chalk4.dim(" Run `horus up` to start the stack."));
1190
+ console.log(chalk5.yellow(" No services are running."));
1191
+ console.log(chalk5.dim(" Run `horus up` to start the stack."));
870
1192
  console.log("");
871
1193
  return;
872
1194
  }
873
1195
  const header = ` ${pad("SERVICE", 14)} ${pad("STATUS", 12)} ${pad("PORTS", 20)} ${pad("UPTIME", 20)}`;
874
- console.log(chalk4.bold(header));
875
- console.log(chalk4.dim(" " + "\u2500".repeat(66)));
1196
+ console.log(chalk5.bold(header));
1197
+ console.log(chalk5.dim(" " + "\u2500".repeat(66)));
876
1198
  for (const service of SERVICES) {
877
1199
  const container = containers.find(
878
1200
  (c) => c.Service === service || c.Name?.includes(service)
879
1201
  );
880
1202
  if (!container) {
881
1203
  console.log(
882
- ` ${pad(service, 14)} ${chalk4.red(pad("stopped", 12))} ${pad("-", 20)} ${pad("-", 20)}`
1204
+ ` ${pad(service, 14)} ${chalk5.red(pad("stopped", 12))} ${pad("-", 20)} ${pad("-", 20)}`
883
1205
  );
884
1206
  continue;
885
1207
  }
@@ -897,9 +1219,9 @@ function pad(str, width) {
897
1219
  }
898
1220
  function getStatusColor(status) {
899
1221
  const lower = status.toLowerCase();
900
- if (lower === "healthy" || lower === "running") return chalk4.green;
901
- if (lower === "starting") return chalk4.yellow;
902
- return chalk4.red;
1222
+ if (lower === "healthy" || lower === "running") return chalk5.green;
1223
+ if (lower === "starting") return chalk5.yellow;
1224
+ return chalk5.red;
903
1225
  }
904
1226
  function formatPorts(publishers) {
905
1227
  if (!publishers || publishers.length === 0) return "-";
@@ -914,50 +1236,52 @@ function extractUptime(status) {
914
1236
  }
915
1237
 
916
1238
  // src/commands/config.ts
917
- import { Command as Command5 } from "commander";
918
- import chalk5 from "chalk";
1239
+ import { Command as Command6 } from "commander";
1240
+ import chalk6 from "chalk";
919
1241
  import { confirm as confirm2 } from "@inquirer/prompts";
920
- var configCommand = new Command5("config").description("View or modify Horus configuration").action(async () => {
1242
+ var configCommand = new Command6("config").description("View or modify Horus configuration").action(async () => {
921
1243
  if (!configExists()) {
922
- console.log(chalk5.red("Horus is not configured yet."));
923
- console.log(chalk5.dim("Run `horus setup` first."));
1244
+ console.log(chalk6.red("Horus is not configured yet."));
1245
+ console.log(chalk6.dim("Run `horus setup` first."));
924
1246
  process.exit(1);
925
1247
  }
926
1248
  const config = loadConfig();
927
1249
  console.log("");
928
- console.log(chalk5.bold("Horus Configuration"));
929
- console.log(chalk5.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"));
930
- console.log(` ${chalk5.bold("version:")} ${config.version}`);
931
- console.log(` ${chalk5.bold("data-dir:")} ${config.data_dir}`);
932
- console.log(` ${chalk5.bold("runtime:")} ${config.runtime}`);
933
- console.log(` ${chalk5.bold("host-repos-path:")} ${config.host_repos_path || chalk5.dim("(not set)")}`);
934
- console.log(` ${chalk5.bold("git-host:")} ${config.git_host || chalk5.dim("(not set)")}`);
935
- console.log(` ${chalk5.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk5.dim("(not set)")}`);
1250
+ console.log(chalk6.bold("Horus Configuration"));
1251
+ console.log(chalk6.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"));
1252
+ console.log(` ${chalk6.bold("version:")} ${config.version}`);
1253
+ console.log(` ${chalk6.bold("data-dir:")} ${config.data_dir}`);
1254
+ console.log(` ${chalk6.bold("runtime:")} ${config.runtime}`);
1255
+ console.log(` ${chalk6.bold("host-repos-path:")} ${config.host_repos_path || chalk6.dim("(not set)")}`);
1256
+ const extraDirs = (config.host_repos_extra_scan_dirs ?? []).join(", ");
1257
+ console.log(` ${chalk6.bold("host-repos-extra-scan-dirs:")} ${extraDirs || chalk6.dim("(not set)")}`);
1258
+ console.log(` ${chalk6.bold("git-host:")} ${config.git_host || chalk6.dim("(not set)")}`);
1259
+ console.log(` ${chalk6.bold("github-token:")} ${config.github_token ? maskApiKey(config.github_token) : chalk6.dim("(not set)")}`);
936
1260
  console.log("");
937
- console.log(chalk5.bold(" Ports:"));
938
- console.log(` ${chalk5.bold("anvil:")} ${config.ports.anvil}`);
939
- console.log(` ${chalk5.bold("vault-rest:")} ${config.ports.vault_rest}`);
940
- console.log(` ${chalk5.bold("vault-mcp:")} ${config.ports.vault_mcp}`);
941
- console.log(` ${chalk5.bold("forge:")} ${config.ports.forge}`);
1261
+ console.log(chalk6.bold(" Ports:"));
1262
+ console.log(` ${chalk6.bold("anvil:")} ${config.ports.anvil}`);
1263
+ console.log(` ${chalk6.bold("vault-rest:")} ${config.ports.vault_rest}`);
1264
+ console.log(` ${chalk6.bold("vault-mcp:")} ${config.ports.vault_mcp}`);
1265
+ console.log(` ${chalk6.bold("forge:")} ${config.ports.forge}`);
942
1266
  console.log("");
943
- console.log(chalk5.bold(" Repos:"));
944
- console.log(` ${chalk5.bold("anvil-notes:")} ${config.repos.anvil_notes || chalk5.dim("(not set)")}`);
945
- console.log(` ${chalk5.bold("vault-knowledge:")} ${config.repos.vault_knowledge || chalk5.dim("(not set)")}`);
946
- console.log(` ${chalk5.bold("forge-registry:")} ${config.repos.forge_registry || chalk5.dim("(not set)")}`);
1267
+ console.log(chalk6.bold(" Repos:"));
1268
+ console.log(` ${chalk6.bold("anvil-notes:")} ${config.repos.anvil_notes || chalk6.dim("(not set)")}`);
1269
+ console.log(` ${chalk6.bold("vault-knowledge:")} ${config.repos.vault_knowledge || chalk6.dim("(not set)")}`);
1270
+ console.log(` ${chalk6.bold("forge-registry:")} ${config.repos.forge_registry || chalk6.dim("(not set)")}`);
947
1271
  console.log("");
948
- console.log(chalk5.dim(` Config file: ~/.horus/config.yaml`));
949
- console.log(chalk5.dim(` Use 'horus config get <key>' or 'horus config set <key> <value>'`));
1272
+ console.log(chalk6.dim(` Config file: ~/.horus/config.yaml`));
1273
+ console.log(chalk6.dim(` Use 'horus config get <key>' or 'horus config set <key> <value>'`));
950
1274
  console.log("");
951
1275
  });
952
1276
  configCommand.command("get <key>").description("Get a configuration value").action(async (key) => {
953
1277
  if (!configExists()) {
954
- console.log(chalk5.red("Horus is not configured yet."));
955
- console.log(chalk5.dim("Run `horus setup` first."));
1278
+ console.log(chalk6.red("Horus is not configured yet."));
1279
+ console.log(chalk6.dim("Run `horus setup` first."));
956
1280
  process.exit(1);
957
1281
  }
958
1282
  if (!isValidKey(key)) {
959
- console.log(chalk5.red(`Unknown config key: ${key}`));
960
- console.log(chalk5.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
1283
+ console.log(chalk6.red(`Unknown config key: ${key}`));
1284
+ console.log(chalk6.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
961
1285
  process.exit(1);
962
1286
  }
963
1287
  const config = loadConfig();
@@ -970,28 +1294,29 @@ configCommand.command("get <key>").description("Get a configuration value").acti
970
1294
  });
971
1295
  configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
972
1296
  if (!configExists()) {
973
- console.log(chalk5.red("Horus is not configured yet."));
974
- console.log(chalk5.dim("Run `horus setup` first."));
1297
+ console.log(chalk6.red("Horus is not configured yet."));
1298
+ console.log(chalk6.dim("Run `horus setup` first."));
975
1299
  process.exit(1);
976
1300
  }
977
1301
  if (!isValidKey(key)) {
978
- console.log(chalk5.red(`Unknown config key: ${key}`));
979
- console.log(chalk5.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
1302
+ console.log(chalk6.red(`Unknown config key: ${key}`));
1303
+ console.log(chalk6.dim(`Valid keys: ${CONFIG_KEYS.join(", ")}`));
980
1304
  process.exit(1);
981
1305
  }
982
1306
  let config = loadConfig();
983
1307
  try {
984
1308
  config = setConfigValue(config, key, value);
985
1309
  } catch (error) {
986
- console.log(chalk5.red(error.message));
1310
+ console.log(chalk6.red(error.message));
987
1311
  process.exit(1);
988
1312
  }
989
1313
  saveConfig(config);
990
1314
  writeEnvFile(config);
991
- console.log(chalk5.green(`Set ${key} and regenerated .env file.`));
1315
+ console.log(chalk6.green(`Set ${key} and regenerated .env file.`));
992
1316
  const needsRestart = [
993
1317
  "data-dir",
994
1318
  "host-repos-path",
1319
+ "host-repos-extra-scan-dirs",
995
1320
  "runtime",
996
1321
  "port.anvil",
997
1322
  "port.vault-rest",
@@ -999,17 +1324,17 @@ configCommand.command("set <key> <value>").description("Set a configuration valu
999
1324
  "port.forge"
1000
1325
  ];
1001
1326
  if (needsRestart.includes(key)) {
1002
- console.log(chalk5.yellow("Restart required for changes to take effect."));
1327
+ console.log(chalk6.yellow("Restart required for changes to take effect."));
1003
1328
  if (process.stdin.isTTY) {
1004
1329
  const restart = await confirm2({
1005
1330
  message: "Restart Horus now?",
1006
1331
  default: false
1007
1332
  });
1008
1333
  if (restart) {
1009
- console.log(chalk5.dim("Run `horus down && horus up` to restart."));
1334
+ console.log(chalk6.dim("Run `horus down && horus up` to restart."));
1010
1335
  }
1011
1336
  } else {
1012
- console.log(chalk5.dim("Run `horus down && horus up` to restart."));
1337
+ console.log(chalk6.dim("Run `horus down && horus up` to restart."));
1013
1338
  }
1014
1339
  }
1015
1340
  });
@@ -1017,212 +1342,6 @@ function isValidKey(key) {
1017
1342
  return CONFIG_KEYS.includes(key);
1018
1343
  }
1019
1344
 
1020
- // src/commands/connect.ts
1021
- import { Command as Command6 } from "commander";
1022
- import chalk6 from "chalk";
1023
- import ora5 from "ora";
1024
- import { checkbox } from "@inquirer/prompts";
1025
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
1026
- import { join as join4 } from "path";
1027
- import { homedir as homedir3 } from "os";
1028
- function detectInstalledClients() {
1029
- const detected = [];
1030
- const home = homedir3();
1031
- const claudeDesktopDir = join4(home, "Library", "Application Support", "Claude");
1032
- if (existsSync4(claudeDesktopDir)) {
1033
- detected.push("claude-desktop");
1034
- }
1035
- const claudeCodeDir = join4(home, ".claude");
1036
- if (existsSync4(claudeCodeDir)) {
1037
- detected.push("claude-code");
1038
- }
1039
- const cursorDir = join4(home, ".cursor");
1040
- const cursorAppDir = join4(home, "Library", "Application Support", "Cursor");
1041
- if (existsSync4(cursorDir) || existsSync4(cursorAppDir)) {
1042
- detected.push("cursor");
1043
- }
1044
- return detected;
1045
- }
1046
- function getConfigPath(target) {
1047
- const home = homedir3();
1048
- switch (target) {
1049
- case "claude-desktop":
1050
- return join4(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1051
- case "claude-code":
1052
- return join4(home, ".claude", "settings.json");
1053
- case "cursor":
1054
- return join4(home, ".cursor", "mcp.json");
1055
- }
1056
- }
1057
- function mergeAndWriteConfig(configPath, mcpServers) {
1058
- let existing = {};
1059
- if (existsSync4(configPath)) {
1060
- try {
1061
- const raw = readFileSync3(configPath, "utf-8");
1062
- existing = JSON.parse(raw);
1063
- } catch {
1064
- existing = {};
1065
- }
1066
- }
1067
- const existingServers = existing.mcpServers ?? {};
1068
- existing.mcpServers = { ...existingServers, ...mcpServers };
1069
- const dir = configPath.substring(0, configPath.lastIndexOf("/"));
1070
- mkdirSync3(dir, { recursive: true });
1071
- writeFileSync3(configPath, JSON.stringify(existing, null, 2) + "\n", "utf-8");
1072
- }
1073
- async function syncSkills(runtime) {
1074
- const home = homedir3();
1075
- const skillsBase = join4(home, ".claude", "skills");
1076
- const skills = ["horus-anvil", "horus-vault", "horus-forge"];
1077
- const forgeContainer = "horus-forge-1";
1078
- for (const skill of skills) {
1079
- const destDir = join4(skillsBase, skill);
1080
- mkdirSync3(destDir, { recursive: true });
1081
- const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
1082
- const dest = join4(destDir, "SKILL.md");
1083
- const result = await runtime.exec(forgeContainer, "cat", src);
1084
- if (result.exitCode === 0 && result.stdout.trim()) {
1085
- writeFileSync3(dest, result.stdout, "utf-8");
1086
- }
1087
- }
1088
- }
1089
- async function syncSkillsForCursor(runtime) {
1090
- const home = homedir3();
1091
- const rulesDir = join4(home, ".cursor", "rules");
1092
- const skills = ["horus-anvil", "horus-vault", "horus-forge"];
1093
- const forgeContainer = "horus-forge-1";
1094
- mkdirSync3(rulesDir, { recursive: true });
1095
- for (const skill of skills) {
1096
- const src = `/home/forge/.claude/skills/${skill}/SKILL.md`;
1097
- const dest = join4(rulesDir, `${skill}.mdc`);
1098
- const result = await runtime.exec(forgeContainer, "cat", src);
1099
- if (result.exitCode === 0 && result.stdout.trim()) {
1100
- const frontmatter = `---
1101
- description: Horus ${skill} reference
1102
- alwaysApply: true
1103
- ---
1104
-
1105
- `;
1106
- writeFileSync3(dest, frontmatter + result.stdout, "utf-8");
1107
- }
1108
- }
1109
- }
1110
- function printNextSteps(targets) {
1111
- console.log("");
1112
- console.log(chalk6.bold("Next steps:"));
1113
- for (const target of targets) {
1114
- switch (target) {
1115
- case "claude-desktop":
1116
- console.log(` ${chalk6.cyan("Claude Desktop")} Restart Claude Desktop to pick up the new MCP configuration`);
1117
- break;
1118
- case "claude-code":
1119
- console.log(` ${chalk6.cyan("Claude Code")} Start a new Claude Code session`);
1120
- break;
1121
- case "cursor":
1122
- console.log(` ${chalk6.cyan("Cursor")} Restart Cursor to pick up the new MCP configuration and rules`);
1123
- break;
1124
- }
1125
- }
1126
- console.log("");
1127
- }
1128
- var connectCommand = new Command6("connect").description("Configure Claude/Cursor MCP integration").option("--target <client>", "Client to configure: claude-desktop, claude-code, cursor, all (default: auto-detect)").option("--host <host>", "MCP host (default: localhost)", "localhost").option("-y, --yes", "Skip confirmation prompts").action(async (opts) => {
1129
- console.log("");
1130
- console.log(chalk6.bold("Horus Connect"));
1131
- console.log(chalk6.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"));
1132
- console.log("");
1133
- const config = loadConfig();
1134
- const runtimeSpinner = ora5("Detecting runtime...").start();
1135
- let runtime;
1136
- try {
1137
- runtime = await detectRuntime(config.runtime);
1138
- runtimeSpinner.succeed(`Using ${chalk6.cyan(runtime.name)}`);
1139
- } catch (error) {
1140
- runtimeSpinner.fail("No container runtime found");
1141
- console.log(error.message);
1142
- process.exit(1);
1143
- }
1144
- const runningSpinner = ora5("Checking Horus status...").start();
1145
- const running = await runtime.isRunning();
1146
- if (!running) {
1147
- runningSpinner.fail("Horus is not running");
1148
- console.log(chalk6.dim("Run `horus up` first, then re-run `horus connect`."));
1149
- process.exit(1);
1150
- }
1151
- runningSpinner.succeed("Horus is running");
1152
- let targets = [];
1153
- if (opts.target === "all") {
1154
- targets = ["claude-desktop", "claude-code", "cursor"];
1155
- } else if (opts.target) {
1156
- const valid = ["claude-desktop", "claude-code", "cursor"];
1157
- if (!valid.includes(opts.target)) {
1158
- console.log(chalk6.red(`Invalid target: ${opts.target}`));
1159
- console.log(chalk6.dim("Valid targets: claude-desktop, claude-code, cursor, all"));
1160
- process.exit(1);
1161
- }
1162
- targets = [opts.target];
1163
- } else {
1164
- const detected = detectInstalledClients();
1165
- if (detected.length === 0) {
1166
- console.log(chalk6.yellow("No supported clients detected (Claude Desktop, Claude Code, or Cursor)."));
1167
- console.log(chalk6.dim("Use --target to specify a client manually."));
1168
- process.exit(1);
1169
- }
1170
- if (opts.yes) {
1171
- targets = detected;
1172
- console.log(`Detected clients: ${detected.map((t) => chalk6.cyan(t)).join(", ")}`);
1173
- } else {
1174
- const chosen = await checkbox({
1175
- message: "Select clients to configure:",
1176
- choices: detected.map((t) => ({ name: t, value: t, checked: true })),
1177
- validate: (input2) => input2.length > 0 ? true : "Select at least one client."
1178
- });
1179
- targets = chosen;
1180
- }
1181
- }
1182
- if (targets.length === 0) {
1183
- console.log(chalk6.yellow("No clients selected. Exiting."));
1184
- return;
1185
- }
1186
- const host = opts.host;
1187
- const mcpServers = {
1188
- anvil: { url: `http://${host}:${config.ports.anvil}/sse` },
1189
- vault: { url: `http://${host}:${config.ports.vault_mcp}/sse` },
1190
- forge: { url: `http://${host}:${config.ports.forge}/sse` }
1191
- };
1192
- for (const target of targets) {
1193
- const configPath = getConfigPath(target);
1194
- const writeSpinner = ora5(`Configuring ${chalk6.cyan(target)}...`).start();
1195
- try {
1196
- mergeAndWriteConfig(configPath, mcpServers);
1197
- writeSpinner.succeed(`Configured ${chalk6.cyan(target)} \u2014 ${chalk6.dim(configPath)}`);
1198
- } catch (error) {
1199
- writeSpinner.fail(`Failed to configure ${target}`);
1200
- console.log(chalk6.dim(error.message));
1201
- }
1202
- }
1203
- if (targets.includes("claude-code")) {
1204
- const skillsSpinner = ora5("Syncing horus-core skills...").start();
1205
- try {
1206
- await syncSkills(runtime);
1207
- skillsSpinner.succeed("horus-core skills synced to ~/.claude/skills/");
1208
- } catch (error) {
1209
- skillsSpinner.warn("Could not sync skills (Forge container may not be running)");
1210
- console.log(chalk6.dim(error.message));
1211
- }
1212
- }
1213
- if (targets.includes("cursor")) {
1214
- const cursorRulesSpinner = ora5("Syncing horus-core rules for Cursor...").start();
1215
- try {
1216
- await syncSkillsForCursor(runtime);
1217
- cursorRulesSpinner.succeed("horus-core rules synced to ~/.cursor/rules/");
1218
- } catch (error) {
1219
- cursorRulesSpinner.warn("Could not sync Cursor rules (Forge container may not be running)");
1220
- console.log(chalk6.dim(error.message));
1221
- }
1222
- }
1223
- printNextSteps(targets);
1224
- });
1225
-
1226
1345
  // src/commands/update.ts
1227
1346
  import { Command as Command7 } from "commander";
1228
1347
  import chalk7 from "chalk";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkhera30/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "CLI for managing the Horus AI development stack",
5
5
  "type": "module",
6
6
  "bin": {