@imisbahk/hive 0.1.3 → 0.1.6

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 (115) hide show
  1. package/.gitattributes +7 -0
  2. package/.rocket/README.md +22 -22
  3. package/dist/agent/agent.d.ts +7 -1
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +139 -27
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/agent/hive-ctx.d.ts +23 -0
  8. package/dist/agent/hive-ctx.d.ts.map +1 -0
  9. package/dist/agent/hive-ctx.js +68 -0
  10. package/dist/agent/hive-ctx.js.map +1 -0
  11. package/dist/agent/prompts.d.ts +15 -0
  12. package/dist/agent/prompts.d.ts.map +1 -0
  13. package/dist/agent/prompts.js +153 -0
  14. package/dist/agent/prompts.js.map +1 -0
  15. package/dist/cli/commands/chat.d.ts.map +1 -1
  16. package/dist/cli/commands/chat.js +788 -29
  17. package/dist/cli/commands/chat.js.map +1 -1
  18. package/dist/cli/commands/init.d.ts.map +1 -1
  19. package/dist/cli/commands/init.js +93 -27
  20. package/dist/cli/commands/init.js.map +1 -1
  21. package/dist/cli/commands/memory.d.ts +3 -0
  22. package/dist/cli/commands/memory.d.ts.map +1 -0
  23. package/dist/cli/commands/memory.js +104 -0
  24. package/dist/cli/commands/memory.js.map +1 -0
  25. package/dist/cli/index.js +3 -1
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/providers/anthropic.d.ts +1 -0
  28. package/dist/providers/anthropic.d.ts.map +1 -1
  29. package/dist/providers/anthropic.js +1 -0
  30. package/dist/providers/anthropic.js.map +1 -1
  31. package/dist/providers/api-key.js +1 -1
  32. package/dist/providers/api-key.js.map +1 -1
  33. package/dist/providers/base.d.ts +1 -0
  34. package/dist/providers/base.d.ts.map +1 -1
  35. package/dist/providers/base.js +1 -1
  36. package/dist/providers/base.js.map +1 -1
  37. package/dist/providers/groq.d.ts.map +1 -1
  38. package/dist/providers/groq.js +1 -0
  39. package/dist/providers/groq.js.map +1 -1
  40. package/dist/providers/index.d.ts +2 -0
  41. package/dist/providers/index.d.ts.map +1 -1
  42. package/dist/providers/index.js +41 -8
  43. package/dist/providers/index.js.map +1 -1
  44. package/dist/providers/ollama.d.ts.map +1 -1
  45. package/dist/providers/ollama.js +1 -0
  46. package/dist/providers/ollama.js.map +1 -1
  47. package/dist/providers/openai-compatible.d.ts +2 -0
  48. package/dist/providers/openai-compatible.d.ts.map +1 -1
  49. package/dist/providers/openai-compatible.js +5 -3
  50. package/dist/providers/openai-compatible.js.map +1 -1
  51. package/dist/providers/resilience.d.ts +4 -0
  52. package/dist/providers/resilience.d.ts.map +1 -0
  53. package/dist/providers/resilience.js +38 -0
  54. package/dist/providers/resilience.js.map +1 -0
  55. package/dist/storage/db.d.ts +31 -2
  56. package/dist/storage/db.d.ts.map +1 -1
  57. package/dist/storage/db.js +165 -3
  58. package/dist/storage/db.js.map +1 -1
  59. package/dist/storage/schema.d.ts +12 -1
  60. package/dist/storage/schema.d.ts.map +1 -1
  61. package/dist/storage/schema.js +29 -1
  62. package/dist/storage/schema.js.map +1 -1
  63. package/master +0 -0
  64. package/package.json +9 -2
  65. package/.github/workflows/publish.yml +0 -31
  66. package/.rocket/ARCHITECTURE.md +0 -7
  67. package/.rocket/SYMBOLS.md +0 -425
  68. package/001-local-first-storage.md +0 -43
  69. package/003-memory-architechture.md +0 -71
  70. package/CONTRIBUTING.md +0 -150
  71. package/FEATURES.md +0 -63
  72. package/index.md +0 -16
  73. package/prompts/Behaviour.md +0 -23
  74. package/prompts/Browser.md +0 -13
  75. package/prompts/Code.md +0 -12
  76. package/prompts/Debugging.md +0 -15
  77. package/prompts/Execution.md +0 -13
  78. package/prompts/Memory.md +0 -11
  79. package/prompts/Planning.md +0 -13
  80. package/prompts/Product.md +0 -14
  81. package/prompts/Review.md +0 -15
  82. package/prompts/Safety.md +0 -12
  83. package/prompts/Search.md +0 -14
  84. package/prompts/System.md +0 -6
  85. package/prompts/Tools.md +0 -14
  86. package/prompts/Writing.md +0 -13
  87. package/releases/v1/v0.1/RELEASE-NOTES.md +0 -101
  88. package/src/agent/agent.ts +0 -595
  89. package/src/agent/index.ts +0 -2
  90. package/src/browser/browser.ts +0 -410
  91. package/src/cli/commands/chat.ts +0 -864
  92. package/src/cli/commands/config.ts +0 -610
  93. package/src/cli/commands/doctor.ts +0 -655
  94. package/src/cli/commands/init.ts +0 -288
  95. package/src/cli/commands/nuke.ts +0 -64
  96. package/src/cli/commands/status.ts +0 -170
  97. package/src/cli/helpers/providerPrompts.ts +0 -192
  98. package/src/cli/index.ts +0 -68
  99. package/src/cli/theme.ts +0 -88
  100. package/src/cli/ui.ts +0 -127
  101. package/src/providers/anthropic.ts +0 -146
  102. package/src/providers/api-key.ts +0 -23
  103. package/src/providers/base.ts +0 -409
  104. package/src/providers/google.ts +0 -21
  105. package/src/providers/groq.ts +0 -21
  106. package/src/providers/index.ts +0 -65
  107. package/src/providers/mistral.ts +0 -21
  108. package/src/providers/ollama.ts +0 -22
  109. package/src/providers/openai-compatible.ts +0 -82
  110. package/src/providers/openai.ts +0 -21
  111. package/src/providers/openrouter.ts +0 -21
  112. package/src/providers/together.ts +0 -21
  113. package/src/storage/db.ts +0 -476
  114. package/src/storage/schema.ts +0 -116
  115. package/tsconfig.json +0 -51
