@arkhera30/cli 0.1.9 → 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.
Files changed (2) hide show
  1. package/dist/index.js +424 -402
  2. package/package.json +1 -1
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
@@ -455,6 +455,275 @@ function installComposeFile(runtime) {
455
455
  writeFileSync2(COMPOSE_PATH, content, "utf-8");
456
456
  }
457
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
+
458
727
  // src/commands/setup.ts
459
728
  function injectToken(url, token) {
460
729
  if (!token) return url;
@@ -467,26 +736,26 @@ function injectToken(url, token) {
467
736
  return url;
468
737
  }
469
738
  }
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) => {
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) => {
471
740
  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"));
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"));
474
743
  console.log("");
475
744
  if (configExists()) {
476
745
  if (opts.yes) {
477
- 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."));
478
747
  } else {
479
748
  const proceed = await confirm({
480
749
  message: "Horus is already configured. Reconfigure?",
481
750
  default: false
482
751
  });
483
752
  if (!proceed) {
484
- console.log(chalk.dim("Setup cancelled."));
753
+ console.log(chalk2.dim("Setup cancelled."));
485
754
  return;
486
755
  }
487
756
  }
488
757
  }
489
- const checkSpinner = ora("Checking for container runtimes...").start();
758
+ const checkSpinner = ora2("Checking for container runtimes...").start();
490
759
  const [hasDocker, hasPodman] = await Promise.all([
491
760
  checkRuntime("docker"),
492
761
  checkRuntime("podman")
@@ -497,7 +766,7 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
497
766
  ...hasPodman ? ["podman"] : []
498
767
  ];
499
768
  if (available.length === 0) {
500
- console.log(chalk.red("No container runtime found."));
769
+ console.log(chalk2.red("No container runtime found."));
501
770
  console.log("");
502
771
  console.log("Horus requires Docker or Podman with the Compose plugin.");
503
772
  console.log("");
@@ -510,12 +779,12 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
510
779
  if (opts.yes) {
511
780
  const requested = opts.runtime;
512
781
  if (requested && !available.includes(requested)) {
513
- console.log(chalk.red(`Requested runtime "${requested}" is not installed.`));
514
- 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(", ")}`));
515
784
  process.exit(1);
516
785
  }
517
786
  selectedRuntime = requested ?? available[0];
518
- console.log(`Using ${chalk.cyan(selectedRuntime)}`);
787
+ console.log(`Using ${chalk2.cyan(selectedRuntime)}`);
519
788
  } else {
520
789
  selectedRuntime = await select({
521
790
  message: "Which container runtime would you like to use?",
@@ -586,19 +855,19 @@ var setupCommand = new Command("setup").description("Interactive first-run setup
586
855
  };
587
856
  }
588
857
  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."));
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."));
592
861
  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."));
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."));
595
864
  console.log("");
596
865
  const git_host = await input({
597
866
  message: "Git server hostname:",
598
867
  default: "github.com"
599
868
  });
600
869
  const host = git_host.trim();
601
- const example = (repo) => chalk.dim(` e.g., https://${host}/<owner>/${repo}`);
870
+ const example = (repo) => chalk2.dim(` e.g., https://${host}/<owner>/${repo}`);
602
871
  console.log("");
603
872
  const anvil_notes = await input({
604
873
  message: `Anvil notes repo URL:
@@ -619,8 +888,8 @@ ${example("forge-registry")}
619
888
  validate: (v) => v.trim().length > 0 || "Forge needs a registry repo."
620
889
  });
621
890
  console.log("");
622
- console.log(chalk.bold("Authentication"));
623
- 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."));
624
893
  console.log("");
625
894
  const github_token = await password({
626
895
  message: "GitHub personal access token (leave empty to skip):",
@@ -642,7 +911,7 @@ ${example("forge-registry")}
642
911
  github_token: github_token.trim()
643
912
  };
644
913
  }
645
- const configSpinner = ora("Saving configuration...").start();
914
+ const configSpinner = ora2("Saving configuration...").start();
646
915
  try {
647
916
  saveConfig(config);
648
917
  configSpinner.succeed("Configuration saved to ~/.horus/config.yaml");
@@ -651,7 +920,7 @@ ${example("forge-registry")}
651
920
  console.error(error.message);
652
921
  process.exit(1);
653
922
  }
654
- const envSpinner = ora("Generating .env file...").start();
923
+ const envSpinner = ora2("Generating .env file...").start();
655
924
  try {
656
925
  writeEnvFile(config);
657
926
  envSpinner.succeed("Environment file written to ~/.horus/.env");
@@ -660,7 +929,7 @@ ${example("forge-registry")}
660
929
  console.error(error.message);
661
930
  process.exit(1);
662
931
  }
663
- const composeSpinner = ora("Installing docker-compose.yml...").start();
932
+ const composeSpinner = ora2("Installing docker-compose.yml...").start();
664
933
  try {
665
934
  installComposeFile(runtime.name);
666
935
  composeSpinner.succeed("Compose file installed to ~/.horus/docker-compose.yml");
@@ -669,24 +938,24 @@ ${example("forge-registry")}
669
938
  console.error(error.message);
670
939
  process.exit(1);
671
940
  }
672
- 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;
673
942
  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" }
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" }
677
946
  ].filter((r) => r.url);
678
947
  if (reposToClone.length > 0) {
679
948
  console.log("");
680
- console.log(chalk.bold("Cloning repositories..."));
681
- mkdirSync2(dataDir, { recursive: true });
949
+ console.log(chalk2.bold("Cloning repositories..."));
950
+ mkdirSync3(dataDir, { recursive: true });
682
951
  for (const repo of reposToClone) {
683
- const spinner = ora(`Cloning ${repo.label}...`).start();
684
- if (existsSync3(join3(repo.dest, ".git"))) {
952
+ const spinner = ora2(`Cloning ${repo.label}...`).start();
953
+ if (existsSync4(join4(repo.dest, ".git"))) {
685
954
  spinner.succeed(`${repo.label} already cloned`);
686
955
  continue;
687
956
  }
688
957
  try {
689
- mkdirSync2(repo.dest, { recursive: true });
958
+ mkdirSync3(repo.dest, { recursive: true });
690
959
  const cloneUrl = injectToken(repo.url, config.github_token);
691
960
  execSync(`git clone "${cloneUrl}" "${repo.dest}"`, {
692
961
  stdio: "pipe",
@@ -697,37 +966,37 @@ ${example("forge-registry")}
697
966
  spinner.fail(`Failed to clone ${repo.label}`);
698
967
  const msg = error.message || "";
699
968
  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."));
969
+ console.log(chalk2.dim(" Directory exists but has no .git \u2014 check the path."));
701
970
  } else {
702
- console.log(chalk.dim(` ${msg.split("\n")[0]}`));
971
+ console.log(chalk2.dim(` ${msg.split("\n")[0]}`));
703
972
  }
704
- console.log(chalk.dim(` URL: ${repo.url}`));
973
+ console.log(chalk2.dim(` URL: ${repo.url}`));
705
974
  if (!config.github_token) {
706
- 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."));
707
976
  }
708
977
  process.exit(1);
709
978
  }
710
979
  }
711
980
  }
712
981
  console.log("");
713
- console.log(chalk.bold("Pulling container images..."));
982
+ console.log(chalk2.bold("Pulling container images..."));
714
983
  try {
715
984
  await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
716
985
  } 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."));
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."));
719
988
  }
720
989
  console.log("");
721
- console.log(chalk.bold("Starting Horus services..."));
990
+ console.log(chalk2.bold("Starting Horus services..."));
722
991
  try {
723
992
  await composeStreaming(runtime, ["up", "-d"]);
724
993
  } catch (error) {
725
- console.log(chalk.red("Failed to start services."));
726
- console.log(chalk.dim(error.message));
994
+ console.log(chalk2.red("Failed to start services."));
995
+ console.log(chalk2.dim(error.message));
727
996
  process.exit(1);
728
997
  }
729
998
  console.log("");
730
- const healthSpinner = ora("Waiting for services to become healthy...").start();
999
+ const healthSpinner = ora2("Waiting for services to become healthy...").start();
731
1000
  let lastStates = [];
732
1001
  try {
733
1002
  const states = await pollUntilHealthy(
@@ -735,7 +1004,7 @@ ${example("forge-registry")}
735
1004
  (current) => {
736
1005
  lastStates = current;
737
1006
  const summary = current.map((s) => {
738
- 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");
739
1008
  return `${icon} ${s.name}`;
740
1009
  }).join(" ");
741
1010
  healthSpinner.text = `Waiting for services... ${summary}`;
@@ -747,20 +1016,33 @@ ${example("forge-registry")}
747
1016
  lastStates = states;
748
1017
  } catch (error) {
749
1018
  healthSpinner.fail("Some services did not become healthy");
750
- console.log(chalk.dim(error.message));
1019
+ console.log(chalk2.dim(error.message));
751
1020
  console.log("");
752
- 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/"));
753
1022
  process.exit(1);
754
1023
  }
755
1024
  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"));
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
+ }
758
1037
  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}`);
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"));
762
1040
  console.log("");
763
- 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:"));
764
1046
  console.log(` Anvil: http://localhost:${config.ports.anvil}`);
765
1047
  console.log(` Vault REST: http://localhost:${config.ports.vault_rest}`);
766
1048
  console.log(` Vault MCP: http://localhost:${config.ports.vault_mcp}`);
@@ -769,28 +1051,28 @@ ${example("forge-registry")}
769
1051
  });
770
1052
 
771
1053
  // 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) => {
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) => {
776
1058
  if (!configExists() || !composeFileExists()) {
777
- console.log(chalk2.red("Horus is not set up yet."));
778
- 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."));
779
1061
  process.exit(1);
780
1062
  }
781
1063
  const config = loadConfig();
782
- const spinner = ora2("Detecting runtime...").start();
1064
+ const spinner = ora3("Detecting runtime...").start();
783
1065
  let runtime;
784
1066
  try {
785
1067
  runtime = await detectRuntime(config.runtime);
786
- spinner.succeed(`Using ${chalk2.cyan(runtime.name)}`);
1068
+ spinner.succeed(`Using ${chalk3.cyan(runtime.name)}`);
787
1069
  } catch (error) {
788
1070
  spinner.fail("No container runtime found");
789
1071
  console.log(error.message);
790
1072
  process.exit(1);
791
1073
  }
792
1074
  if (opts.pull) {
793
- const pullSpinner = ora2("Pulling latest images...").start();
1075
+ const pullSpinner = ora3("Pulling latest images...").start();
794
1076
  try {
795
1077
  await composeStreaming(runtime, ["pull", "--ignore-pull-failures"]);
796
1078
  pullSpinner.succeed("Images up to date");
@@ -799,29 +1081,29 @@ var upCommand = new Command2("up").description("Start the Horus stack").option("
799
1081
  }
800
1082
  }
801
1083
  console.log("");
802
- console.log(chalk2.bold("Starting Horus services..."));
1084
+ console.log(chalk3.bold("Starting Horus services..."));
803
1085
  try {
804
1086
  await composeStreaming(runtime, ["up", "-d"]);
805
1087
  } catch (error) {
806
- console.log(chalk2.red("Failed to start services."));
807
- console.log(chalk2.dim(error.message));
1088
+ console.log(chalk3.red("Failed to start services."));
1089
+ console.log(chalk3.dim(error.message));
808
1090
  process.exit(1);
809
1091
  }
810
1092
  console.log("");
811
- const statusSpinner = ora2("Checking service status...").start();
1093
+ const statusSpinner = ora3("Checking service status...").start();
812
1094
  try {
813
1095
  const states = await checkAllHealth(runtime);
814
1096
  statusSpinner.stop();
815
- console.log(chalk2.bold("Service Status:"));
1097
+ console.log(chalk3.bold("Service Status:"));
816
1098
  for (const s of states) {
817
- 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;
818
1100
  console.log(` ${color(s.status.padEnd(10))} ${s.name}`);
819
1101
  }
820
1102
  const allHealthy = states.every((s) => s.status === "healthy");
821
1103
  if (!allHealthy) {
822
1104
  console.log("");
823
1105
  console.log(
824
- 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.")
825
1107
  );
826
1108
  }
827
1109
  } catch {
@@ -831,53 +1113,53 @@ var upCommand = new Command2("up").description("Start the Horus stack").option("
831
1113
  });
832
1114
 
833
1115
  // 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 () => {
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 () => {
838
1120
  if (!configExists() || !composeFileExists()) {
839
- console.log(chalk3.red("Horus is not set up yet."));
840
- 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."));
841
1123
  process.exit(1);
842
1124
  }
843
1125
  const config = loadConfig();
844
- const spinner = ora3("Detecting runtime...").start();
1126
+ const spinner = ora4("Detecting runtime...").start();
845
1127
  let runtime;
846
1128
  try {
847
1129
  runtime = await detectRuntime(config.runtime);
848
- spinner.succeed(`Using ${chalk3.cyan(runtime.name)}`);
1130
+ spinner.succeed(`Using ${chalk4.cyan(runtime.name)}`);
849
1131
  } catch (error) {
850
1132
  spinner.fail("No container runtime found");
851
1133
  console.log(error.message);
852
1134
  process.exit(1);
853
1135
  }
854
1136
  console.log("");
855
- console.log(chalk3.bold("Stopping Horus services..."));
1137
+ console.log(chalk4.bold("Stopping Horus services..."));
856
1138
  try {
857
1139
  await composeStreaming(runtime, ["down"]);
858
1140
  } catch (error) {
859
- console.log(chalk3.red("Failed to stop services."));
860
- console.log(chalk3.dim(error.message));
1141
+ console.log(chalk4.red("Failed to stop services."));
1142
+ console.log(chalk4.dim(error.message));
861
1143
  process.exit(1);
862
1144
  }
863
1145
  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."));
1146
+ console.log(chalk4.green("All services stopped."));
1147
+ console.log(chalk4.dim("Data volumes have been preserved. Run `horus up` to restart."));
866
1148
  console.log("");
867
1149
  });
868
1150
 
869
1151
  // 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 () => {
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 () => {
874
1156
  if (!configExists() || !composeFileExists()) {
875
- console.log(chalk4.red("Horus is not set up yet."));
876
- 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."));
877
1159
  process.exit(1);
878
1160
  }
879
1161
  const config = loadConfig();
880
- const spinner = ora4("Checking services...").start();
1162
+ const spinner = ora5("Checking services...").start();
881
1163
  let runtime;
882
1164
  try {
883
1165
  runtime = await detectRuntime(config.runtime);
@@ -898,28 +1180,28 @@ var statusCommand = new Command4("status").description("Show status of Horus ser
898
1180
  }
899
1181
  spinner.stop();
900
1182
  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`);
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`);
906
1188
  console.log("");
907
1189
  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."));
1190
+ console.log(chalk5.yellow(" No services are running."));
1191
+ console.log(chalk5.dim(" Run `horus up` to start the stack."));
910
1192
  console.log("");
911
1193
  return;
912
1194
  }
913
1195
  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)));
