caroushell 0.1.13 → 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 +3 -12
- 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/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()) {
|
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 = [];
|