agentsw 0.10.2 → 0.11.1
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 +56 -18
- package/README.zh-CN.md +43 -12
- package/dist/commands.js +358 -76
- package/dist/discover.js +54 -10
- package/dist/i18n.js +38 -13
- package/dist/index.js +13 -3
- package/dist/menu.js +39 -8
- package/dist/metadata.js +35 -5
- package/dist/target-transaction.js +2 -2
- package/dist/targets/claudecode.js +2 -0
- package/dist/targets/dsh.js +3 -2
- package/dist/targets/omp.js +4 -21
- package/dist/targets/pistyle.js +31 -39
- package/dist/targets/wire.js +65 -0
- package/dist/targets/workbuddy.js +7 -3
- package/dist/ui.js +5 -3
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -11,13 +11,13 @@ import { loadCatalog, searchCatalog } from "./modelsdev.js";
|
|
|
11
11
|
import { classifyUnresolved, enrichProviderModels, resolveMetadataOptions } from "./metadata.js";
|
|
12
12
|
import { loadGatewayCatalog } from "./gateway.js";
|
|
13
13
|
import { resolveTargets, supportsProtocol, targets } from "./targets/index.js";
|
|
14
|
-
import {
|
|
14
|
+
import { discoverProviderCatalog, probeProtocols } from "./discover.js";
|
|
15
15
|
import { appCommand, appPackages, installedVersion, isNewer, latestVersion, normalizeAppVersion, runShell } from "./apps.js";
|
|
16
|
-
import { SafeConfigError, drainPendingWrites, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
16
|
+
import { SafeConfigError, drainPendingWrites, isDryRun, readTextIfExists, setDryRun } from "./fsutil.js";
|
|
17
17
|
import { applyModelFilter } from "./filter.js";
|
|
18
18
|
import { availableProviderId, providerIdFromBaseUrl, providerNameFromBaseUrl } from "./slug.js";
|
|
19
19
|
import { t } from "./i18n.js";
|
|
20
|
-
import { cancelInteractive, emitJson, error, isJson, note, out, table } from "./ui.js";
|
|
20
|
+
import { cancelInteractive, emitJson, error, isJson, note, out, table, warn } from "./ui.js";
|
|
21
21
|
import { withProgress } from "./progress.js";
|
|
22
22
|
import { appsReport, listReport, modelsQueryReport, providerMetadataReport, providerModelsReport, statusReport, } from "./report.js";
|
|
23
23
|
/** Share successes and failures without fetching until a model actually needs the supplement. */
|
|
@@ -202,16 +202,18 @@ export async function cmdAdd(opts) {
|
|
|
202
202
|
const metadataOptions = resolveMetadataOptions(opts, existing);
|
|
203
203
|
const manualIds = (answers.models ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
204
204
|
let modelIds = manualIds;
|
|
205
|
+
// Limits the endpoint declares for its own models, kept from the discovery pass.
|
|
206
|
+
let endpointLimits = new Map();
|
|
205
207
|
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
206
208
|
const shouldDiscover = opts.discover || manualIds.length === 0;
|
|
207
209
|
if (shouldDiscover) {
|
|
208
210
|
if (!opts.discover) {
|
|
209
211
|
note(t("add.autoDiscover"));
|
|
210
212
|
}
|
|
211
|
-
const discover = () => withProgress(t("add.discovering", { url: baseUrl }), (progress) =>
|
|
213
|
+
const discover = () => withProgress(t("add.discovering", { url: baseUrl }), (progress) => discoverProviderCatalog({ baseUrl, apiKey: answers.apiKey, protocol }, {
|
|
212
214
|
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: baseUrl, page, count })),
|
|
213
215
|
}));
|
|
214
|
-
let discovered = [];
|
|
216
|
+
let discovered = { ids: [], limits: new Map() };
|
|
215
217
|
for (let attempt = 0;; attempt++) {
|
|
216
218
|
try {
|
|
217
219
|
discovered = await discover();
|
|
@@ -228,9 +230,10 @@ export async function cmdAdd(opts) {
|
|
|
228
230
|
break;
|
|
229
231
|
}
|
|
230
232
|
}
|
|
231
|
-
if (discovered.length)
|
|
232
|
-
out(t("add.providerLists", { count: discovered.length }));
|
|
233
|
-
modelIds = [...new Set([...manualIds, ...discovered])];
|
|
233
|
+
if (discovered.ids.length)
|
|
234
|
+
out(t("add.providerLists", { count: discovered.ids.length }));
|
|
235
|
+
modelIds = [...new Set([...manualIds, ...discovered.ids])];
|
|
236
|
+
endpointLimits = discovered.limits;
|
|
234
237
|
}
|
|
235
238
|
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
236
239
|
modelIds = [...new Set([...modelIds, ...retained])];
|
|
@@ -242,7 +245,7 @@ export async function cmdAdd(opts) {
|
|
|
242
245
|
fail(t("add.atLeastOne"));
|
|
243
246
|
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
244
247
|
const hint = opts.modelsDev ?? existing?.modelsDevId ?? guessProviderHint(catalog, answers.baseUrl);
|
|
245
|
-
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: existing?.models }));
|
|
248
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: existing?.models }, { endpointLimits }));
|
|
246
249
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
247
250
|
const defaultModel = opts.defaultModel ?? existing?.defaultModel ?? modelIds[0];
|
|
248
251
|
if (!modelIds.includes(defaultModel))
|
|
@@ -287,7 +290,10 @@ async function createProvider(opts) {
|
|
|
287
290
|
const metadataOptions = resolveMetadataOptions(opts, opts.existing);
|
|
288
291
|
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
289
292
|
const hint = opts.modelsDev ?? opts.existing?.modelsDevId ?? guessProviderHint(catalog, baseUrl);
|
|
290
|
-
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: opts.existing?.models }, {
|
|
293
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, modelIds, { ...metadataOptions, modelsDevId: hint, models: opts.existing?.models }, {
|
|
294
|
+
gatewayLoader: opts.gatewayLoader,
|
|
295
|
+
endpointLimits: opts.endpointLimits,
|
|
296
|
+
}));
|
|
291
297
|
const matched = models.filter((m) => m.contextWindow !== undefined).length;
|
|
292
298
|
const defaultModel = opts.defaultModel ?? opts.existing?.defaultModel ?? modelIds[0];
|
|
293
299
|
if (!modelIds.includes(defaultModel))
|
|
@@ -387,12 +393,12 @@ export async function cmdQuickAdd(opts) {
|
|
|
387
393
|
const id = byId?.id ?? sameAccount?.id ?? explicitId ?? availableProviderId(providerIdFromBaseUrl(baseUrl, protocol), store.providers);
|
|
388
394
|
const name = opts.name ?? existing?.name ?? (opts.id === undefined ? providerNameFromBaseUrl(baseUrl, protocol) : multi ? `${opts.id} (${protocol})` : opts.id);
|
|
389
395
|
const modelFilter = parseFilterOpts(opts, existing?.modelFilter);
|
|
390
|
-
const discovered = await withProgress(`${t("add.discovering", { url: baseUrl })} [${protocol}]`, (progress) =>
|
|
396
|
+
const discovered = await withProgress(`${t("add.discovering", { url: baseUrl })} [${protocol}]`, (progress) => discoverProviderCatalog({ baseUrl, apiKey, protocol }, {
|
|
391
397
|
onPage: ({ page, count }) => progress.update(`${t("progress.discoverPage", { url: baseUrl, page, count })} [${protocol}]`),
|
|
392
398
|
}));
|
|
393
|
-
out(t("add.providerLists", { count: discovered.length }));
|
|
399
|
+
out(t("add.providerLists", { count: discovered.ids.length }));
|
|
394
400
|
const retained = [opts.defaultModel === undefined ? existing?.defaultModel : undefined, opts.smallModel === undefined ? existing?.smallModel : undefined].filter((id) => !!id);
|
|
395
|
-
let modelIds = [...new Set([...discovered, ...retained])];
|
|
401
|
+
let modelIds = [...new Set([...discovered.ids, ...retained])];
|
|
396
402
|
const pinned = [...retained, ...(opts.defaultModel ? [opts.defaultModel] : []), ...(opts.smallModel ? [opts.smallModel] : [])];
|
|
397
403
|
const outcome = applyModelFilter(modelIds, modelFilter, pinned);
|
|
398
404
|
reportDropped(outcome.dropped);
|
|
@@ -420,6 +426,7 @@ export async function cmdQuickAdd(opts) {
|
|
|
420
426
|
gatewayModels: opts.gatewayModels,
|
|
421
427
|
metadataMode: opts.metadataMode,
|
|
422
428
|
gatewayLoader,
|
|
429
|
+
endpointLimits: discovered.limits,
|
|
423
430
|
});
|
|
424
431
|
createdIds.push(provider.id);
|
|
425
432
|
note(`\n${t("add.next", { id: provider.id })}`);
|
|
@@ -466,13 +473,14 @@ export async function cmdPrune(id, opts) {
|
|
|
466
473
|
out(`${t("remove.pruning", { id: pc.bold(id) })}\n`);
|
|
467
474
|
const results = await runTargets("prune", provider, opts.apps);
|
|
468
475
|
if (isJson()) {
|
|
476
|
+
const failed = results.filter((r) => r.failed).map((r) => r.app);
|
|
469
477
|
emitJson({
|
|
470
478
|
provider: id,
|
|
471
479
|
op: "prune",
|
|
472
480
|
apps: results.filter((r) => !r.skipped).map((r) => r.app),
|
|
473
481
|
files: results.flatMap((r) => (r.skipped ? [] : r.changed)),
|
|
474
482
|
skipped: results.filter((r) => r.skipped).map((r) => ({ app: r.app, reason: r.skipped })),
|
|
475
|
-
});
|
|
483
|
+
}, failed.length ? t("targets.failed", { apps: failed.join(", ") }) : undefined);
|
|
476
484
|
return;
|
|
477
485
|
}
|
|
478
486
|
reportResults(results);
|
|
@@ -491,7 +499,7 @@ function reportResults(results) {
|
|
|
491
499
|
out(` ${" ".repeat(9)} ${pc.dim(note)}`);
|
|
492
500
|
}
|
|
493
501
|
}
|
|
494
|
-
async function runTargets(op, provider, appsFilter,
|
|
502
|
+
async function runTargets(op, provider, appsFilter, opts = {}) {
|
|
495
503
|
const selected = resolveTargets(appsFilter);
|
|
496
504
|
const explicit = appsFilter !== undefined && appsFilter !== "all";
|
|
497
505
|
const results = [];
|
|
@@ -505,16 +513,29 @@ async function runTargets(op, provider, appsFilter, redactErrors = false) {
|
|
|
505
513
|
});
|
|
506
514
|
continue;
|
|
507
515
|
}
|
|
516
|
+
if (op === "apply" && opts.oneSlot && target.singleProvider && provider.id !== opts.oneSlot.winners.get(target.id)) {
|
|
517
|
+
const winner = opts.oneSlot.winners.get(target.id);
|
|
518
|
+
results.push({
|
|
519
|
+
app: target.id,
|
|
520
|
+
changed: [],
|
|
521
|
+
notes: [],
|
|
522
|
+
skipped: winner === undefined
|
|
523
|
+
? `${target.name} holds one provider at a time; left as it is because this run does not say which`
|
|
524
|
+
: `${target.name} holds one provider at a time; written for ${winner}`,
|
|
525
|
+
});
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
508
528
|
try {
|
|
509
529
|
if (!explicit && !target.detect()) {
|
|
510
530
|
results.push({ app: target.id, changed: [], notes: [], skipped: `${target.name} not detected (pass --apps ${target.id} to force)` });
|
|
511
531
|
continue;
|
|
512
532
|
}
|
|
513
|
-
results.push(await target
|
|
533
|
+
results.push(await (op === "apply" ? target.apply(provider, opts.applyOptions) : target.prune(provider)));
|
|
514
534
|
}
|
|
515
535
|
catch (err) {
|
|
516
|
-
const safe = redactErrors && !(err instanceof SafeConfigError);
|
|
517
|
-
|
|
536
|
+
const safe = (opts.redactErrors === true || isJson()) && !(err instanceof SafeConfigError);
|
|
537
|
+
const message = safe ? t(opts.redactErrors ? "preview.failedSafely" : "targets.failedSafely") : err.message;
|
|
538
|
+
results.push({ app: target.id, changed: [], notes: [], skipped: pc.red(`failed: ${message}`), failed: true });
|
|
518
539
|
process.exitCode = 1;
|
|
519
540
|
}
|
|
520
541
|
}
|
|
@@ -632,20 +653,22 @@ function printFileDiff(file, next, apiKey) {
|
|
|
632
653
|
if (oldText === newText)
|
|
633
654
|
out(pc.dim(t("preview.onlyRedacted")));
|
|
634
655
|
}
|
|
635
|
-
async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
656
|
+
async function runWithOptionalDryRun(op, provider, apps, dryRun, oneSlot, applyOptions) {
|
|
636
657
|
const summarize = (results, files) => ({
|
|
658
|
+
...(results.some((r) => r.failed) ? { failed: results.filter((r) => r.failed).map((r) => r.app) } : {}),
|
|
637
659
|
apps: results.map((r) => r.app),
|
|
638
660
|
files,
|
|
639
661
|
skipped: results.filter((r) => r.skipped).map((r) => ({ app: r.app, reason: r.skipped })),
|
|
640
662
|
});
|
|
641
663
|
if (!dryRun) {
|
|
642
|
-
const results = await runTargets(op, provider, apps);
|
|
664
|
+
const results = await runTargets(op, provider, apps, { oneSlot, applyOptions });
|
|
643
665
|
reportResults(results);
|
|
644
666
|
return summarize(results, results.flatMap((r) => (r.skipped ? [] : r.changed)));
|
|
645
667
|
}
|
|
668
|
+
const wasDryRun = isDryRun();
|
|
646
669
|
setDryRun(true);
|
|
647
670
|
try {
|
|
648
|
-
const results = await runTargets(op, provider, apps, true);
|
|
671
|
+
const results = await runTargets(op, provider, apps, { redactErrors: true, oneSlot, applyOptions });
|
|
649
672
|
const writes = drainPendingWrites();
|
|
650
673
|
for (const r of results) {
|
|
651
674
|
if (r.skipped)
|
|
@@ -658,19 +681,32 @@ async function runWithOptionalDryRun(op, provider, apps, dryRun) {
|
|
|
658
681
|
return summarize(results, writes.map((w) => w.file));
|
|
659
682
|
}
|
|
660
683
|
finally {
|
|
661
|
-
setDryRun(
|
|
684
|
+
setDryRun(wasDryRun);
|
|
662
685
|
}
|
|
663
686
|
}
|
|
664
687
|
export async function cmdUse(id, opts) {
|
|
665
688
|
resolveTargets(opts.apps);
|
|
666
689
|
const store = loadStore();
|
|
667
690
|
const provider = getProvider(store, id);
|
|
668
|
-
|
|
669
|
-
if (!provider.models.some((m) => m.id ===
|
|
670
|
-
fail(t("use.modelMissing", { model
|
|
691
|
+
const requireConfigured = (model) => {
|
|
692
|
+
if (!provider.models.some((m) => m.id === model)) {
|
|
693
|
+
fail(t("use.modelMissing", { model, id, have: provider.models.map((m) => m.id).join(", ") }));
|
|
671
694
|
}
|
|
695
|
+
};
|
|
696
|
+
if (opts.model) {
|
|
697
|
+
requireConfigured(opts.model);
|
|
672
698
|
provider.defaultModel = opts.model;
|
|
673
699
|
}
|
|
700
|
+
// The slot agents reach for cheap background work — Claude Code's haiku variables. Leaving it
|
|
701
|
+
// unset means "same as the default model", so an expensive default silently becomes the
|
|
702
|
+
// background model too; `--no-small-model` returns the slot to that fallback.
|
|
703
|
+
if (typeof opts.smallModel === "string") {
|
|
704
|
+
requireConfigured(opts.smallModel);
|
|
705
|
+
provider.smallModel = opts.smallModel;
|
|
706
|
+
}
|
|
707
|
+
else if (opts.smallModel === false) {
|
|
708
|
+
delete provider.smallModel;
|
|
709
|
+
}
|
|
674
710
|
if (!opts.dryRun) {
|
|
675
711
|
store.active = id;
|
|
676
712
|
saveStore(store);
|
|
@@ -678,20 +714,225 @@ export async function cmdUse(id, opts) {
|
|
|
678
714
|
out(`${t("use.switching", { id: pc.bold(id), protocol: provider.protocol, model: provider.defaultModel })}\n`);
|
|
679
715
|
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
680
716
|
if (isJson()) {
|
|
681
|
-
emitJson({ provider: id, model: provider.defaultModel, dryRun: opts.dryRun === true, ...outcome });
|
|
717
|
+
emitJson({ provider: id, model: provider.defaultModel, dryRun: opts.dryRun === true, ...outcome }, outcome.failed?.length ? t("targets.failed", { apps: outcome.failed.join(", ") }) : undefined);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
function saveRefreshedProvider(store, provider) {
|
|
721
|
+
const previous = getProvider(store, provider.id);
|
|
722
|
+
store.providers[provider.id] = provider;
|
|
723
|
+
try {
|
|
724
|
+
saveStore(store);
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
store.providers[provider.id] = previous;
|
|
728
|
+
throw err;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Shared by `discover` and `sync`: fetch the live model list, re-enrich from the catalog, persist.
|
|
733
|
+
* `pinned` ids survive a listing that dropped them, so they are not reported as "removed upstream".
|
|
734
|
+
* `save: false` keeps the refreshed list in memory for the caller to preview: a dry run shows what a
|
|
735
|
+
* real sync would write without leaving the fetched list behind in the store.
|
|
736
|
+
*/
|
|
737
|
+
async function refreshProviderModels(store, provider, save = true) {
|
|
738
|
+
const listed = await withProgress(t("add.discovering", { url: provider.baseUrl }), (progress) => discoverProviderCatalog(provider, {
|
|
739
|
+
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: provider.baseUrl, page, count })),
|
|
740
|
+
}));
|
|
741
|
+
const pinned = [provider.defaultModel, provider.smallModel].filter((model) => !!model);
|
|
742
|
+
const outcome = applyModelFilter(listed.ids, provider.modelFilter, pinned);
|
|
743
|
+
reportDropped(outcome.dropped);
|
|
744
|
+
const ids = outcome.kept;
|
|
745
|
+
const retained = new Set([...ids, ...pinned]);
|
|
746
|
+
const known = provider.models.map((m) => m.id);
|
|
747
|
+
const added = ids.filter((m) => !known.includes(m));
|
|
748
|
+
const removed = known.filter((m) => !retained.has(m));
|
|
749
|
+
const catalog = await withProgress(t("progress.catalog"), () => loadCatalog());
|
|
750
|
+
provider.models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, [...retained], { ...provider, modelsDevId: provider.modelsDevId ?? guessProviderHint(catalog, provider.baseUrl) }, { endpointLimits: listed.limits }));
|
|
751
|
+
// Reported by the caller, where it reads after the table or the per-app results instead of
|
|
752
|
+
// scrolling away above them.
|
|
753
|
+
const missing = [...new Set(pinned)].filter((model) => !ids.includes(model));
|
|
754
|
+
if (save)
|
|
755
|
+
saveRefreshedProvider(store, provider);
|
|
756
|
+
return { ids, added, removed, missing, catalog };
|
|
757
|
+
}
|
|
758
|
+
function printModelRefresh(provider, refresh) {
|
|
759
|
+
out(`${pc.bold(provider.id)}: ${refresh.ids.length} models (${pc.green(`+${refresh.added.length}`)} / ${pc.red(`-${refresh.removed.length}`)})` +
|
|
760
|
+
(refresh.added.length ? `\n new: ${refresh.added.join(", ")}` : "") +
|
|
761
|
+
(refresh.removed.length ? `\n removed upstream: ${refresh.removed.join(", ")}` : ""));
|
|
762
|
+
out(table(modelRows(provider.models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
763
|
+
printUncatalogedHint(provider.models, refresh.catalog, provider.modelsDevId);
|
|
764
|
+
}
|
|
765
|
+
/** Comma-separated ids, the same shape `--apps` takes. Repeats collapse; blanks are ignored. */
|
|
766
|
+
function splitProviderList(value) {
|
|
767
|
+
return [...new Set(value.split(",").map((id) => id.trim()).filter(Boolean))];
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* One provider selection for every command that acts on providers: `--all` takes every configured
|
|
771
|
+
* one, `--provider` takes a comma-separated list, and `fallback` says what no selection means —
|
|
772
|
+
* `"active"` for the commands that write (sync), `"all"` for the ones that only read and refresh.
|
|
773
|
+
* Every id is resolved up front so an unknown one fails before the first network call, never after
|
|
774
|
+
* half a run has been saved. An empty store is an error only when `--all` asked for it by name; the
|
|
775
|
+
* implicit all-providers fallback keeps reporting zero, as it did before there was a flag.
|
|
776
|
+
*/
|
|
777
|
+
function resolveProviderIds(store, opts, fallback) {
|
|
778
|
+
if (opts.all && opts.provider !== undefined)
|
|
779
|
+
fail(t("select.allAndProvider"));
|
|
780
|
+
let ids;
|
|
781
|
+
if (opts.all) {
|
|
782
|
+
ids = Object.keys(store.providers);
|
|
783
|
+
if (!ids.length)
|
|
784
|
+
fail(t("select.noProviders"));
|
|
785
|
+
}
|
|
786
|
+
else if (opts.provider !== undefined) {
|
|
787
|
+
ids = splitProviderList(opts.provider);
|
|
788
|
+
if (!ids.length)
|
|
789
|
+
fail(t("select.noneSelected"));
|
|
790
|
+
}
|
|
791
|
+
else if (fallback === "all") {
|
|
792
|
+
ids = Object.keys(store.providers);
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
if (!store.active)
|
|
796
|
+
fail(t("sync.noActive"));
|
|
797
|
+
ids = [store.active];
|
|
798
|
+
}
|
|
799
|
+
for (const id of ids)
|
|
800
|
+
getProvider(store, id);
|
|
801
|
+
return ids;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Apps that name a single provider are written once (see `RunTargetsOptions.oneSlot`); for the rest
|
|
805
|
+
* the last writer owns whatever "current" pointer the app keeps, so a multi-provider sync finishes
|
|
806
|
+
* on the active provider and leaves the agents where the store says they already are.
|
|
807
|
+
*/
|
|
808
|
+
function activeLast(store, ids) {
|
|
809
|
+
const active = store.active;
|
|
810
|
+
if (ids.length < 2 || !active || !ids.includes(active))
|
|
811
|
+
return ids;
|
|
812
|
+
return [...ids.filter((id) => id !== active), active];
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Which provider each single-slot app should receive. The active provider wins where the app can
|
|
816
|
+
* accept it; where it cannot — Claude Code takes anthropic endpoints only, and the active provider
|
|
817
|
+
* may well be an openai one — the single compatible provider in the run wins instead, because
|
|
818
|
+
* writing the only candidate takes nothing away. With several compatible candidates and no active
|
|
819
|
+
* provider among them the choice is the user's, so the app is left pointing where it already does.
|
|
820
|
+
*/
|
|
821
|
+
function oneSlotWinners(selected, store, ids) {
|
|
822
|
+
const winners = new Map();
|
|
823
|
+
for (const target of selected) {
|
|
824
|
+
if (!target.singleProvider)
|
|
825
|
+
continue;
|
|
826
|
+
const usable = ids.filter((id) => supportsProtocol(target, store.providers[id].protocol));
|
|
827
|
+
const winner = store.active !== undefined && usable.includes(store.active)
|
|
828
|
+
? store.active
|
|
829
|
+
: usable.length === 1 ? usable[0] : undefined;
|
|
830
|
+
if (winner !== undefined)
|
|
831
|
+
winners.set(target.id, winner);
|
|
832
|
+
}
|
|
833
|
+
return winners;
|
|
834
|
+
}
|
|
835
|
+
/** Section separator plus the tail line that says how many of several providers came through. */
|
|
836
|
+
function printMultiSummary(count, failed, key) {
|
|
837
|
+
const done = count - failed;
|
|
838
|
+
out(`\n${failed
|
|
839
|
+
? pc.yellow(t(key === "sync" ? "sync.summaryFailed" : "discover.summaryFailed", { ok: done, count }))
|
|
840
|
+
: t(key === "sync" ? "sync.summaryOk" : "discover.summaryOk", { count })}`);
|
|
841
|
+
}
|
|
842
|
+
function syncTargets(opts) {
|
|
843
|
+
const selected = resolveTargets(opts.apps);
|
|
844
|
+
if (opts.keepDefault) {
|
|
845
|
+
const unsupported = selected.filter((target) => !target.supportsKeepDefault);
|
|
846
|
+
if (unsupported.length)
|
|
847
|
+
fail(t("sync.keepDefaultUnsupported", { apps: unsupported.map((target) => target.id).join(", ") }));
|
|
848
|
+
}
|
|
849
|
+
return selected;
|
|
850
|
+
}
|
|
851
|
+
async function syncProviders(opts, sourceStore) {
|
|
852
|
+
const selectedTargets = syncTargets(opts);
|
|
853
|
+
const store = sourceStore ?? loadStore();
|
|
854
|
+
const ids = activeLast(store, resolveProviderIds(store, opts, "active"));
|
|
855
|
+
const many = ids.length > 1;
|
|
856
|
+
const oneSlot = many ? { winners: oneSlotWinners(selectedTargets, store, ids) } : undefined;
|
|
857
|
+
const saveRefresh = opts.dryRun !== true;
|
|
858
|
+
const reports = [];
|
|
859
|
+
const failed = [];
|
|
860
|
+
for (const [index, id] of ids.entries()) {
|
|
861
|
+
const provider = structuredClone(getProvider(store, id));
|
|
862
|
+
if (index)
|
|
863
|
+
out("");
|
|
864
|
+
let refresh;
|
|
865
|
+
if (opts.refresh !== false) {
|
|
866
|
+
try {
|
|
867
|
+
const wasDryRun = isDryRun();
|
|
868
|
+
try {
|
|
869
|
+
if (opts.dryRun)
|
|
870
|
+
setDryRun(true);
|
|
871
|
+
refresh = await refreshProviderModels(store, provider, saveRefresh);
|
|
872
|
+
}
|
|
873
|
+
finally {
|
|
874
|
+
// Metadata cache writes are not target configuration previews.
|
|
875
|
+
if (opts.dryRun)
|
|
876
|
+
setDryRun(wasDryRun);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
catch (err) {
|
|
880
|
+
// One unreachable provider must not strand the others: report it, write nothing for it,
|
|
881
|
+
// and fail the run. Text mode keeps the single-provider exception contract.
|
|
882
|
+
if (!many && !isJson())
|
|
883
|
+
throw new Error(`${err.message}\n${t("sync.refreshFailed")}`);
|
|
884
|
+
warn(t("sync.refreshSkipped", { id, message: err.message }));
|
|
885
|
+
process.exitCode = 1;
|
|
886
|
+
failed.push(id);
|
|
887
|
+
reports.push({ provider: id, refreshFailed: true });
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (!isJson()) {
|
|
891
|
+
printModelRefresh(provider, refresh);
|
|
892
|
+
// --dry-run promises to write nothing, so say the fetched list only lived long enough to preview.
|
|
893
|
+
if (!saveRefresh)
|
|
894
|
+
note(t("sync.refreshNotSaved"));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
out(`${t(opts.keepDefault ? "sync.syncingKeepDefault" : "sync.syncing", { id: pc.bold(id), model: provider.defaultModel })}\n`);
|
|
898
|
+
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun, oneSlot, { keepDefault: opts.keepDefault });
|
|
899
|
+
if (outcome.failed?.length)
|
|
900
|
+
failed.push(id);
|
|
901
|
+
if (refresh?.missing.length) {
|
|
902
|
+
const models = refresh.missing.join(", ");
|
|
903
|
+
warn(t(opts.dryRun ? "refresh.pinnedMissing" : "sync.pinnedMissingWritten", { models, id }));
|
|
904
|
+
}
|
|
905
|
+
reports.push({
|
|
906
|
+
provider: id,
|
|
907
|
+
model: provider.defaultModel,
|
|
908
|
+
...(refresh
|
|
909
|
+
? {
|
|
910
|
+
refreshed: {
|
|
911
|
+
models: refresh.ids.length,
|
|
912
|
+
added: refresh.added,
|
|
913
|
+
removed: refresh.removed,
|
|
914
|
+
missing: refresh.missing,
|
|
915
|
+
saved: saveRefresh,
|
|
916
|
+
},
|
|
917
|
+
}
|
|
918
|
+
: {}),
|
|
919
|
+
...outcome,
|
|
920
|
+
});
|
|
682
921
|
}
|
|
922
|
+
if (many)
|
|
923
|
+
printMultiSummary(ids.length, failed.length, "sync");
|
|
924
|
+
return { reports, failed };
|
|
683
925
|
}
|
|
684
926
|
export async function cmdSync(opts) {
|
|
685
|
-
|
|
686
|
-
const store = loadStore();
|
|
687
|
-
const id = opts.provider ?? store.active;
|
|
688
|
-
if (!id)
|
|
689
|
-
fail(t("sync.noActive"));
|
|
690
|
-
const provider = getProvider(store, id);
|
|
691
|
-
out(`${t("sync.syncing", { id: pc.bold(id), model: provider.defaultModel })}\n`);
|
|
692
|
-
const outcome = await runWithOptionalDryRun("apply", provider, opts.apps, opts.dryRun);
|
|
927
|
+
const { reports, failed } = await syncProviders(opts);
|
|
693
928
|
if (isJson()) {
|
|
694
|
-
emitJson(
|
|
929
|
+
emitJson(reports.length > 1
|
|
930
|
+
? {
|
|
931
|
+
dryRun: opts.dryRun === true,
|
|
932
|
+
providers: reports,
|
|
933
|
+
files: [...new Set(reports.flatMap((report) => report.files ?? []))],
|
|
934
|
+
}
|
|
935
|
+
: { ...(reports[0] ?? {}), dryRun: opts.dryRun === true }, failed.length ? t("sync.failed", { ids: failed.join(", ") }) : undefined);
|
|
695
936
|
}
|
|
696
937
|
}
|
|
697
938
|
export function cmdStatus() {
|
|
@@ -770,7 +1011,8 @@ export async function cmdModels(query, opts) {
|
|
|
770
1011
|
}
|
|
771
1012
|
export async function cmdRefreshMeta(opts = {}) {
|
|
772
1013
|
const store = loadStore();
|
|
773
|
-
|
|
1014
|
+
// Omitting the selection still means every provider: this command only reads catalogs.
|
|
1015
|
+
const providers = resolveProviderIds(store, opts, "all").map((id) => getProvider(store, id));
|
|
774
1016
|
resolveMetadataOptions(opts);
|
|
775
1017
|
for (const provider of providers)
|
|
776
1018
|
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
@@ -797,50 +1039,88 @@ export async function cmdRefreshMeta(opts = {}) {
|
|
|
797
1039
|
for (const provider of providers)
|
|
798
1040
|
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
799
1041
|
}
|
|
800
|
-
export async function cmdDiscover(
|
|
801
|
-
|
|
1042
|
+
export async function cmdDiscover(ids, opts) {
|
|
1043
|
+
if (opts.keepDefault && !opts.sync)
|
|
1044
|
+
fail(t("discover.keepDefaultRequiresSync"));
|
|
1045
|
+
syncTargets(opts);
|
|
802
1046
|
const store = loadStore();
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1047
|
+
// Ids may arrive as positional arguments, as one comma-separated list, or through `-p` for
|
|
1048
|
+
// symmetry with sync; all three say the same thing, so accept them together.
|
|
1049
|
+
const listed = [...(Array.isArray(ids) ? ids : [ids ?? ""]), opts.provider ?? ""].filter(Boolean).join(",");
|
|
1050
|
+
const selected = resolveProviderIds(store, { provider: listed || undefined, all: opts.all }, "active");
|
|
1051
|
+
// Reject bad metadata options once, before the first provider is contacted.
|
|
1052
|
+
resolveMetadataOptions(opts);
|
|
1053
|
+
const many = selected.length > 1;
|
|
1054
|
+
const reports = [];
|
|
1055
|
+
const refreshed = [];
|
|
1056
|
+
const failed = [];
|
|
1057
|
+
for (const [index, id] of selected.entries()) {
|
|
1058
|
+
const provider = structuredClone(getProvider(store, id));
|
|
1059
|
+
if (index)
|
|
1060
|
+
out("");
|
|
1061
|
+
Object.assign(provider, resolveMetadataOptions(opts, provider));
|
|
1062
|
+
// flags override and re-persist the filter; --no-filter clears it
|
|
1063
|
+
const flagFilter = parseFilterOpts(opts, provider.modelFilter);
|
|
1064
|
+
if (opts.filter === false)
|
|
1065
|
+
provider.modelFilter = undefined;
|
|
1066
|
+
else if (flagFilter)
|
|
1067
|
+
provider.modelFilter = flagFilter;
|
|
1068
|
+
let refresh;
|
|
1069
|
+
try {
|
|
1070
|
+
const wasDryRun = isDryRun();
|
|
1071
|
+
try {
|
|
1072
|
+
if (opts.dryRun)
|
|
1073
|
+
setDryRun(true);
|
|
1074
|
+
refresh = await refreshProviderModels(store, provider, false);
|
|
1075
|
+
if (opts.dryRun)
|
|
1076
|
+
store.providers[id] = provider;
|
|
1077
|
+
else
|
|
1078
|
+
saveRefreshedProvider(store, provider);
|
|
1079
|
+
}
|
|
1080
|
+
finally {
|
|
1081
|
+
if (opts.dryRun)
|
|
1082
|
+
setDryRun(wasDryRun);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
catch (err) {
|
|
1086
|
+
// One unreachable endpoint must not strand the others; text mode still throws for one.
|
|
1087
|
+
if (!many && !isJson())
|
|
1088
|
+
throw err;
|
|
1089
|
+
warn(t("sync.refreshSkipped", { id, message: err.message }));
|
|
1090
|
+
process.exitCode = 1;
|
|
1091
|
+
failed.push(id);
|
|
1092
|
+
reports.push({ provider: id, refreshFailed: true });
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
refreshed.push(id);
|
|
1096
|
+
reports.push({ provider: id, models: refresh.ids.length, added: refresh.added, removed: refresh.removed, missing: refresh.missing });
|
|
1097
|
+
if (!isJson())
|
|
1098
|
+
printModelRefresh(provider, refresh);
|
|
1099
|
+
if (refresh.missing.length)
|
|
1100
|
+
warn(t("refresh.pinnedMissing", { models: refresh.missing.join(", "), id }));
|
|
828
1101
|
}
|
|
829
|
-
saveStore(store);
|
|
830
1102
|
if (isJson()) {
|
|
831
|
-
|
|
1103
|
+
if (opts.sync && refreshed.length) {
|
|
1104
|
+
// Collect application results without emitting or fetching the discovered lists again.
|
|
1105
|
+
const synced = await syncProviders({ provider: refreshed.join(","), apps: opts.apps, refresh: false, keepDefault: opts.keepDefault, dryRun: opts.dryRun }, store);
|
|
1106
|
+
failed.push(...synced.failed);
|
|
1107
|
+
for (const report of reports)
|
|
1108
|
+
Object.assign(report, synced.reports.find((entry) => entry.provider === report.provider));
|
|
1109
|
+
}
|
|
1110
|
+
const files = [...new Set(reports.flatMap((report) => report.files ?? []))];
|
|
1111
|
+
emitJson(many
|
|
1112
|
+
? { providers: reports, ...(opts.sync ? { files, dryRun: opts.dryRun === true } : {}) }
|
|
1113
|
+
: { ...(reports[0] ?? {}), ...(opts.sync ? { dryRun: opts.dryRun === true } : {}) }, failed.length ? t("discover.failed", { ids: failed.join(", ") }) : undefined);
|
|
832
1114
|
return;
|
|
833
1115
|
}
|
|
834
|
-
|
|
835
|
-
(
|
|
836
|
-
|
|
837
|
-
out(table(modelRows(provider.models), MODEL_HEADER, MODEL_TABLE_OPTIONS));
|
|
838
|
-
printUncatalogedHint(provider.models, catalog, provider.modelsDevId);
|
|
839
|
-
if (opts.sync) {
|
|
1116
|
+
if (many)
|
|
1117
|
+
printMultiSummary(selected.length, selected.length - refreshed.length, "discover");
|
|
1118
|
+
if (opts.sync && refreshed.length) {
|
|
840
1119
|
out("");
|
|
841
|
-
|
|
1120
|
+
// The lists are already fresh: write them without a second fetch.
|
|
1121
|
+
await syncProviders({ provider: refreshed.join(","), apps: opts.apps, refresh: false, keepDefault: opts.keepDefault, dryRun: opts.dryRun }, store);
|
|
842
1122
|
}
|
|
843
|
-
else {
|
|
1123
|
+
else if (!opts.sync) {
|
|
844
1124
|
note(`\n${t("discover.next")}`);
|
|
845
1125
|
}
|
|
846
1126
|
}
|
|
@@ -1101,8 +1381,9 @@ export async function cmdImport(opts) {
|
|
|
1101
1381
|
continue;
|
|
1102
1382
|
}
|
|
1103
1383
|
let ids = [...c.models];
|
|
1384
|
+
let endpointLimits = new Map();
|
|
1104
1385
|
if (ids.length === 0) {
|
|
1105
|
-
const discover = () => withProgress(t("import.discovering", { id: c.id }), (progress) =>
|
|
1386
|
+
const discover = () => withProgress(t("import.discovering", { id: c.id }), (progress) => discoverProviderCatalog({ baseUrl: c.baseUrl, apiKey: apiKey, protocol: c.protocol }, {
|
|
1106
1387
|
onPage: ({ page, count }) => progress.update(t("progress.discoverPage", { url: c.baseUrl, page, count })),
|
|
1107
1388
|
}));
|
|
1108
1389
|
let listed;
|
|
@@ -1126,14 +1407,15 @@ export async function cmdImport(opts) {
|
|
|
1126
1407
|
out(pc.yellow(t("import.skippedAfterFailure", { id: c.id })));
|
|
1127
1408
|
continue;
|
|
1128
1409
|
}
|
|
1129
|
-
ids = listed;
|
|
1410
|
+
ids = listed.ids;
|
|
1411
|
+
endpointLimits = listed.limits;
|
|
1130
1412
|
ids = applyModelFilter(ids, undefined, []).kept;
|
|
1131
1413
|
}
|
|
1132
1414
|
if (ids.length === 0)
|
|
1133
1415
|
fail(t("import.noModelsImport", { id: c.id }));
|
|
1134
1416
|
const id = availableProviderId(c.id, store.providers);
|
|
1135
1417
|
const hint = guessProviderHint(catalog, c.baseUrl);
|
|
1136
|
-
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader }));
|
|
1418
|
+
const models = await withProgress(t("progress.metadata"), () => enrichProviderModels(catalog, ids, { ...metadataOptions, modelsDevId: hint }, { gatewayLoader, endpointLimits }));
|
|
1137
1419
|
const defaultModel = c.defaultModel && ids.includes(c.defaultModel) ? c.defaultModel : ids[0];
|
|
1138
1420
|
const existed = store.providers[id] !== undefined;
|
|
1139
1421
|
store.providers[id] = {
|