caroushell 0.1.12 → 0.1.14
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/dist/ai-suggester.js +43 -2
- package/dist/app.js +9 -16
- package/dist/carousel.js +16 -25
- package/dist/file-suggester.js +7 -3
- package/dist/hello-new-user.js +19 -0
- package/dist/history-suggester.js +19 -12
- package/dist/keyboard.js +1 -24
- package/dist/terminal.js +18 -2
- package/package.json +1 -1
package/dist/ai-suggester.js
CHANGED
|
@@ -5,6 +5,27 @@ exports.generateContent = generateContent;
|
|
|
5
5
|
exports.listModels = listModels;
|
|
6
6
|
const logs_1 = require("./logs");
|
|
7
7
|
const config_1 = require("./config");
|
|
8
|
+
function debounceAsync(fn, delayMs) {
|
|
9
|
+
let tim = null;
|
|
10
|
+
let rejectLast = null;
|
|
11
|
+
return (...args) => {
|
|
12
|
+
if (tim)
|
|
13
|
+
clearTimeout(tim);
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
if (rejectLast) {
|
|
16
|
+
rejectLast(new Error("debounced"));
|
|
17
|
+
}
|
|
18
|
+
rejectLast = reject;
|
|
19
|
+
tim = setTimeout(() => {
|
|
20
|
+
tim = null;
|
|
21
|
+
rejectLast = null;
|
|
22
|
+
fn(...args)
|
|
23
|
+
.then(resolve)
|
|
24
|
+
.catch(reject);
|
|
25
|
+
}, delayMs);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
}
|
|
8
29
|
async function generateContent(prompt, options) {
|
|
9
30
|
const apiKey = options?.apiKey ||
|
|
10
31
|
process.env.CAROUSHELL_API_KEY ||
|
|
@@ -100,6 +121,8 @@ async function extractText(json) {
|
|
|
100
121
|
class AISuggester {
|
|
101
122
|
constructor(opts) {
|
|
102
123
|
this.prefix = "🤖";
|
|
124
|
+
this.latestSuggestions = [];
|
|
125
|
+
this.debouncedSuggest = debounceAsync(this.runSuggestNow.bind(this), 350);
|
|
103
126
|
this.apiKey = opts?.apiKey;
|
|
104
127
|
this.apiUrl = opts?.apiUrl;
|
|
105
128
|
this.model = opts?.model;
|
|
@@ -118,11 +141,29 @@ class AISuggester {
|
|
|
118
141
|
descriptionForAi() {
|
|
119
142
|
return "";
|
|
120
143
|
}
|
|
121
|
-
|
|
144
|
+
latest() {
|
|
145
|
+
return this.latestSuggestions;
|
|
146
|
+
}
|
|
147
|
+
async refreshSuggestions(carousel, maxDisplayed) {
|
|
122
148
|
if (!this.apiKey || !this.apiUrl || !this.model) {
|
|
123
149
|
(0, logs_1.logLine)("AI generation skipped: missing API configuration");
|
|
124
|
-
|
|
150
|
+
this.latestSuggestions = [];
|
|
151
|
+
carousel.render();
|
|
152
|
+
return;
|
|
125
153
|
}
|
|
154
|
+
try {
|
|
155
|
+
const suggestions = await this.debouncedSuggest(carousel, maxDisplayed);
|
|
156
|
+
this.latestSuggestions = suggestions.slice(0, maxDisplayed);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
if (err?.message !== "debounced") {
|
|
160
|
+
(0, logs_1.logLine)("ai suggest error: " + err?.message);
|
|
161
|
+
}
|
|
162
|
+
this.latestSuggestions = [];
|
|
163
|
+
}
|
|
164
|
+
carousel.render();
|
|
165
|
+
}
|
|
166
|
+
async runSuggestNow(carousel, maxDisplayed) {
|
|
126
167
|
const descriptions = [];
|
|
127
168
|
for (const suggester of carousel.getSuggesters()) {
|
|
128
169
|
const desc = suggester.descriptionForAi();
|
package/dist/app.js
CHANGED
|
@@ -8,15 +8,6 @@ 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
|
-
function debounce(fn, ms) {
|
|
12
|
-
// Debounce function to limit the rate at which a function can fire
|
|
13
|
-
let t = null;
|
|
14
|
-
return (...args) => {
|
|
15
|
-
if (t)
|
|
16
|
-
clearTimeout(t);
|
|
17
|
-
t = setTimeout(() => fn(...args), ms);
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
11
|
class App {
|
|
21
12
|
constructor() {
|
|
22
13
|
this.usingFileSuggestions = false;
|
|
@@ -32,9 +23,9 @@ class App {
|
|
|
32
23
|
bottomRows: 2,
|
|
33
24
|
terminal: this.terminal,
|
|
34
25
|
});
|
|
35
|
-
this.queueUpdateSuggestions =
|
|
36
|
-
|
|
37
|
-
}
|
|
26
|
+
this.queueUpdateSuggestions = () => {
|
|
27
|
+
void this.carousel.updateSuggestions();
|
|
28
|
+
};
|
|
38
29
|
this.handlers = {
|
|
39
30
|
"ctrl-c": () => {
|
|
40
31
|
if (this.carousel.isPromptRowSelected() && !this.carousel.hasInput()) {
|
|
@@ -142,7 +133,7 @@ class App {
|
|
|
142
133
|
}
|
|
143
134
|
async run() {
|
|
144
135
|
await this.init();
|
|
145
|
-
this.keyboard.
|
|
136
|
+
this.keyboard.enableCapture();
|
|
146
137
|
this.keyboard.on("key", (evt) => {
|
|
147
138
|
void this.handleKey(evt);
|
|
148
139
|
});
|
|
@@ -174,7 +165,8 @@ class App {
|
|
|
174
165
|
this.terminal.renderBlock(lines);
|
|
175
166
|
// Ensure command output starts on the next line
|
|
176
167
|
this.terminal.write("\n");
|
|
177
|
-
this.keyboard.
|
|
168
|
+
this.keyboard.disableCapture();
|
|
169
|
+
this.terminal.disableWrites();
|
|
178
170
|
try {
|
|
179
171
|
const storeInHistory = await (0, spawner_1.runUserCommand)(cmd);
|
|
180
172
|
if (storeInHistory) {
|
|
@@ -182,13 +174,14 @@ class App {
|
|
|
182
174
|
}
|
|
183
175
|
}
|
|
184
176
|
finally {
|
|
185
|
-
this.
|
|
177
|
+
this.terminal.enableWrites();
|
|
178
|
+
this.keyboard.enableCapture();
|
|
186
179
|
}
|
|
187
180
|
}
|
|
188
181
|
exit() {
|
|
189
182
|
// Clear terminal contents before shutting down to leave a clean screen.
|
|
190
183
|
this.terminal.renderBlock([]);
|
|
191
|
-
this.keyboard.
|
|
184
|
+
this.keyboard.disableCapture();
|
|
192
185
|
process.exit(0);
|
|
193
186
|
}
|
|
194
187
|
async tryAutocompleteFile() {
|
package/dist/carousel.js
CHANGED
|
@@ -5,61 +5,49 @@ const logs_1 = require("./logs");
|
|
|
5
5
|
const terminal_1 = require("./terminal");
|
|
6
6
|
class Carousel {
|
|
7
7
|
constructor(opts) {
|
|
8
|
-
this.latestTop = [];
|
|
9
|
-
this.latestBottom = [];
|
|
10
8
|
this.index = 0;
|
|
11
9
|
this.inputBuffer = "";
|
|
12
10
|
this.inputCursor = 0;
|
|
13
|
-
this.emptyRow = "---";
|
|
14
11
|
this.terminal = opts.terminal;
|
|
15
12
|
this.top = opts.top;
|
|
16
13
|
this.bottom = opts.bottom;
|
|
17
14
|
this.topRowCount = opts.topRows;
|
|
18
15
|
this.bottomRowCount = opts.bottomRows;
|
|
19
|
-
this.latestTop = this.createEmptyRows(this.topRowCount);
|
|
20
|
-
this.latestBottom = this.createEmptyRows(this.bottomRowCount);
|
|
21
|
-
}
|
|
22
|
-
createEmptyRows(count) {
|
|
23
|
-
return Array(count).fill(this.emptyRow);
|
|
24
16
|
}
|
|
25
17
|
async updateSuggestions(input) {
|
|
26
18
|
if (typeof input === "string") {
|
|
27
19
|
this.setInputBuffer(input);
|
|
28
20
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
void topPromise.then((r) => {
|
|
32
|
-
this.latestTop = r;
|
|
33
|
-
this.render();
|
|
34
|
-
});
|
|
35
|
-
void bottomPromise.then((r) => {
|
|
36
|
-
this.latestBottom = r;
|
|
37
|
-
this.render();
|
|
38
|
-
});
|
|
21
|
+
void this.top.refreshSuggestions(this, this.topRowCount);
|
|
22
|
+
void this.bottom.refreshSuggestions(this, this.bottomRowCount);
|
|
39
23
|
}
|
|
40
24
|
up() {
|
|
41
25
|
this.index += 1;
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
const topLength = this.top.latest().length;
|
|
27
|
+
if (this.index >= topLength) {
|
|
28
|
+
this.index = topLength;
|
|
44
29
|
}
|
|
45
30
|
}
|
|
46
31
|
down() {
|
|
47
32
|
this.index -= 1;
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
const bottomLength = this.bottom.latest().length;
|
|
34
|
+
if (-this.index >= bottomLength) {
|
|
35
|
+
this.index = -bottomLength;
|
|
50
36
|
}
|
|
51
37
|
}
|
|
52
38
|
getRow(rowIndex) {
|
|
39
|
+
const latestTop = this.top.latest();
|
|
40
|
+
const latestBottom = this.bottom.latest();
|
|
53
41
|
if (rowIndex < 0) {
|
|
54
42
|
const bottomIndex = -rowIndex - 1;
|
|
55
|
-
return
|
|
43
|
+
return latestBottom[bottomIndex] || "";
|
|
56
44
|
}
|
|
57
45
|
if (rowIndex === 0) {
|
|
58
46
|
return this.inputBuffer;
|
|
59
47
|
}
|
|
60
48
|
if (rowIndex > 0) {
|
|
61
49
|
const topIndex = rowIndex - 1;
|
|
62
|
-
return
|
|
50
|
+
return latestTop[topIndex] || "";
|
|
63
51
|
}
|
|
64
52
|
return "";
|
|
65
53
|
}
|
|
@@ -259,7 +247,10 @@ class Carousel {
|
|
|
259
247
|
if (this.top === suggester)
|
|
260
248
|
return;
|
|
261
249
|
this.top = suggester;
|
|
262
|
-
this.
|
|
250
|
+
if (this.index > 0) {
|
|
251
|
+
const topLength = this.top.latest().length;
|
|
252
|
+
this.index = Math.min(this.index, topLength);
|
|
253
|
+
}
|
|
263
254
|
}
|
|
264
255
|
getSuggesters() {
|
|
265
256
|
return [this.top, this.bottom];
|
package/dist/file-suggester.js
CHANGED
|
@@ -12,6 +12,7 @@ class FileSuggester {
|
|
|
12
12
|
constructor() {
|
|
13
13
|
this.prefix = "📂";
|
|
14
14
|
this.files = [];
|
|
15
|
+
this.latestSuggestions = [];
|
|
15
16
|
}
|
|
16
17
|
async init() {
|
|
17
18
|
await this.refreshFiles();
|
|
@@ -75,10 +76,13 @@ class FileSuggester {
|
|
|
75
76
|
const converted = dirDisplay.replace(/\//g, path_1.default.sep);
|
|
76
77
|
return path_1.default.resolve(process.cwd(), converted);
|
|
77
78
|
}
|
|
78
|
-
|
|
79
|
+
latest() {
|
|
80
|
+
return this.latestSuggestions;
|
|
81
|
+
}
|
|
82
|
+
async refreshSuggestions(carousel, maxDisplayed) {
|
|
79
83
|
const { prefix } = carousel.getWordInfoAtCursor();
|
|
80
|
-
|
|
81
|
-
|
|
84
|
+
this.latestSuggestions = await this.getMatchingFiles(prefix);
|
|
85
|
+
carousel.render();
|
|
82
86
|
}
|
|
83
87
|
async findUniqueMatch(prefix) {
|
|
84
88
|
const normalized = prefix.trim();
|
package/dist/hello-new-user.js
CHANGED
|
@@ -41,6 +41,7 @@ const fs_1 = require("fs");
|
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
42
|
const readline_1 = __importDefault(require("readline"));
|
|
43
43
|
const ai_suggester_1 = require("./ai-suggester");
|
|
44
|
+
const preferredModels = ["gemini-2.5-flash-lite", "gpt-4o-mini"];
|
|
44
45
|
function isInteractive() {
|
|
45
46
|
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
46
47
|
}
|
|
@@ -49,6 +50,17 @@ async function prompt(question, rl) {
|
|
|
49
50
|
rl.question(question, (answer) => resolve(answer));
|
|
50
51
|
});
|
|
51
52
|
}
|
|
53
|
+
function findShortestMatches(models, preferredList) {
|
|
54
|
+
const matches = [];
|
|
55
|
+
for (const pref of preferredList) {
|
|
56
|
+
const hits = models.filter((modelId) => modelId.includes(pref));
|
|
57
|
+
if (hits.length) {
|
|
58
|
+
const shortest = hits.reduce((best, candidate) => candidate.length < best.length ? candidate : best);
|
|
59
|
+
matches.push(shortest);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [...new Set(matches)];
|
|
63
|
+
}
|
|
52
64
|
async function runHelloNewUserFlow(configPath) {
|
|
53
65
|
if (!isInteractive()) {
|
|
54
66
|
throw new Error(`Missing config at ${configPath} and no interactive terminal is available.\n` +
|
|
@@ -92,10 +104,17 @@ async function runHelloNewUserFlow(configPath) {
|
|
|
92
104
|
}
|
|
93
105
|
const models = await (0, ai_suggester_1.listModels)(apiUrl, apiKey);
|
|
94
106
|
if (models.length > 0) {
|
|
107
|
+
const preferred = findShortestMatches(models, preferredModels);
|
|
95
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.");
|
|
96
109
|
for (const model of models.slice(0, 5)) {
|
|
97
110
|
console.log(` - ${model}`);
|
|
98
111
|
}
|
|
112
|
+
if (preferred.length) {
|
|
113
|
+
console.log("Recommended models from your provider:");
|
|
114
|
+
for (const model of preferred) {
|
|
115
|
+
console.log(` - ${model}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
99
118
|
}
|
|
100
119
|
let model = "";
|
|
101
120
|
while (!model) {
|
|
@@ -44,24 +44,31 @@ class HistorySuggester {
|
|
|
44
44
|
.catch(() => { });
|
|
45
45
|
await fs_1.promises.appendFile(this.filePath, this.serializeHistoryEntry(command), "utf8");
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
latest() {
|
|
48
|
+
return this.items;
|
|
49
|
+
}
|
|
50
|
+
async refreshSuggestions(carousel, maxDisplayed) {
|
|
48
51
|
const input = carousel.getCurrentRow();
|
|
52
|
+
let results;
|
|
49
53
|
if (!input) {
|
|
50
54
|
// this.items 0 index is newest
|
|
51
|
-
|
|
55
|
+
results = this.items;
|
|
52
56
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
seen.
|
|
61
|
-
|
|
57
|
+
else {
|
|
58
|
+
const q = input.toLowerCase();
|
|
59
|
+
const matched = [];
|
|
60
|
+
// iterate from newest to oldest so we skip older duplicates
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
63
|
+
const it = this.items[i];
|
|
64
|
+
if (it.toLowerCase().includes(q) && !seen.has(it)) {
|
|
65
|
+
seen.add(it);
|
|
66
|
+
matched.push(it);
|
|
67
|
+
}
|
|
62
68
|
}
|
|
69
|
+
results = matched;
|
|
63
70
|
}
|
|
64
|
-
|
|
71
|
+
carousel.render();
|
|
65
72
|
}
|
|
66
73
|
descriptionForAi() {
|
|
67
74
|
const lines = [];
|
package/dist/keyboard.js
CHANGED
|
@@ -50,38 +50,15 @@ for (const seq of Object.keys(KEYMAP)) {
|
|
|
50
50
|
class Keyboard extends events_1.EventEmitter {
|
|
51
51
|
constructor() {
|
|
52
52
|
super(...arguments);
|
|
53
|
-
this.active = false;
|
|
54
53
|
this.capturing = false;
|
|
55
54
|
this.buffer = '';
|
|
56
55
|
this.stdin = process.stdin;
|
|
57
56
|
this.onData = (data) => this.handleData(data);
|
|
58
57
|
}
|
|
59
|
-
start() {
|
|
60
|
-
if (this.active)
|
|
61
|
-
return;
|
|
62
|
-
this.active = true;
|
|
63
|
-
this.stdin.setEncoding('utf8');
|
|
64
|
-
this.enableCapture();
|
|
65
|
-
}
|
|
66
|
-
stop() {
|
|
67
|
-
if (!this.active)
|
|
68
|
-
return;
|
|
69
|
-
this.active = false;
|
|
70
|
-
this.disableCapture();
|
|
71
|
-
}
|
|
72
|
-
pause() {
|
|
73
|
-
if (!this.active)
|
|
74
|
-
return;
|
|
75
|
-
this.disableCapture();
|
|
76
|
-
}
|
|
77
|
-
resume() {
|
|
78
|
-
if (!this.active)
|
|
79
|
-
return;
|
|
80
|
-
this.enableCapture();
|
|
81
|
-
}
|
|
82
58
|
enableCapture() {
|
|
83
59
|
if (this.capturing)
|
|
84
60
|
return;
|
|
61
|
+
this.stdin.setEncoding('utf8');
|
|
85
62
|
if (this.stdin.isTTY)
|
|
86
63
|
this.stdin.setRawMode(true);
|
|
87
64
|
this.stdin.on('data', this.onData);
|
package/dist/terminal.js
CHANGED
|
@@ -21,6 +21,16 @@ class Terminal {
|
|
|
21
21
|
this.activeRows = 0;
|
|
22
22
|
this.cursorRow = 0;
|
|
23
23
|
this.cursorCol = 0;
|
|
24
|
+
this.writesDisabled = false;
|
|
25
|
+
}
|
|
26
|
+
disableWrites() {
|
|
27
|
+
this.writesDisabled = true;
|
|
28
|
+
}
|
|
29
|
+
enableWrites() {
|
|
30
|
+
this.writesDisabled = false;
|
|
31
|
+
}
|
|
32
|
+
canWrite() {
|
|
33
|
+
return !this.writesDisabled;
|
|
24
34
|
}
|
|
25
35
|
moveCursorToTopOfBlock() {
|
|
26
36
|
if (this.activeRows === 0)
|
|
@@ -51,6 +61,8 @@ class Terminal {
|
|
|
51
61
|
}
|
|
52
62
|
}
|
|
53
63
|
write(text) {
|
|
64
|
+
if (!this.canWrite())
|
|
65
|
+
return;
|
|
54
66
|
this.out.write(text);
|
|
55
67
|
}
|
|
56
68
|
hideCursor() {
|
|
@@ -61,6 +73,8 @@ class Terminal {
|
|
|
61
73
|
}
|
|
62
74
|
// Render a block of lines by clearing previous block (if any) and writing fresh
|
|
63
75
|
renderBlock(lines, cursorRow, cursorCol) {
|
|
76
|
+
if (!this.canWrite())
|
|
77
|
+
return;
|
|
64
78
|
this.withCork(() => {
|
|
65
79
|
this.moveCursorToTopOfBlock();
|
|
66
80
|
if (this.activeRows > 0) {
|
|
@@ -68,9 +82,9 @@ class Terminal {
|
|
|
68
82
|
readline_1.default.clearScreenDown(this.out);
|
|
69
83
|
}
|
|
70
84
|
for (let i = 0; i < lines.length; i++) {
|
|
71
|
-
this.
|
|
85
|
+
this.write(lines[i]);
|
|
72
86
|
if (i < lines.length - 1)
|
|
73
|
-
this.
|
|
87
|
+
this.write("\n");
|
|
74
88
|
}
|
|
75
89
|
this.activeRows = lines.length;
|
|
76
90
|
this.cursorRow = Math.max(0, this.activeRows - 1);
|
|
@@ -87,6 +101,8 @@ class Terminal {
|
|
|
87
101
|
});
|
|
88
102
|
}
|
|
89
103
|
moveCursorTo(lineIndex, column) {
|
|
104
|
+
if (!this.canWrite())
|
|
105
|
+
return;
|
|
90
106
|
if (this.activeRows === 0)
|
|
91
107
|
return;
|
|
92
108
|
const safeLine = Math.min(Math.max(lineIndex, 0), Math.max(0, this.activeRows - 1));
|