wasper-cli 0.1.0 → 0.2.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 +1887 -641
  4. package/dist/index.js +1218 -128
  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,33 @@ 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
+ );
3048
3161
  `;
3049
3162
 
3050
3163
  // src/db/index.ts
@@ -3188,6 +3301,39 @@ var init_db = __esm(() => {
3188
3301
  db.query(`UPDATE saved_requests SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
3189
3302
  },
3190
3303
  deleteSavedRequest: (id) => db.query("DELETE FROM saved_requests WHERE id = ?").run(id),
3304
+ getSpecHistory: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC").all(),
3305
+ getLastSpec: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC LIMIT 1").get(),
3306
+ upsertSpec: (url, title, version, endpointCount) => {
3307
+ const existing = db.query("SELECT id FROM spec_history WHERE url = ?").get(url);
3308
+ if (existing) {
3309
+ db.query("UPDATE spec_history SET title=?, version=?, endpoint_count=?, last_used=unixepoch() WHERE url=?").run(title, version, endpointCount, url);
3310
+ } else {
3311
+ db.query(`INSERT INTO spec_history (id, url, title, version, endpoint_count)
3312
+ VALUES (?, ?, ?, ?, ?)`).run(randomUUID2(), url, title, version, endpointCount);
3313
+ }
3314
+ },
3315
+ deleteSpec: (id) => {
3316
+ db.query("DELETE FROM spec_history WHERE id = ?").run(id);
3317
+ },
3318
+ getWorkflows: () => db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all(),
3319
+ getWorkflow: (id) => db.query("SELECT * FROM workflows WHERE id = ?").get(id),
3320
+ 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 }),
3321
+ updateWorkflow: (id, patch) => {
3322
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
3323
+ const params = { $id: id };
3324
+ for (const [k, v] of Object.entries(patch))
3325
+ params[`$${k}`] = v;
3326
+ db.query(`UPDATE workflows SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
3327
+ },
3328
+ deleteWorkflow: (id) => db.query("DELETE FROM workflows WHERE id = ?").run(id),
3329
+ getCaptureBins: () => db.query("SELECT * FROM capture_bins ORDER BY created_at DESC").all(),
3330
+ getCaptureBin: (id) => db.query("SELECT * FROM capture_bins WHERE id = ?").get(id),
3331
+ insertCaptureBin: (id, name) => {
3332
+ db.query("INSERT INTO capture_bins (id, name) VALUES (?, ?)").run(id, name);
3333
+ },
3334
+ deleteCaptureBin: (id) => {
3335
+ db.query("DELETE FROM capture_bins WHERE id = ?").run(id);
3336
+ },
3191
3337
  getSetting: (key) => (db.query("SELECT value FROM settings WHERE key = ?").get(key) ?? null)?.value ?? null,
3192
3338
  setSetting: (key, value) => {
3193
3339
  db.run("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
@@ -3226,6 +3372,18 @@ class LogBus {
3226
3372
  }
3227
3373
  }
3228
3374
  }
3375
+ broadcastServerEvent(payload) {
3376
+ if (this.clients.size === 0)
3377
+ return;
3378
+ const data = JSON.stringify({ type: "server_event", ...payload });
3379
+ for (const ws of this.clients) {
3380
+ try {
3381
+ ws.send(data);
3382
+ } catch {
3383
+ this.clients.delete(ws);
3384
+ }
3385
+ }
3386
+ }
3229
3387
  get clientCount() {
3230
3388
  return this.clients.size;
3231
3389
  }
@@ -3428,7 +3586,7 @@ var package_default;
3428
3586
  var init_package = __esm(() => {
3429
3587
  package_default = {
3430
3588
  name: "wasper-cli",
3431
- version: "0.1.0",
3589
+ version: "0.2.0",
3432
3590
  description: "Host an MCP server + API proxy from any OpenAPI spec. Like Drizzle Studio, but for APIs.",
3433
3591
  type: "module",
3434
3592
  homepage: "https://wasper.site",
@@ -4156,12 +4314,276 @@ var init_handler = __esm(() => {
4156
4314
  };
4157
4315
  });
4158
4316
 
4317
+ // src/proxy/capture.ts
4318
+ async function captureHandler(req) {
4319
+ if (req.method === "OPTIONS") {
4320
+ return new Response(null, { status: 204, headers: CORS3 });
4321
+ }
4322
+ const url = new URL(req.url);
4323
+ const binId = url.pathname.split("/").filter(Boolean)[1];
4324
+ if (!binId)
4325
+ return new Response("Not found", { status: 404 });
4326
+ const bin = dbQueries.getCaptureBin(binId);
4327
+ if (!bin) {
4328
+ return new Response(JSON.stringify({ error: "Capture bin not found or deleted" }), {
4329
+ status: 404,
4330
+ headers: { "Content-Type": "application/json", ...CORS3 }
4331
+ });
4332
+ }
4333
+ const body = req.method !== "GET" && req.method !== "HEAD" ? await req.text().catch(() => null) : null;
4334
+ const reqHeaders = {};
4335
+ for (const [k, v] of req.headers.entries()) {
4336
+ if (!["host", "connection"].includes(k.toLowerCase()))
4337
+ reqHeaders[k] = v;
4338
+ }
4339
+ const id = randomUUID2();
4340
+ const now = Date.now();
4341
+ dbQueries.insertLog({
4342
+ id,
4343
+ source: "capture",
4344
+ tool_name: binId,
4345
+ method: req.method,
4346
+ url: req.url,
4347
+ request_headers: JSON.stringify(reqHeaders),
4348
+ request_body: body,
4349
+ status_code: 200,
4350
+ response_headers: null,
4351
+ response_body: null,
4352
+ latency_ms: 0,
4353
+ error: null
4354
+ });
4355
+ logBus.emit({
4356
+ id,
4357
+ source: "capture",
4358
+ tool_name: binId,
4359
+ method: req.method,
4360
+ url: req.url,
4361
+ request_headers: JSON.stringify(reqHeaders),
4362
+ request_body: body,
4363
+ status_code: 200,
4364
+ response_headers: null,
4365
+ response_body: null,
4366
+ latency_ms: 0,
4367
+ error: null,
4368
+ created_at: now
4369
+ });
4370
+ return new Response(JSON.stringify({ ok: true, id, captured_at: now }), {
4371
+ status: 200,
4372
+ headers: { "Content-Type": "application/json", ...CORS3 }
4373
+ });
4374
+ }
4375
+ var CORS3;
4376
+ var init_capture = __esm(() => {
4377
+ init_db();
4378
+ init_bus();
4379
+ CORS3 = {
4380
+ "Access-Control-Allow-Origin": "*",
4381
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD",
4382
+ "Access-Control-Allow-Headers": "*",
4383
+ "Access-Control-Expose-Headers": "*"
4384
+ };
4385
+ });
4386
+
4387
+ // src/workflows/engine.ts
4388
+ function interpolate(val, ctx) {
4389
+ return val.replace(/\{\{(\w+)\}\}/g, (_, k) => ctx[k] ?? `{{${k}}}`);
4390
+ }
4391
+ function interpolateDeep(obj, ctx) {
4392
+ if (typeof obj === "string")
4393
+ return interpolate(obj, ctx);
4394
+ if (Array.isArray(obj))
4395
+ return obj.map((v) => interpolateDeep(v, ctx));
4396
+ if (obj !== null && typeof obj === "object") {
4397
+ const out = {};
4398
+ for (const [k, v] of Object.entries(obj))
4399
+ out[k] = interpolateDeep(v, ctx);
4400
+ return out;
4401
+ }
4402
+ return obj;
4403
+ }
4404
+ function resolveJsonPath(data, path) {
4405
+ if (!path || path === "$")
4406
+ return data;
4407
+ const normalized = path.startsWith("$.") ? path.slice(2) : path.startsWith("$[") ? path.slice(1) : path;
4408
+ if (!normalized)
4409
+ return data;
4410
+ const parts = [];
4411
+ for (const seg of normalized.split(".")) {
4412
+ const m = seg.match(/^(\w+)\[(\d+)\]$/);
4413
+ if (m) {
4414
+ parts.push(m[1], parseInt(m[2], 10));
4415
+ } else if (/^\d+$/.test(seg)) {
4416
+ parts.push(parseInt(seg, 10));
4417
+ } else {
4418
+ parts.push(seg);
4419
+ }
4420
+ }
4421
+ let cur = data;
4422
+ for (const part of parts) {
4423
+ if (cur === null || cur === undefined)
4424
+ return;
4425
+ cur = typeof part === "number" ? cur[part] : cur[part];
4426
+ }
4427
+ return cur;
4428
+ }
4429
+ async function executeStep(step, ctx) {
4430
+ const { spec } = getState();
4431
+ let base = spec.baseUrl;
4432
+ if (!base?.startsWith("http") && spec.url) {
4433
+ try {
4434
+ base = new URL(spec.url).origin;
4435
+ } catch {}
4436
+ }
4437
+ if (!base?.startsWith("http"))
4438
+ throw new Error("Spec has no absolute server URL");
4439
+ let urlPath = step.path;
4440
+ for (const [k, v] of Object.entries(step.pathParams ?? {})) {
4441
+ urlPath = urlPath.replace(`{${k}}`, encodeURIComponent(interpolate(v, ctx)));
4442
+ }
4443
+ urlPath = interpolate(urlPath, ctx);
4444
+ const urlObj = new URL(`${base.replace(/\/$/, "")}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`);
4445
+ for (const [k, v] of Object.entries(step.queryParams ?? {})) {
4446
+ urlObj.searchParams.set(k, interpolate(v, ctx));
4447
+ }
4448
+ const stepHeaders = {};
4449
+ for (const [k, v] of Object.entries(step.headers ?? {})) {
4450
+ stepHeaders[k] = interpolate(v, ctx);
4451
+ }
4452
+ const authRow = dbQueries.getAuthConfig();
4453
+ const authConfig = authRow ? JSON.parse(authRow.config) : { type: "none" };
4454
+ const { url: authedUrl, headers: authedHeaders } = await applyAuth(urlObj.toString(), stepHeaders, authConfig);
4455
+ const noBodyMethod = ["GET", "HEAD", "OPTIONS"].includes(step.method.toUpperCase());
4456
+ let bodyStr;
4457
+ if (!noBodyMethod && step.body !== undefined && step.body !== null) {
4458
+ const interpolated = interpolateDeep(step.body, ctx);
4459
+ bodyStr = typeof interpolated === "string" ? interpolated : JSON.stringify(interpolated);
4460
+ if (!authedHeaders["Content-Type"] && !authedHeaders["content-type"]) {
4461
+ authedHeaders["Content-Type"] = "application/json";
4462
+ }
4463
+ }
4464
+ const start = Date.now();
4465
+ const res = await fetch(authedUrl, {
4466
+ method: step.method.toUpperCase(),
4467
+ headers: authedHeaders,
4468
+ ...bodyStr !== undefined ? { body: bodyStr } : {},
4469
+ signal: AbortSignal.timeout(30000)
4470
+ });
4471
+ const latency = Date.now() - start;
4472
+ const responseHeaders = {};
4473
+ res.headers.forEach((v, k) => {
4474
+ responseHeaders[k] = v;
4475
+ });
4476
+ const responseText = await res.text();
4477
+ let responseData;
4478
+ try {
4479
+ responseData = JSON.parse(responseText);
4480
+ } catch {
4481
+ responseData = responseText;
4482
+ }
4483
+ const extractedVars = {};
4484
+ for (const ext of step.extract ?? []) {
4485
+ const val = resolveJsonPath(responseData, ext.path);
4486
+ if (val !== undefined && val !== null) {
4487
+ extractedVars[ext.var] = typeof val === "string" ? val : JSON.stringify(val);
4488
+ }
4489
+ }
4490
+ const assertions = [];
4491
+ for (const a of step.assert ?? []) {
4492
+ if (a.type === "status") {
4493
+ const expected = a.statusCode ?? 200;
4494
+ const pass2 = res.status === expected;
4495
+ assertions.push({ pass: pass2, message: `HTTP ${res.status} ${pass2 ? "==" : "!="} ${expected}` });
4496
+ } else if (a.type === "json") {
4497
+ const val = resolveJsonPath(responseData, a.path ?? "$");
4498
+ if ("eq" in a) {
4499
+ const pass2 = JSON.stringify(val) === JSON.stringify(a.eq);
4500
+ assertions.push({ pass: pass2, message: `${a.path} ${pass2 ? "==" : "!="} ${JSON.stringify(a.eq)}` });
4501
+ } else if ("contains" in a && typeof val === "string") {
4502
+ const pass2 = val.includes(a.contains);
4503
+ assertions.push({ pass: pass2, message: `${a.path} ${pass2 ? "contains" : "doesn't contain"} "${a.contains}"` });
4504
+ }
4505
+ }
4506
+ }
4507
+ const pass = assertions.length === 0 ? res.ok : assertions.every((a) => a.pass);
4508
+ return {
4509
+ stepId: step.id,
4510
+ label: step.label,
4511
+ method: step.method,
4512
+ resolvedPath: urlPath,
4513
+ requestUrl: authedUrl,
4514
+ requestHeaders: authedHeaders,
4515
+ requestBody: bodyStr,
4516
+ status: res.status,
4517
+ statusText: res.statusText,
4518
+ responseHeaders,
4519
+ latency,
4520
+ extractedVars,
4521
+ assertions,
4522
+ pass,
4523
+ responseBody: responseText.slice(0, 1e4)
4524
+ };
4525
+ }
4526
+ async function runWorkflow(steps, emit, signal) {
4527
+ const ctx = {};
4528
+ let passed = 0;
4529
+ emit({ type: "run_start", totalSteps: steps.length });
4530
+ for (const step of steps) {
4531
+ if (signal?.aborted) {
4532
+ emit({ type: "run_aborted", message: "Run cancelled" });
4533
+ return;
4534
+ }
4535
+ emit({ type: "step_start", stepId: step.id, label: step.label, method: step.method, path: step.path });
4536
+ try {
4537
+ const result = await executeStep(step, ctx);
4538
+ for (const [k, v] of Object.entries(result.extractedVars))
4539
+ ctx[k] = v;
4540
+ ctx[`${step.id}_status`] = String(result.status ?? "");
4541
+ if (result.pass)
4542
+ passed++;
4543
+ emit({
4544
+ type: "step_done",
4545
+ stepId: result.stepId,
4546
+ label: result.label,
4547
+ method: result.method,
4548
+ resolvedPath: result.resolvedPath,
4549
+ requestUrl: result.requestUrl,
4550
+ requestHeaders: result.requestHeaders,
4551
+ requestBody: result.requestBody,
4552
+ status: result.status,
4553
+ statusText: result.statusText,
4554
+ responseHeaders: result.responseHeaders,
4555
+ latency: result.latency,
4556
+ extractedVars: result.extractedVars,
4557
+ assertions: result.assertions,
4558
+ pass: result.pass,
4559
+ responseBody: result.responseBody
4560
+ });
4561
+ } catch (e) {
4562
+ emit({
4563
+ type: "step_error",
4564
+ stepId: step.id,
4565
+ label: step.label,
4566
+ method: step.method,
4567
+ path: step.path,
4568
+ error: e instanceof Error ? e.message : String(e),
4569
+ pass: false
4570
+ });
4571
+ }
4572
+ }
4573
+ emit({ type: "run_done", totalSteps: steps.length, passedSteps: passed });
4574
+ }
4575
+ var init_engine2 = __esm(() => {
4576
+ init_engine();
4577
+ init_db();
4578
+ init_state();
4579
+ });
4580
+
4159
4581
  // src/api/routes.ts
4160
4582
  import dns from "dns/promises";
4161
4583
  function json(data, status = 200) {
4162
4584
  return new Response(JSON.stringify(data), {
4163
4585
  status,
4164
- headers: { "Content-Type": "application/json", ...CORS3 }
4586
+ headers: { "Content-Type": "application/json", ...CORS4 }
4165
4587
  });
4166
4588
  }
4167
4589
  function notFound(msg = "Not found") {
@@ -4172,7 +4594,7 @@ function badRequest(msg) {
4172
4594
  }
4173
4595
  async function apiRouter(req) {
4174
4596
  if (req.method === "OPTIONS")
4175
- return new Response(null, { status: 204, headers: CORS3 });
4597
+ return new Response(null, { status: 204, headers: CORS4 });
4176
4598
  const { pathname: path, searchParams } = new URL(req.url);
4177
4599
  const method = req.method;
4178
4600
  if (path === "/api/status" && method === "GET")
@@ -4239,6 +4661,24 @@ async function apiRouter(req) {
4239
4661
  return handleUpdateSaved(req, path);
4240
4662
  if (path.startsWith("/api/saved/") && method === "DELETE")
4241
4663
  return handleDeleteSaved(path);
4664
+ if (path === "/api/workflows" && method === "GET")
4665
+ return handleGetWorkflows();
4666
+ if (path === "/api/workflows" && method === "POST")
4667
+ return handleCreateWorkflow(req);
4668
+ if (path === "/api/workflows/generate" && method === "POST")
4669
+ return handleGenerateWorkflow(req);
4670
+ if (path.startsWith("/api/workflows/") && path.endsWith("/run") && method === "POST")
4671
+ return handleRunWorkflow(path);
4672
+ if (path.startsWith("/api/workflows/") && method === "PUT")
4673
+ return handleUpdateWorkflow(req, path);
4674
+ if (path.startsWith("/api/workflows/") && method === "DELETE")
4675
+ return handleDeleteWorkflow(path);
4676
+ if (path === "/api/capture/bins" && method === "GET")
4677
+ return handleGetCaptureBins();
4678
+ if (path === "/api/capture/bins" && method === "POST")
4679
+ return handleCreateCaptureBin(req);
4680
+ if (path.startsWith("/api/capture/bins/") && method === "DELETE")
4681
+ return handleDeleteCaptureBin(path);
4242
4682
  return notFound("API route not found");
4243
4683
  }
4244
4684
  function handleStatus() {
@@ -4280,8 +4720,14 @@ async function handleSetFeatures(req) {
4280
4720
  patch[key] = body[key];
4281
4721
  }
4282
4722
  setFeatures(patch);
4723
+ persistAndBroadcastFeatures();
4283
4724
  return json(getFeatures());
4284
4725
  }
4726
+ function persistAndBroadcastFeatures() {
4727
+ const f = getFeatures();
4728
+ dbQueries.setSetting("features", JSON.stringify({ mcp: f.mcp, proxy: f.proxy, ai: f.ai }));
4729
+ logBus.broadcastServerEvent({ kind: "features", data: f });
4730
+ }
4285
4731
  function handleServerInfo() {
4286
4732
  const state = hasState() ? getState() : null;
4287
4733
  return json({
@@ -4331,10 +4777,12 @@ async function handleSpecUpload(req) {
4331
4777
  return badRequest("Empty spec content");
4332
4778
  try {
4333
4779
  const state = loadSpecFromText(content, filename);
4780
+ const suggestedVars = extractSuggestedVars(content, state.spec.baseUrl);
4334
4781
  return json({
4335
4782
  ok: true,
4336
4783
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
4337
- endpointCount: state.operations.length
4784
+ endpointCount: state.operations.length,
4785
+ suggestedVars
4338
4786
  });
4339
4787
  } catch (e) {
4340
4788
  return json({ error: e instanceof Error ? e.message : String(e) }, 400);
@@ -4351,10 +4799,12 @@ async function handleSpecReloadUrl(req) {
4351
4799
  return badRequest("Missing url field");
4352
4800
  try {
4353
4801
  const state = await loadSpec(body.url);
4802
+ const suggestedVars = extractSuggestedVars(state.spec.raw, state.spec.baseUrl);
4354
4803
  return json({
4355
4804
  ok: true,
4356
4805
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
4357
- endpointCount: state.operations.length
4806
+ endpointCount: state.operations.length,
4807
+ suggestedVars
4358
4808
  });
4359
4809
  } catch (e) {
4360
4810
  return json({ error: e instanceof Error ? e.message : String(e) }, 400);
@@ -4975,7 +5425,11 @@ Authentication workflow: if requests return 401/403, call list_auth_profiles fir
4975
5425
 
4976
5426
  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.
4977
5427
 
4978
- Be concise and practical. Format code and JSON in code blocks.`;
5428
+ Be concise and practical. Format code and JSON in code blocks.${body.extra_context ? `
5429
+
5430
+ ---
5431
+ ## Current context
5432
+ ${body.extra_context}` : ""}`;
4979
5433
  const provider = ai.provider ?? "anthropic";
4980
5434
  const requiresKey = provider !== "ollama" && provider !== "custom";
4981
5435
  if (requiresKey && !ai.apiKey) {
@@ -5058,7 +5512,7 @@ Be concise and practical. Format code and JSON in code blocks.`;
5058
5512
  headers: {
5059
5513
  "Content-Type": "text/event-stream",
5060
5514
  "Cache-Control": "no-cache",
5061
- ...CORS3
5515
+ ...CORS4
5062
5516
  }
5063
5517
  });
