Skip to content

Commit b433a70

Browse files
authored
Merge pull request #1013 from boboliu-1010/fix/x402-terminal-output
fix(cli): correct payment preflight, pagination and text output
2 parents 1efd6a4 + 86e5823 commit b433a70

63 files changed

Lines changed: 1103 additions & 175 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎ts/src/adapters/inbound/cli/arity/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ function applyArity(y: Argv, fields: ZodObject<ZodRawShape>): Argv {
134134
for (const f of introspectFields(fields)) {
135135
y.option(f.kebab, {
136136
type: f.baseType === "boolean" ? "boolean" : "string",
137+
...(f.kebab === "body-file" ? { nargs: 1 } : {}),
137138
...(f.isArray ? { array: true } : {}), // repeatable flag → yargs collects into an array
138139
describe: f.description,
139140
demandOption: false, // requiredness is enforced by zod, not yargs

‎ts/src/adapters/inbound/cli/commands/bai.ts‎

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { baiRechargeText, baiUsageText, baiRecordsText, baiOrdersText } from "../render/bai.js";
12
import {
23
assertBaiRechargeMinimum,
34
baiRechargeAmount,
@@ -57,10 +58,12 @@ const rechargeFields = z.object({
5758

5859
export const baiRechargeSpec: ChainSpec = {
5960
path: ["bai", "recharge"],
61+
formatText: baiRechargeText,
6062
network: "optional",
6163
wallet: "optional",
6264
auth: "conditional",
6365
broadcasts: true,
66+
supportsWait: false,
6467
capability: "bai.recharge",
6568
requires,
6669
positionals: [{ field: "amount" }],
@@ -141,6 +144,7 @@ export function registerBaiCommands(registry: CommandRegistry, service: BaiServi
141144
});
142145
registry.add({
143146
path: ["bai", "report-recharge"],
147+
formatText: baiRechargeText,
144148
network: "none",
145149
wallet: "none",
146150
auth: "none",
@@ -170,12 +174,19 @@ export function registerBaiCommands(registry: CommandRegistry, service: BaiServi
170174
examples: [
171175
{ cmd: "wallet-cli bai report-recharge 0x" + "a".repeat(64) + " --chain base --amount 1" },
172176
],
173-
run: async (_ctx, _network, input) => service.rechargeReport(input),
177+
run: async (ctx, _network, input) => {
178+
ctx.emit({
179+
type: "activity",
180+
message: "Submitting the existing transaction hash to B.AI; no new payment will be sent…",
181+
});
182+
return service.rechargeReport(input);
183+
},
174184
} satisfies CommandDefinition);
175185

176186
const empty = z.object({});
177187
registry.add({
178188
path: ["bai", "usage-summary"],
189+
formatText: baiUsageText,
179190
network: "none",
180191
wallet: "none",
181192
auth: "none",
@@ -186,7 +197,10 @@ export function registerBaiCommands(registry: CommandRegistry, service: BaiServi
186197
fields: empty,
187198
input: empty,
188199
examples: [{ cmd: "wallet-cli bai usage-summary" }],
189-
run: async () => service.usage(),
200+
run: async (ctx) => {
201+
ctx.emit({ type: "activity", message: "Fetching B.AI credit balance and usage summary…" });
202+
return service.usage();
203+
},
190204
} satisfies CommandDefinition);
191205

192206
const usageListFields = listFields.extend({
@@ -199,6 +213,7 @@ export function registerBaiCommands(registry: CommandRegistry, service: BaiServi
199213
});
200214
registry.add({
201215
path: ["bai", "usage-records"],
216+
formatText: baiRecordsText,
202217
network: "none",
203218
wallet: "none",
204219
auth: "none",
@@ -207,11 +222,15 @@ export function registerBaiCommands(registry: CommandRegistry, service: BaiServi
207222
fields: usageListFields,
208223
input: usageListFields,
209224
examples: [{ cmd: "wallet-cli bai usage-records --limit 20" }],
210-
run: async (_context, _network, input) => service.usageList(input),
225+
run: async (ctx, _network, input) => {
226+
ctx.emit({ type: "activity", message: "Fetching B.AI usage records…" });
227+
return service.usageList(input);
228+
},
211229
} satisfies CommandDefinition);
212230

213231
registry.add({
214232
path: ["bai", "recharge-orders"],
233+
formatText: baiOrdersText,
215234
network: "none",
216235
wallet: "none",
217236
auth: "none",
@@ -221,6 +240,7 @@ export function registerBaiCommands(registry: CommandRegistry, service: BaiServi
221240
input: listFields,
222241
examples: [{ cmd: "wallet-cli bai recharge-orders --limit 20" }],
223242
run: async (context, _network, input) => {
243+
context.emit({ type: "activity", message: "Fetching B.AI recharge orders…" });
224244
const { warnings, ...result } = await service.rechargeList(input);
225245
for (const warning of warnings ?? []) context.warn(warning);
226246
return result;

‎ts/src/adapters/inbound/cli/commands/erc8004.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { agentShowText } from "../render/x402.js";
1+
import { agentShowText, operatorCheckText } from "../render/x402.js";
22
import { z } from "zod";
33
import type { ChainSpec, FamilyBinding } from "../contracts/index.js";
44
import type { AgentService } from "../../../../application/use-cases/agent-service.js";
55
import { Schemas, addressFamilyFieldsFor } from "../schemas/index.js";
66
import { governanceTxRefine, tronTxModeFields, txModeFields } from "./shared.js";
7-
import { TextFormatters, renderGenericText } from "../render/index.js";
7+
import { TextFormatters } from "../render/index.js";
88

99
const agentId = z
1010
.string()
@@ -152,7 +152,7 @@ export const operatorCheckSpec: ChainSpec = {
152152
wallet: "none",
153153
auth: "none",
154154
capability: "erc8004.identity.read",
155-
formatText: (data, ctx) => renderGenericText(ctx.command, ctx.net, data),
155+
formatText: operatorCheckText,
156156
positionals: [{ field: "owner" }, { field: "operator" }],
157157
summary: "Check an owner-wide Agent operator approval",
158158
baseFields: z.object({
@@ -181,8 +181,8 @@ export function showEvmBinding(service: AgentService): FamilyBinding {
181181
return binding(
182182
"evm",
183183
async (ctx, net, input) => {
184-
const result = await service.show(net, input.id);
185-
for (const warning of result.warnings ?? []) ctx.warn(warning);
184+
const { warnings, ...result } = await service.show(net, input.id);
185+
for (const warning of warnings ?? []) ctx.warn(warning);
186186
return result;
187187
},
188188
[],
@@ -193,8 +193,8 @@ export function showTronBinding(service: AgentService): FamilyBinding {
193193
return binding(
194194
"tron",
195195
async (ctx, net, input) => {
196-
const result = await service.show(net, input.id);
197-
for (const warning of result.warnings ?? []) ctx.warn(warning);
196+
const { warnings, ...result } = await service.show(net, input.id);
197+
for (const warning of warnings ?? []) ctx.warn(warning);
198198
return result;
199199
},
200200
[],

‎ts/src/adapters/inbound/cli/commands/wallet.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,8 @@ export function registerWalletCommands(
599599
examples: [{ cmd: "wallet-cli delete old --yes" }],
600600
formatText: TextFormatters.walletDelete,
601601
run: async (ctx, _net, input) => {
602+
const deletionWarning = wallets.deletionWarning(input.account);
603+
if (deletionWarning) ctx.warn(deletionWarning);
602604
if (!input.yes) {
603605
if (!ctx.prompt.isTTY()) {
604606
throw new UsageError(

‎ts/src/adapters/inbound/cli/commands/x402.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,13 @@ describe("x402 command surface", () => {
8787
).toBe(false);
8888
});
8989
});
90+
91+
it("does not advertise unsupported provider type or facilitator waiting", () => {
92+
const registry = new CommandRegistry();
93+
registerX402Commands(registry, service());
94+
expect(registry.resolveNeutral(["x402", "provider-list"])!.fields.shape).not.toHaveProperty(
95+
"type",
96+
);
97+
for (const command of ["pay", "roundtrip"])
98+
expect(registry.resolveNeutral(["x402", command])!.supportsWait).toBe(false);
99+
});

‎ts/src/adapters/inbound/cli/commands/x402.ts‎

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import {
22
paymentText,
3+
roundtripText,
4+
catalogUpdateText,
5+
serveText,
36
providerListText,
47
providerShowText,
58
providerEndpointsText,
@@ -89,6 +92,7 @@ const payCommand: CommandDefinition = {
8992
wallet: "optional",
9093
auth: "conditional",
9194
broadcasts: true,
95+
supportsWait: false,
9296
capability: "x402.pay",
9397
positionals: [{ field: "url" }],
9498
summary: "Request an endpoint and pay an x402 challenge",
@@ -115,12 +119,24 @@ const payCommand: CommandDefinition = {
115119
};
116120

117121
const listFields = z.object({
118-
limit: integerLiteral(1, 200).default(20),
119-
offset: integerLiteral(0, Number.MAX_SAFE_INTEGER).default(0),
120-
type: z.string().trim().min(1).optional(),
121-
category: z.string().trim().min(1).optional(),
122-
capability: z.string().trim().min(1).optional(),
123-
network: z.string().trim().min(1).optional().describe("CAIP-2 network id"),
122+
limit: integerLiteral(1, 200).default(20).describe("maximum providers to return"),
123+
offset: integerLiteral(0, Number.MAX_SAFE_INTEGER)
124+
.default(0)
125+
.describe("zero-based pagination offset"),
126+
127+
category: z
128+
.string()
129+
.trim()
130+
.min(1)
131+
.optional()
132+
.describe("filter by a category exposed by the provider catalog"),
133+
capability: z
134+
.string()
135+
.trim()
136+
.min(1)
137+
.optional()
138+
.describe("filter by a featured tag exposed by the provider catalog"),
139+
network: z.string().trim().min(1).optional().describe("network alias or CAIP-2 network id"),
124140
includeBlocked: z.boolean().default(false).describe("include providers marked as blocked"),
125141
});
126142

@@ -178,7 +194,8 @@ const roundtripFields = serveFields.omit({ host: true, resourceUrl: true, daemon
178194
const roundtripInput = roundtripFields
179195
.superRefine(serveRefinement)
180196
.refine((value) => !(value.maxGasfreeFee !== undefined && value.maxGasfreeFeeRaw !== undefined), {
181-
message: "GasFree fee limits are mutually exclusive",
197+
path: ["maxGasfreeFee"],
198+
message: "cannot be combined with --max-gasfree-fee-raw",
182199
params: { errorCode: "invalid_option" },
183200
});
184201

@@ -212,6 +229,7 @@ export function registerX402Commands(registry: CommandRegistry, service: X402Ser
212229

213230
registry.add({
214231
path: ["x402", "serve"],
232+
formatText: serveText,
215233
network: "optional",
216234
wallet: "none",
217235
auth: "none",
@@ -225,16 +243,21 @@ export function registerX402Commands(registry: CommandRegistry, service: X402Ser
225243
],
226244
run: async (_ctx, network, input) => {
227245
if (!network) throw new Error("x402 serve requires a resolved network");
228-
return service.serve(network, input);
246+
return service.serve(network, {
247+
...input,
248+
...(_ctx.output === "text" ? { accessLog: "text" as const } : {}),
249+
});
229250
},
230251
} satisfies CommandDefinition);
231252

232253
registry.add({
233254
path: ["x402", "roundtrip"],
255+
formatText: roundtripText,
234256
network: "optional",
235257
wallet: "optional",
236258
auth: "conditional",
237259
broadcasts: true,
260+
supportsWait: false,
238261
capability: "x402.pay",
239262
summary: "Start a local paywall, pay it, and exit",
240263
fields: roundtripFields,
@@ -257,7 +280,7 @@ export function registerX402Commands(registry: CommandRegistry, service: X402Ser
257280
input: listFields,
258281
examples: [
259282
{ cmd: "wallet-cli x402 provider-list" },
260-
{ cmd: "wallet-cli x402 provider-list --network tron:728126428 --capability recharge" },
283+
{ cmd: "wallet-cli x402 provider-list --network tron:728126428" },
261284
],
262285
run: async (_ctx, _network, input) => providerResult(_ctx, service.providerList(input)),
263286
} satisfies CommandDefinition);
@@ -281,14 +304,15 @@ export function registerX402Commands(registry: CommandRegistry, service: X402Ser
281304
summary,
282305
fields,
283306
input: fields,
284-
examples: [{ cmd: `wallet-cli x402 ${verb} bai/recharge` }],
307+
examples: [{ cmd: `wallet-cli x402 ${verb} defillama` }],
285308
run: async (_ctx, _network, input) => providerResult(_ctx, run(input.provider)),
286309
} satisfies CommandDefinition);
287310
}
288311

289312
const empty = z.object({});
290313
registry.add({
291314
path: ["x402", "update-catalog"],
315+
formatText: catalogUpdateText,
292316
network: "none",
293317
wallet: "none",
294318
auth: "none",
@@ -337,6 +361,7 @@ async function providerResult(
337361
ctx: Parameters<CommandDefinition["run"]>[0],
338362
pending: Promise<Record<string, unknown>>,
339363
) {
364+
ctx.emit({ type: "activity", message: "Loading provider catalog data…" });
340365
const { warnings, ...data } = await pending;
341366
if (Array.isArray(warnings))
342367
for (const warning of warnings) if (typeof warning === "string") ctx.warn(warning);

‎ts/src/adapters/inbound/cli/contracts/command.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ interface CommandDefinitionBase<I, O> {
5858
auth: AuthRequirement;
5959
/** broadcasts a transaction on-chain (✍️); enables the --wait global flag in help projection. */
6060
broadcasts?: boolean;
61+
/** Override waiting support for payments submitted by an external facilitator. */
62+
supportsWait?: boolean;
6163
/** opt-in interactive master-password handling: "establish" = set on first wallet else verify; "verify" = require existing. Commands without this keep the lazy hasMasterPassword guard. */
6264
passwordMode?: "establish" | "verify";
6365
/** expose one or more `fields` entries as leading positionals (`block [<number>]`, `use [<account>]`,
@@ -140,6 +142,8 @@ export interface ChainSpec<_I = any, O = any> {
140142
wallet: WalletRequirement;
141143
auth: AuthRequirement;
142144
broadcasts?: boolean;
145+
/** Override waiting support for payments submitted by an external facilitator. */
146+
supportsWait?: boolean;
143147
capability?: string;
144148
stdin?: StdinChannel;
145149
/** the stdin channel belongs to ONE family (e.g. `--tx-stdin` carries TRON's transaction JSON).
@@ -179,6 +183,7 @@ export type CommandExecutionSpec = Pick<
179183
| "wallet"
180184
| "auth"
181185
| "broadcasts"
186+
| "supportsWait"
182187
| "capability"
183188
| "interactive"
184189
| "passwordMode"

‎ts/src/adapters/inbound/cli/contracts/envelope.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface Pagination {
2222
offset: number;
2323
limit: number | null;
2424
total: number | null;
25+
hasMore?: boolean;
26+
nextCursor?: string | null;
2527
}
2628
export interface Meta {
2729
durationMs: number;

‎ts/src/adapters/inbound/cli/help/index.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ export class HelpService {
294294
auth: cmd.auth,
295295
wallet: cmd.wallet,
296296
broadcasts: cmd.broadcasts,
297+
supportsWait: cmd.supportsWait,
297298
fields: introspectFields(cmd.fields),
298299
inputFlags: inputFlagsFor(cmd),
299300
exclusive: cmd.exclusive,
@@ -316,6 +317,7 @@ export class HelpService {
316317
auth: spec.auth,
317318
wallet: spec.wallet,
318319
broadcasts: spec.broadcasts,
320+
supportsWait: spec.supportsWait,
319321
fields: introspectFields(mergedFields(def)),
320322
fieldFamilies: fieldFamilies(def),
321323
inputFlags: spec.stdin ? inputFlagsFor(spec) : [],
@@ -337,6 +339,7 @@ export class HelpService {
337339
auth: CommandDefinition["auth"];
338340
wallet: CommandDefinition["wallet"];
339341
broadcasts?: boolean;
342+
supportsWait?: boolean;
340343
fields: FieldInfo[];
341344
/** family-specific flags, so each can be marked with the family it belongs to. */
342345
fieldFamilies?: Map<string, ChainFamily>;
@@ -471,7 +474,7 @@ export class HelpService {
471474
c.network,
472475
c.auth,
473476
c.wallet,
474-
c.broadcasts ?? false,
477+
c.supportsWait ?? c.broadcasts ?? false,
475478
c.secretsTtyOnly ?? false,
476479
))
477480
lines.push(globalFlagLine(g));

‎ts/src/adapters/inbound/cli/input/secret/index.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ export class SecretResolver implements ISecretResolver {
197197
}
198198
this.#stdinUsedBy = kind;
199199
if (!this.#byPath.has("-")) {
200+
if (kind === "apiKey" && process.stdin.isTTY)
201+
this.streams.diagnostic(
202+
"info",
203+
"Reading API key from stdin; finish input with Ctrl-D. Use a pipe for non-interactive input.",
204+
);
200205
this.#byPath.set("-", this.streams.readStdinOnce().replace(/\r?\n$/, ""));
201206
}
202207
return this.#byPath.get("-")!;

0 commit comments

Comments
 (0)