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

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
@@ -45,7 +58,7 @@ solgrid run -p <provider> -e <endpoint> -i '{"screen_name":"nasa"}' --wait
45
58
  | `files get -i <fileId>` | 查看文件的类型、大小和状态 |
46
59
  | `files url -i <fileId>` | 为已上传的文件重新签发 24 小时有效的 URL |
47
60
  | `files download -i <fileId> [-o path] [--force]` | 通过签名 URL 下载文件到本地 |
48
- | `skills list [-q text] [--kind k] [--mine] [-l n] [--page n]` | 在 SkillHub 搜索 Skill(匹配标题和标签) |
61
+ | `skills list [-q text] [--kind k] [--mine] [-l n] [--page n]` | 在 SkillHub 搜索 Skill(`-q` 按标题搜索) |
49
62
  | `skills get (-s slug \| -i id)` | 查看 Skill 详情 |
50
63
  | `skills tags` | 列出 Skill 可用的标签 |
51
64
  | `skills create [-s slug] -t <title> --kind <k> -d <text> (--dir <dir> \| --zip <file>) [--cover f] [--preview f] [--tag id]… [-y]` | 发布 Skill,创建后立即公开 |
@@ -89,8 +102,8 @@ solgrid skills create -t "Mecha portrait" --kind image \
89
102
 
90
103
  用 `--dir` 时 slug 默认取 `name`,传了 `-s` 就必须和 `name` 一致;用 `--zip` 时必须传 `-s`,由服务端核对。`skills update` 换包时,新包的 `name` 也必须等于原来的 slug。
91
104
  - `-d` 是显示在标题下的简短描述(纯文本,不按 Markdown 渲染)。Skill 的详细说明请写在 `SKILL.md` 里。
92
- - `--kind` 表示 Skill 产出的作品类型:`image`、`video`、`web`、`audio`。除视频外都需要封面(`--cover`);视频和音频需要预览(`--preview`,图片、视频或音频)。
93
- - `--tag` 填 `solgrid skills tags` 列出的标签 ID,最多 3 个。
105
+ - `--kind` 表示 Skill 产出的作品类型:`image`、`video`、`web`、`audio`。除视频外都需要封面(`--cover`,图片)。预览(`--preview`)的类型由 kind 决定:图片和网页类可选,只能是图片;视频类必填,只能是视频;音频类必填,只能是音频。
106
+ - `--tag` 填 `solgrid skills tags` 列出的标签 ID,最多 3 个,不能重复。
94
107
  - `create`、`delete` 会先请你确认;在脚本或 Agent 中使用时加 `-y`。
95
108
  - `update` 只修改传入的选项;`--tag` 会替换全部标签,`--clear-tags` 清空标签,`--clear-preview` 去掉预览(仅图片和网页类 Skill)。只能修改、删除自己的 Skill。
96
109
 
@@ -119,6 +132,7 @@ solgrid run -p <provider> -e <endpoint> -i '<同样的输入>' --idempotency-key
119
132
  | `~/.config/solgrid/cli/config.json` | 当前激活的 key |
120
133
  | `SOLGRID_API_KEY` | 设置后优先于本机保存的 key,适合 CI 和 Agent |
121
134
  | `SOLGRID_API_BASE_URL` | 覆盖 API 地址,默认为 `https://api.solgrid.ai` |
135
+ | `SOLGRID_LANG` / `--lang` | 服务端返回文本(标签名、错误信息)的语言:`en`(默认)或 `zh`;`--lang` 是全局选项,优先于环境变量。标签 ID、错误键和 CLI 自身的提示不变 |
122
136
  | `NO_COLOR` / `FORCE_COLOR` | 关闭或强制彩色输出 |
123
137
 
124
138
  ## 开发
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.2" };
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,
@@ -2341,8 +2344,10 @@ var CONSOLE_URLS = {
2341
2344
  };
2342
2345
  var ENV = {
2343
2346
  apiKey: "SOLGRID_API_KEY",
2344
- apiBaseUrl: "SOLGRID_API_BASE_URL"
2347
+ apiBaseUrl: "SOLGRID_API_BASE_URL",
2348
+ language: "SOLGRID_LANG"
2345
2349
  };
