agentsw 0.9.2 → 0.10.0

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/dist/discover.js CHANGED
@@ -4,10 +4,15 @@ function modelListUrl(baseUrl) {
4
4
  if (url.protocol !== "https:" && url.protocol !== "http:")
5
5
  throw new Error("provider URL must use HTTP or HTTPS");
6
6
  const pathname = url.pathname.replace(/\/+$/, "");
7
- url.pathname = /\/v\d+(?:beta\d*)?$/.test(pathname) ? `${pathname}/models` : `${pathname}/v1/models`;
7
+ url.pathname =
8
+ /\/v\d+(?:beta\d*)?$/.test(pathname) ? `${pathname}/models`
9
+ : /\/models$/.test(pathname) ? pathname
10
+ : `${pathname}/v1/models`;
8
11
  url.hash = "";
9
12
  return url;
10
13
  }
14
+ /** Hard cap on model-list pages; an endpoint emitting endless fresh cursors must not loop forever. */
15
+ export const MAX_MODEL_PAGES = 100;
11
16
  function parseModelPage(body) {
12
17
  if (!body || typeof body !== "object")
13
18
  throw new Error("invalid model-list response");
@@ -30,7 +35,7 @@ function parseModelPage(body) {
30
35
  * Resellers expose the id list here but no metadata (context, thinking, modalities) —
31
36
  * metadata is filled from models.dev afterwards (enrichModels).
32
37
  */
33
- export async function discoverProviderModels(opts) {
38
+ export async function discoverProviderModels(opts, hooks = {}) {
34
39
  const url = modelListUrl(opts.baseUrl);
35
40
  const headers = opts.protocol === "anthropic"
36
41
  ? { "x-api-key": opts.apiKey, "anthropic-version": "2023-06-01" }
@@ -40,21 +45,25 @@ export async function discoverProviderModels(opts) {
40
45
  const initialCursor = url.searchParams.get("after_id");
41
46
  if (initialCursor)
42
47
  cursors.add(initialCursor);
43
- while (true) {
48
+ for (let page = 1;; page++) {
49
+ if (page > MAX_MODEL_PAGES) {
50
+ throw new Error(`model-list pagination exceeded ${MAX_MODEL_PAGES} pages; the endpoint keeps reporting more models`);
51
+ }
44
52
  const res = await fetch(url.toString(), { headers, signal: AbortSignal.timeout(20000) });
45
53
  if (!res.ok)
46
54
  throw new Error(`model discovery -> HTTP ${res.status}`);
47
- const page = parseModelPage(await res.json());
48
- for (const id of page.ids)
55
+ const parsed = parseModelPage(await res.json());
56
+ for (const id of parsed.ids)
49
57
  ids.add(id);
50
- if (!page.hasMore)
58
+ hooks.onPage?.({ page, count: ids.size });
59
+ if (!parsed.hasMore)
51
60
  break;
52
- if (!page.lastId || !page.lastId.trim())
61
+ if (!parsed.lastId || !parsed.lastId.trim())
53
62
  throw new Error("model-list pagination is missing last_id");
54
- if (cursors.has(page.lastId))
63
+ if (cursors.has(parsed.lastId))
55
64
  throw new Error("model-list pagination repeated a cursor");
56
- cursors.add(page.lastId);
57
- url.searchParams.set("after_id", page.lastId);
65
+ cursors.add(parsed.lastId);
66
+ url.searchParams.set("after_id", parsed.lastId);
58
67
  }
59
68
  if (ids.size === 0)
60
69
  throw new Error("provider returned no models");
@@ -65,7 +74,7 @@ export async function discoverProviderModels(opts) {
65
74
  * with each protocol's auth headers. Returns the protocols that responded
66
75
  * with a valid model list (at least one model id).
67
76
  */
68
- export async function probeProtocols(opts) {
77
+ export async function probeProtocols(opts, hooks = {}) {
69
78
  const url = modelListUrl(opts.baseUrl).toString();
70
79
  const attempts = [
71
80
  { protocol: "openai", headers: { authorization: `Bearer ${opts.apiKey}` } },
@@ -73,6 +82,7 @@ export async function probeProtocols(opts) {
73
82
  ];
74
83
  const supported = [];
75
84
  for (const a of attempts) {
85
+ hooks.onAttempt?.(a.protocol);
76
86
  try {
77
87
  const res = await fetch(url, { headers: a.headers, signal: AbortSignal.timeout(15000) });
78
88
  if (!res.ok)
package/dist/gateway.js CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { readJsonIfExists, writeFileAtomic } from "./fsutil.js";
4
4
  import { configDir } from "./store.js";
5
+ import { warn } from "./ui.js";
5
6
  const API_URL = "https://ai-gateway.vercel.sh/v1/models";
6
7
  const CACHE_FILE = path.join(configDir, "ai-gateway.json");
7
8
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
@@ -188,14 +189,14 @@ export async function loadGatewayCatalog(opts = {}) {
188
189
  writeFileAtomic(CACHE_FILE, JSON.stringify({ version: 1, fetchedAt: catalog.fetchedAt, body }) + "\n", 0o600);
189
190
  }
190
191
  catch {
191
- process.stderr.write("warning: could not cache AI Gateway metadata\n");
192
+ warn("could not cache AI Gateway metadata");
192
193
  }
193
194
  return catalog;
194
195
  }
195
196
  catch {
196
- process.stderr.write(cached
197
- ? "warning: could not refresh AI Gateway metadata; using cached metadata\n"
198
- : "warning: could not load AI Gateway metadata\n");
197
+ warn(cached
198
+ ? "could not refresh AI Gateway metadata; using cached metadata"
199
+ : "could not load AI Gateway metadata");
199
200
  return cached;
200
201
  }
201
202
  }
package/dist/i18n.js CHANGED
@@ -6,6 +6,7 @@ const messages = {
6
6
  "help.language": { en: "UI language: en | zh-CN (also AGENTSW_LANG)", "zh-CN": "界面语言: en | zh-CN (也可用 AGENTSW_LANG)" },
7
7
  "error.language": { en: "unsupported language \"{value}\"; use en or zh-CN", "zh-CN": "不支持语言 \"{value}\";请使用 en 或 zh-CN" },
8
8
  "error.unknownCommand": { en: "unknown command '{value}', see --help", "zh-CN": "未知命令 '{value}',请查看 --help" },
9
+ "error.limit": { en: "--limit must be a positive integer", "zh-CN": "--limit 必须是正整数" },
9
10
  "cmd.add": { en: "add or update a provider (interactive when flags are omitted)", "zh-CN": "添加或更新供应商(省略参数时进入交互流程)" },
10
11
  "cmd.quick": { en: "quick add — auto-detect protocol(s) and models from just base URL + API key", "zh-CN": "快速添加 — 只需 base URL + API key,自动探测协议和模型" },
11
12
  "cmd.list": { en: "list configured providers (or agent-local entries with --apps)", "zh-CN": "列出已配置供应商(--apps 可查看智能体独有配置)" },
@@ -130,8 +131,6 @@ const messages = {
130
131
  "menu.discoverFor": { en: "which provider's model list should be updated?", "zh-CN": "选择要更新模型列表的供应商" },
131
132
  "menu.pushRefresh": { en: "also sync this provider to agent configs after updating its model list?", "zh-CN": "更新模型列表后,是否同时将该供应商同步到智能体配置?" },
132
133
  "menu.removeProvider": { en: "which provider configuration should be deleted?", "zh-CN": "选择要删除配置的供应商" },
133
- "menu.reallyRemove": { en: "really remove {id}?", "zh-CN": "确认删除 {id}?" },
134
- "menu.pruneConfigs": { en: "also remove its entries from app configs?", "zh-CN": "同时从应用配置中移除其条目?" },
135
134
  "menu.upgrade": { en: "upgrade all installed agents that have updates?", "zh-CN": "是否升级所有有新版本的已安装智能体?" },
136
135
  "menu.installApp": { en: "Install a new coding agent", "zh-CN": "安装新的智能体" },
137
136
  "menu.pickApp": { en: "which agent to install?", "zh-CN": "安装哪个智能体?" },
@@ -149,7 +148,6 @@ const messages = {
149
148
  "add.baseUrlInvalid": { en: "must start with http(s)://", "zh-CN": "必须以 http(s):// 开头" },
150
149
  "add.apiKey": { en: "API key", "zh-CN": "API 密钥(API key)" },
151
150
  "add.models": { en: "model ids (comma separated, or leave blank to auto-discover)", "zh-CN": "模型 id(逗号分隔,留空则自动发现)" },
152
- "add.cancelled": { en: "cancelled", "zh-CN": "已取消" },
153
151
  "import.already": { en: "already configured as {id}", "zh-CN": "已配置为 {id}" },
154
152
  "import.noneNew": { en: "nothing new to import (every discovered provider is already configured)", "zh-CN": "没有可导入的新供应商(发现项均已配置)" },
155
153
  "import.noneFound": { en: "no custom providers found in supported app configs or in cc-switch", "zh-CN": "支持的应用配置与 cc-switch 中均未发现自定义供应商" },
@@ -258,6 +256,152 @@ const messages = {
258
256
  },
259
257
  "refresh.checked": { en: "checked model metadata ({changed} provider(s) changed)", "zh-CN": "已检查模型元数据({changed} 个供应商有变化)" },
260
258
  "refresh.next": { en: "run `agentsw sync` to push updated metadata into app configs", "zh-CN": "运行 `agentsw sync` 将更新的元数据写入各智能体配置" },
259
+ "opt.json": {
260
+ en: "machine-readable JSON on stdout; disables prompts and progress",
261
+ "zh-CN": "stdout 输出机器可读 JSON(同时关闭交互提问与进度)",
262
+ },
263
+ "opt.quiet": {
264
+ en: "suppress progress, hints and warnings (keep results and errors)",
265
+ "zh-CN": "抑制进度、提示与警告(保留结果与错误)",
266
+ },
267
+ "opt.noColor": { en: "disable colored output (NO_COLOR also works)", "zh-CN": "关闭彩色输出(也支持 NO_COLOR)" },
268
+ "error.jsonRequiresCommand": {
269
+ en: "--json requires an explicit command (for example: agentsw status --json)",
270
+ "zh-CN": "--json 需要指定命令(例如 agentsw status --json)",
271
+ },
272
+ "common.cancelled": { en: "cancelled", "zh-CN": "已取消" },
273
+ "menu.cancelledAction": { en: "cancelled — back to menu", "zh-CN": "已取消 —— 返回菜单" },
274
+ "menu.back": { en: "← Back to main menu", "zh-CN": "← 返回主菜单" },
275
+ "progress.probe": { en: "probing {url} ({protocol}) …", "zh-CN": "正在探测 {url}({protocol})…" },
276
+ "progress.discoverPage": {
277
+ en: "listing models from {url} — page {page}, {count} models",
278
+ "zh-CN": "正在从 {url} 获取模型列表 —— 第 {page} 页,{count} 个模型",
279
+ },
280
+ "progress.catalog": { en: "loading models.dev catalog …", "zh-CN": "正在加载 models.dev 目录…" },
281
+ "progress.gateway": { en: "loading AI Gateway catalog …", "zh-CN": "正在加载 AI Gateway 目录…" },
282
+ "progress.metadata": { en: "supplementing model metadata …", "zh-CN": "正在补全模型元数据…" },
283
+ "progress.apps": {
284
+ en: "checking installed and latest versions … ({done}/{total})",
285
+ "zh-CN": "正在检查已安装版本与最新版本…({done}/{total})",
286
+ },
287
+ "progress.refresh": { en: "refreshing model metadata …", "zh-CN": "正在刷新模型元数据…" },
288
+ "add.retryDiscovery": {
289
+ en: "model discovery failed: {error}. Try again with the same endpoint?",
290
+ "zh-CN": "模型发现失败:{error}。用同一端点重试吗?",
291
+ },
292
+ "quick.retryProbe": {
293
+ en: "no protocol detected at {url}. Re-enter the base URL and API key?",
294
+ "zh-CN": "{url} 未探测到可用协议。重新输入 base URL 与 API key?",
295
+ },
296
+ "import.retryDiscovery": {
297
+ en: "discovery failed for {id}: {error}. Retry discovery for this provider?",
298
+ "zh-CN": "{id} 的模型发现失败:{error}。为该供应商重试?",
299
+ },
300
+ "import.skippedAfterFailure": {
301
+ en: "skipped {id}: discovery failed; add it later with agentsw add",
302
+ "zh-CN": "已跳过 {id}:发现失败;之后可用 agentsw add 补上",
303
+ },
304
+ "models.catalogUnavailable": {
305
+ en: "models.dev catalog unavailable (offline and no cache)\nrun: agentsw models --refresh (or check your network)",
306
+ "zh-CN": "models.dev 目录不可用(离线且无缓存)\n请运行 agentsw models --refresh(或检查网络)",
307
+ },
308
+ "models.usage": {
309
+ en: "usage: agentsw models <query> | agentsw models --provider <id>",
310
+ "zh-CN": "用法: agentsw models <查询> | agentsw models --provider <id>",
311
+ },
312
+ "models.noMatch": {
313
+ en: "no models.dev entries match \"{query}\"\nrun: agentsw models --refresh to update the catalog",
314
+ "zh-CN": "models.dev 中没有匹配 \"{query}\" 的条目\n请运行 agentsw models --refresh 更新目录",
315
+ },
316
+ "models.metadataRequiresProvider": {
317
+ en: "--metadata requires --provider (run: agentsw models --provider <id> --metadata)",
318
+ "zh-CN": "--metadata 需要配合 --provider(请运行 agentsw models --provider <id> --metadata)",
319
+ },
320
+ "status.noProviders": {
321
+ en: "no providers saved yet\nrun: agentsw add — or agentsw import to pull providers from your agents",
322
+ "zh-CN": "尚未保存任何供应商\n请运行 agentsw add,或用 agentsw import 从各智能体导入",
323
+ },
324
+ "apps.checking": { en: "checking installed and latest versions ...", "zh-CN": "正在检查已安装版本与最新版本..." },
325
+ "apps.notInstalled": { en: "not installed", "zh-CN": "未安装" },
326
+ "apps.installable": { en: "installable", "zh-CN": "可安装" },
327
+ "apps.upToDate": { en: "up to date", "zh-CN": "已是最新" },
328
+ "apps.upgradeAvailable": { en: "upgrade available", "zh-CN": "可升级" },
329
+ "apps.unknown": { en: "unknown", "zh-CN": "未知" },
330
+ "apps.checkFailedProbe": { en: "installed version check failed", "zh-CN": "已安装版本检查失败" },
331
+ "apps.checkFailedUnknown": { en: "installed version unknown", "zh-CN": "已安装版本未知" },
332
+ "apps.checkFailedLatest": { en: "latest version unavailable", "zh-CN": "无法获取最新版本" },
333
+ "apps.upgradeWith": { en: "upgrade with: agentsw upgrade {ids}", "zh-CN": "升级命令: agentsw upgrade {ids}" },
334
+ "install.unknownApp": { en: "unknown app \"{value}\" (supported: {apps})", "zh-CN": "未知应用 \"{value}\"(支持:{apps})" },
335
+ "install.notInstallable": {
336
+ en: "{name} is not installable on {platform} (or is managed by its desktop app)",
337
+ "zh-CN": "{name} 无法在 {platform} 上安装(或由桌面应用自行管理)",
338
+ },
339
+ "install.already": {
340
+ en: "{name} already installed ({version}); use `agentsw upgrade {id}`",
341
+ "zh-CN": "{name} 已安装({version});可用 `agentsw upgrade {id}`",
342
+ },
343
+ "install.installing": { en: "installing {name}: {command}", "zh-CN": "正在安装 {name}:{command}" },
344
+ "install.notDetected": {
345
+ en: "{name}: installer completed but the app is still not detected; check the installation and PATH",
346
+ "zh-CN": "{name}:安装命令已结束但仍未检测到该应用;请检查安装与 PATH",
347
+ },
348
+ "install.versionUnknown": {
349
+ en: "{name}: installer completed; app detected but version unknown",
350
+ "zh-CN": "{name}:安装命令已结束;检测到应用但版本未知",
351
+ },
352
+ "install.installed": { en: "{name} installed: {version}", "zh-CN": "{name} 已安装:{version}" },
353
+ "upgrade.unknownApps": { en: "unknown app(s): {ids}", "zh-CN": "未知应用:{ids}" },
354
+ "upgrade.checking": { en: "checking versions ...", "zh-CN": "正在检查版本..." },
355
+ "upgrade.statusUnknown": { en: "{id}: {reason}; update status unknown", "zh-CN": "{id}:{reason};升级状态未知" },
356
+ "upgrade.cannotDetermine": {
357
+ en: "could not determine update status for every installed app",
358
+ "zh-CN": "无法确定所有已安装应用的升级状态",
359
+ },
360
+ "upgrade.allCurrent": { en: "all checked apps are up to date", "zh-CN": "已检查的应用均为最新" },
361
+ "upgrade.none": { en: "no installed CLI-managed apps to upgrade", "zh-CN": "没有可升级的、由 CLI 管理的应用" },
362
+ "upgrade.notCli": { en: "{id}: not CLI-upgradable", "zh-CN": "{id}:不支持通过 CLI 升级" },
363
+ "upgrade.notInstalled": {
364
+ en: "{id}: not installed (use `agentsw install {id}`)",
365
+ "zh-CN": "{id}:未安装(请使用 `agentsw install {id}`)",
366
+ },
367
+ "upgrade.upgrading": { en: "upgrading {name}: {command}", "zh-CN": "正在升级 {name}:{command}" },
368
+ "upgrade.unknownVersion": {
369
+ en: "upgrade command completed but installed version is unknown or app is not detected",
370
+ "zh-CN": "升级命令已结束,但已安装版本未知或未检测到应用",
371
+ },
372
+ "upgrade.olderThanAvailable": {
373
+ en: "upgrade command completed but {version} is older than available {expected}",
374
+ "zh-CN": "升级命令已结束,但 {version} 低于可用的 {expected}",
375
+ },
376
+ "upgrade.done": { en: "{id} -> {version}", "zh-CN": "{id} -> {version}" },
377
+ "upgrade.failed": { en: "{id} upgrade failed: {message}", "zh-CN": "{id} 升级失败:{message}" },
378
+ "discover.defaultMissing": {
379
+ en: "default model {model} no longer listed; keeping it anyway",
380
+ "zh-CN": "默认模型 {model} 已不在列表中;仍然保留",
381
+ },
382
+ "discover.next": {
383
+ en: "run `agentsw sync` to push into app configs",
384
+ "zh-CN": "运行 `agentsw sync` 写入各智能体配置",
385
+ },
386
+ "dryRun.skip": { en: "skip", "zh-CN": "跳过" },
387
+ "dryRun.wouldWrite": {
388
+ en: "dry run — {count} file(s) would be written:",
389
+ "zh-CN": "预演 — 将写入 {count} 个文件:",
390
+ },
391
+ "preview.unchanged": { en: "unchanged", "zh-CN": "未变化" },
392
+ "preview.header": { en: "--- {file} (redacted configuration)", "zh-CN": "--- {file}(已脱敏配置)" },
393
+ "preview.contentWithheld": {
394
+ en: "[content withheld: unsupported or malformed configuration]",
395
+ "zh-CN": "[内容已隐去:配置格式不支持或无法解析]",
396
+ },
397
+ "preview.onlyRedacted": {
398
+ en: "[only redacted values or formatting changed]",
399
+ "zh-CN": "[仅脱敏值或格式发生变化]",
400
+ },
401
+ "preview.failedSafely": {
402
+ en: "configuration could not be previewed safely",
403
+ "zh-CN": "无法安全预览配置",
404
+ },
261
405
  };
262
406
  let current = detectSystemLocale();
263
407
  export function normalizeLocale(value) {
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { detectSystemLocale, extractCliLocale, normalizeLocale, setLocale, t } f
6
6
  import { cmdMenu } from "./menu.js";
7
7
  import { loadStore } from "./store.js";
8
8
  import { cmdListLocalProviders, cmdRemoveProvider, cmdRename } from "./provider-actions.js";
9
+ import { configureOutput, error, isJson, setCommandName } from "./ui.js";
9
10
  // Locale lookup is best effort: help/version and app-local commands do not need the store.
10
11
  function savedLocale() {
11
12
  try {
@@ -31,7 +32,16 @@ program
31
32
  .name("agentsw")
32
33
  .description(t("root.description"))
33
34
  .option("--lang <locale>", t("help.language"), parseLocale)
35
+ .option("--json", t("opt.json"))
36
+ .option("-q, --quiet", t("opt.quiet"))
37
+ .option("--no-color", t("opt.noColor"))
34
38
  .version(version);
39
+ // Root options are accepted on either side of the subcommand; read the merged view and publish it once.
40
+ program.hook("preAction", (_thisCommand, actionCommand) => {
41
+ const opts = actionCommand.optsWithGlobals();
42
+ configureOutput({ json: opts.json === true, quiet: opts.quiet === true, version });
43
+ setCommandName(actionCommand.name());
44
+ });
35
45
  // bare invocation -> interactive menu (help on non-TTY); unknown subcommand -> error.
36
46
  // excess arguments are surfaced by us, so the message names the offending command.
37
47
  program
@@ -40,6 +50,9 @@ program
40
50
  const cmd = handlerArgs[handlerArgs.length - 1];
41
51
  if (cmd.args.length > 0)
42
52
  program.error(t("error.unknownCommand", { value: cmd.args[0] }));
53
+ // The menu is inherently interactive; reject instead of silently ignoring the flag.
54
+ if (isJson())
55
+ error(t("error.jsonRequiresCommand"));
43
56
  if (process.stdin.isTTY)
44
57
  return cmdMenu();
45
58
  program.outputHelp();
@@ -175,6 +188,5 @@ program
175
188
  .option("--gateway-models <json>", t("opt.gatewayModels"))
176
189
  .action(cmdRefreshMeta);
177
190
  program.parseAsync().catch((err) => {
178
- process.stderr.write(`error: ${err.message}\n`);
179
- process.exit(1);
191
+ error(err.message);
180
192
  });