@itpay/cli 2.0.2 → 2.0.5

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 (70) hide show
  1. package/README.md +92 -142
  2. package/dist/src/client/backend.js +27 -9
  3. package/dist/src/commands/buy.js +84 -132
  4. package/dist/src/commands/cart.js +274 -169
  5. package/dist/src/commands/catalog.js +63 -38
  6. package/dist/src/commands/checkout.js +117 -78
  7. package/dist/src/commands/docs.js +97 -51
  8. package/dist/src/commands/guidance.js +87 -15
  9. package/dist/src/commands/install.js +50 -87
  10. package/dist/src/commands/next.js +45 -0
  11. package/dist/src/commands/order.js +44 -69
  12. package/dist/src/commands/orders.js +43 -15
  13. package/dist/src/commands/pay.js +51 -22
  14. package/dist/src/commands/readyz.js +8 -4
  15. package/dist/src/commands/refund.js +132 -14
  16. package/dist/src/commands/services.js +719 -148
  17. package/dist/src/main.js +711 -201
  18. package/dist/src/render/output.js +2 -3
  19. package/dist/src/state/cart_session.js +13 -17
  20. package/dist/src/state/client_context.js +4 -2
  21. package/dist/src/state/config.js +3 -5
  22. package/dist/src/state/device_authority.js +1 -1
  23. package/docs/agent/buyer/cart-checkout.json +27 -83
  24. package/docs/agent/buyer/install-and-setup.json +23 -67
  25. package/docs/agent/buyer/orders-refunds.json +31 -53
  26. package/docs/agent/buyer/payment-flow.json +24 -57
  27. package/docs/agent/buyer/quickstart.json +39 -162
  28. package/docs/agent/buyer/render-hosts.json +43 -57
  29. package/docs/cli-reference/agent-types.md +45 -0
  30. package/docs/cli-reference/commands/buy.md +167 -0
  31. package/docs/cli-reference/commands/cart/add.md +86 -0
  32. package/docs/cli-reference/commands/cart/clear.md +53 -0
  33. package/docs/cli-reference/commands/cart/index.md +30 -0
  34. package/docs/cli-reference/commands/cart/next.md +71 -0
  35. package/docs/cli-reference/commands/cart/remove.md +53 -0
  36. package/docs/cli-reference/commands/cart/show.md +65 -0
  37. package/docs/cli-reference/commands/catalog/index.md +26 -0
  38. package/docs/cli-reference/commands/catalog/list.md +45 -0
  39. package/docs/cli-reference/commands/checkout.md +74 -0
  40. package/docs/cli-reference/commands/docs/index.md +28 -0
  41. package/docs/cli-reference/commands/docs/list.md +51 -0
  42. package/docs/cli-reference/commands/docs/search.md +69 -0
  43. package/docs/cli-reference/commands/docs/show.md +68 -0
  44. package/docs/cli-reference/commands/install.md +112 -0
  45. package/docs/cli-reference/commands/next.md +87 -0
  46. package/docs/cli-reference/commands/order.md +92 -0
  47. package/docs/cli-reference/commands/orders.md +83 -0
  48. package/docs/cli-reference/commands/pay.md +103 -0
  49. package/docs/cli-reference/commands/readyz.md +39 -0
  50. package/docs/cli-reference/commands/refund/cancel.md +62 -0
  51. package/docs/cli-reference/commands/refund/create.md +85 -0
  52. package/docs/cli-reference/commands/refund/get.md +60 -0
  53. package/docs/cli-reference/commands/refund/index.md +33 -0
  54. package/docs/cli-reference/commands/refund/list.md +68 -0
  55. package/docs/cli-reference/commands/refund/watch.md +73 -0
  56. package/docs/cli-reference/commands/services/action.md +43 -0
  57. package/docs/cli-reference/commands/services/checkout.md +82 -0
  58. package/docs/cli-reference/commands/services/events.md +73 -0
  59. package/docs/cli-reference/commands/services/get.md +66 -0
  60. package/docs/cli-reference/commands/services/index.md +45 -0
  61. package/docs/cli-reference/commands/services/invoke.md +67 -0
  62. package/docs/cli-reference/commands/services/list.md +61 -0
  63. package/docs/cli-reference/commands/services/next.md +162 -0
  64. package/docs/cli-reference/commands/services/quote.md +59 -0
  65. package/docs/cli-reference/commands/services/read-result.md +98 -0
  66. package/docs/cli-reference/commands/services/start.md +53 -0
  67. package/docs/cli-reference/conventions.md +94 -0
  68. package/docs/cli-reference/index.md +64 -0
  69. package/package.json +1 -1
  70. package/skills/itpay-buyer/SKILL.md +47 -113
