sneakoscope 5.1.2 → 5.3.0

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 (58) 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/crates/sks-core/src/main.rs +1 -1
  5. package/dist/bin/sks.js +1 -1
  6. package/dist/commands/doctor.js +32 -3
  7. package/dist/config/skills-manifest.json +58 -58
  8. package/dist/core/agents/agent-lifecycle.js +5 -3
  9. package/dist/core/agents/native-cli-worker.js +0 -1
  10. package/dist/core/codex-app/sks-menubar.js +462 -130
  11. package/dist/core/commands/basic-cli.js +2 -4
  12. package/dist/core/commands/menubar-command.js +2 -1
  13. package/dist/core/doctor/doctor-dirty-planner.js +51 -7
  14. package/dist/core/fsx.js +1 -1
  15. package/dist/core/naruto/naruto-real-worker-child.js +1 -1
  16. package/dist/core/update/update-migration-state.js +23 -3
  17. package/dist/core/version.js +1 -1
  18. package/dist/core/zellij/zellij-slot-telemetry.js +1 -1
  19. package/dist/scripts/doctor-context7-mcp-repair-blackbox.js +16 -0
  20. package/dist/scripts/doctor-fix-production-blackbox.js +26 -0
  21. package/dist/scripts/doctor-startup-config-repair-blackbox.js +13 -0
  22. package/dist/scripts/doctor-supabase-mcp-repair-blackbox.js +14 -0
  23. package/dist/scripts/naruto-real-active-pool-runtime-check.js +26 -1
  24. package/dist/scripts/project-skill-dedupe-blackbox.js +36 -0
  25. package/dist/scripts/release-gate-script-parity-check.js +44 -16
  26. package/dist/scripts/release-metadata-1-19-check.js +1 -1
  27. package/dist/scripts/sks-menubar-install-check.js +57 -20
  28. package/dist/scripts/supabase-secret-preservation-blackbox.js +29 -0
  29. package/dist/scripts/update-preserves-supabase-keys-blackbox.js +27 -0
  30. package/dist/scripts/zellij-slot-telemetry-runtime-check.js +1 -0
  31. package/package.json +8 -65
  32. package/bench/tasks/t01-off-by-one/repo/package.json +0 -9
  33. package/bench/tasks/t01-off-by-one/repo/src/pagination.js +0 -5
  34. package/bench/tasks/t01-off-by-one/repo/test.js +0 -5
  35. package/bench/tasks/t01-off-by-one/task.json +0 -13
  36. package/bench/tasks/t02-signature-cochange/repo/package.json +0 -9
  37. package/bench/tasks/t02-signature-cochange/repo/src/admin.js +0 -5
  38. package/bench/tasks/t02-signature-cochange/repo/src/audit.js +0 -5
  39. package/bench/tasks/t02-signature-cochange/repo/src/labels.js +0 -3
  40. package/bench/tasks/t02-signature-cochange/repo/src/profile.js +0 -5
  41. package/bench/tasks/t02-signature-cochange/repo/test.js +0 -9
  42. package/bench/tasks/t02-signature-cochange/task.json +0 -12
  43. package/bench/tasks/t03-type-puzzle/repo/package.json +0 -9
  44. package/bench/tasks/t03-type-puzzle/repo/src/config.js +0 -4
  45. package/bench/tasks/t03-type-puzzle/repo/test.js +0 -6
  46. package/bench/tasks/t03-type-puzzle/task.json +0 -13
  47. package/bench/tasks/t04-refactor-preserve/repo/package.json +0 -9
  48. package/bench/tasks/t04-refactor-preserve/repo/src/cart.js +0 -8
  49. package/bench/tasks/t04-refactor-preserve/repo/test.js +0 -4
  50. package/bench/tasks/t04-refactor-preserve/task.json +0 -12
  51. package/bench/tasks/t05-performance/repo/package.json +0 -9
  52. package/bench/tasks/t05-performance/repo/src/pairs.js +0 -9
  53. package/bench/tasks/t05-performance/repo/test.js +0 -11
  54. package/bench/tasks/t05-performance/task.json +0 -12
  55. package/bench/tasks/t06-mistake-rule/repo/package.json +0 -9
  56. package/bench/tasks/t06-mistake-rule/repo/src/loader.js +0 -6
  57. package/bench/tasks/t06-mistake-rule/repo/test.js +0 -7
  58. package/bench/tasks/t06-mistake-rule/task.json +0 -12
