@imisbahk/hive 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.github/workflows/publish.yml +31 -0
  2. package/.rocket/README.md +8 -8
  3. package/.rocket/SYMBOLS.md +260 -117
  4. package/CONTRIBUTING.md +2 -1
  5. package/FEATURES.md +55 -0
  6. package/README.md +13 -11
  7. package/dist/agent/agent.d.ts +10 -1
  8. package/dist/agent/agent.d.ts.map +1 -1
  9. package/dist/agent/agent.js +351 -1
  10. package/dist/agent/agent.js.map +1 -1
  11. package/dist/browser/browser.d.ts +9 -0
  12. package/dist/browser/browser.d.ts.map +1 -0
  13. package/dist/browser/browser.js +338 -0
  14. package/dist/browser/browser.js.map +1 -0
  15. package/dist/cli/commands/chat.d.ts +5 -1
  16. package/dist/cli/commands/chat.d.ts.map +1 -1
  17. package/dist/cli/commands/chat.js +560 -38
  18. package/dist/cli/commands/chat.js.map +1 -1
  19. package/dist/cli/commands/config.d.ts +11 -0
  20. package/dist/cli/commands/config.d.ts.map +1 -1
  21. package/dist/cli/commands/config.js +50 -15
  22. package/dist/cli/commands/config.js.map +1 -1
  23. package/dist/cli/commands/init.d.ts.map +1 -1
  24. package/dist/cli/commands/init.js +39 -14
  25. package/dist/cli/commands/init.js.map +1 -1
  26. package/dist/cli/commands/nuke.d.ts.map +1 -1
  27. package/dist/cli/commands/nuke.js +5 -4
  28. package/dist/cli/commands/nuke.js.map +1 -1
  29. package/dist/cli/commands/status.d.ts +5 -0
  30. package/dist/cli/commands/status.d.ts.map +1 -1
  31. package/dist/cli/commands/status.js +15 -5
  32. package/dist/cli/commands/status.js.map +1 -1
  33. package/dist/cli/index.js +34 -12
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/cli/ui.d.ts +7 -0
  36. package/dist/cli/ui.d.ts.map +1 -0
  37. package/dist/cli/ui.js +96 -0
  38. package/dist/cli/ui.js.map +1 -0
  39. package/dist/providers/base.d.ts +37 -1
  40. package/dist/providers/base.d.ts.map +1 -1
  41. package/dist/providers/base.js +104 -0
  42. package/dist/providers/base.js.map +1 -1
  43. package/dist/providers/openai-compatible.d.ts +2 -1
  44. package/dist/providers/openai-compatible.d.ts.map +1 -1
  45. package/dist/providers/openai-compatible.js +18 -1
  46. package/dist/providers/openai-compatible.js.map +1 -1
  47. package/package.json +9 -1
  48. package/prompts/Browser.md +13 -0
  49. package/prompts/Debugging.md +15 -0
  50. package/prompts/Execution.md +13 -0
  51. package/prompts/Planning.md +13 -0
  52. package/prompts/Product.md +14 -0
  53. package/prompts/Review.md +15 -0
  54. package/prompts/Safety.md +12 -0
  55. package/prompts/Search.md +14 -0
  56. package/prompts/Tools.md +14 -0
  57. package/prompts/Writing.md +13 -0
  58. package/releases/v1/v0.1/RELEASE-NOTES.md +46 -0
  59. package/src/agent/agent.ts +442 -2
  60. package/src/browser/browser.ts +410 -0
  61. package/src/cli/commands/chat.ts +705 -34
  62. package/src/cli/commands/config.ts +78 -15
  63. package/src/cli/commands/init.ts +60 -14
  64. package/src/cli/commands/nuke.ts +11 -7
  65. package/src/cli/commands/status.ts +28 -5
  66. package/src/cli/index.ts +37 -9
  67. package/src/cli/ui.ts +120 -0
  68. package/src/providers/base.ts +176 -1
  69. package/src/providers/openai-compatible.ts +24 -0
