@imisbahk/hive 0.1.2 → 0.1.4

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 (84) hide show
  1. package/.gitattributes +7 -0
  2. package/.rocket/README.md +9 -9
  3. package/dist/agent/agent.d.ts +4 -0
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +40 -4
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/cli/commands/chat.d.ts.map +1 -1
  8. package/dist/cli/commands/chat.js +642 -12
  9. package/dist/cli/commands/chat.js.map +1 -1
  10. package/dist/cli/commands/doctor.d.ts +8 -0
  11. package/dist/cli/commands/doctor.d.ts.map +1 -0
  12. package/dist/cli/commands/doctor.js +503 -0
  13. package/dist/cli/commands/doctor.js.map +1 -0
  14. package/dist/cli/commands/memory.d.ts +3 -0
  15. package/dist/cli/commands/memory.d.ts.map +1 -0
  16. package/dist/cli/commands/memory.js +104 -0
  17. package/dist/cli/commands/memory.js.map +1 -0
  18. package/dist/cli/index.js +5 -1
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/providers/api-key.js +1 -1
  21. package/dist/providers/api-key.js.map +1 -1
  22. package/dist/providers/base.js +1 -1
  23. package/dist/providers/base.js.map +1 -1
  24. package/dist/providers/openai-compatible.js +2 -2
  25. package/dist/providers/openai-compatible.js.map +1 -1
  26. package/dist/storage/db.d.ts +31 -2
  27. package/dist/storage/db.d.ts.map +1 -1
  28. package/dist/storage/db.js +165 -3
  29. package/dist/storage/db.js.map +1 -1
  30. package/dist/storage/schema.d.ts +12 -1
  31. package/dist/storage/schema.d.ts.map +1 -1
  32. package/dist/storage/schema.js +29 -1
  33. package/dist/storage/schema.js.map +1 -1
  34. package/package.json +8 -2
  35. package/.github/workflows/publish.yml +0 -31
  36. package/.rocket/ARCHITECTURE.md +0 -7
  37. package/.rocket/SYMBOLS.md +0 -425
  38. package/001-local-first-storage.md +0 -43
  39. package/003-memory-architechture.md +0 -71
  40. package/CONTRIBUTING.md +0 -150
  41. package/FEATURES.md +0 -55
  42. package/index.md +0 -16
  43. package/prompts/Behaviour.md +0 -23
  44. package/prompts/Browser.md +0 -13
  45. package/prompts/Code.md +0 -12
  46. package/prompts/Debugging.md +0 -15
  47. package/prompts/Execution.md +0 -13
  48. package/prompts/Memory.md +0 -11
  49. package/prompts/Planning.md +0 -13
  50. package/prompts/Product.md +0 -14
  51. package/prompts/Review.md +0 -15
  52. package/prompts/Safety.md +0 -12
  53. package/prompts/Search.md +0 -14
  54. package/prompts/System.md +0 -6
  55. package/prompts/Tools.md +0 -14
  56. package/prompts/Writing.md +0 -13
  57. package/releases/v1/v0.1/RELEASE-NOTES.md +0 -46
  58. package/src/agent/agent.ts +0 -595
  59. package/src/agent/index.ts +0 -2
  60. package/src/browser/browser.ts +0 -410
  61. package/src/cli/commands/chat.ts +0 -864
  62. package/src/cli/commands/config.ts +0 -610
  63. package/src/cli/commands/init.ts +0 -288
  64. package/src/cli/commands/nuke.ts +0 -64
  65. package/src/cli/commands/status.ts +0 -170
  66. package/src/cli/helpers/providerPrompts.ts +0 -192
  67. package/src/cli/index.ts +0 -66
  68. package/src/cli/theme.ts +0 -88
  69. package/src/cli/ui.ts +0 -127
  70. package/src/providers/anthropic.ts +0 -146
  71. package/src/providers/api-key.ts +0 -23
  72. package/src/providers/base.ts +0 -409
  73. package/src/providers/google.ts +0 -21
  74. package/src/providers/groq.ts +0 -21
  75. package/src/providers/index.ts +0 -65
  76. package/src/providers/mistral.ts +0 -21
  77. package/src/providers/ollama.ts +0 -22
  78. package/src/providers/openai-compatible.ts +0 -82
  79. package/src/providers/openai.ts +0 -21
  80. package/src/providers/openrouter.ts +0 -21
  81. package/src/providers/together.ts +0 -21
  82. package/src/storage/db.ts +0 -476
  83. package/src/storage/schema.ts +0 -116
  84. package/tsconfig.json +0 -51
@@ -1,11 +1,16 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
1
3
  import { stdin, stdout } from "node:process";
2
4
  import * as readline from "node:readline";
3
5
  import { createInterface } from "node:readline/promises";
6
+ import { spawnSync } from "node:child_process";
4
7
  import chalk from "chalk";
