@itpay/cli 2.0.14 → 2.0.16

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 (56) hide show
  1. package/README.md +5 -4
  2. package/dist/src/commands/buy.js +3 -0
  3. package/dist/src/commands/checkout.js +4 -2
  4. package/dist/src/commands/checkout_handoff.js +7 -0
  5. package/dist/src/commands/compatibility.js +0 -1
  6. package/dist/src/commands/guidance.js +6 -5
  7. package/dist/src/commands/install.js +15 -10
  8. package/dist/src/commands/readyz.js +6 -2
  9. package/dist/src/commands/services.js +14 -6
  10. package/dist/src/main.js +60 -27
  11. package/dist/src/render/telegram.js +51 -25
  12. package/dist/src/state/client_context.js +2 -0
  13. package/dist/src/state/config.js +48 -7
  14. package/docs/agent/buyer/identity-and-sessions.json +10 -10
  15. package/docs/agent/buyer/install-and-setup.json +13 -12
  16. package/docs/agent/buyer/payment-flow.json +1 -0
  17. package/docs/agent/buyer/quickstart.json +4 -2
  18. package/docs/agent/buyer/render-hosts.json +18 -2
  19. package/docs/cli-reference/agent-types.md +12 -2
  20. package/docs/cli-reference/commands/buy.md +9 -4
  21. package/docs/cli-reference/commands/cart/add.md +1 -1
  22. package/docs/cli-reference/commands/cart/clear.md +1 -1
  23. package/docs/cli-reference/commands/cart/index.md +1 -1
  24. package/docs/cli-reference/commands/cart/next.md +1 -1
  25. package/docs/cli-reference/commands/cart/remove.md +2 -2
  26. package/docs/cli-reference/commands/cart/show.md +1 -1
  27. package/docs/cli-reference/commands/catalog/index.md +1 -1
  28. package/docs/cli-reference/commands/checkout.md +5 -3
  29. package/docs/cli-reference/commands/device.md +1 -1
  30. package/docs/cli-reference/commands/docs/index.md +1 -1
  31. package/docs/cli-reference/commands/docs/list.md +1 -1
  32. package/docs/cli-reference/commands/docs/search.md +1 -1
  33. package/docs/cli-reference/commands/docs/show.md +1 -1
  34. package/docs/cli-reference/commands/install.md +46 -12
  35. package/docs/cli-reference/commands/next.md +1 -1
  36. package/docs/cli-reference/commands/readyz.md +61 -10
  37. package/docs/cli-reference/commands/refund/cancel.md +1 -1
  38. package/docs/cli-reference/commands/refund/create.md +1 -1
  39. package/docs/cli-reference/commands/refund/index.md +1 -1
  40. package/docs/cli-reference/commands/refund/list.md +1 -1
  41. package/docs/cli-reference/commands/services/action.md +1 -1
  42. package/docs/cli-reference/commands/services/checkout.md +5 -1
  43. package/docs/cli-reference/commands/services/events.md +1 -1
  44. package/docs/cli-reference/commands/services/get.md +1 -1
  45. package/docs/cli-reference/commands/services/index.md +1 -1
  46. package/docs/cli-reference/commands/services/invoke.md +1 -1
  47. package/docs/cli-reference/commands/services/list.md +1 -1
  48. package/docs/cli-reference/commands/services/read-result.md +1 -1
  49. package/docs/cli-reference/conventions.md +7 -1
  50. package/docs/skill-bundle-rollout/01-mcp-authentication.md +256 -0
  51. package/docs/skill-bundle-rollout/02-platform-bundle-repositories.md +279 -0
  52. package/docs/skill-bundle-rollout/03-platform-publishing.md +271 -0
  53. package/docs/skill-bundle-rollout/04-first-wave-platforms.md +32 -0
  54. package/docs/skill-bundle-rollout/README.md +99 -0
  55. package/package.json +1 -1
  56. package/skills/itpay/SKILL.md +4 -4
package/README.md CHANGED
@@ -15,7 +15,7 @@ itpay --agent-type codex-desktop readyz --json
15
15
  # follow next.command: typed skill show, then catalog list
16
16
  ```
17
17
 
18
- The production Backend is permanently pinned to `https://app.itpay.ai`. Runtime environment variables cannot redirect the CLI to another Backend.
18
+ The CLI defaults to the production Backend `https://app.itpay.ai`. Explicit tests may set `ITPAY_BACKEND_URL=https://dev.itpay.ai`; every other Backend URL is rejected before network or local state access.
19
19
 
20
20
  ## Output Contract
21
21
 
