@itpay/cli 0.1.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 ADDED
@@ -0,0 +1,4311 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { execFileSync } from "node:child_process";
8
+ import { fileURLToPath } from "node:url";
9
+ import QRCode from "qrcode";
10
+
11
+ const VERSION = "0.1.2";
12
+ const DEFAULT_API_BASE = process.env.ITPAY_API_BASE || process.env.ITPAY_CORE_BASE_URL || process.env.VOLTAGENT_API_BASE || "http://localhost:3000";
13
+ const CONFIG_DIR = path.join(os.homedir(), ".itp");
14
+ const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
15
+ const STATE_PATH = path.join(CONFIG_DIR, "state.json");
16
+ const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
17
+ const RUNS_DIR = path.join(CONFIG_DIR, "runs");
18
+ const LOCK_PATH = path.join(CONFIG_DIR, "state.lock");
19
+ const CLI_FILE = fileURLToPath(import.meta.url);
20
+ const CLI_DIR = path.dirname(CLI_FILE);
21
+ const PACKAGE_ROOT = path.dirname(CLI_DIR);
22
+
23
+ main().catch((error) => {
24
+ outputError(error);
25
+ process.exit(1);
26
+ });
27
+
28
+ async function main() {
29
+ const args = process.argv.slice(2);
30
+ if ((args.length === 1 && args[0] === "--version") || args[0] === "version") {
31
+ output({ version: VERSION });
32
+ return;
33
+ }
34
+
35
+ const [group, command, ...rest] = args;
36
+ const flags = parseFlags(rest);
37
+
38
+ if (group === "usage") {
39
+ await usage(parseFlags(args.slice(1)));
40
+ return;
41
+ }
42
+ if (group === "balance") {
43
+ await balance(parseFlags(args.slice(1)));
44
+ return;
45
+ }
46
+ if (group === "plans" && (!command || String(command).startsWith("--"))) {
47
+ await plansList(parseFlags(args.slice(1)));
48
+ return;
49
+ }
50
+ if (group === "setup") {
51
+ await setup(parseFlags(args.slice(1)));
52
+ return;
53
+ }
54
+ if (group === "buy") {
55
+ const buyFlags = parseFlags(command && String(command).startsWith("--") ? args.slice(1) : rest);
56
+ if (command && !String(command).startsWith("--")) buyFlags.selection = command;
57
+ await buyerBuy(buyFlags);
58
+ return;
59
+ }
60
+ if (group === "buyer") {
61
+ await buyer(command, rest, parseFlags(rest));
62
+ return;
63
+ }
64
+ if (group === "ops") {
65
+ await ops(command, rest, parseFlags(rest));
66
+ return;
67
+ }
68
+ if (group === "docs") {
69
+ const docsCommand = command && !String(command).startsWith("--") ? command : "list";
70
+ const docsFlags = parseFlags(command && String(command).startsWith("--") ? args.slice(1) : rest);
71
+ await docs(docsCommand, command && String(command).startsWith("--") ? [] : rest, docsFlags);
72
+ return;
73
+ }
74
+ if (group === "status") {
75
+ await agentStatus(parseFlags(args.slice(1)));
76
+ return;
77
+ }
78
+ if (group === "resume") {
79
+ await resume(parseFlags(args.slice(1)));
80
+ return;
81
+ }
82
+ if (group === "runs") {
83
+ const runCommand = command && !String(command).startsWith("--") ? command : "current";
84
+ const runFlags = parseFlags(command && String(command).startsWith("--") ? args.slice(1) : rest);
85
+ if (rest[0] && !String(rest[0]).startsWith("--")) {
86
+ runFlags.run_id = rest[0];
87
+ }
88
+ await runs(runCommand, runFlags);
89
+ return;
90
+ }
91
+ if (group === "doctor") {
92
+ await doctor(parseFlags(args.slice(1)));
93
+ return;
94
+ }
95
+ if (group === "skill") {
96
+ const skillCommand = command && !String(command).startsWith("--") ? command : "show";
97
+ const skillFlags = parseFlags(command && String(command).startsWith("--") ? args.slice(1) : rest);
98
+ await skill(skillCommand, skillFlags);
99
+ return;
100
+ }
101
+ if (group === "keys") {
102
+ await keys(command, parseFlags(rest));
103
+ return;
104
+ }
105
+ if (group === "token") {
106
+ await token(command, parseFlags(rest));
107
+ return;
108
+ }
109
+ if (group === "sync") {
110
+ await sync(parseFlags(args.slice(1)));
111
+ return;
112
+ }
113
+ if (group === "admin") {
114
+ await admin(command, rest, parseFlags(rest));
115
+ return;
116
+ }
117
+ if (group === "auth" && command === "device") {
118
+ const deviceFlags = parseFlags(rest.slice(1));
119
+ if (rest[0] === "poll" && rest[1] && !String(rest[1]).startsWith("--")) {
120
+ deviceFlags.auth_id = rest[1];
121
+ }
122
+ await authDevice(rest[0], deviceFlags);
123
+ return;
124
+ }
125
+
126
+ switch (`${group || ""} ${command || ""}`.trim()) {
127
+ case "auth register":
128
+ await authRegister(flags);
129
+ return;
130
+ case "auth login":
131
+ await authLogin(flags);
132
+ return;
133
+ case "auth status":
134
+ await authStatus(flags);
135
+ return;
136
+ case "account show":
137
+ await accountShow(flags);
138
+ return;
139
+ case "account login-link":
140
+ await accountLoginLink(flags);
141
+ return;
142
+ case "account set-password":
143
+ await accountSetPassword(flags);
144
+ return;
145
+ case "plans list":
146
+ await plansList(flags);
147
+ return;
148
+ case "plans show":
149
+ await plansShow(rest[0], flags);
150
+ return;
151
+ case "checkout create":
152
+ await checkoutCreate(flags);
153
+ return;
154
+ case "checkout recover":
155
+ await checkoutRecover(rest[0] || readState().last_checkout_id, flags);
156
+ return;
157
+ case "checkout open":
158
+ await checkoutOpen(flags);
159
+ return;
160
+ case "checkout qr":
161
+ await checkoutQR(rest[0] || readState().last_checkout_id, flags);
162
+ return;
163
+ case "checkout list":
164
+ await checkoutList(flags);
165
+ return;
166
+ case "payment wait":
167
+ await paymentWait(rest[0] || readState().last_checkout_id, flags);
168
+ return;
169
+ case "balance":
170
+ await balance(flags);
171
+ return;
172
+ case "grants list":
173
+ await grantsList(flags);
174
+ return;
175
+ case "grants show":
176
+ await grantsShow(rest[0], flags);
177
+ return;
178
+ case "grants install":
179
+ await grantsInstall(rest[0], flags);
180
+ return;
181
+ case "grants revoke":
182
+ await grantsRevoke(rest[0], flags);
183
+ return;
184
+ case "install claude-code":
185
+ case "install codex":
186
+ case "install openclaw":
187
+ await installRuntime(command, flags);
188
+ return;
189
+ case "doctor":
190
+ await doctor(flags);
191
+ return;
192
+ default:
193
+ output({
194
+ version: VERSION,
195
+ commands: [
196
+ "auth register",
197
+ "auth register --host gemini --display chat --no-wait --json",
198
+ "auth login",
199
+ "auth device start",
200
+ "auth device poll <auth_id>",
201
+ "setup --credits 100 --method alipay",
202
+ "setup --plan credit-300 --method alipay",
203
+ "setup --plan credit-300 --method alipay --host gemini --display chat --json",
204
+ "setup --credits 100 --target codex --method alipay --install-runtime",
205
+ "buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --json",
206
+ "buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --no-wait --json",
207
+ "buyer catalog search --query 企业工商 --category business_data_api --provider itpay_enterprise_data --json",
208
+ "buyer catalog get --variant var_pubg_couple_skin_cny20 --json",
209
+ "buyer cart create --variant var_pubg_couple_skin_cny20 --json",
210
+ "buyer cart create --variants var_itpay_enterprise_precise_lookup_cny05,var_itpay_enterprise_fuzzy_search_cny01 --quantities 1,1 --json",
211
+ "buyer cart show <cart_id> --json",
212
+ "buyer cart add <cart_id> --variant var_itpay_enterprise_fuzzy_search_cny01 --quantity 1 --json",
213
+ "buyer cart remove <cart_id> --line <cart_line_item_id> --json",
214
+ "buyer shelf manifest --json",
215
+ "buyer shelf snapshot --version <catalog_version> --json",
216
+ "buyer shelf delta --since <catalog_version> --json",
217
+ "buyer checkout create --cart <cart_id> --method alipay --email buyer@example.com --phone +8613800000000 --json",
218
+ "buyer checkout status <checkout_id> --json",
219
+ "buyer checkout resume <checkout_id> --json",
220
+ "buyer auth status --json",
221
+ "buyer payment wait <payment_intent_id> --json",
222
+ "buyer payment refresh-qr <payment_intent_id> --reason order-not-found --json",
223
+ "buyer deliveries list --checkout <checkout_id> --json",
224
+ "buyer deliveries show <delivery_id> --checkout <checkout_id> --json",
225
+ "buyer vault grants list --checkout <checkout_id> --json",
226
+ "buyer vault grants read <agent_read_grant_id> --json",
227
+ "buyer vault read --order <order_id> --artifact <vault_artifact_id> --json",
228
+ "account login-link --json",
229
+ "ops sandbox worker run-once --json",
230
+ "ops sandbox recover-alipay-once --json",
231
+ "ops sandbox payment query <payment_intent_id> --json",
232
+ "status --json",
233
+ "docs list --role buyer --json",
234
+ "docs show quickstart --role buyer --json",
235
+ "docs search <question> --role buyer --json",
236
+ "resume --json",
237
+ "resume --run-id <run_id> --host gemini --display none --json",
238
+ "runs list|current|show <run_id>|forget <run_id>",
239
+ "auth status",
240
+ "account show",
241
+ "account login-link",
242
+ "account set-password --password-stdin",
243
+ "plans list",
244
+ "plans show <plan>",
245
+ "checkout create --credits 100 --method alipay",
246
+ "checkout create --plan credit-300 --method alipay --idempotency-key <uuid>",
247
+ "checkout qr <checkout_id>",
248
+ "checkout open",
249
+ "checkout recover <checkout_id>",
250
+ "checkout list --limit 20",
251
+ "payment wait <checkout_id> --timeout 120",
252
+ "balance",
253
+ "usage --today --model <model>",
254
+ "grants list",
255
+ "grants show <grant_id>",
256
+ "grants install <grant_id> --target codex",
257
+ "grants revoke <grant_id>",
258
+ "keys list",
259
+ "keys rotate --grant <grant_id>",
260
+ "keys revoke --grant <grant_id>",
261
+ "token issue --grant <grant_id> --stdout",
262
+ "sync",
263
+ "skill show",
264
+ "skill show --role buyer --json",
265
+ "skill path --role buyer",
266
+ "admin orders --access-token <root_token> --new-api-user <id>",
267
+ "admin payment-events --access-token <root_token> --new-api-user <id>",
268
+ "admin outbox --access-token <root_token> --new-api-user <id>",
269
+ "admin process-outbox --access-token <root_token> --new-api-user <id>",
270
+ "admin recover-order <order_id> --access-token <root_token> --new-api-user <id>",
271
+ "install claude-code|codex|openclaw --grant <grant_id> [--offline|--no-test]",
272
+ "doctor"
273
+ ]
274
+ });
275
+ }
276
+ }
277
+
278
+ async function authRegister(flags) {
279
+ const response = await completeDeviceAuth(flags);
280
+ output(response);
281
+ }
282
+
283
+ async function setup(flags) {
284
+ return await withStateLock(async () => {
285
+ const target = flags.target || flags.runtime || "generic";
286
+ const purchase = normalizePurchaseFlags(flags, true);
287
+ const plan = purchase.plan || null;
288
+ const credits = purchase.credits || null;
289
+ const method = flags.method || "alipay";
290
+ validateLivePaymentFlags(method, flags);
291
+ const shouldInstallRuntime = Boolean(flags.install_runtime || flags.install_config || flags.write_runtime_config) && !flags.no_runtime_install && !flags.no_install;
292
+ if (shouldInstallRuntime && target === "generic") {
293
+ throw new Error("--install-runtime requires --target codex, --target claude-code, or --target openclaw");
294
+ }
295
+
296
+ let run = prepareSetupRun(flags, { target, plan, credits, method, install_runtime: shouldInstallRuntime });
297
+ const setupFlags = { ...flags, runtime: target, run_id: run.run_id };
298
+ if (shouldReturnAfterAgentTextQR(setupFlags)) {
299
+ setupFlags.no_wait_auth = true;
300
+ setupFlags.no_wait_payment = true;
301
+ }
302
+ run = updateRun(run, { phase: "checking_auth", status: "running", api_base: apiBase(setupFlags) });
303
+
304
+ const auth = await ensureAuthenticated(setupFlags);
305
+ if (!auth.authenticated) {
306
+ run = mergeRun(run, {
307
+ phase: "waiting_human_auth",
308
+ status: "waiting_human_auth",
309
+ auth: {
310
+ auth_id: auth.auth_id,
311
+ status: "pending",
312
+ expires_at: auth.expires_at
313
+ },
314
+ human_action: auth.human_action || null,
315
+ safe_summary: "Waiting for Alipay authentication scan."
316
+ });
317
+ writeRun(run);
318
+ output(agentRunResponse(run, {
319
+ status: "waiting_human_auth",
320
+ action: "scan_alipay_auth",
321
+ auth_id: auth.auth_id,
322
+ user_code: auth.user_code,
323
+ verification_uri: auth.verification_uri,
324
+ verification_uri_complete: auth.verification_uri_complete,
325
+ alipay_authorization_url: auth.alipay_authorization_url,
326
+ expires_at: auth.expires_at,
327
+ interval: auth.interval,
328
+ human_action: auth.human_action,
329
+ next_action: {
330
+ type: "show_qr_and_wait",
331
+ command: resumeCommand(run, setupFlags, { no_wait_payment: Boolean(setupFlags.no_wait_payment) }),
332
+ retry_after_ms: Number(auth.interval || 2) * 1000
333
+ }
334
+ }));
335
+ return;
336
+ }
337
+
338
+ run = mergeRun(run, {
339
+ phase: "authenticated",
340
+ status: "running",
341
+ account: {
342
+ authenticated: true,
343
+ account_id: auth.account_id,
344
+ device_id: auth.device_id,
345
+ newapi_user_id: auth.newapi_user_id || null,
346
+ session_reused: Boolean(auth.session_reused)
347
+ },
348
+ auth: {
349
+ ...(run.auth || {}),
350
+ status: "consumed"
351
+ },
352
+ human_action: null,
353
+ safe_summary: "Agent device authenticated."
354
+ });
355
+ writeRun(run);
356
+
357
+ let checkout = run.checkout?.checkout_id
358
+ ? await api(`/api/itp/checkout/${encodeURIComponent(run.checkout.checkout_id)}`, { method: "GET" }, setupFlags)
359
+ : null;
360
+ if (!checkout || isTerminalCheckoutFailure(checkout.status)) {
361
+ checkout = await createCheckoutResult({
362
+ ...setupFlags,
363
+ plan,
364
+ credits,
365
+ method,
366
+ idempotency_key: flags.idempotency_key || run.idempotency_key
367
+ });
368
+ }
369
+
370
+ run = mergeRun(run, {
371
+ phase: checkout.grant_id ? "grant_ready" : "waiting_human_payment",
372
+ status: checkout.grant_id ? "grant_ready" : "waiting_human_payment",
373
+ plan_id: checkout.plan_id || plan,
374
+ credits: checkout.credits || credits,
375
+ purchase_kind: checkout.purchase?.kind || purchase.kind,
376
+ checkout: {
377
+ checkout_id: checkout.checkout_id,
378
+ order_id: checkout.order_id,
379
+ status: checkout.status,
380
+ expires_at: checkout.expires_at,
381
+ purchase: checkout.purchase || null
382
+ },
383
+ payment: {
384
+ provider: method,
385
+ status: checkout.status
386
+ },
387
+ grant: {
388
+ ...(run.grant || {}),
389
+ grant_id: checkout.grant_id || run.grant?.grant_id || null
390
+ },
391
+ human_action: checkout.human_action || null,
392
+ safe_summary: checkout.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay payment scan."
393
+ });
394
+ writeRun(run);
395
+
396
+ if (checkout.human_action) {
397
+ await renderHumanAction(checkout.human_action, setupFlags);
398
+ } else if (checkout.payment?.cashier_url) {
399
+ process.stderr.write(`Open Alipay payment URL: ${checkout.payment.cashier_url}\n`);
400
+ }
401
+
402
+ if (!checkout.grant_id && (setupFlags.no_wait_payment || setupFlags.no_wait)) {
403
+ output(agentRunResponse(run, {
404
+ status: "waiting_human_payment",
405
+ action: "scan_alipay_payment",
406
+ account_id: auth.account_id,
407
+ device_id: auth.device_id,
408
+ checkout_id: checkout.checkout_id,
409
+ order_id: checkout.order_id,
410
+ plan_id: checkout.plan_id || plan,
411
+ credits: checkout.credits || credits,
412
+ purchase: checkout.purchase || null,
413
+ expires_at: checkout.expires_at,
414
+ payment: checkout.payment,
415
+ human_action: checkout.human_action,
416
+ next_action: checkout.next_action || {
417
+ type: "show_qr_and_wait",
418
+ command: resumeCommand(run, setupFlags),
419
+ retry_after_ms: 2000
420
+ }
421
+ }));
422
+ return;
423
+ }
424
+
425
+ const payment = checkout.grant_id
426
+ ? { status: checkout.status, checkout_id: checkout.checkout_id, order_id: checkout.order_id, grant_id: checkout.grant_id }
427
+ : await paymentWaitResult(checkout.checkout_id, setupFlags);
428
+ run = mergeRun(readRun(run.run_id) || run, {
429
+ phase: "grant_ready",
430
+ checkout: {
431
+ ...(run.checkout || {}),
432
+ checkout_id: payment.checkout_id,
433
+ order_id: payment.order_id,
434
+ status: payment.status
435
+ },
436
+ payment: {
437
+ provider: method,
438
+ status: payment.status
439
+ },
440
+ grant: {
441
+ grant_id: payment.grant_id,
442
+ installed: false
443
+ },
444
+ human_action: null,
445
+ safe_summary: "Payment verified and grant is ready."
446
+ });
447
+ writeRun(run);
448
+
449
+ const grant = await grantsInstallResult(payment.grant_id, { ...setupFlags, target });
450
+ const runtimeInstall = shouldInstallRuntime
451
+ ? await installRuntimeResult(target, {
452
+ ...setupFlags,
453
+ grant: payment.grant_id,
454
+ no_test: flags.test ? false : true
455
+ })
456
+ : {
457
+ status: "skipped",
458
+ reason: "runtime_config_install_is_opt_in",
459
+ command: target === "generic"
460
+ ? `${cliCommand("install")} <target> --grant ${shellQuote(payment.grant_id)} --json`
461
+ : cliCommand("install", target, "--grant", payment.grant_id, "--json")
462
+ };
463
+ const tokenCommand = cliCommand("token", "issue", "--grant", payment.grant_id, "--stdout");
464
+ run = mergeRun(readRun(run.run_id) || run, {
465
+ phase: shouldInstallRuntime ? "done" : "grant_ready",
466
+ status: shouldInstallRuntime ? "installed" : "grant_ready",
467
+ grant: {
468
+ grant_id: payment.grant_id,
469
+ installed: true,
470
+ credential_store: grant.credential?.credential_store || null
471
+ },
472
+ result: {
473
+ base_url: grant.base_url,
474
+ openai_base_url: grant.openai_base_url,
475
+ anthropic_base_url: grant.anthropic_base_url,
476
+ gemini_base_url: grant.gemini_base_url
477
+ },
478
+ safe_summary: shouldInstallRuntime ? "Runtime configured." : "Grant credential stored."
479
+ });
480
+ writeRun(run);
481
+
482
+ output(agentRunResponse(run, {
483
+ status: shouldInstallRuntime ? "installed" : "grant_ready",
484
+ account_id: auth.account_id,
485
+ device_id: auth.device_id,
486
+ checkout_id: checkout.checkout_id,
487
+ order_id: checkout.order_id,
488
+ plan_id: checkout.plan_id || plan,
489
+ credits: checkout.credits || credits,
490
+ purchase: checkout.purchase || null,
491
+ grant_id: payment.grant_id,
492
+ target,
493
+ base_url: grant.base_url,
494
+ openai_base_url: grant.openai_base_url,
495
+ anthropic_base_url: grant.anthropic_base_url,
496
+ gemini_base_url: grant.gemini_base_url,
497
+ credential: {
498
+ stored: true,
499
+ credential_store: grant.credential?.credential_store,
500
+ warning: grant.credential?.warning,
501
+ token_command: tokenCommand,
502
+ stdout_required_for_raw_token: true
503
+ },
504
+ auth,
505
+ checkout,
506
+ payment,
507
+ grant_install: grant,
508
+ runtime_install: runtimeInstall,
509
+ next_action: shouldInstallRuntime
510
+ ? null
511
+ : {
512
+ type: "configure_agent_optional",
513
+ token_command: tokenCommand,
514
+ runtime_install_command: runtimeInstall.command
515
+ }
516
+ }));
517
+ });
518
+ }
519
+
520
+ async function buyerBuy(flags) {
521
+ const selectionID = flags.selection || flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id;
522
+ if (!selectionID) throw new Error("catalog variant id is required, for example: itp buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --json");
523
+ const selection = await resolveBuyerCatalogSelection(selectionID, flags);
524
+ const cart = await createBuyerCart(selection, flags);
525
+ let checkout = await createBuyerCheckoutFromCart(cart, selection, flags);
526
+ if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
527
+ await renderHumanAction(checkout.human_action, flags);
528
+ if (flags.no_wait || flags.no_wait_auth) {
529
+ output(buyerRunOutput({
530
+ status: "waiting_human_auth",
531
+ selection,
532
+ cart,
533
+ checkout,
534
+ human_action: checkout.human_action,
535
+ agent_next_actions: checkout.agent_next_actions || ["wait_human_auth", "poll_checkout"],
536
+ next: {
537
+ command: cliCommand("buyer", "checkout", "resume", checkout.checkout_id, "--json"),
538
+ safe_for_agent: true,
539
+ instruction: "After presenting the first-purchase auth-to-payment QR, keep this resume command running/waiting. Do not stop at QR display unless the human explicitly asks you to pause."
540
+ }
541
+ }));
542
+ return;
543
+ }
544
+ checkout = await waitBuyerCheckoutAuth(checkout, flags);
545
+ }
546
+ const intent = checkout.payment_intent_id
547
+ ? await getBuyerPaymentIntent(checkout.payment_intent_id, flags)
548
+ : await createBuyerPaymentIntent(checkout.checkout_id, flags);
549
+ await renderItPayPaymentAction(intent, flags);
550
+
551
+ if (flags.no_wait || flags.no_wait_payment) {
552
+ output(buyerRunOutput({
553
+ status: "waiting_user_payment",
554
+ selection,
555
+ cart,
556
+ checkout,
557
+ payment_intent: intent,
558
+ agent_next_actions: intent.agent_next_actions || ["wait_payment"],
559
+ next: { command: cliCommand("buyer", "payment", "wait", intent.payment_intent_id, "--json") }
560
+ }));
561
+ return;
562
+ }
563
+
564
+ const event = await waitBuyerPayment(intent, flags);
565
+ const delivery = await waitBuyerDelivery(checkout.checkout_id, flags);
566
+ const finalCheckout = delivery.checkout || checkout;
567
+ const delivered = isBuyerDeliveryComplete(finalCheckout);
568
+ output(buyerRunOutput({
569
+ status: delivered ? "delivery_claimable" : event.event_type === "payment_intent.verified" ? "payment_verified" : "waiting_user_payment",
570
+ selection,
571
+ cart,
572
+ checkout: finalCheckout,
573
+ payment_intent: intent,
574
+ payment_event: event,
575
+ delivery: delivery.delivery || finalCheckout.delivery || null,
576
+ agent_next_actions: delivered ? deliveryAwareAgentNextActions(finalCheckout) : (finalCheckout.agent_next_actions || event.agent_next_actions || intent.agent_next_actions || ["poll_checkout"]),
577
+ optional_agent_read_grant: optionalAgentReadGrantHint(finalCheckout.checkout_id || checkout.checkout_id, finalCheckout),
578
+ next: delivered
579
+ ? { type: "human_check_email", safe_for_agent: true }
580
+ : { command: cliCommand("buyer", "checkout", "status", checkout.checkout_id, "--json"), safe_for_agent: true }
581
+ }));
582
+ }
583
+
584
+ async function buyer(command, rest, flags) {
585
+ const subcommand = rest[0] && !String(rest[0]).startsWith("--") ? rest[0] : "";
586
+ if (command === "catalog") {
587
+ if (subcommand === "search") {
588
+ const query = flags.query || flags.q || "";
589
+ const body = {
590
+ query,
591
+ filters: buyerCatalogSearchFilters(flags),
592
+ context: {},
593
+ pagination: {}
594
+ };
595
+ if (flags.currency) body.context.currency = String(flags.currency);
596
+ if (flags.page_size || flags.limit) body.pagination.limit = Number(flags.page_size || flags.limit);
597
+ if (flags.cursor) body.pagination.cursor = String(flags.cursor);
598
+ const catalog = await coreApi("/api/ucp/v1/catalog/search", { method: "POST", body }, flags);
599
+ output(buyerRunOutput({
600
+ status: "catalog_search_results",
601
+ catalog,
602
+ products: catalog.products || [],
603
+ agent_next_actions: ["choose_variant"]
604
+ }));
605
+ return;
606
+ }
607
+ if (subcommand === "get") {
608
+ const selectionID = flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id || rest[1];
609
+ const detail = await getBuyerUCPProduct(selectionID, flags);
610
+ const selection = selectionFromUCPProduct(detail, selectionID, flags);
611
+ output(buyerRunOutput({
612
+ status: "catalog_product",
613
+ product: detail.product,
614
+ messages: detail.messages || [],
615
+ selection,
616
+ agent_next_actions: ["create_cart"]
617
+ }));
618
+ return;
619
+ }
620
+ }
621
+ if (command === "cart") {
622
+ if (subcommand === "create") {
623
+ const selectionIDs = buyerCartSelectionIDs(rest, flags);
624
+ const selections = await resolveBuyerCatalogSelections(selectionIDs, flags);
625
+ const cart = await createBuyerCartFromSelections(selections, flags);
626
+ output(buyerRunOutput({
627
+ status: "cart_created",
628
+ selection: selections.length === 1 ? selections[0] : undefined,
629
+ selections,
630
+ cart,
631
+ cart_id: cart.cart_id || cart.id,
632
+ agent_next_actions: cart.agent_next_actions || ["create_checkout_from_cart"]
633
+ }));
634
+ return;
635
+ }
636
+ if (subcommand === "add") {
637
+ const cartID = flags.cart || flags.cart_id || positional(rest, 1) || readState().last_core_cart_id;
638
+ if (!cartID) throw new Error("cart_id is required");
639
+ const selectionIDs = buyerCartSelectionIDs(["add"], flags);
640
+ const selections = await resolveBuyerCatalogSelections(selectionIDs, flags);
641
+ if (selections.length !== 1) throw new Error("buyer cart add requires exactly one --variant");
642
+ const cart = await addBuyerCartLineItem(cartID, selections[0], flags);
643
+ output(buyerRunOutput({
644
+ status: "cart_updated",
645
+ selection: selections[0],
646
+ cart,
647
+ cart_id: cart.cart_id || cart.id,
648
+ agent_next_actions: cart.agent_next_actions || ["view_cart", "create_checkout_from_cart"]
649
+ }));
650
+ return;
651
+ }
652
+ if (subcommand === "remove" || subcommand === "delete") {
653
+ const cartID = flags.cart || flags.cart_id || positional(rest, 1) || readState().last_core_cart_id;
654
+ const lineID = flags.line || flags.line_id || flags.cart_line_item_id || positional(rest, 2);
655
+ if (!cartID) throw new Error("cart_id is required");
656
+ if (!lineID) throw new Error("cart_line_item_id is required; run buyer cart show <cart_id> --json first");
657
+ const cart = await removeBuyerCartLineItem(cartID, lineID, flags);
658
+ output(buyerRunOutput({
659
+ status: "cart_updated",
660
+ cart,
661
+ cart_id: cart.cart_id || cart.id,
662
+ removed_line_item_id: lineID,
663
+ agent_next_actions: cart.agent_next_actions || ["view_cart", "create_checkout_from_cart"]
664
+ }));
665
+ return;
666
+ }
667
+ if (subcommand === "show" || subcommand === "status") {
668
+ const cartID = flags.cart || flags.cart_id || positional(rest, 1) || readState().last_core_cart_id;
669
+ if (!cartID) throw new Error("cart_id is required");
670
+ const cart = await getBuyerCart(cartID, flags);
671
+ output(buyerRunOutput({
672
+ status: cart.status || "cart_ready",
673
+ cart,
674
+ cart_id: cart.cart_id || cart.id,
675
+ agent_next_actions: cart.agent_next_actions || ["create_checkout_from_cart"]
676
+ }));
677
+ return;
678
+ }
679
+ }
680
+ if (command === "shelf") {
681
+ if (subcommand === "manifest") {
682
+ output(await coreApi("/v1/public/shelf/manifest", { method: "GET" }, flags));
683
+ return;
684
+ }
685
+ if (subcommand === "snapshot") {
686
+ const version = flags.version || positional(rest, 1);
687
+ if (!version) throw new Error("snapshot version is required");
688
+ output(await coreApi(`/v1/public/shelf/snapshots/${encodeURIComponent(version)}`, { method: "GET" }, flags));
689
+ return;
690
+ }
691
+ if (subcommand === "delta") {
692
+ const since = flags.since || positional(rest, 1);
693
+ if (!since) throw new Error("delta --since version is required");
694
+ const params = new URLSearchParams({ since });
695
+ output(await coreApi(`/v1/public/shelf/delta?${params.toString()}`, { method: "GET" }, flags));
696
+ return;
697
+ }
698
+ }
699
+ if (command === "checkout") {
700
+ if (subcommand === "create") {
701
+ const cartID = flags.cart || flags.cart_id;
702
+ if (cartID) {
703
+ const cart = await getBuyerCart(cartID, flags);
704
+ const checkout = await createBuyerCheckoutFromCart(cart, null, flags);
705
+ if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
706
+ await renderHumanAction(checkout.human_action, flags);
707
+ }
708
+ output(buyerRunOutput({
709
+ status: "checkout_created",
710
+ cart,
711
+ checkout,
712
+ agent_next_actions: checkout.agent_next_actions || ["create_payment_intent"],
713
+ next: checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth"
714
+ ? {
715
+ command: cliCommand("buyer", "checkout", "resume", checkout.checkout_id, "--json"),
716
+ safe_for_agent: true,
717
+ instruction: "After presenting the first-purchase auth-to-payment QR, keep this resume command running/waiting. Do not stop at QR display unless the human explicitly asks you to pause."
718
+ }
719
+ : undefined
720
+ }));
721
+ return;
722
+ }
723
+ const selectionID = flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id;
724
+ const selection = await resolveBuyerCatalogSelection(selectionID, flags);
725
+ const cart = await createBuyerCart(selection, flags);
726
+ const checkout = await createBuyerCheckoutFromCart(cart, selection, flags);
727
+ if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
728
+ await renderHumanAction(checkout.human_action, flags);
729
+ }
730
+ output(buyerRunOutput({
731
+ status: "checkout_created",
732
+ selection,
733
+ cart,
734
+ checkout,
735
+ agent_next_actions: checkout.agent_next_actions || ["create_payment_intent"],
736
+ next: checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth"
737
+ ? {
738
+ command: cliCommand("buyer", "checkout", "resume", checkout.checkout_id, "--json"),
739
+ safe_for_agent: true,
740
+ instruction: "After presenting the first-purchase auth-to-payment QR, keep this resume command running/waiting. Do not stop at QR display unless the human explicitly asks you to pause."
741
+ }
742
+ : undefined
743
+ }));
744
+ return;
745
+ }
746
+ if (subcommand === "status") {
747
+ const checkoutID = flags.checkout || flags.checkout_id || positional(rest, 1) || readState().last_core_checkout_id;
748
+ if (!checkoutID) throw new Error("checkout_id is required");
749
+ const checkout = await getBuyerCheckout(checkoutID, flags);
750
+ await maybeClaimBuyerSessionForCheckout(checkout, flags);
751
+ output(buyerRunOutput({
752
+ status: checkout.delivery_status || checkout.status,
753
+ checkout,
754
+ delivery: checkout.delivery,
755
+ agent_next_actions: deliveryAwareAgentNextActions(checkout),
756
+ optional_agent_read_grant: optionalAgentReadGrantHint(checkout.checkout_id, checkout)
757
+ }));
758
+ return;
759
+ }
760
+ if (subcommand === "resume") {
761
+ const checkoutID = flags.checkout || flags.checkout_id || positional(rest, 1) || readState().last_core_checkout_id;
762
+ if (!checkoutID) throw new Error("checkout_id is required");
763
+ let checkout = await getBuyerCheckout(checkoutID, flags);
764
+ if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
765
+ await renderHumanAction(checkout.human_action, flags);
766
+ if (flags.no_wait || flags.no_wait_auth) {
767
+ output(buyerRunOutput({
768
+ status: "waiting_human_auth",
769
+ checkout,
770
+ human_action: checkout.human_action,
771
+ agent_next_actions: checkout.agent_next_actions || ["wait_human_auth", "poll_checkout"],
772
+ next: {
773
+ command: cliCommand("buyer", "checkout", "resume", checkoutID, "--json"),
774
+ safe_for_agent: true,
775
+ instruction: "Run this resume command and keep it active; do not stop after showing the auth-to-payment QR unless the human explicitly asks you to pause."
776
+ }
777
+ }));
778
+ return;
779
+ }
780
+ checkout = await waitBuyerCheckoutAuth(checkout, flags);
781
+ if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
782
+ output(buyerRunOutput({
783
+ status: "waiting_human_auth",
784
+ checkout,
785
+ human_action: checkout.human_action,
786
+ agent_next_actions: checkout.agent_next_actions || ["wait_human_auth", "poll_checkout"],
787
+ next: {
788
+ command: cliCommand("buyer", "checkout", "resume", checkoutID, "--json"),
789
+ safe_for_agent: true,
790
+ instruction: "Auth is still pending. Keep waiting/resuming the same checkout; do not create a new checkout."
791
+ }
792
+ }));
793
+ return;
794
+ }
795
+ }
796
+ await maybeClaimBuyerSessionForCheckout(checkout, flags);
797
+ if (checkout.payment_intent_id) {
798
+ const intent = await getBuyerPaymentIntent(checkout.payment_intent_id, flags);
799
+ await renderItPayPaymentAction(intent, flags);
800
+ output(buyerRunOutput({ status: intent.status === "verified" ? "payment_verified" : "waiting_user_payment", checkout, payment_intent: intent, agent_next_actions: intent.agent_next_actions || checkout.agent_next_actions || ["wait_payment"] }));
801
+ return;
802
+ }
803
+ if (checkout.agent_next_actions?.includes("create_payment_intent") || checkout.next_required_action === "create_payment_intent") {
804
+ const intent = await createBuyerPaymentIntent(checkout.checkout_id, flags);
805
+ await renderItPayPaymentAction(intent, flags);
806
+ output(buyerRunOutput({ status: "waiting_user_payment", checkout, payment_intent: intent, agent_next_actions: intent.agent_next_actions || ["wait_payment"] }));
807
+ return;
808
+ }
809
+ output(buyerRunOutput({
810
+ status: checkout.delivery_status || checkout.status,
811
+ checkout,
812
+ delivery: checkout.delivery,
813
+ agent_next_actions: deliveryAwareAgentNextActions(checkout),
814
+ optional_agent_read_grant: optionalAgentReadGrantHint(checkout.checkout_id, checkout)
815
+ }));
816
+ return;
817
+ }
818
+ }
819
+ if (command === "payment" && subcommand === "wait") {
820
+ const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 1) || readState().last_core_payment_intent_id;
821
+ if (!paymentIntentID) throw new Error("payment_intent_id is required");
822
+ const intent = await getBuyerPaymentIntent(paymentIntentID, flags);
823
+ const event = intent.status === "verified"
824
+ ? { event_type: "payment_intent.verified", payment_intent_id: paymentIntentID, agent_next_actions: intent.agent_next_actions || ["poll_checkout"] }
825
+ : await waitBuyerPayment(intent, flags);
826
+ output(buyerRunOutput({ status: event.event_type === "payment_intent.verified" ? "payment_verified" : "waiting_user_payment", payment_intent: intent, payment_event: event, agent_next_actions: event.agent_next_actions || intent.agent_next_actions }));
827
+ return;
828
+ }
829
+ if (command === "payment" && subcommand === "refresh-qr") {
830
+ const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 1) || readState().last_core_payment_intent_id;
831
+ if (!paymentIntentID) throw new Error("payment_intent_id is required");
832
+ const refreshed = await refreshBuyerPaymentQR(paymentIntentID, flags);
833
+ await renderItPayPaymentAction(refreshed, flags);
834
+ output(buyerRunOutput({
835
+ status: refreshed.status === "verified" ? "payment_verified" : "waiting_user_payment",
836
+ payment_intent: refreshed,
837
+ agent_next_actions: refreshed.agent_next_actions || ["wait_payment"],
838
+ next: refreshed.status === "verified"
839
+ ? { command: cliCommand("buyer", "checkout", "status", refreshed.checkout_id, "--json"), safe_for_agent: true }
840
+ : { command: cliCommand("buyer", "payment", "wait", refreshed.payment_intent_id, "--json"), safe_for_agent: true }
841
+ }));
842
+ return;
843
+ }
844
+ if (command === "deliveries") {
845
+ if (subcommand === "list") {
846
+ const checkoutID = flags.checkout || flags.checkout_id || readState().last_core_checkout_id;
847
+ if (!checkoutID) throw new Error("--checkout is required");
848
+ const checkout = await getBuyerCheckout(checkoutID, flags);
849
+ output(buyerDeliveryListOutput(checkout));
850
+ return;
851
+ }
852
+ if (subcommand === "show") {
853
+ const checkoutID = flags.checkout || flags.checkout_id || readState().last_core_checkout_id;
854
+ if (!checkoutID) throw new Error("--checkout is required for agent-safe delivery status");
855
+ const checkout = await getBuyerCheckout(checkoutID, flags);
856
+ output({
857
+ schema_version: "itp.buyer.v1",
858
+ status: checkout.delivery?.status || checkout.delivery_status,
859
+ delivery_id: positional(rest, 1) || flags.delivery || flags.delivery_id || null,
860
+ checkout_id: checkout.checkout_id,
861
+ delivery: checkout.delivery || null,
862
+ agent_next_actions: deliveryAwareAgentNextActions(checkout),
863
+ optional_agent_read_grant: optionalAgentReadGrantHint(checkout.checkout_id, checkout),
864
+ secrets: { raw_content_included: false, claim_token_included: false }
865
+ });
866
+ return;
867
+ }
868
+ }
869
+ if (command === "vault") {
870
+ if (subcommand === "grants") {
871
+ const action = rest[1] && !String(rest[1]).startsWith("--") ? rest[1] : "list";
872
+ if (action === "list") {
873
+ const grants = await listBuyerAgentReadGrants(flags);
874
+ output(buyerRunOutput({
875
+ status: "agent_read_grants",
876
+ ...grants,
877
+ agent_next_actions: grants.agent_readable_grants?.length ? ["read_agent_grant_view"] : ["wait_for_human_agent_read_grant"]
878
+ }));
879
+ return;
880
+ }
881
+ if (action === "read" || action === "show") {
882
+ const grantID = flags.grant || flags.grant_id || flags.agent_read_grant_id || positional(rest, 2);
883
+ if (!grantID) throw new Error("agent_read_grant_id is required");
884
+ const view = await readBuyerAgentReadGrant(grantID, flags);
885
+ output(buyerRunOutput({
886
+ status: "agent_read_grant_view",
887
+ grant: view,
888
+ agent_next_actions: ["use_human_approved_fields_only"]
889
+ }));
890
+ return;
891
+ }
892
+ }
893
+ if (subcommand === "read") {
894
+ const view = await readBuyerVaultArtifactGrant(flags);
895
+ output(buyerRunOutput({
896
+ status: "agent_read_grant_view",
897
+ grant: view,
898
+ agent_next_actions: ["use_human_approved_fields_only"]
899
+ }));
900
+ return;
901
+ }
902
+ }
903
+ if (command === "account" && subcommand === "login-link") {
904
+ await accountLoginLink(flags);
905
+ return;
906
+ }
907
+ if (command === "auth" && subcommand === "status") {
908
+ output(await buyerAuthStatusOutput(flags));
909
+ return;
910
+ }
911
+ throw new Error(`unknown buyer command: ${[command, subcommand].filter(Boolean).join(" ") || ""}`);
912
+ }
913
+
914
+ function buyerCatalogSearchFilters(flags = {}) {
915
+ const filters = {};
916
+ const categories = csvValues(flags.category || flags.categories);
917
+ if (categories.length) filters.categories = categories;
918
+
919
+ const mappings = [
920
+ ["service_type", "ai.itpay.service_type"],
921
+ ["delivery_method", "ai.itpay.delivery_method"],
922
+ ["provider", "ai.itpay.provider"],
923
+ ["provider_product_id", "ai.itpay.provider_product_id"],
924
+ ["provider_product", "ai.itpay.provider_product_id"],
925
+ ["sensitivity_level", "ai.itpay.sensitivity_level"],
926
+ ["sensitivity", "ai.itpay.sensitivity_level"],
927
+ ["delivery_mode", "ai.itpay.delivery_mode"],
928
+ ["settlement_group", "ai.itpay.settlement_group"]
929
+ ];
930
+ for (const [flagName, filterName] of mappings) {
931
+ if (flags[flagName]) filters[filterName] = String(flags[flagName]);
932
+ }
933
+
934
+ const listMappings = [
935
+ ["use_case", "ai.itpay.taxonomy.use_cases"],
936
+ ["use_cases", "ai.itpay.taxonomy.use_cases"],
937
+ ["input_facet", "ai.itpay.taxonomy.input_facets"],
938
+ ["input_facets", "ai.itpay.taxonomy.input_facets"],
939
+ ["output_facet", "ai.itpay.taxonomy.output_facets"],
940
+ ["output_facets", "ai.itpay.taxonomy.output_facets"],
941
+ ["required_profile_field", "ai.itpay.required_profile_fields"],
942
+ ["required_profile_fields", "ai.itpay.required_profile_fields"],
943
+ ["agent_runtime", "ai.itpay.agent_runtimes"],
944
+ ["agent_runtimes", "ai.itpay.agent_runtimes"]
945
+ ];
946
+ for (const [flagName, filterName] of listMappings) {
947
+ const values = csvValues(flags[flagName]);
948
+ if (values.length) filters[filterName] = values;
949
+ }
950
+
951
+ const boolMappings = [
952
+ ["payment_qr_mpm", "ai.itpay.payment.qr_mpm"],
953
+ ["merchant_verified", "ai.itpay.merchant_verified"],
954
+ ["requires_human_input", "ai.itpay.requires_human_input"],
955
+ ["requires_webauthn_reveal", "ai.itpay.requires_webauthn_reveal"],
956
+ ["agent_may_execute_query", "ai.itpay.agent_may_execute_query"],
957
+ ["agent_may_view_raw_result", "ai.itpay.agent_may_view_raw_result"]
958
+ ];
959
+ for (const [flagName, filterName] of boolMappings) {
960
+ if (flags[flagName] !== undefined) filters[filterName] = booleanFlag(flags[flagName]);
961
+ }
962
+
963
+ const hasMin = flags.price_min !== undefined || flags.min_price !== undefined;
964
+ const hasMax = flags.price_max !== undefined || flags.max_price !== undefined;
965
+ const min = hasMin ? Number(flags.price_min ?? flags.min_price) : NaN;
966
+ const max = hasMax ? Number(flags.price_max ?? flags.max_price) : NaN;
967
+ if (Number.isFinite(min) || Number.isFinite(max)) {
968
+ filters.price = {};
969
+ if (Number.isFinite(min)) filters.price.min = min;
970
+ if (Number.isFinite(max)) filters.price.max = max;
971
+ }
972
+ return filters;
973
+ }
974
+
975
+ function csvValues(value) {
976
+ if (value === undefined || value === null || value === false) return [];
977
+ if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean);
978
+ return String(value).split(",").map((item) => item.trim()).filter(Boolean);
979
+ }
980
+
981
+ function booleanFlag(value) {
982
+ if (value === true || value === false) return value;
983
+ const normalized = String(value).trim().toLowerCase();
984
+ if (["1", "true", "yes", "y", "on"].includes(normalized)) return true;
985
+ if (["0", "false", "no", "n", "off"].includes(normalized)) return false;
986
+ throw new Error(`invalid boolean flag value: ${value}`);
987
+ }
988
+
989
+ async function ops(command, rest, flags) {
990
+ if (command !== "sandbox") throw new Error(`unknown ops command: ${command || ""}`);
991
+ const area = rest[0];
992
+ const action = rest[1];
993
+ if (area === "worker" && action === "run-once") {
994
+ output(await coreApi("/v1/sandbox/workers/run-once", { method: "POST", ops: true }, flags));
995
+ return;
996
+ }
997
+ if (area === "recover-alipay-once") {
998
+ output(await coreApi("/v1/local/workers/recover-alipay-sandbox-once", { method: "POST", ops: true }, flags));
999
+ return;
1000
+ }
1001
+ if (area === "payment" && action === "query") {
1002
+ const paymentIntentID = flags.payment_intent || flags.payment_intent_id || positional(rest, 2);
1003
+ if (!paymentIntentID) throw new Error("payment_intent_id is required");
1004
+ output(await coreApi(`/v1/payment-intents/${encodeURIComponent(paymentIntentID)}/alipay-sandbox-query`, { method: "POST", ops: true }, flags));
1005
+ return;
1006
+ }
1007
+ throw new Error(`unknown ops sandbox command: ${rest.join(" ")}`);
1008
+ }
1009
+
1010
+ async function resolveBuyerCatalogSelection(selectionID, flags = {}) {
1011
+ if (!selectionID) throw new Error("catalog selection id is required");
1012
+ const detail = await getBuyerUCPProduct(selectionID, flags);
1013
+ return selectionFromUCPProduct(detail, selectionID, flags);
1014
+ }
1015
+
1016
+ async function resolveBuyerCatalogSelections(selectionIDs, flags = {}) {
1017
+ const ids = selectionIDs.map((id) => String(id).trim()).filter(Boolean);
1018
+ if (!ids.length) throw new Error("at least one catalog variant id is required");
1019
+ const selections = [];
1020
+ for (const id of ids) {
1021
+ selections.push(await resolveBuyerCatalogSelection(id, flags));
1022
+ }
1023
+ return selections;
1024
+ }
1025
+
1026
+ async function getBuyerUCPProduct(selectionID, flags = {}) {
1027
+ if (!selectionID) throw new Error("catalog selection id is required");
1028
+ const body = {
1029
+ id: String(selectionID),
1030
+ filters: {},
1031
+ context: {}
1032
+ };
1033
+ if (flags.currency) body.context.currency = String(flags.currency);
1034
+ return await coreApi("/api/ucp/v1/catalog/product", { method: "POST", body }, flags);
1035
+ }
1036
+
1037
+ function selectionFromUCPProduct(detail, selectionID, flags = {}) {
1038
+ const product = detail?.product;
1039
+ if (!product) throw new Error(`catalog selection not found: ${selectionID}`);
1040
+ const variants = Array.isArray(product.variants) ? product.variants : [];
1041
+ const selectedVariantID = product.selected?.variant_id || selectionID;
1042
+ const variant = variants.find((candidate) => candidate.id === selectionID) ||
1043
+ variants.find((candidate) => candidate.id === selectedVariantID) ||
1044
+ variants[0];
1045
+ if (!variant) throw new Error(`catalog product has no variants: ${selectionID}`);
1046
+ const metadata = {
1047
+ ...(product.metadata || {}),
1048
+ ...(variant.metadata || {})
1049
+ };
1050
+ const requiredFields = Array.isArray(metadata["ai.itpay.required_profile_fields"])
1051
+ ? metadata["ai.itpay.required_profile_fields"].map((field) => String(field)).filter(Boolean)
1052
+ : [];
1053
+ return {
1054
+ catalog_item_id: product.id,
1055
+ catalog_variant_id: variant.id,
1056
+ ucp_variant_id: variant.id,
1057
+ offer_id: flags.offer || flags.offer_id || metadata["ai.itpay.offer_id"] || "",
1058
+ catalog_version: metadata["ai.itpay.catalog_version"] || product.selected?.catalog_version || "",
1059
+ expected_amount: Number(flags.expected_amount || variant.price?.amount || 0),
1060
+ currency: flags.currency || variant.price?.currency || metadata.currency || "",
1061
+ title: product.title,
1062
+ description: product.description || variant.description || "",
1063
+ variant_title: variant.title,
1064
+ required_contact_fields: requiredFields,
1065
+ product,
1066
+ variant,
1067
+ metadata,
1068
+ purchasable: variant.availability?.available !== false
1069
+ };
1070
+ }
1071
+
1072
+ async function createBuyerCart(selection, flags = {}) {
1073
+ return await createBuyerCartFromSelections([selection], flags);
1074
+ }
1075
+
1076
+ async function createBuyerCartFromSelections(selections, flags = {}) {
1077
+ if (!Array.isArray(selections) || !selections.length) throw new Error("at least one catalog selection is required");
1078
+ const quantities = buyerCartQuantities(selections.length, flags);
1079
+ const lineItems = selections.map((selection, index) => {
1080
+ if (!selection?.purchasable) throw new Error(`catalog variant is not purchasable: ${selection?.catalog_variant_id || selection?.ucp_variant_id || index}`);
1081
+ return {
1082
+ item: { id: selection.catalog_variant_id || selection.ucp_variant_id },
1083
+ quantity: quantities[index],
1084
+ input: buyerLineInputForSelection(selection, flags, index)
1085
+ };
1086
+ });
1087
+ const currencies = new Set(selections.map((selection) => String(flags.currency || selection.currency || "")).filter(Boolean));
1088
+ if (currencies.size > 1) {
1089
+ throw new Error(`selected variants have different currencies: ${Array.from(currencies).join(", ")}`);
1090
+ }
1091
+ const currency = flags.currency || selections[0]?.currency || "";
1092
+ const body = {
1093
+ line_items: lineItems,
1094
+ context: {},
1095
+ client_reference_id: flags.cart_client_reference_id || flags.client_reference_id || `cli_cart_${Date.now()}`
1096
+ };
1097
+ if (currency) body.context.currency = String(currency);
1098
+ const cart = await coreApi("/api/ucp/v1/carts", {
1099
+ method: "POST",
1100
+ idempotencyKey: flags.cart_idempotency_key || `idem_cli_cart_${cryptoRandom()}`,
1101
+ body
1102
+ }, flags);
1103
+ writeState({ ...readState(), last_core_cart_id: cart.cart_id || cart.id });
1104
+ return cart;
1105
+ }
1106
+
1107
+ function buyerCartSelectionIDs(rest, flags = {}) {
1108
+ const raw = flags.variants || flags.variant_ids || flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id || positional(rest, 1);
1109
+ if (!raw) return [];
1110
+ if (Array.isArray(raw)) return raw.flatMap((value) => splitCSV(value));
1111
+ return splitCSV(raw);
1112
+ }
1113
+
1114
+ function buyerCartQuantities(count, flags = {}) {
1115
+ const raw = flags.quantities || flags.quantity || flags.qty;
1116
+ const values = raw === undefined || raw === null || raw === true
1117
+ ? []
1118
+ : splitCSV(raw).map((value) => Number(value));
1119
+ if (values.length && values.length !== count) {
1120
+ throw new Error(`--quantities must provide ${count} value(s), got ${values.length}`);
1121
+ }
1122
+ const quantities = values.length ? values : Array(count).fill(1);
1123
+ for (const quantity of quantities) {
1124
+ if (!Number.isInteger(quantity) || quantity <= 0) {
1125
+ throw new Error(`cart quantity must be a positive integer, got ${quantity}`);
1126
+ }
1127
+ }
1128
+ return quantities;
1129
+ }
1130
+
1131
+ function buyerLineInputForSelection(selection, flags = {}, index = 0) {
1132
+ const input = parseBuyerInputs(flags, index);
1133
+ const providerProductID = String(selection?.metadata?.["ai.itpay.provider_product_id"] || selection?.variant?.metadata?.["ai.itpay.provider_product_id"] || "");
1134
+ if (providerProductID === "81api_company_fuzzy_search") {
1135
+ if (!input.company_name && flags.company_name) input.company_name = String(flags.company_name).trim();
1136
+ if (!input.company_name && flags.keyword) input.company_name = String(flags.keyword).trim();
1137
+ if (!input.PageNum && flags.page_num) input.PageNum = String(flags.page_num).trim();
1138
+ if (!input.PageNum) input.PageNum = "1";
1139
+ if (!input.company_name) {
1140
+ throw new Error("企业工商数据模糊查询 requires --input company_name=<关键词> or --company-name <关键词>. Ask the user for a company keyword/short name before checkout.");
1141
+ }
1142
+ }
1143
+ if (providerProductID === "81api_company_base_info") {
1144
+ if (!input.company_name_or_credit_no && flags.company_name_or_credit_no) input.company_name_or_credit_no = String(flags.company_name_or_credit_no).trim();
1145
+ if (!input.company_name_or_credit_no && flags.company_name) input.company_name_or_credit_no = String(flags.company_name).trim();
1146
+ if (!input.isRaiseErrorCode && flags.is_raise_error_code !== undefined) input.isRaiseErrorCode = String(flags.is_raise_error_code).trim();
1147
+ if (!input.isRaiseErrorCode) input.isRaiseErrorCode = "0";
1148
+ if (!input.company_name_or_credit_no) {
1149
+ throw new Error("企业工商数据精准查询 requires --input company_name_or_credit_no=<完整企业名称或统一社会信用代码>. If the user only gave a brand/short name, run fuzzy search first or resolve the exact registered company name before checkout.");
1150
+ }
1151
+ }
1152
+ return input;
1153
+ }
1154
+
1155
+ function parseBuyerInputs(flags = {}, index = 0) {
1156
+ const input = {};
1157
+ const rawValues = [];
1158
+ for (const key of ["input", "inputs"]) {
1159
+ const raw = flags[key];
1160
+ if (Array.isArray(raw)) rawValues.push(...raw);
1161
+ else if (raw !== undefined && raw !== true) rawValues.push(raw);
1162
+ }
1163
+ for (const raw of rawValues) {
1164
+ for (const part of splitInputParts(raw)) {
1165
+ const eq = part.indexOf("=");
1166
+ if (eq <= 0) throw new Error(`invalid --input ${part}; expected key=value`);
1167
+ const key = part.slice(0, eq).trim();
1168
+ const value = part.slice(eq + 1).trim();
1169
+ if (key) input[key] = value;
1170
+ }
1171
+ }
1172
+ const indexed = flags[`input_${index + 1}`] || flags[`inputs_${index + 1}`];
1173
+ if (indexed && indexed !== true) {
1174
+ for (const part of splitInputParts(indexed)) {
1175
+ const eq = part.indexOf("=");
1176
+ if (eq <= 0) throw new Error(`invalid indexed input ${part}; expected key=value`);
1177
+ input[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
1178
+ }
1179
+ }
1180
+ return input;
1181
+ }
1182
+
1183
+ function splitInputParts(raw) {
1184
+ const text = String(raw || "").trim();
1185
+ if (!text) return [];
1186
+ if (text.startsWith("{")) {
1187
+ const parsed = JSON.parse(text);
1188
+ return Object.entries(parsed).map(([key, value]) => `${key}=${value}`);
1189
+ }
1190
+ return text.split(",").map((part) => part.trim()).filter(Boolean);
1191
+ }
1192
+
1193
+ function splitCSV(value) {
1194
+ return String(value || "")
1195
+ .split(",")
1196
+ .map((part) => part.trim())
1197
+ .filter(Boolean);
1198
+ }
1199
+
1200
+ async function getBuyerCart(cartID, flags = {}) {
1201
+ if (!cartID) throw new Error("cart_id is required");
1202
+ return await coreApi(`/api/ucp/v1/carts/${encodeURIComponent(cartID)}`, { method: "GET" }, flags);
1203
+ }
1204
+
1205
+ async function addBuyerCartLineItem(cartID, selection, flags = {}) {
1206
+ if (!cartID) throw new Error("cart_id is required");
1207
+ if (!selection?.purchasable) throw new Error(`catalog variant is not purchasable: ${selection?.catalog_variant_id || selection?.ucp_variant_id || ""}`);
1208
+ const quantity = buyerCartQuantities(1, flags)[0];
1209
+ const body = {
1210
+ item: { id: selection.catalog_variant_id || selection.ucp_variant_id },
1211
+ quantity,
1212
+ input: buyerLineInputForSelection(selection, flags, 0)
1213
+ };
1214
+ const cart = await coreApi(`/api/ucp/v1/carts/${encodeURIComponent(cartID)}/line-items`, {
1215
+ method: "POST",
1216
+ idempotencyKey: flags.cart_idempotency_key || `idem_cli_cart_add_${cryptoRandom()}`,
1217
+ body
1218
+ }, flags);
1219
+ writeState({ ...readState(), last_core_cart_id: cart.cart_id || cart.id });
1220
+ return cart;
1221
+ }
1222
+
1223
+ async function removeBuyerCartLineItem(cartID, lineID, flags = {}) {
1224
+ if (!cartID) throw new Error("cart_id is required");
1225
+ if (!lineID) throw new Error("cart_line_item_id is required");
1226
+ const cart = await coreApi(`/api/ucp/v1/carts/${encodeURIComponent(cartID)}/line-items/${encodeURIComponent(lineID)}`, {
1227
+ method: "DELETE"
1228
+ }, flags);
1229
+ writeState({ ...readState(), last_core_cart_id: cart.cart_id || cart.id });
1230
+ return cart;
1231
+ }
1232
+
1233
+ async function createBuyerCheckoutFromCart(cart, selection = null, flags = {}) {
1234
+ const cartID = typeof cart === "string" ? cart : (cart?.cart_id || cart?.id);
1235
+ if (!cartID) throw new Error("cart_id is required");
1236
+ const deliveryContact = {};
1237
+ if (flags.email) deliveryContact.email = flags.email;
1238
+ if (flags.phone) deliveryContact.phone = flags.phone;
1239
+ const missing = requiredDeliveryContactFields(selection).filter((field) => !deliveryContact[field]);
1240
+ if (missing.length) {
1241
+ throw new Error(`missing required delivery contact: ${missing.join(", ")}; provide ${missing.map((field) => `--${field} <value>`).join(" ")}`);
1242
+ }
1243
+ const request = {
1244
+ method: "POST",
1245
+ idempotencyKey: flags.checkout_idempotency_key || flags.idempotency_key || `idem_cli_checkout_${cartID}`,
1246
+ body: {
1247
+ cart_id: cartID,
1248
+ client_reference_id: flags.checkout_client_reference_id || flags.client_reference_id || `cli_checkout_${cartID}`,
1249
+ delivery_contact: deliveryContact
1250
+ }
1251
+ };
1252
+ let checkout;
1253
+ try {
1254
+ checkout = await coreApi("/api/ucp/v1/checkouts", request, flags);
1255
+ } catch (error) {
1256
+ if (error?.status === 401 && readSessionToken(readCredentials()) && !flags.access_token) {
1257
+ writeCredentials(deleteSessionCredential(readCredentials()));
1258
+ checkout = await coreApi("/api/ucp/v1/checkouts", request, flags);
1259
+ } else {
1260
+ throw error;
1261
+ }
1262
+ }
1263
+ writeState({ ...readState(), last_core_cart_id: cartID, last_core_checkout_id: checkout.checkout_id });
1264
+ rememberCoreAuthAction(checkout.checkout_id, checkout.human_action);
1265
+ return checkout;
1266
+ }
1267
+
1268
+ function requiredDeliveryContactFields(selection) {
1269
+ if (Array.isArray(selection?.required_contact_fields)) {
1270
+ return selection.required_contact_fields.map((field) => String(field).trim()).filter(Boolean);
1271
+ }
1272
+ const fields = selection?.delivery?.requires_contact_fields;
1273
+ return Array.isArray(fields) ? fields.map((field) => String(field).trim()).filter(Boolean) : [];
1274
+ }
1275
+
1276
+ async function createBuyerPaymentIntent(checkoutID, flags = {}) {
1277
+ if (!checkoutID) throw new Error("checkout_id is required");
1278
+ const method = String(flags.method || flags.payment_method || "alipay").toLowerCase();
1279
+ const provider = String(flags.provider || flags.preferred_provider || method).toLowerCase();
1280
+ const intent = await coreApi(`/v1/checkouts/${encodeURIComponent(checkoutID)}/payment-intents`, {
1281
+ method: "POST",
1282
+ idempotencyKey: flags.payment_idempotency_key || flags.idempotency_key || `idem_cli_payment_${cryptoRandom()}`,
1283
+ body: {
1284
+ payment_method_type: method,
1285
+ preferred_provider: provider
1286
+ }
1287
+ }, flags);
1288
+ writeState({ ...readState(), last_core_checkout_id: checkoutID, last_core_payment_intent_id: intent.payment_intent_id });
1289
+ return intent;
1290
+ }
1291
+
1292
+ async function waitBuyerCheckoutAuth(checkout, flags = {}) {
1293
+ const checkoutID = checkout?.checkout_id || checkout;
1294
+ if (!checkoutID) throw new Error("checkout_id is required");
1295
+ const timeoutMs = Number(flags.auth_timeout || flags.timeout || 900) * 1000;
1296
+ const started = Date.now();
1297
+ let lastHeartbeatAt = 0;
1298
+ let current = typeof checkout === "string" ? await getBuyerCheckout(checkoutID, flags) : checkout;
1299
+ const authAction = current?.human_action || readCoreAuthAction(checkoutID) || null;
1300
+ rememberCoreAuthAction(checkoutID, authAction);
1301
+ while (Date.now() - started < timeoutMs) {
1302
+ if (current.payment_intent_id || current.identity_status === "identity_resolved" || current.next_required_action !== "auth_qr") {
1303
+ await maybeClaimBuyerSessionFromAuthAction(authAction, flags);
1304
+ return current;
1305
+ }
1306
+ lastHeartbeatAt = writeWaitHeartbeat({
1307
+ kind: "ItPay buyer auth",
1308
+ idName: "checkout_id",
1309
+ idValue: checkoutID,
1310
+ status: current.identity_status || current.next_required_action || "waiting_human_auth",
1311
+ action: current.human_action || null,
1312
+ lastHeartbeatAt,
1313
+ flags,
1314
+ command: cliCommand("buyer", "checkout", "resume", checkoutID, "--json")
1315
+ });
1316
+ await sleep(Number(flags.auth_poll_ms || flags.poll_ms || 2000));
1317
+ current = await getBuyerCheckout(checkoutID, flags);
1318
+ }
1319
+ await maybeClaimBuyerSessionFromAuthAction(authAction, flags);
1320
+ return current;
1321
+ }
1322
+
1323
+ async function maybeClaimBuyerSessionFromAuthAction(action, flags = {}) {
1324
+ const parsed = parseBuyerAuthActionURL(action);
1325
+ if (!parsed.authSessionID || !parsed.displayToken) return null;
1326
+ try {
1327
+ const response = await coreApi(`/v1/buyer/auth-sessions/${encodeURIComponent(parsed.authSessionID)}/agent-session?display_token=${encodeURIComponent(parsed.displayToken)}`, {
1328
+ method: "POST"
1329
+ }, flags);
1330
+ const rawToken = response.raw_session_token || response.session_token || response.session?.raw_session_token;
1331
+ if (!rawToken) return response;
1332
+ writeSessionCredentials({
1333
+ account_id: response.buyer_account_id,
1334
+ device_id: response.agent_device_id,
1335
+ session_token: rawToken
1336
+ });
1337
+ writeConfig({
1338
+ api_base: coreApiBase(flags),
1339
+ account_id: response.buyer_account_id,
1340
+ device_id: response.agent_device_id
1341
+ });
1342
+ forgetCoreAuthAction(response.checkout_id);
1343
+ return response;
1344
+ } catch (error) {
1345
+ if (!flags.quiet) {
1346
+ process.stderr.write(`ItPay buyer session claim skipped: ${safeErrorMessage(error)}\n`);
1347
+ }
1348
+ return null;
1349
+ }
1350
+ }
1351
+
1352
+ async function maybeClaimBuyerSessionForCheckout(checkout, flags = {}) {
1353
+ if (!checkout?.checkout_id) return null;
1354
+ if (readSessionToken()) return null;
1355
+ if (checkout.identity_status !== "identity_resolved" && !checkout.payment_intent_id) return null;
1356
+ const action = checkout.human_action || readCoreAuthAction(checkout.checkout_id);
1357
+ return await maybeClaimBuyerSessionFromAuthAction(action, flags);
1358
+ }
1359
+
1360
+ function parseBuyerAuthActionURL(action) {
1361
+ const rawURL = action?.url || action?.auth_url || "";
1362
+ if (!rawURL) return {};
1363
+ try {
1364
+ const parsed = new URL(rawURL);
1365
+ const match = parsed.pathname.match(/\/v1\/buyer\/auth-sessions\/([^/]+)$/);
1366
+ return {
1367
+ authSessionID: match ? decodeURIComponent(match[1]) : "",
1368
+ displayToken: parsed.searchParams.get("display_token") || ""
1369
+ };
1370
+ } catch {
1371
+ return {};
1372
+ }
1373
+ }
1374
+
1375
+ function rememberCoreAuthAction(checkoutID, action) {
1376
+ if (!checkoutID || action?.kind !== "auth_qr") return;
1377
+ const parsed = parseBuyerAuthActionURL(action);
1378
+ if (!parsed.authSessionID || !parsed.displayToken) return;
1379
+ const state = readState();
1380
+ const existing = state.core_auth_actions && typeof state.core_auth_actions === "object" ? state.core_auth_actions : {};
1381
+ const entries = Object.entries(existing).slice(-19);
1382
+ const next = Object.fromEntries(entries);
1383
+ next[checkoutID] = {
1384
+ kind: "auth_qr",
1385
+ id: action.id || action.auth_session_id || parsed.authSessionID,
1386
+ auth_session_id: action.auth_session_id || parsed.authSessionID,
1387
+ url: action.url,
1388
+ web_url: action.web_url || action.url,
1389
+ expires_at: action.expires_at || null,
1390
+ saved_at: new Date().toISOString()
1391
+ };
1392
+ writeState({ ...state, core_auth_actions: next, last_core_auth_checkout_id: checkoutID });
1393
+ }
1394
+
1395
+ function readCoreAuthAction(checkoutID) {
1396
+ if (!checkoutID) return null;
1397
+ const state = readState();
1398
+ return state.core_auth_actions?.[checkoutID] || null;
1399
+ }
1400
+
1401
+ function forgetCoreAuthAction(checkoutID) {
1402
+ if (!checkoutID) return;
1403
+ const state = readState();
1404
+ if (!state.core_auth_actions?.[checkoutID]) return;
1405
+ const next = { ...state.core_auth_actions };
1406
+ delete next[checkoutID];
1407
+ writeState({ ...state, core_auth_actions: next });
1408
+ }
1409
+
1410
+ async function getBuyerCheckout(checkoutID, flags = {}) {
1411
+ return await coreApi(`/v1/checkouts/${encodeURIComponent(checkoutID)}`, { method: "GET" }, flags);
1412
+ }
1413
+
1414
+ async function getBuyerPaymentIntent(paymentIntentID, flags = {}) {
1415
+ return await coreApi(`/v1/payment-intents/${encodeURIComponent(paymentIntentID)}`, { method: "GET" }, flags);
1416
+ }
1417
+
1418
+ async function refreshBuyerPaymentQR(paymentIntentID, flags = {}) {
1419
+ const intent = await getBuyerPaymentIntent(paymentIntentID, flags);
1420
+ if (intent.status === "verified") return intent;
1421
+ const refreshURL = intent.qr_refresh_url;
1422
+ if (!refreshURL) {
1423
+ throw new Error("payment intent does not expose qr_refresh_url; refresh is supported only for refreshable Alipay sandbox QR intents");
1424
+ }
1425
+ return await coreApi(refreshURL, {
1426
+ method: "POST",
1427
+ body: {
1428
+ reason: normalizeQRRefreshReasonForCLI(flags.reason || flags.refresh_reason || "order_not_found")
1429
+ }
1430
+ }, flags);
1431
+ }
1432
+
1433
+ function normalizeQRRefreshReasonForCLI(reason) {
1434
+ const normalized = String(reason || "").trim().toLowerCase().replaceAll("-", "_");
1435
+ if (["order_not_found", "qr_unavailable", "manual_refresh", "human_open"].includes(normalized)) return normalized;
1436
+ return "manual_refresh";
1437
+ }
1438
+
1439
+ async function renderItPayPaymentAction(intent, flags = {}) {
1440
+ const action = intent?.human_action ? { ...intent.human_action } : (intent?.payment_url ? {
1441
+ id: intent.payment_intent_id,
1442
+ title: "Scan with Alipay",
1443
+ url: intent.payment_url,
1444
+ expires_at: intent.qr?.expires_at
1445
+ } : null);
1446
+ if (action) {
1447
+ if (intent?.qr_png_url || intent?.qr?.png_url) {
1448
+ action.qr_png_url = intent.qr_png_url || intent.qr.png_url;
1449
+ }
1450
+ if (intent?.mobile_wallet_url) {
1451
+ action.mobile_wallet_url = intent.mobile_wallet_url;
1452
+ }
1453
+ }
1454
+ if (action && (intent?.qr_image_url || intent?.qr?.image_url)) {
1455
+ action.qr_image_url = intent.qr_image_url || intent.qr.image_url;
1456
+ action.display_mode = intent.qr?.scan_mode || "itpay_entry_qr";
1457
+ action.description = action.description || "Scan the ItPay payment entry QR; ItPay will safely hand off to Alipay.";
1458
+ }
1459
+ const result = await renderHumanAction(action, flags);
1460
+ if (intent && action) {
1461
+ intent.human_action = { ...(intent.human_action || {}), ...action };
1462
+ if (action.local_qr_path) intent.local_qr_path = action.local_qr_path;
1463
+ if (action.preferred_qr_url) intent.preferred_qr_url = action.preferred_qr_url;
1464
+ if (action.mobile_wallet_url) intent.mobile_wallet_url = action.mobile_wallet_url;
1465
+ }
1466
+ return result;
1467
+ }
1468
+
1469
+ async function waitBuyerPayment(intent, flags = {}) {
1470
+ const paymentIntentID = intent?.payment_intent_id || intent;
1471
+ if (!paymentIntentID) throw new Error("payment_intent_id is required");
1472
+ let cursor = flags.cursor || intent?.agent_wait?.cursor || "";
1473
+ const waitURL = flags.wait_url || intent?.agent_wait?.wait_url || `/v1/payment-intents/${encodeURIComponent(paymentIntentID)}/events/wait`;
1474
+ const timeoutMs = Number(flags.timeout || 900) * 1000;
1475
+ const started = Date.now();
1476
+ let lastHeartbeatAt = 0;
1477
+ let lastEvent = null;
1478
+ while (Date.now() - started < timeoutMs) {
1479
+ const params = new URLSearchParams();
1480
+ if (cursor) params.set("cursor", cursor);
1481
+ params.set("timeout", String(flags.poll_timeout || "30s"));
1482
+ const event = await coreApi(appendURLQuery(waitURL, params), { method: "GET" }, flags);
1483
+ lastEvent = event;
1484
+ cursor = event.cursor || cursor;
1485
+ if (event.event_type === "payment_intent.verified") return event;
1486
+ if (event.event_type && event.event_type !== "wait.timeout") return event;
1487
+ lastHeartbeatAt = writeWaitHeartbeat({
1488
+ kind: "ItPay payment notify",
1489
+ idName: "payment_intent_id",
1490
+ idValue: paymentIntentID,
1491
+ status: event.event_type === "wait.timeout" ? "still_waiting" : (event.event_type || "still_waiting"),
1492
+ action: intent?.human_action || null,
1493
+ lastHeartbeatAt,
1494
+ flags,
1495
+ command: cliCommand("buyer", "payment", "wait", paymentIntentID, "--json")
1496
+ });
1497
+ }
1498
+ return lastEvent || { event_type: "wait.timeout", payment_intent_id: paymentIntentID, cursor, agent_next_actions: ["wait_payment"] };
1499
+ }
1500
+
1501
+ async function waitBuyerDelivery(checkoutID, flags = {}) {
1502
+ const timeoutMs = Number(flags.delivery_timeout || 30) * 1000;
1503
+ const started = Date.now();
1504
+ let checkout = await getBuyerCheckout(checkoutID, flags);
1505
+ while (!isBuyerDeliveryComplete({ checkout }) && Date.now() - started < timeoutMs) {
1506
+ await sleep(Number(flags.delivery_poll_ms || 2000));
1507
+ checkout = await getBuyerCheckout(checkoutID, flags);
1508
+ }
1509
+ return { checkout, delivery: checkout.delivery || null };
1510
+ }
1511
+
1512
+ function isBuyerDeliveryComplete(result) {
1513
+ const checkout = result?.checkout || result || {};
1514
+ const delivery = checkout.delivery || result?.delivery || {};
1515
+ return checkout.delivery_status === "delivered" ||
1516
+ delivery.status === "delivery_claimable" ||
1517
+ delivery.next_required_action === "check_email" ||
1518
+ checkout.agent_next_actions?.includes("stop_check_email");
1519
+ }
1520
+
1521
+ function buyerRunOutput(value = {}) {
1522
+ return stripInternalBuyerFields({
1523
+ schema_version: "itp.buyer.v1",
1524
+ docs: value.docs || buyerDocsFor(value),
1525
+ ...value,
1526
+ secrets: {
1527
+ raw_content_included: false,
1528
+ claim_token_included: false,
1529
+ provider_raw_payload_included: false
1530
+ }
1531
+ });
1532
+ }
1533
+
1534
+ function buyerDocsFor(value = {}) {
1535
+ const topics = new Set();
1536
+ const status = String(value.status || "").toLowerCase();
1537
+ const actions = Array.isArray(value.agent_next_actions) ? value.agent_next_actions : [];
1538
+ if (status.includes("catalog")) topics.add("catalog-search");
1539
+ if (status.includes("cart") || actions.includes("create_checkout_from_cart")) topics.add("cart-checkout");
1540
+ if (status.includes("checkout") || actions.includes("create_payment_intent")) topics.add("cart-checkout");
1541
+ if (status.includes("human_auth") || actions.includes("wait_human_auth") || value.human_action?.kind === "auth_qr" || value.checkout?.human_action?.kind === "auth_qr") {
1542
+ topics.add("cart-checkout");
1543
+ topics.add("payment-qr");
1544
+ }
1545
+ if (status.includes("waiting_user_payment") || actions.includes("wait_payment") || value.payment_intent?.human_action || value.payment_intent?.qr_image_url) {
1546
+ topics.add("payment-qr");
1547
+ topics.add("payment-wait");
1548
+ }
1549
+ if (status.includes("payment_verified") || value.payment_event?.event_type === "payment_intent.verified") topics.add("secure-delivery");
1550
+ if (status.includes("delivery") || value.delivery || actions.includes("stop_check_email")) {
1551
+ topics.add("secure-delivery");
1552
+ topics.add("human-claim-ui");
1553
+ topics.add("vault-agent-read");
1554
+ }
1555
+ if (status.includes("agent_read_grant") || value.agent_readable_grants || value.grant || actions.includes("read_agent_grant_view") || actions.includes("list_agent_read_grants")) {
1556
+ topics.add("vault-agent-read");
1557
+ }
1558
+ if (topics.size === 0) topics.add("quickstart");
1559
+ return Array.from(topics).map((topic) => buyerDocRef(topic));
1560
+ }
1561
+
1562
+ function buyerDocRef(topic) {
1563
+ return {
1564
+ topic,
1565
+ command: cliCommand("docs", "show", topic, "--role", "buyer", "--json")
1566
+ };
1567
+ }
1568
+
1569
+ function buyerDeliveryListOutput(checkout) {
1570
+ const delivery = checkout.delivery || null;
1571
+ const checkoutID = checkout.checkout_id;
1572
+ return buyerRunOutput({
1573
+ status: delivery?.status || checkout.delivery_status,
1574
+ checkout_id: checkoutID,
1575
+ deliveries: delivery ? [{
1576
+ checkout_id: checkoutID,
1577
+ status: delivery.status,
1578
+ next_required_action: delivery.next_required_action,
1579
+ channels: delivery.channels || [],
1580
+ artifact_status: delivery.artifact_status,
1581
+ sensitive_content_redacted: delivery.sensitive_content_redacted !== false
1582
+ }] : [],
1583
+ agent_next_actions: deliveryAwareAgentNextActions(checkout),
1584
+ optional_agent_read_grant: optionalAgentReadGrantHint(checkoutID, checkout)
1585
+ });
1586
+ }
1587
+
1588
+ function deliveryAwareAgentNextActions(checkout = {}) {
1589
+ const actions = Array.isArray(checkout.agent_next_actions) ? [...checkout.agent_next_actions] : [];
1590
+ if (isBuyerDeliveryComplete(checkout) && !actions.includes("optionally_request_agent_read_grant")) {
1591
+ actions.push("optionally_request_agent_read_grant");
1592
+ }
1593
+ return actions;
1594
+ }
1595
+
1596
+ function optionalAgentReadGrantHint(checkoutID, checkout = {}) {
1597
+ if (!checkoutID || !isBuyerDeliveryComplete(checkout)) return undefined;
1598
+ return {
1599
+ type: "optional_human_passkey_agent_read_grant",
1600
+ safe_for_agent: true,
1601
+ requires_human: true,
1602
+ instruction: "If the human wants you to analyze or use the delivered result, ask them to open the ItPay claim/account page, reveal with Passkey, choose 'Give to Agent / 一键给 Agent', select fields, and confirm. After they approve, do not ask for a grant id; run the probe command.",
1603
+ docs_command: cliCommand("docs", "show", "vault-agent-read", "--role", "buyer", "--json"),
1604
+ probe_command: cliCommand("buyer", "vault", "grants", "list", "--checkout", checkoutID, "--json"),
1605
+ read_pattern: cliCommand("buyer", "vault", "read", "--order", "<order_id>", "--artifact", "<vault_artifact_id>", "--json")
1606
+ };
1607
+ }
1608
+
1609
+ async function buyerAuthStatusOutput(flags = {}) {
1610
+ const config = readConfig();
1611
+ const credentials = readCredentials();
1612
+ const token = readSessionToken(credentials);
1613
+ if (token && config.account_id) {
1614
+ try {
1615
+ const status = await coreApi(`/v1/buyer/accounts/${encodeURIComponent(config.account_id)}/auth/status`, { method: "GET" }, flags);
1616
+ return buyerRunOutput({
1617
+ status: "authenticated_buyer_session",
1618
+ authenticated: true,
1619
+ buyer_account_id: status.buyer_account_id || config.account_id,
1620
+ agent_device_id: config.device_id || null,
1621
+ account_status: status.account_status,
1622
+ sensitive_redacted: true,
1623
+ agent_next_actions: ["search_catalog", "view_orders", "list_agent_read_grants"]
1624
+ });
1625
+ } catch (error) {
1626
+ return buyerRunOutput({
1627
+ status: "buyer_session_invalid",
1628
+ authenticated: false,
1629
+ buyer_account_id: config.account_id || null,
1630
+ agent_device_id: config.device_id || null,
1631
+ error: safeErrorMessage(error),
1632
+ agent_next_actions: ["create_checkout_for_human_auth"]
1633
+ });
1634
+ }
1635
+ }
1636
+ return buyerRunOutput({
1637
+ status: "public_purchase_mode",
1638
+ authenticated: false,
1639
+ auth_required_for_discovery: false,
1640
+ agent_next_actions: ["search_catalog", "create_checkout_for_human_auth"],
1641
+ note: "Catalog discovery is public. Checkout creates a human auth-to-payment QR and saves a buyer session after the human authorizes."
1642
+ });
1643
+ }
1644
+
1645
+ async function listBuyerAgentReadGrants(flags = {}) {
1646
+ const params = new URLSearchParams();
1647
+ const mappings = [
1648
+ ["checkout", "checkout_id"],
1649
+ ["checkout_id", "checkout_id"],
1650
+ ["order", "order_id"],
1651
+ ["order_id", "order_id"],
1652
+ ["artifact", "vault_artifact_id"],
1653
+ ["vault_artifact", "vault_artifact_id"],
1654
+ ["vault_artifact_id", "vault_artifact_id"],
1655
+ ["line", "order_line_item_id"],
1656
+ ["line_id", "order_line_item_id"],
1657
+ ["order_line_item_id", "order_line_item_id"]
1658
+ ];
1659
+ for (const [flagName, paramName] of mappings) {
1660
+ if (flags[flagName] !== undefined && flags[flagName] !== null && flags[flagName] !== false) {
1661
+ params.set(paramName, String(flags[flagName]));
1662
+ }
1663
+ }
1664
+ const query = params.toString();
1665
+ return await coreApi(`/v1/vault/agent-read-grants${query ? `?${query}` : ""}`, { method: "GET" }, flags);
1666
+ }
1667
+
1668
+ async function readBuyerAgentReadGrant(grantID, flags = {}) {
1669
+ if (!grantID) throw new Error("agent_read_grant_id is required");
1670
+ return await coreApi(`/v1/vault/agent-read-grants/${encodeURIComponent(grantID)}/view`, { method: "GET" }, flags);
1671
+ }
1672
+
1673
+ async function readBuyerVaultArtifactGrant(flags = {}) {
1674
+ const grants = await listBuyerAgentReadGrants(flags);
1675
+ const list = Array.isArray(grants.agent_readable_grants) ? grants.agent_readable_grants : [];
1676
+ if (!list.length) {
1677
+ throw new Error("no active agent-readable grant found; ask the human to open the account portal and confirm one-key agent authorization with Passkey");
1678
+ }
1679
+ const grant = list[0];
1680
+ const grantID = grant.agent_read_grant_id || grant.agent_readGrantID;
1681
+ if (!grantID) throw new Error("active grant did not include agent_read_grant_id");
1682
+ const view = await readBuyerAgentReadGrant(grantID, flags);
1683
+ return {
1684
+ ...view,
1685
+ discovered_grant: grant
1686
+ };
1687
+ }
1688
+
1689
+ async function agentStatus(flags) {
1690
+ const runId = flags.run_id || readState().current_run_id;
1691
+ const run = readRun(runId);
1692
+ if (!run) {
1693
+ const config = readConfig();
1694
+ const credentials = readCredentials();
1695
+ let auth = { authenticated: Boolean(readSessionToken(credentials)), account_id: config.account_id || null, device_id: config.device_id || null };
1696
+ if (auth.authenticated && flags.refresh) {
1697
+ try {
1698
+ auth = await api("/api/itp/auth/status", { method: "GET" }, flags);
1699
+ } catch (error) {
1700
+ auth = { authenticated: false, account_id: config.account_id || null, error: error.message };
1701
+ }
1702
+ }
1703
+ output({
1704
+ schema_version: "itp.agent.v1",
1705
+ status: auth.authenticated ? "idle" : "unauthenticated",
1706
+ phase: auth.authenticated ? "idle" : "unauthenticated",
1707
+ authenticated: Boolean(auth.authenticated),
1708
+ account_id: auth.account_id || null,
1709
+ device_id: auth.device_id || null,
1710
+ next: auth.authenticated
1711
+ ? { type: "choose_credits_or_plan", command: cliCommand("plans", "--json"), safe_for_agent: true }
1712
+ : { type: "start_auth", command: cliCommand("setup", "--plan", "credit-300", "--method", "alipay", "--json"), safe_for_agent: true },
1713
+ secrets: { raw_key_included: false, session_token_included: false }
1714
+ });
1715
+ return;
1716
+ }
1717
+ const refreshed = flags.refresh ? await refreshRun(run, flags) : run;
1718
+ writeRun(refreshed);
1719
+ output(agentRunResponse(refreshed));
1720
+ }
1721
+
1722
+ async function resume(flags) {
1723
+ const runId = flags.run_id || readState().current_run_id;
1724
+ const run = readRun(runId);
1725
+ if (!run) throw new Error("no active run found");
1726
+ if (["done", "installed"].includes(run.status) || run.phase === "done") {
1727
+ output(agentRunResponse(run));
1728
+ return;
1729
+ }
1730
+ await setup({
1731
+ ...flags,
1732
+ run_id: run.run_id,
1733
+ resume: true,
1734
+ plan: run.plan_id || flags.plan,
1735
+ credits: run.plan_id ? flags.credits : run.credits || flags.credits,
1736
+ method: run.payment_method || flags.method,
1737
+ target: run.target || flags.target,
1738
+ install_runtime: run.install_runtime || flags.install_runtime
1739
+ });
1740
+ }
1741
+
1742
+ async function runs(command, flags) {
1743
+ if (command === "current") {
1744
+ const run = readRun(flags.run_id || readState().current_run_id);
1745
+ output(run ? agentRunResponse(run) : { schema_version: "itp.agent.v1", status: "none", runs: [] });
1746
+ return;
1747
+ }
1748
+ if (command === "list") {
1749
+ output({ runs: listRuns().map(agentRunResponse) });
1750
+ return;
1751
+ }
1752
+ if (command === "show") {
1753
+ const run = readRun(flags.run_id || flags.id);
1754
+ if (!run) throw new Error("run not found");
1755
+ output(agentRunResponse(run));
1756
+ return;
1757
+ }
1758
+ if (command === "forget") {
1759
+ const runId = flags.run_id || flags.id;
1760
+ if (!runId || runId === "forget") throw new Error("run_id is required");
1761
+ const file = runPath(runId);
1762
+ if (fs.existsSync(file)) fs.unlinkSync(file);
1763
+ const state = readState();
1764
+ if (state.current_run_id === runId) {
1765
+ delete state.current_run_id;
1766
+ writeState(state);
1767
+ }
1768
+ output({ status: "forgotten", run_id: runId });
1769
+ return;
1770
+ }
1771
+ throw new Error(`unknown runs command: ${command || ""}`);
1772
+ }
1773
+
1774
+ async function startDeviceAuth(flags) {
1775
+ const runtime = flags.runtime || "unknown";
1776
+ return await api("/api/itp/auth/device/start", {
1777
+ method: "POST",
1778
+ body: {
1779
+ device: {
1780
+ display_name: os.hostname(),
1781
+ runtime,
1782
+ os: os.platform(),
1783
+ arch: os.arch(),
1784
+ itp_version: VERSION
1785
+ }
1786
+ }
1787
+ }, flags);
1788
+ }
1789
+
1790
+ async function completeDeviceAuth(flags) {
1791
+ const start = await startDeviceAuth(flags);
1792
+ if (flags.no_wait) {
1793
+ writeState({ ...readState(), last_auth_id: start.auth_id });
1794
+ updateCurrentRun({
1795
+ phase: "waiting_human_auth",
1796
+ auth: { auth_id: start.auth_id, status: "pending", expires_at: start.expires_at },
1797
+ human_action: start.human_action || null,
1798
+ safe_summary: "Waiting for Alipay authentication scan."
1799
+ }, flags);
1800
+ await renderHumanAction(start.human_action, flags);
1801
+ return start;
1802
+ }
1803
+ await maybeMockApproveDeviceAuth(start.auth_id, flags);
1804
+ const response = await waitDeviceAuth(start.auth_id, flags, start);
1805
+ if (!response.auth) {
1806
+ throw new Error(`device auth ended without session: ${response.status || "unknown"}`);
1807
+ }
1808
+ writeSessionCredentials(response.auth);
1809
+ writeConfig({
1810
+ api_base: apiBase(flags),
1811
+ account_id: response.auth.account_id,
1812
+ device_id: response.auth.device_id,
1813
+ web_console_url: response.auth.web_console_url
1814
+ });
1815
+ writeState({ ...readState(), last_auth_id: start.auth_id });
1816
+ return { ...sanitizeAuthResponse(response.auth), auth_id: start.auth_id, status: response.status };
1817
+ }
1818
+
1819
+ async function ensureAuthenticated(flags) {
1820
+ const config = readConfig();
1821
+ const credentials = readCredentials();
1822
+ if (readSessionToken(credentials)) {
1823
+ try {
1824
+ const status = await api("/api/itp/auth/status", { method: "GET" }, flags);
1825
+ if (status.authenticated !== false) {
1826
+ return {
1827
+ authenticated: true,
1828
+ account_id: status.account_id || config.account_id || null,
1829
+ device_id: status.device_id || config.device_id || null,
1830
+ newapi_user_id: status.newapi_user_id || null,
1831
+ session_reused: true
1832
+ };
1833
+ }
1834
+ } catch {
1835
+ deleteSessionCredential(credentials);
1836
+ writeCredentials(credentials);
1837
+ }
1838
+ }
1839
+ const resumableRun = readRun(flags.run_id || readState().current_run_id);
1840
+ if ((flags.resume || flags.auth_id) && resumableRun?.auth?.auth_id && !resumableRun.account?.authenticated && !resumableRun.checkout?.checkout_id && !flags.no_wait && !flags.no_wait_auth) {
1841
+ await renderHumanAction(resumableRun.human_action, flags);
1842
+ await maybeMockApproveDeviceAuth(resumableRun.auth.auth_id, flags);
1843
+ const response = await waitDeviceAuth(resumableRun.auth.auth_id, flags);
1844
+ if (!response.auth) {
1845
+ return {
1846
+ authenticated: false,
1847
+ status: response.status || "waiting_human_auth",
1848
+ auth_id: resumableRun.auth.auth_id,
1849
+ expires_at: resumableRun.auth.expires_at,
1850
+ human_action: resumableRun.human_action
1851
+ };
1852
+ }
1853
+ writeSessionCredentials(response.auth);
1854
+ writeConfig({
1855
+ api_base: apiBase(flags),
1856
+ account_id: response.auth.account_id,
1857
+ device_id: response.auth.device_id,
1858
+ web_console_url: response.auth.web_console_url
1859
+ });
1860
+ writeState({ ...readState(), last_auth_id: resumableRun.auth.auth_id });
1861
+ return { ...sanitizeAuthResponse(response.auth), auth_id: resumableRun.auth.auth_id, authenticated: true, session_reused: false };
1862
+ }
1863
+ if (flags.no_wait || flags.no_wait_auth) {
1864
+ const start = await startDeviceAuth(flags);
1865
+ writeState({ ...readState(), last_auth_id: start.auth_id });
1866
+ updateCurrentRun({
1867
+ phase: "waiting_human_auth",
1868
+ auth: { auth_id: start.auth_id, status: "pending", expires_at: start.expires_at },
1869
+ human_action: start.human_action || null,
1870
+ safe_summary: "Waiting for Alipay authentication scan."
1871
+ }, flags);
1872
+ await renderHumanAction(start.human_action, flags);
1873
+ return {
1874
+ authenticated: false,
1875
+ status: "waiting_human_auth",
1876
+ action: "scan_alipay_auth",
1877
+ auth_id: start.auth_id,
1878
+ user_code: start.user_code,
1879
+ verification_uri: start.verification_uri,
1880
+ verification_uri_complete: start.verification_uri_complete,
1881
+ alipay_authorization_url: start.alipay_authorization_url,
1882
+ expires_at: start.expires_at,
1883
+ interval: start.interval,
1884
+ human_action: start.human_action
1885
+ };
1886
+ }
1887
+ const auth = await completeDeviceAuth(flags);
1888
+ return { ...auth, authenticated: true, session_reused: false };
1889
+ }
1890
+
1891
+ async function maybeMockApproveDeviceAuth(authId, flags) {
1892
+ if (!(flags.mock_approve || process.env.ITPAY_MOCK_APPROVE === "true" || process.env.ITPAY_MOCK_APPROVE === "1")) {
1893
+ return;
1894
+ }
1895
+ if (!fakeTestingAllowed(flags)) {
1896
+ throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
1897
+ }
1898
+ const alipayUserId = flags.alipay_user_id || process.env.ITPAY_MOCK_ALIPAY_USER_ID || `2088${crypto.randomInt(100000000000, 999999999999)}`;
1899
+ await api(`/api/itp/auth/device/${encodeURIComponent(authId)}/mock-approve`, {
1900
+ method: "POST",
1901
+ body: { alipay_user_id: alipayUserId }
1902
+ }, flags);
1903
+ }
1904
+
1905
+ async function authDevice(command, flags) {
1906
+ if (command === "start") {
1907
+ const response = await startDeviceAuth(flags);
1908
+ writeState({ ...readState(), last_auth_id: response.auth_id });
1909
+ await renderHumanAction(response.human_action, flags);
1910
+ output(response);
1911
+ return;
1912
+ }
1913
+ if (command === "poll") {
1914
+ const authId = flags.auth_id || flags.device_auth_id || readState().last_auth_id;
1915
+ if (!authId) throw new Error("auth_id is required");
1916
+ const response = await waitDeviceAuth(authId, flags);
1917
+ if (response.auth) {
1918
+ writeSessionCredentials(response.auth);
1919
+ writeConfig({
1920
+ api_base: apiBase(flags),
1921
+ account_id: response.auth.account_id,
1922
+ device_id: response.auth.device_id,
1923
+ web_console_url: response.auth.web_console_url
1924
+ });
1925
+ }
1926
+ output(response.auth ? { ...sanitizeAuthResponse(response.auth), auth_id: authId, status: response.status } : response);
1927
+ return;
1928
+ }
1929
+ throw new Error(`unknown auth device command: ${command || ""}`);
1930
+ }
1931
+
1932
+ async function waitDeviceAuth(authId, flags, start = null) {
1933
+ const started = Date.now();
1934
+ const timeoutMs = Number(flags.timeout || 600) * 1000;
1935
+ let intervalMs = 2000;
1936
+ let lastHeartbeatAt = 0;
1937
+ let lastStatus = "authorization_pending";
1938
+ const action = start?.human_action || null;
1939
+ if (start) {
1940
+ updateCurrentRun({
1941
+ phase: "waiting_human_auth",
1942
+ auth: { auth_id: start.auth_id, status: "pending", expires_at: start.expires_at },
1943
+ human_action: start.human_action || null,
1944
+ safe_summary: "Waiting for Alipay authentication scan."
1945
+ }, flags);
1946
+ await renderHumanAction(start.human_action, flags);
1947
+ if (!start.human_action) process.stderr.write(`Open Alipay auth URL: ${start.verification_uri_complete || start.alipay_authorization_url}\n`);
1948
+ if (start.user_code) process.stderr.write(`Alipay auth code: ${start.user_code}\n`);
1949
+ }
1950
+ while (Date.now() - started < timeoutMs) {
1951
+ const response = await api(`/api/itp/auth/device/${encodeURIComponent(authId)}/poll`, { method: "POST" }, flags);
1952
+ lastStatus = response.status || lastStatus;
1953
+ if (response.auth?.session_token) {
1954
+ return response;
1955
+ }
1956
+ if (response.status === "authorization_pending") {
1957
+ intervalMs = Number(response.interval || 2) * 1000;
1958
+ lastHeartbeatAt = writeWaitHeartbeat({
1959
+ kind: "Alipay authentication",
1960
+ idName: "auth_id",
1961
+ idValue: authId,
1962
+ status: response.status,
1963
+ action,
1964
+ lastHeartbeatAt,
1965
+ flags,
1966
+ command: cliCommand("auth", "device", "poll", authId, "--timeout", String(Math.ceil((timeoutMs - (Date.now() - started)) / 1000)), "--json")
1967
+ });
1968
+ await sleep(intervalMs);
1969
+ continue;
1970
+ }
1971
+ if (response.status === "approved") {
1972
+ return response;
1973
+ }
1974
+ if (response.status === "expired" || response.status === "consumed" || response.error) {
1975
+ throw new Error(response.error || `device auth ended with status: ${response.status}`);
1976
+ }
1977
+ await sleep(intervalMs);
1978
+ }
1979
+ throw new Error(`device auth timed out at status ${lastStatus}; run \`itp auth device poll ${authId} --timeout 600\``);
1980
+ }
1981
+
1982
+ async function authLogin(flags) {
1983
+ const runtime = flags.runtime || "unknown";
1984
+ if (flags.password && !flags.password_stdin) {
1985
+ throw new Error("use --password-stdin to avoid leaking passwords into shell history");
1986
+ }
1987
+ const password = flags.password_stdin
1988
+ ? fs.readFileSync(0, "utf8").trim()
1989
+ : undefined;
1990
+ if (flags.password_stdin && !password) {
1991
+ throw new Error("password is required on stdin");
1992
+ }
1993
+ const response = await api("/api/itp/auth/login", {
1994
+ method: "POST",
1995
+ body: {
1996
+ username: flags.username || undefined,
1997
+ password,
1998
+ access_token: flags.access_token || undefined,
1999
+ device: {
2000
+ display_name: os.hostname(),
2001
+ runtime,
2002
+ os: os.platform(),
2003
+ arch: os.arch(),
2004
+ itp_version: VERSION
2005
+ }
2006
+ }
2007
+ }, flags);
2008
+ writeSessionCredentials(response);
2009
+ writeConfig({
2010
+ api_base: apiBase(flags),
2011
+ account_id: response.account_id,
2012
+ device_id: response.device_id,
2013
+ web_console_url: response.web_console_url
2014
+ });
2015
+ output(sanitizeAuthResponse(response));
2016
+ }
2017
+
2018
+ async function authStatus(flags) {
2019
+ const config = readConfig();
2020
+ const credentials = readCredentials();
2021
+ if (!readSessionToken(credentials)) {
2022
+ output({ authenticated: false, account_id: config.account_id || null });
2023
+ return;
2024
+ }
2025
+ try {
2026
+ const status = await api("/api/itp/auth/status", { method: "GET" }, flags);
2027
+ output(status);
2028
+ } catch (error) {
2029
+ output({
2030
+ authenticated: false,
2031
+ account_id: config.account_id || null,
2032
+ error: error.message
2033
+ });
2034
+ }
2035
+ }
2036
+
2037
+ async function accountShow(flags) {
2038
+ output(await api("/api/itp/account", { method: "GET" }, flags));
2039
+ }
2040
+
2041
+ async function accountLoginLink(flags) {
2042
+ const config = readConfig();
2043
+ const accountID = flags.account || flags.account_id || flags.buyer_account || flags.buyer_account_id || config.account_id;
2044
+ if (!accountID) {
2045
+ throw new Error("buyer account id is required; complete a buyer purchase first or pass --account-id");
2046
+ }
2047
+ if (!readSessionToken()) {
2048
+ throw new Error("buyer account session is required; complete first-purchase auth or run buyer checkout resume first");
2049
+ }
2050
+ const link = await coreApi(`/v1/buyer/accounts/${encodeURIComponent(accountID)}/portal-login-links`, { method: "POST" }, flags);
2051
+ output(buyerRunOutput({
2052
+ status: "account_portal_login_link_created",
2053
+ account_id: accountID,
2054
+ portal_login_link: link,
2055
+ login_url: link.login_url,
2056
+ expires_at: link.expires_at,
2057
+ agent_next_actions: link.agent_next_actions || ["show_login_link_to_human_only"],
2058
+ next: {
2059
+ type: "human_open_account_portal_link",
2060
+ safe_for_agent: false,
2061
+ requires_human: true,
2062
+ agent_must_not_open: true,
2063
+ instruction: "Give this one-time ItPay account portal link to the human buyer. Do not open it yourself; the human portal is redacted and protected content stays locked until human reveal."
2064
+ }
2065
+ }));
2066
+ }
2067
+
2068
+ async function accountSetPassword(flags) {
2069
+ if (!flags.password_stdin) {
2070
+ throw new Error("use --password-stdin to avoid leaking passwords into shell history");
2071
+ }
2072
+ const password = fs.readFileSync(0, "utf8").trim();
2073
+ if (!password) throw new Error("password is required on stdin");
2074
+ output(await api("/api/itp/account/password", {
2075
+ method: "POST",
2076
+ body: { password }
2077
+ }, flags));
2078
+ }
2079
+
2080
+ async function plansList(flags) {
2081
+ output(await api("/api/itp/plans", { method: "GET" }, flags));
2082
+ }
2083
+
2084
+ async function plansShow(plan, flags) {
2085
+ if (!plan) throw new Error("plan id is required");
2086
+ output(await api(`/api/itp/plans/${encodeURIComponent(plan)}`, { method: "GET" }, flags));
2087
+ }
2088
+
2089
+ async function checkoutCreate(flags) {
2090
+ const response = await createCheckoutResult(flags);
2091
+ await renderHumanAction(response.human_action, flags);
2092
+ output(response);
2093
+ }
2094
+
2095
+ async function createCheckoutResult(flags) {
2096
+ const purchase = normalizePurchaseFlags(flags, true);
2097
+ const method = flags.method || "alipay";
2098
+ validateLivePaymentFlags(method, flags);
2099
+ const currentRun = readRun(flags.run_id || readState().current_run_id);
2100
+ const idempotencyKey = flags.idempotency_key || currentRun?.idempotency_key || cryptoRandom();
2101
+ const body = {
2102
+ payment_method: method,
2103
+ idempotency_key: idempotencyKey
2104
+ };
2105
+ if (purchase.plan) body.plan_id = purchase.plan;
2106
+ if (purchase.credits) body.credits = purchase.credits;
2107
+ const response = await api("/api/itp/checkout", {
2108
+ method: "POST",
2109
+ body
2110
+ }, flags);
2111
+ writeState({ ...readState(), last_checkout_id: response.checkout_id, last_grant_id: response.grant_id || null });
2112
+ updateCurrentRun({
2113
+ phase: response.grant_id ? "grant_ready" : "waiting_human_payment",
2114
+ plan_id: response.plan_id || purchase.plan || null,
2115
+ credits: response.credits || purchase.credits || null,
2116
+ purchase_kind: response.purchase?.kind || purchase.kind,
2117
+ checkout: {
2118
+ checkout_id: response.checkout_id,
2119
+ order_id: response.order_id,
2120
+ status: response.status,
2121
+ expires_at: response.expires_at,
2122
+ purchase: response.purchase || null
2123
+ },
2124
+ payment: {
2125
+ provider: method,
2126
+ status: response.status
2127
+ },
2128
+ grant: {
2129
+ ...(currentRun?.grant || {}),
2130
+ grant_id: response.grant_id || currentRun?.grant?.grant_id || null
2131
+ },
2132
+ human_action: response.human_action || null,
2133
+ safe_summary: response.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay payment scan."
2134
+ }, flags);
2135
+ return response;
2136
+ }
2137
+
2138
+ function validateLivePaymentFlags(method, flags = {}) {
2139
+ if (String(method).toLowerCase() === "fake" && !fakeTestingAllowed(flags)) {
2140
+ throw new Error("fake payment is developer-only and disabled for agent runs; use --method alipay for local, sandbox, and live testing");
2141
+ }
2142
+ if ((flags.mock_approve || process.env.ITPAY_MOCK_APPROVE === "true" || process.env.ITPAY_MOCK_APPROVE === "1") && !fakeTestingAllowed(flags)) {
2143
+ throw new Error("mock approval is developer-only and disabled for agent runs; use real Alipay sandbox authentication");
2144
+ }
2145
+ }
2146
+
2147
+ function fakeTestingAllowed(flags = {}) {
2148
+ return flags.allow_fake || process.env.ITP_ALLOW_FAKE_PAYMENT === "true" || process.env.ITP_ALLOW_FAKE_PAYMENT === "1";
2149
+ }
2150
+
2151
+ async function paymentWait(checkoutId, flags) {
2152
+ output(await paymentWaitResult(checkoutId, flags));
2153
+ }
2154
+
2155
+ async function paymentWaitResult(checkoutId, flags) {
2156
+ if (!checkoutId) throw new Error("checkout_id is required");
2157
+ const started = Date.now();
2158
+ const timeoutMs = Number(flags.timeout || 120) * 1000;
2159
+ let lastRecoverAt = 0;
2160
+ let lastHeartbeatAt = 0;
2161
+ let lastResponse = null;
2162
+ while (Date.now() - started < timeoutMs) {
2163
+ let response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}`, { method: "GET" }, flags);
2164
+ lastResponse = response;
2165
+ if (isTerminalCheckoutFailure(response.status)) {
2166
+ throw new Error(`checkout ended with status: ${response.status}`);
2167
+ }
2168
+ if (isSuccessfulCheckout(response)) {
2169
+ writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
2170
+ updateCurrentRun({
2171
+ phase: "grant_ready",
2172
+ checkout: { checkout_id: checkoutId, order_id: response.order_id, status: response.status, expires_at: response.expires_at },
2173
+ grant: { grant_id: response.grant_id, installed: false },
2174
+ human_action: null,
2175
+ safe_summary: "Payment verified and grant is ready."
2176
+ }, flags);
2177
+ return { status: "grant_issued", checkout_id: checkoutId, order_id: response.order_id, grant_id: response.grant_id };
2178
+ }
2179
+ if (shouldRecoverCheckout(response.status) && Date.now() - lastRecoverAt > 5000) {
2180
+ lastRecoverAt = Date.now();
2181
+ response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/recover`, { method: "POST" }, flags);
2182
+ lastResponse = response;
2183
+ if (isTerminalCheckoutFailure(response.status)) {
2184
+ throw new Error(`checkout ended with status: ${response.status}`);
2185
+ }
2186
+ if (isSuccessfulCheckout(response)) {
2187
+ writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
2188
+ updateCurrentRun({
2189
+ phase: "grant_ready",
2190
+ checkout: { checkout_id: checkoutId, order_id: response.order_id, status: response.status, expires_at: response.expires_at },
2191
+ grant: { grant_id: response.grant_id, installed: false },
2192
+ human_action: null,
2193
+ safe_summary: "Payment verified and grant is ready."
2194
+ }, flags);
2195
+ return { status: "grant_issued", checkout_id: checkoutId, order_id: response.order_id, grant_id: response.grant_id, recovered: true };
2196
+ }
2197
+ }
2198
+ lastHeartbeatAt = writeWaitHeartbeat({
2199
+ kind: "Alipay payment",
2200
+ idName: "checkout_id",
2201
+ idValue: checkoutId,
2202
+ status: response.status || "waiting",
2203
+ action: response.human_action || null,
2204
+ lastHeartbeatAt,
2205
+ flags,
2206
+ command: cliCommand("payment", "wait", checkoutId, "--timeout", String(Math.ceil((timeoutMs - (Date.now() - started)) / 1000)), "--json")
2207
+ });
2208
+ await sleep(2000);
2209
+ }
2210
+ try {
2211
+ const response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/recover`, { method: "POST" }, flags);
2212
+ lastResponse = response;
2213
+ if (isTerminalCheckoutFailure(response.status)) {
2214
+ throw new Error(`checkout ended with status: ${response.status}`);
2215
+ }
2216
+ if (isSuccessfulCheckout(response)) {
2217
+ writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
2218
+ updateCurrentRun({
2219
+ phase: "grant_ready",
2220
+ checkout: { checkout_id: checkoutId, order_id: response.order_id, status: response.status, expires_at: response.expires_at },
2221
+ grant: { grant_id: response.grant_id, installed: false },
2222
+ human_action: null,
2223
+ safe_summary: "Payment verified and grant is ready."
2224
+ }, flags);
2225
+ return { status: "grant_issued", checkout_id: checkoutId, order_id: response.order_id, grant_id: response.grant_id, recovered: true };
2226
+ }
2227
+ } catch (error) {
2228
+ if (isTerminalCheckoutFailure(lastResponse?.status)) {
2229
+ throw error;
2230
+ }
2231
+ // Preserve the timeout message below; recover is a best-effort final attempt.
2232
+ }
2233
+ if (lastResponse?.status) {
2234
+ throw new Error(`payment wait timed out at checkout status ${lastResponse.status}; run \`itp checkout recover ${checkoutId}\` later`);
2235
+ }
2236
+ throw new Error("payment wait timed out; run `itp checkout recover` later");
2237
+ }
2238
+
2239
+ async function checkoutRecover(checkoutId, flags) {
2240
+ if (!checkoutId) throw new Error("checkout_id is required");
2241
+ const response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/recover`, { method: "POST" }, flags);
2242
+ if (response.grant_id) {
2243
+ writeState({ ...readState(), last_checkout_id: checkoutId, last_grant_id: response.grant_id });
2244
+ }
2245
+ output(response);
2246
+ }
2247
+
2248
+ async function checkoutOpen(flags) {
2249
+ const state = readState();
2250
+ if (!state.last_checkout_id) throw new Error("no checkout found in local state");
2251
+ const response = await api(`/api/itp/checkout/${encodeURIComponent(state.last_checkout_id)}`, { method: "GET" }, flags);
2252
+ output(response.payment || response);
2253
+ }
2254
+
2255
+ async function checkoutQR(checkoutId, flags) {
2256
+ if (!checkoutId) throw new Error("checkout_id is required");
2257
+ const response = await api(`/api/itp/checkout/${encodeURIComponent(checkoutId)}/qr`, { method: "GET" }, flags);
2258
+ await renderHumanAction(response.human_action, flags);
2259
+ output(response);
2260
+ }
2261
+
2262
+ async function checkoutList(flags) {
2263
+ const params = new URLSearchParams();
2264
+ if (flags.limit) params.set("limit", flags.limit);
2265
+ const suffix = params.toString() ? `?${params.toString()}` : "";
2266
+ output(await api(`/api/itp/orders${suffix}`, { method: "GET" }, flags));
2267
+ }
2268
+
2269
+ async function balance(flags) {
2270
+ output(await api("/api/itp/balance", { method: "GET" }, flags));
2271
+ }
2272
+
2273
+ async function usage(flags) {
2274
+ const params = new URLSearchParams();
2275
+ if (flags.model) params.set("model", flags.model);
2276
+ if (flags.grant) params.set("grant_id", flags.grant);
2277
+ if (flags.grant_id) params.set("grant_id", flags.grant_id);
2278
+ if (flags.from) params.set("from", flags.from);
2279
+ if (flags.to) params.set("to", flags.to);
2280
+ if (flags.today) {
2281
+ const start = new Date();
2282
+ start.setHours(0, 0, 0, 0);
2283
+ params.set("from", Math.floor(start.getTime() / 1000).toString());
2284
+ }
2285
+ const suffix = params.toString() ? `?${params.toString()}` : "";
2286
+ output(await api(`/api/itp/usage${suffix}`, { method: "GET" }, flags));
2287
+ }
2288
+
2289
+ async function grantsList(flags) {
2290
+ output(await api("/api/itp/grants", { method: "GET" }, flags));
2291
+ }
2292
+
2293
+ async function grantsShow(grantId, flags) {
2294
+ if (!grantId) throw new Error("grant_id is required");
2295
+ output(await api(`/api/itp/grants/${encodeURIComponent(grantId)}`, { method: "GET" }, flags));
2296
+ }
2297
+
2298
+ async function grantsInstall(grantId, flags) {
2299
+ output(await grantsInstallResult(grantId, flags));
2300
+ }
2301
+
2302
+ async function grantsInstallResult(grantId, flags) {
2303
+ grantId = grantId || readState().last_grant_id;
2304
+ if (!grantId) throw new Error("grant_id is required");
2305
+ const target = flags.target || "generic";
2306
+ const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/install`, {
2307
+ method: "POST",
2308
+ body: { target }
2309
+ }, flags);
2310
+ const storedCredential = storeGrantCredential(grantId, {
2311
+ key: response.credential.key_once,
2312
+ target,
2313
+ base_url: response.base_url,
2314
+ openai_base_url: response.openai_base_url,
2315
+ anthropic_base_url: response.anthropic_base_url,
2316
+ gemini_base_url: response.gemini_base_url,
2317
+ models: response.models || [],
2318
+ install_profiles: response.install_profiles || []
2319
+ });
2320
+ const grantsDir = path.join(CONFIG_DIR, "grants");
2321
+ ensureConfigDir();
2322
+ fs.mkdirSync(grantsDir, { recursive: true });
2323
+ try {
2324
+ fs.chmodSync(grantsDir, 0o700);
2325
+ } catch {
2326
+ // Best effort; grant metadata does not contain gateway keys.
2327
+ }
2328
+ const metadataPath = path.join(grantsDir, `${grantId}.json`);
2329
+ fs.writeFileSync(metadataPath, JSON.stringify({
2330
+ grant_id: grantId,
2331
+ target,
2332
+ base_url: response.base_url,
2333
+ models: response.models,
2334
+ install_profiles: response.install_profiles
2335
+ }, null, 2), { mode: 0o600 });
2336
+ fs.chmodSync(metadataPath, 0o600);
2337
+ writeState({ ...readState(), last_grant_id: grantId });
2338
+ updateCurrentRun({
2339
+ phase: "grant_ready",
2340
+ grant: {
2341
+ grant_id: grantId,
2342
+ installed: true,
2343
+ credential_store: storedCredential.credential_store || null
2344
+ },
2345
+ result: {
2346
+ base_url: response.base_url,
2347
+ openai_base_url: response.openai_base_url,
2348
+ anthropic_base_url: response.anthropic_base_url,
2349
+ gemini_base_url: response.gemini_base_url
2350
+ },
2351
+ safe_summary: "Grant credential stored."
2352
+ }, flags);
2353
+ return {
2354
+ ...response,
2355
+ credential: {
2356
+ type: response.credential.type,
2357
+ stored: true,
2358
+ credential_store: storedCredential.credential_store,
2359
+ warning: storedCredential.credential_warning || undefined
2360
+ }
2361
+ };
2362
+ }
2363
+
2364
+ async function grantsRevoke(grantId, flags) {
2365
+ grantId = grantId || flags.grant || readState().last_grant_id;
2366
+ if (!grantId) throw new Error("grant_id is required");
2367
+ const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/revoke`, { method: "POST" }, flags);
2368
+ deleteGrantCredential(grantId);
2369
+ const state = readState();
2370
+ if (state.last_grant_id === grantId) {
2371
+ delete state.last_grant_id;
2372
+ writeState(state);
2373
+ }
2374
+ output(response);
2375
+ }
2376
+
2377
+ async function installRuntime(target, flags) {
2378
+ output(await installRuntimeResult(target, flags));
2379
+ }
2380
+
2381
+ async function installRuntimeResult(target, flags) {
2382
+ const grantId = flags.grant || readState().last_grant_id;
2383
+ if (!grantId) throw new Error("grant id is required");
2384
+ const credentials = readGrantCredential(grantId);
2385
+ if (!credentials) throw new Error(`grant ${grantId} is not installed; run grants install first`);
2386
+ if (!credentials.key) {
2387
+ throw new Error(`grant ${grantId} credential key is unavailable; run keys rotate or grants install again`);
2388
+ }
2389
+
2390
+ const dryRun = Boolean(flags.dry_run);
2391
+ const result = installTargetConfig(target, grantId, credentials, dryRun);
2392
+ const shouldTest = !dryRun && !flags.offline && !flags.no_test;
2393
+ const modelCheck = shouldTest
2394
+ ? await doctorModelCheck(target, credentials)
2395
+ : { attempted: false, skipped: true, reason: dryRun ? "dry_run" : flags.offline ? "offline" : "no_test" };
2396
+ result.model_check = modelCheck;
2397
+ result.tested = Boolean(modelCheck.ok);
2398
+ if (modelCheck.attempted && !modelCheck.ok) {
2399
+ result.warnings.push(`Model endpoint check failed: ${modelCheck.error || modelCheck.status || "unknown error"}`);
2400
+ }
2401
+
2402
+ if (!dryRun && !flags.offline) {
2403
+ await api(`/api/itp/grants/${encodeURIComponent(grantId)}/install-ack`, {
2404
+ method: "POST",
2405
+ body: {
2406
+ target,
2407
+ status: "installed",
2408
+ tested: result.tested,
2409
+ config_path: result.files[0]?.path || "",
2410
+ last_error: result.warnings.join("; ")
2411
+ }
2412
+ }, flags);
2413
+ }
2414
+ return result;
2415
+ }
2416
+
2417
+ function shouldRecoverCheckout(status) {
2418
+ return ["paid_verified", "granting", "grant_failed"].includes(status);
2419
+ }
2420
+
2421
+ function isSuccessfulCheckout(response) {
2422
+ return Boolean(response?.grant_id) && ["grant_issued", "grant_installed"].includes(response.status);
2423
+ }
2424
+
2425
+ function isTerminalCheckoutFailure(status) {
2426
+ return ["expired", "payment_failed", "verify_failed", "amount_mismatch", "revoked"].includes(status);
2427
+ }
2428
+
2429
+ function installTargetConfig(target, grantId, credentials, dryRun) {
2430
+ if (target === "codex") return installCodex(grantId, credentials, dryRun);
2431
+ if (target === "claude-code") return installClaudeCode(grantId, credentials, dryRun);
2432
+ if (target === "openclaw") return installOpenClaw(grantId, credentials, dryRun);
2433
+ throw new Error(`unsupported install target: ${target}`);
2434
+ }
2435
+
2436
+ function installClaudeCode(grantId, credentials, dryRun) {
2437
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
2438
+ const current = readJSON(settingsPath, {});
2439
+ current.env = {
2440
+ ...(current.env || {}),
2441
+ ANTHROPIC_BASE_URL: credentials.anthropic_base_url,
2442
+ ANTHROPIC_AUTH_TOKEN_HELPER: `${quoteShell(currentExecutable())} token issue --grant ${quoteShell(grantId)} --stdout`
2443
+ };
2444
+ delete current.env.ANTHROPIC_API_KEY;
2445
+ const write = writeJSONWithBackup(settingsPath, current, dryRun);
2446
+ return {
2447
+ target: "claude-code",
2448
+ grant_id: grantId,
2449
+ status: dryRun ? "dry_run" : "installed",
2450
+ files: [{ path: settingsPath, action: write.action, backup_path: write.backup_path || null }],
2451
+ warnings: [
2452
+ "Claude Code will call itp as an auth token helper; no gateway key was written to settings.json."
2453
+ ]
2454
+ };
2455
+ }
2456
+
2457
+ function installCodex(grantId, credentials, dryRun) {
2458
+ const configPath = path.join(os.homedir(), ".codex", "config.toml");
2459
+ const envPath = path.join(CONFIG_DIR, "voltagent.env");
2460
+ const existing = readText(configPath, "");
2461
+ const block = [
2462
+ 'model_provider = "voltagent"',
2463
+ 'model = "openai-code-default"',
2464
+ "",
2465
+ "[model_providers.voltagent]",
2466
+ 'name = "VoltaGent"',
2467
+ `base_url = "${escapeTomlString(credentials.openai_base_url)}"`,
2468
+ 'env_key = "VOLTAGENT_API_KEY"'
2469
+ ].join("\n");
2470
+ const nextConfig = replaceManagedBlock(existing, "voltagent", block);
2471
+ const configWrite = writeTextWithBackup(configPath, nextConfig, 0o600, dryRun);
2472
+ const envWrite = writeTextWithBackup(envPath, `export VOLTAGENT_API_KEY=${quoteShell(credentials.key)}\n`, 0o600, dryRun);
2473
+ return {
2474
+ target: "codex",
2475
+ grant_id: grantId,
2476
+ status: dryRun ? "dry_run" : "installed",
2477
+ files: [
2478
+ { path: configPath, action: configWrite.action, backup_path: configWrite.backup_path || null },
2479
+ { path: envPath, action: envWrite.action, backup_path: envWrite.backup_path || null }
2480
+ ],
2481
+ warnings: [
2482
+ "Codex reads VOLTAGENT_API_KEY from its process environment; source ~/.itp/voltagent.env before starting Codex if your launcher does not load it."
2483
+ ]
2484
+ };
2485
+ }
2486
+
2487
+ function installOpenClaw(grantId, credentials, dryRun) {
2488
+ const configPath = path.join(os.homedir(), ".openclaw", "config.json");
2489
+ const current = readJSON(configPath, {});
2490
+ current.models = current.models || {};
2491
+ current.models.providers = current.models.providers || {};
2492
+ current.models.providers.voltagent = {
2493
+ baseUrl: credentials.openai_base_url,
2494
+ api: "openai-compatible",
2495
+ apiKey: credentials.key,
2496
+ models: credentials.models || []
2497
+ };
2498
+ const write = writeJSONWithBackup(configPath, current, dryRun);
2499
+ return {
2500
+ target: "openclaw",
2501
+ grant_id: grantId,
2502
+ status: dryRun ? "dry_run" : "installed",
2503
+ files: [{ path: configPath, action: write.action, backup_path: write.backup_path || null }],
2504
+ warnings: [
2505
+ "OpenClaw does not expose a stable token-helper contract here, so the key is written only to the user-level config file with 0600 permissions."
2506
+ ]
2507
+ };
2508
+ }
2509
+
2510
+ async function doctor(flags) {
2511
+ const config = readConfig();
2512
+ const credentials = readCredentials();
2513
+ const target = flags.target || null;
2514
+ const grantId = flags.grant || readState().last_grant_id || null;
2515
+ const grantCredential = grantId ? readGrantCredential(grantId) : null;
2516
+ output({
2517
+ itp_version: VERSION,
2518
+ api_base: config.api_base || apiBase(flags),
2519
+ account_id: config.account_id || null,
2520
+ device_id: config.device_id || null,
2521
+ authenticated: Boolean(readSessionToken(credentials)),
2522
+ grants_cached: Object.keys(credentials).filter((k) => k.startsWith("grant_")).length,
2523
+ grant_id: grantId,
2524
+ target,
2525
+ target_config: target ? runtimeConfigStatus(target) : null,
2526
+ model_check: grantCredential && !flags.offline ? await doctorModelCheck(target, grantCredential) : null,
2527
+ grant_credential_store: grantCredential?.credential_store || null,
2528
+ warning: grantCredential?.credential_warning || undefined,
2529
+ credential_store: {
2530
+ path: CREDENTIALS_PATH,
2531
+ mode: fileMode(CREDENTIALS_PATH),
2532
+ native: detectNativeCredentialStore(),
2533
+ fallback: true
2534
+ },
2535
+ session_credential_store: credentials.session_token_store || (credentials.session_token ? "file" : null),
2536
+ session_credential_warning: credentials.session_token_warning || undefined
2537
+ });
2538
+ }
2539
+
2540
+ async function docs(command, rest = [], flags = {}) {
2541
+ const role = normalizeDocsRole(flags.role || "buyer");
2542
+ if (command === "list" || !command) {
2543
+ const docsList = listAgentDocs(role);
2544
+ output({
2545
+ schema_version: "itp.agent_doc_index.v1",
2546
+ role,
2547
+ topics: docsList.map((doc) => ({
2548
+ topic: doc.topic,
2549
+ title: doc.title,
2550
+ purpose: doc.purpose,
2551
+ command: cliCommand("docs", "show", doc.topic, "--role", role, "--json"),
2552
+ next_docs: Array.isArray(doc.next_docs) ? doc.next_docs.map((next) => next.topic).filter(Boolean) : []
2553
+ })),
2554
+ start_here: cliCommand("docs", "show", "quickstart", "--role", role, "--json"),
2555
+ search_command: cliCommand("docs", "search", "<question>", "--role", role, "--json")
2556
+ });
2557
+ return;
2558
+ }
2559
+ if (command === "show" || command === "read") {
2560
+ const topic = flags.topic || positionalArgs(rest)[0] || "quickstart";
2561
+ output(loadAgentDoc(role, topic));
2562
+ return;
2563
+ }
2564
+ if (command === "search") {
2565
+ const query = String(flags.query || flags.q || positionalArgs(rest).join(" ")).trim();
2566
+ if (!query) throw new Error("docs search query is required");
2567
+ const matches = searchAgentDocs(role, query);
2568
+ output({
2569
+ schema_version: "itp.agent_doc_search.v1",
2570
+ role,
2571
+ query,
2572
+ matches,
2573
+ fallback: matches.length ? null : {
2574
+ topic: "quickstart",
2575
+ command: cliCommand("docs", "show", "quickstart", "--role", role, "--json")
2576
+ }
2577
+ });
2578
+ return;
2579
+ }
2580
+ throw new Error(`unknown docs command: ${command}`);
2581
+ }
2582
+
2583
+ function normalizeDocsRole(role) {
2584
+ const normalized = String(role || "buyer").trim().toLowerCase();
2585
+ if (normalized === "buyer" || normalized === "itpay-buyer") return "buyer";
2586
+ throw new Error(`unsupported docs role: ${role}`);
2587
+ }
2588
+
2589
+ function listAgentDocs(role) {
2590
+ const docsDir = resolveDocsDir(role);
2591
+ return fs.readdirSync(docsDir)
2592
+ .filter((name) => name.endsWith(".json"))
2593
+ .map((name) => loadAgentDoc(role, name.replace(/\.json$/, "")))
2594
+ .sort((a, b) => docTopicOrder(a.topic) - docTopicOrder(b.topic) || a.topic.localeCompare(b.topic));
2595
+ }
2596
+
2597
+ function loadAgentDoc(role, topic) {
2598
+ const normalizedTopic = normalizeDocTopic(topic);
2599
+ const file = path.join(resolveDocsDir(role), `${normalizedTopic}.json`);
2600
+ if (!fs.existsSync(file)) {
2601
+ throw new Error(`agent docs topic not found: ${normalizedTopic}`);
2602
+ }
2603
+ const doc = readJSON(file, null);
2604
+ if (!doc || doc.role !== role || doc.topic !== normalizedTopic) {
2605
+ throw new Error(`invalid agent docs topic: ${normalizedTopic}`);
2606
+ }
2607
+ return {
2608
+ ...doc,
2609
+ source: {
2610
+ packaged_path: file,
2611
+ command: cliCommand("docs", "show", normalizedTopic, "--role", role, "--json")
2612
+ }
2613
+ };
2614
+ }
2615
+
2616
+ function searchAgentDocs(role, query) {
2617
+ const rawQuery = String(query).toLowerCase();
2618
+ const terms = rawQuery.split(/\s+/).filter(Boolean);
2619
+ return listAgentDocs(role)
2620
+ .map((doc) => {
2621
+ const docTerms = (doc.search_terms || []).map((term) => String(term).toLowerCase()).filter(Boolean);
2622
+ const haystack = [
2623
+ doc.topic,
2624
+ doc.title,
2625
+ doc.purpose,
2626
+ ...(doc.when_to_use || []),
2627
+ ...(doc.agent_rules || []),
2628
+ ...(doc.forbidden || []),
2629
+ ...docTerms
2630
+ ].join(" ").toLowerCase();
2631
+ const score = terms.reduce((sum, term) => sum + (haystack.includes(term) ? 1 : 0), 0) +
2632
+ docTerms.reduce((sum, term) => sum + (rawQuery.includes(term) ? 1 : 0), 0);
2633
+ return { doc, score };
2634
+ })
2635
+ .filter((entry) => entry.score > 0)
2636
+ .sort((a, b) => b.score - a.score || docTopicOrder(a.doc.topic) - docTopicOrder(b.doc.topic))
2637
+ .slice(0, 5)
2638
+ .map((entry) => ({
2639
+ topic: entry.doc.topic,
2640
+ title: entry.doc.title,
2641
+ purpose: entry.doc.purpose,
2642
+ score: entry.score,
2643
+ command: cliCommand("docs", "show", entry.doc.topic, "--role", role, "--json"),
2644
+ next_docs: Array.isArray(entry.doc.next_docs) ? entry.doc.next_docs.map((next) => next.topic).filter(Boolean) : []
2645
+ }));
2646
+ }
2647
+
2648
+ function resolveDocsDir(role) {
2649
+ const candidates = [
2650
+ process.env.ITPAY_CLI_DOCS_DIR,
2651
+ path.join(PACKAGE_ROOT, "docs", "agent", role),
2652
+ path.join(path.dirname(CLI_DIR), "share", "itpay_cli", "docs", "agent", role),
2653
+ path.join(process.cwd(), "docs", "agent", role)
2654
+ ].filter(Boolean);
2655
+ const found = candidates.find((candidate) => fs.existsSync(candidate));
2656
+ if (!found) {
2657
+ throw new Error(`ItPay agent docs not found for role ${role}. Checked: ${candidates.join(", ")}`);
2658
+ }
2659
+ return found;
2660
+ }
2661
+
2662
+ function normalizeDocTopic(topic) {
2663
+ return String(topic || "").trim().toLowerCase().replaceAll("_", "-");
2664
+ }
2665
+
2666
+ function docTopicOrder(topic) {
2667
+ const order = [
2668
+ "quickstart",
2669
+ "catalog-search",
2670
+ "product-recommendation",
2671
+ "cart-checkout",
2672
+ "payment-qr",
2673
+ "payment-wait",
2674
+ "qr-refresh",
2675
+ "secure-delivery",
2676
+ "human-claim-ui",
2677
+ "account-portal",
2678
+ "vault-agent-read",
2679
+ "recovery",
2680
+ "safety-policy"
2681
+ ];
2682
+ const index = order.indexOf(topic);
2683
+ return index === -1 ? 999 : index;
2684
+ }
2685
+
2686
+ async function skill(command, flags) {
2687
+ const role = normalizeSkillRole(flags.role || flags.skill || "buyer");
2688
+ const skillPath = resolveSkillPath(role);
2689
+ if (!command || command === "show" || command === "read") {
2690
+ const content = fs.readFileSync(skillPath, "utf8");
2691
+ if (flags.json) {
2692
+ output({ skill: role === "voltagent" ? "voltagent" : "itpay-buyer", role, path: skillPath, content });
2693
+ } else {
2694
+ process.stdout.write(content.endsWith("\n") ? content : `${content}\n`);
2695
+ }
2696
+ return;
2697
+ }
2698
+ if (command === "path") {
2699
+ if (flags.json) {
2700
+ output({ skill: role === "voltagent" ? "voltagent" : "itpay-buyer", role, path: skillPath });
2701
+ } else {
2702
+ process.stdout.write(`${skillPath}\n`);
2703
+ }
2704
+ return;
2705
+ }
2706
+ throw new Error(`unknown skill command: ${command}`);
2707
+ }
2708
+
2709
+ function normalizeSkillRole(role) {
2710
+ const normalized = String(role || "buyer").trim().toLowerCase();
2711
+ if (normalized === "buyer" || normalized === "itpay-buyer") return "buyer";
2712
+ if (normalized === "voltagent" || normalized === "legacy") return "voltagent";
2713
+ if (normalized === "merchant" || normalized === "itpay-merchant") {
2714
+ throw new Error("merchant skill is not packaged yet; use --role buyer for current external-agent tests");
2715
+ }
2716
+ throw new Error(`unsupported skill role: ${role}`);
2717
+ }
2718
+
2719
+ function resolveSkillPath(role = "buyer") {
2720
+ const skillDirName = role === "voltagent" ? "voltagent" : "itpay-buyer";
2721
+ const envPath = role === "buyer" ? process.env.ITPAY_BUYER_SKILL_PATH : process.env.ITPAY_CLI_SKILL_PATH;
2722
+ const candidates = [
2723
+ envPath,
2724
+ process.env.ITPAY_CLI_SKILL_PATH,
2725
+ path.join(PACKAGE_ROOT, "skills", skillDirName, "SKILL.md"),
2726
+ path.join(path.dirname(CLI_DIR), "share", "itpay_cli", "skills", skillDirName, "SKILL.md"),
2727
+ path.join(process.cwd(), "skills", skillDirName, "SKILL.md")
2728
+ ].filter(Boolean);
2729
+ const found = candidates.find((candidate) => fs.existsSync(candidate));
2730
+ if (!found) {
2731
+ throw new Error(`ItPay skill file not found for role ${role}. Checked: ${candidates.join(", ")}`);
2732
+ }
2733
+ return found;
2734
+ }
2735
+
2736
+ async function doctorModelCheck(target, credentials) {
2737
+ const baseUrl = target === "claude-code"
2738
+ ? credentials.anthropic_base_url
2739
+ : target === "codex" || target === "openclaw"
2740
+ ? credentials.openai_base_url
2741
+ : credentials.openai_base_url || credentials.base_url;
2742
+ if (!baseUrl || !credentials.key) {
2743
+ return { attempted: false, ok: false, error: "missing base_url or credential" };
2744
+ }
2745
+ const controller = new AbortController();
2746
+ const timer = setTimeout(() => controller.abort(), 3000);
2747
+ try {
2748
+ const response = await fetch(`${baseUrl.replace(/\/$/, "")}/models`, {
2749
+ method: "GET",
2750
+ headers: { Authorization: `Bearer ${credentials.key}` },
2751
+ signal: controller.signal
2752
+ });
2753
+ return {
2754
+ attempted: true,
2755
+ ok: response.ok,
2756
+ status: response.status,
2757
+ endpoint: `${baseUrl.replace(/\/$/, "")}/models`
2758
+ };
2759
+ } catch (error) {
2760
+ return {
2761
+ attempted: true,
2762
+ ok: false,
2763
+ endpoint: `${baseUrl.replace(/\/$/, "")}/models`,
2764
+ error: error.message
2765
+ };
2766
+ } finally {
2767
+ clearTimeout(timer);
2768
+ }
2769
+ }
2770
+
2771
+ function runtimeConfigStatus(target) {
2772
+ const paths = {
2773
+ "claude-code": [path.join(os.homedir(), ".claude", "settings.json")],
2774
+ codex: [path.join(os.homedir(), ".codex", "config.toml"), path.join(CONFIG_DIR, "voltagent.env")],
2775
+ openclaw: [path.join(os.homedir(), ".openclaw", "config.json")]
2776
+ };
2777
+ return (paths[target] || []).map((file) => ({
2778
+ path: file,
2779
+ exists: fs.existsSync(file),
2780
+ mode: fileMode(file)
2781
+ }));
2782
+ }
2783
+
2784
+ async function keys(command, flags) {
2785
+ const credentials = readCredentials();
2786
+ const grants = Object.entries(credentials)
2787
+ .filter(([key]) => key.startsWith("grant_"))
2788
+ .map(([key, value]) => ({
2789
+ grant_id: key.slice("grant_".length),
2790
+ target: value.target,
2791
+ base_url: value.base_url,
2792
+ credential_store: value.credential_store || "file",
2793
+ key: value.key ? maskSecret(value.key) : "stored"
2794
+ }));
2795
+ if (!command || command === "list") {
2796
+ output({ keys: grants });
2797
+ return;
2798
+ }
2799
+ if (command === "revoke") {
2800
+ const grantId = flags.grant || grants[0]?.grant_id;
2801
+ if (!grantId) throw new Error("grant id is required");
2802
+ const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/revoke`, { method: "POST" }, flags);
2803
+ deleteGrantCredential(grantId);
2804
+ output(response);
2805
+ return;
2806
+ }
2807
+ if (command === "rotate") {
2808
+ const grantId = flags.grant || grants[0]?.grant_id;
2809
+ if (!grantId) throw new Error("grant id is required");
2810
+ const existing = readCredentials()[`grant_${grantId}`] || {};
2811
+ const response = await api(`/api/itp/grants/${encodeURIComponent(grantId)}/rotate`, { method: "POST" }, flags);
2812
+ const storedCredential = storeGrantCredential(grantId, {
2813
+ key: response.credential.key_once,
2814
+ target: existing.target || flags.target || "",
2815
+ base_url: response.base_url,
2816
+ openai_base_url: response.openai_base_url,
2817
+ anthropic_base_url: response.anthropic_base_url,
2818
+ gemini_base_url: response.gemini_base_url,
2819
+ models: response.models || [],
2820
+ install_profiles: response.install_profiles || []
2821
+ });
2822
+ output({
2823
+ ...response,
2824
+ credential: {
2825
+ type: response.credential.type,
2826
+ rotated: true,
2827
+ stored: true,
2828
+ credential_store: storedCredential.credential_store,
2829
+ warning: storedCredential.credential_warning || undefined
2830
+ }
2831
+ });
2832
+ return;
2833
+ }
2834
+ throw new Error(`unknown keys command: ${command}`);
2835
+ }
2836
+
2837
+ async function token(command, flags) {
2838
+ if (command !== "issue") throw new Error(`unknown token command: ${command || ""}`);
2839
+ const grantId = flags.grant || readState().last_grant_id;
2840
+ if (!grantId) throw new Error("grant id is required");
2841
+ const credentials = readGrantCredential(grantId);
2842
+ if (!credentials?.key) throw new Error(`grant ${grantId} is not installed locally`);
2843
+ if (flags.stdout) {
2844
+ process.stdout.write(credentials.key);
2845
+ return;
2846
+ }
2847
+ output({
2848
+ grant_id: grantId,
2849
+ key: maskSecret(credentials.key),
2850
+ stdout_required_for_raw_token: true
2851
+ });
2852
+ }
2853
+
2854
+ async function sync(flags) {
2855
+ const [account, balanceResult, grants] = await Promise.all([
2856
+ api("/api/itp/account", { method: "GET" }, flags),
2857
+ api("/api/itp/balance", { method: "GET" }, flags),
2858
+ api("/api/itp/grants", { method: "GET" }, flags)
2859
+ ]);
2860
+ output({ account, balance: balanceResult, grants });
2861
+ }
2862
+
2863
+ async function refreshRun(run, flags = {}) {
2864
+ let next = { ...run };
2865
+ const credentials = readCredentials();
2866
+ const hasSession = Boolean(readSessionToken(credentials));
2867
+
2868
+ if (next.auth?.auth_id && !hasSession) {
2869
+ try {
2870
+ const auth = await api(`/api/itp/auth/device/${encodeURIComponent(next.auth.auth_id)}/poll`, { method: "POST" }, flags);
2871
+ if (auth.auth?.session_token) {
2872
+ writeSessionCredentials(auth.auth);
2873
+ writeConfig({
2874
+ api_base: apiBase(flags),
2875
+ account_id: auth.auth.account_id,
2876
+ device_id: auth.auth.device_id,
2877
+ web_console_url: auth.auth.web_console_url
2878
+ });
2879
+ next = mergeRun(next, {
2880
+ phase: "authenticated",
2881
+ status: "running",
2882
+ account: {
2883
+ authenticated: true,
2884
+ account_id: auth.auth.account_id,
2885
+ device_id: auth.auth.device_id,
2886
+ newapi_user_id: auth.auth.newapi_user_id || null
2887
+ },
2888
+ auth: { status: "consumed" },
2889
+ human_action: null,
2890
+ safe_summary: "Agent device authenticated."
2891
+ });
2892
+ } else if (auth.status === "authorization_pending") {
2893
+ next = mergeRun(next, {
2894
+ phase: "waiting_human_auth",
2895
+ status: "waiting_human_auth",
2896
+ auth: { status: "pending", expires_at: auth.expires_at },
2897
+ human_action: auth.human_action || auth.next_action?.human_action || next.human_action || null,
2898
+ safe_summary: "Waiting for Alipay authentication scan."
2899
+ });
2900
+ } else if (auth.status) {
2901
+ next = mergeRun(next, {
2902
+ phase: auth.status === "expired" ? "expired" : "failed",
2903
+ status: auth.status === "expired" ? "expired" : "failed",
2904
+ auth: { status: auth.status },
2905
+ safe_summary: auth.error || `Auth status: ${auth.status}`
2906
+ });
2907
+ }
2908
+ } catch (error) {
2909
+ next = mergeRun(next, { last_error: safeErrorMessage(error), safe_summary: "Could not refresh auth status." });
2910
+ }
2911
+ }
2912
+
2913
+ if (readSessionToken(readCredentials())) {
2914
+ try {
2915
+ const authStatusResult = await api("/api/itp/auth/status", { method: "GET" }, flags);
2916
+ next = mergeRun(next, {
2917
+ account: {
2918
+ authenticated: authStatusResult.authenticated !== false,
2919
+ account_id: authStatusResult.account_id || next.account?.account_id || null,
2920
+ device_id: authStatusResult.device_id || next.account?.device_id || null,
2921
+ newapi_user_id: authStatusResult.newapi_user_id || null
2922
+ }
2923
+ });
2924
+ } catch (error) {
2925
+ next = mergeRun(next, { last_error: safeErrorMessage(error) });
2926
+ }
2927
+ }
2928
+
2929
+ if (next.checkout?.checkout_id && readSessionToken(readCredentials())) {
2930
+ try {
2931
+ const checkout = await api(`/api/itp/checkout/${encodeURIComponent(next.checkout.checkout_id)}`, { method: "GET" }, flags);
2932
+ next = mergeRun(next, {
2933
+ phase: checkout.grant_id ? "grant_ready" : checkout.status === "waiting_user_payment" ? "waiting_human_payment" : next.phase,
2934
+ status: checkout.grant_id ? "grant_ready" : checkout.status === "waiting_user_payment" ? "waiting_human_payment" : next.status,
2935
+ checkout: {
2936
+ checkout_id: checkout.checkout_id,
2937
+ order_id: checkout.order_id,
2938
+ status: checkout.status,
2939
+ expires_at: checkout.expires_at
2940
+ },
2941
+ payment: { provider: next.payment_method, status: checkout.status },
2942
+ grant: { ...(next.grant || {}), grant_id: checkout.grant_id || next.grant?.grant_id || null },
2943
+ human_action: checkout.human_action || null,
2944
+ safe_summary: checkout.grant_id ? "Payment verified and grant is ready." : checkout.status === "waiting_user_payment" ? "Waiting for Alipay payment scan." : `Checkout status: ${checkout.status}`
2945
+ });
2946
+ } catch (error) {
2947
+ next = mergeRun(next, { last_error: safeErrorMessage(error), safe_summary: "Could not refresh checkout status." });
2948
+ }
2949
+ }
2950
+
2951
+ const grantId = next.grant?.grant_id || readState().last_grant_id;
2952
+ if (grantId) {
2953
+ const credential = readGrantCredential(grantId);
2954
+ if (credential?.key || credential?.credential_ref) {
2955
+ next = mergeRun(next, {
2956
+ phase: next.install_runtime ? next.phase : "grant_ready",
2957
+ grant: { grant_id: grantId, installed: true, credential_store: credential.credential_store || "file" },
2958
+ safe_summary: "Grant credential stored."
2959
+ });
2960
+ }
2961
+ }
2962
+ return next;
2963
+ }
2964
+
2965
+ function agentRunResponse(run, extra = {}) {
2966
+ const status = extra.status || (run.status && run.status !== "running" ? run.status : phaseToStatus(run.phase));
2967
+ return {
2968
+ schema_version: "itp.agent.v1",
2969
+ status,
2970
+ run_id: run.run_id,
2971
+ phase: run.phase || status,
2972
+ auth_id: extra.auth_id || run.auth?.auth_id || null,
2973
+ account_id: extra.account_id || run.account?.account_id || null,
2974
+ device_id: extra.device_id || run.account?.device_id || null,
2975
+ checkout_id: extra.checkout_id || run.checkout?.checkout_id || null,
2976
+ order_id: extra.order_id || run.checkout?.order_id || null,
2977
+ grant_id: extra.grant_id || run.grant?.grant_id || null,
2978
+ plan_id: extra.plan_id || run.plan_id || null,
2979
+ credits: extra.credits || run.credits || run.checkout?.purchase?.credits_granted || null,
2980
+ purchase: extra.purchase || run.checkout?.purchase || undefined,
2981
+ target: extra.target || run.target || "generic",
2982
+ base_url: extra.base_url || run.result?.base_url || undefined,
2983
+ openai_base_url: extra.openai_base_url || run.result?.openai_base_url || undefined,
2984
+ anthropic_base_url: extra.anthropic_base_url || run.result?.anthropic_base_url || undefined,
2985
+ gemini_base_url: extra.gemini_base_url || run.result?.gemini_base_url || undefined,
2986
+ credential: extra.credential || (run.grant?.installed ? {
2987
+ stored: true,
2988
+ credential_store: run.grant?.credential_store,
2989
+ token_command: run.grant?.grant_id ? cliCommand("token", "issue", "--grant", run.grant.grant_id, "--stdout") : undefined,
2990
+ stdout_required_for_raw_token: true
2991
+ } : undefined),
2992
+ human_action: extra.human_action || run.human_action || undefined,
2993
+ next: extra.next || nextActionForRun(run),
2994
+ next_action: extra.next_action || undefined,
2995
+ safe_user_message: extra.safe_user_message || safeUserMessageForRun(run),
2996
+ safe_summary: run.safe_summary || undefined,
2997
+ warnings: extra.warnings || [],
2998
+ secrets: {
2999
+ raw_key_included: false,
3000
+ session_token_included: false
3001
+ },
3002
+ ...extra
3003
+ };
3004
+ }
3005
+
3006
+ function phaseToStatus(phase) {
3007
+ if (phase === "waiting_human_auth") return "waiting_human_auth";
3008
+ if (phase === "waiting_human_payment") return "waiting_human_payment";
3009
+ if (phase === "grant_ready") return "grant_ready";
3010
+ if (phase === "done") return "done";
3011
+ if (phase === "expired") return "expired";
3012
+ if (phase === "failed") return "failed";
3013
+ return phase || "running";
3014
+ }
3015
+
3016
+ function nextActionForRun(run) {
3017
+ if (run.phase === "waiting_human_auth") {
3018
+ return { type: "show_qr_and_wait", command: resumeCommand(run, runResumeFlags(run)), retry_after_ms: 2000, safe_for_agent: true };
3019
+ }
3020
+ if (run.phase === "waiting_human_payment") {
3021
+ return { type: "show_qr_and_wait", command: resumeCommand(run, runResumeFlags(run, { display: "none" })), retry_after_ms: 2000, safe_for_agent: true };
3022
+ }
3023
+ if (["paid_verified", "granting", "grant_failed"].includes(run.checkout?.status)) {
3024
+ return { type: "recover_checkout", command: cliCommand("checkout", "recover", run.checkout.checkout_id, "--json"), safe_for_agent: true };
3025
+ }
3026
+ if (run.grant?.grant_id && !run.grant?.installed) {
3027
+ return { type: "install_grant", command: cliCommand("grants", "install", run.grant.grant_id, "--target", run.target || "generic", "--json"), safe_for_agent: true };
3028
+ }
3029
+ if (run.grant?.installed) {
3030
+ return { type: "done", safe_for_agent: true };
3031
+ }
3032
+ return { type: "resume", command: resumeCommand(run, runResumeFlags(run)), safe_for_agent: true };
3033
+ }
3034
+
3035
+ function runResumeFlags(run, overrides = {}) {
3036
+ return {
3037
+ host: run.agent_host || undefined,
3038
+ display: run.agent_display || undefined,
3039
+ qr_format: run.agent_qr_format || undefined,
3040
+ api_base: run.api_base || undefined,
3041
+ ...overrides
3042
+ };
3043
+ }
3044
+
3045
+ function resumeCommand(run, flags = {}, options = {}) {
3046
+ const args = ["resume", "--run-id", run.run_id, "--json"];
3047
+ appendPassthroughFlag(args, flags, "host");
3048
+ appendPassthroughFlag(args, flags, "display");
3049
+ appendPassthroughFlag(args, flags, "qr_format", "qr-format");
3050
+ appendPassthroughFlag(args, flags, "api_base", "api-base");
3051
+ appendPassthroughFlag(args, flags, "api_timeout", "api-timeout");
3052
+ if (options.no_wait_payment) args.push("--no-wait-payment");
3053
+ return cliCommand(...args);
3054
+ }
3055
+
3056
+ function appendPassthroughFlag(args, flags, key, flagName = key) {
3057
+ const value = flags[key];
3058
+ if (value === undefined || value === null || value === false) return;
3059
+ args.push(`--${flagName}`);
3060
+ if (value !== true) args.push(String(value));
3061
+ }
3062
+
3063
+ function cliCommand(...args) {
3064
+ const override = process.env.ITP_COMMAND;
3065
+ const base = override
3066
+ ? override
3067
+ : `${shellQuote(process.execPath)} ${shellQuote(CLI_FILE)}`;
3068
+ return [base, ...args.map((arg) => shellQuote(String(arg)))].join(" ");
3069
+ }
3070
+
3071
+ function shellQuote(value) {
3072
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
3073
+ return `'${value.replace(/'/g, "'\\''")}'`;
3074
+ }
3075
+
3076
+ function safeUserMessageForRun(run) {
3077
+ if (run.phase === "waiting_human_auth") return "Please scan the Alipay authentication QR. I will continue automatically after approval.";
3078
+ if (run.phase === "waiting_human_payment") return "Please scan the Alipay payment QR. I will continue automatically after payment is verified.";
3079
+ if (run.grant?.installed) return "Payment verified. The API credential is stored locally.";
3080
+ return run.safe_summary || "ITPay setup is in progress.";
3081
+ }
3082
+
3083
+ function safeErrorMessage(error) {
3084
+ return String(error?.message || error || "unknown error")
3085
+ .replace(/itp_sess_[A-Za-z0-9_-]+/g, "itp_sess_****")
3086
+ .replace(/sk-[A-Za-z0-9_-]+/g, "sk-****");
3087
+ }
3088
+
3089
+ function humanActionSummaryLines(action) {
3090
+ if (!action?.url) return [];
3091
+ const lines = [
3092
+ "ITP HUMAN ACTION REQUIRED",
3093
+ `Title: ${action.title || "Scan with Alipay"}`,
3094
+ `URL: ${action.url}`
3095
+ ];
3096
+ if (action.local_qr_path) {
3097
+ lines.push(`Local QR image: ${action.local_qr_path}`);
3098
+ }
3099
+ if (action.qr_png_url) {
3100
+ lines.push(`QR PNG: ${action.qr_png_url}`);
3101
+ }
3102
+ if (action.qr_image_url) {
3103
+ lines.push(`QR image: ${action.qr_image_url}`);
3104
+ }
3105
+ if (action.mobile_wallet_url) {
3106
+ lines.push(`Mobile wallet link: ${action.mobile_wallet_url}`);
3107
+ }
3108
+ if (action.oauth_start_url) {
3109
+ lines.push(`Alipay auth fallback: ${action.oauth_start_url}`);
3110
+ }
3111
+ if (action.fallback_text && !action.fallback_text.includes(action.url)) {
3112
+ lines.push(`Fallback: ${action.fallback_text}`);
3113
+ }
3114
+ if (action.expires_at) {
3115
+ lines.push(`Expires at: ${formatActionTime(action.expires_at)}`);
3116
+ }
3117
+ return lines;
3118
+ }
3119
+
3120
+ function writeHumanActionSummary(action, suffix = "") {
3121
+ const lines = humanActionSummaryLines(action);
3122
+ if (!lines.length) return;
3123
+ process.stderr.write(`\n${lines.join("\n")}${suffix ? `\n${suffix}` : ""}\n\n`);
3124
+ }
3125
+
3126
+ function waitHeartbeatMs(flags = {}) {
3127
+ if (flags.quiet || process.env.ITP_WAIT_HEARTBEAT_SECONDS === "0") return 0;
3128
+ return Math.max(5000, Number(flags.heartbeat || process.env.ITP_WAIT_HEARTBEAT_SECONDS || 20) * 1000);
3129
+ }
3130
+
3131
+ function writeWaitHeartbeat({ kind, idName, idValue, status, action, lastHeartbeatAt, flags, command }) {
3132
+ const heartbeatMs = waitHeartbeatMs(flags);
3133
+ if (!heartbeatMs) return lastHeartbeatAt;
3134
+ const now = Date.now();
3135
+ if (now - lastHeartbeatAt < heartbeatMs) return lastHeartbeatAt;
3136
+ const lines = [
3137
+ `ITP waiting for ${kind}: ${idName}=${idValue} status=${status}`,
3138
+ action?.url ? `URL: ${action.url}` : null,
3139
+ action?.local_qr_path ? `Local QR image: ${action.local_qr_path}` : null,
3140
+ action?.qr_png_url ? `QR PNG: ${action.qr_png_url}` : null,
3141
+ action?.qr_image_url ? `QR image: ${action.qr_image_url}` : null,
3142
+ action?.mobile_wallet_url ? `Mobile wallet link: ${action.mobile_wallet_url}` : null,
3143
+ action?.oauth_start_url ? `Alipay auth fallback: ${action.oauth_start_url}` : null,
3144
+ command ? `Resume command: ${command}` : null
3145
+ ].filter(Boolean);
3146
+ process.stderr.write(`\n${lines.join("\n")}\n\n`);
3147
+ return now;
3148
+ }
3149
+
3150
+ async function renderHumanAction(action, flags = {}) {
3151
+ if (!action?.url) return null;
3152
+ const qrImageURL = preferredHumanActionQRURL(action);
3153
+ const mode = String(flags.display || process.env.ITP_DISPLAY || "").toLowerCase() || "auto";
3154
+ annotateHumanActionPresentation(action, qrImageURL);
3155
+ if (flags.json || mode === "none" || mode === "json") {
3156
+ if (qrImageURL && shouldPrepareLocalQRForJSON(mode, flags, action)) {
3157
+ const localPath = await prepareLocalQRFile(action, qrImageURL, flags, mode !== "file" && !flags.qr_file && !process.env.ITP_QR_FILE);
3158
+ if (localPath) {
3159
+ attachAgentQRImage(action, qrImageURL, localPath);
3160
+ persistHumanAction(action, flags);
3161
+ return { rendered: false, mode: "json-local-qr", outputs: [localPath] };
3162
+ }
3163
+ }
3164
+ if (!qrImageURL && shouldGenerateLocalQRFromActionURL(action) && shouldPrepareLocalQRForJSON(mode, flags, action)) {
3165
+ const localPath = await prepareGeneratedActionQRFile(action, flags, mode !== "file" && !flags.qr_file && !process.env.ITP_QR_FILE);
3166
+ if (localPath) {
3167
+ attachAgentGeneratedQRImage(action, localPath);
3168
+ annotateHumanActionPresentation(action, "");
3169
+ persistHumanAction(action, flags);
3170
+ return { rendered: false, mode: "json-local-auth-qr", outputs: [localPath] };
3171
+ }
3172
+ }
3173
+ return { rendered: false, mode: flags.json ? "json" : mode };
3174
+ }
3175
+ const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
3176
+ if (["discord", "telegram", "whatsapp"].includes(host)) {
3177
+ return { rendered: false, mode: "chat-json", host };
3178
+ }
3179
+ if (shouldUseAgentTextQR(flags)) {
3180
+ if (qrImageURL) {
3181
+ const localPath = await prepareLocalQRFile(action, qrImageURL, flags, true);
3182
+ attachAgentQRImage(action, qrImageURL, localPath);
3183
+ persistHumanAction(action, flags);
3184
+ return { rendered: false, mode: localPath ? "agent-local-image-qr" : "agent-image-qr", host, outputs: [localPath || "preferred_qr_url"] };
3185
+ }
3186
+ await attachAgentTextQR(action, flags);
3187
+ persistHumanAction(action, flags);
3188
+ return { rendered: false, mode: "agent-text-qr", host, outputs: ["agent_text_qr"] };
3189
+ }
3190
+
3191
+ const renderResult = { rendered: false, mode, outputs: [] };
3192
+ writeHumanActionSummary(action);
3193
+ if ((mode === "auto" || mode === "browser") && shouldOpenBrowser(flags)) {
3194
+ if (openBrowser(action.mobile_wallet_url || qrImageURL || action.url)) {
3195
+ renderResult.rendered = true;
3196
+ renderResult.outputs.push("browser");
3197
+ }
3198
+ if (mode === "browser") return renderResult;
3199
+ }
3200
+
3201
+ if (mode === "file" || flags.qr_file || process.env.ITP_QR_FILE) {
3202
+ const file = flags.qr_file || process.env.ITP_QR_FILE || defaultQRFilePath(action, qrImageURL);
3203
+ if (qrImageURL) {
3204
+ await downloadQRImage(qrImageURL, file, flags);
3205
+ action.local_qr_path = file;
3206
+ action.local_qr_mime = qrMimeType(qrImageURL, action);
3207
+ } else {
3208
+ await QRCode.toFile(file, action.url, { errorCorrectionLevel: "M", width: 512, margin: 2 });
3209
+ action.local_qr_path = file;
3210
+ action.local_qr_mime = "image/png";
3211
+ }
3212
+ process.stderr.write(`Alipay QR image: ${file}\n`);
3213
+ renderResult.rendered = true;
3214
+ renderResult.outputs.push(file);
3215
+ if (mode === "file") return renderResult;
3216
+ }
3217
+
3218
+ if (qrImageURL) {
3219
+ process.stderr.write(`Alipay QR image URL: ${qrImageURL}\n`);
3220
+ if (action.mobile_wallet_url) process.stderr.write(`Mobile wallet link: ${action.mobile_wallet_url}\n`);
3221
+ renderResult.outputs.push("preferred_qr_url");
3222
+ return renderResult;
3223
+ }
3224
+
3225
+ if ((mode === "auto" || mode === "terminal") && shouldRenderTerminalQR(host)) {
3226
+ const qr = await QRCode.toString(action.url, {
3227
+ type: terminalQRType(flags, host),
3228
+ small: true,
3229
+ errorCorrectionLevel: "M"
3230
+ });
3231
+ process.stderr.write(`\n${action.title || "Scan with Alipay"}\n`);
3232
+ if (action.description) process.stderr.write(`${action.description}\n`);
3233
+ process.stderr.write(`${qr}\n`);
3234
+ process.stderr.write(`Alipay action URL: ${action.url}\n`);
3235
+ if (action.expires_at) process.stderr.write(`Expires at: ${formatActionTime(action.expires_at)}\n`);
3236
+ renderResult.rendered = true;
3237
+ renderResult.outputs.push("terminal");
3238
+ return renderResult;
3239
+ }
3240
+
3241
+ process.stderr.write(`${action.fallback_text || `Open Alipay URL: ${action.url}`}\n`);
3242
+ renderResult.outputs.push("url");
3243
+ return renderResult;
3244
+ }
3245
+
3246
+ function preferredHumanActionQRURL(action) {
3247
+ return action?.qr_png_url ||
3248
+ humanActionPresentationURL(action, "qr_png_url") ||
3249
+ action?.qr_image_url ||
3250
+ action?.qr?.png_url ||
3251
+ action?.qr?.image_url ||
3252
+ humanActionPresentationURL(action, "qr_svg_url") ||
3253
+ "";
3254
+ }
3255
+
3256
+ function humanActionPresentationURL(action, type) {
3257
+ const display = action?.presentation?.display;
3258
+ if (!Array.isArray(display)) return "";
3259
+ const found = display.find((item) => item?.type === type && item?.url);
3260
+ return found?.url || "";
3261
+ }
3262
+
3263
+ function annotateHumanActionPresentation(action, qrImageURL) {
3264
+ if (!action) return action;
3265
+ if (qrImageURL) {
3266
+ action.preferred_qr_url = qrImageURL;
3267
+ action.preferred_qr_mime = qrMimeType(qrImageURL, action);
3268
+ }
3269
+ const mobileURL = action.mobile_wallet_url || humanActionPresentationURL(action, "mobile_wallet_url");
3270
+ if (mobileURL) action.mobile_wallet_url = mobileURL;
3271
+ action.agent_display_hint = {
3272
+ primary: action.local_qr_path ? "local_qr_path" : (action.qr_png_url ? "qr_png_url" : "preferred_qr_url"),
3273
+ desktop: action.kind === "auth_qr"
3274
+ ? "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."
3275
+ : "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.",
3276
+ mobile: "Show mobile_wallet_url as a clickable human-only fallback when present.",
3277
+ proof: "Only payment_intent.verified proves payment. QR display or page open is not payment proof."
3278
+ };
3279
+ return action;
3280
+ }
3281
+
3282
+ function shouldPrepareLocalQRForJSON(mode, flags = {}, action = {}) {
3283
+ if (mode === "file" || flags.qr_file || process.env.ITP_QR_FILE) return true;
3284
+ if (action?.qr_png_url || action?.preferred_qr_url || action?.qr_image_url) return true;
3285
+ if (action?.kind === "auth_qr" && mode !== "none" && mode !== "json") return true;
3286
+ return mode === "agent" || mode === "chat";
3287
+ }
3288
+
3289
+ function shouldGenerateLocalQRFromActionURL(action = {}) {
3290
+ return action?.kind === "auth_qr" && Boolean(action?.url);
3291
+ }
3292
+
3293
+ async function prepareGeneratedActionQRFile(action, flags = {}, optional = false) {
3294
+ if (!action?.url) return "";
3295
+ const file = flags.qr_file || process.env.ITP_QR_FILE || defaultQRFilePath(action, "");
3296
+ try {
3297
+ await QRCode.toFile(file, action.url, { errorCorrectionLevel: "M", width: 512, margin: 2 });
3298
+ } catch (error) {
3299
+ if (!optional) throw error;
3300
+ action.local_qr_error = safeErrorMessage(error);
3301
+ return "";
3302
+ }
3303
+ action.local_qr_path = file;
3304
+ action.local_qr_mime = "image/png";
3305
+ return file;
3306
+ }
3307
+
3308
+ async function prepareLocalQRFile(action, qrImageURL, flags = {}, optional = false) {
3309
+ if (!qrImageURL) return "";
3310
+ const file = flags.qr_file || process.env.ITP_QR_FILE || defaultQRFilePath(action, qrImageURL);
3311
+ try {
3312
+ await downloadQRImage(qrImageURL, file, flags);
3313
+ } catch (error) {
3314
+ if (!optional) throw error;
3315
+ action.local_qr_error = safeErrorMessage(error);
3316
+ return "";
3317
+ }
3318
+ action.local_qr_path = file;
3319
+ action.local_qr_mime = qrMimeType(qrImageURL, action);
3320
+ return file;
3321
+ }
3322
+
3323
+ function defaultQRFilePath(action, qrImageURL) {
3324
+ const id = sanitizeFilename(action?.payment_intent_id || action?.id || "qr");
3325
+ return path.join(os.tmpdir(), `itp-${id}.${qrFileExtension(qrImageURL, action)}`);
3326
+ }
3327
+
3328
+ function qrFileExtension(qrImageURL, action = {}) {
3329
+ const mime = qrMimeType(qrImageURL, action);
3330
+ if (mime === "image/png") return "png";
3331
+ if (mime === "image/svg+xml") return "svg";
3332
+ return "img";
3333
+ }
3334
+
3335
+ function qrMimeType(qrImageURL, action = {}) {
3336
+ if (qrImageURL && action?.qr_png_url && qrImageURL === action.qr_png_url) return "image/png";
3337
+ if (String(qrImageURL || "").toLowerCase().includes(".png")) return "image/png";
3338
+ if (String(qrImageURL || "").toLowerCase().includes(".svg")) return "image/svg+xml";
3339
+ return action?.preferred_qr_mime || "image/png";
3340
+ }
3341
+
3342
+ function sanitizeFilename(value) {
3343
+ return String(value || "qr").replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 96) || "qr";
3344
+ }
3345
+
3346
+ function formatActionTime(value) {
3347
+ if (value === null || value === undefined || value === "") return "";
3348
+ if (typeof value === "number" || /^[0-9]+$/.test(String(value))) {
3349
+ const numeric = Number(value);
3350
+ const millis = numeric > 100000000000 ? numeric : numeric * 1000;
3351
+ const date = new Date(millis);
3352
+ if (!Number.isNaN(date.getTime())) return date.toISOString();
3353
+ }
3354
+ const date = new Date(String(value));
3355
+ if (!Number.isNaN(date.getTime())) return date.toISOString();
3356
+ return String(value);
3357
+ }
3358
+
3359
+ function shouldUseAgentTextQR(flags = {}) {
3360
+ const mode = String(flags.display || process.env.ITP_DISPLAY || "").toLowerCase() || "auto";
3361
+ const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
3362
+ return mode === "chat" || mode === "agent" || (["gemini", "gemini-cli"].includes(host) && mode === "auto");
3363
+ }
3364
+
3365
+ function shouldReturnAfterAgentTextQR(flags = {}) {
3366
+ if (flags.wait || flags.wait_human) return false;
3367
+ return shouldUseAgentTextQR(flags);
3368
+ }
3369
+
3370
+ function attachAgentQRImage(action, qrImageURL, localPath = "") {
3371
+ if (!action || !qrImageURL) return action;
3372
+ action.preferred_qr_url = qrImageURL;
3373
+ if (qrMimeType(qrImageURL, action) === "image/png") {
3374
+ action.qr_png_url = action.qr_png_url || qrImageURL;
3375
+ } else {
3376
+ action.qr_image_url = action.qr_image_url || qrImageURL;
3377
+ }
3378
+ if (!Array.isArray(action.display)) {
3379
+ action.display = [];
3380
+ }
3381
+ if (!action.display.some((item) => item?.type === "image")) {
3382
+ action.display.push({
3383
+ type: "image",
3384
+ format: qrFileExtension(qrImageURL, action),
3385
+ url: qrImageURL,
3386
+ local_path: localPath || undefined,
3387
+ 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."
3388
+ });
3389
+ }
3390
+ return action;
3391
+ }
3392
+
3393
+ function attachAgentGeneratedQRImage(action, localPath = "") {
3394
+ if (!action || !localPath) return action;
3395
+ if (!Array.isArray(action.display)) {
3396
+ action.display = [];
3397
+ }
3398
+ if (!action.display.some((item) => item?.type === "image" && item?.local_path === localPath)) {
3399
+ action.display.push({
3400
+ type: "image",
3401
+ format: "png",
3402
+ local_path: localPath,
3403
+ instructions: "Render this local ItPay first-purchase entry QR for the human to scan. It starts account login/registration/profile authorization and should continue to payment for the same checkout after approval; it is not payment proof."
3404
+ });
3405
+ }
3406
+ return action;
3407
+ }
3408
+
3409
+ async function attachAgentTextQR(action, flags = {}) {
3410
+ if (!action?.url || action.agent_text_qr) return action;
3411
+ const qr = await QRCode.toString(action.url, {
3412
+ type: "utf8",
3413
+ small: true,
3414
+ errorCorrectionLevel: "M"
3415
+ });
3416
+ const lines = qr.replace(/\s+$/g, "").split(/\r?\n/);
3417
+ const text = lines.join("\n");
3418
+ const sha256 = crypto.createHash("sha256").update(text).digest("hex");
3419
+ action.agent_text_qr = {
3420
+ type: "terminal_text",
3421
+ format: "unicode",
3422
+ lines,
3423
+ text,
3424
+ fenced_text: `\`\`\`text\n${text}\n\`\`\``,
3425
+ line_count: lines.length,
3426
+ sha256,
3427
+ url: action.url,
3428
+ allowed_characters: "Only U+2588 FULL BLOCK, U+2580 UPPER HALF BLOCK, U+2584 LOWER HALF BLOCK, spaces, and newlines are valid inside the QR body.",
3429
+ instructions: "Paste fenced_text verbatim into the normal assistant message, not the shell panel. Do not retype, translate, add line numbers, or add words inside the QR body. If any letters, digits, or Chinese characters appear inside the QR body, discard it and use the QR image URL/fallback link instead."
3430
+ };
3431
+ if (!Array.isArray(action.display)) {
3432
+ action.display = [];
3433
+ }
3434
+ if (!action.display.some((item) => item?.type === "terminal_text")) {
3435
+ action.display.push({
3436
+ type: "terminal_text",
3437
+ format: "unicode",
3438
+ lines
3439
+ });
3440
+ }
3441
+ return action;
3442
+ }
3443
+
3444
+ async function downloadQRImage(qrImageURL, file, flags = {}) {
3445
+ const controller = new AbortController();
3446
+ const timer = setTimeout(() => controller.abort(), apiTimeoutMs(flags));
3447
+ let response;
3448
+ try {
3449
+ response = await fetch(qrImageURL, { method: "GET", signal: controller.signal });
3450
+ } catch (error) {
3451
+ if (error?.name === "AbortError") {
3452
+ throw new Error(`QR image download timed out: ${qrImageURL}`);
3453
+ }
3454
+ throw error;
3455
+ } finally {
3456
+ clearTimeout(timer);
3457
+ }
3458
+ if (!response.ok) {
3459
+ throw new Error(`QR image download failed: ${response.status}`);
3460
+ }
3461
+ const bytes = new Uint8Array(await response.arrayBuffer());
3462
+ fs.mkdirSync(path.dirname(file), { recursive: true });
3463
+ fs.writeFileSync(file, bytes);
3464
+ }
3465
+
3466
+ function persistHumanAction(action, flags = {}) {
3467
+ const runId = flags.run_id || readState().current_run_id;
3468
+ if (!runId || !action?.id) return;
3469
+ const run = readRun(runId);
3470
+ if (!run?.human_action?.id || run.human_action.id !== action.id) return;
3471
+ writeRun(mergeRun(run, { human_action: action }));
3472
+ }
3473
+
3474
+ function shouldRenderTerminalQR(host) {
3475
+ if (process.stderr.isTTY) return true;
3476
+ return ["gemini", "gemini-cli"].includes(host);
3477
+ }
3478
+
3479
+ function terminalQRType(flags = {}, host = "") {
3480
+ const requested = String(flags.qr_format || process.env.ITP_QR_FORMAT || "").toLowerCase();
3481
+ if (requested === "unicode" || requested === "utf8") return "utf8";
3482
+ if (requested === "ansi" || requested === "terminal") return "terminal";
3483
+ if (["gemini", "gemini-cli"].includes(host)) return "utf8";
3484
+ return "terminal";
3485
+ }
3486
+
3487
+ function shouldOpenBrowser(flags = {}) {
3488
+ if (flags.no_open_browser || process.env.ITP_OPEN_BROWSER === "false" || process.env.ITP_OPEN_BROWSER === "0") return false;
3489
+ if (flags.open_browser || process.env.ITP_OPEN_BROWSER === "true" || process.env.ITP_OPEN_BROWSER === "1") return true;
3490
+ if (process.env.SSH_CONNECTION || process.env.CI) return false;
3491
+ return Boolean(process.stderr.isTTY && (process.platform === "darwin" || process.platform === "win32" || process.env.DISPLAY || process.env.WAYLAND_DISPLAY || process.env.WSL_DISTRO_NAME));
3492
+ }
3493
+
3494
+ function openBrowser(targetURL) {
3495
+ try {
3496
+ if (process.platform === "darwin") {
3497
+ execFileSync("open", [targetURL], { stdio: "ignore", timeout: 2000 });
3498
+ return true;
3499
+ }
3500
+ if (process.platform === "win32") {
3501
+ execFileSync("cmd", ["/c", "start", "", targetURL], { stdio: "ignore", timeout: 2000 });
3502
+ return true;
3503
+ }
3504
+ if (process.env.WSL_DISTRO_NAME && commandExists("cmd.exe")) {
3505
+ execFileSync("cmd.exe", ["/c", "start", "", targetURL], { stdio: "ignore", timeout: 2000 });
3506
+ return true;
3507
+ }
3508
+ if (commandExists("xdg-open")) {
3509
+ execFileSync("xdg-open", [targetURL], { stdio: "ignore", timeout: 2000 });
3510
+ return true;
3511
+ }
3512
+ } catch {
3513
+ return false;
3514
+ }
3515
+ return false;
3516
+ }
3517
+
3518
+ async function admin(command, rest, flags) {
3519
+ if (command === "orders") {
3520
+ const params = new URLSearchParams();
3521
+ for (const key of ["account_id", "status", "provider", "limit"]) {
3522
+ if (flags[key]) params.set(key, flags[key]);
3523
+ }
3524
+ const suffix = params.toString() ? `?${params.toString()}` : "";
3525
+ output(await api(`/api/itp/admin/orders${suffix}`, { method: "GET" }, flags));
3526
+ return;
3527
+ }
3528
+ if (command === "payment-events") {
3529
+ const params = new URLSearchParams();
3530
+ for (const key of ["order_id", "checkout_id", "out_trade_no", "provider", "event_type", "signature_verified", "limit"]) {
3531
+ if (flags[key]) params.set(key, flags[key]);
3532
+ }
3533
+ const suffix = params.toString() ? `?${params.toString()}` : "";
3534
+ output(await api(`/api/itp/admin/payment-events${suffix}`, { method: "GET" }, flags));
3535
+ return;
3536
+ }
3537
+ if (command === "outbox") {
3538
+ const params = new URLSearchParams();
3539
+ for (const key of ["status", "event_type", "aggregate_id", "limit"]) {
3540
+ if (flags[key]) params.set(key, flags[key]);
3541
+ }
3542
+ const suffix = params.toString() ? `?${params.toString()}` : "";
3543
+ output(await api(`/api/itp/admin/outbox${suffix}`, { method: "GET" }, flags));
3544
+ return;
3545
+ }
3546
+ if (command === "process-outbox") {
3547
+ output(await api("/api/itp/admin/outbox/process", { method: "POST" }, flags));
3548
+ return;
3549
+ }
3550
+ if (command === "recover-order") {
3551
+ const orderId = rest[0];
3552
+ if (!orderId) throw new Error("order_id is required");
3553
+ output(await api(`/api/itp/admin/orders/${encodeURIComponent(orderId)}/recover`, { method: "POST" }, flags));
3554
+ return;
3555
+ }
3556
+ throw new Error(`unknown admin command: ${command || ""}`);
3557
+ }
3558
+
3559
+ async function coreApi(pathname, options = {}, flags = {}) {
3560
+ const headers = { "Content-Type": "application/json" };
3561
+ const credentials = readCredentials();
3562
+ if (flags.access_token) {
3563
+ const raw = String(flags.access_token);
3564
+ headers.Authorization = raw.toLowerCase().startsWith("bearer ") ? raw : `Bearer ${raw}`;
3565
+ } else {
3566
+ const sessionToken = readSessionToken(credentials);
3567
+ if (sessionToken) headers.Authorization = `Bearer ${sessionToken}`;
3568
+ }
3569
+ if (options.idempotencyKey || flags.idempotency_key) {
3570
+ headers["Idempotency-Key"] = String(options.idempotencyKey || flags.idempotency_key);
3571
+ }
3572
+ if (!options.noAgentHeaders) {
3573
+ headers["X-ItPay-Agent-Fingerprint"] = coreAgentFingerprint(flags);
3574
+ headers["X-ItPay-Agent-Name"] = coreAgentDisplayName(flags);
3575
+ }
3576
+ if (options.ops) {
3577
+ headers["X-ItPay-Ops-Token"] = sandboxOpsToken(flags);
3578
+ }
3579
+ if (flags.request_id) headers["X-Request-ID"] = String(flags.request_id);
3580
+ if (flags.correlation_id) headers["X-Correlation-ID"] = String(flags.correlation_id);
3581
+ const targetURL = coreURL(pathname, flags);
3582
+ const timeoutMs = apiTimeoutMs(flags);
3583
+ const controller = new AbortController();
3584
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3585
+ let response;
3586
+ try {
3587
+ response = await fetch(targetURL, {
3588
+ method: options.method || "GET",
3589
+ headers,
3590
+ body: options.body ? JSON.stringify(options.body) : undefined,
3591
+ signal: controller.signal
3592
+ });
3593
+ } catch (error) {
3594
+ if (error?.name === "AbortError") {
3595
+ throw new Error(`request timed out after ${Math.ceil(timeoutMs / 1000)}s: ${targetURL}`);
3596
+ }
3597
+ throw error;
3598
+ } finally {
3599
+ clearTimeout(timer);
3600
+ }
3601
+ const text = await response.text();
3602
+ let payload = {};
3603
+ if (text) {
3604
+ try {
3605
+ payload = JSON.parse(text);
3606
+ } catch {
3607
+ payload = { text };
3608
+ }
3609
+ }
3610
+ if (!response.ok || payload.success === false) {
3611
+ const error = new Error(payload.error || payload.message || `request failed: ${response.status}`);
3612
+ error.status = response.status;
3613
+ throw error;
3614
+ }
3615
+ return payload.data ?? payload;
3616
+ }
3617
+
3618
+ function coreAgentFingerprint(flags = {}) {
3619
+ const explicit = flags.agent_fingerprint || flags.agent_device_fingerprint || process.env.ITPAY_AGENT_FINGERPRINT || process.env.ITPAY_AGENT_DEVICE_FINGERPRINT;
3620
+ if (explicit) return String(explicit);
3621
+ const state = readState();
3622
+ if (state.core_agent_fingerprint) return state.core_agent_fingerprint;
3623
+ const fingerprint = `itp_cli_${cryptoRandom()}`;
3624
+ writeState({ ...state, core_agent_fingerprint: fingerprint });
3625
+ return fingerprint;
3626
+ }
3627
+
3628
+ function coreAgentDisplayName(flags = {}) {
3629
+ return String(flags.agent_name || flags.agent_display_name || process.env.ITPAY_AGENT_NAME || "ItPay CLI buyer agent");
3630
+ }
3631
+
3632
+ function coreURL(pathname, flags = {}) {
3633
+ if (/^https?:\/\//i.test(String(pathname))) return String(pathname);
3634
+ return `${coreApiBase(flags)}${pathname.startsWith("/") ? "" : "/"}${pathname}`;
3635
+ }
3636
+
3637
+ function coreApiBase(flags = {}) {
3638
+ const base = flags.api_base || flags.core_api_base || process.env.ITPAY_API_BASE || process.env.ITPAY_CORE_API_BASE || process.env.ITPAY_CORE_BASE_URL || "http://127.0.0.1:18080";
3639
+ return String(base).replace(/\/$/, "");
3640
+ }
3641
+
3642
+ function sandboxOpsToken(flags = {}) {
3643
+ const token = flags.ops_token || flags.sandbox_ops_token || process.env.ITPAY_SANDBOX_OPS_TOKEN || process.env.ITPAY_OPS_TOKEN;
3644
+ if (!token) throw new Error("sandbox ops token is required; set ITPAY_SANDBOX_OPS_TOKEN or pass --ops-token");
3645
+ return String(token);
3646
+ }
3647
+
3648
+ function queryString(params) {
3649
+ const raw = params.toString();
3650
+ return raw ? `?${raw}` : "";
3651
+ }
3652
+
3653
+ function appendURLQuery(target, params) {
3654
+ const suffix = params.toString();
3655
+ if (!suffix) return target;
3656
+ return `${target}${String(target).includes("?") ? "&" : "?"}${suffix}`;
3657
+ }
3658
+
3659
+ function positional(values, index) {
3660
+ const value = values[index];
3661
+ if (!value || String(value).startsWith("--")) return "";
3662
+ return String(value);
3663
+ }
3664
+
3665
+ function positionalArgs(values = []) {
3666
+ const result = [];
3667
+ for (let i = 0; i < values.length; i += 1) {
3668
+ const value = values[i];
3669
+ if (!value) continue;
3670
+ if (String(value).startsWith("--")) {
3671
+ const next = values[i + 1];
3672
+ if (next && !String(next).startsWith("--")) i += 1;
3673
+ continue;
3674
+ }
3675
+ result.push(String(value));
3676
+ }
3677
+ return result;
3678
+ }
3679
+
3680
+ function stripInternalBuyerFields(value) {
3681
+ if (Array.isArray(value)) return value.map(stripInternalBuyerFields);
3682
+ if (!value || typeof value !== "object") return value;
3683
+ const result = {};
3684
+ for (const [key, nested] of Object.entries(value)) {
3685
+ if (key === "next_actions") continue;
3686
+ result[key] = stripInternalBuyerFields(nested);
3687
+ }
3688
+ return result;
3689
+ }
3690
+
3691
+ async function api(pathname, options, flags = {}) {
3692
+ const config = readConfig();
3693
+ const credentials = readCredentials();
3694
+ const base = apiBase(flags, config);
3695
+ const headers = { "Content-Type": "application/json" };
3696
+ if (flags.access_token) {
3697
+ headers.Authorization = String(flags.access_token);
3698
+ } else {
3699
+ const sessionToken = readSessionToken(credentials);
3700
+ if (sessionToken) {
3701
+ headers.Authorization = `Bearer ${sessionToken}`;
3702
+ }
3703
+ }
3704
+ if (flags.new_api_user || flags.new_api_user_id || flags.user_id) {
3705
+ headers["New-Api-User"] = String(flags.new_api_user || flags.new_api_user_id || flags.user_id);
3706
+ }
3707
+ const timeoutMs = apiTimeoutMs(flags);
3708
+ const controller = new AbortController();
3709
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3710
+ let response;
3711
+ try {
3712
+ response = await fetch(`${base}${pathname}`, {
3713
+ method: options.method,
3714
+ headers,
3715
+ body: options.body ? JSON.stringify(options.body) : undefined,
3716
+ signal: controller.signal
3717
+ });
3718
+ } catch (error) {
3719
+ if (error?.name === "AbortError") {
3720
+ throw new Error(`request timed out after ${Math.ceil(timeoutMs / 1000)}s: ${pathname}`);
3721
+ }
3722
+ throw error;
3723
+ } finally {
3724
+ clearTimeout(timer);
3725
+ }
3726
+ const payload = await response.json().catch(() => ({}));
3727
+ if (!response.ok || payload.success === false) {
3728
+ throw new Error(payload.message || `request failed: ${response.status}`);
3729
+ }
3730
+ return payload.data ?? payload;
3731
+ }
3732
+
3733
+ function apiTimeoutMs(flags = {}) {
3734
+ const seconds = Number(flags.api_timeout || process.env.ITP_API_TIMEOUT_SECONDS || 45);
3735
+ if (!Number.isFinite(seconds) || seconds <= 0) return 45000;
3736
+ return Math.max(5000, seconds * 1000);
3737
+ }
3738
+
3739
+ function parseFlags(args) {
3740
+ const flags = {};
3741
+ for (let i = 0; i < args.length; i += 1) {
3742
+ const arg = args[i];
3743
+ if (!arg.startsWith("--")) continue;
3744
+ const key = arg.slice(2).replaceAll("-", "_");
3745
+ const next = args[i + 1];
3746
+ if (!next || next.startsWith("--")) {
3747
+ flags[key] = true;
3748
+ } else {
3749
+ flags[key] = next;
3750
+ i += 1;
3751
+ }
3752
+ }
3753
+ return flags;
3754
+ }
3755
+
3756
+ function normalizePurchaseFlags(flags, required = false) {
3757
+ const plan = typeof flags.plan === "string" ? flags.plan.trim() : "";
3758
+ const rawCredits = flags.credits ?? flags.credit ?? null;
3759
+ const hasCredits = rawCredits !== null && rawCredits !== undefined && rawCredits !== false;
3760
+ if (plan && hasCredits) {
3761
+ throw new Error("use either --plan or --credits, not both");
3762
+ }
3763
+ if (hasCredits) {
3764
+ const credits = Number(rawCredits);
3765
+ if (!Number.isInteger(credits) || credits < 20) {
3766
+ throw new Error("--credits must be an integer greater than or equal to 20");
3767
+ }
3768
+ return {
3769
+ kind: "custom",
3770
+ plan: null,
3771
+ credits,
3772
+ key: `credits-${credits}`
3773
+ };
3774
+ }
3775
+ if (plan) {
3776
+ if (plan === "coding-100") {
3777
+ throw new Error("coding-100 is disabled; use credit-100, credit-300, credit-500, or --credits <amount>");
3778
+ }
3779
+ return {
3780
+ kind: "plan",
3781
+ plan,
3782
+ credits: null,
3783
+ key: `plan-${plan}`
3784
+ };
3785
+ }
3786
+ if (required) {
3787
+ throw new Error("choose a purchase: --credits <integer >=20> or --plan credit-100|credit-300|credit-500");
3788
+ }
3789
+ return { kind: null, plan: null, credits: null, key: "none" };
3790
+ }
3791
+
3792
+ function apiBase(flags = {}, config = readConfig()) {
3793
+ return (flags.api_base || config.api_base || DEFAULT_API_BASE).replace(/\/$/, "");
3794
+ }
3795
+
3796
+ function readConfig() {
3797
+ return readJSON(CONFIG_PATH, {});
3798
+ }
3799
+
3800
+ function writeConfig(value) {
3801
+ writeJSON0600(CONFIG_PATH, value);
3802
+ }
3803
+
3804
+ function readState() {
3805
+ return readJSON(STATE_PATH, {});
3806
+ }
3807
+
3808
+ function writeState(value) {
3809
+ writeJSON0600(STATE_PATH, value);
3810
+ }
3811
+
3812
+ function runPath(runId) {
3813
+ return path.join(RUNS_DIR, `${runId}.json`);
3814
+ }
3815
+
3816
+ function readRun(runId) {
3817
+ if (!runId) return null;
3818
+ return readJSON(runPath(runId), null);
3819
+ }
3820
+
3821
+ function listRuns() {
3822
+ if (!fs.existsSync(RUNS_DIR)) return [];
3823
+ return fs.readdirSync(RUNS_DIR)
3824
+ .filter((name) => name.endsWith(".json"))
3825
+ .map((name) => readJSON(path.join(RUNS_DIR, name), null))
3826
+ .filter(Boolean)
3827
+ .sort((a, b) => String(b.updated_at || "").localeCompare(String(a.updated_at || "")));
3828
+ }
3829
+
3830
+ function writeRun(run) {
3831
+ if (!run?.run_id) throw new Error("run_id is required");
3832
+ ensureConfigDir();
3833
+ fs.mkdirSync(RUNS_DIR, { recursive: true });
3834
+ try {
3835
+ fs.chmodSync(RUNS_DIR, 0o700);
3836
+ } catch {
3837
+ // Best effort on platforms without POSIX modes.
3838
+ }
3839
+ const next = {
3840
+ ...run,
3841
+ schema_version: "itp.run.v1",
3842
+ updated_at: new Date().toISOString()
3843
+ };
3844
+ const file = runPath(next.run_id);
3845
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
3846
+ fs.writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
3847
+ fs.renameSync(tmp, file);
3848
+ try {
3849
+ fs.chmodSync(file, 0o600);
3850
+ } catch {
3851
+ // Best effort on platforms without POSIX modes.
3852
+ }
3853
+ writeState({ ...readState(), current_run_id: next.run_id });
3854
+ return next;
3855
+ }
3856
+
3857
+ function mergeRun(run, patch) {
3858
+ return {
3859
+ ...(run || {}),
3860
+ ...patch,
3861
+ auth: patch.auth === undefined ? run?.auth : { ...(run?.auth || {}), ...(patch.auth || {}) },
3862
+ account: patch.account === undefined ? run?.account : { ...(run?.account || {}), ...(patch.account || {}) },
3863
+ checkout: patch.checkout === undefined ? run?.checkout : { ...(run?.checkout || {}), ...(patch.checkout || {}) },
3864
+ payment: patch.payment === undefined ? run?.payment : { ...(run?.payment || {}), ...(patch.payment || {}) },
3865
+ grant: patch.grant === undefined ? run?.grant : { ...(run?.grant || {}), ...(patch.grant || {}) },
3866
+ result: patch.result === undefined ? run?.result : { ...(run?.result || {}), ...(patch.result || {}) }
3867
+ };
3868
+ }
3869
+
3870
+ function updateRun(run, patch) {
3871
+ return writeRun(mergeRun(run, patch));
3872
+ }
3873
+
3874
+ function updateCurrentRun(patch, flags = {}) {
3875
+ const run = readRun(flags.run_id || readState().current_run_id);
3876
+ if (!run) return null;
3877
+ return writeRun(mergeRun(run, patch));
3878
+ }
3879
+
3880
+ function prepareSetupRun(flags, options) {
3881
+ const explicitRunId = flags.run_id || null;
3882
+ const state = readState();
3883
+ let run = explicitRunId ? readRun(explicitRunId) : (!flags.new_run ? readRun(state.current_run_id) : null);
3884
+ const reusable = run
3885
+ && !["done", "installed", "failed", "cancelled"].includes(run.status)
3886
+ && run.phase !== "done"
3887
+ && (!run.plan_id || run.plan_id === options.plan)
3888
+ && (options.plan || !run.credits || Number(run.credits) === Number(options.credits || 0))
3889
+ && (!run.payment_method || run.payment_method === options.method);
3890
+ if (reusable) {
3891
+ return writeRun(mergeRun(run, {
3892
+ target: options.target,
3893
+ plan_id: options.plan,
3894
+ credits: options.credits,
3895
+ purchase_kind: options.plan ? "plan" : "custom",
3896
+ payment_method: options.method,
3897
+ agent_host: flags.host || run.agent_host || null,
3898
+ agent_display: flags.display || run.agent_display || null,
3899
+ agent_qr_format: flags.qr_format || run.agent_qr_format || null,
3900
+ install_runtime: Boolean(options.install_runtime),
3901
+ status: "running"
3902
+ }));
3903
+ }
3904
+ if (flags.resume && explicitRunId && !run) {
3905
+ throw new Error(`run not found: ${explicitRunId}`);
3906
+ }
3907
+ const runId = explicitRunId || `run_${cryptoRandom()}`;
3908
+ return writeRun({
3909
+ schema_version: "itp.run.v1",
3910
+ run_id: runId,
3911
+ created_at: new Date().toISOString(),
3912
+ api_base: apiBase(flags),
3913
+ target: options.target,
3914
+ install_runtime: Boolean(options.install_runtime),
3915
+ plan_id: options.plan,
3916
+ credits: options.credits,
3917
+ purchase_kind: options.plan ? "plan" : "custom",
3918
+ payment_method: options.method,
3919
+ agent_host: flags.host || null,
3920
+ agent_display: flags.display || null,
3921
+ agent_qr_format: flags.qr_format || null,
3922
+ idempotency_key: flags.idempotency_key || `setup:${runId}:${options.plan || `credits-${options.credits}`}`,
3923
+ phase: "new",
3924
+ status: "running",
3925
+ safe_summary: "Setup started."
3926
+ });
3927
+ }
3928
+
3929
+ async function withStateLock(fn) {
3930
+ ensureConfigDir();
3931
+ const staleMs = 10 * 60 * 1000;
3932
+ try {
3933
+ const stat = fs.statSync(LOCK_PATH);
3934
+ const lock = readJSON(LOCK_PATH, {});
3935
+ if ((lock.pid && !processIsRunning(lock.pid)) || Date.now() - stat.mtimeMs > staleMs) {
3936
+ fs.unlinkSync(LOCK_PATH);
3937
+ }
3938
+ } catch {
3939
+ // No lock or unreadable stale state.
3940
+ }
3941
+ let fd;
3942
+ try {
3943
+ fd = fs.openSync(LOCK_PATH, "wx", 0o600);
3944
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }));
3945
+ } catch {
3946
+ throw new Error("another itp setup/status operation is running; retry shortly or remove stale ~/.itp/state.lock");
3947
+ } finally {
3948
+ if (fd !== undefined) fs.closeSync(fd);
3949
+ }
3950
+ try {
3951
+ return await fn();
3952
+ } finally {
3953
+ try {
3954
+ fs.unlinkSync(LOCK_PATH);
3955
+ } catch {
3956
+ // Best effort cleanup.
3957
+ }
3958
+ }
3959
+ }
3960
+
3961
+ function readCredentials() {
3962
+ return readJSON(CREDENTIALS_PATH, {});
3963
+ }
3964
+
3965
+ function writeCredentials(value) {
3966
+ writeJSON0600(CREDENTIALS_PATH, value);
3967
+ }
3968
+
3969
+ function writeJSON0600(file, value) {
3970
+ ensureConfigDir();
3971
+ fs.writeFileSync(file, JSON.stringify(value, null, 2), { mode: 0o600 });
3972
+ fs.chmodSync(file, 0o600);
3973
+ }
3974
+
3975
+ function writeSessionCredentials(response) {
3976
+ const currentAccountId = readConfig().account_id;
3977
+ let credentials = deleteSessionCredential(readCredentials());
3978
+ if (currentAccountId && currentAccountId !== response.account_id) {
3979
+ for (const key of Object.keys(credentials)) {
3980
+ if (key.startsWith("grant_")) {
3981
+ const grantId = key.slice("grant_".length);
3982
+ deleteGrantCredential(grantId);
3983
+ }
3984
+ }
3985
+ credentials = {};
3986
+ }
3987
+ writeCredentials({ ...credentials, ...storeSessionCredential(response) });
3988
+ }
3989
+
3990
+ function storeSessionCredential(response) {
3991
+ const token = response.session_token;
3992
+ const ref = `voltagent:session:${response.account_id}:${response.device_id}`;
3993
+ const nativeStore = writeNativeSecret(ref, token);
3994
+ if (nativeStore.ok) {
3995
+ return {
3996
+ session_token_store: nativeStore.store,
3997
+ session_token_ref: nativeStore.ref
3998
+ };
3999
+ }
4000
+ return {
4001
+ session_token: token,
4002
+ session_token_store: "file",
4003
+ session_token_warning: nativeStore.error
4004
+ };
4005
+ }
4006
+
4007
+ function readSessionToken(credentials = readCredentials()) {
4008
+ if (credentials.session_token) {
4009
+ return credentials.session_token;
4010
+ }
4011
+ if (credentials.session_token_store && credentials.session_token_ref) {
4012
+ return readNativeSecret(credentials.session_token_store, credentials.session_token_ref);
4013
+ }
4014
+ return "";
4015
+ }
4016
+
4017
+ function deleteSessionCredential(credentials) {
4018
+ if (!credentials) {
4019
+ return {};
4020
+ }
4021
+ if (credentials.session_token_store && credentials.session_token_ref) {
4022
+ deleteNativeSecret(credentials.session_token_store, credentials.session_token_ref);
4023
+ }
4024
+ delete credentials.session_token;
4025
+ delete credentials.session_token_store;
4026
+ delete credentials.session_token_ref;
4027
+ delete credentials.session_token_warning;
4028
+ return credentials;
4029
+ }
4030
+
4031
+ function sanitizeAuthResponse(response) {
4032
+ const { session_token, ...safe } = response;
4033
+ return { ...safe, session_stored: Boolean(session_token) };
4034
+ }
4035
+
4036
+ function storeGrantCredential(grantId, credential) {
4037
+ const credentials = readCredentials();
4038
+ const key = credential.key;
4039
+ const record = { ...credential };
4040
+ delete record.key;
4041
+ const nativeStore = writeNativeSecret(grantSecretRef(grantId), key);
4042
+ if (nativeStore.ok) {
4043
+ record.credential_store = nativeStore.store;
4044
+ record.credential_ref = nativeStore.ref;
4045
+ } else {
4046
+ record.key = key;
4047
+ record.credential_store = "file";
4048
+ record.credential_warning = nativeStore.error;
4049
+ }
4050
+ credentials[`grant_${grantId}`] = record;
4051
+ writeCredentials(credentials);
4052
+ return record;
4053
+ }
4054
+
4055
+ function readGrantCredential(grantId) {
4056
+ const record = readCredentials()[`grant_${grantId}`];
4057
+ if (!record) return null;
4058
+ if (record.key) return record;
4059
+ const key = readNativeSecret(record.credential_store, record.credential_ref);
4060
+ return key ? { ...record, key } : record;
4061
+ }
4062
+
4063
+ function deleteGrantCredential(grantId) {
4064
+ const credentials = readCredentials();
4065
+ const record = credentials[`grant_${grantId}`];
4066
+ if (record?.credential_store && record?.credential_ref) {
4067
+ deleteNativeSecret(record.credential_store, record.credential_ref);
4068
+ }
4069
+ delete credentials[`grant_${grantId}`];
4070
+ writeCredentials(credentials);
4071
+ }
4072
+
4073
+ function grantSecretRef(grantId) {
4074
+ return `voltagent:${grantId}`;
4075
+ }
4076
+
4077
+ function detectNativeCredentialStore() {
4078
+ if (!shouldUseNativeCredentialStore()) return "file";
4079
+ if (process.platform === "darwin" && commandExists("security")) return "macos-keychain";
4080
+ if (process.platform === "linux" && commandExists("secret-tool")) return "secret-tool";
4081
+ return "unavailable";
4082
+ }
4083
+
4084
+ function writeNativeSecret(ref, secret) {
4085
+ if (!secret) return { ok: false, error: "empty secret" };
4086
+ if (!shouldUseNativeCredentialStore()) {
4087
+ return { ok: false, error: "native credential store disabled for non-interactive agent host" };
4088
+ }
4089
+ if (process.platform === "darwin" && commandExists("security")) {
4090
+ try {
4091
+ execFileSync("security", [
4092
+ "add-generic-password",
4093
+ "-a",
4094
+ ref,
4095
+ "-s",
4096
+ "VoltaGent",
4097
+ "-w",
4098
+ secret,
4099
+ "-U"
4100
+ ], { stdio: "ignore" });
4101
+ return { ok: true, store: "macos-keychain", ref };
4102
+ } catch (error) {
4103
+ return { ok: false, error: `macOS Keychain unavailable: ${error.message}` };
4104
+ }
4105
+ }
4106
+ if (process.platform === "linux" && commandExists("secret-tool")) {
4107
+ try {
4108
+ execFileSync("secret-tool", [
4109
+ "store",
4110
+ "--label=VoltaGent",
4111
+ "service",
4112
+ "VoltaGent",
4113
+ "account",
4114
+ ref
4115
+ ], { input: secret, stdio: ["pipe", "ignore", "ignore"] });
4116
+ return { ok: true, store: "secret-tool", ref };
4117
+ } catch (error) {
4118
+ return { ok: false, error: `secret-tool unavailable: ${error.message}` };
4119
+ }
4120
+ }
4121
+ return { ok: false, error: "native credential store unavailable" };
4122
+ }
4123
+
4124
+ function shouldUseNativeCredentialStore() {
4125
+ const store = String(process.env.ITP_CREDENTIAL_STORE || "").toLowerCase();
4126
+ if (store === "file") return false;
4127
+ if (store === "native" || store === "keychain" || store === "secret-tool") return true;
4128
+ const disabled = String(process.env.ITP_DISABLE_NATIVE_CREDENTIAL_STORE || "").toLowerCase();
4129
+ if (["1", "true", "yes"].includes(disabled)) return false;
4130
+ if (process.env.CODEX_CI || process.env.CODEX_SHELL || process.env.CODEX_THREAD_ID) return false;
4131
+ if (process.env.CI && !process.env.GITHUB_ACTIONS) return false;
4132
+ return true;
4133
+ }
4134
+
4135
+ function readNativeSecret(store, ref) {
4136
+ if (!store || !ref) return "";
4137
+ try {
4138
+ if (store === "macos-keychain") {
4139
+ return execFileSync("security", [
4140
+ "find-generic-password",
4141
+ "-a",
4142
+ ref,
4143
+ "-s",
4144
+ "VoltaGent",
4145
+ "-w"
4146
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
4147
+ }
4148
+ if (store === "secret-tool") {
4149
+ return execFileSync("secret-tool", [
4150
+ "lookup",
4151
+ "service",
4152
+ "VoltaGent",
4153
+ "account",
4154
+ ref
4155
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
4156
+ }
4157
+ } catch {
4158
+ return "";
4159
+ }
4160
+ return "";
4161
+ }
4162
+
4163
+ function deleteNativeSecret(store, ref) {
4164
+ try {
4165
+ if (store === "macos-keychain") {
4166
+ execFileSync("security", [
4167
+ "delete-generic-password",
4168
+ "-a",
4169
+ ref,
4170
+ "-s",
4171
+ "VoltaGent"
4172
+ ], { stdio: "ignore" });
4173
+ }
4174
+ if (store === "secret-tool") {
4175
+ execFileSync("secret-tool", [
4176
+ "clear",
4177
+ "service",
4178
+ "VoltaGent",
4179
+ "account",
4180
+ ref
4181
+ ], { stdio: "ignore" });
4182
+ }
4183
+ } catch {
4184
+ // The local record is still removed; missing native secrets are harmless.
4185
+ }
4186
+ }
4187
+
4188
+ function commandExists(command) {
4189
+ try {
4190
+ execFileSync("which", [command], { stdio: "ignore" });
4191
+ return true;
4192
+ } catch {
4193
+ return false;
4194
+ }
4195
+ }
4196
+
4197
+ function processIsRunning(pid) {
4198
+ const numericPid = Number(pid);
4199
+ if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
4200
+ try {
4201
+ process.kill(numericPid, 0);
4202
+ return true;
4203
+ } catch {
4204
+ return false;
4205
+ }
4206
+ }
4207
+
4208
+ function ensureConfigDir() {
4209
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
4210
+ try {
4211
+ fs.chmodSync(CONFIG_DIR, 0o700);
4212
+ } catch {
4213
+ // Best effort; individual secret files are still written as 0600.
4214
+ }
4215
+ }
4216
+
4217
+ function readText(file, fallback) {
4218
+ try {
4219
+ return fs.readFileSync(file, "utf8");
4220
+ } catch {
4221
+ return fallback;
4222
+ }
4223
+ }
4224
+
4225
+ function readJSON(file, fallback) {
4226
+ try {
4227
+ return JSON.parse(fs.readFileSync(file, "utf8"));
4228
+ } catch {
4229
+ return fallback;
4230
+ }
4231
+ }
4232
+
4233
+ function writeJSONWithBackup(file, value, dryRun) {
4234
+ return writeTextWithBackup(file, `${JSON.stringify(value, null, 2)}\n`, 0o600, dryRun);
4235
+ }
4236
+
4237
+ function writeTextWithBackup(file, content, mode, dryRun) {
4238
+ const backupPath = fs.existsSync(file) ? `${file}.itp-bak-${Date.now()}` : "";
4239
+ if (dryRun) {
4240
+ return { action: fs.existsSync(file) ? "would_update" : "would_create", backup_path: backupPath || null };
4241
+ }
4242
+ const dir = path.dirname(file);
4243
+ if (dir === CONFIG_DIR) {
4244
+ ensureConfigDir();
4245
+ } else {
4246
+ fs.mkdirSync(dir, { recursive: true });
4247
+ }
4248
+ if (backupPath) {
4249
+ fs.copyFileSync(file, backupPath);
4250
+ fs.chmodSync(backupPath, mode);
4251
+ }
4252
+ fs.writeFileSync(file, content, { mode });
4253
+ fs.chmodSync(file, mode);
4254
+ return { action: backupPath ? "updated" : "created", backup_path: backupPath || null };
4255
+ }
4256
+
4257
+ function fileMode(file) {
4258
+ try {
4259
+ return `0${(fs.statSync(file).mode & 0o777).toString(8)}`;
4260
+ } catch {
4261
+ return null;
4262
+ }
4263
+ }
4264
+
4265
+ function replaceManagedBlock(source, name, block) {
4266
+ const start = `# >>> itp ${name}`;
4267
+ const end = `# <<< itp ${name}`;
4268
+ const managed = `${start}\n${block.trim()}\n${end}`;
4269
+ const pattern = new RegExp(`${escapeRegExp(start)}[\\s\\S]*?${escapeRegExp(end)}`, "m");
4270
+ const trimmed = source.trimEnd();
4271
+ if (pattern.test(source)) return source.replace(pattern, managed);
4272
+ return `${trimmed}${trimmed ? "\n\n" : ""}${managed}\n`;
4273
+ }
4274
+
4275
+ function escapeRegExp(value) {
4276
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4277
+ }
4278
+
4279
+ function escapeTomlString(value) {
4280
+ return String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"');
4281
+ }
4282
+
4283
+ function currentExecutable() {
4284
+ return process.argv[1] || "itp";
4285
+ }
4286
+
4287
+ function quoteShell(value) {
4288
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
4289
+ }
4290
+
4291
+ function output(value) {
4292
+ console.log(JSON.stringify(value, null, 2));
4293
+ }
4294
+
4295
+ function outputError(error) {
4296
+ console.error(JSON.stringify({ success: false, message: safeErrorMessage(error) }, null, 2));
4297
+ }
4298
+
4299
+ function maskSecret(secret) {
4300
+ if (!secret) return "";
4301
+ if (secret.length <= 8) return "********";
4302
+ return `${secret.slice(0, 4)}********${secret.slice(-4)}`;
4303
+ }
4304
+
4305
+ function cryptoRandom() {
4306
+ return crypto.randomUUID();
4307
+ }
4308
+
4309
+ function sleep(ms) {
4310
+ return new Promise((resolve) => setTimeout(resolve, ms));
4311
+ }