@itpay/cli 2.0.31 → 2.0.33

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 (35) hide show
  1. package/README.md +13 -3
  2. package/dist/src/client/backend.js +3 -0
  3. package/dist/src/commands/checkout.js +1 -1
  4. package/dist/src/commands/feedback.js +143 -0
  5. package/dist/src/commands/guidance.js +17 -412
  6. package/dist/src/commands/install.js +1 -1
  7. package/dist/src/commands/order.js +4 -4
  8. package/dist/src/commands/orders.js +1 -4
  9. package/dist/src/commands/readyz.js +2 -2
  10. package/dist/src/commands/services.js +53 -30
  11. package/dist/src/commands/vault.js +1 -1
  12. package/dist/src/main.js +69 -9
  13. package/dist/src/state/config.js +1 -1
  14. package/docs/agent/buyer/catalog-list.json +11 -8
  15. package/docs/agent/buyer/install-and-setup.json +15 -13
  16. package/docs/agent/buyer/orders-refunds.json +7 -3
  17. package/docs/agent/buyer/payment-flow.json +1 -2
  18. package/docs/agent/buyer/purchased-content.json +5 -0
  19. package/docs/agent/buyer/quickstart.json +29 -40
  20. package/docs/agent/buyer/service-feedback.json +69 -0
  21. package/docs/cli-reference/commands/checkout.md +1 -1
  22. package/docs/cli-reference/commands/feedback/index.md +29 -0
  23. package/docs/cli-reference/commands/feedback/submit.md +123 -0
  24. package/docs/cli-reference/commands/install.md +3 -1
  25. package/docs/cli-reference/commands/order.md +1 -1
  26. package/docs/cli-reference/commands/readyz.md +3 -3
  27. package/docs/cli-reference/commands/services/action.md +1 -1
  28. package/docs/cli-reference/commands/services/invoke.md +5 -5
  29. package/docs/cli-reference/commands/services/list.md +3 -3
  30. package/docs/cli-reference/commands/services/next.md +5 -5
  31. package/docs/cli-reference/commands/vault/read.md +1 -1
  32. package/docs/cli-reference/conventions.md +27 -0
  33. package/docs/cli-reference/index.md +5 -0
  34. package/package.json +1 -1
  35. package/skills/itpay/SKILL.md +67 -150
package/README.md CHANGED
@@ -8,13 +8,23 @@ The official V3 CLI and the single ItPay entry point for services, purchases, ac
8
8
 
9
9
  ```bash
10
10
  npm install -g @itpay/cli
11
- itpay readyz --json
12
- itpay skill show itpay --json
13
11
  itpay install --json
14
12
  itpay --agent-type codex-desktop readyz --json
15
- # read the typed Skill, understand the human's intent, then choose the first command
13
+ # execute the returned next.command until the typed Skill is loaded
16
14
  ```
17
15
 
16
+ Official Agent install prompt(可直接复制给 Agent):
17
+
18
+ > 安装或更新官方 `@itpay/cli`。从 `itpay install --json` 开始,选择你真实
19
+ > 所在的平台,完整执行 iTPay 返回的每个 `next.command` 并读取内置 Skill。
20
+ > 初始化后,根据我的自然语言需求自行使用 iTPay,不要让我运行命令,也不要向
21
+ > 我讲解命令、内部 ID 或技术步骤。只有需要我选择、授权、付款、提供联系方式
22
+ > 或确认退款时,才用通俗语言询问我;展示官方操作入口后停止等待。不要猜命令、
23
+ > 切换身份或环境、绕过授权或重复创建资源。准备完成后只回复“iTPay 已就绪”。
24
+
25
+ Use a separate test prompt to set the exact official Dev Backend. Never put a
26
+ Dev override in the public install prompt.
27
+
18
28
  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
29
 
20
30
  ## Output Contract