@@ -46,11 +46,11 @@ Normative per-command contracts: [CLI Command Reference](docs/cli-reference/inde
46
46
 
47
47
  `--agent-type` identifies the stable runtime and registered Agent instance. Every returned ItPay command preserves it. Different windows or chats of the same type reuse one Agent Instance; they are not separate identities. `--host` only selects the human presentation surface, and `--target` only routes output to a Host destination. Use `itpay install <agent_type> --json` for the exact responsibility.
48
48
 
49
- The local installation keeps one Ed25519 private key and one production Device registration for `https://app.itpay.ai`, with one Agent Instance per Agent Type. A rejected session is renewed and the same request is retried once; revoked v2 registrations are never silently replaced.
49
+ The local installation keeps one Ed25519 private key and a separate registration for each official Backend, with one Agent Instance per Agent Type under each registration. A rejected session is renewed and the same request is retried once; revoked v2 registrations are never silently replaced.
50
50
 
51
51
  ## Command Families
52
52
 
53
- - `readyz`: fixed production Backend liveness; `catalog list`: compatibility-gated discovery.
53
+ - `readyz`: selected official Backend liveness; `catalog list`: compatibility-gated discovery.
54
54
  - `services start/invoke/action/checkout/next`: generic Service Execution flow.
55
55
  - `cart add/show/remove/clear/next`, `buy`: canonical Cart and ordinary Checkout flow.
56
56
  - `checkout`: authoritative payment and fulfillment recovery.
@@ -75,11 +75,12 @@ itpay --agent-type <agent_type> services checkout <service_execution_id> --resum
75
75
  itpay checkout --id <checkout_id> --token <display_token> --json
76
76
  ```
77
77
 
78
- The local `~/.itpay-v3` directory stores one owner-only signing key, the `https://app.itpay.ai` Device registration and Agent instances, idempotency operations, and recovery handles. Backend state remains authoritative. Do not delete or rotate this identity to recover quota.
78
+ The local `~/.itpay-v3` directory stores one owner-only signing key, Backend-scoped Device registrations and Agent instances, idempotency operations, and recovery handles. Production uses `cart.json` / `operations.json`; dev uses `cart.dev.json` / `operations.dev.json`. Backend state remains authoritative. Do not delete or rotate this identity to recover quota.
79
79
 
80
80
  ## Environment
81
81
 
82
82
  - `ITPAY_AGENT_TYPE`: stable alternative to global `--agent-type`.
83
+ - `ITPAY_BACKEND_URL`: optional test override; only the exact official URL `https://dev.itpay.ai` is accepted. Unset it for production.
83
84
  - `ITPAY_BEARER_TOKEN`: account-scoped Buyer session for account-only commands such as `orders`.
84
85
  - `ITPAY_CART_SESSION_PATH`: local recovery-state path override.
85
86
  - `ITPAY_CURRENCY`: ordinary Cart currency, default `CNY`.
@@ -141,6 +141,7 @@ export async function runBuy(backend, config, options) {
141
141
  ...(qrPNGURL ? { qrPNGURL } : {}),
142
142
  ...(paymentIntent ? { paymentIntent } : {}),
143
143
  ...(options.agentType ? { agentType: options.agentType } : {}),
144
+ ...(options.target ? { target: options.target } : {}),
144
145
  });
145
146
  writeCommandEnvelope(envelope, { jsonOutput: true, ...(options.output ? { output: options.output } : {}) });
146
147
  return {
@@ -198,7 +199,9 @@ function buildBuyEnvelope(input) {
198
199
  platform,
199
200
  url: input.checkoutURL,
200
201
  amount,
202
+ plan: input.plan,
201
203
  ...(input.agentType ? { agentType: input.agentType } : {}),
204
+ ...(input.target ? { target: input.target } : {}),
202
205
  ...(input.qrPNGURL ? { qrImageURL: input.qrPNGURL } : {}),
203
206
  ...(input.plan.ideImageAttach?.status === "downloaded" && input.plan.ideImageAttach.localPath
204
207
  ? { localPath: input.plan.ideImageAttach.localPath }
@@ -46,7 +46,7 @@ export async function runCheckoutPresentation(backend, options) {
46
46
  ...(options.baseURL ? { baseURL: options.baseURL } : {}),
47
47
  });
48
48
  }
49
- const envelope = pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, options.agentType);
49
+ const envelope = pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, options.agentType, options.target);
50
50
  const plainResult = checkoutPlainResult(envelope.result);
51
51
  if (!options.jsonOutput && platformKeyForHost(host) === "terminal") {
52
52
  plainResult.push("qr:", await renderTerminalQR(checkoutURL, "terminal"));
@@ -57,14 +57,16 @@ export async function runCheckoutPresentation(backend, options) {
57
57
  plainResult,
58
58
  });
59
59
  }