@@ -1,97 +1,136 @@
1
- // Reads the canonical V3 checkout presentation. Requires both checkout_id
2
- // and the checkout-scoped display_token. Supports terminal and agent markdown.
3
- import { formatMoney } from "../render/output.js";
4
- import { hintFor } from "../render/status.js";
5
- import { resolveOutput } from "../render/sink.js";
6
- import { ensureIdeImageAttach, ideImageAttachBlock } from "../render/ide.js";
1
+ // Reads one canonical Checkout presentation. This command never creates a
2
+ // Checkout and only prepares a payment handoff while the Checkout is pending.
3
+ import { ensureIdeImageAttach } from "../render/ide.js";
4
+ import { buildAgentChatHandoff } from "../render/markdown.js";
5
+ import { platformKeyForHost } from "../render/plan.js";
6
+ import { renderTerminalQR } from "../render/qr.js";
7
7
  import { DEFAULT_BASE_URL } from "../state/config.js";
8
+ import { buildCheckoutQRPlan } from "./buy.js";
9
+ import { writeCommandEnvelope } from "./guidance.js";
8
10
  export async function runCheckoutPresentation(backend, options) {
9
- const out = resolveOutput(options.output);
10
11
  const presentation = await backend.getCheckoutPresentation(options.checkoutID, options.displayToken);
12
+ const host = options.host ?? "terminal";
13
+ if (!checkoutNeedsHumanHandoff(presentation.checkout.status)) {
14
+ const envelope = terminalCheckoutEnvelope(presentation);
15
+ writeCommandEnvelope(envelope, {
16
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
17
+ ...(options.output ? { output: options.output } : {}),
18
+ plainResult: checkoutPlainResult(envelope.result),
19
+ });
20
+ return;
21
+ }
11
22
  const checkoutURL = checkoutPageURL(options.baseURL, options.checkoutID, options.displayToken);
12
23
  const qrPNGURL = presentation.qr_png_url ?? checkoutQRPNGURL(options.baseURL, options.checkoutID, options.displayToken);
13
- const plan = {
14
- kind: "checkout_qr",
15
- host: (options.host ?? "terminal"),
16
- summary: "checkout presentation",
17
- url: checkoutURL,
18
- preferredQRSources: [qrPNGURL],
19
- platform: {
20
- text: "checkout presentation",
21
- links: [{ label: "打开付款页面", url: checkoutURL }],
22
- buttons: [],
23
- blocks: [],
24
- },
25
- };
24
+ const nextCommand = `itpay checkout --id ${options.checkoutID} --token ${options.displayToken}`;
25
+ const plan = buildCheckoutQRPlan({
26
+ host,
27
+ checkoutID: options.checkoutID,
28
+ checkoutURL,
29
+ displayToken: options.displayToken,
30
+ qrPayload: checkoutURL,
31
+ qrPNGURL,
32
+ nextAction: presentation.checkout.next_action,
33
+ orderItems: presentation.items.map((item) => ({
34
+ title: item.title,
35
+ quantity: item.quantity,
36
+ amountMinor: item.amount_minor,
37
+ currency: item.currency,
38
+ })),
39
+ orderCurrency: presentation.checkout.currency,
40
+ });
26
41
  await ensureIdeImageAttach(plan, {
27
42
  ...(options.baseURL ? { baseURL: options.baseURL } : {}),
28
43
  });
29
- if (options.host === "codex" || options.host === "claude-code" || options.host === "trae") {
30
- out(renderCheckoutMarkdown(presentation, plan) + "\n");
31
- }
32
- else {
33
- out(renderCheckoutText(presentation) + "\n");
34
- if (plan.ideImageAttach) {
35
- out(ideImageAttachBlock(plan.ideImageAttach).filter((l) => l.length > 0).join("\n") + "\n");
36
- }
37
- out(`hint: ${hintFor("checkout", presentation.checkout.status)}\n`);
44
+ const envelope = pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand);
45
+ const plainResult = checkoutPlainResult(envelope.result);
46
+ if (!options.jsonOutput && platformKeyForHost(host) === "terminal") {
47
+ plainResult.push("qr:", await renderTerminalQR(checkoutURL, "terminal"));
38
48
  }
49
+ writeCommandEnvelope(envelope, {
50
+ ...(options.jsonOutput !== undefined ? { jsonOutput: options.jsonOutput } : {}),
51
+ ...(options.output ? { output: options.output } : {}),
52
+ plainResult,
53
+ });
39
54
  }
