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

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/args.js ADDED
@@ -0,0 +1,96 @@
1
+ const BOOLEAN_FLAGS = new Set([
2
+ "daemon", "dry-run", "force", "help", "human", "include-blocked", "json", "raw", "version",
3
+ ]);
4
+ export class CliError extends Error {
5
+ code;
6
+ hint;
7
+ exitCode;
8
+ details;
9
+ constructor(code, message, hint, exitCode = 1, details) {
10
+ super(message);
11
+ this.code = code;
12
+ this.hint = hint;
13
+ this.exitCode = exitCode;
14
+ this.details = details;
15
+ }
16
+ }
17
+ export function parseArgs(argv) {
18
+ const [command = "help", ...rest] = argv;
19
+ const positional = [];
20
+ const options = {};
21
+ for (let i = 0; i < rest.length; i += 1) {
22
+ const item = rest[i];
23
+ if (item === "-h") {
24
+ options.help = true;
25
+ continue;
26
+ }
27
+ if (item === "-V") {
28
+ options.version = true;
29
+ continue;
30
+ }
31
+ if (item === "-d") {
32
+ options.daemon = true;
33
+ continue;
34
+ }
35
+ if (item === "-n") {
36
+ const next = rest[i + 1];
37
+ if (!next || next.startsWith("-"))
38
+ options.limit = true;
39
+ else {
40
+ options.limit = next;
41
+ i += 1;
42
+ }
43
+ continue;
44
+ }
45
+ if (!item.startsWith("--")) {
46
+ positional.push(item);
47
+ continue;
48
+ }
49
+ const eq = item.indexOf("=");
50
+ const key = eq > 2 ? item.slice(2, eq) : item.slice(2);
51
+ const inline = eq > 2 ? item.slice(eq + 1) : undefined;
52
+ const next = rest[i + 1];
53
+ if (inline !== undefined)
54
+ options[key] = inline;
55
+ else if (BOOLEAN_FLAGS.has(key))
56
+ options[key] = true;
57
+ else if (!next || next.startsWith("--")) {
58
+ throw new CliError("MISSING_ARGUMENT", `--${key} requires a value`, `Pass --${key} <value>.`, 2);
59
+ }
60
+ else {
61
+ if (key === "header") {
62
+ const current = options[key];
63
+ options[key] = Array.isArray(current) ? [...current, next] : current ? [String(current), next] : [next];
64
+ }
65
+ else
66
+ options[key] = next;
67
+ i += 1;
68
+ }
69
+ }
70
+ return { command, positional, options };
71
+ }
72
+ export function opt(options, key, fallback) {
73
+ const value = options[key];
74
+ return typeof value === "string" ? value : fallback;
75
+ }
76
+ export function hasFlag(options, key) {
77
+ return options[key] === true;
78
+ }
79
+ export function outputMode(options) {
80
+ if (hasFlag(options, "json") && hasFlag(options, "human")) {
81
+ throw new CliError("INVALID_ARGUMENT", "--json and --human are mutually exclusive", "Pass either --json or --human, not both.", 2);
82
+ }
83
+ return hasFlag(options, "json") ? "json" : "human";
84
+ }
85
+ export function requireArgument(value, name, usage) {
86
+ if (value === undefined || value === "") {
87
+ throw new CliError("MISSING_ARGUMENT", `${name} is required`, `Usage: ${usage}`, 2);
88
+ }
89
+ return value;
90
+ }
91
+ export function optAll(options, key) {
92
+ const value = options[key];
93
+ if (Array.isArray(value))
94
+ return value;
95
+ return typeof value === "string" ? [value] : [];
96
+ }
@@ -0,0 +1,558 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { setTimeout as delay } from "node:timers/promises";
5
+ import { CliError, hasFlag, opt, outputMode, parseArgs, requireArgument } from "./args.js";
6
+ import { catalogBuild } from "./gateway-commands.js";
7
+ import { helpText } from "./help.js";
8
+ import { positiveIntegerOption, readJson, readText } from "./http-client.js";
9
+ import { emit, printJson } from "./output.js";
10
+ const CATALOG_UPDATE_RETRIES = 3;
11
+ const FIELD_WEIGHTS = {
12
+ fqn: 12,
13
+ title: 10,
14
+ tags: 8,
15
+ chain_kinds: 8,
16
+ chains: 8,
17
+ category: 6,
18
+ category_meta: 6,
19
+ endpoints: 6,
20
+ i18n: 5,
21
+ description: 4,
22
+ use_case: 4,
23
+ service_url: 2,
24
+ };
25
+ function stringList(value) {
26
+ return Array.isArray(value) ? value.filter(item => item != null).map(String) : [];
27
+ }
28
+ function dictValues(value) {
29
+ if (!value || typeof value !== "object")
30
+ return [];
31
+ const out = [];
32
+ for (const child of Object.values(value)) {
33
+ if (child && typeof child === "object" && !Array.isArray(child))
34
+ out.push(...dictValues(child));
35
+ else if (Array.isArray(child))
36
+ out.push(...child.filter(item => item != null).map(String));
37
+ else if (child != null)
38
+ out.push(String(child));
39
+ }
40
+ return out;
41
+ }
42
+ function chainMetaValues(chainsMeta) {
43
+ return chainsMeta.flatMap(dictValues);
44
+ }
45
+ function endpointFields(endpoints) {
46
+ const values = [];
47
+ for (const endpoint of endpoints) {
48
+ values.push(String(endpoint.method ?? ""), String(endpoint.path ?? ""), String(endpoint.probe_status ?? ""));
49
+ const paid = endpoint.paid;
50
+ if (paid && typeof paid === "object") {
51
+ values.push(String(paid.network ?? ""), String(paid.currency ?? ""), String(paid.amount_raw ?? ""));
52
+ }
53
+ values.push(String(endpoint.title ?? ""), String(endpoint.description ?? ""), String(endpoint.use_case ?? endpoint.useCase ?? ""));
54
+ }
55
+ return values;
56
+ }
57
+ function scoreFields(terms, fields) {
58
+ let score = 0;
59
+ const matchedFields = [];
60
+ for (const [field, values] of Object.entries(fields)) {
61
+ const haystack = values.filter(Boolean).join(" ").toLowerCase();
62
+ if (!haystack)
63
+ continue;
64
+ const count = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0);
65
+ if (count) {
66
+ score += (FIELD_WEIGHTS[field] ?? 1) * count;
67
+ matchedFields.push(field);
68
+ }
69
+ }
70
+ return { score, matchedFields };
71
+ }
72
+ async function readCatalog(source, options) {
73
+ const text = await readText(source, options);
74
+ const parsed = JSON.parse(text);
75
+ if (Array.isArray(parsed))
76
+ return parsed;
77
+ if (Array.isArray(parsed.providers))
78
+ return parsed.providers;
79
+ if (Array.isArray(parsed.items))
80
+ return parsed.items;
81
+ return [];
82
+ }
83
+ async function readCatalogObject(source, options) {
84
+ const parsed = JSON.parse(await readText(source, options));
85
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
86
+ throw new Error(`expected JSON object from ${source}`);
87
+ }
88
+ return parsed;
89
+ }
90
+ function writeJson(file, value) {
91
+ fs.mkdirSync(path.dirname(file), { recursive: true });
92
+ fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
93
+ }
94
+ function cacheDir() {
95
+ return path.join(os.homedir(), ".cache", "x402-cli", "catalog");
96
+ }
97
+ function cachedCatalogPath() {
98
+ return path.join(cacheDir(), "catalog.json");
99
+ }
100
+ function providerFilename(fqn) {
101
+ return `${sanitizeProviderName(fqn).replace(/\//g, "__")}.json`;
102
+ }
103
+ function sanitizeProviderName(name) {
104
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,127}$/.test(name) || name.includes("..")) {
105
+ throw new Error("provider name must be a safe FQN using letters, numbers, dots, underscores, dashes, or slashes");
106
+ }
107
+ return name;
108
+ }
109
+ function safeOutputPath(baseDir, ...parts) {
110
+ const root = path.resolve(baseDir);
111
+ const target = path.resolve(root, ...parts);
112
+ if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
113
+ throw new Error(`refusing to write outside output directory: ${target}`);
114
+ }
115
+ return target;
116
+ }
117
+ function ensureWritable(file, options) {
118
+ if (fs.existsSync(file) && !hasFlag(options, "force")) {
119
+ throw new Error(`${file} already exists; pass --force to overwrite`);
120
+ }
121
+ }
122
+ export function defaultCatalogSource() {
123
+ const envSource = process.env.X402_CATALOG || process.env.X402_GATEWAY_CATALOG;
124
+ if (envSource)
125
+ return envSource;
126
+ return fs.existsSync(cachedCatalogPath())
127
+ ? cachedCatalogPath()
128
+ : "https://x402-catalog.bankofai.io/api/catalog.json";
129
+ }
130
+ function remoteBaseFromCatalogPayload(payload) {
131
+ const base = payload.base_url ?? payload.baseUrl;
132
+ return typeof base === "string" && /^https?:\/\//.test(base) ? `${base.replace(/\/+$/, "")}/` : undefined;
133
+ }
134
+ async function remoteBaseFromSource(source, payload, options) {
135
+ const fromPayload = payload ? remoteBaseFromCatalogPayload(payload) : undefined;
136
+ if (fromPayload) {
137
+ if (/^https?:\/\//.test(source) && new URL(fromPayload).origin !== new URL(source).origin)
138
+ throw new Error("catalog base_url must use the same origin as the catalog source");
139
+ return fromPayload;
140
+ }
141
+ if (source.startsWith("http://") || source.startsWith("https://")) {
142
+ return `${source.slice(0, source.lastIndexOf("/") + 1)}`;
143
+ }
144
+ try {
145
+ return remoteBaseFromCatalogPayload(await readCatalogObject(source, options));
146
+ }
147
+ catch {
148
+ return undefined;
149
+ }
150
+ }
151
+ function catalogDetailSource(source, section, name) {
152
+ name = sanitizeProviderName(name);
153
+ if (source.startsWith("http://") || source.startsWith("https://")) {
154
+ const base = new URL(source);
155
+ const pathname = base.pathname.endsWith("/catalog.json")
156
+ ? base.pathname.slice(0, -"catalog.json".length)
157
+ : base.pathname.endsWith("/")
158
+ ? base.pathname
159
+ : `${base.pathname}/`;
160
+ base.pathname = `${pathname}${section}/${providerFilename(name)}`;
161
+ base.search = "";
162
+ base.hash = "";
163
+ return base.toString();
164
+ }
165
+ const stat = fs.existsSync(source) ? fs.statSync(source) : undefined;
166
+ const root = stat?.isDirectory() ? source : path.dirname(source);
167
+ const direct = path.join(root, section, `${name}.json`);
168
+ if (fs.existsSync(direct))
169
+ return direct;
170
+ return path.join(root, section, providerFilename(name));
171
+ }
172
+ async function readCatalogProvider(source, name, options) {
173
+ const providers = await readCatalog(source, options);
174
+ const summary = providers.find((item) => item.name === name || item.fqn === name);
175
+ if (!summary)
176
+ throw new Error(`provider not found: ${name}`);
177
+ const fqn = summary.fqn ?? summary.name ?? name;
178
+ try {
179
+ return await readJson(catalogDetailSource(source, "providers", fqn), options);
180
+ }
181
+ catch {
182
+ return summary;
183
+ }
184
+ }
185
+ async function readCatalogPayProvider(source, name, options) {
186
+ const providers = await readCatalog(source, options);
187
+ const summary = providers.find((item) => item.name === name || item.fqn === name);
188
+ const fqn = summary?.fqn ?? summary?.name ?? name;
189
+ try {
190
+ return await readJson(catalogDetailSource(source, "pay", fqn), options);
191
+ }
192
+ catch {
193
+ if (summary)
194
+ return readCatalogProvider(source, name, options);
195
+ throw new Error(`provider not found: ${name}`);
196
+ }
197
+ }
198
+ async function cacheProviderAssets(source, catalogPayload, options) {
199
+ const base = await remoteBaseFromSource(source, catalogPayload, options);
200
+ const warnings = [];
201
+ if (!base)
202
+ return { detailCount: 0, payCount: 0, warnings };
203
+ let detailCount = 0;
204
+ let payCount = 0;
205
+ for (const provider of catalogPayload.providers ?? []) {
206
+ const fqn = provider?.fqn ?? provider?.name;
207
+ if (typeof fqn !== "string" || !fqn)
208
+ continue;
209
+ const filename = providerFilename(fqn);
210
+ try {
211
+ const detail = await readJson(new URL(`providers/${filename}`, base).toString(), options);
212
+ writeJson(path.join(cacheDir(), "providers", filename), detail);
213
+ detailCount += 1;
214
+ }
215
+ catch (error) {
216
+ warnings.push(`failed to cache provider detail ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
217
+ }
218
+ try {
219
+ const pay = await readJson(new URL(`pay/${filename}`, base).toString(), options);
220
+ writeJson(path.join(cacheDir(), "pay", filename), pay);
221
+ payCount += 1;
222
+ }
223
+ catch (error) {
224
+ warnings.push(`failed to cache pay JSON ${fqn}: ${error instanceof Error ? error.message : String(error)}`);
225
+ }
226
+ }
227
+ return { detailCount, payCount, warnings };
228
+ }
229
+ async function catalogUpdate(source, options) {
230
+ let payload;
231
+ const warnings = [];
232
+ for (let attempt = 1; attempt <= CATALOG_UPDATE_RETRIES; attempt += 1) {
233
+ try {
234
+ payload = await readCatalogObject(source, options);
235
+ break;
236
+ }
237
+ catch (error) {
238
+ const message = error instanceof Error ? error.message : String(error);
239
+ if (attempt === CATALOG_UPDATE_RETRIES)
240
+ throw error;
241
+ warnings.push(`catalog update attempt ${attempt} failed: ${message}`);
242
+ await delay(250 * attempt);
243
+ }
244
+ }
245
+ if (!payload)
246
+ throw new Error(`failed to read catalog from ${source}`);
247
+ writeJson(cachedCatalogPath(), payload);
248
+ const cached = await cacheProviderAssets(source, payload, options);
249
+ const result = {
250
+ source,
251
+ path: cachedCatalogPath(),
252
+ providerCount: payload.provider_count ?? payload.providerCount ?? (payload.providers ?? []).length,
253
+ detailCount: cached.detailCount,
254
+ payCount: cached.payCount,
255
+ warnings: [...warnings, ...cached.warnings],
256
+ };
257
+ emit({ command: "catalog update", mode: outputMode(options), result });
258
+ }
259
+ function zhCopy(title, subtitle, description, useCase) {
260
+ return { title, subtitle, description, useCase };
261
+ }
262
+ function submissionCatalog(detail) {
263
+ const title = String(detail.title ?? detail.fqn);
264
+ const subtitle = String(detail.subtitle ?? detail.use_case ?? title);
265
+ const description = String(detail.description ?? subtitle);
266
+ const useCase = String(detail.use_case ?? detail.useCase ?? description);
267
+ return {
268
+ version: 1,
269
+ fqn: detail.fqn,
270
+ title,
271
+ subtitle,
272
+ description,
273
+ useCase,
274
+ i18n: detail.i18n ?? { "zh-CN": zhCopy(title, subtitle, description, useCase) },
275
+ logo: detail.logo ?? "https://x402-catalog.bankofai.io/assets/providers/default.png",
276
+ category: detail.category ?? "other",
277
+ chains: detail.chains ?? [],
278
+ isFirstParty: Boolean(detail.is_first_party ?? detail.isFirstParty),
279
+ isFeatured: Boolean(detail.is_featured ?? detail.isFeatured),
280
+ featuredTags: detail.featured_tags ?? detail.featuredTags ?? [],
281
+ serviceUrl: detail.service_url ?? detail.serviceUrl,
282
+ endpoints: (detail.endpoints ?? []).map((endpoint) => {
283
+ const endpointTitle = endpoint.title ?? endpoint.path;
284
+ const endpointSubtitle = endpoint.subtitle ?? endpoint.path;
285
+ const endpointDescription = endpoint.description ?? description;
286
+ const endpointUseCase = endpoint.use_case ?? endpoint.useCase ?? useCase;
287
+ return {
288
+ method: endpoint.method,
289
+ path: endpoint.path,
290
+ url: endpoint.url,
291
+ title: endpointTitle,
292
+ subtitle: endpointSubtitle,
293
+ description: endpointDescription,
294
+ useCase: endpointUseCase,
295
+ i18n: endpoint.i18n ?? { "zh-CN": zhCopy(endpointTitle, endpointSubtitle, endpointDescription, endpointUseCase) },
296
+ metered: Boolean(endpoint.metered),
297
+ minPriceUsd: endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0,
298
+ maxPriceUsd: endpoint.max_price_usd ?? endpoint.maxPriceUsd ?? 0,
299
+ };
300
+ }),
301
+ status: detail.status ?? {
302
+ catalog: "draft",
303
+ gateway: "unknown",
304
+ payment: "unknown",
305
+ upstream: "unknown",
306
+ },
307
+ };
308
+ }
309
+ function payMarkdownFromDetail(detail) {
310
+ const lines = [
311
+ `# ${detail.title ?? detail.fqn}`,
312
+ "",
313
+ "## Service",
314
+ "",
315
+ `- FQN: \`${detail.fqn}\``,
316
+ `- Service URL: \`${detail.service_url ?? detail.serviceUrl ?? ""}\``,
317
+ `- Category: \`${detail.category ?? ""}\``,
318
+ `- Chains: \`${(detail.chains ?? []).join(", ")}\``,
319
+ "",
320
+ "## Endpoints",
321
+ "",
322
+ ];
323
+ for (const endpoint of detail.endpoints ?? []) {
324
+ const metered = Boolean(endpoint.metered);
325
+ const price = endpoint.min_price_usd ?? endpoint.minPriceUsd ?? 0;
326
+ lines.push(`### ${endpoint.method} ${endpoint.path}`, "", endpoint.description ?? "", "", `- URL: \`${endpoint.url ?? ""}\``, `- Metered: \`${String(metered)}\``, `- Price: \`$${price}\``, "");
327
+ if (metered) {
328
+ lines.push("```bash", `x402-cli pay '${endpoint.url ?? ""}'`, "```", "");
329
+ }
330
+ else {
331
+ lines.push("No payment required.", "");
332
+ }
333
+ }
334
+ lines.push("## Notes", "", "This file is public. Do not include upstream API keys, bearer tokens, provider.yml, `.env`, passwords, or private infrastructure URLs.", "");
335
+ return lines.join("\n");
336
+ }
337
+ async function catalogExportGateway(gatewayUrl, options) {
338
+ requireArgument(gatewayUrl, "gateway-url", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
339
+ const providerFqn = requireArgument(opt(options, "provider"), "--provider", "x402-cli catalog export-gateway <gateway-url> --provider <fqn> [options]");
340
+ sanitizeProviderName(providerFqn);
341
+ const base = gatewayUrl.replace(/\/+$/, "");
342
+ const detail = await readJson(`${base}/__402/catalog/providers/${providerFilename(providerFqn)}`, options);
343
+ const outputRoot = opt(options, "output-dir", "providers");
344
+ const target = opt(options, "output-dir")
345
+ ? path.resolve(outputRoot)
346
+ : safeOutputPath("providers", providerFilename(providerFqn).replace(/\.json$/, ""));
347
+ fs.mkdirSync(target, { recursive: true });
348
+ const catalogPath = path.join(target, "catalog.json");
349
+ const payMdPath = path.join(target, "pay.md");
350
+ ensureWritable(catalogPath, options);
351
+ ensureWritable(payMdPath, options);
352
+ writeJson(catalogPath, submissionCatalog(detail));
353
+ fs.writeFileSync(payMdPath, payMarkdownFromDetail(detail));
354
+ emit({ command: "catalog export-gateway", mode: outputMode(options), result: { provider: providerFqn, catalog: catalogPath, payMd: payMdPath } });
355
+ }
356
+ async function readProviderDetailForSearch(source, fqn, options) {
357
+ try {
358
+ return await readJson(catalogDetailSource(source, "providers", fqn), options);
359
+ }
360
+ catch {
361
+ return {};
362
+ }
363
+ }
364
+ async function searchCatalog(source, query, options) {
365
+ const providers = await readCatalog(source, options);
366
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
367
+ if (!terms.length)
368
+ return [];
369
+ const includeBlocked = hasFlag(options, "include-blocked");
370
+ const hits = [];
371
+ for (const provider of providers) {
372
+ if (provider.block && !includeBlocked)
373
+ continue;
374
+ const fqn = String(provider.fqn ?? provider.name ?? "");
375
+ if (!fqn)
376
+ continue;
377
+ const detail = await readProviderDetailForSearch(source, fqn, options);
378
+ const tags = stringList(detail.featured_tags ?? provider.featured_tags ?? detail.tags ?? provider.tags);
379
+ const endpoints = Array.isArray(detail.endpoints) ? detail.endpoints : Array.isArray(provider.endpoints) ? provider.endpoints : [];
380
+ const categoryMeta = detail.category_meta ?? provider.category_meta;
381
+ const chains = stringList(detail.chains ?? provider.chains);
382
+ const chainKinds = stringList(detail.chain_kinds ?? provider.chain_kinds);
383
+ const chainsMetaRaw = detail.chains_meta ?? provider.chains_meta ?? [];
384
+ const chainsMeta = Array.isArray(chainsMetaRaw) ? chainsMetaRaw.filter(item => item && typeof item === "object") : [];
385
+ const titleZh = String(detail.title_zh ?? provider.title_zh ?? "");
386
+ const mainTitle = String(detail.main_title ?? provider.main_title ?? detail.mainTitle ?? provider.mainTitle ?? "");
387
+ const subTitle = String(detail.sub_title ?? provider.sub_title ?? detail.subTitle ?? provider.subTitle ?? "");
388
+ const fields = {
389
+ fqn: [fqn],
390
+ title: [String(detail.title ?? provider.title ?? ""), mainTitle],
391
+ i18n: [titleZh, subTitle, ...dictValues(detail.i18n ?? provider.i18n)],
392
+ category: [String(detail.category ?? provider.category ?? "")],
393
+ category_meta: dictValues(categoryMeta),
394
+ chains: [...chains, ...chainMetaValues(chainsMeta)],
395
+ chain_kinds: chainKinds,
396
+ service_url: [String(detail.service_url ?? provider.service_url ?? detail.serviceUrl ?? provider.serviceUrl ?? "")],
397
+ description: [String(detail.description ?? provider.description ?? "")],
398
+ use_case: [String(detail.use_case ?? provider.use_case ?? detail.useCase ?? provider.useCase ?? "")],
399
+ tags,
400
+ endpoints: endpointFields(endpoints),
401
+ };
402
+ const scored = scoreFields(terms, fields);
403
+ if (scored.score === 0)
404
+ continue;
405
+ hits.push({
406
+ provider,
407
+ detail,
408
+ fqn,
409
+ title: fields.title[0],
410
+ category: fields.category[0],
411
+ serviceUrl: fields.service_url[0],
412
+ description: fields.description[0] || undefined,
413
+ useCase: fields.use_case[0] || undefined,
414
+ titleZh: titleZh || undefined,
415
+ mainTitle: mainTitle || undefined,
416
+ subTitle: subTitle || undefined,
417
+ categoryMeta: categoryMeta && typeof categoryMeta === "object" ? categoryMeta : undefined,
418
+ chains,
419
+ chainKinds,
420
+ chainsMeta,
421
+ tags,
422
+ endpoints,
423
+ score: scored.score,
424
+ matchedFields: scored.matchedFields,
425
+ });
426
+ }
427
+ return hits
428
+ .sort((a, b) => b.score - a.score || a.fqn.localeCompare(b.fqn))
429
+ .slice(0, positiveIntegerOption(options, "limit", 10));
430
+ }
431
+ function searchHitToJson(hit) {
432
+ return {
433
+ fqn: hit.fqn,
434
+ title: hit.title,
435
+ category: hit.category,
436
+ serviceUrl: hit.serviceUrl,
437
+ description: hit.description,
438
+ useCase: hit.useCase,
439
+ title_zh: hit.titleZh,
440
+ main_title: hit.mainTitle,
441
+ sub_title: hit.subTitle,
442
+ category_meta: hit.categoryMeta,
443
+ chains: hit.chains,
444
+ chain_kinds: hit.chainKinds,
445
+ chains_meta: hit.chainsMeta,
446
+ tags: hit.tags,
447
+ score: hit.score,
448
+ matchedFields: hit.matchedFields,
449
+ endpoints: hit.endpoints,
450
+ };
451
+ }
452
+ export async function catalogSearch(source, query, options) {
453
+ positiveIntegerOption(options, "limit", 10);
454
+ const hits = await searchCatalog(source, query, options);
455
+ const results = hits.map(searchHitToJson);
456
+ if (outputMode(options) === "json") {
457
+ emit({ command: "catalog search", mode: "json", result: { query, catalog: source, count: hits.length, results } });
458
+ return;
459
+ }
460
+ if (!hits.length) {
461
+ process.stdout.write("no matches\n");
462
+ return;
463
+ }
464
+ for (const hit of hits) {
465
+ const tags = hit.tags.length ? hit.tags.join(",") : "-";
466
+ process.stdout.write(`${hit.fqn.padEnd(32)} score=${String(hit.score).padEnd(3)} category=${hit.category.padEnd(12)} tags=${tags}\n`);
467
+ if (hit.title)
468
+ process.stdout.write(` ${hit.title}\n`);
469
+ if (hit.description)
470
+ process.stdout.write(` ${hit.description.split("\n")[0]}\n`);
471
+ if (hit.serviceUrl)
472
+ process.stdout.write(` service: ${hit.serviceUrl}\n`);
473
+ for (const endpoint of hit.endpoints.slice(0, 3)) {
474
+ const method = String(endpoint.method ?? "");
475
+ const pathText = String(endpoint.path ?? endpoint.url ?? "");
476
+ const paid = endpoint.paid;
477
+ let suffix = "";
478
+ if (paid && typeof paid === "object") {
479
+ suffix = ` ${paid.network ?? ""} ${paid.currency ?? ""} ${paid.amount_raw ?? ""}`.trimEnd();
480
+ }
481
+ process.stdout.write(` ${method.padEnd(6)} ${pathText}${suffix ? ` ${suffix}` : ""}\n`);
482
+ }
483
+ process.stdout.write("\n");
484
+ }
485
+ }
486
+ async function catalogShow(source, name, options) {
487
+ requireArgument(name, "provider", "x402-cli catalog show <provider> [--catalog <source>]");
488
+ const provider = await readCatalogProvider(source, name, options);
489
+ if (outputMode(options) === "json") {
490
+ emit({ command: "catalog show", mode: "json", result: provider });
491
+ return;
492
+ }
493
+ process.stdout.write(`${provider.fqn ?? provider.name} - ${provider.title ?? provider.main_title ?? provider.name}\n`);
494
+ if (provider.description)
495
+ process.stdout.write(`${String(provider.description).split("\n")[0]}\n`);
496
+ if (provider.category)
497
+ process.stdout.write(`category: ${provider.category}\n`);
498
+ if (provider.chains)
499
+ process.stdout.write(`chains: ${provider.chains.join(", ")}\n`);
500
+ }
501
+ async function catalogEndpoints(source, name, options) {
502
+ requireArgument(name, "provider", "x402-cli catalog endpoints <provider> [--catalog <source>]");
503
+ const provider = await readCatalogProvider(source, name, options);
504
+ const endpoints = provider.endpoints ?? [];
505
+ if (outputMode(options) === "json") {
506
+ emit({ command: "catalog endpoints", mode: "json", result: { provider: provider.fqn ?? provider.name, endpoints } });
507
+ return;
508
+ }
509
+ for (const endpoint of endpoints) {
510
+ process.stdout.write(`${String(endpoint.method ?? "").padEnd(6)} ${endpoint.path ?? endpoint.url ?? ""}\n`);
511
+ if (endpoint.description)
512
+ process.stdout.write(` ${String(endpoint.description).split("\n")[0]}\n`);
513
+ }
514
+ }
515
+ async function catalogPayJson(source, name, options) {
516
+ requireArgument(name, "provider", "x402-cli catalog pay-json <provider> [--catalog <source>]");
517
+ const provider = await readCatalogPayProvider(source, name, options);
518
+ const endpoint = (provider.endpoints ?? []).find((item) => item.paid || item.x402_routes?.length || item.x402Routes?.length) ?? provider.endpoints?.[0];
519
+ if (!endpoint)
520
+ throw new Error(`provider has no endpoints: ${name}`);
521
+ const result = {
522
+ provider: provider.fqn ?? provider.name,
523
+ url: endpoint.url ?? endpoint.path,
524
+ method: endpoint.method,
525
+ paid: endpoint.paid,
526
+ x402_routes: endpoint.x402_routes ?? endpoint.x402Routes ?? [],
527
+ endpoint,
528
+ };
529
+ if (hasFlag(options, "raw"))
530
+ printJson(result);
531
+ else
532
+ emit({ command: "catalog pay-json", mode: outputMode(options), result });
533
+ }
534
+ export async function handleCatalog(args) {
535
+ const { command, positional, options } = parseArgs(args);
536
+ if (hasFlag(options, "help") || ["help", "--help", "-h"].includes(command)) {
537
+ const topic = command === "help" ? positional[0] : command;
538
+ process.stdout.write(helpText(topic ? `catalog-${topic}` : "catalog"));
539
+ return;
540
+ }
541
+ const source = opt(options, "catalog", defaultCatalogSource());
542
+ if (command === "update")
543
+ await catalogUpdate(source, options);
544
+ else if (command === "search")
545
+ await catalogSearch(source, requireArgument(positional.join(" "), "query", "x402-cli catalog search <query> [options]"), options);
546
+ else if (command === "show")
547
+ await catalogShow(source, positional[0], options);
548
+ else if (command === "endpoints")
549
+ await catalogEndpoints(source, positional[0], options);
550
+ else if (command === "pay-json")
551
+ await catalogPayJson(source, positional[0], options);
552
+ else if (command === "export-gateway")
553
+ await catalogExportGateway(positional[0], options);
554
+ else if (command === "build")
555
+ catalogBuild(positional[0] ?? "providers", options);
556
+ else
557
+ throw new CliError("UNKNOWN_COMMAND", `Unknown catalog command: ${command}`, "Run x402-cli catalog --help to list commands.", 2);
558
+ }