@arkhera30/cli 0.1.9 → 0.1.11

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