sneakoscope 4.7.1 → 4.7.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.
Files changed (36) hide show
  1. package/README.md +11 -11
  2. package/crates/sks-core/Cargo.lock +1 -1
  3. package/crates/sks-core/Cargo.toml +1 -1
  4. package/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/cli/command-registry.js +1 -1
  7. package/dist/cli/install-helpers.js +215 -57
  8. package/dist/commands/codex-lb.js +143 -7
  9. package/dist/commands/doctor.js +59 -2
  10. package/dist/core/codex-app/codex-app-fast-ui-repair.js +11 -1
  11. package/dist/core/codex-app/codex-app-restart.js +65 -0
  12. package/dist/core/codex-app/sks-menubar.js +445 -0
  13. package/dist/core/codex-app.js +239 -3
  14. package/dist/core/codex-control/codex-sdk-config-policy.js +2 -2
  15. package/dist/core/codex-control/codex-task-runner.js +2 -2
  16. package/dist/core/codex-lb/codex-lb-setup.js +1 -1
  17. package/dist/core/commands/basic-cli.js +14 -7
  18. package/dist/core/doctor/doctor-dirty-planner.js +2 -0
  19. package/dist/core/doctor/doctor-readiness-matrix.js +4 -0
  20. package/dist/core/fsx.js +1 -1
  21. package/dist/core/imagegen/imagegen-capability.js +2 -2
  22. package/dist/core/init.js +1 -1
  23. package/dist/core/managed-assets/managed-assets-manifest.js +1 -1
  24. package/dist/core/provider/provider-context.js +1 -1
  25. package/dist/core/routes.js +2 -2
  26. package/dist/core/update-check.js +67 -18
  27. package/dist/core/version.js +1 -1
  28. package/dist/scripts/codex-app-provider-model-ui-check.js +114 -0
  29. package/dist/scripts/codex-lb-fast-mode-truth-check.js +90 -0
  30. package/dist/scripts/codex-lb-setup-truthfulness-check.js +6 -1
  31. package/dist/scripts/doctor-fixes-codex-app-fast-ui-check.js +5 -1
  32. package/dist/scripts/gpt-image-2-real-file-smoke.js +2 -2
  33. package/dist/scripts/provider-context-config-toml-check.js +2 -2
  34. package/dist/scripts/sks-menubar-install-check.js +59 -0
  35. package/dist/scripts/update-default-command-check.js +28 -0
  36. package/package.json +5 -1
@@ -1,12 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import readline from 'node:readline/promises';
3
3
  import { stdin as input, stdout as output } from 'node:process';
4
- import { projectRoot, readStdin } from '../core/fsx.js';
4
+ import { projectRoot, readStdin, readText } from '../core/fsx.js';
5
5
  import { flag, readOption } from '../cli/args.js';
6
6
  import { printJson } from '../cli/output.js';
7
7
  import { codexLbMetrics, readCodexLbCircuit, recordCodexLbHealthEvent, resetCodexLbCircuit, codexLbProofEvidence } from '../core/codex-lb-circuit.js';
8
8
  import { checkCodexLbResponseChain, codexLbStatus, configureCodexLb, formatCodexLbStatusText, releaseCodexLbAuthHold, repairCodexLbAuth, unselectCodexLbProvider } from '../cli/install-helpers.js';
9
9
  import { buildCodexLbSetupPlan, codexLbPersistenceSummary, renderCodexLbSetupPlan } from '../core/codex-lb/codex-lb-setup.js';
