@bankofai/x402-cli 1.0.0-beta.0 → 1.0.0-beta.2
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/README.md +14 -2
- package/dist/cli.js +923 -84
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -29,6 +29,18 @@ Run the compiled CLI:
|
|
|
29
29
|
node dist/cli.js <command> [options]
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
Common CLI options:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
x402-cli --help
|
|
36
|
+
x402-cli --version
|
|
37
|
+
x402-cli pay --help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Output is human-readable by default. Add `--json` to commands such as `pay`,
|
|
41
|
+
`serve`, `gateway check`, and `catalog search` for a stable machine-readable
|
|
42
|
+
envelope with `ok`, `command`, `result`, or structured `error` fields.
|
|
43
|
+
|
|
32
44
|
## Commands
|
|
33
45
|
|
|
34
46
|
### Serve
|
|
@@ -96,10 +108,10 @@ Aliases accepted:
|
|
|
96
108
|
|
|
97
109
|
## Facilitator
|
|
98
110
|
|
|
99
|
-
|
|
111
|
+
Pass a facilitator URL when needed:
|
|
100
112
|
|
|
101
113
|
```bash
|
|
102
|
-
|
|
114
|
+
x402-cli serve --facilitator-url https://facilitator.bankofai.io ...
|
|
103
115
|
```
|
|
104
116
|
|
|
105
117
|
CLI payment challenges and payload selection always emit `scheme: "exact"` for
|
package/dist/cli.js
CHANGED
|
@@ -2,24 +2,58 @@
|
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
4
4
|
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
5
6
|
import path from "node:path";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
7
9
|
import YAML from "yaml";
|
|
8
10
|
import { createPaymentPayload, decodeRequired, decodeResponse, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
|
|
9
|
-
import { getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
|
|
11
|
+
import { findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
|
|
12
|
+
const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "help", "human", "include-blocked", "json", "version"]);
|
|
10
13
|
function parseArgs(argv) {
|
|
11
14
|
const [command = "help", ...rest] = argv;
|
|
12
15
|
const positional = [];
|
|
13
16
|
const options = {};
|
|
14
17
|
for (let i = 0; i < rest.length; i += 1) {
|
|
15
18
|
const item = rest[i];
|
|
19
|
+
if (item === "-h") {
|
|
20
|
+
options.help = true;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (item === "-V") {
|
|
24
|
+
options.version = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (item === "-d") {
|
|
28
|
+
options.daemon = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (item === "-n") {
|
|
32
|
+
const next = rest[i + 1];
|
|
33
|
+
if (!next || next.startsWith("-")) {
|
|
34
|
+
options.limit = true;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
options.limit = next;
|
|
38
|
+
i += 1;
|
|
39
|
+
}
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
16
42
|
if (!item.startsWith("--")) {
|
|
17
43
|
positional.push(item);
|
|
18
44
|
continue;
|
|
19
45
|
}
|
|
20
|
-
const
|
|
46
|
+
const eq = item.indexOf("=");
|
|
47
|
+
const key = eq > 2 ? item.slice(2, eq) : item.slice(2);
|
|
48
|
+
const inline = eq > 2 ? item.slice(eq + 1) : undefined;
|
|
21
49
|
const next = rest[i + 1];
|
|
22
|
-
if (
|
|
50
|
+
if (inline !== undefined) {
|
|
51
|
+
options[key] = inline;
|
|
52
|
+
}
|
|
53
|
+
else if (BOOLEAN_FLAGS.has(key)) {
|
|
54
|
+
options[key] = true;
|
|
55
|
+
}
|
|
56
|
+
else if (!next || next.startsWith("-")) {
|
|
23
57
|
options[key] = true;
|
|
24
58
|
}
|
|
25
59
|
else {
|
|
@@ -35,10 +69,25 @@ function parseArgs(argv) {
|
|
|
35
69
|
}
|
|
36
70
|
return { command, positional, options };
|
|
37
71
|
}
|
|
72
|
+
function getVersion() {
|
|
73
|
+
try {
|
|
74
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
75
|
+
return String(pkg.version ?? "0.0.0");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return "0.0.0";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
38
81
|
function opt(options, key, fallback) {
|
|
39
82
|
const value = options[key];
|
|
40
83
|
return typeof value === "string" ? value : fallback;
|
|
41
84
|
}
|
|
85
|
+
function hasFlag(options, key) {
|
|
86
|
+
return options[key] === true;
|
|
87
|
+
}
|
|
88
|
+
function outputMode(options) {
|
|
89
|
+
return hasFlag(options, "json") ? "json" : "human";
|
|
90
|
+
}
|
|
42
91
|
function optAll(options, key) {
|
|
43
92
|
const value = options[key];
|
|
44
93
|
if (Array.isArray(value))
|
|
@@ -48,6 +97,247 @@ function optAll(options, key) {
|
|
|
48
97
|
function printJson(value) {
|
|
49
98
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
50
99
|
}
|
|
100
|
+
function emit(args) {
|
|
101
|
+
const mode = args.mode ?? "human";
|
|
102
|
+
if (mode === "json") {
|
|
103
|
+
const envelope = {
|
|
104
|
+
ok: !args.error,
|
|
105
|
+
command: args.command,
|
|
106
|
+
};
|
|
107
|
+
if (args.network)
|
|
108
|
+
envelope.network = args.network;
|
|
109
|
+
if (args.scheme)
|
|
110
|
+
envelope.scheme = args.scheme;
|
|
111
|
+
if (args.error)
|
|
112
|
+
envelope.error = args.error;
|
|
113
|
+
else
|
|
114
|
+
envelope.result = args.result ?? null;
|
|
115
|
+
printJson(envelope);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (args.error) {
|
|
119
|
+
process.stderr.write(`ERROR ${args.command}: ${args.error.code}\n`);
|
|
120
|
+
process.stderr.write(` ${args.error.message}\n`);
|
|
121
|
+
if (args.error.hint)
|
|
122
|
+
process.stderr.write(` hint: ${args.error.hint}\n`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
|
|
126
|
+
process.stdout.write(`OK ${args.command}${suffix ? ` (${suffix})` : ""}\n`);
|
|
127
|
+
if (args.result && typeof args.result === "object" && !Array.isArray(args.result)) {
|
|
128
|
+
for (const [key, value] of Object.entries(args.result)) {
|
|
129
|
+
if (value === undefined)
|
|
130
|
+
continue;
|
|
131
|
+
if (value && typeof value === "object") {
|
|
132
|
+
process.stdout.write(` ${key}: ${JSON.stringify(value)}\n`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
process.stdout.write(` ${key}: ${value}\n`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (args.result !== undefined) {
|
|
140
|
+
process.stdout.write(` ${args.result}\n`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function classify(error) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
const lower = message.toLowerCase();
|
|
146
|
+
if (lower.includes("missing private key") || lower.includes("could not find a wallet")) {
|
|
147
|
+
return {
|
|
148
|
+
code: "WALLET_NOT_CONFIGURED",
|
|
149
|
+
message,
|
|
150
|
+
hint: "Set PRIVATE_KEY, TRON_PRIVATE_KEY, EVM_PRIVATE_KEY, or configure agent-wallet with a payer wallet.",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (lower.includes("wallets_config") || lower.includes("wallet config")) {
|
|
154
|
+
return {
|
|
155
|
+
code: "WALLET_CONFIG_CORRUPT",
|
|
156
|
+
message,
|
|
157
|
+
hint: "Check ~/.agent-wallet/wallets_config.json or recreate the local agent-wallet configuration.",
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (lower.includes("does not exist") && lower.includes("account [t")) {
|
|
161
|
+
return {
|
|
162
|
+
code: "TRON_ACCOUNT_NOT_ACTIVATED",
|
|
163
|
+
message,
|
|
164
|
+
hint: "Activate the TRON address by sending it a small amount of TRX before signing contract calls.",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (lower.includes("permit2_insufficient_balance") || lower.includes("insufficient") && lower.includes("balance")) {
|
|
168
|
+
return {
|
|
169
|
+
code: "INSUFFICIENT_TOKEN_BALANCE",
|
|
170
|
+
message,
|
|
171
|
+
hint: "Fund the payer address with the exact token and network advertised by the provider, then retry.",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (lower.includes("transfer_from_failed") || lower.includes("transferfrom failed")) {
|
|
175
|
+
return {
|
|
176
|
+
code: "TOKEN_TRANSFER_FAILED",
|
|
177
|
+
message,
|
|
178
|
+
hint: "Check token balance, token contract, payer address, and that the selected x402 route matches the provider requirement.",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (lower.includes("insufficient funds for gas") || lower.includes("insufficient gas") || lower.includes("energy")) {
|
|
182
|
+
return {
|
|
183
|
+
code: "INSUFFICIENT_GAS",
|
|
184
|
+
message,
|
|
185
|
+
hint: "Fund the payer address with the native gas token for this network.",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (lower.includes("deadline") || lower.includes("expired")) {
|
|
189
|
+
return {
|
|
190
|
+
code: "DEADLINE_OR_CLOCK_SKEW",
|
|
191
|
+
message,
|
|
192
|
+
hint: "Check local clock sync and retry with a fresh payment requirement.",
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (lower.includes("permittransferfrom") || lower.includes("invalid signature") || lower.includes("permit reverted")) {
|
|
196
|
+
return {
|
|
197
|
+
code: "PERMIT_REVERTED",
|
|
198
|
+
message,
|
|
199
|
+
hint: "The token or Permit2 contract rejected the signature; retry with a fresh requirement and verify token/network support.",
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (lower.includes("tokenregistry") && lower.includes("import")) {
|
|
203
|
+
return {
|
|
204
|
+
code: "SDK_API_DRIFT",
|
|
205
|
+
message,
|
|
206
|
+
hint: "Installed x402 SDK packages do not match this CLI; reinstall @bankofai/x402-cli and SDK dependencies.",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (lower.includes("429") || lower.includes("too many requests") || lower.includes("rate limit")) {
|
|
210
|
+
return {
|
|
211
|
+
code: "RATE_LIMITED",
|
|
212
|
+
message,
|
|
213
|
+
hint: "Wait briefly and retry; the upstream service or RPC is rate limiting requests.",
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
if (lower.includes("402 response missing")) {
|
|
217
|
+
return {
|
|
218
|
+
code: "INVALID_X402_RESPONSE",
|
|
219
|
+
message,
|
|
220
|
+
hint: "The endpoint returned HTTP 402 without a PAYMENT-REQUIRED header.",
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
if (lower.includes("no matching payment requirement")) {
|
|
224
|
+
return {
|
|
225
|
+
code: "NO_MATCHING_PAYMENT_REQUIREMENT",
|
|
226
|
+
message,
|
|
227
|
+
hint: "Relax --network, --token, or --scheme, or use values offered by the provider.",
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (lower.includes("exceeds --max")) {
|
|
231
|
+
return {
|
|
232
|
+
code: "PAYMENT_AMOUNT_TOO_HIGH",
|
|
233
|
+
message,
|
|
234
|
+
hint: "Increase the max amount flag only if this provider price is expected.",
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (lower.includes("failed to fetch") || lower.includes("fetch failed") || lower.includes("econnrefused")) {
|
|
238
|
+
return {
|
|
239
|
+
code: "NETWORK_ERROR",
|
|
240
|
+
message,
|
|
241
|
+
hint: "Check the URL, local server, proxy, and network connectivity.",
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
code: "IO_ERROR",
|
|
246
|
+
message,
|
|
247
|
+
hint: "Run with --json for structured output, and check the provider/gateway logs for details.",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function helpText(topic = "root") {
|
|
251
|
+
const sections = {
|
|
252
|
+
root: `x402-cli ${getVersion()}
|
|
253
|
+
|
|
254
|
+
Usage:
|
|
255
|
+
x402-cli <command> [options]
|
|
256
|
+
|
|
257
|
+
Commands:
|
|
258
|
+
pay <url> Pay an x402-protected URL
|
|
259
|
+
serve Run a local x402 paywall endpoint
|
|
260
|
+
roundtrip Start serve, pay it, then exit
|
|
261
|
+
gateway <command> Manage local gateway provider files
|
|
262
|
+
catalog <command> Search, cache, and export provider catalog assets
|
|
263
|
+
|
|
264
|
+
Global options:
|
|
265
|
+
-h, --help Show help
|
|
266
|
+
-V, --version Show version
|
|
267
|
+
--json Print machine-readable JSON envelope
|
|
268
|
+
--human Print human-readable output (default)
|
|
269
|
+
`,
|
|
270
|
+
pay: `Usage:
|
|
271
|
+
x402-cli pay <url> [options]
|
|
272
|
+
|
|
273
|
+
Options:
|
|
274
|
+
--method <method> HTTP method (default: GET)
|
|
275
|
+
--header "Name: Value" Request header, repeatable
|
|
276
|
+
--body <body> Request body for non-GET methods
|
|
277
|
+
--network <caip2> Require a specific network
|
|
278
|
+
--token <symbol> Require a specific token
|
|
279
|
+
--scheme <scheme> Require a specific x402 scheme
|
|
280
|
+
--max-amount <amount> Maximum human-readable payment amount
|
|
281
|
+
--max-rawAmount <amount> Maximum smallest-unit payment amount
|
|
282
|
+
--dry-run Read requirements but do not sign or pay
|
|
283
|
+
--private-key <hex> Explicit payer private key
|
|
284
|
+
--rpc-url <url> Explicit network RPC URL
|
|
285
|
+
--json Print JSON envelope
|
|
286
|
+
`,
|
|
287
|
+
serve: `Usage:
|
|
288
|
+
x402-cli serve --pay-to <address> [options]
|
|
289
|
+
|
|
290
|
+
Options:
|
|
291
|
+
--pay-to <address> Recipient wallet address
|
|
292
|
+
--amount <amount> Human-readable token amount
|
|
293
|
+
--rawAmount <amount> Smallest-unit amount
|
|
294
|
+
--network <caip2> Payment network (default: tron:nile)
|
|
295
|
+
--token <symbol> Token symbol (default: USDT)
|
|
296
|
+
--asset <address> Explicit token address
|
|
297
|
+
--decimals <count> Token decimals for unregistered --asset
|
|
298
|
+
--host <host> Bind host (default: 127.0.0.1)
|
|
299
|
+
--port <port> Bind port (default: 4020)
|
|
300
|
+
--resource-url <url> URL advertised in payment requirements
|
|
301
|
+
--facilitator-url <url> Facilitator base URL
|
|
302
|
+
--daemon Run in background and print the child pid
|
|
303
|
+
--json Print JSON envelope
|
|
304
|
+
`,
|
|
305
|
+
roundtrip: `Usage:
|
|
306
|
+
x402-cli roundtrip --pay-to <address> [serve/pay options]
|
|
307
|
+
`,
|
|
308
|
+
gateway: `Usage:
|
|
309
|
+
x402-cli gateway <search|start|check|scaffold|catalog> [options]
|
|
310
|
+
|
|
311
|
+
Commands:
|
|
312
|
+
search <query> Search a gateway/catalog artifact
|
|
313
|
+
start Start a local x402 gateway process
|
|
314
|
+
check <providers> Validate provider.yml files
|
|
315
|
+
scaffold <name> Write a starter provider.yml
|
|
316
|
+
catalog <command> Build/check/search gateway catalog assets
|
|
317
|
+
`,
|
|
318
|
+
catalog: `Usage:
|
|
319
|
+
x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build> [options]
|
|
320
|
+
|
|
321
|
+
Commands:
|
|
322
|
+
update Cache hosted/local catalog assets under ~/.cache
|
|
323
|
+
search <query> Search providers
|
|
324
|
+
show <provider> Show provider detail JSON
|
|
325
|
+
endpoints <provider> List provider endpoints
|
|
326
|
+
pay-json <provider> Print provider pay JSON
|
|
327
|
+
export-gateway <url> Export catalog.json and pay.md from a gateway
|
|
328
|
+
build <providers> Build catalog from provider.yml files
|
|
329
|
+
|
|
330
|
+
Options:
|
|
331
|
+
--catalog <source> catalog.json path or URL
|
|
332
|
+
--provider <fqn> Provider FQN for export-gateway
|
|
333
|
+
--output-dir <dir> Output directory for generated files
|
|
334
|
+
-n, --limit <count> Search result limit
|
|
335
|
+
--include-blocked Include blocked providers in search
|
|
336
|
+
--json Print JSON envelope where supported
|
|
337
|
+
`,
|
|
338
|
+
};
|
|
339
|
+
return sections[topic] ?? sections.root;
|
|
340
|
+
}
|
|
51
341
|
function readYaml(file) {
|
|
52
342
|
return YAML.parse(fs.readFileSync(file, "utf8"));
|
|
53
343
|
}
|
|
@@ -145,6 +435,67 @@ function providerCatalog(provider) {
|
|
|
145
435
|
})),
|
|
146
436
|
};
|
|
147
437
|
}
|
|
438
|
+
const FIELD_WEIGHTS = {
|
|
439
|
+
fqn: 12,
|
|
440
|
+
title: 10,
|
|
441
|
+
tags: 8,
|
|
442
|
+
chain_kinds: 8,
|
|
443
|
+
chains: 8,
|
|
444
|
+
category: 6,
|
|
445
|
+
category_meta: 6,
|
|
446
|
+
endpoints: 6,
|
|
447
|
+
i18n: 5,
|
|
448
|
+
description: 4,
|
|
449
|
+
use_case: 4,
|
|
450
|
+
service_url: 2,
|
|
451
|
+
};
|
|
452
|
+
function stringList(value) {
|
|
453
|
+
return Array.isArray(value) ? value.filter(item => item != null).map(String) : [];
|
|
454
|
+
}
|
|
455
|
+
function dictValues(value) {
|
|
456
|
+
if (!value || typeof value !== "object")
|
|
457
|
+
return [];
|
|
458
|
+
const out = [];
|
|
459
|
+
for (const child of Object.values(value)) {
|
|
460
|
+
if (child && typeof child === "object" && !Array.isArray(child))
|
|
461
|
+
out.push(...dictValues(child));
|
|
462
|
+
else if (Array.isArray(child))
|
|
463
|
+
out.push(...child.filter(item => item != null).map(String));
|
|
464
|
+
else if (child != null)
|
|
465
|
+
out.push(String(child));
|
|
466
|
+
}
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
function chainMetaValues(chainsMeta) {
|
|
470
|
+
return chainsMeta.flatMap(dictValues);
|
|
471
|
+
}
|
|
472
|
+
function endpointFields(endpoints) {
|
|
473
|
+
const values = [];
|
|
474
|
+
for (const endpoint of endpoints) {
|
|
475
|
+
values.push(String(endpoint.method ?? ""), String(endpoint.path ?? ""), String(endpoint.probe_status ?? ""));
|
|
476
|
+
const paid = endpoint.paid;
|
|
477
|
+
if (paid && typeof paid === "object") {
|
|
478
|
+
values.push(String(paid.network ?? ""), String(paid.currency ?? ""), String(paid.amount_raw ?? ""));
|
|
479
|
+
}
|
|
480
|
+
values.push(String(endpoint.title ?? ""), String(endpoint.description ?? ""), String(endpoint.use_case ?? endpoint.useCase ?? ""));
|
|
481
|
+
}
|
|
482
|
+
return values;
|
|
483
|
+
}
|
|
484
|
+
function scoreFields(terms, fields) {
|
|
485
|
+
let score = 0;
|
|
486
|
+
const matchedFields = [];
|
|
487
|
+
for (const [field, values] of Object.entries(fields)) {
|
|
488
|
+
const haystack = values.filter(Boolean).join(" ").toLowerCase();
|
|
489
|
+
if (!haystack)
|
|
490
|
+
continue;
|
|
491
|
+
const count = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0);
|
|
492
|
+
if (count) {
|
|
493
|
+
score += (FIELD_WEIGHTS[field] ?? 1) * count;
|
|
494
|
+
matchedFields.push(field);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return { score, matchedFields };
|
|
498
|
+
}
|
|
148
499
|
async function readCatalog(source) {
|
|
149
500
|
const text = await readText(source);
|
|
150
501
|
const parsed = JSON.parse(text);
|
|
@@ -156,6 +507,13 @@ async function readCatalog(source) {
|
|
|
156
507
|
return parsed.items;
|
|
157
508
|
return [];
|
|
158
509
|
}
|
|
510
|
+
async function readCatalogObject(source) {
|
|
511
|
+
const parsed = JSON.parse(await readText(source));
|
|
512
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
513
|
+
throw new Error(`expected JSON object from ${source}`);
|
|
514
|
+
}
|
|
515
|
+
return parsed;
|
|
516
|
+
}
|
|
159
517
|
async function readText(source) {
|
|
160
518
|
if (!source.startsWith("http://") && !source.startsWith("https://")) {
|
|
161
519
|
return fs.readFileSync(source, "utf8");
|
|
@@ -168,6 +526,69 @@ async function readText(source) {
|
|
|
168
526
|
async function readJson(source) {
|
|
169
527
|
return JSON.parse(await readText(source));
|
|
170
528
|
}
|
|
529
|
+
function writeJson(file, value) {
|
|
530
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
531
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
532
|
+
}
|
|
533
|
+
async function responsePayload(response) {
|
|
534
|
+
const text = await response.text();
|
|
535
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
536
|
+
if (contentType.toLowerCase().includes("json")) {
|
|
537
|
+
try {
|
|
538
|
+
return JSON.parse(text);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
return text;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return text;
|
|
545
|
+
}
|
|
546
|
+
async function withSdkStdoutRedirect(enabled, fn) {
|
|
547
|
+
if (!enabled)
|
|
548
|
+
return fn();
|
|
549
|
+
const originalLog = console.log;
|
|
550
|
+
console.log = (...args) => {
|
|
551
|
+
process.stderr.write(`${args.map(arg => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ")}\n`);
|
|
552
|
+
};
|
|
553
|
+
try {
|
|
554
|
+
return await fn();
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
console.log = originalLog;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function cacheDir() {
|
|
561
|
+
return path.join(os.homedir(), ".cache", "x402-cli", "catalog");
|
|
562
|
+
}
|
|
563
|
+
function cachedCatalogPath() {
|
|
564
|
+
return path.join(cacheDir(), "catalog.json");
|
|
565
|
+
}
|
|
566
|
+
function providerFilename(fqn) {
|
|
567
|
+
return `${fqn.replace(/\//g, "__")}.json`;
|
|
568
|
+
}
|
|
569
|
+
function defaultCatalogSource() {
|
|
570
|
+
return fs.existsSync(cachedCatalogPath())
|
|
571
|
+
? cachedCatalogPath()
|
|
572
|
+
: "https://x402-catelog.bankofai.io/api/catalog.json";
|
|
573
|
+
}
|
|
574
|
+
function remoteBaseFromCatalogPayload(payload) {
|
|
575
|
+
const base = payload.base_url ?? payload.baseUrl;
|
|
576
|
+
return typeof base === "string" && /^https?:\/\//.test(base) ? `${base.replace(/\/+$/, "")}/` : undefined;
|
|
577
|
+
}
|
|
578
|
+
async function remoteBaseFromSource(source, payload) {
|
|
579
|
+
const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
|
|
580
|
+
if (fromPayload)
|
|
581
|
+
return fromPayload;
|
|
582
|
+
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
583
|
+
return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
return remoteBaseFromCatalogPayload(await readCatalogObject(source));
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
171
592
|
function catalogDetailSource(source, section, name) {
|
|
172
593
|
if (source.startsWith("http://") || source.startsWith("https://")) {
|
|
173
594
|
const base = new URL(source);
|
|
@@ -183,7 +604,10 @@ function catalogDetailSource(source, section, name) {
|
|
|
183
604
|
}
|
|
184
605
|
const stat = fs.existsSync(source) ? fs.statSync(source) : undefined;
|
|
185
606
|
const root = stat?.isDirectory() ? source : path.dirname(source);
|
|
186
|
-
|
|
607
|
+
const direct = path.join(root, section, `${name}.json`);
|
|
608
|
+
if (fs.existsSync(direct))
|
|
609
|
+
return direct;
|
|
610
|
+
return path.join(root, section, providerFilename(name));
|
|
187
611
|
}
|
|
188
612
|
async function readCatalogProvider(source, name) {
|
|
189
613
|
const providers = await readCatalog(source);
|
|
@@ -213,19 +637,168 @@ async function readCatalogPayProvider(source, name) {
|
|
|
213
637
|
throw new Error(`provider not found: ${name}`);
|
|
214
638
|
}
|
|
215
639
|
}
|
|
640
|
+
async function cacheProviderAssets(source, catalogPayload) {
|
|
641
|
+
const base = await remoteBaseFromSource(source, catalogPayload);
|
|
642
|
+
if (!base)
|
|
643
|
+
return { detailCount: 0, payCount: 0 };
|
|
644
|
+
let detailCount = 0;
|
|
645
|
+
let payCount = 0;
|
|
646
|
+
for (const provider of catalogPayload.providers ?? []) {
|
|
647
|
+
const fqn = provider?.fqn ?? provider?.name;
|
|
648
|
+
if (typeof fqn !== "string" || !fqn)
|
|
649
|
+
continue;
|
|
650
|
+
const filename = providerFilename(fqn);
|
|
651
|
+
try {
|
|
652
|
+
const detail = await readJson(new URL(`providers/${filename}`, base).toString());
|
|
653
|
+
writeJson(path.join(cacheDir(), "providers", filename), detail);
|
|
654
|
+
detailCount += 1;
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
// A partial cache is still useful.
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
const pay = await readJson(new URL(`pay/${filename}`, base).toString());
|
|
661
|
+
writeJson(path.join(cacheDir(), "pay", filename), pay);
|
|
662
|
+
payCount += 1;
|
|
663
|
+
}
|
|
664
|
+
catch {
|
|
665
|
+
// A partial cache is still useful.
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return { detailCount, payCount };
|
|
669
|
+
}
|
|
670
|
+
async function catalogUpdate(source, options) {
|
|
671
|
+
const payload = await readCatalogObject(source);
|
|
672
|
+
writeJson(cachedCatalogPath(), payload);
|
|
673
|
+
const cached = await cacheProviderAssets(source, payload);
|
|
674
|
+
const result = {
|
|
675
|
+
source,
|
|
676
|
+
path: cachedCatalogPath(),
|
|
677
|
+
providerCount: payload.provider_count ?? payload.providerCount ?? (payload.providers ?? []).length,
|
|
678
|
+
detailCount: cached.detailCount,
|
|
679
|
+
payCount: cached.payCount,
|
|
680
|
+
};
|
|
681
|
+
emit({ command: "catalog update", mode: outputMode(options), result });
|
|
682
|
+
}
|
|
683
|
+
function zhCopy(title, subtitle, description, useCase) {
|
|
684
|
+
return { title, subtitle, description, useCase };
|
|
685
|
+
}
|
|
686
|
+
function submissionCatalog(detail) {
|
|
687
|
+
const title = String(detail.title ?? detail.fqn);
|
|
688
|
+
const subtitle = String(detail.subtitle ?? detail.use_case ?? title);
|
|
689
|
+
const description = String(detail.description ?? subtitle);
|
|
690
|
+
const useCase = String(detail.use_case ?? detail.useCase ?? description);
|
|
691
|
+
return {
|
|
692
|
+
version: 1,
|
|
693
|
+
fqn: detail.fqn,
|
|
694
|
+
title,
|
|
695
|
+
subtitle,
|
|
696
|
+
description,
|
|
697
|
+
useCase,
|
|
698
|
+
i18n: detail.i18n ?? { "zh-CN": zhCopy(title, subtitle, description, useCase) },
|
|
699
|
+
logo: detail.logo ?? "https://x402-catelog.bankofai.io/assets/providers/default.png",
|
|
700
|
+
category: detail.category ?? "other",
|
|
701
|
+
chains: detail.chains ?? [],
|
|
702
|
+
isFirstParty: Boolean(detail.is_first_party ?? detail.isFirstParty),
|
|
703
|
+
isFeatured: Boolean(detail.is_featured ?? detail.isFeatured),
|
|
704
|
+
featuredTags: detail.featured_tags ?? detail.featuredTags ?? [],
|
|
705
|
+
serviceUrl: detail.service_url ?? detail.serviceUrl,
|
|
706
|
+
endpoints: (detail.endpoints ?? []).map((endpoint) => {
|
|
707
|
+
const endpointTitle = endpoint.title ?? endpoint.path;
|
|
708
|
+
const endpointSubtitle = endpoint.subtitle ?? endpoint.path;
|
|
709
|
+
const endpointDescription = endpoint.description ?? description;
|
|
710
|
+
const endpointUseCase = endpoint.use_case ?? endpoint.useCase ?? useCase;
|
|
711
|
+
return {
|
|
712
|
+
method: endpoint.method,
|
|
713
|
+
path: endpoint.path,
|
|
714
|
+
url: endpoint.url,
|
|
715
|
+
title: endpointTitle,
|
|
716
|
+
subtitle: endpointSubtitle,
|
|
717
|
+
description: endpointDescription,
|
|
718
|
+
useCase: endpointUseCase,
|
|
719
|
+
i18n: endpoint.i18n ?? { "zh-CN": zhCopy(endpointTitle, endpointSubtitle, endpointDescription, endpointUseCase) },
|
|
720
|
+
metered: Boolean(endpoint.metered),
|
|
721
|
+
minPriceUsd: endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0,
|
|
722
|
+
maxPriceUsd: endpoint.max_price_usd ?? endpoint.maxPriceUsd ?? 0,
|
|
723
|
+
};
|
|
724
|
+
}),
|
|
725
|
+
status: detail.status ?? {
|
|
726
|
+
catalog: "draft",
|
|
727
|
+
gateway: "unknown",
|
|
728
|
+
payment: "unknown",
|
|
729
|
+
upstream: "unknown",
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function payMarkdownFromDetail(detail) {
|
|
734
|
+
const lines = [
|
|
735
|
+
`# ${detail.title ?? detail.fqn}`,
|
|
736
|
+
"",
|
|
737
|
+
"## Service",
|
|
738
|
+
"",
|
|
739
|
+
`- FQN: \`${detail.fqn}\``,
|
|
740
|
+
`- Service URL: \`${detail.service_url ?? detail.serviceUrl ?? ""}\``,
|
|
741
|
+
`- Category: \`${detail.category ?? ""}\``,
|
|
742
|
+
`- Chains: \`${(detail.chains ?? []).join(", ")}\``,
|
|
743
|
+
"",
|
|
744
|
+
"## Endpoints",
|
|
745
|
+
"",
|
|
746
|
+
];
|
|
747
|
+
for (const endpoint of detail.endpoints ?? []) {
|
|
748
|
+
const metered = Boolean(endpoint.metered);
|
|
749
|
+
const price = endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0;
|
|
750
|
+
lines.push(`### ${endpoint.method} ${endpoint.path}`, "", endpoint.description ?? "", "", `- URL: \`${endpoint.url ?? ""}\``, `- Metered: \`${String(metered)}\``, `- Price: \`$${price}\``, "");
|
|
751
|
+
if (metered) {
|
|
752
|
+
lines.push("```bash", `x402-cli pay '${endpoint.url ?? ""}'`, "```", "");
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
lines.push("No payment required.", "");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
lines.push("## Notes", "", "This file is public. Do not include upstream API keys, bearer tokens, provider.yml, `.env`, passwords, or private infrastructure URLs.", "");
|
|
759
|
+
return lines.join("\n");
|
|
760
|
+
}
|
|
761
|
+
async function catalogExportGateway(gatewayUrl, options) {
|
|
762
|
+
const providerFqn = opt(options, "provider");
|
|
763
|
+
if (!providerFqn)
|
|
764
|
+
throw new Error("--provider is required");
|
|
765
|
+
const base = gatewayUrl.replace(/\/+$/, "");
|
|
766
|
+
const detail = await readJson(`${base}/__402/catalog/providers/${providerFqn}.json`);
|
|
767
|
+
const target = opt(options, "output-dir", path.join("providers", providerFqn));
|
|
768
|
+
fs.mkdirSync(target, { recursive: true });
|
|
769
|
+
const catalogPath = path.join(target, "catalog.json");
|
|
770
|
+
const payMdPath = path.join(target, "pay.md");
|
|
771
|
+
writeJson(catalogPath, submissionCatalog(detail));
|
|
772
|
+
fs.writeFileSync(payMdPath, payMarkdownFromDetail(detail));
|
|
773
|
+
emit({ command: "catalog export-gateway", mode: outputMode(options), result: { provider: providerFqn, catalog: catalogPath, payMd: payMdPath } });
|
|
774
|
+
}
|
|
216
775
|
function buildRequirement(options) {
|
|
217
776
|
const network = normalizeNetwork(opt(options, "network", "tron:nile"));
|
|
218
777
|
const tokenSymbol = opt(options, "token", "USDT");
|
|
219
|
-
const
|
|
220
|
-
const
|
|
778
|
+
const explicitAsset = opt(options, "asset");
|
|
779
|
+
const registryToken = explicitAsset
|
|
780
|
+
? findTokenByAddress(network, explicitAsset)
|
|
781
|
+
: getToken(network, tokenSymbol);
|
|
782
|
+
const decimalsOption = opt(options, "decimals");
|
|
783
|
+
if (explicitAsset && !registryToken && decimalsOption === undefined) {
|
|
784
|
+
throw new Error("When --asset is set without a registry match, --decimals must be provided");
|
|
785
|
+
}
|
|
786
|
+
if (!explicitAsset && !registryToken)
|
|
787
|
+
throw new Error(`unknown token ${tokenSymbol} on ${network}`);
|
|
788
|
+
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : registryToken.decimals;
|
|
789
|
+
if (!Number.isInteger(decimals) || decimals < 0)
|
|
790
|
+
throw new Error("--decimals must be a non-negative integer");
|
|
791
|
+
const amount = opt(options, "rawAmount") ?? toSmallestUnit(opt(options, "amount", "0.0001"), decimals);
|
|
792
|
+
const assetAddress = explicitAsset ?? registryToken.address;
|
|
793
|
+
const assetTransferMethod = registryToken?.assetTransferMethod ?? "permit2";
|
|
221
794
|
return {
|
|
222
795
|
scheme: "exact",
|
|
223
796
|
network,
|
|
224
797
|
amount,
|
|
225
|
-
asset:
|
|
798
|
+
asset: assetAddress,
|
|
226
799
|
payTo: opt(options, "pay-to") ?? opt(options, "payTo") ?? "",
|
|
227
800
|
maxTimeoutSeconds: Number(opt(options, "valid-for-seconds", "300")),
|
|
228
|
-
extra:
|
|
801
|
+
extra: assetTransferMethod ? { assetTransferMethod } : {},
|
|
229
802
|
};
|
|
230
803
|
}
|
|
231
804
|
async function facilitatorPost(baseUrl, path, body) {
|
|
@@ -250,6 +823,34 @@ function requestHeaders(options) {
|
|
|
250
823
|
}
|
|
251
824
|
return headersOut;
|
|
252
825
|
}
|
|
826
|
+
function stripFlag(argv, flag) {
|
|
827
|
+
return argv.filter(item => item !== flag);
|
|
828
|
+
}
|
|
829
|
+
function serveDaemon(argv, options) {
|
|
830
|
+
const daemonArgs = stripFlag(stripFlag(argv, "--daemon"), "-d");
|
|
831
|
+
const script = fileURLToPath(import.meta.url);
|
|
832
|
+
const child = spawn(process.execPath, [script, ...daemonArgs], {
|
|
833
|
+
detached: true,
|
|
834
|
+
stdio: "ignore",
|
|
835
|
+
env: process.env,
|
|
836
|
+
});
|
|
837
|
+
child.unref();
|
|
838
|
+
const host = opt(options, "host", "127.0.0.1");
|
|
839
|
+
const port = Number(opt(options, "port", "4020"));
|
|
840
|
+
const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
|
|
841
|
+
emit({
|
|
842
|
+
command: "server",
|
|
843
|
+
mode: outputMode(options),
|
|
844
|
+
network: normalizeNetwork(opt(options, "network", "tron:nile")),
|
|
845
|
+
scheme: "exact",
|
|
846
|
+
result: {
|
|
847
|
+
pid: child.pid,
|
|
848
|
+
pay_url: resourceUrl,
|
|
849
|
+
resource_url: resourceUrl,
|
|
850
|
+
daemon: true,
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
}
|
|
253
854
|
function validateAmountLimits(selected, options) {
|
|
254
855
|
const maxRaw = opt(options, "max-rawAmount") ?? opt(options, "max-raw-amount");
|
|
255
856
|
const maxAmount = opt(options, "max-amount");
|
|
@@ -257,8 +858,15 @@ function validateAmountLimits(selected, options) {
|
|
|
257
858
|
throw new Error(`payment raw amount ${selected.amount} exceeds --max-rawAmount ${maxRaw}`);
|
|
258
859
|
}
|
|
259
860
|
if (maxAmount) {
|
|
260
|
-
const token =
|
|
261
|
-
|
|
861
|
+
const token = findTokenByAddress(selected.network, selected.asset);
|
|
862
|
+
const decimalsOption = opt(options, "decimals");
|
|
863
|
+
if (!token && decimalsOption === undefined) {
|
|
864
|
+
throw new Error("cannot evaluate --max-amount for an unknown asset; pass --max-rawAmount or --decimals");
|
|
865
|
+
}
|
|
866
|
+
const decimals = decimalsOption !== undefined ? Number(decimalsOption) : token.decimals;
|
|
867
|
+
if (!Number.isInteger(decimals) || decimals < 0)
|
|
868
|
+
throw new Error("--decimals must be a non-negative integer");
|
|
869
|
+
if (BigInt(selected.amount) > BigInt(toSmallestUnit(maxAmount, decimals))) {
|
|
262
870
|
throw new Error(`payment amount exceeds --max-amount ${maxAmount}`);
|
|
263
871
|
}
|
|
264
872
|
}
|
|
@@ -266,7 +874,7 @@ function validateAmountLimits(selected, options) {
|
|
|
266
874
|
async function serve(options) {
|
|
267
875
|
const host = opt(options, "host", "127.0.0.1");
|
|
268
876
|
const port = Number(opt(options, "port", "4020"));
|
|
269
|
-
const facilitatorUrl = opt(options, "facilitator-url",
|
|
877
|
+
const facilitatorUrl = opt(options, "facilitator-url", "https://facilitator.bankofai.io");
|
|
270
878
|
const requirement = buildRequirement(options);
|
|
271
879
|
if (!requirement.payTo)
|
|
272
880
|
throw new Error("--pay-to is required");
|
|
@@ -330,7 +938,13 @@ async function serve(options) {
|
|
|
330
938
|
}
|
|
331
939
|
});
|
|
332
940
|
server.listen(port, host, () => {
|
|
333
|
-
|
|
941
|
+
emit({
|
|
942
|
+
command: "server",
|
|
943
|
+
network: requirement.network,
|
|
944
|
+
scheme: "exact",
|
|
945
|
+
mode: outputMode(options),
|
|
946
|
+
result: { pay_url: resourceUrl, token: opt(options, "token", "USDT"), rawAmount: requirement.amount, pay_to: requirement.payTo },
|
|
947
|
+
});
|
|
334
948
|
});
|
|
335
949
|
}
|
|
336
950
|
function selectRequirement(accepts, options) {
|
|
@@ -354,6 +968,8 @@ function selectRequirement(accepts, options) {
|
|
|
354
968
|
return selected;
|
|
355
969
|
}
|
|
356
970
|
async function pay(url, options) {
|
|
971
|
+
if (!url)
|
|
972
|
+
throw new Error("URL is required");
|
|
357
973
|
const method = opt(options, "method", "GET");
|
|
358
974
|
const baseHeaders = requestHeaders(options);
|
|
359
975
|
const probe = await fetch(url, {
|
|
@@ -362,7 +978,16 @@ async function pay(url, options) {
|
|
|
362
978
|
body: ["GET", "HEAD"].includes(method.toUpperCase()) ? undefined : opt(options, "body"),
|
|
363
979
|
});
|
|
364
980
|
if (probe.status !== 402) {
|
|
365
|
-
|
|
981
|
+
emit({
|
|
982
|
+
command: "client",
|
|
983
|
+
mode: outputMode(options),
|
|
984
|
+
result: {
|
|
985
|
+
url,
|
|
986
|
+
status: probe.status,
|
|
987
|
+
message: "Not a payment-required endpoint",
|
|
988
|
+
response: await responsePayload(probe),
|
|
989
|
+
},
|
|
990
|
+
});
|
|
366
991
|
return;
|
|
367
992
|
}
|
|
368
993
|
const header = probe.headers.get(headers.required);
|
|
@@ -372,11 +997,11 @@ async function pay(url, options) {
|
|
|
372
997
|
const selected = selectRequirement(required.accepts ?? [], options);
|
|
373
998
|
validateAmountLimits(selected, options);
|
|
374
999
|
if (options["dry-run"]) {
|
|
375
|
-
|
|
376
|
-
ok: true,
|
|
1000
|
+
emit({
|
|
377
1001
|
command: "client",
|
|
378
1002
|
network: selected.network,
|
|
379
1003
|
scheme: "exact",
|
|
1004
|
+
mode: outputMode(options),
|
|
380
1005
|
result: {
|
|
381
1006
|
url,
|
|
382
1007
|
resource: required.resource?.url ?? url,
|
|
@@ -386,13 +1011,13 @@ async function pay(url, options) {
|
|
|
386
1011
|
});
|
|
387
1012
|
return;
|
|
388
1013
|
}
|
|
389
|
-
const payload = await createPaymentPayload({
|
|
1014
|
+
const payload = await withSdkStdoutRedirect(outputMode(options) === "json", () => createPaymentPayload({
|
|
390
1015
|
selected,
|
|
391
1016
|
resource: required.resource?.url ?? url,
|
|
392
1017
|
extensions: required.extensions,
|
|
393
1018
|
rpcUrl: opt(options, "rpc-url"),
|
|
394
1019
|
privateKey: opt(options, "private-key"),
|
|
395
|
-
});
|
|
1020
|
+
}));
|
|
396
1021
|
const retryHeaders = new Headers(baseHeaders);
|
|
397
1022
|
retryHeaders.set(headers.signature, encodeSignature(payload));
|
|
398
1023
|
const paid = await fetch(url, {
|
|
@@ -400,18 +1025,24 @@ async function pay(url, options) {
|
|
|
400
1025
|
headers: retryHeaders,
|
|
401
1026
|
body: opt(options, "body"),
|
|
402
1027
|
});
|
|
403
|
-
const
|
|
1028
|
+
const body = await responsePayload(paid);
|
|
404
1029
|
const paymentResponse = paid.headers.get(headers.response);
|
|
405
|
-
|
|
406
|
-
|
|
1030
|
+
const result = {
|
|
1031
|
+
url,
|
|
1032
|
+
status: paid.status,
|
|
1033
|
+
paid: paid.ok,
|
|
1034
|
+
response: body,
|
|
1035
|
+
...(paymentResponse ? { paymentResponse: decodeResponse(paymentResponse) } : {}),
|
|
1036
|
+
};
|
|
1037
|
+
if (!paid.ok) {
|
|
1038
|
+
throw new Error(`HTTP ${paid.status} from ${url}: ${typeof body === "string" ? body.slice(0, 500) : JSON.stringify(body).slice(0, 500)}`);
|
|
1039
|
+
}
|
|
1040
|
+
emit({
|
|
407
1041
|
command: "client",
|
|
408
1042
|
network: selected.network,
|
|
409
1043
|
scheme: "exact",
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
body: text,
|
|
413
|
-
...(paymentResponse ? { paymentResponse: decodeResponse(paymentResponse) } : {}),
|
|
414
|
-
},
|
|
1044
|
+
mode: outputMode(options),
|
|
1045
|
+
result,
|
|
415
1046
|
});
|
|
416
1047
|
}
|
|
417
1048
|
async function roundtrip(options) {
|
|
@@ -421,12 +1052,46 @@ async function roundtrip(options) {
|
|
|
421
1052
|
await pay(`http://127.0.0.1:${port}/pay`, options);
|
|
422
1053
|
process.exit(0);
|
|
423
1054
|
}
|
|
424
|
-
function
|
|
425
|
-
|
|
426
|
-
|
|
1055
|
+
function executableInPath(name) {
|
|
1056
|
+
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
1057
|
+
if (!dir)
|
|
1058
|
+
continue;
|
|
1059
|
+
const candidate = path.join(dir, name);
|
|
1060
|
+
try {
|
|
1061
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
1062
|
+
return candidate;
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
// Try the next PATH entry.
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return undefined;
|
|
427
1069
|
}
|
|
428
|
-
|
|
429
|
-
const
|
|
1070
|
+
function gatewayCommand(options) {
|
|
1071
|
+
const explicit = opt(options, "gateway-bin");
|
|
1072
|
+
const candidates = [
|
|
1073
|
+
explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
|
|
1074
|
+
{ file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
|
|
1075
|
+
executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
|
|
1076
|
+
{ file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
|
|
1077
|
+
].filter(Boolean);
|
|
1078
|
+
for (const candidate of candidates) {
|
|
1079
|
+
try {
|
|
1080
|
+
fs.accessSync(candidate.file, fs.constants.R_OK);
|
|
1081
|
+
if (candidate.file.endsWith(".js")) {
|
|
1082
|
+
return { command: process.execPath, argsPrefix: [candidate.file], source: candidate.source };
|
|
1083
|
+
}
|
|
1084
|
+
return { command: candidate.file, argsPrefix: [], source: candidate.source };
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
// Try the next candidate.
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
throw new Error("x402-gateway runtime not found. Install/publish a x402-gateway npm binary, run from a checkout with ../x402-gateway/dist/cli.js, or pass --gateway-bin <path>.");
|
|
1091
|
+
}
|
|
1092
|
+
async function gatewayStart(args, options) {
|
|
1093
|
+
const gateway = gatewayCommand(options);
|
|
1094
|
+
const child = spawn(gateway.command, [...gateway.argsPrefix, ...args], {
|
|
430
1095
|
stdio: "inherit",
|
|
431
1096
|
env: process.env,
|
|
432
1097
|
});
|
|
@@ -435,7 +1100,7 @@ async function gatewayStart(args) {
|
|
|
435
1100
|
child.on("error", reject);
|
|
436
1101
|
});
|
|
437
1102
|
}
|
|
438
|
-
function gatewayCheck(target) {
|
|
1103
|
+
function gatewayCheck(target, options) {
|
|
439
1104
|
const files = providerFiles(target);
|
|
440
1105
|
const providers = files.map(loadProviderFile);
|
|
441
1106
|
const names = new Set();
|
|
@@ -444,7 +1109,11 @@ function gatewayCheck(target) {
|
|
|
444
1109
|
throw new Error(`duplicate provider name: ${provider.name}`);
|
|
445
1110
|
names.add(provider.name);
|
|
446
1111
|
}
|
|
447
|
-
|
|
1112
|
+
emit({
|
|
1113
|
+
command: "gateway check",
|
|
1114
|
+
mode: outputMode(options),
|
|
1115
|
+
result: { providers: providers.map(p => p.name), count: providers.length },
|
|
1116
|
+
});
|
|
448
1117
|
}
|
|
449
1118
|
function gatewayScaffold(name, options) {
|
|
450
1119
|
const outputDir = opt(options, "output-dir", path.join("providers", name));
|
|
@@ -465,12 +1134,12 @@ operator:
|
|
|
465
1134
|
network: tron-nile
|
|
466
1135
|
currencies:
|
|
467
1136
|
usd: ["USDT"]
|
|
468
|
-
recipient:
|
|
1137
|
+
recipient: <provider-recipient-address>
|
|
469
1138
|
scheme: exact
|
|
470
1139
|
protocol: exact
|
|
471
1140
|
asset_transfer_method: permit2
|
|
472
|
-
facilitator_url:
|
|
473
|
-
facilitator_api_key:
|
|
1141
|
+
facilitator_url: https://facilitator.bankofai.io
|
|
1142
|
+
facilitator_api_key: <facilitator-api-key>
|
|
474
1143
|
valid_for_seconds: 300
|
|
475
1144
|
|
|
476
1145
|
endpoints:
|
|
@@ -480,9 +1149,13 @@ endpoints:
|
|
|
480
1149
|
dimensions:
|
|
481
1150
|
- tiers:
|
|
482
1151
|
- price_usd: 0.0001
|
|
483
|
-
`;
|
|
1152
|
+
`;
|
|
484
1153
|
fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
|
|
485
|
-
|
|
1154
|
+
emit({
|
|
1155
|
+
command: "gateway scaffold",
|
|
1156
|
+
mode: outputMode(options),
|
|
1157
|
+
result: { file: path.join(outputDir, "provider.yml") },
|
|
1158
|
+
});
|
|
486
1159
|
}
|
|
487
1160
|
function catalogBuild(target, options) {
|
|
488
1161
|
const providers = providerFiles(target).map(loadProviderFile).map(providerCatalog);
|
|
@@ -491,13 +1164,17 @@ function catalogBuild(target, options) {
|
|
|
491
1164
|
if (output) {
|
|
492
1165
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
493
1166
|
fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
|
|
494
|
-
|
|
1167
|
+
emit({
|
|
1168
|
+
command: "catalog build",
|
|
1169
|
+
mode: outputMode(options),
|
|
1170
|
+
result: { output, count: providers.length },
|
|
1171
|
+
});
|
|
495
1172
|
}
|
|
496
1173
|
else {
|
|
497
1174
|
printJson(catalog);
|
|
498
1175
|
}
|
|
499
1176
|
}
|
|
500
|
-
function catalogPayAssets(target) {
|
|
1177
|
+
function catalogPayAssets(target, options) {
|
|
501
1178
|
const rows = providerFiles(target).map(loadProviderFile).flatMap(provider => (provider.endpoints ?? []).map((endpoint) => ({
|
|
502
1179
|
provider: provider.name,
|
|
503
1180
|
method: endpoint.method,
|
|
@@ -508,41 +1185,172 @@ function catalogPayAssets(target) {
|
|
|
508
1185
|
scheme: "exact",
|
|
509
1186
|
assetTransferMethod: providerAssetTransferMethod(provider),
|
|
510
1187
|
})));
|
|
511
|
-
|
|
1188
|
+
if (outputMode(options) === "json") {
|
|
1189
|
+
printJson({ assets: rows, count: rows.length });
|
|
1190
|
+
}
|
|
1191
|
+
else {
|
|
1192
|
+
emit({
|
|
1193
|
+
command: "gateway catalog pay-assets",
|
|
1194
|
+
mode: "human",
|
|
1195
|
+
result: { count: rows.length, assets: rows },
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
512
1198
|
}
|
|
513
|
-
async function
|
|
1199
|
+
async function readProviderDetailForSearch(source, fqn) {
|
|
1200
|
+
try {
|
|
1201
|
+
return await readJson(catalogDetailSource(source, "providers", fqn));
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
return {};
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async function searchCatalog(source, query, options) {
|
|
514
1208
|
const providers = await readCatalog(source);
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
.
|
|
536
|
-
.
|
|
1209
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1210
|
+
if (!terms.length)
|
|
1211
|
+
return [];
|
|
1212
|
+
const includeBlocked = hasFlag(options, "include-blocked");
|
|
1213
|
+
const hits = [];
|
|
1214
|
+
for (const provider of providers) {
|
|
1215
|
+
if (provider.block && !includeBlocked)
|
|
1216
|
+
continue;
|
|
1217
|
+
const fqn = String(provider.fqn ?? provider.name ?? "");
|
|
1218
|
+
if (!fqn)
|
|
1219
|
+
continue;
|
|
1220
|
+
const detail = await readProviderDetailForSearch(source, fqn);
|
|
1221
|
+
const tags = stringList(detail.featured_tags ?? provider.featured_tags ?? detail.tags ?? provider.tags);
|
|
1222
|
+
const endpoints = Array.isArray(detail.endpoints) ? detail.endpoints : Array.isArray(provider.endpoints) ? provider.endpoints : [];
|
|
1223
|
+
const categoryMeta = detail.category_meta ?? provider.category_meta;
|
|
1224
|
+
const chains = stringList(detail.chains ?? provider.chains);
|
|
1225
|
+
const chainKinds = stringList(detail.chain_kinds ?? provider.chain_kinds);
|
|
1226
|
+
const chainsMetaRaw = detail.chains_meta ?? provider.chains_meta ?? [];
|
|
1227
|
+
const chainsMeta = Array.isArray(chainsMetaRaw) ? chainsMetaRaw.filter(item => item && typeof item === "object") : [];
|
|
1228
|
+
const titleZh = String(detail.title_zh ?? provider.title_zh ?? "");
|
|
1229
|
+
const mainTitle = String(detail.main_title ?? provider.main_title ?? detail.mainTitle ?? provider.mainTitle ?? "");
|
|
1230
|
+
const subTitle = String(detail.sub_title ?? provider.sub_title ?? detail.subTitle ?? provider.subTitle ?? "");
|
|
1231
|
+
const fields = {
|
|
1232
|
+
fqn: [fqn],
|
|
1233
|
+
title: [String(detail.title ?? provider.title ?? ""), mainTitle],
|
|
1234
|
+
i18n: [titleZh, subTitle, ...dictValues(detail.i18n ?? provider.i18n)],
|
|
1235
|
+
category: [String(detail.category ?? provider.category ?? "")],
|
|
1236
|
+
category_meta: dictValues(categoryMeta),
|
|
1237
|
+
chains: [...chains, ...chainMetaValues(chainsMeta)],
|
|
1238
|
+
chain_kinds: chainKinds,
|
|
1239
|
+
service_url: [String(detail.service_url ?? provider.service_url ?? detail.serviceUrl ?? provider.serviceUrl ?? "")],
|
|
1240
|
+
description: [String(detail.description ?? provider.description ?? "")],
|
|
1241
|
+
use_case: [String(detail.use_case ?? provider.use_case ?? detail.useCase ?? provider.useCase ?? "")],
|
|
1242
|
+
tags,
|
|
1243
|
+
endpoints: endpointFields(endpoints),
|
|
1244
|
+
};
|
|
1245
|
+
const scored = scoreFields(terms, fields);
|
|
1246
|
+
if (scored.score === 0)
|
|
1247
|
+
continue;
|
|
1248
|
+
hits.push({
|
|
1249
|
+
provider,
|
|
1250
|
+
detail,
|
|
1251
|
+
fqn,
|
|
1252
|
+
title: fields.title[0],
|
|
1253
|
+
category: fields.category[0],
|
|
1254
|
+
serviceUrl: fields.service_url[0],
|
|
1255
|
+
description: fields.description[0] || undefined,
|
|
1256
|
+
useCase: fields.use_case[0] || undefined,
|
|
1257
|
+
titleZh: titleZh || undefined,
|
|
1258
|
+
mainTitle: mainTitle || undefined,
|
|
1259
|
+
subTitle: subTitle || undefined,
|
|
1260
|
+
categoryMeta: categoryMeta && typeof categoryMeta === "object" ? categoryMeta : undefined,
|
|
1261
|
+
chains,
|
|
1262
|
+
chainKinds,
|
|
1263
|
+
chainsMeta,
|
|
1264
|
+
tags,
|
|
1265
|
+
endpoints,
|
|
1266
|
+
score: scored.score,
|
|
1267
|
+
matchedFields: scored.matchedFields,
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
return hits
|
|
1271
|
+
.sort((a, b) => b.score - a.score || a.fqn.localeCompare(b.fqn))
|
|
537
1272
|
.slice(0, Number(opt(options, "limit", "10")));
|
|
538
|
-
printJson({ query, count: hits.length, results: hits.map(item => item.provider) });
|
|
539
1273
|
}
|
|
540
|
-
|
|
541
|
-
|
|
1274
|
+
function searchHitToJson(hit) {
|
|
1275
|
+
return {
|
|
1276
|
+
fqn: hit.fqn,
|
|
1277
|
+
title: hit.title,
|
|
1278
|
+
category: hit.category,
|
|
1279
|
+
serviceUrl: hit.serviceUrl,
|
|
1280
|
+
description: hit.description,
|
|
1281
|
+
useCase: hit.useCase,
|
|
1282
|
+
title_zh: hit.titleZh,
|
|
1283
|
+
main_title: hit.mainTitle,
|
|
1284
|
+
sub_title: hit.subTitle,
|
|
1285
|
+
category_meta: hit.categoryMeta,
|
|
1286
|
+
chains: hit.chains,
|
|
1287
|
+
chain_kinds: hit.chainKinds,
|
|
1288
|
+
chains_meta: hit.chainsMeta,
|
|
1289
|
+
tags: hit.tags,
|
|
1290
|
+
score: hit.score,
|
|
1291
|
+
matchedFields: hit.matchedFields,
|
|
1292
|
+
endpoints: hit.endpoints,
|
|
1293
|
+
};
|
|
542
1294
|
}
|
|
543
|
-
async function
|
|
1295
|
+
async function catalogSearch(source, query, options) {
|
|
1296
|
+
const hits = await searchCatalog(source, query, options);
|
|
1297
|
+
const results = hits.map(searchHitToJson);
|
|
1298
|
+
if (outputMode(options) === "json") {
|
|
1299
|
+
printJson({ ok: true, command: "catalog search", query, catalog: source, count: hits.length, results });
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (!hits.length) {
|
|
1303
|
+
process.stdout.write("no matches\n");
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
for (const hit of hits) {
|
|
1307
|
+
const tags = hit.tags.length ? hit.tags.join(",") : "-";
|
|
1308
|
+
process.stdout.write(`${hit.fqn.padEnd(32)} score=${String(hit.score).padEnd(3)} category=${hit.category.padEnd(12)} tags=${tags}\n`);
|
|
1309
|
+
if (hit.title)
|
|
1310
|
+
process.stdout.write(` ${hit.title}\n`);
|
|
1311
|
+
if (hit.description)
|
|
1312
|
+
process.stdout.write(` ${hit.description.split("\n")[0]}\n`);
|
|
1313
|
+
if (hit.serviceUrl)
|
|
1314
|
+
process.stdout.write(` service: ${hit.serviceUrl}\n`);
|
|
1315
|
+
for (const endpoint of hit.endpoints.slice(0, 3)) {
|
|
1316
|
+
const method = String(endpoint.method ?? "");
|
|
1317
|
+
const pathText = String(endpoint.path ?? endpoint.url ?? "");
|
|
1318
|
+
const paid = endpoint.paid;
|
|
1319
|
+
let suffix = "";
|
|
1320
|
+
if (paid && typeof paid === "object") {
|
|
1321
|
+
suffix = ` ${paid.network ?? ""} ${paid.currency ?? ""} ${paid.amount_raw ?? ""}`.trimEnd();
|
|
1322
|
+
}
|
|
1323
|
+
process.stdout.write(` ${method.padEnd(6)} ${pathText}${suffix ? ` ${suffix}` : ""}\n`);
|
|
1324
|
+
}
|
|
1325
|
+
process.stdout.write("\n");
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
async function catalogShow(source, name, options) {
|
|
544
1329
|
const provider = await readCatalogProvider(source, name);
|
|
545
|
-
|
|
1330
|
+
if (outputMode(options) === "json") {
|
|
1331
|
+
printJson({ ok: true, command: "catalog show", result: provider });
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
process.stdout.write(`${provider.fqn ?? provider.name} - ${provider.title ?? provider.main_title ?? provider.name}\n`);
|
|
1335
|
+
if (provider.description)
|
|
1336
|
+
process.stdout.write(`${String(provider.description).split("\n")[0]}\n`);
|
|
1337
|
+
if (provider.category)
|
|
1338
|
+
process.stdout.write(`category: ${provider.category}\n`);
|
|
1339
|
+
if (provider.chains)
|
|
1340
|
+
process.stdout.write(`chains: ${provider.chains.join(", ")}\n`);
|
|
1341
|
+
}
|
|
1342
|
+
async function catalogEndpoints(source, name, options) {
|
|
1343
|
+
const provider = await readCatalogProvider(source, name);
|
|
1344
|
+
const endpoints = provider.endpoints ?? [];
|
|
1345
|
+
if (outputMode(options) === "json") {
|
|
1346
|
+
printJson({ ok: true, command: "catalog endpoints", provider: provider.fqn ?? provider.name, endpoints });
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
for (const endpoint of endpoints) {
|
|
1350
|
+
process.stdout.write(`${String(endpoint.method ?? "").padEnd(6)} ${endpoint.path ?? endpoint.url ?? ""}\n`);
|
|
1351
|
+
if (endpoint.description)
|
|
1352
|
+
process.stdout.write(` ${String(endpoint.description).split("\n")[0]}\n`);
|
|
1353
|
+
}
|
|
546
1354
|
}
|
|
547
1355
|
async function catalogPayJson(source, name) {
|
|
548
1356
|
const provider = await readCatalogPayProvider(source, name);
|
|
@@ -560,16 +1368,22 @@ async function catalogPayJson(source, name) {
|
|
|
560
1368
|
}
|
|
561
1369
|
async function handleGateway(args) {
|
|
562
1370
|
const { command, positional, options } = parseArgs(args);
|
|
563
|
-
if (command === "
|
|
564
|
-
|
|
1371
|
+
if (hasFlag(options, "help") || command === "help") {
|
|
1372
|
+
process.stdout.write(helpText("gateway"));
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (command === "search")
|
|
1376
|
+
await catalogSearch(opt(options, "catalog", defaultCatalogSource()), positional.join(" "), options);
|
|
1377
|
+
else if (command === "start")
|
|
1378
|
+
await gatewayStart(["--providers", opt(options, "providers", opt(options, "providers-dir", positional[0] ?? "providers")), "--host", opt(options, "host", "127.0.0.1"), "--port", opt(options, "port", "4020")], options);
|
|
565
1379
|
else if (command === "check")
|
|
566
|
-
gatewayCheck(positional[0] ?? opt(options, "providers", "providers"));
|
|
1380
|
+
gatewayCheck(positional[0] ?? opt(options, "providers", "providers"), options);
|
|
567
1381
|
else if (command === "scaffold")
|
|
568
1382
|
gatewayScaffold(positional[0] ?? "example-provider", options);
|
|
569
1383
|
else if (command === "catalog")
|
|
570
1384
|
await handleGatewayCatalog(positional, options);
|
|
571
1385
|
else
|
|
572
|
-
throw new Error("Usage: x402-cli gateway <start|check|scaffold|catalog>");
|
|
1386
|
+
throw new Error("Usage: x402-cli gateway <search|start|check|scaffold|catalog>");
|
|
573
1387
|
}
|
|
574
1388
|
async function handleGatewayCatalog(positional, options) {
|
|
575
1389
|
const sub = positional[0] ?? "build";
|
|
@@ -577,35 +1391,56 @@ async function handleGatewayCatalog(positional, options) {
|
|
|
577
1391
|
if (sub === "build")
|
|
578
1392
|
catalogBuild(target, options);
|
|
579
1393
|
else if (sub === "check")
|
|
580
|
-
gatewayCheck(target);
|
|
1394
|
+
gatewayCheck(target, options);
|
|
581
1395
|
else if (sub === "pay-assets")
|
|
582
|
-
catalogPayAssets(target);
|
|
1396
|
+
catalogPayAssets(target, options);
|
|
583
1397
|
else if (sub === "search")
|
|
584
|
-
await catalogSearch(opt(options, "catalog",
|
|
1398
|
+
await catalogSearch(opt(options, "catalog", defaultCatalogSource()), positional.slice(2).join(" ") || opt(options, "query", ""), options);
|
|
585
1399
|
else
|
|
586
1400
|
throw new Error("Usage: x402-cli gateway catalog <build|check|pay-assets|search>");
|
|
587
1401
|
}
|
|
588
1402
|
async function handleCatalog(args) {
|
|
589
1403
|
const { command, positional, options } = parseArgs(args);
|
|
590
|
-
|
|
591
|
-
|
|
1404
|
+
if (hasFlag(options, "help") || command === "help") {
|
|
1405
|
+
process.stdout.write(helpText("catalog"));
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const source = opt(options, "catalog", defaultCatalogSource());
|
|
1409
|
+
if (command === "update")
|
|
1410
|
+
await catalogUpdate(source, options);
|
|
1411
|
+
else if (command === "search")
|
|
592
1412
|
await catalogSearch(source, positional.join(" "), options);
|
|
593
1413
|
else if (command === "show")
|
|
594
|
-
await catalogShow(source, positional[0]);
|
|
1414
|
+
await catalogShow(source, positional[0], options);
|
|
595
1415
|
else if (command === "endpoints")
|
|
596
|
-
await catalogEndpoints(source, positional[0]);
|
|
1416
|
+
await catalogEndpoints(source, positional[0], options);
|
|
597
1417
|
else if (command === "pay-json")
|
|
598
1418
|
await catalogPayJson(source, positional[0]);
|
|
1419
|
+
else if (command === "export-gateway")
|
|
1420
|
+
await catalogExportGateway(positional[0], options);
|
|
599
1421
|
else if (command === "build")
|
|
600
1422
|
catalogBuild(positional[0] ?? "providers", options);
|
|
601
1423
|
else
|
|
602
|
-
throw new Error("Usage: x402-cli catalog <search|show|endpoints|pay-json|build>");
|
|
1424
|
+
throw new Error("Usage: x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build>");
|
|
603
1425
|
}
|
|
604
1426
|
async function main() {
|
|
605
1427
|
const argv = process.argv.slice(2);
|
|
606
1428
|
const { command, positional, options } = parseArgs(argv);
|
|
607
|
-
if (command === "
|
|
608
|
-
|
|
1429
|
+
if (command === "--help" || command === "-h" || command === "help" || hasFlag(options, "help")) {
|
|
1430
|
+
const topic = command === "help" ? positional[0] : command.startsWith("-") ? undefined : command;
|
|
1431
|
+
process.stdout.write(helpText(topic));
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
if (command === "--version" || command === "-V" || hasFlag(options, "version")) {
|
|
1435
|
+
process.stdout.write(`${getVersion()}\n`);
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
if (command === "serve") {
|
|
1439
|
+
if (hasFlag(options, "daemon"))
|
|
1440
|
+
serveDaemon(argv, options);
|
|
1441
|
+
else
|
|
1442
|
+
await serve(options);
|
|
1443
|
+
}
|
|
609
1444
|
else if (command === "pay")
|
|
610
1445
|
await pay(positional[0], options);
|
|
611
1446
|
else if (command === "roundtrip")
|
|
@@ -615,10 +1450,14 @@ async function main() {
|
|
|
615
1450
|
else if (command === "catalog")
|
|
616
1451
|
await handleCatalog(argv.slice(1));
|
|
617
1452
|
else {
|
|
618
|
-
process.stdout.write(
|
|
1453
|
+
process.stdout.write(helpText());
|
|
619
1454
|
}
|
|
620
1455
|
}
|
|
621
1456
|
main().catch(error => {
|
|
622
|
-
|
|
1457
|
+
emit({
|
|
1458
|
+
command: process.argv[2] ?? "x402-cli",
|
|
1459
|
+
mode: process.argv.includes("--json") ? "json" : "human",
|
|
1460
|
+
error: classify(error),
|
|
1461
|
+
});
|
|
623
1462
|
process.exit(1);
|
|
624
1463
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bankofai/x402-cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc -p tsconfig.json",
|
|
13
|
+
"test": "npm run build && node --test tests/*.test.mjs",
|
|
13
14
|
"start": "node dist/cli.js",
|
|
14
15
|
"dev": "tsx src/cli.ts"
|
|
15
16
|
},
|