caroushell 0.1.0
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/ai-suggester.js +119 -0
- package/dist/app.js +138 -0
- package/dist/carousel.js +160 -0
- package/dist/config.js +51 -0
- package/dist/history-suggester.js +78 -0
- package/dist/keyboard.js +116 -0
- package/dist/logs.js +68 -0
- package/dist/main.js +15 -0
- package/dist/terminal.js +108 -0
- package/dist/test-generate.js +16 -0
- package/package.json +46 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ubershmekel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Caroushell
|
|
2
|
+
|
|
3
|
+
Caroushell is an interactive terminal carousel that suggests commands from your
|
|
4
|
+
history, AI prompts, and configuration snippets so you can pick the next shell
|
|
5
|
+
command without leaving the keyboard.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Dual-pane carousel that combines history-based and AI-generated command
|
|
10
|
+
suggestions.
|
|
11
|
+
- Runs the selected command directly in your current terminal session.
|
|
12
|
+
- Logs activity under `~/.caroushell/logs` for easy troubleshooting.
|
|
13
|
+
- Extensible config file (`~/.caroushell/config.json`) so you can point the CLI
|
|
14
|
+
at different API keys or settings.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
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:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"GEMINI_API_KEY": "your-api-key"
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
Install globally (recommended):
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g caroushell
|
|
34
|
+
caroushell
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or run it ad-hoc with NPX once it is published:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx caroushell
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
Caroushell opens an interactive prompt:
|
|
46
|
+
|
|
47
|
+
- Type to update the suggestions immediately and trigger refreshed history/AI
|
|
48
|
+
results.
|
|
49
|
+
- Use arrow keys to move between suggestions in the carousel.
|
|
50
|
+
- Press `Enter` to run the highlighted command.
|
|
51
|
+
- Press `Ctrl+C` to exit. `Ctrl+D` exits when the current row is empty.
|
|
52
|
+
|
|
53
|
+
Logs are written to `~/.caroushell/logs/MM-DD.txt`. Inspect these files if you
|
|
54
|
+
need to debug AI suggestions or the terminal renderer. Configuration lives at
|
|
55
|
+
`~/.caroushell/config.json` (override via `CAROUSHELL_CONFIG_PATH`).
|
|
56
|
+
|
|
57
|
+
## Development
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install
|
|
61
|
+
npm run dev # tsx watch mode
|
|
62
|
+
npm run build # emits dist/
|
|
63
|
+
npm run test:generate
|
|
64
|
+
npm publish --dry-run # verify package contents before publishing
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The `prepare` script automatically builds before `npm publish` or when
|
|
68
|
+
installing from git. The package ships only the compiled `dist/` output plus
|
|
69
|
+
this README and the MIT license so `npx caroushell` works immediately.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
Released under the [MIT License](./LICENSE).
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AISuggester = void 0;
|
|
4
|
+
exports.generateContent = generateContent;
|
|
5
|
+
const logs_1 = require("./logs");
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
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";
|
|
10
|
+
const temperature = options?.temperature ?? 0.3;
|
|
11
|
+
const maxOutputTokens = options?.maxOutputTokens ?? 256;
|
|
12
|
+
if (!apiKey) {
|
|
13
|
+
(0, logs_1.logLine)("AI generation skipped: missing API key");
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
if (!prompt.trim()) {
|
|
17
|
+
(0, logs_1.logLine)("AI generation skipped: empty prompt");
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
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}`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
contents: [
|
|
30
|
+
{
|
|
31
|
+
role: "user",
|
|
32
|
+
parts: [{ text: prompt }],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
generationConfig: {
|
|
36
|
+
temperature,
|
|
37
|
+
maxOutputTokens,
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
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
|
+
}
|
|
67
|
+
}
|
|
68
|
+
class AISuggester {
|
|
69
|
+
constructor(opts) {
|
|
70
|
+
this.prefix = "🤖";
|
|
71
|
+
this.apiKey = opts?.apiKey;
|
|
72
|
+
this.model = opts?.model || "gemini-2.5-flash-lite";
|
|
73
|
+
}
|
|
74
|
+
async init() {
|
|
75
|
+
this.apiKey =
|
|
76
|
+
this.apiKey ||
|
|
77
|
+
(await (0, config_1.getConfig)()).GEMINI_API_KEY ||
|
|
78
|
+
process.env.GEMINI_API_KEY;
|
|
79
|
+
}
|
|
80
|
+
descriptionForAi() {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
async suggest(carousel, maxDisplayed) {
|
|
84
|
+
if (!this.apiKey) {
|
|
85
|
+
(0, logs_1.logLine)("AI generation skipped: missing API key");
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
const descriptions = [];
|
|
89
|
+
for (const suggester of carousel.getSuggesters()) {
|
|
90
|
+
const desc = suggester.descriptionForAi();
|
|
91
|
+
if (desc) {
|
|
92
|
+
descriptions.push(desc);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const prompt = `You are a shell assistant. Given a partial shell input, suggest ${maxDisplayed}\
|
|
96
|
+
useful, concise shell commands that the user might run next.\
|
|
97
|
+
Return one suggestion per line, no numbering, no extra text.
|
|
98
|
+
|
|
99
|
+
The current line is: "${carousel.getCurrentRow()}
|
|
100
|
+
|
|
101
|
+
${descriptions.join("\n\n")}
|
|
102
|
+
`;
|
|
103
|
+
(0, logs_1.logLine)(prompt);
|
|
104
|
+
const text = await generateContent(prompt, {
|
|
105
|
+
apiKey: this.apiKey,
|
|
106
|
+
model: this.model,
|
|
107
|
+
temperature: 0.3,
|
|
108
|
+
maxOutputTokens: 128,
|
|
109
|
+
});
|
|
110
|
+
const lines = text
|
|
111
|
+
.split(/\r?\n/)
|
|
112
|
+
.map((s) => s.trim())
|
|
113
|
+
.filter(Boolean)
|
|
114
|
+
.slice(0, maxDisplayed);
|
|
115
|
+
(0, logs_1.logLine)(`AI lines: ${lines.length}`);
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.AISuggester = AISuggester;
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.App = void 0;
|
|
4
|
+
const terminal_1 = require("./terminal");
|
|
5
|
+
const keyboard_1 = require("./keyboard");
|
|
6
|
+
const carousel_1 = require("./carousel");
|
|
7
|
+
const history_suggester_1 = require("./history-suggester");
|
|
8
|
+
const ai_suggester_1 = require("./ai-suggester");
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
function debounce(fn, ms) {
|
|
11
|
+
// Debounce function to limit the rate at which a function can fire
|
|
12
|
+
let t = null;
|
|
13
|
+
return (...args) => {
|
|
14
|
+
if (t)
|
|
15
|
+
clearTimeout(t);
|
|
16
|
+
t = setTimeout(() => fn(...args), ms);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
class App {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.terminal = new terminal_1.Terminal();
|
|
22
|
+
this.keyboard = new keyboard_1.Keyboard();
|
|
23
|
+
this.history = new history_suggester_1.HistorySuggester();
|
|
24
|
+
this.ai = new ai_suggester_1.AISuggester();
|
|
25
|
+
this.carousel = new carousel_1.Carousel({
|
|
26
|
+
top: this.history,
|
|
27
|
+
bottom: this.ai,
|
|
28
|
+
topRows: 2,
|
|
29
|
+
bottomRows: 2,
|
|
30
|
+
terminal: this.terminal,
|
|
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 () => {
|
|
41
|
+
await this.carousel.updateSuggestions();
|
|
42
|
+
}, 300);
|
|
43
|
+
const handlers = {
|
|
44
|
+
"ctrl-c": () => this.exit(),
|
|
45
|
+
"ctrl-d": () => {
|
|
46
|
+
if (this.carousel.getCurrentRow().length === 0)
|
|
47
|
+
this.exit();
|
|
48
|
+
},
|
|
49
|
+
backspace: () => {
|
|
50
|
+
this.carousel.deleteBeforeCursor();
|
|
51
|
+
// Immediate prompt redraw with existing suggestions
|
|
52
|
+
this.render();
|
|
53
|
+
// Async fetch of new suggestions
|
|
54
|
+
updateSuggestions();
|
|
55
|
+
},
|
|
56
|
+
enter: async () => {
|
|
57
|
+
const cmd = this.carousel.getCurrentRow().trim();
|
|
58
|
+
this.carousel.setInputBuffer("", 0);
|
|
59
|
+
await this.runCommand(cmd);
|
|
60
|
+
this.carousel.resetIndex();
|
|
61
|
+
updateSuggestions();
|
|
62
|
+
},
|
|
63
|
+
char: (evt) => {
|
|
64
|
+
this.carousel.insertAtCursor(evt.sequence);
|
|
65
|
+
// Immediate prompt redraw with existing suggestions
|
|
66
|
+
this.render();
|
|
67
|
+
// Async fetch of new suggestions
|
|
68
|
+
updateSuggestions();
|
|
69
|
+
},
|
|
70
|
+
up: () => {
|
|
71
|
+
this.carousel.up();
|
|
72
|
+
this.render();
|
|
73
|
+
},
|
|
74
|
+
down: () => {
|
|
75
|
+
this.carousel.down();
|
|
76
|
+
this.render();
|
|
77
|
+
},
|
|
78
|
+
left: () => {
|
|
79
|
+
this.carousel.moveCursorLeft();
|
|
80
|
+
this.render();
|
|
81
|
+
},
|
|
82
|
+
right: () => {
|
|
83
|
+
this.carousel.moveCursorRight();
|
|
84
|
+
this.render();
|
|
85
|
+
},
|
|
86
|
+
home: () => { },
|
|
87
|
+
end: () => { },
|
|
88
|
+
delete: () => { },
|
|
89
|
+
escape: () => { },
|
|
90
|
+
};
|
|
91
|
+
this.keyboard.on("key", async (evt) => {
|
|
92
|
+
const fn = handlers[evt.name];
|
|
93
|
+
if (fn)
|
|
94
|
+
await fn(evt);
|
|
95
|
+
});
|
|
96
|
+
// Initial draw
|
|
97
|
+
this.render();
|
|
98
|
+
await this.carousel.updateSuggestions();
|
|
99
|
+
}
|
|
100
|
+
render() {
|
|
101
|
+
this.carousel.render();
|
|
102
|
+
// Cursor placement handled inside carousel render.
|
|
103
|
+
}
|
|
104
|
+
async runCommand(cmd) {
|
|
105
|
+
const { yellow, reset } = terminal_1.colors;
|
|
106
|
+
if (!cmd) {
|
|
107
|
+
// Log an empty line
|
|
108
|
+
this.terminal.renderBlock([">"]);
|
|
109
|
+
this.terminal.write("\n");
|
|
110
|
+
this.terminal.resetBlockTracking();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Log command in yellow
|
|
114
|
+
const width = process.stdout.columns || 80;
|
|
115
|
+
const lines = [`${yellow}$ ${cmd}${reset}`];
|
|
116
|
+
this.terminal.renderBlock(lines);
|
|
117
|
+
// Ensure command output starts on the next line
|
|
118
|
+
this.terminal.write("\n");
|
|
119
|
+
await this.history.add(cmd);
|
|
120
|
+
// Spawn shell
|
|
121
|
+
const isWin = process.platform === "win32";
|
|
122
|
+
const proc = (0, child_process_1.spawn)(isWin ? "cmd.exe" : "/bin/bash", [isWin ? "/c" : "-lc", cmd], {
|
|
123
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
124
|
+
});
|
|
125
|
+
await new Promise((resolve) => {
|
|
126
|
+
proc.stdout.on("data", (d) => process.stdout.write(d));
|
|
127
|
+
proc.stderr.on("data", (d) => process.stderr.write(d));
|
|
128
|
+
proc.on("close", () => resolve());
|
|
129
|
+
});
|
|
130
|
+
// After arbitrary output, reset render block tracking
|
|
131
|
+
this.terminal.resetBlockTracking();
|
|
132
|
+
}
|
|
133
|
+
exit() {
|
|
134
|
+
this.keyboard.stop();
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.App = App;
|
package/dist/carousel.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Carousel = void 0;
|
|
4
|
+
const logs_1 = require("./logs");
|
|
5
|
+
const terminal_1 = require("./terminal");
|
|
6
|
+
class Carousel {
|
|
7
|
+
constructor(opts) {
|
|
8
|
+
this.latestTop = [];
|
|
9
|
+
this.latestBottom = [];
|
|
10
|
+
this.index = 0;
|
|
11
|
+
this.inputBuffer = "";
|
|
12
|
+
this.inputCursor = 0;
|
|
13
|
+
this.terminal = opts.terminal;
|
|
14
|
+
this.top = opts.top;
|
|
15
|
+
this.bottom = opts.bottom;
|
|
16
|
+
this.topRowCount = opts.topRows;
|
|
17
|
+
this.bottomRowCount = opts.bottomRows;
|
|
18
|
+
const empty = "---";
|
|
19
|
+
this.latestTop = Array(this.topRowCount).fill(empty);
|
|
20
|
+
this.latestBottom = Array(this.bottomRowCount).fill(empty);
|
|
21
|
+
}
|
|
22
|
+
async updateSuggestions(input) {
|
|
23
|
+
if (typeof input === "string") {
|
|
24
|
+
this.setInputBuffer(input);
|
|
25
|
+
}
|
|
26
|
+
const topPromise = this.top.suggest(this, this.topRowCount);
|
|
27
|
+
const bottomPromise = this.bottom.suggest(this, this.bottomRowCount);
|
|
28
|
+
topPromise.then((r) => {
|
|
29
|
+
this.latestTop = r;
|
|
30
|
+
this.render();
|
|
31
|
+
});
|
|
32
|
+
bottomPromise.then((r) => {
|
|
33
|
+
this.latestBottom = r;
|
|
34
|
+
this.render();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
up() {
|
|
38
|
+
this.index += 1;
|
|
39
|
+
if (this.index >= this.latestTop.length) {
|
|
40
|
+
this.index = this.latestTop.length;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
down() {
|
|
44
|
+
this.index -= 1;
|
|
45
|
+
if (-this.index >= this.latestBottom.length) {
|
|
46
|
+
this.index = -this.latestBottom.length;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
getRow(rowIndex) {
|
|
50
|
+
if (rowIndex < 0) {
|
|
51
|
+
const bottomIndex = -rowIndex - 1;
|
|
52
|
+
return this.latestBottom[bottomIndex] || "";
|
|
53
|
+
}
|
|
54
|
+
if (rowIndex === 0) {
|
|
55
|
+
return this.inputBuffer;
|
|
56
|
+
}
|
|
57
|
+
if (rowIndex > 0) {
|
|
58
|
+
const topIndex = rowIndex - 1;
|
|
59
|
+
return this.latestTop[topIndex] || "";
|
|
60
|
+
}
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
getPrefixByIndex(index) {
|
|
64
|
+
if (index < 0) {
|
|
65
|
+
return this.bottom.prefix;
|
|
66
|
+
}
|
|
67
|
+
if (index > 0) {
|
|
68
|
+
return this.top.prefix;
|
|
69
|
+
}
|
|
70
|
+
return "$> ";
|
|
71
|
+
}
|
|
72
|
+
getFormattedRow(rowIndex) {
|
|
73
|
+
const rowStr = this.getRow(rowIndex);
|
|
74
|
+
let prefix = this.getPrefixByIndex(rowIndex);
|
|
75
|
+
const { brightWhite, reset, dim } = terminal_1.colors;
|
|
76
|
+
let color = dim;
|
|
77
|
+
if (this.index === rowIndex) {
|
|
78
|
+
color = brightWhite;
|
|
79
|
+
if (rowIndex !== 0) {
|
|
80
|
+
prefix = "> ";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return `${color}${prefix}${rowStr}${reset}`;
|
|
84
|
+
}
|
|
85
|
+
getCurrentRow() {
|
|
86
|
+
return this.getRow(this.index);
|
|
87
|
+
}
|
|
88
|
+
setInputBuffer(value, cursorPos = value.length) {
|
|
89
|
+
this.inputBuffer = value;
|
|
90
|
+
this.inputCursor = Math.max(0, Math.min(cursorPos, this.inputBuffer.length));
|
|
91
|
+
}
|
|
92
|
+
resetIndex() {
|
|
93
|
+
this.index = 0;
|
|
94
|
+
}
|
|
95
|
+
adoptSelectionIntoInput() {
|
|
96
|
+
if (this.index === 0)
|
|
97
|
+
return;
|
|
98
|
+
const current = this.getRow(this.index);
|
|
99
|
+
this.setInputBuffer(current, current.length);
|
|
100
|
+
this.index = 0;
|
|
101
|
+
}
|
|
102
|
+
insertAtCursor(text) {
|
|
103
|
+
if (!text)
|
|
104
|
+
return;
|
|
105
|
+
this.adoptSelectionIntoInput();
|
|
106
|
+
const before = this.inputBuffer.slice(0, this.inputCursor);
|
|
107
|
+
const after = this.inputBuffer.slice(this.inputCursor);
|
|
108
|
+
this.inputBuffer = `${before}${text}${after}`;
|
|
109
|
+
this.inputCursor += text.length;
|
|
110
|
+
}
|
|
111
|
+
deleteBeforeCursor() {
|
|
112
|
+
this.adoptSelectionIntoInput();
|
|
113
|
+
if (this.inputCursor === 0)
|
|
114
|
+
return;
|
|
115
|
+
const before = this.inputBuffer.slice(0, this.inputCursor - 1);
|
|
116
|
+
const after = this.inputBuffer.slice(this.inputCursor);
|
|
117
|
+
this.inputBuffer = `${before}${after}`;
|
|
118
|
+
this.inputCursor -= 1;
|
|
119
|
+
}
|
|
120
|
+
moveCursorLeft() {
|
|
121
|
+
this.adoptSelectionIntoInput();
|
|
122
|
+
if (this.inputCursor === 0)
|
|
123
|
+
return;
|
|
124
|
+
this.inputCursor -= 1;
|
|
125
|
+
}
|
|
126
|
+
moveCursorRight() {
|
|
127
|
+
this.adoptSelectionIntoInput();
|
|
128
|
+
if (this.inputCursor >= this.inputBuffer.length)
|
|
129
|
+
return;
|
|
130
|
+
this.inputCursor += 1;
|
|
131
|
+
}
|
|
132
|
+
getPromptCursorColumn() {
|
|
133
|
+
const prefix = this.getPrefixByIndex(0);
|
|
134
|
+
return prefix.length + this.inputCursor;
|
|
135
|
+
}
|
|
136
|
+
render() {
|
|
137
|
+
(0, logs_1.logLine)("Rendering carousel");
|
|
138
|
+
// Draw all the lines
|
|
139
|
+
const width = process.stdout.columns || 80;
|
|
140
|
+
const { brightWhite, reset, dim } = terminal_1.colors;
|
|
141
|
+
const lines = [];
|
|
142
|
+
const start = this.index + this.topRowCount;
|
|
143
|
+
const rowCount = this.topRowCount + this.bottomRowCount + 1;
|
|
144
|
+
const end = start - rowCount;
|
|
145
|
+
for (let i = start; i > end; i--) {
|
|
146
|
+
lines.push({
|
|
147
|
+
rowIndex: i,
|
|
148
|
+
text: this.getFormattedRow(i).slice(0, width - 2),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const promptLineIndex = lines.findIndex((line) => line.rowIndex === 0);
|
|
152
|
+
const cursorRow = this.topRowCount;
|
|
153
|
+
const cursorCol = this.getPromptCursorColumn();
|
|
154
|
+
this.terminal.renderBlock(lines.map((line) => line.text), cursorRow, cursorCol);
|
|
155
|
+
}
|
|
156
|
+
getSuggesters() {
|
|
157
|
+
return [this.top, this.bottom];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
exports.Carousel = Carousel;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.configFolder = configFolder;
|
|
37
|
+
exports.getConfig = getConfig;
|
|
38
|
+
const fs_1 = require("fs");
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const os = __importStar(require("os"));
|
|
41
|
+
function configFolder(subpath) {
|
|
42
|
+
const home = os.homedir();
|
|
43
|
+
// Default path: ~/.caroushell/history
|
|
44
|
+
return path.join(home, ".caroushell", subpath);
|
|
45
|
+
}
|
|
46
|
+
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;
|
|
51
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HistorySuggester = void 0;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const config_1 = require("./config");
|
|
10
|
+
class HistorySuggester {
|
|
11
|
+
constructor(filePath) {
|
|
12
|
+
this.prefix = "⌛";
|
|
13
|
+
this.items = [];
|
|
14
|
+
this.maxItems = 1000;
|
|
15
|
+
const home = process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
16
|
+
// Default path: ~/.caroushell/history
|
|
17
|
+
this.filePath = filePath || (0, config_1.configFolder)("history");
|
|
18
|
+
}
|
|
19
|
+
async init() {
|
|
20
|
+
try {
|
|
21
|
+
await fs_1.promises.mkdir(path_1.default.dirname(this.filePath), { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
try {
|
|
25
|
+
const data = await fs_1.promises.readFile(this.filePath, "utf8");
|
|
26
|
+
this.items = data.split(/\r?\n/).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
this.items = [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async add(command) {
|
|
33
|
+
if (!command.trim())
|
|
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");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async suggest(carousel, maxDisplayed) {
|
|
47
|
+
const input = carousel.getCurrentRow();
|
|
48
|
+
if (!input) {
|
|
49
|
+
return this.items.reverse();
|
|
50
|
+
}
|
|
51
|
+
const q = input.toLowerCase();
|
|
52
|
+
const matched = [];
|
|
53
|
+
for (let i = this.items.length - 1; i >= 0; i--) {
|
|
54
|
+
const it = this.items[i];
|
|
55
|
+
if (it.toLowerCase().includes(q))
|
|
56
|
+
matched.push(it);
|
|
57
|
+
}
|
|
58
|
+
return matched;
|
|
59
|
+
}
|
|
60
|
+
descriptionForAi() {
|
|
61
|
+
const lines = [];
|
|
62
|
+
const maxHistoryLines = 20;
|
|
63
|
+
const start = Math.max(0, this.items.length - maxHistoryLines);
|
|
64
|
+
const end = this.items.length - 1;
|
|
65
|
+
const reverseSlice = this.items.slice(start, end).reverse();
|
|
66
|
+
if (reverseSlice.length > 0) {
|
|
67
|
+
lines.push(`The most recent command is: "${reverseSlice[0]}"`);
|
|
68
|
+
}
|
|
69
|
+
if (reverseSlice.length > 1) {
|
|
70
|
+
lines.push("The most recent commands are (from recent to oldest):");
|
|
71
|
+
for (let i = 0; i < reverseSlice.length; i++) {
|
|
72
|
+
lines.push(` ${i + 1}. ${reverseSlice[i]}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.HistorySuggester = HistorySuggester;
|
package/dist/keyboard.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Keyboard = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
// Map escape/control sequences to semantic key names
|
|
6
|
+
const KEYMAP = {
|
|
7
|
+
// Control keys
|
|
8
|
+
'\u0003': { name: 'ctrl-c', ctrl: true }, // ^C
|
|
9
|
+
'\u0004': { name: 'ctrl-d', ctrl: true }, // ^D
|
|
10
|
+
'\r': { name: 'enter' },
|
|
11
|
+
'\n': { name: 'enter' },
|
|
12
|
+
'\u007f': { name: 'backspace' }, // DEL
|
|
13
|
+
'\u0008': { name: 'backspace' }, // BS (Windows)
|
|
14
|
+
'\u001b': { name: 'escape' },
|
|
15
|
+
// Arrows (ANSI)
|
|
16
|
+
'\u001b[A': { name: 'up' },
|
|
17
|
+
'\u001b[B': { name: 'down' },
|
|
18
|
+
'\u001b[C': { name: 'right' },
|
|
19
|
+
'\u001b[D': { name: 'left' },
|
|
20
|
+
// Home/End/Delete variants
|
|
21
|
+
'\u001b[H': { name: 'home' },
|
|
22
|
+
'\u001b[F': { name: 'end' },
|
|
23
|
+
'\u001b[1~': { name: 'home' },
|
|
24
|
+
'\u001b[4~': { name: 'end' },
|
|
25
|
+
'\u001b[3~': { name: 'delete' },
|
|
26
|
+
};
|
|
27
|
+
// For efficient prefix checks
|
|
28
|
+
const KEY_PREFIXES = new Set();
|
|
29
|
+
for (const seq of Object.keys(KEYMAP)) {
|
|
30
|
+
for (let i = 1; i <= seq.length; i++) {
|
|
31
|
+
KEY_PREFIXES.add(seq.slice(0, i));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
class Keyboard extends events_1.EventEmitter {
|
|
35
|
+
constructor() {
|
|
36
|
+
super(...arguments);
|
|
37
|
+
this.active = false;
|
|
38
|
+
this.buffer = '';
|
|
39
|
+
}
|
|
40
|
+
start() {
|
|
41
|
+
if (this.active)
|
|
42
|
+
return;
|
|
43
|
+
this.active = true;
|
|
44
|
+
const stdin = process.stdin;
|
|
45
|
+
stdin.setEncoding('utf8');
|
|
46
|
+
if (stdin.isTTY)
|
|
47
|
+
stdin.setRawMode(true);
|
|
48
|
+
stdin.resume();
|
|
49
|
+
const onData = (data) => this.handleData(data);
|
|
50
|
+
stdin.on('data', onData);
|
|
51
|
+
this.once('stop', () => {
|
|
52
|
+
stdin.off('data', onData);
|
|
53
|
+
if (stdin.isTTY)
|
|
54
|
+
stdin.setRawMode(false);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
stop() {
|
|
58
|
+
if (!this.active)
|
|
59
|
+
return;
|
|
60
|
+
this.active = false;
|
|
61
|
+
this.emit('stop');
|
|
62
|
+
}
|
|
63
|
+
handleData(data) {
|
|
64
|
+
this.buffer += data;
|
|
65
|
+
this.processBuffer();
|
|
66
|
+
}
|
|
67
|
+
processBuffer() {
|
|
68
|
+
// Try to consume as many full key sequences as possible
|
|
69
|
+
while (this.buffer.length > 0) {
|
|
70
|
+
const evt = this.matchSequence(this.buffer);
|
|
71
|
+
if (evt === 'need-more')
|
|
72
|
+
return; // wait for more bytes
|
|
73
|
+
if (evt) {
|
|
74
|
+
this.emit('key', evt);
|
|
75
|
+
this.buffer = this.buffer.slice(evt.sequence.length);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// No mapped sequence at buffer start; emit first char as 'char'
|
|
79
|
+
const ch = this.buffer[0];
|
|
80
|
+
const code = ch.charCodeAt(0);
|
|
81
|
+
if (code < 32 && ch !== '\t') {
|
|
82
|
+
// ignore other control chars
|
|
83
|
+
this.buffer = this.buffer.slice(1);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
this.emit('key', { name: 'char', sequence: ch });
|
|
87
|
+
this.buffer = this.buffer.slice(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
matchSequence(buf) {
|
|
91
|
+
// Fast path: exact match
|
|
92
|
+
const exact = KEYMAP[buf];
|
|
93
|
+
if (exact)
|
|
94
|
+
return { ...exact, sequence: buf };
|
|
95
|
+
// Try the longest possible mapped sequence that matches the buffer prefix
|
|
96
|
+
// Limit search by checking prefixes set.
|
|
97
|
+
let maxLen = 0;
|
|
98
|
+
let matched = null;
|
|
99
|
+
for (const seq of Object.keys(KEYMAP)) {
|
|
100
|
+
if (buf.startsWith(seq)) {
|
|
101
|
+
if (seq.length > maxLen) {
|
|
102
|
+
maxLen = seq.length;
|
|
103
|
+
matched = { ...KEYMAP[seq], sequence: seq };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (matched)
|
|
108
|
+
return matched;
|
|
109
|
+
// If current buffer is a prefix to any known sequence, wait for more
|
|
110
|
+
if (KEY_PREFIXES.has(buf))
|
|
111
|
+
return 'need-more';
|
|
112
|
+
// No sequence match
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.Keyboard = Keyboard;
|
package/dist/logs.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.logLine = logLine;
|
|
37
|
+
exports.ensureLogFolderExists = ensureLogFolderExists;
|
|
38
|
+
const fs_1 = require("fs");
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const config_1 = require("./config");
|
|
41
|
+
function getLogDir() {
|
|
42
|
+
return (0, config_1.configFolder)("logs");
|
|
43
|
+
}
|
|
44
|
+
function getLogFilePath(d = new Date()) {
|
|
45
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
46
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
47
|
+
return path.join(getLogDir(), `${mm}-${dd}.txt`);
|
|
48
|
+
}
|
|
49
|
+
async function ensureDir(dir) {
|
|
50
|
+
await fs_1.promises.mkdir(dir, { recursive: true });
|
|
51
|
+
}
|
|
52
|
+
function timestamp(date = new Date()) {
|
|
53
|
+
// local time iso string
|
|
54
|
+
return date.toISOString();
|
|
55
|
+
}
|
|
56
|
+
async function logLine(message, when = new Date()) {
|
|
57
|
+
const dir = getLogDir();
|
|
58
|
+
await ensureDir(dir);
|
|
59
|
+
const file = getLogFilePath(when);
|
|
60
|
+
const line = `[${timestamp(when)}] ${message}\n`;
|
|
61
|
+
await fs_1.promises.appendFile(file, line, "utf8");
|
|
62
|
+
}
|
|
63
|
+
// Ensure the ~/.caroushell/logs folder exists early in app startup
|
|
64
|
+
async function ensureLogFolderExists() {
|
|
65
|
+
const dir = getLogDir();
|
|
66
|
+
await ensureDir(dir);
|
|
67
|
+
return dir;
|
|
68
|
+
}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const app_1 = require("./app");
|
|
5
|
+
const logs_1 = require("./logs");
|
|
6
|
+
async function main() {
|
|
7
|
+
await (0, logs_1.ensureLogFolderExists)();
|
|
8
|
+
(0, logs_1.logLine)("Caroushell started");
|
|
9
|
+
const app = new app_1.App();
|
|
10
|
+
await app.run();
|
|
11
|
+
}
|
|
12
|
+
main().catch((err) => {
|
|
13
|
+
console.error(err);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
});
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Terminal = exports.colors = void 0;
|
|
7
|
+
const readline_1 = __importDefault(require("readline"));
|
|
8
|
+
// Color helpers
|
|
9
|
+
exports.colors = {
|
|
10
|
+
reset: "\x1b[0m",
|
|
11
|
+
white: "\x1b[37m",
|
|
12
|
+
brightWhite: "\x1b[97m",
|
|
13
|
+
dim: "\x1b[2m",
|
|
14
|
+
yellow: "\x1b[33m",
|
|
15
|
+
};
|
|
16
|
+
class Terminal {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.out = process.stdout;
|
|
19
|
+
this.activeRows = 0;
|
|
20
|
+
this.cursorRow = 0;
|
|
21
|
+
this.cursorCol = 0;
|
|
22
|
+
}
|
|
23
|
+
moveCursorToTopOfBlock() {
|
|
24
|
+
if (this.activeRows === 0)
|
|
25
|
+
return;
|
|
26
|
+
readline_1.default.cursorTo(this.out, 0);
|
|
27
|
+
if (this.cursorRow > 0) {
|
|
28
|
+
readline_1.default.moveCursor(this.out, 0, -this.cursorRow);
|
|
29
|
+
}
|
|
30
|
+
this.cursorRow = 0;
|
|
31
|
+
this.cursorCol = 0;
|
|
32
|
+
}
|
|
33
|
+
withCork(fn) {
|
|
34
|
+
// Cork is like "don't flush" and then "uncork" is like flush.
|
|
35
|
+
// This prevents a flicker on the screen when we move the cursor around to render.
|
|
36
|
+
// Node's Writable has cork/uncork; guard for environments that may not.
|
|
37
|
+
const w = this.out;
|
|
38
|
+
const hasCork = typeof w.cork === "function" &&
|
|
39
|
+
typeof w.uncork === "function";
|
|
40
|
+
if (!hasCork) {
|
|
41
|
+
return fn();
|
|
42
|
+
}
|
|
43
|
+
w.cork();
|
|
44
|
+
try {
|
|
45
|
+
return fn();
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
w.uncork();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
write(text) {
|
|
52
|
+
this.out.write(text);
|
|
53
|
+
}
|
|
54
|
+
hideCursor() {
|
|
55
|
+
this.write("\x1b[?25l");
|
|
56
|
+
}
|
|
57
|
+
showCursor() {
|
|
58
|
+
this.write("\x1b[?25h");
|
|
59
|
+
}
|
|
60
|
+
// Render a block of lines by clearing previous block (if any) and writing fresh
|
|
61
|
+
renderBlock(lines, cursorRow, cursorCol) {
|
|
62
|
+
this.withCork(() => {
|
|
63
|
+
this.moveCursorToTopOfBlock();
|
|
64
|
+
if (this.activeRows > 0) {
|
|
65
|
+
readline_1.default.cursorTo(this.out, 0);
|
|
66
|
+
readline_1.default.clearScreenDown(this.out);
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < lines.length; i++) {
|
|
69
|
+
this.out.write(lines[i]);
|
|
70
|
+
if (i < lines.length - 1)
|
|
71
|
+
this.out.write("\n");
|
|
72
|
+
}
|
|
73
|
+
this.activeRows = lines.length;
|
|
74
|
+
this.cursorRow = Math.max(0, this.activeRows - 1);
|
|
75
|
+
const lastLine = lines[this.cursorRow] || "";
|
|
76
|
+
this.cursorCol = lastLine.length;
|
|
77
|
+
const needsPosition = typeof cursorRow === "number" || typeof cursorCol === "number";
|
|
78
|
+
if (needsPosition) {
|
|
79
|
+
const targetRow = typeof cursorRow === "number"
|
|
80
|
+
? Math.min(Math.max(cursorRow, 0), Math.max(0, this.activeRows - 1))
|
|
81
|
+
: this.cursorRow;
|
|
82
|
+
const targetCol = Math.max(0, cursorCol ?? this.cursorCol);
|
|
83
|
+
this.moveCursorTo(targetRow, targetCol);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
moveCursorTo(lineIndex, column) {
|
|
88
|
+
if (this.activeRows === 0)
|
|
89
|
+
return;
|
|
90
|
+
const safeLine = Math.min(Math.max(lineIndex, 0), Math.max(0, this.activeRows - 1));
|
|
91
|
+
const safeColumn = Math.max(0, column);
|
|
92
|
+
const rowDelta = safeLine - this.cursorRow;
|
|
93
|
+
if (rowDelta !== 0) {
|
|
94
|
+
readline_1.default.moveCursor(this.out, 0, rowDelta);
|
|
95
|
+
}
|
|
96
|
+
readline_1.default.cursorTo(this.out, safeColumn);
|
|
97
|
+
this.cursorRow = safeLine;
|
|
98
|
+
this.cursorCol = safeColumn;
|
|
99
|
+
}
|
|
100
|
+
// When we have printed arbitrary output that is not managed by renderBlock,
|
|
101
|
+
// reset internal line tracking so the next render starts fresh.
|
|
102
|
+
resetBlockTracking() {
|
|
103
|
+
this.activeRows = 0;
|
|
104
|
+
this.cursorRow = 0;
|
|
105
|
+
this.cursorCol = 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
exports.Terminal = Terminal;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ai_suggester_1 = require("./ai-suggester");
|
|
4
|
+
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.");
|
|
7
|
+
}
|
|
8
|
+
const question = "What is the capital of France?";
|
|
9
|
+
const answer = await (0, ai_suggester_1.generateContent)(question);
|
|
10
|
+
console.log(`Q: ${question}`);
|
|
11
|
+
console.log(`A: ${answer.trim()}`);
|
|
12
|
+
}
|
|
13
|
+
main().catch((err) => {
|
|
14
|
+
console.error(err);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "caroushell",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Terminal carousel that suggests commands from history, config, and AI.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "dist/main.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"caroushell": "dist/main.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"dev": "tsx src/main.ts",
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"prepare": "npm run build",
|
|
19
|
+
"start": "node dist/main.js",
|
|
20
|
+
"test:generate": "tsx src/test-generate.ts"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"cli",
|
|
24
|
+
"shell",
|
|
25
|
+
"history",
|
|
26
|
+
"ai"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/ubershmekel/caroushell.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/ubershmekel/caroushell/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/ubershmekel/caroushell#readme",
|
|
36
|
+
"author": "Ubershmekel",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^24.10.0",
|
|
43
|
+
"tsx": "^4.19.2",
|
|
44
|
+
"typescript": "^5.6.3"
|
|
45
|
+
}
|
|
46
|
+
}
|