heyeric 1.1.0 → 1.3.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/dist/chunk-2CBZLAF5.js +19 -0
- package/dist/index.js +104 -40
- package/dist/sanitize.js +6 -0
- package/package.json +7 -3
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// src/sanitize.ts
|
|
2
|
+
function sanitize(raw) {
|
|
3
|
+
let command = raw.trim();
|
|
4
|
+
for (const prefix of ["```bash", "```sh", "```"]) {
|
|
5
|
+
if (command.startsWith(prefix)) {
|
|
6
|
+
command = command.slice(prefix.length);
|
|
7
|
+
break;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
command = command.trim();
|
|
11
|
+
command = command.split("\n")[0];
|
|
12
|
+
command = command.trim().replace(/```$/, "");
|
|
13
|
+
command = command.replace(/^`+|`+$/g, "");
|
|
14
|
+
return command.trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
sanitize
|
|
19
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
sanitize
|
|
4
|
+
} from "./chunk-2CBZLAF5.js";
|
|
2
5
|
|
|
3
6
|
// src/index.ts
|
|
4
7
|
import { execSync, spawn } from "child_process";
|
|
@@ -6,8 +9,10 @@ import { createInterface } from "readline";
|
|
|
6
9
|
import { readFileSync } from "fs";
|
|
7
10
|
import { platform } from "os";
|
|
8
11
|
import { basename } from "path";
|
|
12
|
+
import cliSpinners from "cli-spinners";
|
|
9
13
|
function die(msg) {
|
|
10
|
-
process.stderr.write(`
|
|
14
|
+
process.stderr.write(`
|
|
15
|
+
eric: ${msg}
|
|
11
16
|
`);
|
|
12
17
|
process.exit(1);
|
|
13
18
|
}
|
|
@@ -51,6 +56,7 @@ var lsOutput = getLs();
|
|
|
51
56
|
var history = getHistory();
|
|
52
57
|
var systemPrompt = `You are a bash command generator.
|
|
53
58
|
Convert the user's natural-language description into a single bash command.
|
|
59
|
+
Prefer simple, well-known commands over complex one-liners when possible.
|
|
54
60
|
Return ONLY the command. No explanation. No markdown. No backticks. No newlines.
|
|
55
61
|
|
|
56
62
|
OS: ${os}
|
|
@@ -74,67 +80,125 @@ Recent command history:
|
|
|
74
80
|
}
|
|
75
81
|
systemPrompt += `
|
|
76
82
|
Description to convert:`;
|
|
77
|
-
|
|
83
|
+
function startSpinner() {
|
|
84
|
+
const spinner = cliSpinners.dots;
|
|
85
|
+
let i = 0;
|
|
86
|
+
process.stderr.write("\x1B[?25l");
|
|
87
|
+
const timer = setInterval(() => {
|
|
88
|
+
const frame = spinner.frames[i++ % spinner.frames.length];
|
|
89
|
+
process.stderr.write(`\r\x1B[2m${frame}\x1B[0m`);
|
|
90
|
+
}, spinner.interval);
|
|
91
|
+
return () => {
|
|
92
|
+
clearInterval(timer);
|
|
93
|
+
process.stderr.write("\r\x1B[K");
|
|
94
|
+
process.stderr.write("\x1B[?25h");
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function askLLM(messages, isRefinement = false) {
|
|
98
|
+
if (!isRefinement) {
|
|
99
|
+
const stopSpinner2 = startSpinner();
|
|
100
|
+
try {
|
|
101
|
+
return await streamLLM(messages, stopSpinner2);
|
|
102
|
+
} catch (e) {
|
|
103
|
+
stopSpinner2();
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const stopSpinner = startSpinner();
|
|
108
|
+
try {
|
|
109
|
+
return await streamLLM(messages, stopSpinner);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
stopSpinner();
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function streamLLM(messages, stopSpinner) {
|
|
78
116
|
const resp = await fetch(apiUrl, {
|
|
79
117
|
method: "POST",
|
|
80
118
|
headers: {
|
|
81
119
|
"Content-Type": "application/json",
|
|
82
120
|
Authorization: `Bearer ${apiKey}`
|
|
83
121
|
},
|
|
84
|
-
body: JSON.stringify({
|
|
85
|
-
model,
|
|
86
|
-
messages: [
|
|
87
|
-
{ role: "system", content: systemPrompt },
|
|
88
|
-
{ role: "user", content: query }
|
|
89
|
-
],
|
|
90
|
-
stream: false,
|
|
91
|
-
max_tokens: 200
|
|
92
|
-
})
|
|
122
|
+
body: JSON.stringify({ model, messages, stream: true, max_tokens: 200 })
|
|
93
123
|
});
|
|
94
124
|
if (!resp.ok) {
|
|
125
|
+
stopSpinner();
|
|
95
126
|
const body = await resp.text();
|
|
96
127
|
die(`API error ${resp.status}: ${body}`);
|
|
97
128
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
die("No
|
|
129
|
+
if (!resp.body) {
|
|
130
|
+
stopSpinner();
|
|
131
|
+
die("No response body");
|
|
101
132
|
}
|
|
102
|
-
let
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
let raw = "";
|
|
134
|
+
let spinnerStopped = false;
|
|
135
|
+
const reader = resp.body.getReader();
|
|
136
|
+
const decoder = new TextDecoder();
|
|
137
|
+
let buffer = "";
|
|
138
|
+
while (true) {
|
|
139
|
+
const { done, value } = await reader.read();
|
|
140
|
+
if (done) break;
|
|
141
|
+
buffer += decoder.decode(value, { stream: true });
|
|
142
|
+
const lines = buffer.split("\n");
|
|
143
|
+
buffer = lines.pop() ?? "";
|
|
144
|
+
for (const line of lines) {
|
|
145
|
+
if (!line.startsWith("data: ")) continue;
|
|
146
|
+
const data = line.slice(6).trim();
|
|
147
|
+
if (data === "[DONE]") break;
|
|
148
|
+
try {
|
|
149
|
+
const chunk = JSON.parse(data);
|
|
150
|
+
const token = chunk.choices?.[0]?.delta?.content;
|
|
151
|
+
if (!token) continue;
|
|
152
|
+
if (!spinnerStopped) {
|
|
153
|
+
stopSpinner();
|
|
154
|
+
spinnerStopped = true;
|
|
155
|
+
process.stderr.write(`\x1B[1;32m\u276F\x1B[0m \x1B[1m`);
|
|
156
|
+
}
|
|
157
|
+
process.stderr.write(token);
|
|
158
|
+
raw += token;
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
107
161
|
}
|
|
108
162
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
command = command.trim().replace(/```$/, "");
|
|
112
|
-
command = command.replace(/^`+|`+$/g, "");
|
|
113
|
-
command = command.trim();
|
|
114
|
-
if (!command) die("API returned empty command");
|
|
115
|
-
process.stderr.write(`\x1B[1;32m\u276F\x1B[0m \x1B[1m${command}\x1B[0m
|
|
116
|
-
`);
|
|
117
|
-
process.stderr.write(`\x1B[2mEnter to run \xB7 Type to refine \xB7 Ctrl+C to cancel\x1B[0m
|
|
163
|
+
if (spinnerStopped) {
|
|
164
|
+
process.stderr.write(`\x1B[0m
|
|
118
165
|
`);
|
|
166
|
+
} else {
|
|
167
|
+
stopSpinner();
|
|
168
|
+
}
|
|
169
|
+
return sanitize(raw);
|
|
170
|
+
}
|
|
171
|
+
function prompt() {
|
|
119
172
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
120
|
-
|
|
173
|
+
return new Promise((resolve) => {
|
|
121
174
|
rl.question("\x1B[2m\u203A \x1B[0m", (input) => {
|
|
122
175
|
rl.close();
|
|
123
176
|
resolve(input.trim());
|
|
124
177
|
});
|
|
125
178
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
179
|
+
}
|
|
180
|
+
async function main() {
|
|
181
|
+
const messages = [
|
|
182
|
+
{ role: "system", content: systemPrompt },
|
|
183
|
+
{ role: "user", content: query }
|
|
184
|
+
];
|
|
185
|
+
let command = await askLLM(messages);
|
|
186
|
+
if (!command) die("API returned empty command");
|
|
187
|
+
messages.push({ role: "assistant", content: command });
|
|
188
|
+
while (true) {
|
|
189
|
+
process.stderr.write(`\x1B[2mEnter to run \xB7 Type to refine \xB7 Ctrl+C to cancel\x1B[0m
|
|
190
|
+
`);
|
|
191
|
+
const answer = await prompt();
|
|
192
|
+
if (!answer) {
|
|
193
|
+
const child = spawn(command, { shell: true, stdio: "inherit" });
|
|
194
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
messages.push({ role: "user", content: answer });
|
|
198
|
+
command = await askLLM(messages, true);
|
|
199
|
+
if (!command) die("API returned empty command");
|
|
200
|
+
messages.push({ role: "assistant", content: command });
|
|
133
201
|
}
|
|
134
|
-
const child = spawn(command, { shell: true, stdio: "inherit" });
|
|
135
|
-
child.on("close", (code) => {
|
|
136
|
-
process.exit(code ?? 0);
|
|
137
|
-
});
|
|
138
202
|
}
|
|
139
203
|
main().catch((err) => {
|
|
140
204
|
die(err.message);
|
package/dist/sanitize.js
ADDED
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "heyeric",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Natural language to bash commands via LLM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"eric": "./dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"build": "tsup src/index.ts --format esm --clean",
|
|
11
|
-
"dev": "tsx src/index.ts"
|
|
10
|
+
"build": "tsup src/index.ts src/sanitize.ts --format esm --clean",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"test": "tsx --test src/index.test.ts"
|
|
12
13
|
},
|
|
13
14
|
"files": [
|
|
14
15
|
"dist"
|
|
@@ -17,5 +18,8 @@
|
|
|
17
18
|
"tsup": "^8",
|
|
18
19
|
"tsx": "^4",
|
|
19
20
|
"typescript": "^5"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"cli-spinners": "^3.4.0"
|
|
20
24
|
}
|
|
21
25
|
}
|