wasper-cli 0.3.1 → 0.3.3

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 (4) hide show
  1. package/README.md +77 -83
  2. package/dist/cli.js +1845 -834
  3. package/dist/index.js +90 -31
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -21,7 +21,7 @@ var package_default;
21
21
  var init_package = __esm(() => {
22
22
  package_default = {
23
23
  name: "wasper-cli",
24
- version: "0.3.1",
24
+ version: "0.3.3",
25
25
  description: "Host an MCP server + API proxy from any OpenAPI spec. Like Drizzle Studio, but for APIs.",
26
26
  type: "module",
27
27
  homepage: "https://wasper.site",
@@ -111,25 +111,69 @@ var init_version = __esm(() => {
111
111
  // src/daemon.ts
112
112
  import { join } from "path";
113
113
  import { homedir } from "os";
114
- import { mkdir, readFile, writeFile, unlink } from "fs/promises";
114
+ import { mkdir, readFile, readdir, writeFile, unlink } from "fs/promises";
115
115
  async function ensureDir() {
116
- await mkdir(DIR, { recursive: true });
116
+ await mkdir(WASPER_DIR, { recursive: true });
117
+ }
118
+ function stateFile(port) {
119
+ return join(WASPER_DIR, `server-${port}.json`);
120
+ }
121
+ function logFile(port) {
122
+ return join(WASPER_DIR, `server-${port}.log`);
117
123
  }
118
124
  async function writeDaemonState(s) {
119
125
  await ensureDir();
120
- await writeFile(STATE_FILE, JSON.stringify(s, null, 2), "utf-8");
126
+ await writeFile(stateFile(s.port), JSON.stringify(s, null, 2), "utf-8");
121
127
  }
122
- async function readDaemonState() {
128
+ async function readAllDaemonStates() {
123
129
  try {
124
- const raw = await readFile(STATE_FILE, "utf-8");
125
- return JSON.parse(raw);
130
+ const files = await readdir(WASPER_DIR);
131
+ const states = [];
132
+ for (const f of files) {
133
+ if (!f.match(/^server(-\d+)?\.json$/))
134
+ continue;
135
+ const filePath = join(WASPER_DIR, f);
136
+ try {
137
+ const raw = await readFile(filePath, "utf-8");
138
+ const state = JSON.parse(raw);
139
+ if (isProcessAlive(state.pid)) {
140
+ states.push(state);
141
+ } else {
142
+ await unlink(filePath).catch(() => {});
143
+ }
144
+ } catch {}
145
+ }
146
+ return states.sort((a, b) => a.port - b.port);
126
147
  } catch {
127
- return null;
148
+ return [];
149
+ }
150
+ }
151
+ async function readDaemonState(port) {
152
+ if (port !== undefined) {
153
+ try {
154
+ const raw = await readFile(stateFile(port), "utf-8");
155
+ const state = JSON.parse(raw);
156
+ return isProcessAlive(state.pid) ? state : null;
157
+ } catch {
158
+ return null;
159
+ }
128
160
  }
161
+ const all = await readAllDaemonStates();
162
+ if (all.length === 0)
163
+ return null;
164
+ if (all.length === 1)
165
+ return all[0];
166
+ return all.find((s) => s.port === DEFAULT_PORT) ?? all[0];
129
167
  }
130
- async function clearDaemonState() {
168
+ async function clearDaemonState(port) {
169
+ try {
170
+ await unlink(stateFile(port));
171
+ } catch {}
131
172
  try {
132
- await unlink(STATE_FILE);
173
+ const raw = await readFile(join(WASPER_DIR, "server.json"), "utf-8");
174
+ const state = JSON.parse(raw);
175
+ if (state.port === port)
176
+ await unlink(join(WASPER_DIR, "server.json")).catch(() => {});
133
177
  } catch {}
134
178
  }
135
179
  function isProcessAlive(pid) {
@@ -163,353 +207,830 @@ async function spawnDaemon(specUrl, port, opts = {}) {
163
207
  args.push("--readonly");
164
208
  }
165
209
  args.push("--_daemon");
166
- const logDir = DIR;
167
210
  await ensureDir();
168
- const logPath = join(logDir, "server.log");
169
211
  const child = Bun.spawn([process.execPath, Bun.main, ...args], {
170
212
  detached: true,
171
213
  cwd: process.cwd(),
172
214
  env: { ...process.env },
173
- stdio: ["ignore", Bun.file(logPath), Bun.file(logPath)]
215
+ stdio: ["ignore", Bun.file(logFile(port)), Bun.file(logFile(port))]
174
216
  });
175
217
  child.unref();
176
218
  return child.pid;
177
219
  }
178
- var DIR, STATE_FILE;
220
+ var WASPER_DIR, DEFAULT_PORT = 3388;
179
221
  var init_daemon = __esm(() => {
180
- DIR = join(homedir(), ".wasper");
181
- STATE_FILE = join(DIR, "server.json");
222
+ WASPER_DIR = join(homedir(), ".wasper");
182
223
  });
183
224
 
184
- // src/ui.ts
185
- class Spinner {
186
- i = 0;
187
- timer = null;
188
- msg = "";
189
- start(msg) {
190
- this.msg = msg;
191
- if (!isTTY) {
192
- process.stdout.write(` ${msg}
193
- `);
194
- return;
195
- }
196
- this.i = 0;
197
- this.timer = setInterval(() => {
198
- const f = FRAMES[this.i++ % FRAMES.length];
199
- process.stdout.write(`\r ${paint.cyan(f)} ${this.msg}\x1B[K`);
200
- }, 80);
201
- }
202
- update(msg) {
203
- this.msg = msg;
204
- }
205
- stop(icon = "\u2713", msg = "", color = "green") {
206
- if (this.timer) {
207
- clearInterval(this.timer);
208
- this.timer = null;
209
- }
210
- if (!isTTY) {
211
- if (msg)
212
- process.stdout.write(` ${icon} ${msg}
213
- `);
214
- return;
215
- }
216
- process.stdout.write(msg ? `\r ${paint[color](icon)} ${msg}\x1B[K
217
- ` : `\r\x1B[K`);
218
- }
219
- }
220
- function printBanner(opts) {
221
- const { port, pid, specTitle, specVersion, endpointCount, origin, host, tokenSet } = opts;
222
- const base = origin ?? `http://localhost:${port}`;
223
- const arrow = paint.cyan("\u279C");
224
- const dot = paint.dim("\xB7");
225
- const hint = [
226
- `${paint.bold("r")} reload`,
227
- `${paint.bold("b")} background`,
228
- `${paint.bold("/")} commands ${paint.dim("(Tab to complete)")}`,
229
- `${paint.bold("q")} quit`,
230
- `${paint.bold("?")} help`
231
- ].join(` ${dot} `);
232
- const lines = [
233
- "",
234
- ` ${paint.bold("wasper")} ${paint.dim("PID " + pid)}`,
235
- "",
236
- ` ${arrow} ${paint.dim("Studio ")} ${paint.url(base + "/")}`,
237
- ` ${arrow} ${paint.dim("MCP ")} ${paint.url(base + "/mcp")}`,
238
- ` ${arrow} ${paint.dim("OpenAPI")} ${paint.url(base + "/openapi.json")}`,
239
- ""
240
- ];
241
- if (origin) {
242
- lines.push(` ${arrow} ${paint.dim("Local ")} ${paint.url(`http://localhost:${port}/`)}${host && host !== "0.0.0.0" ? ` ${dot} ${paint.dim("bound to " + host)}` : ""}`, "");
243
- }
244
- if (specTitle) {
245
- const ep = endpointCount != null ? ` ${dot} ${paint.green(endpointCount + " endpoints")}` : "";
246
- lines.push(` ${paint.green("\u2713")} ${paint.bold(specTitle)} ${paint.dim("v" + (specVersion ?? ""))}${ep}`);
247
- } else {
248
- lines.push(` ${paint.yellow("\u25CB")} ${paint.dim("No spec \u2014 start with --url <url>")}`);
249
- }
250
- if (tokenSet) {
251
- lines.push(` ${paint.green("\u2713")} ${paint.dim("Access token required (Authorization: Bearer \u2026 or ?token=)")}`);
252
- } else if (origin) {
253
- lines.push(` ${paint.yellow("!")} ${paint.yellow("Publicly reachable without a token \u2014 consider --token <secret>")}`);
225
+ // src/db/schema.ts
226
+ var SCHEMA = `
227
+ CREATE TABLE IF NOT EXISTS auth_config (
228
+ id TEXT PRIMARY KEY DEFAULT 'default',
229
+ type TEXT NOT NULL DEFAULT 'none',
230
+ config TEXT NOT NULL DEFAULT '{}',
231
+ token_cache TEXT,
232
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
233
+ );
234
+
235
+ CREATE TABLE IF NOT EXISTS request_logs (
236
+ id TEXT PRIMARY KEY,
237
+ source TEXT NOT NULL DEFAULT 'mcp',
238
+ tool_name TEXT,
239
+ method TEXT NOT NULL,
240
+ url TEXT NOT NULL,
241
+ request_headers TEXT,
242
+ request_body TEXT,
243
+ status_code INTEGER,
244
+ response_headers TEXT,
245
+ response_body TEXT,
246
+ latency_ms INTEGER,
247
+ error TEXT,
248
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
249
+ );
250
+
251
+ CREATE INDEX IF NOT EXISTS idx_logs_created ON request_logs(created_at DESC);
252
+
253
+ CREATE TABLE IF NOT EXISTS settings (
254
+ key TEXT PRIMARY KEY,
255
+ value TEXT NOT NULL DEFAULT '{}'
256
+ );
257
+
258
+ CREATE TABLE IF NOT EXISTS intercept_rules (
259
+ id TEXT PRIMARY KEY,
260
+ enabled INTEGER NOT NULL DEFAULT 1,
261
+ name TEXT NOT NULL DEFAULT '',
262
+ sort_order INTEGER NOT NULL DEFAULT 0,
263
+ match_path TEXT NOT NULL DEFAULT '',
264
+ match_method TEXT NOT NULL DEFAULT '',
265
+ target_host TEXT NOT NULL DEFAULT '',
266
+ strip_prefix TEXT NOT NULL DEFAULT '',
267
+ add_prefix TEXT NOT NULL DEFAULT '',
268
+ add_headers TEXT NOT NULL DEFAULT '{}',
269
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
270
+ );
271
+ CREATE INDEX IF NOT EXISTS idx_rules_order ON intercept_rules(sort_order, created_at);
272
+
273
+ CREATE TABLE IF NOT EXISTS auth_profiles (
274
+ id TEXT PRIMARY KEY,
275
+ name TEXT NOT NULL,
276
+ description TEXT NOT NULL DEFAULT '',
277
+ type TEXT NOT NULL DEFAULT 'none',
278
+ config TEXT NOT NULL DEFAULT '{}',
279
+ token_cache TEXT,
280
+ is_active INTEGER NOT NULL DEFAULT 0,
281
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
282
+ );
283
+
284
+ CREATE TABLE IF NOT EXISTS saved_requests (
285
+ id TEXT PRIMARY KEY,
286
+ name TEXT NOT NULL DEFAULT 'Untitled',
287
+ folder TEXT NOT NULL DEFAULT '',
288
+ method TEXT NOT NULL DEFAULT 'GET',
289
+ url TEXT NOT NULL DEFAULT '',
290
+ headers TEXT NOT NULL DEFAULT '[]',
291
+ params TEXT NOT NULL DEFAULT '[]',
292
+ body TEXT NOT NULL DEFAULT '',
293
+ body_type TEXT NOT NULL DEFAULT 'none',
294
+ raw_type TEXT NOT NULL DEFAULT 'text/plain',
295
+ form_rows TEXT NOT NULL DEFAULT '[]',
296
+ auth TEXT NOT NULL DEFAULT '{}',
297
+ notes TEXT NOT NULL DEFAULT '',
298
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
299
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
300
+ );
301
+ CREATE INDEX IF NOT EXISTS idx_saved_folder ON saved_requests(folder, created_at DESC);
302
+
303
+ CREATE TABLE IF NOT EXISTS spec_history (
304
+ id TEXT PRIMARY KEY,
305
+ url TEXT NOT NULL UNIQUE,
306
+ title TEXT,
307
+ version TEXT,
308
+ endpoint_count INTEGER,
309
+ last_used INTEGER NOT NULL DEFAULT (unixepoch()),
310
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
311
+ );
312
+ CREATE INDEX IF NOT EXISTS idx_spec_history_last_used ON spec_history(last_used DESC);
313
+
314
+ CREATE TABLE IF NOT EXISTS workflows (
315
+ id TEXT PRIMARY KEY,
316
+ name TEXT NOT NULL DEFAULT 'Untitled Workflow',
317
+ description TEXT NOT NULL DEFAULT '',
318
+ steps TEXT NOT NULL DEFAULT '[]',
319
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
320
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
321
+ );
322
+ CREATE INDEX IF NOT EXISTS idx_workflows_updated ON workflows(updated_at DESC);
323
+
324
+ CREATE TABLE IF NOT EXISTS capture_bins (
325
+ id TEXT PRIMARY KEY,
326
+ name TEXT NOT NULL DEFAULT '',
327
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
328
+ );
329
+
330
+ CREATE TABLE IF NOT EXISTS chat_memory (
331
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
332
+ role TEXT NOT NULL,
333
+ content TEXT NOT NULL,
334
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
335
+ );
336
+ CREATE INDEX IF NOT EXISTS idx_memory_created ON chat_memory(created_at DESC);
337
+ `;
338
+
339
+ // src/db/index.ts
340
+ import { Database } from "bun:sqlite";
341
+ import { join as join2 } from "path";
342
+ import { mkdirSync, existsSync } from "fs";
343
+ import { homedir as homedir2 } from "os";
344
+ import { randomUUID } from "crypto";
345
+ function resolveDataDir() {
346
+ if (process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR) {
347
+ return process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR;
254
348
  }
255
- lines.push("", ` ${hint}`, "");
256
- console.log(lines.join(`
257
- `));
258
- }
259
- function fmtUptime(ms) {
260
- const s = Math.floor(ms / 1000);
261
- const m = Math.floor(s / 60);
262
- const h = Math.floor(m / 60);
263
- if (h > 0)
264
- return `${h}h ${m % 60}m`;
265
- if (m > 0)
266
- return `${m}m ${s % 60}s`;
267
- return `${s}s`;
349
+ try {
350
+ const legacy = join2(import.meta.dir, "../../data");
351
+ if (!Bun.main.includes("$bunfs") && (existsSync(join2(legacy, "wasper.db")) || existsSync(join2(legacy, "openapi-agent.db"))))
352
+ return legacy;
353
+ } catch {}
354
+ const oldDir = join2(homedir2(), ".openapi-agent", "data");
355
+ if (existsSync(oldDir))
356
+ return oldDir;
357
+ return join2(homedir2(), ".wasper", "data");
268
358
  }
269
- function printStatus(opts) {
270
- const { running, pid, port, uptime, specTitle, specVersion, endpointCount, origin } = opts;
271
- if (!running) {
272
- console.log(`
273
- ${paint.dim("\u25CB")} ${paint.bold("OpenAPI Agent")} ${paint.dim("\xB7")} ${paint.yellow("not running")}
274
- `);
275
- return;
359
+ var DATA_DIR, DB_PATH, db, hasOldSchema, dbQueries;
360
+ var init_db = __esm(() => {
361
+ DATA_DIR = resolveDataDir();
362
+ mkdirSync(DATA_DIR, { recursive: true });
363
+ DB_PATH = join2(DATA_DIR, existsSync(join2(DATA_DIR, "openapi-agent.db")) && !existsSync(join2(DATA_DIR, "wasper.db")) ? "openapi-agent.db" : "wasper.db");
364
+ db = new Database(DB_PATH, { create: true });
365
+ db.exec("PRAGMA journal_mode = WAL;");
366
+ db.exec("PRAGMA foreign_keys = OFF;");
367
+ hasOldSchema = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='specs'").get() !== null;
368
+ if (hasOldSchema) {
369
+ db.exec("DROP TABLE IF EXISTS tools; DROP TABLE IF EXISTS specs; DROP TABLE IF EXISTS auth_configs; DROP TABLE IF EXISTS request_logs;");
276
370
  }
277
- const rows = [
278
- ["pid ", String(pid)],
279
- ["port ", String(port)],
280
- ["uptime ", uptime != null ? fmtUptime(uptime) : "\u2014"],
281
- ["spec ", specTitle ? `${specTitle} ${paint.dim("v" + (specVersion ?? ""))}` : "\u2014"],
282
- ["endpoints", String(endpointCount ?? "\u2014")]
283
- ];
284
- const maxKey = Math.max(...rows.map(([k]) => k.length));
285
- console.log(`
286
- ${paint.green("\u25CF")} ${paint.bold("OpenAPI Agent")} ${paint.dim("\xB7")} ${paint.green("running")}`);
287
- console.log();
288
- for (const [k, v] of rows) {
289
- console.log(` ${paint.dim(k.padEnd(maxKey))} ${v}`);
290
- }
291
- if (port) {
292
- const base = origin ?? `http://localhost:${port}`;
293
- console.log();
294
- console.log(` ${paint.url(`${base}/`)} ${paint.dim("\xB7")} ${paint.url(`${base}/mcp`)}`);
295
- }
296
- console.log();
297
- }
298
- var isTTY, esc = (s) => isTTY ? s : "", clr, paint, FRAMES;
299
- var init_ui = __esm(() => {
300
- isTTY = process.stdout.isTTY ?? false;
301
- clr = {
302
- reset: esc("\x1B[0m"),
303
- bold: esc("\x1B[1m"),
304
- dim: esc("\x1B[2m"),
305
- green: esc("\x1B[32m"),
306
- cyan: esc("\x1B[36m"),
307
- yellow: esc("\x1B[33m"),
308
- red: esc("\x1B[31m"),
309
- gray: esc("\x1B[90m")
310
- };
311
- paint = {
312
- green: (s) => `${clr.green}${s}${clr.reset}`,
313
- cyan: (s) => `${clr.cyan}${s}${clr.reset}`,
314
- yellow: (s) => `${clr.yellow}${s}${clr.reset}`,
315
- red: (s) => `${clr.red}${s}${clr.reset}`,
316
- gray: (s) => `${clr.gray}${s}${clr.reset}`,
317
- dim: (s) => `${clr.dim}${s}${clr.reset}`,
318
- bold: (s) => `${clr.bold}${s}${clr.reset}`,
319
- url: (s) => `${clr.cyan}${s}${clr.reset}`
371
+ db.exec(SCHEMA);
372
+ dbQueries = {
373
+ getAuthConfig: () => db.query("SELECT * FROM auth_config WHERE id = 'default'").get(),
374
+ setAuthConfig: (type, config) => db.query(`INSERT INTO auth_config (id, type, config, updated_at)
375
+ VALUES ('default', ?, ?, unixepoch())
376
+ ON CONFLICT(id) DO UPDATE SET type = excluded.type, config = excluded.config, updated_at = unixepoch()`).run(type, JSON.stringify(config)),
377
+ updateTokenCache: (tokenCache) => db.query("UPDATE auth_config SET token_cache = ? WHERE id = 'default'").run(tokenCache ? JSON.stringify(tokenCache) : null),
378
+ getRecentLogs: (limit = 500) => db.query("SELECT * FROM request_logs ORDER BY created_at DESC LIMIT ?").all(limit),
379
+ insertLog: (data) => db.query(`INSERT INTO request_logs
380
+ (id, source, tool_name, method, url, request_headers, request_body,
381
+ status_code, response_headers, response_body, latency_ms, error)
382
+ VALUES ($id, $source, $tool_name, $method, $url, $request_headers, $request_body,
383
+ $status_code, $response_headers, $response_body, $latency_ms, $error)`).run({
384
+ $id: data.id,
385
+ $source: data.source,
386
+ $tool_name: data.tool_name,
387
+ $method: data.method,
388
+ $url: data.url,
389
+ $request_headers: data.request_headers,
390
+ $request_body: data.request_body,
391
+ $status_code: data.status_code,
392
+ $response_headers: data.response_headers,
393
+ $response_body: data.response_body,
394
+ $latency_ms: data.latency_ms,
395
+ $error: data.error
396
+ }),
397
+ clearLogs: () => db.query("DELETE FROM request_logs").run(),
398
+ getRules: () => db.query("SELECT * FROM intercept_rules ORDER BY sort_order, created_at").all(),
399
+ insertRule: (rule) => db.query(`INSERT INTO intercept_rules (id,enabled,name,sort_order,match_path,match_method,target_host,strip_prefix,add_prefix,add_headers)
400
+ VALUES ($id,$enabled,$name,$sort_order,$match_path,$match_method,$target_host,$strip_prefix,$add_prefix,$add_headers)`).run({
401
+ $id: rule.id,
402
+ $enabled: rule.enabled,
403
+ $name: rule.name,
404
+ $sort_order: rule.sort_order,
405
+ $match_path: rule.match_path,
406
+ $match_method: rule.match_method,
407
+ $target_host: rule.target_host,
408
+ $strip_prefix: rule.strip_prefix,
409
+ $add_prefix: rule.add_prefix,
410
+ $add_headers: rule.add_headers
411
+ }),
412
+ updateRule: (id, patch) => {
413
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
414
+ const params = { $id: id };
415
+ for (const [k, v] of Object.entries(patch))
416
+ params[`$${k}`] = v;
417
+ db.query(`UPDATE intercept_rules SET ${cols} WHERE id = $id`).run(params);
418
+ },
419
+ deleteRule: (id) => db.query("DELETE FROM intercept_rules WHERE id = ?").run(id),
420
+ getSettings: () => db.query("SELECT value FROM settings WHERE key='app' LIMIT 1").get() ?? null,
421
+ setSettings: (value) => {
422
+ db.run("INSERT INTO settings(key,value) VALUES('app',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [JSON.stringify(value)]);
423
+ },
424
+ getProfiles: () => db.query("SELECT * FROM auth_profiles ORDER BY name COLLATE NOCASE").all(),
425
+ getActiveProfile: () => db.query("SELECT * FROM auth_profiles WHERE is_active = 1 LIMIT 1").get(),
426
+ insertProfile: (p) => db.query(`INSERT INTO auth_profiles (id,name,description,type,config,token_cache,is_active)
427
+ VALUES ($id,$name,$description,$type,$config,$token_cache,$is_active)`).run({
428
+ $id: p.id,
429
+ $name: p.name,
430
+ $description: p.description,
431
+ $type: p.type,
432
+ $config: p.config,
433
+ $token_cache: p.token_cache,
434
+ $is_active: p.is_active
435
+ }),
436
+ updateProfile: (id, patch) => {
437
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
438
+ const params = { $id: id };
439
+ for (const [k, v] of Object.entries(patch))
440
+ params[`$${k}`] = v;
441
+ db.query(`UPDATE auth_profiles SET ${cols} WHERE id = $id`).run(params);
442
+ },
443
+ deleteProfile: (id) => db.query("DELETE FROM auth_profiles WHERE id = ?").run(id),
444
+ activateProfile: (id) => {
445
+ const profile = db.query("SELECT * FROM auth_profiles WHERE id = ?").get(id);
446
+ if (!profile)
447
+ return;
448
+ db.query("UPDATE auth_profiles SET is_active = 0").run();
449
+ db.query("UPDATE auth_profiles SET is_active = 1 WHERE id = ?").run(id);
450
+ db.query(`INSERT INTO auth_config (id, type, config, updated_at) VALUES ('default', $type, $config, unixepoch())
451
+ ON CONFLICT(id) DO UPDATE SET type=excluded.type, config=excluded.config, updated_at=unixepoch()`).run({ $type: profile.type, $config: profile.config });
452
+ },
453
+ getSavedRequests: () => db.query("SELECT * FROM saved_requests ORDER BY folder, name COLLATE NOCASE").all(),
454
+ getSavedRequest: (id) => db.query("SELECT * FROM saved_requests WHERE id = ?").get(id),
455
+ insertSavedRequest: (r) => db.query(`INSERT INTO saved_requests
456
+ (id, name, folder, method, url, headers, params, body, body_type, raw_type, form_rows, auth, notes)
457
+ VALUES ($id,$name,$folder,$method,$url,$headers,$params,$body,$body_type,$raw_type,$form_rows,$auth,$notes)`).run({
458
+ $id: r.id,
459
+ $name: r.name,
460
+ $folder: r.folder,
461
+ $method: r.method,
462
+ $url: r.url,
463
+ $headers: r.headers,
464
+ $params: r.params,
465
+ $body: r.body,
466
+ $body_type: r.body_type,
467
+ $raw_type: r.raw_type,
468
+ $form_rows: r.form_rows,
469
+ $auth: r.auth,
470
+ $notes: r.notes
471
+ }),
472
+ updateSavedRequest: (id, patch) => {
473
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
474
+ const params = { $id: id };
475
+ for (const [k, v] of Object.entries(patch))
476
+ params[`$${k}`] = v;
477
+ db.query(`UPDATE saved_requests SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
478
+ },
479
+ deleteSavedRequest: (id) => db.query("DELETE FROM saved_requests WHERE id = ?").run(id),
480
+ getSpecHistory: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC").all(),
481
+ getLastSpec: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC LIMIT 1").get(),
482
+ upsertSpec: (url, title, version, endpointCount) => {
483
+ const existing = db.query("SELECT id FROM spec_history WHERE url = ?").get(url);
484
+ if (existing) {
485
+ db.query("UPDATE spec_history SET title=?, version=?, endpoint_count=?, last_used=unixepoch() WHERE url=?").run(title, version, endpointCount, url);
486
+ } else {
487
+ db.query(`INSERT INTO spec_history (id, url, title, version, endpoint_count)
488
+ VALUES (?, ?, ?, ?, ?)`).run(randomUUID(), url, title, version, endpointCount);
489
+ }
490
+ },
491
+ deleteSpec: (id) => {
492
+ db.query("DELETE FROM spec_history WHERE id = ?").run(id);
493
+ },
494
+ getWorkflows: () => db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all(),
495
+ getWorkflow: (id) => db.query("SELECT * FROM workflows WHERE id = ?").get(id),
496
+ 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 }),
497
+ updateWorkflow: (id, patch) => {
498
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
499
+ const params = { $id: id };
500
+ for (const [k, v] of Object.entries(patch))
501
+ params[`$${k}`] = v;
502
+ db.query(`UPDATE workflows SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
503
+ },
504
+ deleteWorkflow: (id) => db.query("DELETE FROM workflows WHERE id = ?").run(id),
505
+ getCaptureBins: () => db.query("SELECT * FROM capture_bins ORDER BY created_at DESC").all(),
506
+ getCaptureBin: (id) => db.query("SELECT * FROM capture_bins WHERE id = ?").get(id),
507
+ insertCaptureBin: (id, name) => {
508
+ db.query("INSERT INTO capture_bins (id, name) VALUES (?, ?)").run(id, name);
509
+ },
510
+ deleteCaptureBin: (id) => {
511
+ db.query("DELETE FROM capture_bins WHERE id = ?").run(id);
512
+ },
513
+ getSetting: (key) => (db.query("SELECT value FROM settings WHERE key = ?").get(key) ?? null)?.value ?? null,
514
+ setSetting: (key, value) => {
515
+ db.run("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
516
+ },
517
+ saveMemory: (role, content) => {
518
+ db.query("INSERT INTO chat_memory (role, content) VALUES (?, ?)").run(role, content);
519
+ },
520
+ getMemory: (limit = 20) => {
521
+ const rows = db.query("SELECT role, content FROM chat_memory ORDER BY created_at DESC LIMIT ?").all(limit);
522
+ return rows.reverse();
523
+ },
524
+ clearMemory: () => {
525
+ db.query("DELETE FROM chat_memory").run();
526
+ },
527
+ trimMemory: (keepLast = 40) => {
528
+ db.query("DELETE FROM chat_memory WHERE id NOT IN (SELECT id FROM chat_memory ORDER BY created_at DESC LIMIT ?)").run(keepLast);
529
+ }
320
530
  };
321
- FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
322
531
  });
323
532
 
324
- // src/commands/stop.ts
325
- var exports_stop = {};
326
- __export(exports_stop, {
327
- run: () => run
328
- });
329
- async function run() {
330
- const state = await readDaemonState();
331
- if (!state) {
332
- console.log(`
333
- ${paint.dim("\u25CB")} ${paint.bold("OpenAPI Agent")} \xB7 ${paint.yellow("No running instance found")}
334
- `);
335
- process.exit(1);
336
- }
337
- if (!isProcessAlive(state.pid)) {
338
- await clearDaemonState();
339
- console.log(`
340
- ${paint.dim("\u25CB")} Process ${state.pid} is no longer running. Cleaned up state.
341
- `);
342
- process.exit(0);
343
- }
533
+ // src/config.ts
534
+ function setServerConfig(c) {
535
+ config = c;
536
+ }
537
+ function getServerConfig() {
538
+ return config;
539
+ }
540
+ function updateServerConfig(patch) {
541
+ config = { ...config, ...patch };
542
+ }
543
+ function getFeatures() {
544
+ return features;
545
+ }
546
+ function setFeatures(patch) {
547
+ features = { ...features, ...patch };
548
+ }
549
+ function readonlyViolation(method) {
550
+ if (!features.readonly)
551
+ return null;
552
+ if (SAFE_METHODS.has(method.toUpperCase()))
553
+ return null;
554
+ return `Read-only mode is enabled \u2014 ${method.toUpperCase()} requests are blocked. Ask the operator to run /readonly off.`;
555
+ }
556
+ function isAuthorized(req) {
557
+ if (!config.token)
558
+ return true;
559
+ const auth = req.headers.get("authorization");
560
+ if (auth === `Bearer ${config.token}`)
561
+ return true;
344
562
  try {
345
- process.kill(state.pid, "SIGTERM");
563
+ return new URL(req.url).searchParams.get("token") === config.token;
346
564
  } catch {
347
- console.log(`
348
- ${paint.red("\u2717")} Failed to stop process ${state.pid}
349
- `);
350
- process.exit(1);
351
- }
352
- for (let i = 0;i < 30; i++) {
353
- await Bun.sleep(100);
354
- if (!isProcessAlive(state.pid))
355
- break;
565
+ return false;
356
566
  }
357
- await clearDaemonState();
358
- console.log(`
359
- ${paint.green("\u2713")} ${paint.bold("Stopped OpenAPI Agent")} ${paint.dim(`(was PID ${state.pid})`)}
360
- `);
361
567
  }
362
- var init_stop = __esm(() => {
363
- init_daemon();
364
- init_ui();
568
+ var config, features, SAFE_METHODS;
569
+ var init_config = __esm(() => {
570
+ config = {
571
+ port: 3388,
572
+ host: "0.0.0.0",
573
+ origin: null,
574
+ token: null
575
+ };
576
+ features = { mcp: true, proxy: true, ai: true, readonly: false };
577
+ SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
365
578
  });
366
579
 
367
- // src/commands/update.ts
368
- var exports_update = {};
369
- __export(exports_update, {
370
- run: () => run2,
371
- printUpdateNotice: () => printUpdateNotice,
372
- performUpdate: () => performUpdate,
373
- fetchLatestVersion: () => fetchLatestVersion,
374
- checkForUpdate: () => checkForUpdate
375
- });
376
- import { join as join2 } from "path";
377
- import { homedir as homedir2 } from "os";
378
- import { chmod, rename, unlink as unlink2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
379
- async function fetchLatestVersion() {
380
- try {
381
- const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
382
- signal: AbortSignal.timeout(5000),
383
- headers: { Accept: "application/json" }
384
- });
385
- if (!res.ok)
386
- return null;
387
- const data = await res.json();
388
- return data.version ?? null;
389
- } catch {
390
- return null;
391
- }
392
- }
393
- async function checkForUpdate() {
394
- if (process.env.WASPER_NO_UPDATE_CHECK)
395
- return null;
396
- let state = null;
397
- try {
398
- state = JSON.parse(await readFile2(CHECK_FILE, "utf-8"));
399
- } catch {}
400
- let latest = state?.latest ?? null;
401
- if (!state || Date.now() - state.lastCheck > CHECK_INTERVAL) {
402
- latest = await fetchLatestVersion();
403
- if (latest) {
404
- await mkdir2(join2(homedir2(), ".wasper"), { recursive: true }).catch(() => {});
405
- await writeFile2(CHECK_FILE, JSON.stringify({ lastCheck: Date.now(), latest }), "utf-8").catch(() => {});
580
+ // src/ui.ts
581
+ class Spinner {
582
+ i = 0;
583
+ timer = null;
584
+ msg = "";
585
+ start(msg) {
586
+ this.msg = msg;
587
+ if (!isTTY) {
588
+ process.stdout.write(` ${msg}
589
+ `);
590
+ return;
406
591
  }
592
+ this.i = 0;
593
+ this.timer = setInterval(() => {
594
+ const f = FRAMES[this.i++ % FRAMES.length];
595
+ process.stdout.write(`\r ${paint.cyan(f)} ${this.msg}\x1B[K`);
596
+ }, 80);
407
597
  }
408
- if (latest && compareSemver(latest, VERSION) > 0)
409
- return latest;
410
- return null;
411
- }
412
- function printUpdateNotice(latest) {
413
- console.log(` ${paint.yellow("\u25B2")} Update available ${paint.dim(VERSION)} \u2192 ${paint.green(latest)} ${paint.dim("\xB7")} run ${paint.bold("wasper update")}
414
- `);
415
- }
416
- function binaryAssetName() {
417
- const os = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux";
418
- const arch = process.arch === "arm64" ? "arm64" : "x64";
419
- return `wasper-${os}-${arch}${os === "windows" ? ".exe" : ""}`;
420
- }
421
- async function updateCompiledBinary(latest) {
422
- const exe = process.execPath;
423
- const asset = binaryAssetName();
424
- const url = `https://github.com/${REPO}/releases/download/v${latest}/${asset}`;
425
- const res = await fetch(url, { redirect: "follow" });
426
- if (!res.ok)
427
- throw new Error(`Download failed (HTTP ${res.status}) \u2014 ${url}`);
428
- const bytes = new Uint8Array(await res.arrayBuffer());
429
- if (bytes.byteLength < 1024 * 100)
430
- throw new Error("Downloaded file is suspiciously small \u2014 aborting");
431
- const tmp = `${exe}.update`;
432
- const old = `${exe}.old`;
433
- await Bun.write(tmp, bytes);
434
- await chmod(tmp, 493);
435
- await rename(exe, old);
436
- try {
437
- await rename(tmp, exe);
438
- } catch (e) {
439
- await rename(old, exe).catch(() => {});
440
- throw e;
598
+ update(msg) {
599
+ this.msg = msg;
441
600
  }
442
- await unlink2(old).catch(() => {});
443
- }
444
- async function updatePackageInstall() {
445
- const tryRun = async (cmd) => {
446
- try {
447
- const p = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
448
- const code = await p.exited;
449
- return code === 0;
450
- } catch {
451
- return false;
601
+ stop(icon = "\u2713", msg = "", color = "green") {
602
+ if (this.timer) {
603
+ clearInterval(this.timer);
604
+ this.timer = null;
452
605
  }
453
- };
454
- if (await tryRun(["bun", "add", "-g", `${PACKAGE_NAME}@latest`]))
455
- return;
456
- if (await tryRun(["npm", "install", "-g", `${PACKAGE_NAME}@latest`]))
457
- return;
458
- throw new Error("Neither `bun add -g` nor `npm install -g` succeeded. Update manually.");
606
+ if (!isTTY) {
607
+ if (msg)
608
+ process.stdout.write(` ${icon} ${msg}
609
+ `);
610
+ return;
611
+ }
612
+ process.stdout.write(msg ? `\r ${paint[color](icon)} ${msg}\x1B[K
613
+ ` : `\r\x1B[K`);
614
+ }
459
615
  }
460
- async function performUpdate(opts = {}) {
461
- const spinner = new Spinner;
462
- if (!opts.quiet)
463
- spinner.start("Checking for updates\u2026");
464
- const latest = await fetchLatestVersion();
465
- if (!latest) {
466
- spinner.stop("\u2717", "Could not reach the npm registry", "red");
467
- return false;
616
+ function printBanner(opts) {
617
+ const { port, pid, specTitle, specVersion, endpointCount, origin, host, tokenSet } = opts;
618
+ const base = origin ?? `http://localhost:${port}`;
619
+ const arrow = paint.cyan("\u279C");
620
+ const dot = paint.dim("\xB7");
621
+ const hint = [
622
+ `${paint.bold("r")} reload`,
623
+ `${paint.bold("b")} background`,
624
+ `${paint.bold("/")} commands ${paint.dim("(Tab to complete)")}`,
625
+ `${paint.bold("q")} quit`,
626
+ `${paint.bold("?")} help`
627
+ ].join(` ${dot} `);
628
+ const lines = [
629
+ "",
630
+ ` ${paint.bold("wasper")} ${paint.dim("PID " + pid)}`,
631
+ "",
632
+ ` ${arrow} ${paint.dim("Studio ")} ${paint.url(base + "/")}`,
633
+ ` ${arrow} ${paint.dim("MCP ")} ${paint.url(base + "/mcp")}`,
634
+ ` ${arrow} ${paint.dim("OpenAPI")} ${paint.url(base + "/openapi.json")}`,
635
+ ""
636
+ ];
637
+ if (origin) {
638
+ lines.push(` ${arrow} ${paint.dim("Local ")} ${paint.url(`http://localhost:${port}/`)}${host && host !== "0.0.0.0" ? ` ${dot} ${paint.dim("bound to " + host)}` : ""}`, "");
468
639
  }
469
- if (compareSemver(latest, VERSION) <= 0) {
470
- spinner.stop("\u2713", `Already up to date ${paint.dim("v" + VERSION)}`, "green");
471
- return false;
640
+ if (specTitle) {
641
+ const ep = endpointCount != null ? ` ${dot} ${paint.green(endpointCount + " endpoints")}` : "";
642
+ lines.push(` ${paint.green("\u2713")} ${paint.bold(specTitle)} ${paint.dim("v" + (specVersion ?? ""))}${ep}`);
643
+ } else {
644
+ lines.push(` ${paint.yellow("\u25CB")} ${paint.dim("No spec \u2014 start with --url <url>")}`);
472
645
  }
473
- spinner.stop();
474
- if (!opts.quiet)
475
- spinner.start(`Updating ${paint.dim(VERSION)} \u2192 ${paint.green(latest)}\u2026`);
476
- try {
477
- if (isCompiledBinary())
478
- await updateCompiledBinary(latest);
479
- else
480
- await updatePackageInstall();
481
- spinner.stop("\u2713", `Updated to ${paint.bold("v" + latest)} ${paint.dim("\u2014 restart any running servers to use it")}`, "green");
482
- await writeFile2(CHECK_FILE, JSON.stringify({ lastCheck: Date.now(), latest }), "utf-8").catch(() => {});
483
- return true;
484
- } catch (e) {
485
- spinner.stop("\u2717", `Update failed: ${e instanceof Error ? e.message : String(e)}`, "red");
486
- return false;
646
+ if (tokenSet) {
647
+ lines.push(` ${paint.green("\u2713")} ${paint.dim("Access token required (Authorization: Bearer \u2026 or ?token=)")}`);
648
+ } else if (origin) {
649
+ lines.push(` ${paint.yellow("!")} ${paint.yellow("Publicly reachable without a token \u2014 consider --token <secret>")}`);
487
650
  }
651
+ lines.push("", ` ${hint}`, "");
652
+ console.log(lines.join(`
653
+ `));
488
654
  }
489
- async function run2() {
655
+ function fmtUptime(ms) {
656
+ const s = Math.floor(ms / 1000);
657
+ const m = Math.floor(s / 60);
658
+ const h = Math.floor(m / 60);
659
+ if (h > 0)
660
+ return `${h}h ${m % 60}m`;
661
+ if (m > 0)
662
+ return `${m}m ${s % 60}s`;
663
+ return `${s}s`;
664
+ }
665
+ function printStatus(opts) {
666
+ const { running, pid, port, uptime, specTitle, specVersion, endpointCount, origin } = opts;
667
+ if (!running) {
668
+ console.log(`
669
+ ${paint.dim("\u25CB")} ${paint.bold("OpenAPI Agent")} ${paint.dim("\xB7")} ${paint.yellow("not running")}
670
+ `);
671
+ return;
672
+ }
673
+ const rows = [
674
+ ["pid ", String(pid)],
675
+ ["port ", String(port)],
676
+ ["uptime ", uptime != null ? fmtUptime(uptime) : "\u2014"],
677
+ ["spec ", specTitle ? `${specTitle} ${paint.dim("v" + (specVersion ?? ""))}` : "\u2014"],
678
+ ["endpoints", String(endpointCount ?? "\u2014")]
679
+ ];
680
+ const maxKey = Math.max(...rows.map(([k]) => k.length));
681
+ console.log(`
682
+ ${paint.green("\u25CF")} ${paint.bold("OpenAPI Agent")} ${paint.dim("\xB7")} ${paint.green("running")}`);
490
683
  console.log();
491
- await performUpdate();
684
+ for (const [k, v] of rows) {
685
+ console.log(` ${paint.dim(k.padEnd(maxKey))} ${v}`);
686
+ }
687
+ if (port) {
688
+ const base = origin ?? `http://localhost:${port}`;
689
+ console.log();
690
+ console.log(` ${paint.url(`${base}/`)} ${paint.dim("\xB7")} ${paint.url(`${base}/mcp`)}`);
691
+ }
492
692
  console.log();
493
693
  }
494
- var CHECK_FILE, CHECK_INTERVAL;
495
- var init_update = __esm(() => {
496
- init_version();
497
- init_ui();
498
- CHECK_FILE = join2(homedir2(), ".wasper", "update-check.json");
499
- CHECK_INTERVAL = 24 * 60 * 60 * 1000;
694
+ var isTTY, esc = (s) => isTTY ? s : "", clr, paint, FRAMES;
695
+ var init_ui = __esm(() => {
696
+ isTTY = process.stdout.isTTY ?? false;
697
+ clr = {
698
+ reset: esc("\x1B[0m"),
699
+ bold: esc("\x1B[1m"),
700
+ dim: esc("\x1B[2m"),
701
+ green: esc("\x1B[32m"),
702
+ cyan: esc("\x1B[36m"),
703
+ yellow: esc("\x1B[33m"),
704
+ red: esc("\x1B[31m"),
705
+ gray: esc("\x1B[90m")
706
+ };
707
+ paint = {
708
+ green: (s) => `${clr.green}${s}${clr.reset}`,
709
+ cyan: (s) => `${clr.cyan}${s}${clr.reset}`,
710
+ yellow: (s) => `${clr.yellow}${s}${clr.reset}`,
711
+ red: (s) => `${clr.red}${s}${clr.reset}`,
712
+ gray: (s) => `${clr.gray}${s}${clr.reset}`,
713
+ dim: (s) => `${clr.dim}${s}${clr.reset}`,
714
+ bold: (s) => `${clr.bold}${s}${clr.reset}`,
715
+ url: (s) => `${clr.cyan}${s}${clr.reset}`
716
+ };
717
+ FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
500
718
  });
501
719
 
502
- // src/commands/status.ts
503
- var exports_status = {};
504
- __export(exports_status, {
505
- run: () => run3
720
+ // src/commands/up.ts
721
+ var exports_up = {};
722
+ __export(exports_up, {
723
+ run: () => run
506
724
  });
507
- async function run3() {
508
- const state = await readDaemonState();
509
- if (!state || !isProcessAlive(state.pid)) {
725
+ import { parseArgs } from "util";
726
+ async function run() {
727
+ const { values } = parseArgs({
728
+ args: process.argv.slice(2).filter((a) => a !== "up"),
729
+ options: {
730
+ url: { type: "string" },
731
+ port: { type: "string", default: process.env.WASPER_PORT ?? "3388" },
732
+ host: { type: "string" },
733
+ origin: { type: "string" },
734
+ token: { type: "string" },
735
+ "no-mcp": { type: "boolean" },
736
+ "no-proxy": { type: "boolean" },
737
+ "no-ai": { type: "boolean" },
738
+ readonly: { type: "boolean" },
739
+ force: { type: "boolean", short: "f" },
740
+ help: { type: "boolean", short: "h" }
741
+ },
742
+ strict: false
743
+ });
744
+ if (values.help) {
745
+ printHelp();
746
+ process.exit(0);
747
+ }
748
+ let specUrl = values.url ? String(values.url) : null;
749
+ specUrl ??= process.env.WASPER_SPEC_URL ?? null;
750
+ const PORT = parseInt(String(values.port ?? "3388"), 10);
751
+ const HOST = (values.host ? String(values.host) : null) ?? process.env.WASPER_HOST ?? "0.0.0.0";
752
+ const ORIGIN = ((values.origin ? String(values.origin) : null) ?? process.env.WASPER_ORIGIN ?? null)?.replace(/\/$/, "") ?? null;
753
+ const TOKEN = (values.token ? String(values.token) : null) ?? process.env.WASPER_TOKEN ?? null;
754
+ if (!specUrl) {
755
+ const last = dbQueries.getLastSpec();
756
+ if (last)
757
+ specUrl = last.url;
758
+ }
759
+ const existing = await readDaemonState(PORT);
760
+ if (existing && isProcessAlive(existing.pid) && !values.force) {
761
+ const base2 = existing.origin ?? `http://localhost:${existing.port}`;
762
+ console.log(`
763
+ ${paint.yellow("\u25CF")} Already running on :${PORT} ${paint.dim(`PID ${existing.pid}`)}`);
764
+ console.log(` ${paint.dim("\u279C")} ${paint.cyan(base2 + "/")}`);
765
+ console.log(`
766
+ ${paint.dim("wasper ps \xB7 wasper down --port " + PORT + " \xB7 wasper up --port " + PORT + " --force")}
767
+ `);
768
+ process.exit(0);
769
+ }
770
+ setServerConfig({ port: PORT, host: HOST, origin: ORIGIN, token: TOKEN });
771
+ setFeatures({
772
+ ...values["no-mcp"] ? { mcp: false } : {},
773
+ ...values["no-proxy"] ? { proxy: false } : {},
774
+ ...values["no-ai"] ? { ai: false } : {},
775
+ readonly: !!values.readonly
776
+ });
777
+ if (existing && isProcessAlive(existing.pid)) {
778
+ try {
779
+ process.kill(existing.pid, "SIGTERM");
780
+ } catch {}
781
+ await Bun.sleep(400);
782
+ }
783
+ const pid = await spawnDaemon(specUrl, PORT, {
784
+ host: HOST,
785
+ origin: ORIGIN,
786
+ token: TOKEN,
787
+ features: getFeatures()
788
+ });
789
+ await Bun.sleep(600);
790
+ await writeDaemonState({ pid, port: PORT, specUrl, startedAt: Date.now(), host: HOST, origin: ORIGIN, token: TOKEN });
791
+ const base = ORIGIN ?? `http://localhost:${PORT}`;
792
+ console.log(`
793
+ ${paint.green("\u2713")} Started on :${PORT} ${paint.dim(`PID ${pid}`)}`);
794
+ if (specUrl)
795
+ console.log(` ${paint.dim("\u21A9")} ${specUrl}`);
796
+ console.log(` ${paint.dim("\u279C")} ${paint.cyan(base + "/")}`);
797
+ console.log(` MCP ${paint.dim(base + "/mcp")}`);
798
+ console.log(`
799
+ ${paint.dim("wasper ps \xB7 wasper down \xB7 wasper logs -f" + (PORT !== 3388 ? " --port " + PORT : ""))}
800
+ `);
801
+ process.exit(0);
802
+ }
803
+ function printHelp() {
804
+ console.log(`
805
+ Usage: wasper up [options]
806
+
807
+ Start the wasper daemon in the background.
808
+ Run multiple instances with different --port values.
809
+
810
+ Options:
811
+ --url, -u OpenAPI spec URL or local path
812
+ --port Port (default: 3388, env WASPER_PORT)
813
+ --host Bind address (default: 0.0.0.0)
814
+ --origin Public URL (env WASPER_ORIGIN)
815
+ --token Access token (env WASPER_TOKEN)
816
+ --no-mcp Disable the MCP endpoint
817
+ --no-proxy Disable the HTTP proxy
818
+ --no-ai Disable the AI chat endpoint
819
+ --readonly Block non-GET upstream requests
820
+ --force, -f Restart if already running on that port
821
+
822
+ Examples:
823
+ wasper up --url https://api.example.com/openapi.json
824
+ wasper up --url https://api2.example.com/openapi.json --port 3389
825
+ wasper ps # list all running instances
826
+ `);
827
+ }
828
+ var init_up = __esm(() => {
829
+ init_daemon();
830
+ init_db();
831
+ init_config();
832
+ init_ui();
833
+ });
834
+
835
+ // src/commands/ps.ts
836
+ var exports_ps = {};
837
+ __export(exports_ps, {
838
+ run: () => run2
839
+ });
840
+ function uptime(startedAt) {
841
+ const secs = Math.floor((Date.now() - startedAt) / 1000);
842
+ if (secs < 60)
843
+ return `${secs}s`;
844
+ if (secs < 3600)
845
+ return `${Math.floor(secs / 60)}m`;
846
+ if (secs < 86400)
847
+ return `${Math.floor(secs / 3600)}h`;
848
+ return `${Math.floor(secs / 86400)}d`;
849
+ }
850
+ async function run2() {
851
+ const all = await readAllDaemonStates();
852
+ if (all.length === 0) {
853
+ console.log(`
854
+ ${paint.dim("\u25CB")} No running instances
855
+ ${paint.dim("wasper up --url <spec> [--port <port>]")}
856
+ `);
857
+ process.exit(0);
858
+ }
859
+ console.log();
860
+ const header = ` ${"PORT".padEnd(7)} ${"PID".padEnd(8)} ${"UP".padEnd(6)} ${"SPEC / URL"}`;
861
+ console.log(paint.dim(header));
862
+ console.log(paint.dim(" " + "\u2500".repeat(header.length - 2)));
863
+ for (const s of all) {
864
+ const port = paint.green(String(s.port).padEnd(7));
865
+ const pid = paint.dim(String(s.pid).padEnd(8));
866
+ const up = paint.dim(uptime(s.startedAt).padEnd(6));
867
+ const spec = s.specUrl ? s.specUrl : paint.dim("no spec");
868
+ console.log(` ${port} ${pid} ${up} ${spec}`);
869
+ }
870
+ console.log();
871
+ console.log(paint.dim(` wasper status --port <port> \u2014 details`));
872
+ console.log(paint.dim(` wasper down --port <port> \u2014 stop one`));
873
+ console.log(paint.dim(` wasper down --all \u2014 stop all`));
874
+ console.log();
875
+ process.exit(0);
876
+ }
877
+ var init_ps = __esm(() => {
878
+ init_daemon();
879
+ init_ui();
880
+ });
881
+
882
+ // src/commands/stop.ts
883
+ var exports_stop = {};
884
+ __export(exports_stop, {
885
+ run: () => run3
886
+ });
887
+ async function run3() {
888
+ const args = process.argv.slice(2).filter((a) => a !== "down" && a !== "stop");
889
+ const stopAll = args.includes("--all") || args.includes("-a");
890
+ const portIdx = args.findIndex((a) => a === "--port" || a === "-p");
891
+ const portArg = portIdx >= 0 ? parseInt(args[portIdx + 1] ?? "", 10) : undefined;
892
+ if (stopAll) {
893
+ const all = await readAllDaemonStates();
894
+ if (!all.length) {
895
+ console.log(`
896
+ ${paint.dim("\u25CB")} No running instances
897
+ `);
898
+ process.exit(0);
899
+ }
900
+ for (const state2 of all) {
901
+ try {
902
+ process.kill(state2.pid, "SIGTERM");
903
+ } catch {}
904
+ await Bun.sleep(300);
905
+ await clearDaemonState(state2.port);
906
+ const label = state2.specUrl ? paint.dim(state2.specUrl) : paint.dim("no spec");
907
+ console.log(` ${paint.green("\u2713")} Stopped :${state2.port} ${label}`);
908
+ }
909
+ console.log();
910
+ process.exit(0);
911
+ }
912
+ const state = await readDaemonState(portArg);
913
+ if (!state) {
914
+ if (portArg) {
915
+ console.log(`
916
+ ${paint.dim("\u25CB")} No instance running on :${portArg}
917
+ `);
918
+ } else {
919
+ const all = await readAllDaemonStates();
920
+ if (all.length > 1) {
921
+ console.log(`
922
+ ${paint.yellow("\u25CB")} Multiple instances running \u2014 specify a port or use --all:
923
+ `);
924
+ for (const s of all) {
925
+ console.log(` :${s.port} ${paint.dim(s.specUrl ?? "no spec")}`);
926
+ }
927
+ console.log(`
928
+ ${paint.dim("wasper down --port <port> \xB7 wasper down --all")}
929
+ `);
930
+ process.exit(1);
931
+ }
932
+ console.log(`
933
+ ${paint.dim("\u25CB")} No running instance found
934
+ `);
935
+ }
936
+ process.exit(1);
937
+ }
938
+ if (!isProcessAlive(state.pid)) {
939
+ await clearDaemonState(state.port);
940
+ console.log(`
941
+ ${paint.dim("\u25CB")} Process ${state.pid} already gone. Cleaned up state.
942
+ `);
943
+ process.exit(0);
944
+ }
945
+ try {
946
+ process.kill(state.pid, "SIGTERM");
947
+ } catch {
948
+ console.log(`
949
+ ${paint.red("\u2717")} Failed to stop PID ${state.pid}
950
+ `);
951
+ process.exit(1);
952
+ }
953
+ for (let i = 0;i < 30; i++) {
954
+ await Bun.sleep(100);
955
+ if (!isProcessAlive(state.pid))
956
+ break;
957
+ }
958
+ await clearDaemonState(state.port);
959
+ console.log(`
960
+ ${paint.green("\u2713")} Stopped :${state.port} ${paint.dim(`PID ${state.pid}`)}
961
+ `);
962
+ process.exit(0);
963
+ }
964
+ var init_stop = __esm(() => {
965
+ init_daemon();
966
+ init_ui();
967
+ });
968
+
969
+ // src/commands/status.ts
970
+ var exports_status = {};
971
+ __export(exports_status, {
972
+ run: () => run4
973
+ });
974
+ function uptime2(startedAt) {
975
+ const secs = Math.floor((Date.now() - startedAt) / 1000);
976
+ if (secs < 60)
977
+ return `${secs}s`;
978
+ if (secs < 3600)
979
+ return `${Math.floor(secs / 60)}m`;
980
+ if (secs < 86400)
981
+ return `${Math.floor(secs / 3600)}h`;
982
+ return `${Math.floor(secs / 86400)}d`;
983
+ }
984
+ async function run4() {
985
+ const args = process.argv.slice(2).filter((a) => a !== "status");
986
+ const portIdx = args.findIndex((a) => a === "--port" || a === "-p");
987
+ const portArg = portIdx >= 0 ? parseInt(args[portIdx + 1] ?? "", 10) : undefined;
988
+ if (portArg) {
989
+ const state = await readDaemonState(portArg);
990
+ if (!state || !isProcessAlive(state.pid)) {
991
+ console.log(`
992
+ ${paint.dim("\u25CB")} No instance on :${portArg}
993
+ `);
994
+ process.exit(1);
995
+ }
996
+ await printSingleStatus(state);
997
+ process.exit(0);
998
+ }
999
+ const all = await readAllDaemonStates();
1000
+ if (all.length === 0) {
510
1001
  printStatus({ running: false });
511
- process.exit(state ? 1 : 0);
1002
+ process.exit(0);
1003
+ }
1004
+ if (all.length === 1) {
1005
+ await printSingleStatus(all[0]);
1006
+ process.exit(0);
1007
+ }
1008
+ console.log(`
1009
+ ${paint.green("\u25CF")} ${paint.bold("wasper")} ${all.length} instances running
1010
+ `);
1011
+ for (const s of all) {
1012
+ const base = s.origin ?? `http://localhost:${s.port}`;
1013
+ let specInfo = "";
1014
+ try {
1015
+ const res = await fetch(`http://localhost:${s.port}/api/server-info`, {
1016
+ headers: s.token ? { Authorization: `Bearer ${s.token}` } : undefined,
1017
+ signal: AbortSignal.timeout(1500)
1018
+ });
1019
+ if (res.ok) {
1020
+ const info = await res.json();
1021
+ if (info.spec)
1022
+ specInfo = ` ${info.spec.title} ${paint.dim(info.spec.endpointCount + " ep")}`;
1023
+ }
1024
+ } catch {}
1025
+ console.log(` ${paint.green("\u25CF")} :${String(s.port).padEnd(5)} ${paint.cyan(base + "/")}${specInfo}`);
1026
+ console.log(` ${paint.dim(`PID ${s.pid} up ${uptime2(s.startedAt)}`)}`);
512
1027
  }
1028
+ console.log(`
1029
+ ${paint.dim("wasper status --port <port> \u2014 details for one instance")}
1030
+ `);
1031
+ process.exit(0);
1032
+ }
1033
+ async function printSingleStatus(state) {
513
1034
  let spec = null;
514
1035
  try {
515
1036
  const res = await fetch(`http://localhost:${state.port}/api/server-info`, {
@@ -541,9 +1062,9 @@ var init_status = __esm(() => {
541
1062
  // src/commands/reload.ts
542
1063
  var exports_reload = {};
543
1064
  __export(exports_reload, {
544
- run: () => run4
1065
+ run: () => run5
545
1066
  });
546
- async function run4() {
1067
+ async function run5() {
547
1068
  const state = await readDaemonState();
548
1069
  if (!state || !isProcessAlive(state.pid)) {
549
1070
  console.log(`
@@ -566,6 +1087,7 @@ async function run4() {
566
1087
  }
567
1088
  spinner.stop("\u2713", `Reloaded "${data.spec}" \xB7 ${paint.green(String(data.endpoints ?? 0) + " endpoints")}`, "green");
568
1089
  console.log();
1090
+ process.exit(0);
569
1091
  } catch (e) {
570
1092
  spinner.stop("\u2717", `Failed: ${e instanceof Error ? e.message : String(e)}`, "red");
571
1093
  process.exit(1);
@@ -576,318 +1098,62 @@ var init_reload = __esm(() => {
576
1098
  init_ui();
577
1099
  });
578
1100
 
579
- // src/db/schema.ts
580
- var SCHEMA = `
581
- CREATE TABLE IF NOT EXISTS auth_config (
582
- id TEXT PRIMARY KEY DEFAULT 'default',
583
- type TEXT NOT NULL DEFAULT 'none',
584
- config TEXT NOT NULL DEFAULT '{}',
585
- token_cache TEXT,
586
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
587
- );
588
-
589
- CREATE TABLE IF NOT EXISTS request_logs (
590
- id TEXT PRIMARY KEY,
591
- source TEXT NOT NULL DEFAULT 'mcp',
592
- tool_name TEXT,
593
- method TEXT NOT NULL,
594
- url TEXT NOT NULL,
595
- request_headers TEXT,
596
- request_body TEXT,
597
- status_code INTEGER,
598
- response_headers TEXT,
599
- response_body TEXT,
600
- latency_ms INTEGER,
601
- error TEXT,
602
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
603
- );
604
-
605
- CREATE INDEX IF NOT EXISTS idx_logs_created ON request_logs(created_at DESC);
606
-
607
- CREATE TABLE IF NOT EXISTS settings (
608
- key TEXT PRIMARY KEY,
609
- value TEXT NOT NULL DEFAULT '{}'
610
- );
611
-
612
- CREATE TABLE IF NOT EXISTS intercept_rules (
613
- id TEXT PRIMARY KEY,
614
- enabled INTEGER NOT NULL DEFAULT 1,
615
- name TEXT NOT NULL DEFAULT '',
616
- sort_order INTEGER NOT NULL DEFAULT 0,
617
- match_path TEXT NOT NULL DEFAULT '',
618
- match_method TEXT NOT NULL DEFAULT '',
619
- target_host TEXT NOT NULL DEFAULT '',
620
- strip_prefix TEXT NOT NULL DEFAULT '',
621
- add_prefix TEXT NOT NULL DEFAULT '',
622
- add_headers TEXT NOT NULL DEFAULT '{}',
623
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
624
- );
625
- CREATE INDEX IF NOT EXISTS idx_rules_order ON intercept_rules(sort_order, created_at);
626
-
627
- CREATE TABLE IF NOT EXISTS auth_profiles (
628
- id TEXT PRIMARY KEY,
629
- name TEXT NOT NULL,
630
- description TEXT NOT NULL DEFAULT '',
631
- type TEXT NOT NULL DEFAULT 'none',
632
- config TEXT NOT NULL DEFAULT '{}',
633
- token_cache TEXT,
634
- is_active INTEGER NOT NULL DEFAULT 0,
635
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
636
- );
637
-
638
- CREATE TABLE IF NOT EXISTS saved_requests (
639
- id TEXT PRIMARY KEY,
640
- name TEXT NOT NULL DEFAULT 'Untitled',
641
- folder TEXT NOT NULL DEFAULT '',
642
- method TEXT NOT NULL DEFAULT 'GET',
643
- url TEXT NOT NULL DEFAULT '',
644
- headers TEXT NOT NULL DEFAULT '[]',
645
- params TEXT NOT NULL DEFAULT '[]',
646
- body TEXT NOT NULL DEFAULT '',
647
- body_type TEXT NOT NULL DEFAULT 'none',
648
- raw_type TEXT NOT NULL DEFAULT 'text/plain',
649
- form_rows TEXT NOT NULL DEFAULT '[]',
650
- auth TEXT NOT NULL DEFAULT '{}',
651
- notes TEXT NOT NULL DEFAULT '',
652
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
653
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
654
- );
655
- CREATE INDEX IF NOT EXISTS idx_saved_folder ON saved_requests(folder, created_at DESC);
656
-
657
- CREATE TABLE IF NOT EXISTS spec_history (
658
- id TEXT PRIMARY KEY,
659
- url TEXT NOT NULL UNIQUE,
660
- title TEXT,
661
- version TEXT,
662
- endpoint_count INTEGER,
663
- last_used INTEGER NOT NULL DEFAULT (unixepoch()),
664
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
665
- );
666
- CREATE INDEX IF NOT EXISTS idx_spec_history_last_used ON spec_history(last_used DESC);
667
-
668
- CREATE TABLE IF NOT EXISTS workflows (
669
- id TEXT PRIMARY KEY,
670
- name TEXT NOT NULL DEFAULT 'Untitled Workflow',
671
- description TEXT NOT NULL DEFAULT '',
672
- steps TEXT NOT NULL DEFAULT '[]',
673
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
674
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
675
- );
676
- CREATE INDEX IF NOT EXISTS idx_workflows_updated ON workflows(updated_at DESC);
677
-
678
- CREATE TABLE IF NOT EXISTS capture_bins (
679
- id TEXT PRIMARY KEY,
680
- name TEXT NOT NULL DEFAULT '',
681
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
682
- );
683
-
684
- CREATE TABLE IF NOT EXISTS chat_memory (
685
- id INTEGER PRIMARY KEY AUTOINCREMENT,
686
- role TEXT NOT NULL,
687
- content TEXT NOT NULL,
688
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
689
- );
690
- CREATE INDEX IF NOT EXISTS idx_memory_created ON chat_memory(created_at DESC);
691
- `;
692
-
693
- // src/db/index.ts
694
- import { Database } from "bun:sqlite";
695
- import { join as join3 } from "path";
696
- import { mkdirSync, existsSync } from "fs";
697
- import { homedir as homedir3 } from "os";
698
- import { randomUUID } from "crypto";
699
- function resolveDataDir() {
700
- if (process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR) {
701
- return process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR;
702
- }
703
- try {
704
- const legacy = join3(import.meta.dir, "../../data");
705
- if (!Bun.main.includes("$bunfs") && (existsSync(join3(legacy, "wasper.db")) || existsSync(join3(legacy, "openapi-agent.db"))))
706
- return legacy;
707
- } catch {}
708
- const oldDir = join3(homedir3(), ".openapi-agent", "data");
709
- if (existsSync(oldDir))
710
- return oldDir;
711
- return join3(homedir3(), ".wasper", "data");
712
- }
713
- var DATA_DIR, DB_PATH, db, hasOldSchema, dbQueries;
714
- var init_db = __esm(() => {
715
- DATA_DIR = resolveDataDir();
716
- mkdirSync(DATA_DIR, { recursive: true });
717
- DB_PATH = join3(DATA_DIR, existsSync(join3(DATA_DIR, "openapi-agent.db")) && !existsSync(join3(DATA_DIR, "wasper.db")) ? "openapi-agent.db" : "wasper.db");
718
- db = new Database(DB_PATH, { create: true });
719
- db.exec("PRAGMA journal_mode = WAL;");
720
- db.exec("PRAGMA foreign_keys = OFF;");
721
- hasOldSchema = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='specs'").get() !== null;
722
- if (hasOldSchema) {
723
- db.exec("DROP TABLE IF EXISTS tools; DROP TABLE IF EXISTS specs; DROP TABLE IF EXISTS auth_configs; DROP TABLE IF EXISTS request_logs;");
724
- }
725
- db.exec(SCHEMA);
726
- dbQueries = {
727
- getAuthConfig: () => db.query("SELECT * FROM auth_config WHERE id = 'default'").get(),
728
- setAuthConfig: (type, config) => db.query(`INSERT INTO auth_config (id, type, config, updated_at)
729
- VALUES ('default', ?, ?, unixepoch())
730
- ON CONFLICT(id) DO UPDATE SET type = excluded.type, config = excluded.config, updated_at = unixepoch()`).run(type, JSON.stringify(config)),
731
- updateTokenCache: (tokenCache) => db.query("UPDATE auth_config SET token_cache = ? WHERE id = 'default'").run(tokenCache ? JSON.stringify(tokenCache) : null),
732
- getRecentLogs: (limit = 500) => db.query("SELECT * FROM request_logs ORDER BY created_at DESC LIMIT ?").all(limit),
733
- insertLog: (data) => db.query(`INSERT INTO request_logs
734
- (id, source, tool_name, method, url, request_headers, request_body,
735
- status_code, response_headers, response_body, latency_ms, error)
736
- VALUES ($id, $source, $tool_name, $method, $url, $request_headers, $request_body,
737
- $status_code, $response_headers, $response_body, $latency_ms, $error)`).run({
738
- $id: data.id,
739
- $source: data.source,
740
- $tool_name: data.tool_name,
741
- $method: data.method,
742
- $url: data.url,
743
- $request_headers: data.request_headers,
744
- $request_body: data.request_body,
745
- $status_code: data.status_code,
746
- $response_headers: data.response_headers,
747
- $response_body: data.response_body,
748
- $latency_ms: data.latency_ms,
749
- $error: data.error
750
- }),
751
- clearLogs: () => db.query("DELETE FROM request_logs").run(),
752
- getRules: () => db.query("SELECT * FROM intercept_rules ORDER BY sort_order, created_at").all(),
753
- insertRule: (rule) => db.query(`INSERT INTO intercept_rules (id,enabled,name,sort_order,match_path,match_method,target_host,strip_prefix,add_prefix,add_headers)
754
- VALUES ($id,$enabled,$name,$sort_order,$match_path,$match_method,$target_host,$strip_prefix,$add_prefix,$add_headers)`).run({
755
- $id: rule.id,
756
- $enabled: rule.enabled,
757
- $name: rule.name,
758
- $sort_order: rule.sort_order,
759
- $match_path: rule.match_path,
760
- $match_method: rule.match_method,
761
- $target_host: rule.target_host,
762
- $strip_prefix: rule.strip_prefix,
763
- $add_prefix: rule.add_prefix,
764
- $add_headers: rule.add_headers
765
- }),
766
- updateRule: (id, patch) => {
767
- const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
768
- const params = { $id: id };
769
- for (const [k, v] of Object.entries(patch))
770
- params[`$${k}`] = v;
771
- db.query(`UPDATE intercept_rules SET ${cols} WHERE id = $id`).run(params);
772
- },
773
- deleteRule: (id) => db.query("DELETE FROM intercept_rules WHERE id = ?").run(id),
774
- getSettings: () => db.query("SELECT value FROM settings WHERE key='app' LIMIT 1").get() ?? null,
775
- setSettings: (value) => {
776
- db.run("INSERT INTO settings(key,value) VALUES('app',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [JSON.stringify(value)]);
777
- },
778
- getProfiles: () => db.query("SELECT * FROM auth_profiles ORDER BY name COLLATE NOCASE").all(),
779
- getActiveProfile: () => db.query("SELECT * FROM auth_profiles WHERE is_active = 1 LIMIT 1").get(),
780
- insertProfile: (p) => db.query(`INSERT INTO auth_profiles (id,name,description,type,config,token_cache,is_active)
781
- VALUES ($id,$name,$description,$type,$config,$token_cache,$is_active)`).run({
782
- $id: p.id,
783
- $name: p.name,
784
- $description: p.description,
785
- $type: p.type,
786
- $config: p.config,
787
- $token_cache: p.token_cache,
788
- $is_active: p.is_active
789
- }),
790
- updateProfile: (id, patch) => {
791
- const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
792
- const params = { $id: id };
793
- for (const [k, v] of Object.entries(patch))
794
- params[`$${k}`] = v;
795
- db.query(`UPDATE auth_profiles SET ${cols} WHERE id = $id`).run(params);
796
- },
797
- deleteProfile: (id) => db.query("DELETE FROM auth_profiles WHERE id = ?").run(id),
798
- activateProfile: (id) => {
799
- const profile = db.query("SELECT * FROM auth_profiles WHERE id = ?").get(id);
800
- if (!profile)
801
- return;
802
- db.query("UPDATE auth_profiles SET is_active = 0").run();
803
- db.query("UPDATE auth_profiles SET is_active = 1 WHERE id = ?").run(id);
804
- db.query(`INSERT INTO auth_config (id, type, config, updated_at) VALUES ('default', $type, $config, unixepoch())
805
- ON CONFLICT(id) DO UPDATE SET type=excluded.type, config=excluded.config, updated_at=unixepoch()`).run({ $type: profile.type, $config: profile.config });
806
- },
807
- getSavedRequests: () => db.query("SELECT * FROM saved_requests ORDER BY folder, name COLLATE NOCASE").all(),
808
- getSavedRequest: (id) => db.query("SELECT * FROM saved_requests WHERE id = ?").get(id),
809
- insertSavedRequest: (r) => db.query(`INSERT INTO saved_requests
810
- (id, name, folder, method, url, headers, params, body, body_type, raw_type, form_rows, auth, notes)
811
- VALUES ($id,$name,$folder,$method,$url,$headers,$params,$body,$body_type,$raw_type,$form_rows,$auth,$notes)`).run({
812
- $id: r.id,
813
- $name: r.name,
814
- $folder: r.folder,
815
- $method: r.method,
816
- $url: r.url,
817
- $headers: r.headers,
818
- $params: r.params,
819
- $body: r.body,
820
- $body_type: r.body_type,
821
- $raw_type: r.raw_type,
822
- $form_rows: r.form_rows,
823
- $auth: r.auth,
824
- $notes: r.notes
825
- }),
826
- updateSavedRequest: (id, patch) => {
827
- const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
828
- const params = { $id: id };
829
- for (const [k, v] of Object.entries(patch))
830
- params[`$${k}`] = v;
831
- db.query(`UPDATE saved_requests SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
832
- },
833
- deleteSavedRequest: (id) => db.query("DELETE FROM saved_requests WHERE id = ?").run(id),
834
- getSpecHistory: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC").all(),
835
- getLastSpec: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC LIMIT 1").get(),
836
- upsertSpec: (url, title, version, endpointCount) => {
837
- const existing = db.query("SELECT id FROM spec_history WHERE url = ?").get(url);
838
- if (existing) {
839
- db.query("UPDATE spec_history SET title=?, version=?, endpoint_count=?, last_used=unixepoch() WHERE url=?").run(title, version, endpointCount, url);
840
- } else {
841
- db.query(`INSERT INTO spec_history (id, url, title, version, endpoint_count)
842
- VALUES (?, ?, ?, ?, ?)`).run(randomUUID(), url, title, version, endpointCount);
843
- }
844
- },
845
- deleteSpec: (id) => {
846
- db.query("DELETE FROM spec_history WHERE id = ?").run(id);
847
- },
848
- getWorkflows: () => db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all(),
849
- getWorkflow: (id) => db.query("SELECT * FROM workflows WHERE id = ?").get(id),
850
- 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 }),
851
- updateWorkflow: (id, patch) => {
852
- const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
853
- const params = { $id: id };
854
- for (const [k, v] of Object.entries(patch))
855
- params[`$${k}`] = v;
856
- db.query(`UPDATE workflows SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
857
- },
858
- deleteWorkflow: (id) => db.query("DELETE FROM workflows WHERE id = ?").run(id),
859
- getCaptureBins: () => db.query("SELECT * FROM capture_bins ORDER BY created_at DESC").all(),
860
- getCaptureBin: (id) => db.query("SELECT * FROM capture_bins WHERE id = ?").get(id),
861
- insertCaptureBin: (id, name) => {
862
- db.query("INSERT INTO capture_bins (id, name) VALUES (?, ?)").run(id, name);
863
- },
864
- deleteCaptureBin: (id) => {
865
- db.query("DELETE FROM capture_bins WHERE id = ?").run(id);
866
- },
867
- getSetting: (key) => (db.query("SELECT value FROM settings WHERE key = ?").get(key) ?? null)?.value ?? null,
868
- setSetting: (key, value) => {
869
- db.run("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
870
- },
871
- saveMemory: (role, content) => {
872
- db.query("INSERT INTO chat_memory (role, content) VALUES (?, ?)").run(role, content);
873
- },
874
- getMemory: (limit = 20) => {
875
- const rows = db.query("SELECT role, content FROM chat_memory ORDER BY created_at DESC LIMIT ?").all(limit);
876
- return rows.reverse();
877
- },
878
- clearMemory: () => {
879
- db.query("DELETE FROM chat_memory").run();
880
- },
881
- trimMemory: (keepLast = 40) => {
882
- db.query("DELETE FROM chat_memory WHERE id NOT IN (SELECT id FROM chat_memory ORDER BY created_at DESC LIMIT ?)").run(keepLast);
883
- }
884
- };
885
- });
1101
+ // src/commands/logs.ts
1102
+ var exports_logs = {};
1103
+ __export(exports_logs, {
1104
+ run: () => run6
1105
+ });
1106
+ async function run6() {
1107
+ const args = process.argv.slice(2).filter((a) => a !== "logs");
1108
+ const follow = args.includes("-f") || args.includes("--follow");
1109
+ const portIdx = args.findIndex((a) => a === "--port" || a === "-p");
1110
+ const portArg = portIdx >= 0 ? parseInt(args[portIdx + 1] ?? "", 10) : undefined;
1111
+ const nIdx = args.findIndex((a) => a === "-n" || a === "--lines");
1112
+ const lines = nIdx >= 0 ? args[nIdx + 1] ?? "100" : "100";
1113
+ let port = portArg;
1114
+ if (!port) {
1115
+ const all = await readAllDaemonStates();
1116
+ if (all.length === 0) {
1117
+ console.log(`
1118
+ ${paint.yellow("\u25CB")} wasper is not running ${paint.dim("\u2192 wasper up")}
1119
+ `);
1120
+ process.exit(1);
1121
+ }
1122
+ if (all.length > 1) {
1123
+ console.log(`
1124
+ ${paint.yellow("\u25CB")} Multiple instances \u2014 specify --port:
1125
+ `);
1126
+ for (const s of all)
1127
+ console.log(` :${s.port} ${paint.dim(s.specUrl ?? "no spec")}`);
1128
+ console.log(`
1129
+ ${paint.dim("wasper logs --port <port> [-f]")}
1130
+ `);
1131
+ process.exit(1);
1132
+ }
1133
+ port = all[0].port;
1134
+ }
1135
+ const path = logFile(port);
1136
+ const file = Bun.file(path);
1137
+ if (!await file.exists()) {
1138
+ console.log(`
1139
+ ${paint.yellow("\u25CB")} No log file for :${port}`);
1140
+ console.log(` ${paint.dim("Expected: " + path)}
1141
+ `);
1142
+ process.exit(1);
1143
+ }
1144
+ const tailArgs = follow ? ["tail", "-f", "-n", lines, path] : ["tail", "-n", lines, path];
1145
+ const proc = Bun.spawn(tailArgs, { stdout: "inherit", stderr: "inherit" });
1146
+ await proc.exited;
1147
+ }
1148
+ var init_logs = __esm(() => {
1149
+ init_daemon();
1150
+ init_ui();
1151
+ });
886
1152
 
887
1153
  // src/commands/ls.ts
888
1154
  var exports_ls = {};
889
1155
  __export(exports_ls, {
890
- run: () => run5
1156
+ run: () => run7
891
1157
  });
892
1158
  function ago(ts) {
893
1159
  const secs = Math.floor(Date.now() / 1000) - ts;
@@ -901,40 +1167,852 @@ function ago(ts) {
901
1167
  return `${Math.floor(secs / 86400)}d ago`;
902
1168
  return new Date(ts * 1000).toLocaleDateString();
903
1169
  }
904
- async function run5() {
905
- const history = dbQueries.getSpecHistory();
906
- if (history.length === 0) {
907
- console.log(`
908
- No saved specs yet.
909
- `);
910
- console.log(` ${paint.dim("Run:")} wasper --url <spec-url>
911
- `);
912
- process.exit(0);
1170
+ async function run7() {
1171
+ const history = dbQueries.getSpecHistory();
1172
+ if (history.length === 0) {
1173
+ console.log(`
1174
+ No saved specs yet.
1175
+ `);
1176
+ console.log(` ${paint.dim("Run:")} wasper --url <spec-url>
1177
+ `);
1178
+ process.exit(0);
1179
+ }
1180
+ const COL_URL = isTTY ? 50 : 60;
1181
+ const COL_TITLE = 28;
1182
+ console.log();
1183
+ if (isTTY) {
1184
+ const header = ` ${"#".padEnd(3)} ${"URL".padEnd(COL_URL)} ${"Title".padEnd(COL_TITLE)} ${"Endpoints".padEnd(10)} Last used`;
1185
+ console.log(paint.dim(header));
1186
+ console.log(paint.dim(" " + "\u2500".repeat(header.length - 2)));
1187
+ }
1188
+ history.forEach((row, i) => {
1189
+ const num = String(i + 1).padEnd(3);
1190
+ const url = row.url.length > COL_URL ? row.url.slice(0, COL_URL - 1) + "\u2026" : row.url.padEnd(COL_URL);
1191
+ const title = (row.title ?? "\u2014").slice(0, COL_TITLE).padEnd(COL_TITLE);
1192
+ const eps = (row.endpoint_count != null ? String(row.endpoint_count) : "\u2014").padEnd(10);
1193
+ const time = ago(row.last_used);
1194
+ console.log(` ${paint.cyan(num)} ${url} ${paint.dim(title)} ${eps} ${paint.dim(time)}`);
1195
+ });
1196
+ console.log();
1197
+ console.log(paint.dim(` wasper use <number> \u2014 start server with that spec`));
1198
+ console.log(paint.dim(` wasper rm <number> \u2014 remove a spec from history`));
1199
+ console.log();
1200
+ process.exit(0);
1201
+ }
1202
+ var init_ls = __esm(() => {
1203
+ init_db();
1204
+ init_ui();
1205
+ });
1206
+
1207
+ // src/commands/use.ts
1208
+ var exports_use = {};
1209
+ __export(exports_use, {
1210
+ run: () => run8
1211
+ });
1212
+ async function run8() {
1213
+ const raw = process.argv.slice(2).filter((a) => !a.startsWith("-"));
1214
+ const portIdx = process.argv.findIndex((a) => a === "--port" || a === "-p");
1215
+ const PORT = portIdx >= 0 ? parseInt(process.argv[portIdx + 1] ?? "3388", 10) : parseInt(process.env.WASPER_PORT ?? "3388", 10);
1216
+ const target = raw[1];
1217
+ if (!target) {
1218
+ console.error(`
1219
+ Usage: wasper use <number|url> [--port <port>]
1220
+ `);
1221
+ process.exit(1);
1222
+ }
1223
+ const history = dbQueries.getSpecHistory();
1224
+ let url = null;
1225
+ const num = parseInt(target, 10);
1226
+ if (!isNaN(num) && num >= 1 && num <= history.length) {
1227
+ url = history[num - 1]?.url ?? null;
1228
+ } else if (target.startsWith("http")) {
1229
+ url = target;
1230
+ } else {
1231
+ const match = history.find((r) => r.title?.toLowerCase().includes(target.toLowerCase()));
1232
+ if (match)
1233
+ url = match.url;
1234
+ }
1235
+ if (!url) {
1236
+ console.error(`
1237
+ ${paint.red("\u2717")} Spec not found: ${target}`);
1238
+ console.error(` Run ${paint.cyan("wasper ls")} to see saved specs.
1239
+ `);
1240
+ process.exit(1);
1241
+ }
1242
+ const existing = await readDaemonState(PORT);
1243
+ if (existing && isProcessAlive(existing.pid)) {
1244
+ try {
1245
+ process.kill(existing.pid, "SIGTERM");
1246
+ } catch {}
1247
+ await Bun.sleep(400);
1248
+ }
1249
+ const pid = await spawnDaemon(url, PORT, { features: getFeatures() });
1250
+ await Bun.sleep(600);
1251
+ await writeDaemonState({ pid, port: PORT, specUrl: url, startedAt: Date.now() });
1252
+ console.log(`
1253
+ ${paint.green("\u2713")} Started on :${PORT} ${paint.dim(`PID ${pid}`)}`);
1254
+ console.log(` ${paint.dim("\u21A9")} ${url}`);
1255
+ console.log(` ${paint.dim("\u279C")} ${paint.cyan(`http://localhost:${PORT}/`)}
1256
+ `);
1257
+ process.exit(0);
1258
+ }
1259
+ var init_use = __esm(() => {
1260
+ init_db();
1261
+ init_daemon();
1262
+ init_config();
1263
+ init_ui();
1264
+ });
1265
+
1266
+ // src/commands/rm.ts
1267
+ var exports_rm = {};
1268
+ __export(exports_rm, {
1269
+ run: () => run9
1270
+ });
1271
+ async function run9() {
1272
+ const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
1273
+ const target = args[1];
1274
+ if (!target) {
1275
+ console.error(`
1276
+ Usage: wasper rm <number|url>
1277
+ `);
1278
+ process.exit(1);
1279
+ }
1280
+ const history = dbQueries.getSpecHistory();
1281
+ let id = null;
1282
+ let label = null;
1283
+ const num = parseInt(target, 10);
1284
+ if (!isNaN(num) && num >= 1 && num <= history.length) {
1285
+ const row = history[num - 1];
1286
+ if (row) {
1287
+ id = row.id;
1288
+ label = row.title ?? row.url;
1289
+ }
1290
+ } else {
1291
+ const match = history.find((r) => r.url === target || r.title?.toLowerCase().includes(target.toLowerCase()));
1292
+ if (match) {
1293
+ id = match.id;
1294
+ label = match.title ?? match.url;
1295
+ }
1296
+ }
1297
+ if (!id) {
1298
+ console.error(`
1299
+ ${paint.red("\u2717")} Spec not found: ${target}
1300
+ `);
1301
+ process.exit(1);
1302
+ }
1303
+ dbQueries.deleteSpec(id);
1304
+ console.log(`
1305
+ ${paint.green("\u2713")} Removed ${paint.dim(label ?? id)}
1306
+ `);
1307
+ process.exit(0);
1308
+ }
1309
+ var init_rm = __esm(() => {
1310
+ init_db();
1311
+ init_ui();
1312
+ });
1313
+
1314
+ // src/commands/feature.ts
1315
+ var exports_feature = {};
1316
+ __export(exports_feature, {
1317
+ run: () => run10
1318
+ });
1319
+ async function run10() {
1320
+ const args = process.argv.slice(2);
1321
+ const name = args.find((a) => FEATURE_NAMES.includes(a));
1322
+ const value = args.find((a) => a === "on" || a === "off");
1323
+ const portIdx = args.findIndex((a) => a === "--port" || a === "-p");
1324
+ const portArg = portIdx >= 0 ? parseInt(args[portIdx + 1] ?? "", 10) : undefined;
1325
+ const state = portArg ? await readDaemonState(portArg) : await resolveSingleDaemon();
1326
+ if (!state || !isProcessAlive(state.pid)) {
1327
+ const hint = portArg ? `:${portArg}` : "";
1328
+ console.log(`
1329
+ ${paint.yellow("\u25CB")} wasper${hint} is not running ${paint.dim("\u2192 wasper up")}
1330
+ `);
1331
+ process.exit(1);
1332
+ }
1333
+ const authHdr = state.token ? { Authorization: `Bearer ${state.token}` } : {};
1334
+ const base = `http://localhost:${state.port}`;
1335
+ const cur = await fetch(`${base}/api/features`, {
1336
+ headers: authHdr,
1337
+ signal: AbortSignal.timeout(3000)
1338
+ }).then((r) => r.json());
1339
+ const next = value === "on" ? true : value === "off" ? false : !cur[name];
1340
+ await fetch(`${base}/api/features`, {
1341
+ method: "PUT",
1342
+ headers: { "Content-Type": "application/json", ...authHdr },
1343
+ body: JSON.stringify({ [name]: next }),
1344
+ signal: AbortSignal.timeout(3000)
1345
+ });
1346
+ const port = state.port !== 3388 ? ` ${paint.dim(":" + state.port)}` : "";
1347
+ const icon = next ? paint.green("\u2713") : paint.yellow("\u25CB");
1348
+ const status = next ? paint.green("on") : paint.yellow("off");
1349
+ console.log(`
1350
+ ${icon} ${LABELS[name]} ${status}${port}
1351
+ `);
1352
+ process.exit(0);
1353
+ }
1354
+ async function resolveSingleDaemon() {
1355
+ const all = await readAllDaemonStates();
1356
+ if (all.length <= 1)
1357
+ return all[0] ?? null;
1358
+ console.log(`
1359
+ ${paint.yellow("\u25CB")} Multiple instances running \u2014 specify --port:
1360
+ `);
1361
+ for (const s of all)
1362
+ console.log(` :${s.port} ${paint.dim(s.specUrl ?? "no spec")}`);
1363
+ console.log();
1364
+ process.exit(1);
1365
+ }
1366
+ var FEATURE_NAMES, LABELS;
1367
+ var init_feature = __esm(() => {
1368
+ init_daemon();
1369
+ init_ui();
1370
+ FEATURE_NAMES = ["mcp", "proxy", "ai", "readonly"];
1371
+ LABELS = {
1372
+ mcp: "MCP endpoint",
1373
+ proxy: "HTTP proxy",
1374
+ ai: "AI chat",
1375
+ readonly: "Read-only mode"
1376
+ };
1377
+ });
1378
+
1379
+ // src/commands/auth-cmd.ts
1380
+ var exports_auth_cmd = {};
1381
+ __export(exports_auth_cmd, {
1382
+ run: () => run11
1383
+ });
1384
+ async function run11() {
1385
+ const args = process.argv.slice(2).filter((a) => a !== "auth");
1386
+ const sub = args[0] ?? "list";
1387
+ const name = args.slice(1).join(" ");
1388
+ if (!sub || sub === "list") {
1389
+ const profiles = dbQueries.getProfiles();
1390
+ if (!profiles.length) {
1391
+ console.log(`
1392
+ ${paint.dim("\u25CB")} No auth profiles saved`);
1393
+ console.log(` ${paint.dim("Create them in the studio (Authentication tab)")}
1394
+ `);
1395
+ process.exit(0);
1396
+ }
1397
+ console.log();
1398
+ for (const p of profiles) {
1399
+ const mark = p.is_active === 1 ? paint.green("\u25CF") : paint.dim("\u25CB");
1400
+ const desc = p.description ? ` ${paint.dim(p.description)}` : "";
1401
+ console.log(` ${mark} ${paint.bold(p.name)} ${paint.dim(`(${p.type})`)}${desc}`);
1402
+ }
1403
+ console.log(`
1404
+ ${paint.dim("wasper auth use <name> \xB7 wasper auth none")}
1405
+ `);
1406
+ process.exit(0);
1407
+ }
1408
+ if (sub === "use") {
1409
+ if (!name) {
1410
+ console.log(`
1411
+ ${paint.red("\u2717")} Usage: wasper auth use <name>
1412
+ `);
1413
+ process.exit(1);
1414
+ }
1415
+ const profiles = dbQueries.getProfiles();
1416
+ const target = profiles.find((p) => p.name.toLowerCase() === name.toLowerCase()) ?? profiles.find((p) => p.id === name);
1417
+ if (!target) {
1418
+ console.log(`
1419
+ ${paint.red("\u2717")} Profile not found: "${name}"`);
1420
+ console.log(` ${paint.dim("wasper auth \u2014 list profiles")}
1421
+ `);
1422
+ process.exit(1);
1423
+ }
1424
+ dbQueries.activateProfile(target.id);
1425
+ console.log(`
1426
+ ${paint.green("\u2713")} Active auth: ${paint.bold(target.name)} ${paint.dim(`(${target.type})`)}
1427
+ `);
1428
+ const state = await readDaemonState();
1429
+ if (state && isProcessAlive(state.pid)) {
1430
+ try {
1431
+ await fetch(`http://localhost:${state.port}/api/auth/profiles/${encodeURIComponent(target.id)}/activate`, {
1432
+ method: "POST",
1433
+ headers: state.token ? { Authorization: `Bearer ${state.token}` } : undefined,
1434
+ signal: AbortSignal.timeout(2000)
1435
+ });
1436
+ } catch {}
1437
+ }
1438
+ process.exit(0);
1439
+ }
1440
+ if (sub === "none") {
1441
+ dbQueries.setAuthConfig("none", {});
1442
+ console.log(`
1443
+ ${paint.green("\u2713")} Auth disabled
1444
+ `);
1445
+ const state = await readDaemonState();
1446
+ if (state && isProcessAlive(state.pid)) {
1447
+ try {
1448
+ await fetch(`http://localhost:${state.port}/api/auth`, {
1449
+ method: "PUT",
1450
+ headers: {
1451
+ "Content-Type": "application/json",
1452
+ ...state.token ? { Authorization: `Bearer ${state.token}` } : {}
1453
+ },
1454
+ body: JSON.stringify({ type: "none" }),
1455
+ signal: AbortSignal.timeout(2000)
1456
+ });
1457
+ } catch {}
1458
+ }
1459
+ process.exit(0);
1460
+ }
1461
+ console.log(`
1462
+ ${paint.red("\u2717")} Unknown: wasper auth ${sub}`);
1463
+ console.log(` ${paint.dim("wasper auth \xB7 wasper auth use <name> \xB7 wasper auth none")}
1464
+ `);
1465
+ process.exit(1);
1466
+ }
1467
+ var init_auth_cmd = __esm(() => {
1468
+ init_daemon();
1469
+ init_db();
1470
+ init_ui();
1471
+ });
1472
+
1473
+ // src/commands/spec-cmd.ts
1474
+ var exports_spec_cmd = {};
1475
+ __export(exports_spec_cmd, {
1476
+ run: () => run12
1477
+ });
1478
+ async function run12() {
1479
+ const args = process.argv.slice(2).filter((a) => a !== "spec");
1480
+ const portIdx = args.findIndex((a) => a === "--port" || a === "-p");
1481
+ const portArg = portIdx >= 0 ? parseInt(args[portIdx + 1] ?? "", 10) : undefined;
1482
+ const url = args.find((a) => !a.startsWith("-") && a !== args[portIdx + 1]);
1483
+ if (!url) {
1484
+ console.log(`
1485
+ ${paint.red("\u2717")} Usage: wasper spec <url> [--port <port>]`);
1486
+ console.log(` ${paint.dim("Load a new OpenAPI spec on the running daemon")}
1487
+ `);
1488
+ process.exit(1);
1489
+ }
1490
+ const state = portArg ? await readDaemonState(portArg) : await resolveSingleDaemon2();
1491
+ if (!state || !isProcessAlive(state.pid)) {
1492
+ console.log(`
1493
+ ${paint.yellow("\u25CB")} wasper is not running ${paint.dim("\u2192 wasper up --url " + url)}
1494
+ `);
1495
+ process.exit(1);
1496
+ }
1497
+ const port = state.port !== 3388 ? ` ${paint.dim(":" + state.port)}` : "";
1498
+ process.stdout.write(`
1499
+ ${paint.dim("\u2192")} Loading ${paint.cyan(url)}...${port}
1500
+ `);
1501
+ try {
1502
+ const res = await fetch(`http://localhost:${state.port}/api/spec/reload-url`, {
1503
+ method: "POST",
1504
+ headers: {
1505
+ "Content-Type": "application/json",
1506
+ ...state.token ? { Authorization: `Bearer ${state.token}` } : {}
1507
+ },
1508
+ body: JSON.stringify({ url }),
1509
+ signal: AbortSignal.timeout(30000)
1510
+ });
1511
+ const data = await res.json();
1512
+ if (!res.ok || data.error) {
1513
+ console.log(` ${paint.red("\u2717")} ${data.error ?? "Failed to load spec"}
1514
+ `);
1515
+ process.exit(1);
1516
+ }
1517
+ const title = data.spec?.title ?? url;
1518
+ const ver = data.spec?.version ? ` ${paint.dim("v" + data.spec.version)}` : "";
1519
+ const eps = data.endpointCount != null ? ` ${paint.dim("\xB7")} ${paint.green(data.endpointCount + " endpoints")}` : "";
1520
+ console.log(` ${paint.green("\u2713")} ${paint.bold(title)}${ver}${eps}
1521
+ `);
1522
+ } catch (e) {
1523
+ console.log(` ${paint.red("\u2717")} ${e instanceof Error ? e.message : String(e)}
1524
+ `);
1525
+ process.exit(1);
1526
+ }
1527
+ process.exit(0);
1528
+ }
1529
+ async function resolveSingleDaemon2() {
1530
+ const all = await readAllDaemonStates();
1531
+ if (all.length <= 1)
1532
+ return all[0] ?? null;
1533
+ console.log(`
1534
+ ${paint.yellow("\u25CB")} Multiple instances running \u2014 specify --port:
1535
+ `);
1536
+ for (const s of all)
1537
+ console.log(` :${s.port} ${paint.dim(s.specUrl ?? "no spec")}`);
1538
+ console.log();
1539
+ process.exit(1);
1540
+ }
1541
+ var init_spec_cmd = __esm(() => {
1542
+ init_daemon();
1543
+ init_ui();
1544
+ });
1545
+
1546
+ // src/commands/service.ts
1547
+ var exports_service = {};
1548
+ __export(exports_service, {
1549
+ run: () => run13
1550
+ });
1551
+ import { join as join3 } from "path";
1552
+ import { homedir as homedir3 } from "os";
1553
+ import { mkdir as mkdir2, writeFile as writeFile2, unlink as unlink2 } from "fs/promises";
1554
+ async function resolveWasperBin() {
1555
+ const exec = process.execPath;
1556
+ if (!exec.endsWith("/bun") && !exec.endsWith("/bun-debug") && !exec.endsWith("/bun-profile")) {
1557
+ return exec;
1558
+ }
1559
+ try {
1560
+ const r = await Bun.$`which wasper`.quiet();
1561
+ const p = r.stdout.toString().trim();
1562
+ if (p)
1563
+ return p;
1564
+ } catch {}
1565
+ return `${exec} ${Bun.main}`;
1566
+ }
1567
+ async function buildSystemdUnit(wasperBin, port, specUrl) {
1568
+ const wasperDir = join3(homedir3(), ".wasper");
1569
+ const logPath = join3(wasperDir, `server-${port}.log`);
1570
+ const envBlock = [`PORT=${port}`, ...specUrl ? [`WASPER_SPEC_URL=${specUrl}`] : []];
1571
+ const execArgs = ["start", "--_daemon", "--port", String(port), ...specUrl ? ["--url", specUrl] : []];
1572
+ return `[Unit]
1573
+ Description=Wasper OpenAPI Agent
1574
+ After=network.target
1575
+
1576
+ [Service]
1577
+ Type=simple
1578
+ ExecStart=${wasperBin} ${execArgs.join(" ")}
1579
+ Restart=on-failure
1580
+ RestartSec=5
1581
+ Environment=${envBlock.join(" ")}
1582
+ StandardOutput=append:${logPath}
1583
+ StandardError=append:${logPath}
1584
+
1585
+ [Install]
1586
+ WantedBy=default.target
1587
+ `;
1588
+ }
1589
+ async function systemctl(...args) {
1590
+ const proc = Bun.spawn(["systemctl", "--user", ...args], {
1591
+ stdout: "inherit",
1592
+ stderr: "inherit"
1593
+ });
1594
+ return proc.exited;
1595
+ }
1596
+ async function installLinux(port, specUrl) {
1597
+ const wasperBin = await resolveWasperBin();
1598
+ await mkdir2(SYSTEMD_UNIT_DIR, { recursive: true });
1599
+ await mkdir2(join3(homedir3(), ".wasper"), { recursive: true });
1600
+ const unit = await buildSystemdUnit(wasperBin, port, specUrl);
1601
+ await writeFile2(SYSTEMD_UNIT_FILE, unit);
1602
+ await systemctl("daemon-reload");
1603
+ await systemctl("enable", SYSTEMD_SERVICE);
1604
+ console.log(`
1605
+ ${paint.green("\u2713")} Service installed`);
1606
+ console.log(` ${paint.dim(SYSTEMD_UNIT_FILE)}`);
1607
+ console.log(`
1608
+ ${paint.dim("Start now: wasper service start")}`);
1609
+ console.log(` ${paint.dim("Auto-starts on login (systemd user session)")}
1610
+ `);
1611
+ }
1612
+ async function uninstallLinux() {
1613
+ await systemctl("stop", SYSTEMD_SERVICE).catch(() => {});
1614
+ await systemctl("disable", SYSTEMD_SERVICE).catch(() => {});
1615
+ try {
1616
+ await unlink2(SYSTEMD_UNIT_FILE);
1617
+ } catch {}
1618
+ await systemctl("daemon-reload");
1619
+ console.log(`
1620
+ ${paint.green("\u2713")} Service uninstalled
1621
+ `);
1622
+ }
1623
+ function buildPlist(wasperBin, port, specUrl) {
1624
+ const logFile2 = join3(homedir3(), ".wasper", `server-${port}.log`);
1625
+ const args = [wasperBin, "start", "--_daemon", "--port", String(port), ...specUrl ? ["--url", specUrl] : []];
1626
+ const argsXml = args.map((a) => ` <string>${a}</string>`).join(`
1627
+ `);
1628
+ return `<?xml version="1.0" encoding="UTF-8"?>
1629
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1630
+ <plist version="1.0">
1631
+ <dict>
1632
+ <key>Label</key>
1633
+ <string>${LAUNCH_LABEL}</string>
1634
+ <key>ProgramArguments</key>
1635
+ <array>
1636
+ ${argsXml}
1637
+ </array>
1638
+ <key>RunAtLoad</key>
1639
+ <true/>
1640
+ <key>KeepAlive</key>
1641
+ <true/>
1642
+ <key>StandardOutPath</key>
1643
+ <string>${logFile2}</string>
1644
+ <key>StandardErrorPath</key>
1645
+ <string>${logFile2}</string>
1646
+ </dict>
1647
+ </plist>
1648
+ `;
1649
+ }
1650
+ async function launchctl(...args) {
1651
+ const proc = Bun.spawn(["launchctl", ...args], { stdout: "inherit", stderr: "inherit" });
1652
+ return proc.exited;
1653
+ }
1654
+ async function installMac(port, specUrl) {
1655
+ const wasperBin = await resolveWasperBin();
1656
+ await mkdir2(LAUNCH_DIR, { recursive: true });
1657
+ await mkdir2(join3(homedir3(), ".wasper"), { recursive: true });
1658
+ const plist = buildPlist(wasperBin, port, specUrl);
1659
+ await writeFile2(LAUNCH_PLIST, plist);
1660
+ await launchctl("load", "-w", LAUNCH_PLIST);
1661
+ console.log(`
1662
+ ${paint.green("\u2713")} Launch Agent installed`);
1663
+ console.log(` ${paint.dim(LAUNCH_PLIST)}`);
1664
+ console.log(`
1665
+ ${paint.dim("Auto-starts on login (macOS LaunchAgent)")}
1666
+ `);
1667
+ }
1668
+ async function uninstallMac() {
1669
+ await launchctl("unload", "-w", LAUNCH_PLIST).catch(() => {});
1670
+ try {
1671
+ await unlink2(LAUNCH_PLIST);
1672
+ } catch {}
1673
+ console.log(`
1674
+ ${paint.green("\u2713")} Launch Agent uninstalled
1675
+ `);
1676
+ }
1677
+ async function run13() {
1678
+ const rawArgs = process.argv.slice(2).filter((a) => a !== "service");
1679
+ const sub = rawArgs.find((a) => !a.startsWith("-")) ?? "help";
1680
+ const portIdx = rawArgs.findIndex((a) => a === "--port" || a === "-p");
1681
+ const port = portIdx >= 0 ? parseInt(rawArgs[portIdx + 1] ?? "3388", 10) : parseInt(process.env.WASPER_PORT ?? "", 10) || 3388;
1682
+ const urlIdx = rawArgs.findIndex((a) => a === "--url" || a === "-u");
1683
+ const specUrl = urlIdx >= 0 ? rawArgs[urlIdx + 1] : process.env.WASPER_SPEC_URL;
1684
+ if (!IS_LINUX && !IS_MAC) {
1685
+ console.log(`
1686
+ ${paint.yellow("\u25CB")} Service management is only supported on Linux (systemd) and macOS (launchd)
1687
+ `);
1688
+ process.exit(1);
1689
+ }
1690
+ const platform = IS_LINUX ? "Linux (systemd --user)" : "macOS (LaunchAgent)";
1691
+ switch (sub) {
1692
+ case "install": {
1693
+ if (IS_LINUX)
1694
+ await installLinux(port, specUrl);
1695
+ else
1696
+ await installMac(port, specUrl);
1697
+ break;
1698
+ }
1699
+ case "uninstall": {
1700
+ if (IS_LINUX)
1701
+ await uninstallLinux();
1702
+ else
1703
+ await uninstallMac();
1704
+ break;
1705
+ }
1706
+ case "start": {
1707
+ if (IS_LINUX) {
1708
+ await systemctl("start", SYSTEMD_SERVICE);
1709
+ } else {
1710
+ await launchctl("load", "-w", LAUNCH_PLIST);
1711
+ }
1712
+ break;
1713
+ }
1714
+ case "stop": {
1715
+ if (IS_LINUX) {
1716
+ await systemctl("stop", SYSTEMD_SERVICE);
1717
+ } else {
1718
+ await launchctl("unload", LAUNCH_PLIST);
1719
+ }
1720
+ break;
1721
+ }
1722
+ case "restart": {
1723
+ if (IS_LINUX) {
1724
+ await systemctl("restart", SYSTEMD_SERVICE);
1725
+ } else {
1726
+ await launchctl("unload", LAUNCH_PLIST).catch(() => {});
1727
+ await Bun.sleep(500);
1728
+ await launchctl("load", "-w", LAUNCH_PLIST);
1729
+ }
1730
+ break;
1731
+ }
1732
+ case "status": {
1733
+ if (IS_LINUX) {
1734
+ await systemctl("status", SYSTEMD_SERVICE);
1735
+ } else {
1736
+ await launchctl("list", LAUNCH_LABEL);
1737
+ }
1738
+ break;
1739
+ }
1740
+ case "enable": {
1741
+ if (IS_LINUX) {
1742
+ await systemctl("enable", SYSTEMD_SERVICE);
1743
+ } else {
1744
+ console.log(`
1745
+ ${paint.dim("On macOS, RunAtLoad=true in the plist controls auto-start.")}`);
1746
+ console.log(` ${paint.dim("Use: wasper service install to re-install with auto-start enabled.")}
1747
+ `);
1748
+ }
1749
+ break;
1750
+ }
1751
+ case "disable": {
1752
+ if (IS_LINUX) {
1753
+ await systemctl("disable", SYSTEMD_SERVICE);
1754
+ } else {
1755
+ console.log(`
1756
+ ${paint.dim("On macOS, use: wasper service uninstall to remove auto-start.")}
1757
+ `);
1758
+ }
1759
+ break;
1760
+ }
1761
+ case "logs": {
1762
+ if (IS_LINUX) {
1763
+ const proc = Bun.spawn(["journalctl", "--user", "-u", SYSTEMD_SERVICE, "-f", "--no-pager"], {
1764
+ stdout: "inherit",
1765
+ stderr: "inherit"
1766
+ });
1767
+ await proc.exited;
1768
+ } else {
1769
+ const logPath = join3(homedir3(), ".wasper", "server.log");
1770
+ const proc = Bun.spawn(["tail", "-f", logPath], { stdout: "inherit", stderr: "inherit" });
1771
+ await proc.exited;
1772
+ }
1773
+ break;
1774
+ }
1775
+ case "cat": {
1776
+ if (IS_LINUX) {
1777
+ const wasperBin = await resolveWasperBin();
1778
+ process.stdout.write(await buildSystemdUnit(wasperBin, port, specUrl));
1779
+ } else {
1780
+ const wasperBin = await resolveWasperBin();
1781
+ process.stdout.write(buildPlist(wasperBin, port, specUrl));
1782
+ }
1783
+ break;
1784
+ }
1785
+ default: {
1786
+ console.log(`
1787
+ ${paint.bold("wasper service")} \u2014 Manage wasper as a system service ${paint.dim(`(${platform})`)}
1788
+
1789
+ ${paint.bold("Commands")}
1790
+ wasper service install Install and enable (auto-start on login)
1791
+ wasper service uninstall Remove the service
1792
+ wasper service start Start the service now
1793
+ wasper service stop Stop the service
1794
+ wasper service restart Restart the service
1795
+ wasper service status Show service status
1796
+ wasper service enable Enable auto-start on login ${paint.dim("(Linux)")}
1797
+ wasper service disable Disable auto-start ${paint.dim("(Linux)")}
1798
+ wasper service logs Follow service logs
1799
+ wasper service cat Print the generated unit/plist
1800
+
1801
+ ${paint.bold("Options")}
1802
+ --url <spec> OpenAPI spec URL to embed in the service definition
1803
+ --port <port> Port ${paint.dim("(default: 3388)")}
1804
+ `);
1805
+ }
1806
+ }
1807
+ }
1808
+ var IS_LINUX, IS_MAC, SYSTEMD_SERVICE = "wasper", SYSTEMD_UNIT_DIR, SYSTEMD_UNIT_FILE, LAUNCH_LABEL = "com.wasper.agent", LAUNCH_DIR, LAUNCH_PLIST;
1809
+ var init_service = __esm(() => {
1810
+ init_ui();
1811
+ IS_LINUX = process.platform === "linux";
1812
+ IS_MAC = process.platform === "darwin";
1813
+ SYSTEMD_UNIT_DIR = join3(homedir3(), ".config", "systemd", "user");
1814
+ SYSTEMD_UNIT_FILE = join3(SYSTEMD_UNIT_DIR, `${SYSTEMD_SERVICE}.service`);
1815
+ LAUNCH_DIR = join3(homedir3(), "Library", "LaunchAgents");
1816
+ LAUNCH_PLIST = join3(LAUNCH_DIR, `${LAUNCH_LABEL}.plist`);
1817
+ });
1818
+
1819
+ // src/commands/update.ts
1820
+ var exports_update = {};
1821
+ __export(exports_update, {
1822
+ run: () => run14,
1823
+ printUpdateNotice: () => printUpdateNotice,
1824
+ performUpdate: () => performUpdate,
1825
+ fetchLatestVersion: () => fetchLatestVersion,
1826
+ checkForUpdate: () => checkForUpdate
1827
+ });
1828
+ import { join as join4 } from "path";
1829
+ import { homedir as homedir4 } from "os";
1830
+ import { chmod, rename, unlink as unlink3, mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
1831
+ async function fetchLatestVersion() {
1832
+ try {
1833
+ const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, {
1834
+ signal: AbortSignal.timeout(5000),
1835
+ headers: { Accept: "application/json" }
1836
+ });
1837
+ if (!res.ok)
1838
+ return null;
1839
+ const data = await res.json();
1840
+ return data.version ?? null;
1841
+ } catch {
1842
+ return null;
1843
+ }
1844
+ }
1845
+ async function checkForUpdate() {
1846
+ if (process.env.WASPER_NO_UPDATE_CHECK)
1847
+ return null;
1848
+ let state = null;
1849
+ try {
1850
+ state = JSON.parse(await readFile2(CHECK_FILE, "utf-8"));
1851
+ } catch {}
1852
+ let latest = state?.latest ?? null;
1853
+ if (!state || Date.now() - state.lastCheck > CHECK_INTERVAL) {
1854
+ latest = await fetchLatestVersion();
1855
+ if (latest) {
1856
+ await mkdir3(join4(homedir4(), ".wasper"), { recursive: true }).catch(() => {});
1857
+ await writeFile3(CHECK_FILE, JSON.stringify({ lastCheck: Date.now(), latest }), "utf-8").catch(() => {});
1858
+ }
1859
+ }
1860
+ if (latest && compareSemver(latest, VERSION) > 0)
1861
+ return latest;
1862
+ return null;
1863
+ }
1864
+ function printUpdateNotice(latest) {
1865
+ console.log(` ${paint.yellow("\u25B2")} Update available ${paint.dim(VERSION)} \u2192 ${paint.green(latest)} ${paint.dim("\xB7")} run ${paint.bold("wasper update")}
1866
+ `);
1867
+ }
1868
+ function binaryAssetName() {
1869
+ const os = process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux";
1870
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
1871
+ return `wasper-${os}-${arch}${os === "windows" ? ".exe" : ""}`;
1872
+ }
1873
+ async function updateCompiledBinary(latest) {
1874
+ const exe = process.execPath;
1875
+ const asset = binaryAssetName();
1876
+ const url = `https://github.com/${REPO}/releases/download/v${latest}/${asset}`;
1877
+ const res = await fetch(url, { redirect: "follow" });
1878
+ if (!res.ok)
1879
+ throw new Error(`Download failed (HTTP ${res.status}) \u2014 ${url}`);
1880
+ const bytes = new Uint8Array(await res.arrayBuffer());
1881
+ if (bytes.byteLength < 1024 * 100)
1882
+ throw new Error("Downloaded file is suspiciously small \u2014 aborting");
1883
+ const tmp = `${exe}.update`;
1884
+ const old = `${exe}.old`;
1885
+ await Bun.write(tmp, bytes);
1886
+ await chmod(tmp, 493);
1887
+ await rename(exe, old);
1888
+ try {
1889
+ await rename(tmp, exe);
1890
+ } catch (e) {
1891
+ await rename(old, exe).catch(() => {});
1892
+ throw e;
1893
+ }
1894
+ await unlink3(old).catch(() => {});
1895
+ }
1896
+ async function updatePackageInstall() {
1897
+ const tryRun = async (cmd) => {
1898
+ try {
1899
+ const p = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" });
1900
+ const code = await p.exited;
1901
+ return code === 0;
1902
+ } catch {
1903
+ return false;
1904
+ }
1905
+ };
1906
+ if (await tryRun(["bun", "add", "-g", `${PACKAGE_NAME}@latest`]))
1907
+ return;
1908
+ if (await tryRun(["npm", "install", "-g", `${PACKAGE_NAME}@latest`]))
1909
+ return;
1910
+ throw new Error("Neither `bun add -g` nor `npm install -g` succeeded. Update manually.");
1911
+ }
1912
+ async function performUpdate(opts = {}) {
1913
+ const spinner = new Spinner;
1914
+ if (!opts.quiet)
1915
+ spinner.start("Checking for updates\u2026");
1916
+ const latest = await fetchLatestVersion();
1917
+ if (!latest) {
1918
+ spinner.stop("\u2717", "Could not reach the npm registry", "red");
1919
+ return false;
913
1920
  }
914
- const COL_URL = isTTY ? 50 : 60;
915
- const COL_TITLE = 28;
916
- console.log();
917
- if (isTTY) {
918
- const header = ` ${"#".padEnd(3)} ${"URL".padEnd(COL_URL)} ${"Title".padEnd(COL_TITLE)} ${"Endpoints".padEnd(10)} Last used`;
919
- console.log(paint.dim(header));
920
- console.log(paint.dim(" " + "\u2500".repeat(header.length - 2)));
1921
+ if (compareSemver(latest, VERSION) <= 0) {
1922
+ spinner.stop("\u2713", `Already up to date ${paint.dim("v" + VERSION)}`, "green");
1923
+ return false;
921
1924
  }
922
- history.forEach((row, i) => {
923
- const num = String(i + 1).padEnd(3);
924
- const url = row.url.length > COL_URL ? row.url.slice(0, COL_URL - 1) + "\u2026" : row.url.padEnd(COL_URL);
925
- const title = (row.title ?? "\u2014").slice(0, COL_TITLE).padEnd(COL_TITLE);
926
- const eps = (row.endpoint_count != null ? String(row.endpoint_count) : "\u2014").padEnd(10);
927
- const time = ago(row.last_used);
928
- console.log(` ${paint.cyan(num)} ${url} ${paint.dim(title)} ${eps} ${paint.dim(time)}`);
929
- });
1925
+ spinner.stop();
1926
+ if (!opts.quiet)
1927
+ spinner.start(`Updating ${paint.dim(VERSION)} \u2192 ${paint.green(latest)}\u2026`);
1928
+ try {
1929
+ if (isCompiledBinary())
1930
+ await updateCompiledBinary(latest);
1931
+ else
1932
+ await updatePackageInstall();
1933
+ spinner.stop("\u2713", `Updated to ${paint.bold("v" + latest)} ${paint.dim("\u2014 restart any running servers to use it")}`, "green");
1934
+ await writeFile3(CHECK_FILE, JSON.stringify({ lastCheck: Date.now(), latest }), "utf-8").catch(() => {});
1935
+ return true;
1936
+ } catch (e) {
1937
+ spinner.stop("\u2717", `Update failed: ${e instanceof Error ? e.message : String(e)}`, "red");
1938
+ return false;
1939
+ }
1940
+ }
1941
+ async function run14() {
930
1942
  console.log();
931
- console.log(paint.dim(` wasper use <number> \u2014 start server with that spec`));
932
- console.log(paint.dim(` wasper rm <number> \u2014 remove a spec from history`));
1943
+ await performUpdate();
933
1944
  console.log();
934
1945
  }
935
- var init_ls = __esm(() => {
936
- init_db();
1946
+ var CHECK_FILE, CHECK_INTERVAL;
1947
+ var init_update = __esm(() => {
1948
+ init_version();
1949
+ init_ui();
1950
+ CHECK_FILE = join4(homedir4(), ".wasper", "update-check.json");
1951
+ CHECK_INTERVAL = 24 * 60 * 60 * 1000;
1952
+ });
1953
+
1954
+ // src/commands/help.ts
1955
+ var exports_help = {};
1956
+ __export(exports_help, {
1957
+ run: () => run15
1958
+ });
1959
+ async function run15() {
1960
+ const b = (s) => paint.bold(s);
1961
+ const d = (s) => paint.dim(s);
1962
+ const c = (s) => paint.cyan(s);
1963
+ console.log(`
1964
+ ${b("wasper")} ${d("v" + VERSION)}
1965
+
1966
+ ${b("Daemon")}
1967
+ ${c("wasper up")} ${d("[--url <spec>] [--port <port>]")}
1968
+ Start daemon in background (default port: 3388)
1969
+ ${c("wasper up")} ${d("--url <spec2> --port 3389")}
1970
+ Run a second instance on a different port
1971
+ ${c("wasper down")} ${d("[--port <port>]")} Stop one instance
1972
+ ${c("wasper down --all")} Stop all instances
1973
+ ${c("wasper ps")} List all running instances
1974
+ ${c("wasper status")} ${d("[--port <p>]")} Status of one or all instances
1975
+ ${c("wasper logs")} ${d("[-f] [--port <p>]")} Tail server logs
1976
+
1977
+ ${b("Spec")}
1978
+ ${c("wasper spec")} ${d("<url> [--port <p>]")} Load a new spec on the running daemon
1979
+ ${c("wasper reload")} ${d("[--port <p>]")} Hot-reload current spec
1980
+ ${c("wasper ls")} List saved spec history
1981
+ ${c("wasper use")} ${d("<n|url> [--port <p>]")} Restart with a saved spec
1982
+ ${c("wasper rm")} ${d("<n|url>")} Remove spec from history
1983
+
1984
+ ${b("Features")} ${d("toggle on the running daemon")}
1985
+ ${c("wasper mcp")} ${d("[on|off] [--port <p>]")}
1986
+ ${c("wasper proxy")} ${d("[on|off] [--port <p>]")}
1987
+ ${c("wasper ai")} ${d("[on|off] [--port <p>]")}
1988
+ ${c("wasper readonly")} ${d("[on|off] [--port <p>]")}
1989
+
1990
+ ${b("Auth")}
1991
+ ${c("wasper auth")} List saved auth profiles
1992
+ ${c("wasper auth use")} ${d("<name>")} Switch active profile
1993
+ ${c("wasper auth none")} Disable auth
1994
+
1995
+ ${b("Service")} ${d("auto-start on login")}
1996
+ ${c("wasper service install")} ${d("[--port <p>] [--url <spec>]")}
1997
+ ${c("wasper service uninstall")}
1998
+ ${c("wasper service start")} ${d("|")} ${c("stop")} ${d("|")} ${c("status")} ${d("|")} ${c("logs")}
1999
+
2000
+ ${b("Other")}
2001
+ ${c("wasper update")} Update to latest version
2002
+ ${c("wasper --version")} Print version
2003
+
2004
+ ${d("Multi-instance example:")}
2005
+ ${d(" wasper up --url https://api1.com/openapi.json --port 3388")}
2006
+ ${d(" wasper up --url https://api2.com/openapi.json --port 3389")}
2007
+ ${d(" wasper ps")}
2008
+ ${d(" wasper mcp off --port 3389")}
2009
+ ${d(" wasper down --all")}
2010
+ `);
2011
+ process.exit(0);
2012
+ }
2013
+ var init_help = __esm(() => {
937
2014
  init_ui();
2015
+ init_version();
938
2016
  });
939
2017
 
940
2018
  // ../../node_modules/js-yaml/dist/js-yaml.mjs
@@ -3708,6 +4786,11 @@ function parseSpecText(text, url, name) {
3708
4786
  const info = doc.info ?? {};
3709
4787
  const servers = doc.servers ?? [];
3710
4788
  let baseUrl = servers[0]?.url ?? "";
4789
+ if (!baseUrl && doc.swagger && doc.host) {
4790
+ const scheme = doc.schemes?.[0] ?? "https";
4791
+ const basePath = typeof doc.basePath === "string" ? doc.basePath.replace(/\/$/, "") : "";
4792
+ baseUrl = `${scheme}://${doc.host}${basePath}`;
4793
+ }
3711
4794
  if (!baseUrl && url) {
3712
4795
  try {
3713
4796
  baseUrl = new URL(url).origin;
@@ -4100,8 +5183,8 @@ async function applyAuth(url, headers, authConfig) {
4100
5183
  function cacheKey(c) {
4101
5184
  return `${c.tokenUrl ?? ""}|${c.clientId ?? ""}|${c.scope ?? ""}`;
4102
5185
  }
4103
- function getCachedToken(config) {
4104
- const key = cacheKey(config);
5186
+ function getCachedToken(config2) {
5187
+ const key = cacheKey(config2);
4105
5188
  const mem = memCaches.get(key);
4106
5189
  if (mem && Date.now() < mem.expires_at - 30000)
4107
5190
  return mem.access_token;
@@ -4120,21 +5203,21 @@ function getCachedToken(config) {
4120
5203
  }
4121
5204
  return null;
4122
5205
  }
4123
- async function getOrRefreshOAuthToken(config) {
4124
- const cached = getCachedToken(config);
5206
+ async function getOrRefreshOAuthToken(config2) {
5207
+ const cached = getCachedToken(config2);
4125
5208
  if (cached)
4126
5209
  return cached;
4127
- if (!config.tokenUrl || !config.clientId)
5210
+ if (!config2.tokenUrl || !config2.clientId)
4128
5211
  return null;
4129
5212
  const params = new URLSearchParams({
4130
5213
  grant_type: "client_credentials",
4131
- client_id: config.clientId,
4132
- client_secret: config.clientSecret ?? ""
5214
+ client_id: config2.clientId,
5215
+ client_secret: config2.clientSecret ?? ""
4133
5216
  });
4134
- if (config.scope)
4135
- params.set("scope", config.scope);
5217
+ if (config2.scope)
5218
+ params.set("scope", config2.scope);
4136
5219
  try {
4137
- const res = await fetch(config.tokenUrl, {
5220
+ const res = await fetch(config2.tokenUrl, {
4138
5221
  method: "POST",
4139
5222
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
4140
5223
  body: params.toString()
@@ -4145,7 +5228,7 @@ async function getOrRefreshOAuthToken(config) {
4145
5228
  if (!data.access_token)
4146
5229
  return null;
4147
5230
  const cache = { access_token: data.access_token, expires_at: Date.now() + (data.expires_in ?? 3600) * 1000 };
4148
- memCaches.set(cacheKey(config), cache);
5231
+ memCaches.set(cacheKey(config2), cache);
4149
5232
  dbQueries.updateTokenCache(cache);
4150
5233
  return cache.access_token;
4151
5234
  } catch {
@@ -4169,53 +5252,6 @@ var init_engine = __esm(() => {
4169
5252
  memCaches = new Map;
4170
5253
  });
4171
5254
 
4172
- // src/config.ts
4173
- function setServerConfig(c) {
4174
- config = c;
4175
- }
4176
- function getServerConfig() {
4177
- return config;
4178
- }
4179
- function updateServerConfig(patch) {
4180
- config = { ...config, ...patch };
4181
- }
4182
- function getFeatures() {
4183
- return features;
4184
- }
4185
- function setFeatures(patch) {
4186
- features = { ...features, ...patch };
4187
- }
4188
- function readonlyViolation(method) {
4189
- if (!features.readonly)
4190
- return null;
4191
- if (SAFE_METHODS.has(method.toUpperCase()))
4192
- return null;
4193
- return `Read-only mode is enabled \u2014 ${method.toUpperCase()} requests are blocked. Ask the operator to run /readonly off.`;
4194
- }
4195
- function isAuthorized(req) {
4196
- if (!config.token)
4197
- return true;
4198
- const auth = req.headers.get("authorization");
4199
- if (auth === `Bearer ${config.token}`)
4200
- return true;
4201
- try {
4202
- return new URL(req.url).searchParams.get("token") === config.token;
4203
- } catch {
4204
- return false;
4205
- }
4206
- }
4207
- var config, features, SAFE_METHODS;
4208
- var init_config = __esm(() => {
4209
- config = {
4210
- port: 3388,
4211
- host: "0.0.0.0",
4212
- origin: null,
4213
- token: null
4214
- };
4215
- features = { mcp: true, proxy: true, ai: true, readonly: false };
4216
- SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
4217
- });
4218
-
4219
5255
  // src/mcp/server.ts
4220
5256
  function summarizeAuth(type, config2) {
4221
5257
  switch (type) {
@@ -5838,6 +6874,7 @@ async function handleSpecUpload(req) {
5838
6874
  try {
5839
6875
  const state = loadSpecFromText(content, filename);
5840
6876
  const suggestedVars = extractSuggestedVars(content, state.spec.baseUrl);
6877
+ logBus.broadcastServerEvent({ kind: "spec_changed" });
5841
6878
  return json({
5842
6879
  ok: true,
5843
6880
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
@@ -5860,6 +6897,7 @@ async function handleSpecReloadUrl(req) {
5860
6897
  try {
5861
6898
  const state = await loadSpec(body.url);
5862
6899
  const suggestedVars = extractSuggestedVars(state.spec.raw, state.spec.baseUrl);
6900
+ logBus.broadcastServerEvent({ kind: "spec_changed" });
5863
6901
  return json({
5864
6902
  ok: true,
5865
6903
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
@@ -5871,7 +6909,8 @@ async function handleSpecReloadUrl(req) {
5871
6909
  }
5872
6910
  }
5873
6911
  function handleGetLogs(searchParams) {
5874
- const limit = Math.min(parseInt(searchParams.get("limit") ?? "500"), 2000);
6912
+ const raw = parseInt(searchParams.get("limit") ?? "500", 10);
6913
+ const limit = Math.min(Number.isFinite(raw) ? raw : 500, 2000);
5875
6914
  return json(dbQueries.getRecentLogs(limit));
5876
6915
  }
5877
6916
  function handleClearLogs() {
@@ -5893,6 +6932,8 @@ async function handleSetAuth(req) {
5893
6932
  return json({ type: body.type, config: body.config });
5894
6933
  }
5895
6934
  async function handleTestAuth() {
6935
+ if (!hasState())
6936
+ return badRequest("No spec loaded");
5896
6937
  const { spec } = getState();
5897
6938
  const authRow = dbQueries.getAuthConfig();
5898
6939
  const authConfig = authRow ? JSON.parse(authRow.config) : { type: "none" };
@@ -5906,6 +6947,8 @@ async function handleTestAuth() {
5906
6947
  }
5907
6948
  }
5908
6949
  function handleGetEndpoints() {
6950
+ if (!hasState())
6951
+ return json([]);
5909
6952
  return json(getState().operations);
5910
6953
  }
5911
6954
  function handleGetSettings() {
@@ -5927,6 +6970,8 @@ async function handleSetSettings(req) {
5927
6970
  return json(body);
5928
6971
  }
5929
6972
  async function executeTool(name, args, cache = new Map) {
6973
+ if (!hasState())
6974
+ return { text: "No spec loaded.", isError: true };
5930
6975
  const { operations, spec } = getState();
5931
6976
  if (name === "search_endpoints") {
5932
6977
  const cacheKey2 = `search:${String(args.query ?? "").toLowerCase()}`;
@@ -6586,10 +7631,11 @@ async function handleExplorerRequest(req) {
6586
7631
  };
6587
7632
  if (timeoutMs > 0)
6588
7633
  fetchOpts.signal = AbortSignal.timeout(timeoutMs);
7634
+ const parsedUrl = new URL(authedUrl);
6589
7635
  let dnsMs = 0;
6590
7636
  let resolvedAddr = "";
6591
7637
  try {
6592
- const u = new URL(authedUrl);
7638
+ const u = parsedUrl;
6593
7639
  const h = u.hostname;
6594
7640
  const defaultPort = u.protocol === "https:" ? 443 : 80;
6595
7641
  const port = u.port ? Number(u.port) : defaultPort;
@@ -6608,7 +7654,7 @@ async function handleExplorerRequest(req) {
6608
7654
  const waitMs = Math.round(performance.now() - fetchStart);
6609
7655
  const resHeaders = Object.fromEntries(res.headers.entries());
6610
7656
  const ct = res.headers.get("content-type") ?? "";
6611
- const u = new URL(authedUrl);
7657
+ const u = parsedUrl;
6612
7658
  const networkInfo = {
6613
7659
  scheme: u.protocol.replace(":", ""),
6614
7660
  host: u.host,
@@ -7489,15 +8535,15 @@ var init_repl = __esm(() => {
7489
8535
  // src/commands/start.ts
7490
8536
  var exports_start = {};
7491
8537
  __export(exports_start, {
7492
- run: () => run6
8538
+ run: () => run16
7493
8539
  });
7494
- import { parseArgs } from "util";
8540
+ import { parseArgs as parseArgs2 } from "util";
7495
8541
  import { createInterface } from "readline";
7496
- import { homedir as homedir4 } from "os";
7497
- import { join as join4, dirname } from "path";
8542
+ import { homedir as homedir5 } from "os";
8543
+ import { join as join5, dirname } from "path";
7498
8544
  import { mkdirSync as mkdirSync2 } from "fs";
7499
- async function run6(overrideOpts) {
7500
- const { values } = parseArgs({
8545
+ async function run16(overrideOpts) {
8546
+ const { values } = parseArgs2({
7501
8547
  args: process.argv.slice(2).filter((a) => a !== "start"),
7502
8548
  options: {
7503
8549
  url: { type: "string" },
@@ -7517,7 +8563,7 @@ async function run6(overrideOpts) {
7517
8563
  strict: false
7518
8564
  });
7519
8565
  if (values.help) {
7520
- printHelp();
8566
+ printHelp2();
7521
8567
  process.exit(0);
7522
8568
  }
7523
8569
  let specUrl = overrideOpts?.url ?? (values.url ? String(values.url) : null) ?? process.env.WASPER_SPEC_URL ?? null;
@@ -7682,7 +8728,7 @@ async function run6(overrideOpts) {
7682
8728
  ${paint.dim("shutting down")}
7683
8729
 
7684
8730
  `);
7685
- clearDaemonState().finally(() => {
8731
+ clearDaemonState(PORT).finally(() => {
7686
8732
  db.close();
7687
8733
  server.stop();
7688
8734
  process.exit(0);
@@ -8008,18 +9054,21 @@ function printInteractiveHelp() {
8008
9054
  ${k("/status")} ${k("/reload")} ${k("/help")} ${k("/quit")}
8009
9055
  `);
8010
9056
  }
8011
- function printHelp() {
9057
+ function printHelp2() {
8012
9058
  console.log(`
8013
- Usage: wasper [start] [options]
9059
+ Usage: wasper start [options]
9060
+
9061
+ Starts wasper in the foreground with an interactive REPL.
9062
+ For background (daemon) mode \u2014 the default \u2014 use: wasper up
8014
9063
 
8015
- wasper [--url <spec-url>] [--port <port>] Start in foreground (auto-resumes last spec)
8016
- wasper start --background Start in background
8017
- wasper stop Stop background server
8018
- wasper status Show server status
9064
+ wasper up [--url <spec>] Start daemon in background (default)
9065
+ wasper start [--url <spec>] Start in foreground with REPL
9066
+ wasper down Stop the daemon
9067
+ wasper status Show daemon status
9068
+ wasper logs [-f] Tail server logs
9069
+ wasper service install Install as system service (auto-start)
8019
9070
  wasper reload Hot-reload the spec
8020
9071
  wasper ls List saved specs (history)
8021
- wasper use <number|url> Start with a saved spec
8022
- wasper rm <number|url> Remove a spec from history
8023
9072
 
8024
9073
  Options:
8025
9074
  --url, -u OpenAPI spec URL or local path
@@ -8034,17 +9083,17 @@ Options:
8034
9083
  --no-proxy Start with the HTTP proxy disabled
8035
9084
  --no-ai Start with the AI chat endpoint disabled
8036
9085
  --readonly Block all non-GET upstream requests (agent guardrail)
8037
- --background, -b Start detached in background
9086
+ --background, -b Start detached in background (same as wasper up)
8038
9087
  --daemon, -d Same as --background
8039
9088
  -h, --help Show this help
8040
9089
 
8041
- Interactive mode supports slash commands \u2014 press / and type:
9090
+ Interactive REPL slash commands (foreground mode):
8042
9091
  /mcp on|off \xB7 /proxy on|off \xB7 /ai on|off \xB7 /readonly on|off
8043
9092
  /auth use <role> \xB7 /token new \xB7 /spec <url> \xB7 /tail \xB7 /help
8044
9093
 
8045
9094
  Self-hosting:
8046
- wasper start --url <spec> --origin https://api.example.com --token <secret> -b
8047
- Then open the studio with ?server=https://api.example.com&token=<secret>
9095
+ wasper up --url <spec> --origin https://api.example.com --token <secret>
9096
+ wasper service install --url <spec> --port 3388
8048
9097
  `);
8049
9098
  }
8050
9099
  function buildScalarHtml(title, req) {
@@ -8079,10 +9128,10 @@ function promptYN(question, defaultYes = true) {
8079
9128
  function claudeDesktopConfigPath() {
8080
9129
  const p = process.platform;
8081
9130
  if (p === "darwin")
8082
- return join4(homedir4(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
9131
+ return join5(homedir5(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
8083
9132
  if (p === "win32")
8084
- return join4(process.env.APPDATA ?? join4(homedir4(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
8085
- return join4(homedir4(), ".config", "Claude", "claude_desktop_config.json");
9133
+ return join5(process.env.APPDATA ?? join5(homedir5(), "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
9134
+ return join5(homedir5(), ".config", "Claude", "claude_desktop_config.json");
8086
9135
  }
8087
9136
  async function configureClaudeDesktop(mcpUrl) {
8088
9137
  const cfgPath = claudeDesktopConfigPath();
@@ -8156,98 +9205,6 @@ var init_start = __esm(() => {
8156
9205
  SPIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
8157
9206
  });
8158
9207
 
8159
- // src/commands/use.ts
8160
- var exports_use = {};
8161
- __export(exports_use, {
8162
- run: () => run7
8163
- });
8164
- async function run7() {
8165
- const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
8166
- const target = args[1];
8167
- if (!target) {
8168
- console.error(`
8169
- Usage: wasper use <number|url>
8170
- `);
8171
- process.exit(1);
8172
- }
8173
- const history = dbQueries.getSpecHistory();
8174
- let url = null;
8175
- const num = parseInt(target, 10);
8176
- if (!isNaN(num) && num >= 1 && num <= history.length) {
8177
- url = history[num - 1]?.url ?? null;
8178
- } else if (target.startsWith("http")) {
8179
- url = target;
8180
- } else {
8181
- const match = history.find((r) => r.title?.toLowerCase().includes(target.toLowerCase()));
8182
- if (match)
8183
- url = match.url;
8184
- }
8185
- if (!url) {
8186
- console.error(`
8187
- ${paint.red("\u2717")} Spec not found: ${target}
8188
- `);
8189
- console.error(` Run ${paint.cyan("wasper ls")} to see saved specs.
8190
- `);
8191
- process.exit(1);
8192
- }
8193
- console.log(`
8194
- ${paint.dim("\u2192")} Starting with ${paint.cyan(url)}
8195
- `);
8196
- const { run: startRun } = await Promise.resolve().then(() => (init_start(), exports_start));
8197
- await startRun({ url });
8198
- }
8199
- var init_use = __esm(() => {
8200
- init_db();
8201
- init_ui();
8202
- });
8203
-
8204
- // src/commands/rm.ts
8205
- var exports_rm = {};
8206
- __export(exports_rm, {
8207
- run: () => run8
8208
- });
8209
- async function run8() {
8210
- const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
8211
- const target = args[1];
8212
- if (!target) {
8213
- console.error(`
8214
- Usage: wasper rm <number|url>
8215
- `);
8216
- process.exit(1);
8217
- }
8218
- const history = dbQueries.getSpecHistory();
8219
- let id = null;
8220
- let label = null;
8221
- const num = parseInt(target, 10);
8222
- if (!isNaN(num) && num >= 1 && num <= history.length) {
8223
- const row = history[num - 1];
8224
- if (row) {
8225
- id = row.id;
8226
- label = row.title ?? row.url;
8227
- }
8228
- } else {
8229
- const match = history.find((r) => r.url === target || r.title?.toLowerCase().includes(target.toLowerCase()));
8230
- if (match) {
8231
- id = match.id;
8232
- label = match.title ?? match.url;
8233
- }
8234
- }
8235
- if (!id) {
8236
- console.error(`
8237
- ${paint.red("\u2717")} Spec not found: ${target}
8238
- `);
8239
- process.exit(1);
8240
- }
8241
- dbQueries.deleteSpec(id);
8242
- console.log(`
8243
- ${paint.green("\u2713")} Removed ${paint.dim(label ?? id)}
8244
- `);
8245
- }
8246
- var init_rm = __esm(() => {
8247
- init_db();
8248
- init_ui();
8249
- });
8250
-
8251
9208
  // cli.ts
8252
9209
  var rawArgs = process.argv.slice(2);
8253
9210
  if (rawArgs.includes("--version") || rawArgs.includes("-v")) {
@@ -8255,20 +9212,53 @@ if (rawArgs.includes("--version") || rawArgs.includes("-v")) {
8255
9212
  console.log(VERSION2);
8256
9213
  process.exit(0);
8257
9214
  }
8258
- var subcommand = rawArgs.find((a) => !a.startsWith("-")) ?? "start";
9215
+ var IS_DAEMON_CHILD = rawArgs.includes("--_daemon");
9216
+ var SUBCOMMANDS = new Set([
9217
+ "up",
9218
+ "down",
9219
+ "stop",
9220
+ "status",
9221
+ "ps",
9222
+ "reload",
9223
+ "logs",
9224
+ "ls",
9225
+ "list",
9226
+ "use",
9227
+ "rm",
9228
+ "remove",
9229
+ "mcp",
9230
+ "proxy",
9231
+ "ai",
9232
+ "readonly",
9233
+ "auth",
9234
+ "spec",
9235
+ "service",
9236
+ "update",
9237
+ "start",
9238
+ "help"
9239
+ ]);
9240
+ var firstPositional = rawArgs.find((a) => !a.startsWith("-"));
9241
+ var subcommand = firstPositional && SUBCOMMANDS.has(firstPositional) ? firstPositional : IS_DAEMON_CHILD ? "start" : "up";
8259
9242
  switch (subcommand) {
9243
+ case "up":
9244
+ await Promise.resolve().then(() => (init_up(), exports_up)).then((m) => m.run());
9245
+ break;
9246
+ case "ps":
9247
+ await Promise.resolve().then(() => (init_ps(), exports_ps)).then((m) => m.run());
9248
+ break;
9249
+ case "down":
8260
9250
  case "stop":
8261
9251
  await Promise.resolve().then(() => (init_stop(), exports_stop)).then((m) => m.run());
8262
9252
  break;
8263
- case "update":
8264
- await Promise.resolve().then(() => (init_update(), exports_update)).then((m) => m.run());
8265
- break;
8266
9253
  case "status":
8267
9254
  await Promise.resolve().then(() => (init_status(), exports_status)).then((m) => m.run());
8268
9255
  break;
8269
9256
  case "reload":
8270
9257
  await Promise.resolve().then(() => (init_reload(), exports_reload)).then((m) => m.run());
8271
9258
  break;
9259
+ case "logs":
9260
+ await Promise.resolve().then(() => (init_logs(), exports_logs)).then((m) => m.run());
9261
+ break;
8272
9262
  case "ls":
8273
9263
  case "list":
8274
9264
  await Promise.resolve().then(() => (init_ls(), exports_ls)).then((m) => m.run());
@@ -8280,6 +9270,27 @@ switch (subcommand) {
8280
9270
  case "remove":
8281
9271
  await Promise.resolve().then(() => (init_rm(), exports_rm)).then((m) => m.run());
8282
9272
  break;
9273
+ case "mcp":
9274
+ case "proxy":
9275
+ case "ai":
9276
+ case "readonly":
9277
+ await Promise.resolve().then(() => (init_feature(), exports_feature)).then((m) => m.run());
9278
+ break;
9279
+ case "auth":
9280
+ await Promise.resolve().then(() => (init_auth_cmd(), exports_auth_cmd)).then((m) => m.run());
9281
+ break;
9282
+ case "spec":
9283
+ await Promise.resolve().then(() => (init_spec_cmd(), exports_spec_cmd)).then((m) => m.run());
9284
+ break;
9285
+ case "service":
9286
+ await Promise.resolve().then(() => (init_service(), exports_service)).then((m) => m.run());
9287
+ break;
9288
+ case "update":
9289
+ await Promise.resolve().then(() => (init_update(), exports_update)).then((m) => m.run());
9290
+ break;
9291
+ case "help":
9292
+ await Promise.resolve().then(() => (init_help(), exports_help)).then((m) => m.run());
9293
+ break;
8283
9294
  case "start":
8284
9295
  default:
8285
9296
  await Promise.resolve().then(() => (init_start(), exports_start)).then((m) => m.run());