5
- import { buildBrowserAugmentedPrompt, HiveAgent } from "../../agent/agent.js";
6
- import { closeHiveDatabase, getPrimaryAgent, openHiveDatabase, } from "../../storage/db.js";
8
+ import fetch from "node-fetch";
9
+ import { RUNTIME_SYSTEM_GUARDRAILS, buildBrowserAugmentedPrompt, HiveAgent, } from "../../agent/agent.js";
10
+ import { closeHiveDatabase, deleteKnowledge, findClosestKnowledge, getPrimaryAgent, getHiveHomeDir, insertKnowledge, listKnowledge, listConversationMessages, listRecentConversations, openHiveDatabase, updateConversationTitle, clearEpisodes, } from "../../storage/db.js";
7
11
  import { createProvider } from "../../providers/index.js";
8
- import { renderError, renderHiveHeader, renderInfo, renderSeparator, } from "../ui.js";
12
+ import { renderError, renderHiveHeader, renderInfo, renderSeparator, renderSuccess, } from "../ui.js";
13
+ import { openPage } from "../../browser/browser.js";
9
14
  import { runConfigKeyCommandWithOptions, runConfigModelCommandWithOptions, runConfigProviderCommandWithOptions, runConfigShowCommandWithOptions, runConfigThemeCommandWithOptions, } from "./config.js";
10
15
  import { runStatusCommandWithOptions } from "./status.js";
11
16
  import { getTheme } from "../theme.js";
