sneakoscope 4.8.7 → 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 (261) hide show
  1. package/README.md +51 -726
  2. package/config/bench-baseline.json +7 -0
  3. package/crates/sks-core/Cargo.lock +1 -1
  4. package/crates/sks-core/Cargo.toml +1 -1
  5. package/crates/sks-core/src/main.rs +1 -1
  6. package/dist/bin/install.js +35 -0
  7. package/dist/bin/sks.js +1 -1
  8. package/dist/cli/cli-theme.js +51 -0
  9. package/dist/cli/command-registry.js +15 -4
  10. package/dist/cli/help-fast.js +18 -8
  11. package/dist/cli/insane-search-command.js +36 -8
  12. package/dist/cli/install-helpers.js +32 -0
  13. package/dist/commands/codex-lb.js +2 -0
  14. package/dist/commands/doctor.js +94 -9
  15. package/dist/commands/proof.js +6 -3
  16. package/dist/config/skills-manifest.json +82 -54
  17. package/dist/core/agents/agent-conflict-graph.js +5 -0
  18. package/dist/core/agents/agent-lease.js +5 -0
  19. package/dist/core/agents/agent-ledger-schemas.js +1 -0
  20. package/dist/core/agents/agent-lifecycle.js +5 -3
  21. package/dist/core/agents/agent-orchestrator.js +268 -62
  22. package/dist/core/agents/agent-output-validator.js +14 -3
  23. package/dist/core/agents/agent-patch-queue-store.js +95 -2
  24. package/dist/core/agents/agent-patch-queue.js +6 -3
  25. package/dist/core/agents/agent-patch-schema.js +13 -0
  26. package/dist/core/agents/agent-proof-evidence.js +6 -1
  27. package/dist/core/agents/agent-runner-fake.js +40 -0
  28. package/dist/core/agents/agent-worker-pipeline.js +36 -2
  29. package/dist/core/agents/codex-exec-worker-adapter.js +2 -0
  30. package/dist/core/agents/native-cli-worker.js +0 -1
  31. package/dist/core/agents/native-worker-backend-router.js +58 -1
  32. package/dist/core/codex-app/sks-menubar.js +477 -132
  33. package/dist/core/codex-app.js +1 -0
  34. package/dist/core/codex-control/codex-sdk-config-policy.js +2 -1
  35. package/dist/core/codex-control/codex-sdk-env-policy.js +1 -1
  36. package/dist/core/codex-control/schemas/agent-worker-result.schema.js +12 -3
  37. package/dist/core/codex-hooks/codex-hook-managed-install.js +5 -0
  38. package/dist/core/codex-hooks/codex-hook-state-writer.js +70 -11
  39. package/dist/core/codex-hooks/codex-hook-trust-doctor.js +2 -2
  40. package/dist/core/codex-lb/codex-lb-env.js +20 -1
  41. package/dist/core/commands/basic-cli.js +18 -1
  42. package/dist/core/commands/check-command.js +37 -5
  43. package/dist/core/commands/computer-use-command.js +62 -7
  44. package/dist/core/commands/db-command.js +31 -5
  45. package/dist/core/commands/fast-mode-command.js +3 -0
  46. package/dist/core/commands/gate-result-contract.js +43 -0
  47. package/dist/core/commands/gates-command.js +6 -1
  48. package/dist/core/commands/gc-command.js +29 -2
  49. package/dist/core/commands/goal-command.js +9 -2
  50. package/dist/core/commands/gx-command.js +79 -7
  51. package/dist/core/commands/image-ux-review-command.js +188 -13
  52. package/dist/core/commands/mad-db-command.js +85 -176
  53. package/dist/core/commands/mad-sks-command.js +230 -29
  54. package/dist/core/commands/menubar-command.js +24 -2
  55. package/dist/core/commands/naruto-command.js +64 -10
  56. package/dist/core/commands/plan-command.js +76 -0
  57. package/dist/core/commands/ppt-command.js +159 -24
  58. package/dist/core/commands/qa-loop-command.js +6 -2
  59. package/dist/core/commands/release-command.js +55 -2
  60. package/dist/core/commands/research-command.js +1 -1
  61. package/dist/core/commands/review-command.js +217 -0
  62. package/dist/core/commands/route-success-helpers.js +59 -0
  63. package/dist/core/commands/run-command.js +19 -7
  64. package/dist/core/commands/seo-command.js +49 -1
  65. package/dist/core/commands/ui-command.js +161 -0
  66. package/dist/core/commands/uninstall-command.js +15 -1
  67. package/dist/core/db-safety.js +2 -2
  68. package/dist/core/doctor/doctor-dirty-planner.js +51 -7
  69. package/dist/core/doctor/doctor-transaction.js +8 -0
  70. package/dist/core/doctor/doctor-zellij-repair.js +1 -0
  71. package/dist/core/doctor/imagegen-repair.js +161 -0
  72. package/dist/core/feature-fixture-runner.js +2 -2
  73. package/dist/core/feature-fixtures.js +58 -37
  74. package/dist/core/feature-registry.js +102 -2
  75. package/dist/core/fsx.js +39 -6
  76. package/dist/core/image-ux-review/imagegen-adapter.js +48 -11
  77. package/dist/core/image-ux-review.js +16 -0
  78. package/dist/core/imagegen/imagegen-capability.js +11 -5
  79. package/dist/core/imagegen/require-imagegen.js +57 -0
  80. package/dist/core/init/skills.js +10 -5
  81. package/dist/core/mad-db/mad-db-coordinator.js +94 -18
  82. package/dist/core/mad-db/mad-db-policy.js +12 -10
  83. package/dist/core/mad-sks/executors/executor-base.js +8 -2
  84. package/dist/core/mad-sks/executors/index.js +4 -0
  85. package/dist/core/mad-sks/executors/sql-plane-executor.js +194 -0
  86. package/dist/core/naruto/naruto-active-pool.js +15 -4
  87. package/dist/core/naruto/naruto-real-worker-child.js +11 -1
  88. package/dist/core/naruto/naruto-role-policy.js +3 -0
  89. package/dist/core/naruto/naruto-task-hints.js +10 -0
  90. package/dist/core/naruto/naruto-work-graph.js +8 -2
  91. package/dist/core/naruto/naruto-work-item.js +6 -0
  92. package/dist/core/naruto/solution-tournament.js +101 -0
  93. package/dist/core/permission-gates.js +30 -0
  94. package/dist/core/pipeline-internals/runtime-core.js +11 -3
  95. package/dist/core/pipeline-internals/runtime-gates.js +27 -0
  96. package/dist/core/ppt-review/index.js +6 -1
  97. package/dist/core/ppt-review/slide-imagegen-review.js +75 -10
  98. package/dist/core/ppt.js +70 -2
  99. package/dist/core/proof/auto-finalize.js +70 -11
  100. package/dist/core/proof/proof-schema.js +2 -0
  101. package/dist/core/proof/route-adapter.js +5 -1
  102. package/dist/core/proof/route-finalizer.js +8 -5
  103. package/dist/core/proof/selftest-proof-fixtures.js +18 -5
  104. package/dist/core/proof/validation.js +2 -0
  105. package/dist/core/provider/model-router.js +53 -0
  106. package/dist/core/release/release-gate-node.js +3 -0
  107. package/dist/core/release/release-gate-resource-governor.js +1 -0
  108. package/dist/core/routes/constants.js +1 -1
  109. package/dist/core/routes.js +65 -18
  110. package/dist/core/safety/mutation-guard.js +5 -1
  111. package/dist/core/stop-gate/gate-evaluator.js +102 -0
  112. package/dist/core/stop-gate/stop-gate-check.js +16 -1
  113. package/dist/core/stop-gate/stop-gate-writer.js +4 -0
  114. package/dist/core/triwiki/agents-md-projector.js +184 -0
  115. package/dist/core/triwiki-wrongness/wrongness-schema.js +3 -1
  116. package/dist/core/trust-kernel/trust-kernel-schema.js +1 -0
  117. package/dist/core/trust-kernel/trust-report.js +1 -1
  118. package/dist/core/trust-kernel/trust-status.js +2 -0
  119. package/dist/core/ui/dashboard-html.js +111 -0
  120. package/dist/core/update/update-migration-state.js +386 -15
  121. package/dist/core/update-check.js +178 -81
  122. package/dist/core/verification/diff-quality.js +100 -0
  123. package/dist/core/verification/impact-scan.js +164 -0
  124. package/dist/core/verification/machine-feedback.js +146 -0
  125. package/dist/core/verification/mistake-rule-compiler.js +195 -0
  126. package/dist/core/version.js +1 -1
  127. package/dist/core/zellij/zellij-self-heal.js +9 -6
  128. package/dist/core/zellij/zellij-slot-telemetry.js +1 -1
  129. package/dist/scripts/check-feature-quality.js +2 -2
  130. package/dist/scripts/cli-output-consistency-check.js +57 -0
  131. package/dist/scripts/coding-bench-check.js +136 -0
  132. package/dist/scripts/doctor-imagegen-repair-check.js +62 -0
  133. package/dist/scripts/ensure-bin-executable.js +11 -3
  134. package/dist/scripts/harness-benchmark-check.js +104 -0
  135. package/dist/scripts/legacy-update-e2e-check.js +228 -0
  136. package/dist/scripts/mad-db-command-check.js +11 -8
  137. package/dist/scripts/mad-db-route-identity-check.js +8 -6
  138. package/dist/scripts/mad-db-safety-conflict-matrix-check.js +31 -8
  139. package/dist/scripts/mad-db-skill-policy-snapshot-check.js +4 -5
  140. package/dist/scripts/naruto-real-active-pool-runtime-check.js +26 -1
  141. package/dist/scripts/ppt-real-imagegen-wiring-check.js +5 -1
  142. package/dist/scripts/proof-root-cause-policy-check.js +2 -0
  143. package/dist/scripts/release-dag-full-coverage-check.js +13 -4
  144. package/dist/scripts/release-gate-dag-runner.js +14 -0
  145. package/dist/scripts/release-gate-existence-audit.js +2 -1
  146. package/dist/scripts/release-gate-planner.js +2 -1
  147. package/dist/scripts/release-gate-script-parity-check.js +45 -17
  148. package/dist/scripts/release-metadata-1-19-check.js +2 -1
  149. package/dist/scripts/seo-geo-feature-fixture-quality-check.js +1 -1
  150. package/dist/scripts/sks-1-11-gate-lib.js +8 -6
  151. package/dist/scripts/sks-menubar-install-check.js +57 -20
  152. package/dist/scripts/ux-review-real-loop-fixture-check.js +9 -4
  153. package/dist/scripts/ux-review-run-wires-imagegen-check.js +4 -0
  154. package/dist/scripts/zellij-slot-telemetry-runtime-check.js +1 -0
  155. package/docs/assets/sneakoscope-architecture-pipeline.jpg +0 -0
  156. package/docs/demo.tape +28 -0
  157. package/package.json +31 -24
  158. package/schemas/codex/completion-proof.schema.json +3 -2
  159. package/schemas/release/release-gate-node.schema.json +2 -1
  160. package/dist/scripts/agent-backfill-route-blackbox.js +0 -5
  161. package/dist/scripts/agent-dynamic-pool-route-blackbox.js +0 -5
  162. package/dist/scripts/agent-parallel-write-blackbox.js +0 -56
  163. package/dist/scripts/agent-patch-swarm-route-blackbox.js +0 -10
  164. package/dist/scripts/agent-route-blackbox-lib.js +0 -132
  165. package/dist/scripts/blackbox-command-import-smoke.js +0 -143
  166. package/dist/scripts/blackbox-global-shim.js +0 -77
  167. package/dist/scripts/blackbox-matrix.js +0 -70
  168. package/dist/scripts/blackbox-npx-one-shot.js +0 -69
  169. package/dist/scripts/blackbox-pack-install.js +0 -174
  170. package/dist/scripts/brand-neutrality-zero-leakage-blackbox.js +0 -4
  171. package/dist/scripts/build-once-runner-blackbox.js +0 -34
  172. package/dist/scripts/codex-0140-integration-blackbox.js +0 -13
  173. package/dist/scripts/codex-agent-type-blackbox.js +0 -4
  174. package/dist/scripts/codex-app-harness-blackbox.js +0 -4
  175. package/dist/scripts/codex-app-skill-agent-blackbox.js +0 -4
  176. package/dist/scripts/codex-hook-approval-blackbox.js +0 -4
  177. package/dist/scripts/codex-init-deep-directory-local-blackbox.js +0 -4
  178. package/dist/scripts/codex-native-feature-broker-blackbox.js +0 -4
  179. package/dist/scripts/codex-native-pattern-analysis-blackbox.js +0 -4
  180. package/dist/scripts/codex-native-read-repair-split-blackbox.js +0 -55
  181. package/dist/scripts/codex-native-reference-cache-blackbox.js +0 -41
  182. package/dist/scripts/core-skill-integrity-blackbox.js +0 -32
  183. package/dist/scripts/dfix-fast-blackbox-check.js +0 -37
  184. package/dist/scripts/dfix-parallel-write-blackbox.js +0 -48
  185. package/dist/scripts/dfix-patch-swarm-route-blackbox.js +0 -10
  186. package/dist/scripts/doctor-dirty-repair-blackbox.js +0 -22
  187. package/dist/scripts/doctor-dirty-semantic-blackbox.js +0 -8
  188. package/dist/scripts/doctor-native-capability-repair-blackbox.js +0 -39
  189. package/dist/scripts/doctor-transaction-engine-blackbox.js +0 -28
  190. package/dist/scripts/doctor-zellij-fix-blackbox.js +0 -4
  191. package/dist/scripts/doctor-zellij-no-homebrew-blackbox.js +0 -4
  192. package/dist/scripts/doctor-zellij-upgrade-blackbox.js +0 -4
  193. package/dist/scripts/gate-pack-runner-blackbox.js +0 -27
  194. package/dist/scripts/gate-pack-v2-blackbox.js +0 -18
  195. package/dist/scripts/geo-cli-blackbox-check.js +0 -18
  196. package/dist/scripts/loop-collision-blackbox.js +0 -3
  197. package/dist/scripts/loop-concurrency-oversubscription-blackbox.js +0 -3
  198. package/dist/scripts/loop-fixture-production-misuse-blackbox.js +0 -3
  199. package/dist/scripts/loop-kill-interrupt-real-blackbox.js +0 -3
  200. package/dist/scripts/loop-merge-strategy-blackbox.js +0 -3
  201. package/dist/scripts/loop-mesh-production-e2e-blackbox.js +0 -3
  202. package/dist/scripts/loop-side-effect-blackbox.js +0 -3
  203. package/dist/scripts/mad-db-operation-lifecycle-blackbox.js +0 -29
  204. package/dist/scripts/mad-sks-actual-executor-blackbox.js +0 -5
  205. package/dist/scripts/mad-zellij-headless-fallback-blackbox.js +0 -4
  206. package/dist/scripts/mad-zellij-self-heal-blackbox.js +0 -4
  207. package/dist/scripts/naruto-loop-mesh-blackbox.js +0 -3
  208. package/dist/scripts/naruto-real-parallelism-blackbox.js +0 -307
  209. package/dist/scripts/naruto-worktree-coding-blackbox.js +0 -29
  210. package/dist/scripts/parallel-runtime-real-blackbox.js +0 -44
  211. package/dist/scripts/pipeline-codex-native-doctor-mad-routing-real-blackbox.js +0 -77
  212. package/dist/scripts/pipeline-codex-native-e2e-blackbox.js +0 -13
  213. package/dist/scripts/pipeline-codex-native-image-routing-real-blackbox.js +0 -50
  214. package/dist/scripts/pipeline-codex-native-loop-routing-real-blackbox.js +0 -74
  215. package/dist/scripts/pipeline-codex-native-qa-routing-real-blackbox.js +0 -51
  216. package/dist/scripts/pipeline-codex-native-research-routing-real-blackbox.js +0 -45
  217. package/dist/scripts/pipeline-execution-profile-routing-blackbox.js +0 -4
  218. package/dist/scripts/postinstall-global-doctor-blackbox.js +0 -12
  219. package/dist/scripts/ppt-full-e2e-blackbox-check.js +0 -109
  220. package/dist/scripts/ppt-imagegen-blackbox-check.js +0 -46
  221. package/dist/scripts/qa-backfill-route-blackbox.js +0 -5
  222. package/dist/scripts/qa-patch-swarm-route-blackbox.js +0 -10
  223. package/dist/scripts/release-full-parallelism-blackbox.js +0 -41
  224. package/dist/scripts/release-triwiki-first-runner-blackbox.js +0 -48
  225. package/dist/scripts/release-wiring-3110-blackbox.js +0 -27
  226. package/dist/scripts/release-wiring-3112-blackbox.js +0 -17
  227. package/dist/scripts/release-wiring-3113-blackbox.js +0 -17
  228. package/dist/scripts/research-backfill-route-blackbox.js +0 -5
  229. package/dist/scripts/research-final-reviewer-blackbox.js +0 -70
  230. package/dist/scripts/research-stage-cycle-runtime-blackbox.js +0 -40
  231. package/dist/scripts/research-synthesis-writer-blackbox.js +0 -24
  232. package/dist/scripts/route-blackbox-realism-check.js +0 -21
  233. package/dist/scripts/scheduler-resource-claim-blackbox.js +0 -24
  234. package/dist/scripts/seo-cli-blackbox-check.js +0 -18
  235. package/dist/scripts/sks-3110-all-feature-regression-blackbox.js +0 -116
  236. package/dist/scripts/sks-3112-all-feature-regression-blackbox.js +0 -29
  237. package/dist/scripts/sks-3113-all-feature-regression-blackbox.js +0 -17
  238. package/dist/scripts/sks-400-all-feature-regression-blackbox.js +0 -21
  239. package/dist/scripts/sks-400-extreme-parallel-blackbox.js +0 -8
  240. package/dist/scripts/sks-400-five-minute-blackbox.js +0 -9
  241. package/dist/scripts/sks-400-legacy-purge-blackbox.js +0 -8
  242. package/dist/scripts/sks-401-all-feature-regression-blackbox.js +0 -46
  243. package/dist/scripts/sks-401-five-minute-actual-blackbox.js +0 -23
  244. package/dist/scripts/sks-402-all-feature-regression-blackbox.js +0 -9
  245. package/dist/scripts/sks-402-five-minute-real-blackbox.js +0 -22
  246. package/dist/scripts/sksd-warm-cache-blackbox.js +0 -12
  247. package/dist/scripts/team-backfill-route-blackbox.js +0 -5
  248. package/dist/scripts/team-parallel-write-blackbox.js +0 -55
  249. package/dist/scripts/team-patch-swarm-route-blackbox.js +0 -10
  250. package/dist/scripts/triwiki-affected-graph-blackbox.js +0 -28
  251. package/dist/scripts/triwiki-proof-bank-blackbox.js +0 -30
  252. package/dist/scripts/triwiki-proof-bank-lock-blackbox.js +0 -7
  253. package/dist/scripts/ux-patch-swarm-route-blackbox.js +0 -10
  254. package/dist/scripts/ux-review-imagegen-blackbox-check.js +0 -67
  255. package/dist/scripts/zellij-initial-main-only-blackbox.js +0 -28
  256. package/dist/scripts/zellij-pane-lock-concurrency-blackbox.js +0 -80
  257. package/dist/scripts/zellij-pane-lock-open-worker-integration-blackbox.js +0 -137
  258. package/dist/scripts/zellij-self-heal-typed-blackbox.js +0 -4
  259. package/dist/scripts/zellij-slot-telemetry-real-blackbox.js +0 -20
  260. package/dist/scripts/zellij-stacked-fallback-integration-blackbox.js +0 -81
  261. package/dist/scripts/zellij-worker-pane-real-ui-blackbox.js +0 -202
