@itpay/cli 0.2.17 → 2.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +148 -447
  2. package/bin/itp +1 -150
  3. package/dist/src/client/backend.js +154 -0
  4. package/dist/src/client/http.js +76 -0
  5. package/dist/src/client/types.js +4 -0
  6. package/dist/src/commands/buy.js +351 -0
  7. package/dist/src/commands/cart.js +264 -0
  8. package/dist/src/commands/catalog.js +26 -0
  9. package/dist/src/commands/checkout.js +106 -0
  10. package/dist/src/commands/docs.js +61 -0
  11. package/dist/src/commands/guidance.js +422 -0
  12. package/dist/src/commands/install.js +95 -0
  13. package/dist/src/commands/order.js +78 -0
  14. package/dist/src/commands/orders.js +22 -0
  15. package/dist/src/commands/pay.js +26 -0
  16. package/dist/src/commands/readyz.js +8 -0
  17. package/dist/src/commands/refund.js +20 -0
  18. package/dist/src/commands/services.js +317 -0
  19. package/dist/src/main.js +606 -0
  20. package/dist/src/render/feishu.js +201 -0
  21. package/dist/src/render/ide.js +321 -0
  22. package/dist/src/render/index.js +57 -0
  23. package/dist/src/render/interaction.js +49 -0
  24. package/dist/src/render/markdown.js +83 -0
  25. package/dist/src/render/output.js +42 -0
  26. package/dist/src/render/plain_chat.js +60 -0
  27. package/dist/src/render/plan.js +31 -0
  28. package/dist/src/render/qr.js +32 -0
  29. package/dist/src/render/sink.js +6 -0
  30. package/dist/src/render/status.js +37 -0
  31. package/dist/src/render/telegram.js +172 -0
  32. package/dist/src/render/terminal.js +148 -0
  33. package/dist/src/render/terminal_image.js +19 -0
  34. package/dist/src/state/cart_session.js +151 -0
  35. package/dist/src/state/client_context.js +73 -0
  36. package/dist/src/state/config.js +82 -0
  37. package/dist/src/state/device_authority.js +217 -0
  38. package/dist/src/state/operation_journal.js +80 -0
  39. package/docs/agent/buyer/cart-checkout.json +56 -94
  40. package/docs/agent/buyer/catalog-list.json +47 -0
  41. package/docs/agent/buyer/install-and-setup.json +82 -0
  42. package/docs/agent/buyer/orders-refunds.json +76 -0
  43. package/docs/agent/buyer/payment-flow.json +77 -0
  44. package/docs/agent/buyer/quickstart.json +143 -75
  45. package/docs/agent/buyer/render-hosts.json +79 -0
  46. package/package.json +32 -13
  47. package/skills/itpay-buyer/SKILL.md +107 -238
  48. package/docs/agent/buyer/account-portal.json +0 -81
  49. package/docs/agent/buyer/catalog-search.json +0 -106
  50. package/docs/agent/buyer/human-claim-ui.json +0 -77
  51. package/docs/agent/buyer/payment-qr.json +0 -97
  52. package/docs/agent/buyer/payment-wait.json +0 -84
  53. package/docs/agent/buyer/product-recommendation.json +0 -80
  54. package/docs/agent/buyer/qr-refresh.json +0 -67
  55. package/docs/agent/buyer/recovery.json +0 -85
  56. package/docs/agent/buyer/safety-policy.json +0 -70
  57. package/docs/agent/buyer/secure-delivery.json +0 -90
  58. package/docs/agent/buyer/vault-agent-read.json +0 -95
  59. package/install.ps1 +0 -65
  60. package/install.sh +0 -66
  61. package/lib/account-status.js +0 -157
  62. package/lib/buyer.js +0 -2332
  63. package/lib/client-context.js +0 -126
  64. package/lib/docs.js +0 -200
  65. package/lib/env.js +0 -723
  66. package/lib/http.js +0 -151
  67. package/lib/ops.js +0 -135
  68. package/lib/render-human.js +0 -718
  69. package/lib/runtime.js +0 -1456
