@imisbahk/hive 0.1.0 → 0.1.2

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 (76) hide show
  1. package/.github/workflows/publish.yml +31 -0
  2. package/.rocket/README.md +8 -8
  3. package/.rocket/SYMBOLS.md +260 -117
  4. package/Aborted +0 -0
  5. package/CONTRIBUTING.md +2 -1
  6. package/FEATURES.md +55 -0
  7. package/README.md +13 -11
  8. package/bun.lock +554 -0
  9. package/dist/agent/agent.d.ts +10 -1
  10. package/dist/agent/agent.d.ts.map +1 -1
  11. package/dist/agent/agent.js +351 -1
  12. package/dist/agent/agent.js.map +1 -1
  13. package/dist/browser/browser.d.ts +9 -0
  14. package/dist/browser/browser.d.ts.map +1 -0
  15. package/dist/browser/browser.js +338 -0
  16. package/dist/browser/browser.js.map +1 -0
  17. package/dist/cli/commands/chat.d.ts +5 -1
  18. package/dist/cli/commands/chat.d.ts.map +1 -1
  19. package/dist/cli/commands/chat.js +580 -38
  20. package/dist/cli/commands/chat.js.map +1 -1
  21. package/dist/cli/commands/config.d.ts +13 -0
  22. package/dist/cli/commands/config.d.ts.map +1 -1
  23. package/dist/cli/commands/config.js +257 -16
  24. package/dist/cli/commands/config.js.map +1 -1
  25. package/dist/cli/commands/init.d.ts.map +1 -1
  26. package/dist/cli/commands/init.js +39 -14
  27. package/dist/cli/commands/init.js.map +1 -1
  28. package/dist/cli/commands/nuke.d.ts.map +1 -1
  29. package/dist/cli/commands/nuke.js +5 -4
  30. package/dist/cli/commands/nuke.js.map +1 -1
  31. package/dist/cli/commands/status.d.ts +5 -0
  32. package/dist/cli/commands/status.d.ts.map +1 -1
  33. package/dist/cli/commands/status.js +16 -6
  34. package/dist/cli/commands/status.js.map +1 -1
  35. package/dist/cli/index.js +34 -12
  36. package/dist/cli/index.js.map +1 -1
  37. package/dist/cli/theme.d.ts +22 -0
  38. package/dist/cli/theme.d.ts.map +1 -0
  39. package/dist/cli/theme.js +63 -0
  40. package/dist/cli/theme.js.map +1 -0
  41. package/dist/cli/ui.d.ts +7 -0
  42. package/dist/cli/ui.d.ts.map +1 -0
  43. package/dist/cli/ui.js +101 -0
  44. package/dist/cli/ui.js.map +1 -0
  45. package/dist/providers/base.d.ts +37 -1
  46. package/dist/providers/base.d.ts.map +1 -1
  47. package/dist/providers/base.js +104 -0
  48. package/dist/providers/base.js.map +1 -1
  49. package/dist/providers/openai-compatible.d.ts +2 -1
  50. package/dist/providers/openai-compatible.d.ts.map +1 -1
  51. package/dist/providers/openai-compatible.js +18 -1
  52. package/dist/providers/openai-compatible.js.map +1 -1
  53. package/package.json +9 -1
  54. package/prompts/Browser.md +13 -0
  55. package/prompts/Debugging.md +15 -0
  56. package/prompts/Execution.md +13 -0
  57. package/prompts/Planning.md +13 -0
  58. package/prompts/Product.md +14 -0
  59. package/prompts/Review.md +15 -0
  60. package/prompts/Safety.md +12 -0
  61. package/prompts/Search.md +14 -0
  62. package/prompts/Tools.md +14 -0
  63. package/prompts/Writing.md +13 -0
  64. package/releases/v1/v0.1/RELEASE-NOTES.md +46 -0
  65. package/src/agent/agent.ts +442 -2
  66. package/src/browser/browser.ts +410 -0
  67. package/src/cli/commands/chat.ts +729 -34
  68. package/src/cli/commands/config.ts +344 -16
  69. package/src/cli/commands/init.ts +60 -14
  70. package/src/cli/commands/nuke.ts +11 -7
  71. package/src/cli/commands/status.ts +29 -6
  72. package/src/cli/index.ts +37 -9
  73. package/src/cli/theme.ts +88 -0
  74. package/src/cli/ui.ts +127 -0
  75. package/src/providers/base.ts +176 -1
  76. package/src/providers/openai-compatible.ts +24 -0