@@ -18,6 +23,24 @@ const COMMAND_HELP_TEXT = [
18
23
  "Commands:",
19
24
  " /help show commands",
20
25
  " /new start a new conversation",
26
+ " /remember <fact> save a fact",
27
+ " /forget <thing> delete closest fact",
28
+ " /pin <fact> pin fact into context",
29
+ " /summarize <url> summarize a web page",
30
+ " /tldr summarize this conversation",
31
+ " /recap summarize persona + knowledge",
32
+ " /mode <name> switch response mode",
33
+ " /status show mode/provider/model",
34
+ " /export export conversation markdown",
35
+ " /save <title> name this conversation",
36
+ " /history list recent conversations",
37
+ " /clear clear the screen",
38
+ " /think <question>think step by step",
39
+ " /retry resend last message",
40
+ " /copy copy last reply",
41
+ " /hive memory list show knowledge items",
42
+ " /hive memory clear clear episodic memory",
43
+ " /hive memory show show current persona",
21
44
  " /browse <url> read a webpage",
22
45
  " browse <url> same as /browse",
23
46
  " /search <query> search the web",
@@ -36,6 +59,9 @@ const HIVE_SHORTCUT_HELP_TEXT = [
36
59
  " /hive help list shortcuts",
37
60
  " /hive status run hive status",
38
61
  " /hive config show run hive config show",
62
+ " /hive memory list list knowledge",
63
+ " /hive memory clear clear episodes",
64
+ " /hive memory show show persona",
39
65
  "",
40
66
  "Interactive config commands (in chat):",
41
67
  " /hive config provider",
@@ -53,6 +79,13 @@ const PREVIEW_AGENT_NAME = "jarvis";
53
79
  const PREVIEW_PROVIDER = "google";
54
80
  const PREVIEW_MODEL = "gemini-2.0-flash";
55
81
  const PREVIEW_NEW_MESSAGE = "Started a new preview conversation context.";
82
+ const MODE_PROMPTS = {
83
+ default: null,
84
+ research: "Every answer must be grounded in current web evidence. Perform web search as needed and cite sources inline.",
85
+ code: "Think and respond like a focused software engineer. Prioritize concise technical answers and code.",
86
+ brainstorm: "Be creative and opinionated. Offer bold suggestions and push back on weak ideas when helpful.",
87
+ brief: "Keep every response to a maximum of 3 sentences while preserving key details.",
88
+ };
56
89
  const COMMAND_SUGGESTIONS = [
57
90
  {
58
91
  label: "/help",
@@ -64,6 +97,81 @@ const COMMAND_SUGGESTIONS = [
64
97
  insertText: "/new",
65
98
  description: "start a new conversation",
66
99
  },
100
+ {
101
+ label: "/remember <fact>",
102
+ insertText: "/remember ",
103
+ description: "save to knowledge graph",
104
+ },
105
+ {
106
+ label: "/pin <fact>",
107
+ insertText: "/pin ",
108
+ description: "pin fact into context",
109
+ },
110
+ {
111
+ label: "/forget <thing>",
112
+ insertText: "/forget ",
113
+ description: "delete closest fact",
114
+ },
115
+ {
116
+ label: "/summarize <url>",
117
+ insertText: "/summarize ",
118
+ description: "summarize a web page",
119
+ },
120
+ {
121
+ label: "/tldr",
122
+ insertText: "/tldr",
123
+ description: "summarize this conversation",
124
+ },
125
+ {
126
+ label: "/recap",
127
+ insertText: "/recap",
128
+ description: "summarize persona & knowledge",
129
+ },
130
+ {
131
+ label: "/mode <name>",
132
+ insertText: "/mode ",
133
+ description: "switch response style",
134
+ },
135
+ {
136
+ label: "/export",
137
+ insertText: "/export",
138
+ description: "export conversation markdown",
139
+ },
140
+ {
141
+ label: "/save <title>",
142
+ insertText: "/save ",
143
+ description: "set conversation title",
144
+ },
145
+ {
146
+ label: "/history",
147
+ insertText: "/history",
148
+ description: "list recent conversations",
149
+ },
150
+ {
151
+ label: "/status",
152
+ insertText: "/status",
153
+ description: "show session status",
154
+ },
155
+ {
156
+ label: "/clear",
157
+ insertText: "/clear",
158
+ description: "clear the screen",
159
+ },
160
+ {
161
+ label: "/think <question>",
162
+ insertText: "/think ",
163
+ description: "think step by step",
164
+ },
165
+ {
166
+ label: "/retry",
167
+ insertText: "/retry",
168
+ description: "resend last message",
169
+ },
170
+ {
171
+ label: "/copy",
172
+ insertText: "/copy",
173
+ description: "copy last reply",
174
+ },
67
175
  {
68
176
  label: "/browse <url>",
69
177
  insertText: "/browse ",
@@ -94,6 +202,21 @@ const COMMAND_SUGGESTIONS = [
94
202
  insertText: "/hive config show",
95
203
  description: "run hive config show",
96
204
  },
205
+ {
206
+ label: "/hive memory list",
207
+ insertText: "/hive memory list",
208
+ description: "list knowledge entries",
209
+ },
210
+ {
211
+ label: "/hive memory clear",
212
+ insertText: "/hive memory clear",
213
+ description: "clear episodic memory",
214
+ },
215
+ {
216
+ label: "/hive memory show",
217
+ insertText: "/hive memory show",
218
+ description: "show current persona",
219
+ },
97
220
  {
98
221
  label: "/hive init",
99
222
  insertText: "/hive init",
@@ -142,6 +265,7 @@ export function registerChatCommand(program) {
142
265
  export async function runChatCommand(options, context = {}) {
143
266
  console.clear();
144
267
  renderHiveHeader("Chat");
268
+ void checkForUpdates();
145
269
  const entrypoint = context.entrypoint ?? "chat-command";
146
270
  if (entrypoint === "chat-command") {
147
271
  renderInfo("`hive chat` is deprecated. Run `hive`.");
@@ -162,6 +286,7 @@ export async function runChatCommand(options, context = {}) {
162
286
  let provider = await createProvider(activeProfile.provider);
163
287
  let agent = new HiveAgent(db, provider, activeProfile);
164
288
  let agentName = resolveAgentName(activeProfile.agent_name);
289
+ let currentMode = "default";
165
290
  const model = options.model ?? activeProfile.model;
166
291
  let conversationId = options.conversation;
167
292
  const runOptions = {
@@ -169,6 +294,8 @@ export async function runChatCommand(options, context = {}) {
169
294
  title: options.title,
170
295
  temperature,
171
296
  };
297
+ const lastUserPromptRef = { value: null };
298
+ const lastAssistantRef = { value: "" };
172
299
  renderChatPreamble({
173
300
  agentName,
174
301
  provider: profile.provider,
@@ -178,7 +305,11 @@ export async function runChatCommand(options, context = {}) {
178
305
  const augmentedMessage = await buildBrowserAugmentedPrompt(options.message, {
179
306
  locationHint: profile.location ?? undefined,
180
307
  });
181
- conversationId = await streamReply(agent, augmentedMessage, conversationId, runOptions, agentName);
308
+ const systemAddition = getModeSystemPrompt(currentMode);
309
+ lastUserPromptRef.value = options.message;
310
+ const streamResult = await streamReply(agent, augmentedMessage, conversationId, runOptions, agentName, systemAddition);
311
+ conversationId = streamResult.conversationId;
312
+ lastAssistantRef.value = streamResult.assistantText;
182
313
  renderInfo(`conversation: ${conversationId}`);
183
314
  return;
184
315
  }
@@ -187,25 +318,53 @@ export async function runChatCommand(options, context = {}) {
187
318
  if (prompt.length === 0) {
188
319
  continue;
189
320
  }
321
+ const normalizedPrompt = prompt.trim().toLowerCase();
190
322
  if (prompt === "/") {
191
323
  printChatHelp();
192
324
  continue;
193
325
  }
194
- if (prompt === "/help") {
326
+ if (normalizedPrompt === "/help") {
195
327
  printChatHelp();
196
328
  continue;
197
329
  }
198
- if (prompt === "/exit" || prompt === "/quit") {
330
+ if (normalizedPrompt === "/exit" || normalizedPrompt === "/quit") {
199
331
  break;
200
332
  }
201
- if (prompt === "/new") {
333
+ if (normalizedPrompt === "/new") {
202
334
  conversationId = undefined;
335
+ currentMode = "default";
336
+ lastUserPromptRef.value = null;
337
+ lastAssistantRef.value = "";
203
338
  renderInfo("Started a new conversation context.");
204
339
  continue;
205
340
  }
206
341
  try {
342
+ const handled = await handleChatSlashCommand({
343
+ prompt,
344
+ db,
345
+ agent,
346
+ provider,
347
+ agentName,
348
+ conversationId,
349
+ activeProfilePersona: activeProfile.persona,
350
+ mode: currentMode,
351
+ providerName: activeProfile.provider,
352
+ modelName: runOptions.model ?? activeProfile.model,
353
+ setConversationId: (id) => {
354
+ conversationId = id;
355
+ },
356
+ setMode: (mode) => {
357
+ currentMode = mode;
358
+ },
359
+ lastUserPromptRef,
360
+ lastAssistantRef,
361
+ });
362
+ if (handled) {
363
+ continue;
364
+ }
207
365
  const shortcutResult = await handleHiveShortcut(prompt, {
208
366
  allowInteractiveConfig: true,
367
+ db,
209
368
  });
210
369
  if (shortcutResult === "handled") {
211
370
  continue;
@@ -229,14 +388,17 @@ export async function runChatCommand(options, context = {}) {
229
388
  continue;
230
389
  }
231
390
  if (isUnknownSlashCommand(prompt)) {
232
- renderError(`Unknown command: ${prompt}`);
233
- renderInfo("Run `/help` to view supported commands.");
391
+ renderError("✗ Unknown command. Type /help for available commands.");
234
392
  continue;
235
393
  }
236
394
  const augmentedPrompt = await buildBrowserAugmentedPrompt(prompt, {
237
395
  locationHint: profile.location ?? undefined,
238
396
  });
239
- conversationId = await streamReply(agent, augmentedPrompt, conversationId, runOptions, agentName);
397
+ lastUserPromptRef.value = prompt;
398
+ const systemAddition = getModeSystemPrompt(currentMode);
399
+ const streamResult = await streamReply(agent, augmentedPrompt, conversationId, runOptions, agentName, systemAddition);
400
+ conversationId = streamResult.conversationId;
401
+ lastAssistantRef.value = streamResult.assistantText;
240
402
  }
241
403
  catch (error) {
242
404
  renderError(formatError(error));
@@ -247,18 +409,21 @@ export async function runChatCommand(options, context = {}) {
247
409
  closeHiveDatabase(db);
248
410
  }
249
411
  }
250
- async function streamReply(agent, prompt, conversationId, options, agentName) {
412
+ async function streamReply(agent, prompt, conversationId, options, agentName, systemAddition) {
251
413
  process.stdout.write(getTheme().accent(`${agentName}${PROMPT_SYMBOL} `));
252
414
  let activeConversationId = conversationId;
415
+ let assistantText = "";
253
416
  for await (const event of agent.chat(prompt, {
254
417
  conversationId: activeConversationId,
255
418
  model: options.model,
256
419
  temperature: options.temperature,
257
420
  title: options.title,
421
+ systemAddition,
258
422
  })) {
259
423
  if (event.type === "token") {
260
424
  process.stdout.write(event.token);
261
425
  activeConversationId = event.conversationId;
426
+ assistantText += event.token;
262
427
  continue;
263
428
  }
264
429
  activeConversationId = event.conversationId;
@@ -268,7 +433,7 @@ async function streamReply(agent, prompt, conversationId, options, agentName) {
268
433
  if (!activeConversationId) {
269
434
  throw new Error("Conversation state was not returned by the agent.");
270
435
  }
271
- return activeConversationId;
436
+ return { conversationId: activeConversationId, assistantText };
272
437
  }
273
438
  function parseTemperature(raw) {
274
439
  if (raw === undefined) {
@@ -355,6 +520,16 @@ function isHiveShortcut(prompt) {
355
520
  const normalized = prompt.trim().toLowerCase();
356
521
  return normalized === HIVE_SHORTCUT_PREFIX || normalized.startsWith(`${HIVE_SHORTCUT_PREFIX} `);
357
522
  }
523
+ function getModeSystemPrompt(mode) {
524
+ return MODE_PROMPTS[mode] ?? undefined;
525
+ }
526
+ function combineSystemAdditions(parts) {
527
+ const merged = parts
528
+ .map((part) => part?.trim() ?? "")
529
+ .filter((part) => part.length > 0)
530
+ .join("\n\n");
531
+ return merged.length > 0 ? merged : undefined;
532
+ }
358
533
  function isUnknownSlashCommand(prompt) {
359
534
  const normalized = prompt.trim().toLowerCase();
360
535
  if (!normalized.startsWith("/")) {
@@ -364,6 +539,26 @@ function isUnknownSlashCommand(prompt) {
364
539
  normalized === "/new" ||
365
540
  normalized === "/exit" ||
366
541
  normalized === "/quit" ||
542
+ normalized === "/remember" ||
543
+ normalized.startsWith("/remember ") ||
544
+ normalized === "/forget" ||
545
+ normalized.startsWith("/forget ") ||
546
+ normalized === "/summarize" ||
547
+ normalized.startsWith("/summarize ") ||
548
+ normalized === "/tldr" ||
549
+ normalized === "/recap" ||
550
+ normalized === "/mode" ||
551
+ normalized.startsWith("/mode ") ||
552
+ normalized === "/export" ||
553
+ normalized === "/history" ||
554
+ normalized === "/clear" ||
555
+ normalized === "/think" ||
556
+ normalized.startsWith("/think ") ||
557
+ normalized.startsWith("/save") ||
558
+ normalized.startsWith("/pin") ||
559
+ normalized === "/status" ||
560
+ normalized === "/retry" ||
561
+ normalized === "/copy" ||
367
562
  normalized === "/browse" ||
368
563
  normalized.startsWith("/browse ") ||
369
564
  normalized === "/search" ||
@@ -400,6 +595,52 @@ async function handleHiveShortcut(prompt, options = {}) {
400
595
  restoreChatInputAfterInteractiveCommand();
401
596
  return "handled";
402
597
  }
598
+ if (subcommand === "memory list") {
599
+ const db = options.db;
600
+ if (!db) {
601
+ renderError("Memory commands unavailable: database not open.");
602
+ return "handled";
603
+ }
604
+ const rows = listKnowledge(db, { limit: 1000 });
605
+ if (rows.length === 0) {
606
+ renderInfo("No knowledge stored.");
607
+ return "handled";
608
+ }
609
+ rows.forEach((row, index) => {
610
+ const pinnedLabel = row.pinned ? " (pinned)" : "";
611
+ renderInfo(`${index + 1}. ${row.content}${pinnedLabel}`);
612
+ });
613
+ return "handled";
614
+ }
615
+ if (subcommand === "memory clear") {
616
+ const db = options.db;
617
+ if (!db) {
618
+ renderError("Memory commands unavailable: database not open.");
619
+ return "handled";
620
+ }
621
+ const confirmed = await promptYesNo("This will delete all episodic memories. Continue? (y/n) ");
622
+ if (!confirmed) {
623
+ renderInfo("Cancelled.");
624
+ return "handled";
625
+ }
626
+ clearEpisodes(db);
627
+ renderSuccess("Episodes cleared.");
628
+ return "handled";
629
+ }
630
+ if (subcommand === "memory show") {
631
+ const db = options.db;
632
+ if (!db) {
633
+ renderError("Memory commands unavailable: database not open.");
634
+ return "handled";
635
+ }
636
+ const agent = getPrimaryAgent(db);
637
+ if (!agent) {
638
+ renderError("Hive is not initialized. Run `hive init` first.");
639
+ return "handled";
640
+ }
641
+ renderInfo(agent.persona);
642
+ return "handled";
643
+ }
403
644
  if (subcommand === "config provider") {
404
645
  if (!options.allowInteractiveConfig) {
405
646
  renderInfo("Interactive config commands are unavailable here.");
@@ -445,6 +686,316 @@ async function handleHiveShortcut(prompt, options = {}) {
445
686
  renderInfo("Use `/hive help` to list available shortcuts.");
446
687
  return "handled";
447
688
  }
689
+ async function handleChatSlashCommand(input) {
690
+ const normalized = input.prompt.trim();
691
+ const lower = normalized.toLowerCase();
692
+ if (!lower.startsWith("/")) {
693
+ return false;
694
+ }
695
+ if (lower === "/clear") {
696
+ console.clear();
697
+ renderHiveHeader("Chat");
698
+ renderChatPreamble({
699
+ agentName: input.agentName,
700
+ provider: input.providerName,
701
+ model: input.modelName,
702
+ });
703
+ input.lastUserPromptRef.value = null;
704
+ input.lastAssistantRef.value = "";
705
+ return true;
706
+ }
707
+ if (lower.startsWith("/remember")) {
708
+ const fact = normalized.slice("/remember".length).trim();
709
+ if (fact.length === 0) {
710
+ renderError("Usage: /remember <fact>");
711
+ return true;
712
+ }
713
+ insertKnowledge(input.db, { content: fact });
714
+ renderSuccess("✓ Remembered.");
715
+ input.lastUserPromptRef.value = null;
716
+ return true;
717
+ }
718
+ if (lower.startsWith("/forget")) {
719
+ const query = normalized.slice("/forget".length).trim();
720
+ if (query.length === 0) {
721
+ renderError("Usage: /forget <thing>");
722
+ return true;
723
+ }
724
+ const match = findClosestKnowledge(input.db, query);
725
+ if (!match) {
726
+ renderError("No similar knowledge found.");
727
+ return true;
728
+ }
729
+ const confirmed = await promptYesNo(`Forget "${match.content}"? (y/n) `);
730
+ if (!confirmed) {
731
+ renderInfo("Kept.");
732
+ return true;
733
+ }
734
+ deleteKnowledge(input.db, match.id);
735
+ renderSuccess("✓ Forgotten.");
736
+ input.lastUserPromptRef.value = null;
737
+ return true;
738
+ }
739
+ if (lower.startsWith("/mode")) {
740
+ const modeName = normalized.slice("/mode".length).trim().toLowerCase();
741
+ if (!modeName || !Object.hasOwn(MODE_PROMPTS, modeName)) {
742
+ renderError("Usage: /mode <default|research|code|brainstorm|brief>");
743
+ return true;
744
+ }
745
+ input.setMode(modeName);
746
+ renderSuccess(`✓ Mode set to ${modeName}.`);
747
+ input.lastUserPromptRef.value = null;
748
+ return true;
749
+ }
750
+ if (lower.startsWith("/pin")) {
751
+ const fact = normalized.slice("/pin".length).trim();
752
+ if (fact.length === 0) {
753
+ renderError("Usage: /pin <fact>");
754
+ return true;
755
+ }
756
+ insertKnowledge(input.db, { content: fact, pinned: true });
757
+ renderSuccess("✓ Pinned.");
758
+ input.lastUserPromptRef.value = null;
759
+ return true;
760
+ }
761
+ if (lower === "/export") {
762
+ if (!input.conversationId) {
763
+ renderError("No conversation to export. Start chatting first.");
764
+ return true;
765
+ }
766
+ const messages = listConversationMessages(input.db, input.conversationId);
767
+ const exportDir = join(getHiveHomeDir(), "exports");
768
+ mkdirSync(exportDir, { recursive: true });
769
+ const exportPath = join(exportDir, `${input.conversationId}.md`);
770
+ writeFileSync(exportPath, formatConversationMarkdown(messages, input.conversationId));
771
+ renderSuccess(`✓ Exported to ${exportPath}`);
772
+ input.lastUserPromptRef.value = null;
773
+ return true;
774
+ }
775
+ if (lower === "/history") {
776
+ const rows = listRecentConversations(input.db, 10);
777
+ if (rows.length === 0) {
778
+ renderInfo("No past conversations found.");
779
+ return true;
780
+ }
781
+ rows.forEach((row, index) => {
782
+ const title = row.title?.trim().length ? row.title : "(untitled)";
783
+ renderInfo(`${index + 1}. ${title} · ${row.updated_at} · ${row.message_count} messages`);
784
+ });
785
+ const answer = await promptLine("Pick a conversation number (or blank to cancel): ");
786
+ if (!answer) {
787
+ return true;
788
+ }
789
+ const choice = Number.parseInt(answer, 10);
790
+ if (Number.isNaN(choice) || choice < 1 || choice > rows.length) {
791
+ renderError("Invalid selection.");
792
+ return true;
793
+ }
794
+ const selected = rows[choice - 1];
795
+ input.setConversationId(selected.id);
796
+ renderInfo(`Continuing conversation ${selected.title ?? selected.id}.`);
797
+ input.lastUserPromptRef.value = null;
798
+ input.lastAssistantRef.value = "";
799
+ return true;
800
+ }
801
+ if (lower === "/tldr") {
802
+ if (!input.conversationId) {
803
+ renderError("No conversation yet. Say something first.");
804
+ return true;
805
+ }
806
+ const history = listConversationMessages(input.db, input.conversationId);
807
+ if (history.length === 0) {
808
+ renderError("Nothing to summarize yet.");
809
+ return true;
810
+ }
811
+ await streamEphemeral({
812
+ provider: input.provider,
813
+ agentName: input.agentName,
814
+ model: input.modelName,
815
+ messages: buildEphemeralMessages({
816
+ persona: input.activeProfilePersona,
817
+ mode: input.mode,
818
+ systemInstruction: "Summarize this conversation in 3-5 bullet points.",
819
+ history,
820
+ }),
821
+ });
822
+ input.lastUserPromptRef.value = null;
823
+ return true;
824
+ }
825
+ if (lower === "/recap") {
826
+ const knowledge = listKnowledge(input.db, { limit: 500 });
827
+ await streamEphemeral({
828
+ provider: input.provider,
829
+ agentName: input.agentName,
830
+ model: input.modelName,
831
+ messages: buildRecapMessages({
832
+ persona: input.activeProfilePersona,
833
+ knowledge,
834
+ mode: input.mode,
835
+ }),
836
+ });
837
+ input.lastUserPromptRef.value = null;
838
+ return true;
839
+ }
840
+ if (lower.startsWith("/summarize")) {
841
+ const url = normalized.slice("/summarize".length).trim();
842
+ if (url.length === 0) {
843
+ renderError("Usage: /summarize <url>");
844
+ return true;
845
+ }
846
+ try {
847
+ const content = await openPage(url);
848
+ await streamEphemeral({
849
+ provider: input.provider,
850
+ agentName: input.agentName,
851
+ model: input.modelName,
852
+ messages: buildEphemeralMessages({
853
+ persona: input.activeProfilePersona,
854
+ mode: input.mode,
855
+ userMessage: `Summarize this page concisely:\n\n${content}`,
856
+ }),
857
+ });
858
+ }
859
+ catch (error) {
860
+ renderError(`Unable to summarize page: ${formatError(error)}`);
861
+ }
862
+ return true;
863
+ }
864
+ if (lower.startsWith("/save")) {
865
+ const title = normalized.slice("/save".length).trim();
866
+ if (!input.conversationId) {
867
+ renderError("No active conversation to title.");
868
+ return true;
869
+ }
870
+ if (title.length === 0) {
871
+ renderError("Usage: /save <title>");
872
+ return true;
873
+ }
874
+ updateConversationTitle(input.db, input.conversationId, title);
875
+ renderSuccess(`✓ Saved title "${title}".`);
876
+ input.lastUserPromptRef.value = null;
877
+ return true;
878
+ }
879
+ if (lower.startsWith("/think")) {
880
+ const question = normalized.slice("/think".length).trim();
881
+ if (question.length === 0) {
882
+ renderError("Usage: /think <question>");
883
+ return true;
884
+ }
885
+ await streamEphemeral({
886
+ provider: input.provider,
887
+ agentName: input.agentName,
888
+ model: input.modelName,
889
+ messages: buildEphemeralMessages({
890
+ persona: input.activeProfilePersona,
891
+ mode: input.mode,
892
+ userMessage: `Think through this step by step, show your reasoning:\n\n${question}`,
893
+ }),
894
+ });
895
+ return true;
896
+ }
897
+ if (lower === "/status") {
898
+ const info = [
899
+ `mode=${input.mode}`,
900
+ `provider=${input.providerName}`,
901
+ `model=${input.modelName}`,
902
+ `conversation=${input.conversationId ?? "none"}`,
903
+ ].join(" · ");
904
+ renderInfo(info);
905
+ return true;
906
+ }
907
+ if (lower === "/retry") {
908
+ const userPrompt = input.lastUserPromptRef.value;
909
+ if (!userPrompt) {
910
+ renderError("Nothing to retry yet.");
911
+ return true;
912
+ }
913
+ const systemAddition = getModeSystemPrompt(input.mode);
914
+ const retryResult = await streamReply(input.agent, userPrompt, input.conversationId, { model: input.modelName }, input.agentName, systemAddition);
915
+ input.setConversationId(retryResult.conversationId);
916
+ input.lastAssistantRef.value = retryResult.assistantText;
917
+ return true;
918
+ }
919
+ if (lower === "/copy") {
920
+ if (!input.lastAssistantRef.value) {
921
+ renderError("Nothing to copy yet.");
922
+ return true;
923
+ }
924
+ const copied = copyToClipboard(input.lastAssistantRef.value);
925
+ if (copied) {
926
+ renderSuccess("✓ Copied last reply to clipboard.");
927
+ }
928
+ else {
929
+ renderError("Clipboard tool not available.");
930
+ }
931
+ return true;
932
+ }
933
+ return false;
934
+ }
935
+ function copyToClipboard(text) {
936
+ const platform = process.platform;
937
+ const buffer = Buffer.from(text, "utf8");
938
+ if (platform === "darwin") {
939
+ const result = spawnSync("pbcopy", [], { input: buffer });
940
+ return result.status === 0;
941
+ }
942
+ const result = spawnSync("xclip", ["-selection", "clipboard"], { input: buffer });
943
+ return result.status === 0;
944
+ }
945
+ let cachedLocalVersion = null;
946
+ async function checkForUpdates() {
947
+ try {
948
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3000));
949
+ const latest = (await Promise.race([
950
+ fetch("https://registry.npmjs.org/@imisbahk/hive/latest").then((response) => response.json()),
951
+ timeout,
952
+ ]));
953
+ if (!latest?.version || typeof latest.version !== "string") {
954
+ return;
955
+ }
956
+ const localVersion = getLocalVersion();
957
+ if (isVersionNewer(latest.version, localVersion)) {
958
+ const amber = chalk.hex("#ffbf00");
959
+ console.log(amber.dim(`✦ Update available v${localVersion} → npm update -g @imisbahk/hive`));
960
+ }
961
+ }
962
+ catch {
963
+ // Silently ignore update check failures.
964
+ }
965
+ }
966
+ function getLocalVersion() {
967
+ if (cachedLocalVersion) {
968
+ return cachedLocalVersion;
969
+ }
970
+ try {
971
+ const raw = readFileSync(new URL("../../../package.json", import.meta.url), "utf8");
972
+ const parsed = JSON.parse(raw);
973
+ if (parsed.version) {
974
+ cachedLocalVersion = parsed.version;
975
+ return cachedLocalVersion;
976
+ }
977
+ }
978
+ catch {
979
+ // ignore
980
+ }
981
+ cachedLocalVersion = "0.0.0";
982
+ return cachedLocalVersion;
983
+ }
984
+ function isVersionNewer(remote, local) {
985
+ const toNumbers = (value) => value.split(".").map((part) => Number.parseInt(part, 10));
986
+ const r = toNumbers(remote);
987
+ const l = toNumbers(local);
988
+ const length = Math.max(r.length, l.length);
989
+ for (let index = 0; index < length; index += 1) {
990
+ const rv = r[index] ?? 0;
991
+ const lv = l[index] ?? 0;
992
+ if (rv > lv)
993
+ return true;
994
+ if (rv < lv)
995
+ return false;
996
+ }
997
+ return false;
998
+ }
448
999
  function getCommandSuggestions(input) {
449
1000
  const normalized = input.trimStart().toLowerCase();
450
1001
  if (!normalized.startsWith("/")) {
@@ -456,6 +1007,85 @@ function getCommandSuggestions(input) {
456
1007
  suggestion.label.toLowerCase().includes(normalized.slice(1)));
457
1008
  return [...prefixMatches, ...fallbackMatches];
458
1009
  }
1010
+ async function promptLine(question) {
1011
+ const rl = createInterface({
1012
+ input: stdin,
1013
+ output: stdout,
1014
+ terminal: true,
1015
+ });
1016
+ try {
1017
+ return (await rl.question(question)).trim();
1018
+ }
1019
+ finally {
1020
+ rl.close();
1021
+ }
1022
+ }
1023
+ async function promptYesNo(question) {
1024
+ const answer = (await promptLine(question)).toLowerCase();
1025
+ return answer === "y" || answer === "yes";
1026
+ }
1027
+ async function streamEphemeral(input) {
1028
+ process.stdout.write(getTheme().accent(`${input.agentName}${PROMPT_SYMBOL} `));
1029
+ let hadOutput = false;
1030
+ for await (const token of input.provider.streamChat({
1031
+ model: input.model ?? input.provider.defaultModel,
1032
+ messages: input.messages,
1033
+ })) {
1034
+ hadOutput = true;
1035
+ process.stdout.write(token);
1036
+ }
1037
+ if (!hadOutput) {
1038
+ process.stdout.write("(no response)");
1039
+ }
1040
+ process.stdout.write("\n");
1041
+ renderSeparator(EXCHANGE_SEPARATOR);
1042
+ }
1043
+ function buildEphemeralMessages(input) {
1044
+ const messages = [
1045
+ { role: "system", content: RUNTIME_SYSTEM_GUARDRAILS },
1046
+ { role: "system", content: input.persona },
1047
+ ];
1048
+ const modePrompt = getModeSystemPrompt(input.mode);
1049
+ if (modePrompt) {
1050
+ messages.push({ role: "system", content: modePrompt });
1051
+ }
1052
+ if (input.systemInstruction) {
1053
+ messages.push({ role: "system", content: input.systemInstruction });
1054
+ }
1055
+ if (input.history) {
1056
+ messages.push(...input.history.map((message) => ({
1057
+ role: message.role,
1058
+ content: message.content,
1059
+ })));
1060
+ }
1061
+ if (input.userMessage) {
1062
+ messages.push({ role: "user", content: input.userMessage });
1063
+ }
1064
+ return messages;
1065
+ }
1066
+ function buildRecapMessages(input) {
1067
+ const knowledgeLines = input.knowledge.length > 0
1068
+ ? input.knowledge.map((row) => `- ${row.content}`).join("\n")
1069
+ : "No knowledge stored yet.";
1070
+ const userMessage = `Summarize everything you know about the user based on persona and knowledge facts below. Be concise.\n\nPersona:\n${input.persona}\n\nKnowledge facts:\n${knowledgeLines}`;
1071
+ return buildEphemeralMessages({
1072
+ persona: input.persona,
1073
+ mode: input.mode,
1074
+ userMessage,
1075
+ });
1076
+ }
1077
+ function formatConversationMarkdown(messages, conversationId) {
1078
+ const lines = [`# Conversation ${conversationId}`, ""];
1079
+ for (const message of messages) {
1080
+ const speaker = message.role === "user"
1081
+ ? "User"
1082
+ : message.role === "assistant"
1083
+ ? "Hive"
1084
+ : "System";
1085
+ lines.push(`**${speaker}:**`, message.content, "");
1086
+ }
1087
+ return lines.join("\n");
1088
+ }
459
1089
  async function readPromptWithSuggestions() {
460
1090
  const accent = getTheme().accent;
461
1091
  const promptPrefix = accent(USER_PROMPT);