@ordewell/core 0.10.3 → 0.10.4

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/dist/index.mjs CHANGED
@@ -6406,7 +6406,7 @@ var CODEX_MANIFEST = {
6406
6406
  }
6407
6407
  },
6408
6408
  modelDiscovery: {
6409
- // Codex has no `codex models` subcommand (see docs/adr/0004). Discovery
6409
+ // Codex has no `codex models` subcommand (see ADR-0004). Discovery
6410
6410
  // tries three sources in order, each falling through to the next:
6411
6411
  //
6412
6412
  // 1. `codex app-server` stdio JSON-RPC (`initialize` → `model/list`) —
@@ -6508,9 +6508,11 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
6508
6508
  variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
6509
6509
  });
6510
6510
  }
6511
- if (models.length === 0) return [];
6512
- const prefOrder = preferred?.map((p) => p.id) ?? [];
6513
- models.sort((a, b) => {
6511
+ return preferredFirst(models, manifest);
6512
+ }
6513
+ function preferredFirst(models, manifest) {
6514
+ const prefOrder = manifest.modelDiscovery.preferredPatterns?.map((p) => p.id) ?? [];
6515
+ return [...models].sort((a, b) => {
6514
6516
  const ai = prefOrder.indexOf(a.modelId);
6515
6517
  const bi = prefOrder.indexOf(b.modelId);
6516
6518
  if (ai >= 0 && bi >= 0) return ai - bi;
@@ -6518,7 +6520,36 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
6518
6520
  if (bi >= 0) return 1;
6519
6521
  return a.modelId.localeCompare(b.modelId);
6520
6522
  });
6521
- return models;
6523
+ }
6524
+ function parseOpenCodeModelsApi(stdout, manifest) {
6525
+ let data;
6526
+ try {
6527
+ data = JSON.parse(stdout)?.data;
6528
+ } catch {
6529
+ return [];
6530
+ }
6531
+ if (!Array.isArray(data)) return [];
6532
+ const prefLabels = new Map((manifest.modelDiscovery.preferredPatterns ?? []).map((p) => [p.id, p.label]));
6533
+ const seen = /* @__PURE__ */ new Set();
6534
+ const models = [];
6535
+ for (const entry of data) {
6536
+ if (!entry || typeof entry.providerID !== "string" || typeof entry.modelID !== "string") continue;
6537
+ if (entry.enabled === false) continue;
6538
+ const fullId = `${entry.providerID}/${entry.modelID}`;
6539
+ if (seen.has(fullId)) continue;
6540
+ seen.add(fullId);
6541
+ const variantIds = Array.isArray(entry.variants) ? entry.variants.flatMap((v) => {
6542
+ const id = v?.id;
6543
+ return typeof id === "string" && id ? [id] : [];
6544
+ }) : [];
6545
+ models.push({
6546
+ modelId: fullId,
6547
+ modelLabel: prefLabels.get(fullId) || typeof entry.name === "string" && entry.name || entry.modelID,
6548
+ runnerProvider: entry.providerID,
6549
+ variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
6550
+ });
6551
+ }
6552
+ return preferredFirst(models, manifest);
6522
6553
  }
6523
6554
 
6524
6555
  // src/plugins/builtin/opencode.manifest.ts
@@ -6534,13 +6565,17 @@ var OPENCODE_MANIFEST = {
6534
6565
  modelDiscovery: {
6535
6566
  method: "command",
6536
6567
  discoveryCommands: [
6568
+ // v1 prints per-model variants under --verbose; v2 rejects the flag (a
6569
+ // fast non-zero exit) and serves the same catalog, variants included, over
6570
+ // `api model.list`. v1 is asked first so it never reaches the `api` form.
6537
6571
  { command: "opencode", args: ["models", "--verbose"], parser: parseOpenCodeModelsVerbose },
6572
+ { command: "opencode", args: ["api", "model.list"], parser: parseOpenCodeModelsApi },
6538
6573
  { command: "opencode", args: ["models"], parser: parseOpenCodeModels }
6539
6574
  ],
6540
6575
  // No fallbackModels: the list must always reflect what `opencode models`
6541
6576
  // actually reports for THIS user (their configured providers only). A
6542
6577
  // hardcoded fallback previously leaked models the user didn't have.
6543
- // No static variants — per-model variants are fetched from opencode models --verbose
6578
+ // No static variants — per-model variants come from the first two commands above
6544
6579
  providerQualifiedIds: true
6545
6580
  },
6546
6581
  contextFile: "AGENTS.md",
@@ -14059,6 +14094,7 @@ function parseCodexModelsCache(content) {
14059
14094
  }
14060
14095
  }
14061
14096
  var GEMINI_MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
14097
+ var NEGATIVE_TTL_MS = 6e4;
14062
14098
  function geminiDiscoveryCacheKey(apiKey) {
14063
14099
  const dir = globalDataDir();
14064
14100
  const hash = apiKey.slice(-8);
@@ -14188,26 +14224,33 @@ async function discoverFromApi(manifest, config, fetchImpl) {
14188
14224
  return null;
14189
14225
  }
14190
14226
  }
14227
+ function wasKilled(err) {
14228
+ return !!err && typeof err === "object" && err.killed === true;
14229
+ }
14191
14230
  async function discoverFromCommand(manifest, execImpl) {
14192
14231
  const discovery = manifest.modelDiscovery;
14193
14232
  const commands = discovery.discoveryCommands ?? [
14194
14233
  { command: discovery.command, args: discovery.args || [], parser: discovery.parser }
14195
14234
  ];
14196
14235
  const runCommands = async () => {
14236
+ let timedOut2 = false;
14197
14237
  for (const cmd of commands) {
14198
14238
  try {
14199
14239
  const { stdout } = await execImpl(`${cmd.command} ${cmd.args.join(" ")}`, { timeout: 45e3 });
14200
14240
  const models = parseModelOutput(stdout, cmd.parser, manifest);
14201
- if (models.length > 0) return applyVariants(mergeCanonicalAliases(models, manifest), manifest);
14202
- } catch {
14241
+ if (models.length > 0) return { models: applyVariants(mergeCanonicalAliases(models, manifest), manifest), timedOut: timedOut2 };
14242
+ } catch (err) {
14243
+ if (wasKilled(err)) timedOut2 = true;
14203
14244
  }
14204
14245
  }
14205
- return null;
14246
+ return { models: null, timedOut: timedOut2 };
14206
14247
  };
14207
14248
  const first = await runCommands();
14208
- if (first) return { models: first, fromFallback: false };
14209
- const second = await runCommands();
14210
- if (second) return { models: second, fromFallback: false };
14249
+ if (first.models) return { models: first.models, fromFallback: false };
14250
+ if (first.timedOut) {
14251
+ const second = await runCommands();
14252
+ if (second.models) return { models: second.models, fromFallback: false };
14253
+ }
14211
14254
  return { models: applyVariants(buildFallbackModels(manifest), manifest), fromFallback: true };
14212
14255
  }
14213
14256
  function mergeCanonicalAliases(discovered, manifest) {
@@ -14244,16 +14287,33 @@ function parseLineByLine(stdout) {
14244
14287
  }
14245
14288
  var ModelDiscovery = class {
14246
14289
  cache = /* @__PURE__ */ new Map();
14290
+ /**
14291
+ * Degraded results (a fallback list, or empty) remembered for
14292
+ * {@link NEGATIVE_TTL_MS}. They are deliberately not cached — a transient
14293
+ * failure must heal — but re-running a failing CLI on every lookup is the
14294
+ * other extreme.
14295
+ */
14296
+ misses = /* @__PURE__ */ new Map();
14297
+ /**
14298
+ * Discoveries still running, so a second lookup of the same runner joins the
14299
+ * first instead of spawning the CLI again. A slow CLI is the case this
14300
+ * exists for: a planner switch and a runner-list refresh land inside one
14301
+ * another's wait, and two `opencode models` calls race each other for the
14302
+ * same background service.
14303
+ */
14304
+ inflight = /* @__PURE__ */ new Map();
14247
14305
  registry;
14248
14306
  execImpl;
14249
14307
  fetchImpl;
14250
14308
  appServerClient;
14251
14309
  readFileImpl;
14252
- constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl) {
14310
+ now;
14311
+ constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl, now = Date.now) {
14253
14312
  this.registry = registry;
14254
14313
  this.execImpl = execImpl;
14255
14314
  this.fetchImpl = fetchImpl;
14256
14315
  this.appServerClient = appServerClient;
14316
+ this.now = now;
14257
14317
  this.readFileImpl = readFileImpl ?? ((filePath) => {
14258
14318
  try {
14259
14319
  return fsSync3.readFileSync(filePath.replace(/^~/, osMod.homedir()), "utf8");
@@ -14297,10 +14357,20 @@ var ModelDiscovery = class {
14297
14357
  const runnerLabel = manifest.displayName || runner;
14298
14358
  return models.map((m) => ({ ...m, runnerId: runner, runnerLabel }));
14299
14359
  }
14300
- async discoverRaw(runner) {
14301
- if (this.cache.has(runner)) {
14302
- return this.cache.get(runner);
14303
- }
14360
+ discoverRaw(runner) {
14361
+ const cached2 = this.cache.get(runner);
14362
+ if (cached2) return Promise.resolve(cached2);
14363
+ const remembered = this.misses.get(runner);
14364
+ if (remembered && this.now() - remembered.at < NEGATIVE_TTL_MS) return Promise.resolve(remembered.models);
14365
+ const running = this.inflight.get(runner);
14366
+ if (running) return running;
14367
+ const started = this.discoverUncached(runner).finally(() => {
14368
+ if (this.inflight.get(runner) === started) this.inflight.delete(runner);
14369
+ });
14370
+ this.inflight.set(runner, started);
14371
+ return started;
14372
+ }
14373
+ async discoverUncached(runner) {
14304
14374
  const manifest = this.registry.getManifest(runner);
14305
14375
  if (!manifest) {
14306
14376
  return [];
@@ -14332,10 +14402,13 @@ var ModelDiscovery = class {
14332
14402
  models = buildFallbackModels(manifest);
14333
14403
  }
14334
14404
  if (cacheable && models.length > 0) this.cache.set(runner, models);
14405
+ else this.misses.set(runner, { models, at: this.now() });
14335
14406
  return models;
14336
14407
  }
14337
14408
  clear() {
14338
14409
  this.cache.clear();
14410
+ this.misses.clear();
14411
+ this.inflight.clear();
14339
14412
  }
14340
14413
  /** Whatever `discover()` has already cached for `runner` — never triggers discovery. */
14341
14414
  getCached(runner) {
@@ -25021,15 +25094,15 @@ var ModelResolver = class {
25021
25094
  return toOrchestratorOptions({}, ORCHESTRATOR_SHORTCUTS);
25022
25095
  }
25023
25096
  async modelsForRunners(runners) {
25024
- const out = {};
25025
- for (const runner of runners) {
25097
+ const unique = [...new Set(runners)];
25098
+ const discovered = await Promise.all(unique.map(async (runner) => {
25026
25099
  try {
25027
- out[runner] = await this.discovery.discover(runner);
25100
+ return [runner, await this.discovery.discover(runner)];
25028
25101
  } catch {
25029
- out[runner] = [];
25102
+ return [runner, []];
25030
25103
  }
25031
- }
25032
- return out;
25104
+ }));
25105
+ return Object.fromEntries(discovered);
25033
25106
  }
25034
25107
  async pickerOptions() {
25035
25108
  if (this.cachedPickerOptions) return this.cachedPickerOptions;
@@ -25116,24 +25189,28 @@ var defaultExec3 = async (command, options) => {
25116
25189
  return { stdout: String(stdout) };
25117
25190
  };
25118
25191
  var CHECK_TIMEOUT_MS = 8e3;
25192
+ var NEGATIVE_TTL_MS2 = 6e4;
25119
25193
  var RunnerInstallation = class {
25120
25194
  cache = /* @__PURE__ */ new Map();
25121
25195
  inflight = /* @__PURE__ */ new Map();
25122
25196
  registry;
25123
25197
  execImpl;
25124
- constructor(registry, execImpl = defaultExec3) {
25198
+ now;
25199
+ constructor(registry, execImpl = defaultExec3, now = Date.now) {
25125
25200
  this.registry = registry;
25126
25201
  this.execImpl = execImpl;
25202
+ this.now = now;
25127
25203
  }
25128
25204
  /** True if the runner's CLI responds to `--version`. Cached per process. */
25129
25205
  async isInstalled(runner) {
25130
- if (this.cache.has(runner)) return this.cache.get(runner);
25206
+ const cached2 = this.cache.get(runner);
25207
+ if (cached2 && (cached2.installed || this.now() - cached2.at < NEGATIVE_TTL_MS2)) return cached2.installed;
25131
25208
  const existing = this.inflight.get(runner);
25132
25209
  if (existing) return existing;
25133
25210
  const manifest = this.registry.getManifest(runner);
25134
25211
  const command = manifest?.runner.command;
25135
25212
  if (!command) {
25136
- this.cache.set(runner, false);
25213
+ this.cache.set(runner, { installed: false, at: this.now() });
25137
25214
  return false;
25138
25215
  }
25139
25216
  const probe = (async () => {
@@ -25144,13 +25221,22 @@ var RunnerInstallation = class {
25144
25221
  return false;
25145
25222
  }
25146
25223
  })().then((installed) => {
25147
- this.cache.set(runner, installed);
25224
+ this.cache.set(runner, { installed, at: this.now() });
25148
25225
  this.inflight.delete(runner);
25149
25226
  return installed;
25150
25227
  });
25151
25228
  this.inflight.set(runner, probe);
25152
25229
  return probe;
25153
25230
  }
25231
+ /**
25232
+ * Forget cached "not installed" verdicts so the next probe re-runs them. A
25233
+ * runner-list refresh calls this, so a CLI installed while the host runs is
25234
+ * offered without a restart. An installed verdict stays: it cannot have
25235
+ * changed without the binary going away, and re-probing one costs a spawn.
25236
+ */
25237
+ refresh() {
25238
+ for (const [runner, probe] of this.cache) if (!probe.installed) this.cache.delete(runner);
25239
+ }
25154
25240
  /**
25155
25241
  * Whether this runner can serve as a harness planner (ADR-0009, T9), and why
25156
25242
  * not when it cannot. Surfaces call this to grey out an unusable agent in the