caroushell 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,20 +11,28 @@ history, and AI suggestions as you type.
11
11
  - Press `Enter` to run the highlighted command.
12
12
  - Logs activity under `~/.caroushell/logs` for easy troubleshooting.
13
13
  - Extensible config file (`~/.caroushell/config.json`) so you can point the CLI
14
- at different API keys or settings.
14
+ at different AI providers.
15
15
 
16
16
  ## Requirements
17
17
 
18
18
  - Node.js 18 or newer.
19
- - A `~/.caroushell/config.json` file that contains the tokens Caroushell needs.
20
- Currently the file expects a Gemini API key:
19
+ - On first launch Caroushell will prompt you for an OpenAI-compatible endpoint
20
+ URL, API key, and model name, then store them in `~/.caroushell/config.json`.
21
+ - You can also create the file manually:
21
22
 
22
23
  ```json
23
24
  {
24
- "GEMINI_API_KEY": "your-api-key"
25
+ "apiUrl": "https://openrouter.ai/api/v1",
26
+ "apiKey": "your-api-key",
27
+ "model": "gpt-4o-mini"
25
28
  }
26
29
  ```
27
30
 
31
+ Any endpoint that implements the OpenAI Chat Completions API (OpenRouter,
32
+ OpenAI, etc.) will work as long as the URL, key, and model are valid. If you
33
+ only provide a Gemini API key in the config, Caroushell will default to the
34
+ Gemini Flash Lite 2.5 endpoint and model.
35
+
28
36
  ## Installation
29
37
 
30
38
  Install globally (recommended):
@@ -2,87 +2,128 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AISuggester = void 0;
4
4
  exports.generateContent = generateContent;
5
+ exports.listModels = listModels;
5
6
  const logs_1 = require("./logs");
6
7
  const config_1 = require("./config");