1196
+ console.log(chalk5.bold(header));
1197
+ console.log(chalk5.dim(" " + "\u2500".repeat(66)));
916
1198
  for (const service of SERVICES) {
917
1199
  const container = containers.find(
918
1200
  (c) => c.Service === service || c.Name?.includes(service)
919
1201
  );
920
1202
  if (!container) {
921
1203
  console.log(
922
- ` ${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)}`
923
1205
  );
924
1206
  continue;
925
1207
  }
@@ -937,9 +1219,9 @@ function pad(str, width) {
937
1219
  }
938
1220
  function getStatusColor(status) {
939
1221
  const lower = status.toLowerCase();
940
- if (lower === "healthy" || lower === "running") return chalk4.green;
941
- if (lower === "starting") return chalk4.yellow;
942
- return chalk4.red;
1222
+ if (lower === "healthy" || lower === "running") return chalk5.green;
1223
+ if (lower === "starting") return chalk5.yellow;
1224
+ return chalk5.red;
943
1225
  }
944
1226
  function formatPorts(publishers) {
945
1227
  if (!publishers || publishers.length === 0) return "-";
@@ -954,52 +1236,52 @@ function extractUptime(status) {
954
1236
  }
955
1237
 
956
1238
  // src/commands/config.ts
957
- import { Command as Command5 } from "commander";
958
- import chalk5 from "chalk";
1239
+ import { Command as Command6 } from "commander";
1240
+ import chalk6 from "chalk";
959
1241
  import { confirm as confirm2 } from "@inquirer/prompts";
960
- 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 () => {
961
1243
  if (!configExists()) {
962
- console.log(chalk5.red("Horus is not configured yet."));
963
- 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."));
964
1246
  process.exit(1);
965
1247
  }
966
1248
  const config = loadConfig();
967
1249
  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)")}`);
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)")}`);
974
1256
  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)")}`);
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)")}`);
978
1260
  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}`);
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}`);
984
1266
  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)")}`);
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)")}`);
989
1271
  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>'`));
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>'`));
992
1274
  console.log("");
