wasper-cli 0.1.0 → 0.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/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/cli.js +2536 -931
- package/dist/index.js +2134 -685
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2938,6 +2938,92 @@ function extractSecuritySchemes(doc) {
|
|
|
2938
2938
|
}
|
|
2939
2939
|
return result;
|
|
2940
2940
|
}
|
|
2941
|
+
function toEnvKey(str) {
|
|
2942
|
+
return str.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase()).replace(/[^a-zA-Z0-9_]/g, "") || str.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
|
|
2943
|
+
}
|
|
2944
|
+
function extractSuggestedVars(rawText, baseUrl) {
|
|
2945
|
+
let raw;
|
|
2946
|
+
try {
|
|
2947
|
+
raw = rawText.trimStart().startsWith("{") ? JSON.parse(rawText) : index_vite_proxy_tmp_default.load(rawText);
|
|
2948
|
+
} catch {
|
|
2949
|
+
return [];
|
|
2950
|
+
}
|
|
2951
|
+
const vars = [];
|
|
2952
|
+
const seen = new Set;
|
|
2953
|
+
const add = (key, value, description, source) => {
|
|
2954
|
+
const k = key.trim();
|
|
2955
|
+
if (!k || seen.has(k))
|
|
2956
|
+
return;
|
|
2957
|
+
seen.add(k);
|
|
2958
|
+
vars.push({ key: k, value, description, source });
|
|
2959
|
+
};
|
|
2960
|
+
if (baseUrl)
|
|
2961
|
+
add("baseUrl", baseUrl, "API base URL", "server");
|
|
2962
|
+
const servers = raw.servers ?? [];
|
|
2963
|
+
for (const server of servers.slice(0, 5)) {
|
|
2964
|
+
if (!server.variables)
|
|
2965
|
+
continue;
|
|
2966
|
+
for (const [key, def] of Object.entries(server.variables)) {
|
|
2967
|
+
add(toEnvKey(key), def.default ?? "", def.description ?? `Server variable: ${key}`, "server");
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
const components = raw.components ?? {};
|
|
2971
|
+
const secSchemes = components.securitySchemes ?? {};
|
|
2972
|
+
for (const [schemeName, scheme] of Object.entries(secSchemes)) {
|
|
2973
|
+
if (!scheme || typeof scheme !== "object")
|
|
2974
|
+
continue;
|
|
2975
|
+
const type = scheme.type;
|
|
2976
|
+
const slug = toEnvKey(schemeName);
|
|
2977
|
+
if (type === "http") {
|
|
2978
|
+
const httpScheme = (scheme.scheme ?? "").toLowerCase();
|
|
2979
|
+
if (httpScheme === "bearer") {
|
|
2980
|
+
add(`${slug}Token`, "", `Bearer token for ${schemeName}`, "auth");
|
|
2981
|
+
} else if (httpScheme === "basic") {
|
|
2982
|
+
add(`${slug}Username`, "", `Username for ${schemeName}`, "auth");
|
|
2983
|
+
add(`${slug}Password`, "", `Password for ${schemeName}`, "auth");
|
|
2984
|
+
}
|
|
2985
|
+
} else if (type === "apiKey") {
|
|
2986
|
+
const keyName = scheme.name ?? schemeName;
|
|
2987
|
+
add(toEnvKey(keyName), "", `API key header/param: ${keyName}`, "auth");
|
|
2988
|
+
} else if (type === "oauth2") {
|
|
2989
|
+
add(`${slug}ClientId`, "", `OAuth2 client ID for ${schemeName}`, "auth");
|
|
2990
|
+
add(`${slug}ClientSecret`, "", `OAuth2 client secret for ${schemeName}`, "auth");
|
|
2991
|
+
add(`${slug}Token`, "", `OAuth2 access token for ${schemeName}`, "auth");
|
|
2992
|
+
} else if (type === "openIdConnect") {
|
|
2993
|
+
add(`${slug}Token`, "", `Access token for ${schemeName}`, "auth");
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
const paths = raw.paths ?? {};
|
|
2997
|
+
const pathKeys = Object.keys(paths);
|
|
2998
|
+
const paramFreq = {};
|
|
2999
|
+
for (const [pathStr, pathItem] of Object.entries(paths)) {
|
|
3000
|
+
if (!pathItem || typeof pathItem !== "object")
|
|
3001
|
+
continue;
|
|
3002
|
+
const params = pathItem.parameters ?? [];
|
|
3003
|
+
const seenInPath = new Set;
|
|
3004
|
+
for (const p of params) {
|
|
3005
|
+
if (p.in === "path" && p.name && !seenInPath.has(p.name)) {
|
|
3006
|
+
seenInPath.add(p.name);
|
|
3007
|
+
paramFreq[p.name] = (paramFreq[p.name] ?? 0) + 1;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
for (const [, param] of pathStr.matchAll(/\{([^}]+)\}/g)) {
|
|
3011
|
+
if (!param)
|
|
3012
|
+
continue;
|
|
3013
|
+
if (!seenInPath.has(param)) {
|
|
3014
|
+
seenInPath.add(param);
|
|
3015
|
+
paramFreq[param] = (paramFreq[param] ?? 0) + 1;
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
const threshold = Math.max(3, Math.floor(pathKeys.length * 0.15));
|
|
3020
|
+
for (const [name, count] of Object.entries(paramFreq)) {
|
|
3021
|
+
if (count >= threshold) {
|
|
3022
|
+
add(toEnvKey(name), "", `Common path param: {${name}}`, "path");
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
return vars;
|
|
3026
|
+
}
|
|
2941
3027
|
var HTTP_METHODS;
|
|
2942
3028
|
var init_parser = __esm(() => {
|
|
2943
3029
|
init_js_yaml();
|
|
@@ -3045,6 +3131,41 @@ var SCHEMA = `
|
|
|
3045
3131
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
3046
3132
|
);
|
|
3047
3133
|
CREATE INDEX IF NOT EXISTS idx_saved_folder ON saved_requests(folder, created_at DESC);
|
|
3134
|
+
|
|
3135
|
+
CREATE TABLE IF NOT EXISTS spec_history (
|
|
3136
|
+
id TEXT PRIMARY KEY,
|
|
3137
|
+
url TEXT NOT NULL UNIQUE,
|
|
3138
|
+
title TEXT,
|
|
3139
|
+
version TEXT,
|
|
3140
|
+
endpoint_count INTEGER,
|
|
3141
|
+
last_used INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
3142
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
3143
|
+
);
|
|
3144
|
+
CREATE INDEX IF NOT EXISTS idx_spec_history_last_used ON spec_history(last_used DESC);
|
|
3145
|
+
|
|
3146
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
3147
|
+
id TEXT PRIMARY KEY,
|
|
3148
|
+
name TEXT NOT NULL DEFAULT 'Untitled Workflow',
|
|
3149
|
+
description TEXT NOT NULL DEFAULT '',
|
|
3150
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
3151
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
3152
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
3153
|
+
);
|
|
3154
|
+
CREATE INDEX IF NOT EXISTS idx_workflows_updated ON workflows(updated_at DESC);
|
|
3155
|
+
|
|
3156
|
+
CREATE TABLE IF NOT EXISTS capture_bins (
|
|
3157
|
+
id TEXT PRIMARY KEY,
|
|
3158
|
+
name TEXT NOT NULL DEFAULT '',
|
|
3159
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
3160
|
+
);
|
|
3161
|
+
|
|
3162
|
+
CREATE TABLE IF NOT EXISTS chat_memory (
|
|
3163
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
3164
|
+
role TEXT NOT NULL,
|
|
3165
|
+
content TEXT NOT NULL,
|
|
3166
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
3167
|
+
);
|
|
3168
|
+
CREATE INDEX IF NOT EXISTS idx_memory_created ON chat_memory(created_at DESC);
|
|
3048
3169
|
`;
|
|
3049
3170
|
|
|
3050
3171
|
// src/db/index.ts
|
|
@@ -3188,9 +3309,55 @@ var init_db = __esm(() => {
|
|
|
3188
3309
|
db.query(`UPDATE saved_requests SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
|
|
3189
3310
|
},
|
|
3190
3311
|
deleteSavedRequest: (id) => db.query("DELETE FROM saved_requests WHERE id = ?").run(id),
|
|
3312
|
+
getSpecHistory: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC").all(),
|
|
3313
|
+
getLastSpec: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC LIMIT 1").get(),
|
|
3314
|
+
upsertSpec: (url, title, version, endpointCount) => {
|
|
3315
|
+
const existing = db.query("SELECT id FROM spec_history WHERE url = ?").get(url);
|
|
3316
|
+
if (existing) {
|
|
3317
|
+
db.query("UPDATE spec_history SET title=?, version=?, endpoint_count=?, last_used=unixepoch() WHERE url=?").run(title, version, endpointCount, url);
|
|
3318
|
+
} else {
|
|
3319
|
+
db.query(`INSERT INTO spec_history (id, url, title, version, endpoint_count)
|
|
3320
|
+
VALUES (?, ?, ?, ?, ?)`).run(randomUUID2(), url, title, version, endpointCount);
|
|
3321
|
+
}
|
|
3322
|
+
},
|
|
3323
|
+
deleteSpec: (id) => {
|
|
3324
|
+
db.query("DELETE FROM spec_history WHERE id = ?").run(id);
|
|
3325
|
+
},
|
|
3326
|
+
getWorkflows: () => db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all(),
|
|
3327
|
+
getWorkflow: (id) => db.query("SELECT * FROM workflows WHERE id = ?").get(id),
|
|
3328
|
+
insertWorkflow: (w) => db.query("INSERT INTO workflows (id, name, description, steps) VALUES ($id, $name, $description, $steps)").run({ $id: w.id, $name: w.name, $description: w.description, $steps: w.steps }),
|
|
3329
|
+
updateWorkflow: (id, patch) => {
|
|
3330
|
+
const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
|
|
3331
|
+
const params = { $id: id };
|
|
3332
|
+
for (const [k, v] of Object.entries(patch))
|
|
3333
|
+
params[`$${k}`] = v;
|
|
3334
|
+
db.query(`UPDATE workflows SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
|
|
3335
|
+
},
|
|
3336
|
+
deleteWorkflow: (id) => db.query("DELETE FROM workflows WHERE id = ?").run(id),
|
|
3337
|
+
getCaptureBins: () => db.query("SELECT * FROM capture_bins ORDER BY created_at DESC").all(),
|
|
3338
|
+
getCaptureBin: (id) => db.query("SELECT * FROM capture_bins WHERE id = ?").get(id),
|
|
3339
|
+
insertCaptureBin: (id, name) => {
|
|
3340
|
+
db.query("INSERT INTO capture_bins (id, name) VALUES (?, ?)").run(id, name);
|
|
3341
|
+
},
|
|
3342
|
+
deleteCaptureBin: (id) => {
|
|
3343
|
+
db.query("DELETE FROM capture_bins WHERE id = ?").run(id);
|
|
3344
|
+
},
|
|
3191
3345
|
getSetting: (key) => (db.query("SELECT value FROM settings WHERE key = ?").get(key) ?? null)?.value ?? null,
|
|
3192
3346
|
setSetting: (key, value) => {
|
|
3193
3347
|
db.run("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
|
|
3348
|
+
},
|
|
3349
|
+
saveMemory: (role, content) => {
|
|
3350
|
+
db.query("INSERT INTO chat_memory (role, content) VALUES (?, ?)").run(role, content);
|
|
3351
|
+
},
|
|
3352
|
+
getMemory: (limit = 20) => {
|
|
3353
|
+
const rows = db.query("SELECT role, content FROM chat_memory ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
3354
|
+
return rows.reverse();
|
|
3355
|
+
},
|
|
3356
|
+
clearMemory: () => {
|
|
3357
|
+
db.query("DELETE FROM chat_memory").run();
|
|
3358
|
+
},
|
|
3359
|
+
trimMemory: (keepLast = 40) => {
|
|
3360
|
+
db.query("DELETE FROM chat_memory WHERE id NOT IN (SELECT id FROM chat_memory ORDER BY created_at DESC LIMIT ?)").run(keepLast);
|
|
3194
3361
|
}
|
|
3195
3362
|
};
|
|
3196
3363
|
});
|
|
@@ -3226,6 +3393,18 @@ class LogBus {
|
|
|
3226
3393
|
}
|
|
3227
3394
|
}
|
|
3228
3395
|
}
|
|
3396
|
+
broadcastServerEvent(payload) {
|
|
3397
|
+
if (this.clients.size === 0)
|
|
3398
|
+
return;
|
|
3399
|
+
const data = JSON.stringify({ type: "server_event", ...payload });
|
|
3400
|
+
for (const ws of this.clients) {
|
|
3401
|
+
try {
|
|
3402
|
+
ws.send(data);
|
|
3403
|
+
} catch {
|
|
3404
|
+
this.clients.delete(ws);
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3229
3408
|
get clientCount() {
|
|
3230
3409
|
return this.clients.size;
|
|
3231
3410
|
}
|
|
@@ -3428,7 +3607,7 @@ var package_default;
|
|
|
3428
3607
|
var init_package = __esm(() => {
|
|
3429
3608
|
package_default = {
|
|
3430
3609
|
name: "wasper-cli",
|
|
3431
|
-
version: "0.
|
|
3610
|
+
version: "0.3.0",
|
|
3432
3611
|
description: "Host an MCP server + API proxy from any OpenAPI spec. Like Drizzle Studio, but for APIs.",
|
|
3433
3612
|
type: "module",
|
|
3434
3613
|
homepage: "https://wasper.site",
|
|
@@ -4156,160 +4335,965 @@ var init_handler = __esm(() => {
|
|
|
4156
4335
|
};
|
|
4157
4336
|
});
|
|
4158
4337
|
|
|
4159
|
-
// src/
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4338
|
+
// src/proxy/capture.ts
|
|
4339
|
+
async function captureHandler(req) {
|
|
4340
|
+
if (req.method === "OPTIONS") {
|
|
4341
|
+
return new Response(null, { status: 204, headers: CORS3 });
|
|
4342
|
+
}
|
|
4343
|
+
const url = new URL(req.url);
|
|
4344
|
+
const binId = url.pathname.split("/").filter(Boolean)[1];
|
|
4345
|
+
if (!binId)
|
|
4346
|
+
return new Response("Not found", { status: 404 });
|
|
4347
|
+
const bin = dbQueries.getCaptureBin(binId);
|
|
4348
|
+
if (!bin) {
|
|
4349
|
+
return new Response(JSON.stringify({ error: "Capture bin not found or deleted" }), {
|
|
4350
|
+
status: 404,
|
|
4351
|
+
headers: { "Content-Type": "application/json", ...CORS3 }
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
const body = req.method !== "GET" && req.method !== "HEAD" ? await req.text().catch(() => null) : null;
|
|
4355
|
+
const reqHeaders = {};
|
|
4356
|
+
for (const [k, v] of req.headers.entries()) {
|
|
4357
|
+
if (!["host", "connection"].includes(k.toLowerCase()))
|
|
4358
|
+
reqHeaders[k] = v;
|
|
4359
|
+
}
|
|
4360
|
+
const id = randomUUID2();
|
|
4361
|
+
const now = Date.now();
|
|
4362
|
+
dbQueries.insertLog({
|
|
4363
|
+
id,
|
|
4364
|
+
source: "capture",
|
|
4365
|
+
tool_name: binId,
|
|
4366
|
+
method: req.method,
|
|
4367
|
+
url: req.url,
|
|
4368
|
+
request_headers: JSON.stringify(reqHeaders),
|
|
4369
|
+
request_body: body,
|
|
4370
|
+
status_code: 200,
|
|
4371
|
+
response_headers: null,
|
|
4372
|
+
response_body: null,
|
|
4373
|
+
latency_ms: 0,
|
|
4374
|
+
error: null
|
|
4375
|
+
});
|
|
4376
|
+
logBus.emit({
|
|
4377
|
+
id,
|
|
4378
|
+
source: "capture",
|
|
4379
|
+
tool_name: binId,
|
|
4380
|
+
method: req.method,
|
|
4381
|
+
url: req.url,
|
|
4382
|
+
request_headers: JSON.stringify(reqHeaders),
|
|
4383
|
+
request_body: body,
|
|
4384
|
+
status_code: 200,
|
|
4385
|
+
response_headers: null,
|
|
4386
|
+
response_body: null,
|
|
4387
|
+
latency_ms: 0,
|
|
4388
|
+
error: null,
|
|
4389
|
+
created_at: now
|
|
4390
|
+
});
|
|
4391
|
+
return new Response(JSON.stringify({ ok: true, id, captured_at: now }), {
|
|
4392
|
+
status: 200,
|
|
4164
4393
|
headers: { "Content-Type": "application/json", ...CORS3 }
|
|
4165
4394
|
});
|
|
4166
4395
|
}
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
if (path === "/api/logs" && method === "DELETE")
|
|
4183
|
-
return handleClearLogs();
|
|
4184
|
-
if (path === "/api/auth/profiles" && method === "GET")
|
|
4185
|
-
return handleGetProfiles();
|
|
4186
|
-
if (path === "/api/auth/profiles" && method === "POST")
|
|
4187
|
-
return handleCreateProfile(req);
|
|
4188
|
-
if (path.startsWith("/api/auth/profiles/") && method === "PUT")
|
|
4189
|
-
return handleUpdateProfile(req, path);
|
|
4190
|
-
if (path.startsWith("/api/auth/profiles/") && method === "DELETE")
|
|
4191
|
-
return handleDeleteProfile(path);
|
|
4192
|
-
if (path.match(/^\/api\/auth\/profiles\/[^/]+\/activate$/) && method === "POST")
|
|
4193
|
-
return handleActivateProfile(path);
|
|
4194
|
-
if (path === "/api/auth" && method === "GET")
|
|
4195
|
-
return handleGetAuth();
|
|
4196
|
-
if (path === "/api/auth" && method === "PUT")
|
|
4197
|
-
return handleSetAuth(req);
|
|
4198
|
-
if (path === "/api/auth/test" && method === "POST")
|
|
4199
|
-
return handleTestAuth();
|
|
4200
|
-
if (path === "/api/explorer/request" && method === "POST")
|
|
4201
|
-
return handleExplorerRequest(req);
|
|
4202
|
-
if (path === "/api/spec/endpoints" && method === "GET")
|
|
4203
|
-
return handleGetEndpoints();
|
|
4204
|
-
if (path === "/api/settings" && method === "GET")
|
|
4205
|
-
return handleGetSettings();
|
|
4206
|
-
if (path === "/api/settings" && method === "PUT")
|
|
4207
|
-
return handleSetSettings(req);
|
|
4208
|
-
if (path === "/api/spec/upload" && method === "POST")
|
|
4209
|
-
return handleSpecUpload(req);
|
|
4210
|
-
if (path === "/api/spec/reload-url" && method === "POST")
|
|
4211
|
-
return handleSpecReloadUrl(req);
|
|
4212
|
-
if (path === "/api/intercept" && method === "GET")
|
|
4213
|
-
return handleGetRules();
|
|
4214
|
-
if (path === "/api/intercept" && method === "POST")
|
|
4215
|
-
return handleCreateRule(req);
|
|
4216
|
-
if (path.startsWith("/api/intercept/") && method === "PUT")
|
|
4217
|
-
return handleUpdateRule(req, path);
|
|
4218
|
-
if (path.startsWith("/api/intercept/") && method === "DELETE")
|
|
4219
|
-
return handleDeleteRule(path);
|
|
4220
|
-
if (path === "/api/ai/chat" && method === "POST")
|
|
4221
|
-
return handleAiChat(req);
|
|
4222
|
-
if (path === "/api/debug/dns" && method === "GET")
|
|
4223
|
-
return handleDnsQuery(searchParams);
|
|
4224
|
-
if (path === "/api/debug/ping" && method === "GET")
|
|
4225
|
-
return handlePing(searchParams);
|
|
4226
|
-
if (path === "/api/reload" && method === "POST")
|
|
4227
|
-
return handleReload();
|
|
4228
|
-
if (path === "/api/server-info" && method === "GET")
|
|
4229
|
-
return handleServerInfo();
|
|
4230
|
-
if (path === "/api/features" && method === "GET")
|
|
4231
|
-
return json(getFeatures());
|
|
4232
|
-
if (path === "/api/features" && method === "PUT")
|
|
4233
|
-
return handleSetFeatures(req);
|
|
4234
|
-
if (path === "/api/saved" && method === "GET")
|
|
4235
|
-
return handleGetSaved();
|
|
4236
|
-
if (path === "/api/saved" && method === "POST")
|
|
4237
|
-
return handleCreateSaved(req);
|
|
4238
|
-
if (path.startsWith("/api/saved/") && method === "PUT")
|
|
4239
|
-
return handleUpdateSaved(req, path);
|
|
4240
|
-
if (path.startsWith("/api/saved/") && method === "DELETE")
|
|
4241
|
-
return handleDeleteSaved(path);
|
|
4242
|
-
return notFound("API route not found");
|
|
4396
|
+
var CORS3;
|
|
4397
|
+
var init_capture = __esm(() => {
|
|
4398
|
+
init_db();
|
|
4399
|
+
init_bus();
|
|
4400
|
+
CORS3 = {
|
|
4401
|
+
"Access-Control-Allow-Origin": "*",
|
|
4402
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD",
|
|
4403
|
+
"Access-Control-Allow-Headers": "*",
|
|
4404
|
+
"Access-Control-Expose-Headers": "*"
|
|
4405
|
+
};
|
|
4406
|
+
});
|
|
4407
|
+
|
|
4408
|
+
// src/workflows/engine.ts
|
|
4409
|
+
function interpolate(val, ctx) {
|
|
4410
|
+
return val.replace(/\{\{(\w+)\}\}/g, (_, k) => ctx[k] ?? `{{${k}}}`);
|
|
4243
4411
|
}
|
|
4244
|
-
function
|
|
4245
|
-
if (
|
|
4246
|
-
return
|
|
4412
|
+
function interpolateDeep(obj, ctx) {
|
|
4413
|
+
if (typeof obj === "string")
|
|
4414
|
+
return interpolate(obj, ctx);
|
|
4415
|
+
if (Array.isArray(obj))
|
|
4416
|
+
return obj.map((v) => interpolateDeep(v, ctx));
|
|
4417
|
+
if (obj !== null && typeof obj === "object") {
|
|
4418
|
+
const out = {};
|
|
4419
|
+
for (const [k, v] of Object.entries(obj))
|
|
4420
|
+
out[k] = interpolateDeep(v, ctx);
|
|
4421
|
+
return out;
|
|
4247
4422
|
}
|
|
4248
|
-
|
|
4249
|
-
return json({
|
|
4250
|
-
ok: true,
|
|
4251
|
-
version: VERSION,
|
|
4252
|
-
spec: { title: spec.title, version: spec.version, baseUrl: spec.baseUrl, url: spec.url },
|
|
4253
|
-
endpointCount: operations.length,
|
|
4254
|
-
wsClients: logBus.clientCount
|
|
4255
|
-
});
|
|
4423
|
+
return obj;
|
|
4256
4424
|
}
|
|
4257
|
-
|
|
4258
|
-
if (!
|
|
4259
|
-
return
|
|
4260
|
-
const
|
|
4261
|
-
if (!
|
|
4262
|
-
return
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4425
|
+
function resolveJsonPath(data, path) {
|
|
4426
|
+
if (!path || path === "$")
|
|
4427
|
+
return data;
|
|
4428
|
+
const normalized = path.startsWith("$.") ? path.slice(2) : path.startsWith("$[") ? path.slice(1) : path;
|
|
4429
|
+
if (!normalized)
|
|
4430
|
+
return data;
|
|
4431
|
+
const parts = [];
|
|
4432
|
+
for (const seg of normalized.split(".")) {
|
|
4433
|
+
const m = seg.match(/^(\w+)\[(\d+)\]$/);
|
|
4434
|
+
if (m) {
|
|
4435
|
+
parts.push(m[1], parseInt(m[2], 10));
|
|
4436
|
+
} else if (/^\d+$/.test(seg)) {
|
|
4437
|
+
parts.push(parseInt(seg, 10));
|
|
4438
|
+
} else {
|
|
4439
|
+
parts.push(seg);
|
|
4440
|
+
}
|
|
4441
|
+
}
|
|
4442
|
+
let cur = data;
|
|
4443
|
+
for (const part of parts) {
|
|
4444
|
+
if (cur === null || cur === undefined)
|
|
4445
|
+
return;
|
|
4446
|
+
cur = typeof part === "number" ? cur[part] : cur[part];
|
|
4268
4447
|
}
|
|
4448
|
+
return cur;
|
|
4269
4449
|
}
|
|
4270
|
-
async function
|
|
4271
|
-
|
|
4450
|
+
async function executeStep(step, ctx) {
|
|
4451
|
+
const { spec } = getState();
|
|
4452
|
+
let base = spec.baseUrl;
|
|
4453
|
+
if (!base?.startsWith("http") && spec.url) {
|
|
4454
|
+
try {
|
|
4455
|
+
base = new URL(spec.url).origin;
|
|
4456
|
+
} catch {}
|
|
4457
|
+
}
|
|
4458
|
+
if (!base?.startsWith("http"))
|
|
4459
|
+
throw new Error("Spec has no absolute server URL");
|
|
4460
|
+
let urlPath = step.path;
|
|
4461
|
+
for (const [k, v] of Object.entries(step.pathParams ?? {})) {
|
|
4462
|
+
urlPath = urlPath.replace(`{${k}}`, encodeURIComponent(interpolate(v, ctx)));
|
|
4463
|
+
}
|
|
4464
|
+
urlPath = interpolate(urlPath, ctx);
|
|
4465
|
+
const urlObj = new URL(`${base.replace(/\/$/, "")}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`);
|
|
4466
|
+
for (const [k, v] of Object.entries(step.queryParams ?? {})) {
|
|
4467
|
+
urlObj.searchParams.set(k, interpolate(v, ctx));
|
|
4468
|
+
}
|
|
4469
|
+
const stepHeaders = {};
|
|
4470
|
+
for (const [k, v] of Object.entries(step.headers ?? {})) {
|
|
4471
|
+
stepHeaders[k] = interpolate(v, ctx);
|
|
4472
|
+
}
|
|
4473
|
+
const authRow = dbQueries.getAuthConfig();
|
|
4474
|
+
const authConfig = authRow ? JSON.parse(authRow.config) : { type: "none" };
|
|
4475
|
+
const { url: authedUrl, headers: authedHeaders } = await applyAuth(urlObj.toString(), stepHeaders, authConfig);
|
|
4476
|
+
const noBodyMethod = ["GET", "HEAD", "OPTIONS"].includes(step.method.toUpperCase());
|
|
4477
|
+
let bodyStr;
|
|
4478
|
+
if (!noBodyMethod && step.body !== undefined && step.body !== null) {
|
|
4479
|
+
const interpolated = interpolateDeep(step.body, ctx);
|
|
4480
|
+
bodyStr = typeof interpolated === "string" ? interpolated : JSON.stringify(interpolated);
|
|
4481
|
+
if (!authedHeaders["Content-Type"] && !authedHeaders["content-type"]) {
|
|
4482
|
+
authedHeaders["Content-Type"] = "application/json";
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
const start = Date.now();
|
|
4486
|
+
const res = await fetch(authedUrl, {
|
|
4487
|
+
method: step.method.toUpperCase(),
|
|
4488
|
+
headers: authedHeaders,
|
|
4489
|
+
...bodyStr !== undefined ? { body: bodyStr } : {},
|
|
4490
|
+
signal: AbortSignal.timeout(30000)
|
|
4491
|
+
});
|
|
4492
|
+
const latency = Date.now() - start;
|
|
4493
|
+
const responseHeaders = {};
|
|
4494
|
+
res.headers.forEach((v, k) => {
|
|
4495
|
+
responseHeaders[k] = v;
|
|
4496
|
+
});
|
|
4497
|
+
const responseText = await res.text();
|
|
4498
|
+
let responseData;
|
|
4272
4499
|
try {
|
|
4273
|
-
|
|
4500
|
+
responseData = JSON.parse(responseText);
|
|
4274
4501
|
} catch {
|
|
4275
|
-
|
|
4502
|
+
responseData = responseText;
|
|
4276
4503
|
}
|
|
4277
|
-
const
|
|
4278
|
-
for (const
|
|
4279
|
-
|
|
4280
|
-
|
|
4504
|
+
const extractedVars = {};
|
|
4505
|
+
for (const ext of step.extract ?? []) {
|
|
4506
|
+
const val = resolveJsonPath(responseData, ext.path);
|
|
4507
|
+
if (val !== undefined && val !== null) {
|
|
4508
|
+
extractedVars[ext.var] = typeof val === "string" ? val : JSON.stringify(val);
|
|
4509
|
+
}
|
|
4281
4510
|
}
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
}
|
|
4298
|
-
}
|
|
4511
|
+
const assertions = [];
|
|
4512
|
+
for (const a of step.assert ?? []) {
|
|
4513
|
+
if (a.type === "status") {
|
|
4514
|
+
const expected = a.statusCode ?? 200;
|
|
4515
|
+
const pass2 = res.status === expected;
|
|
4516
|
+
assertions.push({ pass: pass2, message: `HTTP ${res.status} ${pass2 ? "==" : "!="} ${expected}` });
|
|
4517
|
+
} else if (a.type === "json") {
|
|
4518
|
+
const val = resolveJsonPath(responseData, a.path ?? "$");
|
|
4519
|
+
if ("eq" in a) {
|
|
4520
|
+
const pass2 = JSON.stringify(val) === JSON.stringify(a.eq);
|
|
4521
|
+
assertions.push({ pass: pass2, message: `${a.path} ${pass2 ? "==" : "!="} ${JSON.stringify(a.eq)}` });
|
|
4522
|
+
} else if ("contains" in a && typeof val === "string") {
|
|
4523
|
+
const pass2 = val.includes(a.contains);
|
|
4524
|
+
assertions.push({ pass: pass2, message: `${a.path} ${pass2 ? "contains" : "doesn't contain"} "${a.contains}"` });
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
const pass = assertions.length === 0 ? res.ok : assertions.every((a) => a.pass);
|
|
4529
|
+
return {
|
|
4530
|
+
stepId: step.id,
|
|
4531
|
+
label: step.label,
|
|
4532
|
+
method: step.method,
|
|
4533
|
+
resolvedPath: urlPath,
|
|
4534
|
+
requestUrl: authedUrl,
|
|
4535
|
+
requestHeaders: authedHeaders,
|
|
4536
|
+
requestBody: bodyStr,
|
|
4537
|
+
status: res.status,
|
|
4538
|
+
statusText: res.statusText,
|
|
4539
|
+
responseHeaders,
|
|
4540
|
+
latency,
|
|
4541
|
+
extractedVars,
|
|
4542
|
+
assertions,
|
|
4543
|
+
pass,
|
|
4544
|
+
responseBody: responseText.slice(0, 1e4)
|
|
4545
|
+
};
|
|
4299
4546
|
}
|
|
4300
|
-
async function
|
|
4301
|
-
|
|
4302
|
-
let
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4547
|
+
async function runWorkflow(steps, emit, signal) {
|
|
4548
|
+
const ctx = {};
|
|
4549
|
+
let passed = 0;
|
|
4550
|
+
emit({ type: "run_start", totalSteps: steps.length });
|
|
4551
|
+
for (const step of steps) {
|
|
4552
|
+
if (signal?.aborted) {
|
|
4553
|
+
emit({ type: "run_aborted", message: "Run cancelled" });
|
|
4554
|
+
return;
|
|
4555
|
+
}
|
|
4556
|
+
emit({ type: "step_start", stepId: step.id, label: step.label, method: step.method, path: step.path });
|
|
4306
4557
|
try {
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4558
|
+
const result = await executeStep(step, ctx);
|
|
4559
|
+
for (const [k, v] of Object.entries(result.extractedVars))
|
|
4560
|
+
ctx[k] = v;
|
|
4561
|
+
ctx[`${step.id}_status`] = String(result.status ?? "");
|
|
4562
|
+
if (result.pass)
|
|
4563
|
+
passed++;
|
|
4564
|
+
emit({
|
|
4565
|
+
type: "step_done",
|
|
4566
|
+
stepId: result.stepId,
|
|
4567
|
+
label: result.label,
|
|
4568
|
+
method: result.method,
|
|
4569
|
+
resolvedPath: result.resolvedPath,
|
|
4570
|
+
requestUrl: result.requestUrl,
|
|
4571
|
+
requestHeaders: result.requestHeaders,
|
|
4572
|
+
requestBody: result.requestBody,
|
|
4573
|
+
status: result.status,
|
|
4574
|
+
statusText: result.statusText,
|
|
4575
|
+
responseHeaders: result.responseHeaders,
|
|
4576
|
+
latency: result.latency,
|
|
4577
|
+
extractedVars: result.extractedVars,
|
|
4578
|
+
assertions: result.assertions,
|
|
4579
|
+
pass: result.pass,
|
|
4580
|
+
responseBody: result.responseBody
|
|
4581
|
+
});
|
|
4582
|
+
} catch (e) {
|
|
4583
|
+
emit({
|
|
4584
|
+
type: "step_error",
|
|
4585
|
+
stepId: step.id,
|
|
4586
|
+
label: step.label,
|
|
4587
|
+
method: step.method,
|
|
4588
|
+
path: step.path,
|
|
4589
|
+
error: e instanceof Error ? e.message : String(e),
|
|
4590
|
+
pass: false
|
|
4591
|
+
});
|
|
4310
4592
|
}
|
|
4311
|
-
|
|
4312
|
-
|
|
4593
|
+
}
|
|
4594
|
+
emit({ type: "run_done", totalSteps: steps.length, passedSteps: passed });
|
|
4595
|
+
}
|
|
4596
|
+
var init_engine2 = __esm(() => {
|
|
4597
|
+
init_engine();
|
|
4598
|
+
init_db();
|
|
4599
|
+
init_state();
|
|
4600
|
+
});
|
|
4601
|
+
|
|
4602
|
+
// src/agent/harness.ts
|
|
4603
|
+
function mergeSignals(a, b) {
|
|
4604
|
+
if (!a && !b)
|
|
4605
|
+
return new AbortController().signal;
|
|
4606
|
+
if (!a)
|
|
4607
|
+
return b;
|
|
4608
|
+
if (!b)
|
|
4609
|
+
return a;
|
|
4610
|
+
const ctrl = new AbortController;
|
|
4611
|
+
const abort = () => ctrl.abort();
|
|
4612
|
+
a.addEventListener("abort", abort, { once: true });
|
|
4613
|
+
b.addEventListener("abort", abort, { once: true });
|
|
4614
|
+
return ctrl.signal;
|
|
4615
|
+
}
|
|
4616
|
+
async function fetchWithRetry(url, opts, emit, signal, maxRetries = 4) {
|
|
4617
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
4618
|
+
const stepSignal = signal;
|
|
4619
|
+
let res;
|
|
4620
|
+
try {
|
|
4621
|
+
res = await fetch(url, { ...opts, signal: stepSignal });
|
|
4622
|
+
} catch (e) {
|
|
4623
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4624
|
+
if (signal?.aborted)
|
|
4625
|
+
throw e;
|
|
4626
|
+
if (attempt === maxRetries)
|
|
4627
|
+
throw e;
|
|
4628
|
+
const isNetwork = msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("network") || msg.includes("fetch");
|
|
4629
|
+
if (!isNetwork)
|
|
4630
|
+
throw e;
|
|
4631
|
+
const delay2 = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 300, 15000);
|
|
4632
|
+
emit({ type: "info", message: `Network error, retrying in ${Math.round(delay2 / 1000)}s\u2026` });
|
|
4633
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
4634
|
+
continue;
|
|
4635
|
+
}
|
|
4636
|
+
if (!RETRYABLE_STATUS.has(res.status) || attempt === maxRetries)
|
|
4637
|
+
return res;
|
|
4638
|
+
const retryAfter = parseInt(res.headers.get("retry-after") ?? "0", 10);
|
|
4639
|
+
const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * Math.pow(2, attempt) + Math.random() * 300, 30000);
|
|
4640
|
+
const label = res.status === 429 ? "Rate limited" : `Server error ${res.status}`;
|
|
4641
|
+
emit({ type: "info", message: `${label} \u2014 retrying in ${Math.round(delay / 1000)}s\u2026 (attempt ${attempt + 1}/${maxRetries})` });
|
|
4642
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
4643
|
+
}
|
|
4644
|
+
return fetch(url, opts);
|
|
4645
|
+
}
|
|
4646
|
+
function trimContext(messages) {
|
|
4647
|
+
if (JSON.stringify(messages).length <= MAX_CONTEXT_CHARS)
|
|
4648
|
+
return { messages, trimmed: false };
|
|
4649
|
+
const result = [...messages];
|
|
4650
|
+
while (JSON.stringify(result).length > MAX_CONTEXT_CHARS && result.length > 2) {
|
|
4651
|
+
let removed = false;
|
|
4652
|
+
const toolIdx = result.findIndex((m) => m.role === "tool");
|
|
4653
|
+
if (toolIdx !== -1) {
|
|
4654
|
+
result.splice(toolIdx, 1);
|
|
4655
|
+
if (toolIdx > 0) {
|
|
4656
|
+
const prev = result[toolIdx - 1];
|
|
4657
|
+
if (prev?.role === "assistant") {
|
|
4658
|
+
const tc = prev.tool_calls;
|
|
4659
|
+
if (Array.isArray(tc) && tc.length)
|
|
4660
|
+
result.splice(toolIdx - 1, 1);
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4663
|
+
removed = true;
|
|
4664
|
+
}
|
|
4665
|
+
if (!removed) {
|
|
4666
|
+
const anthropicIdx = result.findIndex((m) => {
|
|
4667
|
+
if (m.role !== "user")
|
|
4668
|
+
return false;
|
|
4669
|
+
const c = m.content;
|
|
4670
|
+
return Array.isArray(c) && c.some((b) => b.type === "tool_result");
|
|
4671
|
+
});
|
|
4672
|
+
if (anthropicIdx !== -1) {
|
|
4673
|
+
result.splice(anthropicIdx, 1);
|
|
4674
|
+
if (anthropicIdx > 0 && result[anthropicIdx - 1]?.role === "assistant") {
|
|
4675
|
+
result.splice(anthropicIdx - 1, 1);
|
|
4676
|
+
}
|
|
4677
|
+
removed = true;
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
if (!removed)
|
|
4681
|
+
break;
|
|
4682
|
+
}
|
|
4683
|
+
return { messages: result, trimmed: true };
|
|
4684
|
+
}
|
|
4685
|
+
async function* readSSE(body) {
|
|
4686
|
+
const reader = body.getReader();
|
|
4687
|
+
const decoder = new TextDecoder;
|
|
4688
|
+
let buf = "";
|
|
4689
|
+
try {
|
|
4690
|
+
while (true) {
|
|
4691
|
+
const { done, value } = await reader.read();
|
|
4692
|
+
if (done)
|
|
4693
|
+
break;
|
|
4694
|
+
buf += decoder.decode(value, { stream: true });
|
|
4695
|
+
const parts = buf.split(`
|
|
4696
|
+
|
|
4697
|
+
`);
|
|
4698
|
+
buf = parts.pop() ?? "";
|
|
4699
|
+
for (const part of parts) {
|
|
4700
|
+
let data = "";
|
|
4701
|
+
for (const line of part.split(`
|
|
4702
|
+
`)) {
|
|
4703
|
+
if (line.startsWith("data: ")) {
|
|
4704
|
+
data = line.slice(6);
|
|
4705
|
+
break;
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
if (!data || data === "[DONE]")
|
|
4709
|
+
continue;
|
|
4710
|
+
try {
|
|
4711
|
+
yield JSON.parse(data);
|
|
4712
|
+
} catch {}
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
} finally {
|
|
4716
|
+
reader.releaseLock();
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
function buildAnthropicTools(schemas) {
|
|
4720
|
+
return schemas.map((s) => ({
|
|
4721
|
+
name: s.name,
|
|
4722
|
+
description: s.description,
|
|
4723
|
+
input_schema: { type: "object", properties: s.params, required: s.required }
|
|
4724
|
+
}));
|
|
4725
|
+
}
|
|
4726
|
+
async function streamAnthropic(cfg, system, messages, tools, emit, signal) {
|
|
4727
|
+
const base = (cfg.baseUrl || "https://api.anthropic.com").replace(/\/$/, "");
|
|
4728
|
+
const systemContent = cfg.enablePromptCache ? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }] : system;
|
|
4729
|
+
const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
|
|
4730
|
+
const res = await fetchWithRetry(`${base}/v1/messages`, {
|
|
4731
|
+
method: "POST",
|
|
4732
|
+
headers: {
|
|
4733
|
+
"Content-Type": "application/json",
|
|
4734
|
+
"x-api-key": cfg.apiKey,
|
|
4735
|
+
"anthropic-version": "2023-06-01",
|
|
4736
|
+
...cfg.enablePromptCache ? { "anthropic-beta": "prompt-caching-1-0" } : {},
|
|
4737
|
+
...cfg.extraHeaders
|
|
4738
|
+
},
|
|
4739
|
+
body: JSON.stringify({
|
|
4740
|
+
model: cfg.model,
|
|
4741
|
+
max_tokens: cfg.maxTokens,
|
|
4742
|
+
temperature: cfg.temperature,
|
|
4743
|
+
...cfg.topK > 0 ? { top_k: cfg.topK } : {},
|
|
4744
|
+
system: systemContent,
|
|
4745
|
+
messages,
|
|
4746
|
+
tools,
|
|
4747
|
+
stream: true
|
|
4748
|
+
})
|
|
4749
|
+
}, emit, stepSignal);
|
|
4750
|
+
if (!res.ok) {
|
|
4751
|
+
const body = await res.text();
|
|
4752
|
+
const retryable = RETRYABLE_STATUS.has(res.status);
|
|
4753
|
+
throw Object.assign(new Error(`Anthropic ${res.status}: ${body}`), { retryable });
|
|
4754
|
+
}
|
|
4755
|
+
const result = { text: "", thinking: "", stopReason: "", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
4756
|
+
const blocks = [];
|
|
4757
|
+
const inputAccum = {};
|
|
4758
|
+
for await (const ev of readSSE(res.body)) {
|
|
4759
|
+
if (signal.aborted)
|
|
4760
|
+
break;
|
|
4761
|
+
const evType = ev.type;
|
|
4762
|
+
if (evType === "message_start") {
|
|
4763
|
+
const usage = ev.message?.usage;
|
|
4764
|
+
if (usage) {
|
|
4765
|
+
result.usage.input = usage.input_tokens ?? 0;
|
|
4766
|
+
result.usage.cacheRead = usage.cache_read_input_tokens ?? 0;
|
|
4767
|
+
result.usage.cacheWrite = usage.cache_creation_input_tokens ?? 0;
|
|
4768
|
+
}
|
|
4769
|
+
} else if (evType === "content_block_start") {
|
|
4770
|
+
const idx = ev.index;
|
|
4771
|
+
const cb = ev.content_block;
|
|
4772
|
+
blocks[idx] = { type: cb.type, id: cb.id, name: cb.name };
|
|
4773
|
+
if (cb.type === "tool_use")
|
|
4774
|
+
inputAccum[idx] = "";
|
|
4775
|
+
} else if (evType === "content_block_delta") {
|
|
4776
|
+
const idx = ev.index;
|
|
4777
|
+
const delta = ev.delta;
|
|
4778
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
4779
|
+
result.text += delta.text;
|
|
4780
|
+
if (!blocks[idx])
|
|
4781
|
+
blocks[idx] = { type: "text" };
|
|
4782
|
+
blocks[idx].text = (blocks[idx].text ?? "") + delta.text;
|
|
4783
|
+
emit({ type: "text_delta", text: delta.text });
|
|
4784
|
+
} else if (delta.type === "thinking_delta" && delta.thinking) {
|
|
4785
|
+
result.thinking += delta.thinking;
|
|
4786
|
+
emit({ type: "thinking", text: delta.thinking });
|
|
4787
|
+
} else if (delta.type === "input_json_delta" && delta.partial_json) {
|
|
4788
|
+
inputAccum[idx] = (inputAccum[idx] ?? "") + delta.partial_json;
|
|
4789
|
+
}
|
|
4790
|
+
} else if (evType === "content_block_stop") {
|
|
4791
|
+
const idx = ev.index;
|
|
4792
|
+
if (blocks[idx]?.type === "tool_use") {
|
|
4793
|
+
try {
|
|
4794
|
+
blocks[idx].input = JSON.parse(inputAccum[idx] ?? "{}");
|
|
4795
|
+
} catch {
|
|
4796
|
+
blocks[idx].input = {};
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
} else if (evType === "message_delta") {
|
|
4800
|
+
const delta = ev.delta;
|
|
4801
|
+
const usage = ev.usage;
|
|
4802
|
+
if (delta.stop_reason)
|
|
4803
|
+
result.stopReason = delta.stop_reason;
|
|
4804
|
+
if (usage?.output_tokens)
|
|
4805
|
+
result.usage.output = usage.output_tokens;
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
for (const b of blocks) {
|
|
4809
|
+
if (b.type === "tool_use" && b.id && b.name) {
|
|
4810
|
+
result.toolUses.push({ id: b.id, name: b.name, input: b.input ?? {} });
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
result._anthropicBlocks = blocks;
|
|
4814
|
+
return result;
|
|
4815
|
+
}
|
|
4816
|
+
function buildOpenAITools(schemas) {
|
|
4817
|
+
return schemas.map((s) => ({
|
|
4818
|
+
type: "function",
|
|
4819
|
+
function: { name: s.name, description: s.description, parameters: { type: "object", properties: s.params, required: s.required } }
|
|
4820
|
+
}));
|
|
4821
|
+
}
|
|
4822
|
+
async function streamOpenAI(cfg, system, messages, tools, emit, signal) {
|
|
4823
|
+
const providerBases = {
|
|
4824
|
+
openai: "https://api.openai.com",
|
|
4825
|
+
mistral: "https://api.mistral.ai",
|
|
4826
|
+
groq: "https://api.groq.com/openai",
|
|
4827
|
+
"github-copilot": "https://api.githubcopilot.com"
|
|
4828
|
+
};
|
|
4829
|
+
const base = (cfg.baseUrl || providerBases[cfg.provider] || "https://api.openai.com").replace(/\/$/, "");
|
|
4830
|
+
const providerHeaders = cfg.provider === "github-copilot" ? { "Copilot-Integration-Id": "vscode-chat", "Editor-Version": "vscode/1.85.0" } : {};
|
|
4831
|
+
const authHeaders = cfg.apiKey ? { Authorization: `Bearer ${cfg.apiKey}` } : {};
|
|
4832
|
+
const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
|
|
4833
|
+
const res = await fetchWithRetry(`${base}/v1/chat/completions`, {
|
|
4834
|
+
method: "POST",
|
|
4835
|
+
headers: { "Content-Type": "application/json", ...authHeaders, ...providerHeaders, ...cfg.extraHeaders },
|
|
4836
|
+
body: JSON.stringify({
|
|
4837
|
+
model: cfg.model,
|
|
4838
|
+
max_tokens: cfg.maxTokens,
|
|
4839
|
+
temperature: cfg.temperature,
|
|
4840
|
+
messages: [{ role: "system", content: system }, ...messages],
|
|
4841
|
+
tools,
|
|
4842
|
+
tool_choice: "auto",
|
|
4843
|
+
stream: true,
|
|
4844
|
+
stream_options: { include_usage: true }
|
|
4845
|
+
})
|
|
4846
|
+
}, emit, stepSignal);
|
|
4847
|
+
if (!res.ok) {
|
|
4848
|
+
const body = await res.text();
|
|
4849
|
+
const retryable = RETRYABLE_STATUS.has(res.status);
|
|
4850
|
+
throw Object.assign(new Error(body), { retryable });
|
|
4851
|
+
}
|
|
4852
|
+
const result = { text: "", thinking: "", stopReason: "", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
4853
|
+
const tcAccum = {};
|
|
4854
|
+
for await (const ev of readSSE(res.body)) {
|
|
4855
|
+
if (signal.aborted)
|
|
4856
|
+
break;
|
|
4857
|
+
if (ev.object === "error") {
|
|
4858
|
+
const retryable = ev.code === "1300" || ev.raw_status_code === 429 || ev.raw_status_code === 503;
|
|
4859
|
+
throw Object.assign(new Error(JSON.stringify(ev)), { retryable });
|
|
4860
|
+
}
|
|
4861
|
+
if (ev.usage) {
|
|
4862
|
+
const u = ev.usage;
|
|
4863
|
+
result.usage.input = u.prompt_tokens ?? 0;
|
|
4864
|
+
result.usage.output = u.completion_tokens ?? 0;
|
|
4865
|
+
}
|
|
4866
|
+
const choices = ev.choices;
|
|
4867
|
+
const choice = choices?.[0];
|
|
4868
|
+
if (!choice)
|
|
4869
|
+
continue;
|
|
4870
|
+
const fr = choice.finish_reason;
|
|
4871
|
+
if (fr)
|
|
4872
|
+
result.stopReason = fr;
|
|
4873
|
+
const delta = choice.delta;
|
|
4874
|
+
if (!delta)
|
|
4875
|
+
continue;
|
|
4876
|
+
if (typeof delta.content === "string" && delta.content) {
|
|
4877
|
+
result.text += delta.content;
|
|
4878
|
+
emit({ type: "text_delta", text: delta.content });
|
|
4879
|
+
}
|
|
4880
|
+
const tcDeltas = delta.tool_calls;
|
|
4881
|
+
if (tcDeltas) {
|
|
4882
|
+
for (const tc of tcDeltas) {
|
|
4883
|
+
if (!tcAccum[tc.index])
|
|
4884
|
+
tcAccum[tc.index] = { id: "", name: "", args: "" };
|
|
4885
|
+
const e = tcAccum[tc.index];
|
|
4886
|
+
if (tc.id)
|
|
4887
|
+
e.id += tc.id;
|
|
4888
|
+
if (tc.function?.name)
|
|
4889
|
+
e.name += tc.function.name;
|
|
4890
|
+
if (tc.function?.arguments)
|
|
4891
|
+
e.args += tc.function.arguments;
|
|
4892
|
+
}
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
for (const tc of Object.values(tcAccum)) {
|
|
4896
|
+
let input = {};
|
|
4897
|
+
try {
|
|
4898
|
+
input = JSON.parse(tc.args);
|
|
4899
|
+
} catch {}
|
|
4900
|
+
result.toolUses.push({ id: tc.id, name: tc.name, input });
|
|
4901
|
+
}
|
|
4902
|
+
result._openaiTcAccum = tcAccum;
|
|
4903
|
+
return result;
|
|
4904
|
+
}
|
|
4905
|
+
async function callOllama(cfg, system, messages, emit, signal) {
|
|
4906
|
+
const base = (cfg.baseUrl || "http://localhost:11434").replace(/\/$/, "");
|
|
4907
|
+
const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
|
|
4908
|
+
const res = await fetchWithRetry(`${base}/api/chat`, {
|
|
4909
|
+
method: "POST",
|
|
4910
|
+
headers: { "Content-Type": "application/json" },
|
|
4911
|
+
body: JSON.stringify({ model: cfg.model, messages: [{ role: "system", content: system }, ...messages], stream: false })
|
|
4912
|
+
}, emit, stepSignal);
|
|
4913
|
+
if (!res.ok)
|
|
4914
|
+
throw new Error(`Ollama ${res.status}: ${await res.text()}`);
|
|
4915
|
+
const d = await res.json();
|
|
4916
|
+
const text = d.message?.content ?? "";
|
|
4917
|
+
emit({ type: "text_delta", text });
|
|
4918
|
+
return { text, thinking: "", stopReason: "end_turn", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
4919
|
+
}
|
|
4920
|
+
async function callGemini(cfg, system, messages, emit, signal) {
|
|
4921
|
+
const base = (cfg.baseUrl || "https://generativelanguage.googleapis.com").replace(/\/$/, "");
|
|
4922
|
+
const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
|
|
4923
|
+
const res = await fetchWithRetry(`${base}/v1beta/models/${cfg.model}:generateContent?key=${cfg.apiKey}`, {
|
|
4924
|
+
method: "POST",
|
|
4925
|
+
headers: { "Content-Type": "application/json" },
|
|
4926
|
+
body: JSON.stringify({
|
|
4927
|
+
systemInstruction: { parts: [{ text: system }] },
|
|
4928
|
+
contents: messages.map((m) => ({
|
|
4929
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
4930
|
+
parts: [{ text: m.content }]
|
|
4931
|
+
})),
|
|
4932
|
+
generationConfig: { maxOutputTokens: cfg.maxTokens }
|
|
4933
|
+
})
|
|
4934
|
+
}, emit, stepSignal);
|
|
4935
|
+
if (!res.ok)
|
|
4936
|
+
throw new Error(`Gemini ${res.status}: ${await res.text()}`);
|
|
4937
|
+
const d = await res.json();
|
|
4938
|
+
const text = d.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
|
|
4939
|
+
emit({ type: "text_delta", text });
|
|
4940
|
+
return { text, thinking: "", stopReason: "end_turn", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
4941
|
+
}
|
|
4942
|
+
async function runAgentLoop(config2, system, initialMessages, toolSchemas, executeTool, emit, signal = new AbortController().signal, toolCache = new Map) {
|
|
4943
|
+
const cfg = { ...DEFAULTS, ...config2 };
|
|
4944
|
+
const isAnthropic = cfg.provider === "anthropic";
|
|
4945
|
+
const isOllama = cfg.provider === "ollama";
|
|
4946
|
+
const isGemini = cfg.provider === "gemini";
|
|
4947
|
+
const anthropicTools = buildAnthropicTools(toolSchemas);
|
|
4948
|
+
const openaiTools = buildOpenAITools(toolSchemas);
|
|
4949
|
+
const messages = [...initialMessages];
|
|
4950
|
+
const allToolCalls = [];
|
|
4951
|
+
const totalTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
4952
|
+
let totalToolsUsed = 0;
|
|
4953
|
+
let consecutiveErrors = 0;
|
|
4954
|
+
const endpointErrors = {};
|
|
4955
|
+
for (let iter = 0;iter < cfg.maxIterations; iter++) {
|
|
4956
|
+
if (signal.aborted) {
|
|
4957
|
+
return { content: "", toolCalls: allToolCalls, stopReason: "cancelled", tokens: totalTokens };
|
|
4958
|
+
}
|
|
4959
|
+
const { messages: trimmed, trimmed: didTrim } = trimContext(messages);
|
|
4960
|
+
if (didTrim) {
|
|
4961
|
+
emit({ type: "info", message: "Context trimmed to fit within limits." });
|
|
4962
|
+
messages.splice(0, messages.length, ...trimmed);
|
|
4963
|
+
}
|
|
4964
|
+
let turn;
|
|
4965
|
+
try {
|
|
4966
|
+
if (isAnthropic) {
|
|
4967
|
+
turn = await streamAnthropic(cfg, system, messages, anthropicTools, emit, signal);
|
|
4968
|
+
} else if (isOllama) {
|
|
4969
|
+
turn = await callOllama(cfg, system, messages, emit, signal);
|
|
4970
|
+
} else if (isGemini) {
|
|
4971
|
+
turn = await callGemini(cfg, system, messages, emit, signal);
|
|
4972
|
+
} else {
|
|
4973
|
+
turn = await streamOpenAI(cfg, system, messages, openaiTools, emit, signal);
|
|
4974
|
+
}
|
|
4975
|
+
} catch (e) {
|
|
4976
|
+
if (signal.aborted) {
|
|
4977
|
+
return { content: "", toolCalls: allToolCalls, stopReason: "cancelled", tokens: totalTokens };
|
|
4978
|
+
}
|
|
4979
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4980
|
+
const retryable = e.retryable ?? false;
|
|
4981
|
+
emit({ type: "error", message: msg, retryable });
|
|
4982
|
+
throw e;
|
|
4983
|
+
}
|
|
4984
|
+
totalTokens.input += turn.usage.input;
|
|
4985
|
+
totalTokens.output += turn.usage.output;
|
|
4986
|
+
totalTokens.cacheRead += turn.usage.cacheRead;
|
|
4987
|
+
totalTokens.cacheWrite += turn.usage.cacheWrite;
|
|
4988
|
+
if (turn.usage.input || turn.usage.output) {
|
|
4989
|
+
emit({ type: "token_usage", ...turn.usage });
|
|
4990
|
+
}
|
|
4991
|
+
const wantsTools = isAnthropic ? turn.stopReason === "tool_use" : turn.stopReason === "tool_calls";
|
|
4992
|
+
if (!wantsTools || turn.toolUses.length === 0) {
|
|
4993
|
+
return { content: turn.text, toolCalls: allToolCalls, stopReason: "end_turn", tokens: totalTokens };
|
|
4994
|
+
}
|
|
4995
|
+
if (totalToolsUsed >= cfg.maxTotalTools) {
|
|
4996
|
+
return {
|
|
4997
|
+
content: `Agent stopped: reached ${cfg.maxTotalTools} tool calls. Break your request into smaller steps.`,
|
|
4998
|
+
toolCalls: allToolCalls,
|
|
4999
|
+
stopReason: "max_tools",
|
|
5000
|
+
tokens: totalTokens
|
|
5001
|
+
};
|
|
5002
|
+
}
|
|
5003
|
+
const dedupeKey = (name, input) => `${name}:${JSON.stringify(input)}`;
|
|
5004
|
+
const turnResults = [];
|
|
5005
|
+
const executeOne = async (use) => {
|
|
5006
|
+
totalToolsUsed++;
|
|
5007
|
+
const key = dedupeKey(use.name, use.input);
|
|
5008
|
+
const cachedResult = toolCache.get(key);
|
|
5009
|
+
const isCached = !!cachedResult;
|
|
5010
|
+
emit({ type: "tool_start", id: use.id, tool: use.name, input: use.input, cached: isCached });
|
|
5011
|
+
let result;
|
|
5012
|
+
let ms = 0;
|
|
5013
|
+
if (isCached) {
|
|
5014
|
+
result = cachedResult;
|
|
5015
|
+
} else {
|
|
5016
|
+
const t0 = Date.now();
|
|
5017
|
+
result = await executeTool(use.name, use.input);
|
|
5018
|
+
ms = Date.now() - t0;
|
|
5019
|
+
if (!result.isError && use.name !== "execute_api_request" && use.name !== "fetch_url") {
|
|
5020
|
+
toolCache.set(key, result);
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
5023
|
+
emit({ type: "tool_done", id: use.id, tool: use.name, output: result.text, isError: result.isError, ms, cached: isCached });
|
|
5024
|
+
return { id: use.id, name: use.name, text: result.text, isError: result.isError, ms, cached: isCached };
|
|
5025
|
+
};
|
|
5026
|
+
const toolUses = turn.toolUses;
|
|
5027
|
+
if (cfg.parallelTools && toolUses.length > 1) {
|
|
5028
|
+
const pure = toolUses.filter((u) => u.name !== "execute_api_request" && u.name !== "fetch_url");
|
|
5029
|
+
const sideEffect = toolUses.filter((u) => u.name === "execute_api_request" || u.name === "fetch_url");
|
|
5030
|
+
const pureResults = await Promise.all(pure.map((u) => executeOne(u)));
|
|
5031
|
+
const sideEffectResults = [];
|
|
5032
|
+
for (const u of sideEffect)
|
|
5033
|
+
sideEffectResults.push(await executeOne(u));
|
|
5034
|
+
const resultMap = new Map([...pureResults, ...sideEffectResults].map((r) => [r.id, r]));
|
|
5035
|
+
for (const u of toolUses) {
|
|
5036
|
+
const r = resultMap.get(u.id);
|
|
5037
|
+
if (r)
|
|
5038
|
+
turnResults.push(r);
|
|
5039
|
+
}
|
|
5040
|
+
} else {
|
|
5041
|
+
for (const u of toolUses)
|
|
5042
|
+
turnResults.push(await executeOne(u));
|
|
5043
|
+
}
|
|
5044
|
+
for (const r of turnResults) {
|
|
5045
|
+
allToolCalls.push({ id: r.id, tool: r.name, input: turn.toolUses.find((u) => u.id === r.id)?.input ?? {}, output: r.text, isError: r.isError, ms: r.ms, cached: r.cached });
|
|
5046
|
+
if (r.isError) {
|
|
5047
|
+
consecutiveErrors++;
|
|
5048
|
+
if (r.name === "execute_api_request") {
|
|
5049
|
+
const eid = String(turn.toolUses.find((u) => u.id === r.id)?.input?.operationId ?? r.id);
|
|
5050
|
+
endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
|
|
5051
|
+
if (endpointErrors[eid] >= cfg.maxEndpointErrors) {
|
|
5052
|
+
return {
|
|
5053
|
+
content: `Endpoint "${eid}" failed ${cfg.maxEndpointErrors} times. Last error: ${r.text}`,
|
|
5054
|
+
toolCalls: allToolCalls,
|
|
5055
|
+
stopReason: "max_endpoint_errors",
|
|
5056
|
+
tokens: totalTokens
|
|
5057
|
+
};
|
|
5058
|
+
}
|
|
5059
|
+
}
|
|
5060
|
+
if (consecutiveErrors >= cfg.maxConsecutiveErrors) {
|
|
5061
|
+
return {
|
|
5062
|
+
content: `Stopped after ${cfg.maxConsecutiveErrors} consecutive errors. Last: ${r.text}`,
|
|
5063
|
+
toolCalls: allToolCalls,
|
|
5064
|
+
stopReason: "max_errors",
|
|
5065
|
+
tokens: totalTokens
|
|
5066
|
+
};
|
|
5067
|
+
}
|
|
5068
|
+
} else {
|
|
5069
|
+
consecutiveErrors = 0;
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
if (isAnthropic) {
|
|
5073
|
+
const blocks = turn._anthropicBlocks ?? [];
|
|
5074
|
+
messages.push({ role: "assistant", content: blocks });
|
|
5075
|
+
messages.push({
|
|
5076
|
+
role: "user",
|
|
5077
|
+
content: turnResults.map((r) => ({ type: "tool_result", tool_use_id: r.id, content: r.text }))
|
|
5078
|
+
});
|
|
5079
|
+
} else {
|
|
5080
|
+
const tc = turn._openaiTcAccum ?? {};
|
|
5081
|
+
messages.push({
|
|
5082
|
+
role: "assistant",
|
|
5083
|
+
content: turn.text || null,
|
|
5084
|
+
tool_calls: Object.values(tc).map((t) => ({ id: t.id, type: "function", function: { name: t.name, arguments: t.args } }))
|
|
5085
|
+
});
|
|
5086
|
+
for (const r of turnResults) {
|
|
5087
|
+
messages.push({ role: "tool", tool_call_id: r.id, content: r.text });
|
|
5088
|
+
}
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
return { content: "(max iterations reached)", toolCalls: allToolCalls, stopReason: "max_iterations", tokens: totalTokens };
|
|
5092
|
+
}
|
|
5093
|
+
var RETRYABLE_STATUS, MAX_CONTEXT_CHARS = 300000, DEFAULTS;
|
|
5094
|
+
var init_harness = __esm(() => {
|
|
5095
|
+
RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
|
|
5096
|
+
DEFAULTS = {
|
|
5097
|
+
apiKey: "",
|
|
5098
|
+
baseUrl: "",
|
|
5099
|
+
extraHeaders: {},
|
|
5100
|
+
maxTokens: 4096,
|
|
5101
|
+
temperature: 1,
|
|
5102
|
+
topK: 0,
|
|
5103
|
+
maxIterations: 40,
|
|
5104
|
+
maxTotalTools: 40,
|
|
5105
|
+
maxConsecutiveErrors: 5,
|
|
5106
|
+
maxEndpointErrors: 3,
|
|
5107
|
+
stepTimeoutMs: 60000,
|
|
5108
|
+
parallelTools: true,
|
|
5109
|
+
enablePromptCache: true
|
|
5110
|
+
};
|
|
5111
|
+
});
|
|
5112
|
+
|
|
5113
|
+
// src/api/routes.ts
|
|
5114
|
+
import dns from "dns/promises";
|
|
5115
|
+
function json(data, status = 200) {
|
|
5116
|
+
return new Response(JSON.stringify(data), {
|
|
5117
|
+
status,
|
|
5118
|
+
headers: { "Content-Type": "application/json", ...CORS4 }
|
|
5119
|
+
});
|
|
5120
|
+
}
|
|
5121
|
+
function notFound(msg = "Not found") {
|
|
5122
|
+
return json({ error: msg }, 404);
|
|
5123
|
+
}
|
|
5124
|
+
function badRequest(msg) {
|
|
5125
|
+
return json({ error: msg }, 400);
|
|
5126
|
+
}
|
|
5127
|
+
async function apiRouter(req) {
|
|
5128
|
+
if (req.method === "OPTIONS")
|
|
5129
|
+
return new Response(null, { status: 204, headers: CORS4 });
|
|
5130
|
+
const { pathname: path, searchParams } = new URL(req.url);
|
|
5131
|
+
const method = req.method;
|
|
5132
|
+
if (path === "/api/status" && method === "GET")
|
|
5133
|
+
return handleStatus();
|
|
5134
|
+
if (path === "/api/logs" && method === "GET")
|
|
5135
|
+
return handleGetLogs(searchParams);
|
|
5136
|
+
if (path === "/api/logs" && method === "DELETE")
|
|
5137
|
+
return handleClearLogs();
|
|
5138
|
+
if (path === "/api/auth/profiles" && method === "GET")
|
|
5139
|
+
return handleGetProfiles();
|
|
5140
|
+
if (path === "/api/auth/profiles" && method === "POST")
|
|
5141
|
+
return handleCreateProfile(req);
|
|
5142
|
+
if (path.startsWith("/api/auth/profiles/") && method === "PUT")
|
|
5143
|
+
return handleUpdateProfile(req, path);
|
|
5144
|
+
if (path.startsWith("/api/auth/profiles/") && method === "DELETE")
|
|
5145
|
+
return handleDeleteProfile(path);
|
|
5146
|
+
if (path.match(/^\/api\/auth\/profiles\/[^/]+\/activate$/) && method === "POST")
|
|
5147
|
+
return handleActivateProfile(path);
|
|
5148
|
+
if (path === "/api/auth" && method === "GET")
|
|
5149
|
+
return handleGetAuth();
|
|
5150
|
+
if (path === "/api/auth" && method === "PUT")
|
|
5151
|
+
return handleSetAuth(req);
|
|
5152
|
+
if (path === "/api/auth/test" && method === "POST")
|
|
5153
|
+
return handleTestAuth();
|
|
5154
|
+
if (path === "/api/explorer/request" && method === "POST")
|
|
5155
|
+
return handleExplorerRequest(req);
|
|
5156
|
+
if (path === "/api/spec/endpoints" && method === "GET")
|
|
5157
|
+
return handleGetEndpoints();
|
|
5158
|
+
if (path === "/api/settings" && method === "GET")
|
|
5159
|
+
return handleGetSettings();
|
|
5160
|
+
if (path === "/api/settings" && method === "PUT")
|
|
5161
|
+
return handleSetSettings(req);
|
|
5162
|
+
if (path === "/api/spec/upload" && method === "POST")
|
|
5163
|
+
return handleSpecUpload(req);
|
|
5164
|
+
if (path === "/api/spec/reload-url" && method === "POST")
|
|
5165
|
+
return handleSpecReloadUrl(req);
|
|
5166
|
+
if (path === "/api/intercept" && method === "GET")
|
|
5167
|
+
return handleGetRules();
|
|
5168
|
+
if (path === "/api/intercept" && method === "POST")
|
|
5169
|
+
return handleCreateRule(req);
|
|
5170
|
+
if (path.startsWith("/api/intercept/") && method === "PUT")
|
|
5171
|
+
return handleUpdateRule(req, path);
|
|
5172
|
+
if (path.startsWith("/api/intercept/") && method === "DELETE")
|
|
5173
|
+
return handleDeleteRule(path);
|
|
5174
|
+
if (path === "/api/ai/chat" && method === "POST")
|
|
5175
|
+
return handleAiChat(req);
|
|
5176
|
+
if (path === "/api/ai/memory" && method === "GET")
|
|
5177
|
+
return json({ memory: dbQueries.getMemory(40) });
|
|
5178
|
+
if (path === "/api/ai/memory" && method === "DELETE") {
|
|
5179
|
+
dbQueries.clearMemory();
|
|
5180
|
+
return json({ success: true });
|
|
5181
|
+
}
|
|
5182
|
+
if (path === "/api/debug/dns" && method === "GET")
|
|
5183
|
+
return handleDnsQuery(searchParams);
|
|
5184
|
+
if (path === "/api/debug/ping" && method === "GET")
|
|
5185
|
+
return handlePing(searchParams);
|
|
5186
|
+
if (path === "/api/reload" && method === "POST")
|
|
5187
|
+
return handleReload();
|
|
5188
|
+
if (path === "/api/server-info" && method === "GET")
|
|
5189
|
+
return handleServerInfo();
|
|
5190
|
+
if (path === "/api/features" && method === "GET")
|
|
5191
|
+
return json(getFeatures());
|
|
5192
|
+
if (path === "/api/features" && method === "PUT")
|
|
5193
|
+
return handleSetFeatures(req);
|
|
5194
|
+
if (path === "/api/saved" && method === "GET")
|
|
5195
|
+
return handleGetSaved();
|
|
5196
|
+
if (path === "/api/saved" && method === "POST")
|
|
5197
|
+
return handleCreateSaved(req);
|
|
5198
|
+
if (path.startsWith("/api/saved/") && method === "PUT")
|
|
5199
|
+
return handleUpdateSaved(req, path);
|
|
5200
|
+
if (path.startsWith("/api/saved/") && method === "DELETE")
|
|
5201
|
+
return handleDeleteSaved(path);
|
|
5202
|
+
if (path === "/api/workflows" && method === "GET")
|
|
5203
|
+
return handleGetWorkflows();
|
|
5204
|
+
if (path === "/api/workflows" && method === "POST")
|
|
5205
|
+
return handleCreateWorkflow(req);
|
|
5206
|
+
if (path === "/api/workflows/generate" && method === "POST")
|
|
5207
|
+
return handleGenerateWorkflow(req);
|
|
5208
|
+
if (path.startsWith("/api/workflows/") && path.endsWith("/run") && method === "POST")
|
|
5209
|
+
return handleRunWorkflow(path);
|
|
5210
|
+
if (path.startsWith("/api/workflows/") && method === "PUT")
|
|
5211
|
+
return handleUpdateWorkflow(req, path);
|
|
5212
|
+
if (path.startsWith("/api/workflows/") && method === "DELETE")
|
|
5213
|
+
return handleDeleteWorkflow(path);
|
|
5214
|
+
if (path === "/api/capture/bins" && method === "GET")
|
|
5215
|
+
return handleGetCaptureBins();
|
|
5216
|
+
if (path === "/api/capture/bins" && method === "POST")
|
|
5217
|
+
return handleCreateCaptureBin(req);
|
|
5218
|
+
if (path.startsWith("/api/capture/bins/") && method === "DELETE")
|
|
5219
|
+
return handleDeleteCaptureBin(path);
|
|
5220
|
+
return notFound("API route not found");
|
|
5221
|
+
}
|
|
5222
|
+
function handleStatus() {
|
|
5223
|
+
if (!hasState()) {
|
|
5224
|
+
return json({ ok: true, version: VERSION, spec: null, endpointCount: 0, wsClients: logBus.clientCount });
|
|
5225
|
+
}
|
|
5226
|
+
const { spec, operations } = getState();
|
|
5227
|
+
return json({
|
|
5228
|
+
ok: true,
|
|
5229
|
+
version: VERSION,
|
|
5230
|
+
spec: { title: spec.title, version: spec.version, baseUrl: spec.baseUrl, url: spec.url },
|
|
5231
|
+
endpointCount: operations.length,
|
|
5232
|
+
wsClients: logBus.clientCount
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5235
|
+
async function handleReload() {
|
|
5236
|
+
if (!hasState())
|
|
5237
|
+
return badRequest("No spec loaded");
|
|
5238
|
+
const { specUrl } = getState();
|
|
5239
|
+
if (!specUrl)
|
|
5240
|
+
return json({ error: "Spec was uploaded manually \u2014 cannot reload from URL" }, 400);
|
|
5241
|
+
try {
|
|
5242
|
+
const s = await loadSpec(specUrl);
|
|
5243
|
+
return json({ ok: true, spec: s.spec.title, version: s.spec.version, endpoints: s.operations.length });
|
|
5244
|
+
} catch (e) {
|
|
5245
|
+
return json({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
async function handleSetFeatures(req) {
|
|
5249
|
+
let body;
|
|
5250
|
+
try {
|
|
5251
|
+
body = await req.json();
|
|
5252
|
+
} catch {
|
|
5253
|
+
return badRequest("Invalid JSON");
|
|
5254
|
+
}
|
|
5255
|
+
const patch = {};
|
|
5256
|
+
for (const key of ["mcp", "proxy", "ai", "readonly"]) {
|
|
5257
|
+
if (typeof body[key] === "boolean")
|
|
5258
|
+
patch[key] = body[key];
|
|
5259
|
+
}
|
|
5260
|
+
setFeatures(patch);
|
|
5261
|
+
persistAndBroadcastFeatures();
|
|
5262
|
+
return json(getFeatures());
|
|
5263
|
+
}
|
|
5264
|
+
function persistAndBroadcastFeatures() {
|
|
5265
|
+
const f = getFeatures();
|
|
5266
|
+
dbQueries.setSetting("features", JSON.stringify({ mcp: f.mcp, proxy: f.proxy, ai: f.ai }));
|
|
5267
|
+
logBus.broadcastServerEvent({ kind: "features", data: f });
|
|
5268
|
+
}
|
|
5269
|
+
function handleServerInfo() {
|
|
5270
|
+
const state = hasState() ? getState() : null;
|
|
5271
|
+
return json({
|
|
5272
|
+
pid: process.pid,
|
|
5273
|
+
port: parseInt(process.env._OA_PORT ?? "3388", 10),
|
|
5274
|
+
startedAt: parseInt(process.env._OA_STARTED ?? "0", 10),
|
|
5275
|
+
features: getFeatures(),
|
|
5276
|
+
spec: state ? {
|
|
5277
|
+
title: state.spec.title,
|
|
5278
|
+
version: state.spec.version,
|
|
5279
|
+
endpointCount: state.operations.length,
|
|
5280
|
+
specUrl: state.specUrl ?? null
|
|
5281
|
+
} : null
|
|
5282
|
+
});
|
|
5283
|
+
}
|
|
5284
|
+
async function handleSpecUpload(req) {
|
|
5285
|
+
let content;
|
|
5286
|
+
let filename = "spec";
|
|
5287
|
+
const ct = req.headers.get("content-type") ?? "";
|
|
5288
|
+
if (ct.includes("multipart/form-data")) {
|
|
5289
|
+
let form;
|
|
5290
|
+
try {
|
|
5291
|
+
form = await req.formData();
|
|
5292
|
+
} catch {
|
|
5293
|
+
return badRequest("Invalid form data");
|
|
5294
|
+
}
|
|
5295
|
+
const file = form.get("file");
|
|
5296
|
+
if (!file || typeof file === "string")
|
|
4313
5297
|
return badRequest("No file in form data");
|
|
4314
5298
|
filename = file.name ?? "spec";
|
|
4315
5299
|
content = await file.text();
|
|
@@ -4331,10 +5315,12 @@ async function handleSpecUpload(req) {
|
|
|
4331
5315
|
return badRequest("Empty spec content");
|
|
4332
5316
|
try {
|
|
4333
5317
|
const state = loadSpecFromText(content, filename);
|
|
5318
|
+
const suggestedVars = extractSuggestedVars(content, state.spec.baseUrl);
|
|
4334
5319
|
return json({
|
|
4335
5320
|
ok: true,
|
|
4336
5321
|
spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
|
|
4337
|
-
endpointCount: state.operations.length
|
|
5322
|
+
endpointCount: state.operations.length,
|
|
5323
|
+
suggestedVars
|
|
4338
5324
|
});
|
|
4339
5325
|
} catch (e) {
|
|
4340
5326
|
return json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
@@ -4351,10 +5337,12 @@ async function handleSpecReloadUrl(req) {
|
|
|
4351
5337
|
return badRequest("Missing url field");
|
|
4352
5338
|
try {
|
|
4353
5339
|
const state = await loadSpec(body.url);
|
|
5340
|
+
const suggestedVars = extractSuggestedVars(state.spec.raw, state.spec.baseUrl);
|
|
4354
5341
|
return json({
|
|
4355
5342
|
ok: true,
|
|
4356
5343
|
spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
|
|
4357
|
-
endpointCount: state.operations.length
|
|
5344
|
+
endpointCount: state.operations.length,
|
|
5345
|
+
suggestedVars
|
|
4358
5346
|
});
|
|
4359
5347
|
} catch (e) {
|
|
4360
5348
|
return json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
@@ -4416,39 +5404,54 @@ async function handleSetSettings(req) {
|
|
|
4416
5404
|
dbQueries.setSettings(body);
|
|
4417
5405
|
return json(body);
|
|
4418
5406
|
}
|
|
4419
|
-
async function executeTool(name, args) {
|
|
5407
|
+
async function executeTool(name, args, cache = new Map) {
|
|
4420
5408
|
const { operations, spec } = getState();
|
|
4421
5409
|
if (name === "search_endpoints") {
|
|
5410
|
+
const cacheKey2 = `search:${String(args.query ?? "").toLowerCase()}`;
|
|
5411
|
+
const hit = cache.get(cacheKey2);
|
|
5412
|
+
if (hit)
|
|
5413
|
+
return hit;
|
|
4422
5414
|
const q = String(args.query ?? "").toLowerCase();
|
|
4423
5415
|
const terms = q.split(/\s+/).filter(Boolean);
|
|
4424
5416
|
const matches = operations.filter((op) => {
|
|
4425
5417
|
const hay = [op.operationId, op.path, op.method, ...op.tags ?? [], op.summary ?? "", op.description ?? ""].join(" ").toLowerCase();
|
|
4426
5418
|
return terms.every((t) => hay.includes(t));
|
|
4427
5419
|
}).slice(0, 30).map((op) => ({ operationId: op.operationId, method: op.method.toUpperCase(), path: op.path, summary: op.summary ?? null, tags: op.tags }));
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
|
|
5420
|
+
const text = !matches.length ? `No endpoints found matching "${args.query}". Total: ${operations.length}.` : JSON.stringify({ count: matches.length, total: operations.length, endpoints: matches }, null, 2);
|
|
5421
|
+
const result = { text, isError: false };
|
|
5422
|
+
cache.set(cacheKey2, result);
|
|
5423
|
+
return result;
|
|
4431
5424
|
}
|
|
4432
5425
|
if (name === "get_endpoint_schema") {
|
|
5426
|
+
const cacheKey2 = `schema:${String(args.operationId ?? "")}`;
|
|
5427
|
+
const hit = cache.get(cacheKey2);
|
|
5428
|
+
if (hit)
|
|
5429
|
+
return hit;
|
|
4433
5430
|
const op = operations.find((o) => o.operationId === args.operationId);
|
|
4434
5431
|
if (!op)
|
|
4435
5432
|
return { text: `Endpoint not found: "${args.operationId}"`, isError: true };
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
5433
|
+
const text = JSON.stringify({
|
|
5434
|
+
operationId: op.operationId,
|
|
5435
|
+
method: op.method.toUpperCase(),
|
|
5436
|
+
path: op.path,
|
|
5437
|
+
summary: op.summary ?? null,
|
|
5438
|
+
description: op.description ?? null,
|
|
5439
|
+
tags: op.tags,
|
|
5440
|
+
parameters: op.parameters,
|
|
5441
|
+
requestBody: op.requestBody ?? null,
|
|
5442
|
+
responses: op.responses
|
|
5443
|
+
}, null, 2);
|
|
5444
|
+
const result = { text, isError: false };
|
|
5445
|
+
cache.set(cacheKey2, result);
|
|
5446
|
+
return result;
|
|
4450
5447
|
}
|
|
4451
5448
|
if (name === "execute_api_request") {
|
|
5449
|
+
const now = Date.now();
|
|
5450
|
+
const gap = now - _lastApiCallMs;
|
|
5451
|
+
if (gap < MIN_API_CALL_INTERVAL_MS) {
|
|
5452
|
+
await new Promise((r) => setTimeout(r, MIN_API_CALL_INTERVAL_MS - gap));
|
|
5453
|
+
}
|
|
5454
|
+
_lastApiCallMs = Date.now();
|
|
4452
5455
|
const op = operations.find((o) => o.operationId === args.operationId);
|
|
4453
5456
|
if (!op)
|
|
4454
5457
|
return { text: `Endpoint not found: "${args.operationId}"`, isError: true };
|
|
@@ -4479,20 +5482,81 @@ async function executeTool(name, args) {
|
|
|
4479
5482
|
const bodyStr = reqBody !== undefined ? typeof reqBody === "string" ? reqBody : JSON.stringify(reqBody) : null;
|
|
4480
5483
|
if (bodyStr !== null && op.requestBody?.contentType)
|
|
4481
5484
|
authedHeaders["Content-Type"] = op.requestBody.contentType;
|
|
5485
|
+
const logId = randomUUID2();
|
|
4482
5486
|
try {
|
|
4483
5487
|
const start = Date.now();
|
|
4484
5488
|
const res = await fetch(authedUrl, { method: op.method.toUpperCase(), headers: authedHeaders, body: bodyStr ?? undefined });
|
|
4485
|
-
const
|
|
5489
|
+
const responseText = await res.text();
|
|
4486
5490
|
const latency = Date.now() - start;
|
|
4487
|
-
|
|
5491
|
+
const resHeaders = Object.fromEntries(res.headers.entries());
|
|
5492
|
+
dbQueries.insertLog({
|
|
5493
|
+
id: logId,
|
|
5494
|
+
source: "ai",
|
|
5495
|
+
tool_name: String(args.operationId ?? op.operationId),
|
|
5496
|
+
method: op.method.toUpperCase(),
|
|
5497
|
+
url: authedUrl,
|
|
5498
|
+
request_headers: JSON.stringify(authedHeaders),
|
|
5499
|
+
request_body: bodyStr,
|
|
5500
|
+
status_code: res.status,
|
|
5501
|
+
response_headers: JSON.stringify(resHeaders),
|
|
5502
|
+
response_body: responseText.slice(0, 8192),
|
|
5503
|
+
latency_ms: latency,
|
|
5504
|
+
error: null
|
|
5505
|
+
});
|
|
5506
|
+
logBus.emit({
|
|
5507
|
+
id: logId,
|
|
5508
|
+
source: "ai",
|
|
5509
|
+
tool_name: String(args.operationId ?? op.operationId),
|
|
5510
|
+
method: op.method.toUpperCase(),
|
|
5511
|
+
url: authedUrl,
|
|
5512
|
+
request_headers: JSON.stringify(authedHeaders),
|
|
5513
|
+
request_body: bodyStr,
|
|
5514
|
+
status_code: res.status,
|
|
5515
|
+
response_headers: JSON.stringify(resHeaders),
|
|
5516
|
+
response_body: responseText.slice(0, 2048),
|
|
5517
|
+
latency_ms: latency,
|
|
5518
|
+
error: null,
|
|
5519
|
+
created_at: Date.now()
|
|
5520
|
+
});
|
|
5521
|
+
let pretty = responseText;
|
|
4488
5522
|
try {
|
|
4489
|
-
pretty = JSON.stringify(JSON.parse(
|
|
5523
|
+
pretty = JSON.stringify(JSON.parse(responseText), null, 2);
|
|
4490
5524
|
} catch {}
|
|
4491
5525
|
return { text: `HTTP ${res.status} (${latency}ms)
|
|
4492
5526
|
|
|
4493
5527
|
${pretty}`, isError: !res.ok };
|
|
4494
5528
|
} catch (e) {
|
|
4495
|
-
|
|
5529
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
5530
|
+
dbQueries.insertLog({
|
|
5531
|
+
id: logId,
|
|
5532
|
+
source: "ai",
|
|
5533
|
+
tool_name: String(args.operationId ?? op.operationId),
|
|
5534
|
+
method: op.method.toUpperCase(),
|
|
5535
|
+
url: authedUrl,
|
|
5536
|
+
request_headers: JSON.stringify(authedHeaders),
|
|
5537
|
+
request_body: bodyStr,
|
|
5538
|
+
status_code: null,
|
|
5539
|
+
response_headers: null,
|
|
5540
|
+
response_body: null,
|
|
5541
|
+
latency_ms: null,
|
|
5542
|
+
error: errMsg
|
|
5543
|
+
});
|
|
5544
|
+
logBus.emit({
|
|
5545
|
+
id: logId,
|
|
5546
|
+
source: "ai",
|
|
5547
|
+
tool_name: String(args.operationId ?? op.operationId),
|
|
5548
|
+
method: op.method.toUpperCase(),
|
|
5549
|
+
url: authedUrl,
|
|
5550
|
+
request_headers: null,
|
|
5551
|
+
request_body: bodyStr,
|
|
5552
|
+
status_code: null,
|
|
5553
|
+
response_headers: null,
|
|
5554
|
+
response_body: null,
|
|
5555
|
+
latency_ms: null,
|
|
5556
|
+
error: errMsg,
|
|
5557
|
+
created_at: Date.now()
|
|
5558
|
+
});
|
|
5559
|
+
return { text: `Network error: ${errMsg}`, isError: true };
|
|
4496
5560
|
}
|
|
4497
5561
|
}
|
|
4498
5562
|
if (name === "fetch_url") {
|
|
@@ -4642,10 +5706,25 @@ ${stripped}`, isError: !res.ok };
|
|
|
4642
5706
|
}
|
|
4643
5707
|
if (name === "save_auth_token") {
|
|
4644
5708
|
const profileName = String(args.name ?? "AI Login").trim();
|
|
5709
|
+
const tokenType = String(args.token_type ?? "bearer");
|
|
5710
|
+
if (tokenType === "basic" || args.username && args.password) {
|
|
5711
|
+
const username = String(args.username ?? "").trim();
|
|
5712
|
+
const password = String(args.password ?? "").trim();
|
|
5713
|
+
if (!username || !password)
|
|
5714
|
+
return { text: "Error: username and password are required for basic auth", isError: true };
|
|
5715
|
+
const authConfig2 = { type: "basic", username, password };
|
|
5716
|
+
const profileId2 = randomUUID2();
|
|
5717
|
+
try {
|
|
5718
|
+
dbQueries.insertProfile({ id: profileId2, name: profileName, description: "Saved by AI", type: "basic", config: JSON.stringify(authConfig2), token_cache: null, is_active: 0 });
|
|
5719
|
+
dbQueries.activateProfile(profileId2);
|
|
5720
|
+
return { text: JSON.stringify({ success: true, message: `Saved and activated basic auth profile "${profileName}"`, id: profileId2 }), isError: false };
|
|
5721
|
+
} catch (e) {
|
|
5722
|
+
return { text: `Error saving profile: ${e instanceof Error ? e.message : String(e)}`, isError: true };
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
4645
5725
|
const token = String(args.token ?? "").trim();
|
|
4646
5726
|
if (!token)
|
|
4647
|
-
return { text: "Error: token is required", isError: true };
|
|
4648
|
-
const tokenType = String(args.token_type ?? "bearer");
|
|
5727
|
+
return { text: "Error: token is required for bearer/apikey auth", isError: true };
|
|
4649
5728
|
const headerName = String(args.header_name ?? "X-Api-Key");
|
|
4650
5729
|
let authConfig;
|
|
4651
5730
|
let type;
|
|
@@ -4670,274 +5749,6 @@ ${stripped}`, isError: !res.ok };
|
|
|
4670
5749
|
}
|
|
4671
5750
|
return { text: `Unknown tool: ${name}`, isError: true };
|
|
4672
5751
|
}
|
|
4673
|
-
async function fetchWithRetry(url, opts, emit, maxRetries = 3) {
|
|
4674
|
-
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
4675
|
-
const res = await fetch(url, opts);
|
|
4676
|
-
if (res.status !== 429 || attempt === maxRetries)
|
|
4677
|
-
return res;
|
|
4678
|
-
const retryAfter = parseInt(res.headers.get("retry-after") ?? "0", 10);
|
|
4679
|
-
const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * Math.pow(2, attempt) + Math.random() * 500, 30000);
|
|
4680
|
-
emit({ type: "info", message: `Rate limited \u2014 retrying in ${Math.round(delay / 1000)}s\u2026 (attempt ${attempt + 1}/${maxRetries})` });
|
|
4681
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
4682
|
-
}
|
|
4683
|
-
return fetch(url, opts);
|
|
4684
|
-
}
|
|
4685
|
-
async function anthropicAgentLoop(apiKey, model, system, initialMessages, emit) {
|
|
4686
|
-
const msgs = [...initialMessages];
|
|
4687
|
-
const toolCalls = [];
|
|
4688
|
-
let totalTools = 0;
|
|
4689
|
-
let consecutiveErrors = 0;
|
|
4690
|
-
const endpointErrors = {};
|
|
4691
|
-
for (let iter = 0;iter < 40; iter++) {
|
|
4692
|
-
const res = await fetchWithRetry("https://api.anthropic.com/v1/messages", {
|
|
4693
|
-
method: "POST",
|
|
4694
|
-
headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
|
|
4695
|
-
body: JSON.stringify({ model, max_tokens: 4096, system, messages: msgs, tools: ANTHROPIC_TOOLS, stream: true })
|
|
4696
|
-
}, emit);
|
|
4697
|
-
if (!res.ok)
|
|
4698
|
-
throw new Error(`Anthropic error: ${await res.text()}`);
|
|
4699
|
-
let fullText = "";
|
|
4700
|
-
let stopReason = "";
|
|
4701
|
-
const contentBlocks = [];
|
|
4702
|
-
const inputAccum = {};
|
|
4703
|
-
const reader = res.body.getReader();
|
|
4704
|
-
const decoder = new TextDecoder;
|
|
4705
|
-
let buf = "";
|
|
4706
|
-
while (true) {
|
|
4707
|
-
const { done, value } = await reader.read();
|
|
4708
|
-
if (done)
|
|
4709
|
-
break;
|
|
4710
|
-
buf += decoder.decode(value, { stream: true });
|
|
4711
|
-
const parts = buf.split(`
|
|
4712
|
-
|
|
4713
|
-
`);
|
|
4714
|
-
buf = parts.pop() ?? "";
|
|
4715
|
-
for (const part of parts) {
|
|
4716
|
-
let dataLine = "";
|
|
4717
|
-
for (const line of part.split(`
|
|
4718
|
-
`)) {
|
|
4719
|
-
if (line.startsWith("data: ")) {
|
|
4720
|
-
dataLine = line.slice(6);
|
|
4721
|
-
break;
|
|
4722
|
-
}
|
|
4723
|
-
}
|
|
4724
|
-
if (!dataLine || dataLine === "[DONE]")
|
|
4725
|
-
continue;
|
|
4726
|
-
let ev;
|
|
4727
|
-
try {
|
|
4728
|
-
ev = JSON.parse(dataLine);
|
|
4729
|
-
} catch {
|
|
4730
|
-
continue;
|
|
4731
|
-
}
|
|
4732
|
-
const type = ev.type;
|
|
4733
|
-
if (type === "content_block_start") {
|
|
4734
|
-
const idx = ev.index;
|
|
4735
|
-
const cb = ev.content_block;
|
|
4736
|
-
contentBlocks[idx] = { type: cb.type, id: cb.id, name: cb.name };
|
|
4737
|
-
if (cb.type === "tool_use")
|
|
4738
|
-
inputAccum[idx] = "";
|
|
4739
|
-
} else if (type === "content_block_delta") {
|
|
4740
|
-
const idx = ev.index;
|
|
4741
|
-
const delta = ev.delta;
|
|
4742
|
-
if (delta.type === "text_delta" && delta.text) {
|
|
4743
|
-
fullText += delta.text;
|
|
4744
|
-
if (!contentBlocks[idx])
|
|
4745
|
-
contentBlocks[idx] = { type: "text", text: "" };
|
|
4746
|
-
contentBlocks[idx].text = (contentBlocks[idx].text ?? "") + delta.text;
|
|
4747
|
-
emit({ type: "text_delta", text: delta.text });
|
|
4748
|
-
} else if (delta.type === "input_json_delta" && delta.partial_json) {
|
|
4749
|
-
inputAccum[idx] = (inputAccum[idx] ?? "") + delta.partial_json;
|
|
4750
|
-
}
|
|
4751
|
-
} else if (type === "content_block_stop") {
|
|
4752
|
-
const idx = ev.index;
|
|
4753
|
-
if (contentBlocks[idx]?.type === "tool_use") {
|
|
4754
|
-
try {
|
|
4755
|
-
contentBlocks[idx].input = JSON.parse(inputAccum[idx] ?? "{}");
|
|
4756
|
-
} catch {
|
|
4757
|
-
contentBlocks[idx].input = {};
|
|
4758
|
-
}
|
|
4759
|
-
}
|
|
4760
|
-
} else if (type === "message_delta") {
|
|
4761
|
-
const delta = ev.delta;
|
|
4762
|
-
if (delta.stop_reason)
|
|
4763
|
-
stopReason = delta.stop_reason;
|
|
4764
|
-
}
|
|
4765
|
-
}
|
|
4766
|
-
}
|
|
4767
|
-
if (stopReason !== "tool_use")
|
|
4768
|
-
return { content: fullText, toolCalls };
|
|
4769
|
-
if (totalTools >= MAX_TOTAL_TOOLS) {
|
|
4770
|
-
return { content: `Agent stopped: reached ${MAX_TOTAL_TOOLS} tool calls. Please break your request into smaller steps.`, toolCalls };
|
|
4771
|
-
}
|
|
4772
|
-
msgs.push({ role: "assistant", content: contentBlocks });
|
|
4773
|
-
const toolResults = [];
|
|
4774
|
-
for (const block of contentBlocks) {
|
|
4775
|
-
if (block.type !== "tool_use" || !block.id || !block.name)
|
|
4776
|
-
continue;
|
|
4777
|
-
totalTools++;
|
|
4778
|
-
emit({ type: "tool_start", tool: block.name, input: block.input ?? {} });
|
|
4779
|
-
const result = await executeTool(block.name, block.input ?? {});
|
|
4780
|
-
emit({ type: "tool_done", tool: block.name, input: block.input ?? {}, output: result.text, isError: result.isError });
|
|
4781
|
-
toolCalls.push({ tool: block.name, input: block.input ?? {}, output: result.text, isError: result.isError });
|
|
4782
|
-
if (result.isError) {
|
|
4783
|
-
consecutiveErrors++;
|
|
4784
|
-
if (block.name === "execute_api_request" && block.input?.operationId) {
|
|
4785
|
-
const eid = String(block.input.operationId);
|
|
4786
|
-
endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
|
|
4787
|
-
if (endpointErrors[eid] >= MAX_SAME_ENDPOINT_ERRORS) {
|
|
4788
|
-
const stopContent = result.text + `
|
|
4789
|
-
|
|
4790
|
-
[AGENT LOOP STOPPED: endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times \u2014 stopping to avoid loop]`;
|
|
4791
|
-
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: stopContent });
|
|
4792
|
-
msgs.push({ role: "user", content: toolResults });
|
|
4793
|
-
return { content: `Endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times. Last error: ${result.text}`, toolCalls };
|
|
4794
|
-
}
|
|
4795
|
-
}
|
|
4796
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
4797
|
-
const stopMsg = `Stopped after ${MAX_CONSECUTIVE_ERRORS} consecutive errors. Last: ${result.text}`;
|
|
4798
|
-
const stopContent = result.text + `
|
|
4799
|
-
|
|
4800
|
-
[AGENT LOOP STOPPED: ${stopMsg}]`;
|
|
4801
|
-
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: stopContent });
|
|
4802
|
-
msgs.push({ role: "user", content: toolResults });
|
|
4803
|
-
return { content: stopMsg, toolCalls };
|
|
4804
|
-
}
|
|
4805
|
-
} else {
|
|
4806
|
-
consecutiveErrors = 0;
|
|
4807
|
-
}
|
|
4808
|
-
toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result.text });
|
|
4809
|
-
}
|
|
4810
|
-
if (!toolResults.length)
|
|
4811
|
-
return { content: fullText, toolCalls };
|
|
4812
|
-
msgs.push({ role: "user", content: toolResults });
|
|
4813
|
-
}
|
|
4814
|
-
return { content: "(max iterations reached)", toolCalls };
|
|
4815
|
-
}
|
|
4816
|
-
async function openaiCompatibleLoop(base, apiKey, model, extraHeaders, system, initialMessages, emit) {
|
|
4817
|
-
const msgs = [{ role: "system", content: system }, ...initialMessages];
|
|
4818
|
-
const toolCalls = [];
|
|
4819
|
-
const authHeaders = {};
|
|
4820
|
-
if (apiKey)
|
|
4821
|
-
authHeaders["Authorization"] = `Bearer ${apiKey}`;
|
|
4822
|
-
let totalTools = 0;
|
|
4823
|
-
let consecutiveErrors = 0;
|
|
4824
|
-
const endpointErrors = {};
|
|
4825
|
-
for (let iter = 0;iter < 40; iter++) {
|
|
4826
|
-
const res = await fetchWithRetry(`${base}/v1/chat/completions`, {
|
|
4827
|
-
method: "POST",
|
|
4828
|
-
headers: { "Content-Type": "application/json", ...authHeaders, ...extraHeaders },
|
|
4829
|
-
body: JSON.stringify({ model, messages: msgs, tools: OPENAI_TOOLS, tool_choice: "auto", stream: true })
|
|
4830
|
-
}, emit);
|
|
4831
|
-
if (!res.ok)
|
|
4832
|
-
throw new Error(await res.text());
|
|
4833
|
-
let fullContent = "";
|
|
4834
|
-
let finishReason = "";
|
|
4835
|
-
const tcAccum = {};
|
|
4836
|
-
const reader = res.body.getReader();
|
|
4837
|
-
const dec = new TextDecoder;
|
|
4838
|
-
let buf = "";
|
|
4839
|
-
outer:
|
|
4840
|
-
while (true) {
|
|
4841
|
-
const { done, value } = await reader.read();
|
|
4842
|
-
if (done)
|
|
4843
|
-
break;
|
|
4844
|
-
buf += dec.decode(value, { stream: true });
|
|
4845
|
-
const parts = buf.split(`
|
|
4846
|
-
|
|
4847
|
-
`);
|
|
4848
|
-
buf = parts.pop() ?? "";
|
|
4849
|
-
for (const part of parts) {
|
|
4850
|
-
let data = "";
|
|
4851
|
-
for (const line of part.split(`
|
|
4852
|
-
`)) {
|
|
4853
|
-
if (line.startsWith("data: ")) {
|
|
4854
|
-
data = line.slice(6);
|
|
4855
|
-
break;
|
|
4856
|
-
}
|
|
4857
|
-
}
|
|
4858
|
-
if (!data)
|
|
4859
|
-
continue;
|
|
4860
|
-
if (data === "[DONE]")
|
|
4861
|
-
break outer;
|
|
4862
|
-
let ev;
|
|
4863
|
-
try {
|
|
4864
|
-
ev = JSON.parse(data);
|
|
4865
|
-
} catch {
|
|
4866
|
-
continue;
|
|
4867
|
-
}
|
|
4868
|
-
if (ev.object === "error")
|
|
4869
|
-
throw new Error(JSON.stringify(ev));
|
|
4870
|
-
const choices = ev.choices;
|
|
4871
|
-
const choice = choices?.[0];
|
|
4872
|
-
if (!choice)
|
|
4873
|
-
continue;
|
|
4874
|
-
const fr = choice.finish_reason;
|
|
4875
|
-
if (fr)
|
|
4876
|
-
finishReason = fr;
|
|
4877
|
-
const delta = choice.delta;
|
|
4878
|
-
if (!delta)
|
|
4879
|
-
continue;
|
|
4880
|
-
if (typeof delta.content === "string" && delta.content) {
|
|
4881
|
-
fullContent += delta.content;
|
|
4882
|
-
emit({ type: "text_delta", text: delta.content });
|
|
4883
|
-
}
|
|
4884
|
-
const tcDeltas = delta.tool_calls;
|
|
4885
|
-
if (tcDeltas) {
|
|
4886
|
-
for (const tc of tcDeltas) {
|
|
4887
|
-
if (!tcAccum[tc.index])
|
|
4888
|
-
tcAccum[tc.index] = { id: "", name: "", args: "" };
|
|
4889
|
-
const entry = tcAccum[tc.index];
|
|
4890
|
-
if (tc.id)
|
|
4891
|
-
entry.id += tc.id;
|
|
4892
|
-
if (tc.function?.name)
|
|
4893
|
-
entry.name += tc.function.name;
|
|
4894
|
-
if (tc.function?.arguments)
|
|
4895
|
-
entry.args += tc.function.arguments;
|
|
4896
|
-
}
|
|
4897
|
-
}
|
|
4898
|
-
}
|
|
4899
|
-
}
|
|
4900
|
-
if (finishReason !== "tool_calls")
|
|
4901
|
-
return { content: fullContent, toolCalls };
|
|
4902
|
-
if (totalTools >= MAX_TOTAL_TOOLS) {
|
|
4903
|
-
return { content: `Agent stopped: reached ${MAX_TOTAL_TOOLS} tool calls. Please break your request into smaller steps.`, toolCalls };
|
|
4904
|
-
}
|
|
4905
|
-
const msgToolCalls = Object.values(tcAccum).map((tc) => ({
|
|
4906
|
-
id: tc.id,
|
|
4907
|
-
type: "function",
|
|
4908
|
-
function: { name: tc.name, arguments: tc.args }
|
|
4909
|
-
}));
|
|
4910
|
-
msgs.push({ role: "assistant", content: fullContent || null, tool_calls: msgToolCalls });
|
|
4911
|
-
for (const tc of Object.values(tcAccum)) {
|
|
4912
|
-
let args = {};
|
|
4913
|
-
try {
|
|
4914
|
-
args = JSON.parse(tc.args);
|
|
4915
|
-
} catch {}
|
|
4916
|
-
totalTools++;
|
|
4917
|
-
emit({ type: "tool_start", tool: tc.name, input: args });
|
|
4918
|
-
const result = await executeTool(tc.name, args);
|
|
4919
|
-
emit({ type: "tool_done", tool: tc.name, input: args, output: result.text, isError: result.isError });
|
|
4920
|
-
toolCalls.push({ tool: tc.name, input: args, output: result.text, isError: result.isError });
|
|
4921
|
-
msgs.push({ role: "tool", tool_call_id: tc.id, content: result.text });
|
|
4922
|
-
if (result.isError) {
|
|
4923
|
-
consecutiveErrors++;
|
|
4924
|
-
if (tc.name === "execute_api_request" && args.operationId) {
|
|
4925
|
-
const eid = String(args.operationId);
|
|
4926
|
-
endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
|
|
4927
|
-
if (endpointErrors[eid] >= MAX_SAME_ENDPOINT_ERRORS) {
|
|
4928
|
-
return { content: `Endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times. Last error: ${result.text}`, toolCalls };
|
|
4929
|
-
}
|
|
4930
|
-
}
|
|
4931
|
-
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
|
|
4932
|
-
return { content: `Stopped after ${MAX_CONSECUTIVE_ERRORS} consecutive errors. Last: ${result.text}`, toolCalls };
|
|
4933
|
-
}
|
|
4934
|
-
} else {
|
|
4935
|
-
consecutiveErrors = 0;
|
|
4936
|
-
}
|
|
4937
|
-
}
|
|
4938
|
-
}
|
|
4939
|
-
return { content: "(max iterations reached)", toolCalls };
|
|
4940
|
-
}
|
|
4941
5752
|
async function handleAiChat(req) {
|
|
4942
5753
|
let body;
|
|
4943
5754
|
try {
|
|
@@ -4948,39 +5759,58 @@ async function handleAiChat(req) {
|
|
|
4948
5759
|
const settingsRow = dbQueries.getSettings();
|
|
4949
5760
|
const settings = settingsRow ? JSON.parse(settingsRow.value) : {};
|
|
4950
5761
|
const ai = settings.ai ?? {};
|
|
5762
|
+
const provider = ai.provider ?? "anthropic";
|
|
5763
|
+
const providerDefaults = PROVIDER_DEFAULTS[provider] ?? { model: "" };
|
|
5764
|
+
const requiresKey = provider !== "ollama" && provider !== "custom";
|
|
5765
|
+
if (requiresKey && !ai.apiKey) {
|
|
5766
|
+
return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
|
|
5767
|
+
}
|
|
5768
|
+
if (!hasState())
|
|
5769
|
+
return json({ error: "No spec loaded." }, 400);
|
|
4951
5770
|
const { spec, operations } = getState();
|
|
4952
5771
|
const preview = operations.slice(0, 40).map((op) => `- ${op.method.toUpperCase()} ${op.path}${op.summary ? `: ${op.summary}` : ""}`).join(`
|
|
4953
5772
|
`);
|
|
4954
5773
|
const activeAuth = dbQueries.getActiveProfile();
|
|
4955
|
-
const authLine = activeAuth ? `Active auth: "${activeAuth.name}" (${activeAuth.type})` : "No active auth profile.
|
|
5774
|
+
const authLine = activeAuth ? `Active auth: "${activeAuth.name}" (${activeAuth.type})` : "No active auth profile. Call list_auth_profiles, then set_active_auth or save_auth_token.";
|
|
5775
|
+
const memory = dbQueries.getMemory(20);
|
|
5776
|
+
const memorySection = memory.length ? `
|
|
5777
|
+
## Memory from previous sessions
|
|
5778
|
+
${memory.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content.slice(0, 300)}${m.content.length > 300 ? "\u2026" : ""}`).join(`
|
|
5779
|
+
`)}
|
|
5780
|
+
` : "";
|
|
4956
5781
|
const system = `You are an AI assistant for the "${spec.title}" API (v${spec.version}). Base URL: ${spec.baseUrl}.
|
|
4957
5782
|
Total endpoints: ${operations.length}. Sample:
|
|
4958
5783
|
${preview}${operations.length > 40 ? `
|
|
4959
5784
|
... and ${operations.length - 40} more` : ""}
|
|
4960
5785
|
|
|
4961
5786
|
${authLine}
|
|
5787
|
+
${memorySection}
|
|
5788
|
+
Tools:
|
|
5789
|
+
- search_endpoints / get_endpoint_schema \u2014 explore API structure (results cached; never repeat the same query)
|
|
5790
|
+
- execute_api_request \u2014 call an endpoint
|
|
5791
|
+
- list_auth_profiles / set_active_auth / save_auth_token \u2014 manage credentials
|
|
5792
|
+
\u2022 save_auth_token supports token_type="basic" with username+password for HTTP Basic auth
|
|
5793
|
+
- fetch_url \u2014 external docs
|
|
5794
|
+
- dns_lookup \u2014 connectivity diagnostics
|
|
5795
|
+
- get_recent_logs \u2014 proxy traffic history
|
|
5796
|
+
- run_security_check \u2014 static security analysis
|
|
4962
5797
|
|
|
4963
|
-
|
|
4964
|
-
- search_endpoints / get_endpoint_schema: explore API structure
|
|
4965
|
-
- execute_api_request: call an endpoint
|
|
4966
|
-
- list_auth_profiles: list all saved auth profiles (name, type, active)
|
|
4967
|
-
- set_active_auth(name): switch to a saved profile before making requests
|
|
4968
|
-
- save_auth_token(name, token): IMMEDIATELY call this after a successful login that returns a token \u2014 saves the token as a named profile and activates it so subsequent requests are authenticated
|
|
4969
|
-
- fetch_url: fetch external docs
|
|
4970
|
-
- dns_lookup: DNS resolution / connectivity
|
|
4971
|
-
- get_recent_logs: recent request/response traffic
|
|
4972
|
-
- run_security_check: security analysis on an endpoint
|
|
5798
|
+
Auth workflow: 401/403 \u2192 list_auth_profiles \u2192 set_active_auth OR find login endpoint \u2192 save_auth_token \u2192 retry.
|
|
4973
5799
|
|
|
4974
|
-
|
|
5800
|
+
Rules:
|
|
5801
|
+
- Never repeat a search you already ran \u2014 results are cached.
|
|
5802
|
+
- Diagnose errors before retrying. Three failures on the same endpoint stops the agent.
|
|
5803
|
+
- Do not fire rapid successive API requests.
|
|
4975
5804
|
|
|
4976
|
-
|
|
5805
|
+
Be concise. Format code and JSON in fenced blocks.${ai.customInstructions ? `
|
|
4977
5806
|
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
5807
|
+
---
|
|
5808
|
+
## Custom instructions
|
|
5809
|
+
${ai.customInstructions}` : ""}${body.extra_context ? `
|
|
5810
|
+
|
|
5811
|
+
---
|
|
5812
|
+
## Context
|
|
5813
|
+
${body.extra_context}` : ""}`;
|
|
4984
5814
|
const { readable, writable } = new TransformStream;
|
|
4985
5815
|
const writer = writable.getWriter();
|
|
4986
5816
|
const enc = new TextEncoder;
|
|
@@ -4990,62 +5820,39 @@ Be concise and practical. Format code and JSON in code blocks.`;
|
|
|
4990
5820
|
`)).catch(() => {});
|
|
4991
5821
|
};
|
|
4992
5822
|
const msgs = body.messages;
|
|
5823
|
+
const toolCache = new Map;
|
|
5824
|
+
const abortCtrl = new AbortController;
|
|
5825
|
+
const lastUserMsg = [...msgs].reverse().find((m) => m.role === "user");
|
|
5826
|
+
const userMemoryContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : null;
|
|
4993
5827
|
(async () => {
|
|
4994
5828
|
try {
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
if (!ai.baseUrl) {
|
|
5015
|
-
emit({ type: "error", message: "Custom provider requires a Base URL." });
|
|
5016
|
-
await writer.close();
|
|
5017
|
-
return;
|
|
5018
|
-
}
|
|
5019
|
-
result = await openaiCompatibleLoop(ai.baseUrl.replace(/\/$/, ""), ai.apiKey, ai.model || "", {}, system, msgs, emit);
|
|
5020
|
-
} else if (provider === "ollama") {
|
|
5021
|
-
const base = (ai.baseUrl || "http://localhost:11434").replace(/\/$/, "");
|
|
5022
|
-
const res = await fetch(`${base}/api/chat`, {
|
|
5023
|
-
method: "POST",
|
|
5024
|
-
headers: { "Content-Type": "application/json" },
|
|
5025
|
-
body: JSON.stringify({ model: ai.model || "llama3", messages: [{ role: "system", content: system }, ...msgs], stream: false })
|
|
5026
|
-
});
|
|
5027
|
-
const d = await res.json();
|
|
5028
|
-
result = { content: d.message.content ?? "", toolCalls: [] };
|
|
5029
|
-
} else if (provider === "gemini") {
|
|
5030
|
-
const model = ai.model || "gemini-1.5-flash";
|
|
5031
|
-
const base = (ai.baseUrl || "https://generativelanguage.googleapis.com").replace(/\/$/, "");
|
|
5032
|
-
const res = await fetch(`${base}/v1beta/models/${model}:generateContent?key=${ai.apiKey}`, {
|
|
5033
|
-
method: "POST",
|
|
5034
|
-
headers: { "Content-Type": "application/json" },
|
|
5035
|
-
body: JSON.stringify({
|
|
5036
|
-
systemInstruction: { parts: [{ text: system }] },
|
|
5037
|
-
contents: msgs.map((m) => ({ role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.content }] })),
|
|
5038
|
-
generationConfig: { maxOutputTokens: 4096 }
|
|
5039
|
-
})
|
|
5040
|
-
});
|
|
5041
|
-
const d = await res.json();
|
|
5042
|
-
result = { content: d.candidates[0]?.content.parts[0]?.text ?? "", toolCalls: [] };
|
|
5043
|
-
} else {
|
|
5044
|
-
emit({ type: "error", message: `Unknown provider: ${provider}` });
|
|
5045
|
-
await writer.close();
|
|
5046
|
-
return;
|
|
5829
|
+
const result = await runAgentLoop({
|
|
5830
|
+
provider,
|
|
5831
|
+
apiKey: ai.apiKey,
|
|
5832
|
+
model: ai.model || providerDefaults.model,
|
|
5833
|
+
baseUrl: ai.baseUrl || providerDefaults.baseUrl,
|
|
5834
|
+
maxTokens: ai.maxTokens ?? 4096,
|
|
5835
|
+
stepTimeoutMs: ai.stepTimeoutMs ?? 60000,
|
|
5836
|
+
temperature: ai.temperature,
|
|
5837
|
+
topK: ai.topK && ai.topK > 0 ? ai.topK : undefined,
|
|
5838
|
+
parallelTools: true,
|
|
5839
|
+
enablePromptCache: true
|
|
5840
|
+
}, system, msgs, TOOL_SCHEMAS, (name, args) => executeTool(name, args, toolCache), emit, abortCtrl.signal, toolCache);
|
|
5841
|
+
if (result.content && result.stopReason !== "max_iterations") {
|
|
5842
|
+
try {
|
|
5843
|
+
if (userMemoryContent)
|
|
5844
|
+
dbQueries.saveMemory("user", userMemoryContent.slice(0, 1000));
|
|
5845
|
+
dbQueries.saveMemory("assistant", result.content.slice(0, 1000));
|
|
5846
|
+
dbQueries.trimMemory(40);
|
|
5847
|
+
} catch {}
|
|
5047
5848
|
}
|
|
5048
|
-
emit({
|
|
5849
|
+
emit({
|
|
5850
|
+
type: "done",
|
|
5851
|
+
content: result.content,
|
|
5852
|
+
toolCalls: result.toolCalls,
|
|
5853
|
+
stopReason: result.stopReason,
|
|
5854
|
+
tokens: result.tokens
|
|
5855
|
+
});
|
|
5049
5856
|
} catch (e) {
|
|
5050
5857
|
emit({ type: "error", message: e instanceof Error ? e.message : String(e) });
|
|
5051
5858
|
} finally {
|
|
@@ -5055,11 +5862,7 @@ Be concise and practical. Format code and JSON in code blocks.`;
|
|
|
5055
5862
|
}
|
|
5056
5863
|
})();
|
|
5057
5864
|
return new Response(readable, {
|
|
5058
|
-
headers: {
|
|
5059
|
-
"Content-Type": "text/event-stream",
|
|
5060
|
-
"Cache-Control": "no-cache",
|
|
5061
|
-
...CORS3
|
|
5062
|
-
}
|
|
5865
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...CORS4 }
|
|
5063
5866
|
});
|
|
5064
5867
|
}
|
|
5065
5868
|
function handleGetProfiles() {
|
|
@@ -5406,72 +6209,278 @@ async function handlePing(params) {
|
|
|
5406
6209
|
}
|
|
5407
6210
|
return json({ host, port, resolvedIp, checks, timestamp: new Date().toISOString() });
|
|
5408
6211
|
}
|
|
5409
|
-
function handleGetSaved() {
|
|
5410
|
-
return json(dbQueries.getSavedRequests());
|
|
6212
|
+
function handleGetSaved() {
|
|
6213
|
+
return json(dbQueries.getSavedRequests());
|
|
6214
|
+
}
|
|
6215
|
+
async function handleCreateSaved(req) {
|
|
6216
|
+
let body;
|
|
6217
|
+
try {
|
|
6218
|
+
body = await req.json();
|
|
6219
|
+
} catch {
|
|
6220
|
+
return badRequest("Invalid JSON");
|
|
6221
|
+
}
|
|
6222
|
+
if (!body.name || typeof body.name !== "string")
|
|
6223
|
+
return badRequest("name is required");
|
|
6224
|
+
const id = randomUUID2();
|
|
6225
|
+
dbQueries.insertSavedRequest({
|
|
6226
|
+
id,
|
|
6227
|
+
name: String(body.name),
|
|
6228
|
+
folder: String(body.folder ?? ""),
|
|
6229
|
+
method: String(body.method ?? "GET"),
|
|
6230
|
+
url: String(body.url ?? ""),
|
|
6231
|
+
headers: typeof body.headers === "string" ? body.headers : JSON.stringify(body.headers ?? []),
|
|
6232
|
+
params: typeof body.params === "string" ? body.params : JSON.stringify(body.params ?? []),
|
|
6233
|
+
body: String(body.body ?? ""),
|
|
6234
|
+
body_type: String(body.body_type ?? "none"),
|
|
6235
|
+
raw_type: String(body.raw_type ?? "text/plain"),
|
|
6236
|
+
form_rows: typeof body.form_rows === "string" ? body.form_rows : JSON.stringify(body.form_rows ?? []),
|
|
6237
|
+
auth: typeof body.auth === "string" ? body.auth : JSON.stringify(body.auth ?? {}),
|
|
6238
|
+
notes: String(body.notes ?? "")
|
|
6239
|
+
});
|
|
6240
|
+
return json(dbQueries.getSavedRequest(id), 201);
|
|
6241
|
+
}
|
|
6242
|
+
async function handleUpdateSaved(req, path) {
|
|
6243
|
+
const id = path.replace("/api/saved/", "");
|
|
6244
|
+
if (!dbQueries.getSavedRequest(id))
|
|
6245
|
+
return notFound();
|
|
6246
|
+
let body;
|
|
6247
|
+
try {
|
|
6248
|
+
body = await req.json();
|
|
6249
|
+
} catch {
|
|
6250
|
+
return badRequest("Invalid JSON");
|
|
6251
|
+
}
|
|
6252
|
+
const patch = {};
|
|
6253
|
+
const allowed = ["name", "folder", "method", "url", "headers", "params", "body", "body_type", "raw_type", "form_rows", "auth", "notes"];
|
|
6254
|
+
for (const key of allowed) {
|
|
6255
|
+
if (key in body)
|
|
6256
|
+
patch[key] = typeof body[key] === "string" ? String(body[key]) : JSON.stringify(body[key]);
|
|
6257
|
+
}
|
|
6258
|
+
if (Object.keys(patch).length)
|
|
6259
|
+
dbQueries.updateSavedRequest(id, patch);
|
|
6260
|
+
return json(dbQueries.getSavedRequest(id));
|
|
6261
|
+
}
|
|
6262
|
+
function handleDeleteSaved(path) {
|
|
6263
|
+
const id = path.replace("/api/saved/", "");
|
|
6264
|
+
if (!dbQueries.getSavedRequest(id))
|
|
6265
|
+
return notFound();
|
|
6266
|
+
dbQueries.deleteSavedRequest(id);
|
|
6267
|
+
return json({ ok: true });
|
|
6268
|
+
}
|
|
6269
|
+
function workflowRow(row) {
|
|
6270
|
+
if (!row)
|
|
6271
|
+
return null;
|
|
6272
|
+
let steps = [];
|
|
6273
|
+
try {
|
|
6274
|
+
steps = JSON.parse(row.steps);
|
|
6275
|
+
} catch {}
|
|
6276
|
+
return { ...row, steps };
|
|
6277
|
+
}
|
|
6278
|
+
function handleGetWorkflows() {
|
|
6279
|
+
const rows = dbQueries.getWorkflows().map((r) => workflowRow(r)).filter(Boolean);
|
|
6280
|
+
return json(rows);
|
|
6281
|
+
}
|
|
6282
|
+
async function handleCreateWorkflow(req) {
|
|
6283
|
+
let body;
|
|
6284
|
+
try {
|
|
6285
|
+
body = await req.json();
|
|
6286
|
+
} catch {
|
|
6287
|
+
return badRequest("Invalid JSON");
|
|
6288
|
+
}
|
|
6289
|
+
const id = randomUUID2();
|
|
6290
|
+
dbQueries.insertWorkflow({
|
|
6291
|
+
id,
|
|
6292
|
+
name: String(body.name ?? "Untitled Workflow"),
|
|
6293
|
+
description: String(body.description ?? ""),
|
|
6294
|
+
steps: typeof body.steps === "string" ? body.steps : JSON.stringify(body.steps ?? [])
|
|
6295
|
+
});
|
|
6296
|
+
return json(workflowRow(dbQueries.getWorkflow(id)), 201);
|
|
6297
|
+
}
|
|
6298
|
+
async function handleUpdateWorkflow(req, path) {
|
|
6299
|
+
const id = path.slice("/api/workflows/".length);
|
|
6300
|
+
if (!dbQueries.getWorkflow(id))
|
|
6301
|
+
return notFound();
|
|
6302
|
+
let body;
|
|
6303
|
+
try {
|
|
6304
|
+
body = await req.json();
|
|
6305
|
+
} catch {
|
|
6306
|
+
return badRequest("Invalid JSON");
|
|
6307
|
+
}
|
|
6308
|
+
const patch = {};
|
|
6309
|
+
if ("name" in body)
|
|
6310
|
+
patch.name = String(body.name);
|
|
6311
|
+
if ("description" in body)
|
|
6312
|
+
patch.description = String(body.description);
|
|
6313
|
+
if ("steps" in body)
|
|
6314
|
+
patch.steps = typeof body.steps === "string" ? body.steps : JSON.stringify(body.steps);
|
|
6315
|
+
if (Object.keys(patch).length)
|
|
6316
|
+
dbQueries.updateWorkflow(id, patch);
|
|
6317
|
+
return json(workflowRow(dbQueries.getWorkflow(id)));
|
|
6318
|
+
}
|
|
6319
|
+
function handleDeleteWorkflow(path) {
|
|
6320
|
+
const id = path.slice("/api/workflows/".length);
|
|
6321
|
+
if (!dbQueries.getWorkflow(id))
|
|
6322
|
+
return notFound();
|
|
6323
|
+
dbQueries.deleteWorkflow(id);
|
|
6324
|
+
return json({ ok: true });
|
|
5411
6325
|
}
|
|
5412
|
-
async function
|
|
6326
|
+
async function handleGenerateWorkflow(req) {
|
|
6327
|
+
if (!hasState())
|
|
6328
|
+
return badRequest("No spec loaded");
|
|
5413
6329
|
let body;
|
|
5414
6330
|
try {
|
|
5415
6331
|
body = await req.json();
|
|
5416
6332
|
} catch {
|
|
5417
6333
|
return badRequest("Invalid JSON");
|
|
5418
6334
|
}
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
const
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
6335
|
+
const settingsRow = dbQueries.getSettings();
|
|
6336
|
+
const settings = settingsRow ? JSON.parse(settingsRow.value) : {};
|
|
6337
|
+
const ai = settings.ai ?? {};
|
|
6338
|
+
const provider = ai.provider ?? "anthropic";
|
|
6339
|
+
if (provider !== "ollama" && !ai.apiKey) {
|
|
6340
|
+
return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
|
|
6341
|
+
}
|
|
6342
|
+
const { spec, operations } = getState();
|
|
6343
|
+
const endpointList = operations.slice(0, 80).map((op) => `${op.method.toUpperCase()} ${op.path}${op.operationId ? ` [${op.operationId}]` : ""}${op.summary ? ` \u2014 ${op.summary}` : ""}`).join(`
|
|
6344
|
+
`);
|
|
6345
|
+
const userPrompt = body.prompt?.trim() || "Generate a realistic end-to-end test workflow covering authentication and CRUD operations.";
|
|
6346
|
+
const systemMsg = `You generate API test workflows as JSON for the "${spec.title}" API (base: ${spec.baseUrl}).
|
|
6347
|
+
|
|
6348
|
+
Available endpoints:
|
|
6349
|
+
${endpointList}
|
|
6350
|
+
|
|
6351
|
+
Return ONLY valid JSON (no markdown fences) matching this schema exactly:
|
|
6352
|
+
{
|
|
6353
|
+
"name": "string",
|
|
6354
|
+
"description": "string",
|
|
6355
|
+
"steps": [
|
|
6356
|
+
{
|
|
6357
|
+
"id": "step_1",
|
|
6358
|
+
"label": "Human-readable name",
|
|
6359
|
+
"method": "GET|POST|PUT|PATCH|DELETE",
|
|
6360
|
+
"path": "/exact/path/from/spec",
|
|
6361
|
+
"operationId": "operationId or null",
|
|
6362
|
+
"pathParams": {},
|
|
6363
|
+
"queryParams": {},
|
|
6364
|
+
"headers": {},
|
|
6365
|
+
"body": null,
|
|
6366
|
+
"extract": [{"var": "varName", "path": "$.field.nested"}],
|
|
6367
|
+
"assert": [{"type": "status", "statusCode": 200}]
|
|
6368
|
+
}
|
|
6369
|
+
]
|
|
5438
6370
|
}
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
6371
|
+
|
|
6372
|
+
Rules:
|
|
6373
|
+
- Use {{varName}} in path/headers/body values to reference vars extracted in prior steps
|
|
6374
|
+
- For auth: extract token after login, set headers: {"Authorization": "Bearer {{token}}"}
|
|
6375
|
+
- Keep 3\u20138 steps covering a realistic user journey
|
|
6376
|
+
- Only use paths that exist in the endpoint list above`;
|
|
6377
|
+
try {
|
|
6378
|
+
let text = "";
|
|
6379
|
+
if (provider === "anthropic") {
|
|
6380
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
6381
|
+
method: "POST",
|
|
6382
|
+
headers: { "Content-Type": "application/json", "x-api-key": ai.apiKey, "anthropic-version": "2023-06-01" },
|
|
6383
|
+
body: JSON.stringify({ model: ai.model || "claude-sonnet-4-6", max_tokens: 4096, system: systemMsg, messages: [{ role: "user", content: userPrompt }] })
|
|
6384
|
+
});
|
|
6385
|
+
if (!res.ok)
|
|
6386
|
+
throw new Error(`Anthropic: ${await res.text()}`);
|
|
6387
|
+
const d = await res.json();
|
|
6388
|
+
text = d.content.find((b) => b.type === "text")?.text ?? "";
|
|
6389
|
+
} else {
|
|
6390
|
+
const base = (ai.baseUrl || (provider === "openai" ? "https://api.openai.com" : provider === "groq" ? "https://api.groq.com/openai" : provider === "mistral" ? "https://api.mistral.ai" : "https://api.openai.com")).replace(/\/$/, "");
|
|
6391
|
+
const hdrs = { "Content-Type": "application/json" };
|
|
6392
|
+
if (ai.apiKey)
|
|
6393
|
+
hdrs["Authorization"] = `Bearer ${ai.apiKey}`;
|
|
6394
|
+
const res = await fetch(`${base}/v1/chat/completions`, {
|
|
6395
|
+
method: "POST",
|
|
6396
|
+
headers: hdrs,
|
|
6397
|
+
body: JSON.stringify({ model: ai.model || "gpt-4o-mini", max_tokens: 2048, response_format: { type: "json_object" }, messages: [{ role: "system", content: systemMsg }, { role: "user", content: userPrompt }] })
|
|
6398
|
+
});
|
|
6399
|
+
if (!res.ok)
|
|
6400
|
+
throw new Error(await res.text());
|
|
6401
|
+
const d = await res.json();
|
|
6402
|
+
text = d.choices[0]?.message.content ?? "";
|
|
6403
|
+
}
|
|
6404
|
+
let parsed;
|
|
6405
|
+
try {
|
|
6406
|
+
parsed = JSON.parse(text);
|
|
6407
|
+
} catch {
|
|
6408
|
+
const m = text.match(/```(?:json)?\s*\n?([\s\S]+?)\n?```/);
|
|
6409
|
+
if (m)
|
|
6410
|
+
parsed = JSON.parse(m[1]);
|
|
6411
|
+
else
|
|
6412
|
+
throw new Error("AI response was not valid JSON");
|
|
6413
|
+
}
|
|
6414
|
+
return json(parsed);
|
|
6415
|
+
} catch (e) {
|
|
6416
|
+
return json({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
function handleRunWorkflow(path) {
|
|
6420
|
+
const id = path.slice("/api/workflows/".length, -"/run".length);
|
|
6421
|
+
const row = dbQueries.getWorkflow(id);
|
|
6422
|
+
if (!row)
|
|
5442
6423
|
return notFound();
|
|
5443
|
-
|
|
6424
|
+
if (!hasState())
|
|
6425
|
+
return badRequest("No spec loaded");
|
|
6426
|
+
let steps;
|
|
5444
6427
|
try {
|
|
5445
|
-
|
|
6428
|
+
steps = JSON.parse(row.steps);
|
|
5446
6429
|
} catch {
|
|
5447
|
-
return badRequest("Invalid JSON");
|
|
5448
|
-
}
|
|
5449
|
-
const patch = {};
|
|
5450
|
-
const allowed = ["name", "folder", "method", "url", "headers", "params", "body", "body_type", "raw_type", "form_rows", "auth", "notes"];
|
|
5451
|
-
for (const key of allowed) {
|
|
5452
|
-
if (key in body)
|
|
5453
|
-
patch[key] = typeof body[key] === "string" ? String(body[key]) : JSON.stringify(body[key]);
|
|
6430
|
+
return badRequest("Invalid workflow steps JSON");
|
|
5454
6431
|
}
|
|
5455
|
-
if (
|
|
5456
|
-
|
|
5457
|
-
|
|
6432
|
+
if (!steps.length)
|
|
6433
|
+
return badRequest("Workflow has no steps");
|
|
6434
|
+
const { readable, writable } = new TransformStream;
|
|
6435
|
+
const writer = writable.getWriter();
|
|
6436
|
+
const enc = new TextEncoder;
|
|
6437
|
+
const emit = (e) => {
|
|
6438
|
+
writer.write(enc.encode(`data: ${JSON.stringify(e)}
|
|
6439
|
+
|
|
6440
|
+
`)).catch(() => {});
|
|
6441
|
+
};
|
|
6442
|
+
(async () => {
|
|
6443
|
+
try {
|
|
6444
|
+
await runWorkflow(steps, emit);
|
|
6445
|
+
} catch (e) {
|
|
6446
|
+
emit({ type: "error", message: e instanceof Error ? e.message : String(e) });
|
|
6447
|
+
} finally {
|
|
6448
|
+
try {
|
|
6449
|
+
await writer.close();
|
|
6450
|
+
} catch {}
|
|
6451
|
+
}
|
|
6452
|
+
})();
|
|
6453
|
+
return new Response(readable, {
|
|
6454
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...CORS4 }
|
|
6455
|
+
});
|
|
5458
6456
|
}
|
|
5459
|
-
function
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
6457
|
+
function handleGetCaptureBins() {
|
|
6458
|
+
return json(dbQueries.getCaptureBins());
|
|
6459
|
+
}
|
|
6460
|
+
async function handleCreateCaptureBin(req) {
|
|
6461
|
+
const body = await req.json().catch(() => ({}));
|
|
6462
|
+
const id = randomUUID2().replace(/-/g, "").slice(0, 8);
|
|
6463
|
+
const name = String(body.name ?? "").trim() || "Untitled bin";
|
|
6464
|
+
dbQueries.insertCaptureBin(id, name);
|
|
6465
|
+
return json({ id, name, created_at: Math.floor(Date.now() / 1000) }, 201);
|
|
6466
|
+
}
|
|
6467
|
+
function handleDeleteCaptureBin(path) {
|
|
6468
|
+
const id = path.replace("/api/capture/bins/", "");
|
|
6469
|
+
dbQueries.deleteCaptureBin(id);
|
|
5464
6470
|
return json({ ok: true });
|
|
5465
6471
|
}
|
|
5466
|
-
var
|
|
6472
|
+
var CORS4, TOOL_DEFS, _lastApiCallMs = 0, MIN_API_CALL_INTERVAL_MS = 400, TOOL_SCHEMAS, PROVIDER_DEFAULTS;
|
|
5467
6473
|
var init_routes = __esm(() => {
|
|
5468
6474
|
init_db();
|
|
5469
6475
|
init_engine();
|
|
5470
6476
|
init_bus();
|
|
5471
6477
|
init_state();
|
|
6478
|
+
init_parser();
|
|
5472
6479
|
init_config();
|
|
5473
6480
|
init_version();
|
|
5474
|
-
|
|
6481
|
+
init_engine2();
|
|
6482
|
+
init_harness();
|
|
6483
|
+
CORS4 = {
|
|
5475
6484
|
"Access-Control-Allow-Origin": "*",
|
|
5476
6485
|
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
5477
6486
|
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
@@ -5539,25 +6548,34 @@ var init_routes = __esm(() => {
|
|
|
5539
6548
|
required: ["name"]
|
|
5540
6549
|
},
|
|
5541
6550
|
save_auth_token: {
|
|
5542
|
-
description: "Save a bearer token
|
|
6551
|
+
description: "Save a bearer token, API key, or basic auth credentials as a named auth profile and immediately activate it. Call this right after a successful login endpoint returns a token so all subsequent API requests are authenticated.",
|
|
5543
6552
|
params: {
|
|
5544
6553
|
name: { type: "string", description: 'Profile name, e.g. "user session" or the username' },
|
|
5545
|
-
token: { type: "string", description: "The bearer token or API key value
|
|
5546
|
-
token_type: { type: "string", enum: ["bearer", "apikey_header", "apikey_query"], description: "Token type (default: bearer)" },
|
|
5547
|
-
header_name: { type: "string", description: "Header name for apikey_header type (default: X-Api-Key)" }
|
|
6554
|
+
token: { type: "string", description: "The bearer token or API key value (omit for basic auth)" },
|
|
6555
|
+
token_type: { type: "string", enum: ["bearer", "apikey_header", "apikey_query", "basic"], description: "Token type (default: bearer)" },
|
|
6556
|
+
header_name: { type: "string", description: "Header name for apikey_header type (default: X-Api-Key)" },
|
|
6557
|
+
username: { type: "string", description: "Username for basic auth" },
|
|
6558
|
+
password: { type: "string", description: "Password for basic auth" }
|
|
5548
6559
|
},
|
|
5549
|
-
required: ["name"
|
|
6560
|
+
required: ["name"]
|
|
5550
6561
|
}
|
|
5551
6562
|
};
|
|
5552
|
-
|
|
6563
|
+
TOOL_SCHEMAS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
|
|
5553
6564
|
name,
|
|
5554
6565
|
description: def.description,
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
OPENAI_TOOLS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
|
|
5558
|
-
type: "function",
|
|
5559
|
-
function: { name, description: def.description, parameters: { type: "object", properties: def.params, required: def.required } }
|
|
6566
|
+
params: def.params,
|
|
6567
|
+
required: def.required
|
|
5560
6568
|
}));
|
|
6569
|
+
PROVIDER_DEFAULTS = {
|
|
6570
|
+
anthropic: { model: "claude-haiku-4-5-20251001" },
|
|
6571
|
+
openai: { model: "gpt-4o-mini", baseUrl: "https://api.openai.com" },
|
|
6572
|
+
mistral: { model: "mistral-small-latest", baseUrl: "https://api.mistral.ai" },
|
|
6573
|
+
groq: { model: "llama-3.1-70b-versatile", baseUrl: "https://api.groq.com/openai" },
|
|
6574
|
+
"github-copilot": { model: "gpt-4o", baseUrl: "https://api.githubcopilot.com" },
|
|
6575
|
+
ollama: { model: "llama3", baseUrl: "http://localhost:11434" },
|
|
6576
|
+
gemini: { model: "gemini-1.5-flash" },
|
|
6577
|
+
custom: { model: "" }
|
|
6578
|
+
};
|
|
5561
6579
|
});
|
|
5562
6580
|
|
|
5563
6581
|
// src/daemon.ts
|
|
@@ -5677,7 +6695,7 @@ function printBanner(opts) {
|
|
|
5677
6695
|
const hint = [
|
|
5678
6696
|
`${paint.bold("r")} reload`,
|
|
5679
6697
|
`${paint.bold("b")} background`,
|
|
5680
|
-
`${paint.bold("/")} commands`,
|
|
6698
|
+
`${paint.bold("/")} commands ${paint.dim("(Tab to complete)")}`,
|
|
5681
6699
|
`${paint.bold("q")} quit`,
|
|
5682
6700
|
`${paint.bold("?")} help`
|
|
5683
6701
|
].join(` ${dot} `);
|
|
@@ -5908,6 +6926,392 @@ var init_update = __esm(() => {
|
|
|
5908
6926
|
CHECK_INTERVAL = 24 * 60 * 60 * 1000;
|
|
5909
6927
|
});
|
|
5910
6928
|
|
|
6929
|
+
// src/repl.ts
|
|
6930
|
+
function stripAnsi(s) {
|
|
6931
|
+
return s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
|
|
6932
|
+
}
|
|
6933
|
+
function visualRows(line, cols) {
|
|
6934
|
+
const len = stripAnsi(line).length;
|
|
6935
|
+
if (len === 0)
|
|
6936
|
+
return 1;
|
|
6937
|
+
return Math.ceil(len / cols);
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6940
|
+
class Repl {
|
|
6941
|
+
buf = "";
|
|
6942
|
+
history = [];
|
|
6943
|
+
histIdx = -1;
|
|
6944
|
+
savedBuf = "";
|
|
6945
|
+
running = false;
|
|
6946
|
+
statusText = "";
|
|
6947
|
+
promptDrawn = false;
|
|
6948
|
+
drawingPrompt = false;
|
|
6949
|
+
promptVisualRows = 0;
|
|
6950
|
+
onCmd = null;
|
|
6951
|
+
cols = process.stdout.columns || 80;
|
|
6952
|
+
dynSuggestions = [];
|
|
6953
|
+
constructor() {
|
|
6954
|
+
if (isTTY) {
|
|
6955
|
+
process.stdout.on("resize", () => {
|
|
6956
|
+
this.cols = process.stdout.columns || 80;
|
|
6957
|
+
if (this.running)
|
|
6958
|
+
this.redraw();
|
|
6959
|
+
});
|
|
6960
|
+
}
|
|
6961
|
+
}
|
|
6962
|
+
setDynamicSuggestions(items) {
|
|
6963
|
+
this.dynSuggestions = items;
|
|
6964
|
+
if (this.running)
|
|
6965
|
+
this.redraw();
|
|
6966
|
+
}
|
|
6967
|
+
setStatus(text) {
|
|
6968
|
+
this.statusText = text;
|
|
6969
|
+
if (this.running)
|
|
6970
|
+
this.redraw();
|
|
6971
|
+
}
|
|
6972
|
+
print(line) {
|
|
6973
|
+
process.stdout.write(line + `
|
|
6974
|
+
`);
|
|
6975
|
+
}
|
|
6976
|
+
start(onCmd) {
|
|
6977
|
+
this.onCmd = onCmd;
|
|
6978
|
+
this.running = true;
|
|
6979
|
+
if (!isTTY || !process.stdin.setRawMode) {
|
|
6980
|
+
this.startSimple(onCmd);
|
|
6981
|
+
return;
|
|
6982
|
+
}
|
|
6983
|
+
const origWrite = process.stdout.write.bind(process.stdout);
|
|
6984
|
+
const self = this;
|
|
6985
|
+
const patched = function(chunk, enc, cb) {
|
|
6986
|
+
if (self.drawingPrompt)
|
|
6987
|
+
return origWrite(chunk, enc, cb);
|
|
6988
|
+
const str = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? new TextDecoder().decode(chunk) : String(chunk);
|
|
6989
|
+
const isSpinnerWrite = str.startsWith("\r") && !str.includes(`
|
|
6990
|
+
`);
|
|
6991
|
+
if (isSpinnerWrite || !self.promptDrawn)
|
|
6992
|
+
return origWrite(chunk, enc, cb);
|
|
6993
|
+
self.drawingPrompt = true;
|
|
6994
|
+
for (let i = 0;i < self.promptVisualRows - 1; i++)
|
|
6995
|
+
origWrite("\x1B[A");
|
|
6996
|
+
origWrite("\r\x1B[J");
|
|
6997
|
+
self.promptDrawn = false;
|
|
6998
|
+
const r = origWrite(chunk, enc, cb);
|
|
6999
|
+
if (!str.endsWith(`
|
|
7000
|
+
`))
|
|
7001
|
+
origWrite(`
|
|
7002
|
+
`);
|
|
7003
|
+
const lines = self.buildPromptLines();
|
|
7004
|
+
self.promptVisualRows = lines.reduce((sum, l) => sum + visualRows(l, self.cols), 0);
|
|
7005
|
+
origWrite(lines.join(`
|
|
7006
|
+
`));
|
|
7007
|
+
self.promptDrawn = true;
|
|
7008
|
+
self.drawingPrompt = false;
|
|
7009
|
+
return r;
|
|
7010
|
+
};
|
|
7011
|
+
patched.__orig = origWrite;
|
|
7012
|
+
process.stdout.write = patched;
|
|
7013
|
+
process.stdin.setRawMode(true);
|
|
7014
|
+
process.stdin.resume();
|
|
7015
|
+
process.stdin.setEncoding("utf8");
|
|
7016
|
+
process.stdin.on("data", (key) => {
|
|
7017
|
+
this.handleKey(key).catch(() => {});
|
|
7018
|
+
});
|
|
7019
|
+
this.drawPrompt();
|
|
7020
|
+
}
|
|
7021
|
+
stop() {
|
|
7022
|
+
this.running = false;
|
|
7023
|
+
if (this.promptDrawn)
|
|
7024
|
+
this.clearPrompt();
|
|
7025
|
+
if (process.stdout.write.__orig) {
|
|
7026
|
+
process.stdout.write = process.stdout.write.__orig;
|
|
7027
|
+
}
|
|
7028
|
+
try {
|
|
7029
|
+
process.stdin.setRawMode(false);
|
|
7030
|
+
process.stdin.pause();
|
|
7031
|
+
} catch {}
|
|
7032
|
+
}
|
|
7033
|
+
getSuggestions() {
|
|
7034
|
+
if (!this.buf.startsWith("/"))
|
|
7035
|
+
return [];
|
|
7036
|
+
const raw = this.buf.slice(1);
|
|
7037
|
+
if (!raw)
|
|
7038
|
+
return BASE.slice(0, 6);
|
|
7039
|
+
const parts = raw.split(" ");
|
|
7040
|
+
const base = parts[0] ?? "";
|
|
7041
|
+
if (raw.startsWith("auth use ") && this.dynSuggestions.length) {
|
|
7042
|
+
const typed = raw.slice("auth use ".length);
|
|
7043
|
+
return this.dynSuggestions.filter((s) => s.label.toLowerCase().startsWith(typed.toLowerCase()));
|
|
7044
|
+
}
|
|
7045
|
+
if (parts.length >= 2 && SUB[base]) {
|
|
7046
|
+
return (SUB[base] ?? []).filter((s) => s.value.startsWith(raw));
|
|
7047
|
+
}
|
|
7048
|
+
return BASE.filter((c) => c.value.startsWith(raw));
|
|
7049
|
+
}
|
|
7050
|
+
getGhostText() {
|
|
7051
|
+
if (!this.buf.startsWith("/"))
|
|
7052
|
+
return "";
|
|
7053
|
+
const suggestions = this.getSuggestions();
|
|
7054
|
+
if (!suggestions.length)
|
|
7055
|
+
return "";
|
|
7056
|
+
const first = suggestions[0];
|
|
7057
|
+
const full = "/" + first.value;
|
|
7058
|
+
if (full === this.buf || !full.startsWith(this.buf))
|
|
7059
|
+
return "";
|
|
7060
|
+
return full.slice(this.buf.length);
|
|
7061
|
+
}
|
|
7062
|
+
buildPromptLines() {
|
|
7063
|
+
const lines = [];
|
|
7064
|
+
const suggestions = this.getSuggestions();
|
|
7065
|
+
const DOT = paint.dim(" \xB7 ");
|
|
7066
|
+
if (this.buf.startsWith("/") && suggestions.length > 0) {
|
|
7067
|
+
const items = suggestions.slice(0, 5);
|
|
7068
|
+
const row = items.map((s, i) => {
|
|
7069
|
+
const label = "/" + s.label;
|
|
7070
|
+
const desc = s.desc ? paint.dim(" " + s.desc) : "";
|
|
7071
|
+
return i === 0 ? paint.cyan(label) + desc : paint.dim(label);
|
|
7072
|
+
}).join(DOT);
|
|
7073
|
+
lines.push(` ${row}`);
|
|
7074
|
+
}
|
|
7075
|
+
if (this.statusText) {
|
|
7076
|
+
const plain = stripAnsi(this.statusText);
|
|
7077
|
+
const truncated = plain.length > this.cols - 4 ? plain.slice(0, this.cols - 7) + "\u2026" : plain;
|
|
7078
|
+
lines.push(` ${paint.dim(truncated)}`);
|
|
7079
|
+
}
|
|
7080
|
+
const ghost = this.getGhostText();
|
|
7081
|
+
lines.push(` ${paint.cyan("\u276F")} ${this.buf}${ghost ? paint.dim(ghost) : ""}`);
|
|
7082
|
+
return lines;
|
|
7083
|
+
}
|
|
7084
|
+
clearPrompt() {
|
|
7085
|
+
if (!this.promptDrawn)
|
|
7086
|
+
return;
|
|
7087
|
+
this.drawingPrompt = true;
|
|
7088
|
+
for (let i = 0;i < this.promptVisualRows - 1; i++)
|
|
7089
|
+
process.stdout.write("\x1B[A");
|
|
7090
|
+
process.stdout.write("\r\x1B[J");
|
|
7091
|
+
this.promptDrawn = false;
|
|
7092
|
+
this.drawingPrompt = false;
|
|
7093
|
+
}
|
|
7094
|
+
drawPrompt() {
|
|
7095
|
+
this.drawingPrompt = true;
|
|
7096
|
+
const lines = this.buildPromptLines();
|
|
7097
|
+
this.promptVisualRows = lines.reduce((sum, l) => sum + visualRows(l, this.cols), 0);
|
|
7098
|
+
process.stdout.write(lines.join(`
|
|
7099
|
+
`));
|
|
7100
|
+
this.promptDrawn = true;
|
|
7101
|
+
this.drawingPrompt = false;
|
|
7102
|
+
}
|
|
7103
|
+
redraw() {
|
|
7104
|
+
this.clearPrompt();
|
|
7105
|
+
this.drawPrompt();
|
|
7106
|
+
}
|
|
7107
|
+
async handleKey(key) {
|
|
7108
|
+
if (!this.running)
|
|
7109
|
+
return;
|
|
7110
|
+
if (key === "\x03" || key === "\x04") {
|
|
7111
|
+
this.stop();
|
|
7112
|
+
process.emit("SIGINT");
|
|
7113
|
+
return;
|
|
7114
|
+
}
|
|
7115
|
+
if (key === "\f") {
|
|
7116
|
+
this.drawingPrompt = true;
|
|
7117
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
7118
|
+
this.drawingPrompt = false;
|
|
7119
|
+
this.promptDrawn = false;
|
|
7120
|
+
this.drawPrompt();
|
|
7121
|
+
return;
|
|
7122
|
+
}
|
|
7123
|
+
if (key === "\x15") {
|
|
7124
|
+
this.buf = "";
|
|
7125
|
+
this.redraw();
|
|
7126
|
+
return;
|
|
7127
|
+
}
|
|
7128
|
+
if (key === "\x17") {
|
|
7129
|
+
this.buf = this.buf.replace(/\S+\s*$/, "");
|
|
7130
|
+
this.redraw();
|
|
7131
|
+
return;
|
|
7132
|
+
}
|
|
7133
|
+
if (key.startsWith("\x1B")) {
|
|
7134
|
+
if (key === "\x1B") {
|
|
7135
|
+
this.buf = "";
|
|
7136
|
+
this.redraw();
|
|
7137
|
+
return;
|
|
7138
|
+
}
|
|
7139
|
+
if (key === "\x1B[A") {
|
|
7140
|
+
this.historyUp();
|
|
7141
|
+
return;
|
|
7142
|
+
}
|
|
7143
|
+
if (key === "\x1B[B") {
|
|
7144
|
+
this.historyDown();
|
|
7145
|
+
return;
|
|
7146
|
+
}
|
|
7147
|
+
if (key === "\x1B[C") {
|
|
7148
|
+
const g = this.getGhostText();
|
|
7149
|
+
if (g) {
|
|
7150
|
+
this.buf += g;
|
|
7151
|
+
const raw = this.buf.slice(1);
|
|
7152
|
+
if (SUB[raw])
|
|
7153
|
+
this.buf += " ";
|
|
7154
|
+
this.redraw();
|
|
7155
|
+
}
|
|
7156
|
+
return;
|
|
7157
|
+
}
|
|
7158
|
+
return;
|
|
7159
|
+
}
|
|
7160
|
+
if (key === "\t") {
|
|
7161
|
+
const g = this.getGhostText();
|
|
7162
|
+
if (g) {
|
|
7163
|
+
this.buf += g;
|
|
7164
|
+
const raw = this.buf.slice(1);
|
|
7165
|
+
if (SUB[raw])
|
|
7166
|
+
this.buf += " ";
|
|
7167
|
+
} else {
|
|
7168
|
+
const suggestions = this.getSuggestions();
|
|
7169
|
+
if (suggestions[0] && "/" + suggestions[0].value !== this.buf) {
|
|
7170
|
+
this.buf = "/" + suggestions[0].value;
|
|
7171
|
+
}
|
|
7172
|
+
}
|
|
7173
|
+
this.redraw();
|
|
7174
|
+
return;
|
|
7175
|
+
}
|
|
7176
|
+
if (key === "\x7F" || key === "\b") {
|
|
7177
|
+
if (this.buf.length > 0) {
|
|
7178
|
+
this.buf = this.buf.slice(0, -1);
|
|
7179
|
+
this.redraw();
|
|
7180
|
+
}
|
|
7181
|
+
return;
|
|
7182
|
+
}
|
|
7183
|
+
if (key === "\r" || key === `
|
|
7184
|
+
`) {
|
|
7185
|
+
await this.submit();
|
|
7186
|
+
return;
|
|
7187
|
+
}
|
|
7188
|
+
if (this.buf === "") {
|
|
7189
|
+
switch (key.toLowerCase()) {
|
|
7190
|
+
case "r":
|
|
7191
|
+
await this.dispatchImmediate("r");
|
|
7192
|
+
return;
|
|
7193
|
+
case "b":
|
|
7194
|
+
await this.dispatchImmediate("b");
|
|
7195
|
+
return;
|
|
7196
|
+
case "s":
|
|
7197
|
+
await this.dispatchImmediate("s");
|
|
7198
|
+
return;
|
|
7199
|
+
case "q":
|
|
7200
|
+
await this.dispatchImmediate("q");
|
|
7201
|
+
return;
|
|
7202
|
+
case "?":
|
|
7203
|
+
case "h":
|
|
7204
|
+
await this.dispatchImmediate("h");
|
|
7205
|
+
return;
|
|
7206
|
+
case "/":
|
|
7207
|
+
this.buf = "/";
|
|
7208
|
+
this.redraw();
|
|
7209
|
+
return;
|
|
7210
|
+
}
|
|
7211
|
+
}
|
|
7212
|
+
const printable = key.replace(/[^\x20-\x7E]/g, "");
|
|
7213
|
+
if (printable) {
|
|
7214
|
+
this.buf += printable;
|
|
7215
|
+
this.redraw();
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
async submit() {
|
|
7219
|
+
const cmd = this.buf.trim();
|
|
7220
|
+
this.buf = "";
|
|
7221
|
+
this.clearPrompt();
|
|
7222
|
+
process.stdout.write(`
|
|
7223
|
+
`);
|
|
7224
|
+
this.promptDrawn = false;
|
|
7225
|
+
if (cmd) {
|
|
7226
|
+
this.history.unshift(cmd);
|
|
7227
|
+
if (this.history.length > 200)
|
|
7228
|
+
this.history.pop();
|
|
7229
|
+
this.histIdx = -1;
|
|
7230
|
+
this.savedBuf = "";
|
|
7231
|
+
if (this.onCmd)
|
|
7232
|
+
await this.onCmd(cmd);
|
|
7233
|
+
}
|
|
7234
|
+
this.drawPrompt();
|
|
7235
|
+
}
|
|
7236
|
+
async dispatchImmediate(key) {
|
|
7237
|
+
this.clearPrompt();
|
|
7238
|
+
process.stdout.write(`
|
|
7239
|
+
`);
|
|
7240
|
+
this.promptDrawn = false;
|
|
7241
|
+
if (this.onCmd)
|
|
7242
|
+
await this.onCmd(key);
|
|
7243
|
+
this.drawPrompt();
|
|
7244
|
+
}
|
|
7245
|
+
historyUp() {
|
|
7246
|
+
if (!this.history.length)
|
|
7247
|
+
return;
|
|
7248
|
+
if (this.histIdx === -1)
|
|
7249
|
+
this.savedBuf = this.buf;
|
|
7250
|
+
this.histIdx = Math.min(this.histIdx + 1, this.history.length - 1);
|
|
7251
|
+
this.buf = this.history[this.histIdx] ?? "";
|
|
7252
|
+
this.redraw();
|
|
7253
|
+
}
|
|
7254
|
+
historyDown() {
|
|
7255
|
+
if (this.histIdx === -1)
|
|
7256
|
+
return;
|
|
7257
|
+
this.histIdx--;
|
|
7258
|
+
this.buf = this.histIdx === -1 ? this.savedBuf : this.history[this.histIdx] ?? "";
|
|
7259
|
+
this.redraw();
|
|
7260
|
+
}
|
|
7261
|
+
startSimple(onCmd) {
|
|
7262
|
+
process.stdout.write(" \u276F ");
|
|
7263
|
+
process.stdin.setEncoding("utf8");
|
|
7264
|
+
let line = "";
|
|
7265
|
+
process.stdin.on("data", async (chunk) => {
|
|
7266
|
+
for (const ch of chunk) {
|
|
7267
|
+
if (ch === `
|
|
7268
|
+
` || ch === "\r") {
|
|
7269
|
+
const cmd = line.trim();
|
|
7270
|
+
line = "";
|
|
7271
|
+
if (cmd)
|
|
7272
|
+
await onCmd(cmd);
|
|
7273
|
+
process.stdout.write(" \u276F ");
|
|
7274
|
+
} else {
|
|
7275
|
+
line += ch;
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
});
|
|
7279
|
+
}
|
|
7280
|
+
}
|
|
7281
|
+
var BASE, SUB;
|
|
7282
|
+
var init_repl = __esm(() => {
|
|
7283
|
+
init_ui();
|
|
7284
|
+
BASE = [
|
|
7285
|
+
{ value: "help", label: "help", desc: "Show all commands" },
|
|
7286
|
+
{ value: "status", label: "status", desc: "Show server status" },
|
|
7287
|
+
{ value: "reload", label: "reload", desc: "Hot-reload the spec" },
|
|
7288
|
+
{ value: "spec", label: "spec", desc: "Load a different spec" },
|
|
7289
|
+
{ value: "mcp", label: "mcp", desc: "Toggle MCP endpoint" },
|
|
7290
|
+
{ value: "proxy", label: "proxy", desc: "Toggle HTTP proxy" },
|
|
7291
|
+
{ value: "ai", label: "ai", desc: "Toggle AI chat" },
|
|
7292
|
+
{ value: "readonly", label: "readonly", desc: "Toggle read-only mode" },
|
|
7293
|
+
{ value: "auth", label: "auth", desc: "Manage auth roles" },
|
|
7294
|
+
{ value: "token", label: "token", desc: "Manage access token" },
|
|
7295
|
+
{ value: "tail", label: "tail", desc: "Live request log" },
|
|
7296
|
+
{ value: "open", label: "open", desc: "Open studio in browser" },
|
|
7297
|
+
{ value: "update", label: "update", desc: "Update wasper" },
|
|
7298
|
+
{ value: "quit", label: "quit", desc: "Quit" }
|
|
7299
|
+
];
|
|
7300
|
+
SUB = {
|
|
7301
|
+
auth: [
|
|
7302
|
+
{ value: "auth list", label: "list", desc: "List auth profiles" },
|
|
7303
|
+
{ value: "auth use ", label: "use", desc: "Switch active profile" },
|
|
7304
|
+
{ value: "auth none", label: "none", desc: "Disable auth" }
|
|
7305
|
+
],
|
|
7306
|
+
mcp: [{ value: "mcp on", label: "on", desc: "" }, { value: "mcp off", label: "off", desc: "" }],
|
|
7307
|
+
proxy: [{ value: "proxy on", label: "on", desc: "" }, { value: "proxy off", label: "off", desc: "" }],
|
|
7308
|
+
ai: [{ value: "ai on", label: "on", desc: "" }, { value: "ai off", label: "off", desc: "" }],
|
|
7309
|
+
readonly: [{ value: "readonly on", label: "on", desc: "" }, { value: "readonly off", label: "off", desc: "" }],
|
|
7310
|
+
token: [{ value: "token new", label: "new", desc: "Generate token" }, { value: "token off", label: "off", desc: "Remove token" }],
|
|
7311
|
+
tail: [{ value: "tail on", label: "on", desc: "" }, { value: "tail off", label: "off", desc: "" }]
|
|
7312
|
+
};
|
|
7313
|
+
});
|
|
7314
|
+
|
|
5911
7315
|
// src/commands/start.ts
|
|
5912
7316
|
var exports_start = {};
|
|
5913
7317
|
__export(exports_start, {
|
|
@@ -5942,18 +7346,44 @@ async function run2(overrideOpts) {
|
|
|
5942
7346
|
printHelp();
|
|
5943
7347
|
process.exit(0);
|
|
5944
7348
|
}
|
|
5945
|
-
|
|
7349
|
+
let specUrl = overrideOpts?.url ?? (values.url ? String(values.url) : null) ?? process.env.WASPER_SPEC_URL ?? null;
|
|
5946
7350
|
const PORT = overrideOpts?.port ?? parseInt(String(values.port ?? "3388"), 10);
|
|
5947
7351
|
const HOST = overrideOpts?.host ?? (values.host ? String(values.host) : null) ?? process.env.WASPER_HOST ?? "0.0.0.0";
|
|
5948
7352
|
const ORIGIN = (overrideOpts?.origin ?? (values.origin ? String(values.origin) : null) ?? process.env.WASPER_ORIGIN ?? null)?.replace(/\/$/, "") ?? null;
|
|
5949
7353
|
const TOKEN = overrideOpts?.token ?? (values.token ? String(values.token) : null) ?? process.env.WASPER_TOKEN ?? null;
|
|
5950
7354
|
const bgNow = overrideOpts?.daemon ?? !!(values.background || values.daemon);
|
|
5951
7355
|
const isDaemon = overrideOpts?.isDaemon ?? !!values["_daemon"];
|
|
7356
|
+
if (!specUrl && !isDaemon) {
|
|
7357
|
+
const last = dbQueries.getLastSpec();
|
|
7358
|
+
if (last) {
|
|
7359
|
+
specUrl = last.url;
|
|
7360
|
+
if (isTTY)
|
|
7361
|
+
console.log(` ${paint.dim("\u21A9")} Resuming ${paint.cyan(last.title ?? last.url)} ${paint.dim("(last used)")}
|
|
7362
|
+
`);
|
|
7363
|
+
}
|
|
7364
|
+
}
|
|
5952
7365
|
setServerConfig({ port: PORT, host: HOST, origin: ORIGIN, token: TOKEN });
|
|
7366
|
+
{
|
|
7367
|
+
const saved = dbQueries.getSetting("features");
|
|
7368
|
+
if (saved) {
|
|
7369
|
+
try {
|
|
7370
|
+
const obj = JSON.parse(saved);
|
|
7371
|
+
const patch = {};
|
|
7372
|
+
if (obj.mcp === false)
|
|
7373
|
+
patch.mcp = false;
|
|
7374
|
+
if (obj.proxy === false)
|
|
7375
|
+
patch.proxy = false;
|
|
7376
|
+
if (obj.ai === false)
|
|
7377
|
+
patch.ai = false;
|
|
7378
|
+
if (Object.keys(patch).length)
|
|
7379
|
+
setFeatures(patch);
|
|
7380
|
+
} catch {}
|
|
7381
|
+
}
|
|
7382
|
+
}
|
|
5953
7383
|
setFeatures({
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
7384
|
+
...values["no-mcp"] ? { mcp: false } : {},
|
|
7385
|
+
...values["no-proxy"] ? { proxy: false } : {},
|
|
7386
|
+
...values["no-ai"] ? { ai: false } : {},
|
|
5957
7387
|
readonly: !!values.readonly
|
|
5958
7388
|
});
|
|
5959
7389
|
if (bgNow) {
|
|
@@ -5980,6 +7410,7 @@ async function run2(overrideOpts) {
|
|
|
5980
7410
|
specVersion = state.spec.version;
|
|
5981
7411
|
endpointCount = state.operations.length;
|
|
5982
7412
|
spinner.stop();
|
|
7413
|
+
dbQueries.upsertSpec(specUrl, specTitle ?? null, specVersion ?? null, endpointCount);
|
|
5983
7414
|
} catch (e) {
|
|
5984
7415
|
spinner.stop("\u2717", `Failed to load spec: ${e instanceof Error ? e.message : String(e)}`, "red");
|
|
5985
7416
|
}
|
|
@@ -6002,6 +7433,8 @@ async function run2(overrideOpts) {
|
|
|
6002
7433
|
if (req.method === "OPTIONS") {
|
|
6003
7434
|
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
6004
7435
|
}
|
|
7436
|
+
if (pathname.startsWith("/c/"))
|
|
7437
|
+
return captureHandler(req);
|
|
6005
7438
|
if (!isAuthorized(req)) {
|
|
6006
7439
|
return new Response(JSON.stringify({ error: "Unauthorized: pass Authorization: Bearer <token> or ?token=" }), {
|
|
6007
7440
|
status: 401,
|
|
@@ -6092,127 +7525,129 @@ async function run2(overrideOpts) {
|
|
|
6092
7525
|
function attachKeyboard(opts) {
|
|
6093
7526
|
const ctx = { specUrl: opts.specUrl, PORT: opts.PORT, tailOff: null };
|
|
6094
7527
|
let isReloading = false;
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
7528
|
+
const repl = new Repl;
|
|
7529
|
+
const buildStatus = () => {
|
|
7530
|
+
const state = hasState() ? getState() : null;
|
|
7531
|
+
const f = getFeatures();
|
|
7532
|
+
const cfg = getServerConfig();
|
|
7533
|
+
const active = dbQueries.getActiveProfile();
|
|
7534
|
+
const parts = [];
|
|
7535
|
+
if (state) {
|
|
7536
|
+
parts.push(`${state.spec.title} v${state.spec.version} \xB7 ${state.operations.length} ep`);
|
|
7537
|
+
} else {
|
|
7538
|
+
parts.push("no spec");
|
|
7539
|
+
}
|
|
7540
|
+
parts.push(`:${ctx.PORT}`);
|
|
7541
|
+
const flags = [];
|
|
7542
|
+
if (!f.mcp)
|
|
7543
|
+
flags.push("mcp:off");
|
|
7544
|
+
if (!f.proxy)
|
|
7545
|
+
flags.push("proxy:off");
|
|
7546
|
+
if (!f.ai)
|
|
7547
|
+
flags.push("ai:off");
|
|
7548
|
+
if (f.readonly)
|
|
7549
|
+
flags.push("readonly:on");
|
|
7550
|
+
if (flags.length)
|
|
7551
|
+
parts.push(flags.join(" "));
|
|
7552
|
+
parts.push(`auth:${active ? active.name : "none"}`);
|
|
7553
|
+
if (cfg.token)
|
|
7554
|
+
parts.push("token:set");
|
|
7555
|
+
if (ctx.tailOff)
|
|
7556
|
+
parts.push("tail:on");
|
|
7557
|
+
return parts.join(" \xB7 ");
|
|
7558
|
+
};
|
|
7559
|
+
const refreshStatus = () => repl.setStatus(buildStatus());
|
|
7560
|
+
const refreshAuthSuggestions = () => {
|
|
7561
|
+
const profiles = dbQueries.getProfiles();
|
|
7562
|
+
repl.setDynamicSuggestions(profiles.map((p) => ({
|
|
7563
|
+
value: `auth use ${p.name}`,
|
|
7564
|
+
label: p.name,
|
|
7565
|
+
desc: p.type
|
|
7566
|
+
})));
|
|
7567
|
+
};
|
|
6100
7568
|
const reload = async () => {
|
|
6101
7569
|
if (isReloading)
|
|
6102
7570
|
return;
|
|
6103
|
-
process.stdout.write(`
|
|
6104
|
-
`);
|
|
6105
7571
|
if (!ctx.specUrl) {
|
|
6106
|
-
console.log(`
|
|
6107
|
-
|
|
7572
|
+
console.log(`
|
|
7573
|
+
${paint.yellow("\u25CB")} No spec URL \u2014 use /spec <url>
|
|
7574
|
+
`);
|
|
6108
7575
|
return;
|
|
6109
7576
|
}
|
|
6110
7577
|
isReloading = true;
|
|
6111
|
-
|
|
7578
|
+
let fi = 0;
|
|
7579
|
+
const base = buildStatus();
|
|
7580
|
+
const spinTimer = setInterval(() => {
|
|
7581
|
+
repl.setStatus(`${SPIN_FRAMES[fi++ % SPIN_FRAMES.length]} Reloading\u2026 \xB7 ${base}`);
|
|
7582
|
+
}, 80);
|
|
6112
7583
|
try {
|
|
6113
7584
|
const state = await loadSpec(ctx.specUrl);
|
|
6114
|
-
|
|
7585
|
+
clearInterval(spinTimer);
|
|
7586
|
+
dbQueries.upsertSpec(ctx.specUrl, state.spec.title, state.spec.version, state.operations.length);
|
|
7587
|
+
console.log(` ${paint.green("\u2713")} ${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} \xB7 ${paint.green(state.operations.length + " endpoints")}
|
|
7588
|
+
`);
|
|
6115
7589
|
} catch (e) {
|
|
6116
|
-
|
|
7590
|
+
clearInterval(spinTimer);
|
|
7591
|
+
console.log(` ${paint.red("\u2717")} Reload failed: ${e instanceof Error ? e.message : String(e)}
|
|
7592
|
+
`);
|
|
6117
7593
|
} finally {
|
|
6118
7594
|
isReloading = false;
|
|
7595
|
+
refreshStatus();
|
|
6119
7596
|
}
|
|
6120
|
-
console.log();
|
|
6121
7597
|
};
|
|
6122
|
-
const
|
|
6123
|
-
|
|
6124
|
-
if (
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
});
|
|
6129
|
-
const handleKey = async (key) => {
|
|
6130
|
-
if (key === "\x03") {
|
|
6131
|
-
if (cmdBuf !== null) {
|
|
6132
|
-
cmdBuf = null;
|
|
6133
|
-
process.stdout.write("\r\x1B[K");
|
|
6134
|
-
return;
|
|
6135
|
-
}
|
|
6136
|
-
process.emit("SIGINT");
|
|
6137
|
-
return;
|
|
6138
|
-
}
|
|
6139
|
-
if (cmdBuf !== null) {
|
|
6140
|
-
if (key === "\x1B") {
|
|
6141
|
-
cmdBuf = null;
|
|
6142
|
-
process.stdout.write("\r\x1B[K");
|
|
6143
|
-
return;
|
|
6144
|
-
}
|
|
6145
|
-
if (key === "\x7F" || key === "\b") {
|
|
6146
|
-
cmdBuf = cmdBuf.slice(0, -1);
|
|
6147
|
-
if (!cmdBuf) {
|
|
6148
|
-
cmdBuf = null;
|
|
6149
|
-
process.stdout.write("\r\x1B[K");
|
|
7598
|
+
const handler = async (input) => {
|
|
7599
|
+
const cmd = input.trim();
|
|
7600
|
+
if (cmd.length === 1) {
|
|
7601
|
+
switch (cmd.toLowerCase()) {
|
|
7602
|
+
case "r":
|
|
7603
|
+
await reload();
|
|
6150
7604
|
return;
|
|
6151
|
-
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
}
|
|
6168
|
-
return;
|
|
6169
|
-
}
|
|
6170
|
-
if (key === "/") {
|
|
6171
|
-
cmdBuf = "/";
|
|
6172
|
-
printPrompt();
|
|
6173
|
-
return;
|
|
6174
|
-
}
|
|
6175
|
-
switch (key.toLowerCase()) {
|
|
6176
|
-
case "r":
|
|
6177
|
-
await reload();
|
|
6178
|
-
break;
|
|
6179
|
-
case "b": {
|
|
6180
|
-
process.stdin.setRawMode(false);
|
|
6181
|
-
process.stdin.pause();
|
|
6182
|
-
process.on("SIGHUP", () => {});
|
|
6183
|
-
console.log(`
|
|
6184
|
-
${paint.green("\u2713")} Detached ${paint.dim(`PID ${process.pid}`)}`);
|
|
6185
|
-
console.log(` ${paint.dim("\u279C")} ${paint.dim("wasper status")} ${paint.dim("\xB7")} ${paint.dim("wasper stop")}
|
|
7605
|
+
case "s":
|
|
7606
|
+
printInlineStatus(ctx);
|
|
7607
|
+
return;
|
|
7608
|
+
case "q":
|
|
7609
|
+
process.emit("SIGINT");
|
|
7610
|
+
return;
|
|
7611
|
+
case "h":
|
|
7612
|
+
case "?":
|
|
7613
|
+
printInteractiveHelp();
|
|
7614
|
+
return;
|
|
7615
|
+
case "b": {
|
|
7616
|
+
repl.stop();
|
|
7617
|
+
process.on("SIGHUP", () => {});
|
|
7618
|
+
console.log(`
|
|
7619
|
+
${paint.green("\u2713")} Detached ${paint.dim("PID " + process.pid)}`);
|
|
7620
|
+
console.log(` ${paint.dim("\u279C")} ${paint.dim("wasper status")} ${paint.dim("\xB7")} ${paint.dim("wasper stop")}
|
|
6186
7621
|
`);
|
|
6187
|
-
|
|
7622
|
+
return;
|
|
7623
|
+
}
|
|
6188
7624
|
}
|
|
6189
|
-
case "s":
|
|
6190
|
-
printInlineStatus(ctx);
|
|
6191
|
-
break;
|
|
6192
|
-
case "?":
|
|
6193
|
-
case "h":
|
|
6194
|
-
printInteractiveHelp();
|
|
6195
|
-
break;
|
|
6196
|
-
case "q":
|
|
6197
|
-
process.emit("SIGINT");
|
|
6198
|
-
break;
|
|
6199
7625
|
}
|
|
7626
|
+
const slashCmd = cmd.startsWith("/") ? cmd : `/${cmd}`;
|
|
7627
|
+
await runSlashCommand(slashCmd, ctx, reload);
|
|
7628
|
+
refreshStatus();
|
|
7629
|
+
refreshAuthSuggestions();
|
|
6200
7630
|
};
|
|
7631
|
+
refreshAuthSuggestions();
|
|
7632
|
+
repl.setStatus(buildStatus());
|
|
7633
|
+
repl.start(handler);
|
|
6201
7634
|
}
|
|
6202
7635
|
function printInlineStatus(ctx) {
|
|
6203
7636
|
const state = hasState() ? getState() : null;
|
|
6204
7637
|
const f = getFeatures();
|
|
6205
7638
|
const cfg = getServerConfig();
|
|
6206
|
-
const
|
|
7639
|
+
const on = (v) => v ? paint.green("on") : paint.dim("off");
|
|
7640
|
+
const dot = paint.dim("\xB7");
|
|
6207
7641
|
console.log(`
|
|
6208
|
-
${paint.
|
|
7642
|
+
${paint.green("\u25CF")} ${paint.bold("wasper")} ${paint.dim("PID " + process.pid)} ${dot} ${paint.dim(":" + ctx.PORT)}`);
|
|
6209
7643
|
if (state) {
|
|
6210
|
-
console.log(` ${paint.bold(state.spec.title)}
|
|
7644
|
+
console.log(` ${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} ${dot} ${paint.green(state.operations.length + " endpoints")}`);
|
|
7645
|
+
} else {
|
|
7646
|
+
console.log(` ${paint.dim("no spec loaded")}`);
|
|
6211
7647
|
}
|
|
6212
|
-
console.log(` mcp ${
|
|
7648
|
+
console.log(` mcp ${on(f.mcp)} ${dot} proxy ${on(f.proxy)} ${dot} ai ${on(f.ai)} ${dot} readonly ${on(f.readonly)} ${dot} token ${cfg.token ? paint.green("set") : paint.dim("none")}`);
|
|
6213
7649
|
const active = dbQueries.getActiveProfile();
|
|
6214
|
-
|
|
6215
|
-
console.log(` auth role: ${paint.bold(active.name)} ${paint.dim(`(${active.type})`)}`);
|
|
7650
|
+
console.log(` auth ${active ? paint.bold(active.name) + " " + paint.dim("(" + active.type + ")") : paint.dim("none")}`);
|
|
6216
7651
|
console.log();
|
|
6217
7652
|
}
|
|
6218
7653
|
async function runSlashCommand(input, ctx, reload) {
|
|
@@ -6224,6 +7659,7 @@ async function runSlashCommand(input, ctx, reload) {
|
|
|
6224
7659
|
const cur = getFeatures()[name];
|
|
6225
7660
|
const next = arg === "on" ? true : arg === "off" ? false : !cur;
|
|
6226
7661
|
setFeatures({ [name]: next });
|
|
7662
|
+
persistAndBroadcastFeatures();
|
|
6227
7663
|
console.log(` ${next ? paint.green("\u2713") : paint.yellow("\u25CB")} ${label} ${next ? paint.green("enabled") : paint.yellow("disabled")}
|
|
6228
7664
|
`);
|
|
6229
7665
|
};
|
|
@@ -6368,40 +7804,48 @@ async function runSlashCommand(input, ctx, reload) {
|
|
|
6368
7804
|
}
|
|
6369
7805
|
}
|
|
6370
7806
|
function printInteractiveHelp() {
|
|
7807
|
+
const k = (s) => paint.bold(s);
|
|
7808
|
+
const d = (s) => paint.dim(s);
|
|
7809
|
+
const hr = d("\u2500".repeat(50));
|
|
6371
7810
|
console.log(`
|
|
6372
|
-
${paint.bold("Keys")}
|
|
6373
|
-
${
|
|
6374
|
-
${
|
|
6375
|
-
${
|
|
6376
|
-
${
|
|
6377
|
-
|
|
6378
|
-
${
|
|
7811
|
+
${paint.bold("Keys")} ${d("(when input is empty)")}
|
|
7812
|
+
${hr}
|
|
7813
|
+
${k("r")} Hot-reload spec ${k("b")} Detach to background
|
|
7814
|
+
${k("s")} Print status ${k("q")} Quit
|
|
7815
|
+
${k("/")} Start a command ${k("?")} This help
|
|
7816
|
+
|
|
7817
|
+
${d("\u2191 / \u2193")} cycle command history \xB7 ${d("\u2192 or Tab")} accept autocomplete
|
|
7818
|
+
${d("Ctrl+L")} clear screen \xB7 ${d("Ctrl+U")} clear input \xB7 ${d("Esc")} cancel
|
|
6379
7819
|
|
|
6380
7820
|
${paint.bold("Slash commands")}
|
|
6381
|
-
${
|
|
6382
|
-
${
|
|
6383
|
-
${
|
|
6384
|
-
${
|
|
6385
|
-
${
|
|
6386
|
-
${
|
|
6387
|
-
${
|
|
6388
|
-
${
|
|
6389
|
-
${
|
|
6390
|
-
${
|
|
6391
|
-
${
|
|
6392
|
-
${
|
|
6393
|
-
${
|
|
7821
|
+
${hr}
|
|
7822
|
+
${k("/spec")} ${d("<url>")} Load a different OpenAPI spec
|
|
7823
|
+
${k("/mcp")} ${d("[on|off]")} Toggle the MCP endpoint
|
|
7824
|
+
${k("/proxy")} ${d("[on|off]")} Toggle the HTTP proxy
|
|
7825
|
+
${k("/ai")} ${d("[on|off]")} Toggle the AI chat endpoint
|
|
7826
|
+
${k("/readonly")} ${d("[on|off]")} Block non-GET upstream requests
|
|
7827
|
+
${k("/auth")} List saved auth profiles
|
|
7828
|
+
${k("/auth use")} ${d("<name>")} Switch active auth profile
|
|
7829
|
+
${k("/auth none")} Disable auth
|
|
7830
|
+
${k("/token")} ${d("[new|off|<v>]")} Show / rotate / set the access token
|
|
7831
|
+
${k("/tail")} ${d("[on|off]")} Live request log in this terminal
|
|
7832
|
+
${k("/open")} Open the studio in a browser
|
|
7833
|
+
${k("/update")} Update wasper to the latest version
|
|
7834
|
+
${k("/status")} ${k("/reload")} ${k("/help")} ${k("/quit")}
|
|
6394
7835
|
`);
|
|
6395
7836
|
}
|
|
6396
7837
|
function printHelp() {
|
|
6397
7838
|
console.log(`
|
|
6398
7839
|
Usage: wasper [start] [options]
|
|
6399
7840
|
|
|
6400
|
-
|
|
7841
|
+
wasper [--url <spec-url>] [--port <port>] Start in foreground (auto-resumes last spec)
|
|
6401
7842
|
wasper start --background Start in background
|
|
6402
7843
|
wasper stop Stop background server
|
|
6403
7844
|
wasper status Show server status
|
|
6404
7845
|
wasper reload Hot-reload the spec
|
|
7846
|
+
wasper ls List saved specs (history)
|
|
7847
|
+
wasper use <number|url> Start with a saved spec
|
|
7848
|
+
wasper rm <number|url> Remove a spec from history
|
|
6405
7849
|
|
|
6406
7850
|
Options:
|
|
6407
7851
|
--url, -u OpenAPI spec URL or local path
|
|
@@ -6520,9 +7964,11 @@ async function runFirstTimeSetup(port, origin) {
|
|
|
6520
7964
|
${paint.dim("Skip future prompts: set WASPER_NO_FIRST_RUN=1")}
|
|
6521
7965
|
`);
|
|
6522
7966
|
}
|
|
7967
|
+
var SPIN_FRAMES;
|
|
6523
7968
|
var init_start = __esm(() => {
|
|
6524
7969
|
init_server();
|
|
6525
7970
|
init_handler();
|
|
7971
|
+
init_capture();
|
|
6526
7972
|
init_routes();
|
|
6527
7973
|
init_bus();
|
|
6528
7974
|
init_db();
|
|
@@ -6531,6 +7977,9 @@ var init_start = __esm(() => {
|
|
|
6531
7977
|
init_config();
|
|
6532
7978
|
init_update();
|
|
6533
7979
|
init_ui();
|
|
7980
|
+
init_repl();
|
|
7981
|
+
init_routes();
|
|
7982
|
+
SPIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
6534
7983
|
});
|
|
6535
7984
|
|
|
6536
7985
|
// index.ts
|