@itpay/cli 0.2.9 → 0.2.11

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
@@ -239,9 +239,9 @@ 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
- - If you know the current chat channel and `render_plan.platforms.<channel>` exists, use that branch before `render_plan.selected`.
243
- - In OpenClaw Telegram, send `render_plan.platforms.telegram.openclaw_message` with OpenClaw `message send --media ... --presentation ...`; do not put legacy inline media directives inside human prose.
244
- - If the channel is unsupported, use `render_plan.selected.fallback` / plain chat. Do not rewrite the chosen plan into a custom table or omit the real URL.
242
+ - If `human_output_required=true`, send root `human_output` before any next ItPay command. It is not optional UI decoration.
243
+ - In OpenClaw Telegram, pass `human_output.message/media/presentation` to `openclaw message send`; buttons use native `text/url/callback_data`.
244
+ - `render_plan` is diagnostic/compatibility detail. Do not choose a shorter fallback when root `human_output` is present.
245
245
  - If status is `payment_handoff_required`, `next` is the human reply step, not payment wait.
246
246
  - Treat only `payment_intent.verified` as payment success.
247
247
 
@@ -26,7 +26,7 @@
26
26
  {
27
27
  "intent": "create payment intent through the high-level buy flow",
28
28
  "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
29
- "success_signal": "response.status is payment_handoff_required; response.human_visible_markdown and response.render_plan are present; render the QR/link to the human before any status check"
29
+ "success_signal": "response.status is payment_handoff_required; response.human_output_required is true; send response.human_output to the human before any status check"
30
30
  },
31
31
  {
32
32
  "intent": "recover a scanner order-not-found display problem",
@@ -41,14 +41,12 @@
41
41
  "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.",
42
42
  "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.",
43
43
  "Do not describe auth_qr as payment success or payment proof. Only payment_intent.verified proves payment.",
44
- "If you know the current chat channel and response.render_plan.platforms.<channel> exists, use that channel branch before response.render_plan.selected.",
45
- "If response.render_plan.selected.platform is current_channel_required, choose the branch matching the current channel; selected.fallback is only for unsupported plain chat clients.",
46
- "Do not rewrite the chosen render plan into a custom table. Use its text/markdown, media, links, and buttons as provided.",
47
- "If selected.links exists, include the real URL. Do not only show the link label.",
48
- "If selected.buttons exists but the current adapter cannot render native buttons, degrade to selected.links plus short text; do not fake payment success or skip the status check.",
49
- "In OpenClaw Telegram, prefer response.render_plan.platforms.telegram.openclaw_message: send its message/media/presentation through OpenClaw message send. Do not place legacy inline media directives inside prose; that fallback cannot render inline buttons.",
50
- "In Codex or Claude Code app clients, send response.human_visible_markdown or response.render_plan.platforms.codex_app.markdown to the human first. Do not start a hidden long wait before the human-visible message is sent.",
51
- "For Telegram-style message clients, use response.render_plan.platforms.telegram: send photo media first, include links, and expose native buttons for refresh/status when the adapter supports them.",
44
+ "If response.human_output_required is true, send response.human_output before any next ItPay command. It is the mandatory human-facing output, not decoration.",
45
+ "Do not rewrite response.human_output into a custom table. Use its text/markdown, media, links, and buttons as provided.",
46
+ "If human_output links or buttons include URLs, include the real URL. Do not only show the link label.",
47
+ "In OpenClaw Telegram, pass human_output.message/media/presentation to openclaw message send. Its presentation.blocks[].buttons[] uses native text/url/callback_data; do not simulate buttons in normal prose.",
48
+ "In Codex or Claude Code app clients, send response.human_output.markdown first. Do not start a hidden long wait before the human-visible message is sent.",
49
+ "For Telegram-style message clients, send photo media first, include links, and expose native buttons for refresh/status when the adapter supports them.",
52
50
  "For plain chat clients, show response.human_visible_markdown and stop unless the human asks you to check status or returns with a payment question.",
53
51
  "When response.status is payment_handoff_required, response.next is the human reply step, not payment wait.",
54
52
  "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.",
@@ -35,7 +35,7 @@
35
35
  }
