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/commands.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
import { parseDocument } from "yaml";
|
|
5
|
+
import { envAssignments } from "./envfile.js";
|
|
1
6
|
import pc from "picocolors";
|
|
2
7
|
import prompts from "prompts";
|
|
3
8
|
import { loadStore, saveStore, getProvider, configFile } from "./store.js";
|
|
4
9
|
import { scanCandidates, normalizeUrl, findMatchingProvider } from "./import.js";
|
|
5
|
-
import {
|
|
10
|
+
import { loadCatalog, searchCatalog } from "./modelsdev.js";
|
|
11
|
+
import { enrichProviderModels, getMetadataMode, resolveMetadataOptions } from "./metadata.js";
|
|
12
|
+
import { loadGatewayCatalog } from "./gateway.js";
|
|
6
13
|
import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
|
|
7
14
|
import { discoverProviderModels, probeProtocols } from "./discover.js";
|
|
8
|
-
import { appCommand, appPackages, installedVersion, isNewer, latestVersion, runShell } from "./apps.js";
|
|
15
|
+
import { appCommand, appPackages, installedVersion, isNewer, latestVersion, normalizeAppVersion, runShell } from "./apps.js";
|
|
9
16
|
import { drainPendingWrites, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
10
17
|
import { applyModelFilter } from "./filter.js";
|
|
11
18
|
import { availableProviderId, providerIdFromBaseUrl, providerNameFromBaseUrl } from "./slug.js";
|
|
12
19
|
import { t } from "./i18n.js";
|
|
20
|
+
/** Share successes and failures without fetching until a model actually needs the supplement. */
|
|
21
|
+
function sharedGatewayLoader(refresh = false) {
|
|
22
|
+
let pending;
|
|
23
|
+
return () => pending ??= loadGatewayCatalog({ refresh }).then((catalog) => catalog ?? null);
|
|
24
|
+
}
|
|
13
25
|
function fail(message) {
|
|
14
26
|
process.stderr.write(pc.red(`error: ${message}\n`));
|
|
15
27
|
process.exit(1);
|
|
@@ -53,16 +65,16 @@ function modelRows(models) {
|
|
|
53
65
|
fmtTokens(m.contextWindow),
|
|
54
66
|
fmtTokens(m.maxInput),
|
|
55
67
|
fmtTokens(m.maxOutput),
|
|
56
|
-
m.reasoning ? (m.reasoningEfforts?.join("/")
|
|
68
|
+
m.reasoning === undefined ? "-" : m.reasoning ? (m.reasoningEfforts?.length ? m.reasoningEfforts.join("/") : "yes") : "no",
|
|
57
69
|
m.cost ? `$${m.cost.input ?? "?"}/$${m.cost.output ?? "?"}` : "-",
|
|
58
70
|
]);
|
|
59
71
|
}
|
|
60
72
|
const MODEL_HEADER = ["MODEL", "CTX", "IN", "OUT", "REASONING", "$IN/$OUT per M"];
|
|
61
|
-
/** Print a dim hint when some models have no
|
|
73
|
+
/** Print a dim hint when some models have no catalog metadata. */
|
|
62
74
|
function printUncatalogedHint(models) {
|
|
63
75
|
const bare = models.filter((m) => m.contextWindow === undefined && !m.cost);
|
|
64
76
|
if (bare.length > 0) {
|
|
65
|
-
console.log(pc.dim(`${bare.length} model(s) have no
|
|
77
|
+
console.log(pc.dim(`${bare.length} model(s) have no catalog metadata (shown as "-")`));
|
|
66
78
|
}
|
|
67
79
|
}
|
|
68
80
|
/** Guess the models.dev provider whose API host matches the configured baseUrl. */
|
|
@@ -89,14 +101,15 @@ function guessProviderHint(catalog, baseUrl) {
|
|
|
89
101
|
}
|
|
90
102
|
return undefined;
|
|
91
103
|
}
|
|
92
|
-
function parseFilterOpts(opts) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
function parseFilterOpts(opts, previous) {
|
|
105
|
+
if (opts.include === undefined && opts.exclude === undefined && opts.dedup === undefined)
|
|
106
|
+
return previous;
|
|
107
|
+
return {
|
|
108
|
+
...previous,
|
|
109
|
+
...(opts.include !== undefined ? { include: opts.include.split(",").map((x) => x.trim()).filter(Boolean) } : {}),
|
|
110
|
+
...(opts.exclude !== undefined ? { exclude: opts.exclude.split(",").map((x) => x.trim()).filter(Boolean) } : {}),
|
|
111
|
+
...(opts.dedup !== undefined ? { dedup: opts.dedup } : {}),
|
|
112
|
+
};
|
|
100
113
|
}
|
|
101
114
|
function reportDropped(dropped) {
|
|
102
115
|
if (!dropped.length)
|
|
@@ -107,6 +120,7 @@ function reportDropped(dropped) {
|
|
|
107
120
|
}
|
|
108
121
|
export async function cmdAdd(opts) {
|
|
109
122
|
const store = loadStore();
|
|
123
|
+
resolveMetadataOptions(opts);
|
|
110
124
|
const interactive = process.stdin.isTTY && !opts.yes;
|
|
111
125
|
let answers = {};
|
|
112
126
|
if (interactive) {
|
|
@@ -179,13 +193,17 @@ export async function cmdAdd(opts) {
|
|
|
179
193
|
if (protocol !== "openai" && protocol !== "anthropic")
|
|
180
194
|
fail(t("add.protocolInvalid"));
|
|
181
195
|
const wire = answers.openaiApi ?? opts.openaiApi;
|
|
182
|
-
if (wire && wire !== "completions" && wire !== "responses")
|
|
196
|
+
if (wire !== undefined && wire !== "completions" && wire !== "responses")
|
|
183
197
|
fail(t("add.openaiApiInvalid"));
|
|
184
|
-
const openaiApi = protocol === "openai" ? wire : undefined;
|
|
185
198
|
const baseUrl = normalizeUrl(answers.baseUrl);
|
|
199
|
+
const explicitId = answers.id || undefined;
|
|
200
|
+
const existing = explicitId ? store.providers[explicitId] : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey: answers.apiKey });
|
|
201
|
+
const id = explicitId ?? existing?.id ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
202
|
+
const openaiApi = protocol === "openai" ? wire ?? existing?.openaiApi : undefined;
|
|
203
|
+
const metadataOptions = resolveMetadataOptions(opts, existing);
|
|
186
204
|
const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
187
205
|
let modelIds = manualIds;
|
|
188
|
-
const modelFilter = parseFilterOpts(opts);
|
|
206
|
+
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
189
207
|
const shouldDiscover = opts.discover || manualIds.length === 0;
|
|
190
208
|
if (shouldDiscover) {
|
|
191
209
|
if (!opts.discover) {
|
|
@@ -196,36 +214,37 @@ export async function cmdAdd(opts) {
|
|
|
196
214
|
console.log(t("add.providerLists", { count: discovered.length }));
|
|
197
215
|
modelIds = [...new Set([...manualIds, ...discovered])];
|
|
198
216
|
}
|
|
199
|
-
const
|
|
217
|
+
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
218
|
+
modelIds = [...new Set([...modelIds, ...retained])];
|
|
219
|
+
const pinned = [...manualIds, ...retained, ...(opts.defaultModel ? [opts.defaultModel] : []), ...(opts.smallModel ? [opts.smallModel] : [])];
|
|
200
220
|
const outcome = applyModelFilter(modelIds, modelFilter, pinned);
|
|
201
221
|
reportDropped(outcome.dropped);
|
|
202
222
|
modelIds = outcome.kept;
|
|
203
223
|
if (modelIds.length === 0)
|
|
204
224
|
fail(t("add.atLeastOne"));
|
|
205
225
|
const catalog = await loadCatalog();
|
|
206
|
-
const hint = opts.modelsDev ?? guessProviderHint(catalog, answers.baseUrl);
|
|
207
|
-
const models =
|
|
226
|
+
const hint = opts.modelsDev ?? existing?.modelsDevId ?? guessProviderHint(catalog, answers.baseUrl);
|
|
227
|
+
const models = await enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: existing?.models });
|
|
208
228
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
209
|
-
const defaultModel = opts.defaultModel ?? modelIds[0];
|
|
229
|
+
const defaultModel = opts.defaultModel ?? existing?.defaultModel ?? modelIds[0];
|
|
210
230
|
if (!modelIds.includes(defaultModel))
|
|
211
231
|
fail(t("add.defaultMissing", { model: defaultModel }));
|
|
212
232
|
if (opts.smallModel && !modelIds.includes(opts.smallModel))
|
|
213
233
|
fail(t("add.smallMissing", { model: opts.smallModel }));
|
|
214
|
-
const explicitId = answers.id || undefined;
|
|
215
|
-
const existing = explicitId ? undefined : findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey: answers.apiKey });
|
|
216
|
-
const id = explicitId ?? existing?.id ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
217
234
|
const provider = {
|
|
235
|
+
...existing,
|
|
218
236
|
id,
|
|
219
237
|
name: answers.name || existing?.name || (explicitId ? explicitId : providerNameFromBaseUrl(baseUrl, protocol)),
|
|
220
238
|
protocol,
|
|
221
|
-
|
|
239
|
+
openaiApi,
|
|
222
240
|
baseUrl,
|
|
223
241
|
apiKey: answers.apiKey,
|
|
224
242
|
models,
|
|
225
243
|
defaultModel,
|
|
226
|
-
smallModel: opts.smallModel,
|
|
227
|
-
reasoningEffort: opts.reasoningEffort,
|
|
244
|
+
smallModel: opts.smallModel ?? existing?.smallModel,
|
|
245
|
+
reasoningEffort: opts.reasoningEffort ?? existing?.reasoningEffort,
|
|
228
246
|
modelsDevId: hint,
|
|
247
|
+
...metadataOptions,
|
|
229
248
|
modelFilter,
|
|
230
249
|
};
|
|
231
250
|
const existed = store.providers[provider.id] !== undefined;
|
|
@@ -243,25 +262,31 @@ export async function cmdAdd(opts) {
|
|
|
243
262
|
/** Shared logic to create and save a single provider from discovered models. */
|
|
244
263
|
async function createProvider(opts) {
|
|
245
264
|
const { store, id, name, protocol, openaiApi, baseUrl, apiKey, modelIds, modelFilter } = opts;
|
|
265
|
+
const metadataOptions = resolveMetadataOptions(opts, opts.existing);
|
|
246
266
|
const catalog = await loadCatalog();
|
|
247
|
-
const hint = guessProviderHint(catalog, baseUrl);
|
|
248
|
-
const models =
|
|
267
|
+
const hint = opts.modelsDev ?? opts.existing?.modelsDevId ?? guessProviderHint(catalog, baseUrl);
|
|
268
|
+
const models = await enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: opts.existing?.models }, { gatewayLoader: opts.gatewayLoader });
|
|
249
269
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
250
|
-
const defaultModel = opts.defaultModel ?? modelIds[0];
|
|
270
|
+
const defaultModel = opts.defaultModel ?? opts.existing?.defaultModel ?? modelIds[0];
|
|
251
271
|
if (!modelIds.includes(defaultModel))
|
|
252
272
|
fail(t("add.defaultMissing", { model: defaultModel }));
|
|
273
|
+
const smallModel = opts.smallModel ?? opts.existing?.smallModel;
|
|
274
|
+
if (smallModel && !modelIds.includes(smallModel))
|
|
275
|
+
fail(t("add.smallMissing", { model: smallModel }));
|
|
253
276
|
const provider = {
|
|
277
|
+
...opts.existing,
|
|
254
278
|
id,
|
|
255
279
|
name,
|
|
256
280
|
protocol,
|
|
257
|
-
|
|
281
|
+
openaiApi: protocol === "openai" ? openaiApi ?? opts.existing?.openaiApi : undefined,
|
|
258
282
|
baseUrl,
|
|
259
283
|
apiKey,
|
|
260
284
|
models,
|
|
261
285
|
defaultModel,
|
|
262
|
-
smallModel
|
|
263
|
-
reasoningEffort: opts.reasoningEffort,
|
|
286
|
+
smallModel,
|
|
287
|
+
reasoningEffort: opts.reasoningEffort ?? opts.existing?.reasoningEffort,
|
|
264
288
|
modelsDevId: hint,
|
|
289
|
+
...metadataOptions,
|
|
265
290
|
modelFilter,
|
|
266
291
|
};
|
|
267
292
|
const existed = store.providers[provider.id] !== undefined;
|
|
@@ -282,6 +307,7 @@ async function createProvider(opts) {
|
|
|
282
307
|
*/
|
|
283
308
|
export async function cmdQuickAdd(opts) {
|
|
284
309
|
const store = loadStore();
|
|
310
|
+
resolveMetadataOptions(opts);
|
|
285
311
|
const interactive = process.stdin.isTTY && !opts.yes;
|
|
286
312
|
let baseUrl;
|
|
287
313
|
let apiKey;
|
|
@@ -306,19 +332,23 @@ export async function cmdQuickAdd(opts) {
|
|
|
306
332
|
if (protocols.length === 0) {
|
|
307
333
|
fail(t("quick.noProtocol"));
|
|
308
334
|
}
|
|
309
|
-
|
|
335
|
+
if (opts.openaiApi !== undefined && opts.openaiApi !== "completions" && opts.openaiApi !== "responses")
|
|
336
|
+
fail(t("add.openaiApiInvalid"));
|
|
310
337
|
const multi = protocols.length > 1;
|
|
311
338
|
const createdIds = [];
|
|
339
|
+
const gatewayLoader = sharedGatewayLoader();
|
|
312
340
|
for (const protocol of protocols) {
|
|
313
341
|
const explicitId = opts.id === undefined ? undefined : `${opts.id}${multi ? `-${protocol}` : ""}`;
|
|
314
|
-
const existing = explicitId === undefined ? findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey }) :
|
|
342
|
+
const existing = explicitId === undefined ? findMatchingProvider(Object.values(store.providers), { protocol, baseUrl, apiKey }) : store.providers[explicitId];
|
|
315
343
|
const id = explicitId ?? existing?.id ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
316
|
-
const name = existing?.name ?? (opts.id === undefined ? providerNameFromBaseUrl(baseUrl, protocol) : multi ? `${opts.id} (${protocol})` : opts.id);
|
|
344
|
+
const name = opts.name ?? existing?.name ?? (opts.id === undefined ? providerNameFromBaseUrl(baseUrl, protocol) : multi ? `${opts.id} (${protocol})` : opts.id);
|
|
345
|
+
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
317
346
|
process.stderr.write(pc.dim(`${t("add.discovering", { url: baseUrl })} [${protocol}]\n`));
|
|
318
347
|
const discovered = await discoverProviderModels({ baseUrl, apiKey, protocol });
|
|
319
348
|
console.log(t("add.providerLists", { count: discovered.length }));
|
|
320
|
-
|
|
321
|
-
|
|
349
|
+
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
350
|
+
let modelIds = [...new Set([...discovered, ...retained])];
|
|
351
|
+
const pinned = [...retained, ...(opts.defaultModel ? [opts.defaultModel] : []), ...(opts.smallModel ? [opts.smallModel] : [])];
|
|
322
352
|
const outcome = applyModelFilter(modelIds, modelFilter, pinned);
|
|
323
353
|
reportDropped(outcome.dropped);
|
|
324
354
|
modelIds = outcome.kept;
|
|
@@ -338,6 +368,13 @@ export async function cmdQuickAdd(opts) {
|
|
|
338
368
|
smallModel: opts.smallModel,
|
|
339
369
|
reasoningEffort: opts.reasoningEffort,
|
|
340
370
|
modelFilter,
|
|
371
|
+
existing,
|
|
372
|
+
openaiApi: opts.openaiApi,
|
|
373
|
+
modelsDev: opts.modelsDev,
|
|
374
|
+
gatewayMetadata: opts.gatewayMetadata,
|
|
375
|
+
gatewayModels: opts.gatewayModels,
|
|
376
|
+
metadataMode: opts.metadataMode,
|
|
377
|
+
gatewayLoader,
|
|
341
378
|
});
|
|
342
379
|
createdIds.push(provider.id);
|
|
343
380
|
console.log(pc.dim(`\n${t("add.next", { id: provider.id })}`));
|
|
@@ -384,7 +421,7 @@ function reportResults(results) {
|
|
|
384
421
|
console.log(` ${" ".repeat(9)} ${pc.dim(note)}`);
|
|
385
422
|
}
|
|
386
423
|
}
|
|
387
|
-
async function runTargets(op, provider, appsFilter) {
|
|
424
|
+
async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
388
425
|
const selected = resolveTargets(appsFilter);
|
|
389
426
|
const explicit = appsFilter !== undefined && appsFilter !== "all";
|
|
390
427
|
const results = [];
|
|
@@ -398,48 +435,131 @@ async function runTargets(op, provider, appsFilter) {
|
|
|
398
435
|
});
|
|
399
436
|
continue;
|
|
400
437
|
}
|
|
401
|
-
if (!target.detect() && !explicit) {
|
|
402
|
-
results.push({ app: target.id, changed: [], notes: [], skipped: `${target.name} not detected (pass --apps ${target.id} to force)` });
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
438
|
try {
|
|
439
|
+
if (!explicit && !target.detect()) {
|
|
440
|
+
results.push({ app: target.id, changed: [], notes: [], skipped: `${target.name} not detected (pass --apps ${target.id} to force)` });
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
406
443
|
results.push(await target[op](provider));
|
|
407
444
|
}
|
|
408
445
|
catch (err) {
|
|
409
|
-
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${err.message}`) });
|
|
446
|
+
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(redactErrors ? "failed: configuration could not be previewed safely" : `failed: ${err.message}`) });
|
|
410
447
|
process.exitCode = 1;
|
|
411
448
|
}
|
|
412
449
|
}
|
|
413
450
|
return results;
|
|
414
451
|
}
|
|
415
|
-
/**
|
|
416
|
-
function
|
|
452
|
+
/** Parse first: an unknown format or malformed document must never fall back to raw secrets. */
|
|
453
|
+
function previewDocument(file, text) {
|
|
454
|
+
if (!text.trim())
|
|
455
|
+
return undefined;
|
|
456
|
+
if (path.basename(file) === ".env" || file.endsWith(".env")) {
|
|
457
|
+
return Object.fromEntries(envAssignments(file, text).map(({ name }) => [name, "[REDACTED]"]));
|
|
458
|
+
}
|
|
459
|
+
switch (path.extname(file).toLowerCase()) {
|
|
460
|
+
case ".json":
|
|
461
|
+
case ".jsonc": {
|
|
462
|
+
const errors = [];
|
|
463
|
+
const value = parseJsonc(text, errors, { allowTrailingComma: true });
|
|
464
|
+
if (errors.length)
|
|
465
|
+
throw new Error("invalid JSON configuration");
|
|
466
|
+
return value;
|
|
467
|
+
}
|
|
468
|
+
case ".toml": return parseToml(text);
|
|
469
|
+
case ".yaml":
|
|
470
|
+
case ".yml": {
|
|
471
|
+
const document = parseDocument(text);
|
|
472
|
+
if (document.errors.length)
|
|
473
|
+
throw new Error("invalid YAML configuration");
|
|
474
|
+
return document.toJS();
|
|
475
|
+
}
|
|
476
|
+
default: throw new Error("unsupported configuration format");
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/** Normalized semantic diff: comments and unparseable raw input never enter output. */
|
|
480
|
+
function printFileDiff(file, next, apiKey) {
|
|
417
481
|
const before = readTextIfExists(file) ?? "";
|
|
418
482
|
if (before === next) {
|
|
419
483
|
console.log(`${pc.dim("unchanged")} ${file}`);
|
|
420
484
|
return;
|
|
421
485
|
}
|
|
422
|
-
console.log(pc.bold(`--- ${file}`));
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
486
|
+
console.log(pc.bold(`--- ${file} (redacted configuration)`));
|
|
487
|
+
let oldText;
|
|
488
|
+
let newText;
|
|
489
|
+
try {
|
|
490
|
+
const oldDocument = previewDocument(file, before);
|
|
491
|
+
const newDocument = previewDocument(file, next);
|
|
492
|
+
const sensitive = /key|token|secret|password|auth|credential|cookie|headers/i;
|
|
493
|
+
const credentialFile = path.basename(file) === ".credentials.yaml";
|
|
494
|
+
const secrets = new Set(apiKey ? [apiKey] : []);
|
|
495
|
+
const visited = new WeakSet();
|
|
496
|
+
const collect = (value, hidden = false) => {
|
|
497
|
+
if (typeof value === "string" && hidden && value)
|
|
498
|
+
secrets.add(value);
|
|
499
|
+
if (!value || typeof value !== "object" || visited.has(value))
|
|
500
|
+
return;
|
|
501
|
+
visited.add(value);
|
|
502
|
+
for (const [key, child] of Object.entries(value))
|
|
503
|
+
collect(child, hidden || sensitive.test(key));
|
|
504
|
+
};
|
|
505
|
+
collect(oldDocument, credentialFile);
|
|
506
|
+
collect(newDocument, credentialFile);
|
|
507
|
+
const orderedSecrets = [...secrets].sort((a, b) => b.length - a.length);
|
|
508
|
+
const redact = (key, value) => {
|
|
509
|
+
if (sensitive.test(key))
|
|
510
|
+
return "[REDACTED]";
|
|
511
|
+
if ((key === "" || credentialFile) && typeof value === "string")
|
|
512
|
+
return "[REDACTED]";
|
|
513
|
+
if (typeof value !== "string")
|
|
514
|
+
return value;
|
|
515
|
+
let safe = value;
|
|
516
|
+
if (/^https?:\/\//i.test(safe)) {
|
|
517
|
+
const url = new URL(safe);
|
|
518
|
+
if (url.username)
|
|
519
|
+
url.username = "[REDACTED]";
|
|
520
|
+
if (url.password)
|
|
521
|
+
url.password = "[REDACTED]";
|
|
522
|
+
for (const name of new Set(url.searchParams.keys()))
|
|
523
|
+
if (sensitive.test(name))
|
|
524
|
+
url.searchParams.set(name, "[REDACTED]");
|
|
525
|
+
url.hash = "";
|
|
526
|
+
safe = url.toString();
|
|
527
|
+
}
|
|
528
|
+
for (const secret of orderedSecrets)
|
|
529
|
+
safe = safe.split(secret).join("[REDACTED]");
|
|
530
|
+
return safe.replace(/\b(?:Bearer|Basic)\s+[^\s"',;]+/gi, "[REDACTED]");
|
|
531
|
+
};
|
|
532
|
+
oldText = JSON.stringify(oldDocument, redact, 2) ?? "";
|
|
533
|
+
newText = JSON.stringify(newDocument, redact, 2) ?? "";
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
console.log(pc.dim("[content withheld: unsupported or malformed configuration]"));
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const oldLines = oldText.split("\n");
|
|
540
|
+
const newLines = newText.split("\n");
|
|
541
|
+
const oldSeen = new Map();
|
|
542
|
+
for (const line of oldLines)
|
|
543
|
+
oldSeen.set(line, (oldSeen.get(line) ?? 0) + 1);
|
|
544
|
+
const newSeen = new Map();
|
|
545
|
+
for (const line of newLines)
|
|
546
|
+
newSeen.set(line, (newSeen.get(line) ?? 0) + 1);
|
|
547
|
+
for (const line of oldLines) {
|
|
548
|
+
const count = newSeen.get(line) ?? 0;
|
|
549
|
+
if (count > 0)
|
|
550
|
+
newSeen.set(line, count - 1);
|
|
434
551
|
else
|
|
435
|
-
console.log(pc.red(`- ${
|
|
552
|
+
console.log(pc.red(`- ${line}`));
|
|
436
553
|
}
|
|
437
|
-
for (const
|
|
438
|
-
|
|
439
|
-
|
|
554
|
+
for (const line of newLines) {
|
|
555
|
+
const count = oldSeen.get(line) ?? 0;
|
|
556
|
+
if (count > 0)
|
|
557
|
+
oldSeen.set(line, count - 1);
|
|
440
558
|
else
|
|
441
|
-
console.log(pc.green(`+ ${
|
|
559
|
+
console.log(pc.green(`+ ${line}`));
|
|
442
560
|
}
|
|
561
|
+
if (oldText === newText)
|
|
562
|
+
console.log(pc.dim("[only redacted values or formatting changed]"));
|
|
443
563
|
}
|
|
444
564
|
async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
445
565
|
if (!dryRun) {
|
|
@@ -448,7 +568,7 @@ async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
|
448
568
|
}
|
|
449
569
|
setDryRun(true);
|
|
450
570
|
try {
|
|
451
|
-
const results = await runTargets(op, provider, apps);
|
|
571
|
+
const results = await runTargets(op, provider, apps, true);
|
|
452
572
|
const writes = drainPendingWrites();
|
|
453
573
|
for (const r of results) {
|
|
454
574
|
if (r.skipped)
|
|
@@ -456,13 +576,14 @@ async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
|
456
576
|
}
|
|
457
577
|
console.log(pc.bold(`\ndry run — ${writes.length} file(s) would be written:\n`));
|
|
458
578
|
for (const w of writes)
|
|
459
|
-
printFileDiff(w.file, w.content);
|
|
579
|
+
printFileDiff(w.file, w.content, provider.apiKey);
|
|
460
580
|
}
|
|
461
581
|
finally {
|
|
462
582
|
setDryRun(false);
|
|
463
583
|
}
|
|
464
584
|
}
|
|
465
585
|
export async function cmdUse(id, opts) {
|
|
586
|
+
resolveTargets(opts.apps);
|
|
466
587
|
const store = loadStore();
|
|
467
588
|
const provider = getProvider(store, id);
|
|
468
589
|
if (opts.model) {
|
|
@@ -479,6 +600,7 @@ export async function cmdUse(id, opts) {
|
|
|
479
600
|
await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
480
601
|
}
|
|
481
602
|
export async function cmdSync(opts) {
|
|
603
|
+
resolveTargets(opts.apps);
|
|
482
604
|
const store = loadStore();
|
|
483
605
|
const id = opts.provider ?? store.active;
|
|
484
606
|
if (!id)
|
|
@@ -501,9 +623,21 @@ export function cmdStatus() {
|
|
|
501
623
|
console.log(table(rows, ["APP", t("table.found"), t("table.protocols"), t("table.current"), t("table.config")]));
|
|
502
624
|
}
|
|
503
625
|
export async function cmdModels(query, opts) {
|
|
626
|
+
if (opts.metadata && !opts.provider)
|
|
627
|
+
fail("--metadata requires --provider");
|
|
504
628
|
if (opts.provider) {
|
|
505
629
|
const store = loadStore();
|
|
506
630
|
const provider = getProvider(store, opts.provider);
|
|
631
|
+
if (opts.metadata) {
|
|
632
|
+
console.log(JSON.stringify({
|
|
633
|
+
provider: provider.id,
|
|
634
|
+
gatewayMetadata: provider.gatewayMetadata ?? "auto",
|
|
635
|
+
metadataMode: getMetadataMode(provider),
|
|
636
|
+
gatewayModelAliases: provider.gatewayModelAliases ?? {},
|
|
637
|
+
models: provider.models.map(({ id, metadata }) => ({ id, metadata })),
|
|
638
|
+
}, null, 2));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
507
641
|
console.log(`${pc.bold(provider.id)} (${provider.protocol}) · ${provider.baseUrl}`);
|
|
508
642
|
console.log(table(modelRows(provider.models), MODEL_HEADER));
|
|
509
643
|
printUncatalogedHint(provider.models);
|
|
@@ -526,45 +660,54 @@ export async function cmdModels(query, opts) {
|
|
|
526
660
|
});
|
|
527
661
|
console.log(table(rows, ["PROVIDER", ...MODEL_HEADER]));
|
|
528
662
|
}
|
|
529
|
-
export async function cmdRefreshMeta() {
|
|
663
|
+
export async function cmdRefreshMeta(opts = {}) {
|
|
530
664
|
const store = loadStore();
|
|
665
|
+
const providers = opts.provider ? [getProvider(store, opts.provider)] : Object.values(store.providers);
|
|
666
|
+
resolveMetadataOptions(opts);
|
|
667
|
+
for (const provider of providers)
|
|
668
|
+
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
531
669
|
const catalog = await loadCatalog({ refresh: true });
|
|
532
|
-
|
|
533
|
-
fail("models.dev catalog unavailable");
|
|
670
|
+
const gatewayLoader = sharedGatewayLoader(true);
|
|
534
671
|
let updated = 0;
|
|
535
|
-
for (const provider of
|
|
672
|
+
for (const provider of providers) {
|
|
536
673
|
const before = JSON.stringify(provider.models);
|
|
537
|
-
provider.models =
|
|
674
|
+
provider.models = await enrichProviderModels(catalog, provider.models.map((m) => m.id), {
|
|
675
|
+
...provider,
|
|
676
|
+
modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl),
|
|
677
|
+
}, { gatewayLoader });
|
|
538
678
|
if (JSON.stringify(provider.models) !== before)
|
|
539
679
|
updated++;
|
|
540
680
|
}
|
|
541
681
|
saveStore(store);
|
|
542
|
-
console.log(pc.green(`
|
|
682
|
+
console.log(pc.green(`checked model metadata (${updated} provider(s) changed)`));
|
|
543
683
|
if (updated > 0)
|
|
544
684
|
console.log(pc.dim("run `agentsw sync` to push updated metadata into app configs"));
|
|
545
685
|
}
|
|
546
686
|
export async function cmdDiscover(id, opts) {
|
|
687
|
+
resolveTargets(opts.apps);
|
|
547
688
|
const store = loadStore();
|
|
548
689
|
const provider = getProvider(store, id);
|
|
690
|
+
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
549
691
|
// flags override and re-persist the filter; --no-filter clears it
|
|
550
|
-
const flagFilter = parseFilterOpts(opts);
|
|
692
|
+
const flagFilter = parseFilterOpts(opts, provider.modelFilter);
|
|
551
693
|
if (opts.filter === false)
|
|
552
694
|
provider.modelFilter = undefined;
|
|
553
695
|
else if (flagFilter)
|
|
554
696
|
provider.modelFilter = flagFilter;
|
|
555
697
|
process.stderr.write(pc.dim(`discovering models from ${provider.baseUrl} ...\n`));
|
|
556
698
|
const listed = await discoverProviderModels(provider);
|
|
557
|
-
const
|
|
699
|
+
const pinned = [provider.defaultModel, provider.smallModel].filter((model) => !!model);
|
|
700
|
+
const outcome = applyModelFilter(listed, provider.modelFilter, pinned);
|
|
558
701
|
reportDropped(outcome.dropped);
|
|
559
702
|
const ids = outcome.kept;
|
|
560
703
|
const known = provider.models.map((m) => m.id);
|
|
561
704
|
const added = ids.filter((m) => !known.includes(m));
|
|
562
705
|
const gone = known.filter((m) => !ids.includes(m));
|
|
563
706
|
const catalog = await loadCatalog();
|
|
564
|
-
|
|
707
|
+
const retainedIds = [...new Set([...ids, ...pinned])];
|
|
708
|
+
provider.models = await enrichProviderModels(catalog, retainedIds, { ...provider, modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl) });
|
|
565
709
|
if (!ids.includes(provider.defaultModel)) {
|
|
566
710
|
console.log(pc.yellow(`default model ${provider.defaultModel} no longer listed; keeping it anyway`));
|
|
567
|
-
provider.models.push({ id: provider.defaultModel });
|
|
568
711
|
}
|
|
569
712
|
saveStore(store);
|
|
570
713
|
console.log(`${pc.bold(id)}: ${ids.length} models (${pc.green(`+${added.length}`)} / ${pc.red(`-${gone.length}`)})` +
|
|
@@ -582,18 +725,29 @@ export async function cmdDiscover(id, opts) {
|
|
|
582
725
|
}
|
|
583
726
|
async function collectAppRows() {
|
|
584
727
|
return Promise.all(appPackages.map(async (app) => {
|
|
585
|
-
const [
|
|
586
|
-
Promise.resolve(installedVersion(app)),
|
|
728
|
+
const [installedResult, latestResult] = await Promise.allSettled([
|
|
729
|
+
Promise.resolve().then(() => installedVersion(app)),
|
|
587
730
|
latestVersion(app),
|
|
588
731
|
]);
|
|
589
|
-
const
|
|
732
|
+
const installed = installedResult.status === "fulfilled" ? installedResult.value : "?";
|
|
733
|
+
const latest = latestResult.status === "fulfilled" ? latestResult.value : undefined;
|
|
734
|
+
const knownInstalled = normalizeAppVersion(installed);
|
|
735
|
+
const knownLatest = normalizeAppVersion(latest);
|
|
736
|
+
const checkFailed = installedResult.status === "rejected"
|
|
737
|
+
? "installed version check failed"
|
|
738
|
+
: installed && !knownInstalled
|
|
739
|
+
? "installed version unknown"
|
|
740
|
+
: installed && !knownLatest
|
|
741
|
+
? "latest version unavailable"
|
|
742
|
+
: undefined;
|
|
590
743
|
return {
|
|
591
744
|
id: app.id,
|
|
592
745
|
name: app.name,
|
|
593
746
|
installed,
|
|
594
747
|
latest,
|
|
595
|
-
upgradable: knownInstalled && !!
|
|
748
|
+
upgradable: !!knownInstalled && !!knownLatest && isNewer(knownInstalled, knownLatest),
|
|
596
749
|
installable: !installed && !!appCommand(app, "install"),
|
|
750
|
+
checkFailed,
|
|
597
751
|
};
|
|
598
752
|
}));
|
|
599
753
|
}
|
|
@@ -610,7 +764,7 @@ export async function cmdApps() {
|
|
|
610
764
|
? r.installable
|
|
611
765
|
? pc.dim("installable")
|
|
612
766
|
: pc.dim("-")
|
|
613
|
-
: r.
|
|
767
|
+
: r.checkFailed || !normalizeAppVersion(r.latest)
|
|
614
768
|
? pc.dim("unknown")
|
|
615
769
|
: pc.green("up to date"),
|
|
616
770
|
]), ["APP", "INSTALLED", "LATEST", "STATUS"]));
|
|
@@ -632,38 +786,69 @@ export async function cmdInstall(id) {
|
|
|
632
786
|
}
|
|
633
787
|
console.log(`installing ${app.name}: ${pc.dim(installCmd)}`);
|
|
634
788
|
runShell(installCmd);
|
|
635
|
-
|
|
789
|
+
const detected = installedVersion(app);
|
|
790
|
+
if (!detected)
|
|
791
|
+
throw new Error(`${app.name}: installer completed but the app is still not detected; check the installation and PATH`);
|
|
792
|
+
const version = normalizeAppVersion(detected);
|
|
793
|
+
if (!version) {
|
|
794
|
+
console.log(pc.yellow(`${app.name}: installer completed; app detected but version unknown`));
|
|
795
|
+
process.exitCode = 1;
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
console.log(pc.green(`${app.name} installed: ${version}`));
|
|
636
799
|
}
|
|
637
800
|
export async function cmdUpgrade(ids) {
|
|
638
801
|
let selected = appPackages.filter((a) => ids.length === 0 || ids.includes(a.id));
|
|
639
802
|
const unknown = ids.filter((id) => !appPackages.some((a) => a.id === id));
|
|
640
803
|
if (unknown.length)
|
|
641
804
|
fail(`unknown app(s): ${unknown.join(", ")}`);
|
|
805
|
+
const expectedVersions = new Map();
|
|
642
806
|
if (ids.length === 0) {
|
|
643
807
|
// no args: upgrade everything that is installed and outdated
|
|
644
808
|
process.stderr.write(pc.dim("checking versions ...\n"));
|
|
645
809
|
const rows = await collectAppRows();
|
|
646
|
-
const
|
|
810
|
+
const managed = rows.filter((row) => !!appCommand(appPackages.find((app) => app.id === row.id), "upgrade"));
|
|
811
|
+
const failed = managed.filter((row) => row.checkFailed);
|
|
812
|
+
for (const row of failed)
|
|
813
|
+
console.log(pc.yellow(`${row.id}: ${row.checkFailed}; update status unknown`));
|
|
814
|
+
if (failed.length)
|
|
815
|
+
process.exitCode = 1;
|
|
816
|
+
const upgradable = managed.filter((row) => row.upgradable);
|
|
647
817
|
if (upgradable.length === 0) {
|
|
648
|
-
|
|
818
|
+
if (failed.length)
|
|
819
|
+
console.log("could not determine update status for every installed app");
|
|
820
|
+
else if (managed.some((row) => row.installed))
|
|
821
|
+
console.log("all checked apps are up to date");
|
|
822
|
+
else
|
|
823
|
+
console.log("no installed CLI-managed apps to upgrade");
|
|
649
824
|
return;
|
|
650
825
|
}
|
|
651
|
-
|
|
826
|
+
for (const row of upgradable)
|
|
827
|
+
expectedVersions.set(row.id, row.latest);
|
|
828
|
+
selected = appPackages.filter((app) => expectedVersions.has(app.id));
|
|
652
829
|
}
|
|
653
830
|
for (const app of selected) {
|
|
654
831
|
const cmd = appCommand(app, "upgrade");
|
|
655
832
|
if (!cmd) {
|
|
656
833
|
console.log(`${pc.yellow("skip")} ${app.id}: not CLI-upgradable`);
|
|
834
|
+
process.exitCode = 1;
|
|
657
835
|
continue;
|
|
658
836
|
}
|
|
659
|
-
if (!installedVersion(app)) {
|
|
660
|
-
console.log(`${pc.yellow("skip")} ${app.id}: not installed (use \`agentsw install ${app.id}\`)`);
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
console.log(`upgrading ${app.name}: ${pc.dim(cmd)}`);
|
|
664
837
|
try {
|
|
838
|
+
if (!installedVersion(app)) {
|
|
839
|
+
console.log(`${pc.yellow("skip")} ${app.id}: not installed (use \`agentsw install ${app.id}\`)`);
|
|
840
|
+
process.exitCode = 1;
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
console.log(`upgrading ${app.name}: ${pc.dim(cmd)}`);
|
|
665
844
|
runShell(cmd);
|
|
666
|
-
|
|
845
|
+
const version = normalizeAppVersion(installedVersion(app));
|
|
846
|
+
if (!version)
|
|
847
|
+
throw new Error("upgrade command completed but installed version is unknown or app is not detected");
|
|
848
|
+
const expected = expectedVersions.get(app.id);
|
|
849
|
+
if (expected && isNewer(version, expected))
|
|
850
|
+
throw new Error(`upgrade command completed but ${version} is older than available ${expected}`);
|
|
851
|
+
console.log(pc.green(`${app.id} -> ${version}`));
|
|
667
852
|
}
|
|
668
853
|
catch (err) {
|
|
669
854
|
console.log(pc.red(`${app.id} upgrade failed: ${err.message}`));
|
|
@@ -673,6 +858,7 @@ export async function cmdUpgrade(ids) {
|
|
|
673
858
|
}
|
|
674
859
|
/** Collect custom providers, dedupe by endpoint and credentials, and import what is new. */
|
|
675
860
|
export async function cmdImport(opts) {
|
|
861
|
+
const metadataOptions = resolveMetadataOptions(opts);
|
|
676
862
|
const rows = scanCandidates();
|
|
677
863
|
const fresh = rows.filter((r) => !r.configured);
|
|
678
864
|
for (const r of rows) {
|
|
@@ -717,6 +903,7 @@ export async function cmdImport(opts) {
|
|
|
717
903
|
}
|
|
718
904
|
const store = loadStore();
|
|
719
905
|
const catalog = await loadCatalog();
|
|
906
|
+
const gatewayLoader = sharedGatewayLoader();
|
|
720
907
|
const imported = [];
|
|
721
908
|
for (const c of chosen) {
|
|
722
909
|
let apiKey = c.apiKey;
|
|
@@ -754,7 +941,7 @@ export async function cmdImport(opts) {
|
|
|
754
941
|
fail(t("import.noModelsImport", { id: c.id }));
|
|
755
942
|
const id = availableProviderId(c.id, store.providers);
|
|
756
943
|
const hint = guessProviderHint(catalog, c.baseUrl);
|
|
757
|
-
const models =
|
|
944
|
+
const models = await enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader });
|
|
758
945
|
const defaultModel = c.defaultModel && ids.includes(c.defaultModel) ? c.defaultModel : ids[0];
|
|
759
946
|
const existed = store.providers[id] !== undefined;
|
|
760
947
|
store.providers[id] = {
|
|
@@ -767,6 +954,7 @@ export async function cmdImport(opts) {
|
|
|
767
954
|
models,
|
|
768
955
|
defaultModel,
|
|
769
956
|
modelsDevId: hint,
|
|
957
|
+
...metadataOptions,
|
|
770
958
|
};
|
|
771
959
|
if (!store.active)
|
|
772
960
|
store.active = id;
|