@@ -0,0 +1,606 @@
1
+ // V3 CLI entrypoint. Each command maps 1:1 to a route family in
2
+ // services/backend/internal/httpapi/handlers/*.go. Commands only
3
+ // orchestrate; HTTP and rendering live in src/client and src/render.
4
+ import { Command } from "commander";
5
+ import { loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
6
+ import { CartSession } from "./state/cart_session.js";
7
+ import { defaultHostForAgentType, normalizeHost } from "./state/client_context.js";
8
+ import { HttpError } from "./client/http.js";
9
+ import { runReadyz } from "./commands/readyz.js";
10
+ import { runBuy } from "./commands/buy.js";
11
+ import { runCatalogList } from "./commands/catalog.js";
12
+ import { runCheckoutPresentation } from "./commands/checkout.js";
13
+ import { runPay } from "./commands/pay.js";
14
+ import { runOrder } from "./commands/order.js";
15
+ import { runListOrders } from "./commands/orders.js";
16
+ import { runRefund } from "./commands/refund.js";
17
+ import { runCartAdd, runCartAddServer, runCartAbandonServer, runCartClear, runCartNext, runCartRemove, runCartRemoveServer, runCartShowServer, } from "./commands/cart.js";
18
+ import { printErrorRecovery } from "./commands/guidance.js";
19
+ import { runDocsList, runDocsShow, runDocsSearch } from "./commands/docs.js";
20
+ import { runInstall } from "./commands/install.js";
21
+ import { collectOption, parseKeyValueList, runServicesAction, runServicesCheckout, runServicesEvents, runServicesGet, runServicesInvoke, runServicesList, runServicesNext, runServicesReadResult, runServicesStart, } from "./commands/services.js";
22
+ const program = new Command();
23
+ program
24
+ .name("itpay")
25
+ .description("V3 ItPay CLI — checkout, payment, order, and refund commands")
26
+ .option("--agent-type <type>", "agent runtime type used for device enrollment and client-specific guidance")
27
+ .version("2.0.0-rc.2");
28
+ function withHost(value) {
29
+ const host = normalizeHost(value);
30
+ if (!host) {
31
+ throw new Error(`invalid --host "${value ?? ""}". Supported: terminal, codex, claude-code, telegram, discord, whatsapp, feishu, lark, plain-chat`);
32
+ }
33
+ return host;
34
+ }
35
+ function parseRequiredContactFields(value) {
36
+ if (!value) {
37
+ return undefined;
38
+ }
39
+ const parsed = value
40
+ .split(",")
41
+ .map((item) => item.trim().toLowerCase())
42
+ .filter((item) => item === "email" || item === "phone");
43
+ return parsed.length > 0 ? parsed : undefined;
44
+ }
45
+ function resolveCheckoutPresentationArgs(input) {
46
+ if (input.requestedCheckoutID && input.requestedDisplayToken) {
47
+ return { checkoutID: input.requestedCheckoutID, displayToken: input.requestedDisplayToken };
48
+ }
49
+ if (input.requestedCheckoutID && !input.requestedDisplayToken) {
50
+ if (input.savedCheckoutID === input.requestedCheckoutID && input.savedDisplayToken) {
51
+ return { checkoutID: input.requestedCheckoutID, displayToken: input.savedDisplayToken };
52
+ }
53
+ const savedHint = input.savedCheckoutID ? ` Saved checkout is ${input.savedCheckoutID}.` : "";
54
+ throw new Error(`display token is required for checkout ${input.requestedCheckoutID}.${savedHint} ` +
55
+ "Pass --token for that checkout or run `itpay checkout` without --id to use the saved checkout.");
56
+ }
57
+ if (input.requestedDisplayToken) {
58
+ if (input.savedCheckoutID) {
59
+ return { checkoutID: input.savedCheckoutID, displayToken: input.requestedDisplayToken };
60
+ }
61
+ throw new Error("checkout id is required when --token is provided and no saved checkout exists");
62
+ }
63
+ if (input.savedCheckoutID && input.savedDisplayToken) {
64
+ return { checkoutID: input.savedCheckoutID, displayToken: input.savedDisplayToken };
65
+ }
66
+ throw new Error("checkout id and display token are required; pass --id/--token or create a checkout first");
67
+ }
68
+ function reportCLIError(error) {
69
+ if (error instanceof HttpError) {
70
+ process.stderr.write(`[${error.status}] ${error.code}: ${error.message}\n`);
71
+ printErrorRecovery(error, (text) => process.stderr.write(text));
72
+ process.exitCode = 1;
73
+ return;
74
+ }
75
+ if (error instanceof Error) {
76
+ process.stderr.write(`${error.message}\n`);
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ throw error;
81
+ }
82
+ program
83
+ .command("readyz")
84
+ .description("Probe the V3 backend readiness endpoint")
85
+ .action(async () => {
86
+ await withBackend(async (backend) => runReadyz(backend));
87
+ });
88
+ program
89
+ .command("next")
90
+ .description("Show the next recommended agent action from remembered server handles")
91
+ .option("--json", "output JSON instead of terminal text")
92
+ .action(async (options) => {
93
+ const config = loadConfig();
94
+ const backend = newBackendClient(config);
95
+ const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
96
+ try {
97
+ await runCartNext(backend, session, { jsonOutput: Boolean(options.json) });
98
+ }
99
+ catch (error) {
100
+ reportCLIError(error);
101
+ }
102
+ });
103
+ // --- catalog --------------------------------------------------------------
104
+ const catalogCmd = program.command("catalog").description("Browse V3 service catalog");
105
+ catalogCmd
106
+ .command("list")
107
+ .description("List all available services from the published catalog manifest")
108
+ .option("--json", "output JSON instead of terminal text")
109
+ .action(async (options) => {
110
+ await withBackend(async (backend) => runCatalogList(backend, { jsonOutput: Boolean(options.json) }));
111
+ });
112
+ // --- install --------------------------------------------------------------
113
+ const installCmd = program.command("install").description("Show setup instructions for each agent host");
114
+ installCmd
115
+ .argument("[target]", "install target: claude-code, codex, terminal, telegram, feishu")
116
+ .description("Show agent-specific installation and configuration instructions")
117
+ .action((target) => {
118
+ runInstall(target);
119
+ });
120
+ // --- docs -----------------------------------------------------------------
121
+ const docsCmd = program.command("docs").description("Browse agent documentation");
122
+ docsCmd
123
+ .command("list")
124
+ .description("List all available agent doc topics")
125
+ .action(() => {
126
+ runDocsList();
127
+ });
128
+ docsCmd
129
+ .command("show")
130
+ .description("Show a specific doc topic")
131
+ .argument("<topic>", "doc topic name")
132
+ .action((topic) => {
133
+ runDocsShow(topic);
134
+ });
135
+ docsCmd
136
+ .command("search")
137
+ .description("Search doc topics by keyword")
138
+ .argument("<query>", "search query")
139
+ .action((query) => {
140
+ runDocsSearch(query);
141
+ });
142
+ // --- cart ----------------------------------------------------------------
143
+ const cart = program.command("cart").description("V3 canonical server cart");
144
+ cart
145
+ .command("add")
146
+ .description("Add a variant/offer/quantity to the canonical server cart")
147
+ .requiredOption("--item <catalog_item_id>")
148
+ .requiredOption("--variant <catalog_variant_id>")
149
+ .requiredOption("--offer <offer_id>")
150
+ .option("--quantity <n>", "quantity", (value) => Number.parseInt(value, 10), 1)
151
+ .option("--input <json>")
152
+ .option("--host <host>", "client host (terminal, codex, telegram, feishu, lark, ...)", "terminal")
153
+ .option("--target <target>", "chat id / channel id / open id for IM hosts")
154
+ .option("--json", "output JSON instead of terminal text")
155
+ .option("--local", "only add to the local draft cache; not valid for service-backed flows")
156
+ .action(async (options) => {
157
+ const config = loadConfig();
158
+ const sessionPath = cartSessionPath();
159
+ const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
160
+ const addOptions = {
161
+ catalogItemID: options.item,
162
+ catalogVariantID: options.variant,
163
+ offerID: options.offer,
164
+ quantity: options.quantity,
165
+ ...(options.input ? { input: JSON.parse(options.input) } : {}),
166
+ };
167
+ try {
168
+ if (options.local) {
169
+ runCartAdd(session, addOptions);
170
+ }
171
+ else {
172
+ const backend = newBackendClient(config);
173
+ await runCartAddServer({
174
+ ...addOptions,
175
+ backend,
176
+ config,
177
+ session,
178
+ host: withHost(options.host),
179
+ ...(options.target ? { target: options.target } : {}),
180
+ jsonOutput: Boolean(options.json),
181
+ });
182
+ }
183
+ }
184
+ catch (error) {
185
+ reportCLIError(error);
186
+ }
187
+ finally {
188
+ session.saveToFile(sessionPath);
189
+ }
190
+ });
191
+ cart
192
+ .command("next")
193
+ .description("Show the next recommended agent action for the remembered server cart")
194
+ .option("--json", "output JSON instead of terminal text")
195
+ .action(async (options) => {
196
+ const config = loadConfig();
197
+ const backend = newBackendClient(config);
198
+ const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
199
+ try {
200
+ await runCartNext(backend, session, { jsonOutput: Boolean(options.json) });
201
+ }
202
+ catch (error) {
203
+ reportCLIError(error);
204
+ }
205
+ });
206
+ cart
207
+ .command("remove")
208
+ .description("Remove a line from the canonical server cart")
209
+ .option("--line <cart_item_id>", "server cart item id; defaults to last remembered cart item")
210
+ .option("--variant <catalog_variant_id>", "local draft variant id, only with --local")
211
+ .option("--offer <offer_id>", "local draft offer id, only with --local")
212
+ .option("--local", "only remove from the explicit local draft cache")
213
+ .action(async (options) => {
214
+ const config = loadConfig();
215
+ const sessionPath = cartSessionPath();
216
+ const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
217
+ try {
218
+ if (options.local) {
219
+ if (!options.variant || !options.offer) {
220
+ throw new Error("--local remove requires --variant and --offer");
221
+ }
222
+ runCartRemove(session, {
223
+ catalogVariantID: options.variant,
224
+ offerID: options.offer,
225
+ });
226
+ }
227
+ else {
228
+ const backend = newBackendClient(config);
229
+ await runCartRemoveServer(backend, session, options.line);
230
+ }
231
+ }
232
+ catch (error) {
233
+ reportCLIError(error);
234
+ }
235
+ finally {
236
+ session.saveToFile(sessionPath);
237
+ }
238
+ });
239
+ cart
240
+ .command("show")
241
+ .description("Print the canonical server cart or local draft fallback")
242
+ .action(async () => {
243
+ const config = loadConfig();
244
+ const backend = newBackendClient(config);
245
+ const sessionPath = cartSessionPath();
246
+ const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
247
+ try {
248
+ await runCartShowServer(backend, session);
249
+ }
250
+ catch (error) {
251
+ reportCLIError(error);
252
+ }
253
+ });
254
+ cart
255
+ .command("clear")
256
+ .description("Abandon the canonical server cart or clear local draft fallback")
257
+ .option("--local", "only clear local handles and explicit local draft")
258
+ .action(async (options) => {
259
+ const config = loadConfig();
260
+ const sessionPath = cartSessionPath();
261
+ const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
262
+ try {
263
+ if (options.local) {
264
+ runCartClear(session);
265
+ }
266
+ else {
267
+ const backend = newBackendClient(config);
268
+ await runCartAbandonServer(backend, session);
269
+ }
270
+ }
271
+ catch (error) {
272
+ reportCLIError(error);
273
+ }
274
+ finally {
275
+ session.saveToFile(sessionPath);
276
+ }
277
+ });
278
+ // --- buy / checkout ------------------------------------------------------
279
+ program
280
+ .command("buy")
281
+ .description("Create a V3 cart and checkout, then render the checkout QR for the host")
282
+ .option("--host <host>", "client host (terminal, telegram, feishu, lark, ...)", "terminal")
283
+ .option("--target <target>", "chat id / channel id / open id for IM hosts")
284
+ .option("--item <catalog_item_id>")
285
+ .option("--variant <catalog_variant_id>")
286
+ .option("--offer <offer_id>")
287
+ .option("--cart <cart_id>", "existing canonical server cart id")
288
+ .option("--quantity <n>", "quantity", (value) => Number.parseInt(value, 10), 1)
289
+ .option("--ref <client_reference_id>")
290
+ .option("--contact-email <email>")
291
+ .option("--contact-phone <phone>")
292
+ .option("--require-contact <fields>", "comma-separated required contact fields: email,phone")
293
+ .option("--qr-format <format>", "unicode|utf8|ansi|terminal")
294
+ .option("--qr-file <path>", "explicit QR file path")
295
+ .option("--pay", "also create a payment intent and optionally wait for verification")
296
+ .option("--method <alipay|wechatpay>", "payment method for --pay", "alipay")
297
+ .option("--no-wait", "do not wait for payment verification after --pay")
298
+ .option("--timeout <seconds>", "max seconds to wait for payment", (value) => Number.parseInt(value, 10), 120)
299
+ .option("--json", "output JSON instead of terminal text")
300
+ .action(async (options) => {
301
+ const config = loadConfig();
302
+ const backend = newBackendClient(config);
303
+ const sessionPath = cartSessionPath();
304
+ const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
305
+ const host = withHost(options.host);
306
+ if (options.item && options.variant && options.offer) {
307
+ runCartAdd(session, {
308
+ catalogItemID: options.item,
309
+ catalogVariantID: options.variant,
310
+ offerID: options.offer,
311
+ quantity: options.quantity,
312
+ });
313
+ session.saveToFile(sessionPath);
314
+ }
315
+ const contact = {};
316
+ if (options.contactEmail)
317
+ contact.email = options.contactEmail;
318
+ if (options.contactPhone)
319
+ contact.phone = options.contactPhone;
320
+ const requiredContactFields = parseRequiredContactFields(options.requireContact);
321
+ const method = options.method === "wechatpay" ? "wechatpay" : "alipay";
322
+ const buyOptions = {
323
+ cartSession: session,
324
+ host,
325
+ ...(options.cart ? { cartID: options.cart } : {}),
326
+ ...(options.target ? { target: options.target } : {}),
327
+ ...(options.ref ? { clientReferenceID: options.ref } : {}),
328
+ ...(Object.keys(contact).length > 0 ? { contact } : {}),
329
+ ...(requiredContactFields ? { requiredContactFields } : {}),
330
+ ...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
331
+ ...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
332
+ ...(options.pay ? { pay: true, payMethod: method, noWait: options.wait === false, payTimeoutSec: options.timeout } : {}),
333
+ ...(options.json ? { jsonOutput: true } : {}),
334
+ };
335
+ try {
336
+ await runBuy(backend, config, buyOptions);
337
+ }
338
+ catch (error) {
339
+ reportCLIError(error);
340
+ }
341
+ finally {
342
+ session.saveToFile(sessionPath);
343
+ }
344
+ });
345
+ program
346
+ .command("checkout")
347
+ .description("Read the canonical V3 checkout presentation by checkout_id + display_token")
348
+ .option("--host <host>", "client host")
349
+ .option("--target <target>")
350
+ .option("--id <checkout_id>")
351
+ .option("--token <display_token>")
352
+ .action(async (options) => {
353
+ const config = loadConfig();
354
+ const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
355
+ const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
356
+ const snap = session.show();
357
+ const { checkoutID, displayToken } = resolveCheckoutPresentationArgs({
358
+ ...(options.id ? { requestedCheckoutID: options.id } : {}),
359
+ ...(options.token ? { requestedDisplayToken: options.token } : {}),
360
+ ...(snap.lastCheckoutID ? { savedCheckoutID: snap.lastCheckoutID } : {}),
361
+ ...(snap.lastDisplayToken ? { savedDisplayToken: snap.lastDisplayToken } : {}),
362
+ });
363
+ const backend = newBackendClient(config);
364
+ try {
365
+ await runCheckoutPresentation(backend, {
366
+ checkoutID,
367
+ displayToken,
368
+ host,
369
+ baseURL: config.baseURL,
370
+ });
371
+ }
372
+ catch (error) {
373
+ if (error instanceof HttpError && error.status === 404 && snap.lastServiceExecutionID) {
374
+ process.stderr.write(`[${error.status}] ${error.code}: ${error.message}\n`);
375
+ process.stderr.write("recovery:\n");
376
+ process.stderr.write(" - Reissue the existing Service Execution checkout handoff\n");
377
+ process.stderr.write(` itpay services checkout ${snap.lastServiceExecutionID} --resume --json\n`);
378
+ process.exitCode = 1;
379
+ return;
380
+ }
381
+ reportCLIError(error);
382
+ }
383
+ });
384
+ program
385
+ .command("pay")
386
+ .description("Create a V3 payment intent (CLI escape hatch — usually done by the checkout page)")
387
+ .requiredOption("--checkout <checkout_id>")
388
+ .requiredOption("--method <alipay|wechatpay>")
389
+ .option("--provider <name>")
390
+ .option("--buyer <buyer_id>")
391
+ .option("--refresh", "request a fresh provider payment action for the existing intent")
392
+ .action(async (options) => {
393
+ const config = loadConfig();
394
+ const backend = newBackendClient(config);
395
+ const method = options.method === "wechatpay" ? "wechatpay" : "alipay";
396
+ const payOptions = {
397
+ checkoutID: options.checkout,
398
+ method,
399
+ ...(options.provider ? { preferredProvider: options.provider } : {}),
400
+ ...(options.buyer ? { buyerID: options.buyer } : {}),
401
+ ...(options.refresh ? { refreshAction: true } : {}),
402
+ };
403
+ await runPay(backend, config, payOptions);
404
+ });
405
+ program
406
+ .command("order")
407
+ .description("Read a V3 order by id")
408
+ .argument("<order_id>")
409
+ .option("--host <host>", "client host")
410
+ .action(async (orderID, options) => {
411
+ const host = withHost(options.host ?? "terminal");
412
+ await withBackend(async (backend) => runOrder(backend, orderID, { host }));
413
+ });
414
+ program
415
+ .command("orders")
416
+ .description("List V3 orders for the account-scoped bearer session")
417
+ .option("--limit <n>", "max orders", (value) => Number.parseInt(value, 10), 20)
418
+ .option("--status <status>")
419
+ .action(async (options) => {
420
+ const config = loadConfig();
421
+ const backend = newBackendClient(config);
422
+ await runListOrders(backend, config, {
423
+ limit: options.limit,
424
+ status: options.status,
425
+ });
426
+ });
427
+ program
428
+ .command("refund")
429
+ .description("Create a V3 refund request for an order")
430
+ .requiredOption("--order <order_id>")
431
+ .requiredOption("--payment-intent <payment_intent_id>")
432
+ .requiredOption("--amount-minor <n>", "amount in minor units", (value) => Number.parseInt(value, 10))
433
+ .requiredOption("--currency <currency>")
434
+ .option("--reason <reason>")
435
+ .option("--created-by <created_by>")
436
+ .action(async (options) => {
437
+ const config = loadConfig();
438
+ const backend = newBackendClient(config);
439
+ const refundOptions = {
440
+ orderID: options.order,
441
+ paymentIntentID: options.paymentIntent,
442
+ amountMinor: options.amountMinor,
443
+ currency: options.currency,
444
+ ...(options.reason ? { reason: options.reason } : {}),
445
+ ...(options.createdBy ? { createdBy: options.createdBy } : {}),
446
+ };
447
+ await runRefund(backend, config, refundOptions);
448
+ });
449
+ // --- service execution ----------------------------------------------------
450
+ const services = program.command("services").description("Generic V3 Service Execution commands");
451
+ services
452
+ .command("start")
453
+ .description("Start a contract-backed service execution")
454
+ .argument("<service_id>")
455
+ .option("--host <host>", "client host")
456
+ .option("--target <target>")
457
+ .option("--buyer <buyer_id>")
458
+ .action(async (serviceID, options) => {
459
+ const config = loadConfig();
460
+ const backend = newBackendClient(config);
461
+ await runServicesStart(backend, config, serviceID, {
462
+ host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
463
+ ...(options.target ? { target: options.target } : {}),
464
+ ...(options.buyer ? { buyerID: options.buyer } : {}),
465
+ });
466
+ });
467
+ services
468
+ .command("invoke")
469
+ .description("Invoke an agent-visible service capability")
470
+ .argument("<service_execution_id>")
471
+ .requiredOption("--capability <capability_id>")
472
+ .option("--input <key=value>", "redacted input summary", collectOption, [])
473
+ .option("--json", "output JSON")
474
+ .action(async (serviceExecutionID, options) => {
475
+ const config = loadConfig();
476
+ const backend = newBackendClient(config);
477
+ await runServicesInvoke(backend, config, serviceExecutionID, options.capability, parseKeyValueList(options.input), { jsonOutput: Boolean(options.json) });
478
+ });
479
+ services
480
+ .command("action")
481
+ .description("Record a service execution action or human handoff result")
482
+ .argument("<service_execution_id>")
483
+ .requiredOption("--action <action_type>")
484
+ .option("--actor-type <actor_type>")
485
+ .option("--actor-id <actor_id>")
486
+ .option("--status <status>", "pending, approved, rejected, expired, or cancelled")
487
+ .option("--candidate <rank>", "select a displayed candidate by its rank", Number)
488
+ .option("--result-item <service_capability_result_item_id>")
489
+ .option("--selected-candidate-hash <hash>")
490
+ .option("--required-before <step>")
491
+ .option("--input <key=value>", "action input snapshot", collectOption, [])
492
+ .action(async (serviceExecutionID, options) => {
493
+ const config = loadConfig();
494
+ const backend = newBackendClient(config);
495
+ await runServicesAction(backend, serviceExecutionID, options.action, parseKeyValueList(options.input), {
496
+ ...(options.actorType ? { actorType: options.actorType } : {}),
497
+ ...(options.actorId ? { actorID: options.actorId } : {}),
498
+ ...(options.status ? { status: options.status } : {}),
499
+ ...(options.candidate !== undefined ? { candidateRank: options.candidate } : {}),
500
+ ...(options.resultItem ? { resultItemID: options.resultItem } : {}),
501
+ ...(options.selectedCandidateHash ? { selectedCandidateHash: options.selectedCandidateHash } : {}),
502
+ ...(options.requiredBefore ? { requiredBefore: options.requiredBefore } : {}),
503
+ });
504
+ });
505
+ services
506
+ .command("checkout")
507
+ .description("Create checkout from a service execution and render the ItPay checkout handoff")
508
+ .argument("<service_execution_id>")
509
+ .option("--capability <capability_id>")
510
+ .option("--email <delivery_email>")
511
+ .option("--resume", "reissue the existing checkout handoff without creating another checkout")
512
+ .option("--host <host>", "client host (terminal, codex, telegram, feishu, lark, ...)")
513
+ .option("--target <target>", "chat id / channel id / open id for IM hosts")
514
+ .option("--qr-format <format>", "unicode|utf8|ansi|terminal")
515
+ .option("--qr-file <path>", "explicit QR file path")
516
+ .option("--json", "output JSON instead of terminal text")
517
+ .action(async (serviceExecutionID, options) => {
518
+ const config = loadConfig();
519
+ const backend = newBackendClient(config);
520
+ const sessionPath = cartSessionPath();
521
+ const session = CartSession.loadFromFile(sessionPath, config.checkoutCurrency);
522
+ await runServicesCheckout(backend, config, serviceExecutionID, options.capability, {
523
+ ...(options.email ? { email: options.email } : {}),
524
+ resume: Boolean(options.resume),
525
+ host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
526
+ ...(options.target ? { target: options.target } : {}),
527
+ ...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
528
+ ...(options.qrFile ? { qrFilePath: options.qrFile } : {}),
529
+ jsonOutput: Boolean(options.json),
530
+ persistHandoff: (handoff) => {
531
+ session.rememberCheckout({
532
+ cartID: handoff.cartID,
533
+ checkoutID: handoff.checkoutID,
534
+ displayToken: handoff.displayToken,
535
+ checkoutURL: handoff.checkoutURL,
536
+ serviceExecutionID: handoff.serviceExecutionID,
537
+ });
538
+ session.saveToFile(sessionPath);
539
+ },
540
+ });
541
+ });
542
+ services
543
+ .command("list")
544
+ .description("Recover service executions visible to this enrolled device or account")
545
+ .option("--limit <number>", "maximum executions", "50")
546
+ .option("--json", "output compact JSON")
547
+ .action(async (options) => {
548
+ const config = loadConfig();
549
+ const backend = newBackendClient(config);
550
+ await runServicesList(backend, { limit: Number.parseInt(options.limit, 10), jsonOutput: Boolean(options.json) });
551
+ });
552
+ services
553
+ .command("get")
554
+ .description("Read a service execution timeline")
555
+ .argument("<service_execution_id>")
556
+ .option("--json", "output compact JSON")
557
+ .action(async (serviceExecutionID, options) => {
558
+ const config = loadConfig();
559
+ const backend = newBackendClient(config);
560
+ await runServicesGet(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
561
+ });
562
+ services
563
+ .command("next")
564
+ .description("Show the next recommended agent action for a Service Execution")
565
+ .argument("<service_execution_id>")
566
+ .option("--json", "output JSON instead of terminal text")
567
+ .action(async (serviceExecutionID, options) => {
568
+ const config = loadConfig();
569
+ const backend = newBackendClient(config);
570
+ await runServicesNext(backend, serviceExecutionID, { jsonOutput: Boolean(options.json) });
571
+ });
572
+ services
573
+ .command("read-result")
574
+ .description("Read a human-granted service result for this agent")
575
+ .argument("<service_execution_id>")
576
+ .action(async (serviceExecutionID) => {
577
+ const config = loadConfig();
578
+ const backend = newBackendClient(config);
579
+ await runServicesReadResult(backend, serviceExecutionID);
580
+ });
581
+ services
582
+ .command("events")
583
+ .description("List redacted service execution events")
584
+ .argument("<service_execution_id>")
585
+ .action(async (serviceExecutionID) => {
586
+ const config = loadConfig();
587
+ const backend = newBackendClient(config);
588
+ await runServicesEvents(backend, serviceExecutionID);
589
+ });
590
+ async function withBackend(action) {
591
+ const config = loadConfig();
592
+ const backend = newBackendClient(config);
593
+ try {
594
+ await action(backend);
595
+ }
596
+ catch (error) {
597
+ if (error instanceof HttpError) {
598
+ reportCLIError(error);
599
+ return;
600
+ }
601
+ throw error;
602
+ }
603
+ }
604
+ program.parseAsync(process.argv).catch((error) => {
605
+ reportCLIError(error);
606
+ });