synabun 2.0.7 → 2.0.10
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 +67 -0
- package/README.md +1 -1
- package/THIRD-PARTY-LICENSES.md +46 -4
- package/hooks/claude-code/post-plan.mjs +6 -30
- package/hooks/claude-code/session-start.mjs +1 -4
- package/hooks/claude-code/shared.mjs +106 -72
- package/launcher.mjs +9 -3
- package/lib/package-processes.js +670 -0
- package/lib/runtime-dependencies.js +225 -0
- package/lib/start-launcher.js +6 -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/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/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/tools/agents.d.ts +1 -1
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +1 -1
- 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 +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.js +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -1
- 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 +16 -4
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/package-lock.json +3 -3
- package/mcp-server/package.json +3 -3
- package/mcp-server/src/services/browser-bench.ts +1 -1
- package/mcp-server/src/services/categories.ts +77 -32
- package/mcp-server/src/services/desktop-bench.ts +1 -1
- package/mcp-server/src/tools/agents.ts +1 -1
- 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 +1 -1
- 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 +22 -9
- package/neural-interface/i18n/en.json +236 -7
- package/neural-interface/i18n/pt-BR.json +238 -9
- package/neural-interface/lib/account-usage.js +5 -3
- package/neural-interface/lib/agent-sandbox-elevation.js +102 -0
- package/neural-interface/lib/agent-sandbox-setting.js +57 -0
- package/neural-interface/lib/app-controls-api.js +161 -0
- package/neural-interface/lib/assistant-api.js +40 -7
- package/neural-interface/lib/assistant-brains/opencode.js +13 -1
- package/neural-interface/lib/assistant-brains/opencode2-browser-policy/index.js +44 -0
- package/neural-interface/lib/assistant-brains/opencode2-route-gate/index.js +57 -0
- package/neural-interface/lib/assistant-catalog.js +17 -2
- package/neural-interface/lib/assistant-dispatch.js +3 -0
- package/neural-interface/lib/assistant-envelope.js +19 -1
- package/neural-interface/lib/assistant-route-gate-codex.js +3 -1
- package/neural-interface/lib/assistant-runtime.js +131 -8
- package/neural-interface/lib/claude-model-catalog.js +64 -14
- package/neural-interface/lib/codex-account-auth.js +4 -2
- package/neural-interface/lib/codex-accounts.js +14 -1
- package/neural-interface/lib/codex-settings-schema.js +111 -47
- package/neural-interface/lib/editor-completion-opencode.js +132 -35
- package/neural-interface/lib/exec-loop-command.js +11 -2
- package/neural-interface/lib/install-details.js +23 -5
- package/neural-interface/lib/launch-cache.js +293 -0
- package/neural-interface/lib/login-start.js +524 -0
- package/neural-interface/lib/native-loop-providers.js +24 -4
- package/neural-interface/lib/native-loop-runtime.js +19 -0
- package/neural-interface/lib/opencode-db-schema.js +75 -0
- package/neural-interface/lib/opencode-history.js +16 -1
- package/neural-interface/lib/opencode-protocol.js +173 -0
- package/neural-interface/lib/opencode-provider-view.js +81 -0
- package/neural-interface/lib/opencode-runtime-config.js +10 -3
- package/neural-interface/lib/opencode-serve-records.js +606 -0
- package/neural-interface/lib/opencode-v2-client.js +33 -6
- package/neural-interface/lib/opencode-v2-ws-requests.js +19 -5
- package/neural-interface/lib/opencode-worker-activity.js +229 -0
- package/neural-interface/lib/opencode2-client.js +760 -0
- package/neural-interface/lib/opencode2-proxy.js +116 -0
- package/neural-interface/lib/opencode2-translate.js +1008 -0
- package/neural-interface/lib/process-identity.js +361 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +3 -2
- package/neural-interface/lib/session-cache-builder.js +26 -5
- package/neural-interface/lib/session-indexer.js +3 -1
- package/neural-interface/lib/session-title-generator.js +20 -0
- package/neural-interface/lib/style-guide/api.js +4 -3
- package/neural-interface/lib/untrusted-file-headers.js +27 -0
- package/neural-interface/package-lock.json +1342 -44
- package/neural-interface/package.json +5 -1
- package/neural-interface/public/index.html +2 -2
- package/neural-interface/public/shared/account-menu.js +4 -1
- package/neural-interface/public/shared/account-usage.js +62 -2
- package/neural-interface/public/shared/account-view.js +2 -1
- package/neural-interface/public/shared/agent-runtime-options.js +55 -11
- package/neural-interface/public/shared/assistant/asst-account-menu.js +298 -0
- package/neural-interface/public/shared/assistant/asst-agents-dock.js +172 -17
- package/neural-interface/public/shared/assistant/asst-api.js +18 -3
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +424 -152
- package/neural-interface/public/shared/assistant/asst-composer.js +1 -1
- package/neural-interface/public/shared/assistant/asst-hosts.js +49 -2
- package/neural-interface/public/shared/assistant/asst-menu.js +275 -77
- package/neural-interface/public/shared/assistant/asst-model-chip.js +135 -0
- package/neural-interface/public/shared/assistant/asst-panel.js +236 -35
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +28 -10
- package/neural-interface/public/shared/assistant/asst-redact.js +186 -0
- package/neural-interface/public/shared/assistant/asst-render.js +382 -36
- package/neural-interface/public/shared/assistant/asst-run-stage.js +231 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +193 -3
- package/neural-interface/public/shared/assistant/asst-scene-styles.js +273 -0
- package/neural-interface/public/shared/assistant/asst-scenes.js +528 -0
- package/neural-interface/public/shared/assistant/asst-sidepanel-styles.js +3 -15
- package/neural-interface/public/shared/assistant/asst-state.js +14 -0
- package/neural-interface/public/shared/assistant/asst-styles.js +109 -17
- package/neural-interface/public/shared/assistant/asst-tool-kinds.js +13 -1
- package/neural-interface/public/shared/cc-dropdown.js +480 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +14 -2
- package/neural-interface/public/shared/cdx/cdx-render.js +15 -3
- package/neural-interface/public/shared/cdx/cdx-styles.js +49 -5
- package/neural-interface/public/shared/cdx/cdx-tabs.js +152 -35
- package/neural-interface/public/shared/cp/cp-markdown.js +4 -57
- package/neural-interface/public/shared/data-cache.js +223 -0
- package/neural-interface/public/shared/desktop-bridge.js +11 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +78 -1
- package/neural-interface/public/shared/desktop-titlebar.js +214 -28
- package/neural-interface/public/shared/desktop-update-view.js +15 -5
- package/neural-interface/public/shared/file-links.js +73 -8
- package/neural-interface/public/shared/html-shell.js +74 -80
- package/neural-interface/public/shared/launch-prefetch.js +59 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-caps.js +3 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +11 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-modelpicker.js +54 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +97 -3
- package/neural-interface/public/shared/ocp-v2/ocp-v2-projectbar.js +3 -2
- package/neural-interface/public/shared/ocp-v2/ocp-v2-providers.js +11 -9
- package/neural-interface/public/shared/ocp-v2/ocp-v2-rehydrate.js +15 -4
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +1 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send-logic.js +401 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +16 -9
- package/neural-interface/public/shared/ocp-v2/ocp-v2-sessions-logic.js +60 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +83 -5
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +195 -19
- package/neural-interface/public/shared/opencode-providers.js +39 -0
- package/neural-interface/public/shared/schedules-view.js +318 -0
- package/neural-interface/public/shared/settings/settings-app-pane.js +335 -0
- package/neural-interface/public/shared/settings/settings-app.js +243 -0
- package/neural-interface/public/shared/settings/settings-data.js +88 -0
- package/neural-interface/public/shared/settings/settings-ia.js +17 -0
- package/neural-interface/public/shared/showcase-privacy.js +2 -1
- package/neural-interface/public/shared/sidepanel-session-status.js +49 -0
- package/neural-interface/public/shared/styles.css +543 -372
- package/neural-interface/public/shared/ui-assistant.js +27 -5
- package/neural-interface/public/shared/ui-automation-studio.js +67 -27
- package/neural-interface/public/shared/ui-bookmarks.js +2 -1
- package/neural-interface/public/shared/ui-claude-panel.js +126 -12
- package/neural-interface/public/shared/ui-codex-panel.js +1 -0
- package/neural-interface/public/shared/ui-detail.js +6 -6
- package/neural-interface/public/shared/ui-explorer.js +2 -0
- package/neural-interface/public/shared/ui-file-links.js +1 -0
- package/neural-interface/public/shared/ui-image-gallery.js +6 -5
- package/neural-interface/public/shared/ui-invite.js +18 -2
- package/neural-interface/public/shared/ui-link.js +9 -9
- package/neural-interface/public/shared/ui-menubar.js +232 -33
- package/neural-interface/public/shared/ui-native-loop-router.js +2 -0
- package/neural-interface/public/shared/ui-opencode-panel-v2.js +1 -0
- package/neural-interface/public/shared/ui-schedules-studio.js +2099 -869
- package/neural-interface/public/shared/ui-sessions.js +15 -4
- package/neural-interface/public/shared/ui-settings.js +87 -36
- package/neural-interface/public/shared/ui-sidebar.js +4 -4
- package/neural-interface/public/shared/ui-sidepanel-session-entry.js +13 -0
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +55 -4
- package/neural-interface/public/shared/ui-sidepanel-tray.js +84 -8
- package/neural-interface/public/shared/ui-skills.js +70 -12
- package/neural-interface/public/shared/ui-sync.js +9 -0
- package/neural-interface/public/shared/ui-terminal.js +36 -20
- package/neural-interface/public/shared/ui-trash.js +8 -1
- package/neural-interface/public/shared/ui-tutorial-steps.js +7 -3
- package/neural-interface/public/shared/ui-tutorial.js +15 -5
- package/neural-interface/public/shared/ui-update.js +87 -16
- package/neural-interface/public/shared/ui-whiteboard.js +2 -1
- package/neural-interface/public/variant/2d/main.js +4 -1
- package/neural-interface/public/variant/3d/main.js +3 -0
- package/neural-interface/public/vendor/highlight/LICENSE +29 -0
- package/neural-interface/public/vendor/highlight/github-dark-dimmed-11.12.0.min.css +9 -0
- package/neural-interface/public/vendor/highlight/highlight-11.12.0.min.js +1262 -0
- package/neural-interface/public/vendor/highlight/manifest.json +5 -0
- package/neural-interface/public/vendor/html2canvas/LICENSE +22 -0
- package/neural-interface/public/vendor/html2canvas/html2canvas-1.4.1.min.js +20 -0
- package/neural-interface/public/vendor/html2canvas/manifest.json +4 -0
- package/neural-interface/public/vendor/marked/LICENSE.md +44 -0
- package/neural-interface/public/vendor/marked/manifest.json +4 -0
- package/neural-interface/public/vendor/marked/marked-14.1.4.esm.js +2502 -0
- package/neural-interface/public/vendor/mermaid/LICENSE +21 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/abnfDiagram-PAR5IXJI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architecture-7GRP2DOG-M7G3SWCO.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/architectureDiagram-2PNWDENW.mjs +36 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/blockDiagram-PUWGUBOA.mjs +129 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/c4Diagram-3NLNHZP5.mjs +38 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-2NMSUPJH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-32MCUW2P.mjs +206 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3NPNU5GP.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-3SMGFRGC.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4CAJECRU.mjs +14 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-4OADUZ5Q.mjs +10 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IMINLNL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5IQCK5RY.mjs +3 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-5MVFYWFU.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AQ6EADP3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-AZZRMDJM.mjs +15 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-CLS4B6BI.mjs +168 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-DZP67EKU.mjs +70 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-EICP7K5X.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-GDY2UAB4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-H7VHZCWX.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-IJBDOHL6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JFFVMJIA.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JQRUD6KW.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-JZGDI5JK.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-KD6SPIPN.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NF366YI6.mjs +231 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-NUDPKBEF.mjs +32 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-OJINMHTS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Q6NGPIAH.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-QJSWEUOL.mjs +132 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-R7JDMUKV.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RCXAM2GK.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-RT4MAWR3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SFMINX4W.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-SLDQ463B.mjs +88 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-STOV2HOB.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-T5OCTHI4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-TCEHX7R3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VQC2EELJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VRZKL3EH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VTHDQ7BV.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-VY2OSTJR.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-WUBWJARI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YBMQ5JGJ.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-YDMLL4PJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-Z6V44ARX.mjs +62 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZJJ7VQJP.mjs +339 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZNLK7HRR.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZQN7UHIL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZS6VBONO.mjs +174 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/chunk-ZUNWM646.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-FGAMPOII.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/classDiagram-v2-AI5RJEK4.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cose-bilkent-Q72C2OJ3.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefin-OW5HDTMX-6OCTWOGG.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/cynefinDiagram-2OOWUY4M.mjs +62 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/dagre-MPVFI544.mjs +4 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-AE4NWXFW.mjs +24 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-BUJID5UG.mjs +41 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-C44CZ5NI.mjs +24 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-EYOLEXXS.mjs +30 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/diagram-P7B6PWOJ.mjs +3 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ebnfDiagram-FTPZ3PV6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/erDiagram-H3ERR26V.mjs +99 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/eventmodeling-NTZA5JFV-WKSWQHFS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/flowDiagram-YHGXBVSY.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ganttDiagram-RWFTB7ED.mjs +292 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraph-4MIJSDKK-JH2HKYUO.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/gitGraphDiagram-MAR6GKOP.mjs +106 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/info-A6RAGUB7-3TX3OFWW.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/infoDiagram-LV547AFH.mjs +2 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/ishikawaDiagram-PUJQGNDS.mjs +70 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/journeyDiagram-EW5QJT4W.mjs +139 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/kanban-definition-JSQERAZB.mjs +89 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/katex-CZ4GXH2S.mjs +257 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/mindmap-definition-VBJCLQLM.mjs +96 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/packet-AYTQ26CC-MKII6P3Y.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pegDiagram-7WILOQTJ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pie-WAS4IAKB-6B4OPMJ5.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/pieDiagram-EFA7CBSB.mjs +39 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/quadrantDiagram-WARVYA7P.mjs +7 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/radar-RG4KPBEZ-UPTR3XUL.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-74A4TZTK-MEFUPFMS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-abnf-HS5TGJTU-R5YG2S5D.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-ebnf-LZEXJU2U-G5QEX2L6.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroad-peg-WCYAUIDC-H5I2ZRY2.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/railroadDiagram-5NIQVNLQ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/requirementDiagram-SXREGOKZ.mjs +84 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sankeyDiagram-VV74M2VP.mjs +40 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sequenceDiagram-52ZFIFDL.mjs +162 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/sizeCapture-FGA5AGYP.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-3QAKPIVI.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/stateDiagram-v2-HN2P4RDZ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanes-QLVP42QD.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/swimlanesDiagram-JKPDBFCN.mjs +8 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/timeline-definition-GS45XAXW.mjs +120 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treeView-Q6P3EWNA-ZRJGANNQ.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/treemap-WGGIJYW6-K64VN5AS.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/vennDiagram-QIQU2ZRJ.mjs +34 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardley-WFR3VGLG-D67OWCCH.mjs +1 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/wardleyDiagram-U6YNM66N.mjs +78 -0
- package/neural-interface/public/vendor/mermaid/chunks/mermaid.esm.min/xychartDiagram-XJAZEDNM.mjs +7 -0
- package/neural-interface/public/vendor/mermaid/manifest.json +4 -0
- package/neural-interface/public/vendor/mermaid/mermaid-11.17.2.esm.min.mjs +14 -0
- package/neural-interface/scripts/build-vendor.mjs +84 -3
- package/neural-interface/scripts/rebuild-pty.js +14 -2
- package/neural-interface/server.js +1279 -372
- package/npm-shrinkwrap.json +4424 -0
- package/package.json +54 -2
- package/postinstall.js +61 -47
- package/preuninstall.js +8 -35
- package/setup.js +76 -44
- package/skills/synabun/modules/idea.md +1 -1
- package/skills/synabun/modules/schedule.md +2 -2
- package/updater.cmd +2 -2
- package/updater.mjs +97 -3
|
@@ -12,6 +12,8 @@ import { createClaudeAccounts as createPanelClaudeAccounts } from './lib/claude-
|
|
|
12
12
|
import { createClaudePanelAuth } from './lib/claude-panel-auth.js';
|
|
13
13
|
import { createCodexPanelAuth } from './lib/codex-panel-auth.js';
|
|
14
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';
|
|
15
17
|
import { createCodexAccountAuth } from './lib/codex-account-auth.js';
|
|
16
18
|
import { registerProviderPanelApiKeyAuthRoutes, redactProviderSecrets, providerApiKeyBodyError, providerDiagnosticSink } from './lib/provider-panel-api-key-auth.js';
|
|
17
19
|
import { createProviderPanelTerminalAuth, providerLoginSpec, registerProviderPanelTerminalAuthRoutes } from './lib/provider-panel-terminal-auth.js';
|
|
@@ -123,6 +125,8 @@ import { createCliResolver, spawnArgsFor } from './lib/cli-resolver.js';
|
|
|
123
125
|
import { osPath } from './lib/os-path.js';
|
|
124
126
|
import { createCliInstaller, CLI_INSTALL_GUIDES } from './lib/cli-install.js';
|
|
125
127
|
import { createCliInstallApi, createCliPathApi, createGitBashApi } from './lib/cli-install-api.js';
|
|
128
|
+
import { createAppControlsApi, createAppSettingsStore } from './lib/app-controls-api.js';
|
|
129
|
+
import { createLoginStart } from './lib/login-start.js';
|
|
126
130
|
import { createCliPathManager } from './lib/cli-path.js';
|
|
127
131
|
import { composeTerminalPath, terminalLaunchCommand, terminalArgument } from './lib/terminal-path.js';
|
|
128
132
|
import {
|
|
@@ -143,6 +147,11 @@ import {
|
|
|
143
147
|
} from './lib/backup-zip-reader.js';
|
|
144
148
|
import { planRestoreWrites, removeRestoreUpload, saveRestoreUpload } from './lib/system-restore.js';
|
|
145
149
|
import * as opencodeV2Client from './lib/opencode-v2-client.js';
|
|
150
|
+
import { basicAuthHeader, checkOpencodeServer, opencodeProblemText, protocolForVersion, serveSpecFor } from './lib/opencode-protocol.js';
|
|
151
|
+
import { SERVE_TOKEN_ENV } from './lib/process-identity.js';
|
|
152
|
+
import { createServeRecords } from './lib/opencode-serve-records.js';
|
|
153
|
+
import { opencode2Proxy } from './lib/opencode2-proxy.js';
|
|
154
|
+
import { opencodeDbFlavor, sessionTablesFor } from './lib/opencode-db-schema.js';
|
|
146
155
|
import {
|
|
147
156
|
handleOpencodeV2Request, opencodeV2WsCapabilities, opencodeSdkVersion, buildOpencodeRuntimeMcpConfig, shouldRelayOpencodeV2Message,
|
|
148
157
|
buildSessionListQuery, mapSessionListRow, OPENCODE_V2_WS_MUTATION_TYPES, guardOpencodeV2Request,
|
|
@@ -158,6 +167,7 @@ import {
|
|
|
158
167
|
createCodexNativeLoopAdapter,
|
|
159
168
|
createOpenCodeNativeLoopAdapter,
|
|
160
169
|
} from './lib/native-loop-providers.js';
|
|
170
|
+
import { createOpenCodeActivityRelay } from './lib/opencode-worker-activity.js';
|
|
161
171
|
import {
|
|
162
172
|
mergeCodexModelContextCapabilities,
|
|
163
173
|
resolveCodexExtendedContext,
|
|
@@ -236,15 +246,20 @@ import { generateSessionTitle } from './lib/session-title-generator.js';
|
|
|
236
246
|
import {
|
|
237
247
|
CLAUDE_EFFORT_LEVELS,
|
|
238
248
|
CLAUDE_FALLBACK_MODELS,
|
|
249
|
+
binaryIdentity,
|
|
239
250
|
checkClaudeCliSkew,
|
|
240
251
|
discoverClaudeModels,
|
|
252
|
+
markClaudeModelsStale,
|
|
241
253
|
onClaudeModelsChanged,
|
|
254
|
+
setClaudeModelStore,
|
|
242
255
|
shellCommand,
|
|
243
256
|
} from './lib/claude-model-catalog.js';
|
|
257
|
+
import { createLaunchCache, createLimiter, createSnapshotStore, launchCacheDir } from './lib/launch-cache.js';
|
|
258
|
+
import { compactProviderCatalog, isProviderCatalog, providerCatalogSignature } from './lib/opencode-provider-view.js';
|
|
244
259
|
import { resolveClaudeSdkExecutable } from './lib/claude-executable.js';
|
|
245
260
|
import { collectInstallDetails } from './lib/install-details.js';
|
|
246
261
|
import { withPathEnv, pathEnvValue, getAugmentedPath, rememberedOsPath, rememberPackagedPath, rememberedPackagedPath, withPackagedPathLast } from './lib/augmented-path.js';
|
|
247
|
-
import { readCodexSettingsSchema, readCodexFeatureSupport, validateCodexSettings } from './lib/codex-settings-schema.js';
|
|
262
|
+
import { cachedCodexSettingsSchema, readCodexSettingsSchema, readCodexFeatureSupport, setCodexSettingsStore, validateCodexSettings } from './lib/codex-settings-schema.js';
|
|
248
263
|
import { semverNewer, synabunReleaseChannel } from './lib/synabun-version.js';
|
|
249
264
|
import { selectSynabunUpdate, buildSynabunInstallPlan, refuseStaleUpdateClick } from './lib/synabun-update-plan.js';
|
|
250
265
|
import { collectActivity, countWhere, recentCliSessions } from './lib/system-activity.js';
|
|
@@ -272,11 +287,12 @@ import {
|
|
|
272
287
|
clampPointsToRect as wbClampPointsToRect,
|
|
273
288
|
convertPctInPlace as wbConvertPctInPlace,
|
|
274
289
|
} from '../mcp-server/dist/services/whiteboard-geometry.js';
|
|
275
|
-
import { ensureProjectCategories } from '../hooks/claude-code/shared.mjs';
|
|
290
|
+
import { ensureProjectCategories as ensureProjectCategoriesInFile, ensureCategories as ensureCategoriesInFile, FEATURE_CATEGORIES } from '../hooks/claude-code/shared.mjs';
|
|
276
291
|
import { getDataHome, getDataHomeDiagnostics, isGlobalInstall, pathIsInside } from '../lib/paths.js';
|
|
277
292
|
import { launcherLogPath } from '../lib/first-launch-protection.js';
|
|
278
|
-
import { registerStartLauncher, startLauncherState } from '../lib/start-launcher.js';
|
|
293
|
+
import { launcherRegistrationAllowed, registerStartLauncher, startLauncherState } from '../lib/start-launcher.js';
|
|
279
294
|
import { effectiveNpmPrefix } from '../lib/child-path.js';
|
|
295
|
+
import { checkRuntimeDependencies, fallbackInstallArgs, fallbackInstallEnv, findInstalled, mcpCompiler, npmCommand, npmInvocation } from '../lib/runtime-dependencies.js';
|
|
280
296
|
import { redactDeep, redactSecrets } from '../lib/log-redact.js';
|
|
281
297
|
import { applyPackagedChildPath, durableEntry, mcpStdioCommand, npmGlobalBinFor, packagedNpmPrefix, packagedPathFolders, packagedRuntime } from '../lib/packaged-runtime.js';
|
|
282
298
|
import { dataHomeId } from '../lib/desktop-app.js';
|
|
@@ -304,6 +320,7 @@ import {
|
|
|
304
320
|
clearOpenCodeHistoryInWorker,
|
|
305
321
|
getOpenCodeHistoryStats,
|
|
306
322
|
OPENCODE_HISTORY_CONFIRMATION,
|
|
323
|
+
OPENCODE2_HISTORY_REFUSAL,
|
|
307
324
|
} from './lib/opencode-history.js';
|
|
308
325
|
import {
|
|
309
326
|
clearProjectStorageInWorker,
|
|
@@ -328,11 +345,15 @@ const __dirname = dirname(__filename);
|
|
|
328
345
|
// This sweeps ALL platform directories, not just this host's: a vendored
|
|
329
346
|
// checkout can carry several, and repairing only the current one left e.g.
|
|
330
347
|
// prebuilds/darwin-x64/spawn-helper broken for Intel Macs.
|
|
348
|
+
// Both node_modules an install can have: the package root's (`npm i -g synabun` puts every dependency there) and
|
|
349
|
+
// the Neural Interface's own (a development checkout, a desktop build).
|
|
331
350
|
try {
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
351
|
+
for (const root of new Set([__dirname, resolve(__dirname, '..')])) {
|
|
352
|
+
const sweep = ensureVendoredExecutables({ root, log: (msg) => console.log(msg) });
|
|
353
|
+
for (const failure of sweep.failed) {
|
|
354
|
+
console.warn(`[binaries] ${failure.path} is not launchable (${failure.state}).`
|
|
355
|
+
+ (failure.repairCommand ? ` Try: ${failure.repairCommand}` : ''));
|
|
356
|
+
}
|
|
336
357
|
}
|
|
337
358
|
} catch (err) {
|
|
338
359
|
console.warn('[binaries] Could not verify vendored binary permissions:', err.message);
|
|
@@ -454,6 +475,8 @@ import {
|
|
|
454
475
|
import { createOpencodeCompletionEngine } from './lib/editor-completion-opencode.js';
|
|
455
476
|
import { createEndpointCompletionEngine } from './lib/editor-completion-endpoint.js';
|
|
456
477
|
import { ownerRouteGuard } from './lib/http-guards.js';
|
|
478
|
+
import { setUntrustedFileHeaders } from './lib/untrusted-file-headers.js';
|
|
479
|
+
import { BUNDLED_LOGO_FILES, UPLOADED_IMAGE_PATH } from './lib/request-gate.js';
|
|
457
480
|
import { createCloudflaredManager } from './lib/cloudflared/manager.js';
|
|
458
481
|
import { createQuickTunnel } from './lib/cloudflared/tunnel.js';
|
|
459
482
|
import { createGitBashManager } from './lib/git-bash/manager.js';
|
|
@@ -464,6 +487,9 @@ const PORT = process.env.NEURAL_PORT || 3344;
|
|
|
464
487
|
const EMBEDDING_DIMS = getEmbeddingDims();
|
|
465
488
|
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
466
489
|
const DATA_HOME = getDataHome();
|
|
490
|
+
// Where the slow lists windows and menus wait for (model lists, OpenCode's providers, Codex probes) are kept
|
|
491
|
+
// across restarts (lib/launch-cache.js): disposable, so outside data/ (no backup carries them) and outside the package.
|
|
492
|
+
const LAUNCH_CACHE_DIR = launchCacheDir(DATA_HOME);
|
|
467
493
|
registerCodexPlanRoutes(app, resolve(DATA_HOME, 'data', 'codex-plans'));
|
|
468
494
|
const DATA_HOME_DIAGNOSTICS = getDataHomeDiagnostics(DATA_HOME, PACKAGE_ROOT);
|
|
469
495
|
const CODEX_SIDEPANEL_PERMISSIONS_PATH = resolve(DATA_HOME, 'data', 'codex-sidepanel-permissions.json');
|
|
@@ -538,49 +564,31 @@ const GLOBAL_CATEGORIES_PATH = resolve(CATEGORIES_DATA_DIR, 'custom-categories.j
|
|
|
538
564
|
const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
|
|
539
565
|
const CATEGORIES_POINT_ID = '00000000-0000-0000-0000-000000000000';
|
|
540
566
|
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
|
|
544
|
-
{
|
|
545
|
-
name: 'uncategorized',
|
|
546
|
-
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.',
|
|
547
|
-
color: '#6b7280',
|
|
548
|
-
},
|
|
549
|
-
{
|
|
550
|
-
name: 'conversations',
|
|
551
|
-
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?"',
|
|
552
|
-
color: '#a855f7',
|
|
553
|
-
is_parent: true,
|
|
554
|
-
},
|
|
555
|
-
{
|
|
556
|
-
name: 'communication-style',
|
|
557
|
-
description: 'User communication patterns and preferences.',
|
|
558
|
-
},
|
|
559
|
-
{
|
|
560
|
-
name: 'plans',
|
|
561
|
-
description: 'Implementation plans stored after plan mode approval.',
|
|
562
|
-
color: '#06d6a0',
|
|
563
|
-
is_parent: true,
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
name: 'ideas',
|
|
567
|
-
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.',
|
|
568
|
-
color: '#f59e0b',
|
|
569
|
-
},
|
|
570
|
-
];
|
|
571
|
-
|
|
567
|
+
// No default categories: a fresh install has none. A project's defaults are made
|
|
568
|
+
// when the project is added (ensureProjectCategories), a feature's category the
|
|
569
|
+
// first time the feature saves into it.
|
|
572
570
|
function getCategoriesPath() {
|
|
573
571
|
// Single categories file — no per-connection paths needed with SQLite
|
|
574
|
-
if (!existsSync(GLOBAL_CATEGORIES_PATH)) {
|
|
575
|
-
const dir = resolve(GLOBAL_CATEGORIES_PATH, '..');
|
|
576
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
577
|
-
writeFileSync(GLOBAL_CATEGORIES_PATH, JSON.stringify({ version: 1, categories: DEFAULT_CATEGORIES }, null, 2), 'utf-8');
|
|
578
|
-
}
|
|
579
|
-
|
|
580
572
|
return GLOBAL_CATEGORIES_PATH;
|
|
581
573
|
}
|
|
582
574
|
|
|
575
|
+
// The file not written yet: what SQLite (the source of truth) holds, which is nothing on a fresh install.
|
|
576
|
+
function categoriesFromDb() {
|
|
577
|
+
try {
|
|
578
|
+
return dbGetCategories().map((row) => {
|
|
579
|
+
const category = { name: row.name, description: row.description || '', created_at: row.created_at };
|
|
580
|
+
if (row.parent) category.parent = row.parent;
|
|
581
|
+
if (row.color) category.color = row.color;
|
|
582
|
+
if (row.is_parent) category.is_parent = true;
|
|
583
|
+
return category;
|
|
584
|
+
});
|
|
585
|
+
} catch {
|
|
586
|
+
return [];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
583
590
|
function loadCategories() {
|
|
591
|
+
if (!existsSync(GLOBAL_CATEGORIES_PATH)) return categoriesFromDb();
|
|
584
592
|
try {
|
|
585
593
|
const raw = readFileSync(getCategoriesPath(), 'utf-8');
|
|
586
594
|
const data = JSON.parse(raw);
|
|
@@ -593,6 +601,27 @@ function loadCategories() {
|
|
|
593
601
|
}
|
|
594
602
|
}
|
|
595
603
|
|
|
604
|
+
// 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.
|
|
605
|
+
function syncCategoriesToDb(created) {
|
|
606
|
+
if (!created?.length) return created || [];
|
|
607
|
+
try { dbSaveCategories(loadCategories()); } catch (err) { console.error('Failed to sync categories to SQLite:', err.message); }
|
|
608
|
+
return created;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// A project's default categories (parent + project / architecture / bugs / config), made once, when the project is
|
|
612
|
+
// registered; never for a project already in the registry. Adds what is missing only.
|
|
613
|
+
function ensureProjectCategories(projects) {
|
|
614
|
+
return syncCategoriesToDb(ensureProjectCategoriesInFile(projects).created);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// A feature's own category (FEATURE_CATEGORIES: conversations, communication-style, plans, uncategorized), made the
|
|
618
|
+
// first time the feature saves into it; a fresh install has none. Adds it only when it is missing.
|
|
619
|
+
function ensureFeatureCategory(name) {
|
|
620
|
+
const feature = Object.prototype.hasOwnProperty.call(FEATURE_CATEGORIES, name) ? FEATURE_CATEGORIES[name] : null;
|
|
621
|
+
if (!feature) return [];
|
|
622
|
+
return syncCategoriesToDb(ensureCategoriesInFile([{ name, description: feature.description, is_parent: feature.is_parent }]).created);
|
|
623
|
+
}
|
|
624
|
+
|
|
596
625
|
function saveCategories(categories) {
|
|
597
626
|
const filePath = getCategoriesPath();
|
|
598
627
|
const dir = resolve(filePath, '..');
|
|
@@ -823,11 +852,19 @@ app.use('/skins', express.static(join(__dirname, 'skins'), {
|
|
|
823
852
|
lastModified: true,
|
|
824
853
|
setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache'),
|
|
825
854
|
}));
|
|
855
|
+
// The user's own file icons (POST /api/file-icons, SVG among them): an <img> shows them, opened by their address
|
|
856
|
+
// they run nothing and an SVG is a download (lib/untrusted-file-headers.js).
|
|
826
857
|
app.use('/custom-icons', express.static(CUSTOM_ICONS_DIR, {
|
|
827
858
|
etag: true,
|
|
828
859
|
lastModified: true,
|
|
829
|
-
setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache'),
|
|
860
|
+
setHeaders: (res, path) => { res.setHeader('Cache-Control', 'no-cache'); setUntrustedFileHeaders(res, path); },
|
|
830
861
|
}));
|
|
862
|
+
// A category's uploaded logo sits beside the app's own files ("/logo-<name>.<ext>"): it gets the same headers.
|
|
863
|
+
app.use(function uploadedLogoHeaders(req, res, next) {
|
|
864
|
+
const path = String(req.path || '').toLowerCase();
|
|
865
|
+
if (UPLOADED_IMAGE_PATH.test(path) && (!BUNDLED_LOGO_FILES.includes(path) || isCategoryUpload(path))) setUntrustedFileHeaders(res, path);
|
|
866
|
+
next();
|
|
867
|
+
});
|
|
831
868
|
app.use(express.static(join(__dirname, 'public'), {
|
|
832
869
|
etag: true,
|
|
833
870
|
lastModified: true,
|
|
@@ -2391,6 +2428,18 @@ function cleanupOldUpdaterStages(baseDir) {
|
|
|
2391
2428
|
} catch {}
|
|
2392
2429
|
}
|
|
2393
2430
|
|
|
2431
|
+
// lib/package-processes.js and the module it imports (only node: modules in either).
|
|
2432
|
+
const UPDATER_STAGED_LIBS = ['package-processes.js', 'runtime-dependencies.js'];
|
|
2433
|
+
|
|
2434
|
+
// The forms of the package folder this server knows: as it resolved it, and as it was started
|
|
2435
|
+
// (`node <prefix>\node_modules\synabun\neural-interface\server.js` through a junction or a link).
|
|
2436
|
+
function updaterPackageRoots() {
|
|
2437
|
+
const roots = [PACKAGE_ROOT];
|
|
2438
|
+
const started = process.argv[1] ? resolve(process.argv[1]) : '';
|
|
2439
|
+
if (basename(started) === 'server.js' && basename(dirname(started)) === 'neural-interface') roots.push(resolve(started, '..', '..'));
|
|
2440
|
+
return [...new Set(roots)];
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2394
2443
|
function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids = [], safetySnapshot = null, handoffPath = null }) {
|
|
2395
2444
|
const sourceUpdater = resolve(PACKAGE_ROOT, 'updater.mjs');
|
|
2396
2445
|
if (!existsSync(sourceUpdater)) {
|
|
@@ -2408,6 +2457,16 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids
|
|
|
2408
2457
|
const payloadPath = resolve(stageDir, 'payload.json');
|
|
2409
2458
|
const runnerPath = resolve(stageDir, process.platform === 'win32' ? 'run-update.cmd' : 'run-update.sh');
|
|
2410
2459
|
copyFileSync(sourceUpdater, stagedUpdater);
|
|
2460
|
+
// What updater.mjs imports on Windows to stop the processes AI clients started from this folder: staged as lib/
|
|
2461
|
+
// next to it, with a package.json that makes those .js files ES modules on every Node SynaBun runs on. One that
|
|
2462
|
+
// cannot be copied leaves the updater without that step, never without the update.
|
|
2463
|
+
try {
|
|
2464
|
+
mkdirSync(resolve(stageDir, 'lib'), { recursive: true });
|
|
2465
|
+
writeFileSync(resolve(stageDir, 'lib', 'package.json'), '{ "type": "module" }\n', 'utf8');
|
|
2466
|
+
for (const file of UPDATER_STAGED_LIBS) copyFileSync(resolve(PACKAGE_ROOT, 'lib', file), resolve(stageDir, 'lib', file));
|
|
2467
|
+
} catch (err) {
|
|
2468
|
+
console.warn(`[server] could not stage lib/ for the updater: ${err?.message}`);
|
|
2469
|
+
}
|
|
2411
2470
|
|
|
2412
2471
|
const payload = {
|
|
2413
2472
|
serverPid,
|
|
@@ -2419,6 +2478,9 @@ function stageSynabunUpdater({ installPlan, serverPid, autoRestart, cleanupPids
|
|
|
2419
2478
|
target: installPlan.target,
|
|
2420
2479
|
channel: installPlan.channel,
|
|
2421
2480
|
cleanupPids,
|
|
2481
|
+
// The folder `npm i -g` replaces, in every form known here: on Windows the updater stops what still runs from it.
|
|
2482
|
+
packageRoot: PACKAGE_ROOT,
|
|
2483
|
+
packageRoots: updaterPackageRoots(),
|
|
2422
2484
|
safetySnapshot,
|
|
2423
2485
|
handoffPath,
|
|
2424
2486
|
// Where the relaunched launcher leaves its reason if it stops.
|
|
@@ -3438,22 +3500,60 @@ function getNativeCodexBin() {
|
|
|
3438
3500
|
// its full path; on Windows the opencode.exe found, started directly with the
|
|
3439
3501
|
// PATH a new terminal has (a command file that leads nowhere: through cmd.exe,
|
|
3440
3502
|
// every word escaped). A command line in Settings is run as written.
|
|
3441
|
-
|
|
3503
|
+
// `protocol` is the OpenCode major it is (detectOpencodeProtocol): OpenCode 2
|
|
3504
|
+
// also gets its loopback host and a fresh password (lib/opencode-protocol.js
|
|
3505
|
+
// serveSpecFor), which the answer carries as `password`.
|
|
3506
|
+
function opencodeServeLaunch(port, { env = process.env, protocol = 1, password = null } = {}) {
|
|
3442
3507
|
const found = cliResolver.launcher('opencode');
|
|
3443
3508
|
_noteCliLookup('opencode', found, 'OpenCode');
|
|
3444
3509
|
const line = configuredCommandLine('opencode');
|
|
3510
|
+
const spec = serveSpecFor(protocol, port, { env, password });
|
|
3511
|
+
const words = spec.args.join(' ');
|
|
3445
3512
|
if (IS_WIN) {
|
|
3446
|
-
const spawnEnv = withPathEnv(env, getAugmentedPath());
|
|
3513
|
+
const spawnEnv = withPathEnv(spec.env, getAugmentedPath());
|
|
3447
3514
|
if (!line && found.spec) {
|
|
3448
|
-
const launch = spawnArgsFor(found.spec,
|
|
3449
|
-
return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath}
|
|
3515
|
+
const launch = spawnArgsFor(found.spec, spec.args, { env: spawnEnv });
|
|
3516
|
+
return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath} ${words}`, password: spec.password };
|
|
3450
3517
|
}
|
|
3451
|
-
const text = `${line || 'opencode'}
|
|
3452
|
-
return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text };
|
|
3518
|
+
const text = `${line || 'opencode'} ${words}`;
|
|
3519
|
+
return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text, password: spec.password };
|
|
3453
3520
|
}
|
|
3454
3521
|
const cmd = line || (found.path ? shellSingleQuote(found.path) : 'opencode');
|
|
3455
3522
|
// A packaged application's runtime and npm after the user's own (the login shell adds the user's profile on top).
|
|
3456
|
-
return { command: DEFAULT_SHELL, args: ['-lc', `exec ${cmd}
|
|
3523
|
+
return { command: DEFAULT_SHELL, args: ['-lc', `exec ${cmd} ${words}`], options: { env: withPackagedPathLast(spec.env) }, label: `${cmd} ${words}`, password: spec.password };
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
// Which OpenCode major the binary SynaBun starts is: the resolver's `--version`
|
|
3527
|
+
// of it (cached by the file, so an upgrade in place is read again), then
|
|
3528
|
+
// lib/opencode-protocol.js protocolForVersion. `{ protocol: 1 | 2 | null,
|
|
3529
|
+
// major, version, supported, reason, path }`; `protocol` null with `supported`
|
|
3530
|
+
// false and a `major` is an OpenCode SynaBun does not speak (3 and later). A
|
|
3531
|
+
// command line in Settings has no version to read (`protocol` null, `major`
|
|
3532
|
+
// null): its serve is told apart by what answers (checkOpencodeServer).
|
|
3533
|
+
async function detectOpencodeProtocol({ force = false } = {}) {
|
|
3534
|
+
let found = cliResolver.launcher('opencode');
|
|
3535
|
+
if (configuredCommandLine('opencode')) {
|
|
3536
|
+
return { protocol: null, major: null, version: null, supported: true, reason: null, path: null, commandLine: true };
|
|
3537
|
+
}
|
|
3538
|
+
let version = found.version || null;
|
|
3539
|
+
if (!version || force || found.state === 'unverified') {
|
|
3540
|
+
try {
|
|
3541
|
+
const status = await cliResolver.resolve('opencode', { force });
|
|
3542
|
+
version = status?.version || version;
|
|
3543
|
+
found = cliResolver.launcher('opencode');
|
|
3544
|
+
version = found.version || version;
|
|
3545
|
+
} catch { /* the launch below says what is wrong */ }
|
|
3546
|
+
}
|
|
3547
|
+
if (!found.path) return { protocol: null, major: null, version: null, supported: false, reason: 'OpenCode is not installed.', path: null };
|
|
3548
|
+
return { ...protocolForVersion(version), path: found.path };
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
// The OpenCode major as far as it is known now, without starting anything (for
|
|
3552
|
+
// a command line built at once): the resolver's cached --version, else the
|
|
3553
|
+
// major the shared serve answered with, else 1.
|
|
3554
|
+
function knownOpencodeMajor() {
|
|
3555
|
+
const fromVersion = protocolForVersion(cliResolver.launcher('opencode').version).protocol;
|
|
3556
|
+
return fromVersion || _ocpProtocol || 1;
|
|
3457
3557
|
}
|
|
3458
3558
|
|
|
3459
3559
|
function extractCodexConfigValues(result) {
|
|
@@ -3708,6 +3808,25 @@ function readCodexRuntimeModelCatalog(codexHome = CODEX_DEFAULT_HOME) {
|
|
|
3708
3808
|
}
|
|
3709
3809
|
const CODEX_ACCOUNTS_PATH = resolve(DATA_HOME, 'data', 'codex-accounts.json');
|
|
3710
3810
|
|
|
3811
|
+
// Admin sandbox (lib/agent-sandbox-setting.js, lib/agent-sandbox-elevation.js): one global value,
|
|
3812
|
+
// off by default, for every agent SynaBun starts: may it use a sandbox whose setup needs Windows
|
|
3813
|
+
// administrator approval. Codex is the provider it changes (windows.sandbox, a launch override on
|
|
3814
|
+
// every Codex process, never written into config.toml). Changed from the Assistant panel only; idle
|
|
3815
|
+
// Codex app-servers (panel tabs, the Assistant's brain) restart under it, busy ones keep theirs.
|
|
3816
|
+
const _agentSandbox = createAdminSandboxSetting({
|
|
3817
|
+
path: resolve(DATA_HOME, 'data', 'agent-sandbox.json'),
|
|
3818
|
+
broadcast: (message) => broadcastSync(message),
|
|
3819
|
+
log: (tag, message) => console.log(`[${tag}]`, message),
|
|
3820
|
+
});
|
|
3821
|
+
function adminSandboxOn() { return _agentSandbox.enabled(); }
|
|
3822
|
+
/** The `-c` words every Codex SynaBun starts gets for the setting (outside an account's launch plan). */
|
|
3823
|
+
function codexSandboxArgs() { return codexWindowsSandboxArgs(adminSandboxOn()); }
|
|
3824
|
+
_agentSandbox.subscribe(() => {
|
|
3825
|
+
for (const connection of [..._codexLiveConnections]) {
|
|
3826
|
+
if (!connection.busy()) { try { connection.credentialsChanged(); } catch {} }
|
|
3827
|
+
}
|
|
3828
|
+
});
|
|
3829
|
+
|
|
3711
3830
|
// The Codex accounts registry (lib/codex-accounts.js): one instance for the
|
|
3712
3831
|
// sidepanel, the Assistant (brain, workers, catalog), loops and every route, so
|
|
3713
3832
|
// one account state is shown everywhere. Its ChatGPT half is Codex's own
|
|
@@ -3721,6 +3840,8 @@ function _codexAccounts() {
|
|
|
3721
3840
|
dataHome: DATA_HOME, homeDir: getHomePath(), accountsRoot: CODEX_ACCOUNTS_ROOT,
|
|
3722
3841
|
readNativeStatus: (accountId) => codexAccountAuth().nativeStatus(accountId),
|
|
3723
3842
|
prepareDefaultConfig: () => ensureCanonicalCodexConfig({ strict: true }),
|
|
3843
|
+
// Every launch plan carries the Windows sandbox the Admin sandbox setting allows.
|
|
3844
|
+
adminSandbox: () => adminSandboxOn(),
|
|
3724
3845
|
log: (tag, message) => console.log(`[${tag}]`, message),
|
|
3725
3846
|
});
|
|
3726
3847
|
_codexAccountsInstance.subscribe((event) => {
|
|
@@ -3728,6 +3849,8 @@ function _codexAccounts() {
|
|
|
3728
3849
|
broadcastSync({ type: 'codex:accounts-changed', accountId: event.accountId || null, removed: !!event.removed, created: !!event.created });
|
|
3729
3850
|
broadcastSync({ type: 'assistant:accounts-changed', provider: 'codex', accountId: event.accountId || null, removed: !!event.removed });
|
|
3730
3851
|
_assistantCatalog?.invalidate?.();
|
|
3852
|
+
// The account's model list is read again (another sign-in is another key; a removed account's is dropped).
|
|
3853
|
+
codexModelListsChanged(event.accountId || null, { removed: !!event.removed });
|
|
3731
3854
|
} catch {}
|
|
3732
3855
|
});
|
|
3733
3856
|
}
|
|
@@ -3850,6 +3973,8 @@ function handleCodexSkinWebSocket(ws) {
|
|
|
3850
3973
|
let childUsesHeldKey = false;
|
|
3851
3974
|
// Which credential the running app-server started with (launchFor().credential).
|
|
3852
3975
|
let childCredential = null;
|
|
3976
|
+
// Which Windows sandbox it started with (launchFor().windowsSandbox, the Admin sandbox setting).
|
|
3977
|
+
let childWindowsSandbox = null;
|
|
3853
3978
|
const CODEX_GREETING_MARKER_START = '[SYNABUN_CODEX_GREETING_START]';
|
|
3854
3979
|
const CODEX_GREETING_MARKER_END = '[SYNABUN_CODEX_GREETING_END]';
|
|
3855
3980
|
const CODEX_PLAN_MODE_PREFIXES = [
|
|
@@ -3978,7 +4103,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
3978
4103
|
codexSessionSettings.set(activeAccountId, threadId, settings);
|
|
3979
4104
|
}
|
|
3980
4105
|
if (['skills_config_write', 'experimental_features_set'].includes(type)) {
|
|
3981
|
-
if (type === 'experimental_features_set') codexContextModels = [];
|
|
4106
|
+
if (type === 'experimental_features_set') { codexContextModels = []; codexModelListsChanged(null); }
|
|
3982
4107
|
sendToClient({ type: 'catalogs_changed', catalogs: type === 'skills_config_write' ? ['skills'] : ['models', 'skills', 'apps', 'mcp', 'features'] });
|
|
3983
4108
|
}
|
|
3984
4109
|
}
|
|
@@ -4092,12 +4217,12 @@ CRITICAL — When you need clarification during planning:
|
|
|
4092
4217
|
sendToClient({ type: 'account_state', accountId: activeAccountId, account: _codexAccounts().describeForClient(activeAccountId), accounts: listCodexAccountsForClient() });
|
|
4093
4218
|
}
|
|
4094
4219
|
|
|
4095
|
-
// The account's credential or mode changed (withCodexAccountCredentialChange)
|
|
4096
|
-
// app-server is retired, and the next request starts one under
|
|
4097
|
-
// conversation resumes from its thread then. The transcript stays.
|
|
4220
|
+
// The account's credential or mode changed (withCodexAccountCredentialChange), or the Admin
|
|
4221
|
+
// sandbox setting did: an idle app-server is retired, and the next request starts one under
|
|
4222
|
+
// the new launch plan; the conversation resumes from its thread then. The transcript stays.
|
|
4098
4223
|
function retireForCredentialChange() {
|
|
4099
4224
|
const next = _codexAccounts().launchFor(activeAccountId);
|
|
4100
|
-
if (child && !activeTurnId && !pendingServerRequests.size && (!next.ok || next.credential !== childCredential)) {
|
|
4225
|
+
if (child && !activeTurnId && !pendingServerRequests.size && (!next.ok || next.credential !== childCredential || next.windowsSandbox !== childWindowsSandbox)) {
|
|
4101
4226
|
const old = child;
|
|
4102
4227
|
const oldHome = activeCodexHome;
|
|
4103
4228
|
const oldThreadIds = ownedWriterThreadIds();
|
|
@@ -4118,6 +4243,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
4118
4243
|
unregisterCodexMcpProfileRuntime();
|
|
4119
4244
|
child = null;
|
|
4120
4245
|
childCredential = null;
|
|
4246
|
+
childWindowsSandbox = null;
|
|
4121
4247
|
retireCodexProcess(old, { codexHome: oldHome, threadIds: oldThreadIds, reason: 'Codex account sign-in changed' });
|
|
4122
4248
|
}
|
|
4123
4249
|
sendAccountState();
|
|
@@ -4488,9 +4614,10 @@ CRITICAL — When you need clarification during planning:
|
|
|
4488
4614
|
}
|
|
4489
4615
|
|
|
4490
4616
|
async function readSettingsMetadata() {
|
|
4491
|
-
|
|
4617
|
+
// Both probes start Codex itself, off the event loop, once per binary (kept across restarts).
|
|
4618
|
+
const schema = await readCodexSettingsSchema(getCodexBin());
|
|
4492
4619
|
const features = await listCodexPages(request, 'experimentalFeature/list', { limit: 100 });
|
|
4493
|
-
const featureSupport = readCodexFeatureSupport(getCodexBin(), features.data);
|
|
4620
|
+
const featureSupport = await readCodexFeatureSupport(getCodexBin(), features.data);
|
|
4494
4621
|
// The catalog's enabled field is a process-start snapshot. config/read is
|
|
4495
4622
|
// authoritative for saved defaults after reloadUserConfig writes.
|
|
4496
4623
|
return { ...schema, features: features.data.map(feature => ({ ...feature,
|
|
@@ -5183,7 +5310,8 @@ CRITICAL — When you need clarification during planning:
|
|
|
5183
5310
|
args.unshift(codexBin);
|
|
5184
5311
|
codexBin = process.execPath;
|
|
5185
5312
|
}
|
|
5186
|
-
// API-key mode with a key SynaBun
|
|
5313
|
+
// The Windows sandbox the Admin sandbox setting allows and, in API-key mode with a key SynaBun
|
|
5314
|
+
// holds, an ephemeral credential store: before the subcommand.
|
|
5187
5315
|
args.splice(args.length - 1, 0, ...codexAppServerArgs(launch));
|
|
5188
5316
|
const useShell = !codexBin.includes(sep)
|
|
5189
5317
|
|| (process.platform === 'win32' && /\.(cmd|bat)$/i.test(codexBin));
|
|
@@ -5212,10 +5340,11 @@ CRITICAL — When you need clarification during planning:
|
|
|
5212
5340
|
childApiKeyLogin = launch.apiKey || null;
|
|
5213
5341
|
childUsesHeldKey = !!launch.apiKey;
|
|
5214
5342
|
childCredential = launch.credential;
|
|
5343
|
+
childWindowsSandbox = launch.windowsSandbox;
|
|
5215
5344
|
childRetirementPromise = null;
|
|
5216
5345
|
writerLifecycleState.releaseRequested = false;
|
|
5217
5346
|
registerCodexMcpProfileRuntime();
|
|
5218
|
-
console.log(`[codex-skin] Spawned app-server pid ${proc.pid || '?'} account=${activeAccountId} mode=${launch.mode || 'none'} profile=${codexMcpProfileState.value} runtime=${codexMcpRuntimeId} CODEX_HOME=${activeCodexHome}`);
|
|
5347
|
+
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}`);
|
|
5219
5348
|
proc.stdout.setEncoding('utf-8');
|
|
5220
5349
|
proc.stdout.on('data', (chunk) => {
|
|
5221
5350
|
if (child !== proc) return; // stale process from a previous account
|
|
@@ -5479,13 +5608,13 @@ CRITICAL — When you need clarification during planning:
|
|
|
5479
5608
|
// Sync tool approvals only after the base transport is known to be valid.
|
|
5480
5609
|
syncCodexToolPermissions([..._codexPermittedTools], activeCodexHome);
|
|
5481
5610
|
const codexBin = getCodexBin();
|
|
5611
|
+
// The version the resolver read (its `--version`, cached per binary and asked off the request): a
|
|
5612
|
+
// blocking spawn here froze the whole server for up to 5 s. Before the first probe of a binary it is
|
|
5613
|
+
// null, and the app-server's own userAgent fills it in at initialize (below).
|
|
5482
5614
|
let cliVersion = null;
|
|
5483
5615
|
let sdkVersion = null;
|
|
5484
|
-
try {
|
|
5485
|
-
|
|
5486
|
-
cliVersion = parseCodexVersion(versionResult.stdout);
|
|
5487
|
-
} catch {}
|
|
5488
|
-
try { sdkVersion = JSON.parse(readFileSync(join(__dirname, 'node_modules', '@openai', 'codex-sdk', 'package.json'), 'utf-8')).version; } catch {}
|
|
5616
|
+
try { cliVersion = parseCodexVersion(cliResolver.launcher('codex').version); } catch {}
|
|
5617
|
+
sdkVersion = findInstalled(__dirname, '@openai/codex-sdk', { stopAt: PACKAGE_ROOT })?.version || null;
|
|
5489
5618
|
codexCapabilities.reset({ codexBin, codexBinSource: _codexBinSource, cliVersion, sdkVersion });
|
|
5490
5619
|
startChild();
|
|
5491
5620
|
bootPromise = request('initialize', {
|
|
@@ -6372,11 +6501,18 @@ CRITICAL — When you need clarification during planning:
|
|
|
6372
6501
|
}
|
|
6373
6502
|
return;
|
|
6374
6503
|
}
|
|
6504
|
+
// The sandbox check below is synchronous: the schema it validates against is read first (asynchronously,
|
|
6505
|
+
// once per Codex binary), so the check itself never starts Codex.
|
|
6506
|
+
if (msg.type === 'thread_settings_update' && Object.prototype.hasOwnProperty.call(msg, 'sandboxMode') && msg.sandboxMode !== null) {
|
|
6507
|
+
await readCodexSettingsSchema(getCodexBin()).catch(() => null);
|
|
6508
|
+
}
|
|
6375
6509
|
if (await handleCodexCompatMessage(msg, {
|
|
6376
6510
|
activeThreadId, activeTurnId, cwd: PACKAGE_ROOT,
|
|
6377
6511
|
role: codexRole, getActiveThreadId: () => activeThreadId,
|
|
6378
6512
|
sandboxPolicyForMode: (mode, cwd) => {
|
|
6379
|
-
|
|
6513
|
+
const schema = cachedCodexSettingsSchema(getCodexBin());
|
|
6514
|
+
if (!schema) throw new Error('Codex could not report its settings schema');
|
|
6515
|
+
validateCodexSettings({ sandbox_mode: mode }, schema);
|
|
6380
6516
|
return buildCodexSandboxPolicy(cwd, mode);
|
|
6381
6517
|
},
|
|
6382
6518
|
limitAppCall: message => appBoundary.consume(activeThreadId, message.originCallId),
|
|
@@ -6723,6 +6859,8 @@ CRITICAL — When you need clarification during planning:
|
|
|
6723
6859
|
await request('config/batchWrite', { edits, reloadUserConfig: true }, 10000);
|
|
6724
6860
|
const config = await readConfigState(msg.cwd || PACKAGE_ROOT);
|
|
6725
6861
|
codexContextModels = [];
|
|
6862
|
+
// config.toml is every account's (shared): their kept model lists are read again.
|
|
6863
|
+
codexModelListsChanged(null);
|
|
6726
6864
|
sendToClient({ type: 'config_saved', requestId: msg.requestId || null, config });
|
|
6727
6865
|
sendToClient({ type: 'catalogs_changed', catalogs: ['models', 'skills', 'apps', 'mcp', 'features', 'config'] });
|
|
6728
6866
|
} catch (err) {
|
|
@@ -7373,8 +7511,63 @@ CRITICAL — When you need clarification during planning:
|
|
|
7373
7511
|
ws.on('close', handleCodexSocketClose);
|
|
7374
7512
|
}
|
|
7375
7513
|
|
|
7376
|
-
|
|
7377
|
-
|
|
7514
|
+
// The Codex model list of each account (GET /api/codex/models, the Studios' model check, the Assistant's
|
|
7515
|
+
// catalog through the route): read from the account's app-server, answered at once from the last good list
|
|
7516
|
+
// (lib/launch-cache.js, kept across restarts) and read again in the background once it is old. Keyed on what
|
|
7517
|
+
// it depends on: the account and its home, the Codex binary (an update is another binary) and the account's
|
|
7518
|
+
// sign-in, so one account's list is never given for another. An empty list is what a failure leaves (Codex
|
|
7519
|
+
// missing, signed out): never kept.
|
|
7520
|
+
const CODEX_MODEL_LIST_CACHE_MS = 10 * 60_000;
|
|
7521
|
+
|
|
7522
|
+
function codexModelListKey(accountId) {
|
|
7523
|
+
const id = String(accountId || 'default');
|
|
7524
|
+
const account = findCodexAccount(id);
|
|
7525
|
+
let signIn = '';
|
|
7526
|
+
try {
|
|
7527
|
+
const row = _codexAccounts().describe(id);
|
|
7528
|
+
signIn = createHash('sha256').update(`${row?.mode || ''}\n${row?.identity || ''}\n${row?.signedIn}`).digest('hex').slice(0, 16);
|
|
7529
|
+
} catch { /* unreadable: keyed on the rest */ }
|
|
7530
|
+
return JSON.stringify([id, account?.home || CODEX_DEFAULT_HOME, binaryIdentity(configuredCommandLine('codex') || getCodexBin()), signIn]);
|
|
7531
|
+
}
|
|
7532
|
+
|
|
7533
|
+
const _codexModelListCache = createLaunchCache({
|
|
7534
|
+
name: 'codex-models',
|
|
7535
|
+
dir: LAUNCH_CACHE_DIR,
|
|
7536
|
+
ttlMs: CODEX_MODEL_LIST_CACHE_MS,
|
|
7537
|
+
maxEntries: 24,
|
|
7538
|
+
load: async (key) => {
|
|
7539
|
+
const [accountId] = JSON.parse(key);
|
|
7540
|
+
const account = findCodexAccount(accountId);
|
|
7541
|
+
if (!account) throw new Error(`Codex account not found: ${accountId}`);
|
|
7542
|
+
const codexHome = account.home || CODEX_DEFAULT_HOME;
|
|
7543
|
+
return mergeCodexModelContextCapabilities(
|
|
7544
|
+
await queryCodexAppServerModels(codexHome, _codexAccounts().launchFor(account.id)),
|
|
7545
|
+
readCodexRuntimeModelCatalog(codexHome),
|
|
7546
|
+
);
|
|
7547
|
+
},
|
|
7548
|
+
accept: (models) => Array.isArray(models) && models.length > 0,
|
|
7549
|
+
// The Assistant's catalog is built from the default account's list: when that list changes, its copy is
|
|
7550
|
+
// dropped and rebuilt (which tells the open windows).
|
|
7551
|
+
onChange: (key) => {
|
|
7552
|
+
try { if (JSON.parse(key)[0] === 'default') _assistantCatalog?.refresh?.().catch(() => {}); } catch {}
|
|
7553
|
+
},
|
|
7554
|
+
log: (message) => console.warn(message),
|
|
7555
|
+
});
|
|
7556
|
+
|
|
7557
|
+
/**
|
|
7558
|
+
* A Codex account changed (sign-in, mode, removal) or its config was saved (`accountId` null: every
|
|
7559
|
+
* account): its kept lists are asked for again in the background, a removed account's are dropped.
|
|
7560
|
+
*/
|
|
7561
|
+
function codexModelListsChanged(accountId = null, { removed = false } = {}) {
|
|
7562
|
+
for (const key of _codexModelListCache.keys()) {
|
|
7563
|
+
let id = null;
|
|
7564
|
+
try { id = JSON.parse(key)[0]; } catch { continue; }
|
|
7565
|
+
if (accountId && id !== accountId) continue;
|
|
7566
|
+
if (removed) _codexModelListCache.invalidate(key);
|
|
7567
|
+
else _codexModelListCache.markStale(key);
|
|
7568
|
+
}
|
|
7569
|
+
if (!removed) scheduleLaunchRefresh('codex-models');
|
|
7570
|
+
}
|
|
7378
7571
|
|
|
7379
7572
|
// `launch` (lib/codex-accounts.js launchFor()): the list is read under the account's own credential,
|
|
7380
7573
|
// with no inherited one, as its sessions would run (in API-key mode, the key it holds over stdin).
|
|
@@ -7382,7 +7575,8 @@ async function queryCodexAppServerModels(codexHome = CODEX_DEFAULT_HOME, launch
|
|
|
7382
7575
|
ensureCanonicalCodexConfig({ strict: true, codexHome: launch?.home || codexHome });
|
|
7383
7576
|
// The Codex the resolver found (Settings' path first); a command line in Settings ("wsl codex") as written.
|
|
7384
7577
|
let codexBin = configuredCommandLine('codex') || getCodexBin();
|
|
7385
|
-
|
|
7578
|
+
// Without an account's launch plan, still the Windows sandbox the Admin sandbox setting allows.
|
|
7579
|
+
const args = [...(launch?.ok ? codexAppServerArgs(launch) : codexSandboxArgs()), 'app-server'];
|
|
7386
7580
|
if (!/\s/.test(codexBin) && /\.js$/i.test(codexBin)) {
|
|
7387
7581
|
args.unshift(codexBin);
|
|
7388
7582
|
codexBin = process.execPath;
|
|
@@ -7501,22 +7695,11 @@ app.get('/api/codex/models', async (req, res) => {
|
|
|
7501
7695
|
const accountId = String(req.query?.accountId || 'default');
|
|
7502
7696
|
const account = findCodexAccount(accountId);
|
|
7503
7697
|
if (!account) return res.status(404).json({ ok: false, error: `Codex account not found: ${accountId}`, models: [] });
|
|
7504
|
-
|
|
7698
|
+
// The kept list at once (read again in the background when old); a first read, another sign-in or
|
|
7699
|
+
// binary, or ?refresh=1 waits for the account's app-server.
|
|
7505
7700
|
const force = req.query?.refresh === '1';
|
|
7506
|
-
const cached = _codexModelListCache.
|
|
7507
|
-
|
|
7508
|
-
return res.json({ ok: true, source: 'cache', models: cached.models });
|
|
7509
|
-
}
|
|
7510
|
-
const models = mergeCodexModelContextCapabilities(
|
|
7511
|
-
await queryCodexAppServerModels(codexHome, _codexAccounts().launchFor(account.id)),
|
|
7512
|
-
readCodexRuntimeModelCatalog(codexHome),
|
|
7513
|
-
);
|
|
7514
|
-
// The Assistant's catalog is built from the default account's list: when that list
|
|
7515
|
-
// changes, its copy is dropped and rebuilt (which tells the open windows).
|
|
7516
|
-
const listChanged = codexHome === CODEX_DEFAULT_HOME && !!cached && JSON.stringify(cached.models) !== JSON.stringify(models);
|
|
7517
|
-
_codexModelListCache.set(codexHome, { at: Date.now(), models });
|
|
7518
|
-
if (listChanged) _assistantCatalog?.refresh?.().catch(() => {});
|
|
7519
|
-
res.json({ ok: true, source: 'codex-app-server', models });
|
|
7701
|
+
const { value: models, cached } = await _codexModelListCache.read(codexModelListKey(account.id), { refresh: force });
|
|
7702
|
+
res.json({ ok: true, source: cached ? 'cache' : 'codex-app-server', models: Array.isArray(models) ? models : [] });
|
|
7520
7703
|
} catch (err) {
|
|
7521
7704
|
res.json({ ok: false, source: 'fallback', error: err.message, models: [] });
|
|
7522
7705
|
}
|
|
@@ -7552,6 +7735,12 @@ let _ocpBaseUrl = () => `http://127.0.0.1:${_ocpPort}`;
|
|
|
7552
7735
|
let _ocpStarting = false;
|
|
7553
7736
|
let _ocpReady = false;
|
|
7554
7737
|
let _ocpVersion = null;
|
|
7738
|
+
// The OpenCode major the shared serve speaks (1 or 2; null until one answered),
|
|
7739
|
+
// the password SynaBun gave the OpenCode 2 serve it started, and the sentence
|
|
7740
|
+
// that says why OpenCode cannot be used (shown by the panel and the tray).
|
|
7741
|
+
let _ocpProtocol = null;
|
|
7742
|
+
let _ocpPassword = null;
|
|
7743
|
+
let _ocpProblem = null;
|
|
7555
7744
|
let _ocpHistoryClearInProgress = false;
|
|
7556
7745
|
|
|
7557
7746
|
// ── OpenCode V2 (clean-slate sidepanel) — client set & bridge ──
|
|
@@ -7559,9 +7748,18 @@ const _ocpV2WsClients = new Set(); // all connected /ws/opencode-v2 clients
|
|
|
7559
7748
|
let _ocpV2ListenerInstalled = false;
|
|
7560
7749
|
let _ocpV2ClientSeq = 0;
|
|
7561
7750
|
const OCP_MANAGED_PID_PATH = resolve(DATA_HOME, 'data', 'opencode-managed.pid');
|
|
7751
|
+
// Every OpenCode serve SynaBun starts, on record while it runs (lib/opencode-serve-records.js): what a crash left
|
|
7752
|
+
// running is found by these records alone, and stopped only when it is provably SynaBun's and nothing uses it.
|
|
7753
|
+
// Process state of this machine, so outside data/ (no backup or restore carries it).
|
|
7754
|
+
const opencodeServeRecords = createServeRecords({
|
|
7755
|
+
dir: resolve(DATA_HOME, 'runtime', 'opencode-serves'),
|
|
7756
|
+
log: (message) => console.log(`[opencode] ${message}`),
|
|
7757
|
+
});
|
|
7758
|
+
migrateOpencodeManagedPid();
|
|
7562
7759
|
let _ocpBootReconcileTried = false;
|
|
7563
7760
|
let _ocpLastBootAt = 0; // Date.now() when serve process last became ready
|
|
7564
7761
|
let _ocpAuthReconciling = false; // prevent concurrent reconciliation
|
|
7762
|
+
let _ocpAuthStaleNoted = 0; // the boot whose server SynaBun said it would not restart (once per boot)
|
|
7565
7763
|
|
|
7566
7764
|
// ── Verbose trace logging (opt-in: OCP_VERBOSE=1 to enable) ──
|
|
7567
7765
|
const OCP_VERBOSE = process.env.OCP_VERBOSE === '1';
|
|
@@ -7626,24 +7824,95 @@ function persistMcpToOpencodeConfig(name, mcpConfig) {
|
|
|
7626
7824
|
console.log(`[opencode] Persisted MCP server "${name}" to ${configPath}`);
|
|
7627
7825
|
}
|
|
7628
7826
|
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
} catch {}
|
|
7634
|
-
|
|
7827
|
+
// opencode-managed.pid is where an older SynaBun kept its shared serve: a bare pid, or a record without the token,
|
|
7828
|
+
// machine, user, boot and owner every record holds now. Neither can ever prove which process it names, so neither
|
|
7829
|
+
// is ever acted on: the file goes, and a serve it named that still runs is left running.
|
|
7830
|
+
function migrateOpencodeManagedPid() {
|
|
7831
|
+
try { unlinkSync(OCP_MANAGED_PID_PATH); } catch {}
|
|
7832
|
+
}
|
|
7833
|
+
|
|
7834
|
+
// Every serve SynaBun starts is on record from its spawn until its process has ended (lib/opencode-serve-records.js):
|
|
7835
|
+
// the process SynaBun spawned at once, under the token SynaBun put in its environment (SERVE_TOKEN_ENV); and, once
|
|
7836
|
+
// something answers on its port, the process that answers when the spawned one started it (proved through the
|
|
7837
|
+
// parent chain). No other process is ever put on record. `answering()` says whether the serve that answers on the
|
|
7838
|
+
// port is this start's: 'ours', 'foreign' (another server took the port: a start collision, which is left running and
|
|
7839
|
+
// never recorded) or 'unknown' (the OS could not say who listens: only the spawned process stays on record). The
|
|
7840
|
+
// spawned process's record goes with its exit, which Node has seen; a process it started that outlives it stays on
|
|
7841
|
+
// record until it has provably ended. Every serve SynaBun starts comes through here: the shared serve, the isolated
|
|
7842
|
+
// serves, the completion serve.
|
|
7843
|
+
function trackOpencodeServe(proc, { kind, id = null, port, token }) {
|
|
7844
|
+
const pid = Number(proc?.pid) || null;
|
|
7845
|
+
const exited = () => !proc || proc.exitCode != null || proc.signalCode != null;
|
|
7846
|
+
const spawned = pid ? opencodeServeRecords.record({ kind, id, port, pid, token }).catch(() => null) : Promise.resolve(null);
|
|
7847
|
+
let started = Promise.resolve(null);
|
|
7848
|
+
const forget = () => {
|
|
7849
|
+
spawned.then((record) => { if (record) opencodeServeRecords.remove(record); }).catch(() => {});
|
|
7850
|
+
started.then((record) => { if (record && opencodeServeRecords.verifySync(record) === 'gone') opencodeServeRecords.remove(record); }).catch(() => {});
|
|
7851
|
+
};
|
|
7852
|
+
if (exited()) forget();
|
|
7853
|
+
else proc.once?.('exit', forget);
|
|
7854
|
+
return {
|
|
7855
|
+
spawned,
|
|
7856
|
+
async answering() {
|
|
7857
|
+
await spawned;
|
|
7858
|
+
const who = await opencodeServeRecords.answering({ port, pid, exited });
|
|
7859
|
+
if (who.state !== 'ours') return who.state;
|
|
7860
|
+
// Read again now that it answers (its login shell has exec'd OpenCode by now), and the process that answers
|
|
7861
|
+
// when the spawned one started it.
|
|
7862
|
+
if (!exited()) await opencodeServeRecords.record({ kind, id, port, pid, token }).catch(() => null);
|
|
7863
|
+
if (who.pid !== pid) started = opencodeServeRecords.record({ kind, id, port, pid: who.pid, token }).catch(() => null);
|
|
7864
|
+
await started;
|
|
7865
|
+
return 'ours';
|
|
7866
|
+
},
|
|
7867
|
+
};
|
|
7635
7868
|
}
|
|
7636
7869
|
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7870
|
+
// Look for the OpenCode serves an earlier SynaBun left running (lib/opencode-serve-records.js), when SynaBun
|
|
7871
|
+
// starts and before a port is picked for a serve: one look at a time, shared by everyone who waits for it.
|
|
7872
|
+
function sweepLeftoverOpencodeServes() {
|
|
7873
|
+
return opencodeServeRecords.sweep().then((result) => {
|
|
7874
|
+
if (result.stopped.length) console.log(`[opencode] stopped ${result.stopped.length} serve(s) an earlier SynaBun left running: ${result.stopped.map((r) => `${r.kind} on port ${r.port} (pid ${r.pid})`).join(', ')}`);
|
|
7875
|
+
return result;
|
|
7876
|
+
}, () => null);
|
|
7877
|
+
}
|
|
7878
|
+
|
|
7879
|
+
// SynaBun's shared serve, on record (kind 'shared'), on `port` when one is named, the newest first: only a record
|
|
7880
|
+
// this server may act on (lib/opencode-serve-records.js claimable): written on this machine, for this user, in this
|
|
7881
|
+
// boot, and its own or one whose SynaBun has positively ended. Never another live SynaBun's (a data home two servers
|
|
7882
|
+
// share), one whose owner was never recorded or cannot be read now, or one of another machine or user: their serves
|
|
7883
|
+
// are servers SynaBun did not start, as far as this server is concerned.
|
|
7884
|
+
function readOpencodeManagedRecord({ port = null } = {}) {
|
|
7885
|
+
return opencodeServeRecords.list({ kind: 'shared', port }).find((record) => opencodeServeRecords.claimable(record)) || null;
|
|
7886
|
+
}
|
|
7887
|
+
|
|
7888
|
+
// A record goes once its process has provably ended (or its pid is another process's by now); one that cannot be
|
|
7889
|
+
// read now may still run, and stays.
|
|
7890
|
+
function clearOpencodeManagedPid(record = readOpencodeManagedRecord()) {
|
|
7891
|
+
if (record && opencodeServeRecords.verifySync(record) === 'gone') opencodeServeRecords.remove(record);
|
|
7643
7892
|
}
|
|
7644
7893
|
|
|
7645
|
-
|
|
7646
|
-
|
|
7894
|
+
// The recorded serves this server may act on while the OS still reports each as that very process: the same pid
|
|
7895
|
+
// and start time, the user's own, the token of its spawn in its environment (and, given a port, while it listens
|
|
7896
|
+
// there). Only such a process, or SynaBun's own child, is ever stopped.
|
|
7897
|
+
function verifiedOpencodeManagedProcesses({ port = null } = {}) {
|
|
7898
|
+
return opencodeServeRecords.list({ kind: 'shared', port }).filter((record) => record.pid !== process.pid
|
|
7899
|
+
&& opencodeServeRecords.claimable(record)
|
|
7900
|
+
&& opencodeServeRecords.verifySync(record) === 'same'
|
|
7901
|
+
&& (port == null || process.platform === 'win32' || listPortListenerPids(port).includes(record.pid)));
|
|
7902
|
+
}
|
|
7903
|
+
|
|
7904
|
+
function verifiedOpencodeManagedProcess({ port = null } = {}) {
|
|
7905
|
+
return verifiedOpencodeManagedProcesses({ port })[0] || null;
|
|
7906
|
+
}
|
|
7907
|
+
|
|
7908
|
+
// Signal SynaBun's recorded serve, checked once more right before: a pid that is another process by now, or one
|
|
7909
|
+
// that cannot be read, is left alone. Windows ends the serve's tree with taskkill.
|
|
7910
|
+
function signalOwnedOpencodeProcess(record, signal) {
|
|
7911
|
+
if (!record || opencodeServeRecords.verifySync(record) !== 'same') return false;
|
|
7912
|
+
if (process.platform === 'win32') {
|
|
7913
|
+
try { spawn('taskkill', ['/pid', String(record.pid), '/T', '/F'], { shell: true, windowsHide: true, stdio: 'ignore' }); return true; } catch { return false; }
|
|
7914
|
+
}
|
|
7915
|
+
return signalPids([record.pid], signal).length > 0;
|
|
7647
7916
|
}
|
|
7648
7917
|
|
|
7649
7918
|
function parseOpencodeLogTimestamp(fileName) {
|
|
@@ -7686,7 +7955,9 @@ function shouldReconcileOpencodeForStoredAuth() {
|
|
|
7686
7955
|
return false;
|
|
7687
7956
|
}
|
|
7688
7957
|
|
|
7689
|
-
// Runtime auth reconciliation: restart serve if auth.json changed since last boot
|
|
7958
|
+
// Runtime auth reconciliation: restart serve if auth.json changed since last boot. Automatic, so only a serve
|
|
7959
|
+
// SynaBun can prove is its own is restarted (its child, or its recorded serve that is still that process); a
|
|
7960
|
+
// server someone else runs is used as it is, and said once.
|
|
7690
7961
|
async function reconcileAuthIfStale() {
|
|
7691
7962
|
if (_ocpAuthReconciling || !_ocpReady || !_ocpLastBootAt) return false;
|
|
7692
7963
|
try {
|
|
@@ -7694,6 +7965,13 @@ async function reconcileAuthIfStale() {
|
|
|
7694
7965
|
if (!existsSync(authPath)) return false;
|
|
7695
7966
|
const authMtime = statSync(authPath).mtimeMs;
|
|
7696
7967
|
if (authMtime <= _ocpLastBootAt) return false;
|
|
7968
|
+
if (!_ocpProc && !verifiedOpencodeManagedProcess({ port: _ocpPort })) {
|
|
7969
|
+
if (_ocpAuthStaleNoted !== _ocpLastBootAt) {
|
|
7970
|
+
_ocpAuthStaleNoted = _ocpLastBootAt;
|
|
7971
|
+
console.warn(`[opencode] auth.json changed, but the OpenCode server on port ${_ocpPort} was not started by SynaBun: it is not restarted (it shows the new credentials once whoever runs it restarts it)`);
|
|
7972
|
+
}
|
|
7973
|
+
return false;
|
|
7974
|
+
}
|
|
7697
7975
|
_ocpAuthReconciling = true;
|
|
7698
7976
|
console.log('[opencode] auth.json modified after last boot — restarting to pick up new credentials');
|
|
7699
7977
|
await takeoverOpencodeServer();
|
|
@@ -7717,15 +7995,7 @@ function listPortListenerPids(port) {
|
|
|
7717
7995
|
return [];
|
|
7718
7996
|
}
|
|
7719
7997
|
|
|
7720
|
-
|
|
7721
|
-
if (process.platform === 'win32') return [];
|
|
7722
|
-
try {
|
|
7723
|
-
const out = execSync('pgrep -f "opencode.*serve"', { encoding: 'utf8' }).trim();
|
|
7724
|
-
return out.split('\n').filter(Boolean).map((pid) => Number(pid)).filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid);
|
|
7725
|
-
} catch {}
|
|
7726
|
-
return [];
|
|
7727
|
-
}
|
|
7728
|
-
|
|
7998
|
+
// The raw signal. Only signalOwnedOpencodeProcess calls it, for a process it has just checked is SynaBun's.
|
|
7729
7999
|
function signalPids(pids, signal) {
|
|
7730
8000
|
const targeted = [];
|
|
7731
8001
|
for (const pid of pids) {
|
|
@@ -7737,19 +8007,6 @@ function signalPids(pids, signal) {
|
|
|
7737
8007
|
return targeted;
|
|
7738
8008
|
}
|
|
7739
8009
|
|
|
7740
|
-
async function waitForOpencodeShutdown(timeoutMs = 6000) {
|
|
7741
|
-
const deadline = Date.now() + timeoutMs;
|
|
7742
|
-
while (Date.now() < deadline) {
|
|
7743
|
-
const listenerPids = listPortListenerPids(_ocpPort);
|
|
7744
|
-
if (!listenerPids.length) {
|
|
7745
|
-
const healthy = await checkOpencodeHealth();
|
|
7746
|
-
if (!healthy) return true;
|
|
7747
|
-
}
|
|
7748
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
7749
|
-
}
|
|
7750
|
-
return false;
|
|
7751
|
-
}
|
|
7752
|
-
|
|
7753
8010
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
7754
8011
|
// OpenCode V2 — clean-slate broadcast bridge & WS handler
|
|
7755
8012
|
// Pass-through: forwards opencodeV2Client envelopes verbatim to /ws/opencode-v2
|
|
@@ -7784,7 +8041,15 @@ const OPENCODE_HISTORY_WRITE_TYPES = new Set([
|
|
|
7784
8041
|
...OPENCODE_V2_WS_MUTATION_TYPES,
|
|
7785
8042
|
]);
|
|
7786
8043
|
|
|
8044
|
+
// Set while the provider cache announces its own change (it is not a reason to read the list again).
|
|
8045
|
+
let _announcingOpencodeProviders = false;
|
|
8046
|
+
|
|
7787
8047
|
function broadcastToOpencodeV2Clients(data, { nativeRunId = null } = {}) {
|
|
8048
|
+
// The kept provider list follows what the panels are told: a credential change, the serve becoming
|
|
8049
|
+
// ready, a catalog event (the same ones the panel re-reads on, ocp-v2-status-logic.js).
|
|
8050
|
+
if (data?.type === 'providers:changed' && !_announcingOpencodeProviders) noteOpencodeProvidersChanged({ announce: true });
|
|
8051
|
+
else if (data?.type === 'server:status' && data.status === 'ready') noteOpencodeProvidersChanged();
|
|
8052
|
+
else if (data?.type === 'event' && /^(?:catalog\.updated|models-dev\.refreshed|integration\.)/.test(String(data.eventType || ''))) noteOpencodeProvidersChanged();
|
|
7788
8053
|
// OpenCode 1.18 mirrors every bus event as a `sync` envelope; the panel acts
|
|
7789
8054
|
// on the bus event, so relaying the mirror only doubles the traffic.
|
|
7790
8055
|
if (!shouldRelayOpencodeV2Message(data)) return;
|
|
@@ -7826,6 +8091,7 @@ function installOpencodeV2Listener() {
|
|
|
7826
8091
|
type: 'server:status',
|
|
7827
8092
|
status: event?.status || 'unknown',
|
|
7828
8093
|
version: _ocpVersion,
|
|
8094
|
+
protocol: _ocpProtocol,
|
|
7829
8095
|
port: event?.port || _ocpPort,
|
|
7830
8096
|
managed: !!_ocpProc,
|
|
7831
8097
|
});
|
|
@@ -7862,6 +8128,78 @@ function allocOpencodeIsoPort() {
|
|
|
7862
8128
|
return null;
|
|
7863
8129
|
}
|
|
7864
8130
|
|
|
8131
|
+
// OpenCode 2 cannot open its database while another serve of the same data
|
|
8132
|
+
// folder is opening it ("database is locked", and the serve exits): SynaBun
|
|
8133
|
+
// starts its OpenCode 2 serves one at a time, each until it answers. Every
|
|
8134
|
+
// start goes through here: the shared serve (ensureOpencodeServer), the
|
|
8135
|
+
// isolated serves of panel sessions, loops, workers and brains
|
|
8136
|
+
// (ensureIsolatedServe) and the editor's completion serve
|
|
8137
|
+
// (lib/editor-completion-opencode.js `startOneAtATime`). A start holds its turn
|
|
8138
|
+
// from the spawn until its serve answers, or until a failed start's process is
|
|
8139
|
+
// gone. OpenCode 1 serves start as they always did, side by side.
|
|
8140
|
+
let _ocpStartChain = Promise.resolve();
|
|
8141
|
+
function oneOpencodeStartAtATime(protocol, run) {
|
|
8142
|
+
if (protocol === 1) return run();
|
|
8143
|
+
const next = _ocpStartChain.then(run, run);
|
|
8144
|
+
_ocpStartChain = next.then(() => {}, () => {});
|
|
8145
|
+
return next;
|
|
8146
|
+
}
|
|
8147
|
+
|
|
8148
|
+
// Resolves once a child process has exited (true), or after `timeoutMs` (false): a failed start keeps its turn
|
|
8149
|
+
// until its process has let go of the database.
|
|
8150
|
+
function opencodeChildExited(proc, timeoutMs = 3000) {
|
|
8151
|
+
if (!proc || proc.exitCode != null || proc.signalCode != null) return Promise.resolve(true);
|
|
8152
|
+
return new Promise((done) => {
|
|
8153
|
+
const timer = setTimeout(() => done(false), timeoutMs);
|
|
8154
|
+
timer.unref?.();
|
|
8155
|
+
proc.once?.('exit', () => { clearTimeout(timer); done(true); });
|
|
8156
|
+
});
|
|
8157
|
+
}
|
|
8158
|
+
|
|
8159
|
+
// A start that failed ends its own process before its turn is over. It was sent SIGTERM (taskkill /T /F on Windows);
|
|
8160
|
+
// one still running after `timeoutMs` gets SIGKILL through its handle, which only ever reaches SynaBun's own child
|
|
8161
|
+
// (Node stops signalling a child once it has seen its exit, so never a pid another program has by now), and is
|
|
8162
|
+
// waited for once more. True once its exit is seen. Its record stays until then (trackOpencodeServe removes it on
|
|
8163
|
+
// that exit): one that outlives even SIGKILL stays on record for the look after this server has ended.
|
|
8164
|
+
async function endOpencodeChild(proc, timeoutMs = 3000) {
|
|
8165
|
+
if (await opencodeChildExited(proc, timeoutMs)) return true;
|
|
8166
|
+
console.warn(`[opencode] serve process ${proc.pid} did not end within ${Math.round(timeoutMs / 1000)} s of SIGTERM: SIGKILL`);
|
|
8167
|
+
try {
|
|
8168
|
+
if (IS_WIN && proc.pid) spawn('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).on?.('error', () => {});
|
|
8169
|
+
else proc.kill('SIGKILL');
|
|
8170
|
+
} catch {}
|
|
8171
|
+
if (await opencodeChildExited(proc, timeoutMs)) return true;
|
|
8172
|
+
console.warn(`[opencode] serve process ${proc.pid} still runs after SIGKILL: it stays on record`);
|
|
8173
|
+
return false;
|
|
8174
|
+
}
|
|
8175
|
+
|
|
8176
|
+
function killIsolatedServeProcess(proc) {
|
|
8177
|
+
try {
|
|
8178
|
+
if (proc && !proc.killed && proc.exitCode == null) {
|
|
8179
|
+
if (IS_WIN && proc.pid) {
|
|
8180
|
+
spawn('taskkill', ['/pid', String(proc.pid), '/T', '/F'], {
|
|
8181
|
+
shell: true,
|
|
8182
|
+
windowsHide: true,
|
|
8183
|
+
stdio: 'ignore',
|
|
8184
|
+
});
|
|
8185
|
+
} else {
|
|
8186
|
+
proc.kill('SIGTERM');
|
|
8187
|
+
}
|
|
8188
|
+
}
|
|
8189
|
+
} catch {}
|
|
8190
|
+
}
|
|
8191
|
+
|
|
8192
|
+
// An isolated start that is no longer wanted ends what it started. While its entry is still the one under its
|
|
8193
|
+
// term id, that is a stop like any other; a start that was cancelled (stopIsolatedServe) or replaced (a scoped
|
|
8194
|
+
// restart under the same term id) meanwhile ends only its own process and client: the replacement keeps its
|
|
8195
|
+
// bindings and its config folder, which has the same path.
|
|
8196
|
+
function endIsolatedStart(termId, entry) {
|
|
8197
|
+
if (_ocpIsoServes.get(termId) === entry) { stopIsolatedServe(termId); return; }
|
|
8198
|
+
try { entry.unsub?.(); } catch {}
|
|
8199
|
+
try { entry.client?.stop(); } catch {}
|
|
8200
|
+
killIsolatedServeProcess(entry.proc);
|
|
8201
|
+
}
|
|
8202
|
+
|
|
7865
8203
|
// Sidepanel variant of setupOpencodeLoopConfig: inject the terminal identity and
|
|
7866
8204
|
// pinned profile into the MCP env (no browser pins → the child acquires its own
|
|
7867
8205
|
// tab) and do not add the loop permission bypass — the panel is user-supervised.
|
|
@@ -7978,13 +8316,18 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, k
|
|
|
7978
8316
|
for (let i = 0; i < 60 && !entry.ready; i++) await new Promise(r => setTimeout(r, 250));
|
|
7979
8317
|
return entry.ready ? entry : null;
|
|
7980
8318
|
}
|
|
7981
|
-
const port = allocOpencodeIsoPort();
|
|
7982
|
-
if (port == null) { console.warn('[ocp-iso] no free port for isolated serve'); return null; }
|
|
7983
8319
|
let xdgRoot;
|
|
7984
8320
|
try { xdgRoot = requestedXdgRoot || setupOpencodeSidepanelConfig(termId, requestedProfile); }
|
|
7985
8321
|
catch (err) { console.warn(`[ocp-iso] config setup failed: ${err.message}`); return null; }
|
|
7986
|
-
|
|
8322
|
+
// Registered before anything is waited for: a second start for the same term id waits for this one.
|
|
8323
|
+
entry = { termId, port: null, xdgRoot, mcpProfile: requestedProfile, mcpProfileRefreshFailed: false, proc: null, client: null, ready: false, starting: true, keepAlive: !!keepAlive, unsub: null, sessions: new Set(), lastUsed: Date.now() };
|
|
7987
8324
|
_ocpIsoServes.set(termId, entry);
|
|
8325
|
+
// What an earlier SynaBun left running goes first (its ports with it); then the port is picked.
|
|
8326
|
+
await sweepLeftoverOpencodeServes();
|
|
8327
|
+
if (_ocpIsoServes.get(termId) !== entry) return null;
|
|
8328
|
+
let port = allocOpencodeIsoPort();
|
|
8329
|
+
if (port == null) { console.warn('[ocp-iso] no free port for isolated serve'); endIsolatedStart(termId, entry); return null; }
|
|
8330
|
+
entry.port = port;
|
|
7988
8331
|
try {
|
|
7989
8332
|
const isolatedEnv = {
|
|
7990
8333
|
...process.env,
|
|
@@ -8003,33 +8346,85 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, k
|
|
|
8003
8346
|
};
|
|
8004
8347
|
delete isolatedEnv.CLAUDECODE;
|
|
8005
8348
|
if (!brainCredential) delete isolatedEnv.SYNABUN_BRAIN_TOKEN;
|
|
8006
|
-
|
|
8007
|
-
|
|
8008
|
-
|
|
8009
|
-
...launch.options,
|
|
8010
|
-
detached: false, shell: false,
|
|
8011
|
-
});
|
|
8012
|
-
entry.proc = proc;
|
|
8349
|
+
// The OpenCode major decides how the serve starts (OpenCode 2: a password of
|
|
8350
|
+
// its own) and how it is spoken to; one SynaBun does not speak never starts.
|
|
8351
|
+
const detected = await detectOpencodeProtocol();
|
|
8013
8352
|
const tag = termId.slice(-8);
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8353
|
+
if (detected.major != null && !detected.supported) { console.warn(`[ocp-iso] serve ${tag}: ${detected.reason}`); endIsolatedStart(termId, entry); return null; }
|
|
8354
|
+
// This start is wanted while its entry is the one under its term id: a stop cancels it, a scoped restart replaces it.
|
|
8355
|
+
const wanted = () => _ocpIsoServes.get(termId) === entry;
|
|
8356
|
+
let check = null;
|
|
8357
|
+
let started = 'cancelled';
|
|
8358
|
+
// Another server that takes the port while this serve boots is a start collision: it is left running (and never
|
|
8359
|
+
// put on record), this start's own process is ended, and the start tries the next free port (three at most).
|
|
8360
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
8361
|
+
started = await oneOpencodeStartAtATime(detected.protocol, async () => {
|
|
8362
|
+
// Its turn may come after it was cancelled or replaced: then nothing is started for it.
|
|
8363
|
+
if (!wanted()) return 'cancelled';
|
|
8364
|
+
// A token of this spawn in its environment: what tells its processes from any other (lib/opencode-serve-records.js).
|
|
8365
|
+
const token = opencodeServeRecords.newToken();
|
|
8366
|
+
const launch = opencodeServeLaunch(port, { env: { ...isolatedEnv, [SERVE_TOKEN_ENV]: token }, protocol: detected.protocol });
|
|
8367
|
+
const proc = spawn(launch.command, launch.args, {
|
|
8368
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
8369
|
+
...launch.options,
|
|
8370
|
+
detached: false, shell: false,
|
|
8371
|
+
});
|
|
8372
|
+
entry.proc = proc;
|
|
8373
|
+
entry.password = launch.password;
|
|
8374
|
+
// On record from its spawn: a crash of SynaBun from here on does not leave it running for good.
|
|
8375
|
+
const tracked = trackOpencodeServe(proc, { kind: 'isolated', id: termId, port, token });
|
|
8376
|
+
// While it boots, its start sees an exit itself (another server on the port is a collision); afterwards
|
|
8377
|
+
// its exit ends the serve.
|
|
8378
|
+
let booting = true;
|
|
8379
|
+
proc.stdout?.on('data', (d) => console.log(`[ocp-iso:${tag}:out] ${String(d).trim()}`));
|
|
8380
|
+
proc.stderr?.on('data', (d) => console.log(`[ocp-iso:${tag}:err] ${String(d).trim()}`));
|
|
8381
|
+
proc.on('exit', (code) => {
|
|
8382
|
+
console.log(`[ocp-iso] serve ${tag} exited code=${code}`);
|
|
8383
|
+
if (!booting && _ocpIsoServes.get(termId)?.proc === proc) stopIsolatedServe(termId);
|
|
8384
|
+
});
|
|
8020
8385
|
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8386
|
+
// Healthy only when the right API answers JSON (an OpenCode 2 web page never is), and only from this start's
|
|
8387
|
+
// own process (the spawned one, or one it started).
|
|
8388
|
+
let outcome = 'unhealthy';
|
|
8389
|
+
for (let i = 0; i < 60; i++) {
|
|
8390
|
+
await new Promise(r => setTimeout(r, 250));
|
|
8391
|
+
if (!wanted()) { outcome = 'cancelled'; break; } // cancelled or replaced mid-boot
|
|
8392
|
+
check = await checkOpencodeServer({ port, protocol: detected.protocol, password: launch.password, timeoutMs: 3000 });
|
|
8393
|
+
if (check.ok) {
|
|
8394
|
+
if (await tracked.answering() !== 'foreign') { booting = false; return 'ready'; }
|
|
8395
|
+
outcome = 'collision';
|
|
8396
|
+
break;
|
|
8397
|
+
}
|
|
8398
|
+
if (proc.exitCode != null || proc.signalCode != null) { outcome = 'exited'; break; }
|
|
8399
|
+
if (['mismatch', 'html', 'locked'].includes(check.state) && i >= 3) break;
|
|
8400
|
+
}
|
|
8401
|
+
// Not started, no longer wanted, or another server's port: this start's own process is gone before the next
|
|
8402
|
+
// start may open the database (SIGTERM, then SIGKILL), and on record until its exit is seen. Its exit no
|
|
8403
|
+
// longer speaks for the entry.
|
|
8404
|
+
if (entry.proc === proc) entry.proc = null;
|
|
8405
|
+
killIsolatedServeProcess(proc);
|
|
8406
|
+
await endOpencodeChild(proc);
|
|
8407
|
+
return outcome;
|
|
8408
|
+
});
|
|
8409
|
+
if (started !== 'collision' || !wanted() || attempt === 3) break;
|
|
8410
|
+
const taken = port;
|
|
8411
|
+
port = allocOpencodeIsoPort();
|
|
8412
|
+
if (port == null) { console.warn(`[ocp-iso] serve ${tag}: another server answers on port ${taken}, and no other port is free`); break; }
|
|
8413
|
+
entry.port = port;
|
|
8414
|
+
console.warn(`[ocp-iso] serve ${tag}: another server answers on port ${taken}; it is left running, and this serve starts on port ${port}`);
|
|
8415
|
+
}
|
|
8416
|
+
if (started !== 'ready') {
|
|
8417
|
+
if (started === 'unhealthy') console.warn(`[ocp-iso] serve ${tag} not healthy within 15s${check && check.state !== 'down' ? `: ${opencodeProblemText({ check, port })}` : ''}`);
|
|
8418
|
+
if (started === 'exited') console.warn(`[ocp-iso] serve ${tag} exited before it answered`);
|
|
8419
|
+
if (started === 'collision') console.warn(`[ocp-iso] serve ${tag}: another server took every port it tried`);
|
|
8420
|
+
endIsolatedStart(termId, entry);
|
|
8421
|
+
return null;
|
|
8029
8422
|
}
|
|
8030
|
-
if (!
|
|
8423
|
+
if (!wanted()) { endIsolatedStart(termId, entry); return null; }
|
|
8424
|
+
entry.protocol = check.protocol;
|
|
8425
|
+
entry.version = check.version || null;
|
|
8031
8426
|
|
|
8032
|
-
const client = opencodeV2Client.createClientInstance({ port });
|
|
8427
|
+
const client = opencodeV2Client.createClientInstance({ port, protocol: entry.protocol, password: entry.password, version: entry.version || '' });
|
|
8033
8428
|
entry.client = client;
|
|
8034
8429
|
entry.unsub = client.onEvent((envelope) => {
|
|
8035
8430
|
if (!envelope) return;
|
|
@@ -8057,6 +8452,7 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, k
|
|
|
8057
8452
|
});
|
|
8058
8453
|
client.start();
|
|
8059
8454
|
await client.waitUntilConnected?.(10_000);
|
|
8455
|
+
if (!wanted()) { endIsolatedStart(termId, entry); return null; }
|
|
8060
8456
|
entry.ready = true;
|
|
8061
8457
|
entry.starting = false;
|
|
8062
8458
|
entry.lastUsed = Date.now();
|
|
@@ -8064,7 +8460,7 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, k
|
|
|
8064
8460
|
return entry;
|
|
8065
8461
|
} catch (err) {
|
|
8066
8462
|
console.warn(`[ocp-iso] spawn failed: ${err.message}`);
|
|
8067
|
-
|
|
8463
|
+
endIsolatedStart(termId, entry);
|
|
8068
8464
|
return null;
|
|
8069
8465
|
}
|
|
8070
8466
|
}
|
|
@@ -8083,19 +8479,7 @@ function stopIsolatedServe(termId, { preserveBindings = false } = {}) {
|
|
|
8083
8479
|
}
|
|
8084
8480
|
try { entry.unsub?.(); } catch {}
|
|
8085
8481
|
try { entry.client?.stop(); } catch {}
|
|
8086
|
-
|
|
8087
|
-
if (entry.proc && !entry.proc.killed) {
|
|
8088
|
-
if (IS_WIN && entry.proc.pid) {
|
|
8089
|
-
spawn('taskkill', ['/pid', String(entry.proc.pid), '/T', '/F'], {
|
|
8090
|
-
shell: true,
|
|
8091
|
-
windowsHide: true,
|
|
8092
|
-
stdio: 'ignore',
|
|
8093
|
-
});
|
|
8094
|
-
} else {
|
|
8095
|
-
entry.proc.kill('SIGTERM');
|
|
8096
|
-
}
|
|
8097
|
-
}
|
|
8098
|
-
} catch {}
|
|
8482
|
+
killIsolatedServeProcess(entry.proc);
|
|
8099
8483
|
try { if (entry.xdgRoot && existsSync(entry.xdgRoot)) rmSync(entry.xdgRoot, { recursive: true, force: true }); } catch {}
|
|
8100
8484
|
}
|
|
8101
8485
|
|
|
@@ -8318,7 +8702,7 @@ function handleOpencodeV2Ws(ws) {
|
|
|
8318
8702
|
}
|
|
8319
8703
|
if (type === 'identify') {
|
|
8320
8704
|
// Sent on every (re)connect: the panel gates features on this list.
|
|
8321
|
-
send({ type: 'capabilities', capabilities: opencodeV2WsCapabilities() });
|
|
8705
|
+
send({ type: 'capabilities', capabilities: opencodeV2WsCapabilities({ protocol: _ocpProtocol }) });
|
|
8322
8706
|
return;
|
|
8323
8707
|
}
|
|
8324
8708
|
// Canonical session id first (msg.sessionId is rewritten in place), then
|
|
@@ -8344,11 +8728,14 @@ function handleOpencodeV2Ws(ws) {
|
|
|
8344
8728
|
id,
|
|
8345
8729
|
ready,
|
|
8346
8730
|
version: _ocpVersion,
|
|
8731
|
+
// 1 or 2: the OpenCode API the shared serve speaks; `problem` why OpenCode cannot be used.
|
|
8732
|
+
protocol: _ocpProtocol,
|
|
8733
|
+
...(ready ? {} : { problem: _ocpProblem || null }),
|
|
8347
8734
|
sdkVersion: opencodeSdkVersion(),
|
|
8348
8735
|
port: _ocpPort,
|
|
8349
8736
|
managed: !!_ocpProc,
|
|
8350
8737
|
connected: opencodeV2Client.isConnected(),
|
|
8351
|
-
capabilities: opencodeV2WsCapabilities(),
|
|
8738
|
+
capabilities: opencodeV2WsCapabilities({ protocol: _ocpProtocol }),
|
|
8352
8739
|
});
|
|
8353
8740
|
break;
|
|
8354
8741
|
}
|
|
@@ -8359,11 +8746,14 @@ function handleOpencodeV2Ws(ws) {
|
|
|
8359
8746
|
// registered project. Reading the DB directly returns ALL sessions
|
|
8360
8747
|
// so the sidepanel resume dropdown and boot tab-restore can
|
|
8361
8748
|
// surface sessions outside SynaBun (every other registered project).
|
|
8749
|
+
// OpenCode 2 keeps them in other tables of the same file
|
|
8750
|
+
// (lib/opencode-db-schema.js); neither set of tables: the API.
|
|
8362
8751
|
const db = getOpencodeDb();
|
|
8363
|
-
|
|
8752
|
+
const flavor = db ? opencodeDbFlavor(db, _ocpProtocol) : null;
|
|
8753
|
+
if (db && flavor) {
|
|
8364
8754
|
try {
|
|
8365
8755
|
// msg.options: search / archived / roots / limit (see buildSessionListQuery).
|
|
8366
|
-
const query = buildSessionListQuery(msg.options || {});
|
|
8756
|
+
const query = buildSessionListQuery(msg.options || {}, { flavor });
|
|
8367
8757
|
const rows = db.prepare(query.sql).all(...query.params);
|
|
8368
8758
|
const sessions = rows.filter((row) => {
|
|
8369
8759
|
const run = activeNativeOpenCodeRunForSession(row.id);
|
|
@@ -8586,30 +8976,37 @@ function handleOpencodeV2Ws(ws) {
|
|
|
8586
8976
|
});
|
|
8587
8977
|
}
|
|
8588
8978
|
|
|
8979
|
+
// Healthy only when the API of the shared serve's major answers JSON
|
|
8980
|
+
// (lib/opencode-protocol.js checkOpencodeServer): OpenCode 2 answers every
|
|
8981
|
+
// OpenCode 1 path with its web app and status 200, which is never healthy.
|
|
8982
|
+
// `_ocpLastCheck` keeps the whole answer (what answered, and why it is not usable).
|
|
8983
|
+
let _ocpLastCheck = null;
|
|
8589
8984
|
async function checkOpencodeHealth() {
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
} catch {}
|
|
8598
|
-
return false;
|
|
8985
|
+
const check = await checkOpencodeServer({ port: _ocpPort, protocol: _ocpProtocol, password: _ocpPassword, timeoutMs: 3000 });
|
|
8986
|
+
_ocpLastCheck = check;
|
|
8987
|
+
if (check.ok) {
|
|
8988
|
+
_ocpVersion = check.version || null;
|
|
8989
|
+
if (check.protocol) _ocpProtocol = check.protocol;
|
|
8990
|
+
}
|
|
8991
|
+
return check.ok;
|
|
8599
8992
|
}
|
|
8600
8993
|
|
|
8994
|
+
// At the first start: a server already on the port that is SynaBun's own serve of an earlier run (its recorded
|
|
8995
|
+
// process, still that process) is taken over, and so is one SynaBun owns whose credentials are older than
|
|
8996
|
+
// auth.json. Automatic, so a server someone else runs is never stopped: it is used as it is.
|
|
8601
8997
|
async function reconcileOpencodeServerIfStale() {
|
|
8602
|
-
|
|
8603
|
-
const
|
|
8604
|
-
const
|
|
8605
|
-
|
|
8606
|
-
shouldReconcileOpencodeForStoredAuth()
|
|
8607
|
-
);
|
|
8608
|
-
if (!shouldReconcile) return false;
|
|
8998
|
+
if (_ocpBootReconcileTried) return false;
|
|
8999
|
+
const owned = verifiedOpencodeManagedProcess({ port: _ocpPort });
|
|
9000
|
+
const staleAuth = shouldReconcileOpencodeForStoredAuth();
|
|
9001
|
+
if (!owned && !staleAuth) return false;
|
|
8609
9002
|
_ocpBootReconcileTried = true;
|
|
9003
|
+
if (!owned) {
|
|
9004
|
+
console.warn(`[opencode] The OpenCode server on port ${_ocpPort} was not started by SynaBun: it is used as it is, not restarted (it may not have the newest provider credentials until whoever runs it restarts it)`);
|
|
9005
|
+
return false;
|
|
9006
|
+
}
|
|
8610
9007
|
_ocpReady = false;
|
|
8611
9008
|
_ocpStarting = false;
|
|
8612
|
-
console.log(`[opencode]
|
|
9009
|
+
console.log(`[opencode] SynaBun's own earlier server (pid ${owned.pid}) holds port ${_ocpPort}; taking it over`);
|
|
8613
9010
|
await takeoverOpencodeServer();
|
|
8614
9011
|
return true;
|
|
8615
9012
|
}
|
|
@@ -8641,6 +9038,65 @@ async function prewarmOpencodeMcpServers() {
|
|
|
8641
9038
|
}
|
|
8642
9039
|
}
|
|
8643
9040
|
|
|
9041
|
+
// Is something listening on 127.0.0.1:port? A TCP connect, so it answers on Windows too (no lsof there).
|
|
9042
|
+
function opencodePortAnswers(port, timeoutMs = 500) {
|
|
9043
|
+
return new Promise((resolveAnswer) => {
|
|
9044
|
+
const socket = netConnect({ host: '127.0.0.1', port });
|
|
9045
|
+
const done = (value) => { try { socket.destroy(); } catch {} resolveAnswer(value); };
|
|
9046
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
9047
|
+
socket.once('connect', () => done(true));
|
|
9048
|
+
socket.once('error', () => done(false));
|
|
9049
|
+
});
|
|
9050
|
+
}
|
|
9051
|
+
|
|
9052
|
+
// Stop SynaBun's own recorded serve (verifiedOpencodeManagedProcess) on a port: SIGTERM, then SIGKILL while it
|
|
9053
|
+
// is still there, each checked against the record right before (signalOwnedOpencodeProcess). True once the
|
|
9054
|
+
// port is free.
|
|
9055
|
+
async function stopOwnedOpencodeServe(owned, port) {
|
|
9056
|
+
signalOwnedOpencodeProcess(owned, 'SIGTERM');
|
|
9057
|
+
for (let i = 0; i < 24 && listPortListenerPids(port).length; i++) await new Promise((r) => setTimeout(r, 250));
|
|
9058
|
+
if (listPortListenerPids(port).includes(owned.pid)) signalOwnedOpencodeProcess(owned, 'SIGKILL');
|
|
9059
|
+
for (let i = 0; i < 12 && listPortListenerPids(port).length; i++) await new Promise((r) => setTimeout(r, 250));
|
|
9060
|
+
// Its record goes once it has provably ended; one that still runs, or cannot be read, stays on record for the next look.
|
|
9061
|
+
clearOpencodeManagedPid(owned);
|
|
9062
|
+
return !listPortListenerPids(port).length && !(await opencodePortAnswers(port));
|
|
9063
|
+
}
|
|
9064
|
+
|
|
9065
|
+
// The shared serve moves to one of the next free ports, never into the isolated serves' range (4120–4400):
|
|
9066
|
+
// from 4096, 4097 to 4119. `why` is said with the move. False when no port could be had.
|
|
9067
|
+
async function moveSharedOpencodePort(why) {
|
|
9068
|
+
for (let port = _ocpPort + 1; port <= _ocpPort + 23; port++) {
|
|
9069
|
+
if ((port >= 4120 && port <= 4400) || port > 65535) continue;
|
|
9070
|
+
if (listPortListenerPids(port).length || await opencodePortAnswers(port)) continue;
|
|
9071
|
+
console.warn(`[opencode] ${why} The shared OpenCode serve uses port ${port} instead.`);
|
|
9072
|
+
_ocpPort = port;
|
|
9073
|
+
return true;
|
|
9074
|
+
}
|
|
9075
|
+
return false;
|
|
9076
|
+
}
|
|
9077
|
+
|
|
9078
|
+
// The shared serve's port is held by a server SynaBun cannot use: an OpenCode
|
|
9079
|
+
// 2 whose password SynaBun does not hold, a web page, something else. SynaBun's
|
|
9080
|
+
// own serve from an earlier run (its recorded process, while the OS still
|
|
9081
|
+
// reports that very process) is stopped and the port reused; anything else,
|
|
9082
|
+
// and a recorded pid that is another process by now, is left running, and the
|
|
9083
|
+
// shared serve moves to another port.
|
|
9084
|
+
async function freeSharedOpencodePortIfHeld(check) {
|
|
9085
|
+
const owned = verifiedOpencodeManagedProcess({ port: _ocpPort });
|
|
9086
|
+
if (owned) {
|
|
9087
|
+
console.log(`[opencode] SynaBun's earlier serve (pid ${owned.pid}) holds port ${_ocpPort} and cannot be used (${check?.error || check?.state}); stopping it`);
|
|
9088
|
+
if (await stopOwnedOpencodeServe(owned, _ocpPort)) return true;
|
|
9089
|
+
}
|
|
9090
|
+
return moveSharedOpencodePort(opencodeProblemText({ check, port: _ocpPort }));
|
|
9091
|
+
}
|
|
9092
|
+
|
|
9093
|
+
// Tell every OpenCode panel that OpenCode cannot be used, and why.
|
|
9094
|
+
function reportOpencodeProblem(problem) {
|
|
9095
|
+
_ocpProblem = problem || 'OpenCode could not be started.';
|
|
9096
|
+
console.warn(`[opencode] ${_ocpProblem}`);
|
|
9097
|
+
broadcastToOpencodeV2Clients({ type: 'server:status', status: 'error', error: _ocpProblem, version: _ocpVersion, protocol: _ocpProtocol, port: _ocpPort, managed: !!_ocpProc });
|
|
9098
|
+
}
|
|
9099
|
+
|
|
8644
9100
|
async function ensureOpencodeServer({ allowDuringHistoryClear = false } = {}) {
|
|
8645
9101
|
if (_ocpHistoryClearInProgress && !allowDuringHistoryClear) return false;
|
|
8646
9102
|
if (_ocpReady) return true;
|
|
@@ -8653,6 +9109,23 @@ async function ensureOpencodeServer({ allowDuringHistoryClear = false } = {}) {
|
|
|
8653
9109
|
}
|
|
8654
9110
|
_ocpStarting = true;
|
|
8655
9111
|
|
|
9112
|
+
// 0. Which OpenCode is installed: its major decides how the serve starts and
|
|
9113
|
+
// how SynaBun speaks to it. A major SynaBun does not know is refused here, with
|
|
9114
|
+
// a sentence the panel shows, instead of failing call by call later.
|
|
9115
|
+
const detected = await detectOpencodeProtocol();
|
|
9116
|
+
if (detected.major != null && !detected.supported) {
|
|
9117
|
+
_ocpStarting = false;
|
|
9118
|
+
reportOpencodeProblem(detected.reason);
|
|
9119
|
+
return false;
|
|
9120
|
+
}
|
|
9121
|
+
_ocpProblem = null;
|
|
9122
|
+
// A serve already running is adopted whatever is installed: an OpenCode 1
|
|
9123
|
+
// answers SynaBun as it is, an OpenCode 2 only to the password SynaBun gave it.
|
|
9124
|
+
if (!_ocpProc) _ocpProtocol = null;
|
|
9125
|
+
// A serve an earlier SynaBun left running (on this port or any other it used) goes before anything here is
|
|
9126
|
+
// adopted, and its port with it.
|
|
9127
|
+
await sweepLeftoverOpencodeServes();
|
|
9128
|
+
|
|
8656
9129
|
// 1. Check if already running externally
|
|
8657
9130
|
if (await checkOpencodeHealth()) {
|
|
8658
9131
|
if (await reconcileOpencodeServerIfStale()) return true;
|
|
@@ -8663,66 +9136,111 @@ async function ensureOpencodeServer({ allowDuringHistoryClear = false } = {}) {
|
|
|
8663
9136
|
// One event stream on this serve: the v2 client's. MCP prewarm runs on
|
|
8664
9137
|
// the v2 client too.
|
|
8665
9138
|
installOpencodeV2Listener();
|
|
8666
|
-
await opencodeV2Client.connect({ port: _ocpPort });
|
|
8667
|
-
broadcastToOpencodeV2Clients({ type: 'server:status', status: 'ready', version: _ocpVersion, port: _ocpPort, managed: false });
|
|
8668
|
-
console.log(`[opencode] External server detected on port ${_ocpPort} (v${_ocpVersion || '?'})`);
|
|
9139
|
+
await opencodeV2Client.connect({ port: _ocpPort, protocol: _ocpProtocol, password: _ocpPassword, version: _ocpVersion });
|
|
9140
|
+
broadcastToOpencodeV2Clients({ type: 'server:status', status: 'ready', version: _ocpVersion, protocol: _ocpProtocol, port: _ocpPort, managed: false });
|
|
9141
|
+
console.log(`[opencode] External server detected on port ${_ocpPort} (v${_ocpVersion || '?'}, OpenCode ${_ocpProtocol || '?'} API)`);
|
|
8669
9142
|
prewarmOpencodeMcpServers();
|
|
8670
9143
|
return true;
|
|
8671
9144
|
}
|
|
9145
|
+
if (_ocpLastCheck && _ocpLastCheck.state !== 'down' && !(await freeSharedOpencodePortIfHeld(_ocpLastCheck))) {
|
|
9146
|
+
_ocpStarting = false;
|
|
9147
|
+
reportOpencodeProblem(opencodeProblemText({ check: _ocpLastCheck, port: _ocpPort }));
|
|
9148
|
+
return false;
|
|
9149
|
+
}
|
|
8672
9150
|
|
|
8673
|
-
// 2. Spawn opencode serve
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
9151
|
+
// 2. Spawn opencode serve, and 3. poll until ready (max 15s): one start at a time with every other serve
|
|
9152
|
+
// SynaBun starts (oneOpencodeStartAtATime), its turn held until the serve answers or a failed start's process
|
|
9153
|
+
// is gone. null for a command line in Settings: what answers says which major it is.
|
|
9154
|
+
// Another server that takes the port while SynaBun's serve starts is a start collision: it is left running (and
|
|
9155
|
+
// never put on record), SynaBun's own process is ended, and the shared serve tries the next free port (three at most).
|
|
9156
|
+
let outcome = null;
|
|
9157
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
9158
|
+
_ocpProtocol = detected.protocol;
|
|
9159
|
+
outcome = await oneOpencodeStartAtATime(detected.protocol, async () => {
|
|
9160
|
+
let proc = null;
|
|
9161
|
+
let tracked = null;
|
|
9162
|
+
try {
|
|
9163
|
+
// A token of this spawn in its environment: what tells its processes from any other (lib/opencode-serve-records.js).
|
|
9164
|
+
const token = opencodeServeRecords.newToken();
|
|
9165
|
+
const launch = opencodeServeLaunch(_ocpPort, { env: { ...process.env, FORCE_COLOR: '0', OPENCODE_DISABLE_CLAUDE_CODE: '1', [SERVE_TOKEN_ENV]: token }, protocol: detected.protocol });
|
|
9166
|
+
_ocpPassword = launch.password;
|
|
9167
|
+
console.log(`[opencode] Spawning: ${launch.label}`);
|
|
9168
|
+
proc = spawn(launch.command, launch.args, {
|
|
9169
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
9170
|
+
...launch.options,
|
|
9171
|
+
detached: false,
|
|
9172
|
+
shell: false,
|
|
9173
|
+
});
|
|
9174
|
+
_ocpProc = proc;
|
|
9175
|
+
// On record from its spawn: a crash of SynaBun from here on does not leave it running for good.
|
|
9176
|
+
tracked = trackOpencodeServe(proc, { kind: 'shared', id: 'shared', port: _ocpPort, token });
|
|
9177
|
+
proc.stdout?.on('data', (d) => console.log(`[opencode:stdout] ${d.toString().trim()}`));
|
|
9178
|
+
proc.stderr?.on('data', (d) => console.log(`[opencode:stderr] ${d.toString().trim()}`));
|
|
9179
|
+
proc.on('exit', (code) => {
|
|
9180
|
+
console.log(`[opencode] Process exited with code ${code}`);
|
|
9181
|
+
// A serve stopped or replaced meanwhile says nothing about the one running now. (Its record goes with
|
|
9182
|
+
// its process either way: trackOpencodeServe.)
|
|
9183
|
+
if (_ocpProc !== proc) return;
|
|
9184
|
+
_ocpProc = null;
|
|
9185
|
+
_ocpReady = false;
|
|
9186
|
+
opencodeV2Client.disconnect().catch(() => {});
|
|
9187
|
+
broadcastToOpencodeV2Clients({ type: 'server:status', status: 'offline', port: _ocpPort });
|
|
9188
|
+
});
|
|
9189
|
+
} catch (err) {
|
|
9190
|
+
console.error(`[opencode] Failed to spawn: ${err.message}`);
|
|
9191
|
+
return 'not-spawned';
|
|
9192
|
+
}
|
|
9193
|
+
for (let i = 0; i < 30; i++) {
|
|
9194
|
+
await new Promise(r => setTimeout(r, 500));
|
|
9195
|
+
if (await checkOpencodeHealth()) {
|
|
9196
|
+
// Only SynaBun's own process counts (the spawned one, or one it started): another server that took the
|
|
9197
|
+
// port meanwhile answers for itself.
|
|
9198
|
+
if (await tracked.answering() !== 'foreign') return 'ready';
|
|
9199
|
+
if (_ocpProc === proc) _ocpProc = null;
|
|
9200
|
+
killIsolatedServeProcess(proc);
|
|
9201
|
+
await endOpencodeChild(proc);
|
|
9202
|
+
return 'collision';
|
|
9203
|
+
}
|
|
9204
|
+
// Something answers, but not the API SynaBun expects (another major, a web page): no use waiting.
|
|
9205
|
+
if (_ocpLastCheck && ['mismatch', 'html', 'locked'].includes(_ocpLastCheck.state) && i >= 3) break;
|
|
9206
|
+
}
|
|
9207
|
+
stopOpencodeServer();
|
|
9208
|
+
await endOpencodeChild(proc);
|
|
9209
|
+
return 'unhealthy';
|
|
8692
9210
|
});
|
|
8693
|
-
|
|
8694
|
-
|
|
9211
|
+
if (outcome !== 'collision' || attempt === 3) break;
|
|
9212
|
+
if (!(await moveSharedOpencodePort(`Another server took port ${_ocpPort} while SynaBun's OpenCode serve was starting: it is left running.`))) break;
|
|
9213
|
+
}
|
|
9214
|
+
if (outcome === 'not-spawned') {
|
|
8695
9215
|
_ocpStarting = false;
|
|
8696
9216
|
return false;
|
|
8697
9217
|
}
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
await opencodeV2Client.connect({ port: _ocpPort });
|
|
8710
|
-
broadcastToOpencodeV2Clients({ type: 'server:status', status: 'ready', version: _ocpVersion, port: _ocpPort, managed: true });
|
|
8711
|
-
console.log(`[opencode] Server ready on port ${_ocpPort} (v${_ocpVersion || '?'})`);
|
|
8712
|
-
prewarmOpencodeMcpServers();
|
|
8713
|
-
return true;
|
|
8714
|
-
}
|
|
9218
|
+
if (outcome === 'ready') {
|
|
9219
|
+
_ocpReady = true;
|
|
9220
|
+
_ocpStarting = false;
|
|
9221
|
+
_ocpBootReconcileTried = true;
|
|
9222
|
+
_ocpLastBootAt = Date.now();
|
|
9223
|
+
installOpencodeV2Listener();
|
|
9224
|
+
await opencodeV2Client.connect({ port: _ocpPort, protocol: _ocpProtocol, password: _ocpPassword, version: _ocpVersion });
|
|
9225
|
+
broadcastToOpencodeV2Clients({ type: 'server:status', status: 'ready', version: _ocpVersion, protocol: _ocpProtocol, port: _ocpPort, managed: true });
|
|
9226
|
+
console.log(`[opencode] Server ready on port ${_ocpPort} (v${_ocpVersion || '?'}, OpenCode ${_ocpProtocol || '?'} API)`);
|
|
9227
|
+
prewarmOpencodeMcpServers();
|
|
9228
|
+
return true;
|
|
8715
9229
|
}
|
|
8716
9230
|
|
|
8717
|
-
|
|
8718
|
-
|
|
9231
|
+
const why = outcome === 'collision'
|
|
9232
|
+
? `Another server took port ${_ocpPort} while SynaBun's OpenCode serve was starting, on every port it tried; it is left running.`
|
|
9233
|
+
: _ocpLastCheck && _ocpLastCheck.state !== 'down'
|
|
9234
|
+
? opencodeProblemText({ check: _ocpLastCheck, port: _ocpPort })
|
|
9235
|
+
: 'OpenCode did not start within 15 seconds.';
|
|
9236
|
+
console.error(`[opencode] Server did not become healthy: ${why}`);
|
|
8719
9237
|
_ocpStarting = false;
|
|
9238
|
+
reportOpencodeProblem(why);
|
|
8720
9239
|
return false;
|
|
8721
9240
|
}
|
|
8722
9241
|
|
|
8723
9242
|
function stopOpencodeServer({ stopManagedOllama = true } = {}) {
|
|
8724
9243
|
opencodeV2Client.disconnect().catch(() => {});
|
|
8725
|
-
const managedPid = readOpencodeManagedPid();
|
|
8726
9244
|
const procPid = _ocpProc?.pid || null;
|
|
8727
9245
|
const isWin = process.platform === 'win32';
|
|
8728
9246
|
|
|
@@ -8748,14 +9266,16 @@ function stopOpencodeServer({ stopManagedOllama = true } = {}) {
|
|
|
8748
9266
|
}
|
|
8749
9267
|
_ocpProc = null;
|
|
8750
9268
|
}
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
9269
|
+
// SynaBun's recorded serves (a process its child started that answers on the port; one of an earlier run whose
|
|
9270
|
+
// SynaBun has ended), only while the OS still reports each as that very process: a pid another program has by now,
|
|
9271
|
+
// or one that cannot be read, is never signalled (signalOwnedOpencodeProcess checks once more right before;
|
|
9272
|
+
// taskkill on Windows).
|
|
9273
|
+
const owned = verifiedOpencodeManagedProcesses();
|
|
9274
|
+
for (const record of owned) if (record.pid !== procPid) signalOwnedOpencodeProcess(record, 'SIGTERM');
|
|
9275
|
+
// A record whose process has provably ended goes. One whose serve was just signalled stays until that serve has
|
|
9276
|
+
// ended (its exit removes it, or the next look for leftovers does): a crash meanwhile does not leave it running
|
|
9277
|
+
// unrecorded.
|
|
9278
|
+
if (!owned.length) clearOpencodeManagedPid();
|
|
8759
9279
|
_ocpReady = false;
|
|
8760
9280
|
_ocpStarting = false;
|
|
8761
9281
|
broadcastToOpencodeV2Clients({ type: 'server:status', status: 'offline', port: _ocpPort });
|
|
@@ -8765,7 +9285,13 @@ function stopOpencodeServer({ stopManagedOllama = true } = {}) {
|
|
|
8765
9285
|
}
|
|
8766
9286
|
}
|
|
8767
9287
|
|
|
8768
|
-
// Takeover:
|
|
9288
|
+
// Takeover: a fresh shared serve that boots from the updated auth/config files. A serve is stopped only when it
|
|
9289
|
+
// is SynaBun's own: its child (a plain restart), or its recorded serve of an earlier run while the OS still
|
|
9290
|
+
// reports that very process, checked again before each signal. No other process is ever signalled: not every
|
|
9291
|
+
// `opencode … serve` of the machine (the user's own, OpenCode 2's background service, SynaBun's isolated serves),
|
|
9292
|
+
// not a recorded pid another program has by now. A server SynaBun did not start is left running, and SynaBun
|
|
9293
|
+
// starts its own on another port. The automatic callers (reconcileOpencodeServerIfStale, reconcileAuthIfStale)
|
|
9294
|
+
// call this only for a serve that is SynaBun's.
|
|
8769
9295
|
async function takeoverOpencodeServer() {
|
|
8770
9296
|
if (_ocpHistoryClearInProgress) throw new Error('OpenCode history is being cleared');
|
|
8771
9297
|
if (_ocpProc) {
|
|
@@ -8774,32 +9300,23 @@ async function takeoverOpencodeServer() {
|
|
|
8774
9300
|
await new Promise(r => setTimeout(r, 1000));
|
|
8775
9301
|
return ensureOpencodeServer();
|
|
8776
9302
|
}
|
|
8777
|
-
// External server — terminate the actual port listener so we can take over with
|
|
8778
|
-
// a managed process that will boot from the updated auth/config files.
|
|
8779
9303
|
_ocpReady = false;
|
|
8780
9304
|
_ocpStarting = false;
|
|
9305
|
+
const owned = verifiedOpencodeManagedProcess({ port: _ocpPort });
|
|
9306
|
+
if (!owned) {
|
|
9307
|
+
await opencodeV2Client.disconnect().catch(() => {});
|
|
9308
|
+
if (!(await moveSharedOpencodePort(`The OpenCode server on port ${_ocpPort} was not started by SynaBun: it is left running.`))) {
|
|
9309
|
+
throw new Error(`The OpenCode server on port ${_ocpPort} was not started by SynaBun and is left running; no other port is free for SynaBun's own`);
|
|
9310
|
+
}
|
|
9311
|
+
return ensureOpencodeServer();
|
|
9312
|
+
}
|
|
8781
9313
|
try {
|
|
8782
9314
|
await ocpProxy('POST', '/global/dispose');
|
|
8783
9315
|
} catch {}
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
const terminated = signalPids(candidatePids, 'SIGTERM');
|
|
8789
|
-
console.log(`[opencode] Sent SIGTERM to external server PIDs: ${terminated.join(', ') || 'none'}`);
|
|
8790
|
-
} else {
|
|
8791
|
-
console.log(`[opencode] No external listener PIDs found on port ${_ocpPort}; waiting for shutdown`);
|
|
8792
|
-
}
|
|
8793
|
-
if (!(await waitForOpencodeShutdown())) {
|
|
8794
|
-
const stubbornPids = [...new Set([...listPortListenerPids(_ocpPort), ...listOpencodeServePids()])];
|
|
8795
|
-
if (stubbornPids.length) {
|
|
8796
|
-
const killed = signalPids(stubbornPids, 'SIGKILL');
|
|
8797
|
-
console.log(`[opencode] Sent SIGKILL to stubborn external PIDs: ${killed.join(', ') || 'none'}`);
|
|
8798
|
-
}
|
|
8799
|
-
await waitForOpencodeShutdown(4000);
|
|
8800
|
-
}
|
|
8801
|
-
if (await checkOpencodeHealth()) {
|
|
8802
|
-
throw new Error(`External OpenCode server on port ${_ocpPort} survived takeover`);
|
|
9316
|
+
await opencodeV2Client.disconnect().catch(() => {});
|
|
9317
|
+
console.log(`[opencode] Stopping SynaBun's earlier server (pid ${owned.pid}) on port ${_ocpPort}`);
|
|
9318
|
+
if (!(await stopOwnedOpencodeServe(owned, _ocpPort)) && await checkOpencodeHealth()) {
|
|
9319
|
+
throw new Error(`SynaBun's OpenCode server on port ${_ocpPort} survived takeover`);
|
|
8803
9320
|
}
|
|
8804
9321
|
await new Promise(r => setTimeout(r, 500));
|
|
8805
9322
|
return ensureOpencodeServer();
|
|
@@ -8835,7 +9352,43 @@ function getOpencodeDb() {
|
|
|
8835
9352
|
|
|
8836
9353
|
// ── Proxy helper: forward REST calls to opencode serve ──
|
|
8837
9354
|
|
|
9355
|
+
// The OpenCode 2 serve behind a base URL (the shared one or an isolated one):
|
|
9356
|
+
// `{ client, password }`, or null for an OpenCode 1 serve.
|
|
9357
|
+
function opencode2ServeFor(baseUrl) {
|
|
9358
|
+
let port = null;
|
|
9359
|
+
try { port = Number(new URL(baseUrl).port) || null; } catch { return null; }
|
|
9360
|
+
if (port === _ocpPort && _ocpProtocol === 2) {
|
|
9361
|
+
const client = opencodeV2Client.getClient();
|
|
9362
|
+
return client ? { client, password: _ocpPassword } : null;
|
|
9363
|
+
}
|
|
9364
|
+
for (const entry of _ocpIsoServes.values()) {
|
|
9365
|
+
if (entry.port === port && entry.protocol === 2 && entry.client) return { client: entry.client.getClient(), password: entry.password };
|
|
9366
|
+
}
|
|
9367
|
+
return null;
|
|
9368
|
+
}
|
|
9369
|
+
|
|
8838
9370
|
async function ocpProxy(method, path, body = null, timeoutMs = 30000, baseUrl = _ocpBaseUrl()) {
|
|
9371
|
+
// OpenCode 2 answers these OpenCode 1 paths with its web app: each is
|
|
9372
|
+
// translated onto its API instead (lib/opencode2-proxy.js), /api/* passes
|
|
9373
|
+
// through with the serve's password.
|
|
9374
|
+
const v2 = opencode2ServeFor(baseUrl);
|
|
9375
|
+
if (v2?.client) {
|
|
9376
|
+
ocpTrace('proxy:req', { tid: _ocpTraceId(), method, path, opencode: 2, timeoutMs });
|
|
9377
|
+
return opencode2Proxy(v2.client, method, path, body, {
|
|
9378
|
+
rawFetch: async (apiPath, init = {}) => {
|
|
9379
|
+
const resp = await fetch(`${baseUrl}${apiPath}`, {
|
|
9380
|
+
method: init.method || method,
|
|
9381
|
+
headers: { 'Content-Type': 'application/json', authorization: basicAuthHeader(v2.password || '') },
|
|
9382
|
+
...(init.body != null && (init.method || method) !== 'GET' ? { body: JSON.stringify(init.body) } : {}),
|
|
9383
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
9384
|
+
});
|
|
9385
|
+
const text = await resp.text();
|
|
9386
|
+
let data;
|
|
9387
|
+
try { data = JSON.parse(text); } catch { data = text; }
|
|
9388
|
+
return { status: resp.status, data };
|
|
9389
|
+
},
|
|
9390
|
+
});
|
|
9391
|
+
}
|
|
8839
9392
|
const url = `${baseUrl}${path}`;
|
|
8840
9393
|
const opts = {
|
|
8841
9394
|
method,
|
|
@@ -9067,6 +9620,7 @@ app.post('/api/sidepanel/session-title', async (req, res) => {
|
|
|
9067
9620
|
execFile: execFileAsync,
|
|
9068
9621
|
getCodexBin,
|
|
9069
9622
|
getCodexAccountHome,
|
|
9623
|
+
codexConfigArgs: codexSandboxArgs,
|
|
9070
9624
|
getAugmentedPath,
|
|
9071
9625
|
ensureOpenCode: ensureOpencodeServer,
|
|
9072
9626
|
openCodeClient: opencodeV2Client,
|
|
@@ -9125,40 +9679,29 @@ async function opencodePortIsAlive(port) {
|
|
|
9125
9679
|
}
|
|
9126
9680
|
}
|
|
9127
9681
|
|
|
9128
|
-
function
|
|
9129
|
-
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) return false;
|
|
9130
|
-
try {
|
|
9131
|
-
process.kill(pid, 0);
|
|
9132
|
-
return true;
|
|
9133
|
-
} catch {
|
|
9134
|
-
return false;
|
|
9135
|
-
}
|
|
9136
|
-
}
|
|
9137
|
-
|
|
9138
|
-
async function waitForOpencodeProcessesToStop(ports, pids, timeoutMs = 8000) {
|
|
9682
|
+
async function waitForOpencodeProcessesToStop(ports, stopped, timeoutMs = 8000) {
|
|
9139
9683
|
const uniquePorts = [...new Set(ports.filter((port) => Number.isFinite(port) && port > 0))];
|
|
9140
|
-
const uniquePids = [...new Set(pids.filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid))];
|
|
9141
9684
|
const deadline = Date.now() + timeoutMs;
|
|
9142
9685
|
while (Date.now() < deadline) {
|
|
9143
9686
|
const alive = await Promise.all(uniquePorts.map(opencodePortIsAlive));
|
|
9144
|
-
if (!alive.some(Boolean) &&
|
|
9687
|
+
if (!alive.some(Boolean) && stopped()) return true;
|
|
9145
9688
|
await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
9146
9689
|
}
|
|
9147
9690
|
return false;
|
|
9148
9691
|
}
|
|
9149
9692
|
|
|
9693
|
+
// Only what SynaBun started is stopped for a history clear: its children, by their handles (which never signal a
|
|
9694
|
+
// process that has exited), and its recorded serve while the OS still reports that very process. A server someone
|
|
9695
|
+
// else runs on one of these ports is left running, and the clear then says so instead of killing it.
|
|
9150
9696
|
async function stopOpenCodeForHistoryClear() {
|
|
9151
|
-
const isolated = [..._ocpIsoServes.values()].map((entry) => ({
|
|
9152
|
-
port: entry.port,
|
|
9153
|
-
pid: entry.proc?.pid || null,
|
|
9154
|
-
}));
|
|
9697
|
+
const isolated = [..._ocpIsoServes.values()].map((entry) => ({ port: entry.port, proc: entry.proc || null }));
|
|
9155
9698
|
const ports = [_ocpPort, ...isolated.map((entry) => entry.port)];
|
|
9156
|
-
const
|
|
9157
|
-
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9699
|
+
const children = [_ocpProc, ...isolated.map((entry) => entry.proc)].filter(Boolean);
|
|
9700
|
+
const owned = verifiedOpencodeManagedProcess();
|
|
9701
|
+
const gone = (child) => child.exitCode != null || child.signalCode != null;
|
|
9702
|
+
// Still running until it has provably ended: one that cannot be read now counts as running.
|
|
9703
|
+
const ownedRuns = () => !!owned && opencodeServeRecords.verifySync(owned) !== 'gone';
|
|
9704
|
+
const stopped = () => children.every(gone) && !ownedRuns();
|
|
9162
9705
|
|
|
9163
9706
|
await Promise.allSettled(isolated.map(({ port }) => fetch(
|
|
9164
9707
|
`http://127.0.0.1:${port}/global/dispose`,
|
|
@@ -9168,21 +9711,23 @@ async function stopOpenCodeForHistoryClear() {
|
|
|
9168
9711
|
stopAllIsolatedServes();
|
|
9169
9712
|
stopOpencodeServer({ stopManagedOllama: false });
|
|
9170
9713
|
|
|
9171
|
-
if (await waitForOpencodeProcessesToStop(ports,
|
|
9172
|
-
if (process.platform !== 'win32'
|
|
9173
|
-
|
|
9174
|
-
|
|
9714
|
+
if (await waitForOpencodeProcessesToStop(ports, stopped)) return;
|
|
9715
|
+
if (process.platform !== 'win32') {
|
|
9716
|
+
for (const child of children) if (!gone(child)) { try { child.kill('SIGKILL'); } catch {} }
|
|
9717
|
+
if (owned) signalOwnedOpencodeProcess(owned, 'SIGKILL');
|
|
9718
|
+
await waitForOpencodeProcessesToStop(ports, stopped, 4000);
|
|
9175
9719
|
}
|
|
9176
9720
|
const stillAlive = (await Promise.all(ports.map(async (port) => (
|
|
9177
9721
|
(await opencodePortIsAlive(port)) ? port : null
|
|
9178
9722
|
)))).filter(Boolean);
|
|
9179
|
-
const stillRunningPids = [...
|
|
9723
|
+
const stillRunningPids = [...children.filter((child) => !gone(child)).map((child) => child.pid), ...(ownedRuns() ? [owned.pid] : [])];
|
|
9180
9724
|
if (stillAlive.length || stillRunningPids.length) {
|
|
9181
9725
|
const details = [
|
|
9182
9726
|
...stillAlive.map((port) => `port ${port}`),
|
|
9183
9727
|
...stillRunningPids.map((pid) => `process ${pid}`),
|
|
9184
9728
|
];
|
|
9185
|
-
|
|
9729
|
+
const someoneElse = stillAlive.length && !stillRunningPids.length ? ' (an OpenCode server SynaBun did not start answers there: stop it, then clear again)' : '';
|
|
9730
|
+
throw new Error(`OpenCode did not release ${details.join(', ')}${someoneElse}`);
|
|
9186
9731
|
}
|
|
9187
9732
|
}
|
|
9188
9733
|
|
|
@@ -9225,6 +9770,12 @@ app.delete('/api/opencode/history', async (req, res) => {
|
|
|
9225
9770
|
blockers,
|
|
9226
9771
|
});
|
|
9227
9772
|
}
|
|
9773
|
+
// OpenCode 2's database is not cleared here (lib/opencode-history.js): refused before anything is stopped.
|
|
9774
|
+
let historyStats = null;
|
|
9775
|
+
try { historyStats = getOpenCodeHistoryStats({ dbPath: getOpencodeDbPath(), dataRoot: dirname(getOpencodeDbPath()) }); } catch { /* the clear itself says */ }
|
|
9776
|
+
if (historyStats?.opencode2 || _ocpProtocol === 2) {
|
|
9777
|
+
return res.status(409).json({ ok: false, opencode2: true, error: historyStats?.reason || OPENCODE2_HISTORY_REFUSAL });
|
|
9778
|
+
}
|
|
9228
9779
|
|
|
9229
9780
|
wasRunning = await checkOpencodeHealth();
|
|
9230
9781
|
await stopOpenCodeForHistoryClear();
|
|
@@ -9300,7 +9851,7 @@ app.get('/api/opencode/status', async (req, res) => {
|
|
|
9300
9851
|
} else if (!running && _ocpReady) {
|
|
9301
9852
|
_ocpReady = false;
|
|
9302
9853
|
}
|
|
9303
|
-
res.json({ ok: true, running, port: _ocpPort, version: _ocpVersion, managed: !!_ocpProc });
|
|
9854
|
+
res.json({ ok: true, running, port: _ocpPort, version: _ocpVersion, protocol: _ocpProtocol, managed: !!_ocpProc, ...(running ? {} : { problem: _ocpProblem || (_ocpLastCheck && _ocpLastCheck.state !== 'down' ? opencodeProblemText({ check: _ocpLastCheck, port: _ocpPort }) : null) }) });
|
|
9304
9855
|
} catch (err) {
|
|
9305
9856
|
res.status(500).json({ ok: false, error: err.message });
|
|
9306
9857
|
}
|
|
@@ -9357,14 +9908,108 @@ app.post('/api/opencode/snapshot', (req, res) => {
|
|
|
9357
9908
|
}
|
|
9358
9909
|
});
|
|
9359
9910
|
|
|
9911
|
+
// ── OpenCode's provider list, kept (lib/launch-cache.js) ──
|
|
9912
|
+
// The shared serve's /provider is the whole models.dev catalog (about 7 MB, 231 providers on a typical
|
|
9913
|
+
// install) and took 1.3-1.5 s on every call. It is read once and kept: answered at once, read again in the
|
|
9914
|
+
// background when it is old, when the serve restarts and when a catalog event says it changed. Keyed on what
|
|
9915
|
+
// it depends on: the OpenCode major, the binary (an update), its credentials file and its config files (a
|
|
9916
|
+
// provider connected or removed rewrites auth.json: another key, so the next read waits for the new list).
|
|
9917
|
+
// Only a provider list is kept: a web page (an OpenCode 2 serve answering an OpenCode 1 path) or an error
|
|
9918
|
+
// never is. ?view=compact (the connected providers, lib/opencode-provider-view.js) is what the pickers read,
|
|
9919
|
+
// and the only part kept across restarts: the whole catalog is megabytes.
|
|
9920
|
+
const OPENCODE_PROVIDERS_TTL_MS = 5 * 60_000;
|
|
9921
|
+
|
|
9922
|
+
function opencodeFileStamp(file) {
|
|
9923
|
+
try { const stat = statSync(file); return `${Math.round(stat.mtimeMs)}:${stat.size}`; } catch { return '-'; }
|
|
9924
|
+
}
|
|
9925
|
+
|
|
9926
|
+
function opencodeProvidersKey() {
|
|
9927
|
+
const configDir = dirname(getOpencodeConfigPath());
|
|
9928
|
+
return [
|
|
9929
|
+
knownOpencodeMajor(),
|
|
9930
|
+
configuredCommandLine('opencode') || binaryIdentity(cliResolver.launcher('opencode').path || ''),
|
|
9931
|
+
opencodeFileStamp(getOpencodeAuthPath()),
|
|
9932
|
+
opencodeFileStamp(getOpencodeConfigPath()),
|
|
9933
|
+
opencodeFileStamp(join(configDir, 'opencode.json')),
|
|
9934
|
+
opencodeFileStamp(join(configDir, 'opencode.jsonc')),
|
|
9935
|
+
].join('|');
|
|
9936
|
+
}
|
|
9937
|
+
|
|
9938
|
+
const _opencodeProviders = createLaunchCache({
|
|
9939
|
+
name: 'opencode-providers',
|
|
9940
|
+
dir: LAUNCH_CACHE_DIR,
|
|
9941
|
+
ttlMs: OPENCODE_PROVIDERS_TTL_MS,
|
|
9942
|
+
maxEntries: 2,
|
|
9943
|
+
load: async () => {
|
|
9944
|
+
if (!_ocpReady) throw new Error('OpenCode server not running');
|
|
9945
|
+
await reconcileAuthIfStale();
|
|
9946
|
+
const r = await ocpProxy('GET', '/provider');
|
|
9947
|
+
if (r.status !== 200 || !isProviderCatalog(r.data)) {
|
|
9948
|
+
throw Object.assign(new Error(r.status === 200 ? 'OpenCode did not answer with a provider list' : `OpenCode answered ${r.status}`), { status: r.status === 200 ? 502 : r.status, data: r.data });
|
|
9949
|
+
}
|
|
9950
|
+
return { full: r.data, compact: compactProviderCatalog(r.data), signature: providerCatalogSignature(r.data) };
|
|
9951
|
+
},
|
|
9952
|
+
accept: (value) => !!value && (isProviderCatalog(value.full) || isProviderCatalog(value.compact)),
|
|
9953
|
+
equals: (a, b) => !!a?.signature && a.signature === b?.signature && !!a.full === !!b.full,
|
|
9954
|
+
persistValue: (value) => (value?.compact ? { compact: value.compact, signature: value.signature } : undefined),
|
|
9955
|
+
// A refresh found another list than the kept one: every window's pickers read it again.
|
|
9956
|
+
onChange: (_key, value, previous) => { if (value?.signature !== previous?.signature) announceOpencodeProvidersChanged(); },
|
|
9957
|
+
log: (message) => console.warn(message),
|
|
9958
|
+
});
|
|
9959
|
+
|
|
9960
|
+
// The serialized answer of each view, made once per list (the whole catalog is megabytes of JSON), with an
|
|
9961
|
+
// ETag of its own so Express does not hash megabytes for every answer.
|
|
9962
|
+
const _opencodeProviderTexts = new WeakMap();
|
|
9963
|
+
function sendOpencodeProviders(res, value, view) {
|
|
9964
|
+
let texts = _opencodeProviderTexts.get(value);
|
|
9965
|
+
if (!texts) { texts = {}; _opencodeProviderTexts.set(value, texts); }
|
|
9966
|
+
if (!texts[view]) {
|
|
9967
|
+
const text = `{"ok":true,"data":${JSON.stringify(view === 'compact' ? value.compact : value.full)}}`;
|
|
9968
|
+
texts[view] = { text, etag: `W/"ocp-${view}-${createHash('sha1').update(text).digest('hex').slice(0, 20)}"` };
|
|
9969
|
+
}
|
|
9970
|
+
res.set('ETag', texts[view].etag);
|
|
9971
|
+
res.type('json').send(texts[view].text);
|
|
9972
|
+
}
|
|
9973
|
+
|
|
9974
|
+
/**
|
|
9975
|
+
* The provider list for a view ('full' | 'compact'): the kept one at once, else the serve's (one read for
|
|
9976
|
+
* every caller). `refresh` waits for a new read. The whole catalog is never kept across a restart, so a
|
|
9977
|
+
* restored entry answers 'compact' only and 'full' reads the serve.
|
|
9978
|
+
*/
|
|
9979
|
+
async function readOpencodeProviders(view = 'full', { refresh = false } = {}) {
|
|
9980
|
+
const key = opencodeProvidersKey();
|
|
9981
|
+
let { value } = await _opencodeProviders.read(key, { refresh });
|
|
9982
|
+
if (view === 'full' && !value?.full) ({ value } = await _opencodeProviders.read(key, { refresh: true }));
|
|
9983
|
+
if (!value || !(view === 'full' ? value.full : value.compact)) throw new Error('OpenCode did not answer with a provider list');
|
|
9984
|
+
return value;
|
|
9985
|
+
}
|
|
9986
|
+
|
|
9987
|
+
// Every window learns the list changed (an OpenCode panel's own socket says so only where one is open).
|
|
9988
|
+
function announceOpencodeProvidersChanged() {
|
|
9989
|
+
try { broadcastSync({ type: 'opencode:providers-changed' }); } catch {}
|
|
9990
|
+
_announcingOpencodeProviders = true;
|
|
9991
|
+
try { broadcastToOpencodeV2Clients({ type: 'providers:changed' }); } finally { _announcingOpencodeProviders = false; }
|
|
9992
|
+
}
|
|
9993
|
+
|
|
9994
|
+
// The serve restarted, a credential changed or OpenCode said its catalog changed: the kept list is read
|
|
9995
|
+
// again in the background (still answered meanwhile; a credential change is another key anyway). `announce`:
|
|
9996
|
+
// the OpenCode panels were told already; every other window is told too.
|
|
9997
|
+
function noteOpencodeProvidersChanged({ announce = false } = {}) {
|
|
9998
|
+
try {
|
|
9999
|
+
_opencodeProviders.markStale();
|
|
10000
|
+
scheduleLaunchRefresh('opencode-providers');
|
|
10001
|
+
if (announce) broadcastSync({ type: 'opencode:providers-changed' });
|
|
10002
|
+
} catch { /* before the cache exists (start-up): nothing is kept yet */ }
|
|
10003
|
+
}
|
|
10004
|
+
|
|
9360
10005
|
app.get('/api/opencode/providers', async (req, res) => {
|
|
9361
10006
|
try {
|
|
9362
10007
|
if (!_ocpReady) return res.json({ ok: false, error: 'OpenCode server not running', data: [] });
|
|
9363
|
-
await
|
|
9364
|
-
|
|
9365
|
-
res.status(r.status).json({ ok: r.status === 200, data: r.data });
|
|
10008
|
+
const value = await readOpencodeProviders('full', { refresh: req.query?.refresh === '1' });
|
|
10009
|
+
sendOpencodeProviders(res, value, 'full');
|
|
9366
10010
|
} catch (err) {
|
|
9367
|
-
res.status(
|
|
10011
|
+
if (err?.status && err.status !== 502) return res.status(err.status).json({ ok: false, data: err.data });
|
|
10012
|
+
res.status(err?.status || 500).json({ ok: false, error: err.message });
|
|
9368
10013
|
}
|
|
9369
10014
|
});
|
|
9370
10015
|
|
|
@@ -9404,8 +10049,10 @@ app.get('/api/opencode-v2/status', async (req, res) => {
|
|
|
9404
10049
|
running,
|
|
9405
10050
|
port: _ocpPort,
|
|
9406
10051
|
version: _ocpVersion,
|
|
10052
|
+
protocol: _ocpProtocol,
|
|
9407
10053
|
managed: !!_ocpProc,
|
|
9408
10054
|
connected: opencodeV2Client.isConnected(),
|
|
10055
|
+
...(running ? {} : { problem: _ocpProblem || (_ocpLastCheck && _ocpLastCheck.state !== 'down' ? opencodeProblemText({ check: _ocpLastCheck, port: _ocpPort }) : null) }),
|
|
9409
10056
|
});
|
|
9410
10057
|
} catch (err) {
|
|
9411
10058
|
res.status(500).json({ ok: false, error: err.message });
|
|
@@ -9437,15 +10084,17 @@ app.post('/api/opencode-v2/serve/stop', (req, res) => {
|
|
|
9437
10084
|
// Models disabled in Settings → OpenCode (GET / PUT, broadcasts opencode:hidden-models-changed)
|
|
9438
10085
|
app.use('/api/opencode/hidden-models', createOpencodeHiddenModelsRouter({ store: opencodeHiddenModels, isGuestRequest, broadcastSync }));
|
|
9439
10086
|
|
|
9440
|
-
// Full provider data including connected[] array and auth methods
|
|
10087
|
+
// Full provider data including connected[] array and auth methods. ?view=compact: only the connected
|
|
10088
|
+
// providers, with what the pickers read (lib/opencode-provider-view.js); ?refresh=1 reads the serve again.
|
|
9441
10089
|
app.get('/api/opencode/providers/full', async (req, res) => {
|
|
9442
10090
|
try {
|
|
9443
10091
|
if (!_ocpReady) return res.json({ ok: false, error: 'OpenCode server not running', data: {} });
|
|
9444
|
-
|
|
9445
|
-
const
|
|
9446
|
-
res
|
|
10092
|
+
const view = req.query?.view === 'compact' ? 'compact' : 'full';
|
|
10093
|
+
const value = await readOpencodeProviders(view, { refresh: req.query?.refresh === '1' });
|
|
10094
|
+
sendOpencodeProviders(res, value, view);
|
|
9447
10095
|
} catch (err) {
|
|
9448
|
-
res.status(
|
|
10096
|
+
if (err?.status && err.status !== 502) return res.status(err.status).json({ ok: false, data: err.data });
|
|
10097
|
+
res.status(err?.status || 500).json({ ok: false, error: err.message });
|
|
9449
10098
|
}
|
|
9450
10099
|
});
|
|
9451
10100
|
|
|
@@ -9797,6 +10446,8 @@ function _panelClaudeAccounts() {
|
|
|
9797
10446
|
// The Assistant's account list and catalog listen to their own event.
|
|
9798
10447
|
broadcastSync({ type: 'assistant:accounts-changed', provider: 'claude-code', accountId: event.accountId || null, removed: !!event.removed });
|
|
9799
10448
|
_assistantCatalog?.invalidate?.();
|
|
10449
|
+
// The model list is read under the default account: asked again (another sign-in is another key).
|
|
10450
|
+
if (!event.accountId || event.accountId === 'default') { markClaudeModelsStale(); scheduleLaunchRefresh('claude-models'); }
|
|
9800
10451
|
} catch {}
|
|
9801
10452
|
});
|
|
9802
10453
|
}
|
|
@@ -9997,6 +10648,31 @@ function handleClaudeSkinWebSocket(ws) {
|
|
|
9997
10648
|
|
|
9998
10649
|
// Ask the installed CLI which models this account can run. Falls back to a small
|
|
9999
10650
|
// static list so the picker still renders when the CLI is missing or errors.
|
|
10651
|
+
// The sign-in the CLI reads its model list under (the default account: discovery runs with no
|
|
10652
|
+
// CLAUDE_CONFIG_DIR of its own), as a hash: another account can be entitled to other models, so the
|
|
10653
|
+
// kept list is keyed on it and never given for another account.
|
|
10654
|
+
function claudeModelAccountKey() {
|
|
10655
|
+
try {
|
|
10656
|
+
const row = _panelClaudeAccounts().describe('default');
|
|
10657
|
+
if (!row) return '';
|
|
10658
|
+
return createHash('sha256').update(`${row.mode || ''}\n${row.identity || ''}\n${row.signedIn}`).digest('hex').slice(0, 16);
|
|
10659
|
+
} catch { return ''; }
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10662
|
+
// The last verified Claude list survives a restart (DATA_HOME/cache/launch/claude-models.json).
|
|
10663
|
+
setClaudeModelStore(createSnapshotStore({ dir: LAUNCH_CACHE_DIR, name: 'claude-models', log: (message) => console.warn(message) }));
|
|
10664
|
+
|
|
10665
|
+
// What Codex said about its settings schema and feature switches (lib/codex-settings-schema.js), per binary
|
|
10666
|
+
// (path, mtime, size: an update is another binary), kept across restarts: asking costs a Codex start per value.
|
|
10667
|
+
const _codexSettingsProbes = createLaunchCache({
|
|
10668
|
+
name: 'codex-settings',
|
|
10669
|
+
dir: LAUNCH_CACHE_DIR,
|
|
10670
|
+
ttlMs: Number.MAX_SAFE_INTEGER,
|
|
10671
|
+
maxEntries: 8,
|
|
10672
|
+
load: async () => { throw new Error('asked by lib/codex-settings-schema.js'); },
|
|
10673
|
+
});
|
|
10674
|
+
setCodexSettingsStore({ get: (key) => _codexSettingsProbes.peek(key), set: (key, value) => _codexSettingsProbes.set(key, value) });
|
|
10675
|
+
|
|
10000
10676
|
async function getClaudeModelsForClient(force = false) {
|
|
10001
10677
|
try {
|
|
10002
10678
|
const bin = getClaudeBin();
|
|
@@ -10005,7 +10681,9 @@ async function getClaudeModelsForClient(force = false) {
|
|
|
10005
10681
|
// installation is worth one line in the log. Cached per binary (a spawn only
|
|
10006
10682
|
// when the CLI changes).
|
|
10007
10683
|
checkClaudeCliSkew(bin).catch(() => {});
|
|
10008
|
-
|
|
10684
|
+
// Answered from the kept list at once (asked again in the background when it is old);
|
|
10685
|
+
// only a first read, another binary or account, or ?refresh=1 waits for the CLI.
|
|
10686
|
+
const result = await discoverClaudeModels(bin, { force, cwd: PACKAGE_ROOT, account: claudeModelAccountKey() });
|
|
10009
10687
|
if (result?.models?.length) return result;
|
|
10010
10688
|
} catch (err) {
|
|
10011
10689
|
console.warn('[claude-models] discovery failed:', err.message);
|
|
@@ -10770,11 +11448,15 @@ app.get('/api/images', (req, res) => {
|
|
|
10770
11448
|
}
|
|
10771
11449
|
});
|
|
10772
11450
|
|
|
10773
|
-
// GET /api/images/file/:filename — serve an image file
|
|
11451
|
+
// GET /api/images/file/:filename — serve an image file. These are the user's files (screenshots, pastes,
|
|
11452
|
+
// attachments, whiteboard images), and an SVG is a document: opened as a page it runs its own script in this origin.
|
|
11453
|
+
// So every answer runs nothing (a CSP with no script source, a sandbox) and is read only as the type it is sent as,
|
|
11454
|
+
// and an SVG is a download when it is opened as a page. An <img> in the app ignores all three and shows the image.
|
|
10774
11455
|
app.get('/api/images/file/:filename', (req, res) => {
|
|
10775
11456
|
const filename = basename(req.params.filename);
|
|
10776
11457
|
const fp = join(IMAGES_DIR, filename);
|
|
10777
11458
|
if (!existsSync(fp)) return res.status(404).json({ error: 'Not found' });
|
|
11459
|
+
setUntrustedFileHeaders(res, filename);
|
|
10778
11460
|
res.sendFile(fp);
|
|
10779
11461
|
});
|
|
10780
11462
|
|
|
@@ -12613,15 +13295,17 @@ function unknownAutomationContextMode(mode) {
|
|
|
12613
13295
|
return mode != null && mode !== '' && !['default', 'extended', '1m'].includes(mode);
|
|
12614
13296
|
}
|
|
12615
13297
|
|
|
13298
|
+
// The model list of the account that lives in `codexHome` (Default when none does): the one the
|
|
13299
|
+
// Codex panel's selector reads, from the same cache as GET /api/codex/models.
|
|
12616
13300
|
async function codexAutomationModels(codexHome) {
|
|
12617
13301
|
const home = codexHome || CODEX_DEFAULT_HOME;
|
|
12618
|
-
|
|
12619
|
-
|
|
12620
|
-
|
|
12621
|
-
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
return models;
|
|
13302
|
+
let accountId = 'default';
|
|
13303
|
+
try {
|
|
13304
|
+
const owner = _codexAccounts().list().find((row) => row?.home && samePath(row.home, home));
|
|
13305
|
+
if (owner?.id) accountId = owner.id;
|
|
13306
|
+
} catch { /* the default account */ }
|
|
13307
|
+
const models = await _codexModelListCache.get(codexModelListKey(accountId));
|
|
13308
|
+
return Array.isArray(models) ? models : [];
|
|
12625
13309
|
}
|
|
12626
13310
|
|
|
12627
13311
|
async function resolveAutomationLaunchChoice(profile, model, contextMode, codexHome) {
|
|
@@ -13497,6 +14181,8 @@ app.post('/api/loop/launch', async (req, res) => {
|
|
|
13497
14181
|
mcpProfile: cliMcpProfile,
|
|
13498
14182
|
codexAccountId: codexAccount?.id || null,
|
|
13499
14183
|
codexHome: codexAccount ? (codexAccount.home || CODEX_DEFAULT_HOME) : null,
|
|
14184
|
+
// A Codex exec loop keeps the Admin sandbox setting it started with (every iteration is its own `codex exec`).
|
|
14185
|
+
adminSandbox: cliProfile === 'codex' ? adminSandboxOn() : undefined,
|
|
13500
14186
|
// Continuation mechanism. 'hook': Claude Code ONLY — the Stop/UserPromptSubmit hooks in
|
|
13501
14187
|
// .claude/ set awaitingNext and attachLoopDriver() drives /clear + re-prompt; if those
|
|
13502
14188
|
// hooks are absent (e.g. a claude-code task run under a hookless harness) the loop STALLS.
|
|
@@ -14495,6 +15181,8 @@ async function launchScheduledLoop(schedule) {
|
|
|
14495
15181
|
mcpProfile: cliMcpProfile || null,
|
|
14496
15182
|
codexAccountId: codexAccount?.id || null,
|
|
14497
15183
|
codexHome: codexAccount ? (codexAccount.home || CODEX_DEFAULT_HOME) : null,
|
|
15184
|
+
// A Codex exec loop keeps the Admin sandbox setting it started with (every iteration is its own `codex exec`).
|
|
15185
|
+
adminSandbox: cliProfile === 'codex' ? adminSandboxOn() : undefined,
|
|
14498
15186
|
driverType: nativeRuntime ? 'native' : (cliProfile === 'claude-code' ? 'hook' : 'exec'),
|
|
14499
15187
|
runtimeType: nativeRuntime ? 'native' : 'pty',
|
|
14500
15188
|
surface: nativeRuntime ? 'sidepanel' : 'terminal',
|
|
@@ -16593,18 +17281,52 @@ async function checkSynabunUpdate() {
|
|
|
16593
17281
|
}
|
|
16594
17282
|
|
|
16595
17283
|
app.get('/api/system/version', async (req, res) => {
|
|
16596
|
-
|
|
16597
|
-
|
|
17284
|
+
// Settings > App: with automatic updates off the server looks only when the user asks (?force=1). A page's own
|
|
17285
|
+
// timer or a drawer that refreshes itself says `passive=1`, and never makes it look then.
|
|
17286
|
+
const auto = automaticUpdateCheck();
|
|
17287
|
+
const force = req.query.force === '1' && (auto || req.query.passive !== '1');
|
|
17288
|
+
if (force || (auto && !_synabunUpdateCache.checkedAt)) {
|
|
16598
17289
|
try { await checkSynabunUpdate(); } catch {}
|
|
16599
|
-
} else {
|
|
17290
|
+
} else if (auto) {
|
|
16600
17291
|
const age = Date.now() - new Date(_synabunUpdateCache.checkedAt).getTime();
|
|
16601
17292
|
if (age > 60 * 60 * 1000) {
|
|
16602
17293
|
checkSynabunUpdate().catch(() => {});
|
|
16603
17294
|
}
|
|
16604
17295
|
}
|
|
16605
|
-
res.json(_synabunUpdateCache);
|
|
17296
|
+
res.json({ ..._synabunUpdateCache, current: _synabunUpdateCache.current || installedSynabunVersion(), autoCheck: auto });
|
|
16606
17297
|
});
|
|
16607
17298
|
|
|
17299
|
+
// ── Settings > App (lib/app-controls-api.js): automatic update checks, start at login, the data and logs folders ──
|
|
17300
|
+
// The desktop app keeps its own (packaging/desktop/src/app-prefs.mjs): there this only says so.
|
|
17301
|
+
const appSettings = createAppSettingsStore({ path: join(DATA_HOME, 'data', 'app-settings.json') });
|
|
17302
|
+
/** Does the server look for updates by itself (at start, and when the page asks without ?force=1)? */
|
|
17303
|
+
function automaticUpdateCheck() {
|
|
17304
|
+
if (PACKAGED_RUNTIME?.shell) return true;
|
|
17305
|
+
try { return appSettings.get().autoUpdateCheck !== false; } catch { return true; }
|
|
17306
|
+
}
|
|
17307
|
+
function installedSynabunVersion() {
|
|
17308
|
+
try { return JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf-8')).version || null; } catch { return null; }
|
|
17309
|
+
}
|
|
17310
|
+
const loginStart = createLoginStart({
|
|
17311
|
+
packageRoot: PACKAGE_ROOT,
|
|
17312
|
+
home: os.homedir(),
|
|
17313
|
+
dataHome: DATA_HOME,
|
|
17314
|
+
entry: PACKAGED_RUNTIME && !PACKAGED_RUNTIME.shell ? durableEntry(PACKAGED_RUNTIME) : null,
|
|
17315
|
+
desktopShell: !!PACKAGED_RUNTIME?.shell,
|
|
17316
|
+
// Never from a test run or a throwaway data folder (the same rule as the synabun:// handler).
|
|
17317
|
+
allowed: () => launcherRegistrationAllowed({ env: process.env, dataHome: DATA_HOME }),
|
|
17318
|
+
log: (line) => console.log(`[app] ${line}`),
|
|
17319
|
+
});
|
|
17320
|
+
app.use('/api/system/app', createAppControlsApi({
|
|
17321
|
+
store: appSettings,
|
|
17322
|
+
loginStart,
|
|
17323
|
+
folders: { data: DATA_HOME, logs: join(DATA_HOME, 'data') },
|
|
17324
|
+
install: PACKAGED_RUNTIME?.shell ? 'desktop' : INSTALL_KIND,
|
|
17325
|
+
isGuestRequest,
|
|
17326
|
+
port: () => PORT,
|
|
17327
|
+
log: (line) => console.warn(line),
|
|
17328
|
+
}));
|
|
17329
|
+
|
|
16608
17330
|
// GET /api/system/about — Settings → About (lib/system-about.js): the app, the runtime, the project's links and
|
|
16609
17331
|
// every dependency of the product's packages with the version installed, read from the files on disk on the first
|
|
16610
17332
|
// request and kept for the life of the process. No environment, no token, no setting. Owner only (/api/system is
|
|
@@ -17150,12 +17872,13 @@ app.post('/api/system/restore', async (req, res) => {
|
|
|
17150
17872
|
const mcpDistCheck = resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'index.js');
|
|
17151
17873
|
if (!existsSync(mcpDistCheck)) {
|
|
17152
17874
|
try {
|
|
17153
|
-
//
|
|
17875
|
+
// TypeScript is a devDependency of the package root (a development checkout also has it in mcp-server/).
|
|
17154
17876
|
const mcpDir = resolve(PACKAGE_ROOT, 'mcp-server');
|
|
17155
|
-
if (!
|
|
17156
|
-
|
|
17877
|
+
if (!mcpCompiler(PACKAGE_ROOT)) {
|
|
17878
|
+
const npm = npmCommand(npmInvocation(), fallbackInstallArgs({ includeDev: true }));
|
|
17879
|
+
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' })) });
|
|
17157
17880
|
}
|
|
17158
|
-
|
|
17881
|
+
await execFileAsync(process.execPath, [mcpCompiler(PACKAGE_ROOT), '-p', mcpDir], { cwd: mcpDir, timeout: 120_000, maxBuffer: 16 * 1024 * 1024, windowsHide: true });
|
|
17159
17882
|
results.files.push('(auto-built MCP server)');
|
|
17160
17883
|
} catch (buildErr) {
|
|
17161
17884
|
results.errors.push(`MCP server auto-build failed: ${buildErr.message}`);
|
|
@@ -17422,6 +18145,8 @@ const editorConfigStore = createEditorConfigStore({
|
|
|
17422
18145
|
// the first request, stopped after ten idle minutes and at shutdown, never the shared serve), or a
|
|
17423
18146
|
// user-configured endpoint is, whose key is SYNABUN_EDITOR_AI_KEY in the data home's .env.
|
|
17424
18147
|
const editorEndpointKey = createEndpointKeyStore({ envPath: ENV_PATH, parseEnvFile, writeEnvFile });
|
|
18148
|
+
// The completion serve's process → its record (trackOpencodeServe), from its spawn until it has ended.
|
|
18149
|
+
const editorCompletionServeTracks = new WeakMap();
|
|
17425
18150
|
const editorCompletionServe = createOpencodeCompletionEngine({
|
|
17426
18151
|
dataDir: resolve(DATA_HOME, 'data', 'editor-complete'),
|
|
17427
18152
|
system: COMPLETION_SYSTEM,
|
|
@@ -17431,18 +18156,30 @@ const editorCompletionServe = createOpencodeCompletionEngine({
|
|
|
17431
18156
|
},
|
|
17432
18157
|
// A provider's {file:./key} is relative to that configuration: it is written whole for the serve's own folder.
|
|
17433
18158
|
userConfigDir: () => dirname(getOpencodeConfigPath()),
|
|
17434
|
-
|
|
18159
|
+
// What an earlier SynaBun left running goes first (lib/opencode-serve-records.js), then a port is picked.
|
|
18160
|
+
allocPort: async () => { await sweepLeftoverOpencodeServes(); return allocOpencodeIsoPort(); },
|
|
18161
|
+
// On record from its spawn (spawnServe), its record gone with its process: a crash of SynaBun does not leave it
|
|
18162
|
+
// running. Once something answers on its port: is it this start's own serve ('foreign': another server took the
|
|
18163
|
+
// port, which the engine leaves running while it tries another)?
|
|
18164
|
+
trackServe: ({ proc }) => editorCompletionServeTracks.get(proc)?.answering(),
|
|
17435
18165
|
// As the isolated serves are started (opencodeServeLaunch): the CLI the resolver found, through the login shell.
|
|
17436
|
-
|
|
17437
|
-
|
|
18166
|
+
// OpenCode 2 gets the password the engine made for this start, and its start waits its turn among every serve
|
|
18167
|
+
// SynaBun starts (one OpenCode 2 database cannot be opened by two serves at once).
|
|
18168
|
+
detectProtocol: () => detectOpencodeProtocol(),
|
|
18169
|
+
startOneAtATime: oneOpencodeStartAtATime,
|
|
18170
|
+
spawnServe: ({ port, env, cwd, protocol = 1, password = null }) => {
|
|
18171
|
+
const token = opencodeServeRecords.newToken();
|
|
18172
|
+
const launch = opencodeServeLaunch(port, { env: { ...env, [SERVE_TOKEN_ENV]: token }, protocol, password });
|
|
17438
18173
|
const proc = spawn(launch.command, launch.args, { stdio: ['ignore', 'ignore', 'pipe'], ...launch.options, cwd, detached: false, shell: false, windowsHide: true });
|
|
18174
|
+
editorCompletionServeTracks.set(proc, trackOpencodeServe(proc, { kind: 'completion', id: 'editor-complete', port, token }));
|
|
17439
18175
|
proc.stderr?.on('data', (d) => console.log(`[editor-complete:err] ${String(d).trim().slice(0, 500)}`));
|
|
17440
18176
|
return proc;
|
|
17441
18177
|
},
|
|
17442
|
-
|
|
18178
|
+
// SIGTERM to stop it; SIGKILL, through its handle, for a failed start's process that did not end (Windows: taskkill /T /F).
|
|
18179
|
+
killServe: (proc, signal = 'SIGTERM') => {
|
|
17443
18180
|
try {
|
|
17444
18181
|
if (IS_WIN && proc.pid) spawn('taskkill', ['/pid', String(proc.pid), '/T', '/F'], { shell: true, windowsHide: true, stdio: 'ignore' });
|
|
17445
|
-
else proc.kill(
|
|
18182
|
+
else proc.kill(signal);
|
|
17446
18183
|
} catch { /* already gone */ }
|
|
17447
18184
|
},
|
|
17448
18185
|
createClient: opencodeV2Client.createClientInstance,
|
|
@@ -17455,8 +18192,8 @@ const editorCompletion = createEditorCompletionService({
|
|
|
17455
18192
|
// The model list comes from the shared serve while it runs: listing models starts nothing.
|
|
17456
18193
|
sharedProviders: async () => {
|
|
17457
18194
|
if (!_ocpReady) return null;
|
|
17458
|
-
|
|
17459
|
-
return
|
|
18195
|
+
// The connected providers, from the kept list (the same read as the pickers').
|
|
18196
|
+
try { return (await readOpencodeProviders('compact')).compact; } catch { return null; }
|
|
17460
18197
|
},
|
|
17461
18198
|
isHiddenModel: (id) => opencodeHiddenModels.has(id),
|
|
17462
18199
|
usage: createUsageStore({
|
|
@@ -18421,29 +19158,34 @@ app.post('/api/setup/build', async (req, res) => {
|
|
|
18421
19158
|
const mcpDist = resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'index.js');
|
|
18422
19159
|
const mcpNeedsBuild = !existsSync(mcpDist);
|
|
18423
19160
|
|
|
18424
|
-
// Phase 1: npm
|
|
18425
|
-
|
|
18426
|
-
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
}
|
|
18434
|
-
|
|
18435
|
-
|
|
19161
|
+
// Phase 1: the dependencies. `npm i -g synabun` installed them at the package root (lib/runtime-dependencies.js
|
|
19162
|
+
// checks that Node finds them from both parts); only what is missing is installed, once, at the package root,
|
|
19163
|
+
// and never inside a packaged application. A fresh checkout also needs the TypeScript that builds the MCP server.
|
|
19164
|
+
const report = checkRuntimeDependencies({ packageRoot: PACKAGE_ROOT });
|
|
19165
|
+
const compilerMissing = mcpNeedsBuild && !mcpCompiler(PACKAGE_ROOT);
|
|
19166
|
+
if (report.ok && !compilerMissing) {
|
|
19167
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'skipped', output: 'dependencies already installed' });
|
|
19168
|
+
} else if (PACKAGED_RUNTIME) {
|
|
19169
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'error', output: 'This application is incomplete: reinstall SynaBun.' });
|
|
19170
|
+
send('complete', { ok: false, error: 'The application is incomplete' });
|
|
19171
|
+
return res.end();
|
|
19172
|
+
} else {
|
|
19173
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'start' });
|
|
18436
19174
|
try {
|
|
18437
|
-
// The user's own
|
|
18438
|
-
const
|
|
18439
|
-
|
|
18440
|
-
|
|
18441
|
-
|
|
19175
|
+
// The npm that started SynaBun, else the user's own on PATH, else the one next to this Node (npmInvocation).
|
|
19176
|
+
const npm = npmCommand(npmInvocation(), fallbackInstallArgs({ includeDev: mcpNeedsBuild }));
|
|
19177
|
+
const { stdout } = await execFileAsync(npm.file, npm.args, {
|
|
19178
|
+
cwd: PACKAGE_ROOT,
|
|
19179
|
+
timeout: 600_000,
|
|
19180
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
19181
|
+
shell: npm.shell,
|
|
19182
|
+
windowsHide: true,
|
|
19183
|
+
env: withPackagedPathLast(fallbackInstallEnv({ ...process.env, NODE_ENV: 'development' })),
|
|
18442
19184
|
});
|
|
18443
|
-
send('install', { dir: dir.name, status: 'done', output: tail(stdout) });
|
|
19185
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'done', output: tail(stdout) });
|
|
18444
19186
|
} catch (err) {
|
|
18445
|
-
send('install', { dir: dir.name, status: 'error', output: tail(err.stderr || err.message) });
|
|
18446
|
-
send('complete', { ok: false, error:
|
|
19187
|
+
for (const dir of dirs) send('install', { dir: dir.name, status: 'error', output: tail(err.stderr || err.message) });
|
|
19188
|
+
send('complete', { ok: false, error: 'npm install failed in the package root' });
|
|
18447
19189
|
return res.end();
|
|
18448
19190
|
}
|
|
18449
19191
|
}
|
|
@@ -18460,10 +19202,14 @@ app.post('/api/setup/build', async (req, res) => {
|
|
|
18460
19202
|
} else {
|
|
18461
19203
|
send('build', { status: 'start' });
|
|
18462
19204
|
try {
|
|
18463
|
-
|
|
19205
|
+
// The TypeScript Node finds from mcp-server/ (the root's devDependency in a fresh checkout), by this Node.
|
|
19206
|
+
const tsc = mcpCompiler(PACKAGE_ROOT);
|
|
19207
|
+
if (!tsc) throw new Error('TypeScript is not installed: npm install in the package root installs it');
|
|
19208
|
+
const { stdout } = await execFileAsync(process.execPath, [tsc, '-p', resolve(PACKAGE_ROOT, 'mcp-server')], {
|
|
18464
19209
|
cwd: resolve(PACKAGE_ROOT, 'mcp-server'),
|
|
18465
|
-
timeout:
|
|
18466
|
-
|
|
19210
|
+
timeout: 120_000,
|
|
19211
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
19212
|
+
windowsHide: true,
|
|
18467
19213
|
});
|
|
18468
19214
|
send('build', { status: 'done', output: tail(stdout) });
|
|
18469
19215
|
} catch (err) {
|
|
@@ -20000,11 +20746,13 @@ app.get('/api/opencode/sessions', (req, res) => {
|
|
|
20000
20746
|
|
|
20001
20747
|
let rows;
|
|
20002
20748
|
try {
|
|
20749
|
+
// OpenCode 1's tables, or OpenCode 2's (lib/opencode-db-schema.js).
|
|
20750
|
+
const tables = sessionTablesFor(opencodeDbFlavor(db, _ocpProtocol));
|
|
20003
20751
|
const stmt = db.prepare(`
|
|
20004
20752
|
SELECT s.id, s.title, s.directory, s.time_created, s.time_updated,
|
|
20005
20753
|
p.worktree as project_worktree,
|
|
20006
|
-
|
|
20007
|
-
FROM session s
|
|
20754
|
+
${tables.messageCount} as message_count
|
|
20755
|
+
FROM ${tables.session} s
|
|
20008
20756
|
LEFT JOIN project p ON s.project_id = p.id
|
|
20009
20757
|
WHERE s.time_archived IS NULL
|
|
20010
20758
|
ORDER BY s.time_updated DESC
|
|
@@ -20547,7 +21295,7 @@ app.post('/api/session-indexing/mirror', async (req, res) => {
|
|
|
20547
21295
|
try {
|
|
20548
21296
|
await mirrorExistingChunks((event) => {
|
|
20549
21297
|
try { broadcastSync(event); } catch {}
|
|
20550
|
-
});
|
|
21298
|
+
}, { ensureCategory: () => ensureFeatureCategory('conversations') });
|
|
20551
21299
|
} catch (err) {
|
|
20552
21300
|
console.error('Mirror error:', err);
|
|
20553
21301
|
try { broadcastSync({ type: 'mirror:error', error: err.message }); } catch {}
|
|
@@ -20686,16 +21434,15 @@ app.post('/api/claude-code/integrations', (req, res) => {
|
|
|
20686
21434
|
writeClaudeSettings(filePath, settings);
|
|
20687
21435
|
}
|
|
20688
21436
|
|
|
20689
|
-
// Save to registered projects list
|
|
21437
|
+
// Save to registered projects list, and create the new project's default category tree (parent + children)
|
|
21438
|
+
// then, once: a project already registered gets nothing, so a default the user deleted stays deleted
|
|
20690
21439
|
const projectLabel = label || basename(normalized);
|
|
20691
21440
|
if (!registered) {
|
|
20692
21441
|
projects.push({ path: normalized, label: projectLabel });
|
|
20693
21442
|
saveHookProjects(projects);
|
|
21443
|
+
try { ensureProjectCategories([{ path: normalized, label: projectLabel }]); } catch {}
|
|
20694
21444
|
}
|
|
20695
21445
|
|
|
20696
|
-
// Create default category tree for the project (parent + children)
|
|
20697
|
-
try { ensureProjectCategories(); } catch {}
|
|
20698
|
-
|
|
20699
21446
|
// Probe for git "dubious ownership" — signals the UI to show an explicit trust prompt.
|
|
20700
21447
|
// Only relevant on Windows when the project dir is owned by a different SID than the
|
|
20701
21448
|
// account running Node (external drives, copied repos, etc.). No-op on Mac/Linux.
|
|
@@ -22310,6 +23057,8 @@ app.get('/api/skills-studio/artifact/:id/icon', (req, res) => {
|
|
|
22310
23057
|
const mimeMap = { '.png': 'image/png', '.svg': 'image/svg+xml', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.webp': 'image/webp' };
|
|
22311
23058
|
res.setHeader('Content-Type', mimeMap[ext] || 'application/octet-stream');
|
|
22312
23059
|
res.setHeader('Cache-Control', 'no-cache');
|
|
23060
|
+
// A skill's icon is whatever file the skill folder holds: opened by its address it runs nothing.
|
|
23061
|
+
setUntrustedFileHeaders(res, iconPath, mimeMap[ext]);
|
|
22313
23062
|
res.send(readConfinedFile(resolveArtifactFile(id).realBase, iconPath, { maxBytes: ICON_MAX_BYTES }));
|
|
22314
23063
|
} catch (err) {
|
|
22315
23064
|
res.status(skillsStudioStatus(err)).json({ error: err.message });
|
|
@@ -24282,7 +25031,8 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
24282
25031
|
'claude-code': (m) => `${cmd} --model ${m}`,
|
|
24283
25032
|
'codex': (m) => `${cmd} --model ${m}`,
|
|
24284
25033
|
'gemini': (m) => `${cmd} --model ${m}`,
|
|
24285
|
-
'
|
|
25034
|
+
// OpenCode 2's terminal app has no --model (it prints its help and exits): the model is picked in it.
|
|
25035
|
+
'opencode': (m) => (knownOpencodeMajor() >= 2 ? cmd : `${cmd} --model ${m}`),
|
|
24286
25036
|
};
|
|
24287
25037
|
const builder = modelMap[profileId];
|
|
24288
25038
|
if (builder) cmd = builder(quotedModel);
|
|
@@ -24299,6 +25049,11 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
24299
25049
|
cmd = `${cmd} -c model_reasoning_effort="${codexEffort}"`;
|
|
24300
25050
|
}
|
|
24301
25051
|
}
|
|
25052
|
+
// Every Codex terminal: the Windows sandbox the Admin sandbox setting allows. A bare value
|
|
25053
|
+
// (Codex reads one that is not TOML as a string), so no shell can strip a quote from it.
|
|
25054
|
+
if (profileId === 'codex') {
|
|
25055
|
+
cmd = `${cmd} -c ${terminalArgument(`windows.sandbox=${codexWindowsSandbox(adminSandboxOn())}`, DEFAULT_SHELL)}`;
|
|
25056
|
+
}
|
|
24302
25057
|
|
|
24303
25058
|
// Loop-only: bypass permission prompts for unattended Claude Code loops.
|
|
24304
25059
|
// The loop runs in a PTY with no human to answer prompts, so accessing
|
|
@@ -25132,6 +25887,9 @@ async function releaseNativeLoopResources(run) {
|
|
|
25132
25887
|
reconcileKeepAwake();
|
|
25133
25888
|
}
|
|
25134
25889
|
|
|
25890
|
+
// An OpenCode worker's tool calls, for the Assistant's run stages and tray: a compact copy, at a bounded rate.
|
|
25891
|
+
const openCodeWorkerActivity = createOpenCodeActivityRelay({ send: (message) => broadcastSync(message) });
|
|
25892
|
+
|
|
25135
25893
|
_nativeLoopRuntime = new NativeLoopRuntime({
|
|
25136
25894
|
stateDir: LOOP_DIR,
|
|
25137
25895
|
ledgerPath: resolve(LOOP_DATA_DIR, 'native-loop-runs.json'),
|
|
@@ -25142,11 +25900,10 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25142
25900
|
judgeGoal: createLoopGoalJudge({ judgeLoopGoal, surfaceConfig: typesafeSurfaceConfig }),
|
|
25143
25901
|
log: (runId, tag, message, meta) => loopLog(runId, tag, message, meta),
|
|
25144
25902
|
onEvent: (message) => {
|
|
25145
|
-
// OpenCode already streams from its isolated serve into the V2 panel
|
|
25146
|
-
//
|
|
25147
|
-
|
|
25148
|
-
|
|
25149
|
-
}
|
|
25903
|
+
// OpenCode already streams from its isolated serve into the V2 panel: only its
|
|
25904
|
+
// tool calls go to the sync socket, compact (lib/opencode-worker-activity.js).
|
|
25905
|
+
// Claude and Codex SDK events need the shared sync socket to reach their renderers.
|
|
25906
|
+
if (!openCodeWorkerActivity.observe(message)) broadcastSync(message);
|
|
25150
25907
|
},
|
|
25151
25908
|
onFinish: releaseNativeLoopResources,
|
|
25152
25909
|
providerFactories: {
|
|
@@ -25194,6 +25951,8 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25194
25951
|
extraEnv: { ...(state.extraEnv || {}), ...codexExecEnv(launch) },
|
|
25195
25952
|
unsetEnv: launch.unset,
|
|
25196
25953
|
codexHome: launch.home,
|
|
25954
|
+
// The Admin sandbox setting as the run starts: it keeps that Windows sandbox while it lives.
|
|
25955
|
+
adminSandbox: adminSandboxOn(),
|
|
25197
25956
|
// Native schedules use the same trusted global CLI as the interactive
|
|
25198
25957
|
// sidepanel; SynaBun carries no Codex for the SDK to find by itself.
|
|
25199
25958
|
codexPath: getNativeCodexBin(),
|
|
@@ -25283,6 +26042,8 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25283
26042
|
return codexAccountAuth().rateLimits(id, opts);
|
|
25284
26043
|
} });
|
|
25285
26044
|
_assistantAccountUsage = accountUsage;
|
|
26045
|
+
// The quotas the account menus show, read once now that they can be (the launch warm-up may have run first).
|
|
26046
|
+
scheduleLaunchRefresh('account-usage', 5000);
|
|
25286
26047
|
claudeAccounts.subscribe(({ accountId }) => accountUsage.invalidate('claude-code', accountId));
|
|
25287
26048
|
_codexAccounts().subscribe(({ accountId }) => accountUsage.invalidate('codex', accountId));
|
|
25288
26049
|
// A Claude Code worker whose dispatch names no account runs under this one (Assistant → accounts).
|
|
@@ -25350,6 +26111,12 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25350
26111
|
});
|
|
25351
26112
|
const assistantUsage = createUsageLedger({ dataDir: resolve(DATA_HOME, 'data'), log: assistantLog, judgments });
|
|
25352
26113
|
const catalogFetch = async (path) => {
|
|
26114
|
+
// OpenCode's providers are read in process, the connected ones only (the whole models.dev catalog is
|
|
26115
|
+
// megabytes and the catalog lists connected providers alone): what the route answers for ?view=compact.
|
|
26116
|
+
if (path === '/api/opencode/providers/full') {
|
|
26117
|
+
if (!_ocpReady) return { ok: false, error: 'OpenCode server not running', data: {} };
|
|
26118
|
+
try { return { ok: true, data: (await readOpencodeProviders('compact')).compact }; } catch { return null; }
|
|
26119
|
+
}
|
|
25353
26120
|
try {
|
|
25354
26121
|
const response = await fetch(`http://127.0.0.1:${PORT}${path}`, { signal: AbortSignal.timeout(4000) });
|
|
25355
26122
|
return response.ok ? await response.json() : null;
|
|
@@ -25377,9 +26144,13 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25377
26144
|
reconcileHidden: (args) => assistantHiddenModels.reconcile(args),
|
|
25378
26145
|
// A provider lists other models than before: every open window reloads its menus.
|
|
25379
26146
|
onChange: ({ providers }) => broadcastSync({ type: 'assistant:catalog-changed', providers }),
|
|
26147
|
+
// An old build answers at once while the next is made (its lists are kept by their own caches).
|
|
26148
|
+
staleWhileRevalidate: true,
|
|
25380
26149
|
log: (tag, message) => assistantLog(tag, message),
|
|
25381
26150
|
});
|
|
25382
26151
|
_assistantCatalog = assistantCatalog;
|
|
26152
|
+
// Built once the provider lists are warm (the launch warm-up may have run before the Assistant existed).
|
|
26153
|
+
scheduleLaunchRefresh('assistant-catalog', 8000);
|
|
25383
26154
|
// A Models toggle or a Settings → OpenCode checkbox reaches routing on the next call.
|
|
25384
26155
|
assistantHiddenModels.onChange((change) => {
|
|
25385
26156
|
// What a catalog build settled about a hidden alias is already in that build. When it
|
|
@@ -25549,7 +26320,8 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25549
26320
|
app.use('/api/assistant/hidden-models', createAssistantHiddenModelsRouter({
|
|
25550
26321
|
hiddenModels: assistantHiddenModels, catalog: assistantCatalog, opencodeStore: opencodeHiddenModels, isGuestRequest, broadcastSync,
|
|
25551
26322
|
}));
|
|
25552
|
-
|
|
26323
|
+
// Quotas as last read (read again in the background): a window's menu never waits for the quota probe.
|
|
26324
|
+
const buildCatalog = async (opts = {}) => { accountUsage.refresh().catch(() => {}); return { ...(await assistantCatalog.get(opts)), accounts: accountUsage.list({ stale: true }) }; };
|
|
25553
26325
|
// Clarification: a materially ambiguous request gets 1-3 questions before it
|
|
25554
26326
|
// is routed; the user's words and answers become the brief every worker of
|
|
25555
26327
|
// that request receives. Knobs: assistant-config.json `clarify` (read live).
|
|
@@ -25738,6 +26510,8 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25738
26510
|
userContext: assistantUserContext,
|
|
25739
26511
|
// The greeting a conversation opened with (told to the brain once) and when the person was last here.
|
|
25740
26512
|
greeting: _greeting,
|
|
26513
|
+
// The Admin sandbox setting: every session view shows it (features.adminSandbox gates the panel's control).
|
|
26514
|
+
adminSandbox: _agentSandbox,
|
|
25741
26515
|
});
|
|
25742
26516
|
assistantDesktop?.attach?.({ runtime: _assistantRuntime, dispatcher: _assistantDispatcher });
|
|
25743
26517
|
// Warm the catalog so brain capabilities (vision, tier) are known on first attach,
|
|
@@ -25794,6 +26568,8 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25794
26568
|
// router (startWhatsAppLink), and the port for that route's localhost Host check.
|
|
25795
26569
|
whatsapp: () => _whatsapp,
|
|
25796
26570
|
port: () => Number(PORT),
|
|
26571
|
+
// GET / PUT /api/assistant/admin-sandbox (the PUT answers UI_ONLY to agents).
|
|
26572
|
+
adminSandbox: _agentSandbox,
|
|
25797
26573
|
}));
|
|
25798
26574
|
const shutdownAssistant = () => {
|
|
25799
26575
|
try { assistantRouter.shutdown(); } catch {}
|
|
@@ -26056,6 +26832,9 @@ function prepareExecWrapperFile(terminalSessionId, loopState, taskFile, sentinel
|
|
|
26056
26832
|
model: loopState.model,
|
|
26057
26833
|
effort: loopState.effort,
|
|
26058
26834
|
mcpEnvOverrides,
|
|
26835
|
+
// The Admin sandbox setting the loop started with (a loop file from before it: the current one).
|
|
26836
|
+
codexConfigArgs: codexWindowsSandboxArgs(typeof loopState.adminSandbox === 'boolean' ? loopState.adminSandbox : adminSandboxOn()),
|
|
26837
|
+
...(profile === 'opencode' ? { opencodeMajor: knownOpencodeMajor() } : {}),
|
|
26059
26838
|
});
|
|
26060
26839
|
const wrapperFile = join(
|
|
26061
26840
|
os.tmpdir(),
|
|
@@ -26781,6 +27560,7 @@ const providerPanelTerminalAuth = createProviderPanelTerminalAuth({
|
|
|
26781
27560
|
provider, accountId, claudeAccounts: _panelClaudeAccounts(), codexAccount: findCodexAccount,
|
|
26782
27561
|
claudeExecutable: getClaudeSdkExecutable, codexExecutable: getCodexBin,
|
|
26783
27562
|
env: withPathEnv(process.env, getAugmentedPath()),
|
|
27563
|
+
codexConfigArgs: codexSandboxArgs(),
|
|
26784
27564
|
}),
|
|
26785
27565
|
createSession: createTerminalSession,
|
|
26786
27566
|
whenSpawned: id => terminalHost.whenSpawned(id),
|
|
@@ -26855,7 +27635,8 @@ app.post('/api/terminal/sessions', async (req, res) => {
|
|
|
26855
27635
|
} catch (err) {
|
|
26856
27636
|
let msg = err.message;
|
|
26857
27637
|
if (process.platform !== 'win32' && (msg.includes('posix_spawn') || msg.includes('EACCES'))) {
|
|
26858
|
-
|
|
27638
|
+
const ptyDir = findInstalled(__dirname, 'node-pty', { stopAt: PACKAGE_ROOT })?.path || join(__dirname, 'node_modules', 'node-pty');
|
|
27639
|
+
msg = `Terminal failed: spawn-helper lacks execute permission. Try: chmod +x "${join(ptyDir, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper')}"`;
|
|
26859
27640
|
}
|
|
26860
27641
|
return res.status(500).json({ error: msg });
|
|
26861
27642
|
}
|
|
@@ -26937,6 +27718,7 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
26937
27718
|
let _gitMissingLogged = false;
|
|
26938
27719
|
let _gitExePath = null; // Cached absolute path — preferring .exe on Windows
|
|
26939
27720
|
let _gitResolved = false;
|
|
27721
|
+
let _gitResolving = null; // the lookup resolveGitPathInBackground() runs, while it runs
|
|
26940
27722
|
|
|
26941
27723
|
// Resolve git's absolute path at first use. On Windows, `where git` lists all matches;
|
|
26942
27724
|
// prefer .exe so spawnSync works without cmd.exe quoting issues. On Unix, use `command -v`.
|
|
@@ -27002,15 +27784,39 @@ function logGitFailure(where, err) {
|
|
|
27002
27784
|
console.warn(`[git] '${where}' failed:`, msg.trim().split('\n')[0]);
|
|
27003
27785
|
}
|
|
27004
27786
|
|
|
27005
|
-
//
|
|
27787
|
+
// resolveGitPath()'s lookup without blocking: `where git` (Windows) or `command -v git`, run once in the
|
|
27788
|
+
// background at start-up, so the first git call of a request finds the answer kept instead of running the
|
|
27789
|
+
// lookup itself (a synchronous spawn through cmd.exe holds every request on a Windows PC without git).
|
|
27790
|
+
function resolveGitPathInBackground() {
|
|
27791
|
+
if (_gitResolved) return Promise.resolve(_gitExePath || gitBash.gitExecutable());
|
|
27792
|
+
if (_gitResolving) return _gitResolving;
|
|
27793
|
+
_gitResolving = new Promise((done) => {
|
|
27794
|
+
const [command, args] = IS_WINDOWS ? ['where', ['git']] : ['/bin/sh', ['-c', 'command -v git']];
|
|
27795
|
+
execFile(command, args, { encoding: 'utf8', timeout: 3000, windowsHide: true }, (error, stdout) => {
|
|
27796
|
+
if (!_gitResolved) {
|
|
27797
|
+
_gitResolved = true;
|
|
27798
|
+
const lines = error ? [] : String(stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
27799
|
+
_gitExePath = (IS_WINDOWS ? lines.find((line) => /\.exe$/i.test(line)) : null) || lines[0] || null;
|
|
27800
|
+
if (_gitExePath) console.log(`[git] resolved: ${_gitExePath}`);
|
|
27801
|
+
else console.warn('[git] resolution failed:', error?.message || 'resolver returned no paths');
|
|
27802
|
+
}
|
|
27803
|
+
done(_gitExePath || gitBash.gitExecutable());
|
|
27804
|
+
});
|
|
27805
|
+
}).finally(() => { _gitResolving = null; });
|
|
27806
|
+
return _gitResolving;
|
|
27807
|
+
}
|
|
27808
|
+
|
|
27809
|
+
// Startup probe — always logs git status so Windows users can see whether git works at all. Asynchronous:
|
|
27810
|
+
// after the lookup above, one `git --version` (no folder, no repository's configuration) off the event loop.
|
|
27006
27811
|
setImmediate(() => {
|
|
27007
|
-
|
|
27008
|
-
|
|
27009
|
-
|
|
27010
|
-
|
|
27011
|
-
|
|
27012
|
-
|
|
27013
|
-
|
|
27812
|
+
resolveGitPathInBackground().then((gitPath) => new Promise((done) => {
|
|
27813
|
+
const shell = IS_WINDOWS && (!gitPath || /\.(cmd|bat)$/i.test(gitPath));
|
|
27814
|
+
execFile(gitPath || 'git', ['--version'], { encoding: 'utf8', timeout: 3000, windowsHide: true, shell }, (error, stdout, stderr) => {
|
|
27815
|
+
if (!error) console.log(`[git] ${String(stdout || '').trim()} — OK`);
|
|
27816
|
+
else console.warn(`[git] startup probe FAILED: ${String(stderr || '').trim() || error.message || 'unknown error'}. File explorer git UI will be hidden.`);
|
|
27817
|
+
done();
|
|
27818
|
+
});
|
|
27819
|
+
})).catch(() => {});
|
|
27014
27820
|
});
|
|
27015
27821
|
|
|
27016
27822
|
// Git "dubious ownership" detection — CVE-2022-24765 safety check refuses to operate on
|
|
@@ -27897,7 +28703,9 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
27897
28703
|
const found = CLI_DEFAULTS[profile] && command === CLI_DEFAULTS[profile].command ? cliResolver.launcher(profile) : null;
|
|
27898
28704
|
const direct = found?.spec?.kind === 'direct' ? found.launchPath : null;
|
|
27899
28705
|
let spawnCmd = direct || command;
|
|
27900
|
-
|
|
28706
|
+
// Codex: the Windows sandbox the Admin sandbox setting allows, before its flag (a bare value: no quote for a shell to lose).
|
|
28707
|
+
const leadArgs = profile === 'codex' ? ['-c', `windows.sandbox=${codexWindowsSandbox(adminSandboxOn())}`] : [];
|
|
28708
|
+
let spawnArgs = [...leadArgs, ...sidecarConfig.args];
|
|
27901
28709
|
let useShell = IS_WIN && !direct;
|
|
27902
28710
|
let tmpFile = null;
|
|
27903
28711
|
|
|
@@ -27909,10 +28717,11 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
27909
28717
|
// Use PowerShell which correctly passes $msg as a single argument value.
|
|
27910
28718
|
tmpFile = join(os.tmpdir(), `synabun-relay-${randomBytes(4).toString('hex')}.txt`);
|
|
27911
28719
|
writeFileSync(tmpFile, message, 'utf-8');
|
|
27912
|
-
const flag =
|
|
28720
|
+
const flag = sidecarConfig.args[0]; // e.g. '-p', '-q'
|
|
28721
|
+
const lead = leadArgs.map((word) => ` '${word}'`).join('');
|
|
27913
28722
|
spawnCmd = 'powershell.exe';
|
|
27914
28723
|
spawnArgs = ['-NoProfile', '-NonInteractive', '-Command',
|
|
27915
|
-
`$msg = Get-Content -Raw '${tmpFile}'; & '${String(direct || command).replace(/'/g, "''")}' ${flag} $msg`];
|
|
28724
|
+
`$msg = Get-Content -Raw '${tmpFile}'; & '${String(direct || command).replace(/'/g, "''")}'${lead} ${flag} $msg`];
|
|
27916
28725
|
useShell = false;
|
|
27917
28726
|
} else {
|
|
27918
28727
|
// Unix: pass directly as the flag's value arg (no shell quoting issues)
|
|
@@ -33383,6 +34192,83 @@ app.get('/api/browser/detect-browsers', (req, res) => {
|
|
|
33383
34192
|
res.json({ browsers });
|
|
33384
34193
|
});
|
|
33385
34194
|
|
|
34195
|
+
// --- Launch warm-up ---
|
|
34196
|
+
// After the server listens (never before it, and never in front of the first page), the slow lists the
|
|
34197
|
+
// windows, menus and Settings wait for are read once in the background: one step at a time, a pause between
|
|
34198
|
+
// two, so a first click finds them kept (lib/launch-cache.js) and nothing competes with the page. Every step
|
|
34199
|
+
// is asynchronous: a CLI runs as a child process, never through a synchronous spawn. A step that is not ready
|
|
34200
|
+
// yet (the Assistant still starting, OpenCode's serve not up) is skipped: its own ready event runs it again.
|
|
34201
|
+
// scheduleLaunchRefresh(name) runs one step again after an event (an account switch, a provider connected,
|
|
34202
|
+
// a serve restart), debounced, behind the same one-at-a-time queue.
|
|
34203
|
+
const _launchQueue = createLimiter(1);
|
|
34204
|
+
const _launchTimers = new Map();
|
|
34205
|
+
const LAUNCH_STEP_GAP_MS = 250;
|
|
34206
|
+
const LAUNCH_TASKS = {
|
|
34207
|
+
// Where git is: the lookup a first git call would otherwise run synchronously.
|
|
34208
|
+
'git-path': () => resolveGitPathInBackground(),
|
|
34209
|
+
// Claude Code's model list (one CLI start; the kept list answers meanwhile). Only where Claude Code is found.
|
|
34210
|
+
'claude-models': async () => { if (cliFoundForWarmup('claude-code')) await getClaudeModelsForClient(true); },
|
|
34211
|
+
// OpenCode's providers, once its serve answers (the serve's ready event runs this step again).
|
|
34212
|
+
'opencode-providers': async () => { if (_ocpReady) await readOpencodeProviders('compact', { refresh: true }); },
|
|
34213
|
+
// Every signed-in Codex account's model list (one app-server each, one after another).
|
|
34214
|
+
'codex-models': async () => {
|
|
34215
|
+
// Only where Codex is found: listing prepares the account's config.toml, which a machine without Codex never gets.
|
|
34216
|
+
if (!cliFoundForWarmup('codex')) return;
|
|
34217
|
+
let rows = [];
|
|
34218
|
+
try { rows = _codexAccounts().list().filter((row) => row?.signedIn !== false); } catch { rows = [{ id: 'default' }]; }
|
|
34219
|
+
for (const row of rows) {
|
|
34220
|
+
try { await _codexModelListCache.read(codexModelListKey(row.id), { refresh: true }); } catch { /* that account's list stays as kept */ }
|
|
34221
|
+
}
|
|
34222
|
+
},
|
|
34223
|
+
// The Assistant's catalog, from the lists above (every view the windows ask for is made from it).
|
|
34224
|
+
'assistant-catalog': async () => { await _assistantCatalog?.full?.({ force: true }); },
|
|
34225
|
+
// Subscription quotas the account menus show.
|
|
34226
|
+
'account-usage': async () => { await _assistantAccountUsage?.refresh?.(); },
|
|
34227
|
+
// What the Codex panel's settings and cog menu validate against (a Codex start per setting value, once per version).
|
|
34228
|
+
'codex-settings': async () => {
|
|
34229
|
+
const found = cliResolver.launcher('codex');
|
|
34230
|
+
const bin = found.launchPath || found.path;
|
|
34231
|
+
// A file the resolver checked (a bare command name or a command line has no binary identity to key on).
|
|
34232
|
+
if (found.state === 'installed' && bin && /[\\/]/.test(bin)) await readCodexSettingsSchema(bin).catch(() => null);
|
|
34233
|
+
},
|
|
34234
|
+
};
|
|
34235
|
+
|
|
34236
|
+
// A CLI the resolver found (probed or not yet): the warm-up starts nothing for a tool that is not installed.
|
|
34237
|
+
function cliFoundForWarmup(toolKey) {
|
|
34238
|
+
try {
|
|
34239
|
+
const found = cliResolver.launcher(toolKey);
|
|
34240
|
+
return !!found.path && found.state !== 'broken' && found.state !== 'missing';
|
|
34241
|
+
} catch { return false; }
|
|
34242
|
+
}
|
|
34243
|
+
|
|
34244
|
+
function runLaunchStep(name) {
|
|
34245
|
+
const task = LAUNCH_TASKS[name];
|
|
34246
|
+
if (!task) return Promise.resolve();
|
|
34247
|
+
return _launchQueue(async () => {
|
|
34248
|
+
const started = Date.now();
|
|
34249
|
+
try { await task(); } catch (error) { console.warn(`[launch-cache] ${name}: ${error?.message || error}`); }
|
|
34250
|
+
const took = Date.now() - started;
|
|
34251
|
+
if (took > 2000) console.log(`[launch-cache] ${name} warmed in ${(took / 1000).toFixed(1)} s`);
|
|
34252
|
+
await new Promise((done) => { const timer = setTimeout(done, LAUNCH_STEP_GAP_MS); timer.unref?.(); });
|
|
34253
|
+
});
|
|
34254
|
+
}
|
|
34255
|
+
|
|
34256
|
+
/** Run one warm-up step again soon (an event changed what it reads); bursts of events run it once. */
|
|
34257
|
+
function scheduleLaunchRefresh(name, delayMs = 600) {
|
|
34258
|
+
if (!LAUNCH_TASKS[name] || _launchTimers.has(name)) return;
|
|
34259
|
+
const timer = setTimeout(() => { _launchTimers.delete(name); runLaunchStep(name); }, delayMs);
|
|
34260
|
+
timer.unref?.();
|
|
34261
|
+
_launchTimers.set(name, timer);
|
|
34262
|
+
}
|
|
34263
|
+
|
|
34264
|
+
function startLaunchWarmup() {
|
|
34265
|
+
if (process.env.SYNABUN_LAUNCH_WARMUP === 'off') return;
|
|
34266
|
+
const timer = setTimeout(() => {
|
|
34267
|
+
for (const name of Object.keys(LAUNCH_TASKS)) runLaunchStep(name);
|
|
34268
|
+
}, 3000);
|
|
34269
|
+
timer.unref?.();
|
|
34270
|
+
}
|
|
34271
|
+
|
|
33386
34272
|
// --- Start ---
|
|
33387
34273
|
reloadConfig();
|
|
33388
34274
|
|
|
@@ -33415,6 +34301,15 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
33415
34301
|
else if (!launcher.ok) console.warn(`[server] Start Server launcher not registered: ${launcher.errors[0]}`);
|
|
33416
34302
|
} catch {}
|
|
33417
34303
|
|
|
34304
|
+
// Start at login (Settings > App): while it is on, its entry starts this install (another Node, a moved
|
|
34305
|
+
// install). One that is off, or that the user turned off in the system, is left as it is.
|
|
34306
|
+
try {
|
|
34307
|
+
const healed = loginStart.heal();
|
|
34308
|
+
if (healed.changed) console.log('[server] start at login: the entry now starts this install');
|
|
34309
|
+
} catch (err) {
|
|
34310
|
+
console.warn(`[server] start at login: ${err.message}`);
|
|
34311
|
+
}
|
|
34312
|
+
|
|
33418
34313
|
// Auto-heal legacy synabun-symlink plugin state and reinstall via CC CLI
|
|
33419
34314
|
try {
|
|
33420
34315
|
const repair = pluginInstaller.repairSynabunPlugins();
|
|
@@ -33462,11 +34357,19 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
33462
34357
|
// opencode-<sid> node_modules that never got their per-loop cleanup.
|
|
33463
34358
|
setTimeout(() => { try { cleanupStaleLoopConfigs(); } catch {} }, 1500);
|
|
33464
34359
|
|
|
34360
|
+
// OpenCode serves an earlier SynaBun left running (it crashed, or was killed): found by their records alone and
|
|
34361
|
+
// stopped only when they are provably its own and nothing uses them (lib/opencode-serve-records.js). An
|
|
34362
|
+
// OpenCode start that comes earlier makes the same look first (and this one then finds nothing left).
|
|
34363
|
+
setTimeout(() => { sweepLeftoverOpencodeServes(); }, 1000);
|
|
34364
|
+
|
|
33465
34365
|
// Compare the installed Claude CLI with the release the Agent SDK was built
|
|
33466
34366
|
// against, once, so an older installation is named in the log before the
|
|
33467
34367
|
// first session runs on it (checkClaudeCliSkew in lib/claude-model-catalog.js).
|
|
33468
34368
|
setTimeout(() => { checkClaudeCliSkew(getClaudeBin()).catch(() => {}); }, 1000);
|
|
33469
34369
|
|
|
34370
|
+
// The lists windows, menus and Settings wait for, read once in the background (see LAUNCH_TASKS above).
|
|
34371
|
+
startLaunchWarmup();
|
|
34372
|
+
|
|
33470
34373
|
// Windows: the Git Bash every Claude Code gets (lib/git-bash/). Looked for now (the settings of the user's own
|
|
33471
34374
|
// claude follow it); a PC with none, and an app that carries none, gets SynaBun's copy, fetched in the background.
|
|
33472
34375
|
setTimeout(() => {
|
|
@@ -33501,7 +34404,7 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
33501
34404
|
},
|
|
33502
34405
|
() => {
|
|
33503
34406
|
try {
|
|
33504
|
-
const oc = rebuildOpencodeCache({ getOpencodeDb });
|
|
34407
|
+
const oc = rebuildOpencodeCache({ getOpencodeDb, protocol: _ocpProtocol });
|
|
33505
34408
|
if (oc.updated > 0) console.log(` Session cache (opencode): indexed ${oc.updated}/${oc.sessions}`);
|
|
33506
34409
|
} catch (err) { console.warn(' Session cache (opencode) error:', err.message); }
|
|
33507
34410
|
},
|
|
@@ -33586,8 +34489,12 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
33586
34489
|
}
|
|
33587
34490
|
} catch { /* ok */ }
|
|
33588
34491
|
|
|
33589
|
-
// Check for SynaBun updates (npm + github releases)
|
|
33590
|
-
|
|
34492
|
+
// Check for SynaBun updates (npm + github releases), unless Settings > App turned the automatic check off
|
|
34493
|
+
if (automaticUpdateCheck()) {
|
|
34494
|
+
try { await checkSynabunUpdate(); } catch {}
|
|
34495
|
+
} else {
|
|
34496
|
+
console.log(' Updates: automatic check off (Settings > App)');
|
|
34497
|
+
}
|
|
33591
34498
|
|
|
33592
34499
|
// Prune any external CLI tool left in our node_modules before the version
|
|
33593
34500
|
// check runs — defends against `npm install` not auto-pruning packages that
|