@@ -58,6 +58,9 @@ export class BackendClient {
58
58
  getOrderDeliveryAccess(orderID) {
59
59
  return this.http.get(`/v1/orders/${encodeURIComponent(orderID)}/delivery-access`);
60
60
  }
61
+ submitServiceFeedback(orderID, input) {
62
+ return this.http.post(`/v1/orders/${encodeURIComponent(orderID)}/feedback`, input);
63
+ }
61
64
  listAccountOrders(limit, status, bearer, cursor) {
62
65
  const qs = new URLSearchParams({ limit: String(limit) });
63
66
  if (status) {
@@ -110,7 +110,7 @@ function terminalCheckoutEnvelope(presentation) {
110
110
  const recovery = [];
111
111
  if (payment === "verified") {
112
112
  status = "completed";
113
- instruction = "先告诉用户:付款已经确认,订单已经记录,不需要再次付款;结果会在同一订单下继续准备,如果最终无法正常交付,可以从原订单申请退款,处理方式由内容是否已使用决定。然后只执行 next.command 读取同一 Execution;不要再次展示付款入口、调用 pay 或创建新 Checkout/Execution,也不要承诺退款结果。";
113
+ instruction = "告诉用户:付款已经确认,订单已经记录,不需要再次付款;结果会在同一订单下继续准备,如果最终无法交付,可以从原订单申请退款,处理方式由内容是否已使用决定。然后只执行 next.command 读取同一笔服务;Agent 不再展示付款入口或创建新订单,也不承诺退款结果。";
114
114
  next = serviceExecutionIDs.length === 1
115
115
  ? { command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "读取同一笔已付款 Service Execution" }
116
116
  : presentation.completed_order_id
@@ -0,0 +1,143 @@
1
+ import { CLI_VERSION } from "../state/config.js";
2
+ import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
3
+ const MAX_FEEDBACK_NOTE_CODE_POINTS = 2000;
4
+ export async function runFeedbackSubmit(backend, orderID, options) {
5
+ const normalizedOrderID = orderID?.trim() ?? "";
6
+ if (!normalizedOrderID) {
7
+ throw new CommandContractError("order_required", "--order is required", "先恢复用户所说的原订单;Agent 自己取得订单,不要求用户提供内部 ID。本次未提交反馈。", [{ command: "itpay orders --json", reason: "列出当前授权账号的订单摘要" }]);
8
+ }
9
+ const rating = normalizeFeedbackRating(options.rating);
10
+ const itemRank = normalizeItemRank(options.itemRank);
11
+ const userNote = normalizeUserNote(options.note ?? "");
12
+ const order = await backend.getOrder(normalizedOrderID);
13
+ const choices = feedbackItemChoices(order);
14
+ if (choices.length === 0) {
15
+ throw new CommandContractError("feedback_unavailable", "this order has no service item available for feedback", "告诉用户这笔订单当前没有可评价的服务项目并停止;不要猜测项目 ID、切换身份或创建其他反馈。", []);
16
+ }
17
+ if (choices.length > 1 && itemRank === undefined) {
18
+ writeCommandEnvelope({
19
+ status: "feedback_item_selection_required",
20
+ result: {
21
+ ...(order.order_code ? { order_code: order.order_code } : {}),
22
+ items: choices.map(({ rank, title, subject }) => ({ rank, title, ...(subject ? { subject } : {}) })),
23
+ },
24
+ instruction: "用服务名称和主题让用户选择要评价哪一项;不要展示内部 ID。用户选择后,Agent 使用同一订单、评分和留言并加入所选 item rank 自己执行提交。",
25
+ next: null,
26
+ recovery: [],
27
+ }, outputOptions(options));
28
+ return;
29
+ }
30
+ const choice = itemRank === undefined
31
+ ? choices[0]
32
+ : choices.find((candidate) => candidate.rank === itemRank);
33
+ if (!choice) {
34
+ throw new CommandContractError("feedback_item_invalid", `item rank ${itemRank} is not available for feedback`, "只使用当前订单返回的服务项目编号;不要猜测内部 ID。本次未提交反馈。", []);
35
+ }
36
+ const note = formatFeedbackNote({
37
+ userNote,
38
+ outcome: order.status,
39
+ serviceTitle: choice.title,
40
+ environment: options.environment,
41
+ ...(options.agentType ? { agentType: options.agentType } : {}),
42
+ });
43
+ if (codePointLength(note) > MAX_FEEDBACK_NOTE_CODE_POINTS) {
44
+ throw new CommandContractError("feedback_note_too_long", `formatted feedback note exceeds ${MAX_FEEDBACK_NOTE_CODE_POINTS} Unicode code points`, "请用户缩短反馈内容;不要截断、改写或拆分提交。本次未提交反馈。", []);
45
+ }
46
+ const response = await backend.submitServiceFeedback(normalizedOrderID, {
47
+ order_item_id: choice.item.order_item_id,
48
+ rating,
49
+ note,
50
+ });
51
+ writeCommandEnvelope({
52
+ status: "feedback_submitted",
53
+ result: {
54
+ ...(order.order_code ? { order_code: order.order_code } : {}),
55
+ service_title: choice.title,
56
+ rating: response.feedback.rating,
57
+ feedback_status: response.feedback.status,
58
+ },
59
+ instruction: "告诉用户反馈已经记录并表示感谢,然后停止。不要承诺回复、处理时间、退款或结果变更。",
60
+ next: null,
61
+ recovery: [],
62
+ }, outputOptions(options));
63
+ }
64
+ export function normalizeFeedbackRating(value) {
65
+ const normalized = value?.trim().toLowerCase() ?? "";
66
+ const numeric = normalized.match(/^([1-5])(?:\s*(?:\/\s*5|分|星|stars?))?$/u);
67
+ if (numeric)
68
+ return Number(numeric[1]);
69
+ const chinese = normalized.match(/^([一二三四五])(?:分|星)?$/u)?.[1];
70
+ if (chinese)
71
+ return { 一: 1, 二: 2, 三: 3, 四: 4, 五: 5 }[chinese];
72
+ throw new CommandContractError("feedback_rating_invalid", "--rating must be an explicit score from 1 to 5", "请用户明确给出 1–5 分;不要从好评、差评或情绪推断评分。本次未提交反馈。", []);
73
+ }
74
+ function normalizeItemRank(value) {
75
+ if (value === undefined)
76
+ return undefined;
77
+ const match = value.trim().match(/^#?([1-9]\d*)$/u);
78
+ if (!match) {
79
+ throw new CommandContractError("feedback_item_invalid", "--item-rank must be a positive integer", "使用当前订单返回的正整数服务项目编号;本次未提交反馈。", []);
80
+ }
81
+ const rank = Number(match[1]);
82
+ if (!Number.isSafeInteger(rank)) {
83
+ throw new CommandContractError("feedback_item_invalid", "--item-rank is outside the supported integer range", "使用当前订单返回的服务项目编号;本次未提交反馈。", []);
84
+ }
85
+ return rank;
86
+ }
87
+ function feedbackItemChoices(order) {
88
+ return order.items.flatMap((item, index) => {
89
+ if (!item.order_item_id?.trim())
90
+ return [];
91
+ const subject = feedbackSubject(item);
92
+ return [{
93
+ rank: index + 1,
94
+ item,
95
+ title: safeLine(item.title) || "未命名服务",
96
+ ...(subject ? { subject } : {}),
97
+ }];
98
+ });
99
+ }
100
+ function feedbackSubject(item) {
101
+ for (const field of ["company_name_or_credit_no", "company_name", "company", "target", "keyword"]) {
102
+ const value = item.input?.[field];
103
+ if (typeof value === "string") {
104
+ const safe = safeLine(value);
105
+ if (safe)
106
+ return safe.slice(0, 160);
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+ function normalizeUserNote(value) {
112
+ return value
113
+ .replaceAll("\r\n", "\n")
114
+ .replaceAll("\r", "\n")
115
+ .replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/gu, " ")
116
+ .trim();
117
+ }
118
+ function formatFeedbackNote(input) {
119
+ const summary = input.userNote
120
+ ? `## Summary\n${input.userNote.split("\n").map((line) => `> ${line}`).join("\n")}\n\n`
121
+ : "";
122
+ return `${summary}## Context\n` + [
123
+ "- Source: user-confirmed via agent",
124
+ `- Outcome: ${safeLine(input.outcome) || "unknown"}`,
125
+ `- Service: ${safeLine(input.serviceTitle) || "未命名服务"}`,
126
+ `- Client: @itpay/cli ${CLI_VERSION}`,
127
+ `- Agent type: ${safeLine(input.agentType ?? "unspecified")}`,
128
+ `- Environment: ${input.environment}`,
129
+ ].join("\n");
130
+ }
131
+ function safeLine(value) {
132
+ return value.replace(/[\r\n\u0000-\u001F\u007F]+/gu, " ").trim();
133
+ }
134
+ function codePointLength(value) {
135
+ return Array.from(value).length;
136
+ }
137
+ function outputOptions(options) {
138
+ return {
139
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
140
+ ...(options.output ? { output: options.output } : {}),
141
+ ...(options.agentType ? { agentType: options.agentType } : {}),
142
+ };
143
+ }
@@ -5,6 +5,12 @@ import { qualifyBackendCommand } from "../state/config.js";
5
5
  export function isTerminalServiceExecutionStatus(status) {
6
6
  return status === "failed" || status === "refunded" || status === "cancelled";
7
7
  }
8
+ export function appendOptionalFeedbackInvitation(instruction, outcome) {
9
+ const invitation = outcome === "delivered"
10
+ ? "结果解释完毕后,可以询问用户是否愿意给这次服务 1–5 分和一句可选建议。只有用户明确评分后,Agent 才使用同一订单提交反馈;不要展示命令或内部 ID,同一对话最多询问一次。"
11
+ : "先完成同一订单的恢复和退款权利解释;处理清楚后,可以询问用户是否愿意给 1–5 分并说明卡壳点。只有用户明确评分后才提交反馈,同一对话最多询问一次。";
12
+ return `${instruction} ${invitation}`;
13
+ }
8
14
  export class CommandContractError extends Error {
9
15
  code;
10
16
  instruction;
@@ -57,49 +63,6 @@ export function writeCommandEnvelope(value, options = {}) {
57
63
  }
58
64
  }
59
65
  }
60
- export function attachAgentGuidance(payload, guidance) {
61
- return {
62
- ...payload,
63
- agent_guidance: guidance,
64
- };
65
- }
66
- export function printAgentGuidance(guidance, output) {
67
- const out = resolveOutput(output);
68
- const agentType = declaredAgentType();
69
- out(`${guidance.summary}\n`);
70
- if (guidance.visible_results?.length) {
71
- out("results:\n");
72
- for (const item of guidance.visible_results) {
73
- out(` ${item.rank}. ${item.title}\n`);
74
- for (const [key, value] of Object.entries(item.safe_payload)) {
75
- if (key === "name" || key === "company_name" || value === "" || value === undefined || value === null)
76
- continue;
77
- out(` ${key}: ${String(value)}\n`);
78
- }
79
- }
80
- }
81
- if (guidance.next_actions.length === 0) {
82
- out("next actions: none\n");
83
- }
84
- else {
85
- out("next actions:\n");
86
- for (const action of guidance.next_actions) {
87
- out(` - ${action.label}\n`);
88
- out(` ${qualifyBackendCommand(qualifyItPayCommand(action.command, agentType))}\n`);
89
- if (action.requires_human)
90
- out(" requires human confirmation\n");
91
- if (action.reason)
92
- out(` reason: ${action.reason}\n`);
93
- }
94
- }
95
- if (guidance.recovery.length > 0) {
96
- out("recovery:\n");
97
- for (const action of guidance.recovery) {
98
- out(` - ${action.label}\n`);
99
- out(` ${qualifyBackendCommand(qualifyItPayCommand(action.command, agentType))}\n`);
100
- }
101
- }
102
- }
103
66
  function qualifyEnvelope(value, agentType) {
104
67
  return {
105
68
  ...value,
@@ -110,181 +73,31 @@ function qualifyEnvelope(value, agentType) {
110
73
  })),
111
74
  };
112
75
  }
113
- export function buildCartGuidance(cart, serviceModel) {
114
- const serviceLine = latestServiceLine(cart);
115
- if (serviceLine?.service_execution_id) {
116
- const serviceGuidance = serviceModel
117
- ? buildServiceReadModelGuidance(serviceModel)
118
- : buildServiceHandleGuidance(serviceLine.service_execution_id, serviceLine.service_capability_id);
119
- return {
120
- ...serviceGuidance,
121
- kind: "cart_service_execution",
122
- summary: `cart ${cart.cart_id}: service-backed line ${lineID(serviceLine)} is ready for Service Execution`,
123
- state: {
124
- ...serviceGuidance.state,
125
- cart_id: cart.cart_id,
126
- cart_item_id: lineID(serviceLine),
127
- service_capability_id: serviceLine.service_capability_id,
128
- },
129
- };
130
- }
131
- if (cart.items.length === 0) {
132
- return {
133
- kind: "empty_cart",
134
- summary: `cart ${cart.cart_id}: empty`,
135
- state: { cart_id: cart.cart_id, status: cart.status },
136
- next_actions: [
137
- {
138
- id: "browse_catalog",
139
- label: "Browse services",
140
- command: "itpay catalog list",
141
- },
142
- ],
143
- recovery: [],
144
- };
145
- }
146
- return {
147
- kind: "cart_checkout_ready",
148
- summary: `cart ${cart.cart_id}: ready for checkout`,
149
- state: { cart_id: cart.cart_id, status: cart.status, amount_minor: cart.amount_minor, currency: cart.currency },
150
- next_actions: [
151
- {
152
- id: "checkout_cart",
153
- label: "Create ItPay checkout",
154
- command: `itpay buy --cart ${cart.cart_id} --host <client> --contact-email <email>`,
155
- requires_human: true,
156
- reason: "The human must review and pay on the ItPay checkout page.",
157
- },
158
- ],
159
- recovery: [
160
- {
161
- id: "show_cart",
162
- label: "Inspect current server cart",
163
- command: "itpay cart show",
164
- },
165
- ],
166
- };
167
- }
168
- export function buildServiceStartedGuidance(response) {
169
- return buildServiceGuidance({
170
- execution: response.execution,
171
- capabilities: response.capabilities,
172
- });
173
- }
174
- export function buildServiceReadModelGuidance(model) {
175
- return buildServiceGuidance({
176
- execution: model.execution,
177
- capabilities: model.capabilities,
178
- resultItems: model.result_items,
179
- checkoutBindings: model.checkout_bindings,
180
- deliveryBindings: model.delivery_bindings,
181
- });
182
- }
183
- export function buildServiceInvokedGuidance(response, capabilities = []) {
184
- return buildServiceGuidance({
185
- execution: response.execution,
186
- capabilities,
187
- resultItems: response.result_items,
188
- ...(response.next_actions ? { backendNextActions: response.next_actions } : {}),
189
- ...(response.effective_quota ? { effectiveQuota: response.effective_quota } : {}),
190
- providerCalled: response.provider_called,
191
- });
192
- }
193
- export function buildServiceActionGuidance(action) {
194
- const serviceExecutionID = action.service_execution_id;
195
- return {
196
- kind: "service_action_recorded",
197
- summary: `service execution ${serviceExecutionID}: action ${action.action_type} recorded`,
198
- state: {
199
- service_execution_id: serviceExecutionID,
200
- action_type: action.action_type,
201
- status: action.status,
202
- result_item_id: action.result_item_id,
203
- },
204
- next_actions: [
205
- {
206
- id: "inspect_service_execution",
207
- label: "Read updated Service Execution state",
208
- command: `itpay services next ${serviceExecutionID} --json`,
209
- },
210
- ],
211
- recovery: [
212
- {
213
- id: "timeline",
214
- label: "Inspect full timeline",
215
- command: `itpay services get ${serviceExecutionID}`,
216
- },
217
- ],
218
- };
219
- }
220
- export function buildServiceHandleGuidance(serviceExecutionID, checkoutCapabilityID) {
221
- const actions = [
222
- {
223
- id: "inspect_service_execution",
224
- label: "Read Service Execution state and capabilities",
225
- command: `itpay services next ${serviceExecutionID} --json`,
226
- },
227
- ];
228
- if (checkoutCapabilityID) {
229
- actions.push({
230
- id: "checkout_service",
231
- label: "Create checkout after human confirmation",
232
- command: `itpay services checkout ${serviceExecutionID} --capability ${checkoutCapabilityID} --json`,
233
- requires_human: true,
234
- reason: "Inspect the Service Execution first; the CLI will request delivery contact only when the selected capability requires it.",
235
- });
236
- }
237
- return {
238
- kind: "service_execution_handle",
239
- summary: `service execution ${serviceExecutionID}: inspect before invoking`,
240
- state: { service_execution_id: serviceExecutionID },
241
- next_actions: actions,
242
- recovery: [],
243
- };
244
- }
245
76
  export function errorRecoveryActions(error) {
246
77
  if (!(error instanceof HttpError))
247
78
  return [];
248
79
  if (error.code === "agent_identity_required") {
249
- return [
250
- {
251
- id: "inspect_agent_setup",
252
- label: "Inspect supported Agent Type setup",
253
- command: "itpay install --json",
254
- },
255
- ];
80
+ return [{ id: "inspect_agent_setup", label: "Inspect supported Agent Type setup", command: "itpay install --json" }];
256
81
  }
257
82
  if (error.code === "agent_device_session_required") {
258
- return [
259
- {
83
+ return [{
260
84
  id: "read_agent_session_rules",
261
85
  label: "Read ItPay identity and session recovery rules",
262
86
  command: "itpay skill show itpay --json",
263
87
  reason: "The CLI already attempted one automatic session renewal; do not rotate identity or loop retries.",
264
- },
265
- ];
88
+ }];
266
89
  }
267
90
  if (error.code === "quota_exhausted" || error.code === "checkout_required") {
268
- return [
269
- {
91
+ return [{
270
92
  id: "inspect_service_execution",
271
93
  label: "Inspect Service Execution before checkout",
272
94
  command: "itpay services next <service_execution_id> --json",
273
- },
274
- ];
95
+ }];
275
96
  }
276
97
  if (error.code === "cart_item_locked" || error.status === 409) {
277
98
  return [
278
- {
279
- id: "show_cart",
280
- label: "Inspect the canonical server cart",
281
- command: "itpay cart show",
282
- },
283
- {
284
- id: "continue_checkout",
285
- label: "Continue the last locally remembered checkout",
286
- command: "itpay checkout",
287
- },
99
+ { id: "show_cart", label: "Inspect the canonical server cart", command: "itpay cart show" },
100
+ { id: "continue_checkout", label: "Continue the last locally remembered checkout", command: "itpay checkout" },
288
101
  {
289
102
  id: "recover_service_execution",
290
103
  label: "List recoverable Service Executions if the local handoff is missing",
@@ -293,22 +106,18 @@ export function errorRecoveryActions(error) {
293
106
  ];
294
107
  }
295
108
  if (error.status === 404) {
296
- return [
297
- {
109
+ return [{
298
110
  id: "recover_service_executions",
299
111
  label: "List visible Service Executions and follow their next instruction",
300
112
  command: "itpay services list",
301
- },
302
- ];
113
+ }];
303
114
  }
304
115
  if (error.status === 502 || error.status === 503 || error.status === 504) {
305
- return [
306
- {
116
+ return [{
307
117
  id: "retry_after_backend_recovers",
308
118
  label: "Retry after the selected official Backend is reachable",
309
119
  command: "itpay readyz",
310
- },
311
- ];
120
+ }];
312
121
  }
313
122
  return [];
314
123
  }
@@ -323,207 +132,3 @@ export function printErrorRecovery(error, output) {
323
132
  out(` ${action.command}\n`);
324
133
  }
325
134
  }
326
- function buildServiceGuidance(input) {
327
- const execution = input.execution;
328
- const capabilities = input.capabilities ?? [];
329
- const prePurchase = firstPrePurchaseCapability(capabilities);
330
- const paid = firstPaidCapability(capabilities);
331
- const resultItem = input.resultItems?.[0];
332
- const checkoutID = input.checkoutBindings?.at(-1)?.checkout_id;
333
- const delivery = input.deliveryBindings?.[0];
334
- const deliveryMode = String(delivery?.redacted_summary?.delivery_mode ?? (delivery?.vault_artifact_id ? "vault_artifact" : ""));
335
- const backendCheckout = input.backendNextActions?.find((action) => action.kind === "create_checkout");
336
- const nextActions = [];
337
- const recovery = [
338
- {
339
- id: "timeline",
340
- label: "Inspect full timeline",
341
- command: `itpay services get ${execution.service_execution_id}`,
342
- },
343
- ];
344
- if (isTerminalServiceExecutionStatus(execution.status)) {
345
- // Terminal executions are inspectable, but no command may advance or replay them.
346
- }
347
- else if (deliveryMode === "agent_visible_result" && (input.resultItems?.length ?? 0) > 0) {
348
- nextActions.push({
349
- id: "use_agent_visible_result",
350
- label: "Use the safe candidate list shown above",
351
- command: `itpay services next ${execution.service_execution_id} --json`,
352
- reason: "This result is already visible to the agent. Do not call services read-result; that command is only for Vault deliveries authorized by a human.",
353
- });
354
- }
355
- else if (execution.status === "completed" || execution.next_action === "completed") {
356
- nextActions.push({
357
- id: "inspect_order_or_grant",
358
- label: "Inspect order, claim, or grant from the checkout/order owner",
359
- command: "itpay orders --limit 20",
360
- requires_human: true,
361
- reason: "Delivery visibility is controlled by the human account and grant flow.",
362
- });
363
- }
364
- else if (execution.phase === "delivery" || execution.next_action === "view_delivery") {
365
- const grantStatus = String(delivery?.grant_status ?? "");
366
- if (grantStatus === "active") {
367
- nextActions.push({
368
- id: "read_granted_result",
369
- label: "Read the human-granted service result",
370
- command: `itpay services read-result ${execution.service_execution_id}`,
371
- reason: "The human granted temporary agent access to this delivery.",
372
- });
373
- }
374
- else {
375
- nextActions.push({
376
- id: grantStatus === "expired" ? "ask_human_to_reauthorize" : "wait_for_human_agent_grant",
377
- label: grantStatus === "expired" ? "Ask the human to authorize AI access again" : "Wait for human AI-access authorization",
378
- command: `itpay services get ${execution.service_execution_id}`,
379
- requires_human: true,
380
- reason: grantStatus === "expired"
381
- ? "The 15 minute human grant expired."
382
- : "The human must approve AI access on the Credential page.",
383
- });
384
- }
385
- }
386
- else if (execution.next_action === "pay_checkout" || execution.status === "checkout_pending") {
387
- nextActions.push({
388
- id: "open_existing_checkout",
389
- label: "Open the existing ItPay checkout handoff",
390
- command: `itpay services checkout ${execution.service_execution_id} --resume --json`,
391
- requires_human: true,
392
- reason: checkoutID
393
- ? `Checkout ${checkoutID} already exists; reissue a short-lived handoff without creating another checkout.`
394
- : "Recover the existing checkout handoff from Service Execution owner facts.",
395
- });
396
- }
397
- else if (execution.checkout_required || execution.next_action === "create_checkout") {
398
- const capabilityID = backendCheckout?.capability_id ?? paid?.capability_id ?? input.checkoutCapabilityID ?? execution.current_capability_id;
399
- if (capabilityID) {
400
- const checkoutCapability = capabilities.find((capability) => capability.capability_id === capabilityID);
401
- const emailRequired = checkoutCapability?.delivery_email_required === true;
402
- nextActions.push({
403
- id: "checkout_service",
404
- label: "Create ItPay checkout for the paid service capability",
405
- command: `itpay services checkout ${execution.service_execution_id} --capability ${capabilityID}${emailRequired ? " --email <email>" : ""} --json`,
406
- requires_human: true,
407
- reason: emailRequired
408
- ? deliveryEmailGuidance(checkoutCapability?.delivery_email_purpose)
409
- : "This capability returns an agent-visible result after payment and does not require a delivery email.",
410
- });
411
- }
412
- else {
413
- nextActions.push({
414
- id: "inspect_capabilities",
415
- label: "Inspect capabilities before checkout",
416
- command: `itpay services get ${execution.service_execution_id}`,
417
- });
418
- }
419
- }
420
- else if ((input.providerCalled || execution.next_action === "select_candidate") && (input.resultItems?.length ?? 0) === 0) {
421
- // Terminal empty result: a later explicit human request starts a new flow.
422
- }
423
- else if (needsHumanSelection(execution, resultItem)) {
424
- nextActions.push({
425
- id: "select_result_item",
426
- label: "Ask the human to select a result item, then submit the selection",
427
- command: `itpay services action ${execution.service_execution_id} --action select_candidate --actor-type human --status approved --candidate <rank>`,
428
- requires_human: true,
429
- reason: "Do not choose a candidate without explicit human confirmation.",
430
- });
431
- }
432
- else if (prePurchase) {
433
- const action = {
434
- id: "invoke_capability",
435
- label: `Invoke ${prePurchase.capability_id}`,
436
- command: `itpay services invoke ${execution.service_execution_id} --capability ${prePurchase.capability_id} --input key=value --json`,
437
- };
438
- if (prePurchase.free_quota_limit) {
439
- action.reason = `One keyword per execution. Free quota limit: ${prePurchase.free_quota_limit} per ${prePurchase.quota_subject || "subject"}.`;
440
- }
441
- nextActions.push(action);
442
- }
443
- else {
444
- nextActions.push({
445
- id: "inspect_service_execution",
446
- label: "Read Service Execution state",
447
- command: `itpay services next ${execution.service_execution_id} --json`,
448
- });
449
- }
450
- return {
451
- kind: "service_execution",
452
- summary: `service execution ${execution.service_execution_id}: ${execution.status}/${execution.phase}${input.effectiveQuota ? `, quota ${input.effectiveQuota.remaining}/${input.effectiveQuota.limit}` : ""}`,
453
- state: {
454
- service_execution_id: execution.service_execution_id,
455
- service_id: execution.service_id,
456
- status: execution.status,
457
- phase: execution.phase,
458
- current_capability_id: execution.current_capability_id,
459
- checkout_required: execution.checkout_required,
460
- next_action: execution.next_action,
461
- delivery: delivery
462
- ? {
463
- status: delivery.status,
464
- delivery_mode: deliveryMode || undefined,
465
- vault_artifact_id: delivery.vault_artifact_id,
466
- vault_status: delivery.vault_status,
467
- vault_payload_state: delivery.vault_payload_state,
468
- reveal_status: delivery.reveal_status,
469
- grant_status: delivery.grant_status ?? "missing",
470
- grant_expires_at: delivery.grant_expires_at,
471
- }
472
- : undefined,
473
- capabilities: capabilities.map((capability) => ({
474
- capability_id: capability.capability_id,
475
- agent_visible: capability.agent_visible,
476
- requires_payment: capability.requires_payment,
477
- vault_required: capability.vault_required,
478
- delivery_email_required: capability.delivery_email_required,
479
- delivery_email_purpose: capability.delivery_email_purpose,
480
- price_amount_minor: capability.price_amount_minor,
481
- price_currency: capability.price_currency,
482
- free_quota_limit: capability.free_quota_limit,
483
- })),
484
- result_items: (input.resultItems ?? []).map((item) => ({
485
- result_item_id: item.service_capability_result_item_id,
486
- rank: item.rank,
487
- display_title: item.display_title,
488
- safe_payload: item.safe_payload,
489
- })),
490
- effective_quota: input.effectiveQuota,
491
- },
492
- next_actions: nextActions,
493
- recovery,
494
- ...(input.resultItems?.length
495
- ? { visible_results: input.resultItems.map((item) => ({ rank: item.rank, title: item.display_title, safe_payload: item.safe_payload })) }
496
- : {}),
497
- };
498
- }
499
- function deliveryEmailGuidance(purpose) {
500
- switch (purpose) {
501
- case "receipt":
502
- return "Ask the human for their email. It is used to send the order receipt; never invent or substitute an address.";
503
- case "claim":
504
- return "Ask the human for their email. It is used to send the protected result claim link; never invent or substitute an address.";
505
- case "receipt_and_claim":
506
- return "Ask the human for their email. It is used to send the order receipt and protected result claim link; never invent or substitute an address.";
507
- default:
508
- return "Ask the human for the required email and state only the Backend-declared purpose; never invent or substitute an address.";
509
- }
510
- }
511
- function firstPrePurchaseCapability(capabilities) {
512
- return capabilities.find((capability) => capability.agent_visible && !capability.requires_payment)
513
- ?? capabilities.find((capability) => capability.agent_visible);
514
- }
515
- function firstPaidCapability(capabilities) {
516
- return (capabilities.find((capability) => capability.requires_payment && capability.vault_required) ??
517
- capabilities.find((capability) => capability.requires_payment));
518
- }
519
- function needsHumanSelection(execution, resultItem) {
520
- if (execution.phase !== "pre_purchase")
521
- return false;
522
- return execution.next_action === "select_candidate" || execution.next_action === "human_action_required" || resultItem !== undefined;
523
- }
524
- function latestServiceLine(cart) {
525
- return [...cart.items].reverse().find((item) => item.service_execution_id);
526
- }
527
- function lineID(line) {
528
- return line.cart_item_id ?? line.line_item_id ?? line.checkout_item_id ?? "<unknown_line>";
529
- }