agentsw 0.7.1 → 0.8.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/README.md CHANGED
@@ -23,7 +23,7 @@ switching to vfing (openai) · default model glm-5.3-flash
23
23
 
24
24
  skip claude Claude Code does not support openai-protocol providers
25
25
  ok codex ~/.codex/config.toml, ~/.codex/auth.json
26
- backup: ~/.config/agentsw/backups/config.toml.2026-08-31T15-58-24-678Z
26
+ backup: ~/.config/agentsw/backups/transaction-<unique>
27
27
  ok omp ~/.omp/agent/models.yml
28
28
  select in omp with: omp --model vfing/glm-5.3-flash
29
29
  skip pi pi not detected (pass --apps pi to force)
@@ -39,18 +39,18 @@ ok dsh ~/.dsh/settings.yaml, ~/.dsh/.credentials.yaml
39
39
  - **It runs where a desktop app cannot.** Headless servers, containers, devcontainers, CI.
40
40
  `npx agentsw use myproxy -a codex,omp` is one line in a provisioning script; nine GUI
41
41
  clicks are not.
42
- - **`--dry-run` prints diffs.** Review what a switch will write before it writes it, or pipe
43
- it into a code review. Every file is backed up first regardless.
42
+ - **`--dry-run` prints redacted diffs.** Preview changes without writing files or creating
43
+ backups. Existing configuration files are backed up when actual changes are committed.
44
44
  - **Non-destructive by design.** Only the fields agentsw owns are rewritten. Provider-level
45
45
  keys it does not model — `authHeader`, `headers`, `compat`, `discovery` — per-model extras
46
46
  like `thinkingLevelMap`, and YAML comments all survive a re-sync.
47
47
  - **Model metadata, not just an id list.** Reseller `/v1/models` endpoints return bare ids.
