@solgridai/cli 0.2.0-beta.1 → 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
@@ -58,7 +58,7 @@ solgrid run -p <provider> -e <endpoint> -i '{"screen_name":"nasa"}' --wait
58
58
  | `files get -i <fileId>` | 查看文件的类型、大小和状态 |
59
59
  | `files url -i <fileId>` | 为已上传的文件重新签发 24 小时有效的 URL |
60
60
  | `files download -i <fileId> [-o path] [--force]` | 通过签名 URL 下载文件到本地 |
61
- | `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` 按标题搜索) |
62
62
  | `skills get (-s slug \| -i id)` | 查看 Skill 详情 |
63
63
  | `skills tags` | 列出 Skill 可用的标签 |
64
64
  | `skills create [-s slug] -t <title> --kind <k> -d <text> (--dir <dir> \| --zip <file>) [--cover f] [--preview f] [--tag id]… [-y]` | 发布 Skill,创建后立即公开 |
@@ -102,8 +102,8 @@ solgrid skills create -t "Mecha portrait" --kind image \
102
102
 
103
103
  用 `--dir` 时 slug 默认取 `name`,传了 `-s` 就必须和 `name` 一致;用 `--zip` 时必须传 `-s`,由服务端核对。`skills update` 换包时,新包的 `name` 也必须等于原来的 slug。
104
104
  - `-d` 是显示在标题下的简短描述(纯文本,不按 Markdown 渲染)。Skill 的详细说明请写在 `SKILL.md` 里。
105
- - `--kind` 表示 Skill 产出的作品类型:`image`、`video`、`web`、`audio`。除视频外都需要封面(`--cover`);视频和音频需要预览(`--preview`,图片、视频或音频)。
106
- - `--tag` 填 `solgrid skills tags` 列出的标签 ID,最多 3 个。
105
+ - `--kind` 表示 Skill 产出的作品类型:`image`、`video`、`web`、`audio`。除视频外都需要封面(`--cover`,图片)。预览(`--preview`)的类型由 kind 决定:图片和网页类可选,只能是图片;视频类必填,只能是视频;音频类必填,只能是音频。
106
+ - `--tag` 填 `solgrid skills tags` 列出的标签 ID,最多 3 个,不能重复。
107
107
  - `create`、`delete` 会先请你确认;在脚本或 Agent 中使用时加 `-y`。
108
108
  - `update` 只修改传入的选项;`--tag` 会替换全部标签,`--clear-tags` 清空标签,`--clear-preview` 去掉预览(仅图片和网页类 Skill)。只能修改、删除自己的 Skill。
109
109
 
@@ -132,6 +132,7 @@ solgrid run -p <provider> -e <endpoint> -i '<同样的输入>' --idempotency-key
132
132
  | `~/.config/solgrid/cli/config.json` | 当前激活的 key |
133
133
  | `SOLGRID_API_KEY` | 设置后优先于本机保存的 key,适合 CI 和 Agent |
134
134
  | `SOLGRID_API_BASE_URL` | 覆盖 API 地址,默认为 `https://api.solgrid.ai` |
135
+ | `SOLGRID_LANG` / `--lang` | 服务端返回文本(标签名、错误信息)的语言:`en`(默认)或 `zh`;`--lang` 是全局选项,优先于环境变量。标签 ID、错误键和 CLI 自身的提示不变 |
135
136
  | `NO_COLOR` / `FORCE_COLOR` | 关闭或强制彩色输出 |
136
137
 
