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.
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/cli.js +2536 -931
- package/dist/index.js +2134 -685
- 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.
|
|
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:
|
|
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
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
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
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
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
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
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 =
|
|
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 =
|
|
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", ...
|
|
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:
|
|
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
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
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
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
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
|
|
6011
|
+
const responseText = await res.text();
|
|
4955
6012
|
const latency = Date.now() - start;
|
|
4956
|
-
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6327
|
+
Be concise. Format code and JSON in fenced blocks.${ai.customInstructions ? `
|
|
5446
6328
|
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
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
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
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({
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
6946
|
+
if (!hasState())
|
|
6947
|
+
return badRequest("No spec loaded");
|
|
6948
|
+
let steps;
|
|
5913
6949
|
try {
|
|
5914
|
-
|
|
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 (
|
|
5925
|
-
|
|
5926
|
-
|
|
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
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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"
|
|
7082
|
+
required: ["name"]
|
|
6019
7083
|
}
|
|
6020
7084
|
};
|
|
6021
|
-
|
|
7085
|
+
TOOL_SCHEMAS = Object.entries(TOOL_DEFS).map(([name, def]) => ({
|
|
6022
7086
|
name,
|
|
6023
7087
|
description: def.description,
|
|
6024
|
-
|
|
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: () =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
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
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
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(`
|
|
6228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6244
|
-
|
|
6245
|
-
if (
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
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
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
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
|
-
|
|
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
|
|
7813
|
+
const on = (v) => v ? paint.green("on") : paint.dim("off");
|
|
7814
|
+
const dot = paint.dim("\xB7");
|
|
6328
7815
|
console.log(`
|
|
6329
|
-
${paint.
|
|
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)}
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
${
|
|
6495
|
-
${
|
|
6496
|
-
${
|
|
6497
|
-
${
|
|
6498
|
-
|
|
6499
|
-
${
|
|
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
|
-
${
|
|
6503
|
-
${
|
|
6504
|
-
${
|
|
6505
|
-
${
|
|
6506
|
-
${
|
|
6507
|
-
${
|
|
6508
|
-
${
|
|
6509
|
-
${
|
|
6510
|
-
${
|
|
6511
|
-
${
|
|
6512
|
-
${
|
|
6513
|
-
${
|
|
6514
|
-
${
|
|
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
|
-
|
|
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());
|