7
8
  async function generateContent(prompt, options) {
8
- const apiKey = options?.apiKey || process.env.GEMINI_API_KEY;
9
- const model = options?.model || "gemini-2.5-flash-lite";
9
+ const apiKey = options?.apiKey ||
10
+ process.env.CAROUSHELL_API_KEY ||
11
+ process.env.GEMINI_API_KEY;
12
+ const apiUrl = options?.apiUrl || process.env.CAROUSHELL_API_URL;
13
+ const model = options?.model || process.env.CAROUSHELL_MODEL || process.env.OPENAI_MODEL;
10
14
  const temperature = options?.temperature ?? 0.3;
11
15
  const maxOutputTokens = options?.maxOutputTokens ?? 256;
12
16
  if (!apiKey) {
13
17
  (0, logs_1.logLine)("AI generation skipped: missing API key");
14
18
  return "";
15
19
  }
20
+ if (!apiUrl) {
21
+ (0, logs_1.logLine)("AI generation skipped: missing API URL");
22
+ return "";
23
+ }
24
+ if (!model) {
25
+ (0, logs_1.logLine)("AI generation skipped: missing model");
26
+ return "";
27
+ }
16
28
  if (!prompt.trim()) {
17
29
  (0, logs_1.logLine)("AI generation skipped: empty prompt");
18
30
  return "";
19
31
  }
20
32
  try {
21
33
  const start = Date.now();
22
- const fetchImpl = globalThis.fetch;
23
- if (!fetchImpl)
24
- return "";
25
- const res = await fetchImpl(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, {
34
+ const request = buildRequest({
35
+ apiKey,
36
+ apiUrl,
37
+ model,
38
+ prompt,
39
+ temperature,
40
+ maxOutputTokens,
41
+ });
42
+ const res = await fetch(request.url, request.init);
43
+ if (!res.ok) {
44
+ return "ai fetch error: " + res.statusText;
45
+ }
46
+ const out = await extractText(await res.json());
47
+ const text = typeof out === "string" ? out : "";
48
+ const duration = Date.now() - start;
49
+ (0, logs_1.logLine)(`AI duration: ${duration} ms`);
50
+ return text;
51
+ }
52
+ catch (err) {
53
+ return "ai error: " + err.message;
54
+ }
55
+ }
56
+ async function listModels(apiUrl, apiKey) {
57
+ const url = apiUrl.replace("/chat/completions", "") + "/models";
58
+ const res = await fetch(url, { headers: headers(apiKey) });
59
+ const models = await res.json();
60
+ return models.data.map((m) => m.id);
61
+ }
62
+ function headers(apiKey) {
63
+ return {
64
+ "Content-Type": "application/json",
65
+ Authorization: `Bearer ${apiKey}`,
66
+ "HTTP-Referer": "https://github.com/ubershmekel/caroushell",
67
+ "X-Title": "Caroushell",
68
+ };
69
+ }
70
+ function buildRequest(args) {
71
+ return {
72
+ url: args.apiUrl + "/chat/completions",
73
+ init: {
26
74
  method: "POST",
27
- headers: { "Content-Type": "application/json" },
75
+ headers: headers(args.apiKey),
28
76
  body: JSON.stringify({
29
- contents: [
77
+ model: args.model,
78
+ temperature: args.temperature,
79
+ max_tokens: args.maxOutputTokens,
80
+ messages: [
30
81
  {
31
- role: "user",
32
- parts: [{ text: prompt }],
82
+ role: "system",
83
+ content: "You are a shell assistant that suggests terminal command completions.",
33
84
  },
85
+ { role: "user", content: args.prompt },
34
86
  ],
35
- generationConfig: {
36
- temperature,
37
- maxOutputTokens,
38
- },
39
87
  }),
40
- });
41
- if (!res.ok)
42
- return "";
43
- const json = (await res.json());
44
- const text = json?.candidates?.[0]?.content?.parts?.[0]?.text || "";
45
- const out = typeof text === "string" ? text : "";
46
- const duration = Date.now() - start;
47
- // Log duration and each non-empty line of the AI text
48
- try {
49
- await (0, logs_1.logLine)(`AI duration: ${duration} ms`);
50
- if (out.trim()) {
51
- const lines = out
52
- .split(/\r?\n/)
53
- .map((s) => s.trim())
54
- .filter(Boolean);
55
- // .map((s) => `AI text: ${s}`);
56
- // await logLines(lines);
57
- }
58
- }
59
- catch {
60
- // best-effort logging; ignore failures
61
- }
62
- return out;
63
- }
64
- catch {
65
- return "";
66
- }
88
+ },
89
+ };
90
+ }
91
+ async function extractText(json) {
92
+ const typed = json;
93
+ return typed?.choices?.[0]?.message?.content || "";
67
94
  }
