agentsw 0.8.0 → 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.
- package/README.md +6 -0
- package/dist/commands.js +3 -2
- package/dist/fsutil.js +14 -1
- package/dist/i18n.js +4 -0
- package/dist/jsonc.js +4 -5
- package/dist/menu.js +40 -3
- package/dist/targets/codex.js +15 -4
- package/dist/yaml.js +28 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,6 +121,12 @@ dsh yes openai+anthropic vfing · glm-5.3-flash ~/.ds
|
|
|
121
121
|
An agent that is not installed is skipped, not guessed at. `--apps codex,omp` narrows a run;
|
|
122
122
|
`--apps` with an uninstalled agent forces it.
|
|
123
123
|
|
|
124
|
+
The interactive menu asks the same question before writing: a multi-select where the first row
|
|
125
|
+
is **all detected apps**, detected agents are listed first and undetected ones are marked. The
|
|
126
|
+
last selection is remembered, so a menu `use`/`sync` keeps rewriting only the agents you picked.
|
|
127
|
+
An error in any config file (invalid JSON/YAML/TOML) is reported with the file and position, never
|
|
128
|
+
with the offending line, and dry-run keeps that detail instead of hiding it.
|
|
129
|
+
|
|
124
130
|
## Import what you already configured
|
|
125
131
|
|
|
126
132
|
`import` reads every agent's config **and cc-switch's SQLite store** (read-only), resolves
|
package/dist/commands.js
CHANGED
|
@@ -13,7 +13,7 @@ import { loadGatewayCatalog } from "./gateway.js";
|
|
|
13
13
|
import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
|
|
14
14
|
import { discoverProviderModels, probeProtocols } from "./discover.js";
|
|
15
15
|
import { appCommand, appPackages, installedVersion, isNewer, latestVersion, normalizeAppVersion, runShell } from "./apps.js";
|
|
16
|
-
import { drainPendingWrites, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
16
|
+
import { SafeConfigError, drainPendingWrites, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
17
17
|
import { applyModelFilter } from "./filter.js";
|
|
18
18
|
import { availableProviderId, providerIdFromBaseUrl, providerNameFromBaseUrl } from "./slug.js";
|
|
19
19
|
import { t } from "./i18n.js";
|
|
@@ -443,7 +443,8 @@ async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
|
443
443
|
results.push(await target[op](provider));
|
|
444
444
|
}
|
|
445
445
|
catch (err) {
|
|
446
|
-
|
|
446
|
+
const safe = redactErrors && !(err instanceof SafeConfigError);
|
|
447
|
+
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${safe ? "configuration could not be previewed safely" : err.message}`) });
|
|
447
448
|
process.exitCode = 1;
|
|
448
449
|
}
|
|
449
450
|
}
|
package/dist/fsutil.js
CHANGED
|
@@ -3,6 +3,13 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
/**
|
|
7
|
+
* An error whose message is guaranteed to carry no file content. Dry-run redacts
|
|
8
|
+
* unknown errors wholesale (parser messages can embed credentials), so config
|
|
9
|
+
* parsers must throw this to stay actionable there.
|
|
10
|
+
*/
|
|
11
|
+
export class SafeConfigError extends Error {
|
|
12
|
+
}
|
|
6
13
|
// AGENTSW_HOME is an explicit portable/test override. Native Windows otherwise
|
|
7
14
|
// uses USERPROFILE/os.homedir(), while Unix follows HOME as expected.
|
|
8
15
|
export const home = process.env.AGENTSW_HOME?.trim() ||
|
|
@@ -88,7 +95,13 @@ export function readJsonIfExists(file) {
|
|
|
88
95
|
const text = readTextIfExists(file);
|
|
89
96
|
if (text === undefined)
|
|
90
97
|
return undefined;
|
|
91
|
-
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(text);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Node quotes surrounding input in the message, which may hold credentials.
|
|
103
|
+
throw new SafeConfigError(`${file}: invalid JSON configuration`);
|
|
104
|
+
}
|
|
92
105
|
}
|
|
93
106
|
/** Compatibility preview switch. Each target captures its value when its scoped work starts. */
|
|
94
107
|
let dryRun = false;
|
package/dist/i18n.js
CHANGED
|
@@ -123,6 +123,10 @@ const messages = {
|
|
|
123
123
|
"menu.defaultModel": { en: "default model", "zh-CN": "默认模型" },
|
|
124
124
|
"menu.keepDefault": { en: "keep current default ({model})", "zh-CN": "保持当前默认模型({model})" },
|
|
125
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 确认" },
|
|
126
130
|
"menu.discoverFor": { en: "which provider's model list should be updated?", "zh-CN": "选择要更新模型列表的供应商" },
|
|
127
131
|
"menu.pushRefresh": { en: "also sync this provider to agent configs after updating its model list?", "zh-CN": "更新模型列表后,是否同时将该供应商同步到智能体配置?" },
|
|
128
132
|
"menu.removeProvider": { en: "which provider configuration should be deleted?", "zh-CN": "选择要删除配置的供应商" },
|
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
|
-
|
|
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
|
|
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
|
|
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
|
@@ -62,6 +62,42 @@ async function pickProvider(message) {
|
|
|
62
62
|
const p = store.providers[id];
|
|
63
63
|
return { id, defaultModel: p.defaultModel, models: p.models.map((m) => m.id) };
|
|
64
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
|
+
}
|
|
65
101
|
async function renameFromMenu() {
|
|
66
102
|
const picked = await pickProvider(t("menu.renameProvider"));
|
|
67
103
|
if (!picked)
|
|
@@ -201,6 +237,7 @@ export async function cmdMenu() {
|
|
|
201
237
|
const picked = await pickProvider(t("menu.pickProvider"));
|
|
202
238
|
if (!picked)
|
|
203
239
|
continue;
|
|
240
|
+
const apps = await pickApps(t("menu.pickApps"));
|
|
204
241
|
const { model } = await prompts({
|
|
205
242
|
type: picked.models.length > 1 ? "select" : null,
|
|
206
243
|
name: "model",
|
|
@@ -211,7 +248,7 @@ export async function cmdMenu() {
|
|
|
211
248
|
...picked.models.filter((m) => m !== picked.defaultModel).map((m) => ({ title: m, value: m })),
|
|
212
249
|
],
|
|
213
250
|
}, cancel);
|
|
214
|
-
await cmdUse(picked.id, { model: model || undefined });
|
|
251
|
+
await cmdUse(picked.id, { model: model || undefined, apps });
|
|
215
252
|
}
|
|
216
253
|
else if (action === "status") {
|
|
217
254
|
cmdStatus();
|
|
@@ -220,14 +257,14 @@ export async function cmdMenu() {
|
|
|
220
257
|
cmdList();
|
|
221
258
|
}
|
|
222
259
|
else if (action === "sync") {
|
|
223
|
-
await cmdSync({});
|
|
260
|
+
await cmdSync({ apps: await pickApps(t("menu.pickApps")) });
|
|
224
261
|
}
|
|
225
262
|
else if (action === "discover") {
|
|
226
263
|
const picked = await pickProvider(t("menu.discoverFor"));
|
|
227
264
|
if (!picked)
|
|
228
265
|
continue;
|
|
229
266
|
const sync = await askToggle(t("menu.pushRefresh"));
|
|
230
|
-
await cmdDiscover(picked.id, { sync });
|
|
267
|
+
await cmdDiscover(picked.id, { sync, apps: sync ? await pickApps(t("menu.pickApps")) : undefined });
|
|
231
268
|
}
|
|
232
269
|
else if (action === "metadata") {
|
|
233
270
|
const picked = await pickProvider(t("menu.metadataProvider"));
|
package/dist/targets/codex.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
4
|
-
import { backupFile, home, readJsonIfExists, readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
4
|
+
import { SafeConfigError, backupFile, home, readJsonIfExists, readTextIfExists, writeFileAtomic } from "../fsutil.js";
|
|
5
5
|
import { isJsonObject } from "../jsonc.js";
|
|
6
6
|
import { transactionalTarget } from "../target-transaction.js";
|
|
7
7
|
const dir = path.join(home, ".codex");
|
|
8
8
|
const configFile = path.join(dir, "config.toml");
|
|
9
9
|
const authFile = path.join(dir, "auth.json");
|
|
10
|
+
/** smol-toml quotes the offending source line, which may hold a key: report only the position. */
|
|
11
|
+
function parseConfigToml(file, text) {
|
|
12
|
+
try {
|
|
13
|
+
return parseToml(text);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const { line, column } = err;
|
|
17
|
+
const position = line === undefined ? "" : ` (line ${line}${column === undefined ? "" : `, col ${column}`})`;
|
|
18
|
+
throw new SafeConfigError(`${file}: invalid TOML configuration${position}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
10
21
|
/**
|
|
11
22
|
* Codex CLI: [model_providers.<id>] in ~/.codex/config.toml selects a custom
|
|
12
23
|
* OpenAI-protocol endpoint; the key ships via ~/.codex/auth.json OPENAI_API_KEY.
|
|
@@ -21,7 +32,7 @@ export const codex = transactionalTarget({
|
|
|
21
32
|
async apply(provider) {
|
|
22
33
|
const notes = [];
|
|
23
34
|
const text = readTextIfExists(configFile);
|
|
24
|
-
const config =
|
|
35
|
+
const config = text ? parseConfigToml(configFile, text) : {};
|
|
25
36
|
const authValue = readJsonIfExists(authFile);
|
|
26
37
|
const auth = authValue === undefined ? {} : authValue;
|
|
27
38
|
if (!isJsonObject(auth))
|
|
@@ -84,7 +95,7 @@ export const codex = transactionalTarget({
|
|
|
84
95
|
const text = readTextIfExists(configFile);
|
|
85
96
|
if (!text)
|
|
86
97
|
return { app: this.id, changed: [], notes: [], skipped: "no config.toml" };
|
|
87
|
-
const config =
|
|
98
|
+
const config = parseConfigToml(configFile, text);
|
|
88
99
|
if (config.model_providers !== undefined && !isJsonObject(config.model_providers)) {
|
|
89
100
|
throw new Error(`${configFile}: expected model_providers to be a table`);
|
|
90
101
|
}
|
|
@@ -114,7 +125,7 @@ export const codex = transactionalTarget({
|
|
|
114
125
|
if (!text)
|
|
115
126
|
return undefined;
|
|
116
127
|
try {
|
|
117
|
-
const config =
|
|
128
|
+
const config = parseConfigToml(configFile, text);
|
|
118
129
|
if (!config.model_provider)
|
|
119
130
|
return undefined;
|
|
120
131
|
return `${String(config.model_provider)} · ${String(config.model ?? "?")}`;
|
package/dist/yaml.js
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
import YAML from "yaml";
|
|
2
|
+
import { SafeConfigError } from "./fsutil.js";
|
|
3
|
+
/** YAML errors embed the offending source line, which may hold credentials: keep only the code and position. */
|
|
4
|
+
function describeYamlErrors(file, errors) {
|
|
5
|
+
const details = [...new Set(errors.map((error) => {
|
|
6
|
+
const position = error.linePos?.[0];
|
|
7
|
+
return [
|
|
8
|
+
error.code,
|
|
9
|
+
position ? `line ${position.line}` : undefined,
|
|
10
|
+
position ? `col ${position.col}` : undefined,
|
|
11
|
+
].filter(Boolean).join(" ");
|
|
12
|
+
}))].slice(0, 3);
|
|
13
|
+
return details.length
|
|
14
|
+
? `${file}: invalid YAML configuration (${details.join("; ")})`
|
|
15
|
+
: `${file}: invalid YAML configuration`;
|
|
16
|
+
}
|
|
2
17
|
/** Resolve aliases in document context before edits can mutate or remove anchors. */
|
|
3
18
|
export function parseYamlMapping(file, text) {
|
|
19
|
+
let doc;
|
|
20
|
+
try {
|
|
21
|
+
doc = text ? YAML.parseDocument(text) : new YAML.Document({});
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new SafeConfigError(`${file}: invalid YAML configuration`);
|
|
25
|
+
}
|
|
26
|
+
if (doc.errors.length)
|
|
27
|
+
throw new SafeConfigError(describeYamlErrors(file, doc.errors));
|
|
28
|
+
if (doc.contents == null)
|
|
29
|
+
doc.contents = doc.createNode({});
|
|
4
30
|
try {
|
|
5
|
-
const doc = text ? YAML.parseDocument(text) : new YAML.Document({});
|
|
6
|
-
if (doc.errors.length)
|
|
7
|
-
throw new Error("parse error");
|
|
8
|
-
if (doc.contents == null)
|
|
9
|
-
doc.contents = doc.createNode({});
|
|
10
31
|
// YAML permits cycles; application configuration does not. The document
|
|
11
32
|
// conversion also enforces YAML's alias expansion limit before we detach.
|
|
12
33
|
JSON.stringify(doc.toJS());
|
|
@@ -27,8 +48,8 @@ export function parseYamlMapping(file, text) {
|
|
|
27
48
|
return doc;
|
|
28
49
|
}
|
|
29
50
|
catch {
|
|
30
|
-
// Parser messages include source lines, which may contain credentials.
|
|
31
|
-
throw new
|
|
51
|
+
// Parser and conversion messages include source lines, which may contain credentials.
|
|
52
|
+
throw new SafeConfigError(`${file}: invalid YAML configuration (expected an acyclic mapping with valid aliases)`);
|
|
32
53
|
}
|
|
33
54
|
}
|
|
34
55
|
/** Validate the emitted configuration before any backup or write. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentsw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "CLI provider switcher for coding agents: manage OpenAI/Anthropic-protocol providers in one place and sync them, with models.dev metadata, into Claude Code, Codex, omp, pi, prime-agent, opencode, Hermes, WorkBuddy and DeepSeek Harness",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|