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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/dist/cli.js +2536 -931
  4. package/dist/index.js +2134 -685
  5. 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.1.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/api/routes.ts
4160
- import dns from "dns/promises";
4161
- function json(data, status = 200) {
4162
- return new Response(JSON.stringify(data), {
4163
- status,
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
- function notFound(msg = "Not found") {
4168
- return json({ error: msg }, 404);
4169
- }
4170
- function badRequest(msg) {
4171
- return json({ error: msg }, 400);
4172
- }
4173
- async function apiRouter(req) {
4174
- if (req.method === "OPTIONS")
4175
- return new Response(null, { status: 204, headers: CORS3 });
4176
- const { pathname: path, searchParams } = new URL(req.url);
4177
- const method = req.method;
4178
- if (path === "/api/status" && method === "GET")
4179
- return handleStatus();
4180
- if (path === "/api/logs" && method === "GET")
4181
- return handleGetLogs(searchParams);
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 handleStatus() {
4245
- if (!hasState()) {
4246
- return json({ ok: true, version: VERSION, spec: null, endpointCount: 0, wsClients: logBus.clientCount });
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
- const { spec, operations } = getState();
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
- async function handleReload() {
4258
- if (!hasState())
4259
- return badRequest("No spec loaded");
4260
- const { specUrl } = getState();
4261
- if (!specUrl)
4262
- return json({ error: "Spec was uploaded manually \u2014 cannot reload from URL" }, 400);
4263
- try {
4264
- const s = await loadSpec(specUrl);
4265
- return json({ ok: true, spec: s.spec.title, version: s.spec.version, endpoints: s.operations.length });
4266
- } catch (e) {
4267
- return json({ error: e instanceof Error ? e.message : String(e) }, 500);
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 handleSetFeatures(req) {
4271
- let body;
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
- body = await req.json();
4500
+ responseData = JSON.parse(responseText);
4274
4501
  } catch {
4275
- return badRequest("Invalid JSON");
4502
+ responseData = responseText;
4276
4503
  }
4277
- const patch = {};
4278
- for (const key of ["mcp", "proxy", "ai", "readonly"]) {
4279
- if (typeof body[key] === "boolean")
4280
- patch[key] = body[key];
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
- setFeatures(patch);
4283
- return json(getFeatures());
4284
- }
4285
- function handleServerInfo() {
4286
- const state = hasState() ? getState() : null;
4287
- return json({
4288
- pid: process.pid,
4289
- port: parseInt(process.env._OA_PORT ?? "3388", 10),
4290
- startedAt: parseInt(process.env._OA_STARTED ?? "0", 10),
4291
- features: getFeatures(),
4292
- spec: state ? {
4293
- title: state.spec.title,
4294
- version: state.spec.version,
4295
- endpointCount: state.operations.length,
4296
- specUrl: state.specUrl ?? null
4297
- } : null
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 handleSpecUpload(req) {
4301
- let content;
4302
- let filename = "spec";
4303
- const ct = req.headers.get("content-type") ?? "";
4304
- if (ct.includes("multipart/form-data")) {
4305
- let form;
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
- form = await req.formData();
4308
- } catch {
4309
- return badRequest("Invalid form data");
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
- const file = form.get("file");
4312
- if (!file || typeof file === "string")
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
- if (!matches.length)
4429
- return { text: `No endpoints found matching "${args.query}". Total: ${operations.length}.`, isError: false };
4430
- return { text: JSON.stringify({ count: matches.length, total: operations.length, endpoints: matches }, null, 2), isError: false };
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
- return {
4437
- text: JSON.stringify({
4438
- operationId: op.operationId,
4439
- method: op.method.toUpperCase(),
4440
- path: op.path,
4441
- summary: op.summary ?? null,
4442
- description: op.description ?? null,
4443
- tags: op.tags,
4444
- parameters: op.parameters,
4445
- requestBody: op.requestBody ?? null,
4446
- responses: op.responses
4447
- }, null, 2),
4448
- isError: false
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 text = await res.text();
5489
+ const responseText = await res.text();
4486
5490
  const latency = Date.now() - start;
4487
- let pretty = text;
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(text), null, 2);
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
- return { text: `Network error: ${e instanceof Error ? e.message : String(e)}`, isError: true };
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. If the API requires auth, call list_auth_profiles first, then set_active_auth, or login and call save_auth_token.";
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
- Tools available:
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
- Authentication workflow: if requests return 401/403, call list_auth_profiles first. If a profile exists, call set_active_auth. If none, find and call the login endpoint, extract the token from the response, then call save_auth_token immediately. After saving, retry the original request.
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
- IMPORTANT: if an endpoint returns an error, diagnose it (check the schema, check auth) and fix the root cause before retrying. If the same endpoint fails 3 times the agent will be forcibly stopped. Do not retry without changing something.
5805
+ Be concise. Format code and JSON in fenced blocks.${ai.customInstructions ? `
4977
5806
 
4978
- Be concise and practical. Format code and JSON in code blocks.`;
4979
- const provider = ai.provider ?? "anthropic";
4980
- const requiresKey = provider !== "ollama" && provider !== "custom";
4981
- if (requiresKey && !ai.apiKey) {
4982
- return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
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
- let result;
4996
- if (provider === "anthropic") {
4997
- result = await anthropicAgentLoop(ai.apiKey, ai.model || "claude-haiku-4-5-20251001", system, msgs, emit);
4998
- } else if (provider === "openai") {
4999
- const base = (ai.baseUrl || "https://api.openai.com").replace(/\/$/, "");
5000
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "gpt-4o-mini", {}, system, msgs, emit);
5001
- } else if (provider === "mistral") {
5002
- const base = (ai.baseUrl || "https://api.mistral.ai").replace(/\/$/, "");
5003
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "mistral-small-latest", {}, system, msgs, emit);
5004
- } else if (provider === "github-copilot") {
5005
- const base = (ai.baseUrl || "https://api.githubcopilot.com").replace(/\/$/, "");
5006
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "gpt-4o", {
5007
- "Copilot-Integration-Id": "vscode-chat",
5008
- "Editor-Version": "vscode/1.85.0"
5009
- }, system, msgs, emit);
5010
- } else if (provider === "groq") {
5011
- const base = (ai.baseUrl || "https://api.groq.com/openai").replace(/\/$/, "");
5012
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "llama-3.1-70b-versatile", {}, system, msgs, emit);
5013
- } else if (provider === "custom") {
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({ type: "done", content: result.content, toolCalls: result.toolCalls });
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 handleCreateSaved(req) {
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
- if (!body.name || typeof body.name !== "string")
5420
- return badRequest("name is required");
5421
- const id = randomUUID2();
5422
- dbQueries.insertSavedRequest({
5423
- id,
5424
- name: String(body.name),
5425
- folder: String(body.folder ?? ""),
5426
- method: String(body.method ?? "GET"),
5427
- url: String(body.url ?? ""),
5428
- headers: typeof body.headers === "string" ? body.headers : JSON.stringify(body.headers ?? []),
5429
- params: typeof body.params === "string" ? body.params : JSON.stringify(body.params ?? []),
5430
- body: String(body.body ?? ""),
5431
- body_type: String(body.body_type ?? "none"),
5432
- raw_type: String(body.raw_type ?? "text/plain"),
5433
- form_rows: typeof body.form_rows === "string" ? body.form_rows : JSON.stringify(body.form_rows ?? []),
5434
- auth: typeof body.auth === "string" ? body.auth : JSON.stringify(body.auth ?? {}),
5435
- notes: String(body.notes ?? "")
5436
- });
5437
- return json(dbQueries.getSavedRequest(id), 201);
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
- async function handleUpdateSaved(req, path) {
5440
- const id = path.replace("/api/saved/", "");
5441
- if (!dbQueries.getSavedRequest(id))
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
- let body;
6424
+ if (!hasState())
6425
+ return badRequest("No spec loaded");
6426
+ let steps;
5444
6427
  try {
5445
- body = await req.json();
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 (Object.keys(patch).length)
5456
- dbQueries.updateSavedRequest(id, patch);
5457
- return json(dbQueries.getSavedRequest(id));
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 handleDeleteSaved(path) {
5460
- const id = path.replace("/api/saved/", "");
5461
- if (!dbQueries.getSavedRequest(id))
5462
- return notFound();
5463
- dbQueries.deleteSavedRequest(id);
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 CORS3, TOOL_DEFS, ANTHROPIC_TOOLS, OPENAI_TOOLS, MAX_TOTAL_TOOLS = 40, MAX_CONSECUTIVE_ERRORS = 5, MAX_SAME_ENDPOINT_ERRORS = 3;
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
- CORS3 = {
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 or API key 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.",
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 to save" },
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", "token"]
6560
+ required: ["name"]
5550
6561
  }
5551
6562
  };
5552
- ANTHROPIC_TOOLS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
6563
+ TOOL_SCHEMAS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
5553
6564
  name,
5554
6565
  description: def.description,
5555
- input_schema: { type: "object", properties: def.params, required: def.required }
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
- const specUrl = overrideOpts?.url ?? (values.url ? String(values.url) : null) ?? process.env.WASPER_SPEC_URL ?? null;
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
- mcp: !values["no-mcp"],
5955
- proxy: !values["no-proxy"],
5956
- ai: !values["no-ai"],
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
- let cmdBuf = null;
6096
- process.stdin.setRawMode(true);
6097
- process.stdin.resume();
6098
- process.stdin.setEncoding("utf8");
6099
- const spinner = new Spinner;
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(` ${paint.yellow("\u25CB")} No spec URL \u2014 use /spec <url> or start with --url <url>`);
6107
- console.log();
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
- spinner.start(`Reloading spec\u2026`);
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
- spinner.stop("\u2713", `${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} ${paint.dim("\xB7")} ${paint.green(state.operations.length + " endpoints")}`, "green");
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
- spinner.stop("\u2717", `Reload failed: ${e instanceof Error ? e.message : String(e)}`, "red");
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 printPrompt = () => process.stdout.write(`\r\x1B[K ${paint.cyan("\u276F")} ${cmdBuf}`);
6123
- process.stdin.on("data", async (chunk) => {
6124
- if (chunk.startsWith("\x1B") && chunk.length > 1)
6125
- return;
6126
- for (const key of chunk)
6127
- await handleKey(key);
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
- printPrompt();
6153
- return;
6154
- }
6155
- if (key === "\r" || key === `
6156
- `) {
6157
- const cmd = cmdBuf;
6158
- cmdBuf = null;
6159
- process.stdout.write("\r\x1B[K");
6160
- await runSlashCommand(cmd, ctx, reload);
6161
- return;
6162
- }
6163
- const printable = key.replace(/[^\x20-\x7E]/g, "");
6164
- if (printable) {
6165
- cmdBuf += printable;
6166
- printPrompt();
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
- break;
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 flag = (on) => on ? paint.green("on") : paint.red("off");
7639
+ const on = (v) => v ? paint.green("on") : paint.dim("off");
7640
+ const dot = paint.dim("\xB7");
6207
7641
  console.log(`
6208
- ${paint.dim("\u25CF")} ${paint.bold("OpenAPI Agent")} PID ${process.pid} port ${ctx.PORT}`);
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)} v${state.spec.version} \xB7 ${state.operations.length} endpoints`);
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 ${flag(f.mcp)} ${paint.dim("\xB7")} proxy ${flag(f.proxy)} ${paint.dim("\xB7")} ai ${flag(f.ai)} ${paint.dim("\xB7")} readonly ${flag(f.readonly)} ${paint.dim("\xB7")} token ${cfg.token ? paint.green("set") : paint.dim("none")}`);
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
- if (active)
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
- ${paint.dim("\u2500".repeat(46))}
6374
- ${paint.bold("r")} Hot-reload OpenAPI spec from URL
6375
- ${paint.bold("s")} Print current status
6376
- ${paint.bold("b")} Detach to background (survive terminal close)
6377
- ${paint.bold("q")} Quit gracefully
6378
- ${paint.bold("/")} Type a slash command ${paint.dim("(Esc cancels)")}
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
- ${paint.dim("\u2500".repeat(46))}
6382
- ${paint.bold("/mcp")} ${paint.dim("[on|off]")} Toggle the MCP endpoint
6383
- ${paint.bold("/proxy")} ${paint.dim("[on|off]")} Toggle the HTTP proxy
6384
- ${paint.bold("/ai")} ${paint.dim("[on|off]")} Toggle the AI chat endpoint
6385
- ${paint.bold("/readonly")} ${paint.dim("[on|off]")} Block non-GET upstream requests
6386
- ${paint.bold("/auth")} List auth roles
6387
- ${paint.bold("/auth use")} ${paint.dim("<name>")} Switch active auth role
6388
- ${paint.bold("/auth none")} Disable auth
6389
- ${paint.bold("/token")} ${paint.dim("[new|off|<v>]")} Show / rotate / set the access token
6390
- ${paint.bold("/spec")} ${paint.dim("<url>")} Load a different OpenAPI spec
6391
- ${paint.bold("/tail")} ${paint.dim("[on|off]")} Live request log in this terminal
6392
- ${paint.bold("/open")} Open the studio in a browser
6393
- ${paint.bold("/status")} ${paint.dim("\xB7")} ${paint.bold("/reload")} ${paint.dim("\xB7")} ${paint.bold("/help")} ${paint.dim("\xB7")} ${paint.bold("/quit")}
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
- openapi-agent [--url <spec-url>] [--port <port>] Start in foreground
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