@imisbahk/hive 0.1.0

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 (132) hide show
  1. package/.rocket/ARCHITECTURE.md +7 -0
  2. package/.rocket/README.md +31 -0
  3. package/.rocket/SYMBOLS.md +282 -0
  4. package/.rocket/config.json +18 -0
  5. package/001-local-first-storage.md +43 -0
  6. package/003-memory-architechture.md +71 -0
  7. package/CONTRIBUTING.md +149 -0
  8. package/LICENSE.md +21 -0
  9. package/README.md +146 -0
  10. package/dist/agent/agent.d.ts +32 -0
  11. package/dist/agent/agent.d.ts.map +1 -0
  12. package/dist/agent/agent.js +103 -0
  13. package/dist/agent/agent.js.map +1 -0
  14. package/dist/agent/index.d.ts +3 -0
  15. package/dist/agent/index.d.ts.map +1 -0
  16. package/dist/agent/index.js +2 -0
  17. package/dist/agent/index.js.map +1 -0
  18. package/dist/cli/commands/chat.d.ts +12 -0
  19. package/dist/cli/commands/chat.d.ts.map +1 -0
  20. package/dist/cli/commands/chat.js +117 -0
  21. package/dist/cli/commands/chat.js.map +1 -0
  22. package/dist/cli/commands/config.d.ts +7 -0
  23. package/dist/cli/commands/config.d.ts.map +1 -0
  24. package/dist/cli/commands/config.js +234 -0
  25. package/dist/cli/commands/config.js.map +1 -0
  26. package/dist/cli/commands/init.d.ts +8 -0
  27. package/dist/cli/commands/init.d.ts.map +1 -0
  28. package/dist/cli/commands/init.js +186 -0
  29. package/dist/cli/commands/init.js.map +1 -0
  30. package/dist/cli/commands/nuke.d.ts +4 -0
  31. package/dist/cli/commands/nuke.d.ts.map +1 -0
  32. package/dist/cli/commands/nuke.js +47 -0
  33. package/dist/cli/commands/nuke.js.map +1 -0
  34. package/dist/cli/commands/status.d.ts +4 -0
  35. package/dist/cli/commands/status.d.ts.map +1 -0
  36. package/dist/cli/commands/status.js +114 -0
  37. package/dist/cli/commands/status.js.map +1 -0
  38. package/dist/cli/helpers/providerPrompts.d.ts +13 -0
  39. package/dist/cli/helpers/providerPrompts.d.ts.map +1 -0
  40. package/dist/cli/helpers/providerPrompts.js +138 -0
  41. package/dist/cli/helpers/providerPrompts.js.map +1 -0
  42. package/dist/cli/index.d.ts +3 -0
  43. package/dist/cli/index.d.ts.map +1 -0
  44. package/dist/cli/index.js +31 -0
  45. package/dist/cli/index.js.map +1 -0
  46. package/dist/providers/anthropic.d.ts +10 -0
  47. package/dist/providers/anthropic.d.ts.map +1 -0
  48. package/dist/providers/anthropic.js +108 -0
  49. package/dist/providers/anthropic.js.map +1 -0
  50. package/dist/providers/api-key.d.ts +3 -0
  51. package/dist/providers/api-key.d.ts.map +1 -0
  52. package/dist/providers/api-key.js +15 -0
  53. package/dist/providers/api-key.js.map +1 -0
  54. package/dist/providers/base.d.ts +41 -0
  55. package/dist/providers/base.d.ts.map +1 -0
  56. package/dist/providers/base.js +157 -0
  57. package/dist/providers/base.js.map +1 -0
  58. package/dist/providers/google.d.ts +6 -0
  59. package/dist/providers/google.d.ts.map +1 -0
  60. package/dist/providers/google.js +19 -0
  61. package/dist/providers/google.js.map +1 -0
  62. package/dist/providers/groq.d.ts +6 -0
  63. package/dist/providers/groq.d.ts.map +1 -0
  64. package/dist/providers/groq.js +19 -0
  65. package/dist/providers/groq.js.map +1 -0
  66. package/dist/providers/index.d.ts +4 -0
  67. package/dist/providers/index.d.ts.map +1 -0
  68. package/dist/providers/index.js +58 -0
  69. package/dist/providers/index.js.map +1 -0
  70. package/dist/providers/mistral.d.ts +6 -0
  71. package/dist/providers/mistral.d.ts.map +1 -0
  72. package/dist/providers/mistral.js +19 -0
  73. package/dist/providers/mistral.js.map +1 -0
  74. package/dist/providers/ollama.d.ts +6 -0
  75. package/dist/providers/ollama.d.ts.map +1 -0
  76. package/dist/providers/ollama.js +20 -0
  77. package/dist/providers/ollama.js.map +1 -0
  78. package/dist/providers/openai-compatible.d.ts +22 -0
  79. package/dist/providers/openai-compatible.d.ts.map +1 -0
  80. package/dist/providers/openai-compatible.js +36 -0
  81. package/dist/providers/openai-compatible.js.map +1 -0
  82. package/dist/providers/openai.d.ts +6 -0
  83. package/dist/providers/openai.d.ts.map +1 -0
  84. package/dist/providers/openai.js +19 -0
  85. package/dist/providers/openai.js.map +1 -0
  86. package/dist/providers/openrouter.d.ts +6 -0
  87. package/dist/providers/openrouter.d.ts.map +1 -0
  88. package/dist/providers/openrouter.js +19 -0
  89. package/dist/providers/openrouter.js.map +1 -0
  90. package/dist/providers/together.d.ts +6 -0
  91. package/dist/providers/together.d.ts.map +1 -0
  92. package/dist/providers/together.js +19 -0
  93. package/dist/providers/together.js.map +1 -0
  94. package/dist/storage/db.d.ts +48 -0
  95. package/dist/storage/db.d.ts.map +1 -0
  96. package/dist/storage/db.js +298 -0
  97. package/dist/storage/db.js.map +1 -0
  98. package/dist/storage/schema.d.ts +43 -0
  99. package/dist/storage/schema.d.ts.map +1 -0
  100. package/dist/storage/schema.js +69 -0
  101. package/dist/storage/schema.js.map +1 -0
  102. package/index.md +16 -0
  103. package/package.json +48 -0
  104. package/prompts/Behaviour.md +23 -0
  105. package/prompts/Code.md +12 -0
  106. package/prompts/Memory.md +11 -0
  107. package/prompts/System.md +6 -0
  108. package/releases/v1/v0.1/RELEASE-NOTES.md +0 -0
  109. package/src/agent/agent.ts +155 -0
  110. package/src/agent/index.ts +2 -0
  111. package/src/cli/commands/chat.ts +169 -0
  112. package/src/cli/commands/config.ts +282 -0
  113. package/src/cli/commands/init.ts +242 -0
  114. package/src/cli/commands/nuke.ts +60 -0
  115. package/src/cli/commands/status.ts +147 -0
  116. package/src/cli/helpers/providerPrompts.ts +192 -0
  117. package/src/cli/index.ts +38 -0
  118. package/src/providers/anthropic.ts +146 -0
  119. package/src/providers/api-key.ts +23 -0
  120. package/src/providers/base.ts +234 -0
  121. package/src/providers/google.ts +21 -0
  122. package/src/providers/groq.ts +21 -0
  123. package/src/providers/index.ts +65 -0
  124. package/src/providers/mistral.ts +21 -0
  125. package/src/providers/ollama.ts +22 -0
  126. package/src/providers/openai-compatible.ts +58 -0
  127. package/src/providers/openai.ts +21 -0
  128. package/src/providers/openrouter.ts +21 -0
  129. package/src/providers/together.ts +21 -0
  130. package/src/storage/db.ts +476 -0
  131. package/src/storage/schema.ts +116 -0
  132. package/tsconfig.json +51 -0
