sneakoscope 4.7.3 โ†’ 4.8.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.
package/README.md CHANGED
@@ -49,7 +49,7 @@ sks seo-geo-optimizer apply latest --mode seo --apply --json
49
49
  sks seo-geo-optimizer audit --mode geo --target package --offline --json
50
50
  ```
51
51
 
52
- > ๐Ÿ“‹ **Current release: `v4.7.3`** โ€” full release history lives in [CHANGELOG.md](CHANGELOG.md). This README documents how Sneakoscope works today, not its version-by-version changes. Release readiness is tracked in [docs/release-readiness.md](docs/release-readiness.md).
52
+ > ๐Ÿ“‹ **Current release: `v4.8.0`** โ€” full release history lives in [CHANGELOG.md](CHANGELOG.md). This README documents how Sneakoscope works today, not its version-by-version changes. Release readiness is tracked in [docs/release-readiness.md](docs/release-readiness.md).
53
53
 
54
54
  ## ๐Ÿฅ Parallelism, UX, And Integrations
55
55
 
@@ -74,7 +74,7 @@ sks seo-geo-optimizer audit --mode geo --target package --offline --json
74
74
  sks insane-search x "site:x.com product launch"
75
75
  ```
76
76
 
77
- - **CLI-only SKS update notices.** Codex App hooks no longer stop normal work to ask for an SKS update. CLI launch surfaces such as `sks --mad` print a non-blocking latest-version notice, `sks update-check` / `sks update check` show the explicit status, and `sks doctor --fix` runs the guarded global SKS update path before repair.
77
+ - **CLI-only SKS update notices.** Codex App hooks no longer stop normal work to ask for an SKS update. CLI launch surfaces such as `sks --mad` print a non-blocking latest-version notice, `sks update-check` / `sks update check` show the explicit status, bare `sks update` checks npm latest and updates the global package, and `sks doctor --fix` runs the guarded repair path.
78
78
 
79
79
  ## ๐Ÿงน Retention And Cleanup
80
80
 
@@ -318,7 +318,7 @@ sks deps check --yes
318
318
  sks codex-app check
319
319
  sks doctor --fix
320
320
  sks fix-path
321
- sks update now
321
+ sks update
322
322
  ```
323
323
 
324
324
  `sks doctor --fix` repairs stale SKS-managed Codex TOML, agent role `config_file` paths, and unsupported managed role fields. It now dedupes repeated TOML tables before repair, keeps the first existing external MCP block such as `context7` or `supabase` unchanged, and only removes/repairs SKS-owned stale MCP blocks such as broken `node_repl`.
@@ -363,7 +363,7 @@ sks codex-app set-openrouter-key --api-key-stdin
363
363
  sks codex-app glm-profile install
364
364
  ```
365
365
 
366
- This writes the OpenRouter key to the SKS user secret store, writes redacted metadata only, and installs Codex Desktop-compatible `openrouter` provider plus selectable `sks-glm-52-*` reasoning profiles. The npm package does not patch the Codex Desktop macOS menu binary; a top-level `Set OpenRouter Key` menu/modal requires upstream Codex Desktop UI support. The package-owned command surface above is the supported repair path.
366
+ This writes the OpenRouter key to the SKS user secret store, writes redacted metadata only, and installs Codex Desktop-compatible `openrouter` provider plus selectable `sks-glm-52-*` reasoning profiles. The npm package does not patch the Codex Desktop macOS menu binary; a top-level menu between Window and Help requires upstream Codex Desktop UI support. On macOS, `sks doctor --fix` installs or refreshes a right-side SKS menu bar companion that appears as a compact circled `S` status icon with codex-lb, ChatGPT OAuth, OpenRouter/GLM, Fast Check, SKS Version Check, Update SKS Now, settings, and Codex restart actions.
367
367
 
368
368
  ### Switching auth mode: codex-lb โ†” ChatGPT OAuth
369
369
 
@@ -404,7 +404,7 @@ sks --mad --allow-package-install --allow-service-control --allow-network --yes
404
404
 
405
405
  This syncs existing codex-lb provider auth, creates/uses the `sks-mad-high` xhigh maintenance profile, opens the MAD-SKS permission gate for that Zellij run, starts a same-mission bounded native agent swarm, and launches a Codex CLI layout whose right-side lanes read that MAD ledger. Bare `sks --mad` grants target-project file and shell scope only; add explicit `--allow-*` flags for packages, services, network, Computer Use, browser use, generated assets, file permissions, DB writes, or other high-risk scopes. MAD-SKS is not a DB-only unlock and does not create a MadDB capability. Catastrophic database wipe/all-row/project-management safeguards remain active outside the first-class MadDB route, and the pipeline contract still forbids unrequested fallback implementation code.
406
406
 
407
- Before launching, SKS checks npm for a newer `sneakoscope` and prints a non-blocking update notice when one is available; use `sks update now` or `sks doctor --fix` when you want SKS to update itself. Use `--yes` to approve missing dependency installs automatically. Tune MAD swarm startup with `--mad-agents <n>`, `--mad-swarm-work-items <n>`, and `--mad-swarm-backend <backend>`; `--no-mad-swarm` keeps only the cockpit UI if you need a temporary fallback.
407
+ Before launching, SKS checks npm for a newer `sneakoscope` and prints a non-blocking update notice when one is available; use `sks update` when you want SKS to update itself from npm. Use `--yes` to approve missing dependency installs automatically. Tune MAD swarm startup with `--mad-agents <n>`, `--mad-swarm-work-items <n>`, and `--mad-swarm-backend <backend>`; `--no-mad-swarm` keeps only the cockpit UI if you need a temporary fallback.
408
408
 
409
409
  ### Team Missions
410
410
 
@@ -679,12 +679,12 @@ If PATH or npm has duplicate global installs, `sks doctor --fix` keeps one globa
679
679
 
680
680
  ```sh
681
681
  sks update-check --json
682
- sks update now
682
+ sks update
683
683
  npm ls -g sneakoscope --depth=0
684
684
  sks doctor --fix
685
685
  ```
686
686
 
687
- CLI update checks compare npm latest against the effective installed version from source metadata, PATH `sks --version`, and global npm package metadata. Codex App hooks do not force update choices during ordinary work. `sks update now` installs through npm global mode instead of mutating the current project's dependencies, and `sks doctor --fix` runs that guarded global update path before setup/config repair. If a global update succeeded but an old shim remains earlier on PATH, `sks doctor --fix` can remove duplicate global installs and refresh the managed setup.
687
+ CLI update checks compare npm latest against the effective installed version from source metadata, PATH `sks --version`, and global npm package metadata. Codex App hooks do not force update choices during ordinary work. Bare `sks update` checks npm latest and installs through npm global mode instead of mutating the current project's dependencies; `sks update check` is the status-only form, and `sks update now` remains an explicit alias. Successful/current update runs also install or refresh the macOS right-side `SKS` menu bar companion so it is already visible when Codex App is opened. If a global update succeeded but an old shim remains earlier on PATH, `sks doctor --fix` can remove duplicate global installs and refresh the managed setup.
688
688
 
689
689
  ### Zellij is missing
690
690
 