@@ -1,11 +1,17 @@
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";
7
- import { createProvider } from "../../providers/index.js";
8
- import { renderError, renderHiveHeader, renderInfo, renderSeparator, } from "../ui.js";
8
+ import fetch from "node-fetch";
9
+ import { RUNTIME_SYSTEM_GUARDRAILS, buildBrowserAugmentedPrompt, HiveAgent, } from "../../agent/agent.js";
10
+ import { initializeHiveCtxSession, } from "../../agent/hive-ctx.js";
11
+ import { closeHiveDatabase, deleteKnowledge, findClosestKnowledge, getPrimaryAgent, getHiveHomeDir, insertKnowledge, listKnowledge, listConversationMessages, listRecentConversations, openHiveDatabase, updateConversationTitle, clearEpisodes, } from "../../storage/db.js";
12
+ import { createProvider, pingProvider } from "../../providers/index.js";
13
+ import { renderError, renderHiveHeader, renderInfo, renderSeparator, renderSuccess, } from "../ui.js";
14
+ import { openPage } from "../../browser/browser.js";
9
15
  import { runConfigKeyCommandWithOptions, runConfigModelCommandWithOptions, runConfigProviderCommandWithOptions, runConfigShowCommandWithOptions, runConfigThemeCommandWithOptions, } from "./config.js";
10
16
  import { runStatusCommandWithOptions } from "./status.js";
11
17
  import { getTheme } from "../theme.js";
