@imisbahk/hive 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.gitattributes +7 -0
  2. package/.rocket/README.md +9 -9
  3. package/dist/agent/agent.d.ts +4 -0
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +40 -4
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/cli/commands/chat.d.ts.map +1 -1
  8. package/dist/cli/commands/chat.js +642 -12
  9. package/dist/cli/commands/chat.js.map +1 -1
  10. package/dist/cli/commands/memory.d.ts +3 -0
  11. package/dist/cli/commands/memory.d.ts.map +1 -0
  12. package/dist/cli/commands/memory.js +104 -0
  13. package/dist/cli/commands/memory.js.map +1 -0
  14. package/dist/cli/index.js +3 -1
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/providers/api-key.js +1 -1
  17. package/dist/providers/api-key.js.map +1 -1
  18. package/dist/providers/base.js +1 -1
  19. package/dist/providers/base.js.map +1 -1
  20. package/dist/providers/openai-compatible.js +2 -2
  21. package/dist/providers/openai-compatible.js.map +1 -1
  22. package/dist/storage/db.d.ts +31 -2
  23. package/dist/storage/db.d.ts.map +1 -1
  24. package/dist/storage/db.js +165 -3
  25. package/dist/storage/db.js.map +1 -1
  26. package/dist/storage/schema.d.ts +12 -1
  27. package/dist/storage/schema.d.ts.map +1 -1
  28. package/dist/storage/schema.js +29 -1
  29. package/dist/storage/schema.js.map +1 -1
  30. package/package.json +8 -2
  31. package/.github/workflows/publish.yml +0 -31
  32. package/.rocket/ARCHITECTURE.md +0 -7
  33. package/.rocket/SYMBOLS.md +0 -425
  34. package/001-local-first-storage.md +0 -43
  35. package/003-memory-architechture.md +0 -71
  36. package/CONTRIBUTING.md +0 -150
  37. package/FEATURES.md +0 -63
  38. package/index.md +0 -16
  39. package/prompts/Behaviour.md +0 -23
  40. package/prompts/Browser.md +0 -13
  41. package/prompts/Code.md +0 -12
  42. package/prompts/Debugging.md +0 -15
  43. package/prompts/Execution.md +0 -13
  44. package/prompts/Memory.md +0 -11
  45. package/prompts/Planning.md +0 -13
  46. package/prompts/Product.md +0 -14
  47. package/prompts/Review.md +0 -15
  48. package/prompts/Safety.md +0 -12
  49. package/prompts/Search.md +0 -14
  50. package/prompts/System.md +0 -6
  51. package/prompts/Tools.md +0 -14
  52. package/prompts/Writing.md +0 -13
  53. package/releases/v1/v0.1/RELEASE-NOTES.md +0 -101
  54. package/src/agent/agent.ts +0 -595
  55. package/src/agent/index.ts +0 -2
  56. package/src/browser/browser.ts +0 -410
  57. package/src/cli/commands/chat.ts +0 -864
  58. package/src/cli/commands/config.ts +0 -610
  59. package/src/cli/commands/doctor.ts +0 -655
  60. package/src/cli/commands/init.ts +0 -288
  61. package/src/cli/commands/nuke.ts +0 -64
  62. package/src/cli/commands/status.ts +0 -170
  63. package/src/cli/helpers/providerPrompts.ts +0 -192
  64. package/src/cli/index.ts +0 -68
  65. package/src/cli/theme.ts +0 -88
  66. package/src/cli/ui.ts +0 -127
  67. package/src/providers/anthropic.ts +0 -146
  68. package/src/providers/api-key.ts +0 -23
  69. package/src/providers/base.ts +0 -409
  70. package/src/providers/google.ts +0 -21
  71. package/src/providers/groq.ts +0 -21
  72. package/src/providers/index.ts +0 -65
  73. package/src/providers/mistral.ts +0 -21
  74. package/src/providers/ollama.ts +0 -22
  75. package/src/providers/openai-compatible.ts +0 -82
  76. package/src/providers/openai.ts +0 -21
  77. package/src/providers/openrouter.ts +0 -21
  78. package/src/providers/together.ts +0 -21
  79. package/src/storage/db.ts +0 -476
  80. package/src/storage/schema.ts +0 -116
  81. package/tsconfig.json +0 -51