36
36
  ],
37
37
  "agent_rules": [
38
- "If payment wait returns payment_handoff_required, render response.render_plan.platforms.<current channel> when available; otherwise use response.render_plan.selected/fallback before running any later status check.",
38
+ "If payment wait returns payment_handoff_required, send response.human_output before running any later status check.",
39
39
  "Start payment status checks only after the QR image/link has already been sent to the human or the human asks to check status.",
40
40
  "If you run payment wait without a short timeout while payment is still pending, CLI returns payment_handoff_required and human_visible_markdown instead of long-polling.",
41
41
  "Payment wait/status check is a recovery loop around the same payment_intent_id, not a one-shot command.",
@@ -45,7 +45,7 @@
45
45
  {
46
46
  "intent": "buy a selected variant with the high-level cart-first flow",
47
47
  "command": "itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
48
- "success_signal": "response.status becomes waiting_human_auth or payment_handoff_required; if payment_handoff_required, render response.human_visible_markdown or response.render_plan to the human before any payment status check"
48
+ "success_signal": "response.status becomes waiting_human_auth or payment_handoff_required; if human_output_required is true, send response.human_output before any payment status check"
49
49
  }
50
50
  ],
51
51
  "agent_rules": [
@@ -62,13 +62,13 @@
62
62
  "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.",
63
63
  "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.",
64
64
  "Vault grants are only for reading approved delivered artifacts. They are not buyer session credentials and do not authorize refunds or account/order management.",
65
- "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/render_plan, then only check status after the human responds or uses a platform button.",
66
- "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.",
65
+ "Use high-level itp buy when possible, but in agent app clients split payment into two visible steps: first return and send human_output, then only check status after the human responds or uses a platform button.",
66
+ "If response.human_output_required is true, send response.human_output immediately in the same user-visible message. It includes the QR image/link/buttons.",
67
67
  "If response.status is payment_handoff_required, do not run payment wait from next.command; next is the human-visible reply. Only after the human asks to check payment should you run after_human_response.check_payment_command.",
68
68
  "Show the returned payment QR exactly as provided before starting payment wait.",
69
69
  "Payment truth comes only from payment_intent.verified.",
70
70
  "Secure delivery goes to the human first; report redacted delivery status only.",
71
- "When a response includes render_plan, first use render_plan.platforms.<current channel> if it exists. If selected.platform is current_channel_required, selected.fallback is only for unsupported plain chat. In OpenClaw Telegram, use render_plan.platforms.telegram.openclaw_message instead of handwritten legacy media directive prose."
71
+ "When response.human_output_required is true, root human_output is the execution path. render_plan is diagnostic/compatibility detail. In OpenClaw Telegram, pass human_output.message/media/presentation to openclaw message send instead of handwritten prose."
72
72
  ],
73
73
  "forbidden": [
74
74
  "Do not call ops commands from the buyer path.",
package/lib/buyer.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  writeCredentials, writeSessionCredentials, writeState
5
5
  } from "./env.js";
6
6
  import { coreApi, coreApiBase } from "./http.js";