@@ -76,7 +76,7 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "sks-core"
79
- version = "4.7.3"
79
+ version = "4.8.0"
80
80
  dependencies = [
81
81
  "serde_json",
82
82
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sks-core"
3
- version = "4.7.3"
3
+ version = "4.8.0"
4
4
  edition = "2021"
5
5
 
6
6
  [dependencies]
@@ -4,7 +4,7 @@ use std::io::{self, Read, Seek, SeekFrom};
4
4
  fn main() {
5
5
  let mut args = std::env::args().skip(1);
6
6
  match args.next().as_deref() {
7
- Some("--version") => println!("sks-rs 4.7.3"),
7
+ Some("--version") => println!("sks-rs 4.8.0"),
8
8
  Some("compact-info") => {
9
9
  let mut input = String::new();
10
10
  let _ = io::stdin().read_to_string(&mut input);
package/dist/bin/sks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const FAST_PACKAGE_VERSION = '4.7.3';
2
+ const FAST_PACKAGE_VERSION = '4.8.0';
3
3
  const args = process.argv.slice(2);
4
4
  try {
5
5
  if (args[0] === '--agent' && args[1] === 'worker') {
@@ -85,7 +85,7 @@ export const COMMANDS = {
85
85
  run: entry('beta', 'Classify and execute a task through the SKS trust kernel', 'dist/core/commands/run-command.js', argsCommand(() => import('../core/commands/run-command.js'), 'runCommand', 'dist/core/commands/run-command.js')),
86
86
  status: entry('stable', 'Show concise active mission and trust status', 'dist/core/commands/status-command.js', argsCommand(() => import('../core/commands/status-command.js'), 'statusCommand', 'dist/core/commands/status-command.js')),
87
87
  root: entry('stable', 'Show active SKS root', 'dist/commands/root.js', directCommand(() => import('../commands/root.js'), 'dist/commands/root.js')),
88
- update: entry('stable', 'Update the global SKS npm package', 'dist/core/commands/basic-cli.js', subcommand(() => import(basicModule), 'updateCommand', 'dist/core/commands/basic-cli.js', 'check')),
88
+ update: entry('stable', 'Update the global SKS npm package', 'dist/core/commands/basic-cli.js', subcommand(() => import(basicModule), 'updateCommand', 'dist/core/commands/basic-cli.js', 'now')),
89
89
  'update-check': entry('stable', 'Check npm package freshness', 'dist/core/commands/basic-cli.js', basicArgs('updateCheckCommand')),
90
90
  wizard: entry('stable', 'Open setup wizard help', 'dist/core/commands/basic-cli.js', basicNoArgs('quickstartCommand')),
91
91
  usage: entry('stable', 'Show focused usage topic', 'dist/core/commands/basic-cli.js', basicArgs('usageCommand')),
@@ -39,6 +39,7 @@ import { planDoctorDirtyRepair } from '../core/doctor/doctor-dirty-planner.js';
39
39
  import { doctorRepairPostcheck } from '../core/doctor/doctor-repair-postcheck.js';
40
40
  import { withSecretPreservationGuard } from '../core/config/config-migration-journal.js';
41
41
  import { writeProjectUpdateMigrationReceipt } from '../core/update/update-migration-state.js';
42
+ import { installSksMenuBar } from '../core/codex-app/sks-menubar.js';
42
43
  export async function run(_command, args = []) {
43
44
  const root = await projectRoot();
44
45
  const doctorFix = flag(args, '--fix');
@@ -274,6 +275,27 @@ async function runDoctor(args = [], root, doctorFix) {
274
275
  blockers: [err?.message || String(err)]
275
276
  }))
276
277
  : codexAppUiPlan;
278
+ const sksMenuBar = await installSksMenuBar({
279
+ root,
280
+ apply: doctorFix,
281
+ launch: doctorFix
282
+ }).catch((err) => ({
283
+ schema: 'sks.codex-app-sks-menubar.v1',
284
+ ok: false,
285
+ apply: doctorFix,
286
+ status: 'blocked',
287
+ platform: process.platform,
288
+ app_path: null,
289
+ executable_path: null,
290
+ launch_agent_path: null,
291
+ action_script_path: null,
292
+ report_path: `${root}/.sneakoscope/reports/sks-menubar.json`,
293
+ menu_items: [],
294
+ actions: [],
295
+ launch: { requested: doctorFix, method: 'none', ok: false, error: err?.message || String(err) },
296
+ blockers: [err?.message || String(err)],
297
+ warnings: []
298
+ }));
277
299
  const zellijRepair = shouldRunZellijRepair
278
300
  ? await runDoctorZellijRepair({ root, args, doctorFix }).catch((err) => ({
279
301
  schema: 'sks.zellij-self-heal.v1',
@@ -434,6 +456,20 @@ async function runDoctor(args = [], root, doctorFix) {
434
456
  rollback_evidence: 'optional_supabase_no_ready_mutation_required'
435
457
  })
436
458
  },
459
+ {
460
+ id: 'sks_menubar',
461
+ required_for_ready: false,
462
+ run: async () => ({
463
+ id: 'sks_menubar',
464
+ ok: sksMenuBar?.ok !== false,
465
+ repaired: doctorFix && Array.isArray(sksMenuBar?.actions) && sksMenuBar.actions.length > 0,
466
+ required_for_ready: false,
467
+ blockers: sksMenuBar?.blockers || [],
468
+ warnings: sksMenuBar?.warnings || [],
469
+ artifact_path: sksMenuBar?.report_path || null,
470
+ rollback_evidence: sksMenuBar?.launch_agent_path || sksMenuBar?.report_path || 'sks_menubar_optional_no_core_mutation'
471
+ })
472
+ },
437
473
  {
438
474
  id: 'command_alias_cleanup',
439
475
  run: async () => ({
@@ -621,6 +657,7 @@ async function runDoctor(args = [], root, doctorFix) {
621
657
  agent_role_config: agentRoleConfigRepair,
622
658
  repair: configRepair,
623
659
  codex_app_ui: codexAppUi,
660
+ sks_menubar: sksMenuBar,
624
661
  codex_0138_doctor: codex0138Doctor,
625
662
  codex_plugin_inventory: pluginInventory?.report || null,
626
663
  codex_plugin_app_template_policy: pluginPolicy,
@@ -658,6 +695,7 @@ async function runDoctor(args = [], root, doctorFix) {
658
695
  rust,
659
696
  codex_app: codexApp,
660
697
  codex_app_ui: codexAppUi,
698
+ sks_menubar: sksMenuBar,
661
699
  provider_context: providerContext,
662
700
  codex_lb: codexLb,
663
701
  codex_doctor: authoritativeCodexDoctor,
@@ -697,7 +735,7 @@ async function runDoctor(args = [], root, doctorFix) {
697
735
  ready,
698
736
  sneakoscope: { ok: await exists(`${root}/.sneakoscope`) },
699
737
  package: { bytes: pkgBytes, human: formatBytes(pkgBytes) },
700
- repair: { sks_update: sksUpdate, setup: setupRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, context7: context7Repair, codex_startup: codexStartupRepair, startup_config: startupConfigRepair, context7_mcp: context7McpRepair, supabase_mcp: supabaseMcpRepair, doctor_transaction: doctorFixTransaction, doctor_dirty_plan: doctorDirtyPlan, doctor_postcheck: doctorFixPostcheck, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup }
738
+ repair: { sks_update: sksUpdate, setup: setupRepair, codex_config: configRepair, migration_journal: migrationJournal, global_sks_installs: globalSksInstallCleanup, agent_role_config: agentRoleConfigRepair, zellij: zellijRepair, context7: context7Repair, codex_startup: codexStartupRepair, startup_config: startupConfigRepair, context7_mcp: context7McpRepair, supabase_mcp: supabaseMcpRepair, sks_menubar: sksMenuBar, doctor_transaction: doctorFixTransaction, doctor_dirty_plan: doctorDirtyPlan, doctor_postcheck: doctorFixPostcheck, codex_native: codexNativeRepair, doctor_native_capability: doctorNativeCapabilityRepair, command_aliases: commandAliasCleanup }
701
739
  };
702
740
  if (reportFile)
703
741
  await writeJsonReportFile(reportFile, result);
@@ -821,6 +859,16 @@ async function runDoctor(args = [], root, doctorFix) {
821
859
  }
822
860
  if (codexAppUi.next_action)
823
861
  console.log(` next action: ${codexAppUi.next_action}`);
862
+ console.log('SKS Menu Bar:');
863
+ console.log(` status: ${sksMenuBar.status || (sksMenuBar.ok ? 'ok' : 'blocked')}`);
864
+ if (sksMenuBar.app_path)
865
+ console.log(` app: ${sksMenuBar.app_path}`);
866
+ if (sksMenuBar.launch_agent_path)
867
+ console.log(` launch agent: ${sksMenuBar.launch_agent_path}`);
868
+ if (Array.isArray(sksMenuBar.blockers) && sksMenuBar.blockers.length)
869
+ console.log(` blockers: ${sksMenuBar.blockers.join(', ')}`);
870
+ if (Array.isArray(sksMenuBar.warnings) && sksMenuBar.warnings.length)
871
+ console.log(` warnings: ${sksMenuBar.warnings.join(', ')}`);
824
872
  console.log(`Provider: ${providerContext.provider || 'unknown'} ${providerContext.service_tier || ''} (${providerContext.source || 'unknown'}, ${providerContext.confidence || 'low'})`);
825
873
  const imagegenReady = imagegen.auth_readiness;
826
874
  if (imagegenReady) {
@@ -989,7 +1037,7 @@ function doctorPhaseIdsForProfile(profile) {
989
1037
  ];
990
1038
  if (profile === 'migration')
991
1039
  return required;
992
- const optional = ['supabase_mcp_repair', 'native_capability_repair'];
1040
+ const optional = ['supabase_mcp_repair', 'native_capability_repair', 'sks_menubar'];
993
1041
  if (profile === 'full' || profile === 'capabilities')
994
1042
  return ['setup', ...required, ...optional];
995
1043
  return [...required, ...optional];
@@ -0,0 +1,458 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { ensureDir, exists, runProcess, which, writeJsonAtomic, writeTextAtomic } from '../fsx.js';
5
+ const LABEL = 'com.sneakoscope.sks-menubar';
6
+ const MENU_ITEMS = [
7
+ 'Use codex-lb',
8
+ 'Use ChatGPT OAuth',
9
+ 'Set OpenRouter Key and GLM Profiles',
10
+ 'Fast Check',
11
+ 'SKS Version Check',
12
+ 'Update SKS Now',
13
+ 'Open Codex Settings',
14
+ 'Restart Codex',
15
+ 'Quit SKS Menu'
16
+ ];
17
+ export async function installSksMenuBar(opts = {}) {
18
+ const apply = opts.apply === true;
19
+ const env = opts.env || process.env;
20
+ const home = path.resolve(opts.home || env.HOME || os.homedir());
21
+ const root = path.resolve(opts.root || process.cwd());
22
+ const installDir = path.join(home, '.codex', 'sks-menubar');
23
+ const appPath = path.join(installDir, 'SKSMenuBar.app');
24
+ const contentsPath = path.join(appPath, 'Contents');
25
+ const macosPath = path.join(contentsPath, 'MacOS');
26
+ const executablePath = path.join(macosPath, 'SKSMenuBar');
27
+ const sourcePath = path.join(installDir, 'SKSMenuBar.swift');
28
+ const infoPlistPath = path.join(contentsPath, 'Info.plist');
29
+ const actionScriptPath = path.join(installDir, 'sks-menubar-action.sh');
30
+ const launchAgentPath = path.join(home, 'Library', 'LaunchAgents', `${LABEL}.plist`);
31
+ const reportPath = path.join(root, '.sneakoscope', 'reports', 'sks-menubar.json');
32
+ const actions = [];
33
+ const warnings = [];
34
+ if (process.platform !== 'darwin') {
35
+ const result = {
36
+ schema: 'sks.codex-app-sks-menubar.v1',
37
+ ok: true,
38
+ apply,
39
+ status: 'unsupported_platform',
40
+ platform: process.platform,
41
+ app_path: null,
42
+ executable_path: null,
43
+ launch_agent_path: null,
44
+ action_script_path: null,
45
+ report_path: apply ? reportPath : null,
46
+ menu_items: MENU_ITEMS,
47
+ actions: [],
48
+ launch: { requested: false, method: 'none', ok: true },
49
+ blockers: [],
50
+ warnings: ['sks_menubar_requires_macos']
51
+ };
52
+ if (apply)
53
+ await writeJsonAtomic(reportPath, result).catch(() => undefined);
54
+ return result;
55
+ }
56
+ if (!apply) {
57
+ const installed = await exists(executablePath);
58
+ const launchAgent = await exists(launchAgentPath);
59
+ return {
60
+ schema: 'sks.codex-app-sks-menubar.v1',
61
+ ok: true,
62
+ apply,
63
+ status: 'planned',
64
+ platform: process.platform,
65
+ app_path: appPath,
66
+ executable_path: executablePath,
67
+ launch_agent_path: launchAgentPath,
68
+ action_script_path: actionScriptPath,
69
+ report_path: reportPath,
70
+ menu_items: MENU_ITEMS,
71
+ actions: installed ? ['menubar_app_present'] : ['menubar_app_install_available'],
72
+ launch: {
73
+ requested: false,
74
+ method: 'skipped',
75
+ ok: true
76
+ },
77
+ blockers: [],
78
+ warnings: launchAgent ? [] : ['launch_agent_not_installed_yet']
79
+ };
80
+ }
81
+ const swiftc = env.SKS_MENUBAR_SWIFTC || await which('swiftc').catch(() => null) || await fallbackTool('/usr/bin/swiftc');
82
+ const launchctl = env.SKS_MENUBAR_LAUNCHCTL || await which('launchctl').catch(() => null) || await fallbackTool('/bin/launchctl');
83
+ const open = env.SKS_MENUBAR_OPEN || await which('open').catch(() => null) || await fallbackTool('/usr/bin/open');
84
+ if (!swiftc)
85
+ return await blockedResult('swiftc_missing');
86
+ await ensureDir(installDir);
87
+ await ensureDir(macosPath);
88
+ await ensureDir(path.dirname(launchAgentPath));
89
+ const sksEntry = resolveSksEntry(opts.sksEntry);
90
+ await writeTextAtomic(actionScriptPath, actionScriptSource({ nodeBin: process.execPath, sksEntry }));
91
+ await fs.chmod(actionScriptPath, 0o755);
92
+ actions.push(`wrote ${actionScriptPath}`);
93
+ await writeTextAtomic(sourcePath, swiftMenuSource(actionScriptPath));
94
+ actions.push(`wrote ${sourcePath}`);
95
+ await writeTextAtomic(infoPlistPath, infoPlistSource());
96
+ actions.push(`wrote ${infoPlistPath}`);
97
+ const compile = await runProcess(swiftc, ['-framework', 'Cocoa', sourcePath, '-o', executablePath], {
98
+ timeoutMs: 60_000,
99
+ maxOutputBytes: 64 * 1024
100
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
101
+ if (compile.code !== 0) {
102
+ return await blockedResult('swift_compile_failed', String(compile.stderr || compile.stdout || '').trim());
103
+ }
104
+ await fs.chmod(executablePath, 0o755).catch(() => undefined);
105
+ actions.push(`compiled ${executablePath}`);
106
+ await writeTextAtomic(launchAgentPath, launchAgentSource(executablePath, installDir));
107
+ actions.push(`wrote ${launchAgentPath}`);
108
+ const launchRequested = opts.launch !== false && env.SKS_SKIP_SKS_MENUBAR_LAUNCH !== '1';
109
+ const launch = launchRequested && launchctl
110
+ ? await launchWithLaunchctl({ launchctl, open, appPath, executablePath, launchAgentPath })
111
+ : {
112
+ requested: launchRequested,
113
+ method: 'skipped',
114
+ ok: !launchRequested,
115
+ error: launchRequested ? 'launchctl_missing' : null
116
+ };
117
+ if (launchRequested && !launchctl)
118
+ warnings.push('launchctl_missing');
119
+ if (launch.method === 'open-fallback')
120
+ warnings.push('launchctl_bootstrap_failed_open_fallback_used');
121
+ const ok = launch.ok === true;
122
+ const result = {
123
+ schema: 'sks.codex-app-sks-menubar.v1',
124
+ ok,
125
+ apply,
126
+ status: ok
127
+ ? launch.requested === false || launch.method === 'skipped'
128
+ ? 'installed_launch_skipped'
129
+ : launch.method === 'open-fallback'
130
+ ? 'installed_open_fallback'
131
+ : 'installed'
132
+ : 'blocked',
133
+ platform: process.platform,
134
+ app_path: appPath,
135
+ executable_path: executablePath,
136
+ launch_agent_path: launchAgentPath,
137
+ action_script_path: actionScriptPath,
138
+ report_path: reportPath,
139
+ menu_items: MENU_ITEMS,
140
+ actions,
141
+ launch,
142
+ blockers: ok ? [] : [launch.error || 'sks_menubar_launch_failed'],
143
+ warnings
144
+ };
145
+ await writeJsonAtomic(reportPath, result).catch(() => undefined);
146
+ return result;
147
+ async function blockedResult(reason, detail) {
148
+ const result = {
149
+ schema: 'sks.codex-app-sks-menubar.v1',
150
+ ok: false,
151
+ apply,
152
+ status: 'blocked',
153
+ platform: process.platform,
154
+ app_path: appPath,
155
+ executable_path: executablePath,
156
+ launch_agent_path: launchAgentPath,
157
+ action_script_path: actionScriptPath,
158
+ report_path: reportPath,
159
+ menu_items: MENU_ITEMS,
160
+ actions,
161
+ launch: { requested: false, method: 'none', ok: false, error: detail || reason },
162
+ blockers: [reason],
163
+ warnings: detail ? [detail] : []
164
+ };
165
+ await writeJsonAtomic(reportPath, result).catch(() => undefined);
166
+ return result;
167
+ }
168
+ }
169
+ async function fallbackTool(candidate) {
170
+ return await exists(candidate).then((ok) => ok ? candidate : null).catch(() => null);
171
+ }
172
+ function resolveSksEntry(explicit) {
173
+ if (explicit)
174
+ return path.resolve(explicit);
175
+ const argvEntry = process.argv[1] ? path.resolve(process.argv[1]) : '';
176
+ return argvEntry || path.join(process.cwd(), 'dist', 'bin', 'sks.js');
177
+ }
178
+ function actionScriptSource(input) {
179
+ return `#!/bin/zsh
180
+ set -e
181
+ export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
182
+ NODE_BIN=${shellQuote(input.nodeBin)}
183
+ SKS_ENTRY=${shellQuote(input.sksEntry)}
184
+ if [ -x "$NODE_BIN" ] && [ -f "$SKS_ENTRY" ]; then
185
+ exec "$NODE_BIN" "$SKS_ENTRY" "$@"
186
+ fi
187
+ if command -v sks >/dev/null 2>&1; then
188
+ exec sks "$@"
189
+ fi
190
+ echo "SKS command not found. Run npm link or install Sneakoscope Codex, then run sks doctor --fix again." >&2
191
+ exit 127
192
+ `;
193
+ }
194
+ function swiftMenuSource(actionScriptPath) {
195
+ return `import Cocoa
196
+ import Foundation
197
+
198
+ let actionScript = ${swiftString(actionScriptPath)}
199
+
200
+ func shellQuote(_ value: String) -> String {
201
+ return "'" + value.replacingOccurrences(of: "'", with: "'\\\\''") + "'"
202
+ }
203
+
204
+ func runDetached(_ executable: String, _ args: [String] = []) {
205
+ let process = Process()
206
+ process.executableURL = URL(fileURLWithPath: executable)
207
+ process.arguments = args
208
+ try? process.run()
209
+ }
210
+
211
+ func runInTerminal(_ command: String) {
212
+ let escaped = command
213
+ .replacingOccurrences(of: "\\\\", with: "\\\\\\\\")
214
+ .replacingOccurrences(of: "\\\"", with: "\\\\\\\"")
215
+ let script = "tell application \\"Terminal\\" to activate\\n" +
216
+ "tell application \\"Terminal\\" to do script \\"\(escaped)\\""
217
+ runDetached("/usr/bin/osascript", ["-e", script])
218
+ }
219
+
220
+ final class AppDelegate: NSObject, NSApplicationDelegate {
221
+ var statusItem: NSStatusItem!
222
+
223
+ func applicationDidFinishLaunching(_ notification: Notification) {
224
+ NSApp.setActivationPolicy(.accessory)
225
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
226
+ if let button = statusItem.button {
227
+ configureStatusButton(button)
228
+ }
229
+
230
+ let menu = NSMenu()
231
+ add(menu, "Use codex-lb", #selector(useCodexLb))
232
+ add(menu, "Use ChatGPT OAuth", #selector(useChatGptOAuth))
233
+ menu.addItem(NSMenuItem.separator())
234
+ add(menu, "Set OpenRouter Key and GLM Profiles", #selector(setOpenRouterKey))
235
+ add(menu, "Fast Check", #selector(fastCheck))
236
+ add(menu, "SKS Version Check", #selector(sksVersionCheck))
237
+ add(menu, "Update SKS Now", #selector(updateSksNow))
238
+ menu.addItem(NSMenuItem.separator())
239
+ add(menu, "Open Codex Settings", #selector(openCodexSettings))
240
+ add(menu, "Restart Codex", #selector(restartCodex))
241
+ menu.addItem(NSMenuItem.separator())
242
+ add(menu, "Quit SKS Menu", #selector(quit))
243
+ statusItem.menu = menu
244
+ }
245
+
246
+ func configureStatusButton(_ button: NSStatusBarButton) {
247
+ if #available(macOS 11.0, *), let image = NSImage(systemSymbolName: "s.circle.fill", accessibilityDescription: "SKS") {
248
+ image.isTemplate = true
249
+ button.image = image
250
+ button.imagePosition = .imageOnly
251
+ button.title = ""
252
+ } else {
253
+ button.title = "S"
254
+ }
255
+ button.toolTip = "SKS - Sneakoscope Codex settings"
256
+ button.setAccessibilityLabel("SKS")
257
+ button.setAccessibilityHelp("Open SKS menu")
258
+ }
259
+
260
+ func add(_ menu: NSMenu, _ title: String, _ selector: Selector) {
261
+ let item = NSMenuItem(title: title, action: selector, keyEquivalent: "")
262
+ item.target = self
263
+ menu.addItem(item)
264
+ }
265
+
266
+ func runSks(_ args: [String], tail: String = "echo; echo 'SKS command finished. Close this window when ready.'") {
267
+ let quoted = args.map(shellQuote).joined(separator: " ")
268
+ runInTerminal("\\(shellQuote(actionScript)) \\(quoted); \\(tail)")
269
+ }
270
+
271
+ @objc func useCodexLb() {
272
+ runSks(["codex-lb", "use-codex-lb"])
273
+ }
274
+
275
+ @objc func useChatGptOAuth() {
276
+ runSks(["codex-lb", "use-oauth"])
277
+ }
278
+
279
+ @objc func setOpenRouterKey() {
280
+ 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.'"
281
+ runInTerminal(command)
282
+ }
283
+
284
+ @objc func fastCheck() {
285
+ runSks(["codex-lb", "fast-check"])
286
+ }
287
+
288
+ @objc func sksVersionCheck() {
289
+ let sks = shellQuote(actionScript)
290
+ runInTerminal("echo 'SKS version'; \\(sks) --version; echo; echo 'Checking npm latest'; \\(sks) update check; echo; echo 'SKS version check finished. Close this window when ready.'")
291
+ }
292
+
293
+ @objc func updateSksNow() {
294
+ runSks(["update"], tail: "echo; echo 'SKS update finished. Close this window when ready.'")
295
+ }
296
+
297
+ @objc func openCodexSettings() {
298
+ runDetached("/usr/bin/open", ["codex://settings"])
299
+ }
300
+
301
+ @objc func restartCodex() {
302
+ runInTerminal("/usr/bin/osascript -e 'tell application \\"Codex\\" to quit'; sleep 1; /usr/bin/open -a Codex; echo 'Codex restart requested.'")
303
+ }
304
+
305
+ @objc func quit() {
306
+ NSApplication.shared.terminate(nil)
307
+ }
308
+ }
309
+
310
+ let app = NSApplication.shared
311
+ let delegate = AppDelegate()
312
+ app.delegate = delegate
313
+ app.run()
314
+ `;
315
+ }
316
+ function infoPlistSource() {
317
+ return `<?xml version="1.0" encoding="UTF-8"?>
318
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
319
+ <plist version="1.0">
320
+ <dict>
321
+ <key>CFBundleExecutable</key>
322
+ <string>SKSMenuBar</string>
323
+ <key>CFBundleIdentifier</key>
324
+ <string>${LABEL}</string>
325
+ <key>CFBundleName</key>
326
+ <string>SKS Menu Bar</string>
327
+ <key>CFBundlePackageType</key>
328
+ <string>APPL</string>
329
+ <key>CFBundleVersion</key>
330
+ <string>1</string>
331
+ <key>LSUIElement</key>
332
+ <true/>
333
+ </dict>
334
+ </plist>
335
+ `;
336
+ }
337
+ function launchAgentSource(executablePath, installDir) {
338
+ return `<?xml version="1.0" encoding="UTF-8"?>
339
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
340
+ <plist version="1.0">
341
+ <dict>
342
+ <key>Label</key>
343
+ <string>${LABEL}</string>
344
+ <key>ProgramArguments</key>
345
+ <array>
346
+ <string>${escapeXml(executablePath)}</string>
347
+ </array>
348
+ <key>RunAtLoad</key>
349
+ <true/>
350
+ <key>KeepAlive</key>
351
+ <true/>
352
+ <key>StandardOutPath</key>
353
+ <string>${escapeXml(path.join(installDir, 'menubar.out.log'))}</string>
354
+ <key>StandardErrorPath</key>
355
+ <string>${escapeXml(path.join(installDir, 'menubar.err.log'))}</string>
356
+ </dict>
357
+ </plist>
358
+ `;
359
+ }
360
+ async function launchWithLaunchctl(input) {
361
+ const uid = typeof process.getuid === 'function' ? process.getuid() : null;
362
+ const domain = uid === null ? 'gui' : `gui/${uid}`;
363
+ await runProcess(input.launchctl, ['bootout', `${domain}/${LABEL}`], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => undefined);
364
+ await runProcess(input.launchctl, ['bootout', domain, input.launchAgentPath], { timeoutMs: 5_000, maxOutputBytes: 16 * 1024 }).catch(() => undefined);
365
+ await terminateExistingMenuBarProcess(input.executablePath);
366
+ const bootstrap = await runProcess(input.launchctl, ['bootstrap', domain, input.launchAgentPath], {
367
+ timeoutMs: 10_000,
368
+ maxOutputBytes: 32 * 1024
369
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
370
+ if (bootstrap.code === 0) {
371
+ const kickstart = await runProcess(input.launchctl, ['kickstart', '-k', `${domain}/${LABEL}`], {
372
+ timeoutMs: 5_000,
373
+ maxOutputBytes: 32 * 1024
374
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
375
+ if (kickstart.code !== 0) {
376
+ const printed = await waitForLaunchctlRunning(input.launchctl, `${domain}/${LABEL}`);
377
+ if (printed.running) {
378
+ return {
379
+ requested: true,
380
+ method: 'launchctl',
381
+ ok: true,
382
+ bootstrap_code: bootstrap.code,
383
+ kickstart_code: kickstart.code,
384
+ print_code: printed.code,
385
+ error: null
386
+ };
387
+ }
388
+ }
389
+ return {
390
+ requested: true,
391
+ method: 'launchctl',
392
+ ok: kickstart.code === 0,
393
+ bootstrap_code: bootstrap.code,
394
+ kickstart_code: kickstart.code,
395
+ error: kickstart.code === 0 ? null : String(kickstart.stderr || kickstart.stdout || '').trim() || 'launchctl_kickstart_failed'
396
+ };
397
+ }
398
+ if (input.open) {
399
+ const opened = await runProcess(input.open, [input.appPath], {
400
+ timeoutMs: 10_000,
401
+ maxOutputBytes: 32 * 1024
402
+ }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
403
+ return {
404
+ requested: true,
405
+ method: 'open-fallback',
406
+ ok: opened.code === 0,
407
+ bootstrap_code: bootstrap.code,
408
+ open_code: opened.code,
409
+ error: opened.code === 0
410
+ ? null
411
+ : String(opened.stderr || opened.stdout || bootstrap.stderr || bootstrap.stdout || '').trim() || 'sks_menubar_launch_failed'
412
+ };
413
+ }
414
+ return {
415
+ requested: true,
416
+ method: 'launchctl',
417
+ ok: false,
418
+ bootstrap_code: bootstrap.code,
419
+ error: String(bootstrap.stderr || bootstrap.stdout || '').trim() || 'launchctl_bootstrap_failed'
420
+ };
421
+ }
422
+ async function waitForLaunchctlRunning(launchctl, service) {
423
+ let lastCode = null;
424
+ for (let attempt = 0; attempt < 30; attempt += 1) {
425
+ const printed = await runProcess(launchctl, ['print', service], {
426
+ timeoutMs: 2_000,
427
+ maxOutputBytes: 64 * 1024
428
+ }).catch(() => ({ code: 1, stdout: '', stderr: '' }));
429
+ lastCode = printed.code;
430
+ if (printed.code === 0 && /\bstate = running\b|\bpid = \d+\b/.test(`${printed.stdout || ''}\n${printed.stderr || ''}`)) {
431
+ return { code: printed.code, running: true };
432
+ }
433
+ await new Promise((resolve) => setTimeout(resolve, 500));
434
+ }
435
+ return { code: lastCode, running: false };
436
+ }
437
+ async function terminateExistingMenuBarProcess(executablePath) {
438
+ const pkill = await which('pkill').catch(() => null) || await fallbackTool('/usr/bin/pkill');
439
+ if (!pkill)
440
+ return;
441
+ await runProcess(pkill, ['-f', executablePath], { timeoutMs: 5_000, maxOutputBytes: 8 * 1024 }).catch(() => undefined);
442
+ await new Promise((resolve) => setTimeout(resolve, 250));
443
+ }
444
+ function swiftString(value) {
445
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
446
+ }
447
+ function shellQuote(value) {
448
+ return `'${value.replace(/'/g, `'\\''`)}'`;
449
+ }
450
+ function escapeXml(value) {
451
+ return value
452
+ .replace(/&/g, '&amp;')
453
+ .replace(/</g, '&lt;')
454
+ .replace(/>/g, '&gt;')
455
+ .replace(/"/g, '&quot;')
456
+ .replace(/'/g, '&apos;');
457
+ }
458
+ //# sourceMappingURL=sks-menubar.js.map
@@ -457,7 +457,11 @@ function codexNativeSksMenuStatus() {
457
457
  status: 'unsupported_official_api',
458
458
  requested_label: 'SKS',
459
459
  requested_position: 'between Window and Help',
460
+ companion_supported: process.platform === 'darwin',
461
+ companion_position: 'right macOS menu bar status item',
462
+ companion_install_command: 'sks doctor --fix',
460
463
  alternatives: [
464
+ 'sks doctor --fix',
461
465
  'sks codex-lb use-codex-lb',
462
466
  'sks codex-lb use-oauth',
463
467
  'sks codex-app set-openrouter-key --api-key-stdin',
@@ -559,7 +563,7 @@ export function codexAppGuidance({ appInstalled, codex, mcpList, featureList, re
559
563
  lines.push('Codex App git actions are enabled for Commit, Push, Commit and Push, and PR flows; SKS hooks treat those app metadata actions as lightweight git UI actions.');
560
564
  }
561
565
  if (nativeSksMenu?.supported === false) {
562
- lines.push('Native macOS SKS menu is not installed because Codex App does not expose a documented third-party native menu extension API; use `sks codex-lb use-codex-lb`, `sks codex-lb use-oauth`, and `sks codex-app set-openrouter-key --api-key-stdin` instead.');
566
+ lines.push('Codex App does not expose a documented third-party API for inserting SKS between Window and Help, so SKS uses a right-side macOS menu bar companion instead. Run `sks doctor --fix` to install or refresh it.');
563
567
  }
564
568
  if (appInstalled && (!computerUseReady || !browserToolReady)) {
565
569
  lines.push('Open Codex App settings and enable recommended MCP/plugin tools. Codex CLI 0.130.0+ remote-control/app-server sessions can pick up config changes live; restart older CLI/TUI sessions.');
@@ -113,24 +113,29 @@ export async function updateCheckCommand(args = []) {
113
113
  return printJson(result);
114
114
  console.log(`${sksTextLogo()}\n\n${formatSksUpdateCheckText(result)}`);
115
115
  }
116
- export async function updateCommand(sub = 'check', args = []) {
117
- const action = String(sub || 'check').toLowerCase();
116
+ export async function updateCommand(sub = 'now', args = []) {
117
+ let action = String(sub || 'now').toLowerCase();
118
+ let effectiveArgs = args;
119
+ if (action.startsWith('-')) {
120
+ effectiveArgs = [String(sub), ...args];
121
+ action = 'now';
122
+ }
118
123
  if (action === 'check' || action === 'status')
119
- return updateCheckCommand(args);
124
+ return updateCheckCommand(effectiveArgs);
120
125
  if (action !== 'now') {
121
- console.error('Usage: sks update check|now [--version <version>] [--json] [--dry-run]');
126
+ console.error('Usage: sks update [check|now] [--version <version>] [--json] [--dry-run]');
122
127
  process.exitCode = 1;
123
128
  return;
124
129
  }
125
130
  const root = await projectRoot();
126
131
  const result = await withSecretPreservationGuard(root, 'update-now', async () => runSksUpdateNow({
127
- version: valueAfter(args, '--version') || valueAfter(args, '-v'),
128
- dryRun: flag(args, '--dry-run'),
132
+ version: valueAfter(effectiveArgs, '--version') || valueAfter(effectiveArgs, '-v'),
133
+ dryRun: flag(effectiveArgs, '--dry-run'),
129
134
  projectRoot: root,
130
135
  timeoutMs: 10 * 60 * 1000,
131
136
  maxOutputBytes: 128 * 1024
132
137
  }));
133
- if (flag(args, '--json'))
138
+ if (flag(effectiveArgs, '--json'))
134
139
  return printJson(result);
135
140
  console.log(`${sksTextLogo()}\n`);
136
141
  console.log(`SKS update ${result.status}`);
@@ -144,6 +149,8 @@ export async function updateCommand(sub = 'check', args = []) {
144
149
  console.log(`New version: ${result.new_version}`);
145
150
  if (result.project_receipt)
146
151
  console.log(`Migration receipt: ${result.project_receipt.root} (${result.migration_current ? 'current' : 'not current'})`);
152
+ if (result.sks_menubar)
153
+ console.log(`SKS menu bar: ${result.sks_menubar.status}${result.sks_menubar.app_path ? ` (${result.sks_menubar.app_path})` : ''}`);
147
154
  for (const stage of result.stages || [])
148
155
  console.log(`Stage ${stage.id}: ${stage.ok ? 'ok' : 'failed'} ${stage.status}`);
149
156
  if (result.error)
@@ -89,6 +89,8 @@ function phaseInputFiles(id) {
89
89
  return ['src/core/doctor/codex-startup-config-repair.ts', '.codex/config.toml'];
90
90
  if (id.includes('supabase'))
91
91
  return ['src/core/doctor/supabase-mcp-repair.ts', '.codex/config.toml'];
92
+ if (id.includes('menubar'))
93
+ return ['src/core/codex-app/sks-menubar.ts', 'src/commands/doctor.ts', 'package.json'];
92
94
  if (id.includes('skill'))
93
95
  return ['.agents/skills', 'src/scripts/skill-registry-ledger-check.ts'];
94
96
  if (id.includes('native'))
@@ -60,6 +60,8 @@ export function buildDoctorReadinessMatrix(input = {}) {
60
60
  blockers.add('codex_app_fast_ui_repair_requires_confirmation');
61
61
  if (input.codex_app_ui?.fast_selector === 'repaired')
62
62
  warnings.add('codex_app_fast_selector_repaired_restart_app_if_needed');
63
+ if (input.sks_menubar?.ok === false)
64
+ warnings.add(`sks_menubar_${input.sks_menubar?.status || 'blocked'}`);
63
65
  const codex0138Doctor = input.codex_0138_doctor || null;
64
66
  if (codex0138Doctor?.ok === false)
65
67
  for (const blocker of normalizeList(codex0138Doctor.blockers))
@@ -148,6 +150,7 @@ export function buildDoctorReadinessMatrix(input = {}) {
148
150
  codex_app_harness_matrix: codexAppHarness,
149
151
  fast_mode_ready: input.fast_mode_ready !== false,
150
152
  codex_app_ui: input.codex_app_ui || null,
153
+ sks_menubar: input.sks_menubar || null,
151
154
  hooks_ready: input.hooks_ready !== false,
152
155
  codex_app_ready: input.codex_app?.ok === true,
153
156
  codex_app_required_for_cli: false,
@@ -205,6 +208,7 @@ function buildRepairReadiness(input = {}) {
205
208
  add('context7_repair', input.context7_repair, true);
206
209
  add('context7_mcp_repair', input.context7_mcp_repair, true);
207
210
  add('supabase_mcp_repair', input.supabase_mcp_repair, input.supabase_mcp_repair?.ready_blocking === true);
211
+ add('sks_menubar', input.sks_menubar, false);
208
212
  add('command_alias_cleanup', input.command_aliases, true);
209
213
  add('native_capability_repair', input.doctor_native_capability, false);
210
214
  if (input.doctor_fix_transaction) {
package/dist/core/fsx.js CHANGED
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import crypto from 'node:crypto';
6
6
  import { spawn } from 'node:child_process';
7
7
  import { fileURLToPath } from 'node:url';
8
- export const PACKAGE_VERSION = '4.7.3';
8
+ export const PACKAGE_VERSION = '4.8.0';
9
9
  export const DEFAULT_PROCESS_TAIL_BYTES = 256 * 1024;
10
10
  export const DEFAULT_PROCESS_TIMEOUT_MS = 30 * 60 * 1000;
11
11
  export function nowIso() {
@@ -1,6 +1,6 @@
1
1
  import { REQUIRED_CODEX_MODEL } from '../codex-model-guard.js';
2
2
  export const MANAGED_ASSET_SCHEMA_VERSION = 1;
3
- export const MANAGED_ASSET_VERSION = '4.7.3';
3
+ export const MANAGED_ASSET_VERSION = '4.8.0';
4
4
  export const MANAGED_ASSET_MARKER = 'SKS-MANAGED-ASSET';
5
5
  export const MANAGED_AGENT_ROLES = Object.freeze([
6
6
  role('sks-explorer', 'analysis-scout.toml', 'analysis_scout', 'SKS analysis scout for bounded read/write slices retained for stale Codex agent-role config repair.', 'workspace-write', ['analysis-scout', 'analysis_scout']),
@@ -699,7 +699,7 @@ export const COMMAND_CATALOG = [
699
699
  { name: 'quickstart', usage: 'sks quickstart', description: 'Show the shortest safe setup and verification flow.' },
700
700
  { name: 'bootstrap', usage: 'sks bootstrap [--install-scope global|project] [--local-only] [--json]', description: 'Initialize the current project, install SKS Codex App files/skills, check Context7/Codex App/Zellij, and print ready true/false.' },
701
701
  { name: 'root', usage: 'sks root [--json]', description: 'Show whether SKS is using a project root or the per-user global SKS runtime root.' },
702
- { name: 'update', usage: 'sks update check|now [--version <version>] [--json] [--dry-run]', description: 'Check for SKS updates or install the requested package version through npm global mode.' },
702
+ { name: 'update', usage: 'sks update [check|now] [--version <version>] [--json] [--dry-run]', description: 'Check npm for the latest SKS release and update the global package by default; use `check` for status-only.' },
703
703
  { name: 'deps', usage: 'sks deps check [--json] [--yes]', description: 'Check Node/npm, Codex CLI, and Zellij readiness; pass --yes to repair missing Codex CLI/Zellij tooling when supported.' },
704
704
  { name: 'codex', usage: 'sks codex compatibility|version|doctor|schema|0.142 [--json]', description: 'Check Codex CLI rust-v0.142.0 compatibility, installed version, 0.142 manifest/capability evidence, inherited legacy baselines, and vendored hook schema snapshot freshness.' },
705
705
  { name: 'codex-app', usage: 'sks codex-app [check|glm-profile install|set-openrouter-key --api-key-stdin|product-design|chrome-extension|pat status|remote-control]', description: 'Check Codex App install, GLM/OpenRouter model profile visibility, codex-lb key-entry guidance, Product Design plugin readiness, Codex Chrome Extension web verification readiness, PAT-safe status, first-party MCP/plugin readiness, and Codex CLI 0.130.0+ remote-control availability.' },
@@ -4,6 +4,7 @@ import { PACKAGE_VERSION, packageRoot, readJson, runProcess, which } from './fsx
4
4
  import { createRequestedScopeContract } from './safety/requested-scope-contract.js';
5
5
  import { guardedPackageInstall, guardContextForRoute } from './safety/mutation-guard.js';
6
6
  import { isUpdateMigrationReceiptCurrent, resolveInstalledSksEntrypoint, runPackageLocalDoctor, writeProjectUpdateMigrationReceipt } from './update/update-migration-state.js';
7
+ import { installSksMenuBar } from './codex-app/sks-menubar.js';
7
8
  const DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
8
9
  export async function runSksUpdateCheck(options = {}) {
9
10
  const packageName = options.packageName || 'sneakoscope';
@@ -164,6 +165,9 @@ export async function runSksUpdateNow(options = {}) {
164
165
  }).catch(() => null);
165
166
  const migrationCurrent = isUpdateMigrationReceiptCurrent(receipt);
166
167
  stage('project_receipt', migrationCurrent, migrationCurrent ? 'current' : 'failed', { root: projectReceiptRoot });
168
+ const sksMenuBar = migrationCurrent
169
+ ? await installUpdateSksMenuBar({ root: projectReceiptRoot, env, stage })
170
+ : null;
167
171
  return buildUpdateNowResult({
168
172
  packageName,
169
173
  from: check.current,
@@ -185,19 +189,14 @@ export async function runSksUpdateNow(options = {}) {
185
189
  newVersionDoctor: null,
186
190
  projectReceipt: receipt,
187
191
  migrationCurrent,
192
+ sksMenuBar,
188
193
  stages,
189
194
  error: null
190
195
  });
191
196
  }
192
- const oldVersionDoctor = await runPackageLocalDoctor({
193
- root: projectReceiptRoot,
194
- args: ['doctor', '--fix', '--yes', '--profile', 'migration', '--machine-only', '--report-file', path.join(projectReceiptRoot, '.sneakoscope', 'update', `old-version-doctor-${Date.now()}.json`)],
195
- env,
196
- timeoutMs: 15_000,
197
- maxOutputBytes: 32 * 1024
198
- });
199
- stage('old_version_doctor_preflight', oldVersionDoctor.ok, oldVersionDoctor.status, { entrypoint: oldVersionDoctor.entrypoint, exit_code: oldVersionDoctor.exit_code });
200
- if (!oldVersionDoctor.ok && env.SKS_UPDATE_SKIP_OLD_DOCTOR_PREFLIGHT !== '1') {
197
+ if (options.dryRun) {
198
+ stage('old_version_doctor_preflight', true, 'skipped_dry_run', { reason: 'dry_run_does_not_run_doctor_fix' });
199
+ stage('npm_install', true, 'dry_run', { command });
201
200
  return buildUpdateNowResult({
202
201
  packageName,
203
202
  from: check.current,
@@ -210,21 +209,32 @@ export async function runSksUpdateNow(options = {}) {
210
209
  cwd,
211
210
  registry,
212
211
  globalRoot,
213
- status: 'failed',
214
- ok: false,
212
+ status: 'dry_run',
213
+ ok: true,
215
214
  installCode: null,
216
- oldVersionDoctor,
215
+ oldVersionDoctor: null,
217
216
  newBinary: null,
218
217
  newVersion: null,
219
218
  newVersionDoctor: null,
220
219
  projectReceipt: null,
221
220
  migrationCurrent: false,
222
221
  stages,
223
- error: oldVersionDoctor.error || 'old-version Doctor preflight failed'
222
+ error: null
224
223
  });
225
224
  }
226
- if (options.dryRun) {
227
- stage('npm_install', true, 'dry_run', { command });
225
+ const oldDoctorTimeoutOverride = Number.parseInt(env.SKS_UPDATE_OLD_DOCTOR_TIMEOUT_MS || '', 10);
226
+ const oldDoctorTimeoutMs = Number.isFinite(oldDoctorTimeoutOverride) && oldDoctorTimeoutOverride > 0
227
+ ? oldDoctorTimeoutOverride
228
+ : 60_000;
229
+ const oldVersionDoctor = await runPackageLocalDoctor({
230
+ root: projectReceiptRoot,
231
+ args: ['doctor', '--fix', '--yes', '--profile', 'migration', '--machine-only', '--report-file', path.join(projectReceiptRoot, '.sneakoscope', 'update', `old-version-doctor-${Date.now()}.json`)],
232
+ env,
233
+ timeoutMs: oldDoctorTimeoutMs,
234
+ maxOutputBytes: 32 * 1024
235
+ });
236
+ stage('old_version_doctor_preflight', oldVersionDoctor.ok, oldVersionDoctor.status, { entrypoint: oldVersionDoctor.entrypoint, exit_code: oldVersionDoctor.exit_code, timeout_ms: oldDoctorTimeoutMs });
237
+ if (!oldVersionDoctor.ok && env.SKS_UPDATE_SKIP_OLD_DOCTOR_PREFLIGHT !== '1') {
228
238
  return buildUpdateNowResult({
229
239
  packageName,
230
240
  from: check.current,
@@ -237,8 +247,8 @@ export async function runSksUpdateNow(options = {}) {
237
247
  cwd,
238
248
  registry,
239
249
  globalRoot,
240
- status: 'dry_run',
241
- ok: true,
250
+ status: 'failed',
251
+ ok: false,
242
252
  installCode: null,
243
253
  oldVersionDoctor,
244
254
  newBinary: null,
@@ -247,7 +257,7 @@ export async function runSksUpdateNow(options = {}) {
247
257
  projectReceipt: null,
248
258
  migrationCurrent: false,
249
259
  stages,
250
- error: null
260
+ error: oldVersionDoctor.error || 'old-version Doctor preflight failed'
251
261
  });
252
262
  }
253
263
  const mutationLedgerRoot = env.SKS_MUTATION_LEDGER_ROOT || packageRoot();
@@ -277,6 +287,7 @@ export async function runSksUpdateNow(options = {}) {
277
287
  let newVersionDoctor = null;
278
288
  let projectReceipt = null;
279
289
  let migrationCurrent = false;
290
+ let sksMenuBar = null;
280
291
  if (installOk) {
281
292
  newBinary = await resolveInstalledSksEntrypoint({ packageName, globalRoot, env });
282
293
  stage('resolve_new_package_local_binary', Boolean(newBinary), newBinary ? 'resolved' : 'missing', { new_binary: newBinary });
@@ -310,6 +321,8 @@ export async function runSksUpdateNow(options = {}) {
310
321
  }).catch(() => null);
311
322
  migrationCurrent = isUpdateMigrationReceiptCurrent(projectReceipt);
312
323
  stage('project_receipt', migrationCurrent, migrationCurrent ? 'current' : 'failed', { root: projectReceiptRoot });
324
+ if (migrationCurrent)
325
+ sksMenuBar = await installUpdateSksMenuBar({ root: projectReceiptRoot, env, stage });
313
326
  }
314
327
  }
315
328
  const ok = installOk && Boolean(newBinary) && newVersionDoctor?.ok === true && migrationCurrent;
@@ -334,6 +347,7 @@ export async function runSksUpdateNow(options = {}) {
334
347
  newVersionDoctor,
335
348
  projectReceipt,
336
349
  migrationCurrent,
350
+ sksMenuBar,
337
351
  stages,
338
352
  error: ok ? null : updateNowError(install, newBinary, newVersionDoctor, migrationCurrent)
339
353
  });
@@ -500,10 +514,45 @@ function buildUpdateNowResult(input) {
500
514
  new_version_doctor: input.newVersionDoctor,
501
515
  project_receipt: input.projectReceipt,
502
516
  migration_current: input.migrationCurrent,
517
+ sks_menubar: input.sksMenuBar || null,
503
518
  stages: input.stages,
504
519
  error: input.error
505
520
  };
506
521
  }
522
+ async function installUpdateSksMenuBar(input) {
523
+ if (input.env.SKS_UPDATE_SKIP_SKS_MENUBAR === '1') {
524
+ input.stage('sks_menubar', true, 'skipped', { reason: 'SKS_UPDATE_SKIP_SKS_MENUBAR=1' });
525
+ return null;
526
+ }
527
+ const result = await installSksMenuBar({
528
+ root: input.root,
529
+ apply: true,
530
+ launch: true,
531
+ env: input.env
532
+ }).catch((err) => ({
533
+ schema: 'sks.codex-app-sks-menubar.v1',
534
+ ok: false,
535
+ apply: true,
536
+ status: 'blocked',
537
+ platform: process.platform,
538
+ app_path: null,
539
+ executable_path: null,
540
+ launch_agent_path: null,
541
+ action_script_path: null,
542
+ report_path: path.join(input.root, '.sneakoscope', 'reports', 'sks-menubar.json'),
543
+ menu_items: [],
544
+ actions: [],
545
+ launch: { requested: true, method: 'none', ok: false, error: err?.message || String(err) },
546
+ blockers: [err?.message || String(err)],
547
+ warnings: []
548
+ }));
549
+ input.stage('sks_menubar', result.ok !== false, result.status, {
550
+ app_path: result.app_path,
551
+ launch_agent_path: result.launch_agent_path,
552
+ launch: result.launch
553
+ });
554
+ return result;
555
+ }
507
556
  async function detectNpmGlobalRoot(npmBin, env, opts = {}) {
508
557
  const result = await runProcess(npmBin, ['root', '--global', '--silent'], {
509
558
  env,
@@ -1,2 +1,2 @@
1
- export const PACKAGE_VERSION = '4.7.3';
1
+ export const PACKAGE_VERSION = '4.8.0';
2
2
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,67 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { installSksMenuBar } from '../core/codex-app/sks-menubar.js';
5
+ const temp = await fs.mkdtemp(path.join(os.tmpdir(), 'sks-menubar-check-'));
6
+ const result = await installSksMenuBar({
7
+ apply: true,
8
+ launch: false,
9
+ home: temp,
10
+ root: temp,
11
+ sksEntry: path.join(process.cwd(), 'dist', 'bin', 'sks.js')
12
+ });
13
+ const executableExists = result.executable_path ? await exists(result.executable_path) : false;
14
+ const launchAgentExists = result.launch_agent_path ? await exists(result.launch_agent_path) : false;
15
+ const actionScriptExists = result.action_script_path ? await exists(result.action_script_path) : false;
16
+ const generatedSourcePath = result.app_path ? path.join(path.dirname(result.app_path), 'SKSMenuBar.swift') : null;
17
+ const generatedSource = generatedSourcePath ? await fs.readFile(generatedSourcePath, 'utf8').catch(() => '') : '';
18
+ const hasVisibleIconSource = generatedSource.includes('NSStatusItem.squareLength')
19
+ && generatedSource.includes('s.circle.fill')
20
+ && generatedSource.includes('configureStatusButton');
21
+ const expectedMenuItems = [
22
+ 'Use codex-lb',
23
+ 'Use ChatGPT OAuth',
24
+ 'Set OpenRouter Key and GLM Profiles',
25
+ 'Fast Check',
26
+ 'SKS Version Check',
27
+ 'Update SKS Now'
28
+ ];
29
+ const missingExpectedItems = expectedMenuItems.filter((item) => !result.menu_items.includes(item));
30
+ const hasExpectedItems = missingExpectedItems.length === 0;
31
+ const ok = process.platform === 'darwin'
32
+ ? result.ok === true
33
+ && result.status === 'installed_launch_skipped'
34
+ && executableExists
35
+ && launchAgentExists
36
+ && actionScriptExists
37
+ && hasExpectedItems
38
+ && hasVisibleIconSource
39
+ : result.ok === true && result.status === 'unsupported_platform';
40
+ const report = {
41
+ schema: 'sks.sks-menubar-install-check.v1',
42
+ ok,
43
+ temp,
44
+ result,
45
+ executable_exists: executableExists,
46
+ launch_agent_exists: launchAgentExists,
47
+ action_script_exists: actionScriptExists,
48
+ generated_source_path: generatedSourcePath,
49
+ has_visible_icon_source: hasVisibleIconSource,
50
+ expected_menu_items: expectedMenuItems,
51
+ missing_expected_items: missingExpectedItems,
52
+ has_expected_items: hasExpectedItems,
53
+ blockers: ok ? [] : ['sks_menubar_install_check_failed']
54
+ };
55
+ console.log(JSON.stringify(report, null, 2));
56
+ if (!ok)
57
+ process.exit(1);
58
+ async function exists(file) {
59
+ try {
60
+ await fs.access(file);
61
+ return true;
62
+ }
63
+ catch {
64
+ return false;
65
+ }
66
+ }
67
+ //# sourceMappingURL=sks-menubar-install-check.js.map
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs';
2
+ function assertGate(ok, message, detail) {
3
+ if (!ok) {
4
+ console.error(JSON.stringify({ schema: 'sks.update-default-command-check.v1', ok: false, message, detail }, null, 2));
5
+ process.exit(1);
6
+ }
7
+ }
8
+ const registry = fs.readFileSync('src/cli/command-registry.ts', 'utf8');
9
+ const basicCli = fs.readFileSync('src/core/commands/basic-cli.ts', 'utf8');
10
+ const update = fs.readFileSync('src/core/update-check.ts', 'utf8');
11
+ const routes = fs.readFileSync('src/core/routes.ts', 'utf8');
12
+ assertGate(registry.includes("subcommand(() => import(basicModule), 'updateCommand', 'dist/core/commands/basic-cli.js', 'now')"), 'bare sks update must default to update now');
13
+ assertGate(basicCli.includes("export async function updateCommand(sub: any = 'now'"), 'updateCommand default must be now');
14
+ assertGate(basicCli.includes("action.startsWith('-')") && basicCli.includes('effectiveArgs = [String(sub), ...args]'), 'sks update --json/--dry-run must be treated as update now with flags');
15
+ assertGate(update.includes('installSksMenuBar') && update.includes('sks_menubar'), 'update now must refresh the SKS menu bar companion stage');
16
+ assertGate(routes.includes('sks update [check|now]'), 'command catalog must document bare sks update as the default update path');
17
+ console.log(JSON.stringify({
18
+ schema: 'sks.update-default-command-check.v1',
19
+ ok: true,
20
+ checks: [
21
+ 'registry_default_now',
22
+ 'basic_cli_default_now',
23
+ 'flag_first_default_now',
24
+ 'update_menubar_stage',
25
+ 'command_catalog_usage'
26
+ ]
27
+ }, null, 2));
28
+ //# sourceMappingURL=update-default-command-check.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "sneakoscope",
3
3
  "displayName": "ใ……ใ…‹ใ……",
4
- "version": "4.7.3",
4
+ "version": "4.8.0",
5
5
  "description": "Sneakoscope Codex: fast proof-first Codex trust layer with image-based Voxel TriWiki.",
6
6
  "type": "module",
7
7
  "homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
@@ -168,6 +168,8 @@
168
168
  "codex:0.135-compat:require-real": "node ./dist/scripts/codex-0-135-compat-check.js --require-real",
169
169
  "doctor:codex-doctor-parity": "node ./dist/scripts/doctor-codex-doctor-parity-check.js",
170
170
  "doctor:codex-doctor-parity:actual": "node ./dist/scripts/doctor-codex-doctor-parity-check.js --actual-codex",
171
+ "doctor:sks-menubar": "node ./dist/scripts/sks-menubar-install-check.js",
172
+ "update:default-command": "node ./dist/scripts/update-default-command-check.js",
171
173
  "codex:permission-profiles": "node ./dist/scripts/codex-permission-profiles-check.js",
172
174
  "codex:legacy-profile-consumers-removed": "node ./dist/scripts/codex-legacy-profile-consumers-removed-check.js",
173
175
  "terminal:keyboard-enhancement-safety": "node ./dist/scripts/terminal-keyboard-enhancement-safety-check.js",