agentsw 0.3.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/LICENSE +21 -0
- package/README.md +227 -0
- package/README.zh-CN.md +221 -0
- package/dist/apps.js +178 -0
- package/dist/commands.js +661 -0
- package/dist/discover.js +21 -0
- package/dist/filter.js +61 -0
- package/dist/fsutil.js +56 -0
- package/dist/i18n.js +214 -0
- package/dist/import.js +66 -0
- package/dist/index.js +113 -0
- package/dist/menu.js +157 -0
- package/dist/modelsdev.js +110 -0
- package/dist/slug.js +18 -0
- package/dist/sources/ccswitch.js +151 -0
- package/dist/store.js +23 -0
- package/dist/targets/claudecode.js +96 -0
- package/dist/targets/codex.js +136 -0
- package/dist/targets/dsh.js +296 -0
- package/dist/targets/hermes.js +173 -0
- package/dist/targets/index.js +26 -0
- package/dist/targets/omp.js +170 -0
- package/dist/targets/opencode.js +135 -0
- package/dist/targets/pistyle.js +208 -0
- package/dist/targets/types.js +1 -0
- package/dist/targets/wire.js +109 -0
- package/dist/targets/workbuddy.js +143 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
package/dist/commands.js
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import prompts from "prompts";
|
|
3
|
+
import { loadStore, saveStore, getProvider, configFile } from "./store.js";
|
|
4
|
+
import { scanCandidates, normalizeUrl } from "./import.js";
|
|
5
|
+
import { enrichModels, loadCatalog, searchCatalog } from "./modelsdev.js";
|
|
6
|
+
import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
|
|
7
|
+
import { discoverProviderModels } from "./discover.js";
|
|
8
|
+
import { appPackages, installedVersion, isNewer, latestVersion, runShell } from "./apps.js";
|
|
9
|
+
import { drainPendingWrites, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
10
|
+
import { applyModelFilter } from "./filter.js";
|
|
11
|
+
import { t } from "./i18n.js";
|
|
12
|
+
function fail(message) {
|
|
13
|
+
process.stderr.write(pc.red(`error: ${message}\n`));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
function table(rows, header) {
|
|
17
|
+
const all = header ? [header, ...rows] : rows;
|
|
18
|
+
if (all.length === 0)
|
|
19
|
+
return "";
|
|
20
|
+
const widths = [];
|
|
21
|
+
for (const row of all) {
|
|
22
|
+
row.forEach((cell, i) => {
|
|
23
|
+
// eslint-disable-next-line no-control-regex
|
|
24
|
+
const len = cell.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
25
|
+
widths[i] = Math.max(widths[i] ?? 0, len);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const render = (row) => row
|
|
29
|
+
.map((cell, i) => {
|
|
30
|
+
const len = cell.replace(/\u001b\[[0-9;]*m/g, "").length;
|
|
31
|
+
return cell + " ".repeat((widths[i] ?? 0) - len);
|
|
32
|
+
})
|
|
33
|
+
.join(" ")
|
|
34
|
+
.trimEnd();
|
|
35
|
+
const lines = all.map(render);
|
|
36
|
+
if (header)
|
|
37
|
+
lines.splice(1, 0, widths.map((w) => "-".repeat(w)).join(" "));
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
function fmtTokens(n) {
|
|
41
|
+
if (!n)
|
|
42
|
+
return "-";
|
|
43
|
+
if (n >= 1_000_000)
|
|
44
|
+
return `${(n / 1_000_000).toFixed(n % 1_000_000 ? 1 : 0)}M`;
|
|
45
|
+
if (n >= 1000)
|
|
46
|
+
return `${Math.round(n / 1000)}K`;
|
|
47
|
+
return String(n);
|
|
48
|
+
}
|
|
49
|
+
function modelRows(models) {
|
|
50
|
+
return models.map((m) => [
|
|
51
|
+
m.id,
|
|
52
|
+
fmtTokens(m.contextWindow),
|
|
53
|
+
fmtTokens(m.maxInput),
|
|
54
|
+
fmtTokens(m.maxOutput),
|
|
55
|
+
m.reasoning ? (m.reasoningEfforts?.join("/") ?? "yes") : "no",
|
|
56
|
+
m.cost ? `$${m.cost.input ?? "?"}/$${m.cost.output ?? "?"}` : "-",
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
const MODEL_HEADER = ["MODEL", "CTX", "IN", "OUT", "REASONING", "$IN/$OUT per M"];
|
|
60
|
+
/** Guess the models.dev provider whose API host matches the configured baseUrl. */
|
|
61
|
+
function guessProviderHint(catalog, baseUrl) {
|
|
62
|
+
if (!catalog)
|
|
63
|
+
return undefined;
|
|
64
|
+
let host;
|
|
65
|
+
try {
|
|
66
|
+
host = new URL(baseUrl).host;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
for (const p of Object.values(catalog)) {
|
|
72
|
+
if (!p.api)
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
if (new URL(p.api).host === host)
|
|
76
|
+
return p.id;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
/* ignore malformed catalog urls */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
function parseFilterOpts(opts) {
|
|
85
|
+
const split = (s) => s?.split(",").map((x) => x.trim()).filter(Boolean);
|
|
86
|
+
const include = split(opts.include);
|
|
87
|
+
const exclude = split(opts.exclude);
|
|
88
|
+
// dedup (dropping snapshot duplicates) is on by default; persist only the opt-out
|
|
89
|
+
if (!include?.length && !exclude?.length && opts.dedup !== false)
|
|
90
|
+
return undefined;
|
|
91
|
+
return { include, exclude, ...(opts.dedup === false ? { dedup: false } : {}) };
|
|
92
|
+
}
|
|
93
|
+
function reportDropped(dropped) {
|
|
94
|
+
if (!dropped.length)
|
|
95
|
+
return;
|
|
96
|
+
console.log(pc.dim(`filtered out ${dropped.length} model(s):`));
|
|
97
|
+
for (const d of dropped)
|
|
98
|
+
console.log(pc.dim(` - ${d.id} (${d.reason})`));
|
|
99
|
+
}
|
|
100
|
+
export async function cmdAdd(opts) {
|
|
101
|
+
const store = loadStore();
|
|
102
|
+
const interactive = process.stdin.isTTY && !opts.yes;
|
|
103
|
+
let answers = {};
|
|
104
|
+
if (interactive) {
|
|
105
|
+
prompts.override({
|
|
106
|
+
id: opts.id,
|
|
107
|
+
name: opts.name,
|
|
108
|
+
protocol: opts.protocol,
|
|
109
|
+
baseUrl: opts.baseUrl,
|
|
110
|
+
apiKey: opts.apiKey,
|
|
111
|
+
models: opts.models,
|
|
112
|
+
openaiApi: opts.openaiApi,
|
|
113
|
+
});
|
|
114
|
+
answers = await prompts([
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
name: "id",
|
|
118
|
+
message: t("add.id"),
|
|
119
|
+
validate: (v) => (/^[a-z0-9][a-z0-9_-]*$/.test(v) ? true : t("add.idInvalid")),
|
|
120
|
+
},
|
|
121
|
+
{ type: "text", name: "name", message: t("add.name"), initial: (prev) => prev },
|
|
122
|
+
{
|
|
123
|
+
type: "select",
|
|
124
|
+
name: "protocol",
|
|
125
|
+
message: t("add.protocol"),
|
|
126
|
+
hint: t("menu.selectInstructions"),
|
|
127
|
+
choices: [
|
|
128
|
+
{ title: t("add.openai"), value: "openai" },
|
|
129
|
+
{ title: t("add.anthropic"), value: "anthropic" },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
// only openai endpoints have two wires; anthropic skips the question
|
|
134
|
+
type: (_prev, values) => values.protocol === "openai" ? "select" : null,
|
|
135
|
+
name: "openaiApi",
|
|
136
|
+
message: t("add.openaiApi"),
|
|
137
|
+
hint: t("menu.selectInstructions"),
|
|
138
|
+
choices: [
|
|
139
|
+
{ title: t("add.completions"), value: "completions" },
|
|
140
|
+
{ title: t("add.responses"), value: "responses" },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
{ type: "text", name: "baseUrl", message: t("add.baseUrl"), validate: (v) => (/^https?:\/\//.test(v) ? true : t("add.baseUrlInvalid")) },
|
|
144
|
+
{ type: "password", name: "apiKey", message: t("add.apiKey") },
|
|
145
|
+
{
|
|
146
|
+
type: opts.discover ? null : "text",
|
|
147
|
+
name: "models",
|
|
148
|
+
message: t("add.models"),
|
|
149
|
+
},
|
|
150
|
+
], { onCancel: () => fail(t("add.cancelled")) });
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const required = opts.discover
|
|
154
|
+
? ["id", "protocol", "baseUrl", "apiKey"]
|
|
155
|
+
: ["id", "protocol", "baseUrl", "apiKey", "models"];
|
|
156
|
+
for (const field of required) {
|
|
157
|
+
if (!opts[field])
|
|
158
|
+
fail(t("add.fieldRequired", { field: field === "baseUrl" ? "base-url" : field === "apiKey" ? "api-key" : field }));
|
|
159
|
+
}
|
|
160
|
+
answers = {
|
|
161
|
+
id: opts.id,
|
|
162
|
+
name: opts.name ?? opts.id,
|
|
163
|
+
protocol: opts.protocol,
|
|
164
|
+
baseUrl: opts.baseUrl,
|
|
165
|
+
apiKey: opts.apiKey,
|
|
166
|
+
models: opts.models ?? "",
|
|
167
|
+
...(opts.openaiApi ? { openaiApi: opts.openaiApi } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const protocol = answers.protocol;
|
|
171
|
+
if (protocol !== "openai" && protocol !== "anthropic")
|
|
172
|
+
fail(t("add.protocolInvalid"));
|
|
173
|
+
const wire = answers.openaiApi ?? opts.openaiApi;
|
|
174
|
+
if (wire && wire !== "completions" && wire !== "responses")
|
|
175
|
+
fail(t("add.openaiApiInvalid"));
|
|
176
|
+
const openaiApi = protocol === "openai" ? wire : undefined;
|
|
177
|
+
const baseUrl = answers.baseUrl.replace(/\/+$/, "");
|
|
178
|
+
const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
179
|
+
let modelIds = manualIds;
|
|
180
|
+
const modelFilter = parseFilterOpts(opts);
|
|
181
|
+
if (opts.discover) {
|
|
182
|
+
process.stderr.write(pc.dim(`${t("add.discovering", { url: baseUrl })}\n`));
|
|
183
|
+
const discovered = await discoverProviderModels({ baseUrl, apiKey: answers.apiKey, protocol });
|
|
184
|
+
console.log(t("add.providerLists", { count: discovered.length }));
|
|
185
|
+
modelIds = [...new Set([...manualIds, ...discovered])];
|
|
186
|
+
}
|
|
187
|
+
const pinned = [...manualIds, ...(opts.defaultModel ? [opts.defaultModel] : [])];
|
|
188
|
+
const outcome = applyModelFilter(modelIds, modelFilter, pinned);
|
|
189
|
+
reportDropped(outcome.dropped);
|
|
190
|
+
modelIds = outcome.kept;
|
|
191
|
+
if (modelIds.length === 0)
|
|
192
|
+
fail(t("add.atLeastOne"));
|
|
193
|
+
const catalog = await loadCatalog();
|
|
194
|
+
const hint = opts.modelsDev ?? guessProviderHint(catalog, answers.baseUrl);
|
|
195
|
+
const models = enrichModels(catalog, modelIds, hint);
|
|
196
|
+
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
197
|
+
const defaultModel = opts.defaultModel ?? modelIds[0];
|
|
198
|
+
if (!modelIds.includes(defaultModel))
|
|
199
|
+
fail(t("add.defaultMissing", { model: defaultModel }));
|
|
200
|
+
if (opts.smallModel && !modelIds.includes(opts.smallModel))
|
|
201
|
+
fail(t("add.smallMissing", { model: opts.smallModel }));
|
|
202
|
+
const provider = {
|
|
203
|
+
id: answers.id,
|
|
204
|
+
name: answers.name || answers.id,
|
|
205
|
+
protocol,
|
|
206
|
+
...(openaiApi ? { openaiApi } : {}),
|
|
207
|
+
baseUrl,
|
|
208
|
+
apiKey: answers.apiKey,
|
|
209
|
+
models,
|
|
210
|
+
defaultModel,
|
|
211
|
+
smallModel: opts.smallModel,
|
|
212
|
+
reasoningEffort: opts.reasoningEffort,
|
|
213
|
+
modelsDevId: hint,
|
|
214
|
+
modelFilter,
|
|
215
|
+
};
|
|
216
|
+
const existed = store.providers[provider.id] !== undefined;
|
|
217
|
+
store.providers[provider.id] = provider;
|
|
218
|
+
if (!store.active)
|
|
219
|
+
store.active = provider.id;
|
|
220
|
+
saveStore(store);
|
|
221
|
+
const savedStatus = t(existed ? "add.updated" : "add.added");
|
|
222
|
+
console.log(pc.green(t("add.saved", { status: savedStatus, id: pc.bold(provider.id), protocol })));
|
|
223
|
+
console.log(t("add.metadata", { matched, total: models.length }) + (hint ? t("add.providerHint", { hint }) : ""));
|
|
224
|
+
console.log(table(modelRows(models), MODEL_HEADER));
|
|
225
|
+
console.log(pc.dim(`\n${t("add.next", { id: provider.id })}`));
|
|
226
|
+
}
|
|
227
|
+
export function cmdList() {
|
|
228
|
+
const store = loadStore();
|
|
229
|
+
const ids = Object.keys(store.providers);
|
|
230
|
+
if (ids.length === 0) {
|
|
231
|
+
console.log(t("list.none", { file: configFile }));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const rows = ids.map((id) => {
|
|
235
|
+
const p = store.providers[id];
|
|
236
|
+
return [
|
|
237
|
+
store.active === id ? pc.green("*") : " ",
|
|
238
|
+
id,
|
|
239
|
+
p.protocol,
|
|
240
|
+
p.baseUrl,
|
|
241
|
+
p.defaultModel,
|
|
242
|
+
String(p.models.length),
|
|
243
|
+
];
|
|
244
|
+
});
|
|
245
|
+
console.log(table(rows, [" ", "ID", t("table.protocol"), "BASE URL", t("table.defaultModel"), t("table.models")]));
|
|
246
|
+
}
|
|
247
|
+
export async function cmdRemove(id, opts) {
|
|
248
|
+
const store = loadStore();
|
|
249
|
+
const provider = getProvider(store, id);
|
|
250
|
+
if (opts.prune) {
|
|
251
|
+
console.log(`${t("remove.pruning", { id: pc.bold(id) })}\n`);
|
|
252
|
+
reportResults(await runTargets("prune", provider, undefined));
|
|
253
|
+
console.log("");
|
|
254
|
+
}
|
|
255
|
+
delete store.providers[id];
|
|
256
|
+
if (store.active === id)
|
|
257
|
+
store.active = undefined;
|
|
258
|
+
saveStore(store);
|
|
259
|
+
console.log(pc.green(t("remove.removed", { id })));
|
|
260
|
+
if (!opts.prune)
|
|
261
|
+
console.log(pc.dim(t("remove.note")));
|
|
262
|
+
}
|
|
263
|
+
export async function cmdPrune(id, opts) {
|
|
264
|
+
const store = loadStore();
|
|
265
|
+
const provider = getProvider(store, id);
|
|
266
|
+
console.log(`${t("remove.pruning", { id: pc.bold(id) })}\n`);
|
|
267
|
+
reportResults(await runTargets("prune", provider, opts.apps));
|
|
268
|
+
}
|
|
269
|
+
function reportResults(results) {
|
|
270
|
+
for (const r of results) {
|
|
271
|
+
if (r.skipped) {
|
|
272
|
+
console.log(`${pc.yellow(t("common.skip"))} ${r.app.padEnd(9)} ${pc.dim(r.skipped)}`);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
console.log(`${pc.green("ok ")} ${r.app.padEnd(9)} ${r.changed.join(", ")}`);
|
|
276
|
+
for (const note of r.notes)
|
|
277
|
+
console.log(` ${" ".repeat(9)} ${pc.dim(note)}`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function runTargets(op, provider, appsFilter) {
|
|
281
|
+
const selected = resolveTargets(appsFilter);
|
|
282
|
+
const explicit = appsFilter !== undefined && appsFilter !== "all";
|
|
283
|
+
const results = [];
|
|
284
|
+
for (const target of selected) {
|
|
285
|
+
if (op === "apply" && !supportsProtocol(target, provider.protocol)) {
|
|
286
|
+
results.push({
|
|
287
|
+
app: target.id,
|
|
288
|
+
changed: [],
|
|
289
|
+
notes: [],
|
|
290
|
+
skipped: `${target.name} does not support ${provider.protocol}-protocol providers`,
|
|
291
|
+
});
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (!target.detect() && !explicit) {
|
|
295
|
+
results.push({ app: target.id, changed: [], notes: [], skipped: `${target.name} not detected (pass --apps ${target.id} to force)` });
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
results.push(await target[op](provider));
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${err.message}`) });
|
|
303
|
+
process.exitCode = 1;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return results;
|
|
307
|
+
}
|
|
308
|
+
/** naive line diff for config previews: order-preserving added/removed lines */
|
|
309
|
+
function printFileDiff(file, next) {
|
|
310
|
+
const before = readTextIfExists(file) ?? "";
|
|
311
|
+
if (before === next) {
|
|
312
|
+
console.log(`${pc.dim("unchanged")} ${file}`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
console.log(pc.bold(`--- ${file}`));
|
|
316
|
+
const oldLines = before.split("\n");
|
|
317
|
+
const newLines = next.split("\n");
|
|
318
|
+
const oldSeen = {};
|
|
319
|
+
for (const l of oldLines)
|
|
320
|
+
oldSeen[l] = (oldSeen[l] ?? 0) + 1;
|
|
321
|
+
const newSeen = {};
|
|
322
|
+
for (const l of newLines)
|
|
323
|
+
newSeen[l] = (newSeen[l] ?? 0) + 1;
|
|
324
|
+
for (const l of oldLines) {
|
|
325
|
+
if ((newSeen[l] ?? 0) > 0)
|
|
326
|
+
newSeen[l]--;
|
|
327
|
+
else
|
|
328
|
+
console.log(pc.red(`- ${l}`));
|
|
329
|
+
}
|
|
330
|
+
for (const l of newLines) {
|
|
331
|
+
if ((oldSeen[l] ?? 0) > 0)
|
|
332
|
+
oldSeen[l]--;
|
|
333
|
+
else
|
|
334
|
+
console.log(pc.green(`+ ${l}`));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
338
|
+
if (!dryRun) {
|
|
339
|
+
reportResults(await runTargets(op, provider, apps));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
setDryRun(true);
|
|
343
|
+
try {
|
|
344
|
+
const results = await runTargets(op, provider, apps);
|
|
345
|
+
const writes = drainPendingWrites();
|
|
346
|
+
for (const r of results) {
|
|
347
|
+
if (r.skipped)
|
|
348
|
+
console.log(`${pc.yellow("skip")} ${r.app.padEnd(9)} ${pc.dim(r.skipped)}`);
|
|
349
|
+
}
|
|
350
|
+
console.log(pc.bold(`\ndry run — ${writes.length} file(s) would be written:\n`));
|
|
351
|
+
for (const w of writes)
|
|
352
|
+
printFileDiff(w.file, w.content);
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
setDryRun(false);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
export async function cmdUse(id, opts) {
|
|
359
|
+
const store = loadStore();
|
|
360
|
+
const provider = getProvider(store, id);
|
|
361
|
+
if (opts.model) {
|
|
362
|
+
if (!provider.models.some((m) => m.id === opts.model)) {
|
|
363
|
+
fail(t("use.modelMissing", { model: opts.model, id, have: provider.models.map((m) => m.id).join(", ") }));
|
|
364
|
+
}
|
|
365
|
+
provider.defaultModel = opts.model;
|
|
366
|
+
}
|
|
367
|
+
if (!opts.dryRun) {
|
|
368
|
+
store.active = id;
|
|
369
|
+
saveStore(store);
|
|
370
|
+
}
|
|
371
|
+
console.log(`${t("use.switching", { id: pc.bold(id), protocol: provider.protocol, model: provider.defaultModel })}\n`);
|
|
372
|
+
await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
373
|
+
}
|
|
374
|
+
export async function cmdSync(opts) {
|
|
375
|
+
const store = loadStore();
|
|
376
|
+
const id = opts.provider ?? store.active;
|
|
377
|
+
if (!id)
|
|
378
|
+
fail(t("sync.noActive"));
|
|
379
|
+
const provider = getProvider(store, id);
|
|
380
|
+
console.log(`${t("sync.syncing", { id: pc.bold(id), model: provider.defaultModel })}\n`);
|
|
381
|
+
await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
382
|
+
}
|
|
383
|
+
export function cmdStatus() {
|
|
384
|
+
const store = loadStore();
|
|
385
|
+
console.log(t("status.config", { file: configFile }));
|
|
386
|
+
console.log(`${t("status.active", { id: store.active ? pc.bold(store.active) : pc.dim(t("status.none")) })}\n`);
|
|
387
|
+
const rows = targets.map((target) => [
|
|
388
|
+
target.id,
|
|
389
|
+
target.detect() ? pc.green(t("common.yes")) : pc.dim(t("common.no")),
|
|
390
|
+
target.protocols.join("+"),
|
|
391
|
+
target.current() ?? pc.dim("-"),
|
|
392
|
+
pc.dim(target.configPaths[0] ?? ""),
|
|
393
|
+
]);
|
|
394
|
+
console.log(table(rows, ["APP", t("table.found"), t("table.protocols"), t("table.current"), t("table.config")]));
|
|
395
|
+
}
|
|
396
|
+
export async function cmdModels(query, opts) {
|
|
397
|
+
if (opts.provider) {
|
|
398
|
+
const store = loadStore();
|
|
399
|
+
const provider = getProvider(store, opts.provider);
|
|
400
|
+
console.log(`${pc.bold(provider.id)} (${provider.protocol}) · ${provider.baseUrl}`);
|
|
401
|
+
console.log(table(modelRows(provider.models), MODEL_HEADER));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const catalog = await loadCatalog({ refresh: opts.refresh });
|
|
405
|
+
if (!catalog)
|
|
406
|
+
fail("models.dev catalog unavailable (offline and no cache)");
|
|
407
|
+
if (!query)
|
|
408
|
+
fail("usage: agentsw models <query> | agentsw models --provider <id>");
|
|
409
|
+
const limit = opts.limit ? Number(opts.limit) : 30;
|
|
410
|
+
const hits = searchCatalog(catalog, query, limit);
|
|
411
|
+
if (hits.length === 0) {
|
|
412
|
+
console.log(`no models.dev entries match "${query}"`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const rows = hits.map((h) => {
|
|
416
|
+
const row = modelRows([h.spec])[0];
|
|
417
|
+
return [h.provider, ...row];
|
|
418
|
+
});
|
|
419
|
+
console.log(table(rows, ["PROVIDER", ...MODEL_HEADER]));
|
|
420
|
+
}
|
|
421
|
+
export async function cmdRefreshMeta() {
|
|
422
|
+
const store = loadStore();
|
|
423
|
+
const catalog = await loadCatalog({ refresh: true });
|
|
424
|
+
if (!catalog)
|
|
425
|
+
fail("models.dev catalog unavailable");
|
|
426
|
+
let updated = 0;
|
|
427
|
+
for (const provider of Object.values(store.providers)) {
|
|
428
|
+
const before = JSON.stringify(provider.models);
|
|
429
|
+
provider.models = enrichModels(catalog, provider.models.map((m) => m.id), provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl));
|
|
430
|
+
if (JSON.stringify(provider.models) !== before)
|
|
431
|
+
updated++;
|
|
432
|
+
}
|
|
433
|
+
saveStore(store);
|
|
434
|
+
console.log(pc.green(`refreshed models.dev metadata (${updated} provider(s) changed)`));
|
|
435
|
+
if (updated > 0)
|
|
436
|
+
console.log(pc.dim("run `agentsw sync` to push updated metadata into app configs"));
|
|
437
|
+
}
|
|
438
|
+
export async function cmdDiscover(id, opts) {
|
|
439
|
+
const store = loadStore();
|
|
440
|
+
const provider = getProvider(store, id);
|
|
441
|
+
// flags override and re-persist the filter; --no-filter clears it
|
|
442
|
+
const flagFilter = parseFilterOpts(opts);
|
|
443
|
+
if (opts.filter === false)
|
|
444
|
+
provider.modelFilter = undefined;
|
|
445
|
+
else if (flagFilter)
|
|
446
|
+
provider.modelFilter = flagFilter;
|
|
447
|
+
process.stderr.write(pc.dim(`discovering models from ${provider.baseUrl} ...\n`));
|
|
448
|
+
const listed = await discoverProviderModels(provider);
|
|
449
|
+
const outcome = applyModelFilter(listed, provider.modelFilter, [provider.defaultModel]);
|
|
450
|
+
reportDropped(outcome.dropped);
|
|
451
|
+
const ids = outcome.kept;
|
|
452
|
+
const known = provider.models.map((m) => m.id);
|
|
453
|
+
const added = ids.filter((m) => !known.includes(m));
|
|
454
|
+
const gone = known.filter((m) => !ids.includes(m));
|
|
455
|
+
const catalog = await loadCatalog();
|
|
456
|
+
provider.models = enrichModels(catalog, ids, provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl));
|
|
457
|
+
if (!ids.includes(provider.defaultModel)) {
|
|
458
|
+
console.log(pc.yellow(`default model ${provider.defaultModel} no longer listed; keeping it anyway`));
|
|
459
|
+
provider.models.push({ id: provider.defaultModel });
|
|
460
|
+
}
|
|
461
|
+
saveStore(store);
|
|
462
|
+
console.log(`${pc.bold(id)}: ${ids.length} models (${pc.green(`+${added.length}`)} / ${pc.red(`-${gone.length}`)})` +
|
|
463
|
+
(added.length ? `\n new: ${added.join(", ")}` : "") +
|
|
464
|
+
(gone.length ? `\n removed upstream: ${gone.join(", ")}` : ""));
|
|
465
|
+
console.log(table(modelRows(provider.models), MODEL_HEADER));
|
|
466
|
+
if (opts.sync) {
|
|
467
|
+
console.log("");
|
|
468
|
+
await cmdSync({ provider: id, apps: opts.apps });
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
console.log(pc.dim("\nrun `agentsw sync` to push into app configs"));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function collectAppRows() {
|
|
475
|
+
return Promise.all(appPackages.map(async (app) => {
|
|
476
|
+
const [installed, latest] = await Promise.all([
|
|
477
|
+
Promise.resolve(installedVersion(app)),
|
|
478
|
+
latestVersion(app),
|
|
479
|
+
]);
|
|
480
|
+
return {
|
|
481
|
+
id: app.id,
|
|
482
|
+
name: app.name,
|
|
483
|
+
installed,
|
|
484
|
+
latest,
|
|
485
|
+
upgradable: !!installed && installed !== "?" && !!latest && isNewer(installed, latest),
|
|
486
|
+
installable: !installed && !!app.installCmd,
|
|
487
|
+
};
|
|
488
|
+
}));
|
|
489
|
+
}
|
|
490
|
+
export async function cmdApps() {
|
|
491
|
+
process.stderr.write(pc.dim("checking installed and latest versions ...\n"));
|
|
492
|
+
const rows = await collectAppRows();
|
|
493
|
+
console.log(table(rows.map((r) => [
|
|
494
|
+
r.id,
|
|
495
|
+
r.installed ?? pc.dim("not installed"),
|
|
496
|
+
r.latest ?? pc.dim("?"),
|
|
497
|
+
r.upgradable
|
|
498
|
+
? pc.yellow("upgrade available")
|
|
499
|
+
: r.installed
|
|
500
|
+
? pc.green("up to date")
|
|
501
|
+
: r.installable
|
|
502
|
+
? pc.dim("installable")
|
|
503
|
+
: pc.dim("-"),
|
|
504
|
+
]), ["APP", "INSTALLED", "LATEST", "STATUS"]));
|
|
505
|
+
const upgradable = rows.filter((r) => r.upgradable).map((r) => r.id);
|
|
506
|
+
if (upgradable.length)
|
|
507
|
+
console.log(pc.dim(`\nupgrade with: agentsw upgrade ${upgradable.join(" ")}`));
|
|
508
|
+
}
|
|
509
|
+
export async function cmdInstall(id) {
|
|
510
|
+
const app = appPackages.find((a) => a.id === id);
|
|
511
|
+
if (!app)
|
|
512
|
+
fail(`unknown app "${id}" (supported: ${appPackages.map((a) => a.id).join(", ")})`);
|
|
513
|
+
if (!app.installCmd)
|
|
514
|
+
fail(`${app.name} is not CLI-installable (desktop app manages itself)`);
|
|
515
|
+
const installed = installedVersion(app);
|
|
516
|
+
if (installed) {
|
|
517
|
+
console.log(`${app.name} already installed (${installed}); use \`agentsw upgrade ${id}\``);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
console.log(`installing ${app.name}: ${pc.dim(app.installCmd)}`);
|
|
521
|
+
runShell(app.installCmd);
|
|
522
|
+
console.log(pc.green(`${app.name} installed: ${installedVersion(app) ?? "version unknown"}`));
|
|
523
|
+
}
|
|
524
|
+
export async function cmdUpgrade(ids) {
|
|
525
|
+
let selected = appPackages.filter((a) => ids.length === 0 || ids.includes(a.id));
|
|
526
|
+
const unknown = ids.filter((id) => !appPackages.some((a) => a.id === id));
|
|
527
|
+
if (unknown.length)
|
|
528
|
+
fail(`unknown app(s): ${unknown.join(", ")}`);
|
|
529
|
+
if (ids.length === 0) {
|
|
530
|
+
// no args: upgrade everything that is installed and outdated
|
|
531
|
+
process.stderr.write(pc.dim("checking versions ...\n"));
|
|
532
|
+
const rows = await collectAppRows();
|
|
533
|
+
const upgradable = rows.filter((r) => r.upgradable).map((r) => r.id);
|
|
534
|
+
if (upgradable.length === 0) {
|
|
535
|
+
console.log("everything is up to date");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
selected = appPackages.filter((a) => upgradable.includes(a.id));
|
|
539
|
+
}
|
|
540
|
+
for (const app of selected) {
|
|
541
|
+
const cmd = app.upgradeCmd ?? app.installCmd;
|
|
542
|
+
if (!cmd) {
|
|
543
|
+
console.log(`${pc.yellow("skip")} ${app.id}: not CLI-upgradable`);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (!installedVersion(app)) {
|
|
547
|
+
console.log(`${pc.yellow("skip")} ${app.id}: not installed (use \`agentsw install ${app.id}\`)`);
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
console.log(`upgrading ${app.name}: ${pc.dim(cmd)}`);
|
|
551
|
+
try {
|
|
552
|
+
runShell(cmd);
|
|
553
|
+
console.log(pc.green(`${app.id} -> ${installedVersion(app) ?? "?"}`));
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
console.log(pc.red(`${app.id} upgrade failed: ${err.message}`));
|
|
557
|
+
process.exitCode = 1;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
/** Collect custom providers from app configs, dedupe by protocol+baseUrl, import what is new. */
|
|
562
|
+
export async function cmdImport(opts) {
|
|
563
|
+
const rows = scanCandidates();
|
|
564
|
+
const fresh = rows.filter((r) => !r.configured);
|
|
565
|
+
for (const r of rows) {
|
|
566
|
+
if (r.configured) {
|
|
567
|
+
console.log(`${pc.yellow(t("import.skip"))} ${pc.bold(r.id)} · ${r.protocol} · ${r.baseUrl} — ${t("import.already", { id: pc.bold(r.configured) })}`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
if (fresh.length === 0) {
|
|
571
|
+
console.log(rows.length ? t("import.noneNew") : t("import.noneFound"));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
console.log("");
|
|
575
|
+
console.log(table(fresh.map((r) => [
|
|
576
|
+
r.id,
|
|
577
|
+
r.protocol,
|
|
578
|
+
r.baseUrl,
|
|
579
|
+
String(r.models.length),
|
|
580
|
+
r.sources.join(","),
|
|
581
|
+
r.apiKey ? pc.green(t("import.keyYes")) : r.keyEnv ? pc.yellow(t("import.keyEnv", { name: r.keyEnv })) : pc.red(t("import.keyMissing")),
|
|
582
|
+
]), ["ID", t("table.protocol"), "BASE URL", t("table.models"), t("table.from"), t("table.key")]));
|
|
583
|
+
let chosen;
|
|
584
|
+
if (process.stdin.isTTY && !opts.all) {
|
|
585
|
+
const { pick } = await prompts({
|
|
586
|
+
type: "multiselect",
|
|
587
|
+
name: "pick",
|
|
588
|
+
message: t("import.which"),
|
|
589
|
+
instructions: t("import.multiInstructions"),
|
|
590
|
+
choices: fresh.map((r, i) => ({
|
|
591
|
+
title: `${r.id} · ${r.protocol} · ${r.baseUrl} · ${r.models.length ? t("import.modelsCount", { count: r.models.length }) : t("import.noModels")}`,
|
|
592
|
+
value: i,
|
|
593
|
+
selected: true,
|
|
594
|
+
})),
|
|
595
|
+
}, { onCancel: () => fail(t("add.cancelled")) });
|
|
596
|
+
if (!Array.isArray(pick) || pick.length === 0) {
|
|
597
|
+
console.log(pc.dim(t("import.nothingSelected")));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
chosen = pick.map((i) => fresh[i]).filter((r) => r !== undefined);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
chosen = fresh;
|
|
604
|
+
}
|
|
605
|
+
const store = loadStore();
|
|
606
|
+
const catalog = await loadCatalog();
|
|
607
|
+
const imported = [];
|
|
608
|
+
for (const c of chosen) {
|
|
609
|
+
let apiKey = c.apiKey;
|
|
610
|
+
if (!apiKey) {
|
|
611
|
+
const why = c.keyEnv ? t("import.keyRefMissing", { name: c.keyEnv }) : t("import.keyNotStored");
|
|
612
|
+
if (!process.stdin.isTTY || opts.all) {
|
|
613
|
+
fail(t("import.missingKey", { id: c.id, why }));
|
|
614
|
+
}
|
|
615
|
+
const a = await prompts({
|
|
616
|
+
type: "password",
|
|
617
|
+
name: "key",
|
|
618
|
+
message: t("import.keyPrompt", { id: c.id, url: c.baseUrl, why }),
|
|
619
|
+
validate: (v) => (v.trim() ? true : t("import.required")),
|
|
620
|
+
}, { onCancel: () => fail(t("add.cancelled")) });
|
|
621
|
+
apiKey = a.key;
|
|
622
|
+
}
|
|
623
|
+
let ids = [...c.models];
|
|
624
|
+
if (ids.length === 0) {
|
|
625
|
+
process.stderr.write(pc.dim(`${t("import.discovering", { id: c.id })}\n`));
|
|
626
|
+
try {
|
|
627
|
+
ids = await discoverProviderModels({ baseUrl: c.baseUrl, apiKey: apiKey, protocol: c.protocol });
|
|
628
|
+
}
|
|
629
|
+
catch (err) {
|
|
630
|
+
fail(t("import.discoveryFailed", { id: c.id, error: err.message }));
|
|
631
|
+
}
|
|
632
|
+
ids = applyModelFilter(ids, undefined, []).kept;
|
|
633
|
+
}
|
|
634
|
+
if (ids.length === 0)
|
|
635
|
+
fail(t("import.noModelsImport", { id: c.id }));
|
|
636
|
+
let id = c.id;
|
|
637
|
+
for (let n = 2; store.providers[id]; n++)
|
|
638
|
+
id = `${c.id}-${n}`;
|
|
639
|
+
const hint = guessProviderHint(catalog, c.baseUrl);
|
|
640
|
+
const models = enrichModels(catalog, ids, hint);
|
|
641
|
+
const defaultModel = c.defaultModel && ids.includes(c.defaultModel) ? c.defaultModel : ids[0];
|
|
642
|
+
const existed = store.providers[id] !== undefined;
|
|
643
|
+
store.providers[id] = {
|
|
644
|
+
id,
|
|
645
|
+
name: c.name || id,
|
|
646
|
+
protocol: c.protocol,
|
|
647
|
+
...(c.openaiApi ? { openaiApi: c.openaiApi } : {}),
|
|
648
|
+
baseUrl: normalizeUrl(c.baseUrl),
|
|
649
|
+
apiKey: apiKey,
|
|
650
|
+
models,
|
|
651
|
+
defaultModel,
|
|
652
|
+
modelsDevId: hint,
|
|
653
|
+
};
|
|
654
|
+
if (!store.active)
|
|
655
|
+
store.active = id;
|
|
656
|
+
imported.push(id);
|
|
657
|
+
console.log(`${pc.green(t(existed ? "import.updated" : "import.imported"))} ${pc.bold(id)} · ${c.protocol} · ${c.baseUrl} · ${t("import.modelsCount", { count: ids.length })} [${t("import.from")} ${c.sources.join(", ")}]`);
|
|
658
|
+
}
|
|
659
|
+
saveStore(store);
|
|
660
|
+
console.log(pc.dim(`\n${t("import.next", { id: imported[0] })}`));
|
|
661
|
+
}
|
package/dist/discover.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List model ids from the provider's own /v1/models endpoint.
|
|
3
|
+
* Resellers expose the id list here but no metadata (context, thinking, modalities) —
|
|
4
|
+
* metadata is filled from models.dev afterwards (enrichModels).
|
|
5
|
+
*/
|
|
6
|
+
export async function discoverProviderModels(opts) {
|
|
7
|
+
const base = opts.baseUrl.replace(/\/+$/, "");
|
|
8
|
+
const url = /\/v\d+$/.test(base) ? `${base}/models` : `${base}/v1/models`;
|
|
9
|
+
const headers = opts.protocol === "anthropic"
|
|
10
|
+
? { "x-api-key": opts.apiKey, "anthropic-version": "2023-06-01" }
|
|
11
|
+
: { authorization: `Bearer ${opts.apiKey}` };
|
|
12
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(20000) });
|
|
13
|
+
if (!res.ok)
|
|
14
|
+
throw new Error(`${url} -> HTTP ${res.status}`);
|
|
15
|
+
const body = (await res.json());
|
|
16
|
+
const rows = body.data ?? body.models ?? [];
|
|
17
|
+
const ids = rows.map((r) => r.id).filter((id) => typeof id === "string" && id.length > 0);
|
|
18
|
+
if (ids.length === 0)
|
|
19
|
+
throw new Error(`${url} returned no models`);
|
|
20
|
+
return [...new Set(ids)].sort();
|
|
21
|
+
}
|