wasper-cli 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/dist/cli.js +1887 -641
  4. package/dist/index.js +1218 -128
  5. package/package.json +1 -1
package/dist/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.1.0",
24
+ version: "0.2.0",
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",
@@ -225,7 +225,7 @@ function printBanner(opts) {
225
225
  const hint = [
226
226
  `${paint.bold("r")} reload`,
227
227
  `${paint.bold("b")} background`,
228
- `${paint.bold("/")} commands`,
228
+ `${paint.bold("/")} commands ${paint.dim("(Tab to complete)")}`,
229
229
  `${paint.bold("q")} quit`,
230
230
  `${paint.bold("?")} help`
231
231
  ].join(` ${dot} `);
@@ -576,6 +576,346 @@ var init_reload = __esm(() => {
576
576
  init_ui();
577
577
  });
578
578
 
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
+
685
+ // src/db/index.ts
686
+ import { Database } from "bun:sqlite";
687
+ import { join as join3 } from "path";
688
+ import { mkdirSync, existsSync } from "fs";
689
+ import { homedir as homedir3 } from "os";
690
+ import { randomUUID } from "crypto";
691
+ function resolveDataDir() {
692
+ if (process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR) {
693
+ return process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR;
694
+ }
695
+ try {
696
+ const legacy = join3(import.meta.dir, "../../data");
697
+ if (!Bun.main.includes("$bunfs") && (existsSync(join3(legacy, "wasper.db")) || existsSync(join3(legacy, "openapi-agent.db"))))
698
+ return legacy;
699
+ } catch {}
700
+ const oldDir = join3(homedir3(), ".openapi-agent", "data");
701
+ if (existsSync(oldDir))
702
+ return oldDir;
703
+ return join3(homedir3(), ".wasper", "data");
704
+ }
705
+ var DATA_DIR, DB_PATH, db, hasOldSchema, dbQueries;
706
+ var init_db = __esm(() => {
707
+ DATA_DIR = resolveDataDir();
708
+ mkdirSync(DATA_DIR, { recursive: true });
709
+ DB_PATH = join3(DATA_DIR, existsSync(join3(DATA_DIR, "openapi-agent.db")) && !existsSync(join3(DATA_DIR, "wasper.db")) ? "openapi-agent.db" : "wasper.db");
710
+ db = new Database(DB_PATH, { create: true });
711
+ db.exec("PRAGMA journal_mode = WAL;");
712
+ db.exec("PRAGMA foreign_keys = OFF;");
713
+ hasOldSchema = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='specs'").get() !== null;
714
+ if (hasOldSchema) {
715
+ db.exec("DROP TABLE IF EXISTS tools; DROP TABLE IF EXISTS specs; DROP TABLE IF EXISTS auth_configs; DROP TABLE IF EXISTS request_logs;");
716
+ }
717
+ db.exec(SCHEMA);
718
+ dbQueries = {
719
+ getAuthConfig: () => db.query("SELECT * FROM auth_config WHERE id = 'default'").get(),
720
+ setAuthConfig: (type, config) => db.query(`INSERT INTO auth_config (id, type, config, updated_at)
721
+ VALUES ('default', ?, ?, unixepoch())
722
+ ON CONFLICT(id) DO UPDATE SET type = excluded.type, config = excluded.config, updated_at = unixepoch()`).run(type, JSON.stringify(config)),
723
+ updateTokenCache: (tokenCache) => db.query("UPDATE auth_config SET token_cache = ? WHERE id = 'default'").run(tokenCache ? JSON.stringify(tokenCache) : null),
724
+ getRecentLogs: (limit = 500) => db.query("SELECT * FROM request_logs ORDER BY created_at DESC LIMIT ?").all(limit),
725
+ insertLog: (data) => db.query(`INSERT INTO request_logs
726
+ (id, source, tool_name, method, url, request_headers, request_body,
727
+ status_code, response_headers, response_body, latency_ms, error)
728
+ VALUES ($id, $source, $tool_name, $method, $url, $request_headers, $request_body,
729
+ $status_code, $response_headers, $response_body, $latency_ms, $error)`).run({
730
+ $id: data.id,
731
+ $source: data.source,
732
+ $tool_name: data.tool_name,
733
+ $method: data.method,
734
+ $url: data.url,
735
+ $request_headers: data.request_headers,
736
+ $request_body: data.request_body,
737
+ $status_code: data.status_code,
738
+ $response_headers: data.response_headers,
739
+ $response_body: data.response_body,
740
+ $latency_ms: data.latency_ms,
741
+ $error: data.error
742
+ }),
743
+ clearLogs: () => db.query("DELETE FROM request_logs").run(),
744
+ getRules: () => db.query("SELECT * FROM intercept_rules ORDER BY sort_order, created_at").all(),
745
+ 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)
746
+ VALUES ($id,$enabled,$name,$sort_order,$match_path,$match_method,$target_host,$strip_prefix,$add_prefix,$add_headers)`).run({
747
+ $id: rule.id,
748
+ $enabled: rule.enabled,
749
+ $name: rule.name,
750
+ $sort_order: rule.sort_order,
751
+ $match_path: rule.match_path,
752
+ $match_method: rule.match_method,
753
+ $target_host: rule.target_host,
754
+ $strip_prefix: rule.strip_prefix,
755
+ $add_prefix: rule.add_prefix,
756
+ $add_headers: rule.add_headers
757
+ }),
758
+ updateRule: (id, patch) => {
759
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
760
+ const params = { $id: id };
761
+ for (const [k, v] of Object.entries(patch))
762
+ params[`$${k}`] = v;
763
+ db.query(`UPDATE intercept_rules SET ${cols} WHERE id = $id`).run(params);
764
+ },
765
+ deleteRule: (id) => db.query("DELETE FROM intercept_rules WHERE id = ?").run(id),
766
+ getSettings: () => db.query("SELECT value FROM settings WHERE key='app' LIMIT 1").get() ?? null,
767
+ setSettings: (value) => {
768
+ db.run("INSERT INTO settings(key,value) VALUES('app',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [JSON.stringify(value)]);
769
+ },
770
+ getProfiles: () => db.query("SELECT * FROM auth_profiles ORDER BY name COLLATE NOCASE").all(),
771
+ getActiveProfile: () => db.query("SELECT * FROM auth_profiles WHERE is_active = 1 LIMIT 1").get(),
772
+ insertProfile: (p) => db.query(`INSERT INTO auth_profiles (id,name,description,type,config,token_cache,is_active)
773
+ VALUES ($id,$name,$description,$type,$config,$token_cache,$is_active)`).run({
774
+ $id: p.id,
775
+ $name: p.name,
776
+ $description: p.description,
777
+ $type: p.type,
778
+ $config: p.config,
779
+ $token_cache: p.token_cache,
780
+ $is_active: p.is_active
781
+ }),
782
+ updateProfile: (id, patch) => {
783
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
784
+ const params = { $id: id };
785
+ for (const [k, v] of Object.entries(patch))
786
+ params[`$${k}`] = v;
787
+ db.query(`UPDATE auth_profiles SET ${cols} WHERE id = $id`).run(params);
788
+ },
789
+ deleteProfile: (id) => db.query("DELETE FROM auth_profiles WHERE id = ?").run(id),
790
+ activateProfile: (id) => {
791
+ const profile = db.query("SELECT * FROM auth_profiles WHERE id = ?").get(id);
792
+ if (!profile)
793
+ return;
794
+ db.query("UPDATE auth_profiles SET is_active = 0").run();
795
+ db.query("UPDATE auth_profiles SET is_active = 1 WHERE id = ?").run(id);
796
+ db.query(`INSERT INTO auth_config (id, type, config, updated_at) VALUES ('default', $type, $config, unixepoch())
797
+ ON CONFLICT(id) DO UPDATE SET type=excluded.type, config=excluded.config, updated_at=unixepoch()`).run({ $type: profile.type, $config: profile.config });
798
+ },
799
+ getSavedRequests: () => db.query("SELECT * FROM saved_requests ORDER BY folder, name COLLATE NOCASE").all(),
800
+ getSavedRequest: (id) => db.query("SELECT * FROM saved_requests WHERE id = ?").get(id),
801
+ insertSavedRequest: (r) => db.query(`INSERT INTO saved_requests
802
+ (id, name, folder, method, url, headers, params, body, body_type, raw_type, form_rows, auth, notes)
803
+ VALUES ($id,$name,$folder,$method,$url,$headers,$params,$body,$body_type,$raw_type,$form_rows,$auth,$notes)`).run({
804
+ $id: r.id,
805
+ $name: r.name,
806
+ $folder: r.folder,
807
+ $method: r.method,
808
+ $url: r.url,
809
+ $headers: r.headers,
810
+ $params: r.params,
811
+ $body: r.body,
812
+ $body_type: r.body_type,
813
+ $raw_type: r.raw_type,
814
+ $form_rows: r.form_rows,
815
+ $auth: r.auth,
816
+ $notes: r.notes
817
+ }),
818
+ updateSavedRequest: (id, patch) => {
819
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
820
+ const params = { $id: id };
821
+ for (const [k, v] of Object.entries(patch))
822
+ params[`$${k}`] = v;
823
+ db.query(`UPDATE saved_requests SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
824
+ },
825
+ deleteSavedRequest: (id) => db.query("DELETE FROM saved_requests WHERE id = ?").run(id),
826
+ getSpecHistory: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC").all(),
827
+ getLastSpec: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC LIMIT 1").get(),
828
+ upsertSpec: (url, title, version, endpointCount) => {
829
+ const existing = db.query("SELECT id FROM spec_history WHERE url = ?").get(url);
830
+ if (existing) {
831
+ db.query("UPDATE spec_history SET title=?, version=?, endpoint_count=?, last_used=unixepoch() WHERE url=?").run(title, version, endpointCount, url);
832
+ } else {
833
+ db.query(`INSERT INTO spec_history (id, url, title, version, endpoint_count)
834
+ VALUES (?, ?, ?, ?, ?)`).run(randomUUID(), url, title, version, endpointCount);
835
+ }
836
+ },
837
+ deleteSpec: (id) => {
838
+ db.query("DELETE FROM spec_history WHERE id = ?").run(id);
839
+ },
840
+ getWorkflows: () => db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all(),
841
+ getWorkflow: (id) => db.query("SELECT * FROM workflows WHERE id = ?").get(id),
842
+ 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 }),
843
+ updateWorkflow: (id, patch) => {
844
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
845
+ const params = { $id: id };
846
+ for (const [k, v] of Object.entries(patch))
847
+ params[`$${k}`] = v;
848
+ db.query(`UPDATE workflows SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
849
+ },
850
+ deleteWorkflow: (id) => db.query("DELETE FROM workflows WHERE id = ?").run(id),
851
+ getCaptureBins: () => db.query("SELECT * FROM capture_bins ORDER BY created_at DESC").all(),
852
+ getCaptureBin: (id) => db.query("SELECT * FROM capture_bins WHERE id = ?").get(id),
853
+ insertCaptureBin: (id, name) => {
854
+ db.query("INSERT INTO capture_bins (id, name) VALUES (?, ?)").run(id, name);
855
+ },
856
+ deleteCaptureBin: (id) => {
857
+ db.query("DELETE FROM capture_bins WHERE id = ?").run(id);
858
+ },
859
+ getSetting: (key) => (db.query("SELECT value FROM settings WHERE key = ?").get(key) ?? null)?.value ?? null,
860
+ setSetting: (key, value) => {
861
+ db.run("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
862
+ }
863
+ };
864
+ });
865
+
866
+ // src/commands/ls.ts
867
+ var exports_ls = {};
868
+ __export(exports_ls, {
869
+ run: () => run5
870
+ });
871
+ function ago(ts) {
872
+ const secs = Math.floor(Date.now() / 1000) - ts;
873
+ if (secs < 60)
874
+ return "just now";
875
+ if (secs < 3600)
876
+ return `${Math.floor(secs / 60)}m ago`;
877
+ if (secs < 86400)
878
+ return `${Math.floor(secs / 3600)}h ago`;
879
+ if (secs < 86400 * 30)
880
+ return `${Math.floor(secs / 86400)}d ago`;
881
+ return new Date(ts * 1000).toLocaleDateString();
882
+ }
883
+ async function run5() {
884
+ const history = dbQueries.getSpecHistory();
885
+ if (history.length === 0) {
886
+ console.log(`
887
+ No saved specs yet.
888
+ `);
889
+ console.log(` ${paint.dim("Run:")} wasper --url <spec-url>
890
+ `);
891
+ process.exit(0);
892
+ }
893
+ const COL_URL = isTTY ? 50 : 60;
894
+ const COL_TITLE = 28;
895
+ console.log();
896
+ if (isTTY) {
897
+ const header = ` ${"#".padEnd(3)} ${"URL".padEnd(COL_URL)} ${"Title".padEnd(COL_TITLE)} ${"Endpoints".padEnd(10)} Last used`;
898
+ console.log(paint.dim(header));
899
+ console.log(paint.dim(" " + "\u2500".repeat(header.length - 2)));
900
+ }
901
+ history.forEach((row, i) => {
902
+ const num = String(i + 1).padEnd(3);
903
+ const url = row.url.length > COL_URL ? row.url.slice(0, COL_URL - 1) + "\u2026" : row.url.padEnd(COL_URL);
904
+ const title = (row.title ?? "\u2014").slice(0, COL_TITLE).padEnd(COL_TITLE);
905
+ const eps = (row.endpoint_count != null ? String(row.endpoint_count) : "\u2014").padEnd(10);
906
+ const time = ago(row.last_used);
907
+ console.log(` ${paint.cyan(num)} ${url} ${paint.dim(title)} ${eps} ${paint.dim(time)}`);
908
+ });
909
+ console.log();
910
+ console.log(paint.dim(` wasper use <number> \u2014 start server with that spec`));
911
+ console.log(paint.dim(` wasper rm <number> \u2014 remove a spec from history`));
912
+ console.log();
913
+ }
914
+ var init_ls = __esm(() => {
915
+ init_db();
916
+ init_ui();
917
+ });
918
+
579
919
  // ../../node_modules/js-yaml/dist/js-yaml.mjs
580
920
  var __create, __defProp2, __getOwnPropDesc, __getOwnPropNames, __getProtoOf, __hasOwnProp, __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports), __copyProps = (to, from, except, desc) => {
581
921
  if (from && typeof from === "object" || typeof from === "function")
@@ -3258,502 +3598,361 @@ var init_js_yaml = __esm(() => {
3258
3598
  const index = objects.indexOf(object);
3259
3599
  if (index !== -1) {
3260
3600
  if (duplicatesIndexes.indexOf(index) === -1)
3261
- duplicatesIndexes.push(index);
3262
- } else {
3263
- objects.push(object);
3264
- if (Array.isArray(object))
3265
- for (let i = 0, length = object.length;i < length; i += 1)
3266
- inspectNode(object[i], objects, duplicatesIndexes);
3267
- else {
3268
- const objectKeyList = Object.keys(object);
3269
- for (let i = 0, length = objectKeyList.length;i < length; i += 1)
3270
- inspectNode(object[objectKeyList[i]], objects, duplicatesIndexes);
3271
- }
3272
- }
3273
- }
3274
- }
3275
- function dump(input, options) {
3276
- options = options || {};
3277
- const state = new State(options);
3278
- if (!state.noRefs)
3279
- getDuplicateReferences(input, state);
3280
- let value = input;
3281
- if (state.replacer)
3282
- value = state.replacer.call({ "": value }, "", value);
3283
- if (writeNode(state, 0, value, true, true))
3284
- return state.dump + `
3285
- `;
3286
- return "";
3287
- }
3288
- module.exports.dump = dump;
3289
- });
3290
- import_js_yaml = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin((exports, module) => {
3291
- var loader = require_loader();
3292
- var dumper = require_dumper();
3293
- function renamed(from, to) {
3294
- return function() {
3295
- throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
3296
- };
3297
- }
3298
- module.exports.Type = require_type();
3299
- module.exports.Schema = require_schema();
3300
- module.exports.FAILSAFE_SCHEMA = require_failsafe();
3301
- module.exports.JSON_SCHEMA = require_json();
3302
- module.exports.CORE_SCHEMA = require_core();
3303
- module.exports.DEFAULT_SCHEMA = require_default();
3304
- module.exports.load = loader.load;
3305
- module.exports.loadAll = loader.loadAll;
3306
- module.exports.dump = dumper.dump;
3307
- module.exports.YAMLException = require_exception();
3308
- module.exports.types = {
3309
- binary: require_binary(),
3310
- float: require_float(),
3311
- map: require_map(),
3312
- null: require_null(),
3313
- pairs: require_pairs(),
3314
- set: require_set(),
3315
- timestamp: require_timestamp(),
3316
- bool: require_bool(),
3317
- int: require_int(),
3318
- merge: require_merge(),
3319
- omap: require_omap(),
3320
- seq: require_seq(),
3321
- str: require_str()
3322
- };
3323
- module.exports.safeLoad = renamed("safeLoad", "load");
3324
- module.exports.safeLoadAll = renamed("safeLoadAll", "loadAll");
3325
- module.exports.safeDump = renamed("safeDump", "dump");
3326
- }))(), 1);
3327
- ({ Type, Schema, FAILSAFE_SCHEMA, JSON_SCHEMA, CORE_SCHEMA, DEFAULT_SCHEMA, load, loadAll, dump, YAMLException, types, safeLoad, safeLoadAll, safeDump } = import_js_yaml.default);
3328
- index_vite_proxy_tmp_default = import_js_yaml.default;
3329
- });
3330
-
3331
- // src/openapi/parser.ts
3332
- import { randomUUID } from "crypto";
3333
- async function fetchAndParseSpec(url, name) {
3334
- const res = await fetch(url, { headers: { Accept: "application/json, application/yaml, text/yaml, */*" } });
3335
- if (!res.ok)
3336
- throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
3337
- return parseSpecText(await res.text(), url, name);
3338
- }
3339
- function parseSpecText(text, url, name) {
3340
- let raw;
3341
- try {
3342
- raw = text.trimStart().startsWith("{") ? JSON.parse(text) : index_vite_proxy_tmp_default.load(text);
3343
- } catch {
3344
- throw new Error("Invalid OpenAPI spec: could not parse as JSON or YAML");
3345
- }
3346
- const doc = deref(raw, raw);
3347
- const info = doc.info ?? {};
3348
- const servers = doc.servers ?? [];
3349
- let baseUrl = servers[0]?.url ?? "";
3350
- if (!baseUrl && url) {
3351
- try {
3352
- baseUrl = new URL(url).origin;
3353
- } catch {}
3354
- }
3355
- if (baseUrl && !baseUrl.startsWith("http") && url) {
3356
- try {
3357
- baseUrl = new URL(baseUrl, url).href.replace(/\/$/, "");
3358
- } catch {}
3359
- }
3360
- return {
3361
- id: randomUUID(),
3362
- name: name ?? (info.title ?? "Untitled API"),
3363
- url: url ?? null,
3364
- raw: JSON.stringify(raw),
3365
- title: info.title ?? "Untitled API",
3366
- version: info.version ?? "1.0.0",
3367
- baseUrl,
3368
- operations: extractOperations(doc),
3369
- securitySchemes: extractSecuritySchemes(doc)
3370
- };
3371
- }
3372
- function deref(node, root, depth = 0) {
3373
- if (depth > 20 || node === null || typeof node !== "object")
3374
- return node;
3375
- if (Array.isArray(node))
3376
- return node.map((item) => deref(item, root, depth + 1));
3377
- const obj = node;
3378
- if (typeof obj["$ref"] === "string" && obj["$ref"].startsWith("#/")) {
3379
- return deref(resolveRef(obj["$ref"], root), root, depth + 1);
3380
- }
3381
- const result = {};
3382
- for (const [k, v] of Object.entries(obj)) {
3383
- result[k] = deref(v, root, depth + 1);
3384
- }
3385
- return result;
3386
- }
3387
- function resolveRef(ref, root) {
3388
- let current = root;
3389
- for (const part of ref.slice(2).split("/")) {
3390
- const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
3391
- if (current && typeof current === "object" && !Array.isArray(current)) {
3392
- current = current[key];
3393
- } else
3394
- return;
3395
- }
3396
- return current;
3397
- }
3398
- function extractOperations(doc) {
3399
- const paths = doc.paths ?? {};
3400
- const ops = [];
3401
- let idx = 0;
3402
- for (const [pathStr, pathItem] of Object.entries(paths)) {
3403
- if (!pathItem || typeof pathItem !== "object")
3404
- continue;
3405
- const pi = pathItem;
3406
- const pathParams = parseParameters(pi.parameters ?? []);
3407
- for (const method of HTTP_METHODS) {
3408
- const opObj = pi[method];
3409
- if (!opObj)
3410
- continue;
3411
- const rawId = opObj.operationId ?? `${method}_${pathStr.replace(/[^a-zA-Z0-9]/g, "_")}_${idx++}`;
3412
- const operationId = rawId.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
3413
- const opParams = parseParameters(opObj.parameters ?? []);
3414
- const paramMap = new Map;
3415
- for (const p of [...pathParams, ...opParams])
3416
- paramMap.set(`${p.in}:${p.name}`, p);
3417
- ops.push({
3418
- operationId,
3419
- method,
3420
- path: pathStr,
3421
- summary: opObj.summary,
3422
- description: opObj.description,
3423
- tags: opObj.tags ?? ["default"],
3424
- parameters: [...paramMap.values()],
3425
- requestBody: parseRequestBody(opObj.requestBody),
3426
- responses: parseResponses(opObj.responses ?? {}),
3427
- security: opObj.security
3428
- });
3601
+ duplicatesIndexes.push(index);
3602
+ } else {
3603
+ objects.push(object);
3604
+ if (Array.isArray(object))
3605
+ for (let i = 0, length = object.length;i < length; i += 1)
3606
+ inspectNode(object[i], objects, duplicatesIndexes);
3607
+ else {
3608
+ const objectKeyList = Object.keys(object);
3609
+ for (let i = 0, length = objectKeyList.length;i < length; i += 1)
3610
+ inspectNode(object[objectKeyList[i]], objects, duplicatesIndexes);
3611
+ }
3612
+ }
3613
+ }
3429
3614
  }
3430
- }
3431
- return ops;
3432
- }
3433
- function parseParameters(params) {
3434
- return params.flatMap((p) => {
3435
- if (!p || typeof p !== "object")
3436
- return [];
3437
- const param = p;
3438
- const name = param.name;
3439
- const paramIn = param.in;
3440
- if (!name || !paramIn || !["path", "query", "header", "cookie"].includes(paramIn))
3441
- return [];
3442
- return [{
3443
- name,
3444
- in: paramIn,
3445
- description: param.description,
3446
- required: param.required ?? paramIn === "path",
3447
- schema: param.schema ?? { type: "string" }
3448
- }];
3615
+ function dump(input, options) {
3616
+ options = options || {};
3617
+ const state = new State(options);
3618
+ if (!state.noRefs)
3619
+ getDuplicateReferences(input, state);
3620
+ let value = input;
3621
+ if (state.replacer)
3622
+ value = state.replacer.call({ "": value }, "", value);
3623
+ if (writeNode(state, 0, value, true, true))
3624
+ return state.dump + `
3625
+ `;
3626
+ return "";
3627
+ }
3628
+ module.exports.dump = dump;
3449
3629
  });
3450
- }
3451
- function parseRequestBody(rb) {
3452
- if (!rb || typeof rb !== "object")
3453
- return;
3454
- const body = rb;
3455
- const content = body.content ?? {};
3456
- const contentType = Object.keys(content).find((ct) => ct.includes("json")) ?? Object.keys(content)[0];
3457
- if (!contentType)
3458
- return;
3459
- return {
3460
- description: body.description,
3461
- required: body.required ?? false,
3462
- contentType,
3463
- schema: content[contentType]?.schema ?? { type: "object" }
3464
- };
3465
- }
3466
- function parseResponses(responses) {
3467
- const result = {};
3468
- for (const [code, resp] of Object.entries(responses)) {
3469
- if (!resp || typeof resp !== "object")
3470
- continue;
3471
- const r = resp;
3472
- const content = r.content;
3473
- const jsonCt = content ? Object.keys(content).find((ct) => ct.includes("json")) : undefined;
3474
- result[code] = {
3475
- description: r.description,
3476
- contentType: jsonCt,
3477
- schema: jsonCt ? content?.[jsonCt]?.schema : undefined
3478
- };
3479
- }
3480
- return result;
3481
- }
3482
- function extractSecuritySchemes(doc) {
3483
- const schemes = doc.components?.securitySchemes;
3484
- if (!schemes)
3485
- return {};
3486
- const result = {};
3487
- for (const [name, scheme] of Object.entries(schemes)) {
3488
- if (!scheme || typeof scheme !== "object")
3489
- continue;
3490
- const s = scheme;
3491
- result[name] = {
3492
- type: s.type,
3493
- scheme: s.scheme,
3494
- in: s.in,
3495
- name: s.name,
3496
- flows: s.flows,
3497
- openIdConnectUrl: s.openIdConnectUrl
3630
+ import_js_yaml = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin((exports, module) => {
3631
+ var loader = require_loader();
3632
+ var dumper = require_dumper();
3633
+ function renamed(from, to) {
3634
+ return function() {
3635
+ throw new Error("Function yaml." + from + " is removed in js-yaml 4. Use yaml." + to + " instead, which is now safe by default.");
3636
+ };
3637
+ }
3638
+ module.exports.Type = require_type();
3639
+ module.exports.Schema = require_schema();
3640
+ module.exports.FAILSAFE_SCHEMA = require_failsafe();
3641
+ module.exports.JSON_SCHEMA = require_json();
3642
+ module.exports.CORE_SCHEMA = require_core();
3643
+ module.exports.DEFAULT_SCHEMA = require_default();
3644
+ module.exports.load = loader.load;
3645
+ module.exports.loadAll = loader.loadAll;
3646
+ module.exports.dump = dumper.dump;
3647
+ module.exports.YAMLException = require_exception();
3648
+ module.exports.types = {
3649
+ binary: require_binary(),
3650
+ float: require_float(),
3651
+ map: require_map(),
3652
+ null: require_null(),
3653
+ pairs: require_pairs(),
3654
+ set: require_set(),
3655
+ timestamp: require_timestamp(),
3656
+ bool: require_bool(),
3657
+ int: require_int(),
3658
+ merge: require_merge(),
3659
+ omap: require_omap(),
3660
+ seq: require_seq(),
3661
+ str: require_str()
3498
3662
  };
3499
- }
3500
- return result;
3501
- }
3502
- var HTTP_METHODS;
3503
- var init_parser = __esm(() => {
3504
- init_js_yaml();
3505
- HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
3506
- });
3507
-
3508
- // src/state.ts
3509
- function hasState() {
3510
- return _state !== null;
3511
- }
3512
- function getState() {
3513
- if (!_state)
3514
- throw new Error("No spec loaded");
3515
- return _state;
3516
- }
3517
- async function loadSpec(url) {
3518
- const spec = await fetchAndParseSpec(url);
3519
- _state = { spec, operations: spec.operations, specUrl: url };
3520
- return _state;
3521
- }
3522
- function loadSpecFromText(text, filename) {
3523
- const spec = parseSpecText(text, undefined, filename?.replace(/\.[^.]+$/, ""));
3524
- _state = { spec, operations: spec.operations };
3525
- return _state;
3526
- }
3527
- var _state = null;
3528
- var init_state = __esm(() => {
3529
- init_parser();
3663
+ module.exports.safeLoad = renamed("safeLoad", "load");
3664
+ module.exports.safeLoadAll = renamed("safeLoadAll", "loadAll");
3665
+ module.exports.safeDump = renamed("safeDump", "dump");
3666
+ }))(), 1);
3667
+ ({ Type, Schema, FAILSAFE_SCHEMA, JSON_SCHEMA, CORE_SCHEMA, DEFAULT_SCHEMA, load, loadAll, dump, YAMLException, types, safeLoad, safeLoadAll, safeDump } = import_js_yaml.default);
3668
+ index_vite_proxy_tmp_default = import_js_yaml.default;
3530
3669
  });
3531
3670
 
3532
- // src/db/schema.ts
3533
- var SCHEMA = `
3534
- CREATE TABLE IF NOT EXISTS auth_config (
3535
- id TEXT PRIMARY KEY DEFAULT 'default',
3536
- type TEXT NOT NULL DEFAULT 'none',
3537
- config TEXT NOT NULL DEFAULT '{}',
3538
- token_cache TEXT,
3539
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
3540
- );
3541
-
3542
- CREATE TABLE IF NOT EXISTS request_logs (
3543
- id TEXT PRIMARY KEY,
3544
- source TEXT NOT NULL DEFAULT 'mcp',
3545
- tool_name TEXT,
3546
- method TEXT NOT NULL,
3547
- url TEXT NOT NULL,
3548
- request_headers TEXT,
3549
- request_body TEXT,
3550
- status_code INTEGER,
3551
- response_headers TEXT,
3552
- response_body TEXT,
3553
- latency_ms INTEGER,
3554
- error TEXT,
3555
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
3556
- );
3557
-
3558
- CREATE INDEX IF NOT EXISTS idx_logs_created ON request_logs(created_at DESC);
3559
-
3560
- CREATE TABLE IF NOT EXISTS settings (
3561
- key TEXT PRIMARY KEY,
3562
- value TEXT NOT NULL DEFAULT '{}'
3563
- );
3564
-
3565
- CREATE TABLE IF NOT EXISTS intercept_rules (
3566
- id TEXT PRIMARY KEY,
3567
- enabled INTEGER NOT NULL DEFAULT 1,
3568
- name TEXT NOT NULL DEFAULT '',
3569
- sort_order INTEGER NOT NULL DEFAULT 0,
3570
- match_path TEXT NOT NULL DEFAULT '',
3571
- match_method TEXT NOT NULL DEFAULT '',
3572
- target_host TEXT NOT NULL DEFAULT '',
3573
- strip_prefix TEXT NOT NULL DEFAULT '',
3574
- add_prefix TEXT NOT NULL DEFAULT '',
3575
- add_headers TEXT NOT NULL DEFAULT '{}',
3576
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
3577
- );
3578
- CREATE INDEX IF NOT EXISTS idx_rules_order ON intercept_rules(sort_order, created_at);
3579
-
3580
- CREATE TABLE IF NOT EXISTS auth_profiles (
3581
- id TEXT PRIMARY KEY,
3582
- name TEXT NOT NULL,
3583
- description TEXT NOT NULL DEFAULT '',
3584
- type TEXT NOT NULL DEFAULT 'none',
3585
- config TEXT NOT NULL DEFAULT '{}',
3586
- token_cache TEXT,
3587
- is_active INTEGER NOT NULL DEFAULT 0,
3588
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
3589
- );
3590
-
3591
- CREATE TABLE IF NOT EXISTS saved_requests (
3592
- id TEXT PRIMARY KEY,
3593
- name TEXT NOT NULL DEFAULT 'Untitled',
3594
- folder TEXT NOT NULL DEFAULT '',
3595
- method TEXT NOT NULL DEFAULT 'GET',
3596
- url TEXT NOT NULL DEFAULT '',
3597
- headers TEXT NOT NULL DEFAULT '[]',
3598
- params TEXT NOT NULL DEFAULT '[]',
3599
- body TEXT NOT NULL DEFAULT '',
3600
- body_type TEXT NOT NULL DEFAULT 'none',
3601
- raw_type TEXT NOT NULL DEFAULT 'text/plain',
3602
- form_rows TEXT NOT NULL DEFAULT '[]',
3603
- auth TEXT NOT NULL DEFAULT '{}',
3604
- notes TEXT NOT NULL DEFAULT '',
3605
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
3606
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
3607
- );
3608
- CREATE INDEX IF NOT EXISTS idx_saved_folder ON saved_requests(folder, created_at DESC);
3609
- `;
3610
-
3611
- // src/db/index.ts
3612
- import { Database } from "bun:sqlite";
3613
- import { join as join3 } from "path";
3614
- import { mkdirSync, existsSync } from "fs";
3615
- import { homedir as homedir3 } from "os";
3671
+ // src/openapi/parser.ts
3616
3672
  import { randomUUID as randomUUID2 } from "crypto";
3617
- function resolveDataDir() {
3618
- if (process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR) {
3619
- return process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR;
3620
- }
3673
+ async function fetchAndParseSpec(url, name) {
3674
+ const res = await fetch(url, { headers: { Accept: "application/json, application/yaml, text/yaml, */*" } });
3675
+ if (!res.ok)
3676
+ throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
3677
+ return parseSpecText(await res.text(), url, name);
3678
+ }
3679
+ function parseSpecText(text, url, name) {
3680
+ let raw;
3621
3681
  try {
3622
- const legacy = join3(import.meta.dir, "../../data");
3623
- if (!Bun.main.includes("$bunfs") && (existsSync(join3(legacy, "wasper.db")) || existsSync(join3(legacy, "openapi-agent.db"))))
3624
- return legacy;
3625
- } catch {}
3626
- const oldDir = join3(homedir3(), ".openapi-agent", "data");
3627
- if (existsSync(oldDir))
3628
- return oldDir;
3629
- return join3(homedir3(), ".wasper", "data");
3682
+ raw = text.trimStart().startsWith("{") ? JSON.parse(text) : index_vite_proxy_tmp_default.load(text);
3683
+ } catch {
3684
+ throw new Error("Invalid OpenAPI spec: could not parse as JSON or YAML");
3685
+ }
3686
+ const doc = deref(raw, raw);
3687
+ const info = doc.info ?? {};
3688
+ const servers = doc.servers ?? [];
3689
+ let baseUrl = servers[0]?.url ?? "";
3690
+ if (!baseUrl && url) {
3691
+ try {
3692
+ baseUrl = new URL(url).origin;
3693
+ } catch {}
3694
+ }
3695
+ if (baseUrl && !baseUrl.startsWith("http") && url) {
3696
+ try {
3697
+ baseUrl = new URL(baseUrl, url).href.replace(/\/$/, "");
3698
+ } catch {}
3699
+ }
3700
+ return {
3701
+ id: randomUUID2(),
3702
+ name: name ?? (info.title ?? "Untitled API"),
3703
+ url: url ?? null,
3704
+ raw: JSON.stringify(raw),
3705
+ title: info.title ?? "Untitled API",
3706
+ version: info.version ?? "1.0.0",
3707
+ baseUrl,
3708
+ operations: extractOperations(doc),
3709
+ securitySchemes: extractSecuritySchemes(doc)
3710
+ };
3630
3711
  }
3631
- var DATA_DIR, DB_PATH, db, hasOldSchema, dbQueries;
3632
- var init_db = __esm(() => {
3633
- DATA_DIR = resolveDataDir();
3634
- mkdirSync(DATA_DIR, { recursive: true });
3635
- DB_PATH = join3(DATA_DIR, existsSync(join3(DATA_DIR, "openapi-agent.db")) && !existsSync(join3(DATA_DIR, "wasper.db")) ? "openapi-agent.db" : "wasper.db");
3636
- db = new Database(DB_PATH, { create: true });
3637
- db.exec("PRAGMA journal_mode = WAL;");
3638
- db.exec("PRAGMA foreign_keys = OFF;");
3639
- hasOldSchema = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='specs'").get() !== null;
3640
- if (hasOldSchema) {
3641
- db.exec("DROP TABLE IF EXISTS tools; DROP TABLE IF EXISTS specs; DROP TABLE IF EXISTS auth_configs; DROP TABLE IF EXISTS request_logs;");
3712
+ function deref(node, root, depth = 0) {
3713
+ if (depth > 20 || node === null || typeof node !== "object")
3714
+ return node;
3715
+ if (Array.isArray(node))
3716
+ return node.map((item) => deref(item, root, depth + 1));
3717
+ const obj = node;
3718
+ if (typeof obj["$ref"] === "string" && obj["$ref"].startsWith("#/")) {
3719
+ return deref(resolveRef(obj["$ref"], root), root, depth + 1);
3642
3720
  }
3643
- db.exec(SCHEMA);
3644
- dbQueries = {
3645
- getAuthConfig: () => db.query("SELECT * FROM auth_config WHERE id = 'default'").get(),
3646
- setAuthConfig: (type, config) => db.query(`INSERT INTO auth_config (id, type, config, updated_at)
3647
- VALUES ('default', ?, ?, unixepoch())
3648
- ON CONFLICT(id) DO UPDATE SET type = excluded.type, config = excluded.config, updated_at = unixepoch()`).run(type, JSON.stringify(config)),
3649
- updateTokenCache: (tokenCache) => db.query("UPDATE auth_config SET token_cache = ? WHERE id = 'default'").run(tokenCache ? JSON.stringify(tokenCache) : null),
3650
- getRecentLogs: (limit = 500) => db.query("SELECT * FROM request_logs ORDER BY created_at DESC LIMIT ?").all(limit),
3651
- insertLog: (data) => db.query(`INSERT INTO request_logs
3652
- (id, source, tool_name, method, url, request_headers, request_body,
3653
- status_code, response_headers, response_body, latency_ms, error)
3654
- VALUES ($id, $source, $tool_name, $method, $url, $request_headers, $request_body,
3655
- $status_code, $response_headers, $response_body, $latency_ms, $error)`).run({
3656
- $id: data.id,
3657
- $source: data.source,
3658
- $tool_name: data.tool_name,
3659
- $method: data.method,
3660
- $url: data.url,
3661
- $request_headers: data.request_headers,
3662
- $request_body: data.request_body,
3663
- $status_code: data.status_code,
3664
- $response_headers: data.response_headers,
3665
- $response_body: data.response_body,
3666
- $latency_ms: data.latency_ms,
3667
- $error: data.error
3668
- }),
3669
- clearLogs: () => db.query("DELETE FROM request_logs").run(),
3670
- getRules: () => db.query("SELECT * FROM intercept_rules ORDER BY sort_order, created_at").all(),
3671
- 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)
3672
- VALUES ($id,$enabled,$name,$sort_order,$match_path,$match_method,$target_host,$strip_prefix,$add_prefix,$add_headers)`).run({
3673
- $id: rule.id,
3674
- $enabled: rule.enabled,
3675
- $name: rule.name,
3676
- $sort_order: rule.sort_order,
3677
- $match_path: rule.match_path,
3678
- $match_method: rule.match_method,
3679
- $target_host: rule.target_host,
3680
- $strip_prefix: rule.strip_prefix,
3681
- $add_prefix: rule.add_prefix,
3682
- $add_headers: rule.add_headers
3683
- }),
3684
- updateRule: (id, patch) => {
3685
- const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
3686
- const params = { $id: id };
3687
- for (const [k, v] of Object.entries(patch))
3688
- params[`$${k}`] = v;
3689
- db.query(`UPDATE intercept_rules SET ${cols} WHERE id = $id`).run(params);
3690
- },
3691
- deleteRule: (id) => db.query("DELETE FROM intercept_rules WHERE id = ?").run(id),
3692
- getSettings: () => db.query("SELECT value FROM settings WHERE key='app' LIMIT 1").get() ?? null,
3693
- setSettings: (value) => {
3694
- db.run("INSERT INTO settings(key,value) VALUES('app',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [JSON.stringify(value)]);
3695
- },
3696
- getProfiles: () => db.query("SELECT * FROM auth_profiles ORDER BY name COLLATE NOCASE").all(),
3697
- getActiveProfile: () => db.query("SELECT * FROM auth_profiles WHERE is_active = 1 LIMIT 1").get(),
3698
- insertProfile: (p) => db.query(`INSERT INTO auth_profiles (id,name,description,type,config,token_cache,is_active)
3699
- VALUES ($id,$name,$description,$type,$config,$token_cache,$is_active)`).run({
3700
- $id: p.id,
3701
- $name: p.name,
3702
- $description: p.description,
3703
- $type: p.type,
3704
- $config: p.config,
3705
- $token_cache: p.token_cache,
3706
- $is_active: p.is_active
3707
- }),
3708
- updateProfile: (id, patch) => {
3709
- const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
3710
- const params = { $id: id };
3711
- for (const [k, v] of Object.entries(patch))
3712
- params[`$${k}`] = v;
3713
- db.query(`UPDATE auth_profiles SET ${cols} WHERE id = $id`).run(params);
3714
- },
3715
- deleteProfile: (id) => db.query("DELETE FROM auth_profiles WHERE id = ?").run(id),
3716
- activateProfile: (id) => {
3717
- const profile = db.query("SELECT * FROM auth_profiles WHERE id = ?").get(id);
3718
- if (!profile)
3719
- return;
3720
- db.query("UPDATE auth_profiles SET is_active = 0").run();
3721
- db.query("UPDATE auth_profiles SET is_active = 1 WHERE id = ?").run(id);
3722
- db.query(`INSERT INTO auth_config (id, type, config, updated_at) VALUES ('default', $type, $config, unixepoch())
3723
- ON CONFLICT(id) DO UPDATE SET type=excluded.type, config=excluded.config, updated_at=unixepoch()`).run({ $type: profile.type, $config: profile.config });
3724
- },
3725
- getSavedRequests: () => db.query("SELECT * FROM saved_requests ORDER BY folder, name COLLATE NOCASE").all(),
3726
- getSavedRequest: (id) => db.query("SELECT * FROM saved_requests WHERE id = ?").get(id),
3727
- insertSavedRequest: (r) => db.query(`INSERT INTO saved_requests
3728
- (id, name, folder, method, url, headers, params, body, body_type, raw_type, form_rows, auth, notes)
3729
- VALUES ($id,$name,$folder,$method,$url,$headers,$params,$body,$body_type,$raw_type,$form_rows,$auth,$notes)`).run({
3730
- $id: r.id,
3731
- $name: r.name,
3732
- $folder: r.folder,
3733
- $method: r.method,
3734
- $url: r.url,
3735
- $headers: r.headers,
3736
- $params: r.params,
3737
- $body: r.body,
3738
- $body_type: r.body_type,
3739
- $raw_type: r.raw_type,
3740
- $form_rows: r.form_rows,
3741
- $auth: r.auth,
3742
- $notes: r.notes
3743
- }),
3744
- updateSavedRequest: (id, patch) => {
3745
- const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
3746
- const params = { $id: id };
3747
- for (const [k, v] of Object.entries(patch))
3748
- params[`$${k}`] = v;
3749
- db.query(`UPDATE saved_requests SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
3750
- },
3751
- deleteSavedRequest: (id) => db.query("DELETE FROM saved_requests WHERE id = ?").run(id),
3752
- getSetting: (key) => (db.query("SELECT value FROM settings WHERE key = ?").get(key) ?? null)?.value ?? null,
3753
- setSetting: (key, value) => {
3754
- db.run("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
3721
+ const result = {};
3722
+ for (const [k, v] of Object.entries(obj)) {
3723
+ result[k] = deref(v, root, depth + 1);
3724
+ }
3725
+ return result;
3726
+ }
3727
+ function resolveRef(ref, root) {
3728
+ let current = root;
3729
+ for (const part of ref.slice(2).split("/")) {
3730
+ const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
3731
+ if (current && typeof current === "object" && !Array.isArray(current)) {
3732
+ current = current[key];
3733
+ } else
3734
+ return;
3735
+ }
3736
+ return current;
3737
+ }
3738
+ function extractOperations(doc) {
3739
+ const paths = doc.paths ?? {};
3740
+ const ops = [];
3741
+ let idx = 0;
3742
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
3743
+ if (!pathItem || typeof pathItem !== "object")
3744
+ continue;
3745
+ const pi = pathItem;
3746
+ const pathParams = parseParameters(pi.parameters ?? []);
3747
+ for (const method of HTTP_METHODS) {
3748
+ const opObj = pi[method];
3749
+ if (!opObj)
3750
+ continue;
3751
+ const rawId = opObj.operationId ?? `${method}_${pathStr.replace(/[^a-zA-Z0-9]/g, "_")}_${idx++}`;
3752
+ const operationId = rawId.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
3753
+ const opParams = parseParameters(opObj.parameters ?? []);
3754
+ const paramMap = new Map;
3755
+ for (const p of [...pathParams, ...opParams])
3756
+ paramMap.set(`${p.in}:${p.name}`, p);
3757
+ ops.push({
3758
+ operationId,
3759
+ method,
3760
+ path: pathStr,
3761
+ summary: opObj.summary,
3762
+ description: opObj.description,
3763
+ tags: opObj.tags ?? ["default"],
3764
+ parameters: [...paramMap.values()],
3765
+ requestBody: parseRequestBody(opObj.requestBody),
3766
+ responses: parseResponses(opObj.responses ?? {}),
3767
+ security: opObj.security
3768
+ });
3755
3769
  }
3770
+ }
3771
+ return ops;
3772
+ }
3773
+ function parseParameters(params) {
3774
+ return params.flatMap((p) => {
3775
+ if (!p || typeof p !== "object")
3776
+ return [];
3777
+ const param = p;
3778
+ const name = param.name;
3779
+ const paramIn = param.in;
3780
+ if (!name || !paramIn || !["path", "query", "header", "cookie"].includes(paramIn))
3781
+ return [];
3782
+ return [{
3783
+ name,
3784
+ in: paramIn,
3785
+ description: param.description,
3786
+ required: param.required ?? paramIn === "path",
3787
+ schema: param.schema ?? { type: "string" }
3788
+ }];
3789
+ });
3790
+ }
3791
+ function parseRequestBody(rb) {
3792
+ if (!rb || typeof rb !== "object")
3793
+ return;
3794
+ const body = rb;
3795
+ const content = body.content ?? {};
3796
+ const contentType = Object.keys(content).find((ct) => ct.includes("json")) ?? Object.keys(content)[0];
3797
+ if (!contentType)
3798
+ return;
3799
+ return {
3800
+ description: body.description,
3801
+ required: body.required ?? false,
3802
+ contentType,
3803
+ schema: content[contentType]?.schema ?? { type: "object" }
3804
+ };
3805
+ }
3806
+ function parseResponses(responses) {
3807
+ const result = {};
3808
+ for (const [code, resp] of Object.entries(responses)) {
3809
+ if (!resp || typeof resp !== "object")
3810
+ continue;
3811
+ const r = resp;
3812
+ const content = r.content;
3813
+ const jsonCt = content ? Object.keys(content).find((ct) => ct.includes("json")) : undefined;
3814
+ result[code] = {
3815
+ description: r.description,
3816
+ contentType: jsonCt,
3817
+ schema: jsonCt ? content?.[jsonCt]?.schema : undefined
3818
+ };
3819
+ }
3820
+ return result;
3821
+ }
3822
+ function extractSecuritySchemes(doc) {
3823
+ const schemes = doc.components?.securitySchemes;
3824
+ if (!schemes)
3825
+ return {};
3826
+ const result = {};
3827
+ for (const [name, scheme] of Object.entries(schemes)) {
3828
+ if (!scheme || typeof scheme !== "object")
3829
+ continue;
3830
+ const s = scheme;
3831
+ result[name] = {
3832
+ type: s.type,
3833
+ scheme: s.scheme,
3834
+ in: s.in,
3835
+ name: s.name,
3836
+ flows: s.flows,
3837
+ openIdConnectUrl: s.openIdConnectUrl
3838
+ };
3839
+ }
3840
+ return result;
3841
+ }
3842
+ function toEnvKey(str) {
3843
+ return str.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase()).replace(/^[A-Z]/, (c) => c.toLowerCase()).replace(/[^a-zA-Z0-9_]/g, "") || str.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase();
3844
+ }
3845
+ function extractSuggestedVars(rawText, baseUrl) {
3846
+ let raw;
3847
+ try {
3848
+ raw = rawText.trimStart().startsWith("{") ? JSON.parse(rawText) : index_vite_proxy_tmp_default.load(rawText);
3849
+ } catch {
3850
+ return [];
3851
+ }
3852
+ const vars = [];
3853
+ const seen = new Set;
3854
+ const add = (key, value, description, source) => {
3855
+ const k = key.trim();
3856
+ if (!k || seen.has(k))
3857
+ return;
3858
+ seen.add(k);
3859
+ vars.push({ key: k, value, description, source });
3756
3860
  };
3861
+ if (baseUrl)
3862
+ add("baseUrl", baseUrl, "API base URL", "server");
3863
+ const servers = raw.servers ?? [];
3864
+ for (const server of servers.slice(0, 5)) {
3865
+ if (!server.variables)
3866
+ continue;
3867
+ for (const [key, def] of Object.entries(server.variables)) {
3868
+ add(toEnvKey(key), def.default ?? "", def.description ?? `Server variable: ${key}`, "server");
3869
+ }
3870
+ }
3871
+ const components = raw.components ?? {};
3872
+ const secSchemes = components.securitySchemes ?? {};
3873
+ for (const [schemeName, scheme] of Object.entries(secSchemes)) {
3874
+ if (!scheme || typeof scheme !== "object")
3875
+ continue;
3876
+ const type = scheme.type;
3877
+ const slug = toEnvKey(schemeName);
3878
+ if (type === "http") {
3879
+ const httpScheme = (scheme.scheme ?? "").toLowerCase();
3880
+ if (httpScheme === "bearer") {
3881
+ add(`${slug}Token`, "", `Bearer token for ${schemeName}`, "auth");
3882
+ } else if (httpScheme === "basic") {
3883
+ add(`${slug}Username`, "", `Username for ${schemeName}`, "auth");
3884
+ add(`${slug}Password`, "", `Password for ${schemeName}`, "auth");
3885
+ }
3886
+ } else if (type === "apiKey") {
3887
+ const keyName = scheme.name ?? schemeName;
3888
+ add(toEnvKey(keyName), "", `API key header/param: ${keyName}`, "auth");
3889
+ } else if (type === "oauth2") {
3890
+ add(`${slug}ClientId`, "", `OAuth2 client ID for ${schemeName}`, "auth");
3891
+ add(`${slug}ClientSecret`, "", `OAuth2 client secret for ${schemeName}`, "auth");
3892
+ add(`${slug}Token`, "", `OAuth2 access token for ${schemeName}`, "auth");
3893
+ } else if (type === "openIdConnect") {
3894
+ add(`${slug}Token`, "", `Access token for ${schemeName}`, "auth");
3895
+ }
3896
+ }
3897
+ const paths = raw.paths ?? {};
3898
+ const pathKeys = Object.keys(paths);
3899
+ const paramFreq = {};
3900
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
3901
+ if (!pathItem || typeof pathItem !== "object")
3902
+ continue;
3903
+ const params = pathItem.parameters ?? [];
3904
+ const seenInPath = new Set;
3905
+ for (const p of params) {
3906
+ if (p.in === "path" && p.name && !seenInPath.has(p.name)) {
3907
+ seenInPath.add(p.name);
3908
+ paramFreq[p.name] = (paramFreq[p.name] ?? 0) + 1;
3909
+ }
3910
+ }
3911
+ for (const [, param] of pathStr.matchAll(/\{([^}]+)\}/g)) {
3912
+ if (!param)
3913
+ continue;
3914
+ if (!seenInPath.has(param)) {
3915
+ seenInPath.add(param);
3916
+ paramFreq[param] = (paramFreq[param] ?? 0) + 1;
3917
+ }
3918
+ }
3919
+ }
3920
+ const threshold = Math.max(3, Math.floor(pathKeys.length * 0.15));
3921
+ for (const [name, count] of Object.entries(paramFreq)) {
3922
+ if (count >= threshold) {
3923
+ add(toEnvKey(name), "", `Common path param: {${name}}`, "path");
3924
+ }
3925
+ }
3926
+ return vars;
3927
+ }
3928
+ var HTTP_METHODS;
3929
+ var init_parser = __esm(() => {
3930
+ init_js_yaml();
3931
+ HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
3932
+ });
3933
+
3934
+ // src/state.ts
3935
+ function hasState() {
3936
+ return _state !== null;
3937
+ }
3938
+ function getState() {
3939
+ if (!_state)
3940
+ throw new Error("No spec loaded");
3941
+ return _state;
3942
+ }
3943
+ async function loadSpec(url) {
3944
+ const spec = await fetchAndParseSpec(url);
3945
+ _state = { spec, operations: spec.operations, specUrl: url };
3946
+ return _state;
3947
+ }
3948
+ function loadSpecFromText(text, filename) {
3949
+ const spec = parseSpecText(text, undefined, filename?.replace(/\.[^.]+$/, ""));
3950
+ _state = { spec, operations: spec.operations };
3951
+ return _state;
3952
+ }
3953
+ var _state = null;
3954
+ var init_state = __esm(() => {
3955
+ init_parser();
3757
3956
  });
3758
3957
 
3759
3958
  // src/logs/bus.ts
@@ -3787,6 +3986,18 @@ class LogBus {
3787
3986
  }
3788
3987
  }
3789
3988
  }
3989
+ broadcastServerEvent(payload) {
3990
+ if (this.clients.size === 0)
3991
+ return;
3992
+ const data = JSON.stringify({ type: "server_event", ...payload });
3993
+ for (const ws of this.clients) {
3994
+ try {
3995
+ ws.send(data);
3996
+ } catch {
3997
+ this.clients.delete(ws);
3998
+ }
3999
+ }
4000
+ }
3790
4001
  get clientCount() {
3791
4002
  return this.clients.size;
3792
4003
  }
@@ -4262,7 +4473,7 @@ async function executeOperation(op, baseUrl, args) {
4262
4473
  authedHeaders["Content-Type"] = op.requestBody.contentType;
4263
4474
  }
4264
4475
  const startTime = Date.now();
4265
- const logId = randomUUID2();
4476
+ const logId = randomUUID();
4266
4477
  try {
4267
4478
  const res = await fetch(authedUrl, {
4268
4479
  method: op.method.toUpperCase(),
@@ -4519,7 +4730,7 @@ async function proxyHandler(req) {
4519
4730
  const { url: authedUrl, headers: authedHeaders } = await applyAuth(targetUrl, proxyHeaders, authConfig);
4520
4731
  const requestBody = req.method !== "GET" && req.method !== "HEAD" ? await req.text() : null;
4521
4732
  const startTime = Date.now();
4522
- const logId = randomUUID2();
4733
+ const logId = randomUUID();
4523
4734
  try {
4524
4735
  const res = await fetch(authedUrl, {
4525
4736
  method: req.method,
@@ -4625,12 +4836,276 @@ var init_handler = __esm(() => {
4625
4836
  };
4626
4837
  });
4627
4838
 
4839
+ // src/proxy/capture.ts
4840
+ async function captureHandler(req) {
4841
+ if (req.method === "OPTIONS") {
4842
+ return new Response(null, { status: 204, headers: CORS3 });
4843
+ }
4844
+ const url = new URL(req.url);
4845
+ const binId = url.pathname.split("/").filter(Boolean)[1];
4846
+ if (!binId)
4847
+ return new Response("Not found", { status: 404 });
4848
+ const bin = dbQueries.getCaptureBin(binId);
4849
+ if (!bin) {
4850
+ return new Response(JSON.stringify({ error: "Capture bin not found or deleted" }), {
4851
+ status: 404,
4852
+ headers: { "Content-Type": "application/json", ...CORS3 }
4853
+ });
4854
+ }
4855
+ const body = req.method !== "GET" && req.method !== "HEAD" ? await req.text().catch(() => null) : null;
4856
+ const reqHeaders = {};
4857
+ for (const [k, v] of req.headers.entries()) {
4858
+ if (!["host", "connection"].includes(k.toLowerCase()))
4859
+ reqHeaders[k] = v;
4860
+ }
4861
+ const id = randomUUID();
4862
+ const now = Date.now();
4863
+ dbQueries.insertLog({
4864
+ id,
4865
+ source: "capture",
4866
+ tool_name: binId,
4867
+ method: req.method,
4868
+ url: req.url,
4869
+ request_headers: JSON.stringify(reqHeaders),
4870
+ request_body: body,
4871
+ status_code: 200,
4872
+ response_headers: null,
4873
+ response_body: null,
4874
+ latency_ms: 0,
4875
+ error: null
4876
+ });
4877
+ logBus.emit({
4878
+ id,
4879
+ source: "capture",
4880
+ tool_name: binId,
4881
+ method: req.method,
4882
+ url: req.url,
4883
+ request_headers: JSON.stringify(reqHeaders),
4884
+ request_body: body,
4885
+ status_code: 200,
4886
+ response_headers: null,
4887
+ response_body: null,
4888
+ latency_ms: 0,
4889
+ error: null,
4890
+ created_at: now
4891
+ });
4892
+ return new Response(JSON.stringify({ ok: true, id, captured_at: now }), {
4893
+ status: 200,
4894
+ headers: { "Content-Type": "application/json", ...CORS3 }
4895
+ });
4896
+ }
4897
+ var CORS3;
4898
+ var init_capture = __esm(() => {
4899
+ init_db();
4900
+ init_bus();
4901
+ CORS3 = {
4902
+ "Access-Control-Allow-Origin": "*",
4903
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD",
4904
+ "Access-Control-Allow-Headers": "*",
4905
+ "Access-Control-Expose-Headers": "*"
4906
+ };
4907
+ });
4908
+
4909
+ // src/workflows/engine.ts
4910
+ function interpolate(val, ctx) {
4911
+ return val.replace(/\{\{(\w+)\}\}/g, (_, k) => ctx[k] ?? `{{${k}}}`);
4912
+ }
4913
+ function interpolateDeep(obj, ctx) {
4914
+ if (typeof obj === "string")
4915
+ return interpolate(obj, ctx);
4916
+ if (Array.isArray(obj))
4917
+ return obj.map((v) => interpolateDeep(v, ctx));
4918
+ if (obj !== null && typeof obj === "object") {
4919
+ const out = {};
4920
+ for (const [k, v] of Object.entries(obj))
4921
+ out[k] = interpolateDeep(v, ctx);
4922
+ return out;
4923
+ }
4924
+ return obj;
4925
+ }
4926
+ function resolveJsonPath(data, path) {
4927
+ if (!path || path === "$")
4928
+ return data;
4929
+ const normalized = path.startsWith("$.") ? path.slice(2) : path.startsWith("$[") ? path.slice(1) : path;
4930
+ if (!normalized)
4931
+ return data;
4932
+ const parts = [];
4933
+ for (const seg of normalized.split(".")) {
4934
+ const m = seg.match(/^(\w+)\[(\d+)\]$/);
4935
+ if (m) {
4936
+ parts.push(m[1], parseInt(m[2], 10));
4937
+ } else if (/^\d+$/.test(seg)) {
4938
+ parts.push(parseInt(seg, 10));
4939
+ } else {
4940
+ parts.push(seg);
4941
+ }
4942
+ }
4943
+ let cur = data;
4944
+ for (const part of parts) {
4945
+ if (cur === null || cur === undefined)
4946
+ return;
4947
+ cur = typeof part === "number" ? cur[part] : cur[part];
4948
+ }
4949
+ return cur;
4950
+ }
4951
+ async function executeStep(step, ctx) {
4952
+ const { spec } = getState();
4953
+ let base = spec.baseUrl;
4954
+ if (!base?.startsWith("http") && spec.url) {
4955
+ try {
4956
+ base = new URL(spec.url).origin;
4957
+ } catch {}
4958
+ }
4959
+ if (!base?.startsWith("http"))
4960
+ throw new Error("Spec has no absolute server URL");
4961
+ let urlPath = step.path;
4962
+ for (const [k, v] of Object.entries(step.pathParams ?? {})) {
4963
+ urlPath = urlPath.replace(`{${k}}`, encodeURIComponent(interpolate(v, ctx)));
4964
+ }
4965
+ urlPath = interpolate(urlPath, ctx);
4966
+ const urlObj = new URL(`${base.replace(/\/$/, "")}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`);
4967
+ for (const [k, v] of Object.entries(step.queryParams ?? {})) {
4968
+ urlObj.searchParams.set(k, interpolate(v, ctx));
4969
+ }
4970
+ const stepHeaders = {};
4971
+ for (const [k, v] of Object.entries(step.headers ?? {})) {
4972
+ stepHeaders[k] = interpolate(v, ctx);
4973
+ }
4974
+ const authRow = dbQueries.getAuthConfig();
4975
+ const authConfig = authRow ? JSON.parse(authRow.config) : { type: "none" };
4976
+ const { url: authedUrl, headers: authedHeaders } = await applyAuth(urlObj.toString(), stepHeaders, authConfig);
4977
+ const noBodyMethod = ["GET", "HEAD", "OPTIONS"].includes(step.method.toUpperCase());
4978
+ let bodyStr;
4979
+ if (!noBodyMethod && step.body !== undefined && step.body !== null) {
4980
+ const interpolated = interpolateDeep(step.body, ctx);
4981
+ bodyStr = typeof interpolated === "string" ? interpolated : JSON.stringify(interpolated);
4982
+ if (!authedHeaders["Content-Type"] && !authedHeaders["content-type"]) {
4983
+ authedHeaders["Content-Type"] = "application/json";
4984
+ }
4985
+ }
4986
+ const start = Date.now();
4987
+ const res = await fetch(authedUrl, {
4988
+ method: step.method.toUpperCase(),
4989
+ headers: authedHeaders,
4990
+ ...bodyStr !== undefined ? { body: bodyStr } : {},
4991
+ signal: AbortSignal.timeout(30000)
4992
+ });
4993
+ const latency = Date.now() - start;
4994
+ const responseHeaders = {};
4995
+ res.headers.forEach((v, k) => {
4996
+ responseHeaders[k] = v;
4997
+ });
4998
+ const responseText = await res.text();
4999
+ let responseData;
5000
+ try {
5001
+ responseData = JSON.parse(responseText);
5002
+ } catch {
5003
+ responseData = responseText;
5004
+ }
5005
+ const extractedVars = {};
5006
+ for (const ext of step.extract ?? []) {
5007
+ const val = resolveJsonPath(responseData, ext.path);
5008
+ if (val !== undefined && val !== null) {
5009
+ extractedVars[ext.var] = typeof val === "string" ? val : JSON.stringify(val);
5010
+ }
5011
+ }
5012
+ const assertions = [];
5013
+ for (const a of step.assert ?? []) {
5014
+ if (a.type === "status") {
5015
+ const expected = a.statusCode ?? 200;
5016
+ const pass2 = res.status === expected;
5017
+ assertions.push({ pass: pass2, message: `HTTP ${res.status} ${pass2 ? "==" : "!="} ${expected}` });
5018
+ } else if (a.type === "json") {
5019
+ const val = resolveJsonPath(responseData, a.path ?? "$");
5020
+ if ("eq" in a) {
5021
+ const pass2 = JSON.stringify(val) === JSON.stringify(a.eq);
5022
+ assertions.push({ pass: pass2, message: `${a.path} ${pass2 ? "==" : "!="} ${JSON.stringify(a.eq)}` });
5023
+ } else if ("contains" in a && typeof val === "string") {
5024
+ const pass2 = val.includes(a.contains);
5025
+ assertions.push({ pass: pass2, message: `${a.path} ${pass2 ? "contains" : "doesn't contain"} "${a.contains}"` });
5026
+ }
5027
+ }
5028
+ }
5029
+ const pass = assertions.length === 0 ? res.ok : assertions.every((a) => a.pass);
5030
+ return {
5031
+ stepId: step.id,
5032
+ label: step.label,
5033
+ method: step.method,
5034
+ resolvedPath: urlPath,
5035
+ requestUrl: authedUrl,
5036
+ requestHeaders: authedHeaders,
5037
+ requestBody: bodyStr,
5038
+ status: res.status,
5039
+ statusText: res.statusText,
5040
+ responseHeaders,
5041
+ latency,
5042
+ extractedVars,
5043
+ assertions,
5044
+ pass,
5045
+ responseBody: responseText.slice(0, 1e4)
5046
+ };
5047
+ }
5048
+ async function runWorkflow(steps, emit, signal) {
5049
+ const ctx = {};
5050
+ let passed = 0;
5051
+ emit({ type: "run_start", totalSteps: steps.length });
5052
+ for (const step of steps) {
5053
+ if (signal?.aborted) {
5054
+ emit({ type: "run_aborted", message: "Run cancelled" });
5055
+ return;
5056
+ }
5057
+ emit({ type: "step_start", stepId: step.id, label: step.label, method: step.method, path: step.path });
5058
+ try {
5059
+ const result = await executeStep(step, ctx);
5060
+ for (const [k, v] of Object.entries(result.extractedVars))
5061
+ ctx[k] = v;
5062
+ ctx[`${step.id}_status`] = String(result.status ?? "");
5063
+ if (result.pass)
5064
+ passed++;
5065
+ emit({
5066
+ type: "step_done",
5067
+ stepId: result.stepId,
5068
+ label: result.label,
5069
+ method: result.method,
5070
+ resolvedPath: result.resolvedPath,
5071
+ requestUrl: result.requestUrl,
5072
+ requestHeaders: result.requestHeaders,
5073
+ requestBody: result.requestBody,
5074
+ status: result.status,
5075
+ statusText: result.statusText,
5076
+ responseHeaders: result.responseHeaders,
5077
+ latency: result.latency,
5078
+ extractedVars: result.extractedVars,
5079
+ assertions: result.assertions,
5080
+ pass: result.pass,
5081
+ responseBody: result.responseBody
5082
+ });
5083
+ } catch (e) {
5084
+ emit({
5085
+ type: "step_error",
5086
+ stepId: step.id,
5087
+ label: step.label,
5088
+ method: step.method,
5089
+ path: step.path,
5090
+ error: e instanceof Error ? e.message : String(e),
5091
+ pass: false
5092
+ });
5093
+ }
5094
+ }
5095
+ emit({ type: "run_done", totalSteps: steps.length, passedSteps: passed });
5096
+ }
5097
+ var init_engine2 = __esm(() => {
5098
+ init_engine();
5099
+ init_db();
5100
+ init_state();
5101
+ });
5102
+
4628
5103
  // src/api/routes.ts
4629
5104
  import dns from "dns/promises";
4630
5105
  function json(data, status = 200) {
4631
5106
  return new Response(JSON.stringify(data), {
4632
5107
  status,
4633
- headers: { "Content-Type": "application/json", ...CORS3 }
5108
+ headers: { "Content-Type": "application/json", ...CORS4 }
4634
5109
  });
4635
5110
  }
4636
5111
  function notFound(msg = "Not found") {
@@ -4641,7 +5116,7 @@ function badRequest(msg) {
4641
5116
  }
4642
5117
  async function apiRouter(req) {
4643
5118
  if (req.method === "OPTIONS")
4644
- return new Response(null, { status: 204, headers: CORS3 });
5119
+ return new Response(null, { status: 204, headers: CORS4 });
4645
5120
  const { pathname: path, searchParams } = new URL(req.url);
4646
5121
  const method = req.method;
4647
5122
  if (path === "/api/status" && method === "GET")
@@ -4708,6 +5183,24 @@ async function apiRouter(req) {
4708
5183
  return handleUpdateSaved(req, path);
4709
5184
  if (path.startsWith("/api/saved/") && method === "DELETE")
4710
5185
  return handleDeleteSaved(path);
5186
+ if (path === "/api/workflows" && method === "GET")
5187
+ return handleGetWorkflows();
5188
+ if (path === "/api/workflows" && method === "POST")
5189
+ return handleCreateWorkflow(req);
5190
+ if (path === "/api/workflows/generate" && method === "POST")
5191
+ return handleGenerateWorkflow(req);
5192
+ if (path.startsWith("/api/workflows/") && path.endsWith("/run") && method === "POST")
5193
+ return handleRunWorkflow(path);
5194
+ if (path.startsWith("/api/workflows/") && method === "PUT")
5195
+ return handleUpdateWorkflow(req, path);
5196
+ if (path.startsWith("/api/workflows/") && method === "DELETE")
5197
+ return handleDeleteWorkflow(path);
5198
+ if (path === "/api/capture/bins" && method === "GET")
5199
+ return handleGetCaptureBins();
5200
+ if (path === "/api/capture/bins" && method === "POST")
5201
+ return handleCreateCaptureBin(req);
5202
+ if (path.startsWith("/api/capture/bins/") && method === "DELETE")
5203
+ return handleDeleteCaptureBin(path);
4711
5204
  return notFound("API route not found");
4712
5205
  }
4713
5206
  function handleStatus() {
@@ -4749,8 +5242,14 @@ async function handleSetFeatures(req) {
4749
5242
  patch[key] = body[key];
4750
5243
  }
4751
5244
  setFeatures(patch);
5245
+ persistAndBroadcastFeatures();
4752
5246
  return json(getFeatures());
4753
5247
  }
5248
+ function persistAndBroadcastFeatures() {
5249
+ const f = getFeatures();
5250
+ dbQueries.setSetting("features", JSON.stringify({ mcp: f.mcp, proxy: f.proxy, ai: f.ai }));
5251
+ logBus.broadcastServerEvent({ kind: "features", data: f });
5252
+ }
4754
5253
  function handleServerInfo() {
4755
5254
  const state = hasState() ? getState() : null;
4756
5255
  return json({
@@ -4800,10 +5299,12 @@ async function handleSpecUpload(req) {
4800
5299
  return badRequest("Empty spec content");
4801
5300
  try {
4802
5301
  const state = loadSpecFromText(content, filename);
5302
+ const suggestedVars = extractSuggestedVars(content, state.spec.baseUrl);
4803
5303
  return json({
4804
5304
  ok: true,
4805
5305
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
4806
- endpointCount: state.operations.length
5306
+ endpointCount: state.operations.length,
5307
+ suggestedVars
4807
5308
  });
4808
5309
  } catch (e) {
4809
5310
  return json({ error: e instanceof Error ? e.message : String(e) }, 400);
@@ -4820,10 +5321,12 @@ async function handleSpecReloadUrl(req) {
4820
5321
  return badRequest("Missing url field");
4821
5322
  try {
4822
5323
  const state = await loadSpec(body.url);
5324
+ const suggestedVars = extractSuggestedVars(state.spec.raw, state.spec.baseUrl);
4823
5325
  return json({
4824
5326
  ok: true,
4825
5327
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
4826
- endpointCount: state.operations.length
5328
+ endpointCount: state.operations.length,
5329
+ suggestedVars
4827
5330
  });
4828
5331
  } catch (e) {
4829
5332
  return json({ error: e instanceof Error ? e.message : String(e) }, 400);
@@ -5128,7 +5631,7 @@ ${stripped}`, isError: !res.ok };
5128
5631
  authConfig = { type: "bearer", token };
5129
5632
  type = "bearer";
5130
5633
  }
5131
- const profileId = randomUUID2();
5634
+ const profileId = randomUUID();
5132
5635
  try {
5133
5636
  dbQueries.insertProfile({ id: profileId, name: profileName, description: "Saved by AI", type, config: JSON.stringify(authConfig), token_cache: null, is_active: 0 });
5134
5637
  dbQueries.activateProfile(profileId);
@@ -5444,7 +5947,11 @@ Authentication workflow: if requests return 401/403, call list_auth_profiles fir
5444
5947
 
5445
5948
  IMPORTANT: if an endpoint returns an error, diagnose it (check the schema, check auth) and fix the root cause before retrying. If the same endpoint fails 3 times the agent will be forcibly stopped. Do not retry without changing something.
5446
5949
 
5447
- Be concise and practical. Format code and JSON in code blocks.`;
5950
+ Be concise and practical. Format code and JSON in code blocks.${body.extra_context ? `
5951
+
5952
+ ---
5953
+ ## Current context
5954
+ ${body.extra_context}` : ""}`;
5448
5955
  const provider = ai.provider ?? "anthropic";
5449
5956
  const requiresKey = provider !== "ollama" && provider !== "custom";
5450
5957
  if (requiresKey && !ai.apiKey) {
@@ -5527,7 +6034,7 @@ Be concise and practical. Format code and JSON in code blocks.`;
5527
6034
  headers: {
5528
6035
  "Content-Type": "text/event-stream",
5529
6036
  "Cache-Control": "no-cache",
5530
- ...CORS3
6037
+ ...CORS4
5531
6038
  }
5532
6039
  });
5533
6040
  }
@@ -5542,7 +6049,7 @@ async function handleCreateProfile(req) {
5542
6049
  return badRequest("Invalid JSON");
5543
6050
  }
5544
6051
  const profile = {
5545
- id: randomUUID2(),
6052
+ id: randomUUID(),
5546
6053
  name: String(body.name ?? "").trim() || "Unnamed",
5547
6054
  description: String(body.description ?? ""),
5548
6055
  type: String(body.type ?? "none"),
@@ -5595,7 +6102,7 @@ async function handleCreateRule(req) {
5595
6102
  return badRequest("Invalid JSON");
5596
6103
  }
5597
6104
  const rule = {
5598
- id: randomUUID2(),
6105
+ id: randomUUID(),
5599
6106
  enabled: body.enabled ?? 1,
5600
6107
  name: body.name ?? "",
5601
6108
  sort_order: body.sort_order ?? 0,
@@ -5887,7 +6394,7 @@ async function handleCreateSaved(req) {
5887
6394
  }
5888
6395
  if (!body.name || typeof body.name !== "string")
5889
6396
  return badRequest("name is required");
5890
- const id = randomUUID2();
6397
+ const id = randomUUID();
5891
6398
  dbQueries.insertSavedRequest({
5892
6399
  id,
5893
6400
  name: String(body.name),
@@ -5905,42 +6412,247 @@ async function handleCreateSaved(req) {
5905
6412
  });
5906
6413
  return json(dbQueries.getSavedRequest(id), 201);
5907
6414
  }
5908
- async function handleUpdateSaved(req, path) {
5909
- const id = path.replace("/api/saved/", "");
5910
- if (!dbQueries.getSavedRequest(id))
6415
+ async function handleUpdateSaved(req, path) {
6416
+ const id = path.replace("/api/saved/", "");
6417
+ if (!dbQueries.getSavedRequest(id))
6418
+ return notFound();
6419
+ let body;
6420
+ try {
6421
+ body = await req.json();
6422
+ } catch {
6423
+ return badRequest("Invalid JSON");
6424
+ }
6425
+ const patch = {};
6426
+ const allowed = ["name", "folder", "method", "url", "headers", "params", "body", "body_type", "raw_type", "form_rows", "auth", "notes"];
6427
+ for (const key of allowed) {
6428
+ if (key in body)
6429
+ patch[key] = typeof body[key] === "string" ? String(body[key]) : JSON.stringify(body[key]);
6430
+ }
6431
+ if (Object.keys(patch).length)
6432
+ dbQueries.updateSavedRequest(id, patch);
6433
+ return json(dbQueries.getSavedRequest(id));
6434
+ }
6435
+ function handleDeleteSaved(path) {
6436
+ const id = path.replace("/api/saved/", "");
6437
+ if (!dbQueries.getSavedRequest(id))
6438
+ return notFound();
6439
+ dbQueries.deleteSavedRequest(id);
6440
+ return json({ ok: true });
6441
+ }
6442
+ function workflowRow(row) {
6443
+ if (!row)
6444
+ return null;
6445
+ let steps = [];
6446
+ try {
6447
+ steps = JSON.parse(row.steps);
6448
+ } catch {}
6449
+ return { ...row, steps };
6450
+ }
6451
+ function handleGetWorkflows() {
6452
+ const rows = dbQueries.getWorkflows().map((r) => workflowRow(r)).filter(Boolean);
6453
+ return json(rows);
6454
+ }
6455
+ async function handleCreateWorkflow(req) {
6456
+ let body;
6457
+ try {
6458
+ body = await req.json();
6459
+ } catch {
6460
+ return badRequest("Invalid JSON");
6461
+ }
6462
+ const id = randomUUID();
6463
+ dbQueries.insertWorkflow({
6464
+ id,
6465
+ name: String(body.name ?? "Untitled Workflow"),
6466
+ description: String(body.description ?? ""),
6467
+ steps: typeof body.steps === "string" ? body.steps : JSON.stringify(body.steps ?? [])
6468
+ });
6469
+ return json(workflowRow(dbQueries.getWorkflow(id)), 201);
6470
+ }
6471
+ async function handleUpdateWorkflow(req, path) {
6472
+ const id = path.slice("/api/workflows/".length);
6473
+ if (!dbQueries.getWorkflow(id))
6474
+ return notFound();
6475
+ let body;
6476
+ try {
6477
+ body = await req.json();
6478
+ } catch {
6479
+ return badRequest("Invalid JSON");
6480
+ }
6481
+ const patch = {};
6482
+ if ("name" in body)
6483
+ patch.name = String(body.name);
6484
+ if ("description" in body)
6485
+ patch.description = String(body.description);
6486
+ if ("steps" in body)
6487
+ patch.steps = typeof body.steps === "string" ? body.steps : JSON.stringify(body.steps);
6488
+ if (Object.keys(patch).length)
6489
+ dbQueries.updateWorkflow(id, patch);
6490
+ return json(workflowRow(dbQueries.getWorkflow(id)));
6491
+ }
6492
+ function handleDeleteWorkflow(path) {
6493
+ const id = path.slice("/api/workflows/".length);
6494
+ if (!dbQueries.getWorkflow(id))
6495
+ return notFound();
6496
+ dbQueries.deleteWorkflow(id);
6497
+ return json({ ok: true });
6498
+ }
6499
+ async function handleGenerateWorkflow(req) {
6500
+ if (!hasState())
6501
+ return badRequest("No spec loaded");
6502
+ let body;
6503
+ try {
6504
+ body = await req.json();
6505
+ } catch {
6506
+ return badRequest("Invalid JSON");
6507
+ }
6508
+ const settingsRow = dbQueries.getSettings();
6509
+ const settings = settingsRow ? JSON.parse(settingsRow.value) : {};
6510
+ const ai = settings.ai ?? {};
6511
+ const provider = ai.provider ?? "anthropic";
6512
+ if (provider !== "ollama" && !ai.apiKey) {
6513
+ return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
6514
+ }
6515
+ const { spec, operations } = getState();
6516
+ const endpointList = operations.slice(0, 80).map((op) => `${op.method.toUpperCase()} ${op.path}${op.operationId ? ` [${op.operationId}]` : ""}${op.summary ? ` \u2014 ${op.summary}` : ""}`).join(`
6517
+ `);
6518
+ const userPrompt = body.prompt?.trim() || "Generate a realistic end-to-end test workflow covering authentication and CRUD operations.";
6519
+ const systemMsg = `You generate API test workflows as JSON for the "${spec.title}" API (base: ${spec.baseUrl}).
6520
+
6521
+ Available endpoints:
6522
+ ${endpointList}
6523
+
6524
+ Return ONLY valid JSON (no markdown fences) matching this schema exactly:
6525
+ {
6526
+ "name": "string",
6527
+ "description": "string",
6528
+ "steps": [
6529
+ {
6530
+ "id": "step_1",
6531
+ "label": "Human-readable name",
6532
+ "method": "GET|POST|PUT|PATCH|DELETE",
6533
+ "path": "/exact/path/from/spec",
6534
+ "operationId": "operationId or null",
6535
+ "pathParams": {},
6536
+ "queryParams": {},
6537
+ "headers": {},
6538
+ "body": null,
6539
+ "extract": [{"var": "varName", "path": "$.field.nested"}],
6540
+ "assert": [{"type": "status", "statusCode": 200}]
6541
+ }
6542
+ ]
6543
+ }
6544
+
6545
+ Rules:
6546
+ - Use {{varName}} in path/headers/body values to reference vars extracted in prior steps
6547
+ - For auth: extract token after login, set headers: {"Authorization": "Bearer {{token}}"}
6548
+ - Keep 3\u20138 steps covering a realistic user journey
6549
+ - Only use paths that exist in the endpoint list above`;
6550
+ try {
6551
+ let text = "";
6552
+ if (provider === "anthropic") {
6553
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
6554
+ method: "POST",
6555
+ headers: { "Content-Type": "application/json", "x-api-key": ai.apiKey, "anthropic-version": "2023-06-01" },
6556
+ body: JSON.stringify({ model: ai.model || "claude-sonnet-4-6", max_tokens: 4096, system: systemMsg, messages: [{ role: "user", content: userPrompt }] })
6557
+ });
6558
+ if (!res.ok)
6559
+ throw new Error(`Anthropic: ${await res.text()}`);
6560
+ const d = await res.json();
6561
+ text = d.content.find((b) => b.type === "text")?.text ?? "";
6562
+ } else {
6563
+ const base = (ai.baseUrl || (provider === "openai" ? "https://api.openai.com" : provider === "groq" ? "https://api.groq.com/openai" : provider === "mistral" ? "https://api.mistral.ai" : "https://api.openai.com")).replace(/\/$/, "");
6564
+ const hdrs = { "Content-Type": "application/json" };
6565
+ if (ai.apiKey)
6566
+ hdrs["Authorization"] = `Bearer ${ai.apiKey}`;
6567
+ const res = await fetch(`${base}/v1/chat/completions`, {
6568
+ method: "POST",
6569
+ headers: hdrs,
6570
+ body: JSON.stringify({ model: ai.model || "gpt-4o-mini", max_tokens: 2048, response_format: { type: "json_object" }, messages: [{ role: "system", content: systemMsg }, { role: "user", content: userPrompt }] })
6571
+ });
6572
+ if (!res.ok)
6573
+ throw new Error(await res.text());
6574
+ const d = await res.json();
6575
+ text = d.choices[0]?.message.content ?? "";
6576
+ }
6577
+ let parsed;
6578
+ try {
6579
+ parsed = JSON.parse(text);
6580
+ } catch {
6581
+ const m = text.match(/```(?:json)?\s*\n?([\s\S]+?)\n?```/);
6582
+ if (m)
6583
+ parsed = JSON.parse(m[1]);
6584
+ else
6585
+ throw new Error("AI response was not valid JSON");
6586
+ }
6587
+ return json(parsed);
6588
+ } catch (e) {
6589
+ return json({ error: e instanceof Error ? e.message : String(e) }, 500);
6590
+ }
6591
+ }
6592
+ function handleRunWorkflow(path) {
6593
+ const id = path.slice("/api/workflows/".length, -"/run".length);
6594
+ const row = dbQueries.getWorkflow(id);
6595
+ if (!row)
5911
6596
  return notFound();
5912
- let body;
6597
+ if (!hasState())
6598
+ return badRequest("No spec loaded");
6599
+ let steps;
5913
6600
  try {
5914
- body = await req.json();
6601
+ steps = JSON.parse(row.steps);
5915
6602
  } catch {
5916
- return badRequest("Invalid JSON");
6603
+ return badRequest("Invalid workflow steps JSON");
5917
6604
  }
5918
- const patch = {};
5919
- const allowed = ["name", "folder", "method", "url", "headers", "params", "body", "body_type", "raw_type", "form_rows", "auth", "notes"];
5920
- for (const key of allowed) {
5921
- if (key in body)
5922
- patch[key] = typeof body[key] === "string" ? String(body[key]) : JSON.stringify(body[key]);
5923
- }
5924
- if (Object.keys(patch).length)
5925
- dbQueries.updateSavedRequest(id, patch);
5926
- return json(dbQueries.getSavedRequest(id));
6605
+ if (!steps.length)
6606
+ return badRequest("Workflow has no steps");
6607
+ const { readable, writable } = new TransformStream;
6608
+ const writer = writable.getWriter();
6609
+ const enc = new TextEncoder;
6610
+ const emit = (e) => {
6611
+ writer.write(enc.encode(`data: ${JSON.stringify(e)}
6612
+
6613
+ `)).catch(() => {});
6614
+ };
6615
+ (async () => {
6616
+ try {
6617
+ await runWorkflow(steps, emit);
6618
+ } catch (e) {
6619
+ emit({ type: "error", message: e instanceof Error ? e.message : String(e) });
6620
+ } finally {
6621
+ try {
6622
+ await writer.close();
6623
+ } catch {}
6624
+ }
6625
+ })();
6626
+ return new Response(readable, {
6627
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...CORS4 }
6628
+ });
5927
6629
  }
5928
- function handleDeleteSaved(path) {
5929
- const id = path.replace("/api/saved/", "");
5930
- if (!dbQueries.getSavedRequest(id))
5931
- return notFound();
5932
- dbQueries.deleteSavedRequest(id);
6630
+ function handleGetCaptureBins() {
6631
+ return json(dbQueries.getCaptureBins());
6632
+ }
6633
+ async function handleCreateCaptureBin(req) {
6634
+ const body = await req.json().catch(() => ({}));
6635
+ const id = randomUUID().replace(/-/g, "").slice(0, 8);
6636
+ const name = String(body.name ?? "").trim() || "Untitled bin";
6637
+ dbQueries.insertCaptureBin(id, name);
6638
+ return json({ id, name, created_at: Math.floor(Date.now() / 1000) }, 201);
6639
+ }
6640
+ function handleDeleteCaptureBin(path) {
6641
+ const id = path.replace("/api/capture/bins/", "");
6642
+ dbQueries.deleteCaptureBin(id);
5933
6643
  return json({ ok: true });
5934
6644
  }
5935
- var CORS3, TOOL_DEFS, ANTHROPIC_TOOLS, OPENAI_TOOLS, MAX_TOTAL_TOOLS = 40, MAX_CONSECUTIVE_ERRORS = 5, MAX_SAME_ENDPOINT_ERRORS = 3;
6645
+ var CORS4, TOOL_DEFS, ANTHROPIC_TOOLS, OPENAI_TOOLS, MAX_TOTAL_TOOLS = 40, MAX_CONSECUTIVE_ERRORS = 5, MAX_SAME_ENDPOINT_ERRORS = 3;
5936
6646
  var init_routes = __esm(() => {
5937
6647
  init_db();
5938
6648
  init_engine();
5939
6649
  init_bus();
5940
6650
  init_state();
6651
+ init_parser();
5941
6652
  init_config();
5942
6653
  init_version();
5943
- CORS3 = {
6654
+ init_engine2();
6655
+ CORS4 = {
5944
6656
  "Access-Control-Allow-Origin": "*",
5945
6657
  "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
5946
6658
  "Access-Control-Allow-Headers": "Content-Type, Authorization"
@@ -6029,17 +6741,403 @@ var init_routes = __esm(() => {
6029
6741
  }));
6030
6742
  });
6031
6743
 
6744
+ // src/repl.ts
6745
+ function stripAnsi(s) {
6746
+ return s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
6747
+ }
6748
+ function visualRows(line, cols) {
6749
+ const len = stripAnsi(line).length;
6750
+ if (len === 0)
6751
+ return 1;
6752
+ return Math.ceil(len / cols);
6753
+ }
6754
+
6755
+ class Repl {
6756
+ buf = "";
6757
+ history = [];
6758
+ histIdx = -1;
6759
+ savedBuf = "";
6760
+ running = false;
6761
+ statusText = "";
6762
+ promptDrawn = false;
6763
+ drawingPrompt = false;
6764
+ promptVisualRows = 0;
6765
+ onCmd = null;
6766
+ cols = process.stdout.columns || 80;
6767
+ dynSuggestions = [];
6768
+ constructor() {
6769
+ if (isTTY) {
6770
+ process.stdout.on("resize", () => {
6771
+ this.cols = process.stdout.columns || 80;
6772
+ if (this.running)
6773
+ this.redraw();
6774
+ });
6775
+ }
6776
+ }
6777
+ setDynamicSuggestions(items) {
6778
+ this.dynSuggestions = items;
6779
+ if (this.running)
6780
+ this.redraw();
6781
+ }
6782
+ setStatus(text) {
6783
+ this.statusText = text;
6784
+ if (this.running)
6785
+ this.redraw();
6786
+ }
6787
+ print(line) {
6788
+ process.stdout.write(line + `
6789
+ `);
6790
+ }
6791
+ start(onCmd) {
6792
+ this.onCmd = onCmd;
6793
+ this.running = true;
6794
+ if (!isTTY || !process.stdin.setRawMode) {
6795
+ this.startSimple(onCmd);
6796
+ return;
6797
+ }
6798
+ const origWrite = process.stdout.write.bind(process.stdout);
6799
+ const self = this;
6800
+ const patched = function(chunk, enc, cb) {
6801
+ if (self.drawingPrompt)
6802
+ return origWrite(chunk, enc, cb);
6803
+ const str = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? new TextDecoder().decode(chunk) : String(chunk);
6804
+ const isSpinnerWrite = str.startsWith("\r") && !str.includes(`
6805
+ `);
6806
+ if (isSpinnerWrite || !self.promptDrawn)
6807
+ return origWrite(chunk, enc, cb);
6808
+ self.drawingPrompt = true;
6809
+ for (let i = 0;i < self.promptVisualRows - 1; i++)
6810
+ origWrite("\x1B[A");
6811
+ origWrite("\r\x1B[J");
6812
+ self.promptDrawn = false;
6813
+ const r = origWrite(chunk, enc, cb);
6814
+ if (!str.endsWith(`
6815
+ `))
6816
+ origWrite(`
6817
+ `);
6818
+ const lines = self.buildPromptLines();
6819
+ self.promptVisualRows = lines.reduce((sum, l) => sum + visualRows(l, self.cols), 0);
6820
+ origWrite(lines.join(`
6821
+ `));
6822
+ self.promptDrawn = true;
6823
+ self.drawingPrompt = false;
6824
+ return r;
6825
+ };
6826
+ patched.__orig = origWrite;
6827
+ process.stdout.write = patched;
6828
+ process.stdin.setRawMode(true);
6829
+ process.stdin.resume();
6830
+ process.stdin.setEncoding("utf8");
6831
+ process.stdin.on("data", (key) => {
6832
+ this.handleKey(key).catch(() => {});
6833
+ });
6834
+ this.drawPrompt();
6835
+ }
6836
+ stop() {
6837
+ this.running = false;
6838
+ if (this.promptDrawn)
6839
+ this.clearPrompt();
6840
+ if (process.stdout.write.__orig) {
6841
+ process.stdout.write = process.stdout.write.__orig;
6842
+ }
6843
+ try {
6844
+ process.stdin.setRawMode(false);
6845
+ process.stdin.pause();
6846
+ } catch {}
6847
+ }
6848
+ getSuggestions() {
6849
+ if (!this.buf.startsWith("/"))
6850
+ return [];
6851
+ const raw = this.buf.slice(1);
6852
+ if (!raw)
6853
+ return BASE.slice(0, 6);
6854
+ const parts = raw.split(" ");
6855
+ const base = parts[0] ?? "";
6856
+ if (raw.startsWith("auth use ") && this.dynSuggestions.length) {
6857
+ const typed = raw.slice("auth use ".length);
6858
+ return this.dynSuggestions.filter((s) => s.label.toLowerCase().startsWith(typed.toLowerCase()));
6859
+ }
6860
+ if (parts.length >= 2 && SUB[base]) {
6861
+ return (SUB[base] ?? []).filter((s) => s.value.startsWith(raw));
6862
+ }
6863
+ return BASE.filter((c) => c.value.startsWith(raw));
6864
+ }
6865
+ getGhostText() {
6866
+ if (!this.buf.startsWith("/"))
6867
+ return "";
6868
+ const suggestions = this.getSuggestions();
6869
+ if (!suggestions.length)
6870
+ return "";
6871
+ const first = suggestions[0];
6872
+ const full = "/" + first.value;
6873
+ if (full === this.buf || !full.startsWith(this.buf))
6874
+ return "";
6875
+ return full.slice(this.buf.length);
6876
+ }
6877
+ buildPromptLines() {
6878
+ const lines = [];
6879
+ const suggestions = this.getSuggestions();
6880
+ const DOT = paint.dim(" \xB7 ");
6881
+ if (this.buf.startsWith("/") && suggestions.length > 0) {
6882
+ const items = suggestions.slice(0, 5);
6883
+ const row = items.map((s, i) => {
6884
+ const label = "/" + s.label;
6885
+ const desc = s.desc ? paint.dim(" " + s.desc) : "";
6886
+ return i === 0 ? paint.cyan(label) + desc : paint.dim(label);
6887
+ }).join(DOT);
6888
+ lines.push(` ${row}`);
6889
+ }
6890
+ if (this.statusText) {
6891
+ const plain = stripAnsi(this.statusText);
6892
+ const truncated = plain.length > this.cols - 4 ? plain.slice(0, this.cols - 7) + "\u2026" : plain;
6893
+ lines.push(` ${paint.dim(truncated)}`);
6894
+ }
6895
+ const ghost = this.getGhostText();
6896
+ lines.push(` ${paint.cyan("\u276F")} ${this.buf}${ghost ? paint.dim(ghost) : ""}`);
6897
+ return lines;
6898
+ }
6899
+ clearPrompt() {
6900
+ if (!this.promptDrawn)
6901
+ return;
6902
+ this.drawingPrompt = true;
6903
+ for (let i = 0;i < this.promptVisualRows - 1; i++)
6904
+ process.stdout.write("\x1B[A");
6905
+ process.stdout.write("\r\x1B[J");
6906
+ this.promptDrawn = false;
6907
+ this.drawingPrompt = false;
6908
+ }
6909
+ drawPrompt() {
6910
+ this.drawingPrompt = true;
6911
+ const lines = this.buildPromptLines();
6912
+ this.promptVisualRows = lines.reduce((sum, l) => sum + visualRows(l, this.cols), 0);
6913
+ process.stdout.write(lines.join(`
6914
+ `));
6915
+ this.promptDrawn = true;
6916
+ this.drawingPrompt = false;
6917
+ }
6918
+ redraw() {
6919
+ this.clearPrompt();
6920
+ this.drawPrompt();
6921
+ }
6922
+ async handleKey(key) {
6923
+ if (!this.running)
6924
+ return;
6925
+ if (key === "\x03" || key === "\x04") {
6926
+ this.stop();
6927
+ process.emit("SIGINT");
6928
+ return;
6929
+ }
6930
+ if (key === "\f") {
6931
+ this.drawingPrompt = true;
6932
+ process.stdout.write("\x1B[2J\x1B[H");
6933
+ this.drawingPrompt = false;
6934
+ this.promptDrawn = false;
6935
+ this.drawPrompt();
6936
+ return;
6937
+ }
6938
+ if (key === "\x15") {
6939
+ this.buf = "";
6940
+ this.redraw();
6941
+ return;
6942
+ }
6943
+ if (key === "\x17") {
6944
+ this.buf = this.buf.replace(/\S+\s*$/, "");
6945
+ this.redraw();
6946
+ return;
6947
+ }
6948
+ if (key.startsWith("\x1B")) {
6949
+ if (key === "\x1B") {
6950
+ this.buf = "";
6951
+ this.redraw();
6952
+ return;
6953
+ }
6954
+ if (key === "\x1B[A") {
6955
+ this.historyUp();
6956
+ return;
6957
+ }
6958
+ if (key === "\x1B[B") {
6959
+ this.historyDown();
6960
+ return;
6961
+ }
6962
+ if (key === "\x1B[C") {
6963
+ const g = this.getGhostText();
6964
+ if (g) {
6965
+ this.buf += g;
6966
+ const raw = this.buf.slice(1);
6967
+ if (SUB[raw])
6968
+ this.buf += " ";
6969
+ this.redraw();
6970
+ }
6971
+ return;
6972
+ }
6973
+ return;
6974
+ }
6975
+ if (key === "\t") {
6976
+ const g = this.getGhostText();
6977
+ if (g) {
6978
+ this.buf += g;
6979
+ const raw = this.buf.slice(1);
6980
+ if (SUB[raw])
6981
+ this.buf += " ";
6982
+ } else {
6983
+ const suggestions = this.getSuggestions();
6984
+ if (suggestions[0] && "/" + suggestions[0].value !== this.buf) {
6985
+ this.buf = "/" + suggestions[0].value;
6986
+ }
6987
+ }
6988
+ this.redraw();
6989
+ return;
6990
+ }
6991
+ if (key === "\x7F" || key === "\b") {
6992
+ if (this.buf.length > 0) {
6993
+ this.buf = this.buf.slice(0, -1);
6994
+ this.redraw();
6995
+ }
6996
+ return;
6997
+ }
6998
+ if (key === "\r" || key === `
6999
+ `) {
7000
+ await this.submit();
7001
+ return;
7002
+ }
7003
+ if (this.buf === "") {
7004
+ switch (key.toLowerCase()) {
7005
+ case "r":
7006
+ await this.dispatchImmediate("r");
7007
+ return;
7008
+ case "b":
7009
+ await this.dispatchImmediate("b");
7010
+ return;
7011
+ case "s":
7012
+ await this.dispatchImmediate("s");
7013
+ return;
7014
+ case "q":
7015
+ await this.dispatchImmediate("q");
7016
+ return;
7017
+ case "?":
7018
+ case "h":
7019
+ await this.dispatchImmediate("h");
7020
+ return;
7021
+ case "/":
7022
+ this.buf = "/";
7023
+ this.redraw();
7024
+ return;
7025
+ }
7026
+ }
7027
+ const printable = key.replace(/[^\x20-\x7E]/g, "");
7028
+ if (printable) {
7029
+ this.buf += printable;
7030
+ this.redraw();
7031
+ }
7032
+ }
7033
+ async submit() {
7034
+ const cmd = this.buf.trim();
7035
+ this.buf = "";
7036
+ this.clearPrompt();
7037
+ process.stdout.write(`
7038
+ `);
7039
+ this.promptDrawn = false;
7040
+ if (cmd) {
7041
+ this.history.unshift(cmd);
7042
+ if (this.history.length > 200)
7043
+ this.history.pop();
7044
+ this.histIdx = -1;
7045
+ this.savedBuf = "";
7046
+ if (this.onCmd)
7047
+ await this.onCmd(cmd);
7048
+ }
7049
+ this.drawPrompt();
7050
+ }
7051
+ async dispatchImmediate(key) {
7052
+ this.clearPrompt();
7053
+ process.stdout.write(`
7054
+ `);
7055
+ this.promptDrawn = false;
7056
+ if (this.onCmd)
7057
+ await this.onCmd(key);
7058
+ this.drawPrompt();
7059
+ }
7060
+ historyUp() {
7061
+ if (!this.history.length)
7062
+ return;
7063
+ if (this.histIdx === -1)
7064
+ this.savedBuf = this.buf;
7065
+ this.histIdx = Math.min(this.histIdx + 1, this.history.length - 1);
7066
+ this.buf = this.history[this.histIdx] ?? "";
7067
+ this.redraw();
7068
+ }
7069
+ historyDown() {
7070
+ if (this.histIdx === -1)
7071
+ return;
7072
+ this.histIdx--;
7073
+ this.buf = this.histIdx === -1 ? this.savedBuf : this.history[this.histIdx] ?? "";
7074
+ this.redraw();
7075
+ }
7076
+ startSimple(onCmd) {
7077
+ process.stdout.write(" \u276F ");
7078
+ process.stdin.setEncoding("utf8");
7079
+ let line = "";
7080
+ process.stdin.on("data", async (chunk) => {
7081
+ for (const ch of chunk) {
7082
+ if (ch === `
7083
+ ` || ch === "\r") {
7084
+ const cmd = line.trim();
7085
+ line = "";
7086
+ if (cmd)
7087
+ await onCmd(cmd);
7088
+ process.stdout.write(" \u276F ");
7089
+ } else {
7090
+ line += ch;
7091
+ }
7092
+ }
7093
+ });
7094
+ }
7095
+ }
7096
+ var BASE, SUB;
7097
+ var init_repl = __esm(() => {
7098
+ init_ui();
7099
+ BASE = [
7100
+ { value: "help", label: "help", desc: "Show all commands" },
7101
+ { value: "status", label: "status", desc: "Show server status" },
7102
+ { value: "reload", label: "reload", desc: "Hot-reload the spec" },
7103
+ { value: "spec", label: "spec", desc: "Load a different spec" },
7104
+ { value: "mcp", label: "mcp", desc: "Toggle MCP endpoint" },
7105
+ { value: "proxy", label: "proxy", desc: "Toggle HTTP proxy" },
7106
+ { value: "ai", label: "ai", desc: "Toggle AI chat" },
7107
+ { value: "readonly", label: "readonly", desc: "Toggle read-only mode" },
7108
+ { value: "auth", label: "auth", desc: "Manage auth roles" },
7109
+ { value: "token", label: "token", desc: "Manage access token" },
7110
+ { value: "tail", label: "tail", desc: "Live request log" },
7111
+ { value: "open", label: "open", desc: "Open studio in browser" },
7112
+ { value: "update", label: "update", desc: "Update wasper" },
7113
+ { value: "quit", label: "quit", desc: "Quit" }
7114
+ ];
7115
+ SUB = {
7116
+ auth: [
7117
+ { value: "auth list", label: "list", desc: "List auth profiles" },
7118
+ { value: "auth use ", label: "use", desc: "Switch active profile" },
7119
+ { value: "auth none", label: "none", desc: "Disable auth" }
7120
+ ],
7121
+ mcp: [{ value: "mcp on", label: "on", desc: "" }, { value: "mcp off", label: "off", desc: "" }],
7122
+ proxy: [{ value: "proxy on", label: "on", desc: "" }, { value: "proxy off", label: "off", desc: "" }],
7123
+ ai: [{ value: "ai on", label: "on", desc: "" }, { value: "ai off", label: "off", desc: "" }],
7124
+ readonly: [{ value: "readonly on", label: "on", desc: "" }, { value: "readonly off", label: "off", desc: "" }],
7125
+ token: [{ value: "token new", label: "new", desc: "Generate token" }, { value: "token off", label: "off", desc: "Remove token" }],
7126
+ tail: [{ value: "tail on", label: "on", desc: "" }, { value: "tail off", label: "off", desc: "" }]
7127
+ };
7128
+ });
7129
+
6032
7130
  // src/commands/start.ts
6033
7131
  var exports_start = {};
6034
7132
  __export(exports_start, {
6035
- run: () => run5
7133
+ run: () => run6
6036
7134
  });
6037
7135
  import { parseArgs } from "util";
6038
7136
  import { createInterface } from "readline";
6039
7137
  import { homedir as homedir4 } from "os";
6040
7138
  import { join as join4, dirname } from "path";
6041
7139
  import { mkdirSync as mkdirSync2 } from "fs";
6042
- async function run5(overrideOpts) {
7140
+ async function run6(overrideOpts) {
6043
7141
  const { values } = parseArgs({
6044
7142
  args: process.argv.slice(2).filter((a) => a !== "start"),
6045
7143
  options: {
@@ -6063,18 +7161,44 @@ async function run5(overrideOpts) {
6063
7161
  printHelp();
6064
7162
  process.exit(0);
6065
7163
  }
6066
- const specUrl = overrideOpts?.url ?? (values.url ? String(values.url) : null) ?? process.env.WASPER_SPEC_URL ?? null;
7164
+ let specUrl = overrideOpts?.url ?? (values.url ? String(values.url) : null) ?? process.env.WASPER_SPEC_URL ?? null;
6067
7165
  const PORT = overrideOpts?.port ?? parseInt(String(values.port ?? "3388"), 10);
6068
7166
  const HOST = overrideOpts?.host ?? (values.host ? String(values.host) : null) ?? process.env.WASPER_HOST ?? "0.0.0.0";
6069
7167
  const ORIGIN = (overrideOpts?.origin ?? (values.origin ? String(values.origin) : null) ?? process.env.WASPER_ORIGIN ?? null)?.replace(/\/$/, "") ?? null;
6070
7168
  const TOKEN = overrideOpts?.token ?? (values.token ? String(values.token) : null) ?? process.env.WASPER_TOKEN ?? null;
6071
7169
  const bgNow = overrideOpts?.daemon ?? !!(values.background || values.daemon);
6072
7170
  const isDaemon = overrideOpts?.isDaemon ?? !!values["_daemon"];
7171
+ if (!specUrl && !isDaemon) {
7172
+ const last = dbQueries.getLastSpec();
7173
+ if (last) {
7174
+ specUrl = last.url;
7175
+ if (isTTY)
7176
+ console.log(` ${paint.dim("\u21A9")} Resuming ${paint.cyan(last.title ?? last.url)} ${paint.dim("(last used)")}
7177
+ `);
7178
+ }
7179
+ }
6073
7180
  setServerConfig({ port: PORT, host: HOST, origin: ORIGIN, token: TOKEN });
7181
+ {
7182
+ const saved = dbQueries.getSetting("features");
7183
+ if (saved) {
7184
+ try {
7185
+ const obj = JSON.parse(saved);
7186
+ const patch = {};
7187
+ if (obj.mcp === false)
7188
+ patch.mcp = false;
7189
+ if (obj.proxy === false)
7190
+ patch.proxy = false;
7191
+ if (obj.ai === false)
7192
+ patch.ai = false;
7193
+ if (Object.keys(patch).length)
7194
+ setFeatures(patch);
7195
+ } catch {}
7196
+ }
7197
+ }
6074
7198
  setFeatures({
6075
- mcp: !values["no-mcp"],
6076
- proxy: !values["no-proxy"],
6077
- ai: !values["no-ai"],
7199
+ ...values["no-mcp"] ? { mcp: false } : {},
7200
+ ...values["no-proxy"] ? { proxy: false } : {},
7201
+ ...values["no-ai"] ? { ai: false } : {},
6078
7202
  readonly: !!values.readonly
6079
7203
  });
6080
7204
  if (bgNow) {
@@ -6101,6 +7225,7 @@ async function run5(overrideOpts) {
6101
7225
  specVersion = state.spec.version;
6102
7226
  endpointCount = state.operations.length;
6103
7227
  spinner.stop();
7228
+ dbQueries.upsertSpec(specUrl, specTitle ?? null, specVersion ?? null, endpointCount);
6104
7229
  } catch (e) {
6105
7230
  spinner.stop("\u2717", `Failed to load spec: ${e instanceof Error ? e.message : String(e)}`, "red");
6106
7231
  }
@@ -6123,6 +7248,8 @@ async function run5(overrideOpts) {
6123
7248
  if (req.method === "OPTIONS") {
6124
7249
  return new Response(null, { status: 204, headers: CORS_HEADERS });
6125
7250
  }
7251
+ if (pathname.startsWith("/c/"))
7252
+ return captureHandler(req);
6126
7253
  if (!isAuthorized(req)) {
6127
7254
  return new Response(JSON.stringify({ error: "Unauthorized: pass Authorization: Bearer <token> or ?token=" }), {
6128
7255
  status: 401,
@@ -6213,127 +7340,129 @@ async function run5(overrideOpts) {
6213
7340
  function attachKeyboard(opts) {
6214
7341
  const ctx = { specUrl: opts.specUrl, PORT: opts.PORT, tailOff: null };
6215
7342
  let isReloading = false;
6216
- let cmdBuf = null;
6217
- process.stdin.setRawMode(true);
6218
- process.stdin.resume();
6219
- process.stdin.setEncoding("utf8");
6220
- const spinner = new Spinner;
7343
+ const repl = new Repl;
7344
+ const buildStatus = () => {
7345
+ const state = hasState() ? getState() : null;
7346
+ const f = getFeatures();
7347
+ const cfg = getServerConfig();
7348
+ const active = dbQueries.getActiveProfile();
7349
+ const parts = [];
7350
+ if (state) {
7351
+ parts.push(`${state.spec.title} v${state.spec.version} \xB7 ${state.operations.length} ep`);
7352
+ } else {
7353
+ parts.push("no spec");
7354
+ }
7355
+ parts.push(`:${ctx.PORT}`);
7356
+ const flags = [];
7357
+ if (!f.mcp)
7358
+ flags.push("mcp:off");
7359
+ if (!f.proxy)
7360
+ flags.push("proxy:off");
7361
+ if (!f.ai)
7362
+ flags.push("ai:off");
7363
+ if (f.readonly)
7364
+ flags.push("readonly:on");
7365
+ if (flags.length)
7366
+ parts.push(flags.join(" "));
7367
+ parts.push(`auth:${active ? active.name : "none"}`);
7368
+ if (cfg.token)
7369
+ parts.push("token:set");
7370
+ if (ctx.tailOff)
7371
+ parts.push("tail:on");
7372
+ return parts.join(" \xB7 ");
7373
+ };
7374
+ const refreshStatus = () => repl.setStatus(buildStatus());
7375
+ const refreshAuthSuggestions = () => {
7376
+ const profiles = dbQueries.getProfiles();
7377
+ repl.setDynamicSuggestions(profiles.map((p) => ({
7378
+ value: `auth use ${p.name}`,
7379
+ label: p.name,
7380
+ desc: p.type
7381
+ })));
7382
+ };
6221
7383
  const reload = async () => {
6222
7384
  if (isReloading)
6223
7385
  return;
6224
- process.stdout.write(`
6225
- `);
6226
7386
  if (!ctx.specUrl) {
6227
- console.log(` ${paint.yellow("\u25CB")} No spec URL \u2014 use /spec <url> or start with --url <url>`);
6228
- console.log();
7387
+ console.log(`
7388
+ ${paint.yellow("\u25CB")} No spec URL \u2014 use /spec <url>
7389
+ `);
6229
7390
  return;
6230
7391
  }
6231
7392
  isReloading = true;
6232
- spinner.start(`Reloading spec\u2026`);
7393
+ let fi = 0;
7394
+ const base = buildStatus();
7395
+ const spinTimer = setInterval(() => {
7396
+ repl.setStatus(`${SPIN_FRAMES[fi++ % SPIN_FRAMES.length]} Reloading\u2026 \xB7 ${base}`);
7397
+ }, 80);
6233
7398
  try {
6234
7399
  const state = await loadSpec(ctx.specUrl);
6235
- spinner.stop("\u2713", `${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} ${paint.dim("\xB7")} ${paint.green(state.operations.length + " endpoints")}`, "green");
7400
+ clearInterval(spinTimer);
7401
+ dbQueries.upsertSpec(ctx.specUrl, state.spec.title, state.spec.version, state.operations.length);
7402
+ console.log(` ${paint.green("\u2713")} ${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} \xB7 ${paint.green(state.operations.length + " endpoints")}
7403
+ `);
6236
7404
  } catch (e) {
6237
- spinner.stop("\u2717", `Reload failed: ${e instanceof Error ? e.message : String(e)}`, "red");
7405
+ clearInterval(spinTimer);
7406
+ console.log(` ${paint.red("\u2717")} Reload failed: ${e instanceof Error ? e.message : String(e)}
7407
+ `);
6238
7408
  } finally {
6239
7409
  isReloading = false;
7410
+ refreshStatus();
6240
7411
  }
6241
- console.log();
6242
7412
  };
6243
- const printPrompt = () => process.stdout.write(`\r\x1B[K ${paint.cyan("\u276F")} ${cmdBuf}`);
6244
- process.stdin.on("data", async (chunk) => {
6245
- if (chunk.startsWith("\x1B") && chunk.length > 1)
6246
- return;
6247
- for (const key of chunk)
6248
- await handleKey(key);
6249
- });
6250
- const handleKey = async (key) => {
6251
- if (key === "\x03") {
6252
- if (cmdBuf !== null) {
6253
- cmdBuf = null;
6254
- process.stdout.write("\r\x1B[K");
6255
- return;
6256
- }
6257
- process.emit("SIGINT");
6258
- return;
6259
- }
6260
- if (cmdBuf !== null) {
6261
- if (key === "\x1B") {
6262
- cmdBuf = null;
6263
- process.stdout.write("\r\x1B[K");
6264
- return;
6265
- }
6266
- if (key === "\x7F" || key === "\b") {
6267
- cmdBuf = cmdBuf.slice(0, -1);
6268
- if (!cmdBuf) {
6269
- cmdBuf = null;
6270
- process.stdout.write("\r\x1B[K");
7413
+ const handler = async (input) => {
7414
+ const cmd = input.trim();
7415
+ if (cmd.length === 1) {
7416
+ switch (cmd.toLowerCase()) {
7417
+ case "r":
7418
+ await reload();
6271
7419
  return;
6272
- }
6273
- printPrompt();
6274
- return;
6275
- }
6276
- if (key === "\r" || key === `
6277
- `) {
6278
- const cmd = cmdBuf;
6279
- cmdBuf = null;
6280
- process.stdout.write("\r\x1B[K");
6281
- await runSlashCommand(cmd, ctx, reload);
6282
- return;
6283
- }
6284
- const printable = key.replace(/[^\x20-\x7E]/g, "");
6285
- if (printable) {
6286
- cmdBuf += printable;
6287
- printPrompt();
6288
- }
6289
- return;
6290
- }
6291
- if (key === "/") {
6292
- cmdBuf = "/";
6293
- printPrompt();
6294
- return;
6295
- }
6296
- switch (key.toLowerCase()) {
6297
- case "r":
6298
- await reload();
6299
- break;
6300
- case "b": {
6301
- process.stdin.setRawMode(false);
6302
- process.stdin.pause();
6303
- process.on("SIGHUP", () => {});
6304
- console.log(`
6305
- ${paint.green("\u2713")} Detached ${paint.dim(`PID ${process.pid}`)}`);
6306
- console.log(` ${paint.dim("\u279C")} ${paint.dim("wasper status")} ${paint.dim("\xB7")} ${paint.dim("wasper stop")}
7420
+ case "s":
7421
+ printInlineStatus(ctx);
7422
+ return;
7423
+ case "q":
7424
+ process.emit("SIGINT");
7425
+ return;
7426
+ case "h":
7427
+ case "?":
7428
+ printInteractiveHelp();
7429
+ return;
7430
+ case "b": {
7431
+ repl.stop();
7432
+ process.on("SIGHUP", () => {});
7433
+ console.log(`
7434
+ ${paint.green("\u2713")} Detached ${paint.dim("PID " + process.pid)}`);
7435
+ console.log(` ${paint.dim("\u279C")} ${paint.dim("wasper status")} ${paint.dim("\xB7")} ${paint.dim("wasper stop")}
6307
7436
  `);
6308
- break;
7437
+ return;
7438
+ }
6309
7439
  }
6310
- case "s":
6311
- printInlineStatus(ctx);
6312
- break;
6313
- case "?":
6314
- case "h":
6315
- printInteractiveHelp();
6316
- break;
6317
- case "q":
6318
- process.emit("SIGINT");
6319
- break;
6320
7440
  }
7441
+ const slashCmd = cmd.startsWith("/") ? cmd : `/${cmd}`;
7442
+ await runSlashCommand(slashCmd, ctx, reload);
7443
+ refreshStatus();
7444
+ refreshAuthSuggestions();
6321
7445
  };
7446
+ refreshAuthSuggestions();
7447
+ repl.setStatus(buildStatus());
7448
+ repl.start(handler);
6322
7449
  }
6323
7450
  function printInlineStatus(ctx) {
6324
7451
  const state = hasState() ? getState() : null;
6325
7452
  const f = getFeatures();
6326
7453
  const cfg = getServerConfig();
6327
- const flag = (on) => on ? paint.green("on") : paint.red("off");
7454
+ const on = (v) => v ? paint.green("on") : paint.dim("off");
7455
+ const dot = paint.dim("\xB7");
6328
7456
  console.log(`
6329
- ${paint.dim("\u25CF")} ${paint.bold("OpenAPI Agent")} PID ${process.pid} port ${ctx.PORT}`);
7457
+ ${paint.green("\u25CF")} ${paint.bold("wasper")} ${paint.dim("PID " + process.pid)} ${dot} ${paint.dim(":" + ctx.PORT)}`);
6330
7458
  if (state) {
6331
- console.log(` ${paint.bold(state.spec.title)} v${state.spec.version} \xB7 ${state.operations.length} endpoints`);
7459
+ console.log(` ${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} ${dot} ${paint.green(state.operations.length + " endpoints")}`);
7460
+ } else {
7461
+ console.log(` ${paint.dim("no spec loaded")}`);
6332
7462
  }
6333
- console.log(` mcp ${flag(f.mcp)} ${paint.dim("\xB7")} proxy ${flag(f.proxy)} ${paint.dim("\xB7")} ai ${flag(f.ai)} ${paint.dim("\xB7")} readonly ${flag(f.readonly)} ${paint.dim("\xB7")} token ${cfg.token ? paint.green("set") : paint.dim("none")}`);
7463
+ console.log(` mcp ${on(f.mcp)} ${dot} proxy ${on(f.proxy)} ${dot} ai ${on(f.ai)} ${dot} readonly ${on(f.readonly)} ${dot} token ${cfg.token ? paint.green("set") : paint.dim("none")}`);
6334
7464
  const active = dbQueries.getActiveProfile();
6335
- if (active)
6336
- console.log(` auth role: ${paint.bold(active.name)} ${paint.dim(`(${active.type})`)}`);
7465
+ console.log(` auth ${active ? paint.bold(active.name) + " " + paint.dim("(" + active.type + ")") : paint.dim("none")}`);
6337
7466
  console.log();
6338
7467
  }
6339
7468
  async function runSlashCommand(input, ctx, reload) {
@@ -6345,6 +7474,7 @@ async function runSlashCommand(input, ctx, reload) {
6345
7474
  const cur = getFeatures()[name];
6346
7475
  const next = arg === "on" ? true : arg === "off" ? false : !cur;
6347
7476
  setFeatures({ [name]: next });
7477
+ persistAndBroadcastFeatures();
6348
7478
  console.log(` ${next ? paint.green("\u2713") : paint.yellow("\u25CB")} ${label} ${next ? paint.green("enabled") : paint.yellow("disabled")}
6349
7479
  `);
6350
7480
  };
@@ -6489,40 +7619,48 @@ async function runSlashCommand(input, ctx, reload) {
6489
7619
  }
6490
7620
  }
6491
7621
  function printInteractiveHelp() {
7622
+ const k = (s) => paint.bold(s);
7623
+ const d = (s) => paint.dim(s);
7624
+ const hr = d("\u2500".repeat(50));
6492
7625
  console.log(`
6493
- ${paint.bold("Keys")}
6494
- ${paint.dim("\u2500".repeat(46))}
6495
- ${paint.bold("r")} Hot-reload OpenAPI spec from URL
6496
- ${paint.bold("s")} Print current status
6497
- ${paint.bold("b")} Detach to background (survive terminal close)
6498
- ${paint.bold("q")} Quit gracefully
6499
- ${paint.bold("/")} Type a slash command ${paint.dim("(Esc cancels)")}
7626
+ ${paint.bold("Keys")} ${d("(when input is empty)")}
7627
+ ${hr}
7628
+ ${k("r")} Hot-reload spec ${k("b")} Detach to background
7629
+ ${k("s")} Print status ${k("q")} Quit
7630
+ ${k("/")} Start a command ${k("?")} This help
7631
+
7632
+ ${d("\u2191 / \u2193")} cycle command history \xB7 ${d("\u2192 or Tab")} accept autocomplete
7633
+ ${d("Ctrl+L")} clear screen \xB7 ${d("Ctrl+U")} clear input \xB7 ${d("Esc")} cancel
6500
7634
 
6501
7635
  ${paint.bold("Slash commands")}
6502
- ${paint.dim("\u2500".repeat(46))}
6503
- ${paint.bold("/mcp")} ${paint.dim("[on|off]")} Toggle the MCP endpoint
6504
- ${paint.bold("/proxy")} ${paint.dim("[on|off]")} Toggle the HTTP proxy
6505
- ${paint.bold("/ai")} ${paint.dim("[on|off]")} Toggle the AI chat endpoint
6506
- ${paint.bold("/readonly")} ${paint.dim("[on|off]")} Block non-GET upstream requests
6507
- ${paint.bold("/auth")} List auth roles
6508
- ${paint.bold("/auth use")} ${paint.dim("<name>")} Switch active auth role
6509
- ${paint.bold("/auth none")} Disable auth
6510
- ${paint.bold("/token")} ${paint.dim("[new|off|<v>]")} Show / rotate / set the access token
6511
- ${paint.bold("/spec")} ${paint.dim("<url>")} Load a different OpenAPI spec
6512
- ${paint.bold("/tail")} ${paint.dim("[on|off]")} Live request log in this terminal
6513
- ${paint.bold("/open")} Open the studio in a browser
6514
- ${paint.bold("/status")} ${paint.dim("\xB7")} ${paint.bold("/reload")} ${paint.dim("\xB7")} ${paint.bold("/help")} ${paint.dim("\xB7")} ${paint.bold("/quit")}
7636
+ ${hr}
7637
+ ${k("/spec")} ${d("<url>")} Load a different OpenAPI spec
7638
+ ${k("/mcp")} ${d("[on|off]")} Toggle the MCP endpoint
7639
+ ${k("/proxy")} ${d("[on|off]")} Toggle the HTTP proxy
7640
+ ${k("/ai")} ${d("[on|off]")} Toggle the AI chat endpoint
7641
+ ${k("/readonly")} ${d("[on|off]")} Block non-GET upstream requests
7642
+ ${k("/auth")} List saved auth profiles
7643
+ ${k("/auth use")} ${d("<name>")} Switch active auth profile
7644
+ ${k("/auth none")} Disable auth
7645
+ ${k("/token")} ${d("[new|off|<v>]")} Show / rotate / set the access token
7646
+ ${k("/tail")} ${d("[on|off]")} Live request log in this terminal
7647
+ ${k("/open")} Open the studio in a browser
7648
+ ${k("/update")} Update wasper to the latest version
7649
+ ${k("/status")} ${k("/reload")} ${k("/help")} ${k("/quit")}
6515
7650
  `);
6516
7651
  }
6517
7652
  function printHelp() {
6518
7653
  console.log(`
6519
7654
  Usage: wasper [start] [options]
6520
7655
 
6521
- openapi-agent [--url <spec-url>] [--port <port>] Start in foreground
7656
+ wasper [--url <spec-url>] [--port <port>] Start in foreground (auto-resumes last spec)
6522
7657
  wasper start --background Start in background
6523
7658
  wasper stop Stop background server
6524
7659
  wasper status Show server status
6525
7660
  wasper reload Hot-reload the spec
7661
+ wasper ls List saved specs (history)
7662
+ wasper use <number|url> Start with a saved spec
7663
+ wasper rm <number|url> Remove a spec from history
6526
7664
 
6527
7665
  Options:
6528
7666
  --url, -u OpenAPI spec URL or local path
@@ -6641,9 +7779,11 @@ async function runFirstTimeSetup(port, origin) {
6641
7779
  ${paint.dim("Skip future prompts: set WASPER_NO_FIRST_RUN=1")}
6642
7780
  `);
6643
7781
  }
7782
+ var SPIN_FRAMES;
6644
7783
  var init_start = __esm(() => {
6645
7784
  init_server();
6646
7785
  init_handler();
7786
+ init_capture();
6647
7787
  init_routes();
6648
7788
  init_bus();
6649
7789
  init_db();
@@ -6652,6 +7792,101 @@ var init_start = __esm(() => {
6652
7792
  init_config();
6653
7793
  init_update();
6654
7794
  init_ui();
7795
+ init_repl();
7796
+ init_routes();
7797
+ SPIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
7798
+ });
7799
+
7800
+ // src/commands/use.ts
7801
+ var exports_use = {};
7802
+ __export(exports_use, {
7803
+ run: () => run7
7804
+ });
7805
+ async function run7() {
7806
+ const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
7807
+ const target = args[1];
7808
+ if (!target) {
7809
+ console.error(`
7810
+ Usage: wasper use <number|url>
7811
+ `);
7812
+ process.exit(1);
7813
+ }
7814
+ const history = dbQueries.getSpecHistory();
7815
+ let url = null;
7816
+ const num = parseInt(target, 10);
7817
+ if (!isNaN(num) && num >= 1 && num <= history.length) {
7818
+ url = history[num - 1]?.url ?? null;
7819
+ } else if (target.startsWith("http")) {
7820
+ url = target;
7821
+ } else {
7822
+ const match = history.find((r) => r.title?.toLowerCase().includes(target.toLowerCase()));
7823
+ if (match)
7824
+ url = match.url;
7825
+ }
7826
+ if (!url) {
7827
+ console.error(`
7828
+ ${paint.red("\u2717")} Spec not found: ${target}
7829
+ `);
7830
+ console.error(` Run ${paint.cyan("wasper ls")} to see saved specs.
7831
+ `);
7832
+ process.exit(1);
7833
+ }
7834
+ console.log(`
7835
+ ${paint.dim("\u2192")} Starting with ${paint.cyan(url)}
7836
+ `);
7837
+ const { run: startRun } = await Promise.resolve().then(() => (init_start(), exports_start));
7838
+ await startRun({ url });
7839
+ }
7840
+ var init_use = __esm(() => {
7841
+ init_db();
7842
+ init_ui();
7843
+ });
7844
+
7845
+ // src/commands/rm.ts
7846
+ var exports_rm = {};
7847
+ __export(exports_rm, {
7848
+ run: () => run8
7849
+ });
7850
+ async function run8() {
7851
+ const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
7852
+ const target = args[1];
7853
+ if (!target) {
7854
+ console.error(`
7855
+ Usage: wasper rm <number|url>
7856
+ `);
7857
+ process.exit(1);
7858
+ }
7859
+ const history = dbQueries.getSpecHistory();
7860
+ let id = null;
7861
+ let label = null;
7862
+ const num = parseInt(target, 10);
7863
+ if (!isNaN(num) && num >= 1 && num <= history.length) {
7864
+ const row = history[num - 1];
7865
+ if (row) {
7866
+ id = row.id;
7867
+ label = row.title ?? row.url;
7868
+ }
7869
+ } else {
7870
+ const match = history.find((r) => r.url === target || r.title?.toLowerCase().includes(target.toLowerCase()));
7871
+ if (match) {
7872
+ id = match.id;
7873
+ label = match.title ?? match.url;
7874
+ }
7875
+ }
7876
+ if (!id) {
7877
+ console.error(`
7878
+ ${paint.red("\u2717")} Spec not found: ${target}
7879
+ `);
7880
+ process.exit(1);
7881
+ }
7882
+ dbQueries.deleteSpec(id);
7883
+ console.log(`
7884
+ ${paint.green("\u2713")} Removed ${paint.dim(label ?? id)}
7885
+ `);
7886
+ }
7887
+ var init_rm = __esm(() => {
7888
+ init_db();
7889
+ init_ui();
6655
7890
  });
6656
7891
 
6657
7892
  // cli.ts
@@ -6675,6 +7910,17 @@ switch (subcommand) {
6675
7910
  case "reload":
6676
7911
  await Promise.resolve().then(() => (init_reload(), exports_reload)).then((m) => m.run());
6677
7912
  break;
7913
+ case "ls":
7914
+ case "list":
7915
+ await Promise.resolve().then(() => (init_ls(), exports_ls)).then((m) => m.run());
7916
+ break;
7917
+ case "use":
7918
+ await Promise.resolve().then(() => (init_use(), exports_use)).then((m) => m.run());
7919
+ break;
7920
+ case "rm":
7921
+ case "remove":
7922
+ await Promise.resolve().then(() => (init_rm(), exports_rm)).then((m) => m.run());
7923
+ break;
6678
7924
  case "start":
6679
7925
  default:
6680
7926
  await Promise.resolve().then(() => (init_start(), exports_start)).then((m) => m.run());