@itpay/cli 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -213,7 +213,7 @@ redacted secure delivery status:
213
213
  ```bash
214
214
  itp buyer catalog search --query 企业工商 --json
215
215
  itp buyer cart create --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=阿里 --json
216
- itp buyer checkout create --cart <cart_id> --email buyer@example.com --json
216
+ itp buyer checkout create --cart <cart_id> --email <buyer_email> --json
217
217
  itp buyer payment wait <payment_intent_id> --json
218
218
  itp buyer checkout status <checkout_id> --json
219
219
  ```
@@ -221,7 +221,7 @@ itp buyer checkout status <checkout_id> --json
221
221
  For the one-command buyer helper:
222
222
 
223
223
  ```bash
224
- itp buy var_itpay_enterprise_fuzzy_search_cny01 --email buyer@example.com --input company_name=阿里 --json
224
+ itp buy var_itpay_enterprise_fuzzy_search_cny01 --email <buyer_email> --input company_name=阿里 --display agent --no-wait-payment --json
225
225
  ```
226
226
 
227
227
  For multi-item cart tests:
@@ -239,6 +239,7 @@ Payment QR rules:
239
239
  - Otherwise render the ItPay-hosted `qr_png_url` / `preferred_qr_url`.
240
240
  - Use `mobile_wallet_url` only as a human mobile fallback.
241
241
  - Do not generate your own QR from payment URLs.
242
+ - In agent app clients, send `human_visible_markdown` to the human first, then run `next.command`.
242
243
  - Treat only `payment_intent.verified` as payment success.
243
244
 
244
245
  If the human wants the agent to analyze delivered content, the human must reveal