137
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.1" };
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 {
@@ -2344,8 +2344,10 @@ var CONSOLE_URLS = {
2344
2344
  };
2345
2345
  var ENV = {
2346
2346
  apiKey: "SOLGRID_API_KEY",
2347
- apiBaseUrl: "SOLGRID_API_BASE_URL"
2347
+ apiBaseUrl: "SOLGRID_API_BASE_URL",
2348
+ language: "SOLGRID_LANG"
2348
2349
  };
2350
+ var LANGUAGES = ["en", "zh"];
2349
2351
 
2350
2352
  // src/core/config.ts
2351
2353
  function resolveConfigDir(env, homeDir) {
@@ -2591,6 +2593,9 @@ class ApiClient {
2591
2593
  this.options = options;
2592
2594
  this.baseUrl = options.baseUrl.replace(/\/+$/, "");
2593
2595
  }
2596
+ languageHeader() {
2597
+ return this.options.language ? { "Accept-Language": this.options.language } : {};
2598
+ }
2594
2599
  post(path, body = {}, headers = {}, request = {}) {
2595
2600
  return this.send("POST", path, {
2596
2601
  body: JSON.stringify(body),
@@ -2632,6 +2637,7 @@ class ApiClient {
2632
2637
  method: "GET",
2633
2638
  headers: {
2634
2639
  "X-Solgrid-Client": this.options.clientId,
2640
+ ...this.languageHeader(),
2635
2641
  ...this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {}
2636
2642
  },
2637
2643
  signal: AbortSignal.timeout(TRANSFER_TIMEOUT_MS)
@@ -2658,6 +2664,7 @@ class ApiClient {
2658
2664
  headers: {
2659
2665
  Accept: "application/json",
2660
2666
  "X-Solgrid-Client": this.options.clientId,
2667
+ ...this.languageHeader(),
2661
2668
  ...this.options.apiKey ? { Authorization: `Bearer ${this.options.apiKey}` } : {},
2662
2669
  ...options.headers
2663
2670
  },
@@ -3119,13 +3126,29 @@ function apiClient(ctx) {
3119
3126
  });
3120
3127
  }
3121
3128
  sentKeys.set(ctx, resolved);
3129
+ const language = requestLanguage(ctx);
3122
3130
  return new ApiClient({
3123
3131
  baseUrl: ctx.env[ENV.apiBaseUrl]?.trim() || DEFAULT_API_BASE_URL,
3124
3132
  apiKey: resolved.key,
3125
3133
  clientId: `cli@${buildInfo.version}`,
3134
+ ...language ? { language } : {},
3126
3135
  fetch: ctx.fetch
3127
3136
  });
3128
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
+ }
3129
3152
  function withAuthContext(ctx, error) {
3130
3153
  const key = sentKeys.get(ctx);
3131
3154
  if (!(error instanceof ApiError) || error.code !== "AUTH_FAILED" || !key)
@@ -4158,6 +4181,7 @@ function registerSessionIdCommand(program, ctx) {
4158
4181
 
4159
4182
  // src/commands/skills.ts
4160
4183
  import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
4184
+ import { basename as basename4 } from "node:path";
4161
4185
 
4162
4186
  // src/core/skill-markdown.ts
4163
4187
  var MAX_SKILL_NAME_LENGTH = 64;
@@ -4609,6 +4633,10 @@ async function listSkillTags(client) {
4609
4633
  return checked.tags;
4610
4634
  }
4611
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
+ }
4612
4640
  if (tags.length > MAX_TAGS) {
4613
4641
  throw new CliError("USAGE", `A Skill takes at most ${MAX_TAGS} tags; ${tags.length} were given.`);
4614
4642
  }
@@ -4620,6 +4648,9 @@ function checkTags(tags, available) {
4620
4648
  });
4621
4649
  }
4622
4650
  }
4651
+ function previewKindFor(kind) {
4652
+ return kind === "video" ? "video" : kind === "audio" ? "audio" : "image";
4653
+ }
4623
4654
  var PUBLIC_DISPLAY = "1";
4624
4655
  function prepareSkillFile(path, role) {
4625
4656
  let size;
@@ -4693,7 +4724,7 @@ function collect2(value, previous = []) {
4693
4724
  }
4694
4725
  function addContentOptions(command, required) {
4695
4726
  const note = required ? "" : " (replaces the current one)";
4696
- 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}`);
4697
4728
  return command;
4698
4729
  }
4699
4730
  function readDescription(opts) {
@@ -4719,7 +4750,22 @@ function checkMedia(kind, hasCover, hasPreview) {
4719
4750
  }
4720
4751
  if ((kind === "video" || kind === "audio") && !hasPreview) {
4721
4752
  throw new CliError("USAGE", `${aSkill(kind)} needs a preview.`, {
4722
- 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."
4723
4769
  });
4724
4770
  }
4725
4771
  }
@@ -4811,7 +4857,7 @@ function registerSkillsCommand(program, ctx) {
4811
4857
  const skills = program.command("skills").description("Find, publish and manage Skills on SkillHub.").addHelpText("after", `
4812
4858
  A Skill is public as soon as it is created. Its slug identifies it everywhere and cannot
4813
4859
  be changed later.`);
4814
- 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) => {
4815
4861
  const client = apiClient(ctx);
4816
4862
  out.progress.start("Searching Skills…");
4817
4863
  const data = await listSkills(client, {
@@ -4876,7 +4922,7 @@ be changed later.`);
4876
4922
  if (opts.cover)
4877
4923
  prepareSkillFile(opts.cover, "cover");
4878
4924
  if (opts.preview)
4879
- prepareSkillFile(opts.preview, "preview");
4925
+ checkPreviewFile(kind, opts.preview);
4880
4926
  const pkg = skillPackage(opts);
4881
4927
  try {
4882
4928
  const slug = slugFor(pkg, opts);
@@ -4957,8 +5003,9 @@ be changed later.`);
4957
5003
  if (pkg?.name !== undefined && pkg.name !== skill.slug) {
4958
5004
  throw nameMismatch(pkg.name, skill.slug);
4959
5005
  }
5006
+ const current = contentOf(skill);
4960
5007
  const next = {
4961
- ...contentOf(skill),
5008
+ ...current,
4962
5009
  ...opts.title !== undefined ? { title: opts.title } : {},
4963
5010
  ...opts.kind !== undefined ? { kind: opts.kind } : {},
4964
5011
  ...description !== undefined ? { description } : {},
@@ -4971,6 +5018,14 @@ be changed later.`);
4971
5018
  });