@@ -3,6 +3,8 @@ 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';
7
+ import { withHeartbeat } from '../../cli/cli-theme.js';
6
8
  export const SKS_MENUBAR_LABEL = 'com.sneakoscope.sks-menubar';
7
9
  const LABEL = SKS_MENUBAR_LABEL;
8
10
  const CONTROL_CENTER_DOMAIN = 'com.apple.controlcenter';
@@ -16,8 +18,10 @@ const MENU_ITEMS = [
16
18
  'Fast Check',
17
19
  'SKS Version Check',
18
20
  'Update SKS Now',
21
+ 'Open Dashboard',
19
22
  'Open Codex Settings',
20
23
  'Restart Codex',
24
+ 'View Last Log',
21
25
  'Quit SKS Menu'
22
26
  ];
23
27
  export function sksMenuBarPaths(homeInput, rootInput) {
@@ -41,10 +45,13 @@ export function sksMenuBarPaths(homeInput, rootInput) {
41
45
  info_plist_path: path.join(contentsPath, 'Info.plist'),
42
46
  action_script_path: path.join(installDir, 'sks-menubar-action.sh'),
43
47
  build_stamp_path: path.join(installDir, 'build-stamp.json'),
48
+ config_path: path.join(installDir, 'config.json'),
44
49
  launch_agent_path: path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`),
45
50
  report_path: path.join(root, '.sneakoscope', 'reports', 'sks-menubar.json'),
46
51
  stdout_log_path: path.join(installDir, 'menubar.out.log'),
47
- 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')
48
55
  };
49
56
  }
50
57
  export async function installSksMenuBar(opts = {}) {
@@ -55,6 +62,7 @@ export async function installSksMenuBar(opts = {}) {
55
62
  const warnings = [];
56
63
  const nextActions = defaultNextActions();
57
64
  let secretEnvCleanup;
65
+ let codexBundleId = null;
58
66
  if (process.platform !== 'darwin') {
59
67
  const result = {
60
68
  schema: 'sks.codex-app-sks-menubar.v1',
@@ -67,7 +75,9 @@ export async function installSksMenuBar(opts = {}) {
67
75
  launch_agent_path: null,
68
76
  action_script_path: null,
69
77
  build_stamp_path: null,
78
+ config_path: null,
70
79
  report_path: apply ? paths.report_path : null,
80
+ codex_bundle_id: null,
71
81
  menu_items: MENU_ITEMS,
72
82
  actions: [],
73
83
  launch: { requested: false, method: 'none', ok: true },
@@ -84,9 +94,10 @@ export async function installSksMenuBar(opts = {}) {
84
94
  const installed = await exists(paths.executable_path);
85
95
  const launchAgent = await exists(paths.launch_agent_path);
86
96
  const status = await inspectSksMenuBarStatus({ home: paths.home, root: paths.root }).catch(() => null);
97
+ const statusWarnings = [...(status?.warnings || []), ...(launchAgent ? [] : ['launch_agent_not_installed_yet'])];
87
98
  return {
88
99
  schema: 'sks.codex-app-sks-menubar.v1',
89
- ok: true,
100
+ ok: status ? status.ok === true : true,
90
101
  apply,
91
102
  status: 'planned',
92
103
  platform: process.platform,
@@ -95,7 +106,9 @@ export async function installSksMenuBar(opts = {}) {
95
106
  launch_agent_path: paths.launch_agent_path,
96
107
  action_script_path: paths.action_script_path,
97
108
  build_stamp_path: paths.build_stamp_path,
109
+ config_path: paths.config_path,
98
110
  report_path: paths.report_path,
111
+ codex_bundle_id: status?.codex_sync.bundle_id || null,
99
112
  menu_items: MENU_ITEMS,
100
113
  actions: installed ? ['menubar_app_present'] : ['menubar_app_install_available'],
101
114
  launch: {
@@ -114,11 +127,12 @@ export async function installSksMenuBar(opts = {}) {
114
127
  build_stamp: status?.build_stamp || null,
115
128
  tcc_automation_status: 'unknown',
116
129
  next_actions: launchAgent ? defaultNextActions() : ['Run: sks menubar install'],
117
- blockers: [],
118
- warnings: launchAgent ? [] : ['launch_agent_not_installed_yet']
130
+ blockers: status?.blockers || [],
131
+ warnings: statusWarnings
119
132
  };
120
133
  }
121
134
  await ensureDir(paths.install_dir);
135
+ await ensureDir(paths.logs_dir);
122
136
  await ensureDir(path.dirname(paths.launch_agent_path));
123
137
  secretEnvCleanup = await cleanupMacLaunchSecretEnvironment({ env }).catch((err) => ({
124
138
  ok: false,
@@ -137,9 +151,17 @@ export async function installSksMenuBar(opts = {}) {
137
151
  const open = env.SKS_MENUBAR_OPEN || await which('open').catch(() => null) || await fallbackTool('/usr/bin/open');
138
152
  const codesign = env.SKS_MENUBAR_CODESIGN || await which('codesign').catch(() => null) || await fallbackTool('/usr/bin/codesign');
139
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');
140
157
  if (!swiftc)
141
158
  return await blockedResult('swiftc_missing', 'swiftc not found');
142
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}`);
143
165
  const target = await resolveSksEntryForInstall({
144
166
  ...(opts.sksEntry ? { explicit: opts.sksEntry } : {}),
145
167
  root: paths.root,
@@ -160,6 +182,9 @@ export async function installSksMenuBar(opts = {}) {
160
182
  const swiftSource = swiftMenuSource({
161
183
  actionScriptPath: paths.action_script_path,
162
184
  buildStampPath: paths.build_stamp_path,
185
+ configPath: paths.config_path,
186
+ lastActionLogPath: paths.last_action_log_path,
187
+ codexBundleId: config.codex_bundle_id,
163
188
  packageVersion: PACKAGE_VERSION
164
189
  });
165
190
  const infoPlist = infoPlistSource(PACKAGE_VERSION);
@@ -198,9 +223,6 @@ export async function installSksMenuBar(opts = {}) {
198
223
  }
199
224
  }
200
225
  if (!stampMatches && !binaryStable) {
201
- const clt = await xcodeCltStatus(xcodeSelect);
202
- if (!clt.ok)
203
- return await blockedResult('xcode_clt_missing', clt.error || 'Xcode Command Line Tools missing');
204
226
  try {
205
227
  await buildMenuBarAppAtomically({
206
228
  paths,
@@ -208,7 +230,8 @@ export async function installSksMenuBar(opts = {}) {
208
230
  codesign,
209
231
  swiftSource,
210
232
  infoPlist,
211
- actions
233
+ actions,
234
+ quiet: opts.quiet === true
212
235
  });
213
236
  }
214
237
  catch (err) {
@@ -245,7 +268,7 @@ export async function installSksMenuBar(opts = {}) {
245
268
  warnings.push(preferredPosition.warning);
246
269
  }
247
270
  const launch = launchRequested && launchctl && !(stampMatches && runningBeforeLaunch)
248
- ? await launchWithLaunchctl({ launchctl, open, paths, skipIfRunning: stampMatches })
271
+ ? await launchWithLaunchctl({ launchctl, open, paths, skipIfRunning: stampMatches, quiet: opts.quiet === true })
249
272
  : {
250
273
  requested: launchRequested && !(stampMatches && runningBeforeLaunch),
251
274
  method: 'skipped',
@@ -274,7 +297,9 @@ export async function installSksMenuBar(opts = {}) {
274
297
  launch_agent_path: paths.launch_agent_path,
275
298
  action_script_path: paths.action_script_path,
276
299
  build_stamp_path: paths.build_stamp_path,
300
+ config_path: paths.config_path,
277
301
  report_path: paths.report_path,
302
+ codex_bundle_id: codexBundleId,
278
303
  menu_items: MENU_ITEMS,
279
304
  actions,
280
305
  launch,
@@ -282,10 +307,7 @@ export async function installSksMenuBar(opts = {}) {
282
307
  build_stamp: stamp,
283
308
  tcc_automation_status: 'unknown',
284
309
  secret_env_cleanup: secretEnvCleanup,
285
- next_actions: [
286
- ...nextActions,
287
- 'If Terminal automation was denied: System Settings > Privacy & Security > Automation > SKS Menu Bar > Terminal.'
288
- ],
310
+ next_actions: nextActions,
289
311
  blockers: ok ? [] : [launch.error || 'sks_menubar_launch_failed'],
290
312
  warnings
291
313
  };
@@ -303,7 +325,9 @@ export async function installSksMenuBar(opts = {}) {
303
325
  launch_agent_path: paths.launch_agent_path,
304
326
  action_script_path: paths.action_script_path,
305
327
  build_stamp_path: paths.build_stamp_path,
328
+ config_path: paths.config_path,
306
329
  report_path: paths.report_path,
330
+ codex_bundle_id: typeof codexBundleId === 'string' ? codexBundleId : null,
307
331
  menu_items: MENU_ITEMS,
308
332
  actions,
309
333
  launch: { requested: false, method: 'none', ok: false, error: detail || reason },
@@ -355,6 +379,84 @@ export async function cleanupMacLaunchSecretEnvironment(opts = {}) {
355
379
  next_actions: ['Rotate CODEX_LB_API_KEY and OPENROUTER_API_KEY if they were previously exposed in launchd.']
356
380
  };
357
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
+ }
358
460
  export async function inspectSksMenuBarStatus(opts = {}) {
359
461
  const paths = sksMenuBarPaths(opts.home || opts.env?.HOME, opts.root);
360
462
  const installed = await exists(paths.executable_path);
@@ -364,6 +466,16 @@ export async function inspectSksMenuBarStatus(opts = {}) {
364
466
  const sksEntry = shellAssignment(actionText, 'SKS_ENTRY');
365
467
  const nodeExists = nodeBin ? await isExecutable(nodeBin) : false;
366
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
+ };
367
479
  const buildStamp = await readJson(paths.build_stamp_path, null);
368
480
  const launchd = await inspectLaunchdService(opts.env);
369
481
  const signature = await inspectSignature(paths.app_path, opts.env);
@@ -371,10 +483,14 @@ export async function inspectSksMenuBarStatus(opts = {}) {
371
483
  const warnings = [];
372
484
  if (!installed)
373
485
  blockers.push('menubar_app_missing');
374
- if (installed && !sksEntryExists)
375
- 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');
376
490
  if (installed && signature.checked && !signature.ok)
377
491
  warnings.push('codesign_identifier_unexpected');
492
+ if (!codexSync.ok)
493
+ warnings.push('codex_sync_disabled');
378
494
  if (buildStamp?.package_version && buildStamp.package_version !== PACKAGE_VERSION)
379
495
  warnings.push('build_stamp_package_version_mismatch');
380
496
  return {
@@ -390,8 +506,12 @@ export async function inspectSksMenuBarStatus(opts = {}) {
390
506
  node_exists: nodeExists,
391
507
  sks_entry: sksEntry,
392
508
  sks_entry_exists: sksEntryExists,
393
- ok: Boolean(nodeExists && sksEntryExists)
509
+ smoke_code: actionSmoke.code,
510
+ smoke_output: actionSmoke.output,
511
+ version_detected: actionSmoke.versionDetected,
512
+ ok: actionSmoke.ok
394
513
  },
514
+ codex_sync: codexSync,
395
515
  build_stamp: buildStamp,
396
516
  package_version: PACKAGE_VERSION,
397
517
  signature,
@@ -540,16 +660,7 @@ async function resolveSksEntryForInstall(input) {
540
660
  let existsResolved = await exists(resolved);
541
661
  let projectLocal = isSubpath(resolved, input.root);
542
662
  if (projectLocal) {
543
- const globalCandidate = await findGlobalSksEntry(input.home, input.env, packaged);
544
- if (globalCandidate && globalCandidate !== resolved) {
545
- input.warnings.push('sks_entry_project_local_ignored_global_package_used');
546
- resolved = globalCandidate;
547
- existsResolved = true;
548
- projectLocal = isSubpath(resolved, input.root);
549
- }
550
- else {
551
- input.warnings.push('sks_entry_resolved_under_project_root');
552
- }
663
+ input.warnings.push('sks_entry_project_local');
553
664
  }
554
665
  const usedPreviousScript = !existsResolved && await exists(input.actionScriptPath);
555
666
  if (!existsResolved && usedPreviousScript)
@@ -563,81 +674,195 @@ async function resolveSksEntryForInstall(input) {
563
674
  used_previous_script: usedPreviousScript
564
675
  };
565
676
  }
566
- async function findGlobalSksEntry(home, env, packaged) {
567
- const candidates = new Set();
568
- const nodeRoot = path.resolve(path.dirname(process.execPath), '..');
569
- candidates.add(path.join(nodeRoot, 'lib', 'node_modules', 'sneakoscope', 'dist', 'bin', 'sks.js'));
570
- candidates.add(path.join(home, '.nvm', 'versions', 'node'));
571
- const nvmRoot = path.join(home, '.nvm', 'versions', 'node');
572
- try {
573
- const versions = await fs.readdir(nvmRoot);
574
- for (const version of versions.sort().reverse()) {
575
- candidates.add(path.join(nvmRoot, version, 'lib', 'node_modules', 'sneakoscope', 'dist', 'bin', 'sks.js'));
576
- }
577
- }
578
- catch { }
579
- if (env.SKS_GLOBAL_ROOT)
580
- candidates.add(path.join(path.resolve(env.SKS_GLOBAL_ROOT), 'dist', 'bin', 'sks.js'));
581
- for (const candidate of candidates) {
582
- if (candidate === nvmRoot)
583
- continue;
584
- if (path.resolve(candidate) === path.resolve(packaged))
585
- continue;
586
- if (await exists(candidate))
587
- return path.resolve(candidate);
588
- }
589
- return null;
590
- }
591
- function actionScriptSource(input) {
677
+ export function actionScriptSource(input) {
592
678
  return `#!/bin/zsh
593
679
  set -e
594
680
  export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
595
681
  NODE_BIN=${shellQuote(input.nodeBin)}
596
682
  SKS_ENTRY=${shellQuote(input.sksEntry)}
597
- if [ -x "$NODE_BIN" ] && [ -f "$SKS_ENTRY" ]; then
598
- 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
599
730
  fi
600
- for cand in "$HOME"/.nvm/versions/node/*/bin/node(Nn[-1]); do
601
- if [ -x "$cand" ] && [ -f "$SKS_ENTRY" ]; then
602
- 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
603
735
  fi
604
736
  done
605
- if /bin/zsh -lc 'command -v sks' >/dev/null 2>&1; then
606
- exec /bin/zsh -lc "sks $(printf '%q ' "$@")"
607
- fi
737
+ run_node_entry "$SKS_ENTRY" "$@" || true
738
+ notify_sks_missing
608
739
  echo "SKS command not found. Run npm install -g sneakoscope or sks doctor --fix, then try again." >&2
609
740
  exit 127
610
741
  `;
611
742
  }
612
- 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
+ `;
613
793
  return `import Cocoa
614
794
  import Foundation
615
795
 
616
796
  let actionScript = ${swiftString(input.actionScriptPath)}
617
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'}
618
801
  let packageVersion = ${swiftString(input.packageVersion)}
619
802
 
620
803
  func shellQuote(_ value: String) -> String {
621
804
  return "'" + value.replacingOccurrences(of: "'", with: "'\\\\''") + "'"
622
805
  }
623
806
 
807
+ func clipped(_ value: String, limit: Int = 700) -> String {
808
+ return String(value.prefix(limit))
809
+ }
810
+
624
811
  func showAlert(_ message: String, informative: String = "") {
625
812
  DispatchQueue.main.async {
813
+ NSApp.activate(ignoringOtherApps: true)
626
814
  let alert = NSAlert()
627
815
  alert.messageText = message
628
- alert.informativeText = informative
816
+ alert.informativeText = clipped(informative)
629
817
  alert.alertStyle = .warning
630
818
  alert.runModal()
631
819
  }
632
820
  }
633
821
 
634
- 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) {
635
854
  let process = Process()
636
855
  let output = Pipe()
637
856
  process.executableURL = URL(fileURLWithPath: executable)
638
857
  process.arguments = args
639
858
  process.standardOutput = output
640
859
  process.standardError = output
860
+ var inputPipe: Pipe?
861
+ if stdinText != nil {
862
+ let pipe = Pipe()
863
+ process.standardInput = pipe
864
+ inputPipe = pipe
865
+ }
641
866
  process.terminationHandler = { proc in
642
867
  let data = output.fileHandleForReading.readDataToEndOfFile()
643
868
  let text = String(data: data, encoding: .utf8) ?? ""
@@ -645,14 +870,17 @@ func runProcess(_ executable: String, _ args: [String] = [], completion: ((Int32
645
870
  }
646
871
  do {
647
872
  try process.run()
873
+ if let stdinText = stdinText, let inputPipe = inputPipe {
874
+ inputPipe.fileHandleForWriting.write(Data(stdinText.utf8))
875
+ inputPipe.fileHandleForWriting.closeFile()
876
+ }
648
877
  } catch {
649
878
  completion?(-1, String(describing: error))
650
879
  }
651
880
  }
652
881
 
653
882
  func showNotification(_ title: String, _ body: String) {
654
- let clipped = String(body.prefix(700))
655
- let script = "display notification " + shellQuote(clipped) + " with title " + shellQuote(title)
883
+ let script = "display notification " + shellQuote(clipped(body)) + " with title " + shellQuote(title)
656
884
  runProcess("/usr/bin/osascript", ["-e", script]) { code, output in
657
885
  if code != 0 {
658
886
  showAlert(title, informative: output)
@@ -660,35 +888,40 @@ func showNotification(_ title: String, _ body: String) {
660
888
  }
661
889
  }
662
890
 
663
- func runInTerminal(_ command: String) {
664
- let commandWithExit = "printf '\\\\e]0;SKS\\\\a'; " + command + "; exit"
665
- let escaped = commandWithExit
666
- .replacingOccurrences(of: "\\\\", with: "\\\\\\\\")
667
- .replacingOccurrences(of: "\\\"", with: "\\\\\\\"")
668
- let script = """
669
- tell application "Terminal"
670
- activate
671
- set sksWindow to missing value
672
- repeat with w in windows
673
- if name of w contains "SKS" then set sksWindow to w
674
- end repeat
675
- if sksWindow is missing value then
676
- do script "\\(escaped)"
677
- else
678
- do script "\\(escaped)" in selected tab of sksWindow
679
- end if
680
- end tell
681
- """
682
- runProcess("/usr/bin/osascript", ["-e", script]) { code, output in
683
- if code != 0 {
684
- let denied = output.contains("-1743") || output.localizedCaseInsensitiveContains("not authorized")
685
- if denied {
686
- showAlert("SKS menu cannot control Terminal", informative: "Open System Settings > Privacy & Security > Automation, then allow SKS Menu Bar to control Terminal.")
687
- } else {
688
- showAlert("SKS menu action failed", informative: output)
689
- }
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]")
690
908
  }
691
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
+ }
692
925
  }
693
926
 
694
927
  struct MenuState {
@@ -696,27 +929,37 @@ struct MenuState {
696
929
  let line: String
697
930
  }
698
931
 
699
- final class AppDelegate: NSObject, NSApplicationDelegate {
932
+ struct MenuBarConfig {
933
+ let quitWithCodex: Bool
934
+ }
935
+
936
+ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
700
937
  var statusItem: NSStatusItem!
701
938
  var statusLineItem: NSMenuItem!
939
+ var codexLbItem: NSMenuItem!
940
+ var oauthItem: NSMenuItem!
702
941
  var timer: Timer?
942
+ var busy = false
943
+ var lastFailure = false
944
+ var quitWithCodex = false
703
945
 
704
946
  func applicationDidFinishLaunching(_ notification: Notification) {
705
947
  NSApp.setActivationPolicy(.accessory)
948
+ quitWithCodex = readConfig().quitWithCodex
706
949
  statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
707
950
  statusItem.autosaveName = "com.sneakoscope.sks-menubar"
708
- statusItem.isVisible = true
709
951
  if let button = statusItem.button {
710
952
  configureStatusButton(button, title: "SKS")
711
953
  }
712
954
 
713
955
  let menu = NSMenu()
956
+ menu.delegate = self
714
957
  statusLineItem = NSMenuItem(title: "SKS v\\(packageVersion) - starting", action: nil, keyEquivalent: "")
715
958
  statusLineItem.isEnabled = false
716
959
  menu.addItem(statusLineItem)
717
960
  menu.addItem(NSMenuItem.separator())
718
- add(menu, "Use codex-lb", #selector(useCodexLb))
719
- 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))
720
963
  add(menu, "Set codex-lb Domain and Key", #selector(setCodexLbDomainAndKey))
721
964
  menu.addItem(NSMenuItem.separator())
722
965
  add(menu, "Set OpenRouter Key and GLM Profiles", #selector(setOpenRouterKey))
@@ -724,15 +967,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
724
967
  add(menu, "SKS Version Check", #selector(sksVersionCheck))
725
968
  add(menu, "Update SKS Now", #selector(updateSksNow))
726
969
  menu.addItem(NSMenuItem.separator())
970
+ add(menu, "Open Dashboard", #selector(openDashboard))
727
971
  add(menu, "Open Codex Settings", #selector(openCodexSettings))
728
972
  add(menu, "Restart Codex", #selector(restartCodex))
729
973
  menu.addItem(NSMenuItem.separator())
974
+ add(menu, "View Last Log", #selector(viewLastLog))
730
975
  add(menu, "Quit SKS Menu", #selector(quit))
731
976
  statusItem.menu = menu
977
+ configureCodexLifecycleSync()
732
978
  updateState()
733
979
  timer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in self?.updateState() }
734
980
  }
735
981
 
982
+ func menuWillOpen(_ menu: NSMenu) {
983
+ updateState()
984
+ updateAuthModeChecks()
985
+ }
986
+
736
987
  func configureStatusButton(_ button: NSStatusBarButton, title: String) {
737
988
  button.image = nil
738
989
  button.title = title
@@ -748,10 +999,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
748
999
  button.setAccessibilityHelp("Open SKS menu")
749
1000
  }
750
1001
 
751
- func add(_ menu: NSMenu, _ title: String, _ selector: Selector) {
1002
+ func add(_ menu: NSMenu, _ title: String, _ selector: Selector) -> NSMenuItem {
752
1003
  let item = NSMenuItem(title: title, action: selector, keyEquivalent: "")
753
1004
  item.target = self
754
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)
755
1017
  }
756
1018
 
757
1019
  func updateState() {
@@ -763,20 +1025,26 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
763
1025
  }
764
1026
 
765
1027
  func readMenuState() -> MenuState {
766
- let actionTargetOk = actionScriptTargetExists()
767
- 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() {
768
1035
  return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - action script broken (run sks doctor --fix)")
769
1036
  }
1037
+ if lastFailure {
1038
+ return MenuState(title: "SKS ⚠", line: "SKS v\\(packageVersion) - last action failed")
1039
+ }
770
1040
  if updateAvailable() {
771
1041
  return MenuState(title: "SKS ↑", line: "SKS v\\(packageVersion) - update available")
772
1042
  }
773
1043
  return MenuState(title: "SKS", line: "SKS v\\(packageVersion) - OK")
774
1044
  }
775
1045
 
776
- func actionScriptTargetExists() -> Bool {
777
- guard let text = try? String(contentsOfFile: actionScript, encoding: .utf8) else { return false }
778
- guard let entry = shellAssignment(text, key: "SKS_ENTRY") else { return false }
779
- return FileManager.default.fileExists(atPath: entry)
1046
+ func actionScriptUsable() -> Bool {
1047
+ return FileManager.default.isExecutableFile(atPath: actionScript)
780
1048
  }
781
1049
 
782
1050
  func updateAvailable() -> Bool {
@@ -788,48 +1056,83 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
788
1056
  return latest != packageVersion && !latest.isEmpty
789
1057
  }
790
1058
 
791
- func shellAssignment(_ text: String, key: String) -> String? {
792
- for line in text.components(separatedBy: .newlines) {
793
- if line.hasPrefix(key + "=") {
794
- var value = String(line.dropFirst(key.count + 1))
795
- if value.hasPrefix("'") && value.hasSuffix("'") && value.count >= 2 {
796
- value.removeFirst()
797
- value.removeLast()
798
- 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
+ }
799
1076
  }
800
- return value
1077
+ completion?(code, redacted)
801
1078
  }
802
1079
  }
803
- return nil
804
1080
  }
805
1081
 
806
- func runSksInTerminal(_ args: [String], tail: String = "echo; echo 'SKS command finished.'") {
807
- let quoted = args.map(shellQuote).joined(separator: " ")
808
- 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)
809
1084
  }
810
1085
 
811
- func runSksBackground(_ args: [String], title: String) {
812
- runProcess(actionScript, args) { code, output in
813
- let status = code == 0 ? "OK" : "failed (\\(code))"
814
- 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
815
1110
  }
816
1111
  }
817
1112
 
818
1113
  @objc func useCodexLb() {
819
- 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
+ }
820
1117
  }
821
1118
 
822
1119
  @objc func useChatGptOAuth() {
823
- 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
+ }
824
1123
  }
825
1124
 
826
1125
  @objc func setCodexLbDomainAndKey() {
827
- 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
+ }
828
1131
  }
829
1132
 
830
1133
  @objc func setOpenRouterKey() {
831
- 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.'"
832
- 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")
833
1136
  }
834
1137
 
835
1138
  @objc func fastCheck() {
@@ -841,7 +1144,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
841
1144
  }
842
1145
 
843
1146
  @objc func updateSksNow() {
844
- runSksInTerminal(["update"], tail: "echo; echo 'SKS update finished.'")
1147
+ runSksBackground(["update"], title: "Update SKS Now")
1148
+ }
1149
+
1150
+ @objc func openDashboard() {
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
+ }
845
1162
  }
846
1163
 
847
1164
  @objc func openCodexSettings() {
@@ -849,7 +1166,29 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
849
1166
  }
850
1167
 
851
1168
  @objc func restartCodex() {
852
- 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
+ }
853
1192
  }
854
1193
 
855
1194
  @objc func quit() {
@@ -916,10 +1255,11 @@ async function buildMenuBarAppAtomically(input) {
916
1255
  input.actions.push(`wrote ${input.paths.source_path}`);
917
1256
  await writeTextAtomic(path.join(input.paths.staging_app_path, 'Contents', 'Info.plist'), input.infoPlist);
918
1257
  const stagingExecutable = path.join(input.paths.staging_app_path, 'Contents', 'MacOS', 'SKSMenuBar');
919
- const compile = await runProcess(input.swiftc, ['-framework', 'Cocoa', input.paths.source_path, '-o', stagingExecutable], {
1258
+ const compileWork = runProcess(input.swiftc, ['-framework', 'Cocoa', input.paths.source_path, '-o', stagingExecutable], {
920
1259
  timeoutMs: 60_000,
921
1260
  maxOutputBytes: 64 * 1024
922
1261
  }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
1262
+ const compile = input.quiet === true ? await compileWork : await withHeartbeat('swiftc SKS menu bar', compileWork, { warnAfterMs: 30_000 });
923
1263
  if (compile.code !== 0) {
924
1264
  throw new MenuBarBuildError('swift_compile_failed', String(compile.stderr || compile.stdout || '').trim());
925
1265
  }
@@ -956,7 +1296,8 @@ async function launchWithLaunchctl(input) {
956
1296
  stdoutFile: path.join(input.paths.install_dir, 'launchctl.out.log'),
957
1297
  stderrFile: path.join(input.paths.install_dir, 'launchctl.err.log')
958
1298
  };
959
- const already = await waitForLaunchctlRunning(input.launchctl, service);
1299
+ const alreadyWork = waitForLaunchctlRunning(input.launchctl, service);
1300
+ const already = input.quiet === true ? await alreadyWork : await withHeartbeat('launchctl SKS menu bar wait', alreadyWork, { warnAfterMs: 10_000 });
960
1301
  if (input.skipIfRunning && already.running) {
961
1302
  return { requested: true, method: 'launchctl', ok: true, print_code: already.code, error: null };
962
1303
  }
@@ -975,7 +1316,8 @@ async function launchWithLaunchctl(input) {
975
1316
  ...stdio
976
1317
  }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
977
1318
  if (kickstart.code !== 0) {
978
- const printed = await waitForLaunchctlRunning(input.launchctl, service);
1319
+ const printedWork = waitForLaunchctlRunning(input.launchctl, service);
1320
+ const printed = input.quiet === true ? await printedWork : await withHeartbeat('launchctl SKS menu bar wait', printedWork, { warnAfterMs: 10_000 });
979
1321
  if (printed.running) {
980
1322
  return {
981
1323
  requested: true,
@@ -1064,13 +1406,14 @@ async function inspectLaunchdService(env = process.env) {
1064
1406
  const text = `${result.stdout || ''}\n${result.stderr || ''}`;
1065
1407
  const state = text.match(/\bstate = ([^\n]+)/)?.[1]?.trim() || null;
1066
1408
  const pidText = text.match(/\bpid = (\d+)/)?.[1] || null;
1409
+ const running = result.code === 0 && (state === 'running' || Boolean(pidText));
1067
1410
  return {
1068
1411
  checked: true,
1069
- ok: result.code === 0,
1412
+ ok: running,
1070
1413
  service,
1071
1414
  state,
1072
1415
  pid: pidText ? Number(pidText) : null,
1073
- 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()
1074
1417
  };
1075
1418
  }
1076
1419
  async function inspectSignature(appPath, env = process.env) {
@@ -1162,6 +1505,8 @@ async function writeReport(reportPath, result) {
1162
1505
  }
1163
1506
  catch (err) {
1164
1507
  result.report_write_failed = true;
1508
+ if (!result.warnings.includes('menubar_report_write_failed'))
1509
+ result.warnings.push('menubar_report_write_failed');
1165
1510
  console.error(`warning: failed to write SKS menubar report ${reportPath}: ${err?.message || err}`);
1166
1511
  }
1167
1512
  }