@@ -3,6 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { ensureDir, exists, PACKAGE_VERSION, readJson, readText, runProcess, sha256, which, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
6
+ import { findCodexApp } from '../codex-app.js';
6
7
  import { withHeartbeat } from '../../cli/cli-theme.js';
7
8
  export const SKS_MENUBAR_LABEL = 'com.sneakoscope.sks-menubar';
8
9
  const LABEL = SKS_MENUBAR_LABEL;
@@ -20,6 +21,7 @@ const MENU_ITEMS = [
20
21
  'Open Dashboard',
21
22
  'Open Codex Settings',
22
23
  'Restart Codex',
24
+ 'View Last Log',
23
25
  'Quit SKS Menu'
24
26
  ];
25
27
  export function sksMenuBarPaths(homeInput, rootInput) {
@@ -43,10 +45,13 @@ export function sksMenuBarPaths(homeInput, rootInput) {
43
45
  info_plist_path: path.join(contentsPath, 'Info.plist'),
44
46
  action_script_path: path.join(installDir, 'sks-menubar-action.sh'),
45
47
  build_stamp_path: path.join(installDir, 'build-stamp.json'),
48
+ config_path: path.join(installDir, 'config.json'),
46
49
  launch_agent_path: path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`),
47
50
  report_path: path.join(root, '.sneakoscope', 'reports', 'sks-menubar.json'),
48
51
  stdout_log_path: path.join(installDir, 'menubar.out.log'),
49
- stderr_log_path: path.join(installDir, 'menubar.err.log')
52
+ stderr_log_path: path.join(installDir, 'menubar.err.log'),
53
+ logs_dir: path.join(installDir, 'logs'),
54
+ last_action_log_path: path.join(installDir, 'logs', 'last-action.log')
50
55
  };
51
56
  }
52
57
  export async function installSksMenuBar(opts = {}) {
@@ -57,6 +62,7 @@ export async function installSksMenuBar(opts = {}) {
57
62
  const warnings = [];
58
63
  const nextActions = defaultNextActions();
59
64
  let secretEnvCleanup;
65
+ let codexBundleId = null;
60
66
  if (process.platform !== 'darwin') {
61
67
  const result = {
62
68
  schema: 'sks.codex-app-sks-menubar.v1',
@@ -69,7 +75,9 @@ export async function installSksMenuBar(opts = {}) {
69
75
  launch_agent_path: null,
70
76
  action_script_path: null,
71
77
  build_stamp_path: null,
78
+ config_path: null,
72
79
  report_path: apply ? paths.report_path : null,
80
+ codex_bundle_id: null,
73
81
  menu_items: MENU_ITEMS,
74
82
  actions: [],
75
83
  launch: { requested: false, method: 'none', ok: true },
@@ -86,9 +94,10 @@ export async function installSksMenuBar(opts = {}) {
86
94
  const installed = await exists(paths.executable_path);
87
95
  const launchAgent = await exists(paths.launch_agent_path);
88
96
  const status = await inspectSksMenuBarStatus({ home: paths.home, root: paths.root }).catch(() => null);
97
+ const statusWarnings = [...(status?.warnings || []), ...(launchAgent ? [] : ['launch_agent_not_installed_yet'])];
89
98
  return {
90
99
  schema: 'sks.codex-app-sks-menubar.v1',
91
- ok: true,
100
+ ok: status ? status.ok === true : true,
92
101
  apply,
93
102
  status: 'planned',
94
103
  platform: process.platform,
@@ -97,7 +106,9 @@ export async function installSksMenuBar(opts = {}) {
97
106
  launch_agent_path: paths.launch_agent_path,
98
107
  action_script_path: paths.action_script_path,
99
108
  build_stamp_path: paths.build_stamp_path,
109
+ config_path: paths.config_path,
100
110
  report_path: paths.report_path,
111
+ codex_bundle_id: status?.codex_sync.bundle_id || null,
101
112
  menu_items: MENU_ITEMS,
102
113
  actions: installed ? ['menubar_app_present'] : ['menubar_app_install_available'],
103
114
  launch: {
@@ -116,11 +127,12 @@ export async function installSksMenuBar(opts = {}) {
116
127
  build_stamp: status?.build_stamp || null,
117
128
  tcc_automation_status: 'unknown',
118
129
  next_actions: launchAgent ? defaultNextActions() : ['Run: sks menubar install'],
119
- blockers: [],
120
- warnings: launchAgent ? [] : ['launch_agent_not_installed_yet']
130
+ blockers: status?.blockers || [],
131
+ warnings: statusWarnings
121
132
  };
122
133
  }
123
134
  await ensureDir(paths.install_dir);
135
+ await ensureDir(paths.logs_dir);
124
136
  await ensureDir(path.dirname(paths.launch_agent_path));
125
137
  secretEnvCleanup = await cleanupMacLaunchSecretEnvironment({ env }).catch((err) => ({
126
138
  ok: false,
@@ -139,9 +151,17 @@ export async function installSksMenuBar(opts = {}) {
139
151
  const open = env.SKS_MENUBAR_OPEN || await which('open').catch(() => null) || await fallbackTool('/usr/bin/open');
140
152
  const codesign = env.SKS_MENUBAR_CODESIGN || await which('codesign').catch(() => null) || await fallbackTool('/usr/bin/codesign');
141
153
  const xcodeSelect = env.SKS_MENUBAR_XCODE_SELECT || await which('xcode-select').catch(() => null) || await fallbackTool('/usr/bin/xcode-select');
154
+ const clt = await xcodeCltStatus(xcodeSelect);
155
+ if (!clt.ok)
156
+ return await blockedResult('xcode_clt_missing', clt.error || 'Xcode Command Line Tools missing');
142
157
  if (!swiftc)
143
158
  return await blockedResult('swiftc_missing', 'swiftc not found');
144
159
  const swiftcVersion = await toolVersion(swiftc, ['--version']);
160
+ codexBundleId = await resolveCodexBundleId({ home: paths.home, env, warnings });
161
+ if (!codexBundleId)
162
+ warnings.push('codex_app_bundle_id_unresolved');
163
+ const config = await writeDefaultMenuBarConfig(paths.config_path, codexBundleId);
164
+ actions.push(`wrote ${paths.config_path}`);
145
165
  const target = await resolveSksEntryForInstall({
146
166
  ...(opts.sksEntry ? { explicit: opts.sksEntry } : {}),
147
167
  root: paths.root,
@@ -162,6 +182,9 @@ export async function installSksMenuBar(opts = {}) {
162
182
  const swiftSource = swiftMenuSource({
163
183
  actionScriptPath: paths.action_script_path,
164
184
  buildStampPath: paths.build_stamp_path,
185
+ configPath: paths.config_path,
186
+ lastActionLogPath: paths.last_action_log_path,
187
+ codexBundleId: config.codex_bundle_id,
165
188
  packageVersion: PACKAGE_VERSION
166
189
  });
167
190
  const infoPlist = infoPlistSource(PACKAGE_VERSION);
@@ -200,9 +223,6 @@ export async function installSksMenuBar(opts = {}) {
200
223
  }
201
224
  }
202
225
  if (!stampMatches && !binaryStable) {
203
- const clt = await xcodeCltStatus(xcodeSelect);
204
- if (!clt.ok)
205
- return await blockedResult('xcode_clt_missing', clt.error || 'Xcode Command Line Tools missing');
206
226
  try {
207
227
  await buildMenuBarAppAtomically({
208
228
  paths,
@@ -277,7 +297,9 @@ export async function installSksMenuBar(opts = {}) {
277
297
  launch_agent_path: paths.launch_agent_path,
278
298
  action_script_path: paths.action_script_path,
279
299
  build_stamp_path: paths.build_stamp_path,
300
+ config_path: paths.config_path,
280
301
  report_path: paths.report_path,
302
+ codex_bundle_id: codexBundleId,
281
303
  menu_items: MENU_ITEMS,
282
304
  actions,
283
305
  launch,
@@ -285,10 +307,7 @@ export async function installSksMenuBar(opts = {}) {
285
307
  build_stamp: stamp,
286
308
  tcc_automation_status: 'unknown',
287
309
  secret_env_cleanup: secretEnvCleanup,
288
- next_actions: [
289
- ...nextActions,
290
- 'If Terminal automation was denied: System Settings > Privacy & Security > Automation > SKS Menu Bar > Terminal.'
291
- ],
310
+ next_actions: nextActions,
292
311
  blockers: ok ? [] : [launch.error || 'sks_menubar_launch_failed'],
293
312
  warnings
294
313
  };
@@ -306,7 +325,9 @@ export async function installSksMenuBar(opts = {}) {
306
325
  launch_agent_path: paths.launch_agent_path,
307
326
  action_script_path: paths.action_script_path,
308
327
  build_stamp_path: paths.build_stamp_path,
328
+ config_path: paths.config_path,
309
329
  report_path: paths.report_path,
330
+ codex_bundle_id: typeof codexBundleId === 'string' ? codexBundleId : null,
310
331
  menu_items: MENU_ITEMS,
311
332
  actions,
312
333
  launch: { requested: false, method: 'none', ok: false, error: detail || reason },
@@ -358,6 +379,84 @@ export async function cleanupMacLaunchSecretEnvironment(opts = {}) {
358
379
  next_actions: ['Rotate CODEX_LB_API_KEY and OPENROUTER_API_KEY if they were previously exposed in launchd.']
359
380
  };
360
381
  }
382
+ async function writeDefaultMenuBarConfig(configPath, codexBundleId) {
383
+ const previous = await readMenuBarConfig(configPath);
384
+ const config = {
385
+ schema: 'sks.sks-menubar-config.v1',
386
+ codex_bundle_id: codexBundleId,
387
+ quit_with_codex: previous.quit_with_codex === true
388
+ };
389
+ await writeJsonAtomic(configPath, config);
390
+ return config;
391
+ }
392
+ async function readMenuBarConfig(configPath) {
393
+ const config = await readJson(configPath, null);
394
+ return {
395
+ schema: 'sks.sks-menubar-config.v1',
396
+ codex_bundle_id: typeof config?.codex_bundle_id === 'string' && config.codex_bundle_id.trim()
397
+ ? config.codex_bundle_id.trim()
398
+ : null,
399
+ quit_with_codex: config?.quit_with_codex === true
400
+ };
401
+ }
402
+ async function resolveCodexBundleId(input) {
403
+ if (process.platform !== 'darwin')
404
+ return null;
405
+ const appPath = await findCodexApp({ home: input.home, env: input.env }).catch(() => null);
406
+ if (!appPath) {
407
+ input.warnings.push('codex_app_not_found_for_bundle_sync');
408
+ return null;
409
+ }
410
+ const mdls = input.env.SKS_MENUBAR_MDLS || await which('mdls').catch(() => null) || await fallbackTool('/usr/bin/mdls');
411
+ if (mdls) {
412
+ const result = await runProcess(mdls, ['-name', 'kMDItemCFBundleIdentifier', '-raw', appPath], {
413
+ timeoutMs: 3_000,
414
+ maxOutputBytes: 8 * 1024
415
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
416
+ const value = String(result.stdout || '').trim();
417
+ if (result.code === 0 && value && value !== '(null)' && value !== 'null')
418
+ return value;
419
+ }
420
+ const defaults = input.env.SKS_MENUBAR_DEFAULTS || await which('defaults').catch(() => null) || await fallbackTool('/usr/bin/defaults');
421
+ if (defaults) {
422
+ const result = await runProcess(defaults, ['read', path.join(appPath, 'Contents', 'Info'), 'CFBundleIdentifier'], {
423
+ timeoutMs: 3_000,
424
+ maxOutputBytes: 8 * 1024
425
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
426
+ const value = String(result.stdout || '').trim();
427
+ if (result.code === 0 && value)
428
+ return value;
429
+ }
430
+ return null;
431
+ }
432
+ async function smokeSksMenuBarAction(actionScriptPath) {
433
+ if (!(await exists(actionScriptPath)))
434
+ return { ok: false, code: null, output: null, versionDetected: false };
435
+ const result = await runProcess('/bin/zsh', [actionScriptPath, 'version'], {
436
+ timeoutMs: 5_000,
437
+ maxOutputBytes: 16 * 1024
438
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
439
+ const output = String(`${result.stdout || ''}\n${result.stderr || ''}`).trim();
440
+ const versionDetected = /\b(?:sks|sneakoscope)?\s*v?\d+\.\d+\.\d+\b/i.test(output);
441
+ return {
442
+ ok: result.code === 0 && versionDetected,
443
+ code: result.code,
444
+ output: output ? output.slice(0, 700) : null,
445
+ versionDetected
446
+ };
447
+ }
448
+ async function isCodexAppRunningByBundleId(bundleId, env = process.env) {
449
+ if (process.platform !== 'darwin' || !bundleId)
450
+ return false;
451
+ const osascript = env.SKS_MENUBAR_OSASCRIPT || await which('osascript').catch(() => null) || await fallbackTool('/usr/bin/osascript');
452
+ if (!osascript)
453
+ return false;
454
+ const result = await runProcess(osascript, ['-e', `application id "${bundleId.replace(/"/g, '\\"')}" is running`], {
455
+ timeoutMs: 2_000,
456
+ maxOutputBytes: 8 * 1024
457
+ }).catch(() => ({ code: 1, stdout: '', stderr: '' }));
458
+ return result.code === 0 && String(result.stdout || '').trim().toLowerCase() === 'true';
459
+ }
361
460
  export async function inspectSksMenuBarStatus(opts = {}) {
362
461
  const paths = sksMenuBarPaths(opts.home || opts.env?.HOME, opts.root);
363
462
  const installed = await exists(paths.executable_path);
@@ -367,6 +466,16 @@ export async function inspectSksMenuBarStatus(opts = {}) {
367
466
  const sksEntry = shellAssignment(actionText, 'SKS_ENTRY');
368
467
  const nodeExists = nodeBin ? await isExecutable(nodeBin) : false;
369
468
  const sksEntryExists = sksEntry ? await exists(sksEntry) : false;
469
+ const actionSmoke = await smokeSksMenuBarAction(paths.action_script_path);
470
+ const config = await readMenuBarConfig(paths.config_path);
471
+ const codexRunning = config.codex_bundle_id ? await isCodexAppRunningByBundleId(config.codex_bundle_id, opts.env) : null;
472
+ const codexSync = {
473
+ ok: Boolean(config.codex_bundle_id),
474
+ bundle_id: config.codex_bundle_id,
475
+ codex_running: codexRunning,
476
+ icon_visible_expected: config.codex_bundle_id ? codexRunning === true : true,
477
+ warning: config.codex_bundle_id ? null : 'codex_sync_disabled'
478
+ };
370
479
  const buildStamp = await readJson(paths.build_stamp_path, null);
371
480
  const launchd = await inspectLaunchdService(opts.env);
372
481
  const signature = await inspectSignature(paths.app_path, opts.env);
@@ -374,10 +483,14 @@ export async function inspectSksMenuBarStatus(opts = {}) {
374
483
  const warnings = [];
375
484
  if (!installed)
376
485
  blockers.push('menubar_app_missing');
377
- if (installed && !sksEntryExists)
378
- blockers.push('action_script_target_missing');
486
+ if (installed && launchd.checked && !launchd.ok)
487
+ blockers.push('launchd_not_running');
488
+ if (installed && !actionSmoke.ok)
489
+ blockers.push('action_script_smoke_failed');
379
490
  if (installed && signature.checked && !signature.ok)
380
491
  warnings.push('codesign_identifier_unexpected');
492
+ if (!codexSync.ok)
493
+ warnings.push('codex_sync_disabled');
381
494
  if (buildStamp?.package_version && buildStamp.package_version !== PACKAGE_VERSION)
382
495
  warnings.push('build_stamp_package_version_mismatch');
383
496
  return {
@@ -393,8 +506,12 @@ export async function inspectSksMenuBarStatus(opts = {}) {
393
506
  node_exists: nodeExists,
394
507
  sks_entry: sksEntry,
395
508
  sks_entry_exists: sksEntryExists,
396
- ok: Boolean(nodeExists && sksEntryExists)
509
+ smoke_code: actionSmoke.code,
510
+ smoke_output: actionSmoke.output,
511
+ version_detected: actionSmoke.versionDetected,
512
+ ok: actionSmoke.ok
397
513
  },
514
+ codex_sync: codexSync,
398
515
  build_stamp: buildStamp,
399
516
  package_version: PACKAGE_VERSION,
400
517
  signature,
@@ -543,16 +660,7 @@ async function resolveSksEntryForInstall(input) {
543
660
  let existsResolved = await exists(resolved);
544
661
  let projectLocal = isSubpath(resolved, input.root);
545
662
  if (projectLocal) {
546
- const globalCandidate = await findGlobalSksEntry(input.home, input.env, packaged);
547
- if (globalCandidate && globalCandidate !== resolved) {
548
- input.warnings.push('sks_entry_project_local_ignored_global_package_used');
549
- resolved = globalCandidate;
550
- existsResolved = true;
551
- projectLocal = isSubpath(resolved, input.root);
552
- }
553
- else {
554
- input.warnings.push('sks_entry_resolved_under_project_root');
555
- }
663
+ input.warnings.push('sks_entry_project_local');
556
664
  }
557
665
  const usedPreviousScript = !existsResolved && await exists(input.actionScriptPath);
558
666
  if (!existsResolved && usedPreviousScript)
@@ -566,81 +674,195 @@ async function resolveSksEntryForInstall(input) {
566
674
  used_previous_script: usedPreviousScript
567
675
  };
568
676
  }
569
- async function findGlobalSksEntry(home, env, packaged) {
570
- const candidates = new Set();
571
- const nodeRoot = path.resolve(path.dirname(process.execPath), '..');
572
- candidates.add(path.join(nodeRoot, 'lib', 'node_modules', 'sneakoscope', 'dist', 'bin', 'sks.js'));
573
- candidates.add(path.join(home, '.nvm', 'versions', 'node'));
574
- const nvmRoot = path.join(home, '.nvm', 'versions', 'node');
575
- try {
576
- const versions = await fs.readdir(nvmRoot);
577
- for (const version of versions.sort().reverse()) {
578
- candidates.add(path.join(nvmRoot, version, 'lib', 'node_modules', 'sneakoscope', 'dist', 'bin', 'sks.js'));
579
- }
580
- }
581
- catch { }
582
- if (env.SKS_GLOBAL_ROOT)
583
- candidates.add(path.join(path.resolve(env.SKS_GLOBAL_ROOT), 'dist', 'bin', 'sks.js'));
584
- for (const candidate of candidates) {
585
- if (candidate === nvmRoot)
586
- continue;
587
- if (path.resolve(candidate) === path.resolve(packaged))
588
- continue;
589
- if (await exists(candidate))
590
- return path.resolve(candidate);
591
- }
592
- return null;
593
- }
594
- function actionScriptSource(input) {
677
+ export function actionScriptSource(input) {
595
678
  return `#!/bin/zsh
596
679
  set -e
597
680
  export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
598
681
  NODE_BIN=${shellQuote(input.nodeBin)}
599
682
  SKS_ENTRY=${shellQuote(input.sksEntry)}
600
- if [ -x "$NODE_BIN" ] && [ -f "$SKS_ENTRY" ]; then
601
- exec "$NODE_BIN" "$SKS_ENTRY" "$@"
683
+
684
+ notify_sks_missing() {
685
+ /usr/bin/osascript -e 'display notification "sks CLI를 찾을 수 없습니다. sks doctor --fix 또는 npm install -g sneakoscope 실행 후 다시 시도하세요." with title "SKS Menu Bar"' >/dev/null 2>&1 || true
686
+ }
687
+
688
+ resolve_node_bin() {
689
+ if [ -x "$NODE_BIN" ]; then
690
+ printf '%s\\n' "$NODE_BIN"
691
+ return 0
692
+ fi
693
+ local login_node
694
+ login_node="$(/bin/zsh -lc 'command -v node' 2>/dev/null | /usr/bin/head -n 1 || true)"
695
+ if [ -n "$login_node" ] && [ -x "$login_node" ]; then
696
+ printf '%s\\n' "$login_node"
697
+ return 0
698
+ fi
699
+ for cand in "$HOME"/.nvm/versions/node/*/bin/node(Nn[-1]) /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
700
+ if [ -x "$cand" ]; then
701
+ printf '%s\\n' "$cand"
702
+ return 0
703
+ fi
704
+ done
705
+ return 1
706
+ }
707
+
708
+ run_node_entry() {
709
+ local entry="$1"
710
+ shift
711
+ if [ ! -f "$entry" ]; then
712
+ return 1
713
+ fi
714
+ local node_bin
715
+ node_bin="$(resolve_node_bin || true)"
716
+ if [ -z "$node_bin" ]; then
717
+ return 1
718
+ fi
719
+ exec "$node_bin" "$entry" "$@"
720
+ }
721
+
722
+ SKS_BIN="$(/bin/zsh -lc 'command -v sks' 2>/dev/null | /usr/bin/head -n 1 || true)"
723
+ if [ -n "$SKS_BIN" ] && [ -x "$SKS_BIN" ]; then
724
+ exec "$SKS_BIN" "$@"
725
+ fi
726
+
727
+ NPM_ROOT="$(/bin/zsh -lc 'npm root -g' 2>/dev/null | /usr/bin/head -n 1 || true)"
728
+ if [ -n "$NPM_ROOT" ]; then
729
+ run_node_entry "$NPM_ROOT/sneakoscope/dist/bin/sks.js" "$@" || true
602
730
  fi
603
- for cand in "$HOME"/.nvm/versions/node/*/bin/node(Nn[-1]); do
604
- if [ -x "$cand" ] && [ -f "$SKS_ENTRY" ]; then
605
- exec "$cand" "$SKS_ENTRY" "$@"
731
+
732
+ for entry in "$HOME"/.nvm/versions/node/*/lib/node_modules/sneakoscope/dist/bin/sks.js(Nn[-1]) /opt/homebrew/lib/node_modules/sneakoscope/dist/bin/sks.js /usr/local/lib/node_modules/sneakoscope/dist/bin/sks.js; do
733
+ if [ -f "$entry" ]; then
734
+ run_node_entry "$entry" "$@" || true
606
735
  fi
607
736
  done
608
- if /bin/zsh -lc 'command -v sks' >/dev/null 2>&1; then
609
- exec /bin/zsh -lc "sks $(printf '%q ' "$@")"
610
- fi
737
+ run_node_entry "$SKS_ENTRY" "$@" || true
738
+ notify_sks_missing
611
739
  echo "SKS command not found. Run npm install -g sneakoscope or sks doctor --fix, then try again." >&2
612
740
  exit 127
613
741
  `;
614
742
  }
615
- function swiftMenuSource(input) {
743
+ export function swiftMenuSource(input) {
744
+ const codexLifecycleSource = input.codexBundleId ? `
745
+ func configureCodexLifecycleSync() {
746
+ setIconVisible(isCodexRunning())
747
+ let center = NSWorkspace.shared.notificationCenter
748
+ center.addObserver(self, selector: #selector(workspaceAppLaunched(_:)), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
749
+ center.addObserver(self, selector: #selector(workspaceAppTerminated(_:)), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
750
+ }
751
+
752
+ @objc func workspaceAppLaunched(_ notification: Notification) {
753
+ guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
754
+ if app.bundleIdentifier == codexBundleId {
755
+ setIconVisible(true)
756
+ updateState()
757
+ }
758
+ }
759
+
760
+ @objc func workspaceAppTerminated(_ notification: Notification) {
761
+ guard let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication else { return }
762
+ if app.bundleIdentifier == codexBundleId {
763
+ if quitWithCodex {
764
+ NSApplication.shared.terminate(nil)
765
+ } else {
766
+ setIconVisible(false)
767
+ }
768
+ }
769
+ }
770
+
771
+ func setIconVisible(_ visible: Bool) {
772
+ statusItem.isVisible = visible
773
+ }
774
+
775
+ func isCodexRunning() -> Bool {
776
+ guard let bundle = codexBundleId else { return true }
777
+ return NSWorkspace.shared.runningApplications.contains { $0.bundleIdentifier == bundle }
778
+ }
779
+ ` : `
780
+ func configureCodexLifecycleSync() {
781
+ setIconVisible(true)
782
+ statusLineItem.title = "Codex app not detected — sync disabled"
783
+ }
784
+
785
+ func setIconVisible(_ visible: Bool) {
786
+ statusItem.isVisible = visible
787
+ }
788
+
789
+ func isCodexRunning() -> Bool {
790
+ return true
791
+ }
792
+ `;
616
793
  return `import Cocoa
617
794
  import Foundation
618
795
 
619
796
  let actionScript = ${swiftString(input.actionScriptPath)}
620
797
  let buildStampPath = ${swiftString(input.buildStampPath)}
798
+ let menubarConfigPath = ${swiftString(input.configPath)}
799
+ let lastActionLogPath = ${swiftString(input.lastActionLogPath)}
800
+ let codexBundleId: String? = ${input.codexBundleId ? swiftString(input.codexBundleId) : 'nil'}
621
801
  let packageVersion = ${swiftString(input.packageVersion)}
622
802
 
623
803
  func shellQuote(_ value: String) -> String {
624
804
  return "'" + value.replacingOccurrences(of: "'", with: "'\\\\''") + "'"
625
805
  }
626
806
 
807
+ func clipped(_ value: String, limit: Int = 700) -> String {
808
+ return String(value.prefix(limit))
809
+ }
810
+
627
811
  func showAlert(_ message: String, informative: String = "") {
628
812
  DispatchQueue.main.async {
813
+ NSApp.activate(ignoringOtherApps: true)
629
814
  let alert = NSAlert()
630
815
  alert.messageText = message
631
- alert.informativeText = informative
816
+ alert.informativeText = clipped(informative)
632
817
  alert.alertStyle = .warning
633
818
  alert.runModal()
634
819
  }
635
820
  }
636
821
 
637
- func runProcess(_ executable: String, _ args: [String] = [], completion: ((Int32, String) -> Void)? = nil) {
822
+ func promptText(title: String, message: String, placeholder: String = "", secure: Bool = false) -> String? {
823
+ NSApp.activate(ignoringOtherApps: true)
824
+ let alert = NSAlert()
825
+ alert.messageText = title
826
+ alert.informativeText = message
827
+ alert.addButton(withTitle: "OK")
828
+ alert.addButton(withTitle: "Cancel")
829
+ let field: NSTextField = secure ? NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 320, height: 24)) : NSTextField(frame: NSRect(x: 0, y: 0, width: 320, height: 24))
830
+ field.placeholderString = placeholder
831
+ alert.accessoryView = field
832
+ let response = alert.runModal()
833
+ if response != .alertFirstButtonReturn { return nil }
834
+ let value = field.stringValue.trimmingCharacters(in: .whitespacesAndNewlines)
835
+ return value.isEmpty ? nil : value
836
+ }
837
+
838
+ func promptChoice(title: String, message: String, options: [String]) -> String? {
839
+ NSApp.activate(ignoringOtherApps: true)
840
+ let alert = NSAlert()
841
+ alert.messageText = title
842
+ alert.informativeText = message
843
+ alert.addButton(withTitle: "OK")
844
+ alert.addButton(withTitle: "Cancel")
845
+ let popup = NSPopUpButton(frame: NSRect(x: 0, y: 0, width: 320, height: 26), pullsDown: false)
846
+ popup.addItems(withTitles: options)
847
+ alert.accessoryView = popup
848
+ let response = alert.runModal()
849
+ if response != .alertFirstButtonReturn { return nil }
850
+ return popup.titleOfSelectedItem
851
+ }
852
+
853
+ func runProcess(_ executable: String, _ args: [String] = [], stdinText: String? = nil, completion: ((Int32, String) -> Void)? = nil) {
638
854
  let process = Process()
639
855
  let output = Pipe()
640
856
  process.executableURL = URL(fileURLWithPath: executable)
641
857
  process.arguments = args
642
858
  process.standardOutput = output
643
859
  process.standardError = output
860
+ var inputPipe: Pipe?
861
+ if stdinText != nil {
862
+ let pipe = Pipe()
863
+ process.standardInput = pipe
864
+ inputPipe = pipe
865
+ }
644
866
  process.terminationHandler = { proc in
645
867
  let data = output.fileHandleForReading.readDataToEndOfFile()
646
868
  let text = String(data: data, encoding: .utf8) ?? ""
@@ -648,14 +870,17 @@ func runProcess(_ executable: String, _ args: [String] = [], completion: ((Int32
648
870
  }
649
871
  do {
650
872
  try process.run()
873
+ if let stdinText = stdinText, let inputPipe = inputPipe {
874
+ inputPipe.fileHandleForWriting.write(Data(stdinText.utf8))
875
+ inputPipe.fileHandleForWriting.closeFile()
876
+ }
651
877
  } catch {
652
878
  completion?(-1, String(describing: error))
653
879
  }
654
880
  }
655
881
 
656
882
  func showNotification(_ title: String, _ body: String) {
657
- let clipped = String(body.prefix(700))
658
- let script = "display notification " + shellQuote(clipped) + " with title " + shellQuote(title)
883
+ let script = "display notification " + shellQuote(clipped(body)) + " with title " + shellQuote(title)
659
884
  runProcess("/usr/bin/osascript", ["-e", script]) { code, output in
660
885
  if code != 0 {
661
886
  showAlert(title, informative: output)
@@ -663,35 +888,40 @@ func showNotification(_ title: String, _ body: String) {
663
888
  }
664
889
  }
665
890
 
666
- func runInTerminal(_ command: String) {
667
- let commandWithExit = "printf '\\\\e]0;SKS\\\\a'; " + command + "; exit"
668
- let escaped = commandWithExit
669
- .replacingOccurrences(of: "\\\\", with: "\\\\\\\\")
670
- .replacingOccurrences(of: "\\\"", with: "\\\\\\\"")
671
- let script = """
672
- tell application "Terminal"
673
- activate
674
- set sksWindow to missing value
675
- repeat with w in windows
676
- if name of w contains "SKS" then set sksWindow to w
677
- end repeat
678
- if sksWindow is missing value then
679
- do script "\\(escaped)"
680
- else
681
- do script "\\(escaped)" in selected tab of sksWindow
682
- end if
683
- end tell
684
- """
685
- runProcess("/usr/bin/osascript", ["-e", script]) { code, output in
686
- if code != 0 {
687
- let denied = output.contains("-1743") || output.localizedCaseInsensitiveContains("not authorized")
688
- if denied {
689
- showAlert("SKS menu cannot control Terminal", informative: "Open System Settings > Privacy & Security > Automation, then allow SKS Menu Bar to control Terminal.")
690
- } else {
691
- showAlert("SKS menu action failed", informative: output)
692
- }
891
+ func redactSecrets(_ value: String, secrets: [String] = []) -> String {
892
+ var text = value
893
+ for secret in secrets where secret.count >= 4 {
894
+ text = text.replacingOccurrences(of: secret, with: "[redacted]")
895
+ }
896
+ let patterns = [
897
+ #"sk-proj-[A-Za-z0-9_-]{12,}"#,
898
+ #"sk-or-v1-[A-Za-z0-9_-]{12,}"#,
899
+ #"sk-or-[A-Za-z0-9_-]{12,}"#,
900
+ #"sk-clb-[A-Za-z0-9_-]{8,}"#,
901
+ #"sk-[A-Za-z0-9_-]{20,}"#,
902
+ #"(?i)(api[_-]?key|secret|token)\\s*[:=]\\s*[^\\s"',}]+"#
903
+ ]
904
+ for pattern in patterns {
905
+ if let regex = try? NSRegularExpression(pattern: pattern) {
906
+ let range = NSRange(text.startIndex..<text.endIndex, in: text)
907
+ text = regex.stringByReplacingMatches(in: text, range: range, withTemplate: "[redacted]")
693
908
  }
694
909
  }
910
+ return text
911
+ }
912
+
913
+ func writeActionLog(_ text: String) {
914
+ let url = URL(fileURLWithPath: lastActionLogPath)
915
+ try? FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
916
+ if !FileManager.default.fileExists(atPath: lastActionLogPath) {
917
+ FileManager.default.createFile(atPath: lastActionLogPath, contents: Data(), attributes: [.posixPermissions: 0o600])
918
+ }
919
+ try? FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: lastActionLogPath)
920
+ if let handle = try? FileHandle(forWritingTo: url) {
921
+ try? handle.truncate(atOffset: 0)
922
+ handle.write(Data(text.utf8))
923
+ try? handle.close()
924
+ }
695
925
  }
696
926
 
697
927
  struct MenuState {
@@ -699,27 +929,37 @@ struct MenuState {
699
929
  let line: String
700
930
  }
701
931
 
702
- final class AppDelegate: NSObject, NSApplicationDelegate {
932
+ struct MenuBarConfig {
933
+ let quitWithCodex: Bool
934
+ }
935
+
936
+ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
703
937
  var statusItem: NSStatusItem!
704
938
  var statusLineItem: NSMenuItem!
939
+ var codexLbItem: NSMenuItem!
940
+ var oauthItem: NSMenuItem!
705
941
  var timer: Timer?
942
+ var busy = false
943
+ var lastFailure = false
944
+ var quitWithCodex = false
706
945
 
707
946
  func applicationDidFinishLaunching(_ notification: Notification) {
708
947
  NSApp.setActivationPolicy(.accessory)
948
+ quitWithCodex = readConfig().quitWithCodex
709
949
  statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
710
950
  statusItem.autosaveName = "com.sneakoscope.sks-menubar"
711
- statusItem.isVisible = true
712
951
  if let button = statusItem.button {
713
952
  configureStatusButton(button, title: "SKS")
714
953
  }
715
954
 
716
955
  let menu = NSMenu()
956
+ menu.delegate = self
717
957
  statusLineItem = NSMenuItem(title: "SKS v\\(packageVersion) - starting", action: nil, keyEquivalent: "")
718
958
  statusLineItem.isEnabled = false
719
959
  menu.addItem(statusLineItem)
720
960
  menu.addItem(NSMenuItem.separator())
721
- add(menu, "Use codex-lb", #selector(useCodexLb))
722
- add(menu, "Use ChatGPT OAuth", #selector(useChatGptOAuth))
961
+ codexLbItem = add(menu, "Use codex-lb", #selector(useCodexLb))
962
+ oauthItem = add(menu, "Use ChatGPT OAuth", #selector(useChatGptOAuth))
723
963
  add(menu, "Set codex-lb Domain and Key", #selector(setCodexLbDomainAndKey))
724
964
  menu.addItem(NSMenuItem.separator())
725
965
  add(menu, "Set OpenRouter Key and GLM Profiles", #selector(setOpenRouterKey))
@@ -731,12 +971,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
731
971
  add(menu, "Open Codex Settings", #selector(openCodexSettings))
732
972
  add(menu, "Restart Codex", #selector(restartCodex))
733
973
  menu.addItem(NSMenuItem.separator())
974
+ add(menu, "View Last Log", #selector(viewLastLog))
734
975
  add(menu, "Quit SKS Menu", #selector(quit))
735
976
  statusItem.menu = menu
977
+ configureCodexLifecycleSync()
736
978
  updateState()
737
979
  timer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in self?.updateState() }
738
980
  }
739
981
 
982
+ func menuWillOpen(_ menu: NSMenu) {
983
+ updateState()
984
+ updateAuthModeChecks()
985
+ }
986
+
740
987
  func configureStatusButton(_ button: NSStatusBarButton, title: String) {
741
988
  button.image = nil
742
989
  button.title = title
@@ -752,10 +999,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
752
999
  button.setAccessibilityHelp("Open SKS menu")
753
1000
  }
754
1001
 
755
- func add(_ menu: NSMenu, _ title: String, _ selector: Selector) {
1002
+ func add(_ menu: NSMenu, _ title: String, _ selector: Selector) -> NSMenuItem {
756
1003
  let item = NSMenuItem(title: title, action: selector, keyEquivalent: "")
757
1004
  item.target = self
758
1005
  menu.addItem(item)
1006
+ return item
1007
+ }
1008
+
1009
+ ${codexLifecycleSource}
1010
+
1011
+ func readConfig() -> MenuBarConfig {
1012
+ guard let data = FileManager.default.contents(atPath: menubarConfigPath),
1013
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
1014
+ return MenuBarConfig(quitWithCodex: false)
1015
+ }
1016
+ return MenuBarConfig(quitWithCodex: json["quit_with_codex"] as? Bool == true)
759
1017
  }
760
1018
 
761
1019
  func updateState() {
@@ -767,20 +1025,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
767
1025
  }
768
1026
 
769
1027
  func readMenuState() -> MenuState {
770
- let actionTargetOk = actionScriptTargetExists()
771
- if !actionTargetOk {
1028
+ if codexBundleId == nil {
1029
+ return MenuState(title: "SKS", line: "Codex app not detected — sync disabled")
1030
+ }
1031
+ if busy {
1032
+ return MenuState(title: "SKS ⋯", line: "SKS v\\(packageVersion) - working")
1033
+ }
1034
+ if !actionScriptUsable() {
772
1035
  return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - action script broken (run sks doctor --fix)")
773
1036
  }
1037
+ if lastFailure {
1038
+ return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - last action failed")
1039
+ }
774
1040
  if updateAvailable() {
775
1041
  return MenuState(title: "SKS ↑", line: "SKS v\\(packageVersion) - update available")
776
1042
  }
777
1043
  return MenuState(title: "SKS", line: "SKS v\\(packageVersion) - OK")
778
1044
  }
779
1045
 
780
- func actionScriptTargetExists() -> Bool {
781
- guard let text = try? String(contentsOfFile: actionScript, encoding: .utf8) else { return false }
782
- guard let entry = shellAssignment(text, key: "SKS_ENTRY") else { return false }
783
- return FileManager.default.fileExists(atPath: entry)
1046
+ func actionScriptUsable() -> Bool {
1047
+ return FileManager.default.isExecutableFile(atPath: actionScript)
784
1048
  }
785
1049
 
786
1050
  func updateAvailable() -> Bool {
@@ -792,48 +1056,83 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
792
1056
  return latest != packageVersion && !latest.isEmpty
793
1057
  }
794
1058
 
795
- func shellAssignment(_ text: String, key: String) -> String? {
796
- for line in text.components(separatedBy: .newlines) {
797
- if line.hasPrefix(key + "=") {
798
- var value = String(line.dropFirst(key.count + 1))
799
- if value.hasPrefix("'") && value.hasSuffix("'") && value.count >= 2 {
800
- value.removeFirst()
801
- value.removeLast()
802
- return value.replacingOccurrences(of: "'\\\\''", with: "'")
1059
+ func runSksCapture(_ args: [String], title: String, stdinText: String? = nil, notify: Bool = true, completion: ((Int32, String) -> Void)? = nil) {
1060
+ busy = true
1061
+ lastFailure = false
1062
+ updateState()
1063
+ runProcess(actionScript, args, stdinText: stdinText) { [weak self] code, output in
1064
+ let redacted = redactSecrets(output, secrets: stdinText == nil ? [] : [stdinText ?? ""])
1065
+ writeActionLog("$ sks \\(args.joined(separator: " "))\\n\\(redacted)\\n")
1066
+ DispatchQueue.main.async {
1067
+ self?.busy = false
1068
+ self?.lastFailure = code != 0
1069
+ self?.updateState()
1070
+ if notify {
1071
+ if code == 0 {
1072
+ showNotification(title, "OK\\n" + redacted)
1073
+ } else {
1074
+ showAlert(title + " failed", informative: redacted)
1075
+ }
803
1076
  }
804
- return value
1077
+ completion?(code, redacted)
805
1078
  }
806
1079
  }
807
- return nil
808
1080
  }
809
1081
 
810
- func runSksInTerminal(_ args: [String], tail: String = "echo; echo 'SKS command finished.'") {
811
- let quoted = args.map(shellQuote).joined(separator: " ")
812
- runInTerminal("\\(shellQuote(actionScript)) \\(quoted); \\(tail)")
1082
+ func runSksBackground(_ args: [String], title: String, stdinText: String? = nil, completion: ((Int32, String) -> Void)? = nil) {
1083
+ runSksCapture(args, title: title, stdinText: stdinText, notify: true, completion: completion)
813
1084
  }
814
1085
 
815
- func runSksBackground(_ args: [String], title: String) {
816
- runProcess(actionScript, args) { code, output in
817
- let status = code == 0 ? "OK" : "failed (\\(code))"
818
- showNotification(title, status + "\\n" + output)
1086
+ func startSksDetached(_ args: [String], title: String) {
1087
+ let process = Process()
1088
+ process.executableURL = URL(fileURLWithPath: actionScript)
1089
+ process.arguments = args
1090
+ writeActionLog("$ sks \\(args.joined(separator: " "))\\nstarted\\n")
1091
+ if let handle = try? FileHandle(forWritingTo: URL(fileURLWithPath: lastActionLogPath)) {
1092
+ process.standardOutput = handle
1093
+ process.standardError = handle
1094
+ }
1095
+ do {
1096
+ try process.run()
1097
+ showNotification(title, "started")
1098
+ } catch {
1099
+ showAlert(title + " failed", informative: String(describing: error))
1100
+ }
1101
+ }
1102
+
1103
+ func updateAuthModeChecks() {
1104
+ runSksCapture(["codex-lb", "status", "--json"], title: "SKS Auth Status", notify: false) { [weak self] code, output in
1105
+ guard let self = self else { return }
1106
+ let lower = output.lowercased()
1107
+ let codexLbActive = code == 0 && (lower.contains(#""configured": true"#) || lower.contains(#""model_provider": "codex-lb""#) || lower.contains(#""mode": "codex-lb""#))
1108
+ self.codexLbItem.state = codexLbActive ? .on : .off
1109
+ self.oauthItem.state = codexLbActive ? .off : .on
819
1110
  }
820
1111
  }
821
1112
 
822
1113
  @objc func useCodexLb() {
823
- runSksInTerminal(["codex-lb", "use-codex-lb"])
1114
+ runSksBackground(["codex-lb", "use-codex-lb", "--json"], title: "Use codex-lb") { [weak self] code, _ in
1115
+ if code == 0 { self?.updateAuthModeChecks() }
1116
+ }
824
1117
  }
825
1118
 
826
1119
  @objc func useChatGptOAuth() {
827
- runSksInTerminal(["codex-lb", "use-oauth"])
1120
+ runSksBackground(["codex-lb", "use-oauth", "--json"], title: "Use ChatGPT OAuth") { [weak self] code, _ in
1121
+ if code == 0 { self?.updateAuthModeChecks() }
1122
+ }
828
1123
  }
829
1124
 
830
1125
  @objc func setCodexLbDomainAndKey() {
831
- runSksInTerminal(["codex-lb", "setup"])
1126
+ guard let domain = promptText(title: "Set codex-lb Domain", message: "Enter your codex-lb domain or base URL.", placeholder: "https://lb.example.com/backend-api/codex") else { return }
1127
+ guard let key = promptText(title: "Set codex-lb Key", message: "Enter your codex-lb API key.", placeholder: "sk-clb-...", secure: true) else { return }
1128
+ runSksBackground(["codex-lb", "setup", "--host", domain, "--api-key-stdin", "--yes", "--json"], title: "Set codex-lb", stdinText: key + "\\n") { [weak self] code, _ in
1129
+ if code == 0 { self?.updateAuthModeChecks() }
1130
+ }
832
1131
  }
833
1132
 
834
1133
  @objc func setOpenRouterKey() {
835
- let command = "printf 'Paste OpenRouter key, then press Return: '; read -r key; printf '%s\\\\n' \\"$key\\" | \\(shellQuote(actionScript)) codex-app set-openrouter-key --api-key-stdin; \\(shellQuote(actionScript)) codex-app glm-profile install; echo; echo 'OpenRouter/GLM update finished. Restart Codex if the model picker was already open.'"
836
- runInTerminal(command)
1134
+ guard let key = promptText(title: "Set OpenRouter Key", message: "Enter your OpenRouter API key.", placeholder: "sk-or-v1-...", secure: true) else { return }
1135
+ runSksBackground(["codex-app", "set-openrouter-key", "--api-key-stdin", "--json"], title: "Set OpenRouter Key", stdinText: key + "\\n")
837
1136
  }
838
1137
 
839
1138
  @objc func fastCheck() {
@@ -845,13 +1144,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
845
1144
  }
846
1145
 
847
1146
  @objc func updateSksNow() {
848
- runSksInTerminal(["update"], tail: "echo; echo 'SKS update finished.'")
1147
+ runSksBackground(["update"], title: "Update SKS Now")
849
1148
  }
850
1149
 
851
1150
  @objc func openDashboard() {
852
- let command = "\\(shellQuote(actionScript)) ui; echo; echo 'SKS dashboard requested.'"
853
- runInTerminal(command)
854
- runProcess("/usr/bin/open", ["http://127.0.0.1:4477"])
1151
+ let urlString = "http://127.0.0.1:4477"
1152
+ runProcess("/usr/bin/curl", ["-fsS", "--max-time", "1", urlString]) { [weak self] code, _ in
1153
+ DispatchQueue.main.async {
1154
+ if code != 0 {
1155
+ self?.startSksDetached(["ui"], title: "SKS Dashboard")
1156
+ }
1157
+ if let url = URL(string: urlString) {
1158
+ NSWorkspace.shared.open(url)
1159
+ }
1160
+ }
1161
+ }
855
1162
  }
856
1163
 
857
1164
  @objc func openCodexSettings() {
@@ -859,7 +1166,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
859
1166
  }
860
1167
 
861
1168
  @objc func restartCodex() {
862
- runInTerminal("/usr/bin/osascript -e 'tell application \\"Codex\\" to quit'; sleep 1; /usr/bin/open -a Codex; echo 'Codex restart requested.'")
1169
+ let running = NSWorkspace.shared.runningApplications.filter { app in
1170
+ if let bundle = codexBundleId, app.bundleIdentifier == bundle { return true }
1171
+ return app.localizedName == "Codex"
1172
+ }
1173
+ for app in running {
1174
+ app.terminate()
1175
+ }
1176
+ DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
1177
+ if let bundle = codexBundleId, let url = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundle) {
1178
+ NSWorkspace.shared.open(url)
1179
+ showNotification("Restart Codex", "requested")
1180
+ } else {
1181
+ showAlert("Restart Codex failed", informative: "Codex app bundle could not be resolved.")
1182
+ }
1183
+ }
1184
+ }
1185
+
1186
+ @objc func viewLastLog() {
1187
+ if FileManager.default.fileExists(atPath: lastActionLogPath) {
1188
+ NSWorkspace.shared.open(URL(fileURLWithPath: lastActionLogPath))
1189
+ } else {
1190
+ showAlert("No SKS menu log yet", informative: lastActionLogPath)
1191
+ }
863
1192
  }
864
1193
 
865
1194
  @objc func quit() {
@@ -1077,13 +1406,14 @@ async function inspectLaunchdService(env = process.env) {
1077
1406
  const text = `${result.stdout || ''}\n${result.stderr || ''}`;
1078
1407
  const state = text.match(/\bstate = ([^\n]+)/)?.[1]?.trim() || null;
1079
1408
  const pidText = text.match(/\bpid = (\d+)/)?.[1] || null;
1409
+ const running = result.code === 0 && (state === 'running' || Boolean(pidText));
1080
1410
  return {
1081
1411
  checked: true,
1082
- ok: result.code === 0,
1412
+ ok: running,
1083
1413
  service,
1084
1414
  state,
1085
1415
  pid: pidText ? Number(pidText) : null,
1086
- error: result.code === 0 ? null : String(result.stderr || result.stdout || '').trim()
1416
+ error: running ? null : String(result.stderr || result.stdout || state || 'launchd_not_running').trim()
1087
1417
  };
1088
1418
  }
1089
1419
  async function inspectSignature(appPath, env = process.env) {
@@ -1175,6 +1505,8 @@ async function writeReport(reportPath, result) {
1175
1505
  }
1176
1506
  catch (err) {
1177
1507
  result.report_write_failed = true;
1508
+ if (!result.warnings.includes('menubar_report_write_failed'))
1509
+ result.warnings.push('menubar_report_write_failed');
1178
1510
  console.error(`warning: failed to write SKS menubar report ${reportPath}: ${err?.message || err}`);
1179
1511
  }
1180
1512
  }