@@ -1,6 +1,5 @@
1
1
  import process from "node:process";
2
2
 
3
- import chalk from "chalk";
4
3
  import { Command } from "commander";
5
4
  import inquirer from "inquirer";
6
5
  import keytar from "keytar";
@@ -9,6 +8,13 @@ import ora from "ora";
9
8
  import { resolveProviderApiKey } from "../../providers/api-key.js";
10
9
  import { normalizeProviderName, type ProviderName } from "../../providers/base.js";
11
10
  import { promptForModel, promptForProvider } from "../helpers/providerPrompts.js";
11
+ import {
12
+ renderError,
13
+ renderHiveHeader,
14
+ renderInfo,
15
+ renderStep,
16
+ renderSuccess,
17
+ } from "../ui.js";
12
18
  import {
13
19
  closeHiveDatabase,
14
20
  getPrimaryAgent,
@@ -20,6 +26,14 @@ import {
20
26
 
21
27
  const KEYCHAIN_SERVICE = "hive";
22
28
 
29
+ interface ConfigShowRenderOptions {
30
+ showHeader?: boolean;
31
+ }
32
+
33
+ interface ConfigInteractiveRenderOptions {
34
+ showHeader?: boolean;
35
+ }
36
+
23
37
  export function registerConfigCommand(program: Command): void {
24
38
  const configCommand = program
25
39
  .command("config")
@@ -52,9 +66,24 @@ export function registerConfigCommand(program: Command): void {
52
66
  .action(async () => {
53
67
  await runConfigShowCommand();
54
68
  });
69
+
70
+ configCommand.action(() => {
71
+ renderHiveHeader("Config");
72
+ configCommand.outputHelp();
73
+ });
55
74
  }
56
75
 
57
76
  export async function runConfigProviderCommand(): Promise<void> {
77
+ await runConfigProviderCommandWithOptions();
78
+ }
79
+
80
+ export async function runConfigProviderCommandWithOptions(
81
+ options: ConfigInteractiveRenderOptions = {},
82
+ ): Promise<void> {
83
+ const showHeader = options.showHeader ?? true;
84
+ if (showHeader) {
85
+ renderHiveHeader("Config · Provider");
86
+ }
58
87
  const spinner = ora("Loading configuration...").start();
59
88
  const db = openHiveDatabase();
60
89
 
@@ -64,7 +93,7 @@ export async function runConfigProviderCommand(): Promise<void> {
64
93
  const agent = getPrimaryAgent(db);
65
94
  if (!agent) {
66
95
  spinner.stop();
67
- console.error(chalk.red("Hive is not initialized. Run `hive init` first."));
96
+ renderError("Hive is not initialized. Run `hive init` first.");
68
97
  return;
69
98
  }
70
99
 
@@ -110,7 +139,8 @@ export async function runConfigProviderCommand(): Promise<void> {
110
139
  }
111
140
 
112
141
  spinner.succeed("Configuration saved.");
113
- console.log("Provider updated. Run `hive chat` to use it.");
142
+ renderSuccess("Provider updated.");
143
+ renderStep("Run `hive` to use it.");
114
144
  } catch (error) {
115
145
  if (spinner.isSpinning) {
116
146
  spinner.fail("Failed to update provider configuration.");
@@ -122,6 +152,16 @@ export async function runConfigProviderCommand(): Promise<void> {
122
152
  }
123
153
 
124
154
  export async function runConfigModelCommand(): Promise<void> {
155
+ await runConfigModelCommandWithOptions();
156
+ }
157
+
158
+ export async function runConfigModelCommandWithOptions(
159
+ options: ConfigInteractiveRenderOptions = {},
160
+ ): Promise<void> {
161
+ const showHeader = options.showHeader ?? true;
162
+ if (showHeader) {
163
+ renderHiveHeader("Config · Model");
164
+ }
125
165
  const spinner = ora("Loading configuration...").start();
126
166
  const db = openHiveDatabase();
127
167
 
@@ -131,7 +171,7 @@ export async function runConfigModelCommand(): Promise<void> {
131
171
  const agent = getPrimaryAgent(db);
132
172
  if (!agent) {
133
173
  spinner.stop();
134
- console.error(chalk.red("Hive is not initialized. Run `hive init` first."));
174
+ renderError("Hive is not initialized. Run `hive init` first.");
135
175
  return;
136
176
  }
137
177
 
@@ -151,7 +191,8 @@ export async function runConfigModelCommand(): Promise<void> {
151
191
  setMetaValue(db, "model", updatedAgent.model);
152
192
 
153
193
  spinner.succeed("Configuration saved.");
154
- console.log("Model updated. Run `hive chat` to use it.");
194
+ renderSuccess("Model updated.");
195
+ renderStep("Run `hive` to use it.");
155
196
  } catch (error) {
156
197
  if (spinner.isSpinning) {
157
198
  spinner.fail("Failed to update model configuration.");
@@ -163,6 +204,16 @@ export async function runConfigModelCommand(): Promise<void> {
163
204
  }
164
205
 
165
206
  export async function runConfigKeyCommand(): Promise<void> {
207
+ await runConfigKeyCommandWithOptions();
208
+ }
209
+
210
+ export async function runConfigKeyCommandWithOptions(
211
+ options: ConfigInteractiveRenderOptions = {},
212
+ ): Promise<void> {
213
+ const showHeader = options.showHeader ?? true;
214
+ if (showHeader) {
215
+ renderHiveHeader("Config · Key");
216
+ }
166
217
  const spinner = ora("Loading configuration...").start();
167
218
  const db = openHiveDatabase();
168
219
 
@@ -172,14 +223,14 @@ export async function runConfigKeyCommand(): Promise<void> {
172
223
  const agent = getPrimaryAgent(db);
173
224
  if (!agent) {
174
225
  spinner.stop();
175
- console.error(chalk.red("Hive is not initialized. Run `hive init` first."));
226
+ renderError("Hive is not initialized. Run `hive init` first.");
176
227
  return;
177
228
  }
178
229
 
179
230
  const provider = normalizeProviderName(agent.provider);
180
231
 
181
232
  spinner.stop();
182
- console.log(chalk.dim(`Current provider: ${provider}`));
233
+ renderInfo(`Current provider: ${provider}`);
183
234
 
184
235
  const answer = (await inquirer.prompt([
185
236
  {
@@ -195,7 +246,8 @@ export async function runConfigKeyCommand(): Promise<void> {
195
246
  await keytar.setPassword(KEYCHAIN_SERVICE, provider, answer.apiKey.trim());
196
247
 
197
248
  spinner.succeed("Configuration saved.");
198
- console.log("API key updated. Run `hive chat` to use it.");
249
+ renderSuccess("API key updated.");
250
+ renderStep("Run `hive` to use it.");
199
251
  } catch (error) {
200
252
  if (spinner.isSpinning) {
201
253
  spinner.fail("Failed to update API key.");
@@ -207,22 +259,33 @@ export async function runConfigKeyCommand(): Promise<void> {
207
259
  }
208
260
 
209
261
  export async function runConfigShowCommand(): Promise<void> {
262
+ await runConfigShowCommandWithOptions();
263
+ }
264
+
265
+ export async function runConfigShowCommandWithOptions(
266
+ options: ConfigShowRenderOptions = {},
267
+ ): Promise<void> {
268
+ const showHeader = options.showHeader ?? true;
269
+ if (showHeader) {
270
+ renderHiveHeader("Config · Show");
271
+ }
272
+
210
273
  const db = openHiveDatabase();
211
274
 
212
275
  try {
213
276
  const agent = getPrimaryAgent(db);
214
277
  if (!agent) {
215
- console.error(chalk.red("Hive is not initialized. Run `hive init` first."));
278
+ renderError("Hive is not initialized. Run `hive init` first.");
216
279
  return;
217
280
  }
218
281
 
219
282
  const provider = normalizeProviderName(agent.provider);
220
283
  const keyStatus = await getKeyStatus(provider);
221
284
 
222
- console.log(`Provider: ${provider}`);
223
- console.log(`Model: ${agent.model}`);
224
- console.log(`Agent name: ${agent.agent_name ?? "not set"}`);
225
- console.log(`API key: ${keyStatus}`);
285
+ renderStep(`Provider: ${provider}`);
286
+ renderStep(`Model: ${agent.model}`);
287
+ renderStep(`Agent name: ${agent.agent_name ?? "not set"}`);
288
+ renderStep(`API key: ${keyStatus}`);
226
289
  } finally {
227
290
  closeHiveDatabase(db);
228
291
  }
@@ -235,8 +298,8 @@ function ensureInteractiveTerminal(errorMessage: string): void {
235
298
  }
236
299
 
237
300
  function printCurrentProviderAndModel(provider: ProviderName, model: string): void {
238
- console.log(chalk.dim(`Current provider: ${provider}`));
239
- console.log(chalk.dim(`Current model: ${model}`));
301
+ renderInfo(`Current provider: ${provider}`);
302
+ renderInfo(`Current model: ${model}`);
240
303
  }
241
304
 
242
305
  async function getKeyStatus(provider: ProviderName): Promise<"set" | "not set"> {
@@ -2,7 +2,6 @@ import process from "node:process";
2
2
  import * as fs from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
- import chalk from "chalk";
6
5
  import { Command } from "commander";
7
6
  import inquirer from "inquirer";
8
7
  import keytar from "keytar";
@@ -10,6 +9,12 @@ import ora from "ora";
10
9
 
11
10
  import { buildDefaultPersona } from "../../agent/agent.js";
12
11
  import { promptForModel, promptForProvider } from "../helpers/providerPrompts.js";
12
+ import {
13
+ renderHiveHeader,
14
+ renderInfo,
15
+ renderStep,
16
+ renderSuccess,
17
+ } from "../ui.js";
13
18
  import {
14
19
  closeHiveDatabase,
15
20
  getHiveHomeDir,
@@ -49,6 +54,7 @@ export function registerInitCommand(program: Command): void {
49
54
  }
50
55
 
51
56
  export async function runInitCommand(options: InitCommandOptions = {}): Promise<void> {
57
+ renderHiveHeader("Init");
52
58
  const spinner = ora("Preparing init...").start();
53
59
  const db = openHiveDatabase();
54
60
 
@@ -71,7 +77,7 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise<
71
77
  ])) as { reinitialize: boolean };
72
78
 
73
79
  if (!reinitialize) {
74
- console.log(chalk.dim("Initialization cancelled."));
80
+ renderInfo("Initialization cancelled.");
75
81
  return;
76
82
  }
77
83
  }
@@ -101,13 +107,13 @@ export async function runInitCommand(options: InitCommandOptions = {}): Promise<
101
107
  copyPromptsDirectory(options.force ?? false);
102
108
 
103
109
  spinner.succeed("Initialization complete.");
104
- console.log(chalk.green(`HIVE-ID: ${agent.id}`));
110
+ renderSuccess(`HIVE-ID: ${agent.id}`);
105
111
  if (agent.agent_name) {
106
- console.log(chalk.green(`Agent name: ${agent.agent_name}`));
112
+ renderSuccess(`Agent name: ${agent.agent_name}`);
107
113
  }
108
- console.log(chalk.green(`Provider: ${agent.provider}`));
109
- console.log(chalk.green(`Model: ${agent.model}`));
110
- console.log("Run `hive chat` to start talking.");
114
+ renderSuccess(`Provider: ${agent.provider}`);
115
+ renderSuccess(`Model: ${agent.model}`);
116
+ renderStep("Run `hive` to start talking.");
111
117
  } catch (error) {
112
118
  if (spinner.isSpinning) {
113
119
  spinner.fail("Hive initialization failed.");
@@ -225,18 +231,58 @@ function copyPromptsDirectory(force: boolean): void {
225
231
  const destinationPath = join(getHiveHomeDir(), "prompts");
226
232
 
227
233
  if (!fs.existsSync(sourcePath)) {
228
- console.warn(chalk.yellow("Warning: prompts/ folder not found. Skipping prompts load."));
234
+ renderInfo("Warning: prompts/ folder not found. Skipping prompts load.");
235
+ return;
236
+ }
237
+
238
+ if (force && fs.existsSync(destinationPath)) {
239
+ fs.rmSync(destinationPath, { recursive: true, force: true });
240
+ }
241
+
242
+ fs.mkdirSync(destinationPath, { recursive: true });
243
+ const copiedFiles = syncPromptFiles(sourcePath, destinationPath, force);
244
+
245
+ if (copiedFiles === 0) {
246
+ renderStep("Prompts already up to date -> ~/.hive/prompts/");
229
247
  return;
230
248
  }
231
249
 
232
- if (fs.existsSync(destinationPath)) {
233
- if (!force) {
234
- return;
250
+ renderStep(`Prompts loaded -> ~/.hive/prompts/ (${copiedFiles} files)`);
251
+ }
252
+
253
+ function syncPromptFiles(
254
+ sourceDirectory: string,
255
+ destinationDirectory: string,
256
+ overwriteExisting: boolean,
257
+ ): number {
258
+ let copiedCount = 0;
259
+ const entries = fs.readdirSync(sourceDirectory, { withFileTypes: true });
260
+
261
+ for (const entry of entries) {
262
+ const sourceEntryPath = join(sourceDirectory, entry.name);
263
+ const destinationEntryPath = join(destinationDirectory, entry.name);
264
+
265
+ if (entry.isDirectory()) {
266
+ fs.mkdirSync(destinationEntryPath, { recursive: true });
267
+ copiedCount += syncPromptFiles(
268
+ sourceEntryPath,
269
+ destinationEntryPath,
270
+ overwriteExisting,
271
+ );
272
+ continue;
235
273
  }
236
274
 
237
- fs.rmSync(destinationPath, { recursive: true, force: true });
275
+ if (!entry.isFile()) {
276
+ continue;
277
+ }
278
+
279
+ if (!overwriteExisting && fs.existsSync(destinationEntryPath)) {
280
+ continue;
281
+ }
282
+
283
+ fs.copyFileSync(sourceEntryPath, destinationEntryPath);
284
+ copiedCount += 1;
238
285
  }
239
286
 
240
- fs.cpSync(sourcePath, destinationPath, { recursive: true });
241
- console.log("Prompts loaded → ~/.hive/prompts/");
287
+ return copiedCount;
242
288
  }
@@ -2,12 +2,17 @@ import * as fs from "node:fs";
2
2
  import { stdin, stdout } from "node:process";
3
3
  import { createInterface } from "node:readline/promises";
4
4
 
5
- import chalk from "chalk";
6
5
  import { Command } from "commander";
7
6
  import keytar from "keytar";
8
7
 
9
8
  import { SUPPORTED_PROVIDER_NAMES } from "../../providers/base.js";
10
9
  import { getHiveHomeDir } from "../../storage/db.js";
10
+ import {
11
+ renderError,
12
+ renderHiveHeader,
13
+ renderInfo,
14
+ renderSuccess,
15
+ } from "../ui.js";
11
16
 
12
17
  const KEYCHAIN_SERVICE = "hive";
13
18
  const NUKE_CONFIRMATION = "nuke";
@@ -22,10 +27,9 @@ export function registerNukeCommand(program: Command): void {
22
27
  }
23
28
 
24
29
  export async function runNukeCommand(): Promise<void> {
25
- console.log(
26
- chalk.red(
27
- "This will permanently delete your agent, all memory, all conversations, and all keys. This cannot be undone.",
28
- ),
30
+ renderHiveHeader("Nuke");
31
+ renderError(
32
+ "This will permanently delete your agent, all memory, all conversations, and all keys. This cannot be undone.",
29
33
  );
30
34
 
31
35
  const rl = createInterface({
@@ -42,7 +46,7 @@ export async function runNukeCommand(): Promise<void> {
42
46
  }
43
47
 
44
48
  if (confirmation !== NUKE_CONFIRMATION) {
45
- console.log(chalk.dim("Aborted."));
49
+ renderInfo("Aborted.");
46
50
  return;
47
51
  }
48
52
 
@@ -56,5 +60,5 @@ export async function runNukeCommand(): Promise<void> {
56
60
  }
57
61
  }
58
62
 
59
- console.log(chalk.green("The Hive has been nuked. Gone."));
63
+ renderSuccess("The Hive has been nuked. Gone.");
60
64
  }
@@ -5,6 +5,12 @@ import { join } from "node:path";
5
5
  import chalk from "chalk";
6
6
  import { Command } from "commander";
7
7
  import keytar from "keytar";
8
+ import {
9
+ renderHiveHeader,
10
+ renderInfo,
11
+ renderSeparator,
12
+ renderStep,
13
+ } from "../ui.js";
8
14
 
9
15
  import {
10
16
  closeHiveDatabase,
@@ -18,6 +24,10 @@ import {
18
24
  const KEYCHAIN_SERVICE = "hive";
19
25
  const PROMPTS_DIRECTORY = "prompts";
20
26
 
27
+ interface StatusCommandRenderOptions {
28
+ showHeader?: boolean;
29
+ }
30
+
21
31
  export function registerStatusCommand(program: Command): void {
22
32
  program
23
33
  .command("status")
@@ -28,12 +38,23 @@ export function registerStatusCommand(program: Command): void {
28
38
  }
29
39
 
30
40
  export async function runStatusCommand(): Promise<void> {
41
+ await runStatusCommandWithOptions();
42
+ }
43
+
44
+ export async function runStatusCommandWithOptions(
45
+ options: StatusCommandRenderOptions = {},
46
+ ): Promise<void> {
47
+ const showHeader = options.showHeader ?? true;
48
+ if (showHeader) {
49
+ renderHiveHeader("Status");
50
+ }
51
+
31
52
  const db = openHiveDatabase();
32
53
 
33
54
  try {
34
55
  const agent = getPrimaryAgent(db);
35
56
  if (!agent) {
36
- console.log("Hive is not initialized. Run `hive init` to get started.");
57
+ renderInfo("Hive is not initialized. Run `hive init` to get started.");
37
58
  return;
38
59
  }
39
60
 
@@ -45,14 +66,16 @@ export async function runStatusCommand(): Promise<void> {
45
66
  const promptFiles = countFiles(promptsPath);
46
67
  const initializedRaw = getMetaValue(db, "initialized_at") ?? agent.created_at;
47
68
 
48
- console.log(chalk.whiteBright("🐝 The Hive — Status"));
49
- console.log();
69
+ if (showHeader) {
70
+ renderStep("Status");
71
+ }
72
+ renderSeparator();
50
73
  printStatusLine("Agent", agent.agent_name ?? "not set");
51
74
  printStatusLine("Owner", agent.name);
52
75
  printStatusLine("Provider", provider);
53
76
  printStatusLine("Model", agent.model);
54
77
  printStatusLine("API Key", keyStatus);
55
- console.log();
78
+ renderSeparator();
56
79
  printStatusLine(
57
80
  "Database",
58
81
  `${displayPath(dbPath)} (${formatBytes(dbSizeBytes)})`,
@@ -61,7 +84,7 @@ export async function runStatusCommand(): Promise<void> {
61
84
  "Prompts",
62
85
  `${ensureTrailingSlash(displayPath(promptsPath))} (${promptFiles} files)`,
63
86
  );
64
- console.log();
87
+ renderSeparator();
65
88
  printStatusLine("Initialized", formatDate(initializedRaw));
66
89
  } finally {
67
90
  closeHiveDatabase(db);
package/src/cli/index.ts CHANGED
@@ -2,21 +2,21 @@
2
2
 
3
3
  import "dotenv/config";
4
4
 
5
- import chalk from "chalk";
6
5
  import { Command } from "commander";
7
6
 
8
- import { registerChatCommand } from "./commands/chat.js";
7
+ import { registerChatCommand, runChatCommand } from "./commands/chat.js";
9
8
  import { registerConfigCommand } from "./commands/config.js";
10
9
  import { registerInitCommand } from "./commands/init.js";
11
10
  import { registerNukeCommand } from "./commands/nuke.js";
12
11
  import { registerStatusCommand } from "./commands/status.js";
12
+ import { renderError, renderHiveHeader } from "./ui.js";
13
13
 
14
14
  const program = new Command();
15
15
 
16
16
  program
17
17
  .name("hive")
18
18
  .description("Your agent. Always running. Always learning. Always working.")
19
- .version("0.1.0");
19
+ .version("0.1.1");
20
20
 
21
21
  registerInitCommand(program);
22
22
  registerChatCommand(program);
@@ -24,15 +24,43 @@ registerConfigCommand(program);
24
24
  registerStatusCommand(program);
25
25
  registerNukeCommand(program);
26
26
 
27
- program
28
- .parseAsync(process.argv)
29
- .catch((error: unknown) => {
27
+ const argv = process.argv.slice(2);
28
+
29
+ void main();
30
+
31
+ async function main(): Promise<void> {
32
+ try {
33
+ if (argv.length === 0) {
34
+ await runChatCommand({}, { entrypoint: "default" });
35
+ return;
36
+ }
37
+
38
+ if (shouldRenderHelpHeader(argv)) {
39
+ renderHiveHeader(resolveHelpTitle(argv));
40
+ }
41
+
42
+ await program.parseAsync(process.argv);
43
+ } catch (error) {
30
44
  if (error instanceof Error) {
31
- console.error(chalk.red(error.message));
45
+ renderError(error.message);
32
46
  process.exitCode = 1;
33
47
  return;
34
48
  }
35
49
 
36
- console.error(chalk.red(String(error)));
50
+ renderError(String(error));
37
51
  process.exitCode = 1;
38
- });
52
+ }
53
+ }
54
+
55
+ function shouldRenderHelpHeader(args: string[]): boolean {
56
+ return args[0] === "help" || args.includes("-h") || args.includes("--help");
57
+ }
58
+
59
+ function resolveHelpTitle(args: string[]): string {
60
+ if (args[0] === "help") {
61
+ return args[1] ?? "Help";
62
+ }
63
+
64
+ const commandName = args.find((arg) => !arg.startsWith("-"));
65
+ return commandName ?? "Help";
66
+ }
package/src/cli/ui.ts ADDED
@@ -0,0 +1,120 @@
1
+ import { readFileSync } from "node:fs";
2
+ import process from "node:process";
3
+
4
+ import chalk from "chalk";
5
+
6
+ const WORDMARK_LINES = [
7
+ " ██╗ ██╗██╗██╗ ██╗███████╗",
8
+ " ██║ ██║██║██║ ██║██╔════╝",
9
+ " ███████║██║██║ ██║█████╗ ",
10
+ " ██╔══██║██║╚██╗ ██╔╝██╔══╝ ",
11
+ " ██║ ██║██║ ╚████╔╝ ███████╗",
12
+ " ╚═╝ ╚═╝╚═╝ ╚═══╝ ╚══════╝",
13
+ ] as const;
14
+
15
+ const COMMAND_CENTRE_LABEL = "COMMAND CENTRE";
16
+ const MAX_SEPARATOR_WIDTH = 72;
17
+ const MIN_SEPARATOR_WIDTH = 24;
18
+
19
+ let cachedVersion: string | null = null;
20
+
21
+ export function renderHiveHeader(pageTitle?: string): void {
22
+ const terminalWidth = getTerminalWidth();
23
+ const separator = "─".repeat(getSeparatorWidth(terminalWidth));
24
+
25
+ for (const line of WORDMARK_LINES) {
26
+ console.log(chalk.bold.whiteBright(centerText(line, terminalWidth)));
27
+ }
28
+
29
+ console.log("");
30
+ console.log(chalk.dim(centerText(`v${getCliVersion()}`, terminalWidth)));
31
+
32
+ const normalizedTitle = normalizePageTitle(pageTitle);
33
+ const commandCentreTitle = normalizedTitle
34
+ ? `${COMMAND_CENTRE_LABEL} · ${normalizedTitle}`
35
+ : COMMAND_CENTRE_LABEL;
36
+
37
+ console.log(chalk.whiteBright(centerText(commandCentreTitle, terminalWidth)));
38
+ console.log(chalk.dim(centerText(separator, terminalWidth)));
39
+ }
40
+
41
+ export function renderSuccess(message: string): void {
42
+ console.log(chalk.green(message));
43
+ }
44
+
45
+ export function renderError(message: string): void {
46
+ console.error(chalk.red(message));
47
+ }
48
+
49
+ export function renderStep(message: string): void {
50
+ console.log(chalk.whiteBright(message));
51
+ }
52
+
53
+ export function renderInfo(message: string): void {
54
+ console.log(chalk.dim(message));
55
+ }
56
+
57
+ export function renderSeparator(text?: string): void {
58
+ if (text) {
59
+ console.log(chalk.dim(text));
60
+ return;
61
+ }
62
+
63
+ console.log(chalk.dim("─".repeat(getSeparatorWidth(getTerminalWidth()))));
64
+ }
65
+
66
+ function getCliVersion(): string {
67
+ if (cachedVersion) {
68
+ return cachedVersion;
69
+ }
70
+
71
+ try {
72
+ const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf8");
73
+ const parsed = JSON.parse(raw) as { version?: unknown };
74
+ if (typeof parsed.version === "string" && parsed.version.trim().length > 0) {
75
+ cachedVersion = parsed.version.trim();
76
+ return cachedVersion;
77
+ }
78
+ } catch {
79
+ // Fall through to default when package metadata cannot be read.
80
+ }
81
+
82
+ cachedVersion = "0.0.0";
83
+ return cachedVersion;
84
+ }
85
+
86
+ function normalizePageTitle(pageTitle?: string): string {
87
+ const trimmed = pageTitle?.trim() ?? "";
88
+ if (trimmed.length === 0) {
89
+ return "";
90
+ }
91
+
92
+ return trimmed.toUpperCase();
93
+ }
94
+
95
+ function getTerminalWidth(): number {
96
+ if (!process.stdout.isTTY) {
97
+ return 80;
98
+ }
99
+
100
+ const columns = process.stdout.columns;
101
+ if (typeof columns !== "number" || columns < 20) {
102
+ return 80;
103
+ }
104
+
105
+ return columns;
106
+ }
107
+
108
+ function centerText(value: string, totalWidth: number): string {
109
+ if (value.length >= totalWidth) {
110
+ return value;
111
+ }
112
+
113
+ const leftPadding = Math.floor((totalWidth - value.length) / 2);
114
+ return `${" ".repeat(leftPadding)}${value}`;
115
+ }
116
+
117
+ function getSeparatorWidth(terminalWidth: number): number {
118
+ const usableWidth = Math.max(MIN_SEPARATOR_WIDTH, terminalWidth - 8);
119
+ return Math.min(MAX_SEPARATOR_WIDTH, usableWidth);
120
+ }