@itpay/cli 2.0.5 → 2.0.7

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 (34) hide show
  1. package/README.md +8 -4
  2. package/dist/src/client/http.js +29 -23
  3. package/dist/src/commands/catalog.js +3 -2
  4. package/dist/src/commands/checkout.js +23 -13
  5. package/dist/src/commands/guidance.js +37 -13
  6. package/dist/src/commands/readyz.js +3 -3
  7. package/dist/src/commands/services.js +122 -41
  8. package/dist/src/commands/skill.js +55 -0
  9. package/dist/src/main.js +116 -7
  10. package/dist/src/state/agent_type.js +19 -0
  11. package/dist/src/state/config.js +5 -13
  12. package/dist/src/state/device_authority.js +174 -56
  13. package/docs/agent/buyer/catalog-list.json +2 -1
  14. package/docs/agent/buyer/identity-and-sessions.json +64 -0
  15. package/docs/agent/buyer/install-and-setup.json +19 -5
  16. package/docs/agent/buyer/payment-flow.json +5 -1
  17. package/docs/agent/buyer/quickstart.json +12 -4
  18. package/docs/cli-reference/agent-types.md +9 -3
  19. package/docs/cli-reference/commands/catalog/list.md +1 -1
  20. package/docs/cli-reference/commands/checkout.md +4 -4
  21. package/docs/cli-reference/commands/device.md +13 -0
  22. package/docs/cli-reference/commands/install.md +3 -1
  23. package/docs/cli-reference/commands/readyz.md +4 -5
  24. package/docs/cli-reference/commands/services/action.md +10 -5
  25. package/docs/cli-reference/commands/services/checkout.md +3 -3
  26. package/docs/cli-reference/commands/services/invoke.md +6 -6
  27. package/docs/cli-reference/commands/services/next.md +23 -4
  28. package/docs/cli-reference/commands/services/quote.md +5 -1
  29. package/docs/cli-reference/commands/services/start.md +5 -3
  30. package/docs/cli-reference/commands/skill.md +17 -0
  31. package/docs/cli-reference/conventions.md +4 -1
  32. package/docs/cli-reference/index.md +1 -0
  33. package/package.json +1 -1
  34. package/skills/itpay-buyer/SKILL.md +33 -6
package/README.md CHANGED
@@ -4,9 +4,11 @@ The official V3 CLI for Agent-driven ItPay service discovery, checkout, delivery
4
4
 
5
5
  ```bash
6
6
  npm install -g @itpay/cli
7
+ itpay readyz --json
8
+ itpay skill show itpay-buyer --json
7
9
  itpay install --json
8
10
  itpay --agent-type codex-desktop readyz --json
9
- itpay --agent-type codex-desktop catalog list --json
11
+ # follow next.command: typed skill show, then catalog list
10
12
  ```
11
13
 
12
14
  The default API is `https://app.itpay.ai`. Set `ITPAY_BACKEND_URL` only for an intentional test or local environment.
