caroushell 0.1.23 → 0.1.29

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
@@ -64,6 +64,39 @@ OpenAI, etc.) will work as long as the URL, key, and model are valid. If you
64
64
  only provide a Gemini API key in the config, Caroushell will default to the
65
65
  Gemini Flash Lite 2.5 endpoint and model.
66
66
 
67
+ ## Prompt Display
68
+
69
+ Caroushell can render a custom prompt using a template string in
70
+ `~/.caroushell/config.toml`:
71
+
72
+ ```toml
73
+ prompt = "{hostname} {short-directory} $>"
74
+ ```
75
+
76
+ Available tokens:
77
+
78
+ - `{hostname}`
79
+ - `{directory}`
80
+ - `{short-directory}`
81
+
82
+ Examples:
83
+
84
+ ```toml
85
+ prompt = "$> "
86
+ ```
87
+
88
+ ```toml
89
+ prompt = "{directory} $> "
90
+ ```
91
+
92
+ ```toml
93
+ prompt = "{hostname} {short-directory} $>"
94
+ ```
95
+
96
+ `{short-directory}` keeps the final directory name and shortens parent
97
+ directories to their first letter. For example, `/home/user/projects/my-app`
98
+ becomes `/h/u/p/my-app`.
99
+
67
100
  ## Installation
68
101
 
69
102
  Install globally (recommended):
package/dist/app.js CHANGED
@@ -5,7 +5,6 @@ const terminal_1 = require("./terminal");
5
5
  const keyboard_1 = require("./keyboard");
6
6
  const carousel_1 = require("./carousel");
7
7
  const history_suggester_1 = require("./history-suggester");
8
- const ai_suggester_1 = require("./ai-suggester");
9
8
  const file_suggester_1 = require("./file-suggester");
10
9
  const spawner_1 = require("./spawner");
11
10
  const logs_1 = require("./logs");
