heyeric 1.2.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 +80 -21
- 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,38 +80,93 @@ Recent command history:
|
|
|
74
80
|
}
|
|
75
81
|
systemPrompt += `
|
|
76
82
|
Description to convert:`;
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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;
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
107
|
+
const stopSpinner = startSpinner();
|
|
108
|
+
try {
|
|
109
|
+
return await streamLLM(messages, stopSpinner);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
stopSpinner();
|
|
112
|
+
throw e;
|
|
113
|
+
}
|
|
90
114
|
}
|
|
91
|
-
async function
|
|
115
|
+
async function streamLLM(messages, stopSpinner) {
|
|
92
116
|
const resp = await fetch(apiUrl, {
|
|
93
117
|
method: "POST",
|
|
94
118
|
headers: {
|
|
95
119
|
"Content-Type": "application/json",
|
|
96
120
|
Authorization: `Bearer ${apiKey}`
|
|
97
121
|
},
|
|
98
|
-
body: JSON.stringify({ model, messages, stream:
|
|
122
|
+
body: JSON.stringify({ model, messages, stream: true, max_tokens: 200 })
|
|
99
123
|
});
|
|
100
124
|
if (!resp.ok) {
|
|
125
|
+
stopSpinner();
|
|
101
126
|
const body = await resp.text();
|
|
102
127
|
die(`API error ${resp.status}: ${body}`);
|
|
103
128
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
die("No
|
|
129
|
+
if (!resp.body) {
|
|
130
|
+
stopSpinner();
|
|
131
|
+
die("No response body");
|
|
132
|
+
}
|
|
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
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (spinnerStopped) {
|
|
164
|
+
process.stderr.write(`\x1B[0m
|
|
165
|
+
`);
|
|
166
|
+
} else {
|
|
167
|
+
stopSpinner();
|
|
107
168
|
}
|
|
108
|
-
return sanitize(
|
|
169
|
+
return sanitize(raw);
|
|
109
170
|
}
|
|
110
171
|
function prompt() {
|
|
111
172
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
@@ -125,8 +186,6 @@ async function main() {
|
|
|
125
186
|
if (!command) die("API returned empty command");
|
|
126
187
|
messages.push({ role: "assistant", content: command });
|
|
127
188
|
while (true) {
|
|
128
|
-
process.stderr.write(`\x1B[1;32m\u276F\x1B[0m \x1B[1m${command}\x1B[0m
|
|
129
|
-
`);
|
|
130
189
|
process.stderr.write(`\x1B[2mEnter to run \xB7 Type to refine \xB7 Ctrl+C to cancel\x1B[0m
|
|
131
190
|
`);
|
|
132
191
|
const answer = await prompt();
|
|
@@ -136,7 +195,7 @@ async function main() {
|
|
|
136
195
|
return;
|
|
137
196
|
}
|
|
138
197
|
messages.push({ role: "user", content: answer });
|
|
139
|
-
command = await askLLM(messages);
|
|
198
|
+
command = await askLLM(messages, true);
|
|
140
199
|
if (!command) die("API returned empty command");
|
|
141
200
|
messages.push({ role: "assistant", content: command });
|
|
142
201
|
}
|
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
|
}
|