@@ -1,6 +1,6 @@
1
1
  import process from "node:process";
2
+ import * as readline from "node:readline";
2
3
 
3
- import chalk from "chalk";
4
4
  import { Command } from "commander";
5
5
  import inquirer from "inquirer";
6
6
  import keytar from "keytar";
@@ -9,6 +9,21 @@ import ora from "ora";
9
9
  import { resolveProviderApiKey } from "../../providers/api-key.js";
10
10
  import { normalizeProviderName, type ProviderName } from "../../providers/base.js";
11
11
  import { promptForModel, promptForProvider } from "../helpers/providerPrompts.js";
12
+ import {
13
+ renderError,
14
+ renderHiveHeader,
15
+ renderInfo,
16
+ renderStep,
17
+ renderSuccess,
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";
12
27
  import {
13
28
  closeHiveDatabase,
14
29
  getPrimaryAgent,
@@ -19,11 +34,40 @@ import {
19
34
  } from "../../storage/db.js";
20
35
 
21
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;
52
+
53
+ interface ConfigShowRenderOptions {
54
+ showHeader?: boolean;
55
+ }
56
+
57
+ interface ConfigInteractiveRenderOptions {
58
+ showHeader?: boolean;
59
+ }
60
+
61
+ interface ThemeOption {
62
+ name: ThemeName;
63
+ hex: string;
64
+ description?: string;
65
+ }
22
66
 
23
67
  export function registerConfigCommand(program: Command): void {
24
68
  const configCommand = program
25
69
  .command("config")
26
- .description("Update provider, model, or API keys without re-running init");
70
+ .description("Update provider, model, theme, or API keys without re-running init");
27
71
 
28
72
  configCommand
29
73
  .command("provider")
@@ -52,9 +96,31 @@ export function registerConfigCommand(program: Command): void {
52
96
  .action(async () => {
53
97
  await runConfigShowCommand();
54
98
  });
99
+
100
+ configCommand
101
+ .command("theme")
102
+ .description("Change CLI accent theme")
103
+ .action(async () => {
104
+ await runConfigThemeCommand();
105
+ });
106
+
107
+ configCommand.action(() => {
108
+ renderHiveHeader("Config");
109
+ configCommand.outputHelp();
110
+ });
55
111
  }
56
112
 
57
113
  export async function runConfigProviderCommand(): Promise<void> {
114
+ await runConfigProviderCommandWithOptions();
115
+ }
116
+
117
+ export async function runConfigProviderCommandWithOptions(
118
+ options: ConfigInteractiveRenderOptions = {},
119
+ ): Promise<void> {
120
+ const showHeader = options.showHeader ?? true;
121
+ if (showHeader) {
122
+ renderHiveHeader("Config · Provider");
123
+ }
58
124
  const spinner = ora("Loading configuration...").start();
59
125
  const db = openHiveDatabase();
60
126
 
@@ -64,7 +130,7 @@ export async function runConfigProviderCommand(): Promise<void> {
64
130
  const agent = getPrimaryAgent(db);
65
131
  if (!agent) {
66
132
  spinner.stop();
67
- console.error(chalk.red("Hive is not initialized. Run `hive init` first."));
133
+ renderError("Hive is not initialized. Run `hive init` first.");
68
134
  return;
69
135
  }
70
136
 
@@ -110,7 +176,8 @@ export async function runConfigProviderCommand(): Promise<void> {
110
176
  }
111
177
 
112
178
  spinner.succeed("Configuration saved.");
113
- console.log("Provider updated. Run `hive chat` to use it.");
179
+ renderSuccess("Provider updated.");
180
+ renderStep("Run `hive` to use it.");
114
181
  } catch (error) {
115
182
  if (spinner.isSpinning) {
116
183
  spinner.fail("Failed to update provider configuration.");
@@ -122,6 +189,16 @@ export async function runConfigProviderCommand(): Promise<void> {
122
189
  }
123
190
 
124
191
  export async function runConfigModelCommand(): Promise<void> {
192
+ await runConfigModelCommandWithOptions();
193
+ }
194
+
195
+ export async function runConfigModelCommandWithOptions(
196
+ options: ConfigInteractiveRenderOptions = {},
197
+ ): Promise<void> {
198
+ const showHeader = options.showHeader ?? true;
199
+ if (showHeader) {
200
+ renderHiveHeader("Config · Model");
201
+ }
125
202
  const spinner = ora("Loading configuration...").start();
126
203
  const db = openHiveDatabase();
127
204
 
@@ -131,7 +208,7 @@ export async function runConfigModelCommand(): Promise<void> {
131
208
  const agent = getPrimaryAgent(db);
132
209
  if (!agent) {
133
210
  spinner.stop();
134
- console.error(chalk.red("Hive is not initialized. Run `hive init` first."));
211
+ renderError("Hive is not initialized. Run `hive init` first.");
135
212
  return;
136
213
  }
137
214
 
@@ -151,7 +228,8 @@ export async function runConfigModelCommand(): Promise<void> {
151
228
  setMetaValue(db, "model", updatedAgent.model);
152
229
 
153
230
  spinner.succeed("Configuration saved.");
154
- console.log("Model updated. Run `hive chat` to use it.");
231
+ renderSuccess("Model updated.");
232
+ renderStep("Run `hive` to use it.");
155
233
  } catch (error) {
156
234
  if (spinner.isSpinning) {
157
235
  spinner.fail("Failed to update model configuration.");
@@ -163,6 +241,16 @@ export async function runConfigModelCommand(): Promise<void> {
163
241
  }
164
242
 
165
243
  export async function runConfigKeyCommand(): Promise<void> {
244
+ await runConfigKeyCommandWithOptions();
245
+ }
246
+
247
+ export async function runConfigKeyCommandWithOptions(
248
+ options: ConfigInteractiveRenderOptions = {},
249
+ ): Promise<void> {
250
+ const showHeader = options.showHeader ?? true;
251
+ if (showHeader) {
252
+ renderHiveHeader("Config · Key");
253
+ }
166
254
  const spinner = ora("Loading configuration...").start();
167
255
  const db = openHiveDatabase();
168
256
 
@@ -172,14 +260,14 @@ export async function runConfigKeyCommand(): Promise<void> {
172
260
  const agent = getPrimaryAgent(db);
173
261
  if (!agent) {
174
262
  spinner.stop();
175
- console.error(chalk.red("Hive is not initialized. Run `hive init` first."));
263
+ renderError("Hive is not initialized. Run `hive init` first.");
176
264
  return;
177
265
  }
178
266
 
179
267
  const provider = normalizeProviderName(agent.provider);
180
268
 
181
269
  spinner.stop();
182
- console.log(chalk.dim(`Current provider: ${provider}`));
270
+ renderInfo(`Current provider: ${provider}`);
183
271
 
184
272
  const answer = (await inquirer.prompt([
185
273
  {
@@ -195,7 +283,8 @@ export async function runConfigKeyCommand(): Promise<void> {
195
283
  await keytar.setPassword(KEYCHAIN_SERVICE, provider, answer.apiKey.trim());
196
284
 
197
285
  spinner.succeed("Configuration saved.");
198
- console.log("API key updated. Run `hive chat` to use it.");
286
+ renderSuccess("API key updated.");
287
+ renderStep("Run `hive` to use it.");
199
288
  } catch (error) {
200
289
  if (spinner.isSpinning) {
201
290
  spinner.fail("Failed to update API key.");
@@ -207,22 +296,90 @@ export async function runConfigKeyCommand(): Promise<void> {
207
296
  }
208
297
 
209
298
  export async function runConfigShowCommand(): Promise<void> {
299
+ await runConfigShowCommandWithOptions();
300
+ }
301
+
302
+ export async function runConfigThemeCommand(): Promise<void> {
303
+ await runConfigThemeCommandWithOptions();
304
+ }
305
+
306
+ export async function runConfigShowCommandWithOptions(
307
+ options: ConfigShowRenderOptions = {},
308
+ ): Promise<void> {
309
+ const showHeader = options.showHeader ?? true;
310
+ if (showHeader) {
311
+ renderHiveHeader("Config · Show");
312
+ }
313
+
210
314
  const db = openHiveDatabase();
211
315
 
212
316
  try {
213
317
  const agent = getPrimaryAgent(db);
214
318
  if (!agent) {
215
- console.error(chalk.red("Hive is not initialized. Run `hive init` first."));
319
+ renderError("Hive is not initialized. Run `hive init` first.");
216
320
  return;
217
321
  }
218
322
 
219
323
  const provider = normalizeProviderName(agent.provider);
220
324
  const keyStatus = await getKeyStatus(provider);
221
325
 
222
- console.log(`Provider: ${provider}`);
223
- console.log(`Model: ${agent.model}`);
224
- console.log(`Agent name: ${agent.agent_name ?? "not set"}`);
225
- console.log(`API key: ${keyStatus}`);
326
+ renderStep(`Provider: ${provider}`);
327
+ renderStep(`Model: ${agent.model}`);
328
+ renderStep(`Agent name: ${agent.agent_name ?? "not set"}`);
329
+ renderStep(`API key: ${keyStatus}`);
330
+ } finally {
331
+ closeHiveDatabase(db);
332
+ }
333
+ }
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;
226
383
  } finally {
227
384
  closeHiveDatabase(db);
228
385
  }
@@ -235,8 +392,171 @@ function ensureInteractiveTerminal(errorMessage: string): void {
235
392
  }
236
393
 
237
394
  function printCurrentProviderAndModel(provider: ProviderName, model: string): void {
238
- console.log(chalk.dim(`Current provider: ${provider}`));
239
- console.log(chalk.dim(`Current model: ${model}`));
395
+ renderInfo(`Current provider: ${provider}`);
396
+ renderInfo(`Current model: ${model}`);
397
+ }
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}`;
240
560
  }
241
561
 
242
562
  async function getKeyStatus(provider: ProviderName): Promise<"set" | "not set"> {
@@ -277,6 +597,14 @@ function requiredField(message: string): (value: string) => true | string {
277
597
  };
278
598
  }
279
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
+
280
608
  function assertNever(value: never): never {
281
609
  throw new Error(`Unsupported provider: ${String(value)}`);
282
610
  }
@@ -2,7 +2,6 @@ import process from "node:process";
2
2
  import * as fs from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
- import chalk from "chalk";
6
5
  import { Command } from "commander";
7
6
  import inquirer from "inquirer";
8
7
  import keytar from "keytar";
@@ -10,6 +9,12 @@ import ora from "ora";
10
9
 
11
10
  import { buildDefaultPersona } from "../../agent/agent.js";
12
11
  import { promptForModel, promptForProvider } from "../helpers/providerPrompts.js";
12
+ import {
13
+ renderHiveHeader,
14
+ renderInfo,
15
+ renderStep,
16
+ renderSuccess,
17
+ } from "../ui.js";
13
18
  import {
14
19
  closeHiveDatabase,
15
20
  getHiveHomeDir,
@@ -49,6 +54,7 @@ export function registerInitCommand(program: Command): void {
49
54
  }
50
55
 
51
56
  export async function runInitCommand(options: InitCommandOptions = {}): Promise<void> {
57
+ renderHiveHeader("Init");
52
58
  const spinner = ora("Preparing init...").start();
53
59
  const db = openHiveDatabase();
54
60
 
@@ -71,7 +77,7 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise<
71
77
  ])) as { reinitialize: boolean };
72
78
 
73
79
  if (!reinitialize) {
74
- console.log(chalk.dim("Initialization cancelled."));
80
+ renderInfo("Initialization cancelled.");
75
81
  return;
76
82
  }
77
83
  }
@@ -101,13 +107,13 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise<
101
107
  copyPromptsDirectory(options.force ?? false);
102
108
 
103
109
  spinner.succeed("Initialization complete.");
104
- console.log(chalk.green(`HIVE-ID: ${agent.id}`));
110
+ renderSuccess(`HIVE-ID: ${agent.id}`);
105
111
  if (agent.agent_name) {
106
- console.log(chalk.green(`Agent name: ${agent.agent_name}`));
112
+ renderSuccess(`Agent name: ${agent.agent_name}`);
107
113
  }
108
- console.log(chalk.green(`Provider: ${agent.provider}`));
109
- console.log(chalk.green(`Model: ${agent.model}`));
110
- console.log("Run `hive chat` to start talking.");
114
+ renderSuccess(`Provider: ${agent.provider}`);
115
+ renderSuccess(`Model: ${agent.model}`);
116
+ renderStep("Run `hive` to start talking.");
111
117
  } catch (error) {
112
118
  if (spinner.isSpinning) {
113
119
  spinner.fail("Hive initialization failed.");
@@ -225,18 +231,58 @@ function copyPromptsDirectory(force: boolean): void {
225
231
  const destinationPath = join(getHiveHomeDir(), "prompts");
226
232
 
227
233
  if (!fs.existsSync(sourcePath)) {
228
- console.warn(chalk.yellow("Warning: prompts/ folder not found. Skipping prompts load."));
234
+ renderInfo("Warning: prompts/ folder not found. Skipping prompts load.");
235
+ return;
236
+ }
237
+
238
+ if (force && fs.existsSync(destinationPath)) {
239
+ fs.rmSync(destinationPath, { recursive: true, force: true });
240
+ }
241
+
242
+ fs.mkdirSync(destinationPath, { recursive: true });
243
+ const copiedFiles = syncPromptFiles(sourcePath, destinationPath, force);
244
+
245
+ if (copiedFiles === 0) {
246
+ renderStep("Prompts already up to date -> ~/.hive/prompts/");
229
247
  return;
230
248
  }
231
249
 
232
- if (fs.existsSync(destinationPath)) {
233
- if (!force) {
234
- return;
250
+ renderStep(`Prompts loaded -> ~/.hive/prompts/ (${copiedFiles} files)`);
251
+ }
252
+
253
+ function syncPromptFiles(
254
+ sourceDirectory: string,
255
+ destinationDirectory: string,
256
+ overwriteExisting: boolean,
257
+ ): number {
258
+ let copiedCount = 0;
259
+ const entries = fs.readdirSync(sourceDirectory, { withFileTypes: true });
260
+
261
+ for (const entry of entries) {
262
+ const sourceEntryPath = join(sourceDirectory, entry.name);
263
+ const destinationEntryPath = join(destinationDirectory, entry.name);
264
+
265
+ if (entry.isDirectory()) {
266
+ fs.mkdirSync(destinationEntryPath, { recursive: true });
267
+ copiedCount += syncPromptFiles(
268
+ sourceEntryPath,
269
+ destinationEntryPath,
270
+ overwriteExisting,
271
+ );
272
+ continue;
235
273
  }
236
274
 
237
- fs.rmSync(destinationPath, { recursive: true, force: true });
275
+ if (!entry.isFile()) {
276
+ continue;
277
+ }
278
+
279
+ if (!overwriteExisting && fs.existsSync(destinationEntryPath)) {
280
+ continue;
281
+ }
282
+
283
+ fs.copyFileSync(sourceEntryPath, destinationEntryPath);
284
+ copiedCount += 1;
238
285
  }
239
286
 
240
- fs.cpSync(sourcePath, destinationPath, { recursive: true });
241
- console.log("Prompts loaded → ~/.hive/prompts/");
287
+ return copiedCount;
242
288
  }
@@ -2,12 +2,17 @@ import * as fs from "node:fs";
2
2
  import { stdin, stdout } from "node:process";
3
3
  import { createInterface } from "node:readline/promises";
4
4
 
5
- import chalk from "chalk";
6
5
  import { Command } from "commander";
7
6
  import keytar from "keytar";
8
7
 
9
8
  import { SUPPORTED_PROVIDER_NAMES } from "../../providers/base.js";
10
9
  import { getHiveHomeDir } from "../../storage/db.js";
10
+ import {
11
+ renderError,
12
+ renderHiveHeader,
13
+ renderInfo,
14
+ renderSuccess,
15
+ } from "../ui.js";
11
16
 
12
17
  const KEYCHAIN_SERVICE = "hive";
13
18
  const NUKE_CONFIRMATION = "nuke";
@@ -22,10 +27,9 @@ export function registerNukeCommand(program: Command): void {
22
27
  }
23
28
 
24
29
  export async function runNukeCommand(): Promise<void> {
25
- console.log(
26
- chalk.red(
27
- "This will permanently delete your agent, all memory, all conversations, and all keys. This cannot be undone.",
28
- ),
30
+ renderHiveHeader("Nuke");
31
+ renderError(
32
+ "This will permanently delete your agent, all memory, all conversations, and all keys. This cannot be undone.",
29
33
  );
30
34
 
31
35
  const rl = createInterface({
@@ -42,7 +46,7 @@ export async function runNukeCommand(): Promise<void> {
42
46
  }
43
47
 
44
48
  if (confirmation !== NUKE_CONFIRMATION) {
45
- console.log(chalk.dim("Aborted."));
49
+ renderInfo("Aborted.");
46
50
  return;
47
51
  }
48
52
 
@@ -56,5 +60,5 @@ export async function runNukeCommand(): Promise<void> {
56
60
  }
57
61
  }
58
62
 
59
- console.log(chalk.green("The Hive has been nuked. Gone."));
63
+ renderSuccess("The Hive has been nuked. Gone.");
60
64
  }