sneakoscope 10.7.1 → 10.7.2

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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/dist/cli/command-manifest-lite.js +1 -1
  5. package/dist/cli/install-helpers-codex-lb-config.js +44 -12
  6. package/dist/commands/bridge.js +14 -1
  7. package/dist/commands/codex.js +5 -1
  8. package/dist/commands/doctor-console.js +2 -0
  9. package/dist/commands/doctor.js +8 -9
  10. package/dist/config/skills-manifest.json +1 -1
  11. package/dist/core/codex-app/menubar/constants.js +0 -1
  12. package/dist/core/codex-lb/combined-catalog/persistence.js +4 -0
  13. package/dist/core/codex-lb/combined-catalog/storage.js +4 -1
  14. package/dist/core/codex-lb/desktop-bridge/http-forward.js +48 -8
  15. package/dist/core/codex-lb/desktop-bridge/models-endpoint.js +62 -0
  16. package/dist/core/codex-lb/desktop-bridge/provider-handoff.js +13 -4
  17. package/dist/core/codex-lb/desktop-bridge/provider-request-compat.js +89 -0
  18. package/dist/core/codex-lb/desktop-bridge/rejection-log.js +61 -0
  19. package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +52 -4
  20. package/dist/core/codex-lb/desktop-bridge/server.js +5 -0
  21. package/dist/core/codex-lb/desktop-bridge-migration/historical-config.js +2 -6
  22. package/dist/core/codex-lb/desktop-bridge-migration.js +14 -2
  23. package/dist/core/codex-lb/desktop-controller-v3/catalog.js +114 -5
  24. package/dist/core/codex-lb/desktop-controller-v3/model-refresh.js +140 -0
  25. package/dist/core/codex-lb/desktop-controller-v3/openrouter-only-commands.js +5 -5
  26. package/dist/core/codex-lb/desktop-controller-v3/provider-commands.js +14 -2
  27. package/dist/core/codex-lb/desktop-controller-v3/status.js +2 -3
  28. package/dist/core/codex-lb/desktop-controller-v3.js +3 -1
  29. package/dist/core/codex-lb/desktop-service.js +41 -4
  30. package/dist/core/codex-lb/model-refresh-after.js +16 -0
  31. package/dist/core/commands/basic-cli.js +5 -0
  32. package/dist/core/commands/menubar-command.js +5 -1
  33. package/dist/core/doctor/desktop-bridge-catalog-repair.js +20 -17
  34. package/dist/core/subagents/model-tiers.js +19 -3
  35. package/dist/core/update/update-migration-state/desktop-bridge-catalog-repair-stage.js +4 -3
  36. package/dist/core/version.js +1 -1
  37. package/dist/native/sks-menubar/Sources/ControlCenterWindowController.swift +0 -1
  38. package/dist/native/sks-menubar/Sources/OverviewSummary.swift +32 -0
  39. package/dist/native/sks-menubar/Sources/OverviewViewController.swift +15 -5
  40. package/dist/native/sks-menubar/Sources/SidebarItem.swift +0 -2
  41. package/dist/native/sks-menubar/Sources/UpdatesViewController.swift +3 -0
  42. package/package.json +1 -1
  43. package/dist/native/sks-menubar/Sources/RemoteCodingViewController.swift +0 -49
@@ -5,6 +5,14 @@ import { configureProviderCredential, recordProviderCredentialValidation, remove
5
5
  import { configureBridgeProviderProfile, resolveBridgeProviderRegistry, setBridgeProviderEnabled } from '../provider-registry.js';
6
6
  import { commandResult, activeProviderIds, controllerEnv, controllerPaths, nowIso, persistRuntimeSettings, providerCode, quiesceRunningBridge, resolveRawCredentials, stringArray, timeoutMs } from './shared.js';
7
7
  import { desktopBridgeStatusV3, loadCore } from './status.js';
8
+ import { refreshModelCatalog } from './catalog.js';
9
+ async function refreshAfterProviderChange(reason, options) {
10
+ const core = await loadCore(options);
11
+ if (!core.activeCatalog.ok || !core.policy)
12
+ return null;
13
+ const { sync: _sync, ...summary } = await refreshModelCatalog(options, { reason, force: true, allowConfigWrites: true });
14
+ return summary;
15
+ }
8
16
  export async function configureProvider(request, options) {
9
17
  const paths = controllerPaths(options);
10
18
  const restartDeferred = await quiesceRunningBridge(await loadCore(options), options);
@@ -34,12 +42,14 @@ export async function configureProvider(request, options) {
34
42
  if (core.activeCatalog.ok && core.policy) {
35
43
  await persistRuntimeSettings({ ...core, registry }, options, { restartService: false });
36
44
  }
45
+ const modelRefresh = restartDeferred ? null : await refreshAfterProviderChange('provider.configure', options);
37
46
  const status = await desktopBridgeStatusV3(options);
38
47
  const blockers = restartDeferred ? ['desktop_bridge_restart_deferred_until_provider_validation'] : [];
39
48
  return commandResult('provider.configure', true, status, {
40
49
  configuration: configured,
41
50
  restart_deferred: restartDeferred,
42
51
  recovery_action: restartDeferred ? `bridge provider validate ${request.provider_id}` : null,
52
+ model_refresh: modelRefresh,
43
53
  }, blockers, options);
44
54
  }
45
55
  export async function validateProvider(providerId, options) {
@@ -82,8 +92,9 @@ export async function validateProvider(providerId, options) {
82
92
  });
83
93
  }
84
94
  }
