@aeon-ai-pay/aigateway 0.1.4 → 0.1.6

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,15 +1,15 @@
1
1
  /**
2
- * topup:充值 + 一次性 approve facilitator
2
+ * wallet-topup: top up the session wallet and run the one-time facilitator approve.
3
3
  *
4
- * 流程:
5
- * 1. 校验 session key 已存在(来自 aigateway wallet-init
6
- * 2. 检查 USDT 余额 + facilitator allowance
7
- * 3. 如果 USDT < LOW_BALANCE_THRESHOLD1 USDT)或 allowance == 0,触发 WalletConnect 充值
8
- * - 充值 amount: TTY 模式交互选择 presets [5, 10, 20, 50] 或自定义;
9
- *TTY 模式需要传 --amount <usdt>,否则报 TOPUP_REQUIRED
10
- * - 同时按需转 0.0003 BNB 用作 approve gas
11
- * 4. session key 自己广播 ERC20.approve(facilitator, MaxUint256)
12
- * 5. 重新查余额 / allowance,返回最终状态
4
+ * Flow:
5
+ * 1. Verify the session key exists (created earlier by `aigateway wallet-init`).
6
+ * 2. Check USDT balance + facilitator allowance.
7
+ * 3. If USDT < LOW_BALANCE_THRESHOLD (1 USDT) or allowance == 0, open WalletConnect to fund:
8
+ * - Top-up amount: TTY mode picks interactively from presets [5, 10, 20, 50] or a custom value;
9
+ * non-TTY mode requires `--amount <usdt>`, otherwise TOPUP_REQUIRED is emitted.
10
+ * - 0.0003 BNB is transferred too when an approve transaction is needed.
11
+ * 4. The session key broadcasts ERC20.approve(facilitator, MaxUint256) once.
12
+ * 5. Re-query balance / allowance and return the final state.
13
13
  */
14
14
  import { resolve } from "../config.mjs";
15
15
  import { getWalletBalance, getAllowance } from "../balance.mjs";