package/bin/itp CHANGED
@@ -93,8 +93,8 @@ async function main() {
93
93
  output({
94
94
  version: VERSION,
95
95
  commands: [
96
- "buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --json",
97
- "buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --no-wait --json",
96
+ "buy var_pubg_couple_skin_cny20 --email <buyer_email> --phone +8613800000000 --display agent --no-wait-payment --json",
97
+ "buy var_pubg_couple_skin_cny20 --email <buyer_email> --phone +8613800000000 --no-wait --json",
98
98
  "buyer catalog search --query 企业工商 --category business_data_api --provider itpay_enterprise_data --json",
99
99
  "buyer catalog get --variant var_pubg_couple_skin_cny20 --json",
100
100
  "buyer cart create --variant var_pubg_couple_skin_cny20 --json",
@@ -105,7 +105,7 @@ async function main() {
105
105
  "buyer shelf manifest --json",
106
106
  "buyer shelf snapshot --version <catalog_version> --json",
107
107
  "buyer shelf delta --since <catalog_version> --json",
108
- "buyer checkout create --cart <cart_id> --method alipay --email buyer@example.com --phone +8613800000000 --json",
108
+ "buyer checkout create --cart <cart_id> --method alipay --email <buyer_email> --phone +8613800000000 --json",
109
109
  "buyer checkout status <checkout_id> --json",
110
110
  "buyer checkout resume <checkout_id> --json",
111
111
  "buyer auth status --json",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  {
70
70
  "intent": "do both steps through the high-level command",
71
- "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --json",
71
+ "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
72
72
  "success_signal": "response.status is waiting_human_auth, waiting_user_payment, payment_verified, or delivery_claimable"
73
73
  }
74
74
  ],
@@ -8,7 +8,7 @@
8
8
  "Checkout returns human_action.kind=auth_qr for the first-purchase auth-to-payment entry.",
9
9
  "PaymentIntent has been created.",
10
10
  "The CLI returned human_action.kind=payment_qr, qr_png_url, preferred_qr_url, or qr_image_url.",
11
- "The human needs to scan with Alipay sandbox."
11
+ "The human needs to scan with the payment provider."
12
12
  ],
13
13
  "required_state": {
14
14
  "needs": ["payment_intent_id", "human_action", "qr_png_url/preferred_qr_url or local_qr_path"],
@@ -17,8 +17,8 @@
17
17
  "commands": [
18
18
  {
19
19
  "intent": "create payment intent through the high-level buy flow",
20
- "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --json",
21
- "success_signal": "response.payment_intent.human_action.local_qr_path or response.payment_intent.human_action.qr_png_url is present"
20
+ "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
21
+ "success_signal": "response.human_visible_markdown and response.next.command are present"
22
22
  },
23
23
  {
24
24
  "intent": "recover a scanner order-not-found display problem",
@@ -33,14 +33,17 @@
33
33
  "Payment QR display requires both a valid/saved buyer session and payment_intent_id. If payment_intent_id appears before buyer_session is saved, run buyer checkout resume/status instead of showing payment QR.",
34
34
  "Do not stop after showing auth_qr unless the human explicitly asks you to pause. Keep waiting/resuming the same checkout so the post-auth payment handoff can continue.",
35
35
  "Do not describe auth_qr as payment success or payment proof. Only payment_intent.verified proves payment.",
36
+ "In Codex or Claude Code app clients, the default UX is show-then-wait: send response.human_visible_markdown to the human first, then run response.next.command.",
37
+ "For Telegram-style message clients, show response.human_visible_markdown and stop unless the human asks you to keep waiting or returns with a payment question.",
36
38
  "MUST show local_qr_path first when present. Many agent clients do not reliably render remote QR images; local_qr_path is the most reliable desktop/chat display artifact.",
37
39
  "If local_qr_path is not present, show qr_png_url or preferred_qr_url as the primary scannable QR. This is an ItPay-hosted human QR image; it may render the native provider payment code for scanner reliability, but the agent must not request or decode the raw provider payload.",
38
40
  "Use qr_image_url/SVG only as a fallback when PNG/local rendering is unavailable.",
39
41
  "For mobile users, present mobile_wallet_url as a clickable payment button when present.",
40
42
  "payment_entry_url is the stable ItPay status/payment page. qr_png_url/local_qr_path is usually the better desktop scanner artifact.",
41
- "In Alipay sandbox, the provider can briefly show order not found even after precreate succeeded. Tell the human to wait 30-60 seconds and retry the same QR/page. Do not create a new checkout, do not create a new payment intent, and do not repeatedly refresh unless the ItPay page or CLI explicitly asks for recovery.",
43
+ "The provider can briefly show order not found even after precreate succeeded. Tell the human to wait 30-60 seconds and retry the same QR/page. Do not create a new checkout, do not create a new payment intent, and do not repeatedly refresh unless the ItPay page or CLI explicitly asks for recovery.",
42
44
  "Opening the payment page must not be treated as paid.",
43
- "After showing QR, immediately wait for payment verification."
45
+ "Do not run payment wait before the QR/link has been sent to the human.",
46
+ "After showing QR, run next.command or buyer payment wait for the same payment_intent_id."
44
47
  ],
45
48
  "forbidden": [
46
49
  "Do not encode payment_entry_url into a new QR yourself when qr_png_url/local_qr_path is available.",
@@ -58,7 +61,7 @@
58
61
  "command": "itp docs show payment-wait --role buyer --json"
59
62
  },
60
63
  {
61
- "condition": "Alipay sandbox says order not found after waiting on the same QR/page",
64
+ "condition": "the payment provider says order not found after waiting on the same QR/page",
62
65
  "topic": "qr-refresh",
63
66
  "command": "itp docs show qr-refresh --role buyer --json"
64
67
  }
@@ -27,9 +27,11 @@
27
27
  }
28
28
  ],
29
29
  "agent_rules": [
30
+ "Start payment wait only after the QR image/link has already been sent to the human.",
30
31
  "Payment wait is a recovery loop around the same payment_intent_id, not a one-shot command.",
31
32
  "wait.timeout is a heartbeat, not a payment failure.",
32
- "Continue waiting with the same payment_intent_id unless the user explicitly cancels.",
33
+ "In Codex or Claude Code app clients, after showing QR, wait online for a short visible cycle first. If the human still has not paid, explain that the same QR/link remains valid and pause for their reply.",
34
+ "Continue waiting with the same payment_intent_id unless the user explicitly cancels or asks you to pause.",
33
35
  "If the network drops, the process is interrupted, or there is no response, rerun payment wait with the same payment_intent_id or run checkout status/resume with the same checkout_id.",
34
36
  "If status is unclear, tell the human it is unclear and continue querying the same payment_intent_id/checkout_id. Do not create a new checkout.",
35
37
  "Payment success is payment_intent.verified only.",
@@ -5,7 +5,7 @@
5
5
  "title": "QR Refresh Recovery",
6
6
  "purpose": "Teach the agent how to recover scanner problems without changing payment truth or creating a second order.",
7
7
  "when_to_use": [
8
- "The human reports Alipay sandbox says order not found after waiting 30-60 seconds on the same QR/page.",
8
+ "The human reports the payment provider says order not found after waiting 30-60 seconds on the same QR/page.",
9
9
  "The QR expired or the wallet cannot load it.",
10
10
  "The payment intent is still not verified."
11
11
  ],
@@ -29,7 +29,7 @@
29
29
  "QR refresh is display recovery only. ItPay may return the same QR again when the current provider QR is still valid.",
30
30
  "Use the same payment_intent_id.",
31
31
  "If QR does not render, first use the returned local_qr_path, qr_png_url, or preferred_qr_url. Do not generate a new QR from guessed provider payload.",
32
- "For Alipay sandbox order-not-found, first ask the human to wait and scan/open the same QR/page again; use refresh only after that wait has failed.",
32
+ "For provider order-not-found, first ask the human to wait and scan/open the same QR/page again; use refresh only after that wait has failed.",
33
33
  "If refresh succeeds, show only the returned ItPay QR/display URL and continue waiting on the same payment_intent_id.",
34
34
  "After refresh, continue waiting through the normal wait contract."
35
35
  ],
@@ -36,8 +36,8 @@
36
36
  },
37
37
  {
38
38
  "intent": "buy a selected variant with the high-level cart-first flow",
39
- "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --json",
40
- "success_signal": "response.status becomes waiting_human_auth, waiting_user_payment, payment_verified, or delivery_claimable"
39
+ "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
40
+ "success_signal": "response.status becomes waiting_human_auth or waiting_user_payment; if waiting_user_payment, response.human_visible_markdown and response.next.command are present"
41
41
  }
42
42
  ],
43
43
  "agent_rules": [
@@ -54,8 +54,9 @@
54
54
  "After showing auth_qr, keep running/resuming the same checkout unless the human explicitly asks you to pause. Do not stop merely because a QR was displayed.",
55
55
  "When a buyer command returns buyer_session.status=buyer_session_saved, the CLI has stored the buyer account session for this agent device. You may continue with checkout/payment or `buyer vault` commands without asking the human for a token.",
56
56
  "Vault grants are only for reading approved delivered artifacts. They are not buyer session credentials and do not authorize refunds or account/order management.",
57
- "Use high-level itp buy when possible; it is designed to show the first-purchase entry, wait/resume, continue to payment, wait for verification, and poll delivery.",
58
- "Show the returned payment QR exactly as provided.",
57
+ "Use high-level itp buy when possible, but in agent app clients split payment into two visible steps: first return and show human_visible_markdown, then run next.command.",
58
+ "If response.human_visible_markdown is present, send it to the human immediately in the same user-visible message. It includes the QR image and payment link.",
59
+ "Show the returned payment QR exactly as provided before starting payment wait.",
59
60
  "Payment truth comes only from payment_intent.verified.",
60
61
  "Secure delivery goes to the human first; report redacted delivery status only."
61
62
  ],
package/lib/buyer.js CHANGED
@@ -4,17 +4,19 @@ import {
4
4
  writeCredentials, writeSessionCredentials, writeState
5
5
  } from "./env.js";
6
6
  import { coreApi, coreApiBase } from "./http.js";
7
- import { renderHumanAction, renderItPayPaymentAction, writeWaitHeartbeat } from "./render-human.js";
7
+ import { renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
8
8
 
9
9
  async function buyerBuy(flags) {
10
+ rejectBuyerSandboxFlag(flags);
11
+ const showThenWait = shouldReturnAfterAgentTextQR(flags);
10
12
  const selectionID = flags.selection || flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id;
11
- if (!selectionID) throw new Error("catalog variant id is required, for example: itp buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --json");
13
+ if (!selectionID) throw new Error("catalog variant id is required, for example: itp buy <variant_id> --email <buyer_email> --json");
12
14
  const selection = await resolveBuyerCatalogSelection(selectionID, flags);
13
15
  const cart = await createBuyerCart(selection, flags);
14
16
  let checkout = await createBuyerCheckoutFromCart(cart, selection, flags);
15
17
  if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
16
18
  await renderHumanAction(checkout.human_action, flags);
17
- if (flags.no_wait || flags.no_wait_auth) {
19
+ if (flags.no_wait || flags.no_wait_auth || showThenWait) {
18
20
  output(buyerRunOutput({
19
21
  status: "waiting_human_auth",
20
22
  selection,
@@ -41,15 +43,20 @@ async function buyerBuy(flags) {
41
43
  : await createBuyerPaymentIntent(checkout.checkout_id, flags);
42
44
  await renderItPayPaymentAction(intent, flags);
43
45
 
44
- if (flags.no_wait || flags.no_wait_payment) {
46
+ if (flags.no_wait || flags.no_wait_payment || showThenWait) {
45
47
  output(buyerRunOutput({
46
48
  status: "waiting_user_payment",
47
49
  selection,
48
50
  cart,
49
51
  checkout,
50
52
  payment_intent: intent,
53
+ payment_handoff: paymentHandoff(intent),
51
54
  agent_next_actions: intent.agent_next_actions || ["wait_payment"],
52
- next: { command: cliCommand("buyer", "payment", "wait", intent.payment_intent_id, "--json") }
55
+ next: {
56
+ command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
57
+ safe_for_agent: true,
58
+ instruction: "Send human_visible_markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
59
+ }
53
60
  }));
54
61
  return;
55
62
  }
@@ -75,6 +82,7 @@ async function buyerBuy(flags) {
75
82
  }
76
83
 
77
84
  async function buyer(command, rest, flags) {
85
+ rejectBuyerSandboxFlag(flags);
78
86
  const subcommand = rest[0] && !String(rest[0]).startsWith("--") ? rest[0] : "";
79
87
  if (command === "catalog") {
80
88
  if (subcommand === "search") {
@@ -312,8 +320,16 @@ async function buyer(command, rest, flags) {
312
320
  status: intent.status === "verified" ? "payment_verified" : "waiting_user_payment",
313
321
  checkout,
314
322
  payment_intent: intent,
323
+ payment_handoff: intent.status === "verified" ? undefined : paymentHandoff(intent),
315
324
  buyer_session: buyerSessionClaimStatus(claimedSession),
316
- agent_next_actions: intent.agent_next_actions || checkout.agent_next_actions || ["wait_payment"]
325
+ agent_next_actions: intent.agent_next_actions || checkout.agent_next_actions || ["wait_payment"],
326
+ next: intent.status === "verified"
327
+ ? { command: cliCommand("buyer", "checkout", "status", checkout.checkout_id, "--json"), safe_for_agent: true }
328
+ : {
329
+ command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
330
+ safe_for_agent: true,
331
+ instruction: "Send human_visible_markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
332
+ }
317
333
  }));
318
334
  return;
319
335
  }
@@ -326,7 +342,18 @@ async function buyer(command, rest, flags) {
326
342
  }
327
343
  const intent = await createBuyerPaymentIntent(checkout.checkout_id, flags);
328
344
  await renderItPayPaymentAction(intent, flags);
329
- output(buyerRunOutput({ status: "waiting_user_payment", checkout, payment_intent: intent, agent_next_actions: intent.agent_next_actions || ["wait_payment"] }));
345
+ output(buyerRunOutput({
346
+ status: "waiting_user_payment",
347
+ checkout,
348
+ payment_intent: intent,
349
+ payment_handoff: paymentHandoff(intent),
350
+ agent_next_actions: intent.agent_next_actions || ["wait_payment"],
351
+ next: {
352
+ command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
353
+ safe_for_agent: true,
354
+ instruction: "Send human_visible_markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
355
+ }
356
+ }));
330
357
  return;
331
358
  }
332
359
  output(buyerRunOutput({
@@ -367,11 +394,16 @@ async function buyer(command, rest, flags) {
367
394
  output(buyerRunOutput({
368
395
  status: refreshed.status === "verified" ? "payment_verified" : "waiting_user_payment",
369
396
  payment_intent: refreshed,
397
+ payment_handoff: refreshed.status === "verified" ? undefined : paymentHandoff(refreshed),
370
398
  payment_guidance: paymentRecoveryGuidance(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
371
399
  agent_next_actions: refreshed.agent_next_actions || paymentAgentNextActions(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
372
400
  next: refreshed.status === "verified"
373
401
  ? { command: cliCommand("buyer", "checkout", "status", refreshed.checkout_id, "--json"), safe_for_agent: true }
374
- : { command: cliCommand("buyer", "payment", "wait", refreshed.payment_intent_id, "--json"), safe_for_agent: true }
402
+ : {
403
+ command: paymentWaitCommand(refreshed.payment_intent_id, { short: true }),
404
+ safe_for_agent: true,
405
+ instruction: "Send human_visible_markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
406
+ }
375
407
  }));
376
408
  return;
377
409
  }
@@ -792,7 +824,7 @@ async function createBuyerCheckoutFromCart(cart, selection = null, flags = {}) {
792
824
  const cartID = typeof cart === "string" ? cart : (cart?.cart_id || cart?.id);
793
825
  if (!cartID) throw new Error("cart_id is required");
794
826
  const deliveryContact = {};
795
- if (flags.email) deliveryContact.email = flags.email;
827
+ if (flags.email) deliveryContact.email = realBuyerEmail(flags.email);
796
828
  if (flags.phone) deliveryContact.phone = flags.phone;
797
829
  const missing = requiredDeliveryContactFields(selection).filter((field) => !deliveryContact[field]);
798
830
  if (missing.length) {
@@ -831,6 +863,21 @@ function requiredDeliveryContactFields(selection) {
831
863
  return Array.isArray(fields) ? fields.map((field) => String(field).trim()).filter(Boolean) : [];
832
864
  }
833
865
 
866
+ function rejectBuyerSandboxFlag(flags = {}) {
867
+ if (!flags?.sandbox) return;
868
+ throw new Error("--sandbox is not used by buyer commands; environment is selected by the ItPay API base. Run the command without --sandbox.");
869
+ }
870
+
871
+ function realBuyerEmail(value) {
872
+ const email = String(value || "").trim();
873
+ if (!email) return "";
874
+ const domain = email.split("@").pop().toLowerCase();
875
+ if (["example.com", "example.net", "example.org"].includes(domain)) {
876
+ throw new Error("placeholder email rejected; ask the human for their real delivery email before checkout.");
877
+ }
878
+ return email;
879
+ }
880
+
834
881
  async function createBuyerPaymentIntent(checkoutID, flags = {}) {
835
882
  if (!checkoutID) throw new Error("checkout_id is required");
836
883
  const method = String(flags.method || flags.payment_method || "alipay").toLowerCase();
@@ -997,6 +1044,49 @@ function paymentAgentNextActions(intent = {}, event = {}) {
997
1044
  return ["show_payment_qr_if_needed", "wait_same_payment_intent"];
998
1045
  }
999
1046
 
1047
+ function paymentHandoff(intent = {}) {
1048
+ const action = intent.human_action || {};
1049
+ const paymentIntentID = intent.payment_intent_id || action.id || "";
1050
+ const entryURL = intent.payment_entry_url || intent.payment_url || action.url || "";
1051
+ const qrPNGURL = action.qr_png_url || intent.qr_png_url || intent.qr?.png_url || "";
1052
+ const preferredQRURL = action.preferred_qr_url || qrPNGURL || action.qr_image_url || intent.qr_image_url || intent.qr?.image_url || "";
1053
+ const localQRPath = action.local_qr_path || intent.local_qr_path || "";
1054
+ const mobileWalletURL = action.mobile_wallet_url || intent.mobile_wallet_url || "";
1055
+ const waitCommand = paymentWaitCommand(paymentIntentID, { short: true });
1056
+ return {
1057
+ type: "payment_qr_handoff",
1058
+ payment_intent_id: paymentIntentID || null,
1059
+ checkout_id: intent.checkout_id || null,
1060
+ primary: localQRPath ? "local_qr_path" : (qrPNGURL ? "qr_png_url" : (preferredQRURL ? "preferred_qr_url" : "payment_entry_url")),
1061
+ local_qr_path: localQRPath || null,
1062
+ qr_png_url: qrPNGURL || null,
1063
+ preferred_qr_url: preferredQRURL || null,
1064
+ payment_entry_url: entryURL || null,
1065
+ mobile_wallet_url: mobileWalletURL || null,
1066
+ markdown: paymentHandoffMarkdown({ localQRPath, qrPNGURL, preferredQRURL, entryURL, mobileWalletURL }),
1067
+ wait_command: waitCommand || null,
1068
+ safe_for_agent: true,
1069
+ instruction: "Show markdown to the human first. Then run wait_command and wait for payment_intent.verified unless the human asked you to pause."
1070
+ };
1071
+ }
1072
+
1073
+ function paymentHandoffMarkdown({ localQRPath = "", qrPNGURL = "", preferredQRURL = "", entryURL = "", mobileWalletURL = "" } = {}) {
1074
+ const imageURL = localQRPath || qrPNGURL || preferredQRURL;
1075
+ const lines = ["请扫码付款:"];
1076
+ if (imageURL) lines.push(`![ItPay payment QR](${imageURL})`);
1077
+ if (entryURL) lines.push(`[打开付款页面](${entryURL})`);
1078
+ if (mobileWalletURL) lines.push(`[手机钱包打开](${mobileWalletURL})`);
1079
+ lines.push("我会继续等待 ItPay 返回 payment_intent.verified。");
1080
+ return lines.join("\n\n");
1081
+ }
1082
+
1083
+ function paymentWaitCommand(paymentIntentID, { short = false } = {}) {
1084
+ if (!paymentIntentID) return "";
1085
+ return short
1086
+ ? cliCommand("buyer", "payment", "wait", paymentIntentID, "--timeout", "60", "--json")
1087
+ : cliCommand("buyer", "payment", "wait", paymentIntentID, "--json");
1088
+ }
1089
+
1000
1090
  function paymentRecoveryGuidance(intent = {}, event = {}) {
1001
1091
  const paymentIntentID = intent.payment_intent_id || event.payment_intent_id || null;
1002
1092
  return {
@@ -1294,7 +1384,7 @@ async function refreshBuyerPaymentQR(paymentIntentID, flags = {}) {
1294
1384
  if (intent.status === "verified") return intent;
1295
1385
  const refreshURL = intent.qr_refresh_url;
1296
1386
  if (!refreshURL) {
1297
- throw new Error("payment intent does not expose qr_refresh_url; refresh is supported only for refreshable Alipay sandbox QR intents");
1387
+ throw new Error("payment intent does not expose qr_refresh_url; refresh is supported only for refreshable provider QR intents");
1298
1388
  }
1299
1389
  return await coreApi(refreshURL, {
1300
1390
  method: "POST",
@@ -1363,10 +1453,11 @@ function isBuyerDeliveryComplete(result) {
1363
1453
  }
1364
1454
 
1365
1455
  function buyerRunOutput(value = {}) {
1456
+ const body = compactBuyerOutput(value);
1366
1457
  return normalizeBuyerMoneyFields(stripInternalBuyerFields({
1367
1458
  schema_version: "itp.buyer.v1",
1459
+ ...body,
1368
1460
  docs: value.docs || buyerDocsFor(value),
1369
- ...value,
1370
1461
  secrets: {
1371
1462
  raw_content_included: false,
1372
1463
  claim_token_included: false,
@@ -1375,6 +1466,158 @@ function buyerRunOutput(value = {}) {
1375
1466
  }));
1376
1467
  }
1377
1468
 
1469
+ function compactBuyerOutput(value = {}) {
1470
+ const result = {};
1471
+ if (value.status !== undefined) result.status = value.status;
1472
+ if (value.payment_handoff?.markdown) {
1473
+ result.must_reply_to_human_before_next_command = true;
1474
+ result.human_visible_markdown = value.payment_handoff.markdown;
1475
+ }
1476
+ for (const [key, item] of Object.entries(value)) {
1477
+ if (["docs", "status"].includes(key)) continue;
1478
+ result[key] = compactBuyerField(key, item);
1479
+ }
1480
+ return result;
1481
+ }
1482
+
1483
+ function compactBuyerField(key, value) {
1484
+ if (key === "selection") return compactSelection(value);
1485
+ if (key === "selections") return Array.isArray(value) ? value.map(compactSelection) : value;
1486
+ if (key === "product") return compactProduct(value);
1487
+ if (key === "products") return Array.isArray(value) ? value.map(compactProduct) : value;
1488
+ if (key === "cart") return compactCart(value);
1489
+ if (key === "checkout") return compactCheckout(value);
1490
+ if (key === "payment_intent") return compactPaymentIntent(value);
1491
+ return value;
1492
+ }
1493
+
1494
+ function compactSelection(selection = {}) {
1495
+ if (!selection || typeof selection !== "object") return selection;
1496
+ return compactObject({
1497
+ catalog_item_id: selection.catalog_item_id,
1498
+ catalog_variant_id: selection.catalog_variant_id,
1499
+ ucp_variant_id: selection.ucp_variant_id,
1500
+ offer_id: selection.offer_id,
1501
+ title: selection.title,
1502
+ variant_title: selection.variant_title,
1503
+ amount: selection.expected_amount,
1504
+ currency: selection.currency,
1505
+ required_contact_fields: selection.required_contact_fields,
1506
+ purchasable: selection.purchasable
1507
+ });
1508
+ }
1509
+
1510
+ function compactProduct(product = {}) {
1511
+ if (!product || typeof product !== "object") return product;
1512
+ return compactObject({
1513
+ id: product.id,
1514
+ title: product.title,
1515
+ description: product.description,
1516
+ selected: product.selected,
1517
+ variants: Array.isArray(product.variants) ? product.variants.map(compactVariant) : undefined
1518
+ });
1519
+ }
1520
+
1521
+ function compactVariant(variant = {}) {
1522
+ const metadata = variant.metadata || {};
1523
+ return compactObject({
1524
+ id: variant.id,
1525
+ title: variant.title,
1526
+ description: variant.description,
1527
+ price: variant.price,
1528
+ availability: variant.availability,
1529
+ required_profile_fields: metadata["ai.itpay.required_profile_fields"],
1530
+ input_schema_json: metadata["ai.itpay.api.input_schema_json"],
1531
+ agent_explanation_zh: metadata["ai.itpay.agent_explanation_zh"]
1532
+ });
1533
+ }
1534
+
1535
+ function compactCart(cart = {}) {
1536
+ if (!cart || typeof cart !== "object") return cart;
1537
+ return compactObject({
1538
+ cart_id: cart.cart_id || cart.id,
1539
+ status: cart.status,
1540
+ amount: cart.amount,
1541
+ currency: cart.currency,
1542
+ line_items: Array.isArray(cart.line_items) ? cart.line_items.map(compactCartLine) : undefined
1543
+ });
1544
+ }
1545
+
1546
+ function compactCartLine(line = {}) {
1547
+ return compactObject({
1548
+ id: line.id,
1549
+ quantity: line.quantity,
1550
+ amount: line.amount,
1551
+ currency: line.currency,
1552
+ input: line.input,
1553
+ item: line.item ? compactObject({
1554
+ id: line.item.id,
1555
+ title: line.item.title,
1556
+ catalog_item_id: line.item.catalog_item_id,
1557
+ catalog_variant_id: line.item.catalog_variant_id,
1558
+ offer_id: line.item.offer_id
1559
+ }) : undefined
1560
+ });
1561
+ }
1562
+
1563
+ function compactCheckout(checkout = {}) {
1564
+ if (!checkout || typeof checkout !== "object") return checkout;
1565
+ return compactObject({
1566
+ checkout_id: checkout.checkout_id,
1567
+ cart_id: checkout.cart_id,
1568
+ order_id: checkout.order_id,
1569
+ status: checkout.status,
1570
+ delivery_status: checkout.delivery_status,
1571
+ identity_status: checkout.identity_status,
1572
+ next_required_action: checkout.next_required_action,
1573
+ payment_intent_id: checkout.payment_intent_id,
1574
+ amount: checkout.amount,
1575
+ currency: checkout.currency,
1576
+ delivery: checkout.delivery,
1577
+ human_action: checkout.human_action,
1578
+ agent_next_actions: checkout.agent_next_actions
1579
+ });
1580
+ }
1581
+
1582
+ function compactPaymentIntent(intent = {}) {
1583
+ if (!intent || typeof intent !== "object") return intent;
1584
+ return compactObject({
1585
+ payment_intent_id: intent.payment_intent_id,
1586
+ payment_attempt_id: intent.payment_attempt_id,
1587
+ checkout_id: intent.checkout_id,
1588
+ order_id: intent.order_id,
1589
+ status: intent.status,
1590
+ amount: intent.amount,
1591
+ currency: intent.currency,
1592
+ human_action: compactHumanAction(intent.human_action),
1593
+ qr_png_url: intent.qr_png_url,
1594
+ qr_image_url: intent.qr_image_url,
1595
+ mobile_wallet_url: intent.mobile_wallet_url,
1596
+ agent_wait: intent.agent_wait,
1597
+ agent_next_actions: intent.agent_next_actions
1598
+ });
1599
+ }
1600
+
1601
+ function compactHumanAction(action = null) {
1602
+ if (!action || typeof action !== "object") return action;
1603
+ return compactObject({
1604
+ kind: action.kind,
1605
+ url: action.url,
1606
+ local_qr_path: action.local_qr_path,
1607
+ local_qr_mime: action.local_qr_mime,
1608
+ qr_png_url: action.qr_png_url,
1609
+ qr_image_url: action.qr_image_url,
1610
+ preferred_qr_url: action.preferred_qr_url,
1611
+ mobile_wallet_url: action.mobile_wallet_url,
1612
+ agent_display_hint: action.agent_display_hint,
1613
+ expires_at: action.expires_at
1614
+ });
1615
+ }
1616
+
1617
+ function compactObject(value = {}) {
1618
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null));
1619
+ }
1620
+
1378
1621
  function normalizeBuyerMoneyFields(value) {
1379
1622
  if (Array.isArray(value)) return value.map((item) => normalizeBuyerMoneyFields(item));
1380
1623
  if (!value || typeof value !== "object") return value;
@@ -121,7 +121,7 @@ async function renderHumanAction(action, flags = {}) {
121
121
  }
122
122
  return { rendered: false, mode: flags.json ? "json" : mode };
123
123
  }
124
- const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
124
+ const host = agentHost(flags);
125
125
  if (["discord", "telegram", "whatsapp"].includes(host)) {
126
126
  return { rendered: false, mode: "chat-json", host };
127
127
  }
@@ -356,8 +356,8 @@ function formatActionTime(value) {
356
356
 
357
357
  function shouldUseAgentTextQR(flags = {}) {
358
358
  const mode = String(flags.display || process.env.ITP_DISPLAY || "").toLowerCase() || "auto";
359
- const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
360
- return mode === "chat" || mode === "agent" || (["gemini", "gemini-cli"].includes(host) && mode === "auto");
359
+ const host = agentHost(flags);
360
+ return mode === "chat" || mode === "agent" || (agentTextQRHosts().has(host) && mode === "auto");
361
361
  }
362
362
 
363
363
  function shouldReturnAfterAgentTextQR(flags = {}) {
@@ -365,6 +365,18 @@ function shouldReturnAfterAgentTextQR(flags = {}) {
365
365
  return shouldUseAgentTextQR(flags);
366
366
  }
367
367
 
368
+ function agentHost(flags = {}) {
369
+ const explicit = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
370
+ if (explicit) return explicit;
371
+ if (process.env.CODEX_THREAD_ID || process.env.CODEX_SHELL || process.env.CODEX_CI) return "codex";
372
+ if (process.env.CLAUDECODE || process.env.CLAUDE_CODE || process.env.CLAUDECODE_SESSION_ID) return "claude-code";
373
+ return "";
374
+ }
375
+
376
+ function agentTextQRHosts() {
377
+ return new Set(["codex", "codex-cli", "claude", "claude-code", "gemini", "gemini-cli"]);
378
+ }
379
+
368
380
  function attachAgentQRImage(action, qrImageURL, localPath = "") {
369
381
  if (!action || !qrImageURL) return action;
370
382
  action.preferred_qr_url = qrImageURL;
package/lib/runtime.js CHANGED
@@ -448,7 +448,7 @@ async function maybeMockApproveDeviceAuth(authId, flags) {
448
448
  return;
449
449
  }
450
450
  if (!fakeTestingAllowed(flags)) {
451
- throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
451
+ throw new Error("mock approval is developer-only and disabled for agent runs; use real payment-provider authentication");
452
452
  }
453
453
  const alipayUserId = flags.alipay_user_id || process.env.ITPAY_MOCK_ALIPAY_USER_ID || `2088${crypto.randomInt(100000000000, 999999999999)}`;
454
454
  await api(`/api/itp/auth/device/${encodeURIComponent(authId)}/mock-approve`, {
@@ -692,10 +692,10 @@ async function createCheckoutResult(flags) {
692
692
 
693
693
  function validateLivePaymentFlags(method, flags = {}) {
694
694
  if (String(method).toLowerCase() === "fake" && !fakeTestingAllowed(flags)) {
695
- throw new Error("fake payment is developer-only and disabled for agent runs; use --method alipay for local, sandbox, and live testing");
695
+ throw new Error("fake payment is developer-only and disabled for agent runs; use --method alipay for local and live testing");
696
696
  }
697
697
  if ((flags.mock_approve || process.env.ITPAY_MOCK_APPROVE === "true" || process.env.ITPAY_MOCK_APPROVE === "1") && !fakeTestingAllowed(flags)) {
698
- throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
698
+ throw new Error("mock approval is developer-only and disabled for agent runs; use real payment-provider authentication");
699
699
  }
700
700
  }
701
701
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "ItPay CLI, buyer skill, and agent-readable docs for agent-native commerce.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,7 +59,7 @@ read this skill
59
59
  The high-level command can wrap this flow:
60
60
 
61
61
  ```bash
62
- itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --json
62
+ itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
63
63
  ```
64
64
 
65
65
  For step-by-step testing:
@@ -93,7 +93,7 @@ itp buyer cart create --variant var_itpay_enterprise_fuzzy_search_cny01 --input
93
93
  itp buyer cart show <cart_id> --json
94
94
  itp buyer cart add <cart_id> --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=美团 --json
95
95
  itp buyer cart create --variant var_itpay_enterprise_precise_lookup_cny05 --input company_name_or_credit_no=北京京东世纪贸易有限公司 --json
96
- itp buy var_itpay_enterprise_fuzzy_search_cny01 --sandbox --email <buyer_email> --input company_name=京东 --json
96
+ itp buy var_itpay_enterprise_fuzzy_search_cny01 --email <buyer_email> --input company_name=京东 --display agent --no-wait-payment --json
97
97
  ```
98
98
 
99
99
  For cart edits, always read the server cart first with `buyer cart show`.
@@ -206,20 +206,23 @@ Each docs page includes `next_docs`. Follow those links as the state changes.
206
206
  For payment creation in an agent/chat client, prefer:
207
207
 
208
208
  ```bash
209
- itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --json
209
+ itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
210
210
  ```
211
211
 
212
212
  This keeps JSON output machine-readable while allowing the CLI to prepare a
213
- local QR image path for clients that cannot render remote SVG reliably. If the
214
- human is on mobile, present `mobile_wallet_url` as a clickable human-only
215
- fallback; do not convert it into a QR.
213
+ local QR image path for clients that cannot render remote SVG reliably. In
214
+ Codex or Claude Code app clients, prefer `--no-wait-payment`: send
215
+ `human_visible_markdown` to the human first, then run
216
+ `next.command`. If the human is on mobile, present
217
+ `mobile_wallet_url` as a clickable human-only fallback; do not convert it into a
218
+ QR.
216
219
 
217
220
  For first-purchase auth, treat the returned ItPay authorization entry as a
218
221
  single human orchestration entry. It may open Alipay login/registration first
219
222
  and then payment after ItPay receives the OAuth callback. Do not call
220
223
  `oauth_start_url` directly unless the ItPay auth page asks for fallback.
221
- If the payment page says the Alipay sandbox entry is stabilizing/preparing,
222
- or if Alipay sandbox says "order not found", tell the human to wait 30-60
224
+ If the payment page says provider entry is stabilizing/preparing,
225
+ or if the payment provider says "order not found", tell the human to wait 30-60
223
226
  seconds and use the same page/QR again. Do not ask them to refresh repeatedly,
224
227
  and do not create another checkout or payment intent. Use
225
228
  `itp buyer payment refresh-qr ... --reason order-not-found` only after the
@@ -235,7 +238,7 @@ I found the service and selected the matching variant.
235
238
  I created the cart and checkout.
236
239
  Please open the returned ItPay authorization link and approve Alipay login.
237
240
  I am waiting for ItPay account authorization.
238
- Please scan the returned ItPay-hosted QR image with Alipay sandbox.
241
+ Please scan the returned ItPay-hosted QR image with the payment provider.
239
242
  I am waiting for ItPay payment verification.
240
243
  Payment is verified.
241
244
  Delivery is claimable by the human buyer. Please check your email.