@itpay/cli 2.0.3 → 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.
- package/README.md +92 -142
- package/dist/src/client/backend.js +26 -8
- package/dist/src/commands/buy.js +84 -132
- package/dist/src/commands/cart.js +274 -169
- package/dist/src/commands/catalog.js +63 -38
- package/dist/src/commands/checkout.js +117 -78
- package/dist/src/commands/docs.js +97 -51
- package/dist/src/commands/guidance.js +87 -15
- package/dist/src/commands/install.js +50 -87
- package/dist/src/commands/next.js +45 -0
- package/dist/src/commands/order.js +44 -69
- package/dist/src/commands/orders.js +43 -15
- package/dist/src/commands/pay.js +51 -22
- package/dist/src/commands/readyz.js +8 -4
- package/dist/src/commands/refund.js +132 -11
- package/dist/src/commands/services.js +719 -148
- package/dist/src/main.js +711 -193
- package/dist/src/render/output.js +2 -3
- package/dist/src/state/cart_session.js +13 -17
- package/dist/src/state/client_context.js +4 -2
- package/dist/src/state/config.js +2 -4
- package/dist/src/state/device_authority.js +1 -1
- package/docs/agent/buyer/cart-checkout.json +27 -83
- package/docs/agent/buyer/install-and-setup.json +23 -67
- package/docs/agent/buyer/orders-refunds.json +31 -53
- package/docs/agent/buyer/payment-flow.json +24 -57
- package/docs/agent/buyer/quickstart.json +39 -162
- package/docs/agent/buyer/render-hosts.json +43 -57
- package/docs/cli-reference/agent-types.md +45 -0
- package/docs/cli-reference/commands/buy.md +167 -0
- package/docs/cli-reference/commands/cart/add.md +86 -0
- package/docs/cli-reference/commands/cart/clear.md +53 -0
- package/docs/cli-reference/commands/cart/index.md +30 -0
- package/docs/cli-reference/commands/cart/next.md +71 -0
- package/docs/cli-reference/commands/cart/remove.md +53 -0
- package/docs/cli-reference/commands/cart/show.md +65 -0
- package/docs/cli-reference/commands/catalog/index.md +26 -0
- package/docs/cli-reference/commands/catalog/list.md +45 -0
- package/docs/cli-reference/commands/checkout.md +74 -0
- package/docs/cli-reference/commands/docs/index.md +28 -0
- package/docs/cli-reference/commands/docs/list.md +51 -0
- package/docs/cli-reference/commands/docs/search.md +69 -0
- package/docs/cli-reference/commands/docs/show.md +68 -0
- package/docs/cli-reference/commands/install.md +112 -0
- package/docs/cli-reference/commands/next.md +87 -0
- package/docs/cli-reference/commands/order.md +92 -0
- package/docs/cli-reference/commands/orders.md +83 -0
- package/docs/cli-reference/commands/pay.md +103 -0
- package/docs/cli-reference/commands/readyz.md +39 -0
- package/docs/cli-reference/commands/refund/cancel.md +62 -0
- package/docs/cli-reference/commands/refund/create.md +85 -0
- package/docs/cli-reference/commands/refund/get.md +60 -0
- package/docs/cli-reference/commands/refund/index.md +33 -0
- package/docs/cli-reference/commands/refund/list.md +68 -0
- package/docs/cli-reference/commands/refund/watch.md +73 -0
- package/docs/cli-reference/commands/services/action.md +43 -0
- package/docs/cli-reference/commands/services/checkout.md +82 -0
- package/docs/cli-reference/commands/services/events.md +73 -0
- package/docs/cli-reference/commands/services/get.md +66 -0
- package/docs/cli-reference/commands/services/index.md +45 -0
- package/docs/cli-reference/commands/services/invoke.md +67 -0
- package/docs/cli-reference/commands/services/list.md +61 -0
- package/docs/cli-reference/commands/services/next.md +162 -0
- package/docs/cli-reference/commands/services/quote.md +59 -0
- package/docs/cli-reference/commands/services/read-result.md +98 -0
- package/docs/cli-reference/commands/services/start.md +53 -0
- package/docs/cli-reference/conventions.md +94 -0
- package/docs/cli-reference/index.md +64 -0
- package/package.json +1 -1
- package/skills/itpay-buyer/SKILL.md +47 -113
|
@@ -1,97 +1,136 @@
|
|
|
1
|
-
// Reads
|
|
2
|
-
// and
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
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
|
|
14
|
-
|
|
15
|
-
host
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
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 (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
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
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 (
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
28
|
-
const
|
|
29
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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: "
|
|
179
|
-
label: "
|
|
180
|
-
command: "
|
|
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
|
|
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: "
|
|
333
|
-
label: "
|
|
334
|
-
command: `itpay services
|
|
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: "
|
|
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
|
}
|