@@ -0,0 +1,242 @@
1
+ import process from "node:process";
2
+ import * as fs from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import inquirer from "inquirer";
8
+ import keytar from "keytar";
9
+ import ora from "ora";
10
+
11
+ import { buildDefaultPersona } from "../../agent/agent.js";
12
+ import { promptForModel, promptForProvider } from "../helpers/providerPrompts.js";
13
+ import {
14
+ closeHiveDatabase,
15
+ getHiveHomeDir,
16
+ getPrimaryAgent,
17
+ openHiveDatabase,
18
+ setMetaValue,
19
+ upsertPrimaryAgent,
20
+ } from "../../storage/db.js";
21
+ import type { ProviderName } from "../../providers/base.js";
22
+
23
+ interface InitAnswers {
24
+ name: string;
25
+ dob: string;
26
+ location: string;
27
+ profession: string;
28
+ aboutRaw: string;
29
+ provider: ProviderName;
30
+ model: string;
31
+ apiKey?: string;
32
+ agentName?: string;
33
+ }
34
+
35
+ interface InitCommandOptions {
36
+ force?: boolean;
37
+ }
38
+
39
+ const KEYCHAIN_SERVICE = "hive";
40
+
41
+ export function registerInitCommand(program: Command): void {
42
+ program
43
+ .command("init")
44
+ .description("Birth your local Hive agent")
45
+ .option("--force", "overwrite ~/.hive/prompts when loading prompts")
46
+ .action(async (options: InitCommandOptions) => {
47
+ await runInitCommand(options);
48
+ });
49
+ }
50
+
51
+ export async function runInitCommand(options: InitCommandOptions = {}): Promise<void> {
52
+ const spinner = ora("Preparing init...").start();
53
+ const db = openHiveDatabase();
54
+
55
+ try {
56
+ if (!process.stdin.isTTY) {
57
+ throw new Error("`hive init` requires an interactive terminal.");
58
+ }
59
+
60
+ const existing = getPrimaryAgent(db);
61
+ spinner.stop();
62
+
63
+ if (existing) {
64
+ const { reinitialize } = (await inquirer.prompt([
65
+ {
66
+ type: "confirm",
67
+ name: "reinitialize",
68
+ message: "Agent already exists. Reinitialize? (y/n)",
69
+ default: false,
70
+ },
71
+ ])) as { reinitialize: boolean };
72
+
73
+ if (!reinitialize) {
74
+ console.log(chalk.dim("Initialization cancelled."));
75
+ return;
76
+ }
77
+ }
78
+
79
+ const answers = await askInitQuestions();
80
+ spinner.start("Initializing...");
81
+
82
+ if (answers.provider !== "ollama" && answers.apiKey) {
83
+ await keytar.setPassword(KEYCHAIN_SERVICE, answers.provider, answers.apiKey);
84
+ }
85
+
86
+ const agent = upsertPrimaryAgent(db, {
87
+ name: answers.name,
88
+ provider: answers.provider,
89
+ model: answers.model,
90
+ persona: buildDefaultPersona(answers.name),
91
+ dob: answers.dob,
92
+ location: answers.location,
93
+ profession: answers.profession,
94
+ aboutRaw: answers.aboutRaw,
95
+ agentName: answers.agentName ?? null,
96
+ });
97
+
98
+ setMetaValue(db, "initialized_at", new Date().toISOString());
99
+ setMetaValue(db, "provider", agent.provider);
100
+ setMetaValue(db, "model", agent.model);
101
+ copyPromptsDirectory(options.force ?? false);
102
+
103
+ spinner.succeed("Initialization complete.");
104
+ console.log(chalk.green(`HIVE-ID: ${agent.id}`));
105
+ if (agent.agent_name) {
106
+ console.log(chalk.green(`Agent name: ${agent.agent_name}`));
107
+ }
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.");
111
+ } catch (error) {
112
+ if (spinner.isSpinning) {
113
+ spinner.fail("Hive initialization failed.");
114
+ }
115
+ throw error;
116
+ } finally {
117
+ closeHiveDatabase(db);
118
+ }
119
+ }
120
+
121
+ async function askInitQuestions(): Promise<InitAnswers> {
122
+ const { name } = (await inquirer.prompt([
123
+ {
124
+ type: "input",
125
+ name: "name",
126
+ message: "What's your name?",
127
+ validate: requiredField("Name is required."),
128
+ },
129
+ ])) as { name: string };
130
+
131
+ const { dob } = (await inquirer.prompt([
132
+ {
133
+ type: "input",
134
+ name: "dob",
135
+ message: "Date of birth? (DD/MM/YYYY)",
136
+ validate: (value: string) =>
137
+ /^\d{2}\/\d{2}\/\d{4}$/.test(value.trim()) || "Use DD/MM/YYYY format.",
138
+ },
139
+ ])) as { dob: string };
140
+
141
+ const { location } = (await inquirer.prompt([
142
+ {
143
+ type: "input",
144
+ name: "location",
145
+ message: "Where are you based?",
146
+ validate: requiredField("Location is required."),
147
+ },
148
+ ])) as { location: string };
149
+
150
+ const { profession } = (await inquirer.prompt([
151
+ {
152
+ type: "input",
153
+ name: "profession",
154
+ message: "What do you do?",
155
+ validate: requiredField("Profession is required."),
156
+ },
157
+ ])) as { profession: string };
158
+
159
+ const { aboutRaw } = (await inquirer.prompt([
160
+ {
161
+ type: "input",
162
+ name: "aboutRaw",
163
+ message:
164
+ "Tell me about yourself. Who you are, what you're building, what matters to you. No rules.",
165
+ validate: requiredField("About is required."),
166
+ },
167
+ ])) as { aboutRaw: string };
168
+
169
+ const provider = await promptForProvider();
170
+ const model = await promptForModel(provider);
171
+
172
+ let apiKey: string | undefined;
173
+ if (provider !== "ollama") {
174
+ const answer = (await inquirer.prompt([
175
+ {
176
+ type: "password",
177
+ name: "apiKey",
178
+ message: "Enter your API key:",
179
+ mask: "*",
180
+ validate: requiredField("API key is required."),
181
+ },
182
+ ])) as { apiKey: string };
183
+
184
+ apiKey = answer.apiKey.trim();
185
+ }
186
+
187
+ const { agentName } = (await inquirer.prompt([
188
+ {
189
+ type: "input",
190
+ name: "agentName",
191
+ message: "What do you want to call your agent? (optional)",
192
+ },
193
+ ])) as { agentName: string };
194
+
195
+ return {
196
+ name: name.trim(),
197
+ dob: dob.trim(),
198
+ location: location.trim(),
199
+ profession: profession.trim(),
200
+ aboutRaw,
201
+ provider,
202
+ model,
203
+ apiKey,
204
+ agentName: normalizeOptional(agentName),
205
+ };
206
+ }
207
+
208
+ function requiredField(message: string): (value: string) => true | string {
209
+ return (value: string) => {
210
+ if (value.trim().length > 0) {
211
+ return true;
212
+ }
213
+
214
+ return message;
215
+ };
216
+ }
217
+
218
+ function normalizeOptional(value: string): string | undefined {
219
+ const trimmed = value.trim();
220
+ return trimmed.length > 0 ? trimmed : undefined;
221
+ }
222
+
223
+ function copyPromptsDirectory(force: boolean): void {
224
+ const sourcePath = join(process.cwd(), "prompts");
225
+ const destinationPath = join(getHiveHomeDir(), "prompts");
226
+
227
+ if (!fs.existsSync(sourcePath)) {
228
+ console.warn(chalk.yellow("Warning: prompts/ folder not found. Skipping prompts load."));
229
+ return;
230
+ }
231
+
232
+ if (fs.existsSync(destinationPath)) {
233
+ if (!force) {
234
+ return;
235
+ }
236
+
237
+ fs.rmSync(destinationPath, { recursive: true, force: true });
238
+ }
239
+
240
+ fs.cpSync(sourcePath, destinationPath, { recursive: true });
241
+ console.log("Prompts loaded → ~/.hive/prompts/");
242
+ }
@@ -0,0 +1,60 @@
1
+ import * as fs from "node:fs";
2
+ import { stdin, stdout } from "node:process";
3
+ import { createInterface } from "node:readline/promises";
4
+
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import keytar from "keytar";
8
+
9
+ import { SUPPORTED_PROVIDER_NAMES } from "../../providers/base.js";
10
+ import { getHiveHomeDir } from "../../storage/db.js";
11
+
12
+ const KEYCHAIN_SERVICE = "hive";
13
+ const NUKE_CONFIRMATION = "nuke";
14
+
15
+ export function registerNukeCommand(program: Command): void {
16
+ program
17
+ .command("nuke")
18
+ .description("Permanently delete your local Hive data and keys")
19
+ .action(async () => {
20
+ await runNukeCommand();
21
+ });
22
+ }
23
+
24
+ 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
+ ),
29
+ );
30
+
31
+ const rl = createInterface({
32
+ input: stdin,
33
+ output: stdout,
34
+ terminal: true,
35
+ });
36
+
37
+ let confirmation = "";
38
+ try {
39
+ confirmation = (await rl.question('Are you sure? Type "nuke" to confirm: ')).trim();
40
+ } finally {
41
+ rl.close();
42
+ }
43
+
44
+ if (confirmation !== NUKE_CONFIRMATION) {
45
+ console.log(chalk.dim("Aborted."));
46
+ return;
47
+ }
48
+
49
+ fs.rmSync(getHiveHomeDir(), { recursive: true, force: true });
50
+
51
+ for (const providerName of SUPPORTED_PROVIDER_NAMES) {
52
+ try {
53
+ await keytar.deletePassword(KEYCHAIN_SERVICE, providerName);
54
+ } catch {
55
+ // Missing or inaccessible keychain entries are non-fatal for nuke.
56
+ }
57
+ }
58
+
59
+ console.log(chalk.green("The Hive has been nuked. Gone."));
60
+ }
@@ -0,0 +1,147 @@
1
+ import * as fs from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import keytar from "keytar";
8
+
9
+ import {
10
+ closeHiveDatabase,
11
+ getHiveDatabasePath,
12
+ getHiveHomeDir,
13
+ getMetaValue,
14
+ getPrimaryAgent,
15
+ openHiveDatabase,
16
+ } from "../../storage/db.js";
17
+
18
+ const KEYCHAIN_SERVICE = "hive";
19
+ const PROMPTS_DIRECTORY = "prompts";
20
+
21
+ export function registerStatusCommand(program: Command): void {
22
+ program
23
+ .command("status")
24
+ .description("Show Hive setup status")
25
+ .action(async () => {
26
+ await runStatusCommand();
27
+ });
28
+ }
29
+
30
+ export async function runStatusCommand(): Promise<void> {
31
+ const db = openHiveDatabase();
32
+
33
+ try {
34
+ const agent = getPrimaryAgent(db);
35
+ if (!agent) {
36
+ console.log("Hive is not initialized. Run `hive init` to get started.");
37
+ return;
38
+ }
39
+
40
+ const provider = agent.provider;
41
+ const keyStatus = await getApiKeyStatus(provider);
42
+ const dbPath = getHiveDatabasePath();
43
+ const promptsPath = join(getHiveHomeDir(), PROMPTS_DIRECTORY);
44
+ const dbSizeBytes = getFileSize(dbPath);
45
+ const promptFiles = countFiles(promptsPath);
46
+ const initializedRaw = getMetaValue(db, "initialized_at") ?? agent.created_at;
47
+
48
+ console.log(chalk.whiteBright("🐝 The Hive — Status"));
49
+ console.log();
50
+ printStatusLine("Agent", agent.agent_name ?? "not set");
51
+ printStatusLine("Owner", agent.name);
52
+ printStatusLine("Provider", provider);
53
+ printStatusLine("Model", agent.model);
54
+ printStatusLine("API Key", keyStatus);
55
+ console.log();
56
+ printStatusLine(
57
+ "Database",
58
+ `${displayPath(dbPath)} (${formatBytes(dbSizeBytes)})`,
59
+ );
60
+ printStatusLine(
61
+ "Prompts",
62
+ `${ensureTrailingSlash(displayPath(promptsPath))} (${promptFiles} files)`,
63
+ );
64
+ console.log();
65
+ printStatusLine("Initialized", formatDate(initializedRaw));
66
+ } finally {
67
+ closeHiveDatabase(db);
68
+ }
69
+ }
70
+
71
+ async function getApiKeyStatus(providerName: string): Promise<string> {
72
+ let key: string | null = null;
73
+
74
+ try {
75
+ key = await keytar.getPassword(KEYCHAIN_SERVICE, providerName);
76
+ } catch {
77
+ key = null;
78
+ }
79
+
80
+ return key && key.trim().length > 0 ? "set ✓" : "not set ✗";
81
+ }
82
+
83
+ function printStatusLine(label: string, value: string): void {
84
+ const paddedLabel = `${label}:`.padEnd(10, " ");
85
+ console.log(`${chalk.dim(paddedLabel)} ${chalk.whiteBright(value)}`);
86
+ }
87
+
88
+ function getFileSize(path: string): number {
89
+ try {
90
+ return fs.statSync(path).size;
91
+ } catch {
92
+ return 0;
93
+ }
94
+ }
95
+
96
+ function countFiles(path: string): number {
97
+ try {
98
+ return fs
99
+ .readdirSync(path, { withFileTypes: true })
100
+ .filter((entry) => entry.isFile()).length;
101
+ } catch {
102
+ return 0;
103
+ }
104
+ }
105
+
106
+ function formatBytes(bytes: number): string {
107
+ const units = ["B", "KB", "MB", "GB", "TB"];
108
+ let unitIndex = 0;
109
+ let value = bytes;
110
+
111
+ while (value >= 1024 && unitIndex < units.length - 1) {
112
+ value /= 1024;
113
+ unitIndex += 1;
114
+ }
115
+
116
+ if (unitIndex === 0) {
117
+ return `${value} ${units[unitIndex]}`;
118
+ }
119
+
120
+ return `${value.toFixed(1)} ${units[unitIndex]}`;
121
+ }
122
+
123
+ function formatDate(value: string): string {
124
+ const date = new Date(value);
125
+ if (Number.isNaN(date.getTime())) {
126
+ return value;
127
+ }
128
+
129
+ return date.toISOString().slice(0, 10);
130
+ }
131
+
132
+ function displayPath(path: string): string {
133
+ const home = homedir();
134
+ if (path === home) {
135
+ return "~";
136
+ }
137
+
138
+ if (path.startsWith(`${home}/`)) {
139
+ return `~/${path.slice(home.length + 1)}`;
140
+ }
141
+
142
+ return path;
143
+ }
144
+
145
+ function ensureTrailingSlash(path: string): string {
146
+ return path.endsWith("/") ? path : `${path}/`;
147
+ }
@@ -0,0 +1,192 @@
1
+ import inquirer from "inquirer";
2
+ import fetch from "node-fetch";
3
+
4
+ import { SUPPORTED_PROVIDER_NAMES, type ProviderName } from "../../providers/base.js";
5
+
6
+ type HostedProviderName = Exclude<ProviderName, "ollama">;
7
+
8
+ const OLLAMA_TAGS_URL = "http://localhost:11434/api/tags";
9
+
10
+ const MODEL_CHOICES_BY_PROVIDER: Record<HostedProviderName, readonly string[]> = {
11
+ openai: ["gpt-4o", "gpt-4o-mini", "o1"],
12
+ anthropic: [
13
+ "claude-opus-4-6",
14
+ "claude-sonnet-4-6",
15
+ "claude-haiku-4-5-20251001",
16
+ ],
17
+ groq: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768"],
18
+ mistral: ["mistral-large-latest", "mistral-small-latest", "codestral-latest"],
19
+ google: ["gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-2.0-pro-exp-02-05"],
20
+ openrouter: [
21
+ "openai/gpt-4o-mini",
22
+ "anthropic/claude-3.5-sonnet",
23
+ "google/gemini-2.0-flash-001",
24
+ ],
25
+ together: [
26
+ "meta-llama/Llama-3.3-70B-Instruct-Turbo",
27
+ "meta-llama/Llama-3.1-8B-Instruct-Turbo",
28
+ "Qwen/Qwen2.5-Coder-32B-Instruct",
29
+ ],
30
+ };
31
+
32
+ interface OllamaTagsResponse {
33
+ models?: Array<{
34
+ name?: string;
35
+ model?: string;
36
+ }>;
37
+ }
38
+
39
+ interface PromptForProviderOptions {
40
+ message?: string;
41
+ defaultProvider?: ProviderName;
42
+ }
43
+
44
+ interface PromptForModelOptions {
45
+ message?: string;
46
+ defaultModel?: string;
47
+ }
48
+
49
+ export async function promptForProvider(
50
+ options: PromptForProviderOptions = {},
51
+ ): Promise<ProviderName> {
52
+ const answer = (await inquirer.prompt([
53
+ {
54
+ type: "list",
55
+ name: "provider",
56
+ message: options.message ?? "Choose a provider",
57
+ choices: SUPPORTED_PROVIDER_NAMES.map((value) => ({
58
+ name: value,
59
+ value,
60
+ })),
61
+ default: options.defaultProvider,
62
+ },
63
+ ])) as { provider: ProviderName };
64
+
65
+ return answer.provider;
66
+ }
67
+
68
+ export async function promptForModel(
69
+ provider: ProviderName,
70
+ options: PromptForModelOptions = {},
71
+ ): Promise<string> {
72
+ const promptMessage = options.message ?? "Choose a model";
73
+
74
+ if (provider === "ollama") {
75
+ const ollamaModels = await fetchOllamaModels();
76
+
77
+ if (ollamaModels && ollamaModels.length > 0) {
78
+ const defaultSelection =
79
+ options.defaultModel && ollamaModels.includes(options.defaultModel)
80
+ ? [options.defaultModel]
81
+ : undefined;
82
+
83
+ const answer = (await inquirer.prompt([
84
+ {
85
+ type: "checkbox",
86
+ name: "model",
87
+ message: promptMessage,
88
+ choices: ollamaModels.map((value) => ({
89
+ name: value,
90
+ value,
91
+ })),
92
+ default: defaultSelection,
93
+ validate: (values: string[]) =>
94
+ values.length === 1 || "Select exactly one model.",
95
+ },
96
+ ])) as { model: string[] };
97
+
98
+ return answer.model[0];
99
+ }
100
+
101
+ const fallbackMessage =
102
+ ollamaModels === null
103
+ ? "Ollama not detected. Enter model name manually:"
104
+ : "No local Ollama models found. Enter model name manually:";
105
+
106
+ const answer = (await inquirer.prompt([
107
+ {
108
+ type: "input",
109
+ name: "model",
110
+ message: fallbackMessage,
111
+ validate: requiredField("Model is required."),
112
+ },
113
+ ])) as { model: string };
114
+
115
+ return answer.model.trim();
116
+ }
117
+
118
+ const modelChoices = MODEL_CHOICES_BY_PROVIDER[provider];
119
+ const defaultModel =
120
+ options.defaultModel && modelChoices.includes(options.defaultModel)
121
+ ? options.defaultModel
122
+ : undefined;
123
+
124
+ const answer = (await inquirer.prompt([
125
+ {
126
+ type: "list",
127
+ name: "model",
128
+ message: promptMessage,
129
+ choices: modelChoices.map((value) => ({
130
+ name: value,
131
+ value,
132
+ })),
133
+ default: defaultModel,
134
+ },
135
+ ])) as { model: string };
136
+
137
+ return answer.model;
138
+ }
139
+
140
+ async function fetchOllamaModels(): Promise<string[] | null> {
141
+ const controller = new AbortController();
142
+ const timeout = setTimeout(() => {
143
+ controller.abort();
144
+ }, 1000);
145
+
146
+ try {
147
+ const response = await fetch(OLLAMA_TAGS_URL, {
148
+ signal: controller.signal,
149
+ });
150
+
151
+ if (!response.ok) {
152
+ return null;
153
+ }
154
+
155
+ const payload = (await response.json()) as OllamaTagsResponse;
156
+ if (!Array.isArray(payload.models)) {
157
+ return [];
158
+ }
159
+
160
+ return Array.from(
161
+ new Set(
162
+ payload.models
163
+ .map((entry) => {
164
+ if (typeof entry.name === "string" && entry.name.trim().length > 0) {
165
+ return entry.name.trim();
166
+ }
167
+
168
+ if (typeof entry.model === "string" && entry.model.trim().length > 0) {
169
+ return entry.model.trim();
170
+ }
171
+
172
+ return "";
173
+ })
174
+ .filter((value) => value.length > 0),
175
+ ),
176
+ );
177
+ } catch {
178
+ return null;
179
+ } finally {
180
+ clearTimeout(timeout);
181
+ }
182
+ }
183
+
184
+ function requiredField(message: string): (value: string) => true | string {
185
+ return (value: string) => {
186
+ if (value.trim().length > 0) {
187
+ return true;
188
+ }
189
+
190
+ return message;
191
+ };
192
+ }
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "dotenv/config";
4
+
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+
8
+ import { registerChatCommand } from "./commands/chat.js";
9
+ import { registerConfigCommand } from "./commands/config.js";
10
+ import { registerInitCommand } from "./commands/init.js";
11
+ import { registerNukeCommand } from "./commands/nuke.js";
12
+ import { registerStatusCommand } from "./commands/status.js";
13
+
14
+ const program = new Command();
15
+
16
+ program
17
+ .name("hive")
18
+ .description("Your agent. Always running. Always learning. Always working.")
19
+ .version("0.1.0");
20
+
21
+ registerInitCommand(program);
22
+ registerChatCommand(program);
23
+ registerConfigCommand(program);
24
+ registerStatusCommand(program);
25
+ registerNukeCommand(program);
26
+
27
+ program
28
+ .parseAsync(process.argv)
29
+ .catch((error: unknown) => {
30
+ if (error instanceof Error) {
31
+ console.error(chalk.red(error.message));
32
+ process.exitCode = 1;
33
+ return;
34
+ }
35
+
36
+ console.error(chalk.red(String(error)));
37
+ process.exitCode = 1;
38
+ });