@itpay/cli 0.2.12 → 0.2.14

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.
@@ -0,0 +1,126 @@
1
+ const SUPPORTED_HOSTS = new Set(["codex", "claude-code", "telegram", "discord", "whatsapp", "terminal", "plain-chat"]);
2
+ const RUNTIME_TARGETS = new Set(["codex", "claude-code", "openclaw", "generic"]);
3
+ const EXEMPT_GROUPS = new Set(["", "--help", "-h", "help", "version", "docs", "skill", "doctor", "install"]);
4
+
5
+ function clientContextGate(args = []) {
6
+ const group = args[0] || "";
7
+ if (EXEMPT_GROUPS.has(group)) return null;
8
+ const flags = parseArgFlags(args.slice(1));
9
+ const host = clientHost(flags);
10
+ if (!host) return clientContextRequired();
11
+ if (!SUPPORTED_HOSTS.has(host)) return unsupportedClientContext(host);
12
+ if (requiresTarget(host) && !clientTarget(flags)) return clientTargetRequired(host);
13
+ return null;
14
+ }
15
+
16
+ function clientHost(flags = {}) {
17
+ const raw = firstValue(
18
+ flags.host,
19
+ flags.client,
20
+ flags.channel,
21
+ argFlag("host"),
22
+ argFlag("client"),
23
+ argFlag("channel")
24
+ );
25
+ const host = normalizeHost(raw);
26
+ if (host === "openclaw" && flags.channel) return normalizeHost(flags.channel);
27
+ return host;
28
+ }
29
+
30
+ function clientTarget(flags = {}) {
31
+ const target = firstValue(flags.chat_target, flags.reply_target, argFlag("chat-target"), argFlag("reply-target"));
32
+ if (target) return target;
33
+ const flagTarget = flags.target || argFlag("target");
34
+ return flagTarget && !RUNTIME_TARGETS.has(normalizeHost(flagTarget)) ? String(flagTarget) : "";
35
+ }
36
+
37
+ function clientCommandArgs(flags = {}) {
38
+ const host = clientHost(flags);
39
+ const target = clientTarget(flags);
40
+ return [
41
+ ...(host ? ["--host", host] : []),
42
+ ...(target ? ["--target", target] : [])
43
+ ];
44
+ }
45
+
46
+ function requiresTarget(host) {
47
+ return host === "telegram" || host === "discord" || host === "whatsapp";
48
+ }
49
+
50
+ function clientContextRequired() {
51
+ return {
52
+ schema_version: "itp.client_context.v1",
53
+ status: "client_context_required",
54
+ must_rerun: true,
55
+ instruction: "Rerun the same itp command with --host <client>. ItPay will not guess the current chat/app client.",
56
+ allowed_hosts: Array.from(SUPPORTED_HOSTS),
57
+ examples: {
58
+ codex: "itp ... --host codex --json",
59
+ claude_code: "itp ... --host claude-code --json",
60
+ telegram: "itp ... --host telegram --target telegram:<chat_id> --json",
61
+ terminal: "itp ... --host terminal --json"
62
+ }
63
+ };
64
+ }
65
+
66
+ function clientTargetRequired(host) {
67
+ return {
68
+ schema_version: "itp.client_context.v1",
69
+ status: "client_target_required",
70
+ must_rerun: true,
71
+ host,
72
+ instruction: "Rerun the same itp command with the current chat target. For OpenClaw Telegram group/private chat, pass inbound_meta.chat_id as --target.",
73
+ examples: {
74
+ telegram_private: "itp ... --host telegram --target telegram:5559456744 --json",
75
+ telegram_group: "itp ... --host telegram --target telegram:-1001234567890 --json",
76
+ discord: "itp ... --host discord --target discord:<channel_id> --json"
77
+ }
78
+ };
79
+ }
80
+
81
+ function unsupportedClientContext(host) {
82
+ return {
83
+ schema_version: "itp.client_context.v1",
84
+ status: "unsupported_client_context",
85
+ must_rerun: true,
86
+ host,
87
+ instruction: "Use one supported --host value so ItPay can return a single executable human-output instruction.",
88
+ allowed_hosts: Array.from(SUPPORTED_HOSTS)
89
+ };
90
+ }
91
+
92
+ function normalizeHost(value = "") {
93
+ const normalized = String(value || "").trim().toLowerCase().replaceAll("_", "-");
94
+ if (["tg", "openclaw-telegram"].includes(normalized)) return "telegram";
95
+ if (["codex-app", "codex-cli"].includes(normalized)) return "codex";
96
+ if (["claude", "claudecode", "claude-code-app"].includes(normalized)) return "claude-code";
97
+ if (["plain", "plain-chat", "chat"].includes(normalized)) return "plain-chat";
98
+ return normalized;
99
+ }
100
+
101
+ function parseArgFlags(args = []) {
102
+ const flags = {};
103
+ for (let i = 0; i < args.length; i += 1) {
104
+ const arg = args[i];
105
+ if (!String(arg).startsWith("--")) continue;
106
+ const key = String(arg).slice(2).replaceAll("-", "_");
107
+ const next = args[i + 1];
108
+ if (!next || String(next).startsWith("--")) {
109
+ flags[key] = true;
110
+ } else {
111
+ flags[key] = next;
112
+ i += 1;
113
+ }
114
+ }
115
+ return flags;
116
+ }
117
+
118
+ function argFlag(name) {
119
+ return parseArgFlags(process.argv.slice(2))[String(name).replaceAll("-", "_")] || "";
120
+ }
121
+
122
+ function firstValue(...values) {
123
+ return values.find((value) => value !== undefined && value !== null && value !== false && String(value).trim() !== "") || "";
124
+ }
125
+
126
+ export { clientCommandArgs, clientContextGate, clientHost, clientTarget };
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import QRCode from "qrcode";
6
+ import { clientCommandArgs, clientHost, clientTarget } from "./client-context.js";
6
7
  import { apiTimeoutMs, cliCommand, commandExists, mergeRun, readRun, readState, safeErrorMessage, writeRun } from "./env.js";
