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.
@@ -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,38 +80,93 @@ Recent command history:
74
80
  }
75
81
  systemPrompt += `
76
82
  Description to convert:`;
77
- function sanitize(raw) {
78
- let command = raw.trim();
79
- for (const prefix of ["```bash", "```sh", "```"]) {
80
- if (command.startsWith(prefix)) {
81
- command = command.slice(prefix.length);
82
- break;
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
- command = command.trim();
86
- command = command.split("\n")[0];
87
- command = command.trim().replace(/```$/, "");
88
- command = command.replace(/^`+|`+$/g, "");
89
- return command.trim();
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 askLLM(messages) {
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: false, max_tokens: 200 })
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
- const data = await resp.json();
105
- if (!data.choices?.length || !data.choices[0].message?.content) {
106
- die("No completion returned from API");
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(data.choices[0].message.content);
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
  }
@@ -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.2.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
  }