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