7
8
 
8
9
  async function renderItPayPaymentAction(intent, flags = {}) {
@@ -423,6 +424,8 @@ function openclawTelegramMessage({ action = {}, media = [], links = [], buttons
423
424
  media: mediaURL || undefined,
424
425
  presentation,
425
426
  command_args: compactObject({
427
+ channel: "telegram",
428
+ target: clientTarget() || undefined,
426
429
  message: text,
427
430
  media: mediaURL || undefined,
428
431
  presentation
@@ -431,55 +434,6 @@ function openclawTelegramMessage({ action = {}, media = [], links = [], buttons
431
434
  });
432
435
  }
433
436
 
434
- function humanOutputFromRenderPlan(plan = {}) {
435
- if (!plan || typeof plan !== "object" || !plan.kind || !plan.selected) 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 }
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
480
- });
481
- }
482
-
483
437
  function openclawURLButton(text, url) {
484
438
  return compactObject({ text, url });
485
439
  }
@@ -490,15 +444,16 @@ function openclawCallbackButton(text, intent, action = {}) {
490
444
  }
491
445
 
492
446
  function openclawCallbackCommands(action = {}) {
447
+ const contextArgs = clientCommandArgs();
493
448
  if (action.kind === "payment_qr") {
494
449
  const id = action.payment_intent_id || action.id || "";
495
450
  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
451
+ refresh_payment_qr: id ? cliCommand("buyer", "payment", "refresh-qr", id, ...contextArgs, "--json") : undefined,
452
+ check_payment_status: id ? cliCommand("buyer", "payment", "wait", id, "--timeout", "1", ...contextArgs, "--json") : undefined
498
453
  });
499
454
  }
500
455
  if (action.checkout_id) {
501
- return { check_checkout_status: `itp buyer checkout resume ${action.checkout_id} --json` };
456
+ return { check_checkout_status: cliCommand("buyer", "checkout", "resume", action.checkout_id, ...contextArgs, "--json") };
502
457
  }