60
- function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, agentType) {
60
+ function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand, agentType, target) {
61
61
  const platform = platformKeyForHost(plan.host);
62
62
  const amount = formatMoney(presentation.checkout.amount_minor, presentation.checkout.currency);
63
63
  const presentationHandoff = buildCheckoutHandoff({
64
64
  platform,
65
65
  url: checkoutURL,
66
66
  amount,
67
+ plan,
67
68
  ...(agentType ? { agentType } : {}),
69
+ ...(target ? { target } : {}),
68
70
  ...(plan.preferredQRSources[0] ? { qrImageURL: plan.preferredQRSources[0] } : {}),
69
71
  ...(plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath
70
72
  ? { localPath: plan.ideImageAttach.localPath }
@@ -1,3 +1,4 @@
1
+ import { buildOpenClawTelegramAction } from "../render/telegram.js";
1
2
  export function shouldPrepareLocalCheckoutImage(platform) {
2
3
  return platform === "markdown";
3
4
  }
@@ -15,6 +16,12 @@ export function buildCheckoutHandoff(input) {
15
16
  else if (input.platform === "plain_chat" && input.qrImageURL) {
16
17
  handoff.qr_image_url = input.qrImageURL;
17
18
  }
19
+ else if (input.platform === "telegram" && input.qrImageURL) {
20
+ handoff.qr_image_url = input.qrImageURL;
21
+ }
22
+ if (input.agentType?.trim().toLowerCase() === "openclaw" && input.platform === "telegram" && input.plan && input.target) {
23
+ handoff.agent_action = buildOpenClawTelegramAction(input.plan, input.target);
24
+ }
18
25
  return {
19
26
  handoff,
20
27
  instruction: checkoutHandoffInstruction(input.agentType, input.platform, input.amount, Boolean(input.qrImageURL)),
@@ -14,7 +14,6 @@ export async function requirePlatformCompatibility(backend) {
14
14
  maximum_cli_major: platform.maximum_cli_major,
15
15
  platform_revision: platform.platform_revision,
16
16
  api_contract_revision: platform.api_contract_revision,
17
- upgrade_command: `npm install -g @itpay/cli@${platform.minimum_cli_version}`,
18
17
  }, "CLI is incompatible with the active ItPay platform release");
19
18
  }
20
19
  function compareVersions(left, right) {
@@ -1,6 +1,7 @@
1
1
  import { HttpError } from "../client/http.js";
2
2
  import { resolveOutput } from "../render/sink.js";
3
3
  import { declaredAgentType, qualifyItPayCommand } from "../state/agent_type.js";
4
+ import { qualifyBackendCommand } from "../state/config.js";
4
5
  export function isTerminalServiceExecutionStatus(status) {
5
6
  return status === "failed" || status === "refunded" || status === "cancelled";
6
7
  }
@@ -84,7 +85,7 @@ export function printAgentGuidance(guidance, output) {
84
85
  out("next actions:\n");
85
86
  for (const action of guidance.next_actions) {
86
87
  out(` - ${action.label}\n`);
87
- out(` ${qualifyItPayCommand(action.command, agentType)}\n`);
88
+ out(` ${qualifyBackendCommand(qualifyItPayCommand(action.command, agentType))}\n`);
88
89
  if (action.requires_human)
89
90
  out(" requires human confirmation\n");
90
91
  if (action.reason)
@@ -95,17 +96,17 @@ export function printAgentGuidance(guidance, output) {
95
96
  out("recovery:\n");
96
97
  for (const action of guidance.recovery) {
97
98
  out(` - ${action.label}\n`);
98
- out(` ${qualifyItPayCommand(action.command, agentType)}\n`);
99
+ out(` ${qualifyBackendCommand(qualifyItPayCommand(action.command, agentType))}\n`);
99
100
  }
100
101
  }
101
102
  }
102
103
  function qualifyEnvelope(value, agentType) {
103
104
  return {
104
105
  ...value,
105
- next: value.next ? { ...value.next, command: qualifyItPayCommand(value.next.command, agentType) } : null,
106
+ next: value.next ? { ...value.next, command: qualifyBackendCommand(qualifyItPayCommand(value.next.command, agentType)) } : null,
106
107
  recovery: value.recovery.map((action) => ({
107
108
  ...action,
108
- command: qualifyItPayCommand(action.command, agentType),
109
+ command: qualifyBackendCommand(qualifyItPayCommand(action.command, agentType)),
109
110
  })),
110
111
  };
111
112
  }
@@ -304,7 +305,7 @@ export function errorRecoveryActions(error) {
304
305
  return [
305
306
  {
306
307
  id: "retry_after_backend_recovers",
307
- label: "Retry after app.itpay.ai is reachable",
308
+ label: "Retry after the selected official Backend is reachable",
308
309
  command: "itpay readyz",
309
310
  },
310
311
  ];
@@ -7,6 +7,8 @@ export const INSTALL_AGENT_TYPES = [
7
7
  "claude-code-desktop",
8
8
  "claude-code-cli",
9
9
  "workbuddy",
10
+ "kimi-code",
11
+ "openclaw",
10
12
  ];
11
13
  const INSTRUCTIONS = {
12
14
  "codex-desktop": "在 Codex Desktop 中始终传这个 Agent Type;付款时把返回的二维码和链接实际展示到当前对话。",
@@ -14,13 +16,12 @@ const INSTRUCTIONS = {
14
16
  "claude-code-desktop": "在 Claude Code Desktop 中始终传这个 Agent Type;付款时把返回的二维码和链接实际展示到当前对话。",
15
17
  "claude-code-cli": "在 Claude Code CLI 中始终传这个 Agent Type;付款交接只应显示在用户可见终端。",
16
18
  workbuddy: "在 WorkBuddy 中始终传这个 Agent Type。运行会持久化 ~/.itpay-v3 的 ItPay Bash 命令前,把 Shell 工具参数 dangerouslyDisableSandbox 设为 true;用户拒绝权限时停止,不要切换 Node、删除 identity 或处理 lock。Checkout 返回 qr_image_url 时,按当次 instruction 调用 present_files 在右侧打开二维码,不要检查本地二维码文件。",
19
+ "kimi-code": "保持 kimi-code Agent Type,使用标准 CLI 命令和终端付款交接;不要切换到其他平台身份。",
20
+ openclaw: "保持 openclaw Agent Type;每个展示命令都从当前可信会话上下文显式传 --host,IM 入口同时传 --target。Telegram 使用返回的原生 message action,其他入口展示标准二维码和付款链接。",
17
21
  };
18
22
  export function runInstall(target, options = {}) {
19
23
  if (!target || target === "list") {
20
- const agentTypes = INSTALL_AGENT_TYPES.map((agentType) => ({
21
- agent_type: agentType,
22
- default_host: defaultHostForAgentType(agentType),
23
- }));
24
+ const agentTypes = INSTALL_AGENT_TYPES.map(installDefinition);
24
25
  writeCommandEnvelope({
25
26
  status: "install_targets",
26
27
  result: { agent_types: agentTypes },
@@ -29,7 +30,7 @@ export function runInstall(target, options = {}) {
29
30
  recovery: [{ command: "itpay docs show install-and-setup", reason: "查看安装与环境说明" }],
30
31
  }, {
31
32
  ...options,
32
- plainResult: agentTypes.map((item) => `${item.agent_type}: ${item.default_host}`),
33
+ plainResult: agentTypes.map((item) => `${item.agent_type}: ${item.default_host ?? "explicit --host required"}`),
33
34
  });
34
35
  return;
35
36
  }
@@ -40,19 +41,23 @@ export function runInstall(target, options = {}) {
40
41
  writeCommandEnvelope({
41
42
  status: "instructions_ready",
42
43
  result: {
43
- agent_type: normalized,
44
- default_host: defaultHostForAgentType(normalized),
44
+ ...installDefinition(normalized),
45
45
  default_api: DEFAULT_BASE_URL,
46
- install_command: "npm install -g @itpay/cli",
47
46
  },
48
47
  instruction: INSTRUCTIONS[normalized],
49
48
  next: {
50
49
  command: `itpay --agent-type ${normalized} readyz --json`,
51
- reason: "验证固定生产 ItPay API 的可用性",
50
+ reason: "验证当前官方 ItPay API 的可用性",
52
51
  },
53
- recovery: [{ command: "itpay docs show install-and-setup", reason: "查看固定生产后端和首次使用说明" }],
52
+ recovery: [{ command: "itpay docs show install-and-setup", reason: "查看官方 Backend 和首次使用说明" }],
54
53
  }, options);
55
54
  }
56
55
  function isInstallAgentType(value) {
57
56
  return INSTALL_AGENT_TYPES.includes(value);
58
57
  }
58
+ function installDefinition(agentType) {
59
+ if (agentType === "openclaw") {
60
+ return { agent_type: agentType, default_host: null, host_required: true, native_hosts: ["telegram"] };
61
+ }
62
+ return { agent_type: agentType, default_host: defaultHostForAgentType(agentType) ?? null };
63
+ }
@@ -2,10 +2,14 @@
2
2
  import { writeCommandEnvelope } from "./guidance.js";
3
3
  export async function runReadyz(backend, options = {}) {
4
4
  const response = await backend.readyz();
5
+ const backendURL = options.backendURL ?? "https://app.itpay.ai";
6
+ const environment = options.environment ?? "production";
5
7
  writeCommandEnvelope({
6
8
  status: response.status,
7
- result: { backend: "available", ...(options.agentType ? { agent_type: options.agentType } : {}) },
8
- instruction: "ItPay 可用;先完整读取内置 ItPay Skill,再进入当前已支持的 buy 流程。sell 将来也使用同一入口,但当前尚未实现。",
9
+ result: { backend: "available", backend_url: backendURL, environment, ...(options.agentType ? { agent_type: options.agentType } : {}) },
10
+ instruction: environment === "development"
11
+ ? "ItPay dev 可用;后续必须执行返回的完整命令,并继续使用同一个 dev Backend。先完整读取内置 ItPay Skill,再进入当前已支持的 buy 流程。"
12
+ : "ItPay 可用;先完整读取内置 ItPay Skill,再进入当前已支持的 buy 流程。sell 将来也使用同一入口,但当前尚未实现。",
9
13
  next: { command: "itpay skill show itpay --json", reason: "加载完整操作与安全规则" },
10
14
  recovery: [],
11
15
  }, options);
@@ -1,4 +1,5 @@
1
1
  import { operationID } from "../state/config.js";
2
+ import { validateContext } from "../state/client_context.js";
2
3
  import { dispatchRender } from "../render/index.js";
3
4
  import { ensureIdeImageAttach } from "../render/ide.js";
4
5
  import { buildCheckoutHandoff, shouldPrepareLocalCheckoutImage } from "./checkout_handoff.js";
@@ -373,6 +374,11 @@ function actionInputError(serviceExecutionID, message, code = "service_action_in
373
374
  : "使用当前 safe result 中的合法 action 和 candidate rank;需要人确认时先询问用户。", [{ command: `itpay services next ${serviceExecutionID} --json`, reason: "重新读取同一 Execution 的当前可选动作" }]);
374
375
  }
375
376
  export async function runServicesCheckout(backend, config, serviceExecutionID, capabilityID, options = {}) {
377
+ const host = options.host ?? "terminal";
378
+ const contextError = validateContext(host, options.target);
379
+ if (contextError) {
380
+ throw new CommandContractError(contextError.code, contextError.message, "从当前可信会话上下文补齐 Host/target;本次未创建 Checkout。", []);
381
+ }
376
382
  const deliveryContact = {
377
383
  ...(options.deliveryContact ?? {}),
378
384
  ...(options.email ? { email: options.email } : {}),
@@ -409,12 +415,12 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
409
415
  const displayToken = checkout.display_token;
410
416
  const checkoutURL = tokenizedCheckoutURL(checkout.checkout_url, displayToken, checkout.qr_payload);
411
417
  const plan = buildCheckoutQRPlan({
412
- host: options.host ?? "terminal",
418
+ host,
413
419
  checkoutID,
414
420
  checkoutURL,
415
421
  displayToken,
416
422
  qrPayload: checkout.qr_payload,
417
- ...(checkout.qr_png_url ? { qrPNGURL: checkout.qr_png_url } : {}),
423
+ ...(checkout.qr_png_url ? { qrPNGURL: absolutePublicURL(config.baseURL, checkout.qr_png_url) } : {}),
418
424
  nextAction: checkout.checkout.next_action,
419
425
  orderItems: response.cart.items.map((item) => ({
420
426
  title: item.title,
@@ -433,9 +439,9 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
433
439
  checkoutURL,
434
440
  });
435
441
  const platform = platformKeyForHost(plan.host);
436
- if (platform === "telegram" || platform === "feishu" || platform === "lark") {
442
+ if (!options.jsonOutput && (platform === "telegram" || platform === "feishu" || platform === "lark")) {
437
443
  await dispatchRender(plan, {
438
- host: options.host ?? "terminal",
444
+ host,
439
445
  ...(options.target ? { target: options.target } : {}),
440
446
  ...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),
441
447
  ...(options.qrFilePath ? { qrFilePath: options.qrFilePath } : {}),
@@ -452,7 +458,7 @@ export async function runServicesCheckout(backend, config, serviceExecutionID, c
452
458
  ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
453
459
  });
454
460
  }
455
- const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, config.baseURL, options.agentType);
461
+ const envelope = buildServicesCheckoutEnvelope(response, checkoutURL, plan, config.baseURL, options.agentType, options.target);
456
462
  const plainResult = [
457
463
  `service_execution_id: ${response.binding.service_execution_id}`,
458
464
  `checkout_id: ${checkoutID}`,
@@ -933,7 +939,7 @@ function parseValue(value) {
933
939
  return Number(value);
934
940
  return value;
935
941
  }
936
- function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL, agentType) {
942
+ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL, agentType, target) {
937
943
  const checkout = response.checkout;
938
944
  const platform = platformKeyForHost(plan.host);
939
945
  const amount = formatMoney(checkout.checkout.amount_minor, checkout.checkout.currency);
@@ -941,7 +947,9 @@ function buildServicesCheckoutEnvelope(response, checkoutURL, plan, baseURL, age
941
947
  platform,
942
948
  url: checkoutURL,
943
949
  amount,
950
+ plan,
944
951
  ...(agentType ? { agentType } : {}),
952
+ ...(target ? { target } : {}),
945
953
  ...(checkout.qr_png_url ? { qrImageURL: absolutePublicURL(baseURL, checkout.qr_png_url) } : {}),
946
954
  ...(plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath
947
955
  ? { localPath: plan.ideImageAttach.localPath }
package/dist/src/main.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // services/backend/internal/httpapi/handlers/*.go. Commands only
3
3
  // orchestrate; HTTP and rendering live in src/client and src/render.
4
4
  import { Command } from "commander";
5
- import { CLI_VERSION, loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
5
+ import { BackendOverrideError, CLI_VERSION, cliDistribution, loadConfig, cartSessionPath, newBackendClient } from "./state/config.js";
6
6
  import { DeviceAuthority, DeviceAuthorizationError, DeviceStateError } from "./state/device_authority.js";
7
7
  import { CartSession } from "./state/cart_session.js";
8
8
  import { defaultHostForAgentType, normalizeHost, validateContext } from "./state/client_context.js";
@@ -29,10 +29,17 @@ program
29
29
  .description("V3 ItPay CLI — one entry point for buy workflows and future sell workflows")
30
30
  .option("--agent-type <type>", "agent runtime type used for device enrollment and client-specific guidance")
31
31
  .version(CLI_VERSION);
32
- function withHost(value) {
33
- const host = normalizeHost(value);
32
+ function withHost(value, agentType, target) {
33
+ if (!value && agentType?.trim().toLowerCase() === "openclaw") {
34
+ throw new CommandContractError("host_required", "OpenClaw requires an explicit --host entry", "从当前可信 OpenClaw 会话上下文传入 --host;IM 入口同时传 --target。本次未创建或修改资源。", []);
35
+ }
36
+ const host = normalizeHost(value ?? defaultHostForAgentType(agentType));
34
37
  if (!host) {
35
- throw new Error(`invalid --host "${value ?? ""}". Supported: terminal, codex, claude-code, telegram, discord, whatsapp, feishu, lark, plain-chat`);
38
+ throw new CommandContractError("invalid_host", `invalid --host "${value ?? ""}". Supported: terminal, codex, claude-code, telegram, discord, whatsapp, feishu, lark, plain-chat`, "传入受支持的展示 Host;本次未创建或修改资源。", []);
39
+ }
40
+ const contextError = agentType?.trim().toLowerCase() === "openclaw" ? validateContext(host, target) : undefined;
41
+ if (contextError) {
42
+ throw new CommandContractError(contextError.code, contextError.message, "从当前可信会话上下文补齐 --target;本次未创建或修改资源。", []);
36
43
  }
37
44
  return host;
38
45
  }
@@ -83,6 +90,7 @@ function resolveCheckoutPresentationArgs(input) {
83
90
  }
84
91
  function reportCLIError(error, contract) {
85
92
  const commandError = error instanceof CommandContractError ? error : undefined;
93
+ const backendOverrideError = error instanceof BackendOverrideError ? error : undefined;
86
94
  const deviceError = error instanceof DeviceAuthorizationError ? error : undefined;
87
95
  const stateError = error instanceof DeviceStateError ? error : undefined;
88
96
  const httpRecovery = errorRecoveryActions(error).map((action) => ({
@@ -122,11 +130,11 @@ function reportCLIError(error, contract) {
122
130
  : deviceError
123
131
  ? "Device 身份验证失败;停止重试,不要切换 Agent Type、删除状态或旋转私钥。"
124
132
  : undefined;
125
- if (contract || commandError) {
133
+ if (contract || commandError || backendOverrideError) {
126
134
  writeCommandEnvelope({
127
135
  status: "error",
128
136
  error: {
129
- code: incompatible ? "backend_contract_incompatible" : commandError?.code ?? (error instanceof HttpError ? error.code : stateError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
137
+ code: incompatible ? "backend_contract_incompatible" : backendOverrideError?.code ?? commandError?.code ?? (error instanceof HttpError ? error.code : stateError?.code ?? deviceError?.code ?? contract?.code ?? "command_failed"),
130
138
  message: error instanceof Error ? error.message : String(error),
131
139
  },
132
140
  ...(requiredCLIVersion ? {
@@ -148,7 +156,7 @@ function reportCLIError(error, contract) {
148
156
  } : {}),
149
157
  instruction: incompatible
150
158
  ? requiredCLIVersion
151
- ? "当前 CLI 与 Backend 合约不兼容。停止所有 ItPay 业务命令;只执行 recovery.command,将 @itpay/cli 更新到 Backend 指定的精确版本。安装完成后确认 itpay --version 与 result.required_cli_version 完全一致,再重新运行 readyz。不要安装 latest、猜测版本、切换 Agent Type 或删除 Device 身份。"
159
+ ? distributionUpgradeInstruction()
152
160
  : "立即向用户报告 error.message 并结束本次任务。Backend 未提供可验证的兼容 CLI 版本;不要运行其他 ItPay 或 npm 命令,不要猜测版本、切换 Agent Type 或删除 Device 身份。"
153
161
  : backendInternal
154
162
  ? "Backend 内部故障;立即停止并向用户报告。不要重试、检查或删除 Device 身份、创建替代 Execution、切换 Backend,或尝试 quote、checkout、cart、buy、pay 等付费路径。"
@@ -164,17 +172,19 @@ function reportCLIError(error, contract) {
164
172
  ? "Provider 拒绝了本次请求,但未声明这是输入错误;向用户逐字报告 error.message 和 result.quota 并停止。不要修改输入、不要重试、不要创建新 Execution。"
165
173
  : capabilityInputInvalid
166
174
  ? "输入未通过本地校验,上游尚未被调用且用户额度未变化。向用户逐字报告 error.message 并停止,不要原样重试或运行其他恢复命令。用户提供修正后的输入后,继续使用当前未结束的 Execution。"
167
- : commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
175
+ : backendOverrideError
176
+ ? "移除 ITPAY_BACKEND_URL 使用正式环境,或准确设置为 https://dev.itpay.ai。"
177
+ : commandError?.instruction ?? authorizationInstruction ?? contract?.instruction ?? "检查命令参数后重试。",
168
178
  next: null,
169
179
  recovery: incompatible
170
180
  ? requiredCLIVersion
171
- ? [{ command: `npm install -g @itpay/cli@${requiredCLIVersion}`, reason: "安装 Backend 指定的兼容 CLI 版本" }]
181
+ ? [distributionUpgradeAction(requiredCLIVersion)]
172
182
  : []
173
183
  : backendInternal || providerConnectionUnavailable || providerTemporary || providerInputRejected || providerContractMismatch || providerRejected || capabilityInputInvalid
174
184
  ? []
175
- : commandError?.recovery ?? (stateError ? stateRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
185
+ : backendOverrideError ? [] : commandError?.recovery ?? (stateError ? stateRecovery : deviceError ? deviceRecovery : identityRecovery ? httpRecovery : contract?.recovery ?? []),
176
186
  }, {
177
- ...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : {}),
187
+ ...(contract?.jsonOutput !== undefined ? { jsonOutput: contract.jsonOutput } : backendOverrideError ? { jsonOutput: process.argv.includes("--json") } : {}),
178
188
  output: (text) => { process.stderr.write(text); },
179
189
  });
180
190
  process.exitCode = 1;
@@ -197,10 +207,29 @@ function docsErrorFallback(jsonOutput) {
197
207
  return {
198
208
  jsonOutput,
199
209
  code: "docs_unavailable",
200
- instruction: "内置文档缺失或损坏;重新安装同版本 CLI 后重试。",
201
- recovery: [{ command: `npm install -g @itpay/cli@${CLI_VERSION}`, reason: "恢复随包发布的文档" }],
210
+ instruction: "内置文档缺失或损坏;通过当前分发方式恢复同版本 CLI 后重试。",
211
+ recovery: [distributionUpgradeAction(CLI_VERSION)],
202
212
  };
203
213
  }
214
+ function distributionUpgradeAction(version) {
215
+ switch (cliDistribution()) {
216
+ case "openclaw-skill-bundle":
217
+ return { command: "openclaw skills update itpay", reason: `更新包含 ItPay CLI ${version} 的 Skill bundle` };
218
+ case "kimi-plugin-bundle":
219
+ return {
220
+ command: "/plugins install https://github.com/itpay-ai/itpay-plugin-kimi-work",
221
+ reason: `更新包含 ItPay CLI ${version} 的 Kimi plugin`,
222
+ };
223
+ default:
224
+ return { command: `npm install -g @itpay/cli@${version}`, reason: "安装 Backend 指定的兼容 CLI 版本" };
225
+ }
226
+ }
227
+ function distributionUpgradeInstruction() {
228
+ if (cliDistribution() === "npm") {
229
+ return "当前 CLI 与 Backend 合约不兼容。停止所有 ItPay 业务命令;只执行 recovery.command,将 @itpay/cli 更新到 Backend 指定的精确版本。安装完成后确认 itpay --version 与 result.required_cli_version 完全一致,再重新运行 readyz。不要安装 latest、猜测版本、切换 Agent Type 或删除 Device 身份。";
230
+ }
231
+ return "当前平台 bundle 与 Backend 合约不兼容。停止所有 ItPay 业务命令;只执行 recovery.command,更新平台 Skill/plugin 后启动新会话,确认 itpay --version 与 result.required_cli_version 完全一致,再重新运行 readyz。不要运行 npm、猜测版本、切换 Agent Type 或删除 Device 身份。";
232
+ }
204
233
  program
205
234
  .command("readyz")
206
235
  .description("Probe the V3 backend readiness endpoint")
@@ -209,31 +238,34 @@ program
209
238
  const config = loadConfig();
210
239
  const backend = newBackendClient(config);
211
240
  try {
212
- await runReadyz(backend, { jsonOutput: Boolean(options.json), ...(config.agentType ? { agentType: config.agentType } : {}) });
241
+ await runReadyz(backend, {
242
+ jsonOutput: Boolean(options.json), backendURL: config.baseURL, environment: config.environment,
243
+ ...(config.agentType ? { agentType: config.agentType } : {}),
244
+ });
213
245
  }
214
246
  catch (error) {
215
247
  reportCLIError(error, {
216
248
  jsonOutput: Boolean(options.json),
217
249
  code: "backend_unavailable",
218
- instruction: "固定生产后端 https://app.itpay.ai 当前不可用;后端恢复前不要继续下单,也不要切换到其他地址。",
250
+ instruction: `当前官方 Backend ${config.baseURL} 不可用;恢复前不要继续下单,也不要切换环境。`,
219
251
  recovery: [
220
- { command: "itpay readyz", reason: "重试固定生产后端的可用性检查" },
252
+ { command: "itpay readyz", reason: "重试当前官方 Backend 的可用性检查" },
221
253
  ],
222
254
  });
223
255
  }
224
256
  });
225
257
  // --- device ---------------------------------------------------------------
226
- const deviceCmd = program.command("device").description("Recover local Device registration state after an operator-confirmed Backend reset");
258
+ const deviceCmd = program.command("device").description("Recover the current official Backend registration after an operator-confirmed reset");
227
259
  deviceCmd
228
260
  .command("recover")
229
- .description("Forget only the app.itpay.ai registration while preserving the local private key")
261
+ .description("Forget only the current official Backend registration while preserving the local private key")
230
262
  .option("--confirm-backend-reset", "confirm that an operator reset the selected Backend registration database")
231
263
  .option("--json", "output JSON instead of terminal text")
232
264
  .action(async (options) => {
233
265
  const config = loadConfig();
234
266
  try {
235
267
  if (!config.agentType) {
236
- throw new CommandContractError("agent_type_required", "agent type is required for app.itpay.ai Device recovery", "如实声明当前 Agent Type;恢复后必须用同一类型重新登记。", [{ command: "itpay install --json", reason: "选择当前真实 Agent Type" }]);
268
+ throw new CommandContractError("agent_type_required", `agent type is required for ${config.baseURL} Device recovery`, "如实声明当前 Agent Type;恢复后必须用同一类型重新登记。", [{ command: "itpay install --json", reason: "选择当前真实 Agent Type" }]);
237
269
  }
238
270
  if (!options.confirmBackendReset) {
239
271
  throw new CommandContractError("backend_reset_confirmation_required", "--confirm-backend-reset is required", "仅在运营已确认当前 Backend 的 Device 登记数据库被重建或清空后执行;普通 session 失效或 revoked 不得使用。", [{ command: "itpay docs show identity-and-sessions --json", reason: "检查适用边界" }]);
@@ -292,8 +324,8 @@ skillCmd
292
324
  reportCLIError(error, {
293
325
  jsonOutput: Boolean(options.json),
294
326
  code: "skill_unavailable",
295
- instruction: "内置 Skill 缺失或损坏;重新安装同版本 CLI 后重试。",
296
- recovery: [{ command: `npm install -g @itpay/cli@${CLI_VERSION}`, reason: "恢复随包发布的 Skill" }],
327
+ instruction: "内置 Skill 缺失或损坏;通过当前分发方式恢复同版本 CLI 后重试。",
328
+ recovery: [distributionUpgradeAction(CLI_VERSION)],
297
329
  });
298
330
  }
299
331
  });
@@ -450,7 +482,7 @@ cart
450
482
  runCartAdd(session, { ...addOptions, jsonOutput });
451
483
  }
452
484
  else {
453
- const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
485
+ const host = withHost(options.host, config.agentType, options.target);
454
486
  const contextError = validateContext(host, options.target);
455
487
  if (contextError) {
456
488
  throw new CommandContractError(contextError.code, contextError.message, "补齐当前客户端所需的 Host/target;本次未创建或修改 Cart。", [{ command: "itpay cart add --help", reason: "查看客户端参数" }]);
@@ -686,7 +718,7 @@ program
686
718
  }
687
719
  const quantity = positiveInteger(options.quantity, "--quantity");
688
720
  const timeout = positiveInteger(options.timeout, "--timeout");
689
- const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
721
+ const host = withHost(options.host, config.agentType, options.target);
690
722
  const contact = {};
691
723
  if (options.contactEmail)
692
724
  contact.email = options.contactEmail;
@@ -750,11 +782,11 @@ program
750
782
  .option("--json", "output compact JSON")
751
783
  .action(async (options) => {
752
784
  const config = loadConfig();
753
- const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
754
785
  const session = CartSession.loadFromFile(cartSessionPath(), config.checkoutCurrency);
755
786
  const snap = session.show();
756
787
  const backend = newBackendClient(config);
757
788
  try {
789
+ const host = withHost(options.host, config.agentType, options.target);
758
790
  const { checkoutID, displayToken } = resolveCheckoutPresentationArgs({
759
791
  ...(options.id ? { requestedCheckoutID: options.id } : {}),
760
792
  ...(options.token ? { requestedDisplayToken: options.token } : {}),
@@ -765,6 +797,7 @@ program
765
797
  checkoutID,
766
798
  displayToken,
767
799
  host,
800
+ ...(options.target ? { target: options.target } : {}),
768
801
  ...(config.agentType ? { agentType: config.agentType } : {}),
769
802
  baseURL: config.baseURL,
770
803
  jsonOutput: Boolean(options.json),
@@ -811,7 +844,7 @@ program
811
844
  if (!displayToken) {
812
845
  throw new CommandContractError("checkout_token_required", "display token is required for this checkout", "提供同一 Checkout 的 display token;不要拼接其他 Checkout 的 token。", [{ command: "itpay next --json", reason: "恢复本机保存的同一 Checkout" }]);
813
846
  }
814
- const host = withHost(options.host ?? defaultHostForAgentType(config.agentType));
847
+ const host = withHost(options.host, config.agentType, options.target);
815
848
  const contextError = validateContext(host, options.target);
816
849
  if (contextError) {
817
850
  throw new CommandContractError(contextError.code, contextError.message, "为当前 Host 提供有效 target;本次未创建 Payment Intent。", [
@@ -1018,7 +1051,7 @@ services
1018
1051
  const backend = newBackendClient(config);
1019
1052
  try {
1020
1053
  await runServicesStart(backend, serviceID, {
1021
- host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
1054
+ host: withHost(options.host, config.agentType, options.target),
1022
1055
  ...(options.target ? { target: options.target } : {}),
1023
1056
  jsonOutput: Boolean(options.json),
1024
1057
  });
@@ -1144,7 +1177,7 @@ services
1144
1177
  ...(options.email ? { email: options.email } : {}),
1145
1178
  lockedInput: parseKeyValueList(options.input),
1146
1179
  resume: Boolean(options.resume),
1147
- host: withHost(options.host ?? defaultHostForAgentType(config.agentType)),
1180
+ host: withHost(options.host, config.agentType, options.target),
1148
1181
  ...(config.agentType ? { agentType: config.agentType } : {}),
1149
1182
  ...(options.target ? { target: options.target } : {}),
1150
1183
  ...(options.qrFormat ? { qrFormat: options.qrFormat } : {}),