accelr8 1.0.0 → 3.0.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/README.md +118 -0
- package/dist/chunks/agentcli-ZI2O7ID7.js +176 -0
- package/dist/chunks/chunk-3JNUWI22.js +563 -0
- package/dist/chunks/chunk-SF6NNSYZ.js +60 -0
- package/dist/chunks/client-7BVK2DEW.js +3454 -0
- package/dist/chunks/server-ZP2DV4IO.js +930 -0
- package/dist/chunks/update-UEPNGB6Y.js +106 -0
- package/dist/cli.js +88 -0
- package/package.json +63 -12
- package/ascii.txt +0 -11
- package/bin/index.js +0 -314
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/accounts.ts
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
var HANDLE_RE = /^[a-z0-9][a-z0-9-]{2,19}$/;
|
|
8
|
+
var DEFAULT_MODEL = {
|
|
9
|
+
anthropic: "claude-opus-4-8",
|
|
10
|
+
openai: "gpt-4o"
|
|
11
|
+
};
|
|
12
|
+
var DEFAULT_MAX_CALLS_PER_HOUR = 30;
|
|
13
|
+
var DEFAULT_MAX_CALLS_PER_REQUESTER_HOUR = 10;
|
|
14
|
+
var DEFAULT_MAX_OUTPUT_TOKENS = 1024;
|
|
15
|
+
var ACCOUNTS_FILE = "accounts.json";
|
|
16
|
+
var TOKENS_FILE = "agent-tokens.json";
|
|
17
|
+
function dirOf(baseDir) {
|
|
18
|
+
return baseDir ?? path.join(os.homedir(), ".accelr8");
|
|
19
|
+
}
|
|
20
|
+
function ensureDir(dir) {
|
|
21
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
22
|
+
try {
|
|
23
|
+
fs.chmodSync(dir, 448);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readJson(file) {
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = fs.readFileSync(file, "utf8");
|
|
31
|
+
} catch {
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
if ((fs.statSync(file).mode & 63) !== 0) {
|
|
36
|
+
try {
|
|
37
|
+
fs.chmodSync(file, 384);
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
if ((fs.statSync(file).mode & 63) !== 0) {
|
|
41
|
+
process.stderr.write(`accelr8: refusing to load ${file}: it is readable by others and could not be locked to 600. run: chmod 600 ${file}
|
|
42
|
+
`);
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(raw);
|
|
50
|
+
} catch {
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function writeJson(file, value) {
|
|
55
|
+
ensureDir(path.dirname(file));
|
|
56
|
+
const tmp = `${file}.tmp-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
|
|
57
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
58
|
+
fs.chmodSync(tmp, 384);
|
|
59
|
+
fs.renameSync(tmp, file);
|
|
60
|
+
fs.chmodSync(file, 384);
|
|
61
|
+
}
|
|
62
|
+
function isAccount(v) {
|
|
63
|
+
if (!v || typeof v !== "object") return false;
|
|
64
|
+
const a = v;
|
|
65
|
+
const auth = a.auth;
|
|
66
|
+
return typeof a.id === "string" && (a.provider === "anthropic" || a.provider === "openai") && !!auth && typeof auth === "object" && (auth.kind === "api_key" ? typeof auth.apiKey === "string" : auth.kind === "claude_code_login") && typeof a.model === "string" && typeof a.handle === "string" && typeof a.maxCallsPerHour === "number" && typeof a.maxCallsPerRequesterHour === "number" && typeof a.maxOutputTokens === "number" && typeof a.createdAt === "number";
|
|
67
|
+
}
|
|
68
|
+
function loadAccounts(baseDir) {
|
|
69
|
+
const data = readJson(path.join(dirOf(baseDir), ACCOUNTS_FILE));
|
|
70
|
+
if (!Array.isArray(data)) return [];
|
|
71
|
+
return data.filter(isAccount).map((a) => ({ ...a, mode: a.mode ?? "approve" }));
|
|
72
|
+
}
|
|
73
|
+
function saveAccounts(list, baseDir) {
|
|
74
|
+
writeJson(path.join(dirOf(baseDir), ACCOUNTS_FILE), list);
|
|
75
|
+
}
|
|
76
|
+
function addAccount(partial, baseDir) {
|
|
77
|
+
if (!HANDLE_RE.test(partial.handle)) {
|
|
78
|
+
throw new Error(`invalid handle '${partial.handle}' (want 3-20 chars of a-z 0-9 -, starting alphanumeric)`);
|
|
79
|
+
}
|
|
80
|
+
if (partial.auth.kind === "api_key" && !partial.auth.apiKey) {
|
|
81
|
+
throw new Error("apiKey must not be empty");
|
|
82
|
+
}
|
|
83
|
+
const account = {
|
|
84
|
+
id: "acct_" + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6),
|
|
85
|
+
provider: partial.provider,
|
|
86
|
+
auth: partial.auth,
|
|
87
|
+
model: partial.model || DEFAULT_MODEL[partial.provider],
|
|
88
|
+
handle: partial.handle,
|
|
89
|
+
mode: partial.mode ?? "approve",
|
|
90
|
+
maxCallsPerHour: partial.maxCallsPerHour ?? DEFAULT_MAX_CALLS_PER_HOUR,
|
|
91
|
+
maxCallsPerRequesterHour: partial.maxCallsPerRequesterHour ?? DEFAULT_MAX_CALLS_PER_REQUESTER_HOUR,
|
|
92
|
+
maxOutputTokens: partial.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS,
|
|
93
|
+
createdAt: Date.now()
|
|
94
|
+
};
|
|
95
|
+
const list = loadAccounts(baseDir);
|
|
96
|
+
list.push(account);
|
|
97
|
+
saveAccounts(list, baseDir);
|
|
98
|
+
return account;
|
|
99
|
+
}
|
|
100
|
+
function removeAccount(id, baseDir) {
|
|
101
|
+
const list = loadAccounts(baseDir);
|
|
102
|
+
const next = list.filter((a) => a.id !== id);
|
|
103
|
+
if (next.length === list.length) return false;
|
|
104
|
+
saveAccounts(next, baseDir);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
var tokenKey = (projectId, agentId) => `${projectId}:${agentId}`;
|
|
108
|
+
function loadTokens(baseDir) {
|
|
109
|
+
const data = readJson(path.join(dirOf(baseDir), TOKENS_FILE));
|
|
110
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return {};
|
|
111
|
+
const out = {};
|
|
112
|
+
for (const [k, v] of Object.entries(data)) if (typeof v === "string") out[k] = v;
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function saveToken(projectId, agentId, token, baseDir) {
|
|
116
|
+
const tokens = loadTokens(baseDir);
|
|
117
|
+
tokens[tokenKey(projectId, agentId)] = token;
|
|
118
|
+
writeJson(path.join(dirOf(baseDir), TOKENS_FILE), tokens);
|
|
119
|
+
}
|
|
120
|
+
function detectClaudeCode() {
|
|
121
|
+
const dirs = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
|
|
122
|
+
for (const dir of dirs) {
|
|
123
|
+
const candidate = path.join(dir, "claude");
|
|
124
|
+
try {
|
|
125
|
+
if (!fs.existsSync(candidate)) continue;
|
|
126
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
127
|
+
if (fs.statSync(candidate).isFile()) return { available: true, path: candidate };
|
|
128
|
+
} catch {
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { available: false };
|
|
132
|
+
}
|
|
133
|
+
function detectEnvKeys() {
|
|
134
|
+
const out = {};
|
|
135
|
+
const a = process.env.ANTHROPIC_API_KEY?.trim();
|
|
136
|
+
const o = process.env.OPENAI_API_KEY?.trim();
|
|
137
|
+
if (a) out.anthropic = a;
|
|
138
|
+
if (o) out.openai = o;
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function defaultHandle(userName, provider) {
|
|
142
|
+
const suffix = "s" + (provider === "anthropic" ? "claude" : "gpt");
|
|
143
|
+
let name = userName.toLowerCase().replace(/[^a-z0-9-]/g, "").replace(/^-+/, "");
|
|
144
|
+
if (!name) name = "my";
|
|
145
|
+
return name.slice(0, 20 - suffix.length) + suffix;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/executor.ts
|
|
149
|
+
import { spawn } from "node:child_process";
|
|
150
|
+
var ANTHROPIC_VERSION = "2023-06-01";
|
|
151
|
+
var ANTHROPIC_BASE = "https://api.anthropic.com";
|
|
152
|
+
var OPENAI_BASE = "https://api.openai.com";
|
|
153
|
+
var DELTA_MS = 100;
|
|
154
|
+
var DELTA_CHARS = 800;
|
|
155
|
+
var RETRY_CAP_MS = 1e4;
|
|
156
|
+
var STDERR_TAIL = 500;
|
|
157
|
+
var Run = class {
|
|
158
|
+
constructor(cb, secret) {
|
|
159
|
+
this.cb = cb;
|
|
160
|
+
this.secret = secret;
|
|
161
|
+
}
|
|
162
|
+
cb;
|
|
163
|
+
secret;
|
|
164
|
+
ac = new AbortController();
|
|
165
|
+
aborted = false;
|
|
166
|
+
settled = false;
|
|
167
|
+
buf = "";
|
|
168
|
+
timer = null;
|
|
169
|
+
lastFlush = Date.now();
|
|
170
|
+
get isSettled() {
|
|
171
|
+
return this.settled;
|
|
172
|
+
}
|
|
173
|
+
push(text) {
|
|
174
|
+
if (this.settled || !text) return;
|
|
175
|
+
this.buf += text;
|
|
176
|
+
if (this.buf.length >= DELTA_CHARS) {
|
|
177
|
+
this.flush();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (!this.timer) {
|
|
181
|
+
const wait = Math.max(0, this.lastFlush + DELTA_MS - Date.now());
|
|
182
|
+
this.timer = setTimeout(() => {
|
|
183
|
+
this.timer = null;
|
|
184
|
+
this.flush();
|
|
185
|
+
}, wait);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
done(finish) {
|
|
189
|
+
if (this.settled) return;
|
|
190
|
+
if (finish !== "aborted") this.flush();
|
|
191
|
+
this.settle();
|
|
192
|
+
this.cb.onDone(finish);
|
|
193
|
+
}
|
|
194
|
+
error(code, message) {
|
|
195
|
+
if (this.settled) return;
|
|
196
|
+
this.flush();
|
|
197
|
+
this.settle();
|
|
198
|
+
this.cb.onError(code, this.scrub(message));
|
|
199
|
+
}
|
|
200
|
+
abort() {
|
|
201
|
+
if (this.settled) return;
|
|
202
|
+
this.aborted = true;
|
|
203
|
+
this.ac.abort();
|
|
204
|
+
this.done("aborted");
|
|
205
|
+
}
|
|
206
|
+
/** Key must never surface, even echoed back by a provider error body. */
|
|
207
|
+
scrub(message) {
|
|
208
|
+
return this.secret ? message.split(this.secret).join("***") : message;
|
|
209
|
+
}
|
|
210
|
+
flush() {
|
|
211
|
+
if (this.timer) {
|
|
212
|
+
clearTimeout(this.timer);
|
|
213
|
+
this.timer = null;
|
|
214
|
+
}
|
|
215
|
+
if (this.settled || !this.buf) return;
|
|
216
|
+
const text = this.buf;
|
|
217
|
+
this.buf = "";
|
|
218
|
+
this.lastFlush = Date.now();
|
|
219
|
+
this.cb.onDelta(text);
|
|
220
|
+
}
|
|
221
|
+
settle() {
|
|
222
|
+
this.settled = true;
|
|
223
|
+
this.buf = "";
|
|
224
|
+
if (this.timer) {
|
|
225
|
+
clearTimeout(this.timer);
|
|
226
|
+
this.timer = null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function execute(account, ctx, cb, opts) {
|
|
231
|
+
if (account.auth.kind === "claude_code_login") return executeClaudeCli(ctx, cb);
|
|
232
|
+
const run = new Run(cb, account.auth.apiKey);
|
|
233
|
+
if (account.provider === "anthropic") void streamAnthropic(account, account.auth.apiKey, ctx, run, opts);
|
|
234
|
+
else void streamOpenai(account, account.auth.apiKey, ctx, run, opts);
|
|
235
|
+
return { abort: () => run.abort() };
|
|
236
|
+
}
|
|
237
|
+
async function verifyAccount(account, opts) {
|
|
238
|
+
if (account.auth.kind === "claude_code_login") {
|
|
239
|
+
const det = detectClaudeCode();
|
|
240
|
+
return det.available ? { ok: true, message: `claude CLI found at ${det.path ?? "PATH"}` } : { ok: false, message: "claude CLI not found on PATH" };
|
|
241
|
+
}
|
|
242
|
+
const apiKey = account.auth.apiKey;
|
|
243
|
+
const anthropic = account.provider === "anthropic";
|
|
244
|
+
const url = anthropic ? (opts?.anthropicBase ?? ANTHROPIC_BASE) + "/v1/messages" : (opts?.openaiBase ?? OPENAI_BASE) + "/v1/chat/completions";
|
|
245
|
+
const headers = anthropic ? { "content-type": "application/json", "x-api-key": apiKey, "anthropic-version": ANTHROPIC_VERSION } : { "content-type": "application/json", authorization: `Bearer ${apiKey}` };
|
|
246
|
+
const scrub = (m) => apiKey ? m.split(apiKey).join("***") : m;
|
|
247
|
+
try {
|
|
248
|
+
const res = await fetch(url, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers,
|
|
251
|
+
body: JSON.stringify({ model: account.model, max_tokens: 1, messages: [{ role: "user", content: "ping" }] })
|
|
252
|
+
});
|
|
253
|
+
if (res.ok) {
|
|
254
|
+
void res.body?.cancel().catch(() => {
|
|
255
|
+
});
|
|
256
|
+
return { ok: true, message: `${account.provider} key ok (${account.model})` };
|
|
257
|
+
}
|
|
258
|
+
const msg = await providerMessage(res);
|
|
259
|
+
const label = res.status === 401 || res.status === 403 ? "invalid key" : res.status === 429 ? "rate limited" : `status ${res.status}`;
|
|
260
|
+
return { ok: false, message: scrub(`${label}${msg ? ": " + msg : ""}`) };
|
|
261
|
+
} catch (e) {
|
|
262
|
+
return { ok: false, message: scrub("network error: " + errMsg(e)) };
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function streamAnthropic(account, apiKey, ctx, run, opts) {
|
|
266
|
+
const res = await fetchWithRetry((opts?.anthropicBase ?? ANTHROPIC_BASE) + "/v1/messages", {
|
|
267
|
+
method: "POST",
|
|
268
|
+
headers: { "content-type": "application/json", "x-api-key": apiKey, "anthropic-version": ANTHROPIC_VERSION },
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
model: account.model,
|
|
271
|
+
system: ctx.system,
|
|
272
|
+
messages: ctx.messages,
|
|
273
|
+
max_tokens: ctx.maxOutputTokens,
|
|
274
|
+
stream: true
|
|
275
|
+
}),
|
|
276
|
+
signal: run.ac.signal
|
|
277
|
+
}, run);
|
|
278
|
+
if (!res) return;
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
await httpError(res, run);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (!res.body) {
|
|
284
|
+
run.error("internal", "provider sent no response body");
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
let finish = "end";
|
|
288
|
+
try {
|
|
289
|
+
await readSse(res.body, (data) => {
|
|
290
|
+
let ev;
|
|
291
|
+
try {
|
|
292
|
+
ev = JSON.parse(data);
|
|
293
|
+
} catch {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (ev?.type === "content_block_delta" && ev.delta?.type === "text_delta" && typeof ev.delta.text === "string") {
|
|
297
|
+
run.push(ev.delta.text);
|
|
298
|
+
} else if (ev?.type === "message_delta" && ev.delta?.stop_reason === "max_tokens") {
|
|
299
|
+
finish = "length";
|
|
300
|
+
} else if (ev?.type === "message_stop") {
|
|
301
|
+
run.done(finish);
|
|
302
|
+
} else if (ev?.type === "error") {
|
|
303
|
+
run.error(mapAnthropicSseError(ev.error?.type), String(ev.error?.message ?? "provider stream error"));
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
run.done(finish);
|
|
307
|
+
} catch (e) {
|
|
308
|
+
if (!run.aborted) run.error("network", errMsg(e));
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function mapAnthropicSseError(type) {
|
|
312
|
+
switch (type) {
|
|
313
|
+
case "overloaded_error":
|
|
314
|
+
return "overloaded";
|
|
315
|
+
case "rate_limit_error":
|
|
316
|
+
return "rate";
|
|
317
|
+
case "authentication_error":
|
|
318
|
+
case "permission_error":
|
|
319
|
+
return "auth";
|
|
320
|
+
case "invalid_request_error":
|
|
321
|
+
return "badRequest";
|
|
322
|
+
default:
|
|
323
|
+
return "internal";
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function streamOpenai(account, apiKey, ctx, run, opts) {
|
|
327
|
+
const res = await fetchWithRetry((opts?.openaiBase ?? OPENAI_BASE) + "/v1/chat/completions", {
|
|
328
|
+
method: "POST",
|
|
329
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${apiKey}` },
|
|
330
|
+
body: JSON.stringify({
|
|
331
|
+
model: account.model,
|
|
332
|
+
messages: [{ role: "system", content: ctx.system }, ...ctx.messages],
|
|
333
|
+
max_tokens: ctx.maxOutputTokens,
|
|
334
|
+
stream: true
|
|
335
|
+
}),
|
|
336
|
+
signal: run.ac.signal
|
|
337
|
+
}, run);
|
|
338
|
+
if (!res) return;
|
|
339
|
+
if (!res.ok) {
|
|
340
|
+
await httpError(res, run);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (!res.body) {
|
|
344
|
+
run.error("internal", "provider sent no response body");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
let finish = "end";
|
|
348
|
+
try {
|
|
349
|
+
await readSse(res.body, (data) => {
|
|
350
|
+
if (data === "[DONE]") {
|
|
351
|
+
run.done(finish);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
let ev;
|
|
355
|
+
try {
|
|
356
|
+
ev = JSON.parse(data);
|
|
357
|
+
} catch {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const choice = ev?.choices?.[0];
|
|
361
|
+
if (typeof choice?.delta?.content === "string") run.push(choice.delta.content);
|
|
362
|
+
if (choice?.finish_reason === "length") finish = "length";
|
|
363
|
+
});
|
|
364
|
+
run.done(finish);
|
|
365
|
+
} catch (e) {
|
|
366
|
+
if (!run.aborted) run.error("network", errMsg(e));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function executeClaudeCli(ctx, cb) {
|
|
370
|
+
const run = new Run(cb);
|
|
371
|
+
const transcript = ctx.messages.map((m) => m.role === "assistant" ? "[you]: " + m.content : m.content).join("\n");
|
|
372
|
+
const prompt = ctx.system + "\n\n" + transcript;
|
|
373
|
+
let child;
|
|
374
|
+
try {
|
|
375
|
+
const CHAT_ONLY_DISALLOW = "Bash Edit Write Read Glob Grep WebFetch WebSearch NotebookEdit Task MultiEdit";
|
|
376
|
+
const env = { ...process.env };
|
|
377
|
+
delete env.ANTHROPIC_API_KEY;
|
|
378
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
379
|
+
child = spawn("claude", ["-p", "--output-format", "stream-json", "--verbose", "--max-turns", "1", "--permission-mode", "plan", "--disallowedTools", CHAT_ONLY_DISALLOW], {
|
|
380
|
+
// prompt goes on stdin, never argv — argv is world-readable via `ps`, and
|
|
381
|
+
// the prompt carries the whole room transcript.
|
|
382
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
383
|
+
env
|
|
384
|
+
});
|
|
385
|
+
child.stdin?.on("error", () => {
|
|
386
|
+
});
|
|
387
|
+
child.stdin?.end(prompt);
|
|
388
|
+
} catch (e) {
|
|
389
|
+
queueMicrotask(() => run.error("internal", errMsg(e)));
|
|
390
|
+
return { abort: () => run.abort() };
|
|
391
|
+
}
|
|
392
|
+
let produced = false;
|
|
393
|
+
let stdoutBuf = "";
|
|
394
|
+
let stderrTail = "";
|
|
395
|
+
const handleLine = (line) => {
|
|
396
|
+
let ev;
|
|
397
|
+
try {
|
|
398
|
+
ev = JSON.parse(line);
|
|
399
|
+
} catch {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (ev?.type === "assistant") {
|
|
403
|
+
const content = ev.message?.content;
|
|
404
|
+
if (typeof content === "string" && content) {
|
|
405
|
+
produced = true;
|
|
406
|
+
run.push(content);
|
|
407
|
+
} else if (Array.isArray(content)) {
|
|
408
|
+
for (const block of content) {
|
|
409
|
+
if (block?.type === "text" && typeof block.text === "string" && block.text) {
|
|
410
|
+
produced = true;
|
|
411
|
+
run.push(block.text);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
} else if (ev?.type === "result" && typeof ev.result === "string" && ev.result && !produced) {
|
|
416
|
+
produced = true;
|
|
417
|
+
run.push(ev.result);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
child.stdout?.setEncoding("utf8");
|
|
421
|
+
child.stdout?.on("data", (chunk) => {
|
|
422
|
+
stdoutBuf += chunk;
|
|
423
|
+
let nl;
|
|
424
|
+
while ((nl = stdoutBuf.indexOf("\n")) >= 0) {
|
|
425
|
+
const line = stdoutBuf.slice(0, nl).trim();
|
|
426
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
427
|
+
if (line) handleLine(line);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
child.stderr?.setEncoding("utf8");
|
|
431
|
+
child.stderr?.on("data", (chunk) => {
|
|
432
|
+
stderrTail = (stderrTail + chunk).slice(-STDERR_TAIL);
|
|
433
|
+
});
|
|
434
|
+
child.on("error", (e) => {
|
|
435
|
+
if (e.code === "ENOENT") run.error("badRequest", "claude CLI not found");
|
|
436
|
+
else run.error("internal", errMsg(e));
|
|
437
|
+
});
|
|
438
|
+
child.on("close", (code) => {
|
|
439
|
+
if (run.aborted) return;
|
|
440
|
+
const rest = stdoutBuf.trim();
|
|
441
|
+
if (rest) handleLine(rest);
|
|
442
|
+
if (code !== 0 && !produced) run.error("internal", stderrTail.trim() || `claude exited with code ${code}`);
|
|
443
|
+
else run.done("end");
|
|
444
|
+
});
|
|
445
|
+
return {
|
|
446
|
+
abort: () => {
|
|
447
|
+
run.abort();
|
|
448
|
+
try {
|
|
449
|
+
child.kill("SIGTERM");
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
async function fetchWithRetry(url, init, run) {
|
|
456
|
+
for (let attempt = 0; ; attempt++) {
|
|
457
|
+
let res;
|
|
458
|
+
try {
|
|
459
|
+
res = await fetch(url, init);
|
|
460
|
+
} catch (e) {
|
|
461
|
+
if (!run.aborted && !run.isSettled) run.error("network", errMsg(e));
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
if (res.status !== 429) return res;
|
|
465
|
+
if (attempt >= 1) {
|
|
466
|
+
const msg = await providerMessage(res);
|
|
467
|
+
run.error("rate", msg || "rate limited (429)");
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
void res.body?.cancel().catch(() => {
|
|
471
|
+
});
|
|
472
|
+
await sleep(Math.min(retryAfterMs(res.headers.get("retry-after")), RETRY_CAP_MS), run.ac.signal);
|
|
473
|
+
if (run.aborted || run.isSettled) return null;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async function httpError(res, run) {
|
|
477
|
+
const msg = await providerMessage(res);
|
|
478
|
+
const st = res.status;
|
|
479
|
+
if (st === 401 || st === 403) run.error("auth", msg || `authentication failed (${st})`);
|
|
480
|
+
else if (st === 400) run.error("badRequest", msg || "bad request (400)");
|
|
481
|
+
else if (st >= 500) run.error("overloaded", msg || `provider overloaded (${st})`);
|
|
482
|
+
else run.error("badRequest", msg || `unexpected status ${st}`);
|
|
483
|
+
}
|
|
484
|
+
async function providerMessage(res) {
|
|
485
|
+
try {
|
|
486
|
+
const text = await res.text();
|
|
487
|
+
try {
|
|
488
|
+
const json = JSON.parse(text);
|
|
489
|
+
const m = json?.error?.message ?? json?.message;
|
|
490
|
+
if (typeof m === "string" && m) return m;
|
|
491
|
+
} catch {
|
|
492
|
+
}
|
|
493
|
+
return text.slice(0, 300);
|
|
494
|
+
} catch {
|
|
495
|
+
return "";
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function readSse(body, onData) {
|
|
499
|
+
const reader = body.getReader();
|
|
500
|
+
const dec = new TextDecoder();
|
|
501
|
+
let buf = "";
|
|
502
|
+
const drainLines = () => {
|
|
503
|
+
let nl;
|
|
504
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
505
|
+
const line = buf.slice(0, nl).replace(/\r$/, "");
|
|
506
|
+
buf = buf.slice(nl + 1);
|
|
507
|
+
if (line.startsWith("data:")) onData(line.slice(5).trim());
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
for (; ; ) {
|
|
511
|
+
const { done, value } = await reader.read();
|
|
512
|
+
if (done) break;
|
|
513
|
+
if (value) buf += dec.decode(value, { stream: true });
|
|
514
|
+
drainLines();
|
|
515
|
+
}
|
|
516
|
+
buf += dec.decode();
|
|
517
|
+
drainLines();
|
|
518
|
+
const last = buf.trim();
|
|
519
|
+
if (last.startsWith("data:")) onData(last.slice(5).trim());
|
|
520
|
+
}
|
|
521
|
+
function retryAfterMs(header) {
|
|
522
|
+
if (!header) return 1e3;
|
|
523
|
+
const secs = Number(header);
|
|
524
|
+
if (Number.isFinite(secs)) return Math.max(0, secs * 1e3);
|
|
525
|
+
const date = Date.parse(header);
|
|
526
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
527
|
+
return 1e3;
|
|
528
|
+
}
|
|
529
|
+
function sleep(ms, signal) {
|
|
530
|
+
return new Promise((resolve) => {
|
|
531
|
+
if (signal.aborted || ms <= 0) {
|
|
532
|
+
resolve();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const onAbort = () => {
|
|
536
|
+
clearTimeout(t);
|
|
537
|
+
resolve();
|
|
538
|
+
};
|
|
539
|
+
const t = setTimeout(() => {
|
|
540
|
+
signal.removeEventListener("abort", onAbort);
|
|
541
|
+
resolve();
|
|
542
|
+
}, ms);
|
|
543
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
function errMsg(e) {
|
|
547
|
+
return e instanceof Error ? e.message : String(e);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export {
|
|
551
|
+
DEFAULT_MODEL,
|
|
552
|
+
loadAccounts,
|
|
553
|
+
saveAccounts,
|
|
554
|
+
addAccount,
|
|
555
|
+
removeAccount,
|
|
556
|
+
loadTokens,
|
|
557
|
+
saveToken,
|
|
558
|
+
detectClaudeCode,
|
|
559
|
+
detectEnvKeys,
|
|
560
|
+
defaultHandle,
|
|
561
|
+
execute,
|
|
562
|
+
verifyAccount
|
|
563
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/protocol.ts
|
|
4
|
+
var VERSION = "3.0.0";
|
|
5
|
+
var PROTO = 3;
|
|
6
|
+
var PORT = 8787;
|
|
7
|
+
var DEFAULT_SERVER = "wss://accelr8-canvas.patonchain.workers.dev";
|
|
8
|
+
var CANVAS_W = 100;
|
|
9
|
+
var CANVAS_H = 30;
|
|
10
|
+
var LIMITS = {
|
|
11
|
+
chatMaxCp: 4e3,
|
|
12
|
+
// code points per chat message
|
|
13
|
+
chatMaxJson: 8192,
|
|
14
|
+
// serialized bytes per chat message
|
|
15
|
+
chatPerWindow: 10,
|
|
16
|
+
// messages per 10s per session
|
|
17
|
+
chatTail: 50,
|
|
18
|
+
// messages delivered in `joined`
|
|
19
|
+
chatPage: 50,
|
|
20
|
+
// default history page (max 100)
|
|
21
|
+
chatSyncMaxGap: 200,
|
|
22
|
+
// bigger gap -> full resync
|
|
23
|
+
chatRetain: 2e3,
|
|
24
|
+
// messages kept per project
|
|
25
|
+
draftMinIntervalMs: 100,
|
|
26
|
+
// server drops faster arrivals
|
|
27
|
+
draftMaxCp: 4e3,
|
|
28
|
+
framesPerWindow: 200,
|
|
29
|
+
// frames per 10s per session -> disconnect
|
|
30
|
+
invokesPerProjectMin: 20,
|
|
31
|
+
triggersPerUserMin: 6,
|
|
32
|
+
joinGuessesPerMin: 5,
|
|
33
|
+
revealsPerMin: 10,
|
|
34
|
+
secretsPerProject: 50,
|
|
35
|
+
secretValueMax: 8192,
|
|
36
|
+
agentQueueCap: 5,
|
|
37
|
+
agentMaxDepth: 3,
|
|
38
|
+
agentBudgetPerTrigger: 5,
|
|
39
|
+
agentRepeatPerChain: 2
|
|
40
|
+
};
|
|
41
|
+
var DEFAULT_ACCESS = { mode: "owner", users: [] };
|
|
42
|
+
function encode(msg) {
|
|
43
|
+
return JSON.stringify(msg);
|
|
44
|
+
}
|
|
45
|
+
function decode(data) {
|
|
46
|
+
return JSON.parse(data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
VERSION,
|
|
51
|
+
PROTO,
|
|
52
|
+
PORT,
|
|
53
|
+
DEFAULT_SERVER,
|
|
54
|
+
CANVAS_W,
|
|
55
|
+
CANVAS_H,
|
|
56
|
+
LIMITS,
|
|
57
|
+
DEFAULT_ACCESS,
|
|
58
|
+
encode,
|
|
59
|
+
decode
|
|
60
|
+
};
|