2350
+ var LANGUAGES = ["en", "zh"];
2346
2351
 
2347
2352
  // src/core/config.ts
2348
2353
  function resolveConfigDir(env, homeDir) {
@@ -2541,6 +2546,41 @@ function formatAmount(amount) {
2541
2546
  const sign = negative ? "-" : "";
2542
2547
  return amount.currency === "USD" ? `${sign}$${number}` : `${sign}${number} ${amount.currency}`;
2543
2548
  }
2549
+ function plainDecimal(value) {
2550
+ if (!Number.isFinite(value))
2551
+ return;
2552
+ const text = String(value);
2553
+ const match = /^(-?)(\d+)(?:\.(\d+))?e([+-]\d+)$/.exec(text);
2554
+ if (!match)
2555
+ return text;
2556
+ const [, sign = "", whole = "", fraction = "", exponent = "0"] = match;
2557
+ const digits = whole + fraction;
2558
+ const point = whole.length + Number(exponent);
2559
+ if (point <= 0)
2560
+ return `${sign}0.${"0".repeat(-point)}${digits}`;
2561
+ if (point >= digits.length)
2562
+ return `${sign}${digits}${"0".repeat(point - digits.length)}`;
2563
+ return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
2564
+ }
2565
+ function ceilToSixDecimals(value) {
2566
+ const decimal = typeof value === "number" ? plainDecimal(value) : value.trim();
2567
+ if (decimal === undefined || !/^\d+(\.\d+)?$/.test(decimal))
2568
+ return;
2569
+ const [whole = "0", fraction = ""] = decimal.split(".");
2570
+ let micros = BigInt(whole + fraction.slice(0, 6).padEnd(6, "0"));
2571
+ if (/[1-9]/.test(fraction.slice(6)))
2572
+ micros += 1n;
2573
+ const text = micros.toString().padStart(7, "0");
2574
+ return `${text.slice(0, -6)}.${text.slice(-6)}`;
2575
+ }
2576
+ function formatUnitPrice(amount) {
2577
+ if (!amount || typeof amount.value !== "number" && typeof amount.value !== "string")
2578
+ return "-";
2579
+ const decimal = ceilToSixDecimals(amount.value);
2580
+ if (decimal === undefined)
2581
+ return formatAmount(amount);
2582
+ return amount.currency === "USD" ? `$${decimal}` : `${decimal} ${amount.currency}`;
2583
+ }
2544
2584
 
2545
2585
  // src/core/api/client.ts
2546
2586
  var DEFAULT_TIMEOUT_MS = 30000;
@@ -2553,6 +2593,9 @@ class ApiClient {
2553
2593
  this.options = options;
2554
2594
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
2555
2595
  }
2596
+ languageHeader() {
2597
+ return this.options.language ? { "Accept-Language": this.options.language } : {};
2598
+ }
2556
2599
  post(path, body = {}, headers = {}, request = {}) {
2557
2600
  return this.send("POST", path, {
2558
2601
  body: JSON.stringify(body),
@@ -2594,6 +2637,7 @@ class ApiClient {
2594
2637
  method: "GET",
2595
2638
  headers: {
2596
2639
  "X-Solgrid-Client": this.options.clientId,
2640
+ ...this.languageHeader(),
2597
2641
  ...this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {}
2598
2642
  },
2599
2643
  signal: AbortSignal.timeout(TRANSFER_TIMEOUT_MS)
@@ -2620,6 +2664,7 @@ class ApiClient {
2620
2664
  headers: {
2621
2665
  Accept: "application/json",
2622
2666
  "X-Solgrid-Client": this.options.clientId,
2667
+ ...this.languageHeader(),
2623
2668
  ...this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {},
2624
2669
  ...options.headers
2625
2670
  },
@@ -2664,6 +2709,7 @@ function envelopeError(response, envelope, text, url, auth) {
2664
2709
  const status = bodyCode ?? (response.ok ? 400 : response.status);
2665
2710
  const serverMessage = typeof envelope?.message === "string" && envelope.message ? envelope.message : undefined;
2666
2711
  const key = errorKeyOf(envelope);
2712
+ const keyDetail = keyDetailOf(envelope);
2667
2713
  const details = [
2668
2714
  ...validationDetails(envelope?.errorData, key),
2669
2715
  ...frameworkErrorDetails(envelope?.errors)
@@ -2673,6 +2719,7 @@ function envelopeError(response, envelope, text, url, auth) {
2673
2719
  return new ApiError(errorCodeForStatus(status), message, {
2674
2720
  status,
2675
2721
  ...key ? { key } : {},
2722
+ ...keyDetail ? { keyDetail } : {},
2676
2723
  ...auth,
2677
2724
  ...serverJsonHint ? { hint: serverJsonHint } : {},
2678
2725
  details
@@ -2688,10 +2735,20 @@ function hasSerializerErrors(errors) {
2688
2735
  const value = errors?.serializerErrors;
2689
2736
  return Array.isArray(value) ? value.length > 0 : typeof value === "string" && value.length > 0;
2690
2737
  }
2738
+ function keyItemOf(envelope) {
2739
+ const items = Array.isArray(envelope?.errorData) ? envelope.errorData : [];
2740
+ return items.find((entry) => entry && typeof entry === "object" && !entry.propertyName && typeof entry.errorCode === "string" && ERROR_KEY.test(entry.errorCode));
2741
+ }
2742
+ function keyDetailOf(envelope) {
2743
+ const item = keyItemOf(envelope);
2744
+ if (!item)
2745
+ return;
2746
+ const { errorCode: _code, errorMessage: _message, propertyName: _property, ...rest } = item;
2747
+ return Object.keys(rest).length > 0 ? rest : undefined;
2748
+ }
2691
2749
  var ERROR_KEY = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
2692
2750
  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));
2751
+ const item = keyItemOf(envelope);
2695
2752
  if (item?.errorCode)
2696
2753
  return item.errorCode;
2697
2754
  const message = envelope?.message;
@@ -2849,7 +2906,44 @@ function presentApiError(error) {
2849
2906
  `) || undefined;
2850
2907
  return { message: base.message, hint };
2851
2908
  }
2909
+ function isAmount(value) {
2910
+ const amount = value;
2911
+ return typeof amount === "object" && amount !== null && (typeof amount.value === "string" || typeof amount.value === "number");
2912
+ }
2913
+ var RAISE_BUDGET = `Raise or disable the daily budget at ${CONSOLE_URLS.budgets}`;
2914
+ function explainDailyBudget(detail) {
2915
+ const { reason, spent, held, requestedReservation, dailyLimit, remaining } = detail ?? {};
2916
+ const amounts = [spent, held, requestedReservation, dailyLimit, remaining];
2917
+ if (!amounts.every(isAmount))
2918
+ return;
2919
+ const [spentText, heldText, needText, limitText, leftText] = amounts.map((amount) => formatAmount(amount));
2920
+ switch (reason) {
2921
+ case "daily_spend_limit_reached":
2922
+ return {
2923
+ message: `Today's spending (${spentText}) has reached the daily budget of ${limitText}.`,
2924
+ hint: `${RAISE_BUDGET}, or try again tomorrow (the budget resets at 00:00 UTC).`
2925
+ };
2926
+ case "requested_reservation_exceeds_remaining_budget":
2927
+ return {
2928
+ message: `This run requires a reservation of ${needText}, more than the ${leftText} left of today's ${limitText} budget (${spentText} spent).`,
2929
+ hint: RAISE_BUDGET
2930
+ };
2931
+ case "reservation_exceeds_remaining_budget":
2932
+ return {
2933
+ message: `Runs still in progress hold ${heldText} of today's ${limitText} budget, leaving ${leftText}; this run requires a reservation of ${needText}.`,
2934
+ 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.
2935
+ ${RAISE_BUDGET}`
2936
+ };
2937
+ default:
2938
+ return;
2939
+ }
2940
+ }
2852
2941
  function explain(error) {
2942
+ if (error.key === "daily_budget_exceeded") {
2943
+ const budget = explainDailyBudget(error.keyDetail);
2944
+ if (budget)
2945
+ return budget;
2946
+ }
2853
2947
  const known = error.key ? KEY_PRESENTATIONS[error.key] : undefined;
2854
2948
  if (known)
2855
2949
  return known;
@@ -2977,6 +3071,7 @@ function createOutput(ctx, json) {
2977
3071
  message,
2978
3072
  ...cliError instanceof ApiError && cliError.status !== undefined ? { status: cliError.status } : {},
2979
3073
  ...cliError instanceof ApiError && cliError.key ? { key: cliError.key } : {},
3074
+ ...cliError instanceof ApiError && cliError.keyDetail ? { detail: cliError.keyDetail } : {},
2980
3075
  ...cliError.details.length > 0 ? { details: cliError.details } : {},
2981
3076
  ...hint ? { hint } : {}
2982
3077
  },
@@ -3031,13 +3126,29 @@ function apiClient(ctx) {
3031
3126
  });
3032
3127
  }
3033
3128
  sentKeys.set(ctx, resolved);
3129
+ const language = requestLanguage(ctx);
3034
3130
  return new ApiClient({
3035
3131
  baseUrl: ctx.env[ENV.apiBaseUrl]?.trim() || DEFAULT_API_BASE_URL,
3036
3132
  apiKey: resolved.key,
3037
3133
  clientId: `cli@${buildInfo.version}`,
3134
+ ...language ? { language } : {},
3038
3135
  fetch: ctx.fetch
3039
3136
  });
3040
3137
  }
3138
+ function requestLanguage(ctx) {
3139
+ const value = ctx.language ?? ctx.env[ENV.language]?.trim();
3140
+ if (!value)
3141
+ return;
3142
+ return parseLanguage(value, ctx.language === undefined ? ENV.language : "--lang");
3143
+ }
3144
+ function parseLanguage(value, source = "--lang") {
3145
+ const language = value.trim().toLowerCase();
3146
+ if (LANGUAGES.includes(language))
3147
+ return language;
3148
+ throw new CliError("USAGE", `Unsupported language in ${source}: ${value}.`, {
3149
+ hint: `Use one of: ${LANGUAGES.join(", ")}.`
3150
+ });
3151
+ }
3041
3152
  function withAuthContext(ctx, error) {
3042
3153
  const key = sentKeys.get(ctx);
3043
3154
  if (!(error instanceof ApiError) || error.code !== "AUTH_FAILED" || !key)
@@ -4070,6 +4181,7 @@ function registerSessionIdCommand(program, ctx) {
4070
4181
 
4071
4182
  // src/commands/skills.ts
4072
4183
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
4184
+ import { basename as basename4 } from "node:path";
4073
4185
 
4074
4186
  // src/core/skill-markdown.ts
4075
4187
  var MAX_SKILL_NAME_LENGTH = 64;
@@ -4521,6 +4633,10 @@ async function listSkillTags(client) {
4521
4633
  return checked.tags;
4522
4634
  }
4523
4635
  function checkTags(tags, available) {
4636
+ const repeated = [...new Set(tags.filter((tag, index) => tags.indexOf(tag) !== index))];
4637
+ if (repeated.length > 0) {
4638
+ throw new CliError("USAGE", `Tag${repeated.length > 1 ? "s" : ""} given more than once: ${repeated.join(", ")}.`, { hint: "Pass each --tag once." });
4639
+ }
4524
4640
  if (tags.length > MAX_TAGS) {
4525
4641
  throw new CliError("USAGE", `A Skill takes at most ${MAX_TAGS} tags; ${tags.length} were given.`);
4526
4642
  }
@@ -4532,6 +4648,9 @@ function checkTags(tags, available) {
4532
4648
  });
4533
4649
  }
4534
4650
  }
4651
+ function previewKindFor(kind) {
4652
+ return kind === "video" ? "video" : kind === "audio" ? "audio" : "image";
4653
+ }
4535
4654
  var PUBLIC_DISPLAY = "1";
4536
4655
  function prepareSkillFile(path, role) {
4537
4656
  let size;
@@ -4605,7 +4724,7 @@ function collect2(value, previous = []) {
4605
4724
  }
4606
4725
  function addContentOptions(command, required) {
4607
4726
  const note = required ? "" : " (replaces the current one)";
4608
- command.option("-t, --title <title>", `title${note}`).option("--kind <kind>", `what the Skill makes: ${SKILL_KINDS.join(", ")}${note}`, parseKind2).option("-d, --description <text>", `a short description shown under the title${note}`).option("--description-file <path>", "read the description from a file instead").option("--tag <id>", `a tag ID from \`solgrid skills tags\`; up to ${MAX_TAGS}, repeat for several${required ? "" : " (replaces all current tags)"}`, collect2).option("--cover <path>", `cover image; required unless the kind is video${note}`).option("--preview <path>", `preview image, video or audio; required for video and audio${note}`).option("--dir <path>", `a Skill directory with SKILL.md at its top, packed for you${note}`).option("--zip <path>", `or an already packed Skill (.zip with SKILL.md)${note}`);
4727
+ command.option("-t, --title <title>", `title${note}`).option("--kind <kind>", `what the Skill makes: ${SKILL_KINDS.join(", ")}${note}`, parseKind2).option("-d, --description <text>", `a short description shown under the title${note}`).option("--description-file <path>", "read the description from a file instead").option("--tag <id>", `a tag ID from \`solgrid skills tags\`; up to ${MAX_TAGS}, repeat for several${required ? "" : " (replaces all current tags)"}`, collect2).option("--cover <path>", `cover image; required unless the kind is video${note}`).option("--preview <path>", `preview of what the Skill makes: an image for image and web Skills (optional), a video for video Skills, an audio file for audio Skills${note}`).option("--dir <path>", `a Skill directory with SKILL.md at its top, packed for you${note}`).option("--zip <path>", `or an already packed Skill (.zip with SKILL.md)${note}`);
4609
4728
  return command;
4610
4729
  }
4611
4730
  function readDescription(opts) {
@@ -4631,7 +4750,22 @@ function checkMedia(kind, hasCover, hasPreview) {
4631
4750
  }
4632
4751
  if ((kind === "video" || kind === "audio") && !hasPreview) {
4633
4752
  throw new CliError("USAGE", `${aSkill(kind)} needs a preview.`, {
4634
- hint: `Pass --preview <${kind} or image file>.`
4753
+ hint: `Pass --preview <${previewKindFor(kind)} file>.`
4754
+ });
4755
+ }
4756
+ }
4757
+ var A_FILE = {
4758
+ image: "an image",
4759
+ video: "a video",
4760
+ audio: "an audio file",
4761
+ skill: "a Skill package"
4762
+ };
4763
+ function checkPreviewFile(kind, path) {
4764
+ const { kind: fileKind } = prepareSkillFile(path, "preview");
4765
+ const expected = previewKindFor(kind);
4766
+ if (fileKind !== expected) {
4767
+ throw new CliError("USAGE", `${aSkill(kind)}'s preview must be ${A_FILE[expected]}, not ${A_FILE[fileKind]}: ${basename4(path)}.`, {
4768
+ hint: "Image and web Skills take an image preview, video Skills a video, audio Skills an audio file."
4635
4769
  });
4636
4770
  }
4637
4771
  }
@@ -4723,7 +4857,7 @@ function registerSkillsCommand(program, ctx) {
4723
4857
  const skills = program.command("skills").description("Find, publish and manage Skills on SkillHub.").addHelpText("after", `
4724
4858
  A Skill is public as soon as it is created. Its slug identifies it everywhere and cannot
4725
4859
  be changed later.`);
4726
- skills.command("list").description("Search Skills, newest first.").option("-q, --query <words>", "match titles and tags").option("--kind <kind>", `only this kind: ${SKILL_KINDS.join(", ")}`, parseKind2).option("--mine", "only your own Skills").option("-l, --limit <n>", "Skills per page (default 20, up to 100)", parsePositiveInt).option("--page <n>", "page number (default 1)", parsePositiveInt).option(...JSON_FLAG).action((opts) => runAction(ctx, opts.json, async (out) => {
4860
+ skills.command("list").description("Search Skills, newest first.").option("-q, --query <words>", "search by title").option("--kind <kind>", `only this kind: ${SKILL_KINDS.join(", ")}`, parseKind2).option("--mine", "only your own Skills").option("-l, --limit <n>", "Skills per page (default 20, up to 100)", parsePositiveInt).option("--page <n>", "page number (default 1)", parsePositiveInt).option(...JSON_FLAG).action((opts) => runAction(ctx, opts.json, async (out) => {
4727
4861
  const client = apiClient(ctx);
4728
4862
  out.progress.start("Searching Skills…");
4729
4863
  const data = await listSkills(client, {
@@ -4788,7 +4922,7 @@ be changed later.`);
4788
4922
  if (opts.cover)
4789
4923
  prepareSkillFile(opts.cover, "cover");
4790
4924
  if (opts.preview)
4791
- prepareSkillFile(opts.preview, "preview");
4925
+ checkPreviewFile(kind, opts.preview);
4792
4926
  const pkg = skillPackage(opts);
4793
4927
  try {
4794
4928
  const slug = slugFor(pkg, opts);
@@ -4869,8 +5003,9 @@ be changed later.`);
4869
5003
  if (pkg?.name !== undefined && pkg.name !== skill.slug) {
4870
5004
  throw nameMismatch(pkg.name, skill.slug);
4871
5005
  }
5006
+ const current = contentOf(skill);
4872
5007
  const next = {
4873
- ...contentOf(skill),
5008
+ ...current,
4874
5009
  ...opts.title !== undefined ? { title: opts.title } : {},
4875
5010
  ...opts.kind !== undefined ? { kind: opts.kind } : {},
4876
5011
  ...description !== undefined ? { description } : {},
@@ -4883,6 +5018,14 @@ be changed later.`);
4883
5018
  });
4884
5019
  }
4885
5020
  checkMedia(next.kind, Boolean(opts.cover ?? next.coverFileAssetId), Boolean(opts.preview ?? next.previewFileAssetId));
5021
+ if (opts.preview) {
5022
+ checkPreviewFile(next.kind, opts.preview);
5023
+ } else if (next.previewFileAssetId && previewKindFor(current.kind) !== previewKindFor(next.kind)) {
5024
+ const wanted = previewKindFor(next.kind);
5025
+ throw new CliError("USAGE", `Changing this ${current.kind} Skill to ${next.kind} needs a new preview: the current one is ${A_FILE[previewKindFor(current.kind)]}.`, {
5026
+ hint: `Pass --preview <${wanted} file>${wanted === "image" ? ", or --clear-preview" : ""}.`
5027
+ });
5028
+ }
4886
5029
  await checkTagOptions(client, out, opts.tag);
4887
5030
  next.skillFileAssetId = await uploadIfGiven(client, out, pkg?.path, "skill") ?? next.skillFileAssetId;
4888
5031
  next.coverFileAssetId = await uploadIfGiven(client, out, opts.cover, "cover") ?? next.coverFileAssetId;
@@ -4988,8 +5131,8 @@ function withUnit(amount, unit) {
4988
5131
  return unit ? `${amount} ${unit}` : amount;
4989
5132
  }
4990
5133
  function describeLeaf(leaf) {
4991
- const text = withUnit(formatAmount(leaf.amount), unitText(leaf));
4992
- return leaf.flatFee ? `${text} + ${formatAmount(leaf.flatFee)} flat` : text;
5134
+ const text = withUnit(formatUnitPrice(leaf.amount), unitText(leaf));
5135
+ return leaf.flatFee ? `${text} + ${formatUnitPrice(leaf.flatFee)} flat` : text;
4993
5136
  }
4994
5137
  var METER_UNITS = {
4995
5138
  Calls: ["call", "calls"],
@@ -5013,8 +5156,7 @@ function meterText(meter, size = 1) {
5013
5156
  return size === 1 ? `per ${one}` : `per ${countText(size)} ${many}`;
5014
5157
  }
5015
5158
  function rateAmount(rate) {
5016
- const micros = Math.ceil(Number((rate.unitPriceUsd * 1e6).toPrecision(12)));
5017
- return formatAmount({ value: (micros / 1e6).toFixed(6), currency: "USD" });
5159
+ return formatUnitPrice({ value: rate.unitPriceUsd, currency: "USD" });
5018
5160
  }
5019
5161
  function describeRate(rate) {
5020
5162
  return `${rateAmount(rate)} ${meterText(rate.meter, rate.unitSize)}`;
@@ -5034,7 +5176,7 @@ function hasConditions(rate) {
5034
5176
  function summarizeRates(price, rates) {
5035
5177
  const only = rates.length === 1 ? rates[0] : undefined;
5036
5178
  if (only) {
5037
- const amount = price.amount ? formatAmount(price.amount) : rateAmount(only);
5179
+ const amount = price.amount ? formatUnitPrice(price.amount) : rateAmount(only);
5038
5180
  const text = `${amount} ${meterText(only.meter, only.unitSize)}`;
5039
5181
  return hasConditions(only) ? `${text} (${conditionsText(only)})` : text;
5040
5182
  }
@@ -5069,20 +5211,20 @@ function summarizePrice(price, fallback) {
5069
5211
  switch (price.type) {
5070
5212
  case "PER_UNIT_MATRIX": {
5071
5213
  const first = price.variants?.[0]?.price;
5072
- text = `from ${withUnit(formatAmount(price.amount), first ? unitText(first) : "")}`;
5214
+ text = `from ${withUnit(formatUnitPrice(price.amount), first ? unitText(first) : "")}`;
5073
5215
  break;
5074
5216
  }
5075
5217
  case "TIERED":
5076
5218
  text = price.default ? describeLeaf(price.default) : "varies";
5077
5219
  break;
5078
5220
  case "BY_PERIOD":
5079
- text = withUnit(formatAmount(price.amount), price.period ? periodText(price.period) : "");
5221
+ text = withUnit(formatUnitPrice(price.amount), price.period ? periodText(price.period) : "");
5080
5222
  break;
5081
5223
  default:
5082
- text = withUnit(formatAmount(price.amount), unitText(price));
5224
+ text = withUnit(formatUnitPrice(price.amount), unitText(price));
5083
5225
  }
5084
5226
  if (price.flatFee)
5085
- text += ` + ${formatAmount(price.flatFee)} flat`;
5227
+ text += ` + ${formatUnitPrice(price.flatFee)} flat`;
5086
5228
  if (price.type === "TIERED" && price.tiers?.length)
5087
5229
  text += " + add-ons";
5088
5230
  return text;
@@ -5307,11 +5449,15 @@ function renderInput(input, legacySchema, style) {
5307
5449
 
5308
5450
  // src/program.ts
5309
5451
  function createProgram(ctx, options = {}) {
5310
- const program = new Command().name("solgrid").description("Discover, inspect and run Solgrid tools from your terminal or agent.").version(buildInfo.version, "-v, --version", "Print the CLI version.").helpOption("-h, --help", "Show help.").configureOutput({
5452
+ const program = new Command().name("solgrid").description("Discover, inspect and run Solgrid tools from your terminal or agent.").version(buildInfo.version, "-v, --version", "Print the CLI version.").helpOption("-h, --help", "Show help.").option("--lang <code>", `language for text the Solgrid API writes, such as tag names and error messages: ${LANGUAGES.join(" or ")} (default: en; or set ${ENV.language})`).configureOutput({
5311
5453
  writeOut: (text) => ctx.stdout.write(text),
5312
5454
  writeErr: (text) => options.jsonErrors ? undefined : ctx.stderr.write(text),
5313
5455
  outputError: (text, write) => options.jsonErrors ? undefined : write(text)
5314
- }).showHelpAfterError("(run with --help for usage)").exitOverride();
5456
+ }).showHelpAfterError("(run with --help for usage)").exitOverride().hook("preAction", () => {
5457
+ const { lang } = program.opts();
5458
+ if (lang !== undefined)
5459
+ ctx.language = lang;
5460
+ });
5315
5461
  registerToolCommands(program, ctx);
5316
5462
  registerRunCommand(program, ctx);
5317
5463
  registerRunsCommand(program, ctx);
@@ -5347,16 +5493,8 @@ async function runCli(ctx, args) {
5347
5493
  return ctx.exitCode;
5348
5494
  }
5349
5495
  function wantsJson(program, args) {
5350
- let command = program;
5351
- let index = 0;
5352
- for (;index < args.length; index++) {
5353
- const arg = args[index] ?? "";
5354
- const next = command.commands.find((sub) => sub.name() === arg || sub.aliases().includes(arg));
5355
- if (!next)
5356
- break;
5357
- command = next;
5358
- }
5359
- const rest = args.slice(index);
5496
+ const { command, index, local } = resolveCommand(program, args);
5497
+ const rest = local.slice(index);
5360
5498
  if (command.commands.length > 0) {
5361
5499
  const end = rest.indexOf("--");
5362
5500
  return (end === -1 ? rest : rest.slice(0, end)).some((arg) => arg === "--json" || /^-[A-Za-z]*j[A-Za-z]*$/.test(arg));
@@ -5394,14 +5532,39 @@ function wantsJson(program, args) {
5394
5532
  return false;
5395
5533
  }
5396
5534
  function withImplicitHelp(program, args) {
5535
+ const { command, index, local } = resolveCommand(program, args);
5536
+ if (index < local.length)
5537
+ return args;
5538
+ return command.commands.length > 0 ? [...args, "--help"] : args;
5539
+ }
5540
+ function resolveCommand(program, args) {
5541
+ const local = [];
5542
+ for (let i = 0;i < args.length; i++) {
5543
+ const arg = args[i] ?? "";
5544
+ if (arg === "--") {
5545
+ local.push(...args.slice(i));
5546
+ break;
5547
+ }
5548
+ const global = program.options.find((o) => o.long === arg || o.short === arg);
5549
+ if (global) {
5550
+ if (global.required)
5551
+ i++;
5552
+ continue;
5553
+ }
5554
+ if (program.options.some((o) => o.long !== undefined && arg.startsWith(`${o.long}=`)))
5555
+ continue;
5556
+ local.push(arg);
5557
+ }
5397
5558
  let command = program;
5398
- for (const arg of args) {
5559
+ let index = 0;
5560
+ for (;index < local.length; index++) {
5561
+ const arg = local[index] ?? "";
5399
5562
  const next = command.commands.find((sub) => sub.name() === arg || sub.aliases().includes(arg));
5400
5563
  if (!next)
5401
- return args;
5564
+ break;
5402
5565
  command = next;
5403
5566
  }
5404
- return command.commands.length > 0 ? [...args, "--help"] : args;
5567
+ return { command, index, local };
5405
5568
  }
5406
5569
 
5407
5570
  // src/index.ts
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.2",
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
@@ -206,7 +215,7 @@ Use these only when the user asks to publish, change or remove *their* Skill on
206
215
  SkillHub; finding and downloading Skills is always fine.
207
216
 
208
217
  ```bash
209
- solgrid skills list -q "<words>" --json # search titles and tags
218
+ solgrid skills list -q "<words>" --json # search by title
210
219
  solgrid skills get -s <slug> --json # details
211
220
  solgrid skills download -s <slug> -o <path> # the Skill package (.zip)
212
221
  ```
@@ -239,11 +248,17 @@ solgrid skills create -t "<title>" --kind <image|video|web|audio> \
239
248
  --tag <tag-id> --yes --json
240
249
  ```
241
250
 
251
+ Tag names come in English. To show them to a user in Chinese, add the global
252
+ `--lang zh` (e.g. `solgrid skills tags --lang zh --json`); it also turns the
253
+ server's error messages into Chinese. Always pass tag **IDs** to `--tag`.
254
+
242
255
  - A Skill is **public as soon as it is created**, and its slug can never be
243
256
  changed. Confirm the title, slug (the `name` in `SKILL.md`) and files with
244
257
  the user before passing `--yes`.
245
258
  - `--kind` is what the Skill produces. Every kind except `video` needs
246
- `--cover`; `video` and `audio` need `--preview`.
259
+ `--cover` (an image). The `--preview` type follows the kind: an image for
260
+ `image` and `web` (optional), a video for `video`, an audio file for `audio`
261
+ (both required).
247
262
  - `-d` is a short plain-text line shown under the title; the Skill's real
248
263
  instructions belong in `SKILL.md`.
249
264
  - If the CLI refuses the directory, it says which rule it breaks (for example