@itpay/cli 2.0.3 → 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 (77) hide show
  1. package/README.md +96 -142
  2. package/dist/src/client/backend.js +26 -8
  3. package/dist/src/client/http.js +29 -23
  4. package/dist/src/commands/buy.js +84 -132
  5. package/dist/src/commands/cart.js +274 -169
  6. package/dist/src/commands/catalog.js +64 -38
  7. package/dist/src/commands/checkout.js +128 -79
  8. package/dist/src/commands/docs.js +97 -51
  9. package/dist/src/commands/guidance.js +112 -16
  10. package/dist/src/commands/install.js +50 -87
  11. package/dist/src/commands/next.js +45 -0
  12. package/dist/src/commands/order.js +44 -69
  13. package/dist/src/commands/orders.js +43 -15
  14. package/dist/src/commands/pay.js +51 -22
  15. package/dist/src/commands/readyz.js +8 -4
  16. package/dist/src/commands/refund.js +132 -11
  17. package/dist/src/commands/services.js +799 -147
  18. package/dist/src/commands/skill.js +55 -0
  19. package/dist/src/main.js +820 -193
  20. package/dist/src/render/output.js +2 -3
  21. package/dist/src/state/agent_type.js +19 -0
  22. package/dist/src/state/cart_session.js +13 -17
  23. package/dist/src/state/client_context.js +4 -2
  24. package/dist/src/state/config.js +5 -15
  25. package/dist/src/state/device_authority.js +175 -57
  26. package/docs/agent/buyer/cart-checkout.json +27 -83
  27. package/docs/agent/buyer/catalog-list.json +2 -1
  28. package/docs/agent/buyer/identity-and-sessions.json +64 -0
  29. package/docs/agent/buyer/install-and-setup.json +35 -65
  30. package/docs/agent/buyer/orders-refunds.json +31 -53
  31. package/docs/agent/buyer/payment-flow.json +28 -57
  32. package/docs/agent/buyer/quickstart.json +46 -161
  33. package/docs/agent/buyer/render-hosts.json +43 -57
  34. package/docs/cli-reference/agent-types.md +51 -0
  35. package/docs/cli-reference/commands/buy.md +167 -0
  36. package/docs/cli-reference/commands/cart/add.md +86 -0
  37. package/docs/cli-reference/commands/cart/clear.md +53 -0
  38. package/docs/cli-reference/commands/cart/index.md +30 -0
  39. package/docs/cli-reference/commands/cart/next.md +71 -0
  40. package/docs/cli-reference/commands/cart/remove.md +53 -0
  41. package/docs/cli-reference/commands/cart/show.md +65 -0
  42. package/docs/cli-reference/commands/catalog/index.md +26 -0
  43. package/docs/cli-reference/commands/catalog/list.md +45 -0
  44. package/docs/cli-reference/commands/checkout.md +74 -0
  45. package/docs/cli-reference/commands/device.md +13 -0
  46. package/docs/cli-reference/commands/docs/index.md +28 -0
  47. package/docs/cli-reference/commands/docs/list.md +51 -0
  48. package/docs/cli-reference/commands/docs/search.md +69 -0
  49. package/docs/cli-reference/commands/docs/show.md +68 -0
  50. package/docs/cli-reference/commands/install.md +114 -0
  51. package/docs/cli-reference/commands/next.md +87 -0
  52. package/docs/cli-reference/commands/order.md +92 -0
  53. package/docs/cli-reference/commands/orders.md +83 -0
  54. package/docs/cli-reference/commands/pay.md +103 -0
  55. package/docs/cli-reference/commands/readyz.md +38 -0
  56. package/docs/cli-reference/commands/refund/cancel.md +62 -0
  57. package/docs/cli-reference/commands/refund/create.md +85 -0
  58. package/docs/cli-reference/commands/refund/get.md +60 -0
  59. package/docs/cli-reference/commands/refund/index.md +33 -0
  60. package/docs/cli-reference/commands/refund/list.md +68 -0
  61. package/docs/cli-reference/commands/refund/watch.md +73 -0
  62. package/docs/cli-reference/commands/services/action.md +48 -0
  63. package/docs/cli-reference/commands/services/checkout.md +82 -0
  64. package/docs/cli-reference/commands/services/events.md +73 -0
  65. package/docs/cli-reference/commands/services/get.md +66 -0
  66. package/docs/cli-reference/commands/services/index.md +45 -0
  67. package/docs/cli-reference/commands/services/invoke.md +67 -0
  68. package/docs/cli-reference/commands/services/list.md +61 -0
  69. package/docs/cli-reference/commands/services/next.md +181 -0
  70. package/docs/cli-reference/commands/services/quote.md +63 -0
  71. package/docs/cli-reference/commands/services/read-result.md +98 -0
  72. package/docs/cli-reference/commands/services/start.md +55 -0
  73. package/docs/cli-reference/commands/skill.md +17 -0
  74. package/docs/cli-reference/conventions.md +97 -0
  75. package/docs/cli-reference/index.md +65 -0
  76. package/package.json +1 -1
  77. package/skills/itpay-buyer/SKILL.md +71 -110
