@polderlabs/bizar 10.19.2 → 10.19.3

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.
@@ -592,6 +592,109 @@ export function applyModelOverrides({ settingsJsonPath, pickedIds, liveIds = []
592
592
  return { wrote: true, syncedIds: synced, skippedStale: skipped, settingsPath: path };
593
593
  }
594
594
 
595
+ /**
596
+ * Derive a human-readable label for a model ID. Used to populate the
597
+ * `modelPicker` array in settings.json so Claude Code's `/model` picker
598
+ * displays picked models with something nicer than the raw `provider/name`
599
+ * string.
600
+ *
601
+ * minimax/MiniMax-M3 → "MiniMax M3"
602
+ * codex/gpt-5.6-sol → "GPT 5.6 Sol"
603
+ * qct/qwen3.8-max-preview → "Qwen3.8 Max Preview"
604
+ * openrouter/nvidia/foo:free → "nvidia/foo:free"
605
+ *
606
+ * The gateway-reported `name` (when available on the picked profile) always
607
+ * wins. Falls back to a title-cased rendering of the model segment.
608
+ *
609
+ * @param {string} modelId
610
+ * @param {object} [profile] Optional profile with `name` or `displayName`
611
+ * @returns {string}
612
+ */
613
+ export function deriveModelLabel(modelId, profile) {
614
+ const name = profile && typeof profile.name === 'string' && profile.name.trim();
615
+ if (name) return name.trim();
616
+ const displayName = profile && typeof profile.displayName === 'string' && profile.displayName.trim();
617
+ if (displayName) return displayName.trim();
618
+ const id = String(modelId || '').trim();
619
+ if (!id) return '';
620
+ // Drop the leading provider segment (`minimax/MiniMax-M3` → `MiniMax-M3`)
621
+ // so the operator sees the model name, not the namespace.
622
+ const slash = id.indexOf('/');
623
+ const tail = slash >= 0 ? id.slice(slash + 1) : id;
624
+ // Split on word boundaries (hyphens / underscores / dots / colons / path
625
+ // separators) and join with spaces. Case is preserved verbatim — `gpt`
626
+ // stays `gpt`, `MiniMax` stays `MiniMax`, `M2.7` stays `M2.7`. Brand
627
+ // casing belongs to the gateway (`name` field), not us.
628
+ return tail
629
+ .replace(/[\\/]+/g, ' ')
630
+ .replace(/[-_]+/g, ' ')
631
+ .replace(/:/g, ' ')
632
+ .replace(/\s+/g, ' ')
633
+ .trim();
634
+ }
635
+
636
+ /**
637
+ * Sync `userSelected.models` into Claude Code's `modelPicker` array
638
+ * (settings.json). The picker is what populates `/model` — `modelOverrides`
639
+ * alone only silences diagnostics, it does NOT add entries to the picker.
640
+ *
641
+ * Per Claude Code's settings reference: `modelPicker` is an array of
642
+ * `{ id, label }` entries that replaces the gateway-discovered picker
643
+ * contents. Scope is User-or-managed (settings.json is in scope). Each
644
+ * entry preserves the operator's pick order from `userSelected.models`.
645
+ *
646
+ * Behavior:
647
+ * - Reads settings.json; preserves every other field (env, mcpServers,
648
+ * permissions, hooks, etc.).
649
+ * - Writes `modelPicker` as an array of `{ id, label }`.
650
+ * - Filters out picks the live gateway rejects (same stale-ID contract as
651
+ * `applyModelOverrides`); the surviving picks fill the picker.
652
+ * - Empty pick list → writes `modelPicker: []` so the picker falls back
653
+ * to whatever Claude Code's defaults surface.
654
+ * - Atomic replace via temp-file + rename (matches `applyModels`).
655
+ * - Refuses to overwrite a corrupt settings.json.
656
+ * - When `settingsJsonPath === null`, returns a no-op (tests).
657
+ *
658
+ * @param {{
659
+ * settingsJsonPath?: string|null,
660
+ * pickedIds: string[],
661
+ * profiles?: Record<string, object>,
662
+ * liveIds?: string[],
663
+ * }} opts
664
+ * @returns {{
665
+ * wrote: boolean,
666
+ * entries: Array<{id: string, label: string}>,
667
+ * skippedStale: string[],
668
+ * settingsPath: string|null,
669
+ * }}
670
+ */
671
+ export function applyModelPicker({ settingsJsonPath, pickedIds, profiles = {}, liveIds = [] }) {
672
+ const path = settingsJsonPath === undefined
673
+ ? join(homedir(), '.claude', 'settings.json')
674
+ : settingsJsonPath;
675
+ if (path === null) {
676
+ return { wrote: false, entries: [], skippedStale: [], settingsPath: null };
677
+ }
678
+ const live = new Set(Array.isArray(liveIds) ? liveIds : []);
679
+ const picks = Array.isArray(pickedIds) ? pickedIds.filter((id) => typeof id === 'string' && id.trim()) : [];
680
+ const surviving = live.size === 0 ? picks : picks.filter((id) => live.has(id));
681
+ const skipped = live.size === 0 ? [] : picks.filter((id) => !live.has(id));
682
+ const entries = surviving.map((id) => ({ id, label: deriveModelLabel(id, profiles?.[id]) }));
683
+
684
+ let settings = {};
685
+ if (existsSync(path)) {
686
+ try {
687
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
688
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) settings = parsed;
689
+ } catch {
690
+ return { wrote: false, entries: [], skippedStale: skipped, settingsPath: path };
691
+ }
692
+ }
693
+ settings.modelPicker = entries;
694
+ writeAtomic(path, JSON.stringify(settings, null, 2) + '\n');
695
+ return { wrote: true, entries, skippedStale: skipped, settingsPath: path };
696
+ }
697
+
595
698
  // ── F-190 / IMP-017 refresh + operator-override preservation ────────────────
