@solgridai/cli 0.2.0-beta.0 → 0.2.0-beta.1

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/README.md CHANGED
@@ -13,6 +13,19 @@ npm install -g @solgridai/cli
13
13
  solgrid --version
14
14
  ```
15
15
 
16
+ ### 测试版
17
+
18
+ 新版本正式发布前,会先以测试版(例如 `0.2.0-beta.0`)发布到 npm 的 `beta` 标签,用于提前试用新功能。直接执行 `npm install -g @solgridai/cli` 不会装到测试版。
19
+
20
+ ```bash
21
+ npm install -g @solgridai/cli@beta # 安装最新的测试版
22
+ npm install -g @solgridai/cli@0.2.0-beta.0 # 或指定版本
23
+ solgrid --version
24
+ npm install -g @solgridai/cli # 装回正式版
25
+ ```
26
+
27
+ 测试版的新功能可能依赖还没上线到生产环境的接口。这时请按发布说明,用 `SOLGRID_API_BASE_URL` 指向对应的环境,并使用该环境的 API Key(见[配置](#配置))。
28
+
16
29
  ## 快速开始
17
30
 
18
31
  ```bash
package/dist/index.js CHANGED
@@ -2075,7 +2075,7 @@ function useColor() {
2075
2075
  // node_modules/commander/index.js
2076
2076
  var program = new Command;
2077
2077
  // src/build-info.ts
2078
- var buildInfo = { packageName: "@solgridai/cli", version: "0.2.0-beta.0" };
2078
+ var buildInfo = { packageName: "@solgridai/cli", version: "0.2.0-beta.1" };
2079
2079
 
2080
2080
  // src/core/errors.ts
2081
2081
  class CliError extends Error {
@@ -2104,6 +2104,7 @@ class CliError extends Error {
2104
2104
  class ApiError extends CliError {
2105
2105
  status;
2106
2106
  key;
2107
+ keyDetail;
2107
2108
  routeMissing;
2108
2109
  apiKeySentTo;
2109
2110
  signedUrl;
@@ -2112,6 +2113,7 @@ class ApiError extends CliError {
2112
2113
  this.name = "ApiError";
2113
2114
  this.status = options.status;
2114
2115
  this.key = options.key;
2116
+ this.keyDetail = options.keyDetail;
2115
2117
  this.routeMissing = options.routeMissing === true;
2116
2118
  this.apiKeySentTo = options.apiKeySentTo;
2117
2119
  this.signedUrl = options.signedUrl === true;
@@ -2120,6 +2122,7 @@ class ApiError extends CliError {
2120
2122
  return new ApiError(this.code, this.message, {
2121
2123
  ...this.status !== undefined ? { status: this.status } : {},
2122
2124
  ...this.key ? { key: this.key } : {},
2125
+ ...this.keyDetail ? { keyDetail: this.keyDetail } : {},
2123
2126
  routeMissing: this.routeMissing,
2124
2127
  ...this.apiKeySentTo ? { apiKeySentTo: this.apiKeySentTo } : {},
2125
2128
  signedUrl: this.signedUrl,
@@ -2541,6 +2544,41 @@ function formatAmount(amount) {
2541
2544
  const sign = negative ? "-" : "";
2542
2545
  return amount.currency === "USD" ? `${sign}$${number}` : `${sign}${number} ${amount.currency}`;
2543
2546
  }
2547
+ function plainDecimal(value) {
2548
+ if (!Number.isFinite(value))
2549
+ return;
2550
+ const text = String(value);
2551
+ const match = /^(-?)(\d+)(?:\.(\d+))?e([+-]\d+)$/.exec(text);
2552
+ if (!match)
2553
+ return text;
2554
+ const [, sign = "", whole = "", fraction = "", exponent = "0"] = match;
2555
+ const digits = whole + fraction;
2556
+ const point = whole.length + Number(exponent);
2557
+ if (point <= 0)
2558
+ return `${sign}0.${"0".repeat(-point)}${digits}`;
2559
+ if (point >= digits.length)
2560
+ return `${sign}${digits}${"0".repeat(point - digits.length)}`;
2561
+ return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
2562
+ }
2563
+ function ceilToSixDecimals(value) {
2564
+ const decimal = typeof value === "number" ? plainDecimal(value) : value.trim();
2565
+ if (decimal === undefined || !/^\d+(\.\d+)?$/.test(decimal))
2566
+ return;
2567
+ const [whole = "0", fraction = ""] = decimal.split(".");
2568
+ let micros = BigInt(whole + fraction.slice(0, 6).padEnd(6, "0"));
2569
+ if (/[1-9]/.test(fraction.slice(6)))
2570
+ micros += 1n;
2571
+ const text = micros.toString().padStart(7, "0");
2572
+ return `${text.slice(0, -6)}.${text.slice(-6)}`;
2573
+ }
2574
+ function formatUnitPrice(amount) {
2575
+ if (!amount || typeof amount.value !== "number" && typeof amount.value !== "string")
2576
+ return "-";
2577
+ const decimal = ceilToSixDecimals(amount.value);
2578
+ if (decimal === undefined)
2579
+ return formatAmount(amount);
2580
+ return amount.currency === "USD" ? `$${decimal}` : `${decimal} ${amount.currency}`;
2581
+ }
2544
2582
 
2545
2583
  // src/core/api/client.ts
2546
2584
  var DEFAULT_TIMEOUT_MS = 30000;
@@ -2664,6 +2702,7 @@ function envelopeError(response, envelope, text, url, auth) {
2664
2702
  const status = bodyCode ?? (response.ok ? 400 : response.status);
2665
2703
  const serverMessage = typeof envelope?.message === "string" && envelope.message ? envelope.message : undefined;
2666
2704
  const key = errorKeyOf(envelope);
2705
+ const keyDetail = keyDetailOf(envelope);
2667
2706
  const details = [
2668
2707
  ...validationDetails(envelope?.errorData, key),
2669
2708
  ...frameworkErrorDetails(envelope?.errors)
@@ -2673,6 +2712,7 @@ function envelopeError(response, envelope, text, url, auth) {
2673
2712
  return new ApiError(errorCodeForStatus(status), message, {
2674
2713
  status,
2675
2714
  ...key ? { key } : {},
2715
+ ...keyDetail ? { keyDetail } : {},
2676
2716
  ...auth,
2677
2717
  ...serverJsonHint ? { hint: serverJsonHint } : {},
2678
2718
  details
@@ -2688,10 +2728,20 @@ function hasSerializerErrors(errors) {
2688
2728
  const value = errors?.serializerErrors;
2689
2729
  return Array.isArray(value) ? value.length > 0 : typeof value === "string" && value.length > 0;
2690
2730
  }
2731
+ function keyItemOf(envelope) {
2732
+ const items = Array.isArray(envelope?.errorData) ? envelope.errorData : [];
2733
+ return items.find((entry) => entry && typeof entry === "object" && !entry.propertyName && typeof entry.errorCode === "string" && ERROR_KEY.test(entry.errorCode));
2734
+ }
2735
+ function keyDetailOf(envelope) {
2736
+ const item = keyItemOf(envelope);
2737
+ if (!item)
2738
+ return;
2739
+ const { errorCode: _code, errorMessage: _message, propertyName: _property, ...rest } = item;
2740
+ return Object.keys(rest).length > 0 ? rest : undefined;
2741
+ }
2691
2742
  var ERROR_KEY = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
2692
2743
  function errorKeyOf(envelope) {
2693
- const items = Array.isArray(envelope?.errorData) ? envelope.errorData : [];
2694
- const item = items.find((entry) => entry && !entry.propertyName && typeof entry.errorCode === "string" && ERROR_KEY.test(entry.errorCode));
2744
+ const item = keyItemOf(envelope);
2695
2745
  if (item?.errorCode)
2696
2746
  return item.errorCode;
2697
2747
  const message = envelope?.message;
@@ -2849,7 +2899,44 @@ function presentApiError(error) {
2849
2899
  `) || undefined;
2850
2900
  return { message: base.message, hint };
2851
2901
  }
2902
+ function isAmount(value) {
2903
+ const amount = value;
2904
+ return typeof amount === "object" && amount !== null && (typeof amount.value === "string" || typeof amount.value === "number");
2905
+ }
2906
+ var RAISE_BUDGET = `Raise or disable the daily budget at ${CONSOLE_URLS.budgets}`;
2907
+ function explainDailyBudget(detail) {
2908
+ const { reason, spent, held, requestedReservation, dailyLimit, remaining } = detail ?? {};
2909
+ const amounts = [spent, held, requestedReservation, dailyLimit, remaining];
2910
+ if (!amounts.every(isAmount))
2911
+ return;
2912
+ const [spentText, heldText, needText, limitText, leftText] = amounts.map((amount) => formatAmount(amount));
2913
+ switch (reason) {
2914
+ case "daily_spend_limit_reached":
2915
+ return {
2916
+ message: `Today's spending (${spentText}) has reached the daily budget of ${limitText}.`,
2917
+ hint: `${RAISE_BUDGET}, or try again tomorrow (the budget resets at 00:00 UTC).`
2918
+ };
2919
+ case "requested_reservation_exceeds_remaining_budget":
2920
+ return {
2921
+ message: `This run requires a reservation of ${needText}, more than the ${leftText} left of today's ${limitText} budget (${spentText} spent).`,
2922
+ hint: RAISE_BUDGET
2923
+ };
2924
+ case "reservation_exceeds_remaining_budget":
2925
+ return {
2926
+ message: `Runs still in progress hold ${heldText} of today's ${limitText} budget, leaving ${leftText}; this run requires a reservation of ${needText}.`,
2927
+ hint: `Wait for those runs to finish (what they do not use is released), then retry with the same idempotency key, or submit runs one at a time.
2928
+ ${RAISE_BUDGET}`
2929
+ };
2930
+ default:
2931
+ return;
2932
+ }
2933
+ }
2852
2934
  function explain(error) {
2935
+ if (error.key === "daily_budget_exceeded") {
2936
+ const budget = explainDailyBudget(error.keyDetail);
2937
+ if (budget)
2938
+ return budget;
2939
+ }
2853
2940
  const known = error.key ? KEY_PRESENTATIONS[error.key] : undefined;
2854
2941
  if (known)
2855
2942
  return known;
@@ -2977,6 +3064,7 @@ function createOutput(ctx, json) {
2977
3064
  message,
2978
3065
  ...cliError instanceof ApiError && cliError.status !== undefined ? { status: cliError.status } : {},
2979
3066
  ...cliError instanceof ApiError && cliError.key ? { key: cliError.key } : {},
3067
+ ...cliError instanceof ApiError && cliError.keyDetail ? { detail: cliError.keyDetail } : {},
2980
3068
  ...cliError.details.length > 0 ? { details: cliError.details } : {},
2981
3069
  ...hint ? { hint } : {}
2982
3070
  },
@@ -4988,8 +5076,8 @@ function withUnit(amount, unit) {
4988
5076
  return unit ? `${amount} ${unit}` : amount;
4989
5077
  }
4990
5078
  function describeLeaf(leaf) {
4991
- const text = withUnit(formatAmount(leaf.amount), unitText(leaf));
4992
- return leaf.flatFee ? `${text} + ${formatAmount(leaf.flatFee)} flat` : text;
5079
+ const text = withUnit(formatUnitPrice(leaf.amount), unitText(leaf));
5080
+ return leaf.flatFee ? `${text} + ${formatUnitPrice(leaf.flatFee)} flat` : text;
4993
5081
  }
4994
5082
  var METER_UNITS = {
4995
5083
  Calls: ["call", "calls"],
@@ -5013,8 +5101,7 @@ function meterText(meter, size = 1) {
5013
5101
  return size === 1 ? `per ${one}` : `per ${countText(size)} ${many}`;
5014
5102
  }
5015
5103
  function rateAmount(rate) {
5016
- const micros = Math.ceil(Number((rate.unitPriceUsd * 1e6).toPrecision(12)));
5017
- return formatAmount({ value: (micros / 1e6).toFixed(6), currency: "USD" });
5104
+ return formatUnitPrice({ value: rate.unitPriceUsd, currency: "USD" });
5018
5105
  }
5019
5106
  function describeRate(rate) {
5020
5107
  return `${rateAmount(rate)} ${meterText(rate.meter, rate.unitSize)}`;
@@ -5034,7 +5121,7 @@ function hasConditions(rate) {
5034
5121
  function summarizeRates(price, rates) {
5035
5122
  const only = rates.length === 1 ? rates[0] : undefined;
5036
5123
  if (only) {
5037
- const amount = price.amount ? formatAmount(price.amount) : rateAmount(only);
5124
+ const amount = price.amount ? formatUnitPrice(price.amount) : rateAmount(only);
5038
5125
  const text = `${amount} ${meterText(only.meter, only.unitSize)}`;
5039
5126
  return hasConditions(only) ? `${text} (${conditionsText(only)})` : text;
5040
5127
  }
@@ -5069,20 +5156,20 @@ function summarizePrice(price, fallback) {
5069
5156
  switch (price.type) {
5070
5157
  case "PER_UNIT_MATRIX": {
5071
5158
  const first = price.variants?.[0]?.price;
5072
- text = `from ${withUnit(formatAmount(price.amount), first ? unitText(first) : "")}`;
5159
+ text = `from ${withUnit(formatUnitPrice(price.amount), first ? unitText(first) : "")}`;
5073
5160
  break;
5074
5161
  }
5075
5162
  case "TIERED":
5076
5163
  text = price.default ? describeLeaf(price.default) : "varies";
5077
5164
  break;
5078
5165
  case "BY_PERIOD":
5079
- text = withUnit(formatAmount(price.amount), price.period ? periodText(price.period) : "");
5166
+ text = withUnit(formatUnitPrice(price.amount), price.period ? periodText(price.period) : "");
5080
5167
  break;
5081
5168
  default:
5082
- text = withUnit(formatAmount(price.amount), unitText(price));
5169
+ text = withUnit(formatUnitPrice(price.amount), unitText(price));
5083
5170
  }
5084
5171
  if (price.flatFee)
5085
- text += ` + ${formatAmount(price.flatFee)} flat`;
5172
+ text += ` + ${formatUnitPrice(price.flatFee)} flat`;
5086
5173
  if (price.type === "TIERED" && price.tiers?.length)
5087
5174
  text += " + add-ons";
5088
5175
  return text;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solgridai/cli",
3
- "version": "0.2.0-beta.0",
3
+ "version": "0.2.0-beta.1",
4
4
  "description": "Command-line client for Solgrid: discover, inspect and run tools from your terminal or agent.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -172,9 +172,18 @@ with the same key, or look the run up first.
172
172
  | `error.key` | Meaning |
173
173
  |---|---|
174
174
  | `wallet_balance_insufficient` | Balance too low; the user can top up on the console's Wallet page |
175
- | `daily_budget_exceeded` | Daily budget reached; adjustable on the console's Budgets page |
175
+ | `daily_budget_exceeded` | Daily budget reached; adjustable on the console's Budgets page (but see below) |
176
176
  | `per_call_limit_exceeded` | The run costs more than the per-call limit; adjustable on the console's Budgets page |
177
177
 
178
+ - The daily budget counts today's spending plus what runs still in progress
179
+ hold. If `daily_budget_exceeded` comes with
180
+ `error.detail.reason: "reservation_exceeds_remaining_budget"`, those holds
181
+ are the cause: wait for the user's runs in progress to finish, then retry
182
+ with the **same** idempotency key (a refused run was never started), or
183
+ submit runs one at a time. `error.detail` also has `spent`, `held`,
184
+ `requestedReservation`, `dailyLimit` and `remaining`.
185
+ - The budget is checked when a run starts; a run's final cost can exceed what
186
+ it held, so a day's spending can end slightly over the budget.
178
187
  - Never top up the wallet or change budgets on the user's behalf.
179
188
 
180
189
  ## Using local files as input