agentsw 0.6.2 → 0.7.1

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
@@ -164,14 +164,18 @@ Explicit `--models` entries and the default model are never filtered out.
164
164
  |---|---|
165
165
  | `agentsw` | interactive menu (no arguments) |
166
166
  | `add` | add or update a provider; `--discover` fills the model list |
167
+ | `quick` | discover protocols/models from URL + API key; auto-name by hostname + protocol |
167
168
  | `import [--all]` | adopt providers from your agents' configs and cc-switch |
168
169
  | `list` / `status` | configured providers / what each agent points at |
170
+ | `list --apps omp,prime` | list agent-local provider IDs, including entries absent from agentsw |
171
+ | `rename <id> <new-id>` | back up and migrate the ID and config references; supports `--dry-run` |
169
172
  | `use <id>` | switch every detected agent; `-a codex,omp`, `-m <model>`, `--dry-run` |
170
173
  | `sync` | re-apply the active provider (after an agent update, say) |
171
174
  | `discover <id> [--sync]` | refresh the model list + metadata from `/v1/models` |
172
175
  | `models [query]` | search the models.dev catalog |
173
176
  | `refresh` | re-fetch metadata for every configured provider |
174
177
  | `prune <id>` / `remove <id> [--prune]` | remove from app configs / from the store |
178
+ | `remove <id> --apps omp` | delete only the selected app's entry; supports `--dry-run` |
175
179
  | `apps` / `install <app>` / `upgrade` | agent version manager |
176
180
 
177
181
  ```bash
@@ -180,6 +184,24 @@ asw discover myproxy --sync
180
184
  asw remove myproxy --prune
181
185
  ```
182
186
 
187
+ New automatic IDs include the full hostname and protocol, e.g. `api-example-com-openai`.
188
+ An explicit `--id` is retained, and syncing never renames an existing provider. Import
189
+ deduplication compares endpoint, protocol, and credentials; different accounts remain separate.
190
+
191
+ ```bash
192
+ asw rename myproxy api-example-com-openai --dry-run
193
+ asw rename myproxy api-example-com-openai
194
+ asw list --apps omp
195
+ asw remove unused-provider --apps omp --dry-run
196
+ asw remove unused-provider --apps omp
197
+ ```
198
+
199
+ `remove` alone changes only the agentsw store; `--prune` also cleans matching app configs.
200
+ `--apps` is instead app-only and can remove a provider never imported into agentsw.
201
+ Do not combine `--apps` and `--prune`. Renames and removals preflight changes and create
202
+ private backups before writes. App-only removal leaves the store intact, so a later explicit
203
+ sync of a managed provider can add it back to that app.
204
+
183
205
  ## How your configs are treated
184
206
 
185
207
  - Every modified file is backed up first to `~/.config/agentsw/backups/`.
package/README.zh-CN.md CHANGED
@@ -161,14 +161,18 @@ asw discover myproxy --no-filter # 清除规则
161
161
  |---|---|
162
162
  | `agentsw` | 交互菜单(不带参数) |
163
163
  | `add` | 添加或更新供应商;`--discover` 自动填模型列表 |
164
+ | `quick` | 只需 URL + API key,自动探测协议与模型,按完整域名 + 协议命名 |
164
165
  | `import [--all]` | 接管各智能体配置与 cc-switch 里已有的供应商 |
165
166
  | `list` / `status` | 已配置的供应商 / 各智能体当前指向 |
167
+ | `list --apps omp,prime` | 列出智能体本地供应商 ID,包括未导入 agentsw 的条目 |
168
+ | `rename <id> <new-id>` | 备份并迁移 ID 和配置引用,支持 `--dry-run` |
166
169
  | `use <id>` | 切换所有检测到的智能体;`-a codex,omp`、`-m <model>`、`--dry-run` |
167
170
  | `sync` | 重新应用当前供应商(比如某个智能体升级之后) |
168
171
  | `discover <id> [--sync]` | 从 `/v1/models` 刷新模型列表与元数据 |
169
172
  | `models [query]` | 搜索 models.dev 目录 |
170
173
  | `refresh` | 重新拉取所有供应商的元数据 |
171
174
  | `prune <id>` / `remove <id> [--prune]` | 从各应用配置中清除 / 从存储中删除 |
175
+ | `remove <id> --apps omp` | 仅删除指定智能体内的条目,支持 `--dry-run` |
172
176
  | `apps` / `install <app>` / `upgrade` | 智能体版本管理 |
173
177
 
174
178
  ```bash
@@ -177,6 +181,23 @@ asw discover myproxy --sync
177
181
  asw remove myproxy --prune
178
182
  ```
179
183
 
184
+ 新建供应商的自动 ID 使用完整域名和协议,例如 `api-example-com-openai`。
185
+ 显式指定的 `--id` 会保留,同步不会自动重命名。导入去重会同时比较端点、协议和凭据,
186
+ 不同账号保持独立;同一账号的自定义名称优先于自动生成名称。
187
+
188
+ ```bash
189
+ asw rename myproxy api-example-com-openai --dry-run
190
+ asw rename myproxy api-example-com-openai
191
+ asw list --apps omp
192
+ asw remove unused-provider --apps omp --dry-run
193
+ asw remove unused-provider --apps omp
194
+ ```
195
+
196
+ 单独 `remove` 只删除 agentsw 中央条目;`--prune` 还会清理匹配的智能体配置。
197
+ `--apps` 则仅作用于指定智能体,也能删除从未导入 agentsw 的供应商,请勿和 `--prune` 同用。
198
+ 重命名和删除会先检查全部计划变更,再创建私有备份并写入。
199
+ 仅从智能体删除时中央条目仍保留,之后主动同步该供应商会重新写入该智能体。
200
+
180
201
  ## 它如何对待你的配置
181
202
 
182
203
  - 每个被修改的文件先备份到 `~/.config/agentsw/backups/`。
package/dist/commands.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import pc from "picocolors";
2
2
  import prompts from "prompts";
