@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,16 +1,32 @@
1
1
  import { stdin, stdout } from "node:process";
2
+ import * as readline from "node:readline";
2
3
  import { createInterface } from "node:readline/promises";
3
4
 
4
5
  import chalk from "chalk";
5
6
  import { Command } from "commander";
6
7
 
7
- import { HiveAgent } from "../../agent/agent.js";
8
+ import { buildBrowserAugmentedPrompt, HiveAgent } from "../../agent/agent.js";
8
9
  import {
9
10
  closeHiveDatabase,
10
11
  getPrimaryAgent,
11
12
  openHiveDatabase,
12
13
  } from "../../storage/db.js";
13
14
  import { createProvider } from "../../providers/index.js";
15
+ import {
16
+ renderError,
17
+ renderHiveHeader,
18
+ renderInfo,
19
+ renderSeparator,
20
+ } from "../ui.js";
21
+ import {
22
+ runConfigKeyCommandWithOptions,
23
+ runConfigModelCommandWithOptions,
24
+ runConfigProviderCommandWithOptions,
25
+ runConfigShowCommandWithOptions,
26
+ runConfigThemeCommandWithOptions,
27
+ } from "./config.js";
28
+ import { runStatusCommandWithOptions } from "./status.js";
29
+ import { getTheme } from "../theme.js";
14
30
 
