@itpay/cli 0.2.17 → 2.0.0-rc.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.
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,78 @@
1
+ // Read a single V3 order by id. Supports terminal text and agent markdown output.
2
+ import { formatMoney, renderOrder } from "../render/output.js";
3
+ import { hintFor } from "../render/status.js";
4
+ import { resolveOutput } from "../render/sink.js";
5
+ import { ensureIdeImageAttach, ideImageAttachBlock } from "../render/ide.js";
6
+ export async function runOrder(backend, orderID, options = {}) {
7
+ const out = resolveOutput(options.output);
8
+ const order = await backend.getOrder(orderID);
9
+ // Prepare an IDE image attach slot. Order responses usually do not
10
+ // carry a brand QR — the previous buy already produced one. We still
11
+ // call ensureIdeImageAttach so the disabled / failed / no-source
12
+ // states get surfaced consistently across commands.
13
+ const plan = {
14
+ kind: "checkout_qr",
15
+ host: (options.host ?? "terminal"),
16
+ summary: "order presentation",
17
+ url: "",
18
+ preferredQRSources: [],
19
+ platform: {
20
+ text: "order presentation",
21
+ links: [],
22
+ buttons: [],
23
+ blocks: [],
24
+ },
25
+ };
26
+ await ensureIdeImageAttach(plan, {
27
+ ...(options.baseURL ? { baseURL: options.baseURL } : {}),
28
+ });
29
+ if (options.host === "codex" || options.host === "claude-code" || options.host === "trae") {
30
+ out(renderOrderMarkdown(order, plan) + "\n");
31
+ }
32
+ else {
33
+ out(renderOrder(order) + "\n");
34
+ if (plan.ideImageAttach) {
35
+ out(ideImageAttachBlock(plan.ideImageAttach).filter((l) => l.length > 0).join("\n") + "\n");
36
+ }
37
+ out(`hint: ${hintFor("order", order.status)}\n`);
38
+ }
39
+ }
40
+ function renderOrderMarkdown(order, plan) {
41
+ const lines = [];
42
+ lines.push(`## :package: 订单 ${order.order_id}`);
43
+ lines.push("");
44
+ const statusEmoji = order.status === "delivered" ? ":white_check_mark:" : order.status === "refunded" ? ":arrows_counterclockwise:" : ":hourglass:";
45
+ lines.push(`| 字段 | 值 |`);
46
+ lines.push(`|------|-----|`);
47
+ lines.push(`| 状态 | ${statusEmoji} ${order.status} |`);
48
+ lines.push(`| 金额 | ${formatMoney(order.amount_minor, order.currency)} |`);
49
+ lines.push(`| Checkout | \`${order.checkout_id}\``);
50
+ if (order.paid_at)
51
+ lines.push(`| 支付时间 | ${order.paid_at} |`);
52
+ lines.push("");
53
+ if (order.items.length > 0) {
54
+ lines.push(`| 项目 | 数量 | 单价 |`);
55
+ lines.push(`|------|:----:|------|`);
56
+ for (const item of order.items) {
57
+ lines.push(`| ${item.title} | ${item.quantity} | ${formatMoney(item.amount_minor, item.currency)} |`);
58
+ }
59
+ lines.push("");
60
+ }
61
+ if (order.delivery_artifacts.length > 0) {
62
+ lines.push(`### :lock: 交付物`);
63
+ lines.push("");
64
+ for (const artifact of order.delivery_artifacts) {
65
+ const notification = artifact.notification_status ? ` — notification:${artifact.notification_status}` : "";
66
+ const vault = artifact.vault_artifact_id ? ` — vault:\`${artifact.vault_artifact_id}\`` : "";
67
+ lines.push(`- \`${artifact.delivery_artifact_id}\` — ${artifact.artifact_type} — ${artifact.status}${notification}${vault}`);
68
+ if (artifact.public_preview)
69
+ lines.push(` > ${artifact.public_preview}`);
70
+ }
71
+ lines.push("");
72
+ }
73
+ if (plan.ideImageAttach) {
74
+ lines.push(...ideImageAttachBlock(plan.ideImageAttach));
75
+ }
76
+ lines.push(`> :bulb: ${hintFor("order", order.status)}`);
77
+ return lines.join("\n");
78
+ }
@@ -0,0 +1,22 @@
1
+ // List V3 orders visible to the account-scoped session. Requires
2
+ // ITPAY_BEARER_TOKEN to be set to an account-scoped buyer session token.
3
+ // Order-scoped sessions are rejected by the backend with 403 and the
4
+ // CLI surfaces the typed `HttpError` so `main.ts` can render it.
5
+ import { renderOrder } from "../render/output.js";
6
+ import { hintFor } from "../render/status.js";
7
+ import { resolveOutput } from "../render/sink.js";
8
+ export async function runListOrders(backend, config, options) {
9
+ const out = resolveOutput(options.output);
10
+ if (!config.bearerToken) {
11
+ throw new Error("ITPAY_BEARER_TOKEN is required to list account orders");
12
+ }
13
+ const response = await backend.listAccountOrders(options.limit, options.status, config.bearerToken);
14
+ if (response.orders.length === 0) {
15
+ out("no orders found\n");
16
+ return;
17
+ }
18
+ for (const order of response.orders) {
19
+ out(renderOrder(order) + "\n");
20
+ out(`hint: ${hintFor("order", order.status)}\n\n`);
21
+ }
22
+ }
@@ -0,0 +1,26 @@
1
+ // Optional CLI helper: only intended for the V3 `payment-intent` flow when
2
+ // the human checkout page is unavailable. The V3 architecture prefers
3
+ // that the human checkout page creates the payment intent, but the CLI
4
+ // still needs an escape hatch for sandbox/manual testing.
5
+ import { operationID } from "../state/config.js";
6
+ import { resolveOutput } from "../render/sink.js";
7
+ export async function runPay(backend, config, options) {
8
+ const out = resolveOutput(options.output);
9
+ const request = {
10
+ payment_method_type: options.method,
11
+ ...(options.preferredProvider ? { preferred_provider: options.preferredProvider } : {}),
12
+ ...(options.buyerID ? { buyer_id: options.buyerID } : {}),
13
+ ...(options.refreshAction ? { refresh_action: true } : {}),
14
+ };
15
+ const intent = await backend.createPaymentIntent(options.checkoutID, request, await operationID(config, `payment.intent:${options.checkoutID}:${options.method}`));
16
+ out(`payment_intent_id: ${intent.payment_intent_id}\n`);
17
+ out(`status: ${intent.status}\n`);
18
+ out(`method: ${intent.payment_method_type}\n`);
19
+ out(`amount: ${(intent.amount_minor / 100).toFixed(2)} ${intent.currency}\n`);
20
+ if (intent.action?.qr_image_url) {
21
+ out(`qr_image_url: ${intent.action.qr_image_url}\n`);
22
+ }
23
+ if (intent.action?.mobile_wallet_url) {
24
+ out(`mobile_wallet_url: ${intent.action.mobile_wallet_url}\n`);
25
+ }
26
+ }
@@ -0,0 +1,8 @@
1
+ // Liveness probe. Useful for smoke testing CLI wiring before running `buy`.
2
+ import { renderReady } from "../render/output.js";
3
+ import { resolveOutput } from "../render/sink.js";
4
+ export async function runReadyz(backend, options = {}) {
5
+ const out = resolveOutput(options.output);
6
+ const response = await backend.readyz();
7
+ out(renderReady(response) + "\n");
8
+ }
@@ -0,0 +1,20 @@
1
+ // Create a V3 refund request. The backend decides policy state and
2
+ // returns the canonical refund status. The CLI never assumes a refund
3
+ // succeeded locally — it always re-reads canonical state.
4
+ import { operationID } from "../state/config.js";
5
+ import { renderRefund } from "../render/output.js";
6
+ import { hintFor } from "../render/status.js";
7
+ import { resolveOutput } from "../render/sink.js";
8
+ export async function runRefund(backend, config, options) {
9
+ const out = resolveOutput(options.output);
10
+ const request = {
11
+ payment_intent_id: options.paymentIntentID,
12
+ amount_minor: options.amountMinor,
13
+ currency: options.currency,
14
+ ...(options.reason ? { reason: options.reason } : {}),
15
+ ...(options.createdBy ? { created_by: options.createdBy } : {}),
16
+ };
17
+ const refund = await backend.createRefund(options.orderID, request, await operationID(config, `refund.create:${options.orderID}:${options.paymentIntentID}:${options.amountMinor}:${options.currency}`));
18
+ out(renderRefund(refund) + "\n");
19
+ out(`hint: ${hintFor("refund", refund.status)}\n`);
20
+ }
@@ -0,0 +1,317 @@
1
+ import { operationID } from "../state/config.js";
2
+ import { resolveOutput } from "../render/sink.js";
3
+ import { dispatchRender } from "../render/index.js";
4
+ import { ensureIdeImageAttach } from "../render/ide.js";
5
+ import { buildAgentChatHandoff } from "../render/markdown.js";
6
+ import { buildCheckoutQRPlan } from "./buy.js";
7
+ import { attachAgentGuidance, buildServiceActionGuidance, buildServiceInvokedGuidance, buildServiceReadModelGuidance, buildServiceStartedGuidance, printAgentGuidance, } from "./guidance.js";
8
+ const serviceActionStatuses = new Set(["pending", "approved", "rejected", "expired", "cancelled"]);
9
+ export async function runServicesStart(backend, config, serviceID, options = {}) {
10
+ const host = options.host ?? "terminal";
11
+ const response = await backend.startServiceExecution({
12
+ service_id: serviceID,
13
+ agent_device_id: config.agentDeviceID,
14
+ ...(options.buyerID ? { buyer_id: options.buyerID } : {}),
15
+ client_context: {
16
+ agent_device_id: config.agentDeviceID,
17
+ host,
18
+ ...(options.target ? { target: options.target } : {}),
19
+ ...(options.clientContext ?? {}),
20
+ },
21
+ });
22
+ writeJSON(options.output, attachAgentGuidance(response, buildServiceStartedGuidance(response)));
23
+ }
24
+ export async function runServicesInvoke(backend, config, serviceExecutionID, capabilityID, input, options = {}) {
25
+ const idempotencyKey = await operationID(config, `service.invoke:${serviceExecutionID}:${capabilityID}:${stableInput(input)}`);
26
+ const response = await backend.invokeServiceCapability(serviceExecutionID, capabilityID, {
27
+ idempotency_key: idempotencyKey,
28
+ redacted_summary: input,
29
+ });
30
+ const guidance = buildServiceInvokedGuidance(response);
31
+ if (options.jsonOutput) {
32
+ writeJSON(options.output, attachAgentGuidance(response, guidance));
33
+ return;
34
+ }
35
+ printAgentGuidance(guidance, options.output);
36
+ }
37
+ function stableInput(input) {
38
+ return JSON.stringify(Object.fromEntries(Object.entries(input).sort(([left], [right]) => left.localeCompare(right))));
39
+ }
40
+ export async function runServicesAction(backend, serviceExecutionID, actionType, input, options = {}) {
41
+ const selection = await resolveCandidateSelection(backend, serviceExecutionID, actionType, options);
42
+ const request = {
43
+ action_type: actionType,
44
+ input_snapshot: input,
45
+ };
46
+ if (options.actorType)
47
+ request.actor_type = options.actorType;
48
+ if (options.actorID)
49
+ request.actor_id = options.actorID;
50
+ if (options.status)
51
+ request.status = normalizeServiceActionStatus(options.status);
52
+ const resultItemID = selection?.resultItemID ?? options.resultItemID;
53
+ const selectedCandidateHash = selection?.stableHash ?? options.selectedCandidateHash;
54
+ if (resultItemID)
55
+ request.result_item_id = resultItemID;
56
+ if (selectedCandidateHash)
57
+ request.selected_candidate_hash = selectedCandidateHash;
58
+ if (options.requiredBefore)
59
+ request.required_before = options.requiredBefore;
60
+ const response = await backend.recordServiceExecutionAction(serviceExecutionID, request);
61
+ writeJSON(options.output, attachAgentGuidance(response, buildServiceActionGuidance(response)));
62
+ }
63
+ async function resolveCandidateSelection(backend, serviceExecutionID, actionType, options) {
64
+ if (options.candidateRank === undefined)
65
+ return undefined;
66
+ if (actionType !== "select_candidate") {
67
+ throw new Error("--candidate is only valid with --action select_candidate");
68
+ }
69
+ if (options.resultItemID || options.selectedCandidateHash) {
70
+ throw new Error("--candidate cannot be combined with --result-item or --selected-candidate-hash");
71
+ }
72
+ if (!Number.isInteger(options.candidateRank) || options.candidateRank < 1) {
73
+ throw new Error("--candidate must be a positive integer result rank");
74
+ }
75
+ const execution = await backend.getServiceExecution(serviceExecutionID);
76
+ const result = execution.result_items.find((item) => item.rank === options.candidateRank);
77
+ if (!result) {
78
+ throw new Error(`candidate ${options.candidateRank} is not available on service execution ${serviceExecutionID}`);
79
+ }
80
+ return {
81
+ resultItemID: result.service_capability_result_item_id,
82
+ stableHash: result.stable_hash,
83
+ };
84
+ }
85
+ export async function runServicesCheckout(backend, config, serviceExecutionID, capabilityID, options = {}) {
86
+ const deliveryContact = {
87
+ ...(options.deliveryContact ?? {}),
88
+ ...(options.email ? { email: options.email } : {}),
89
+ };
90
+ if (!options.resume && !capabilityID) {
91
+ throw new Error("--capability is required when creating a service checkout; use --resume to recover an existing handoff");
92
+ }
93
+ if (!options.resume && String(deliveryContact.email ?? "").trim() === "") {
94
+ const execution = await backend.getServiceExecution(serviceExecutionID);
95
+ const capability = execution.capabilities.find((item) => item.capability_id === capabilityID);
96
+ if (!capability) {
97
+ throw new Error(`capability ${capabilityID} is not available on service execution ${serviceExecutionID}`);
98
+ }
99
+ if (capability.delivery_email_required) {
100
+ throw new Error("delivery email is required before creating this service checkout; ask the buyer for --email");
101
+ }
102
+ }
103
+ const response = await backend.createServiceExecutionCheckout(serviceExecutionID, {
104
+ ...(capabilityID ? { capability_id: capabilityID } : {}),
105
+ ...(Object.keys(deliveryContact).length > 0 ? { delivery_contact: deliveryContact } : {}),
106
+ ...(options.resume ? { resume: true } : {}),
107
+ });
108
+ const checkout = response.checkout;
109
+ const checkoutID = checkout.checkout.checkout_id;
110
+ const displayToken = checkout.display_token;
111
+ const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
112
+ const plan = buildCheckoutQRPlan({
113
+ host: options.host ?? "terminal",
114
+ checkoutID,
115
+ checkoutURL,
116
+ displayToken,
117
+ qrPayload: checkout.qr_payload,
118
+ ...(checkout.qr_png_url ? { qrPNGURL: checkout.qr_png_url } : {}),
119
+ nextAction: checkout.checkout.next_action,
120
+ orderItems: response.cart.items.map((item) => ({
121
+ title: item.title,
122
+ quantity: item.quantity,
123
+ amountMinor: item.amount_minor,
124
+ currency: item.currency,
125
+ })),
126
+ orderCurrency: checkout.checkout.currency,
127
+ });
128
+ options.persistHandoff?.({
129
+ serviceExecutionID,
130
+ cartID: response.cart.cart_id,
131
+ checkoutID,
132
+ displayToken,
133
+ checkoutURL,
134
+ });
135
+ if (options.jsonOutput) {
136
+ await ensureIdeImageAttach(plan, {
137
+ enabled: config.ideImageAttach,
138
+ ...(config.baseURL ? { baseURL: config.baseURL } : {}),
139
+ ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
140
+ });
141
+ writeJSON(options.output, buildServicesCheckoutJSON(response, checkoutURL, plan));
142
+ return;
143
+ }
144
+ await dispatchRender(plan, {
145
+ host: options.host ?? "terminal",
146
+ isTTY: options.isTTY ?? Boolean(process.stdout.isTTY),
147
+ ...(options.target ? { target: options.target } : {}),
148
+ ...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
149
+ ...(options.qrFilePath ? { qrFilePath: options.qrFilePath } : {}),
150
+ ...(options.output ? { output: options.output } : {}),
151
+ ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
152
+ baseURL: config.baseURL,
153
+ });
154
+ }
155
+ export async function runServicesGet(backend, serviceExecutionID, options = {}) {
156
+ const response = await backend.getServiceExecution(serviceExecutionID);
157
+ const guidance = buildServiceReadModelGuidance(response);
158
+ if (options.jsonOutput) {
159
+ writeJSON(options.output, attachAgentGuidance(response, guidance));
160
+ return;
161
+ }
162
+ printAgentGuidance(guidance, options.output);
163
+ }
164
+ export async function runServicesNext(backend, serviceExecutionID, options = {}) {
165
+ const response = await backend.getServiceExecution(serviceExecutionID);
166
+ const guidance = buildServiceReadModelGuidance(response);
167
+ if (options.jsonOutput) {
168
+ writeJSON(options.output, guidance);
169
+ return;
170
+ }
171
+ printAgentGuidance(guidance, options.output);
172
+ }
173
+ export async function runServicesList(backend, options = {}) {
174
+ const response = await backend.listServiceExecutions(options.limit ?? 50);
175
+ if (options.jsonOutput) {
176
+ writeJSON(options.output, response.executions.map((model) => ({
177
+ execution: model.execution,
178
+ agent_guidance: buildServiceReadModelGuidance(model),
179
+ })));
180
+ return;
181
+ }
182
+ const out = resolveOutput(options.output);
183
+ for (const model of response.executions) {
184
+ const guidance = buildServiceReadModelGuidance(model);
185
+ out(`${guidance.summary}\n`);
186
+ const next = guidance.next_actions[0];
187
+ if (next)
188
+ out(`next: ${next.command}\n`);
189
+ }
190
+ }
191
+ export async function runServicesReadResult(backend, serviceExecutionID, options = {}) {
192
+ writeJSON(options.output, await backend.getGrantedServiceResult(serviceExecutionID));
193
+ }
194
+ export async function runServicesEvents(backend, serviceExecutionID, options = {}) {
195
+ writeJSON(options.output, await backend.listServiceExecutionEvents(serviceExecutionID));
196
+ }
197
+ export function parseKeyValueList(values) {
198
+ const result = {};
199
+ for (const value of values ?? []) {
200
+ const index = value.indexOf("=");
201
+ if (index <= 0) {
202
+ throw new Error(`invalid --input "${value}", expected key=value`);
203
+ }
204
+ result[value.slice(0, index)] = parseValue(value.slice(index + 1));
205
+ }
206
+ return result;
207
+ }
208
+ export function collectOption(value, previous = []) {
209
+ previous.push(value);
210
+ return previous;
211
+ }
212
+ function parseValue(value) {
213
+ if (value === "true")
214
+ return true;
215
+ if (value === "false")
216
+ return false;
217
+ if (/^-?\d+(\.\d+)?$/.test(value))
218
+ return Number(value);
219
+ return value;
220
+ }
221
+ function writeJSON(output, value) {
222
+ resolveOutput(output)(JSON.stringify(value, null, 2) + "\n");
223
+ }
224
+ function buildServicesCheckoutJSON(response, checkoutURL, plan) {
225
+ const checkout = response.checkout;
226
+ const output = {
227
+ kind: "checkout_handoff_required",
228
+ next_action: "open_human_checkout",
229
+ service_execution_id: response.binding.service_execution_id,
230
+ service_quote_lock_id: response.service_quote_lock_id,
231
+ checkout_id: checkout.checkout.checkout_id,
232
+ checkout_status: checkout.checkout.status,
233
+ handoff_reissued: response.handoff_reissued,
234
+ checkout_url: checkoutURL,
235
+ display_token: checkout.display_token,
236
+ qr_payload: checkout.qr_payload,
237
+ amount_minor: checkout.checkout.amount_minor,
238
+ currency: checkout.checkout.currency,
239
+ next: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token}`,
240
+ warning: "Do not call `itpay pay` for the normal buyer flow; show this ItPay checkout URL/QR to the human.",
241
+ };
242
+ const nextActions = [{
243
+ id: "open_human_checkout",
244
+ label: "Show the ItPay checkout URL or branded QR to the human",
245
+ command: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token}`,
246
+ requires_human: true,
247
+ reason: "The human must review and pay on the ItPay checkout page.",
248
+ }];
249
+ output.next_actions = nextActions;
250
+ output.agent_guidance = {
251
+ kind: "checkout_handoff",
252
+ summary: `checkout ${checkout.checkout.checkout_id}: show the ItPay checkout handoff to the human`,
253
+ state: {
254
+ service_execution_id: response.binding.service_execution_id,
255
+ checkout_id: checkout.checkout.checkout_id,
256
+ checkout_status: checkout.checkout.status,
257
+ },
258
+ next_actions: nextActions,
259
+ recovery: [{
260
+ id: "inspect_checkout",
261
+ label: "Read checkout presentation",
262
+ command: `itpay checkout --id ${checkout.checkout.checkout_id} --token ${checkout.display_token}`,
263
+ }],
264
+ };
265
+ if (checkout.qr_png_url) {
266
+ output.qr_png_url = checkout.qr_png_url;
267
+ }
268
+ if (plan.ideImageAttach) {
269
+ output.brand_qr_status = plan.ideImageAttach.status;
270
+ output.brand_qr_mime_type = plan.ideImageAttach.mimeType;
271
+ output.brand_qr_must_render_reason = plan.ideImageAttach.mustRenderReason;
272
+ output.brand_qr_render_action = "agent_must_read_local_path_into_ide_chat";
273
+ if (plan.ideImageAttach.localPath) {
274
+ output.brand_qr_local_path = plan.ideImageAttach.localPath;
275
+ const stableName = plan.ideImageAttach.localPath.split("/").pop();
276
+ if (stableName)
277
+ output.brand_qr_stable_name = stableName;
278
+ }
279
+ if (plan.ideImageAttach.mirrors.length > 0) {
280
+ output.brand_qr_mirrors = [...plan.ideImageAttach.mirrors];
281
+ }
282
+ if (plan.ideImageAttach.caption) {
283
+ output.brand_qr_caption = plan.ideImageAttach.caption;
284
+ }
285
+ if (plan.ideImageAttach.error) {
286
+ output.brand_qr_error = plan.ideImageAttach.error;
287
+ }
288
+ }
289
+ output.agent_action = buildAgentChatHandoff(plan);
290
+ return output;
291
+ }
292
+ function normalizeServiceActionStatus(status) {
293
+ const normalized = status.trim().toLowerCase();
294
+ if (!serviceActionStatuses.has(normalized)) {
295
+ throw new Error(`invalid --status "${status}". Supported: pending, approved, rejected, expired, cancelled`);
296
+ }
297
+ return normalized;
298
+ }
299
+ function tokenizedCheckoutURL(checkoutURL, displayToken, qrPayload) {
300
+ if (qrPayload.trim().length > 0) {
301
+ return qrPayload;
302
+ }
303
+ if (checkoutURL.trim().length === 0 || displayToken.trim().length === 0) {
304
+ return checkoutURL;
305
+ }
306
+ try {
307
+ const parsed = new URL(checkoutURL);
308
+ if (!parsed.searchParams.has("display_token")) {
309
+ parsed.searchParams.set("display_token", displayToken);
310
+ }
311
+ return parsed.toString();
312
+ }
313
+ catch {
314
+ const separator = checkoutURL.includes("?") ? "&" : "?";
315
+ return `${checkoutURL}${separator}display_token=${encodeURIComponent(displayToken)}`;
316
+ }
317
+ }