@imisbahk/hive 0.1.0 → 0.1.1

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