@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 +17 -3
- package/dist/index.js +196 -33
- package/package.json +1 -1
- package/skills/solgrid-cli/SKILL.md +18 -3
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
|
|
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.
|
|
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
|
|
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
|
|
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}
|
|
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>", "
|
|
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
|
-
|
|
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
|
-
...
|
|
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(
|
|
4992
|
-
return leaf.flatFee ? `${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
|
-
|
|
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 ?
|
|
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(
|
|
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(
|
|
5221
|
+
text = withUnit(formatUnitPrice(price.amount), price.period ? periodText(price.period) : "");
|
|
5080
5222
|
break;
|
|
5081
5223
|
default:
|
|
5082
|
-
text = withUnit(
|
|
5224
|
+
text = withUnit(formatUnitPrice(price.amount), unitText(price));
|
|
5083
5225
|
}
|
|
5084
5226
|
if (price.flatFee)
|
|
5085
|
-
text += ` + ${
|
|
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
|
-
|
|
5351
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5564
|
+
break;
|
|
5402
5565
|
command = next;
|
|
5403
5566
|
}
|
|
5404
|
-
return command
|
|
5567
|
+
return { command, index, local };
|
|
5405
5568
|
}
|
|
5406
5569
|
|
|
5407
5570
|
// src/index.ts
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|