3
3
  import { loadStore, saveStore, getProvider, configFile } from "./store.js";
4
- import { scanCandidates, normalizeUrl } from "./import.js";
4
+ import { scanCandidates, normalizeUrl, findMatchingProvider } from "./import.js";
5
5
  import { enrichModels, loadCatalog, searchCatalog } from "./modelsdev.js";
6
6
  import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
7
7
  import { discoverProviderModels, probeProtocols } from "./discover.js";
8
8
  import { appCommand, appPackages, installedVersion, isNewer, latestVersion, runShell } from "./apps.js";
9
9
  import { drainPendingWrites, readTextIfExists, setDryRun } from "./fsutil.js";
10
10
  import { applyModelFilter } from "./filter.js";
11
- import { slugFromBaseUrl } from "./slug.js";
11
+ import { availableProviderId, providerIdFromBaseUrl, providerNameFromBaseUrl } from "./slug.js";
12
12
  import { t } from "./i18n.js";
13
13
  function fail(message) {
14
14
  process.stderr.write(pc.red(`error: ${message}\n`));
@@ -123,8 +123,8 @@ export async function cmdAdd(opts) {
123
123
  {
124
124
  type: "text",
125
125
  name: "id",
126
- message: t("add.id"),
127
- validate: (v) => (/^[a-z0-9][a-z0-9_-]*$/.test(v) ? true : t("add.idInvalid")),
126
+ message: t("add.idAuto"),
127
+ validate: (v) => (!v || /^[a-z0-9][a-z0-9_-]*$/.test(v) ? true : t("add.idInvalid")),
128
128
  },
129
129
  { type: "text", name: "name", message: t("add.name"), initial: (prev) => prev },
130
130
  {
@@ -159,15 +159,15 @@ export async function cmdAdd(opts) {
159
159
  }
160
160
  else {
161
161
  const required = opts.discover
162
- ? ["id", "protocol", "baseUrl", "apiKey"]
163
- : ["id", "protocol", "baseUrl", "apiKey", "models"];
162
+ ? ["protocol", "baseUrl", "apiKey"]
163
+ : ["protocol", "baseUrl", "apiKey", "models"];
164
164
  for (const field of required) {
165
165
  if (!opts[field])
166
166
  fail(t("add.fieldRequired", { field: field === "baseUrl" ? "base-url" : field === "apiKey" ? "api-key" : field }));
167
167
  }
168
168
  answers = {
169
- id: opts.id,
170
- name: opts.name ?? opts.id,
169
+ id: opts.id ?? "",
170
+ name: opts.name ?? "",
171
171
  protocol: opts.protocol,
172
172
  baseUrl: opts.baseUrl,
173
173
  apiKey: opts.apiKey,
@@ -182,7 +182,7 @@ export async function cmdAdd(opts) {
182
182
  if (wire && wire !== "completions" && wire !== "responses")
183
183
  fail(t("add.openaiApiInvalid"));
184
184
  const openaiApi = protocol === "openai" ? wire : undefined;
185
- const baseUrl = answers.baseUrl.replace(/\/+$/, "");
185
+ const baseUrl = normalizeUrl(answers.baseUrl);
186
186
  const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
187
187
  let modelIds = manualIds;
188
188
  const modelFilter = parseFilterOpts(opts);
@@ -211,9 +211,12 @@ export async function cmdAdd(opts) {
211
211
  fail(t("add.defaultMissing", { model: defaultModel }));
212
212
  if (opts.smallModel && !modelIds.includes(opts.smallModel))
213
213
  fail(t("add.smallMissing", { model: opts.smallModel }));
214
+ const explicitId = answers.id || undefined;
215
+ const existing = explicitId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey: answers.apiKey });
216
+ const id = explicitId ?? existing?.id ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
214
217
  const provider = {
215
- id: answers.id,
216
- name: answers.name || answers.id,
218
+ id,
219
+ name: answers.name || existing?.name || (explicitId ? explicitId : providerNameFromBaseUrl(baseUrl, protocol)),
217
220
  protocol,
218
221
  ...(openaiApi ? { openaiApi } : {}),
219
222
  baseUrl,
@@ -282,24 +285,21 @@ export async function cmdQuickAdd(opts) {
282
285
  const interactive = process.stdin.isTTY && !opts.yes;
283
286
  let baseUrl;
284
287
  let apiKey;
285
- let baseId;
286
288
  if (interactive) {
287
289
  const answers = await prompts([
288
290
  { type: "text", name: "baseUrl", message: t("add.baseUrl"), validate: (v) => (/^https?:\/\//.test(v) ? true : t("add.baseUrlInvalid")) },
289
291
  { type: "password", name: "apiKey", message: t("add.apiKey") },
290
292
  ], { onCancel: () => fail(t("add.cancelled")) });
291
- baseUrl = answers.baseUrl.replace(/\/+$/, "");
293
+ baseUrl = normalizeUrl(answers.baseUrl);
292
294
  apiKey = answers.apiKey;
293
- baseId = opts.id ?? slugFromBaseUrl(baseUrl);
294
295
  }
295
296
  else {
296
297
  if (!opts.baseUrl)
297
298
  fail(t("add.fieldRequired", { field: "base-url" }));
298
299
  if (!opts.apiKey)
299
300
  fail(t("add.fieldRequired", { field: "api-key" }));
300
- baseUrl = opts.baseUrl.replace(/\/+$/, "");
301
+ baseUrl = normalizeUrl(opts.baseUrl);
301
302
  apiKey = opts.apiKey;
302
- baseId = opts.id ?? slugFromBaseUrl(baseUrl);
303
303
  }
304
304
  process.stderr.write(pc.dim(t("quick.probing", { url: baseUrl }) + "\n"));
305
305
  const protocols = await probeProtocols({ baseUrl, apiKey });
@@ -310,9 +310,10 @@ export async function cmdQuickAdd(opts) {
310
310
  const multi = protocols.length > 1;
311
311
  const createdIds = [];
312
312
  for (const protocol of protocols) {
313
- const suffix = multi ? `-${protocol}` : "";
314
- const id = `${baseId}${suffix}`;
315
- const name = multi ? `${baseId} (${protocol})` : baseId;
313
+ const explicitId = opts.id === undefined ? undefined : `${opts.id}${multi ? `-${protocol}` : ""}`;
314
+ const existing = explicitId === undefined ? findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey }) : undefined;
315
+ const id = explicitId ?? existing?.id ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
316
+ const name = existing?.name ?? (opts.id === undefined ? providerNameFromBaseUrl(baseUrl, protocol) : multi ? `${opts.id} (${protocol})` : opts.id);
316
317
  process.stderr.write(pc.dim(`${t("add.discovering", { url: baseUrl })} [${protocol}]\n`));
317
318
  const discovered = await discoverProviderModels({ baseUrl, apiKey, protocol });
318
319
  console.log(t("add.providerLists", { count: discovered.length }));
@@ -366,22 +367,6 @@ export function cmdList() {
366
367
  });
367
368
  console.log(table(rows, [" ", "ID", t("table.protocol"), "BASE URL", t("table.defaultModel"), t("table.models")]));
368
369
  }
369
- export async function cmdRemove(id, opts) {
370
- const store = loadStore();
371
- const provider = getProvider(store, id);
372
- if (opts.prune) {
373
- console.log(`${t("remove.pruning", { id: pc.bold(id) })}\n`);
374
- reportResults(await runTargets("prune", provider, undefined));
375
- console.log("");
376
- }
377
- delete store.providers[id];
378
- if (store.active === id)
379
- store.active = undefined;
380
- saveStore(store);
381
- console.log(pc.green(t("remove.removed", { id })));
382
- if (!opts.prune)
383
- console.log(pc.dim(t("remove.note")));
384
- }
385
370
  export async function cmdPrune(id, opts) {
386
371
  const store = loadStore();
387
372
  const provider = getProvider(store, id);
@@ -686,7 +671,7 @@ export async function cmdUpgrade(ids) {
686
671
  }
687
672
  }
688
673
  }
689
- /** Collect custom providers from app configs, dedupe by protocol+baseUrl, import what is new. */
674
+ /** Collect custom providers, dedupe by endpoint and credentials, and import what is new. */
690
675
  export async function cmdImport(opts) {
691
676
  const rows = scanCandidates();
692
677
  const fresh = rows.filter((r) => !r.configured);
@@ -748,6 +733,12 @@ export async function cmdImport(opts) {
748
733
  }, { onCancel: () => fail(t("add.cancelled")) });
749
734
  apiKey = a.key;
750
735
  }
736
+ // A prompted credential can identify a provider imported earlier in this same batch.
737
+ const existing = findMatchingProvider(Object.values(store.providers), { protocol: c.protocol, baseUrl: c.baseUrl, apiKey });
738
+ if (existing) {
739
+ console.log(`${pc.yellow(t("import.skip"))} ${pc.bold(c.id)} · ${c.protocol} · ${c.baseUrl} — ${t("import.already", { id: pc.bold(existing.id) })}`);
740
+ continue;
741
+ }
751
742
  let ids = [...c.models];
752
743
  if (ids.length === 0) {
753
744
  process.stderr.write(pc.dim(`${t("import.discovering", { id: c.id })}\n`));
@@ -761,9 +752,7 @@ export async function cmdImport(opts) {
761
752
  }
762
753
  if (ids.length === 0)
763
754
  fail(t("import.noModelsImport", { id: c.id }));
764
- let id = c.id;
765
- for (let n = 2; store.providers[id]; n++)
766
- id = `${c.id}-${n}`;
755
+ const id = availableProviderId(c.id, store.providers);
767
756
  const hint = guessProviderHint(catalog, c.baseUrl);
768
757
  const models = enrichModels(catalog, ids, hint);
769
758
  const defaultModel = c.defaultModel && ids.includes(c.defaultModel) ? c.defaultModel : ids[0];
@@ -785,5 +774,6 @@ export async function cmdImport(opts) {
785
774
  console.log(`${pc.green(t(existed ? "import.updated" : "import.imported"))} ${pc.bold(id)} · ${c.protocol} · ${c.baseUrl} · ${t("import.modelsCount", { count: ids.length })} [${t("import.from")} ${c.sources.join(", ")}]`);
786
775
  }
787
776
  saveStore(store);
788
- console.log(pc.dim(`\n${t("import.next", { id: imported[0] })}`));
777
+ if (imported.length)
778
+ console.log(pc.dim(`\n${t("import.next", { id: imported[0] })}`));
789
779
  }
@@ -0,0 +1,138 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { backupsDir } from "./fsutil.js";
5
+ function snapshot(file) {
6
+ try {
7
+ const stat = fs.lstatSync(file);
8
+ if (!stat.isFile())
9
+ throw new Error("not a regular file");
10
+ return { text: fs.readFileSync(file, "utf8"), mode: stat.mode & 0o777, identity: `${stat.dev}:${stat.ino}` };
11
+ }
12
+ catch (error) {
13
+ if (error.code === "ENOENT")
14
+ return { text: undefined };
15
+ throw new Error(`${file}: cannot read a regular configuration file`);
16
+ }
17
+ }
18
+ function atomicWrite(file, text, mode) {
19
+ const temp = path.join(path.dirname(file), `.${path.basename(file)}.agentsw-${randomUUID()}.tmp`);
20
+ try {
21
+ fs.writeFileSync(temp, text, { flag: "wx", mode });
22
+ fs.chmodSync(temp, mode);
23
+ fs.renameSync(temp, file);
24
+ }
25
+ finally {
26
+ fs.rmSync(temp, { force: true });
27
+ }
28
+ }
29
+ /** Validate the entire plan and save every original before mutating any configuration. */
30
+ export function commitFileChanges(changes, opts = {}) {
31
+ const paths = new Set();
32
+ const identities = new Set();
33
+ const plan = changes.map((change) => {
34
+ const file = path.resolve(change.file);
35
+ // Resolve the parent too, so directory symlinks cannot disguise duplicate plans.
36
+ let canonical = file;
37
+ try {
38
+ canonical = path.join(fs.realpathSync(path.dirname(file)), path.basename(file));
39
+ }
40
+ catch (error) {
41
+ if (error.code !== "ENOENT") {
42
+ throw new Error(`${file}: cannot resolve configuration directory`);
43
+ }
44
+ }
45
+ if (paths.has(canonical))
46
+ throw new Error(`${file}: duplicate file plan`);
47
+ paths.add(canonical);
48
+ const current = snapshot(file);
49
+ if (current.identity && identities.has(current.identity))
50
+ throw new Error(`${file}: duplicate file plan`);
51
+ if (current.identity)
52
+ identities.add(current.identity);
53
+ if (current.text !== change.before)
54
+ throw new Error(`${file}: configuration changed since it was read`);
55
+ if (change.mode !== undefined && (!Number.isInteger(change.mode) || change.mode < 0 || change.mode > 0o777)) {
56
+ throw new Error(`${file}: invalid file permissions`);
57
+ }
58
+ return { ...change, file, originalMode: current.mode };
59
+ }).filter((change) => change.before !== change.after);
60
+ const files = plan.map((change) => change.file);
61
+ if (!plan.length || opts.dryRun)
62
+ return { files };
63
+ let backupDir;
64
+ let currentFile = backupsDir;
65
+ try {
66
+ fs.mkdirSync(backupsDir, { recursive: true, mode: 0o700 });
67
+ backupDir = fs.mkdtempSync(path.join(backupsDir, "transaction-"));
68
+ fs.chmodSync(backupDir, 0o700);
69
+ for (const [index, change] of plan.entries()) {
70
+ currentFile = change.file;
71
+ if (change.before === undefined)
72
+ continue;
73
+ const backup = path.join(backupDir, `${index}-${path.basename(change.file)}`);
74
+ fs.writeFileSync(backup, change.before, { flag: "wx", mode: 0o600 });
75
+ fs.chmodSync(backup, 0o600);
76
+ }
77
+ fs.writeFileSync(path.join(backupDir, "manifest.json"), JSON.stringify(plan.map((change, index) => ({
78
+ file: change.file,
79
+ backup: change.before === undefined ? null : `${index}-${path.basename(change.file)}`,
80
+ mode: change.originalMode,
81
+ })), null, 2) + "\n", { flag: "wx", mode: 0o600 });
82
+ for (const change of plan) {
83
+ currentFile = change.file;
84
+ if (snapshot(change.file).text !== change.before)
85
+ throw new Error("stale snapshot");
86
+ }
87
+ }
88
+ catch {
89
+ throw new Error(`${currentFile}: backup or pre-write validation failed; configuration files were not changed`);
90
+ }
91
+ const applied = [];
92
+ const createdDirectories = [];
93
+ try {
94
+ for (const change of plan) {
95
+ currentFile = change.file;
96
+ if (snapshot(change.file).text !== change.before)
97
+ throw new Error("stale snapshot");
98
+ if (change.after === undefined)
99
+ fs.unlinkSync(change.file);
100
+ else {
101
+ const missing = [];
102
+ for (let dir = path.dirname(change.file); !fs.existsSync(dir); dir = path.dirname(dir))
103
+ missing.push(dir);
104
+ for (const dir of missing.reverse()) {
105
+ fs.mkdirSync(dir, { mode: 0o700 });
106
+ createdDirectories.push(dir);
107
+ }
108
+ atomicWrite(change.file, change.after, change.mode ?? change.originalMode ?? 0o600);
109
+ }
110
+ applied.push(change);
111
+ }
112
+ }
113
+ catch {
114
+ const failed = [];
115
+ for (const change of applied.reverse()) {
116
+ try {
117
+ // Do not overwrite a third party's intervening edit while rolling back.
118
+ if (snapshot(change.file).text !== change.after)
119
+ throw new Error("stale rollback snapshot");
120
+ if (change.before === undefined)
121
+ fs.unlinkSync(change.file);
122
+ else
123
+ atomicWrite(change.file, change.before, change.originalMode ?? 0o600);
124
+ }
125
+ catch {
126
+ failed.push(change.file);
127
+ }
128
+ }
129
+ for (const dir of createdDirectories.reverse()) {
130
+ try {
131
+ fs.rmdirSync(dir);
132
+ }
133
+ catch { /* Only remove empty directories created by this transaction. */ }
134
+ }
135
+ throw new Error(`${currentFile}: configuration transaction failed; ${failed.length ? `rollback incomplete for ${failed.join(", ")}` : "previous writes rolled back"}; backups: ${backupDir}`);
136
+ }
137
+ return { files, backupDir };
138
+ }
@@ -0,0 +1,27 @@
1
+ /** Tokenize complete dotenv assignments, including quoted multiline values. */
2
+ export function envAssignments(file, text) {
3
+ const result = [];
4
+ const pattern = /^([\t ]*(?:export[\t ]+)?)([A-Za-z_][A-Za-z0-9_]*)([\t ]*=[\t ]*)("(?:\\[\s\S]|[^"\\])*"|'[^']*'|[^\r\n]*)(?:[\t ]*#[^\r\n]*)?[\t ]*(?:\r?\n|$)/gm;
5
+ let end = 0;
6
+ const trivia = (text) => text.split(/\r?\n/).every((line) => !line.trim() || line.trimStart().startsWith("#"));
7
+ for (const match of text.matchAll(pattern)) {
8
+ if (!trivia(text.slice(end, match.index)))
9
+ throw new Error(`${file}: invalid environment configuration`);
10
+ let value = match[4];
11
+ if (value.startsWith('"') || value.startsWith("'")) {
12
+ if (value.at(-1) !== value[0])
13
+ throw new Error(`${file}: invalid quoted environment value`);
14
+ const quote = value[0];
15
+ value = value.slice(1, -1);
16
+ if (quote === '"')
17
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r");
18
+ }
19
+ else
20
+ value = value.split("#", 1)[0].trim();
21
+ result.push({ name: match[2], offset: match.index + match[1].length, value, start: match.index, length: match[0].length });
22
+ end = match.index + match[0].length;
23
+ }
24
+ if (!trivia(text.slice(end)))
25
+ throw new Error(`${file}: invalid environment configuration`);
26
+ return result;
27
+ }
package/dist/i18n.js CHANGED
@@ -8,9 +8,10 @@ const messages = {
8
8
  "error.unknownCommand": { en: "unknown command '{value}', see --help", "zh-CN": "未知命令 '{value}',请查看 --help" },
9
9
  "cmd.add": { en: "add or update a provider (interactive when flags are omitted)", "zh-CN": "添加或更新供应商(省略参数时进入交互流程)" },
10
10
  "cmd.quick": { en: "quick add — auto-detect protocol(s) and models from just base URL + API key", "zh-CN": "快速添加 — 只需 base URL + API key,自动探测协议和模型" },
11
- "cmd.list": { en: "list configured providers", "zh-CN": "列出已配置供应商" },
12
- "cmd.remove": { en: "remove a provider", "zh-CN": "删除供应商" },
11
+ "cmd.list": { en: "list configured providers (or agent-local entries with --apps)", "zh-CN": "列出已配置供应商(--apps 可查看智能体独有配置)" },
12
+ "cmd.remove": { en: "remove a store provider, or only agent-local entries with --apps", "zh-CN": "删除中央供应商,或用 --apps 仅删除指定智能体的供应商" },
13
13
  "cmd.prune": { en: "remove a provider's entries from app configs (keeps it in agentsw)", "zh-CN": "从应用配置移除供应商条目(仍保留在 agentsw)" },
14
+ "cmd.rename": { en: "rename a provider and migrate config references with backups", "zh-CN": "重命名供应商并迁移配置引用(自动备份)" },
14
15
  "cmd.use": { en: "set the active provider and write it into app configs", "zh-CN": "设为当前供应商并写入应用配置" },
15
16
  "cmd.sync": { en: "re-apply the active provider (or --provider) to app configs", "zh-CN": "将当前供应商(或 --provider)重新应用到应用配置" },
16
17
  "cmd.status": { en: "show detected apps and what they currently point at", "zh-CN": "显示检测到的应用及当前供应商" },
@@ -21,7 +22,7 @@ const messages = {
21
22
  "cmd.install": { en: "install an agent CLI", "zh-CN": "安装智能体 CLI" },
22
23
  "cmd.upgrade": { en: "upgrade agent CLIs (no args = everything outdated)", "zh-CN": "升级智能体 CLI(无参数时升级全部过期项)" },
23
24
  "cmd.refresh": { en: "re-fetch models.dev metadata for all providers", "zh-CN": "重新拉取所有供应商的 models.dev 元数据" },
24
- "opt.id": { en: "provider id, e.g. glm, my-proxy", "zh-CN": "供应商 id,例如 glm、my-proxy" },
25
+ "opt.id": { en: "custom provider id (default: full hostname + protocol)", "zh-CN": "自定义供应商 ID(默认:完整域名 + 协议)" },
25
26
  "opt.name": { en: "display name", "zh-CN": "显示名称" },
26
27
  "opt.protocol": { en: "wire protocol: openai | anthropic", "zh-CN": "接口协议: openai | anthropic" },
27
28
  "opt.openaiApi": {
@@ -42,6 +43,8 @@ const messages = {
42
43
  "opt.yes": { en: "non-interactive; require all flags", "zh-CN": "非交互模式;要求提供全部参数" },
43
44
  "opt.prune": { en: "also remove entries from app configs", "zh-CN": "同时从应用配置中移除条目" },
44
45
  "opt.apps": { en: "comma-separated apps or 'all'", "zh-CN": "逗号分隔的应用或 'all'" },
46
+ "opt.removeApps": { en: "delete only from these apps; keep the agentsw store and other apps unchanged", "zh-CN": "仅从这些智能体删除,保留 agentsw 中央配置和其他智能体配置" },
47
+ "opt.manageDryRun": { en: "preview affected paths without changing files or creating backups", "zh-CN": "仅预览受影响路径,不修改文件或创建备份" },
45
48
  "opt.appsDetailed": { en: "comma-separated apps (claude,codex,omp,pi,prime,opencode,hermes,workbuddy) or 'all'", "zh-CN": "逗号分隔的应用(claude,codex,omp,pi,prime,opencode,hermes,workbuddy)或 'all'" },
46
49
  "opt.model": { en: "override default model while switching", "zh-CN": "切换时覆盖默认模型" },
47
50
  "opt.dryRun": { en: "preview config diff without writing", "zh-CN": "仅预览配置差异,不写入" },
@@ -60,50 +63,70 @@ const messages = {
60
63
  "menu.selectInstructions": { en: "↑/↓ move · Enter select", "zh-CN": "↑/↓ 移动 · Enter 选择" },
61
64
  "import.multiInstructions": { en: "↑/↓ move · Space toggle · Enter confirm", "zh-CN": "↑/↓ 移动 · 空格切换 · Enter 确认" },
62
65
  "menu.title": { en: " · interactive menu — Ctrl+C quits, ↑/↓ selects", "zh-CN": " · 交互菜单 — Ctrl+C 退出,↑/↓ 选择" },
63
- "menu.noProviders": { en: "no providers configured yet", "zh-CN": "尚未配置供应商" },
64
- "menu.noProvidersHint": { en: "no providers configured — choose import existing providers or add/update provider first", "zh-CN": "尚未配置供应商——请先选择导入已有供应商或添加/更新供应商" },
65
- "menu.firstScan": { en: "scan existing app configs and import custom providers now?", "zh-CN": "现在扫描应用配置并导入已有自定义供应商?" },
66
+ "menu.noProviders": { en: "no providers saved in agentsw yet", "zh-CN": "agentsw 中尚未保存供应商" },
67
+ "menu.noProvidersHint": { en: "no providers saved in agentsw — choose Add provider or Import existing providers first", "zh-CN": "agentsw 中尚未保存供应商,请先选择“添加供应商”或“导入已有供应商”" },
68
+ "menu.firstScan": { en: "import providers from existing agent configs into agentsw?", "zh-CN": "是否从已有智能体配置中导入供应商到 agentsw?" },
66
69
  "menu.what": { en: "what to do?", "zh-CN": "请选择操作" },
67
- "menu.add": { en: "add / update provider (id · protocol · base URL · API key)", "zh-CN": "添加 / 更新供应商 (id · 协议 · base URL · API key)" },
68
- "menu.import": { en: "import existing providers (scan app configs · auto-dedupe)", "zh-CN": "导入已有供应商 (扫描应用配置 · 自动去重)" },
69
- "menu.use": { en: "switch provider (use and sync into app configs)", "zh-CN": "切换供应商 (写入并同步到应用配置)" },
70
- "menu.status": { en: "status (what each app currently points at)", "zh-CN": "查看状态 (各应用当前指向)" },
71
- "menu.list": { en: "list providers", "zh-CN": "列出供应商" },
72
- "menu.sync": { en: "sync active provider (re-apply to app configs)", "zh-CN": "同步当前供应商 (重新应用到应用配置)" },
73
- "menu.discover": { en: "discover models (refresh provider list + metadata)", "zh-CN": "发现模型 (刷新供应商模型与元数据)" },
74
- "menu.remove": { en: "remove provider", "zh-CN": "删除供应商" },
75
- "menu.apps": { en: "agent versions (check installed CLIs / upgrade)", "zh-CN": "智能体版本 (检查已安装 CLI / 升级)" },
76
- "menu.language": { en: "language / 语言", "zh-CN": "语言 / Language" },
77
- "menu.quit": { en: "quit", "zh-CN": "退出" },
70
+ "menu.add": { en: "Add / update provider (manual setup)", "zh-CN": "添加或更新供应商(手动设置)" },
71
+ "menu.import": { en: "Import existing providers", "zh-CN": "导入已有供应商" },
72
+ "menu.use": { en: "Switch provider and default model", "zh-CN": "切换供应商和默认模型" },
73
+ "menu.status": { en: "View each agent's current configuration", "zh-CN": "查看各智能体当前配置" },
74
+ "menu.list": { en: "View providers saved in agentsw", "zh-CN": "查看 agentsw 供应商列表" },
75
+ "menu.sync": { en: "Re-sync the current provider to agents", "zh-CN": "重新同步当前供应商" },
76
+ "menu.discover": { en: "Update a provider's model list", "zh-CN": "更新供应商模型列表" },
77
+ "menu.remove": { en: "Delete provider configuration", "zh-CN": "删除供应商配置" },
78
+ "menu.rename": { en: "Change a provider ID", "zh-CN": "修改供应商 ID" },
79
+ "menu.renameProvider": { en: "which provider ID should be changed?", "zh-CN": "选择要修改 ID 的供应商" },
80
+ "menu.newId": { en: "new provider ID (used in config references)", "zh-CN": "新的供应商 ID(用于配置引用)" },
81
+ "menu.renameConfirm": { en: "change provider ID from {oldId} to {newId} and update agent config references? Files are backed up first.", "zh-CN": "将供应商 ID 从 {oldId} 改为 {newId},并更新智能体中的关联配置?修改前自动备份。" },
82
+ "menu.removeScope": { en: "where should this provider be removed?", "zh-CN": "从哪里删除供应商?" },
83
+ "menu.removeStore": { en: "Delete only the record in agentsw", "zh-CN": "只删除 agentsw 中的记录" },
84
+ "menu.removeEverywhere": { en: "Delete the agentsw record and matching agent configs", "zh-CN": "同时删除 agentsw 记录和智能体配置" },
85
+ "menu.removeLocal": { en: "Delete only one agent's configuration", "zh-CN": "只删除某个智能体中的配置" },
86
+ "menu.noRemovable": { en: "no provider records found here; return to the menu to choose another scope", "zh-CN": "所选位置没有供应商记录,请返回菜单选择其他删除范围" },
87
+ "menu.removeApp": { en: "which agent's configuration should be changed?", "zh-CN": "选择要删除配置的智能体" },
88
+ "menu.removeStoreHelp": { en: "Keep all agent configs; the agents can still use this provider.", "zh-CN": "保留各智能体配置,它们仍可继续使用该供应商。" },
89
+ "menu.removeEverywhereHelp": { en: "Remove the saved provider and matching agent entries, including related default selections.", "zh-CN": "删除已保存的供应商及各智能体中匹配的配置,并清理相关默认选择。" },
90
+ "menu.removeLocalHelp": { en: "Keep agentsw and other agents unchanged; also works for providers never imported into agentsw.", "zh-CN": "保留 agentsw 和其他智能体;也支持从未导入 agentsw 的供应商。" },
91
+ "menu.removeConfirmStore": { en: "delete {id} from agentsw only? All agent configs stay unchanged. Files are backed up first.", "zh-CN": "确认只删除 agentsw 中的供应商 {id}?各智能体配置保持不变,修改前自动备份。" },
92
+ "menu.removeConfirmEverywhere": { en: "delete {id} from agentsw and matching agent configs? Related default selections will also be cleared. Files are backed up first.", "zh-CN": "确认删除 {id} 的 agentsw 记录和匹配的智能体配置?相关默认选择会一并清理,修改前自动备份。" },
93
+ "menu.removeConfirmLocal": { en: "delete {id} only from {app}? Keep agentsw and other agents unchanged; syncing later may add it back. Files are backed up first.", "zh-CN": "仅从 {app} 删除 {id}?保留 agentsw 和其他智能体配置,再次同步可能恢复。修改前自动备份。" },
94
+ "menu.confirmRename": { en: "Change ID", "zh-CN": "确认修改 ID" },
95
+ "menu.confirmRemove": { en: "Delete configuration", "zh-CN": "确认删除" },
96
+ "menu.cancelAction": { en: "Cancel, return to menu", "zh-CN": "取消,返回菜单" },
97
+ "menu.apps": { en: "Check agent versions and updates", "zh-CN": "检查智能体版本和更新" },
98
+ "menu.language": { en: "Change language / 语言", "zh-CN": "切换界面语言 / Language" },
99
+ "menu.quit": { en: "Exit menu", "zh-CN": "退出菜单" },
78
100
  "menu.yes": { en: "yes", "zh-CN": "是" },
79
101
  "menu.no": { en: "no", "zh-CN": "否" },
80
- "menu.modelSource": { en: "how to fill the model list?", "zh-CN": "如何填写模型列表?" },
81
- "menu.modelDiscover": { en: "discover from the provider's /v1/models (recommended)", "zh-CN": "从供应商 /v1/models 自动发现(推荐)" },
82
- "menu.modelManual": { en: "type model ids manually", "zh-CN": "手动输入模型 id" },
83
- "menu.pickProvider": { en: "switch to provider", "zh-CN": "切换到供应商" },
102
+ "menu.modelSource": { en: "how should the model list be obtained?", "zh-CN": "选择模型列表的获取方式" },
103
+ "menu.modelDiscover": { en: "Fetch from the provider API (recommended)", "zh-CN": "从供应商接口自动获取(推荐)" },
104
+ "menu.modelManual": { en: "Enter model IDs manually", "zh-CN": "手动输入模型 ID" },
105
+ "menu.pickProvider": { en: "which provider should agents switch to?", "zh-CN": "选择要切换到的供应商" },
84
106
  "menu.defaultModel": { en: "default model", "zh-CN": "默认模型" },
85
107
  "menu.keepDefault": { en: "keep current default ({model})", "zh-CN": "保持当前默认模型({model})" },
86
- "menu.active": { en: "active", "zh-CN": "当前" },
87
- "menu.discoverFor": { en: "discover models for", "zh-CN": "为供应商发现模型" },
88
- "menu.pushRefresh": { en: "push refreshed provider into app configs?", "zh-CN": "将刷新后的供应商推送到应用配置?" },
89
- "menu.removeProvider": { en: "remove provider", "zh-CN": "删除供应商" },
108
+ "menu.active": { en: "current in agentsw", "zh-CN": "agentsw 当前" },
109
+ "menu.discoverFor": { en: "which provider's model list should be updated?", "zh-CN": "选择要更新模型列表的供应商" },
110
+ "menu.pushRefresh": { en: "also sync this provider to agent configs after updating its model list?", "zh-CN": "更新模型列表后,是否同时将该供应商同步到智能体配置?" },
111
+ "menu.removeProvider": { en: "which provider configuration should be deleted?", "zh-CN": "选择要删除配置的供应商" },
90
112
  "menu.reallyRemove": { en: "really remove {id}?", "zh-CN": "确认删除 {id}?" },
91
113
  "menu.pruneConfigs": { en: "also remove its entries from app configs?", "zh-CN": "同时从应用配置中移除其条目?" },
92
- "menu.upgrade": { en: "upgrade everything outdated?", "zh-CN": "升级全部过期项?" },
93
- "menu.installApp": { en: "install an agent", "zh-CN": "安装智能体" },
114
+ "menu.upgrade": { en: "upgrade all installed agents that have updates?", "zh-CN": "是否升级所有有新版本的已安装智能体?" },
115
+ "menu.installApp": { en: "Install a new coding agent", "zh-CN": "安装新的智能体" },
94
116
  "menu.pickApp": { en: "which agent to install?", "zh-CN": "安装哪个智能体?" },
95
117
  "menu.installConfirm": { en: "install {name} via: {cmd}?", "zh-CN": "通过以下命令安装 {name}: {cmd}?" },
96
118
  "menu.allInstalled": { en: "all agents already installed", "zh-CN": "所有智能体均已安装" },
97
119
  "menu.bye": { en: "bye", "zh-CN": "再见" },
98
120
  "add.id": { en: "provider id (slug)", "zh-CN": "供应商 id(slug)" },
99
- "add.idInvalid": { en: "lowercase slug, e.g. glm or my-proxy", "zh-CN": "请用小写 slug,例如 glm 或 my-proxy" },
121
+ "add.idAuto": { en: "provider id (leave blank for hostname + protocol)", "zh-CN": "供应商 ID(留空使用完整域名 + 协议)" },
122
+ "add.idInvalid": { en: "start with a lowercase letter or digit; use only lowercase letters, digits, - and _, e.g. my-proxy", "zh-CN": "以小写字母或数字开头,仅含小写字母、数字、-、_,如 my-proxy" },
100
123
  "add.name": { en: "display name", "zh-CN": "显示名称" },
101
124
  "add.protocol": { en: "wire protocol", "zh-CN": "接口协议" },
102
125
  "add.openai": { en: "openai (chat completions)", "zh-CN": "openai (chat completions)" },
103
126
  "add.anthropic": { en: "anthropic (messages)", "zh-CN": "anthropic (messages)" },
104
- "add.baseUrl": { en: "base URL", "zh-CN": "base URL" },
127
+ "add.baseUrl": { en: "API base URL", "zh-CN": "接口地址(Base URL)" },
105
128
  "add.baseUrlInvalid": { en: "must start with http(s)://", "zh-CN": "必须以 http(s):// 开头" },
106
- "add.apiKey": { en: "API key", "zh-CN": "API key" },
129
+ "add.apiKey": { en: "API key", "zh-CN": "API 密钥(API key)" },
107
130
  "add.models": { en: "model ids (comma separated, or leave blank to auto-discover)", "zh-CN": "模型 id(逗号分隔,留空则自动发现)" },
108
131
  "add.cancelled": { en: "cancelled", "zh-CN": "已取消" },
109
132
  "import.already": { en: "already configured as {id}", "zh-CN": "已配置为 {id}" },
@@ -155,7 +178,21 @@ const messages = {
155
178
  "quick.noProtocol": { en: "no supported protocol found at this endpoint (tried openai and anthropic)", "zh-CN": "该端点未检测到支持的协议(已尝试 openai 和 anthropic)" },
156
179
  "quick.noModelsAfterFilter": { en: "{id}: no models left after filtering, skipping", "zh-CN": "{id}: 过滤后无剩余模型,跳过" },
157
180
  "quick.summary": { en: "created {count} provider(s): {ids}", "zh-CN": "创建了 {count} 个供应商: {ids}" },
158
- "menu.quickAdd": { en: "quick add (auto-detect protocol + models)", "zh-CN": "快速添加(自动探测协议 + 模型)" },
181
+ "menu.quickAdd": { en: "Add provider (auto-detect)", "zh-CN": "添加供应商(自动识别)" },
182
+ "menu.quickAddHelp": { en: "Recommended: enter an API URL and key; detect protocols and models automatically.", "zh-CN": "推荐:只填接口地址和 API key,自动识别协议与模型。" },
183
+ "menu.addHelp": { en: "Choose the protocol and model setup yourself; use an existing ID to update a provider.", "zh-CN": "自行选择协议和模型获取方式;填写已有 ID 可更新供应商。" },
184
+ "menu.importHelp": { en: "Read providers from agents or cc-switch into agentsw; leave source configs unchanged.", "zh-CN": "从智能体或 cc-switch 读取配置并保存到 agentsw,不修改来源配置。" },
185
+ "menu.useHelp": { en: "Select a provider and default model, then write them to compatible agent configs.", "zh-CN": "选择供应商和默认模型,然后写入支持该协议的智能体配置。" },
186
+ "menu.statusHelp": { en: "Show detected agents, their current providers and config file locations.", "zh-CN": "查看检测到的智能体、所用供应商及配置文件位置。" },
187
+ "menu.listHelp": { en: "Show only the providers, protocols and default models saved in agentsw.", "zh-CN": "只查看 agentsw 已保存的供应商、协议和默认模型。" },
188
+ "menu.syncHelp": { en: "Write the current agentsw provider to agent configs without fetching a new model list.", "zh-CN": "将 agentsw 当前供应商写入智能体配置,不重新获取模型列表。" },
189
+ "menu.discoverHelp": { en: "Fetch models from the provider, fill in model metadata, and optionally sync to agents.", "zh-CN": "从供应商接口重新获取模型并补全模型参数,可选择同步到智能体。" },
190
+ "menu.renameHelp": { en: "Change the config ID and update references; keep custom display names.", "zh-CN": "修改配置中的标识并更新关联引用,保留自定义显示名称。" },
191
+ "menu.removeHelp": { en: "Choose where to delete: agentsw only, one agent only, or both.", "zh-CN": "先选择删除范围:仅 agentsw、仅某个智能体,或两处一起。" },
192
+ "menu.appsHelp": { en: "Compare installed and latest versions, then choose whether to upgrade.", "zh-CN": "对比已安装版本和最新版本,再选择是否升级。" },
193
+ "menu.installAppHelp": { en: "Choose an agent that is not installed yet, such as Claude Code, Codex or omp.", "zh-CN": "选择尚未安装的编码智能体,如 Claude Code、Codex 或 omp。" },
194
+ "menu.languageHelp": { en: "Choose English or 简体中文 and save the language for future runs.", "zh-CN": "选择简体中文或 English,保存为后续运行的界面语言。" },
195
+ "menu.quitHelp": { en: "End this menu session without performing another action.", "zh-CN": "结束本次交互,不执行其他操作。" },
159
196
  "import.skip": { en: "skip", "zh-CN": "跳过" },
160
197
  "import.modelsCount": { en: "{count} models", "zh-CN": "{count} 个模型" },
161
198
  "table.protocol": { en: "PROTOCOL", "zh-CN": "协议" },
@@ -171,6 +208,12 @@ const messages = {
171
208
  "remove.pruning": { en: "pruning {id} from app configs", "zh-CN": "正在从应用配置清理 {id}" },
172
209
  "remove.removed": { en: "removed provider {id}", "zh-CN": "已删除供应商 {id}" },
173
210
  "remove.note": { en: "note: app configs are unchanged; use --prune to clean them", "zh-CN": "注意:应用配置未改动;可使用 --prune 清理" },
211
+ "manage.preview": { en: "preview: {count} file(s) would change", "zh-CN": "预览:将修改 {count} 个文件" },
212
+ "manage.changed": { en: "updated {count} file(s)", "zh-CN": "已更新 {count} 个文件" },
213
+ "manage.backup": { en: "backup directory: {path}", "zh-CN": "备份目录:{path}" },
214
+ "rename.done": { en: "renamed {oldId} to {newId}", "zh-CN": "已将 {oldId} 重命名为 {newId}" },
215
+ "remove.localDone": { en: "removed {id} only from {apps}; agentsw and other apps are unchanged", "zh-CN": "已仅从 {apps} 删除 {id};agentsw 和其他智能体保持不变" },
216
+ "list.localNone": { en: "no agent-local provider entries found", "zh-CN": "未发现智能体本地供应商条目" },
174
217
  "common.skip": { en: "skip", "zh-CN": "跳过" },
175
218
  "common.yes": { en: "yes", "zh-CN": "是" },
176
219
  "common.no": { en: "no", "zh-CN": "否" },