wasper-cli 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +118 -0
  3. package/dist/cli.js +2536 -931
  4. package/dist/index.js +2134 -685
  5. package/package.json +1 -1
package/dist/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.3.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,367 @@ 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
+ CREATE TABLE IF NOT EXISTS chat_memory (
685
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
686
+ role TEXT NOT NULL,
687
+ content TEXT NOT NULL,
688
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
689
+ );
690
+ CREATE INDEX IF NOT EXISTS idx_memory_created ON chat_memory(created_at DESC);
691
+ `;
692
+
693
+ // src/db/index.ts
694
+ import { Database } from "bun:sqlite";
695
+ import { join as join3 } from "path";
696
+ import { mkdirSync, existsSync } from "fs";
697
+ import { homedir as homedir3 } from "os";
698
+ import { randomUUID } from "crypto";
699
+ function resolveDataDir() {
700
+ if (process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR) {
701
+ return process.env.WASPER_DATA_DIR ?? process.env.OPENAPI_AGENT_DATA_DIR;
702
+ }
703
+ try {
704
+ const legacy = join3(import.meta.dir, "../../data");
705
+ if (!Bun.main.includes("$bunfs") && (existsSync(join3(legacy, "wasper.db")) || existsSync(join3(legacy, "openapi-agent.db"))))
706
+ return legacy;
707
+ } catch {}
708
+ const oldDir = join3(homedir3(), ".openapi-agent", "data");
709
+ if (existsSync(oldDir))
710
+ return oldDir;
711
+ return join3(homedir3(), ".wasper", "data");
712
+ }
713
+ var DATA_DIR, DB_PATH, db, hasOldSchema, dbQueries;
714
+ var init_db = __esm(() => {
715
+ DATA_DIR = resolveDataDir();
716
+ mkdirSync(DATA_DIR, { recursive: true });
717
+ DB_PATH = join3(DATA_DIR, existsSync(join3(DATA_DIR, "openapi-agent.db")) && !existsSync(join3(DATA_DIR, "wasper.db")) ? "openapi-agent.db" : "wasper.db");
718
+ db = new Database(DB_PATH, { create: true });
719
+ db.exec("PRAGMA journal_mode = WAL;");
720
+ db.exec("PRAGMA foreign_keys = OFF;");
721
+ hasOldSchema = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name='specs'").get() !== null;
722
+ if (hasOldSchema) {
723
+ db.exec("DROP TABLE IF EXISTS tools; DROP TABLE IF EXISTS specs; DROP TABLE IF EXISTS auth_configs; DROP TABLE IF EXISTS request_logs;");
724
+ }
725
+ db.exec(SCHEMA);
726
+ dbQueries = {
727
+ getAuthConfig: () => db.query("SELECT * FROM auth_config WHERE id = 'default'").get(),
728
+ setAuthConfig: (type, config) => db.query(`INSERT INTO auth_config (id, type, config, updated_at)
729
+ VALUES ('default', ?, ?, unixepoch())
730
+ ON CONFLICT(id) DO UPDATE SET type = excluded.type, config = excluded.config, updated_at = unixepoch()`).run(type, JSON.stringify(config)),
731
+ updateTokenCache: (tokenCache) => db.query("UPDATE auth_config SET token_cache = ? WHERE id = 'default'").run(tokenCache ? JSON.stringify(tokenCache) : null),
732
+ getRecentLogs: (limit = 500) => db.query("SELECT * FROM request_logs ORDER BY created_at DESC LIMIT ?").all(limit),
733
+ insertLog: (data) => db.query(`INSERT INTO request_logs
734
+ (id, source, tool_name, method, url, request_headers, request_body,
735
+ status_code, response_headers, response_body, latency_ms, error)
736
+ VALUES ($id, $source, $tool_name, $method, $url, $request_headers, $request_body,
737
+ $status_code, $response_headers, $response_body, $latency_ms, $error)`).run({
738
+ $id: data.id,
739
+ $source: data.source,
740
+ $tool_name: data.tool_name,
741
+ $method: data.method,
742
+ $url: data.url,
743
+ $request_headers: data.request_headers,
744
+ $request_body: data.request_body,
745
+ $status_code: data.status_code,
746
+ $response_headers: data.response_headers,
747
+ $response_body: data.response_body,
748
+ $latency_ms: data.latency_ms,
749
+ $error: data.error
750
+ }),
751
+ clearLogs: () => db.query("DELETE FROM request_logs").run(),
752
+ getRules: () => db.query("SELECT * FROM intercept_rules ORDER BY sort_order, created_at").all(),
753
+ insertRule: (rule) => db.query(`INSERT INTO intercept_rules (id,enabled,name,sort_order,match_path,match_method,target_host,strip_prefix,add_prefix,add_headers)
754
+ VALUES ($id,$enabled,$name,$sort_order,$match_path,$match_method,$target_host,$strip_prefix,$add_prefix,$add_headers)`).run({
755
+ $id: rule.id,
756
+ $enabled: rule.enabled,
757
+ $name: rule.name,
758
+ $sort_order: rule.sort_order,
759
+ $match_path: rule.match_path,
760
+ $match_method: rule.match_method,
761
+ $target_host: rule.target_host,
762
+ $strip_prefix: rule.strip_prefix,
763
+ $add_prefix: rule.add_prefix,
764
+ $add_headers: rule.add_headers
765
+ }),
766
+ updateRule: (id, patch) => {
767
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
768
+ const params = { $id: id };
769
+ for (const [k, v] of Object.entries(patch))
770
+ params[`$${k}`] = v;
771
+ db.query(`UPDATE intercept_rules SET ${cols} WHERE id = $id`).run(params);
772
+ },
773
+ deleteRule: (id) => db.query("DELETE FROM intercept_rules WHERE id = ?").run(id),
774
+ getSettings: () => db.query("SELECT value FROM settings WHERE key='app' LIMIT 1").get() ?? null,
775
+ setSettings: (value) => {
776
+ db.run("INSERT INTO settings(key,value) VALUES('app',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [JSON.stringify(value)]);
777
+ },
778
+ getProfiles: () => db.query("SELECT * FROM auth_profiles ORDER BY name COLLATE NOCASE").all(),
779
+ getActiveProfile: () => db.query("SELECT * FROM auth_profiles WHERE is_active = 1 LIMIT 1").get(),
780
+ insertProfile: (p) => db.query(`INSERT INTO auth_profiles (id,name,description,type,config,token_cache,is_active)
781
+ VALUES ($id,$name,$description,$type,$config,$token_cache,$is_active)`).run({
782
+ $id: p.id,
783
+ $name: p.name,
784
+ $description: p.description,
785
+ $type: p.type,
786
+ $config: p.config,
787
+ $token_cache: p.token_cache,
788
+ $is_active: p.is_active
789
+ }),
790
+ updateProfile: (id, patch) => {
791
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
792
+ const params = { $id: id };
793
+ for (const [k, v] of Object.entries(patch))
794
+ params[`$${k}`] = v;
795
+ db.query(`UPDATE auth_profiles SET ${cols} WHERE id = $id`).run(params);
796
+ },
797
+ deleteProfile: (id) => db.query("DELETE FROM auth_profiles WHERE id = ?").run(id),
798
+ activateProfile: (id) => {
799
+ const profile = db.query("SELECT * FROM auth_profiles WHERE id = ?").get(id);
800
+ if (!profile)
801
+ return;
802
+ db.query("UPDATE auth_profiles SET is_active = 0").run();
803
+ db.query("UPDATE auth_profiles SET is_active = 1 WHERE id = ?").run(id);
804
+ db.query(`INSERT INTO auth_config (id, type, config, updated_at) VALUES ('default', $type, $config, unixepoch())
805
+ ON CONFLICT(id) DO UPDATE SET type=excluded.type, config=excluded.config, updated_at=unixepoch()`).run({ $type: profile.type, $config: profile.config });
806
+ },
807
+ getSavedRequests: () => db.query("SELECT * FROM saved_requests ORDER BY folder, name COLLATE NOCASE").all(),
808
+ getSavedRequest: (id) => db.query("SELECT * FROM saved_requests WHERE id = ?").get(id),
809
+ insertSavedRequest: (r) => db.query(`INSERT INTO saved_requests
810
+ (id, name, folder, method, url, headers, params, body, body_type, raw_type, form_rows, auth, notes)
811
+ VALUES ($id,$name,$folder,$method,$url,$headers,$params,$body,$body_type,$raw_type,$form_rows,$auth,$notes)`).run({
812
+ $id: r.id,
813
+ $name: r.name,
814
+ $folder: r.folder,
815
+ $method: r.method,
816
+ $url: r.url,
817
+ $headers: r.headers,
818
+ $params: r.params,
819
+ $body: r.body,
820
+ $body_type: r.body_type,
821
+ $raw_type: r.raw_type,
822
+ $form_rows: r.form_rows,
823
+ $auth: r.auth,
824
+ $notes: r.notes
825
+ }),
826
+ updateSavedRequest: (id, patch) => {
827
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
828
+ const params = { $id: id };
829
+ for (const [k, v] of Object.entries(patch))
830
+ params[`$${k}`] = v;
831
+ db.query(`UPDATE saved_requests SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
832
+ },
833
+ deleteSavedRequest: (id) => db.query("DELETE FROM saved_requests WHERE id = ?").run(id),
834
+ getSpecHistory: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC").all(),
835
+ getLastSpec: () => db.query("SELECT * FROM spec_history ORDER BY last_used DESC LIMIT 1").get(),
836
+ upsertSpec: (url, title, version, endpointCount) => {
837
+ const existing = db.query("SELECT id FROM spec_history WHERE url = ?").get(url);
838
+ if (existing) {
839
+ db.query("UPDATE spec_history SET title=?, version=?, endpoint_count=?, last_used=unixepoch() WHERE url=?").run(title, version, endpointCount, url);
840
+ } else {
841
+ db.query(`INSERT INTO spec_history (id, url, title, version, endpoint_count)
842
+ VALUES (?, ?, ?, ?, ?)`).run(randomUUID(), url, title, version, endpointCount);
843
+ }
844
+ },
845
+ deleteSpec: (id) => {
846
+ db.query("DELETE FROM spec_history WHERE id = ?").run(id);
847
+ },
848
+ getWorkflows: () => db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all(),
849
+ getWorkflow: (id) => db.query("SELECT * FROM workflows WHERE id = ?").get(id),
850
+ insertWorkflow: (w) => db.query("INSERT INTO workflows (id, name, description, steps) VALUES ($id, $name, $description, $steps)").run({ $id: w.id, $name: w.name, $description: w.description, $steps: w.steps }),
851
+ updateWorkflow: (id, patch) => {
852
+ const cols = Object.keys(patch).map((k) => `${k} = $${k}`).join(", ");
853
+ const params = { $id: id };
854
+ for (const [k, v] of Object.entries(patch))
855
+ params[`$${k}`] = v;
856
+ db.query(`UPDATE workflows SET ${cols}, updated_at = unixepoch() WHERE id = $id`).run(params);
857
+ },
858
+ deleteWorkflow: (id) => db.query("DELETE FROM workflows WHERE id = ?").run(id),
859
+ getCaptureBins: () => db.query("SELECT * FROM capture_bins ORDER BY created_at DESC").all(),
860
+ getCaptureBin: (id) => db.query("SELECT * FROM capture_bins WHERE id = ?").get(id),
861
+ insertCaptureBin: (id, name) => {
862
+ db.query("INSERT INTO capture_bins (id, name) VALUES (?, ?)").run(id, name);
863
+ },
864
+ deleteCaptureBin: (id) => {
865
+ db.query("DELETE FROM capture_bins WHERE id = ?").run(id);
866
+ },
867
+ getSetting: (key) => (db.query("SELECT value FROM settings WHERE key = ?").get(key) ?? null)?.value ?? null,
868
+ setSetting: (key, value) => {
869
+ db.run("INSERT INTO settings(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value", [key, value]);
870
+ },
871
+ saveMemory: (role, content) => {
872
+ db.query("INSERT INTO chat_memory (role, content) VALUES (?, ?)").run(role, content);
873
+ },
874
+ getMemory: (limit = 20) => {
875
+ const rows = db.query("SELECT role, content FROM chat_memory ORDER BY created_at DESC LIMIT ?").all(limit);
876
+ return rows.reverse();
877
+ },
878
+ clearMemory: () => {
879
+ db.query("DELETE FROM chat_memory").run();
880
+ },
881
+ trimMemory: (keepLast = 40) => {
882
+ db.query("DELETE FROM chat_memory WHERE id NOT IN (SELECT id FROM chat_memory ORDER BY created_at DESC LIMIT ?)").run(keepLast);
883
+ }
884
+ };
885
+ });
886
+
887
+ // src/commands/ls.ts
888
+ var exports_ls = {};
889
+ __export(exports_ls, {
890
+ run: () => run5
891
+ });
892
+ function ago(ts) {
893
+ const secs = Math.floor(Date.now() / 1000) - ts;
894
+ if (secs < 60)
895
+ return "just now";
896
+ if (secs < 3600)
897
+ return `${Math.floor(secs / 60)}m ago`;
898
+ if (secs < 86400)
899
+ return `${Math.floor(secs / 3600)}h ago`;
900
+ if (secs < 86400 * 30)
901
+ return `${Math.floor(secs / 86400)}d ago`;
902
+ return new Date(ts * 1000).toLocaleDateString();
903
+ }
904
+ async function run5() {
905
+ const history = dbQueries.getSpecHistory();
906
+ if (history.length === 0) {
907
+ console.log(`
908
+ No saved specs yet.
909
+ `);
910
+ console.log(` ${paint.dim("Run:")} wasper --url <spec-url>
911
+ `);
912
+ process.exit(0);
913
+ }
914
+ const COL_URL = isTTY ? 50 : 60;
915
+ const COL_TITLE = 28;
916
+ console.log();
917
+ if (isTTY) {
918
+ const header = ` ${"#".padEnd(3)} ${"URL".padEnd(COL_URL)} ${"Title".padEnd(COL_TITLE)} ${"Endpoints".padEnd(10)} Last used`;
919
+ console.log(paint.dim(header));
920
+ console.log(paint.dim(" " + "\u2500".repeat(header.length - 2)));
921
+ }
922
+ history.forEach((row, i) => {
923
+ const num = String(i + 1).padEnd(3);
924
+ const url = row.url.length > COL_URL ? row.url.slice(0, COL_URL - 1) + "\u2026" : row.url.padEnd(COL_URL);
925
+ const title = (row.title ?? "\u2014").slice(0, COL_TITLE).padEnd(COL_TITLE);
926
+ const eps = (row.endpoint_count != null ? String(row.endpoint_count) : "\u2014").padEnd(10);
927
+ const time = ago(row.last_used);
928
+ console.log(` ${paint.cyan(num)} ${url} ${paint.dim(title)} ${eps} ${paint.dim(time)}`);
929
+ });
930
+ console.log();
931
+ console.log(paint.dim(` wasper use <number> \u2014 start server with that spec`));
932
+ console.log(paint.dim(` wasper rm <number> \u2014 remove a spec from history`));
933
+ console.log();
934
+ }
935
+ var init_ls = __esm(() => {
936
+ init_db();
937
+ init_ui();
938
+ });
939
+
579
940
  // ../../node_modules/js-yaml/dist/js-yaml.mjs
580
941
  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
942
  if (from && typeof from === "object" || typeof from === "function")
@@ -3329,7 +3690,7 @@ var init_js_yaml = __esm(() => {
3329
3690
  });
3330
3691
 
3331
3692
  // src/openapi/parser.ts
3332
- import { randomUUID } from "crypto";
3693
+ import { randomUUID as randomUUID2 } from "crypto";
3333
3694
  async function fetchAndParseSpec(url, name) {
3334
3695
  const res = await fetch(url, { headers: { Accept: "application/json, application/yaml, text/yaml, */*" } });
3335
3696
  if (!res.ok)
@@ -3358,402 +3719,261 @@ function parseSpecText(text, url, name) {
3358
3719
  } catch {}
3359
3720
  }
