@imisbahk/hive 0.1.1 → 0.1.3

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.
@@ -0,0 +1,63 @@
1
+ import chalk from "chalk";
2
+ import { closeHiveDatabase, getMetaValue, openHiveDatabase, } from "../storage/db.js";
3
+ export const DEFAULT_THEME_NAME = "amber";
4
+ export const DEFAULT_THEME_HEX = "#FFA500";
5
+ export const BUILT_IN_THEMES = {
6
+ amber: "#FFA500",
7
+ cyan: "#00BCD4",
8
+ rose: "#FF4081",
9
+ slate: "#90A4AE",
10
+ green: "#00E676",
11
+ };
12
+ export const HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
13
+ export function applyTheme(hex) {
14
+ const normalizedHex = normalizeHex(hex);
15
+ return chalk.hex(normalizedHex);
16
+ }
17
+ export function getTheme() {
18
+ let db = null;
19
+ try {
20
+ db = openHiveDatabase();
21
+ const storedName = getMetaValue(db, "theme");
22
+ const storedHex = getMetaValue(db, "theme_hex");
23
+ return resolveTheme(storedName, storedHex);
24
+ }
25
+ catch {
26
+ return makeTheme(DEFAULT_THEME_NAME, DEFAULT_THEME_HEX);
27
+ }
28
+ finally {
29
+ if (db) {
30
+ closeHiveDatabase(db);
31
+ }
32
+ }
33
+ }
34
+ export function isValidHexColor(value) {
35
+ return HEX_COLOR_PATTERN.test(value);
36
+ }
37
+ function resolveTheme(storedName, storedHex) {
38
+ if (isBuiltInTheme(storedName)) {
39
+ return makeTheme(storedName, BUILT_IN_THEMES[storedName]);
40
+ }
41
+ if (storedName === "custom" && storedHex && isValidHexColor(storedHex)) {
42
+ return makeTheme("custom", storedHex);
43
+ }
44
+ return makeTheme(DEFAULT_THEME_NAME, DEFAULT_THEME_HEX);
45
+ }
46
+ function makeTheme(name, hex) {
47
+ const normalizedHex = normalizeHex(hex);
48
+ return {
49
+ name,
50
+ hex: normalizedHex,
51
+ accent: applyTheme(normalizedHex),
52
+ };
53
+ }
54
+ function normalizeHex(value) {
55
+ if (!isValidHexColor(value)) {
56
+ return DEFAULT_THEME_HEX;
57
+ }
58
+ return value.toUpperCase();
59
+ }
60
+ function isBuiltInTheme(value) {
61
+ return value !== null && value in BUILT_IN_THEMES;
62
+ }
63
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.js","sourceRoot":"","sources":["../../src/cli/theme.ts"],"names":[],"mappings":"AAAA,OAAO,KAA6B,MAAM,OAAO,CAAC;AAElD,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,GACjB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAC1C,MAAM,CAAC,MAAM,iBAAiB,GAAG,SAAS,CAAC;AAE3C,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,SAAS;IACf,IAAI,EAAE,SAAS;IACf,KAAK,EAAE,SAAS;IAChB,KAAK,EAAE,SAAS;CACR,CAAC;AAEX,MAAM,CAAC,MAAM,iBAAiB,GAAG,mBAAmB,CAAC;AAWrD,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,MAAM,aAAa,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,IAAI,EAAE,GAA+C,IAAI,CAAC;IAE1D,IAAI,CAAC;QACH,EAAE,GAAG,gBAAgB,EAAE,CAAC;QACxB,MAAM,UAAU,GAAG,YAAY,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;QAC7C,MAAM,SAAS,GAAG,YAAY,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;QAChD,OAAO,YAAY,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC;IAC1D,CAAC;YAAS,CAAC;QACT,IAAI,EAAE,EAAE,CAAC;YACP,iBAAiB,CAAC,EAAE,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,SAAS,YAAY,CAAC,UAAyB,EAAE,SAAwB;IACvE,IAAI,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,SAAS,CAAC,UAAU,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,IAAI,UAAU,KAAK,QAAQ,IAAI,SAAS,IAAI,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;QACvE,OAAO,SAAS,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,SAAS,CAAC,kBAAkB,EAAE,iBAAiB,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,SAAS,CAAC,IAAe,EAAE,GAAW;IAC7C,MAAM,aAAa,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO;QACL,IAAI;QACJ,GAAG,EAAE,aAAa;QAClB,MAAM,EAAE,UAAU,CAAC,aAAa,CAAC;KAClC,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,KAAa;IACjC,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IAED,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;AAC7B,CAAC;AAED,SAAS,cAAc,CAAC,KAAoB;IAC1C,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,IAAI,eAAe,CAAC;AACpD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/cli/ui.ts"],"names":[],"mappings":"AAoBA,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAkBzD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEnD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAOnD"}
1
+ {"version":3,"file":"ui.d.ts","sourceRoot":"","sources":["../../src/cli/ui.ts"],"names":[],"mappings":"AAsBA,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAmBzD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGnD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEjD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAGhD;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,eAAe,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CASnD"}
package/dist/cli/ui.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import process from "node:process";
3
3
  import chalk from "chalk";
