heyeric 1.3.0 → 1.5.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-3CJG52QJ.js +77 -0
- package/dist/index.js +72 -6
- package/dist/update-check.js +6 -0
- package/package.json +2 -2
|
@@ -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
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
import {
|
|
3
3
|
sanitize
|
|
4
4
|
} from "./chunk-2CBZLAF5.js";
|
|
5
|
+
import {
|
|
6
|
+
checkForUpdate
|
|
7
|
+
} from "./chunk-3CJG52QJ.js";
|
|
5
8
|
|
|
6
9
|
// src/index.ts
|
|
7
10
|
import { execSync, spawn } from "child_process";
|
|
8
11
|
import { createInterface } from "readline";
|
|
9
|
-
import { readFileSync } from "fs";
|
|
10
|
-
import { platform } from "os";
|
|
11
|
-
import { basename } from "path";
|
|
12
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
13
|
+
import { platform, tmpdir } from "os";
|
|
14
|
+
import { basename, join } from "path";
|
|
12
15
|
import cliSpinners from "cli-spinners";
|
|
13
16
|
function die(msg) {
|
|
14
17
|
process.stderr.write(`
|
|
@@ -52,12 +55,28 @@ function getHistory() {
|
|
|
52
55
|
return [];
|
|
53
56
|
}
|
|
54
57
|
}
|
|
58
|
+
var LAST_OUTPUT_FILE = join(tmpdir(), "eric-last-output");
|
|
59
|
+
function getLastOutput() {
|
|
60
|
+
try {
|
|
61
|
+
return readFileSync(LAST_OUTPUT_FILE, "utf-8").trim();
|
|
62
|
+
} catch {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
55
66
|
var lsOutput = getLs();
|
|
56
67
|
var history = getHistory();
|
|
68
|
+
var lastOutput = getLastOutput();
|
|
57
69
|
var systemPrompt = `You are a bash command generator.
|
|
58
70
|
Convert the user's natural-language description into a single bash command.
|
|
59
71
|
Prefer simple, well-known commands over complex one-liners when possible.
|
|
60
|
-
|
|
72
|
+
|
|
73
|
+
If the user asks HOW to do something, generate the actual command to do it.
|
|
74
|
+
For example: "how to run spring boot" \u2192 ./mvnw spring-boot:run
|
|
75
|
+
|
|
76
|
+
If the query truly cannot be answered with a command (e.g., "what is the meaning of life"),
|
|
77
|
+
respond with exactly: NONE
|
|
78
|
+
|
|
79
|
+
Return ONLY the command (or NONE). No explanation. No markdown. No backticks. No newlines.
|
|
61
80
|
|
|
62
81
|
OS: ${os}
|
|
63
82
|
Shell: ${shell}
|
|
@@ -78,6 +97,12 @@ Recent command history:
|
|
|
78
97
|
`;
|
|
79
98
|
}
|
|
80
99
|
}
|
|
100
|
+
if (lastOutput) {
|
|
101
|
+
systemPrompt += `
|
|
102
|
+
Output from last command run:
|
|
103
|
+
${lastOutput}
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
81
106
|
systemPrompt += `
|
|
82
107
|
Description to convert:`;
|
|
83
108
|
function startSpinner() {
|
|
@@ -178,25 +203,66 @@ function prompt() {
|
|
|
178
203
|
});
|
|
179
204
|
}
|
|
180
205
|
async function main() {
|
|
206
|
+
const updatePromise = checkForUpdate();
|
|
181
207
|
const messages = [
|
|
182
208
|
{ role: "system", content: systemPrompt },
|
|
183
209
|
{ role: "user", content: query }
|
|
184
210
|
];
|
|
185
211
|
let command = await askLLM(messages);
|
|
186
212
|
if (!command) die("API returned empty command");
|
|
213
|
+
if (command === "NONE") {
|
|
214
|
+
process.stderr.write(`\r\x1B[K`);
|
|
215
|
+
process.stderr.write(`\x1B[2meric: I can only help with commands. Try rephrasing as an action.\x1B[0m
|
|
216
|
+
`);
|
|
217
|
+
const updateMsg = await updatePromise;
|
|
218
|
+
if (updateMsg) process.stderr.write(updateMsg);
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
187
221
|
messages.push({ role: "assistant", content: command });
|
|
188
222
|
while (true) {
|
|
189
223
|
process.stderr.write(`\x1B[2mEnter to run \xB7 Type to refine \xB7 Ctrl+C to cancel\x1B[0m
|
|
190
224
|
`);
|
|
191
225
|
const answer = await prompt();
|
|
192
226
|
if (!answer) {
|
|
193
|
-
const
|
|
194
|
-
|
|
227
|
+
const updateMsg = await updatePromise;
|
|
228
|
+
if (updateMsg) process.stderr.write(updateMsg);
|
|
229
|
+
const child = spawn(command, {
|
|
230
|
+
shell: true,
|
|
231
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
232
|
+
});
|
|
233
|
+
let captured = "";
|
|
234
|
+
child.stdout.on("data", (data) => {
|
|
235
|
+
const text = data.toString();
|
|
236
|
+
process.stdout.write(text);
|
|
237
|
+
captured += text;
|
|
238
|
+
});
|
|
239
|
+
child.stderr.on("data", (data) => {
|
|
240
|
+
const text = data.toString();
|
|
241
|
+
process.stderr.write(text);
|
|
242
|
+
captured += text;
|
|
243
|
+
});
|
|
244
|
+
child.on("close", (code) => {
|
|
245
|
+
const lines = captured.trim().split("\n");
|
|
246
|
+
const tail = lines.slice(-200).join("\n");
|
|
247
|
+
try {
|
|
248
|
+
writeFileSync(LAST_OUTPUT_FILE, tail, "utf-8");
|
|
249
|
+
} catch {
|
|
250
|
+
}
|
|
251
|
+
process.exit(code ?? 0);
|
|
252
|
+
});
|
|
195
253
|
return;
|
|
196
254
|
}
|
|
197
255
|
messages.push({ role: "user", content: answer });
|
|
198
256
|
command = await askLLM(messages, true);
|
|
199
257
|
if (!command) die("API returned empty command");
|
|
258
|
+
if (command === "NONE") {
|
|
259
|
+
process.stderr.write(`\r\x1B[K`);
|
|
260
|
+
process.stderr.write(`\x1B[2meric: I can only help with commands. Try rephrasing as an action.\x1B[0m
|
|
261
|
+
`);
|
|
262
|
+
const updateMsg = await updatePromise;
|
|
263
|
+
if (updateMsg) process.stderr.write(updateMsg);
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
200
266
|
messages.push({ role: "assistant", content: command });
|
|
201
267
|
}
|
|
202
268
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "heyeric",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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 src/sanitize.ts --format esm --clean",
|
|
10
|
+
"build": "tsup src/index.ts src/sanitize.ts src/update-check.ts --format esm --clean",
|
|
11
11
|
"dev": "tsx src/index.ts",
|
|
12
12
|
"test": "tsx --test src/index.test.ts"
|
|
13
13
|
},
|