3360
3721
  return {
3361
- id: randomUUID(),
3722
+ id: randomUUID2(),
3362
3723
  name: name ?? (info.title ?? "Untitled API"),
3363
3724
  url: url ?? null,
3364
3725
  raw: JSON.stringify(raw),
3365
3726
  title: info.title ?? "Untitled API",
3366
3727
  version: info.version ?? "1.0.0",
3367
3728
  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
- });
3429
- }
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
- }];
3449
- });
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
3498
- };
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();
3530
- });
3531
-
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";
3616
- 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;
3729
+ operations: extractOperations(doc),
3730
+ securitySchemes: extractSecuritySchemes(doc)
3731
+ };
3732
+ }
3733
+ function deref(node, root, depth = 0) {
3734
+ if (depth > 20 || node === null || typeof node !== "object")
3735
+ return node;
3736
+ if (Array.isArray(node))
3737
+ return node.map((item) => deref(item, root, depth + 1));
3738
+ const obj = node;
3739
+ if (typeof obj["$ref"] === "string" && obj["$ref"].startsWith("#/")) {
3740
+ return deref(resolveRef(obj["$ref"], root), root, depth + 1);
3620
3741
  }
3621
- 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");
3742
+ const result = {};
3743
+ for (const [k, v] of Object.entries(obj)) {
3744
+ result[k] = deref(v, root, depth + 1);
3745
+ }
3746
+ return result;
3630
3747
  }
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;");
3748
+ function resolveRef(ref, root) {
3749
+ let current = root;
3750
+ for (const part of ref.slice(2).split("/")) {
3751
+ const key = part.replace(/~1/g, "/").replace(/~0/g, "~");
3752
+ if (current && typeof current === "object" && !Array.isArray(current)) {
3753
+ current = current[key];
3754
+ } else
3755
+ return;
3642
3756
  }
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]);
3757
+ return current;
3758
+ }
3759
+ function extractOperations(doc) {
3760
+ const paths = doc.paths ?? {};
3761
+ const ops = [];
3762
+ let idx = 0;
3763
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
3764
+ if (!pathItem || typeof pathItem !== "object")
3765
+ continue;
3766
+ const pi = pathItem;
3767
+ const pathParams = parseParameters(pi.parameters ?? []);
3768
+ for (const method of HTTP_METHODS) {
3769
+ const opObj = pi[method];
3770
+ if (!opObj)
3771
+ continue;
3772
+ const rawId = opObj.operationId ?? `${method}_${pathStr.replace(/[^a-zA-Z0-9]/g, "_")}_${idx++}`;
3773
+ const operationId = rawId.replace(/[^a-zA-Z0-9_]/g, "_").replace(/^_+|_+$/g, "").replace(/_+/g, "_");
3774
+ const opParams = parseParameters(opObj.parameters ?? []);
3775
+ const paramMap = new Map;
3776
+ for (const p of [...pathParams, ...opParams])
3777
+ paramMap.set(`${p.in}:${p.name}`, p);
3778
+ ops.push({
3779
+ operationId,
3780
+ method,
3781
+ path: pathStr,
3782
+ summary: opObj.summary,
3783
+ description: opObj.description,
3784
+ tags: opObj.tags ?? ["default"],
3785
+ parameters: [...paramMap.values()],
3786
+ requestBody: parseRequestBody(opObj.requestBody),
3787
+ responses: parseResponses(opObj.responses ?? {}),
3788
+ security: opObj.security
3789
+ });
3755
3790
  }
3791
+ }
3792
+ return ops;
3793
+ }
3794
+ function parseParameters(params) {
3795
+ return params.flatMap((p) => {
3796
+ if (!p || typeof p !== "object")
3797
+ return [];
3798
+ const param = p;
3799
+ const name = param.name;
3800
+ const paramIn = param.in;
3801
+ if (!name || !paramIn || !["path", "query", "header", "cookie"].includes(paramIn))
3802
+ return [];
3803
+ return [{
3804
+ name,
3805
+ in: paramIn,
3806
+ description: param.description,
3807
+ required: param.required ?? paramIn === "path",
3808
+ schema: param.schema ?? { type: "string" }
3809
+ }];
3810
+ });
3811
+ }
3812
+ function parseRequestBody(rb) {
3813
+ if (!rb || typeof rb !== "object")
3814
+ return;
3815
+ const body = rb;
3816
+ const content = body.content ?? {};
3817
+ const contentType = Object.keys(content).find((ct) => ct.includes("json")) ?? Object.keys(content)[0];
3818
+ if (!contentType)
3819
+ return;
3820
+ return {
3821
+ description: body.description,
3822
+ required: body.required ?? false,
3823
+ contentType,
3824
+ schema: content[contentType]?.schema ?? { type: "object" }
3825
+ };
3826
+ }
3827
+ function parseResponses(responses) {
3828
+ const result = {};
3829
+ for (const [code, resp] of Object.entries(responses)) {
3830
+ if (!resp || typeof resp !== "object")
3831
+ continue;
3832
+ const r = resp;
3833
+ const content = r.content;
3834
+ const jsonCt = content ? Object.keys(content).find((ct) => ct.includes("json")) : undefined;
3835
+ result[code] = {
3836
+ description: r.description,
3837
+ contentType: jsonCt,
3838
+ schema: jsonCt ? content?.[jsonCt]?.schema : undefined
3839
+ };
3840
+ }
3841
+ return result;
3842
+ }
3843
+ function extractSecuritySchemes(doc) {
3844
+ const schemes = doc.components?.securitySchemes;
3845
+ if (!schemes)
3846
+ return {};
3847
+ const result = {};
3848
+ for (const [name, scheme] of Object.entries(schemes)) {
3849
+ if (!scheme || typeof scheme !== "object")
3850
+ continue;
3851
+ const s = scheme;
3852
+ result[name] = {
3853
+ type: s.type,
3854
+ scheme: s.scheme,
3855
+ in: s.in,
3856
+ name: s.name,
3857
+ flows: s.flows,
3858
+ openIdConnectUrl: s.openIdConnectUrl
3859
+ };
3860
+ }
3861
+ return result;
3862
+ }
3863
+ function toEnvKey(str) {
3864
+ 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();
3865
+ }
3866
+ function extractSuggestedVars(rawText, baseUrl) {
3867
+ let raw;
3868
+ try {
3869
+ raw = rawText.trimStart().startsWith("{") ? JSON.parse(rawText) : index_vite_proxy_tmp_default.load(rawText);
3870
+ } catch {
3871
+ return [];
3872
+ }
3873
+ const vars = [];
3874
+ const seen = new Set;
3875
+ const add = (key, value, description, source) => {
3876
+ const k = key.trim();
3877
+ if (!k || seen.has(k))
3878
+ return;
3879
+ seen.add(k);
3880
+ vars.push({ key: k, value, description, source });
3756
3881
  };
3882
+ if (baseUrl)
3883
+ add("baseUrl", baseUrl, "API base URL", "server");
3884
+ const servers = raw.servers ?? [];
3885
+ for (const server of servers.slice(0, 5)) {
3886
+ if (!server.variables)
3887
+ continue;
3888
+ for (const [key, def] of Object.entries(server.variables)) {
3889
+ add(toEnvKey(key), def.default ?? "", def.description ?? `Server variable: ${key}`, "server");
3890
+ }
3891
+ }
3892
+ const components = raw.components ?? {};
3893
+ const secSchemes = components.securitySchemes ?? {};
3894
+ for (const [schemeName, scheme] of Object.entries(secSchemes)) {
3895
+ if (!scheme || typeof scheme !== "object")
3896
+ continue;
3897
+ const type = scheme.type;
3898
+ const slug = toEnvKey(schemeName);
3899
+ if (type === "http") {
3900
+ const httpScheme = (scheme.scheme ?? "").toLowerCase();
3901
+ if (httpScheme === "bearer") {
3902
+ add(`${slug}Token`, "", `Bearer token for ${schemeName}`, "auth");
3903
+ } else if (httpScheme === "basic") {
3904
+ add(`${slug}Username`, "", `Username for ${schemeName}`, "auth");
3905
+ add(`${slug}Password`, "", `Password for ${schemeName}`, "auth");
3906
+ }
3907
+ } else if (type === "apiKey") {
3908
+ const keyName = scheme.name ?? schemeName;
3909
+ add(toEnvKey(keyName), "", `API key header/param: ${keyName}`, "auth");
3910
+ } else if (type === "oauth2") {
3911
+ add(`${slug}ClientId`, "", `OAuth2 client ID for ${schemeName}`, "auth");
3912
+ add(`${slug}ClientSecret`, "", `OAuth2 client secret for ${schemeName}`, "auth");
3913
+ add(`${slug}Token`, "", `OAuth2 access token for ${schemeName}`, "auth");
3914
+ } else if (type === "openIdConnect") {
3915
+ add(`${slug}Token`, "", `Access token for ${schemeName}`, "auth");
3916
+ }
3917
+ }
3918
+ const paths = raw.paths ?? {};
3919
+ const pathKeys = Object.keys(paths);
3920
+ const paramFreq = {};
3921
+ for (const [pathStr, pathItem] of Object.entries(paths)) {
3922
+ if (!pathItem || typeof pathItem !== "object")
3923
+ continue;
3924
+ const params = pathItem.parameters ?? [];
3925
+ const seenInPath = new Set;
3926
+ for (const p of params) {
3927
+ if (p.in === "path" && p.name && !seenInPath.has(p.name)) {
3928
+ seenInPath.add(p.name);
3929
+ paramFreq[p.name] = (paramFreq[p.name] ?? 0) + 1;
3930
+ }
3931
+ }
3932
+ for (const [, param] of pathStr.matchAll(/\{([^}]+)\}/g)) {
3933
+ if (!param)
3934
+ continue;
3935
+ if (!seenInPath.has(param)) {
3936
+ seenInPath.add(param);
3937
+ paramFreq[param] = (paramFreq[param] ?? 0) + 1;
3938
+ }
3939
+ }
3940
+ }
3941
+ const threshold = Math.max(3, Math.floor(pathKeys.length * 0.15));
3942
+ for (const [name, count] of Object.entries(paramFreq)) {
3943
+ if (count >= threshold) {
3944
+ add(toEnvKey(name), "", `Common path param: {${name}}`, "path");
3945
+ }
3946
+ }
3947
+ return vars;
3948
+ }
3949
+ var HTTP_METHODS;
3950
+ var init_parser = __esm(() => {
3951
+ init_js_yaml();
3952
+ HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options", "trace"];
3953
+ });
3954
+
3955
+ // src/state.ts
3956
+ function hasState() {
3957
+ return _state !== null;
3958
+ }
3959
+ function getState() {
3960
+ if (!_state)
3961
+ throw new Error("No spec loaded");
3962
+ return _state;
3963
+ }
3964
+ async function loadSpec(url) {
3965
+ const spec = await fetchAndParseSpec(url);
3966
+ _state = { spec, operations: spec.operations, specUrl: url };
3967
+ return _state;
3968
+ }
3969
+ function loadSpecFromText(text, filename) {
3970
+ const spec = parseSpecText(text, undefined, filename?.replace(/\.[^.]+$/, ""));
3971
+ _state = { spec, operations: spec.operations };
3972
+ return _state;
3973
+ }
3974
+ var _state = null;
3975
+ var init_state = __esm(() => {
3976
+ init_parser();
3757
3977
  });
3758
3978
 
3759
3979
  // src/logs/bus.ts
@@ -3787,6 +4007,18 @@ class LogBus {
3787
4007
  }
3788
4008
  }
3789
4009
  }
4010
+ broadcastServerEvent(payload) {
4011
+ if (this.clients.size === 0)
4012
+ return;
4013
+ const data = JSON.stringify({ type: "server_event", ...payload });
4014
+ for (const ws of this.clients) {
4015
+ try {
4016
+ ws.send(data);
4017
+ } catch {
4018
+ this.clients.delete(ws);
4019
+ }
4020
+ }
4021
+ }
3790
4022
  get clientCount() {
3791
4023
  return this.clients.size;
3792
4024
  }
@@ -4262,7 +4494,7 @@ async function executeOperation(op, baseUrl, args) {
4262
4494
  authedHeaders["Content-Type"] = op.requestBody.contentType;
4263
4495
  }
4264
4496
  const startTime = Date.now();