5064
5518
  }
@@ -5463,15 +5917,220 @@ function handleDeleteSaved(path) {
5463
5917
  dbQueries.deleteSavedRequest(id);
5464
5918
  return json({ ok: true });
5465
5919
  }
5466
- var CORS3, TOOL_DEFS, ANTHROPIC_TOOLS, OPENAI_TOOLS, MAX_TOTAL_TOOLS = 40, MAX_CONSECUTIVE_ERRORS = 5, MAX_SAME_ENDPOINT_ERRORS = 3;
5920
+ function workflowRow(row) {
5921
+ if (!row)
5922
+ return null;
5923
+ let steps = [];
5924
+ try {
5925
+ steps = JSON.parse(row.steps);
5926
+ } catch {}
5927
+ return { ...row, steps };
5928
+ }
5929
+ function handleGetWorkflows() {
5930
+ const rows = dbQueries.getWorkflows().map((r) => workflowRow(r)).filter(Boolean);
5931
+ return json(rows);
5932
+ }
5933
+ async function handleCreateWorkflow(req) {
5934
+ let body;
5935
+ try {
5936
+ body = await req.json();
5937
+ } catch {
5938
+ return badRequest("Invalid JSON");
5939
+ }
5940
+ const id = randomUUID2();
5941
+ dbQueries.insertWorkflow({
5942
+ id,
5943
+ name: String(body.name ?? "Untitled Workflow"),
5944
+ description: String(body.description ?? ""),
5945
+ steps: typeof body.steps === "string" ? body.steps : JSON.stringify(body.steps ?? [])
5946
+ });
5947
+ return json(workflowRow(dbQueries.getWorkflow(id)), 201);
5948
+ }
5949
+ async function handleUpdateWorkflow(req, path) {
5950
+ const id = path.slice("/api/workflows/".length);
5951
+ if (!dbQueries.getWorkflow(id))
5952
+ return notFound();
5953
+ let body;
5954
+ try {
5955
+ body = await req.json();
5956
+ } catch {
5957
+ return badRequest("Invalid JSON");
5958
+ }
5959
+ const patch = {};
5960
+ if ("name" in body)
5961
+ patch.name = String(body.name);
5962
+ if ("description" in body)
5963
+ patch.description = String(body.description);
5964
+ if ("steps" in body)
5965
+ patch.steps = typeof body.steps === "string" ? body.steps : JSON.stringify(body.steps);
5966
+ if (Object.keys(patch).length)
5967
+ dbQueries.updateWorkflow(id, patch);
5968
+ return json(workflowRow(dbQueries.getWorkflow(id)));
5969
+ }
5970
+ function handleDeleteWorkflow(path) {
5971
+ const id = path.slice("/api/workflows/".length);
5972
+ if (!dbQueries.getWorkflow(id))
5973
+ return notFound();
5974
+ dbQueries.deleteWorkflow(id);
5975
+ return json({ ok: true });
5976
+ }
5977
+ async function handleGenerateWorkflow(req) {
5978
+ if (!hasState())
5979
+ return badRequest("No spec loaded");
5980
+ let body;
5981
+ try {
5982
+ body = await req.json();
5983
+ } catch {
5984
+ return badRequest("Invalid JSON");
5985
+ }
5986
+ const settingsRow = dbQueries.getSettings();
5987
+ const settings = settingsRow ? JSON.parse(settingsRow.value) : {};
5988
+ const ai = settings.ai ?? {};
5989
+ const provider = ai.provider ?? "anthropic";
5990
+ if (provider !== "ollama" && !ai.apiKey) {
5991
+ return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
5992
+ }
5993
+ const { spec, operations } = getState();
5994
+ const endpointList = operations.slice(0, 80).map((op) => `${op.method.toUpperCase()} ${op.path}${op.operationId ? ` [${op.operationId}]` : ""}${op.summary ? ` \u2014 ${op.summary}` : ""}`).join(`
5995
+ `);
5996
+ const userPrompt = body.prompt?.trim() || "Generate a realistic end-to-end test workflow covering authentication and CRUD operations.";
5997
+ const systemMsg = `You generate API test workflows as JSON for the "${spec.title}" API (base: ${spec.baseUrl}).
5998
+
5999
+ Available endpoints:
6000
+ ${endpointList}
6001
+
6002
+ Return ONLY valid JSON (no markdown fences) matching this schema exactly:
6003
+ {
6004
+ "name": "string",
6005
+ "description": "string",
6006
+ "steps": [
6007
+ {
6008
+ "id": "step_1",
6009
+ "label": "Human-readable name",
6010
+ "method": "GET|POST|PUT|PATCH|DELETE",
6011
+ "path": "/exact/path/from/spec",
6012
+ "operationId": "operationId or null",
6013
+ "pathParams": {},
6014
+ "queryParams": {},
6015
+ "headers": {},
6016
+ "body": null,
6017
+ "extract": [{"var": "varName", "path": "$.field.nested"}],
6018
+ "assert": [{"type": "status", "statusCode": 200}]
6019
+ }
6020
+ ]
6021
+ }
6022
+
6023
+ Rules:
6024
+ - Use {{varName}} in path/headers/body values to reference vars extracted in prior steps
6025
+ - For auth: extract token after login, set headers: {"Authorization": "Bearer {{token}}"}
6026
+ - Keep 3\u20138 steps covering a realistic user journey
6027
+ - Only use paths that exist in the endpoint list above`;
6028
+ try {
6029
+ let text = "";
6030
+ if (provider === "anthropic") {
6031
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
6032
+ method: "POST",
6033
+ headers: { "Content-Type": "application/json", "x-api-key": ai.apiKey, "anthropic-version": "2023-06-01" },
6034
+ body: JSON.stringify({ model: ai.model || "claude-sonnet-4-6", max_tokens: 4096, system: systemMsg, messages: [{ role: "user", content: userPrompt }] })
6035
+ });
6036
+ if (!res.ok)
6037
+ throw new Error(`Anthropic: ${await res.text()}`);
6038
+ const d = await res.json();
6039
+ text = d.content.find((b) => b.type === "text")?.text ?? "";
6040
+ } else {
6041
+ 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(/\/$/, "");
6042
+ const hdrs = { "Content-Type": "application/json" };
6043
+ if (ai.apiKey)
6044
+ hdrs["Authorization"] = `Bearer ${ai.apiKey}`;
6045
+ const res = await fetch(`${base}/v1/chat/completions`, {
6046
+ method: "POST",
6047
+ headers: hdrs,
6048
+ 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 }] })
6049
+ });
6050
+ if (!res.ok)
6051
+ throw new Error(await res.text());
6052
+ const d = await res.json();
6053
+ text = d.choices[0]?.message.content ?? "";
6054
+ }
6055
+ let parsed;
6056
+ try {
6057
+ parsed = JSON.parse(text);
6058
+ } catch {
6059
+ const m = text.match(/```(?:json)?\s*\n?([\s\S]+?)\n?```/);
6060
+ if (m)
6061
+ parsed = JSON.parse(m[1]);
6062
+ else
6063
+ throw new Error("AI response was not valid JSON");
6064
+ }
6065
+ return json(parsed);
6066
+ } catch (e) {
6067
+ return json({ error: e instanceof Error ? e.message : String(e) }, 500);
6068
+ }
6069
+ }
6070
+ function handleRunWorkflow(path) {
6071
+ const id = path.slice("/api/workflows/".length, -"/run".length);
6072
+ const row = dbQueries.getWorkflow(id);
6073
+ if (!row)
6074
+ return notFound();
6075
+ if (!hasState())
6076
+ return badRequest("No spec loaded");
6077
+ let steps;
6078
+ try {
6079
+ steps = JSON.parse(row.steps);
6080
+ } catch {
6081
+ return badRequest("Invalid workflow steps JSON");
6082
+ }
6083
+ if (!steps.length)
6084
+ return badRequest("Workflow has no steps");
6085
+ const { readable, writable } = new TransformStream;
6086
+ const writer = writable.getWriter();
6087
+ const enc = new TextEncoder;
6088
+ const emit = (e) => {
6089
+ writer.write(enc.encode(`data: ${JSON.stringify(e)}
6090
+
6091
+ `)).catch(() => {});
6092
+ };
6093
+ (async () => {
6094
+ try {
6095
+ await runWorkflow(steps, emit);
6096
+ } catch (e) {
6097
+ emit({ type: "error", message: e instanceof Error ? e.message : String(e) });
6098
+ } finally {
6099
+ try {
6100
+ await writer.close();
6101
+ } catch {}
6102
+ }
6103
+ })();
6104
+ return new Response(readable, {
6105
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...CORS4 }
6106
+ });
6107
+ }
6108
+ function handleGetCaptureBins() {
6109
+ return json(dbQueries.getCaptureBins());
6110
+ }
6111
+ async function handleCreateCaptureBin(req) {
6112
+ const body = await req.json().catch(() => ({}));
6113
+ const id = randomUUID2().replace(/-/g, "").slice(0, 8);
6114
+ const name = String(body.name ?? "").trim() || "Untitled bin";
6115
+ dbQueries.insertCaptureBin(id, name);
6116
+ return json({ id, name, created_at: Math.floor(Date.now() / 1000) }, 201);
6117
+ }
6118
+ function handleDeleteCaptureBin(path) {
6119
+ const id = path.replace("/api/capture/bins/", "");
6120
+ dbQueries.deleteCaptureBin(id);
6121
+ return json({ ok: true });
6122
+ }
6123
+ var CORS4, TOOL_DEFS, ANTHROPIC_TOOLS, OPENAI_TOOLS, MAX_TOTAL_TOOLS = 40, MAX_CONSECUTIVE_ERRORS = 5, MAX_SAME_ENDPOINT_ERRORS = 3;
5467
6124
  var init_routes = __esm(() => {
5468
6125
  init_db();
5469
6126
  init_engine();
5470
6127
  init_bus();
5471
6128
  init_state();
6129
+ init_parser();
5472
6130
  init_config();
5473
6131
  init_version();
5474
- CORS3 = {
6132
+ init_engine2();
6133
+ CORS4 = {
5475
6134
  "Access-Control-Allow-Origin": "*",
5476
6135
  "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
5477
6136
  "Access-Control-Allow-Headers": "Content-Type, Authorization"
@@ -5677,7 +6336,7 @@ function printBanner(opts) {
5677
6336
  const hint = [
5678
6337
  `${paint.bold("r")} reload`,
5679
6338
  `${paint.bold("b")} background`,
5680
- `${paint.bold("/")} commands`,
6339
+ `${paint.bold("/")} commands ${paint.dim("(Tab to complete)")}`,
5681
6340
  `${paint.bold("q")} quit`,
5682
6341
  `${paint.bold("?")} help`
5683
6342
  ].join(` ${dot} `);
@@ -5908,6 +6567,392 @@ var init_update = __esm(() => {
5908
6567
  CHECK_INTERVAL = 24 * 60 * 60 * 1000;
5909
6568
  });
5910
6569
 
6570
+ // src/repl.ts
6571
+ function stripAnsi(s) {
6572
+ return s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
6573
+ }
6574
+ function visualRows(line, cols) {
6575
+ const len = stripAnsi(line).length;
6576
+ if (len === 0)
6577
+ return 1;
6578
+ return Math.ceil(len / cols);
6579
+ }
6580
+
6581
+ class Repl {
6582
+ buf = "";
6583
+ history = [];
6584
+ histIdx = -1;
6585
+ savedBuf = "";
6586
+ running = false;
6587
+ statusText = "";
6588
+ promptDrawn = false;
6589
+ drawingPrompt = false;
6590
+ promptVisualRows = 0;
6591
+ onCmd = null;
6592
+ cols = process.stdout.columns || 80;
6593
+ dynSuggestions = [];
6594
+ constructor() {
6595
+ if (isTTY) {
6596
+ process.stdout.on("resize", () => {
6597
+ this.cols = process.stdout.columns || 80;
6598
+ if (this.running)
6599
+ this.redraw();
6600
+ });
6601
+ }
6602
+ }
6603
+ setDynamicSuggestions(items) {
6604
+ this.dynSuggestions = items;
6605
+ if (this.running)
6606
+ this.redraw();
6607
+ }
6608
+ setStatus(text) {
6609
+ this.statusText = text;
6610
+ if (this.running)
6611
+ this.redraw();
6612
+ }
6613
+ print(line) {
6614
+ process.stdout.write(line + `
6615
+ `);
6616
+ }
6617
+ start(onCmd) {
6618
+ this.onCmd = onCmd;
6619
+ this.running = true;
6620
+ if (!isTTY || !process.stdin.setRawMode) {
6621
+ this.startSimple(onCmd);
6622
+ return;
6623
+ }
6624
+ const origWrite = process.stdout.write.bind(process.stdout);
6625
+ const self = this;
6626
+ const patched = function(chunk, enc, cb) {
6627
+ if (self.drawingPrompt)
6628
+ return origWrite(chunk, enc, cb);
6629
+ const str = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? new TextDecoder().decode(chunk) : String(chunk);
6630
+ const isSpinnerWrite = str.startsWith("\r") && !str.includes(`
6631
+ `);
6632
+ if (isSpinnerWrite || !self.promptDrawn)
6633
+ return origWrite(chunk, enc, cb);
6634
+ self.drawingPrompt = true;
6635
+ for (let i = 0;i < self.promptVisualRows - 1; i++)
6636
+ origWrite("\x1B[A");
6637
+ origWrite("\r\x1B[J");
6638
+ self.promptDrawn = false;
6639
+ const r = origWrite(chunk, enc, cb);
6640
+ if (!str.endsWith(`
6641
+ `))
6642
+ origWrite(`
6643
+ `);
6644
+ const lines = self.buildPromptLines();
6645
+ self.promptVisualRows = lines.reduce((sum, l) => sum + visualRows(l, self.cols), 0);
6646
+ origWrite(lines.join(`
6647
+ `));
6648
+ self.promptDrawn = true;
6649
+ self.drawingPrompt = false;
6650
+ return r;
6651
+ };
6652
+ patched.__orig = origWrite;
6653
+ process.stdout.write = patched;
6654
+ process.stdin.setRawMode(true);
6655
+ process.stdin.resume();
6656
+ process.stdin.setEncoding("utf8");
6657
+ process.stdin.on("data", (key) => {
6658
+ this.handleKey(key).catch(() => {});
6659
+ });
6660
+ this.drawPrompt();
6661
+ }
6662
+ stop() {
6663
+ this.running = false;
6664
+ if (this.promptDrawn)
6665
+ this.clearPrompt();
6666
+ if (process.stdout.write.__orig) {
6667
+ process.stdout.write = process.stdout.write.__orig;
6668
+ }
6669
+ try {
6670
+ process.stdin.setRawMode(false);
6671
+ process.stdin.pause();
6672
+ } catch {}
6673
+ }
6674
+ getSuggestions() {
6675
+ if (!this.buf.startsWith("/"))
6676
+ return [];
6677
+ const raw = this.buf.slice(1);
6678
+ if (!raw)
6679
+ return BASE.slice(0, 6);
6680
+ const parts = raw.split(" ");
6681
+ const base = parts[0] ?? "";
6682
+ if (raw.startsWith("auth use ") && this.dynSuggestions.length) {
6683
+ const typed = raw.slice("auth use ".length);
6684
+ return this.dynSuggestions.filter((s) => s.label.toLowerCase().startsWith(typed.toLowerCase()));
6685
+ }
6686
+ if (parts.length >= 2 && SUB[base]) {
6687
+ return (SUB[base] ?? []).filter((s) => s.value.startsWith(raw));
6688
+ }
6689
+ return BASE.filter((c) => c.value.startsWith(raw));
6690
+ }
6691
+ getGhostText() {
6692
+ if (!this.buf.startsWith("/"))
6693
+ return "";
6694
+ const suggestions = this.getSuggestions();
6695
+ if (!suggestions.length)
6696
+ return "";
6697
+ const first = suggestions[0];
6698
+ const full = "/" + first.value;
6699
+ if (full === this.buf || !full.startsWith(this.buf))
6700
+ return "";
6701
+ return full.slice(this.buf.length);
6702
+ }
6703
+ buildPromptLines() {
6704
+ const lines = [];
6705
+ const suggestions = this.getSuggestions();
6706
+ const DOT = paint.dim(" \xB7 ");
6707
+ if (this.buf.startsWith("/") && suggestions.length > 0) {
6708
+ const items = suggestions.slice(0, 5);
6709
+ const row = items.map((s, i) => {
6710
+ const label = "/" + s.label;
6711
+ const desc = s.desc ? paint.dim(" " + s.desc) : "";
6712
+ return i === 0 ? paint.cyan(label) + desc : paint.dim(label);
6713
+ }).join(DOT);
6714
+ lines.push(` ${row}`);
6715
+ }
6716
+ if (this.statusText) {
6717
+ const plain = stripAnsi(this.statusText);
6718
+ const truncated = plain.length > this.cols - 4 ? plain.slice(0, this.cols - 7) + "\u2026" : plain;
6719
+ lines.push(` ${paint.dim(truncated)}`);
6720
+ }
6721
+ const ghost = this.getGhostText();
6722
+ lines.push(` ${paint.cyan("\u276F")} ${this.buf}${ghost ? paint.dim(ghost) : ""}`);
6723
+ return lines;
6724
+ }
6725
+ clearPrompt() {
6726
+ if (!this.promptDrawn)
6727
+ return;
6728
+ this.drawingPrompt = true;
6729
+ for (let i = 0;i < this.promptVisualRows - 1; i++)
6730
+ process.stdout.write("\x1B[A");
6731
+ process.stdout.write("\r\x1B[J");
6732
+ this.promptDrawn = false;
6733
+ this.drawingPrompt = false;
6734
+ }
6735
+ drawPrompt() {
6736
+ this.drawingPrompt = true;
6737
+ const lines = this.buildPromptLines();
6738
+ this.promptVisualRows = lines.reduce((sum, l) => sum + visualRows(l, this.cols), 0);
6739
+ process.stdout.write(lines.join(`
6740
+ `));
6741
+ this.promptDrawn = true;
6742
+ this.drawingPrompt = false;
6743
+ }
6744
+ redraw() {
6745
+ this.clearPrompt();
6746
+ this.drawPrompt();
6747
+ }
6748
+ async handleKey(key) {
6749
+ if (!this.running)
6750
+ return;
6751
+ if (key === "\x03" || key === "\x04") {
6752
+ this.stop();
6753
+ process.emit("SIGINT");
6754
+ return;
6755
+ }
6756
+ if (key === "\f") {
6757
+ this.drawingPrompt = true;
6758
+ process.stdout.write("\x1B[2J\x1B[H");
6759
+ this.drawingPrompt = false;
6760
+ this.promptDrawn = false;
6761
+ this.drawPrompt();
6762
+ return;
6763
+ }
6764
+ if (key === "\x15") {
6765
+ this.buf = "";
6766
+ this.redraw();
6767
+ return;
6768
+ }
6769
+ if (key === "\x17") {
6770
+ this.buf = this.buf.replace(/\S+\s*$/, "");
6771
+ this.redraw();
6772
+ return;
6773
+ }
6774
+ if (key.startsWith("\x1B")) {
6775
+ if (key === "\x1B") {
6776
+ this.buf = "";
6777
+ this.redraw();
6778
+ return;
6779
+ }
6780
+ if (key === "\x1B[A") {
6781
+ this.historyUp();
6782
+ return;
6783
+ }
6784
+ if (key === "\x1B[B") {
6785
+ this.historyDown();
6786
+ return;
6787
+ }
6788
+ if (key === "\x1B[C") {
6789
+ const g = this.getGhostText();
6790
+ if (g) {
6791
+ this.buf += g;
6792
+ const raw = this.buf.slice(1);
6793
+ if (SUB[raw])
6794
+ this.buf += " ";
6795
+ this.redraw();
6796
+ }
6797
+ return;
6798
+ }
6799
+ return;
6800
+ }
6801
+ if (key === "\t") {
6802
+ const g = this.getGhostText();
6803
+ if (g) {
6804
+ this.buf += g;
6805
+ const raw = this.buf.slice(1);
6806
+ if (SUB[raw])
6807
+ this.buf += " ";
6808
+ } else {
6809
+ const suggestions = this.getSuggestions();
6810
+ if (suggestions[0] && "/" + suggestions[0].value !== this.buf) {
6811
+ this.buf = "/" + suggestions[0].value;
6812
+ }
6813
+ }
6814
+ this.redraw();
6815
+ return;
6816
+ }
6817
+ if (key === "\x7F" || key === "\b") {
6818
+ if (this.buf.length > 0) {
6819
+ this.buf = this.buf.slice(0, -1);
6820
+ this.redraw();
6821
+ }
6822
+ return;
6823
+ }
6824
+ if (key === "\r" || key === `
6825
+ `) {
6826
+ await this.submit();
6827
+ return;
6828
+ }
6829
+ if (this.buf === "") {
6830
+ switch (key.toLowerCase()) {
6831
+ case "r":
6832
+ await this.dispatchImmediate("r");
6833
+ return;
6834
+ case "b":
6835
+ await this.dispatchImmediate("b");
6836
+ return;
6837
+ case "s":
6838
+ await this.dispatchImmediate("s");
6839
+ return;
6840
+ case "q":
6841
+ await this.dispatchImmediate("q");
6842
+ return;
6843
+ case "?":
6844
+ case "h":
6845
+ await this.dispatchImmediate("h");
6846
+ return;
6847
+ case "/":
6848
+ this.buf = "/";
6849
+ this.redraw();
6850
+ return;
6851
+ }
6852
+ }
6853
+ const printable = key.replace(/[^\x20-\x7E]/g, "");
6854
+ if (printable) {
6855
+ this.buf += printable;
6856
+ this.redraw();
6857
+ }
6858
+ }
6859
+ async submit() {
6860
+ const cmd = this.buf.trim();
6861
+ this.buf = "";
6862
+ this.clearPrompt();
6863
+ process.stdout.write(`
6864
+ `);
6865
+ this.promptDrawn = false;
6866
+ if (cmd) {
6867
+ this.history.unshift(cmd);
6868
+ if (this.history.length > 200)
6869
+ this.history.pop();
6870
+ this.histIdx = -1;
6871
+ this.savedBuf = "";
6872
+ if (this.onCmd)
6873
+ await this.onCmd(cmd);
6874
+ }
6875
+ this.drawPrompt();
6876
+ }
6877
+ async dispatchImmediate(key) {
6878
+ this.clearPrompt();
6879
+ process.stdout.write(`
6880
+ `);
6881
+ this.promptDrawn = false;
6882
+ if (this.onCmd)
6883
+ await this.onCmd(key);
6884
+ this.drawPrompt();
6885
+ }
6886
+ historyUp() {
6887
+ if (!this.history.length)
6888
+ return;
6889
+ if (this.histIdx === -1)
6890
+ this.savedBuf = this.buf;
6891
+ this.histIdx = Math.min(this.histIdx + 1, this.history.length - 1);
6892
+ this.buf = this.history[this.histIdx] ?? "";
6893
+ this.redraw();
6894
+ }
6895
+ historyDown() {
6896
+ if (this.histIdx === -1)
6897
+ return;
6898
+ this.histIdx--;
6899
+ this.buf = this.histIdx === -1 ? this.savedBuf : this.history[this.histIdx] ?? "";
6900
+ this.redraw();
6901
+ }
6902
+ startSimple(onCmd) {
6903
+ process.stdout.write(" \u276F ");
6904
+ process.stdin.setEncoding("utf8");
6905
+ let line = "";
6906
+ process.stdin.on("data", async (chunk) => {
6907
+ for (const ch of chunk) {
6908
+ if (ch === `
6909
+ ` || ch === "\r") {
6910
+ const cmd = line.trim();
6911
+ line = "";
6912
+ if (cmd)
6913
+ await onCmd(cmd);
6914
+ process.stdout.write(" \u276F ");
6915
+ } else {
6916
+ line += ch;
6917
+ }
6918
+ }
6919
+ });
6920
+ }
6921
+ }
6922
+ var BASE, SUB;
6923
+ var init_repl = __esm(() => {
6924
+ init_ui();
6925
+ BASE = [
6926
+ { value: "help", label: "help", desc: "Show all commands" },
6927
+ { value: "status", label: "status", desc: "Show server status" },
6928
+ { value: "reload", label: "reload", desc: "Hot-reload the spec" },
6929
+ { value: "spec", label: "spec", desc: "Load a different spec" },
6930
+ { value: "mcp", label: "mcp", desc: "Toggle MCP endpoint" },
6931
+ { value: "proxy", label: "proxy", desc: "Toggle HTTP proxy" },
6932
+ { value: "ai", label: "ai", desc: "Toggle AI chat" },
6933
+ { value: "readonly", label: "readonly", desc: "Toggle read-only mode" },
6934
+ { value: "auth", label: "auth", desc: "Manage auth roles" },
6935
+ { value: "token", label: "token", desc: "Manage access token" },
6936
+ { value: "tail", label: "tail", desc: "Live request log" },
6937
+ { value: "open", label: "open", desc: "Open studio in browser" },
6938
+ { value: "update", label: "update", desc: "Update wasper" },
6939
+ { value: "quit", label: "quit", desc: "Quit" }
6940
+ ];
6941
+ SUB = {
6942
+ auth: [
6943
+ { value: "auth list", label: "list", desc: "List auth profiles" },
6944
+ { value: "auth use ", label: "use", desc: "Switch active profile" },
6945
+ { value: "auth none", label: "none", desc: "Disable auth" }
6946
+ ],
6947
+ mcp: [{ value: "mcp on", label: "on", desc: "" }, { value: "mcp off", label: "off", desc: "" }],
6948
+ proxy: [{ value: "proxy on", label: "on", desc: "" }, { value: "proxy off", label: "off", desc: "" }],
6949
+ ai: [{ value: "ai on", label: "on", desc: "" }, { value: "ai off", label: "off", desc: "" }],
6950
+ readonly: [{ value: "readonly on", label: "on", desc: "" }, { value: "readonly off", label: "off", desc: "" }],
6951
+ token: [{ value: "token new", label: "new", desc: "Generate token" }, { value: "token off", label: "off", desc: "Remove token" }],
6952
+ tail: [{ value: "tail on", label: "on", desc: "" }, { value: "tail off", label: "off", desc: "" }]
6953
+ };
6954
+ });
6955
+
5911
6956
  // src/commands/start.ts
5912
6957
  var exports_start = {};
5913
6958
  __export(exports_start, {
@@ -5942,18 +6987,44 @@ async function run2(overrideOpts) {
5942
6987
  printHelp();
5943
6988
  process.exit(0);
5944
6989
  }
5945
- const specUrl = overrideOpts?.url ?? (values.url ? String(values.url) : null) ?? process.env.WASPER_SPEC_URL ?? null;
6990
+ let specUrl = overrideOpts?.url ?? (values.url ? String(values.url) : null) ?? process.env.WASPER_SPEC_URL ?? null;
5946
6991
  const PORT = overrideOpts?.port ?? parseInt(String(values.port ?? "3388"), 10);
5947
6992
  const HOST = overrideOpts?.host ?? (values.host ? String(values.host) : null) ?? process.env.WASPER_HOST ?? "0.0.0.0";
5948
6993
  const ORIGIN = (overrideOpts?.origin ?? (values.origin ? String(values.origin) : null) ?? process.env.WASPER_ORIGIN ?? null)?.replace(/\/$/, "") ?? null;
5949
6994
  const TOKEN = overrideOpts?.token ?? (values.token ? String(values.token) : null) ?? process.env.WASPER_TOKEN ?? null;
5950
6995
  const bgNow = overrideOpts?.daemon ?? !!(values.background || values.daemon);
5951
6996
  const isDaemon = overrideOpts?.isDaemon ?? !!values["_daemon"];
6997
+ if (!specUrl && !isDaemon) {
6998
+ const last = dbQueries.getLastSpec();
6999
+ if (last) {
7000
+ specUrl = last.url;
7001
+ if (isTTY)
7002
+ console.log(` ${paint.dim("\u21A9")} Resuming ${paint.cyan(last.title ?? last.url)} ${paint.dim("(last used)")}
7003
+ `);
7004
+ }
7005
+ }
5952
7006
  setServerConfig({ port: PORT, host: HOST, origin: ORIGIN, token: TOKEN });
7007
+ {
7008
+ const saved = dbQueries.getSetting("features");
7009
+ if (saved) {
7010
+ try {
7011
+ const obj = JSON.parse(saved);
7012
+ const patch = {};
7013
+ if (obj.mcp === false)
7014
+ patch.mcp = false;
7015
+ if (obj.proxy === false)
7016
+ patch.proxy = false;
7017
+ if (obj.ai === false)
7018
+ patch.ai = false;
7019
+ if (Object.keys(patch).length)
7020
+ setFeatures(patch);
7021
+ } catch {}
7022
+ }
7023
+ }
5953
7024
  setFeatures({
5954
- mcp: !values["no-mcp"],
5955
- proxy: !values["no-proxy"],
5956
- ai: !values["no-ai"],
7025
+ ...values["no-mcp"] ? { mcp: false } : {},
7026
+ ...values["no-proxy"] ? { proxy: false } : {},
7027
+ ...values["no-ai"] ? { ai: false } : {},
5957
7028
  readonly: !!values.readonly
5958
7029
  });
5959
7030
  if (bgNow) {
@@ -5980,6 +7051,7 @@ async function run2(overrideOpts) {
5980
7051
  specVersion = state.spec.version;
5981
7052
  endpointCount = state.operations.length;
5982
7053
  spinner.stop();
7054
+ dbQueries.upsertSpec(specUrl, specTitle ?? null, specVersion ?? null, endpointCount);
5983
7055
  } catch (e) {
5984
7056
  spinner.stop("\u2717", `Failed to load spec: ${e instanceof Error ? e.message : String(e)}`, "red");
5985
7057
  }
@@ -6002,6 +7074,8 @@ async function run2(overrideOpts) {
6002
7074
  if (req.method === "OPTIONS") {
6003
7075
  return new Response(null, { status: 204, headers: CORS_HEADERS });
6004
7076
  }
7077
+ if (pathname.startsWith("/c/"))
7078
+ return captureHandler(req);
6005
7079
  if (!isAuthorized(req)) {
6006
7080
  return new Response(JSON.stringify({ error: "Unauthorized: pass Authorization: Bearer <token> or ?token=" }), {
6007
7081
  status: 401,
@@ -6092,127 +7166,129 @@ async function run2(overrideOpts) {
6092
7166
  function attachKeyboard(opts) {
6093
7167
  const ctx = { specUrl: opts.specUrl, PORT: opts.PORT, tailOff: null };
6094
7168
  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;
7169
+ const repl = new Repl;
7170
+ const buildStatus = () => {
7171
+ const state = hasState() ? getState() : null;
7172
+ const f = getFeatures();
7173
+ const cfg = getServerConfig();
7174
+ const active = dbQueries.getActiveProfile();
7175
+ const parts = [];
7176
+ if (state) {
7177
+ parts.push(`${state.spec.title} v${state.spec.version} \xB7 ${state.operations.length} ep`);
7178
+ } else {
7179
+ parts.push("no spec");
7180
+ }
7181
+ parts.push(`:${ctx.PORT}`);
7182
+ const flags = [];
7183
+ if (!f.mcp)
7184
+ flags.push("mcp:off");
7185
+ if (!f.proxy)
7186
+ flags.push("proxy:off");
7187
+ if (!f.ai)
7188
+ flags.push("ai:off");
7189
+ if (f.readonly)
7190
+ flags.push("readonly:on");
7191
+ if (flags.length)
7192
+ parts.push(flags.join(" "));
7193
+ parts.push(`auth:${active ? active.name : "none"}`);
7194
+ if (cfg.token)
7195
+ parts.push("token:set");
7196
+ if (ctx.tailOff)
7197
+ parts.push("tail:on");
7198
+ return parts.join(" \xB7 ");
7199
+ };
7200
+ const refreshStatus = () => repl.setStatus(buildStatus());
7201
+ const refreshAuthSuggestions = () => {
7202
+ const profiles = dbQueries.getProfiles();
7203
+ repl.setDynamicSuggestions(profiles.map((p) => ({
7204
+ value: `auth use ${p.name}`,
7205
+ label: p.name,
7206
+ desc: p.type
7207
+ })));
7208
+ };
6100
7209
  const reload = async () => {
6101
7210
  if (isReloading)
6102
7211
  return;
6103
- process.stdout.write(`
6104
- `);
6105
7212
  if (!ctx.specUrl) {
6106
- console.log(` ${paint.yellow("\u25CB")} No spec URL \u2014 use /spec <url> or start with --url <url>`);
6107
- console.log();
7213
+ console.log(`
7214
+ ${paint.yellow("\u25CB")} No spec URL \u2014 use /spec <url>
7215
+ `);
6108
7216
  return;
6109
7217
  }
6110
7218
  isReloading = true;
6111
- spinner.start(`Reloading spec\u2026`);
7219
+ let fi = 0;
7220
+ const base = buildStatus();
7221
+ const spinTimer = setInterval(() => {
7222
+ repl.setStatus(`${SPIN_FRAMES[fi++ % SPIN_FRAMES.length]} Reloading\u2026 \xB7 ${base}`);
7223
+ }, 80);
6112
7224
  try {
6113
7225
  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");
7226
+ clearInterval(spinTimer);
7227
+ dbQueries.upsertSpec(ctx.specUrl, state.spec.title, state.spec.version, state.operations.length);
7228
+ console.log(` ${paint.green("\u2713")} ${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} \xB7 ${paint.green(state.operations.length + " endpoints")}
7229
+ `);
6115
7230
  } catch (e) {
6116
- spinner.stop("\u2717", `Reload failed: ${e instanceof Error ? e.message : String(e)}`, "red");
7231
+ clearInterval(spinTimer);
7232
+ console.log(` ${paint.red("\u2717")} Reload failed: ${e instanceof Error ? e.message : String(e)}
7233
+ `);
6117
7234
  } finally {
6118
7235
  isReloading = false;
7236
+ refreshStatus();
6119
7237
  }
6120
- console.log();
6121
7238
  };
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");
7239
+ const handler = async (input) => {
7240
+ const cmd = input.trim();
7241
+ if (cmd.length === 1) {
7242
+ switch (cmd.toLowerCase()) {
7243
+ case "r":
7244
+ await reload();
6150
7245
  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")}
7246
+ case "s":
7247
+ printInlineStatus(ctx);
7248
+ return;
7249
+ case "q":
7250
+ process.emit("SIGINT");
7251
+ return;
7252
+ case "h":
7253
+ case "?":
7254
+ printInteractiveHelp();
7255
+ return;
7256
+ case "b": {
7257
+ repl.stop();
7258
+ process.on("SIGHUP", () => {});
7259
+ console.log(`
7260
+ ${paint.green("\u2713")} Detached ${paint.dim("PID " + process.pid)}`);
7261
+ console.log(` ${paint.dim("\u279C")} ${paint.dim("wasper status")} ${paint.dim("\xB7")} ${paint.dim("wasper stop")}
6186
7262
  `);
6187
- break;
7263
+ return;
7264
+ }
6188
7265
  }
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
7266
  }
7267
+ const slashCmd = cmd.startsWith("/") ? cmd : `/${cmd}`;
7268
+ await runSlashCommand(slashCmd, ctx, reload);
7269
+ refreshStatus();
7270
+ refreshAuthSuggestions();
6200
7271
  };
7272
+ refreshAuthSuggestions();
7273
+ repl.setStatus(buildStatus());
7274
+ repl.start(handler);
6201
7275
  }
6202
7276
  function printInlineStatus(ctx) {
6203
7277
  const state = hasState() ? getState() : null;
6204
7278
  const f = getFeatures();
6205
7279
  const cfg = getServerConfig();
6206
- const flag = (on) => on ? paint.green("on") : paint.red("off");
7280
+ const on = (v) => v ? paint.green("on") : paint.dim("off");
7281
+ const dot = paint.dim("\xB7");
6207
7282
  console.log(`
6208
- ${paint.dim("\u25CF")} ${paint.bold("OpenAPI Agent")} PID ${process.pid} port ${ctx.PORT}`);
7283
+ ${paint.green("\u25CF")} ${paint.bold("wasper")} ${paint.dim("PID " + process.pid)} ${dot} ${paint.dim(":" + ctx.PORT)}`);
6209
7284
  if (state) {
6210
- console.log(` ${paint.bold(state.spec.title)} v${state.spec.version} \xB7 ${state.operations.length} endpoints`);
7285
+ console.log(` ${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} ${dot} ${paint.green(state.operations.length + " endpoints")}`);
7286
+ } else {
7287
+ console.log(` ${paint.dim("no spec loaded")}`);
6211
7288
  }
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")}`);
7289
+ 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
7290
  const active = dbQueries.getActiveProfile();
6214
- if (active)
6215
- console.log(` auth role: ${paint.bold(active.name)} ${paint.dim(`(${active.type})`)}`);
7291
+ console.log(` auth ${active ? paint.bold(active.name) + " " + paint.dim("(" + active.type + ")") : paint.dim("none")}`);
6216
7292
  console.log();
6217
7293
  }
6218
7294
  async function runSlashCommand(input, ctx, reload) {
@@ -6224,6 +7300,7 @@ async function runSlashCommand(input, ctx, reload) {
6224
7300
  const cur = getFeatures()[name];
6225
7301
  const next = arg === "on" ? true : arg === "off" ? false : !cur;
6226
7302
  setFeatures({ [name]: next });
7303
+ persistAndBroadcastFeatures();
6227
7304
  console.log(` ${next ? paint.green("\u2713") : paint.yellow("\u25CB")} ${label} ${next ? paint.green("enabled") : paint.yellow("disabled")}
6228
7305
  `);
6229
7306
  };
@@ -6368,40 +7445,48 @@ async function runSlashCommand(input, ctx, reload) {
6368
7445
  }
6369
7446
  }
6370
7447
  function printInteractiveHelp() {
7448
+ const k = (s) => paint.bold(s);
7449
+ const d = (s) => paint.dim(s);
7450
+ const hr = d("\u2500".repeat(50));
6371
7451
  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)")}
7452
+ ${paint.bold("Keys")} ${d("(when input is empty)")}
7453
+ ${hr}
7454
+ ${k("r")} Hot-reload spec ${k("b")} Detach to background
7455
+ ${k("s")} Print status ${k("q")} Quit
7456
+ ${k("/")} Start a command ${k("?")} This help
7457
+
7458
+ ${d("\u2191 / \u2193")} cycle command history \xB7 ${d("\u2192 or Tab")} accept autocomplete
7459
+ ${d("Ctrl+L")} clear screen \xB7 ${d("Ctrl+U")} clear input \xB7 ${d("Esc")} cancel
6379
7460
 
6380
7461
  ${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")}
7462
+ ${hr}
7463
+ ${k("/spec")} ${d("<url>")} Load a different OpenAPI spec
7464
+ ${k("/mcp")} ${d("[on|off]")} Toggle the MCP endpoint
7465
+ ${k("/proxy")} ${d("[on|off]")} Toggle the HTTP proxy
7466
+ ${k("/ai")} ${d("[on|off]")} Toggle the AI chat endpoint
7467
+ ${k("/readonly")} ${d("[on|off]")} Block non-GET upstream requests
7468
+ ${k("/auth")} List saved auth profiles
7469
+ ${k("/auth use")} ${d("<name>")} Switch active auth profile
7470
+ ${k("/auth none")} Disable auth
7471
+ ${k("/token")} ${d("[new|off|<v>]")} Show / rotate / set the access token
7472
+ ${k("/tail")} ${d("[on|off]")} Live request log in this terminal
7473
+ ${k("/open")} Open the studio in a browser
7474
+ ${k("/update")} Update wasper to the latest version
7475
+ ${k("/status")} ${k("/reload")} ${k("/help")} ${k("/quit")}
6394
7476
  `);
6395
7477
  }
6396
7478
  function printHelp() {
6397
7479
  console.log(`
6398
7480
  Usage: wasper [start] [options]
6399
7481
 
6400
- openapi-agent [--url <spec-url>] [--port <port>] Start in foreground
7482
+ wasper [--url <spec-url>] [--port <port>] Start in foreground (auto-resumes last spec)
6401
7483
  wasper start --background Start in background
6402
7484
  wasper stop Stop background server
6403
7485
  wasper status Show server status
6404
7486
  wasper reload Hot-reload the spec
7487
+ wasper ls List saved specs (history)
7488
+ wasper use <number|url> Start with a saved spec
7489
+ wasper rm <number|url> Remove a spec from history
6405
7490
 
6406
7491
  Options:
6407
7492
  --url, -u OpenAPI spec URL or local path
@@ -6520,9 +7605,11 @@ async function runFirstTimeSetup(port, origin) {
6520
7605
  ${paint.dim("Skip future prompts: set WASPER_NO_FIRST_RUN=1")}
6521
7606
  `);
6522
7607
  }
7608
+ var SPIN_FRAMES;
6523
7609
  var init_start = __esm(() => {
6524
7610
  init_server();
6525
7611
  init_handler();
7612
+ init_capture();
6526
7613
  init_routes();
6527
7614
  init_bus();
6528
7615
  init_db();
@@ -6531,6 +7618,9 @@ var init_start = __esm(() => {
6531
7618
  init_config();
6532
7619
  init_update();
6533
7620
  init_ui();
7621
+ init_repl();
7622
+ init_routes();
7623
+ SPIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
6534
7624
  });
6535
7625
 
6536
7626
  // index.ts