10
+ import { restartCodexApp } from '../core/codex-app/codex-app-restart.js';
10
11
  export async function run(command, args = []) {
11
12
  const root = await projectRoot();
12
13
  const action = args[0] || 'status';
@@ -41,7 +42,7 @@ export async function run(command, args = []) {
41
42
  if (action === 'health' || action === 'verify-chain' || action === 'chain') {
42
43
  const status = await codexLbStatus();
43
44
  const blocker = !status.env_key_configured ? 'missing_env_key' : !status.base_url ? 'missing_base_url' : 'not_configured';
44
- const result = status.ok ? await checkCodexLbResponseChain(status, { force: true, root }) : { ok: false, status: blocker, codex_lb: status };
45
+ const result = status.ok ? await checkCodexLbResponseChain(status, { force: true, root, fastMode: flag(args, '--fast') || flag(args, '--priority') }) : { ok: false, status: blocker, codex_lb: status };
45
46
  if (flag(args, '--json'))
46
47
  return printJson(result);
47
48
  console.log(`codex-lb response chain: ${result.ok ? 'ok' : `failed (${result.status})`}`);
@@ -49,6 +50,47 @@ export async function run(command, args = []) {
49
50
  process.exitCode = 1;
50
51
  return;
51
52
  }
53
+ if (action === 'fast-check' || action === 'fast' || action === 'verify-fast') {
54
+ const status = await codexLbStatus();
55
+ const blocker = !status.env_key_configured ? 'missing_env_key' : !status.base_url ? 'missing_base_url' : !status.provider_contract_ok ? 'provider_contract_drift' : 'not_configured';
56
+ const chain = status.env_key_configured && status.base_url
57
+ ? await checkCodexLbResponseChain(status, { force: true, cache: false, root, fastMode: true })
58
+ : { ok: false, status: blocker, codex_lb: status };
59
+ const evidence = await fastEvidenceFromChain(chain, readOption(args, '--request-log', readOption(args, '--request-log-json', null)));
60
+ const providerReady = status.provider_contract_ok === true;
61
+ const result = {
62
+ schema: 'sks.codex-lb-fast-check.v1',
63
+ ok: Boolean(providerReady && chain.ok && evidence.fast_requested && evidence.fast_actual),
64
+ status: !providerReady
65
+ ? 'provider_contract_drift'
66
+ : chain.ok
67
+ ? evidence.fast_actual
68
+ ? 'fast_verified'
69
+ : evidence.fast_requested
70
+ ? 'fast_requested_but_actual_unverified'
71
+ : 'fast_not_requested'
72
+ : chain.status,
73
+ codex_lb: status,
74
+ chain,
75
+ evidence,
76
+ blockers: [
77
+ ...(providerReady ? [] : ['codex_lb_provider_contract_drift']),
78
+ ...(chain.ok
79
+ ? evidence.fast_actual
80
+ ? []
81
+ : ['codex_lb_actual_fast_service_tier_unverified']
82
+ : [chain.status || blocker])
83
+ ]
84
+ };
85
+ if (flag(args, '--json'))
86
+ return printJson(result);
87
+ console.log(`codex-lb fast check: ${result.ok ? 'ok' : `blocked (${result.status})`}`);
88
+ if (!result.ok) {
89
+ console.log('Need codex-lb request evidence: requestedServiceTier=priority and actualServiceTier/serviceTier=priority.');
90
+ process.exitCode = 1;
91
+ }
92
+ return;
93
+ }
52
94
  if (action === 'repair' || action === 'resync' || action === 'login') {
53
95
  const result = await repairCodexLbAuth();
54
96
  if (flag(args, '--json'))
@@ -97,20 +139,27 @@ export async function run(command, args = []) {
97
139
  process.exitCode = 1;
98
140
  return;
99
141
  }
100
- const result = await configureCodexLb({ host, apiKey: newKey });
142
+ const result = await configureCodexLb({ host, apiKey: newKey, authMode: flag(args, '--preserve-auth') ? 'preserve' : 'codex-lb', forceCodexLbApiKeyAuth: !flag(args, '--preserve-auth') });
143
+ const restart = await maybeRestartCodexAppForAuthSwitch(args, Boolean(result.ok));
144
+ const output = { ...result, action: 'set-key', restart_app: restart };
101
145
  if (flag(args, '--json'))
102
- return printJson({ ...result, action: 'set-key' });
146
+ return printJson(output);
103
147
  console.log(result.ok ? `codex-lb API key updated (${result.base_url || host}).` : `codex-lb key update failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
148
+ if (restart?.status === 'restarted')
149
+ console.log('Codex App restarted for the new codex-lb auth mode.');
104
150
  if (!result.ok)
105
151
  process.exitCode = 1;
106
152
  return;
107
153
  }
108
154
  if (action === 'use-codex-lb' || action === 'use-lb') {
109
155
  // Switch auth mode -> codex-lb (API key). Re-selects the provider and re-syncs auth.
110
- const result = await repairCodexLbAuth();
156
+ const result = await repairCodexLbAuth({ forceCodexLbApiKeyAuth: true, authMode: 'codex-lb', forceFastMode: !flag(args, '--no-fast') });
157
+ const restart = await maybeRestartCodexAppForAuthSwitch(args, Boolean(result.ok));
111
158
  if (flag(args, '--json'))
112
- return printJson({ ...result, mode: 'codex-lb' });
159
+ return printJson({ ...result, mode: 'codex-lb', restart_app: restart });
113
160
  console.log(result.ok ? 'Auth mode: codex-lb selected (API key).' : `Switch to codex-lb failed: ${result.status}${result.error ? `: ${result.error}` : ''}`);
161
+ if (restart?.status === 'restarted')
162
+ console.log('Codex App restarted for codex-lb auth.');
114
163
  if (!result.ok)
115
164
  process.exitCode = 1;
116
165
  return;
@@ -118,8 +167,9 @@ export async function run(command, args = []) {
118
167
  if (action === 'use-oauth' || action === 'use-chatgpt') {
119
168
  // Switch auth mode -> ChatGPT OAuth. Restores the saved OAuth login if present.
120
169
  const result = await releaseCodexLbAuthHold({ force: flag(args, '--force') });
170
+ const restart = await maybeRestartCodexAppForAuthSwitch(args, !['no_backup', 'auth_in_use', 'failed'].includes(result.status));
121
171
  if (flag(args, '--json'))
122
- return printJson({ ...result, mode: 'oauth' });
172
+ return printJson({ ...result, mode: 'oauth', restart_app: restart });
123
173
  if (result.status === 'no_backup') {
124
174
  console.log('No saved ChatGPT OAuth credentials to restore. Switch to OAuth by logging in:');
125
175
  console.log(' codex login');
@@ -128,6 +178,8 @@ export async function run(command, args = []) {
128
178
  return;
129
179
  }
130
180
  console.log(`Auth mode: ${['released', 'oauth_restored'].includes(result.status) ? 'ChatGPT OAuth restored' : result.status}.`);
181
+ if (restart?.status === 'restarted')
182
+ console.log('Codex App restarted for ChatGPT OAuth.');
131
183
  if (['auth_in_use', 'failed'].includes(result.status))
132
184
  process.exitCode = 1;
133
185
  return;
@@ -224,6 +276,9 @@ export async function run(command, args = []) {
224
276
  const result = await configureCodexLb({
225
277
  host: options.host,
226
278
  apiKey: options.apiKey,
279
+ authMode: flag(args, '--preserve-auth') ? 'preserve' : 'codex-lb',
280
+ forceCodexLbApiKeyAuth: !flag(args, '--preserve-auth'),
281
+ forceFastMode: !flag(args, '--no-fast'),
227
282
  keychain: options.keychain,
228
283
  storeKeychain: options.keychain,
229
284
  useDefaultProvider: options.useDefaultProvider,
@@ -234,7 +289,9 @@ export async function run(command, args = []) {
234
289
  apiKeySource: options.apiKeySource,
235
290
  allowInsecureHttp: options.allowInsecureLocalhost
236
291
  });
292
+ const restart = await maybeRestartCodexAppForAuthSwitch(args, Boolean(result.ok) && !flag(args, '--preserve-auth'));
237
293
  const shaped = { schema: 'sks.codex-lb-setup.v1', ...result, api_key: { present: Boolean(options.apiKey), redacted: true }, env_file_chmod: '0600' };
294
+ shaped.restart_app = restart;
238
295
  if (options.health)
239
296
  shaped.applied_actions = [...(shaped.applied_actions || []), { type: 'run_health_check', target: 'codex-lb response chain', ok: true }];
240
297
  if (options.health)
@@ -284,6 +341,85 @@ export async function run(command, args = []) {
284
341
  console.error(' use-oauth switch auth mode to ChatGPT OAuth (restores saved login, else: codex login)');
285
342
  process.exitCode = 1;
286
343
  }
344
+ async function maybeRestartCodexAppForAuthSwitch(args = [], enabled) {
345
+ if (!enabled)
346
+ return { schema: 'sks.codex-app-restart.v1', ok: true, status: 'skipped', skipped: true, reason: 'previous_step_failed', app_name: 'Codex', blockers: [] };
347
+ const requested = flag(args, '--restart-app') || flag(args, '--restart');
348
+ const shouldRestart = requested || (!flag(args, '--json') && !flag(args, '--no-restart-app') && !flag(args, '--no-restart'));
349
+ return restartCodexApp({ enabled: shouldRestart });
350
+ }
351
+ async function fastEvidenceFromChain(chain = {}, requestLogPath = null) {
352
+ const chainEvidence = chain.service_tier_evidence || {};
353
+ const logRows = requestLogPath ? await readRequestLogRows(String(requestLogPath)) : [];
354
+ const logEvidence = serviceTierEvidenceFromRows(logRows);
355
+ const requested = logEvidence.requested_service_tier || chainEvidence.requested_service_tier || chain.requested_service_tier || null;
356
+ const actual = logEvidence.actual_service_tier || chainEvidence.actual_service_tier || null;
357
+ const effective = logEvidence.effective_service_tier || chainEvidence.effective_service_tier || null;
358
+ return {
359
+ requested_service_tier: requested,
360
+ actual_service_tier: actual,
361
+ effective_service_tier: effective,
362
+ fast_requested: requested === 'priority' || chain.requested_service_tier === 'priority' || chainEvidence.fast_requested === true,
363
+ fast_actual: actual === 'priority' || effective === 'priority' || logEvidence.fast_actual === true || chainEvidence.fast_actual === true,
364
+ chain_evidence: chainEvidence,
365
+ request_log_path: requestLogPath || null,
366
+ request_log_rows: logRows.length
367
+ };
368
+ }
369
+ async function readRequestLogRows(file) {
370
+ if (!file)
371
+ return [];
372
+ const text = await readText(path.isAbsolute(file) ? file : path.resolve(process.cwd(), file), '').catch(() => '');
373
+ const rows = [];
374
+ const trimmed = text.trim();
375
+ if (!trimmed)
376
+ return rows;
377
+ try {
378
+ const parsed = JSON.parse(trimmed);
379
+ if (Array.isArray(parsed))
380
+ return parsed;
381
+ if (Array.isArray(parsed?.rows))
382
+ return parsed.rows;
383
+ if (Array.isArray(parsed?.requests))
384
+ return parsed.requests;
385
+ return [parsed];
386
+ }
387
+ catch { }
388
+ for (const line of text.split(/\r?\n/)) {
389
+ const candidate = line.trim();
390
+ if (!candidate)
391
+ continue;
392
+ try {
393
+ rows.push(JSON.parse(candidate));
394
+ }
395
+ catch { }
396
+ }
397
+ return rows;
398
+ }
399
+ function serviceTierEvidenceFromRows(rows = []) {
400
+ let requested = null;
401
+ let actual = null;
402
+ let effective = null;
403
+ for (const row of rows) {
404
+ requested ||= normalizeTier(row?.requestedServiceTier || row?.requested_service_tier || row?.request?.service_tier || row?.body?.service_tier);
405
+ actual ||= normalizeTier(row?.actualServiceTier || row?.actual_service_tier || row?.response?.actualServiceTier);
406
+ effective ||= normalizeTier(row?.serviceTier || row?.service_tier || row?.response?.serviceTier);
407
+ }
408
+ return {
409
+ requested_service_tier: requested,
410
+ actual_service_tier: actual,
411
+ effective_service_tier: effective,
412
+ fast_actual: actual === 'priority' || effective === 'priority'
413
+ };
414
+ }
415
+ function normalizeTier(value) {
416
+ const text = String(value || '').trim().toLowerCase();
417
+ if (text === 'fast')
418
+ return 'priority';
419
+ if (text === 'priority' || text === 'default' || text === 'flex')
420
+ return text;
421
+ return null;
422
+ }
287
423
  async function resolveNewApiKey(args = []) {
288
424
  const flagKey = readOption(args, '--api-key', '');
289
425
  if (flagKey)
@@ -39,6 +39,7 @@ import { planDoctorDirtyRepair } from '../core/doctor/doctor-dirty-planner.js';
39
39
  import { doctorRepairPostcheck } from '../core/doctor/doctor-repair-postcheck.js';
40
40
  import { withSecretPreservationGuard } from '../core/config/config-migration-journal.js';
41
41
  import { writeProjectUpdateMigrationReceipt } from '../core/update/update-migration-state.js';
42
+ import { installSksMenuBar } from '../core/codex-app/sks-menubar.js';
42
43
  export async function run(_command, args = []) {
43
44
  const root = await projectRoot();
44
45
  const doctorFix = flag(args, '--fix');
@@ -274,6 +275,27 @@ async function runDoctor(args = [], root, doctorFix) {
274
275
  blockers: [err?.message || String(err)]
275
276
  }))
276
277
  : codexAppUiPlan;
278
+ const sksMenuBar = await installSksMenuBar({
279
+ root,
280
+ apply: doctorFix,
281
+ launch: doctorFix
282
+ }).catch((err) => ({
283
+ schema: 'sks.codex-app-sks-menubar.v1',
284
+ ok: false,
285
+ apply: doctorFix,
286
+ status: 'blocked',
287
+ platform: process.platform,
288
+ app_path: null,
289
+ executable_path: null,
290
+ launch_agent_path: null,
291
+ action_script_path: null,
292
+ report_path: `${root}/.sneakoscope/reports/sks-menubar.json`,
293
+ menu_items: [],
294
+ actions: [],
295
+ launch: { requested: doctorFix, method: 'none', ok: false, error: err?.message || String(err) },
296
+ blockers: [err?.message || String(err)],
297
+ warnings: []
298
+ }));
277
299
  const zellijRepair = shouldRunZellijRepair
278
300
  ? await runDoctorZellijRepair({ root, args, doctorFix }).catch((err) => ({
279
301
  schema: 'sks.zellij-self-heal.v1',
@@ -434,6 +456,20 @@ async function runDoctor(args = [], root, doctorFix) {
434
456
  rollback_evidence: 'optional_supabase_no_ready_mutation_required'
435
457
  })
436
458
  },
459
+ {
460
+ id: 'sks_menubar',
461
+ required_for_ready: false,
462
+ run: async () => ({
463
+ id: 'sks_menubar',
464
+ ok: sksMenuBar?.ok !== false,
465
+ repaired: doctorFix && Array.isArray(sksMenuBar?.actions) && sksMenuBar.actions.length > 0,
466
+ required_for_ready: false,
467
+ blockers: sksMenuBar?.blockers || [],
468
+ warnings: sksMenuBar?.warnings || [],
469
+ artifact_path: sksMenuBar?.report_path || null,
470
+ rollback_evidence: sksMenuBar?.launch_agent_path || sksMenuBar?.report_path || 'sks_menubar_optional_no_core_mutation'
471
+ })
472
+ },
437
473
  {
438
474
  id: 'command_alias_cleanup',
439
475
  run: async () => ({
@@ -621,6 +657,7 @@ async function runDoctor(args = [], root, doctorFix) {
621
657
  agent_role_config: agentRoleConfigRepair,
622
658
  repair: configRepair,
623
659
  codex_app_ui: codexAppUi,
660
+ sks_menubar: sksMenuBar,
624
661
  codex_0138_doctor: codex0138Doctor,
625
662
  codex_plugin_inventory: pluginInventory?.report || null,
626
663
  codex_plugin_app_template_policy: pluginPolicy,
@@ -658,6 +695,7 @@ async function runDoctor(args = [], root, doctorFix) {
658
695
  rust,
659
696
  codex_app: codexApp,
660
697
  codex_app_ui: codexAppUi,
698
+ sks_menubar: sksMenuBar,
661
699
  provider_context: providerContext,
662
700
  codex_lb: codexLb,
663
701
  codex_doctor: authoritativeCodexDoctor,
@@ -697,7 +735,7 @@ async function runDoctor(args = [], root, doctorFix) {
697
735
  ready,
698
736
  sneakoscope: { ok: await exists(`${root}/.sneakoscope`) },
699
737
  package: { bytes: pkgBytes, human: formatBytes(pkgBytes) },
700
- repair: { sks_update: sksUpdate, setup: setupRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, context7: context7Repair, codex_startup: codexStartupRepair, startup_config: startupConfigRepair, context7_mcp: context7McpRepair, supabase_mcp: supabaseMcpRepair, doctor_transaction: doctorFixTransaction, doctor_dirty_plan: doctorDirtyPlan, doctor_postcheck: doctorFixPostcheck, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup }
738
+ repair: { sks_update: sksUpdate, setup: setupRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, context7: context7Repair, codex_startup: codexStartupRepair, startup_config: startupConfigRepair, context7_mcp: context7McpRepair, supabase_mcp: supabaseMcpRepair, sks_menubar: sksMenuBar, doctor_transaction: doctorFixTransaction, doctor_dirty_plan: doctorDirtyPlan, doctor_postcheck: doctorFixPostcheck, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup }
701
739
  };
702
740
  if (reportFile)
703
741
  await writeJsonReportFile(reportFile, result);
@@ -801,9 +839,18 @@ async function runDoctor(args = [], root, doctorFix) {
801
839
  console.log(` native agent_type: ${codexAppHarnessMatrix.app_features?.agent_type_supported ? 'ok' : 'fallback message-role'}`);
802
840
  console.log(` init-deep memory: ${codexAppHarnessMatrix.sks_integrations?.init_deep_available ? 'available' : 'missing'}`);
803
841
  console.log(` loop mesh app profile: ${codexAppHarnessMatrix.sks_integrations?.loop_mesh_app_profile_available ? 'available' : 'missing'}`);
842
+ const codexAppUiStatus = codexAppUi;
804
843
  console.log('Codex App UI:');
805
844
  console.log(` fast selector: ${codexAppUi.fast_selector || 'unknown'}`);
806
845
  console.log(` provider selector: ${codexAppUi.provider_selector || 'unknown'}`);
846
+ if (Array.isArray(codexAppUiStatus.provider_blockers) && codexAppUiStatus.provider_blockers.length) {
847
+ console.log(` provider blockers: ${codexAppUiStatus.provider_blockers.join(', ')}`);
848
+ }
849
+ if (Array.isArray(codexAppUiStatus.provider_actions) && codexAppUiStatus.provider_actions.length) {
850
+ console.log(' provider actions:');
851
+ for (const action of codexAppUiStatus.provider_actions)
852
+ console.log(` - ${action}`);
853
+ }
807
854
  console.log(` host-owned config: ${codexAppUi.host_owned_config || 'unknown'}`);
808
855
  if (Array.isArray(codexAppUi.actions) && codexAppUi.actions.some((action) => action.changed)) {
809
856
  console.log(' repaired files:');
@@ -812,6 +859,16 @@ async function runDoctor(args = [], root, doctorFix) {
812
859
  }
813
860
  if (codexAppUi.next_action)
814
861
  console.log(` next action: ${codexAppUi.next_action}`);
862
+ console.log('SKS Menu Bar:');
863
+ console.log(` status: ${sksMenuBar.status || (sksMenuBar.ok ? 'ok' : 'blocked')}`);
864
+ if (sksMenuBar.app_path)
865
+ console.log(` app: ${sksMenuBar.app_path}`);
866
+ if (sksMenuBar.launch_agent_path)
867
+ console.log(` launch agent: ${sksMenuBar.launch_agent_path}`);
868
+ if (Array.isArray(sksMenuBar.blockers) && sksMenuBar.blockers.length)
869
+ console.log(` blockers: ${sksMenuBar.blockers.join(', ')}`);
870
+ if (Array.isArray(sksMenuBar.warnings) && sksMenuBar.warnings.length)
871
+ console.log(` warnings: ${sksMenuBar.warnings.join(', ')}`);
815
872
  console.log(`Provider: ${providerContext.provider || 'unknown'} ${providerContext.service_tier || ''} (${providerContext.source || 'unknown'}, ${providerContext.confidence || 'low'})`);
816
873
  const imagegenReady = imagegen.auth_readiness;
817
874
  if (imagegenReady) {
@@ -980,7 +1037,7 @@ function doctorPhaseIdsForProfile(profile) {
980
1037
  ];
981
1038
  if (profile === 'migration')
982
1039
  return required;
983
- const optional = ['supabase_mcp_repair', 'native_capability_repair'];
1040
+ const optional = ['supabase_mcp_repair', 'native_capability_repair', 'sks_menubar'];
984
1041
  if (profile === 'full' || profile === 'capabilities')
985
1042
  return ['setup', ...required, ...optional];
986
1043
  return [...required, ...optional];
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { ensureDir, nowIso, readText, sha256, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
4
4
  import { codexHome, scanProjectLocalForbiddenKeys, snapshotCodexAppUiState } from './codex-app-ui-state-snapshot.js';
5
5
  import { assertCodexAppUiMutationAllowed } from './codex-app-ui-clobber-guard.js';
6
+ import { codexProviderModelUiStatus } from '../codex-app.js';
6
7
  export const CODEX_APP_FAST_UI_REPAIR_SCHEMA = 'sks.codex-app-fast-ui-repair.v1';
7
8
  const FAST_UI_TOP_LEVEL_RE = /^\s*(?:model|model_reasoning_effort|service_tier)\s*=/;
8
9
  const CODEX_APP_MODE_LOCK_TOP_LEVEL_RE = /^\s*(?:model|model_reasoning_effort)\s*=/;
@@ -61,6 +62,12 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
61
62
  });
62
63
  }
63
64
  const after = await snapshotCodexAppUiState(resolvedRoot, { codexHome: home });
65
+ const providerModelUi = await codexProviderModelUiStatus({
66
+ cwd: resolvedRoot,
67
+ home: path.dirname(home),
68
+ configPath: path.join(home, 'config.toml'),
69
+ codexLbEnvPath: path.join(home, 'sks-codex-lb.env')
70
+ });
64
71
  const changed = actions.some((action) => action.changed);
65
72
  const requiresConfirmation = unsafeReasons.length > 0 && input.force !== true;
66
73
  const safeAutoApply = changed && !requiresConfirmation;
@@ -81,7 +88,10 @@ export async function repairCodexAppFastUi(root = process.cwd(), input = {}) {
81
88
  detected_project_local_forbidden_keys: [...new Set(detectedProjectLocalForbiddenKeys)],
82
89
  unsafe_repair_reasons: [...new Set(unsafeReasons)],
83
90
  fast_selector: changed ? (input.apply ? 'repaired' : 'manual_action_required') : before.indicators.fast_selector === 'maybe_hidden_or_locked' ? 'manual_action_required' : 'ok',
84
- provider_selector: 'ok',
91
+ provider_selector: providerModelUi.ok ? 'ok' : 'manual_action_required',
92
+ provider_model_ui: providerModelUi,
93
+ provider_actions: providerModelUi.ui_actions || [],
94
+ provider_blockers: providerModelUi.blockers || [],
85
95
  host_owned_config: input.apply && changed ? 'repaired_with_backup' : changed ? 'preserved_until_explicit_apply' : 'preserved',
86
96
  actions,
87
97
  before_fast_selector: before.indicators.fast_selector,
@@ -0,0 +1,65 @@
1
+ import { exists, runProcess, which } from '../fsx.js';
2
+ export async function restartCodexApp(opts = {}) {
3
+ const env = opts.env || process.env;
4
+ const appName = String(opts.appName || env.SKS_CODEX_APP_NAME || 'Codex');
5
+ if (opts.enabled === false || env.SKS_SKIP_CODEX_APP_RESTART === '1') {
6
+ return skipped(appName, 'disabled');
7
+ }
8
+ if (process.platform !== 'darwin')
9
+ return skipped(appName, 'not_macos');
10
+ const run = opts.runProcessImpl || runProcess;
11
+ const osascript = await which('osascript').catch(() => null) || await exists('/usr/bin/osascript').then((ok) => ok ? '/usr/bin/osascript' : null).catch(() => null);
12
+ const open = await which('open').catch(() => null) || await exists('/usr/bin/open').then((ok) => ok ? '/usr/bin/open' : null).catch(() => null);
13
+ if (!osascript || !open) {
14
+ return {
15
+ schema: 'sks.codex-app-restart.v1',
16
+ ok: false,
17
+ status: 'blocked',
18
+ app_name: appName,
19
+ blockers: [
20
+ ...(osascript ? [] : ['osascript_missing']),
21
+ ...(open ? [] : ['open_missing'])
22
+ ]
23
+ };
24
+ }
25
+ const quit = await run(osascript, ['-e', `tell application ${JSON.stringify(appName)} to quit`], { timeoutMs: 5000, maxOutputBytes: 8192 }).catch((err) => ({
26
+ code: 1,
27
+ stdout: '',
28
+ stderr: err?.message || String(err)
29
+ }));
30
+ await sleep(Number(opts.delayMs ?? env.SKS_CODEX_APP_RESTART_DELAY_MS ?? 1200));
31
+ const launched = await run(open, ['-a', appName], { timeoutMs: 10000, maxOutputBytes: 8192 }).catch((err) => ({
32
+ code: 1,
33
+ stdout: '',
34
+ stderr: err?.message || String(err)
35
+ }));
36
+ const quitOk = quit.code === 0 || /not running|Can't get application|application isn't running/i.test(String(quit.stderr || quit.stdout || ''));
37
+ const openOk = launched.code === 0;
38
+ return {
39
+ schema: 'sks.codex-app-restart.v1',
40
+ ok: quitOk && openOk,
41
+ status: quitOk && openOk ? 'restarted' : 'blocked',
42
+ app_name: appName,
43
+ quit: { ok: quitOk, code: quit.code, error: quitOk ? null : String(quit.stderr || quit.stdout || '').trim() },
44
+ open: { ok: openOk, code: launched.code, error: openOk ? null : String(launched.stderr || launched.stdout || '').trim() },
45
+ blockers: [
46
+ ...(quitOk ? [] : ['codex_app_quit_failed']),
47
+ ...(openOk ? [] : ['codex_app_open_failed'])
48
+ ]
49
+ };
50
+ }
51
+ function skipped(appName, reason) {
52
+ return {
53
+ schema: 'sks.codex-app-restart.v1',
54
+ ok: true,
55
+ status: 'skipped',
56
+ skipped: true,
57
+ reason,
58
+ app_name: appName,
59
+ blockers: []
60
+ };
61
+ }
62
+ function sleep(ms) {
63
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
64
+ }
65
+ //# sourceMappingURL=codex-app-restart.js.map