596
699
 
597
700
  /**
@@ -1409,8 +1512,9 @@ export async function run(name, args, isHelpRequest) {
1409
1512
  // `[claude-code:unrecognized_model]` for any ID the gateway later
1410
1513
  // rejects; the operator can re-run `bizar models` to drop them.
1411
1514
  const sync = applyModelOverrides({ pickedIds: ids, liveIds: [] });
1515
+ const picker = applyModelPicker({ pickedIds: ids, profiles: block.profiles || {}, liveIds: [] });
1412
1516
  if (wantJson) {
1413
- process.stdout.write(JSON.stringify({ applied: block, sync }, null, 2) + '\n');
1517
+ process.stdout.write(JSON.stringify({ applied: block, sync, picker }, null, 2) + '\n');
1414
1518
  } else {
1415
1519
  console.log(chalk.green(` v ${block.models.length} model(s) saved to userSelected`));
1416
1520
  for (const id of block.models) {
@@ -1504,9 +1608,10 @@ export async function run(name, args, isHelpRequest) {
1504
1608
  const picked = await pickModels({ candidates, current });
1505
1609
  if (picked.length === 0) {
1506
1610
  const block = applyModels({ routerPath, models: [], source: 'live-pick' });
1507
- // Clear Claude Code modelOverrides when the picker is emptied so the
1508
- // session no longer claims to recognise removed IDs.
1611
+ // Clear Claude Code modelOverrides + modelPicker when the picker is
1612
+ // emptied so the session no longer claims to recognise removed IDs.
1509
1613
  applyModelOverrides({ pickedIds: [], liveIds: [] });
1614
+ applyModelPicker({ pickedIds: [], liveIds: [] });
1510
1615
  if (wantJson) {
1511
1616
  process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource }, null, 2) + '\n');
1512
1617
  } else {
@@ -1533,8 +1638,11 @@ export async function run(name, args, isHelpRequest) {
1533
1638
  block.staleIds = partition.staleIds;
1534
1639
  }
1535
1640
  const sync = applyModelOverrides({ pickedIds: picked, liveIds });
1641
+ // Sync the /model picker contents (`modelPicker` setting) so the user's
1642
+ // picks drive the picker without relying on gateway discovery.
1643
+ const picker = applyModelPicker({ pickedIds: picked, profiles, liveIds });
1536
1644
  if (wantJson) {
1537
- process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource, sync }, null, 2) + '\n');
1645
+ process.stdout.write(JSON.stringify({ applied: block, endpoint, endpointSource, sync, picker }, null, 2) + '\n');
1538
1646
  } else {
1539
1647
  console.log(chalk.green(`\n v Saved ${block.models.length} model(s) to ${routerPath}:`));
1540
1648
  console.log(chalk.dim(` Models.dev profiles: ${Object.keys(block.profiles || {}).length}/${block.models.length}`));
@@ -1545,6 +1653,9 @@ export async function run(name, args, isHelpRequest) {
1545
1653
  if (sync.wrote && sync.skippedStale.length > 0) {
1546
1654
  console.log(chalk.yellow(` Settings sync skipped ${sync.skippedStale.length} stale id(s): ${sync.skippedStale.join(', ')}`));
1547
1655
  }
1656
+ if (picker.wrote) {
1657
+ console.log(chalk.dim(` /model picker populated with ${picker.entries.length} entr${picker.entries.length === 1 ? 'y' : 'ies'}`));
1658
+ }
1548
1659
  }
1549
1660
  return true;
1550
1661
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar",
3
- "version": "10.19.2",
3
+ "version": "10.19.3",
4
4
  "description": "Autonomous, human-in-the-loop multi-agent harness for Claude Code with guarded workflows, typed SDK primitives, and MCP tools.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * SDK version constant. Keep synchronized with the workspace package versions.
3
3
  */
4
- export declare const SDK_VERSION: "10.19.2";
4
+ export declare const SDK_VERSION: "10.19.3";
5
5
  //# sourceMappingURL=version.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * SDK version constant. Keep synchronized with the workspace package versions.
3
3
  */
4
- export const SDK_VERSION = "10.19.2";
4
+ export const SDK_VERSION = "10.19.3";
5
5
  //# sourceMappingURL=version.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@polderlabs/bizar-sdk",
3
- "version": "10.19.2",
3
+ "version": "10.19.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",