@itpay/cli 0.1.10 → 0.2.0
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/bin/itp +53 -4563
- package/docs/agent/buyer/cart-checkout.json +20 -11
- package/docs/agent/buyer/catalog-search.json +23 -16
- package/docs/agent/buyer/human-claim-ui.json +2 -0
- package/docs/agent/buyer/payment-qr.json +1 -0
- package/docs/agent/buyer/payment-wait.json +5 -1
- package/docs/agent/buyer/qr-refresh.json +2 -0
- package/docs/agent/buyer/quickstart.json +5 -3
- package/docs/agent/buyer/recovery.json +3 -0
- package/docs/agent/buyer/secure-delivery.json +2 -0
- package/docs/agent/buyer/vault-agent-read.json +4 -0
- package/install.ps1 +15 -3
- package/install.sh +16 -3
- package/lib/buyer.js +1675 -0
- package/lib/docs.js +200 -0
- package/lib/env.js +713 -0
- package/lib/http.js +151 -0
- package/lib/ops.js +135 -0
- package/lib/render-human.js +463 -0
- package/lib/runtime.js +1532 -0
- package/package.json +2 -1
- package/skills/itpay-buyer/SKILL.md +8 -4
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import QRCode from "qrcode";
|
|
6
|
+
import { apiTimeoutMs, cliCommand, commandExists, mergeRun, readRun, readState, safeErrorMessage, writeRun } from "./env.js";
|
|
7
|
+
|
|
8
|
+
async function renderItPayPaymentAction(intent, flags = {}) {
|
|
9
|
+
const action = intent?.human_action ? { ...intent.human_action } : (intent?.payment_url ? {
|
|
10
|
+
id: intent.payment_intent_id,
|
|
11
|
+
title: "Scan with Alipay",
|
|
12
|
+
url: intent.payment_url,
|
|
13
|
+
expires_at: intent.qr?.expires_at
|
|
14
|
+
} : null);
|
|
15
|
+
if (action) {
|
|
16
|
+
if (intent?.qr_png_url || intent?.qr?.png_url) {
|
|
17
|
+
action.qr_png_url = intent.qr_png_url || intent.qr.png_url;
|
|
18
|
+
}
|
|
19
|
+
if (intent?.mobile_wallet_url) {
|
|
20
|
+
action.mobile_wallet_url = intent.mobile_wallet_url;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (action && (intent?.qr_image_url || intent?.qr?.image_url)) {
|
|
24
|
+
action.qr_image_url = intent.qr_image_url || intent.qr.image_url;
|
|
25
|
+
action.display_mode = intent.qr?.scan_mode || "itpay_entry_qr";
|
|
26
|
+
action.description = action.description || "Scan the ItPay payment entry QR; ItPay will safely hand off to Alipay.";
|
|
27
|
+
}
|
|
28
|
+
const result = await renderHumanAction(action, flags);
|
|
29
|
+
if (intent && action) {
|
|
30
|
+
intent.human_action = { ...(intent.human_action || {}), ...action };
|
|
31
|
+
if (action.local_qr_path) intent.local_qr_path = action.local_qr_path;
|
|
32
|
+
if (action.preferred_qr_url) intent.preferred_qr_url = action.preferred_qr_url;
|
|
33
|
+
if (action.mobile_wallet_url) intent.mobile_wallet_url = action.mobile_wallet_url;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function humanActionSummaryLines(action) {
|
|
39
|
+
if (!action?.url) return [];
|
|
40
|
+
const lines = [
|
|
41
|
+
"ITP HUMAN ACTION REQUIRED",
|
|
42
|
+
`Title: ${action.title || "Scan with Alipay"}`,
|
|
43
|
+
`URL: ${action.url}`
|
|
44
|
+
];
|
|
45
|
+
if (action.local_qr_path) {
|
|
46
|
+
lines.push(`Local QR image: ${action.local_qr_path}`);
|
|
47
|
+
}
|
|
48
|
+
if (action.qr_png_url) {
|
|
49
|
+
lines.push(`QR PNG: ${action.qr_png_url}`);
|
|
50
|
+
}
|
|
51
|
+
if (action.qr_image_url) {
|
|
52
|
+
lines.push(`QR image: ${action.qr_image_url}`);
|
|
53
|
+
}
|
|
54
|
+
if (action.mobile_wallet_url) {
|
|
55
|
+
lines.push(`Mobile wallet link: ${action.mobile_wallet_url}`);
|
|
56
|
+
}
|
|
57
|
+
if (action.oauth_start_url) {
|
|
58
|
+
lines.push(`Alipay auth fallback: ${action.oauth_start_url}`);
|
|
59
|
+
}
|
|
60
|
+
if (action.fallback_text && !action.fallback_text.includes(action.url)) {
|
|
61
|
+
lines.push(`Fallback: ${action.fallback_text}`);
|
|
62
|
+
}
|
|
63
|
+
if (action.expires_at) {
|
|
64
|
+
lines.push(`Expires at: ${formatActionTime(action.expires_at)}`);
|
|
65
|
+
}
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeHumanActionSummary(action, suffix = "") {
|
|
70
|
+
const lines = humanActionSummaryLines(action);
|
|
71
|
+
if (!lines.length) return;
|
|
72
|
+
process.stderr.write(`\n${lines.join("\n")}${suffix ? `\n${suffix}` : ""}\n\n`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function waitHeartbeatMs(flags = {}) {
|
|
76
|
+
if (flags.quiet || process.env.ITP_WAIT_HEARTBEAT_SECONDS === "0") return 0;
|
|
77
|
+
return Math.max(5000, Number(flags.heartbeat || process.env.ITP_WAIT_HEARTBEAT_SECONDS || 20) * 1000);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeWaitHeartbeat({ kind, idName, idValue, status, action, lastHeartbeatAt, flags, command }) {
|
|
81
|
+
const heartbeatMs = waitHeartbeatMs(flags);
|
|
82
|
+
if (!heartbeatMs) return lastHeartbeatAt;
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (now - lastHeartbeatAt < heartbeatMs) return lastHeartbeatAt;
|
|
85
|
+
const lines = [
|
|
86
|
+
`ITP waiting for ${kind}: ${idName}=${idValue} status=${status}`,
|
|
87
|
+
action?.url ? `URL: ${action.url}` : null,
|
|
88
|
+
action?.local_qr_path ? `Local QR image: ${action.local_qr_path}` : null,
|
|
89
|
+
action?.qr_png_url ? `QR PNG: ${action.qr_png_url}` : null,
|
|
90
|
+
action?.qr_image_url ? `QR image: ${action.qr_image_url}` : null,
|
|
91
|
+
action?.mobile_wallet_url ? `Mobile wallet link: ${action.mobile_wallet_url}` : null,
|
|
92
|
+
action?.oauth_start_url ? `Alipay auth fallback: ${action.oauth_start_url}` : null,
|
|
93
|
+
command ? `Resume command: ${command}` : null
|
|
94
|
+
].filter(Boolean);
|
|
95
|
+
process.stderr.write(`\n${lines.join("\n")}\n\n`);
|
|
96
|
+
return now;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function renderHumanAction(action, flags = {}) {
|
|
100
|
+
if (!action?.url) return null;
|
|
101
|
+
const qrImageURL = preferredHumanActionQRURL(action);
|
|
102
|
+
const mode = String(flags.display || process.env.ITP_DISPLAY || "").toLowerCase() || "auto";
|
|
103
|
+
annotateHumanActionPresentation(action, qrImageURL);
|
|
104
|
+
if (flags.json || mode === "none" || mode === "json") {
|
|
105
|
+
if (qrImageURL && shouldPrepareLocalQRForJSON(mode, flags, action)) {
|
|
106
|
+
const localPath = await prepareLocalQRFile(action, qrImageURL, flags, mode !== "file" && !flags.qr_file && !process.env.ITP_QR_FILE);
|
|
107
|
+
if (localPath) {
|
|
108
|
+
attachAgentQRImage(action, qrImageURL, localPath);
|
|
109
|
+
persistHumanAction(action, flags);
|
|
110
|
+
return { rendered: false, mode: "json-local-qr", outputs: [localPath] };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (!qrImageURL && shouldGenerateLocalQRFromActionURL(action, mode, flags)) {
|
|
114
|
+
const localPath = await prepareLocalQRFromActionURL(action, flags, mode !== "file" && !flags.qr_file && !process.env.ITP_QR_FILE);
|
|
115
|
+
if (localPath) {
|
|
116
|
+
attachAgentLocalQR(action, localPath);
|
|
117
|
+
annotateHumanActionPresentation(action, "");
|
|
118
|
+
persistHumanAction(action, flags);
|
|
119
|
+
return { rendered: false, mode: "json-local-url-qr", outputs: [localPath] };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { rendered: false, mode: flags.json ? "json" : mode };
|
|
123
|
+
}
|
|
124
|
+
const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
|
|
125
|
+
if (["discord", "telegram", "whatsapp"].includes(host)) {
|
|
126
|
+
return { rendered: false, mode: "chat-json", host };
|
|
127
|
+
}
|
|
128
|
+
if (shouldUseAgentTextQR(flags)) {
|
|
129
|
+
if (qrImageURL) {
|
|
130
|
+
const localPath = await prepareLocalQRFile(action, qrImageURL, flags, true);
|
|
131
|
+
attachAgentQRImage(action, qrImageURL, localPath);
|
|
132
|
+
persistHumanAction(action, flags);
|
|
133
|
+
return { rendered: false, mode: localPath ? "agent-local-image-qr" : "agent-image-qr", host, outputs: [localPath || "preferred_qr_url"] };
|
|
134
|
+
}
|
|
135
|
+
if (shouldGenerateLocalQRFromActionURL(action, mode, flags)) {
|
|
136
|
+
const localPath = await prepareLocalQRFromActionURL(action, flags, true);
|
|
137
|
+
if (localPath) {
|
|
138
|
+
attachAgentLocalQR(action, localPath);
|
|
139
|
+
annotateHumanActionPresentation(action, "");
|
|
140
|
+
persistHumanAction(action, flags);
|
|
141
|
+
return { rendered: false, mode: "agent-local-url-qr", host, outputs: [localPath] };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
process.stderr.write(`No QR image available. Open action URL: ${action.url}\n`);
|
|
145
|
+
persistHumanAction(action, flags);
|
|
146
|
+
return { rendered: false, mode: "agent-url-fallback", host, outputs: ["action_url"] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const renderResult = { rendered: false, mode, outputs: [] };
|
|
150
|
+
writeHumanActionSummary(action);
|
|
151
|
+
if ((mode === "auto" || mode === "browser") && shouldOpenBrowser(flags)) {
|
|
152
|
+
if (openBrowser(action.mobile_wallet_url || qrImageURL || action.url)) {
|
|
153
|
+
renderResult.rendered = true;
|
|
154
|
+
renderResult.outputs.push("browser");
|
|
155
|
+
}
|
|
156
|
+
if (mode === "browser") return renderResult;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (mode === "file" || flags.qr_file || process.env.ITP_QR_FILE) {
|
|
160
|
+
const file = flags.qr_file || process.env.ITP_QR_FILE || defaultQRFilePath(action, qrImageURL);
|
|
161
|
+
if (qrImageURL) {
|
|
162
|
+
await downloadQRImage(qrImageURL, file, flags);
|
|
163
|
+
action.local_qr_path = file;
|
|
164
|
+
action.local_qr_mime = qrMimeType(qrImageURL, action);
|
|
165
|
+
process.stderr.write(`Alipay QR image: ${file}\n`);
|
|
166
|
+
renderResult.rendered = true;
|
|
167
|
+
renderResult.outputs.push(file);
|
|
168
|
+
if (mode === "file") return renderResult;
|
|
169
|
+
} else {
|
|
170
|
+
const localPath = shouldGenerateLocalQRFromActionURL(action, mode, flags)
|
|
171
|
+
? await prepareLocalQRFromActionURL(action, flags, false)
|
|
172
|
+
: "";
|
|
173
|
+
if (localPath) {
|
|
174
|
+
attachAgentLocalQR(action, localPath);
|
|
175
|
+
annotateHumanActionPresentation(action, "");
|
|
176
|
+
process.stderr.write(`Alipay action QR image: ${localPath}\n`);
|
|
177
|
+
renderResult.rendered = true;
|
|
178
|
+
renderResult.outputs.push(localPath);
|
|
179
|
+
if (mode === "file") return renderResult;
|
|
180
|
+
} else {
|
|
181
|
+
process.stderr.write(`No QR image available. Open action URL: ${action.url}\n`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (qrImageURL) {
|
|
187
|
+
process.stderr.write(`Alipay QR image URL: ${qrImageURL}\n`);
|
|
188
|
+
if (action.mobile_wallet_url) process.stderr.write(`Mobile wallet link: ${action.mobile_wallet_url}\n`);
|
|
189
|
+
renderResult.outputs.push("preferred_qr_url");
|
|
190
|
+
return renderResult;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if ((mode === "auto" || mode === "terminal") && shouldRenderTerminalQR(host)) {
|
|
194
|
+
const qr = await QRCode.toString(action.url, {
|
|
195
|
+
type: terminalQRType(flags, host),
|
|
196
|
+
small: true,
|
|
197
|
+
errorCorrectionLevel: "M"
|
|
198
|
+
});
|
|
199
|
+
process.stderr.write(`\n${action.title || "Scan with Alipay"}\n`);
|
|
200
|
+
if (action.description) process.stderr.write(`${action.description}\n`);
|
|
201
|
+
process.stderr.write(`${qr}\n`);
|
|
202
|
+
process.stderr.write(`Alipay action URL: ${action.url}\n`);
|
|
203
|
+
if (action.expires_at) process.stderr.write(`Expires at: ${formatActionTime(action.expires_at)}\n`);
|
|
204
|
+
renderResult.rendered = true;
|
|
205
|
+
renderResult.outputs.push("terminal");
|
|
206
|
+
return renderResult;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
process.stderr.write(`${action.fallback_text || `Open Alipay URL: ${action.url}`}\n`);
|
|
210
|
+
renderResult.outputs.push("url");
|
|
211
|
+
return renderResult;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function preferredHumanActionQRURL(action) {
|
|
215
|
+
return action?.qr_png_url ||
|
|
216
|
+
humanActionPresentationURL(action, "qr_png_url") ||
|
|
217
|
+
action?.qr_image_url ||
|
|
218
|
+
action?.qr?.png_url ||
|
|
219
|
+
action?.qr?.image_url ||
|
|
220
|
+
humanActionPresentationURL(action, "qr_svg_url") ||
|
|
221
|
+
"";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function humanActionPresentationURL(action, type) {
|
|
225
|
+
const display = action?.presentation?.display;
|
|
226
|
+
if (!Array.isArray(display)) return "";
|
|
227
|
+
const found = display.find((item) => item?.type === type && item?.url);
|
|
228
|
+
return found?.url || "";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function annotateHumanActionPresentation(action, qrImageURL) {
|
|
232
|
+
if (!action) return action;
|
|
233
|
+
if (qrImageURL) {
|
|
234
|
+
action.preferred_qr_url = qrImageURL;
|
|
235
|
+
action.preferred_qr_mime = qrMimeType(qrImageURL, action);
|
|
236
|
+
}
|
|
237
|
+
const mobileURL = action.mobile_wallet_url || humanActionPresentationURL(action, "mobile_wallet_url");
|
|
238
|
+
if (mobileURL) action.mobile_wallet_url = mobileURL;
|
|
239
|
+
action.agent_display_hint = {
|
|
240
|
+
primary: action.local_qr_path ? "local_qr_path" : (action.qr_png_url ? "qr_png_url" : "preferred_qr_url"),
|
|
241
|
+
desktop: action.kind === "auth_qr"
|
|
242
|
+
? "Show local_qr_path when present; otherwise show the ItPay first-purchase entry URL. This QR starts login/registration/profile authorization and should continue to payment for the same checkout after approval; it is not payment proof."
|
|
243
|
+
: "Show local_qr_path when present; otherwise show qr_png_url/preferred_qr_url directly. This is an ItPay-hosted human QR image and may render the native provider payment code; do not render your own QR from payment_entry_url.",
|
|
244
|
+
mobile: "Show mobile_wallet_url as a clickable human-only fallback when present.",
|
|
245
|
+
proof: "Only payment_intent.verified proves payment. QR display or page open is not payment proof."
|
|
246
|
+
};
|
|
247
|
+
return action;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function shouldPrepareLocalQRForJSON(mode, flags = {}, action = {}) {
|
|
251
|
+
if (mode === "file" || flags.qr_file || process.env.ITP_QR_FILE) return true;
|
|
252
|
+
if (action?.qr_png_url || action?.preferred_qr_url || action?.qr_image_url) return true;
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function shouldGenerateLocalQRFromActionURL(action = {}, mode = "", flags = {}) {
|
|
257
|
+
if (!action?.url) return false;
|
|
258
|
+
if (action.kind === "auth_qr") return true;
|
|
259
|
+
return mode === "file" || Boolean(flags.qr_file || process.env.ITP_QR_FILE);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function prepareLocalQRFile(action, qrImageURL, flags = {}, optional = false) {
|
|
263
|
+
if (!qrImageURL) return "";
|
|
264
|
+
const file = flags.qr_file || process.env.ITP_QR_FILE || defaultQRFilePath(action, qrImageURL);
|
|
265
|
+
try {
|
|
266
|
+
await downloadQRImage(qrImageURL, file, flags);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
if (!optional) throw error;
|
|
269
|
+
action.local_qr_error = safeErrorMessage(error);
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
action.local_qr_path = file;
|
|
273
|
+
action.local_qr_mime = qrMimeType(qrImageURL, action);
|
|
274
|
+
return file;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function prepareLocalQRFromActionURL(action, flags = {}, optional = false) {
|
|
278
|
+
if (!action?.url) return "";
|
|
279
|
+
const file = flags.qr_file || process.env.ITP_QR_FILE || defaultGeneratedQRFilePath(action);
|
|
280
|
+
try {
|
|
281
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
282
|
+
await QRCode.toFile(file, action.url, {
|
|
283
|
+
type: "png",
|
|
284
|
+
errorCorrectionLevel: "M",
|
|
285
|
+
margin: 2,
|
|
286
|
+
width: 512
|
|
287
|
+
});
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (!optional) throw error;
|
|
290
|
+
action.local_qr_error = safeErrorMessage(error);
|
|
291
|
+
return "";
|
|
292
|
+
}
|
|
293
|
+
action.local_qr_path = file;
|
|
294
|
+
action.local_qr_mime = "image/png";
|
|
295
|
+
return file;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function defaultQRFilePath(action, qrImageURL) {
|
|
299
|
+
const id = sanitizeFilename(action?.payment_intent_id || action?.id || "qr");
|
|
300
|
+
return path.join(os.tmpdir(), `itp-${id}.${qrFileExtension(qrImageURL, action)}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function defaultGeneratedQRFilePath(action) {
|
|
304
|
+
const id = sanitizeFilename(action?.payment_intent_id || action?.id || "qr");
|
|
305
|
+
return path.join(os.tmpdir(), `itp-${id}.png`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function qrFileExtension(qrImageURL, action = {}) {
|
|
309
|
+
const mime = qrMimeType(qrImageURL, action);
|
|
310
|
+
if (mime === "image/png") return "png";
|
|
311
|
+
if (mime === "image/svg+xml") return "svg";
|
|
312
|
+
return "img";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function qrMimeType(qrImageURL, action = {}) {
|
|
316
|
+
if (qrImageURL && action?.qr_png_url && qrImageURL === action.qr_png_url) return "image/png";
|
|
317
|
+
if (String(qrImageURL || "").toLowerCase().includes(".png")) return "image/png";
|
|
318
|
+
if (String(qrImageURL || "").toLowerCase().includes(".svg")) return "image/svg+xml";
|
|
319
|
+
return action?.preferred_qr_mime || "image/png";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function sanitizeFilename(value) {
|
|
323
|
+
return String(value || "qr").replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 96) || "qr";
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function formatActionTime(value) {
|
|
327
|
+
if (value === null || value === undefined || value === "") return "";
|
|
328
|
+
if (typeof value === "number" || /^[0-9]+$/.test(String(value))) {
|
|
329
|
+
const numeric = Number(value);
|
|
330
|
+
const millis = numeric > 100000000000 ? numeric : numeric * 1000;
|
|
331
|
+
const date = new Date(millis);
|
|
332
|
+
if (!Number.isNaN(date.getTime())) return date.toISOString();
|
|
333
|
+
}
|
|
334
|
+
const date = new Date(String(value));
|
|
335
|
+
if (!Number.isNaN(date.getTime())) return date.toISOString();
|
|
336
|
+
return String(value);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function shouldUseAgentTextQR(flags = {}) {
|
|
340
|
+
const mode = String(flags.display || process.env.ITP_DISPLAY || "").toLowerCase() || "auto";
|
|
341
|
+
const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
|
|
342
|
+
return mode === "chat" || mode === "agent" || (["gemini", "gemini-cli"].includes(host) && mode === "auto");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function shouldReturnAfterAgentTextQR(flags = {}) {
|
|
346
|
+
if (flags.wait || flags.wait_human) return false;
|
|
347
|
+
return shouldUseAgentTextQR(flags);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function attachAgentQRImage(action, qrImageURL, localPath = "") {
|
|
351
|
+
if (!action || !qrImageURL) return action;
|
|
352
|
+
action.preferred_qr_url = qrImageURL;
|
|
353
|
+
if (qrMimeType(qrImageURL, action) === "image/png") {
|
|
354
|
+
action.qr_png_url = action.qr_png_url || qrImageURL;
|
|
355
|
+
} else {
|
|
356
|
+
action.qr_image_url = action.qr_image_url || qrImageURL;
|
|
357
|
+
}
|
|
358
|
+
if (!Array.isArray(action.display)) {
|
|
359
|
+
action.display = [];
|
|
360
|
+
}
|
|
361
|
+
if (!action.display.some((item) => item?.type === "image")) {
|
|
362
|
+
action.display.push({
|
|
363
|
+
type: "image",
|
|
364
|
+
format: qrFileExtension(qrImageURL, action),
|
|
365
|
+
url: qrImageURL,
|
|
366
|
+
local_path: localPath || undefined,
|
|
367
|
+
instructions: "Render local_path when present; otherwise render this ItPay-hosted QR image for the human to scan. ItPay may render a native provider payment code inside the image, but the agent must not request, decode, or expose provider payloads. Do not encode payment_entry_url or mobile_wallet_url into your own QR."
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return action;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function attachAgentLocalQR(action, localPath = "") {
|
|
374
|
+
if (!action || !localPath) return action;
|
|
375
|
+
if (!Array.isArray(action.display)) {
|
|
376
|
+
action.display = [];
|
|
377
|
+
}
|
|
378
|
+
if (!action.display.some((item) => item?.type === "image" && item?.local_path === localPath)) {
|
|
379
|
+
action.display.push({
|
|
380
|
+
type: "image",
|
|
381
|
+
format: "png",
|
|
382
|
+
local_path: localPath,
|
|
383
|
+
instructions: "Render this local QR image for the human to scan. It encodes the ItPay human action URL, not a provider raw QR payload."
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return action;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function downloadQRImage(qrImageURL, file, flags = {}) {
|
|
390
|
+
const controller = new AbortController();
|
|
391
|
+
const timer = setTimeout(() => controller.abort(), apiTimeoutMs(flags));
|
|
392
|
+
let response;
|
|
393
|
+
try {
|
|
394
|
+
response = await fetch(qrImageURL, { method: "GET", signal: controller.signal });
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (error?.name === "AbortError") {
|
|
397
|
+
throw new Error(`QR image download timed out: ${qrImageURL}`);
|
|
398
|
+
}
|
|
399
|
+
throw error;
|
|
400
|
+
} finally {
|
|
401
|
+
clearTimeout(timer);
|
|
402
|
+
}
|
|
403
|
+
if (!response.ok) {
|
|
404
|
+
throw new Error(`QR image download failed: ${response.status}`);
|
|
405
|
+
}
|
|
406
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
407
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
408
|
+
fs.writeFileSync(file, bytes);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function persistHumanAction(action, flags = {}) {
|
|
412
|
+
const runId = flags.run_id || readState().current_run_id;
|
|
413
|
+
if (!runId || !action?.id) return;
|
|
414
|
+
const run = readRun(runId);
|
|
415
|
+
if (!run?.human_action?.id || run.human_action.id !== action.id) return;
|
|
416
|
+
writeRun(mergeRun(run, { human_action: action }));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function shouldRenderTerminalQR(host) {
|
|
420
|
+
if (process.stderr.isTTY) return true;
|
|
421
|
+
return ["gemini", "gemini-cli"].includes(host);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function terminalQRType(flags = {}, host = "") {
|
|
425
|
+
const requested = String(flags.qr_format || process.env.ITP_QR_FORMAT || "").toLowerCase();
|
|
426
|
+
if (requested === "unicode" || requested === "utf8") return "utf8";
|
|
427
|
+
if (requested === "ansi" || requested === "terminal") return "terminal";
|
|
428
|
+
if (["gemini", "gemini-cli"].includes(host)) return "utf8";
|
|
429
|
+
return "terminal";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function shouldOpenBrowser(flags = {}) {
|
|
433
|
+
if (flags.no_open_browser || process.env.ITP_OPEN_BROWSER === "false" || process.env.ITP_OPEN_BROWSER === "0") return false;
|
|
434
|
+
if (flags.open_browser || process.env.ITP_OPEN_BROWSER === "true" || process.env.ITP_OPEN_BROWSER === "1") return true;
|
|
435
|
+
if (process.env.SSH_CONNECTION || process.env.CI) return false;
|
|
436
|
+
return Boolean(process.stderr.isTTY && (process.platform === "darwin" || process.platform === "win32" || process.env.DISPLAY || process.env.WAYLAND_DISPLAY || process.env.WSL_DISTRO_NAME));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function openBrowser(targetURL) {
|
|
440
|
+
try {
|
|
441
|
+
if (process.platform === "darwin") {
|
|
442
|
+
execFileSync("open", [targetURL], { stdio: "ignore", timeout: 2000 });
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
if (process.platform === "win32") {
|
|
446
|
+
execFileSync("cmd", ["/c", "start", "", targetURL], { stdio: "ignore", timeout: 2000 });
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
if (process.env.WSL_DISTRO_NAME && commandExists("cmd.exe")) {
|
|
450
|
+
execFileSync("cmd.exe", ["/c", "start", "", targetURL], { stdio: "ignore", timeout: 2000 });
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
if (commandExists("xdg-open")) {
|
|
454
|
+
execFileSync("xdg-open", [targetURL], { stdio: "ignore", timeout: 2000 });
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export { renderItPayPaymentAction, humanActionSummaryLines, writeHumanActionSummary, waitHeartbeatMs, writeWaitHeartbeat, renderHumanAction, preferredHumanActionQRURL, humanActionPresentationURL, annotateHumanActionPresentation, shouldPrepareLocalQRForJSON, shouldGenerateLocalQRFromActionURL, prepareLocalQRFile, prepareLocalQRFromActionURL, defaultQRFilePath, defaultGeneratedQRFilePath, qrFileExtension, qrMimeType, sanitizeFilename, formatActionTime, shouldUseAgentTextQR, shouldReturnAfterAgentTextQR, attachAgentQRImage, attachAgentLocalQR, downloadQRImage, persistHumanAction, shouldRenderTerminalQR, terminalQRType, shouldOpenBrowser, openBrowser };
|