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.
@@ -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(`eric: ${msg}
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
- async function main() {
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
- const data = await resp.json();
99
- if (!data.choices?.length || !data.choices[0].message?.content) {
100
- die("No completion returned from API");
129
+ if (!resp.body) {
130
+ stopSpinner();
131
+ die("No response body");
101
132
  }
102
- let command = data.choices[0].message.content.trim();
103
- for (const prefix of ["```bash", "```sh", "```"]) {
104
- if (command.startsWith(prefix)) {
105
- command = command.slice(prefix.length);
106
- break;
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
- command = command.trim();
110
- command = command.split("\n")[0];
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
- const answer = await new Promise((resolve) => {
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
- if (answer) {
127
- const child2 = spawn(process.argv[0], [...process.argv.slice(1, 2), answer], {
128
- stdio: "inherit",
129
- env: process.env
130
- });
131
- child2.on("close", (code) => process.exit(code ?? 0));
132
- return;
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);
@@ -0,0 +1,6 @@
1
+ import {
2
+ sanitize
3
+ } from "./chunk-2CBZLAF5.js";
4
+ export {
5
+ sanitize
6
+ };
package/package.json CHANGED
@@ -1,14 +1,15 @@
1
1
  {
2
2
  "name": "heyeric",
3
- "version": "1.1.0",
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
  }