7
- import { buildHumanActionRenderPlan, renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
7
+ import { buildHumanActionRenderPlan, humanOutputFromRenderPlan, renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
8
8
 
9
9
  async function buyerBuy(flags) {
10
10
  rejectBuyerSandboxFlag(flags);
@@ -1512,6 +1512,12 @@ function compactBuyerOutput(value = {}) {
1512
1512
  result.must_reply_to_human_before_next_command = true;
1513
1513
  result.human_visible_markdown = value.payment_handoff.markdown;
1514
1514
  }
1515
+ const humanOutput = humanOutputFromRenderPlan(value.render_plan);
1516
+ if (humanOutput) {
1517
+ result.human_output_required = true;
1518
+ result.must_send_human_output_before_next_command = true;
1519
+ result.human_output = humanOutput;
1520
+ }
1515
1521
  for (const [key, item] of Object.entries(value)) {
1516
1522
  if (["docs", "status"].includes(key)) continue;
1517
1523
  result[key] = compactBuyerField(key, item);
@@ -286,8 +286,9 @@ function annotateHumanActionPresentation(action, qrImageURL) {
286
286
  function buildHumanActionRenderPlan(action = {}, intent = {}, flags = {}) {
287
287
  if (!action || typeof action !== "object") return null;
288
288
  const kind = action.kind || (isPaymentIntentHandoff(intent) ? "payment_qr" : "human_action");
289
- const planAction = action.kind === kind ? action : { ...action, kind };
290
289
  const entryURL = intent.payment_entry_url || intent.payment_url || action.url || action.web_url || "";
290
+ const checkoutID = intent.checkout_id || action.checkout_id || checkoutIDFromURL(entryURL);
291
+ const planAction = compactObject({ ...(action.kind === kind ? action : { ...action, kind }), checkout_id: checkoutID || undefined });
291
292
  const qrPNGURL = action.qr_png_url || intent.qr_png_url || intent.qr?.png_url || "";
292
293
  const preferredQRURL = action.preferred_qr_url || qrPNGURL || action.qr_image_url || intent.qr_image_url || intent.qr?.image_url || "";
293
294
  const localQRPath = action.local_qr_path || intent.local_qr_path || "";
@@ -371,7 +372,10 @@ function telegramRenderPlan(action = {}, { localQRPath = "", preferredQRURL = ""
371
372
  { text: "支付遇到问题 / 刷新", intent: "refresh_payment_qr" },
372
373
  { text: "我已付款,查询状态", intent: "check_payment_status" }
373
374
  ]
374
- : entryURL ? [{ text: "打开授权页面", url: entryURL }] : [];
375
+ : [
376
+ ...(entryURL ? [{ text: "打开授权页面", url: entryURL }] : []),
377
+ ...(action.checkout_id ? [{ text: "我已完成,查询状态", intent: "check_checkout_status" }] : [])
378
+ ];
375
379
  return {
376
380
  format: action.kind === "payment_qr" ? "photo_text_inline_buttons" : "text_inline_buttons",
377
381
  media,
@@ -389,16 +393,19 @@ function openclawTelegramMessage({ action = {}, media = [], links = [], buttons
389
393
  const text = action.kind === "payment_qr"
390
394
  ? "请扫码或点击链接完成支付。付款完成后点“我已付款,查询状态”,或直接回复“我已付款”。"
391
395
  : "请打开 ItPay 授权入口,完成后回到当前对话。";
392
- const presentationButtons = links.map((link) => ({ label: link.label, url: link.url }));
396
+ const presentationButtons = links.map((link) => openclawURLButton(link.label, link.url));
393
397
  if (action.kind === "payment_qr") {
394
398
  presentationButtons.push(
395
- { label: "支付遇到问题 / 刷新", action: { type: "command", command: "刷新付款二维码" } },
396
- { label: "我已付款,查询状态", action: { type: "command", command: "我已付款,查询状态" } }
399
+ openclawCallbackButton("支付遇到问题 / 刷新", "refresh_payment_qr", action),
400
+ openclawCallbackButton("我已付款,查询状态", "check_payment_status", action)
397
401
  );
398
402
  } else {
399
403
  for (const button of buttons) {
400
404
  if (button.url && !presentationButtons.some((item) => item.url === button.url)) {
401
- presentationButtons.push({ label: button.text || "打开链接", url: button.url });
405
+ presentationButtons.push(openclawURLButton(button.text || "打开链接", button.url));
406
+ }
407
+ if (button.intent === "check_checkout_status") {
408
+ presentationButtons.push(openclawCallbackButton("我已完成,查询状态", "check_checkout_status", action));
402
409
  }
403
410
  }
404
411
  }
@@ -419,10 +426,92 @@ function openclawTelegramMessage({ action = {}, media = [], links = [], buttons
419
426
  message: text,
420
427
  media: mediaURL || undefined,
421
428
  presentation
429
+ }),
430
+ callbacks: openclawCallbackCommands(action)
431
+ });
432
+ }
433
+
434
+ function humanOutputFromRenderPlan(plan = {}) {
435
+ if (!plan || typeof plan !== "object") return null;
436
+ const selected = plan.selected || {};
437
+ const platforms = plan.platforms || {};
438
+ const base = {
439
+ required: true,
440
+ kind: plan.kind,
441
+ proof_rule: plan.proof_rule,
442
+ instruction: "Send this human_output to the human before running the next ItPay command."
443
+ };
444
+ if (selected.platform === "telegram") return { ...base, ...openclawHumanOutput(selected.openclaw_message) };
445
+ if (selected.platform === "codex_app" || selected.platform === "claude_code") return { ...base, method: "markdown_reply", markdown: selected.markdown };
446
+ if (selected.platform === "terminal") return { ...base, method: "terminal_display", print_terminal_qr: true, print_links: true };
447
+ if (selected.platform && selected.platform !== "current_channel_required") return { ...base, ...plainChatHumanOutput(selected) };
448
+ return {
449
+ ...base,
450
+ method: "current_channel_required",
451
+ instruction: "Choose the branch matching the current chat platform, send it to the human, then stop until the human responds or clicks a platform button.",
452
+ channels: compactObject({
453
+ openclaw_telegram: openclawHumanOutput(platforms.telegram?.openclaw_message),
454
+ codex_app: { method: "markdown_reply", markdown: platforms.codex_app?.markdown },
455
+ claude_code: { method: "markdown_reply", markdown: platforms.claude_code?.markdown },
456
+ plain_chat: plainChatHumanOutput(platforms.plain_chat),
457
+ terminal: { method: "terminal_display", print_terminal_qr: true, print_links: true }
422
458
  })
459
+ };
460
+ }
461
+
462
+ function openclawHumanOutput(message = {}) {
463
+ if (!message) return null;
464
+ return compactObject({
465
+ method: "openclaw_message_send",
466
+ channel: "telegram",
467
+ message: message.command_args?.message || message.message,
468
+ media: message.command_args?.media || message.media,
469
+ presentation: message.command_args?.presentation || message.presentation,
470
+ callbacks: message.callbacks
471
+ });
472
+ }
473
+
474
+ function plainChatHumanOutput(plan = {}) {
475
+ if (!plan) return null;
476
+ return compactObject({
477
+ method: "plain_chat_reply",
478
+ text: plan.text,
479
+ links: plan.links
423
480
  });
424
481
  }
425
482
 
483
+ function openclawURLButton(text, url) {
484
+ return compactObject({ text, url });
485
+ }
486
+
487
+ function openclawCallbackButton(text, intent, action = {}) {
488
+ const id = action.payment_intent_id || action.checkout_id || action.id || "current";
489
+ return { text, callback_data: `itp:${intent}:${id}` };
490
+ }
491
+
492
+ function openclawCallbackCommands(action = {}) {
493
+ if (action.kind === "payment_qr") {
494
+ const id = action.payment_intent_id || action.id || "";
495
+ return compactObject({
496
+ refresh_payment_qr: id ? `itp buyer payment refresh-qr ${id} --json` : undefined,
497
+ check_payment_status: id ? `itp buyer payment wait ${id} --timeout 1 --json` : undefined
498
+ });
499
+ }
500
+ if (action.checkout_id) {
501
+ return { check_checkout_status: `itp buyer checkout resume ${action.checkout_id} --json` };
502
+ }
503
+ return undefined;
504
+ }
505
+
506
+ function checkoutIDFromURL(value = "") {
507
+ try {
508
+ const url = new URL(value);
509
+ return url.pathname.match(/\/checkouts\/([^/]+)/)?.[1] || "";
510
+ } catch {
511
+ return String(value || "").match(/\/checkouts\/([^/?#]+)/)?.[1] || "";
512
+ }
513
+ }
514
+
426
515
  function selectedRenderPlatform(host = "") {
427
516
  const normalized = String(host || "").toLowerCase().replaceAll("-", "_");
428
517
  if (["codex", "codex_app", "codex_cli"].includes(normalized)) return "codex_app";
@@ -691,4 +780,4 @@ function openBrowser(targetURL) {
691
780
  return false;
692
781
  }
693
782
 
694
- export { renderItPayPaymentAction, humanActionSummaryLines, writeHumanActionSummary, waitHeartbeatMs, writeWaitHeartbeat, renderHumanAction, buildHumanActionRenderPlan, preferredHumanActionQRURL, humanActionPresentationURL, annotateHumanActionPresentation, shouldPrepareLocalQRForJSON, shouldGenerateLocalQRFromActionURL, prepareLocalQRFile, prepareLocalQRFromActionURL, defaultQRFilePath, defaultGeneratedQRFilePath, qrFileExtension, qrMimeType, sanitizeFilename, formatActionTime, shouldUseAgentTextQR, shouldReturnAfterAgentTextQR, attachAgentQRImage, attachAgentLocalQR, downloadQRImage, persistHumanAction, shouldRenderTerminalQR, terminalQRType, shouldOpenBrowser, openBrowser };
783
+ export { renderItPayPaymentAction, humanActionSummaryLines, writeHumanActionSummary, waitHeartbeatMs, writeWaitHeartbeat, renderHumanAction, buildHumanActionRenderPlan, humanOutputFromRenderPlan, preferredHumanActionQRURL, humanActionPresentationURL, annotateHumanActionPresentation, shouldPrepareLocalQRForJSON, shouldGenerateLocalQRFromActionURL, prepareLocalQRFile, prepareLocalQRFromActionURL, defaultQRFilePath, defaultGeneratedQRFilePath, qrFileExtension, qrMimeType, sanitizeFilename, formatActionTime, shouldUseAgentTextQR, shouldReturnAfterAgentTextQR, attachAgentQRImage, attachAgentLocalQR, downloadQRImage, persistHumanAction, shouldRenderTerminalQR, terminalQRType, shouldOpenBrowser, openBrowser };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "ItPay CLI, buyer skill, and agent-readable docs for agent-native commerce.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -211,13 +211,13 @@ itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent
211
211
 
212
212
  This keeps JSON output machine-readable while allowing the CLI to prepare a
213
213
  local QR image path for clients that cannot render remote SVG reliably. In
214
- agent/chat clients, prefer `--no-wait-payment`: if you know the current channel
215
- and `render_plan.platforms.<channel>` exists, use that branch first. In Codex
216
- or Claude Code app clients, send the platform markdown exactly. In OpenClaw
217
- Telegram, use `render_plan.platforms.telegram.openclaw_message` so media and
218
- native buttons are sent through the adapter. If `selected.platform` is
219
- `current_channel_required`, `selected.fallback` is only for unsupported plain
220
- chat clients. Do not invent a table or rewrite the UI plan.
214
+ agent/chat clients, prefer `--no-wait-payment`: if `human_output_required=true`,
215
+ send root `human_output` before any next ItPay command. In Codex or Claude Code
216
+ app clients, send `human_output.markdown` exactly. In OpenClaw Telegram, pass
217
+ `human_output.message/media/presentation` to `openclaw message send` so media
218
+ and native `text/url/callback_data` buttons are sent through the adapter.
219
+ `render_plan` is diagnostic/compatibility detail. Do not invent a table or
220
+ rewrite the UI plan.
221
221
 
222
222
  If a response has `status=payment_handoff_required`, `next` is the user-visible
223
223
  reply step, not payment wait. Do not run `buyer payment wait` until the human