agentsw 0.7.2 → 0.9.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.
@@ -0,0 +1,201 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { readJsonIfExists, writeFileAtomic } from "./fsutil.js";
4
+ import { configDir } from "./store.js";
5
+ const API_URL = "https://ai-gateway.vercel.sh/v1/models";
6
+ const CACHE_FILE = path.join(configDir, "ai-gateway.json");
7
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
8
+ const MAX_BODY_BYTES = 5 * 1024 * 1024;
9
+ const REASONING_EFFORTS = {
10
+ none: true, minimal: true, low: true, medium: true, high: true, xhigh: true, max: true,
11
+ };
12
+ const PRICE_FIELDS = [
13
+ ["input", "input"],
14
+ ["output", "output"],
15
+ ["cachedInputTokens", "cacheRead"],
16
+ ["cacheCreationInputTokens", "cacheWrite"],
17
+ ];
18
+ function isStringList(value) {
19
+ return Array.isArray(value) && value.length > 0 &&
20
+ value.every((entry) => typeof entry === "string" && entry.length > 0 && entry.trim() === entry);
21
+ }
22
+ function reasoningEfforts(options) {
23
+ if (!Array.isArray(options))
24
+ return undefined;
25
+ let effort;
26
+ for (const option of options) {
27
+ if (!option || typeof option !== "object" || Array.isArray(option) || !("type" in option) || option.type !== "effort")
28
+ continue;
29
+ if (effort)
30
+ return undefined; // Conflicting declarations are not an authoritative effort list.
31
+ effort = option;
32
+ }
33
+ const values = effort?.values;
34
+ if (!isStringList(values) || !values.every((value) => Object.hasOwn(REASONING_EFFORTS, value)))
35
+ return undefined;
36
+ return [...new Set(values)];
37
+ }
38
+ function perMillion(value) {
39
+ if (typeof value === "string") {
40
+ const text = value.trim();
41
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(text))
42
+ return undefined;
43
+ value = Number(text);
44
+ }
45
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
46
+ return undefined;
47
+ const converted = value * 1_000_000;
48
+ return Number.isFinite(converted) ? converted : undefined;
49
+ }
50
+ /** Normalize public REST metadata without interpreting SDK versions as provider routing. */
51
+ export function parseGatewayCatalog(body, fetchedAt) {
52
+ if (!body || typeof body !== "object" || Array.isArray(body) || !("data" in body) || !Array.isArray(body.data)) {
53
+ throw new Error("Invalid AI Gateway catalog");
54
+ }
55
+ const models = Object.create(null);
56
+ const seen = new Set();
57
+ for (const entry of body.data) {
58
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
59
+ continue;
60
+ const row = entry;
61
+ if (typeof row.id !== "string" || !/^[^/\s]+(?:\/[^/\s]+)+$/.test(row.id))
62
+ continue;
63
+ if (seen.has(row.id))
64
+ throw new Error("Duplicate AI Gateway model identity");
65
+ seen.add(row.id);
66
+ if (row.type !== "language")
67
+ continue;
68
+ const spec = { id: row.id };
69
+ if (typeof row.name === "string" && row.name.trim())
70
+ spec.name = row.name;
71
+ const context = typeof row.context_window === "number" && Number.isSafeInteger(row.context_window) && row.context_window > 0
72
+ ? row.context_window : undefined;
73
+ const output = typeof row.max_tokens === "number" && Number.isSafeInteger(row.max_tokens) && row.max_tokens > 0
74
+ ? row.max_tokens : undefined;
75
+ // Neither member of an inconsistent pair is safe to treat as authoritative.
76
+ if (context === undefined || output === undefined || output <= context) {
77
+ if (context !== undefined)
78
+ spec.contextWindow = context;
79
+ if (output !== undefined)
80
+ spec.maxOutput = output;
81
+ }
82
+ const efforts = reasoningEfforts(row.reasoning_options);
83
+ if (efforts)
84
+ spec.reasoningEfforts = efforts;
85
+ if ((isStringList(row.tags) && row.tags.includes("reasoning")) || efforts?.some((effort) => effort !== "none")) {
86
+ spec.reasoning = true;
87
+ }
88
+ if (row.modalities && typeof row.modalities === "object" && !Array.isArray(row.modalities) &&
89
+ "input" in row.modalities && isStringList(row.modalities.input)) {
90
+ spec.imageInput = row.modalities.input.includes("image");
91
+ }
92
+ const model = { id: row.id, spec };
93
+ if (row.pricing && typeof row.pricing === "object" && !Array.isArray(row.pricing)) {
94
+ const pricing = row.pricing;
95
+ const referenceCost = {};
96
+ for (const [source, target] of PRICE_FIELDS) {
97
+ const price = perMillion(pricing[source]);
98
+ if (price !== undefined)
99
+ referenceCost[target] = price;
100
+ }
101
+ if (Object.keys(referenceCost).length)
102
+ model.referenceCost = referenceCost;
103
+ if ((Array.isArray(pricing.inputTiers) && pricing.inputTiers.length > 0) ||
104
+ (Array.isArray(pricing.outputTiers) && pricing.outputTiers.length > 0)) {
105
+ model.pricingIsVariable = true;
106
+ }
107
+ else if (typeof pricing.varies_by_provider === "boolean") {
108
+ model.pricingIsVariable = pricing.varies_by_provider;
109
+ }
110
+ }
111
+ models[row.id] = model;
112
+ }
113
+ return { fetchedAt, models };
114
+ }
115
+ function readCache() {
116
+ try {
117
+ // Bound local reads as well; the persisted envelope is only slightly larger than the response.
118
+ if (fs.statSync(CACHE_FILE).size > MAX_BODY_BYTES + 1024)
119
+ return undefined;
120
+ const cache = readJsonIfExists(CACHE_FILE);
121
+ if (!cache || typeof cache !== "object" || Array.isArray(cache) ||
122
+ !("version" in cache) || cache.version !== 1 ||
123
+ !("fetchedAt" in cache) || typeof cache.fetchedAt !== "string" || !("body" in cache))
124
+ return undefined;
125
+ const timestamp = Date.parse(cache.fetchedAt);
126
+ if (!Number.isFinite(timestamp) || timestamp > Date.now() || new Date(timestamp).toISOString() !== cache.fetchedAt) {
127
+ return undefined;
128
+ }
129
+ return parseGatewayCatalog(cache.body, cache.fetchedAt);
130
+ }
131
+ catch {
132
+ return undefined;
133
+ }
134
+ }
135
+ async function readResponseBody(response) {
136
+ const contentLength = response.headers.get("content-length");
137
+ if (contentLength !== null && Number(contentLength) > MAX_BODY_BYTES) {
138
+ await response.body?.cancel().catch(() => { });
139
+ throw new Error("AI Gateway catalog exceeds size limit");
140
+ }
141
+ if (!response.body)
142
+ throw new Error("Missing AI Gateway catalog body");
143
+ const reader = response.body.getReader();
144
+ const decoder = new TextDecoder();
145
+ const chunks = [];
146
+ let size = 0;
147
+ try {
148
+ for (;;) {
149
+ const { done, value } = await reader.read();
150
+ if (done)
151
+ break;
152
+ size += value.byteLength;
153
+ if (size > MAX_BODY_BYTES)
154
+ throw new Error("AI Gateway catalog exceeds size limit");
155
+ chunks.push(decoder.decode(value, { stream: true }));
156
+ }
157
+ chunks.push(decoder.decode());
158
+ return chunks.join("");
159
+ }
160
+ catch (error) {
161
+ await reader.cancel().catch(() => { });
162
+ throw error;
163
+ }
164
+ finally {
165
+ reader.releaseLock();
166
+ }
167
+ }
168
+ /** Optional supplement: offline never fetches, and every failure leaves discovery usable. */
169
+ export async function loadGatewayCatalog(opts = {}) {
170
+ const cached = readCache();
171
+ if (opts.offline)
172
+ return cached;
173
+ if (!opts.refresh && cached && Date.now() - Date.parse(cached.fetchedAt) < CACHE_TTL_MS)
174
+ return cached;
175
+ try {
176
+ const response = await fetch(API_URL, {
177
+ signal: AbortSignal.timeout(15_000),
178
+ credentials: "omit",
179
+ redirect: "error",
180
+ });
181
+ if (!response.ok) {
182
+ await response.body?.cancel().catch(() => { });
183
+ throw new Error("AI Gateway catalog request failed");
184
+ }
185
+ const body = JSON.parse(await readResponseBody(response));
186
+ const catalog = parseGatewayCatalog(body, new Date().toISOString());
187
+ try {
188
+ writeFileAtomic(CACHE_FILE, JSON.stringify({ version: 1, fetchedAt: catalog.fetchedAt, body }) + "\n", 0o600);
189
+ }
190
+ catch {
191
+ process.stderr.write("warning: could not cache AI Gateway metadata\n");
192
+ }
193
+ return catalog;
194
+ }
195
+ 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");
199
+ return cached;
200
+ }
201
+ }
package/dist/i18n.js CHANGED
@@ -1,7 +1,7 @@
1
1
  const messages = {
2
2
  "root.description": {
3
- en: "Manage OpenAI/Anthropic-protocol model providers and sync them into coding agents\n(Claude Code, Codex, omp, pi, prime-agent, opencode, Hermes, WorkBuddy, DeepSeek Harness).\nModel metadata (context window, input/output limits, reasoning levels) is enriched from\nmodels.dev; existing providers can be imported from the agents and from cc-switch.\n\nRun without arguments (or via `npx agentsw`) for the interactive menu.",
4
- "zh-CN": "管理 OpenAI / Anthropic 协议的模型供应商并同步到编码智能体\n(Claude Code、Codex、omp、pi、prime-agent、opencode、Hermes、WorkBuddy、DeepSeek Harness)。\n模型元数据(上下文、输入/输出上限、推理等级)由 models.dev 补全;\n也可从各智能体与 cc-switch 导入已有供应商。\n\n不带参数运行(或 `npx agentsw`)即可打开交互菜单。",
3
+ en: "Manage OpenAI/Anthropic-protocol model providers and sync them into coding agents\n(Claude Code, Codex, omp, pi, prime-agent, opencode, Hermes, WorkBuddy, DeepSeek Harness).\nModel metadata (context window, input/output limits, reasoning levels) is enriched from\nmodels.dev first, with automatic AI Gateway supplementation when needed.\nExisting providers can be imported from the agents and from cc-switch.\n\nRun without arguments (or via `npx agentsw`) for the interactive menu.",
4
+ "zh-CN": "管理 OpenAI / Anthropic 协议的模型供应商并同步到编码智能体\n(Claude Code、Codex、omp、pi、prime-agent、opencode、Hermes、WorkBuddy、DeepSeek Harness)。\n模型元数据(上下文、输入/输出上限、推理等级)优先由 models.dev 补全,必要时自动查询 AI Gateway;\n也可从各智能体与 cc-switch 导入已有供应商。\n\n不带参数运行(或 `npx agentsw`)即可打开交互菜单。",
5
5
  },
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" },
@@ -16,12 +16,12 @@ const messages = {
16
16
  "cmd.sync": { en: "re-apply the active provider (or --provider) to app configs", "zh-CN": "将当前供应商(或 --provider)重新应用到应用配置" },
17
17
  "cmd.status": { en: "show detected apps and what they currently point at", "zh-CN": "显示检测到的应用及当前供应商" },
18
18
  "cmd.models": { en: "search models.dev, or show a configured provider's models", "zh-CN": "搜索 models.dev 或显示已配置供应商的模型" },
19
- "cmd.discover": { en: "refresh a provider's models via /v1/models and models.dev", "zh-CN": "通过 /v1/models 和 models.dev 刷新供应商模型" },
19
+ "cmd.discover": { en: "refresh a provider's model list and configured metadata sources", "zh-CN": "获取供应商模型列表,并通过已启用的目录补全参数" },
20
20
  "cmd.import": { en: "scan app configs, preview and dedupe existing custom providers, then import", "zh-CN": "扫描应用配置,预览并去重已有自定义供应商后导入" },
21
21
  "cmd.apps": { en: "check installed agents: current version vs latest", "zh-CN": "检查智能体已安装版本与最新版本" },
22
22
  "cmd.install": { en: "install an agent CLI", "zh-CN": "安装智能体 CLI" },
23
23
  "cmd.upgrade": { en: "upgrade agent CLIs (no args = everything outdated)", "zh-CN": "升级智能体 CLI(无参数时升级全部过期项)" },
24
- "cmd.refresh": { en: "re-fetch models.dev metadata for all providers", "zh-CN": "重新拉取所有供应商的 models.dev 元数据" },
24
+ "cmd.refresh": { en: "refresh metadata for saved models; optionally configure Gateway supplementation", "zh-CN": "刷新已有模型参数,可设置 Gateway 补充源(不改变模型列表)" },
25
25
  "opt.id": { en: "custom provider id (default: full hostname + protocol)", "zh-CN": "自定义供应商 ID(默认:完整域名 + 协议)" },
26
26
  "opt.name": { en: "display name", "zh-CN": "显示名称" },
27
27
  "opt.protocol": { en: "wire protocol: openai | anthropic", "zh-CN": "接口协议: openai | anthropic" },
@@ -41,6 +41,12 @@ const messages = {
41
41
  "opt.dedup": { en: "deduplicate model snapshot aliases", "zh-CN": "合并重复的模型快照别名" },
42
42
  "opt.noDedup": { en: "keep snapshot duplicates (-latest/date suffixes)", "zh-CN": "保留快照重复项(-latest/日期后缀)" },
43
43
  "opt.modelsDev": { en: "models.dev provider id for metadata matching", "zh-CN": "用于元数据匹配的 models.dev 供应商 id" },
44
+ "opt.metadataMode": { en: "save Gateway mode: auto (default, only when needed), on, or off; omission keeps saved mode", "zh-CN": "保存 Gateway 模式:auto(默认,按需补缺)、on 或 off;省略则沿用已保存模式" },
45
+ "opt.gatewayMetadata": { en: "always supplement from AI Gateway (same as --metadata-mode on)", "zh-CN": "始终使用 AI Gateway 补缺(等同 --metadata-mode on)" },
46
+ "opt.noGatewayMetadata": { en: "disable Gateway fetching; keep saved parameters (same as --metadata-mode off)", "zh-CN": "关闭 Gateway 获取,保留已保存参数(等同 --metadata-mode off)" },
47
+ "opt.gatewayModels": { en: 'replace exact aliases as JSON: {"local-id":"creator/model"}; {} clears them', "zh-CN": '用 JSON 替换精确映射:{"本地模型ID":"creator/model"};{} 清空' },
48
+ "opt.metadataDetails": { en: "show field sources, conflicts and Gateway reference pricing as JSON (requires --provider)", "zh-CN": "以 JSON 查看字段来源、冲突和 Gateway 参考价格(需 --provider)" },
49
+ "opt.refreshProvider": { en: "refresh/configure only this saved provider (default: all)", "zh-CN": "仅刷新和设置该供应商(默认全部)" },
44
50
  "opt.yes": { en: "non-interactive; require all flags", "zh-CN": "非交互模式;要求提供全部参数" },
45
51
  "opt.prune": { en: "also remove entries from app configs", "zh-CN": "同时从应用配置中移除条目" },
46
52
  "opt.apps": { en: "comma-separated apps or 'all'", "zh-CN": "逗号分隔的应用或 'all'" },
@@ -75,6 +81,16 @@ const messages = {
75
81
  "menu.list": { en: "View providers saved in agentsw", "zh-CN": "查看 agentsw 供应商列表" },
76
82
  "menu.sync": { en: "Re-sync the current provider to agents", "zh-CN": "重新同步当前供应商" },
77
83
  "menu.discover": { en: "Update a provider's model list", "zh-CN": "更新供应商模型列表" },
84
+ "menu.metadata": { en: "Configure and refresh model metadata", "zh-CN": "设置并刷新模型参数补充源" },
85
+ "menu.metadataHelp": { en: "Keep the model list; choose automatic, always-on or disabled AI Gateway lookup, then refresh saved parameters only.", "zh-CN": "保留模型列表,选择 AI Gateway 自动按需、始终补充或关闭,仅刷新保存的参数。" },
86
+ "menu.metadataProvider": { en: "which provider's metadata settings should be updated?", "zh-CN": "选择要设置模型参数补充源的供应商" },
87
+ "menu.metadataMode": { en: "AI Gateway lookup mode (no API key is sent)", "zh-CN": "AI Gateway 查询模式(请求不携带 API key)" },
88
+ "menu.metadataAuto": { en: "Automatic (recommended)", "zh-CN": "自动按需(推荐)" },
89
+ "menu.metadataAutoHelp": { en: "models.dev first; query Gateway for missing core specs, to refresh its saved fields or verify tracked identity conflicts.", "zh-CN": "优先 models.dev;仅在核心参数缺失、需刷新已有 Gateway 字段或核实旧自动值身份冲突时查询。" },
90
+ "menu.metadataOn": { en: "Always supplement", "zh-CN": "始终补充" },
91
+ "menu.metadataOnHelp": { en: "Check Gateway on every metadata lookup; models.dev still takes priority.", "zh-CN": "每次补全参数都查询 Gateway,仍以 models.dev 为优先。" },
92
+ "menu.metadataOff": { en: "Disabled", "zh-CN": "关闭" },
93
+ "menu.metadataOffHelp": { en: "Never query Gateway; keep previously saved model parameters.", "zh-CN": "不查询 Gateway,保留已保存的模型参数。" },
78
94
  "menu.remove": { en: "Delete provider configuration", "zh-CN": "删除供应商配置" },
79
95
  "menu.rename": { en: "Change a provider ID", "zh-CN": "修改供应商 ID" },
80
96
  "menu.renameProvider": { en: "which provider ID should be changed?", "zh-CN": "选择要修改 ID 的供应商" },
@@ -107,6 +123,10 @@ const messages = {
107
123
  "menu.defaultModel": { en: "default model", "zh-CN": "默认模型" },
108
124
  "menu.keepDefault": { en: "keep current default ({model})", "zh-CN": "保持当前默认模型({model})" },
109
125
  "menu.active": { en: "current in agentsw", "zh-CN": "agentsw 当前" },
126
+ "menu.pickApps": { en: "which agent configs should this write to?", "zh-CN": "写入哪些智能体配置?" },
127
+ "menu.appsAll": { en: "all {count} detected apps", "zh-CN": "全部 {count} 个检测到的应用" },
128
+ "menu.appNotDetected": { en: "(not detected)", "zh-CN": "(未检测到)" },
129
+ "menu.appsHint": { en: "↑/↓ move · Space toggle · Enter confirm", "zh-CN": "↑/↓ 移动 · 空格切换 · Enter 确认" },
110
130
  "menu.discoverFor": { en: "which provider's model list should be updated?", "zh-CN": "选择要更新模型列表的供应商" },
111
131
  "menu.pushRefresh": { en: "also sync this provider to agent configs after updating its model list?", "zh-CN": "更新模型列表后,是否同时将该供应商同步到智能体配置?" },
112
132
  "menu.removeProvider": { en: "which provider configuration should be deleted?", "zh-CN": "选择要删除配置的供应商" },
@@ -172,7 +192,7 @@ const messages = {
172
192
  "add.added": { en: "added", "zh-CN": "已添加" },
173
193
  "add.updated": { en: "updated", "zh-CN": "已更新" },
174
194
  "add.saved": { en: "{status} provider {id} ({protocol})", "zh-CN": "{status}供应商 {id} ({protocol})" },
175
- "add.metadata": { en: "models.dev metadata: {matched}/{total} models matched", "zh-CN": "models.dev 元数据:匹配 {matched}/{total} 个模型" },
195
+ "add.metadata": { en: "model metadata: {matched}/{total} models have a context limit", "zh-CN": "模型参数:{matched}/{total} 个模型已有上下文上限" },
176
196
  "add.providerHint": { en: " (provider hint: {hint})", "zh-CN": " (供应商提示:{hint})" },
177
197
  "add.next": { en: "next: agentsw use {id}", "zh-CN": "下一步: agentsw use {id}" },
178
198
  "quick.probing": { en: "probing protocols at {url} ...", "zh-CN": "正在探测 {url} 支持的协议..." },
@@ -186,7 +206,7 @@ const messages = {
186
206
  "menu.useHelp": { en: "Select a provider and default model, then write them to compatible agent configs.", "zh-CN": "选择供应商和默认模型,然后写入支持该协议的智能体配置。" },
187
207
  "menu.statusHelp": { en: "Show detected agents, their current providers and config file locations.", "zh-CN": "查看检测到的智能体、所用供应商及配置文件位置。" },
188
208
  "menu.listHelp": { en: "Show only the providers, protocols and default models saved in agentsw.", "zh-CN": "只查看 agentsw 已保存的供应商、协议和默认模型。" },
189
- "menu.syncHelp": { en: "Write the current agentsw provider to agent configs without fetching a new model list.", "zh-CN": "将 agentsw 当前供应商写入智能体配置,不重新获取模型列表。" },
209
+ "menu.syncHelp": { en: "Write the current agentsw provider to agent configs without fetching a model list or metadata catalogs.", "zh-CN": "将 agentsw 当前供应商写入智能体配置,不重新获取模型列表或元数据目录。" },
190
210
  "menu.discoverHelp": { en: "Fetch models from the provider, fill in model metadata, and optionally sync to agents.", "zh-CN": "从供应商接口重新获取模型并补全模型参数,可选择同步到智能体。" },
191
211
  "menu.renameHelp": { en: "Change the config ID and update references; keep custom display names.", "zh-CN": "修改配置中的标识并更新关联引用,保留自定义显示名称。" },
192
212
  "menu.removeHelp": { en: "Choose where to delete: agentsw only, one agent only, or both.", "zh-CN": "先选择删除范围:仅 agentsw、仅某个智能体,或两处一起。" },
package/dist/index.js CHANGED
@@ -63,6 +63,10 @@ program
63
63
  .option("--dedup", t("opt.dedup"))
64
64
  .option("--no-dedup", t("opt.noDedup"))
65
65
  .option("--models-dev <id>", t("opt.modelsDev"))
66
+ .option("--metadata-mode <auto|on|off>", t("opt.metadataMode"))
67
+ .option("--gateway-metadata", t("opt.gatewayMetadata"))
68
+ .option("--no-gateway-metadata", t("opt.noGatewayMetadata"))
69
+ .option("--gateway-models <json>", t("opt.gatewayModels"))
66
70
  .option("-y, --yes", t("opt.yes"))
67
71
  .action(cmdAdd);
68
72
  program
@@ -72,6 +76,10 @@ program
72
76
  .option("--name <name>", t("opt.name"))
73
77
  .option("--openai-api <flavor>", t("opt.openaiApi"))
74
78
  .option("--models-dev <id>", t("opt.modelsDev"))
79
+ .option("--metadata-mode <auto|on|off>", t("opt.metadataMode"))
80
+ .option("--gateway-metadata", t("opt.gatewayMetadata"))
81
+ .option("--no-gateway-metadata", t("opt.noGatewayMetadata"))
82
+ .option("--gateway-models <json>", t("opt.gatewayModels"))
75
83
  .option("--base-url <url>", t("opt.baseUrl"))
76
84
  .option("--api-key <key>", t("opt.apiKey"))
77
85
  .option("--default-model <id>", t("opt.defaultModel"))
@@ -126,6 +134,7 @@ program
126
134
  .command("models [query]")
127
135
  .description(t("cmd.models"))
128
136
  .option("-p, --provider <id>", t("opt.showProvider"))
137
+ .option("--metadata", t("opt.metadataDetails"))
129
138
  .option("-r, --refresh", t("opt.refresh"))
130
139
  .option("-l, --limit <n>", t("opt.limit"), "30")
131
140
  .action(cmdModels);
@@ -139,16 +148,32 @@ program
139
148
  .option("--dedup", t("opt.dedup"))
140
149
  .option("--no-dedup", t("opt.noDedup"))
141
150
  .option("--no-filter", t("opt.noFilter"))
151
+ .option("--metadata-mode <auto|on|off>", t("opt.metadataMode"))
152
+ .option("--gateway-metadata", t("opt.gatewayMetadata"))
153
+ .option("--no-gateway-metadata", t("opt.noGatewayMetadata"))
154
+ .option("--gateway-models <json>", t("opt.gatewayModels"))
142
155
  .action(cmdDiscover);
143
156
  program
144
157
  .command("import")
145
158
  .description(t("cmd.import"))
146
159
  .option("--all", t("opt.all"))
160
+ .option("--metadata-mode <auto|on|off>", t("opt.metadataMode"))
161
+ .option("--gateway-metadata", t("opt.gatewayMetadata"))
162
+ .option("--no-gateway-metadata", t("opt.noGatewayMetadata"))
163
+ .option("--gateway-models <json>", t("opt.gatewayModels"))
147
164
  .action(cmdImport);
148
165
  program.command("apps").description(t("cmd.apps")).action(cmdApps);
149
166
  program.command("install <app>").description(t("cmd.install")).action(cmdInstall);
150
167
  program.command("upgrade [apps...]").description(t("cmd.upgrade")).action(cmdUpgrade);
151
- program.command("refresh").description(t("cmd.refresh")).action(cmdRefreshMeta);
168
+ program
169
+ .command("refresh")
170
+ .description(t("cmd.refresh"))
171
+ .option("-p, --provider <id>", t("opt.refreshProvider"))
172
+ .option("--metadata-mode <auto|on|off>", t("opt.metadataMode"))
173
+ .option("--gateway-metadata", t("opt.gatewayMetadata"))
174
+ .option("--no-gateway-metadata", t("opt.noGatewayMetadata"))
175
+ .option("--gateway-models <json>", t("opt.gatewayModels"))
176
+ .action(cmdRefreshMeta);
152
177
  program.parseAsync().catch((err) => {
153
178
  process.stderr.write(`error: ${err.message}\n`);
154
179
  process.exit(1);
package/dist/jsonc.js CHANGED
@@ -1,4 +1,4 @@
1
- import { readTextIfExists } from "./fsutil.js";
1
+ import { SafeConfigError, readTextIfExists } from "./fsutil.js";
2
2
  import { applyEdits, createScanner, getNodeValue, modify, parseTree, printParseErrorCode, } from "jsonc-parser";
3
3
  export function isJsonObject(value) {
4
4
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -9,11 +9,10 @@ function parseJsoncObject(file, text) {
9
9
  const root = parseTree(source, errors, { allowTrailingComma: true });
10
10
  const error = errors[0];
11
11
  if (error) {
12
- // Do not quote source text: config files can contain credentials.
13
- throw new Error(`${file}: invalid JSONC (${printParseErrorCode(error.error)} at offset ${error.offset})`);
12
+ throw new SafeConfigError(`${file}: invalid JSONC (${printParseErrorCode(error.error)} at offset ${error.offset})`);
14
13
  }
15
14
  if (root?.type !== "object")
16
- throw new Error(`${file}: expected a JSON object at the document root`);
15
+ throw new SafeConfigError(`${file}: expected a JSON object at the document root`);
17
16
  // Parsing resolves duplicate keys last-wins, but modify edits the first occurrence.
18
17
  // Reject that ambiguity at any depth rather than syncing a value the agent will not read.
19
18
  const checkDuplicates = (node) => {
@@ -22,7 +21,7 @@ function parseJsoncObject(file, text) {
22
21
  if (names) {
23
22
  const name = child.children[0].value;
24
23
  if (names.has(name)) {
25
- throw new Error(`${file}: invalid JSONC (duplicate object property at offset ${child.offset})`);
24
+ throw new SafeConfigError(`${file}: invalid JSONC (duplicate object property at offset ${child.offset})`);
26
25
  }
27
26
  names.add(name);
28
27
  }
package/dist/menu.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import pc from "picocolors";
2
2
  import prompts from "prompts";
3
- import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall, cmdDiscover, cmdImport, cmdList, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
3
+ import { cmdAdd, cmdQuickAdd, cmdApps, cmdInstall, cmdDiscover, cmdImport, cmdRefreshMeta, cmdList, cmdStatus, cmdSync, cmdUpgrade, cmdUse, } from "./commands.js";
4
4
  import { getLocale, setLocale, t } from "./i18n.js";
5
+ import { getMetadataMode } from "./metadata.js";
5
6
  import { loadStore, saveStore } from "./store.js";
6
7
  import { appPackages, appCommand, installedVersion } from "./apps.js";
7
8
  import { cmdRemoveProvider, cmdRename } from "./provider-actions.js";
@@ -61,6 +62,42 @@ async function pickProvider(message) {
61
62
  const p = store.providers[id];
62
63
  return { id, defaultModel: p.defaultModel, models: p.models.map((m) => m.id) };
63
64
  }
65
+ /**
66
+ * Multi-select target apps, remembering the previous choice. The first row selects
67
+ * every detected app; picking it (or toggling everything off) returns undefined,
68
+ * which keeps the CLI default of "all detected apps". A narrower selection becomes
69
+ * an explicit --apps filter, which force-writes even undetected apps — so only
70
+ * offer real choices.
71
+ */
72
+ async function pickApps(message) {
73
+ const remembered = (loadStore().syncTargets ?? []).filter((id) => targets.some((target) => target.id === id));
74
+ const detected = targets.filter((target) => target.detect());
75
+ const { apps } = await prompts({
76
+ type: "multiselect",
77
+ name: "apps",
78
+ message,
79
+ hint: t("menu.appsHint"),
80
+ instructions: false,
81
+ choices: [
82
+ { title: t("menu.appsAll", { count: detected.length }), value: "all", selected: remembered.length === 0 },
83
+ // Detected apps first, and say so: choosing an undetected one force-creates its config.
84
+ ...[...targets].sort((a, b) => Number(b.detect()) - Number(a.detect())).map((target) => ({
85
+ title: `${target.id} · ${target.name}${target.detect() ? "" : ` ${pc.dim(t("menu.appNotDetected"))}`}`,
86
+ value: target.id,
87
+ selected: remembered.includes(target.id),
88
+ })),
89
+ ],
90
+ }, cancel);
91
+ const picked = (Array.isArray(apps) ? apps : []).filter((value) => value !== "all");
92
+ const all = picked.length === 0 || apps.includes("all") || picked.length === targets.length;
93
+ const store = loadStore();
94
+ if (all)
95
+ delete store.syncTargets;
96
+ else
97
+ store.syncTargets = picked;
98
+ saveStore(store);
99
+ return all ? undefined : picked.join(",");
100
+ }
64
101
  async function renameFromMenu() {
65
102
  const picked = await pickProvider(t("menu.renameProvider"));
66
103
  if (!picked)
@@ -166,6 +203,7 @@ export async function cmdMenu() {
166
203
  { title: t("menu.list"), description: t("menu.listHelp"), value: "list" },
167
204
  { title: t("menu.sync"), description: t("menu.syncHelp"), value: "sync" },
168
205
  { title: t("menu.discover"), description: t("menu.discoverHelp"), value: "discover" },
206
+ { title: t("menu.metadata"), description: t("menu.metadataHelp"), value: "metadata" },
169
207
  { title: t("menu.rename"), description: t("menu.renameHelp"), value: "rename" },
170
208
  { title: t("menu.remove"), description: t("menu.removeHelp"), value: "remove" },
171
209
  { title: t("menu.apps"), description: t("menu.appsHelp"), value: "apps" },
@@ -199,6 +237,7 @@ export async function cmdMenu() {
199
237
  const picked = await pickProvider(t("menu.pickProvider"));
200
238
  if (!picked)
201
239
  continue;
240
+ const apps = await pickApps(t("menu.pickApps"));
202
241
  const { model } = await prompts({
203
242
  type: picked.models.length > 1 ? "select" : null,
204
243
  name: "model",
@@ -209,7 +248,7 @@ export async function cmdMenu() {
209
248
  ...picked.models.filter((m) => m !== picked.defaultModel).map((m) => ({ title: m, value: m })),
210
249
  ],
211
250
  }, cancel);
212
- await cmdUse(picked.id, { model: model || undefined });
251
+ await cmdUse(picked.id, { model: model || undefined, apps });
213
252
  }
214
253
  else if (action === "status") {
215
254
  cmdStatus();
@@ -218,14 +257,33 @@ export async function cmdMenu() {
218
257
  cmdList();
219
258
  }
220
259
  else if (action === "sync") {
221
- await cmdSync({});
260
+ await cmdSync({ apps: await pickApps(t("menu.pickApps")) });
222
261
  }
223
262
  else if (action === "discover") {
224
263
  const picked = await pickProvider(t("menu.discoverFor"));
225
264
  if (!picked)
226
265
  continue;
227
266
  const sync = await askToggle(t("menu.pushRefresh"));
228
- await cmdDiscover(picked.id, { sync });
267
+ await cmdDiscover(picked.id, { sync, apps: sync ? await pickApps(t("menu.pickApps")) : undefined });
268
+ }
269
+ else if (action === "metadata") {
270
+ const picked = await pickProvider(t("menu.metadataProvider"));
271
+ if (!picked)
272
+ continue;
273
+ const current = loadStore().providers[picked.id];
274
+ const { metadataMode } = await prompts({
275
+ type: "select",
276
+ name: "metadataMode",
277
+ message: t("menu.metadataMode"),
278
+ hint: t("menu.selectInstructions"),
279
+ initial: ["auto", "on", "off"].indexOf(getMetadataMode(current)),
280
+ choices: [
281
+ { title: t("menu.metadataAuto"), description: t("menu.metadataAutoHelp"), value: "auto" },
282
+ { title: t("menu.metadataOn"), description: t("menu.metadataOnHelp"), value: "on" },
283
+ { title: t("menu.metadataOff"), description: t("menu.metadataOffHelp"), value: "off" },
284
+ ],
285
+ }, cancel);
286
+ await cmdRefreshMeta({ provider: picked.id, metadataMode });
229
287
  }
230
288
  else if (action === "rename" || action === "remove") {
231
289
  try {