@@ -71,7 +71,7 @@ export async function topup(opts) {
71
71
  const needApprove = allowance === 0n;
72
72
  const needGas = needApprove && bnbRaw === 0n;
73
73
 
74
- // 已就绪:余额够 + approve
74
+ // Already prepared: balance is sufficient and facilitator is approved
75
75
  if (!needTopup && !needApprove) {
76
76
  logInfo("Wallet already prepared (balance ≥ minimum, facilitator approved).");
77
77
  const data = {
@@ -89,7 +89,7 @@ export async function topup(opts) {
89
89
  return;
90
90
  }
91
91
 
92
- // 决定充值金额
92
+ // Decide the top-up amount
93
93
  let topupAmount = null;
94
94
  if (needTopup) {
95
95
  if (balanceLow) {
@@ -134,7 +134,7 @@ export async function topup(opts) {
134
134
  }
135
135
  }
136
136
 
137
- // WalletConnect 充值(USDT + 按需 BNB gas
137
+ // WalletConnect top-up (USDT + optional BNB for approve gas)
138
138
  if (needTopup || needGas) {
139
139
  const willTransfer = [];
140
140
  if (needTopup) willTransfer.push(`${topupAmount} USDT`);
@@ -161,7 +161,7 @@ export async function topup(opts) {
161
161
  }
162
162
  }
163
163
 
164
- // session key 一次性 approve facilitator
164
+ // Session key broadcasts the one-time facilitator approve
165
165
  let approveTx = null;
166
166
  if (needApprove) {
167
167
  let postBnbRaw = bnbRaw;
@@ -193,7 +193,7 @@ export async function topup(opts) {
193
193
  }
194
194
  }
195
195
 
196
- // 最终查余额 + allowance
196
+ // Final balance + allowance re-check
197
197
  let finalUsdt = usdt;
198
198
  let finalBnb = bnb;
199
199
  let finalAllowance = allowance;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * withdraw 命令:将 session key 中的资金转回主钱包(USDT + BNB
2
+ * wallet-withdraw: move the session key's funds (USDT + BNB) back to the main wallet.
3
3
  */
4
4
  import { createPublicClient, createWalletClient, http, parseUnits, formatUnits, encodeFunctionData } from "viem";
5
5
  import { privateKeyToAccount } from "viem/accounts";
@@ -54,7 +54,7 @@ export async function withdraw(opts) {
54
54
 
55
55
  const isWithdrawAll = !opts.amount;
56
56
 
57
- // 无任何资金
57
+ // No funds at all
58
58
  if (balance.usdtRaw === 0n && balance.bnbRaw === 0n) {
59
59
  emitErr("wallet-withdraw", "NO_FUNDS", { message: "No funds to withdraw.", appId });
60
60
  return;
@@ -63,9 +63,9 @@ export async function withdraw(opts) {
63
63
  let usdtTxHash = null;
64
64
  let bnbTxHash = null;
65
65
 
66
- // 1. 赎回 USDT(有 USDT 才执行)
66
+ // 1. Reclaim USDT (only when USDT balance > 0)
67
67
  if (balance.usdtRaw > 0n) {
68
- // USDT 转账需要 BNB gas
68
+ // USDT transfer needs BNB for gas
69
69
  if (balance.bnbRaw === 0n) {
70
70
  emitErr("wallet-withdraw", "INSUFFICIENT_BNB", {
71
71
  message: "No BNB for gas. Withdraw is a normal on-chain transfer and requires BNB to pay gas.",
@@ -119,7 +119,7 @@ export async function withdraw(opts) {
119
119
  }
120
120
  }
121
121
 
122
- // 2. 赎回剩余 BNB(仅赎回全部时)
122
+ // 2. Reclaim remaining BNB (only when withdrawing everything)
123
123
  if (isWithdrawAll) {
124
124
  const freshBalance = balance.usdtRaw > 0n
125
125
  ? await getBalanceByAddress(sessionAddress)
@@ -128,7 +128,7 @@ export async function withdraw(opts) {
128
128
  if (freshBalance.bnbRaw > 0n) {
129
129
  try {
130
130
  const gasPrice = await publicClient.getGasPrice();
131
- // 预留 20% buffer 应对 gas price 波动
131
+ // Reserve a 20% buffer to absorb gas-price fluctuations
132
132
  const gasCost = BNB_TRANSFER_GAS * (gasPrice * 120n / 100n);
133
133
  const sendable = freshBalance.bnbRaw - gasCost;
134
134
 
@@ -159,7 +159,7 @@ export async function withdraw(opts) {
159
159
  }
160
160
  }
161
161
 
162
- // 查询最终余额
162
+ // Final balance lookup
163
163
  let finalBalance;
164
164
  try {
165
165
  finalBalance = await getBalanceByAddress(sessionAddress);
package/src/config.mjs CHANGED
@@ -1,50 +1,50 @@
1
1
  /**
2
- * 配置管理:~/.aigateway/config.json
3
- * 优先级:CLI 参数 > 环境变量 > config.json
2
+ * Config management: ~/.aigateway/config.json
3
+ * Resolution priority: CLI args > env vars > config.json
4
4
  *
5
- * AEON AI Gateway 统一使用同一个 x402 服务端(ai-api.aeon.xyz),
6
- * 不同能力(虚拟卡 / Skill Boss 调用)走不同的路径前缀。
5
+ * AEON AI Gateway uses a single x402 service (ai-api.aeon.xyz).
7
6
  */
8
- import { readFileSync, writeFileSync, mkdirSync, chmodSync } from "fs";
9
- import { join } from "path";
10
- import { homedir } from "os";
7
+ import {readFileSync, writeFileSync, mkdirSync, chmodSync} from "fs";
8
+ import {join} from "path";
9
+ import {homedir} from "os";
11
10
 
12
11
  const CONFIG_DIR = join(homedir(), ".aigateway");
13
12
  const CONFIG_FILE = join(CONFIG_DIR, "config.json");
14
13
 
15
14
  const DEFAULTS = {
16
- serviceUrl: "https://ai-api.aeon.xyz",
15
+ serviceUrl: "https://ai-api-dev.aeon.xyz",
16
+ // serviceUrl: "https://ai-api.aeon.xyz",
17
17
  };
18
18
 
19
19
  export function loadConfig() {
20
- try {
21
- return { ...DEFAULTS, ...JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) };
22
- } catch {
23
- return { ...DEFAULTS };
24
- }
20
+ try {
21
+ return {...DEFAULTS, ...JSON.parse(readFileSync(CONFIG_FILE, "utf-8"))};
22
+ } catch {
23
+ return {...DEFAULTS};
24
+ }
25
25
  }
26
26
 
27
27
  export function saveConfig(config) {
28
- mkdirSync(CONFIG_DIR, { recursive: true });
29
- writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
30
- chmodSync(CONFIG_FILE, 0o600);
28
+ mkdirSync(CONFIG_DIR, {recursive: true});
29
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {mode: 0o600});
30
+ chmodSync(CONFIG_FILE, 0o600);
31
31
  }
32
32
 
33
33
  /**
34
- * 解析配置值,优先级:cliValue > envKey > config[configKey]
34
+ * Resolve a value with priority: cliValue > envKey > config[configKey]
35
35
  */
36
36
  export function resolve(cliValue, envKey, configKey) {
37
- if (cliValue) return cliValue;
38
- if (process.env[envKey]) return process.env[envKey];
39
- const cfg = loadConfig();
40
- return cfg[configKey] || undefined;
37
+ if (cliValue) return cliValue;
38
+ if (process.env[envKey]) return process.env[envKey];
39
+ const cfg = loadConfig();
40
+ return cfg[configKey] || undefined;
41
41
  }
42
42
 
43
43
  export function getConfigPath() {
44
- return CONFIG_FILE;
44
+ return CONFIG_FILE;
45
45
  }
46
46
 
47
47
  export function isSessionKeyMode() {
48
- const config = loadConfig();
49
- return config.mode === "session-key";
48
+ const config = loadConfig();
49
+ return config.mode === "session-key";
50
50
  }
@@ -1,39 +1,46 @@
1
1
  /**
2
- * 错误码常量表 —— CLI 与文档的单一事实源
2
+ * Error-code registry — single source of truth shared by the CLI and the docs.
3
3
  *
4
- * 退出码语义:
5
- * 0 成功
6
- * 1 用户错误(参数、余额、配置、用户拒绝)
7
- * 2 超时(轮询、WalletConnect、签名、链上)
8
- * 3 服务端 / 网络
9
- * 4 内部错误
4
+ * Exit-code semantics:
5
+ * 0 success
6
+ * 1 user error (bad argument, insufficient balance, configuration, user reject)
7
+ * 2 timeout (polling, WalletConnect, signature, on-chain wait)
8
+ * 3 service / network
9
+ * 4 internal error
10
10
  */
11
11
 
12
12
  export const ERROR_CODES = {
13
- // ===== 用户错误(exit 1)=====
13
+ // ===== User error (exit 1) =====
14
14
  WALLET_NOT_CONFIGURED: { exit: 1, message: "Wallet not configured. Run: aigateway wallet-init" },
15
15
  SERVICE_URL_MISSING: { exit: 1, message: "Service URL not configured." },
16
16
  AMOUNT_INVALID: { exit: 1, message: "Invalid amount." },
17
- AMOUNT_OUT_OF_RANGE: { exit: 1, message: "Amount is outside the allowed range." },
18
17
  AMOUNT_EXCEEDS_BALANCE: { exit: 1, message: "Requested amount exceeds available balance." },
19
18
  INSUFFICIENT_USDT: { exit: 1, message: "Insufficient USDT balance." },
20
19
  INSUFFICIENT_BNB: { exit: 1, message: "Insufficient BNB for gas." },
21
20
  NO_FUNDS: { exit: 1, message: "No funds available." },
22
21
  NO_MAIN_WALLET: { exit: 1, message: "No main wallet address configured. Use --to <address>." },
23
- MISSING_PROMPT: { exit: 1, message: "Missing --prompt. Provide a non-empty image prompt." },
22
+ MISSING_MODEL: { exit: 1, message: "Missing --model. Provide a tool model id (see references/tools.md)." },
23
+ MISSING_INPUTS: { exit: 1, message: "Missing --inputs. Provide a JSON object or @path/to/file.json." },
24
+ INVALID_INPUTS_JSON: { exit: 1, message: "Failed to parse --inputs as JSON." },
25
+ INVALID_INPUTS: { exit: 1, message: "Inputs failed schema validation." },
26
+ INPUTS_FILE_NOT_FOUND: { exit: 1, message: "Inputs file (passed via --inputs @path) not found." },
27
+ INVALID_MODEL_ID: { exit: 1, message: "Server rejected the model id." },
28
+ MODEL_PRICING_NOT_CONFIGURED: { exit: 1, message: "This model is not yet priced on the gateway. Ask the operator to add it to skillboss-pricing.json." },
29
+ INVALID_BODY: { exit: 1, message: "Server rejected the request body." },
24
30
  TOPUP_REQUIRED: { exit: 1, message: "Wallet top-up required. Choose an amount and rerun with --topup-amount <usdt>." },
25
31
  TOPUP_AMOUNT_TOO_SMALL: { exit: 1, message: "Top-up amount is below the minimum." },
26
32
  PAYMENT_REJECTED: { exit: 1, message: "Payment approval was rejected. Please try again if you'd like to proceed." },
27
33
 
28
- // ===== 超时(exit 2)=====
34
+ // ===== Timeout (exit 2) =====
29
35
  PAYMENT_TIMEOUT: { exit: 2, message: "Payment approval timed out. Please try again." },
30
36
  WC_SESSION_EXPIRED: { exit: 2, message: "WalletConnect session expired." },
31
- POLL_TIMEOUT: { exit: 2, message: "Polling timed out. Card may still be provisioning." },
32
37
  TX_TIMEOUT: { exit: 2, message: "On-chain transaction timed out." },
38
+ UPDATE_APPLIED: { exit: 2, message: "Package was just upgraded. Rerun the previous command on the new version." },
33
39
 
34
- // ===== 服务/网络(exit 3)=====
40
+ // ===== Service / network (exit 3) =====
35
41
  SERVICE_UNAVAILABLE: { exit: 3, message: "Service unavailable or network error." },
36
42
  PAYMENT_FETCH_FAILED: { exit: 3, message: "Failed to fetch payment requirements." },
43
+ CATALOG_FETCH_FAILED: { exit: 3, message: "Failed to fetch tools catalog from the server." },
37
44
  BALANCE_CHECK_FAILED: { exit: 3, message: "Failed to check balance." },
38
45
  ALLOWANCE_CHECK_FAILED: { exit: 3, message: "Failed to check allowance." },
39
46
  TX_REVERTED: { exit: 3, message: "On-chain transaction reverted." },
@@ -42,9 +49,10 @@ export const ERROR_CODES = {
42
49
  INVALID_PAYMENT_AMOUNT: { exit: 3, message: "Server returned invalid payment amount." },
43
50
  PAYMENT_FAILED: { exit: 3, message: "Payment request failed." },
44
51
  IMAGE_DOWNLOAD_FAILED: { exit: 3, message: "Image download failed." },
52
+ DOWNLOAD_FAILED: { exit: 3, message: "Output file download failed." },
45
53
  FUNDING_FAILED: { exit: 3, message: "Funding flow failed." },
46
54
 
47
- // ===== 内部(exit 4)=====
55
+ // ===== Internal (exit 4) =====
48
56
  INTERNAL_ERROR: { exit: 4, message: "Internal error." },
49
57
  WALLET_ERROR: { exit: 1, message: "Wallet operation failed." },
50
58
  };
package/src/funding.mjs CHANGED
@@ -2,8 +2,8 @@
2
2
  * Funding flow: WalletConnect-based USDT/BNB transfer to session key,
3
3
  * plus on-chain pre-authorization (ERC20.approve facilitator).
4
4
  *
5
- * Shared by: create-image (lazy top-up when balance is short),
6
- * prepare (proactive pre-flight before any image work).
5
+ * Shared by: sb-invoke (lazy top-up when balance is short),
6
+ * wallet-topup (proactive pre-flight).
7
7
  */
8
8
  import {
9
9
  withWallet,
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Minimal JSON Schema validator for tools-catalog inputs schemas.
3
+ *
4
+ * Supports the subset of JSON Schema actually used in tools-catalog.json:
5
+ * - type: object | string | number | integer | boolean | array
6
+ * - required: string[]
7
+ * - properties: { fieldName: <fieldSchema> }
8
+ * - enum: any[]
9
+ * - minimum / maximum (number, integer)
10
+ * - items (for arrays)
11
+ * - oneOf: [schema, ...] (treated as "any-of" — passes if any branch matches)
12
+ *
13
+ * We intentionally avoid pulling in ajv to keep the CLI dep tree small.
14
+ *
15
+ * @returns {{ ok: boolean, errors: Array<{ field: string, message: string, kind: "missing"|"type"|"enum"|"range"|"other" }> }}
16
+ */
17
+ export function validateInputs(inputs, schema) {
18
+ const errors = [];
19
+ if (!schema) return { ok: true, errors };
20
+
21
+ // root must be an object
22
+ if (schema.type === "object" || schema.properties || schema.required) {
23
+ if (typeof inputs !== "object" || inputs === null || Array.isArray(inputs)) {
24
+ errors.push({ field: "(root)", kind: "type", message: "inputs must be a JSON object" });
25
+ return { ok: false, errors };
26
+ }
27
+ }
28
+
29
+ // Required fields
30
+ if (Array.isArray(schema.required)) {
31
+ for (const field of schema.required) {
32
+ const v = inputs[field];
33
+ if (v === undefined || v === null || (typeof v === "string" && v.trim() === "")) {
34
+ errors.push({ field, kind: "missing", message: `required field "${field}" is missing` });
35
+ }
36
+ }
37
+ }
38
+
39
+ // Per-field validation
40
+ if (schema.properties) {
41
+ for (const [field, fieldSchema] of Object.entries(schema.properties)) {
42
+ if (inputs[field] === undefined || inputs[field] === null) continue;
43
+ errors.push(...validateField(field, inputs[field], fieldSchema));
44
+ }
45
+ }
46
+
47
+ return { ok: errors.length === 0, errors };
48
+ }
49
+
50
+ function validateField(field, value, schema) {
51
+ const errors = [];
52
+
53
+ // oneOf: pass if any branch accepts (no errors).
54
+ if (Array.isArray(schema.oneOf)) {
55
+ const branchErrors = schema.oneOf.map((sub) => validateField(field, value, sub));
56
+ const anyPass = branchErrors.some((errs) => errs.length === 0);
57
+ if (!anyPass) {
58
+ errors.push({
59
+ field,
60
+ kind: "type",
61
+ message: `value does not match any of oneOf schemas (tried ${schema.oneOf.length} branches)`,
62
+ });
63
+ }
64
+ return errors;
65
+ }
66
+
67
+ // Type check
68
+ if (schema.type) {
69
+ if (!checkType(value, schema.type)) {
70
+ errors.push({
71
+ field,
72
+ kind: "type",
73
+ message: `expected type "${schema.type}", got "${actualType(value)}"`,
74
+ });
75
+ return errors; // bail out on type mismatch
76
+ }
77
+ }
78
+
79
+ // Enum
80
+ if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
81
+ errors.push({
82
+ field,
83
+ kind: "enum",
84
+ message: `value ${JSON.stringify(value)} must be one of [${schema.enum.map((v) => JSON.stringify(v)).join(", ")}]`,
85
+ });
86
+ }
87
+
88
+ // Number range
89
+ if ((schema.type === "number" || schema.type === "integer") && typeof value === "number") {
90
+ if (schema.minimum != null && value < schema.minimum) {
91
+ errors.push({ field, kind: "range", message: `value ${value} is below minimum ${schema.minimum}` });
92
+ }
93
+ if (schema.maximum != null && value > schema.maximum) {
94
+ errors.push({ field, kind: "range", message: `value ${value} is above maximum ${schema.maximum}` });
95
+ }
96
+ }
97
+
98
+ // Array items
99
+ if (schema.type === "array" && Array.isArray(value) && schema.items) {
100
+ value.forEach((item, i) => {
101
+ errors.push(...validateField(`${field}[${i}]`, item, schema.items));
102
+ });
103
+ }
104
+
105
+ return errors;
106
+ }
107
+
108
+ function checkType(value, type) {
109
+ switch (type) {
110
+ case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
111
+ case "array": return Array.isArray(value);
112
+ case "string": return typeof value === "string";
113
+ case "number": return typeof value === "number" && !Number.isNaN(value);
114
+ case "integer": return typeof value === "number" && Number.isInteger(value);
115
+ case "boolean": return typeof value === "boolean";
116
+ case "null": return value === null;
117
+ default: return true;
118
+ }
119
+ }
120
+
121
+ function actualType(value) {
122
+ if (value === null) return "null";
123
+ if (Array.isArray(value)) return "array";
124
+ return typeof value;
125
+ }
package/src/output.mjs CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
- * 统一输出封装:envelope JSON + 分级日志
2
+ * Unified output envelope: JSON on stdout + tiered stderr logs.
3
3
  *
4
- * stdout: 一行最终 JSONmachine-readable
5
- * - 成功:{ ok: true, command, version, data }
6
- * - 失败:{ ok: false, command, version, error: { code, message, ...context } }
7
- * stderr: 进度日志(human-readable,agent 可忽略)
4
+ * stdout: a single line of final JSON (machine-readable)
5
+ * - success: { ok: true, command, version, data }
6
+ * - failure: { ok: false, command, version, error: { code, message, ...context } }
7
+ * stderr: progress logs (human-readable; agents can ignore them)
8
8
  *
9
- * --legacy-output 模式:保留旧裸字段格式,便于已按旧 JSON 解析的脚本/agent 平滑过渡。
9
+ * --legacy-output mode: emit the pre-envelope shape so scripts / agents that
10
+ * already parse the old JSON can migrate gradually.
10
11
  */
11
12
 
12
13
  import { readFileSync } from "fs";
@@ -30,10 +31,11 @@ export function isLegacyMode() { return LEGACY_MODE; }
30
31
  export function isVerboseMode() { return VERBOSE_MODE; }
31
32
 
32
33
  /**
33
- * 输出成功结果。调用方应在调用后让函数自然返回(不要再 process.exit)。
34
- * @param {string} command - 命令名,如 "create-card" / "wallet-init"
35
- * @param {object} data - envelope 模式下放在 data 字段
36
- * @param {object} [legacyShape] - legacy 模式下直接输出的旧格式对象;省略则使用 data 本身
34
+ * Emit a success result. Callers should let the function return naturally
35
+ * (do not call process.exit afterwards).
36
+ * @param {string} command - command name, e.g. "sb-invoke" / "wallet-init"
37
+ * @param {object} data - placed under envelope.data
38
+ * @param {object} [legacyShape] - the legacy-mode payload; if omitted, `data` is used
37
39
  */
38
40
  export function emitOk(command, data, legacyShape) {
39
41
  if (LEGACY_MODE) {
@@ -44,10 +46,11 @@ export function emitOk(command, data, legacyShape) {
44
46
  }
45
47
 
46
48
  /**
47
- * 输出错误并退出(按错误码对应的 exit 码)。
49
+ * Emit an error and exit with the corresponding exit code.
48
50
  * @param {string} command
49
- * @param {string} code - ERROR_CODES 键名
50
- * @param {object} [details] - 额外字段。message 字段会覆盖默认 message;legacy 字段在 legacy 模式下完全替代输出
51
+ * @param {string} code - key of ERROR_CODES
52
+ * @param {object} [details] - extra fields. `message` overrides the default message;
53
+ * `legacy` fully replaces the output in legacy mode.
51
54
  */
52
55
  export function emitErr(command, code, details = {}) {
53
56
  const info = ERROR_CODES[code] || ERROR_CODES.INTERNAL_ERROR;
@@ -69,17 +72,17 @@ export function emitErr(command, code, details = {}) {
69
72
  process.exit(exit);
70
73
  }
71
74
 
72
- /** 进度日志(quiet 模式压制) */
75
+ /** Progress log (suppressed in quiet mode) */
73
76
  export function logInfo(msg) {
74
77
  if (!QUIET_MODE) console.error(msg);
75
78
  }
76
79
 
77
- /** 详细日志(仅 verbose 模式下输出) */
80
+ /** Verbose log (only emitted in verbose mode) */
78
81
  export function logVerbose(msg) {
79
82
  if (VERBOSE_MODE && !QUIET_MODE) console.error(msg);
80
83
  }
81
84
 
82
- /** 错误日志(quiet 模式下也会输出) */
85
+ /** Error log (still emitted in quiet mode) */
83
86
  export function logError(msg) {
84
87
  console.error(msg);
85
88
  }