bashio 0.4.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.
package/dist/index.js ADDED
@@ -0,0 +1,2503 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Builtins, Cli } from "clipanion";
5
+
6
+ // src/core/config.ts
7
+ import {
8
+ chmodSync,
9
+ existsSync,
10
+ mkdirSync,
11
+ readFileSync,
12
+ writeFileSync
13
+ } from "fs";
14
+ import { homedir } from "os";
15
+ import { join } from "path";
16
+
17
+ // src/core/types.ts
18
+ import { z } from "zod";
19
+ var ProviderName = z.enum([
20
+ "claude",
21
+ "openai",
22
+ "ollama",
23
+ "openrouter"
24
+ ]);
25
+ var SessionCredentials = z.object({
26
+ type: z.literal("session"),
27
+ sessionToken: z.string()
28
+ });
29
+ var ApiKeyCredentials = z.object({
30
+ type: z.literal("api_key"),
31
+ apiKey: z.string()
32
+ });
33
+ var LocalCredentials = z.object({
34
+ type: z.literal("local"),
35
+ host: z.string().default("http://localhost:11434")
36
+ });
37
+ var Credentials = z.discriminatedUnion("type", [
38
+ SessionCredentials,
39
+ ApiKeyCredentials,
40
+ LocalCredentials
41
+ ]);
42
+ var Settings = z.object({
43
+ confirmBeforeExecute: z.boolean().default(true),
44
+ historyEnabled: z.boolean().default(true),
45
+ historyRetentionDays: z.number().default(30),
46
+ historyMaxEntries: z.number().default(2e3),
47
+ autoConfirmShortcuts: z.boolean().default(false)
48
+ });
49
+ var Config = z.object({
50
+ version: z.number().default(1),
51
+ provider: ProviderName,
52
+ model: z.string(),
53
+ credentials: Credentials,
54
+ settings: Settings.optional()
55
+ });
56
+ var ShortcutDefinition = z.object({
57
+ template: z.string(),
58
+ args: z.array(z.string()).default([]),
59
+ description: z.string().optional()
60
+ });
61
+ var ShortcutsFile = z.object({
62
+ version: z.number().default(1),
63
+ shortcuts: z.record(z.string(), ShortcutDefinition)
64
+ });
65
+ var CommandSource = z.enum(["ai", "shortcut"]);
66
+ var HistoryEntry = z.object({
67
+ id: z.number(),
68
+ query: z.string(),
69
+ command: z.string(),
70
+ source: CommandSource,
71
+ workingDirectory: z.string(),
72
+ executed: z.number(),
73
+ exitCode: z.number().nullable(),
74
+ createdAt: z.string()
75
+ });
76
+ var QueryStats = z.object({
77
+ id: z.number(),
78
+ query: z.string(),
79
+ command: z.string(),
80
+ source: CommandSource,
81
+ useCount: z.number(),
82
+ successCount: z.number(),
83
+ suggested: z.number(),
84
+ firstUsed: z.string(),
85
+ lastUsed: z.string()
86
+ });
87
+
88
+ // src/core/config.ts
89
+ var CONFIG_DIR = join(homedir(), ".bashio");
90
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
91
+ function ensureConfigDir() {
92
+ if (!existsSync(CONFIG_DIR)) {
93
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
94
+ }
95
+ }
96
+ function configExists() {
97
+ return existsSync(CONFIG_FILE);
98
+ }
99
+ function loadConfig() {
100
+ if (!configExists()) {
101
+ return null;
102
+ }
103
+ try {
104
+ const raw = readFileSync(CONFIG_FILE, "utf-8");
105
+ const data = JSON.parse(raw);
106
+ return Config.parse(data);
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ function saveConfig(config) {
112
+ ensureConfigDir();
113
+ const data = JSON.stringify(config, null, 2);
114
+ writeFileSync(CONFIG_FILE, data, { encoding: "utf-8", mode: 384 });
115
+ chmodSync(CONFIG_FILE, 384);
116
+ }
117
+ function getConfigPath() {
118
+ return CONFIG_FILE;
119
+ }
120
+ function getConfigDir() {
121
+ return CONFIG_DIR;
122
+ }
123
+
124
+ // src/core/database.ts
125
+ import { chmodSync as chmodSync2, existsSync as existsSync2 } from "fs";
126
+ import Database from "better-sqlite3";
127
+ var DB_PATH = `${getConfigDir()}/history.db`;
128
+ var db = null;
129
+ var SCHEMA = `
130
+ -- History table: stores recent command history (auto-cleaned based on retention)
131
+ CREATE TABLE IF NOT EXISTS history (
132
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
133
+ query TEXT NOT NULL,
134
+ command TEXT NOT NULL,
135
+ source TEXT NOT NULL CHECK(source IN ('ai', 'shortcut')),
136
+ working_directory TEXT NOT NULL,
137
+ executed INTEGER DEFAULT 0,
138
+ exit_code INTEGER,
139
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
140
+ );
141
+
142
+ -- Query stats table: aggregated stats (never deleted, stays tiny)
143
+ -- Keyed by command to handle query variations (typos, synonyms, different phrasings)
144
+ CREATE TABLE IF NOT EXISTS query_stats (
145
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
146
+ command TEXT UNIQUE NOT NULL,
147
+ source TEXT NOT NULL CHECK(source IN ('ai', 'shortcut')),
148
+ use_count INTEGER DEFAULT 1,
149
+ success_count INTEGER DEFAULT 0,
150
+ suggested INTEGER DEFAULT 0,
151
+ first_used TEXT NOT NULL,
152
+ last_used TEXT NOT NULL
153
+ );
154
+
155
+ -- Metadata table for DB version and settings
156
+ CREATE TABLE IF NOT EXISTS metadata (
157
+ key TEXT PRIMARY KEY,
158
+ value TEXT NOT NULL
159
+ );
160
+
161
+ -- Indexes for performance
162
+ CREATE INDEX IF NOT EXISTS idx_history_created ON history(created_at DESC);
163
+ CREATE INDEX IF NOT EXISTS idx_history_query ON history(query);
164
+ CREATE INDEX IF NOT EXISTS idx_history_working_dir ON history(working_directory);
165
+ CREATE INDEX IF NOT EXISTS idx_stats_use_count ON query_stats(use_count DESC);
166
+
167
+ -- Initial metadata
168
+ INSERT OR IGNORE INTO metadata (key, value) VALUES ('db_version', '1');
169
+ INSERT OR IGNORE INTO metadata (key, value) VALUES ('last_cleanup', '0');
170
+ `;
171
+ function initDatabase() {
172
+ if (db) {
173
+ return db;
174
+ }
175
+ ensureConfigDir();
176
+ const isNew = !existsSync2(DB_PATH);
177
+ db = new Database(DB_PATH);
178
+ if (isNew) {
179
+ chmodSync2(DB_PATH, 384);
180
+ }
181
+ db.pragma("journal_mode = WAL");
182
+ db.exec(SCHEMA);
183
+ return db;
184
+ }
185
+ function getDatabase() {
186
+ if (!db) {
187
+ return initDatabase();
188
+ }
189
+ return db;
190
+ }
191
+ function getMetadata(key) {
192
+ const database = getDatabase();
193
+ const row = database.prepare("SELECT value FROM metadata WHERE key = ?").get(key);
194
+ return row?.value ?? null;
195
+ }
196
+ function setMetadata(key, value) {
197
+ const database = getDatabase();
198
+ database.prepare("INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)").run(key, value);
199
+ }
200
+
201
+ // src/core/history.ts
202
+ function rowToHistoryEntry(row) {
203
+ return {
204
+ id: row.id,
205
+ query: row.query,
206
+ command: row.command,
207
+ source: row.source,
208
+ workingDirectory: row.working_directory,
209
+ executed: row.executed,
210
+ exitCode: row.exit_code,
211
+ createdAt: row.created_at
212
+ };
213
+ }
214
+ function recordCommand(data) {
215
+ const db2 = getDatabase();
216
+ const now = (/* @__PURE__ */ new Date()).toISOString();
217
+ const workingDir = data.workingDirectory || process.cwd();
218
+ const historyResult = db2.prepare(
219
+ `INSERT INTO history (query, command, source, working_directory, executed, created_at)
220
+ VALUES (?, ?, ?, ?, 0, ?)`
221
+ ).run(data.query, data.command, data.source, workingDir, now);
222
+ const existingStats = db2.prepare("SELECT id FROM query_stats WHERE command = ?").get(data.command);
223
+ if (existingStats) {
224
+ db2.prepare(
225
+ `UPDATE query_stats
226
+ SET use_count = use_count + 1,
227
+ last_used = ?
228
+ WHERE command = ?`
229
+ ).run(now, data.command);
230
+ } else {
231
+ db2.prepare(
232
+ `INSERT INTO query_stats (command, source, use_count, success_count, suggested, first_used, last_used)
233
+ VALUES (?, ?, 1, 0, 0, ?, ?)`
234
+ ).run(data.command, data.source, now, now);
235
+ }
236
+ return historyResult.lastInsertRowid;
237
+ }
238
+ function markExecuted(historyId, exitCode) {
239
+ const db2 = getDatabase();
240
+ db2.prepare("UPDATE history SET executed = 1, exit_code = ? WHERE id = ?").run(
241
+ exitCode,
242
+ historyId
243
+ );
244
+ const historyRow = db2.prepare("SELECT command FROM history WHERE id = ?").get(historyId);
245
+ if (historyRow && exitCode === 0) {
246
+ db2.prepare(
247
+ "UPDATE query_stats SET success_count = success_count + 1 WHERE command = ?"
248
+ ).run(historyRow.command);
249
+ }
250
+ }
251
+ function getRecentHistory(limit = 20) {
252
+ const db2 = getDatabase();
253
+ const rows = db2.prepare("SELECT * FROM history ORDER BY created_at DESC LIMIT ?").all(limit);
254
+ return rows.map(rowToHistoryEntry);
255
+ }
256
+ function searchHistory(searchTerm, limit = 20) {
257
+ const db2 = getDatabase();
258
+ const pattern = `%${searchTerm}%`;
259
+ const rows = db2.prepare(
260
+ `SELECT * FROM history
261
+ WHERE query LIKE ? OR command LIKE ?
262
+ ORDER BY created_at DESC
263
+ LIMIT ?`
264
+ ).all(pattern, pattern, limit);
265
+ return rows.map(rowToHistoryEntry);
266
+ }
267
+ function getStats() {
268
+ const db2 = getDatabase();
269
+ const totalResult = db2.prepare("SELECT COUNT(*) as count FROM history").get();
270
+ const todayResult = db2.prepare(
271
+ `SELECT COUNT(*) as count FROM history
272
+ WHERE date(created_at) = date('now')`
273
+ ).get();
274
+ const thisWeekResult = db2.prepare(
275
+ `SELECT COUNT(*) as count FROM history
276
+ WHERE created_at >= datetime('now', '-7 days')`
277
+ ).get();
278
+ const executedResult = db2.prepare("SELECT COUNT(*) as count FROM history WHERE executed = 1").get();
279
+ const aiResult = db2.prepare("SELECT COUNT(*) as count FROM history WHERE source = 'ai'").get();
280
+ const shortcutResult = db2.prepare("SELECT COUNT(*) as count FROM history WHERE source = 'shortcut'").get();
281
+ const topCommands = db2.prepare(
282
+ `SELECT command, use_count, source FROM query_stats
283
+ ORDER BY use_count DESC
284
+ LIMIT 5`
285
+ ).all();
286
+ const total = totalResult.count;
287
+ const executed = executedResult.count;
288
+ return {
289
+ totalCommands: total,
290
+ todayCommands: todayResult.count,
291
+ thisWeekCommands: thisWeekResult.count,
292
+ totalExecuted: executed,
293
+ executionRate: total > 0 ? Math.round(executed / total * 100) : 0,
294
+ aiCount: aiResult.count,
295
+ shortcutCount: shortcutResult.count,
296
+ topQueries: topCommands.map((c) => ({
297
+ query: c.command,
298
+ useCount: c.use_count,
299
+ source: c.source
300
+ }))
301
+ };
302
+ }
303
+ function cleanupHistory(config) {
304
+ const db2 = getDatabase();
305
+ const deleteByAgeResult = db2.prepare(
306
+ `DELETE FROM history
307
+ WHERE created_at < datetime('now', '-' || ? || ' days')`
308
+ ).run(config.retentionDays);
309
+ const deleteExcessResult = db2.prepare(
310
+ `DELETE FROM history
311
+ WHERE id NOT IN (
312
+ SELECT id FROM history
313
+ ORDER BY created_at DESC
314
+ LIMIT ?
315
+ )`
316
+ ).run(config.maxEntries);
317
+ setMetadata("last_cleanup", (/* @__PURE__ */ new Date()).toISOString());
318
+ return deleteByAgeResult.changes + deleteExcessResult.changes;
319
+ }
320
+ function clearAllHistory() {
321
+ const db2 = getDatabase();
322
+ const result = db2.prepare("DELETE FROM history").run();
323
+ return result.changes;
324
+ }
325
+ function clearHistoryOlderThan(days) {
326
+ const db2 = getDatabase();
327
+ const result = db2.prepare(
328
+ `DELETE FROM history
329
+ WHERE created_at < datetime('now', '-' || ? || ' days')`
330
+ ).run(days);
331
+ return result.changes;
332
+ }
333
+ function shouldRunCleanup() {
334
+ const lastCleanup = getMetadata("last_cleanup");
335
+ if (!lastCleanup || lastCleanup === "0") {
336
+ return true;
337
+ }
338
+ const lastCleanupDate = new Date(lastCleanup);
339
+ const now = /* @__PURE__ */ new Date();
340
+ const oneDayMs = 24 * 60 * 60 * 1e3;
341
+ return now.getTime() - lastCleanupDate.getTime() > oneDayMs;
342
+ }
343
+ function getHistoryCount() {
344
+ const db2 = getDatabase();
345
+ const result = db2.prepare("SELECT COUNT(*) as count FROM history").get();
346
+ return result.count;
347
+ }
348
+
349
+ // src/cli/commands/AddShortcutCommand.ts
350
+ import { input as input2 } from "@inquirer/prompts";
351
+ import { Command, Option } from "clipanion";
352
+ import pc2 from "picocolors";
353
+
354
+ // src/core/shortcuts.ts
355
+ import { chmodSync as chmodSync3, existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
356
+ import { join as join2 } from "path";
357
+ import { input } from "@inquirer/prompts";
358
+ var SHORTCUTS_FILE = join2(getConfigDir(), "shortcuts.json");
359
+ function shortcutsFileExists() {
360
+ return existsSync3(SHORTCUTS_FILE);
361
+ }
362
+ function loadShortcuts() {
363
+ if (!shortcutsFileExists()) {
364
+ return { version: 1, shortcuts: {} };
365
+ }
366
+ try {
367
+ const raw = readFileSync2(SHORTCUTS_FILE, "utf-8");
368
+ const data = JSON.parse(raw);
369
+ return ShortcutsFile.parse(data);
370
+ } catch {
371
+ return { version: 1, shortcuts: {} };
372
+ }
373
+ }
374
+ function saveShortcuts(shortcuts) {
375
+ ensureConfigDir();
376
+ const data = JSON.stringify(shortcuts, null, 2);
377
+ writeFileSync2(SHORTCUTS_FILE, data, { encoding: "utf-8", mode: 384 });
378
+ chmodSync3(SHORTCUTS_FILE, 384);
379
+ }
380
+ function getShortcut(name) {
381
+ const file = loadShortcuts();
382
+ return file.shortcuts[name] || null;
383
+ }
384
+ function addShortcut(name, definition) {
385
+ const file = loadShortcuts();
386
+ file.shortcuts[name] = definition;
387
+ saveShortcuts(file);
388
+ }
389
+ function removeShortcut(name) {
390
+ const file = loadShortcuts();
391
+ if (!file.shortcuts[name]) {
392
+ return false;
393
+ }
394
+ delete file.shortcuts[name];
395
+ saveShortcuts(file);
396
+ return true;
397
+ }
398
+ function listShortcuts() {
399
+ const file = loadShortcuts();
400
+ return file.shortcuts;
401
+ }
402
+ function getShortcutsFilePath() {
403
+ return SHORTCUTS_FILE;
404
+ }
405
+ function parseInput(queryParts, expectedArgCount) {
406
+ if (queryParts.length === 0) {
407
+ return { name: "", args: [] };
408
+ }
409
+ const name = queryParts[0];
410
+ const remainingParts = queryParts.slice(1);
411
+ if (remainingParts.length === 0) {
412
+ return { name, args: [] };
413
+ }
414
+ if (expectedArgCount === 1) {
415
+ return { name, args: [remainingParts.join(" ")] };
416
+ }
417
+ return { name, args: remainingParts };
418
+ }
419
+ function fillTemplate(template, argNames, argValues) {
420
+ let result = template;
421
+ for (let i = 0; i < argNames.length; i++) {
422
+ const placeholder = `{{${argNames[i]}}}`;
423
+ const value = argValues[i] || "";
424
+ result = result.split(placeholder).join(value);
425
+ }
426
+ return result;
427
+ }
428
+ async function tryResolveShortcut(queryParts) {
429
+ if (queryParts.length === 0) {
430
+ return null;
431
+ }
432
+ const shortcutName = queryParts[0];
433
+ const shortcut = getShortcut(shortcutName);
434
+ if (!shortcut) {
435
+ return null;
436
+ }
437
+ const requiredArgs = shortcut.args || [];
438
+ const { args: providedArgs } = parseInput(queryParts, requiredArgs.length);
439
+ const finalArgs = [...providedArgs];
440
+ if (finalArgs.length < requiredArgs.length) {
441
+ for (let i = finalArgs.length; i < requiredArgs.length; i++) {
442
+ const argName = requiredArgs[i];
443
+ const value = await input({
444
+ message: `Enter ${argName}:`
445
+ });
446
+ finalArgs.push(value);
447
+ }
448
+ }
449
+ const command = fillTemplate(shortcut.template, requiredArgs, finalArgs);
450
+ return {
451
+ name: shortcutName,
452
+ command,
453
+ source: "shortcut"
454
+ };
455
+ }
456
+
457
+ // src/utils/logger.ts
458
+ import pc from "picocolors";
459
+ var logger = {
460
+ info: (msg) => console.log(pc.blue("i"), msg),
461
+ success: (msg) => console.log(pc.green("\u2713"), msg),
462
+ warn: (msg) => console.log(pc.yellow("\u26A0"), msg),
463
+ error: (msg) => console.log(pc.red("\u2717"), msg),
464
+ command: (cmd) => {
465
+ console.log(pc.gray(">"), pc.cyan("Will run:"), pc.white(cmd));
466
+ },
467
+ output: (text) => {
468
+ console.log(pc.gray("\u2500".repeat(50)));
469
+ console.log(text);
470
+ console.log(pc.gray("\u2500".repeat(50)));
471
+ },
472
+ exitCode: (code) => {
473
+ if (code === 0) {
474
+ console.log(pc.green("\u2713"), pc.gray(`Done (exit code: ${code})`));
475
+ } else {
476
+ console.log(pc.red("\u2717"), pc.gray(`Failed (exit code: ${code})`));
477
+ }
478
+ },
479
+ box: (title, content) => {
480
+ const lines = content.split("\n");
481
+ const maxLen = Math.max(title.length, ...lines.map((l) => l.length));
482
+ const border = "\u2500".repeat(maxLen + 4);
483
+ console.log(pc.gray(`\u250C${border}\u2510`));
484
+ console.log(pc.gray("\u2502"), pc.bold(title.padEnd(maxLen + 2)), pc.gray("\u2502"));
485
+ console.log(pc.gray(`\u251C${border}\u2524`));
486
+ for (const line of lines) {
487
+ console.log(pc.gray("\u2502"), line.padEnd(maxLen + 2), pc.gray("\u2502"));
488
+ }
489
+ console.log(pc.gray(`\u2514${border}\u2518`));
490
+ }
491
+ };
492
+
493
+ // src/cli/commands/AddShortcutCommand.ts
494
+ var AddShortcutCommand = class extends Command {
495
+ static paths = [["add-shortcut"], ["--add-shortcut"]];
496
+ static usage = Command.Usage({
497
+ description: "Add a new shortcut",
498
+ examples: [
499
+ ["Interactive mode", "$0 --add-shortcut"],
500
+ [
501
+ "One-liner",
502
+ '$0 --add-shortcut killport "lsof -ti:{{port}} | xargs kill -9" port'
503
+ ]
504
+ ]
505
+ });
506
+ // Optional positional args for one-liner mode
507
+ name = Option.String({ required: false });
508
+ template = Option.String({ required: false });
509
+ args = Option.String({ required: false });
510
+ async execute() {
511
+ let shortcutName;
512
+ let shortcutTemplate;
513
+ let shortcutArgs;
514
+ let shortcutDescription;
515
+ if (this.name && this.template) {
516
+ shortcutName = this.name;
517
+ shortcutTemplate = this.template;
518
+ shortcutArgs = this.args ? this.args.split(",").map((a) => a.trim()) : [];
519
+ } else {
520
+ console.log(pc2.bold("\n Add New Shortcut\n"));
521
+ shortcutName = await input2({
522
+ message: "Shortcut name:",
523
+ validate: (value) => {
524
+ if (!value.trim()) return "Name is required";
525
+ if (value.includes(" ")) return "Name cannot contain spaces";
526
+ return true;
527
+ }
528
+ });
529
+ if (getShortcut(shortcutName)) {
530
+ logger.warn(
531
+ `Shortcut "${shortcutName}" already exists. It will be overwritten.`
532
+ );
533
+ }
534
+ shortcutTemplate = await input2({
535
+ message: "Command template (use {{arg}} for placeholders):",
536
+ validate: (value) => {
537
+ if (!value.trim()) return "Template is required";
538
+ return true;
539
+ }
540
+ });
541
+ const argsInput = await input2({
542
+ message: "Arguments (comma-separated, or leave empty):"
543
+ });
544
+ shortcutArgs = argsInput ? argsInput.split(",").map((a) => a.trim()).filter(Boolean) : [];
545
+ shortcutDescription = await input2({
546
+ message: "Description (optional):"
547
+ });
548
+ }
549
+ const placeholders = shortcutTemplate.match(/\{\{(\w+)\}\}/g) || [];
550
+ const placeholderNames = placeholders.map((p) => p.replace(/[{}]/g, ""));
551
+ for (const ph of placeholderNames) {
552
+ if (!shortcutArgs.includes(ph)) {
553
+ logger.warn(
554
+ `Placeholder "{{${ph}}}" found but not in arguments list. Adding it.`
555
+ );
556
+ shortcutArgs.push(ph);
557
+ }
558
+ }
559
+ addShortcut(shortcutName, {
560
+ template: shortcutTemplate,
561
+ args: shortcutArgs,
562
+ description: shortcutDescription || void 0
563
+ });
564
+ console.log();
565
+ logger.success(`Shortcut "${shortcutName}" added!`);
566
+ if (shortcutArgs.length > 0) {
567
+ console.log(
568
+ pc2.gray(
569
+ ` Usage: s ${shortcutName} ${shortcutArgs.map((a) => `<${a}>`).join(" ")}`
570
+ )
571
+ );
572
+ } else {
573
+ console.log(pc2.gray(` Usage: s ${shortcutName}`));
574
+ }
575
+ console.log();
576
+ return 0;
577
+ }
578
+ };
579
+
580
+ // src/cli/commands/AuthCommand.ts
581
+ import { input as input3, password, select } from "@inquirer/prompts";
582
+ import { Command as Command2 } from "clipanion";
583
+ import pc3 from "picocolors";
584
+
585
+ // src/providers/base.ts
586
+ var SYSTEM_PROMPT_GENERATE = `You are a shell command generator for macOS/Linux terminals.
587
+ Given a natural language description, return ONLY the shell command.
588
+ Rules:
589
+ - Return ONLY the raw command, nothing else
590
+ - No markdown formatting, no backticks, no explanation
591
+ - No "Here's the command:" or similar prefixes
592
+ - If multiple commands are needed, chain them with && or ;
593
+ - Use common Unix utilities (find, grep, awk, sed, curl, etc.)
594
+ - Prefer simple, portable commands over complex ones`;
595
+ var SYSTEM_PROMPT_EXPLAIN = `You are a shell command expert.
596
+ Explain the given shell command in simple terms.
597
+ Break down each part of the command concisely.
598
+ Format as a simple list without markdown.`;
599
+
600
+ // src/providers/claude.ts
601
+ var ClaudeProvider = class {
602
+ name = "Claude";
603
+ model;
604
+ apiKey;
605
+ constructor(config) {
606
+ this.model = config.model;
607
+ if (config.credentials.type === "api_key") {
608
+ this.apiKey = config.credentials.apiKey;
609
+ } else if (config.credentials.type === "session") {
610
+ this.apiKey = config.credentials.sessionToken;
611
+ } else {
612
+ throw new Error("Claude requires API key or session token");
613
+ }
614
+ }
615
+ async call(systemPrompt, userMessage) {
616
+ const messages = [
617
+ { role: "user", content: userMessage }
618
+ ];
619
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
620
+ method: "POST",
621
+ headers: {
622
+ "Content-Type": "application/json",
623
+ "x-api-key": this.apiKey,
624
+ "anthropic-version": "2023-06-01"
625
+ },
626
+ body: JSON.stringify({
627
+ model: this.model,
628
+ max_tokens: 1024,
629
+ system: systemPrompt,
630
+ messages
631
+ })
632
+ });
633
+ if (!response.ok) {
634
+ const error = await response.text();
635
+ throw new Error(`Claude API error: ${response.status} - ${error}`);
636
+ }
637
+ const data = await response.json();
638
+ if (data.error) {
639
+ throw new Error(`Claude API error: ${data.error.message}`);
640
+ }
641
+ const textContent = data.content.find((c) => c.type === "text");
642
+ if (!textContent) {
643
+ throw new Error("No text response from Claude");
644
+ }
645
+ return textContent.text.trim();
646
+ }
647
+ async generateCommand(query, context) {
648
+ const userMessage = context ? `Context: ${context}
649
+
650
+ Task: ${query}` : query;
651
+ return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
652
+ }
653
+ async explainCommand(command) {
654
+ return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
655
+ }
656
+ async validateCredentials() {
657
+ try {
658
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
659
+ method: "POST",
660
+ headers: {
661
+ "Content-Type": "application/json",
662
+ "x-api-key": this.apiKey,
663
+ "anthropic-version": "2023-06-01"
664
+ },
665
+ body: JSON.stringify({
666
+ model: this.model,
667
+ max_tokens: 10,
668
+ messages: [{ role: "user", content: "hi" }]
669
+ })
670
+ });
671
+ return response.ok;
672
+ } catch {
673
+ return false;
674
+ }
675
+ }
676
+ };
677
+ var CLAUDE_MODELS = [
678
+ { value: "claude-sonnet-4-20250514", label: "Claude Sonnet 4 (recommended)" },
679
+ { value: "claude-3-5-sonnet-20241022", label: "Claude 3.5 Sonnet" },
680
+ { value: "claude-3-5-haiku-20241022", label: "Claude 3.5 Haiku (fast)" }
681
+ ];
682
+
683
+ // src/providers/ollama.ts
684
+ var OllamaProvider = class {
685
+ name = "Ollama";
686
+ model;
687
+ host;
688
+ constructor(config) {
689
+ this.model = config.model;
690
+ if (config.credentials.type === "local") {
691
+ this.host = config.credentials.host;
692
+ } else {
693
+ this.host = "http://localhost:11434";
694
+ }
695
+ }
696
+ async call(systemPrompt, userMessage) {
697
+ const response = await fetch(`${this.host}/api/chat`, {
698
+ method: "POST",
699
+ headers: {
700
+ "Content-Type": "application/json"
701
+ },
702
+ body: JSON.stringify({
703
+ model: this.model,
704
+ messages: [
705
+ { role: "system", content: systemPrompt },
706
+ { role: "user", content: userMessage }
707
+ ],
708
+ stream: false
709
+ })
710
+ });
711
+ if (!response.ok) {
712
+ const error = await response.text();
713
+ throw new Error(`Ollama error: ${response.status} - ${error}`);
714
+ }
715
+ const data = await response.json();
716
+ if (data.error) {
717
+ throw new Error(`Ollama error: ${data.error}`);
718
+ }
719
+ const content = data.message?.content || data.response;
720
+ if (!content) {
721
+ throw new Error("No response from Ollama");
722
+ }
723
+ return content.trim();
724
+ }
725
+ async generateCommand(query, context) {
726
+ const userMessage = context ? `Context: ${context}
727
+
728
+ Task: ${query}` : query;
729
+ return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
730
+ }
731
+ async explainCommand(command) {
732
+ return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
733
+ }
734
+ async validateCredentials() {
735
+ try {
736
+ const response = await fetch(`${this.host}/api/tags`);
737
+ return response.ok;
738
+ } catch {
739
+ return false;
740
+ }
741
+ }
742
+ static async getAvailableModels(host = "http://localhost:11434") {
743
+ try {
744
+ const response = await fetch(`${host}/api/tags`);
745
+ if (!response.ok) return [];
746
+ const data = await response.json();
747
+ return data.models.map((m) => m.name);
748
+ } catch {
749
+ return [];
750
+ }
751
+ }
752
+ };
753
+
754
+ // src/providers/openai.ts
755
+ var OpenAIProvider = class {
756
+ name = "OpenAI";
757
+ model;
758
+ apiKey;
759
+ constructor(config) {
760
+ this.model = config.model;
761
+ if (config.credentials.type === "api_key") {
762
+ this.apiKey = config.credentials.apiKey;
763
+ } else {
764
+ throw new Error("OpenAI requires API key");
765
+ }
766
+ }
767
+ async call(systemPrompt, userMessage) {
768
+ const messages = [
769
+ { role: "system", content: systemPrompt },
770
+ { role: "user", content: userMessage }
771
+ ];
772
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
773
+ method: "POST",
774
+ headers: {
775
+ "Content-Type": "application/json",
776
+ Authorization: `Bearer ${this.apiKey}`
777
+ },
778
+ body: JSON.stringify({
779
+ model: this.model,
780
+ max_tokens: 1024,
781
+ messages
782
+ })
783
+ });
784
+ if (!response.ok) {
785
+ const error = await response.text();
786
+ throw new Error(`OpenAI API error: ${response.status} - ${error}`);
787
+ }
788
+ const data = await response.json();
789
+ if (data.error) {
790
+ throw new Error(`OpenAI API error: ${data.error.message}`);
791
+ }
792
+ const content = data.choices[0]?.message?.content;
793
+ if (!content) {
794
+ throw new Error("No response from OpenAI");
795
+ }
796
+ return content.trim();
797
+ }
798
+ async generateCommand(query, context) {
799
+ const userMessage = context ? `Context: ${context}
800
+
801
+ Task: ${query}` : query;
802
+ return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
803
+ }
804
+ async explainCommand(command) {
805
+ return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
806
+ }
807
+ async validateCredentials() {
808
+ try {
809
+ const response = await fetch("https://api.openai.com/v1/models", {
810
+ headers: {
811
+ Authorization: `Bearer ${this.apiKey}`
812
+ }
813
+ });
814
+ return response.ok;
815
+ } catch {
816
+ return false;
817
+ }
818
+ }
819
+ };
820
+ var OPENAI_MODELS = [
821
+ { value: "gpt-4o", label: "GPT-4o (recommended)" },
822
+ { value: "gpt-4o-mini", label: "GPT-4o Mini (fast)" },
823
+ { value: "gpt-4-turbo", label: "GPT-4 Turbo" }
824
+ ];
825
+
826
+ // src/providers/openrouter.ts
827
+ var OpenRouterProvider = class {
828
+ name = "OpenRouter";
829
+ model;
830
+ apiKey;
831
+ constructor(config) {
832
+ this.model = config.model;
833
+ if (config.credentials.type === "api_key") {
834
+ this.apiKey = config.credentials.apiKey;
835
+ } else {
836
+ throw new Error("OpenRouter requires API key");
837
+ }
838
+ }
839
+ async call(systemPrompt, userMessage) {
840
+ const messages = [
841
+ { role: "system", content: systemPrompt },
842
+ { role: "user", content: userMessage }
843
+ ];
844
+ const response = await fetch(
845
+ "https://openrouter.ai/api/v1/chat/completions",
846
+ {
847
+ method: "POST",
848
+ headers: {
849
+ "Content-Type": "application/json",
850
+ Authorization: `Bearer ${this.apiKey}`,
851
+ "HTTP-Referer": "https://github.com/bashio",
852
+ "X-Title": "Bashio"
853
+ },
854
+ body: JSON.stringify({
855
+ model: this.model,
856
+ max_tokens: 1024,
857
+ messages
858
+ })
859
+ }
860
+ );
861
+ if (!response.ok) {
862
+ const error = await response.text();
863
+ throw new Error(`OpenRouter API error: ${response.status} - ${error}`);
864
+ }
865
+ const data = await response.json();
866
+ if (data.error) {
867
+ throw new Error(`OpenRouter API error: ${data.error.message}`);
868
+ }
869
+ const content = data.choices[0]?.message?.content;
870
+ if (!content) {
871
+ throw new Error("No response from OpenRouter");
872
+ }
873
+ return content.trim();
874
+ }
875
+ async generateCommand(query, context) {
876
+ const userMessage = context ? `Context: ${context}
877
+
878
+ Task: ${query}` : query;
879
+ return this.call(SYSTEM_PROMPT_GENERATE, userMessage);
880
+ }
881
+ async explainCommand(command) {
882
+ return this.call(SYSTEM_PROMPT_EXPLAIN, `Explain this command: ${command}`);
883
+ }
884
+ async validateCredentials() {
885
+ try {
886
+ const response = await fetch("https://openrouter.ai/api/v1/models", {
887
+ headers: {
888
+ Authorization: `Bearer ${this.apiKey}`
889
+ }
890
+ });
891
+ return response.ok;
892
+ } catch {
893
+ return false;
894
+ }
895
+ }
896
+ };
897
+ var OPENROUTER_MODELS = [
898
+ { value: "anthropic/claude-sonnet-4", label: "Claude Sonnet 4" },
899
+ { value: "anthropic/claude-3.5-sonnet", label: "Claude 3.5 Sonnet" },
900
+ { value: "openai/gpt-4o", label: "GPT-4o" },
901
+ { value: "google/gemini-pro-1.5", label: "Gemini Pro 1.5" },
902
+ { value: "meta-llama/llama-3.1-70b-instruct", label: "Llama 3.1 70B" }
903
+ ];
904
+
905
+ // src/providers/index.ts
906
+ function createProvider(config) {
907
+ const providerConfig = {
908
+ model: config.model,
909
+ credentials: config.credentials
910
+ };
911
+ switch (config.provider) {
912
+ case "claude":
913
+ return new ClaudeProvider(providerConfig);
914
+ case "openai":
915
+ return new OpenAIProvider(providerConfig);
916
+ case "ollama":
917
+ return new OllamaProvider(providerConfig);
918
+ case "openrouter":
919
+ return new OpenRouterProvider(providerConfig);
920
+ default:
921
+ throw new Error(`Unknown provider: ${config.provider}`);
922
+ }
923
+ }
924
+
925
+ // src/utils/spinner.ts
926
+ import ora from "ora";
927
+ function createSpinner(text) {
928
+ return ora({
929
+ text,
930
+ spinner: "dots"
931
+ });
932
+ }
933
+
934
+ // src/cli/commands/AuthCommand.ts
935
+ var AuthCommand = class extends Command2 {
936
+ static paths = [["auth"], ["--auth"]];
937
+ static usage = Command2.Usage({
938
+ description: "Configure AI provider for Bashio",
939
+ examples: [["Configure AI provider", "$0 --auth"]]
940
+ });
941
+ async execute() {
942
+ console.log(pc3.bold("\n Bashio Setup\n"));
943
+ const provider = await select({
944
+ message: "Select your AI provider:",
945
+ choices: [
946
+ {
947
+ value: "claude",
948
+ name: "Claude (Anthropic)",
949
+ description: "Use Anthropic API key"
950
+ },
951
+ {
952
+ value: "openai",
953
+ name: "ChatGPT (OpenAI)",
954
+ description: "Use OpenAI API key"
955
+ },
956
+ {
957
+ value: "ollama",
958
+ name: "Ollama (Local)",
959
+ description: "Free, runs on your machine"
960
+ },
961
+ {
962
+ value: "openrouter",
963
+ name: "OpenRouter",
964
+ description: "Pay per use, multiple models"
965
+ }
966
+ ]
967
+ });
968
+ let credentials;
969
+ let model;
970
+ switch (provider) {
971
+ case "claude": {
972
+ const apiKey = await password({
973
+ message: "Enter your Anthropic API key:",
974
+ mask: "*"
975
+ });
976
+ credentials = { type: "api_key", apiKey };
977
+ model = await select({
978
+ message: "Select model:",
979
+ choices: CLAUDE_MODELS.map((m) => ({
980
+ value: m.value,
981
+ name: m.label
982
+ }))
983
+ });
984
+ break;
985
+ }
986
+ case "openai": {
987
+ const apiKey = await password({
988
+ message: "Enter your OpenAI API key:",
989
+ mask: "*"
990
+ });
991
+ credentials = { type: "api_key", apiKey };
992
+ model = await select({
993
+ message: "Select model:",
994
+ choices: OPENAI_MODELS.map((m) => ({
995
+ value: m.value,
996
+ name: m.label
997
+ }))
998
+ });
999
+ break;
1000
+ }
1001
+ case "ollama": {
1002
+ const host = await input3({
1003
+ message: "Ollama host:",
1004
+ default: "http://localhost:11434"
1005
+ });
1006
+ credentials = { type: "local", host };
1007
+ const spinner2 = createSpinner("Checking Ollama connection...").start();
1008
+ const availableModels = await OllamaProvider.getAvailableModels(host);
1009
+ if (availableModels.length === 0) {
1010
+ spinner2.fail("Could not connect to Ollama or no models installed");
1011
+ console.log(pc3.yellow("\nMake sure Ollama is running: ollama serve"));
1012
+ console.log(pc3.yellow("Install a model: ollama pull llama3.2\n"));
1013
+ return 1;
1014
+ }
1015
+ spinner2.succeed(`Found ${availableModels.length} models`);
1016
+ const modelChoices = availableModels.map((m) => ({
1017
+ value: m,
1018
+ name: m
1019
+ }));
1020
+ model = await select({
1021
+ message: "Select model:",
1022
+ choices: modelChoices
1023
+ });
1024
+ break;
1025
+ }
1026
+ case "openrouter": {
1027
+ const apiKey = await password({
1028
+ message: "Enter your OpenRouter API key:",
1029
+ mask: "*"
1030
+ });
1031
+ credentials = { type: "api_key", apiKey };
1032
+ model = await select({
1033
+ message: "Select model:",
1034
+ choices: OPENROUTER_MODELS.map((m) => ({
1035
+ value: m.value,
1036
+ name: m.label
1037
+ }))
1038
+ });
1039
+ break;
1040
+ }
1041
+ default:
1042
+ throw new Error(`Unknown provider: ${provider}`);
1043
+ }
1044
+ const config = {
1045
+ version: 1,
1046
+ provider,
1047
+ model,
1048
+ credentials,
1049
+ settings: {
1050
+ confirmBeforeExecute: true,
1051
+ historyEnabled: true,
1052
+ historyRetentionDays: 30,
1053
+ historyMaxEntries: 2e3,
1054
+ autoConfirmShortcuts: false
1055
+ }
1056
+ };
1057
+ const spinner = createSpinner("Validating credentials...").start();
1058
+ try {
1059
+ const providerInstance = createProvider(config);
1060
+ const valid = await providerInstance.validateCredentials();
1061
+ if (!valid) {
1062
+ spinner.fail("Invalid credentials");
1063
+ return 1;
1064
+ }
1065
+ spinner.succeed("Credentials valid");
1066
+ } catch (err) {
1067
+ spinner.fail(
1068
+ `Validation failed: ${err instanceof Error ? err.message : "Unknown error"}`
1069
+ );
1070
+ return 1;
1071
+ }
1072
+ saveConfig(config);
1073
+ console.log();
1074
+ logger.success("Configuration saved!");
1075
+ console.log(pc3.gray(` Provider: ${provider}`));
1076
+ console.log(pc3.gray(` Model: ${model}`));
1077
+ console.log();
1078
+ console.log(
1079
+ pc3.green("You're all set! Try:"),
1080
+ pc3.cyan("b find all png files")
1081
+ );
1082
+ console.log();
1083
+ return 0;
1084
+ }
1085
+ };
1086
+
1087
+ // src/cli/commands/ClearHistoryCommand.ts
1088
+ import { confirm } from "@inquirer/prompts";
1089
+ import { Command as Command3, Option as Option2 } from "clipanion";
1090
+ import pc5 from "picocolors";
1091
+
1092
+ // src/utils/danger-ui.ts
1093
+ import pc4 from "picocolors";
1094
+ var renderDangerBanner = (message, label = "DANGER") => {
1095
+ const labelText = ` ${label} `;
1096
+ const spacer = " ";
1097
+ const contentLength = labelText.length + spacer.length + message.length;
1098
+ const innerWidth = contentLength + 2;
1099
+ const border = "\u2500".repeat(innerWidth);
1100
+ const top = pc4.red(` \u250C${border}\u2510`);
1101
+ const middle = ` ${pc4.red("\u2502 ")}` + pc4.bgRed(pc4.white(labelText)) + pc4.red(`${spacer}${message}`) + pc4.red(" \u2502");
1102
+ const bottom = pc4.red(` \u2514${border}\u2518`);
1103
+ return [top, middle, bottom];
1104
+ };
1105
+
1106
+ // src/cli/commands/ClearHistoryCommand.ts
1107
+ var ClearHistoryCommand = class extends Command3 {
1108
+ static paths = [["--clear-history"]];
1109
+ static usage = Command3.Usage({
1110
+ description: "Clear command history",
1111
+ examples: [
1112
+ ["Clear all history", "$0 --clear-history --all"],
1113
+ ["Clear entries older than 7 days", "$0 --clear-history --older-than 7"]
1114
+ ]
1115
+ });
1116
+ all = Option2.Boolean("--all,-a", false, {
1117
+ description: "Clear all history entries"
1118
+ });
1119
+ olderThan = Option2.String("--older-than,-o", {
1120
+ description: "Clear entries older than N days"
1121
+ });
1122
+ async execute() {
1123
+ if (!this.all && !this.olderThan) {
1124
+ console.log(pc5.bold("\n Clear History\n"));
1125
+ console.log(pc5.gray(" Options:"));
1126
+ console.log(pc5.gray(" --all Clear all history"));
1127
+ console.log(
1128
+ pc5.gray(" --older-than N Clear entries older than N days")
1129
+ );
1130
+ console.log();
1131
+ console.log(pc5.dim(" Example: s --clear-history --older-than 30\n"));
1132
+ return 1;
1133
+ }
1134
+ const currentCount = getHistoryCount();
1135
+ if (currentCount === 0) {
1136
+ logger.info("History is already empty.");
1137
+ return 0;
1138
+ }
1139
+ if (this.all) {
1140
+ console.log();
1141
+ for (const line of renderDangerBanner(
1142
+ `This will permanently delete ${currentCount} history entries.`
1143
+ )) {
1144
+ console.log(line);
1145
+ }
1146
+ console.log();
1147
+ const confirmed = await confirm({
1148
+ message: "Proceed with clearing all history?",
1149
+ default: false
1150
+ });
1151
+ if (!confirmed) {
1152
+ logger.info("Cancelled.");
1153
+ return 0;
1154
+ }
1155
+ const deleted = clearAllHistory();
1156
+ logger.success(`Cleared ${deleted} history entries.`);
1157
+ return 0;
1158
+ }
1159
+ if (this.olderThan) {
1160
+ const days = Number.parseInt(this.olderThan, 10);
1161
+ if (Number.isNaN(days) || days < 1) {
1162
+ logger.error("Invalid number of days.");
1163
+ return 1;
1164
+ }
1165
+ console.log();
1166
+ for (const line of renderDangerBanner(
1167
+ `This will permanently delete entries older than ${days} days.`
1168
+ )) {
1169
+ console.log(line);
1170
+ }
1171
+ console.log();
1172
+ const confirmed = await confirm({
1173
+ message: `Proceed with clearing entries older than ${days} days?`,
1174
+ default: false
1175
+ });
1176
+ if (!confirmed) {
1177
+ logger.info("Cancelled.");
1178
+ return 0;
1179
+ }
1180
+ const deleted = clearHistoryOlderThan(days);
1181
+ if (deleted === 0) {
1182
+ logger.info(`No entries older than ${days} days.`);
1183
+ } else {
1184
+ logger.success(`Cleared ${deleted} entries older than ${days} days.`);
1185
+ }
1186
+ return 0;
1187
+ }
1188
+ return 0;
1189
+ }
1190
+ };
1191
+
1192
+ // src/cli/commands/ConfigCommand.ts
1193
+ import { Command as Command4 } from "clipanion";
1194
+ import pc6 from "picocolors";
1195
+ var ConfigCommand = class extends Command4 {
1196
+ static paths = [["config"], ["--config"]];
1197
+ static usage = Command4.Usage({
1198
+ description: "View current Bashio configuration",
1199
+ examples: [["View config", "$0 --config"]]
1200
+ });
1201
+ async execute() {
1202
+ if (!configExists()) {
1203
+ logger.warn("Bashio is not configured yet.");
1204
+ console.log(pc6.gray("Run 'b --auth' to set up your AI provider.\n"));
1205
+ return 1;
1206
+ }
1207
+ const config = loadConfig();
1208
+ if (!config) {
1209
+ logger.error("Failed to load configuration.");
1210
+ return 1;
1211
+ }
1212
+ console.log(pc6.bold("\n Bashio Configuration\n"));
1213
+ console.log(` Provider: ${pc6.cyan(config.provider)}`);
1214
+ console.log(` Model: ${pc6.cyan(config.model)}`);
1215
+ console.log(` Auth: ${pc6.green("Configured")}`);
1216
+ const settings = config.settings;
1217
+ console.log();
1218
+ console.log(pc6.bold(" Settings"));
1219
+ console.log(
1220
+ ` History: ${settings?.historyEnabled !== false ? pc6.green("enabled") : pc6.gray("disabled")}`
1221
+ );
1222
+ console.log(
1223
+ ` Auto-confirm shortcuts: ${settings?.autoConfirmShortcuts ? pc6.green("enabled") : pc6.gray("disabled")}`
1224
+ );
1225
+ console.log();
1226
+ console.log(pc6.gray(` Config file: ${getConfigPath()}`));
1227
+ console.log();
1228
+ return 0;
1229
+ }
1230
+ };
1231
+
1232
+ // src/cli/commands/DefaultCommand.ts
1233
+ import * as readline from "readline";
1234
+ import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
1235
+ import { Command as Command5, Option as Option3 } from "clipanion";
1236
+ import pc7 from "picocolors";
1237
+
1238
+ // src/core/executor.ts
1239
+ import { spawn } from "child_process";
1240
+ function executeCommand(command) {
1241
+ return new Promise((resolve) => {
1242
+ const shell = process.platform === "win32" ? "cmd" : "/bin/sh";
1243
+ const shellArg = process.platform === "win32" ? "/c" : "-c";
1244
+ const child = spawn(shell, [shellArg, command], {
1245
+ stdio: "inherit",
1246
+ cwd: process.cwd(),
1247
+ env: process.env
1248
+ });
1249
+ child.on("close", (code) => {
1250
+ resolve({
1251
+ exitCode: code ?? 1,
1252
+ stdout: "",
1253
+ stderr: ""
1254
+ });
1255
+ });
1256
+ child.on("error", (err) => {
1257
+ resolve({
1258
+ exitCode: 1,
1259
+ stdout: "",
1260
+ stderr: err.message
1261
+ });
1262
+ });
1263
+ });
1264
+ }
1265
+
1266
+ // src/utils/clipboard.ts
1267
+ import { exec } from "child_process";
1268
+ import { promisify } from "util";
1269
+ var execAsync = promisify(exec);
1270
+ async function copyToClipboard(text) {
1271
+ try {
1272
+ const platform = process.platform;
1273
+ if (platform === "darwin") {
1274
+ await execAsync(`echo ${JSON.stringify(text)} | pbcopy`);
1275
+ } else if (platform === "linux") {
1276
+ try {
1277
+ await execAsync(
1278
+ `echo ${JSON.stringify(text)} | xclip -selection clipboard`
1279
+ );
1280
+ } catch {
1281
+ await execAsync(
1282
+ `echo ${JSON.stringify(text)} | xsel --clipboard --input`
1283
+ );
1284
+ }
1285
+ } else if (platform === "win32") {
1286
+ await execAsync(`echo ${text} | clip`);
1287
+ } else {
1288
+ return false;
1289
+ }
1290
+ return true;
1291
+ } catch {
1292
+ return false;
1293
+ }
1294
+ }
1295
+
1296
+ // src/utils/danger.ts
1297
+ var DANGEROUS_PATTERNS = [
1298
+ {
1299
+ pattern: /\b(?:rm|rmdir)\b[^\n]*(?:-rf|-fr|--recursive|--force|-r\b)/i,
1300
+ reason: "Recursively removes files or directories"
1301
+ },
1302
+ {
1303
+ pattern: /\b(?:mkfs|mkfs\.[a-z0-9]+|format|diskpart|diskutil|fdisk|parted)\b/i,
1304
+ reason: "Formats or repartitions disks/filesystems"
1305
+ },
1306
+ {
1307
+ pattern: /\bdd\b/i,
1308
+ reason: "Writes raw data to disks or devices"
1309
+ },
1310
+ {
1311
+ pattern: /\b(?:shutdown|reboot|poweroff|halt)\b/i,
1312
+ reason: "Shuts down or reboots the system"
1313
+ },
1314
+ {
1315
+ pattern: /\b(?:userdel|deluser|groupdel|dscl\b[^\n]*-delete|net\s+user\b[^\n]*\/delete)\b/i,
1316
+ reason: "Deletes system users or groups"
1317
+ },
1318
+ {
1319
+ pattern: /:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
1320
+ reason: "Fork bomb pattern"
1321
+ },
1322
+ {
1323
+ pattern: /\b(?:chmod|chown)\b[^\n]*\s-?R\b/i,
1324
+ reason: "Recursively changes permissions or ownership"
1325
+ },
1326
+ {
1327
+ pattern: /\b(?:git\s+reset\s+--hard|git\s+clean\s+-fd)\b/i,
1328
+ reason: "Discards uncommitted changes"
1329
+ },
1330
+ {
1331
+ pattern: /\b(?:rd|rmdir)\b[^\n]*\s\/s\b/i,
1332
+ reason: "Recursively removes directories (Windows)"
1333
+ },
1334
+ {
1335
+ pattern: /\bdel\b[^\n]*\s\/f\b/i,
1336
+ reason: "Force deletes files (Windows)"
1337
+ }
1338
+ ];
1339
+ var detectDangerousShellCommand = (command) => {
1340
+ const trimmed = command.trim();
1341
+ if (!trimmed) {
1342
+ return null;
1343
+ }
1344
+ const reasons = [];
1345
+ for (const entry of DANGEROUS_PATTERNS) {
1346
+ if (entry.pattern.test(trimmed)) {
1347
+ reasons.push(entry.reason);
1348
+ }
1349
+ }
1350
+ if (reasons.length === 0) {
1351
+ return null;
1352
+ }
1353
+ return { reasons };
1354
+ };
1355
+
1356
+ // src/cli/commands/DefaultCommand.ts
1357
+ var DefaultCommand = class extends Command5 {
1358
+ static paths = [Command5.Default];
1359
+ static usage = Command5.Usage({
1360
+ description: "Convert natural language to shell commands",
1361
+ examples: [
1362
+ ["Find large files", "$0 find files larger than 100mb"],
1363
+ ["Kill a port", "$0 kill whatever is running on port 3000"],
1364
+ ["Use a shortcut", "$0 killport 3000"]
1365
+ ]
1366
+ });
1367
+ query = Option3.Rest({ required: 0 });
1368
+ async execute() {
1369
+ if (this.query.length === 0) {
1370
+ this.showHelp();
1371
+ return 0;
1372
+ }
1373
+ const config = loadConfig();
1374
+ const historyEnabled = config?.settings?.historyEnabled !== false;
1375
+ const shortcut = await tryResolveShortcut(this.query);
1376
+ if (shortcut) {
1377
+ let historyId2 = null;
1378
+ if (historyEnabled) {
1379
+ historyId2 = recordCommand({
1380
+ query: this.query.join(" "),
1381
+ command: shortcut.command,
1382
+ source: "shortcut"
1383
+ });
1384
+ }
1385
+ const context = {
1386
+ queryText: this.query.join(" "),
1387
+ historyId: historyId2,
1388
+ historyEnabled,
1389
+ shortcutName: shortcut.name
1390
+ };
1391
+ if (config?.settings?.autoConfirmShortcuts) {
1392
+ return this.executeShortcutAutoConfirm(shortcut.command, context);
1393
+ }
1394
+ return this.executeWithConfirmation(
1395
+ shortcut.command,
1396
+ "shortcut",
1397
+ context
1398
+ );
1399
+ }
1400
+ if (!configExists()) {
1401
+ logger.warn("Bashio is not configured yet.");
1402
+ console.log(pc7.gray("Run 'b --auth' to set up your AI provider.\n"));
1403
+ return 1;
1404
+ }
1405
+ if (!config) {
1406
+ logger.error("Failed to load configuration.");
1407
+ console.log(pc7.gray("Run 'b --auth' to reconfigure.\n"));
1408
+ return 1;
1409
+ }
1410
+ const queryText = this.query.join(" ").trim();
1411
+ const provider = createProvider(config);
1412
+ const spinner = createSpinner("Generating command...").start();
1413
+ let generatedCommand;
1414
+ try {
1415
+ generatedCommand = await provider.generateCommand(queryText);
1416
+ spinner.stop();
1417
+ } catch (err) {
1418
+ spinner.fail("Failed to generate command");
1419
+ logger.error(err instanceof Error ? err.message : "Unknown error");
1420
+ return 1;
1421
+ }
1422
+ generatedCommand = this.cleanCommand(generatedCommand);
1423
+ let historyId = null;
1424
+ if (historyEnabled) {
1425
+ historyId = recordCommand({
1426
+ query: queryText,
1427
+ command: generatedCommand,
1428
+ source: "ai"
1429
+ });
1430
+ }
1431
+ return this.executeWithConfirmation(generatedCommand, "ai", {
1432
+ queryText,
1433
+ historyId,
1434
+ historyEnabled
1435
+ });
1436
+ }
1437
+ showHelp() {
1438
+ console.log(pc7.bold("\n Bashio - Natural language to shell commands\n"));
1439
+ console.log(" Usage:");
1440
+ console.log(pc7.cyan(" b <natural language query>"));
1441
+ console.log(pc7.cyan(" b <shortcut> [arguments]"));
1442
+ console.log();
1443
+ console.log(" Examples:");
1444
+ console.log(pc7.gray(" b find all files larger than 100mb"));
1445
+ console.log(pc7.gray(" b kill whatever is running on port 3000"));
1446
+ console.log(pc7.gray(" b killport 3000") + pc7.cyan(" (shortcut)"));
1447
+ console.log();
1448
+ console.log(" Commands:");
1449
+ console.log(pc7.gray(" b --auth Configure AI provider"));
1450
+ console.log(
1451
+ pc7.gray(" b --config View current configuration")
1452
+ );
1453
+ console.log(pc7.gray(" b --model Change AI model"));
1454
+ console.log(pc7.gray(" b --shortcuts List all shortcuts"));
1455
+ console.log(pc7.gray(" b --add-shortcut Add a new shortcut"));
1456
+ console.log(pc7.gray(" b --remove-shortcut Remove a shortcut"));
1457
+ console.log(pc7.gray(" b --edit-shortcuts Edit shortcuts in editor"));
1458
+ console.log();
1459
+ console.log(" History & Stats:");
1460
+ console.log(pc7.gray(" b --history View command history"));
1461
+ console.log(pc7.gray(" b --stats View usage statistics"));
1462
+ console.log(pc7.gray(" b --clear-history Clear command history"));
1463
+ console.log(pc7.gray(" b --suggest-shortcuts Suggest new shortcuts"));
1464
+ console.log();
1465
+ console.log(pc7.gray(" b --help Show help"));
1466
+ console.log();
1467
+ }
1468
+ async executeWithConfirmation(command, source, context) {
1469
+ let currentCommand = command;
1470
+ console.log();
1471
+ if (source === "shortcut" && context.shortcutName) {
1472
+ console.log(pc7.gray(` [shortcut: ${context.shortcutName}]`));
1473
+ }
1474
+ logger.command(currentCommand);
1475
+ console.log();
1476
+ let action = await this.promptConfirmation();
1477
+ while (action !== "yes" && action !== "no" && action !== "copy") {
1478
+ if (action === "explain") {
1479
+ if (source === "shortcut") {
1480
+ console.log(
1481
+ pc7.gray("\n This command comes from a shortcut, not AI.\n")
1482
+ );
1483
+ } else {
1484
+ const explainConfig = loadConfig();
1485
+ if (explainConfig) {
1486
+ const provider = createProvider(explainConfig);
1487
+ const explainSpinner = createSpinner(
1488
+ "Getting explanation..."
1489
+ ).start();
1490
+ try {
1491
+ const explanation = await provider.explainCommand(currentCommand);
1492
+ explainSpinner.stop();
1493
+ console.log();
1494
+ console.log(pc7.bold(" Explanation:"));
1495
+ console.log(pc7.gray(` ${explanation.split("\n").join("\n ")}`));
1496
+ console.log();
1497
+ } catch {
1498
+ explainSpinner.fail("Failed to get explanation");
1499
+ }
1500
+ }
1501
+ }
1502
+ } else if (action === "edit") {
1503
+ try {
1504
+ const edited = await this.editCommandInline(currentCommand);
1505
+ currentCommand = edited.trim();
1506
+ console.log();
1507
+ logger.command(currentCommand);
1508
+ console.log();
1509
+ } catch {
1510
+ logger.error("Edit cancelled");
1511
+ }
1512
+ }
1513
+ action = await this.promptConfirmation();
1514
+ }
1515
+ if (action === "copy") {
1516
+ const success = await copyToClipboard(currentCommand);
1517
+ if (success) {
1518
+ logger.success("Copied to clipboard!");
1519
+ } else {
1520
+ logger.error("Failed to copy to clipboard");
1521
+ }
1522
+ console.log();
1523
+ return 0;
1524
+ }
1525
+ if (action === "no") {
1526
+ logger.info("Cancelled.");
1527
+ return 0;
1528
+ }
1529
+ const danger = detectDangerousShellCommand(currentCommand);
1530
+ if (danger) {
1531
+ const confirmed = await this.promptDangerConfirmation(
1532
+ currentCommand,
1533
+ danger.reasons
1534
+ );
1535
+ if (!confirmed) {
1536
+ logger.info("Cancelled.");
1537
+ return 0;
1538
+ }
1539
+ }
1540
+ console.log();
1541
+ const result = await executeCommand(currentCommand);
1542
+ if (result.exitCode !== 0) {
1543
+ console.log();
1544
+ logger.exitCode(result.exitCode);
1545
+ }
1546
+ console.log();
1547
+ if (context.historyEnabled && context.historyId !== null) {
1548
+ markExecuted(context.historyId, result.exitCode);
1549
+ }
1550
+ return result.exitCode;
1551
+ }
1552
+ async executeShortcutAutoConfirm(command, context) {
1553
+ const danger = detectDangerousShellCommand(command);
1554
+ if (danger) {
1555
+ console.log();
1556
+ if (context.shortcutName) {
1557
+ console.log(pc7.gray(` [shortcut: ${context.shortcutName}]`));
1558
+ }
1559
+ logger.command(command);
1560
+ console.log();
1561
+ const confirmed = await this.promptDangerConfirmation(
1562
+ command,
1563
+ danger.reasons
1564
+ );
1565
+ if (!confirmed) {
1566
+ logger.info("Cancelled.");
1567
+ return 0;
1568
+ }
1569
+ }
1570
+ console.log();
1571
+ const result = await executeCommand(command);
1572
+ if (result.exitCode !== 0) {
1573
+ console.log();
1574
+ logger.exitCode(result.exitCode);
1575
+ }
1576
+ console.log();
1577
+ if (context.historyEnabled && context.historyId !== null) {
1578
+ markExecuted(context.historyId, result.exitCode);
1579
+ }
1580
+ return result.exitCode;
1581
+ }
1582
+ cleanCommand(command) {
1583
+ let cleaned = command.trim();
1584
+ if (cleaned.startsWith("```")) {
1585
+ const lines = cleaned.split("\n");
1586
+ const startIdx = lines[0].startsWith("```") ? 1 : 0;
1587
+ const endIdx = lines[lines.length - 1] === "```" ? lines.length - 1 : lines.length;
1588
+ cleaned = lines.slice(startIdx, endIdx).join("\n");
1589
+ }
1590
+ cleaned = cleaned.replace(/^`|`$/g, "");
1591
+ return cleaned.trim();
1592
+ }
1593
+ async promptConfirmation() {
1594
+ const answer = await input4({
1595
+ message: "Execute? (y/n/e/c/edit)",
1596
+ default: "y"
1597
+ });
1598
+ const normalized = answer.toLowerCase().trim();
1599
+ if (["y", "yes", ""].includes(normalized)) {
1600
+ return "yes";
1601
+ }
1602
+ if (["e", "explain"].includes(normalized)) {
1603
+ return "explain";
1604
+ }
1605
+ if (["c", "copy"].includes(normalized)) {
1606
+ return "copy";
1607
+ }
1608
+ if (["edit"].includes(normalized)) {
1609
+ return "edit";
1610
+ }
1611
+ return "no";
1612
+ }
1613
+ async promptDangerConfirmation(command, reasons) {
1614
+ for (const line of renderDangerBanner(
1615
+ "This command may cause irreversible changes."
1616
+ )) {
1617
+ console.log(line);
1618
+ }
1619
+ console.log();
1620
+ console.log(pc7.yellow(" Reasons:"));
1621
+ for (const reason of reasons) {
1622
+ console.log(pc7.yellow(` - ${reason}`));
1623
+ }
1624
+ console.log();
1625
+ console.log(pc7.gray(" Command:"));
1626
+ console.log(pc7.cyan(` ${command}`));
1627
+ console.log();
1628
+ return confirm2({
1629
+ message: "Proceed with this command?",
1630
+ default: false
1631
+ });
1632
+ }
1633
+ editCommandInline(currentCommand) {
1634
+ return new Promise((resolve, reject) => {
1635
+ const rl = readline.createInterface({
1636
+ input: process.stdin,
1637
+ output: process.stdout
1638
+ });
1639
+ process.stdout.write(`${pc7.green("?")} ${pc7.bold("Edit command:")} `);
1640
+ rl.write(currentCommand);
1641
+ rl.on("line", (answer) => {
1642
+ rl.close();
1643
+ resolve(answer || currentCommand);
1644
+ });
1645
+ rl.on("close", () => {
1646
+ resolve(currentCommand);
1647
+ });
1648
+ rl.on("SIGINT", () => {
1649
+ rl.close();
1650
+ reject(new Error("Edit cancelled"));
1651
+ });
1652
+ });
1653
+ }
1654
+ };
1655
+
1656
+ // src/cli/commands/EditShortcutsCommand.ts
1657
+ import { spawn as spawn2 } from "child_process";
1658
+ import { Command as Command6 } from "clipanion";
1659
+ import pc8 from "picocolors";
1660
+ var EditShortcutsCommand = class extends Command6 {
1661
+ static paths = [["edit-shortcuts"], ["--edit-shortcuts"]];
1662
+ static usage = Command6.Usage({
1663
+ description: "Open shortcuts file in your default editor",
1664
+ examples: [["Edit shortcuts", "$0 --edit-shortcuts"]]
1665
+ });
1666
+ async execute() {
1667
+ const filePath = getShortcutsFilePath();
1668
+ if (!shortcutsFileExists()) {
1669
+ saveShortcuts({ version: 1, shortcuts: {} });
1670
+ logger.info("Created new shortcuts file.");
1671
+ }
1672
+ const editor = process.env.EDITOR || process.env.VISUAL || this.getDefaultEditor();
1673
+ if (!editor) {
1674
+ logger.error("No editor found. Set $EDITOR environment variable.");
1675
+ console.log(pc8.gray(`
1676
+ Example: export EDITOR=vim`));
1677
+ console.log(pc8.gray(` Or: export EDITOR="code --wait"`));
1678
+ console.log(pc8.gray(`
1679
+ File location: ${filePath}
1680
+ `));
1681
+ return 1;
1682
+ }
1683
+ console.log(pc8.gray(`
1684
+ Opening ${filePath} in ${editor}...
1685
+ `));
1686
+ return new Promise((resolve) => {
1687
+ const parts = editor.split(" ");
1688
+ const editorCmd = parts[0];
1689
+ const editorArgs = [...parts.slice(1), filePath];
1690
+ const child = spawn2(editorCmd, editorArgs, {
1691
+ stdio: "inherit",
1692
+ shell: true
1693
+ });
1694
+ child.on("close", (code) => {
1695
+ if (code === 0) {
1696
+ logger.success("Shortcuts file saved.");
1697
+ } else {
1698
+ logger.warn("Editor closed with non-zero exit code.");
1699
+ }
1700
+ console.log();
1701
+ resolve(code ?? 0);
1702
+ });
1703
+ child.on("error", (err) => {
1704
+ logger.error(`Failed to open editor: ${err.message}`);
1705
+ console.log(pc8.gray(`
1706
+ File location: ${filePath}
1707
+ `));
1708
+ resolve(1);
1709
+ });
1710
+ });
1711
+ }
1712
+ getDefaultEditor() {
1713
+ const platform = process.platform;
1714
+ if (platform === "darwin") {
1715
+ return "nano";
1716
+ }
1717
+ if (platform === "win32") {
1718
+ return "notepad";
1719
+ }
1720
+ return "nano";
1721
+ }
1722
+ };
1723
+
1724
+ // src/cli/commands/HistoryCommand.ts
1725
+ import { Command as Command7, Option as Option4 } from "clipanion";
1726
+ import pc10 from "picocolors";
1727
+
1728
+ // src/utils/table.ts
1729
+ import pc9 from "picocolors";
1730
+ var BOX = {
1731
+ topLeft: "\u250C",
1732
+ topRight: "\u2510",
1733
+ bottomLeft: "\u2514",
1734
+ bottomRight: "\u2518",
1735
+ horizontal: "\u2500",
1736
+ vertical: "\u2502",
1737
+ topMid: "\u252C",
1738
+ bottomMid: "\u2534",
1739
+ leftMid: "\u251C",
1740
+ rightMid: "\u2524",
1741
+ midMid: "\u253C"
1742
+ };
1743
+ function padString(str, width, align = "left") {
1744
+ const strippedLength = stripAnsi(str).length;
1745
+ const padding = Math.max(0, width - strippedLength);
1746
+ if (align === "right") {
1747
+ return " ".repeat(padding) + str;
1748
+ }
1749
+ if (align === "center") {
1750
+ const leftPad = Math.floor(padding / 2);
1751
+ const rightPad = padding - leftPad;
1752
+ return " ".repeat(leftPad) + str + " ".repeat(rightPad);
1753
+ }
1754
+ return str + " ".repeat(padding);
1755
+ }
1756
+ function stripAnsi(str) {
1757
+ return str.replace(/\x1B\[[0-9;]*m/g, "");
1758
+ }
1759
+ function wrapText(text, maxWidth) {
1760
+ const words = text.split(" ");
1761
+ const lines = [];
1762
+ let currentLine = "";
1763
+ for (const word of words) {
1764
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
1765
+ if (stripAnsi(testLine).length <= maxWidth) {
1766
+ currentLine = testLine;
1767
+ } else {
1768
+ if (currentLine) {
1769
+ lines.push(currentLine);
1770
+ }
1771
+ if (stripAnsi(word).length > maxWidth) {
1772
+ currentLine = `${word.slice(0, maxWidth - 3)}...`;
1773
+ } else {
1774
+ currentLine = word;
1775
+ }
1776
+ }
1777
+ }
1778
+ if (currentLine) {
1779
+ lines.push(currentLine);
1780
+ }
1781
+ return lines.length > 0 ? lines : [""];
1782
+ }
1783
+ function renderTable(options) {
1784
+ const { columns, data, title } = options;
1785
+ if (title) {
1786
+ console.log(pc9.bold(`
1787
+ ${title}
1788
+ `));
1789
+ }
1790
+ let topBorder = ` ${BOX.topLeft}`;
1791
+ for (let i = 0; i < columns.length; i++) {
1792
+ topBorder += BOX.horizontal.repeat(columns[i].width + 2);
1793
+ topBorder += i < columns.length - 1 ? BOX.topMid : BOX.topRight;
1794
+ }
1795
+ console.log(pc9.dim(topBorder));
1796
+ let headerRow = ` ${BOX.vertical}`;
1797
+ for (const col of columns) {
1798
+ headerRow += ` ${pc9.bold(padString(col.header, col.width, col.align))} ${BOX.vertical}`;
1799
+ }
1800
+ console.log(
1801
+ pc9.dim(headerRow.slice(0, 2)) + headerRow.slice(2, -1) + pc9.dim(BOX.vertical)
1802
+ );
1803
+ let headerSep = ` ${BOX.leftMid}`;
1804
+ for (let i = 0; i < columns.length; i++) {
1805
+ headerSep += BOX.horizontal.repeat(columns[i].width + 2);
1806
+ headerSep += i < columns.length - 1 ? BOX.midMid : BOX.rightMid;
1807
+ }
1808
+ console.log(pc9.dim(headerSep));
1809
+ for (const row of data) {
1810
+ const wrappedColumns = columns.map((col) => {
1811
+ const value = row[col.key] || "";
1812
+ return wrapText(value, col.width);
1813
+ });
1814
+ const maxLines = Math.max(...wrappedColumns.map((lines) => lines.length));
1815
+ for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
1816
+ let dataRow = ` ${BOX.vertical}`;
1817
+ for (let colIdx = 0; colIdx < columns.length; colIdx++) {
1818
+ const col = columns[colIdx];
1819
+ const lines = wrappedColumns[colIdx];
1820
+ const lineValue = lines[lineIdx] || "";
1821
+ let cellValue = padString(lineValue, col.width, col.align);
1822
+ if (col.color && lineValue) {
1823
+ cellValue = col.color(cellValue);
1824
+ }
1825
+ dataRow += ` ${cellValue} ${BOX.vertical}`;
1826
+ }
1827
+ console.log(
1828
+ pc9.dim(dataRow.slice(0, 2)) + dataRow.slice(2, -1) + pc9.dim(BOX.vertical)
1829
+ );
1830
+ }
1831
+ }
1832
+ let bottomBorder = ` ${BOX.bottomLeft}`;
1833
+ for (let i = 0; i < columns.length; i++) {
1834
+ bottomBorder += BOX.horizontal.repeat(columns[i].width + 2);
1835
+ bottomBorder += i < columns.length - 1 ? BOX.bottomMid : BOX.bottomRight;
1836
+ }
1837
+ console.log(pc9.dim(bottomBorder));
1838
+ }
1839
+
1840
+ // src/cli/commands/HistoryCommand.ts
1841
+ var HistoryCommand = class extends Command7 {
1842
+ static paths = [["--history"]];
1843
+ static usage = Command7.Usage({
1844
+ description: "View command history",
1845
+ examples: [
1846
+ ["View recent history", "$0 --history"],
1847
+ ["Limit results", "$0 --history --limit 10"],
1848
+ ["Search history", "$0 --history --search git"]
1849
+ ]
1850
+ });
1851
+ limit = Option4.String("--limit,-l", "20", {
1852
+ description: "Number of entries to show"
1853
+ });
1854
+ search = Option4.String("--search,-s", {
1855
+ description: "Search term to filter history"
1856
+ });
1857
+ async execute() {
1858
+ const limitNum = Number.parseInt(this.limit, 10) || 20;
1859
+ let entries;
1860
+ let title;
1861
+ if (this.search) {
1862
+ entries = searchHistory(this.search, limitNum);
1863
+ title = `Command History (search: "${this.search}")`;
1864
+ } else {
1865
+ entries = getRecentHistory(limitNum);
1866
+ title = "Recent Command History";
1867
+ }
1868
+ if (entries.length === 0) {
1869
+ console.log(pc10.bold(`
1870
+ ${title}
1871
+ `));
1872
+ console.log(pc10.gray(" No history entries found.\n"));
1873
+ return 0;
1874
+ }
1875
+ const data = entries.map((entry, index) => ({
1876
+ num: (index + 1).toString(),
1877
+ command: entry.command,
1878
+ query: entry.source === "ai" ? entry.query : "-",
1879
+ source: entry.source,
1880
+ status: this.getStatusDisplay(entry),
1881
+ time: this.getTimeAgo(entry.createdAt)
1882
+ }));
1883
+ renderTable({
1884
+ title,
1885
+ columns: [
1886
+ { header: "#", key: "num", width: 3, align: "right" },
1887
+ { header: "Command", key: "command", width: 35, color: pc10.yellow },
1888
+ { header: "Query", key: "query", width: 25, color: pc10.gray },
1889
+ {
1890
+ header: "Source",
1891
+ key: "source",
1892
+ width: 8,
1893
+ color: (v) => v.trim() === "ai" ? pc10.blue(v) : pc10.cyan(v)
1894
+ },
1895
+ { header: "Status", key: "status", width: 10 },
1896
+ { header: "Time", key: "time", width: 10 }
1897
+ ],
1898
+ data
1899
+ });
1900
+ console.log(pc10.dim(`
1901
+ Showing ${entries.length} entries
1902
+ `));
1903
+ return 0;
1904
+ }
1905
+ getStatusDisplay(entry) {
1906
+ if (entry.executed === 0) {
1907
+ return `${pc10.gray("\u25CB")} skipped`;
1908
+ }
1909
+ if (entry.exitCode === 0) {
1910
+ return `${pc10.green("\u2713")} success`;
1911
+ }
1912
+ return `${pc10.red("\u2717")} exit:${entry.exitCode}`;
1913
+ }
1914
+ getTimeAgo(isoString) {
1915
+ const date = new Date(isoString);
1916
+ const now = /* @__PURE__ */ new Date();
1917
+ const diffMs = now.getTime() - date.getTime();
1918
+ const minutes = Math.floor(diffMs / (1e3 * 60));
1919
+ const hours = Math.floor(diffMs / (1e3 * 60 * 60));
1920
+ const days = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
1921
+ if (minutes < 1) return "just now";
1922
+ if (minutes < 60) return `${minutes}m ago`;
1923
+ if (hours < 24) return `${hours}h ago`;
1924
+ if (days < 7) return `${days}d ago`;
1925
+ return date.toLocaleDateString();
1926
+ }
1927
+ };
1928
+
1929
+ // src/cli/commands/ModelCommand.ts
1930
+ import * as readline2 from "readline";
1931
+ import { select as select2 } from "@inquirer/prompts";
1932
+ import { Command as Command8 } from "clipanion";
1933
+ import pc11 from "picocolors";
1934
+ var selectWithEsc = async (config) => {
1935
+ const controller = new AbortController();
1936
+ const input6 = process.stdin;
1937
+ const previousRawMode = input6.isTTY && typeof input6.isRaw === "boolean" ? input6.isRaw : null;
1938
+ const onKeypress = (_input, key) => {
1939
+ if (key.name === "escape") {
1940
+ controller.abort();
1941
+ }
1942
+ };
1943
+ readline2.emitKeypressEvents(input6);
1944
+ if (input6.isTTY && typeof input6.setRawMode === "function") {
1945
+ input6.setRawMode(true);
1946
+ }
1947
+ input6.on("keypress", onKeypress);
1948
+ try {
1949
+ return await select2(config, { signal: controller.signal });
1950
+ } finally {
1951
+ input6.off("keypress", onKeypress);
1952
+ if (input6.isTTY && typeof input6.setRawMode === "function" && previousRawMode !== null) {
1953
+ input6.setRawMode(previousRawMode);
1954
+ }
1955
+ }
1956
+ };
1957
+ var isPromptExit = (error) => {
1958
+ if (!(error instanceof Error)) {
1959
+ return false;
1960
+ }
1961
+ return error.name === "ExitPromptError" || error.name === "AbortPromptError" || error.name === "CancelPromptError";
1962
+ };
1963
+ var ModelCommand = class extends Command8 {
1964
+ static paths = [["model"], ["--model"]];
1965
+ static usage = Command8.Usage({
1966
+ description: "Change the AI model for current provider",
1967
+ examples: [["Change model", "$0 --model"]]
1968
+ });
1969
+ async execute() {
1970
+ if (!configExists()) {
1971
+ logger.warn("Bashio is not configured yet.");
1972
+ console.log(
1973
+ pc11.gray("Run 'b --auth' to set up your AI provider first.\n")
1974
+ );
1975
+ return 1;
1976
+ }
1977
+ const config = loadConfig();
1978
+ if (!config) {
1979
+ logger.error("Failed to load configuration.");
1980
+ return 1;
1981
+ }
1982
+ console.log(pc11.bold("\n Change AI Model\n"));
1983
+ console.log(pc11.gray(` Current provider: ${config.provider}`));
1984
+ console.log(pc11.gray(` Current model: ${config.model}`));
1985
+ console.log(pc11.dim(" Press Esc to cancel\n"));
1986
+ let newModel;
1987
+ try {
1988
+ switch (config.provider) {
1989
+ case "claude": {
1990
+ newModel = await selectWithEsc({
1991
+ message: "Select new model:",
1992
+ choices: CLAUDE_MODELS.map((m) => ({
1993
+ value: m.value,
1994
+ name: m.label
1995
+ })),
1996
+ default: config.model
1997
+ });
1998
+ break;
1999
+ }
2000
+ case "openai": {
2001
+ newModel = await selectWithEsc({
2002
+ message: "Select new model:",
2003
+ choices: OPENAI_MODELS.map((m) => ({
2004
+ value: m.value,
2005
+ name: m.label
2006
+ })),
2007
+ default: config.model
2008
+ });
2009
+ break;
2010
+ }
2011
+ case "ollama": {
2012
+ const host = config.credentials.type === "local" ? config.credentials.host : "http://localhost:11434";
2013
+ const spinner = createSpinner("Fetching available models...").start();
2014
+ const availableModels = await OllamaProvider.getAvailableModels(host);
2015
+ spinner.stop();
2016
+ if (availableModels.length === 0) {
2017
+ logger.warn("No models found. Make sure Ollama is running.");
2018
+ console.log(pc11.gray("\n Install a model: ollama pull llama3.2\n"));
2019
+ return 1;
2020
+ }
2021
+ newModel = await selectWithEsc({
2022
+ message: "Select new model:",
2023
+ choices: availableModels.map((m) => ({
2024
+ value: m,
2025
+ name: m
2026
+ })),
2027
+ default: config.model
2028
+ });
2029
+ break;
2030
+ }
2031
+ case "openrouter": {
2032
+ newModel = await selectWithEsc({
2033
+ message: "Select new model:",
2034
+ choices: OPENROUTER_MODELS.map((m) => ({
2035
+ value: m.value,
2036
+ name: m.label
2037
+ })),
2038
+ default: config.model
2039
+ });
2040
+ break;
2041
+ }
2042
+ default:
2043
+ logger.error(`Unknown provider: ${config.provider}`);
2044
+ return 1;
2045
+ }
2046
+ } catch (error) {
2047
+ if (isPromptExit(error)) {
2048
+ console.log(pc11.dim("\n Cancelled.\n"));
2049
+ return 0;
2050
+ }
2051
+ throw error;
2052
+ }
2053
+ if (newModel === config.model) {
2054
+ logger.info("Model unchanged.");
2055
+ return 0;
2056
+ }
2057
+ config.model = newModel;
2058
+ saveConfig(config);
2059
+ console.log();
2060
+ logger.success(`Model changed to: ${newModel}`);
2061
+ console.log();
2062
+ return 0;
2063
+ }
2064
+ };
2065
+
2066
+ // src/cli/commands/RemoveShortcutCommand.ts
2067
+ import { confirm as confirm3 } from "@inquirer/prompts";
2068
+ import { Command as Command9, Option as Option5 } from "clipanion";
2069
+ import pc12 from "picocolors";
2070
+ var RemoveShortcutCommand = class extends Command9 {
2071
+ static paths = [["remove-shortcut"], ["--remove-shortcut"]];
2072
+ static usage = Command9.Usage({
2073
+ description: "Remove a shortcut",
2074
+ examples: [["Remove shortcut", "$0 --remove-shortcut killport"]]
2075
+ });
2076
+ name = Option5.String({ required: true });
2077
+ async execute() {
2078
+ const shortcut = getShortcut(this.name);
2079
+ if (!shortcut) {
2080
+ logger.error(`Shortcut "${this.name}" not found.`);
2081
+ return 1;
2082
+ }
2083
+ console.log();
2084
+ console.log(pc12.gray(` Template: ${shortcut.template}`));
2085
+ if (shortcut.description) {
2086
+ console.log(pc12.gray(` Description: ${shortcut.description}`));
2087
+ }
2088
+ console.log();
2089
+ for (const line of renderDangerBanner(
2090
+ `This will permanently remove shortcut "${this.name}".`
2091
+ )) {
2092
+ console.log(line);
2093
+ }
2094
+ console.log();
2095
+ const confirmed = await confirm3({
2096
+ message: `Proceed with removing "${this.name}"?`,
2097
+ default: false
2098
+ });
2099
+ if (!confirmed) {
2100
+ logger.info("Cancelled.");
2101
+ return 0;
2102
+ }
2103
+ removeShortcut(this.name);
2104
+ logger.success(`Shortcut "${this.name}" removed.`);
2105
+ console.log();
2106
+ return 0;
2107
+ }
2108
+ };
2109
+
2110
+ // src/cli/commands/ShortcutsCommand.ts
2111
+ import { Command as Command10 } from "clipanion";
2112
+ import pc13 from "picocolors";
2113
+ var ShortcutsCommand = class extends Command10 {
2114
+ static paths = [["shortcuts"], ["--shortcuts"]];
2115
+ static usage = Command10.Usage({
2116
+ description: "List all configured shortcuts",
2117
+ examples: [["List shortcuts", "$0 --shortcuts"]]
2118
+ });
2119
+ async execute() {
2120
+ const shortcuts = listShortcuts();
2121
+ const names = Object.keys(shortcuts);
2122
+ if (names.length === 0) {
2123
+ console.log(pc13.yellow("\n No shortcuts configured yet.\n"));
2124
+ console.log(pc13.gray(" Add one with: s --add-shortcut\n"));
2125
+ return 0;
2126
+ }
2127
+ const data = names.map((name) => {
2128
+ const shortcut = shortcuts[name];
2129
+ return {
2130
+ name,
2131
+ template: shortcut.template,
2132
+ args: shortcut.args?.length ? shortcut.args.join(", ") : "-"
2133
+ };
2134
+ });
2135
+ renderTable({
2136
+ title: "Your Shortcuts",
2137
+ columns: [
2138
+ { header: "Name", key: "name", width: 15, color: pc13.cyan },
2139
+ {
2140
+ header: "Command Template",
2141
+ key: "template",
2142
+ width: 45,
2143
+ color: pc13.white
2144
+ },
2145
+ { header: "Arguments", key: "args", width: 15, color: pc13.gray }
2146
+ ],
2147
+ data
2148
+ });
2149
+ console.log(
2150
+ pc13.dim(
2151
+ `
2152
+ Total: ${names.length} shortcut${names.length === 1 ? "" : "s"}
2153
+ `
2154
+ )
2155
+ );
2156
+ return 0;
2157
+ }
2158
+ };
2159
+
2160
+ // src/cli/commands/StatsCommand.ts
2161
+ import { Command as Command11 } from "clipanion";
2162
+ import pc14 from "picocolors";
2163
+ var StatsCommand = class extends Command11 {
2164
+ static paths = [["--stats"]];
2165
+ static usage = Command11.Usage({
2166
+ description: "View usage statistics",
2167
+ examples: [["View stats", "$0 --stats"]]
2168
+ });
2169
+ async execute() {
2170
+ const stats = getStats();
2171
+ console.log(pc14.bold("\n Bashio Usage Statistics\n"));
2172
+ renderTable({
2173
+ title: "Overview",
2174
+ columns: [
2175
+ { header: "Metric", key: "metric", width: 20 },
2176
+ { header: "Value", key: "value", width: 15, align: "right" }
2177
+ ],
2178
+ data: [
2179
+ { metric: "Commands Generated", value: stats.totalCommands.toString() },
2180
+ {
2181
+ metric: "Executed",
2182
+ value: `${stats.totalExecuted} (${stats.executionRate}%)`
2183
+ },
2184
+ { metric: "Today", value: stats.todayCommands.toString() },
2185
+ { metric: "This Week", value: stats.thisWeekCommands.toString() }
2186
+ ]
2187
+ });
2188
+ console.log();
2189
+ const total = stats.aiCount + stats.shortcutCount;
2190
+ const aiPercent = total > 0 ? Math.round(stats.aiCount / total * 100) : 0;
2191
+ const shortcutPercent = total > 0 ? Math.round(stats.shortcutCount / total * 100) : 0;
2192
+ renderTable({
2193
+ title: "Source Breakdown",
2194
+ columns: [
2195
+ { header: "Source", key: "source", width: 15 },
2196
+ { header: "Count", key: "count", width: 10, align: "right" },
2197
+ { header: "Percentage", key: "percent", width: 12, align: "right" }
2198
+ ],
2199
+ data: [
2200
+ {
2201
+ source: "AI Generated",
2202
+ count: stats.aiCount.toString(),
2203
+ percent: `${aiPercent}%`
2204
+ },
2205
+ {
2206
+ source: "Shortcuts",
2207
+ count: stats.shortcutCount.toString(),
2208
+ percent: `${shortcutPercent}%`
2209
+ }
2210
+ ]
2211
+ });
2212
+ if (stats.topQueries.length > 0) {
2213
+ console.log();
2214
+ renderTable({
2215
+ title: "Most Used Commands",
2216
+ columns: [
2217
+ { header: "#", key: "rank", width: 3, align: "right" },
2218
+ { header: "Command", key: "command", width: 45, color: pc14.cyan },
2219
+ { header: "Uses", key: "uses", width: 8, align: "right" },
2220
+ {
2221
+ header: "Source",
2222
+ key: "source",
2223
+ width: 10,
2224
+ color: (v) => v.trim() === "ai" ? pc14.blue(v) : pc14.cyan(v)
2225
+ }
2226
+ ],
2227
+ data: stats.topQueries.map((q, i) => ({
2228
+ rank: (i + 1).toString(),
2229
+ command: q.query.length > 42 ? `${q.query.slice(0, 39)}...` : q.query,
2230
+ uses: q.useCount.toString(),
2231
+ source: q.source
2232
+ }))
2233
+ });
2234
+ }
2235
+ console.log();
2236
+ return 0;
2237
+ }
2238
+ };
2239
+
2240
+ // src/cli/commands/SuggestShortcutsCommand.ts
2241
+ import { confirm as confirm4, input as input5 } from "@inquirer/prompts";
2242
+ import { Command as Command12, Option as Option6 } from "clipanion";
2243
+ import pc15 from "picocolors";
2244
+
2245
+ // src/core/learning.ts
2246
+ function suggestShortcutNameFromCommand(command) {
2247
+ const parts = command.split(/[\s\-&|;()]+/).filter((p) => p.length > 0 && !p.match(/^[.+]/));
2248
+ if (parts.length === 0) {
2249
+ return "cmd";
2250
+ }
2251
+ const mainCmd = parts[0];
2252
+ if (parts.length > 1) {
2253
+ const keyArg = parts[1];
2254
+ if (keyArg && keyArg.length > 0) {
2255
+ const combined = `${mainCmd}${keyArg}`.slice(0, 12);
2256
+ return combined;
2257
+ }
2258
+ }
2259
+ return mainCmd.slice(0, 12);
2260
+ }
2261
+ function getShortcutSuggestions(threshold = 3) {
2262
+ const db2 = getDatabase();
2263
+ const rows = db2.prepare(
2264
+ `SELECT id, command, source, use_count, success_count, suggested
2265
+ FROM query_stats
2266
+ WHERE source = 'ai'
2267
+ AND use_count >= ?
2268
+ AND suggested = 0
2269
+ ORDER BY use_count DESC
2270
+ LIMIT 10`
2271
+ ).all(threshold);
2272
+ const existingShortcuts = listShortcuts();
2273
+ const existingNames = new Set(Object.keys(existingShortcuts));
2274
+ const suggestions = [];
2275
+ for (const row of rows) {
2276
+ let suggestedName = suggestShortcutNameFromCommand(row.command);
2277
+ let counter = 1;
2278
+ const baseName = suggestedName;
2279
+ while (existingNames.has(suggestedName)) {
2280
+ suggestedName = `${baseName}${counter}`;
2281
+ counter++;
2282
+ }
2283
+ suggestions.push({
2284
+ query: row.command,
2285
+ // Store command as "query" for now (for interface compatibility)
2286
+ command: row.command,
2287
+ useCount: row.use_count,
2288
+ suggestedName
2289
+ });
2290
+ }
2291
+ return suggestions;
2292
+ }
2293
+ function markPatternAsSuggested(command) {
2294
+ const db2 = getDatabase();
2295
+ db2.prepare("UPDATE query_stats SET suggested = 1 WHERE command = ?").run(
2296
+ command
2297
+ );
2298
+ }
2299
+
2300
+ // src/cli/commands/SuggestShortcutsCommand.ts
2301
+ var SuggestShortcutsCommand = class extends Command12 {
2302
+ static paths = [["--suggest-shortcuts"]];
2303
+ static usage = Command12.Usage({
2304
+ description: "Suggest shortcuts based on frequently used commands",
2305
+ examples: [
2306
+ ["Get suggestions", "$0 --suggest-shortcuts"],
2307
+ ["Set minimum use count", "$0 --suggest-shortcuts --threshold 5"]
2308
+ ]
2309
+ });
2310
+ threshold = Option6.String("--threshold,-t", "3", {
2311
+ description: "Minimum use count to suggest (default: 3)"
2312
+ });
2313
+ async execute() {
2314
+ const thresholdNum = Number.parseInt(this.threshold, 10) || 3;
2315
+ const suggestions = getShortcutSuggestions(thresholdNum);
2316
+ console.log(pc15.bold("\n Shortcut Suggestions\n"));
2317
+ if (suggestions.length === 0) {
2318
+ console.log(
2319
+ pc15.gray(
2320
+ " No suggestions yet. Use Bashio more to get personalized suggestions."
2321
+ )
2322
+ );
2323
+ console.log(
2324
+ pc15.dim(` (Commands need to be used ${thresholdNum}+ times)
2325
+ `)
2326
+ );
2327
+ return 0;
2328
+ }
2329
+ console.log(
2330
+ pc15.dim(
2331
+ ` Found ${suggestions.length} frequently used commands that could be shortcuts:
2332
+ `
2333
+ )
2334
+ );
2335
+ for (const suggestion of suggestions) {
2336
+ console.log(pc15.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2337
+ console.log(` ${pc15.yellow("Command:")} ${pc15.cyan(suggestion.command)}`);
2338
+ console.log(
2339
+ ` ${pc15.yellow("Used:")} ${pc15.white(suggestion.useCount.toString())} times`
2340
+ );
2341
+ console.log();
2342
+ const action = await this.promptCreateShortcut();
2343
+ if (action === "exit") {
2344
+ console.log(pc15.dim("\n Exiting suggestions.\n"));
2345
+ return 0;
2346
+ }
2347
+ if (action === "yes") {
2348
+ const finalName = await this.getUniqueShortcutName(
2349
+ suggestion.suggestedName
2350
+ );
2351
+ if (!finalName) {
2352
+ markPatternAsSuggested(suggestion.command);
2353
+ console.log();
2354
+ continue;
2355
+ }
2356
+ const hasNumbers = /\d+/.test(suggestion.command);
2357
+ let template = suggestion.command;
2358
+ const args2 = [];
2359
+ if (hasNumbers) {
2360
+ const parameterize = await confirm4({
2361
+ message: "Make numbers into parameters?",
2362
+ default: true
2363
+ });
2364
+ if (parameterize) {
2365
+ const numbers = suggestion.command.match(/\d+/g) || [];
2366
+ const uniqueNumbers = [...new Set(numbers)];
2367
+ for (let i = 0; i < uniqueNumbers.length; i++) {
2368
+ const argName = `arg${i + 1}`;
2369
+ args2.push(argName);
2370
+ template = template.replace(
2371
+ new RegExp(uniqueNumbers[i], "g"),
2372
+ `{{${argName}}}`
2373
+ );
2374
+ }
2375
+ }
2376
+ }
2377
+ addShortcut(finalName, {
2378
+ template,
2379
+ args: args2,
2380
+ description: `Auto-suggested shortcut for: ${suggestion.command}`
2381
+ });
2382
+ markPatternAsSuggested(suggestion.command);
2383
+ logger.success(`\u2713 Created shortcut: ${finalName}`);
2384
+ console.log(
2385
+ pc15.dim(
2386
+ ` Usage: s ${finalName}${args2.length > 0 ? ` ${args2.map((a) => `<${a}>`).join(" ")}` : ""}`
2387
+ )
2388
+ );
2389
+ console.log();
2390
+ } else {
2391
+ markPatternAsSuggested(suggestion.command);
2392
+ console.log();
2393
+ }
2394
+ }
2395
+ console.log(pc15.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
2396
+ return 0;
2397
+ }
2398
+ async promptCreateShortcut() {
2399
+ const answer = await input5({
2400
+ message: "Create shortcut? (y/n/e)",
2401
+ default: "y"
2402
+ });
2403
+ const normalized = answer.toLowerCase().trim();
2404
+ if (["y", "yes", ""].includes(normalized)) {
2405
+ return "yes";
2406
+ }
2407
+ if (["e", "exit"].includes(normalized)) {
2408
+ return "exit";
2409
+ }
2410
+ return "no";
2411
+ }
2412
+ async getUniqueShortcutName(defaultName) {
2413
+ let attempts = 0;
2414
+ const maxAttempts = 5;
2415
+ while (attempts < maxAttempts) {
2416
+ const enteredName = await input5({
2417
+ message: "Shortcut name:",
2418
+ default: defaultName
2419
+ });
2420
+ const finalName = enteredName.trim();
2421
+ if (!finalName) {
2422
+ logger.error("Shortcut name cannot be empty.");
2423
+ attempts++;
2424
+ continue;
2425
+ }
2426
+ const existingShortcut = getShortcut(finalName);
2427
+ if (existingShortcut) {
2428
+ this.showExistingShortcutTable(finalName, existingShortcut);
2429
+ attempts++;
2430
+ continue;
2431
+ }
2432
+ return finalName;
2433
+ }
2434
+ logger.error("Too many invalid attempts. Skipping this suggestion.");
2435
+ return null;
2436
+ }
2437
+ showExistingShortcutTable(name, shortcut) {
2438
+ const boxWidth = 50;
2439
+ const line = "\u2500".repeat(boxWidth);
2440
+ console.log();
2441
+ console.log(pc15.red(` \u250C${line}\u2510`));
2442
+ console.log(
2443
+ pc15.red(" \u2502") + pc15.bold(pc15.red(" \u274C Shortcut Already Exists")) + " ".repeat(boxWidth - 27) + pc15.red("\u2502")
2444
+ );
2445
+ console.log(pc15.red(` \u251C${line}\u2524`));
2446
+ const nameLabel = " Name: ";
2447
+ const nameValue = name;
2448
+ const namePadding = boxWidth - nameLabel.length - nameValue.length + 2;
2449
+ console.log(
2450
+ pc15.red(" \u2502") + pc15.gray(nameLabel) + pc15.white(nameValue) + " ".repeat(Math.max(0, namePadding)) + pc15.red("\u2502")
2451
+ );
2452
+ const cmdLabel = " Command: ";
2453
+ const cmdValue = shortcut.template.length > 35 ? `${shortcut.template.slice(0, 32)}...` : shortcut.template;
2454
+ const cmdPadding = boxWidth - cmdLabel.length - cmdValue.length + 2;
2455
+ console.log(
2456
+ pc15.red(" \u2502") + pc15.gray(cmdLabel) + pc15.cyan(cmdValue) + " ".repeat(Math.max(0, cmdPadding)) + pc15.red("\u2502")
2457
+ );
2458
+ if (shortcut.args && shortcut.args.length > 0) {
2459
+ const argsLabel = " Args: ";
2460
+ const argsValue = shortcut.args.join(", ");
2461
+ const argsPadding = boxWidth - argsLabel.length - argsValue.length + 2;
2462
+ console.log(
2463
+ pc15.red(" \u2502") + pc15.gray(argsLabel) + pc15.yellow(argsValue) + " ".repeat(Math.max(0, argsPadding)) + pc15.red("\u2502")
2464
+ );
2465
+ }
2466
+ console.log(pc15.red(` \u2514${line}\u2518`));
2467
+ console.log();
2468
+ console.log(pc15.yellow(" Please enter a different name:\n"));
2469
+ }
2470
+ };
2471
+
2472
+ // src/cli/index.ts
2473
+ initDatabase();
2474
+ if (shouldRunCleanup()) {
2475
+ const config = loadConfig();
2476
+ const retentionDays = config?.settings?.historyRetentionDays ?? 30;
2477
+ const maxEntries = config?.settings?.historyMaxEntries ?? 2e3;
2478
+ cleanupHistory({ retentionDays, maxEntries });
2479
+ }
2480
+ var cli = new Cli({
2481
+ binaryLabel: "Bashio",
2482
+ binaryName: "b",
2483
+ binaryVersion: "0.4.0"
2484
+ });
2485
+ cli.register(DefaultCommand);
2486
+ cli.register(AuthCommand);
2487
+ cli.register(ConfigCommand);
2488
+ cli.register(ModelCommand);
2489
+ cli.register(ShortcutsCommand);
2490
+ cli.register(AddShortcutCommand);
2491
+ cli.register(RemoveShortcutCommand);
2492
+ cli.register(EditShortcutsCommand);
2493
+ cli.register(HistoryCommand);
2494
+ cli.register(StatsCommand);
2495
+ cli.register(ClearHistoryCommand);
2496
+ cli.register(SuggestShortcutsCommand);
2497
+ cli.register(Builtins.HelpCommand);
2498
+ cli.register(Builtins.VersionCommand);
2499
+
2500
+ // src/index.ts
2501
+ var args = process.argv.slice(2);
2502
+ cli.runExit(args);
2503
+ //# sourceMappingURL=index.js.map