@@ -18,15 +17,16 @@ class App {
18
17
  this.terminal = deps.terminal ?? new terminal_1.Terminal();
19
18
  this.keyboard = deps.keyboard ?? new keyboard_1.Keyboard();
20
19
  this.history = deps.topPanel ?? new history_suggester_1.HistorySuggester();
21
- this.ai = deps.bottomPanel ?? new ai_suggester_1.AISuggester();
20
+ this.bottomSuggester = deps.bottomPanel ?? new carousel_1.NullSuggester();
22
21
  this.files = deps.files ?? new file_suggester_1.FileSuggester();
23
- this.suggesters = deps.suggesters ?? [this.history, this.ai, this.files];
22
+ this.suggesters = deps.suggesters ?? [this.history, this.bottomSuggester, this.files];
24
23
  this.carousel = new carousel_1.Carousel({
25
24
  top: this.history,
26
- bottom: this.ai,
25
+ bottom: this.bottomSuggester,
27
26
  topRows: 2,
28
- bottomRows: 2,
27
+ bottomRows: this.bottomSuggester instanceof carousel_1.NullSuggester ? 0 : 2,
29
28
  terminal: this.terminal,
29
+ promptLine0: deps.promptLine0,
30
30
  });
31
31
  this.queueUpdateSuggestions = () => {
32
32
  void this.carousel.updateSuggestions();
@@ -137,9 +137,9 @@ class App {
137
137
  };
138
138
  }
139
139
  async init() {
140
- await this.history.init();
141
- await this.ai.init();
142
- await this.files.init();
140
+ for (const s of this.suggesters) {
141
+ await s.init();
142
+ }
143
143
  }
144
144
  async run() {
145
145
  await this.init();
@@ -185,6 +185,7 @@ class App {
185
185
  this.terminal.write("\n");
186
186
  this.keyboard.disableCapture();
187
187
  this.terminal.disableWrites();
188
+ await this.preBroadcastCommand(cmd);
188
189
  try {
189
190
  const storeInHistory = await (0, spawner_1.runUserCommand)(cmd);
190
191
  if (storeInHistory) {
@@ -291,6 +292,19 @@ class App {
291
292
  this.usingFileSuggestions = true;
292
293
  this.carousel.setTopSuggester(this.files);
293
294
  }
295
+ async preBroadcastCommand(cmd) {
296
+ const listeners = this.suggesters
297
+ .map((suggester) => suggester.onCommandWillRun?.(cmd))
298
+ .filter(Boolean);
299
+ if (listeners.length === 0)
300
+ return;
301
+ try {
302
+ await Promise.all(listeners);
303
+ }
304
+ catch (err) {
305
+ (0, logs_1.logLine)("suggester onCommandWillRun error: " + err?.message);
306
+ }
307
+ }
294
308
  async broadcastCommand(cmd) {
295
309
  const listeners = this.suggesters
296
310
  .map((suggester) => suggester.onCommandRan?.(cmd))
package/dist/carousel.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Carousel = void 0;
3
+ exports.Carousel = exports.NullSuggester = void 0;
4
4
  exports.getDisplayWidth = getDisplayWidth;
5
5
  const logs_1 = require("./logs");
6
6
  const terminal_1 = require("./terminal");
@@ -57,6 +57,16 @@ function getDisplayWidth(text) {
57
57
  }
58
58
  return width;
59
59
  }
60
+ class NullSuggester {
61
+ constructor() {
62
+ this.prefix = "";
63
+ }
64
+ async init() { }
65
+ async refreshSuggestions() { }
66
+ latest() { return []; }
67
+ descriptionForAi() { return ""; }
68
+ }
69
+ exports.NullSuggester = NullSuggester;
60
70
  class Carousel {
61
71
  constructor(opts) {
62
72
  this.index = 0;
@@ -67,6 +77,7 @@ class Carousel {
67
77
  this.bottom = opts.bottom;
68
78
  this.topRowCount = opts.topRows;
69
79
  this.bottomRowCount = opts.bottomRows;
80
+ this.promptLine0Getter = opts.promptLine0 ?? (() => "$> ");
70
81
  }
71
82
  async updateSuggestions(input) {
72
83
  if (typeof input === "string") {
@@ -386,7 +397,7 @@ class Carousel {
386
397
  return start;
387
398
  }
388
399
  getPromptPrefix(lineIndex) {
389
- return lineIndex === 0 ? "$> " : "> ";
400
+ return lineIndex === 0 ? this.promptLine0Getter() : "> ";
390
401
  }
391
402
  render() {
392
403
  (0, logs_1.logLine)("Rendering carousel");
package/dist/config.js CHANGED
@@ -40,6 +40,7 @@ exports.configFolder = configFolder;
40
40
  exports.getConfigPath = getConfigPath;
41
41
  exports.doesConfigExist = doesConfigExist;
42
42
  exports.getConfig = getConfig;
43
+ exports.buildPromptLine0 = buildPromptLine0;
43
44
  const fs_1 = require("fs");
44
45
  const path = __importStar(require("path"));
45
46
  const os = __importStar(require("os"));
@@ -88,7 +89,6 @@ async function doesConfigExist() {
88
89
  }
89
90
  }
90
91
  async function getConfig() {
91
- const configPath = getConfigPath();
92
92
  const raw = await readConfigFile();
93
93
  const envApiKey = process.env.CAROUSHELL_API_KEY || process.env.GEMINI_API_KEY || undefined;
94
94
  const envApiUrl = process.env.CAROUSHELL_API_URL || undefined;
@@ -107,9 +107,6 @@ async function getConfig() {
107
107
  if (!resolved.model && geminiApiKey) {
108
108
  resolved.model = GEMINI_DEFAULT_MODEL;
109
109
  }
110
- if (!resolved.apiUrl || !resolved.apiKey || !resolved.model) {
111
- throw new Error(`Config at ${configPath} is missing required fields. Please include apiUrl, apiKey, and model (or just GEMINI_API_KEY).`);
112
- }
113
110
  return resolved;
114
111
  }
115
112
  function isGeminiUrl(url) {
@@ -117,3 +114,35 @@ function isGeminiUrl(url) {
117
114
  return (lower.includes("generativelanguage.googleapis.com") ||
118
115
  lower.includes("gemini"));
119
116
  }
117
+ function normalizeCwd(cwd) {
118
+ const normalized = cwd.replace(/\\/g, "/");
119
+ const home = os.homedir().replace(/\\/g, "/");
120
+ if (normalized === home)
121
+ return "~";
122
+ if (normalized.startsWith(home + "/")) {
123
+ return "~" + normalized.slice(home.length);
124
+ }
125
+ return normalized;
126
+ }
127
+ function shortenPath(p) {
128
+ const parts = p.split("/");
129
+ return parts
130
+ .map((part, i) => {
131
+ if (i === parts.length - 1)
132
+ return part;
133
+ if (part === "" || part === "~")
134
+ return part;
135
+ return part[0];
136
+ })
137
+ .join("/");
138
+ }
139
+ function buildPromptLine0(config) {
140
+ return () => {
141
+ const normalized = normalizeCwd(process.cwd());
142
+ const template = config.prompt ?? "$> ";
143
+ return template
144
+ .replace(/\{hostname\}/g, os.hostname())
145
+ .replace(/\{directory\}/g, normalized)
146
+ .replace(/\{short-directory\}/g, shortenPath(normalized));
147
+ };
148
+ }
@@ -42,6 +42,42 @@ const path = __importStar(require("path"));
42
42
  const readline_1 = __importDefault(require("readline"));
43
43
  const ai_suggester_1 = require("./ai-suggester");
44
44
  const preferredModels = ["gemini-2.5-flash-lite", "gpt-4o-mini"];
45
+ const defaultPromptTemplate = "$> ";
46
+ function serializeToml(obj) {
47
+ const lines = [];
48
+ const sections = [];
49
+ for (const [key, value] of Object.entries(obj)) {
50
+ if (value !== undefined && typeof value === "object" && value !== null) {
51
+ sections.push([key, value]);
52
+ }
53
+ else if (value !== undefined) {
54
+ lines.push(`${key} = ${JSON.stringify(value)}`);
55
+ }
56
+ }
57
+ for (const [section, values] of sections) {
58
+ lines.push(`\n[${section}]`);
59
+ for (const [key, value] of Object.entries(values)) {
60
+ if (value !== undefined) {
61
+ lines.push(`${key} = ${JSON.stringify(value)}`);
62
+ }
63
+ }
64
+ }
65
+ return lines.join("\n");
66
+ }
67
+ async function askPromptConfig(prompter, logFn) {
68
+ logFn("Customize your shell prompt with plain text and tokens.");
69
+ logFn("Available tokens:");
70
+ logFn(" {hostname}");
71
+ logFn(" {directory}");
72
+ logFn(" {short-directory}");
73
+ logFn(`Press Enter to keep the default prompt: ${defaultPromptTemplate}`);
74
+ const answer = await prompter.ask("Prompt template: ");
75
+ const trimmed = answer.trim();
76
+ if (!trimmed || trimmed === defaultPromptTemplate.trim()) {
77
+ return undefined;
78
+ }
79
+ return answer;
80
+ }
45
81
  function isInteractive() {
46
82
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
47
83
  }
@@ -50,6 +86,25 @@ async function prompt(question, rl) {
50
86
  rl.question(question, (answer) => resolve(answer));
51
87
  });
52
88
  }
89
+ function createReadlineTerminal() {
90
+ return {
91
+ createPrompter() {
92
+ const rl = readline_1.default.createInterface({
93
+ input: process.stdin,
94
+ output: process.stdout,
95
+ });
96
+ return {
97
+ ask(question) {
98
+ return prompt(question, rl);
99
+ },
100
+ close() {
101
+ rl.close();
102
+ },
103
+ };
104
+ },
105
+ isInteractive,
106
+ };
107
+ }
53
108
  function findShortestMatches(models, preferredList) {
54
109
  const matches = [];
55
110
  for (const pref of preferredList) {
@@ -61,82 +116,94 @@ function findShortestMatches(models, preferredList) {
61
116
  }
62
117
  return [...new Set(matches)];
63
118
  }
64
- async function runHelloNewUserFlow(configPath) {
65
- if (!isInteractive()) {
119
+ async function runHelloNewUserFlow(configPath, deps = {}) {
120
+ const fileSystem = deps.fsOps ?? fs_1.promises;
121
+ const listModelsFn = deps.listModelsFn ?? ai_suggester_1.listModels;
122
+ const logFn = deps.logFn ?? console.log;
123
+ const terminal = deps.terminal ?? createReadlineTerminal();
124
+ if (!terminal.isInteractive()) {
66
125
  throw new Error(`Missing config at ${configPath} and no interactive terminal is available.\n` +
67
126
  "Create the file manually or run Caroushell from a TTY.");
68
127
  }
69
128
  const dir = path.dirname(configPath);
70
- await fs_1.promises.mkdir(dir, { recursive: true });
71
- console.log("");
72
- console.log("Welcome to Caroushell!");
73
- 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}`);
74
- console.log("");
75
- console.log("Some example endpoints you can paste:");
76
- console.log(" - OpenRouter: https://openrouter.ai/api/v1");
77
- console.log(" - OpenAI: https://api.openai.com/v1");
78
- console.log(" - Google: https://generativelanguage.googleapis.com/v1beta/openai");
79
- console.log("");
80
- console.log("Press Ctrl+C any time to abort.\n");
81
- const rl = readline_1.default.createInterface({
82
- input: process.stdin,
83
- output: process.stdout,
84
- });
129
+ await fileSystem.mkdir(dir, { recursive: true });
130
+ logFn("");
131
+ logFn("Welcome to Caroushell!");
132
+ logFn("");
133
+ const prompter = terminal.createPrompter();
134
+ const promptConfig = await askPromptConfig(prompter, logFn);
135
+ const wantsAi = (await prompter.ask("Do you want to set up AI auto-complete? (y/n): "))
136
+ .trim()
137
+ .toLowerCase();
138
+ if (wantsAi !== "y" && wantsAi !== "yes") {
139
+ prompter.close();
140
+ const config = { noAi: true, prompt: promptConfig };
141
+ await fileSystem.writeFile(configPath, serializeToml(config) + "\n", "utf8");
142
+ logFn("\nSkipping AI setup. You can set it up later by editing " + configPath);
143
+ logFn("");
144
+ return null;
145
+ }
146
+ logFn(`\nLet's set up AI suggestions. You'll need an API endpoint URL, a key, and model id. These will be stored at ${configPath}`);
147
+ logFn("");
148
+ logFn("Some example endpoints you can paste:");
149
+ logFn(" - OpenRouter: https://openrouter.ai/api/v1");
150
+ logFn(" - OpenAI: https://api.openai.com/v1");
151
+ logFn(" - Google: https://generativelanguage.googleapis.com/v1beta/openai");
152
+ logFn("");
153
+ logFn("Press Ctrl+C any time to abort.\n");
85
154
  let apiUrl = "";
86
155
  while (!apiUrl) {
87
- const answer = (await prompt("API URL: ", rl)).trim();
156
+ const answer = (await prompter.ask("API URL: ")).trim();
88
157
  if (answer) {
89
158
  apiUrl = answer;
90
159
  }
91
160
  else {
92
- console.log("Please enter a URL (example: https://openrouter.ai/api/v1)");
161
+ logFn("Please enter a URL (example: https://openrouter.ai/api/v1)");
93
162
  }
94
163
  }
95
164
  let apiKey = "";
96
165
  while (!apiKey) {
97
- const answer = (await prompt("API key: ", rl)).trim();
166
+ const answer = (await prompter.ask("API key: ")).trim();
98
167
  if (answer) {
99
168
  apiKey = answer;
100
169
  }
101
170
  else {
102
- console.log("Please enter an API key. The value is stored in the local config file.");
171
+ logFn("Please enter an API key. The value is stored in the local config file.");
103
172
  }
104
173
  }
105
- const models = await (0, ai_suggester_1.listModels)(apiUrl, apiKey);
174
+ const models = await listModelsFn(apiUrl, apiKey);
106
175
  if (models.length > 0) {
107
176
  const preferred = findShortestMatches(models, preferredModels);
108
- console.log("Here are a few example model ids from your api service. Choose a fast and cheap model because AI suggestions happen as you type.");
177
+ logFn("Here are a few example model ids from your api service. Choose a fast and cheap model because AI suggestions happen as you type.");
109
178
  for (const model of models.slice(0, 5)) {
110
- console.log(` - ${model}`);
179
+ logFn(` - ${model}`);
111
180
  }
112
181
  if (preferred.length) {
113
- console.log("Recommended models from your provider:");
182
+ logFn("Recommended models from your provider:");
114
183
  for (const model of preferred) {
115
- console.log(` - ${model}`);
184
+ logFn(` - ${model}`);
116
185
  }
117
186
  }
118
187
  }
119
188
  let model = "";
120
189
  while (!model) {
121
- const answer = (await prompt("Model id: ", rl)).trim();
190
+ const answer = (await prompter.ask("Model id: ")).trim();
122
191
  if (answer) {
123
192
  model = answer;
124
193
  }
125
194
  else {
126
- console.log("Please enter a model id (example: google/gemini-2.5-flash-lite, mistralai/mistral-small-24b-instruct-2501).");
195
+ logFn("Please enter a model id (example: google/gemini-2.5-flash-lite, mistralai/mistral-small-24b-instruct-2501).");
127
196
  }
128
197
  }
129
- rl.close();
198
+ prompter.close();
130
199
  const config = {
131
200
  apiUrl,
132
201
  apiKey,
133
202
  model,
203
+ prompt: promptConfig,
134
204
  };
135
- const tomlBody = Object.entries(config)
136
- .map(([key, value]) => `${key} = ${JSON.stringify(value)}`)
137
- .join("\n");
138
- await fs_1.promises.writeFile(configPath, tomlBody + "\n", "utf8");
139
- console.log(`\nSaved config to ${configPath}`);
140
- console.log("You can edit this file later if you want to switch providers.\n");
205
+ await fileSystem.writeFile(configPath, serializeToml(config) + "\n", "utf8");
206
+ logFn(`\nSaved config to ${configPath}`);
207
+ logFn("You can edit this file later if you want to switch providers.\n");
141
208
  return config;
142
209
  }
@@ -48,9 +48,10 @@ class HistorySuggester {
48
48
  .catch(() => { });
49
49
  await fs_1.promises.appendFile(this.filePath, this.serializeHistoryEntry(command), "utf8");
50
50
  }
51
- async onCommandRan(command) {
51
+ async onCommandWillRun(command) {
52
52
  await this.add(command);
53
53
  }
54
+ async onCommandRan(command) { }
54
55
  latest() {
55
56
  return this.filteredItems;
56
57
  }
package/dist/main.js CHANGED
@@ -4,6 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const fs_1 = require("fs");
5
5
  const path_1 = require("path");
6
6
  const app_1 = require("./app");
7
+ const ai_suggester_1 = require("./ai-suggester");
8
+ const carousel_1 = require("./carousel");
7
9
  const hello_new_user_1 = require("./hello-new-user");
8
10
  const logs_1 = require("./logs");
9
11
  const config_1 = require("./config");
@@ -25,7 +27,11 @@ async function main() {
25
27
  if (!(await (0, config_1.doesConfigExist)())) {
26
28
  await (0, hello_new_user_1.runHelloNewUserFlow)((0, config_1.getConfigPath)());
27
29
  }
28
- const app = new app_1.App();
30
+ const config = await (0, config_1.getConfig)();
31
+ const bottomPanel = config.apiUrl && config.apiKey && config.model
32
+ ? new ai_suggester_1.AISuggester()
33
+ : new carousel_1.NullSuggester();
34
+ const app = new app_1.App({ bottomPanel, promptLine0: (0, config_1.buildPromptLine0)(config) });
29
35
  await app.run();
30
36
  }
31
37
  main().catch((err) => {
package/dist/spawner.js CHANGED
@@ -144,10 +144,19 @@ async function runUserCommand(command) {
144
144
  stdio: "inherit",
145
145
  windowsVerbatimArguments: true,
146
146
  });
147
- await new Promise((resolve, reject) => {
148
- proc.on("error", reject);
149
- proc.on("close", () => resolve());
150
- });
147
+ // While a user command owns the terminal, Ctrl+C should interrupt that command
148
+ // without taking down the parent Caroushell process.
149
+ const ignoreSigint = () => { };
150
+ process.on("SIGINT", ignoreSigint);
151
+ try {
152
+ await new Promise((resolve, reject) => {
153
+ proc.on("error", reject);
154
+ proc.on("close", () => resolve());
155
+ });
156
+ }
157
+ finally {
158
+ process.off("SIGINT", ignoreSigint);
159
+ }
151
160
  // Why save failed commands? Well eg sometimes we want to run a test
152
161
  // many times until we fix it.
153
162
  return true;
package/dist/terminal.js CHANGED
@@ -22,6 +22,8 @@ class Terminal {
22
22
  this.cursorRow = 0;
23
23
  this.cursorCol = 0;
24
24
  this.writesDisabled = false;
25
+ this.pendingBlock = null;
26
+ this.renderScheduled = false;
25
27
  }
26
28
  disableWrites() {
27
29
  this.writesDisabled = true;
@@ -70,6 +72,7 @@ class Terminal {
70
72
  write(text) {
71
73
  if (!this.canWrite())
72
74
  return;
75
+ this.flushPendingRender();
73
76
  this.out.write(text);
74
77
  }
75
78
  hideCursor() {
@@ -80,6 +83,26 @@ class Terminal {
80
83
  }
81
84
  // Render a block of lines by clearing previous block (if any) and writing fresh
82
85
  renderBlock(lines, cursorRow, cursorCol) {
86
+ if (!this.canWrite())
87
+ return;
88
+ this.pendingBlock = {
89
+ lines: [...lines],
90
+ cursorRow,
91
+ cursorCol,
92
+ };
93
+ if (this.renderScheduled)
94
+ return;
95
+ this.renderScheduled = true;
96
+ queueMicrotask(() => this.flushPendingRender());
97
+ }
98
+ flushPendingRender() {
99
+ if (!this.pendingBlock) {
100
+ this.renderScheduled = false;
101
+ return;
102
+ }
103
+ const pending = this.pendingBlock;
104
+ this.pendingBlock = null;
105
+ this.renderScheduled = false;
83
106
  if (!this.canWrite())
84
107
  return;
85
108
  this.withCork(() => {
@@ -88,21 +111,22 @@ class Terminal {
88
111
  readline_1.default.cursorTo(this.out, 0);
89
112
  readline_1.default.clearScreenDown(this.out);
90
113
  }
91
- for (let i = 0; i < lines.length; i++) {
92
- this.write(lines[i]);
93
- if (i < lines.length - 1)
94
- this.write("\n");
114
+ for (let i = 0; i < pending.lines.length; i++) {
115
+ this.out.write(pending.lines[i]);
116
+ if (i < pending.lines.length - 1)
117
+ this.out.write("\n");
95
118
  }
96
- this.activeRows = lines.length;
119
+ this.activeRows = pending.lines.length;
97
120
  this.cursorRow = Math.max(0, this.activeRows - 1);
98
- const lastLine = lines[this.cursorRow] || "";
121
+ const lastLine = pending.lines[this.cursorRow] || "";
99
122
  this.cursorCol = lastLine.length;
100
- const needsPosition = typeof cursorRow === "number" || typeof cursorCol === "number";
123
+ const needsPosition = typeof pending.cursorRow === "number" ||
124
+ typeof pending.cursorCol === "number";
101
125
  if (needsPosition) {
102
- const targetRow = typeof cursorRow === "number"
103
- ? Math.min(Math.max(cursorRow, 0), Math.max(0, this.activeRows - 1))
126
+ const targetRow = typeof pending.cursorRow === "number"
127
+ ? Math.min(Math.max(pending.cursorRow, 0), Math.max(0, this.activeRows - 1))
104
128
  : this.cursorRow;
105
- const targetCol = Math.max(0, cursorCol ?? this.cursorCol);
129
+ const targetCol = Math.max(0, pending.cursorCol ?? this.cursorCol);
106
130
  this.moveCursorTo(targetRow, targetCol);
107
131
  }
108
132
  });
@@ -125,6 +149,8 @@ class Terminal {
125
149
  // When we have printed arbitrary output that is not managed by renderBlock,
126
150
  // reset internal line tracking so the next render starts fresh.
127
151
  resetBlockTracking() {
152
+ this.pendingBlock = null;
153
+ this.renderScheduled = false;
128
154
  this.activeRows = 0;
129
155
  this.cursorRow = 0;
130
156
  this.cursorCol = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caroushell",
3
- "version": "0.1.23",
3
+ "version": "0.1.29",
4
4
  "description": "Terminal carousel that suggests commands from history, config, and AI.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/main.js",