15
31
  interface ChatCommandOptions {
16
32
  message?: string;
@@ -18,6 +34,7 @@ interface ChatCommandOptions {
18
34
  model?: string;
19
35
  title?: string;
20
36
  temperature?: string;
37
+ preview?: boolean;
21
38
  }
22
39
 
23
40
  interface RunChatOptions {
@@ -26,87 +43,287 @@ interface RunChatOptions {
26
43
  temperature?: number;
27
44
  }
28
45
 
46
+ interface RunChatCommandContext {
47
+ entrypoint?: "default" | "chat-command";
48
+ }
49
+
50
+ interface CommandSuggestion {
51
+ label: string;
52
+ insertText: string;
53
+ description: string;
54
+ }
55
+
56
+ type HiveShortcutResult = "not-handled" | "handled" | "config-updated";
57
+
58
+ const PROMPT_SYMBOL = "›";
59
+ const USER_PROMPT = `you${PROMPT_SYMBOL} `;
60
+ const HIVE_SHORTCUT_PREFIX = "/hive";
61
+ const MAX_COMMAND_SUGGESTIONS = 8;
62
+ const COMMAND_LABEL_WIDTH = 24;
63
+ const COMMAND_HELP_TEXT = [
64
+ "Commands:",
65
+ " /help show commands",
66
+ " /new start a new conversation",
67
+ " /browse <url> read a webpage",
68
+ " browse <url> same as /browse",
69
+ " /search <query> search the web",
70
+ " search <query> same as /search",
71
+ " /hive help show Hive command shortcuts",
72
+ " /hive status run `hive status`",
73
+ " /hive config show run `hive config show`",
74
+ " /hive config provider interactive provider setup",
75
+ " /hive config model interactive model setup",
76
+ " /hive config key interactive key setup",
77
+ " /hive config theme interactive theme setup",
78
+ " /exit quit",
79
+ ].join("\n");
80
+ const HIVE_SHORTCUT_HELP_TEXT = [
81
+ "Hive shortcuts:",
82
+ " /hive help list shortcuts",
83
+ " /hive status run hive status",
84
+ " /hive config show run hive config show",
85
+ "",
86
+ "Interactive config commands (in chat):",
87
+ " /hive config provider",
88
+ " /hive config model",
89
+ " /hive config key",
90
+ " /hive config theme",
91
+ "",
92
+ "Safety commands still run from shell:",
93
+ " /hive init",
94
+ " /hive nuke",
95
+ ].join("\n");
96
+ const CHAT_HINT_TEXT = "? for help | /exit to quit";
97
+ const EXCHANGE_SEPARATOR = "────";
98
+ const PREVIEW_AGENT_NAME = "jarvis";
99
+ const PREVIEW_PROVIDER = "google";
100
+ const PREVIEW_MODEL = "gemini-2.0-flash";
101
+ const PREVIEW_NEW_MESSAGE = "Started a new preview conversation context.";
102
+ const COMMAND_SUGGESTIONS: CommandSuggestion[] = [
103
+ {
104
+ label: "/help",
105
+ insertText: "/help",
106
+ description: "show chat commands",
107
+ },
108
+ {
109
+ label: "/new",
110
+ insertText: "/new",
111
+ description: "start a new conversation",
112
+ },
113
+ {
114
+ label: "/browse <url>",
115
+ insertText: "/browse ",
116
+ description: "read a webpage",
117
+ },
118
+ {
119
+ label: "/search <query>",
120
+ insertText: "/search ",
121
+ description: "search the web",
122
+ },
123
+ {
124
+ label: "/exit",
125
+ insertText: "/exit",
126
+ description: "quit chat",
127
+ },
128
+ {
129
+ label: "/hive help",
130
+ insertText: "/hive help",
131
+ description: "show Hive command shortcuts",
132
+ },
133
+ {
134
+ label: "/hive status",
135
+ insertText: "/hive status",
136
+ description: "run hive status",
137
+ },
138
+ {
139
+ label: "/hive config show",
140
+ insertText: "/hive config show",
141
+ description: "run hive config show",
142
+ },
143
+ {
144
+ label: "/hive init",
145
+ insertText: "/hive init",
146
+ description: "run hive init (outside chat)",
147
+ },
148
+ {
149
+ label: "/hive config provider",
150
+ insertText: "/hive config provider",
151
+ description: "interactive provider setup",
152
+ },
153
+ {
154
+ label: "/hive config model",
155
+ insertText: "/hive config model",
156
+ description: "interactive model setup",
157
+ },
158
+ {
159
+ label: "/hive config key",
160
+ insertText: "/hive config key",
161
+ description: "interactive key setup",
162
+ },
163
+ {
164
+ label: "/hive config theme",
165
+ insertText: "/hive config theme",
166
+ description: "interactive theme setup",
167
+ },
168
+ {
169
+ label: "/hive nuke",
170
+ insertText: "/hive nuke",
171
+ description: "run hive nuke (outside chat)",
172
+ },
173
+ ];
174
+
29
175
  export function registerChatCommand(program: Command): void {
30
176
  program
31
177
  .command("chat")
32
- .description("Talk to your Hive agent")
178
+ .description("(Deprecated) Talk to your Hive agent. Use `hive`.")
33
179
  .option("-m, --message <text>", "send a single message and exit")
34
180
  .option("-c, --conversation <id>", "continue an existing conversation")
35
181
  .option("--model <model>", "override model for this session")
36
182
  .option("--title <title>", "title for a newly created conversation")
37
183
  .option("-t, --temperature <value>", "sampling temperature")
184
+ .option("--preview", "run chat UI preview without Hive initialization")
38
185
  .action(async (options: ChatCommandOptions) => {
39
- await runChatCommand(options);
186
+ await runChatCommand(options, { entrypoint: "chat-command" });
40
187
  });
41
188
  }
42
189
 
43
- export async function runChatCommand(options: ChatCommandOptions): Promise<void> {
190
+ export async function runChatCommand(
191
+ options: ChatCommandOptions,
192
+ context: RunChatCommandContext = {},
193
+ ): Promise<void> {
194
+ console.clear();
195
+ renderHiveHeader("Chat");
196
+
197
+ const entrypoint = context.entrypoint ?? "chat-command";
198
+ if (entrypoint === "chat-command") {
199
+ renderInfo("`hive chat` is deprecated. Run `hive`.");
200
+ }
201
+
202
+ if (options.preview) {
203
+ await runPreviewSession(options);
204
+ return;
205
+ }
206
+
44
207
  const temperature = parseTemperature(options.temperature);
45
208
  const db = openHiveDatabase();
46
209
 
47
210
  try {
48
211
  const profile = getPrimaryAgent(db);
49
212
  if (!profile) {
50
- console.error(chalk.red("Hive is not initialized. Run `hive init` first."));
213
+ renderError("Hive is not initialized. Run `hive init` first.");
51
214
  return;
52
215
  }
53
216
 
54
- const provider = await createProvider(profile.provider);
55
- const agent = new HiveAgent(db, provider, profile);
217
+ let activeProfile = profile;
218
+ let provider = await createProvider(activeProfile.provider);
219
+ let agent = new HiveAgent(db, provider, activeProfile);
220
+ let agentName = resolveAgentName(activeProfile.agent_name);
221
+ const model = options.model ?? activeProfile.model;
56
222
 
57
223
  let conversationId = options.conversation;
58
224
  const runOptions: RunChatOptions = {
59
- model: options.model,
225
+ model,
60
226
  title: options.title,
61
227
  temperature,
62
228
  };
63
229
 
230
+ renderChatPreamble({
231
+ agentName,
232
+ provider: profile.provider,
233
+ model,
234
+ });
235
+
64
236
  if (options.message) {
237
+ const augmentedMessage = await buildBrowserAugmentedPrompt(options.message, {
238
+ locationHint: profile.location ?? undefined,
239
+ });
65
240
  conversationId = await streamReply(
66
241
  agent,
67
- options.message,
242
+ augmentedMessage,
68
243
  conversationId,
69
244
  runOptions,
245
+ agentName,
70
246
  );
71
- console.log(chalk.dim(`conversation: ${conversationId}`));
247
+ renderInfo(`conversation: ${conversationId}`);
72
248
  return;
73
249
  }
74
250
 
75
- const rl = createInterface({
76
- input: stdin,
77
- output: stdout,
78
- terminal: true,
79
- });
251
+ while (true) {
252
+ const prompt = await readPromptWithSuggestions();
80
253
 
81
- console.log(chalk.dim("Type /exit to quit, /new to start a fresh conversation."));
254
+ if (prompt.length === 0) {
255
+ continue;
256
+ }
82
257
 
83
- try {
84
- while (true) {
85
- const prompt = (await rl.question(chalk.cyan("you> "))).trim();
258
+ if (prompt === "/") {
259
+ printChatHelp();
260
+ continue;
261
+ }
262
+
263
+ if (prompt === "/help") {
264
+ printChatHelp();
265
+ continue;
266
+ }
267
+
268
+ if (prompt === "/exit" || prompt === "/quit") {
269
+ break;
270
+ }
86
271
 
87
- if (prompt.length === 0) {
272
+ if (prompt === "/new") {
273
+ conversationId = undefined;
274
+ renderInfo("Started a new conversation context.");
88
275
  continue;
89
276
  }
90
277
 
91
- if (prompt === "/exit" || prompt === "/quit") {
92
- break;
278
+ try {
279
+ const shortcutResult = await handleHiveShortcut(prompt, {
280
+ allowInteractiveConfig: true,
281
+ });
282
+ if (shortcutResult === "handled") {
283
+ continue;
93
284
  }
285
+ if (shortcutResult === "config-updated") {
286
+ const latestProfile = getPrimaryAgent(db);
287
+ if (!latestProfile) {
288
+ renderError("Hive is not initialized. Run `hive init` first.");
289
+ continue;
290
+ }
291
+
292
+ activeProfile = latestProfile;
293
+ provider = await createProvider(activeProfile.provider);
294
+ agent = new HiveAgent(db, provider, activeProfile);
295
+ agentName = resolveAgentName(activeProfile.agent_name);
296
+ if (!options.model) {
297
+ runOptions.model = activeProfile.model;
298
+ }
94
299
 
95
- if (prompt === "/new") {
96
300
  conversationId = undefined;
97
- console.log(chalk.dim("Started a new conversation context."));
301
+ renderInfo(
302
+ `Switched to ${activeProfile.provider} · ${runOptions.model ?? activeProfile.model}.`,
303
+ );
304
+ renderInfo("Started a new conversation context.");
98
305
  continue;
99
306
  }
100
307
 
101
- try {
102
- conversationId = await streamReply(agent, prompt, conversationId, runOptions);
103
- } catch (error) {
104
- process.stdout.write("\n");
105
- console.error(formatError(error));
308
+ if (isUnknownSlashCommand(prompt)) {
309
+ renderError(`Unknown command: ${prompt}`);
310
+ renderInfo("Run `/help` to view supported commands.");
311
+ continue;
106
312
  }
313
+
314
+ const augmentedPrompt = await buildBrowserAugmentedPrompt(prompt, {
315
+ locationHint: profile.location ?? undefined,
316
+ });
317
+ conversationId = await streamReply(
318
+ agent,
319
+ augmentedPrompt,
320
+ conversationId,
321
+ runOptions,
322
+ agentName,
323
+ );
324
+ } catch (error) {
325
+ renderError(formatError(error));
107
326
  }
108
- } finally {
109
- rl.close();
110
327
  }
111
328
  } finally {
112
329
  closeHiveDatabase(db);
@@ -118,8 +335,9 @@ async function streamReply(
118
335
  prompt: string,
119
336
  conversationId: string | undefined,
120
337
  options: RunChatOptions,
338
+ agentName: string,
121
339
  ): Promise<string> {
122
- process.stdout.write(chalk.green("hive> "));
340
+ process.stdout.write(getTheme().accent(`${agentName}${PROMPT_SYMBOL} `));
123
341
 
124
342
  let activeConversationId = conversationId;
125
343
 
@@ -139,6 +357,7 @@ async function streamReply(
139
357
  }
140
358
 
141
359
  process.stdout.write("\n");
360
+ renderSeparator(EXCHANGE_SEPARATOR);
142
361
 
143
362
  if (!activeConversationId) {
144
363
  throw new Error("Conversation state was not returned by the agent.");
@@ -162,8 +381,484 @@ function parseTemperature(raw?: string): number | undefined {
162
381
 
163
382
  function formatError(error: unknown): string {
164
383
  if (error instanceof Error) {
165
- return chalk.red(error.message);
384
+ return error.message;
385
+ }
386
+
387
+ return String(error);
388
+ }
389
+
390
+ function resolveAgentName(agentName: string | null | undefined): string {
391
+ const normalized = agentName?.trim();
392
+ if (normalized && normalized.length > 0) {
393
+ return normalized;
394
+ }
395
+
396
+ return "hive";
397
+ }
398
+
399
+ function renderChatPreamble(input: {
400
+ agentName: string;
401
+ provider: string;
402
+ model: string;
403
+ }): void {
404
+ renderInfo(`${input.agentName} · ${input.provider} · ${input.model}`);
405
+ renderInfo(CHAT_HINT_TEXT);
406
+ }
407
+
408
+ function printChatHelp(): void {
409
+ renderInfo(COMMAND_HELP_TEXT);
410
+ }
411
+
412
+ async function runPreviewSession(options: ChatCommandOptions): Promise<void> {
413
+ const model = options.model ?? PREVIEW_MODEL;
414
+ const agentName = PREVIEW_AGENT_NAME;
415
+
416
+ renderChatPreamble({
417
+ agentName,
418
+ provider: PREVIEW_PROVIDER,
419
+ model,
420
+ });
421
+
422
+ if (options.message) {
423
+ await streamPreviewReply(options.message, agentName);
424
+ return;
425
+ }
426
+
427
+ while (true) {
428
+ const prompt = await readPromptWithSuggestions();
429
+
430
+ if (prompt.length === 0) {
431
+ continue;
432
+ }
433
+
434
+ if (prompt === "/") {
435
+ printChatHelp();
436
+ continue;
437
+ }
438
+
439
+ if (prompt === "/help") {
440
+ printChatHelp();
441
+ continue;
442
+ }
443
+
444
+ if (prompt === "/exit" || prompt === "/quit") {
445
+ break;
446
+ }
447
+
448
+ if (prompt === "/new") {
449
+ renderInfo(PREVIEW_NEW_MESSAGE);
450
+ continue;
451
+ }
452
+
453
+ if (isHiveShortcut(prompt)) {
454
+ renderInfo("Hive shortcuts are unavailable in preview mode.");
455
+ continue;
456
+ }
457
+
458
+ if (isUnknownSlashCommand(prompt)) {
459
+ renderError(`Unknown command: ${prompt}`);
460
+ renderInfo("Run `/help` to view supported commands.");
461
+ continue;
462
+ }
463
+
464
+ await streamPreviewReply(prompt, agentName);
465
+ }
466
+ }
467
+
468
+ async function streamPreviewReply(prompt: string, agentName: string): Promise<void> {
469
+ const response = `preview mode: received "${prompt}"`;
470
+ process.stdout.write(getTheme().accent(`${agentName}${PROMPT_SYMBOL} `));
471
+ process.stdout.write(response);
472
+ process.stdout.write("\n");
473
+ renderSeparator(EXCHANGE_SEPARATOR);
474
+ }
475
+
476
+ function isHiveShortcut(prompt: string): boolean {
477
+ const normalized = prompt.trim().toLowerCase();
478
+ return normalized === HIVE_SHORTCUT_PREFIX || normalized.startsWith(`${HIVE_SHORTCUT_PREFIX} `);
479
+ }
480
+
481
+ function isUnknownSlashCommand(prompt: string): boolean {
482
+ const normalized = prompt.trim().toLowerCase();
483
+ if (!normalized.startsWith("/")) {
484
+ return false;
485
+ }
486
+
487
+ if (
488
+ normalized === "/help" ||
489
+ normalized === "/new" ||
490
+ normalized === "/exit" ||
491
+ normalized === "/quit" ||
492
+ normalized === "/browse" ||
493
+ normalized.startsWith("/browse ") ||
494
+ normalized === "/search" ||
495
+ normalized.startsWith("/search ") ||
496
+ normalized === HIVE_SHORTCUT_PREFIX ||
497
+ normalized.startsWith(`${HIVE_SHORTCUT_PREFIX} `)
498
+ ) {
499
+ return false;
500
+ }
501
+
502
+ return true;
503
+ }
504
+
505
+ async function handleHiveShortcut(
506
+ prompt: string,
507
+ options: {
508
+ allowInteractiveConfig?: boolean;
509
+ } = {},
510
+ ): Promise<HiveShortcutResult> {
511
+ const normalized = prompt.trim().replace(/\s+/g, " ");
512
+ const lower = normalized.toLowerCase();
513
+
514
+ if (lower === HIVE_SHORTCUT_PREFIX) {
515
+ renderInfo(HIVE_SHORTCUT_HELP_TEXT);
516
+ return "handled";
517
+ }
518
+
519
+ if (!lower.startsWith(`${HIVE_SHORTCUT_PREFIX} `)) {
520
+ return "not-handled";
521
+ }
522
+
523
+ const rawSubcommand = normalized.slice(HIVE_SHORTCUT_PREFIX.length).trim();
524
+ const subcommand = rawSubcommand.toLowerCase();
525
+
526
+ if (subcommand.length === 0 || subcommand === "help") {
527
+ renderInfo(HIVE_SHORTCUT_HELP_TEXT);
528
+ return "handled";
529
+ }
530
+
531
+ if (subcommand === "status") {
532
+ await runStatusCommandWithOptions({ showHeader: false });
533
+ restoreChatInputAfterInteractiveCommand();
534
+ return "handled";
535
+ }
536
+
537
+ if (subcommand === "config show") {
538
+ await runConfigShowCommandWithOptions({ showHeader: false });
539
+ restoreChatInputAfterInteractiveCommand();
540
+ return "handled";
541
+ }
542
+
543
+ if (subcommand === "config provider") {
544
+ if (!options.allowInteractiveConfig) {
545
+ renderInfo("Interactive config commands are unavailable here.");
546
+ return "handled";
547
+ }
548
+
549
+ await runConfigProviderCommandWithOptions({ showHeader: false });
550
+ restoreChatInputAfterInteractiveCommand();
551
+ return "config-updated";
552
+ }
553
+
554
+ if (subcommand === "config model") {
555
+ if (!options.allowInteractiveConfig) {
556
+ renderInfo("Interactive config commands are unavailable here.");
557
+ return "handled";
558
+ }
559
+
560
+ await runConfigModelCommandWithOptions({ showHeader: false });
561
+ restoreChatInputAfterInteractiveCommand();
562
+ return "config-updated";
563
+ }
564
+
565
+ if (subcommand === "config key") {
566
+ if (!options.allowInteractiveConfig) {
567
+ renderInfo("Interactive config commands are unavailable here.");
568
+ return "handled";
569
+ }
570
+
571
+ await runConfigKeyCommandWithOptions({ showHeader: false });
572
+ restoreChatInputAfterInteractiveCommand();
573
+ return "handled";
574
+ }
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
+
587
+ if (
588
+ subcommand === "init" ||
589
+ subcommand === "nuke"
590
+ ) {
591
+ renderInfo(`Run \`hive ${rawSubcommand}\` from your shell. This command is interactive.`);
592
+ return "handled";
593
+ }
594
+
595
+ renderError(`Unknown Hive shortcut: /hive ${rawSubcommand}`);
596
+ renderInfo("Use `/hive help` to list available shortcuts.");
597
+ return "handled";
598
+ }
599
+
600
+ function getCommandSuggestions(input: string): CommandSuggestion[] {
601
+ const normalized = input.trimStart().toLowerCase();
602
+ if (!normalized.startsWith("/")) {
603
+ return [];
604
+ }
605
+
606
+ const prefixMatches = COMMAND_SUGGESTIONS.filter(
607
+ (suggestion) =>
608
+ suggestion.insertText.toLowerCase().startsWith(normalized) ||
609
+ suggestion.label.toLowerCase().startsWith(normalized),
610
+ );
611
+
612
+ const fallbackMatches = COMMAND_SUGGESTIONS.filter(
613
+ (suggestion) =>
614
+ !prefixMatches.includes(suggestion) &&
615
+ suggestion.label.toLowerCase().includes(normalized.slice(1)),
616
+ );
617
+
618
+ return [...prefixMatches, ...fallbackMatches];
619
+ }
620
+
621
+ async function readPromptWithSuggestions(): Promise<string> {
622
+ const accent = getTheme().accent;
623
+ const promptPrefix = accent(USER_PROMPT);
624
+
625
+ if (!stdin.isTTY || !stdout.isTTY) {
626
+ const rl = createInterface({
627
+ input: stdin,
628
+ output: stdout,
629
+ terminal: true,
630
+ });
631
+
632
+ try {
633
+ return (await rl.question(promptPrefix)).trim();
634
+ } finally {
635
+ rl.close();
636
+ }
637
+ }
638
+
639
+ return new Promise<string>((resolve) => {
640
+ stdin.resume();
641
+ readline.emitKeypressEvents(stdin);
642
+
643
+ const wasRaw = stdin.isRaw ?? false;
644
+ if (!wasRaw) {
645
+ stdin.setRawMode(true);
646
+ }
647
+
648
+ let buffer = "";
649
+ let selectedSuggestionIndex = 0;
650
+ let suggestionWindowStart = 0;
651
+ let renderedSuggestionRows = 0;
652
+
653
+ const cleanup = () => {
654
+ stdin.off("keypress", onKeypress);
655
+ if (!wasRaw) {
656
+ stdin.setRawMode(false);
657
+ }
658
+ };
659
+
660
+ const commit = () => {
661
+ const suggestions = getCommandSuggestions(buffer);
662
+ const selected = suggestions[selectedSuggestionIndex];
663
+ let value = buffer.trim();
664
+
665
+ if (
666
+ selected &&
667
+ value.startsWith("/") &&
668
+ (value === "/" ||
669
+ selected.insertText.toLowerCase().startsWith(value.toLowerCase()) ||
670
+ selected.label.toLowerCase().startsWith(value.toLowerCase()))
671
+ ) {
672
+ value = selected.insertText.trimEnd();
673
+ }
674
+
675
+ if (value === "/") {
676
+ value = "/help";
677
+ }
678
+
679
+ readline.cursorTo(stdout, 0);
680
+ readline.clearLine(stdout, 0);
681
+ stdout.write(`${promptPrefix}${buffer}`);
682
+
683
+ for (let index = 0; index < renderedSuggestionRows; index += 1) {
684
+ readline.moveCursor(stdout, 0, 1);
685
+ readline.cursorTo(stdout, 0);
686
+ readline.clearLine(stdout, 0);
687
+ }
688
+
689
+ for (let index = 0; index < renderedSuggestionRows; index += 1) {
690
+ readline.moveCursor(stdout, 0, -1);
691
+ }
692
+
693
+ renderedSuggestionRows = 0;
694
+ readline.cursorTo(stdout, USER_PROMPT.length + buffer.length);
695
+ stdout.write("\n");
696
+ cleanup();
697
+ resolve(value);
698
+ };
699
+
700
+ const render = () => {
701
+ const suggestions = getCommandSuggestions(buffer);
702
+ if (selectedSuggestionIndex >= suggestions.length) {
703
+ selectedSuggestionIndex = Math.max(0, suggestions.length - 1);
704
+ }
705
+ if (suggestions.length === 0) {
706
+ suggestionWindowStart = 0;
707
+ }
708
+
709
+ const visibleSuggestionCount = Math.min(MAX_COMMAND_SUGGESTIONS, suggestions.length);
710
+ if (selectedSuggestionIndex < suggestionWindowStart) {
711
+ suggestionWindowStart = selectedSuggestionIndex;
712
+ }
713
+ if (
714
+ visibleSuggestionCount > 0 &&
715
+ selectedSuggestionIndex >= suggestionWindowStart + visibleSuggestionCount
716
+ ) {
717
+ suggestionWindowStart = selectedSuggestionIndex - visibleSuggestionCount + 1;
718
+ }
719
+
720
+ const visibleSuggestions = suggestions.slice(
721
+ suggestionWindowStart,
722
+ suggestionWindowStart + visibleSuggestionCount,
723
+ );
724
+
725
+ readline.cursorTo(stdout, 0);
726
+ readline.clearLine(stdout, 0);
727
+ stdout.write(`${promptPrefix}${buffer}`);
728
+
729
+ const rowsToRender = Math.max(renderedSuggestionRows, visibleSuggestions.length);
730
+ for (let index = 0; index < rowsToRender; index += 1) {
731
+ readline.moveCursor(stdout, 0, 1);
732
+ readline.cursorTo(stdout, 0);
733
+ readline.clearLine(stdout, 0);
734
+
735
+ if (index >= visibleSuggestions.length) {
736
+ continue;
737
+ }
738
+
739
+ const suggestion = visibleSuggestions[index];
740
+ const absoluteIndex = suggestionWindowStart + index;
741
+ const marker = absoluteIndex === selectedSuggestionIndex ? ">" : " ";
742
+ const label = suggestion.label.padEnd(COMMAND_LABEL_WIDTH, " ");
743
+ const text = `${marker} ${label} ${suggestion.description}`;
744
+
745
+ if (absoluteIndex === selectedSuggestionIndex) {
746
+ stdout.write(accent(text));
747
+ } else {
748
+ stdout.write(chalk.dim(text));
749
+ }
750
+ }
751
+
752
+ for (let index = 0; index < rowsToRender; index += 1) {
753
+ readline.moveCursor(stdout, 0, -1);
754
+ }
755
+
756
+ readline.cursorTo(stdout, USER_PROMPT.length + buffer.length);
757
+ renderedSuggestionRows = visibleSuggestions.length;
758
+ };
759
+
760
+ const onKeypress = (str: string, key: readline.Key) => {
761
+ if ((key.ctrl && key.name === "c") || (key.ctrl && key.name === "d")) {
762
+ buffer = "/exit";
763
+ commit();
764
+ return;
765
+ }
766
+
767
+ if (key.name === "return" || key.name === "enter") {
768
+ const suggestions = getCommandSuggestions(buffer);
769
+ const selected = suggestions[selectedSuggestionIndex];
770
+ const trimmed = buffer.trim();
771
+
772
+ if (
773
+ selected &&
774
+ trimmed.startsWith("/") &&
775
+ (trimmed === "/" ||
776
+ selected.insertText.toLowerCase().startsWith(trimmed.toLowerCase()) ||
777
+ selected.label.toLowerCase().startsWith(trimmed.toLowerCase()))
778
+ ) {
779
+ buffer = selected.insertText;
780
+ selectedSuggestionIndex = 0;
781
+ suggestionWindowStart = 0;
782
+
783
+ // Commands that expect extra input should stay in edit mode.
784
+ if (buffer.endsWith(" ")) {
785
+ render();
786
+ return;
787
+ }
788
+ }
789
+
790
+ commit();
791
+ return;
792
+ }
793
+
794
+ if (key.name === "backspace") {
795
+ const chars = Array.from(buffer);
796
+ chars.pop();
797
+ buffer = chars.join("");
798
+ selectedSuggestionIndex = 0;
799
+ suggestionWindowStart = 0;
800
+ render();
801
+ return;
802
+ }
803
+
804
+ if (key.name === "up" || key.name === "down") {
805
+ const suggestions = getCommandSuggestions(buffer);
806
+ if (suggestions.length === 0) {
807
+ return;
808
+ }
809
+
810
+ if (key.name === "up") {
811
+ selectedSuggestionIndex =
812
+ selectedSuggestionIndex > 0
813
+ ? selectedSuggestionIndex - 1
814
+ : suggestions.length - 1;
815
+ } else {
816
+ selectedSuggestionIndex =
817
+ selectedSuggestionIndex < suggestions.length - 1
818
+ ? selectedSuggestionIndex + 1
819
+ : 0;
820
+ }
821
+
822
+ render();
823
+ return;
824
+ }
825
+
826
+ if (key.name === "tab") {
827
+ const suggestions = getCommandSuggestions(buffer);
828
+ if (suggestions.length === 0) {
829
+ return;
830
+ }
831
+
832
+ buffer = suggestions[selectedSuggestionIndex]?.insertText ?? buffer;
833
+ selectedSuggestionIndex = 0;
834
+ suggestionWindowStart = 0;
835
+ render();
836
+ return;
837
+ }
838
+
839
+ if (typeof str === "string" && str.length > 0 && !key.ctrl && !key.meta) {
840
+ buffer += str;
841
+ selectedSuggestionIndex = 0;
842
+ suggestionWindowStart = 0;
843
+ render();
844
+ }
845
+ };
846
+
847
+ stdin.on("keypress", onKeypress);
848
+ render();
849
+ });
850
+ }
851
+
852
+ function restoreChatInputAfterInteractiveCommand(): void {
853
+ if (!stdin.isTTY) {
854
+ return;
855
+ }
856
+
857
+ try {
858
+ stdin.setRawMode(false);
859
+ } catch {
860
+ // Ignore terminal mode recovery errors.
166
861
  }
167
862
 
168
- return chalk.red(String(error));
863
+ stdin.resume();
169
864
  }