aether-code 0.6.1 → 0.8.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/LICENSE +21 -21
- package/README.md +140 -140
- package/bin/aether-code.js +228 -379
- package/package.json +39 -38
- package/src/agent.js +115 -185
- package/src/api.js +234 -236
- package/src/config.js +38 -38
- package/src/diff.js +48 -48
- package/src/render.js +58 -198
- package/src/repl.js +246 -292
- package/src/setup.js +139 -139
- package/src/tools.js +621 -358
- package/src/plan.js +0 -133
- package/src/sessions.js +0 -145
package/src/api.js
CHANGED
|
@@ -1,236 +1,234 @@
|
|
|
1
|
-
// API client.
|
|
2
|
-
|
|
3
|
-
import { getConfig } from "./config.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Free balance + plan check via /api/v1/me. Doesn't charge credits.
|
|
7
|
-
*/
|
|
8
|
-
export async function fetchBalance() {
|
|
9
|
-
const { apiKey, baseUrl } = getConfig();
|
|
10
|
-
if (!apiKey) {
|
|
11
|
-
throw new AetherError(
|
|
12
|
-
"No API key. Set AETHER_API_KEY or run `aether config set <key>`.",
|
|
13
|
-
"NO_API_KEY",
|
|
14
|
-
0,
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
let res;
|
|
18
|
-
try {
|
|
19
|
-
res = await fetch(`${baseUrl}/api/v1/me`, {
|
|
20
|
-
method: "GET",
|
|
21
|
-
headers: {
|
|
22
|
-
Accept: "application/json",
|
|
23
|
-
Authorization: `Bearer ${apiKey}`,
|
|
24
|
-
"User-Agent": "aether-code/0.2.0",
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
} catch (e) {
|
|
28
|
-
throw new AetherError(`Network error: ${e.message}`, "NETWORK", 0);
|
|
29
|
-
}
|
|
30
|
-
let data = null;
|
|
31
|
-
try { data = await res.json(); } catch { /* non-JSON */ }
|
|
32
|
-
if (!res.ok) {
|
|
33
|
-
const code = data?.code || `HTTP_${res.status}`;
|
|
34
|
-
const msg = data?.error || `${res.status} ${res.statusText}`;
|
|
35
|
-
throw new AetherError(msg, code, res.status, data);
|
|
36
|
-
}
|
|
37
|
-
return data; // { plan, role, planCredits, topupCredits, balance, isSuspended, rate }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export class AetherError extends Error {
|
|
41
|
-
constructor(message, code, status, data) {
|
|
42
|
-
super(message);
|
|
43
|
-
this.name = "AetherError";
|
|
44
|
-
this.code = code;
|
|
45
|
-
this.status = status;
|
|
46
|
-
this.data = data;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Streaming agent turn. Calls /api/v1/agent/stream and invokes the supplied
|
|
52
|
-
* callbacks as events arrive. Returns the assembled assistant message + final
|
|
53
|
-
* usage/credit info once the stream ends.
|
|
54
|
-
*
|
|
55
|
-
* Event handlers (all optional):
|
|
56
|
-
* onDelta(text) — text fragment from the model
|
|
57
|
-
* onToolCallDelta(part) — partial tool call: { index, id?, name?, args_delta? }
|
|
58
|
-
* onFinish(reason) — per-choice finish reason ("stop" | "tool_calls" | "length")
|
|
59
|
-
*
|
|
60
|
-
* Returns:
|
|
61
|
-
* { message: { role:"assistant", content, tool_calls }, finish_reason,
|
|
62
|
-
* creditsCharged, balanceAfter, usage }
|
|
63
|
-
*/
|
|
64
|
-
export async function agentTurnStream({
|
|
65
|
-
messages,
|
|
66
|
-
tools,
|
|
67
|
-
maxTokens,
|
|
68
|
-
temperature,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
let
|
|
117
|
-
|
|
118
|
-
let
|
|
119
|
-
let
|
|
120
|
-
let
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
buffer
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (!
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (evt.
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
*
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
"
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return data;
|
|
236
|
-
}
|
|
1
|
+
// API client.
|
|
2
|
+
|
|
3
|
+
import { getConfig } from "./config.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Free balance + plan check via /api/v1/me. Doesn't charge credits.
|
|
7
|
+
*/
|
|
8
|
+
export async function fetchBalance() {
|
|
9
|
+
const { apiKey, baseUrl } = getConfig();
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
throw new AetherError(
|
|
12
|
+
"No API key. Set AETHER_API_KEY or run `aether config set <key>`.",
|
|
13
|
+
"NO_API_KEY",
|
|
14
|
+
0,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
let res;
|
|
18
|
+
try {
|
|
19
|
+
res = await fetch(`${baseUrl}/api/v1/me`, {
|
|
20
|
+
method: "GET",
|
|
21
|
+
headers: {
|
|
22
|
+
Accept: "application/json",
|
|
23
|
+
Authorization: `Bearer ${apiKey}`,
|
|
24
|
+
"User-Agent": "aether-code/0.2.0",
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
} catch (e) {
|
|
28
|
+
throw new AetherError(`Network error: ${e.message}`, "NETWORK", 0);
|
|
29
|
+
}
|
|
30
|
+
let data = null;
|
|
31
|
+
try { data = await res.json(); } catch { /* non-JSON */ }
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
const code = data?.code || `HTTP_${res.status}`;
|
|
34
|
+
const msg = data?.error || `${res.status} ${res.statusText}`;
|
|
35
|
+
throw new AetherError(msg, code, res.status, data);
|
|
36
|
+
}
|
|
37
|
+
return data; // { plan, role, planCredits, topupCredits, balance, isSuspended, rate }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class AetherError extends Error {
|
|
41
|
+
constructor(message, code, status, data) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = "AetherError";
|
|
44
|
+
this.code = code;
|
|
45
|
+
this.status = status;
|
|
46
|
+
this.data = data;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Streaming agent turn. Calls /api/v1/agent/stream and invokes the supplied
|
|
52
|
+
* callbacks as events arrive. Returns the assembled assistant message + final
|
|
53
|
+
* usage/credit info once the stream ends.
|
|
54
|
+
*
|
|
55
|
+
* Event handlers (all optional):
|
|
56
|
+
* onDelta(text) — text fragment from the model
|
|
57
|
+
* onToolCallDelta(part) — partial tool call: { index, id?, name?, args_delta? }
|
|
58
|
+
* onFinish(reason) — per-choice finish reason ("stop" | "tool_calls" | "length")
|
|
59
|
+
*
|
|
60
|
+
* Returns:
|
|
61
|
+
* { message: { role:"assistant", content, tool_calls }, finish_reason,
|
|
62
|
+
* creditsCharged, balanceAfter, usage }
|
|
63
|
+
*/
|
|
64
|
+
export async function agentTurnStream({
|
|
65
|
+
messages,
|
|
66
|
+
tools,
|
|
67
|
+
maxTokens,
|
|
68
|
+
temperature,
|
|
69
|
+
onDelta,
|
|
70
|
+
onToolCallDelta,
|
|
71
|
+
onFinish,
|
|
72
|
+
}) {
|
|
73
|
+
const { apiKey, baseUrl } = getConfig();
|
|
74
|
+
if (!apiKey) {
|
|
75
|
+
throw new AetherError(
|
|
76
|
+
"No API key. Set AETHER_API_KEY or run `aether config set <key>`.",
|
|
77
|
+
"NO_API_KEY",
|
|
78
|
+
0,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let res;
|
|
83
|
+
try {
|
|
84
|
+
res = await fetch(`${baseUrl}/api/v1/agent/stream`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
Accept: "text/event-stream",
|
|
89
|
+
Authorization: `Bearer ${apiKey}`,
|
|
90
|
+
"User-Agent": "aether-code/0.1.0",
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
messages,
|
|
94
|
+
tools,
|
|
95
|
+
max_tokens: maxTokens,
|
|
96
|
+
temperature,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
} catch (e) {
|
|
100
|
+
throw new AetherError(`Network error: ${e.message}`, "NETWORK", 0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Non-streaming JSON error response (auth, credit, validation failures)
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
let data = null;
|
|
106
|
+
try { data = await res.json(); } catch { /* non-JSON */ }
|
|
107
|
+
const code = data?.code || `HTTP_${res.status}`;
|
|
108
|
+
const msg = data?.error || `${res.status} ${res.statusText}`;
|
|
109
|
+
throw new AetherError(msg, code, res.status, data);
|
|
110
|
+
}
|
|
111
|
+
if (!res.body) throw new AetherError("Response has no body", "NO_BODY", res.status);
|
|
112
|
+
|
|
113
|
+
// Accumulate streaming state
|
|
114
|
+
let textBuf = "";
|
|
115
|
+
const toolBuf = new Map(); // index -> { id, name, args }
|
|
116
|
+
let finishReason = "stop";
|
|
117
|
+
let creditsCharged = 0;
|
|
118
|
+
let balanceAfter = null;
|
|
119
|
+
let usage = { prompt_tokens: 0, completion_tokens: 0 };
|
|
120
|
+
let streamError = null;
|
|
121
|
+
|
|
122
|
+
const reader = res.body.getReader();
|
|
123
|
+
const decoder = new TextDecoder();
|
|
124
|
+
let buffer = "";
|
|
125
|
+
while (true) {
|
|
126
|
+
const { value, done } = await reader.read();
|
|
127
|
+
if (done) break;
|
|
128
|
+
buffer += decoder.decode(value, { stream: true });
|
|
129
|
+
const lines = buffer.split("\n");
|
|
130
|
+
buffer = lines.pop() ?? "";
|
|
131
|
+
for (const line of lines) {
|
|
132
|
+
if (!line.startsWith("data:")) continue;
|
|
133
|
+
const json = line.slice(5).trim();
|
|
134
|
+
if (!json) continue;
|
|
135
|
+
let evt;
|
|
136
|
+
try { evt = JSON.parse(json); } catch { continue; }
|
|
137
|
+
if (evt.kind === "delta") {
|
|
138
|
+
textBuf += evt.content;
|
|
139
|
+
if (onDelta) onDelta(evt.content);
|
|
140
|
+
} else if (evt.kind === "tool_call_delta") {
|
|
141
|
+
const slot = toolBuf.get(evt.index) || { id: undefined, name: undefined, args: "" };
|
|
142
|
+
if (evt.id) slot.id = evt.id;
|
|
143
|
+
if (evt.name) slot.name = evt.name;
|
|
144
|
+
if (evt.args_delta) slot.args += evt.args_delta;
|
|
145
|
+
toolBuf.set(evt.index, slot);
|
|
146
|
+
if (onToolCallDelta) onToolCallDelta(evt);
|
|
147
|
+
} else if (evt.kind === "finish") {
|
|
148
|
+
finishReason = evt.reason;
|
|
149
|
+
if (onFinish) onFinish(evt.reason);
|
|
150
|
+
} else if (evt.kind === "done") {
|
|
151
|
+
creditsCharged = evt.creditsCharged ?? 0;
|
|
152
|
+
balanceAfter = evt.balanceAfter ?? null;
|
|
153
|
+
usage = evt.usage ?? usage;
|
|
154
|
+
} else if (evt.kind === "error") {
|
|
155
|
+
streamError = evt.error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (streamError) {
|
|
161
|
+
throw new AetherError(streamError, "STREAM_ERROR", 0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Assemble final tool_calls in stable index order
|
|
165
|
+
const tool_calls = [...toolBuf.entries()]
|
|
166
|
+
.sort((a, b) => a[0] - b[0])
|
|
167
|
+
.map(([, slot]) => ({
|
|
168
|
+
id: slot.id || `call_${Math.random().toString(36).slice(2, 10)}`,
|
|
169
|
+
type: "function",
|
|
170
|
+
function: { name: slot.name || "", arguments: slot.args || "" },
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
message: {
|
|
175
|
+
role: "assistant",
|
|
176
|
+
content: textBuf || null,
|
|
177
|
+
tool_calls: tool_calls.length > 0 ? tool_calls : undefined,
|
|
178
|
+
},
|
|
179
|
+
finish_reason: finishReason,
|
|
180
|
+
creditsCharged,
|
|
181
|
+
balanceAfter,
|
|
182
|
+
usage,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Non-streaming variant — kept as a fallback for environments where SSE is
|
|
188
|
+
* problematic (corporate proxies, weird client setups). The CLI defaults to
|
|
189
|
+
* streaming.
|
|
190
|
+
*/
|
|
191
|
+
export async function agentTurn({ messages, tools, maxTokens, temperature }) {
|
|
192
|
+
const { apiKey, baseUrl } = getConfig();
|
|
193
|
+
if (!apiKey) {
|
|
194
|
+
throw new AetherError(
|
|
195
|
+
"No API key. Set AETHER_API_KEY env var or run `aether-cli config set <key>`.",
|
|
196
|
+
"NO_API_KEY",
|
|
197
|
+
0,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let res;
|
|
202
|
+
try {
|
|
203
|
+
res = await fetch(`${baseUrl}/api/v1/agent`, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers: {
|
|
206
|
+
"Content-Type": "application/json",
|
|
207
|
+
Authorization: `Bearer ${apiKey}`,
|
|
208
|
+
"User-Agent": "aether-code/0.1.0",
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify({
|
|
211
|
+
messages,
|
|
212
|
+
tools,
|
|
213
|
+
max_tokens: maxTokens,
|
|
214
|
+
temperature,
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
} catch (e) {
|
|
218
|
+
throw new AetherError(`Network error: ${e.message}`, "NETWORK", 0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let data = null;
|
|
222
|
+
try {
|
|
223
|
+
data = await res.json();
|
|
224
|
+
} catch {
|
|
225
|
+
/* non-JSON */
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
const code = (data && data.code) || `HTTP_${res.status}`;
|
|
230
|
+
const msg = (data && data.error) || `${res.status} ${res.statusText}`;
|
|
231
|
+
throw new AetherError(msg, code, res.status, data);
|
|
232
|
+
}
|
|
233
|
+
return data;
|
|
234
|
+
}
|
package/src/config.js
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
// Config — reads ~/.aetherrc (shared format with aether-cli) and env vars.
|
|
2
|
-
// Same precedence: env wins over file.
|
|
3
|
-
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
|
|
8
|
-
const FILE = path.join(os.homedir(), ".aetherrc");
|
|
9
|
-
const DEFAULT_BASE = "https://trynoguard.com";
|
|
10
|
-
|
|
11
|
-
export function readConfigFile() {
|
|
12
|
-
try {
|
|
13
|
-
const raw = fs.readFileSync(FILE, "utf8");
|
|
14
|
-
return JSON.parse(raw);
|
|
15
|
-
} catch {
|
|
16
|
-
return {};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function writeConfigFile(patch) {
|
|
21
|
-
const current = readConfigFile();
|
|
22
|
-
const next = { ...current, ...patch };
|
|
23
|
-
// 0600 — readable only by the user. Mirrors what ssh, gnupg, etc. enforce
|
|
24
|
-
// on credential files. Treat the API key like an SSH key.
|
|
25
|
-
fs.writeFileSync(FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
26
|
-
try { fs.chmodSync(FILE, 0o600); } catch { /* windows ignores */ }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function getConfig() {
|
|
30
|
-
const file = readConfigFile();
|
|
31
|
-
return {
|
|
32
|
-
apiKey: process.env.AETHER_API_KEY || file.apiKey || "",
|
|
33
|
-
baseUrl: (process.env.AETHER_BASE_URL || file.baseUrl || DEFAULT_BASE).replace(/\/+$/, ""),
|
|
34
|
-
configPath: FILE,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const CONFIG_PATH = FILE;
|
|
1
|
+
// Config — reads ~/.aetherrc (shared format with aether-cli) and env vars.
|
|
2
|
+
// Same precedence: env wins over file.
|
|
3
|
+
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
|
|
8
|
+
const FILE = path.join(os.homedir(), ".aetherrc");
|
|
9
|
+
const DEFAULT_BASE = "https://trynoguard.com";
|
|
10
|
+
|
|
11
|
+
export function readConfigFile() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = fs.readFileSync(FILE, "utf8");
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function writeConfigFile(patch) {
|
|
21
|
+
const current = readConfigFile();
|
|
22
|
+
const next = { ...current, ...patch };
|
|
23
|
+
// 0600 — readable only by the user. Mirrors what ssh, gnupg, etc. enforce
|
|
24
|
+
// on credential files. Treat the API key like an SSH key.
|
|
25
|
+
fs.writeFileSync(FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
26
|
+
try { fs.chmodSync(FILE, 0o600); } catch { /* windows ignores */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getConfig() {
|
|
30
|
+
const file = readConfigFile();
|
|
31
|
+
return {
|
|
32
|
+
apiKey: process.env.AETHER_API_KEY || file.apiKey || "",
|
|
33
|
+
baseUrl: (process.env.AETHER_BASE_URL || file.baseUrl || DEFAULT_BASE).replace(/\/+$/, ""),
|
|
34
|
+
configPath: FILE,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const CONFIG_PATH = FILE;
|
package/src/diff.js
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
// Tiny line-by-line diff for write_file confirmation prompts.
|
|
2
|
-
// Not a "real" diff — just a side-by-side highlight of what's changing.
|
|
3
|
-
// Good enough for confirmation prompts, deliberately not pretending to be `git diff`.
|
|
4
|
-
|
|
5
|
-
import { c } from "./render.js";
|
|
6
|
-
|
|
7
|
-
export function unifiedDiff(oldText, newText, filename) {
|
|
8
|
-
const oldLines = (oldText || "").split("\n");
|
|
9
|
-
const newLines = (newText || "").split("\n");
|
|
10
|
-
const max = Math.max(oldLines.length, newLines.length);
|
|
11
|
-
|
|
12
|
-
// Find common prefix and suffix to keep the diff focused
|
|
13
|
-
let prefix = 0;
|
|
14
|
-
while (prefix < max && oldLines[prefix] === newLines[prefix]) prefix++;
|
|
15
|
-
let suffix = 0;
|
|
16
|
-
while (
|
|
17
|
-
suffix < max - prefix &&
|
|
18
|
-
oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
|
|
19
|
-
) {
|
|
20
|
-
suffix++;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const changedOld = oldLines.slice(prefix, oldLines.length - suffix);
|
|
24
|
-
const changedNew = newLines.slice(prefix, newLines.length - suffix);
|
|
25
|
-
|
|
26
|
-
const lines = [];
|
|
27
|
-
lines.push(c.bold(c.cyan(`@@ ${filename} @@`)));
|
|
28
|
-
if (prefix > 0) lines.push(c.gray(` …${prefix} unchanged line${prefix === 1 ? "" : "s"} above…`));
|
|
29
|
-
for (const l of changedOld) lines.push(c.red(`- ${l}`));
|
|
30
|
-
for (const l of changedNew) lines.push(c.green(`+ ${l}`));
|
|
31
|
-
if (suffix > 0) lines.push(c.gray(` …${suffix} unchanged line${suffix === 1 ? "" : "s"} below…`));
|
|
32
|
-
|
|
33
|
-
// Cap output so massive writes don't flood the terminal
|
|
34
|
-
if (lines.length > 60) {
|
|
35
|
-
return [...lines.slice(0, 30), c.gray(` …${lines.length - 60} more lines hidden…`), ...lines.slice(-30)].join(
|
|
36
|
-
"\n",
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
return lines.join("\n");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function summarizeWrite(oldText, newText, filename) {
|
|
43
|
-
const oldLines = (oldText || "").split("\n").length;
|
|
44
|
-
const newLines = (newText || "").split("\n").length;
|
|
45
|
-
const isCreate = oldText === null || oldText === undefined;
|
|
46
|
-
const verb = isCreate ? "create" : "rewrite";
|
|
47
|
-
return c.dim(`${verb} ${filename} (${oldLines} → ${newLines} lines, ${(newText || "").length} bytes)`);
|
|
48
|
-
}
|
|
1
|
+
// Tiny line-by-line diff for write_file confirmation prompts.
|
|
2
|
+
// Not a "real" diff — just a side-by-side highlight of what's changing.
|
|
3
|
+
// Good enough for confirmation prompts, deliberately not pretending to be `git diff`.
|
|
4
|
+
|
|
5
|
+
import { c } from "./render.js";
|
|
6
|
+
|
|
7
|
+
export function unifiedDiff(oldText, newText, filename) {
|
|
8
|
+
const oldLines = (oldText || "").split("\n");
|
|
9
|
+
const newLines = (newText || "").split("\n");
|
|
10
|
+
const max = Math.max(oldLines.length, newLines.length);
|
|
11
|
+
|
|
12
|
+
// Find common prefix and suffix to keep the diff focused
|
|
13
|
+
let prefix = 0;
|
|
14
|
+
while (prefix < max && oldLines[prefix] === newLines[prefix]) prefix++;
|
|
15
|
+
let suffix = 0;
|
|
16
|
+
while (
|
|
17
|
+
suffix < max - prefix &&
|
|
18
|
+
oldLines[oldLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix]
|
|
19
|
+
) {
|
|
20
|
+
suffix++;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const changedOld = oldLines.slice(prefix, oldLines.length - suffix);
|
|
24
|
+
const changedNew = newLines.slice(prefix, newLines.length - suffix);
|
|
25
|
+
|
|
26
|
+
const lines = [];
|
|
27
|
+
lines.push(c.bold(c.cyan(`@@ ${filename} @@`)));
|
|
28
|
+
if (prefix > 0) lines.push(c.gray(` …${prefix} unchanged line${prefix === 1 ? "" : "s"} above…`));
|
|
29
|
+
for (const l of changedOld) lines.push(c.red(`- ${l}`));
|
|
30
|
+
for (const l of changedNew) lines.push(c.green(`+ ${l}`));
|
|
31
|
+
if (suffix > 0) lines.push(c.gray(` …${suffix} unchanged line${suffix === 1 ? "" : "s"} below…`));
|
|
32
|
+
|
|
33
|
+
// Cap output so massive writes don't flood the terminal
|
|
34
|
+
if (lines.length > 60) {
|
|
35
|
+
return [...lines.slice(0, 30), c.gray(` …${lines.length - 60} more lines hidden…`), ...lines.slice(-30)].join(
|
|
36
|
+
"\n",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function summarizeWrite(oldText, newText, filename) {
|
|
43
|
+
const oldLines = (oldText || "").split("\n").length;
|
|
44
|
+
const newLines = (newText || "").split("\n").length;
|
|
45
|
+
const isCreate = oldText === null || oldText === undefined;
|
|
46
|
+
const verb = isCreate ? "create" : "rewrite";
|
|
47
|
+
return c.dim(`${verb} ${filename} (${oldLines} → ${newLines} lines, ${(newText || "").length} bytes)`);
|
|
48
|
+
}
|