caroushell 0.1.19 → 0.1.21

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
@@ -3,8 +3,8 @@
3
3
  [![npm version](https://img.shields.io/npm/v/caroushell.svg)](https://www.npmjs.com/package/caroushell)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/caroushell.svg)](https://www.npmjs.com/package/caroushell)
5
5
 
6
- Caroushell is an interactive terminal carousel that suggests commands from your
7
- history, and AI suggestions as you type.
6
+ Caroushell is kind of like `bash` but you see history and AI suggestions as you
7
+ type.
8
8
 
9
9
  ## Features
10
10
 
@@ -12,9 +12,6 @@ history, and AI suggestions as you type.
12
12
  - The bottom panel of the carousel shows AI-generated command suggestions.
13
13
  - Go up and down the carousel with arrow keys.
14
14
  - Press `Enter` to run the highlighted command.
15
- - Logs activity under `~/.caroushell/logs` for easy troubleshooting.
16
- - Extensible config file (`~/.caroushell/config.toml`) so you can point the CLI
17
- at different AI providers.
18
15
 
19
16
  ## UI
20
17
 
@@ -42,12 +39,13 @@ It would look like this:
42
39
 
43
40
  ![Caroushell ai suggestion for ffmpeg slowmo](docs/assets/demo.gif)
44
41
 
45
- ## Requirements
42
+ ## Setup
46
43
 
47
44
  - Node.js 18 or newer.
48
45
  - On first launch Caroushell will prompt you for an OpenAI-compatible endpoint
49
46
  URL, API key, and model name, then store them in `~/.caroushell/config.toml`.
50
47
  - You can also create the file manually:
48
+ - Logs are at `~/.caroushell/logs` for easy troubleshooting.
51
49
 
52
50
  ```toml
53
51
  apiUrl = "https://openrouter.ai/api/v1"
@@ -171,8 +171,9 @@ class AISuggester {
171
171
  descriptions.push(desc);
172
172
  }
173
173
  }
174
- const prompt = `You are a shell assistant. Given a partial shell input, suggest ${maxDisplayed}\
175
- useful, concise shell commands that the user might run next.\
174
+ const prompt = `You are a shell assistant. Given a partial shell input, \
175
+ suggest ${maxDisplayed} useful, concise shell commands that the user might run \
176
+ next.
176
177
  Return one suggestion per line, no numbering, no extra text.
177
178
  Return the whole suggestion, not just what remains to type out.
178
179
 
@@ -193,7 +194,7 @@ ${descriptions.join("\n\n")}
193
194
  .map((s) => s.trim())
194
195
  .filter(Boolean)
195
196
  .slice(0, maxDisplayed);
196
- (0, logs_1.logLine)(`AI lines: ${lines.length}`);
197
+ (0, logs_1.logLine)(`AI lines: ${lines}`);
197
198
  return lines;
198
199
  }
199
200
  }
package/dist/app.js CHANGED
@@ -8,14 +8,16 @@ const history_suggester_1 = require("./history-suggester");
8
8
  const ai_suggester_1 = require("./ai-suggester");
9
9
  const file_suggester_1 = require("./file-suggester");
10
10
  const spawner_1 = require("./spawner");
11
+ const logs_1 = require("./logs");
11
12
  class App {
12
- constructor() {
13
+ constructor(deps = {}) {
13
14
  this.usingFileSuggestions = false;
14
- this.terminal = new terminal_1.Terminal();
15
- this.keyboard = new keyboard_1.Keyboard();
16
- this.history = new history_suggester_1.HistorySuggester();
17
- this.ai = new ai_suggester_1.AISuggester();
18
- this.files = new file_suggester_1.FileSuggester();
15
+ this.terminal = deps.terminal ?? new terminal_1.Terminal();
16
+ this.keyboard = deps.keyboard ?? new keyboard_1.Keyboard();
17
+ this.history = deps.topPanel ?? new history_suggester_1.HistorySuggester();
18
+ this.ai = deps.bottomPanel ?? new ai_suggester_1.AISuggester();
19
+ this.files = deps.files ?? new file_suggester_1.FileSuggester();
20
+ this.suggesters = deps.suggesters ?? [this.history, this.ai, this.files];
19
21
  this.carousel = new carousel_1.Carousel({
20
22
  top: this.history,
21
23
  bottom: this.ai,
@@ -170,11 +172,12 @@ class App {
170
172
  try {
171
173
  const storeInHistory = await (0, spawner_1.runUserCommand)(cmd);
172
174
  if (storeInHistory) {
173
- await this.history.add(cmd);
175
+ await this.broadcastCommand(cmd);
174
176
  }
175
177
  }
176
178
  finally {
177
179
  this.terminal.enableWrites();
180
+ this.terminal.reset();
178
181
  this.keyboard.enableCapture();
179
182
  }
180
183
  }
@@ -242,5 +245,18 @@ class App {
242
245
  this.usingFileSuggestions = true;
243
246
  this.carousel.setTopSuggester(this.files);
244
247
  }
248
+ async broadcastCommand(cmd) {
249
+ const listeners = this.suggesters
250
+ .map((suggester) => suggester.onCommandRan?.(cmd))
251
+ .filter(Boolean);
252
+ if (listeners.length === 0)
253
+ return;
254
+ try {
255
+ await Promise.all(listeners);
256
+ }
257
+ catch (err) {
258
+ (0, logs_1.logLine)("suggester onCommandRan error: " + err?.message);
259
+ }
260
+ }
245
261
  }
246
262
  exports.App = App;
@@ -48,6 +48,9 @@ class HistorySuggester {
48
48
  .catch(() => { });
49
49
  await fs_1.promises.appendFile(this.filePath, this.serializeHistoryEntry(command), "utf8");
50
50
  }
51
+ async onCommandRan(command) {
52
+ await this.add(command);
53
+ }
51
54
  latest() {
52
55
  return this.filteredItems;
53
56
  }
package/dist/keyboard.js CHANGED
@@ -48,12 +48,12 @@ for (const seq of Object.keys(KEYMAP)) {
48
48
  }
49
49
  }
50
50
  class Keyboard extends events_1.EventEmitter {
51
- constructor() {
52
- super(...arguments);
51
+ constructor(stdin = process.stdin) {
52
+ super();
53
53
  this.capturing = false;
54
54
  this.buffer = '';
55
- this.stdin = process.stdin;
56
55
  this.onData = (data) => this.handleData(data);
56
+ this.stdin = stdin;
57
57
  }
58
58
  enableCapture() {
59
59
  if (this.capturing)
package/dist/spawner.js CHANGED
@@ -7,6 +7,18 @@ const logs_1 = require("./logs");
7
7
  const isWin = process.platform === "win32";
8
8
  const shellBinary = isWin ? "cmd.exe" : "/bin/bash";
9
9
  const shellArgs = isWin ? ["/d", "/s", "/c"] : ["-lc"];
10
+ const dirStack = [];
11
+ // Track last-known cwd per drive so `E:` switches like cmd.exe
12
+ // into the folder you were in before switching drives.
13
+ const driveCwds = {};
14
+ function updateDriveCwd(cwd = process.cwd()) {
15
+ if (!isWin)
16
+ return;
17
+ const drive = cwd.slice(0, 2).toUpperCase();
18
+ if (/^[A-Z]:$/.test(drive)) {
19
+ driveCwds[drive] = cwd;
20
+ }
21
+ }
10
22
  const builtInCommands = {
11
23
  cd: async (args) => {
12
24
  if (args.length === 1) {
@@ -16,6 +28,7 @@ const builtInCommands = {
16
28
  const dest = expandVars(args[1]);
17
29
  try {
18
30
  process.chdir(dest);
31
+ updateDriveCwd();
19
32
  }
20
33
  catch (err) {
21
34
  process.stderr.write(`cd: ${err.message}\n`);
@@ -23,11 +36,66 @@ const builtInCommands = {
23
36
  }
24
37
  return true;
25
38
  },
39
+ pushd: async (args) => {
40
+ const current = process.cwd();
41
+ if (args.length === 1) {
42
+ const next = dirStack.shift();
43
+ if (!next) {
44
+ process.stderr.write("pushd: no other directory\n");
45
+ return false;
46
+ }
47
+ dirStack.unshift(current);
48
+ try {
49
+ process.chdir(next);
50
+ updateDriveCwd();
51
+ }
52
+ catch (err) {
53
+ process.stderr.write(`pushd: ${err.message}\n`);
54
+ dirStack.shift();
55
+ return false;
56
+ }
57
+ writeDirStack();
58
+ return true;
59
+ }
60
+ const dest = expandVars(args[1]);
61
+ try {
62
+ process.chdir(dest);
63
+ updateDriveCwd();
64
+ }
65
+ catch (err) {
66
+ process.stderr.write(`pushd: ${err.message}\n`);
67
+ return false;
68
+ }
69
+ dirStack.unshift(current);
70
+ writeDirStack();
71
+ return true;
72
+ },
73
+ popd: async () => {
74
+ const next = dirStack.shift();
75
+ if (!next) {
76
+ process.stderr.write("popd: directory stack empty\n");
77
+ return false;
78
+ }
79
+ try {
80
+ process.chdir(next);
81
+ updateDriveCwd();
82
+ }
83
+ catch (err) {
84
+ process.stderr.write(`popd: ${err.message}\n`);
85
+ return false;
86
+ }
87
+ writeDirStack();
88
+ return true;
89
+ },
26
90
  exit: async () => {
27
91
  (0, process_1.exit)(0);
28
92
  return false;
29
93
  },
30
94
  };
95
+ function writeDirStack() {
96
+ const parts = [process.cwd(), ...dirStack];
97
+ process.stdout.write(parts.join(" ") + "\n");
98
+ }
31
99
  function expandVars(input) {
32
100
  let out = input;
33
101
  if (isWin) {
@@ -52,6 +120,20 @@ async function runUserCommand(command) {
52
120
  const trimmed = command.trim();
53
121
  if (!trimmed)
54
122
  return false;
123
+ if (isWin && /^[a-zA-Z]:$/.test(trimmed)) {
124
+ // Windows drive switch (eg "E:") should restore that drive's last cwd.
125
+ const drive = trimmed.toUpperCase();
126
+ const target = driveCwds[drive] ?? `${drive}\\`;
127
+ try {
128
+ process.chdir(target);
129
+ updateDriveCwd();
130
+ return true;
131
+ }
132
+ catch (err) {
133
+ process.stderr.write(`${trimmed}: ${err.message}\n`);
134
+ return false;
135
+ }
136
+ }
55
137
  const args = command.split(/\s+/);
56
138
  if (typeof args[0] === "string" && builtInCommands[args[0]]) {
57
139
  return await builtInCommands[args[0]](args);
@@ -70,3 +152,4 @@ async function runUserCommand(command) {
70
152
  // many times until we fix it.
71
153
  return true;
72
154
  }
155
+ updateDriveCwd();
package/dist/terminal.js CHANGED
@@ -29,6 +29,13 @@ class Terminal {
29
29
  enableWrites() {
30
30
  this.writesDisabled = false;
31
31
  }
32
+ reset() {
33
+ // Some apps (such as vim) change the terminal cursor mode.
34
+ // We need to reset it to the default. To avoid arrow keys causing this:
35
+ // $> OAOBOCODODODODOAOAOCOB
36
+ const RESET_CURSOR_MODE = "\x1b[?1l";
37
+ this.write(RESET_CURSOR_MODE);
38
+ }
32
39
  canWrite() {
33
40
  return !this.writesDisabled;
34
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caroushell",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Terminal carousel that suggests commands from history, config, and AI.",
5
5
  "type": "commonjs",
6
6
  "main": "dist/main.js",
@@ -17,7 +17,7 @@
17
17
  "build": "tsc -p tsconfig.release.json",
18
18
  "prepare": "npm run build",
19
19
  "start": "node dist/main.js",
20
- "test": "node --import tsx --test tests/history-suggester.test.ts",
20
+ "test": "node --import tsx --test tests/*.ts",
21
21
  "test:generate": "tsx src/test-generate.ts",
22
22
  "lint": "eslint . --ext .ts",
23
23
  "release": "tsx scripts/release.ts"