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