heyeric 1.2.0 → 1.4.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/chunk-3CJG52QJ.js +77 -0
- package/dist/index.js +110 -22
- package/dist/sanitize.js +6 -0
- package/dist/update-check.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
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// src/update-check.ts
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { homedir } from "os";
|
|
6
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
function getCurrentVersion() {
|
|
8
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
9
|
+
return pkg.version;
|
|
10
|
+
}
|
|
11
|
+
function isNewer(latest, current) {
|
|
12
|
+
const a = latest.split(".").map(Number);
|
|
13
|
+
const b = current.split(".").map(Number);
|
|
14
|
+
for (let i = 0; i < 3; i++) {
|
|
15
|
+
if ((a[i] ?? 0) > (b[i] ?? 0)) return true;
|
|
16
|
+
if ((a[i] ?? 0) < (b[i] ?? 0)) return false;
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
var CACHE_DIR = join(homedir(), ".heyeric");
|
|
21
|
+
var CACHE_FILE = join(CACHE_DIR, "last-update-check");
|
|
22
|
+
var ONE_DAY_MS = 864e5;
|
|
23
|
+
function readCache() {
|
|
24
|
+
try {
|
|
25
|
+
const raw = readFileSync(CACHE_FILE, "utf-8").trim();
|
|
26
|
+
const sep = raw.indexOf(":");
|
|
27
|
+
if (sep === -1) return null;
|
|
28
|
+
const version = raw.slice(sep + 1);
|
|
29
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) return null;
|
|
30
|
+
return { timestamp: Number(raw.slice(0, sep)), version };
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function writeCache(version) {
|
|
36
|
+
try {
|
|
37
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
38
|
+
writeFileSync(CACHE_FILE, `${Date.now()}:${version}`);
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function sanitizeVersion(v) {
|
|
43
|
+
const match = v.match(/^\d+\.\d+\.\d+/);
|
|
44
|
+
return match ? match[0] : null;
|
|
45
|
+
}
|
|
46
|
+
async function fetchLatestVersion() {
|
|
47
|
+
const resp = await fetch("https://registry.npmjs.org/heyeric/latest", {
|
|
48
|
+
signal: AbortSignal.timeout(3e3)
|
|
49
|
+
});
|
|
50
|
+
if (!resp.ok) return null;
|
|
51
|
+
const data = await resp.json();
|
|
52
|
+
return data.version ? sanitizeVersion(data.version) : null;
|
|
53
|
+
}
|
|
54
|
+
async function checkForUpdate() {
|
|
55
|
+
try {
|
|
56
|
+
const current = getCurrentVersion();
|
|
57
|
+
const cache = readCache();
|
|
58
|
+
let latest = null;
|
|
59
|
+
if (cache && Date.now() - cache.timestamp < ONE_DAY_MS) {
|
|
60
|
+
latest = cache.version;
|
|
61
|
+
} else {
|
|
62
|
+
latest = await fetchLatestVersion();
|
|
63
|
+
if (latest) writeCache(latest);
|
|
64
|
+
}
|
|
65
|
+
if (!latest || !isNewer(latest, current)) return null;
|
|
66
|
+
return `
|
|
67
|
+
\x1B[33mUpdate available: ${current} \u2192 ${latest}\x1B[0m
|
|
68
|
+
\x1B[2mRun: npm i -g heyeric\x1B[0m
|
|
69
|
+
`;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
checkForUpdate
|
|
77
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
sanitize
|
|
4
|
+
} from "./chunk-2CBZLAF5.js";
|
|
5
|
+
import {
|
|
6
|
+
checkForUpdate
|
|
7
|
+
} from "./chunk-3CJG52QJ.js";
|
|
2
8
|
|
|
3
9
|
// src/index.ts
|
|
4
10
|
import { execSync, spawn } from "child_process";
|
|
@@ -6,8 +12,10 @@ import { createInterface } from "readline";
|
|
|
6
12
|
import { readFileSync } from "fs";
|
|
7
13
|
import { platform } from "os";
|
|
8
14
|
import { basename } from "path";
|
|
15
|
+
import cliSpinners from "cli-spinners";
|
|
9
16
|
function die(msg) {
|
|
10
|
-
process.stderr.write(`
|
|
17
|
+
process.stderr.write(`
|
|
18
|
+
eric: ${msg}
|
|
11
19
|
`);
|
|
12
20
|
process.exit(1);
|
|
13
21
|
}
|
|
@@ -51,7 +59,15 @@ var lsOutput = getLs();
|
|
|
51
59
|
var history = getHistory();
|
|
52
60
|
var systemPrompt = `You are a bash command generator.
|
|
53
61
|
Convert the user's natural-language description into a single bash command.
|
|
54
|
-
|
|
62
|
+
Prefer simple, well-known commands over complex one-liners when possible.
|
|
63
|
+
|
|
64
|
+
If the user asks HOW to do something, generate the actual command to do it.
|
|
65
|
+
For example: "how to run spring boot" \u2192 ./mvnw spring-boot:run
|
|
66
|
+
|
|
67
|
+
If the query truly cannot be answered with a command (e.g., "what is the meaning of life"),
|
|
68
|
+
respond with exactly: NONE
|
|
69
|
+
|
|
70
|
+
Return ONLY the command (or NONE). No explanation. No markdown. No backticks. No newlines.
|
|
55
71
|
|
|
56
72
|
OS: ${os}
|
|
57
73
|
Shell: ${shell}
|
|
@@ -74,38 +90,93 @@ Recent command history:
|
|
|
74
90
|
}
|
|
75
91
|
systemPrompt += `
|
|
76
92
|
Description to convert:`;
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
function startSpinner() {
|
|
94
|
+
const spinner = cliSpinners.dots;
|
|
95
|
+
let i = 0;
|
|
96
|
+
process.stderr.write("\x1B[?25l");
|
|
97
|
+
const timer = setInterval(() => {
|
|
98
|
+
const frame = spinner.frames[i++ % spinner.frames.length];
|
|
99
|
+
process.stderr.write(`\r\x1B[2m${frame}\x1B[0m`);
|
|
100
|
+
}, spinner.interval);
|
|
101
|
+
return () => {
|
|
102
|
+
clearInterval(timer);
|
|
103
|
+
process.stderr.write("\r\x1B[K");
|
|
104
|
+
process.stderr.write("\x1B[?25h");
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
async function askLLM(messages, isRefinement = false) {
|
|
108
|
+
if (!isRefinement) {
|
|
109
|
+
const stopSpinner2 = startSpinner();
|
|
110
|
+
try {
|
|
111
|
+
return await streamLLM(messages, stopSpinner2);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
stopSpinner2();
|
|
114
|
+
throw e;
|
|
83
115
|
}
|
|
84
116
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
117
|
+
const stopSpinner = startSpinner();
|
|
118
|
+
try {
|
|
119
|
+
return await streamLLM(messages, stopSpinner);
|
|
120
|
+
} catch (e) {
|
|
121
|
+
stopSpinner();
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
90
124
|
}
|
|
91
|
-
async function
|
|
125
|
+
async function streamLLM(messages, stopSpinner) {
|
|
92
126
|
const resp = await fetch(apiUrl, {
|
|
93
127
|
method: "POST",
|
|
94
128
|
headers: {
|
|
95
129
|
"Content-Type": "application/json",
|
|
96
130
|
Authorization: `Bearer ${apiKey}`
|
|
97
131
|
},
|
|
98
|
-
body: JSON.stringify({ model, messages, stream:
|
|
132
|
+
body: JSON.stringify({ model, messages, stream: true, max_tokens: 200 })
|
|
99
133
|
});
|
|
100
134
|
if (!resp.ok) {
|
|
135
|
+
stopSpinner();
|
|
101
136
|
const body = await resp.text();
|
|
102
137
|
die(`API error ${resp.status}: ${body}`);
|
|
103
138
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
die("No
|
|
139
|
+
if (!resp.body) {
|
|
140
|
+
stopSpinner();
|
|
141
|
+
die("No response body");
|
|
107
142
|
}
|
|
108
|
-
|
|
143
|
+
let raw = "";
|
|
144
|
+
let spinnerStopped = false;
|
|
145
|
+
const reader = resp.body.getReader();
|
|
146
|
+
const decoder = new TextDecoder();
|
|
147
|
+
let buffer = "";
|
|
148
|
+
while (true) {
|
|
149
|
+
const { done, value } = await reader.read();
|
|
150
|
+
if (done) break;
|
|
151
|
+
buffer += decoder.decode(value, { stream: true });
|
|
152
|
+
const lines = buffer.split("\n");
|
|
153
|
+
buffer = lines.pop() ?? "";
|
|
154
|
+
for (const line of lines) {
|
|
155
|
+
if (!line.startsWith("data: ")) continue;
|
|
156
|
+
const data = line.slice(6).trim();
|
|
157
|
+
if (data === "[DONE]") break;
|
|
158
|
+
try {
|
|
159
|
+
const chunk = JSON.parse(data);
|
|
160
|
+
const token = chunk.choices?.[0]?.delta?.content;
|
|
161
|
+
if (!token) continue;
|
|
162
|
+
if (!spinnerStopped) {
|
|
163
|
+
stopSpinner();
|
|
164
|
+
spinnerStopped = true;
|
|
165
|
+
process.stderr.write(`\x1B[1;32m\u276F\x1B[0m \x1B[1m`);
|
|
166
|
+
}
|
|
167
|
+
process.stderr.write(token);
|
|
168
|
+
raw += token;
|
|
169
|
+
} catch {
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (spinnerStopped) {
|
|
174
|
+
process.stderr.write(`\x1B[0m
|
|
175
|
+
`);
|
|
176
|
+
} else {
|
|
177
|
+
stopSpinner();
|
|
178
|
+
}
|
|
179
|
+
return sanitize(raw);
|
|
109
180
|
}
|
|
110
181
|
function prompt() {
|
|
111
182
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -117,27 +188,44 @@ function prompt() {
|
|
|
117
188
|
});
|
|
118
189
|
}
|
|
119
190
|
async function main() {
|
|
191
|
+
const updatePromise = checkForUpdate();
|
|
120
192
|
const messages = [
|
|
121
193
|
{ role: "system", content: systemPrompt },
|
|
122
194
|
{ role: "user", content: query }
|
|
123
195
|
];
|
|
124
196
|
let command = await askLLM(messages);
|
|
125
197
|
if (!command) die("API returned empty command");
|
|
198
|
+
if (command === "NONE") {
|
|
199
|
+
process.stderr.write(`\r\x1B[K`);
|
|
200
|
+
process.stderr.write(`\x1B[2meric: I can only help with commands. Try rephrasing as an action.\x1B[0m
|
|
201
|
+
`);
|
|
202
|
+
const updateMsg = await updatePromise;
|
|
203
|
+
if (updateMsg) process.stderr.write(updateMsg);
|
|
204
|
+
process.exit(0);
|
|
205
|
+
}
|
|
126
206
|
messages.push({ role: "assistant", content: command });
|
|
127
207
|
while (true) {
|
|
128
|
-
process.stderr.write(`\x1B[1;32m\u276F\x1B[0m \x1B[1m${command}\x1B[0m
|
|
129
|
-
`);
|
|
130
208
|
process.stderr.write(`\x1B[2mEnter to run \xB7 Type to refine \xB7 Ctrl+C to cancel\x1B[0m
|
|
131
209
|
`);
|
|
132
210
|
const answer = await prompt();
|
|
133
211
|
if (!answer) {
|
|
212
|
+
const updateMsg = await updatePromise;
|
|
213
|
+
if (updateMsg) process.stderr.write(updateMsg);
|
|
134
214
|
const child = spawn(command, { shell: true, stdio: "inherit" });
|
|
135
215
|
child.on("close", (code) => process.exit(code ?? 0));
|
|
136
216
|
return;
|
|
137
217
|
}
|
|
138
218
|
messages.push({ role: "user", content: answer });
|
|
139
|
-
command = await askLLM(messages);
|
|
219
|
+
command = await askLLM(messages, true);
|
|
140
220
|
if (!command) die("API returned empty command");
|
|
221
|
+
if (command === "NONE") {
|
|
222
|
+
process.stderr.write(`\r\x1B[K`);
|
|
223
|
+
process.stderr.write(`\x1B[2meric: I can only help with commands. Try rephrasing as an action.\x1B[0m
|
|
224
|
+
`);
|
|
225
|
+
const updateMsg = await updatePromise;
|
|
226
|
+
if (updateMsg) process.stderr.write(updateMsg);
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
141
229
|
messages.push({ role: "assistant", content: command });
|
|
142
230
|
}
|
|
143
231
|
}
|
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.4.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 src/update-check.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
|
}
|