@itpay/cli 0.1.6 → 0.1.8

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 CHANGED
@@ -8,7 +8,6 @@ import { execFileSync } from "node:child_process";
8
8
  import { fileURLToPath } from "node:url";
9
9
  import QRCode from "qrcode";
10
10
 
11
- const VERSION = "0.1.5";
12
11
  const DEFAULT_API_BASE = process.env.ITPAY_API_BASE || process.env.ITPAY_CORE_API_BASE || process.env.ITPAY_CORE_BASE_URL || "https://dev.api.itpay.ai";
13
12
  const CONFIG_DIR = path.join(os.homedir(), ".itp");
14
13
  const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
@@ -19,6 +18,7 @@ const LOCK_PATH = path.join(CONFIG_DIR, "state.lock");
19
18
  const CLI_FILE = fileURLToPath(import.meta.url);
20
19
  const CLI_DIR = path.dirname(CLI_FILE);
21
20
  const PACKAGE_ROOT = path.dirname(CLI_DIR);
21
+ const VERSION = packageVersion();
22
22
 
23
23
  main().catch((error) => {
24
24
  outputError(error);
@@ -213,13 +213,14 @@ async function main() {
213
213
  "buyer payment refresh-qr <payment_intent_id> --reason order-not-found --json",
214
214
  "buyer deliveries list --checkout <checkout_id> --json",
215
215
  "buyer deliveries show <delivery_id> --checkout <checkout_id> --json",
216
+ "buyer refund create --order <order_id> --amount-minor 1000 --currency CNY --reason buyer_requested --json",
217
+ "buyer refund list --order <order_id> --json",
218
+ "buyer refund show <refund_id> --json",
219
+ "buyer refund cancel <refund_id> --reason buyer_changed_mind --json",
216
220
  "buyer vault grants list --checkout <checkout_id> --json",
217
221
  "buyer vault grants read <agent_read_grant_id> --json",
218
222
  "buyer vault read --order <order_id> --artifact <vault_artifact_id> --json",
219
223
  "account login-link --json",
220
- "ops sandbox worker run-once --json",
221
- "ops sandbox recover-alipay-once --json",
222
- "ops sandbox payment query <payment_intent_id> --json",
223
224
  "status --json",
224
225
  "docs list --role buyer --json",
225
226
  "docs show quickstart --role buyer --json",
@@ -842,6 +843,58 @@ async function buyer(command, rest, flags) {
842
843
  return;
843
844
  }
844
845
  }
846
+ if (command === "refund") {
847
+ if (subcommand === "create") {
848
+ const orderID = flags.order || flags.order_id || positional(rest, 1);
849
+ if (!orderID) throw new Error("order_id is required");
850
+ const refund = await createBuyerRefund(orderID, flags);
851
+ if (refund.status === "policy_risk_confirmation_required") {
852
+ output(buyerRunOutput(refund));
853
+ return;
854
+ }
855
+ output(buyerRunOutput({
856
+ status: refund.status || "refund_requested",
857
+ refund,
858
+ refund_eligibility: refund.refund_eligibility || null,
859
+ agent_next_actions: ["watch_refund_status", "explain_refund_policy"]
860
+ }));
861
+ return;
862
+ }
863
+ if (subcommand === "list") {
864
+ const orderID = flags.order || flags.order_id || positional(rest, 1);
865
+ if (!orderID) throw new Error("order_id is required");
866
+ const refunds = await listBuyerRefunds(orderID, flags);
867
+ output(buyerRunOutput({
868
+ status: "refunds",
869
+ order_id: orderID,
870
+ ...refunds,
871
+ agent_next_actions: ["show_refund_status"]
872
+ }));
873
+ return;
874
+ }
875
+ if (subcommand === "show") {
876
+ const refundID = flags.refund || flags.refund_id || positional(rest, 1);
877
+ if (!refundID) throw new Error("refund_id is required");
878
+ const refund = await getBuyerRefund(refundID, flags);
879
+ output(buyerRunOutput({
880
+ status: refund.status || "refund",
881
+ refund,
882
+ agent_next_actions: ["show_refund_status"]
883
+ }));
884
+ return;
885
+ }
886
+ if (subcommand === "cancel") {
887
+ const refundID = flags.refund || flags.refund_id || positional(rest, 1);
888
+ if (!refundID) throw new Error("refund_id is required");
889
+ const refund = await cancelBuyerRefund(refundID, flags);
890
+ output(buyerRunOutput({
891
+ status: refund.status || "refund_canceled",
892
+ refund,
893
+ agent_next_actions: ["show_refund_status", "claim_delivery_if_still_needed"]
894
+ }));
895
+ return;
896
+ }
897
+ }
845
898
  if (command === "vault") {
846
899
  if (subcommand === "grants") {
847
900
  const action = rest[1] && !String(rest[1]).startsWith("--") ? rest[1] : "list";
@@ -966,6 +1019,12 @@ function booleanFlag(value) {
966
1019
  throw new Error(`invalid boolean flag value: ${value}`);
967
1020
  }
968
1021
 
1022
+ function intFlag(value, name) {
1023
+ const number = Number(value);
1024
+ if (!Number.isInteger(number)) throw new Error(`${name} must be an integer`);
1025
+ return number;
1026
+ }
1027
+
969
1028
  async function ops(command, rest, flags) {
970
1029
  if (command !== "sandbox") throw new Error(`unknown ops command: ${command || ""}`);
971
1030
  const area = rest[0];
@@ -984,6 +1043,75 @@ async function ops(command, rest, flags) {
984
1043
  output(await coreApi(`/v1/payment-intents/${encodeURIComponent(paymentIntentID)}/alipay-sandbox-query`, { method: "POST", ops: true }, flags));
985
1044
  return;
986
1045
  }
1046
+ if (area === "refund") {
1047
+ const refundID = flags.refund || flags.refund_id || positional(rest, 2);
1048
+ if (!refundID) throw new Error("refund_id is required");
1049
+ if (action === "show") {
1050
+ output(await coreApi(`/v1/sandbox/ops/refunds/${encodeURIComponent(refundID)}`, { method: "GET", ops: true }, flags));
1051
+ return;
1052
+ }
1053
+ if (action === "approve" || action === "reject") {
1054
+ output(await coreApi(`/v1/sandbox/ops/refunds/${encodeURIComponent(refundID)}/${action}`, {
1055
+ method: "POST",
1056
+ ops: true,
1057
+ idempotencyKey: flags.idempotency_key || `idem_cli_refund_${action}_${refundID}`,
1058
+ body: {
1059
+ reason_code: flags.reason || flags.reason_code || (action === "approve" ? "approved_by_ops" : "not_eligible"),
1060
+ note: flags.note || ""
1061
+ }
1062
+ }, flags));
1063
+ return;
1064
+ }
1065
+ if (action === "execute") {
1066
+ output(await coreApi(`/v1/sandbox/ops/refunds/${encodeURIComponent(refundID)}/execute`, {
1067
+ method: "POST",
1068
+ ops: true,
1069
+ idempotencyKey: flags.idempotency_key || `idem_cli_refund_execute_${refundID}`
1070
+ }, flags));
1071
+ return;
1072
+ }
1073
+ }
1074
+ if (area === "ledger" && action === "entries") {
1075
+ const params = new URLSearchParams();
1076
+ if (flags.order || flags.order_id) params.set("order_id", String(flags.order || flags.order_id));
1077
+ if (flags.refund || flags.refund_id) params.set("refund_id", String(flags.refund || flags.refund_id));
1078
+ if (flags.payment_intent || flags.payment_intent_id) params.set("payment_intent_id", String(flags.payment_intent || flags.payment_intent_id));
1079
+ if ([...params.keys()].length === 0) {
1080
+ throw new Error("ledger filter is required: use --order, --refund, or --payment-intent");
1081
+ }
1082
+ output(await coreApi(`/v1/sandbox/ops/ledger/entries${queryString(params)}`, { method: "GET", ops: true }, flags));
1083
+ return;
1084
+ }
1085
+ if (area === "reconciliation") {
1086
+ if (action === "run") {
1087
+ output(await coreApi("/v1/sandbox/ops/reconciliation-runs", {
1088
+ method: "POST",
1089
+ ops: true,
1090
+ idempotencyKey: flags.idempotency_key || `idem_cli_reconciliation_${cryptoRandom()}`,
1091
+ body: {
1092
+ reconciliation_run_id: flags.reconciliation_run_id || flags.run_id || "",
1093
+ status: flags.status || "matched",
1094
+ expected_amount_minor: intFlag(flags.expected_amount_minor || flags.expected || 0, "expected_amount_minor"),
1095
+ observed_amount_minor: intFlag(flags.observed_amount_minor || flags.observed || 0, "observed_amount_minor"),
1096
+ currency: flags.currency || "CNY",
1097
+ raw_statement_ref: flags.raw_statement_ref || flags.statement_ref || ""
1098
+ }
1099
+ }, flags));
1100
+ return;
1101
+ }
1102
+ if (action === "show") {
1103
+ const runID = flags.reconciliation_run_id || flags.run_id || positional(rest, 2);
1104
+ if (!runID) throw new Error("reconciliation_run_id is required");
1105
+ output(await coreApi(`/v1/sandbox/ops/reconciliation-runs/${encodeURIComponent(runID)}`, { method: "GET", ops: true }, flags));
1106
+ return;
1107
+ }
1108
+ }
1109
+ if (area === "settlement" && action === "show") {
1110
+ const settlementBatchID = flags.settlement_batch || flags.settlement_batch_id || positional(rest, 2);
1111
+ if (!settlementBatchID) throw new Error("settlement_batch_id is required");
1112
+ output(await coreApi(`/v1/sandbox/ops/settlement-batches/${encodeURIComponent(settlementBatchID)}`, { method: "GET", ops: true }, flags));
1113
+ return;
1114
+ }
987
1115
  throw new Error(`unknown ops sandbox command: ${rest.join(" ")}`);
988
1116
  }
989
1117
 
@@ -1462,6 +1590,67 @@ async function getBuyerCheckout(checkoutID, flags = {}) {
1462
1590
  return await coreApi(`/v1/checkouts/${encodeURIComponent(checkoutID)}`, { method: "GET" }, flags);
1463
1591
  }
1464
1592
 
1593
+ async function createBuyerRefund(orderID, flags = {}) {
1594
+ if (flags.amount !== undefined) {
1595
+ throw new Error("use --amount-minor for refund amount");
1596
+ }
1597
+ if (flags.refund_scope && flags.refund_scope !== "order") {
1598
+ throw new Error("unsupported_refund_scope");
1599
+ }
1600
+ const order = await getBuyerOrderDetail(orderID, flags);
1601
+ const eligibility = order.refund_eligibility || order.order_detail?.refund_eligibility || null;
1602
+ if (eligibility && eligibility.likely_refundable === false && !booleanFlag(flags.confirm_policy_risk || false)) {
1603
+ return {
1604
+ status: "policy_risk_confirmation_required",
1605
+ order_id: orderID,
1606
+ refund_eligibility: eligibility,
1607
+ agent_next_actions: [
1608
+ "explain_refund_policy",
1609
+ "ask_human_to_confirm_policy_risk",
1610
+ `retry_with_${cliCommand("buyer", "refund", "create", orderID, "--confirm-policy-risk", "true", "--json")}`
1611
+ ],
1612
+ submitted: false
1613
+ };
1614
+ }
1615
+ return await coreApi(`/v1/me/orders/${encodeURIComponent(orderID)}/refunds`, {
1616
+ method: "POST",
1617
+ headers: { "X-ItPay-Client-Surface": "cli" },
1618
+ idempotencyKey: flags.idempotency_key || `idem_cli_refund_create_${orderID}`,
1619
+ body: {
1620
+ refund_scope: flags.refund_scope || "order",
1621
+ order_line_item_ids: csvValues(flags.order_line_item_ids || flags.line_ids || flags.line_id),
1622
+ amount_minor: intFlag(flags.amount_minor, "amount_minor"),
1623
+ currency: flags.currency || "CNY",
1624
+ reason_code: flags.reason || flags.reason_code || "buyer_requested",
1625
+ reason_note: flags.note || flags.reason_note || ""
1626
+ }
1627
+ }, flags);
1628
+ }
1629
+
1630
+ async function getBuyerOrderDetail(orderID, flags = {}) {
1631
+ return await coreApi(`/v1/me/orders/${encodeURIComponent(orderID)}`, { method: "GET" }, flags);
1632
+ }
1633
+
1634
+ async function listBuyerRefunds(orderID, flags = {}) {
1635
+ return await coreApi(`/v1/me/orders/${encodeURIComponent(orderID)}/refunds`, { method: "GET" }, flags);
1636
+ }
1637
+
1638
+ async function getBuyerRefund(refundID, flags = {}) {
1639
+ return await coreApi(`/v1/me/refunds/${encodeURIComponent(refundID)}`, { method: "GET" }, flags);
1640
+ }
1641
+
1642
+ async function cancelBuyerRefund(refundID, flags = {}) {
1643
+ return await coreApi(`/v1/me/refunds/${encodeURIComponent(refundID)}/cancel`, {
1644
+ method: "POST",
1645
+ headers: { "X-ItPay-Client-Surface": "cli" },
1646
+ idempotencyKey: flags.idempotency_key || `idem_cli_refund_cancel_${refundID}`,
1647
+ body: {
1648
+ reason_code: flags.reason || flags.reason_code || "buyer_changed_mind",
1649
+ reason_note: flags.note || flags.reason_note || ""
1650
+ }
1651
+ }, flags);
1652
+ }
1653
+
1465
1654
  async function getBuyerPaymentIntent(paymentIntentID, flags = {}) {
1466
1655
  return await coreApi(`/v1/payment-intents/${encodeURIComponent(paymentIntentID)}`, { method: "GET" }, flags);
1467
1656
  }
@@ -3233,15 +3422,6 @@ async function renderHumanAction(action, flags = {}) {
3233
3422
  return { rendered: false, mode: "json-local-qr", outputs: [localPath] };
3234
3423
  }
3235
3424
  }
3236
- if (!qrImageURL && shouldGenerateLocalQRFromActionURL(action) && shouldPrepareLocalQRForJSON(mode, flags, action)) {
3237
- const localPath = await prepareGeneratedActionQRFile(action, flags, mode !== "file" && !flags.qr_file && !process.env.ITP_QR_FILE);
3238
- if (localPath) {
3239
- attachAgentGeneratedQRImage(action, localPath);
3240
- annotateHumanActionPresentation(action, "");
3241
- persistHumanAction(action, flags);
3242
- return { rendered: false, mode: "json-local-auth-qr", outputs: [localPath] };
3243
- }
3244
- }
3245
3425
  return { rendered: false, mode: flags.json ? "json" : mode };
3246
3426
  }
3247
3427
  const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
@@ -3255,9 +3435,9 @@ async function renderHumanAction(action, flags = {}) {
3255
3435
  persistHumanAction(action, flags);
3256
3436
  return { rendered: false, mode: localPath ? "agent-local-image-qr" : "agent-image-qr", host, outputs: [localPath || "preferred_qr_url"] };
3257
3437
  }
3258
- await attachAgentTextQR(action, flags);
3438
+ process.stderr.write(`No QR image available. Open payment URL: ${action.url}\n`);
3259
3439
  persistHumanAction(action, flags);
3260
- return { rendered: false, mode: "agent-text-qr", host, outputs: ["agent_text_qr"] };
3440
+ return { rendered: false, mode: "agent-url-fallback", host, outputs: ["payment_url"] };
3261
3441
  }
3262
3442
 
3263
3443
  const renderResult = { rendered: false, mode, outputs: [] };
@@ -3276,15 +3456,13 @@ async function renderHumanAction(action, flags = {}) {
3276
3456
  await downloadQRImage(qrImageURL, file, flags);
3277
3457
  action.local_qr_path = file;
3278
3458
  action.local_qr_mime = qrMimeType(qrImageURL, action);
3459
+ process.stderr.write(`Alipay QR image: ${file}\n`);
3460
+ renderResult.rendered = true;
3461
+ renderResult.outputs.push(file);
3462
+ if (mode === "file") return renderResult;
3279
3463
  } else {
3280
- await QRCode.toFile(file, action.url, { errorCorrectionLevel: "M", width: 512, margin: 2 });
3281
- action.local_qr_path = file;
3282
- action.local_qr_mime = "image/png";
3464
+ process.stderr.write(`No QR image available. Open payment URL: ${action.url}\n`);
3283
3465
  }
3284
- process.stderr.write(`Alipay QR image: ${file}\n`);
3285
- renderResult.rendered = true;
3286
- renderResult.outputs.push(file);
3287
- if (mode === "file") return renderResult;
3288
3466
  }
