@bankr/cli 0.2.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +63 -0
- package/dist/commands/llm.js +28 -16
- package/dist/commands/x402.d.ts +65 -0
- package/dist/commands/x402.js +761 -0
- package/dist/lib/chains.js +4 -0
- package/dist/lib/output.d.ts +2 -0
- package/dist/lib/output.js +2 -0
- package/dist/lib/updateCheck.d.ts +8 -0
- package/dist/lib/updateCheck.js +78 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -22,8 +22,10 @@ import { submitCommand, submitJsonCommand } from "./commands/submit.js";
|
|
|
22
22
|
import { updateCommand } from "./commands/update.js";
|
|
23
23
|
import { whoamiCommand } from "./commands/whoami.js";
|
|
24
24
|
import { tokensSearchCommand, tokensInfoCommand } from "./commands/tokens.js";
|
|
25
|
+
import { x402InitCommand, x402AddCommand, x402ConfigureCommand, x402DeployCommand, x402ListCommand, x402PauseResumeCommand, x402DeleteCommand, x402RevenueCommand, x402EnvSetCommand, x402EnvListCommand, x402EnvUnsetCommand, x402SearchCommand, } from "./commands/x402.js";
|
|
25
26
|
import { profileViewCommand, profileCreateCommand, profileUpdateCommand, profileDeleteCommand, profileAddUpdateCommand, } from "./commands/profile.js";
|
|
26
27
|
import * as output from "./lib/output.js";
|
|
28
|
+
import { checkForUpdate } from "./lib/updateCheck.js";
|
|
27
29
|
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
28
30
|
/**
|
|
29
31
|
* Read prompt text from stdin (piped) or interactively.
|
|
@@ -712,6 +714,66 @@ program
|
|
|
712
714
|
.action(async (opts) => {
|
|
713
715
|
await updateCommand({ check: opts.check });
|
|
714
716
|
});
|
|
717
|
+
// ── x402 Endpoint Hosting ─────────────────────────────────────────────
|
|
718
|
+
const x402Cmd = program
|
|
719
|
+
.command("x402")
|
|
720
|
+
.description("Deploy and manage x402 paid API endpoints");
|
|
721
|
+
x402Cmd
|
|
722
|
+
.command("init")
|
|
723
|
+
.description("Scaffold x402/ folder and bankr.x402.json config")
|
|
724
|
+
.action(x402InitCommand);
|
|
725
|
+
x402Cmd
|
|
726
|
+
.command("add <name>")
|
|
727
|
+
.description("Add a new x402 service handler")
|
|
728
|
+
.action(x402AddCommand);
|
|
729
|
+
x402Cmd
|
|
730
|
+
.command("configure <name>")
|
|
731
|
+
.description("Interactively configure pricing and description")
|
|
732
|
+
.action(x402ConfigureCommand);
|
|
733
|
+
x402Cmd
|
|
734
|
+
.command("deploy [name]")
|
|
735
|
+
.description("Bundle and deploy services to Bankr")
|
|
736
|
+
.action(x402DeployCommand);
|
|
737
|
+
x402Cmd
|
|
738
|
+
.command("list")
|
|
739
|
+
.description("List your deployed x402 endpoints")
|
|
740
|
+
.action(x402ListCommand);
|
|
741
|
+
x402Cmd
|
|
742
|
+
.command("pause <name>")
|
|
743
|
+
.description("Pause a deployed endpoint")
|
|
744
|
+
.action(async (name) => x402PauseResumeCommand(name, "pause"));
|
|
745
|
+
x402Cmd
|
|
746
|
+
.command("resume <name>")
|
|
747
|
+
.description("Resume a paused endpoint")
|
|
748
|
+
.action(async (name) => x402PauseResumeCommand(name, "resume"));
|
|
749
|
+
x402Cmd
|
|
750
|
+
.command("delete <name>")
|
|
751
|
+
.description("Delete a deployed endpoint")
|
|
752
|
+
.action(x402DeleteCommand);
|
|
753
|
+
x402Cmd
|
|
754
|
+
.command("revenue [name]")
|
|
755
|
+
.description("View endpoint earnings breakdown")
|
|
756
|
+
.action(x402RevenueCommand);
|
|
757
|
+
const x402EnvCmd = x402Cmd
|
|
758
|
+
.command("env")
|
|
759
|
+
.description("Manage encrypted environment variables");
|
|
760
|
+
x402EnvCmd
|
|
761
|
+
.command("set <keyValue>")
|
|
762
|
+
.description("Set an env var (KEY=VALUE)")
|
|
763
|
+
.action(x402EnvSetCommand);
|
|
764
|
+
x402EnvCmd
|
|
765
|
+
.command("list")
|
|
766
|
+
.description("List env var names")
|
|
767
|
+
.action(x402EnvListCommand);
|
|
768
|
+
x402EnvCmd
|
|
769
|
+
.command("unset <key>")
|
|
770
|
+
.description("Remove an env var")
|
|
771
|
+
.action(x402EnvUnsetCommand);
|
|
772
|
+
x402Cmd
|
|
773
|
+
.command("search [query...]")
|
|
774
|
+
.description("Search the x402 service marketplace (no auth required)")
|
|
775
|
+
.option("--raw", "Output raw JSON (unformatted)")
|
|
776
|
+
.action(async (query, opts) => x402SearchCommand(query, { raw: opts.raw }));
|
|
715
777
|
// Default: treat unrecognized arguments as a prompt
|
|
716
778
|
program
|
|
717
779
|
.arguments("[text...]")
|
|
@@ -737,6 +799,7 @@ async function main() {
|
|
|
737
799
|
output.error(err.message);
|
|
738
800
|
process.exit(1);
|
|
739
801
|
}
|
|
802
|
+
await checkForUpdate(pkg.version);
|
|
740
803
|
}
|
|
741
804
|
main();
|
|
742
805
|
//# sourceMappingURL=cli.js.map
|
package/dist/commands/llm.js
CHANGED
|
@@ -79,15 +79,6 @@ const GATEWAY_MODELS = [
|
|
|
79
79
|
input: IMAGE_INPUT,
|
|
80
80
|
cost: { input: 0.25, output: 1.5, cacheRead: 0.025, cacheWrite: 0.08333 },
|
|
81
81
|
},
|
|
82
|
-
{
|
|
83
|
-
id: "gemini-3-pro",
|
|
84
|
-
name: "Gemini 3 Pro",
|
|
85
|
-
owned_by: "google",
|
|
86
|
-
contextWindow: 1048576,
|
|
87
|
-
maxTokens: 65536,
|
|
88
|
-
input: IMAGE_INPUT,
|
|
89
|
-
cost: { input: 2.0, output: 12.0, cacheRead: 0.2, cacheWrite: 0.375 },
|
|
90
|
-
},
|
|
91
82
|
{
|
|
92
83
|
id: "gemini-3-flash",
|
|
93
84
|
name: "Gemini 3 Flash",
|
|
@@ -238,20 +229,29 @@ const GATEWAY_MODELS = [
|
|
|
238
229
|
id: "minimax-m2.5",
|
|
239
230
|
name: "MiniMax M2.5",
|
|
240
231
|
owned_by: "minimax",
|
|
241
|
-
contextWindow:
|
|
232
|
+
contextWindow: 204800,
|
|
242
233
|
maxTokens: 196608,
|
|
243
234
|
input: TEXT_INPUT,
|
|
244
|
-
cost: { input: 0.
|
|
235
|
+
cost: { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0 },
|
|
245
236
|
},
|
|
246
237
|
{
|
|
247
238
|
id: "minimax-m2.7",
|
|
248
239
|
name: "MiniMax M2.7",
|
|
249
240
|
owned_by: "minimax",
|
|
250
241
|
contextWindow: 204800,
|
|
251
|
-
maxTokens:
|
|
242
|
+
maxTokens: 196608,
|
|
252
243
|
input: TEXT_INPUT,
|
|
253
244
|
cost: { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
|
|
254
245
|
},
|
|
246
|
+
{
|
|
247
|
+
id: "minimax-m2.7-highspeed",
|
|
248
|
+
name: "MiniMax M2.7 Highspeed",
|
|
249
|
+
owned_by: "minimax",
|
|
250
|
+
contextWindow: 204800,
|
|
251
|
+
maxTokens: 196608,
|
|
252
|
+
input: TEXT_INPUT,
|
|
253
|
+
cost: { input: 0.6, output: 2.4, cacheRead: 0.06, cacheWrite: 0 },
|
|
254
|
+
},
|
|
255
255
|
{
|
|
256
256
|
id: "glm-5",
|
|
257
257
|
name: "GLM-5",
|
|
@@ -261,6 +261,15 @@ const GATEWAY_MODELS = [
|
|
|
261
261
|
input: TEXT_INPUT,
|
|
262
262
|
cost: { input: 0.72, output: 2.3, cacheRead: 0, cacheWrite: 0 },
|
|
263
263
|
},
|
|
264
|
+
{
|
|
265
|
+
id: "glm-5-turbo",
|
|
266
|
+
name: "GLM-5 Turbo",
|
|
267
|
+
owned_by: "z-ai",
|
|
268
|
+
contextWindow: 202752,
|
|
269
|
+
maxTokens: 131072,
|
|
270
|
+
input: TEXT_INPUT,
|
|
271
|
+
cost: { input: 1.2, output: 4.0, cacheRead: 0.24, cacheWrite: 0 },
|
|
272
|
+
},
|
|
264
273
|
];
|
|
265
274
|
/** Fetch live model list from the gateway; falls back to hardcoded catalog. */
|
|
266
275
|
async function resolveModels() {
|
|
@@ -715,11 +724,12 @@ export async function setupOpenclawCommand(opts) {
|
|
|
715
724
|
const llmKey = getLlmKey();
|
|
716
725
|
const llmUrl = getLlmUrl();
|
|
717
726
|
const { models } = await resolveModels();
|
|
727
|
+
const activeModels = models.filter((m) => !m.deprecation);
|
|
718
728
|
const providerConfig = {
|
|
719
729
|
baseUrl: llmUrl,
|
|
720
730
|
apiKey: llmKey ?? "${BANKR_API_KEY}",
|
|
721
731
|
api: "openai-completions",
|
|
722
|
-
models:
|
|
732
|
+
models: activeModels.map((m) => ({
|
|
723
733
|
id: m.id,
|
|
724
734
|
name: m.name,
|
|
725
735
|
...(m.owned_by === "anthropic" ? { api: "anthropic-messages" } : {}),
|
|
@@ -753,8 +763,9 @@ export async function setupOpenCodeCommand(opts) {
|
|
|
753
763
|
const llmKey = getLlmKey();
|
|
754
764
|
const llmUrl = getLlmUrl();
|
|
755
765
|
const { models } = await resolveModels();
|
|
766
|
+
const activeModels = models.filter((m) => !m.deprecation);
|
|
756
767
|
const modelsObj = {};
|
|
757
|
-
for (const m of
|
|
768
|
+
for (const m of activeModels) {
|
|
758
769
|
modelsObj[m.id] = { name: m.name };
|
|
759
770
|
}
|
|
760
771
|
const bankrProvider = {
|
|
@@ -796,9 +807,10 @@ export async function setupCursorCommand() {
|
|
|
796
807
|
const llmUrl = getLlmUrl();
|
|
797
808
|
const token = llmKey ?? "<your-bankr-api-key>";
|
|
798
809
|
const { models } = await resolveModels();
|
|
810
|
+
const activeModels = models.filter((m) => !m.deprecation);
|
|
799
811
|
// Pick one model per provider as recommended examples
|
|
800
|
-
const recommendedIds = ["claude-opus-4.6", "gemini-3-pro", "gpt-5.2"];
|
|
801
|
-
const recommended = recommendedIds.filter((id) =>
|
|
812
|
+
const recommendedIds = ["claude-opus-4.6", "gemini-3.1-pro", "gpt-5.2"];
|
|
813
|
+
const recommended = recommendedIds.filter((id) => activeModels.some((m) => m.id === id));
|
|
802
814
|
output.brandBold("Cursor — Bankr LLM Gateway");
|
|
803
815
|
console.log();
|
|
804
816
|
output.info("In Cursor, go to Settings > Models and configure:");
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI commands for x402 endpoint hosting.
|
|
3
|
+
*
|
|
4
|
+
* bankr x402 init — Scaffold x402/ folder + bankr.x402.json
|
|
5
|
+
* bankr x402 add <name> — Add a new service
|
|
6
|
+
* bankr x402 configure <name> — Interactive pricing/description setup
|
|
7
|
+
* bankr x402 deploy [name] — Deploy all or a single service
|
|
8
|
+
* bankr x402 list — List deployed services
|
|
9
|
+
* bankr x402 logs <name> — View request logs (future)
|
|
10
|
+
* bankr x402 pause <name> — Pause a service
|
|
11
|
+
* bankr x402 resume <name> — Resume a service
|
|
12
|
+
* bankr x402 delete <name> — Delete a service
|
|
13
|
+
* bankr x402 revenue [name] — View earnings
|
|
14
|
+
* bankr x402 env set KEY=VALUE — Set encrypted env var
|
|
15
|
+
* bankr x402 env list — List env var names
|
|
16
|
+
* bankr x402 env unset KEY — Remove env var
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* bankr x402 init — Scaffold x402/ folder + bankr.x402.json
|
|
20
|
+
*/
|
|
21
|
+
export declare function x402InitCommand(): Promise<void>;
|
|
22
|
+
export declare function x402AddCommand(name: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* bankr x402 configure <name> — Interactive pricing/description setup
|
|
25
|
+
*/
|
|
26
|
+
export declare function x402ConfigureCommand(name: string): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* bankr x402 deploy [name] — Deploy services
|
|
29
|
+
*
|
|
30
|
+
* This bundles the handler(s) with Bun and uploads via the Bankr API.
|
|
31
|
+
* If no name is provided, deploys all services in x402/.
|
|
32
|
+
*/
|
|
33
|
+
export declare function x402DeployCommand(name?: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* bankr x402 list — List deployed services
|
|
36
|
+
*/
|
|
37
|
+
export declare function x402ListCommand(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* bankr x402 pause/resume <name>
|
|
40
|
+
*/
|
|
41
|
+
export declare function x402PauseResumeCommand(name: string, action: "pause" | "resume"): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* bankr x402 delete <name>
|
|
44
|
+
*/
|
|
45
|
+
export declare function x402DeleteCommand(name: string): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* bankr x402 revenue [name]
|
|
48
|
+
*/
|
|
49
|
+
export declare function x402RevenueCommand(name?: string): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* bankr x402 env set KEY=VALUE
|
|
52
|
+
*/
|
|
53
|
+
export declare function x402EnvSetCommand(keyValue: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* bankr x402 env list
|
|
56
|
+
*/
|
|
57
|
+
export declare function x402EnvListCommand(): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* bankr x402 env unset KEY
|
|
60
|
+
*/
|
|
61
|
+
export declare function x402EnvUnsetCommand(key: string): Promise<void>;
|
|
62
|
+
export declare function x402SearchCommand(queryParts: string[], opts?: {
|
|
63
|
+
raw?: boolean;
|
|
64
|
+
}): Promise<void>;
|
|
65
|
+
//# sourceMappingURL=x402.d.ts.map
|
|
@@ -0,0 +1,761 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI commands for x402 endpoint hosting.
|
|
3
|
+
*
|
|
4
|
+
* bankr x402 init — Scaffold x402/ folder + bankr.x402.json
|
|
5
|
+
* bankr x402 add <name> — Add a new service
|
|
6
|
+
* bankr x402 configure <name> — Interactive pricing/description setup
|
|
7
|
+
* bankr x402 deploy [name] — Deploy all or a single service
|
|
8
|
+
* bankr x402 list — List deployed services
|
|
9
|
+
* bankr x402 logs <name> — View request logs (future)
|
|
10
|
+
* bankr x402 pause <name> — Pause a service
|
|
11
|
+
* bankr x402 resume <name> — Resume a service
|
|
12
|
+
* bankr x402 delete <name> — Delete a service
|
|
13
|
+
* bankr x402 revenue [name] — View earnings
|
|
14
|
+
* bankr x402 env set KEY=VALUE — Set encrypted env var
|
|
15
|
+
* bankr x402 env list — List env var names
|
|
16
|
+
* bankr x402 env unset KEY — Remove env var
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import chalk from "chalk";
|
|
21
|
+
import { input, select, confirm } from "@inquirer/prompts";
|
|
22
|
+
import * as output from "../lib/output.js";
|
|
23
|
+
import { getApiUrl, requireApiKey, readConfig, CLI_USER_AGENT, } from "../lib/config.js";
|
|
24
|
+
// ── Config file helpers ─────────────────────────────────────────────────
|
|
25
|
+
const CONFIG_FILENAME = "bankr.x402.json";
|
|
26
|
+
function findProjectRoot() {
|
|
27
|
+
return process.cwd();
|
|
28
|
+
}
|
|
29
|
+
function loadConfigFile(projectRoot) {
|
|
30
|
+
const candidates = [
|
|
31
|
+
join(projectRoot, CONFIG_FILENAME),
|
|
32
|
+
join(projectRoot, "x402", CONFIG_FILENAME),
|
|
33
|
+
];
|
|
34
|
+
for (const p of candidates) {
|
|
35
|
+
if (existsSync(p)) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
output.error(`Failed to parse ${p}. Check that it is valid JSON.`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function saveConfigFile(projectRoot, config) {
|
|
48
|
+
const configPath = join(projectRoot, CONFIG_FILENAME);
|
|
49
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
50
|
+
}
|
|
51
|
+
// ── API helpers ─────────────────────────────────────────────────────────
|
|
52
|
+
function authHeaders() {
|
|
53
|
+
const headers = {
|
|
54
|
+
"X-API-Key": requireApiKey(),
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
"User-Agent": CLI_USER_AGENT,
|
|
57
|
+
};
|
|
58
|
+
const config = readConfig();
|
|
59
|
+
if (config.partnerKey) {
|
|
60
|
+
headers["X-Partner-Key"] = config.partnerKey;
|
|
61
|
+
}
|
|
62
|
+
return headers;
|
|
63
|
+
}
|
|
64
|
+
async function handleResponse(res) {
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
const body = (await res
|
|
67
|
+
.json()
|
|
68
|
+
.catch(() => ({ message: res.statusText })));
|
|
69
|
+
const msg = body.message || body.error || res.statusText;
|
|
70
|
+
throw new Error(`API error (${res.status}): ${msg}`);
|
|
71
|
+
}
|
|
72
|
+
return res.json();
|
|
73
|
+
}
|
|
74
|
+
// ── Default handler template ────────────────────────────────────────────
|
|
75
|
+
const HANDLER_TEMPLATE = `/**
|
|
76
|
+
* x402 service handler.
|
|
77
|
+
*
|
|
78
|
+
* Receives a standard Web Request, returns a standard Web Response.
|
|
79
|
+
* Bankr wraps the x402 payment layer around this — you just write the logic.
|
|
80
|
+
*
|
|
81
|
+
* Environment variables are available via process.env.
|
|
82
|
+
*/
|
|
83
|
+
export default async function handler(req: Request): Promise<Response> {
|
|
84
|
+
const url = new URL(req.url);
|
|
85
|
+
|
|
86
|
+
return Response.json({
|
|
87
|
+
message: "Hello from SERVICE_NAME!",
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
// ── Commands ────────────────────────────────────────────────────────────
|
|
93
|
+
/**
|
|
94
|
+
* bankr x402 init — Scaffold x402/ folder + bankr.x402.json
|
|
95
|
+
*/
|
|
96
|
+
export async function x402InitCommand() {
|
|
97
|
+
const root = findProjectRoot();
|
|
98
|
+
const x402Dir = join(root, "x402");
|
|
99
|
+
if (existsSync(x402Dir)) {
|
|
100
|
+
output.warn("x402/ directory already exists");
|
|
101
|
+
const overwrite = await confirm({
|
|
102
|
+
message: "Re-initialize config?",
|
|
103
|
+
default: false,
|
|
104
|
+
theme: output.bankrTheme,
|
|
105
|
+
});
|
|
106
|
+
if (!overwrite)
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
mkdirSync(x402Dir, { recursive: true });
|
|
111
|
+
output.success(`Created x402/ directory`);
|
|
112
|
+
}
|
|
113
|
+
const existing = loadConfigFile(root);
|
|
114
|
+
if (!existing) {
|
|
115
|
+
const config = {
|
|
116
|
+
network: "base",
|
|
117
|
+
currency: "USDC",
|
|
118
|
+
services: {},
|
|
119
|
+
};
|
|
120
|
+
saveConfigFile(root, config);
|
|
121
|
+
output.success(`Created ${CONFIG_FILENAME}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
output.info(`${CONFIG_FILENAME} already exists, keeping it`);
|
|
125
|
+
}
|
|
126
|
+
output.info("Next: run 'bankr x402 add <name>' to create your first service");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* bankr x402 add <name> — Add a new service with handler scaffold
|
|
130
|
+
*/
|
|
131
|
+
const MAX_SERVICE_NAME_LENGTH = 47; // bx4- (4) + hash (12) + - (1) = 17 overhead; 64 - 17 = 47
|
|
132
|
+
const SERVICE_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
133
|
+
function validateServiceName(name) {
|
|
134
|
+
if (!SERVICE_NAME_PATTERN.test(name)) {
|
|
135
|
+
return `Invalid service name "${name}". Use only letters, numbers, hyphens, and underscores.`;
|
|
136
|
+
}
|
|
137
|
+
if (name.length > MAX_SERVICE_NAME_LENGTH) {
|
|
138
|
+
return `Service name "${name}" is too long (${name.length} chars). Maximum is ${MAX_SERVICE_NAME_LENGTH} characters.`;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
export async function x402AddCommand(name) {
|
|
143
|
+
const nameError = validateServiceName(name);
|
|
144
|
+
if (nameError) {
|
|
145
|
+
output.error(nameError);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const root = findProjectRoot();
|
|
149
|
+
const x402Dir = join(root, "x402");
|
|
150
|
+
if (!existsSync(x402Dir)) {
|
|
151
|
+
output.error("No x402/ directory found. Run 'bankr x402 init' first.");
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
const serviceDir = join(x402Dir, name);
|
|
155
|
+
if (existsSync(serviceDir)) {
|
|
156
|
+
output.error(`Service "${name}" already exists at x402/${name}/`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
// Ask for method
|
|
160
|
+
const method = await select({
|
|
161
|
+
message: "HTTP method:",
|
|
162
|
+
choices: [
|
|
163
|
+
{ name: "GET — query parameters in URL", value: "GET" },
|
|
164
|
+
{ name: "POST — JSON body", value: "POST" },
|
|
165
|
+
{ name: "Any — accept all methods", value: "*" },
|
|
166
|
+
],
|
|
167
|
+
default: "GET",
|
|
168
|
+
theme: output.bankrTheme,
|
|
169
|
+
});
|
|
170
|
+
// Create service directory and handler
|
|
171
|
+
mkdirSync(serviceDir, { recursive: true });
|
|
172
|
+
const isPost = method === "POST";
|
|
173
|
+
const handlerContent = isPost
|
|
174
|
+
? `/**
|
|
175
|
+
* ${name} — x402 service handler (POST with JSON body).
|
|
176
|
+
*/
|
|
177
|
+
export default async function handler(req: Request): Promise<Response> {
|
|
178
|
+
if (req.method !== "POST") {
|
|
179
|
+
return Response.json({ error: "POST required" }, { status: 405 });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const body = await req.json();
|
|
183
|
+
// TODO: validate body fields
|
|
184
|
+
|
|
185
|
+
return Response.json({
|
|
186
|
+
message: "Hello from ${name}!",
|
|
187
|
+
received: body,
|
|
188
|
+
timestamp: new Date().toISOString(),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
`
|
|
192
|
+
: `/**
|
|
193
|
+
* ${name} — x402 service handler.
|
|
194
|
+
*/
|
|
195
|
+
export default async function handler(req: Request): Promise<Response> {
|
|
196
|
+
const url = new URL(req.url);
|
|
197
|
+
// const myParam = url.searchParams.get("myParam") ?? "default";
|
|
198
|
+
|
|
199
|
+
return Response.json({
|
|
200
|
+
message: "Hello from ${name}!",
|
|
201
|
+
timestamp: new Date().toISOString(),
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
`;
|
|
205
|
+
writeFileSync(join(serviceDir, "index.ts"), handlerContent);
|
|
206
|
+
// Build JSON Schema scaffold
|
|
207
|
+
const schema = {
|
|
208
|
+
input: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
[isPost ? "field" : "param"]: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: isPost ? "Example body field" : "Example query parameter",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
required: [isPost ? "field" : "param"],
|
|
217
|
+
},
|
|
218
|
+
output: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
result: { type: "string", description: "Result value" },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
// Add to config
|
|
226
|
+
let config = loadConfigFile(root);
|
|
227
|
+
if (!config) {
|
|
228
|
+
config = { services: {} };
|
|
229
|
+
}
|
|
230
|
+
config.services[name] = {
|
|
231
|
+
price: "0.001",
|
|
232
|
+
description: `${name} service`,
|
|
233
|
+
methods: method === "*" ? undefined : [method],
|
|
234
|
+
schema,
|
|
235
|
+
};
|
|
236
|
+
saveConfigFile(root, config);
|
|
237
|
+
output.success(`Created x402/${name}/index.ts`);
|
|
238
|
+
output.info(`Default price: $0.001 USDC. Run 'bankr x402 configure ${name}' to customize.`);
|
|
239
|
+
output.info(`Edit the schema in bankr.x402.json to describe your ${isPost ? "body fields" : "query parameters"} for agent discovery.`);
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* bankr x402 configure <name> — Interactive pricing/description setup
|
|
243
|
+
*/
|
|
244
|
+
export async function x402ConfigureCommand(name) {
|
|
245
|
+
const root = findProjectRoot();
|
|
246
|
+
let config = loadConfigFile(root);
|
|
247
|
+
if (!config) {
|
|
248
|
+
config = { services: {} };
|
|
249
|
+
}
|
|
250
|
+
const existing = config.services[name] ?? { price: "0.001" };
|
|
251
|
+
const description = await input({
|
|
252
|
+
message: "Service description:",
|
|
253
|
+
default: existing.description ?? `${name} service`,
|
|
254
|
+
theme: output.bankrTheme,
|
|
255
|
+
});
|
|
256
|
+
const price = await input({
|
|
257
|
+
message: "Price per request (USD):",
|
|
258
|
+
default: existing.price ?? "0.001",
|
|
259
|
+
theme: output.bankrTheme,
|
|
260
|
+
});
|
|
261
|
+
const currency = "USDC";
|
|
262
|
+
const network = await select({
|
|
263
|
+
message: "Network:",
|
|
264
|
+
choices: [
|
|
265
|
+
{ name: "Base", value: "base" },
|
|
266
|
+
{ name: "Base Sepolia (testnet)", value: "base-sepolia" },
|
|
267
|
+
],
|
|
268
|
+
default: existing.network ?? "base",
|
|
269
|
+
theme: output.bankrTheme,
|
|
270
|
+
});
|
|
271
|
+
const method = await select({
|
|
272
|
+
message: "HTTP method:",
|
|
273
|
+
choices: [
|
|
274
|
+
{ name: "GET — query parameters in URL", value: "GET" },
|
|
275
|
+
{ name: "POST — JSON body", value: "POST" },
|
|
276
|
+
{ name: "Any — accept all methods", value: "*" },
|
|
277
|
+
],
|
|
278
|
+
default: existing.methods?.[0] ?? "GET",
|
|
279
|
+
theme: output.bankrTheme,
|
|
280
|
+
});
|
|
281
|
+
config.services[name] = {
|
|
282
|
+
...existing,
|
|
283
|
+
description,
|
|
284
|
+
price,
|
|
285
|
+
currency,
|
|
286
|
+
network,
|
|
287
|
+
methods: method === "*" ? undefined : [method],
|
|
288
|
+
};
|
|
289
|
+
saveConfigFile(root, config);
|
|
290
|
+
output.success(`Updated config for "${name}"`);
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* bankr x402 deploy [name] — Deploy services
|
|
294
|
+
*
|
|
295
|
+
* This bundles the handler(s) with Bun and uploads via the Bankr API.
|
|
296
|
+
* If no name is provided, deploys all services in x402/.
|
|
297
|
+
*/
|
|
298
|
+
export async function x402DeployCommand(name) {
|
|
299
|
+
const root = findProjectRoot();
|
|
300
|
+
const x402Dir = join(root, "x402");
|
|
301
|
+
if (!existsSync(x402Dir)) {
|
|
302
|
+
output.error("No x402/ directory found. Run 'bankr x402 init' first.");
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
// Load or bootstrap config
|
|
306
|
+
let config = loadConfigFile(root);
|
|
307
|
+
if (!config) {
|
|
308
|
+
config = { services: {} };
|
|
309
|
+
}
|
|
310
|
+
// Discover services
|
|
311
|
+
const { readdirSync, statSync } = await import("node:fs");
|
|
312
|
+
const entries = readdirSync(x402Dir);
|
|
313
|
+
const services = [];
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
const entryPath = join(x402Dir, entry);
|
|
316
|
+
if (statSync(entryPath).isDirectory() &&
|
|
317
|
+
existsSync(join(entryPath, "index.ts"))) {
|
|
318
|
+
services.push(entry);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (services.length === 0) {
|
|
322
|
+
output.error("No services found. Run 'bankr x402 add <name>' first.");
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
// Filter to single service if name provided
|
|
326
|
+
const toDeploy = name ? services.filter((s) => s === name) : services;
|
|
327
|
+
if (name && toDeploy.length === 0) {
|
|
328
|
+
output.error(`Service "${name}" not found in x402/`);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
// Validate service names and ensure config entries
|
|
332
|
+
for (const svc of toDeploy) {
|
|
333
|
+
const nameError = validateServiceName(svc);
|
|
334
|
+
if (nameError) {
|
|
335
|
+
output.error(nameError);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
if (!config.services[svc]) {
|
|
339
|
+
config.services[svc] = {
|
|
340
|
+
price: "0.001",
|
|
341
|
+
description: `${svc} service`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const spin = output.spinner(`Deploying ${toDeploy.length} service(s)...`);
|
|
346
|
+
try {
|
|
347
|
+
// Read raw TypeScript source for each service.
|
|
348
|
+
// Bundling + security wrapping happens server-side in the builder Lambda.
|
|
349
|
+
const bundles = [];
|
|
350
|
+
for (const svc of toDeploy) {
|
|
351
|
+
const entrypoint = join(x402Dir, svc, "index.ts");
|
|
352
|
+
if (!existsSync(entrypoint)) {
|
|
353
|
+
spin.fail(`Missing handler: x402/${svc}/index.ts`);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
}
|
|
356
|
+
const source = readFileSync(entrypoint, "utf-8");
|
|
357
|
+
bundles.push({ name: svc, source });
|
|
358
|
+
}
|
|
359
|
+
// Deploy via API (server-side build + deploy)
|
|
360
|
+
const res = await fetch(`${getApiUrl()}/x402/endpoints/deploy`, {
|
|
361
|
+
method: "POST",
|
|
362
|
+
headers: authHeaders(),
|
|
363
|
+
body: JSON.stringify({
|
|
364
|
+
config,
|
|
365
|
+
bundles: bundles.map((b) => ({
|
|
366
|
+
name: b.name,
|
|
367
|
+
source: b.source,
|
|
368
|
+
})),
|
|
369
|
+
}),
|
|
370
|
+
});
|
|
371
|
+
const result = await handleResponse(res);
|
|
372
|
+
spin.succeed(`Deployed ${result.deployments.length} service(s)`);
|
|
373
|
+
// Print results
|
|
374
|
+
console.log();
|
|
375
|
+
for (const dep of result.deployments) {
|
|
376
|
+
const svcConfig = config.services[dep.name];
|
|
377
|
+
output.label(" Service", dep.name);
|
|
378
|
+
output.label(" URL", dep.url);
|
|
379
|
+
output.label(" Price", `$${svcConfig?.price ?? "0.001"} ${svcConfig?.currency ?? "USDC"}/req`);
|
|
380
|
+
output.label(" Version", String(dep.version));
|
|
381
|
+
console.log();
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
spin.fail("Deploy failed");
|
|
386
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
387
|
+
process.exit(1);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* bankr x402 list — List deployed services
|
|
392
|
+
*/
|
|
393
|
+
export async function x402ListCommand() {
|
|
394
|
+
const spin = output.spinner("Fetching endpoints...");
|
|
395
|
+
try {
|
|
396
|
+
const res = await fetch(`${getApiUrl()}/x402/endpoints`, {
|
|
397
|
+
headers: authHeaders(),
|
|
398
|
+
});
|
|
399
|
+
const result = await handleResponse(res);
|
|
400
|
+
spin.stop();
|
|
401
|
+
if (result.endpoints.length === 0) {
|
|
402
|
+
output.info("No deployed endpoints. Run 'bankr x402 deploy' to get started.");
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
for (const ep of result.endpoints) {
|
|
406
|
+
const route = ep.routes[0];
|
|
407
|
+
const status = ep.status === "active"
|
|
408
|
+
? output.fmt.success("active")
|
|
409
|
+
: output.fmt.warn(ep.status);
|
|
410
|
+
console.log(` ${output.fmt.brand(ep.name)} ${status} v${ep.version} $${route?.price ?? "?"} ${route?.currency ?? "USDC"} ${ep.totalRequests} reqs $${ep.totalRevenueUsd} earned`);
|
|
411
|
+
output.dim(` ${ep.description}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
spin.fail("Failed to list endpoints");
|
|
416
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
417
|
+
process.exit(1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* bankr x402 pause/resume <name>
|
|
422
|
+
*/
|
|
423
|
+
export async function x402PauseResumeCommand(name, action) {
|
|
424
|
+
const status = action === "pause" ? "paused" : "active";
|
|
425
|
+
const spin = output.spinner(`${action === "pause" ? "Pausing" : "Resuming"} ${name}...`);
|
|
426
|
+
try {
|
|
427
|
+
const res = await fetch(`${getApiUrl()}/x402/endpoints/${name}`, {
|
|
428
|
+
method: "PATCH",
|
|
429
|
+
headers: authHeaders(),
|
|
430
|
+
body: JSON.stringify({ status }),
|
|
431
|
+
});
|
|
432
|
+
await handleResponse(res);
|
|
433
|
+
spin.succeed(`${name} ${action === "pause" ? "paused" : "resumed"}`);
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
spin.fail(`Failed to ${action} ${name}`);
|
|
437
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* bankr x402 delete <name>
|
|
443
|
+
*/
|
|
444
|
+
export async function x402DeleteCommand(name) {
|
|
445
|
+
const confirmed = await confirm({
|
|
446
|
+
message: `Delete endpoint "${name}"? This cannot be undone.`,
|
|
447
|
+
default: false,
|
|
448
|
+
theme: output.bankrTheme,
|
|
449
|
+
});
|
|
450
|
+
if (!confirmed)
|
|
451
|
+
return;
|
|
452
|
+
const spin = output.spinner(`Deleting ${name}...`);
|
|
453
|
+
try {
|
|
454
|
+
const res = await fetch(`${getApiUrl()}/x402/endpoints/${name}`, {
|
|
455
|
+
method: "DELETE",
|
|
456
|
+
headers: authHeaders(),
|
|
457
|
+
});
|
|
458
|
+
await handleResponse(res);
|
|
459
|
+
spin.succeed(`${name} deleted`);
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
spin.fail(`Failed to delete ${name}`);
|
|
463
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
464
|
+
process.exit(1);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* bankr x402 revenue [name]
|
|
469
|
+
*/
|
|
470
|
+
export async function x402RevenueCommand(name) {
|
|
471
|
+
if (!name) {
|
|
472
|
+
// Show all endpoints revenue summary
|
|
473
|
+
await x402ListCommand();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const spin = output.spinner(`Fetching revenue for ${name}...`);
|
|
477
|
+
try {
|
|
478
|
+
const res = await fetch(`${getApiUrl()}/x402/endpoints/revenue/${name}`, {
|
|
479
|
+
headers: authHeaders(),
|
|
480
|
+
});
|
|
481
|
+
const result = await handleResponse(res);
|
|
482
|
+
spin.stop();
|
|
483
|
+
console.log(`\n Revenue for ${output.fmt.brand(name)}\n`);
|
|
484
|
+
for (const [period, data] of Object.entries(result.revenue)) {
|
|
485
|
+
const label = period === "last7d"
|
|
486
|
+
? "Last 7 days"
|
|
487
|
+
: period === "last30d"
|
|
488
|
+
? "Last 30 days"
|
|
489
|
+
: "All time";
|
|
490
|
+
const earned = (data.totalUsd - data.bankrFeesUsd).toFixed(6);
|
|
491
|
+
console.log(` ${label.padEnd(14)} ${data.requests} reqs $${earned} earned $${data.bankrFeesUsd.toFixed(6)} fees`);
|
|
492
|
+
}
|
|
493
|
+
console.log();
|
|
494
|
+
}
|
|
495
|
+
catch (err) {
|
|
496
|
+
spin.fail("Failed to fetch revenue");
|
|
497
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* bankr x402 env set KEY=VALUE
|
|
503
|
+
*/
|
|
504
|
+
export async function x402EnvSetCommand(keyValue) {
|
|
505
|
+
const eqIdx = keyValue.indexOf("=");
|
|
506
|
+
if (eqIdx === -1) {
|
|
507
|
+
output.error("Usage: bankr x402 env set KEY=VALUE");
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
const key = keyValue.slice(0, eqIdx);
|
|
511
|
+
const value = keyValue.slice(eqIdx + 1);
|
|
512
|
+
if (!key) {
|
|
513
|
+
output.error("Key cannot be empty");
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
const spin = output.spinner(`Setting ${key}...`);
|
|
517
|
+
try {
|
|
518
|
+
const res = await fetch(`${getApiUrl()}/x402/endpoints/env`, {
|
|
519
|
+
method: "POST",
|
|
520
|
+
headers: authHeaders(),
|
|
521
|
+
body: JSON.stringify({ vars: { [key]: value } }),
|
|
522
|
+
});
|
|
523
|
+
await handleResponse(res);
|
|
524
|
+
spin.succeed(`Set ${key}`);
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
spin.fail(`Failed to set ${key}`);
|
|
528
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* bankr x402 env list
|
|
534
|
+
*/
|
|
535
|
+
export async function x402EnvListCommand() {
|
|
536
|
+
const spin = output.spinner("Fetching env vars...");
|
|
537
|
+
try {
|
|
538
|
+
const res = await fetch(`${getApiUrl()}/x402/endpoints/env`, {
|
|
539
|
+
headers: authHeaders(),
|
|
540
|
+
});
|
|
541
|
+
const result = await handleResponse(res);
|
|
542
|
+
spin.stop();
|
|
543
|
+
if (result.vars.length === 0) {
|
|
544
|
+
output.info("No environment variables set.");
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
for (const name of result.vars) {
|
|
548
|
+
console.log(` ${name}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch (err) {
|
|
552
|
+
spin.fail("Failed to list env vars");
|
|
553
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* bankr x402 env unset KEY
|
|
559
|
+
*/
|
|
560
|
+
export async function x402EnvUnsetCommand(key) {
|
|
561
|
+
const spin = output.spinner(`Removing ${key}...`);
|
|
562
|
+
try {
|
|
563
|
+
const res = await fetch(`${getApiUrl()}/x402/endpoints/env/${key}`, {
|
|
564
|
+
method: "DELETE",
|
|
565
|
+
headers: authHeaders(),
|
|
566
|
+
});
|
|
567
|
+
await handleResponse(res);
|
|
568
|
+
spin.succeed(`Removed ${key}`);
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
spin.fail(`Failed to remove ${key}`);
|
|
572
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
573
|
+
process.exit(1);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// ── Schema display helpers ──────────────────────────────────────────────
|
|
577
|
+
const P = " │";
|
|
578
|
+
/**
|
|
579
|
+
* Detects whether a schema object is JSON Schema format (has `type` and `properties`)
|
|
580
|
+
* vs the legacy flat Record<string,string> format.
|
|
581
|
+
*/
|
|
582
|
+
function isJsonSchema(obj) {
|
|
583
|
+
return (typeof obj === "object" &&
|
|
584
|
+
obj !== null &&
|
|
585
|
+
"type" in obj &&
|
|
586
|
+
obj.type === "object" &&
|
|
587
|
+
"properties" in obj);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Renders a JSON Schema properties table.
|
|
591
|
+
* Output: lines like " │ symbol* string Token symbol"
|
|
592
|
+
*/
|
|
593
|
+
function renderJsonSchemaProps(schema) {
|
|
594
|
+
if (!schema.properties)
|
|
595
|
+
return [];
|
|
596
|
+
const required = new Set(schema.required ?? []);
|
|
597
|
+
const entries = Object.entries(schema.properties);
|
|
598
|
+
// Calculate column widths
|
|
599
|
+
let maxName = 0;
|
|
600
|
+
let maxType = 0;
|
|
601
|
+
for (const [name, prop] of entries) {
|
|
602
|
+
const nameLen = name.length + (required.has(name) ? 1 : 0);
|
|
603
|
+
if (nameLen > maxName)
|
|
604
|
+
maxName = nameLen;
|
|
605
|
+
const typeStr = prop.type + (prop.enum ? ` (${prop.enum.join("|")})` : "");
|
|
606
|
+
if (typeStr.length > maxType)
|
|
607
|
+
maxType = typeStr.length;
|
|
608
|
+
}
|
|
609
|
+
const lines = [];
|
|
610
|
+
for (const [name, prop] of entries) {
|
|
611
|
+
const req = required.has(name) ? chalk.red("*") : " ";
|
|
612
|
+
const nameStr = `${chalk.cyan(name)}${req}`;
|
|
613
|
+
const namePad = " ".repeat(Math.max(0, maxName + 1 - name.length - (required.has(name) ? 1 : 0)));
|
|
614
|
+
const typeStr = prop.type + (prop.enum ? ` (${prop.enum.join("|")})` : "");
|
|
615
|
+
const typePad = " ".repeat(Math.max(0, maxType + 2 - typeStr.length));
|
|
616
|
+
const desc = prop.description ? chalk.dim(prop.description) : "";
|
|
617
|
+
lines.push(`${P} ${nameStr}${namePad}${chalk.yellow(typeStr)}${typePad}${desc}`);
|
|
618
|
+
}
|
|
619
|
+
return lines;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Renders a legacy flat Record<string,string> schema as a simple key: value list.
|
|
623
|
+
* Pretty-prints nested objects/arrays with proper indentation.
|
|
624
|
+
*/
|
|
625
|
+
function renderLegacySchema(obj) {
|
|
626
|
+
const lines = [];
|
|
627
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
628
|
+
if (typeof v === "object" && v !== null) {
|
|
629
|
+
const pretty = JSON.stringify(v, null, 2);
|
|
630
|
+
const indented = pretty
|
|
631
|
+
.split("\n")
|
|
632
|
+
.map((line, i) => (i === 0 ? line : `${P} ${" ".repeat(k.length)}${line}`))
|
|
633
|
+
.join("\n");
|
|
634
|
+
lines.push(`${P} ${chalk.cyan(k)} ${chalk.dim(indented)}`);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
lines.push(`${P} ${chalk.cyan(k)} ${chalk.dim(String(v))}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return lines;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Resolves input/output schemas from a route, handling both JSON Schema
|
|
644
|
+
* and legacy flat formats for backward compatibility.
|
|
645
|
+
*/
|
|
646
|
+
function resolveSchemas(schema) {
|
|
647
|
+
if (!schema)
|
|
648
|
+
return { input: undefined, output: undefined };
|
|
649
|
+
// Prefer new JSON Schema `input`/`output` fields
|
|
650
|
+
const inputSchema = schema.input ?? schema.queryParams ?? schema.body;
|
|
651
|
+
const outputSchema = schema.output;
|
|
652
|
+
return { input: inputSchema, output: outputSchema };
|
|
653
|
+
}
|
|
654
|
+
function printServiceFormatted(svc) {
|
|
655
|
+
const route = svc.routes[0];
|
|
656
|
+
const methods = route?.methods?.filter((m) => m !== "*").join(", ") || "ANY";
|
|
657
|
+
const url = svc.url ?? `https://x402.bankr.bot/${svc.slug}`;
|
|
658
|
+
const isGet = methods === "GET";
|
|
659
|
+
// ── Card header ──
|
|
660
|
+
console.log(` ${chalk.dim("┌")} ${output.fmt.brandBold(svc.name)}`);
|
|
661
|
+
if (svc.description) {
|
|
662
|
+
console.log(`${P} ${chalk.dim(svc.description)}`);
|
|
663
|
+
}
|
|
664
|
+
console.log(`${P}`);
|
|
665
|
+
// ── Metadata ──
|
|
666
|
+
console.log(`${P} ${chalk.dim("URL")} ${url}`);
|
|
667
|
+
console.log(`${P} ${chalk.dim("Method")} ${chalk.bold(methods)}`);
|
|
668
|
+
console.log(`${P} ${chalk.dim("Price")} ${chalk.green(`$${route?.price ?? "?"} ${route?.currency ?? "USDC"}`)}`);
|
|
669
|
+
console.log(`${P} ${chalk.dim("Network")} ${route?.network ?? "base"}`);
|
|
670
|
+
// ── Schema ──
|
|
671
|
+
const { input: inputSchema, output: outputSchema } = resolveSchemas(route?.schema);
|
|
672
|
+
if (inputSchema || outputSchema) {
|
|
673
|
+
console.log(`${P}`);
|
|
674
|
+
if (inputSchema) {
|
|
675
|
+
const label = isGet ? "Input (query params)" : "Input (JSON body)";
|
|
676
|
+
console.log(`${P} ${chalk.bold(label)}`);
|
|
677
|
+
if (isJsonSchema(inputSchema)) {
|
|
678
|
+
for (const line of renderJsonSchemaProps(inputSchema)) {
|
|
679
|
+
console.log(line);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
else if (typeof inputSchema === "object" && inputSchema !== null) {
|
|
683
|
+
for (const line of renderLegacySchema(inputSchema)) {
|
|
684
|
+
console.log(line);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (outputSchema) {
|
|
689
|
+
console.log(`${P}`);
|
|
690
|
+
console.log(`${P} ${chalk.bold("Output")}`);
|
|
691
|
+
if (isJsonSchema(outputSchema)) {
|
|
692
|
+
for (const line of renderJsonSchemaProps(outputSchema)) {
|
|
693
|
+
console.log(line);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
else if (typeof outputSchema === "object" && outputSchema !== null) {
|
|
697
|
+
for (const line of renderLegacySchema(outputSchema)) {
|
|
698
|
+
console.log(line);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// ── Example ──
|
|
704
|
+
if (inputSchema && isJsonSchema(inputSchema) && inputSchema.properties) {
|
|
705
|
+
console.log(`${P}`);
|
|
706
|
+
const props = Object.entries(inputSchema.properties);
|
|
707
|
+
if (isGet) {
|
|
708
|
+
const qs = props.map(([k, p]) => `${k}=<${p.type}>`).join("&");
|
|
709
|
+
console.log(`${P} ${chalk.dim("Example")} ${chalk.dim("GET")} ${url}?${qs}`);
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
const bodyObj = {};
|
|
713
|
+
for (const [k, p] of props)
|
|
714
|
+
bodyObj[k] = `<${p.type}>`;
|
|
715
|
+
console.log(`${P} ${chalk.dim("Example")} ${chalk.dim("POST")} ${url}`);
|
|
716
|
+
console.log(`${P} ${chalk.dim(JSON.stringify(bodyObj))}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// ── Tags ──
|
|
720
|
+
if (svc.tags?.length) {
|
|
721
|
+
console.log(`${P}`);
|
|
722
|
+
console.log(`${P} ${svc.tags.map((t) => chalk.dim(`#${t}`)).join(" ")}`);
|
|
723
|
+
}
|
|
724
|
+
console.log(` ${chalk.dim("└" + "─".repeat(60))}`);
|
|
725
|
+
console.log();
|
|
726
|
+
}
|
|
727
|
+
export async function x402SearchCommand(queryParts, opts = {}) {
|
|
728
|
+
const query = queryParts.join(" ").trim();
|
|
729
|
+
if (!query) {
|
|
730
|
+
output.error("Usage: bankr x402 search <query>");
|
|
731
|
+
process.exit(1);
|
|
732
|
+
}
|
|
733
|
+
const spin = output.spinner("Searching services...");
|
|
734
|
+
try {
|
|
735
|
+
const params = new URLSearchParams({ q: query, limit: "10" });
|
|
736
|
+
const res = await fetch(`${getApiUrl()}/x402/endpoints/discover?${params}`);
|
|
737
|
+
const result = await handleResponse(res);
|
|
738
|
+
spin.stop();
|
|
739
|
+
if (result.services.length === 0) {
|
|
740
|
+
output.info(`No services found for "${query}"`);
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
// Raw mode: dump JSON and exit
|
|
744
|
+
if (opts.raw) {
|
|
745
|
+
console.log(JSON.stringify(result.services, null, 2));
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
console.log();
|
|
749
|
+
console.log(` ${chalk.dim(`Found ${result.services.length} service(s) for`)} "${query}"`);
|
|
750
|
+
console.log(` ${chalk.dim("─".repeat(60))}`);
|
|
751
|
+
for (const svc of result.services) {
|
|
752
|
+
printServiceFormatted(svc);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
catch (err) {
|
|
756
|
+
spin.fail("Search failed");
|
|
757
|
+
output.error(err instanceof Error ? err.message : String(err));
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
//# sourceMappingURL=x402.js.map
|
package/dist/lib/chains.js
CHANGED
|
@@ -5,6 +5,7 @@ export const CHAIN_LABELS = {
|
|
|
5
5
|
unichain: "Unichain",
|
|
6
6
|
worldchain: "World Chain",
|
|
7
7
|
arbitrum: "Arbitrum",
|
|
8
|
+
bnb: "BNB Chain",
|
|
8
9
|
solana: "Solana",
|
|
9
10
|
};
|
|
10
11
|
export const VALID_CHAINS = new Set([
|
|
@@ -14,6 +15,7 @@ export const VALID_CHAINS = new Set([
|
|
|
14
15
|
"unichain",
|
|
15
16
|
"worldchain",
|
|
16
17
|
"arbitrum",
|
|
18
|
+
"bnb",
|
|
17
19
|
"solana",
|
|
18
20
|
]);
|
|
19
21
|
export const CHAIN_IDS = {
|
|
@@ -23,6 +25,7 @@ export const CHAIN_IDS = {
|
|
|
23
25
|
unichain: 130,
|
|
24
26
|
worldchain: 480,
|
|
25
27
|
arbitrum: 42161,
|
|
28
|
+
bnb: 56,
|
|
26
29
|
};
|
|
27
30
|
export const NATIVE_SYMBOLS = {
|
|
28
31
|
base: "ETH",
|
|
@@ -31,5 +34,6 @@ export const NATIVE_SYMBOLS = {
|
|
|
31
34
|
worldchain: "ETH",
|
|
32
35
|
arbitrum: "ETH",
|
|
33
36
|
polygon: "POL",
|
|
37
|
+
bnb: "BNB",
|
|
34
38
|
};
|
|
35
39
|
//# sourceMappingURL=chains.js.map
|
package/dist/lib/output.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export declare const fmt: {
|
|
|
4
4
|
readonly brand: import("chalk").ChalkInstance;
|
|
5
5
|
readonly brandBold: import("chalk").ChalkInstance;
|
|
6
6
|
readonly dim: import("chalk").ChalkInstance;
|
|
7
|
+
readonly success: import("chalk").ChalkInstance;
|
|
8
|
+
readonly warn: import("chalk").ChalkInstance;
|
|
7
9
|
};
|
|
8
10
|
export declare const bankrTheme: {
|
|
9
11
|
prefix: string;
|
package/dist/lib/output.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking update check. Call at the end of main().
|
|
3
|
+
* - Checks npm registry at most once every 4 hours (cached)
|
|
4
|
+
* - Prints a one-line notice if a newer version exists
|
|
5
|
+
* - Never throws, never delays CLI exit
|
|
6
|
+
*/
|
|
7
|
+
export declare function checkForUpdate(currentVersion: string): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=updateCheck.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
const PACKAGE_NAME = "@bankr/cli";
|
|
6
|
+
const CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
7
|
+
const CACHE_DIR = join(homedir(), ".bankr");
|
|
8
|
+
const CACHE_FILE = join(CACHE_DIR, "update-check.json");
|
|
9
|
+
function readCache() {
|
|
10
|
+
try {
|
|
11
|
+
if (!existsSync(CACHE_FILE))
|
|
12
|
+
return null;
|
|
13
|
+
return JSON.parse(readFileSync(CACHE_FILE, "utf-8"));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeCache(cache) {
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(CACHE_DIR))
|
|
22
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
23
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cache));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Silent — cache is best-effort
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function compareSemver(a, b) {
|
|
30
|
+
const partsA = a.split(".").map(Number);
|
|
31
|
+
const partsB = b.split(".").map(Number);
|
|
32
|
+
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
|
|
33
|
+
const numA = partsA[i] ?? 0;
|
|
34
|
+
const numB = partsB[i] ?? 0;
|
|
35
|
+
if (numA < numB)
|
|
36
|
+
return -1;
|
|
37
|
+
if (numA > numB)
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Non-blocking update check. Call at the end of main().
|
|
44
|
+
* - Checks npm registry at most once every 4 hours (cached)
|
|
45
|
+
* - Prints a one-line notice if a newer version exists
|
|
46
|
+
* - Never throws, never delays CLI exit
|
|
47
|
+
*/
|
|
48
|
+
export async function checkForUpdate(currentVersion) {
|
|
49
|
+
try {
|
|
50
|
+
// Check cache first
|
|
51
|
+
const cache = readCache();
|
|
52
|
+
if (cache && Date.now() - cache.lastCheck < CHECK_INTERVAL_MS) {
|
|
53
|
+
if (compareSemver(currentVersion, cache.latestVersion) < 0) {
|
|
54
|
+
printNotice(currentVersion, cache.latestVersion);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Fetch latest with a short timeout
|
|
59
|
+
const res = await fetch(`https://registry.npmjs.org/${PACKAGE_NAME}/latest`, { signal: AbortSignal.timeout(3000) });
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
return;
|
|
62
|
+
const data = (await res.json());
|
|
63
|
+
const latest = data.version;
|
|
64
|
+
writeCache({ lastCheck: Date.now(), latestVersion: latest });
|
|
65
|
+
if (compareSemver(currentVersion, latest) < 0) {
|
|
66
|
+
printNotice(currentVersion, latest);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Silent — never block the CLI for update checks
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function printNotice(current, latest) {
|
|
74
|
+
console.log();
|
|
75
|
+
console.log(chalk.yellow(` Update available: ${current} → ${latest}`));
|
|
76
|
+
console.log(chalk.dim(` Run ${chalk.white("bankr update")} to update`));
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=updateCheck.js.map
|