48
- agentsw enriches them from [models.dev](https://models.dev) and writes what each app
48
+ agentsw enriches them from [models.dev](https://models.dev), automatically fills meaningful gaps from AI Gateway when needed, and writes what each app
49
49
  actually reads: `thinkingLevelMap` for pi/prime, `limit`/`attachment` for opencode,
50
50
  `contextWindow`/`maxTokens` for omp, `reasoningEfforts`/`input` for dsh.
51
51
  - **Adopts what you already have** — including [cc-switch](https://github.com/farion1231/cc-switch).
52
52
  `import` scans every installed agent *and* cc-switch's own store, merges duplicates
53
- (same protocol + base URL), and never writes back to either.
53
+ (same protocol + normalized endpoint + credentials), and never writes back to either.
54
54
  - **Wire-aware.** `/v1/chat/completions` and `/v1/responses` are different endpoints;
55
55
  agentsw tracks which one a provider speaks and never downgrades a working one.
56
56
 
@@ -62,7 +62,7 @@ imports from it rather than competing with it.
62
62
 
63
63
  ```bash
64
64
  npx agentsw # zero install: opens the interactive menu
65
- npm install -g agentsw # requires Node >= 22.12
65
+ npm install -g agentsw # requires Node >= 22.13 (built-in SQLite without an extra flag)
66
66
  ```
67
67
 
68
68
  The provider sync and config management work on Linux, macOS, and Windows. On Windows,
@@ -139,13 +139,15 @@ imported sub · openai · https://new.vfing.de/v1 · 18 models [from omp]
139
139
  imported zhipu-glm · anthropic · https://open.bigmodel.cn/api/anthropic · 1 models [from cc-switch]
140
140
  ```
141
141
 
142
- Without `--all` the list is a multi-select preview; nothing is written until you pick.
142
+ In an interactive terminal, omitting `--all` opens a multi-select preview before provider
143
+ records are saved. `--all`, or non-interactive stdin, imports all eligible new entries;
144
+ use `--all` explicitly in scripts. Model discovery may update the local metadata cache.
143
145
 
144
146
  ## Model discovery filters
145
147
 
146
148
  `--discover` lists model ids from the provider's `/v1/models`, then enriches each id from
147
- models.dev. Reseller lists are noisy, so filters are persisted per provider and re-applied on
148
- every `discover`:
149
+ models.dev first, with automatic AI Gateway supplementation when needed. Reseller lists are noisy,
150
+ so filters are persisted per provider and re-applied on every `discover`:
149
151
 
150
152
  ```bash
151
153
  # snapshot duplicates (gpt-5.2-latest, glm-4.7-250414, ...) are DROPPED by default
@@ -158,6 +160,77 @@ asw discover myproxy --no-filter # clear it
158
160
 
159
161
  Explicit `--models` entries and the default model are never filtered out.
160
162
 
163
+ ## Automatic model metadata
164
+
165
+ **No extra flags are needed.** New providers and providers without a saved mode use `auto`:
166
+ models.dev is checked first, then the public
167
+ [AI Gateway model catalog](https://vercel.com/docs/ai-gateway/models-and-providers#dynamic-model-discovery)
168
+ is consulted only for missing core parameters (context window, maximum output, reasoning or image input),
169
+ to refresh unchanged Gateway-supplied fields that models.dev has not replaced, or to verify an identity
170
+ conflict in previously tracked automatic values. Missing names, prices, optional input limits or
171
+ reasoning-effort lists alone do not trigger a Gateway lookup.
172
+ agentsw directly fetches `https://ai-gateway.vercel.sh/v1/models` without provider credentials or an AI SDK dependency.
173
+
174
+ ```bash
175
+ # Refresh only saved models using the saved mode (auto if unset)
176
+ # No model discovery or agent config writes
177
+ asw refresh --provider myproxy
178
+
179
+ # Inspect effective mode, sources, conflicts and reference pricing (no API key)
180
+ asw models --provider myproxy --metadata
181
+
182
+ # Custom or ambiguous names need explicit aliases; wire model IDs stay unchanged
183
+ asw refresh --provider myproxy --metadata-mode auto \
184
+ --gateway-models '{"my-model":"openai/gpt-5.4"}'
185
+
186
+ # Always consult Gateway, even when core parameters are complete
187
+ asw refresh --provider myproxy --metadata-mode on
188
+
189
+ # Stop future Gateway fetches; retain saved model parameters
190
+ asw refresh --provider myproxy --metadata-mode off
191
+
192
+ # Return an explicitly disabled provider to automatic mode
193
+ asw refresh --provider myproxy --metadata-mode auto
194
+
195
+ # Preview and apply separately; sync does not query catalogs
196
+ asw sync --provider myproxy --dry-run
197
+ asw sync --provider myproxy
198
+ ```
199
+
200
+ The menu's **Configure and refresh model metadata** action offers **Automatic (recommended)**,
201
+ **Always supplement**, and **Disabled**, with the provider's current mode selected. Onboarding has no extra prompts.
202
+ `add`, `quick`, `discover`, `import`, and `refresh` accept `--metadata-mode <auto|on|off>`.
203
+ The existing `--gateway-metadata` and `--no-gateway-metadata` flags remain explicit `on` and `off` choices.
204
+ Omitted options preserve saved settings: an existing `false` remains off until you select `auto` or `on`.
205
+ Invalid modes and conflicting mode/boolean flags are rejected before fetching or saving.
206
+ Without `--provider`, `refresh` applies to all providers, including any supplied mode.
207
+ `--gateway-models` replaces the whole alias map (`{}` clears it); aliases do not change the saved mode or override `off`.
208
+ Import still skips already configured accounts.
209
+
210
+ - **Field-level priority:** manual values > models.dev > Gateway gap filling. Tracked automatic values may
211
+ refresh; subsequent manual edits are retained. Legacy values without provenance are conservatively treated
212
+ as manual overrides. Unmodeled extension fields survive enrichment too.
213
+ - **Exact identities:** automatic lookup can map a bare, case-sensitive ID (no `/`) to a unique Gateway
214
+ `creator/model` identity when models.dev evidence is absent or agrees. Ambiguous creator evidence in either
215
+ catalog is rejected; custom or ambiguous names need explicit aliases. Qualified IDs never have their prefixes
216
+ stripped, and matching never guesses from substrings, case folding or versions. Auto/on mode also uses conservative
217
+ exact models.dev matching; off retains the legacy models.dev lookup. Unmatched models remain usable.
218
+ Alias changes clear unchanged automatic values tied to confirmed superseded identities, never manual edits.
219
+ - **No routing changes:** metadata never adds available models or changes IDs, protocols, URLs, defaults or
220
+ accounts. Gateway capabilities are reference specs, not proof of reseller support. `supported_specifications`
221
+ is not a URL version.
222
+ - **Reference-only prices:** Gateway prices are converted to USD per million tokens and stored separately in
223
+ `metadata.gateway.referenceCost`, not effective provider `cost` or runtime agent pricing. Variable/tiered
224
+ pricing is flagged separately.
225
+ Auto does not fetch solely to refresh this reference snapshot: check `metadata.gateway.fetchedAt`, or use
226
+ `refresh --provider <id> --metadata-mode on` to request fresh reference prices.
227
+ - **Auditable and optional:** `metadata.fields` records origins, value snapshots and timestamps;
228
+ `metadata.conflicts` records disagreements with retained values. This audit stays in agentsw, never runtime agent configs.
229
+ Gateway has a separate 24-hour cache; failures use stale cached data or skip supplementation.
230
+ - **Automatic lookup is not automatic sync:** enrichment updates saved metadata during add/quick/discover/import/refresh.
231
+ It does not add an automatic push to agents. Sync behavior is unchanged: ordinary `sync` only writes saved settings
232
+ and fetches neither model lists nor metadata catalogs.
233
+
161
234
  ## Commands
162
235
 
163
236
  | Command | What it does |
@@ -173,7 +246,8 @@ Explicit `--models` entries and the default model are never filtered out.
173
246
  | `sync` | re-apply the active provider (after an agent update, say) |
174
247
  | `discover <id> [--sync]` | refresh the model list + metadata from `/v1/models` |
175
248
  | `models [query]` | search the models.dev catalog |
176
- | `refresh` | re-fetch metadata for every configured provider |
249
+ | `refresh [--provider <id>]` | refresh saved metadata and optionally configure Gateway; keep the model list |
250
+ | `models --provider <id> --metadata` | inspect field sources, conflicts and reference pricing as JSON |
177
251
  | `prune <id>` / `remove <id> [--prune]` | remove from app configs / from the store |
178
252
  | `remove <id> --apps omp` | delete only the selected app's entry; supports `--dry-run` |
179
253
  | `apps` / `install <app>` / `upgrade` | agent version manager |
@@ -204,8 +278,18 @@ sync of a managed provider can add it back to that app.
204
278
 
205
279
  ## How your configs are treated
206
280
 
207
- - Every modified file is backed up first to `~/.config/agentsw/backups/`.
208
- - The provider store lives at `~/.config/agentsw/config.json` (mode 0600).
281
+ - Existing provider-store and agent configuration files are backed up before committed
282
+ changes to `~/.config/agentsw/backups/transaction-*`. New files have no prior copy;
283
+ cache refreshes are not configuration backups. Dry runs create neither files nor backups.
284
+ - Each adapter stages all its files before committing; failed writes trigger best-effort
285
+ rollback. A multi-agent sync is not one filesystem-wide transaction: successful agents
286
+ remain updated if another fails, and failures are reported with a nonzero exit status.
287
+ - The provider store lives at `~/.config/agentsw/config.json` (mode 0600). Concurrent stale
288
+ saves are rejected instead of overwriting newer records; retry after the other writer finishes.
289
+ - Existing file permissions are preserved and new configuration files default to 0600.
290
+ - A busy write lock reports its owner PID/time. If a writer crashed, stop all agentsw
291
+ writers and confirm the lock is abandoned before manually removing the reported
292
+ `.write.lock` file; active or unknown locks are never automatically stolen.
209
293
  - A sync overwrites only the fields agentsw owns. Unmodeled provider-level keys and per-model
210
294
  extras survive; an owned field that stops applying is cleared rather than left stale, and a
211
295
  per-model `api`/`baseUrl` override that contradicts the route is dropped (it would silently
@@ -219,8 +303,10 @@ sync of a managed provider can add it back to that app.
219
303
  - Codex only speaks the Responses API; a chat-completions-only endpoint cannot be used with
220
304
  it, and the sync output says so.
221
305
  - DeepSeek Harness keeps secrets out of `settings.yaml`: the route carries
222
- `apiKeyEnv: AGENTSW_<ID>_API_KEY`, the key goes into `$DSH_HOME/.credentials.yaml`
306
+ `apiKeyEnv: AGENTSW_<ID>_<DIGEST>_API_KEY`, the key goes into `$DSH_HOME/.credentials.yaml`
223
307
  (`refs:`, mode 0600). `$DSH_HOME` defaults to `~/.dsh`.
308
+ The digest distinguishes IDs such as `foo-bar` and `foo_bar`; legacy generated references
309
+ remain recognizable. External/custom references are not renamed or removed indiscriminately.
224
310
  - cc-switch's database is opened read-only and never written to.
225
311
  - The apps manager supports native Windows commands for Claude Code, Codex CLI, pi,
226
312
  opencode, Hermes, and DeepSeek Harness. Oh My Pi and prime-agent still need their own
@@ -250,8 +336,13 @@ npm run dev -- ... # run from source via tsx
250
336
 
251
337
  Each agent is one adapter in [`src/targets/`](./src/targets) implementing
252
338
  `apply` / `prune` / `current` / `candidates`; read-only import sources live in
253
- [`src/sources/`](./src/sources). Adding an app is one file plus a line in
254
- [`src/targets/index.ts`](./src/targets/index.ts).
339
+ [`src/sources/`](./src/sources). To add an app, register it in
340
+ [`src/targets/index.ts`](./src/targets/index.ts), wrap its implementation with
341
+ `transactionalTarget()`, and add its schemas/references to `rename.ts` and `remove.ts`.
342
+ Use shared credential, YAML/JSONC, URL and model helpers; add apply/prune/import,
343
+ rename/removal, malformed-input, dry-run and account-isolation tests. If installable,
344
+ also add its platform commands to `apps.ts`. WorkBuddy local deletion uses the stable
345
+ account selector shown by `list --apps workbuddy`, not an ambiguous hostname alone.
255
346
 
256
347
  ## License
257
348
 
package/README.zh-CN.md CHANGED
@@ -23,7 +23,7 @@ switching to vfing (openai) · default model glm-5.3-flash
23
23
 
24
24
  skip claude Claude Code does not support openai-protocol providers
25
25
  ok codex ~/.codex/config.toml, ~/.codex/auth.json
26
- backup: ~/.config/agentsw/backups/config.toml.2026-08-31T15-58-24-678Z
26
+ backup: ~/.config/agentsw/backups/transaction-<unique>
27
27
  ok omp ~/.omp/agent/models.yml
28
28
  select in omp with: omp --model vfing/glm-5.3-flash
29
29
  skip pi pi not detected (pass --apps pi to force)
@@ -38,18 +38,18 @@ ok dsh ~/.dsh/settings.yaml, ~/.dsh/.credentials.yaml
38
38
 
39
39
  - **它能去桌面应用去不了的地方。** 无头服务器、容器、devcontainer、CI。
40
40
  `npx agentsw use myproxy -a codex,omp` 在开机脚本里是一行;GUI 点九次不是。
41
- - **`--dry-run` 直接打 diff。** 写盘前先看清楚要改什么,或者把 diff 丢进 code review。
42
- 无论如何每个文件都会先备份。
41
+ - **`--dry-run` 输出脱敏 diff。** 写盘前先预览;预览不写文件、不创建备份。
42
+ 正式提交变更时,已有配置文件才会先备份。
43
43
  - **默认不破坏现有配置。** 只覆盖 agentsw 自己管理的字段:它不建模的供应商级键
44
44
  (`authHeader`、`headers`、`compat`、`discovery`)、`thinkingLevelMap` 之类的模型级字段、
45
45
  以及 YAML 注释,都会在重新同步后原样保留。
46
46
  - **写的是元数据,不只是模型 id。** 转售商 `/v1/models` 只返回裸 id;agentsw 从
47
- [models.dev](https://models.dev) 补全,并按各应用真正读取的字段下发:pi/prime 的
47
+ [models.dev](https://models.dev) 优先补全,并在有实际缺口时自动查询 AI Gateway,按各应用真正读取的字段下发:pi/prime 的
48
48
  `thinkingLevelMap`、opencode 的 `limit`/`attachment`、omp 的 `contextWindow`/`maxTokens`、
49
49
  dsh 的 `reasoningEfforts`/`input`。
50
50
  - **接管你已有的配置——包括 [cc-switch](https://github.com/farion1231/cc-switch)。**
51
51
  `import` 会扫描所有已安装的智能体**以及 cc-switch 自己的存储**,合并重复项
52
- (相同协议 + base URL),并且对两者都只读不写。
52
+ (相同协议 + 归一化端点 + 凭据),并且对两者都只读不写。
53
53
  - **区分接口形态。** `/v1/chat/completions` 与 `/v1/responses` 是两个不同端点;
54
54
  agentsw 记录供应商说的是哪一种,并且绝不把可用的 responses 降级。
55
55
 
@@ -61,7 +61,7 @@ ok dsh ~/.dsh/settings.yaml, ~/.dsh/.credentials.yaml
61
61
 
62
62
  ```bash
63
63
  npx agentsw # 免安装,直接打开交互菜单
64
- npm install -g agentsw # 需要 Node >= 22.12
64
+ npm install -g agentsw # 需要 Node >= 22.13,内置 SQLite 无需额外启动参数
65
65
  ```
66
66
 
67
67
  供应商同步与配置管理支持 Linux、macOS 和 Windows。Windows 下 agentsw 自身状态默认存放在
@@ -137,11 +137,13 @@ imported sub · openai · https://new.vfing.de/v1 · 18 models [from omp]
137
137
  imported zhipu-glm · anthropic · https://open.bigmodel.cn/api/anthropic · 1 models [from cc-switch]
138
138
  ```
139
139
 
140
- 不加 `--all` 时先展示多选预览,选定之前不写任何文件。
140
+ 在交互终端中,不加 `--all` 会先展示多选预览,选定后才保存供应商记录。
141
+ 使用 `--all` 或非交互输入时会导入全部符合条件的新条目;脚本中建议明确写 `--all`。
142
+ 模型发现过程中可能更新本地元数据缓存。
141
143
 
142
144
  ## 模型发现过滤
143
145
 
144
- `--discover` 从供应商 `/v1/models` 拉取模型 id,再从 models.dev 补全元数据。
146
+ `--discover` 从供应商 `/v1/models` 拉取模型 id,优先从 models.dev 补全元数据,必要时自动查询 AI Gateway。
145
147
  转售商列表噪音大,过滤规则持久化在供应商上,每次 `discover` 自动复用:
146
148
 
147
149
  ```bash
@@ -155,6 +157,69 @@ asw discover myproxy --no-filter # 清除规则
155
157
 
156
158
  手动 `--models` 指定的 id 和默认模型永不被过滤。
157
159
 
160
+ ## 自动补全模型元数据
161
+
162
+ **默认无需额外参数。** 新供应商和未保存模式的供应商使用 `auto`:先查询 models.dev,
163
+ 仅在核心参数(上下文窗口、最大输出、推理或图片输入能力)仍缺失、已有 Gateway 自动字段
164
+ 未被手动修改或 models.dev 替代而需刷新,或需核实旧自动值的身份冲突时,才查询公开的
165
+ [AI Gateway 模型目录](https://vercel.com/docs/ai-gateway/models-and-providers#dynamic-model-discovery)。
166
+ 仅缺名称、价格、可选输入上限或推理等级列表,不会触发 Gateway 查询。
167
+ 直接请求 `https://ai-gateway.vercel.sh/v1/models`,不携带供应商密钥,也不安装 AI SDK。
168
+
169
+ ```bash
170
+ # 按保存的模式刷新已有模型参数(未设置时为 auto)
171
+ # 不发现新模型、不写智能体配置
172
+ asw refresh --provider myproxy
173
+
174
+ # 查看生效模式、字段来源、冲突和参考价格;JSON 不含 API key
175
+ asw models --provider myproxy --metadata
176
+
177
+ # 自定义或有歧义的名称需显式映射;实际调用的模型 ID 不变
178
+ asw refresh --provider myproxy --metadata-mode auto \
179
+ --gateway-models '{"my-model":"openai/gpt-5.4"}'
180
+
181
+ # 即使核心参数完整,也始终查询 Gateway
182
+ asw refresh --provider myproxy --metadata-mode on
183
+
184
+ # 停止后续 Gateway 获取;保留已保存的参数
185
+ asw refresh --provider myproxy --metadata-mode off
186
+
187
+ # 将显式关闭的供应商恢复为自动模式
188
+ asw refresh --provider myproxy --metadata-mode auto
189
+
190
+ # 单独预览、同步;sync 不查询目录
191
+ asw sync --provider myproxy --dry-run
192
+ asw sync --provider myproxy
193
+ ```
194
+
195
+ 交互菜单的“设置并刷新模型参数补充源”提供“自动按需(推荐)”“始终补充”“关闭”,
196
+ 默认选中该供应商当前模式;添加供应商时不增加提问。
197
+ `add`、`quick`、`discover`、`import`、`refresh` 均支持 `--metadata-mode <auto|on|off>`。
198
+ 原有 `--gateway-metadata` / `--no-gateway-metadata` 仍分别表示显式 `on` / `off`。
199
+ 省略参数会保留已保存的设置:已有 `false` 仍保持关闭,直到主动选择 `auto` 或 `on`。
200
+ 无效模式或相互冲突的模式/布尔开关,会在查询或保存之前报错。
201
+ `refresh` 不传 `--provider` 会作用于全部供应商,包括更新传入的模式。
202
+ `--gateway-models` 替换整个映射,传 `{}` 清空;映射不会更改保存的模式,也不会覆盖 `off`。
203
+ 导入仍跳过已经配置的账号。
204
+
205
+ - **按字段合并:** 用户手动值优先,其次 models.dev,最后 Gateway 补缺。已记录来源的自动值可以刷新,
206
+ 手动修改后会保留;旧版本中没有来源记录的已有值保守视为手动值,不强制覆盖。未知扩展字段也保留。
207
+ - **精确匹配:** 不含 `/` 的裸 ID 可按大小写精确匹配唯一的 Gateway `creator/model` 身份,
208
+ 前提是 models.dev 没有身份线索或线索一致。任一目录中的创建者身份有歧义时拒绝自动匹配;
209
+ 自定义或有歧义的名称仍需显式映射。带命名空间的 ID 不会剥离前缀,不按子串、大小写折叠或版本相似度猜测。
210
+ `auto` / `on` 模式下 models.dev 也采用保守的精确匹配;`off` 保留原有 models.dev 查询行为。
211
+ 匹配失败不妨碍模型继续使用。修改/删除映射时,只清理确认已失效身份的自动值,保留手动修改。
212
+ - **不改变调用语义:** 目录不会给供应商增加模型,不改模型 ID、协议、URL、默认模型或账户;
213
+ Gateway 能力是参考规格,不保证转售商实际实现。`supported_specifications` 不是 URL 版本。
214
+ - **价格仅供参考:** Gateway 价格转换为美元/百万 token,单独保存在 `metadata.gateway.referenceCost`,
215
+ 不写入供应商的有效 `cost`,也不下发为智能体计费价格;分档/供应商差异另有标记。
216
+ 自动模式不会仅为刷新参考价格而查询:可查看 `metadata.gateway.fetchedAt` 判断快照时间,
217
+ 需要重新查询参考价格时使用 `refresh --provider <id> --metadata-mode on`。
218
+ - **可追溯、可降级:** `metadata.fields` 记录来源、原值快照和时间,`metadata.conflicts` 记录保留值与冲突。
219
+ 审计信息仅保存在 agentsw,不写入智能体运行配置。Gateway 目录独立缓存 24 小时;请求失败可使用旧缓存,无缓存时只跳过补充。
220
+ - **自动查询不等于自动同步:** 添加、快速添加、发现、导入、刷新时补全保存的元数据,并不新增自动推送。
221
+ 同步行为不变:普通 `sync` 仅写入已保存的设置,既不获取模型列表,也不请求元数据目录。
222
+
158
223
  ## 命令
159
224
 
160
225
  | 命令 | 作用 |
@@ -170,7 +235,8 @@ asw discover myproxy --no-filter # 清除规则
170
235
  | `sync` | 重新应用当前供应商(比如某个智能体升级之后) |
171
236
  | `discover <id> [--sync]` | 从 `/v1/models` 刷新模型列表与元数据 |
172
237
  | `models [query]` | 搜索 models.dev 目录 |
173
- | `refresh` | 重新拉取所有供应商的元数据 |
238
+ | `refresh [--provider <id>]` | 刷新已有模型参数,可设置 Gateway 补充源,不改变模型列表 |
239
+ | `models --provider <id> --metadata` | 以 JSON 查看字段来源、冲突和参考价格 |
174
240
  | `prune <id>` / `remove <id> [--prune]` | 从各应用配置中清除 / 从存储中删除 |
175
241
  | `remove <id> --apps omp` | 仅删除指定智能体内的条目,支持 `--dry-run` |
176
242
  | `apps` / `install <app>` / `upgrade` | 智能体版本管理 |
@@ -200,8 +266,16 @@ asw remove unused-provider --apps omp
200
266
 
201
267
  ## 它如何对待你的配置
202
268
 
203
- - 每个被修改的文件先备份到 `~/.config/agentsw/backups/`。
204
- - 供应商存储位于 `~/.config/agentsw/config.json`(权限 0600)。
269
+ - 正式提交变更前,已有中央存储和智能体配置先备份到
270
+ `~/.config/agentsw/backups/transaction-*`。新文件没有旧副本;缓存刷新不属于配置备份。
271
+ 预览不写文件,也不创建备份。
272
+ - 每个适配器先准备全部文件再提交,写入失败时尝试回滚。多智能体同步不是全局文件系统事务:
273
+ 某个智能体失败时,其他成功项仍保留,并通过非零退出码报告失败。
274
+ - 供应商存储位于 `~/.config/agentsw/config.json`(权限 0600)。并发写入导致快照过期时,
275
+ 拒绝覆盖新记录;等待其他写入完成后重试。
276
+ - 保留已有文件权限,新建配置文件默认使用 0600。
277
+ - 写锁占用时会显示持有者 PID 和时间。若进程曾异常退出,先停止所有 agentsw 写入并确认
278
+ 锁已遗留,再手动删除报错中指定的 `.write.lock` 后重试;不会自动抢占活动或状态不明的锁。
205
279
  - 同步只覆盖 agentsw 自己管理的字段:未建模的供应商级键与模型级扩展字段保留;
206
280
  自己管理但本次不再写出的字段会被清除而非留成陈旧值;与新路由矛盾的模型级
207
281
  `api`/`baseUrl` 覆盖会被删除(否则它会静默盖过供应商条目)并在输出中点名。
@@ -212,8 +286,9 @@ asw remove unused-provider --apps omp
212
286
  手动指定;同步绝不把已有的 responses 端点降级。
213
287
  - Codex 只支持 Responses API;仅提供 chat-completions 的端点无法用于 Codex,同步输出会提示。
214
288
  - DeepSeek Harness 不把密钥写进 `settings.yaml`:路由里只保存
215
- `apiKeyEnv: AGENTSW_<ID>_API_KEY` 引用,密钥写入 `$DSH_HOME/.credentials.yaml`
289
+ `apiKeyEnv: AGENTSW_<ID>_<DIGEST>_API_KEY` 引用,密钥写入 `$DSH_HOME/.credentials.yaml`
216
290
  的 `refs:`(权限 0600)。`$DSH_HOME` 默认为 `~/.dsh`。
291
+ 摘要用于区分 `foo-bar` 和 `foo_bar` 等 ID;旧版生成引用仍可识别,不随意修改外部或自定义引用。
217
292
  - cc-switch 的数据库以只读方式打开,永不写入。
218
293
  - 安装管理器在 Windows 下为 Claude Code、Codex CLI、pi、opencode、Hermes 和 DeepSeek Harness
219
294
  使用原生安装命令。Oh My Pi 与 prime-agent 仍需使用各自的 Windows 安装方式;WorkBuddy
@@ -242,8 +317,12 @@ npm run dev -- ... # 通过 tsx 从源码运行
242
317
 
243
318
  每个智能体是 [`src/targets/`](./src/targets) 下的一个适配器,实现
244
319
  `apply` / `prune` / `current` / `candidates`;只读的导入来源放在
245
- [`src/sources/`](./src/sources)。新增一个应用 = 一个文件 +
246
- [`src/targets/index.ts`](./src/targets/index.ts) 里的一行。
320
+ [`src/sources/`](./src/sources)。新增应用时,在
321
+ [`src/targets/index.ts`](./src/targets/index.ts) 注册,用 `transactionalTarget()` 包装适配器,
322
+ 并为 `rename.ts`、`remove.ts` 补充对应配置结构与引用处理。
323
+ 复用共享凭据、YAML/JSONC、URL 和模型工具,补齐同步、清理、导入、重命名、删除、
324
+ 异常输入、预览及账户隔离测试;支持安装的应用还需在 `apps.ts` 中注册平台命令。
325
+ 删除 WorkBuddy 本地账户时,使用 `list --apps workbuddy` 显示的稳定账户标识,避免同域名歧义。
247
326
 
248
327
  ## 许可证
249
328
 
package/dist/apps.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { execFileSync, execSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import semver from "semver";
4
5
  import { appDataDir, home, readJsonIfExists } from "./fsutil.js";
5
6
  export const appPackages = [
6
7
  {
@@ -137,12 +138,20 @@ export function installedVersion(app) {
137
138
  if (app.binary) {
138
139
  try {
139
140
  const binary = executableName(app.binary);
140
- const out = execFileSync(binary, app.versionArgs ?? ["--version"], {
141
+ const args = app.versionArgs ?? ["--version"];
142
+ const isBatch = process.platform === "win32" && /\.(cmd|bat)$/i.test(binary);
143
+ const quote = (value) => {
144
+ if (/["%\r\n]/.test(value))
145
+ throw new Error("unsupported Windows version-probe argument");
146
+ return `"${value}"`;
147
+ };
148
+ const out = execFileSync(isBatch ? process.env.ComSpec || "cmd.exe" : binary, isBatch ? ["/d", "/s", "/c", `"${[binary, ...args].map(quote).join(" ")}"`] : args, {
141
149
  encoding: "utf8",
142
150
  timeout: 15000,
143
151
  stdio: ["ignore", "pipe", "pipe"],
152
+ ...(isBatch ? { windowsVerbatimArguments: true } : {}),
144
153
  });
145
- return SEMVERISH.exec(out)?.[0];
154
+ return normalizeAppVersion(SEMVERISH.exec(out)?.[0]) ?? "?";
146
155
  }
147
156
  catch {
148
157
  // binary present but probe failed (e.g. runtime version gate) -> installed, version unknown
@@ -200,25 +209,27 @@ export async function latestVersion(app) {
200
209
  return undefined;
201
210
  }
202
211
  }
203
- /** numeric-aware semver-ish comparison; true when b is newer than a */
212
+ /** Accept SemVer (including prereleases), with the legacy two-component display form. */
213
+ export function normalizeAppVersion(value) {
214
+ if (typeof value !== "string")
215
+ return undefined;
216
+ const version = value.trim();
217
+ return semver.valid(version) ?? (/^v?\d+\.\d+$/.test(version) ? semver.valid(`${version}.0`) ?? undefined : undefined);
218
+ }
219
+ /** Build metadata has no precedence; prereleases sort before the corresponding release. */
204
220
  export function isNewer(installed, latest) {
205
- const pa = installed.split(/[.+-]/).map(Number);
206
- const pb = latest.split(/[.+-]/).map(Number);
207
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
208
- const a = pa[i] ?? 0;
209
- const b = pb[i] ?? 0;
210
- if (Number.isNaN(a) || Number.isNaN(b))
211
- break;
212
- if (b > a)
213
- return true;
214
- if (b < a)
215
- return false;
216
- }
217
- return false;
221
+ const before = normalizeAppVersion(installed);
222
+ const after = normalizeAppVersion(latest);
223
+ return before !== undefined && after !== undefined && semver.gt(after, before);
218
224
  }
219
225
  /** Run an install/upgrade command with live output. Throws on nonzero exit. */
220
226
  export function runShell(command) {
221
- execSync(command, { stdio: "inherit", env: process.env });
227
+ if (process.platform === "win32") {
228
+ execSync(command, { stdio: "inherit", env: process.env });
229
+ }
230
+ else {
231
+ execFileSync("bash", ["-o", "pipefail", "-c", command], { stdio: "inherit", env: process.env });
232
+ }
222
233
  }
223
234
  /** Resolve the platform-specific command while keeping the package table readable. */
224
235
  export function appCommand(app, action, platform = process.platform) {
@@ -227,34 +238,27 @@ export function appCommand(app, action, platform = process.platform) {
227
238
  return action === "install" ? app.installCmd : app.upgradeCmd ?? app.installCmd;
228
239
  }
229
240
  function executableName(binary) {
230
- if (process.platform !== "win32")
231
- return binary;
232
- for (const suffix of ["", ".cmd", ".exe", ".bat"]) {
233
- const candidate = `${binary}${suffix}`;
234
- if (binaryOnPath(candidate))
235
- return candidate;
241
+ return findBinary(binary) ?? binary;
242
+ }
243
+ function findBinary(binary) {
244
+ const names = process.platform === "win32" && !path.extname(binary)
245
+ ? (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map((suffix) => `${binary}${suffix.toLowerCase()}`)
246
+ : [binary];
247
+ const dirs = path.isAbsolute(binary) ? [""] : (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
248
+ for (const dir of dirs) {
249
+ for (const name of names) {
250
+ const file = path.resolve(dir.replace(/^"|"$/g, ""), name);
251
+ try {
252
+ if (!fs.statSync(file).isFile())
253
+ continue;
254
+ fs.accessSync(file, process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
255
+ return file;
256
+ }
257
+ catch { /* Try the next executable; directories and inaccessible files are not commands. */ }
258
+ }
236
259
  }
237
- return binary;
260
+ return undefined;
238
261
  }
239
262
  export function binaryOnPath(binary) {
240
- const dirs = (process.env.PATH ?? "").split(path.delimiter);
241
- return dirs.some((d) => {
242
- try {
243
- const names = process.platform === "win32" && !path.extname(binary)
244
- ? [binary, ...((process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((s) => `${binary}${s.toLowerCase()}`))]
245
- : [binary];
246
- return names.some((name) => {
247
- try {
248
- fs.accessSync(path.join(d, name), process.platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
249
- return true;
250
- }
251
- catch {
252
- return false;
253
- }
254
- });
255
- }
256
- catch {
257
- return false;
258
- }
259
- });
263
+ return findBinary(binary) !== undefined;
260
264
  }