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 +4 -6
- package/dist/ai-suggester.js +4 -3
- package/dist/app.js +23 -7
- package/dist/history-suggester.js +3 -0
- package/dist/keyboard.js +3 -3
- package/dist/spawner.js +83 -0
- package/dist/terminal.js +7 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/caroushell)
|
|
4
4
|
[](https://www.npmjs.com/package/caroushell)
|
|
5
5
|
|
|
6
|
-
Caroushell is
|
|
7
|
-
|
|
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
|

|
|
44
41
|
|
|
45
|
-
##
|
|
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"
|
package/dist/ai-suggester.js
CHANGED
|
@@ -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,
|
|
175
|
-
useful, concise shell commands that the user might run
|
|
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
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
|
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"
|