@bankofai/x402-cli 1.0.1-beta.8 → 1.0.1

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 CHANGED
@@ -1,1014 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import http from "node:http";
3
- import { setTimeout as delay } from "node:timers/promises";
4
- import net from "node:net";
5
- import fs from "node:fs";
6
- import os from "node:os";
7
- import path from "node:path";
8
- import { spawn } from "node:child_process";
9
- import { createRequire } from "node:module";
10
3
  import { fileURLToPath } from "node:url";
11
- import YAML from "yaml";
12
4
  import { createPaymentPayload, decodeRequired, decodeResponse, decodeSignature, encodeRequired, encodeResponse, encodeSignature, headers } from "./x402.js";
13
5
  import { assertRawAmount, findTokenByAddress, getToken, normalizeNetwork, toSmallestUnit } from "./tokens.js";
14
- const BOOLEAN_FLAGS = new Set(["daemon", "dry-run", "force", "help", "human", "include-blocked", "json", "raw", "version"]);
15
- const MAX_HTTP_BODY_BYTES = 10 * 1024 * 1024;
16
- const require = createRequire(import.meta.url);
17
- const DEFAULT_TIMEOUT_MS = 30_000;
18
- const CATALOG_UPDATE_RETRIES = 3;
19
- class CliError extends Error {
20
- code;
21
- hint;
22
- exitCode;
23
- details;
24
- constructor(code, message, hint, exitCode = 1, details) {
25
- super(message);
26
- this.code = code;
27
- this.hint = hint;
28
- this.exitCode = exitCode;
29
- this.details = details;
30
- }
31
- }
32
- function parseArgs(argv) {
33
- const [command = "help", ...rest] = argv;
34
- const positional = [];
35
- const options = {};
36
- for (let i = 0; i < rest.length; i += 1) {
37
- const item = rest[i];
38
- if (item === "-h") {
39
- options.help = true;
40
- continue;
41
- }
42
- if (item === "-V") {
43
- options.version = true;
44
- continue;
45
- }
46
- if (item === "-d") {
47
- options.daemon = true;
48
- continue;
49
- }
50
- if (item === "-n") {
51
- const next = rest[i + 1];
52
- if (!next || next.startsWith("-")) {
53
- options.limit = true;
54
- }
55
- else {
56
- options.limit = next;
57
- i += 1;
58
- }
59
- continue;
60
- }
61
- if (!item.startsWith("--")) {
62
- positional.push(item);
63
- continue;
64
- }
65
- const eq = item.indexOf("=");
66
- const key = eq > 2 ? item.slice(2, eq) : item.slice(2);
67
- const inline = eq > 2 ? item.slice(eq + 1) : undefined;
68
- const next = rest[i + 1];
69
- if (inline !== undefined) {
70
- options[key] = inline;
71
- }
72
- else if (BOOLEAN_FLAGS.has(key)) {
73
- options[key] = true;
74
- }
75
- else if (!next || next.startsWith("--")) {
76
- throw new CliError("MISSING_ARGUMENT", `--${key} requires a value`, `Pass --${key} <value>.`, 2);
77
- }
78
- else {
79
- if (key === "header") {
80
- const current = options[key];
81
- options[key] = Array.isArray(current) ? [...current, next] : current ? [String(current), next] : [next];
82
- }
83
- else {
84
- options[key] = next;
85
- }
86
- i += 1;
87
- }
88
- }
89
- return { command, positional, options };
90
- }
91
- function getVersion() {
92
- try {
93
- const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
94
- return String(pkg.version ?? "0.0.0");
95
- }
96
- catch {
97
- return "0.0.0";
98
- }
99
- }
100
- function opt(options, key, fallback) {
101
- const value = options[key];
102
- return typeof value === "string" ? value : fallback;
103
- }
104
- function hasFlag(options, key) {
105
- return options[key] === true;
106
- }
107
- function outputMode(options) {
108
- if (hasFlag(options, "json") && hasFlag(options, "human")) {
109
- throw new CliError("INVALID_ARGUMENT", "--json and --human are mutually exclusive", "Pass either --json or --human, not both.", 2);
110
- }
111
- return hasFlag(options, "json") ? "json" : "human";
112
- }
113
- function requireArgument(value, name, usage) {
114
- if (value === undefined || value === "") {
115
- throw new CliError("MISSING_ARGUMENT", `${name} is required`, `Usage: ${usage}`, 2);
116
- }
117
- return value;
118
- }
119
- function optAll(options, key) {
120
- const value = options[key];
121
- if (Array.isArray(value))
122
- return value;
123
- return typeof value === "string" ? [value] : [];
124
- }
125
- function printJson(value) {
126
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
127
- }
128
- function emit(args) {
129
- const mode = args.mode ?? "human";
130
- if (mode === "json") {
131
- const envelope = {
132
- ok: !args.error,
133
- command: args.command,
134
- };
135
- if (args.network)
136
- envelope.network = args.network;
137
- if (args.scheme)
138
- envelope.scheme = args.scheme;
139
- if (args.error)
140
- envelope.error = args.error;
141
- else
142
- envelope.result = args.result ?? null;
143
- printJson(envelope);
144
- return;
145
- }
146
- if (args.error) {
147
- process.stderr.write(`ERROR ${args.command}: ${args.error.code}\n`);
148
- process.stderr.write(` ${args.error.message}\n`);
149
- if (args.error.hint)
150
- process.stderr.write(` hint: ${args.error.hint}\n`);
151
- if (args.error.details !== undefined)
152
- process.stderr.write(` details: ${JSON.stringify(args.error.details)}\n`);
153
- return;
154
- }
155
- const suffix = [args.network, args.scheme].filter(Boolean).join(" ");
156
- process.stdout.write(`OK ${args.command}${suffix ? ` (${suffix})` : ""}\n`);
157
- if (args.result && typeof args.result === "object" && !Array.isArray(args.result)) {
158
- for (const [key, value] of Object.entries(args.result)) {
159
- if (value === undefined)
160
- continue;
161
- if (value && typeof value === "object") {
162
- process.stdout.write(` ${key}: ${JSON.stringify(value)}\n`);
163
- }
164
- else {
165
- process.stdout.write(` ${key}: ${value}\n`);
166
- }
167
- }
168
- }
169
- else if (args.result !== undefined) {
170
- process.stdout.write(` ${args.result}\n`);
171
- }
172
- }
173
- function classify(error) {
174
- const message = error instanceof Error ? error.message : String(error);
175
- if (error instanceof CliError) {
176
- return {
177
- code: error.code,
178
- message,
179
- hint: error.hint,
180
- details: error.details,
181
- };
182
- }
183
- const lower = message.toLowerCase();
184
- if (lower.includes("missing private key") || lower.includes("could not find a wallet")) {
185
- return {
186
- code: "WALLET_NOT_CONFIGURED",
187
- message,
188
- hint: "Set PRIVATE_KEY, TRON_PRIVATE_KEY, EVM_PRIVATE_KEY, or configure agent-wallet with a payer wallet.",
189
- };
190
- }
191
- if (lower.includes("wallets_config") || lower.includes("wallet config")) {
192
- return {
193
- code: "WALLET_CONFIG_CORRUPT",
194
- message,
195
- hint: "Check ~/.agent-wallet/wallets_config.json or recreate the local agent-wallet configuration.",
196
- };
197
- }
198
- if (lower.includes("does not exist") && lower.includes("account [t")) {
199
- return {
200
- code: "TRON_ACCOUNT_NOT_ACTIVATED",
201
- message,
202
- hint: "Activate the TRON address by sending it a small amount of TRX before signing contract calls.",
203
- };
204
- }
205
- if (lower.includes("permit2_insufficient_balance") || lower.includes("insufficient") && lower.includes("balance")) {
206
- return {
207
- code: "INSUFFICIENT_TOKEN_BALANCE",
208
- message,
209
- hint: "Fund the payer address with the exact token and network advertised by the provider, then retry.",
210
- };
211
- }
212
- if (lower.includes("transfer_from_failed") || lower.includes("transferfrom failed")) {
213
- return {
214
- code: "TOKEN_TRANSFER_FAILED",
215
- message,
216
- hint: "Check token balance, token contract, payer address, and that the selected x402 route matches the provider requirement.",
217
- };
218
- }
219
- if (lower.includes("insufficient funds for gas") || lower.includes("insufficient gas") || lower.includes("energy")) {
220
- return {
221
- code: "INSUFFICIENT_GAS",
222
- message,
223
- hint: "Fund the payer address with the native gas token for this network.",
224
- };
225
- }
226
- if (lower.includes("deadline") || lower.includes("expired")) {
227
- return {
228
- code: "DEADLINE_OR_CLOCK_SKEW",
229
- message,
230
- hint: "Check local clock sync and retry with a fresh payment requirement.",
231
- };
232
- }
233
- if (lower.includes("permittransferfrom") || lower.includes("invalid signature") || lower.includes("permit reverted")) {
234
- return {
235
- code: "PERMIT_REVERTED",
236
- message,
237
- hint: "The token or Permit2 contract rejected the signature; retry with a fresh requirement and verify token/network support.",
238
- };
239
- }
240
- if (lower.includes("tokenregistry") && lower.includes("import")) {
241
- return {
242
- code: "SDK_API_DRIFT",
243
- message,
244
- hint: "Installed x402 SDK packages do not match this CLI; reinstall @bankofai/x402-cli and SDK dependencies.",
245
- };
246
- }
247
- if (lower.includes("429") || lower.includes("too many requests") || lower.includes("rate limit")) {
248
- return {
249
- code: "RATE_LIMITED",
250
- message,
251
- hint: "Wait briefly and retry; the upstream service or RPC is rate limiting requests.",
252
- };
253
- }
254
- if (lower.includes("402 response missing")) {
255
- return {
256
- code: "INVALID_X402_RESPONSE",
257
- message,
258
- hint: "The endpoint returned HTTP 402 without a PAYMENT-REQUIRED header.",
259
- };
260
- }
261
- if (lower.includes("no matching payment requirement")) {
262
- return {
263
- code: "NO_MATCHING_PAYMENT_REQUIREMENT",
264
- message,
265
- hint: "Relax --network, --token, or --scheme, or use values offered by the provider.",
266
- };
267
- }
268
- if (lower.includes("exceeds --max")) {
269
- return {
270
- code: "PAYMENT_AMOUNT_TOO_HIGH",
271
- message,
272
- hint: "Increase the max amount flag only if this provider price is expected.",
273
- };
274
- }
275
- if (lower.includes("failed to fetch") || lower.includes("fetch failed") || lower.includes("econnrefused") || lower.includes("timed out")) {
276
- return {
277
- code: "NETWORK_ERROR",
278
- message,
279
- hint: "Check the URL, local server, proxy, and network connectivity.",
280
- };
281
- }
282
- if (lower.includes(" is required") || lower.includes("must be") || lower.includes("invalid --") || lower.includes("mutually exclusive")) {
283
- return {
284
- code: lower.includes("required") ? "MISSING_ARGUMENT" : "INVALID_ARGUMENT",
285
- message,
286
- hint: "Run the command with --help to see valid usage and options.",
287
- };
288
- }
289
- return {
290
- code: "IO_ERROR",
291
- message,
292
- hint: "Run with --json for structured output, and check the provider/gateway logs for details.",
293
- };
294
- }
295
- function helpText(topic = "root") {
296
- const sections = {
297
- root: `x402-cli ${getVersion()}
298
-
299
- Usage:
300
- x402-cli <command> [options]
301
-
302
- Commands:
303
- pay <url> Pay an x402-protected URL
304
- serve Run a local x402 paywall endpoint
305
- roundtrip Start serve, pay it, then exit
306
- gateway <command> Manage local gateway provider files
307
- catalog <command> Search, cache, and export provider catalog assets
308
-
309
- Global options:
310
- -h, --help Show help
311
- -V, --version Show version
312
- --json Print machine-readable JSON envelope
313
- --human Print human-readable output (default)
314
- `,
315
- pay: `Usage:
316
- x402-cli pay <url> [options]
317
-
318
- Options:
319
- --method <method> HTTP method (default: GET)
320
- --header "Name: Value" Request header, repeatable
321
- --body <body> Request body for non-GET/HEAD methods
322
- --network <caip2> Require a specific network
323
- --token <symbol> Require a specific token
324
- --scheme <scheme> Require a specific x402 scheme
325
- --gasfree-api-url <url> Override the TRON GasFree relayer API URL
326
- --max-gasfree-fee <amt> Maximum GasFree relayer fee in token units
327
- --max-gasfree-fee-raw <n> Maximum GasFree relayer fee in smallest units
328
- --max-amount <amount> Maximum human-readable payment amount
329
- --max-raw-amount <amount> Maximum smallest-unit payment amount
330
- --dry-run Read requirements but do not sign or pay
331
- --private-key <hex> Explicit payer private key (or PRIVATE_KEY/TRON_PRIVATE_KEY/EVM_PRIVATE_KEY)
332
- --rpc-url <url> Explicit network RPC URL
333
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
334
- --json Print JSON envelope
335
-
336
- Examples:
337
- x402-cli pay https://api.example.com/paid --dry-run --json
338
- x402-cli pay https://api.example.com/paid --max-amount 0.01
339
- `,
340
- serve: `Usage:
341
- x402-cli serve --pay-to <address> [options]
342
-
343
- Options:
344
- --pay-to <address> Recipient wallet address
345
- --amount <amount> Human-readable token amount (default: 0.0001)
346
- --raw-amount <amount> Smallest-unit amount
347
- --network <caip2> Payment network (default: tron:0xcd8690dc)
348
- --scheme <scheme> Payment scheme: exact or exact_gasfree (default: exact)
349
- --token <symbol> Token symbol (default: USDT)
350
- --asset <address> Explicit token address
351
- --decimals <count> Token decimals for unregistered --asset
352
- --host <host> Bind host (default: 127.0.0.1)
353
- --port <port> Bind port (default: 4020)
354
- --resource-url <url> URL advertised in payment requirements
355
- --facilitator-url <url> Facilitator base URL
356
- --timeout-ms <ms> Facilitator timeout in milliseconds (default: 30000)
357
- --daemon Run in background and print the child pid
358
- --json Print JSON envelope
359
-
360
- Examples:
361
- x402-cli serve --pay-to T... --network tron:0xcd8690dc --token USDT
362
- x402-cli serve --pay-to 0x... --network eip155:97 --token USDT --amount 0.0001
363
- `,
364
- roundtrip: `Usage:
365
- x402-cli roundtrip --pay-to <address> [serve/pay options]
366
- `,
367
- gateway: `Usage:
368
- x402-cli gateway <search|start|check|scaffold|catalog> [options]
369
-
370
- Commands:
371
- search <query> Search a gateway/catalog artifact
372
- start Start a local x402 gateway process
373
- check <providers> Validate provider.yml files
374
- scaffold <name> Write a starter provider.yml
375
- catalog <command> Build/check/search gateway catalog assets
376
- `,
377
- "gateway-catalog": `Usage:
378
- x402-cli gateway catalog <build|check|pay-assets|search> [options]
379
-
380
- Commands:
381
- build <providers> Build a local catalog from provider.yml files
382
- check <providers> Validate local provider.yml files
383
- pay-assets <providers> List payable endpoint assets
384
- search <query> Search a catalog artifact
385
- `,
386
- catalog: `Usage:
387
- x402-cli catalog <update|search|show|endpoints|pay-json|export-gateway|build> [options]
388
-
389
- Commands:
390
- update Cache hosted/local catalog assets under ~/.cache
391
- search <query> Search providers
392
- show <provider> Show provider detail JSON
393
- endpoints <provider> List provider endpoints
394
- pay-json <provider> Print provider pay JSON
395
- export-gateway <url> Export catalog.json and pay.md from a gateway
396
- build <providers> Build catalog from provider.yml files
397
-
398
- Options:
399
- --catalog <source> catalog.json path or URL
400
- --provider <fqn> Provider FQN for export-gateway
401
- --output-dir <dir> Output directory for generated files
402
- -n, --limit <count> Search result limit
403
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
404
- --include-blocked Include blocked providers in search
405
- --json Print JSON envelope
406
- `,
407
- "catalog-search": `Usage:
408
- x402-cli catalog search <query> [--catalog <source>] [options]
409
-
410
- Options:
411
- --catalog <source> catalog.json path or URL
412
- -n, --limit <count> Search result limit
413
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
414
- --include-blocked Include blocked providers in search
415
- --json Print JSON envelope
416
- `,
417
- "catalog-show": `Usage:
418
- x402-cli catalog show <provider> [--catalog <source>] [options]
419
-
420
- Options:
421
- --catalog <source> catalog.json path or URL
422
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
423
- --json Print JSON envelope
424
- `,
425
- "catalog-pay-json": `Usage:
426
- x402-cli catalog pay-json <provider> [--catalog <source>] [options]
427
-
428
- Options:
429
- --catalog <source> catalog.json path or URL
430
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
431
- --raw Print raw pay payload instead of JSON envelope
432
- --json Print JSON envelope
433
- `,
434
- "catalog-endpoints": `Usage:
435
- x402-cli catalog endpoints <provider> [--catalog <source>] [options]
436
-
437
- Options:
438
- --catalog <source> catalog.json path or URL
439
- --timeout-ms <ms> Network timeout in milliseconds (default: 30000)
440
- --json Print JSON envelope
441
- `,
442
- "catalog-export-gateway": `Usage:
443
- x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]
444
-
445
- Options:
446
- --provider <fqn> Provider FQN to export
447
- --output-dir <dir> Output directory for generated files
448
- --force Overwrite existing files
449
- --json Print JSON envelope
450
- `,
451
- };
452
- return sections[topic] ?? sections.root;
453
- }
454
- function readYaml(file) {
455
- return YAML.parse(fs.readFileSync(file, "utf8"));
456
- }
457
- function expandEnv(value) {
458
- return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
459
- if (!(name in process.env))
460
- throw new Error(`environment variable \${${name}} is not set`);
461
- return process.env[name] ?? "";
462
- });
463
- }
464
- function expandDeep(value) {
465
- if (typeof value === "string")
466
- return expandEnv(value);
467
- if (Array.isArray(value))
468
- return value.map(expandDeep);
469
- if (value && typeof value === "object") {
470
- return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, expandDeep(item)]));
471
- }
472
- return value;
473
- }
474
- function providerFiles(root) {
475
- const stat = fs.statSync(root);
476
- if (stat.isFile())
477
- return [root];
478
- const out = [];
479
- for (const entry of fs.readdirSync(root, { recursive: true })) {
480
- const file = path.join(root, String(entry));
481
- if (file.endsWith("provider.yml") || file.endsWith("provider.yaml"))
482
- out.push(file);
483
- }
484
- return out.sort();
485
- }
486
- function loadProviderFile(file) {
487
- const provider = expandDeep(readYaml(file));
488
- validateProvider(provider, file);
489
- provider.operator.network = normalizeNetwork(provider.operator.network);
490
- provider.operator.scheme = provider.operator.scheme ?? "exact";
491
- return provider;
492
- }
493
- function validateProvider(provider, file = "provider.yml") {
494
- const required = [
495
- ["name", provider?.name],
496
- ["forward_url", provider?.forward_url],
497
- ["operator.network", provider?.operator?.network],
498
- ["operator.recipient", provider?.operator?.recipient],
499
- ];
500
- for (const [name, value] of required) {
501
- if (typeof value !== "string" || !value.trim())
502
- throw new Error(`${file}: ${name} is required`);
503
- }
504
- try {
505
- const url = new URL(provider.forward_url);
506
- if (!["http:", "https:"].includes(url.protocol))
507
- throw new Error("unsupported protocol");
508
- }
509
- catch {
510
- throw new Error(`${file}: forward_url must be a valid http(s) URL`);
511
- }
512
- if (!Array.isArray(provider.endpoints) || !provider.endpoints.length) {
513
- throw new Error(`${file}: endpoints must contain at least one endpoint`);
514
- }
515
- const seen = new Set();
516
- for (const endpoint of provider.endpoints) {
517
- if (typeof endpoint.method !== "string" || typeof endpoint.path !== "string") {
518
- throw new Error(`${file}: each endpoint needs method and path`);
519
- }
520
- if (!endpoint.method.trim() || !endpoint.path.trim() || !endpoint.path.startsWith("/")) {
521
- throw new Error(`${file}: endpoint method/path must be non-empty and path must start with /`);
522
- }
523
- const price = providerPrice(endpoint);
524
- if (!Number.isFinite(price) || price < 0)
525
- throw new Error(`${file}: endpoint price_usd must be a finite number >= 0`);
526
- const key = `${endpoint.method.toUpperCase()} ${endpoint.path}`;
527
- if (seen.has(key))
528
- throw new Error(`${file}: duplicate endpoint ${key}`);
529
- seen.add(key);
530
- }
531
- }
532
- function providerPrice(endpoint) {
533
- return endpoint?.metering?.dimensions?.[0]?.tiers?.[0]?.price_usd ?? 0;
534
- }
535
- function providerAssetTransferMethod(provider) {
536
- return provider.operator?.asset_transfer_method ?? provider.operator?.assetTransferMethod ?? "permit2";
537
- }
538
- function providerScheme(provider) {
539
- return provider.operator?.scheme ?? "exact";
540
- }
541
- function providerCatalog(provider) {
542
- return {
543
- name: provider.name,
544
- title: provider.title ?? provider.name,
545
- description: provider.description ?? "",
546
- category: provider.category ?? "other",
547
- service_url: provider.display?.service_url,
548
- tags: provider.display?.tags ?? [],
549
- network: normalizeNetwork(provider.operator.network),
550
- currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
551
- endpoints: (provider.endpoints ?? []).map((endpoint) => ({
552
- method: endpoint.method.toUpperCase(),
553
- path: `/providers/${provider.name}${endpoint.path}`,
554
- upstream_path: endpoint.path,
555
- description: endpoint.description ?? "",
556
- paid: providerPrice(endpoint) > 0 ? {
557
- scheme: providerScheme(provider),
558
- network: normalizeNetwork(provider.operator.network),
559
- currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
560
- price_usd: providerPrice(endpoint),
561
- } : null,
562
- x402_routes: providerPrice(endpoint) > 0 ? [{
563
- provider: provider.name,
564
- network: normalizeNetwork(provider.operator.network),
565
- scheme: providerScheme(provider),
566
- assetTransferMethod: providerAssetTransferMethod(provider),
567
- url: `/providers/${provider.name}${endpoint.path}`,
568
- }] : [],
569
- })),
570
- };
571
- }
572
- const FIELD_WEIGHTS = {
573
- fqn: 12,
574
- title: 10,
575
- tags: 8,
576
- chain_kinds: 8,
577
- chains: 8,
578
- category: 6,
579
- category_meta: 6,
580
- endpoints: 6,
581
- i18n: 5,
582
- description: 4,
583
- use_case: 4,
584
- service_url: 2,
585
- };
586
- function stringList(value) {
587
- return Array.isArray(value) ? value.filter(item => item != null).map(String) : [];
588
- }
589
- function dictValues(value) {
590
- if (!value || typeof value !== "object")
591
- return [];
592
- const out = [];
593
- for (const child of Object.values(value)) {
594
- if (child && typeof child === "object" && !Array.isArray(child))
595
- out.push(...dictValues(child));
596
- else if (Array.isArray(child))
597
- out.push(...child.filter(item => item != null).map(String));
598
- else if (child != null)
599
- out.push(String(child));
600
- }
601
- return out;
602
- }
603
- function chainMetaValues(chainsMeta) {
604
- return chainsMeta.flatMap(dictValues);
605
- }
606
- function endpointFields(endpoints) {
607
- const values = [];
608
- for (const endpoint of endpoints) {
609
- values.push(String(endpoint.method ?? ""), String(endpoint.path ?? ""), String(endpoint.probe_status ?? ""));
610
- const paid = endpoint.paid;
611
- if (paid && typeof paid === "object") {
612
- values.push(String(paid.network ?? ""), String(paid.currency ?? ""), String(paid.amount_raw ?? ""));
613
- }
614
- values.push(String(endpoint.title ?? ""), String(endpoint.description ?? ""), String(endpoint.use_case ?? endpoint.useCase ?? ""));
615
- }
616
- return values;
617
- }
618
- function scoreFields(terms, fields) {
619
- let score = 0;
620
- const matchedFields = [];
621
- for (const [field, values] of Object.entries(fields)) {
622
- const haystack = values.filter(Boolean).join(" ").toLowerCase();
623
- if (!haystack)
624
- continue;
625
- const count = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0);
626
- if (count) {
627
- score += (FIELD_WEIGHTS[field] ?? 1) * count;
628
- matchedFields.push(field);
629
- }
630
- }
631
- return { score, matchedFields };
632
- }
633
- async function readCatalog(source, options) {
634
- const text = await readText(source, options);
635
- const parsed = JSON.parse(text);
636
- if (Array.isArray(parsed))
637
- return parsed;
638
- if (Array.isArray(parsed.providers))
639
- return parsed.providers;
640
- if (Array.isArray(parsed.items))
641
- return parsed.items;
642
- return [];
643
- }
644
- async function readCatalogObject(source, options) {
645
- const parsed = JSON.parse(await readText(source, options));
646
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
647
- throw new Error(`expected JSON object from ${source}`);
648
- }
649
- return parsed;
650
- }
651
- async function readText(source, options) {
652
- if (!source.startsWith("http://") && !source.startsWith("https://")) {
653
- return fs.readFileSync(source, "utf8");
654
- }
655
- const response = await fetchWithTimeout(source, {}, timeoutMs(options), `fetch ${source}`);
656
- if (!response.ok)
657
- throw new Error(`failed to fetch ${source}: ${response.status}`);
658
- return readBoundedText(response, `response from ${source}`);
659
- }
660
- async function readBoundedText(response, label) {
661
- const declared = Number(response.headers.get("content-length"));
662
- if (Number.isFinite(declared) && declared > MAX_HTTP_BODY_BYTES) {
663
- throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
664
- }
665
- if (!response.body)
666
- return "";
667
- const reader = response.body.getReader();
668
- const chunks = [];
669
- let size = 0;
670
- for (;;) {
671
- const { done, value } = await reader.read();
672
- if (done)
673
- break;
674
- size += value.byteLength;
675
- if (size > MAX_HTTP_BODY_BYTES) {
676
- await reader.cancel();
677
- throw new Error(`${label} exceeds ${MAX_HTTP_BODY_BYTES} byte limit`);
678
- }
679
- chunks.push(value);
680
- }
681
- const bytes = new Uint8Array(size);
682
- let offset = 0;
683
- for (const chunk of chunks) {
684
- bytes.set(chunk, offset);
685
- offset += chunk.byteLength;
686
- }
687
- return new TextDecoder().decode(bytes);
688
- }
689
- async function readJson(source, options) {
690
- return JSON.parse(await readText(source, options));
691
- }
692
- function writeJson(file, value) {
693
- fs.mkdirSync(path.dirname(file), { recursive: true });
694
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
695
- }
696
- async function responsePayload(response) {
697
- const text = await readBoundedText(response, "HTTP response");
698
- const contentType = response.headers.get("content-type") ?? "";
699
- if (contentType.toLowerCase().includes("json")) {
700
- try {
701
- return JSON.parse(text);
702
- }
703
- catch {
704
- return text;
705
- }
706
- }
707
- return text;
708
- }
709
- async function withSdkStdoutRedirect(enabled, fn) {
710
- if (!enabled)
711
- return fn();
712
- const originalLog = console.log;
713
- console.log = (...args) => {
714
- process.stderr.write(`${args.map(arg => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ")}\n`);
715
- };
716
- try {
717
- return await fn();
718
- }
719
- finally {
720
- console.log = originalLog;
721
- }
722
- }
723
- function cacheDir() {
724
- return path.join(os.homedir(), ".cache", "x402-cli", "catalog");
725
- }
726
- function cachedCatalogPath() {
727
- return path.join(cacheDir(), "catalog.json");
728
- }
729
- function providerFilename(fqn) {
730
- return `${sanitizeProviderName(fqn).replace(/\//g, "__")}.json`;
731
- }
732
- function sanitizeProviderName(name) {
733
- if (!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(name) || name.includes("..")) {
734
- throw new Error("provider name must be a safe FQN using letters, numbers, dots, underscores, dashes, or slashes");
735
- }
736
- return name;
737
- }
738
- function safeOutputPath(baseDir, ...parts) {
739
- const root = path.resolve(baseDir);
740
- const target = path.resolve(root, ...parts);
741
- if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
742
- throw new Error(`refusing to write outside output directory: ${target}`);
743
- }
744
- return target;
745
- }
746
- function ensureWritable(file, options) {
747
- if (fs.existsSync(file) && !hasFlag(options, "force")) {
748
- throw new Error(`${file} already exists; pass --force to overwrite`);
749
- }
750
- }
751
- function defaultCatalogSource() {
752
- const envSource = process.env.X402_CATALOG || process.env.X402_GATEWAY_CATALOG;
753
- if (envSource)
754
- return envSource;
755
- return fs.existsSync(cachedCatalogPath())
756
- ? cachedCatalogPath()
757
- : "https://x402-catalog.bankofai.io/api/catalog.json";
758
- }
759
- function positiveIntegerOption(options, key, fallback) {
760
- const value = opt(options, key, String(fallback));
761
- if (!/^\d+$/.test(value))
762
- throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
763
- const parsed = Number(value);
764
- if (!Number.isSafeInteger(parsed) || parsed <= 0)
765
- throw new CliError("INVALID_ARGUMENT", `--${key} must be a positive integer`, `Pass --${key} with a value greater than zero.`, 2);
766
- return parsed;
767
- }
768
- function timeoutMs(options) {
769
- return options ? positiveIntegerOption(options, "timeout-ms", DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
770
- }
771
- async function fetchWithTimeout(input, init = {}, timeout = DEFAULT_TIMEOUT_MS, label = "request") {
772
- const controller = new AbortController();
773
- const timer = setTimeout(() => controller.abort(), timeout);
774
- try {
775
- return await fetch(input, { ...init, signal: controller.signal });
776
- }
777
- catch (error) {
778
- if (error instanceof Error && error.name === "AbortError")
779
- throw new Error(`${label} timed out after ${timeout}ms`);
780
- throw error;
781
- }
782
- finally {
783
- clearTimeout(timer);
784
- }
785
- }
786
- function remoteBaseFromCatalogPayload(payload) {
787
- const base = payload.base_url ?? payload.baseUrl;
788
- return typeof base === "string" && /^https?:\/\//.test(base) ? `${base.replace(/\/+$/, "")}/` : undefined;
789
- }
790
- async function remoteBaseFromSource(source, payload, options) {
791
- const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
792
- if (fromPayload) {
793
- if (/^https?:\/\//.test(source) && new URL(fromPayload).origin !== new URL(source).origin)
794
- throw new Error("catalog base_url must use the same origin as the catalog source");
795
- return fromPayload;
796
- }
797
- if (source.startsWith("http://") || source.startsWith("https://")) {
798
- return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
799
- }
800
- try {
801
- return remoteBaseFromCatalogPayload(await readCatalogObject(source, options));
802
- }
803
- catch {
804
- return undefined;
805
- }
806
- }
807
- function catalogDetailSource(source, section, name) {
808
- name = sanitizeProviderName(name);
809
- if (source.startsWith("http://") || source.startsWith("https://")) {
810
- const base = new URL(source);
811
- const pathname = base.pathname.endsWith("/catalog.json")
812
- ? base.pathname.slice(0, -"catalog.json".length)
813
- : base.pathname.endsWith("/")
814
- ? base.pathname
815
- : `${base.pathname}/`;
816
- base.pathname = `${pathname}${section}/${providerFilename(name)}`;
817
- base.search = "";
818
- base.hash = "";
819
- return base.toString();
820
- }
821
- const stat = fs.existsSync(source) ? fs.statSync(source) : undefined;
822
- const root = stat?.isDirectory() ? source : path.dirname(source);
823
- const direct = path.join(root, section, `${name}.json`);
824
- if (fs.existsSync(direct))
825
- return direct;
826
- return path.join(root, section, providerFilename(name));
827
- }
828
- async function readCatalogProvider(source, name, options) {
829
- const providers = await readCatalog(source, options);
830
- const summary = providers.find((item) => item.name === name || item.fqn === name);
831
- if (!summary)
832
- throw new Error(`provider not found: ${name}`);
833
- const fqn = summary.fqn ?? summary.name ?? name;
834
- try {
835
- return await readJson(catalogDetailSource(source, "providers", fqn), options);
836
- }
837
- catch {
838
- return summary;
839
- }
840
- }
841
- async function readCatalogPayProvider(source, name, options) {
842
- const providers = await readCatalog(source, options);
843
- const summary = providers.find((item) => item.name === name || item.fqn === name);
844
- const fqn = summary?.fqn ?? summary?.name ?? name;
845
- try {
846
- return await readJson(catalogDetailSource(source, "pay", fqn), options);
847
- }
848
- catch {
849
- if (summary)
850
- return readCatalogProvider(source, name, options);
851
- throw new Error(`provider not found: ${name}`);
852
- }
853
- }
854
- async function cacheProviderAssets(source, catalogPayload, options) {
855
- const base = await remoteBaseFromSource(source, catalogPayload, options);
856
- const warnings = [];
857
- if (!base)
858
- return { detailCount: 0, payCount: 0, warnings };
859
- let detailCount = 0;
860
- let payCount = 0;
861
- for (const provider of catalogPayload.providers ?? []) {
862
- const fqn = provider?.fqn ?? provider?.name;
863
- if (typeof fqn !== "string" || !fqn)
864
- continue;
865
- const filename = providerFilename(fqn);
866
- try {
867
- const detail = await readJson(new URL(`providers/${filename}`, base).toString(), options);
868
- writeJson(path.join(cacheDir(), "providers", filename), detail);
869
- detailCount += 1;
870
- }
871
- catch (error) {
872
- warnings.push(`failed to cache provider detail ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
873
- }
874
- try {
875
- const pay = await readJson(new URL(`pay/${filename}`, base).toString(), options);
876
- writeJson(path.join(cacheDir(), "pay", filename), pay);
877
- payCount += 1;
878
- }
879
- catch (error) {
880
- warnings.push(`failed to cache pay JSON ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
881
- }
882
- }
883
- return { detailCount, payCount, warnings };
884
- }
885
- async function catalogUpdate(source, options) {
886
- let payload;
887
- const warnings = [];
888
- for (let attempt = 1; attempt <= CATALOG_UPDATE_RETRIES; attempt += 1) {
889
- try {
890
- payload = await readCatalogObject(source, options);
891
- break;
892
- }
893
- catch (error) {
894
- const message = error instanceof Error ? error.message : String(error);
895
- if (attempt === CATALOG_UPDATE_RETRIES)
896
- throw error;
897
- warnings.push(`catalog update attempt ${attempt} failed: ${message}`);
898
- await delay(250 * attempt);
899
- }
900
- }
901
- if (!payload)
902
- throw new Error(`failed to read catalog from ${source}`);
903
- writeJson(cachedCatalogPath(), payload);
904
- const cached = await cacheProviderAssets(source, payload, options);
905
- const result = {
906
- source,
907
- path: cachedCatalogPath(),
908
- providerCount: payload.provider_count ?? payload.providerCount ?? (payload.providers ?? []).length,
909
- detailCount: cached.detailCount,
910
- payCount: cached.payCount,
911
- warnings: [...warnings, ...cached.warnings],
912
- };
913
- emit({ command: "catalog update", mode: outputMode(options), result });
914
- }
915
- function zhCopy(title, subtitle, description, useCase) {
916
- return { title, subtitle, description, useCase };
917
- }
918
- function submissionCatalog(detail) {
919
- const title = String(detail.title ?? detail.fqn);
920
- const subtitle = String(detail.subtitle ?? detail.use_case ?? title);
921
- const description = String(detail.description ?? subtitle);
922
- const useCase = String(detail.use_case ?? detail.useCase ?? description);
923
- return {
924
- version: 1,
925
- fqn: detail.fqn,
926
- title,
927
- subtitle,
928
- description,
929
- useCase,
930
- i18n: detail.i18n ?? { "zh-CN": zhCopy(title, subtitle, description, useCase) },
931
- logo: detail.logo ?? "https://x402-catalog.bankofai.io/assets/providers/default.png",
932
- category: detail.category ?? "other",
933
- chains: detail.chains ?? [],
934
- isFirstParty: Boolean(detail.is_first_party ?? detail.isFirstParty),
935
- isFeatured: Boolean(detail.is_featured ?? detail.isFeatured),
936
- featuredTags: detail.featured_tags ?? detail.featuredTags ?? [],
937
- serviceUrl: detail.service_url ?? detail.serviceUrl,
938
- endpoints: (detail.endpoints ?? []).map((endpoint) => {
939
- const endpointTitle = endpoint.title ?? endpoint.path;
940
- const endpointSubtitle = endpoint.subtitle ?? endpoint.path;
941
- const endpointDescription = endpoint.description ?? description;
942
- const endpointUseCase = endpoint.use_case ?? endpoint.useCase ?? useCase;
943
- return {
944
- method: endpoint.method,
945
- path: endpoint.path,
946
- url: endpoint.url,
947
- title: endpointTitle,
948
- subtitle: endpointSubtitle,
949
- description: endpointDescription,
950
- useCase: endpointUseCase,
951
- i18n: endpoint.i18n ?? { "zh-CN": zhCopy(endpointTitle, endpointSubtitle, endpointDescription, endpointUseCase) },
952
- metered: Boolean(endpoint.metered),
953
- minPriceUsd: endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0,
954
- maxPriceUsd: endpoint.max_price_usd ?? endpoint.maxPriceUsd ?? 0,
955
- };
956
- }),
957
- status: detail.status ?? {
958
- catalog: "draft",
959
- gateway: "unknown",
960
- payment: "unknown",
961
- upstream: "unknown",
962
- },
963
- };
964
- }
965
- function payMarkdownFromDetail(detail) {
966
- const lines = [
967
- `# ${detail.title ?? detail.fqn}`,
968
- "",
969
- "## Service",
970
- "",
971
- `- FQN: \`${detail.fqn}\``,
972
- `- Service URL: \`${detail.service_url ?? detail.serviceUrl ?? ""}\``,
973
- `- Category: \`${detail.category ?? ""}\``,
974
- `- Chains: \`${(detail.chains ?? []).join(", ")}\``,
975
- "",
976
- "## Endpoints",
977
- "",
978
- ];
979
- for (const endpoint of detail.endpoints ?? []) {
980
- const metered = Boolean(endpoint.metered);
981
- const price = endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0;
982
- lines.push(`### ${endpoint.method} ${endpoint.path}`, "", endpoint.description ?? "", "", `- URL: \`${endpoint.url ?? ""}\``, `- Metered: \`${String(metered)}\``, `- Price: \`$${price}\``, "");
983
- if (metered) {
984
- lines.push("```bash", `x402-cli pay '${endpoint.url ?? ""}'`, "```", "");
985
- }
986
- else {
987
- lines.push("No payment required.", "");
988
- }
989
- }
990
- lines.push("## Notes", "", "This file is public. Do not include upstream API keys, bearer tokens, provider.yml, `.env`, passwords, or private infrastructure URLs.", "");
991
- return lines.join("\n");
992
- }
993
- async function catalogExportGateway(gatewayUrl, options) {
994
- requireArgument(gatewayUrl, "gateway-url", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
995
- const providerFqn = requireArgument(opt(options, "provider"), "--provider", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
996
- sanitizeProviderName(providerFqn);
997
- const base = gatewayUrl.replace(/\/+$/, "");
998
- const detail = await readJson(`${base}/__402/catalog/providers/${providerFilename(providerFqn)}`, options);
999
- const outputRoot = opt(options, "output-dir", "providers");
1000
- const target = opt(options, "output-dir")
1001
- ? path.resolve(outputRoot)
1002
- : safeOutputPath("providers", providerFilename(providerFqn).replace(/\.json$/, ""));
1003
- fs.mkdirSync(target, { recursive: true });
1004
- const catalogPath = path.join(target, "catalog.json");
1005
- const payMdPath = path.join(target, "pay.md");
1006
- ensureWritable(catalogPath, options);
1007
- ensureWritable(payMdPath, options);
1008
- writeJson(catalogPath, submissionCatalog(detail));
1009
- fs.writeFileSync(payMdPath, payMarkdownFromDetail(detail));
1010
- emit({ command: "catalog export-gateway", mode: outputMode(options), result: { provider: providerFqn, catalog: catalogPath, payMd: payMdPath } });
1011
- }
6
+ import { CliError, hasFlag, opt, optAll, outputMode, parseArgs, requireArgument } from "./args.js";
7
+ import { classify, emit, withSdkStdoutRedirect } from "./output.js";
8
+ import { fetchWithTimeout, readBoundedText, responsePayload, timeoutMs } from "./http-client.js";
9
+ import { startServeDaemon } from "./daemon.js";
10
+ import { getVersion, helpText } from "./help.js";
11
+ import { catalogBuild, catalogPayAssets, gatewayCheck, gatewayScaffold, gatewayStart } from "./gateway-commands.js";
12
+ import { catalogSearch, defaultCatalogSource, handleCatalog } from "./catalog-commands.js";
1012
13
  function buildRequirement(options) {
1013
14
  const network = normalizeNetwork(opt(options, "network", "tron:0xcd8690dc"));
1014
15
  const scheme = opt(options, "scheme", "exact");
@@ -1080,65 +81,6 @@ function requestHeaders(options) {
1080
81
  }
1081
82
  return headersOut;
1082
83
  }
1083
- function stripFlag(argv, flag) {
1084
- return argv.filter(item => item !== flag);
1085
- }
1086
- async function waitForPort(host, port, timeout = 5_000) {
1087
- const deadline = Date.now() + timeout;
1088
- while (Date.now() < deadline) {
1089
- const connected = await new Promise(resolve => {
1090
- const socket = net.createConnection({ host, port });
1091
- socket.setTimeout(250);
1092
- socket.once("connect", () => { socket.destroy(); resolve(true); });
1093
- socket.once("error", () => resolve(false));
1094
- socket.once("timeout", () => { socket.destroy(); resolve(false); });
1095
- });
1096
- if (connected)
1097
- return;
1098
- await delay(50);
1099
- }
1100
- throw new Error(`daemon did not become ready on ${host}:${port}`);
1101
- }
1102
- async function serveDaemon(argv, options) {
1103
- const requirement = buildRequirement(options);
1104
- if (!requirement.payTo)
1105
- throw new Error("--pay-to is required");
1106
- const daemonArgs = stripFlag(stripFlag(argv, "--daemon"), "-d");
1107
- const script = fileURLToPath(import.meta.url);
1108
- const child = spawn(process.execPath, [script, ...daemonArgs], {
1109
- detached: true,
1110
- stdio: "ignore",
1111
- env: process.env,
1112
- });
1113
- child.unref();
1114
- const host = opt(options, "host", "127.0.0.1");
1115
- const port = Number(opt(options, "port", "4020"));
1116
- const resourceUrl = opt(options, "resource-url", `http://${host}:${port}/pay`);
1117
- try {
1118
- await waitForPort(host === "0.0.0.0" ? "127.0.0.1" : host === "::" ? "::1" : host, port);
1119
- }
1120
- catch (error) {
1121
- if (child.pid) {
1122
- try {
1123
- process.kill(child.pid);
1124
- }
1125
- catch { /* child already exited */ }
1126
- }
1127
- throw error;
1128
- }
1129
- emit({
1130
- command: "server",
1131
- mode: outputMode(options),
1132
- network: requirement.network,
1133
- scheme: requirement.scheme,
1134
- result: {
1135
- pid: child.pid,
1136
- pay_url: resourceUrl,
1137
- resource_url: resourceUrl,
1138
- daemon: true,
1139
- },
1140
- });
1141
- }
1142
84
  function validateAmountLimits(selected, options) {
1143
85
  const maxRaw = opt(options, "max-rawAmount") ?? opt(options, "max-raw-amount");
1144
86
  const maxAmount = opt(options, "max-amount");
@@ -1422,336 +364,6 @@ async function roundtrip(options) {
1422
364
  await pay(`http://127.0.0.1:${port}/pay`, options);
1423
365
  process.exit(0);
1424
366
  }
1425
- function executableInPath(name) {
1426
- for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
1427
- if (!dir)
1428
- continue;
1429
- const candidate = path.join(dir, name);
1430
- try {
1431
- fs.accessSync(candidate, fs.constants.X_OK);
1432
- return candidate;
1433
- }
1434
- catch {
1435
- // Try the next PATH entry.
1436
- }
1437
- }
1438
- return undefined;
1439
- }
1440
- function resolveGatewayPackageRuntime() {
1441
- try {
1442
- return require.resolve("@bankofai/x402-gateway/dist/cli.js");
1443
- }
1444
- catch {
1445
- return undefined;
1446
- }
1447
- }
1448
- function gatewayCommand(options) {
1449
- const explicit = opt(options, "gateway-bin");
1450
- const gatewayPackageRuntime = resolveGatewayPackageRuntime();
1451
- const candidates = [
1452
- explicit ? { file: explicit, source: "--gateway-bin" } : undefined,
1453
- gatewayPackageRuntime ? { file: gatewayPackageRuntime, source: "@bankofai/x402-gateway dependency" } : undefined,
1454
- { file: path.resolve(path.dirname(fileURLToPath(import.meta.url)), "gateway", "cli.js"), source: "bundled gateway runtime" },
1455
- executableInPath("x402-gateway") ? { file: executableInPath("x402-gateway"), source: "PATH x402-gateway" } : undefined,
1456
- { file: path.resolve(process.cwd(), "../x402-gateway/dist/cli.js"), source: "sibling ../x402-gateway" },
1457
- ].filter(Boolean);
1458
- for (const candidate of candidates) {
1459
- try {
1460
- fs.accessSync(candidate.file, fs.constants.R_OK);
1461
- if (candidate.file.endsWith(".js")) {
1462
- return { command: process.execPath, argsPrefix: [candidate.file], source: candidate.source };
1463
- }
1464
- return { command: candidate.file, argsPrefix: [], source: candidate.source };
1465
- }
1466
- catch {
1467
- // Try the next candidate.
1468
- }
1469
- }
1470
- throw new Error("x402-gateway runtime not found. Install @bankofai/x402-gateway, run from a checkout with ../x402-gateway/dist/cli.js, or pass --gateway-bin <path>.");
1471
- }
1472
- async function gatewayStart(args, options) {
1473
- const gateway = gatewayCommand(options);
1474
- const child = spawn(gateway.command, [...gateway.argsPrefix, ...args], {
1475
- stdio: "inherit",
1476
- env: process.env,
1477
- });
1478
- await new Promise((resolve, reject) => {
1479
- child.on("exit", code => code === 0 ? resolve() : reject(new Error(`gateway exited with ${code}`)));
1480
- child.on("error", reject);
1481
- });
1482
- }
1483
- function gatewayCheck(target, options) {
1484
- const files = providerFiles(target);
1485
- const providers = files.map(loadProviderFile);
1486
- const names = new Set();
1487
- for (const provider of providers) {
1488
- if (names.has(provider.name))
1489
- throw new Error(`duplicate provider name: ${provider.name}`);
1490
- names.add(provider.name);
1491
- }
1492
- emit({
1493
- command: "gateway check",
1494
- mode: outputMode(options),
1495
- result: { providers: providers.map(p => p.name), count: providers.length },
1496
- });
1497
- }
1498
- function gatewayScaffold(name, options) {
1499
- const outputDir = opt(options, "output-dir", path.join("providers", name));
1500
- const forwardUrl = opt(options, "forward-url", "https://api.example.com");
1501
- fs.mkdirSync(outputDir, { recursive: true });
1502
- const body = `name: ${name}
1503
- title: "${name}"
1504
- description: "x402 provider"
1505
- category: data
1506
- version: v1
1507
-
1508
- forward_url: ${forwardUrl}
1509
-
1510
- routing:
1511
- type: proxy
1512
-
1513
- operator:
1514
- network: tron:0xcd8690dc
1515
- currencies:
1516
- usd: ["USDT"]
1517
- recipient: <provider-recipient-address>
1518
- scheme: exact
1519
- protocol: exact
1520
- asset_transfer_method: permit2
1521
- facilitator_url: https://facilitator.bankofai.io
1522
- facilitator_api_key: <facilitator-api-key>
1523
- valid_for_seconds: 300
1524
-
1525
- endpoints:
1526
- - method: GET
1527
- path: /v1/ping
1528
- metering:
1529
- dimensions:
1530
- - tiers:
1531
- - price_usd: 0.0001
1532
- `;
1533
- fs.writeFileSync(path.join(outputDir, "provider.yml"), body);
1534
- emit({
1535
- command: "gateway scaffold",
1536
- mode: outputMode(options),
1537
- result: { file: path.join(outputDir, "provider.yml") },
1538
- });
1539
- }
1540
- function catalogBuild(target, options) {
1541
- const providers = providerFiles(target).map(loadProviderFile).map(providerCatalog);
1542
- const catalog = { version: 1, generatedAt: new Date().toISOString(), providers };
1543
- const output = opt(options, "output", opt(options, "dist-dir") ? path.join(opt(options, "dist-dir"), "catalog.json") : undefined);
1544
- if (output) {
1545
- fs.mkdirSync(path.dirname(output), { recursive: true });
1546
- fs.writeFileSync(output, JSON.stringify(catalog, null, 2));
1547
- emit({
1548
- command: "catalog build",
1549
- mode: outputMode(options),
1550
- result: { output, count: providers.length },
1551
- });
1552
- }
1553
- else if (outputMode(options) === "json") {
1554
- emit({ command: "catalog build", mode: "json", result: catalog });
1555
- }
1556
- else {
1557
- printJson(catalog);
1558
- }
1559
- }
1560
- function catalogPayAssets(target, options) {
1561
- const rows = providerFiles(target).map(loadProviderFile).flatMap(provider => (provider.endpoints ?? []).map((endpoint) => ({
1562
- provider: provider.name,
1563
- method: endpoint.method,
1564
- path: `/providers/${provider.name}${endpoint.path}`,
1565
- network: normalizeNetwork(provider.operator.network),
1566
- currency: provider.operator.currencies?.usd?.[0] ?? "USDT",
1567
- price_usd: providerPrice(endpoint),
1568
- scheme: providerScheme(provider),
1569
- assetTransferMethod: providerAssetTransferMethod(provider),
1570
- })));
1571
- emit({
1572
- command: "gateway catalog pay-assets",
1573
- mode: outputMode(options),
1574
- result: { count: rows.length, assets: rows },
1575
- });
1576
- }
1577
- async function readProviderDetailForSearch(source, fqn, options) {
1578
- try {
1579
- return await readJson(catalogDetailSource(source, "providers", fqn), options);
1580
- }
1581
- catch {
1582
- return {};
1583
- }
1584
- }
1585
- async function searchCatalog(source, query, options) {
1586
- const providers = await readCatalog(source, options);
1587
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1588
- if (!terms.length)
1589
- return [];
1590
- const includeBlocked = hasFlag(options, "include-blocked");
1591
- const hits = [];
1592
- for (const provider of providers) {
1593
- if (provider.block && !includeBlocked)
1594
- continue;
1595
- const fqn = String(provider.fqn ?? provider.name ?? "");
1596
- if (!fqn)
1597
- continue;
1598
- const detail = await readProviderDetailForSearch(source, fqn, options);
1599
- const tags = stringList(detail.featured_tags ?? provider.featured_tags ?? detail.tags ?? provider.tags);
1600
- const endpoints = Array.isArray(detail.endpoints) ? detail.endpoints : Array.isArray(provider.endpoints) ? provider.endpoints : [];
1601
- const categoryMeta = detail.category_meta ?? provider.category_meta;
1602
- const chains = stringList(detail.chains ?? provider.chains);
1603
- const chainKinds = stringList(detail.chain_kinds ?? provider.chain_kinds);
1604
- const chainsMetaRaw = detail.chains_meta ?? provider.chains_meta ?? [];
1605
- const chainsMeta = Array.isArray(chainsMetaRaw) ? chainsMetaRaw.filter(item => item && typeof item === "object") : [];
1606
- const titleZh = String(detail.title_zh ?? provider.title_zh ?? "");
1607
- const mainTitle = String(detail.main_title ?? provider.main_title ?? detail.mainTitle ?? provider.mainTitle ?? "");
1608
- const subTitle = String(detail.sub_title ?? provider.sub_title ?? detail.subTitle ?? provider.subTitle ?? "");
1609
- const fields = {
1610
- fqn: [fqn],
1611
- title: [String(detail.title ?? provider.title ?? ""), mainTitle],
1612
- i18n: [titleZh, subTitle, ...dictValues(detail.i18n ?? provider.i18n)],
1613
- category: [String(detail.category ?? provider.category ?? "")],
1614
- category_meta: dictValues(categoryMeta),
1615
- chains: [...chains, ...chainMetaValues(chainsMeta)],
1616
- chain_kinds: chainKinds,
1617
- service_url: [String(detail.service_url ?? provider.service_url ?? detail.serviceUrl ?? provider.serviceUrl ?? "")],
1618
- description: [String(detail.description ?? provider.description ?? "")],
1619
- use_case: [String(detail.use_case ?? provider.use_case ?? detail.useCase ?? provider.useCase ?? "")],
1620
- tags,
1621
- endpoints: endpointFields(endpoints),
1622
- };
1623
- const scored = scoreFields(terms, fields);
1624
- if (scored.score === 0)
1625
- continue;
1626
- hits.push({
1627
- provider,
1628
- detail,
1629
- fqn,
1630
- title: fields.title[0],
1631
- category: fields.category[0],
1632
- serviceUrl: fields.service_url[0],
1633
- description: fields.description[0] || undefined,
1634
- useCase: fields.use_case[0] || undefined,
1635
- titleZh: titleZh || undefined,
1636
- mainTitle: mainTitle || undefined,
1637
- subTitle: subTitle || undefined,
1638
- categoryMeta: categoryMeta && typeof categoryMeta === "object" ? categoryMeta : undefined,
1639
- chains,
1640
- chainKinds,
1641
- chainsMeta,
1642
- tags,
1643
- endpoints,
1644
- score: scored.score,
1645
- matchedFields: scored.matchedFields,
1646
- });
1647
- }
1648
- return hits
1649
- .sort((a, b) => b.score - a.score || a.fqn.localeCompare(b.fqn))
1650
- .slice(0, positiveIntegerOption(options, "limit", 10));
1651
- }
1652
- function searchHitToJson(hit) {
1653
- return {
1654
- fqn: hit.fqn,
1655
- title: hit.title,
1656
- category: hit.category,
1657
- serviceUrl: hit.serviceUrl,
1658
- description: hit.description,
1659
- useCase: hit.useCase,
1660
- title_zh: hit.titleZh,
1661
- main_title: hit.mainTitle,
1662
- sub_title: hit.subTitle,
1663
- category_meta: hit.categoryMeta,
1664
- chains: hit.chains,
1665
- chain_kinds: hit.chainKinds,
1666
- chains_meta: hit.chainsMeta,
1667
- tags: hit.tags,
1668
- score: hit.score,
1669
- matchedFields: hit.matchedFields,
1670
- endpoints: hit.endpoints,
1671
- };
1672
- }
1673
- async function catalogSearch(source, query, options) {
1674
- positiveIntegerOption(options, "limit", 10);
1675
- const hits = await searchCatalog(source, query, options);
1676
- const results = hits.map(searchHitToJson);
1677
- if (outputMode(options) === "json") {
1678
- emit({ command: "catalog search", mode: "json", result: { query, catalog: source, count: hits.length, results } });
1679
- return;
1680
- }
1681
- if (!hits.length) {
1682
- process.stdout.write("no matches\n");
1683
- return;
1684
- }
1685
- for (const hit of hits) {
1686
- const tags = hit.tags.length ? hit.tags.join(",") : "-";
1687
- process.stdout.write(`${hit.fqn.padEnd(32)} score=${String(hit.score).padEnd(3)} category=${hit.category.padEnd(12)} tags=${tags}\n`);
1688
- if (hit.title)
1689
- process.stdout.write(` ${hit.title}\n`);
1690
- if (hit.description)
1691
- process.stdout.write(` ${hit.description.split("\n")[0]}\n`);
1692
- if (hit.serviceUrl)
1693
- process.stdout.write(` service: ${hit.serviceUrl}\n`);
1694
- for (const endpoint of hit.endpoints.slice(0, 3)) {
1695
- const method = String(endpoint.method ?? "");
1696
- const pathText = String(endpoint.path ?? endpoint.url ?? "");
1697
- const paid = endpoint.paid;
1698
- let suffix = "";
1699
- if (paid && typeof paid === "object") {
1700
- suffix = ` ${paid.network ?? ""} ${paid.currency ?? ""} ${paid.amount_raw ?? ""}`.trimEnd();
1701
- }
1702
- process.stdout.write(` ${method.padEnd(6)} ${pathText}${suffix ? ` ${suffix}` : ""}\n`);
1703
- }
1704
- process.stdout.write("\n");
1705
- }
1706
- }
1707
- async function catalogShow(source, name, options) {
1708
- requireArgument(name, "provider", "x402-cli catalog show <provider> [--catalog <source>]");
1709
- const provider = await readCatalogProvider(source, name, options);
1710
- if (outputMode(options) === "json") {
1711
- emit({ command: "catalog show", mode: "json", result: provider });
1712
- return;
1713
- }
1714
- process.stdout.write(`${provider.fqn ?? provider.name} - ${provider.title ?? provider.main_title ?? provider.name}\n`);
1715
- if (provider.description)
1716
- process.stdout.write(`${String(provider.description).split("\n")[0]}\n`);
1717
- if (provider.category)
1718
- process.stdout.write(`category: ${provider.category}\n`);
1719
- if (provider.chains)
1720
- process.stdout.write(`chains: ${provider.chains.join(", ")}\n`);
1721
- }
1722
- async function catalogEndpoints(source, name, options) {
1723
- requireArgument(name, "provider", "x402-cli catalog endpoints <provider> [--catalog <source>]");
1724
- const provider = await readCatalogProvider(source, name, options);
1725
- const endpoints = provider.endpoints ?? [];
1726
- if (outputMode(options) === "json") {
1727
- emit({ command: "catalog endpoints", mode: "json", result: { provider: provider.fqn ?? provider.name, endpoints } });
1728
- return;
1729
- }
1730
- for (const endpoint of endpoints) {
1731
- process.stdout.write(`${String(endpoint.method ?? "").padEnd(6)} ${endpoint.path ?? endpoint.url ?? ""}\n`);
1732
- if (endpoint.description)
1733
- process.stdout.write(` ${String(endpoint.description).split("\n")[0]}\n`);
1734
- }
1735
- }
1736
- async function catalogPayJson(source, name, options) {
1737
- requireArgument(name, "provider", "x402-cli catalog pay-json <provider> [--catalog <source>]");
1738
- const provider = await readCatalogPayProvider(source, name, options);
1739
- const endpoint = (provider.endpoints ?? []).find((item) => item.paid || item.x402_routes?.length || item.x402Routes?.length) ?? provider.endpoints?.[0];
1740
- if (!endpoint)
1741
- throw new Error(`provider has no endpoints: ${name}`);
1742
- const result = {
1743
- provider: provider.fqn ?? provider.name,
1744
- url: endpoint.url ?? endpoint.path,
1745
- method: endpoint.method,
1746
- paid: endpoint.paid,
1747
- x402_routes: endpoint.x402_routes ?? endpoint.x402Routes ?? [],
1748
- endpoint,
1749
- };
1750
- if (hasFlag(options, "raw"))
1751
- printJson(result);
1752
- else
1753
- emit({ command: "catalog pay-json", mode: outputMode(options), result });
1754
- }
1755
367
  async function handleGateway(args) {
1756
368
  const { command, positional, options } = parseArgs(args);
1757
369
  if (hasFlag(options, "help") || ["help", "--help", "-h"].includes(command)) {
@@ -1785,31 +397,6 @@ async function handleGatewayCatalog(positional, options) {
1785
397
  else
1786
398
  throw new CliError("UNKNOWN_COMMAND", `Unknown gateway catalog command: ${sub}`, "Run x402-cli gateway catalog --help to list commands.", 2);
1787
399
  }
1788
- async function handleCatalog(args) {
1789
- const { command, positional, options } = parseArgs(args);
1790
- if (hasFlag(options, "help") || ["help", "--help", "-h"].includes(command)) {
1791
- const topic = command === "help" ? positional[0] : command;
1792
- process.stdout.write(helpText(topic ? `catalog-${topic}` : "catalog"));
1793
- return;
1794
- }
1795
- const source = opt(options, "catalog", defaultCatalogSource());
1796
- if (command === "update")
1797
- await catalogUpdate(source, options);
1798
- else if (command === "search")
1799
- await catalogSearch(source, requireArgument(positional.join(" "), "query", "x402-cli catalog search <query> [options]"), options);
1800
- else if (command === "show")
1801
- await catalogShow(source, positional[0], options);
1802
- else if (command === "endpoints")
1803
- await catalogEndpoints(source, positional[0], options);
1804
- else if (command === "pay-json")
1805
- await catalogPayJson(source, positional[0], options);
1806
- else if (command === "export-gateway")
1807
- await catalogExportGateway(positional[0], options);
1808
- else if (command === "build")
1809
- catalogBuild(positional[0] ?? "providers", options);
1810
- else
1811
- throw new CliError("UNKNOWN_COMMAND", `Unknown catalog command: ${command}`, "Run x402-cli catalog --help to list commands.", 2);
1812
- }
1813
400
  async function main() {
1814
401
  const argv = process.argv.slice(2);
1815
402
  const { command, positional, options } = parseArgs(argv);
@@ -1832,7 +419,7 @@ async function main() {
1832
419
  }
1833
420
  if (command === "serve") {
1834
421
  if (hasFlag(options, "daemon"))
1835
- await serveDaemon(argv, options);
422
+ await startServeDaemon(argv, options, buildRequirement(options), fileURLToPath(import.meta.url));
1836
423
  else
1837
424
  await serve(options);
1838
425
  }