@itpay/cli 2.1.66 → 2.1.67

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.
@@ -1,6 +1,6 @@
1
1
  Third-party packages reachable from the ItPay Buyer runtime.
2
2
 
3
- ===== @itpay/hub-client@0.1.0-integration.7286bd58df4a (UNLICENSED) =====
3
+ ===== @itpay/hub-client@0.1.0-integration.27c2f6955647 (UNLICENSED) =====
4
4
  --- LICENSE ---
5
5
  UNLICENSED
6
6
 
package/dist/src/main.js CHANGED
@@ -6167,7 +6167,7 @@ async function runHubWallet(options) {
6167
6167
  };
6168
6168
  } else result = { available_atoms: bal.available.amount_atoms, held_atoms: bal.held.amount_atoms, currency: bal.available.currency, scale: bal.available.scale, period_limit_atoms: null, topup_url: bal.topup_url ?? null, scope: "account_balance", identity };
6169
6169
  result.owner_billing_url = bal.owner_billing_url ?? bal.topup_url ?? null;
6170
- writeCommandEnvelope({ status: "ready", result, instruction: options.usage || options.transactions ? "\u4EC5\u5F53\u524D\u51ED\u636E\u8303\u56F4\uFF1Bruns \u5305\u542B\u5931\u8D25\uFF0C\u4E0D\u662F\u6210\u529F\u4EA4\u4ED8\u6570\u3002\u673A\u5668\u6D41\u6C34\u53EA\u542B\u672C\u51ED\u636E\u6D88\u8D39/\u6D88\u8D39\u9000\u6B3E\uFF0C\u622A\u65AD\u4E0D\u80FD\u5F53\u5168\u91CF\u3002\u5168\u8D26\u53F7\u5145\u503C\u3001\u51BB\u7ED3\u53CA\u539F\u6B3E\u9000\u6B3E\u8BF7\u672C\u4EBA\u6253\u5F00\u8D26\u5355\u5165\u53E3\u3002" : "\u4F59\u989D\u5C5E\u4E8E\u8D26\u53F7\uFF0C\u4E0D\u4EE3\u8868\u672C\u8FDE\u63A5\u6D88\u8D39\u989D\u5EA6\u3002\u4E3B\u52A8\u5145\u503C\u6253\u5F00\u5B98\u65B9\u5165\u53E3\uFF0C\u7531\u672C\u4EBA\u9009\u6863\u5E76\u4ED8\u6B3E\uFF1B\u94FE\u63A5\u4E0D\u662F\u5145\u503C\u8BA2\u5355\u3002", next: null, recovery: [] }, { jsonOutput: Boolean(options.jsonOutput) });
6170
+ writeCommandEnvelope({ status: "ready", result, instruction: options.usage || options.transactions ? "\u4EC5\u5F53\u524D\u51ED\u636E\u8303\u56F4\uFF1Bruns \u5305\u542B\u5931\u8D25\uFF0C\u4E0D\u662F\u6210\u529F\u4EA4\u4ED8\u6570\u3002\u673A\u5668\u6D41\u6C34\u53EA\u542B\u672C\u51ED\u636E\u6D88\u8D39/\u6D88\u8D39\u9000\u6B3E\uFF0C\u622A\u65AD\u4E0D\u80FD\u5F53\u5168\u91CF\u3002\u5168\u8D26\u53F7\u5145\u503C\u3001\u51BB\u7ED3\u53CA\u539F\u6B3E\u9000\u6B3E\u8BF7\u672C\u4EBA\u6253\u5F00\u8D26\u5355\u5165\u53E3\u3002" : "\u4F59\u989D\u5C5E\u4E8E\u8D26\u53F7\uFF0C\u4E0D\u4EE3\u8868\u672C\u8FDE\u63A5\u6D88\u8D39\u989D\u5EA6\u3002\u4E3B\u52A8\u5145\u503C\u6253\u5F00\u5B98\u65B9\u5165\u53E3\uFF0C\u7531\u672C\u4EBA\u9009\u6863\u5E76\u4ED8\u6B3E\uFF1B\u94FE\u63A5\u4E0D\u662F\u5145\u503C\u8BA2\u5355\u3002\u4E5F\u53EF\u4EE5\u544A\u8BC9\u6211\u91D1\u989D\uFF0C\u76F4\u63A5\u7ED9\u4F60\u626B\u7801\u4ED8\u6B3E\u3002", next: null, recovery: [] }, { jsonOutput: Boolean(options.jsonOutput) });
6171
6171
  }
6172
6172
  __name(runHubWallet, "runHubWallet");