68
95
  class AISuggester {
69
96
  constructor(opts) {
70
97
  this.prefix = "🤖";
71
98
  this.apiKey = opts?.apiKey;
72
- this.model = opts?.model || "gemini-2.5-flash-lite";
99
+ this.apiUrl = opts?.apiUrl;
100
+ this.model = opts?.model;
73
101
  }
74
102
  async init() {
103
+ const config = await (0, config_1.getConfig)();
75
104
  this.apiKey =
76
105
  this.apiKey ||
77
- (await (0, config_1.getConfig)()).GEMINI_API_KEY ||
106
+ config.apiKey ||
107
+ config.GEMINI_API_KEY ||
78
108
  process.env.GEMINI_API_KEY;
109
+ this.apiUrl =
110
+ this.apiUrl || config.apiUrl || process.env.CAROUSHELL_API_URL;
111
+ this.model = this.model || config.model || process.env.CAROUSHELL_MODEL;
112
+ // If the user provided only a Gemini key, default the URL/model accordingly.
113
+ if (!this.apiUrl && config.GEMINI_API_KEY) {
114
+ this.apiUrl =
115
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent";
116
+ }
117
+ if (!this.model && config.GEMINI_API_KEY) {
118
+ this.model = "gemini-2.5-flash-lite";
119
+ }
79
120
  }
80
121
  descriptionForAi() {
81
122
  return "";
82
123
  }
83
124
  async suggest(carousel, maxDisplayed) {
84
- if (!this.apiKey) {
85
- (0, logs_1.logLine)("AI generation skipped: missing API key");
125
+ if (!this.apiKey || !this.apiUrl || !this.model) {
126
+ (0, logs_1.logLine)("AI generation skipped: missing API configuration");
86
127
  return [];
87
128
  }
88
129
  const descriptions = [];
@@ -95,6 +136,7 @@ class AISuggester {
95
136
  const prompt = `You are a shell assistant. Given a partial shell input, suggest ${maxDisplayed}\
96
137
  useful, concise shell commands that the user might run next.\
97
138
  Return one suggestion per line, no numbering, no extra text.
139
+ Return the whole suggestion, not just what remains to type out.
98
140
 
99
141
  The current line is: "${carousel.getCurrentRow()}
100
142
 
@@ -103,6 +145,7 @@ ${descriptions.join("\n\n")}
103
145
  (0, logs_1.logLine)(prompt);
104
146
  const text = await generateContent(prompt, {
105
147
  apiKey: this.apiKey,
148
+ apiUrl: this.apiUrl,
106
149
  model: this.model,
107
150
  temperature: 0.3,
108
151
  maxOutputTokens: 128,
package/dist/app.js CHANGED
@@ -29,63 +29,53 @@ class App {
29
29
  bottomRows: 2,
30
30
  terminal: this.terminal,
31
31
  });
32
- }
33
- async init() {
34
- await this.history.init();
35
- await this.ai.init();
36
- }
37
- async run() {
38
- await this.init();
39
- this.keyboard.start();
40
- const updateSuggestions = debounce(async () => {
32
+ this.queueUpdateSuggestions = debounce(async () => {
41
33
  await this.carousel.updateSuggestions();
42
34
  }, 300);
43
- const handlers = {
35
+ this.handlers = {
44
36
  "ctrl-c": () => {
45
- if (this.carousel.isPromptRowSelected() &&
46
- !this.carousel.hasInput()) {
37
+ if (this.carousel.isPromptRowSelected() && !this.carousel.hasInput()) {
47
38
  this.exit();
48
39
  return;
49
40
  }
50
41
  this.carousel.clearInput();
51
42
  this.render();
52
- updateSuggestions();
43
+ this.queueUpdateSuggestions();
53
44
  },
54
45
  "ctrl-d": () => {
55
- if (this.carousel.isPromptRowSelected() &&
56
- !this.carousel.hasInput()) {
46
+ if (this.carousel.isPromptRowSelected() && !this.carousel.hasInput()) {
57
47
  this.exit();
58
48
  return;
59
49
  }
60
50
  this.carousel.deleteAtCursor();
61
51
  this.render();
62
- updateSuggestions();
52
+ this.queueUpdateSuggestions();
63
53
  },
64
54
  "ctrl-u": () => {
65
55
  this.carousel.deleteToLineStart();
66
56
  this.render();
67
- updateSuggestions();
57
+ this.queueUpdateSuggestions();
68
58
  },
69
59
  backspace: () => {
70
60
  this.carousel.deleteBeforeCursor();
71
61
  // Immediate prompt redraw with existing suggestions
72
62
  this.render();
73
63
  // Async fetch of new suggestions
74
- updateSuggestions();
64
+ this.queueUpdateSuggestions();
75
65
  },
76
66
  enter: async () => {
77
67
  const cmd = this.carousel.getCurrentRow().trim();
78
68
  this.carousel.setInputBuffer("", 0);
79
69
  await this.runCommand(cmd);
80
70
  this.carousel.resetIndex();
81
- updateSuggestions();
71
+ this.queueUpdateSuggestions();
82
72
  },
83
73
  char: (evt) => {
84
74
  this.carousel.insertAtCursor(evt.sequence);
85
75
  // Immediate prompt redraw with existing suggestions
86
76
  this.render();
87
77
  // Async fetch of new suggestions
88
- updateSuggestions();
78
+ this.queueUpdateSuggestions();
89
79
  },
90
80
  up: () => {
91
81
  this.carousel.up();
@@ -114,19 +104,32 @@ class App {
114
104
  delete: () => {
115
105
  this.carousel.deleteAtCursor();
116
106
  this.render();
117
- updateSuggestions();
107
+ this.queueUpdateSuggestions();
118
108
  },
119
109
  escape: () => { },
120
110
  };
121
- this.keyboard.on("key", async (evt) => {
122
- const fn = handlers[evt.name];
123
- if (fn)
124
- await fn(evt);
111
+ }
112
+ async init() {
113
+ await this.history.init();
114
+ await this.ai.init();
115
+ }
116
+ async run() {
117
+ await this.init();
118
+ this.keyboard.start();
119
+ this.keyboard.on("key", (evt) => {
120
+ void this.handleKey(evt);
125
121
  });
126
122
  // Initial draw
127
123
  this.render();
128
124
  await this.carousel.updateSuggestions();
129
125
  }
126
+ async handleKey(evt) {
127
+ const fn = this.handlers[evt.name];
128
+ if (fn) {
129
+ await fn(evt);
130
+ }
131
+ }
132
+ queueUpdateSuggestions() { }
130
133
  render() {
131
134
  this.carousel.render();
132
135
  // Cursor placement handled inside carousel render.
@@ -146,8 +149,16 @@ class App {
146
149
  this.terminal.renderBlock(lines);
147
150
  // Ensure command output starts on the next line
148
151
  this.terminal.write("\n");
149
- await this.history.add(cmd);
150
- await (0, spawner_1.runUserCommand)(cmd);
152
+ this.keyboard.pause();
153
+ try {
154
+ const success = await (0, spawner_1.runUserCommand)(cmd);
155
+ if (success) {
156
+ await this.history.add(cmd);
157
+ }
158
+ }
159
+ finally {
160
+ this.keyboard.resume();
161
+ }
151
162
  // After arbitrary output, reset render block tracking
152
163
  this.terminal.resetBlockTracking();
153
164
  }
package/dist/carousel.js CHANGED
@@ -75,7 +75,7 @@ class Carousel {
75
75
  const { brightWhite, reset, dim } = terminal_1.colors;
76
76
  let color = dim;
77
77
  if (this.index === rowIndex) {
78
- color = brightWhite;
78
+ color = terminal_1.colors.purple;
79
79
  if (rowIndex !== 0) {
80
80
  prefix = "> ";
81
81
  }
package/dist/config.js CHANGED
@@ -34,18 +34,74 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.configFolder = configFolder;
37
+ exports.getConfigPath = getConfigPath;
38
+ exports.doesConfigExist = doesConfigExist;
37
39
  exports.getConfig = getConfig;
38
40
  const fs_1 = require("fs");
39
41
  const path = __importStar(require("path"));
40
42
  const os = __importStar(require("os"));
43
+ const GEMINI_DEFAULT_API_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:generateContent";
44
+ const GEMINI_DEFAULT_MODEL = "gemini-2.5-flash-lite";
41
45
  function configFolder(subpath) {
42
46
  const home = os.homedir();
43
- // Default path: ~/.caroushell/history
47
+ // Default path: ~/.caroushell/<subpath>
44
48
  return path.join(home, ".caroushell", subpath);
45
49
  }
50
+ function getConfigPath() {
51
+ return process.env.CAROUSHELL_CONFIG_PATH || configFolder("config.json");
52
+ }
53
+ async function readConfigFile() {
54
+ const configPath = getConfigPath();
55
+ const raw = await fs_1.promises.readFile(configPath, "utf8");
56
+ return JSON.parse(raw);
57
+ }
58
+ async function doesConfigExist() {
59
+ const configPath = getConfigPath();
60
+ try {
61
+ await fs_1.promises.access(configPath);
62
+ const raw = await readConfigFile();
63
+ if (!raw)
64
+ return false;
65
+ return true;
66
+ }
67
+ catch (err) {
68
+ if (err?.code === "ENOENT") {
69
+ return false;
70
+ }
71
+ // detect empty json file syntax error
72
+ if (err instanceof SyntaxError) {
73
+ return false;
74
+ }
75
+ throw err;
76
+ }
77
+ }
46
78
  async function getConfig() {
47
- // Load config from ~/.caroushell/config.json
48
- const configPath = process.env.CAROUSHELL_CONFIG_PATH || configFolder("config.json");
49
- const config = JSON.parse(await fs_1.promises.readFile(configPath, "utf8"));
50
- return config;
79
+ const configPath = getConfigPath();
80
+ const raw = await readConfigFile();
81
+ const envApiKey = process.env.CAROUSHELL_API_KEY || process.env.GEMINI_API_KEY || undefined;
82
+ const envApiUrl = process.env.CAROUSHELL_API_URL || undefined;
83
+ const envModel = process.env.CAROUSHELL_MODEL || undefined;
84
+ const geminiApiKey = raw.GEMINI_API_KEY || process.env.GEMINI_API_KEY || undefined;
85
+ const resolved = {
86
+ ...raw,
87
+ apiUrl: raw.apiUrl || envApiUrl,
88
+ apiKey: raw.apiKey || raw.GEMINI_API_KEY || envApiKey,
89
+ model: raw.model || envModel,
90
+ };
91
+ // If the user only supplied a Gemini key, assume the Gemini defaults.
92
+ if (!resolved.apiUrl && geminiApiKey) {
93
+ resolved.apiUrl = GEMINI_DEFAULT_API_URL;
94
+ }
95
+ if (!resolved.model && geminiApiKey) {
96
+ resolved.model = GEMINI_DEFAULT_MODEL;
97
+ }
98
+ if (!resolved.apiUrl || !resolved.apiKey || !resolved.model) {
99
+ throw new Error(`Config at ${configPath} is missing required fields. Please include apiUrl, apiKey, and model (or just GEMINI_API_KEY).`);
100
+ }
101
+ return resolved;
102
+ }
103
+ function isGeminiUrl(url) {
104
+ const lower = url.toLowerCase();
105
+ return (lower.includes("generativelanguage.googleapis.com") ||
106
+ lower.includes("gemini"));
51
107
  }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.runHelloNewUserFlow = runHelloNewUserFlow;
40
+ const fs_1 = require("fs");
41
+ const path = __importStar(require("path"));
42
+ const readline_1 = __importDefault(require("readline"));
43
+ const ai_suggester_1 = require("./ai-suggester");
44
+ function isInteractive() {
45
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
46
+ }
47
+ async function prompt(question, rl) {
48
+ return await new Promise((resolve) => {
49
+ rl.question(question, (answer) => resolve(answer));
50
+ });
51
+ }
52
+ async function runHelloNewUserFlow(configPath) {
53
+ if (!isInteractive()) {
54
+ throw new Error(`Missing config at ${configPath} and no interactive terminal is available.\n` +
55
+ "Create the file manually or run Caroushell from a TTY.");
56
+ }
57
+ const dir = path.dirname(configPath);
58
+ await fs_1.promises.mkdir(dir, { recursive: true });
59
+ console.log("");
60
+ console.log("Welcome to Caroushell!");
61
+ console.log(`Let's set up AI suggestions. You'll need an API endpoint URL, a key, and model id. These will be stored at ${configPath}`);
62
+ console.log("");
63
+ console.log("Some example endpoints you can paste:");
64
+ console.log(" - OpenRouter: https://openrouter.ai/api/v1");
65
+ console.log(" - OpenAI: https://api.openai.com/v1");
66
+ console.log(" - Google: https://generativelanguage.googleapis.com/v1beta/openai");
67
+ console.log("");
68
+ console.log("Press Ctrl+C any time to abort.\n");
69
+ const rl = readline_1.default.createInterface({
70
+ input: process.stdin,
71
+ output: process.stdout,
72
+ });
73
+ let apiUrl = "";
74
+ while (!apiUrl) {
75
+ const answer = (await prompt("API URL: ", rl)).trim();
76
+ if (answer) {
77
+ apiUrl = answer;
78
+ }
79
+ else {
80
+ console.log("Please enter a URL (example: https://openrouter.ai/api/v1)");
81
+ }
82
+ }
83
+ let apiKey = "";
84
+ while (!apiKey) {
85
+ const answer = (await prompt("API key: ", rl)).trim();
86
+ if (answer) {
87
+ apiKey = answer;
88
+ }
89
+ else {
90
+ console.log("Please enter an API key (the value stays local on this machine).");
91
+ }
92
+ }
93
+ const models = await (0, ai_suggester_1.listModels)(apiUrl, apiKey);
94
+ if (models.length > 0) {
95
+ console.log("Here are a few example model ids.");
96
+ for (const model of models.slice(0, 5)) {
97
+ console.log(` - ${model}`);
98
+ }
99
+ }
100
+ let model = "";
101
+ while (!model) {
102
+ const answer = (await prompt("Model (e.g. gpt-4o-mini, google/gemini-2.5-flash-lite): ", rl)).trim();
103
+ if (answer) {
104
+ model = answer;
105
+ }
106
+ else {
107
+ console.log("Please enter a model name (example: mistralai/mistral-small-24b-instruct-2501).");
108
+ }
109
+ }
110
+ rl.close();
111
+ const config = {
112
+ apiUrl,
113
+ apiKey,
114
+ model,
115
+ };
116
+ await fs_1.promises.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
117
+ console.log(`\nSaved config to ${configPath}`);
118
+ console.log("You can edit this file later if you want to switch providers.\n");
119
+ return config;
120
+ }
@@ -32,16 +32,17 @@ class HistorySuggester {
32
32
  async add(command) {
33
33
  if (!command.trim())
34
34
  return;
35
- // Deduplicate recent duplicate
36
- if (this.items[this.items.length - 1] !== command) {
37
- this.items.push(command);
38
- if (this.items.length > this.maxItems)
39
- this.items.shift();
40
- await fs_1.promises
41
- .mkdir(path_1.default.dirname(this.filePath), { recursive: true })
42
- .catch(() => { });
43
- await fs_1.promises.writeFile(this.filePath, this.items.join("\n"), "utf8");
35
+ if (this.items[this.items.length - 1] === command) {
36
+ // Deduplicate recent duplicate
37
+ return;
44
38
  }
39
+ this.items.push(command);
40
+ if (this.items.length > this.maxItems)
41
+ this.items.shift();
42
+ await fs_1.promises
43
+ .mkdir(path_1.default.dirname(this.filePath), { recursive: true })
44
+ .catch(() => { });
45
+ await fs_1.promises.writeFile(this.filePath, this.items.join("\n"), "utf8");
45
46
  }
46
47
  async suggest(carousel, maxDisplayed) {
47
48
  const input = carousel.getCurrentRow();
package/dist/keyboard.js CHANGED
@@ -36,30 +36,52 @@ class Keyboard extends events_1.EventEmitter {
36
36
  constructor() {
37
37
  super(...arguments);
38
38
  this.active = false;
39
+ this.capturing = false;
39
40
  this.buffer = '';
41
+ this.stdin = process.stdin;
42
+ this.onData = (data) => this.handleData(data);
40
43
  }
41
44
  start() {
42
45
  if (this.active)
43
46
  return;
44
47
  this.active = true;
45
- const stdin = process.stdin;
46
- stdin.setEncoding('utf8');
47
- if (stdin.isTTY)
48
- stdin.setRawMode(true);
49
- stdin.resume();
50
- const onData = (data) => this.handleData(data);
51
- stdin.on('data', onData);
52
- this.once('stop', () => {
53
- stdin.off('data', onData);
54
- if (stdin.isTTY)
55
- stdin.setRawMode(false);
56
- });
48
+ this.stdin.setEncoding('utf8');
49
+ this.enableCapture();
57
50
  }
58
51
  stop() {
59
52
  if (!this.active)
60
53
  return;
61
54
  this.active = false;
62
- this.emit('stop');
55
+ this.disableCapture();
56
+ }
57
+ pause() {
58
+ if (!this.active)
59
+ return;
60
+ this.disableCapture();
61
+ }
62
+ resume() {
63
+ if (!this.active)
64
+ return;
65
+ this.enableCapture();
66
+ }
67
+ enableCapture() {
68
+ if (this.capturing)
69
+ return;
70
+ if (this.stdin.isTTY)
71
+ this.stdin.setRawMode(true);
72
+ this.stdin.on('data', this.onData);
73
+ this.stdin.resume();
74
+ this.capturing = true;
75
+ }
76
+ disableCapture() {
77
+ if (!this.capturing)
78
+ return;
79
+ this.stdin.off('data', this.onData);
80
+ if (this.stdin.isTTY)
81
+ this.stdin.setRawMode(false);
82
+ this.stdin.pause();
83
+ this.buffer = '';
84
+ this.capturing = false;
63
85
  }
64
86
  handleData(data) {
65
87
  this.buffer += data;
package/dist/logs.js CHANGED
@@ -53,13 +53,19 @@ function timestamp(date = new Date()) {
53
53
  // local time iso string
54
54
  return date.toISOString();
55
55
  }
56
- async function logLine(message, when = new Date()) {
56
+ async function writeLogLine(message, when) {
57
57
  const dir = getLogDir();
58
58
  await ensureDir(dir);
59
59
  const file = getLogFilePath(when);
60
60
  const line = `[${timestamp(when)}] ${message}\n`;
61
61
  await fs_1.promises.appendFile(file, line, "utf8");
62
62
  }
63
+ function logLine(message, when = new Date()) {
64
+ // Fire-and-forget logging so callers do not need to await.
65
+ void writeLogLine(message, when).catch((err) => {
66
+ console.error("CRITICAL: Logger itself failed:", err.message);
67
+ });
68
+ }
63
69
  // Ensure the ~/.caroushell/logs folder exists early in app startup
64
70
  async function ensureLogFolderExists() {
65
71
  const dir = getLogDir();
package/dist/main.js CHANGED
@@ -2,10 +2,15 @@
2
2
  "use strict";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const app_1 = require("./app");
5
+ const hello_new_user_1 = require("./hello-new-user");
5
6
  const logs_1 = require("./logs");
7
+ const config_1 = require("./config");
6
8
  async function main() {
7
9
  await (0, logs_1.ensureLogFolderExists)();
8
10
  (0, logs_1.logLine)("Caroushell started");
11
+ if (!(await (0, config_1.doesConfigExist)())) {
12
+ await (0, hello_new_user_1.runHelloNewUserFlow)((0, config_1.getConfigPath)());
13
+ }
9
14
  const app = new app_1.App();
10
15
  await app.run();
11
16
  }
package/dist/spawner.js CHANGED
@@ -10,7 +10,7 @@ const builtInCommands = {
10
10
  cd: async (args) => {
11
11
  if (args.length === 1) {
12
12
  process.stdout.write(process.cwd() + "\n");
13
- return;
13
+ return true;
14
14
  }
15
15
  const dest = expandVars(args[1]);
16
16
  try {
@@ -18,10 +18,13 @@ const builtInCommands = {
18
18
  }
19
19
  catch (err) {
20
20
  process.stderr.write(`cd: ${err.message}\n`);
21
+ return false;
21
22
  }
23
+ return true;
22
24
  },
23
25
  exit: async () => {
24
26
  (0, process_1.exit)(0);
27
+ return false;
25
28
  },
26
29
  };
27
30
  function expandVars(input) {
@@ -46,20 +49,17 @@ function expandVars(input) {
46
49
  async function runUserCommand(command) {
47
50
  const trimmed = command.trim();
48
51
  if (!trimmed)
49
- return;
52
+ return false;
50
53
  const args = command.split(/\s+/);
51
54
  if (typeof args[0] === "string" && builtInCommands[args[0]]) {
52
- await builtInCommands[args[0]](args);
53
- return;
55
+ return await builtInCommands[args[0]](args);
54
56
  }
55
57
  const proc = (0, child_process_1.spawn)(shellBinary, [...shellArgs, command], {
56
- stdio: ["ignore", "pipe", "pipe"],
57
- shell: true,
58
+ stdio: "inherit",
58
59
  });
59
60
  await new Promise((resolve, reject) => {
60
- proc.stdout.on("data", (data) => process.stdout.write(data));
61
- proc.stderr.on("data", (data) => process.stderr.write(data));
62
61
  proc.on("error", reject);
63
62
  proc.on("close", () => resolve());
64
63
  });
64
+ return proc.exitCode === 0;
65
65
  }
package/dist/terminal.js CHANGED
@@ -10,7 +10,9 @@ exports.colors = {
10
10
  reset: "\x1b[0m",
11
11
  white: "\x1b[37m",
12
12
  brightWhite: "\x1b[97m",
13
- dim: "\x1b[2m",
13
+ dimmest: "\x1b[2m",
14
+ dim: "\x1b[37m",
15
+ purple: "\x1b[95m",
14
16
  yellow: "\x1b[33m",
15
17
  };
16
18
  class Terminal {
@@ -1,12 +1,20 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const ai_suggester_1 = require("./ai-suggester");
4
+ const config_1 = require("./config");
4
5
  async function main() {
5
- if (!process.env.GEMINI_API_KEY) {
6
- console.warn("Warning: GEMINI_API_KEY is not set. The answer may be empty.");
6
+ const config = await (0, config_1.getConfig)();
7
+ if (!config.apiKey) {
8
+ console.warn("Warning: no API key configured. The answer may be empty.");
7
9
  }
10
+ const models = await (0, ai_suggester_1.listModels)(config.apiUrl || "", config.apiKey || "");
11
+ console.log("Available models:", models);
8
12
  const question = "What is the capital of France?";
9
- const answer = await (0, ai_suggester_1.generateContent)(question);
13
+ const answer = await (0, ai_suggester_1.generateContent)(question, {
14
+ apiKey: config.apiKey,
15
+ apiUrl: config.apiUrl,
16
+ model: config.model,
17
+ });
10
18
  console.log(`Q: ${question}`);
11
19
  console.log(`A: ${answer.trim()}`);
12
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caroushell",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Terminal carousel that suggests commands from history, config, and AI.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/main.js",
@@ -41,6 +41,9 @@
41
41
  "devDependencies": {
42
42
  "@types/node": "^24.10.0",
43
43
  "@types/shell-quote": "^1.7.5",
44
+ "@typescript-eslint/eslint-plugin": "^8.46.4",
45
+ "@typescript-eslint/parser": "^8.46.4",
46
+ "eslint": "^9.39.1",
44
47
  "tsx": "^4.19.2",
45
48
  "typescript": "^5.6.3"
46
49
  }