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