95
+ const modelRefresh = ok ? await refreshAfterProviderChange('provider.validate', options) : null;
85
96
  const status = await desktopBridgeStatusV3(options);
86
- return commandResult('provider.validate', true, status, { provider_id: providerId, validated: ok, validation: publicValidationResult(result) }, ok ? [] : blockers, options);
97
+ return commandResult('provider.validate', true, status, { provider_id: providerId, validated: ok, validation: publicValidationResult(result), model_refresh: modelRefresh }, ok ? [] : blockers, options);
87
98
  }
88
99
  export async function setProviderState(providerId, enabled, operation, options) {
89
100
  const paths = controllerPaths(options);
@@ -108,8 +119,9 @@ export async function setProviderState(providerId, enabled, operation, options)
108
119
  });
109
120
  }
110
121
  }
122
+ const modelRefresh = await refreshAfterProviderChange(operation, options);
111
123
  const status = await desktopBridgeStatusV3(options);
112
- return commandResult(operation, true, status, { provider_id: providerId, enabled }, [], options);
124
+ return commandResult(operation, true, status, { provider_id: providerId, enabled, model_refresh: modelRefresh }, [], options);
113
125
  }
114
126
  export async function removeCredential(providerId, options) {
115
127
  const paths = controllerPaths(options);
@@ -1,4 +1,4 @@
1
- import { DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, DESKTOP_BRIDGE_MANAGED_MARKER, DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER } from '../../../cli/install-helpers-codex-lb-config.js';
1
+ import { DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER, DESKTOP_BRIDGE_MANAGED_MARKER } from '../../../cli/install-helpers-codex-lb-config.js';
2
2
  import { readActiveCombinedBridgeCatalog } from '../combined-catalog.js';
3
3
  import { desktopBridgeServiceStatus, desktopBridgeServicePaths, readDesktopBridgeServiceSettings } from '../desktop-service.js';
4
4
  import { captureCodexAuthSnapshot } from '../desktop-auth-invariant.js';
@@ -276,8 +276,7 @@ function isManagedConfig(config) {
276
276
  const top = String(config || '').split(/\n\s*\[/)[0] || '';
277
277
  return [
278
278
  DESKTOP_BRIDGE_MANAGED_MARKER,
279
- DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER,
280
- DESKTOP_BRIDGE_MANAGED_MODEL_CATALOG_MARKER
279
+ DESKTOP_BRIDGE_MANAGED_BASE_URL_MARKER
281
280
  ].every((marker) => top.split(/\r?\n/).some((line) => line.trim() === marker))
282
281
  && /^\s*model_provider\s*=\s*"openai"\s*(?:#.*)?$/m.test(top);
283
282
  }
@@ -1,4 +1,4 @@
1
- import { listSelectableModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
1
+ import { listSelectableModels, refreshModels, selectExposedModels, syncCatalog } from './desktop-controller-v3/catalog.js';
2
2
  import { explainRoute, ensureDesktopBridge, repairDesktopBridge, rollbackDesktopBridge, setDefaultProvider, setOfficialModelsMode, setAuthPriority, unmanageDesktopBridge } from './desktop-controller-v3/lifecycle-commands.js';
3
3
  import { configureProvider, removeCredential, setProviderState, validateProvider } from './desktop-controller-v3/provider-commands.js';
4
4
  import { listSubagentModels, openRouterOnlyStatusCommand, setOpenRouterOnly, setSubagentModels } from './desktop-controller-v3/openrouter-only-commands.js';
@@ -55,6 +55,8 @@ export async function executeDesktopBridgeCommandV3(request, options = {}) {
55
55
  return listSelectableModels(options);
56
56
  if (request.operation === 'models.select')
57
57
  return selectExposedModels(request.public_ids, options);
58
+ if (request.operation === 'models.refresh')
59
+ return refreshModels(request, options);
58
60
  if (request.operation === 'catalog.sync')
59
61
  return syncCatalog(options);
60
62
  if (request.operation === 'catalog.status') {
@@ -13,6 +13,8 @@ import { canonicalizeBridgeModelId, normalizeBridgeUpstreamModelId, sha256Stable
13
13
  import { captureCodexAuthSnapshot } from './desktop-auth-invariant.js';
14
14
  import { applyOfficialModelPassthrough, bridgeRoutePolicyPath, writeBridgeRoutingPolicy } from './provider-route-policy.js';
15
15
  import { desktopBridgeRuntimeVersion, desktopBridgeRuntimeVersionStale } from './desktop-bridge/state.js';
16
+ import { REQUEST_COMPAT_INCLUDE_VALUE, REQUEST_COMPAT_MAX_EXTRA_INCLUDE } from './desktop-bridge/provider-request-compat.js';
17
+ import { bridgeRouteIndexPath, combinedBridgeCatalogPath, readActiveCombinedBridgeCatalog } from './combined-catalog.js';
16
18
  import { PACKAGE_VERSION } from '../version.js';
17
19
  import { canonicalChildModelId, readOpenRouterOnlyStateSync } from '../subagents/child-model-allowlist.js';
18
20
  import { BRIDGE_OFFICIAL_ROUTE_ID } from './bridge-contracts.js';
@@ -133,7 +135,7 @@ export async function resolveDesktopBridgeActivationSettings(options = {}) {
133
135
  const merged = { ...(persisted || {}), ...(options.settings || {}) };
134
136
  const host = merged.listen_host || DEFAULT_DESKTOP_BRIDGE_HOST;
135
137
  const port = merged.listen_port ?? await (options.selectAvailablePort || selectAvailableDesktopBridgePort)(host);
136
- return defaultDesktopBridgeServiceSettings({ ...merged, ...(options.providerRegistry ? { provider_registry: options.providerRegistry } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}), client_capability_sha256: sha256(clientCapability), listen_host: host, listen_port: port });
138
+ return defaultDesktopBridgeServiceSettings({ ...merged, ...(options.providerRegistry ? { provider_registry: withPersistedRequestCompat(options.providerRegistry, persisted?.provider_registry) } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}), client_capability_sha256: sha256(clientCapability), listen_host: host, listen_port: port });
137
139
  }
138
140
  export async function readDesktopBridgeServiceSettings(file) {
139
141
  let stat;
@@ -297,7 +299,8 @@ function normalizeEndpointForBinding(value) {
297
299
  export async function resolveDesktopBridgeRuntimeConfig(options = {}) {
298
300
  const home = options.home || options.env?.HOME || process.env.HOME || os.homedir();
299
301
  const paths = overridePaths(desktopBridgeServicePaths(home), options);
300
- let settings = defaultDesktopBridgeServiceSettings({ ...((await readDesktopBridgeServiceSettings(paths.settings_path)) || {}), ...(options.settings || {}), ...(options.providerRegistry ? { provider_registry: options.providerRegistry } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}) });
302
+ const persisted = await readDesktopBridgeServiceSettings(paths.settings_path);
303
+ let settings = defaultDesktopBridgeServiceSettings({ ...(persisted || {}), ...(options.settings || {}), ...(options.providerRegistry ? { provider_registry: withPersistedRequestCompat(options.providerRegistry, persisted?.provider_registry) } : {}), ...(options.routePolicy ? { route_policy: options.routePolicy } : {}), ...(options.providerSessionPins ? { provider_session_pins: [...options.providerSessionPins] } : {}) });
301
304
  const clientCapability = options.clientCapability
302
305
  ? validateClientCapability(options.clientCapability)
303
306
  : await readDesktopBridgeClientCapability(paths.client_capability_path);
@@ -551,12 +554,20 @@ async function autoApplyOfficialModelsAtServe(runtime, home, options) {
551
554
  runtime.config.routePolicy = converged;
552
555
  process.stdout.write(`${JSON.stringify({ schema: 'sks.desktop-bridge-log.v2', event: 'sks.desktop_bridge.official_models_auto_applied', at: new Date().toISOString(), sks_version: PACKAGE_VERSION, mode, policy_generation: converged.policy_generation, secret_fields_redacted: true })}\n`);
553
556
  }
557
+ async function activeModelsCatalog(home) {
558
+ const codexHome = path.join(path.resolve(home), '.codex');
559
+ const active = await readActiveCombinedBridgeCatalog(combinedBridgeCatalogPath(codexHome), bridgeRouteIndexPath(codexHome)).catch(() => null);
560
+ if (!active?.ok || active.catalog.models.length === 0)
561
+ return null;
562
+ return { generation: active.catalog.generation, models: active.catalog.models };
563
+ }
554
564
  export async function serveDesktopBridge(options = {}) {
555
565
  let handle = null;
556
566
  try {
557
567
  const runtime = await resolveDesktopBridgeRuntimeConfig(options);
558
568
  const serveHome = options.home || options.env?.HOME || process.env.HOME || os.homedir();
559
569
  await autoApplyOfficialModelsAtServe(runtime, serveHome, options);
570
+ runtime.config.modelsCatalog = await activeModelsCatalog(serveHome);
560
571
  const supervised = desktopBridgeIsSupervised();
561
572
  const skewMarkerPath = path.join(path.dirname(runtime.paths.state_path), 'desktop-bridge-skew-restart.json');
562
573
  const prepared = await preflightDesktopBridge(runtime.config);
@@ -778,11 +789,37 @@ function normalizeProviderRegistrySnapshot(value) {
778
789
  const providers = Object.fromEntries(['codex-lb', 'openrouter'].map((providerId) => {
779
790
  const provider = registry.providers[providerId];
780
791
  const sourceCatalogGeneration = provider.source_catalog_generation ?? provider.catalog_generation ?? null;
781
- const { catalog_generation: _legacyCatalogGeneration, ...rest } = provider;
782
- return [providerId, { ...rest, source_catalog_generation: sourceCatalogGeneration }];
792
+ const { catalog_generation: _legacyCatalogGeneration, request_compat: requestCompat, ...rest } = provider;
793
+ const normalized = { ...rest, source_catalog_generation: sourceCatalogGeneration };
794
+ if (requestCompat !== undefined)
795
+ normalized.request_compat = validateRequestCompat(requestCompat);
796
+ return [providerId, normalized];
783
797
  }));
784
798
  return { ...registry, providers };
785
799
  }
800
+ function validateRequestCompat(value) {
801
+ if (!value || typeof value !== 'object' || Array.isArray(value))
802
+ throw new Error('desktop_bridge_settings_request_compat_invalid');
803
+ const row = value;
804
+ if (Object.keys(row).some((key) => key !== 'extra_include'))
805
+ throw new Error('desktop_bridge_settings_request_compat_invalid');
806
+ const values = row.extra_include ?? [];
807
+ if (!Array.isArray(values) || values.length > REQUEST_COMPAT_MAX_EXTRA_INCLUDE
808
+ || values.some((entry) => typeof entry !== 'string' || !REQUEST_COMPAT_INCLUDE_VALUE.test(entry))) {
809
+ throw new Error('desktop_bridge_settings_request_compat_invalid');
810
+ }
811
+ return { extra_include: [...new Set(values)] };
812
+ }
813
+ function withPersistedRequestCompat(registry, persisted) {
814
+ let providers = null;
815
+ for (const id of ['codex-lb', 'openrouter']) {
816
+ const compat = persisted?.providers?.[id]?.request_compat;
817
+ if (!compat || registry.providers[id].request_compat)
818
+ continue;
819
+ providers = { ...(providers ?? registry.providers), [id]: { ...registry.providers[id], request_compat: compat } };
820
+ }
821
+ return providers ? { ...registry, generation: sha256Stable(providers), providers } : registry;
822
+ }
786
823
  function overridePaths(base, options) { return { settings_path: options.settingsPath || base.settings_path, state_path: options.statePath || base.state_path, client_capability_path: options.clientCapabilityPath || base.client_capability_path, launch_agent_path: options.launchAgentPath || base.launch_agent_path, stdout_log_path: options.stdoutLogPath || base.stdout_log_path, stderr_log_path: options.stderrLogPath || base.stderr_log_path }; }
787
824
  function launchDomain(uid = typeof process.getuid === 'function' ? process.getuid() : 0) { return `gui/${uid}`; }
788
825
  function launchService(uid) { return `${launchDomain(uid)}/${DESKTOP_BRIDGE_LAUNCHD_LABEL}`; }
@@ -0,0 +1,16 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { exists } from '../fsx.js';
4
+ import { desktopBridgeServicePaths } from './desktop-service.js';
5
+ export async function refreshModelListAfter(reason) {
6
+ if (process.platform !== 'darwin')
7
+ return null;
8
+ if (process.env.NODE_TEST_CONTEXT !== undefined || process.env.SKS_TEST_ISOLATION === '1' || process.env.SKS_RELEASE_UPGRADE_SMOKE === '1')
9
+ return null;
10
+ const home = path.resolve(process.env.HOME || os.homedir());
11
+ if (!(await exists(desktopBridgeServicePaths(home).settings_path)))
12
+ return null;
13
+ const { executeDesktopBridgeCommandV3 } = await import('./desktop-controller-v3.js');
14
+ const command = await executeDesktopBridgeCommandV3({ operation: 'models.refresh', reason, force: false, allow_config_writes: true }, { home, env: process.env }).catch(() => null);
15
+ return command?.result?.model_refresh ?? null;
16
+ }
@@ -330,6 +330,9 @@ export async function setupCommand(args = []) {
330
330
  ...[...(codexHooks?.activation?.blockers || []), ...(codexHooks?.project?.blockers || [])].map((blocker) => `codex_hooks:${blocker}`)
331
331
  ]
332
332
  };
333
+ const modelRefresh = result.ok ? await (await import('../codex-lb/model-refresh-after.js')).refreshModelListAfter('setup') : null;
334
+ if (modelRefresh)
335
+ result.model_refresh = modelRefresh;
333
336
  if (!result.ok)
334
337
  process.exitCode = 1;
335
338
  if (flag(args, '--json')) {
@@ -337,6 +340,8 @@ export async function setupCommand(args = []) {
337
340
  return result;
338
341
  }
339
342
  console.log(`${result.ok ? 'Setup complete' : 'Setup blocked'}: ${root}`);
343
+ if (typeof modelRefresh?.line === 'string')
344
+ console.log(modelRefresh.line);
340
345
  console.log(`Install scope: ${installScope}`);
341
346
  console.log(`Codex CLI: ${cliTools.codex.status}${cliTools.codex.version ? ` ${cliTools.codex.version}` : ''}`);
342
347
  console.log(`SKS Codex hooks: ${codexHooks?.active ? 'active for every Codex project' : installScope === 'project' ? 'this project only' : 'not active (run: sks doctor --fix)'}`);
@@ -31,7 +31,9 @@ export async function menubarCommand(subcommand = 'status', args = []) {
31
31
  return result;
32
32
  }
33
33
  if (action === 'install') {
34
- const result = await installSksMenuBar({ root, ...(home ? { home } : {}), apply: true, launch: !flag(args, '--no-launch'), quiet: flag(args, '--json') });
34
+ const installed = await installSksMenuBar({ root, ...(home ? { home } : {}), apply: true, launch: !flag(args, '--no-launch'), quiet: flag(args, '--json') });
35
+ const modelRefresh = await (await import('../codex-lb/model-refresh-after.js')).refreshModelListAfter('menubar-install');
36
+ const result = modelRefresh ? { ...installed, model_refresh: modelRefresh } : installed;
35
37
  if (flag(args, '--json')) {
36
38
  printJson(result);
37
39
  if (!result.ok)
@@ -46,6 +48,8 @@ export async function menubarCommand(subcommand = 'status', args = []) {
46
48
  console.log(`SKS menu bar install: ${result.status}`);
47
49
  for (const actionLine of result.actions)
48
50
  console.log(`- ${actionLine}`);
51
+ if (typeof modelRefresh?.line === 'string')
52
+ console.log(modelRefresh.line);
49
53
  for (const warning of result.warnings)
50
54
  console.log(`warning: ${warning}`);
51
55
  for (const blocker of result.blockers)
@@ -101,10 +101,8 @@ export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
101
101
  fix: input.fix
102
102
  });
103
103
  const status = await statusImpl({ home: bridgeHome, env: process.env });
104
- const blockers = (status?.readiness?.blockers || []).map(String);
105
- const stale = blockers.filter((blocker) => blocker.endsWith('_catalog_stale'));
106
104
  const reverify = { fix: input.fix, bridgeHome, restarted: restarted.restarted };
107
- if (!status?.management?.managed || stale.length === 0) {
105
+ if (!status?.management?.managed) {
108
106
  return reverifyTransportIfDegraded({
109
107
  ...base,
110
108
  ok: restarted.blockers.length === 0,
@@ -114,28 +112,33 @@ export async function repairDoctorDesktopBridgeCatalog(input, deps = {}) {
114
112
  }, reverify, statusImpl, executeImpl);
115
113
  }
116
114
  const attempts = [];
117
- let syncedAndVerified = false;
115
+ let refresh = null;
116
+ let stale = [];
118
117
  for (let attempt = 1; attempt <= CATALOG_REPAIR_MAX_ATTEMPTS; attempt += 1) {
119
- const sync = await executeImpl({ operation: 'catalog.sync' }, { home: bridgeHome, env: process.env });
120
- const commandOk = sync?.ok === true && sync?.execution?.ok === true;
118
+ const command = await executeImpl({
119
+ operation: 'models.refresh',
120
+ reason: input.reason || (input.fix ? 'doctor-fix' : 'doctor'),
121
+ force: false,
122
+ allow_config_writes: input.fix
123
+ }, { home: bridgeHome, env: process.env });
124
+ refresh = command?.result?.model_refresh && typeof command.result.model_refresh === 'object' ? command.result.model_refresh : null;
121
125
  const after = await statusImpl({ home: bridgeHome, env: process.env });
122
- const stillStale = (after?.readiness?.blockers || [])
123
- .map(String).filter((blocker) => blocker.endsWith('_catalog_stale'));
124
- if (commandOk && stillStale.length === 0) {
125
- syncedAndVerified = true;
126
+ stale = (after?.readiness?.blockers || []).map(String).filter((blocker) => blocker.endsWith('_catalog_stale'));
127
+ if (refresh?.ok === true && stale.length === 0)
126
128
  break;
127
- }
128
- attempts.push(`catalog_sync_attempt_${attempt}:${commandOk ? `still_stale:${stillStale.join(',')}` : (sync?.execution?.blockers || ['failed']).map(String).join(',')}`);
129
+ attempts.push(`model_refresh_attempt_${attempt}:${refresh?.ok === true ? `still_stale:${stale.join(',')}` : (refresh?.blockers || command?.execution?.blockers || ['failed']).map(String).join(',')}`);
129
130
  }
131
+ const refreshed = refresh?.ok === true;
130
132
  return reverifyTransportIfDegraded({
131
133
  ...base,
132
- ok: syncedAndVerified && restarted.blockers.length === 0,
133
- repaired: syncedAndVerified || restarted.restarted,
134
- warnings: [...restarted.warnings, ...(syncedAndVerified ? attempts : [])],
134
+ ok: stale.length === 0 && restarted.blockers.length === 0,
135
+ repaired: (refreshed && refresh?.skipped !== true) || restarted.restarted,
136
+ warnings: [...restarted.warnings, ...(refresh?.warnings || []).map(String), ...(stale.length === 0 ? attempts : [])],
135
137
  blockers: [
136
- ...(syncedAndVerified ? [] : ['desktop_bridge_catalog_still_stale_after_repair', ...attempts]),
138
+ ...(stale.length === 0 ? [] : ['desktop_bridge_catalog_still_stale_after_repair', ...attempts]),
137
139
  ...restarted.blockers
138
- ]
140
+ ],
141
+ model_refresh: refresh
139
142
  }, reverify, statusImpl, executeImpl);
140
143
  }
141
144
  catch (error) {
@@ -56,16 +56,32 @@ function readableRows(file) {
56
56
  }
57
57
  }
58
58
  let catalogPathMemo = null;
59
+ const BRIDGE_BASE_URL_MARKER = '# sks-desktop-bridge-managed-base-url';
60
+ function activeBridgeCatalogPath(codexHome) {
61
+ try {
62
+ const pointer = JSON.parse(fs.readFileSync(path.join(codexHome, 'sks', 'sks-bridge-active-generation.json'), 'utf8'));
63
+ const bundle = path.resolve(codexHome, 'sks', String(pointer?.bundle_directory || ''));
64
+ const file = path.join(bundle, String(pointer?.catalog_filename || ''));
65
+ return bundle.startsWith(path.join(codexHome, 'sks', '.sks-bridge-generations') + path.sep) && path.dirname(file) === bundle ? file : null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
59
71
  function configuredCatalogPath(input) {
60
- const configPath = path.join(codexHomePath(input), 'config.toml');
61
- const key = fileKey(configPath);
72
+ const codexHome = codexHomePath(input);
73
+ const configPath = path.join(codexHome, 'config.toml');
74
+ const key = `${fileKey(configPath)}|${fileKey(path.join(codexHome, 'sks', 'sks-bridge-active-generation.json'))}`;
62
75
  if (catalogPathMemo?.key === key)
63
76
  return catalogPathMemo.path;
64
77
  let resolved = null;
65
78
  try {
66
79
  const stat = fs.statSync(configPath);
67
80
  if (stat.isFile() && stat.size <= 4 * 1024 * 1024) {
68
- const value = readTopLevelTomlString(fs.readFileSync(configPath, 'utf8'), 'model_catalog_json')?.trim();
81
+ const config = fs.readFileSync(configPath, 'utf8');
82
+ const value = readTopLevelTomlString(config, 'model_catalog_json')?.trim();
83
+ if (!value && config.split(/\r?\n/).some((line) => line.trim() === BRIDGE_BASE_URL_MARKER))
84
+ resolved = activeBridgeCatalogPath(codexHome);
69
85
  if (value) {
70
86
  const env = input.env || process.env;
71
87
  const home = input.home || env.HOME || os.homedir();
@@ -17,17 +17,18 @@ export async function runDesktopBridgeCatalogRepairStage() {
17
17
  if (!(await exists(desktopBridgeServicePaths(home).settings_path))) {
18
18
  return skip('desktop_bridge_catalog_repair_no_managed_bridge');
19
19
  }
20
- const repair = await repairDoctorDesktopBridgeCatalog({ fix: true }).catch((error) => ({
20
+ const repair = await repairDoctorDesktopBridgeCatalog({ fix: true, reason: 'update' }).catch((error) => ({
21
21
  ok: false,
22
22
  repaired: false,
23
23
  warnings: [],
24
24
  blockers: [`desktop_bridge_catalog_repair_failed:${error?.message || String(error)}`]
25
25
  }));
26
+ const refreshLine = typeof repair.model_refresh?.line === 'string' ? [repair.model_refresh.line] : [];
26
27
  if (repair.ok) {
27
28
  return {
28
29
  ok: true,
29
30
  status: 'ok',
30
- actions: [repair.repaired ? 'desktop_bridge_catalog_repaired' : 'desktop_bridge_catalog_clean'],
31
+ actions: [repair.repaired ? 'desktop_bridge_catalog_repaired' : 'desktop_bridge_catalog_clean', ...refreshLine],
31
32
  blockers: [],
32
33
  warnings: repair.warnings || []
33
34
  };
@@ -35,7 +36,7 @@ export async function runDesktopBridgeCatalogRepairStage() {
35
36
  return {
36
37
  ok: true,
37
38
  status: 'ok',
38
- actions: [],
39
+ actions: refreshLine,
39
40
  blockers: [],
40
41
  warnings: [
41
42
  ...(repair.blockers || []).map((blocker) => `desktop_bridge_catalog_repair_incomplete:${blocker}`),
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = '10.7.1';
1
+ export const PACKAGE_VERSION = '10.7.2';
@@ -33,7 +33,6 @@ final class ControlCenterWindowController: NSWindowController, NSTableViewDataSo
33
33
  .mcpServers: MCPServersViewController(processClient: processClient, operations: operations, notifications: notifications),
34
34
  .providers: ProvidersViewController(processClient: processClient, operations: operations),
35
35
  .subagentModels: SubagentModelsViewController(processClient: processClient, operations: operations),
36
- .remoteCoding: RemoteCodingViewController(),
37
36
  .localDecision: LocalDecisionViewController(processClient: processClient, operations: operations),
38
37
  .imageGeneration: ImageGenerationViewController(processClient: processClient, operations: operations),
39
38
  .diagnostics: DiagnosticsViewController(processClient: processClient, operations: operations),
@@ -134,3 +134,35 @@ enum OverviewSummary {
134
134
  return value
135
135
  }
136
136
  }
137
+
138
+ /// What `sks bridge models refresh --json` reports, as SKS Center shows it.
139
+ /// Success is the payload's own `ok == true` at both levels; a result that only
140
+ /// carries a schema key is not one.
141
+ enum ModelRefreshSummary {
142
+ struct Outcome: Equatable {
143
+ let ok: Bool
144
+ let text: String
145
+ }
146
+
147
+ static func outcome(fromJSONText text: String, at date: Date = Date()) -> Outcome {
148
+ outcome(fromJSON: jsonObject(text), at: date)
149
+ }
150
+
151
+ static func outcome(fromJSON object: [String: Any]?, at date: Date = Date()) -> Outcome {
152
+ let refresh = (object?["result"] as? [String: Any])?["model_refresh"] as? [String: Any]
153
+ guard object?["ok"] as? Bool == true, refresh?["ok"] as? Bool == true else {
154
+ return Outcome(ok: false, text: refresh?["line"] as? String ?? "Model list refresh failed. Run `sks bridge models refresh` to retry.")
155
+ }
156
+ let total = (refresh?["total"] as? NSNumber)?.intValue ?? 0
157
+ let formatter = DateFormatter()
158
+ formatter.dateFormat = "HH:mm"
159
+ let restart = refresh?["restart_codex_required"] as? Bool == true ? " · restart Codex to see it" : ""
160
+ return Outcome(ok: true, text: "Model list updated at \(formatter.string(from: date)) (\(total) models)\(restart)")
161
+ }
162
+
163
+ private static func jsonObject(_ text: String) -> [String: Any]? {
164
+ let start = text.firstIndex(of: "{") ?? text.startIndex
165
+ guard let data = String(text[start...]).data(using: .utf8) else { return nil }
166
+ return (try? JSONSerialization.jsonObject(with: data)) as? [String: Any]
167
+ }
168
+ }
@@ -6,6 +6,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
6
6
  private let status = NativeView.detail("Loading local SKS status…")
7
7
  private let notificationInbox = NativeView.detail("Notifications: checking authorization…")
8
8
  private let recoveryStatus = NativeView.detail("Progress recovery: no operation state loaded yet.")
9
+ private let modelListStatus = NativeView.detail("Model list: refreshed by Refresh, Run Doctor and updates.")
9
10
  private let healthBadge = NativeView.badge("Checking local status", color: .systemBlue)
10
11
  private let statusSpinner = NativeView.spinner(label: "Checking SKS Center status")
11
12
  private let snapshotDetails = NativeView.detail("")
@@ -36,7 +37,6 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
36
37
  updateCodexButton.setAccessibilityHelp("Update the operator Codex CLI to the preferred latest channel.")
37
38
  let buttons = NativeView.row([refreshButton, doctorButton, updateCodexButton])
38
39
  let shortcuts = NativeView.row([
39
- NativeView.button("Remote Coding…", target: self, action: #selector(openRemoteCoding)),
40
40
  connectionsShortcut(),
41
41
  NativeView.button("Decisions…", target: self, action: #selector(openDecisions)),
42
42
  NativeView.button("Updates…", target: self, action: #selector(openUpdates)),
@@ -50,7 +50,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
50
50
  status.setAccessibilityIdentifier("sks-overview-next-action")
51
51
  let healthCard = NativeView.card(
52
52
  title: "System health", subtitle: "",
53
- views: [NativeView.row([healthBadge, statusSpinner]), status, components, buttons]
53
+ views: [NativeView.row([healthBadge, statusSpinner]), status, modelListStatus, components, buttons]
54
54
  )
55
55
  recoveryCard = NativeView.card(
56
56
  title: "Progress, pause & recovery", subtitle: "Review the current operation before retrying.",
@@ -72,7 +72,6 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
72
72
  return button
73
73
  }
74
74
 
75
- @objc private func openRemoteCoding() { openSection?("Remote Coding") }
76
75
  @objc private func openProviders() { openSection?("Providers") }
77
76
  @objc private func openDecisions() { openSection?("Decisions") }
78
77
  @objc private func openUpdates() { openSection?("Updates") }
@@ -113,7 +112,15 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
113
112
  }
114
113
 
115
114
  @objc private func refreshStatus() {
116
- loadStatus(forceUpdateRefresh: true)
115
+ loadStatus(forceUpdateRefresh: true) { [weak self] in self?.refreshModelList() }
116
+ }
117
+
118
+ /// Refresh, Run Doctor and a Codex CLI update each end with a fresh model list.
119
+ private func refreshModelList() {
120
+ modelListStatus.stringValue = "Refreshing the model list…"
121
+ processClient.run(["bridge", "models", "refresh", "--json"], timeout: NativeView.longMutationTimeout) { [weak self] result in
122
+ self?.modelListStatus.stringValue = ModelRefreshSummary.outcome(fromJSONText: result.output).text
123
+ }
117
124
  }
118
125
 
119
126
  @objc private func updateCodexCLI() {
@@ -145,11 +152,12 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
145
152
  self.status.stringValue = ok
146
153
  ? "Codex CLI update completed. Refreshing shared update status…"
147
154
  : "Codex CLI update needs attention. Open Updates or Diagnostics for structured guidance."
155
+ if ok { self.refreshModelList() }
148
156
  self.loadStatus(forceUpdateRefresh: true)
149
157
  }
150
158
  }
151
159
 
152
- private func loadStatus(forceUpdateRefresh: Bool) {
160
+ private func loadStatus(forceUpdateRefresh: Bool, then completion: (() -> Void)? = nil) {
153
161
  generation += 1
154
162
  let requestGeneration = generation
155
163
  refreshButton?.isEnabled = false
@@ -196,6 +204,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
196
204
  self.refreshButton?.isEnabled = true
197
205
  self.statusSpinner.stopAnimation(nil)
198
206
  self.renderStatus(update: update, mcp: mcp, partial: false)
207
+ completion?()
199
208
  }
200
209
  DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak self] in
201
210
  guard let self = self,
@@ -232,6 +241,7 @@ final class OverviewViewController: NSViewController, ControlCenterPage {
232
241
  if result.code == 0 {
233
242
  NativeView.setBadge(self.healthBadge, text: "Doctor completed", color: .systemGreen)
234
243
  self.status.stringValue = "Doctor completed. No blocking issue was reported."
244
+ self.refreshModelList()
235
245
  } else {
236
246
  NativeView.setBadge(self.healthBadge, text: "Doctor needs attention", color: .systemOrange)
237
247
  self.status.stringValue = "Doctor found an issue. Open Diagnostics · \(NativeView.redactPreview(result.output))"
@@ -6,7 +6,6 @@ enum SidebarItem: String, CaseIterable {
6
6
  case overview = "Overview"
7
7
  case providers = "Providers"
8
8
  case subagentModels = "Subagent Models"
9
- case remoteCoding = "Remote Coding"
10
9
  case updates = "Updates"
11
10
  case mcpServers = "MCP Servers"
12
11
  case localDecision = "Decisions"
@@ -25,7 +24,6 @@ enum SidebarItem: String, CaseIterable {
25
24
  case .mcpServers: return "server.rack"
26
25
  case .localDecision: return "cpu"
27
26
  case .imageGeneration: return "photo"
28
- case .remoteCoding: return "laptopcomputer.and.iphone"
29
27
  case .diagnostics: return "stethoscope"
30
28
  case .settings: return "gearshape"
31
29
  }
@@ -231,6 +231,9 @@ final class UpdatesViewController: NSViewController, ControlCenterPage {
231
231
  self.status.stringValue = "Review ready. Confirm to continue, or cancel to leave the current install unchanged."
232
232
  }
233
233
  }
234
+ if kind == "update", authoritativeState == .succeeded { // The update refreshed the model list; show what it holds now.
235
+ self.processClient.run(["bridge", "models", "refresh", "--json"], timeout: NativeView.mutationTimeout) { [weak self] refreshed in self?.status.stringValue = ModelRefreshSummary.outcome(fromJSONText: refreshed.output).text }
236
+ }
234
237
  let updateOutput = kind == "update-status" ? result.output : nil
235
238
  let approvalRequired = kind == "update-review" && result.code == 0
236
239
  let updateAvailable = updateOutput != nil && NotificationCoordinator.updateIsAvailable(in: result.output)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ㅅㅋㅅ",
4
- "version": "10.7.1",
4
+ "version": "10.7.2",
5
5
  "description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -1,49 +0,0 @@
1
- import Cocoa
2
-
3
- final class RemoteCodingViewController: NSViewController {
4
- private static let websiteURL = URL(string: "https://paseo.sh/")!
5
- private static let docsURL = URL(string: "https://paseo.sh/docs")!
6
-
7
- override func loadView() {
8
- let websiteButton = NativeView.button(
9
- "Visit Paseo",
10
- target: self,
11
- action: #selector(openWebsite)
12
- )
13
- websiteButton.setAccessibilityLabel("Visit Paseo website")
14
- websiteButton.setAccessibilityHelp("Open the independent Paseo website in your default browser.")
15
-
16
- let docsButton = NativeView.button(
17
- "Read Paseo Docs",
18
- target: self,
19
- action: #selector(openDocs)
20
- )
21
- docsButton.setAccessibilityLabel("Read Paseo documentation")
22
- docsButton.setAccessibilityHelp("Open the official Paseo documentation in your default browser.")
23
-
24
- let recommendation = NativeView.card(
25
- title: "Paseo (recommended)",
26
- subtitle: "Run Codex from your desktop, phone, web browser, or terminal.",
27
- views: [
28
- NativeView.row([websiteButton, docsButton]),
29
- NativeView.detail("Paseo is a separate, open-source app. Follow its setup and security guidance to connect your devices.")
30
- ]
31
- )
32
-
33
- view = NativeView.page([
34
- NativeView.title("Remote Coding"),
35
- NativeView.detail(
36
- "Continue coding from another device."
37
- ),
38
- recommendation
39
- ])
40
- }
41
-
42
- @objc private func openWebsite() {
43
- NSWorkspace.shared.open(Self.websiteURL)
44
- }
45
-
46
- @objc private func openDocs() {
47
- NSWorkspace.shared.open(Self.docsURL)
48
- }
49
- }