993
1275
  });
994
1276
  configCommand.command("get <key>").description("Get a configuration value").action(async (key) => {
995
1277
  if (!configExists()) {
996
- console.log(chalk5.red("Horus is not configured yet."));
997
- 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."));
998
1280
  process.exit(1);
999
1281
  }
1000
1282
  if (!isValidKey(key)) {
1001
- console.log(chalk5.red(`Unknown config key: ${key}`));
1002
- 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(", ")}`));
1003
1285
  process.exit(1);
1004
1286
  }
1005
1287
  const config = loadConfig();
@@ -1012,25 +1294,25 @@ configCommand.command("get <key>").description("Get a configuration value").acti
1012
1294
  });
1013
1295
  configCommand.command("set <key> <value>").description("Set a configuration value").action(async (key, value) => {
1014
1296
  if (!configExists()) {
1015
- console.log(chalk5.red("Horus is not configured yet."));
1016
- 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."));
1017
1299
  process.exit(1);
1018
1300
  }
1019
1301
  if (!isValidKey(key)) {
1020
- console.log(chalk5.red(`Unknown config key: ${key}`));
1021
- 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(", ")}`));
1022
1304
  process.exit(1);
1023
1305
  }
1024
1306
  let config = loadConfig();
1025
1307
  try {
1026
1308
  config = setConfigValue(config, key, value);
1027
1309
  } catch (error) {
1028
- console.log(chalk5.red(error.message));
1310
+ console.log(chalk6.red(error.message));
1029
1311
  process.exit(1);
1030
1312
  }