4
+ import { getTheme } from "./theme.js";
4
5
  const WORDMARK_LINES = [
5
6
  " ██╗ ██╗██╗██╗ ██╗███████╗",
6
7
  " ██║ ██║██║██║ ██║██╔════╝",
@@ -16,8 +17,9 @@ let cachedVersion = null;
16
17
  export function renderHiveHeader(pageTitle) {
17
18
  const terminalWidth = getTerminalWidth();
18
19
  const separator = "─".repeat(getSeparatorWidth(terminalWidth));
20
+ const accent = getTheme().accent;
19
21
  for (const line of WORDMARK_LINES) {
20
- console.log(chalk.bold.whiteBright(centerText(line, terminalWidth)));
22
+ console.log(accent.bold(centerText(line, terminalWidth)));
21
23
  }
22
24
  console.log("");
23
25
  console.log(chalk.dim(centerText(`v${getCliVersion()}`, terminalWidth)));
@@ -25,27 +27,30 @@ export function renderHiveHeader(pageTitle) {
25
27
  const commandCentreTitle = normalizedTitle
26
28
  ? `${COMMAND_CENTRE_LABEL} · ${normalizedTitle}`
27
29
  : COMMAND_CENTRE_LABEL;
28
- console.log(chalk.whiteBright(centerText(commandCentreTitle, terminalWidth)));
29
- console.log(chalk.dim(centerText(separator, terminalWidth)));
30
+ console.log(accent(centerText(commandCentreTitle, terminalWidth)));
31
+ console.log(accent(centerText(separator, terminalWidth)));
30
32
  }
31
33
  export function renderSuccess(message) {
32
- console.log(chalk.green(message));
34
+ const accent = getTheme().accent;
35
+ console.log(`${accent("✓")} ${message}`);
33
36
  }
34
37
  export function renderError(message) {
35
38
  console.error(chalk.red(message));
36
39
  }
37
40
  export function renderStep(message) {
38
- console.log(chalk.whiteBright(message));
41
+ const accent = getTheme().accent;
42
+ console.log(`${accent("›")} ${message}`);
39
43
  }
40
44
  export function renderInfo(message) {
41
45
  console.log(chalk.dim(message));
42
46
  }
43
47
  export function renderSeparator(text) {
48
+ const accent = getTheme().accent;
44
49
  if (text) {
45
- console.log(chalk.dim(text));
50
+ console.log(accent(text));
46
51
  return;
47
52
  }
48
- console.log(chalk.dim("─".repeat(getSeparatorWidth(getTerminalWidth()))));
53
+ console.log(accent("─".repeat(getSeparatorWidth(getTerminalWidth()))));
49
54
  }
50
55
  function getCliVersion() {
51
56
  if (cachedVersion) {
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/cli/ui.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../../src/cli/ui.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imisbahk/hive",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Your agent. Always running. Always learning. Always working.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@
25
25
  "dependencies": {
26
26
  "@anthropic-ai/sdk": "^0.36.3",
27
27
  "@imisbahk/genie": "^1.0.0",
28
- "@imisbahk/hive": "^0.1.0",
28
+ "@imisbahk/hive": "^0.1.1",
29
29
  "@modelcontextprotocol/sdk": "^1.26.0",
30
30
  "better-sqlite3": "^12.6.2",
31
31
  "chalk": "^5.3.0",
@@ -44,3 +44,58 @@ hive init
44
44
  ```bash
45
45
  npm install -g @imisbahk/hive@0.1.1
46
46
  ```
47
+
48
+ ## 🐝 v0.1.2 — Themes + Live Accent Preview
49
+
50
+ ### What's in v0.1.2
51
+
52
+ - New `hive config theme` command to set the CLI accent theme.
53
+ - Built-in theme options:
54
+ - `amber` (`#FFA500`) default beehive accent
55
+ - `cyan` (`#00BCD4`)
56
+ - `rose` (`#FF4081`)
57
+ - `slate` (`#90A4AE`)
58
+ - `green` (`#00E676`)
59
+ - `custom` (user-provided hex)
60
+ - Live theme preview: moving through the picker updates the UI accent in real time before selection.
61
+ - Theme persistence in local DB metadata (`~/.hive/hive.db`):
62
+ - `theme`
63
+ - `theme_hex`
64
+ - Accent color is now consistent across the command centre UI:
65
+ - ASCII HIVE wordmark
66
+ - separators
67
+ - prompt symbol (`›`)
68
+ - agent name prefix in chat
69
+ - success indicator (`✓`)
70
+ - step indicator (`›`)
71
+ - New in-chat shortcut: `/hive config theme`
72
+
73
+ ### Upgrade
74
+
75
+ ```bash
76
+ npm install -g @imisbahk/hive@0.1.2
77
+ ```
78
+
79
+ ## 🐝 v0.2.0 — Doctor (Health Checks)
80
+
81
+ ### What's in v0.2.0
82
+
83
+ - New `hive doctor` command runs a full diagnostic pass across local Hive setup.
84
+ - Live, sequential output (no spinner) so checks feel immediate as they complete.
85
+ - Checks include:
86
+ - Agent initialized (DB record exists)
87
+ - Database readable + integrity check + size warning when large
88
+ - API key present in OS keychain
89
+ - Provider reachability (5s timeout)
90
+ - Prompts folder present with files
91
+ - Theme selection from local DB metadata
92
+ - Node version warning if < v20
93
+ - Playwright + Chromium installed
94
+ - Ollama running when provider is `ollama`
95
+ - Basic DB stats (messages, conversations, episodes when table exists)
96
+
97
+ ### Upgrade
98
+
99
+ ```bash
100
+ npm install -g @imisbahk/hive@0.2.0
101
+ ```
@@ -23,8 +23,10 @@ import {
23
23
  runConfigModelCommandWithOptions,
24
24
  runConfigProviderCommandWithOptions,
25
25
  runConfigShowCommandWithOptions,
26
+ runConfigThemeCommandWithOptions,
26
27
  } from "./config.js";
27
28
  import { runStatusCommandWithOptions } from "./status.js";
29
+ import { getTheme } from "../theme.js";
28
30
 
29
31
  interface ChatCommandOptions {
30
32
  message?: string;
@@ -53,7 +55,8 @@ interface CommandSuggestion {
53
55
 
54
56
  type HiveShortcutResult = "not-handled" | "handled" | "config-updated";
55
57
 
56
- const USER_PROMPT = "you ";
58
+ const PROMPT_SYMBOL = "›";
59
+ const USER_PROMPT = `you${PROMPT_SYMBOL} `;
57
60
  const HIVE_SHORTCUT_PREFIX = "/hive";
58
61
  const MAX_COMMAND_SUGGESTIONS = 8;
59
62
  const COMMAND_LABEL_WIDTH = 24;
@@ -71,6 +74,7 @@ const COMMAND_HELP_TEXT = [
71
74
  " /hive config provider interactive provider setup",
72
75
  " /hive config model interactive model setup",
73
76
  " /hive config key interactive key setup",
77
+ " /hive config theme interactive theme setup",
74
78
  " /exit quit",
75
79
  ].join("\n");
76
80
  const HIVE_SHORTCUT_HELP_TEXT = [
@@ -83,6 +87,7 @@ const HIVE_SHORTCUT_HELP_TEXT = [
83
87
  " /hive config provider",
84
88
  " /hive config model",
85
89
  " /hive config key",
90
+ " /hive config theme",
86
91
  "",
87
92
  "Safety commands still run from shell:",
88
93
  " /hive init",
@@ -155,6 +160,11 @@ const COMMAND_SUGGESTIONS: CommandSuggestion[] = [
155
160
  insertText: "/hive config key",
156
161
  description: "interactive key setup",
157
162
  },
163
+ {
164
+ label: "/hive config theme",
165
+ insertText: "/hive config theme",
166
+ description: "interactive theme setup",
167
+ },
158
168
  {
159
169
  label: "/hive nuke",
160
170
  insertText: "/hive nuke",
@@ -327,7 +337,7 @@ async function streamReply(
327
337
  options: RunChatOptions,
328
338
  agentName: string,
329
339
  ): Promise<string> {
330
- process.stdout.write(chalk.whiteBright(`${agentName} `));
340
+ process.stdout.write(getTheme().accent(`${agentName}${PROMPT_SYMBOL} `));
331
341
 
332
342
  let activeConversationId = conversationId;
333
343
 
@@ -457,7 +467,7 @@ async function runPreviewSession(options: ChatCommandOptions): Promise<void> {
457
467
 
458
468
  async function streamPreviewReply(prompt: string, agentName: string): Promise<void> {
459
469
  const response = `preview mode: received "${prompt}"`;
460
- process.stdout.write(chalk.whiteBright(`${agentName} `));
470
+ process.stdout.write(getTheme().accent(`${agentName}${PROMPT_SYMBOL} `));
461
471
  process.stdout.write(response);
462
472
  process.stdout.write("\n");
463
473
  renderSeparator(EXCHANGE_SEPARATOR);
@@ -563,6 +573,17 @@ async function handleHiveShortcut(
563
573
  return "handled";
564
574
  }
565
575
 
576
+ if (subcommand === "config theme") {
577
+ if (!options.allowInteractiveConfig) {
578
+ renderInfo("Interactive config commands are unavailable here.");
579
+ return "handled";
580
+ }
581
+
582
+ await runConfigThemeCommandWithOptions({ showHeader: false });
583
+ restoreChatInputAfterInteractiveCommand();
584
+ return "handled";
585
+ }
586
+
566
587
  if (
567
588
  subcommand === "init" ||
568
589
  subcommand === "nuke"
@@ -598,6 +619,9 @@ function getCommandSuggestions(input: string): CommandSuggestion[] {
598
619
  }
599
620
 
600
621
  async function readPromptWithSuggestions(): Promise<string> {
622
+ const accent = getTheme().accent;
623
+ const promptPrefix = accent(USER_PROMPT);
624
+
601
625
  if (!stdin.isTTY || !stdout.isTTY) {
602
626
  const rl = createInterface({
603
627
  input: stdin,
@@ -606,7 +630,7 @@ async function readPromptWithSuggestions(): Promise<string> {
606
630
  });
607
631
 
608
632
  try {
609
- return (await rl.question(chalk.whiteBright(USER_PROMPT))).trim();
633
+ return (await rl.question(promptPrefix)).trim();
610
634
  } finally {
611
635
  rl.close();
612
636
  }
@@ -654,7 +678,7 @@ async function readPromptWithSuggestions(): Promise<string> {
654
678
 
655
679
  readline.cursorTo(stdout, 0);
656
680
  readline.clearLine(stdout, 0);
657
- stdout.write(chalk.whiteBright(`${USER_PROMPT}${buffer}`));
681
+ stdout.write(`${promptPrefix}${buffer}`);
658
682
 
659
683
  for (let index = 0; index < renderedSuggestionRows; index += 1) {
660
684
  readline.moveCursor(stdout, 0, 1);
@@ -700,7 +724,7 @@ async function readPromptWithSuggestions(): Promise<string> {
700
724
 
701
725
  readline.cursorTo(stdout, 0);
702
726
  readline.clearLine(stdout, 0);
703
- stdout.write(chalk.whiteBright(`${USER_PROMPT}${buffer}`));
727
+ stdout.write(`${promptPrefix}${buffer}`);
704
728
 
705
729
  const rowsToRender = Math.max(renderedSuggestionRows, visibleSuggestions.length);
706
730
  for (let index = 0; index < rowsToRender; index += 1) {
@@ -719,7 +743,7 @@ async function readPromptWithSuggestions(): Promise<string> {
719
743
  const text = `${marker} ${label} ${suggestion.description}`;
720
744
 
721
745
  if (absoluteIndex === selectedSuggestionIndex) {
722
- stdout.write(chalk.whiteBright(text));
746
+ stdout.write(accent(text));
723
747
  } else {
724
748
  stdout.write(chalk.dim(text));
725
749
  }
@@ -1,4 +1,5 @@
1
1
  import process from "node:process";
2
+ import * as readline from "node:readline";
2
3
 
3
4
  import { Command } from "commander";
4
5
  import inquirer from "inquirer";
@@ -15,6 +16,14 @@ import {
15
16
  renderStep,
16
17
  renderSuccess,
17
18
  } from "../ui.js";
19
+ import {
20
+ BUILT_IN_THEMES,
21
+ DEFAULT_THEME_HEX,
22
+ applyTheme,
23
+ getTheme,
24
+ isValidHexColor,
25
+ type ThemeName,
26
+ } from "../theme.js";
18
27
  import {
19
28
  closeHiveDatabase,
20
29
  getPrimaryAgent,
@@ -25,6 +34,21 @@ import {
25
34
  } from "../../storage/db.js";
26
35
 
27
36
  const KEYCHAIN_SERVICE = "hive";
37
+ const THEME_LABEL_WIDTH = 8;
38
+ const ENTER_ALT_SCREEN = "\u001B[?1049h";
39
+ const EXIT_ALT_SCREEN = "\u001B[?1049l";
40
+ const CLEAR_SCREEN = "\u001B[H\u001B[2J";
41
+ const THEME_SELECTOR_TITLE = "COMMAND CENTRE · CONFIG · THEME";
42
+ const THEME_SELECTOR_MAX_SEPARATOR_WIDTH = 72;
43
+ const THEME_SELECTOR_MIN_SEPARATOR_WIDTH = 24;
44
+ const THEME_WORDMARK_LINES = [
45
+ " ██╗ ██╗██╗██╗ ██╗███████╗",
46
+ " ██║ ██║██║██║ ██║██╔════╝",
47
+ " ███████║██║██║ ██║█████╗ ",
48
+ " ██╔══██║██║╚██╗ ██╔╝██╔══╝ ",
49
+ " ██║ ██║██║ ╚████╔╝ ███████╗",
50
+ " ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚══════╝",
51
+ ] as const;
28
52
 
29
53
  interface ConfigShowRenderOptions {
30
54
  showHeader?: boolean;
@@ -34,10 +58,16 @@ interface ConfigInteractiveRenderOptions {
34
58
  showHeader?: boolean;
35
59
  }
36
60
 
61
+ interface ThemeOption {
62
+ name: ThemeName;
63
+ hex: string;
64
+ description?: string;
65
+ }
66
+
37
67
  export function registerConfigCommand(program: Command): void {
38
68
  const configCommand = program
39
69
  .command("config")
40
- .description("Update provider, model, or API keys without re-running init");
70
+ .description("Update provider, model, theme, or API keys without re-running init");
41
71
 
42
72
  configCommand
43
73
  .command("provider")
@@ -67,6 +97,13 @@ export function registerConfigCommand(program: Command): void {
67
97
  await runConfigShowCommand();
68
98
  });
69
99
 
100
+ configCommand
101
+ .command("theme")
102
+ .description("Change CLI accent theme")
103
+ .action(async () => {
104
+ await runConfigThemeCommand();
105
+ });
106
+
70
107
  configCommand.action(() => {
71
108
  renderHiveHeader("Config");
72
109
  configCommand.outputHelp();
@@ -262,6 +299,10 @@ export async function runConfigShowCommand(): Promise<void> {
262
299
  await runConfigShowCommandWithOptions();
263
300
  }
264
301
 
302
+ export async function runConfigThemeCommand(): Promise<void> {
303
+ await runConfigThemeCommandWithOptions();
304
+ }
305
+
265
306
  export async function runConfigShowCommandWithOptions(
266
307
  options: ConfigShowRenderOptions = {},
267
308
  ): Promise<void> {
@@ -291,6 +332,59 @@ export async function runConfigShowCommandWithOptions(
291
332
  }
292
333
  }
293
334
 
335
+ export async function runConfigThemeCommandWithOptions(
336
+ options: ConfigInteractiveRenderOptions = {},
337
+ ): Promise<void> {
338
+ const showHeader = options.showHeader ?? true;
339
+ if (showHeader) {
340
+ renderHiveHeader("Config · Theme");
341
+ }
342
+
343
+ const spinner = ora("Loading themes...").start();
344
+ const db = openHiveDatabase();
345
+
346
+ try {
347
+ ensureInteractiveTerminal("`hive config theme` requires an interactive terminal.");
348
+
349
+ const currentTheme = getTheme();
350
+ const themeOptions = buildThemeOptions(currentTheme.hex, currentTheme.name);
351
+
352
+ spinner.stop();
353
+
354
+ const theme = await selectThemeOption(themeOptions, currentTheme.name);
355
+
356
+ let themeHex = resolveThemeHex(theme, currentTheme.hex);
357
+
358
+ if (theme === "custom") {
359
+ const answer = (await inquirer.prompt([
360
+ {
361
+ type: "input",
362
+ name: "hex",
363
+ message: "Enter hex color: #",
364
+ default: currentTheme.name === "custom" ? currentTheme.hex : undefined,
365
+ validate: validateHexColor,
366
+ },
367
+ ])) as { hex: string };
368
+
369
+ themeHex = normalizeHexColor(answer.hex);
370
+ }
371
+
372
+ spinner.start("Saving theme...");
373
+ setMetaValue(db, "theme", theme);
374
+ setMetaValue(db, "theme_hex", themeHex);
375
+ spinner.succeed("Theme saved.");
376
+
377
+ console.log(applyTheme(themeHex)("✓ Theme set. The Hive is now yours."));
378
+ } catch (error) {
379
+ if (spinner.isSpinning) {
380
+ spinner.fail("Failed to update theme.");
381
+ }
382
+ throw error;
383
+ } finally {
384
+ closeHiveDatabase(db);
385
+ }
386
+ }
387
+
294
388
  function ensureInteractiveTerminal(errorMessage: string): void {
295
389
  if (!process.stdin.isTTY) {
296
390
  throw new Error(errorMessage);
@@ -302,6 +396,169 @@ function printCurrentProviderAndModel(provider: ProviderName, model: string): vo
302
396
  renderInfo(`Current model: ${model}`);
303
397
  }
304
398
 
399
+ function formatThemeChoice(name: string, hex: string, description?: string): string {
400
+ const dot = applyTheme(hex)("●");
401
+ const paddedName = name.padEnd(THEME_LABEL_WIDTH, " ");
402
+ const descriptionSuffix = description ? ` (${description})` : "";
403
+ return `${dot} ${paddedName} ${hex}${descriptionSuffix}`;
404
+ }
405
+
406
+ function buildThemeOptions(currentHex: string, currentTheme: ThemeName): ThemeOption[] {
407
+ const customHex = currentTheme === "custom" ? currentHex : DEFAULT_THEME_HEX;
408
+ return [
409
+ {
410
+ name: "amber",
411
+ hex: BUILT_IN_THEMES.amber,
412
+ description: "default — beehive",
413
+ },
414
+ { name: "cyan", hex: BUILT_IN_THEMES.cyan },
415
+ { name: "rose", hex: BUILT_IN_THEMES.rose },
416
+ { name: "slate", hex: BUILT_IN_THEMES.slate },
417
+ { name: "green", hex: BUILT_IN_THEMES.green },
418
+ { name: "custom", hex: customHex, description: "user provided hex" },
419
+ ];
420
+ }
421
+
422
+ function resolveThemeHex(theme: ThemeName, currentHex: string): string {
423
+ if (theme === "custom") {
424
+ return currentHex;
425
+ }
426
+
427
+ return BUILT_IN_THEMES[theme];
428
+ }
429
+
430
+ async function selectThemeOption(
431
+ themeOptions: ThemeOption[],
432
+ currentTheme: ThemeName,
433
+ ): Promise<ThemeName> {
434
+ const input = process.stdin;
435
+ const output = process.stdout;
436
+ readline.emitKeypressEvents(input);
437
+
438
+ const defaultIndex = themeOptions.findIndex((option) => option.name === currentTheme);
439
+ let selectedIndex = defaultIndex >= 0 ? defaultIndex : 0;
440
+ const wasRaw = input.isRaw ?? false;
441
+
442
+ if (!wasRaw) {
443
+ input.setRawMode(true);
444
+ }
445
+ input.resume();
446
+ output.write(ENTER_ALT_SCREEN);
447
+
448
+ return new Promise<ThemeName>((resolve, reject) => {
449
+ const cleanup = () => {
450
+ input.off("keypress", onKeypress);
451
+ if (!wasRaw) {
452
+ input.setRawMode(false);
453
+ }
454
+ output.write(EXIT_ALT_SCREEN);
455
+ };
456
+
457
+ const render = () => {
458
+ const selectedOption = themeOptions[selectedIndex] ?? themeOptions[0];
459
+ const accent = applyTheme(selectedOption?.hex ?? DEFAULT_THEME_HEX);
460
+ const terminalWidth = getThemeSelectorTerminalWidth(output.columns);
461
+ const headerLines = renderThemeSelectorHeader(accent, terminalWidth);
462
+ const lines = [
463
+ ...headerLines,
464
+ "",
465
+ "Select a theme (live preview):",
466
+ "",
467
+ ...themeOptions.map((option, index) => {
468
+ const marker = index === selectedIndex ? accent("›") : " ";
469
+ return `${marker} ${formatThemeChoice(option.name, option.hex, option.description)}`;
470
+ }),
471
+ "",
472
+ accent("Preview"),
473
+ `${accent("✓")} Theme set. The Hive is now yours.`,
474
+ `${accent("›")} Step indicator`,
475
+ `${accent("you›")} prompt ${accent("hive›")} agent`,
476
+ accent("────────────────────────────────────────"),
477
+ "",
478
+ "Enter to apply, Esc to cancel",
479
+ ];
480
+
481
+ output.write(`${CLEAR_SCREEN}${lines.join("\n")}`);
482
+ };
483
+
484
+ const onKeypress = (keyText: string, key: readline.Key) => {
485
+ if ((key.ctrl && key.name === "c") || key.name === "escape") {
486
+ cleanup();
487
+ reject(new Error("Theme selection cancelled."));
488
+ return;
489
+ }
490
+
491
+ if (key.name === "up") {
492
+ selectedIndex =
493
+ selectedIndex > 0 ? selectedIndex - 1 : themeOptions.length - 1;
494
+ render();
495
+ return;
496
+ }
497
+
498
+ if (key.name === "down") {
499
+ selectedIndex =
500
+ selectedIndex < themeOptions.length - 1 ? selectedIndex + 1 : 0;
501
+ render();
502
+ return;
503
+ }
504
+
505
+ if (key.name === "return" || key.name === "enter") {
506
+ const selectedOption = themeOptions[selectedIndex];
507
+ cleanup();
508
+ resolve(selectedOption?.name ?? "amber");
509
+ return;
510
+ }
511
+
512
+ const digit = Number.parseInt(keyText, 10);
513
+ if (!Number.isNaN(digit) && digit >= 1 && digit <= themeOptions.length) {
514
+ selectedIndex = digit - 1;
515
+ render();
516
+ }
517
+ };
518
+
519
+ input.on("keypress", onKeypress);
520
+ render();
521
+ });
522
+ }
523
+
524
+ function renderThemeSelectorHeader(
525
+ accent: ReturnType<typeof applyTheme>,
526
+ terminalWidth: number,
527
+ ): string[] {
528
+ const separator = "─".repeat(getThemeSelectorSeparatorWidth(terminalWidth));
529
+ const centredWordmark = THEME_WORDMARK_LINES.map((line) =>
530
+ accent.bold(centerText(line, terminalWidth)),
531
+ );
532
+
533
+ return [
534
+ ...centredWordmark,
535
+ accent(centerText(THEME_SELECTOR_TITLE, terminalWidth)),
536
+ accent(centerText(separator, terminalWidth)),
537
+ ];
538
+ }
539
+
540
+ function getThemeSelectorTerminalWidth(columns: number | undefined): number {
541
+ if (typeof columns !== "number" || columns < 20) {
542
+ return 80;
543
+ }
544
+
545
+ return columns;
546
+ }
547
+
548
+ function getThemeSelectorSeparatorWidth(terminalWidth: number): number {
549
+ const usableWidth = Math.max(THEME_SELECTOR_MIN_SEPARATOR_WIDTH, terminalWidth - 8);
550
+ return Math.min(THEME_SELECTOR_MAX_SEPARATOR_WIDTH, usableWidth);
551
+ }
552
+
553
+ function centerText(value: string, totalWidth: number): string {
554
+ if (value.length >= totalWidth) {
555
+ return value;
556
+ }
557
+
558
+ const leftPadding = Math.floor((totalWidth - value.length) / 2);
559
+ return `${" ".repeat(leftPadding)}${value}`;
560
+ }
561
+
305
562
  async function getKeyStatus(provider: ProviderName): Promise<"set" | "not set"> {
306
563
  const apiKey = await resolveProviderApiKey(provider, apiKeyEnvVar(provider));
307
564
  return apiKey ? "set" : "not set";
@@ -340,6 +597,14 @@ function requiredField(message: string): (value: string) => true | string {
340
597
  };
341
598
  }
342
599
 
600
+ function validateHexColor(value: string): true | string {
601
+ return isValidHexColor(value.trim()) || "Use #RRGGBB format.";
602
+ }
603
+
604
+ function normalizeHexColor(value: string): string {
605
+ return value.trim().toUpperCase();
606
+ }
607
+
343
608
  function assertNever(value: never): never {
344
609
  throw new Error(`Unsupported provider: ${String(value)}`);
345
610
  }