@@ -1,97 +1,137 @@
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
- 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
- };
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
+ 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;
59
63
  }
60
- return lines.join("\n");
64
+ else if (platform === "plain_chat" && plan.preferredQRSources[0]) {
65
+ handoff.qr_image_url = plan.preferredQRSources[0];
66
+ }
67
+ const amount = formatMoney(presentation.checkout.amount_minor, presentation.checkout.currency);
68
+ return {
69
+ status: "human_checkout_required",
70
+ result: {
71
+ checkout_id: presentation.checkout.checkout_id,
72
+ payment: "pending",
73
+ amount,
74
+ },
75
+ handoff,
76
+ instruction: pendingInstruction(platform, amount),
77
+ next: { command: nextCommand, reason: "稍后只查询同一 Checkout" },
78
+ recovery: [],
79
+ };
61
80
  }
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("");
81
+ function terminalCheckoutEnvelope(presentation) {
82
+ const checkout = presentation.checkout;
83
+ const serviceExecutionIDs = [...new Set(presentation.items.map((item) => item.service_execution_id).filter((id) => Boolean(id)))];
84
+ const payment = checkout.status === "refunded" ? "refunded"
85
+ : checkout.status === "payment_succeeded" || checkout.status === "completed" ? "verified"
86
+ : checkout.status;
87
+ const result = {
88
+ checkout_id: checkout.checkout_id,
89
+ payment,
90
+ ...(presentation.completed_order_id ? { order_id: presentation.completed_order_id } : {}),
91
+ ...(serviceExecutionIDs.length === 1 ? { service_execution_id: serviceExecutionIDs[0] } : {}),
92
+ ...(serviceExecutionIDs.length > 1 ? { service_execution_ids: serviceExecutionIDs } : {}),
93
+ };
94
+ let status = checkout.status;
95
+ let instruction = "Checkout 已结束;不要再次展示付款二维码。";
96
+ let next = null;
97
+ const recovery = [];
98
+ if (payment === "verified") {
99
+ status = "completed";
100
+ instruction = "Backend 已确认这笔付款。不要再次展示付款入口,不要调用 pay,不要创建新 Checkout 或 Execution。现在只执行 next.command,读取同一 Execution 的履约结果。";
101
+ next = serviceExecutionIDs.length === 1
102
+ ? { command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "读取同一笔已付款 Service Execution" }
103
+ : presentation.completed_order_id
104
+ ? { command: `itpay order ${presentation.completed_order_id}`, reason: "读取已创建订单" }
105
+ : { command: "itpay orders", reason: "恢复已付款订单" };
81
106
  }
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("");
107
+ else if (checkout.status === "refunded") {
108
+ instruction = "该 Checkout 已退款,不要再次付款或展示二维码。";
109
+ if (presentation.completed_order_id)
110
+ next = { command: `itpay order ${presentation.completed_order_id}`, reason: "读取订单与退款状态" };
89
111
  }
90
- if (plan.ideImageAttach) {
91
- lines.push(...ideImageAttachBlock(plan.ideImageAttach));
112
+ else if (checkout.status === "failed" || checkout.status === "expired") {
113
+ instruction = "该 Checkout 已失效;不要继续使用当前付款入口。";
114
+ if (serviceExecutionIDs.length === 1) {
115
+ recovery.push({ command: `itpay services next ${serviceExecutionIDs[0]} --json`, reason: "由服务端决定是否可恢复 Checkout" });
116
+ }
92
117
  }
93
- lines.push(`> :bulb: ${hintFor("checkout", c.status)}`);
94
- return lines.join("\n");
118
+ return { status, result, instruction, next, recovery };
119
+ }
120
+ function checkoutNeedsHumanHandoff(status) {
121
+ return !new Set(["payment_succeeded", "completed", "failed", "expired", "refunded"]).has(status);
122
+ }
123
+ function checkoutPlainResult(result) {
124
+ return Object.entries(result).map(([key, value]) => `${key}: ${typeof value === "string" ? value : JSON.stringify(value)}`);
125
+ }
126
+ function pendingInstruction(platform, amount) {
127
+ if (platform === "markdown")
128
+ return `Backend 尚未确认付款。把 handoff.markdown 原样发送到当前桌面对话,确认二维码、链接和金额 ${amount} 已对用户可见,然后停止等待。不要创建新 Checkout、Execution 或 Payment Intent;稍后仍然只执行 next.command 查询这一笔 Checkout。`;
129
+ if (platform === "terminal")
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。`;
132
+ }
133
+ function formatMoney(amountMinor, currency) {
134
+ return `${(amountMinor / 100).toFixed(2)} ${currency}`;
95
135
  }
96
136
  function checkoutPageURL(baseURL, checkoutID, displayToken) {
97
137
  const root = publicRoot(baseURL);
@@ -104,3 +144,12 @@ function checkoutQRPNGURL(baseURL, checkoutID, displayToken) {
104
144
  function publicRoot(baseURL) {
105
145
  return (baseURL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
106
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,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,56 @@
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";
4
+ export function isTerminalServiceExecutionStatus(status) {
5
+ return status === "failed" || status === "refunded" || status === "cancelled";
6
+ }
7
+ export class CommandContractError extends Error {
8
+ code;
9
+ instruction;
10
+ recovery;
11
+ constructor(code, message, instruction, recovery) {
12
+ super(message);
13
+ this.code = code;
14
+ this.instruction = instruction;
15
+ this.recovery = recovery;
16
+ this.name = "CommandContractError";
17
+ }
18
+ }
19
+ export function writeCommandEnvelope(value, options = {}) {
20
+ const out = resolveOutput(options.output);
21
+ const agentType = options.agentType ?? declaredAgentType();
22
+ const qualified = qualifyEnvelope(value, agentType);
23
+ if (options.jsonOutput) {
24
+ out(JSON.stringify(qualified, null, 2) + "\n");
25
+ return;
26
+ }
27
+ out(`${qualified.status}\n`);
28
+ const facts = "result" in qualified ? qualified.result : qualified.error;
29
+ if (options.plainResult) {
30
+ for (const line of options.plainResult)
31
+ out(`${line}\n`);
32
+ }
33
+ else {
34
+ for (const [key, fact] of Object.entries(facts)) {
35
+ out(`${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
36
+ }
37
+ }
38
+ if ("handoff" in qualified && qualified.handoff) {
39
+ for (const [key, fact] of Object.entries(qualified.handoff)) {
40
+ out(`handoff.${key}: ${typeof fact === "string" ? fact : JSON.stringify(fact)}\n`);
41
+ }
42
+ }
43
+ out(`instruction: ${qualified.instruction}\n`);
44
+ if (qualified.next)
45
+ out(`next: ${qualified.next.command}\n`);
46
+ if (qualified.recovery.length > 0) {
47
+ out("recovery:\n");
48
+ for (const action of qualified.recovery) {
49
+ out(` - ${action.command}\n`);
50
+ out(` reason: ${action.reason}\n`);
51
+ }
52
+ }
53
+ }
3
54
  export function attachAgentGuidance(payload, guidance) {
4
55
  return {
5
56
  ...payload,
@@ -8,11 +59,18 @@ export function attachAgentGuidance(payload, guidance) {
8
59
  }
9
60
  export function printAgentGuidance(guidance, output) {
10
61
  const out = resolveOutput(output);
62
+ const agentType = declaredAgentType();
11
63
  out(`${guidance.summary}\n`);
12
64
  if (guidance.visible_results?.length) {
13
65
  out("results:\n");
14
- for (const item of guidance.visible_results)
66
+ for (const item of guidance.visible_results) {
15
67
  out(` ${item.rank}. ${item.title}\n`);
68
+ for (const [key, value] of Object.entries(item.safe_payload)) {
69
+ if (key === "name" || key === "company_name" || value === "" || value === undefined || value === null)
70
+ continue;
71
+ out(` ${key}: ${String(value)}\n`);
72
+ }
73
+ }
16
74
  }
17
75
  if (guidance.next_actions.length === 0) {
18
76
  out("next actions: none\n");
@@ -21,7 +79,7 @@ export function printAgentGuidance(guidance, output) {
21
79
  out("next actions:\n");
22
80
  for (const action of guidance.next_actions) {
23
81
  out(` - ${action.label}\n`);
24
- out(` ${action.command}\n`);
82
+ out(` ${qualifyItPayCommand(action.command, agentType)}\n`);
25
83
  if (action.requires_human)
26
84
  out(" requires human confirmation\n");
27
85
  if (action.reason)
@@ -32,10 +90,20 @@ export function printAgentGuidance(guidance, output) {
32
90
  out("recovery:\n");
33
91
  for (const action of guidance.recovery) {
34
92
  out(` - ${action.label}\n`);
35
- out(` ${action.command}\n`);
93
+ out(` ${qualifyItPayCommand(action.command, agentType)}\n`);
36
94
  }
37
95
  }
38
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
+ }
39
107
  export function buildCartGuidance(cart, serviceModel) {
40
108
  const serviceLine = latestServiceLine(cart);
41
109
  if (serviceLine?.service_execution_id) {
@@ -126,7 +194,6 @@ export function buildServiceActionGuidance(action) {
126
194
  action_type: action.action_type,
127
195
  status: action.status,
128
196
  result_item_id: action.result_item_id,
129
- selected_candidate_hash: action.selected_candidate_hash,
130
197
  },
131
198
  next_actions: [
132
199
  {
@@ -175,9 +242,19 @@ export function errorRecoveryActions(error) {
175
242
  if (error.code === "agent_identity_required") {
176
243
  return [
177
244
  {
178
- id: "set_agent_identity",
179
- label: "Set a stable agent device id",
180
- command: "export ITPAY_AGENT_DEVICE_ID=<stable_agent_device_id>",
245
+ id: "inspect_agent_setup",
246
+ label: "Inspect supported Agent Type setup",
247
+ command: "itpay install --json",
248
+ },
249
+ ];
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.",
181
258
  },
182
259
  ];
183
260
  }
@@ -253,6 +330,7 @@ function buildServiceGuidance(input) {
253
330
  const resultItem = input.resultItems?.[0];
254
331
  const checkoutID = input.checkoutBindings?.at(-1)?.checkout_id;
255
332
  const delivery = input.deliveryBindings?.[0];
333
+ const deliveryMode = String(delivery?.redacted_summary?.delivery_mode ?? (delivery?.vault_artifact_id ? "vault_artifact" : ""));
256
334
  const backendCheckout = input.backendNextActions?.find((action) => action.kind === "create_checkout");
257
335
  const nextActions = [];
258
336
  const recovery = [
@@ -262,7 +340,18 @@ function buildServiceGuidance(input) {
262
340
  command: `itpay services get ${execution.service_execution_id}`,
263
341
  },
264
342
  ];
265
- if (execution.status === "completed" || execution.next_action === "completed") {
343
+ if (isTerminalServiceExecutionStatus(execution.status)) {
344
+ // Terminal executions are inspectable, but no command may advance or replay them.
345
+ }
346
+ else if (deliveryMode === "agent_visible_result" && (input.resultItems?.length ?? 0) > 0) {
347
+ nextActions.push({
348
+ id: "use_agent_visible_result",
349
+ label: "Use the safe candidate list shown above",
350
+ command: `itpay services next ${execution.service_execution_id} --json`,
351
+ 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.",
352
+ });
353
+ }
354
+ else if (execution.status === "completed" || execution.next_action === "completed") {
266
355
  nextActions.push({
267
356
  id: "inspect_order_or_grant",
268
357
  label: "Inspect order, claim, or grant from the checkout/order owner",
@@ -327,13 +416,13 @@ function buildServiceGuidance(input) {
327
416
  });
328
417
  }
329
418
  }
330
- else if (input.providerCalled && (input.resultItems?.length ?? 0) === 0) {
419
+ else if ((input.providerCalled || execution.next_action === "select_candidate") && (input.resultItems?.length ?? 0) === 0) {
331
420
  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>`,
421
+ id: "start_refined_search",
422
+ label: "Start a new execution with a more specific company name",
423
+ command: `itpay services start ${execution.service_id}`,
335
424
  requires_human: true,
336
- reason: "The provider returned no candidates. Do not repeat the same query.",
425
+ reason: "No candidates were found. This execution is finished; use one new execution per new keyword.",
337
426
  });
338
427
  }
339
428
  else if (needsHumanSelection(execution, resultItem)) {
@@ -344,6 +433,12 @@ function buildServiceGuidance(input) {
344
433
  requires_human: true,
345
434
  reason: "Do not choose a candidate without explicit human confirmation.",
346
435
  });
436
+ nextActions.push({
437
+ id: "start_another_search",
438
+ label: "Search another company in a new execution",
439
+ command: `itpay services start ${execution.service_id}`,
440
+ reason: "This execution has completed its one keyword lookup; do not reuse it for another keyword.",
441
+ });
347
442
  }
348
443
  else if (prePurchase) {
349
444
  const action = {
@@ -352,7 +447,7 @@ function buildServiceGuidance(input) {
352
447
  command: `itpay services invoke ${execution.service_execution_id} --capability ${prePurchase.capability_id} --input key=value --json`,
353
448
  };
354
449
  if (prePurchase.free_quota_limit) {
355
- action.reason = `Free quota limit: ${prePurchase.free_quota_limit} per ${prePurchase.quota_subject || "subject"}.`;
450
+ action.reason = `One keyword per execution. Free quota limit: ${prePurchase.free_quota_limit} per ${prePurchase.quota_subject || "subject"}.`;
356
451
  }
357
452
  nextActions.push(action);
358
453
  }
@@ -377,6 +472,7 @@ function buildServiceGuidance(input) {
377
472
  delivery: delivery
378
473
  ? {
379
474
  status: delivery.status,
475
+ delivery_mode: deliveryMode || undefined,
380
476
  vault_artifact_id: delivery.vault_artifact_id,
381
477
  vault_status: delivery.vault_status,
382
478
  vault_payload_state: delivery.vault_payload_state,
@@ -397,16 +493,16 @@ function buildServiceGuidance(input) {
397
493
  })),
398
494
  result_items: (input.resultItems ?? []).map((item) => ({
399
495
  result_item_id: item.service_capability_result_item_id,
400
- stable_hash: item.stable_hash,
401
496
  rank: item.rank,
402
497
  display_title: item.display_title,
498
+ safe_payload: item.safe_payload,
403
499
  })),
404
500
  effective_quota: input.effectiveQuota,
405
501
  },
406
502
  next_actions: nextActions,
407
503
  recovery,
408
504
  ...(input.resultItems?.length
409
- ? { visible_results: input.resultItems.map((item) => ({ rank: item.rank, title: item.display_title })) }
505
+ ? { visible_results: input.resultItems.map((item) => ({ rank: item.rank, title: item.display_title, safe_payload: item.safe_payload })) }
410
506
  : {}),
411
507
  };
412
508
  }