1031
1313
  saveConfig(config);
1032
1314
  writeEnvFile(config);
1033
- console.log(chalk5.green(`Set ${key} and regenerated .env file.`));
1315
+ console.log(chalk6.green(`Set ${key} and regenerated .env file.`));
1034
1316
  const needsRestart = [
1035
1317
  "data-dir",
1036
1318
  "host-repos-path",
@@ -1042,17 +1324,17 @@ configCommand.command("set <key> <value>").description("Set a configuration valu
1042
1324
  "port.forge"
1043
1325
  ];
1044
1326
  if (needsRestart.includes(key)) {
1045
- console.log(chalk5.yellow("Restart required for changes to take effect."));
1327
+ console.log(chalk6.yellow("Restart required for changes to take effect."));
1046
1328
  if (process.stdin.isTTY) {
1047
1329
  const restart = await confirm2({
1048
1330
  message: "Restart Horus now?",
1049
1331
  default: false
1050
1332
  });
1051
1333
  if (restart) {
1052
- console.log(chalk5.dim("Run `horus down && horus up` to restart."));
1334
+ console.log(chalk6.dim("Run `horus down && horus up` to restart."));
1053
1335
  }
1054
1336
  } else {
1055
- console.log(chalk5.dim("Run `horus down && horus up` to restart."));
1337
+ console.log(chalk6.dim("Run `horus down && horus up` to restart."));
1056
1338
  }
1057
1339
  }
1058
1340
  });
@@ -1060,266 +1342,6 @@ function isValidKey(key) {
1060
1342
  return CONFIG_KEYS.includes(key);
1061
1343
  }
1062
1344
 
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
1345
  // src/commands/update.ts
1324
1346
  import { Command as Command7 } from "commander";
1325
1347
  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.10",
4
4
  "description": "CLI for managing the Horus AI development stack",
5
5
  "type": "module",
6
6
  "bin": {