synabun 2.0.5 → 2.0.9
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/CHANGELOG.md +78 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/post-plan.mjs +6 -30
- package/hooks/claude-code/session-start.mjs +1 -4
- package/hooks/claude-code/shared.mjs +112 -75
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/package-processes.js +670 -0
- package/lib/packaged-runtime.js +138 -11
- package/lib/runtime-dependencies.js +225 -0
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/browser-bench.d.ts +1 -1
- package/mcp-server/dist/services/browser-bench.d.ts.map +1 -1
- package/mcp-server/dist/services/browser-bench.js +1 -1
- package/mcp-server/dist/services/browser-bench.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/categories.d.ts +35 -2
- package/mcp-server/dist/services/categories.d.ts.map +1 -1
- package/mcp-server/dist/services/categories.js +77 -33
- package/mcp-server/dist/services/categories.js.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/desktop-bench.d.ts +7 -7
- package/mcp-server/dist/services/desktop-bench.d.ts.map +1 -1
- package/mcp-server/dist/services/desktop-bench.js +1 -1
- package/mcp-server/dist/services/desktop-bench.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +11 -8
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +35 -14
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/bluesky.js +1 -1
- package/mcp-server/dist/tools/bluesky.js.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.d.ts +1 -1
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.js +1 -1
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts +1 -1
- package/mcp-server/dist/tools/browser-batch.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-batch.js +1 -1
- package/mcp-server/dist/tools/browser-batch.js.map +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.js +1 -1
- package/mcp-server/dist/tools/browser-cheatsheet.js.map +1 -1
- package/mcp-server/dist/tools/browser-interact.d.ts +1 -1
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-interact.js +1 -1
- package/mcp-server/dist/tools/browser-interact.js.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.d.ts +1 -1
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.js +1 -1
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
- package/mcp-server/dist/tools/browser-observe.d.ts +1 -1
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-observe.js +1 -1
- package/mcp-server/dist/tools/browser-observe.js.map +1 -1
- package/mcp-server/dist/tools/card-tools.d.ts +1 -1
- package/mcp-server/dist/tools/card-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/card-tools.js +1 -1
- package/mcp-server/dist/tools/card-tools.js.map +1 -1
- package/mcp-server/dist/tools/category.d.ts +1 -1
- package/mcp-server/dist/tools/category.d.ts.map +1 -1
- package/mcp-server/dist/tools/category.js +5 -1
- package/mcp-server/dist/tools/category.js.map +1 -1
- package/mcp-server/dist/tools/choice.d.ts +1 -1
- package/mcp-server/dist/tools/choice.d.ts.map +1 -1
- package/mcp-server/dist/tools/choice.js +1 -1
- package/mcp-server/dist/tools/choice.js.map +1 -1
- package/mcp-server/dist/tools/computer.d.ts +1 -1
- package/mcp-server/dist/tools/computer.d.ts.map +1 -1
- package/mcp-server/dist/tools/computer.js +1 -1
- package/mcp-server/dist/tools/computer.js.map +1 -1
- package/mcp-server/dist/tools/discord-channel.d.ts +1 -1
- package/mcp-server/dist/tools/discord-channel.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-channel.js +1 -1
- package/mcp-server/dist/tools/discord-channel.js.map +1 -1
- package/mcp-server/dist/tools/discord-guild.d.ts +1 -1
- package/mcp-server/dist/tools/discord-guild.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-guild.js +1 -1
- package/mcp-server/dist/tools/discord-guild.js.map +1 -1
- package/mcp-server/dist/tools/discord-member.d.ts +1 -1
- package/mcp-server/dist/tools/discord-member.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-member.js +1 -1
- package/mcp-server/dist/tools/discord-member.js.map +1 -1
- package/mcp-server/dist/tools/discord-message.d.ts +1 -1
- package/mcp-server/dist/tools/discord-message.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-message.js +1 -1
- package/mcp-server/dist/tools/discord-message.js.map +1 -1
- package/mcp-server/dist/tools/discord-onboarding.d.ts +1 -1
- package/mcp-server/dist/tools/discord-onboarding.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-onboarding.js +1 -1
- package/mcp-server/dist/tools/discord-onboarding.js.map +1 -1
- package/mcp-server/dist/tools/discord-role.d.ts +1 -1
- package/mcp-server/dist/tools/discord-role.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-role.js +1 -1
- package/mcp-server/dist/tools/discord-role.js.map +1 -1
- package/mcp-server/dist/tools/discord-thread.d.ts +1 -1
- package/mcp-server/dist/tools/discord-thread.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-thread.js +1 -1
- package/mcp-server/dist/tools/discord-thread.js.map +1 -1
- package/mcp-server/dist/tools/discord-webhook.d.ts +1 -1
- package/mcp-server/dist/tools/discord-webhook.d.ts.map +1 -1
- package/mcp-server/dist/tools/discord-webhook.js +1 -1
- package/mcp-server/dist/tools/discord-webhook.js.map +1 -1
- package/mcp-server/dist/tools/fb-groups.d.ts +1 -1
- package/mcp-server/dist/tools/fb-groups.d.ts.map +1 -1
- package/mcp-server/dist/tools/fb-groups.js +1 -1
- package/mcp-server/dist/tools/fb-groups.js.map +1 -1
- package/mcp-server/dist/tools/forget.d.ts +1 -1
- package/mcp-server/dist/tools/forget.d.ts.map +1 -1
- package/mcp-server/dist/tools/forget.js +1 -1
- package/mcp-server/dist/tools/forget.js.map +1 -1
- package/mcp-server/dist/tools/git-tools.d.ts +1 -1
- package/mcp-server/dist/tools/git-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/git-tools.js +1 -1
- package/mcp-server/dist/tools/git-tools.js.map +1 -1
- package/mcp-server/dist/tools/gsc/experience.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/experience.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/experience.js +1 -1
- package/mcp-server/dist/tools/gsc/experience.js.map +1 -1
- package/mcp-server/dist/tools/gsc/indexing.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/indexing.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/indexing.js +1 -1
- package/mcp-server/dist/tools/gsc/indexing.js.map +1 -1
- package/mcp-server/dist/tools/gsc/inspect.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/inspect.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/inspect.js +1 -1
- package/mcp-server/dist/tools/gsc/inspect.js.map +1 -1
- package/mcp-server/dist/tools/gsc/navigate.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/navigate.js +1 -1
- package/mcp-server/dist/tools/gsc/navigate.js.map +1 -1
- package/mcp-server/dist/tools/gsc/performance.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/performance.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/performance.js +1 -1
- package/mcp-server/dist/tools/gsc/performance.js.map +1 -1
- package/mcp-server/dist/tools/gsc/settings.d.ts +1 -1
- package/mcp-server/dist/tools/gsc/settings.d.ts.map +1 -1
- package/mcp-server/dist/tools/gsc/settings.js +1 -1
- package/mcp-server/dist/tools/gsc/settings.js.map +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts +1 -1
- package/mcp-server/dist/tools/image-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/image-tools.js +1 -1
- package/mcp-server/dist/tools/image-tools.js.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
- package/mcp-server/dist/tools/loop.d.ts +1 -1
- package/mcp-server/dist/tools/loop.d.ts.map +1 -1
- package/mcp-server/dist/tools/loop.js +1 -1
- package/mcp-server/dist/tools/loop.js.map +1 -1
- package/mcp-server/dist/tools/memories.d.ts +1 -1
- package/mcp-server/dist/tools/memories.d.ts.map +1 -1
- package/mcp-server/dist/tools/memories.js +1 -1
- package/mcp-server/dist/tools/memories.js.map +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts +1 -1
- package/mcp-server/dist/tools/morelogin.d.ts.map +1 -1
- package/mcp-server/dist/tools/morelogin.js +1 -1
- package/mcp-server/dist/tools/morelogin.js.map +1 -1
- package/mcp-server/dist/tools/profile.d.ts +1 -1
- package/mcp-server/dist/tools/profile.d.ts.map +1 -1
- package/mcp-server/dist/tools/profile.js +1 -1
- package/mcp-server/dist/tools/profile.js.map +1 -1
- package/mcp-server/dist/tools/recall.d.ts +1 -1
- package/mcp-server/dist/tools/recall.d.ts.map +1 -1
- package/mcp-server/dist/tools/recall.js +1 -1
- package/mcp-server/dist/tools/recall.js.map +1 -1
- package/mcp-server/dist/tools/reflect.d.ts +1 -1
- package/mcp-server/dist/tools/reflect.d.ts.map +1 -1
- package/mcp-server/dist/tools/reflect.js +1 -1
- package/mcp-server/dist/tools/reflect.js.map +1 -1
- package/mcp-server/dist/tools/remember.d.ts +1 -1
- package/mcp-server/dist/tools/remember.d.ts.map +1 -1
- package/mcp-server/dist/tools/remember.js +13 -11
- package/mcp-server/dist/tools/remember.js.map +1 -1
- package/mcp-server/dist/tools/restore.d.ts +1 -1
- package/mcp-server/dist/tools/restore.d.ts.map +1 -1
- package/mcp-server/dist/tools/restore.js +1 -1
- package/mcp-server/dist/tools/restore.js.map +1 -1
- package/mcp-server/dist/tools/style-guide-tools.d.ts +1 -1
- package/mcp-server/dist/tools/style-guide-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/style-guide-tools.js +1 -1
- package/mcp-server/dist/tools/style-guide-tools.js.map +1 -1
- package/mcp-server/dist/tools/sync.d.ts +1 -1
- package/mcp-server/dist/tools/sync.d.ts.map +1 -1
- package/mcp-server/dist/tools/sync.js +1 -1
- package/mcp-server/dist/tools/sync.js.map +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.d.ts +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.js +1 -1
- package/mcp-server/dist/tools/tictactoe-tools.js.map +1 -1
- package/mcp-server/dist/tools/utils.d.ts +1 -1
- package/mcp-server/dist/tools/utils.d.ts.map +1 -1
- package/mcp-server/dist/tools/utils.js +1 -1
- package/mcp-server/dist/tools/utils.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tools/whiteboard-tools.d.ts +3 -3
- package/mcp-server/dist/tools/whiteboard-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js +1 -1
- package/mcp-server/dist/tools/whiteboard-tools.js.map +1 -1
- package/mcp-server/dist/tools/youtube/analytics.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/analytics.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/analytics.js +1 -1
- package/mcp-server/dist/tools/youtube/analytics.js.map +1 -1
- package/mcp-server/dist/tools/youtube/download.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/download.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/download.js +1 -1
- package/mcp-server/dist/tools/youtube/download.js.map +1 -1
- package/mcp-server/dist/tools/youtube/media.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/media.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/media.js +1 -1
- package/mcp-server/dist/tools/youtube/media.js.map +1 -1
- package/mcp-server/dist/tools/youtube/metadata.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/metadata.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/metadata.js +1 -1
- package/mcp-server/dist/tools/youtube/metadata.js.map +1 -1
- package/mcp-server/dist/tools/youtube/navigate.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/navigate.js +1 -1
- package/mcp-server/dist/tools/youtube/navigate.js.map +1 -1
- package/mcp-server/dist/tools/youtube/playlist.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/playlist.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/playlist.js +1 -1
- package/mcp-server/dist/tools/youtube/playlist.js.map +1 -1
- package/mcp-server/dist/tools/youtube/schedule.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/schedule.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/schedule.js +1 -1
- package/mcp-server/dist/tools/youtube/schedule.js.map +1 -1
- package/mcp-server/dist/tools/youtube/source.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/source.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/source.js +1 -1
- package/mcp-server/dist/tools/youtube/source.js.map +1 -1
- package/mcp-server/dist/tools/youtube/upload.d.ts +1 -1
- package/mcp-server/dist/tools/youtube/upload.d.ts.map +1 -1
- package/mcp-server/dist/tools/youtube/upload.js +4 -9
- package/mcp-server/dist/tools/youtube/upload.js.map +1 -1
- package/mcp-server/dist/tui.js +18 -5
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +3 -3
- package/mcp-server/package.json +3 -3
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/browser-bench.ts +1 -1
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/categories.ts +77 -32
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/desktop-bench.ts +1 -1
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +34 -15
- package/mcp-server/src/tools/bluesky.ts +1 -1
- package/mcp-server/src/tools/browser-advanced.ts +1 -1
- package/mcp-server/src/tools/browser-batch.ts +1 -1
- package/mcp-server/src/tools/browser-cheatsheet.ts +1 -1
- package/mcp-server/src/tools/browser-interact.ts +1 -1
- package/mcp-server/src/tools/browser-navigate.ts +1 -1
- package/mcp-server/src/tools/browser-observe.ts +1 -1
- package/mcp-server/src/tools/card-tools.ts +1 -1
- package/mcp-server/src/tools/category.ts +6 -1
- package/mcp-server/src/tools/choice.ts +1 -1
- package/mcp-server/src/tools/computer.ts +1 -1
- package/mcp-server/src/tools/discord-channel.ts +1 -1
- package/mcp-server/src/tools/discord-guild.ts +1 -1
- package/mcp-server/src/tools/discord-member.ts +1 -1
- package/mcp-server/src/tools/discord-message.ts +1 -1
- package/mcp-server/src/tools/discord-onboarding.ts +1 -1
- package/mcp-server/src/tools/discord-role.ts +1 -1
- package/mcp-server/src/tools/discord-thread.ts +1 -1
- package/mcp-server/src/tools/discord-webhook.ts +1 -1
- package/mcp-server/src/tools/fb-groups.ts +1 -1
- package/mcp-server/src/tools/forget.ts +1 -1
- package/mcp-server/src/tools/git-tools.ts +1 -1
- package/mcp-server/src/tools/gsc/experience.ts +1 -1
- package/mcp-server/src/tools/gsc/indexing.ts +1 -1
- package/mcp-server/src/tools/gsc/inspect.ts +1 -1
- package/mcp-server/src/tools/gsc/navigate.ts +1 -1
- package/mcp-server/src/tools/gsc/performance.ts +1 -1
- package/mcp-server/src/tools/gsc/settings.ts +1 -1
- package/mcp-server/src/tools/image-tools.ts +1 -1
- package/mcp-server/src/tools/leonardo-browser-tools.ts +1 -1
- package/mcp-server/src/tools/loop.ts +1 -1
- package/mcp-server/src/tools/memories.ts +1 -1
- package/mcp-server/src/tools/morelogin.ts +1 -1
- package/mcp-server/src/tools/profile.ts +1 -1
- package/mcp-server/src/tools/recall.ts +1 -1
- package/mcp-server/src/tools/reflect.ts +1 -1
- package/mcp-server/src/tools/remember.ts +13 -12
- package/mcp-server/src/tools/restore.ts +1 -1
- package/mcp-server/src/tools/style-guide-tools.ts +1 -1
- package/mcp-server/src/tools/sync.ts +1 -1
- package/mcp-server/src/tools/tictactoe-tools.ts +1 -1
- package/mcp-server/src/tools/utils.ts +1 -1
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +1 -1
- package/mcp-server/src/tools/youtube/analytics.ts +1 -1
- package/mcp-server/src/tools/youtube/download.ts +1 -1
- package/mcp-server/src/tools/youtube/media.ts +1 -1
- package/mcp-server/src/tools/youtube/metadata.ts +1 -1
- package/mcp-server/src/tools/youtube/navigate.ts +1 -1
- package/mcp-server/src/tools/youtube/playlist.ts +1 -1
- package/mcp-server/src/tools/youtube/schedule.ts +1 -1
- package/mcp-server/src/tools/youtube/source.ts +1 -1
- package/mcp-server/src/tools/youtube/upload.ts +4 -12
- package/mcp-server/src/tui.ts +24 -10
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +645 -57
- package/neural-interface/i18n/pt-BR.json +644 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
- package/neural-interface/lib/agent-sandbox-setting.js +57 -0
- package/neural-interface/lib/assistant-api.js +274 -52
- package/neural-interface/lib/assistant-brains/claude.js +15 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-config.js +62 -14
- package/neural-interface/lib/assistant-dispatch.js +432 -125
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-persona.js +68 -11
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate-codex.js +3 -1
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +312 -48
- package/neural-interface/lib/assistant-user-writer.js +4 -2
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +90 -3
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +3 -3
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/codex-account-auth.js +180 -0
- package/neural-interface/lib/codex-accounts.js +502 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +122 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/exec-loop-command.js +3 -1
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-read.js +34 -2
- package/neural-interface/lib/install-details.js +163 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +22 -4
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +96 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/session-indexer.js +3 -1
- package/neural-interface/lib/session-title-generator.js +6 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/offline.html +16 -1
- package/neural-interface/public/onboarding.html +96 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +542 -0
- package/neural-interface/public/shared/account-usage.js +79 -0
- package/neural-interface/public/shared/account-view.js +336 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +39 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +288 -32
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
- package/neural-interface/public/shared/assistant/asst-menu.js +219 -80
- package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +87 -16
- package/neural-interface/public/shared/assistant/asst-render.js +38 -7
- package/neural-interface/public/shared/assistant/asst-route.js +72 -14
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +1 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +160 -29
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +49 -108
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +172 -111
- package/neural-interface/public/shared/cdx/cdx-tabs.js +264 -664
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +46 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-panel.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +1 -1
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/html-shell.js +1 -0
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +36 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +169 -2
- package/neural-interface/public/shared/pwa-install-page.js +3 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-ia.js +15 -0
- package/neural-interface/public/shared/showcase-privacy.js +490 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +281 -32
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +262 -217
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-explorer.js +2 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-loading.js +30 -6
- package/neural-interface/public/shared/ui-menubar.js +13 -3
- package/neural-interface/public/shared/ui-navbar.js +3 -1
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +55 -3
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +8 -0
- package/neural-interface/public/shared/ui-terminal.js +17 -0
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-update.js +598 -7
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +2 -0
- package/neural-interface/public/sw.js +1 -1
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/scripts/rebuild-pty.js +14 -2
- package/neural-interface/server.js +1384 -679
- package/npm-shrinkwrap.json +4424 -0
- package/package.json +57 -2
- package/postinstall.js +61 -47
- package/preuninstall.js +8 -35
- package/setup.js +80 -47
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/schedule.md +1 -1
- package/updater.cmd +2 -2
- package/updater.mjs +97 -3
|
@@ -9,6 +9,14 @@ import { createSessionListOverlay, overlaySessions, matchesSessionFilter } from
|
|
|
9
9
|
import { findSessionTranscript, sessionProjects, confineArtifact, confineSubPath, confineToRoot, staysInside, isInsideReal, isInside, isPlainName, PathConfineError } from './lib/path-confine.js';
|
|
10
10
|
import { readConfinedFile, writeConfinedFile, makeConfinedDir, removeConfinedEntry, listConfined, openConfinedStream } from './lib/confined-fs.js';
|
|
11
11
|
import { createClaudeAccounts as createPanelClaudeAccounts } from './lib/claude-accounts.js';
|
|
12
|
+
import { createClaudePanelAuth } from './lib/claude-panel-auth.js';
|
|
13
|
+
import { createCodexPanelAuth } from './lib/codex-panel-auth.js';
|
|
14
|
+
import { createCodexAccounts, applyCodexLaunchEnv, codexAppServerArgs, codexExecEnv } from './lib/codex-accounts.js';
|
|
15
|
+
import { codexWindowsSandbox, codexWindowsSandboxArgs } from './lib/agent-sandbox-elevation.js';
|
|
16
|
+
import { createAdminSandboxSetting } from './lib/agent-sandbox-setting.js';
|
|
17
|
+
import { createCodexAccountAuth } from './lib/codex-account-auth.js';
|
|
18
|
+
import { registerProviderPanelApiKeyAuthRoutes, redactProviderSecrets, providerApiKeyBodyError, providerDiagnosticSink } from './lib/provider-panel-api-key-auth.js';
|
|
19
|
+
import { createProviderPanelTerminalAuth, providerLoginSpec, registerProviderPanelTerminalAuthRoutes } from './lib/provider-panel-terminal-auth.js';
|
|
12
20
|
import { claudeLegacyPricing, claudeUsageCostUsd } from './lib/assistant-pricing.js';
|
|
13
21
|
import { HOOK_SCRIPTS, hookDefinitionsForEvent, hookCommandString, hookInstallBlocker, hookInstallPlan, isSynaBunHookCommand, sweepSettingsHooks, ensureHookRoot } from '../lib/claude-hooks.js';
|
|
14
22
|
import { browserV2Enabled, browserRequestMiddleware, assertBrowserRequestActive, browserActionTimeout, markBrowserActionStarted, runBrowserRequest, cancellableBrowserDelay, measureBrowserPhase, createEngagementScheduler, engagementTargetSelector, isEngagementWriteSelector, isXShortcutPublish, isXScriptedWrite } from './lib/browser-execution.js';
|
|
@@ -34,8 +42,9 @@ import { createPtyHostManager } from './lib/pty-host/manager.js';
|
|
|
34
42
|
import { hostNameOf, isAllowedHost, isAllowedWebSocketOrigin, parseCookies, refuseUpgrade } from './lib/http-guards.js';
|
|
35
43
|
import { createRequestGates, extraHostNames, hostsOf, savedValueOf, startedValueOf } from './lib/request-gate.js';
|
|
36
44
|
import { createGitDiffRouter } from './lib/git-diff-api.js';
|
|
37
|
-
import { gitRead, gitReadSync } from './lib/git-read.js';
|
|
45
|
+
import { gitRead, gitReadSync, setGitFallbackFolder, withGitFallback } from './lib/git-read.js';
|
|
38
46
|
import { createPhoneAuthority } from './lib/remote-policy.js';
|
|
47
|
+
import { brainCredentialOf } from './lib/virtual-socket.js';
|
|
39
48
|
import { buildTerminalEnv, interactiveShellArgs, cliWrapperArgs } from './lib/terminal-env.js';
|
|
40
49
|
import { startJudgeBackfill, stopJudgeBackfill } from '../mcp-server/dist/services/memory-judge-backfill.js';
|
|
41
50
|
import { judgeLoopGoal } from '../mcp-server/dist/services/memory-judgments.js';
|
|
@@ -106,12 +115,18 @@ import { loopLog, loopLogPath, flushLoopLogs } from './lib/loop-logger.js';
|
|
|
106
115
|
import {
|
|
107
116
|
buildInstallCommand,
|
|
108
117
|
buildUpdateCommand,
|
|
109
|
-
detectInstallSource,
|
|
110
118
|
isBrewSource,
|
|
111
119
|
parseBrewOutdated,
|
|
120
|
+
quoteCommandPath,
|
|
112
121
|
resolveBrewToken,
|
|
113
122
|
resolveLatest,
|
|
114
123
|
} from './lib/cli-update-planner.js';
|
|
124
|
+
import { createCliResolver, spawnArgsFor } from './lib/cli-resolver.js';
|
|
125
|
+
import { osPath } from './lib/os-path.js';
|
|
126
|
+
import { createCliInstaller, CLI_INSTALL_GUIDES } from './lib/cli-install.js';
|
|
127
|
+
import { createCliInstallApi, createCliPathApi, createGitBashApi } from './lib/cli-install-api.js';
|
|
128
|
+
import { createCliPathManager } from './lib/cli-path.js';
|
|
129
|
+
import { composeTerminalPath, terminalLaunchCommand, terminalArgument } from './lib/terminal-path.js';
|
|
115
130
|
import {
|
|
116
131
|
applyBackupRetention,
|
|
117
132
|
createVerifiedBackup,
|
|
@@ -145,11 +160,6 @@ import {
|
|
|
145
160
|
createCodexNativeLoopAdapter,
|
|
146
161
|
createOpenCodeNativeLoopAdapter,
|
|
147
162
|
} from './lib/native-loop-providers.js';
|
|
148
|
-
import {
|
|
149
|
-
resolveTrustedWindowsCodexBinary,
|
|
150
|
-
selectWindowsCodexLauncher,
|
|
151
|
-
windowsCodexPackageJsonCandidates,
|
|
152
|
-
} from './lib/codex-runtime-path.js';
|
|
153
163
|
import {
|
|
154
164
|
mergeCodexModelContextCapabilities,
|
|
155
165
|
resolveCodexExtendedContext,
|
|
@@ -233,10 +243,14 @@ import {
|
|
|
233
243
|
onClaudeModelsChanged,
|
|
234
244
|
shellCommand,
|
|
235
245
|
} from './lib/claude-model-catalog.js';
|
|
236
|
-
import {
|
|
237
|
-
import {
|
|
246
|
+
import { resolveClaudeSdkExecutable } from './lib/claude-executable.js';
|
|
247
|
+
import { collectInstallDetails } from './lib/install-details.js';
|
|
248
|
+
import { withPathEnv, pathEnvValue, getAugmentedPath, rememberedOsPath, rememberPackagedPath, rememberedPackagedPath, withPackagedPathLast } from './lib/augmented-path.js';
|
|
249
|
+
import { readCodexSettingsSchema, readCodexFeatureSupport, validateCodexSettings } from './lib/codex-settings-schema.js';
|
|
238
250
|
import { semverNewer, synabunReleaseChannel } from './lib/synabun-version.js';
|
|
239
251
|
import { selectSynabunUpdate, buildSynabunInstallPlan, refuseStaleUpdateClick } from './lib/synabun-update-plan.js';
|
|
252
|
+
import { collectActivity, countWhere, recentCliSessions } from './lib/system-activity.js';
|
|
253
|
+
import { createSystemAbout } from './lib/system-about.js';
|
|
240
254
|
import {
|
|
241
255
|
listCodexSessionsFromAccounts,
|
|
242
256
|
mergeCodexSessionSummaries,
|
|
@@ -260,11 +274,15 @@ import {
|
|
|
260
274
|
clampPointsToRect as wbClampPointsToRect,
|
|
261
275
|
convertPctInPlace as wbConvertPctInPlace,
|
|
262
276
|
} from '../mcp-server/dist/services/whiteboard-geometry.js';
|
|
263
|
-
import { ensureProjectCategories } from '../hooks/claude-code/shared.mjs';
|
|
277
|
+
import { ensureProjectCategories as ensureProjectCategoriesInFile, ensureCategories as ensureCategoriesInFile, FEATURE_CATEGORIES } from '../hooks/claude-code/shared.mjs';
|
|
264
278
|
import { getDataHome, getDataHomeDiagnostics, isGlobalInstall, pathIsInside } from '../lib/paths.js';
|
|
265
279
|
import { launcherLogPath } from '../lib/first-launch-protection.js';
|
|
266
280
|
import { registerStartLauncher, startLauncherState } from '../lib/start-launcher.js';
|
|
267
|
-
import {
|
|
281
|
+
import { effectiveNpmPrefix } from '../lib/child-path.js';
|
|
282
|
+
import { checkRuntimeDependencies, fallbackInstallArgs, fallbackInstallEnv, findInstalled, mcpCompiler, npmCommand, npmInvocation } from '../lib/runtime-dependencies.js';
|
|
283
|
+
import { redactDeep, redactSecrets } from '../lib/log-redact.js';
|
|
284
|
+
import { applyPackagedChildPath, durableEntry, mcpStdioCommand, npmGlobalBinFor, packagedNpmPrefix, packagedPathFolders, packagedRuntime } from '../lib/packaged-runtime.js';
|
|
285
|
+
import { dataHomeId } from '../lib/desktop-app.js';
|
|
268
286
|
import { pruneExternalTools } from '../lib/external-tools.js';
|
|
269
287
|
import { renderOfflinePage } from './lib/offline-page.js';
|
|
270
288
|
import {
|
|
@@ -313,11 +331,15 @@ const __dirname = dirname(__filename);
|
|
|
313
331
|
// This sweeps ALL platform directories, not just this host's: a vendored
|
|
314
332
|
// checkout can carry several, and repairing only the current one left e.g.
|
|
315
333
|
// prebuilds/darwin-x64/spawn-helper broken for Intel Macs.
|
|
334
|
+
// Both node_modules an install can have: the package root's (`npm i -g synabun` puts every dependency there) and
|
|
335
|
+
// the Neural Interface's own (a development checkout, a desktop build).
|
|
316
336
|
try {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
337
|
+
for (const root of new Set([__dirname, resolve(__dirname, '..')])) {
|
|
338
|
+
const sweep = ensureVendoredExecutables({ root, log: (msg) => console.log(msg) });
|
|
339
|
+
for (const failure of sweep.failed) {
|
|
340
|
+
console.warn(`[binaries] ${failure.path} is not launchable (${failure.state}).`
|
|
341
|
+
+ (failure.repairCommand ? ` Try: ${failure.repairCommand}` : ''));
|
|
342
|
+
}
|
|
321
343
|
}
|
|
322
344
|
} catch (err) {
|
|
323
345
|
console.warn('[binaries] Could not verify vendored binary permissions:', err.message);
|
|
@@ -346,6 +368,11 @@ let _greetingError = null;
|
|
|
346
368
|
let _assistantDispatcher = null;
|
|
347
369
|
// The Assistant's model catalog (lib/assistant-catalog.js): Settings → WhatsApp checks its brain choice against it.
|
|
348
370
|
let _assistantCatalog = null;
|
|
371
|
+
// The Claude account the Assistant's Claude Code workers use when a dispatch names none (assistant-config.json
|
|
372
|
+
// `workers.claudeAccount`); a function once the Assistant is wired. An account it names cannot be removed.
|
|
373
|
+
let _assistantClaudeWorkerAccount = null;
|
|
374
|
+
// The same for Codex workers (`workers.codexAccount`).
|
|
375
|
+
let _assistantCodexWorkerAccount = null;
|
|
349
376
|
// Who may ask, in front of every route (lib/request-gate.js). A request for the owner's data must name this
|
|
350
377
|
// server: everything under /api and /mcp, the invite sign-in, the rendered offline page and the uploaded icons
|
|
351
378
|
// answer 403 BAD_HOST to any other Host (a page whose name was re-pointed at this machine sends its own); the
|
|
@@ -371,6 +398,7 @@ app.use(express.json({ limit: '25mb' }));
|
|
|
371
398
|
// exceed the JSON limit. Default Express handler dumps a 10-line raw-body stack with no
|
|
372
399
|
// URL — this logs one line with context and responds 413 instead.
|
|
373
400
|
app.use((err, req, res, next) => {
|
|
401
|
+
if (/^\/api\/provider-panel-auth\/[^/]+\/[^/]+\/api-key(?:\?|$)/.test(req.originalUrl || req.url || '')) return providerApiKeyBodyError(err, req, res, next);
|
|
374
402
|
if (err && (err.type === 'entity.too.large' || err.type === 'entity.parse.failed')) {
|
|
375
403
|
const len = req.headers['content-length'] || '?';
|
|
376
404
|
console.warn(`[body-parser] ${err.type} ${req.method} ${req.originalUrl || req.url} content-length=${len} limit=${err.limit || '?'}`);
|
|
@@ -435,6 +463,8 @@ import { createEndpointCompletionEngine } from './lib/editor-completion-endpoint
|
|
|
435
463
|
import { ownerRouteGuard } from './lib/http-guards.js';
|
|
436
464
|
import { createCloudflaredManager } from './lib/cloudflared/manager.js';
|
|
437
465
|
import { createQuickTunnel } from './lib/cloudflared/tunnel.js';
|
|
466
|
+
import { createGitBashManager } from './lib/git-bash/manager.js';
|
|
467
|
+
import { settingsGitBashValue, syncClaudeSettingsGitBash } from './lib/git-bash/claude-settings.js';
|
|
438
468
|
|
|
439
469
|
const PORT = process.env.NEURAL_PORT || 3344;
|
|
440
470
|
// SQLite database is used directly via lib/db.js
|
|
@@ -515,49 +545,31 @@ const GLOBAL_CATEGORIES_PATH = resolve(CATEGORIES_DATA_DIR, 'custom-categories.j
|
|
|
515
545
|
const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
516
546
|
const CATEGORIES_POINT_ID = '00000000-0000-0000-0000-000000000000';
|
|
517
547
|
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
|
|
521
|
-
{
|
|
522
|
-
name: 'uncategorized',
|
|
523
|
-
description: 'Fallback category for memories stored without an explicit category. Any memory whose category is null, undefined, or empty is automatically assigned here during data normalization. Ensures every memory is always categorized for filtering, sidebar visibility toggling, and graph rendering.',
|
|
524
|
-
color: '#6b7280',
|
|
525
|
-
},
|
|
526
|
-
{
|
|
527
|
-
name: 'conversations',
|
|
528
|
-
description: 'Indexed conversation sessions with compacted summaries for cross-session recall. Stores session metadata (date, branch, session ID, file path) and topic summaries so users can search and retrieve past conversations naturally — e.g. "what did we work on yesterday?" or "remember that conversation about auth?"',
|
|
529
|
-
color: '#a855f7',
|
|
530
|
-
is_parent: true,
|
|
531
|
-
},
|
|
532
|
-
{
|
|
533
|
-
name: 'communication-style',
|
|
534
|
-
description: 'User communication patterns and preferences.',
|
|
535
|
-
},
|
|
536
|
-
{
|
|
537
|
-
name: 'plans',
|
|
538
|
-
description: 'Implementation plans stored after plan mode approval.',
|
|
539
|
-
color: '#06d6a0',
|
|
540
|
-
is_parent: true,
|
|
541
|
-
},
|
|
542
|
-
{
|
|
543
|
-
name: 'ideas',
|
|
544
|
-
description: 'Feature ideas, product concepts, brainstorms, and future plans. A general-purpose category for capturing forward-looking thoughts that haven\'t been implemented yet — new features to build, architectural experiments to try, UX improvements to explore, or any creative concept worth revisiting later.',
|
|
545
|
-
color: '#f59e0b',
|
|
546
|
-
},
|
|
547
|
-
];
|
|
548
|
-
|
|
548
|
+
// No default categories: a fresh install has none. A project's defaults are made
|
|
549
|
+
// when the project is added (ensureProjectCategories), a feature's category the
|
|
550
|
+
// first time the feature saves into it.
|
|
549
551
|
function getCategoriesPath() {
|
|
550
552
|
// Single categories file — no per-connection paths needed with SQLite
|
|
551
|
-
if (!existsSync(GLOBAL_CATEGORIES_PATH)) {
|
|
552
|
-
const dir = resolve(GLOBAL_CATEGORIES_PATH, '..');
|
|
553
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
554
|
-
writeFileSync(GLOBAL_CATEGORIES_PATH, JSON.stringify({ version: 1, categories: DEFAULT_CATEGORIES }, null, 2), 'utf-8');
|
|
555
|
-
}
|
|
556
|
-
|
|
557
553
|
return GLOBAL_CATEGORIES_PATH;
|
|
558
554
|
}
|
|
559
555
|
|
|
556
|
+
// The file not written yet: what SQLite (the source of truth) holds, which is nothing on a fresh install.
|
|
557
|
+
function categoriesFromDb() {
|
|
558
|
+
try {
|
|
559
|
+
return dbGetCategories().map((row) => {
|
|
560
|
+
const category = { name: row.name, description: row.description || '', created_at: row.created_at };
|
|
561
|
+
if (row.parent) category.parent = row.parent;
|
|
562
|
+
if (row.color) category.color = row.color;
|
|
563
|
+
if (row.is_parent) category.is_parent = true;
|
|
564
|
+
return category;
|
|
565
|
+
});
|
|
566
|
+
} catch {
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
560
571
|
function loadCategories() {
|
|
572
|
+
if (!existsSync(GLOBAL_CATEGORIES_PATH)) return categoriesFromDb();
|
|
561
573
|
try {
|
|
562
574
|
const raw = readFileSync(getCategoriesPath(), 'utf-8');
|
|
563
575
|
const data = JSON.parse(raw);
|
|
@@ -570,6 +582,27 @@ function loadCategories() {
|
|
|
570
582
|
}
|
|
571
583
|
}
|
|
572
584
|
|
|
585
|
+
// The categories file is written by the hooks' helpers (the file the MCP server watches); SQLite gets a copy, as with every other category write.
|
|
586
|
+
function syncCategoriesToDb(created) {
|
|
587
|
+
if (!created?.length) return created || [];
|
|
588
|
+
try { dbSaveCategories(loadCategories()); } catch (err) { console.error('Failed to sync categories to SQLite:', err.message); }
|
|
589
|
+
return created;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// A project's default categories (parent + project / architecture / bugs / config), made once, when the project is
|
|
593
|
+
// registered; never for a project already in the registry. Adds what is missing only.
|
|
594
|
+
function ensureProjectCategories(projects) {
|
|
595
|
+
return syncCategoriesToDb(ensureProjectCategoriesInFile(projects).created);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// A feature's own category (FEATURE_CATEGORIES: conversations, communication-style, plans, uncategorized), made the
|
|
599
|
+
// first time the feature saves into it; a fresh install has none. Adds it only when it is missing.
|
|
600
|
+
function ensureFeatureCategory(name) {
|
|
601
|
+
const feature = Object.prototype.hasOwnProperty.call(FEATURE_CATEGORIES, name) ? FEATURE_CATEGORIES[name] : null;
|
|
602
|
+
if (!feature) return [];
|
|
603
|
+
return syncCategoriesToDb(ensureCategoriesInFile([{ name, description: feature.description, is_parent: feature.is_parent }]).created);
|
|
604
|
+
}
|
|
605
|
+
|
|
573
606
|
function saveCategories(categories) {
|
|
574
607
|
const filePath = getCategoriesPath();
|
|
575
608
|
const dir = resolve(filePath, '..');
|
|
@@ -754,6 +787,26 @@ const INSTALL_KIND = PACKAGED_RUNTIME ? 'app' : isGlobalInstall() ? 'npm' : 'git
|
|
|
754
787
|
// What a person types to start a packaged application: its entry executable,
|
|
755
788
|
// when that is a path that will still be there once this run is over.
|
|
756
789
|
const START_ENTRY = durableEntry(PACKAGED_RUNTIME) || '';
|
|
790
|
+
// What the desktop app's window asks /api/health (lib/desktop-app.js): the version this server runs and which
|
|
791
|
+
// data home it uses, as a hash and never the path, so it can tell its own server from another installation's.
|
|
792
|
+
const SERVER_VERSION = (() => { try { return String(JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf-8')).version || ''); } catch { return ''; } })();
|
|
793
|
+
const DATA_HOME_ID = dataHomeId(DATA_HOME);
|
|
794
|
+
// The entry this install runs from, even when it does not outlive the run (START_ENTRY is then empty): how the
|
|
795
|
+
// desktop app tells its own server from another install's (lib/desktop-app.js classifyServer).
|
|
796
|
+
const INSTALL_ENTRY = PACKAGED_RUNTIME?.entry || '';
|
|
797
|
+
// A packaged application's own Node runtime, and the global bin of the npm that came with it (where `npm i -g`
|
|
798
|
+
// through it puts commands: the prefix as npm settles it from npm_config_prefix, the user's config file and the
|
|
799
|
+
// global one, else SynaBun's per-user folder its builtin npmrc names, never the application: lib/child-path.js,
|
|
800
|
+
// lib/packaged-runtime.js packagedNpmPrefix). On the PATH of everything this server starts, after everything of
|
|
801
|
+
// the user's: the user's own Node and npm stay first, these are the fallback on a machine without. The bin is asked
|
|
802
|
+
// for each PATH (cached for seconds), so a prefix set later counts without a restart. This process's own environment
|
|
803
|
+
// gets the same order, for every child that simply inherits it (an npm install, git, a CLI); the server's own
|
|
804
|
+
// Node children are started by process.execPath, never through PATH.
|
|
805
|
+
rememberPackagedPath({
|
|
806
|
+
last: packagedPathFolders(PACKAGED_RUNTIME).last,
|
|
807
|
+
npmBin: PACKAGED_RUNTIME ? () => packagedNpmPrefix(PACKAGED_RUNTIME)?.bin || null : null,
|
|
808
|
+
});
|
|
809
|
+
applyPackagedChildPath(process.env, PACKAGED_RUNTIME);
|
|
757
810
|
|
|
758
811
|
// Serve offline.html with what it needs once we are down: the install path,
|
|
759
812
|
// how it was installed, the launcher's state and the bridge code itself.
|
|
@@ -2142,6 +2195,10 @@ async function runGracefulShutdown(reason) {
|
|
|
2142
2195
|
// The public address: its cloudflared is ended with the server that owns the address (killed when it does not
|
|
2143
2196
|
// end by itself; never waited for more than a moment).
|
|
2144
2197
|
try { await tunnel.shutdown({ timeoutMs: 1500 }); } catch (err) { console.warn(' tunnel stop err:', err?.message); }
|
|
2198
|
+
// A CLI install under way (lib/cli-install.js): its installer and what it started are stopped.
|
|
2199
|
+
try { cliInstaller.stopAll(); } catch (err) { console.warn(' cli install stop err:', err?.message); }
|
|
2200
|
+
// A Git for Windows download under way (lib/git-bash/): stopped; its work folder goes with the next attempt.
|
|
2201
|
+
try { gitBash.cancel(); } catch (err) { console.warn(' git bash stop err:', err?.message); }
|
|
2145
2202
|
|
|
2146
2203
|
// 1. opencode sidecar — has its own teardown that handles managed PID + ollama.
|
|
2147
2204
|
try { stopOpencodeServer(); } catch (err) { console.warn(' opencode stop err:', err?.message); }
|
|
@@ -2344,6 +2401,18 @@ function cleanupOldUpdaterStages(baseDir) {
|
|
|
2344
2401
|
} catch {}
|
|
2345
2402
|
}
|
|
2346
2403
|
|
|
2404
|
+
// lib/package-processes.js and the module it imports (only node: modules in either).
|
|
2405
|
+
const UPDATER_STAGED_LIBS = ['package-processes.js', 'runtime-dependencies.js'];
|
|
2406
|
+
|
|
2407
|
+
// The forms of the package folder this server knows: as it resolved it, and as it was started
|
|
2408
|
+
// (`node <prefix>\node_modules\synabun\neural-interface\server.js` through a junction or a link).
|
|
2409
|
+
function updaterPackageRoots() {
|
|
2410
|
+
const roots = [PACKAGE_ROOT];
|
|
2411
|
+
const started = process.argv[1] ? resolve(process.argv[1]) : '';
|
|
2412
|
+
if (basename(started) === 'server.js' && basename(dirname(started)) === 'neural-interface') roots.push(resolve(started, '..', '..'));
|
|
2413
|
+
return [...new Set(roots)];
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2347
2416
|
function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [], safetySnapshot = null, handoffPath = null }) {
|
|
2348
2417
|
const sourceUpdater = resolve(PACKAGE_ROOT, 'updater.mjs');
|
|
2349
2418
|
if (!existsSync(sourceUpdater)) {
|
|
@@ -2361,6 +2430,16 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids
|
|
|
2361
2430
|
const payloadPath = resolve(stageDir, 'payload.json');
|
|
2362
2431
|
const runnerPath = resolve(stageDir, process.platform === 'win32' ? 'run-update.cmd' : 'run-update.sh');
|
|
2363
2432
|
copyFileSync(sourceUpdater, stagedUpdater);
|
|
2433
|
+
// What updater.mjs imports on Windows to stop the processes AI clients started from this folder: staged as lib/
|
|
2434
|
+
// next to it, with a package.json that makes those .js files ES modules on every Node SynaBun runs on. One that
|
|
2435
|
+
// cannot be copied leaves the updater without that step, never without the update.
|
|
2436
|
+
try {
|
|
2437
|
+
mkdirSync(resolve(stageDir, 'lib'), { recursive: true });
|
|
2438
|
+
writeFileSync(resolve(stageDir, 'lib', 'package.json'), '{ "type": "module" }\n', 'utf8');
|
|
2439
|
+
for (const file of UPDATER_STAGED_LIBS) copyFileSync(resolve(PACKAGE_ROOT, 'lib', file), resolve(stageDir, 'lib', file));
|
|
2440
|
+
} catch (err) {
|
|
2441
|
+
console.warn(`[server] could not stage lib/ for the updater: ${err?.message}`);
|
|
2442
|
+
}
|
|
2364
2443
|
|
|
2365
2444
|
const payload = {
|
|
2366
2445
|
serverPid,
|
|
@@ -2372,6 +2451,9 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids
|
|
|
2372
2451
|
target: installPlan.target,
|
|
2373
2452
|
channel: installPlan.channel,
|
|
2374
2453
|
cleanupPids,
|
|
2454
|
+
// The folder `npm i -g` replaces, in every form known here: on Windows the updater stops what still runs from it.
|
|
2455
|
+
packageRoot: PACKAGE_ROOT,
|
|
2456
|
+
packageRoots: updaterPackageRoots(),
|
|
2375
2457
|
safetySnapshot,
|
|
2376
2458
|
handoffPath,
|
|
2377
2459
|
// Where the relaunched launcher leaves its reason if it stops.
|
|
@@ -2462,6 +2544,13 @@ app.post('/api/system/run-update', async (req, res) => {
|
|
|
2462
2544
|
// Nothing is ever installed into the resources of a packaged application.
|
|
2463
2545
|
if (PACKAGED_RUNTIME) {
|
|
2464
2546
|
const openUrl = `${SYNABUN_GITHUB_REPO_URL}/releases`;
|
|
2547
|
+
// The desktop app updates itself, through its window's update function (packaging/desktop).
|
|
2548
|
+
if (PACKAGED_RUNTIME.shell) {
|
|
2549
|
+
return res.status(409).json({
|
|
2550
|
+
error: 'This copy of SynaBun is the desktop app, and it updates itself: use Check for updates in its menu. Nothing was installed.',
|
|
2551
|
+
packaged: true, desktopManaged: true, openUrl,
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2465
2554
|
return res.status(409).json({
|
|
2466
2555
|
error: `This copy of SynaBun is a packaged application. It is updated by installing a newer build over it, not from inside the app. Nothing was installed. Releases: ${openUrl}`,
|
|
2467
2556
|
packaged: true,
|
|
@@ -3247,36 +3336,88 @@ function _heartbeatLock(sessionId, windowId) {
|
|
|
3247
3336
|
}
|
|
3248
3337
|
}
|
|
3249
3338
|
|
|
3250
|
-
//
|
|
3251
|
-
|
|
3252
|
-
//
|
|
3253
|
-
//
|
|
3254
|
-
|
|
3255
|
-
|
|
3339
|
+
// ── Where the user's CLIs are (lib/cli-resolver.js) ──
|
|
3340
|
+
// One resolver for every lookup and launch of Claude Code, Codex and OpenCode:
|
|
3341
|
+
// the version check behind the side panels' banner and the update badges,
|
|
3342
|
+
// Settings' Detect, the one-click install (lib/cli-install.js) and the
|
|
3343
|
+
// launchers below. The path in Settings > Terminal first, then PATH with the OS
|
|
3344
|
+
// PATH read afresh (lib/os-path.js: the registry on Windows, the login shell on
|
|
3345
|
+
// macOS and Linux), then the folders each install method uses; never a copy
|
|
3346
|
+
// inside SynaBun. A CLI installed while SynaBun runs is found by the next
|
|
3347
|
+
// lookup: nothing here is remembered for more than a few seconds, and Re-check
|
|
3348
|
+
// forgets everything.
|
|
3349
|
+
const cliResolver = createCliResolver({
|
|
3350
|
+
isInsideSynabun: (file) => isInsideSynabun(file),
|
|
3351
|
+
configured: (toolKey) => loadCliConfig()[toolKey]?.command ?? null,
|
|
3352
|
+
});
|
|
3353
|
+
// The first read of the OS PATH starts now, so the first lookups already have it.
|
|
3354
|
+
osPath.refresh().catch(() => {});
|
|
3355
|
+
|
|
3356
|
+
// ── Git Bash for Claude Code on Windows (lib/git-bash/) ──
|
|
3357
|
+
// Claude Code on Windows runs its Bash tool and every hook through Git Bash and
|
|
3358
|
+
// falls back to PowerShell without one, where SynaBun's own hooks of an
|
|
3359
|
+
// installed app do not even parse. Which Git Bash a Claude Code started by
|
|
3360
|
+
// SynaBun gets: CLAUDE_CODE_GIT_BASH_PATH (this environment, or the user's
|
|
3361
|
+
// settings), a Git for Windows of the PC's own, then SynaBun's copy
|
|
3362
|
+
// (<resources>\git in the installer; DATA_HOME/runtime/git/<version>, fetched
|
|
3363
|
+
// once, on an npm install). claudeChildEnv() is the last word on the
|
|
3364
|
+
// environment of every Claude Code process SynaBun starts. Nothing changes on
|
|
3365
|
+
// macOS and Linux, or where the PC has a Git Bash Claude Code finds by itself;
|
|
3366
|
+
// the system's PATH and registry are never changed.
|
|
3367
|
+
const gitBash = createGitBashManager({
|
|
3368
|
+
dataHome: DATA_HOME,
|
|
3369
|
+
pathValue: () => [pathEnvValue(process.env), ...rememberedOsPath()].join(';'),
|
|
3370
|
+
settingsValue: () => settingsGitBashValue({ settingsPath: getGlobalClaudeSettingsPath(), read: readClaudeSettingsForWrite }),
|
|
3371
|
+
log: (line) => console.log(line),
|
|
3372
|
+
});
|
|
3373
|
+
function claudeChildEnv(env = process.env) {
|
|
3374
|
+
// A packaged application's runtime and npm after the user's own on PATH (rememberPackagedPath above).
|
|
3375
|
+
const ordered = withPackagedPathLast(env);
|
|
3376
|
+
try { return gitBash.childEnv(ordered); } catch { return ordered; }
|
|
3377
|
+
}
|
|
3378
|
+
// SynaBun's own git features (the terminal's branch and checkout, the File Explorer's and the code editor's git,
|
|
3379
|
+
// the git routes) run the copy's git.exe on a PC without git: its path for spawnGit, its folder for every read.
|
|
3380
|
+
setGitFallbackFolder(() => { const git = gitBash.gitExecutable(); return git ? dirname(git) : null; });
|
|
3381
|
+
// The user's own `claude`, started in another terminal: ~/.claude/settings.json names SynaBun's copy while it is
|
|
3382
|
+
// the one in use and loses that value when it is not; a value of the user's own is never touched. Windows only.
|
|
3383
|
+
function syncGitBashSettings(resolution = gitBash.resolution()) {
|
|
3384
|
+
const settingsPath = getGlobalClaudeSettingsPath();
|
|
3385
|
+
const result = syncClaudeSettingsGitBash({ resolution, settingsPath, read: readClaudeSettingsForWrite, write: writeClaudeSettings, ownRoots: gitBash.ownRoots });
|
|
3386
|
+
gitBash.noteSettings(result);
|
|
3387
|
+
if (result.state === 'written') console.log(`[git-bash] ${settingsPath}: CLAUDE_CODE_GIT_BASH_PATH set to ${result.value} (SynaBun's Git Bash, for a claude started outside SynaBun)`);
|
|
3388
|
+
else if (result.state === 'removed') console.log(`[git-bash] ${settingsPath}: CLAUDE_CODE_GIT_BASH_PATH removed (SynaBun's Git Bash is not the one in use)`);
|
|
3389
|
+
else if (result.state === 'error') console.warn(`[git-bash] ${settingsPath} was not changed: ${result.message}`);
|
|
3390
|
+
return result;
|
|
3391
|
+
}
|
|
3392
|
+
gitBash.onChange((resolution) => { syncGitBashSettings(resolution); });
|
|
3393
|
+
const _cliMissingLogged = new Set();
|
|
3394
|
+
function _noteCliLookup(toolKey, found, label) {
|
|
3395
|
+
if (!found?.path) {
|
|
3396
|
+
if (!_cliMissingLogged.has(toolKey)) {
|
|
3397
|
+
_cliMissingLogged.add(toolKey);
|
|
3398
|
+
console.warn(`[cli] ${label} not found (Settings path, PATH, the OS PATH and the known install folders). The side panel offers to install it.`);
|
|
3399
|
+
}
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
if (_cliMissingLogged.delete(toolKey)) console.log(`[cli] ${label} found: ${found.path}`);
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
/** The Settings > Terminal value of a tool when it is a command line ("wsl codex") that a shell runs as written. */
|
|
3406
|
+
function configuredCommandLine(toolKey) {
|
|
3407
|
+
let value = '';
|
|
3408
|
+
try { value = String(loadCliConfig()[toolKey]?.command || '').trim(); } catch {}
|
|
3409
|
+
if (!value || !/\s/.test(value) || existsSync(value.replace(/^"(.*)"$/, '$1'))) return null;
|
|
3410
|
+
return value;
|
|
3411
|
+
}
|
|
3256
3412
|
|
|
3257
|
-
// The Claude Code a terminal would start: the
|
|
3258
|
-
//
|
|
3259
|
-
//
|
|
3260
|
-
//
|
|
3413
|
+
// The Claude Code a terminal would start: the resolver's answer (Settings'
|
|
3414
|
+
// path, PATH, the OS PATH, the known install folders), always the user's own
|
|
3415
|
+
// installation. The bare 'claude' when there is none, which
|
|
3416
|
+
// resolveClaudeSdkExecutable and every caller read as "not installed".
|
|
3261
3417
|
function getClaudeBin() {
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
try { configured = loadCliConfig()['claude-code']?.command?.trim() || ''; } catch {}
|
|
3266
|
-
const found = resolveClaudeLauncher({ configured, isInsideSynabun });
|
|
3267
|
-
for (const rejected of found.rejected) {
|
|
3268
|
-
console.warn(`[claude-skin] Ignoring Claude CLI inside the SynaBun install: ${rejected}`);
|
|
3269
|
-
}
|
|
3270
|
-
if (!found.path) {
|
|
3271
|
-
if (!_claudeBinMissingAt) console.warn('[claude-skin] Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code');
|
|
3272
|
-
_claudeBinMissingAt = Date.now();
|
|
3273
|
-
_claudeBinPath = 'claude'; // last resort — will fail at spawn time with a clear error
|
|
3274
|
-
return _claudeBinPath;
|
|
3275
|
-
}
|
|
3276
|
-
if (_claudeBinMissingAt) console.log(`[claude-skin] Claude CLI found: ${found.path}`);
|
|
3277
|
-
_claudeBinMissingAt = 0;
|
|
3278
|
-
_claudeBinPath = found.path;
|
|
3279
|
-
return _claudeBinPath;
|
|
3418
|
+
const found = cliResolver.launcher('claude-code');
|
|
3419
|
+
_noteCliLookup('claude-code', found, 'Claude Code');
|
|
3420
|
+
return found.path || 'claude';
|
|
3280
3421
|
}
|
|
3281
3422
|
|
|
3282
3423
|
// The executable every Agent SDK call is given (pathToClaudeCodeExecutable):
|
|
@@ -3291,131 +3432,63 @@ function getClaudeSdkExecutable() {
|
|
|
3291
3432
|
});
|
|
3292
3433
|
}
|
|
3293
3434
|
|
|
3294
|
-
|
|
3435
|
+
// Codex: what the SDK and the app-server start. On Windows the native codex.exe
|
|
3436
|
+
// an npm command file leads to (the SDK spawns without a shell); the file found
|
|
3437
|
+
// otherwise. SynaBun carries no Codex (lib/external-tools.js): without one,
|
|
3438
|
+
// Codex is reported as not installed. 'codex' when there is none.
|
|
3295
3439
|
let _codexBinSource = 'missing';
|
|
3296
|
-
let _codexBinMissingAt = 0;
|
|
3297
|
-
let _nativeCodexBinPath = null;
|
|
3298
3440
|
// The SDK is statically imported at module load; reaching this point is the
|
|
3299
3441
|
// authoritative capability check. Its package.json is intentionally not an
|
|
3300
3442
|
// exported subpath, so resolving that file reports a false negative.
|
|
3301
3443
|
const _codexSdkInstalled = true;
|
|
3302
3444
|
|
|
3303
|
-
function resolveWindowsGlobalCodexRuntime() {
|
|
3304
|
-
const globalPath = getAugmentedPath().split(delimiter)
|
|
3305
|
-
.filter((entry) => !/[\\/]node_modules[\\/]\.bin[\\/]?$/i.test(entry))
|
|
3306
|
-
.join(delimiter);
|
|
3307
|
-
const env = { ...process.env, PATH: globalPath };
|
|
3308
|
-
const whereExecutable = process.env.SystemRoot
|
|
3309
|
-
? join(process.env.SystemRoot, 'System32', 'where.exe')
|
|
3310
|
-
: 'where.exe';
|
|
3311
|
-
const lookup = spawnSync(whereExecutable, ['codex'], {
|
|
3312
|
-
encoding: 'utf-8',
|
|
3313
|
-
env,
|
|
3314
|
-
windowsHide: true,
|
|
3315
|
-
});
|
|
3316
|
-
const launchers = lookup.status === 0
|
|
3317
|
-
? String(lookup.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
3318
|
-
: [];
|
|
3319
|
-
|
|
3320
|
-
const npmRootProbe = spawnSync(
|
|
3321
|
-
process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
|
3322
|
-
['/d', '/s', '/c', 'npm root -g'],
|
|
3323
|
-
{ encoding: 'utf-8', env, windowsHide: true, timeout: 5000 },
|
|
3324
|
-
);
|
|
3325
|
-
const npmRoots = npmRootProbe.status === 0
|
|
3326
|
-
? String(npmRootProbe.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
3327
|
-
: [];
|
|
3328
|
-
const codexPackageJsonPaths = windowsCodexPackageJsonCandidates({
|
|
3329
|
-
npmRoots,
|
|
3330
|
-
appData: process.env.APPDATA,
|
|
3331
|
-
npmConfigPrefix: process.env.NPM_CONFIG_PREFIX || process.env.npm_config_prefix,
|
|
3332
|
-
synabunPackageRoot: PACKAGE_ROOT,
|
|
3333
|
-
nodeExecutable: process.execPath,
|
|
3334
|
-
});
|
|
3335
|
-
const resolved = resolveTrustedWindowsCodexBinary({
|
|
3336
|
-
launchers,
|
|
3337
|
-
codexPackageJsonPaths,
|
|
3338
|
-
arch: process.arch,
|
|
3339
|
-
acceptBinary: (candidate) => !isInsideSynabun(candidate),
|
|
3340
|
-
});
|
|
3341
|
-
return { ...resolved, launchers, codexPackageJsonPaths, npmRoots };
|
|
3342
|
-
}
|
|
3343
|
-
|
|
3344
3445
|
function getCodexBin() {
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
// handed this path, and without one Codex is reported as not installed.
|
|
3350
|
-
const globalPath = getAugmentedPath().split(delimiter)
|
|
3351
|
-
.filter((entry) => !/[\\/]node_modules[\\/]\.bin[\\/]?$/i.test(entry))
|
|
3352
|
-
.join(delimiter);
|
|
3353
|
-
const envWithPath = { ...process.env, PATH: globalPath };
|
|
3354
|
-
if (process.platform === 'win32') {
|
|
3355
|
-
const resolution = resolveWindowsGlobalCodexRuntime();
|
|
3356
|
-
if (resolution.path) {
|
|
3357
|
-
_codexBinPath = resolution.path;
|
|
3358
|
-
_nativeCodexBinPath = resolution.path;
|
|
3359
|
-
} else {
|
|
3360
|
-
_codexBinPath = selectWindowsCodexLauncher(
|
|
3361
|
-
resolution.launchers,
|
|
3362
|
-
(launcher) => !isInsideSynabun(launcher),
|
|
3363
|
-
);
|
|
3364
|
-
}
|
|
3365
|
-
} else {
|
|
3366
|
-
try { _codexBinPath = execSync('which codex', { encoding: 'utf-8', env: envWithPath }).trim(); }
|
|
3367
|
-
catch { _codexBinPath = null; }
|
|
3368
|
-
}
|
|
3369
|
-
if (_codexBinPath && _codexBinPath !== 'codex' && !isInsideSynabun(_codexBinPath)) {
|
|
3370
|
-
if (_codexBinMissingAt) console.log(`[codex-skin] Codex CLI found: ${_codexBinPath}`);
|
|
3371
|
-
_codexBinMissingAt = 0;
|
|
3372
|
-
_codexBinSource = 'global';
|
|
3373
|
-
return _codexBinPath;
|
|
3374
|
-
}
|
|
3375
|
-
if (!_codexBinMissingAt) {
|
|
3376
|
-
if (_codexBinPath && _codexBinPath !== 'codex') console.warn(`[codex-skin] Ignoring Codex CLI inside the SynaBun install: ${_codexBinPath}`);
|
|
3377
|
-
console.warn('[codex-skin] Codex CLI not found. Install: npm install -g @openai/codex');
|
|
3378
|
-
}
|
|
3379
|
-
_codexBinMissingAt = Date.now();
|
|
3380
|
-
_codexBinPath = 'codex';
|
|
3381
|
-
_codexBinSource = 'missing';
|
|
3382
|
-
return _codexBinPath;
|
|
3446
|
+
const found = cliResolver.launcher('codex');
|
|
3447
|
+
_noteCliLookup('codex', found, 'Codex');
|
|
3448
|
+
_codexBinSource = found.path ? 'global' : 'missing';
|
|
3449
|
+
return found.launchPath || found.path || 'codex';
|
|
3383
3450
|
}
|
|
3384
3451
|
|
|
3452
|
+
// Native Codex schedules hand the SDK a program it can start without a shell:
|
|
3453
|
+
// on Windows a codex.exe, never a command file.
|
|
3385
3454
|
function getNativeCodexBin() {
|
|
3386
|
-
|
|
3387
|
-
|
|
3455
|
+
const found = cliResolver.launcher('codex');
|
|
3456
|
+
_codexBinSource = found.path ? 'global' : 'missing';
|
|
3388
3457
|
if (process.platform === 'win32') {
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
_codexBinSource = 'global';
|
|
3394
|
-
console.log(`[codex-native] Resolved Windows runtime (${resolved.source}): ${resolved.path}`);
|
|
3395
|
-
return _nativeCodexBinPath;
|
|
3396
|
-
}
|
|
3397
|
-
if (resolved.reason) {
|
|
3398
|
-
console.warn(`[codex-native] ${resolved.reason}`);
|
|
3399
|
-
}
|
|
3400
|
-
if (resolved.checked?.length) {
|
|
3401
|
-
console.warn(`[codex-native] Checked: ${resolved.checked.slice(0, 16).join(' | ')}`);
|
|
3402
|
-
}
|
|
3403
|
-
const globalPackageFound = resolved.codexPackageJsonPaths.some((candidate) => existsSync(candidate));
|
|
3404
|
-
throw new Error(
|
|
3405
|
-
globalPackageFound
|
|
3406
|
-
? 'The global Codex package was found, but its Windows runtime is missing. Reinstall it with: npm install -g @openai/codex@latest --include=optional, then restart SynaBun.'
|
|
3407
|
-
: 'Codex is not installed globally or could not be located. Install it with: npm install -g @openai/codex@latest --include=optional, then restart SynaBun.',
|
|
3408
|
-
);
|
|
3458
|
+
if (found.launchPath && /\.exe$/i.test(found.launchPath)) return found.launchPath;
|
|
3459
|
+
throw new Error(found.path
|
|
3460
|
+
? `Codex was found at ${found.path}, but not the codex.exe it runs. Reinstall Codex (the Codex panel's Install button, or npm install -g @openai/codex@latest --include=optional), then try again.`
|
|
3461
|
+
: 'Codex is not installed. Install it with the Codex panel\'s Install button, or run: powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"');
|
|
3409
3462
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3463
|
+
if (!found.path) {
|
|
3464
|
+
throw new Error('Codex schedules need Codex installed. Install it with the Codex panel\'s Install button, or run: curl -fsSL https://chatgpt.com/codex/install.sh | sh');
|
|
3465
|
+
}
|
|
3466
|
+
return found.launchPath || found.path;
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
// How `opencode serve --port N` starts, for the shared serve, the isolated
|
|
3470
|
+
// serves of sessions and loops and the editor's completion serve. On macOS and
|
|
3471
|
+
// Linux through the user's login shell, as a terminal starts it (OpenCode gets
|
|
3472
|
+
// the provider keys exported in .zshrc), naming the CLI the resolver found by
|
|
3473
|
+
// its full path; on Windows the opencode.exe found, started directly with the
|
|
3474
|
+
// PATH a new terminal has (a command file that leads nowhere: through cmd.exe,
|
|
3475
|
+
// every word escaped). A command line in Settings is run as written.
|
|
3476
|
+
function opencodeServeLaunch(port, { env = process.env } = {}) {
|
|
3477
|
+
const found = cliResolver.launcher('opencode');
|
|
3478
|
+
_noteCliLookup('opencode', found, 'OpenCode');
|
|
3479
|
+
const line = configuredCommandLine('opencode');
|
|
3480
|
+
if (IS_WIN) {
|
|
3481
|
+
const spawnEnv = withPathEnv(env, getAugmentedPath());
|
|
3482
|
+
if (!line && found.spec) {
|
|
3483
|
+
const launch = spawnArgsFor(found.spec, ['serve', '--port', String(port)], { env: spawnEnv });
|
|
3484
|
+
return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath} serve --port ${port}` };
|
|
3485
|
+
}
|
|
3486
|
+
const text = `${line || 'opencode'} serve --port ${port}`;
|
|
3487
|
+
return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text };
|
|
3416
3488
|
}
|
|
3417
|
-
|
|
3418
|
-
|
|
3489
|
+
const cmd = line || (found.path ? shellSingleQuote(found.path) : 'opencode');
|
|
3490
|
+
// A packaged application's runtime and npm after the user's own (the login shell adds the user's profile on top).
|
|
3491
|
+
return { command: DEFAULT_SHELL, args: ['-lc', `exec ${cmd} serve --port ${port}`], options: { env: withPackagedPathLast(env) }, label: `${cmd} serve --port ${port}` };
|
|
3419
3492
|
}
|
|
3420
3493
|
|
|
3421
3494
|
function extractCodexConfigValues(result) {
|
|
@@ -3455,10 +3528,10 @@ function buildCodexWritableRoots(cwd = PACKAGE_ROOT) {
|
|
|
3455
3528
|
return [...roots];
|
|
3456
3529
|
}
|
|
3457
3530
|
|
|
3458
|
-
function getCodexSidepanelApprovalPolicy() {
|
|
3531
|
+
function getCodexSidepanelApprovalPolicy(config = {}) {
|
|
3459
3532
|
// The sidepanel is interactive; Auto Accept decides which emitted requests
|
|
3460
3533
|
// can be approved without presenting a card.
|
|
3461
|
-
return CODEX_SIDEPANEL_APPROVAL_POLICY;
|
|
3534
|
+
return config.approval_policy ?? CODEX_SIDEPANEL_APPROVAL_POLICY;
|
|
3462
3535
|
}
|
|
3463
3536
|
|
|
3464
3537
|
function buildCodexSandboxPolicy(cwd = PACKAGE_ROOT, sandboxMode) {
|
|
@@ -3654,7 +3727,7 @@ async function resolveOpenCodeMcpDirectory(entry, sessionIds) {
|
|
|
3654
3727
|
// auth.json + sessions/ + state DB, while config.toml and prompts/ are
|
|
3655
3728
|
// symlinked back to the canonical ~/.codex so model/MCP/tool-permission config
|
|
3656
3729
|
// and custom prompts stay shared. The "default" account IS ~/.codex itself.
|
|
3657
|
-
const CODEX_DEFAULT_HOME = resolve(getHomePath(), '.codex');
|
|
3730
|
+
const CODEX_DEFAULT_HOME = resolve(process.env.CODEX_HOME || resolve(getHomePath(), '.codex'));
|
|
3658
3731
|
const CODEX_ACCOUNTS_ROOT = resolve(getHomePath(), '.codex-accounts');
|
|
3659
3732
|
|
|
3660
3733
|
function readCodexRuntimeModelCatalog(codexHome = CODEX_DEFAULT_HOME) {
|
|
@@ -3670,78 +3743,83 @@ function readCodexRuntimeModelCatalog(codexHome = CODEX_DEFAULT_HOME) {
|
|
|
3670
3743
|
}
|
|
3671
3744
|
const CODEX_ACCOUNTS_PATH = resolve(DATA_HOME, 'data', 'codex-accounts.json');
|
|
3672
3745
|
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3746
|
+
// Admin sandbox (lib/agent-sandbox-setting.js, lib/agent-sandbox-elevation.js): one global value,
|
|
3747
|
+
// off by default, for every agent SynaBun starts: may it use a sandbox whose setup needs Windows
|
|
3748
|
+
// administrator approval. Codex is the provider it changes (windows.sandbox, a launch override on
|
|
3749
|
+
// every Codex process, never written into config.toml). Changed from the Assistant panel only; idle
|
|
3750
|
+
// Codex app-servers (panel tabs, the Assistant's brain) restart under it, busy ones keep theirs.
|
|
3751
|
+
const _agentSandbox = createAdminSandboxSetting({
|
|
3752
|
+
path: resolve(DATA_HOME, 'data', 'agent-sandbox.json'),
|
|
3753
|
+
broadcast: (message) => broadcastSync(message),
|
|
3754
|
+
log: (tag, message) => console.log(`[${tag}]`, message),
|
|
3755
|
+
});
|
|
3756
|
+
function adminSandboxOn() { return _agentSandbox.enabled(); }
|
|
3757
|
+
/** The `-c` words every Codex SynaBun starts gets for the setting (outside an account's launch plan). */
|
|
3758
|
+
function codexSandboxArgs() { return codexWindowsSandboxArgs(adminSandboxOn()); }
|
|
3759
|
+
_agentSandbox.subscribe(() => {
|
|
3760
|
+
for (const connection of [..._codexLiveConnections]) {
|
|
3761
|
+
if (!connection.busy()) { try { connection.credentialsChanged(); } catch {} }
|
|
3762
|
+
}
|
|
3763
|
+
});
|
|
3764
|
+
|
|
3765
|
+
// The Codex accounts registry (lib/codex-accounts.js): one instance for the
|
|
3766
|
+
// sidepanel, the Assistant (brain, workers, catalog), loops and every route, so
|
|
3767
|
+
// one account state is shown everywhere. Its ChatGPT half is Codex's own
|
|
3768
|
+
// `account/read` for each account (codexAccountAuth().nativeStatus); a change
|
|
3769
|
+
// of any account reaches every window as `codex:accounts-changed`.
|
|
3770
|
+
let _assistantAccountUsage = null;
|
|
3771
|
+
let _codexAccountsInstance = null;
|
|
3772
|
+
function _codexAccounts() {
|
|
3773
|
+
if (!_codexAccountsInstance) {
|
|
3774
|
+
_codexAccountsInstance = createCodexAccounts({
|
|
3775
|
+
dataHome: DATA_HOME, homeDir: getHomePath(), accountsRoot: CODEX_ACCOUNTS_ROOT,
|
|
3776
|
+
readNativeStatus: (accountId) => codexAccountAuth().nativeStatus(accountId),
|
|
3777
|
+
prepareDefaultConfig: () => ensureCanonicalCodexConfig({ strict: true }),
|
|
3778
|
+
// Every launch plan carries the Windows sandbox the Admin sandbox setting allows.
|
|
3779
|
+
adminSandbox: () => adminSandboxOn(),
|
|
3780
|
+
log: (tag, message) => console.log(`[${tag}]`, message),
|
|
3781
|
+
});
|
|
3782
|
+
_codexAccountsInstance.subscribe((event) => {
|
|
3783
|
+
try {
|
|
3784
|
+
broadcastSync({ type: 'codex:accounts-changed', accountId: event.accountId || null, removed: !!event.removed, created: !!event.created });
|
|
3785
|
+
broadcastSync({ type: 'assistant:accounts-changed', provider: 'codex', accountId: event.accountId || null, removed: !!event.removed });
|
|
3786
|
+
_assistantCatalog?.invalidate?.();
|
|
3787
|
+
} catch {}
|
|
3693
3788
|
});
|
|
3694
|
-
data.defaultId = 'default';
|
|
3695
3789
|
}
|
|
3696
|
-
return
|
|
3790
|
+
return _codexAccountsInstance;
|
|
3697
3791
|
}
|
|
3698
3792
|
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
3703
|
-
writeFileSync(CODEX_ACCOUNTS_PATH, JSON.stringify(data, null, 2), 'utf-8');
|
|
3704
|
-
} catch (err) {
|
|
3705
|
-
console.warn('[codex-accounts] Failed to write registry:', err.message);
|
|
3706
|
-
}
|
|
3793
|
+
// The registry rows (id, label, home, …) for readers that only need where an account lives.
|
|
3794
|
+
function loadCodexAccounts() {
|
|
3795
|
+
return { accounts: _codexAccounts().registryRows(), defaultId: 'default' };
|
|
3707
3796
|
}
|
|
3708
3797
|
|
|
3798
|
+
// The account, or Default when none is named or it is gone (launch selectors, session catalogs).
|
|
3709
3799
|
function getCodexAccount(accountId) {
|
|
3710
|
-
const
|
|
3711
|
-
return
|
|
3712
|
-
|| data.accounts.find((a) => a.id === data.defaultId)
|
|
3713
|
-
|| data.accounts[0]
|
|
3714
|
-
|| null;
|
|
3800
|
+
const accounts = _codexAccounts();
|
|
3801
|
+
return (accountId ? accounts.profileFor(accountId) : null) || accounts.profileFor('default');
|
|
3715
3802
|
}
|
|
3716
3803
|
|
|
3804
|
+
// The account a request names, or null: never another account in its place.
|
|
3717
3805
|
function findCodexAccount(accountId) {
|
|
3718
3806
|
if (!accountId) return null;
|
|
3719
|
-
return
|
|
3807
|
+
return _codexAccounts().profileFor(accountId);
|
|
3720
3808
|
}
|
|
3721
3809
|
|
|
3722
3810
|
function getCodexAccountHome(accountId) {
|
|
3723
3811
|
return getCodexAccount(accountId)?.home || CODEX_DEFAULT_HOME;
|
|
3724
3812
|
}
|
|
3725
3813
|
|
|
3726
|
-
|
|
3727
|
-
return {
|
|
3728
|
-
id: a.id,
|
|
3729
|
-
label: a.label || a.email || 'Account',
|
|
3730
|
-
email: a.email || '',
|
|
3731
|
-
planType: a.planType || '',
|
|
3732
|
-
type: a.type || 'chatgpt',
|
|
3733
|
-
isDefault: a.id === 'default',
|
|
3734
|
-
};
|
|
3735
|
-
}
|
|
3736
|
-
|
|
3814
|
+
// Every account with its state (mode, signed in, identity: an email or a masked key, never a key).
|
|
3737
3815
|
function listCodexAccountsForClient() {
|
|
3738
|
-
return
|
|
3816
|
+
return _codexAccounts().listForClient();
|
|
3739
3817
|
}
|
|
3740
3818
|
|
|
3741
|
-
function ensureCanonicalCodexConfig({ strict = false } = {}) {
|
|
3819
|
+
function ensureCanonicalCodexConfig({ strict = false, codexHome = CODEX_DEFAULT_HOME } = {}) {
|
|
3742
3820
|
try {
|
|
3743
|
-
if (!existsSync(
|
|
3744
|
-
const cfg = resolve(
|
|
3821
|
+
if (!existsSync(codexHome)) mkdirSync(codexHome, { recursive: true });
|
|
3822
|
+
const cfg = resolve(codexHome, 'config.toml');
|
|
3745
3823
|
if (!existsSync(cfg)) writeFileSync(cfg, '', 'utf-8');
|
|
3746
3824
|
const repair = healCodexConfig(cfg);
|
|
3747
3825
|
if (repair.healed) {
|
|
@@ -3751,50 +3829,85 @@ function ensureCanonicalCodexConfig({ strict = false } = {}) {
|
|
|
3751
3829
|
if (strict) throw error;
|
|
3752
3830
|
console.warn(`[codex-config] ${error.message}`);
|
|
3753
3831
|
}
|
|
3832
|
+
// Env/tool CLI overrides require a transport, even after a fresh CLI install
|
|
3833
|
+
// or when setup never connected Codex. Reuse Connect Codex's canonical writer;
|
|
3834
|
+
// leave an already connected transport and all unrelated settings alone.
|
|
3835
|
+
const health = inspectCodexConfig(cfg);
|
|
3836
|
+
if (!health.connected || health.issues.includes('missing_transport')) {
|
|
3837
|
+
const inspection = upsertCodexMcpConfig(cfg, buildCodexMcpDefinition());
|
|
3838
|
+
if (!inspection.valid) throw new Error(`Could not register SynaBun in ${cfg}`);
|
|
3839
|
+
return { healed: true, reason: 'registered_missing_transport', inspection };
|
|
3840
|
+
}
|
|
3841
|
+
if (!health.valid) throw new Error(`Invalid SynaBun MCP configuration in ${cfg}: ${health.issues.join(', ')}`);
|
|
3754
3842
|
return repair;
|
|
3755
3843
|
} catch (err) {
|
|
3756
|
-
console.warn(`[codex-config] Could not prepare ${
|
|
3844
|
+
console.warn(`[codex-config] Could not prepare ${codexHome}/config.toml: ${err.message}`);
|
|
3757
3845
|
if (strict) throw err;
|
|
3758
3846
|
return null;
|
|
3759
3847
|
}
|
|
3760
3848
|
}
|
|
3761
3849
|
|
|
3762
|
-
|
|
3763
|
-
|
|
3850
|
+
// Live Codex connections (panel tabs, the Assistant's brain): their account,
|
|
3851
|
+
// whether a turn is running, and how they restart under a changed credential.
|
|
3852
|
+
const _codexLiveConnections = new Set();
|
|
3853
|
+
|
|
3854
|
+
// A Codex account's credential or mode changes: refused while one of its
|
|
3855
|
+
// connections is mid-turn; afterwards its idle app-servers restart, so the next
|
|
3856
|
+
// message runs under the new credential. Other accounts are not touched.
|
|
3857
|
+
async function withCodexAccountCredentialChange(accountId, change) {
|
|
3858
|
+
const connections = [..._codexLiveConnections].filter((connection) => connection.accountId() === accountId);
|
|
3859
|
+
if (connections.some((connection) => connection.busy())) {
|
|
3860
|
+
throw Object.assign(new Error('Finish or stop this account’s Codex work before changing its sign-in.'), { status: 409 });
|
|
3861
|
+
}
|
|
3862
|
+
const result = await change();
|
|
3863
|
+
for (const connection of connections) { try { connection.credentialsChanged(); } catch {} }
|
|
3864
|
+
return result;
|
|
3764
3865
|
}
|
|
3765
3866
|
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
if (
|
|
3770
|
-
if (!existsSync(targetPath)) {
|
|
3771
|
-
if (kind === 'dir') mkdirSync(targetPath, { recursive: true });
|
|
3772
|
-
else writeFileSync(targetPath, '', 'utf-8');
|
|
3773
|
-
}
|
|
3774
|
-
symlinkSync(targetPath, linkPath, kind === 'dir' ? 'dir' : 'file');
|
|
3775
|
-
} catch (err) {
|
|
3776
|
-
console.warn('[codex-accounts] Failed to link', linkPath, '→', targetPath, err.message);
|
|
3867
|
+
// Codex's own store changed outside a request (a terminal sign-in ended): idle connections restart.
|
|
3868
|
+
function codexAccountStoreChanged(accountId) {
|
|
3869
|
+
for (const connection of [..._codexLiveConnections]) {
|
|
3870
|
+
if (connection.accountId() === accountId && !connection.busy()) { try { connection.credentialsChanged(); } catch {} }
|
|
3777
3871
|
}
|
|
3778
3872
|
}
|
|
3779
3873
|
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
// Only when the file exists: the rules installer links it later otherwise,
|
|
3790
|
-
// and an empty ~/.codex/AGENTS.md is not ours to create.
|
|
3791
|
-
if (existsSync(resolve(CODEX_DEFAULT_HOME, 'AGENTS.md'))) {
|
|
3792
|
-
linkSharedCodexResource(resolve(home, 'AGENTS.md'), resolve(CODEX_DEFAULT_HOME, 'AGENTS.md'), 'file');
|
|
3874
|
+
let _codexAccountAuthInstance = null;
|
|
3875
|
+
function codexAccountAuth() {
|
|
3876
|
+
if (!_codexAccountAuthInstance) {
|
|
3877
|
+
_codexAccountAuthInstance = createCodexAccountAuth({
|
|
3878
|
+
accounts: _codexAccounts(), codexBin: () => getCodexBin(),
|
|
3879
|
+
prepareConfig: (codexHome) => ensureCanonicalCodexConfig({ strict: true, codexHome }),
|
|
3880
|
+
env: () => (withPathEnv(process.env, getAugmentedPath())),
|
|
3881
|
+
changeCredentials: withCodexAccountCredentialChange,
|
|
3882
|
+
});
|
|
3793
3883
|
}
|
|
3794
|
-
return
|
|
3884
|
+
return _codexAccountAuthInstance;
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
// Why a Codex account cannot be removed now ('' when it can): a live connection
|
|
3888
|
+
// (a panel tab, the Assistant's brain) other than `except`, a native loop, a
|
|
3889
|
+
// dispatched run, an Assistant session, or the Assistant's worker choice.
|
|
3890
|
+
function codexAccountInUse(accountId, { except = null } = {}) {
|
|
3891
|
+
if ([..._codexLiveConnections].some((connection) => connection !== except && connection.accountId() === accountId)) {
|
|
3892
|
+
return 'A Codex tab or the Assistant is running under this account. Switch it or close it first.';
|
|
3893
|
+
}
|
|
3894
|
+
try { if (_nativeLoopRuntime?.list({ activeOnly: true }).some((run) => run.provider === 'codex' && run.accountId === accountId)) return 'Stop native Codex automations using this account before removing it.'; } catch {}
|
|
3895
|
+
try { if (_assistantDispatcher?.accountInUse?.('codex', accountId)) return 'Dispatched runs are using this account. Stop them first.'; } catch {}
|
|
3896
|
+
try { if (_assistantRuntime?.accountInUse?.('codex', accountId)) return 'An Assistant session uses this account. Close it first.'; } catch {}
|
|
3897
|
+
try { if (_assistantCodexWorkerAccount?.() === accountId) return 'This is the Assistant\'s Codex worker account. Choose another one first.'; } catch {}
|
|
3898
|
+
return '';
|
|
3795
3899
|
}
|
|
3796
3900
|
|
|
3797
3901
|
function handleCodexSkinWebSocket(ws) {
|
|
3902
|
+
// In API-key mode with a key SynaBun holds, the app-server starts with an ephemeral
|
|
3903
|
+
// credential store and this key is its first request over stdin (lib/codex-accounts.js).
|
|
3904
|
+
// Spawn-only: never logged, sent or kept after that request.
|
|
3905
|
+
let childApiKeyLogin = null;
|
|
3906
|
+
let childUsesHeldKey = false;
|
|
3907
|
+
// Which credential the running app-server started with (launchFor().credential).
|
|
3908
|
+
let childCredential = null;
|
|
3909
|
+
// Which Windows sandbox it started with (launchFor().windowsSandbox, the Admin sandbox setting).
|
|
3910
|
+
let childWindowsSandbox = null;
|
|
3798
3911
|
const CODEX_GREETING_MARKER_START = '[SYNABUN_CODEX_GREETING_START]';
|
|
3799
3912
|
const CODEX_GREETING_MARKER_END = '[SYNABUN_CODEX_GREETING_END]';
|
|
3800
3913
|
const CODEX_PLAN_MODE_PREFIXES = [
|
|
@@ -3843,7 +3956,27 @@ CRITICAL — When you need clarification during planning:
|
|
|
3843
3956
|
// Per-tab Codex account → drives CODEX_HOME of the spawned app-server.
|
|
3844
3957
|
let activeAccountId = 'default';
|
|
3845
3958
|
let activeCodexHome = CODEX_DEFAULT_HOME;
|
|
3846
|
-
|
|
3959
|
+
const panelAuth = createCodexPanelAuth({
|
|
3960
|
+
request: (...args) => request(...args), accountId: () => activeAccountId,
|
|
3961
|
+
onChange: state => sendToClient({ type: 'account_login_state', state }),
|
|
3962
|
+
onSuccess: async id => {
|
|
3963
|
+
const acct = await request('account/read', { refreshToken: false }, 10000);
|
|
3964
|
+
if (id !== activeAccountId) return;
|
|
3965
|
+
// What Codex now holds is the account's state everywhere (lib/codex-accounts.js).
|
|
3966
|
+
if (acct?.account?.type === 'chatgpt') _codexAccounts().noteSubscriptionSignIn(id, acct);
|
|
3967
|
+
else _codexAccounts().noteNativeStatus(id, acct || {});
|
|
3968
|
+
sendToClient({ type: 'account_info', accountId: id, account: acct, accountState: _codexAccounts().describeForClient(id) });
|
|
3969
|
+
sendToClient({ type: 'account_list', accounts: listCodexAccountsForClient(), activeAccountId });
|
|
3970
|
+
},
|
|
3971
|
+
});
|
|
3972
|
+
// This connection in the live set: account changes ask it whether it is busy and restart it when idle.
|
|
3973
|
+
const liveConnection = {
|
|
3974
|
+
accountId: () => activeAccountId,
|
|
3975
|
+
readRateLimits: () => initialized && !childUsesHeldKey ? request('account/rateLimits/read', {}, 2500) : null,
|
|
3976
|
+
busy: () => !!activeTurnId || pendingServerRequests.size > 0 || panelAuth.active(),
|
|
3977
|
+
credentialsChanged: () => retireForCredentialChange(),
|
|
3978
|
+
};
|
|
3979
|
+
_codexLiveConnections.add(liveConnection);
|
|
3847
3980
|
let codexMcpRuntimeId = `codex-sp-${randomUUID()}`;
|
|
3848
3981
|
let codexMcpRuntimeBinding = null;
|
|
3849
3982
|
// 'assistant' when this connection is the main assistant's brain: the MCP
|
|
@@ -3852,6 +3985,12 @@ CRITICAL — When you need clarification during planning:
|
|
|
3852
3985
|
// The assistant brain's computer-use grant (lib/desktop/grants.js): unlocks
|
|
3853
3986
|
// the capability-gated `computer` group in this app-server's MCP child.
|
|
3854
3987
|
let codexDesktopGrant = null;
|
|
3988
|
+
// A WhatsApp conversation brain's credential (whatsapp_send_file): taken from the in-process
|
|
3989
|
+
// brain's own socket object (lib/virtual-socket.js brainCredentialOf), never from a message a
|
|
3990
|
+
// client sends; it reaches the MCP child through the environment only (SYNABUN_BRAIN_TOKEN,
|
|
3991
|
+
// forwarded by name with env_vars), never argv. A name with TOKEN in it stays out of the
|
|
3992
|
+
// brain's own shell commands under Codex's default shell_environment_policy.
|
|
3993
|
+
let codexBrainCredential = null;
|
|
3855
3994
|
// The assistant brain's route gate (lib/assistant-route-gate-codex.js): a
|
|
3856
3995
|
// trusted session-flag PreToolUse hook plus the env it reads. Assistant only.
|
|
3857
3996
|
let codexRouteGate = null;
|
|
@@ -3880,8 +4019,10 @@ CRITICAL — When you need clarification during planning:
|
|
|
3880
4019
|
|
|
3881
4020
|
async function beforeCodexCompatRequest(type, params) {
|
|
3882
4021
|
if (type === 'thread_settings_update') {
|
|
3883
|
-
validateCodexConfigRequirements({ approval_policy: params.approvalPolicy, approvals_reviewer: params.approvalsReviewer }, await readCodexRequirements());
|
|
4022
|
+
validateCodexConfigRequirements({ approval_policy: params.approvalPolicy, approvals_reviewer: params.approvalsReviewer, sandbox_mode: ({ readOnly: 'read-only', workspaceWrite: 'workspace-write', dangerFullAccess: 'danger-full-access' })[params.sandboxPolicy?.type] }, await readCodexRequirements());
|
|
3884
4023
|
} else if (type === 'experimental_features_set') {
|
|
4024
|
+
const metadata = await readSettingsMetadata();
|
|
4025
|
+
validateCodexSettings({ features: params.enablement }, metadata, metadata.features);
|
|
3885
4026
|
validateCodexConfigRequirements({ features: params.enablement }, await readCodexRequirements());
|
|
3886
4027
|
} else if (type === 'feedback_upload') {
|
|
3887
4028
|
await readCodexRequirements();
|
|
@@ -3997,9 +4138,48 @@ CRITICAL — When you need clarification during planning:
|
|
|
3997
4138
|
...details,
|
|
3998
4139
|
}));
|
|
3999
4140
|
}
|
|
4141
|
+
// A tab names its account; one that is no longer set up runs as Default, and says so
|
|
4142
|
+
// (every answer carries activeAccountId), never under Default's home with another name.
|
|
4000
4143
|
function setActiveCodexAccount(id) {
|
|
4001
|
-
|
|
4002
|
-
|
|
4144
|
+
const account = getCodexAccount(id || 'default');
|
|
4145
|
+
activeAccountId = account?.id || 'default';
|
|
4146
|
+
activeCodexHome = account?.home || CODEX_DEFAULT_HOME;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
function sendAccountState() {
|
|
4150
|
+
sendToClient({ type: 'account_state', accountId: activeAccountId, account: _codexAccounts().describeForClient(activeAccountId), accounts: listCodexAccountsForClient() });
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
// The account's credential or mode changed (withCodexAccountCredentialChange), or the Admin
|
|
4154
|
+
// sandbox setting did: an idle app-server is retired, and the next request starts one under
|
|
4155
|
+
// the new launch plan; the conversation resumes from its thread then. The transcript stays.
|
|
4156
|
+
function retireForCredentialChange() {
|
|
4157
|
+
const next = _codexAccounts().launchFor(activeAccountId);
|
|
4158
|
+
if (child && !activeTurnId && !pendingServerRequests.size && (!next.ok || next.credential !== childCredential || next.windowsSandbox !== childWindowsSandbox)) {
|
|
4159
|
+
const old = child;
|
|
4160
|
+
const oldHome = activeCodexHome;
|
|
4161
|
+
const oldThreadIds = ownedWriterThreadIds();
|
|
4162
|
+
panelAuth.reset();
|
|
4163
|
+
initialized = false;
|
|
4164
|
+
bootPromise = null;
|
|
4165
|
+
activeThreadId = null;
|
|
4166
|
+
appBoundary.invalidate();
|
|
4167
|
+
activeThreadRuntimeFingerprint = null;
|
|
4168
|
+
subscribedThreadIds.clear();
|
|
4169
|
+
threadOwnerRoots.clear();
|
|
4170
|
+
activeTurnAutoAccept = false;
|
|
4171
|
+
for (const [, entry] of pending) {
|
|
4172
|
+
try { clearTimeout(entry.timer); entry.reject(new Error('Codex account sign-in changed')); } catch {}
|
|
4173
|
+
}
|
|
4174
|
+
pending.clear();
|
|
4175
|
+
childStdoutBuf = '';
|
|
4176
|
+
unregisterCodexMcpProfileRuntime();
|
|
4177
|
+
child = null;
|
|
4178
|
+
childCredential = null;
|
|
4179
|
+
childWindowsSandbox = null;
|
|
4180
|
+
retireCodexProcess(old, { codexHome: oldHome, threadIds: oldThreadIds, reason: 'Codex account sign-in changed' });
|
|
4181
|
+
}
|
|
4182
|
+
sendAccountState();
|
|
4003
4183
|
}
|
|
4004
4184
|
|
|
4005
4185
|
async function withCodexWriterOperation(operation, action) {
|
|
@@ -4022,6 +4202,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
4022
4202
|
})();
|
|
4023
4203
|
|
|
4024
4204
|
function sendToClient(data) {
|
|
4205
|
+
data = redactProviderSecrets(data);
|
|
4025
4206
|
if (data?.type === 'error') {
|
|
4026
4207
|
// Some legacy catch paths forward only a prefixed RPC message.
|
|
4027
4208
|
const rpcError = { ...data, code: data.code ?? (/Invalid request: unknown variant/i.test(data.message || '') ? -32600 : null) };
|
|
@@ -4146,6 +4327,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
4146
4327
|
const codexHome = activeCodexHome;
|
|
4147
4328
|
const threadIds = ownedWriterThreadIds();
|
|
4148
4329
|
shuttingDown = true;
|
|
4330
|
+
panelAuth.interrupted();
|
|
4149
4331
|
unregisterCodexMcpProfileRuntime({ cleanup: true });
|
|
4150
4332
|
childRetirementPromise = (async () => {
|
|
4151
4333
|
if (unsubscribe) await unsubscribeAllThreadsBeforeRetirement();
|
|
@@ -4364,6 +4546,19 @@ CRITICAL — When you need clarification during planning:
|
|
|
4364
4546
|
return result || {};
|
|
4365
4547
|
}
|
|
4366
4548
|
|
|
4549
|
+
async function readSettingsMetadata() {
|
|
4550
|
+
const schema = readCodexSettingsSchema(getCodexBin());
|
|
4551
|
+
const features = await listCodexPages(request, 'experimentalFeature/list', { limit: 100 });
|
|
4552
|
+
const featureSupport = readCodexFeatureSupport(getCodexBin(), features.data);
|
|
4553
|
+
// The catalog's enabled field is a process-start snapshot. config/read is
|
|
4554
|
+
// authoritative for saved defaults after reloadUserConfig writes.
|
|
4555
|
+
return { ...schema, features: features.data.map(feature => ({ ...feature,
|
|
4556
|
+
configurable: featureSupport[feature.name] === true,
|
|
4557
|
+
enabled: typeof _cachedConfig.features?.[feature.name] === 'boolean'
|
|
4558
|
+
? _cachedConfig.features[feature.name] : feature.enabled,
|
|
4559
|
+
})) };
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4367
4562
|
async function setCodexMcpServerApprovalMode(serverName, approvalMode) {
|
|
4368
4563
|
const name = String(serverName || '').trim();
|
|
4369
4564
|
if (!name) throw new Error('MCP approval request is missing its server identity');
|
|
@@ -4408,7 +4603,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
4408
4603
|
rejected: isolationStats.rejected,
|
|
4409
4604
|
reasons: { ...isolationStats.reasons },
|
|
4410
4605
|
},
|
|
4411
|
-
approvalPolicy: getCodexSidepanelApprovalPolicy(),
|
|
4606
|
+
approvalPolicy: getCodexSidepanelApprovalPolicy(_cachedConfig),
|
|
4412
4607
|
sessionOverrides: codexSessionSettings.get(activeAccountId, activeThreadId),
|
|
4413
4608
|
cwd,
|
|
4414
4609
|
},
|
|
@@ -4498,7 +4693,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
4498
4693
|
function getDefaultThreadParams(cwd = PACKAGE_ROOT, runtime = {}) {
|
|
4499
4694
|
const params = {
|
|
4500
4695
|
cwd,
|
|
4501
|
-
approvalPolicy: getCodexSidepanelApprovalPolicy(),
|
|
4696
|
+
approvalPolicy: getCodexSidepanelApprovalPolicy(_cachedConfig),
|
|
4502
4697
|
sandbox: getCachedConfigValue('sandbox_mode', 'workspace-write'),
|
|
4503
4698
|
personality: getCachedConfigValue('personality', 'pragmatic'),
|
|
4504
4699
|
};
|
|
@@ -4513,16 +4708,19 @@ CRITICAL — When you need clarification during planning:
|
|
|
4513
4708
|
threadId,
|
|
4514
4709
|
cwd,
|
|
4515
4710
|
excludeTurns: true,
|
|
4516
|
-
approvalPolicy: getCodexSidepanelApprovalPolicy(),
|
|
4711
|
+
approvalPolicy: getCodexSidepanelApprovalPolicy(_cachedConfig),
|
|
4517
4712
|
sandbox: getCachedConfigValue('sandbox_mode', 'workspace-write'),
|
|
4518
4713
|
personality: getCachedConfigValue('personality', 'pragmatic'),
|
|
4519
4714
|
};
|
|
4520
4715
|
const sessionSettings = codexSessionSettings.get(activeAccountId, threadId);
|
|
4716
|
+
const sessionSandboxMode = ({ readOnly: 'read-only', workspaceWrite: 'workspace-write', dangerFullAccess: 'danger-full-access' })[sessionSettings.sandboxPolicy?.type];
|
|
4717
|
+
if (sessionSandboxMode) params.sandbox = sessionSandboxMode;
|
|
4521
4718
|
for (const key of ['approvalPolicy', 'approvalsReviewer', 'personality', 'serviceTier']) {
|
|
4522
4719
|
if (Object.prototype.hasOwnProperty.call(sessionSettings, key)) params[key] = sessionSettings[key];
|
|
4523
4720
|
}
|
|
4524
4721
|
if (runtime.model) params.model = runtime.model;
|
|
4525
4722
|
else if (getCachedConfigValue('model')) params.model = getCachedConfigValue('model');
|
|
4723
|
+
if (sessionSettings.model != null) params.model = sessionSettings.model;
|
|
4526
4724
|
if (runtime.config) params.config = runtime.config;
|
|
4527
4725
|
return params;
|
|
4528
4726
|
}
|
|
@@ -5011,6 +5209,12 @@ CRITICAL — When you need clarification during planning:
|
|
|
5011
5209
|
|
|
5012
5210
|
function startChild() {
|
|
5013
5211
|
if (child) return child;
|
|
5212
|
+
// The account decides the credential (lib/codex-accounts.js launchFor()): inherited
|
|
5213
|
+
// OpenAI/Codex credentials out, its CODEX_HOME in, and in API-key mode the key it
|
|
5214
|
+
// holds, given over stdin after initialize to an app-server with an ephemeral store.
|
|
5215
|
+
const launch = _codexAccounts().launchFor(activeAccountId);
|
|
5216
|
+
if (!launch.ok) throw new Error(launch.message);
|
|
5217
|
+
activeCodexHome = launch.home;
|
|
5014
5218
|
let codexBin = getCodexBin();
|
|
5015
5219
|
const runtimeProfilePath = writeCodexRuntimeProfile(codexMcpRuntimeId, codexMcpProfileState.value);
|
|
5016
5220
|
const args = [
|
|
@@ -5031,24 +5235,31 @@ CRITICAL — When you need clarification during planning:
|
|
|
5031
5235
|
// (lib/browser-tool-policy.js), so Codex's own web search stays off, as for its workers.
|
|
5032
5236
|
if (codexRole === 'assistant') args.splice(args.length - 1, 0, '-c', 'web_search="disabled"');
|
|
5033
5237
|
if (codexDesktopGrant) args.splice(args.length - 1, 0, '-c', `mcp_servers.SynaBun.env.SYNABUN_DESKTOP_GRANT=${JSON.stringify(codexDesktopGrant)}`);
|
|
5238
|
+
// The variable's name on the command line, its value only in the app-server's environment (below).
|
|
5239
|
+
if (codexRole === 'assistant' && codexBrainCredential) args.splice(args.length - 1, 0, '-c', 'mcp_servers.SynaBun.env_vars=["SYNABUN_BRAIN_TOKEN"]');
|
|
5034
5240
|
if (codexRole === 'assistant' && codexRouteGate?.flags?.length) args.splice(args.length - 1, 0, ...codexRouteGate.flags);
|
|
5035
5241
|
if (/\.js$/i.test(codexBin)) {
|
|
5036
5242
|
args.unshift(codexBin);
|
|
5037
5243
|
codexBin = process.execPath;
|
|
5038
5244
|
}
|
|
5245
|
+
// The Windows sandbox the Admin sandbox setting allows and, in API-key mode with a key SynaBun
|
|
5246
|
+
// holds, an ephemeral credential store: before the subcommand.
|
|
5247
|
+
args.splice(args.length - 1, 0, ...codexAppServerArgs(launch));
|
|
5039
5248
|
const useShell = !codexBin.includes(sep)
|
|
5040
5249
|
|| (process.platform === 'win32' && /\.(cmd|bat)$/i.test(codexBin));
|
|
5041
5250
|
const env = {
|
|
5042
|
-
...process.env,
|
|
5251
|
+
...applyCodexLaunchEnv(process.env, launch),
|
|
5043
5252
|
NO_COLOR: '1',
|
|
5044
|
-
CODEX_HOME: activeCodexHome,
|
|
5045
5253
|
SYNABUN_PROFILE: codexMcpProfileState.value,
|
|
5046
5254
|
SYNABUN_TERMINAL_SESSION: codexMcpRuntimeId,
|
|
5047
5255
|
SYNABUN_RUNTIME_PROFILE_PATH: runtimeProfilePath,
|
|
5048
5256
|
SYNABUN_TOOL_CATALOG_MODE: 'deferred',
|
|
5049
5257
|
...(codexDesktopGrant ? { SYNABUN_DESKTOP_GRANT: codexDesktopGrant } : {}),
|
|
5050
5258
|
...(codexRole === 'assistant' && codexRouteGate?.env ? codexRouteGate.env : {}),
|
|
5259
|
+
...(codexRole === 'assistant' && codexBrainCredential ? { SYNABUN_BRAIN_TOKEN: codexBrainCredential } : {}),
|
|
5051
5260
|
};
|
|
5261
|
+
// Never one inherited from the launching shell: only this brain's own credential reaches its MCP child.
|
|
5262
|
+
if (!(codexRole === 'assistant' && codexBrainCredential)) delete env.SYNABUN_BRAIN_TOKEN;
|
|
5052
5263
|
delete env.FORCE_COLOR;
|
|
5053
5264
|
delete env.CLICOLOR_FORCE;
|
|
5054
5265
|
const proc = spawn(shellCommand(codexBin, useShell, { isFile: existsSync }), args, {
|
|
@@ -5058,10 +5269,14 @@ CRITICAL — When you need clarification during planning:
|
|
|
5058
5269
|
shell: useShell,
|
|
5059
5270
|
});
|
|
5060
5271
|
child = proc;
|
|
5272
|
+
childApiKeyLogin = launch.apiKey || null;
|
|
5273
|
+
childUsesHeldKey = !!launch.apiKey;
|
|
5274
|
+
childCredential = launch.credential;
|
|
5275
|
+
childWindowsSandbox = launch.windowsSandbox;
|
|
5061
5276
|
childRetirementPromise = null;
|
|
5062
5277
|
writerLifecycleState.releaseRequested = false;
|
|
5063
5278
|
registerCodexMcpProfileRuntime();
|
|
5064
|
-
console.log(`[codex-skin] Spawned app-server pid ${proc.pid || '?'} account=${activeAccountId} profile=${codexMcpProfileState.value} runtime=${codexMcpRuntimeId} CODEX_HOME=${activeCodexHome}`);
|
|
5279
|
+
console.log(`[codex-skin] Spawned app-server pid ${proc.pid || '?'} account=${activeAccountId} mode=${launch.mode || 'none'} windowsSandbox=${launch.windowsSandbox} profile=${codexMcpProfileState.value} runtime=${codexMcpRuntimeId} CODEX_HOME=${activeCodexHome}`);
|
|
5065
5280
|
proc.stdout.setEncoding('utf-8');
|
|
5066
5281
|
proc.stdout.on('data', (chunk) => {
|
|
5067
5282
|
if (child !== proc) return; // stale process from a previous account
|
|
@@ -5073,9 +5288,9 @@ CRITICAL — When you need clarification during planning:
|
|
|
5073
5288
|
if (!line) continue;
|
|
5074
5289
|
let msg;
|
|
5075
5290
|
try {
|
|
5076
|
-
msg = JSON.parse(line);
|
|
5291
|
+
msg = redactProviderSecrets(JSON.parse(line));
|
|
5077
5292
|
} catch (err) {
|
|
5078
|
-
console.warn('[codex-skin] Invalid JSON from app-server
|
|
5293
|
+
console.warn('[codex-skin] Invalid JSON from app-server');
|
|
5079
5294
|
continue;
|
|
5080
5295
|
}
|
|
5081
5296
|
|
|
@@ -5188,6 +5403,9 @@ CRITICAL — When you need clarification during planning:
|
|
|
5188
5403
|
if (msg.method) {
|
|
5189
5404
|
const method = msg.method;
|
|
5190
5405
|
const params = msg.params || {};
|
|
5406
|
+
if (method === 'account/rateLimits/updated') _assistantAccountUsage?.note('codex', activeAccountId, params);
|
|
5407
|
+
if (method === 'account/login/completed') void panelAuth.complete(params);
|
|
5408
|
+
if (method === 'account/updated' && params.authMode === null && !panelAuth.active()) panelAuth.reset();
|
|
5191
5409
|
const scopedThreadId = notificationThreadId(method, params);
|
|
5192
5410
|
const scopedTurnId = notificationTurnId(method, params);
|
|
5193
5411
|
const expectedThreadId = displayedThreadId;
|
|
@@ -5262,11 +5480,11 @@ CRITICAL — When you need clarification during planning:
|
|
|
5262
5480
|
}
|
|
5263
5481
|
});
|
|
5264
5482
|
proc.stderr.setEncoding('utf-8');
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
if (!text)
|
|
5268
|
-
console.log('[codex-skin][stderr]', text);
|
|
5483
|
+
const recordNativeDiagnostic = providerDiagnosticSink(text => {
|
|
5484
|
+
// Key-login attempts expose only structured, filtered errors to the UI.
|
|
5485
|
+
if (!childUsesHeldKey && !panelAuth.active()) console.log('[codex-skin][stderr]', text);
|
|
5269
5486
|
});
|
|
5487
|
+
proc.stderr.on('data', recordNativeDiagnostic);
|
|
5270
5488
|
proc.on('error', (err) => {
|
|
5271
5489
|
if (child !== proc) return;
|
|
5272
5490
|
console.error('[codex-skin] app-server spawn error:', err.message);
|
|
@@ -5279,6 +5497,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5279
5497
|
});
|
|
5280
5498
|
proc.on('close', (code) => {
|
|
5281
5499
|
if (child !== proc) return; // a newer process replaced this one (e.g. account switch)
|
|
5500
|
+
panelAuth.interrupted();
|
|
5282
5501
|
unregisterCodexMcpProfileRuntime();
|
|
5283
5502
|
const wasShuttingDown = shuttingDown;
|
|
5284
5503
|
child = null;
|
|
@@ -5310,7 +5529,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5310
5529
|
// Repair SynaBun's legacy duplicate-env config before Codex parses it.
|
|
5311
5530
|
// Doing this at the sidepanel boundary avoids the circular failure where
|
|
5312
5531
|
// Codex rejects config.toml before it can launch SynaBun's MCP process.
|
|
5313
|
-
const repair = ensureCanonicalCodexConfig();
|
|
5532
|
+
const repair = ensureCanonicalCodexConfig({ strict: true, codexHome: activeCodexHome });
|
|
5314
5533
|
if (repair?.healed) {
|
|
5315
5534
|
sendToClient({
|
|
5316
5535
|
type: 'codex_config_repaired',
|
|
@@ -5319,7 +5538,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5319
5538
|
});
|
|
5320
5539
|
}
|
|
5321
5540
|
// Sync tool approvals only after the base transport is known to be valid.
|
|
5322
|
-
syncCodexToolPermissions([..._codexPermittedTools]);
|
|
5541
|
+
syncCodexToolPermissions([..._codexPermittedTools], activeCodexHome);
|
|
5323
5542
|
const codexBin = getCodexBin();
|
|
5324
5543
|
let cliVersion = null;
|
|
5325
5544
|
let sdkVersion = null;
|
|
@@ -5327,7 +5546,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5327
5546
|
const versionResult = spawnSync(codexBin, ['--version'], { encoding: 'utf-8', timeout: 5000, windowsHide: true });
|
|
5328
5547
|
cliVersion = parseCodexVersion(versionResult.stdout);
|
|
5329
5548
|
} catch {}
|
|
5330
|
-
|
|
5549
|
+
sdkVersion = findInstalled(__dirname, '@openai/codex-sdk', { stopAt: PACKAGE_ROOT })?.version || null;
|
|
5331
5550
|
codexCapabilities.reset({ codexBin, codexBinSource: _codexBinSource, cliVersion, sdkVersion });
|
|
5332
5551
|
startChild();
|
|
5333
5552
|
bootPromise = request('initialize', {
|
|
@@ -5339,9 +5558,27 @@ CRITICAL — When you need clarification during planning:
|
|
|
5339
5558
|
capabilities: {
|
|
5340
5559
|
experimentalApi: true,
|
|
5341
5560
|
},
|
|
5342
|
-
}, 15000).then((result) => {
|
|
5343
|
-
initialized = true;
|
|
5561
|
+
}, 15000).then(async (result) => {
|
|
5344
5562
|
writeRpc({ jsonrpc: '2.0', method: 'initialized' });
|
|
5563
|
+
// API-key mode: the key SynaBun holds signs this app-server in before anything else
|
|
5564
|
+
// runs; its ephemeral store keeps nothing and leaves the account's ChatGPT login alone.
|
|
5565
|
+
if (childApiKeyLogin) {
|
|
5566
|
+
let apiKey = childApiKeyLogin;
|
|
5567
|
+
childApiKeyLogin = null;
|
|
5568
|
+
try {
|
|
5569
|
+
const login = await request('account/login/start', { type: 'apiKey', apiKey }, 15000);
|
|
5570
|
+
if (login?.type !== 'apiKey') throw new Error('unexpected login answer');
|
|
5571
|
+
} catch {
|
|
5572
|
+
// A half-started app-server is not reused: the next request starts a fresh one.
|
|
5573
|
+
const failed = child;
|
|
5574
|
+
child = null;
|
|
5575
|
+
childCredential = null;
|
|
5576
|
+
unregisterCodexMcpProfileRuntime();
|
|
5577
|
+
try { failed?.kill('SIGTERM'); } catch {}
|
|
5578
|
+
throw new Error('Codex could not start on this account\'s API key. Save the key again, or switch the account to its ChatGPT sign-in.');
|
|
5579
|
+
} finally { apiKey = null; }
|
|
5580
|
+
}
|
|
5581
|
+
initialized = true;
|
|
5345
5582
|
codexCapabilities.runtime.serverInfo = result || {};
|
|
5346
5583
|
codexCapabilities.runtime.cliVersion ||= parseCodexVersion(result?.userAgent);
|
|
5347
5584
|
sendToClient({ type: 'capabilities', ...codexCapabilitySnapshot() });
|
|
@@ -5412,9 +5649,11 @@ CRITICAL — When you need clarification during planning:
|
|
|
5412
5649
|
// The close handler's `child !== proc` guard prevents the dying process from
|
|
5413
5650
|
// clobbering the freshly-spawned one. Callers must ensure no turn is active.
|
|
5414
5651
|
async function switchCodexAccount(id) {
|
|
5652
|
+
if (panelAuth.active()) throw new Error('Cancel or finish sign-in before switching accounts');
|
|
5415
5653
|
const oldHome = activeCodexHome;
|
|
5416
5654
|
const oldThreadIds = ownedWriterThreadIds();
|
|
5417
5655
|
setActiveCodexAccount(id);
|
|
5656
|
+
panelAuth.reset();
|
|
5418
5657
|
for (const key of Object.keys(_cachedConfig)) delete _cachedConfig[key];
|
|
5419
5658
|
const old = child;
|
|
5420
5659
|
initialized = false;
|
|
@@ -5498,45 +5737,6 @@ CRITICAL — When you need clarification during planning:
|
|
|
5498
5737
|
return { profile: normalized, restarted: true, threadId: resumeId };
|
|
5499
5738
|
}
|
|
5500
5739
|
|
|
5501
|
-
// After account/login/start opens the OAuth browser, watch the new home's
|
|
5502
|
-
// auth.json. Once Codex writes credentials, read the account and persist its
|
|
5503
|
-
// email/plan as the account label, then push the update to the client.
|
|
5504
|
-
function watchCodexLogin(id, home) {
|
|
5505
|
-
try { _codexLoginWatcher?.close?.(); } catch {}
|
|
5506
|
-
_codexLoginWatcher = null;
|
|
5507
|
-
let finished = false;
|
|
5508
|
-
const finalize = async () => {
|
|
5509
|
-
if (finished) return;
|
|
5510
|
-
finished = true;
|
|
5511
|
-
try { _codexLoginWatcher?.close?.(); } catch {}
|
|
5512
|
-
_codexLoginWatcher = null;
|
|
5513
|
-
try {
|
|
5514
|
-
const acct = await safeAccountRead(false);
|
|
5515
|
-
const info = acct?.account || acct || {};
|
|
5516
|
-
const data = loadCodexAccounts();
|
|
5517
|
-
const entry = data.accounts.find((a) => a.id === id);
|
|
5518
|
-
if (entry) {
|
|
5519
|
-
entry.email = info.email || entry.email || '';
|
|
5520
|
-
entry.planType = info.planType || entry.planType || '';
|
|
5521
|
-
entry.label = info.email || entry.label || 'ChatGPT account';
|
|
5522
|
-
saveCodexAccounts(data);
|
|
5523
|
-
}
|
|
5524
|
-
sendToClient({ type: 'account_info', accountId: id, account: acct || {} });
|
|
5525
|
-
sendToClient({ type: 'account_list', accounts: listCodexAccountsForClient(), activeAccountId });
|
|
5526
|
-
} catch (err) {
|
|
5527
|
-
console.warn('[codex-accounts] login finalize failed:', err.message);
|
|
5528
|
-
}
|
|
5529
|
-
};
|
|
5530
|
-
try {
|
|
5531
|
-
_codexLoginWatcher = fsWatch(home, (_evt, fname) => {
|
|
5532
|
-
if (fname === 'auth.json') setTimeout(finalize, 500);
|
|
5533
|
-
});
|
|
5534
|
-
setTimeout(() => { try { _codexLoginWatcher?.close?.(); } catch {} _codexLoginWatcher = null; }, 5 * 60 * 1000);
|
|
5535
|
-
} catch (err) {
|
|
5536
|
-
console.warn('[codex-accounts] auth.json watch failed:', err.message);
|
|
5537
|
-
}
|
|
5538
|
-
}
|
|
5539
|
-
|
|
5540
5740
|
async function bootstrapThread(threadId, cwd = PACKAGE_ROOT) {
|
|
5541
5741
|
await ensureInitialized();
|
|
5542
5742
|
if (!threadId) {
|
|
@@ -5889,7 +6089,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5889
6089
|
const turnParams = {
|
|
5890
6090
|
threadId,
|
|
5891
6091
|
cwd,
|
|
5892
|
-
approvalPolicy: getCodexSidepanelApprovalPolicy(),
|
|
6092
|
+
approvalPolicy: getCodexSidepanelApprovalPolicy(_cachedConfig),
|
|
5893
6093
|
personality: getCachedConfigValue('personality', 'pragmatic'),
|
|
5894
6094
|
// opts.sandboxMode 'read-only': the Assistant brain of a WhatsApp session (lib/assistant-brains/codex.js).
|
|
5895
6095
|
sandboxPolicy: buildCodexSandboxPolicy(cwd, isPlanModeTurn || opts.sandboxMode === 'read-only' ? 'read-only' : getCachedConfigValue('sandbox_mode')),
|
|
@@ -5898,8 +6098,8 @@ CRITICAL — When you need clarification during planning:
|
|
|
5898
6098
|
if (Object.prototype.hasOwnProperty.call(_cachedConfig, 'service_tier')) turnParams.serviceTier = _cachedConfig.service_tier;
|
|
5899
6099
|
if (getCachedConfigValue('model_reasoning_summary')) turnParams.summary = getCachedConfigValue('model_reasoning_summary');
|
|
5900
6100
|
const sessionSettings = codexSessionSettings.get(activeAccountId, threadId);
|
|
5901
|
-
for (const key of ['personality', 'summary', 'serviceTier', 'approvalPolicy', 'approvalsReviewer', 'disabledPluginIds']) {
|
|
5902
|
-
if (Object.prototype.hasOwnProperty.call(sessionSettings, key)) turnParams[key] = sessionSettings[key];
|
|
6101
|
+
for (const key of ['personality', 'summary', 'serviceTier', 'approvalPolicy', 'sandboxPolicy', 'approvalsReviewer', 'disabledPluginIds']) {
|
|
6102
|
+
if (Object.prototype.hasOwnProperty.call(sessionSettings, key) && sessionSettings[key] !== null) turnParams[key] = sessionSettings[key];
|
|
5903
6103
|
}
|
|
5904
6104
|
if (Array.isArray(opts.localImages) && opts.localImages.length) {
|
|
5905
6105
|
turnParams.local_images = opts.localImages;
|
|
@@ -5908,10 +6108,13 @@ CRITICAL — When you need clarification during planning:
|
|
|
5908
6108
|
else if (getCachedConfigValue('model')) turnParams.model = getCachedConfigValue('model');
|
|
5909
6109
|
if (opts.effort) turnParams.effort = opts.effort;
|
|
5910
6110
|
else if (getCachedConfigValue('model_reasoning_effort')) turnParams.effort = getCachedConfigValue('model_reasoning_effort');
|
|
6111
|
+
if (isPlanModeTurn || opts.sandboxMode === 'read-only') turnParams.sandboxPolicy = buildCodexSandboxPolicy(cwd, 'read-only');
|
|
6112
|
+
if (sessionSettings.model != null) turnParams.model = sessionSettings.model;
|
|
6113
|
+
if (sessionSettings.effort != null) turnParams.effort = sessionSettings.effort;
|
|
5911
6114
|
const collaborationMode = buildCodexCollaborationMode({
|
|
5912
6115
|
mode: isPlanModeTurn ? 'plan' : 'default',
|
|
5913
6116
|
model: turnParams.model,
|
|
5914
|
-
effort: opts.effort || (isPlanModeTurn
|
|
6117
|
+
effort: sessionSettings.effort || opts.effort || (isPlanModeTurn
|
|
5915
6118
|
? getCachedConfigValue('plan_mode_reasoning_effort', turnParams.effort)
|
|
5916
6119
|
: turnParams.effort),
|
|
5917
6120
|
developerInstructions: opts.developerInstructions || null,
|
|
@@ -6140,6 +6343,8 @@ CRITICAL — When you need clarification during planning:
|
|
|
6140
6343
|
if (msg.windowId) wsWindowId = msg.windowId;
|
|
6141
6344
|
if (!child && msg.role === 'assistant') codexRole = 'assistant';
|
|
6142
6345
|
if (!child && msg.role === 'assistant' && typeof msg.desktopGrant === 'string' && /^sbd_[A-Za-z0-9_-]{43}$/.test(msg.desktopGrant)) codexDesktopGrant = msg.desktopGrant;
|
|
6346
|
+
// Only the in-process brain's socket carries one (a client's WebSocket never does); no message field is read.
|
|
6347
|
+
if (!child && msg.role === 'assistant' && brainCredentialOf(ws)) codexBrainCredential = brainCredentialOf(ws);
|
|
6143
6348
|
if (!child && msg.role === 'assistant' && msg.routeGate && Array.isArray(msg.routeGate.flags) && msg.routeGate.flags.every((f) => typeof f === 'string')) {
|
|
6144
6349
|
codexRouteGate = { flags: [...msg.routeGate.flags], env: Object.fromEntries(Object.entries(msg.routeGate.env || {}).filter(([k, v]) => /^SYNABUN_ROUTE_GATE_[A-Z_]+$/.test(k) && typeof v === 'string')) };
|
|
6145
6350
|
}
|
|
@@ -6231,10 +6436,21 @@ CRITICAL — When you need clarification during planning:
|
|
|
6231
6436
|
if (await handleCodexCompatMessage(msg, {
|
|
6232
6437
|
activeThreadId, activeTurnId, cwd: PACKAGE_ROOT,
|
|
6233
6438
|
role: codexRole, getActiveThreadId: () => activeThreadId,
|
|
6439
|
+
sandboxPolicyForMode: (mode, cwd) => {
|
|
6440
|
+
validateCodexSettings({ sandbox_mode: mode }, readCodexSettingsSchema(getCodexBin()));
|
|
6441
|
+
return buildCodexSandboxPolicy(cwd, mode);
|
|
6442
|
+
},
|
|
6234
6443
|
limitAppCall: message => appBoundary.consume(activeThreadId, message.originCallId),
|
|
6235
6444
|
resolveAppCard: (callId, threadId) => appBoundary.resolve(threadId, callId,
|
|
6236
6445
|
() => request('thread/read', { threadId, includeTurns: true }, 10000)),
|
|
6237
6446
|
ensureInitialized, request, send: sendToClient,
|
|
6447
|
+
setFeatureEnablement: async enablement => {
|
|
6448
|
+
await request('config/batchWrite', {
|
|
6449
|
+
edits: buildCodexConfigEdits({ features: enablement }), reloadUserConfig: true,
|
|
6450
|
+
}, 10000);
|
|
6451
|
+
await readConfigState();
|
|
6452
|
+
return { enablement };
|
|
6453
|
+
},
|
|
6238
6454
|
registry: codexCapabilities, capabilities: codexCapabilitySnapshot,
|
|
6239
6455
|
withWriterOperation: withCodexWriterOperation,
|
|
6240
6456
|
beforeRequest: beforeCodexCompatRequest, afterRequest: afterCodexCompatRequest,
|
|
@@ -6397,30 +6613,54 @@ CRITICAL — When you need clarification during planning:
|
|
|
6397
6613
|
return;
|
|
6398
6614
|
}
|
|
6399
6615
|
if (msg.type === 'account_read') {
|
|
6616
|
+
// `accountState` is the account as every surface shows it (lib/codex-accounts.js describe()):
|
|
6617
|
+
// its mode, whether it is signed in and its identity. `account` is the running app-server's
|
|
6618
|
+
// own answer, kept for older pages; an account that is signed out is not started for it.
|
|
6619
|
+
const accounts = _codexAccounts();
|
|
6620
|
+
const known = accounts.describeForClient(activeAccountId);
|
|
6621
|
+
if (known?.signedIn === false && !child) {
|
|
6622
|
+
sendToClient({ type: 'account_info', requestId: msg.requestId || null, accountId: activeAccountId, account: { account: null }, accountState: known, state: panelAuth.snapshot() });
|
|
6623
|
+
return;
|
|
6624
|
+
}
|
|
6400
6625
|
try {
|
|
6401
6626
|
await ensureInitialized();
|
|
6402
6627
|
const result = await request('account/read', { refreshToken: !!msg.refreshToken }, 10000);
|
|
6403
|
-
|
|
6628
|
+
// The running app-server answers for its own store only while it runs on it (not on a held key).
|
|
6629
|
+
if (!childUsesHeldKey && result && Object.hasOwn(result, 'account')) accounts.noteNativeStatus(activeAccountId, result);
|
|
6630
|
+
sendToClient({ type: 'account_info', requestId: msg.requestId || null, accountId: activeAccountId, account: result || {}, accountState: accounts.describeForClient(activeAccountId), state: panelAuth.snapshot() });
|
|
6404
6631
|
} catch (err) {
|
|
6405
|
-
sendToClient({ type: 'account_info', requestId: msg.requestId || null, account: {}, error: err.message });
|
|
6632
|
+
sendToClient({ type: 'account_info', requestId: msg.requestId || null, accountId: activeAccountId, account: {}, accountState: accounts.describeForClient(activeAccountId), error: err.message });
|
|
6406
6633
|
}
|
|
6407
6634
|
return;
|
|
6408
6635
|
}
|
|
6409
6636
|
if (msg.type === 'codex_login') {
|
|
6410
6637
|
try {
|
|
6638
|
+
if (activeTurnId || pendingServerRequests.size) throw new Error('Finish or stop the current turn before signing in');
|
|
6411
6639
|
await ensureInitialized();
|
|
6412
|
-
const
|
|
6413
|
-
sendToClient({ type: 'account_login_started', requestId: msg.requestId || null,
|
|
6640
|
+
const state = await panelAuth.start(msg.loginType || 'chatgpt');
|
|
6641
|
+
sendToClient({ type: 'account_login_started', requestId: msg.requestId || null, state });
|
|
6414
6642
|
} catch (err) {
|
|
6415
6643
|
sendToClient({ type: 'error', requestId: msg.requestId || null, message: `Login failed: ${err.message}` });
|
|
6416
6644
|
}
|
|
6417
6645
|
return;
|
|
6418
6646
|
}
|
|
6647
|
+
if (msg.type === 'account_login_status' || msg.type === 'account_login_cancel') {
|
|
6648
|
+
try {
|
|
6649
|
+
if (msg.type === 'account_login_cancel') await panelAuth.cancel();
|
|
6650
|
+
sendToClient({ type: 'account_login_state', requestId: msg.requestId || null, state: panelAuth.snapshot() });
|
|
6651
|
+
} catch (err) { sendToClient({ type: 'error', requestId: msg.requestId || null, message: err.message }); }
|
|
6652
|
+
return;
|
|
6653
|
+
}
|
|
6419
6654
|
if (msg.type === 'account_logout') {
|
|
6655
|
+
// An older page's Logout: the credential the account uses now is signed out, the
|
|
6656
|
+
// same way as the account menu's per-credential sign-out (lib/codex-account-auth.js).
|
|
6420
6657
|
try {
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6658
|
+
if (activeTurnId || pendingServerRequests.size) throw new Error('Finish or stop the current turn before signing out');
|
|
6659
|
+
await panelAuth.cancel();
|
|
6660
|
+
const row = _codexAccounts().describe(activeAccountId);
|
|
6661
|
+
const account = await codexAccountAuth().signOut(activeAccountId, row?.mode === 'apiKey' ? 'apiKey' : 'subscription');
|
|
6662
|
+
panelAuth.reset();
|
|
6663
|
+
sendToClient({ type: 'account_logged_out', requestId: msg.requestId || null, result: {}, accountState: account });
|
|
6424
6664
|
} catch (err) {
|
|
6425
6665
|
sendToClient({ type: 'error', requestId: msg.requestId || null, message: `Logout failed: ${err.message}` });
|
|
6426
6666
|
}
|
|
@@ -6437,7 +6677,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
6437
6677
|
}
|
|
6438
6678
|
if (msg.type === 'account_switch') {
|
|
6439
6679
|
try {
|
|
6440
|
-
const target =
|
|
6680
|
+
const target = findCodexAccount(msg.accountId);
|
|
6441
6681
|
if (!target) throw new Error('Unknown account');
|
|
6442
6682
|
if (target.id === activeAccountId) {
|
|
6443
6683
|
const acct = await safeAccountRead();
|
|
@@ -6456,22 +6696,15 @@ CRITICAL — When you need clarification during planning:
|
|
|
6456
6696
|
if (msg.type === 'account_add_start') {
|
|
6457
6697
|
try {
|
|
6458
6698
|
if (activeTurnId) throw new Error('Finish or stop the current turn before adding an account');
|
|
6459
|
-
|
|
6460
|
-
const
|
|
6461
|
-
const data = loadCodexAccounts();
|
|
6462
|
-
data.accounts.push({
|
|
6463
|
-
id, label: 'New account', email: '', planType: '',
|
|
6464
|
-
type: 'chatgpt', home, createdAt: new Date().toISOString(),
|
|
6465
|
-
});
|
|
6466
|
-
saveCodexAccounts(data);
|
|
6699
|
+
if (panelAuth.active()) throw new Error('Cancel or finish sign-in before adding an account');
|
|
6700
|
+
const { id } = _codexAccounts().create({});
|
|
6467
6701
|
await withCodexWriterOperation('account_add_switch', () => switchCodexAccount(id));
|
|
6468
|
-
const
|
|
6469
|
-
watchCodexLogin(id, home);
|
|
6702
|
+
const state = await panelAuth.start('chatgpt');
|
|
6470
6703
|
sendToClient({
|
|
6471
6704
|
type: 'account_add_started',
|
|
6472
6705
|
requestId: msg.requestId || null,
|
|
6473
6706
|
accountId: id,
|
|
6474
|
-
|
|
6707
|
+
state,
|
|
6475
6708
|
accounts: listCodexAccountsForClient(),
|
|
6476
6709
|
activeAccountId,
|
|
6477
6710
|
});
|
|
@@ -6483,28 +6716,14 @@ CRITICAL — When you need clarification during planning:
|
|
|
6483
6716
|
if (msg.type === 'account_remove') {
|
|
6484
6717
|
try {
|
|
6485
6718
|
if (msg.accountId === 'default') throw new Error('The Default account cannot be removed');
|
|
6486
|
-
|
|
6487
|
-
const
|
|
6488
|
-
if (
|
|
6489
|
-
const nativeAccountInUse = _nativeLoopRuntime?.list({ activeOnly: true })
|
|
6490
|
-
.some((run) => run.provider === 'codex' && run.accountId === msg.accountId)
|
|
6491
|
-
|| _assistantRuntime?.accountInUse?.('codex', msg.accountId)
|
|
6492
|
-
|| _assistantDispatcher?.accountInUse?.('codex', msg.accountId);
|
|
6493
|
-
if (nativeAccountInUse) {
|
|
6494
|
-
throw new Error('Stop native Codex automations using this account before removing it');
|
|
6495
|
-
}
|
|
6496
|
-
const removed = data.accounts[idx];
|
|
6719
|
+
if (!findCodexAccount(msg.accountId)) throw new Error('Unknown account');
|
|
6720
|
+
const reason = codexAccountInUse(msg.accountId, { except: liveConnection });
|
|
6721
|
+
if (reason) throw new Error(reason);
|
|
6497
6722
|
if (activeAccountId === msg.accountId) {
|
|
6498
6723
|
if (activeTurnId) throw new Error('Finish or stop the current turn before removing the active account');
|
|
6499
6724
|
await withCodexWriterOperation('account_remove_switch', () => switchCodexAccount('default'));
|
|
6500
6725
|
}
|
|
6501
|
-
|
|
6502
|
-
saveCodexAccounts(data);
|
|
6503
|
-
try {
|
|
6504
|
-
if (removed.home && removed.home !== CODEX_DEFAULT_HOME && removed.home.startsWith(CODEX_ACCOUNTS_ROOT)) {
|
|
6505
|
-
rmSync(removed.home, { recursive: true, force: true });
|
|
6506
|
-
}
|
|
6507
|
-
} catch (e) { console.warn('[codex-accounts] Failed to delete home:', e.message); }
|
|
6726
|
+
_codexAccounts().remove(msg.accountId, { inUse: (id) => codexAccountInUse(id, { except: liveConnection }) });
|
|
6508
6727
|
sendToClient({
|
|
6509
6728
|
type: 'account_removed',
|
|
6510
6729
|
requestId: msg.requestId || null,
|
|
@@ -6519,12 +6738,9 @@ CRITICAL — When you need clarification during planning:
|
|
|
6519
6738
|
}
|
|
6520
6739
|
if (msg.type === 'account_rename') {
|
|
6521
6740
|
try {
|
|
6522
|
-
const
|
|
6523
|
-
const acct =
|
|
6524
|
-
if (!acct) throw new Error('Unknown account');
|
|
6525
|
-
const label = String(msg.label || '').trim().slice(0, 60);
|
|
6526
|
-
if (label) acct.label = label;
|
|
6527
|
-
saveCodexAccounts(data);
|
|
6741
|
+
const label = String(msg.label || '').trim().slice(0, 80);
|
|
6742
|
+
const acct = label ? _codexAccounts().rename(msg.accountId, label) : _codexAccounts().describe(msg.accountId);
|
|
6743
|
+
if (!acct || acct.id !== msg.accountId) throw new Error('Unknown account');
|
|
6528
6744
|
sendToClient({
|
|
6529
6745
|
type: 'account_renamed',
|
|
6530
6746
|
requestId: msg.requestId || null,
|
|
@@ -6551,7 +6767,8 @@ CRITICAL — When you need clarification during planning:
|
|
|
6551
6767
|
try {
|
|
6552
6768
|
await ensureInitialized();
|
|
6553
6769
|
const result = await readConfigState(msg.cwd || PACKAGE_ROOT);
|
|
6554
|
-
|
|
6770
|
+
const settingsSchema = await readSettingsMetadata();
|
|
6771
|
+
sendToClient({ type: 'config_data', requestId: msg.requestId || null, config: result || {}, settingsSchema, sessionOverrides: codexSessionSettings.get(activeAccountId, msg.threadId || activeThreadId) });
|
|
6555
6772
|
} catch (err) {
|
|
6556
6773
|
sendToClient({ type: 'config_data', requestId: msg.requestId || null, config: {}, error: err.message });
|
|
6557
6774
|
}
|
|
@@ -6560,12 +6777,14 @@ CRITICAL — When you need clarification during planning:
|
|
|
6560
6777
|
if (msg.type === 'config_write') {
|
|
6561
6778
|
try {
|
|
6562
6779
|
await ensureInitialized();
|
|
6563
|
-
const
|
|
6780
|
+
const metadata = await readSettingsMetadata();
|
|
6781
|
+
const supported = validateCodexSettings(msg.config || {}, metadata, metadata.features);
|
|
6782
|
+
const nextConfig = validateCodexConfigRequirements(supported, await readCodexRequirements());
|
|
6564
6783
|
const edits = buildCodexConfigEdits(nextConfig);
|
|
6565
6784
|
await request('config/batchWrite', { edits, reloadUserConfig: true }, 10000);
|
|
6566
|
-
await readConfigState(msg.cwd || PACKAGE_ROOT);
|
|
6785
|
+
const config = await readConfigState(msg.cwd || PACKAGE_ROOT);
|
|
6567
6786
|
codexContextModels = [];
|
|
6568
|
-
sendToClient({ type: 'config_saved', requestId: msg.requestId || null });
|
|
6787
|
+
sendToClient({ type: 'config_saved', requestId: msg.requestId || null, config });
|
|
6569
6788
|
sendToClient({ type: 'catalogs_changed', catalogs: ['models', 'skills', 'apps', 'mcp', 'features', 'config'] });
|
|
6570
6789
|
} catch (err) {
|
|
6571
6790
|
sendToClient({ type: 'error', requestId: msg.requestId || null, message: `Config save failed: ${err.message}` });
|
|
@@ -6671,7 +6890,11 @@ CRITICAL — When you need clarification during planning:
|
|
|
6671
6890
|
if (msg.type === 'experimental_features') {
|
|
6672
6891
|
try {
|
|
6673
6892
|
await ensureInitialized();
|
|
6893
|
+
await readConfigState(msg.cwd || PACKAGE_ROOT);
|
|
6674
6894
|
const result = await request('experimentalFeature/list', codexPagination(msg), 10000);
|
|
6895
|
+
for (const feature of result?.data || []) {
|
|
6896
|
+
if (typeof _cachedConfig.features?.[feature.name] === 'boolean') feature.enabled = _cachedConfig.features[feature.name];
|
|
6897
|
+
}
|
|
6675
6898
|
sendToClient({ type: 'experimental_features', requestId: msg.requestId || null, features: result || {} });
|
|
6676
6899
|
} catch (err) {
|
|
6677
6900
|
sendToClient({ type: 'experimental_features', requestId: msg.requestId || null, features: {}, error: err.message });
|
|
@@ -7207,15 +7430,21 @@ CRITICAL — When you need clarification during planning:
|
|
|
7207
7430
|
}
|
|
7208
7431
|
void retireCurrentCodexChild('WebSocket closed without a reattach identity', { unsubscribe: false });
|
|
7209
7432
|
};
|
|
7433
|
+
ws.on('close', () => _codexLiveConnections.delete(liveConnection));
|
|
7210
7434
|
ws.on('close', handleCodexSocketClose);
|
|
7211
7435
|
}
|
|
7212
7436
|
|
|
7213
7437
|
const _codexModelListCache = new Map();
|
|
7214
7438
|
const CODEX_MODEL_LIST_CACHE_MS = 60_000;
|
|
7215
7439
|
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7440
|
+
// `launch` (lib/codex-accounts.js launchFor()): the list is read under the account's own credential,
|
|
7441
|
+
// with no inherited one, as its sessions would run (in API-key mode, the key it holds over stdin).
|
|
7442
|
+
async function queryCodexAppServerModels(codexHome = CODEX_DEFAULT_HOME, launch = null) {
|
|
7443
|
+
ensureCanonicalCodexConfig({ strict: true, codexHome: launch?.home || codexHome });
|
|
7444
|
+
// The Codex the resolver found (Settings' path first); a command line in Settings ("wsl codex") as written.
|
|
7445
|
+
let codexBin = configuredCommandLine('codex') || getCodexBin();
|
|
7446
|
+
// Without an account's launch plan, still the Windows sandbox the Admin sandbox setting allows.
|
|
7447
|
+
const args = [...(launch?.ok ? codexAppServerArgs(launch) : codexSandboxArgs()), 'app-server'];
|
|
7219
7448
|
if (!/\s/.test(codexBin) && /\.js$/i.test(codexBin)) {
|
|
7220
7449
|
args.unshift(codexBin);
|
|
7221
7450
|
codexBin = process.execPath;
|
|
@@ -7223,9 +7452,12 @@ async function queryCodexAppServerModels(codexHome = CODEX_DEFAULT_HOME) {
|
|
|
7223
7452
|
const useShell = /\s/.test(codexBin)
|
|
7224
7453
|
|| !codexBin.includes(sep)
|
|
7225
7454
|
|| (process.platform === 'win32' && /\.(cmd|bat)$/i.test(codexBin));
|
|
7226
|
-
const env =
|
|
7455
|
+
const env = launch?.ok
|
|
7456
|
+
? { ...applyCodexLaunchEnv(process.env, launch), NO_COLOR: '1' }
|
|
7457
|
+
: { ...process.env, CODEX_HOME: codexHome, NO_COLOR: '1' };
|
|
7227
7458
|
delete env.FORCE_COLOR;
|
|
7228
7459
|
delete env.CLICOLOR_FORCE;
|
|
7460
|
+
let apiKey = launch?.ok ? launch.apiKey : null;
|
|
7229
7461
|
|
|
7230
7462
|
return new Promise((resolve, reject) => {
|
|
7231
7463
|
// (A configured command may be a command line; a file with a space in its path is one word.)
|
|
@@ -7311,6 +7543,10 @@ async function queryCodexAppServerModels(codexHome = CODEX_DEFAULT_HOME) {
|
|
|
7311
7543
|
clientInfo: { name: 'synabun-codex-models', title: 'SynaBun Codex Models', version: '0.0.1' },
|
|
7312
7544
|
capabilities: { experimentalApi: true },
|
|
7313
7545
|
}, 15000);
|
|
7546
|
+
if (apiKey) {
|
|
7547
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'initialized' }) + '\n');
|
|
7548
|
+
try { await request('account/login/start', { type: 'apiKey', apiKey }, 15000); } finally { apiKey = null; }
|
|
7549
|
+
}
|
|
7314
7550
|
const result = await request('model/list', { includeHidden: false }, 10000);
|
|
7315
7551
|
cleanup();
|
|
7316
7552
|
resolve(Array.isArray(result?.models) ? result.models : (Array.isArray(result?.data) ? result.data : []));
|
|
@@ -7334,7 +7570,7 @@ app.get('/api/codex/models', async (req, res) => {
|
|
|
7334
7570
|
return res.json({ ok: true, source: 'cache', models: cached.models });
|
|
7335
7571
|
}
|
|
7336
7572
|
const models = mergeCodexModelContextCapabilities(
|
|
7337
|
-
await queryCodexAppServerModels(codexHome),
|
|
7573
|
+
await queryCodexAppServerModels(codexHome, _codexAccounts().launchFor(account.id)),
|
|
7338
7574
|
readCodexRuntimeModelCatalog(codexHome),
|
|
7339
7575
|
);
|
|
7340
7576
|
// The Assistant's catalog is built from the default account's list: when that list
|
|
@@ -7348,12 +7584,20 @@ app.get('/api/codex/models', async (req, res) => {
|
|
|
7348
7584
|
}
|
|
7349
7585
|
});
|
|
7350
7586
|
|
|
7351
|
-
// GET /api/codex/accounts —
|
|
7352
|
-
//
|
|
7353
|
-
//
|
|
7354
|
-
|
|
7587
|
+
// GET /api/codex/accounts — every Codex account with its state (mode, signed in,
|
|
7588
|
+
// identity: an email or a masked key, never a key) for the Codex panel's account
|
|
7589
|
+
// menu and the launch selectors (Automation Studio, Schedules). Emails are
|
|
7590
|
+
// personal data: not for invite guests. `?refresh=true` asks Codex about every
|
|
7591
|
+
// account's own store first; otherwise an old answer is asked again in the
|
|
7592
|
+
// background and announced (`codex:accounts-changed`) when it changes.
|
|
7593
|
+
app.get('/api/codex/accounts', async (req, res) => {
|
|
7594
|
+
if (isGuestRequest(req)) return res.status(403).json({ ok: false, error: 'Admin only' });
|
|
7595
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
7355
7596
|
try {
|
|
7356
|
-
|
|
7597
|
+
const accounts = _codexAccounts();
|
|
7598
|
+
if (req.query?.refresh === 'true') await accounts.refreshStatus(null, { force: true });
|
|
7599
|
+
else accounts.refreshStatus().catch(() => {});
|
|
7600
|
+
res.json({ ok: true, accounts: accounts.listForClient() });
|
|
7357
7601
|
} catch (err) {
|
|
7358
7602
|
res.json({ ok: false, error: err.message, accounts: [] });
|
|
7359
7603
|
}
|
|
@@ -7779,7 +8023,7 @@ function updateOpencodeRuntimeProfileConfig(xdgRoot, profile) {
|
|
|
7779
8023
|
return true;
|
|
7780
8024
|
}
|
|
7781
8025
|
|
|
7782
|
-
async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, keepAlive = false, mcpProfile = null } = {}) {
|
|
8026
|
+
async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, keepAlive = false, mcpProfile = null, brainCredential = null } = {}) {
|
|
7783
8027
|
if (_ocpHistoryClearInProgress) throw new Error('OpenCode history is being cleared');
|
|
7784
8028
|
const requestedProfile = normalizeMcpProfileName(mcpProfile || readActiveMcpProfile());
|
|
7785
8029
|
let entry = _ocpIsoServes.get(termId);
|
|
@@ -7804,11 +8048,6 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, k
|
|
|
7804
8048
|
entry = { termId, port, xdgRoot, mcpProfile: requestedProfile, mcpProfileRefreshFailed: false, proc: null, client: null, ready: false, starting: true, keepAlive: !!keepAlive, unsub: null, sessions: new Set(), lastUsed: Date.now() };
|
|
7805
8049
|
_ocpIsoServes.set(termId, entry);
|
|
7806
8050
|
try {
|
|
7807
|
-
const config = loadCliConfig();
|
|
7808
|
-
const cmd = config.opencode?.command || 'opencode';
|
|
7809
|
-
const shellArgs = IS_WIN
|
|
7810
|
-
? ['/d', '/s', '/c', `${cmd} serve --port ${port}`]
|
|
7811
|
-
: ['-lc', `exec ${cmd} serve --port ${port}`];
|
|
7812
8051
|
const isolatedEnv = {
|
|
7813
8052
|
...process.env,
|
|
7814
8053
|
XDG_CONFIG_HOME: xdgRoot,
|
|
@@ -7820,11 +8059,16 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, k
|
|
|
7820
8059
|
// Keep this above the script's own deadlines while retaining a hard cap.
|
|
7821
8060
|
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS:
|
|
7822
8061
|
process.env.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || '300000',
|
|
8062
|
+
// A WhatsApp conversation's brain credential: the config names it ({env:SYNABUN_BRAIN_TOKEN}), only this
|
|
8063
|
+
// serve's environment holds it (a fresh serve per brain launch: the brain stops its serve when it ends).
|
|
8064
|
+
...(brainCredential ? { SYNABUN_BRAIN_TOKEN: brainCredential } : {}),
|
|
7823
8065
|
};
|
|
7824
8066
|
delete isolatedEnv.CLAUDECODE;
|
|
7825
|
-
|
|
8067
|
+
if (!brainCredential) delete isolatedEnv.SYNABUN_BRAIN_TOKEN;
|
|
8068
|
+
const launch = opencodeServeLaunch(port, { env: isolatedEnv });
|
|
8069
|
+
const proc = spawn(launch.command, launch.args, {
|
|
7826
8070
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7827
|
-
|
|
8071
|
+
...launch.options,
|
|
7828
8072
|
detached: false, shell: false,
|
|
7829
8073
|
});
|
|
7830
8074
|
entry.proc = proc;
|
|
@@ -8490,15 +8734,11 @@ async function ensureOpencodeServer({ allowDuringHistoryClear = false } = {}) {
|
|
|
8490
8734
|
|
|
8491
8735
|
// 2. Spawn opencode serve
|
|
8492
8736
|
try {
|
|
8493
|
-
const
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
const shellArgs = IS_WIN
|
|
8497
|
-
? ['/d', '/s', '/c', `${cmd} serve --port ${_ocpPort}`]
|
|
8498
|
-
: ['-lc', `exec ${cmd} serve --port ${_ocpPort}`];
|
|
8499
|
-
_ocpProc = spawn(DEFAULT_SHELL, shellArgs, {
|
|
8737
|
+
const launch = opencodeServeLaunch(_ocpPort, { env: { ...process.env, FORCE_COLOR: '0', OPENCODE_DISABLE_CLAUDE_CODE: '1' } });
|
|
8738
|
+
console.log(`[opencode] Spawning: ${launch.label}`);
|
|
8739
|
+
_ocpProc = spawn(launch.command, launch.args, {
|
|
8500
8740
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
8501
|
-
|
|
8741
|
+
...launch.options,
|
|
8502
8742
|
detached: false,
|
|
8503
8743
|
shell: false,
|
|
8504
8744
|
});
|
|
@@ -8786,7 +9026,7 @@ app.post('/api/ollama/configure', async (req, res) => {
|
|
|
8786
9026
|
} catch {}
|
|
8787
9027
|
if (needsInstall) {
|
|
8788
9028
|
console.log('[ollama] Installing @ai-sdk/openai-compatible…');
|
|
8789
|
-
execSync('npm install @ai-sdk/openai-compatible --save', { cwd: configDir, timeout: 60000, stdio: 'pipe' });
|
|
9029
|
+
execSync('npm install @ai-sdk/openai-compatible --save', { cwd: configDir, timeout: 60000, stdio: 'pipe', env: withPackagedPathLast(process.env) });
|
|
8790
9030
|
console.log('[ollama] Package installed');
|
|
8791
9031
|
}
|
|
8792
9032
|
|
|
@@ -8885,9 +9125,11 @@ app.post('/api/sidepanel/session-title', async (req, res) => {
|
|
|
8885
9125
|
}
|
|
8886
9126
|
const result = await generateSessionTitle(input, {
|
|
8887
9127
|
claudeExecutable: getClaudeSdkExecutable,
|
|
9128
|
+
childEnv: claudeChildEnv,
|
|
8888
9129
|
execFile: execFileAsync,
|
|
8889
9130
|
getCodexBin,
|
|
8890
9131
|
getCodexAccountHome,
|
|
9132
|
+
codexConfigArgs: codexSandboxArgs,
|
|
8891
9133
|
getAugmentedPath,
|
|
8892
9134
|
ensureOpenCode: ensureOpencodeServer,
|
|
8893
9135
|
openCodeClient: opencodeV2Client,
|
|
@@ -9516,6 +9758,7 @@ const _claudeBridgeReady = (async () => {
|
|
|
9516
9758
|
try {
|
|
9517
9759
|
_claudeBridge = await import('./lib/claude-agent-bridge.js');
|
|
9518
9760
|
_claudeBridge.configureClaudeBridge({
|
|
9761
|
+
accountRateLimit: (id, info) => _assistantAccountUsage?.note('claude-code', id, info),
|
|
9519
9762
|
// The greeting a panel conversation opened with, as the one note its model gets (lib/greeting/).
|
|
9520
9763
|
greetingNote: (id, opts) => _greeting?.consumeNote(id, opts) || '',
|
|
9521
9764
|
// A prompt typed in the panel: when the person was last here (a temporary chat is named to nobody).
|
|
@@ -9556,9 +9799,16 @@ const _claudeBridgeReady = (async () => {
|
|
|
9556
9799
|
// a session without one is refused with the install hint. Lazy, so the
|
|
9557
9800
|
// (memoized) resolution happens on demand, not at boot.
|
|
9558
9801
|
claudeExecutable: () => getClaudeSdkExecutable(),
|
|
9802
|
+
// On Windows, the Git Bash every session's Claude Code gets (lib/git-bash/): SynaBun's copy where the PC has
|
|
9803
|
+
// none, so the Bash tool exists and the hooks run in bash. Panel tabs and the Assistant's brain.
|
|
9804
|
+
childEnv: (env) => claudeChildEnv(env),
|
|
9559
9805
|
// Sidepanel tabs can run under another Claude account (its own config
|
|
9560
9806
|
// directory). The default account adds nothing to the environment.
|
|
9561
9807
|
claudeAccountEnv: (accountId) => _panelClaudeAccounts().envFor(accountId),
|
|
9808
|
+
// How a session runs under an account: inherited credentials removed, the
|
|
9809
|
+
// account's directory and, in API-key mode, its key added (launchFor() in
|
|
9810
|
+
// lib/claude-accounts.js). Panel tabs and the Assistant's brain.
|
|
9811
|
+
claudeAccountLaunch: (accountId) => _panelClaudeAccounts().launchFor(accountId),
|
|
9562
9812
|
// This server has the session routes (rename, tag, fork, delete, subagent
|
|
9563
9813
|
// transcripts): the panel offers those actions only when told so.
|
|
9564
9814
|
sessionOps: true,
|
|
@@ -9591,14 +9841,42 @@ const _claudeBridgeReady = (async () => {
|
|
|
9591
9841
|
}
|
|
9592
9842
|
})();
|
|
9593
9843
|
|
|
9594
|
-
// The Claude accounts registry
|
|
9595
|
-
//
|
|
9844
|
+
// The Claude accounts registry: one instance for the sidepanel, the Assistant
|
|
9845
|
+
// (brain, workers, catalog) and every route, so one account state is shown
|
|
9846
|
+
// everywhere (lib/claude-accounts.js describe()). Its subscription half is
|
|
9847
|
+
// Claude Code's own `auth status --json` for each account; a change of any
|
|
9848
|
+
// account reaches every window as `claude:accounts-changed`.
|
|
9596
9849
|
let _panelClaudeAccountsInstance = null;
|
|
9597
9850
|
function _panelClaudeAccounts() {
|
|
9598
|
-
if (!_panelClaudeAccountsInstance)
|
|
9851
|
+
if (!_panelClaudeAccountsInstance) {
|
|
9852
|
+
_panelClaudeAccountsInstance = createPanelClaudeAccounts({
|
|
9853
|
+
dataHome: DATA_HOME, homeDir: getHomePath(),
|
|
9854
|
+
readNativeStatus: (accountId) => panelClaudeAuth().nativeStatus(accountId),
|
|
9855
|
+
log: (tag, message) => console.log(`[${tag}]`, message),
|
|
9856
|
+
});
|
|
9857
|
+
_panelClaudeAccountsInstance.subscribe((event) => {
|
|
9858
|
+
try {
|
|
9859
|
+
broadcastSync({ type: 'claude:accounts-changed', accountId: event.accountId || null, removed: !!event.removed, created: !!event.created });
|
|
9860
|
+
// The Assistant's account list and catalog listen to their own event.
|
|
9861
|
+
broadcastSync({ type: 'assistant:accounts-changed', provider: 'claude-code', accountId: event.accountId || null, removed: !!event.removed });
|
|
9862
|
+
_assistantCatalog?.invalidate?.();
|
|
9863
|
+
} catch {}
|
|
9864
|
+
});
|
|
9865
|
+
}
|
|
9599
9866
|
return _panelClaudeAccountsInstance;
|
|
9600
9867
|
}
|
|
9601
9868
|
|
|
9869
|
+
// Why a Claude account cannot be removed now ('' when it can): a live session
|
|
9870
|
+
// (a panel tab, the Assistant's brain), a dispatched run, or the Assistant's
|
|
9871
|
+
// choice of worker account names it.
|
|
9872
|
+
function claudeAccountInUse(accountId) {
|
|
9873
|
+
try { if (_claudeBridge?.accountInUse?.(accountId)) return 'A Claude tab or the Assistant is running under this account. End that session first.'; } catch {}
|
|
9874
|
+
try { if (_assistantDispatcher?.accountInUse?.('claude-code', accountId)) return 'Dispatched runs are using this account. Stop them first.'; } catch {}
|
|
9875
|
+
try { if (_assistantRuntime?.accountInUse?.('claude-code', accountId)) return 'An Assistant session uses this account. Close it first.'; } catch {}
|
|
9876
|
+
try { if (_assistantClaudeWorkerAccount?.() === accountId) return 'This is the Assistant\'s Claude worker account. Choose another one first.'; } catch {}
|
|
9877
|
+
return '';
|
|
9878
|
+
}
|
|
9879
|
+
|
|
9602
9880
|
// The config directory of the Claude account a sidepanel request names: '' for
|
|
9603
9881
|
// the default account. An account that is no longer set up is an error (404,
|
|
9604
9882
|
// `account_unavailable`), never the default account's folder in its place.
|
|
@@ -9611,14 +9889,159 @@ function panelAccountHome(value) {
|
|
|
9611
9889
|
return home;
|
|
9612
9890
|
}
|
|
9613
9891
|
|
|
9614
|
-
// GET /api/claude/accounts —
|
|
9615
|
-
//
|
|
9616
|
-
|
|
9892
|
+
// GET /api/claude/accounts — every Claude account with its state (mode,
|
|
9893
|
+
// signed in, identity: an email or a masked key, never a key). Emails are
|
|
9894
|
+
// personal data: not for invite guests. `?refresh=true` asks Claude Code about
|
|
9895
|
+
// every account's subscription first; otherwise an answer older than a few
|
|
9896
|
+
// minutes is asked again in the background and announced when it changes.
|
|
9897
|
+
app.get('/api/claude/accounts', async (req, res) => {
|
|
9617
9898
|
if (isGuestRequest(req)) return res.status(403).json({ ok: false, error: 'Admin only' });
|
|
9618
|
-
|
|
9899
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9900
|
+
try {
|
|
9901
|
+
const accounts = _panelClaudeAccounts();
|
|
9902
|
+
if (req.query.refresh === 'true') await accounts.refreshStatus(null, { force: true });
|
|
9903
|
+
else accounts.refreshStatus().catch(() => {});
|
|
9904
|
+
res.json({ ok: true, accounts: accounts.listForClient() });
|
|
9905
|
+
}
|
|
9619
9906
|
catch (err) { res.status(500).json({ ok: false, error: err.message }); }
|
|
9620
9907
|
});
|
|
9621
9908
|
|
|
9909
|
+
let _panelClaudeAuthInstance = null;
|
|
9910
|
+
const panelAuthOwnerGuard = ownerRouteGuard({ isGuestRequest, port: PORT, allowedOrigins: publicWebOrigins, refuseCrossSite: true });
|
|
9911
|
+
function panelClaudeAuth() {
|
|
9912
|
+
if (!_panelClaudeAuthInstance) {
|
|
9913
|
+
_panelClaudeAuthInstance = createClaudePanelAuth({ accounts: _panelClaudeAccounts(), executable: getClaudeSdkExecutable, env: () => (withPathEnv(process.env, getAugmentedPath())), changeCredentials: (id, change) => _claudeBridge?.withPanelAccountAuthChange ? _claudeBridge.withPanelAccountAuthChange(id, change) : change() });
|
|
9914
|
+
process.once('exit', () => _panelClaudeAuthInstance?.close());
|
|
9915
|
+
}
|
|
9916
|
+
return _panelClaudeAuthInstance;
|
|
9917
|
+
}
|
|
9918
|
+
|
|
9919
|
+
// Admin-only account auth; OAuth stays native and pasted keys are profile-scoped.
|
|
9920
|
+
app.get('/api/claude/accounts/:id/auth', panelAuthOwnerGuard, async (req, res) => {
|
|
9921
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9922
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9923
|
+
try {
|
|
9924
|
+
panelAccountHome(req.params.id);
|
|
9925
|
+
const result = req.query.refresh === 'true' ? await panelClaudeAuth().read(req.params.id) : { state: panelClaudeAuth().snapshot(req.params.id) };
|
|
9926
|
+
res.json(result);
|
|
9927
|
+
} catch (error) { res.status(error.status || 500).json({ error: error.message }); }
|
|
9928
|
+
});
|
|
9929
|
+
app.post('/api/claude/accounts/:id/auth/:action', panelAuthOwnerGuard, async (req, res) => {
|
|
9930
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9931
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9932
|
+
try {
|
|
9933
|
+
panelAccountHome(req.params.id);
|
|
9934
|
+
const auth = panelClaudeAuth();
|
|
9935
|
+
if (req.params.action === 'start') return res.json({ state: auth.start(req.params.id) });
|
|
9936
|
+
if (req.params.action === 'cancel') return res.json({ state: auth.cancel(req.params.id, req.body?.operationId) });
|
|
9937
|
+
if (req.params.action === 'code') return res.json({ state: auth.submitCode(req.params.id, req.body?.operationId, req.body?.code) });
|
|
9938
|
+
if (req.params.action === 'logout') {
|
|
9939
|
+
// `credential`: 'apiKey', 'subscription', or both when left out (the older /logout).
|
|
9940
|
+
const credential = typeof req.body?.credential === 'string' ? req.body.credential : 'all';
|
|
9941
|
+
await auth.signOut(req.params.id, credential);
|
|
9942
|
+
return res.json({ ok: true, account: _panelClaudeAccounts().describeForClient(req.params.id) });
|
|
9943
|
+
}
|
|
9944
|
+
res.status(404).json({ error: 'Unknown authentication action' });
|
|
9945
|
+
} catch (error) { res.status(error.status || 500).json({ error: error.message }); }
|
|
9946
|
+
});
|
|
9947
|
+
|
|
9948
|
+
registerProviderPanelApiKeyAuthRoutes(app, {
|
|
9949
|
+
guard: panelAuthOwnerGuard,
|
|
9950
|
+
claude: (id, key) => panelClaudeAuth().signInApiKey(id, key),
|
|
9951
|
+
// A Codex key is checked with OpenAI and kept by SynaBun for that account (lib/codex-account-auth.js);
|
|
9952
|
+
// the account's idle Codex processes restart on it. Codex's own store (its ChatGPT login) is not touched.
|
|
9953
|
+
codex: (id, key) => codexAccountAuth().signInApiKey(id, key),
|
|
9954
|
+
});
|
|
9955
|
+
|
|
9956
|
+
// Codex accounts: the same model and routes as Claude's (lib/codex-accounts.js describe(), launchFor()).
|
|
9957
|
+
// Everything but the list carries the owner guard; a credential or mode change restarts the
|
|
9958
|
+
// account's idle panel processes and is refused while one of them is mid-turn.
|
|
9959
|
+
app.post('/api/codex/accounts', panelAuthOwnerGuard, (req, res) => {
|
|
9960
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9961
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9962
|
+
try {
|
|
9963
|
+
const created = _codexAccounts().create({ label: req.body?.label });
|
|
9964
|
+
res.json({ account: _codexAccounts().describeForClient(created.id) });
|
|
9965
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9966
|
+
});
|
|
9967
|
+
// Rename (`label`) and/or choose what the account's sessions use (`mode`: 'subscription' (ChatGPT) | 'apiKey').
|
|
9968
|
+
app.patch('/api/codex/accounts/:id', panelAuthOwnerGuard, async (req, res) => {
|
|
9969
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9970
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9971
|
+
try {
|
|
9972
|
+
const accounts = _codexAccounts();
|
|
9973
|
+
if (accounts.describe(req.params.id)?.id !== req.params.id) return res.status(404).json({ code: 'ACCOUNT_NOT_FOUND', error: 'That Codex account is no longer set up in SynaBun.' });
|
|
9974
|
+
const { label, mode } = req.body || {};
|
|
9975
|
+
if (label === undefined && mode === undefined) return res.status(400).json({ code: 'NOTHING_TO_CHANGE', error: 'Send a label or a mode.' });
|
|
9976
|
+
if (label !== undefined) accounts.rename(req.params.id, label);
|
|
9977
|
+
if (mode !== undefined) await codexAccountAuth().setMode(req.params.id, mode);
|
|
9978
|
+
res.json({ ok: true, account: accounts.describeForClient(req.params.id) });
|
|
9979
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9980
|
+
});
|
|
9981
|
+
// Remove an added account and its folder. Default stays; an account something uses stays.
|
|
9982
|
+
app.delete('/api/codex/accounts/:id', panelAuthOwnerGuard, (req, res) => {
|
|
9983
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9984
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9985
|
+
try { res.json(_codexAccounts().remove(req.params.id, { inUse: (id) => codexAccountInUse(id) })); }
|
|
9986
|
+
catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9987
|
+
});
|
|
9988
|
+
// `?refresh=true` asks Codex what the account's own store holds first.
|
|
9989
|
+
app.get('/api/codex/accounts/:id/auth', panelAuthOwnerGuard, async (req, res) => {
|
|
9990
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9991
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9992
|
+
try {
|
|
9993
|
+
if (_codexAccounts().describe(req.params.id)?.id !== req.params.id) return res.status(404).json({ code: 'ACCOUNT_NOT_FOUND', error: 'That Codex account is no longer set up in SynaBun.' });
|
|
9994
|
+
if (req.query.refresh === 'true') return res.json(await codexAccountAuth().read(req.params.id));
|
|
9995
|
+
res.json({ accountId: req.params.id, account: _codexAccounts().describeForClient(req.params.id) });
|
|
9996
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9997
|
+
});
|
|
9998
|
+
// `credential`: 'apiKey' (the account's key), 'subscription' (Codex's own logout in its home), or both when left out.
|
|
9999
|
+
app.post('/api/codex/accounts/:id/auth/logout', panelAuthOwnerGuard, async (req, res) => {
|
|
10000
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
10001
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
10002
|
+
try {
|
|
10003
|
+
if (_codexAccounts().describe(req.params.id)?.id !== req.params.id) return res.status(404).json({ code: 'ACCOUNT_NOT_FOUND', error: 'That Codex account is no longer set up in SynaBun.' });
|
|
10004
|
+
const credential = typeof req.body?.credential === 'string' ? req.body.credential : 'all';
|
|
10005
|
+
const account = await codexAccountAuth().signOut(req.params.id, credential);
|
|
10006
|
+
res.json({ ok: true, account });
|
|
10007
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
10008
|
+
});
|
|
10009
|
+
|
|
10010
|
+
app.post('/api/claude/accounts', panelAuthOwnerGuard, (req, res) => {
|
|
10011
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
10012
|
+
try {
|
|
10013
|
+
const created = _panelClaudeAccounts().create({ label: req.body?.label });
|
|
10014
|
+
const row = _panelClaudeAccounts().listForClient().find(a => a.id === created.id);
|
|
10015
|
+
res.json({ account: row });
|
|
10016
|
+
} catch (error) { res.status(error.status || 500).json({ error: error.message }); }
|
|
10017
|
+
});
|
|
10018
|
+
|
|
10019
|
+
// Rename an account (`label`) or choose what its sessions use (`mode`:
|
|
10020
|
+
// 'subscription' | 'apiKey'; the chosen credential must be there). A mode
|
|
10021
|
+
// change ends the account's idle sessions so the next message starts under it.
|
|
10022
|
+
app.patch('/api/claude/accounts/:id', panelAuthOwnerGuard, async (req, res) => {
|
|
10023
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
10024
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
10025
|
+
try {
|
|
10026
|
+
const accounts = _panelClaudeAccounts();
|
|
10027
|
+
if (!accounts.describe(req.params.id) || accounts.describe(req.params.id).id !== req.params.id) return res.status(404).json({ code: 'ACCOUNT_NOT_FOUND', error: 'That Claude account is no longer set up in SynaBun.' });
|
|
10028
|
+
const { label, mode } = req.body || {};
|
|
10029
|
+
if (label === undefined && mode === undefined) return res.status(400).json({ code: 'NOTHING_TO_CHANGE', error: 'Send a label or a mode.' });
|
|
10030
|
+
if (label !== undefined) accounts.rename(req.params.id, label);
|
|
10031
|
+
if (mode !== undefined) await panelClaudeAuth().setMode(req.params.id, mode);
|
|
10032
|
+
res.json({ ok: true, account: accounts.describeForClient(req.params.id) });
|
|
10033
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
10034
|
+
});
|
|
10035
|
+
|
|
10036
|
+
// Remove an added account and its config directory. Default stays; an account
|
|
10037
|
+
// a live session, a dispatched run or the Assistant's worker choice uses stays.
|
|
10038
|
+
app.delete('/api/claude/accounts/:id', panelAuthOwnerGuard, (req, res) => {
|
|
10039
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
10040
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
10041
|
+
try { res.json(_panelClaudeAccounts().remove(req.params.id, { inUse: claudeAccountInUse })); }
|
|
10042
|
+
catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
10043
|
+
});
|
|
10044
|
+
|
|
9622
10045
|
function _readClaudeSkinConfig() {
|
|
9623
10046
|
try {
|
|
9624
10047
|
const cfg = JSON.parse(readFileSync(resolve(DATA_HOME, 'data', 'cli-config.json'), 'utf-8'));
|
|
@@ -10806,8 +11229,9 @@ app.post('/api/youtube/download', async (req, res) => {
|
|
|
10806
11229
|
|
|
10807
11230
|
// A GUI-launched NI may not inherit the user's shell PATH, so yt-dlp can't find
|
|
10808
11231
|
// node/deno/ffmpeg. Prepend the running node's dir + the common Homebrew/local bins.
|
|
11232
|
+
// A packaged application's own runtime still goes last (withPackagedPathLast): the user's node first when there is one.
|
|
10809
11233
|
const extraPath = [dirname(process.execPath), '/opt/homebrew/bin', '/usr/local/bin', join(os.homedir(), '.local/bin')].join(delimiter);
|
|
10810
|
-
const childEnv =
|
|
11234
|
+
const childEnv = withPackagedPathLast(withPathEnv(process.env, `${extraPath}${delimiter}${pathEnvValue(process.env)}`));
|
|
10811
11235
|
|
|
10812
11236
|
const runYtDlp = (finalArgs) => new Promise((done) => {
|
|
10813
11237
|
const child = spawn(yd.cmd, finalArgs, { windowsHide: true, env: childEnv });
|
|
@@ -12767,8 +13191,10 @@ app.get('/api/loop/active', (req, res) => {
|
|
|
12767
13191
|
if (data?.active) {
|
|
12768
13192
|
const elapsed = Date.now() - new Date(data.startedAt).getTime();
|
|
12769
13193
|
const maxMs = ((data.maxMinutes || 30) + 5) * 60 * 1000;
|
|
12770
|
-
// Stale loop — past time cap + grace, delete it
|
|
12771
|
-
|
|
13194
|
+
// Stale loop — past time cap + grace, delete it. Not a dispatched worker's (runMode
|
|
13195
|
+
// 'task'): its maxMinutes is work time, which the dispatcher enforces, and a warm run
|
|
13196
|
+
// idles for a follow-up far past it; a dead one goes with the check below.
|
|
13197
|
+
if (data.runMode !== 'task' && elapsed >= maxMs) {
|
|
12772
13198
|
try { unlinkSync(filePath); } catch { /* ok */ }
|
|
12773
13199
|
continue;
|
|
12774
13200
|
}
|
|
@@ -12780,6 +13206,10 @@ app.get('/api/loop/active', (req, res) => {
|
|
|
12780
13206
|
continue;
|
|
12781
13207
|
}
|
|
12782
13208
|
}
|
|
13209
|
+
// A dispatched worker's time left is its work time left (the dispatcher's work clock); null
|
|
13210
|
+
// while the dispatcher does not know the run (right after a restart): never the wall clock.
|
|
13211
|
+
const isTaskRun = data.runMode === 'task';
|
|
13212
|
+
const work = isTaskRun && data.runId ? (_assistantDispatcher?.peek?.(data.runId) || null) : null;
|
|
12783
13213
|
activeLoops.push({
|
|
12784
13214
|
sessionId: f.replace('.json', ''),
|
|
12785
13215
|
task: data.task,
|
|
@@ -12788,7 +13218,9 @@ app.get('/api/loop/active', (req, res) => {
|
|
|
12788
13218
|
totalIterations: data.totalIterations || 10,
|
|
12789
13219
|
maxMinutes: data.maxMinutes || 30,
|
|
12790
13220
|
elapsedMinutes: Math.round(elapsed / 60000),
|
|
12791
|
-
remainingMinutes:
|
|
13221
|
+
remainingMinutes: work && Number.isFinite(work.workLeftMs)
|
|
13222
|
+
? Math.round(work.workLeftMs / 60000)
|
|
13223
|
+
: isTaskRun ? null : Math.max(0, Math.round(((data.maxMinutes || 30) * 60 * 1000 - elapsed) / 60000)),
|
|
12792
13224
|
startedAt: data.startedAt,
|
|
12793
13225
|
lastIterationAt: data.lastIterationAt,
|
|
12794
13226
|
browserSessionId: data.browserSessionId || null,
|
|
@@ -13128,6 +13560,8 @@ app.post('/api/loop/launch', async (req, res) => {
|
|
|
13128
13560
|
mcpProfile: cliMcpProfile,
|
|
13129
13561
|
codexAccountId: codexAccount?.id || null,
|
|
13130
13562
|
codexHome: codexAccount ? (codexAccount.home || CODEX_DEFAULT_HOME) : null,
|
|
13563
|
+
// A Codex exec loop keeps the Admin sandbox setting it started with (every iteration is its own `codex exec`).
|
|
13564
|
+
adminSandbox: cliProfile === 'codex' ? adminSandboxOn() : undefined,
|
|
13131
13565
|
// Continuation mechanism. 'hook': Claude Code ONLY — the Stop/UserPromptSubmit hooks in
|
|
13132
13566
|
// .claude/ set awaitingNext and attachLoopDriver() drives /clear + re-prompt; if those
|
|
13133
13567
|
// hooks are absent (e.g. a claude-code task run under a hookless harness) the loop STALLS.
|
|
@@ -14126,6 +14560,8 @@ async function launchScheduledLoop(schedule) {
|
|
|
14126
14560
|
mcpProfile: cliMcpProfile || null,
|
|
14127
14561
|
codexAccountId: codexAccount?.id || null,
|
|
14128
14562
|
codexHome: codexAccount ? (codexAccount.home || CODEX_DEFAULT_HOME) : null,
|
|
14563
|
+
// A Codex exec loop keeps the Admin sandbox setting it started with (every iteration is its own `codex exec`).
|
|
14564
|
+
adminSandbox: cliProfile === 'codex' ? adminSandboxOn() : undefined,
|
|
14129
14565
|
driverType: nativeRuntime ? 'native' : (cliProfile === 'claude-code' ? 'hook' : 'exec'),
|
|
14130
14566
|
runtimeType: nativeRuntime ? 'native' : 'pty',
|
|
14131
14567
|
surface: nativeRuntime ? 'sidepanel' : 'terminal',
|
|
@@ -15454,7 +15890,7 @@ function spawnAgentProcess(agent, prompt, extraEnv = {}) {
|
|
|
15454
15890
|
|
|
15455
15891
|
const child = spawn(shellCommand(claudeBin, AGENT_IS_WIN), args, {
|
|
15456
15892
|
cwd: agent.cwd,
|
|
15457
|
-
env: { ...cleanEnv, FORCE_COLOR: '0', NO_COLOR: '1', TERM: 'dumb', ...extraEnv },
|
|
15893
|
+
env: claudeChildEnv({ ...cleanEnv, FORCE_COLOR: '0', NO_COLOR: '1', TERM: 'dumb', ...extraEnv }),
|
|
15458
15894
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
15459
15895
|
shell: AGENT_IS_WIN,
|
|
15460
15896
|
windowsHide: true,
|
|
@@ -16129,6 +16565,18 @@ async function checkSynabunUpdate() {
|
|
|
16129
16565
|
// stable releases with a suffix.
|
|
16130
16566
|
const installedChannel = synabunReleaseChannel(current);
|
|
16131
16567
|
|
|
16568
|
+
// The desktop app updates itself (packaging/desktop: its window's update function, fed by the GitHub
|
|
16569
|
+
// releases' desktop files). Nothing here offers it an npm or GitHub update; there is nothing to check.
|
|
16570
|
+
if (PACKAGED_RUNTIME?.shell) {
|
|
16571
|
+
_synabunUpdateCache = {
|
|
16572
|
+
..._synabunUpdateCache,
|
|
16573
|
+
current, installedChannel, latest: null, npmUpdateAvailable: false, gitUpdateAvailable: false, updateAvailable: false,
|
|
16574
|
+
source: null, installPlan: null, checkedAt: new Date().toISOString(), npmError: null, gitError: null, desktopManaged: true,
|
|
16575
|
+
};
|
|
16576
|
+
console.log(` Updates: from the SynaBun app (v${current})`);
|
|
16577
|
+
return;
|
|
16578
|
+
}
|
|
16579
|
+
|
|
16132
16580
|
const fetchJsonWithTimeout = async (url, ms = 5000) => {
|
|
16133
16581
|
const controller = new AbortController();
|
|
16134
16582
|
const timeout = setTimeout(() => controller.abort(), ms);
|
|
@@ -16224,6 +16672,72 @@ app.get('/api/system/version', async (req, res) => {
|
|
|
16224
16672
|
res.json(_synabunUpdateCache);
|
|
16225
16673
|
});
|
|
16226
16674
|
|
|
16675
|
+
// GET /api/system/about — Settings → About (lib/system-about.js): the app, the runtime, the project's links and
|
|
16676
|
+
// every dependency of the product's packages with the version installed, read from the files on disk on the first
|
|
16677
|
+
// request and kept for the life of the process. No environment, no token, no setting. Owner only (/api/system is
|
|
16678
|
+
// in ADMIN_ONLY_PREFIXES).
|
|
16679
|
+
const systemAbout = createSystemAbout({
|
|
16680
|
+
packageRoot: PACKAGE_ROOT,
|
|
16681
|
+
dataHome: DATA_HOME,
|
|
16682
|
+
port: PORT,
|
|
16683
|
+
install: PACKAGED_RUNTIME?.shell ? 'desktop' : INSTALL_KIND,
|
|
16684
|
+
// The WhatsApp connector is installed on demand, into the data folder (lib/whatsapp/paths.js runtimeDir).
|
|
16685
|
+
installDirs: { 'whatsapp-connector': [join(DATA_HOME, 'runtime', 'whatsapp')] },
|
|
16686
|
+
vendorDir: join(__dirname, 'public', 'vendor'),
|
|
16687
|
+
});
|
|
16688
|
+
app.get('/api/system/about', (req, res) => {
|
|
16689
|
+
res.set('Cache-Control', 'no-store');
|
|
16690
|
+
try {
|
|
16691
|
+
res.json(systemAbout.get());
|
|
16692
|
+
} catch (err) {
|
|
16693
|
+
res.status(500).json({ error: 'about_failed', detail: String(err?.message || err).slice(0, 200) });
|
|
16694
|
+
}
|
|
16695
|
+
});
|
|
16696
|
+
|
|
16697
|
+
// GET /api/system/activity — whether anything runs that a restart would end. The
|
|
16698
|
+
// desktop app installs a downloaded update only while this says idle
|
|
16699
|
+
// (packaging/desktop/src/updater/controller.mjs); lib/system-activity.js adds the
|
|
16700
|
+
// counters up, and one that throws makes the answer "not idle". Terminals are not
|
|
16701
|
+
// counted: nothing here can tell a command that runs from a shell that waits.
|
|
16702
|
+
function systemActivitySources() {
|
|
16703
|
+
const nativeRuns = () => _nativeLoopRuntime?.list?.({ activeOnly: true }) || [];
|
|
16704
|
+
return {
|
|
16705
|
+
assistant: () => {
|
|
16706
|
+
const runtime = _assistantRuntime;
|
|
16707
|
+
const sessions = runtime?._internals?.sessions;
|
|
16708
|
+
const brains = sessions ? countWhere(sessions.keys(), (id) => runtime.isBusy(id)) : 0;
|
|
16709
|
+
const runs = _assistantDispatcher ? _assistantDispatcher.list({ activeOnly: true, limit: 500 }).length : 0;
|
|
16710
|
+
const agents = countWhere(agentRegistry, (agent) => agent?.status === 'running');
|
|
16711
|
+
return brains + runs + agents;
|
|
16712
|
+
},
|
|
16713
|
+
'claude-panel': () => _claudeBridge?.panelSessionsWorking?.() || 0,
|
|
16714
|
+
'codex-panel': () => countWhere(_codexLiveConnections, (connection) => !!connection?.busy?.())
|
|
16715
|
+
+ countWhere(_codexOrphanedProcs, (orphan) => orphan?.isIdle?.() !== true),
|
|
16716
|
+
'opencode-panel': () => {
|
|
16717
|
+
const blockers = getOpenCodeHistoryBlockers();
|
|
16718
|
+
return blockers.activeTurns + blockers.startingTurns;
|
|
16719
|
+
},
|
|
16720
|
+
'claude-cli': () => recentCliSessions(sessionRegistry),
|
|
16721
|
+
loop: () => {
|
|
16722
|
+
// A dispatched worker also runs in the native runtime: it is counted under assistant.
|
|
16723
|
+
const native = countWhere(nativeRuns(), (run) => run?.runMode !== 'task' && run?.source !== 'assistant');
|
|
16724
|
+
const files = countWhere(readLoopStateFiles(), ({ data }) => !!data?.active && (!data.terminalSessionId || isLoopRuntimeAlive(data)));
|
|
16725
|
+
const terminals = countWhere(terminalSessions, (session) => !!session?._loopTerminalSessionId);
|
|
16726
|
+
return Math.max(native + terminals, files);
|
|
16727
|
+
},
|
|
16728
|
+
schedule: () => _scheduleQueue.length + (_scheduleQueueRunning ? 1 : 0),
|
|
16729
|
+
backup: () => (_backupOperationActive ? 1 : 0) + (_autoBackupPromise ? 1 : 0),
|
|
16730
|
+
maintenance: () => (_reindexJob?.running ? 1 : 0) + (_indexingJob && !_indexingJob.done ? 1 : 0)
|
|
16731
|
+
+ countWhere(Object.values(cliInstaller.jobs()), (job) => job?.state === 'running')
|
|
16732
|
+
+ (_projectStorageClearInProgress ? 1 : 0) + (_ocpHistoryClearInProgress ? 1 : 0),
|
|
16733
|
+
};
|
|
16734
|
+
}
|
|
16735
|
+
|
|
16736
|
+
app.get('/api/system/activity', (req, res) => {
|
|
16737
|
+
res.set('Cache-Control', 'no-store');
|
|
16738
|
+
res.json(collectActivity(systemActivitySources(), { log: (line) => console.warn(`[activity] ${line}`) }));
|
|
16739
|
+
});
|
|
16740
|
+
|
|
16227
16741
|
// ═══════════════════════════════════════════
|
|
16228
16742
|
// EXTERNAL CLI TOOL VERSION CHECK
|
|
16229
16743
|
// ═══════════════════════════════════════════
|
|
@@ -16236,6 +16750,7 @@ const TOOL_DEFS = [
|
|
|
16236
16750
|
];
|
|
16237
16751
|
|
|
16238
16752
|
let _toolVersionCache = { checkedAt: null, tools: {} };
|
|
16753
|
+
let _toolVersionGeneration = 0;
|
|
16239
16754
|
|
|
16240
16755
|
// Backwards-compat helper — returns a "MAJOR.MINOR.PATCH" string (no pre).
|
|
16241
16756
|
// Kept because legacy callers expect a string for display purposes only.
|
|
@@ -16255,35 +16770,7 @@ function parseVersion(raw) {
|
|
|
16255
16770
|
// parseSemver / compareSemver / semverNewer live in lib/synabun-version.js, with
|
|
16256
16771
|
// the rules that order SynaBun's own date-style and semver-era releases.
|
|
16257
16772
|
|
|
16258
|
-
|
|
16259
|
-
try {
|
|
16260
|
-
const isWin = process.platform === 'win32';
|
|
16261
|
-
const finder = isWin ? `where ${cmd}` : `command -v ${cmd}`;
|
|
16262
|
-
const opts = { encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'], env };
|
|
16263
|
-
if (!isWin) opts.shell = '/bin/sh';
|
|
16264
|
-
const raw = execSync(finder, opts).trim();
|
|
16265
|
-
const first = raw.split(/\r?\n/)[0] || null;
|
|
16266
|
-
if (!first) return null;
|
|
16267
|
-
// Follow symlinks so install-source detection sees the real install dir
|
|
16268
|
-
// (e.g. /usr/local/bin/claude → /usr/local/lib/node_modules/.../bin/claude).
|
|
16269
|
-
try { return realpathSync(first); } catch { return first; }
|
|
16270
|
-
} catch { return null; }
|
|
16271
|
-
}
|
|
16272
|
-
|
|
16273
|
-
// Async twin of resolveBinPath — version checks run these probes for every
|
|
16274
|
-
// tool; the sync variant blocks the event loop for the full subprocess time.
|
|
16275
|
-
async function resolveBinPathAsync(cmd, env) {
|
|
16276
|
-
try {
|
|
16277
|
-
const isWin = process.platform === 'win32';
|
|
16278
|
-
const finder = isWin ? `where ${cmd}` : `command -v ${cmd}`;
|
|
16279
|
-
const opts = { encoding: 'utf-8', timeout: 2000, env, windowsHide: true };
|
|
16280
|
-
if (!isWin) opts.shell = '/bin/sh';
|
|
16281
|
-
const { stdout } = await execAsync(finder, opts);
|
|
16282
|
-
const first = (stdout || '').trim().split(/\r?\n/)[0] || null;
|
|
16283
|
-
if (!first) return null;
|
|
16284
|
-
try { return realpathSync(first); } catch { return first; }
|
|
16285
|
-
} catch { return null; }
|
|
16286
|
-
}
|
|
16773
|
+
// Where each CLI is: lib/cli-resolver.js (cliResolver, near getClaudeBin).
|
|
16287
16774
|
|
|
16288
16775
|
// detectInstallSource / buildUpdateCommand / NPM_PKGS now live in
|
|
16289
16776
|
// ./lib/cli-update-planner.js so they can be unit tested away from this file.
|
|
@@ -16340,13 +16827,6 @@ function isInsideSynabun(binPath) {
|
|
|
16340
16827
|
}
|
|
16341
16828
|
}
|
|
16342
16829
|
|
|
16343
|
-
// Versions of @anthropic-ai/claude-code that SynaBun has historically shipped
|
|
16344
|
-
// as a bundled dependency. If a `claude --version` probe returns one of these,
|
|
16345
|
-
// it's almost certainly the stale bundle (a real user-global install would be
|
|
16346
|
-
// the current 2.1.116+). Reject hard regardless of where the binary path looks
|
|
16347
|
-
// to come from. Add new versions here if SynaBun ever ships claude-code again.
|
|
16348
|
-
const KNOWN_STALE_CLAUDE_VERSIONS = new Set(['2.1.71', '2.1.89', '2.1.91']);
|
|
16349
|
-
|
|
16350
16830
|
// Remove any external tool that ended up in our own node_modules: Claude Code,
|
|
16351
16831
|
// Codex, OpenCode and Gemini CLI are installed by the user, never by SynaBun,
|
|
16352
16832
|
// and the agent SDKs are installed without the executables they would carry
|
|
@@ -16367,52 +16847,25 @@ function pruneBundledCliTools() {
|
|
|
16367
16847
|
}
|
|
16368
16848
|
}
|
|
16369
16849
|
|
|
16370
|
-
async function checkToolVersions() {
|
|
16371
|
-
|
|
16372
|
-
//
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16377
|
-
|
|
16378
|
-
const
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
//
|
|
16382
|
-
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
|
|
16386
|
-
|
|
16387
|
-
|
|
16388
|
-
|
|
16389
|
-
const isStaleBundle = isInsideSynabun(probedBinPath);
|
|
16390
|
-
|
|
16391
|
-
let installed = null;
|
|
16392
|
-
if (!isStaleBundle) {
|
|
16393
|
-
try {
|
|
16394
|
-
const { stdout } = await execAsync(`${tool.cmd} ${tool.versionArg}`, execOpts);
|
|
16395
|
-
installed = parseVersion(stdout.trim());
|
|
16396
|
-
} catch { /* not installed or errored */ }
|
|
16397
|
-
}
|
|
16398
|
-
|
|
16399
|
-
// Nuclear safety net for the recurring claude phantom-version bug:
|
|
16400
|
-
// only reject when binary path detection failed (truly phantom). If we
|
|
16401
|
-
// resolved a path AND it's outside SynaBun's own dirs, trust the version
|
|
16402
|
-
// — Anthropic ships these versions in the native installer too, so a
|
|
16403
|
-
// version-only blacklist hits legit installs at ~/.local/bin/claude.exe.
|
|
16404
|
-
if (
|
|
16405
|
-
tool.key === 'claude-code' &&
|
|
16406
|
-
installed &&
|
|
16407
|
-
KNOWN_STALE_CLAUDE_VERSIONS.has(installed) &&
|
|
16408
|
-
!probedBinPath
|
|
16409
|
-
) {
|
|
16410
|
-
console.warn(
|
|
16411
|
-
` [version-guard] Rejected claude v${installed} from (unknown path)` +
|
|
16412
|
-
` — matches known-stale SynaBun bundle and no binary path could be resolved.`
|
|
16413
|
-
);
|
|
16414
|
-
installed = null;
|
|
16415
|
-
}
|
|
16850
|
+
async function checkToolVersions({ force = false } = {}) {
|
|
16851
|
+
const generation = ++_toolVersionGeneration;
|
|
16852
|
+
// Every CLI exactly as the panels and launchers find it (lib/cli-resolver.js):
|
|
16853
|
+
// the path in Settings first, then PATH with the OS PATH read afresh, then the
|
|
16854
|
+
// known install folders; installed (with its version), missing, or broken
|
|
16855
|
+
// (found, but `--version` failed: the real error). Never a copy inside
|
|
16856
|
+
// SynaBun, so the badge reflects the CLI the user would run in a terminal.
|
|
16857
|
+
// `force` is Re-check: the OS PATH is read again and every --version asked again.
|
|
16858
|
+
const statuses = await cliResolver.resolveAll(TOOL_DEFS.map((tool) => tool.key), { force });
|
|
16859
|
+
const cleanEnv = withPathEnv(process.env, getAugmentedPath());
|
|
16860
|
+
|
|
16861
|
+
// Only pay for the brew probe when a brew-managed tool is actually present.
|
|
16862
|
+
const brewOutdated = Object.values(statuses).some((status) => status.state === 'installed' && isBrewSource(status.installSource))
|
|
16863
|
+
? await probeBrewOutdated(cleanEnv)
|
|
16864
|
+
: null;
|
|
16865
|
+
|
|
16866
|
+
const checkOneTool = async (tool) => {
|
|
16867
|
+
const status = statuses[tool.key];
|
|
16868
|
+
const installed = status.state === 'installed' ? status.version : null;
|
|
16416
16869
|
|
|
16417
16870
|
// Upstream (npm) version. For a brew install this is NOT the yardstick —
|
|
16418
16871
|
// Homebrew routinely trails npm by a few releases, so comparing against it
|
|
@@ -16433,9 +16886,8 @@ async function checkToolVersions() {
|
|
|
16433
16886
|
}
|
|
16434
16887
|
} catch { /* network error */ }
|
|
16435
16888
|
|
|
16436
|
-
const
|
|
16437
|
-
const
|
|
16438
|
-
const brewToken = isBrewSource(installSource) ? resolveBrewToken(tool.key, binPath) : null;
|
|
16889
|
+
const installSource = status.installSource;
|
|
16890
|
+
const brewToken = isBrewSource(installSource) ? resolveBrewToken(tool.key, status.realPath || status.path) : null;
|
|
16439
16891
|
const latest = resolveLatest({
|
|
16440
16892
|
installSource,
|
|
16441
16893
|
brewToken,
|
|
@@ -16443,8 +16895,10 @@ async function checkToolVersions() {
|
|
|
16443
16895
|
npmLatest: upstreamLatest,
|
|
16444
16896
|
installed,
|
|
16445
16897
|
});
|
|
16898
|
+
// A self-updater is started by the path found, so the update reaches this
|
|
16899
|
+
// very installation even where a terminal's PATH does not name its folder.
|
|
16446
16900
|
const updateCommand = installed
|
|
16447
|
-
? buildUpdateCommand(tool.key, installSource, { brewToken })
|
|
16901
|
+
? buildUpdateCommand(tool.key, installSource, { brewToken, binPath: status.path, platform: process.platform })
|
|
16448
16902
|
: null;
|
|
16449
16903
|
|
|
16450
16904
|
return [tool.key, {
|
|
@@ -16454,32 +16908,33 @@ async function checkToolVersions() {
|
|
|
16454
16908
|
updateAvailable: !!(installed && latest && semverNewer(installed, latest)),
|
|
16455
16909
|
installSource,
|
|
16456
16910
|
brewToken,
|
|
16457
|
-
binPath,
|
|
16911
|
+
binPath: status.path,
|
|
16458
16912
|
updateCommand,
|
|
16459
16913
|
canUpdate: !!updateCommand,
|
|
16914
|
+
state: status.state,
|
|
16915
|
+
version: status.version,
|
|
16916
|
+
error: status.error,
|
|
16917
|
+
foundBy: status.foundBy,
|
|
16918
|
+
realPath: status.realPath,
|
|
16919
|
+
launchPath: status.launchPath,
|
|
16920
|
+
configured: status.configured,
|
|
16921
|
+
checked: status.candidates,
|
|
16922
|
+
guideUrl: CLI_INSTALL_GUIDES[tool.key] || null,
|
|
16923
|
+
install: cliInstaller.describe(tool.key),
|
|
16924
|
+
pathStatus: cliPathManager.inspect(status),
|
|
16460
16925
|
}];
|
|
16461
16926
|
};
|
|
16462
16927
|
|
|
16463
|
-
|
|
16464
|
-
// brew-managed tool is actually present.
|
|
16465
|
-
const probedPaths = await Promise.all(
|
|
16466
|
-
TOOL_DEFS.map((tool) => resolveBinPathAsync(tool.cmd, cleanEnv).catch(() => null)),
|
|
16467
|
-
);
|
|
16468
|
-
const brewOutdated = probedPaths.some((p) => isBrewSource(detectInstallSource(p)))
|
|
16469
|
-
? await probeBrewOutdated(cleanEnv)
|
|
16470
|
-
: null;
|
|
16471
|
-
|
|
16472
|
-
const entries = await Promise.all(
|
|
16473
|
-
TOOL_DEFS.map((tool, i) => checkOneTool(tool, probedPaths[i])),
|
|
16474
|
-
);
|
|
16928
|
+
const entries = await Promise.all(TOOL_DEFS.map((tool) => checkOneTool(tool)));
|
|
16475
16929
|
const results = Object.fromEntries(entries);
|
|
16476
16930
|
|
|
16477
|
-
// Tell a missing tool's install banner
|
|
16478
|
-
//
|
|
16479
|
-
//
|
|
16480
|
-
//
|
|
16481
|
-
// that
|
|
16931
|
+
// Tell a missing tool's install banner the command that fits this machine:
|
|
16932
|
+
// the vendor's own installer for this system (Anthropic's native installer
|
|
16933
|
+
// for Claude Code always), unless the CLIs that ARE installed show a package
|
|
16934
|
+
// manager this machine uses (Homebrew, bun, pnpm …), which is kept to, so
|
|
16935
|
+
// that the manual command never creates a second, conflicting install.
|
|
16482
16936
|
const siblingSources = Object.values(results)
|
|
16937
|
+
.filter((v) => v.state === 'installed')
|
|
16483
16938
|
.map((v) => v.installSource)
|
|
16484
16939
|
.filter((s) => s && s !== 'unknown' && s !== 'other');
|
|
16485
16940
|
const dominantSource = siblingSources
|
|
@@ -16488,23 +16943,40 @@ async function checkToolVersions() {
|
|
|
16488
16943
|
|| 'unknown';
|
|
16489
16944
|
for (const [key, info] of Object.entries(results)) {
|
|
16490
16945
|
const source = info.installed ? info.installSource : dominantSource;
|
|
16491
|
-
info.installCommand = buildInstallCommand(key, source);
|
|
16946
|
+
info.installCommand = buildInstallCommand(key, source, { platform: process.platform });
|
|
16492
16947
|
}
|
|
16493
16948
|
|
|
16949
|
+
// A check started before a newer refresh must never publish its stale answer.
|
|
16950
|
+
if (generation !== _toolVersionGeneration) return;
|
|
16494
16951
|
_toolVersionCache = { checkedAt: new Date().toISOString(), tools: results };
|
|
16495
16952
|
|
|
16496
16953
|
const updates = Object.entries(results).filter(([, v]) => v.updateAvailable);
|
|
16954
|
+
const broken = Object.entries(results).filter(([, v]) => v.state === 'broken');
|
|
16497
16955
|
if (updates.length) {
|
|
16498
16956
|
console.log(` CLI Tools: ${updates.map(([k, v]) => `${k} v${v.installed}→v${v.latest}`).join(', ')}`);
|
|
16499
16957
|
} else {
|
|
16500
16958
|
console.log(' CLI Tools: all up to date');
|
|
16501
16959
|
}
|
|
16960
|
+
for (const [key, info] of broken) console.warn(` CLI Tools: ${key} at ${info.binPath} does not start: ${info.error}`);
|
|
16502
16961
|
}
|
|
16503
16962
|
|
|
16504
16963
|
// Single-flight: concurrent requests share one refresh instead of each
|
|
16505
16964
|
// spawning their own set of subprocess probes.
|
|
16506
16965
|
let _toolVersionInFlight = null;
|
|
16507
|
-
|
|
16966
|
+
let _toolVersionForced = null;
|
|
16967
|
+
function refreshToolVersions({ force = false, fresh = false } = {}) {
|
|
16968
|
+
// A Re-check never takes the answer of a plain check already under way: it
|
|
16969
|
+
// reads the OS PATH and asks every --version again.
|
|
16970
|
+
if (force) {
|
|
16971
|
+
if (fresh || !_toolVersionForced) {
|
|
16972
|
+
const refresh = checkToolVersions({ force: true })
|
|
16973
|
+
.catch(() => {})
|
|
16974
|
+
.finally(() => { if (_toolVersionForced === refresh) _toolVersionForced = null; });
|
|
16975
|
+
_toolVersionForced = refresh;
|
|
16976
|
+
}
|
|
16977
|
+
return _toolVersionForced;
|
|
16978
|
+
}
|
|
16979
|
+
if (_toolVersionForced) return _toolVersionForced;
|
|
16508
16980
|
if (!_toolVersionInFlight) {
|
|
16509
16981
|
_toolVersionInFlight = checkToolVersions()
|
|
16510
16982
|
.catch(() => {})
|
|
@@ -16516,8 +16988,9 @@ function refreshToolVersions() {
|
|
|
16516
16988
|
app.get('/api/system/tool-versions', async (req, res) => {
|
|
16517
16989
|
const force = req.query.force === '1';
|
|
16518
16990
|
if (force || !_toolVersionCache.checkedAt) {
|
|
16519
|
-
// force=1
|
|
16520
|
-
await
|
|
16991
|
+
// force=1 (Re-check, the post-update badge refresh, the panels' poll while
|
|
16992
|
+
// a CLI is missing) needs fresh data: await it.
|
|
16993
|
+
await refreshToolVersions({ force });
|
|
16521
16994
|
} else {
|
|
16522
16995
|
const age = Date.now() - new Date(_toolVersionCache.checkedAt).getTime();
|
|
16523
16996
|
// Stale-while-revalidate: serve the cached value now, refresh in background.
|
|
@@ -16526,6 +16999,40 @@ app.get('/api/system/tool-versions', async (req, res) => {
|
|
|
16526
16999
|
res.json(_toolVersionCache);
|
|
16527
17000
|
});
|
|
16528
17001
|
|
|
17002
|
+
// ── One-click install of a missing CLI (lib/cli-install.js, lib/cli-install-api.js) ──
|
|
17003
|
+
// The side panels' Install button: the vendor's official installer for this
|
|
17004
|
+
// system (or OpenCode's release archive on Windows), never a source a request
|
|
17005
|
+
// names, guarded like the other local mutating endpoints, its progress streamed
|
|
17006
|
+
// as NDJSON. Its last step is the resolver looking again (force); the tool
|
|
17007
|
+
// versions are then refreshed, so every panel's next check shows the CLI.
|
|
17008
|
+
const cliPathManager = createCliPathManager({
|
|
17009
|
+
onChanged: () => { _toolVersionCache = { ..._toolVersionCache, checkedAt: null }; },
|
|
17010
|
+
});
|
|
17011
|
+
app.use('/api/cli/path', createCliPathApi({
|
|
17012
|
+
pathManager: cliPathManager, resolver: cliResolver, isGuestRequest, port: () => PORT,
|
|
17013
|
+
diagnostics: async () => {
|
|
17014
|
+
const fresh = await freshTerminalPath();
|
|
17015
|
+
const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode']);
|
|
17016
|
+
return { platform: process.platform, pathKey: Object.keys(fresh.env).find(key => key.toUpperCase() === 'PATH'),
|
|
17017
|
+
entries: fresh.entries, os: fresh.os, pathext: fresh.env.PATHEXT || fresh.env.PathExt || null,
|
|
17018
|
+
tools: Object.fromEntries(Object.entries(statuses).map(([tool, status]) => [tool, { path: status.path, launchPath: status.launchPath, ...cliPathManager.inspect(status) }])) };
|
|
17019
|
+
},
|
|
17020
|
+
}));
|
|
17021
|
+
const cliInstaller = createCliInstaller({
|
|
17022
|
+
resolver: cliResolver,
|
|
17023
|
+
addToPath: status => cliPathManager.add(status),
|
|
17024
|
+
readOsPath: async () => (await osPath.refresh({ force: true })).entries,
|
|
17025
|
+
onFinished: () => {
|
|
17026
|
+
_toolVersionCache = { ..._toolVersionCache, checkedAt: null };
|
|
17027
|
+
// The install changes the disk: even a Re-check already running predates it.
|
|
17028
|
+
refreshToolVersions({ force: true, fresh: true });
|
|
17029
|
+
},
|
|
17030
|
+
log: (line) => console.log(line),
|
|
17031
|
+
});
|
|
17032
|
+
app.use('/api/cli/install', createCliInstallApi({ installer: cliInstaller, isGuestRequest, port: () => PORT, log: (line) => console.warn(line) }));
|
|
17033
|
+
// Settings beside the CLIs: the Git Bash Claude Code gets on Windows, its download and a new look (lib/git-bash/).
|
|
17034
|
+
app.use('/api/cli/git-bash', createGitBashApi({ manager: gitBash, isGuestRequest, port: () => PORT, onRecheck: async () => { syncGitBashSettings(); } }));
|
|
17035
|
+
|
|
16529
17036
|
// ═══════════════════════════════════════════
|
|
16530
17037
|
// FULL SYSTEM BACKUP & RESTORE
|
|
16531
17038
|
// ═══════════════════════════════════════════
|
|
@@ -16710,12 +17217,13 @@ app.post('/api/system/restore', async (req, res) => {
|
|
|
16710
17217
|
const mcpDistCheck = resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'index.js');
|
|
16711
17218
|
if (!existsSync(mcpDistCheck)) {
|
|
16712
17219
|
try {
|
|
16713
|
-
//
|
|
17220
|
+
// TypeScript is a devDependency of the package root (a development checkout also has it in mcp-server/).
|
|
16714
17221
|
const mcpDir = resolve(PACKAGE_ROOT, 'mcp-server');
|
|
16715
|
-
if (!
|
|
16716
|
-
|
|
17222
|
+
if (!mcpCompiler(PACKAGE_ROOT)) {
|
|
17223
|
+
const npm = npmCommand(npmInvocation(), fallbackInstallArgs({ includeDev: true }));
|
|
17224
|
+
await execFileAsync(npm.file, npm.args, { cwd: PACKAGE_ROOT, timeout: 600_000, maxBuffer: 16 * 1024 * 1024, shell: npm.shell, windowsHide: true, env: withPackagedPathLast(fallbackInstallEnv({ ...process.env, NODE_ENV: 'development' })) });
|
|
16717
17225
|
}
|
|
16718
|
-
|
|
17226
|
+
await execFileAsync(process.execPath, [mcpCompiler(PACKAGE_ROOT), '-p', mcpDir], { cwd: mcpDir, timeout: 120_000, maxBuffer: 16 * 1024 * 1024, windowsHide: true });
|
|
16719
17227
|
results.files.push('(auto-built MCP server)');
|
|
16720
17228
|
} catch (buildErr) {
|
|
16721
17229
|
results.errors.push(`MCP server auto-build failed: ${buildErr.message}`);
|
|
@@ -16992,13 +17500,10 @@ const editorCompletionServe = createOpencodeCompletionEngine({
|
|
|
16992
17500
|
// A provider's {file:./key} is relative to that configuration: it is written whole for the serve's own folder.
|
|
16993
17501
|
userConfigDir: () => dirname(getOpencodeConfigPath()),
|
|
16994
17502
|
allocPort: allocOpencodeIsoPort,
|
|
16995
|
-
// As the isolated serves are started:
|
|
17503
|
+
// As the isolated serves are started (opencodeServeLaunch): the CLI the resolver found, through the login shell.
|
|
16996
17504
|
spawnServe: ({ port, env, cwd }) => {
|
|
16997
|
-
const
|
|
16998
|
-
const
|
|
16999
|
-
? ['/d', '/s', '/c', `${cmd} serve --port ${port}`]
|
|
17000
|
-
: ['-lc', `exec ${cmd} serve --port ${port}`];
|
|
17001
|
-
const proc = spawn(DEFAULT_SHELL, shellArgs, { stdio: ['ignore', 'ignore', 'pipe'], env, cwd, detached: false, shell: false, windowsHide: true });
|
|
17505
|
+
const launch = opencodeServeLaunch(port, { env });
|
|
17506
|
+
const proc = spawn(launch.command, launch.args, { stdio: ['ignore', 'ignore', 'pipe'], ...launch.options, cwd, detached: false, shell: false, windowsHide: true });
|
|
17002
17507
|
proc.stderr?.on('data', (d) => console.log(`[editor-complete:err] ${String(d).trim().slice(0, 500)}`));
|
|
17003
17508
|
return proc;
|
|
17004
17509
|
},
|
|
@@ -17856,7 +18361,7 @@ app.get('/api/setup/check-deps', async (req, res) => {
|
|
|
17856
18361
|
|
|
17857
18362
|
// npm
|
|
17858
18363
|
try {
|
|
17859
|
-
const { stdout } = await execAsync('npm --version', { timeout: 5000 });
|
|
18364
|
+
const { stdout } = await execAsync('npm --version', { timeout: 5000, env: withPackagedPathLast(process.env) });
|
|
17860
18365
|
deps.push({ id: 'npm', name: 'npm', ok: true, version: `v${stdout.trim()}`, detail: `v${stdout.trim()}`, url: 'https://nodejs.org/' });
|
|
17861
18366
|
} catch {
|
|
17862
18367
|
deps.push({ id: 'npm', name: 'npm', ok: false, version: null, detail: 'Not found', url: 'https://nodejs.org/' });
|
|
@@ -17867,6 +18372,38 @@ app.get('/api/setup/check-deps', async (req, res) => {
|
|
|
17867
18372
|
res.json({ deps });
|
|
17868
18373
|
});
|
|
17869
18374
|
|
|
18375
|
+
// GET /api/setup/install-details — what this install has (lib/install-details.js), for the Details areas of the
|
|
18376
|
+
// setup wizard inside the desktop app: the Node.js and the npm and npx SynaBun runs (each asked for its version),
|
|
18377
|
+
// where `npm i -g` puts packages, the user's own npm, every part's dependencies and versions, the MCP build, and
|
|
18378
|
+
// what a packaged application came with. Read only; the owner's only (/api/setup is admin-only for guests).
|
|
18379
|
+
app.get('/api/setup/install-details', async (req, res) => {
|
|
18380
|
+
try {
|
|
18381
|
+
// The one prefix PATH is built from (packagedNpmPrefix; for an npm or Git install, the one the user set).
|
|
18382
|
+
const npm = PACKAGED_RUNTIME ? packagedNpmPrefix(PACKAGED_RUNTIME) : effectiveNpmPrefix();
|
|
18383
|
+
const prefix = npm?.prefix || null;
|
|
18384
|
+
let manifest = null;
|
|
18385
|
+
if (PACKAGED_RUNTIME) { try { manifest = JSON.parse(readFileSync(PACKAGED_RUNTIME.manifestPath, 'utf8')); } catch {} }
|
|
18386
|
+
let browserExecutable = null;
|
|
18387
|
+
try { browserExecutable = chromium.executablePath(); } catch {}
|
|
18388
|
+
let osEntries = [];
|
|
18389
|
+
try { osEntries = (await osPath.refresh()).entries || []; } catch {}
|
|
18390
|
+
res.json(await collectInstallDetails({
|
|
18391
|
+
packageRoot: PACKAGE_ROOT,
|
|
18392
|
+
packaged: PACKAGED_RUNTIME,
|
|
18393
|
+
manifest,
|
|
18394
|
+
osPath: osEntries,
|
|
18395
|
+
npmPrefix: prefix,
|
|
18396
|
+
npmPrefixSource: !prefix ? null : npm.source === 'builtin' || npm.source === 'default' ? 'synabun' : 'user',
|
|
18397
|
+
npmPrefixFrom: prefix && npm.source !== 'builtin' && npm.source !== 'default' ? npm.from || null : null,
|
|
18398
|
+
npmGlobalBin: prefix ? npmGlobalBinFor(prefix) : null,
|
|
18399
|
+
browserExecutable,
|
|
18400
|
+
}));
|
|
18401
|
+
} catch (err) {
|
|
18402
|
+
console.error('GET /api/setup/install-details error:', err.message);
|
|
18403
|
+
res.status(500).json({ error: err.message });
|
|
18404
|
+
}
|
|
18405
|
+
});
|
|
18406
|
+
|
|
17870
18407
|
// GET /api/setup/onboarding — Check what's configured (onboarding wizard)
|
|
17871
18408
|
app.get('/api/setup/onboarding', async (req, res) => {
|
|
17872
18409
|
try {
|
|
@@ -17937,9 +18474,11 @@ app.post('/api/setup/build', async (req, res) => {
|
|
|
17937
18474
|
res.setHeader('Connection', 'keep-alive');
|
|
17938
18475
|
res.flushHeaders();
|
|
17939
18476
|
|
|
18477
|
+
// What npm wrote, as the wizard shows it: secrets out first (lib/log-redact.js), then the last 500 characters.
|
|
17940
18478
|
function send(event, data) {
|
|
17941
|
-
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
18479
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(redactDeep(data))}\n\n`);
|
|
17942
18480
|
}
|
|
18481
|
+
const tail = (text) => redactSecrets(String(text || '')).slice(-500);
|
|
17943
18482
|
|
|
17944
18483
|
const dirs = [
|
|
17945
18484
|
{ name: 'Neural Interface', path: resolve(PACKAGE_ROOT, 'neural-interface') },
|
|
@@ -17950,28 +18489,34 @@ app.post('/api/setup/build', async (req, res) => {
|
|
|
17950
18489
|
const mcpDist = resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'index.js');
|
|
17951
18490
|
const mcpNeedsBuild = !existsSync(mcpDist);
|
|
17952
18491
|
|
|
17953
|
-
// Phase 1: npm
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17958
|
-
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
}
|
|
17963
|
-
|
|
17964
|
-
|
|
18492
|
+
// Phase 1: the dependencies. `npm i -g synabun` installed them at the package root (lib/runtime-dependencies.js
|
|
18493
|
+
// checks that Node finds them from both parts); only what is missing is installed, once, at the package root,
|
|
18494
|
+
// and never inside a packaged application. A fresh checkout also needs the TypeScript that builds the MCP server.
|
|
18495
|
+
const report = checkRuntimeDependencies({ packageRoot: PACKAGE_ROOT });
|
|
18496
|
+
const compilerMissing = mcpNeedsBuild && !mcpCompiler(PACKAGE_ROOT);
|
|
18497
|
+
if (report.ok && !compilerMissing) {
|
|
18498
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'skipped', output: 'dependencies already installed' });
|
|
18499
|
+
} else if (PACKAGED_RUNTIME) {
|
|
18500
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'error', output: 'This application is incomplete: reinstall SynaBun.' });
|
|
18501
|
+
send('complete', { ok: false, error: 'The application is incomplete' });
|
|
18502
|
+
return res.end();
|
|
18503
|
+
} else {
|
|
18504
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'start' });
|
|
17965
18505
|
try {
|
|
17966
|
-
|
|
17967
|
-
|
|
17968
|
-
|
|
17969
|
-
|
|
18506
|
+
// The npm that started SynaBun, else the user's own on PATH, else the one next to this Node (npmInvocation).
|
|
18507
|
+
const npm = npmCommand(npmInvocation(), fallbackInstallArgs({ includeDev: mcpNeedsBuild }));
|
|
18508
|
+
const { stdout } = await execFileAsync(npm.file, npm.args, {
|
|
18509
|
+
cwd: PACKAGE_ROOT,
|
|
18510
|
+
timeout: 600_000,
|
|
18511
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
18512
|
+
shell: npm.shell,
|
|
18513
|
+
windowsHide: true,
|
|
18514
|
+
env: withPackagedPathLast(fallbackInstallEnv({ ...process.env, NODE_ENV: 'development' })),
|
|
17970
18515
|
});
|
|
17971
|
-
send('install', { dir: dir.name, status: 'done', output: (stdout
|
|
18516
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'done', output: tail(stdout) });
|
|
17972
18517
|
} catch (err) {
|
|
17973
|
-
send('install', { dir: dir.name, status: 'error', output: (err.stderr || err.message
|
|
17974
|
-
send('complete', { ok: false, error:
|
|
18518
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'error', output: tail(err.stderr || err.message) });
|
|
18519
|
+
send('complete', { ok: false, error: 'npm install failed in the package root' });
|
|
17975
18520
|
return res.end();
|
|
17976
18521
|
}
|
|
17977
18522
|
}
|
|
@@ -17988,13 +18533,18 @@ app.post('/api/setup/build', async (req, res) => {
|
|
|
17988
18533
|
} else {
|
|
17989
18534
|
send('build', { status: 'start' });
|
|
17990
18535
|
try {
|
|
17991
|
-
|
|
18536
|
+
// The TypeScript Node finds from mcp-server/ (the root's devDependency in a fresh checkout), by this Node.
|
|
18537
|
+
const tsc = mcpCompiler(PACKAGE_ROOT);
|
|
18538
|
+
if (!tsc) throw new Error('TypeScript is not installed: npm install in the package root installs it');
|
|
18539
|
+
const { stdout } = await execFileAsync(process.execPath, [tsc, '-p', resolve(PACKAGE_ROOT, 'mcp-server')], {
|
|
17992
18540
|
cwd: resolve(PACKAGE_ROOT, 'mcp-server'),
|
|
17993
|
-
timeout:
|
|
18541
|
+
timeout: 120_000,
|
|
18542
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
18543
|
+
windowsHide: true,
|
|
17994
18544
|
});
|
|
17995
|
-
send('build', { status: 'done', output: (stdout
|
|
18545
|
+
send('build', { status: 'done', output: tail(stdout) });
|
|
17996
18546
|
} catch (err) {
|
|
17997
|
-
send('build', { status: 'error', output: (err.stderr || err.message
|
|
18547
|
+
send('build', { status: 'error', output: tail(err.stderr || err.message) });
|
|
17998
18548
|
send('complete', { ok: false, error: 'MCP server build failed' });
|
|
17999
18549
|
return res.end();
|
|
18000
18550
|
}
|
|
@@ -18562,7 +19112,7 @@ function injectClaudeMemoryPerms() {
|
|
|
18562
19112
|
|
|
18563
19113
|
function injectCodexMemoryPerms() {
|
|
18564
19114
|
try {
|
|
18565
|
-
const configPath = join(
|
|
19115
|
+
const configPath = join(CODEX_DEFAULT_HOME, 'config.toml');
|
|
18566
19116
|
if (!existsSync(configPath)) return { scope: 'none', paths: [] };
|
|
18567
19117
|
let content = readFileSync(configPath, 'utf-8');
|
|
18568
19118
|
if (!tomlHasSection(content, 'mcp_servers.SynaBun')) return { scope: 'none', paths: [] };
|
|
@@ -20074,7 +20624,7 @@ app.post('/api/session-indexing/mirror', async (req, res) => {
|
|
|
20074
20624
|
try {
|
|
20075
20625
|
await mirrorExistingChunks((event) => {
|
|
20076
20626
|
try { broadcastSync(event); } catch {}
|
|
20077
|
-
});
|
|
20627
|
+
}, { ensureCategory: () => ensureFeatureCategory('conversations') });
|
|
20078
20628
|
} catch (err) {
|
|
20079
20629
|
console.error('Mirror error:', err);
|
|
20080
20630
|
try { broadcastSync({ type: 'mirror:error', error: err.message }); } catch {}
|
|
@@ -20213,16 +20763,15 @@ app.post('/api/claude-code/integrations', (req, res) => {
|
|
|
20213
20763
|
writeClaudeSettings(filePath, settings);
|
|
20214
20764
|
}
|
|
20215
20765
|
|
|
20216
|
-
// Save to registered projects list
|
|
20766
|
+
// Save to registered projects list, and create the new project's default category tree (parent + children)
|
|
20767
|
+
// then, once: a project already registered gets nothing, so a default the user deleted stays deleted
|
|
20217
20768
|
const projectLabel = label || basename(normalized);
|
|
20218
20769
|
if (!registered) {
|
|
20219
20770
|
projects.push({ path: normalized, label: projectLabel });
|
|
20220
20771
|
saveHookProjects(projects);
|
|
20772
|
+
try { ensureProjectCategories([{ path: normalized, label: projectLabel }]); } catch {}
|
|
20221
20773
|
}
|
|
20222
20774
|
|
|
20223
|
-
// Create default category tree for the project (parent + children)
|
|
20224
|
-
try { ensureProjectCategories(); } catch {}
|
|
20225
|
-
|
|
20226
20775
|
// Probe for git "dubious ownership" — signals the UI to show an explicit trust prompt.
|
|
20227
20776
|
// Only relevant on Windows when the project dir is owned by a different SID than the
|
|
20228
20777
|
// account running Node (external drives, copied repos, etc.). No-op on Mac/Linux.
|
|
@@ -20729,9 +21278,9 @@ function tomlRemoveAllToolSections(content, prefix) {
|
|
|
20729
21278
|
* - If disabled → remove that section
|
|
20730
21279
|
* Preserves all non-tool config (model, projects, mcp_servers.SynaBun base, notices).
|
|
20731
21280
|
*/
|
|
20732
|
-
function syncCodexToolPermissions(allowedKeys) {
|
|
21281
|
+
function syncCodexToolPermissions(allowedKeys, codexHome = CODEX_DEFAULT_HOME) {
|
|
20733
21282
|
try {
|
|
20734
|
-
const configPath = join(
|
|
21283
|
+
const configPath = join(codexHome, 'config.toml');
|
|
20735
21284
|
if (!existsSync(configPath)) return;
|
|
20736
21285
|
healCodexConfig(configPath);
|
|
20737
21286
|
const health = inspectCodexConfig(configPath);
|
|
@@ -20875,7 +21424,7 @@ app.delete('/api/setup/gemini/mcp', (req, res) => {
|
|
|
20875
21424
|
|
|
20876
21425
|
app.get('/api/setup/codex/mcp', (req, res) => {
|
|
20877
21426
|
try {
|
|
20878
|
-
const configPath = join(
|
|
21427
|
+
const configPath = join(CODEX_DEFAULT_HOME, 'config.toml');
|
|
20879
21428
|
const toml = buildCodexMcpToml();
|
|
20880
21429
|
const health = inspectCodexConfig(configPath);
|
|
20881
21430
|
res.json({ ok: true, ...health, toml });
|
|
@@ -20886,7 +21435,7 @@ app.get('/api/setup/codex/mcp', (req, res) => {
|
|
|
20886
21435
|
|
|
20887
21436
|
app.post('/api/setup/codex/mcp', (req, res) => {
|
|
20888
21437
|
try {
|
|
20889
|
-
const dir =
|
|
21438
|
+
const dir = CODEX_DEFAULT_HOME;
|
|
20890
21439
|
const configPath = join(dir, 'config.toml');
|
|
20891
21440
|
mkdirSync(dir, { recursive: true });
|
|
20892
21441
|
ensureMcpProfileDefault('codex-browser');
|
|
@@ -20901,7 +21450,7 @@ app.post('/api/setup/codex/mcp', (req, res) => {
|
|
|
20901
21450
|
|
|
20902
21451
|
app.delete('/api/setup/codex/mcp', (req, res) => {
|
|
20903
21452
|
try {
|
|
20904
|
-
const configPath = join(
|
|
21453
|
+
const configPath = join(CODEX_DEFAULT_HOME, 'config.toml');
|
|
20905
21454
|
if (!existsSync(configPath)) return res.json({ ok: true, rules: syncRulesWithMcp('codex', 'remove') });
|
|
20906
21455
|
const health = removeCodexMcpConfig(configPath);
|
|
20907
21456
|
res.json({ ok: true, ...health, message: 'SynaBun MCP removed from Codex CLI.', rules: syncRulesWithMcp('codex', 'remove') });
|
|
@@ -20936,7 +21485,7 @@ app.get('/api/setup/status', (req, res) => {
|
|
|
20936
21485
|
// Codex
|
|
20937
21486
|
let codex = { connected: false, valid: false, repairable: false, issues: [] };
|
|
20938
21487
|
try {
|
|
20939
|
-
codex = inspectCodexConfig(join(
|
|
21488
|
+
codex = inspectCodexConfig(join(CODEX_DEFAULT_HOME, 'config.toml'));
|
|
20940
21489
|
} catch {}
|
|
20941
21490
|
codex.cliCommand = buildCodexMcpCliCommand();
|
|
20942
21491
|
|
|
@@ -22211,6 +22760,7 @@ app.get('/api/health', (req, res) => {
|
|
|
22211
22760
|
const count = countMemories();
|
|
22212
22761
|
res.json({
|
|
22213
22762
|
ok: true, storage: 'sqlite', memories: count, projectDir: PACKAGE_ROOT, install: INSTALL_KIND, startLauncher: getStartLauncherState(), entry: START_ENTRY,
|
|
22763
|
+
version: SERVER_VERSION, dataHomeId: DATA_HOME_ID, installEntry: INSTALL_ENTRY,
|
|
22214
22764
|
// Which server this is, and whether it is on its way out for a restart: a
|
|
22215
22765
|
// page showing the restart tells this one from the one that replaces it.
|
|
22216
22766
|
bootId: SERVER_BOOT_ID,
|
|
@@ -22587,14 +23137,15 @@ const SYNABUN_TOOL_GROUPS = {
|
|
|
22587
23137
|
sync: { label: 'Sync', tools: 1, alwaysOn: true },
|
|
22588
23138
|
loop: { label: 'Loop', tools: 1, alwaysOn: true },
|
|
22589
23139
|
profile: { label: 'Profile', tools: 1, alwaysOn: true },
|
|
23140
|
+
choice: { label: 'Choice', tools: 1, alwaysOn: true },
|
|
22590
23141
|
git: { label: 'Git', tools: 1 },
|
|
22591
23142
|
image: { label: 'Images', tools: 1 },
|
|
22592
23143
|
whiteboard: { label: 'Whiteboard', tools: 5 },
|
|
22593
23144
|
card: { label: 'Cards', tools: 5 },
|
|
22594
23145
|
tictactoe: { label: 'TicTacToe', tools: 1 },
|
|
22595
|
-
browser: { label: 'Browser', tools:
|
|
22596
|
-
browser_twitter: { label: 'Twitter/X', tools:
|
|
22597
|
-
browser_facebook: { label: 'Facebook', tools:
|
|
23146
|
+
browser: { label: 'Browser', tools: 21 },
|
|
23147
|
+
browser_twitter: { label: 'Twitter/X', tools: 2 },
|
|
23148
|
+
browser_facebook: { label: 'Facebook', tools: 4 },
|
|
22598
23149
|
browser_tiktok: { label: 'TikTok', tools: 4 },
|
|
22599
23150
|
browser_whatsapp: { label: 'WhatsApp', tools: 2 },
|
|
22600
23151
|
browser_instagram: { label: 'Instagram', tools: 5 },
|
|
@@ -23754,12 +24305,10 @@ function saveCliConfig(config) {
|
|
|
23754
24305
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
23755
24306
|
writeFileSync(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
23756
24307
|
_cliConfigCache = null;
|
|
23757
|
-
//
|
|
23758
|
-
//
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
_codexBinPath = null;
|
|
23762
|
-
_nativeCodexBinPath = null;
|
|
24308
|
+
// A path saved in Settings counts at once, for the version check and every
|
|
24309
|
+
// launcher alike (lib/cli-resolver.js), with no restart.
|
|
24310
|
+
cliResolver.invalidate();
|
|
24311
|
+
_toolVersionCache = { ..._toolVersionCache, checkedAt: null };
|
|
23763
24312
|
}
|
|
23764
24313
|
|
|
23765
24314
|
// Interactive shell tabs start as login shells, like Terminal.app / iTerm2.
|
|
@@ -23781,14 +24330,16 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
23781
24330
|
|
|
23782
24331
|
let cmd = entry.command.trim();
|
|
23783
24332
|
if (!cmd) return SHELL_PROFILE;
|
|
24333
|
+
const found = cliResolver.launcher(profileId);
|
|
24334
|
+
if (!configuredCommandLine(profileId)) cmd = terminalLaunchCommand(found, DEFAULT_SHELL) || cmd;
|
|
23784
24335
|
|
|
23785
24336
|
// Append resume flag for supported CLIs (mutually exclusive with model)
|
|
23786
24337
|
if (opts.resume && profileId === 'claude-code') {
|
|
23787
|
-
cmd = `${cmd} --resume ${opts.resume}`;
|
|
24338
|
+
cmd = `${cmd} --resume ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
23788
24339
|
} else if (opts.resume && profileId === 'codex') {
|
|
23789
|
-
cmd = `${cmd} resume ${opts.resume}`;
|
|
24340
|
+
cmd = `${cmd} resume ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
23790
24341
|
} else if (opts.resume && profileId === 'opencode') {
|
|
23791
|
-
cmd = `${cmd} -s ${opts.resume}`;
|
|
24342
|
+
cmd = `${cmd} -s ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
23792
24343
|
}
|
|
23793
24344
|
// Append model flag for CLIs that support it
|
|
23794
24345
|
else if (opts.model) {
|
|
@@ -23802,7 +24353,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
23802
24353
|
// suffix `[1m]` contains `[ ]`, which zsh parses as a glob character class —
|
|
23803
24354
|
// unquoted it aborts with "no matches found" and the CLI never launches.
|
|
23804
24355
|
// Mirror the IS_WIN-aware quoting used for --mcp-config below.
|
|
23805
|
-
const quotedModel =
|
|
24356
|
+
const quotedModel = terminalArgument(cleanModel, DEFAULT_SHELL);
|
|
23806
24357
|
const modelMap = {
|
|
23807
24358
|
'claude-code': (m) => `${cmd} --model ${m}`,
|
|
23808
24359
|
'codex': (m) => `${cmd} --model ${m}`,
|
|
@@ -23824,6 +24375,11 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
23824
24375
|
cmd = `${cmd} -c model_reasoning_effort="${codexEffort}"`;
|
|
23825
24376
|
}
|
|
23826
24377
|
}
|
|
24378
|
+
// Every Codex terminal: the Windows sandbox the Admin sandbox setting allows. A bare value
|
|
24379
|
+
// (Codex reads one that is not TOML as a string), so no shell can strip a quote from it.
|
|
24380
|
+
if (profileId === 'codex') {
|
|
24381
|
+
cmd = `${cmd} -c ${terminalArgument(`windows.sandbox=${codexWindowsSandbox(adminSandboxOn())}`, DEFAULT_SHELL)}`;
|
|
24382
|
+
}
|
|
23827
24383
|
|
|
23828
24384
|
// Loop-only: bypass permission prompts for unattended Claude Code loops.
|
|
23829
24385
|
// The loop runs in a PTY with no human to answer prompts, so accessing
|
|
@@ -23841,7 +24397,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
23841
24397
|
// collisions invisibly. Trade-off: project-scope .mcp.json servers are not
|
|
23842
24398
|
// loaded inside loops (user-scope servers ARE — they're merged into the file).
|
|
23843
24399
|
if (opts.mcpConfigPath && profileId === 'claude-code') {
|
|
23844
|
-
const quoted =
|
|
24400
|
+
const quoted = terminalArgument(opts.mcpConfigPath, DEFAULT_SHELL);
|
|
23845
24401
|
cmd = `${cmd} --strict-mcp-config --mcp-config ${quoted}`;
|
|
23846
24402
|
}
|
|
23847
24403
|
|
|
@@ -23854,20 +24410,25 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
23854
24410
|
};
|
|
23855
24411
|
}
|
|
23856
24412
|
|
|
24413
|
+
// Read for every spawn, including a tab restored/split by the client and
|
|
24414
|
+
// provider sign-in terminals. The desktop app uses this same server/PTY host.
|
|
24415
|
+
async function freshTerminalPath(base = process.env) {
|
|
24416
|
+
const os = await osPath.refresh({ force: true });
|
|
24417
|
+
const launchers = ['claude-code', 'codex', 'opencode', 'gemini'].map(tool => cliResolver.launcher(tool));
|
|
24418
|
+
// A packaged application's runtime and npm's global bin (rememberPackagedPath above), after the user's own.
|
|
24419
|
+
const { last: synabunDirs, extra: extraDirs } = rememberedPackagedPath();
|
|
24420
|
+
return { ...composeTerminalPath({ env: base, os, launchers, synabunDirs, extraDirs }), os, launchers };
|
|
24421
|
+
}
|
|
24422
|
+
|
|
23857
24423
|
function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
23858
24424
|
if (terminalHost.ready && !terminalHost.ptyAvailable) {
|
|
23859
24425
|
throw new Error('Terminal not available: node-pty failed to load. Run: cd neural-interface && npm run postinstall');
|
|
23860
24426
|
}
|
|
23861
24427
|
const sessionId = opts.sessionId || randomBytes(16).toString('hex');
|
|
23862
|
-
|
|
24428
|
+
let profileCfg = opts.launch || getTerminalProfile(profile, opts);
|
|
23863
24429
|
const initialCols = cols || 120;
|
|
23864
24430
|
const initialRows = rows || 30;
|
|
23865
24431
|
|
|
23866
|
-
// Native-terminal environment: SynaBun's own TERM_PROGRAM identity,
|
|
23867
|
-
// truecolor, a UTF-8 locale, and none of the launching terminal's or IDE's
|
|
23868
|
-
// session variables (VS Code, tmux, iTerm2, a parent Claude Code session…).
|
|
23869
|
-
const env = buildTerminalEnv({ profileEnv: profileCfg.env, extraEnv: opts.extraEnv });
|
|
23870
|
-
|
|
23871
24432
|
const isLoopOwned = !!(opts.extraEnv && opts.extraEnv.SYNABUN_TERMINAL_SESSION === sessionId);
|
|
23872
24433
|
if (isLoopOwned) {
|
|
23873
24434
|
loopLog(sessionId, 'pty:resolve', 'profile resolved', {
|
|
@@ -23886,6 +24447,7 @@ function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
|
23886
24447
|
profile, cwd,
|
|
23887
24448
|
createdAt: Date.now(),
|
|
23888
24449
|
graceTimer: null,
|
|
24450
|
+
onProviderAuthClosed: opts.onClosed,
|
|
23889
24451
|
claudeSessionId: opts.resume || null, // known Claude session ID (from resume or detect)
|
|
23890
24452
|
};
|
|
23891
24453
|
terminalSessions.set(sessionId, session);
|
|
@@ -23942,6 +24504,7 @@ function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
|
23942
24504
|
// The host already delivered the final output (to clients and the tap)
|
|
23943
24505
|
// before reporting the exit.
|
|
23944
24506
|
session.pty.exited = true;
|
|
24507
|
+
opts.onExit?.({ exitCode });
|
|
23945
24508
|
|
|
23946
24509
|
// Clear grace timer if set
|
|
23947
24510
|
if (session.graceTimer) { clearTimeout(session.graceTimer); session.graceTimer = null; }
|
|
@@ -24020,7 +24583,6 @@ function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
|
24020
24583
|
file: profileCfg.shell,
|
|
24021
24584
|
args: profileCfg.args,
|
|
24022
24585
|
cwd: cwd || process.env.USERPROFILE || process.env.HOME || process.cwd(),
|
|
24023
|
-
env,
|
|
24024
24586
|
cols: initialCols,
|
|
24025
24587
|
rows: initialRows,
|
|
24026
24588
|
name: 'xterm-256color',
|
|
@@ -24036,6 +24598,12 @@ function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
|
24036
24598
|
onResized: ({ cols: c, rows: r }) => { session.pty.cols = c; session.pty.rows = r; },
|
|
24037
24599
|
onClients: ({ total }) => { session.clients.size = total; },
|
|
24038
24600
|
onExit,
|
|
24601
|
+
}, async () => {
|
|
24602
|
+
const fresh = await freshTerminalPath(opts.baseEnv || process.env);
|
|
24603
|
+
profileCfg = opts.launch || getTerminalProfile(profile, opts);
|
|
24604
|
+
// On Windows, the Git Bash a claude started in this tab gets (lib/git-bash/): any tab can start one.
|
|
24605
|
+
const env = claudeChildEnv(buildTerminalEnv({ base: fresh.env, profileEnv: profileCfg.env, extraEnv: opts.extraEnv }));
|
|
24606
|
+
return { file: profileCfg.shell, args: profileCfg.args, env };
|
|
24039
24607
|
});
|
|
24040
24608
|
|
|
24041
24609
|
return sessionId;
|
|
@@ -24665,8 +25233,17 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24665
25233
|
providerFactories: {
|
|
24666
25234
|
'claude-code': (state) => {
|
|
24667
25235
|
const config = _readClaudeSkinConfig();
|
|
25236
|
+
// The account decides the credential (lib/claude-accounts.js launchFor()): inherited credentials out,
|
|
25237
|
+
// its directory and, in API-key mode, its key in. Built here, never stored: the loop state file only
|
|
25238
|
+
// names the account. A run whose account is gone does not start under another identity.
|
|
25239
|
+
const launch = _panelClaudeAccounts().launchFor(state.claudeAccountId || 'default');
|
|
25240
|
+
if (!launch.ok) throw new Error(`${launch.message} The run did not start.`);
|
|
24668
25241
|
return createClaudeNativeLoopAdapter({
|
|
24669
25242
|
...state,
|
|
25243
|
+
extraEnv: { ...(state.extraEnv || {}), ...launch.env },
|
|
25244
|
+
unsetEnv: launch.unset,
|
|
25245
|
+
// On Windows, the Git Bash the worker's Claude Code gets (lib/git-bash/), never stored in the loop state.
|
|
25246
|
+
childEnv: claudeChildEnv,
|
|
24670
25247
|
model: state.contextMode === '1m' && state.model
|
|
24671
25248
|
? `${state.model}[1m]`
|
|
24672
25249
|
: state.model && state.model.includes(':') ? toCliModelName(state.model) : state.model,
|
|
@@ -24686,10 +25263,20 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24686
25263
|
// path, so repair the shared config before the SDK can parse it. Without
|
|
24687
25264
|
// this, legacy inline + table SynaBun env definitions surface as a
|
|
24688
25265
|
// configWarning and the unattended turn never becomes active.
|
|
24689
|
-
|
|
25266
|
+
// The account decides the credential (lib/codex-accounts.js launchFor()): inherited OpenAI/Codex
|
|
25267
|
+
// credentials out, its CODEX_HOME in and, in API-key mode, the key SynaBun holds as CODEX_API_KEY
|
|
25268
|
+
// (which `codex exec` prefers over auth.json). Built here, never stored: the loop state file only
|
|
25269
|
+
// names the account. A run whose account is gone does not start under another identity.
|
|
25270
|
+
const launch = _codexAccounts().launchFor(state.codexAccountId || 'default');
|
|
25271
|
+
if (!launch.ok) throw new Error(`${launch.message} The run did not start.`);
|
|
25272
|
+
ensureCanonicalCodexConfig({ strict: true, codexHome: launch.home });
|
|
24690
25273
|
return createCodexNativeLoopAdapter({
|
|
24691
25274
|
...state,
|
|
24692
|
-
|
|
25275
|
+
extraEnv: { ...(state.extraEnv || {}), ...codexExecEnv(launch) },
|
|
25276
|
+
unsetEnv: launch.unset,
|
|
25277
|
+
codexHome: launch.home,
|
|
25278
|
+
// The Admin sandbox setting as the run starts: it keeps that Windows sandbox while it lives.
|
|
25279
|
+
adminSandbox: adminSandboxOn(),
|
|
24693
25280
|
// Native schedules use the same trusted global CLI as the interactive
|
|
24694
25281
|
// sidepanel; SynaBun carries no Codex for the SDK to find by itself.
|
|
24695
25282
|
codexPath: getNativeCodexBin(),
|
|
@@ -24755,7 +25342,8 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24755
25342
|
import('../mcp-server/dist/tools/remember.js'), import('../mcp-server/dist/services/categories.js'),
|
|
24756
25343
|
import('./lib/assistant-catalog.js'), import('./lib/assistant-config.js'), import('./lib/assistant-router.js'),
|
|
24757
25344
|
]);
|
|
24758
|
-
const {
|
|
25345
|
+
const { createModelPricing } = await import('./lib/assistant-budget.js');
|
|
25346
|
+
const { createBudgetPlanner } = await import('./lib/assistant-budget-planner.js');
|
|
24759
25347
|
const { createAssistantClarifier } = await import('./lib/assistant-clarify.js');
|
|
24760
25348
|
const { createAttachmentStore } = await import('./lib/assistant-attachments.js');
|
|
24761
25349
|
const assistantConfigPath = resolve(LOOP_DATA_DIR, 'assistant-config.json');
|
|
@@ -24763,7 +25351,25 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24763
25351
|
// One store for assistant-config.json: routing knobs re-read on change.
|
|
24764
25352
|
const assistantConfigStore = createAssistantConfigStore({ path: assistantConfigPath, log: (tag, message) => assistantLog(tag, message) });
|
|
24765
25353
|
const assistantConfig = assistantConfigStore.read();
|
|
24766
|
-
|
|
25354
|
+
// The one Claude accounts instance (the sidepanel's): one account state for the brain picker, the catalog,
|
|
25355
|
+
// worker dispatch and the panel. `createClaudeAccounts` stays imported for older callers of this block.
|
|
25356
|
+
void createClaudeAccounts;
|
|
25357
|
+
const claudeAccounts = _panelClaudeAccounts();
|
|
25358
|
+
const { createAccountUsage } = await import('./lib/account-usage.js');
|
|
25359
|
+
const accountUsage = createAccountUsage({ accounts: { 'claude-code': claudeAccounts, codex: _codexAccounts() }, readCodex: async (id, opts) => {
|
|
25360
|
+
for (const connection of _codexLiveConnections) {
|
|
25361
|
+
if (opts.signal?.aborted) return null;
|
|
25362
|
+
if (connection.accountId() !== id) continue;
|
|
25363
|
+
try { const value = await connection.readRateLimits(); if (value && connection.accountId() === id) return value; } catch {}
|
|
25364
|
+
}
|
|
25365
|
+
if (opts.signal?.aborted) return null;
|
|
25366
|
+
return codexAccountAuth().rateLimits(id, opts);
|
|
25367
|
+
} });
|
|
25368
|
+
_assistantAccountUsage = accountUsage;
|
|
25369
|
+
claudeAccounts.subscribe(({ accountId }) => accountUsage.invalidate('claude-code', accountId));
|
|
25370
|
+
_codexAccounts().subscribe(({ accountId }) => accountUsage.invalidate('codex', accountId));
|
|
25371
|
+
// A Claude Code worker whose dispatch names no account runs under this one (Assistant → accounts).
|
|
25372
|
+
_assistantClaudeWorkerAccount = () => assistantConfigStore.workers().claudeAccount;
|
|
24767
25373
|
const memoryStoredForRun = (runId) => {
|
|
24768
25374
|
try { return !!assistantGetDb().prepare('SELECT 1 FROM memory_metadata WHERE source_ref = ? LIMIT 1').get(String(runId)); } catch { return false; }
|
|
24769
25375
|
};
|
|
@@ -24797,8 +25403,10 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24797
25403
|
import('../mcp-server/dist/services/typesafe.js'), import('../mcp-server/dist/services/typesafe-config.js'),
|
|
24798
25404
|
import('./lib/assistant-jev.js'),
|
|
24799
25405
|
]);
|
|
25406
|
+
// The budget split's questions (surface budget-split): a dist without them leaves the split on its fixed rule.
|
|
25407
|
+
const budgetJudgments = await import('../mcp-server/dist/services/budget-judgments.js').catch(() => null);
|
|
24800
25408
|
assistantJev = createAssistantJev({
|
|
24801
|
-
judgePrompt, judgeWorkerOutcome,
|
|
25409
|
+
judgePrompt, judgeWorkerOutcome, judgeBudgetSplit: budgetJudgments?.judgeBudgetSplit || null,
|
|
24802
25410
|
surfaceConfig: typesafeKnobs.surfaceConfig,
|
|
24803
25411
|
enabled: typesafeClient.typesafeEnabled,
|
|
24804
25412
|
retryAfterMs: typesafeClient.typesafeRetryAfterMs,
|
|
@@ -24845,7 +25453,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24845
25453
|
const assistantCatalog = createAssistantCatalog({
|
|
24846
25454
|
fetchJson: catalogFetch,
|
|
24847
25455
|
codexPrice: (model) => assistantPricing.codexPrice(model),
|
|
24848
|
-
listAccounts: () =>
|
|
25456
|
+
listAccounts: () => accountUsage.list(),
|
|
24849
25457
|
readFilter: () => assistantConfigStore.routing().catalogFilter || {},
|
|
24850
25458
|
readHidden: () => assistantHiddenModels.all(),
|
|
24851
25459
|
// A hidden alias ("haiku") hides the model it named when it was hidden: each build settles that.
|
|
@@ -24931,6 +25539,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24931
25539
|
accountEnv: () => claudeAccounts.envFor?.('default') || {},
|
|
24932
25540
|
cwd: DATA_HOME,
|
|
24933
25541
|
env: process.env,
|
|
25542
|
+
childEnv: claudeChildEnv,
|
|
24934
25543
|
log: (tag, message) => assistantLog(tag, message),
|
|
24935
25544
|
}),
|
|
24936
25545
|
usage: assistantUsage,
|
|
@@ -24990,6 +25599,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24990
25599
|
accountEnv: () => claudeAccounts.envFor?.('default') || {},
|
|
24991
25600
|
cwd: DATA_HOME,
|
|
24992
25601
|
env: process.env,
|
|
25602
|
+
childEnv: claudeChildEnv,
|
|
24993
25603
|
label: 'greeting writer',
|
|
24994
25604
|
logTag: 'greeting:writer-error',
|
|
24995
25605
|
log: greetingLog,
|
|
@@ -25022,7 +25632,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25022
25632
|
app.use('/api/assistant/hidden-models', createAssistantHiddenModelsRouter({
|
|
25023
25633
|
hiddenModels: assistantHiddenModels, catalog: assistantCatalog, opencodeStore: opencodeHiddenModels, isGuestRequest, broadcastSync,
|
|
25024
25634
|
}));
|
|
25025
|
-
const buildCatalog = (opts = {}) => assistantCatalog.get(opts);
|
|
25635
|
+
const buildCatalog = async (opts = {}) => { await accountUsage.refresh(); return { ...(await assistantCatalog.get(opts)), accounts: accountUsage.list() }; };
|
|
25026
25636
|
// Clarification: a materially ambiguous request gets 1-3 questions before it
|
|
25027
25637
|
// is routed; the user's words and answers become the brief every worker of
|
|
25028
25638
|
// that request receives. Knobs: assistant-config.json `clarify` (read live).
|
|
@@ -25041,10 +25651,41 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25041
25651
|
},
|
|
25042
25652
|
log: (tag, message) => assistantLog(tag, message),
|
|
25043
25653
|
});
|
|
25654
|
+
// The session budget (lib/assistant-budget-planner.js): one setting for every session, divided
|
|
25655
|
+
// across the providers per session by Jev (surface budget-split) or a fixed rule, at the first
|
|
25656
|
+
// route and at checkpoints. The dispatcher, the runtime and the router read it; it reaches them lazily.
|
|
25657
|
+
const assistantBudget = createBudgetPlanner({
|
|
25658
|
+
setting: () => assistantConfigStore.budget(),
|
|
25659
|
+
spendOf: (sessionId) => _assistantDispatcher?.providerSpend?.(sessionId) || { providers: {}, unpricedRuns: 0, liveRuns: [] },
|
|
25660
|
+
brainOf: (sessionId) => _assistantRuntime?.budgetSession?.(sessionId) || null,
|
|
25661
|
+
isOpen: (sessionId) => _assistantRuntime?.budgetSession?.(sessionId)?.open !== false,
|
|
25662
|
+
catalog: () => assistantCatalog.peek?.() || null,
|
|
25663
|
+
refreshCatalog: () => assistantCatalog.full(),
|
|
25664
|
+
accounts: () => accountUsage.list(),
|
|
25665
|
+
routing: () => assistantConfigStore.routing(),
|
|
25666
|
+
routeMode: (sessionId) => { try { return assistantRouter.modeFor(sessionId); } catch { return null; } },
|
|
25667
|
+
workers: () => assistantConfigStore.workers(),
|
|
25668
|
+
pricing: assistantPricing,
|
|
25669
|
+
// This session's spend by provider from the usage ledger (every model row it booked).
|
|
25670
|
+
recentSpend: (sessionId) => {
|
|
25671
|
+
try {
|
|
25672
|
+
const out = {};
|
|
25673
|
+
for (const row of assistantUsage.sessionView(sessionId)?.models || []) if (row?.provider) out[row.provider] = (out[row.provider] || 0) + (Number(row.costUsd) || 0);
|
|
25674
|
+
return out;
|
|
25675
|
+
} catch { return null; }
|
|
25676
|
+
},
|
|
25677
|
+
judge: assistantJev,
|
|
25678
|
+
emit: (sessionId, view, meta) => { try { _assistantRuntime?.onBudgetSplit?.(sessionId, view, meta); } catch {} },
|
|
25679
|
+
statePath: resolve(LOOP_DATA_DIR, 'assistant-budget-splits.json'),
|
|
25680
|
+
log: (tag, message) => assistantLog(tag, message),
|
|
25681
|
+
});
|
|
25044
25682
|
// DeepSeek-Harness-style routing: the brain proposes, the router enforces the
|
|
25045
25683
|
// session's route mode. Sinks reach the runtime/dispatcher lazily.
|
|
25046
25684
|
const assistantRouter = createAssistantRouter({
|
|
25685
|
+
accountUsage,
|
|
25686
|
+
budget: assistantBudget,
|
|
25047
25687
|
catalog: assistantCatalog,
|
|
25688
|
+
listAccounts: () => accountUsage.list(),
|
|
25048
25689
|
configStore: assistantConfigStore,
|
|
25049
25690
|
getSession: (id) => _assistantRuntime?.routerSession?.(id) || null,
|
|
25050
25691
|
sinks: {
|
|
@@ -25124,20 +25765,25 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25124
25765
|
assistantDesktop = null;
|
|
25125
25766
|
}
|
|
25126
25767
|
_assistantDispatcher = createAssistantDispatcher({
|
|
25768
|
+
accountUsage,
|
|
25127
25769
|
getRuntime: () => _nativeLoopRuntime,
|
|
25128
25770
|
dataDir: LOOP_DATA_DIR,
|
|
25129
25771
|
loopDir: LOOP_DIR,
|
|
25130
25772
|
broadcastSync: (message) => { broadcastSync(message); try { _assistantRuntime?.observeBroadcast?.(message); } catch {} },
|
|
25131
25773
|
log: (runId, tag, message, meta) => { try { loopLog(runId, tag, message, meta); } catch {} },
|
|
25132
25774
|
PACKAGE_ROOT,
|
|
25133
|
-
findCodexAccount, getCodexAccount, CODEX_DEFAULT_HOME, claudeAccounts,
|
|
25775
|
+
findCodexAccount, getCodexAccount, CODEX_DEFAULT_HOME, claudeAccounts, codexAccounts: _codexAccounts(),
|
|
25776
|
+
claudeWorkerAccount: () => assistantConfigStore.workers().claudeAccount,
|
|
25777
|
+
codexWorkerAccount: () => assistantConfigStore.workers().codexAccount,
|
|
25134
25778
|
isValidMcpProfileValue, normalizeMcpProfileName, readActiveMcpProfile,
|
|
25135
25779
|
acquireLoopBrowserAndTab, releaseSharedBrowserTab, reconcileKeepAwake,
|
|
25136
25780
|
memory: assistantMemory, detectProject: assistantDetectProject,
|
|
25137
|
-
// Limits
|
|
25781
|
+
// Limits are read live from assistant-config.json `limits`; the money is the session budget (the planner).
|
|
25138
25782
|
limits: {},
|
|
25139
|
-
readLimits: () => ({ ...(assistantConfigStore.read().limits || {})
|
|
25783
|
+
readLimits: () => ({ ...(assistantConfigStore.read().limits || {}) }),
|
|
25784
|
+
budget: assistantBudget,
|
|
25140
25785
|
brainSpend: (sessionId) => _assistantRuntime?.brainSpend?.(sessionId) || 0,
|
|
25786
|
+
brainSpendByProvider: (sessionId) => _assistantRuntime?.brainSpendByProvider?.(sessionId) || null,
|
|
25141
25787
|
currentTask: (sessionId) => _assistantRuntime?.currentTask?.(sessionId) || null,
|
|
25142
25788
|
pricing: assistantPricing,
|
|
25143
25789
|
usage: assistantUsage,
|
|
@@ -25148,9 +25794,11 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25148
25794
|
testDensity: (sessionId) => _assistantRuntime?.testDensityFor?.(sessionId) ?? null,
|
|
25149
25795
|
});
|
|
25150
25796
|
_assistantRuntime = createAssistantRuntime({
|
|
25797
|
+
accountUsage,
|
|
25151
25798
|
dispatcher: _assistantDispatcher,
|
|
25152
25799
|
phoneAuthority: _phoneAuthority.verify, // the verifier only
|
|
25153
|
-
claudeAccounts,
|
|
25800
|
+
claudeAccounts, codexAccounts: _codexAccounts(),
|
|
25801
|
+
codexHomeForAccount: (accountId) => _codexAccounts().homeFor(accountId),
|
|
25154
25802
|
ClaudeSession: claudeBridgeModule.ClaudeSession,
|
|
25155
25803
|
sdkVersion: claudeBridgeModule.SDK_VERSION,
|
|
25156
25804
|
handleCodexSkinWebSocket, ensureIsolatedServe, stopIsolatedServe, setupOpencodeSidepanelConfig,
|
|
@@ -25167,11 +25815,14 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25167
25815
|
gateUrl: `http://127.0.0.1:${PORT}/api/assistant/route-gate/check`,
|
|
25168
25816
|
codexBin: () => getCodexBin(),
|
|
25169
25817
|
pricing: assistantPricing,
|
|
25818
|
+
budget: assistantBudget,
|
|
25170
25819
|
usage: assistantUsage,
|
|
25171
25820
|
clarifier: assistantClarifier,
|
|
25172
25821
|
userContext: assistantUserContext,
|
|
25173
25822
|
// The greeting a conversation opened with (told to the brain once) and when the person was last here.
|
|
25174
25823
|
greeting: _greeting,
|
|
25824
|
+
// The Admin sandbox setting: every session view shows it (features.adminSandbox gates the panel's control).
|
|
25825
|
+
adminSandbox: _agentSandbox,
|
|
25175
25826
|
});
|
|
25176
25827
|
assistantDesktop?.attach?.({ runtime: _assistantRuntime, dispatcher: _assistantDispatcher });
|
|
25177
25828
|
// Warm the catalog so brain capabilities (vision, tier) are known on first attach,
|
|
@@ -25189,39 +25840,30 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25189
25840
|
if (unverified && attempt < 3) setTimeout(() => { repairRouteEfforts(attempt + 1).catch(() => {}); }, 60_000).unref?.();
|
|
25190
25841
|
};
|
|
25191
25842
|
repairRouteEfforts().catch(() => {});
|
|
25192
|
-
|
|
25193
|
-
|
|
25194
|
-
|
|
25195
|
-
|
|
25196
|
-
|
|
25197
|
-
if (!acct) throw Object.assign(new Error('Unknown Codex account'), { code: 'ACCOUNT_NOT_FOUND', status: 404 });
|
|
25198
|
-
const next = String(label || '').trim().slice(0, 60);
|
|
25199
|
-
if (next) acct.label = next;
|
|
25200
|
-
saveCodexAccounts(data);
|
|
25201
|
-
return acct;
|
|
25202
|
-
},
|
|
25203
|
-
remove: (id, { inUse = () => null } = {}) => {
|
|
25204
|
-
if (id === 'default') throw Object.assign(new Error('The Default account cannot be removed'), { code: 'DEFAULT_ACCOUNT', status: 409 });
|
|
25205
|
-
const data = loadCodexAccounts();
|
|
25206
|
-
const idx = data.accounts.findIndex((a) => a.id === id);
|
|
25207
|
-
if (idx === -1) throw Object.assign(new Error('Unknown Codex account'), { code: 'ACCOUNT_NOT_FOUND', status: 404 });
|
|
25208
|
-
const reason = inUse(id);
|
|
25209
|
-
if (reason) throw Object.assign(new Error(reason), { code: 'ACCOUNT_IN_USE', status: 409 });
|
|
25210
|
-
const [removed] = data.accounts.splice(idx, 1);
|
|
25211
|
-
saveCodexAccounts(data);
|
|
25212
|
-
try {
|
|
25213
|
-
if (removed.home && removed.home !== CODEX_DEFAULT_HOME && removed.home.startsWith(CODEX_ACCOUNTS_ROOT)) rmSync(removed.home, { recursive: true, force: true });
|
|
25214
|
-
} catch (e) { console.warn('[codex-accounts] Failed to delete home:', e.message); }
|
|
25215
|
-
return { removed: id };
|
|
25216
|
-
},
|
|
25217
|
-
};
|
|
25843
|
+
// The one Codex accounts instance (the sidepanel's, lib/codex-accounts.js): the brain picker, the
|
|
25844
|
+
// catalog, worker dispatch and the panel show one account state.
|
|
25845
|
+
const codexAccounts = _codexAccounts();
|
|
25846
|
+
// A Codex worker whose dispatch names no account runs under this one (Assistant → accounts).
|
|
25847
|
+
_assistantCodexWorkerAccount = () => assistantConfigStore.workers().codexAccount;
|
|
25218
25848
|
const createLoginTerminal = async ({ env = {}, cwd = null, profile = 'claude-code' } = {}) => {
|
|
25219
25849
|
const terminalSessionId = createTerminalSession(profile, 120, 30, cwd || getHomePath(), { extraEnv: env });
|
|
25220
25850
|
broadcastSync({ type: 'terminal:session-created', sessionId: terminalSessionId, profile });
|
|
25221
25851
|
return { terminalSessionId, profile };
|
|
25222
25852
|
};
|
|
25223
25853
|
app.use('/api/assistant', createAssistantApi({
|
|
25854
|
+
accountUsage,
|
|
25224
25855
|
dispatcher: _assistantDispatcher, runtime: _assistantRuntime, claudeAccounts, codexAccounts, buildCatalog, createLoginTerminal,
|
|
25856
|
+
// Claude accounts are the sidepanel's: sign-in is Claude Code's own `auth login` in a SynaBun terminal
|
|
25857
|
+
// under the account's config directory, a mode change ends the account's idle sessions first, and an
|
|
25858
|
+
// account a panel tab, the brain, a run or the worker choice uses cannot be removed.
|
|
25859
|
+
claudeSignIn: (accountId) => providerPanelTerminalAuth.start('claude', accountId),
|
|
25860
|
+
claudeSetMode: (accountId, mode) => panelClaudeAuth().setMode(accountId, mode),
|
|
25861
|
+
claudeAccountInUse,
|
|
25862
|
+
// Codex accounts are the sidepanel's too: sign-in is `codex login` in a SynaBun terminal under the
|
|
25863
|
+
// account's CODEX_HOME, a mode change restarts its idle panel processes, and an account something uses stays.
|
|
25864
|
+
codexSignIn: (accountId) => providerPanelTerminalAuth.start('codex', accountId),
|
|
25865
|
+
codexSetMode: (accountId, mode) => codexAccountAuth().setMode(accountId, mode),
|
|
25866
|
+
codexAccountInUse: (accountId) => codexAccountInUse(accountId),
|
|
25225
25867
|
broadcastSync, isGuestRequest, log: (tag, message) => assistantLog(tag, message),
|
|
25226
25868
|
assistantRouter, catalog: assistantCatalog, configStore: assistantConfigStore, taskClasses: TASK_CLASS_META,
|
|
25227
25869
|
pricing: assistantPricing,
|
|
@@ -25233,6 +25875,12 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25233
25875
|
userContext: assistantUserContext,
|
|
25234
25876
|
userContextError: () => assistantUserContextError,
|
|
25235
25877
|
defaultProject: () => assistantDetectProject(assistantConfigStore.read().defaultBrain?.cwd || PACKAGE_ROOT) || 'global',
|
|
25878
|
+
// whatsapp_send_file (POST /api/assistant/whatsapp/send-file): the WhatsApp service, built after this
|
|
25879
|
+
// router (startWhatsAppLink), and the port for that route's localhost Host check.
|
|
25880
|
+
whatsapp: () => _whatsapp,
|
|
25881
|
+
port: () => Number(PORT),
|
|
25882
|
+
// GET / PUT /api/assistant/admin-sandbox (the PUT answers UI_ONLY to agents).
|
|
25883
|
+
adminSandbox: _agentSandbox,
|
|
25236
25884
|
}));
|
|
25237
25885
|
const shutdownAssistant = () => {
|
|
25238
25886
|
try { assistantRouter.shutdown(); } catch {}
|
|
@@ -25241,6 +25889,8 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25241
25889
|
try { _assistantRuntime?.shutdown?.(); } catch {}
|
|
25242
25890
|
try { _assistantDispatcher?.shutdown?.(); } catch {}
|
|
25243
25891
|
try { _greeting?.shutdown?.(); } catch {}
|
|
25892
|
+
// The sessions' budget splits, written now (the planner debounces its writes).
|
|
25893
|
+
try { assistantBudget.flush(); } catch {}
|
|
25244
25894
|
// Usage rows whose append failed get their last try (synchronous: an 'exit' handler runs it too).
|
|
25245
25895
|
try { assistantUsage.flush(); } catch {}
|
|
25246
25896
|
};
|
|
@@ -25472,6 +26122,7 @@ function buildExecMcpEnvOverrides(loopState) {
|
|
|
25472
26122
|
function prepareExecWrapperFile(terminalSessionId, loopState, taskFile, sentinel) {
|
|
25473
26123
|
const config = loadCliConfig();
|
|
25474
26124
|
const profile = loopState.profile;
|
|
26125
|
+
if (profile === 'codex') ensureCanonicalCodexConfig({ strict: true });
|
|
25475
26126
|
const mcpEnvOverrides = buildExecMcpEnvOverrides(loopState);
|
|
25476
26127
|
|
|
25477
26128
|
// Surface the exact pins being injected per exec iteration so a stripped/missing
|
|
@@ -25492,6 +26143,8 @@ function prepareExecWrapperFile(terminalSessionId, loopState, taskFile, sentinel
|
|
|
25492
26143
|
model: loopState.model,
|
|
25493
26144
|
effort: loopState.effort,
|
|
25494
26145
|
mcpEnvOverrides,
|
|
26146
|
+
// The Admin sandbox setting the loop started with (a loop file from before it: the current one).
|
|
26147
|
+
codexConfigArgs: codexWindowsSandboxArgs(typeof loopState.adminSandbox === 'boolean' ? loopState.adminSandbox : adminSandboxOn()),
|
|
25495
26148
|
});
|
|
25496
26149
|
const wrapperFile = join(
|
|
25497
26150
|
os.tmpdir(),
|
|
@@ -26212,6 +26865,39 @@ function handleSessionMonitorWebSocket(ws) {
|
|
|
26212
26865
|
ws.on('error', () => sessionMonitorClients.delete(ws));
|
|
26213
26866
|
}
|
|
26214
26867
|
|
|
26868
|
+
const providerPanelTerminalAuth = createProviderPanelTerminalAuth({
|
|
26869
|
+
resolveSpec: (provider, accountId) => providerLoginSpec({
|
|
26870
|
+
provider, accountId, claudeAccounts: _panelClaudeAccounts(), codexAccount: findCodexAccount,
|
|
26871
|
+
claudeExecutable: getClaudeSdkExecutable, codexExecutable: getCodexBin,
|
|
26872
|
+
env: withPathEnv(process.env, getAugmentedPath()),
|
|
26873
|
+
codexConfigArgs: codexSandboxArgs(),
|
|
26874
|
+
}),
|
|
26875
|
+
createSession: createTerminalSession,
|
|
26876
|
+
whenSpawned: id => terminalHost.whenSpawned(id),
|
|
26877
|
+
killSession: id => { try { terminalSessions.get(id)?.pty.kill(); } catch {} },
|
|
26878
|
+
// A sign-in that ended: ask the CLI what the account now holds. A subscription it confirms is what the
|
|
26879
|
+
// account uses from now on (every window hears it through claude:accounts-changed / codex:accounts-changed).
|
|
26880
|
+
// A Codex app-server reads its login only when it starts: the account's idle ones restart.
|
|
26881
|
+
onSettled: async (provider, accountId, status) => {
|
|
26882
|
+
if (provider === 'codex') {
|
|
26883
|
+
try {
|
|
26884
|
+
const accounts = _codexAccounts();
|
|
26885
|
+
await accounts.refreshStatus([accountId], { force: true });
|
|
26886
|
+
if (status === 'exited' && accounts.describe(accountId)?.subscription?.signedIn === true) accounts.noteSubscriptionSignIn(accountId);
|
|
26887
|
+
codexAccountStoreChanged(accountId);
|
|
26888
|
+
} catch {}
|
|
26889
|
+
return;
|
|
26890
|
+
}
|
|
26891
|
+
if (provider !== 'claude') return;
|
|
26892
|
+
try {
|
|
26893
|
+
const accounts = _panelClaudeAccounts();
|
|
26894
|
+
await accounts.refreshStatus([accountId], { force: true });
|
|
26895
|
+
if (status === 'exited' && accounts.describe(accountId)?.subscription?.signedIn === true) accounts.noteSubscriptionSignIn(accountId);
|
|
26896
|
+
} catch {}
|
|
26897
|
+
},
|
|
26898
|
+
});
|
|
26899
|
+
registerProviderPanelTerminalAuthRoutes(app, { guard: panelAuthOwnerGuard, auth: providerPanelTerminalAuth });
|
|
26900
|
+
|
|
26215
26901
|
// ── Terminal REST endpoints ──
|
|
26216
26902
|
|
|
26217
26903
|
app.get('/api/terminal/sessions', (req, res) => {
|
|
@@ -26259,7 +26945,8 @@ app.post('/api/terminal/sessions', async (req, res) => {
|
|
|
26259
26945
|
} catch (err) {
|
|
26260
26946
|
let msg = err.message;
|
|
26261
26947
|
if (process.platform !== 'win32' && (msg.includes('posix_spawn') || msg.includes('EACCES'))) {
|
|
26262
|
-
|
|
26948
|
+
const ptyDir = findInstalled(__dirname, 'node-pty', { stopAt: PACKAGE_ROOT })?.path || join(__dirname, 'node_modules', 'node-pty');
|
|
26949
|
+
msg = `Terminal failed: spawn-helper lacks execute permission. Try: chmod +x "${join(ptyDir, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper')}"`;
|
|
26263
26950
|
}
|
|
26264
26951
|
return res.status(500).json({ error: msg });
|
|
26265
26952
|
}
|
|
@@ -26272,6 +26959,7 @@ app.delete('/api/terminal/sessions/:id', (req, res) => {
|
|
|
26272
26959
|
const session = terminalSessions.get(req.params.id);
|
|
26273
26960
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
26274
26961
|
const wasClaudeCode = session.profile === 'claude-code';
|
|
26962
|
+
session.onProviderAuthClosed?.();
|
|
26275
26963
|
session.pty.kill();
|
|
26276
26964
|
// Clean up temp files from image paste
|
|
26277
26965
|
if (session.tempFiles) {
|
|
@@ -26284,7 +26972,7 @@ app.delete('/api/terminal/sessions/:id', (req, res) => {
|
|
|
26284
26972
|
});
|
|
26285
26973
|
|
|
26286
26974
|
app.get('/api/terminal/profiles', (req, res) => {
|
|
26287
|
-
const profileIds = ['claude-code', 'codex', 'gemini', 'shell'];
|
|
26975
|
+
const profileIds = ['claude-code', 'codex', 'gemini', 'opencode', 'shell'];
|
|
26288
26976
|
const profiles = profileIds.map(id => {
|
|
26289
26977
|
const p = getTerminalProfile(id);
|
|
26290
26978
|
return { id, shell: p.shell, args: p.args };
|
|
@@ -26343,8 +27031,10 @@ let _gitResolved = false;
|
|
|
26343
27031
|
|
|
26344
27032
|
// Resolve git's absolute path at first use. On Windows, `where git` lists all matches;
|
|
26345
27033
|
// prefer .exe so spawnSync works without cmd.exe quoting issues. On Unix, use `command -v`.
|
|
27034
|
+
// A Windows PC without git: the git.exe of SynaBun's copy of Git for Windows (lib/git-bash/), asked each time,
|
|
27035
|
+
// since a copy that is still being fetched at the first call arrives later.
|
|
26346
27036
|
function resolveGitPath() {
|
|
26347
|
-
if (_gitResolved) return _gitExePath;
|
|
27037
|
+
if (_gitResolved) return _gitExePath || gitBash.gitExecutable();
|
|
26348
27038
|
_gitResolved = true;
|
|
26349
27039
|
try {
|
|
26350
27040
|
const cmd = IS_WINDOWS ? 'where git' : 'command -v git';
|
|
@@ -26361,7 +27051,7 @@ function resolveGitPath() {
|
|
|
26361
27051
|
console.warn('[git] resolution failed:', err?.message || String(err));
|
|
26362
27052
|
_gitExePath = null;
|
|
26363
27053
|
}
|
|
26364
|
-
return _gitExePath;
|
|
27054
|
+
return _gitExePath || gitBash.gitExecutable();
|
|
26365
27055
|
}
|
|
26366
27056
|
|
|
26367
27057
|
// Cross-platform git invocation. Uses resolved absolute path when possible (spawnable
|
|
@@ -26820,16 +27510,16 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
|
|
|
26820
27510
|
for (const f of files) {
|
|
26821
27511
|
if (typeof f !== 'string' || f.includes('..')) return res.status(400).json({ error: 'Invalid file path' });
|
|
26822
27512
|
}
|
|
26823
|
-
const result = spawnSync('git', ['add', '--', ...files], { cwd: dir, stdio: 'pipe', timeout: 10000 });
|
|
27513
|
+
const result = spawnSync('git', ['add', '--', ...files], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 10000 });
|
|
26824
27514
|
if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
|
|
26825
27515
|
} else {
|
|
26826
27516
|
// Stage all changes
|
|
26827
|
-
const result = spawnSync('git', ['add', '-A'], { cwd: dir, stdio: 'pipe', timeout: 10000 });
|
|
27517
|
+
const result = spawnSync('git', ['add', '-A'], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 10000 });
|
|
26828
27518
|
if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
|
|
26829
27519
|
}
|
|
26830
27520
|
|
|
26831
27521
|
// Commit
|
|
26832
|
-
const result = spawnSync('git', ['commit', '-m', message.trim()], { cwd: dir, stdio: 'pipe', timeout: 15000 });
|
|
27522
|
+
const result = spawnSync('git', ['commit', '-m', message.trim()], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 15000 });
|
|
26833
27523
|
if (result.status !== 0) {
|
|
26834
27524
|
const stderr = result.stderr?.toString?.()?.trim() || '';
|
|
26835
27525
|
const stdout = result.stdout?.toString?.()?.trim() || '';
|
|
@@ -26985,14 +27675,10 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
|
|
|
26985
27675
|
return res.json({ ok: true, message: '', source: 'heuristic', summary: 'No changes to commit' });
|
|
26986
27676
|
}
|
|
26987
27677
|
|
|
26988
|
-
// Try Claude CLI
|
|
26989
|
-
|
|
26990
|
-
try {
|
|
26991
|
-
execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { stdio: 'pipe', timeout: 3000 });
|
|
26992
|
-
claudeAvailable = true;
|
|
26993
|
-
} catch {}
|
|
27678
|
+
// Try Claude CLI: the one the panels use (lib/cli-resolver.js), started without a shell.
|
|
27679
|
+
const claudeLaunch = cliResolver.launcher('claude-code');
|
|
26994
27680
|
|
|
26995
|
-
if (
|
|
27681
|
+
if (claudeLaunch.spec && claudeLaunch.state !== 'broken') {
|
|
26996
27682
|
try {
|
|
26997
27683
|
const prompt = [
|
|
26998
27684
|
'Generate a single-line git commit message for these changes.',
|
|
@@ -27007,14 +27693,16 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
|
|
|
27007
27693
|
untrackedList ? '\n## New untracked files\n' + untrackedList.split('\n').slice(0, 20).join('\n') : '',
|
|
27008
27694
|
].join('\n');
|
|
27009
27695
|
|
|
27010
|
-
const
|
|
27011
|
-
const result =
|
|
27696
|
+
const launch = spawnArgsFor(claudeLaunch.spec, ['-p', '--model', 'sonnet']);
|
|
27697
|
+
const result = spawnSync(launch.command, launch.args, {
|
|
27698
|
+
...launch.options,
|
|
27012
27699
|
input: prompt,
|
|
27013
27700
|
cwd: dir,
|
|
27014
27701
|
timeout: 30000,
|
|
27015
27702
|
maxBuffer: 64 * 1024,
|
|
27016
27703
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27017
|
-
|
|
27704
|
+
env: withPathEnv(process.env, getAugmentedPath()),
|
|
27705
|
+
windowsHide: true,
|
|
27018
27706
|
});
|
|
27019
27707
|
|
|
27020
27708
|
const output = (result.stdout || '').toString().trim();
|
|
@@ -27104,27 +27792,25 @@ app.put('/api/cli/config', (req, res) => {
|
|
|
27104
27792
|
}
|
|
27105
27793
|
});
|
|
27106
27794
|
|
|
27107
|
-
|
|
27795
|
+
// Settings > Terminal > Detect: what is installed, whatever Settings says now
|
|
27796
|
+
// (the button fills the field with it), found the way every launcher finds it
|
|
27797
|
+
// (lib/cli-resolver.js), with the OS PATH read afresh.
|
|
27798
|
+
app.post('/api/cli/detect/:profileId', async (req, res) => {
|
|
27108
27799
|
try {
|
|
27109
27800
|
const { profileId } = req.params;
|
|
27110
|
-
|
|
27111
|
-
const
|
|
27112
|
-
|
|
27113
|
-
|
|
27114
|
-
|
|
27115
|
-
|
|
27116
|
-
|
|
27117
|
-
|
|
27118
|
-
|
|
27119
|
-
|
|
27120
|
-
|
|
27121
|
-
|
|
27122
|
-
|
|
27123
|
-
const foundPath = result.split(/\r?\n/)[0].trim();
|
|
27124
|
-
res.json({ ok: true, found: true, path: foundPath });
|
|
27125
|
-
} catch {
|
|
27126
|
-
res.json({ ok: true, found: false, path: null });
|
|
27127
|
-
}
|
|
27801
|
+
if (!['claude-code', 'codex', 'gemini', 'opencode'].includes(profileId)) return res.status(400).json({ ok: false, error: 'Unknown profile' });
|
|
27802
|
+
const status = await cliResolver.resolve(profileId, { force: true, configured: null });
|
|
27803
|
+
res.json({
|
|
27804
|
+
ok: true,
|
|
27805
|
+
found: status.state !== 'missing',
|
|
27806
|
+
path: status.path,
|
|
27807
|
+
version: status.version,
|
|
27808
|
+
state: status.state,
|
|
27809
|
+
foundBy: status.foundBy,
|
|
27810
|
+
pathStatus: cliPathManager.inspect(status),
|
|
27811
|
+
installSource: status.installSource,
|
|
27812
|
+
error: status.error,
|
|
27813
|
+
});
|
|
27128
27814
|
} catch (err) {
|
|
27129
27815
|
res.status(500).json({ ok: false, error: err.message });
|
|
27130
27816
|
}
|
|
@@ -27297,9 +27983,15 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
27297
27983
|
const command = (cliConfig[profile]?.command || '').trim();
|
|
27298
27984
|
if (!command) throw new Error(`No CLI command configured for ${profile}`);
|
|
27299
27985
|
|
|
27300
|
-
|
|
27301
|
-
|
|
27302
|
-
|
|
27986
|
+
// Named by its plain name in Settings: the CLI the panels use (lib/cli-resolver.js), a program started
|
|
27987
|
+
// without a shell; otherwise the Settings value as written.
|
|
27988
|
+
const found = CLI_DEFAULTS[profile] && command === CLI_DEFAULTS[profile].command ? cliResolver.launcher(profile) : null;
|
|
27989
|
+
const direct = found?.spec?.kind === 'direct' ? found.launchPath : null;
|
|
27990
|
+
let spawnCmd = direct || command;
|
|
27991
|
+
// Codex: the Windows sandbox the Admin sandbox setting allows, before its flag (a bare value: no quote for a shell to lose).
|
|
27992
|
+
const leadArgs = profile === 'codex' ? ['-c', `windows.sandbox=${codexWindowsSandbox(adminSandboxOn())}`] : [];
|
|
27993
|
+
let spawnArgs = [...leadArgs, ...sidecarConfig.args];
|
|
27994
|
+
let useShell = IS_WIN && !direct;
|
|
27303
27995
|
let tmpFile = null;
|
|
27304
27996
|
|
|
27305
27997
|
if (sidecarConfig.stdin) {
|
|
@@ -27310,10 +28002,11 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
27310
28002
|
// Use PowerShell which correctly passes $msg as a single argument value.
|
|
27311
28003
|
tmpFile = join(os.tmpdir(), `synabun-relay-${randomBytes(4).toString('hex')}.txt`);
|
|
27312
28004
|
writeFileSync(tmpFile, message, 'utf-8');
|
|
27313
|
-
const flag =
|
|
28005
|
+
const flag = sidecarConfig.args[0]; // e.g. '-p', '-q'
|
|
28006
|
+
const lead = leadArgs.map((word) => ` '${word}'`).join('');
|
|
27314
28007
|
spawnCmd = 'powershell.exe';
|
|
27315
28008
|
spawnArgs = ['-NoProfile', '-NonInteractive', '-Command',
|
|
27316
|
-
`$msg = Get-Content -Raw '${tmpFile}'; & '${command}' ${flag} $msg`];
|
|
28009
|
+
`$msg = Get-Content -Raw '${tmpFile}'; & '${String(direct || command).replace(/'/g, "''")}'${lead} ${flag} $msg`];
|
|
27317
28010
|
useShell = false;
|
|
27318
28011
|
} else {
|
|
27319
28012
|
// Unix: pass directly as the flag's value arg (no shell quoting issues)
|
|
@@ -27324,7 +28017,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
27324
28017
|
|
|
27325
28018
|
const child = spawn(spawnCmd, spawnArgs, {
|
|
27326
28019
|
cwd: session.cwd || process.cwd(),
|
|
27327
|
-
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
28020
|
+
env: { ...withPathEnv(process.env, getAugmentedPath()), FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
27328
28021
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27329
28022
|
shell: useShell,
|
|
27330
28023
|
timeout: LINK_SIDECAR_TIMEOUT_MS,
|
|
@@ -32868,6 +33561,17 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
32868
33561
|
// first session runs on it (checkClaudeCliSkew in lib/claude-model-catalog.js).
|
|
32869
33562
|
setTimeout(() => { checkClaudeCliSkew(getClaudeBin()).catch(() => {}); }, 1000);
|
|
32870
33563
|
|
|
33564
|
+
// Windows: the Git Bash every Claude Code gets (lib/git-bash/). Looked for now (the settings of the user's own
|
|
33565
|
+
// claude follow it); a PC with none, and an app that carries none, gets SynaBun's copy, fetched in the background.
|
|
33566
|
+
setTimeout(() => {
|
|
33567
|
+
try {
|
|
33568
|
+
gitBash.refresh();
|
|
33569
|
+
if (gitBash.status().state === 'missing') gitBash.ensure({ reason: 'first run' });
|
|
33570
|
+
// What an earlier run left in the data home (a stopped attempt's work folder, an older version), off the event loop.
|
|
33571
|
+
gitBash.housekeep().catch(() => {});
|
|
33572
|
+
} catch (err) { console.warn('[git-bash] start check failed:', err?.message || err); }
|
|
33573
|
+
}, 2500);
|
|
33574
|
+
|
|
32871
33575
|
// Warm the session_cache + session_fts tables so the hybrid search has data
|
|
32872
33576
|
// on first keystroke. Each pass is synchronous and holds the main thread, so
|
|
32873
33577
|
// they run one per tick (requests are answered in between), and each only
|
|
@@ -33815,6 +34519,7 @@ process.on('exit', () => {
|
|
|
33815
34519
|
try { terminalHost.killNow(); } catch {}
|
|
33816
34520
|
try { _whatsapp?.killNow(); } catch {}
|
|
33817
34521
|
try { tunnel.killNow(); } catch {}
|
|
34522
|
+
try { gitBash.cancel(); } catch {}
|
|
33818
34523
|
try { stopOpencodeServer(); } catch {}
|
|
33819
34524
|
try { editorCompletionServe.stop('exit'); } catch {}
|
|
33820
34525
|
try { _claudeBridge?.shutdownAllBridges?.(); } catch {}
|