@@ -18,6 +24,24 @@ const COMMAND_HELP_TEXT = [
18
24
  "Commands:",
19
25
  " /help show commands",
20
26
  " /new start a new conversation",
27
+ " /remember <fact> save a fact",
28
+ " /forget <thing> delete closest fact",
29
+ " /pin <fact> pin fact into context",
30
+ " /summarize <url> summarize a web page",
31
+ " /tldr summarize this conversation",
32
+ " /recap summarize persona + knowledge",
33
+ " /mode <name> switch response mode",
34
+ " /status show mode/provider/model",
35
+ " /export export conversation markdown",
36
+ " /save <title> name this conversation",
37
+ " /history list recent conversations",
38
+ " /clear clear the screen",
39
+ " /think <question>think step by step",
40
+ " /retry resend last message",
41
+ " /copy copy last reply",
42
+ " /hive memory list show knowledge items",
43
+ " /hive memory clear clear episodic memory",
44
+ " /hive memory show show current persona",
21
45
  " /browse <url> read a webpage",
22
46
  " browse <url> same as /browse",
23
47
  " /search <query> search the web",
@@ -36,6 +60,9 @@ const HIVE_SHORTCUT_HELP_TEXT = [
36
60
  " /hive help list shortcuts",
37
61
  " /hive status run hive status",
38
62
  " /hive config show run hive config show",
63
+ " /hive memory list list knowledge",
64
+ " /hive memory clear clear episodes",
65
+ " /hive memory show show persona",
39
66
  "",
40
67
  "Interactive config commands (in chat):",
41
68
  " /hive config provider",
@@ -53,6 +80,13 @@ const PREVIEW_AGENT_NAME = "jarvis";
53
80
  const PREVIEW_PROVIDER = "google";
54
81
  const PREVIEW_MODEL = "gemini-2.0-flash";
55
82
  const PREVIEW_NEW_MESSAGE = "Started a new preview conversation context.";
83
+ const MODE_PROMPTS = {
84
+ default: null,
85
+ research: "Every answer must be grounded in current web evidence. Perform web search as needed and cite sources inline.",
86
+ code: "Think and respond like a focused software engineer. Prioritize concise technical answers and code.",
87
+ brainstorm: "Be creative and opinionated. Offer bold suggestions and push back on weak ideas when helpful.",
88
+ brief: "Keep every response to a maximum of 3 sentences while preserving key details.",
89
+ };
56
90
  const COMMAND_SUGGESTIONS = [
57
91
  {
58
92
  label: "/help",
@@ -64,6 +98,81 @@ const COMMAND_SUGGESTIONS = [
64
98
  insertText: "/new",
65
99
  description: "start a new conversation",
66
100
  },
101
+ {
102
+ label: "/remember <fact>",
103
+ insertText: "/remember ",
104
+ description: "save to knowledge graph",
105
+ },
106
+ {
107
+ label: "/pin <fact>",
108
+ insertText: "/pin ",
109
+ description: "pin fact into context",
110
+ },
111
+ {
112
+ label: "/forget <thing>",
113
+ insertText: "/forget ",
114
+ description: "delete closest fact",
115
+ },
116
+ {
117
+ label: "/summarize <url>",
118
+ insertText: "/summarize ",
119
+ description: "summarize a web page",
120
+ },
121
+ {
122
+ label: "/tldr",
123
+ insertText: "/tldr",
124
+ description: "summarize this conversation",
125
+ },
126
+ {
127
+ label: "/recap",
128
+ insertText: "/recap",
129
+ description: "summarize persona & knowledge",
130
+ },
131
+ {
132
+ label: "/mode <name>",
133
+ insertText: "/mode ",
134
+ description: "switch response style",
135
+ },
136
+ {
137
+ label: "/export",
138
+ insertText: "/export",
139
+ description: "export conversation markdown",
140
+ },
141
+ {
142
+ label: "/save <title>",
143
+ insertText: "/save ",
144
+ description: "set conversation title",
145
+ },
146
+ {
147
+ label: "/history",
148
+ insertText: "/history",
149
+ description: "list recent conversations",
150
+ },
151
+ {
152
+ label: "/status",
153
+ insertText: "/status",
154
+ description: "show session status",
155
+ },
156
+ {
157
+ label: "/clear",
158
+ insertText: "/clear",
159
+ description: "clear the screen",
160
+ },
161
+ {
162
+ label: "/think <question>",
163
+ insertText: "/think ",
164
+ description: "think step by step",
165
+ },
166
+ {
167
+ label: "/retry",
168
+ insertText: "/retry",
169
+ description: "resend last message",
170
+ },
171
+ {
172
+ label: "/copy",
173
+ insertText: "/copy",
174
+ description: "copy last reply",
175
+ },
67
176
  {
68
177
  label: "/browse <url>",
69
178
  insertText: "/browse ",
@@ -94,6 +203,21 @@ const COMMAND_SUGGESTIONS = [
94
203
  insertText: "/hive config show",
95
204
  description: "run hive config show",
96
205
  },
206
+ {
207
+ label: "/hive memory list",
208
+ insertText: "/hive memory list",
209
+ description: "list knowledge entries",
210
+ },
211
+ {
212
+ label: "/hive memory clear",
213
+ insertText: "/hive memory clear",
214
+ description: "clear episodic memory",
215
+ },
216
+ {
217
+ label: "/hive memory show",
218
+ insertText: "/hive memory show",
219
+ description: "show current persona",
220
+ },
97
221
  {
98
222
  label: "/hive init",
99
223
  insertText: "/hive init",
@@ -142,6 +266,7 @@ export function registerChatCommand(program) {
142
266
  export async function runChatCommand(options, context = {}) {
143
267
  console.clear();
144
268
  renderHiveHeader("Chat");
269
+ void checkForUpdates();
145
270
  const entrypoint = context.entrypoint ?? "chat-command";
146
271
  if (entrypoint === "chat-command") {
147
272
  renderInfo("`hive chat` is deprecated. Run `hive`.");
@@ -159,16 +284,36 @@ export async function runChatCommand(options, context = {}) {
159
284
  return;
160
285
  }
161
286
  let activeProfile = profile;
287
+ const model = options.model ?? activeProfile.model;
288
+ const ctxStoragePath = join(getHiveHomeDir(), "ctx");
289
+ mkdirSync(ctxStoragePath, { recursive: true });
290
+ let hiveCtx = await initializeHiveCtxSession({
291
+ storagePath: ctxStoragePath,
292
+ profile: activeProfile,
293
+ model,
294
+ });
295
+ if (hiveCtx.warning) {
296
+ console.log(chalk.dim(hiveCtx.warning));
297
+ }
162
298
  let provider = await createProvider(activeProfile.provider);
163
299
  let agent = new HiveAgent(db, provider, activeProfile);
164
300
  let agentName = resolveAgentName(activeProfile.agent_name);
165
- const model = options.model ?? activeProfile.model;
301
+ let currentMode = "default";
302
+ try {
303
+ await pingProvider(provider, model);
304
+ }
305
+ catch {
306
+ renderError("✗ Provider unreachable. Run `hive doctor` to diagnose.");
307
+ return;
308
+ }
166
309
  let conversationId = options.conversation;
167
310
  const runOptions = {
168
311
  model,
169
312
  title: options.title,
170
313
  temperature,
171
314
  };
315
+ const lastUserPromptRef = { value: null };
316
+ const lastAssistantRef = { value: "" };
172
317
  renderChatPreamble({
173
318
  agentName,
174
319
  provider: profile.provider,
@@ -176,9 +321,22 @@ export async function runChatCommand(options, context = {}) {
176
321
  });
177
322
  if (options.message) {
178
323
  const augmentedMessage = await buildBrowserAugmentedPrompt(options.message, {
179
- locationHint: profile.location ?? undefined,
324
+ locationHint: activeProfile.location ?? undefined,
180
325
  });
181
- conversationId = await streamReply(agent, augmentedMessage, conversationId, runOptions, agentName);
326
+ const systemAddition = getModeSystemPrompt(currentMode);
327
+ lastUserPromptRef.value = options.message;
328
+ const streamResult = await streamReply({
329
+ agent,
330
+ prompt: augmentedMessage,
331
+ rawPrompt: options.message,
332
+ conversationId,
333
+ options: runOptions,
334
+ agentName,
335
+ systemAddition,
336
+ hiveCtx: hiveCtx.session,
337
+ });
338
+ conversationId = streamResult.conversationId;
339
+ lastAssistantRef.value = streamResult.assistantText;
182
340
  renderInfo(`conversation: ${conversationId}`);
183
341
  return;
184
342
  }
@@ -187,25 +345,54 @@ export async function runChatCommand(options, context = {}) {
187
345
  if (prompt.length === 0) {
188
346
  continue;
189
347
  }
348
+ const normalizedPrompt = prompt.trim().toLowerCase();
190
349
  if (prompt === "/") {
191
350
  printChatHelp();
192
351
  continue;
193
352
  }
194
- if (prompt === "/help") {
353
+ if (normalizedPrompt === "/help") {
195
354
  printChatHelp();
196
355
  continue;
197
356
  }
198
- if (prompt === "/exit" || prompt === "/quit") {
357
+ if (normalizedPrompt === "/exit" || normalizedPrompt === "/quit") {
199
358
  break;
200
359
  }
201
- if (prompt === "/new") {
360
+ if (normalizedPrompt === "/new") {
202
361
  conversationId = undefined;
362
+ currentMode = "default";
363
+ lastUserPromptRef.value = null;
364
+ lastAssistantRef.value = "";
203
365
  renderInfo("Started a new conversation context.");
204
366
  continue;
205
367
  }
206
368
  try {
369
+ const handled = await handleChatSlashCommand({
370
+ prompt,
371
+ db,
372
+ agent,
373
+ provider,
374
+ agentName,
375
+ conversationId,
376
+ activeProfilePersona: activeProfile.persona,
377
+ mode: currentMode,
378
+ providerName: activeProfile.provider,
379
+ modelName: runOptions.model ?? activeProfile.model,
380
+ setConversationId: (id) => {
381
+ conversationId = id;
382
+ },
383
+ setMode: (mode) => {
384
+ currentMode = mode;
385
+ },
386
+ lastUserPromptRef,
387
+ lastAssistantRef,
388
+ hiveCtx: hiveCtx.session,
389
+ });
390
+ if (handled) {
391
+ continue;
392
+ }
207
393
  const shortcutResult = await handleHiveShortcut(prompt, {
208
394
  allowInteractiveConfig: true,
395
+ db,
209
396
  });
210
397
  if (shortcutResult === "handled") {
211
398
  continue;
@@ -217,29 +404,57 @@ export async function runChatCommand(options, context = {}) {
217
404
  continue;
218
405
  }
219
406
  activeProfile = latestProfile;
407
+ const resolvedModel = options.model ?? activeProfile.model;
408
+ hiveCtx = await initializeHiveCtxSession({
409
+ storagePath: ctxStoragePath,
410
+ profile: activeProfile,
411
+ model: resolvedModel,
412
+ });
413
+ if (hiveCtx.warning) {
414
+ console.log(chalk.dim(hiveCtx.warning));
415
+ }
220
416
  provider = await createProvider(activeProfile.provider);
221
417
  agent = new HiveAgent(db, provider, activeProfile);
222
418
  agentName = resolveAgentName(activeProfile.agent_name);
223
419
  if (!options.model) {
224
420
  runOptions.model = activeProfile.model;
225
421
  }
422
+ try {
423
+ await pingProvider(provider, runOptions.model ?? activeProfile.model);
424
+ }
425
+ catch {
426
+ renderError("✗ Provider unreachable. Run `hive doctor` to diagnose.");
427
+ continue;
428
+ }
226
429
  conversationId = undefined;
227
430
  renderInfo(`Switched to ${activeProfile.provider} · ${runOptions.model ?? activeProfile.model}.`);
228
431
  renderInfo("Started a new conversation context.");
229
432
  continue;
230
433
  }
231
434
  if (isUnknownSlashCommand(prompt)) {
232
- renderError(`Unknown command: ${prompt}`);
233
- renderInfo("Run `/help` to view supported commands.");
435
+ renderError("✗ Unknown command. Type /help for available commands.");
234
436
  continue;
235
437
  }
236
438
  const augmentedPrompt = await buildBrowserAugmentedPrompt(prompt, {
237
- locationHint: profile.location ?? undefined,
439
+ locationHint: activeProfile.location ?? undefined,
440
+ });
441
+ lastUserPromptRef.value = prompt;
442
+ const systemAddition = getModeSystemPrompt(currentMode);
443
+ const streamResult = await streamReply({
444
+ agent,
445
+ prompt: augmentedPrompt,
446
+ rawPrompt: prompt,
447
+ conversationId,
448
+ options: runOptions,
449
+ agentName,
450
+ systemAddition,
451
+ hiveCtx: hiveCtx.session,
238
452
  });
239
- conversationId = await streamReply(agent, augmentedPrompt, conversationId, runOptions, agentName);
453
+ conversationId = streamResult.conversationId;
454
+ lastAssistantRef.value = streamResult.assistantText;
240
455
  }
241
456
  catch (error) {
242
- renderError(formatError(error));
457
+ renderAmberError(formatError(error));
243
458
  }
244
459
  }
245
460
  }
@@ -247,28 +462,84 @@ export async function runChatCommand(options, context = {}) {
247
462
  closeHiveDatabase(db);
248
463
  }
249
464
  }
250
- async function streamReply(agent, prompt, conversationId, options, agentName) {
251
- process.stdout.write(getTheme().accent(`${agentName}${PROMPT_SYMBOL} `));
252
- let activeConversationId = conversationId;
253
- for await (const event of agent.chat(prompt, {
254
- conversationId: activeConversationId,
255
- model: options.model,
256
- temperature: options.temperature,
257
- title: options.title,
258
- })) {
259
- if (event.type === "token") {
260
- process.stdout.write(event.token);
465
+ async function streamReply(input) {
466
+ const theme = getTheme();
467
+ process.stdout.write(theme.accent(`${input.agentName}${PROMPT_SYMBOL} `));
468
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
469
+ let frameIndex = 0;
470
+ let firstToken = false;
471
+ const spinner = setInterval(() => {
472
+ if (firstToken) {
473
+ return;
474
+ }
475
+ readline.cursorTo(stdout, 0);
476
+ readline.clearLine(stdout, 0);
477
+ stdout.write(`${theme.accent(`${input.agentName}${PROMPT_SYMBOL} `)}${chalk.dim(theme.accent(frames[frameIndex]))} thinking...`);
478
+ frameIndex = (frameIndex + 1) % frames.length;
479
+ }, 120);
480
+ let activeConversationId = input.conversationId;
481
+ let assistantText = "";
482
+ let ctxSystemPrompt;
483
+ let ctxTokenCount;
484
+ if (input.hiveCtx) {
485
+ const context = await input.hiveCtx.build(input.rawPrompt);
486
+ ctxSystemPrompt = context.system;
487
+ ctxTokenCount = context.tokens;
488
+ }
489
+ try {
490
+ for await (const event of input.agent.chat(input.prompt, {
491
+ conversationId: activeConversationId,
492
+ model: input.options.model,
493
+ temperature: input.options.temperature,
494
+ title: input.options.title,
495
+ systemAddition: input.systemAddition,
496
+ contextSystemPrompt: ctxSystemPrompt,
497
+ disableLegacyEpisodeStore: Boolean(input.hiveCtx),
498
+ })) {
499
+ if (event.type === "token") {
500
+ if (!firstToken) {
501
+ firstToken = true;
502
+ clearInterval(spinner);
503
+ readline.cursorTo(stdout, 0);
504
+ readline.clearLine(stdout, 0);
505
+ stdout.write(theme.accent(`${input.agentName}${PROMPT_SYMBOL} `));
506
+ }
507
+ process.stdout.write(event.token);
508
+ activeConversationId = event.conversationId;
509
+ assistantText += event.token;
510
+ continue;
511
+ }
261
512
  activeConversationId = event.conversationId;
262
- continue;
263
513
  }
264
- activeConversationId = event.conversationId;
514
+ }
515
+ finally {
516
+ if (!firstToken) {
517
+ clearInterval(spinner);
518
+ readline.cursorTo(stdout, 0);
519
+ readline.clearLine(stdout, 0);
520
+ stdout.write(theme.accent(`${input.agentName}${PROMPT_SYMBOL} `));
521
+ }
522
+ else {
523
+ clearInterval(spinner);
524
+ }
525
+ }
526
+ if (!firstToken) {
527
+ stdout.write("✗ No response received.\n");
528
+ renderSeparator(EXCHANGE_SEPARATOR);
529
+ return { conversationId: activeConversationId ?? "", assistantText };
265
530
  }
266
531
  process.stdout.write("\n");
267
532
  renderSeparator(EXCHANGE_SEPARATOR);
533
+ if (input.hiveCtx) {
534
+ await Promise.resolve(input.hiveCtx.episode(input.rawPrompt, assistantText)).catch(() => { });
535
+ }
536
+ if (ctxTokenCount !== undefined) {
537
+ console.log(chalk.dim(`· ~${ctxTokenCount} ctx tokens`));
538
+ }
268
539
  if (!activeConversationId) {
269
540
  throw new Error("Conversation state was not returned by the agent.");
270
541
  }
271
- return activeConversationId;
542
+ return { conversationId: activeConversationId, assistantText };
272
543
  }
273
544
  function parseTemperature(raw) {
274
545
  if (raw === undefined) {
@@ -286,6 +557,10 @@ function formatError(error) {
286
557
  }
287
558
  return String(error);
288
559
  }
560
+ function renderAmberError(message) {
561
+ const accent = getTheme().accent;
562
+ console.error(accent(`✗ ${message}`));
563
+ }
289
564
  function resolveAgentName(agentName) {
290
565
  const normalized = agentName?.trim();
291
566
  if (normalized && normalized.length > 0) {
@@ -355,6 +630,16 @@ function isHiveShortcut(prompt) {
355
630
  const normalized = prompt.trim().toLowerCase();
356
631
  return normalized === HIVE_SHORTCUT_PREFIX || normalized.startsWith(`${HIVE_SHORTCUT_PREFIX} `);
357
632
  }
633
+ function getModeSystemPrompt(mode) {
634
+ return MODE_PROMPTS[mode] ?? undefined;
635
+ }
636
+ function combineSystemAdditions(parts) {
637
+ const merged = parts
638
+ .map((part) => part?.trim() ?? "")
639
+ .filter((part) => part.length > 0)
640
+ .join("\n\n");
641
+ return merged.length > 0 ? merged : undefined;
642
+ }
358
643
  function isUnknownSlashCommand(prompt) {
359
644
  const normalized = prompt.trim().toLowerCase();
360
645
  if (!normalized.startsWith("/")) {
@@ -364,6 +649,26 @@ function isUnknownSlashCommand(prompt) {
364
649
  normalized === "/new" ||
365
650
  normalized === "/exit" ||
366
651
  normalized === "/quit" ||
652
+ normalized === "/remember" ||
653
+ normalized.startsWith("/remember ") ||
654
+ normalized === "/forget" ||
655
+ normalized.startsWith("/forget ") ||
656
+ normalized === "/summarize" ||
657
+ normalized.startsWith("/summarize ") ||
658
+ normalized === "/tldr" ||
659
+ normalized === "/recap" ||
660
+ normalized === "/mode" ||
661
+ normalized.startsWith("/mode ") ||
662
+ normalized === "/export" ||
663
+ normalized === "/history" ||
664
+ normalized === "/clear" ||
665
+ normalized === "/think" ||
666
+ normalized.startsWith("/think ") ||
667
+ normalized.startsWith("/save") ||
668
+ normalized.startsWith("/pin") ||
669
+ normalized === "/status" ||
670
+ normalized === "/retry" ||
671
+ normalized === "/copy" ||
367
672
  normalized === "/browse" ||
368
673
  normalized.startsWith("/browse ") ||
369
674
  normalized === "/search" ||
@@ -400,6 +705,52 @@ async function handleHiveShortcut(prompt, options = {}) {
400
705
  restoreChatInputAfterInteractiveCommand();
401
706
  return "handled";
402
707
  }
708
+ if (subcommand === "memory list") {
709
+ const db = options.db;
710
+ if (!db) {
711
+ renderError("Memory commands unavailable: database not open.");
712
+ return "handled";
713
+ }
714
+ const rows = listKnowledge(db, { limit: 1000 });
715
+ if (rows.length === 0) {
716
+ renderInfo("No knowledge stored.");
717
+ return "handled";
718
+ }
719
+ rows.forEach((row, index) => {
720
+ const pinnedLabel = row.pinned ? " (pinned)" : "";
721
+ renderInfo(`${index + 1}. ${row.content}${pinnedLabel}`);
722
+ });
723
+ return "handled";
724
+ }
725
+ if (subcommand === "memory clear") {
726
+ const db = options.db;
727
+ if (!db) {
728
+ renderError("Memory commands unavailable: database not open.");
729
+ return "handled";
730
+ }
731
+ const confirmed = await promptYesNo("This will delete all episodic memories. Continue? (y/n) ");
732
+ if (!confirmed) {
733
+ renderInfo("Cancelled.");
734
+ return "handled";
735
+ }
736
+ clearEpisodes(db);
737
+ renderSuccess("Episodes cleared.");
738
+ return "handled";
739
+ }
740
+ if (subcommand === "memory show") {
741
+ const db = options.db;
742
+ if (!db) {
743
+ renderError("Memory commands unavailable: database not open.");
744
+ return "handled";
745
+ }
746
+ const agent = getPrimaryAgent(db);
747
+ if (!agent) {
748
+ renderError("Hive is not initialized. Run `hive init` first.");
749
+ return "handled";
750
+ }
751
+ renderInfo(agent.persona);
752
+ return "handled";
753
+ }
403
754
  if (subcommand === "config provider") {
404
755
  if (!options.allowInteractiveConfig) {
405
756
  renderInfo("Interactive config commands are unavailable here.");
@@ -445,6 +796,335 @@ async function handleHiveShortcut(prompt, options = {}) {
445
796
  renderInfo("Use `/hive help` to list available shortcuts.");
446
797
  return "handled";
447
798
  }
799
+ async function handleChatSlashCommand(input) {
800
+ const normalized = input.prompt.trim();
801
+ const lower = normalized.toLowerCase();
802
+ if (!lower.startsWith("/")) {
803
+ return false;
804
+ }
805
+ if (lower === "/clear") {
806
+ console.clear();
807
+ renderHiveHeader("Chat");
808
+ renderChatPreamble({
809
+ agentName: input.agentName,
810
+ provider: input.providerName,
811
+ model: input.modelName,
812
+ });
813
+ input.lastUserPromptRef.value = null;
814
+ input.lastAssistantRef.value = "";
815
+ return true;
816
+ }
817
+ if (lower.startsWith("/remember")) {
818
+ const fact = normalized.slice("/remember".length).trim();
819
+ if (fact.length === 0) {
820
+ renderError("Usage: /remember <fact>");
821
+ return true;
822
+ }
823
+ if (input.hiveCtx) {
824
+ await input.hiveCtx.remember(fact);
825
+ }
826
+ else {
827
+ insertKnowledge(input.db, { content: fact });
828
+ }
829
+ renderSuccess("✓ Remembered.");
830
+ input.lastUserPromptRef.value = null;
831
+ return true;
832
+ }
833
+ if (lower.startsWith("/forget")) {
834
+ const query = normalized.slice("/forget".length).trim();
835
+ if (query.length === 0) {
836
+ renderError("Usage: /forget <thing>");
837
+ return true;
838
+ }
839
+ const match = findClosestKnowledge(input.db, query);
840
+ if (!match) {
841
+ renderError("No similar knowledge found.");
842
+ return true;
843
+ }
844
+ const confirmed = await promptYesNo(`Forget "${match.content}"? (y/n) `);
845
+ if (!confirmed) {
846
+ renderInfo("Kept.");
847
+ return true;
848
+ }
849
+ deleteKnowledge(input.db, match.id);
850
+ renderSuccess("✓ Forgotten.");
851
+ input.lastUserPromptRef.value = null;
852
+ return true;
853
+ }
854
+ if (lower.startsWith("/mode")) {
855
+ const modeName = normalized.slice("/mode".length).trim().toLowerCase();
856
+ if (!modeName || !Object.hasOwn(MODE_PROMPTS, modeName)) {
857
+ renderError("Usage: /mode <default|research|code|brainstorm|brief>");
858
+ return true;
859
+ }
860
+ input.setMode(modeName);
861
+ renderSuccess(`✓ Mode set to ${modeName}.`);
862
+ input.lastUserPromptRef.value = null;
863
+ return true;
864
+ }
865
+ if (lower.startsWith("/pin")) {
866
+ const fact = normalized.slice("/pin".length).trim();
867
+ if (fact.length === 0) {
868
+ renderError("Usage: /pin <fact>");
869
+ return true;
870
+ }
871
+ if (input.hiveCtx) {
872
+ await input.hiveCtx.remember(fact, { pinned: true });
873
+ }
874
+ else {
875
+ insertKnowledge(input.db, { content: fact, pinned: true });
876
+ }
877
+ renderSuccess("✓ Pinned.");
878
+ input.lastUserPromptRef.value = null;
879
+ return true;
880
+ }
881
+ if (lower === "/export") {
882
+ if (!input.conversationId) {
883
+ renderError("No conversation to export. Start chatting first.");
884
+ return true;
885
+ }
886
+ const messages = listConversationMessages(input.db, input.conversationId);
887
+ const exportDir = join(getHiveHomeDir(), "exports");
888
+ mkdirSync(exportDir, { recursive: true });
889
+ const exportPath = join(exportDir, `${input.conversationId}.md`);
890
+ writeFileSync(exportPath, formatConversationMarkdown(messages, input.conversationId));
891
+ renderSuccess(`✓ Exported to ${exportPath}`);
892
+ input.lastUserPromptRef.value = null;
893
+ return true;
894
+ }
895
+ if (lower === "/history") {
896
+ const rows = listRecentConversations(input.db, 10);
897
+ if (rows.length === 0) {
898
+ renderInfo("No past conversations found.");
899
+ return true;
900
+ }
901
+ rows.forEach((row, index) => {
902
+ const title = row.title?.trim().length ? row.title : "(untitled)";
903
+ renderInfo(`${index + 1}. ${title} · ${row.updated_at} · ${row.message_count} messages`);
904
+ });
905
+ const answer = await promptLine("Pick a conversation number (or blank to cancel): ");
906
+ if (!answer) {
907
+ return true;
908
+ }
909
+ const choice = Number.parseInt(answer, 10);
910
+ if (Number.isNaN(choice) || choice < 1 || choice > rows.length) {
911
+ renderError("Invalid selection.");
912
+ return true;
913
+ }
914
+ const selected = rows[choice - 1];
915
+ input.setConversationId(selected.id);
916
+ renderInfo(`Continuing conversation ${selected.title ?? selected.id}.`);
917
+ input.lastUserPromptRef.value = null;
918
+ input.lastAssistantRef.value = "";
919
+ return true;
920
+ }
921
+ if (lower === "/tldr") {
922
+ if (!input.conversationId) {
923
+ renderError("No conversation yet. Say something first.");
924
+ return true;
925
+ }
926
+ const history = listConversationMessages(input.db, input.conversationId);
927
+ if (history.length === 0) {
928
+ renderError("Nothing to summarize yet.");
929
+ return true;
930
+ }
931
+ await streamEphemeral({
932
+ provider: input.provider,
933
+ agentName: input.agentName,
934
+ model: input.modelName,
935
+ messages: buildEphemeralMessages({
936
+ persona: input.activeProfilePersona,
937
+ mode: input.mode,
938
+ systemInstruction: "Summarize this conversation in 3-5 bullet points.",
939
+ history,
940
+ }),
941
+ });
942
+ input.lastUserPromptRef.value = null;
943
+ return true;
944
+ }
945
+ if (lower === "/recap") {
946
+ const knowledge = listKnowledge(input.db, { limit: 500 });
947
+ await streamEphemeral({
948
+ provider: input.provider,
949
+ agentName: input.agentName,
950
+ model: input.modelName,
951
+ messages: buildRecapMessages({
952
+ persona: input.activeProfilePersona,
953
+ knowledge,
954
+ mode: input.mode,
955
+ }),
956
+ });
957
+ input.lastUserPromptRef.value = null;
958
+ return true;
959
+ }
960
+ if (lower.startsWith("/summarize")) {
961
+ const url = normalized.slice("/summarize".length).trim();
962
+ if (url.length === 0) {
963
+ renderError("Usage: /summarize <url>");
964
+ return true;
965
+ }
966
+ try {
967
+ const content = await openPage(url);
968
+ await streamEphemeral({
969
+ provider: input.provider,
970
+ agentName: input.agentName,
971
+ model: input.modelName,
972
+ messages: buildEphemeralMessages({
973
+ persona: input.activeProfilePersona,
974
+ mode: input.mode,
975
+ userMessage: `Summarize this page concisely:\n\n${content}`,
976
+ }),
977
+ });
978
+ }
979
+ catch (error) {
980
+ renderError(`Unable to summarize page: ${formatError(error)}`);
981
+ }
982
+ return true;
983
+ }
984
+ if (lower.startsWith("/save")) {
985
+ const title = normalized.slice("/save".length).trim();
986
+ if (!input.conversationId) {
987
+ renderError("No active conversation to title.");
988
+ return true;
989
+ }
990
+ if (title.length === 0) {
991
+ renderError("Usage: /save <title>");
992
+ return true;
993
+ }
994
+ updateConversationTitle(input.db, input.conversationId, title);
995
+ renderSuccess(`✓ Saved title "${title}".`);
996
+ input.lastUserPromptRef.value = null;
997
+ return true;
998
+ }
999
+ if (lower.startsWith("/think")) {
1000
+ const question = normalized.slice("/think".length).trim();
1001
+ if (question.length === 0) {
1002
+ renderError("Usage: /think <question>");
1003
+ return true;
1004
+ }
1005
+ await streamEphemeral({
1006
+ provider: input.provider,
1007
+ agentName: input.agentName,
1008
+ model: input.modelName,
1009
+ messages: buildEphemeralMessages({
1010
+ persona: input.activeProfilePersona,
1011
+ mode: input.mode,
1012
+ userMessage: `Think through this step by step, show your reasoning:\n\n${question}`,
1013
+ }),
1014
+ });
1015
+ return true;
1016
+ }
1017
+ if (lower === "/status") {
1018
+ const info = [
1019
+ `mode=${input.mode}`,
1020
+ `provider=${input.providerName}`,
1021
+ `model=${input.modelName}`,
1022
+ `conversation=${input.conversationId ?? "none"}`,
1023
+ ].join(" · ");
1024
+ renderInfo(info);
1025
+ return true;
1026
+ }
1027
+ if (lower === "/retry") {
1028
+ const userPrompt = input.lastUserPromptRef.value;
1029
+ if (!userPrompt) {
1030
+ renderError("Nothing to retry yet.");
1031
+ return true;
1032
+ }
1033
+ const systemAddition = getModeSystemPrompt(input.mode);
1034
+ const retryResult = await streamReply({
1035
+ agent: input.agent,
1036
+ prompt: userPrompt,
1037
+ rawPrompt: userPrompt,
1038
+ conversationId: input.conversationId,
1039
+ options: { model: input.modelName },
1040
+ agentName: input.agentName,
1041
+ systemAddition,
1042
+ hiveCtx: input.hiveCtx,
1043
+ });
1044
+ input.setConversationId(retryResult.conversationId);
1045
+ input.lastAssistantRef.value = retryResult.assistantText;
1046
+ return true;
1047
+ }
1048
+ if (lower === "/copy") {
1049
+ if (!input.lastAssistantRef.value) {
1050
+ renderError("Nothing to copy yet.");
1051
+ return true;
1052
+ }
1053
+ const copied = copyToClipboard(input.lastAssistantRef.value);
1054
+ if (copied) {
1055
+ renderSuccess("✓ Copied last reply to clipboard.");
1056
+ }
1057
+ else {
1058
+ renderError("Clipboard tool not available.");
1059
+ }
1060
+ return true;
1061
+ }
1062
+ return false;
1063
+ }
1064
+ function copyToClipboard(text) {
1065
+ const platform = process.platform;
1066
+ const buffer = Buffer.from(text, "utf8");
1067
+ if (platform === "darwin") {
1068
+ const result = spawnSync("pbcopy", [], { input: buffer });
1069
+ return result.status === 0;
1070
+ }
1071
+ const result = spawnSync("xclip", ["-selection", "clipboard"], { input: buffer });
1072
+ return result.status === 0;
1073
+ }
1074
+ let cachedLocalVersion = null;
1075
+ async function checkForUpdates() {
1076
+ try {
1077
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), 3000));
1078
+ const latest = (await Promise.race([
1079
+ fetch("https://registry.npmjs.org/@imisbahk/hive/latest").then((response) => response.json()),
1080
+ timeout,
1081
+ ]));
1082
+ if (!latest?.version || typeof latest.version !== "string") {
1083
+ return;
1084
+ }
1085
+ const localVersion = getLocalVersion();
1086
+ if (isVersionNewer(latest.version, localVersion)) {
1087
+ const amber = chalk.hex("#ffbf00");
1088
+ console.log(amber.dim(`✦ Update available v${localVersion} → npm update -g @imisbahk/hive`));
1089
+ }
1090
+ }
1091
+ catch {
1092
+ // Silently ignore update check failures.
1093
+ }
1094
+ }
1095
+ function getLocalVersion() {
1096
+ if (cachedLocalVersion) {
1097
+ return cachedLocalVersion;
1098
+ }
1099
+ try {
1100
+ const raw = readFileSync(new URL("../../../package.json", import.meta.url), "utf8");
1101
+ const parsed = JSON.parse(raw);
1102
+ if (parsed.version) {
1103
+ cachedLocalVersion = parsed.version;
1104
+ return cachedLocalVersion;
1105
+ }
1106
+ }
1107
+ catch {
1108
+ // ignore
1109
+ }
1110
+ cachedLocalVersion = "0.0.0";
1111
+ return cachedLocalVersion;
1112
+ }
1113
+ function isVersionNewer(remote, local) {
1114
+ const toNumbers = (value) => value.split(".").map((part) => Number.parseInt(part, 10));
1115
+ const r = toNumbers(remote);
1116
+ const l = toNumbers(local);
1117
+ const length = Math.max(r.length, l.length);
1118
+ for (let index = 0; index < length; index += 1) {
1119
+ const rv = r[index] ?? 0;
1120
+ const lv = l[index] ?? 0;
1121
+ if (rv > lv)
1122
+ return true;
1123
+ if (rv < lv)
1124
+ return false;
1125
+ }
1126
+ return false;
1127
+ }
448
1128
  function getCommandSuggestions(input) {
449
1129
  const normalized = input.trimStart().toLowerCase();
450
1130
  if (!normalized.startsWith("/")) {
@@ -456,6 +1136,85 @@ function getCommandSuggestions(input) {
456
1136
  suggestion.label.toLowerCase().includes(normalized.slice(1)));
457
1137
  return [...prefixMatches, ...fallbackMatches];
458
1138
  }
1139
+ async function promptLine(question) {
1140
+ const rl = createInterface({
1141
+ input: stdin,
1142
+ output: stdout,
1143
+ terminal: true,
1144
+ });
1145
+ try {
1146
+ return (await rl.question(question)).trim();
1147
+ }
1148
+ finally {
1149
+ rl.close();
1150
+ }
1151
+ }
1152
+ async function promptYesNo(question) {
1153
+ const answer = (await promptLine(question)).toLowerCase();
1154
+ return answer === "y" || answer === "yes";
1155
+ }
1156
+ async function streamEphemeral(input) {
1157
+ process.stdout.write(getTheme().accent(`${input.agentName}${PROMPT_SYMBOL} `));
1158
+ let hadOutput = false;
1159
+ for await (const token of input.provider.streamChat({
1160
+ model: input.model ?? input.provider.defaultModel,
1161
+ messages: input.messages,
1162
+ })) {
1163
+ hadOutput = true;
1164
+ process.stdout.write(token);
1165
+ }
1166
+ if (!hadOutput) {
1167
+ process.stdout.write("(no response)");
1168
+ }
1169
+ process.stdout.write("\n");
1170
+ renderSeparator(EXCHANGE_SEPARATOR);
1171
+ }
1172
+ function buildEphemeralMessages(input) {
1173
+ const messages = [
1174
+ { role: "system", content: RUNTIME_SYSTEM_GUARDRAILS },
1175
+ { role: "system", content: input.persona },
1176
+ ];
1177
+ const modePrompt = getModeSystemPrompt(input.mode);
1178
+ if (modePrompt) {
1179
+ messages.push({ role: "system", content: modePrompt });
1180
+ }
1181
+ if (input.systemInstruction) {
1182
+ messages.push({ role: "system", content: input.systemInstruction });
1183
+ }
1184
+ if (input.history) {
1185
+ messages.push(...input.history.map((message) => ({
1186
+ role: message.role,
1187
+ content: message.content,
1188
+ })));
1189
+ }
1190
+ if (input.userMessage) {
1191
+ messages.push({ role: "user", content: input.userMessage });
1192
+ }
1193
+ return messages;
1194
+ }
1195
+ function buildRecapMessages(input) {
1196
+ const knowledgeLines = input.knowledge.length > 0
1197
+ ? input.knowledge.map((row) => `- ${row.content}`).join("\n")
1198
+ : "No knowledge stored yet.";
1199
+ 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}`;
1200
+ return buildEphemeralMessages({
1201
+ persona: input.persona,
1202
+ mode: input.mode,
1203
+ userMessage,
1204
+ });
1205
+ }
1206
+ function formatConversationMarkdown(messages, conversationId) {
1207
+ const lines = [`# Conversation ${conversationId}`, ""];
1208
+ for (const message of messages) {
1209
+ const speaker = message.role === "user"
1210
+ ? "User"
1211
+ : message.role === "assistant"
1212
+ ? "Hive"
1213
+ : "System";
1214
+ lines.push(`**${speaker}:**`, message.content, "");
1215
+ }
1216
+ return lines.join("\n");
1217
+ }
459
1218
  async function readPromptWithSuggestions() {
460
1219
  const accent = getTheme().accent;
461
1220
  const promptPrefix = accent(USER_PROMPT);