40
- function renderCheckoutText(presentation) {
41
- const lines = [];
42
- const c = presentation.checkout;
43
- lines.push(`checkout ${c.checkout_id}`);
44
- lines.push(` status: ${c.status}`);
45
- lines.push(` next_action: ${c.next_action}`);
46
- lines.push(` amount: ${formatMoney(c.amount_minor, c.currency)}`);
47
- lines.push(` buyer: ${presentation.buyer_session.state}`);
48
- if (presentation.items.length > 0) {
49
- lines.push(" items:");
50
- for (const item of presentation.items) {
51
- lines.push(` - ${item.title} × ${item.quantity} (${formatMoney(item.amount_minor, item.currency)})`);
52
- }
55
+ function pendingCheckoutEnvelope(presentation, checkoutURL, plan, nextCommand) {
56
+ const platform = platformKeyForHost(plan.host);
57
+ const handoff = { url: checkoutURL };
58
+ if (plan.ideImageAttach?.status === "downloaded" && plan.ideImageAttach.localPath) {
59
+ handoff.qr_local_path = plan.ideImageAttach.localPath;
53
60
  }
54
- if (presentation.payment_intents.length > 0) {
55
- lines.push(" payment_intents:");
56
- for (const intent of presentation.payment_intents) {
57
- lines.push(` - ${intent.payment_intent_id} ${intent.status} (${intent.payment_method_type}, ${formatMoney(intent.amount_minor, intent.currency)})`);
58
- }
61
+ if (platform === "markdown") {
62
+ handoff.markdown = buildAgentChatHandoff(plan).markdown;
63
+ }
64
+ else if (platform === "plain_chat" && presentation.qr_png_url) {
65
+ handoff.qr_image_url = presentation.qr_png_url;
59
66
  }
60
- return lines.join("\n");
67
+ return {
68
+ status: "human_checkout_required",
69
+ result: {
70
+ checkout_id: presentation.checkout.checkout_id,
71
+ payment: "pending",
72
+ amount: formatMoney(presentation.checkout.amount_minor, presentation.checkout.currency),
73
+ },
74
+ handoff,
75
+ instruction: pendingInstruction(platform),
76
+ next: { command: nextCommand, reason: "稍后查询同一笔 Checkout 状态" },
77
+ recovery: [],
78
+ };
61
79
  }
62
- function renderCheckoutMarkdown(presentation, plan) {
63
- const c = presentation.checkout;
64
- const lines = [];
65
- lines.push(`## :mag: Checkout ${c.checkout_id}`);
66
- lines.push("");
67
- lines.push(`| 字段 | 值 |`);
68
- lines.push(`|------|-----|`);
69
- lines.push(`| 状态 | ${c.status} |`);
70
- lines.push(`| 操作 | ${c.next_action} |`);
71
- lines.push(`| 金额 | ${formatMoney(c.amount_minor, c.currency)} |`);
72
- lines.push(`| 买家 | ${presentation.buyer_session.state} |`);
73
- lines.push("");
74
- if (presentation.items.length > 0) {
75
- lines.push(`| 项目 | 数量 | 单价 |`);
76
- lines.push(`|------|:----:|------|`);
77
- for (const item of presentation.items) {
78
- lines.push(`| ${item.title} | ${item.quantity} | ${formatMoney(item.amount_minor, item.currency)} |`);
79
- }
80
- lines.push("");
80
+ function terminalCheckoutEnvelope(presentation) {
81
+ const checkout = presentation.checkout;
82
+ const serviceExecutionIDs = [...new Set(presentation.items.map((item) => item.service_execution_id).filter((id) => Boolean(id)))];
83
+ const payment = checkout.status === "refunded" ? "refunded"
84
+ : checkout.status === "payment_succeeded" || checkout.status === "completed" ? "verified"
85
+ : checkout.status;
86
+ const result = {
87
+ checkout_id: checkout.checkout_id,
88
+ payment,
89
+ ...(presentation.completed_order_id ? { order_id: presentation.completed_order_id } : {}),
90
+ ...(serviceExecutionIDs.length === 1 ? { service_execution_id: serviceExecutionIDs[0] } : {}),
91
+ ...(serviceExecutionIDs.length > 1 ? { service_execution_ids: serviceExecutionIDs } : {}),
92
+ };
93
+ let status = checkout.status;
94
+ let instruction = "Checkout 已结束;不要再次展示付款二维码。";
95
+ let next = null;
96
+ const recovery = [];
97
+ if (payment === "verified") {
98
+ status = "completed";
99
+ instruction = "付款已确认,不要再次展示付款二维码。";
100
+ next = serviceExecutionIDs.length === 1
101
+ ? { command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "读取履约状态" }
102
+ : presentation.completed_order_id
103
+ ? { command: `itpay order ${presentation.completed_order_id}`, reason: "读取已创建订单" }
104
+ : { command: "itpay orders", reason: "恢复已付款订单" };
81
105
  }
82
- if (presentation.payment_intents.length > 0) {
83
- lines.push(`### :credit_card: 支付`);
84
- lines.push("");
85
- for (const intent of presentation.payment_intents) {
86
- lines.push(`- \`${intent.payment_intent_id}\` — ${intent.payment_method_type} — ${intent.status} — ${formatMoney(intent.amount_minor, intent.currency)}`);
87
- }
88
- lines.push("");
106
+ else if (checkout.status === "refunded") {
107
+ instruction = "该 Checkout 已退款,不要再次付款或展示二维码。";
108
+ if (presentation.completed_order_id)
109
+ next = { command: `itpay order ${presentation.completed_order_id}`, reason: "读取订单与退款状态" };
89
110
  }
90
- if (plan.ideImageAttach) {
91
- lines.push(...ideImageAttachBlock(plan.ideImageAttach));
111
+ else if (checkout.status === "failed" || checkout.status === "expired") {
112
+ instruction = "该 Checkout 已失效;不要继续使用当前付款入口。";
113
+ if (serviceExecutionIDs.length === 1) {
114
+ recovery.push({ command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "由服务端决定是否可恢复 Checkout" });
115
+ }
92
116
  }
93
- lines.push(`> :bulb: ${hintFor("checkout", c.status)}`);
94
- return lines.join("\n");
117
+ return { status, result, instruction, next, recovery };
118
+ }
119
+ function checkoutNeedsHumanHandoff(status) {
120
+ return !new Set(["payment_succeeded", "completed", "failed", "expired", "refunded"]).has(status);
121
+ }
122
+ function checkoutPlainResult(result) {
123
+ return Object.entries(result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
124
+ }
125
+ function pendingInstruction(platform) {
126
+ if (platform === "markdown")
127
+ return "把 handoff.markdown 原样发送到当前桌面对话;二维码和链接可见后等待用户操作,不要创建新 Checkout。";
128
+ if (platform === "terminal")
129
+ return "在用户可见终端展示二维码和付款链接,然后等待用户操作;不要创建新 Checkout。";
130
+ return "把付款链接和可用二维码附件发送给用户,然后等待用户操作;不要创建新 Checkout。";
131
+ }
132
+ function formatMoney(amountMinor, currency) {
133
+ return `${(amountMinor / 100).toFixed(2)} ${currency}`;
95
134
  }
96
135
  function checkoutPageURL(baseURL, checkoutID, displayToken) {
97
136
  const root = publicRoot(baseURL);
@@ -1,61 +1,107 @@
1
- import { readFileSync, readdirSync, existsSync } from "node:fs";
2
- import { resolve, dirname } from "node:path";
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- const __dirname = dirname(fileURLToPath(import.meta.url));
5
- function findDocsDir() {
6
- if (process.env.ITPAY_CLI_DOCS_DIR) {
7
- return process.env.ITPAY_CLI_DOCS_DIR;
8
- }
9
- // dist/src/commands → ../../../docs/agent/buyer = <pkg>/docs/agent/buyer
10
- const pkgPath = resolve(__dirname, "..", "..", "..", "docs", "agent", "buyer");
11
- if (existsSync(pkgPath))
12
- return pkgPath;
13
- // src/commands → ../../docs/agent/buyer = <pkg>/docs/agent/buyer (dev mode)
14
- const devPath = resolve(__dirname, "..", "..", "docs", "agent", "buyer");
15
- if (existsSync(devPath))
16
- return devPath;
17
- return pkgPath;
18
- }
19
- const DOCS_DIR = findDocsDir();
20
- function loadDocs() {
21
- const files = readdirSync(DOCS_DIR).filter((f) => f.endsWith(".json"));
22
- return files.map((file) => {
23
- const raw = readFileSync(resolve(DOCS_DIR, file), "utf-8");
24
- return JSON.parse(raw);
4
+ import { resolveOutput } from "../render/sink.js";
5
+ import { CommandContractError, writeCommandEnvelope } from "./guidance.js";
6
+ const commandDir = dirname(fileURLToPath(import.meta.url));
7
+ export function runDocsList(options = {}) {
8
+ const topics = loadDocs().map(({ topic, title, purpose }) => ({ topic, title, purpose }));
9
+ writeCommandEnvelope({
10
+ status: "listed",
11
+ result: { topics },
12
+ instruction: "选择与当前步骤最接近的一个 topic;不要一次加载全部文档。",
13
+ next: null,
14
+ recovery: [],
15
+ }, {
16
+ ...options,
17
+ plainResult: topics.flatMap((doc) => [`${doc.topic}: ${doc.title}`, ` ${doc.purpose}`]),
25
18
  });
26
19
  }
27
- export function runDocsList() {
28
- const docs = loadDocs();
29
- process.stdout.write(`Agent docs (${docs.length} topics):\n\n`);
30
- for (const doc of docs) {
31
- process.stdout.write(` ${doc.topic}\n`);
32
- process.stdout.write(` title: ${doc.title}\n`);
33
- process.stdout.write(` purpose: ${doc.purpose}\n\n`);
34
- }
35
- }
36
- export function runDocsShow(topic) {
37
- const docs = loadDocs();
38
- const doc = docs.find((d) => d.topic === topic);
20
+ export function runDocsShow(topic, options = {}) {
21
+ const normalized = topic.trim();
22
+ const doc = loadDocs().find((candidate) => candidate.topic === normalized);
39
23
  if (!doc) {
40
- process.stderr.write(`doc topic "${topic}" not found. Use "itpay docs list" to see available topics.\n`);
41
- process.exitCode = 1;
24
+ throw new CommandContractError("doc_not_found", `doc topic not found: ${topic}`, "使用稳定 topic 名称;不要根据标题猜 topic。", [
25
+ { command: "itpay docs list --json", reason: "列出全部 topic" },
26
+ { command: `itpay docs search ${shellWord(normalized || "topic")} --json`, reason: "按关键词重新搜索" },
27
+ ]);
28
+ }
29
+ const envelope = {
30
+ status: "shown",
31
+ result: { topic: doc.topic, content: doc },
32
+ instruction: "只执行文档中与当前服务端状态匹配的步骤;服务端返回的当前 next 优先。",
33
+ next: null,
34
+ recovery: [],
35
+ };
36
+ if (options.jsonOutput) {
37
+ writeCommandEnvelope(envelope, options);
42
38
  return;
43
39
  }
44
- process.stdout.write(JSON.stringify(doc, null, 2) + "\n");
45
- }
46
- export function runDocsSearch(query) {
47
- const docs = loadDocs();
48
- const lower = query.toLowerCase();
49
- const results = docs.filter((doc) => {
50
- const text = [doc.topic, doc.title, doc.purpose, ...(doc.search_terms ?? [])].join(" ").toLowerCase();
51
- return text.includes(lower);
52
- });
53
- if (results.length === 0) {
54
- process.stdout.write(`no docs match "${query}"\n`);
40
+ const out = resolveOutput(options.output);
41
+ out("shown\n");
42
+ out(`${JSON.stringify(doc, null, 2)}\n`);
43
+ out(`instruction: ${envelope.instruction}\n`);
44
+ }
45
+ export function runDocsSearch(query, options = {}) {
46
+ const normalized = query.trim().toLowerCase();
47
+ if (!normalized) {
48
+ throw new CommandContractError("docs_query_required", "docs search query must not be empty", "提供一个 topic、标题、用途或 search term 关键词。", [{ command: "itpay docs list --json", reason: "不确定关键词时列出 topic" }]);
49
+ }
50
+ const topics = loadDocs()
51
+ .filter((doc) => searchableText(doc).includes(normalized))
52
+ .map(({ topic, title, purpose }) => ({ topic, title, purpose }));
53
+ if (topics.length === 0) {
54
+ writeCommandEnvelope({
55
+ status: "no_match",
56
+ result: { query, topics: [] },
57
+ instruction: "没有匹配文档;缩短关键词,或列出全部 topic。",
58
+ next: { command: "itpay docs list --json", reason: "浏览稳定 topic" },
59
+ recovery: [],
60
+ }, options);
55
61
  return;
56
62
  }
57
- process.stdout.write(`${results.length} matching docs:\n\n`);
58
- for (const doc of results) {
59
- process.stdout.write(` ${doc.topic} ${doc.title}\n`);
63
+ writeCommandEnvelope({
64
+ status: "matched",
65
+ result: { query, topics },
66
+ instruction: topics.length === 1
67
+ ? "已唯一匹配;读取该 topic。"
68
+ : "选择最相关的一个 topic;不要同时展开全部结果。",
69
+ next: topics.length === 1
70
+ ? { command: `itpay docs show ${topics[0].topic} --json`, reason: "读取唯一匹配文档" }
71
+ : null,
72
+ recovery: [],
73
+ }, {
74
+ ...options,
75
+ plainResult: topics.map((doc) => `${doc.topic}: ${doc.title}`),
76
+ });
77
+ }
78
+ function loadDocs() {
79
+ const docsDir = findDocsDir();
80
+ const files = readdirSync(docsDir).filter((file) => file.endsWith(".json")).sort();
81
+ return files.map((file) => parseDoc(readFileSync(resolve(docsDir, file), "utf8"), file))
82
+ .sort((left, right) => left.topic.localeCompare(right.topic));
83
+ }
84
+ function findDocsDir() {
85
+ if (process.env.ITPAY_CLI_DOCS_DIR)
86
+ return resolve(process.env.ITPAY_CLI_DOCS_DIR);
87
+ const packagePath = resolve(commandDir, "..", "..", "..", "docs", "agent", "buyer");
88
+ if (existsSync(packagePath))
89
+ return packagePath;
90
+ return resolve(commandDir, "..", "..", "docs", "agent", "buyer");
91
+ }
92
+ function parseDoc(raw, file) {
93
+ const value = JSON.parse(raw);
94
+ if (typeof value.schema_version !== "string" ||
95
+ typeof value.topic !== "string" ||
96
+ typeof value.title !== "string" ||
97
+ typeof value.purpose !== "string") {
98
+ throw new Error(`invalid agent doc: ${file}`);
60
99
  }
100
+ return value;
101
+ }
102
+ function searchableText(doc) {
103
+ return [doc.topic, doc.title, doc.purpose, ...(doc.search_terms ?? [])].join(" ").toLowerCase();
104
+ }
105
+ function shellWord(value) {
106
+ return /^[a-zA-Z0-9._-]+$/.test(value) ? value : JSON.stringify(value);
61
107
  }
@@ -1,5 +1,53 @@
1
1
  import { HttpError } from "../client/http.js";
2
2
  import { resolveOutput } from "../render/sink.js";
3
+ export function isTerminalServiceExecutionStatus(status) {
4
+ return status === "failed" || status === "refunded" || status === "cancelled";
5
+ }
6
+ export class CommandContractError extends Error {
7
+ code;
8
+ instruction;
9
+ recovery;
10
+ constructor(code, message, instruction, recovery) {
11
+ super(message);
12
+ this.code = code;
13
+ this.instruction = instruction;
14
+ this.recovery = recovery;
15
+ this.name = "CommandContractError";
16
+ }
17
+ }
18
+ export function writeCommandEnvelope(value, options = {}) {
19
+ const out = resolveOutput(options.output);
20
+ if (options.jsonOutput) {
21
+ out(JSON.stringify(value, null, 2) + "\n");
22
+ return;
23
+ }
24
+ out(`${value.status}\n`);
25
+ const facts = "result" in value ? value.result : value.error;
26
+ if (options.plainResult) {
27
+ for (const line of options.plainResult)
28
+ out(`${line}\n`);
29
+ }
30
+ else {
31
+ for (const [key, fact] of Object.entries(facts)) {
32
+ out(`${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
33
+ }
34
+ }
35
+ if ("handoff" in value && value.handoff) {
36
+ for (const [key, fact] of Object.entries(value.handoff)) {
37
+ out(`handoff.${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
38
+ }
39
+ }
40
+ out(`instruction: ${value.instruction}\n`);
41
+ if (value.next)
42
+ out(`next: ${value.next.command}\n`);
43
+ if (value.recovery.length > 0) {
44
+ out("recovery:\n");
45
+ for (const action of value.recovery) {
46
+ out(` - ${action.command}\n`);
47
+ out(` reason: ${action.reason}\n`);
48
+ }
49
+ }
50
+ }
3
51
  export function attachAgentGuidance(payload, guidance) {
4
52
  return {
5
53
  ...payload,
@@ -11,8 +59,14 @@ export function printAgentGuidance(guidance, output) {
11
59
  out(`${guidance.summary}\n`);
12
60
  if (guidance.visible_results?.length) {
13
61
  out("results:\n");
14
- for (const item of guidance.visible_results)
62
+ for (const item of guidance.visible_results) {
15
63
  out(` ${item.rank}. ${item.title}\n`);
64
+ for (const [key, value] of Object.entries(item.safe_payload)) {
65
+ if (key === "name" || key === "company_name" || value === "" || value === undefined || value === null)
66
+ continue;
67
+ out(` ${key}: ${String(value)}\n`);
68
+ }
69
+ }
16
70
  }
17
71
  if (guidance.next_actions.length === 0) {
18
72
  out("next actions: none\n");
@@ -126,7 +180,6 @@ export function buildServiceActionGuidance(action) {
126
180
  action_type: action.action_type,
127
181
  status: action.status,
128
182
  result_item_id: action.result_item_id,
129
- selected_candidate_hash: action.selected_candidate_hash,
130
183
  },
131
184
  next_actions: [
132
185
  {
@@ -172,12 +225,12 @@ export function buildServiceHandleGuidance(serviceExecutionID, checkoutCapabilit
172
225
  export function errorRecoveryActions(error) {
173
226
  if (!(error instanceof HttpError))
174
227
  return [];
175
- if (error.code === "agent_identity_required") {
228
+ if (error.code === "agent_identity_required" || error.code === "agent_device_session_required") {
176
229
  return [
177
230
  {
178
- id: "set_agent_identity",
179
- label: "Set a stable agent device id",
180
- command: "export ITPAY_AGENT_DEVICE_ID=<stable_agent_device_id>",
231
+ id: "inspect_agent_setup",
232
+ label: "Inspect supported Agent Type setup",
233
+ command: "itpay install --json",
181
234
  },
182
235
  ];
183
236
  }
@@ -253,6 +306,7 @@ function buildServiceGuidance(input) {
253
306
  const resultItem = input.resultItems?.[0];
254
307
  const checkoutID = input.checkoutBindings?.at(-1)?.checkout_id;
255
308
  const delivery = input.deliveryBindings?.[0];
309
+ const deliveryMode = String(delivery?.redacted_summary?.delivery_mode ?? (delivery?.vault_artifact_id ? "vault_artifact" : ""));
256
310
  const backendCheckout = input.backendNextActions?.find((action) => action.kind === "create_checkout");
257
311
  const nextActions = [];
258
312
  const recovery = [
@@ -262,7 +316,18 @@ function buildServiceGuidance(input) {
262
316
  command: `itpay services get ${execution.service_execution_id}`,
263
317
  },
264
318
  ];
265
- if (execution.status === "completed" || execution.next_action === "completed") {
319
+ if (isTerminalServiceExecutionStatus(execution.status)) {
320
+ // Terminal executions are inspectable, but no command may advance or replay them.
321
+ }
322
+ else if (deliveryMode === "agent_visible_result" && (input.resultItems?.length ?? 0) > 0) {
323
+ nextActions.push({
324
+ id: "use_agent_visible_result",
325
+ label: "Use the safe candidate list shown above",
326
+ command: `itpay services next ${execution.service_execution_id} --json`,
327
+ 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.",
328
+ });
329
+ }
330
+ else if (execution.status === "completed" || execution.next_action === "completed") {
266
331
  nextActions.push({
267
332
  id: "inspect_order_or_grant",
268
333
  label: "Inspect order, claim, or grant from the checkout/order owner",
@@ -327,13 +392,13 @@ function buildServiceGuidance(input) {
327
392
  });
328
393
  }
329
394
  }
330
- else if (input.providerCalled && (input.resultItems?.length ?? 0) === 0) {
395
+ else if ((input.providerCalled || execution.next_action === "select_candidate") && (input.resultItems?.length ?? 0) === 0) {
331
396
  nextActions.push({
332
- id: "refine_search",
333
- label: "Ask for a more specific company name before another lookup",
334
- command: `itpay services invoke ${execution.service_execution_id} --capability ${execution.current_capability_id ?? "<capability_id>"} --input keyword=<more_specific_company_name>`,
397
+ id: "start_refined_search",
398
+ label: "Start a new execution with a more specific company name",
399
+ command: `itpay services start ${execution.service_id}`,
335
400
  requires_human: true,
336
- reason: "The provider returned no candidates. Do not repeat the same query.",
401
+ reason: "No candidates were found. This execution is finished; use one new execution per new keyword.",
337
402
  });
338
403
  }
339
404
  else if (needsHumanSelection(execution, resultItem)) {
@@ -344,6 +409,12 @@ function buildServiceGuidance(input) {
344
409
  requires_human: true,
345
410
  reason: "Do not choose a candidate without explicit human confirmation.",
346
411
  });
412
+ nextActions.push({
413
+ id: "start_another_search",
414
+ label: "Search another company in a new execution",
415
+ command: `itpay services start ${execution.service_id}`,
416
+ reason: "This execution has completed its one keyword lookup; do not reuse it for another keyword.",
417
+ });
347
418
  }
348
419
  else if (prePurchase) {
349
420
  const action = {
@@ -352,7 +423,7 @@ function buildServiceGuidance(input) {
352
423
  command: `itpay services invoke ${execution.service_execution_id} --capability ${prePurchase.capability_id} --input key=value --json`,
353
424
  };
354
425
  if (prePurchase.free_quota_limit) {
355
- action.reason = `Free quota limit: ${prePurchase.free_quota_limit} per ${prePurchase.quota_subject || "subject"}.`;
426
+ action.reason = `One keyword per execution. Free quota limit: ${prePurchase.free_quota_limit} per ${prePurchase.quota_subject || "subject"}.`;
356
427
  }
357
428
  nextActions.push(action);
358
429
  }
@@ -377,6 +448,7 @@ function buildServiceGuidance(input) {
377
448
  delivery: delivery
378
449
  ? {
379
450
  status: delivery.status,
451
+ delivery_mode: deliveryMode || undefined,
380
452
  vault_artifact_id: delivery.vault_artifact_id,
381
453
  vault_status: delivery.vault_status,
382
454
  vault_payload_state: delivery.vault_payload_state,
@@ -397,16 +469,16 @@ function buildServiceGuidance(input) {
397
469
  })),
398
470
  result_items: (input.resultItems ?? []).map((item) => ({
399
471
  result_item_id: item.service_capability_result_item_id,
400
- stable_hash: item.stable_hash,
401
472
  rank: item.rank,
402
473
  display_title: item.display_title,
474
+ safe_payload: item.safe_payload,
403
475
  })),
404
476
  effective_quota: input.effectiveQuota,
405
477
  },
406
478
  next_actions: nextActions,
407
479
  recovery,
408
480
  ...(input.resultItems?.length
409
- ? { visible_results: input.resultItems.map((item) => ({ rank: item.rank, title: item.display_title })) }
481
+ ? { visible_results: input.resultItems.map((item) => ({ rank: item.rank, title: item.display_title, safe_payload: item.safe_payload })) }
410
482
  : {}),
411
483
  };
412
484
  }