503
458
  return undefined;
504
459
  }
@@ -526,14 +481,7 @@ function selectedRenderPlan(host = "", platforms = {}) {
526
481
  if (selectedPlatform && platforms[selectedPlatform]) {
527
482
  return { platform: selectedPlatform, ...platforms[selectedPlatform] };
528
483
  }
529
- return compactObject({
530
- platform: "current_channel_required",
531
- rule: "Use render_plan.platforms.<current channel> when available; fallback is only for unsupported plain chat clients.",
532
- telegram: platforms.telegram,
533
- codex_app: platforms.codex_app,
534
- claude_code: platforms.claude_code,
535
- fallback: platforms.plain_chat
536
- });
484
+ return { platform: "plain_chat", ...platforms.plain_chat };
537
485
  }
538
486
 
539
487
  function compactObject(value = {}) {
@@ -641,20 +589,7 @@ function shouldReturnAfterAgentTextQR(flags = {}) {
641
589
  }
642
590
 
643
591
  function agentHost(flags = {}) {
644
- const explicit = String(
645
- process.env.ITP_HOST ||
646
- flags.host ||
647
- flags.channel ||
648
- process.env.OPENCLAW_CURRENT_CHANNEL_PROVIDER ||
649
- process.env.OPENCLAW_CHANNEL ||
650
- process.env.CURRENT_CHANNEL_PROVIDER ||
651
- process.env.AGENT_CHANNEL ||
652
- ""
653
- ).toLowerCase();
654
- if (explicit) return explicit;
655
- if (process.env.CODEX_THREAD_ID || process.env.CODEX_SHELL || process.env.CODEX_CI) return "codex";
656
- if (process.env.CLAUDECODE || process.env.CLAUDE_CODE || process.env.CLAUDECODE_SESSION_ID) return "claude-code";
657
- return "";
592
+ return clientHost(flags);
658
593
  }
659
594
 
660
595
  function agentTextQRHosts() {
@@ -780,4 +715,4 @@ function openBrowser(targetURL) {
780
715
  return false;
781
716
  }
782
717
 
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 };
718
+ 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "ItPay CLI, buyer skill, and agent-readable docs for agent-native commerce.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,10 +20,33 @@ protocol from this file. Use the CLI docs graph whenever you need details.
20
20
 
21
21
  ## Start Here
22
22
 
23
+ Every non-doc ItPay command must include the current client context. The CLI
24
+ will stop with `client_context_required` or `client_target_required` rather
25
+ than guess.
26
+
27
+ ```text
28
+ Codex app: --host codex
29
+ Claude Code app: --host claude-code
30
+ Terminal: --host terminal
31
+ Plain chat: --host plain-chat
32
+ OpenClaw Telegram private/group chat: --host telegram --target <inbound_meta.chat_id>
33
+ ```
34
+
35
+ For Telegram groups or @mention flows, use the same OpenClaw inbound
36
+ `chat_id` as the target, for example `--target telegram:-1001234567890`.
37
+ OpenClaw owns group allowlists and mention policy; ItPay only echoes the target
38
+ into the returned `agent_action`.
39
+
40
+ If a response contains `agent_action`, run `agent_action.command` or
41
+ `agent_action.argv` before writing any normal reply. Do not summarize it, do
42
+ not rewrite it as Markdown, and do not run wait/status first. If the command
43
+ cannot run, stop and tell the human the agent lacks the required OpenClaw
44
+ message tool.
45
+
23
46
  Run these commands before buying:
24
47
 
25
48
  ```bash
26
- itp status --refresh --json
49
+ itp status --refresh --host <client> --json
27
50
  itp docs show quickstart --role buyer --json
28
51
  itp docs list --role buyer --json
29
52
  ```
@@ -59,41 +82,41 @@ read this skill
59
82
  The high-level command can wrap this flow:
60
83
 
61
84
  ```bash
62
- itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
85
+ itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --host <client> --json
63
86
  ```
64
87
 
65
88
  For step-by-step testing:
66
89
 
67
90
  ```bash
68
- itp buyer catalog search --query "<user request>" --json
69
- itp buyer catalog search --query "企业工商信息 查询" --category business_data_api --provider itpay_enterprise_data --service-type ai_api --json
70
- itp buyer catalog get --variant <variant_id> --json
71
- itp buyer cart create --variant <variant_id> --json
72
- itp buyer cart create --variants <variant_id_1>,<variant_id_2> --quantities 1,1 --json
73
- itp buyer cart show <cart_id> --json
74
- itp buyer cart add <cart_id> --variant <variant_id> --input key=value --quantity 1 --json
75
- itp buyer cart remove <cart_id> --line <cart_line_item_id> --json
76
- itp buyer checkout create --cart <cart_id> --email <buyer_email> --phone <buyer_phone> --json
77
- itp buyer checkout resume <checkout_id> --json
78
- itp buyer payment wait <payment_intent_id> --timeout 1 --json
79
- itp buyer checkout status <checkout_id> --json
80
- itp buyer refund create --order <order_id> --amount-minor <minor_units> --currency CNY --reason buyer_requested --json
81
- itp buyer refund list --order <order_id> --json
82
- itp buyer refund show <refund_id> --json
83
- itp buyer refund cancel <refund_id> --reason buyer_changed_mind --json
84
- itp buyer vault grants list --checkout <checkout_id> --json
85
- itp buyer vault read --order <order_id> --artifact <vault_artifact_id> --json
91
+ itp buyer catalog search --query "<user request>" --host <client> --json
92
+ itp buyer catalog search --query "企业工商信息 查询" --category business_data_api --provider itpay_enterprise_data --service-type ai_api --host <client> --json
93
+ itp buyer catalog get --variant <variant_id> --host <client> --json
94
+ itp buyer cart create --variant <variant_id> --host <client> --json
95
+ itp buyer cart create --variants <variant_id_1>,<variant_id_2> --quantities 1,1 --host <client> --json
96
+ itp buyer cart show <cart_id> --host <client> --json
97
+ itp buyer cart add <cart_id> --variant <variant_id> --input key=value --quantity 1 --host <client> --json
98
+ itp buyer cart remove <cart_id> --line <cart_line_item_id> --host <client> --json
99
+ itp buyer checkout create --cart <cart_id> --email <buyer_email> --phone <buyer_phone> --host <client> --json
100
+ itp buyer checkout resume <checkout_id> --host <client> --json
101
+ itp buyer payment wait <payment_intent_id> --timeout 1 --host <client> --json
102
+ itp buyer checkout status <checkout_id> --host <client> --json
103
+ itp buyer refund create --order <order_id> --amount-minor <minor_units> --currency CNY --reason buyer_requested --host <client> --json
104
+ itp buyer refund list --order <order_id> --host <client> --json
105
+ itp buyer refund show <refund_id> --host <client> --json
106
+ itp buyer refund cancel <refund_id> --reason buyer_changed_mind --host <client> --json
107
+ itp buyer vault grants list --checkout <checkout_id> --host <client> --json
108
+ itp buyer vault read --order <order_id> --artifact <vault_artifact_id> --host <client> --json
86
109
  ```
87
110
 
88
111
  For API products, read the product metadata input schema before cart creation.
89
112
  Enterprise data products require query input at cart time:
90
113
 
91
114
  ```bash
92
- itp buyer cart create --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=京东 --json
93
- itp buyer cart show <cart_id> --json
94
- itp buyer cart add <cart_id> --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=美团 --json
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 --email <buyer_email> --input company_name=京东 --display agent --no-wait-payment --json
115
+ itp buyer cart create --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=京东 --host <client> --json
116
+ itp buyer cart show <cart_id> --host <client> --json
117
+ itp buyer cart add <cart_id> --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=美团 --host <client> --json
118
+ itp buyer cart create --variant var_itpay_enterprise_precise_lookup_cny05 --input company_name_or_credit_no=北京京东世纪贸易有限公司 --host <client> --json
119
+ itp buy var_itpay_enterprise_fuzzy_search_cny01 --email <buyer_email> --input company_name=京东 --display agent --no-wait-payment --host <client> --json
97
120
  ```
98
121
 
99
122
  For cart edits, always read the server cart first with `buyer cart show`.
@@ -112,19 +135,19 @@ Refund commands use ItPay shared order state. If `itp buyer refund create`
112
135
  returns `policy_risk_confirmation_required`, explain the returned
113
136
  `refund_eligibility.policy` and `agent_guidance` to the human first. Only retry
114
137
  with `--confirm-policy-risk true` after explicit human confirmation.
115
- Do not guess `order_id`; if missing, run `buyer checkout status <checkout_id> --json`.
138
+ Do not guess `order_id`; if missing, run `buyer checkout status <checkout_id> --host <client> --json`.
116
139
  Refund amounts use minor units: CNY 1000 means CNY 10.00.
117
140
  Refund commands require a server-verified buyer session, not a vault grant. If
118
141
  the CLI says the buyer session is required or expired, run
119
- `itp status --refresh --json` and follow the returned `next.command`.
142
+ `itp status --refresh --host <client> --json` and follow the returned `next.command`.
120
143
  Current buyer refunds are whole-order only; do not use line-item refund scope.
121
144
  If the human cancels a refund before provider or money movement starts, use
122
- `buyer refund cancel <refund_id> --json`; after cancel, the delivery claim can
145
+ `buyer refund cancel <refund_id> --host <client> --json`; after cancel, the delivery claim can
123
146
  be unlocked again by the ItPay backend.
124
147
 
125
148
  ## Non-Negotiable Rules
126
149
 
127
- 1. Use `--json` for every ItPay command.
150
+ 1. Use `--json` and current client context for every non-doc ItPay command. Use `--host codex`, `--host claude-code`, `--host terminal`, `--host plain-chat`, or for OpenClaw Telegram use `--host telegram --target <inbound_meta.chat_id>`.
128
151
  2. Do not invent service IDs, variant IDs, checkout IDs, payment URLs, QR URLs,
129
152
  payment intent IDs, delivery IDs, or claim links.
130
153
  3. When the user asks for several compatible services, use one cart and one
@@ -206,18 +229,17 @@ Each docs page includes `next_docs`. Follow those links as the state changes.
206
229
  For payment creation in an agent/chat client, prefer:
207
230
 
208
231
  ```bash
209
- itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
232
+ itp buy <variant_id> --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --host <client> --json
210
233
  ```
211
234
 
212
235
  This keeps JSON output machine-readable while allowing the CLI to prepare a
213
236
  local QR image path for clients that cannot render remote SVG reliably. In
214
237
  agent/chat clients, prefer `--no-wait-payment`. In OpenClaw Telegram, if
215
- `agent_instruction` is present, call `openclaw message send` with
216
- `agent_instruction.openclaw_message.command_args`. The `media` field is the QR
217
- image, `message` is the exact human text, and
218
- `presentation.blocks[].type="buttons"` creates Telegram native inline buttons.
219
- Do not rewrite it as a table or normal prose. In Codex or Claude Code app
220
- clients, if `human_output_required=true`, send `human_output.markdown` exactly
238
+ `agent_action` is present, execute `agent_action.command` or `agent_action.argv`
239
+ exactly. It already contains `openclaw message send`, the chat target, QR
240
+ media, exact human text, and `presentation.blocks[].type="buttons"` for
241
+ Telegram native inline buttons. Do not rewrite it as a table or normal prose.
242
+ In Codex or Claude Code app clients, send `agent_instruction.markdown` exactly
221
243
  before any next ItPay command.
222
244
 
223
245
  If a response has `status=payment_handoff_required`, `next` is the user-visible