agentsw 0.7.0 → 0.7.2
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 +30 -11
- package/README.zh-CN.md +26 -11
- package/dist/apps.js +48 -44
- package/dist/commands.js +221 -74
- package/dist/config-transaction.js +169 -84
- package/dist/discover.js +55 -17
- package/dist/envfile.js +101 -20
- package/dist/fsutil.js +109 -14
- package/dist/i18n.js +62 -39
- package/dist/import.js +2 -2
- package/dist/index.js +16 -1
- package/dist/jsonc.js +4 -10
- package/dist/menu.js +28 -22
- package/dist/modelsdev.js +7 -3
- package/dist/provider-identity.js +25 -0
- package/dist/remove.js +84 -38
- package/dist/rename.js +91 -58
- package/dist/store.js +38 -7
- package/dist/target-transaction.js +25 -0
- package/dist/targets/claudecode.js +12 -3
- package/dist/targets/codex.js +34 -3
- package/dist/targets/dsh.js +117 -65
- package/dist/targets/hermes.js +98 -49
- package/dist/targets/omp.js +11 -47
- package/dist/targets/opencode.js +27 -6
- package/dist/targets/pistyle.js +12 -8
- package/dist/targets/wire.js +15 -9
- package/dist/targets/workbuddy.js +151 -76
- package/dist/yaml.js +39 -0
- package/package.json +4 -2
package/dist/fsutil.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
4
6
|
// AGENTSW_HOME is an explicit portable/test override. Native Windows otherwise
|
|
5
7
|
// uses USERPROFILE/os.homedir(), while Unix follows HOME as expected.
|
|
6
8
|
export const home = process.env.AGENTSW_HOME?.trim() ||
|
|
@@ -24,14 +26,62 @@ export function expandHome(p) {
|
|
|
24
26
|
return p.startsWith("~") ? path.join(home, p.slice(1)) : p;
|
|
25
27
|
}
|
|
26
28
|
export function ensureDir(dir) {
|
|
27
|
-
|
|
29
|
+
if (isDryRun() || stagedFiles.getStore())
|
|
30
|
+
return;
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
32
|
+
}
|
|
33
|
+
/** Read exactly one regular file. A broken path or unreadable file is not an empty config. */
|
|
34
|
+
export function readFileSnapshot(file) {
|
|
35
|
+
try {
|
|
36
|
+
const stat = fs.lstatSync(file, { bigint: true });
|
|
37
|
+
if (!stat.isFile())
|
|
38
|
+
throw new Error(`${file}: not a regular configuration file`);
|
|
39
|
+
return { text: fs.readFileSync(file, "utf8"), mode: Number(stat.mode & 511n), identity: `${stat.dev}:${stat.ino}` };
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code === "ENOENT")
|
|
43
|
+
return { text: undefined };
|
|
44
|
+
throw new Error(`${file}: cannot read a regular configuration file`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const stagedFiles = new AsyncLocalStorage();
|
|
48
|
+
function stagedSnapshot(scope, file) {
|
|
49
|
+
if (scope.closed)
|
|
50
|
+
throw new Error("configuration transaction already finished");
|
|
51
|
+
let snapshot = scope.reads.get(file);
|
|
52
|
+
if (!snapshot) {
|
|
53
|
+
snapshot = readFileSnapshot(file);
|
|
54
|
+
scope.reads.set(file, snapshot);
|
|
55
|
+
}
|
|
56
|
+
return snapshot;
|
|
57
|
+
}
|
|
58
|
+
/** Scope only this asynchronous adapter invocation, never unrelated filesystem calls. */
|
|
59
|
+
export async function stageFileWrites(operation) {
|
|
60
|
+
if (stagedFiles.getStore())
|
|
61
|
+
throw new Error("nested configuration transaction is not supported");
|
|
62
|
+
const scope = { reads: new Map(), writes: new Map(), dryRun, closed: false };
|
|
63
|
+
try {
|
|
64
|
+
const result = await stagedFiles.run(scope, operation);
|
|
65
|
+
return { result, changes: [...scope.writes.values()], reads: scope.reads, dryRun: scope.dryRun };
|
|
66
|
+
}
|
|
67
|
+
finally {
|
|
68
|
+
scope.closed = true;
|
|
69
|
+
}
|
|
28
70
|
}
|
|
29
71
|
export function readTextIfExists(file) {
|
|
72
|
+
const scope = stagedFiles.getStore();
|
|
73
|
+
if (scope) {
|
|
74
|
+
file = path.resolve(file);
|
|
75
|
+
const snapshot = stagedSnapshot(scope, file);
|
|
76
|
+
return scope.writes.has(file) ? scope.writes.get(file).after : snapshot.text;
|
|
77
|
+
}
|
|
30
78
|
try {
|
|
31
79
|
return fs.readFileSync(file, "utf8");
|
|
32
80
|
}
|
|
33
|
-
catch {
|
|
34
|
-
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error.code === "ENOENT")
|
|
83
|
+
return undefined;
|
|
84
|
+
throw error;
|
|
35
85
|
}
|
|
36
86
|
}
|
|
37
87
|
export function readJsonIfExists(file) {
|
|
@@ -40,9 +90,12 @@ export function readJsonIfExists(file) {
|
|
|
40
90
|
return undefined;
|
|
41
91
|
return JSON.parse(text);
|
|
42
92
|
}
|
|
43
|
-
/**
|
|
93
|
+
/** Compatibility preview switch. Each target captures its value when its scoped work starts. */
|
|
44
94
|
let dryRun = false;
|
|
45
95
|
const pendingWrites = [];
|
|
96
|
+
export function isDryRun() {
|
|
97
|
+
return stagedFiles.getStore()?.dryRun ?? dryRun;
|
|
98
|
+
}
|
|
46
99
|
export function setDryRun(on) {
|
|
47
100
|
dryRun = on;
|
|
48
101
|
pendingWrites.length = 0;
|
|
@@ -50,25 +103,67 @@ export function setDryRun(on) {
|
|
|
50
103
|
export function drainPendingWrites() {
|
|
51
104
|
return pendingWrites.splice(0);
|
|
52
105
|
}
|
|
53
|
-
/**
|
|
106
|
+
/** Publish previews only after the adapter has validated all of its input files. */
|
|
107
|
+
export function recordPendingWrites(changes) {
|
|
108
|
+
for (const change of changes) {
|
|
109
|
+
if (change.before !== change.after && change.after !== undefined) {
|
|
110
|
+
pendingWrites.push({ file: change.file, content: change.after });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Atomic replacement, preserving existing permissions and defaulting new secret files to 0600. */
|
|
54
115
|
export function writeFileAtomic(file, content, mode) {
|
|
116
|
+
const scope = stagedFiles.getStore();
|
|
117
|
+
if (scope) {
|
|
118
|
+
file = path.resolve(file);
|
|
119
|
+
const expected = stagedSnapshot(scope, file);
|
|
120
|
+
scope.writes.set(file, { file, before: expected.text, after: content, mode, expected });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
55
123
|
if (dryRun) {
|
|
56
124
|
pendingWrites.push({ file, content });
|
|
57
125
|
return;
|
|
58
126
|
}
|
|
127
|
+
const existing = readFileSnapshot(file);
|
|
128
|
+
const permissions = mode ?? existing.mode ?? 0o600;
|
|
129
|
+
if (!Number.isInteger(permissions) || permissions < 0 || permissions > 0o777) {
|
|
130
|
+
throw new Error(`${file}: invalid file permissions`);
|
|
131
|
+
}
|
|
59
132
|
ensureDir(path.dirname(file));
|
|
60
|
-
const tmp =
|
|
61
|
-
|
|
62
|
-
|
|
133
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.agentsw-${randomUUID()}.tmp`);
|
|
134
|
+
try {
|
|
135
|
+
fs.writeFileSync(tmp, content, { flag: "wx", mode: permissions });
|
|
136
|
+
fs.chmodSync(tmp, permissions);
|
|
137
|
+
fs.renameSync(tmp, file);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
fs.rmSync(tmp, { force: true });
|
|
141
|
+
}
|
|
63
142
|
}
|
|
64
143
|
export const backupsDir = path.join(appDataDir("agentsw"), "backups");
|
|
65
|
-
/**
|
|
144
|
+
/** Each original gets a private, unique directory even for equal basenames and timestamps. */
|
|
66
145
|
export function backupFile(file) {
|
|
67
|
-
|
|
146
|
+
const scope = stagedFiles.getStore();
|
|
147
|
+
if (scope) {
|
|
148
|
+
stagedSnapshot(scope, path.resolve(file));
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
if (dryRun)
|
|
152
|
+
return undefined;
|
|
153
|
+
const snapshot = readFileSnapshot(file);
|
|
154
|
+
if (snapshot.text === undefined)
|
|
68
155
|
return undefined;
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const dest = path.join(
|
|
72
|
-
|
|
156
|
+
fs.mkdirSync(backupsDir, { recursive: true, mode: 0o700 });
|
|
157
|
+
const dir = fs.mkdtempSync(path.join(backupsDir, "file-"));
|
|
158
|
+
const dest = path.join(dir, path.basename(file));
|
|
159
|
+
try {
|
|
160
|
+
fs.chmodSync(dir, 0o700);
|
|
161
|
+
fs.copyFileSync(file, dest, fs.constants.COPYFILE_EXCL);
|
|
162
|
+
fs.chmodSync(dest, 0o600);
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
73
168
|
return dest;
|
|
74
169
|
}
|
package/dist/i18n.js
CHANGED
|
@@ -38,6 +38,7 @@ const messages = {
|
|
|
38
38
|
"opt.discover": { en: "list model ids from the provider's /v1/models", "zh-CN": "从供应商 /v1/models 获取模型 id" },
|
|
39
39
|
"opt.include": { en: "keep only models matching comma-separated globs", "zh-CN": "仅保留匹配逗号分隔 glob 的模型" },
|
|
40
40
|
"opt.exclude": { en: "drop models matching comma-separated globs", "zh-CN": "排除匹配逗号分隔 glob 的模型" },
|
|
41
|
+
"opt.dedup": { en: "deduplicate model snapshot aliases", "zh-CN": "合并重复的模型快照别名" },
|
|
41
42
|
"opt.noDedup": { en: "keep snapshot duplicates (-latest/date suffixes)", "zh-CN": "保留快照重复项(-latest/日期后缀)" },
|
|
42
43
|
"opt.modelsDev": { en: "models.dev provider id for metadata matching", "zh-CN": "用于元数据匹配的 models.dev 供应商 id" },
|
|
43
44
|
"opt.yes": { en: "non-interactive; require all flags", "zh-CN": "非交互模式;要求提供全部参数" },
|
|
@@ -45,7 +46,7 @@ const messages = {
|
|
|
45
46
|
"opt.apps": { en: "comma-separated apps or 'all'", "zh-CN": "逗号分隔的应用或 'all'" },
|
|
46
47
|
"opt.removeApps": { en: "delete only from these apps; keep the agentsw store and other apps unchanged", "zh-CN": "仅从这些智能体删除,保留 agentsw 中央配置和其他智能体配置" },
|
|
47
48
|
"opt.manageDryRun": { en: "preview affected paths without changing files or creating backups", "zh-CN": "仅预览受影响路径,不修改文件或创建备份" },
|
|
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'" },
|
|
49
|
+
"opt.appsDetailed": { en: "comma-separated apps (claude,codex,omp,pi,prime,opencode,hermes,workbuddy,dsh) or 'all'", "zh-CN": "逗号分隔的应用(claude,codex,omp,pi,prime,opencode,hermes,workbuddy,dsh)或 'all'" },
|
|
49
50
|
"opt.model": { en: "override default model while switching", "zh-CN": "切换时覆盖默认模型" },
|
|
50
51
|
"opt.dryRun": { en: "preview config diff without writing", "zh-CN": "仅预览配置差异,不写入" },
|
|
51
52
|
"opt.provider": { en: "sync this provider instead of the active one", "zh-CN": "同步指定供应商而非当前供应商" },
|
|
@@ -63,62 +64,70 @@ const messages = {
|
|
|
63
64
|
"menu.selectInstructions": { en: "↑/↓ move · Enter select", "zh-CN": "↑/↓ 移动 · Enter 选择" },
|
|
64
65
|
"import.multiInstructions": { en: "↑/↓ move · Space toggle · Enter confirm", "zh-CN": "↑/↓ 移动 · 空格切换 · Enter 确认" },
|
|
65
66
|
"menu.title": { en: " · interactive menu — Ctrl+C quits, ↑/↓ selects", "zh-CN": " · 交互菜单 — Ctrl+C 退出,↑/↓ 选择" },
|
|
66
|
-
"menu.noProviders": { en: "no providers
|
|
67
|
-
"menu.noProvidersHint": { en: "no providers
|
|
68
|
-
"menu.firstScan": { en: "
|
|
67
|
+
"menu.noProviders": { en: "no providers saved in agentsw yet", "zh-CN": "agentsw 中尚未保存供应商" },
|
|
68
|
+
"menu.noProvidersHint": { en: "no providers saved in agentsw — choose Add provider or Import existing providers first", "zh-CN": "agentsw 中尚未保存供应商,请先选择“添加供应商”或“导入已有供应商”" },
|
|
69
|
+
"menu.firstScan": { en: "import providers from existing agent configs into agentsw?", "zh-CN": "是否从已有智能体配置中导入供应商到 agentsw?" },
|
|
69
70
|
"menu.what": { en: "what to do?", "zh-CN": "请选择操作" },
|
|
70
|
-
"menu.add": { en: "
|
|
71
|
-
"menu.import": { en: "
|
|
72
|
-
"menu.use": { en: "
|
|
73
|
-
"menu.status": { en: "
|
|
74
|
-
"menu.list": { en: "
|
|
75
|
-
"menu.sync": { en: "sync
|
|
76
|
-
"menu.discover": { en: "
|
|
77
|
-
"menu.remove": { en: "
|
|
78
|
-
"menu.rename": { en: "
|
|
79
|
-
"menu.renameProvider": { en: "which provider
|
|
80
|
-
"menu.newId": { en: "new provider ID", "zh-CN": "新的供应商 ID" },
|
|
81
|
-
"menu.renameConfirm": { en: "
|
|
71
|
+
"menu.add": { en: "Add / update provider (manual setup)", "zh-CN": "添加或更新供应商(手动设置)" },
|
|
72
|
+
"menu.import": { en: "Import existing providers", "zh-CN": "导入已有供应商" },
|
|
73
|
+
"menu.use": { en: "Switch provider and default model", "zh-CN": "切换供应商和默认模型" },
|
|
74
|
+
"menu.status": { en: "View each agent's current configuration", "zh-CN": "查看各智能体当前配置" },
|
|
75
|
+
"menu.list": { en: "View providers saved in agentsw", "zh-CN": "查看 agentsw 供应商列表" },
|
|
76
|
+
"menu.sync": { en: "Re-sync the current provider to agents", "zh-CN": "重新同步当前供应商" },
|
|
77
|
+
"menu.discover": { en: "Update a provider's model list", "zh-CN": "更新供应商模型列表" },
|
|
78
|
+
"menu.remove": { en: "Delete provider configuration", "zh-CN": "删除供应商配置" },
|
|
79
|
+
"menu.rename": { en: "Change a provider ID", "zh-CN": "修改供应商 ID" },
|
|
80
|
+
"menu.renameProvider": { en: "which provider ID should be changed?", "zh-CN": "选择要修改 ID 的供应商" },
|
|
81
|
+
"menu.newId": { en: "new provider ID (used in config references)", "zh-CN": "新的供应商 ID(用于配置引用)" },
|
|
82
|
+
"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
83
|
"menu.removeScope": { en: "where should this provider be removed?", "zh-CN": "从哪里删除供应商?" },
|
|
83
|
-
"menu.removeStore": { en: "
|
|
84
|
-
"menu.removeEverywhere": { en: "agentsw and matching
|
|
85
|
-
"menu.removeLocal": { en: "
|
|
86
|
-
"menu.noRemovable": { en: "no
|
|
87
|
-
"menu.removeApp": { en: "which agent should
|
|
88
|
-
"menu.
|
|
89
|
-
"menu.
|
|
90
|
-
"menu.
|
|
91
|
-
"menu.
|
|
84
|
+
"menu.removeStore": { en: "Delete only the record in agentsw", "zh-CN": "只删除 agentsw 中的记录" },
|
|
85
|
+
"menu.removeEverywhere": { en: "Delete the agentsw record and matching agent configs", "zh-CN": "同时删除 agentsw 记录和智能体配置" },
|
|
86
|
+
"menu.removeLocal": { en: "Delete only one agent's configuration", "zh-CN": "只删除某个智能体中的配置" },
|
|
87
|
+
"menu.noRemovable": { en: "no provider records found here; return to the menu to choose another scope", "zh-CN": "所选位置没有供应商记录,请返回菜单选择其他删除范围" },
|
|
88
|
+
"menu.removeApp": { en: "which agent's configuration should be changed?", "zh-CN": "选择要删除配置的智能体" },
|
|
89
|
+
"menu.removeStoreHelp": { en: "Keep all agent configs; the agents can still use this provider.", "zh-CN": "保留各智能体配置,它们仍可继续使用该供应商。" },
|
|
90
|
+
"menu.removeEverywhereHelp": { en: "Remove the saved provider and matching agent entries, including related default selections.", "zh-CN": "删除已保存的供应商及各智能体中匹配的配置,并清理相关默认选择。" },
|
|
91
|
+
"menu.removeLocalHelp": { en: "Keep agentsw and other agents unchanged; also works for providers never imported into agentsw.", "zh-CN": "保留 agentsw 和其他智能体;也支持从未导入 agentsw 的供应商。" },
|
|
92
|
+
"menu.removeConfirmStore": { en: "delete {id} from agentsw only? All agent configs stay unchanged. Files are backed up first.", "zh-CN": "确认只删除 agentsw 中的供应商 {id}?各智能体配置保持不变,修改前自动备份。" },
|
|
93
|
+
"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 记录和匹配的智能体配置?相关默认选择会一并清理,修改前自动备份。" },
|
|
94
|
+
"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 和其他智能体配置,再次同步可能恢复。修改前自动备份。" },
|
|
95
|
+
"menu.confirmRename": { en: "Change ID", "zh-CN": "确认修改 ID" },
|
|
96
|
+
"menu.confirmRemove": { en: "Delete configuration", "zh-CN": "确认删除" },
|
|
97
|
+
"menu.cancelAction": { en: "Cancel, return to menu", "zh-CN": "取消,返回菜单" },
|
|
98
|
+
"menu.apps": { en: "Check agent versions and updates", "zh-CN": "检查智能体版本和更新" },
|
|
99
|
+
"menu.language": { en: "Change language / 语言", "zh-CN": "切换界面语言 / Language" },
|
|
100
|
+
"menu.quit": { en: "Exit menu", "zh-CN": "退出菜单" },
|
|
92
101
|
"menu.yes": { en: "yes", "zh-CN": "是" },
|
|
93
102
|
"menu.no": { en: "no", "zh-CN": "否" },
|
|
94
|
-
"menu.modelSource": { en: "how
|
|
95
|
-
"menu.modelDiscover": { en: "
|
|
96
|
-
"menu.modelManual": { en: "
|
|
97
|
-
"menu.pickProvider": { en: "switch to
|
|
103
|
+
"menu.modelSource": { en: "how should the model list be obtained?", "zh-CN": "选择模型列表的获取方式" },
|
|
104
|
+
"menu.modelDiscover": { en: "Fetch from the provider API (recommended)", "zh-CN": "从供应商接口自动获取(推荐)" },
|
|
105
|
+
"menu.modelManual": { en: "Enter model IDs manually", "zh-CN": "手动输入模型 ID" },
|
|
106
|
+
"menu.pickProvider": { en: "which provider should agents switch to?", "zh-CN": "选择要切换到的供应商" },
|
|
98
107
|
"menu.defaultModel": { en: "default model", "zh-CN": "默认模型" },
|
|
99
108
|
"menu.keepDefault": { en: "keep current default ({model})", "zh-CN": "保持当前默认模型({model})" },
|
|
100
|
-
"menu.active": { en: "
|
|
101
|
-
"menu.discoverFor": { en: "
|
|
102
|
-
"menu.pushRefresh": { en: "
|
|
103
|
-
"menu.removeProvider": { en: "
|
|
109
|
+
"menu.active": { en: "current in agentsw", "zh-CN": "agentsw 当前" },
|
|
110
|
+
"menu.discoverFor": { en: "which provider's model list should be updated?", "zh-CN": "选择要更新模型列表的供应商" },
|
|
111
|
+
"menu.pushRefresh": { en: "also sync this provider to agent configs after updating its model list?", "zh-CN": "更新模型列表后,是否同时将该供应商同步到智能体配置?" },
|
|
112
|
+
"menu.removeProvider": { en: "which provider configuration should be deleted?", "zh-CN": "选择要删除配置的供应商" },
|
|
104
113
|
"menu.reallyRemove": { en: "really remove {id}?", "zh-CN": "确认删除 {id}?" },
|
|
105
114
|
"menu.pruneConfigs": { en: "also remove its entries from app configs?", "zh-CN": "同时从应用配置中移除其条目?" },
|
|
106
|
-
"menu.upgrade": { en: "upgrade
|
|
107
|
-
"menu.installApp": { en: "
|
|
115
|
+
"menu.upgrade": { en: "upgrade all installed agents that have updates?", "zh-CN": "是否升级所有有新版本的已安装智能体?" },
|
|
116
|
+
"menu.installApp": { en: "Install a new coding agent", "zh-CN": "安装新的智能体" },
|
|
108
117
|
"menu.pickApp": { en: "which agent to install?", "zh-CN": "安装哪个智能体?" },
|
|
109
118
|
"menu.installConfirm": { en: "install {name} via: {cmd}?", "zh-CN": "通过以下命令安装 {name}: {cmd}?" },
|
|
110
119
|
"menu.allInstalled": { en: "all agents already installed", "zh-CN": "所有智能体均已安装" },
|
|
111
120
|
"menu.bye": { en: "bye", "zh-CN": "再见" },
|
|
112
121
|
"add.id": { en: "provider id (slug)", "zh-CN": "供应商 id(slug)" },
|
|
113
122
|
"add.idAuto": { en: "provider id (leave blank for hostname + protocol)", "zh-CN": "供应商 ID(留空使用完整域名 + 协议)" },
|
|
114
|
-
"add.idInvalid": { en: "lowercase
|
|
123
|
+
"add.idInvalid": { en: "start with a lowercase letter or digit; use only lowercase letters, digits, - and _, e.g. my-proxy", "zh-CN": "以小写字母或数字开头,仅含小写字母、数字、-、_,如 my-proxy" },
|
|
115
124
|
"add.name": { en: "display name", "zh-CN": "显示名称" },
|
|
116
125
|
"add.protocol": { en: "wire protocol", "zh-CN": "接口协议" },
|
|
117
126
|
"add.openai": { en: "openai (chat completions)", "zh-CN": "openai (chat completions)" },
|
|
118
127
|
"add.anthropic": { en: "anthropic (messages)", "zh-CN": "anthropic (messages)" },
|
|
119
|
-
"add.baseUrl": { en: "base URL", "zh-CN": "
|
|
128
|
+
"add.baseUrl": { en: "API base URL", "zh-CN": "接口地址(Base URL)" },
|
|
120
129
|
"add.baseUrlInvalid": { en: "must start with http(s)://", "zh-CN": "必须以 http(s):// 开头" },
|
|
121
|
-
"add.apiKey": { en: "API key", "zh-CN": "API key" },
|
|
130
|
+
"add.apiKey": { en: "API key", "zh-CN": "API 密钥(API key)" },
|
|
122
131
|
"add.models": { en: "model ids (comma separated, or leave blank to auto-discover)", "zh-CN": "模型 id(逗号分隔,留空则自动发现)" },
|
|
123
132
|
"add.cancelled": { en: "cancelled", "zh-CN": "已取消" },
|
|
124
133
|
"import.already": { en: "already configured as {id}", "zh-CN": "已配置为 {id}" },
|
|
@@ -170,7 +179,21 @@ const messages = {
|
|
|
170
179
|
"quick.noProtocol": { en: "no supported protocol found at this endpoint (tried openai and anthropic)", "zh-CN": "该端点未检测到支持的协议(已尝试 openai 和 anthropic)" },
|
|
171
180
|
"quick.noModelsAfterFilter": { en: "{id}: no models left after filtering, skipping", "zh-CN": "{id}: 过滤后无剩余模型,跳过" },
|
|
172
181
|
"quick.summary": { en: "created {count} provider(s): {ids}", "zh-CN": "创建了 {count} 个供应商: {ids}" },
|
|
173
|
-
"menu.quickAdd": { en: "
|
|
182
|
+
"menu.quickAdd": { en: "Add provider (auto-detect)", "zh-CN": "添加供应商(自动识别)" },
|
|
183
|
+
"menu.quickAddHelp": { en: "Recommended: enter an API URL and key; detect protocols and models automatically.", "zh-CN": "推荐:只填接口地址和 API key,自动识别协议与模型。" },
|
|
184
|
+
"menu.addHelp": { en: "Choose the protocol and model setup yourself; use an existing ID to update a provider.", "zh-CN": "自行选择协议和模型获取方式;填写已有 ID 可更新供应商。" },
|
|
185
|
+
"menu.importHelp": { en: "Read providers from agents or cc-switch into agentsw; leave source configs unchanged.", "zh-CN": "从智能体或 cc-switch 读取配置并保存到 agentsw,不修改来源配置。" },
|
|
186
|
+
"menu.useHelp": { en: "Select a provider and default model, then write them to compatible agent configs.", "zh-CN": "选择供应商和默认模型,然后写入支持该协议的智能体配置。" },
|
|
187
|
+
"menu.statusHelp": { en: "Show detected agents, their current providers and config file locations.", "zh-CN": "查看检测到的智能体、所用供应商及配置文件位置。" },
|
|
188
|
+
"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 当前供应商写入智能体配置,不重新获取模型列表。" },
|
|
190
|
+
"menu.discoverHelp": { en: "Fetch models from the provider, fill in model metadata, and optionally sync to agents.", "zh-CN": "从供应商接口重新获取模型并补全模型参数,可选择同步到智能体。" },
|
|
191
|
+
"menu.renameHelp": { en: "Change the config ID and update references; keep custom display names.", "zh-CN": "修改配置中的标识并更新关联引用,保留自定义显示名称。" },
|
|
192
|
+
"menu.removeHelp": { en: "Choose where to delete: agentsw only, one agent only, or both.", "zh-CN": "先选择删除范围:仅 agentsw、仅某个智能体,或两处一起。" },
|
|
193
|
+
"menu.appsHelp": { en: "Compare installed and latest versions, then choose whether to upgrade.", "zh-CN": "对比已安装版本和最新版本,再选择是否升级。" },
|
|
194
|
+
"menu.installAppHelp": { en: "Choose an agent that is not installed yet, such as Claude Code, Codex or omp.", "zh-CN": "选择尚未安装的编码智能体,如 Claude Code、Codex 或 omp。" },
|
|
195
|
+
"menu.languageHelp": { en: "Choose English or 简体中文 and save the language for future runs.", "zh-CN": "选择简体中文或 English,保存为后续运行的界面语言。" },
|
|
196
|
+
"menu.quitHelp": { en: "End this menu session without performing another action.", "zh-CN": "结束本次交互,不执行其他操作。" },
|
|
174
197
|
"import.skip": { en: "skip", "zh-CN": "跳过" },
|
|
175
198
|
"import.modelsCount": { en: "{count} models", "zh-CN": "{count} 个模型" },
|
|
176
199
|
"table.protocol": { en: "PROTOCOL", "zh-CN": "协议" },
|
package/dist/import.js
CHANGED
|
@@ -12,12 +12,12 @@ export function normalizeUrl(u) {
|
|
|
12
12
|
}
|
|
13
13
|
/** True when the path already names the API version, e.g. `.../v1`. */
|
|
14
14
|
const hasApiVersion = (baseUrl) => /\/v\d+(?:beta\d*)?$/.test(baseUrl.split(/[?#]/, 1)[0]);
|
|
15
|
-
/** Apps may append
|
|
15
|
+
/** Apps may append /v1; other API versions and URL components remain significant. */
|
|
16
16
|
export function endpointKey(baseUrl) {
|
|
17
17
|
const normalized = normalizeUrl(baseUrl);
|
|
18
18
|
const suffixAt = normalized.search(/[?#]/);
|
|
19
19
|
const path = suffixAt < 0 ? normalized : normalized.slice(0, suffixAt);
|
|
20
|
-
return path.replace(/\/
|
|
20
|
+
return path.replace(/\/v1$/, "") + (suffixAt < 0 ? "" : normalized.slice(suffixAt));
|
|
21
21
|
}
|
|
22
22
|
/** An unresolved credential is not evidence that two configurations use the same account. */
|
|
23
23
|
export function findMatchingProvider(existing, candidate) {
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,17 @@ import { detectSystemLocale, extractCliLocale, normalizeLocale, setLocale, t } f
|
|
|
6
6
|
import { cmdMenu } from "./menu.js";
|
|
7
7
|
import { loadStore } from "./store.js";
|
|
8
8
|
import { cmdListLocalProviders, cmdRemoveProvider, cmdRename } from "./provider-actions.js";
|
|
9
|
+
// Locale lookup is best effort: help/version and app-local commands do not need the store.
|
|
10
|
+
function savedLocale() {
|
|
11
|
+
try {
|
|
12
|
+
return loadStore().language;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
9
18
|
// Locale precedence: CLI flag > environment > saved preference > system locale.
|
|
10
|
-
setLocale(extractCliLocale() ?? process.env.AGENTSW_LANG ??
|
|
19
|
+
setLocale(extractCliLocale() ?? process.env.AGENTSW_LANG ?? savedLocale() ?? detectSystemLocale());
|
|
11
20
|
function parseLocale(value) {
|
|
12
21
|
const locale = normalizeLocale(value);
|
|
13
22
|
if (!locale)
|
|
@@ -51,6 +60,7 @@ program
|
|
|
51
60
|
.option("-d, --discover", t("opt.discover"))
|
|
52
61
|
.option("--include <globs>", t("opt.include"))
|
|
53
62
|
.option("--exclude <globs>", t("opt.exclude"))
|
|
63
|
+
.option("--dedup", t("opt.dedup"))
|
|
54
64
|
.option("--no-dedup", t("opt.noDedup"))
|
|
55
65
|
.option("--models-dev <id>", t("opt.modelsDev"))
|
|
56
66
|
.option("-y, --yes", t("opt.yes"))
|
|
@@ -59,6 +69,9 @@ program
|
|
|
59
69
|
.command("quick")
|
|
60
70
|
.description(t("cmd.quick"))
|
|
61
71
|
.option("--id <slug>", t("opt.id"))
|
|
72
|
+
.option("--name <name>", t("opt.name"))
|
|
73
|
+
.option("--openai-api <flavor>", t("opt.openaiApi"))
|
|
74
|
+
.option("--models-dev <id>", t("opt.modelsDev"))
|
|
62
75
|
.option("--base-url <url>", t("opt.baseUrl"))
|
|
63
76
|
.option("--api-key <key>", t("opt.apiKey"))
|
|
64
77
|
.option("--default-model <id>", t("opt.defaultModel"))
|
|
@@ -66,6 +79,7 @@ program
|
|
|
66
79
|
.option("--reasoning-effort <level>", t("opt.reasoning"))
|
|
67
80
|
.option("--include <globs>", t("opt.include"))
|
|
68
81
|
.option("--exclude <globs>", t("opt.exclude"))
|
|
82
|
+
.option("--dedup", t("opt.dedup"))
|
|
69
83
|
.option("--no-dedup", t("opt.noDedup"))
|
|
70
84
|
.option("-y, --yes", t("opt.yes"))
|
|
71
85
|
.action(cmdQuickAdd);
|
|
@@ -122,6 +136,7 @@ program
|
|
|
122
136
|
.option("-a, --apps <apps>", t("opt.appsSync"))
|
|
123
137
|
.option("--include <globs>", t("opt.setInclude"))
|
|
124
138
|
.option("--exclude <globs>", t("opt.setExclude"))
|
|
139
|
+
.option("--dedup", t("opt.dedup"))
|
|
125
140
|
.option("--no-dedup", t("opt.noDedup"))
|
|
126
141
|
.option("--no-filter", t("opt.noFilter"))
|
|
127
142
|
.action(cmdDiscover);
|
package/dist/jsonc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { 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);
|
|
@@ -34,15 +34,9 @@ function parseJsoncObject(file, text) {
|
|
|
34
34
|
}
|
|
35
35
|
/** Only missing files initialize to an empty object; unreadable or invalid files must not be replaced. */
|
|
36
36
|
export function readJsoncObject(file) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
if (error.code === "ENOENT")
|
|
43
|
-
return { file, text: "{}\n", value: {} };
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
37
|
+
const text = readTextIfExists(file);
|
|
38
|
+
if (text === undefined)
|
|
39
|
+
return { file, text: "{}\n", value: {} };
|
|
46
40
|
return { file, text, value: parseJsoncObject(file, text) };
|
|
47
41
|
}
|
|
48
42
|
function comments(text) {
|
package/dist/menu.js
CHANGED
|
@@ -13,8 +13,8 @@ function bye() {
|
|
|
13
13
|
process.exit(0);
|
|
14
14
|
}
|
|
15
15
|
const cancel = { onCancel: () => bye() };
|
|
16
|
-
async function askToggle(message, initial = false) {
|
|
17
|
-
const { v } = await prompts({ type: "toggle", name: "v", message, active: t("menu.yes"), inactive: t("menu.no"), initial }, cancel);
|
|
16
|
+
async function askToggle(message, initial = false, labels) {
|
|
17
|
+
const { v } = await prompts({ type: "toggle", name: "v", message, active: labels?.active ?? t("menu.yes"), inactive: labels?.inactive ?? t("menu.no"), initial }, cancel);
|
|
18
18
|
return v === true;
|
|
19
19
|
}
|
|
20
20
|
async function chooseLanguage() {
|
|
@@ -77,7 +77,9 @@ async function renameFromMenu() {
|
|
|
77
77
|
return;
|
|
78
78
|
const next = String(newId).trim();
|
|
79
79
|
await cmdRename(picked.id, next, { dryRun: true });
|
|
80
|
-
if (await askToggle(t("menu.renameConfirm", { oldId: picked.id, newId: next })
|
|
80
|
+
if (await askToggle(t("menu.renameConfirm", { oldId: picked.id, newId: next }), false, {
|
|
81
|
+
active: t("menu.confirmRename"), inactive: t("menu.cancelAction"),
|
|
82
|
+
})) {
|
|
81
83
|
await cmdRename(picked.id, next);
|
|
82
84
|
}
|
|
83
85
|
}
|
|
@@ -88,9 +90,9 @@ async function removeFromMenu() {
|
|
|
88
90
|
message: t("menu.removeScope"),
|
|
89
91
|
hint: t("menu.selectInstructions"),
|
|
90
92
|
choices: [
|
|
91
|
-
{ title: t("menu.removeStore"), value: "store" },
|
|
92
|
-
{ title: t("menu.
|
|
93
|
-
{ title: t("menu.
|
|
93
|
+
{ title: t("menu.removeStore"), description: t("menu.removeStoreHelp"), value: "store" },
|
|
94
|
+
{ title: t("menu.removeLocal"), description: t("menu.removeLocalHelp"), value: "local" },
|
|
95
|
+
{ title: t("menu.removeEverywhere"), description: t("menu.removeEverywhereHelp"), value: "everywhere" },
|
|
94
96
|
],
|
|
95
97
|
}, cancel);
|
|
96
98
|
if (!scope)
|
|
@@ -130,8 +132,12 @@ async function removeFromMenu() {
|
|
|
130
132
|
return;
|
|
131
133
|
const opts = scope === "local" ? { apps: picked.app } : { prune: scope === "everywhere" };
|
|
132
134
|
await cmdRemoveProvider(picked.id, { ...opts, dryRun: true });
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
+
const confirmation = picked.app
|
|
136
|
+
? t("menu.removeConfirmLocal", { id: picked.id, app: picked.app })
|
|
137
|
+
: t(scope === "everywhere" ? "menu.removeConfirmEverywhere" : "menu.removeConfirmStore", { id: picked.id });
|
|
138
|
+
if (await askToggle(confirmation, false, {
|
|
139
|
+
active: t("menu.confirmRemove"), inactive: t("menu.cancelAction"),
|
|
140
|
+
})) {
|
|
135
141
|
await cmdRemoveProvider(picked.id, opts);
|
|
136
142
|
}
|
|
137
143
|
}
|
|
@@ -152,20 +158,20 @@ export async function cmdMenu() {
|
|
|
152
158
|
message: t("menu.what"),
|
|
153
159
|
hint: t("menu.selectInstructions"),
|
|
154
160
|
choices: [
|
|
155
|
-
{ title: t("menu.
|
|
156
|
-
{ title: t("menu.
|
|
157
|
-
{ title: t("menu.import"), value: "import" },
|
|
158
|
-
{ title: t("menu.use"), value: "use" },
|
|
159
|
-
{ title: t("menu.status"), value: "status" },
|
|
160
|
-
{ title: t("menu.list"), value: "list" },
|
|
161
|
-
{ title: t("menu.sync"), value: "sync" },
|
|
162
|
-
{ title: t("menu.discover"), value: "discover" },
|
|
163
|
-
{ title: t("menu.rename"), value: "rename" },
|
|
164
|
-
{ title: t("menu.remove"), value: "remove" },
|
|
165
|
-
{ title: t("menu.apps"), value: "apps" },
|
|
166
|
-
{ title: t("menu.installApp"), value: "install" },
|
|
167
|
-
{ title: t("menu.language"), value: "language" },
|
|
168
|
-
{ title: t("menu.quit"), value: "quit" },
|
|
161
|
+
{ title: t("menu.quickAdd"), description: t("menu.quickAddHelp"), value: "quickAdd" },
|
|
162
|
+
{ title: t("menu.add"), description: t("menu.addHelp"), value: "add" },
|
|
163
|
+
{ title: t("menu.import"), description: t("menu.importHelp"), value: "import" },
|
|
164
|
+
{ title: t("menu.use"), description: t("menu.useHelp"), value: "use" },
|
|
165
|
+
{ title: t("menu.status"), description: t("menu.statusHelp"), value: "status" },
|
|
166
|
+
{ title: t("menu.list"), description: t("menu.listHelp"), value: "list" },
|
|
167
|
+
{ title: t("menu.sync"), description: t("menu.syncHelp"), value: "sync" },
|
|
168
|
+
{ title: t("menu.discover"), description: t("menu.discoverHelp"), value: "discover" },
|
|
169
|
+
{ title: t("menu.rename"), description: t("menu.renameHelp"), value: "rename" },
|
|
170
|
+
{ title: t("menu.remove"), description: t("menu.removeHelp"), value: "remove" },
|
|
171
|
+
{ title: t("menu.apps"), description: t("menu.appsHelp"), value: "apps" },
|
|
172
|
+
{ title: t("menu.installApp"), description: t("menu.installAppHelp"), value: "install" },
|
|
173
|
+
{ title: t("menu.language"), description: t("menu.languageHelp"), value: "language" },
|
|
174
|
+
{ title: t("menu.quit"), description: t("menu.quitHelp"), value: "quit" },
|
|
169
175
|
],
|
|
170
176
|
}, cancel);
|
|
171
177
|
if (action === undefined || action === "quit")
|
package/dist/modelsdev.js
CHANGED
|
@@ -75,9 +75,13 @@ export function findModelMeta(catalog, modelId, providerHint) {
|
|
|
75
75
|
if (m)
|
|
76
76
|
return { spec: toModelSpec(modelId, m), provider: hinted.id };
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
const providers = Object.values(catalog).filter((p) => p !== hinted);
|
|
79
|
+
for (const p of providers) {
|
|
80
|
+
const m = p.models[modelId];
|
|
81
|
+
if (m)
|
|
82
|
+
return { spec: toModelSpec(modelId, m), provider: p.id };
|
|
83
|
+
}
|
|
84
|
+
for (const p of providers) {
|
|
81
85
|
const m = scan(p);
|
|
82
86
|
if (m)
|
|
83
87
|
return { spec: toModelSpec(modelId, m), provider: p.id };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** Previous releases used this lossy encoding; recognize it, never generate it. */
|
|
3
|
+
export function legacyManagedCredentialRef(id) {
|
|
4
|
+
return `AGENTSW_${id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_API_KEY`;
|
|
5
|
+
}
|
|
6
|
+
/** Human-readable prefix plus an exact, case-sensitive provider identity digest. */
|
|
7
|
+
export function managedCredentialRef(id) {
|
|
8
|
+
const prefix = id.toUpperCase().replace(/[^A-Z0-9]/g, "_").slice(0, 48) || "PROVIDER";
|
|
9
|
+
const digest = createHash("sha256").update(id).digest("hex").slice(0, 24).toUpperCase();
|
|
10
|
+
return `AGENTSW_${prefix}_${digest}_API_KEY`;
|
|
11
|
+
}
|
|
12
|
+
export function isManagedCredentialRef(ref, id) {
|
|
13
|
+
return ref === managedCredentialRef(id) || ref === legacyManagedCredentialRef(id);
|
|
14
|
+
}
|
|
15
|
+
/** Account-qualified app-local selector; never embeds endpoints or credentials. */
|
|
16
|
+
export function localProviderId(candidate) {
|
|
17
|
+
const identity = JSON.stringify([
|
|
18
|
+
candidate.id,
|
|
19
|
+
candidate.protocol,
|
|
20
|
+
candidate.baseUrl,
|
|
21
|
+
candidate.apiKey ?? null,
|
|
22
|
+
candidate.keyEnv ?? null,
|
|
23
|
+
]);
|
|
24
|
+
return `local-${createHash("sha256").update(identity).digest("hex").slice(0, 32)}`;
|
|
25
|
+
}
|