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.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/cli.js +1887 -641
- package/dist/index.js +1218 -128
- 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.
|
|
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", ...
|
|
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:
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
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
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
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(`
|
|
6107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6123
|
-
|
|
6124
|
-
if (
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
});
|
|
6129
|
-
const handleKey = async (key) => {
|
|
6130
|
-
if (key === "\x03") {
|
|
6131
|
-
if (cmdBuf !== null) {
|
|
6132
|
-
cmdBuf = null;
|
|
6133
|
-
process.stdout.write("\r\x1B[K");
|
|
6134
|
-
return;
|
|
6135
|
-
}
|
|
6136
|
-
process.emit("SIGINT");
|
|
6137
|
-
return;
|
|
6138
|
-
}
|
|
6139
|
-
if (cmdBuf !== null) {
|
|
6140
|
-
if (key === "\x1B") {
|
|
6141
|
-
cmdBuf = null;
|
|
6142
|
-
process.stdout.write("\r\x1B[K");
|
|
6143
|
-
return;
|
|
6144
|
-
}
|
|
6145
|
-
if (key === "\x7F" || key === "\b") {
|
|
6146
|
-
cmdBuf = cmdBuf.slice(0, -1);
|
|
6147
|
-
if (!cmdBuf) {
|
|
6148
|
-
cmdBuf = null;
|
|
6149
|
-
process.stdout.write("\r\x1B[K");
|
|
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
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
}
|
|
6168
|
-
return;
|
|
6169
|
-
}
|
|
6170
|
-
if (key === "/") {
|
|
6171
|
-
cmdBuf = "/";
|
|
6172
|
-
printPrompt();
|
|
6173
|
-
return;
|
|
6174
|
-
}
|
|
6175
|
-
switch (key.toLowerCase()) {
|
|
6176
|
-
case "r":
|
|
6177
|
-
await reload();
|
|
6178
|
-
break;
|
|
6179
|
-
case "b": {
|
|
6180
|
-
process.stdin.setRawMode(false);
|
|
6181
|
-
process.stdin.pause();
|
|
6182
|
-
process.on("SIGHUP", () => {});
|
|
6183
|
-
console.log(`
|
|
6184
|
-
${paint.green("\u2713")} Detached ${paint.dim(`PID ${process.pid}`)}`);
|
|
6185
|
-
console.log(` ${paint.dim("\u279C")} ${paint.dim("wasper status")} ${paint.dim("\xB7")} ${paint.dim("wasper stop")}
|
|
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
|
-
|
|
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
|
|
7280
|
+
const on = (v) => v ? paint.green("on") : paint.dim("off");
|
|
7281
|
+
const dot = paint.dim("\xB7");
|
|
6207
7282
|
console.log(`
|
|
6208
|
-
${paint.
|
|
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)}
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
${
|
|
6374
|
-
${
|
|
6375
|
-
${
|
|
6376
|
-
${
|
|
6377
|
-
|
|
6378
|
-
${
|
|
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
|
-
${
|
|
6382
|
-
${
|
|
6383
|
-
${
|
|
6384
|
-
${
|
|
6385
|
-
${
|
|
6386
|
-
${
|
|
6387
|
-
${
|
|
6388
|
-
${
|
|
6389
|
-
${
|
|
6390
|
-
${
|
|
6391
|
-
${
|
|
6392
|
-
${
|
|
6393
|
-
${
|
|
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
|
-
|
|
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
|