6173
6173
  async function runHubDoctor(options) {
@@ -7893,28 +7893,138 @@ function registerAgentCommand(program3) {
7893
7893
  }
7894
7894
  __name(registerAgentCommand, "registerAgentCommand");
7895
7895
 
7896
+ // src/commands/wallet_topup.ts
7897
+ import { randomUUID as randomUUID14 } from "node:crypto";
7898
+ import { existsSync as existsSync12, mkdirSync as mkdirSync15, readdirSync as readdirSync5, readFileSync as readFileSync17, writeFileSync as writeFileSync14, renameSync as renameSync10 } from "node:fs";
7899
+ import { join as join9 } from "node:path";
7900
+
7901
+ // src/render/browser.ts
7902
+ import { spawn } from "node:child_process";
7903
+ import { platform } from "node:os";
7904
+ function isPresentableURL(raw, baseURL, env = process.env) {
7905
+ let url;
7906
+ try {
7907
+ url = new URL(raw);
7908
+ } catch {
7909
+ return false;
7910
+ }
7911
+ if (url.username || url.password) return false;
7912
+ if (env.ITPAY_CLI_DEV === "1" && /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(url.origin)) return true;
7913
+ if (url.protocol !== "https:") return false;
7914
+ const allowedOrigins = new Set(OFFICIAL_BACKEND_URLS);
7915
+ if (baseURL) {
7916
+ try {
7917
+ allowedOrigins.add(new URL(baseURL).origin);
7918
+ } catch {
7919
+ }
7920
+ }
7921
+ return allowedOrigins.has(url.origin);
7922
+ }
7923
+ __name(isPresentableURL, "isPresentableURL");
7924
+ function openInSystemBrowser(raw, baseURL, env = process.env) {
7925
+ if (!isPresentableURL(raw, baseURL, env)) {
7926
+ return Promise.resolve({ status: "failed", reason: "url_rejected" });
7927
+ }
7928
+ const [command, args] = openerArgv(raw);
7929
+ if (!command) {
7930
+ return Promise.resolve({ status: "unavailable", reason: "no system opener on this platform" });
7931
+ }
7932
+ return new Promise((resolvePromise) => {
7933
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
7934
+ child.once("error", (error) => resolvePromise({ status: "failed", reason: error.message }));
7935
+ child.once("spawn", () => {
7936
+ child.unref();
7937
+ resolvePromise({ status: "dispatch_accepted", argv: [command, ...args] });
7938
+ });
7939
+ });
7940
+ }
7941
+ __name(openInSystemBrowser, "openInSystemBrowser");
7942
+ function openerArgv(url) {
7943
+ switch (platform()) {
7944
+ case "darwin":
7945
+ return ["open", [url]];
7946
+ case "win32":
7947
+ return ["cmd.exe", ["/c", "start", "", url]];
7948
+ case "linux":
7949
+ case "freebsd":
7950
+ case "openbsd":
7951
+ return ["xdg-open", [url]];
7952
+ default:
7953
+ return [void 0, []];
7954
+ }
7955
+ }
7956
+ __name(openerArgv, "openerArgv");
7957
+
7958
+ // src/commands/wallet_topup.ts
7959
+ async function walletTopup(options) {
7960
+ const cfg = loadHubConfig(), api = new HubClient2(cfg), me = await api.me();
7961
+ const root = join9(hubTasksDir(), ".funding");
7962
+ mkdirSync15(root, { recursive: true, mode: 448 });
7963
+ let path2;
7964
+ if (options.topupStatus) {
7965
+ const name = readdirSync5(root).find((name2) => {
7966
+ try {
7967
+ return JSON.parse(readFileSync17(join9(root, name2), "utf8")).topup_id === options.topupStatus;
7968
+ } catch {
7969
+ return false;
7970
+ }
7971
+ });
7972
+ if (!name) throw Error("not_found: original top-up is not in this installation");
7973
+ path2 = join9(root, name);
7974
+ } else {
7975
+ if (!options.topup || !/^\d+(?:\.\d{1,2})?$/.test(options.topup)) throw Error("invalid_input: enter a positive CNY amount with at most two decimal places");
7976
+ path2 = join9(root, encodeURIComponent(options.topup) + ".json");
7977
+ }
7978
+ await withAsyncFileLock(path2 + ".lock", async () => {
7979
+ let saved = existsSync12(path2) ? JSON.parse(readFileSync17(path2, "utf8")) : void 0;
7980
+ if (saved && saved.buyer_id !== me.buyer_id) throw Error("authorization_required: original top-up account changed");
7981
+ if (!options.topupStatus && saved?.topup_id && ["credited", "failed", "closed_unpaid"].includes(saved.status ?? "")) {
7982
+ writeFileSync14(join9(root, "receipt-" + encodeURIComponent(saved.topup_id) + ".json"), JSON.stringify(saved), { mode: 384 });
7983
+ }
7984
+ if (!options.topupStatus && (!saved || saved.status === "credited" || saved.status === "failed" || saved.status === "closed_unpaid")) saved = { key: "cli-wallet-" + randomUUID14(), amount: options.topup, buyer_id: me.buyer_id, ...options.request ? { request_id: options.request } : {} };
7985
+ if (!saved) throw Error("not_found: original top-up is unavailable");
7986
+ if (options.request) saved.request_id = options.request;
7987
+ const task = saved.request_id ? new HubTaskStore().get(saved.request_id) : void 0;
7988
+ if (saved.request_id && !task?.intent_id) throw Error("invalid_state: keep the original request; no funding intent is available");
7989
+ const save2 = /* @__PURE__ */ __name(() => {
7990
+ const tmp = path2 + ".tmp";
7991
+ writeFileSync14(tmp, JSON.stringify(saved), { mode: 384 });
7992
+ renameSync10(tmp, path2);
7993
+ }, "save");
7994
+ save2();
7995
+ const data = options.topupStatus ? await api.topupStatus(options.topupStatus) : await api.createTopup({ amount_cny: saved.amount, payer_currency: "CNY", ...task?.intent_id ? { continuation_id: task.intent_id } : {} }, saved.key);
7996
+ saved.topup_id = data.topup_id;
7997
+ saved.status = data.status;
7998
+ save2();
7999
+ const browser = data.page_url && !options.topupStatus && !options.noOpen ? await openInSystemBrowser(data.page_url, cfg.baseURL) : { status: "unavailable", reason: "host link presentation" };
8000
+ const next = data.status === "credited" ? saved.request_id ? { command: `itpay api call --pending ${saved.request_id}${options.jsonOutput ? " --json" : ""}`, reason: "\u5230\u8D26\u540E\u7EE7\u7EED\u81EA\u5DF1\u7684\u539F\u8BF7\u6C42" } : null : { command: `itpay api wallet --topup-status ${data.topup_id}${options.jsonOutput ? " --json" : ""}`, reason: "\u53EA\u8BFB\u6838\u5BF9\u539F\u5145\u503C\uFF1B\u672C\u4EBA\u4ED8\u6B3E\u540E\u7EE7\u7EED" };
8001
+ writeCommandEnvelope({ status: data.status, result: { ...data, browser, ...saved.request_id ? { request_id: saved.request_id } : {} }, instruction: data.status === "credited" ? `\u8D26\u53F7 ${data.account_hint ?? ""} \u5DF2\u5230\u8D26 \xA5${data.amount ?? saved.amount}\u3002\u4E0D\u8981\u91CD\u590D\u4ED8\u6B3E\uFF1B\u5982\u6709\u539F\u8BF7\u6C42\uFF0C\u7EE7\u7EED\u8FD4\u56DE\u7684\u539F call --pending\u3002\u5145\u503C\u4E0D\u589E\u52A0\u6D88\u8D39\u6388\u6743\u3002` : `${data.notice ? data.notice + "\u3002" : ""}\u4E3A\u8D26\u53F7 ${data.account_hint ?? ""} \u5145\u503C \xA5${data.amount ?? saved.amount}\u3002\u53EF\u626B\u7801\u6216\u6253\u5F00\u540C\u5355\u6536\u94F6\u53F0\uFF1B\u6309\u5BBF\u4E3B\u80FD\u529B\u5C55\u793A\u5B98\u65B9\u56FE\u7247\u3002${data.expires_at ? "\u6709\u6548\u81F3 " + data.expires_at + "\u3002" : ""}\u5C55\u793A\u80FD\u529B\u4E0D\u8DB3\u65F6\u7167\u5E38\u4F7F\u7528\u540C\u5355\u94FE\u63A5\u529E\u7406\u3002\u5145\u503C\u4E0D\u589E\u52A0\u6D88\u8D39\u6388\u6743\u3002`, next, recovery: [] }, { jsonOutput: options.jsonOutput });
8002
+ });
8003
+ }
8004
+ __name(walletTopup, "walletTopup");
8005
+
7896
8006
  // src/state/hub_show_cache.ts
7897
8007
  import { createHash as createHash13 } from "node:crypto";
7898
- import { mkdirSync as mkdirSync15, readFileSync as readFileSync17, renameSync as renameSync10, unlinkSync as unlinkSync9, writeFileSync as writeFileSync14 } from "node:fs";
7899
- import { dirname as dirname11, join as join9 } from "node:path";
8008
+ import { mkdirSync as mkdirSync16, readFileSync as readFileSync18, renameSync as renameSync11, unlinkSync as unlinkSync9, writeFileSync as writeFileSync15 } from "node:fs";
8009
+ import { dirname as dirname11, join as join10 } from "node:path";
7900
8010
  function cachePath(operation) {
7901
8011
  const origin2 = loadHubConfig().baseURL;
7902
8012
  const key = createHash13("sha256").update(origin2 + "\0" + operation).digest("hex");
7903
- return join9(dirname11(hubCredentialsPath()), "shown", key + ".json");
8013
+ return join10(dirname11(hubCredentialsPath()), "shown", key + ".json");
7904
8014
  }
7905
8015
  __name(cachePath, "cachePath");
7906
8016
  function saveShown(value) {
7907
8017
  const path2 = cachePath(value.operation_id);
7908
- mkdirSync15(dirname11(path2), { recursive: true, mode: 448 });
8018
+ mkdirSync16(dirname11(path2), { recursive: true, mode: 448 });
7909
8019
  withFileLock(path2 + ".lock", () => {
7910
8020
  const temporary = path2 + "." + process.pid + ".tmp";
7911
8021
  const publication = createHash13("sha256").update(value.current_publication_id).digest("hex");
7912
8022
  const versioned = path2 + "." + publication;
7913
8023
  try {
7914
- writeFileSync14(temporary, JSON.stringify(value), { mode: 384 });
7915
- renameSync10(temporary, versioned);
7916
- writeFileSync14(temporary, JSON.stringify({ publication: versioned }), { mode: 384 });
7917
- renameSync10(temporary, path2);
8024
+ writeFileSync15(temporary, JSON.stringify(value), { mode: 384 });
8025
+ renameSync11(temporary, versioned);
8026
+ writeFileSync15(temporary, JSON.stringify({ publication: versioned }), { mode: 384 });
8027
+ renameSync11(temporary, path2);
7918
8028
  } finally {
7919
8029
  try {
7920
8030
  unlinkSync9(temporary);
@@ -7927,9 +8037,9 @@ __name(saveShown, "saveShown");
7927
8037
  function loadShown(operation) {
7928
8038
  try {
7929
8039
  const path2 = cachePath(operation);
7930
- const pointer = JSON.parse(readFileSync17(path2, "utf8"));
8040
+ const pointer = JSON.parse(readFileSync18(path2, "utf8"));
7931
8041
  if (!pointer.publication.startsWith(path2 + ".") || !/^[a-f0-9]{64}$/.test(pointer.publication.slice(path2.length + 1))) throw new Error("invalid display pointer");
7932
- const value = JSON.parse(readFileSync17(pointer.publication, "utf8"));
8042
+ const value = JSON.parse(readFileSync18(pointer.publication, "utf8"));
7933
8043
  if (value.operation_id === operation && value.current_publication_id && value.input_schema && value.price_card.mode !== "unconfigured") return value;
7934
8044
  } catch {
7935
8045
  }
@@ -7984,6 +8094,9 @@ function render(task, snapshot, json) {
7984
8094
  const run = snapshot.run;
7985
8095
  if (run) {
7986
8096
  const zero2 = run.captured_atoms === "0" && run.held_atoms === "0";
8097
+ const balanceNotice = run.low_balance;
8098
+ const firstNotice = Boolean(balanceNotice && !task.low_balance_notified);
8099
+ if (firstNotice) new HubTaskStore().update(task.request_id, { low_balance_notified: true });
7987
8100
  writeCommandEnvelope({
7988
8101
  status: run.execution_state === "succeeded" ? "succeeded" : snapshot.terminal ? "failed" : "pending",
7989
8102
  result: {
@@ -7994,6 +8107,7 @@ function render(task, snapshot, json) {
7994
8107
  delivery_state: run.delivery_state,
7995
8108
  captured_atoms: run.captured_atoms ?? null,
7996
8109
  held_atoms: run.held_atoms ?? null,
8110
+ ...firstNotice ? { low_balance: balanceNotice } : {},
7997
8111
  result: run.result ?? null,
7998
8112
  result_expires_at: run.result_expires_at ?? null,
7999
8113
  error: run.error ?? null,
@@ -8006,6 +8120,7 @@ function render(task, snapshot, json) {
8006
8120
  return;
8007
8121
  }
8008
8122
  const reason = snapshot.intent?.blocking_reason ?? "unknown";
8123
+ const uncharged = ["insufficient_balance", "authorization_denied", "authorization_revoked", "budget_exceeded"].includes(reason);
8009
8124
  const target2 = snapshot.required_action?.web_action?.target ?? snapshot.required_action?.target;
8010
8125
  const changed = snapshot.terminal && reason === "publication_changed";
8011
8126
  const price = safePriceChange(snapshot.price_change);
@@ -8013,9 +8128,9 @@ function render(task, snapshot, json) {
8013
8128
  const label = price?.current_price ? "CNY " + priceLabel({ mode: "per_call", amount_atoms: price.current_price.amount_atoms }) : "\u6682\u4E0D\u53EF\u8BFB\u53D6";
8014
8129
  writeCommandEnvelope({
8015
8130
  status: changed ? "price_changed" : reason,
8016
- result: { request_id: task.request_id, intent_id: task.intent_id, captured_atoms: zero ? "0" : null, held_atoms: zero ? "0" : null, ...price ? { price_change: price } : {}, ...price?.current_price ? { current_price: price.current_price } : {} },
8131
+ result: { request_id: task.request_id, intent_id: task.intent_id, account: snapshot.account_hint, captured_atoms: zero || uncharged ? "0" : null, held_atoms: zero || uncharged ? "0" : null, ...price ? { price_change: price } : {}, ...price?.current_price ? { current_price: price.current_price } : {} },
8017
8132
  ...target2 && !changed ? { handoff: { type: "open_url", url: target2, label: "\u6253\u5F00\u5B98\u65B9\u9875\u9762" } } : {},
8018
- instruction: changed ? `\u5F53\u524D\u4EF7\u683C ${label}\u3002${zero ? "\u539F\u8BF7\u6C42\u5DF2\u7ED3\u675F\uFF0C\u6263\u8D390\u3001\u51BB\u7ED30\uFF0C\u672C\u6B21\u6CA1\u6263\u94B1\u3002\u5148 show \u6838\u5BF9\u5F53\u524D\u5546\u54C1\uFF0C\u8BF4\u660E\u65B0\u4EF7\u5E76\u53D6\u5F97\u660E\u786E\u540C\u610F\u540E\u518D\u7528\u65B0 call\u3002" : "\u539F\u8BF7\u6C42\u8D39\u7528\u4ECD\u987B\u6838\u5BF9\u3002\u5148 status \u539F\u8BF7\u6C42\uFF0C\u672A\u786E\u8BA4\u96F6\u6263\u8D39\u548C\u96F6\u51BB\u7ED3\u524D\u4E0D\u8981\u65B0 call\u3002"}` : reason === "insufficient_balance" ? "\u8BF7\u5728\u539F\u5355\u5B98\u65B9\u9875\u9762\u5145\u503C\uFF0C\u5230\u8D26\u540E\u7EE7\u7EED\u539F\u8BF7\u6C42\u3002" : "\u4FDD\u7559\u539F\u8BF7\u6C42\uFF0C\u6309\u5B98\u65B9\u64CD\u4F5C\u5B8C\u6210\u6388\u6743\u6216\u6838\u5BF9\u3002",
8133
+ instruction: changed ? `\u5F53\u524D\u4EF7\u683C ${label}\u3002${zero ? "\u539F\u8BF7\u6C42\u5DF2\u7ED3\u675F\uFF0C\u6263\u8D390\u3001\u51BB\u7ED30\uFF0C\u672C\u6B21\u6CA1\u6263\u94B1\u3002\u5148 show \u6838\u5BF9\u5F53\u524D\u5546\u54C1\uFF0C\u8BF4\u660E\u65B0\u4EF7\u5E76\u53D6\u5F97\u660E\u786E\u540C\u610F\u540E\u518D\u7528\u65B0 call\u3002" : "\u539F\u8BF7\u6C42\u8D39\u7528\u4ECD\u987B\u6838\u5BF9\u3002\u5148 status \u539F\u8BF7\u6C42\uFF0C\u672A\u786E\u8BA4\u96F6\u6263\u8D39\u548C\u96F6\u51BB\u7ED3\u524D\u4E0D\u8981\u65B0 call\u3002"}` : reason === "insufficient_balance" ? "\u8BF7\u5728\u539F\u5355\u5B98\u65B9\u9875\u9762\u5145\u503C\uFF0C\u5230\u8D26\u540E\u7EE7\u7EED\u539F\u8BF7\u6C42\u3002\u4E5F\u53EF\u4EE5\u544A\u8BC9\u6211\u91D1\u989D\uFF0C\u76F4\u63A5\u7ED9\u4F60\u626B\u7801\u4ED8\u6B3E\u3002" : "\u4FDD\u7559\u539F\u8BF7\u6C42\uFF0C\u6309\u5B98\u65B9\u64CD\u4F5C\u5B8C\u6210\u6388\u6743\u6216\u6838\u5BF9\u3002",
8019
8134
  next: changed && zero ? { command: `itpay api show ${task.operation_id}${json ? " --json" : ""}`, reason: "\u67E5\u770B\u5F53\u524D\u7528\u9014\u4E0E\u4EF7\u683C" } : changed ? statusAction(task.request_id, json) : snapshot.terminal ? null : pendingAction(task.request_id, json),
8020
8135
  recovery: []
8021
8136
  }, { jsonOutput: json });
@@ -8168,7 +8283,7 @@ async function shelfCall(operation, options) {
8168
8283
  __name(shelfCall, "shelfCall");
8169
8284
 
8170
8285
  // src/commands/admin.ts
8171
- import { readFileSync as readFileSync18 } from "node:fs";
8286
+ import { readFileSync as readFileSync19 } from "node:fs";
8172
8287
  var actions = /* @__PURE__ */ new Set(["operations_list", "ingestions_list", "import_github", "ingestion_status", "ingestion_retry", "credentials_list", "credential_put", "source_requirements", "source_bind", "acceptance_status", "acceptance_set"]);
8173
8288
  function parseAdminInput(action, raw) {
8174
8289
  if (!actions.has(action)) throw Error("unknown admin action");
@@ -8181,7 +8296,7 @@ __name(parseAdminInput, "parseAdminInput");
8181
8296
  async function runAdminCommand(action, source) {
8182
8297
  if (source !== "-" && !source.startsWith("@")) throw Error("Use stdin or a private file, never secret command arguments");
8183
8298
  if (action === "credential_put" && source !== "-") throw Error("credential_put accepts stdin only");
8184
- const raw = readFileSync18(source === "-" ? 0 : source.slice(1), "utf8");
8299
+ const raw = readFileSync19(source === "-" ? 0 : source.slice(1), "utf8");
8185
8300
  const args = parseAdminInput(action, raw);
8186
8301
  const cfg = loadHubConfig();
8187
8302
  if (!cfg.apiKey || cfg.bearerToken) throw Error("Explicit scoped ITPAY_HUB_KEY required");
@@ -8322,8 +8437,12 @@ function registerAPICommands(program3) {
8322
8437
  reportCLIError(error, { jsonOutput: Boolean(options.json), code: "hub_doctor_failed", instruction: "\u6309\u8F93\u51FA\u9010\u6761\u4FEE\u590D\u672C\u5730\u6808\u3002", recovery: [] });
8323
8438
  }
8324
8439
  });
8325
- hubCmd.command("wallet").description("Read wallet, current credential usage or scoped transactions").option("--usage", "read current Key/Consumer usage; not account-wide").option("--transactions", "read scoped transactions; owner history uses the official page").option("--json", "output JSON").action(async (options) => {
8440
+ hubCmd.command("wallet").description("Read wallet or prepare an explicit own-wallet top-up").option("--topup <amount>", "prepare a CNY wallet top-up; minimum 10 CNY").option("--topup-status <topup_id>", "read the original top-up").option("--request <request_id>", "retain your original pending API request").option("--no-open", "this host does not open a browser; retain the same payment links").option("--usage", "read current Key/Consumer usage; not account-wide").option("--transactions", "read scoped transactions; owner history uses the official page").option("--json", "output JSON").action(async (options) => {
8326
8441
  try {
8442
+ if (options.topup || options.topupStatus) {
8443
+ await walletTopup({ topup: options.topup, topupStatus: options.topupStatus, request: options.request, noOpen: options.open === false, jsonOutput: Boolean(options.json) });
8444
+ return;
8445
+ }
8327
8446
  await runHubWallet({ jsonOutput: Boolean(options.json), usage: Boolean(options.usage), transactions: Boolean(options.transactions) });
8328
8447
  } catch (error) {
8329
8448
  reportCLIError(error, { jsonOutput: Boolean(options.json), code: "hub_wallet_failed", instruction: "\u786E\u8BA4\u539F\u5B89\u88C5\u7684\u8FDE\u63A5\u8EAB\u4EFD\u3002", recovery: [] });
@@ -15331,65 +15450,8 @@ function projectCheckoutPresentation(presentation, model) {
15331
15450
  }
15332
15451
  __name(projectCheckoutPresentation, "projectCheckoutPresentation");
15333
15452
 
15334
- // src/render/browser.ts
15335
- import { spawn } from "node:child_process";
15336
- import { platform } from "node:os";
15337
- function isPresentableURL(raw, baseURL, env = process.env) {
15338
- let url;
15339
- try {
15340
- url = new URL(raw);
15341
- } catch {
15342
- return false;
15343
- }
15344
- if (url.username || url.password) return false;
15345
- if (env.ITPAY_CLI_DEV === "1" && /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(url.origin)) return true;
15346
- if (url.protocol !== "https:") return false;
15347
- const allowedOrigins = new Set(OFFICIAL_BACKEND_URLS);
15348
- if (baseURL) {
15349
- try {
15350
- allowedOrigins.add(new URL(baseURL).origin);
15351
- } catch {
15352
- }
15353
- }
15354
- return allowedOrigins.has(url.origin);
15355
- }
15356
- __name(isPresentableURL, "isPresentableURL");
15357
- function openInSystemBrowser(raw, baseURL, env = process.env) {
15358
- if (!isPresentableURL(raw, baseURL, env)) {
15359
- return Promise.resolve({ status: "failed", reason: "url_rejected" });
15360
- }
15361
- const [command, args] = openerArgv(raw);
15362
- if (!command) {
15363
- return Promise.resolve({ status: "unavailable", reason: "no system opener on this platform" });
15364
- }
15365
- return new Promise((resolvePromise) => {
15366
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
15367
- child.once("error", (error) => resolvePromise({ status: "failed", reason: error.message }));
15368
- child.once("spawn", () => {
15369
- child.unref();
15370
- resolvePromise({ status: "dispatch_accepted", argv: [command, ...args] });
15371
- });
15372
- });
15373
- }
15374
- __name(openInSystemBrowser, "openInSystemBrowser");
15375
- function openerArgv(url) {
15376
- switch (platform()) {
15377
- case "darwin":
15378
- return ["open", [url]];
15379
- case "win32":
15380
- return ["cmd.exe", ["/c", "start", "", url]];
15381
- case "linux":
15382
- case "freebsd":
15383
- case "openbsd":
15384
- return ["xdg-open", [url]];
15385
- default:
15386
- return [void 0, []];
15387
- }
15388
- }
15389
- __name(openerArgv, "openerArgv");
15390
-
15391
15453
  // src/render/terminal_image.ts
15392
- import { readFileSync as readFileSync19 } from "node:fs";
15454
+ import { readFileSync as readFileSync20 } from "node:fs";
15393
15455
  import path from "node:path";
15394
15456
  function supportsInlineTerminalImages() {
15395
15457
  return Boolean(process.env.ITERM_SESSION_ID) || process.env.TERM_PROGRAM === "iTerm.app";
@@ -15398,7 +15460,7 @@ __name(supportsInlineTerminalImages, "supportsInlineTerminalImages");
15398
15460
  function renderInlineTerminalImage(filePath, out) {
15399
15461
  if (!supportsInlineTerminalImages()) return;
15400
15462
  try {
15401
- const buf = readFileSync19(filePath);
15463
+ const buf = readFileSync20(filePath);
15402
15464
  const name = path.basename(filePath);
15403
15465
  const nameB64 = Buffer.from(name).toString("base64");
15404
15466
  const dataB64 = buf.toString("base64");
@@ -17285,8 +17347,8 @@ var {
17285
17347
  } = import_index.default;
17286
17348
 
17287
17349
  // src/state/cart_session.ts
17288
- import { chmodSync as chmodSync7, existsSync as existsSync12, mkdirSync as mkdirSync16, readFileSync as readFileSync20, renameSync as renameSync11, writeFileSync as writeFileSync15 } from "node:fs";
17289
- import { randomUUID as randomUUID14 } from "node:crypto";
17350
+ import { chmodSync as chmodSync7, existsSync as existsSync13, mkdirSync as mkdirSync17, readFileSync as readFileSync21, renameSync as renameSync12, writeFileSync as writeFileSync16 } from "node:fs";
17351
+ import { randomUUID as randomUUID15 } from "node:crypto";
17290
17352
  import { dirname as dirname12 } from "node:path";
17291
17353
  var CartSession = class _CartSession {
17292
17354
  static {
@@ -17299,9 +17361,9 @@ var CartSession = class _CartSession {
17299
17361
  }
17300
17362
  static loadFromFile(path2, currency) {
17301
17363
  const session = new _CartSession(currency);
17302
- if (existsSync12(path2)) {
17364
+ if (existsSync13(path2)) {
17303
17365
  try {
17304
- const raw = readFileSync20(path2, "utf-8");
17366
+ const raw = readFileSync21(path2, "utf-8");
17305
17367
  const persisted = JSON.parse(raw);
17306
17368
  if (Array.isArray(persisted.items)) {
17307
17369
  persisted.items.forEach((item) => {
@@ -17335,12 +17397,12 @@ var CartSession = class _CartSession {
17335
17397
  ...this.state.lastCheckoutURL ? { lastCheckoutURL: this.state.lastCheckoutURL } : {}
17336
17398
  };
17337
17399
  const contents = JSON.stringify(toSave, null, 2);
17338
- if (existsSync12(path2) && readFileSync20(path2, "utf-8") === contents) return;
17339
- mkdirSync16(dirname12(path2), { recursive: true, mode: 448 });
17340
- const temporary = `${path2}.${process.pid}.${randomUUID14()}.tmp`;
17341
- writeFileSync15(temporary, contents, { encoding: "utf-8", mode: 384 });
17400
+ if (existsSync13(path2) && readFileSync21(path2, "utf-8") === contents) return;
17401
+ mkdirSync17(dirname12(path2), { recursive: true, mode: 448 });
17402
+ const temporary = `${path2}.${process.pid}.${randomUUID15()}.tmp`;
17403
+ writeFileSync16(temporary, contents, { encoding: "utf-8", mode: 384 });
17342
17404
  chmodSync7(temporary, 384);
17343
- renameSync11(temporary, path2);
17405
+ renameSync12(temporary, path2);
17344
17406
  chmodSync7(path2, 384);
17345
17407
  }
17346
17408
  add(item) {
@@ -17467,14 +17529,14 @@ import { createHash as createHash14 } from "node:crypto";
17467
17529
 
17468
17530
  // src/state/skill_identity.generated.ts
17469
17531
  var CANONICAL_SKILL_SHA256 = "8dd5c3a00d3ff35ce8490e3703d012dd4c936c1827b2a5541f630783e2877a67";
17470
- var CANONICAL_API_SKILL_SHA256 = "9c69ea354e255ef5d71ec2d7c473446fc6b4e2e33ab6bebcea3472cd22bfe80a";
17532
+ var CANONICAL_API_SKILL_SHA256 = "c37e2efb288bf4545c828671afacb3f783f3e7020dbb4cccd0b7d273e636e84e";
17471
17533
 
17472
17534
  // src/commands/skill.ts
17473
- import { existsSync as existsSync14, readFileSync as readFileSync22 } from "node:fs";
17535
+ import { existsSync as existsSync15, readFileSync as readFileSync23 } from "node:fs";
17474
17536
  import { resolve as resolve10 } from "node:path";
17475
17537
 
17476
17538
  // src/state/package_resources.ts
17477
- import { existsSync as existsSync13, readFileSync as readFileSync21 } from "node:fs";
17539
+ import { existsSync as existsSync14, readFileSync as readFileSync22 } from "node:fs";
17478
17540
  import { basename as basename2, dirname as dirname13, resolve as resolve9 } from "node:path";
17479
17541
  import { fileURLToPath } from "node:url";
17480
17542
  function packageResourcePath(...segments) {
@@ -17482,7 +17544,7 @@ function packageResourcePath(...segments) {
17482
17544
  const root = resolve9(dirname13(modulePath), "..", "..");
17483
17545
  if (!basename2(modulePath).startsWith("package_resources.") && basename2(modulePath) !== "main.js") throw new Error("unsupported CLI resource layout");
17484
17546
  const metadata = resolve9(root, "package.json");
17485
- if (!existsSync13(metadata) || JSON.parse(readFileSync21(metadata, "utf8")).name !== "@itpay/cli") throw new Error("current CLI package resources are unavailable");
17547
+ if (!existsSync14(metadata) || JSON.parse(readFileSync22(metadata, "utf8")).name !== "@itpay/cli") throw new Error("current CLI package resources are unavailable");
17486
17548
  return resolve9(root, ...segments);
17487
17549
  }
17488
17550
  __name(packageResourcePath, "packageResourcePath");
@@ -17506,7 +17568,7 @@ function runSkillShow(name, options = {}) {
17506
17568
  const original = packagedSkill();
17507
17569
  let content;
17508
17570
  try {
17509
- content = readFileSync22(findSkillPath(), "utf8");
17571
+ content = readFileSync23(findSkillPath(), "utf8");
17510
17572
  } catch (error) {
17511
17573
  if (process.env.ITPAY_CLI_SKILLS_DIR) {
17512
17574
  const cause = error && typeof error === "object" && "code" in error ? String(error.code) : "read_failed";
@@ -17554,8 +17616,8 @@ ${CODEX_WAIT_GUIDANCE}
17554
17616
  __name(runSkillShow, "runSkillShow");
17555
17617
  function packagedSkill() {
17556
17618
  let path2 = findSkillPath(false);
17557
- if (!existsSync14(path2) && process.env.ITPAY_CLI_SKILLS_DIR) path2 = findSkillPath();
17558
- const content = readFileSync22(path2, "utf8");
17619
+ if (!existsSync15(path2) && process.env.ITPAY_CLI_SKILLS_DIR) path2 = findSkillPath();
17620
+ const content = readFileSync23(path2, "utf8");
17559
17621
  if (createHash14("sha256").update(content).digest("hex") !== CANONICAL_SKILL_SHA256) {
17560
17622
  throw new CommandContractError("skill_override_mismatch", "Skill does not match this pinned CLI build", "\u4F7F\u7528\u5F53\u524D\u5305\u540C\u6E90\u7684\u5B8C\u6574 Skill\uFF1B\u4FDD\u7559\u539F\u4EFB\u52A1\u548C\u8EAB\u4EFD\uFF0C\u4E0D\u5B89\u88C5\u53E6\u4E00\u5957 CLI\u3002", []);
17561
17623
  }
@@ -17567,7 +17629,7 @@ function findSkillPath(allowOverride = true) {
17567
17629
  if (allowOverride && process.env.ITPAY_CLI_SKILLS_DIR) {
17568
17630
  const configuredRoot = resolve10(process.env.ITPAY_CLI_SKILLS_DIR);
17569
17631
  const directPath = resolve10(configuredRoot, "SKILL.md");
17570
- if (existsSync14(directPath)) return directPath;
17632
+ if (existsSync15(directPath)) return directPath;
17571
17633
  return resolve10(configuredRoot, ITPAY_SKILL, "SKILL.md");
17572
17634
  }
17573
17635
  return packageResourcePath("skills", ITPAY_SKILL, "SKILL.md");
@@ -17583,7 +17645,7 @@ __name(validateSkill, "validateSkill");
17583
17645
  function showAPISkill(options) {
17584
17646
  let content;
17585
17647
  try {
17586
- content = readFileSync22(packageResourcePath("skills", "itpay-api", "SKILL.md"), "utf8");
17648
+ content = readFileSync23(packageResourcePath("skills", "itpay-api", "SKILL.md"), "utf8");
17587
17649
  } catch {
17588
17650
  throw new CommandContractError(
17589
17651
  "skill_unavailable",
@@ -19695,7 +19757,7 @@ function writeCartNextEnvelope(envelope, options) {
19695
19757
  __name(writeCartNextEnvelope, "writeCartNextEnvelope");
19696
19758
 
19697
19759
  // src/commands/docs.ts
19698
- import { readFileSync as readFileSync23, readdirSync as readdirSync5 } from "node:fs";
19760
+ import { readFileSync as readFileSync24, readdirSync as readdirSync6 } from "node:fs";
19699
19761
  import { resolve as resolve11 } from "node:path";
19700
19762
  function runDocsList(options = {}) {
19701
19763
  const topics = loadDocs().map(({ topic, title, purpose }) => ({ topic, title, purpose }));
@@ -19770,12 +19832,12 @@ function runDocsSearch(query, options = {}) {
19770
19832
  __name(runDocsSearch, "runDocsSearch");
19771
19833
  function loadDocs() {
19772
19834
  const docsDir = findDocsDir();
19773
- const files = readdirSync5(docsDir).filter((file) => file.endsWith(".json")).sort();
19774
- const docs = files.map((file) => parseDoc(readFileSync23(resolve11(docsDir, file), "utf8"), file));
19835
+ const files = readdirSync6(docsDir).filter((file) => file.endsWith(".json")).sort();
19836
+ const docs = files.map((file) => parseDoc(readFileSync24(resolve11(docsDir, file), "utf8"), file));
19775
19837
  if (process.env.ITPAY_CLI_DOCS_DIR) {
19776
19838
  const canonical = packageResourcePath("docs", "agent", "buyer");
19777
- const expectedFiles = readdirSync5(canonical).filter((file) => file.endsWith(".json")).sort();
19778
- if (JSON.stringify(files) !== JSON.stringify(expectedFiles) || files.some((file, index) => JSON.stringify(docs[index]) !== JSON.stringify(parseDoc(readFileSync23(resolve11(canonical, file), "utf8"), file)))) {
19839
+ const expectedFiles = readdirSync6(canonical).filter((file) => file.endsWith(".json")).sort();
19840
+ if (JSON.stringify(files) !== JSON.stringify(expectedFiles) || files.some((file, index) => JSON.stringify(docs[index]) !== JSON.stringify(parseDoc(readFileSync24(resolve11(canonical, file), "utf8"), file)))) {
19779
19841
  throw new CommandContractError(
19780
19842
  "docs_override_mismatch",
19781
19843
  "configured Buyer docs differ from the current package",
@@ -1,4 +1,4 @@
1
- const CLI_VERSION = "2.1.66";
1
+ const CLI_VERSION = "2.1.67";
2
2
  const API_CONTRACT_REVISION = "sha256:f20af3780a0e12a79a6ca9507ba65167a4018338495af66d87931e55619b618a";
3
3
  export {
4
4
  API_CONTRACT_REVISION,
@@ -27,3 +27,8 @@ A `price_changed` receipt includes `current_price` (CNY, scale 6, atoms) and ver
27
27
  Use a secret manager to inject ITPAY_HUB_KEY or pipe a private secret source to `itpay api credentials import --json`. Never pass the secret in argv or chat. With --profile, import into that same installation; an explicit environment Key remains selected. It does not need OAuth. Saved OAuth and Key remain private; the last explicit import/connect selects the credential, while a Key environment override selects that Key. Supplying both environment bearer and Key is an error. Requests remain isolated by original credential; neither a new Key nor OAuth can take over old requests.
28
28
 
29
29
  Check `wallet` identity.account_hint and credential_kind; matching hints identify the same wallet, not the same consumer. Invalid/revoked Key or insufficient Key scope points to the official /me/keys page, without OAuth fallback. Hub MCP accepts Key only through its existing X-ItPay-Key protocol header; normal ChatGPT MCP uses OAuth. Do not paste a Key into chat or put it in a Bearer field. Services identity is independent.
30
+
31
+
32
+ ### 本人钱包定额充值
33
+
34
+ `itpay api wallet --topup 30 --json` 准备本人钱包的人民币充值,按分报价,最低 ¥10。若为已有挂起请求充值,加 `--request <原 request_id>`;付款后按返回 next 读取原充值并继续原 call --pending。CLI 在宿主可打开浏览器时主动打开官方 page_url,同时返回 checkout_url 和 image。`--no-open` 用于无浏览器打开能力的宿主。`wallet --topup-status <topup_id>` 只读原充值。OAuth/API Hub Key 准备充值不要求消费 grant;充值不会增加调用额度、商户收款或退款权限。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "2.1.66",
3
+ "version": "2.1.67",
4
4
  "description": "The ItPay CLI for services, orders, and human-authorized purchased content.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -110,7 +110,7 @@
110
110
  "node": ">=18"
111
111
  },
112
112
  "devDependencies": {
113
- "@itpay/hub-client": "file:dependencies/itpay-hub-client-0.1.0-integration.7286bd58df4a.tgz",
113
+ "@itpay/hub-client": "file:dependencies/itpay-hub-client-0.1.0-integration.27c2f6955647.tgz",
114
114
  "@modelcontextprotocol/sdk": "^1.29.0",
115
115
  "@types/node": "^22.7.5",
116
116
  "@types/qrcode": "^1.5.5",
@@ -148,5 +148,5 @@
148
148
  "services",
149
149
  "api"
150
150
  ],
151
- "gitHead": "f0a951df3d1152e1255523d647f4602ee5b76659"
151
+ "gitHead": "d3e88d44ef74e7f8d762f04fb5105731c77b69cb"
152
152
  }
@@ -48,3 +48,14 @@ New task-driven installations use one official Agent connection page. Each busin
48
48
  A user may securely inject ITPAY_HUB_KEY or import the selected Key with api credentials import through private stdin. Never ask for it in chat, put it in argv/logs, or echo it. A Key authorizes API Hub only within its catalog, effects and spending grant. It does not authorize Services or payment writes. An explicit environment Key wins over a saved OAuth connection; importing a Key selects it while preserving OAuth privately. An explicit normal connect selects OAuth. Never switch credentials to resume an existing request: its original task namespace remains isolated.
49
49
 
50
50
  For an API task with a selected Key, agent prepares only that API installation and reads its identity without OAuth. Read wallet identity.credential_kind and account_hint before work; all hosts in a shared-wallet test must show the same hint. If mismatched, stop that new task and have the owner select the intended Key or normally reconnect the intended account; keep all existing requests and do not revoke another host. Invalid/revoked Key or insufficient scope directs the owner to /me/keys and secure stdin import, never automatic OAuth fallback. A top-up URL is navigation, not a created top-up; /hub/mcp is a protocol connection URL, not a payment page.
51
+
52
+
53
+ ## Amount-based own-wallet top-up
54
+
55
+ When the user only asks to top up, return wallet.topup_url and say: “也可以告诉我金额,直接给你扫码付款。” For insufficient_balance, keep the original request and its continuation link with the same hint. Never turn these two cases into an unsolicited payment order.
56
+
57
+ When an amount is explicit, run `itpay api wallet --topup 30 --json` (use the user's amount). If continuing a saved insufficient-balance task, add `--request <original request_id>`. OAuth and valid api_hub Keys can prepare their own wallet top-up without a spending grant. The server quotes positive CNY amounts in cents, minimum ¥10; for a smaller amount say “最低 ¥10,本单按 ¥10”. The reply and official card must identify the masked credited account, actual amount and expiry.
58
+
59
+ When browser capability is available, immediately open the returned official page_url. Provide the same checkout_url and, where supported, the returned official image. Scan and click are user choices. Limited display capability does not stop payment; a link-only host can use the same checkout. Do not draw or reconstruct QR codes. `--no-open` expresses a host without browser opening. Poll the returned original top-up status; when credited follow the returned original call --pending, never create another business request. A shared funding card does not transfer another connection's task.
60
+
61
+ Report low_balance once per notification_id with available_atoms, compared_price_atoms and the official wallet link. Key authorization/budget failures use the exact server-issued Key settings link, never a top-up to bypass a grant. Funding never enables API spending or merchant payment/refund permission.