3289
3467
 
3290
3468
  if (qrImageURL) {
@@ -3354,27 +3532,7 @@ function annotateHumanActionPresentation(action, qrImageURL) {
3354
3532
  function shouldPrepareLocalQRForJSON(mode, flags = {}, action = {}) {
3355
3533
  if (mode === "file" || flags.qr_file || process.env.ITP_QR_FILE) return true;
3356
3534
  if (action?.qr_png_url || action?.preferred_qr_url || action?.qr_image_url) return true;
3357
- if (action?.kind === "auth_qr" && mode !== "none" && mode !== "json") return true;
3358
- return mode === "agent" || mode === "chat";
3359
- }
3360
-
3361
- function shouldGenerateLocalQRFromActionURL(action = {}) {
3362
- return action?.kind === "auth_qr" && Boolean(action?.url);
3363
- }
3364
-
3365
- async function prepareGeneratedActionQRFile(action, flags = {}, optional = false) {
3366
- if (!action?.url) return "";
3367
- const file = flags.qr_file || process.env.ITP_QR_FILE || defaultQRFilePath(action, "");
3368
- try {
3369
- await QRCode.toFile(file, action.url, { errorCorrectionLevel: "M", width: 512, margin: 2 });
3370
- } catch (error) {
3371
- if (!optional) throw error;
3372
- action.local_qr_error = safeErrorMessage(error);
3373
- return "";
3374
- }
3375
- action.local_qr_path = file;
3376
- action.local_qr_mime = "image/png";
3377
- return file;
3535
+ return false;
3378
3536
  }
3379
3537
 
3380
3538
  async function prepareLocalQRFile(action, qrImageURL, flags = {}, optional = false) {
@@ -3462,57 +3620,6 @@ function attachAgentQRImage(action, qrImageURL, localPath = "") {
3462
3620
  return action;
3463
3621
  }
3464
3622
 
3465
- function attachAgentGeneratedQRImage(action, localPath = "") {
3466
- if (!action || !localPath) return action;
3467
- if (!Array.isArray(action.display)) {
3468
- action.display = [];
3469
- }
3470
- if (!action.display.some((item) => item?.type === "image" && item?.local_path === localPath)) {
3471
- action.display.push({
3472
- type: "image",
3473
- format: "png",
3474
- local_path: localPath,
3475
- 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."
3476
- });
3477
- }
3478
- return action;
3479
- }
3480
-
3481
- async function attachAgentTextQR(action, flags = {}) {
3482
- if (!action?.url || action.agent_text_qr) return action;
3483
- const qr = await QRCode.toString(action.url, {
3484
- type: "utf8",
3485
- small: true,
3486
- errorCorrectionLevel: "M"
3487
- });
3488
- const lines = qr.replace(/\s+$/g, "").split(/\r?\n/);
3489
- const text = lines.join("\n");
3490
- const sha256 = crypto.createHash("sha256").update(text).digest("hex");
3491
- action.agent_text_qr = {
3492
- type: "terminal_text",
3493
- format: "unicode",
3494
- lines,
3495
- text,
3496
- fenced_text: `\`\`\`text\n${text}\n\`\`\``,
3497
- line_count: lines.length,
3498
- sha256,
3499
- url: action.url,
3500
- 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.",
3501
- 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."
3502
- };
3503
- if (!Array.isArray(action.display)) {
3504
- action.display = [];
3505
- }
3506
- if (!action.display.some((item) => item?.type === "terminal_text")) {
3507
- action.display.push({
3508
- type: "terminal_text",
3509
- format: "unicode",
3510
- lines
3511
- });
3512
- }
3513
- return action;
3514
- }
3515
-
3516
3623
  async function downloadQRImage(qrImageURL, file, flags = {}) {
3517
3624
  const controller = new AbortController();
3518
3625
  const timer = setTimeout(() => controller.abort(), apiTimeoutMs(flags));
@@ -3648,6 +3755,7 @@ async function coreApi(pathname, options = {}, flags = {}) {
3648
3755
  if (options.ops) {
3649
3756
  headers["X-ItPay-Ops-Token"] = sandboxOpsToken(flags);
3650
3757
  }
3758
+ Object.assign(headers, options.headers || {});
3651
3759
  if (flags.request_id) headers["X-Request-ID"] = String(flags.request_id);
3652
3760
  if (flags.correlation_id) headers["X-Correlation-ID"] = String(flags.correlation_id);
3653
3761
  const targetURL = coreURL(pathname, flags);
@@ -3869,6 +3977,14 @@ function readConfig() {
3869
3977
  return readJSON(CONFIG_PATH, {});
3870
3978
  }
3871
3979
 
3980
+ function packageVersion() {
3981
+ try {
3982
+ return JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8")).version || "0.0.0";
3983
+ } catch {
3984
+ return "0.0.0";
3985
+ }
3986
+ }
3987
+
3872
3988
  function writeConfig(value) {
3873
3989
  writeJSON0600(CONFIG_PATH, value);
3874
3990
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "ItPay CLI, buyer skill, and agent-readable docs for agent-native commerce.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,7 @@
37
37
  "node": ">=18"
38
38
  },
39
39
  "dependencies": {
40
- "qrcode": "^1.5.4"
40
+ "qrcode": "^1.5.4",
41
+ "json5": "^2.2.3"
41
42
  }
42
43
  }
@@ -73,6 +73,10 @@ itp buyer checkout create --cart <cart_id> --email <buyer_email> --phone <buyer_
73
73
  itp buyer checkout resume <checkout_id> --json
74
74
  itp buyer payment wait <payment_intent_id> --json
75
75
  itp buyer checkout status <checkout_id> --json
76
+ itp buyer refund create --order <order_id> --amount-minor <minor_units> --currency CNY --reason buyer_requested --json
77
+ itp buyer refund list --order <order_id> --json
78
+ itp buyer refund show <refund_id> --json
79
+ itp buyer refund cancel <refund_id> --reason buyer_changed_mind --json
76
80
  itp buyer vault grants list --checkout <checkout_id> --json
77
81
  itp buyer vault read --order <order_id> --artifact <vault_artifact_id> --json
78
82
  ```
@@ -100,6 +104,15 @@ registered company name or unified social credit code. If the user says
100
104
  "京东" or "那个京东商城", do not buy precise lookup until you resolve the exact
101
105
  registered name or run fuzzy search first.
102
106
 
107
+ Refund commands use ItPay shared order state. If `itp buyer refund create`
108
+ returns `policy_risk_confirmation_required`, explain the returned
109
+ `refund_eligibility.policy` and `agent_guidance` to the human first. Only retry
110
+ with `--confirm-policy-risk true` after explicit human confirmation.
111
+ Current buyer refunds are whole-order only; do not use line-item refund scope.
112
+ If the human cancels a refund before provider or money movement starts, use
113
+ `buyer refund cancel <refund_id> --json`; after cancel, the delivery claim can
114
+ be unlocked again by the ItPay backend.
115
+
103
116
  ## Non-Negotiable Rules
104
117
 
105
118
  1. Use `--json` for every ItPay command.