4972
5019
  }
4973
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
+ }
4974
5029
  await checkTagOptions(client, out, opts.tag);
4975
5030
  next.skillFileAssetId = await uploadIfGiven(client, out, pkg?.path, "skill") ?? next.skillFileAssetId;
4976
5031
  next.coverFileAssetId = await uploadIfGiven(client, out, opts.cover, "cover") ?? next.coverFileAssetId;
@@ -5394,11 +5449,15 @@ function renderInput(input, legacySchema, style) {
5394
5449
 
5395
5450
  // src/program.ts
5396
5451
  function createProgram(ctx, options = {}) {
5397
- 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({
5398
5453
  writeOut: (text) => ctx.stdout.write(text),
5399
5454
  writeErr: (text) => options.jsonErrors ? undefined : ctx.stderr.write(text),
5400
5455
  outputError: (text, write) => options.jsonErrors ? undefined : write(text)
5401
- }).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
+ });
5402
5461
  registerToolCommands(program, ctx);
5403
5462
  registerRunCommand(program, ctx);
5404
5463
  registerRunsCommand(program, ctx);
@@ -5434,16 +5493,8 @@ async function runCli(ctx, args) {
5434
5493
  return ctx.exitCode;
5435
5494
  }
5436
5495
  function wantsJson(program, args) {
5437
- let command = program;
5438
- let index = 0;
5439
- for (;index < args.length; index++) {
5440
- const arg = args[index] ?? "";
5441
- const next = command.commands.find((sub) => sub.name() === arg || sub.aliases().includes(arg));
5442
- if (!next)
5443
- break;
5444
- command = next;
5445
- }
5446
- const rest = args.slice(index);
5496
+ const { command, index, local } = resolveCommand(program, args);
5497
+ const rest = local.slice(index);
5447
5498
  if (command.commands.length > 0) {
5448
5499
  const end = rest.indexOf("--");
5449
5500
  return (end === -1 ? rest : rest.slice(0, end)).some((arg) => arg === "--json" || /^-[A-Za-z]*j[A-Za-z]*$/.test(arg));
@@ -5481,14 +5532,39 @@ function wantsJson(program, args) {
5481
5532
  return false;
5482
5533
  }
5483
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
+ }
5484
5558
  let command = program;
5485
- for (const arg of args) {
5559
+ let index = 0;
5560
+ for (;index < local.length; index++) {
5561
+ const arg = local[index] ?? "";
5486
5562
  const next = command.commands.find((sub) => sub.name() === arg || sub.aliases().includes(arg));
5487
5563
  if (!next)
5488
- return args;
5564
+ break;
5489
5565
  command = next;
5490
5566
  }
5491
- return command.commands.length > 0 ? [...args, "--help"] : args;
5567
+ return { command, index, local };
5492
5568
  }
5493
5569
 
5494
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.1",
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": {
@@ -215,7 +215,7 @@ Use these only when the user asks to publish, change or remove *their* Skill on
215
215
  SkillHub; finding and downloading Skills is always fine.
216
216
 
217
217
  ```bash
218
- solgrid skills list -q "<words>" --json # search titles and tags
218
+ solgrid skills list -q "<words>" --json # search by title
219
219
  solgrid skills get -s <slug> --json # details
220
220
  solgrid skills download -s <slug> -o <path> # the Skill package (.zip)
221
221
  ```
@@ -248,11 +248,17 @@ solgrid skills create -t "<title>" --kind <image|video|web|audio> \
248
248
  --tag <tag-id> --yes --json
249
249
  ```
250
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
+
251
255
  - A Skill is **public as soon as it is created**, and its slug can never be
252
256
  changed. Confirm the title, slug (the `name` in `SKILL.md`) and files with
253
257
  the user before passing `--yes`.
254
258
  - `--kind` is what the Skill produces. Every kind except `video` needs
255
- `--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).
256
262
  - `-d` is a short plain-text line shown under the title; the Skill's real
257
263
  instructions belong in `SKILL.md`.
258
264
  - If the CLI refuses the directory, it says which rule it breaks (for example