@@ -1,655 +0,0 @@
1
- import * as fs from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import process from "node:process";
5
-
6
- import Database from "better-sqlite3";
7
- import { Command } from "commander";
8
- import fetch from "node-fetch";
9
- import keytar from "keytar";
10
-
11
- import { normalizeProviderName, type ProviderName } from "../../providers/base.js";
12
- import {
13
- closeHiveDatabase,
14
- getHiveDatabasePath,
15
- getHiveHomeDir,
16
- getMetaValue,
17
- getPrimaryAgent,
18
- type HiveDatabase,
19
- } from "../../storage/db.js";
20
- import {
21
- BUILT_IN_THEMES,
22
- DEFAULT_THEME_HEX,
23
- DEFAULT_THEME_NAME,
24
- isValidHexColor,
25
- type ThemeName,
26
- } from "../theme.js";
27
- import { renderError, renderHiveHeader, renderInfo, renderSuccess } from "../ui.js";
28
-
29
- const KEYCHAIN_SERVICE = "hive";
30
- const PROMPTS_DIRECTORY = "prompts";
31
- const PROVIDER_PING_TIMEOUT_MS = 5_000;
32
- const OLLAMA_PING_TIMEOUT_MS = 5_000;
33
- const DB_SIZE_WARNING_BYTES = 100 * 1024 * 1024;
34
- const NODE_MAJOR_WARNING_VERSION = 20;
35
- const CHECK_LABEL_WIDTH = 22;
36
-
37
- interface DoctorOptions {
38
- showHeader?: boolean;
39
- }
40
-
41
- interface ThemeDetails {
42
- name: ThemeName;
43
- hex: string;
44
- }
45
-
46
- interface CheckCounter {
47
- warnings: number;
48
- errors: number;
49
- }
50
-
51
- export function registerDoctorCommand(program: Command): void {
52
- program
53
- .command("doctor")
54
- .description("Run a full Hive health check")
55
- .action(async () => {
56
- await runDoctorCommand();
57
- });
58
- }
59
-
60
- export async function runDoctorCommand(options: DoctorOptions = {}): Promise<void> {
61
- const showHeader = options.showHeader ?? true;
62
- if (showHeader) {
63
- renderHiveHeader("Doctor");
64
- }
65
-
66
- renderInfo("");
67
- renderInfo("Running diagnostics...");
68
- renderInfo("");
69
-
70
- const counters: CheckCounter = { warnings: 0, errors: 0 };
71
- const dbPath = getHiveDatabasePath();
72
- const promptsPath = join(getHiveHomeDir(), PROMPTS_DIRECTORY);
73
-
74
- let dbSizeBytes = 0;
75
- let db: HiveDatabase | null = null;
76
- let providerName: ProviderName | null = null;
77
- let providerLookupError: string | null = null;
78
- let keychainApiKey: string | null = null;
79
-
80
- const databaseCheck = checkDatabase(dbPath);
81
- dbSizeBytes = databaseCheck.sizeBytes;
82
- db = databaseCheck.ok ? databaseCheck.db : null;
83
-
84
- let agentName = "missing";
85
- if (db) {
86
- const agent = getPrimaryAgent(db);
87
- if (agent) {
88
- agentName = agent.agent_name?.trim() ? agent.agent_name : agent.name;
89
- renderSuccess(formatCheckLine("Agent initialized", agentName));
90
-
91
- try {
92
- providerName = normalizeProviderName(agent.provider);
93
- } catch {
94
- providerName = null;
95
- providerLookupError = `unsupported (${agent.provider})`;
96
- }
97
- } else {
98
- renderFailure("Agent initialized", "not initialized", counters);
99
- }
100
- } else {
101
- renderFailure("Agent initialized", "not checked (database unavailable)", counters);
102
- }
103
-
104
- if (databaseCheck.ok) {
105
- renderSuccess(formatCheckLine("Database", `${displayPath(dbPath)} (${formatBytes(dbSizeBytes)})`));
106
- } else {
107
- renderFailure("Database", databaseCheck.message, counters);
108
- }
109
-
110
- if (dbSizeBytes > DB_SIZE_WARNING_BYTES) {
111
- renderWarning(
112
- "Database size",
113
- `${formatBytes(dbSizeBytes)} exceeds ${formatBytes(DB_SIZE_WARNING_BYTES)}`,
114
- counters,
115
- );
116
- }
117
-
118
- if (providerName) {
119
- if (providerName === "ollama") {
120
- renderSuccess(formatCheckLine("API Key", "not required (ollama)"));
121
- } else {
122
- keychainApiKey = await readKeychainApiKey(providerName);
123
- if (keychainApiKey && keychainApiKey.trim().length > 0) {
124
- renderSuccess(formatCheckLine("API Key", "set"));
125
- } else {
126
- renderFailure("API Key", "missing in keychain", counters);
127
- }
128
- }
129
- } else if (providerLookupError) {
130
- renderFailure("API Key", "not checked (provider unsupported)", counters);
131
- } else {
132
- renderFailure("API Key", "not checked (provider unavailable)", counters);
133
- }
134
-
135
- if (providerName) {
136
- const providerReachable = await checkProviderReachable(providerName, keychainApiKey);
137
- if (providerReachable.ok) {
138
- renderSuccess(
139
- formatCheckLine("Provider", `${providerName} — reachable`),
140
- );
141
- } else if (providerReachable.warning) {
142
- renderWarning(
143
- "Provider",
144
- `${providerName} — ${providerReachable.message}`,
145
- counters,
146
- );
147
- } else {
148
- renderFailure("Provider", `${providerName} — ${providerReachable.message}`, counters);
149
- }
150
- } else if (providerLookupError) {
151
- renderFailure("Provider", providerLookupError, counters);
152
- } else {
153
- renderFailure("Provider", "not checked (provider unavailable)", counters);
154
- }
155
-
156
- const promptsCheck = checkPromptsDirectory(promptsPath);
157
- if (promptsCheck.ok) {
158
- renderSuccess(
159
- formatCheckLine("Prompts", `${promptsCheck.fileCount} files loaded`),
160
- );
161
- } else {
162
- renderFailure("Prompts", promptsCheck.message, counters);
163
- }
164
-
165
- if (db) {
166
- const theme = resolveThemeDetails(db);
167
- renderSuccess(formatCheckLine("Theme", `${theme.name} ${theme.hex}`));
168
- } else {
169
- renderFailure("Theme", "not checked (database unavailable)", counters);
170
- }
171
-
172
- const nodeCheck = checkNodeVersion(process.version);
173
- if (nodeCheck.ok) {
174
- renderSuccess(formatCheckLine("Node version", process.version));
175
- } else {
176
- renderWarning("Node version", nodeCheck.message, counters);
177
- }
178
-
179
- const playwrightCheck = await checkPlaywrightInstallation();
180
- if (playwrightCheck.ok) {
181
- renderSuccess(formatCheckLine("Playwright", "chromium installed"));
182
- } else {
183
- renderFailure("Playwright", playwrightCheck.message, counters);
184
- }
185
-
186
- if (providerName === "ollama") {
187
- const ollamaCheck = await checkOllamaRunning();
188
- if (ollamaCheck.ok) {
189
- renderSuccess(formatCheckLine("Ollama", "running"));
190
- } else {
191
- renderWarning("Ollama", "not running", counters);
192
- }
193
- }
194
-
195
- if (db) {
196
- const messageCount = countRowsIfTableExists(db, "messages");
197
- const conversationCount = countRowsIfTableExists(db, "conversations");
198
- const episodeCount = countRowsIfTableExists(db, "episodes");
199
-
200
- if (episodeCount === null) {
201
- renderInfo(formatInfoLine("Memory", "episodes table not found"));
202
- } else {
203
- renderInfo(formatInfoLine("Memory", `${episodeCount} episodes stored`));
204
- }
205
-
206
- if (messageCount === null) {
207
- renderInfo(formatInfoLine("Messages", "messages table not found"));
208
- } else {
209
- renderInfo(formatInfoLine("Messages", `${messageCount} total`));
210
- }
211
-
212
- if (conversationCount === null) {
213
- renderInfo(formatInfoLine("Conversations", "conversations table not found"));
214
- } else {
215
- renderInfo(formatInfoLine("Conversations", `${conversationCount} total`));
216
- }
217
- } else {
218
- renderInfo(formatInfoLine("Memory", "not checked"));
219
- renderInfo(formatInfoLine("Conversations", "not checked"));
220
- }
221
-
222
- renderInfo("");
223
- renderSummary(counters);
224
-
225
- if (db) {
226
- closeHiveDatabase(db);
227
- }
228
- }
229
-
230
- function checkDatabase(
231
- databasePath: string,
232
- ): { ok: true; db: HiveDatabase; sizeBytes: number } | { ok: false; message: string; sizeBytes: number } {
233
- if (!fs.existsSync(databasePath)) {
234
- return {
235
- ok: false,
236
- message: `${displayPath(databasePath)} not found`,
237
- sizeBytes: 0,
238
- };
239
- }
240
-
241
- const stats = fs.statSync(databasePath);
242
- if (!stats.isFile()) {
243
- return {
244
- ok: false,
245
- message: `${displayPath(databasePath)} is not a file`,
246
- sizeBytes: 0,
247
- };
248
- }
249
-
250
- try {
251
- const db = new Database(databasePath, {
252
- readonly: true,
253
- fileMustExist: true,
254
- });
255
-
256
- const integrity = db.pragma("integrity_check", { simple: true });
257
- if (integrity !== "ok") {
258
- db.close();
259
- return {
260
- ok: false,
261
- message: `integrity check failed (${String(integrity)})`,
262
- sizeBytes: stats.size,
263
- };
264
- }
265
-
266
- return { ok: true, db, sizeBytes: stats.size };
267
- } catch (error) {
268
- return {
269
- ok: false,
270
- message: `unreadable (${formatError(error)})`,
271
- sizeBytes: stats.size,
272
- };
273
- }
274
- }
275
-
276
- async function readKeychainApiKey(providerName: ProviderName): Promise<string | null> {
277
- try {
278
- return await keytar.getPassword(KEYCHAIN_SERVICE, providerName);
279
- } catch {
280
- return null;
281
- }
282
- }
283
-
284
- async function checkProviderReachable(
285
- providerName: ProviderName,
286
- apiKey: string | null,
287
- ): Promise<{ ok: boolean; warning?: boolean; message?: string }> {
288
- const target = buildProviderPingTarget(providerName, apiKey);
289
- if (!target) {
290
- return {
291
- ok: false,
292
- message: "missing API key",
293
- };
294
- }
295
-
296
- try {
297
- const response = await fetchWithTimeout(target.url, {
298
- method: "GET",
299
- headers: target.headers,
300
- }, PROVIDER_PING_TIMEOUT_MS);
301
-
302
- if (response.ok) {
303
- return { ok: true };
304
- }
305
-
306
- if (response.status === 401 || response.status === 403) {
307
- return { ok: false, message: `auth failed (${response.status})` };
308
- }
309
-
310
- if (response.status >= 500) {
311
- return { ok: false, warning: true, message: `service unavailable (${response.status})` };
312
- }
313
-
314
- return { ok: false, message: `HTTP ${response.status}` };
315
- } catch (error) {
316
- return {
317
- ok: false,
318
- warning: isTimeoutError(error),
319
- message: isTimeoutError(error) ? "timeout after 5s" : formatError(error),
320
- };
321
- }
322
- }
323
-
324
- function buildProviderPingTarget(
325
- providerName: ProviderName,
326
- apiKey: string | null,
327
- ): { url: string; headers?: Record<string, string> } | null {
328
- switch (providerName) {
329
- case "openai":
330
- return buildOpenAICompatiblePing(
331
- process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1",
332
- apiKey,
333
- );
334
- case "google":
335
- return buildOpenAICompatiblePing(
336
- process.env.GOOGLE_BASE_URL ?? "https://generativelanguage.googleapis.com/v1beta/openai",
337
- apiKey,
338
- );
339
- case "groq":
340
- return buildOpenAICompatiblePing(
341
- process.env.GROQ_BASE_URL ?? "https://api.groq.com/openai/v1",
342
- apiKey,
343
- );
344
- case "mistral":
345
- return buildOpenAICompatiblePing(
346
- process.env.MISTRAL_BASE_URL ?? "https://api.mistral.ai/v1",
347
- apiKey,
348
- );
349
- case "openrouter":
350
- return buildOpenAICompatiblePing(
351
- process.env.OPENROUTER_BASE_URL ?? "https://openrouter.ai/api/v1",
352
- apiKey,
353
- );
354
- case "together":
355
- return buildOpenAICompatiblePing(
356
- process.env.TOGETHER_BASE_URL ?? "https://api.together.xyz/v1",
357
- apiKey,
358
- );
359
- case "ollama":
360
- return {
361
- url: "http://localhost:11434/api/tags",
362
- };
363
- case "anthropic":
364
- if (!apiKey || apiKey.trim().length === 0) {
365
- return null;
366
- }
367
-
368
- return {
369
- url: "https://api.anthropic.com/v1/models",
370
- headers: {
371
- "x-api-key": apiKey,
372
- "anthropic-version": "2023-06-01",
373
- },
374
- };
375
- default:
376
- return null;
377
- }
378
- }
379
-
380
- function buildOpenAICompatiblePing(
381
- baseUrl: string,
382
- apiKey: string | null,
383
- ): { url: string; headers?: Record<string, string> } | null {
384
- if (!apiKey || apiKey.trim().length === 0) {
385
- return null;
386
- }
387
-
388
- return {
389
- url: `${baseUrl.replace(/\/$/, "")}/models`,
390
- headers: {
391
- authorization: `Bearer ${apiKey}`,
392
- },
393
- };
394
- }
395
-
396
- function checkPromptsDirectory(
397
- promptsPath: string,
398
- ): { ok: true; fileCount: number } | { ok: false; message: string } {
399
- if (!fs.existsSync(promptsPath)) {
400
- return {
401
- ok: false,
402
- message: `${ensureTrailingSlash(displayPath(promptsPath))} missing`,
403
- };
404
- }
405
-
406
- const stats = fs.statSync(promptsPath);
407
- if (!stats.isDirectory()) {
408
- return {
409
- ok: false,
410
- message: `${displayPath(promptsPath)} is not a directory`,
411
- };
412
- }
413
-
414
- const fileCount = countFilesRecursively(promptsPath);
415
- if (fileCount <= 0) {
416
- return {
417
- ok: false,
418
- message: `${ensureTrailingSlash(displayPath(promptsPath))} has no files`,
419
- };
420
- }
421
-
422
- return {
423
- ok: true,
424
- fileCount,
425
- };
426
- }
427
-
428
- function checkNodeVersion(version: string): { ok: boolean; message: string } {
429
- const major = parseNodeMajorVersion(version);
430
- if (major !== null && major >= NODE_MAJOR_WARNING_VERSION) {
431
- return {
432
- ok: true,
433
- message: version,
434
- };
435
- }
436
-
437
- return {
438
- ok: false,
439
- message: `${version} (recommended v20+)`,
440
- };
441
- }
442
-
443
- function parseNodeMajorVersion(version: string): number | null {
444
- const match = /^v(\d+)/.exec(version.trim());
445
- if (!match) {
446
- return null;
447
- }
448
-
449
- const major = Number.parseInt(match[1] ?? "", 10);
450
- return Number.isNaN(major) ? null : major;
451
- }
452
-
453
- async function checkPlaywrightInstallation(): Promise<
454
- { ok: true } | { ok: false; message: string }
455
- > {
456
- try {
457
- const playwright = (await import("playwright")) as {
458
- chromium?: {
459
- executablePath: () => string;
460
- };
461
- };
462
-
463
- const executablePath = playwright.chromium?.executablePath();
464
- if (!executablePath || !fs.existsSync(executablePath)) {
465
- return { ok: false, message: "chromium not installed" };
466
- }
467
-
468
- return { ok: true };
469
- } catch {
470
- return { ok: false, message: "playwright not installed" };
471
- }
472
- }
473
-
474
- async function checkOllamaRunning(): Promise<{ ok: boolean }> {
475
- try {
476
- const response = await fetchWithTimeout(
477
- "http://localhost:11434",
478
- { method: "GET" },
479
- OLLAMA_PING_TIMEOUT_MS,
480
- );
481
-
482
- return { ok: response.ok };
483
- } catch {
484
- return { ok: false };
485
- }
486
- }
487
-
488
- function resolveThemeDetails(db: HiveDatabase): ThemeDetails {
489
- const rawThemeName = getMetaValue(db, "theme");
490
- const rawThemeHex = getMetaValue(db, "theme_hex");
491
-
492
- if (rawThemeName && rawThemeName in BUILT_IN_THEMES) {
493
- const name = rawThemeName as keyof typeof BUILT_IN_THEMES;
494
- return { name, hex: BUILT_IN_THEMES[name] };
495
- }
496
-
497
- if (rawThemeName === "custom" && rawThemeHex && isValidHexColor(rawThemeHex)) {
498
- return { name: "custom", hex: rawThemeHex.toUpperCase() };
499
- }
500
-
501
- return {
502
- name: DEFAULT_THEME_NAME,
503
- hex: DEFAULT_THEME_HEX,
504
- };
505
- }
506
-
507
- function countRowsIfTableExists(db: HiveDatabase, tableName: string): number | null {
508
- if (!tableExists(db, tableName)) {
509
- return null;
510
- }
511
-
512
- const row = db
513
- .prepare(`SELECT COUNT(1) AS count FROM ${tableName}`)
514
- .get() as { count: number };
515
-
516
- return row.count;
517
- }
518
-
519
- function tableExists(db: HiveDatabase, tableName: string): boolean {
520
- const row = db
521
- .prepare(
522
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1",
523
- )
524
- .get(tableName) as { name: string } | undefined;
525
-
526
- return Boolean(row);
527
- }
528
-
529
- function countFilesRecursively(path: string): number {
530
- let total = 0;
531
- const entries = fs.readdirSync(path, { withFileTypes: true });
532
-
533
- for (const entry of entries) {
534
- const absolutePath = join(path, entry.name);
535
- if (entry.isDirectory()) {
536
- total += countFilesRecursively(absolutePath);
537
- continue;
538
- }
539
-
540
- if (entry.isFile()) {
541
- total += 1;
542
- }
543
- }
544
-
545
- return total;
546
- }
547
-
548
- function formatCheckLine(label: string, value: string): string {
549
- return `${label.padEnd(CHECK_LABEL_WIDTH, " ")} ${value}`;
550
- }
551
-
552
- function formatInfoLine(label: string, value: string): string {
553
- return `· ${formatCheckLine(label, value)}`;
554
- }
555
-
556
- function renderFailure(label: string, value: string, counters: CheckCounter): void {
557
- counters.errors += 1;
558
- renderError(`✗ ${formatCheckLine(label, value)}`);
559
- }
560
-
561
- function renderWarning(label: string, value: string, counters: CheckCounter): void {
562
- counters.warnings += 1;
563
- renderError(`✗ ${formatCheckLine(label, value)}`);
564
- }
565
-
566
- function renderSummary(counters: CheckCounter): void {
567
- if (counters.errors === 0 && counters.warnings === 0) {
568
- renderSuccess("All checks passed.");
569
- return;
570
- }
571
-
572
- const warningWord = counters.warnings === 1 ? "warning" : "warnings";
573
- const errorWord = counters.errors === 1 ? "error" : "errors";
574
- const summary = `${counters.warnings} ${warningWord}, ${counters.errors} ${errorWord}.`;
575
-
576
- if (counters.errors > 0) {
577
- renderError(summary);
578
- return;
579
- }
580
-
581
- renderInfo(summary);
582
- }
583
-
584
- async function fetchWithTimeout(
585
- url: string,
586
- init: {
587
- method: "GET";
588
- headers?: Record<string, string>;
589
- },
590
- timeoutMs: number,
591
- ): Promise<Awaited<ReturnType<typeof fetch>>> {
592
- const controller = new AbortController();
593
- const timer = setTimeout(() => controller.abort(), timeoutMs);
594
-
595
- try {
596
- return await fetch(url, {
597
- method: init.method,
598
- headers: init.headers,
599
- signal: controller.signal,
600
- });
601
- } finally {
602
- clearTimeout(timer);
603
- }
604
- }
605
-
606
- function isTimeoutError(error: unknown): boolean {
607
- if (!error || typeof error !== "object") {
608
- return false;
609
- }
610
-
611
- const maybeError = error as { name?: string; type?: string };
612
- return maybeError.name === "AbortError" || maybeError.type === "aborted";
613
- }
614
-
615
- function formatBytes(bytes: number): string {
616
- const units = ["B", "KB", "MB", "GB", "TB"];
617
- let unitIndex = 0;
618
- let value = bytes;
619
-
620
- while (value >= 1024 && unitIndex < units.length - 1) {
621
- value /= 1024;
622
- unitIndex += 1;
623
- }
624
-
625
- if (unitIndex === 0) {
626
- return `${value} ${units[unitIndex]}`;
627
- }
628
-
629
- return `${value.toFixed(1)} ${units[unitIndex]}`;
630
- }
631
-
632
- function displayPath(path: string): string {
633
- const home = homedir();
634
- if (path === home) {
635
- return "~";
636
- }
637
-
638
- if (path.startsWith(`${home}/`)) {
639
- return `~/${path.slice(home.length + 1)}`;
640
- }
641
-
642
- return path;
643
- }
644
-
645
- function ensureTrailingSlash(path: string): string {
646
- return path.endsWith("/") ? path : `${path}/`;
647
- }
648
-
649
- function formatError(error: unknown): string {
650
- if (error instanceof Error) {
651
- return error.message;
652
- }
653
-
654
- return String(error);
655
- }