@@ -38,7 +40,9 @@ Normative per-command contracts: [CLI Command Reference](docs/cli-reference/inde
38
40
  | `claude-code-cli` | `terminal` |
39
41
  | `workbuddy` | `plain-chat` |
40
42
 
41
- `--agent-type` identifies the stable runtime and registered Agent instance. `--host` only selects the human presentation surface. Use `itpay install <agent_type> --json` for the exact responsibility.
43
+ `--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.
44
+
45
+ The local installation keeps one Ed25519 private key. Each normalized Backend API base URL (`dev`, `test`, `app`, or local) has an independent server device registration, quota lineage, Agent instances, and sessions. A rejected session is renewed and the same request is retried once; revoked v2 registrations are never silently replaced.
42
46
 
43
47
  ## Command Families
44
48
 
@@ -50,7 +54,7 @@ Normative per-command contracts: [CLI Command Reference](docs/cli-reference/inde
50
54
  - `order`, `orders`: exact order and account order views.
51
55
  - `refund create/list/get/watch/cancel`: Refund Owner flow.
52
56
  - `services get/events`: redacted support diagnostics; normal flows should use `services next`.
53
- - `install`, `docs list/show/search`: offline packaged guidance.
57
+ - `install`, `skill show`, `docs list/show/search`: offline packaged guidance.
54
58
  - `pay`: operator escape hatch only; normal buyers use the ItPay Checkout page.
55
59
 
56
60
  Run `itpay <command> --help` or browse [the command index](docs/cli-reference/index.md) for parameters.
@@ -67,7 +71,7 @@ itpay --agent-type <agent_type> services checkout <service_execution_id> --resum
67
71
  itpay checkout --id <checkout_id> --token <display_token> --json
68
72
  ```
69
73
 
70
- The local `~/.itpay-v3` directory stores the signed Device Authority, Agent instances, idempotency operations, and recovery handles. Backend state remains authoritative.
74
+ The local `~/.itpay-v3` directory stores one owner-only signing key, Backend-scoped Device registrations and Agent instances, idempotency operations, and recovery handles. Backend state remains authoritative. Do not delete or rotate this identity to recover quota.
71
75
 
72
76
  ## Environment
73
77
 
@@ -17,6 +17,7 @@ export class HttpClient {
17
17
  fetchImpl;
18
18
  defaultHeaders;
19
19
  requestAuthorizer;
20
+ recoverAuthorization;
20
21
  constructor(config) {
21
22
  this.baseURL = config.baseURL.replace(/\/$/, "");
22
23
  this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
@@ -26,35 +27,40 @@ export class HttpClient {
26
27
  ...(config.defaultHeaders ?? {}),
27
28
  };
28
29
  this.requestAuthorizer = config.requestAuthorizer;
30
+ this.recoverAuthorization = config.recoverAuthorization;
29
31
  }
30
32
  async request(path, options = {}) {
31
33
  const url = path.startsWith("http") ? path : this.baseURL + path;
32
- const headers = { ...this.defaultHeaders };
33
34
  const method = options.method ?? "GET";
34
35
  const body = options.body !== undefined ? JSON.stringify(options.body) : "";
35
- if (this.requestAuthorizer) {
36
- Object.assign(headers, await this.requestAuthorizer({ method, path: new URL(url).pathname + new URL(url).search, body }));
36
+ const requestPath = new URL(url).pathname + new URL(url).search;
37
+ for (let attempt = 0; attempt < 2; attempt += 1) {
38
+ const headers = { ...this.defaultHeaders };
39
+ if (this.requestAuthorizer) {
40
+ Object.assign(headers, await this.requestAuthorizer({ method, path: requestPath, body }));
41
+ }
42
+ if (options.bearer)
43
+ headers.Authorization = `Bearer ${options.bearer}`;
44
+ if (options.idempotencyKey)
45
+ headers["Idempotency-Key"] = options.idempotencyKey;
46
+ const response = await this.fetchImpl(url, {
47
+ method,
48
+ headers,
49
+ ...(options.body !== undefined ? { body } : {}),
50
+ ...(options.signal ? { signal: options.signal } : {}),
51
+ });
52
+ const text = await response.text();
53
+ const parsed = text.length > 0 ? safeParseJson(text) : undefined;
54
+ if (response.ok)
55
+ return parsed;
56
+ const error = new HttpError(response.status, parsed, `HTTP ${response.status}`);
57
+ if (attempt === 0 && error.status === 401 && error.code === "agent_device_session_required" && this.recoverAuthorization) {
58
+ await this.recoverAuthorization();
59
+ continue;
60
+ }
61
+ throw error;
37
62
  }
38
- if (options.bearer) {
39
- headers.Authorization = `Bearer ${options.bearer}`;
40
- }
41
- if (options.idempotencyKey) {
42
- headers["Idempotency-Key"] = options.idempotencyKey;
43
- }
44
- const requestInit = {
45
- method,
46
- headers,
47
- ...(options.body !== undefined ? { body } : {}),
48
- ...(options.signal ? { signal: options.signal } : {}),
49
- };
50
- const response = await this.fetchImpl(url, requestInit);
51
- const text = await response.text();
52
- const parsed = text.length > 0 ? safeParseJson(text) : undefined;
53
- if (!response.ok) {
54
- const errPayload = parsed;
55
- throw new HttpError(response.status, errPayload, `HTTP ${response.status}`);
56
- }
57
- return parsed;
63
+ throw new Error("unreachable HTTP retry state");
58
64
  }
59
65
  get(path, options = {}) {
60
66
  return this.request(path, { ...options, method: "GET" });
@@ -4,6 +4,7 @@ export async function runCatalogList(backend, options = {}) {
4
4
  const services = manifest.manifest.items.map(summarizeService);
5
5
  const firstServiceID = manifest.manifest.items.find((item) => item.service_id)?.service_id;
6
6
  const empty = services.length === 0;
7
+ const jsonFlag = options.jsonOutput ? " --json" : "";
7
8
  writeCommandEnvelope({
8
9
  status: empty ? "catalog_empty" : "listed",
9
10
  result: { catalog_version: manifest.version, services },
@@ -11,9 +12,9 @@ export async function runCatalogList(backend, options = {}) {
11
12
  ? "当前没有已发布服务;稍后重试,不要猜测 service_id。"
12
13
  : "向用户解释主服务、辅助步骤和价格;得到用户意图后再启动对应 service_id。",
13
14
  next: empty
14
- ? { command: "itpay catalog list", reason: "稍后重新读取已发布目录" }
15
+ ? { command: `itpay catalog list${jsonFlag}`, reason: "稍后重新读取已发布目录" }
15
16
  : {
16
- command: `itpay services start ${services.length === 1 && firstServiceID ? firstServiceID : "<service_id>"}`,
17
+ command: `itpay services start ${services.length === 1 && firstServiceID ? firstServiceID : "<service_id>"}${jsonFlag}`,
17
18
  reason: "启动用户选择的服务",
18
19
  },
19
20
  recovery: [],
@@ -20,8 +20,8 @@ export async function runCheckoutPresentation(backend, options) {
20
20
  return;
21
21
  }
22
22
  const checkoutURL = checkoutPageURL(options.baseURL, options.checkoutID, options.displayToken);
23
- const qrPNGURL = presentation.qr_png_url ?? checkoutQRPNGURL(options.baseURL, options.checkoutID, options.displayToken);
24
- const nextCommand = `itpay checkout --id ${options.checkoutID} --token ${options.displayToken}`;
23
+ const qrPNGURL = absolutePublicURL(options.baseURL, presentation.qr_png_url ?? checkoutQRPNGURL(options.baseURL, options.checkoutID, options.displayToken));
24
+ const nextCommand = `itpay checkout --id ${options.checkoutID} --token ${options.displayToken} --json`;
25
25
  const plan = buildCheckoutQRPlan({
26
26
  host,
27
27
  checkoutID: options.checkoutID,
@@ -61,19 +61,20 @@ function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand) {
61
61
  if (platform === "markdown") {
62
62
  handoff.markdown = buildAgentChatHandoff(plan).markdown;
63
63
  }
64
- else if (platform === "plain_chat" && presentation.qr_png_url) {
65
- handoff.qr_image_url = presentation.qr_png_url;
64
+ else if (platform === "plain_chat" && plan.preferredQRSources[0]) {
65
+ handoff.qr_image_url = plan.preferredQRSources[0];
66
66
  }
67
+ const amount = formatMoney(presentation.checkout.amount_minor, presentation.checkout.currency);
67
68
  return {
68
69
  status: "human_checkout_required",
69
70
  result: {
70
71
  checkout_id: presentation.checkout.checkout_id,
71
72
  payment: "pending",
72
- amount: formatMoney(presentation.checkout.amount_minor, presentation.checkout.currency),
73
+ amount,
73
74
  },
74
75
  handoff,
75
- instruction: pendingInstruction(platform),
76
- next: { command: nextCommand, reason: "稍后查询同一笔 Checkout 状态" },
76
+ instruction: pendingInstruction(platform, amount),
77
+ next: { command: nextCommand, reason: "稍后只查询同一 Checkout" },
77
78
  recovery: [],
78
79
  };
79
80
  }
@@ -96,9 +97,9 @@ function terminalCheckoutEnvelope(presentation) {
96
97
  const recovery = [];
97
98
  if (payment === "verified") {
98
99
  status = "completed";
99
- instruction = "付款已确认,不要再次展示付款二维码。";
100
+ instruction = "Backend 已确认这笔付款。不要再次展示付款入口,不要调用 pay,不要创建新 Checkout 或 Execution。现在只执行 next.command,读取同一 Execution 的履约结果。";
100
101
  next = serviceExecutionIDs.length === 1
101
- ? { command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "读取履约状态" }
102
+ ? { command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "读取同一笔已付款 Service Execution" }
102
103
  : presentation.completed_order_id
103
104
  ? { command: `itpay order ${presentation.completed_order_id}`, reason: "读取已创建订单" }
104
105
  : { command: "itpay orders", reason: "恢复已付款订单" };
@@ -122,12 +123,12 @@ function checkoutNeedsHumanHandoff(status) {
122
123
  function checkoutPlainResult(result) {
123
124
  return Object.entries(result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
124
125
  }
125
- function pendingInstruction(platform) {
126
+ function pendingInstruction(platform, amount) {
126
127
  if (platform === "markdown")
127
- return "把 handoff.markdown 原样发送到当前桌面对话;二维码和链接可见后等待用户操作,不要创建新 Checkout。";
128
+ return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、链接和金额 ${amount} 已对用户可见,然后停止等待。不要创建新 Checkout、Execution 或 Payment Intent;稍后仍然只执行 next.command 查询这一笔 Checkout。`;
128
129
  if (platform === "terminal")
129
- return "在用户可见终端展示二维码和付款链接,然后等待用户操作;不要创建新 Checkout。";
130
- return "把付款链接和可用二维码附件发送给用户,然后等待用户操作;不要创建新 Checkout。";
130
+ return `Backend 尚未确认付款。在用户可见终端展示当前同一 Checkout 的二维码、链接和金额 ${amount},然后停止等待。不要创建新 Checkout、Execution 或 Payment Intent;稍后仍然只执行 next.command 查询这一笔 Checkout。`;
131
+ return `Backend 尚未确认付款。把 handoff.url 作为可点击链接发送给用户,优先把 handoff.qr_local_path 作为图片附件发送,不能发送本地附件时使用 handoff.qr_image_url,并说明金额 ${amount},然后停止等待。不要声称付款成功,不要创建新 Checkout、Execution 或 Payment Intent。稍后仍然只执行 next.command 查询这一笔 Checkout。`;
131
132
  }
132
133
  function formatMoney(amountMinor, currency) {
133
134
  return `${(amountMinor / 100).toFixed(2)} ${currency}`;
@@ -143,3 +144,12 @@ function checkoutQRPNGURL(baseURL, checkoutID, displayToken) {
143
144
  function publicRoot(baseURL) {
144
145
  return (baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
145
146
  }
147
+ function absolutePublicURL(baseURL, value) {
148
+ try {
149
+ const root = publicRoot(baseURL);
150
+ return new URL(value, `${root}/`).toString();
151
+ }
152
+ catch {
153
+ return value;
154
+ }
155
+ }
@@ -1,5 +1,6 @@
1
1
  import { HttpError } from "../client/http.js";
2
2
  import { resolveOutput } from "../render/sink.js";
3
+ import { declaredAgentType, qualifyItPayCommand } from "../state/agent_type.js";
3
4
  export function isTerminalServiceExecutionStatus(status) {
4
5
  return status === "failed" || status === "refunded" || status === "cancelled";
5
6
  }
@@ -17,12 +18,14 @@ export class CommandContractError extends Error {
17
18
  }
18
19
  export function writeCommandEnvelope(value, options = {}) {
19
20
  const out = resolveOutput(options.output);
21
+ const agentType = options.agentType ?? declaredAgentType();
22
+ const qualified = qualifyEnvelope(value, agentType);
20
23
  if (options.jsonOutput) {
21
- out(JSON.stringify(value, null, 2) + "\n");
24
+ out(JSON.stringify(qualified, null, 2) + "\n");
22
25
  return;
23
26
  }
24
- out(`${value.status}\n`);
25
- const facts = "result" in value ? value.result : value.error;
27
+ out(`${qualified.status}\n`);
28
+ const facts = "result" in qualified ? qualified.result : qualified.error;
26
29
  if (options.plainResult) {
27
30
  for (const line of options.plainResult)
28
31
  out(`${line}\n`);
@@ -32,17 +35,17 @@ export function writeCommandEnvelope(value, options = {}) {
32
35
  out(`${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
33
36
  }
34
37
  }
35
- if ("handoff" in value && value.handoff) {
36
- for (const [key, fact] of Object.entries(value.handoff)) {
38
+ if ("handoff" in qualified && qualified.handoff) {
39
+ for (const [key, fact] of Object.entries(qualified.handoff)) {
37
40
  out(`handoff.${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
38
41
  }
39
42
  }
40
- out(`instruction: ${value.instruction}\n`);
41
- if (value.next)
42
- out(`next: ${value.next.command}\n`);
43
- if (value.recovery.length > 0) {
43
+ out(`instruction: ${qualified.instruction}\n`);
44
+ if (qualified.next)
45
+ out(`next: ${qualified.next.command}\n`);
46
+ if (qualified.recovery.length > 0) {
44
47
  out("recovery:\n");
45
- for (const action of value.recovery) {
48
+ for (const action of qualified.recovery) {
46
49
  out(` - ${action.command}\n`);
47
50
  out(` reason: ${action.reason}\n`);
48
51
  }
@@ -56,6 +59,7 @@ export function attachAgentGuidance(payload, guidance) {
56
59
  }
57
60
  export function printAgentGuidance(guidance, output) {
58
61
  const out = resolveOutput(output);
62
+ const agentType = declaredAgentType();
59
63
  out(`${guidance.summary}\n`);
60
64
  if (guidance.visible_results?.length) {
61
65
  out("results:\n");
@@ -75,7 +79,7 @@ export function printAgentGuidance(guidance, output) {
75
79
  out("next actions:\n");
76
80
  for (const action of guidance.next_actions) {
77
81
  out(` - ${action.label}\n`);
78
- out(` ${action.command}\n`);
82
+ out(` ${qualifyItPayCommand(action.command, agentType)}\n`);
79
83
  if (action.requires_human)
80
84
  out(" requires human confirmation\n");
81
85
  if (action.reason)
@@ -86,10 +90,20 @@ export function printAgentGuidance(guidance, output) {
86
90
  out("recovery:\n");
87
91
  for (const action of guidance.recovery) {
88
92
  out(` - ${action.label}\n`);
89
- out(` ${action.command}\n`);
93
+ out(` ${qualifyItPayCommand(action.command, agentType)}\n`);
90
94
  }
91
95
  }
92
96
  }
97
+ function qualifyEnvelope(value, agentType) {
98
+ return {
99
+ ...value,
100
+ next: value.next ? { ...value.next, command: qualifyItPayCommand(value.next.command, agentType) } : null,
101
+ recovery: value.recovery.map((action) => ({
102
+ ...action,
103
+ command: qualifyItPayCommand(action.command, agentType),
104
+ })),
105
+ };
106
+ }
93
107
  export function buildCartGuidance(cart, serviceModel) {
94
108
  const serviceLine = latestServiceLine(cart);
95
109
  if (serviceLine?.service_execution_id) {
@@ -225,7 +239,7 @@ export function buildServiceHandleGuidance(serviceExecutionID, checkoutCapabilit
225
239
  export function errorRecoveryActions(error) {
226
240
  if (!(error instanceof HttpError))
227
241
  return [];
228
- if (error.code === "agent_identity_required" || error.code === "agent_device_session_required") {
242
+ if (error.code === "agent_identity_required") {
229
243
  return [
230
244
  {
231
245
  id: "inspect_agent_setup",
@@ -234,6 +248,16 @@ export function errorRecoveryActions(error) {
234
248
  },
235
249
  ];
236
250
  }
251
+ if (error.code === "agent_device_session_required") {
252
+ return [
253
+ {
254
+ id: "read_agent_session_rules",
255
+ label: "Read identity and session recovery rules",
256
+ command: "itpay skill show itpay-buyer --json",
257
+ reason: "The CLI already attempted one automatic session renewal; do not rotate identity or loop retries.",
258
+ },
259
+ ];
260
+ }
237
261
  if (error.code === "quota_exhausted" || error.code === "checkout_required") {
238
262
  return [
239
263
  {
@@ -4,9 +4,9 @@ export async function runReadyz(backend, options = {}) {
4
4
  const response = await backend.readyz();
5
5
  writeCommandEnvelope({
6
6
  status: response.status,
7
- result: { backend: "available" },
8
- instruction: "ItPay 可用,可以读取服务目录。",
9
- next: { command: "itpay catalog list", reason: "发现可用服务" },
7
+ result: { backend: "available", ...(options.agentType ? { agent_type: options.agentType } : {}) },
8
+ instruction: "ItPay 可用;先完整读取内置 Buyer Skill,再开始服务流程。",
9
+ next: { command: "itpay skill show itpay-buyer --json", reason: "加载完整操作与安全规则" },
10
10
  recovery: [],
11
11
  }, options);
12
12
  }