4265
- const logId = randomUUID2();
4497
+ const logId = randomUUID();
4266
4498
  try {
4267
4499
  const res = await fetch(authedUrl, {
4268
4500
  method: op.method.toUpperCase(),
@@ -4519,7 +4751,7 @@ async function proxyHandler(req) {
4519
4751
  const { url: authedUrl, headers: authedHeaders } = await applyAuth(targetUrl, proxyHeaders, authConfig);
4520
4752
  const requestBody = req.method !== "GET" && req.method !== "HEAD" ? await req.text() : null;
4521
4753
  const startTime = Date.now();
4522
- const logId = randomUUID2();
4754
+ const logId = randomUUID();
4523
4755
  try {
4524
4756
  const res = await fetch(authedUrl, {
4525
4757
  method: req.method,
@@ -4611,17 +4843,792 @@ async function proxyHandler(req) {
4611
4843
  });
4612
4844
  }
4613
4845
  }
4614
- var CORS2;
4615
- var init_handler = __esm(() => {
4616
- init_db();
4617
- init_engine();
4618
- init_bus();
4619
- init_state();
4620
- init_config();
4621
- CORS2 = {
4622
- "Access-Control-Allow-Origin": "*",
4623
- "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
4624
- "Access-Control-Allow-Headers": "Content-Type, Authorization"
4846
+ var CORS2;
4847
+ var init_handler = __esm(() => {
4848
+ init_db();
4849
+ init_engine();
4850
+ init_bus();
4851
+ init_state();
4852
+ init_config();
4853
+ CORS2 = {
4854
+ "Access-Control-Allow-Origin": "*",
4855
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
4856
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
4857
+ };
4858
+ });
4859
+
4860
+ // src/proxy/capture.ts
4861
+ async function captureHandler(req) {
4862
+ if (req.method === "OPTIONS") {
4863
+ return new Response(null, { status: 204, headers: CORS3 });
4864
+ }
4865
+ const url = new URL(req.url);
4866
+ const binId = url.pathname.split("/").filter(Boolean)[1];
4867
+ if (!binId)
4868
+ return new Response("Not found", { status: 404 });
4869
+ const bin = dbQueries.getCaptureBin(binId);
4870
+ if (!bin) {
4871
+ return new Response(JSON.stringify({ error: "Capture bin not found or deleted" }), {
4872
+ status: 404,
4873
+ headers: { "Content-Type": "application/json", ...CORS3 }
4874
+ });
4875
+ }
4876
+ const body = req.method !== "GET" && req.method !== "HEAD" ? await req.text().catch(() => null) : null;
4877
+ const reqHeaders = {};
4878
+ for (const [k, v] of req.headers.entries()) {
4879
+ if (!["host", "connection"].includes(k.toLowerCase()))
4880
+ reqHeaders[k] = v;
4881
+ }
4882
+ const id = randomUUID();
4883
+ const now = Date.now();
4884
+ dbQueries.insertLog({
4885
+ id,
4886
+ source: "capture",
4887
+ tool_name: binId,
4888
+ method: req.method,
4889
+ url: req.url,
4890
+ request_headers: JSON.stringify(reqHeaders),
4891
+ request_body: body,
4892
+ status_code: 200,
4893
+ response_headers: null,
4894
+ response_body: null,
4895
+ latency_ms: 0,
4896
+ error: null
4897
+ });
4898
+ logBus.emit({
4899
+ id,
4900
+ source: "capture",
4901
+ tool_name: binId,
4902
+ method: req.method,
4903
+ url: req.url,
4904
+ request_headers: JSON.stringify(reqHeaders),
4905
+ request_body: body,
4906
+ status_code: 200,
4907
+ response_headers: null,
4908
+ response_body: null,
4909
+ latency_ms: 0,
4910
+ error: null,
4911
+ created_at: now
4912
+ });
4913
+ return new Response(JSON.stringify({ ok: true, id, captured_at: now }), {
4914
+ status: 200,
4915
+ headers: { "Content-Type": "application/json", ...CORS3 }
4916
+ });
4917
+ }
4918
+ var CORS3;
4919
+ var init_capture = __esm(() => {
4920
+ init_db();
4921
+ init_bus();
4922
+ CORS3 = {
4923
+ "Access-Control-Allow-Origin": "*",
4924
+ "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD",
4925
+ "Access-Control-Allow-Headers": "*",
4926
+ "Access-Control-Expose-Headers": "*"
4927
+ };
4928
+ });
4929
+
4930
+ // src/workflows/engine.ts
4931
+ function interpolate(val, ctx) {
4932
+ return val.replace(/\{\{(\w+)\}\}/g, (_, k) => ctx[k] ?? `{{${k}}}`);
4933
+ }
4934
+ function interpolateDeep(obj, ctx) {
4935
+ if (typeof obj === "string")
4936
+ return interpolate(obj, ctx);
4937
+ if (Array.isArray(obj))
4938
+ return obj.map((v) => interpolateDeep(v, ctx));
4939
+ if (obj !== null && typeof obj === "object") {
4940
+ const out = {};
4941
+ for (const [k, v] of Object.entries(obj))
4942
+ out[k] = interpolateDeep(v, ctx);
4943
+ return out;
4944
+ }
4945
+ return obj;
4946
+ }
4947
+ function resolveJsonPath(data, path) {
4948
+ if (!path || path === "$")
4949
+ return data;
4950
+ const normalized = path.startsWith("$.") ? path.slice(2) : path.startsWith("$[") ? path.slice(1) : path;
4951
+ if (!normalized)
4952
+ return data;
4953
+ const parts = [];
4954
+ for (const seg of normalized.split(".")) {
4955
+ const m = seg.match(/^(\w+)\[(\d+)\]$/);
4956
+ if (m) {
4957
+ parts.push(m[1], parseInt(m[2], 10));
4958
+ } else if (/^\d+$/.test(seg)) {
4959
+ parts.push(parseInt(seg, 10));
4960
+ } else {
4961
+ parts.push(seg);
4962
+ }
4963
+ }
4964
+ let cur = data;
4965
+ for (const part of parts) {
4966
+ if (cur === null || cur === undefined)
4967
+ return;
4968
+ cur = typeof part === "number" ? cur[part] : cur[part];
4969
+ }
4970
+ return cur;
4971
+ }
4972
+ async function executeStep(step, ctx) {
4973
+ const { spec } = getState();
4974
+ let base = spec.baseUrl;
4975
+ if (!base?.startsWith("http") && spec.url) {
4976
+ try {
4977
+ base = new URL(spec.url).origin;
4978
+ } catch {}
4979
+ }
4980
+ if (!base?.startsWith("http"))
4981
+ throw new Error("Spec has no absolute server URL");
4982
+ let urlPath = step.path;
4983
+ for (const [k, v] of Object.entries(step.pathParams ?? {})) {
4984
+ urlPath = urlPath.replace(`{${k}}`, encodeURIComponent(interpolate(v, ctx)));
4985
+ }
4986
+ urlPath = interpolate(urlPath, ctx);
4987
+ const urlObj = new URL(`${base.replace(/\/$/, "")}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`);
4988
+ for (const [k, v] of Object.entries(step.queryParams ?? {})) {
4989
+ urlObj.searchParams.set(k, interpolate(v, ctx));
4990
+ }
4991
+ const stepHeaders = {};
4992
+ for (const [k, v] of Object.entries(step.headers ?? {})) {
4993
+ stepHeaders[k] = interpolate(v, ctx);
4994
+ }
4995
+ const authRow = dbQueries.getAuthConfig();
4996
+ const authConfig = authRow ? JSON.parse(authRow.config) : { type: "none" };
4997
+ const { url: authedUrl, headers: authedHeaders } = await applyAuth(urlObj.toString(), stepHeaders, authConfig);
4998
+ const noBodyMethod = ["GET", "HEAD", "OPTIONS"].includes(step.method.toUpperCase());
4999
+ let bodyStr;
5000
+ if (!noBodyMethod && step.body !== undefined && step.body !== null) {
5001
+ const interpolated = interpolateDeep(step.body, ctx);
5002
+ bodyStr = typeof interpolated === "string" ? interpolated : JSON.stringify(interpolated);
5003
+ if (!authedHeaders["Content-Type"] && !authedHeaders["content-type"]) {
5004
+ authedHeaders["Content-Type"] = "application/json";
5005
+ }
5006
+ }
5007
+ const start = Date.now();
5008
+ const res = await fetch(authedUrl, {
5009
+ method: step.method.toUpperCase(),
5010
+ headers: authedHeaders,
5011
+ ...bodyStr !== undefined ? { body: bodyStr } : {},
5012
+ signal: AbortSignal.timeout(30000)
5013
+ });
5014
+ const latency = Date.now() - start;
5015
+ const responseHeaders = {};
5016
+ res.headers.forEach((v, k) => {
5017
+ responseHeaders[k] = v;
5018
+ });
5019
+ const responseText = await res.text();
5020
+ let responseData;
5021
+ try {
5022
+ responseData = JSON.parse(responseText);
5023
+ } catch {
5024
+ responseData = responseText;
5025
+ }
5026
+ const extractedVars = {};
5027
+ for (const ext of step.extract ?? []) {
5028
+ const val = resolveJsonPath(responseData, ext.path);
5029
+ if (val !== undefined && val !== null) {
5030
+ extractedVars[ext.var] = typeof val === "string" ? val : JSON.stringify(val);
5031
+ }
5032
+ }
5033
+ const assertions = [];
5034
+ for (const a of step.assert ?? []) {
5035
+ if (a.type === "status") {
5036
+ const expected = a.statusCode ?? 200;
5037
+ const pass2 = res.status === expected;
5038
+ assertions.push({ pass: pass2, message: `HTTP ${res.status} ${pass2 ? "==" : "!="} ${expected}` });
5039
+ } else if (a.type === "json") {
5040
+ const val = resolveJsonPath(responseData, a.path ?? "$");
5041
+ if ("eq" in a) {
5042
+ const pass2 = JSON.stringify(val) === JSON.stringify(a.eq);
5043
+ assertions.push({ pass: pass2, message: `${a.path} ${pass2 ? "==" : "!="} ${JSON.stringify(a.eq)}` });
5044
+ } else if ("contains" in a && typeof val === "string") {
5045
+ const pass2 = val.includes(a.contains);
5046
+ assertions.push({ pass: pass2, message: `${a.path} ${pass2 ? "contains" : "doesn't contain"} "${a.contains}"` });
5047
+ }
5048
+ }
5049
+ }
5050
+ const pass = assertions.length === 0 ? res.ok : assertions.every((a) => a.pass);
5051
+ return {
5052
+ stepId: step.id,
5053
+ label: step.label,
5054
+ method: step.method,
5055
+ resolvedPath: urlPath,
5056
+ requestUrl: authedUrl,
5057
+ requestHeaders: authedHeaders,
5058
+ requestBody: bodyStr,
5059
+ status: res.status,
5060
+ statusText: res.statusText,
5061
+ responseHeaders,
5062
+ latency,
5063
+ extractedVars,
5064
+ assertions,
5065
+ pass,
5066
+ responseBody: responseText.slice(0, 1e4)
5067
+ };
5068
+ }
5069
+ async function runWorkflow(steps, emit, signal) {
5070
+ const ctx = {};
5071
+ let passed = 0;
5072
+ emit({ type: "run_start", totalSteps: steps.length });
5073
+ for (const step of steps) {
5074
+ if (signal?.aborted) {
5075
+ emit({ type: "run_aborted", message: "Run cancelled" });
5076
+ return;
5077
+ }
5078
+ emit({ type: "step_start", stepId: step.id, label: step.label, method: step.method, path: step.path });
5079
+ try {
5080
+ const result = await executeStep(step, ctx);
5081
+ for (const [k, v] of Object.entries(result.extractedVars))
5082
+ ctx[k] = v;
5083
+ ctx[`${step.id}_status`] = String(result.status ?? "");
5084
+ if (result.pass)
5085
+ passed++;
5086
+ emit({
5087
+ type: "step_done",
5088
+ stepId: result.stepId,
5089
+ label: result.label,
5090
+ method: result.method,
5091
+ resolvedPath: result.resolvedPath,
5092
+ requestUrl: result.requestUrl,
5093
+ requestHeaders: result.requestHeaders,
5094
+ requestBody: result.requestBody,
5095
+ status: result.status,
5096
+ statusText: result.statusText,
5097
+ responseHeaders: result.responseHeaders,
5098
+ latency: result.latency,
5099
+ extractedVars: result.extractedVars,
5100
+ assertions: result.assertions,
5101
+ pass: result.pass,
5102
+ responseBody: result.responseBody
5103
+ });
5104
+ } catch (e) {
5105
+ emit({
5106
+ type: "step_error",
5107
+ stepId: step.id,
5108
+ label: step.label,
5109
+ method: step.method,
5110
+ path: step.path,
5111
+ error: e instanceof Error ? e.message : String(e),
5112
+ pass: false
5113
+ });
5114
+ }
5115
+ }
5116
+ emit({ type: "run_done", totalSteps: steps.length, passedSteps: passed });
5117
+ }
5118
+ var init_engine2 = __esm(() => {
5119
+ init_engine();
5120
+ init_db();
5121
+ init_state();
5122
+ });
5123
+
5124
+ // src/agent/harness.ts
5125
+ function mergeSignals(a, b) {
5126
+ if (!a && !b)
5127
+ return new AbortController().signal;
5128
+ if (!a)
5129
+ return b;
5130
+ if (!b)
5131
+ return a;
5132
+ const ctrl = new AbortController;
5133
+ const abort = () => ctrl.abort();
5134
+ a.addEventListener("abort", abort, { once: true });
5135
+ b.addEventListener("abort", abort, { once: true });
5136
+ return ctrl.signal;
5137
+ }
5138
+ async function fetchWithRetry(url, opts, emit, signal, maxRetries = 4) {
5139
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
5140
+ const stepSignal = signal;
5141
+ let res;
5142
+ try {
5143
+ res = await fetch(url, { ...opts, signal: stepSignal });
5144
+ } catch (e) {
5145
+ const msg = e instanceof Error ? e.message : String(e);
5146
+ if (signal?.aborted)
5147
+ throw e;
5148
+ if (attempt === maxRetries)
5149
+ throw e;
5150
+ const isNetwork = msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("network") || msg.includes("fetch");
5151
+ if (!isNetwork)
5152
+ throw e;
5153
+ const delay2 = Math.min(1000 * Math.pow(2, attempt) + Math.random() * 300, 15000);
5154
+ emit({ type: "info", message: `Network error, retrying in ${Math.round(delay2 / 1000)}s\u2026` });
5155
+ await new Promise((r) => setTimeout(r, delay2));
5156
+ continue;
5157
+ }
5158
+ if (!RETRYABLE_STATUS.has(res.status) || attempt === maxRetries)
5159
+ return res;
5160
+ const retryAfter = parseInt(res.headers.get("retry-after") ?? "0", 10);
5161
+ const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * Math.pow(2, attempt) + Math.random() * 300, 30000);
5162
+ const label = res.status === 429 ? "Rate limited" : `Server error ${res.status}`;
5163
+ emit({ type: "info", message: `${label} \u2014 retrying in ${Math.round(delay / 1000)}s\u2026 (attempt ${attempt + 1}/${maxRetries})` });
5164
+ await new Promise((r) => setTimeout(r, delay));
5165
+ }
5166
+ return fetch(url, opts);
5167
+ }
5168
+ function trimContext(messages) {
5169
+ if (JSON.stringify(messages).length <= MAX_CONTEXT_CHARS)
5170
+ return { messages, trimmed: false };
5171
+ const result = [...messages];
5172
+ while (JSON.stringify(result).length > MAX_CONTEXT_CHARS && result.length > 2) {
5173
+ let removed = false;
5174
+ const toolIdx = result.findIndex((m) => m.role === "tool");
5175
+ if (toolIdx !== -1) {
5176
+ result.splice(toolIdx, 1);
5177
+ if (toolIdx > 0) {
5178
+ const prev = result[toolIdx - 1];
5179
+ if (prev?.role === "assistant") {
5180
+ const tc = prev.tool_calls;
5181
+ if (Array.isArray(tc) && tc.length)
5182
+ result.splice(toolIdx - 1, 1);
5183
+ }
5184
+ }
5185
+ removed = true;
5186
+ }
5187
+ if (!removed) {
5188
+ const anthropicIdx = result.findIndex((m) => {
5189
+ if (m.role !== "user")
5190
+ return false;
5191
+ const c = m.content;
5192
+ return Array.isArray(c) && c.some((b) => b.type === "tool_result");
5193
+ });
5194
+ if (anthropicIdx !== -1) {
5195
+ result.splice(anthropicIdx, 1);
5196
+ if (anthropicIdx > 0 && result[anthropicIdx - 1]?.role === "assistant") {
5197
+ result.splice(anthropicIdx - 1, 1);
5198
+ }
5199
+ removed = true;
5200
+ }
5201
+ }
5202
+ if (!removed)
5203
+ break;
5204
+ }
5205
+ return { messages: result, trimmed: true };
5206
+ }
5207
+ async function* readSSE(body) {
5208
+ const reader = body.getReader();
5209
+ const decoder = new TextDecoder;
5210
+ let buf = "";
5211
+ try {
5212
+ while (true) {
5213
+ const { done, value } = await reader.read();
5214
+ if (done)
5215
+ break;
5216
+ buf += decoder.decode(value, { stream: true });
5217
+ const parts = buf.split(`
5218
+
5219
+ `);
5220
+ buf = parts.pop() ?? "";
5221
+ for (const part of parts) {
5222
+ let data = "";
5223
+ for (const line of part.split(`
5224
+ `)) {
5225
+ if (line.startsWith("data: ")) {
5226
+ data = line.slice(6);
5227
+ break;
5228
+ }
5229
+ }
5230
+ if (!data || data === "[DONE]")
5231
+ continue;
5232
+ try {
5233
+ yield JSON.parse(data);
5234
+ } catch {}
5235
+ }
5236
+ }
5237
+ } finally {
5238
+ reader.releaseLock();
5239
+ }
5240
+ }
5241
+ function buildAnthropicTools(schemas) {
5242
+ return schemas.map((s) => ({
5243
+ name: s.name,
5244
+ description: s.description,
5245
+ input_schema: { type: "object", properties: s.params, required: s.required }
5246
+ }));
5247
+ }
5248
+ async function streamAnthropic(cfg, system, messages, tools, emit, signal) {
5249
+ const base = (cfg.baseUrl || "https://api.anthropic.com").replace(/\/$/, "");
5250
+ const systemContent = cfg.enablePromptCache ? [{ type: "text", text: system, cache_control: { type: "ephemeral" } }] : system;
5251
+ const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
5252
+ const res = await fetchWithRetry(`${base}/v1/messages`, {
5253
+ method: "POST",
5254
+ headers: {
5255
+ "Content-Type": "application/json",
5256
+ "x-api-key": cfg.apiKey,
5257
+ "anthropic-version": "2023-06-01",
5258
+ ...cfg.enablePromptCache ? { "anthropic-beta": "prompt-caching-1-0" } : {},
5259
+ ...cfg.extraHeaders
5260
+ },
5261
+ body: JSON.stringify({
5262
+ model: cfg.model,
5263
+ max_tokens: cfg.maxTokens,
5264
+ temperature: cfg.temperature,
5265
+ ...cfg.topK > 0 ? { top_k: cfg.topK } : {},
5266
+ system: systemContent,
5267
+ messages,
5268
+ tools,
5269
+ stream: true
5270
+ })
5271
+ }, emit, stepSignal);
5272
+ if (!res.ok) {
5273
+ const body = await res.text();
5274
+ const retryable = RETRYABLE_STATUS.has(res.status);
5275
+ throw Object.assign(new Error(`Anthropic ${res.status}: ${body}`), { retryable });
5276
+ }
5277
+ const result = { text: "", thinking: "", stopReason: "", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
5278
+ const blocks = [];
5279
+ const inputAccum = {};
5280
+ for await (const ev of readSSE(res.body)) {
5281
+ if (signal.aborted)
5282
+ break;
5283
+ const evType = ev.type;
5284
+ if (evType === "message_start") {
5285
+ const usage = ev.message?.usage;
5286
+ if (usage) {
5287
+ result.usage.input = usage.input_tokens ?? 0;
5288
+ result.usage.cacheRead = usage.cache_read_input_tokens ?? 0;
5289
+ result.usage.cacheWrite = usage.cache_creation_input_tokens ?? 0;
5290
+ }
5291
+ } else if (evType === "content_block_start") {
5292
+ const idx = ev.index;
5293
+ const cb = ev.content_block;
5294
+ blocks[idx] = { type: cb.type, id: cb.id, name: cb.name };
5295
+ if (cb.type === "tool_use")
5296
+ inputAccum[idx] = "";
5297
+ } else if (evType === "content_block_delta") {
5298
+ const idx = ev.index;
5299
+ const delta = ev.delta;
5300
+ if (delta.type === "text_delta" && delta.text) {
5301
+ result.text += delta.text;
5302
+ if (!blocks[idx])
5303
+ blocks[idx] = { type: "text" };
5304
+ blocks[idx].text = (blocks[idx].text ?? "") + delta.text;
5305
+ emit({ type: "text_delta", text: delta.text });
5306
+ } else if (delta.type === "thinking_delta" && delta.thinking) {
5307
+ result.thinking += delta.thinking;
5308
+ emit({ type: "thinking", text: delta.thinking });
5309
+ } else if (delta.type === "input_json_delta" && delta.partial_json) {
5310
+ inputAccum[idx] = (inputAccum[idx] ?? "") + delta.partial_json;
5311
+ }
5312
+ } else if (evType === "content_block_stop") {
5313
+ const idx = ev.index;
5314
+ if (blocks[idx]?.type === "tool_use") {
5315
+ try {
5316
+ blocks[idx].input = JSON.parse(inputAccum[idx] ?? "{}");
5317
+ } catch {
5318
+ blocks[idx].input = {};
5319
+ }
5320
+ }
5321
+ } else if (evType === "message_delta") {
5322
+ const delta = ev.delta;
5323
+ const usage = ev.usage;
5324
+ if (delta.stop_reason)
5325
+ result.stopReason = delta.stop_reason;
5326
+ if (usage?.output_tokens)
5327
+ result.usage.output = usage.output_tokens;
5328
+ }
5329
+ }
5330
+ for (const b of blocks) {
5331
+ if (b.type === "tool_use" && b.id && b.name) {
5332
+ result.toolUses.push({ id: b.id, name: b.name, input: b.input ?? {} });
5333
+ }
5334
+ }
5335
+ result._anthropicBlocks = blocks;
5336
+ return result;
5337
+ }
5338
+ function buildOpenAITools(schemas) {
5339
+ return schemas.map((s) => ({
5340
+ type: "function",
5341
+ function: { name: s.name, description: s.description, parameters: { type: "object", properties: s.params, required: s.required } }
5342
+ }));
5343
+ }
5344
+ async function streamOpenAI(cfg, system, messages, tools, emit, signal) {
5345
+ const providerBases = {
5346
+ openai: "https://api.openai.com",
5347
+ mistral: "https://api.mistral.ai",
5348
+ groq: "https://api.groq.com/openai",
5349
+ "github-copilot": "https://api.githubcopilot.com"
5350
+ };
5351
+ const base = (cfg.baseUrl || providerBases[cfg.provider] || "https://api.openai.com").replace(/\/$/, "");
5352
+ const providerHeaders = cfg.provider === "github-copilot" ? { "Copilot-Integration-Id": "vscode-chat", "Editor-Version": "vscode/1.85.0" } : {};
5353
+ const authHeaders = cfg.apiKey ? { Authorization: `Bearer ${cfg.apiKey}` } : {};
5354
+ const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
5355
+ const res = await fetchWithRetry(`${base}/v1/chat/completions`, {
5356
+ method: "POST",
5357
+ headers: { "Content-Type": "application/json", ...authHeaders, ...providerHeaders, ...cfg.extraHeaders },
5358
+ body: JSON.stringify({
5359
+ model: cfg.model,
5360
+ max_tokens: cfg.maxTokens,
5361
+ temperature: cfg.temperature,
5362
+ messages: [{ role: "system", content: system }, ...messages],
5363
+ tools,
5364
+ tool_choice: "auto",
5365
+ stream: true,
5366
+ stream_options: { include_usage: true }
5367
+ })
5368
+ }, emit, stepSignal);
5369
+ if (!res.ok) {
5370
+ const body = await res.text();
5371
+ const retryable = RETRYABLE_STATUS.has(res.status);
5372
+ throw Object.assign(new Error(body), { retryable });
5373
+ }
5374
+ const result = { text: "", thinking: "", stopReason: "", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
5375
+ const tcAccum = {};
5376
+ for await (const ev of readSSE(res.body)) {
5377
+ if (signal.aborted)
5378
+ break;
5379
+ if (ev.object === "error") {
5380
+ const retryable = ev.code === "1300" || ev.raw_status_code === 429 || ev.raw_status_code === 503;
5381
+ throw Object.assign(new Error(JSON.stringify(ev)), { retryable });
5382
+ }
5383
+ if (ev.usage) {
5384
+ const u = ev.usage;
5385
+ result.usage.input = u.prompt_tokens ?? 0;
5386
+ result.usage.output = u.completion_tokens ?? 0;
5387
+ }
5388
+ const choices = ev.choices;
5389
+ const choice = choices?.[0];
5390
+ if (!choice)
5391
+ continue;
5392
+ const fr = choice.finish_reason;
5393
+ if (fr)
5394
+ result.stopReason = fr;
5395
+ const delta = choice.delta;
5396
+ if (!delta)
5397
+ continue;
5398
+ if (typeof delta.content === "string" && delta.content) {
5399
+ result.text += delta.content;
5400
+ emit({ type: "text_delta", text: delta.content });
5401
+ }
5402
+ const tcDeltas = delta.tool_calls;
5403
+ if (tcDeltas) {
5404
+ for (const tc of tcDeltas) {
5405
+ if (!tcAccum[tc.index])
5406
+ tcAccum[tc.index] = { id: "", name: "", args: "" };
5407
+ const e = tcAccum[tc.index];
5408
+ if (tc.id)
5409
+ e.id += tc.id;
5410
+ if (tc.function?.name)
5411
+ e.name += tc.function.name;
5412
+ if (tc.function?.arguments)
5413
+ e.args += tc.function.arguments;
5414
+ }
5415
+ }
5416
+ }
5417
+ for (const tc of Object.values(tcAccum)) {
5418
+ let input = {};
5419
+ try {
5420
+ input = JSON.parse(tc.args);
5421
+ } catch {}
5422
+ result.toolUses.push({ id: tc.id, name: tc.name, input });
5423
+ }
5424
+ result._openaiTcAccum = tcAccum;
5425
+ return result;
5426
+ }
5427
+ async function callOllama(cfg, system, messages, emit, signal) {
5428
+ const base = (cfg.baseUrl || "http://localhost:11434").replace(/\/$/, "");
5429
+ const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
5430
+ const res = await fetchWithRetry(`${base}/api/chat`, {
5431
+ method: "POST",
5432
+ headers: { "Content-Type": "application/json" },
5433
+ body: JSON.stringify({ model: cfg.model, messages: [{ role: "system", content: system }, ...messages], stream: false })
5434
+ }, emit, stepSignal);
5435
+ if (!res.ok)
5436
+ throw new Error(`Ollama ${res.status}: ${await res.text()}`);
5437
+ const d = await res.json();
5438
+ const text = d.message?.content ?? "";
5439
+ emit({ type: "text_delta", text });
5440
+ return { text, thinking: "", stopReason: "end_turn", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
5441
+ }
5442
+ async function callGemini(cfg, system, messages, emit, signal) {
5443
+ const base = (cfg.baseUrl || "https://generativelanguage.googleapis.com").replace(/\/$/, "");
5444
+ const stepSignal = mergeSignals(signal, AbortSignal.timeout(cfg.stepTimeoutMs));
5445
+ const res = await fetchWithRetry(`${base}/v1beta/models/${cfg.model}:generateContent?key=${cfg.apiKey}`, {
5446
+ method: "POST",
5447
+ headers: { "Content-Type": "application/json" },
5448
+ body: JSON.stringify({
5449
+ systemInstruction: { parts: [{ text: system }] },
5450
+ contents: messages.map((m) => ({
5451
+ role: m.role === "assistant" ? "model" : "user",
5452
+ parts: [{ text: m.content }]
5453
+ })),
5454
+ generationConfig: { maxOutputTokens: cfg.maxTokens }
5455
+ })
5456
+ }, emit, stepSignal);
5457
+ if (!res.ok)
5458
+ throw new Error(`Gemini ${res.status}: ${await res.text()}`);
5459
+ const d = await res.json();
5460
+ const text = d.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
5461
+ emit({ type: "text_delta", text });
5462
+ return { text, thinking: "", stopReason: "end_turn", toolUses: [], usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
5463
+ }
5464
+ async function runAgentLoop(config2, system, initialMessages, toolSchemas, executeTool, emit, signal = new AbortController().signal, toolCache = new Map) {
5465
+ const cfg = { ...DEFAULTS, ...config2 };
5466
+ const isAnthropic = cfg.provider === "anthropic";
5467
+ const isOllama = cfg.provider === "ollama";
5468
+ const isGemini = cfg.provider === "gemini";
5469
+ const anthropicTools = buildAnthropicTools(toolSchemas);
5470
+ const openaiTools = buildOpenAITools(toolSchemas);
5471
+ const messages = [...initialMessages];
5472
+ const allToolCalls = [];
5473
+ const totalTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
5474
+ let totalToolsUsed = 0;
5475
+ let consecutiveErrors = 0;
5476
+ const endpointErrors = {};
5477
+ for (let iter = 0;iter < cfg.maxIterations; iter++) {
5478
+ if (signal.aborted) {
5479
+ return { content: "", toolCalls: allToolCalls, stopReason: "cancelled", tokens: totalTokens };
5480
+ }
5481
+ const { messages: trimmed, trimmed: didTrim } = trimContext(messages);
5482
+ if (didTrim) {
5483
+ emit({ type: "info", message: "Context trimmed to fit within limits." });
5484
+ messages.splice(0, messages.length, ...trimmed);
5485
+ }
5486
+ let turn;
5487
+ try {
5488
+ if (isAnthropic) {
5489
+ turn = await streamAnthropic(cfg, system, messages, anthropicTools, emit, signal);
5490
+ } else if (isOllama) {
5491
+ turn = await callOllama(cfg, system, messages, emit, signal);
5492
+ } else if (isGemini) {
5493
+ turn = await callGemini(cfg, system, messages, emit, signal);
5494
+ } else {
5495
+ turn = await streamOpenAI(cfg, system, messages, openaiTools, emit, signal);
5496
+ }
5497
+ } catch (e) {
5498
+ if (signal.aborted) {
5499
+ return { content: "", toolCalls: allToolCalls, stopReason: "cancelled", tokens: totalTokens };
5500
+ }
5501
+ const msg = e instanceof Error ? e.message : String(e);
5502
+ const retryable = e.retryable ?? false;
5503
+ emit({ type: "error", message: msg, retryable });
5504
+ throw e;
5505
+ }
5506
+ totalTokens.input += turn.usage.input;
5507
+ totalTokens.output += turn.usage.output;
5508
+ totalTokens.cacheRead += turn.usage.cacheRead;
5509
+ totalTokens.cacheWrite += turn.usage.cacheWrite;
5510
+ if (turn.usage.input || turn.usage.output) {
5511
+ emit({ type: "token_usage", ...turn.usage });
5512
+ }
5513
+ const wantsTools = isAnthropic ? turn.stopReason === "tool_use" : turn.stopReason === "tool_calls";
5514
+ if (!wantsTools || turn.toolUses.length === 0) {
5515
+ return { content: turn.text, toolCalls: allToolCalls, stopReason: "end_turn", tokens: totalTokens };
5516
+ }
5517
+ if (totalToolsUsed >= cfg.maxTotalTools) {
5518
+ return {
5519
+ content: `Agent stopped: reached ${cfg.maxTotalTools} tool calls. Break your request into smaller steps.`,
5520
+ toolCalls: allToolCalls,
5521
+ stopReason: "max_tools",
5522
+ tokens: totalTokens
5523
+ };
5524
+ }
5525
+ const dedupeKey = (name, input) => `${name}:${JSON.stringify(input)}`;
5526
+ const turnResults = [];
5527
+ const executeOne = async (use) => {
5528
+ totalToolsUsed++;
5529
+ const key = dedupeKey(use.name, use.input);
5530
+ const cachedResult = toolCache.get(key);
5531
+ const isCached = !!cachedResult;
5532
+ emit({ type: "tool_start", id: use.id, tool: use.name, input: use.input, cached: isCached });
5533
+ let result;
5534
+ let ms = 0;
5535
+ if (isCached) {
5536
+ result = cachedResult;
5537
+ } else {
5538
+ const t0 = Date.now();
5539
+ result = await executeTool(use.name, use.input);
5540
+ ms = Date.now() - t0;
5541
+ if (!result.isError && use.name !== "execute_api_request" && use.name !== "fetch_url") {
5542
+ toolCache.set(key, result);
5543
+ }
5544
+ }
5545
+ emit({ type: "tool_done", id: use.id, tool: use.name, output: result.text, isError: result.isError, ms, cached: isCached });
5546
+ return { id: use.id, name: use.name, text: result.text, isError: result.isError, ms, cached: isCached };
5547
+ };
5548
+ const toolUses = turn.toolUses;
5549
+ if (cfg.parallelTools && toolUses.length > 1) {
5550
+ const pure = toolUses.filter((u) => u.name !== "execute_api_request" && u.name !== "fetch_url");
5551
+ const sideEffect = toolUses.filter((u) => u.name === "execute_api_request" || u.name === "fetch_url");
5552
+ const pureResults = await Promise.all(pure.map((u) => executeOne(u)));
5553
+ const sideEffectResults = [];
5554
+ for (const u of sideEffect)
5555
+ sideEffectResults.push(await executeOne(u));
5556
+ const resultMap = new Map([...pureResults, ...sideEffectResults].map((r) => [r.id, r]));
5557
+ for (const u of toolUses) {
5558
+ const r = resultMap.get(u.id);
5559
+ if (r)
5560
+ turnResults.push(r);
5561
+ }
5562
+ } else {
5563
+ for (const u of toolUses)
5564
+ turnResults.push(await executeOne(u));
5565
+ }
5566
+ for (const r of turnResults) {
5567
+ allToolCalls.push({ id: r.id, tool: r.name, input: turn.toolUses.find((u) => u.id === r.id)?.input ?? {}, output: r.text, isError: r.isError, ms: r.ms, cached: r.cached });
5568
+ if (r.isError) {
5569
+ consecutiveErrors++;
5570
+ if (r.name === "execute_api_request") {
5571
+ const eid = String(turn.toolUses.find((u) => u.id === r.id)?.input?.operationId ?? r.id);
5572
+ endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
5573
+ if (endpointErrors[eid] >= cfg.maxEndpointErrors) {
5574
+ return {
5575
+ content: `Endpoint "${eid}" failed ${cfg.maxEndpointErrors} times. Last error: ${r.text}`,
5576
+ toolCalls: allToolCalls,
5577
+ stopReason: "max_endpoint_errors",
5578
+ tokens: totalTokens
5579
+ };
5580
+ }
5581
+ }
5582
+ if (consecutiveErrors >= cfg.maxConsecutiveErrors) {
5583
+ return {
5584
+ content: `Stopped after ${cfg.maxConsecutiveErrors} consecutive errors. Last: ${r.text}`,
5585
+ toolCalls: allToolCalls,
5586
+ stopReason: "max_errors",
5587
+ tokens: totalTokens
5588
+ };
5589
+ }
5590
+ } else {
5591
+ consecutiveErrors = 0;
5592
+ }
5593
+ }
5594
+ if (isAnthropic) {
5595
+ const blocks = turn._anthropicBlocks ?? [];
5596
+ messages.push({ role: "assistant", content: blocks });
5597
+ messages.push({
5598
+ role: "user",
5599
+ content: turnResults.map((r) => ({ type: "tool_result", tool_use_id: r.id, content: r.text }))
5600
+ });
5601
+ } else {
5602
+ const tc = turn._openaiTcAccum ?? {};
5603
+ messages.push({
5604
+ role: "assistant",
5605
+ content: turn.text || null,
5606
+ tool_calls: Object.values(tc).map((t) => ({ id: t.id, type: "function", function: { name: t.name, arguments: t.args } }))
5607
+ });
5608
+ for (const r of turnResults) {
5609
+ messages.push({ role: "tool", tool_call_id: r.id, content: r.text });
5610
+ }
5611
+ }
5612
+ }
5613
+ return { content: "(max iterations reached)", toolCalls: allToolCalls, stopReason: "max_iterations", tokens: totalTokens };
5614
+ }
5615
+ var RETRYABLE_STATUS, MAX_CONTEXT_CHARS = 300000, DEFAULTS;
5616
+ var init_harness = __esm(() => {
5617
+ RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
5618
+ DEFAULTS = {
5619
+ apiKey: "",
5620
+ baseUrl: "",
5621
+ extraHeaders: {},
5622
+ maxTokens: 4096,
5623
+ temperature: 1,
5624
+ topK: 0,
5625
+ maxIterations: 40,
5626
+ maxTotalTools: 40,
5627
+ maxConsecutiveErrors: 5,
5628
+ maxEndpointErrors: 3,
5629
+ stepTimeoutMs: 60000,
5630
+ parallelTools: true,
5631
+ enablePromptCache: true
4625
5632
  };
4626
5633
  });
4627
5634
 
@@ -4630,7 +5637,7 @@ import dns from "dns/promises";
4630
5637
  function json(data, status = 200) {
4631
5638
  return new Response(JSON.stringify(data), {
4632
5639
  status,
4633
- headers: { "Content-Type": "application/json", ...CORS3 }
5640
+ headers: { "Content-Type": "application/json", ...CORS4 }
4634
5641
  });
4635
5642
  }
4636
5643
  function notFound(msg = "Not found") {
@@ -4641,7 +5648,7 @@ function badRequest(msg) {
4641
5648
  }
4642
5649
  async function apiRouter(req) {
4643
5650
  if (req.method === "OPTIONS")
4644
- return new Response(null, { status: 204, headers: CORS3 });
5651
+ return new Response(null, { status: 204, headers: CORS4 });
4645
5652
  const { pathname: path, searchParams } = new URL(req.url);
4646
5653
  const method = req.method;
4647
5654
  if (path === "/api/status" && method === "GET")
@@ -4688,6 +5695,12 @@ async function apiRouter(req) {
4688
5695
  return handleDeleteRule(path);
4689
5696
  if (path === "/api/ai/chat" && method === "POST")
4690
5697
  return handleAiChat(req);
5698
+ if (path === "/api/ai/memory" && method === "GET")
5699
+ return json({ memory: dbQueries.getMemory(40) });
5700
+ if (path === "/api/ai/memory" && method === "DELETE") {
5701
+ dbQueries.clearMemory();
5702
+ return json({ success: true });
5703
+ }
4691
5704
  if (path === "/api/debug/dns" && method === "GET")
4692
5705
  return handleDnsQuery(searchParams);
4693
5706
  if (path === "/api/debug/ping" && method === "GET")
@@ -4708,6 +5721,24 @@ async function apiRouter(req) {
4708
5721
  return handleUpdateSaved(req, path);
4709
5722
  if (path.startsWith("/api/saved/") && method === "DELETE")
4710
5723
  return handleDeleteSaved(path);
5724
+ if (path === "/api/workflows" && method === "GET")
5725
+ return handleGetWorkflows();
5726
+ if (path === "/api/workflows" && method === "POST")
5727
+ return handleCreateWorkflow(req);
5728
+ if (path === "/api/workflows/generate" && method === "POST")
5729
+ return handleGenerateWorkflow(req);
5730
+ if (path.startsWith("/api/workflows/") && path.endsWith("/run") && method === "POST")
5731
+ return handleRunWorkflow(path);
5732
+ if (path.startsWith("/api/workflows/") && method === "PUT")
5733
+ return handleUpdateWorkflow(req, path);
5734
+ if (path.startsWith("/api/workflows/") && method === "DELETE")
5735
+ return handleDeleteWorkflow(path);
5736
+ if (path === "/api/capture/bins" && method === "GET")
5737
+ return handleGetCaptureBins();
5738
+ if (path === "/api/capture/bins" && method === "POST")
5739
+ return handleCreateCaptureBin(req);
5740
+ if (path.startsWith("/api/capture/bins/") && method === "DELETE")
5741
+ return handleDeleteCaptureBin(path);
4711
5742
  return notFound("API route not found");
4712
5743
  }
4713
5744
  function handleStatus() {
@@ -4749,8 +5780,14 @@ async function handleSetFeatures(req) {
4749
5780
  patch[key] = body[key];
4750
5781
  }
4751
5782
  setFeatures(patch);
5783
+ persistAndBroadcastFeatures();
4752
5784
  return json(getFeatures());
4753
5785
  }
5786
+ function persistAndBroadcastFeatures() {
5787
+ const f = getFeatures();
5788
+ dbQueries.setSetting("features", JSON.stringify({ mcp: f.mcp, proxy: f.proxy, ai: f.ai }));
5789
+ logBus.broadcastServerEvent({ kind: "features", data: f });
5790
+ }
4754
5791
  function handleServerInfo() {
4755
5792
  const state = hasState() ? getState() : null;
4756
5793
  return json({
@@ -4800,10 +5837,12 @@ async function handleSpecUpload(req) {
4800
5837
  return badRequest("Empty spec content");
4801
5838
  try {
4802
5839
  const state = loadSpecFromText(content, filename);
5840
+ const suggestedVars = extractSuggestedVars(content, state.spec.baseUrl);
4803
5841
  return json({
4804
5842
  ok: true,
4805
5843
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
4806
- endpointCount: state.operations.length
5844
+ endpointCount: state.operations.length,
5845
+ suggestedVars
4807
5846
  });
4808
5847
  } catch (e) {
4809
5848
  return json({ error: e instanceof Error ? e.message : String(e) }, 400);
@@ -4820,10 +5859,12 @@ async function handleSpecReloadUrl(req) {
4820
5859
  return badRequest("Missing url field");
4821
5860
  try {
4822
5861
  const state = await loadSpec(body.url);
5862
+ const suggestedVars = extractSuggestedVars(state.spec.raw, state.spec.baseUrl);
4823
5863
  return json({
4824
5864
  ok: true,
4825
5865
  spec: { title: state.spec.title, version: state.spec.version, baseUrl: state.spec.baseUrl },
4826
- endpointCount: state.operations.length
5866
+ endpointCount: state.operations.length,
5867
+ suggestedVars
4827
5868
  });
4828
5869
  } catch (e) {
4829
5870
  return json({ error: e instanceof Error ? e.message : String(e) }, 400);
@@ -4885,39 +5926,54 @@ async function handleSetSettings(req) {
4885
5926
  dbQueries.setSettings(body);
4886
5927
  return json(body);
4887
5928
  }
4888
- async function executeTool(name, args) {
5929
+ async function executeTool(name, args, cache = new Map) {
4889
5930
  const { operations, spec } = getState();
4890
5931
  if (name === "search_endpoints") {
5932
+ const cacheKey2 = `search:${String(args.query ?? "").toLowerCase()}`;
5933
+ const hit = cache.get(cacheKey2);
5934
+ if (hit)
5935
+ return hit;
4891
5936
  const q = String(args.query ?? "").toLowerCase();
4892
5937
  const terms = q.split(/\s+/).filter(Boolean);
4893
5938
  const matches = operations.filter((op) => {
4894
5939
  const hay = [op.operationId, op.path, op.method, ...op.tags ?? [], op.summary ?? "", op.description ?? ""].join(" ").toLowerCase();
4895
5940
  return terms.every((t) => hay.includes(t));
4896
5941
  }).slice(0, 30).map((op) => ({ operationId: op.operationId, method: op.method.toUpperCase(), path: op.path, summary: op.summary ?? null, tags: op.tags }));
4897
- if (!matches.length)
4898
- return { text: `No endpoints found matching "${args.query}". Total: ${operations.length}.`, isError: false };
4899
- return { text: JSON.stringify({ count: matches.length, total: operations.length, endpoints: matches }, null, 2), isError: false };
5942
+ const text = !matches.length ? `No endpoints found matching "${args.query}". Total: ${operations.length}.` : JSON.stringify({ count: matches.length, total: operations.length, endpoints: matches }, null, 2);
5943
+ const result = { text, isError: false };
5944
+ cache.set(cacheKey2, result);
5945
+ return result;
4900
5946
  }
4901
5947
  if (name === "get_endpoint_schema") {
5948
+ const cacheKey2 = `schema:${String(args.operationId ?? "")}`;
5949
+ const hit = cache.get(cacheKey2);
5950
+ if (hit)
5951
+ return hit;
4902
5952
  const op = operations.find((o) => o.operationId === args.operationId);
4903
5953
  if (!op)
4904
5954
  return { text: `Endpoint not found: "${args.operationId}"`, isError: true };
4905
- return {
4906
- text: JSON.stringify({
4907
- operationId: op.operationId,
4908
- method: op.method.toUpperCase(),
4909
- path: op.path,
4910
- summary: op.summary ?? null,
4911
- description: op.description ?? null,
4912
- tags: op.tags,
4913
- parameters: op.parameters,
4914
- requestBody: op.requestBody ?? null,
4915
- responses: op.responses
4916
- }, null, 2),
4917
- isError: false
4918
- };
5955
+ const text = JSON.stringify({
5956
+ operationId: op.operationId,
5957
+ method: op.method.toUpperCase(),
5958
+ path: op.path,
5959
+ summary: op.summary ?? null,
5960
+ description: op.description ?? null,
5961
+ tags: op.tags,
5962
+ parameters: op.parameters,
5963
+ requestBody: op.requestBody ?? null,
5964
+ responses: op.responses
5965
+ }, null, 2);
5966
+ const result = { text, isError: false };
5967
+ cache.set(cacheKey2, result);
5968
+ return result;
4919
5969
  }
4920
5970
  if (name === "execute_api_request") {
5971
+ const now = Date.now();
5972
+ const gap = now - _lastApiCallMs;
5973
+ if (gap < MIN_API_CALL_INTERVAL_MS) {
5974
+ await new Promise((r) => setTimeout(r, MIN_API_CALL_INTERVAL_MS - gap));
5975
+ }
5976
+ _lastApiCallMs = Date.now();
4921
5977
  const op = operations.find((o) => o.operationId === args.operationId);
4922
5978
  if (!op)
4923
5979
  return { text: `Endpoint not found: "${args.operationId}"`, isError: true };
@@ -4948,20 +6004,81 @@ async function executeTool(name, args) {
4948
6004
  const bodyStr = reqBody !== undefined ? typeof reqBody === "string" ? reqBody : JSON.stringify(reqBody) : null;
4949
6005
  if (bodyStr !== null && op.requestBody?.contentType)
4950
6006
  authedHeaders["Content-Type"] = op.requestBody.contentType;
6007
+ const logId = randomUUID();
4951
6008
  try {
4952
6009
  const start = Date.now();
4953
6010
  const res = await fetch(authedUrl, { method: op.method.toUpperCase(), headers: authedHeaders, body: bodyStr ?? undefined });
4954
- const text = await res.text();
6011
+ const responseText = await res.text();
4955
6012
  const latency = Date.now() - start;
4956
- let pretty = text;
6013
+ const resHeaders = Object.fromEntries(res.headers.entries());
6014
+ dbQueries.insertLog({
6015
+ id: logId,
6016
+ source: "ai",
6017
+ tool_name: String(args.operationId ?? op.operationId),
6018
+ method: op.method.toUpperCase(),
6019
+ url: authedUrl,
6020
+ request_headers: JSON.stringify(authedHeaders),
6021
+ request_body: bodyStr,
6022
+ status_code: res.status,
6023
+ response_headers: JSON.stringify(resHeaders),
6024
+ response_body: responseText.slice(0, 8192),
6025
+ latency_ms: latency,
6026
+ error: null
6027
+ });
6028
+ logBus.emit({
6029
+ id: logId,
6030
+ source: "ai",
6031
+ tool_name: String(args.operationId ?? op.operationId),
6032
+ method: op.method.toUpperCase(),
6033
+ url: authedUrl,
6034
+ request_headers: JSON.stringify(authedHeaders),
6035
+ request_body: bodyStr,
6036
+ status_code: res.status,
6037
+ response_headers: JSON.stringify(resHeaders),
6038
+ response_body: responseText.slice(0, 2048),
6039
+ latency_ms: latency,
6040
+ error: null,
6041
+ created_at: Date.now()
6042
+ });
6043
+ let pretty = responseText;
4957
6044
  try {
4958
- pretty = JSON.stringify(JSON.parse(text), null, 2);
6045
+ pretty = JSON.stringify(JSON.parse(responseText), null, 2);
4959
6046
  } catch {}
4960
6047
  return { text: `HTTP ${res.status} (${latency}ms)
4961
6048
 
4962
6049
  ${pretty}`, isError: !res.ok };
4963
6050
  } catch (e) {
4964
- return { text: `Network error: ${e instanceof Error ? e.message : String(e)}`, isError: true };
6051
+ const errMsg = e instanceof Error ? e.message : String(e);
6052
+ dbQueries.insertLog({
6053
+ id: logId,
6054
+ source: "ai",
6055
+ tool_name: String(args.operationId ?? op.operationId),
6056
+ method: op.method.toUpperCase(),
6057
+ url: authedUrl,
6058
+ request_headers: JSON.stringify(authedHeaders),
6059
+ request_body: bodyStr,
6060
+ status_code: null,
6061
+ response_headers: null,
6062
+ response_body: null,
6063
+ latency_ms: null,
6064
+ error: errMsg
6065
+ });
6066
+ logBus.emit({
6067
+ id: logId,
6068
+ source: "ai",
6069
+ tool_name: String(args.operationId ?? op.operationId),
6070
+ method: op.method.toUpperCase(),
6071
+ url: authedUrl,
6072
+ request_headers: null,
6073
+ request_body: bodyStr,
6074
+ status_code: null,
6075
+ response_headers: null,
6076
+ response_body: null,
6077
+ latency_ms: null,
6078
+ error: errMsg,
6079
+ created_at: Date.now()
6080
+ });
6081
+ return { text: `Network error: ${errMsg}`, isError: true };
4965
6082
  }
4966
6083
  }
4967
6084
  if (name === "fetch_url") {
@@ -5111,10 +6228,25 @@ ${stripped}`, isError: !res.ok };
5111
6228
  }
5112
6229
  if (name === "save_auth_token") {
5113
6230
  const profileName = String(args.name ?? "AI Login").trim();
6231
+ const tokenType = String(args.token_type ?? "bearer");
6232
+ if (tokenType === "basic" || args.username && args.password) {
6233
+ const username = String(args.username ?? "").trim();
6234
+ const password = String(args.password ?? "").trim();
6235
+ if (!username || !password)
6236
+ return { text: "Error: username and password are required for basic auth", isError: true };
6237
+ const authConfig2 = { type: "basic", username, password };
6238
+ const profileId2 = randomUUID();
6239
+ try {
6240
+ dbQueries.insertProfile({ id: profileId2, name: profileName, description: "Saved by AI", type: "basic", config: JSON.stringify(authConfig2), token_cache: null, is_active: 0 });
6241
+ dbQueries.activateProfile(profileId2);
6242
+ return { text: JSON.stringify({ success: true, message: `Saved and activated basic auth profile "${profileName}"`, id: profileId2 }), isError: false };
6243
+ } catch (e) {
6244
+ return { text: `Error saving profile: ${e instanceof Error ? e.message : String(e)}`, isError: true };
6245
+ }
6246
+ }
5114
6247
  const token = String(args.token ?? "").trim();
5115
6248
  if (!token)
5116
- return { text: "Error: token is required", isError: true };
5117
- const tokenType = String(args.token_type ?? "bearer");
6249
+ return { text: "Error: token is required for bearer/apikey auth", isError: true };
5118
6250
  const headerName = String(args.header_name ?? "X-Api-Key");
5119
6251
  let authConfig;
5120
6252
  let type;
@@ -5128,7 +6260,7 @@ ${stripped}`, isError: !res.ok };
5128
6260
  authConfig = { type: "bearer", token };
5129
6261
  type = "bearer";
5130
6262
  }
5131
- const profileId = randomUUID2();
6263
+ const profileId = randomUUID();
5132
6264
  try {
5133
6265
  dbQueries.insertProfile({ id: profileId, name: profileName, description: "Saved by AI", type, config: JSON.stringify(authConfig), token_cache: null, is_active: 0 });
5134
6266
  dbQueries.activateProfile(profileId);
@@ -5139,274 +6271,6 @@ ${stripped}`, isError: !res.ok };
5139
6271
  }
5140
6272
  return { text: `Unknown tool: ${name}`, isError: true };
5141
6273
  }
5142
- async function fetchWithRetry(url, opts, emit, maxRetries = 3) {
5143
- for (let attempt = 0;attempt <= maxRetries; attempt++) {
5144
- const res = await fetch(url, opts);
5145
- if (res.status !== 429 || attempt === maxRetries)
5146
- return res;
5147
- const retryAfter = parseInt(res.headers.get("retry-after") ?? "0", 10);
5148
- const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * Math.pow(2, attempt) + Math.random() * 500, 30000);
5149
- emit({ type: "info", message: `Rate limited \u2014 retrying in ${Math.round(delay / 1000)}s\u2026 (attempt ${attempt + 1}/${maxRetries})` });
5150
- await new Promise((r) => setTimeout(r, delay));
5151
- }
5152
- return fetch(url, opts);
5153
- }
5154
- async function anthropicAgentLoop(apiKey, model, system, initialMessages, emit) {
5155
- const msgs = [...initialMessages];
5156
- const toolCalls = [];
5157
- let totalTools = 0;
5158
- let consecutiveErrors = 0;
5159
- const endpointErrors = {};
5160
- for (let iter = 0;iter < 40; iter++) {
5161
- const res = await fetchWithRetry("https://api.anthropic.com/v1/messages", {
5162
- method: "POST",
5163
- headers: { "Content-Type": "application/json", "x-api-key": apiKey, "anthropic-version": "2023-06-01" },
5164
- body: JSON.stringify({ model, max_tokens: 4096, system, messages: msgs, tools: ANTHROPIC_TOOLS, stream: true })
5165
- }, emit);
5166
- if (!res.ok)
5167
- throw new Error(`Anthropic error: ${await res.text()}`);
5168
- let fullText = "";
5169
- let stopReason = "";
5170
- const contentBlocks = [];
5171
- const inputAccum = {};
5172
- const reader = res.body.getReader();
5173
- const decoder = new TextDecoder;
5174
- let buf = "";
5175
- while (true) {
5176
- const { done, value } = await reader.read();
5177
- if (done)
5178
- break;
5179
- buf += decoder.decode(value, { stream: true });
5180
- const parts = buf.split(`
5181
-
5182
- `);
5183
- buf = parts.pop() ?? "";
5184
- for (const part of parts) {
5185
- let dataLine = "";
5186
- for (const line of part.split(`
5187
- `)) {
5188
- if (line.startsWith("data: ")) {
5189
- dataLine = line.slice(6);
5190
- break;
5191
- }
5192
- }
5193
- if (!dataLine || dataLine === "[DONE]")
5194
- continue;
5195
- let ev;
5196
- try {
5197
- ev = JSON.parse(dataLine);
5198
- } catch {
5199
- continue;
5200
- }
5201
- const type = ev.type;
5202
- if (type === "content_block_start") {
5203
- const idx = ev.index;
5204
- const cb = ev.content_block;
5205
- contentBlocks[idx] = { type: cb.type, id: cb.id, name: cb.name };
5206
- if (cb.type === "tool_use")
5207
- inputAccum[idx] = "";
5208
- } else if (type === "content_block_delta") {
5209
- const idx = ev.index;
5210
- const delta = ev.delta;
5211
- if (delta.type === "text_delta" && delta.text) {
5212
- fullText += delta.text;
5213
- if (!contentBlocks[idx])
5214
- contentBlocks[idx] = { type: "text", text: "" };
5215
- contentBlocks[idx].text = (contentBlocks[idx].text ?? "") + delta.text;
5216
- emit({ type: "text_delta", text: delta.text });
5217
- } else if (delta.type === "input_json_delta" && delta.partial_json) {
5218
- inputAccum[idx] = (inputAccum[idx] ?? "") + delta.partial_json;
5219
- }
5220
- } else if (type === "content_block_stop") {
5221
- const idx = ev.index;
5222
- if (contentBlocks[idx]?.type === "tool_use") {
5223
- try {
5224
- contentBlocks[idx].input = JSON.parse(inputAccum[idx] ?? "{}");
5225
- } catch {
5226
- contentBlocks[idx].input = {};
5227
- }
5228
- }
5229
- } else if (type === "message_delta") {
5230
- const delta = ev.delta;
5231
- if (delta.stop_reason)
5232
- stopReason = delta.stop_reason;
5233
- }
5234
- }
5235
- }
5236
- if (stopReason !== "tool_use")
5237
- return { content: fullText, toolCalls };
5238
- if (totalTools >= MAX_TOTAL_TOOLS) {
5239
- return { content: `Agent stopped: reached ${MAX_TOTAL_TOOLS} tool calls. Please break your request into smaller steps.`, toolCalls };
5240
- }
5241
- msgs.push({ role: "assistant", content: contentBlocks });
5242
- const toolResults = [];
5243
- for (const block of contentBlocks) {
5244
- if (block.type !== "tool_use" || !block.id || !block.name)
5245
- continue;
5246
- totalTools++;
5247
- emit({ type: "tool_start", tool: block.name, input: block.input ?? {} });
5248
- const result = await executeTool(block.name, block.input ?? {});
5249
- emit({ type: "tool_done", tool: block.name, input: block.input ?? {}, output: result.text, isError: result.isError });
5250
- toolCalls.push({ tool: block.name, input: block.input ?? {}, output: result.text, isError: result.isError });
5251
- if (result.isError) {
5252
- consecutiveErrors++;
5253
- if (block.name === "execute_api_request" && block.input?.operationId) {
5254
- const eid = String(block.input.operationId);
5255
- endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
5256
- if (endpointErrors[eid] >= MAX_SAME_ENDPOINT_ERRORS) {
5257
- const stopContent = result.text + `
5258
-
5259
- [AGENT LOOP STOPPED: endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times \u2014 stopping to avoid loop]`;
5260
- toolResults.push({ type: "tool_result", tool_use_id: block.id, content: stopContent });
5261
- msgs.push({ role: "user", content: toolResults });
5262
- return { content: `Endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times. Last error: ${result.text}`, toolCalls };
5263
- }
5264
- }
5265
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
5266
- const stopMsg = `Stopped after ${MAX_CONSECUTIVE_ERRORS} consecutive errors. Last: ${result.text}`;
5267
- const stopContent = result.text + `
5268
-
5269
- [AGENT LOOP STOPPED: ${stopMsg}]`;
5270
- toolResults.push({ type: "tool_result", tool_use_id: block.id, content: stopContent });
5271
- msgs.push({ role: "user", content: toolResults });
5272
- return { content: stopMsg, toolCalls };
5273
- }
5274
- } else {
5275
- consecutiveErrors = 0;
5276
- }
5277
- toolResults.push({ type: "tool_result", tool_use_id: block.id, content: result.text });
5278
- }
5279
- if (!toolResults.length)
5280
- return { content: fullText, toolCalls };
5281
- msgs.push({ role: "user", content: toolResults });
5282
- }
5283
- return { content: "(max iterations reached)", toolCalls };
5284
- }
5285
- async function openaiCompatibleLoop(base, apiKey, model, extraHeaders, system, initialMessages, emit) {
5286
- const msgs = [{ role: "system", content: system }, ...initialMessages];
5287
- const toolCalls = [];
5288
- const authHeaders = {};
5289
- if (apiKey)
5290
- authHeaders["Authorization"] = `Bearer ${apiKey}`;
5291
- let totalTools = 0;
5292
- let consecutiveErrors = 0;
5293
- const endpointErrors = {};
5294
- for (let iter = 0;iter < 40; iter++) {
5295
- const res = await fetchWithRetry(`${base}/v1/chat/completions`, {
5296
- method: "POST",
5297
- headers: { "Content-Type": "application/json", ...authHeaders, ...extraHeaders },
5298
- body: JSON.stringify({ model, messages: msgs, tools: OPENAI_TOOLS, tool_choice: "auto", stream: true })
5299
- }, emit);
5300
- if (!res.ok)
5301
- throw new Error(await res.text());
5302
- let fullContent = "";
5303
- let finishReason = "";
5304
- const tcAccum = {};
5305
- const reader = res.body.getReader();
5306
- const dec = new TextDecoder;
5307
- let buf = "";
5308
- outer:
5309
- while (true) {
5310
- const { done, value } = await reader.read();
5311
- if (done)
5312
- break;
5313
- buf += dec.decode(value, { stream: true });
5314
- const parts = buf.split(`
5315
-
5316
- `);
5317
- buf = parts.pop() ?? "";
5318
- for (const part of parts) {
5319
- let data = "";
5320
- for (const line of part.split(`
5321
- `)) {
5322
- if (line.startsWith("data: ")) {
5323
- data = line.slice(6);
5324
- break;
5325
- }
5326
- }
5327
- if (!data)
5328
- continue;
5329
- if (data === "[DONE]")
5330
- break outer;
5331
- let ev;
5332
- try {
5333
- ev = JSON.parse(data);
5334
- } catch {
5335
- continue;
5336
- }
5337
- if (ev.object === "error")
5338
- throw new Error(JSON.stringify(ev));
5339
- const choices = ev.choices;
5340
- const choice = choices?.[0];
5341
- if (!choice)
5342
- continue;
5343
- const fr = choice.finish_reason;
5344
- if (fr)
5345
- finishReason = fr;
5346
- const delta = choice.delta;
5347
- if (!delta)
5348
- continue;
5349
- if (typeof delta.content === "string" && delta.content) {
5350
- fullContent += delta.content;
5351
- emit({ type: "text_delta", text: delta.content });
5352
- }
5353
- const tcDeltas = delta.tool_calls;
5354
- if (tcDeltas) {
5355
- for (const tc of tcDeltas) {
5356
- if (!tcAccum[tc.index])
5357
- tcAccum[tc.index] = { id: "", name: "", args: "" };
5358
- const entry = tcAccum[tc.index];
5359
- if (tc.id)
5360
- entry.id += tc.id;
5361
- if (tc.function?.name)
5362
- entry.name += tc.function.name;
5363
- if (tc.function?.arguments)
5364
- entry.args += tc.function.arguments;
5365
- }
5366
- }
5367
- }
5368
- }
5369
- if (finishReason !== "tool_calls")
5370
- return { content: fullContent, toolCalls };
5371
- if (totalTools >= MAX_TOTAL_TOOLS) {
5372
- return { content: `Agent stopped: reached ${MAX_TOTAL_TOOLS} tool calls. Please break your request into smaller steps.`, toolCalls };
5373
- }
5374
- const msgToolCalls = Object.values(tcAccum).map((tc) => ({
5375
- id: tc.id,
5376
- type: "function",
5377
- function: { name: tc.name, arguments: tc.args }
5378
- }));
5379
- msgs.push({ role: "assistant", content: fullContent || null, tool_calls: msgToolCalls });
5380
- for (const tc of Object.values(tcAccum)) {
5381
- let args = {};
5382
- try {
5383
- args = JSON.parse(tc.args);
5384
- } catch {}
5385
- totalTools++;
5386
- emit({ type: "tool_start", tool: tc.name, input: args });
5387
- const result = await executeTool(tc.name, args);
5388
- emit({ type: "tool_done", tool: tc.name, input: args, output: result.text, isError: result.isError });
5389
- toolCalls.push({ tool: tc.name, input: args, output: result.text, isError: result.isError });
5390
- msgs.push({ role: "tool", tool_call_id: tc.id, content: result.text });
5391
- if (result.isError) {
5392
- consecutiveErrors++;
5393
- if (tc.name === "execute_api_request" && args.operationId) {
5394
- const eid = String(args.operationId);
5395
- endpointErrors[eid] = (endpointErrors[eid] ?? 0) + 1;
5396
- if (endpointErrors[eid] >= MAX_SAME_ENDPOINT_ERRORS) {
5397
- return { content: `Endpoint "${eid}" failed ${MAX_SAME_ENDPOINT_ERRORS} times. Last error: ${result.text}`, toolCalls };
5398
- }
5399
- }
5400
- if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
5401
- return { content: `Stopped after ${MAX_CONSECUTIVE_ERRORS} consecutive errors. Last: ${result.text}`, toolCalls };
5402
- }
5403
- } else {
5404
- consecutiveErrors = 0;
5405
- }
5406
- }
5407
- }
5408
- return { content: "(max iterations reached)", toolCalls };
5409
- }
5410
6274
  async function handleAiChat(req) {
5411
6275
  let body;
5412
6276
  try {
@@ -5417,39 +6281,58 @@ async function handleAiChat(req) {
5417
6281
  const settingsRow = dbQueries.getSettings();
5418
6282
  const settings = settingsRow ? JSON.parse(settingsRow.value) : {};
5419
6283
  const ai = settings.ai ?? {};
6284
+ const provider = ai.provider ?? "anthropic";
6285
+ const providerDefaults = PROVIDER_DEFAULTS[provider] ?? { model: "" };
6286
+ const requiresKey = provider !== "ollama" && provider !== "custom";
6287
+ if (requiresKey && !ai.apiKey) {
6288
+ return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
6289
+ }
6290
+ if (!hasState())
6291
+ return json({ error: "No spec loaded." }, 400);
5420
6292
  const { spec, operations } = getState();
5421
6293
  const preview = operations.slice(0, 40).map((op) => `- ${op.method.toUpperCase()} ${op.path}${op.summary ? `: ${op.summary}` : ""}`).join(`
5422
6294
  `);
5423
6295
  const activeAuth = dbQueries.getActiveProfile();
5424
- const authLine = activeAuth ? `Active auth: "${activeAuth.name}" (${activeAuth.type})` : "No active auth profile. If the API requires auth, call list_auth_profiles first, then set_active_auth, or login and call save_auth_token.";
6296
+ const authLine = activeAuth ? `Active auth: "${activeAuth.name}" (${activeAuth.type})` : "No active auth profile. Call list_auth_profiles, then set_active_auth or save_auth_token.";
6297
+ const memory = dbQueries.getMemory(20);
6298
+ const memorySection = memory.length ? `
6299
+ ## Memory from previous sessions
6300
+ ${memory.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content.slice(0, 300)}${m.content.length > 300 ? "\u2026" : ""}`).join(`
6301
+ `)}
6302
+ ` : "";
5425
6303
  const system = `You are an AI assistant for the "${spec.title}" API (v${spec.version}). Base URL: ${spec.baseUrl}.
5426
6304
  Total endpoints: ${operations.length}. Sample:
5427
6305
  ${preview}${operations.length > 40 ? `
5428
6306
  ... and ${operations.length - 40} more` : ""}
5429
6307
 
5430
6308
  ${authLine}
6309
+ ${memorySection}
6310
+ Tools:
6311
+ - search_endpoints / get_endpoint_schema \u2014 explore API structure (results cached; never repeat the same query)
6312
+ - execute_api_request \u2014 call an endpoint
6313
+ - list_auth_profiles / set_active_auth / save_auth_token \u2014 manage credentials
6314
+ \u2022 save_auth_token supports token_type="basic" with username+password for HTTP Basic auth
6315
+ - fetch_url \u2014 external docs
6316
+ - dns_lookup \u2014 connectivity diagnostics
6317
+ - get_recent_logs \u2014 proxy traffic history
6318
+ - run_security_check \u2014 static security analysis
5431
6319
 
5432
- Tools available:
5433
- - search_endpoints / get_endpoint_schema: explore API structure
5434
- - execute_api_request: call an endpoint
5435
- - list_auth_profiles: list all saved auth profiles (name, type, active)
5436
- - set_active_auth(name): switch to a saved profile before making requests
5437
- - save_auth_token(name, token): IMMEDIATELY call this after a successful login that returns a token \u2014 saves the token as a named profile and activates it so subsequent requests are authenticated
5438
- - fetch_url: fetch external docs
5439
- - dns_lookup: DNS resolution / connectivity
5440
- - get_recent_logs: recent request/response traffic
5441
- - run_security_check: security analysis on an endpoint
6320
+ Auth workflow: 401/403 \u2192 list_auth_profiles \u2192 set_active_auth OR find login endpoint \u2192 save_auth_token \u2192 retry.
5442
6321
 
5443
- Authentication workflow: if requests return 401/403, call list_auth_profiles first. If a profile exists, call set_active_auth. If none, find and call the login endpoint, extract the token from the response, then call save_auth_token immediately. After saving, retry the original request.
6322
+ Rules:
6323
+ - Never repeat a search you already ran \u2014 results are cached.
6324
+ - Diagnose errors before retrying. Three failures on the same endpoint stops the agent.
6325
+ - Do not fire rapid successive API requests.
5444
6326
 
5445
- 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.
6327
+ Be concise. Format code and JSON in fenced blocks.${ai.customInstructions ? `
5446
6328
 
5447
- Be concise and practical. Format code and JSON in code blocks.`;
5448
- const provider = ai.provider ?? "anthropic";
5449
- const requiresKey = provider !== "ollama" && provider !== "custom";
5450
- if (requiresKey && !ai.apiKey) {
5451
- return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
5452
- }
6329
+ ---
6330
+ ## Custom instructions
6331
+ ${ai.customInstructions}` : ""}${body.extra_context ? `
6332
+
6333
+ ---
6334
+ ## Context
6335
+ ${body.extra_context}` : ""}`;
5453
6336
  const { readable, writable } = new TransformStream;
5454
6337
  const writer = writable.getWriter();
5455
6338
  const enc = new TextEncoder;
@@ -5459,62 +6342,39 @@ Be concise and practical. Format code and JSON in code blocks.`;
5459
6342
  `)).catch(() => {});
5460
6343
  };
5461
6344
  const msgs = body.messages;
6345
+ const toolCache = new Map;
6346
+ const abortCtrl = new AbortController;
6347
+ const lastUserMsg = [...msgs].reverse().find((m) => m.role === "user");
6348
+ const userMemoryContent = typeof lastUserMsg?.content === "string" ? lastUserMsg.content : null;
5462
6349
  (async () => {
5463
6350
  try {
5464
- let result;
5465
- if (provider === "anthropic") {
5466
- result = await anthropicAgentLoop(ai.apiKey, ai.model || "claude-haiku-4-5-20251001", system, msgs, emit);
5467
- } else if (provider === "openai") {
5468
- const base = (ai.baseUrl || "https://api.openai.com").replace(/\/$/, "");
5469
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "gpt-4o-mini", {}, system, msgs, emit);
5470
- } else if (provider === "mistral") {
5471
- const base = (ai.baseUrl || "https://api.mistral.ai").replace(/\/$/, "");
5472
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "mistral-small-latest", {}, system, msgs, emit);
5473
- } else if (provider === "github-copilot") {
5474
- const base = (ai.baseUrl || "https://api.githubcopilot.com").replace(/\/$/, "");
5475
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "gpt-4o", {
5476
- "Copilot-Integration-Id": "vscode-chat",
5477
- "Editor-Version": "vscode/1.85.0"
5478
- }, system, msgs, emit);
5479
- } else if (provider === "groq") {
5480
- const base = (ai.baseUrl || "https://api.groq.com/openai").replace(/\/$/, "");
5481
- result = await openaiCompatibleLoop(base, ai.apiKey, ai.model || "llama-3.1-70b-versatile", {}, system, msgs, emit);
5482
- } else if (provider === "custom") {
5483
- if (!ai.baseUrl) {
5484
- emit({ type: "error", message: "Custom provider requires a Base URL." });
5485
- await writer.close();
5486
- return;
5487
- }
5488
- result = await openaiCompatibleLoop(ai.baseUrl.replace(/\/$/, ""), ai.apiKey, ai.model || "", {}, system, msgs, emit);
5489
- } else if (provider === "ollama") {
5490
- const base = (ai.baseUrl || "http://localhost:11434").replace(/\/$/, "");
5491
- const res = await fetch(`${base}/api/chat`, {
5492
- method: "POST",
5493
- headers: { "Content-Type": "application/json" },
5494
- body: JSON.stringify({ model: ai.model || "llama3", messages: [{ role: "system", content: system }, ...msgs], stream: false })
5495
- });
5496
- const d = await res.json();
5497
- result = { content: d.message.content ?? "", toolCalls: [] };
5498
- } else if (provider === "gemini") {
5499
- const model = ai.model || "gemini-1.5-flash";
5500
- const base = (ai.baseUrl || "https://generativelanguage.googleapis.com").replace(/\/$/, "");
5501
- const res = await fetch(`${base}/v1beta/models/${model}:generateContent?key=${ai.apiKey}`, {
5502
- method: "POST",
5503
- headers: { "Content-Type": "application/json" },
5504
- body: JSON.stringify({
5505
- systemInstruction: { parts: [{ text: system }] },
5506
- contents: msgs.map((m) => ({ role: m.role === "assistant" ? "model" : "user", parts: [{ text: m.content }] })),
5507
- generationConfig: { maxOutputTokens: 4096 }
5508
- })
5509
- });
5510
- const d = await res.json();
5511
- result = { content: d.candidates[0]?.content.parts[0]?.text ?? "", toolCalls: [] };
5512
- } else {
5513
- emit({ type: "error", message: `Unknown provider: ${provider}` });
5514
- await writer.close();
5515
- return;
6351
+ const result = await runAgentLoop({
6352
+ provider,
6353
+ apiKey: ai.apiKey,
6354
+ model: ai.model || providerDefaults.model,
6355
+ baseUrl: ai.baseUrl || providerDefaults.baseUrl,
6356
+ maxTokens: ai.maxTokens ?? 4096,
6357
+ stepTimeoutMs: ai.stepTimeoutMs ?? 60000,
6358
+ temperature: ai.temperature,
6359
+ topK: ai.topK && ai.topK > 0 ? ai.topK : undefined,
6360
+ parallelTools: true,
6361
+ enablePromptCache: true
6362
+ }, system, msgs, TOOL_SCHEMAS, (name, args) => executeTool(name, args, toolCache), emit, abortCtrl.signal, toolCache);
6363
+ if (result.content && result.stopReason !== "max_iterations") {
6364
+ try {
6365
+ if (userMemoryContent)
6366
+ dbQueries.saveMemory("user", userMemoryContent.slice(0, 1000));
6367
+ dbQueries.saveMemory("assistant", result.content.slice(0, 1000));
6368
+ dbQueries.trimMemory(40);
6369
+ } catch {}
5516
6370
  }
5517
- emit({ type: "done", content: result.content, toolCalls: result.toolCalls });
6371
+ emit({
6372
+ type: "done",
6373
+ content: result.content,
6374
+ toolCalls: result.toolCalls,
6375
+ stopReason: result.stopReason,
6376
+ tokens: result.tokens
6377
+ });
5518
6378
  } catch (e) {
5519
6379
  emit({ type: "error", message: e instanceof Error ? e.message : String(e) });
5520
6380
  } finally {
@@ -5524,11 +6384,7 @@ Be concise and practical. Format code and JSON in code blocks.`;
5524
6384
  }
5525
6385
  })();
5526
6386
  return new Response(readable, {
5527
- headers: {
5528
- "Content-Type": "text/event-stream",
5529
- "Cache-Control": "no-cache",
5530
- ...CORS3
5531
- }
6387
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...CORS4 }
5532
6388
  });
5533
6389
  }
5534
6390
  function handleGetProfiles() {
@@ -5542,7 +6398,7 @@ async function handleCreateProfile(req) {
5542
6398
  return badRequest("Invalid JSON");
5543
6399
  }
5544
6400
  const profile = {
5545
- id: randomUUID2(),
6401
+ id: randomUUID(),
5546
6402
  name: String(body.name ?? "").trim() || "Unnamed",
5547
6403
  description: String(body.description ?? ""),
5548
6404
  type: String(body.type ?? "none"),
@@ -5595,7 +6451,7 @@ async function handleCreateRule(req) {
5595
6451
  return badRequest("Invalid JSON");
5596
6452
  }
5597
6453
  const rule = {
5598
- id: randomUUID2(),
6454
+ id: randomUUID(),
5599
6455
  enabled: body.enabled ?? 1,
5600
6456
  name: body.name ?? "",
5601
6457
  sort_order: body.sort_order ?? 0,
@@ -5887,7 +6743,7 @@ async function handleCreateSaved(req) {
5887
6743
  }
5888
6744
  if (!body.name || typeof body.name !== "string")
5889
6745
  return badRequest("name is required");
5890
- const id = randomUUID2();
6746
+ const id = randomUUID();
5891
6747
  dbQueries.insertSavedRequest({
5892
6748
  id,
5893
6749
  name: String(body.name),
@@ -5905,42 +6761,248 @@ async function handleCreateSaved(req) {
5905
6761
  });
5906
6762
  return json(dbQueries.getSavedRequest(id), 201);
5907
6763
  }
5908
- async function handleUpdateSaved(req, path) {
5909
- const id = path.replace("/api/saved/", "");
5910
- if (!dbQueries.getSavedRequest(id))
6764
+ async function handleUpdateSaved(req, path) {
6765
+ const id = path.replace("/api/saved/", "");
6766
+ if (!dbQueries.getSavedRequest(id))
6767
+ return notFound();
6768
+ let body;
6769
+ try {
6770
+ body = await req.json();
6771
+ } catch {
6772
+ return badRequest("Invalid JSON");
6773
+ }
6774
+ const patch = {};
6775
+ const allowed = ["name", "folder", "method", "url", "headers", "params", "body", "body_type", "raw_type", "form_rows", "auth", "notes"];
6776
+ for (const key of allowed) {
6777
+ if (key in body)
6778
+ patch[key] = typeof body[key] === "string" ? String(body[key]) : JSON.stringify(body[key]);
6779
+ }
6780
+ if (Object.keys(patch).length)
6781
+ dbQueries.updateSavedRequest(id, patch);
6782
+ return json(dbQueries.getSavedRequest(id));
6783
+ }
6784
+ function handleDeleteSaved(path) {
6785
+ const id = path.replace("/api/saved/", "");
6786
+ if (!dbQueries.getSavedRequest(id))
6787
+ return notFound();
6788
+ dbQueries.deleteSavedRequest(id);
6789
+ return json({ ok: true });
6790
+ }
6791
+ function workflowRow(row) {
6792
+ if (!row)
6793
+ return null;
6794
+ let steps = [];
6795
+ try {
6796
+ steps = JSON.parse(row.steps);
6797
+ } catch {}
6798
+ return { ...row, steps };
6799
+ }
6800
+ function handleGetWorkflows() {
6801
+ const rows = dbQueries.getWorkflows().map((r) => workflowRow(r)).filter(Boolean);
6802
+ return json(rows);
6803
+ }
6804
+ async function handleCreateWorkflow(req) {
6805
+ let body;
6806
+ try {
6807
+ body = await req.json();
6808
+ } catch {
6809
+ return badRequest("Invalid JSON");
6810
+ }
6811
+ const id = randomUUID();
6812
+ dbQueries.insertWorkflow({
6813
+ id,
6814
+ name: String(body.name ?? "Untitled Workflow"),
6815
+ description: String(body.description ?? ""),
6816
+ steps: typeof body.steps === "string" ? body.steps : JSON.stringify(body.steps ?? [])
6817
+ });
6818
+ return json(workflowRow(dbQueries.getWorkflow(id)), 201);
6819
+ }
6820
+ async function handleUpdateWorkflow(req, path) {
6821
+ const id = path.slice("/api/workflows/".length);
6822
+ if (!dbQueries.getWorkflow(id))
6823
+ return notFound();
6824
+ let body;
6825
+ try {
6826
+ body = await req.json();
6827
+ } catch {
6828
+ return badRequest("Invalid JSON");
6829
+ }
6830
+ const patch = {};
6831
+ if ("name" in body)
6832
+ patch.name = String(body.name);
6833
+ if ("description" in body)
6834
+ patch.description = String(body.description);
6835
+ if ("steps" in body)
6836
+ patch.steps = typeof body.steps === "string" ? body.steps : JSON.stringify(body.steps);
6837
+ if (Object.keys(patch).length)
6838
+ dbQueries.updateWorkflow(id, patch);
6839
+ return json(workflowRow(dbQueries.getWorkflow(id)));
6840
+ }
6841
+ function handleDeleteWorkflow(path) {
6842
+ const id = path.slice("/api/workflows/".length);
6843
+ if (!dbQueries.getWorkflow(id))
6844
+ return notFound();
6845
+ dbQueries.deleteWorkflow(id);
6846
+ return json({ ok: true });
6847
+ }
6848
+ async function handleGenerateWorkflow(req) {
6849
+ if (!hasState())
6850
+ return badRequest("No spec loaded");
6851
+ let body;
6852
+ try {
6853
+ body = await req.json();
6854
+ } catch {
6855
+ return badRequest("Invalid JSON");
6856
+ }
6857
+ const settingsRow = dbQueries.getSettings();
6858
+ const settings = settingsRow ? JSON.parse(settingsRow.value) : {};
6859
+ const ai = settings.ai ?? {};
6860
+ const provider = ai.provider ?? "anthropic";
6861
+ if (provider !== "ollama" && !ai.apiKey) {
6862
+ return json({ error: "No AI API key configured. Go to Settings \u2192 AI Provider to add one." }, 400);
6863
+ }
6864
+ const { spec, operations } = getState();
6865
+ const endpointList = operations.slice(0, 80).map((op) => `${op.method.toUpperCase()} ${op.path}${op.operationId ? ` [${op.operationId}]` : ""}${op.summary ? ` \u2014 ${op.summary}` : ""}`).join(`
6866
+ `);
6867
+ const userPrompt = body.prompt?.trim() || "Generate a realistic end-to-end test workflow covering authentication and CRUD operations.";
6868
+ const systemMsg = `You generate API test workflows as JSON for the "${spec.title}" API (base: ${spec.baseUrl}).
6869
+
6870
+ Available endpoints:
6871
+ ${endpointList}
6872
+
6873
+ Return ONLY valid JSON (no markdown fences) matching this schema exactly:
6874
+ {
6875
+ "name": "string",
6876
+ "description": "string",
6877
+ "steps": [
6878
+ {
6879
+ "id": "step_1",
6880
+ "label": "Human-readable name",
6881
+ "method": "GET|POST|PUT|PATCH|DELETE",
6882
+ "path": "/exact/path/from/spec",
6883
+ "operationId": "operationId or null",
6884
+ "pathParams": {},
6885
+ "queryParams": {},
6886
+ "headers": {},
6887
+ "body": null,
6888
+ "extract": [{"var": "varName", "path": "$.field.nested"}],
6889
+ "assert": [{"type": "status", "statusCode": 200}]
6890
+ }
6891
+ ]
6892
+ }
6893
+
6894
+ Rules:
6895
+ - Use {{varName}} in path/headers/body values to reference vars extracted in prior steps
6896
+ - For auth: extract token after login, set headers: {"Authorization": "Bearer {{token}}"}
6897
+ - Keep 3\u20138 steps covering a realistic user journey
6898
+ - Only use paths that exist in the endpoint list above`;
6899
+ try {
6900
+ let text = "";
6901
+ if (provider === "anthropic") {
6902
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
6903
+ method: "POST",
6904
+ headers: { "Content-Type": "application/json", "x-api-key": ai.apiKey, "anthropic-version": "2023-06-01" },
6905
+ body: JSON.stringify({ model: ai.model || "claude-sonnet-4-6", max_tokens: 4096, system: systemMsg, messages: [{ role: "user", content: userPrompt }] })
6906
+ });
6907
+ if (!res.ok)
6908
+ throw new Error(`Anthropic: ${await res.text()}`);
6909
+ const d = await res.json();
6910
+ text = d.content.find((b) => b.type === "text")?.text ?? "";
6911
+ } else {
6912
+ 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(/\/$/, "");
6913
+ const hdrs = { "Content-Type": "application/json" };
6914
+ if (ai.apiKey)
6915
+ hdrs["Authorization"] = `Bearer ${ai.apiKey}`;
6916
+ const res = await fetch(`${base}/v1/chat/completions`, {
6917
+ method: "POST",
6918
+ headers: hdrs,
6919
+ 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 }] })
6920
+ });
6921
+ if (!res.ok)
6922
+ throw new Error(await res.text());
6923
+ const d = await res.json();
6924
+ text = d.choices[0]?.message.content ?? "";
6925
+ }
6926
+ let parsed;
6927
+ try {
6928
+ parsed = JSON.parse(text);
6929
+ } catch {
6930
+ const m = text.match(/```(?:json)?\s*\n?([\s\S]+?)\n?```/);
6931
+ if (m)
6932
+ parsed = JSON.parse(m[1]);
6933
+ else
6934
+ throw new Error("AI response was not valid JSON");
6935
+ }
6936
+ return json(parsed);
6937
+ } catch (e) {
6938
+ return json({ error: e instanceof Error ? e.message : String(e) }, 500);
6939
+ }
6940
+ }
6941
+ function handleRunWorkflow(path) {
6942
+ const id = path.slice("/api/workflows/".length, -"/run".length);
6943
+ const row = dbQueries.getWorkflow(id);
6944
+ if (!row)
5911
6945
  return notFound();
5912
- let body;
6946
+ if (!hasState())
6947
+ return badRequest("No spec loaded");
6948
+ let steps;
5913
6949
  try {
5914
- body = await req.json();
6950
+ steps = JSON.parse(row.steps);
5915
6951
  } catch {
5916
- return badRequest("Invalid JSON");
5917
- }
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]);
6952
+ return badRequest("Invalid workflow steps JSON");
5923
6953
  }
5924
- if (Object.keys(patch).length)
5925
- dbQueries.updateSavedRequest(id, patch);
5926
- return json(dbQueries.getSavedRequest(id));
6954
+ if (!steps.length)
6955
+ return badRequest("Workflow has no steps");
6956
+ const { readable, writable } = new TransformStream;
6957
+ const writer = writable.getWriter();
6958
+ const enc = new TextEncoder;
6959
+ const emit = (e) => {
6960
+ writer.write(enc.encode(`data: ${JSON.stringify(e)}
6961
+
6962
+ `)).catch(() => {});
6963
+ };
6964
+ (async () => {
6965
+ try {
6966
+ await runWorkflow(steps, emit);
6967
+ } catch (e) {
6968
+ emit({ type: "error", message: e instanceof Error ? e.message : String(e) });
6969
+ } finally {
6970
+ try {
6971
+ await writer.close();
6972
+ } catch {}
6973
+ }
6974
+ })();
6975
+ return new Response(readable, {
6976
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", ...CORS4 }
6977
+ });
5927
6978
  }
5928
- function handleDeleteSaved(path) {
5929
- const id = path.replace("/api/saved/", "");
5930
- if (!dbQueries.getSavedRequest(id))
5931
- return notFound();
5932
- dbQueries.deleteSavedRequest(id);
6979
+ function handleGetCaptureBins() {
6980
+ return json(dbQueries.getCaptureBins());
6981
+ }
6982
+ async function handleCreateCaptureBin(req) {
6983
+ const body = await req.json().catch(() => ({}));
6984
+ const id = randomUUID().replace(/-/g, "").slice(0, 8);
6985
+ const name = String(body.name ?? "").trim() || "Untitled bin";
6986
+ dbQueries.insertCaptureBin(id, name);
6987
+ return json({ id, name, created_at: Math.floor(Date.now() / 1000) }, 201);
6988
+ }
6989
+ function handleDeleteCaptureBin(path) {
6990
+ const id = path.replace("/api/capture/bins/", "");
6991
+ dbQueries.deleteCaptureBin(id);
5933
6992
  return json({ ok: true });
5934
6993
  }
5935
- var CORS3, TOOL_DEFS, ANTHROPIC_TOOLS, OPENAI_TOOLS, MAX_TOTAL_TOOLS = 40, MAX_CONSECUTIVE_ERRORS = 5, MAX_SAME_ENDPOINT_ERRORS = 3;
6994
+ var CORS4, TOOL_DEFS, _lastApiCallMs = 0, MIN_API_CALL_INTERVAL_MS = 400, TOOL_SCHEMAS, PROVIDER_DEFAULTS;
5936
6995
  var init_routes = __esm(() => {
5937
6996
  init_db();
5938
6997
  init_engine();
5939
6998
  init_bus();
5940
6999
  init_state();
7000
+ init_parser();
5941
7001
  init_config();
5942
7002
  init_version();
5943
- CORS3 = {
7003
+ init_engine2();
7004
+ init_harness();
7005
+ CORS4 = {
5944
7006
  "Access-Control-Allow-Origin": "*",
5945
7007
  "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
5946
7008
  "Access-Control-Allow-Headers": "Content-Type, Authorization"
@@ -6008,38 +7070,433 @@ var init_routes = __esm(() => {
6008
7070
  required: ["name"]
6009
7071
  },
6010
7072
  save_auth_token: {
6011
- description: "Save a bearer token or API key as a named auth profile and immediately activate it. Call this right after a successful login endpoint returns a token so all subsequent API requests are authenticated.",
7073
+ description: "Save a bearer token, API key, or basic auth credentials as a named auth profile and immediately activate it. Call this right after a successful login endpoint returns a token so all subsequent API requests are authenticated.",
6012
7074
  params: {
6013
7075
  name: { type: "string", description: 'Profile name, e.g. "user session" or the username' },
6014
- token: { type: "string", description: "The bearer token or API key value to save" },
6015
- token_type: { type: "string", enum: ["bearer", "apikey_header", "apikey_query"], description: "Token type (default: bearer)" },
6016
- header_name: { type: "string", description: "Header name for apikey_header type (default: X-Api-Key)" }
7076
+ token: { type: "string", description: "The bearer token or API key value (omit for basic auth)" },
7077
+ token_type: { type: "string", enum: ["bearer", "apikey_header", "apikey_query", "basic"], description: "Token type (default: bearer)" },
7078
+ header_name: { type: "string", description: "Header name for apikey_header type (default: X-Api-Key)" },
7079
+ username: { type: "string", description: "Username for basic auth" },
7080
+ password: { type: "string", description: "Password for basic auth" }
6017
7081
  },
6018
- required: ["name", "token"]
7082
+ required: ["name"]
6019
7083
  }
6020
7084
  };
6021
- ANTHROPIC_TOOLS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
7085
+ TOOL_SCHEMAS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
6022
7086
  name,
6023
7087
  description: def.description,
6024
- input_schema: { type: "object", properties: def.params, required: def.required }
6025
- }));
6026
- OPENAI_TOOLS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
6027
- type: "function",
6028
- function: { name, description: def.description, parameters: { type: "object", properties: def.params, required: def.required } }
7088
+ params: def.params,
7089
+ required: def.required
6029
7090
  }));
7091
+ PROVIDER_DEFAULTS = {
7092
+ anthropic: { model: "claude-haiku-4-5-20251001" },
7093
+ openai: { model: "gpt-4o-mini", baseUrl: "https://api.openai.com" },
7094
+ mistral: { model: "mistral-small-latest", baseUrl: "https://api.mistral.ai" },
7095
+ groq: { model: "llama-3.1-70b-versatile", baseUrl: "https://api.groq.com/openai" },
7096
+ "github-copilot": { model: "gpt-4o", baseUrl: "https://api.githubcopilot.com" },
7097
+ ollama: { model: "llama3", baseUrl: "http://localhost:11434" },
7098
+ gemini: { model: "gemini-1.5-flash" },
7099
+ custom: { model: "" }
7100
+ };
7101
+ });
7102
+
7103
+ // src/repl.ts
7104
+ function stripAnsi(s) {
7105
+ return s.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
7106
+ }
7107
+ function visualRows(line, cols) {
7108
+ const len = stripAnsi(line).length;
7109
+ if (len === 0)
7110
+ return 1;
7111
+ return Math.ceil(len / cols);
7112
+ }
7113
+
7114
+ class Repl {
7115
+ buf = "";
7116
+ history = [];
7117
+ histIdx = -1;
7118
+ savedBuf = "";
7119
+ running = false;
7120
+ statusText = "";
7121
+ promptDrawn = false;
7122
+ drawingPrompt = false;
7123
+ promptVisualRows = 0;
7124
+ onCmd = null;
7125
+ cols = process.stdout.columns || 80;
7126
+ dynSuggestions = [];
7127
+ constructor() {
7128
+ if (isTTY) {
7129
+ process.stdout.on("resize", () => {
7130
+ this.cols = process.stdout.columns || 80;
7131
+ if (this.running)
7132
+ this.redraw();
7133
+ });
7134
+ }
7135
+ }
7136
+ setDynamicSuggestions(items) {
7137
+ this.dynSuggestions = items;
7138
+ if (this.running)
7139
+ this.redraw();
7140
+ }
7141
+ setStatus(text) {
7142
+ this.statusText = text;
7143
+ if (this.running)
7144
+ this.redraw();
7145
+ }
7146
+ print(line) {
7147
+ process.stdout.write(line + `
7148
+ `);
7149
+ }
7150
+ start(onCmd) {
7151
+ this.onCmd = onCmd;
7152
+ this.running = true;
7153
+ if (!isTTY || !process.stdin.setRawMode) {
7154
+ this.startSimple(onCmd);
7155
+ return;
7156
+ }
7157
+ const origWrite = process.stdout.write.bind(process.stdout);
7158
+ const self = this;
7159
+ const patched = function(chunk, enc, cb) {
7160
+ if (self.drawingPrompt)
7161
+ return origWrite(chunk, enc, cb);
7162
+ const str = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? new TextDecoder().decode(chunk) : String(chunk);
7163
+ const isSpinnerWrite = str.startsWith("\r") && !str.includes(`
7164
+ `);
7165
+ if (isSpinnerWrite || !self.promptDrawn)
7166
+ return origWrite(chunk, enc, cb);
7167
+ self.drawingPrompt = true;
7168
+ for (let i = 0;i < self.promptVisualRows - 1; i++)
7169
+ origWrite("\x1B[A");
7170
+ origWrite("\r\x1B[J");
7171
+ self.promptDrawn = false;
7172
+ const r = origWrite(chunk, enc, cb);
7173
+ if (!str.endsWith(`
7174
+ `))
7175
+ origWrite(`
7176
+ `);
7177
+ const lines = self.buildPromptLines();
7178
+ self.promptVisualRows = lines.reduce((sum, l) => sum + visualRows(l, self.cols), 0);
7179
+ origWrite(lines.join(`
7180
+ `));
7181
+ self.promptDrawn = true;
7182
+ self.drawingPrompt = false;
7183
+ return r;
7184
+ };
7185
+ patched.__orig = origWrite;
7186
+ process.stdout.write = patched;
7187
+ process.stdin.setRawMode(true);
7188
+ process.stdin.resume();
7189
+ process.stdin.setEncoding("utf8");
7190
+ process.stdin.on("data", (key) => {
7191
+ this.handleKey(key).catch(() => {});
7192
+ });
7193
+ this.drawPrompt();
7194
+ }
7195
+ stop() {
7196
+ this.running = false;
7197
+ if (this.promptDrawn)
7198
+ this.clearPrompt();
7199
+ if (process.stdout.write.__orig) {
7200
+ process.stdout.write = process.stdout.write.__orig;
7201
+ }
7202
+ try {
7203
+ process.stdin.setRawMode(false);
7204
+ process.stdin.pause();
7205
+ } catch {}
7206
+ }
7207
+ getSuggestions() {
7208
+ if (!this.buf.startsWith("/"))
7209
+ return [];
7210
+ const raw = this.buf.slice(1);
7211
+ if (!raw)
7212
+ return BASE.slice(0, 6);
7213
+ const parts = raw.split(" ");
7214
+ const base = parts[0] ?? "";
7215
+ if (raw.startsWith("auth use ") && this.dynSuggestions.length) {
7216
+ const typed = raw.slice("auth use ".length);
7217
+ return this.dynSuggestions.filter((s) => s.label.toLowerCase().startsWith(typed.toLowerCase()));
7218
+ }
7219
+ if (parts.length >= 2 && SUB[base]) {
7220
+ return (SUB[base] ?? []).filter((s) => s.value.startsWith(raw));
7221
+ }
7222
+ return BASE.filter((c) => c.value.startsWith(raw));
7223
+ }
7224
+ getGhostText() {
7225
+ if (!this.buf.startsWith("/"))
7226
+ return "";
7227
+ const suggestions = this.getSuggestions();
7228
+ if (!suggestions.length)
7229
+ return "";
7230
+ const first = suggestions[0];
7231
+ const full = "/" + first.value;
7232
+ if (full === this.buf || !full.startsWith(this.buf))
7233
+ return "";
7234
+ return full.slice(this.buf.length);
7235
+ }
7236
+ buildPromptLines() {
7237
+ const lines = [];
7238
+ const suggestions = this.getSuggestions();
7239
+ const DOT = paint.dim(" \xB7 ");
7240
+ if (this.buf.startsWith("/") && suggestions.length > 0) {
7241
+ const items = suggestions.slice(0, 5);
7242
+ const row = items.map((s, i) => {
7243
+ const label = "/" + s.label;
7244
+ const desc = s.desc ? paint.dim(" " + s.desc) : "";
7245
+ return i === 0 ? paint.cyan(label) + desc : paint.dim(label);
7246
+ }).join(DOT);
7247
+ lines.push(` ${row}`);
7248
+ }
7249
+ if (this.statusText) {
7250
+ const plain = stripAnsi(this.statusText);
7251
+ const truncated = plain.length > this.cols - 4 ? plain.slice(0, this.cols - 7) + "\u2026" : plain;
7252
+ lines.push(` ${paint.dim(truncated)}`);
7253
+ }
7254
+ const ghost = this.getGhostText();
7255
+ lines.push(` ${paint.cyan("\u276F")} ${this.buf}${ghost ? paint.dim(ghost) : ""}`);
7256
+ return lines;
7257
+ }
7258
+ clearPrompt() {
7259
+ if (!this.promptDrawn)
7260
+ return;
7261
+ this.drawingPrompt = true;
7262
+ for (let i = 0;i < this.promptVisualRows - 1; i++)
7263
+ process.stdout.write("\x1B[A");
7264
+ process.stdout.write("\r\x1B[J");
7265
+ this.promptDrawn = false;
7266
+ this.drawingPrompt = false;
7267
+ }
7268
+ drawPrompt() {
7269
+ this.drawingPrompt = true;
7270
+ const lines = this.buildPromptLines();
7271
+ this.promptVisualRows = lines.reduce((sum, l) => sum + visualRows(l, this.cols), 0);
7272
+ process.stdout.write(lines.join(`
7273
+ `));
7274
+ this.promptDrawn = true;
7275
+ this.drawingPrompt = false;
7276
+ }
7277
+ redraw() {
7278
+ this.clearPrompt();
7279
+ this.drawPrompt();
7280
+ }
7281
+ async handleKey(key) {
7282
+ if (!this.running)
7283
+ return;
7284
+ if (key === "\x03" || key === "\x04") {
7285
+ this.stop();
7286
+ process.emit("SIGINT");
7287
+ return;
7288
+ }
7289
+ if (key === "\f") {
7290
+ this.drawingPrompt = true;
7291
+ process.stdout.write("\x1B[2J\x1B[H");
7292
+ this.drawingPrompt = false;
7293
+ this.promptDrawn = false;
7294
+ this.drawPrompt();
7295
+ return;
7296
+ }
7297
+ if (key === "\x15") {
7298
+ this.buf = "";
7299
+ this.redraw();
7300
+ return;
7301
+ }
7302
+ if (key === "\x17") {
7303
+ this.buf = this.buf.replace(/\S+\s*$/, "");
7304
+ this.redraw();
7305
+ return;
7306
+ }
7307
+ if (key.startsWith("\x1B")) {
7308
+ if (key === "\x1B") {
7309
+ this.buf = "";
7310
+ this.redraw();
7311
+ return;
7312
+ }
7313
+ if (key === "\x1B[A") {
7314
+ this.historyUp();
7315
+ return;
7316
+ }
7317
+ if (key === "\x1B[B") {
7318
+ this.historyDown();
7319
+ return;
7320
+ }
7321
+ if (key === "\x1B[C") {
7322
+ const g = this.getGhostText();
7323
+ if (g) {
7324
+ this.buf += g;
7325
+ const raw = this.buf.slice(1);
7326
+ if (SUB[raw])
7327
+ this.buf += " ";
7328
+ this.redraw();
7329
+ }
7330
+ return;
7331
+ }
7332
+ return;
7333
+ }
7334
+ if (key === "\t") {
7335
+ const g = this.getGhostText();
7336
+ if (g) {
7337
+ this.buf += g;
7338
+ const raw = this.buf.slice(1);
7339
+ if (SUB[raw])
7340
+ this.buf += " ";
7341
+ } else {
7342
+ const suggestions = this.getSuggestions();
7343
+ if (suggestions[0] && "/" + suggestions[0].value !== this.buf) {
7344
+ this.buf = "/" + suggestions[0].value;
7345
+ }
7346
+ }
7347
+ this.redraw();
7348
+ return;
7349
+ }
7350
+ if (key === "\x7F" || key === "\b") {
7351
+ if (this.buf.length > 0) {
7352
+ this.buf = this.buf.slice(0, -1);
7353
+ this.redraw();
7354
+ }
7355
+ return;
7356
+ }
7357
+ if (key === "\r" || key === `
7358
+ `) {
7359
+ await this.submit();
7360
+ return;
7361
+ }
7362
+ if (this.buf === "") {
7363
+ switch (key.toLowerCase()) {
7364
+ case "r":
7365
+ await this.dispatchImmediate("r");
7366
+ return;
7367
+ case "b":
7368
+ await this.dispatchImmediate("b");
7369
+ return;
7370
+ case "s":
7371
+ await this.dispatchImmediate("s");
7372
+ return;
7373
+ case "q":
7374
+ await this.dispatchImmediate("q");
7375
+ return;
7376
+ case "?":
7377
+ case "h":
7378
+ await this.dispatchImmediate("h");
7379
+ return;
7380
+ case "/":
7381
+ this.buf = "/";
7382
+ this.redraw();
7383
+ return;
7384
+ }
7385
+ }
7386
+ const printable = key.replace(/[^\x20-\x7E]/g, "");
7387
+ if (printable) {
7388
+ this.buf += printable;
7389
+ this.redraw();
7390
+ }
7391
+ }
7392
+ async submit() {
7393
+ const cmd = this.buf.trim();
7394
+ this.buf = "";
7395
+ this.clearPrompt();
7396
+ process.stdout.write(`
7397
+ `);
7398
+ this.promptDrawn = false;
7399
+ if (cmd) {
7400
+ this.history.unshift(cmd);
7401
+ if (this.history.length > 200)
7402
+ this.history.pop();
7403
+ this.histIdx = -1;
7404
+ this.savedBuf = "";
7405
+ if (this.onCmd)
7406
+ await this.onCmd(cmd);
7407
+ }
7408
+ this.drawPrompt();
7409
+ }
7410
+ async dispatchImmediate(key) {
7411
+ this.clearPrompt();
7412
+ process.stdout.write(`
7413
+ `);
7414
+ this.promptDrawn = false;
7415
+ if (this.onCmd)
7416
+ await this.onCmd(key);
7417
+ this.drawPrompt();
7418
+ }
7419
+ historyUp() {
7420
+ if (!this.history.length)
7421
+ return;
7422
+ if (this.histIdx === -1)
7423
+ this.savedBuf = this.buf;
7424
+ this.histIdx = Math.min(this.histIdx + 1, this.history.length - 1);
7425
+ this.buf = this.history[this.histIdx] ?? "";
7426
+ this.redraw();
7427
+ }
7428
+ historyDown() {
7429
+ if (this.histIdx === -1)
7430
+ return;
7431
+ this.histIdx--;
7432
+ this.buf = this.histIdx === -1 ? this.savedBuf : this.history[this.histIdx] ?? "";
7433
+ this.redraw();
7434
+ }
7435
+ startSimple(onCmd) {
7436
+ process.stdout.write(" \u276F ");
7437
+ process.stdin.setEncoding("utf8");
7438
+ let line = "";
7439
+ process.stdin.on("data", async (chunk) => {
7440
+ for (const ch of chunk) {
7441
+ if (ch === `
7442
+ ` || ch === "\r") {
7443
+ const cmd = line.trim();
7444
+ line = "";
7445
+ if (cmd)
7446
+ await onCmd(cmd);
7447
+ process.stdout.write(" \u276F ");
7448
+ } else {
7449
+ line += ch;
7450
+ }
7451
+ }
7452
+ });
7453
+ }
7454
+ }
7455
+ var BASE, SUB;
7456
+ var init_repl = __esm(() => {
7457
+ init_ui();
7458
+ BASE = [
7459
+ { value: "help", label: "help", desc: "Show all commands" },
7460
+ { value: "status", label: "status", desc: "Show server status" },
7461
+ { value: "reload", label: "reload", desc: "Hot-reload the spec" },
7462
+ { value: "spec", label: "spec", desc: "Load a different spec" },
7463
+ { value: "mcp", label: "mcp", desc: "Toggle MCP endpoint" },
7464
+ { value: "proxy", label: "proxy", desc: "Toggle HTTP proxy" },
7465
+ { value: "ai", label: "ai", desc: "Toggle AI chat" },
7466
+ { value: "readonly", label: "readonly", desc: "Toggle read-only mode" },
7467
+ { value: "auth", label: "auth", desc: "Manage auth roles" },
7468
+ { value: "token", label: "token", desc: "Manage access token" },
7469
+ { value: "tail", label: "tail", desc: "Live request log" },
7470
+ { value: "open", label: "open", desc: "Open studio in browser" },
7471
+ { value: "update", label: "update", desc: "Update wasper" },
7472
+ { value: "quit", label: "quit", desc: "Quit" }
7473
+ ];
7474
+ SUB = {
7475
+ auth: [
7476
+ { value: "auth list", label: "list", desc: "List auth profiles" },
7477
+ { value: "auth use ", label: "use", desc: "Switch active profile" },
7478
+ { value: "auth none", label: "none", desc: "Disable auth" }
7479
+ ],
7480
+ mcp: [{ value: "mcp on", label: "on", desc: "" }, { value: "mcp off", label: "off", desc: "" }],
7481
+ proxy: [{ value: "proxy on", label: "on", desc: "" }, { value: "proxy off", label: "off", desc: "" }],
7482
+ ai: [{ value: "ai on", label: "on", desc: "" }, { value: "ai off", label: "off", desc: "" }],
7483
+ readonly: [{ value: "readonly on", label: "on", desc: "" }, { value: "readonly off", label: "off", desc: "" }],
7484
+ token: [{ value: "token new", label: "new", desc: "Generate token" }, { value: "token off", label: "off", desc: "Remove token" }],
7485
+ tail: [{ value: "tail on", label: "on", desc: "" }, { value: "tail off", label: "off", desc: "" }]
7486
+ };
6030
7487
  });
6031
7488
 
6032
7489
  // src/commands/start.ts
6033
7490
  var exports_start = {};
6034
7491
  __export(exports_start, {
6035
- run: () => run5
7492
+ run: () => run6
6036
7493
  });
6037
7494
  import { parseArgs } from "util";
6038
7495
  import { createInterface } from "readline";
6039
7496
  import { homedir as homedir4 } from "os";
6040
7497
  import { join as join4, dirname } from "path";
6041
7498
  import { mkdirSync as mkdirSync2 } from "fs";
6042
- async function run5(overrideOpts) {
7499
+ async function run6(overrideOpts) {
6043
7500
  const { values } = parseArgs({
6044
7501
  args: process.argv.slice(2).filter((a) => a !== "start"),
6045
7502
  options: {
@@ -6063,18 +7520,44 @@ async function run5(overrideOpts) {
6063
7520
  printHelp();
6064
7521
  process.exit(0);
6065
7522
  }
6066
- const specUrl = overrideOpts?.url ?? (values.url ? String(values.url) : null) ?? process.env.WASPER_SPEC_URL ?? null;
7523
+ let specUrl = overrideOpts?.url ?? (values.url ? String(values.url) : null) ?? process.env.WASPER_SPEC_URL ?? null;
6067
7524
  const PORT = overrideOpts?.port ?? parseInt(String(values.port ?? "3388"), 10);
6068
7525
  const HOST = overrideOpts?.host ?? (values.host ? String(values.host) : null) ?? process.env.WASPER_HOST ?? "0.0.0.0";
6069
7526
  const ORIGIN = (overrideOpts?.origin ?? (values.origin ? String(values.origin) : null) ?? process.env.WASPER_ORIGIN ?? null)?.replace(/\/$/, "") ?? null;
6070
7527
  const TOKEN = overrideOpts?.token ?? (values.token ? String(values.token) : null) ?? process.env.WASPER_TOKEN ?? null;
6071
7528
  const bgNow = overrideOpts?.daemon ?? !!(values.background || values.daemon);
6072
7529
  const isDaemon = overrideOpts?.isDaemon ?? !!values["_daemon"];
7530
+ if (!specUrl && !isDaemon) {
7531
+ const last = dbQueries.getLastSpec();
7532
+ if (last) {
7533
+ specUrl = last.url;
7534
+ if (isTTY)
7535
+ console.log(` ${paint.dim("\u21A9")} Resuming ${paint.cyan(last.title ?? last.url)} ${paint.dim("(last used)")}
7536
+ `);
7537
+ }
7538
+ }
6073
7539
  setServerConfig({ port: PORT, host: HOST, origin: ORIGIN, token: TOKEN });
7540
+ {
7541
+ const saved = dbQueries.getSetting("features");
7542
+ if (saved) {
7543
+ try {
7544
+ const obj = JSON.parse(saved);
7545
+ const patch = {};
7546
+ if (obj.mcp === false)
7547
+ patch.mcp = false;
7548
+ if (obj.proxy === false)
7549
+ patch.proxy = false;
7550
+ if (obj.ai === false)
7551
+ patch.ai = false;
7552
+ if (Object.keys(patch).length)
7553
+ setFeatures(patch);
7554
+ } catch {}
7555
+ }
7556
+ }
6074
7557
  setFeatures({
6075
- mcp: !values["no-mcp"],
6076
- proxy: !values["no-proxy"],
6077
- ai: !values["no-ai"],
7558
+ ...values["no-mcp"] ? { mcp: false } : {},
7559
+ ...values["no-proxy"] ? { proxy: false } : {},
7560
+ ...values["no-ai"] ? { ai: false } : {},
6078
7561
  readonly: !!values.readonly
6079
7562
  });
6080
7563
  if (bgNow) {
@@ -6101,6 +7584,7 @@ async function run5(overrideOpts) {
6101
7584
  specVersion = state.spec.version;
6102
7585
  endpointCount = state.operations.length;
6103
7586
  spinner.stop();
7587
+ dbQueries.upsertSpec(specUrl, specTitle ?? null, specVersion ?? null, endpointCount);
6104
7588
  } catch (e) {
6105
7589
  spinner.stop("\u2717", `Failed to load spec: ${e instanceof Error ? e.message : String(e)}`, "red");
6106
7590
  }
@@ -6123,6 +7607,8 @@ async function run5(overrideOpts) {
6123
7607
  if (req.method === "OPTIONS") {
6124
7608
  return new Response(null, { status: 204, headers: CORS_HEADERS });
6125
7609
  }
7610
+ if (pathname.startsWith("/c/"))
7611
+ return captureHandler(req);
6126
7612
  if (!isAuthorized(req)) {
6127
7613
  return new Response(JSON.stringify({ error: "Unauthorized: pass Authorization: Bearer <token> or ?token=" }), {
6128
7614
  status: 401,
@@ -6213,127 +7699,129 @@ async function run5(overrideOpts) {
6213
7699
  function attachKeyboard(opts) {
6214
7700
  const ctx = { specUrl: opts.specUrl, PORT: opts.PORT, tailOff: null };
6215
7701
  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;
7702
+ const repl = new Repl;
7703
+ const buildStatus = () => {
7704
+ const state = hasState() ? getState() : null;
7705
+ const f = getFeatures();
7706
+ const cfg = getServerConfig();
7707
+ const active = dbQueries.getActiveProfile();
7708
+ const parts = [];
7709
+ if (state) {
7710
+ parts.push(`${state.spec.title} v${state.spec.version} \xB7 ${state.operations.length} ep`);
7711
+ } else {
7712
+ parts.push("no spec");
7713
+ }
7714
+ parts.push(`:${ctx.PORT}`);
7715
+ const flags = [];
7716
+ if (!f.mcp)
7717
+ flags.push("mcp:off");
7718
+ if (!f.proxy)
7719
+ flags.push("proxy:off");
7720
+ if (!f.ai)
7721
+ flags.push("ai:off");
7722
+ if (f.readonly)
7723
+ flags.push("readonly:on");
7724
+ if (flags.length)
7725
+ parts.push(flags.join(" "));
7726
+ parts.push(`auth:${active ? active.name : "none"}`);
7727
+ if (cfg.token)
7728
+ parts.push("token:set");
7729
+ if (ctx.tailOff)
7730
+ parts.push("tail:on");
7731
+ return parts.join(" \xB7 ");
7732
+ };
7733
+ const refreshStatus = () => repl.setStatus(buildStatus());
7734
+ const refreshAuthSuggestions = () => {
7735
+ const profiles = dbQueries.getProfiles();
7736
+ repl.setDynamicSuggestions(profiles.map((p) => ({
7737
+ value: `auth use ${p.name}`,
7738
+ label: p.name,
7739
+ desc: p.type
7740
+ })));
7741
+ };
6221
7742
  const reload = async () => {
6222
7743
  if (isReloading)
6223
7744
  return;
6224
- process.stdout.write(`
6225
- `);
6226
7745
  if (!ctx.specUrl) {
6227
- console.log(` ${paint.yellow("\u25CB")} No spec URL \u2014 use /spec <url> or start with --url <url>`);
6228
- console.log();
7746
+ console.log(`
7747
+ ${paint.yellow("\u25CB")} No spec URL \u2014 use /spec <url>
7748
+ `);
6229
7749
  return;
6230
7750
  }
6231
7751
  isReloading = true;
6232
- spinner.start(`Reloading spec\u2026`);
7752
+ let fi = 0;
7753
+ const base = buildStatus();
7754
+ const spinTimer = setInterval(() => {
7755
+ repl.setStatus(`${SPIN_FRAMES[fi++ % SPIN_FRAMES.length]} Reloading\u2026 \xB7 ${base}`);
7756
+ }, 80);
6233
7757
  try {
6234
7758
  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");
7759
+ clearInterval(spinTimer);
7760
+ dbQueries.upsertSpec(ctx.specUrl, state.spec.title, state.spec.version, state.operations.length);
7761
+ console.log(` ${paint.green("\u2713")} ${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} \xB7 ${paint.green(state.operations.length + " endpoints")}
7762
+ `);
6236
7763
  } catch (e) {
6237
- spinner.stop("\u2717", `Reload failed: ${e instanceof Error ? e.message : String(e)}`, "red");
7764
+ clearInterval(spinTimer);
7765
+ console.log(` ${paint.red("\u2717")} Reload failed: ${e instanceof Error ? e.message : String(e)}
7766
+ `);
6238
7767
  } finally {
6239
7768
  isReloading = false;
7769
+ refreshStatus();
6240
7770
  }
6241
- console.log();
6242
7771
  };
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");
7772
+ const handler = async (input) => {
7773
+ const cmd = input.trim();
7774
+ if (cmd.length === 1) {
7775
+ switch (cmd.toLowerCase()) {
7776
+ case "r":
7777
+ await reload();
6271
7778
  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")}
7779
+ case "s":
7780
+ printInlineStatus(ctx);
7781
+ return;
7782
+ case "q":
7783
+ process.emit("SIGINT");
7784
+ return;
7785
+ case "h":
7786
+ case "?":
7787
+ printInteractiveHelp();
7788
+ return;
7789
+ case "b": {
7790
+ repl.stop();
7791
+ process.on("SIGHUP", () => {});
7792
+ console.log(`
7793
+ ${paint.green("\u2713")} Detached ${paint.dim("PID " + process.pid)}`);
7794
+ console.log(` ${paint.dim("\u279C")} ${paint.dim("wasper status")} ${paint.dim("\xB7")} ${paint.dim("wasper stop")}
6307
7795
  `);
6308
- break;
7796
+ return;
7797
+ }
6309
7798
  }
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
7799
  }
7800
+ const slashCmd = cmd.startsWith("/") ? cmd : `/${cmd}`;
7801
+ await runSlashCommand(slashCmd, ctx, reload);
7802
+ refreshStatus();
7803
+ refreshAuthSuggestions();
6321
7804
  };
7805
+ refreshAuthSuggestions();
7806
+ repl.setStatus(buildStatus());
7807
+ repl.start(handler);
6322
7808
  }
6323
7809
  function printInlineStatus(ctx) {
6324
7810
  const state = hasState() ? getState() : null;
6325
7811
  const f = getFeatures();
6326
7812
  const cfg = getServerConfig();
6327
- const flag = (on) => on ? paint.green("on") : paint.red("off");
7813
+ const on = (v) => v ? paint.green("on") : paint.dim("off");
7814
+ const dot = paint.dim("\xB7");
6328
7815
  console.log(`
6329
- ${paint.dim("\u25CF")} ${paint.bold("OpenAPI Agent")} PID ${process.pid} port ${ctx.PORT}`);
7816
+ ${paint.green("\u25CF")} ${paint.bold("wasper")} ${paint.dim("PID " + process.pid)} ${dot} ${paint.dim(":" + ctx.PORT)}`);
6330
7817
  if (state) {
6331
- console.log(` ${paint.bold(state.spec.title)} v${state.spec.version} \xB7 ${state.operations.length} endpoints`);
7818
+ console.log(` ${paint.bold(state.spec.title)} ${paint.dim("v" + state.spec.version)} ${dot} ${paint.green(state.operations.length + " endpoints")}`);
7819
+ } else {
7820
+ console.log(` ${paint.dim("no spec loaded")}`);
6332
7821
  }
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")}`);
7822
+ 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
7823
  const active = dbQueries.getActiveProfile();
6335
- if (active)
6336
- console.log(` auth role: ${paint.bold(active.name)} ${paint.dim(`(${active.type})`)}`);
7824
+ console.log(` auth ${active ? paint.bold(active.name) + " " + paint.dim("(" + active.type + ")") : paint.dim("none")}`);
6337
7825
  console.log();
6338
7826
  }
6339
7827
  async function runSlashCommand(input, ctx, reload) {
@@ -6345,6 +7833,7 @@ async function runSlashCommand(input, ctx, reload) {
6345
7833
  const cur = getFeatures()[name];
6346
7834
  const next = arg === "on" ? true : arg === "off" ? false : !cur;
6347
7835
  setFeatures({ [name]: next });
7836
+ persistAndBroadcastFeatures();
6348
7837
  console.log(` ${next ? paint.green("\u2713") : paint.yellow("\u25CB")} ${label} ${next ? paint.green("enabled") : paint.yellow("disabled")}
6349
7838
  `);
6350
7839
  };
@@ -6489,40 +7978,48 @@ async function runSlashCommand(input, ctx, reload) {
6489
7978
  }
6490
7979
  }
6491
7980
  function printInteractiveHelp() {
7981
+ const k = (s) => paint.bold(s);
7982
+ const d = (s) => paint.dim(s);
7983
+ const hr = d("\u2500".repeat(50));
6492
7984
  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)")}
7985
+ ${paint.bold("Keys")} ${d("(when input is empty)")}
7986
+ ${hr}
7987
+ ${k("r")} Hot-reload spec ${k("b")} Detach to background
7988
+ ${k("s")} Print status ${k("q")} Quit
7989
+ ${k("/")} Start a command ${k("?")} This help
7990
+
7991
+ ${d("\u2191 / \u2193")} cycle command history \xB7 ${d("\u2192 or Tab")} accept autocomplete
7992
+ ${d("Ctrl+L")} clear screen \xB7 ${d("Ctrl+U")} clear input \xB7 ${d("Esc")} cancel
6500
7993
 
6501
7994
  ${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")}
7995
+ ${hr}
7996
+ ${k("/spec")} ${d("<url>")} Load a different OpenAPI spec
7997
+ ${k("/mcp")} ${d("[on|off]")} Toggle the MCP endpoint
7998
+ ${k("/proxy")} ${d("[on|off]")} Toggle the HTTP proxy
7999
+ ${k("/ai")} ${d("[on|off]")} Toggle the AI chat endpoint
8000
+ ${k("/readonly")} ${d("[on|off]")} Block non-GET upstream requests
8001
+ ${k("/auth")} List saved auth profiles
8002
+ ${k("/auth use")} ${d("<name>")} Switch active auth profile
8003
+ ${k("/auth none")} Disable auth
8004
+ ${k("/token")} ${d("[new|off|<v>]")} Show / rotate / set the access token
8005
+ ${k("/tail")} ${d("[on|off]")} Live request log in this terminal
8006
+ ${k("/open")} Open the studio in a browser
8007
+ ${k("/update")} Update wasper to the latest version
8008
+ ${k("/status")} ${k("/reload")} ${k("/help")} ${k("/quit")}
6515
8009
  `);
6516
8010
  }
6517
8011
  function printHelp() {
6518
8012
  console.log(`
6519
8013
  Usage: wasper [start] [options]
6520
8014
 
6521
- openapi-agent [--url <spec-url>] [--port <port>] Start in foreground
8015
+ wasper [--url <spec-url>] [--port <port>] Start in foreground (auto-resumes last spec)
6522
8016
  wasper start --background Start in background
6523
8017
  wasper stop Stop background server
6524
8018
  wasper status Show server status
6525
8019
  wasper reload Hot-reload the spec
8020
+ wasper ls List saved specs (history)
8021
+ wasper use <number|url> Start with a saved spec
8022
+ wasper rm <number|url> Remove a spec from history
6526
8023
 
6527
8024
  Options:
6528
8025
  --url, -u OpenAPI spec URL or local path
@@ -6641,9 +8138,11 @@ async function runFirstTimeSetup(port, origin) {
6641
8138
  ${paint.dim("Skip future prompts: set WASPER_NO_FIRST_RUN=1")}
6642
8139
  `);
6643
8140
  }
8141
+ var SPIN_FRAMES;
6644
8142
  var init_start = __esm(() => {
6645
8143
  init_server();
6646
8144
  init_handler();
8145
+ init_capture();
6647
8146
  init_routes();
6648
8147
  init_bus();
6649
8148
  init_db();
@@ -6652,6 +8151,101 @@ var init_start = __esm(() => {
6652
8151
  init_config();
6653
8152
  init_update();
6654
8153
  init_ui();
8154
+ init_repl();
8155
+ init_routes();
8156
+ SPIN_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
8157
+ });
8158
+
8159
+ // src/commands/use.ts
8160
+ var exports_use = {};
8161
+ __export(exports_use, {
8162
+ run: () => run7
8163
+ });
8164
+ async function run7() {
8165
+ const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
8166
+ const target = args[1];
8167
+ if (!target) {
8168
+ console.error(`
8169
+ Usage: wasper use <number|url>
8170
+ `);
8171
+ process.exit(1);
8172
+ }
8173
+ const history = dbQueries.getSpecHistory();
8174
+ let url = null;
8175
+ const num = parseInt(target, 10);
8176
+ if (!isNaN(num) && num >= 1 && num <= history.length) {
8177
+ url = history[num - 1]?.url ?? null;
8178
+ } else if (target.startsWith("http")) {
8179
+ url = target;
8180
+ } else {
8181
+ const match = history.find((r) => r.title?.toLowerCase().includes(target.toLowerCase()));
8182
+ if (match)
8183
+ url = match.url;
8184
+ }
8185
+ if (!url) {
8186
+ console.error(`
8187
+ ${paint.red("\u2717")} Spec not found: ${target}
8188
+ `);
8189
+ console.error(` Run ${paint.cyan("wasper ls")} to see saved specs.
8190
+ `);
8191
+ process.exit(1);
8192
+ }
8193
+ console.log(`
8194
+ ${paint.dim("\u2192")} Starting with ${paint.cyan(url)}
8195
+ `);
8196
+ const { run: startRun } = await Promise.resolve().then(() => (init_start(), exports_start));
8197
+ await startRun({ url });
8198
+ }
8199
+ var init_use = __esm(() => {
8200
+ init_db();
8201
+ init_ui();
8202
+ });
8203
+
8204
+ // src/commands/rm.ts
8205
+ var exports_rm = {};
8206
+ __export(exports_rm, {
8207
+ run: () => run8
8208
+ });
8209
+ async function run8() {
8210
+ const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
8211
+ const target = args[1];
8212
+ if (!target) {
8213
+ console.error(`
8214
+ Usage: wasper rm <number|url>
8215
+ `);
8216
+ process.exit(1);
8217
+ }
8218
+ const history = dbQueries.getSpecHistory();
8219
+ let id = null;
8220
+ let label = null;
8221
+ const num = parseInt(target, 10);
8222
+ if (!isNaN(num) && num >= 1 && num <= history.length) {
8223
+ const row = history[num - 1];
8224
+ if (row) {
8225
+ id = row.id;
8226
+ label = row.title ?? row.url;
8227
+ }
8228
+ } else {
8229
+ const match = history.find((r) => r.url === target || r.title?.toLowerCase().includes(target.toLowerCase()));
8230
+ if (match) {
8231
+ id = match.id;
8232
+ label = match.title ?? match.url;
8233
+ }
8234
+ }
8235
+ if (!id) {
8236
+ console.error(`
8237
+ ${paint.red("\u2717")} Spec not found: ${target}
8238
+ `);
8239
+ process.exit(1);
8240
+ }
8241
+ dbQueries.deleteSpec(id);
8242
+ console.log(`
8243
+ ${paint.green("\u2713")} Removed ${paint.dim(label ?? id)}
8244
+ `);
8245
+ }
8246
+ var init_rm = __esm(() => {
8247
+ init_db();
8248
+ init_ui();
6655
8249
  });
6656
8250
 
6657
8251
  // cli.ts
@@ -6675,6 +8269,17 @@ switch (subcommand) {
6675
8269
  case "reload":
6676
8270
  await Promise.resolve().then(() => (init_reload(), exports_reload)).then((m) => m.run());
6677
8271
  break;
8272
+ case "ls":
8273
+ case "list":
8274
+ await Promise.resolve().then(() => (init_ls(), exports_ls)).then((m) => m.run());
8275
+ break;
8276
+ case "use":
8277
+ await Promise.resolve().then(() => (init_use(), exports_use)).then((m) => m.run());
8278
+ break;
8279
+ case "rm":
8280
+ case "remove":
8281
+ await Promise.resolve().then(() => (init_rm(), exports_rm)).then((m) => m.run());
8282
+ break;
6678
8283
  case "start":
6679
8284
  default:
6680
8285
  await Promise.resolve().then(() => (init_start(), exports_start)).then((m) => m.run());