caroushell 0.1.3 → 0.1.6
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 +13 -9
- package/dist/ai-suggester.js +0 -8
- package/dist/app.js +8 -4
- package/dist/carousel.js +2 -2
- package/dist/config.js +17 -5
- package/dist/hello-new-user.js +4 -1
- package/dist/history-suggester.js +40 -8
- package/dist/main.js +1 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -10,22 +10,26 @@ history, and AI suggestions as you type.
|
|
|
10
10
|
- Go up and down the carousel with arrow keys.
|
|
11
11
|
- Press `Enter` to run the highlighted command.
|
|
12
12
|
- Logs activity under `~/.caroushell/logs` for easy troubleshooting.
|
|
13
|
-
- Extensible config file (`~/.caroushell/config.
|
|
13
|
+
- Extensible config file (`~/.caroushell/config.toml`) so you can point the CLI
|
|
14
14
|
at different AI providers.
|
|
15
15
|
|
|
16
16
|
## Requirements
|
|
17
17
|
|
|
18
18
|
- Node.js 18 or newer.
|
|
19
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.
|
|
20
|
+
URL, API key, and model name, then store them in `~/.caroushell/config.toml`.
|
|
21
21
|
- You can also create the file manually:
|
|
22
22
|
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
```toml
|
|
24
|
+
apiUrl = "https://openrouter.ai/api/v1"
|
|
25
|
+
apiKey = "your-api-key"
|
|
26
|
+
model = "gpt-4o-mini"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
or
|
|
30
|
+
|
|
31
|
+
```toml
|
|
32
|
+
GEMINI_API_KEY = "AIzaSyD...N-wK"
|
|
29
33
|
```
|
|
30
34
|
|
|
31
35
|
Any endpoint that implements the OpenAI Chat Completions API (OpenRouter,
|
|
@@ -60,7 +64,7 @@ Caroushell opens an interactive prompt:
|
|
|
60
64
|
|
|
61
65
|
Logs are written to `~/.caroushell/logs/MM-DD.txt`. Inspect these files if you
|
|
62
66
|
need to debug AI suggestions or the terminal renderer. Configuration lives at
|
|
63
|
-
`~/.caroushell/config.
|
|
67
|
+
`~/.caroushell/config.toml` (override via `CAROUSHELL_CONFIG_PATH`).
|
|
64
68
|
|
|
65
69
|
## Development
|
|
66
70
|
|
package/dist/ai-suggester.js
CHANGED
|
@@ -109,14 +109,6 @@ class AISuggester {
|
|
|
109
109
|
this.apiUrl =
|
|
110
110
|
this.apiUrl || config.apiUrl || process.env.CAROUSHELL_API_URL;
|
|
111
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
|
-
}
|
|
120
112
|
}
|
|
121
113
|
descriptionForAi() {
|
|
122
114
|
return "";
|
package/dist/app.js
CHANGED
|
@@ -67,7 +67,13 @@ class App {
|
|
|
67
67
|
const cmd = this.carousel.getCurrentRow().trim();
|
|
68
68
|
this.carousel.setInputBuffer("", 0);
|
|
69
69
|
await this.runCommand(cmd);
|
|
70
|
+
// Carousel should point to the prompt
|
|
70
71
|
this.carousel.resetIndex();
|
|
72
|
+
// After arbitrary output, reset render block tracking
|
|
73
|
+
this.terminal.resetBlockTracking();
|
|
74
|
+
// Render the prompt, without this we'd wait for the suggestions to call render
|
|
75
|
+
// and it would appear slow
|
|
76
|
+
this.render();
|
|
71
77
|
this.queueUpdateSuggestions();
|
|
72
78
|
},
|
|
73
79
|
char: (evt) => {
|
|
@@ -129,7 +135,6 @@ class App {
|
|
|
129
135
|
await fn(evt);
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
|
-
queueUpdateSuggestions() { }
|
|
133
138
|
render() {
|
|
134
139
|
this.carousel.render();
|
|
135
140
|
// Cursor placement handled inside carousel render.
|
|
@@ -140,7 +145,6 @@ class App {
|
|
|
140
145
|
// Log an empty line
|
|
141
146
|
this.terminal.renderBlock([">"]);
|
|
142
147
|
this.terminal.write("\n");
|
|
143
|
-
this.terminal.resetBlockTracking();
|
|
144
148
|
return;
|
|
145
149
|
}
|
|
146
150
|
// Log command in yellow
|
|
@@ -159,10 +163,10 @@ class App {
|
|
|
159
163
|
finally {
|
|
160
164
|
this.keyboard.resume();
|
|
161
165
|
}
|
|
162
|
-
// After arbitrary output, reset render block tracking
|
|
163
|
-
this.terminal.resetBlockTracking();
|
|
164
166
|
}
|
|
165
167
|
exit() {
|
|
168
|
+
// Clear terminal contents before shutting down to leave a clean screen.
|
|
169
|
+
this.terminal.renderBlock([]);
|
|
166
170
|
this.keyboard.stop();
|
|
167
171
|
process.exit(0);
|
|
168
172
|
}
|
package/dist/carousel.js
CHANGED
|
@@ -25,11 +25,11 @@ class Carousel {
|
|
|
25
25
|
}
|
|
26
26
|
const topPromise = this.top.suggest(this, this.topRowCount);
|
|
27
27
|
const bottomPromise = this.bottom.suggest(this, this.bottomRowCount);
|
|
28
|
-
topPromise.then((r) => {
|
|
28
|
+
void topPromise.then((r) => {
|
|
29
29
|
this.latestTop = r;
|
|
30
30
|
this.render();
|
|
31
31
|
});
|
|
32
|
-
bottomPromise.then((r) => {
|
|
32
|
+
void bottomPromise.then((r) => {
|
|
33
33
|
this.latestBottom = r;
|
|
34
34
|
this.render();
|
|
35
35
|
});
|
package/dist/config.js
CHANGED
|
@@ -32,6 +32,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.configFolder = configFolder;
|
|
37
40
|
exports.getConfigPath = getConfigPath;
|
|
@@ -40,7 +43,8 @@ exports.getConfig = getConfig;
|
|
|
40
43
|
const fs_1 = require("fs");
|
|
41
44
|
const path = __importStar(require("path"));
|
|
42
45
|
const os = __importStar(require("os"));
|
|
43
|
-
const
|
|
46
|
+
const toml_1 = __importDefault(require("toml"));
|
|
47
|
+
const GEMINI_DEFAULT_API_URL = "https://generativelanguage.googleapis.com/v1beta/openai";
|
|
44
48
|
const GEMINI_DEFAULT_MODEL = "gemini-2.5-flash-lite";
|
|
45
49
|
function configFolder(subpath) {
|
|
46
50
|
const home = os.homedir();
|
|
@@ -48,19 +52,27 @@ function configFolder(subpath) {
|
|
|
48
52
|
return path.join(home, ".caroushell", subpath);
|
|
49
53
|
}
|
|
50
54
|
function getConfigPath() {
|
|
51
|
-
return process.env.CAROUSHELL_CONFIG_PATH || configFolder("config.
|
|
55
|
+
return process.env.CAROUSHELL_CONFIG_PATH || configFolder("config.toml");
|
|
52
56
|
}
|
|
53
57
|
async function readConfigFile() {
|
|
54
58
|
const configPath = getConfigPath();
|
|
55
59
|
const raw = await fs_1.promises.readFile(configPath, "utf8");
|
|
56
|
-
|
|
60
|
+
if (!raw.trim()) {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return toml_1.default.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
throw new Error(`Error parsing config file at ${configPath}: ${err}`);
|
|
68
|
+
}
|
|
57
69
|
}
|
|
58
70
|
async function doesConfigExist() {
|
|
59
71
|
const configPath = getConfigPath();
|
|
60
72
|
try {
|
|
61
73
|
await fs_1.promises.access(configPath);
|
|
62
74
|
const raw = await readConfigFile();
|
|
63
|
-
if (!raw)
|
|
75
|
+
if (!raw || Object.keys(raw).length === 0)
|
|
64
76
|
return false;
|
|
65
77
|
return true;
|
|
66
78
|
}
|
|
@@ -68,7 +80,7 @@ async function doesConfigExist() {
|
|
|
68
80
|
if (err?.code === "ENOENT") {
|
|
69
81
|
return false;
|
|
70
82
|
}
|
|
71
|
-
//
|
|
83
|
+
// Treat invalid TOML as missing so we can reprompt the user.
|
|
72
84
|
if (err instanceof SyntaxError) {
|
|
73
85
|
return false;
|
|
74
86
|
}
|
package/dist/hello-new-user.js
CHANGED
|
@@ -113,7 +113,10 @@ async function runHelloNewUserFlow(configPath) {
|
|
|
113
113
|
apiKey,
|
|
114
114
|
model,
|
|
115
115
|
};
|
|
116
|
-
|
|
116
|
+
const tomlBody = Object.entries(config)
|
|
117
|
+
.map(([key, value]) => `${key} = ${JSON.stringify(value)}`)
|
|
118
|
+
.join("\n");
|
|
119
|
+
await fs_1.promises.writeFile(configPath, tomlBody + "\n", "utf8");
|
|
117
120
|
console.log(`\nSaved config to ${configPath}`);
|
|
118
121
|
console.log("You can edit this file later if you want to switch providers.\n");
|
|
119
122
|
return config;
|
|
@@ -23,7 +23,7 @@ class HistorySuggester {
|
|
|
23
23
|
catch { }
|
|
24
24
|
try {
|
|
25
25
|
const data = await fs_1.promises.readFile(this.filePath, "utf8");
|
|
26
|
-
this.items =
|
|
26
|
+
this.items = this.parseHistory(data);
|
|
27
27
|
}
|
|
28
28
|
catch {
|
|
29
29
|
this.items = [];
|
|
@@ -32,29 +32,34 @@ class HistorySuggester {
|
|
|
32
32
|
async add(command) {
|
|
33
33
|
if (!command.trim())
|
|
34
34
|
return;
|
|
35
|
-
if (this.items[
|
|
35
|
+
if (this.items[0] === command) {
|
|
36
36
|
// Deduplicate recent duplicate
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
|
-
this.items.
|
|
39
|
+
this.items.unshift(command);
|
|
40
40
|
if (this.items.length > this.maxItems)
|
|
41
|
-
this.items.
|
|
41
|
+
this.items.pop();
|
|
42
42
|
await fs_1.promises
|
|
43
43
|
.mkdir(path_1.default.dirname(this.filePath), { recursive: true })
|
|
44
44
|
.catch(() => { });
|
|
45
|
-
await fs_1.promises.
|
|
45
|
+
await fs_1.promises.appendFile(this.filePath, this.serializeHistoryEntry(command), "utf8");
|
|
46
46
|
}
|
|
47
47
|
async suggest(carousel, maxDisplayed) {
|
|
48
48
|
const input = carousel.getCurrentRow();
|
|
49
49
|
if (!input) {
|
|
50
|
-
|
|
50
|
+
// this.items 0 index is newest
|
|
51
|
+
return this.items;
|
|
51
52
|
}
|
|
52
53
|
const q = input.toLowerCase();
|
|
53
54
|
const matched = [];
|
|
54
|
-
|
|
55
|
+
// iterate from newest to oldest so we skip older duplicates
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
55
58
|
const it = this.items[i];
|
|
56
|
-
if (it.toLowerCase().includes(q))
|
|
59
|
+
if (it.toLowerCase().includes(q) && !seen.has(it)) {
|
|
60
|
+
seen.add(it);
|
|
57
61
|
matched.push(it);
|
|
62
|
+
}
|
|
58
63
|
}
|
|
59
64
|
return matched;
|
|
60
65
|
}
|
|
@@ -75,5 +80,32 @@ class HistorySuggester {
|
|
|
75
80
|
}
|
|
76
81
|
return lines.join("\n");
|
|
77
82
|
}
|
|
83
|
+
parseHistory(data) {
|
|
84
|
+
const entries = [];
|
|
85
|
+
let currentLines = [];
|
|
86
|
+
const flush = () => {
|
|
87
|
+
if (currentLines.length > 0) {
|
|
88
|
+
entries.push(currentLines.join("\n"));
|
|
89
|
+
currentLines = [];
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const rows = data.split(/\n/);
|
|
93
|
+
for (const rawLine of rows) {
|
|
94
|
+
const line = rawLine.replace(/\r$/, "");
|
|
95
|
+
if (line.startsWith("+")) {
|
|
96
|
+
currentLines.push(line.slice(1));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
flush();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
flush();
|
|
103
|
+
return entries.slice(-this.maxItems).reverse();
|
|
104
|
+
}
|
|
105
|
+
serializeHistoryEntry(command) {
|
|
106
|
+
const timestamp = new Date().toISOString();
|
|
107
|
+
const lines = command.split("\n").map((line) => `+${line}`);
|
|
108
|
+
return `\n# ${timestamp}\n${lines.join("\n")}\n`;
|
|
109
|
+
}
|
|
78
110
|
}
|
|
79
111
|
exports.HistorySuggester = HistorySuggester;
|
package/dist/main.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caroushell",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Terminal carousel that suggests commands from history, config, and AI.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"dev": "tsx src/main.ts",
|
|
17
|
-
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"build": "tsc -p tsconfig.release.json",
|
|
18
18
|
"prepare": "npm run build",
|
|
19
19
|
"start": "node dist/main.js",
|
|
20
|
-
"test:generate": "tsx src/test-generate.ts"
|
|
20
|
+
"test:generate": "tsx src/test-generate.ts",
|
|
21
|
+
"lint": "eslint . --ext .ts",
|
|
22
|
+
"release": "tsx scripts/release.ts"
|
|
21
23
|
},
|
|
22
24
|
"keywords": [
|
|
23
25
|
"cli",
|
|
@@ -46,5 +48,8 @@
|
|
|
46
48
|
"eslint": "^9.39.1",
|
|
47
49
|
"tsx": "^4.19.2",
|
|
48
50
|
"typescript": "^5.6.3"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"toml": "^3.0.0"
|
|
49
54
|
}
|
|
50
55
|
}
|