synabun 2.0.5 → 2.0.7
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 +40 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/shared.mjs +6 -3
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +627 -57
- package/neural-interface/i18n/pt-BR.json +626 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +255 -52
- package/neural-interface/lib/assistant-brains/claude.js +15 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-config.js +62 -14
- package/neural-interface/lib/assistant-dispatch.js +432 -125
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-persona.js +68 -11
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +307 -47
- package/neural-interface/lib/assistant-user-writer.js +4 -2
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +90 -3
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +3 -3
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-read.js +34 -2
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +16 -3
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/offline.html +16 -1
- package/neural-interface/public/onboarding.html +96 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
- package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
- package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
- package/neural-interface/public/shared/assistant/asst-render.js +38 -7
- package/neural-interface/public/shared/assistant/asst-route.js +72 -14
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +1 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +46 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-panel.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +1 -1
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/html-shell.js +1 -0
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
- package/neural-interface/public/shared/pwa-install-page.js +3 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-ia.js +15 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +278 -31
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +212 -213
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-loading.js +30 -6
- package/neural-interface/public/shared/ui-menubar.js +13 -3
- package/neural-interface/public/shared/ui-navbar.js +3 -1
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +55 -3
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +6 -0
- package/neural-interface/public/shared/ui-terminal.js +17 -0
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +2 -0
- package/neural-interface/public/sw.js +1 -1
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/server.js +1219 -608
- package/package.json +4 -1
- package/setup.js +9 -8
|
@@ -9,6 +9,12 @@ import { createSessionListOverlay, overlaySessions, matchesSessionFilter } from
|
|
|
9
9
|
import { findSessionTranscript, sessionProjects, confineArtifact, confineSubPath, confineToRoot, staysInside, isInsideReal, isInside, isPlainName, PathConfineError } from './lib/path-confine.js';
|
|
10
10
|
import { readConfinedFile, writeConfinedFile, makeConfinedDir, removeConfinedEntry, listConfined, openConfinedStream } from './lib/confined-fs.js';
|
|
11
11
|
import { createClaudeAccounts as createPanelClaudeAccounts } from './lib/claude-accounts.js';
|
|
12
|
+
import { createClaudePanelAuth } from './lib/claude-panel-auth.js';
|
|
13
|
+
import { createCodexPanelAuth } from './lib/codex-panel-auth.js';
|
|
14
|
+
import { createCodexAccounts, applyCodexLaunchEnv, codexAppServerArgs, codexExecEnv } from './lib/codex-accounts.js';
|
|
15
|
+
import { createCodexAccountAuth } from './lib/codex-account-auth.js';
|
|
16
|
+
import { registerProviderPanelApiKeyAuthRoutes, redactProviderSecrets, providerApiKeyBodyError, providerDiagnosticSink } from './lib/provider-panel-api-key-auth.js';
|
|
17
|
+
import { createProviderPanelTerminalAuth, providerLoginSpec, registerProviderPanelTerminalAuthRoutes } from './lib/provider-panel-terminal-auth.js';
|
|
12
18
|
import { claudeLegacyPricing, claudeUsageCostUsd } from './lib/assistant-pricing.js';
|
|
13
19
|
import { HOOK_SCRIPTS, hookDefinitionsForEvent, hookCommandString, hookInstallBlocker, hookInstallPlan, isSynaBunHookCommand, sweepSettingsHooks, ensureHookRoot } from '../lib/claude-hooks.js';
|
|
14
20
|
import { browserV2Enabled, browserRequestMiddleware, assertBrowserRequestActive, browserActionTimeout, markBrowserActionStarted, runBrowserRequest, cancellableBrowserDelay, measureBrowserPhase, createEngagementScheduler, engagementTargetSelector, isEngagementWriteSelector, isXShortcutPublish, isXScriptedWrite } from './lib/browser-execution.js';
|
|
@@ -34,8 +40,9 @@ import { createPtyHostManager } from './lib/pty-host/manager.js';
|
|
|
34
40
|
import { hostNameOf, isAllowedHost, isAllowedWebSocketOrigin, parseCookies, refuseUpgrade } from './lib/http-guards.js';
|
|
35
41
|
import { createRequestGates, extraHostNames, hostsOf, savedValueOf, startedValueOf } from './lib/request-gate.js';
|
|
36
42
|
import { createGitDiffRouter } from './lib/git-diff-api.js';
|
|
37
|
-
import { gitRead, gitReadSync } from './lib/git-read.js';
|
|
43
|
+
import { gitRead, gitReadSync, setGitFallbackFolder, withGitFallback } from './lib/git-read.js';
|
|
38
44
|
import { createPhoneAuthority } from './lib/remote-policy.js';
|
|
45
|
+
import { brainCredentialOf } from './lib/virtual-socket.js';
|
|
39
46
|
import { buildTerminalEnv, interactiveShellArgs, cliWrapperArgs } from './lib/terminal-env.js';
|
|
40
47
|
import { startJudgeBackfill, stopJudgeBackfill } from '../mcp-server/dist/services/memory-judge-backfill.js';
|
|
41
48
|
import { judgeLoopGoal } from '../mcp-server/dist/services/memory-judgments.js';
|
|
@@ -106,12 +113,18 @@ import { loopLog, loopLogPath, flushLoopLogs } from './lib/loop-logger.js';
|
|
|
106
113
|
import {
|
|
107
114
|
buildInstallCommand,
|
|
108
115
|
buildUpdateCommand,
|
|
109
|
-
detectInstallSource,
|
|
110
116
|
isBrewSource,
|
|
111
117
|
parseBrewOutdated,
|
|
118
|
+
quoteCommandPath,
|
|
112
119
|
resolveBrewToken,
|
|
113
120
|
resolveLatest,
|
|
114
121
|
} from './lib/cli-update-planner.js';
|
|
122
|
+
import { createCliResolver, spawnArgsFor } from './lib/cli-resolver.js';
|
|
123
|
+
import { osPath } from './lib/os-path.js';
|
|
124
|
+
import { createCliInstaller, CLI_INSTALL_GUIDES } from './lib/cli-install.js';
|
|
125
|
+
import { createCliInstallApi, createCliPathApi, createGitBashApi } from './lib/cli-install-api.js';
|
|
126
|
+
import { createCliPathManager } from './lib/cli-path.js';
|
|
127
|
+
import { composeTerminalPath, terminalLaunchCommand, terminalArgument } from './lib/terminal-path.js';
|
|
115
128
|
import {
|
|
116
129
|
applyBackupRetention,
|
|
117
130
|
createVerifiedBackup,
|
|
@@ -145,11 +158,6 @@ import {
|
|
|
145
158
|
createCodexNativeLoopAdapter,
|
|
146
159
|
createOpenCodeNativeLoopAdapter,
|
|
147
160
|
} from './lib/native-loop-providers.js';
|
|
148
|
-
import {
|
|
149
|
-
resolveTrustedWindowsCodexBinary,
|
|
150
|
-
selectWindowsCodexLauncher,
|
|
151
|
-
windowsCodexPackageJsonCandidates,
|
|
152
|
-
} from './lib/codex-runtime-path.js';
|
|
153
161
|
import {
|
|
154
162
|
mergeCodexModelContextCapabilities,
|
|
155
163
|
resolveCodexExtendedContext,
|
|
@@ -233,10 +241,14 @@ import {
|
|
|
233
241
|
onClaudeModelsChanged,
|
|
234
242
|
shellCommand,
|
|
235
243
|
} from './lib/claude-model-catalog.js';
|
|
236
|
-
import {
|
|
237
|
-
import {
|
|
244
|
+
import { resolveClaudeSdkExecutable } from './lib/claude-executable.js';
|
|
245
|
+
import { collectInstallDetails } from './lib/install-details.js';
|
|
246
|
+
import { withPathEnv, pathEnvValue, getAugmentedPath, rememberedOsPath, rememberPackagedPath, rememberedPackagedPath, withPackagedPathLast } from './lib/augmented-path.js';
|
|
247
|
+
import { readCodexSettingsSchema, readCodexFeatureSupport, validateCodexSettings } from './lib/codex-settings-schema.js';
|
|
238
248
|
import { semverNewer, synabunReleaseChannel } from './lib/synabun-version.js';
|
|
239
249
|
import { selectSynabunUpdate, buildSynabunInstallPlan, refuseStaleUpdateClick } from './lib/synabun-update-plan.js';
|
|
250
|
+
import { collectActivity, countWhere, recentCliSessions } from './lib/system-activity.js';
|
|
251
|
+
import { createSystemAbout } from './lib/system-about.js';
|
|
240
252
|
import {
|
|
241
253
|
listCodexSessionsFromAccounts,
|
|
242
254
|
mergeCodexSessionSummaries,
|
|
@@ -264,7 +276,10 @@ import { ensureProjectCategories } from '../hooks/claude-code/shared.mjs';
|
|
|
264
276
|
import { getDataHome, getDataHomeDiagnostics, isGlobalInstall, pathIsInside } from '../lib/paths.js';
|
|
265
277
|
import { launcherLogPath } from '../lib/first-launch-protection.js';
|
|
266
278
|
import { registerStartLauncher, startLauncherState } from '../lib/start-launcher.js';
|
|
267
|
-
import {
|
|
279
|
+
import { effectiveNpmPrefix } from '../lib/child-path.js';
|
|
280
|
+
import { redactDeep, redactSecrets } from '../lib/log-redact.js';
|
|
281
|
+
import { applyPackagedChildPath, durableEntry, mcpStdioCommand, npmGlobalBinFor, packagedNpmPrefix, packagedPathFolders, packagedRuntime } from '../lib/packaged-runtime.js';
|
|
282
|
+
import { dataHomeId } from '../lib/desktop-app.js';
|
|
268
283
|
import { pruneExternalTools } from '../lib/external-tools.js';
|
|
269
284
|
import { renderOfflinePage } from './lib/offline-page.js';
|
|
270
285
|
import {
|
|
@@ -346,6 +361,11 @@ let _greetingError = null;
|
|
|
346
361
|
let _assistantDispatcher = null;
|
|
347
362
|
// The Assistant's model catalog (lib/assistant-catalog.js): Settings → WhatsApp checks its brain choice against it.
|
|
348
363
|
let _assistantCatalog = null;
|
|
364
|
+
// The Claude account the Assistant's Claude Code workers use when a dispatch names none (assistant-config.json
|
|
365
|
+
// `workers.claudeAccount`); a function once the Assistant is wired. An account it names cannot be removed.
|
|
366
|
+
let _assistantClaudeWorkerAccount = null;
|
|
367
|
+
// The same for Codex workers (`workers.codexAccount`).
|
|
368
|
+
let _assistantCodexWorkerAccount = null;
|
|
349
369
|
// Who may ask, in front of every route (lib/request-gate.js). A request for the owner's data must name this
|
|
350
370
|
// server: everything under /api and /mcp, the invite sign-in, the rendered offline page and the uploaded icons
|
|
351
371
|
// answer 403 BAD_HOST to any other Host (a page whose name was re-pointed at this machine sends its own); the
|
|
@@ -371,6 +391,7 @@ app.use(express.json({ limit: '25mb' }));
|
|
|
371
391
|
// exceed the JSON limit. Default Express handler dumps a 10-line raw-body stack with no
|
|
372
392
|
// URL — this logs one line with context and responds 413 instead.
|
|
373
393
|
app.use((err, req, res, next) => {
|
|
394
|
+
if (/^\/api\/provider-panel-auth\/[^/]+\/[^/]+\/api-key(?:\?|$)/.test(req.originalUrl || req.url || '')) return providerApiKeyBodyError(err, req, res, next);
|
|
374
395
|
if (err && (err.type === 'entity.too.large' || err.type === 'entity.parse.failed')) {
|
|
375
396
|
const len = req.headers['content-length'] || '?';
|
|
376
397
|
console.warn(`[body-parser] ${err.type} ${req.method} ${req.originalUrl || req.url} content-length=${len} limit=${err.limit || '?'}`);
|
|
@@ -435,6 +456,8 @@ import { createEndpointCompletionEngine } from './lib/editor-completion-endpoint
|
|
|
435
456
|
import { ownerRouteGuard } from './lib/http-guards.js';
|
|
436
457
|
import { createCloudflaredManager } from './lib/cloudflared/manager.js';
|
|
437
458
|
import { createQuickTunnel } from './lib/cloudflared/tunnel.js';
|
|
459
|
+
import { createGitBashManager } from './lib/git-bash/manager.js';
|
|
460
|
+
import { settingsGitBashValue, syncClaudeSettingsGitBash } from './lib/git-bash/claude-settings.js';
|
|
438
461
|
|
|
439
462
|
const PORT = process.env.NEURAL_PORT || 3344;
|
|
440
463
|
// SQLite database is used directly via lib/db.js
|
|
@@ -754,6 +777,26 @@ const INSTALL_KIND = PACKAGED_RUNTIME ? 'app' : isGlobalInstall() ? 'npm' : 'git
|
|
|
754
777
|
// What a person types to start a packaged application: its entry executable,
|
|
755
778
|
// when that is a path that will still be there once this run is over.
|
|
756
779
|
const START_ENTRY = durableEntry(PACKAGED_RUNTIME) || '';
|
|
780
|
+
// What the desktop app's window asks /api/health (lib/desktop-app.js): the version this server runs and which
|
|
781
|
+
// data home it uses, as a hash and never the path, so it can tell its own server from another installation's.
|
|
782
|
+
const SERVER_VERSION = (() => { try { return String(JSON.parse(readFileSync(resolve(PACKAGE_ROOT, 'package.json'), 'utf-8')).version || ''); } catch { return ''; } })();
|
|
783
|
+
const DATA_HOME_ID = dataHomeId(DATA_HOME);
|
|
784
|
+
// The entry this install runs from, even when it does not outlive the run (START_ENTRY is then empty): how the
|
|
785
|
+
// desktop app tells its own server from another install's (lib/desktop-app.js classifyServer).
|
|
786
|
+
const INSTALL_ENTRY = PACKAGED_RUNTIME?.entry || '';
|
|
787
|
+
// A packaged application's own Node runtime, and the global bin of the npm that came with it (where `npm i -g`
|
|
788
|
+
// through it puts commands: the prefix as npm settles it from npm_config_prefix, the user's config file and the
|
|
789
|
+
// global one, else SynaBun's per-user folder its builtin npmrc names, never the application: lib/child-path.js,
|
|
790
|
+
// lib/packaged-runtime.js packagedNpmPrefix). On the PATH of everything this server starts, after everything of
|
|
791
|
+
// the user's: the user's own Node and npm stay first, these are the fallback on a machine without. The bin is asked
|
|
792
|
+
// for each PATH (cached for seconds), so a prefix set later counts without a restart. This process's own environment
|
|
793
|
+
// gets the same order, for every child that simply inherits it (an npm install, git, a CLI); the server's own
|
|
794
|
+
// Node children are started by process.execPath, never through PATH.
|
|
795
|
+
rememberPackagedPath({
|
|
796
|
+
last: packagedPathFolders(PACKAGED_RUNTIME).last,
|
|
797
|
+
npmBin: PACKAGED_RUNTIME ? () => packagedNpmPrefix(PACKAGED_RUNTIME)?.bin || null : null,
|
|
798
|
+
});
|
|
799
|
+
applyPackagedChildPath(process.env, PACKAGED_RUNTIME);
|
|
757
800
|
|
|
758
801
|
// Serve offline.html with what it needs once we are down: the install path,
|
|
759
802
|
// how it was installed, the launcher's state and the bridge code itself.
|
|
@@ -2142,6 +2185,10 @@ async function runGracefulShutdown(reason) {
|
|
|
2142
2185
|
// The public address: its cloudflared is ended with the server that owns the address (killed when it does not
|
|
2143
2186
|
// end by itself; never waited for more than a moment).
|
|
2144
2187
|
try { await tunnel.shutdown({ timeoutMs: 1500 }); } catch (err) { console.warn(' tunnel stop err:', err?.message); }
|
|
2188
|
+
// A CLI install under way (lib/cli-install.js): its installer and what it started are stopped.
|
|
2189
|
+
try { cliInstaller.stopAll(); } catch (err) { console.warn(' cli install stop err:', err?.message); }
|
|
2190
|
+
// A Git for Windows download under way (lib/git-bash/): stopped; its work folder goes with the next attempt.
|
|
2191
|
+
try { gitBash.cancel(); } catch (err) { console.warn(' git bash stop err:', err?.message); }
|
|
2145
2192
|
|
|
2146
2193
|
// 1. opencode sidecar — has its own teardown that handles managed PID + ollama.
|
|
2147
2194
|
try { stopOpencodeServer(); } catch (err) { console.warn(' opencode stop err:', err?.message); }
|
|
@@ -2462,6 +2509,13 @@ app.post('/api/system/run-update', async (req, res) => {
|
|
|
2462
2509
|
// Nothing is ever installed into the resources of a packaged application.
|
|
2463
2510
|
if (PACKAGED_RUNTIME) {
|
|
2464
2511
|
const openUrl = `${SYNABUN_GITHUB_REPO_URL}/releases`;
|
|
2512
|
+
// The desktop app updates itself, through its window's update function (packaging/desktop).
|
|
2513
|
+
if (PACKAGED_RUNTIME.shell) {
|
|
2514
|
+
return res.status(409).json({
|
|
2515
|
+
error: 'This copy of SynaBun is the desktop app, and it updates itself: use Check for updates in its menu. Nothing was installed.',
|
|
2516
|
+
packaged: true, desktopManaged: true, openUrl,
|
|
2517
|
+
});
|
|
2518
|
+
}
|
|
2465
2519
|
return res.status(409).json({
|
|
2466
2520
|
error: `This copy of SynaBun is a packaged application. It is updated by installing a newer build over it, not from inside the app. Nothing was installed. Releases: ${openUrl}`,
|
|
2467
2521
|
packaged: true,
|
|
@@ -3247,36 +3301,88 @@ function _heartbeatLock(sessionId, windowId) {
|
|
|
3247
3301
|
}
|
|
3248
3302
|
}
|
|
3249
3303
|
|
|
3250
|
-
//
|
|
3251
|
-
|
|
3252
|
-
//
|
|
3253
|
-
//
|
|
3254
|
-
|
|
3255
|
-
|
|
3304
|
+
// ── Where the user's CLIs are (lib/cli-resolver.js) ──
|
|
3305
|
+
// One resolver for every lookup and launch of Claude Code, Codex and OpenCode:
|
|
3306
|
+
// the version check behind the side panels' banner and the update badges,
|
|
3307
|
+
// Settings' Detect, the one-click install (lib/cli-install.js) and the
|
|
3308
|
+
// launchers below. The path in Settings > Terminal first, then PATH with the OS
|
|
3309
|
+
// PATH read afresh (lib/os-path.js: the registry on Windows, the login shell on
|
|
3310
|
+
// macOS and Linux), then the folders each install method uses; never a copy
|
|
3311
|
+
// inside SynaBun. A CLI installed while SynaBun runs is found by the next
|
|
3312
|
+
// lookup: nothing here is remembered for more than a few seconds, and Re-check
|
|
3313
|
+
// forgets everything.
|
|
3314
|
+
const cliResolver = createCliResolver({
|
|
3315
|
+
isInsideSynabun: (file) => isInsideSynabun(file),
|
|
3316
|
+
configured: (toolKey) => loadCliConfig()[toolKey]?.command ?? null,
|
|
3317
|
+
});
|
|
3318
|
+
// The first read of the OS PATH starts now, so the first lookups already have it.
|
|
3319
|
+
osPath.refresh().catch(() => {});
|
|
3320
|
+
|
|
3321
|
+
// ── Git Bash for Claude Code on Windows (lib/git-bash/) ──
|
|
3322
|
+
// Claude Code on Windows runs its Bash tool and every hook through Git Bash and
|
|
3323
|
+
// falls back to PowerShell without one, where SynaBun's own hooks of an
|
|
3324
|
+
// installed app do not even parse. Which Git Bash a Claude Code started by
|
|
3325
|
+
// SynaBun gets: CLAUDE_CODE_GIT_BASH_PATH (this environment, or the user's
|
|
3326
|
+
// settings), a Git for Windows of the PC's own, then SynaBun's copy
|
|
3327
|
+
// (<resources>\git in the installer; DATA_HOME/runtime/git/<version>, fetched
|
|
3328
|
+
// once, on an npm install). claudeChildEnv() is the last word on the
|
|
3329
|
+
// environment of every Claude Code process SynaBun starts. Nothing changes on
|
|
3330
|
+
// macOS and Linux, or where the PC has a Git Bash Claude Code finds by itself;
|
|
3331
|
+
// the system's PATH and registry are never changed.
|
|
3332
|
+
const gitBash = createGitBashManager({
|
|
3333
|
+
dataHome: DATA_HOME,
|
|
3334
|
+
pathValue: () => [pathEnvValue(process.env), ...rememberedOsPath()].join(';'),
|
|
3335
|
+
settingsValue: () => settingsGitBashValue({ settingsPath: getGlobalClaudeSettingsPath(), read: readClaudeSettingsForWrite }),
|
|
3336
|
+
log: (line) => console.log(line),
|
|
3337
|
+
});
|
|
3338
|
+
function claudeChildEnv(env = process.env) {
|
|
3339
|
+
// A packaged application's runtime and npm after the user's own on PATH (rememberPackagedPath above).
|
|
3340
|
+
const ordered = withPackagedPathLast(env);
|
|
3341
|
+
try { return gitBash.childEnv(ordered); } catch { return ordered; }
|
|
3342
|
+
}
|
|
3343
|
+
// SynaBun's own git features (the terminal's branch and checkout, the File Explorer's and the code editor's git,
|
|
3344
|
+
// the git routes) run the copy's git.exe on a PC without git: its path for spawnGit, its folder for every read.
|
|
3345
|
+
setGitFallbackFolder(() => { const git = gitBash.gitExecutable(); return git ? dirname(git) : null; });
|
|
3346
|
+
// The user's own `claude`, started in another terminal: ~/.claude/settings.json names SynaBun's copy while it is
|
|
3347
|
+
// the one in use and loses that value when it is not; a value of the user's own is never touched. Windows only.
|
|
3348
|
+
function syncGitBashSettings(resolution = gitBash.resolution()) {
|
|
3349
|
+
const settingsPath = getGlobalClaudeSettingsPath();
|
|
3350
|
+
const result = syncClaudeSettingsGitBash({ resolution, settingsPath, read: readClaudeSettingsForWrite, write: writeClaudeSettings, ownRoots: gitBash.ownRoots });
|
|
3351
|
+
gitBash.noteSettings(result);
|
|
3352
|
+
if (result.state === 'written') console.log(`[git-bash] ${settingsPath}: CLAUDE_CODE_GIT_BASH_PATH set to ${result.value} (SynaBun's Git Bash, for a claude started outside SynaBun)`);
|
|
3353
|
+
else if (result.state === 'removed') console.log(`[git-bash] ${settingsPath}: CLAUDE_CODE_GIT_BASH_PATH removed (SynaBun's Git Bash is not the one in use)`);
|
|
3354
|
+
else if (result.state === 'error') console.warn(`[git-bash] ${settingsPath} was not changed: ${result.message}`);
|
|
3355
|
+
return result;
|
|
3356
|
+
}
|
|
3357
|
+
gitBash.onChange((resolution) => { syncGitBashSettings(resolution); });
|
|
3358
|
+
const _cliMissingLogged = new Set();
|
|
3359
|
+
function _noteCliLookup(toolKey, found, label) {
|
|
3360
|
+
if (!found?.path) {
|
|
3361
|
+
if (!_cliMissingLogged.has(toolKey)) {
|
|
3362
|
+
_cliMissingLogged.add(toolKey);
|
|
3363
|
+
console.warn(`[cli] ${label} not found (Settings path, PATH, the OS PATH and the known install folders). The side panel offers to install it.`);
|
|
3364
|
+
}
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
if (_cliMissingLogged.delete(toolKey)) console.log(`[cli] ${label} found: ${found.path}`);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
/** The Settings > Terminal value of a tool when it is a command line ("wsl codex") that a shell runs as written. */
|
|
3371
|
+
function configuredCommandLine(toolKey) {
|
|
3372
|
+
let value = '';
|
|
3373
|
+
try { value = String(loadCliConfig()[toolKey]?.command || '').trim(); } catch {}
|
|
3374
|
+
if (!value || !/\s/.test(value) || existsSync(value.replace(/^"(.*)"$/, '$1'))) return null;
|
|
3375
|
+
return value;
|
|
3376
|
+
}
|
|
3256
3377
|
|
|
3257
|
-
// The Claude Code a terminal would start: the
|
|
3258
|
-
//
|
|
3259
|
-
//
|
|
3260
|
-
//
|
|
3378
|
+
// The Claude Code a terminal would start: the resolver's answer (Settings'
|
|
3379
|
+
// path, PATH, the OS PATH, the known install folders), always the user's own
|
|
3380
|
+
// installation. The bare 'claude' when there is none, which
|
|
3381
|
+
// resolveClaudeSdkExecutable and every caller read as "not installed".
|
|
3261
3382
|
function getClaudeBin() {
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
try { configured = loadCliConfig()['claude-code']?.command?.trim() || ''; } catch {}
|
|
3266
|
-
const found = resolveClaudeLauncher({ configured, isInsideSynabun });
|
|
3267
|
-
for (const rejected of found.rejected) {
|
|
3268
|
-
console.warn(`[claude-skin] Ignoring Claude CLI inside the SynaBun install: ${rejected}`);
|
|
3269
|
-
}
|
|
3270
|
-
if (!found.path) {
|
|
3271
|
-
if (!_claudeBinMissingAt) console.warn('[claude-skin] Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code');
|
|
3272
|
-
_claudeBinMissingAt = Date.now();
|
|
3273
|
-
_claudeBinPath = 'claude'; // last resort — will fail at spawn time with a clear error
|
|
3274
|
-
return _claudeBinPath;
|
|
3275
|
-
}
|
|
3276
|
-
if (_claudeBinMissingAt) console.log(`[claude-skin] Claude CLI found: ${found.path}`);
|
|
3277
|
-
_claudeBinMissingAt = 0;
|
|
3278
|
-
_claudeBinPath = found.path;
|
|
3279
|
-
return _claudeBinPath;
|
|
3383
|
+
const found = cliResolver.launcher('claude-code');
|
|
3384
|
+
_noteCliLookup('claude-code', found, 'Claude Code');
|
|
3385
|
+
return found.path || 'claude';
|
|
3280
3386
|
}
|
|
3281
3387
|
|
|
3282
3388
|
// The executable every Agent SDK call is given (pathToClaudeCodeExecutable):
|
|
@@ -3291,131 +3397,63 @@ function getClaudeSdkExecutable() {
|
|
|
3291
3397
|
});
|
|
3292
3398
|
}
|
|
3293
3399
|
|
|
3294
|
-
|
|
3400
|
+
// Codex: what the SDK and the app-server start. On Windows the native codex.exe
|
|
3401
|
+
// an npm command file leads to (the SDK spawns without a shell); the file found
|
|
3402
|
+
// otherwise. SynaBun carries no Codex (lib/external-tools.js): without one,
|
|
3403
|
+
// Codex is reported as not installed. 'codex' when there is none.
|
|
3295
3404
|
let _codexBinSource = 'missing';
|
|
3296
|
-
let _codexBinMissingAt = 0;
|
|
3297
|
-
let _nativeCodexBinPath = null;
|
|
3298
3405
|
// The SDK is statically imported at module load; reaching this point is the
|
|
3299
3406
|
// authoritative capability check. Its package.json is intentionally not an
|
|
3300
3407
|
// exported subpath, so resolving that file reports a false negative.
|
|
3301
3408
|
const _codexSdkInstalled = true;
|
|
3302
3409
|
|
|
3303
|
-
function resolveWindowsGlobalCodexRuntime() {
|
|
3304
|
-
const globalPath = getAugmentedPath().split(delimiter)
|
|
3305
|
-
.filter((entry) => !/[\\/]node_modules[\\/]\.bin[\\/]?$/i.test(entry))
|
|
3306
|
-
.join(delimiter);
|
|
3307
|
-
const env = { ...process.env, PATH: globalPath };
|
|
3308
|
-
const whereExecutable = process.env.SystemRoot
|
|
3309
|
-
? join(process.env.SystemRoot, 'System32', 'where.exe')
|
|
3310
|
-
: 'where.exe';
|
|
3311
|
-
const lookup = spawnSync(whereExecutable, ['codex'], {
|
|
3312
|
-
encoding: 'utf-8',
|
|
3313
|
-
env,
|
|
3314
|
-
windowsHide: true,
|
|
3315
|
-
});
|
|
3316
|
-
const launchers = lookup.status === 0
|
|
3317
|
-
? String(lookup.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
3318
|
-
: [];
|
|
3319
|
-
|
|
3320
|
-
const npmRootProbe = spawnSync(
|
|
3321
|
-
process.env.ComSpec || process.env.COMSPEC || 'cmd.exe',
|
|
3322
|
-
['/d', '/s', '/c', 'npm root -g'],
|
|
3323
|
-
{ encoding: 'utf-8', env, windowsHide: true, timeout: 5000 },
|
|
3324
|
-
);
|
|
3325
|
-
const npmRoots = npmRootProbe.status === 0
|
|
3326
|
-
? String(npmRootProbe.stdout || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
|
3327
|
-
: [];
|
|
3328
|
-
const codexPackageJsonPaths = windowsCodexPackageJsonCandidates({
|
|
3329
|
-
npmRoots,
|
|
3330
|
-
appData: process.env.APPDATA,
|
|
3331
|
-
npmConfigPrefix: process.env.NPM_CONFIG_PREFIX || process.env.npm_config_prefix,
|
|
3332
|
-
synabunPackageRoot: PACKAGE_ROOT,
|
|
3333
|
-
nodeExecutable: process.execPath,
|
|
3334
|
-
});
|
|
3335
|
-
const resolved = resolveTrustedWindowsCodexBinary({
|
|
3336
|
-
launchers,
|
|
3337
|
-
codexPackageJsonPaths,
|
|
3338
|
-
arch: process.arch,
|
|
3339
|
-
acceptBinary: (candidate) => !isInsideSynabun(candidate),
|
|
3340
|
-
});
|
|
3341
|
-
return { ...resolved, launchers, codexPackageJsonPaths, npmRoots };
|
|
3342
|
-
}
|
|
3343
|
-
|
|
3344
3410
|
function getCodexBin() {
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
// handed this path, and without one Codex is reported as not installed.
|
|
3350
|
-
const globalPath = getAugmentedPath().split(delimiter)
|
|
3351
|
-
.filter((entry) => !/[\\/]node_modules[\\/]\.bin[\\/]?$/i.test(entry))
|
|
3352
|
-
.join(delimiter);
|
|
3353
|
-
const envWithPath = { ...process.env, PATH: globalPath };
|
|
3354
|
-
if (process.platform === 'win32') {
|
|
3355
|
-
const resolution = resolveWindowsGlobalCodexRuntime();
|
|
3356
|
-
if (resolution.path) {
|
|
3357
|
-
_codexBinPath = resolution.path;
|
|
3358
|
-
_nativeCodexBinPath = resolution.path;
|
|
3359
|
-
} else {
|
|
3360
|
-
_codexBinPath = selectWindowsCodexLauncher(
|
|
3361
|
-
resolution.launchers,
|
|
3362
|
-
(launcher) => !isInsideSynabun(launcher),
|
|
3363
|
-
);
|
|
3364
|
-
}
|
|
3365
|
-
} else {
|
|
3366
|
-
try { _codexBinPath = execSync('which codex', { encoding: 'utf-8', env: envWithPath }).trim(); }
|
|
3367
|
-
catch { _codexBinPath = null; }
|
|
3368
|
-
}
|
|
3369
|
-
if (_codexBinPath && _codexBinPath !== 'codex' && !isInsideSynabun(_codexBinPath)) {
|
|
3370
|
-
if (_codexBinMissingAt) console.log(`[codex-skin] Codex CLI found: ${_codexBinPath}`);
|
|
3371
|
-
_codexBinMissingAt = 0;
|
|
3372
|
-
_codexBinSource = 'global';
|
|
3373
|
-
return _codexBinPath;
|
|
3374
|
-
}
|
|
3375
|
-
if (!_codexBinMissingAt) {
|
|
3376
|
-
if (_codexBinPath && _codexBinPath !== 'codex') console.warn(`[codex-skin] Ignoring Codex CLI inside the SynaBun install: ${_codexBinPath}`);
|
|
3377
|
-
console.warn('[codex-skin] Codex CLI not found. Install: npm install -g @openai/codex');
|
|
3378
|
-
}
|
|
3379
|
-
_codexBinMissingAt = Date.now();
|
|
3380
|
-
_codexBinPath = 'codex';
|
|
3381
|
-
_codexBinSource = 'missing';
|
|
3382
|
-
return _codexBinPath;
|
|
3411
|
+
const found = cliResolver.launcher('codex');
|
|
3412
|
+
_noteCliLookup('codex', found, 'Codex');
|
|
3413
|
+
_codexBinSource = found.path ? 'global' : 'missing';
|
|
3414
|
+
return found.launchPath || found.path || 'codex';
|
|
3383
3415
|
}
|
|
3384
3416
|
|
|
3417
|
+
// Native Codex schedules hand the SDK a program it can start without a shell:
|
|
3418
|
+
// on Windows a codex.exe, never a command file.
|
|
3385
3419
|
function getNativeCodexBin() {
|
|
3386
|
-
|
|
3387
|
-
|
|
3420
|
+
const found = cliResolver.launcher('codex');
|
|
3421
|
+
_codexBinSource = found.path ? 'global' : 'missing';
|
|
3388
3422
|
if (process.platform === 'win32') {
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
_codexBinSource = 'global';
|
|
3394
|
-
console.log(`[codex-native] Resolved Windows runtime (${resolved.source}): ${resolved.path}`);
|
|
3395
|
-
return _nativeCodexBinPath;
|
|
3396
|
-
}
|
|
3397
|
-
if (resolved.reason) {
|
|
3398
|
-
console.warn(`[codex-native] ${resolved.reason}`);
|
|
3399
|
-
}
|
|
3400
|
-
if (resolved.checked?.length) {
|
|
3401
|
-
console.warn(`[codex-native] Checked: ${resolved.checked.slice(0, 16).join(' | ')}`);
|
|
3402
|
-
}
|
|
3403
|
-
const globalPackageFound = resolved.codexPackageJsonPaths.some((candidate) => existsSync(candidate));
|
|
3404
|
-
throw new Error(
|
|
3405
|
-
globalPackageFound
|
|
3406
|
-
? 'The global Codex package was found, but its Windows runtime is missing. Reinstall it with: npm install -g @openai/codex@latest --include=optional, then restart SynaBun.'
|
|
3407
|
-
: 'Codex is not installed globally or could not be located. Install it with: npm install -g @openai/codex@latest --include=optional, then restart SynaBun.',
|
|
3408
|
-
);
|
|
3423
|
+
if (found.launchPath && /\.exe$/i.test(found.launchPath)) return found.launchPath;
|
|
3424
|
+
throw new Error(found.path
|
|
3425
|
+
? `Codex was found at ${found.path}, but not the codex.exe it runs. Reinstall Codex (the Codex panel's Install button, or npm install -g @openai/codex@latest --include=optional), then try again.`
|
|
3426
|
+
: 'Codex is not installed. Install it with the Codex panel\'s Install button, or run: powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"');
|
|
3409
3427
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3428
|
+
if (!found.path) {
|
|
3429
|
+
throw new Error('Codex schedules need Codex installed. Install it with the Codex panel\'s Install button, or run: curl -fsSL https://chatgpt.com/codex/install.sh | sh');
|
|
3430
|
+
}
|
|
3431
|
+
return found.launchPath || found.path;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
// How `opencode serve --port N` starts, for the shared serve, the isolated
|
|
3435
|
+
// serves of sessions and loops and the editor's completion serve. On macOS and
|
|
3436
|
+
// Linux through the user's login shell, as a terminal starts it (OpenCode gets
|
|
3437
|
+
// the provider keys exported in .zshrc), naming the CLI the resolver found by
|
|
3438
|
+
// its full path; on Windows the opencode.exe found, started directly with the
|
|
3439
|
+
// PATH a new terminal has (a command file that leads nowhere: through cmd.exe,
|
|
3440
|
+
// every word escaped). A command line in Settings is run as written.
|
|
3441
|
+
function opencodeServeLaunch(port, { env = process.env } = {}) {
|
|
3442
|
+
const found = cliResolver.launcher('opencode');
|
|
3443
|
+
_noteCliLookup('opencode', found, 'OpenCode');
|
|
3444
|
+
const line = configuredCommandLine('opencode');
|
|
3445
|
+
if (IS_WIN) {
|
|
3446
|
+
const spawnEnv = withPathEnv(env, getAugmentedPath());
|
|
3447
|
+
if (!line && found.spec) {
|
|
3448
|
+
const launch = spawnArgsFor(found.spec, ['serve', '--port', String(port)], { env: spawnEnv });
|
|
3449
|
+
return { command: launch.command, args: launch.args, options: { ...launch.options, env: spawnEnv, windowsHide: true }, label: `${found.launchPath} serve --port ${port}` };
|
|
3450
|
+
}
|
|
3451
|
+
const text = `${line || 'opencode'} serve --port ${port}`;
|
|
3452
|
+
return { command: DEFAULT_SHELL, args: ['/d', '/s', '/c', text], options: { env: spawnEnv, windowsHide: true }, label: text };
|
|
3416
3453
|
}
|
|
3417
|
-
|
|
3418
|
-
|
|
3454
|
+
const cmd = line || (found.path ? shellSingleQuote(found.path) : 'opencode');
|
|
3455
|
+
// 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} serve --port ${port}`], options: { env: withPackagedPathLast(env) }, label: `${cmd} serve --port ${port}` };
|
|
3419
3457
|
}
|
|
3420
3458
|
|
|
3421
3459
|
function extractCodexConfigValues(result) {
|
|
@@ -3455,10 +3493,10 @@ function buildCodexWritableRoots(cwd = PACKAGE_ROOT) {
|
|
|
3455
3493
|
return [...roots];
|
|
3456
3494
|
}
|
|
3457
3495
|
|
|
3458
|
-
function getCodexSidepanelApprovalPolicy() {
|
|
3496
|
+
function getCodexSidepanelApprovalPolicy(config = {}) {
|
|
3459
3497
|
// The sidepanel is interactive; Auto Accept decides which emitted requests
|
|
3460
3498
|
// can be approved without presenting a card.
|
|
3461
|
-
return CODEX_SIDEPANEL_APPROVAL_POLICY;
|
|
3499
|
+
return config.approval_policy ?? CODEX_SIDEPANEL_APPROVAL_POLICY;
|
|
3462
3500
|
}
|
|
3463
3501
|
|
|
3464
3502
|
function buildCodexSandboxPolicy(cwd = PACKAGE_ROOT, sandboxMode) {
|
|
@@ -3654,7 +3692,7 @@ async function resolveOpenCodeMcpDirectory(entry, sessionIds) {
|
|
|
3654
3692
|
// auth.json + sessions/ + state DB, while config.toml and prompts/ are
|
|
3655
3693
|
// symlinked back to the canonical ~/.codex so model/MCP/tool-permission config
|
|
3656
3694
|
// and custom prompts stay shared. The "default" account IS ~/.codex itself.
|
|
3657
|
-
const CODEX_DEFAULT_HOME = resolve(getHomePath(), '.codex');
|
|
3695
|
+
const CODEX_DEFAULT_HOME = resolve(process.env.CODEX_HOME || resolve(getHomePath(), '.codex'));
|
|
3658
3696
|
const CODEX_ACCOUNTS_ROOT = resolve(getHomePath(), '.codex-accounts');
|
|
3659
3697
|
|
|
3660
3698
|
function readCodexRuntimeModelCatalog(codexHome = CODEX_DEFAULT_HOME) {
|
|
@@ -3670,78 +3708,62 @@ function readCodexRuntimeModelCatalog(codexHome = CODEX_DEFAULT_HOME) {
|
|
|
3670
3708
|
}
|
|
3671
3709
|
const CODEX_ACCOUNTS_PATH = resolve(DATA_HOME, 'data', 'codex-accounts.json');
|
|
3672
3710
|
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3711
|
+
// The Codex accounts registry (lib/codex-accounts.js): one instance for the
|
|
3712
|
+
// sidepanel, the Assistant (brain, workers, catalog), loops and every route, so
|
|
3713
|
+
// one account state is shown everywhere. Its ChatGPT half is Codex's own
|
|
3714
|
+
// `account/read` for each account (codexAccountAuth().nativeStatus); a change
|
|
3715
|
+
// of any account reaches every window as `codex:accounts-changed`.
|
|
3716
|
+
let _assistantAccountUsage = null;
|
|
3717
|
+
let _codexAccountsInstance = null;
|
|
3718
|
+
function _codexAccounts() {
|
|
3719
|
+
if (!_codexAccountsInstance) {
|
|
3720
|
+
_codexAccountsInstance = createCodexAccounts({
|
|
3721
|
+
dataHome: DATA_HOME, homeDir: getHomePath(), accountsRoot: CODEX_ACCOUNTS_ROOT,
|
|
3722
|
+
readNativeStatus: (accountId) => codexAccountAuth().nativeStatus(accountId),
|
|
3723
|
+
prepareDefaultConfig: () => ensureCanonicalCodexConfig({ strict: true }),
|
|
3724
|
+
log: (tag, message) => console.log(`[${tag}]`, message),
|
|
3725
|
+
});
|
|
3726
|
+
_codexAccountsInstance.subscribe((event) => {
|
|
3727
|
+
try {
|
|
3728
|
+
broadcastSync({ type: 'codex:accounts-changed', accountId: event.accountId || null, removed: !!event.removed, created: !!event.created });
|
|
3729
|
+
broadcastSync({ type: 'assistant:accounts-changed', provider: 'codex', accountId: event.accountId || null, removed: !!event.removed });
|
|
3730
|
+
_assistantCatalog?.invalidate?.();
|
|
3731
|
+
} catch {}
|
|
3693
3732
|
});
|
|
3694
|
-
data.defaultId = 'default';
|
|
3695
3733
|
}
|
|
3696
|
-
return
|
|
3734
|
+
return _codexAccountsInstance;
|
|
3697
3735
|
}
|
|
3698
3736
|
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
3703
|
-
writeFileSync(CODEX_ACCOUNTS_PATH, JSON.stringify(data, null, 2), 'utf-8');
|
|
3704
|
-
} catch (err) {
|
|
3705
|
-
console.warn('[codex-accounts] Failed to write registry:', err.message);
|
|
3706
|
-
}
|
|
3737
|
+
// The registry rows (id, label, home, …) for readers that only need where an account lives.
|
|
3738
|
+
function loadCodexAccounts() {
|
|
3739
|
+
return { accounts: _codexAccounts().registryRows(), defaultId: 'default' };
|
|
3707
3740
|
}
|
|
3708
3741
|
|
|
3742
|
+
// The account, or Default when none is named or it is gone (launch selectors, session catalogs).
|
|
3709
3743
|
function getCodexAccount(accountId) {
|
|
3710
|
-
const
|
|
3711
|
-
return
|
|
3712
|
-
|| data.accounts.find((a) => a.id === data.defaultId)
|
|
3713
|
-
|| data.accounts[0]
|
|
3714
|
-
|| null;
|
|
3744
|
+
const accounts = _codexAccounts();
|
|
3745
|
+
return (accountId ? accounts.profileFor(accountId) : null) || accounts.profileFor('default');
|
|
3715
3746
|
}
|
|
3716
3747
|
|
|
3748
|
+
// The account a request names, or null: never another account in its place.
|
|
3717
3749
|
function findCodexAccount(accountId) {
|
|
3718
3750
|
if (!accountId) return null;
|
|
3719
|
-
return
|
|
3751
|
+
return _codexAccounts().profileFor(accountId);
|
|
3720
3752
|
}
|
|
3721
3753
|
|
|
3722
3754
|
function getCodexAccountHome(accountId) {
|
|
3723
3755
|
return getCodexAccount(accountId)?.home || CODEX_DEFAULT_HOME;
|
|
3724
3756
|
}
|
|
3725
3757
|
|
|
3726
|
-
|
|
3727
|
-
return {
|
|
3728
|
-
id: a.id,
|
|
3729
|
-
label: a.label || a.email || 'Account',
|
|
3730
|
-
email: a.email || '',
|
|
3731
|
-
planType: a.planType || '',
|
|
3732
|
-
type: a.type || 'chatgpt',
|
|
3733
|
-
isDefault: a.id === 'default',
|
|
3734
|
-
};
|
|
3735
|
-
}
|
|
3736
|
-
|
|
3758
|
+
// Every account with its state (mode, signed in, identity: an email or a masked key, never a key).
|
|
3737
3759
|
function listCodexAccountsForClient() {
|
|
3738
|
-
return
|
|
3760
|
+
return _codexAccounts().listForClient();
|
|
3739
3761
|
}
|
|
3740
3762
|
|
|
3741
|
-
function ensureCanonicalCodexConfig({ strict = false } = {}) {
|
|
3763
|
+
function ensureCanonicalCodexConfig({ strict = false, codexHome = CODEX_DEFAULT_HOME } = {}) {
|
|
3742
3764
|
try {
|
|
3743
|
-
if (!existsSync(
|
|
3744
|
-
const cfg = resolve(
|
|
3765
|
+
if (!existsSync(codexHome)) mkdirSync(codexHome, { recursive: true });
|
|
3766
|
+
const cfg = resolve(codexHome, 'config.toml');
|
|
3745
3767
|
if (!existsSync(cfg)) writeFileSync(cfg, '', 'utf-8');
|
|
3746
3768
|
const repair = healCodexConfig(cfg);
|
|
3747
3769
|
if (repair.healed) {
|
|
@@ -3751,50 +3773,83 @@ function ensureCanonicalCodexConfig({ strict = false } = {}) {
|
|
|
3751
3773
|
if (strict) throw error;
|
|
3752
3774
|
console.warn(`[codex-config] ${error.message}`);
|
|
3753
3775
|
}
|
|
3776
|
+
// Env/tool CLI overrides require a transport, even after a fresh CLI install
|
|
3777
|
+
// or when setup never connected Codex. Reuse Connect Codex's canonical writer;
|
|
3778
|
+
// leave an already connected transport and all unrelated settings alone.
|
|
3779
|
+
const health = inspectCodexConfig(cfg);
|
|
3780
|
+
if (!health.connected || health.issues.includes('missing_transport')) {
|
|
3781
|
+
const inspection = upsertCodexMcpConfig(cfg, buildCodexMcpDefinition());
|
|
3782
|
+
if (!inspection.valid) throw new Error(`Could not register SynaBun in ${cfg}`);
|
|
3783
|
+
return { healed: true, reason: 'registered_missing_transport', inspection };
|
|
3784
|
+
}
|
|
3785
|
+
if (!health.valid) throw new Error(`Invalid SynaBun MCP configuration in ${cfg}: ${health.issues.join(', ')}`);
|
|
3754
3786
|
return repair;
|
|
3755
3787
|
} catch (err) {
|
|
3756
|
-
console.warn(`[codex-config] Could not prepare ${
|
|
3788
|
+
console.warn(`[codex-config] Could not prepare ${codexHome}/config.toml: ${err.message}`);
|
|
3757
3789
|
if (strict) throw err;
|
|
3758
3790
|
return null;
|
|
3759
3791
|
}
|
|
3760
3792
|
}
|
|
3761
3793
|
|
|
3762
|
-
|
|
3763
|
-
|
|
3794
|
+
// Live Codex connections (panel tabs, the Assistant's brain): their account,
|
|
3795
|
+
// whether a turn is running, and how they restart under a changed credential.
|
|
3796
|
+
const _codexLiveConnections = new Set();
|
|
3797
|
+
|
|
3798
|
+
// A Codex account's credential or mode changes: refused while one of its
|
|
3799
|
+
// connections is mid-turn; afterwards its idle app-servers restart, so the next
|
|
3800
|
+
// message runs under the new credential. Other accounts are not touched.
|
|
3801
|
+
async function withCodexAccountCredentialChange(accountId, change) {
|
|
3802
|
+
const connections = [..._codexLiveConnections].filter((connection) => connection.accountId() === accountId);
|
|
3803
|
+
if (connections.some((connection) => connection.busy())) {
|
|
3804
|
+
throw Object.assign(new Error('Finish or stop this account’s Codex work before changing its sign-in.'), { status: 409 });
|
|
3805
|
+
}
|
|
3806
|
+
const result = await change();
|
|
3807
|
+
for (const connection of connections) { try { connection.credentialsChanged(); } catch {} }
|
|
3808
|
+
return result;
|
|
3809
|
+
}
|
|
3810
|
+
|
|
3811
|
+
// Codex's own store changed outside a request (a terminal sign-in ended): idle connections restart.
|
|
3812
|
+
function codexAccountStoreChanged(accountId) {
|
|
3813
|
+
for (const connection of [..._codexLiveConnections]) {
|
|
3814
|
+
if (connection.accountId() === accountId && !connection.busy()) { try { connection.credentialsChanged(); } catch {} }
|
|
3815
|
+
}
|
|
3764
3816
|
}
|
|
3765
3817
|
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
} catch (err) {
|
|
3776
|
-
console.warn('[codex-accounts] Failed to link', linkPath, '→', targetPath, err.message);
|
|
3818
|
+
let _codexAccountAuthInstance = null;
|
|
3819
|
+
function codexAccountAuth() {
|
|
3820
|
+
if (!_codexAccountAuthInstance) {
|
|
3821
|
+
_codexAccountAuthInstance = createCodexAccountAuth({
|
|
3822
|
+
accounts: _codexAccounts(), codexBin: () => getCodexBin(),
|
|
3823
|
+
prepareConfig: (codexHome) => ensureCanonicalCodexConfig({ strict: true, codexHome }),
|
|
3824
|
+
env: () => (withPathEnv(process.env, getAugmentedPath())),
|
|
3825
|
+
changeCredentials: withCodexAccountCredentialChange,
|
|
3826
|
+
});
|
|
3777
3827
|
}
|
|
3828
|
+
return _codexAccountAuthInstance;
|
|
3778
3829
|
}
|
|
3779
3830
|
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
linkSharedCodexResource(resolve(home, 'config.toml'), resolve(CODEX_DEFAULT_HOME, 'config.toml'), 'file');
|
|
3787
|
-
linkSharedCodexResource(resolve(home, 'prompts'), resolve(CODEX_DEFAULT_HOME, 'prompts'), 'dir');
|
|
3788
|
-
// Global instructions (and SynaBun's rules block in them) reach every account.
|
|
3789
|
-
// Only when the file exists: the rules installer links it later otherwise,
|
|
3790
|
-
// and an empty ~/.codex/AGENTS.md is not ours to create.
|
|
3791
|
-
if (existsSync(resolve(CODEX_DEFAULT_HOME, 'AGENTS.md'))) {
|
|
3792
|
-
linkSharedCodexResource(resolve(home, 'AGENTS.md'), resolve(CODEX_DEFAULT_HOME, 'AGENTS.md'), 'file');
|
|
3831
|
+
// Why a Codex account cannot be removed now ('' when it can): a live connection
|
|
3832
|
+
// (a panel tab, the Assistant's brain) other than `except`, a native loop, a
|
|
3833
|
+
// dispatched run, an Assistant session, or the Assistant's worker choice.
|
|
3834
|
+
function codexAccountInUse(accountId, { except = null } = {}) {
|
|
3835
|
+
if ([..._codexLiveConnections].some((connection) => connection !== except && connection.accountId() === accountId)) {
|
|
3836
|
+
return 'A Codex tab or the Assistant is running under this account. Switch it or close it first.';
|
|
3793
3837
|
}
|
|
3794
|
-
return
|
|
3838
|
+
try { if (_nativeLoopRuntime?.list({ activeOnly: true }).some((run) => run.provider === 'codex' && run.accountId === accountId)) return 'Stop native Codex automations using this account before removing it.'; } catch {}
|
|
3839
|
+
try { if (_assistantDispatcher?.accountInUse?.('codex', accountId)) return 'Dispatched runs are using this account. Stop them first.'; } catch {}
|
|
3840
|
+
try { if (_assistantRuntime?.accountInUse?.('codex', accountId)) return 'An Assistant session uses this account. Close it first.'; } catch {}
|
|
3841
|
+
try { if (_assistantCodexWorkerAccount?.() === accountId) return 'This is the Assistant\'s Codex worker account. Choose another one first.'; } catch {}
|
|
3842
|
+
return '';
|
|
3795
3843
|
}
|
|
3796
3844
|
|
|
3797
3845
|
function handleCodexSkinWebSocket(ws) {
|
|
3846
|
+
// In API-key mode with a key SynaBun holds, the app-server starts with an ephemeral
|
|
3847
|
+
// credential store and this key is its first request over stdin (lib/codex-accounts.js).
|
|
3848
|
+
// Spawn-only: never logged, sent or kept after that request.
|
|
3849
|
+
let childApiKeyLogin = null;
|
|
3850
|
+
let childUsesHeldKey = false;
|
|
3851
|
+
// Which credential the running app-server started with (launchFor().credential).
|
|
3852
|
+
let childCredential = null;
|
|
3798
3853
|
const CODEX_GREETING_MARKER_START = '[SYNABUN_CODEX_GREETING_START]';
|
|
3799
3854
|
const CODEX_GREETING_MARKER_END = '[SYNABUN_CODEX_GREETING_END]';
|
|
3800
3855
|
const CODEX_PLAN_MODE_PREFIXES = [
|
|
@@ -3843,7 +3898,27 @@ CRITICAL — When you need clarification during planning:
|
|
|
3843
3898
|
// Per-tab Codex account → drives CODEX_HOME of the spawned app-server.
|
|
3844
3899
|
let activeAccountId = 'default';
|
|
3845
3900
|
let activeCodexHome = CODEX_DEFAULT_HOME;
|
|
3846
|
-
|
|
3901
|
+
const panelAuth = createCodexPanelAuth({
|
|
3902
|
+
request: (...args) => request(...args), accountId: () => activeAccountId,
|
|
3903
|
+
onChange: state => sendToClient({ type: 'account_login_state', state }),
|
|
3904
|
+
onSuccess: async id => {
|
|
3905
|
+
const acct = await request('account/read', { refreshToken: false }, 10000);
|
|
3906
|
+
if (id !== activeAccountId) return;
|
|
3907
|
+
// What Codex now holds is the account's state everywhere (lib/codex-accounts.js).
|
|
3908
|
+
if (acct?.account?.type === 'chatgpt') _codexAccounts().noteSubscriptionSignIn(id, acct);
|
|
3909
|
+
else _codexAccounts().noteNativeStatus(id, acct || {});
|
|
3910
|
+
sendToClient({ type: 'account_info', accountId: id, account: acct, accountState: _codexAccounts().describeForClient(id) });
|
|
3911
|
+
sendToClient({ type: 'account_list', accounts: listCodexAccountsForClient(), activeAccountId });
|
|
3912
|
+
},
|
|
3913
|
+
});
|
|
3914
|
+
// This connection in the live set: account changes ask it whether it is busy and restart it when idle.
|
|
3915
|
+
const liveConnection = {
|
|
3916
|
+
accountId: () => activeAccountId,
|
|
3917
|
+
readRateLimits: () => initialized && !childUsesHeldKey ? request('account/rateLimits/read', {}, 2500) : null,
|
|
3918
|
+
busy: () => !!activeTurnId || pendingServerRequests.size > 0 || panelAuth.active(),
|
|
3919
|
+
credentialsChanged: () => retireForCredentialChange(),
|
|
3920
|
+
};
|
|
3921
|
+
_codexLiveConnections.add(liveConnection);
|
|
3847
3922
|
let codexMcpRuntimeId = `codex-sp-${randomUUID()}`;
|
|
3848
3923
|
let codexMcpRuntimeBinding = null;
|
|
3849
3924
|
// 'assistant' when this connection is the main assistant's brain: the MCP
|
|
@@ -3852,6 +3927,12 @@ CRITICAL — When you need clarification during planning:
|
|
|
3852
3927
|
// The assistant brain's computer-use grant (lib/desktop/grants.js): unlocks
|
|
3853
3928
|
// the capability-gated `computer` group in this app-server's MCP child.
|
|
3854
3929
|
let codexDesktopGrant = null;
|
|
3930
|
+
// A WhatsApp conversation brain's credential (whatsapp_send_file): taken from the in-process
|
|
3931
|
+
// brain's own socket object (lib/virtual-socket.js brainCredentialOf), never from a message a
|
|
3932
|
+
// client sends; it reaches the MCP child through the environment only (SYNABUN_BRAIN_TOKEN,
|
|
3933
|
+
// forwarded by name with env_vars), never argv. A name with TOKEN in it stays out of the
|
|
3934
|
+
// brain's own shell commands under Codex's default shell_environment_policy.
|
|
3935
|
+
let codexBrainCredential = null;
|
|
3855
3936
|
// The assistant brain's route gate (lib/assistant-route-gate-codex.js): a
|
|
3856
3937
|
// trusted session-flag PreToolUse hook plus the env it reads. Assistant only.
|
|
3857
3938
|
let codexRouteGate = null;
|
|
@@ -3880,8 +3961,10 @@ CRITICAL — When you need clarification during planning:
|
|
|
3880
3961
|
|
|
3881
3962
|
async function beforeCodexCompatRequest(type, params) {
|
|
3882
3963
|
if (type === 'thread_settings_update') {
|
|
3883
|
-
validateCodexConfigRequirements({ approval_policy: params.approvalPolicy, approvals_reviewer: params.approvalsReviewer }, await readCodexRequirements());
|
|
3964
|
+
validateCodexConfigRequirements({ approval_policy: params.approvalPolicy, approvals_reviewer: params.approvalsReviewer, sandbox_mode: ({ readOnly: 'read-only', workspaceWrite: 'workspace-write', dangerFullAccess: 'danger-full-access' })[params.sandboxPolicy?.type] }, await readCodexRequirements());
|
|
3884
3965
|
} else if (type === 'experimental_features_set') {
|
|
3966
|
+
const metadata = await readSettingsMetadata();
|
|
3967
|
+
validateCodexSettings({ features: params.enablement }, metadata, metadata.features);
|
|
3885
3968
|
validateCodexConfigRequirements({ features: params.enablement }, await readCodexRequirements());
|
|
3886
3969
|
} else if (type === 'feedback_upload') {
|
|
3887
3970
|
await readCodexRequirements();
|
|
@@ -3997,9 +4080,47 @@ CRITICAL — When you need clarification during planning:
|
|
|
3997
4080
|
...details,
|
|
3998
4081
|
}));
|
|
3999
4082
|
}
|
|
4083
|
+
// A tab names its account; one that is no longer set up runs as Default, and says so
|
|
4084
|
+
// (every answer carries activeAccountId), never under Default's home with another name.
|
|
4000
4085
|
function setActiveCodexAccount(id) {
|
|
4001
|
-
|
|
4002
|
-
|
|
4086
|
+
const account = getCodexAccount(id || 'default');
|
|
4087
|
+
activeAccountId = account?.id || 'default';
|
|
4088
|
+
activeCodexHome = account?.home || CODEX_DEFAULT_HOME;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
function sendAccountState() {
|
|
4092
|
+
sendToClient({ type: 'account_state', accountId: activeAccountId, account: _codexAccounts().describeForClient(activeAccountId), accounts: listCodexAccountsForClient() });
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
// The account's credential or mode changed (withCodexAccountCredentialChange): an idle
|
|
4096
|
+
// app-server is retired, and the next request starts one under the new credential; the
|
|
4097
|
+
// conversation resumes from its thread then. The transcript stays.
|
|
4098
|
+
function retireForCredentialChange() {
|
|
4099
|
+
const next = _codexAccounts().launchFor(activeAccountId);
|
|
4100
|
+
if (child && !activeTurnId && !pendingServerRequests.size && (!next.ok || next.credential !== childCredential)) {
|
|
4101
|
+
const old = child;
|
|
4102
|
+
const oldHome = activeCodexHome;
|
|
4103
|
+
const oldThreadIds = ownedWriterThreadIds();
|
|
4104
|
+
panelAuth.reset();
|
|
4105
|
+
initialized = false;
|
|
4106
|
+
bootPromise = null;
|
|
4107
|
+
activeThreadId = null;
|
|
4108
|
+
appBoundary.invalidate();
|
|
4109
|
+
activeThreadRuntimeFingerprint = null;
|
|
4110
|
+
subscribedThreadIds.clear();
|
|
4111
|
+
threadOwnerRoots.clear();
|
|
4112
|
+
activeTurnAutoAccept = false;
|
|
4113
|
+
for (const [, entry] of pending) {
|
|
4114
|
+
try { clearTimeout(entry.timer); entry.reject(new Error('Codex account sign-in changed')); } catch {}
|
|
4115
|
+
}
|
|
4116
|
+
pending.clear();
|
|
4117
|
+
childStdoutBuf = '';
|
|
4118
|
+
unregisterCodexMcpProfileRuntime();
|
|
4119
|
+
child = null;
|
|
4120
|
+
childCredential = null;
|
|
4121
|
+
retireCodexProcess(old, { codexHome: oldHome, threadIds: oldThreadIds, reason: 'Codex account sign-in changed' });
|
|
4122
|
+
}
|
|
4123
|
+
sendAccountState();
|
|
4003
4124
|
}
|
|
4004
4125
|
|
|
4005
4126
|
async function withCodexWriterOperation(operation, action) {
|
|
@@ -4022,6 +4143,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
4022
4143
|
})();
|
|
4023
4144
|
|
|
4024
4145
|
function sendToClient(data) {
|
|
4146
|
+
data = redactProviderSecrets(data);
|
|
4025
4147
|
if (data?.type === 'error') {
|
|
4026
4148
|
// Some legacy catch paths forward only a prefixed RPC message.
|
|
4027
4149
|
const rpcError = { ...data, code: data.code ?? (/Invalid request: unknown variant/i.test(data.message || '') ? -32600 : null) };
|
|
@@ -4146,6 +4268,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
4146
4268
|
const codexHome = activeCodexHome;
|
|
4147
4269
|
const threadIds = ownedWriterThreadIds();
|
|
4148
4270
|
shuttingDown = true;
|
|
4271
|
+
panelAuth.interrupted();
|
|
4149
4272
|
unregisterCodexMcpProfileRuntime({ cleanup: true });
|
|
4150
4273
|
childRetirementPromise = (async () => {
|
|
4151
4274
|
if (unsubscribe) await unsubscribeAllThreadsBeforeRetirement();
|
|
@@ -4364,6 +4487,19 @@ CRITICAL — When you need clarification during planning:
|
|
|
4364
4487
|
return result || {};
|
|
4365
4488
|
}
|
|
4366
4489
|
|
|
4490
|
+
async function readSettingsMetadata() {
|
|
4491
|
+
const schema = readCodexSettingsSchema(getCodexBin());
|
|
4492
|
+
const features = await listCodexPages(request, 'experimentalFeature/list', { limit: 100 });
|
|
4493
|
+
const featureSupport = readCodexFeatureSupport(getCodexBin(), features.data);
|
|
4494
|
+
// The catalog's enabled field is a process-start snapshot. config/read is
|
|
4495
|
+
// authoritative for saved defaults after reloadUserConfig writes.
|
|
4496
|
+
return { ...schema, features: features.data.map(feature => ({ ...feature,
|
|
4497
|
+
configurable: featureSupport[feature.name] === true,
|
|
4498
|
+
enabled: typeof _cachedConfig.features?.[feature.name] === 'boolean'
|
|
4499
|
+
? _cachedConfig.features[feature.name] : feature.enabled,
|
|
4500
|
+
})) };
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4367
4503
|
async function setCodexMcpServerApprovalMode(serverName, approvalMode) {
|
|
4368
4504
|
const name = String(serverName || '').trim();
|
|
4369
4505
|
if (!name) throw new Error('MCP approval request is missing its server identity');
|
|
@@ -4408,7 +4544,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
4408
4544
|
rejected: isolationStats.rejected,
|
|
4409
4545
|
reasons: { ...isolationStats.reasons },
|
|
4410
4546
|
},
|
|
4411
|
-
approvalPolicy: getCodexSidepanelApprovalPolicy(),
|
|
4547
|
+
approvalPolicy: getCodexSidepanelApprovalPolicy(_cachedConfig),
|
|
4412
4548
|
sessionOverrides: codexSessionSettings.get(activeAccountId, activeThreadId),
|
|
4413
4549
|
cwd,
|
|
4414
4550
|
},
|
|
@@ -4498,7 +4634,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
4498
4634
|
function getDefaultThreadParams(cwd = PACKAGE_ROOT, runtime = {}) {
|
|
4499
4635
|
const params = {
|
|
4500
4636
|
cwd,
|
|
4501
|
-
approvalPolicy: getCodexSidepanelApprovalPolicy(),
|
|
4637
|
+
approvalPolicy: getCodexSidepanelApprovalPolicy(_cachedConfig),
|
|
4502
4638
|
sandbox: getCachedConfigValue('sandbox_mode', 'workspace-write'),
|
|
4503
4639
|
personality: getCachedConfigValue('personality', 'pragmatic'),
|
|
4504
4640
|
};
|
|
@@ -4513,16 +4649,19 @@ CRITICAL — When you need clarification during planning:
|
|
|
4513
4649
|
threadId,
|
|
4514
4650
|
cwd,
|
|
4515
4651
|
excludeTurns: true,
|
|
4516
|
-
approvalPolicy: getCodexSidepanelApprovalPolicy(),
|
|
4652
|
+
approvalPolicy: getCodexSidepanelApprovalPolicy(_cachedConfig),
|
|
4517
4653
|
sandbox: getCachedConfigValue('sandbox_mode', 'workspace-write'),
|
|
4518
4654
|
personality: getCachedConfigValue('personality', 'pragmatic'),
|
|
4519
4655
|
};
|
|
4520
4656
|
const sessionSettings = codexSessionSettings.get(activeAccountId, threadId);
|
|
4657
|
+
const sessionSandboxMode = ({ readOnly: 'read-only', workspaceWrite: 'workspace-write', dangerFullAccess: 'danger-full-access' })[sessionSettings.sandboxPolicy?.type];
|
|
4658
|
+
if (sessionSandboxMode) params.sandbox = sessionSandboxMode;
|
|
4521
4659
|
for (const key of ['approvalPolicy', 'approvalsReviewer', 'personality', 'serviceTier']) {
|
|
4522
4660
|
if (Object.prototype.hasOwnProperty.call(sessionSettings, key)) params[key] = sessionSettings[key];
|
|
4523
4661
|
}
|
|
4524
4662
|
if (runtime.model) params.model = runtime.model;
|
|
4525
4663
|
else if (getCachedConfigValue('model')) params.model = getCachedConfigValue('model');
|
|
4664
|
+
if (sessionSettings.model != null) params.model = sessionSettings.model;
|
|
4526
4665
|
if (runtime.config) params.config = runtime.config;
|
|
4527
4666
|
return params;
|
|
4528
4667
|
}
|
|
@@ -5011,6 +5150,12 @@ CRITICAL — When you need clarification during planning:
|
|
|
5011
5150
|
|
|
5012
5151
|
function startChild() {
|
|
5013
5152
|
if (child) return child;
|
|
5153
|
+
// The account decides the credential (lib/codex-accounts.js launchFor()): inherited
|
|
5154
|
+
// OpenAI/Codex credentials out, its CODEX_HOME in, and in API-key mode the key it
|
|
5155
|
+
// holds, given over stdin after initialize to an app-server with an ephemeral store.
|
|
5156
|
+
const launch = _codexAccounts().launchFor(activeAccountId);
|
|
5157
|
+
if (!launch.ok) throw new Error(launch.message);
|
|
5158
|
+
activeCodexHome = launch.home;
|
|
5014
5159
|
let codexBin = getCodexBin();
|
|
5015
5160
|
const runtimeProfilePath = writeCodexRuntimeProfile(codexMcpRuntimeId, codexMcpProfileState.value);
|
|
5016
5161
|
const args = [
|
|
@@ -5031,24 +5176,30 @@ CRITICAL — When you need clarification during planning:
|
|
|
5031
5176
|
// (lib/browser-tool-policy.js), so Codex's own web search stays off, as for its workers.
|
|
5032
5177
|
if (codexRole === 'assistant') args.splice(args.length - 1, 0, '-c', 'web_search="disabled"');
|
|
5033
5178
|
if (codexDesktopGrant) args.splice(args.length - 1, 0, '-c', `mcp_servers.SynaBun.env.SYNABUN_DESKTOP_GRANT=${JSON.stringify(codexDesktopGrant)}`);
|
|
5179
|
+
// The variable's name on the command line, its value only in the app-server's environment (below).
|
|
5180
|
+
if (codexRole === 'assistant' && codexBrainCredential) args.splice(args.length - 1, 0, '-c', 'mcp_servers.SynaBun.env_vars=["SYNABUN_BRAIN_TOKEN"]');
|
|
5034
5181
|
if (codexRole === 'assistant' && codexRouteGate?.flags?.length) args.splice(args.length - 1, 0, ...codexRouteGate.flags);
|
|
5035
5182
|
if (/\.js$/i.test(codexBin)) {
|
|
5036
5183
|
args.unshift(codexBin);
|
|
5037
5184
|
codexBin = process.execPath;
|
|
5038
5185
|
}
|
|
5186
|
+
// API-key mode with a key SynaBun holds: an ephemeral credential store, before the subcommand.
|
|
5187
|
+
args.splice(args.length - 1, 0, ...codexAppServerArgs(launch));
|
|
5039
5188
|
const useShell = !codexBin.includes(sep)
|
|
5040
5189
|
|| (process.platform === 'win32' && /\.(cmd|bat)$/i.test(codexBin));
|
|
5041
5190
|
const env = {
|
|
5042
|
-
...process.env,
|
|
5191
|
+
...applyCodexLaunchEnv(process.env, launch),
|
|
5043
5192
|
NO_COLOR: '1',
|
|
5044
|
-
CODEX_HOME: activeCodexHome,
|
|
5045
5193
|
SYNABUN_PROFILE: codexMcpProfileState.value,
|
|
5046
5194
|
SYNABUN_TERMINAL_SESSION: codexMcpRuntimeId,
|
|
5047
5195
|
SYNABUN_RUNTIME_PROFILE_PATH: runtimeProfilePath,
|
|
5048
5196
|
SYNABUN_TOOL_CATALOG_MODE: 'deferred',
|
|
5049
5197
|
...(codexDesktopGrant ? { SYNABUN_DESKTOP_GRANT: codexDesktopGrant } : {}),
|
|
5050
5198
|
...(codexRole === 'assistant' && codexRouteGate?.env ? codexRouteGate.env : {}),
|
|
5199
|
+
...(codexRole === 'assistant' && codexBrainCredential ? { SYNABUN_BRAIN_TOKEN: codexBrainCredential } : {}),
|
|
5051
5200
|
};
|
|
5201
|
+
// Never one inherited from the launching shell: only this brain's own credential reaches its MCP child.
|
|
5202
|
+
if (!(codexRole === 'assistant' && codexBrainCredential)) delete env.SYNABUN_BRAIN_TOKEN;
|
|
5052
5203
|
delete env.FORCE_COLOR;
|
|
5053
5204
|
delete env.CLICOLOR_FORCE;
|
|
5054
5205
|
const proc = spawn(shellCommand(codexBin, useShell, { isFile: existsSync }), args, {
|
|
@@ -5058,10 +5209,13 @@ CRITICAL — When you need clarification during planning:
|
|
|
5058
5209
|
shell: useShell,
|
|
5059
5210
|
});
|
|
5060
5211
|
child = proc;
|
|
5212
|
+
childApiKeyLogin = launch.apiKey || null;
|
|
5213
|
+
childUsesHeldKey = !!launch.apiKey;
|
|
5214
|
+
childCredential = launch.credential;
|
|
5061
5215
|
childRetirementPromise = null;
|
|
5062
5216
|
writerLifecycleState.releaseRequested = false;
|
|
5063
5217
|
registerCodexMcpProfileRuntime();
|
|
5064
|
-
console.log(`[codex-skin] Spawned app-server pid ${proc.pid || '?'} account=${activeAccountId} profile=${codexMcpProfileState.value} runtime=${codexMcpRuntimeId} CODEX_HOME=${activeCodexHome}`);
|
|
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}`);
|
|
5065
5219
|
proc.stdout.setEncoding('utf-8');
|
|
5066
5220
|
proc.stdout.on('data', (chunk) => {
|
|
5067
5221
|
if (child !== proc) return; // stale process from a previous account
|
|
@@ -5073,9 +5227,9 @@ CRITICAL — When you need clarification during planning:
|
|
|
5073
5227
|
if (!line) continue;
|
|
5074
5228
|
let msg;
|
|
5075
5229
|
try {
|
|
5076
|
-
msg = JSON.parse(line);
|
|
5230
|
+
msg = redactProviderSecrets(JSON.parse(line));
|
|
5077
5231
|
} catch (err) {
|
|
5078
|
-
console.warn('[codex-skin] Invalid JSON from app-server
|
|
5232
|
+
console.warn('[codex-skin] Invalid JSON from app-server');
|
|
5079
5233
|
continue;
|
|
5080
5234
|
}
|
|
5081
5235
|
|
|
@@ -5188,6 +5342,9 @@ CRITICAL — When you need clarification during planning:
|
|
|
5188
5342
|
if (msg.method) {
|
|
5189
5343
|
const method = msg.method;
|
|
5190
5344
|
const params = msg.params || {};
|
|
5345
|
+
if (method === 'account/rateLimits/updated') _assistantAccountUsage?.note('codex', activeAccountId, params);
|
|
5346
|
+
if (method === 'account/login/completed') void panelAuth.complete(params);
|
|
5347
|
+
if (method === 'account/updated' && params.authMode === null && !panelAuth.active()) panelAuth.reset();
|
|
5191
5348
|
const scopedThreadId = notificationThreadId(method, params);
|
|
5192
5349
|
const scopedTurnId = notificationTurnId(method, params);
|
|
5193
5350
|
const expectedThreadId = displayedThreadId;
|
|
@@ -5262,11 +5419,11 @@ CRITICAL — When you need clarification during planning:
|
|
|
5262
5419
|
}
|
|
5263
5420
|
});
|
|
5264
5421
|
proc.stderr.setEncoding('utf-8');
|
|
5265
|
-
|
|
5266
|
-
|
|
5267
|
-
if (!text)
|
|
5268
|
-
console.log('[codex-skin][stderr]', text);
|
|
5422
|
+
const recordNativeDiagnostic = providerDiagnosticSink(text => {
|
|
5423
|
+
// Key-login attempts expose only structured, filtered errors to the UI.
|
|
5424
|
+
if (!childUsesHeldKey && !panelAuth.active()) console.log('[codex-skin][stderr]', text);
|
|
5269
5425
|
});
|
|
5426
|
+
proc.stderr.on('data', recordNativeDiagnostic);
|
|
5270
5427
|
proc.on('error', (err) => {
|
|
5271
5428
|
if (child !== proc) return;
|
|
5272
5429
|
console.error('[codex-skin] app-server spawn error:', err.message);
|
|
@@ -5279,6 +5436,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5279
5436
|
});
|
|
5280
5437
|
proc.on('close', (code) => {
|
|
5281
5438
|
if (child !== proc) return; // a newer process replaced this one (e.g. account switch)
|
|
5439
|
+
panelAuth.interrupted();
|
|
5282
5440
|
unregisterCodexMcpProfileRuntime();
|
|
5283
5441
|
const wasShuttingDown = shuttingDown;
|
|
5284
5442
|
child = null;
|
|
@@ -5310,7 +5468,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5310
5468
|
// Repair SynaBun's legacy duplicate-env config before Codex parses it.
|
|
5311
5469
|
// Doing this at the sidepanel boundary avoids the circular failure where
|
|
5312
5470
|
// Codex rejects config.toml before it can launch SynaBun's MCP process.
|
|
5313
|
-
const repair = ensureCanonicalCodexConfig();
|
|
5471
|
+
const repair = ensureCanonicalCodexConfig({ strict: true, codexHome: activeCodexHome });
|
|
5314
5472
|
if (repair?.healed) {
|
|
5315
5473
|
sendToClient({
|
|
5316
5474
|
type: 'codex_config_repaired',
|
|
@@ -5319,7 +5477,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5319
5477
|
});
|
|
5320
5478
|
}
|
|
5321
5479
|
// Sync tool approvals only after the base transport is known to be valid.
|
|
5322
|
-
syncCodexToolPermissions([..._codexPermittedTools]);
|
|
5480
|
+
syncCodexToolPermissions([..._codexPermittedTools], activeCodexHome);
|
|
5323
5481
|
const codexBin = getCodexBin();
|
|
5324
5482
|
let cliVersion = null;
|
|
5325
5483
|
let sdkVersion = null;
|
|
@@ -5339,9 +5497,27 @@ CRITICAL — When you need clarification during planning:
|
|
|
5339
5497
|
capabilities: {
|
|
5340
5498
|
experimentalApi: true,
|
|
5341
5499
|
},
|
|
5342
|
-
}, 15000).then((result) => {
|
|
5343
|
-
initialized = true;
|
|
5500
|
+
}, 15000).then(async (result) => {
|
|
5344
5501
|
writeRpc({ jsonrpc: '2.0', method: 'initialized' });
|
|
5502
|
+
// API-key mode: the key SynaBun holds signs this app-server in before anything else
|
|
5503
|
+
// runs; its ephemeral store keeps nothing and leaves the account's ChatGPT login alone.
|
|
5504
|
+
if (childApiKeyLogin) {
|
|
5505
|
+
let apiKey = childApiKeyLogin;
|
|
5506
|
+
childApiKeyLogin = null;
|
|
5507
|
+
try {
|
|
5508
|
+
const login = await request('account/login/start', { type: 'apiKey', apiKey }, 15000);
|
|
5509
|
+
if (login?.type !== 'apiKey') throw new Error('unexpected login answer');
|
|
5510
|
+
} catch {
|
|
5511
|
+
// A half-started app-server is not reused: the next request starts a fresh one.
|
|
5512
|
+
const failed = child;
|
|
5513
|
+
child = null;
|
|
5514
|
+
childCredential = null;
|
|
5515
|
+
unregisterCodexMcpProfileRuntime();
|
|
5516
|
+
try { failed?.kill('SIGTERM'); } catch {}
|
|
5517
|
+
throw new Error('Codex could not start on this account\'s API key. Save the key again, or switch the account to its ChatGPT sign-in.');
|
|
5518
|
+
} finally { apiKey = null; }
|
|
5519
|
+
}
|
|
5520
|
+
initialized = true;
|
|
5345
5521
|
codexCapabilities.runtime.serverInfo = result || {};
|
|
5346
5522
|
codexCapabilities.runtime.cliVersion ||= parseCodexVersion(result?.userAgent);
|
|
5347
5523
|
sendToClient({ type: 'capabilities', ...codexCapabilitySnapshot() });
|
|
@@ -5412,9 +5588,11 @@ CRITICAL — When you need clarification during planning:
|
|
|
5412
5588
|
// The close handler's `child !== proc` guard prevents the dying process from
|
|
5413
5589
|
// clobbering the freshly-spawned one. Callers must ensure no turn is active.
|
|
5414
5590
|
async function switchCodexAccount(id) {
|
|
5591
|
+
if (panelAuth.active()) throw new Error('Cancel or finish sign-in before switching accounts');
|
|
5415
5592
|
const oldHome = activeCodexHome;
|
|
5416
5593
|
const oldThreadIds = ownedWriterThreadIds();
|
|
5417
5594
|
setActiveCodexAccount(id);
|
|
5595
|
+
panelAuth.reset();
|
|
5418
5596
|
for (const key of Object.keys(_cachedConfig)) delete _cachedConfig[key];
|
|
5419
5597
|
const old = child;
|
|
5420
5598
|
initialized = false;
|
|
@@ -5498,45 +5676,6 @@ CRITICAL — When you need clarification during planning:
|
|
|
5498
5676
|
return { profile: normalized, restarted: true, threadId: resumeId };
|
|
5499
5677
|
}
|
|
5500
5678
|
|
|
5501
|
-
// After account/login/start opens the OAuth browser, watch the new home's
|
|
5502
|
-
// auth.json. Once Codex writes credentials, read the account and persist its
|
|
5503
|
-
// email/plan as the account label, then push the update to the client.
|
|
5504
|
-
function watchCodexLogin(id, home) {
|
|
5505
|
-
try { _codexLoginWatcher?.close?.(); } catch {}
|
|
5506
|
-
_codexLoginWatcher = null;
|
|
5507
|
-
let finished = false;
|
|
5508
|
-
const finalize = async () => {
|
|
5509
|
-
if (finished) return;
|
|
5510
|
-
finished = true;
|
|
5511
|
-
try { _codexLoginWatcher?.close?.(); } catch {}
|
|
5512
|
-
_codexLoginWatcher = null;
|
|
5513
|
-
try {
|
|
5514
|
-
const acct = await safeAccountRead(false);
|
|
5515
|
-
const info = acct?.account || acct || {};
|
|
5516
|
-
const data = loadCodexAccounts();
|
|
5517
|
-
const entry = data.accounts.find((a) => a.id === id);
|
|
5518
|
-
if (entry) {
|
|
5519
|
-
entry.email = info.email || entry.email || '';
|
|
5520
|
-
entry.planType = info.planType || entry.planType || '';
|
|
5521
|
-
entry.label = info.email || entry.label || 'ChatGPT account';
|
|
5522
|
-
saveCodexAccounts(data);
|
|
5523
|
-
}
|
|
5524
|
-
sendToClient({ type: 'account_info', accountId: id, account: acct || {} });
|
|
5525
|
-
sendToClient({ type: 'account_list', accounts: listCodexAccountsForClient(), activeAccountId });
|
|
5526
|
-
} catch (err) {
|
|
5527
|
-
console.warn('[codex-accounts] login finalize failed:', err.message);
|
|
5528
|
-
}
|
|
5529
|
-
};
|
|
5530
|
-
try {
|
|
5531
|
-
_codexLoginWatcher = fsWatch(home, (_evt, fname) => {
|
|
5532
|
-
if (fname === 'auth.json') setTimeout(finalize, 500);
|
|
5533
|
-
});
|
|
5534
|
-
setTimeout(() => { try { _codexLoginWatcher?.close?.(); } catch {} _codexLoginWatcher = null; }, 5 * 60 * 1000);
|
|
5535
|
-
} catch (err) {
|
|
5536
|
-
console.warn('[codex-accounts] auth.json watch failed:', err.message);
|
|
5537
|
-
}
|
|
5538
|
-
}
|
|
5539
|
-
|
|
5540
5679
|
async function bootstrapThread(threadId, cwd = PACKAGE_ROOT) {
|
|
5541
5680
|
await ensureInitialized();
|
|
5542
5681
|
if (!threadId) {
|
|
@@ -5889,7 +6028,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5889
6028
|
const turnParams = {
|
|
5890
6029
|
threadId,
|
|
5891
6030
|
cwd,
|
|
5892
|
-
approvalPolicy: getCodexSidepanelApprovalPolicy(),
|
|
6031
|
+
approvalPolicy: getCodexSidepanelApprovalPolicy(_cachedConfig),
|
|
5893
6032
|
personality: getCachedConfigValue('personality', 'pragmatic'),
|
|
5894
6033
|
// opts.sandboxMode 'read-only': the Assistant brain of a WhatsApp session (lib/assistant-brains/codex.js).
|
|
5895
6034
|
sandboxPolicy: buildCodexSandboxPolicy(cwd, isPlanModeTurn || opts.sandboxMode === 'read-only' ? 'read-only' : getCachedConfigValue('sandbox_mode')),
|
|
@@ -5898,8 +6037,8 @@ CRITICAL — When you need clarification during planning:
|
|
|
5898
6037
|
if (Object.prototype.hasOwnProperty.call(_cachedConfig, 'service_tier')) turnParams.serviceTier = _cachedConfig.service_tier;
|
|
5899
6038
|
if (getCachedConfigValue('model_reasoning_summary')) turnParams.summary = getCachedConfigValue('model_reasoning_summary');
|
|
5900
6039
|
const sessionSettings = codexSessionSettings.get(activeAccountId, threadId);
|
|
5901
|
-
for (const key of ['personality', 'summary', 'serviceTier', 'approvalPolicy', 'approvalsReviewer', 'disabledPluginIds']) {
|
|
5902
|
-
if (Object.prototype.hasOwnProperty.call(sessionSettings, key)) turnParams[key] = sessionSettings[key];
|
|
6040
|
+
for (const key of ['personality', 'summary', 'serviceTier', 'approvalPolicy', 'sandboxPolicy', 'approvalsReviewer', 'disabledPluginIds']) {
|
|
6041
|
+
if (Object.prototype.hasOwnProperty.call(sessionSettings, key) && sessionSettings[key] !== null) turnParams[key] = sessionSettings[key];
|
|
5903
6042
|
}
|
|
5904
6043
|
if (Array.isArray(opts.localImages) && opts.localImages.length) {
|
|
5905
6044
|
turnParams.local_images = opts.localImages;
|
|
@@ -5908,10 +6047,13 @@ CRITICAL — When you need clarification during planning:
|
|
|
5908
6047
|
else if (getCachedConfigValue('model')) turnParams.model = getCachedConfigValue('model');
|
|
5909
6048
|
if (opts.effort) turnParams.effort = opts.effort;
|
|
5910
6049
|
else if (getCachedConfigValue('model_reasoning_effort')) turnParams.effort = getCachedConfigValue('model_reasoning_effort');
|
|
6050
|
+
if (isPlanModeTurn || opts.sandboxMode === 'read-only') turnParams.sandboxPolicy = buildCodexSandboxPolicy(cwd, 'read-only');
|
|
6051
|
+
if (sessionSettings.model != null) turnParams.model = sessionSettings.model;
|
|
6052
|
+
if (sessionSettings.effort != null) turnParams.effort = sessionSettings.effort;
|
|
5911
6053
|
const collaborationMode = buildCodexCollaborationMode({
|
|
5912
6054
|
mode: isPlanModeTurn ? 'plan' : 'default',
|
|
5913
6055
|
model: turnParams.model,
|
|
5914
|
-
effort: opts.effort || (isPlanModeTurn
|
|
6056
|
+
effort: sessionSettings.effort || opts.effort || (isPlanModeTurn
|
|
5915
6057
|
? getCachedConfigValue('plan_mode_reasoning_effort', turnParams.effort)
|
|
5916
6058
|
: turnParams.effort),
|
|
5917
6059
|
developerInstructions: opts.developerInstructions || null,
|
|
@@ -6140,6 +6282,8 @@ CRITICAL — When you need clarification during planning:
|
|
|
6140
6282
|
if (msg.windowId) wsWindowId = msg.windowId;
|
|
6141
6283
|
if (!child && msg.role === 'assistant') codexRole = 'assistant';
|
|
6142
6284
|
if (!child && msg.role === 'assistant' && typeof msg.desktopGrant === 'string' && /^sbd_[A-Za-z0-9_-]{43}$/.test(msg.desktopGrant)) codexDesktopGrant = msg.desktopGrant;
|
|
6285
|
+
// Only the in-process brain's socket carries one (a client's WebSocket never does); no message field is read.
|
|
6286
|
+
if (!child && msg.role === 'assistant' && brainCredentialOf(ws)) codexBrainCredential = brainCredentialOf(ws);
|
|
6143
6287
|
if (!child && msg.role === 'assistant' && msg.routeGate && Array.isArray(msg.routeGate.flags) && msg.routeGate.flags.every((f) => typeof f === 'string')) {
|
|
6144
6288
|
codexRouteGate = { flags: [...msg.routeGate.flags], env: Object.fromEntries(Object.entries(msg.routeGate.env || {}).filter(([k, v]) => /^SYNABUN_ROUTE_GATE_[A-Z_]+$/.test(k) && typeof v === 'string')) };
|
|
6145
6289
|
}
|
|
@@ -6231,10 +6375,21 @@ CRITICAL — When you need clarification during planning:
|
|
|
6231
6375
|
if (await handleCodexCompatMessage(msg, {
|
|
6232
6376
|
activeThreadId, activeTurnId, cwd: PACKAGE_ROOT,
|
|
6233
6377
|
role: codexRole, getActiveThreadId: () => activeThreadId,
|
|
6378
|
+
sandboxPolicyForMode: (mode, cwd) => {
|
|
6379
|
+
validateCodexSettings({ sandbox_mode: mode }, readCodexSettingsSchema(getCodexBin()));
|
|
6380
|
+
return buildCodexSandboxPolicy(cwd, mode);
|
|
6381
|
+
},
|
|
6234
6382
|
limitAppCall: message => appBoundary.consume(activeThreadId, message.originCallId),
|
|
6235
6383
|
resolveAppCard: (callId, threadId) => appBoundary.resolve(threadId, callId,
|
|
6236
6384
|
() => request('thread/read', { threadId, includeTurns: true }, 10000)),
|
|
6237
6385
|
ensureInitialized, request, send: sendToClient,
|
|
6386
|
+
setFeatureEnablement: async enablement => {
|
|
6387
|
+
await request('config/batchWrite', {
|
|
6388
|
+
edits: buildCodexConfigEdits({ features: enablement }), reloadUserConfig: true,
|
|
6389
|
+
}, 10000);
|
|
6390
|
+
await readConfigState();
|
|
6391
|
+
return { enablement };
|
|
6392
|
+
},
|
|
6238
6393
|
registry: codexCapabilities, capabilities: codexCapabilitySnapshot,
|
|
6239
6394
|
withWriterOperation: withCodexWriterOperation,
|
|
6240
6395
|
beforeRequest: beforeCodexCompatRequest, afterRequest: afterCodexCompatRequest,
|
|
@@ -6397,30 +6552,54 @@ CRITICAL — When you need clarification during planning:
|
|
|
6397
6552
|
return;
|
|
6398
6553
|
}
|
|
6399
6554
|
if (msg.type === 'account_read') {
|
|
6555
|
+
// `accountState` is the account as every surface shows it (lib/codex-accounts.js describe()):
|
|
6556
|
+
// its mode, whether it is signed in and its identity. `account` is the running app-server's
|
|
6557
|
+
// own answer, kept for older pages; an account that is signed out is not started for it.
|
|
6558
|
+
const accounts = _codexAccounts();
|
|
6559
|
+
const known = accounts.describeForClient(activeAccountId);
|
|
6560
|
+
if (known?.signedIn === false && !child) {
|
|
6561
|
+
sendToClient({ type: 'account_info', requestId: msg.requestId || null, accountId: activeAccountId, account: { account: null }, accountState: known, state: panelAuth.snapshot() });
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6400
6564
|
try {
|
|
6401
6565
|
await ensureInitialized();
|
|
6402
6566
|
const result = await request('account/read', { refreshToken: !!msg.refreshToken }, 10000);
|
|
6403
|
-
|
|
6567
|
+
// The running app-server answers for its own store only while it runs on it (not on a held key).
|
|
6568
|
+
if (!childUsesHeldKey && result && Object.hasOwn(result, 'account')) accounts.noteNativeStatus(activeAccountId, result);
|
|
6569
|
+
sendToClient({ type: 'account_info', requestId: msg.requestId || null, accountId: activeAccountId, account: result || {}, accountState: accounts.describeForClient(activeAccountId), state: panelAuth.snapshot() });
|
|
6404
6570
|
} catch (err) {
|
|
6405
|
-
sendToClient({ type: 'account_info', requestId: msg.requestId || null, account: {}, error: err.message });
|
|
6571
|
+
sendToClient({ type: 'account_info', requestId: msg.requestId || null, accountId: activeAccountId, account: {}, accountState: accounts.describeForClient(activeAccountId), error: err.message });
|
|
6406
6572
|
}
|
|
6407
6573
|
return;
|
|
6408
6574
|
}
|
|
6409
6575
|
if (msg.type === 'codex_login') {
|
|
6410
6576
|
try {
|
|
6577
|
+
if (activeTurnId || pendingServerRequests.size) throw new Error('Finish or stop the current turn before signing in');
|
|
6411
6578
|
await ensureInitialized();
|
|
6412
|
-
const
|
|
6413
|
-
sendToClient({ type: 'account_login_started', requestId: msg.requestId || null,
|
|
6579
|
+
const state = await panelAuth.start(msg.loginType || 'chatgpt');
|
|
6580
|
+
sendToClient({ type: 'account_login_started', requestId: msg.requestId || null, state });
|
|
6414
6581
|
} catch (err) {
|
|
6415
6582
|
sendToClient({ type: 'error', requestId: msg.requestId || null, message: `Login failed: ${err.message}` });
|
|
6416
6583
|
}
|
|
6417
6584
|
return;
|
|
6418
6585
|
}
|
|
6586
|
+
if (msg.type === 'account_login_status' || msg.type === 'account_login_cancel') {
|
|
6587
|
+
try {
|
|
6588
|
+
if (msg.type === 'account_login_cancel') await panelAuth.cancel();
|
|
6589
|
+
sendToClient({ type: 'account_login_state', requestId: msg.requestId || null, state: panelAuth.snapshot() });
|
|
6590
|
+
} catch (err) { sendToClient({ type: 'error', requestId: msg.requestId || null, message: err.message }); }
|
|
6591
|
+
return;
|
|
6592
|
+
}
|
|
6419
6593
|
if (msg.type === 'account_logout') {
|
|
6594
|
+
// An older page's Logout: the credential the account uses now is signed out, the
|
|
6595
|
+
// same way as the account menu's per-credential sign-out (lib/codex-account-auth.js).
|
|
6420
6596
|
try {
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6597
|
+
if (activeTurnId || pendingServerRequests.size) throw new Error('Finish or stop the current turn before signing out');
|
|
6598
|
+
await panelAuth.cancel();
|
|
6599
|
+
const row = _codexAccounts().describe(activeAccountId);
|
|
6600
|
+
const account = await codexAccountAuth().signOut(activeAccountId, row?.mode === 'apiKey' ? 'apiKey' : 'subscription');
|
|
6601
|
+
panelAuth.reset();
|
|
6602
|
+
sendToClient({ type: 'account_logged_out', requestId: msg.requestId || null, result: {}, accountState: account });
|
|
6424
6603
|
} catch (err) {
|
|
6425
6604
|
sendToClient({ type: 'error', requestId: msg.requestId || null, message: `Logout failed: ${err.message}` });
|
|
6426
6605
|
}
|
|
@@ -6437,7 +6616,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
6437
6616
|
}
|
|
6438
6617
|
if (msg.type === 'account_switch') {
|
|
6439
6618
|
try {
|
|
6440
|
-
const target =
|
|
6619
|
+
const target = findCodexAccount(msg.accountId);
|
|
6441
6620
|
if (!target) throw new Error('Unknown account');
|
|
6442
6621
|
if (target.id === activeAccountId) {
|
|
6443
6622
|
const acct = await safeAccountRead();
|
|
@@ -6456,22 +6635,15 @@ CRITICAL — When you need clarification during planning:
|
|
|
6456
6635
|
if (msg.type === 'account_add_start') {
|
|
6457
6636
|
try {
|
|
6458
6637
|
if (activeTurnId) throw new Error('Finish or stop the current turn before adding an account');
|
|
6459
|
-
|
|
6460
|
-
const
|
|
6461
|
-
const data = loadCodexAccounts();
|
|
6462
|
-
data.accounts.push({
|
|
6463
|
-
id, label: 'New account', email: '', planType: '',
|
|
6464
|
-
type: 'chatgpt', home, createdAt: new Date().toISOString(),
|
|
6465
|
-
});
|
|
6466
|
-
saveCodexAccounts(data);
|
|
6638
|
+
if (panelAuth.active()) throw new Error('Cancel or finish sign-in before adding an account');
|
|
6639
|
+
const { id } = _codexAccounts().create({});
|
|
6467
6640
|
await withCodexWriterOperation('account_add_switch', () => switchCodexAccount(id));
|
|
6468
|
-
const
|
|
6469
|
-
watchCodexLogin(id, home);
|
|
6641
|
+
const state = await panelAuth.start('chatgpt');
|
|
6470
6642
|
sendToClient({
|
|
6471
6643
|
type: 'account_add_started',
|
|
6472
6644
|
requestId: msg.requestId || null,
|
|
6473
6645
|
accountId: id,
|
|
6474
|
-
|
|
6646
|
+
state,
|
|
6475
6647
|
accounts: listCodexAccountsForClient(),
|
|
6476
6648
|
activeAccountId,
|
|
6477
6649
|
});
|
|
@@ -6483,28 +6655,14 @@ CRITICAL — When you need clarification during planning:
|
|
|
6483
6655
|
if (msg.type === 'account_remove') {
|
|
6484
6656
|
try {
|
|
6485
6657
|
if (msg.accountId === 'default') throw new Error('The Default account cannot be removed');
|
|
6486
|
-
|
|
6487
|
-
const
|
|
6488
|
-
if (
|
|
6489
|
-
const nativeAccountInUse = _nativeLoopRuntime?.list({ activeOnly: true })
|
|
6490
|
-
.some((run) => run.provider === 'codex' && run.accountId === msg.accountId)
|
|
6491
|
-
|| _assistantRuntime?.accountInUse?.('codex', msg.accountId)
|
|
6492
|
-
|| _assistantDispatcher?.accountInUse?.('codex', msg.accountId);
|
|
6493
|
-
if (nativeAccountInUse) {
|
|
6494
|
-
throw new Error('Stop native Codex automations using this account before removing it');
|
|
6495
|
-
}
|
|
6496
|
-
const removed = data.accounts[idx];
|
|
6658
|
+
if (!findCodexAccount(msg.accountId)) throw new Error('Unknown account');
|
|
6659
|
+
const reason = codexAccountInUse(msg.accountId, { except: liveConnection });
|
|
6660
|
+
if (reason) throw new Error(reason);
|
|
6497
6661
|
if (activeAccountId === msg.accountId) {
|
|
6498
6662
|
if (activeTurnId) throw new Error('Finish or stop the current turn before removing the active account');
|
|
6499
6663
|
await withCodexWriterOperation('account_remove_switch', () => switchCodexAccount('default'));
|
|
6500
6664
|
}
|
|
6501
|
-
|
|
6502
|
-
saveCodexAccounts(data);
|
|
6503
|
-
try {
|
|
6504
|
-
if (removed.home && removed.home !== CODEX_DEFAULT_HOME && removed.home.startsWith(CODEX_ACCOUNTS_ROOT)) {
|
|
6505
|
-
rmSync(removed.home, { recursive: true, force: true });
|
|
6506
|
-
}
|
|
6507
|
-
} catch (e) { console.warn('[codex-accounts] Failed to delete home:', e.message); }
|
|
6665
|
+
_codexAccounts().remove(msg.accountId, { inUse: (id) => codexAccountInUse(id, { except: liveConnection }) });
|
|
6508
6666
|
sendToClient({
|
|
6509
6667
|
type: 'account_removed',
|
|
6510
6668
|
requestId: msg.requestId || null,
|
|
@@ -6519,12 +6677,9 @@ CRITICAL — When you need clarification during planning:
|
|
|
6519
6677
|
}
|
|
6520
6678
|
if (msg.type === 'account_rename') {
|
|
6521
6679
|
try {
|
|
6522
|
-
const
|
|
6523
|
-
const acct =
|
|
6524
|
-
if (!acct) throw new Error('Unknown account');
|
|
6525
|
-
const label = String(msg.label || '').trim().slice(0, 60);
|
|
6526
|
-
if (label) acct.label = label;
|
|
6527
|
-
saveCodexAccounts(data);
|
|
6680
|
+
const label = String(msg.label || '').trim().slice(0, 80);
|
|
6681
|
+
const acct = label ? _codexAccounts().rename(msg.accountId, label) : _codexAccounts().describe(msg.accountId);
|
|
6682
|
+
if (!acct || acct.id !== msg.accountId) throw new Error('Unknown account');
|
|
6528
6683
|
sendToClient({
|
|
6529
6684
|
type: 'account_renamed',
|
|
6530
6685
|
requestId: msg.requestId || null,
|
|
@@ -6551,7 +6706,8 @@ CRITICAL — When you need clarification during planning:
|
|
|
6551
6706
|
try {
|
|
6552
6707
|
await ensureInitialized();
|
|
6553
6708
|
const result = await readConfigState(msg.cwd || PACKAGE_ROOT);
|
|
6554
|
-
|
|
6709
|
+
const settingsSchema = await readSettingsMetadata();
|
|
6710
|
+
sendToClient({ type: 'config_data', requestId: msg.requestId || null, config: result || {}, settingsSchema, sessionOverrides: codexSessionSettings.get(activeAccountId, msg.threadId || activeThreadId) });
|
|
6555
6711
|
} catch (err) {
|
|
6556
6712
|
sendToClient({ type: 'config_data', requestId: msg.requestId || null, config: {}, error: err.message });
|
|
6557
6713
|
}
|
|
@@ -6560,12 +6716,14 @@ CRITICAL — When you need clarification during planning:
|
|
|
6560
6716
|
if (msg.type === 'config_write') {
|
|
6561
6717
|
try {
|
|
6562
6718
|
await ensureInitialized();
|
|
6563
|
-
const
|
|
6719
|
+
const metadata = await readSettingsMetadata();
|
|
6720
|
+
const supported = validateCodexSettings(msg.config || {}, metadata, metadata.features);
|
|
6721
|
+
const nextConfig = validateCodexConfigRequirements(supported, await readCodexRequirements());
|
|
6564
6722
|
const edits = buildCodexConfigEdits(nextConfig);
|
|
6565
6723
|
await request('config/batchWrite', { edits, reloadUserConfig: true }, 10000);
|
|
6566
|
-
await readConfigState(msg.cwd || PACKAGE_ROOT);
|
|
6724
|
+
const config = await readConfigState(msg.cwd || PACKAGE_ROOT);
|
|
6567
6725
|
codexContextModels = [];
|
|
6568
|
-
sendToClient({ type: 'config_saved', requestId: msg.requestId || null });
|
|
6726
|
+
sendToClient({ type: 'config_saved', requestId: msg.requestId || null, config });
|
|
6569
6727
|
sendToClient({ type: 'catalogs_changed', catalogs: ['models', 'skills', 'apps', 'mcp', 'features', 'config'] });
|
|
6570
6728
|
} catch (err) {
|
|
6571
6729
|
sendToClient({ type: 'error', requestId: msg.requestId || null, message: `Config save failed: ${err.message}` });
|
|
@@ -6671,7 +6829,11 @@ CRITICAL — When you need clarification during planning:
|
|
|
6671
6829
|
if (msg.type === 'experimental_features') {
|
|
6672
6830
|
try {
|
|
6673
6831
|
await ensureInitialized();
|
|
6832
|
+
await readConfigState(msg.cwd || PACKAGE_ROOT);
|
|
6674
6833
|
const result = await request('experimentalFeature/list', codexPagination(msg), 10000);
|
|
6834
|
+
for (const feature of result?.data || []) {
|
|
6835
|
+
if (typeof _cachedConfig.features?.[feature.name] === 'boolean') feature.enabled = _cachedConfig.features[feature.name];
|
|
6836
|
+
}
|
|
6675
6837
|
sendToClient({ type: 'experimental_features', requestId: msg.requestId || null, features: result || {} });
|
|
6676
6838
|
} catch (err) {
|
|
6677
6839
|
sendToClient({ type: 'experimental_features', requestId: msg.requestId || null, features: {}, error: err.message });
|
|
@@ -7207,15 +7369,20 @@ CRITICAL — When you need clarification during planning:
|
|
|
7207
7369
|
}
|
|
7208
7370
|
void retireCurrentCodexChild('WebSocket closed without a reattach identity', { unsubscribe: false });
|
|
7209
7371
|
};
|
|
7372
|
+
ws.on('close', () => _codexLiveConnections.delete(liveConnection));
|
|
7210
7373
|
ws.on('close', handleCodexSocketClose);
|
|
7211
7374
|
}
|
|
7212
7375
|
|
|
7213
7376
|
const _codexModelListCache = new Map();
|
|
7214
7377
|
const CODEX_MODEL_LIST_CACHE_MS = 60_000;
|
|
7215
7378
|
|
|
7216
|
-
|
|
7217
|
-
|
|
7218
|
-
|
|
7379
|
+
// `launch` (lib/codex-accounts.js launchFor()): the list is read under the account's own credential,
|
|
7380
|
+
// with no inherited one, as its sessions would run (in API-key mode, the key it holds over stdin).
|
|
7381
|
+
async function queryCodexAppServerModels(codexHome = CODEX_DEFAULT_HOME, launch = null) {
|
|
7382
|
+
ensureCanonicalCodexConfig({ strict: true, codexHome: launch?.home || codexHome });
|
|
7383
|
+
// The Codex the resolver found (Settings' path first); a command line in Settings ("wsl codex") as written.
|
|
7384
|
+
let codexBin = configuredCommandLine('codex') || getCodexBin();
|
|
7385
|
+
const args = [...(launch?.ok ? codexAppServerArgs(launch) : []), 'app-server'];
|
|
7219
7386
|
if (!/\s/.test(codexBin) && /\.js$/i.test(codexBin)) {
|
|
7220
7387
|
args.unshift(codexBin);
|
|
7221
7388
|
codexBin = process.execPath;
|
|
@@ -7223,9 +7390,12 @@ async function queryCodexAppServerModels(codexHome = CODEX_DEFAULT_HOME) {
|
|
|
7223
7390
|
const useShell = /\s/.test(codexBin)
|
|
7224
7391
|
|| !codexBin.includes(sep)
|
|
7225
7392
|
|| (process.platform === 'win32' && /\.(cmd|bat)$/i.test(codexBin));
|
|
7226
|
-
const env =
|
|
7393
|
+
const env = launch?.ok
|
|
7394
|
+
? { ...applyCodexLaunchEnv(process.env, launch), NO_COLOR: '1' }
|
|
7395
|
+
: { ...process.env, CODEX_HOME: codexHome, NO_COLOR: '1' };
|
|
7227
7396
|
delete env.FORCE_COLOR;
|
|
7228
7397
|
delete env.CLICOLOR_FORCE;
|
|
7398
|
+
let apiKey = launch?.ok ? launch.apiKey : null;
|
|
7229
7399
|
|
|
7230
7400
|
return new Promise((resolve, reject) => {
|
|
7231
7401
|
// (A configured command may be a command line; a file with a space in its path is one word.)
|
|
@@ -7311,6 +7481,10 @@ async function queryCodexAppServerModels(codexHome = CODEX_DEFAULT_HOME) {
|
|
|
7311
7481
|
clientInfo: { name: 'synabun-codex-models', title: 'SynaBun Codex Models', version: '0.0.1' },
|
|
7312
7482
|
capabilities: { experimentalApi: true },
|
|
7313
7483
|
}, 15000);
|
|
7484
|
+
if (apiKey) {
|
|
7485
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'initialized' }) + '\n');
|
|
7486
|
+
try { await request('account/login/start', { type: 'apiKey', apiKey }, 15000); } finally { apiKey = null; }
|
|
7487
|
+
}
|
|
7314
7488
|
const result = await request('model/list', { includeHidden: false }, 10000);
|
|
7315
7489
|
cleanup();
|
|
7316
7490
|
resolve(Array.isArray(result?.models) ? result.models : (Array.isArray(result?.data) ? result.data : []));
|
|
@@ -7334,7 +7508,7 @@ app.get('/api/codex/models', async (req, res) => {
|
|
|
7334
7508
|
return res.json({ ok: true, source: 'cache', models: cached.models });
|
|
7335
7509
|
}
|
|
7336
7510
|
const models = mergeCodexModelContextCapabilities(
|
|
7337
|
-
await queryCodexAppServerModels(codexHome),
|
|
7511
|
+
await queryCodexAppServerModels(codexHome, _codexAccounts().launchFor(account.id)),
|
|
7338
7512
|
readCodexRuntimeModelCatalog(codexHome),
|
|
7339
7513
|
);
|
|
7340
7514
|
// The Assistant's catalog is built from the default account's list: when that list
|
|
@@ -7348,12 +7522,20 @@ app.get('/api/codex/models', async (req, res) => {
|
|
|
7348
7522
|
}
|
|
7349
7523
|
});
|
|
7350
7524
|
|
|
7351
|
-
// GET /api/codex/accounts —
|
|
7352
|
-
//
|
|
7353
|
-
//
|
|
7354
|
-
|
|
7525
|
+
// GET /api/codex/accounts — every Codex account with its state (mode, signed in,
|
|
7526
|
+
// identity: an email or a masked key, never a key) for the Codex panel's account
|
|
7527
|
+
// menu and the launch selectors (Automation Studio, Schedules). Emails are
|
|
7528
|
+
// personal data: not for invite guests. `?refresh=true` asks Codex about every
|
|
7529
|
+
// account's own store first; otherwise an old answer is asked again in the
|
|
7530
|
+
// background and announced (`codex:accounts-changed`) when it changes.
|
|
7531
|
+
app.get('/api/codex/accounts', async (req, res) => {
|
|
7532
|
+
if (isGuestRequest(req)) return res.status(403).json({ ok: false, error: 'Admin only' });
|
|
7533
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
7355
7534
|
try {
|
|
7356
|
-
|
|
7535
|
+
const accounts = _codexAccounts();
|
|
7536
|
+
if (req.query?.refresh === 'true') await accounts.refreshStatus(null, { force: true });
|
|
7537
|
+
else accounts.refreshStatus().catch(() => {});
|
|
7538
|
+
res.json({ ok: true, accounts: accounts.listForClient() });
|
|
7357
7539
|
} catch (err) {
|
|
7358
7540
|
res.json({ ok: false, error: err.message, accounts: [] });
|
|
7359
7541
|
}
|
|
@@ -7779,7 +7961,7 @@ function updateOpencodeRuntimeProfileConfig(xdgRoot, profile) {
|
|
|
7779
7961
|
return true;
|
|
7780
7962
|
}
|
|
7781
7963
|
|
|
7782
|
-
async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, keepAlive = false, mcpProfile = null } = {}) {
|
|
7964
|
+
async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, keepAlive = false, mcpProfile = null, brainCredential = null } = {}) {
|
|
7783
7965
|
if (_ocpHistoryClearInProgress) throw new Error('OpenCode history is being cleared');
|
|
7784
7966
|
const requestedProfile = normalizeMcpProfileName(mcpProfile || readActiveMcpProfile());
|
|
7785
7967
|
let entry = _ocpIsoServes.get(termId);
|
|
@@ -7804,11 +7986,6 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, k
|
|
|
7804
7986
|
entry = { termId, port, xdgRoot, mcpProfile: requestedProfile, mcpProfileRefreshFailed: false, proc: null, client: null, ready: false, starting: true, keepAlive: !!keepAlive, unsub: null, sessions: new Set(), lastUsed: Date.now() };
|
|
7805
7987
|
_ocpIsoServes.set(termId, entry);
|
|
7806
7988
|
try {
|
|
7807
|
-
const config = loadCliConfig();
|
|
7808
|
-
const cmd = config.opencode?.command || 'opencode';
|
|
7809
|
-
const shellArgs = IS_WIN
|
|
7810
|
-
? ['/d', '/s', '/c', `${cmd} serve --port ${port}`]
|
|
7811
|
-
: ['-lc', `exec ${cmd} serve --port ${port}`];
|
|
7812
7989
|
const isolatedEnv = {
|
|
7813
7990
|
...process.env,
|
|
7814
7991
|
XDG_CONFIG_HOME: xdgRoot,
|
|
@@ -7820,11 +7997,16 @@ async function ensureIsolatedServe(termId, { xdgRoot: requestedXdgRoot = null, k
|
|
|
7820
7997
|
// Keep this above the script's own deadlines while retaining a hard cap.
|
|
7821
7998
|
OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS:
|
|
7822
7999
|
process.env.OPENCODE_EXPERIMENTAL_BASH_DEFAULT_TIMEOUT_MS || '300000',
|
|
8000
|
+
// A WhatsApp conversation's brain credential: the config names it ({env:SYNABUN_BRAIN_TOKEN}), only this
|
|
8001
|
+
// serve's environment holds it (a fresh serve per brain launch: the brain stops its serve when it ends).
|
|
8002
|
+
...(brainCredential ? { SYNABUN_BRAIN_TOKEN: brainCredential } : {}),
|
|
7823
8003
|
};
|
|
7824
8004
|
delete isolatedEnv.CLAUDECODE;
|
|
7825
|
-
|
|
8005
|
+
if (!brainCredential) delete isolatedEnv.SYNABUN_BRAIN_TOKEN;
|
|
8006
|
+
const launch = opencodeServeLaunch(port, { env: isolatedEnv });
|
|
8007
|
+
const proc = spawn(launch.command, launch.args, {
|
|
7826
8008
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
7827
|
-
|
|
8009
|
+
...launch.options,
|
|
7828
8010
|
detached: false, shell: false,
|
|
7829
8011
|
});
|
|
7830
8012
|
entry.proc = proc;
|
|
@@ -8490,15 +8672,11 @@ async function ensureOpencodeServer({ allowDuringHistoryClear = false } = {}) {
|
|
|
8490
8672
|
|
|
8491
8673
|
// 2. Spawn opencode serve
|
|
8492
8674
|
try {
|
|
8493
|
-
const
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
const shellArgs = IS_WIN
|
|
8497
|
-
? ['/d', '/s', '/c', `${cmd} serve --port ${_ocpPort}`]
|
|
8498
|
-
: ['-lc', `exec ${cmd} serve --port ${_ocpPort}`];
|
|
8499
|
-
_ocpProc = spawn(DEFAULT_SHELL, shellArgs, {
|
|
8675
|
+
const launch = opencodeServeLaunch(_ocpPort, { env: { ...process.env, FORCE_COLOR: '0', OPENCODE_DISABLE_CLAUDE_CODE: '1' } });
|
|
8676
|
+
console.log(`[opencode] Spawning: ${launch.label}`);
|
|
8677
|
+
_ocpProc = spawn(launch.command, launch.args, {
|
|
8500
8678
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
8501
|
-
|
|
8679
|
+
...launch.options,
|
|
8502
8680
|
detached: false,
|
|
8503
8681
|
shell: false,
|
|
8504
8682
|
});
|
|
@@ -8786,7 +8964,7 @@ app.post('/api/ollama/configure', async (req, res) => {
|
|
|
8786
8964
|
} catch {}
|
|
8787
8965
|
if (needsInstall) {
|
|
8788
8966
|
console.log('[ollama] Installing @ai-sdk/openai-compatible…');
|
|
8789
|
-
execSync('npm install @ai-sdk/openai-compatible --save', { cwd: configDir, timeout: 60000, stdio: 'pipe' });
|
|
8967
|
+
execSync('npm install @ai-sdk/openai-compatible --save', { cwd: configDir, timeout: 60000, stdio: 'pipe', env: withPackagedPathLast(process.env) });
|
|
8790
8968
|
console.log('[ollama] Package installed');
|
|
8791
8969
|
}
|
|
8792
8970
|
|
|
@@ -8885,6 +9063,7 @@ app.post('/api/sidepanel/session-title', async (req, res) => {
|
|
|
8885
9063
|
}
|
|
8886
9064
|
const result = await generateSessionTitle(input, {
|
|
8887
9065
|
claudeExecutable: getClaudeSdkExecutable,
|
|
9066
|
+
childEnv: claudeChildEnv,
|
|
8888
9067
|
execFile: execFileAsync,
|
|
8889
9068
|
getCodexBin,
|
|
8890
9069
|
getCodexAccountHome,
|
|
@@ -9516,6 +9695,7 @@ const _claudeBridgeReady = (async () => {
|
|
|
9516
9695
|
try {
|
|
9517
9696
|
_claudeBridge = await import('./lib/claude-agent-bridge.js');
|
|
9518
9697
|
_claudeBridge.configureClaudeBridge({
|
|
9698
|
+
accountRateLimit: (id, info) => _assistantAccountUsage?.note('claude-code', id, info),
|
|
9519
9699
|
// The greeting a panel conversation opened with, as the one note its model gets (lib/greeting/).
|
|
9520
9700
|
greetingNote: (id, opts) => _greeting?.consumeNote(id, opts) || '',
|
|
9521
9701
|
// A prompt typed in the panel: when the person was last here (a temporary chat is named to nobody).
|
|
@@ -9556,9 +9736,16 @@ const _claudeBridgeReady = (async () => {
|
|
|
9556
9736
|
// a session without one is refused with the install hint. Lazy, so the
|
|
9557
9737
|
// (memoized) resolution happens on demand, not at boot.
|
|
9558
9738
|
claudeExecutable: () => getClaudeSdkExecutable(),
|
|
9739
|
+
// On Windows, the Git Bash every session's Claude Code gets (lib/git-bash/): SynaBun's copy where the PC has
|
|
9740
|
+
// none, so the Bash tool exists and the hooks run in bash. Panel tabs and the Assistant's brain.
|
|
9741
|
+
childEnv: (env) => claudeChildEnv(env),
|
|
9559
9742
|
// Sidepanel tabs can run under another Claude account (its own config
|
|
9560
9743
|
// directory). The default account adds nothing to the environment.
|
|
9561
9744
|
claudeAccountEnv: (accountId) => _panelClaudeAccounts().envFor(accountId),
|
|
9745
|
+
// How a session runs under an account: inherited credentials removed, the
|
|
9746
|
+
// account's directory and, in API-key mode, its key added (launchFor() in
|
|
9747
|
+
// lib/claude-accounts.js). Panel tabs and the Assistant's brain.
|
|
9748
|
+
claudeAccountLaunch: (accountId) => _panelClaudeAccounts().launchFor(accountId),
|
|
9562
9749
|
// This server has the session routes (rename, tag, fork, delete, subagent
|
|
9563
9750
|
// transcripts): the panel offers those actions only when told so.
|
|
9564
9751
|
sessionOps: true,
|
|
@@ -9591,14 +9778,42 @@ const _claudeBridgeReady = (async () => {
|
|
|
9591
9778
|
}
|
|
9592
9779
|
})();
|
|
9593
9780
|
|
|
9594
|
-
// The Claude accounts registry
|
|
9595
|
-
//
|
|
9781
|
+
// The Claude accounts registry: one instance for the sidepanel, the Assistant
|
|
9782
|
+
// (brain, workers, catalog) and every route, so one account state is shown
|
|
9783
|
+
// everywhere (lib/claude-accounts.js describe()). Its subscription half is
|
|
9784
|
+
// Claude Code's own `auth status --json` for each account; a change of any
|
|
9785
|
+
// account reaches every window as `claude:accounts-changed`.
|
|
9596
9786
|
let _panelClaudeAccountsInstance = null;
|
|
9597
9787
|
function _panelClaudeAccounts() {
|
|
9598
|
-
if (!_panelClaudeAccountsInstance)
|
|
9788
|
+
if (!_panelClaudeAccountsInstance) {
|
|
9789
|
+
_panelClaudeAccountsInstance = createPanelClaudeAccounts({
|
|
9790
|
+
dataHome: DATA_HOME, homeDir: getHomePath(),
|
|
9791
|
+
readNativeStatus: (accountId) => panelClaudeAuth().nativeStatus(accountId),
|
|
9792
|
+
log: (tag, message) => console.log(`[${tag}]`, message),
|
|
9793
|
+
});
|
|
9794
|
+
_panelClaudeAccountsInstance.subscribe((event) => {
|
|
9795
|
+
try {
|
|
9796
|
+
broadcastSync({ type: 'claude:accounts-changed', accountId: event.accountId || null, removed: !!event.removed, created: !!event.created });
|
|
9797
|
+
// The Assistant's account list and catalog listen to their own event.
|
|
9798
|
+
broadcastSync({ type: 'assistant:accounts-changed', provider: 'claude-code', accountId: event.accountId || null, removed: !!event.removed });
|
|
9799
|
+
_assistantCatalog?.invalidate?.();
|
|
9800
|
+
} catch {}
|
|
9801
|
+
});
|
|
9802
|
+
}
|
|
9599
9803
|
return _panelClaudeAccountsInstance;
|
|
9600
9804
|
}
|
|
9601
9805
|
|
|
9806
|
+
// Why a Claude account cannot be removed now ('' when it can): a live session
|
|
9807
|
+
// (a panel tab, the Assistant's brain), a dispatched run, or the Assistant's
|
|
9808
|
+
// choice of worker account names it.
|
|
9809
|
+
function claudeAccountInUse(accountId) {
|
|
9810
|
+
try { if (_claudeBridge?.accountInUse?.(accountId)) return 'A Claude tab or the Assistant is running under this account. End that session first.'; } catch {}
|
|
9811
|
+
try { if (_assistantDispatcher?.accountInUse?.('claude-code', accountId)) return 'Dispatched runs are using this account. Stop them first.'; } catch {}
|
|
9812
|
+
try { if (_assistantRuntime?.accountInUse?.('claude-code', accountId)) return 'An Assistant session uses this account. Close it first.'; } catch {}
|
|
9813
|
+
try { if (_assistantClaudeWorkerAccount?.() === accountId) return 'This is the Assistant\'s Claude worker account. Choose another one first.'; } catch {}
|
|
9814
|
+
return '';
|
|
9815
|
+
}
|
|
9816
|
+
|
|
9602
9817
|
// The config directory of the Claude account a sidepanel request names: '' for
|
|
9603
9818
|
// the default account. An account that is no longer set up is an error (404,
|
|
9604
9819
|
// `account_unavailable`), never the default account's folder in its place.
|
|
@@ -9611,14 +9826,159 @@ function panelAccountHome(value) {
|
|
|
9611
9826
|
return home;
|
|
9612
9827
|
}
|
|
9613
9828
|
|
|
9614
|
-
// GET /api/claude/accounts —
|
|
9615
|
-
//
|
|
9616
|
-
|
|
9829
|
+
// GET /api/claude/accounts — every Claude account with its state (mode,
|
|
9830
|
+
// signed in, identity: an email or a masked key, never a key). Emails are
|
|
9831
|
+
// personal data: not for invite guests. `?refresh=true` asks Claude Code about
|
|
9832
|
+
// every account's subscription first; otherwise an answer older than a few
|
|
9833
|
+
// minutes is asked again in the background and announced when it changes.
|
|
9834
|
+
app.get('/api/claude/accounts', async (req, res) => {
|
|
9617
9835
|
if (isGuestRequest(req)) return res.status(403).json({ ok: false, error: 'Admin only' });
|
|
9618
|
-
|
|
9836
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9837
|
+
try {
|
|
9838
|
+
const accounts = _panelClaudeAccounts();
|
|
9839
|
+
if (req.query.refresh === 'true') await accounts.refreshStatus(null, { force: true });
|
|
9840
|
+
else accounts.refreshStatus().catch(() => {});
|
|
9841
|
+
res.json({ ok: true, accounts: accounts.listForClient() });
|
|
9842
|
+
}
|
|
9619
9843
|
catch (err) { res.status(500).json({ ok: false, error: err.message }); }
|
|
9620
9844
|
});
|
|
9621
9845
|
|
|
9846
|
+
let _panelClaudeAuthInstance = null;
|
|
9847
|
+
const panelAuthOwnerGuard = ownerRouteGuard({ isGuestRequest, port: PORT, allowedOrigins: publicWebOrigins, refuseCrossSite: true });
|
|
9848
|
+
function panelClaudeAuth() {
|
|
9849
|
+
if (!_panelClaudeAuthInstance) {
|
|
9850
|
+
_panelClaudeAuthInstance = createClaudePanelAuth({ accounts: _panelClaudeAccounts(), executable: getClaudeSdkExecutable, env: () => (withPathEnv(process.env, getAugmentedPath())), changeCredentials: (id, change) => _claudeBridge?.withPanelAccountAuthChange ? _claudeBridge.withPanelAccountAuthChange(id, change) : change() });
|
|
9851
|
+
process.once('exit', () => _panelClaudeAuthInstance?.close());
|
|
9852
|
+
}
|
|
9853
|
+
return _panelClaudeAuthInstance;
|
|
9854
|
+
}
|
|
9855
|
+
|
|
9856
|
+
// Admin-only account auth; OAuth stays native and pasted keys are profile-scoped.
|
|
9857
|
+
app.get('/api/claude/accounts/:id/auth', panelAuthOwnerGuard, async (req, res) => {
|
|
9858
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9859
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9860
|
+
try {
|
|
9861
|
+
panelAccountHome(req.params.id);
|
|
9862
|
+
const result = req.query.refresh === 'true' ? await panelClaudeAuth().read(req.params.id) : { state: panelClaudeAuth().snapshot(req.params.id) };
|
|
9863
|
+
res.json(result);
|
|
9864
|
+
} catch (error) { res.status(error.status || 500).json({ error: error.message }); }
|
|
9865
|
+
});
|
|
9866
|
+
app.post('/api/claude/accounts/:id/auth/:action', panelAuthOwnerGuard, async (req, res) => {
|
|
9867
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9868
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9869
|
+
try {
|
|
9870
|
+
panelAccountHome(req.params.id);
|
|
9871
|
+
const auth = panelClaudeAuth();
|
|
9872
|
+
if (req.params.action === 'start') return res.json({ state: auth.start(req.params.id) });
|
|
9873
|
+
if (req.params.action === 'cancel') return res.json({ state: auth.cancel(req.params.id, req.body?.operationId) });
|
|
9874
|
+
if (req.params.action === 'code') return res.json({ state: auth.submitCode(req.params.id, req.body?.operationId, req.body?.code) });
|
|
9875
|
+
if (req.params.action === 'logout') {
|
|
9876
|
+
// `credential`: 'apiKey', 'subscription', or both when left out (the older /logout).
|
|
9877
|
+
const credential = typeof req.body?.credential === 'string' ? req.body.credential : 'all';
|
|
9878
|
+
await auth.signOut(req.params.id, credential);
|
|
9879
|
+
return res.json({ ok: true, account: _panelClaudeAccounts().describeForClient(req.params.id) });
|
|
9880
|
+
}
|
|
9881
|
+
res.status(404).json({ error: 'Unknown authentication action' });
|
|
9882
|
+
} catch (error) { res.status(error.status || 500).json({ error: error.message }); }
|
|
9883
|
+
});
|
|
9884
|
+
|
|
9885
|
+
registerProviderPanelApiKeyAuthRoutes(app, {
|
|
9886
|
+
guard: panelAuthOwnerGuard,
|
|
9887
|
+
claude: (id, key) => panelClaudeAuth().signInApiKey(id, key),
|
|
9888
|
+
// A Codex key is checked with OpenAI and kept by SynaBun for that account (lib/codex-account-auth.js);
|
|
9889
|
+
// the account's idle Codex processes restart on it. Codex's own store (its ChatGPT login) is not touched.
|
|
9890
|
+
codex: (id, key) => codexAccountAuth().signInApiKey(id, key),
|
|
9891
|
+
});
|
|
9892
|
+
|
|
9893
|
+
// Codex accounts: the same model and routes as Claude's (lib/codex-accounts.js describe(), launchFor()).
|
|
9894
|
+
// Everything but the list carries the owner guard; a credential or mode change restarts the
|
|
9895
|
+
// account's idle panel processes and is refused while one of them is mid-turn.
|
|
9896
|
+
app.post('/api/codex/accounts', panelAuthOwnerGuard, (req, res) => {
|
|
9897
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9898
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9899
|
+
try {
|
|
9900
|
+
const created = _codexAccounts().create({ label: req.body?.label });
|
|
9901
|
+
res.json({ account: _codexAccounts().describeForClient(created.id) });
|
|
9902
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9903
|
+
});
|
|
9904
|
+
// Rename (`label`) and/or choose what the account's sessions use (`mode`: 'subscription' (ChatGPT) | 'apiKey').
|
|
9905
|
+
app.patch('/api/codex/accounts/:id', panelAuthOwnerGuard, async (req, res) => {
|
|
9906
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9907
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9908
|
+
try {
|
|
9909
|
+
const accounts = _codexAccounts();
|
|
9910
|
+
if (accounts.describe(req.params.id)?.id !== req.params.id) return res.status(404).json({ code: 'ACCOUNT_NOT_FOUND', error: 'That Codex account is no longer set up in SynaBun.' });
|
|
9911
|
+
const { label, mode } = req.body || {};
|
|
9912
|
+
if (label === undefined && mode === undefined) return res.status(400).json({ code: 'NOTHING_TO_CHANGE', error: 'Send a label or a mode.' });
|
|
9913
|
+
if (label !== undefined) accounts.rename(req.params.id, label);
|
|
9914
|
+
if (mode !== undefined) await codexAccountAuth().setMode(req.params.id, mode);
|
|
9915
|
+
res.json({ ok: true, account: accounts.describeForClient(req.params.id) });
|
|
9916
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9917
|
+
});
|
|
9918
|
+
// Remove an added account and its folder. Default stays; an account something uses stays.
|
|
9919
|
+
app.delete('/api/codex/accounts/:id', panelAuthOwnerGuard, (req, res) => {
|
|
9920
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9921
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9922
|
+
try { res.json(_codexAccounts().remove(req.params.id, { inUse: (id) => codexAccountInUse(id) })); }
|
|
9923
|
+
catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9924
|
+
});
|
|
9925
|
+
// `?refresh=true` asks Codex what the account's own store holds first.
|
|
9926
|
+
app.get('/api/codex/accounts/:id/auth', panelAuthOwnerGuard, async (req, res) => {
|
|
9927
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9928
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9929
|
+
try {
|
|
9930
|
+
if (_codexAccounts().describe(req.params.id)?.id !== req.params.id) return res.status(404).json({ code: 'ACCOUNT_NOT_FOUND', error: 'That Codex account is no longer set up in SynaBun.' });
|
|
9931
|
+
if (req.query.refresh === 'true') return res.json(await codexAccountAuth().read(req.params.id));
|
|
9932
|
+
res.json({ accountId: req.params.id, account: _codexAccounts().describeForClient(req.params.id) });
|
|
9933
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9934
|
+
});
|
|
9935
|
+
// `credential`: 'apiKey' (the account's key), 'subscription' (Codex's own logout in its home), or both when left out.
|
|
9936
|
+
app.post('/api/codex/accounts/:id/auth/logout', panelAuthOwnerGuard, async (req, res) => {
|
|
9937
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9938
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9939
|
+
try {
|
|
9940
|
+
if (_codexAccounts().describe(req.params.id)?.id !== req.params.id) return res.status(404).json({ code: 'ACCOUNT_NOT_FOUND', error: 'That Codex account is no longer set up in SynaBun.' });
|
|
9941
|
+
const credential = typeof req.body?.credential === 'string' ? req.body.credential : 'all';
|
|
9942
|
+
const account = await codexAccountAuth().signOut(req.params.id, credential);
|
|
9943
|
+
res.json({ ok: true, account });
|
|
9944
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9945
|
+
});
|
|
9946
|
+
|
|
9947
|
+
app.post('/api/claude/accounts', panelAuthOwnerGuard, (req, res) => {
|
|
9948
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9949
|
+
try {
|
|
9950
|
+
const created = _panelClaudeAccounts().create({ label: req.body?.label });
|
|
9951
|
+
const row = _panelClaudeAccounts().listForClient().find(a => a.id === created.id);
|
|
9952
|
+
res.json({ account: row });
|
|
9953
|
+
} catch (error) { res.status(error.status || 500).json({ error: error.message }); }
|
|
9954
|
+
});
|
|
9955
|
+
|
|
9956
|
+
// Rename an account (`label`) or choose what its sessions use (`mode`:
|
|
9957
|
+
// 'subscription' | 'apiKey'; the chosen credential must be there). A mode
|
|
9958
|
+
// change ends the account's idle sessions so the next message starts under it.
|
|
9959
|
+
app.patch('/api/claude/accounts/:id', panelAuthOwnerGuard, async (req, res) => {
|
|
9960
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9961
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9962
|
+
try {
|
|
9963
|
+
const accounts = _panelClaudeAccounts();
|
|
9964
|
+
if (!accounts.describe(req.params.id) || accounts.describe(req.params.id).id !== req.params.id) return res.status(404).json({ code: 'ACCOUNT_NOT_FOUND', error: 'That Claude account is no longer set up in SynaBun.' });
|
|
9965
|
+
const { label, mode } = req.body || {};
|
|
9966
|
+
if (label === undefined && mode === undefined) return res.status(400).json({ code: 'NOTHING_TO_CHANGE', error: 'Send a label or a mode.' });
|
|
9967
|
+
if (label !== undefined) accounts.rename(req.params.id, label);
|
|
9968
|
+
if (mode !== undefined) await panelClaudeAuth().setMode(req.params.id, mode);
|
|
9969
|
+
res.json({ ok: true, account: accounts.describeForClient(req.params.id) });
|
|
9970
|
+
} catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9971
|
+
});
|
|
9972
|
+
|
|
9973
|
+
// Remove an added account and its config directory. Default stays; an account
|
|
9974
|
+
// a live session, a dispatched run or the Assistant's worker choice uses stays.
|
|
9975
|
+
app.delete('/api/claude/accounts/:id', panelAuthOwnerGuard, (req, res) => {
|
|
9976
|
+
if (isGuestRequest(req)) return res.status(403).json({ error: 'Admin only' });
|
|
9977
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
9978
|
+
try { res.json(_panelClaudeAccounts().remove(req.params.id, { inUse: claudeAccountInUse })); }
|
|
9979
|
+
catch (error) { res.status(error.status || 500).json({ code: error.code || null, error: error.message }); }
|
|
9980
|
+
});
|
|
9981
|
+
|
|
9622
9982
|
function _readClaudeSkinConfig() {
|
|
9623
9983
|
try {
|
|
9624
9984
|
const cfg = JSON.parse(readFileSync(resolve(DATA_HOME, 'data', 'cli-config.json'), 'utf-8'));
|
|
@@ -10806,8 +11166,9 @@ app.post('/api/youtube/download', async (req, res) => {
|
|
|
10806
11166
|
|
|
10807
11167
|
// A GUI-launched NI may not inherit the user's shell PATH, so yt-dlp can't find
|
|
10808
11168
|
// node/deno/ffmpeg. Prepend the running node's dir + the common Homebrew/local bins.
|
|
11169
|
+
// A packaged application's own runtime still goes last (withPackagedPathLast): the user's node first when there is one.
|
|
10809
11170
|
const extraPath = [dirname(process.execPath), '/opt/homebrew/bin', '/usr/local/bin', join(os.homedir(), '.local/bin')].join(delimiter);
|
|
10810
|
-
const childEnv =
|
|
11171
|
+
const childEnv = withPackagedPathLast(withPathEnv(process.env, `${extraPath}${delimiter}${pathEnvValue(process.env)}`));
|
|
10811
11172
|
|
|
10812
11173
|
const runYtDlp = (finalArgs) => new Promise((done) => {
|
|
10813
11174
|
const child = spawn(yd.cmd, finalArgs, { windowsHide: true, env: childEnv });
|
|
@@ -12767,8 +13128,10 @@ app.get('/api/loop/active', (req, res) => {
|
|
|
12767
13128
|
if (data?.active) {
|
|
12768
13129
|
const elapsed = Date.now() - new Date(data.startedAt).getTime();
|
|
12769
13130
|
const maxMs = ((data.maxMinutes || 30) + 5) * 60 * 1000;
|
|
12770
|
-
// Stale loop — past time cap + grace, delete it
|
|
12771
|
-
|
|
13131
|
+
// Stale loop — past time cap + grace, delete it. Not a dispatched worker's (runMode
|
|
13132
|
+
// 'task'): its maxMinutes is work time, which the dispatcher enforces, and a warm run
|
|
13133
|
+
// idles for a follow-up far past it; a dead one goes with the check below.
|
|
13134
|
+
if (data.runMode !== 'task' && elapsed >= maxMs) {
|
|
12772
13135
|
try { unlinkSync(filePath); } catch { /* ok */ }
|
|
12773
13136
|
continue;
|
|
12774
13137
|
}
|
|
@@ -12780,6 +13143,10 @@ app.get('/api/loop/active', (req, res) => {
|
|
|
12780
13143
|
continue;
|
|
12781
13144
|
}
|
|
12782
13145
|
}
|
|
13146
|
+
// A dispatched worker's time left is its work time left (the dispatcher's work clock); null
|
|
13147
|
+
// while the dispatcher does not know the run (right after a restart): never the wall clock.
|
|
13148
|
+
const isTaskRun = data.runMode === 'task';
|
|
13149
|
+
const work = isTaskRun && data.runId ? (_assistantDispatcher?.peek?.(data.runId) || null) : null;
|
|
12783
13150
|
activeLoops.push({
|
|
12784
13151
|
sessionId: f.replace('.json', ''),
|
|
12785
13152
|
task: data.task,
|
|
@@ -12788,7 +13155,9 @@ app.get('/api/loop/active', (req, res) => {
|
|
|
12788
13155
|
totalIterations: data.totalIterations || 10,
|
|
12789
13156
|
maxMinutes: data.maxMinutes || 30,
|
|
12790
13157
|
elapsedMinutes: Math.round(elapsed / 60000),
|
|
12791
|
-
remainingMinutes:
|
|
13158
|
+
remainingMinutes: work && Number.isFinite(work.workLeftMs)
|
|
13159
|
+
? Math.round(work.workLeftMs / 60000)
|
|
13160
|
+
: isTaskRun ? null : Math.max(0, Math.round(((data.maxMinutes || 30) * 60 * 1000 - elapsed) / 60000)),
|
|
12792
13161
|
startedAt: data.startedAt,
|
|
12793
13162
|
lastIterationAt: data.lastIterationAt,
|
|
12794
13163
|
browserSessionId: data.browserSessionId || null,
|
|
@@ -15454,7 +15823,7 @@ function spawnAgentProcess(agent, prompt, extraEnv = {}) {
|
|
|
15454
15823
|
|
|
15455
15824
|
const child = spawn(shellCommand(claudeBin, AGENT_IS_WIN), args, {
|
|
15456
15825
|
cwd: agent.cwd,
|
|
15457
|
-
env: { ...cleanEnv, FORCE_COLOR: '0', NO_COLOR: '1', TERM: 'dumb', ...extraEnv },
|
|
15826
|
+
env: claudeChildEnv({ ...cleanEnv, FORCE_COLOR: '0', NO_COLOR: '1', TERM: 'dumb', ...extraEnv }),
|
|
15458
15827
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
15459
15828
|
shell: AGENT_IS_WIN,
|
|
15460
15829
|
windowsHide: true,
|
|
@@ -16129,6 +16498,18 @@ async function checkSynabunUpdate() {
|
|
|
16129
16498
|
// stable releases with a suffix.
|
|
16130
16499
|
const installedChannel = synabunReleaseChannel(current);
|
|
16131
16500
|
|
|
16501
|
+
// The desktop app updates itself (packaging/desktop: its window's update function, fed by the GitHub
|
|
16502
|
+
// releases' desktop files). Nothing here offers it an npm or GitHub update; there is nothing to check.
|
|
16503
|
+
if (PACKAGED_RUNTIME?.shell) {
|
|
16504
|
+
_synabunUpdateCache = {
|
|
16505
|
+
..._synabunUpdateCache,
|
|
16506
|
+
current, installedChannel, latest: null, npmUpdateAvailable: false, gitUpdateAvailable: false, updateAvailable: false,
|
|
16507
|
+
source: null, installPlan: null, checkedAt: new Date().toISOString(), npmError: null, gitError: null, desktopManaged: true,
|
|
16508
|
+
};
|
|
16509
|
+
console.log(` Updates: from the SynaBun app (v${current})`);
|
|
16510
|
+
return;
|
|
16511
|
+
}
|
|
16512
|
+
|
|
16132
16513
|
const fetchJsonWithTimeout = async (url, ms = 5000) => {
|
|
16133
16514
|
const controller = new AbortController();
|
|
16134
16515
|
const timeout = setTimeout(() => controller.abort(), ms);
|
|
@@ -16224,6 +16605,72 @@ app.get('/api/system/version', async (req, res) => {
|
|
|
16224
16605
|
res.json(_synabunUpdateCache);
|
|
16225
16606
|
});
|
|
16226
16607
|
|
|
16608
|
+
// GET /api/system/about — Settings → About (lib/system-about.js): the app, the runtime, the project's links and
|
|
16609
|
+
// every dependency of the product's packages with the version installed, read from the files on disk on the first
|
|
16610
|
+
// request and kept for the life of the process. No environment, no token, no setting. Owner only (/api/system is
|
|
16611
|
+
// in ADMIN_ONLY_PREFIXES).
|
|
16612
|
+
const systemAbout = createSystemAbout({
|
|
16613
|
+
packageRoot: PACKAGE_ROOT,
|
|
16614
|
+
dataHome: DATA_HOME,
|
|
16615
|
+
port: PORT,
|
|
16616
|
+
install: PACKAGED_RUNTIME?.shell ? 'desktop' : INSTALL_KIND,
|
|
16617
|
+
// The WhatsApp connector is installed on demand, into the data folder (lib/whatsapp/paths.js runtimeDir).
|
|
16618
|
+
installDirs: { 'whatsapp-connector': [join(DATA_HOME, 'runtime', 'whatsapp')] },
|
|
16619
|
+
vendorDir: join(__dirname, 'public', 'vendor'),
|
|
16620
|
+
});
|
|
16621
|
+
app.get('/api/system/about', (req, res) => {
|
|
16622
|
+
res.set('Cache-Control', 'no-store');
|
|
16623
|
+
try {
|
|
16624
|
+
res.json(systemAbout.get());
|
|
16625
|
+
} catch (err) {
|
|
16626
|
+
res.status(500).json({ error: 'about_failed', detail: String(err?.message || err).slice(0, 200) });
|
|
16627
|
+
}
|
|
16628
|
+
});
|
|
16629
|
+
|
|
16630
|
+
// GET /api/system/activity — whether anything runs that a restart would end. The
|
|
16631
|
+
// desktop app installs a downloaded update only while this says idle
|
|
16632
|
+
// (packaging/desktop/src/updater/controller.mjs); lib/system-activity.js adds the
|
|
16633
|
+
// counters up, and one that throws makes the answer "not idle". Terminals are not
|
|
16634
|
+
// counted: nothing here can tell a command that runs from a shell that waits.
|
|
16635
|
+
function systemActivitySources() {
|
|
16636
|
+
const nativeRuns = () => _nativeLoopRuntime?.list?.({ activeOnly: true }) || [];
|
|
16637
|
+
return {
|
|
16638
|
+
assistant: () => {
|
|
16639
|
+
const runtime = _assistantRuntime;
|
|
16640
|
+
const sessions = runtime?._internals?.sessions;
|
|
16641
|
+
const brains = sessions ? countWhere(sessions.keys(), (id) => runtime.isBusy(id)) : 0;
|
|
16642
|
+
const runs = _assistantDispatcher ? _assistantDispatcher.list({ activeOnly: true, limit: 500 }).length : 0;
|
|
16643
|
+
const agents = countWhere(agentRegistry, (agent) => agent?.status === 'running');
|
|
16644
|
+
return brains + runs + agents;
|
|
16645
|
+
},
|
|
16646
|
+
'claude-panel': () => _claudeBridge?.panelSessionsWorking?.() || 0,
|
|
16647
|
+
'codex-panel': () => countWhere(_codexLiveConnections, (connection) => !!connection?.busy?.())
|
|
16648
|
+
+ countWhere(_codexOrphanedProcs, (orphan) => orphan?.isIdle?.() !== true),
|
|
16649
|
+
'opencode-panel': () => {
|
|
16650
|
+
const blockers = getOpenCodeHistoryBlockers();
|
|
16651
|
+
return blockers.activeTurns + blockers.startingTurns;
|
|
16652
|
+
},
|
|
16653
|
+
'claude-cli': () => recentCliSessions(sessionRegistry),
|
|
16654
|
+
loop: () => {
|
|
16655
|
+
// A dispatched worker also runs in the native runtime: it is counted under assistant.
|
|
16656
|
+
const native = countWhere(nativeRuns(), (run) => run?.runMode !== 'task' && run?.source !== 'assistant');
|
|
16657
|
+
const files = countWhere(readLoopStateFiles(), ({ data }) => !!data?.active && (!data.terminalSessionId || isLoopRuntimeAlive(data)));
|
|
16658
|
+
const terminals = countWhere(terminalSessions, (session) => !!session?._loopTerminalSessionId);
|
|
16659
|
+
return Math.max(native + terminals, files);
|
|
16660
|
+
},
|
|
16661
|
+
schedule: () => _scheduleQueue.length + (_scheduleQueueRunning ? 1 : 0),
|
|
16662
|
+
backup: () => (_backupOperationActive ? 1 : 0) + (_autoBackupPromise ? 1 : 0),
|
|
16663
|
+
maintenance: () => (_reindexJob?.running ? 1 : 0) + (_indexingJob && !_indexingJob.done ? 1 : 0)
|
|
16664
|
+
+ countWhere(Object.values(cliInstaller.jobs()), (job) => job?.state === 'running')
|
|
16665
|
+
+ (_projectStorageClearInProgress ? 1 : 0) + (_ocpHistoryClearInProgress ? 1 : 0),
|
|
16666
|
+
};
|
|
16667
|
+
}
|
|
16668
|
+
|
|
16669
|
+
app.get('/api/system/activity', (req, res) => {
|
|
16670
|
+
res.set('Cache-Control', 'no-store');
|
|
16671
|
+
res.json(collectActivity(systemActivitySources(), { log: (line) => console.warn(`[activity] ${line}`) }));
|
|
16672
|
+
});
|
|
16673
|
+
|
|
16227
16674
|
// ═══════════════════════════════════════════
|
|
16228
16675
|
// EXTERNAL CLI TOOL VERSION CHECK
|
|
16229
16676
|
// ═══════════════════════════════════════════
|
|
@@ -16236,6 +16683,7 @@ const TOOL_DEFS = [
|
|
|
16236
16683
|
];
|
|
16237
16684
|
|
|
16238
16685
|
let _toolVersionCache = { checkedAt: null, tools: {} };
|
|
16686
|
+
let _toolVersionGeneration = 0;
|
|
16239
16687
|
|
|
16240
16688
|
// Backwards-compat helper — returns a "MAJOR.MINOR.PATCH" string (no pre).
|
|
16241
16689
|
// Kept because legacy callers expect a string for display purposes only.
|
|
@@ -16255,35 +16703,7 @@ function parseVersion(raw) {
|
|
|
16255
16703
|
// parseSemver / compareSemver / semverNewer live in lib/synabun-version.js, with
|
|
16256
16704
|
// the rules that order SynaBun's own date-style and semver-era releases.
|
|
16257
16705
|
|
|
16258
|
-
|
|
16259
|
-
try {
|
|
16260
|
-
const isWin = process.platform === 'win32';
|
|
16261
|
-
const finder = isWin ? `where ${cmd}` : `command -v ${cmd}`;
|
|
16262
|
-
const opts = { encoding: 'utf-8', timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'], env };
|
|
16263
|
-
if (!isWin) opts.shell = '/bin/sh';
|
|
16264
|
-
const raw = execSync(finder, opts).trim();
|
|
16265
|
-
const first = raw.split(/\r?\n/)[0] || null;
|
|
16266
|
-
if (!first) return null;
|
|
16267
|
-
// Follow symlinks so install-source detection sees the real install dir
|
|
16268
|
-
// (e.g. /usr/local/bin/claude → /usr/local/lib/node_modules/.../bin/claude).
|
|
16269
|
-
try { return realpathSync(first); } catch { return first; }
|
|
16270
|
-
} catch { return null; }
|
|
16271
|
-
}
|
|
16272
|
-
|
|
16273
|
-
// Async twin of resolveBinPath — version checks run these probes for every
|
|
16274
|
-
// tool; the sync variant blocks the event loop for the full subprocess time.
|
|
16275
|
-
async function resolveBinPathAsync(cmd, env) {
|
|
16276
|
-
try {
|
|
16277
|
-
const isWin = process.platform === 'win32';
|
|
16278
|
-
const finder = isWin ? `where ${cmd}` : `command -v ${cmd}`;
|
|
16279
|
-
const opts = { encoding: 'utf-8', timeout: 2000, env, windowsHide: true };
|
|
16280
|
-
if (!isWin) opts.shell = '/bin/sh';
|
|
16281
|
-
const { stdout } = await execAsync(finder, opts);
|
|
16282
|
-
const first = (stdout || '').trim().split(/\r?\n/)[0] || null;
|
|
16283
|
-
if (!first) return null;
|
|
16284
|
-
try { return realpathSync(first); } catch { return first; }
|
|
16285
|
-
} catch { return null; }
|
|
16286
|
-
}
|
|
16706
|
+
// Where each CLI is: lib/cli-resolver.js (cliResolver, near getClaudeBin).
|
|
16287
16707
|
|
|
16288
16708
|
// detectInstallSource / buildUpdateCommand / NPM_PKGS now live in
|
|
16289
16709
|
// ./lib/cli-update-planner.js so they can be unit tested away from this file.
|
|
@@ -16340,13 +16760,6 @@ function isInsideSynabun(binPath) {
|
|
|
16340
16760
|
}
|
|
16341
16761
|
}
|
|
16342
16762
|
|
|
16343
|
-
// Versions of @anthropic-ai/claude-code that SynaBun has historically shipped
|
|
16344
|
-
// as a bundled dependency. If a `claude --version` probe returns one of these,
|
|
16345
|
-
// it's almost certainly the stale bundle (a real user-global install would be
|
|
16346
|
-
// the current 2.1.116+). Reject hard regardless of where the binary path looks
|
|
16347
|
-
// to come from. Add new versions here if SynaBun ever ships claude-code again.
|
|
16348
|
-
const KNOWN_STALE_CLAUDE_VERSIONS = new Set(['2.1.71', '2.1.89', '2.1.91']);
|
|
16349
|
-
|
|
16350
16763
|
// Remove any external tool that ended up in our own node_modules: Claude Code,
|
|
16351
16764
|
// Codex, OpenCode and Gemini CLI are installed by the user, never by SynaBun,
|
|
16352
16765
|
// and the agent SDKs are installed without the executables they would carry
|
|
@@ -16367,52 +16780,25 @@ function pruneBundledCliTools() {
|
|
|
16367
16780
|
}
|
|
16368
16781
|
}
|
|
16369
16782
|
|
|
16370
|
-
async function checkToolVersions() {
|
|
16371
|
-
|
|
16372
|
-
//
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16377
|
-
|
|
16378
|
-
const
|
|
16379
|
-
|
|
16380
|
-
|
|
16381
|
-
//
|
|
16382
|
-
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
|
|
16386
|
-
|
|
16387
|
-
|
|
16388
|
-
|
|
16389
|
-
const isStaleBundle = isInsideSynabun(probedBinPath);
|
|
16390
|
-
|
|
16391
|
-
let installed = null;
|
|
16392
|
-
if (!isStaleBundle) {
|
|
16393
|
-
try {
|
|
16394
|
-
const { stdout } = await execAsync(`${tool.cmd} ${tool.versionArg}`, execOpts);
|
|
16395
|
-
installed = parseVersion(stdout.trim());
|
|
16396
|
-
} catch { /* not installed or errored */ }
|
|
16397
|
-
}
|
|
16398
|
-
|
|
16399
|
-
// Nuclear safety net for the recurring claude phantom-version bug:
|
|
16400
|
-
// only reject when binary path detection failed (truly phantom). If we
|
|
16401
|
-
// resolved a path AND it's outside SynaBun's own dirs, trust the version
|
|
16402
|
-
// — Anthropic ships these versions in the native installer too, so a
|
|
16403
|
-
// version-only blacklist hits legit installs at ~/.local/bin/claude.exe.
|
|
16404
|
-
if (
|
|
16405
|
-
tool.key === 'claude-code' &&
|
|
16406
|
-
installed &&
|
|
16407
|
-
KNOWN_STALE_CLAUDE_VERSIONS.has(installed) &&
|
|
16408
|
-
!probedBinPath
|
|
16409
|
-
) {
|
|
16410
|
-
console.warn(
|
|
16411
|
-
` [version-guard] Rejected claude v${installed} from (unknown path)` +
|
|
16412
|
-
` — matches known-stale SynaBun bundle and no binary path could be resolved.`
|
|
16413
|
-
);
|
|
16414
|
-
installed = null;
|
|
16415
|
-
}
|
|
16783
|
+
async function checkToolVersions({ force = false } = {}) {
|
|
16784
|
+
const generation = ++_toolVersionGeneration;
|
|
16785
|
+
// Every CLI exactly as the panels and launchers find it (lib/cli-resolver.js):
|
|
16786
|
+
// the path in Settings first, then PATH with the OS PATH read afresh, then the
|
|
16787
|
+
// known install folders; installed (with its version), missing, or broken
|
|
16788
|
+
// (found, but `--version` failed: the real error). Never a copy inside
|
|
16789
|
+
// SynaBun, so the badge reflects the CLI the user would run in a terminal.
|
|
16790
|
+
// `force` is Re-check: the OS PATH is read again and every --version asked again.
|
|
16791
|
+
const statuses = await cliResolver.resolveAll(TOOL_DEFS.map((tool) => tool.key), { force });
|
|
16792
|
+
const cleanEnv = withPathEnv(process.env, getAugmentedPath());
|
|
16793
|
+
|
|
16794
|
+
// Only pay for the brew probe when a brew-managed tool is actually present.
|
|
16795
|
+
const brewOutdated = Object.values(statuses).some((status) => status.state === 'installed' && isBrewSource(status.installSource))
|
|
16796
|
+
? await probeBrewOutdated(cleanEnv)
|
|
16797
|
+
: null;
|
|
16798
|
+
|
|
16799
|
+
const checkOneTool = async (tool) => {
|
|
16800
|
+
const status = statuses[tool.key];
|
|
16801
|
+
const installed = status.state === 'installed' ? status.version : null;
|
|
16416
16802
|
|
|
16417
16803
|
// Upstream (npm) version. For a brew install this is NOT the yardstick —
|
|
16418
16804
|
// Homebrew routinely trails npm by a few releases, so comparing against it
|
|
@@ -16433,9 +16819,8 @@ async function checkToolVersions() {
|
|
|
16433
16819
|
}
|
|
16434
16820
|
} catch { /* network error */ }
|
|
16435
16821
|
|
|
16436
|
-
const
|
|
16437
|
-
const
|
|
16438
|
-
const brewToken = isBrewSource(installSource) ? resolveBrewToken(tool.key, binPath) : null;
|
|
16822
|
+
const installSource = status.installSource;
|
|
16823
|
+
const brewToken = isBrewSource(installSource) ? resolveBrewToken(tool.key, status.realPath || status.path) : null;
|
|
16439
16824
|
const latest = resolveLatest({
|
|
16440
16825
|
installSource,
|
|
16441
16826
|
brewToken,
|
|
@@ -16443,8 +16828,10 @@ async function checkToolVersions() {
|
|
|
16443
16828
|
npmLatest: upstreamLatest,
|
|
16444
16829
|
installed,
|
|
16445
16830
|
});
|
|
16831
|
+
// A self-updater is started by the path found, so the update reaches this
|
|
16832
|
+
// very installation even where a terminal's PATH does not name its folder.
|
|
16446
16833
|
const updateCommand = installed
|
|
16447
|
-
? buildUpdateCommand(tool.key, installSource, { brewToken })
|
|
16834
|
+
? buildUpdateCommand(tool.key, installSource, { brewToken, binPath: status.path, platform: process.platform })
|
|
16448
16835
|
: null;
|
|
16449
16836
|
|
|
16450
16837
|
return [tool.key, {
|
|
@@ -16454,32 +16841,33 @@ async function checkToolVersions() {
|
|
|
16454
16841
|
updateAvailable: !!(installed && latest && semverNewer(installed, latest)),
|
|
16455
16842
|
installSource,
|
|
16456
16843
|
brewToken,
|
|
16457
|
-
binPath,
|
|
16844
|
+
binPath: status.path,
|
|
16458
16845
|
updateCommand,
|
|
16459
16846
|
canUpdate: !!updateCommand,
|
|
16847
|
+
state: status.state,
|
|
16848
|
+
version: status.version,
|
|
16849
|
+
error: status.error,
|
|
16850
|
+
foundBy: status.foundBy,
|
|
16851
|
+
realPath: status.realPath,
|
|
16852
|
+
launchPath: status.launchPath,
|
|
16853
|
+
configured: status.configured,
|
|
16854
|
+
checked: status.candidates,
|
|
16855
|
+
guideUrl: CLI_INSTALL_GUIDES[tool.key] || null,
|
|
16856
|
+
install: cliInstaller.describe(tool.key),
|
|
16857
|
+
pathStatus: cliPathManager.inspect(status),
|
|
16460
16858
|
}];
|
|
16461
16859
|
};
|
|
16462
16860
|
|
|
16463
|
-
|
|
16464
|
-
// brew-managed tool is actually present.
|
|
16465
|
-
const probedPaths = await Promise.all(
|
|
16466
|
-
TOOL_DEFS.map((tool) => resolveBinPathAsync(tool.cmd, cleanEnv).catch(() => null)),
|
|
16467
|
-
);
|
|
16468
|
-
const brewOutdated = probedPaths.some((p) => isBrewSource(detectInstallSource(p)))
|
|
16469
|
-
? await probeBrewOutdated(cleanEnv)
|
|
16470
|
-
: null;
|
|
16471
|
-
|
|
16472
|
-
const entries = await Promise.all(
|
|
16473
|
-
TOOL_DEFS.map((tool, i) => checkOneTool(tool, probedPaths[i])),
|
|
16474
|
-
);
|
|
16861
|
+
const entries = await Promise.all(TOOL_DEFS.map((tool) => checkOneTool(tool)));
|
|
16475
16862
|
const results = Object.fromEntries(entries);
|
|
16476
16863
|
|
|
16477
|
-
// Tell a missing tool's install banner
|
|
16478
|
-
//
|
|
16479
|
-
//
|
|
16480
|
-
//
|
|
16481
|
-
// that
|
|
16864
|
+
// Tell a missing tool's install banner the command that fits this machine:
|
|
16865
|
+
// the vendor's own installer for this system (Anthropic's native installer
|
|
16866
|
+
// for Claude Code always), unless the CLIs that ARE installed show a package
|
|
16867
|
+
// manager this machine uses (Homebrew, bun, pnpm …), which is kept to, so
|
|
16868
|
+
// that the manual command never creates a second, conflicting install.
|
|
16482
16869
|
const siblingSources = Object.values(results)
|
|
16870
|
+
.filter((v) => v.state === 'installed')
|
|
16483
16871
|
.map((v) => v.installSource)
|
|
16484
16872
|
.filter((s) => s && s !== 'unknown' && s !== 'other');
|
|
16485
16873
|
const dominantSource = siblingSources
|
|
@@ -16488,23 +16876,40 @@ async function checkToolVersions() {
|
|
|
16488
16876
|
|| 'unknown';
|
|
16489
16877
|
for (const [key, info] of Object.entries(results)) {
|
|
16490
16878
|
const source = info.installed ? info.installSource : dominantSource;
|
|
16491
|
-
info.installCommand = buildInstallCommand(key, source);
|
|
16879
|
+
info.installCommand = buildInstallCommand(key, source, { platform: process.platform });
|
|
16492
16880
|
}
|
|
16493
16881
|
|
|
16882
|
+
// A check started before a newer refresh must never publish its stale answer.
|
|
16883
|
+
if (generation !== _toolVersionGeneration) return;
|
|
16494
16884
|
_toolVersionCache = { checkedAt: new Date().toISOString(), tools: results };
|
|
16495
16885
|
|
|
16496
16886
|
const updates = Object.entries(results).filter(([, v]) => v.updateAvailable);
|
|
16887
|
+
const broken = Object.entries(results).filter(([, v]) => v.state === 'broken');
|
|
16497
16888
|
if (updates.length) {
|
|
16498
16889
|
console.log(` CLI Tools: ${updates.map(([k, v]) => `${k} v${v.installed}→v${v.latest}`).join(', ')}`);
|
|
16499
16890
|
} else {
|
|
16500
16891
|
console.log(' CLI Tools: all up to date');
|
|
16501
16892
|
}
|
|
16893
|
+
for (const [key, info] of broken) console.warn(` CLI Tools: ${key} at ${info.binPath} does not start: ${info.error}`);
|
|
16502
16894
|
}
|
|
16503
16895
|
|
|
16504
16896
|
// Single-flight: concurrent requests share one refresh instead of each
|
|
16505
16897
|
// spawning their own set of subprocess probes.
|
|
16506
16898
|
let _toolVersionInFlight = null;
|
|
16507
|
-
|
|
16899
|
+
let _toolVersionForced = null;
|
|
16900
|
+
function refreshToolVersions({ force = false, fresh = false } = {}) {
|
|
16901
|
+
// A Re-check never takes the answer of a plain check already under way: it
|
|
16902
|
+
// reads the OS PATH and asks every --version again.
|
|
16903
|
+
if (force) {
|
|
16904
|
+
if (fresh || !_toolVersionForced) {
|
|
16905
|
+
const refresh = checkToolVersions({ force: true })
|
|
16906
|
+
.catch(() => {})
|
|
16907
|
+
.finally(() => { if (_toolVersionForced === refresh) _toolVersionForced = null; });
|
|
16908
|
+
_toolVersionForced = refresh;
|
|
16909
|
+
}
|
|
16910
|
+
return _toolVersionForced;
|
|
16911
|
+
}
|
|
16912
|
+
if (_toolVersionForced) return _toolVersionForced;
|
|
16508
16913
|
if (!_toolVersionInFlight) {
|
|
16509
16914
|
_toolVersionInFlight = checkToolVersions()
|
|
16510
16915
|
.catch(() => {})
|
|
@@ -16516,8 +16921,9 @@ function refreshToolVersions() {
|
|
|
16516
16921
|
app.get('/api/system/tool-versions', async (req, res) => {
|
|
16517
16922
|
const force = req.query.force === '1';
|
|
16518
16923
|
if (force || !_toolVersionCache.checkedAt) {
|
|
16519
|
-
// force=1
|
|
16520
|
-
await
|
|
16924
|
+
// force=1 (Re-check, the post-update badge refresh, the panels' poll while
|
|
16925
|
+
// a CLI is missing) needs fresh data: await it.
|
|
16926
|
+
await refreshToolVersions({ force });
|
|
16521
16927
|
} else {
|
|
16522
16928
|
const age = Date.now() - new Date(_toolVersionCache.checkedAt).getTime();
|
|
16523
16929
|
// Stale-while-revalidate: serve the cached value now, refresh in background.
|
|
@@ -16526,6 +16932,40 @@ app.get('/api/system/tool-versions', async (req, res) => {
|
|
|
16526
16932
|
res.json(_toolVersionCache);
|
|
16527
16933
|
});
|
|
16528
16934
|
|
|
16935
|
+
// ── One-click install of a missing CLI (lib/cli-install.js, lib/cli-install-api.js) ──
|
|
16936
|
+
// The side panels' Install button: the vendor's official installer for this
|
|
16937
|
+
// system (or OpenCode's release archive on Windows), never a source a request
|
|
16938
|
+
// names, guarded like the other local mutating endpoints, its progress streamed
|
|
16939
|
+
// as NDJSON. Its last step is the resolver looking again (force); the tool
|
|
16940
|
+
// versions are then refreshed, so every panel's next check shows the CLI.
|
|
16941
|
+
const cliPathManager = createCliPathManager({
|
|
16942
|
+
onChanged: () => { _toolVersionCache = { ..._toolVersionCache, checkedAt: null }; },
|
|
16943
|
+
});
|
|
16944
|
+
app.use('/api/cli/path', createCliPathApi({
|
|
16945
|
+
pathManager: cliPathManager, resolver: cliResolver, isGuestRequest, port: () => PORT,
|
|
16946
|
+
diagnostics: async () => {
|
|
16947
|
+
const fresh = await freshTerminalPath();
|
|
16948
|
+
const statuses = await cliResolver.resolveAll(['claude-code', 'codex', 'opencode']);
|
|
16949
|
+
return { platform: process.platform, pathKey: Object.keys(fresh.env).find(key => key.toUpperCase() === 'PATH'),
|
|
16950
|
+
entries: fresh.entries, os: fresh.os, pathext: fresh.env.PATHEXT || fresh.env.PathExt || null,
|
|
16951
|
+
tools: Object.fromEntries(Object.entries(statuses).map(([tool, status]) => [tool, { path: status.path, launchPath: status.launchPath, ...cliPathManager.inspect(status) }])) };
|
|
16952
|
+
},
|
|
16953
|
+
}));
|
|
16954
|
+
const cliInstaller = createCliInstaller({
|
|
16955
|
+
resolver: cliResolver,
|
|
16956
|
+
addToPath: status => cliPathManager.add(status),
|
|
16957
|
+
readOsPath: async () => (await osPath.refresh({ force: true })).entries,
|
|
16958
|
+
onFinished: () => {
|
|
16959
|
+
_toolVersionCache = { ..._toolVersionCache, checkedAt: null };
|
|
16960
|
+
// The install changes the disk: even a Re-check already running predates it.
|
|
16961
|
+
refreshToolVersions({ force: true, fresh: true });
|
|
16962
|
+
},
|
|
16963
|
+
log: (line) => console.log(line),
|
|
16964
|
+
});
|
|
16965
|
+
app.use('/api/cli/install', createCliInstallApi({ installer: cliInstaller, isGuestRequest, port: () => PORT, log: (line) => console.warn(line) }));
|
|
16966
|
+
// Settings beside the CLIs: the Git Bash Claude Code gets on Windows, its download and a new look (lib/git-bash/).
|
|
16967
|
+
app.use('/api/cli/git-bash', createGitBashApi({ manager: gitBash, isGuestRequest, port: () => PORT, onRecheck: async () => { syncGitBashSettings(); } }));
|
|
16968
|
+
|
|
16529
16969
|
// ═══════════════════════════════════════════
|
|
16530
16970
|
// FULL SYSTEM BACKUP & RESTORE
|
|
16531
16971
|
// ═══════════════════════════════════════════
|
|
@@ -16713,9 +17153,9 @@ app.post('/api/system/restore', async (req, res) => {
|
|
|
16713
17153
|
// Ensure dev deps are installed for TypeScript
|
|
16714
17154
|
const mcpDir = resolve(PACKAGE_ROOT, 'mcp-server');
|
|
16715
17155
|
if (!existsSync(resolve(mcpDir, 'node_modules', 'typescript'))) {
|
|
16716
|
-
execSync('npm install', { cwd: mcpDir, stdio: 'pipe', timeout: 180_000, env: { ...process.env, NODE_ENV: 'development' } });
|
|
17156
|
+
execSync('npm install', { cwd: mcpDir, stdio: 'pipe', timeout: 180_000, env: withPackagedPathLast({ ...process.env, NODE_ENV: 'development' }) });
|
|
16717
17157
|
}
|
|
16718
|
-
execSync('npm run build', { cwd: mcpDir, stdio: 'pipe', timeout: 60_000 });
|
|
17158
|
+
execSync('npm run build', { cwd: mcpDir, stdio: 'pipe', timeout: 60_000, env: withPackagedPathLast(process.env) });
|
|
16719
17159
|
results.files.push('(auto-built MCP server)');
|
|
16720
17160
|
} catch (buildErr) {
|
|
16721
17161
|
results.errors.push(`MCP server auto-build failed: ${buildErr.message}`);
|
|
@@ -16992,13 +17432,10 @@ const editorCompletionServe = createOpencodeCompletionEngine({
|
|
|
16992
17432
|
// A provider's {file:./key} is relative to that configuration: it is written whole for the serve's own folder.
|
|
16993
17433
|
userConfigDir: () => dirname(getOpencodeConfigPath()),
|
|
16994
17434
|
allocPort: allocOpencodeIsoPort,
|
|
16995
|
-
// As the isolated serves are started:
|
|
17435
|
+
// As the isolated serves are started (opencodeServeLaunch): the CLI the resolver found, through the login shell.
|
|
16996
17436
|
spawnServe: ({ port, env, cwd }) => {
|
|
16997
|
-
const
|
|
16998
|
-
const
|
|
16999
|
-
? ['/d', '/s', '/c', `${cmd} serve --port ${port}`]
|
|
17000
|
-
: ['-lc', `exec ${cmd} serve --port ${port}`];
|
|
17001
|
-
const proc = spawn(DEFAULT_SHELL, shellArgs, { stdio: ['ignore', 'ignore', 'pipe'], env, cwd, detached: false, shell: false, windowsHide: true });
|
|
17437
|
+
const launch = opencodeServeLaunch(port, { env });
|
|
17438
|
+
const proc = spawn(launch.command, launch.args, { stdio: ['ignore', 'ignore', 'pipe'], ...launch.options, cwd, detached: false, shell: false, windowsHide: true });
|
|
17002
17439
|
proc.stderr?.on('data', (d) => console.log(`[editor-complete:err] ${String(d).trim().slice(0, 500)}`));
|
|
17003
17440
|
return proc;
|
|
17004
17441
|
},
|
|
@@ -17856,7 +18293,7 @@ app.get('/api/setup/check-deps', async (req, res) => {
|
|
|
17856
18293
|
|
|
17857
18294
|
// npm
|
|
17858
18295
|
try {
|
|
17859
|
-
const { stdout } = await execAsync('npm --version', { timeout: 5000 });
|
|
18296
|
+
const { stdout } = await execAsync('npm --version', { timeout: 5000, env: withPackagedPathLast(process.env) });
|
|
17860
18297
|
deps.push({ id: 'npm', name: 'npm', ok: true, version: `v${stdout.trim()}`, detail: `v${stdout.trim()}`, url: 'https://nodejs.org/' });
|
|
17861
18298
|
} catch {
|
|
17862
18299
|
deps.push({ id: 'npm', name: 'npm', ok: false, version: null, detail: 'Not found', url: 'https://nodejs.org/' });
|
|
@@ -17867,6 +18304,38 @@ app.get('/api/setup/check-deps', async (req, res) => {
|
|
|
17867
18304
|
res.json({ deps });
|
|
17868
18305
|
});
|
|
17869
18306
|
|
|
18307
|
+
// GET /api/setup/install-details — what this install has (lib/install-details.js), for the Details areas of the
|
|
18308
|
+
// setup wizard inside the desktop app: the Node.js and the npm and npx SynaBun runs (each asked for its version),
|
|
18309
|
+
// where `npm i -g` puts packages, the user's own npm, every part's dependencies and versions, the MCP build, and
|
|
18310
|
+
// what a packaged application came with. Read only; the owner's only (/api/setup is admin-only for guests).
|
|
18311
|
+
app.get('/api/setup/install-details', async (req, res) => {
|
|
18312
|
+
try {
|
|
18313
|
+
// The one prefix PATH is built from (packagedNpmPrefix; for an npm or Git install, the one the user set).
|
|
18314
|
+
const npm = PACKAGED_RUNTIME ? packagedNpmPrefix(PACKAGED_RUNTIME) : effectiveNpmPrefix();
|
|
18315
|
+
const prefix = npm?.prefix || null;
|
|
18316
|
+
let manifest = null;
|
|
18317
|
+
if (PACKAGED_RUNTIME) { try { manifest = JSON.parse(readFileSync(PACKAGED_RUNTIME.manifestPath, 'utf8')); } catch {} }
|
|
18318
|
+
let browserExecutable = null;
|
|
18319
|
+
try { browserExecutable = chromium.executablePath(); } catch {}
|
|
18320
|
+
let osEntries = [];
|
|
18321
|
+
try { osEntries = (await osPath.refresh()).entries || []; } catch {}
|
|
18322
|
+
res.json(await collectInstallDetails({
|
|
18323
|
+
packageRoot: PACKAGE_ROOT,
|
|
18324
|
+
packaged: PACKAGED_RUNTIME,
|
|
18325
|
+
manifest,
|
|
18326
|
+
osPath: osEntries,
|
|
18327
|
+
npmPrefix: prefix,
|
|
18328
|
+
npmPrefixSource: !prefix ? null : npm.source === 'builtin' || npm.source === 'default' ? 'synabun' : 'user',
|
|
18329
|
+
npmPrefixFrom: prefix && npm.source !== 'builtin' && npm.source !== 'default' ? npm.from || null : null,
|
|
18330
|
+
npmGlobalBin: prefix ? npmGlobalBinFor(prefix) : null,
|
|
18331
|
+
browserExecutable,
|
|
18332
|
+
}));
|
|
18333
|
+
} catch (err) {
|
|
18334
|
+
console.error('GET /api/setup/install-details error:', err.message);
|
|
18335
|
+
res.status(500).json({ error: err.message });
|
|
18336
|
+
}
|
|
18337
|
+
});
|
|
18338
|
+
|
|
17870
18339
|
// GET /api/setup/onboarding — Check what's configured (onboarding wizard)
|
|
17871
18340
|
app.get('/api/setup/onboarding', async (req, res) => {
|
|
17872
18341
|
try {
|
|
@@ -17937,9 +18406,11 @@ app.post('/api/setup/build', async (req, res) => {
|
|
|
17937
18406
|
res.setHeader('Connection', 'keep-alive');
|
|
17938
18407
|
res.flushHeaders();
|
|
17939
18408
|
|
|
18409
|
+
// What npm wrote, as the wizard shows it: secrets out first (lib/log-redact.js), then the last 500 characters.
|
|
17940
18410
|
function send(event, data) {
|
|
17941
|
-
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
18411
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(redactDeep(data))}\n\n`);
|
|
17942
18412
|
}
|
|
18413
|
+
const tail = (text) => redactSecrets(String(text || '')).slice(-500);
|
|
17943
18414
|
|
|
17944
18415
|
const dirs = [
|
|
17945
18416
|
{ name: 'Neural Interface', path: resolve(PACKAGE_ROOT, 'neural-interface') },
|
|
@@ -17963,14 +18434,15 @@ app.post('/api/setup/build', async (req, res) => {
|
|
|
17963
18434
|
|
|
17964
18435
|
send('install', { dir: dir.name, status: 'start' });
|
|
17965
18436
|
try {
|
|
18437
|
+
// The user's own npm when they have one, a packaged application's after it (withPackagedPathLast).
|
|
17966
18438
|
const { stdout, stderr } = await execAsync('npm install', {
|
|
17967
18439
|
cwd: dir.path,
|
|
17968
18440
|
timeout: 180_000,
|
|
17969
|
-
env: { ...process.env, NODE_ENV: 'development' },
|
|
18441
|
+
env: withPackagedPathLast({ ...process.env, NODE_ENV: 'development' }),
|
|
17970
18442
|
});
|
|
17971
|
-
send('install', { dir: dir.name, status: 'done', output: (stdout
|
|
18443
|
+
send('install', { dir: dir.name, status: 'done', output: tail(stdout) });
|
|
17972
18444
|
} catch (err) {
|
|
17973
|
-
send('install', { dir: dir.name, status: 'error', output: (err.stderr || err.message
|
|
18445
|
+
send('install', { dir: dir.name, status: 'error', output: tail(err.stderr || err.message) });
|
|
17974
18446
|
send('complete', { ok: false, error: `npm install failed for ${dir.name}` });
|
|
17975
18447
|
return res.end();
|
|
17976
18448
|
}
|
|
@@ -17991,10 +18463,11 @@ app.post('/api/setup/build', async (req, res) => {
|
|
|
17991
18463
|
const { stdout, stderr } = await execAsync('npm run build', {
|
|
17992
18464
|
cwd: resolve(PACKAGE_ROOT, 'mcp-server'),
|
|
17993
18465
|
timeout: 60_000,
|
|
18466
|
+
env: withPackagedPathLast(process.env),
|
|
17994
18467
|
});
|
|
17995
|
-
send('build', { status: 'done', output: (stdout
|
|
18468
|
+
send('build', { status: 'done', output: tail(stdout) });
|
|
17996
18469
|
} catch (err) {
|
|
17997
|
-
send('build', { status: 'error', output: (err.stderr || err.message
|
|
18470
|
+
send('build', { status: 'error', output: tail(err.stderr || err.message) });
|
|
17998
18471
|
send('complete', { ok: false, error: 'MCP server build failed' });
|
|
17999
18472
|
return res.end();
|
|
18000
18473
|
}
|
|
@@ -18562,7 +19035,7 @@ function injectClaudeMemoryPerms() {
|
|
|
18562
19035
|
|
|
18563
19036
|
function injectCodexMemoryPerms() {
|
|
18564
19037
|
try {
|
|
18565
|
-
const configPath = join(
|
|
19038
|
+
const configPath = join(CODEX_DEFAULT_HOME, 'config.toml');
|
|
18566
19039
|
if (!existsSync(configPath)) return { scope: 'none', paths: [] };
|
|
18567
19040
|
let content = readFileSync(configPath, 'utf-8');
|
|
18568
19041
|
if (!tomlHasSection(content, 'mcp_servers.SynaBun')) return { scope: 'none', paths: [] };
|
|
@@ -20729,9 +21202,9 @@ function tomlRemoveAllToolSections(content, prefix) {
|
|
|
20729
21202
|
* - If disabled → remove that section
|
|
20730
21203
|
* Preserves all non-tool config (model, projects, mcp_servers.SynaBun base, notices).
|
|
20731
21204
|
*/
|
|
20732
|
-
function syncCodexToolPermissions(allowedKeys) {
|
|
21205
|
+
function syncCodexToolPermissions(allowedKeys, codexHome = CODEX_DEFAULT_HOME) {
|
|
20733
21206
|
try {
|
|
20734
|
-
const configPath = join(
|
|
21207
|
+
const configPath = join(codexHome, 'config.toml');
|
|
20735
21208
|
if (!existsSync(configPath)) return;
|
|
20736
21209
|
healCodexConfig(configPath);
|
|
20737
21210
|
const health = inspectCodexConfig(configPath);
|
|
@@ -20875,7 +21348,7 @@ app.delete('/api/setup/gemini/mcp', (req, res) => {
|
|
|
20875
21348
|
|
|
20876
21349
|
app.get('/api/setup/codex/mcp', (req, res) => {
|
|
20877
21350
|
try {
|
|
20878
|
-
const configPath = join(
|
|
21351
|
+
const configPath = join(CODEX_DEFAULT_HOME, 'config.toml');
|
|
20879
21352
|
const toml = buildCodexMcpToml();
|
|
20880
21353
|
const health = inspectCodexConfig(configPath);
|
|
20881
21354
|
res.json({ ok: true, ...health, toml });
|
|
@@ -20886,7 +21359,7 @@ app.get('/api/setup/codex/mcp', (req, res) => {
|
|
|
20886
21359
|
|
|
20887
21360
|
app.post('/api/setup/codex/mcp', (req, res) => {
|
|
20888
21361
|
try {
|
|
20889
|
-
const dir =
|
|
21362
|
+
const dir = CODEX_DEFAULT_HOME;
|
|
20890
21363
|
const configPath = join(dir, 'config.toml');
|
|
20891
21364
|
mkdirSync(dir, { recursive: true });
|
|
20892
21365
|
ensureMcpProfileDefault('codex-browser');
|
|
@@ -20901,7 +21374,7 @@ app.post('/api/setup/codex/mcp', (req, res) => {
|
|
|
20901
21374
|
|
|
20902
21375
|
app.delete('/api/setup/codex/mcp', (req, res) => {
|
|
20903
21376
|
try {
|
|
20904
|
-
const configPath = join(
|
|
21377
|
+
const configPath = join(CODEX_DEFAULT_HOME, 'config.toml');
|
|
20905
21378
|
if (!existsSync(configPath)) return res.json({ ok: true, rules: syncRulesWithMcp('codex', 'remove') });
|
|
20906
21379
|
const health = removeCodexMcpConfig(configPath);
|
|
20907
21380
|
res.json({ ok: true, ...health, message: 'SynaBun MCP removed from Codex CLI.', rules: syncRulesWithMcp('codex', 'remove') });
|
|
@@ -20936,7 +21409,7 @@ app.get('/api/setup/status', (req, res) => {
|
|
|
20936
21409
|
// Codex
|
|
20937
21410
|
let codex = { connected: false, valid: false, repairable: false, issues: [] };
|
|
20938
21411
|
try {
|
|
20939
|
-
codex = inspectCodexConfig(join(
|
|
21412
|
+
codex = inspectCodexConfig(join(CODEX_DEFAULT_HOME, 'config.toml'));
|
|
20940
21413
|
} catch {}
|
|
20941
21414
|
codex.cliCommand = buildCodexMcpCliCommand();
|
|
20942
21415
|
|
|
@@ -22211,6 +22684,7 @@ app.get('/api/health', (req, res) => {
|
|
|
22211
22684
|
const count = countMemories();
|
|
22212
22685
|
res.json({
|
|
22213
22686
|
ok: true, storage: 'sqlite', memories: count, projectDir: PACKAGE_ROOT, install: INSTALL_KIND, startLauncher: getStartLauncherState(), entry: START_ENTRY,
|
|
22687
|
+
version: SERVER_VERSION, dataHomeId: DATA_HOME_ID, installEntry: INSTALL_ENTRY,
|
|
22214
22688
|
// Which server this is, and whether it is on its way out for a restart: a
|
|
22215
22689
|
// page showing the restart tells this one from the one that replaces it.
|
|
22216
22690
|
bootId: SERVER_BOOT_ID,
|
|
@@ -22587,14 +23061,15 @@ const SYNABUN_TOOL_GROUPS = {
|
|
|
22587
23061
|
sync: { label: 'Sync', tools: 1, alwaysOn: true },
|
|
22588
23062
|
loop: { label: 'Loop', tools: 1, alwaysOn: true },
|
|
22589
23063
|
profile: { label: 'Profile', tools: 1, alwaysOn: true },
|
|
23064
|
+
choice: { label: 'Choice', tools: 1, alwaysOn: true },
|
|
22590
23065
|
git: { label: 'Git', tools: 1 },
|
|
22591
23066
|
image: { label: 'Images', tools: 1 },
|
|
22592
23067
|
whiteboard: { label: 'Whiteboard', tools: 5 },
|
|
22593
23068
|
card: { label: 'Cards', tools: 5 },
|
|
22594
23069
|
tictactoe: { label: 'TicTacToe', tools: 1 },
|
|
22595
|
-
browser: { label: 'Browser', tools:
|
|
22596
|
-
browser_twitter: { label: 'Twitter/X', tools:
|
|
22597
|
-
browser_facebook: { label: 'Facebook', tools:
|
|
23070
|
+
browser: { label: 'Browser', tools: 21 },
|
|
23071
|
+
browser_twitter: { label: 'Twitter/X', tools: 2 },
|
|
23072
|
+
browser_facebook: { label: 'Facebook', tools: 4 },
|
|
22598
23073
|
browser_tiktok: { label: 'TikTok', tools: 4 },
|
|
22599
23074
|
browser_whatsapp: { label: 'WhatsApp', tools: 2 },
|
|
22600
23075
|
browser_instagram: { label: 'Instagram', tools: 5 },
|
|
@@ -23754,12 +24229,10 @@ function saveCliConfig(config) {
|
|
|
23754
24229
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
23755
24230
|
writeFileSync(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
23756
24231
|
_cliConfigCache = null;
|
|
23757
|
-
//
|
|
23758
|
-
//
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
_codexBinPath = null;
|
|
23762
|
-
_nativeCodexBinPath = null;
|
|
24232
|
+
// A path saved in Settings counts at once, for the version check and every
|
|
24233
|
+
// launcher alike (lib/cli-resolver.js), with no restart.
|
|
24234
|
+
cliResolver.invalidate();
|
|
24235
|
+
_toolVersionCache = { ..._toolVersionCache, checkedAt: null };
|
|
23763
24236
|
}
|
|
23764
24237
|
|
|
23765
24238
|
// Interactive shell tabs start as login shells, like Terminal.app / iTerm2.
|
|
@@ -23781,14 +24254,16 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
23781
24254
|
|
|
23782
24255
|
let cmd = entry.command.trim();
|
|
23783
24256
|
if (!cmd) return SHELL_PROFILE;
|
|
24257
|
+
const found = cliResolver.launcher(profileId);
|
|
24258
|
+
if (!configuredCommandLine(profileId)) cmd = terminalLaunchCommand(found, DEFAULT_SHELL) || cmd;
|
|
23784
24259
|
|
|
23785
24260
|
// Append resume flag for supported CLIs (mutually exclusive with model)
|
|
23786
24261
|
if (opts.resume && profileId === 'claude-code') {
|
|
23787
|
-
cmd = `${cmd} --resume ${opts.resume}`;
|
|
24262
|
+
cmd = `${cmd} --resume ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
23788
24263
|
} else if (opts.resume && profileId === 'codex') {
|
|
23789
|
-
cmd = `${cmd} resume ${opts.resume}`;
|
|
24264
|
+
cmd = `${cmd} resume ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
23790
24265
|
} else if (opts.resume && profileId === 'opencode') {
|
|
23791
|
-
cmd = `${cmd} -s ${opts.resume}`;
|
|
24266
|
+
cmd = `${cmd} -s ${terminalArgument(opts.resume, DEFAULT_SHELL)}`;
|
|
23792
24267
|
}
|
|
23793
24268
|
// Append model flag for CLIs that support it
|
|
23794
24269
|
else if (opts.model) {
|
|
@@ -23802,7 +24277,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
23802
24277
|
// suffix `[1m]` contains `[ ]`, which zsh parses as a glob character class —
|
|
23803
24278
|
// unquoted it aborts with "no matches found" and the CLI never launches.
|
|
23804
24279
|
// Mirror the IS_WIN-aware quoting used for --mcp-config below.
|
|
23805
|
-
const quotedModel =
|
|
24280
|
+
const quotedModel = terminalArgument(cleanModel, DEFAULT_SHELL);
|
|
23806
24281
|
const modelMap = {
|
|
23807
24282
|
'claude-code': (m) => `${cmd} --model ${m}`,
|
|
23808
24283
|
'codex': (m) => `${cmd} --model ${m}`,
|
|
@@ -23841,7 +24316,7 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
23841
24316
|
// collisions invisibly. Trade-off: project-scope .mcp.json servers are not
|
|
23842
24317
|
// loaded inside loops (user-scope servers ARE — they're merged into the file).
|
|
23843
24318
|
if (opts.mcpConfigPath && profileId === 'claude-code') {
|
|
23844
|
-
const quoted =
|
|
24319
|
+
const quoted = terminalArgument(opts.mcpConfigPath, DEFAULT_SHELL);
|
|
23845
24320
|
cmd = `${cmd} --strict-mcp-config --mcp-config ${quoted}`;
|
|
23846
24321
|
}
|
|
23847
24322
|
|
|
@@ -23854,20 +24329,25 @@ function getTerminalProfile(profileId, opts = {}) {
|
|
|
23854
24329
|
};
|
|
23855
24330
|
}
|
|
23856
24331
|
|
|
24332
|
+
// Read for every spawn, including a tab restored/split by the client and
|
|
24333
|
+
// provider sign-in terminals. The desktop app uses this same server/PTY host.
|
|
24334
|
+
async function freshTerminalPath(base = process.env) {
|
|
24335
|
+
const os = await osPath.refresh({ force: true });
|
|
24336
|
+
const launchers = ['claude-code', 'codex', 'opencode', 'gemini'].map(tool => cliResolver.launcher(tool));
|
|
24337
|
+
// A packaged application's runtime and npm's global bin (rememberPackagedPath above), after the user's own.
|
|
24338
|
+
const { last: synabunDirs, extra: extraDirs } = rememberedPackagedPath();
|
|
24339
|
+
return { ...composeTerminalPath({ env: base, os, launchers, synabunDirs, extraDirs }), os, launchers };
|
|
24340
|
+
}
|
|
24341
|
+
|
|
23857
24342
|
function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
23858
24343
|
if (terminalHost.ready && !terminalHost.ptyAvailable) {
|
|
23859
24344
|
throw new Error('Terminal not available: node-pty failed to load. Run: cd neural-interface && npm run postinstall');
|
|
23860
24345
|
}
|
|
23861
24346
|
const sessionId = opts.sessionId || randomBytes(16).toString('hex');
|
|
23862
|
-
|
|
24347
|
+
let profileCfg = opts.launch || getTerminalProfile(profile, opts);
|
|
23863
24348
|
const initialCols = cols || 120;
|
|
23864
24349
|
const initialRows = rows || 30;
|
|
23865
24350
|
|
|
23866
|
-
// Native-terminal environment: SynaBun's own TERM_PROGRAM identity,
|
|
23867
|
-
// truecolor, a UTF-8 locale, and none of the launching terminal's or IDE's
|
|
23868
|
-
// session variables (VS Code, tmux, iTerm2, a parent Claude Code session…).
|
|
23869
|
-
const env = buildTerminalEnv({ profileEnv: profileCfg.env, extraEnv: opts.extraEnv });
|
|
23870
|
-
|
|
23871
24351
|
const isLoopOwned = !!(opts.extraEnv && opts.extraEnv.SYNABUN_TERMINAL_SESSION === sessionId);
|
|
23872
24352
|
if (isLoopOwned) {
|
|
23873
24353
|
loopLog(sessionId, 'pty:resolve', 'profile resolved', {
|
|
@@ -23886,6 +24366,7 @@ function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
|
23886
24366
|
profile, cwd,
|
|
23887
24367
|
createdAt: Date.now(),
|
|
23888
24368
|
graceTimer: null,
|
|
24369
|
+
onProviderAuthClosed: opts.onClosed,
|
|
23889
24370
|
claudeSessionId: opts.resume || null, // known Claude session ID (from resume or detect)
|
|
23890
24371
|
};
|
|
23891
24372
|
terminalSessions.set(sessionId, session);
|
|
@@ -23942,6 +24423,7 @@ function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
|
23942
24423
|
// The host already delivered the final output (to clients and the tap)
|
|
23943
24424
|
// before reporting the exit.
|
|
23944
24425
|
session.pty.exited = true;
|
|
24426
|
+
opts.onExit?.({ exitCode });
|
|
23945
24427
|
|
|
23946
24428
|
// Clear grace timer if set
|
|
23947
24429
|
if (session.graceTimer) { clearTimeout(session.graceTimer); session.graceTimer = null; }
|
|
@@ -24020,7 +24502,6 @@ function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
|
24020
24502
|
file: profileCfg.shell,
|
|
24021
24503
|
args: profileCfg.args,
|
|
24022
24504
|
cwd: cwd || process.env.USERPROFILE || process.env.HOME || process.cwd(),
|
|
24023
|
-
env,
|
|
24024
24505
|
cols: initialCols,
|
|
24025
24506
|
rows: initialRows,
|
|
24026
24507
|
name: 'xterm-256color',
|
|
@@ -24036,6 +24517,12 @@ function createTerminalSession(profile, cols, rows, cwd, opts = {}) {
|
|
|
24036
24517
|
onResized: ({ cols: c, rows: r }) => { session.pty.cols = c; session.pty.rows = r; },
|
|
24037
24518
|
onClients: ({ total }) => { session.clients.size = total; },
|
|
24038
24519
|
onExit,
|
|
24520
|
+
}, async () => {
|
|
24521
|
+
const fresh = await freshTerminalPath(opts.baseEnv || process.env);
|
|
24522
|
+
profileCfg = opts.launch || getTerminalProfile(profile, opts);
|
|
24523
|
+
// On Windows, the Git Bash a claude started in this tab gets (lib/git-bash/): any tab can start one.
|
|
24524
|
+
const env = claudeChildEnv(buildTerminalEnv({ base: fresh.env, profileEnv: profileCfg.env, extraEnv: opts.extraEnv }));
|
|
24525
|
+
return { file: profileCfg.shell, args: profileCfg.args, env };
|
|
24039
24526
|
});
|
|
24040
24527
|
|
|
24041
24528
|
return sessionId;
|
|
@@ -24665,8 +25152,17 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24665
25152
|
providerFactories: {
|
|
24666
25153
|
'claude-code': (state) => {
|
|
24667
25154
|
const config = _readClaudeSkinConfig();
|
|
25155
|
+
// The account decides the credential (lib/claude-accounts.js launchFor()): inherited credentials out,
|
|
25156
|
+
// its directory and, in API-key mode, its key in. Built here, never stored: the loop state file only
|
|
25157
|
+
// names the account. A run whose account is gone does not start under another identity.
|
|
25158
|
+
const launch = _panelClaudeAccounts().launchFor(state.claudeAccountId || 'default');
|
|
25159
|
+
if (!launch.ok) throw new Error(`${launch.message} The run did not start.`);
|
|
24668
25160
|
return createClaudeNativeLoopAdapter({
|
|
24669
25161
|
...state,
|
|
25162
|
+
extraEnv: { ...(state.extraEnv || {}), ...launch.env },
|
|
25163
|
+
unsetEnv: launch.unset,
|
|
25164
|
+
// On Windows, the Git Bash the worker's Claude Code gets (lib/git-bash/), never stored in the loop state.
|
|
25165
|
+
childEnv: claudeChildEnv,
|
|
24670
25166
|
model: state.contextMode === '1m' && state.model
|
|
24671
25167
|
? `${state.model}[1m]`
|
|
24672
25168
|
: state.model && state.model.includes(':') ? toCliModelName(state.model) : state.model,
|
|
@@ -24686,10 +25182,18 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24686
25182
|
// path, so repair the shared config before the SDK can parse it. Without
|
|
24687
25183
|
// this, legacy inline + table SynaBun env definitions surface as a
|
|
24688
25184
|
// configWarning and the unattended turn never becomes active.
|
|
24689
|
-
|
|
25185
|
+
// The account decides the credential (lib/codex-accounts.js launchFor()): inherited OpenAI/Codex
|
|
25186
|
+
// credentials out, its CODEX_HOME in and, in API-key mode, the key SynaBun holds as CODEX_API_KEY
|
|
25187
|
+
// (which `codex exec` prefers over auth.json). Built here, never stored: the loop state file only
|
|
25188
|
+
// names the account. A run whose account is gone does not start under another identity.
|
|
25189
|
+
const launch = _codexAccounts().launchFor(state.codexAccountId || 'default');
|
|
25190
|
+
if (!launch.ok) throw new Error(`${launch.message} The run did not start.`);
|
|
25191
|
+
ensureCanonicalCodexConfig({ strict: true, codexHome: launch.home });
|
|
24690
25192
|
return createCodexNativeLoopAdapter({
|
|
24691
25193
|
...state,
|
|
24692
|
-
|
|
25194
|
+
extraEnv: { ...(state.extraEnv || {}), ...codexExecEnv(launch) },
|
|
25195
|
+
unsetEnv: launch.unset,
|
|
25196
|
+
codexHome: launch.home,
|
|
24693
25197
|
// Native schedules use the same trusted global CLI as the interactive
|
|
24694
25198
|
// sidepanel; SynaBun carries no Codex for the SDK to find by itself.
|
|
24695
25199
|
codexPath: getNativeCodexBin(),
|
|
@@ -24755,7 +25259,8 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24755
25259
|
import('../mcp-server/dist/tools/remember.js'), import('../mcp-server/dist/services/categories.js'),
|
|
24756
25260
|
import('./lib/assistant-catalog.js'), import('./lib/assistant-config.js'), import('./lib/assistant-router.js'),
|
|
24757
25261
|
]);
|
|
24758
|
-
const {
|
|
25262
|
+
const { createModelPricing } = await import('./lib/assistant-budget.js');
|
|
25263
|
+
const { createBudgetPlanner } = await import('./lib/assistant-budget-planner.js');
|
|
24759
25264
|
const { createAssistantClarifier } = await import('./lib/assistant-clarify.js');
|
|
24760
25265
|
const { createAttachmentStore } = await import('./lib/assistant-attachments.js');
|
|
24761
25266
|
const assistantConfigPath = resolve(LOOP_DATA_DIR, 'assistant-config.json');
|
|
@@ -24763,7 +25268,25 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24763
25268
|
// One store for assistant-config.json: routing knobs re-read on change.
|
|
24764
25269
|
const assistantConfigStore = createAssistantConfigStore({ path: assistantConfigPath, log: (tag, message) => assistantLog(tag, message) });
|
|
24765
25270
|
const assistantConfig = assistantConfigStore.read();
|
|
24766
|
-
|
|
25271
|
+
// The one Claude accounts instance (the sidepanel's): one account state for the brain picker, the catalog,
|
|
25272
|
+
// worker dispatch and the panel. `createClaudeAccounts` stays imported for older callers of this block.
|
|
25273
|
+
void createClaudeAccounts;
|
|
25274
|
+
const claudeAccounts = _panelClaudeAccounts();
|
|
25275
|
+
const { createAccountUsage } = await import('./lib/account-usage.js');
|
|
25276
|
+
const accountUsage = createAccountUsage({ accounts: { 'claude-code': claudeAccounts, codex: _codexAccounts() }, readCodex: async (id, opts) => {
|
|
25277
|
+
for (const connection of _codexLiveConnections) {
|
|
25278
|
+
if (opts.signal?.aborted) return null;
|
|
25279
|
+
if (connection.accountId() !== id) continue;
|
|
25280
|
+
try { const value = await connection.readRateLimits(); if (value && connection.accountId() === id) return value; } catch {}
|
|
25281
|
+
}
|
|
25282
|
+
if (opts.signal?.aborted) return null;
|
|
25283
|
+
return codexAccountAuth().rateLimits(id, opts);
|
|
25284
|
+
} });
|
|
25285
|
+
_assistantAccountUsage = accountUsage;
|
|
25286
|
+
claudeAccounts.subscribe(({ accountId }) => accountUsage.invalidate('claude-code', accountId));
|
|
25287
|
+
_codexAccounts().subscribe(({ accountId }) => accountUsage.invalidate('codex', accountId));
|
|
25288
|
+
// A Claude Code worker whose dispatch names no account runs under this one (Assistant → accounts).
|
|
25289
|
+
_assistantClaudeWorkerAccount = () => assistantConfigStore.workers().claudeAccount;
|
|
24767
25290
|
const memoryStoredForRun = (runId) => {
|
|
24768
25291
|
try { return !!assistantGetDb().prepare('SELECT 1 FROM memory_metadata WHERE source_ref = ? LIMIT 1').get(String(runId)); } catch { return false; }
|
|
24769
25292
|
};
|
|
@@ -24797,8 +25320,10 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24797
25320
|
import('../mcp-server/dist/services/typesafe.js'), import('../mcp-server/dist/services/typesafe-config.js'),
|
|
24798
25321
|
import('./lib/assistant-jev.js'),
|
|
24799
25322
|
]);
|
|
25323
|
+
// The budget split's questions (surface budget-split): a dist without them leaves the split on its fixed rule.
|
|
25324
|
+
const budgetJudgments = await import('../mcp-server/dist/services/budget-judgments.js').catch(() => null);
|
|
24800
25325
|
assistantJev = createAssistantJev({
|
|
24801
|
-
judgePrompt, judgeWorkerOutcome,
|
|
25326
|
+
judgePrompt, judgeWorkerOutcome, judgeBudgetSplit: budgetJudgments?.judgeBudgetSplit || null,
|
|
24802
25327
|
surfaceConfig: typesafeKnobs.surfaceConfig,
|
|
24803
25328
|
enabled: typesafeClient.typesafeEnabled,
|
|
24804
25329
|
retryAfterMs: typesafeClient.typesafeRetryAfterMs,
|
|
@@ -24845,7 +25370,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24845
25370
|
const assistantCatalog = createAssistantCatalog({
|
|
24846
25371
|
fetchJson: catalogFetch,
|
|
24847
25372
|
codexPrice: (model) => assistantPricing.codexPrice(model),
|
|
24848
|
-
listAccounts: () =>
|
|
25373
|
+
listAccounts: () => accountUsage.list(),
|
|
24849
25374
|
readFilter: () => assistantConfigStore.routing().catalogFilter || {},
|
|
24850
25375
|
readHidden: () => assistantHiddenModels.all(),
|
|
24851
25376
|
// A hidden alias ("haiku") hides the model it named when it was hidden: each build settles that.
|
|
@@ -24931,6 +25456,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24931
25456
|
accountEnv: () => claudeAccounts.envFor?.('default') || {},
|
|
24932
25457
|
cwd: DATA_HOME,
|
|
24933
25458
|
env: process.env,
|
|
25459
|
+
childEnv: claudeChildEnv,
|
|
24934
25460
|
log: (tag, message) => assistantLog(tag, message),
|
|
24935
25461
|
}),
|
|
24936
25462
|
usage: assistantUsage,
|
|
@@ -24990,6 +25516,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
24990
25516
|
accountEnv: () => claudeAccounts.envFor?.('default') || {},
|
|
24991
25517
|
cwd: DATA_HOME,
|
|
24992
25518
|
env: process.env,
|
|
25519
|
+
childEnv: claudeChildEnv,
|
|
24993
25520
|
label: 'greeting writer',
|
|
24994
25521
|
logTag: 'greeting:writer-error',
|
|
24995
25522
|
log: greetingLog,
|
|
@@ -25022,7 +25549,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25022
25549
|
app.use('/api/assistant/hidden-models', createAssistantHiddenModelsRouter({
|
|
25023
25550
|
hiddenModels: assistantHiddenModels, catalog: assistantCatalog, opencodeStore: opencodeHiddenModels, isGuestRequest, broadcastSync,
|
|
25024
25551
|
}));
|
|
25025
|
-
const buildCatalog = (opts = {}) => assistantCatalog.get(opts);
|
|
25552
|
+
const buildCatalog = async (opts = {}) => { await accountUsage.refresh(); return { ...(await assistantCatalog.get(opts)), accounts: accountUsage.list() }; };
|
|
25026
25553
|
// Clarification: a materially ambiguous request gets 1-3 questions before it
|
|
25027
25554
|
// is routed; the user's words and answers become the brief every worker of
|
|
25028
25555
|
// that request receives. Knobs: assistant-config.json `clarify` (read live).
|
|
@@ -25041,10 +25568,41 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25041
25568
|
},
|
|
25042
25569
|
log: (tag, message) => assistantLog(tag, message),
|
|
25043
25570
|
});
|
|
25571
|
+
// The session budget (lib/assistant-budget-planner.js): one setting for every session, divided
|
|
25572
|
+
// across the providers per session by Jev (surface budget-split) or a fixed rule, at the first
|
|
25573
|
+
// route and at checkpoints. The dispatcher, the runtime and the router read it; it reaches them lazily.
|
|
25574
|
+
const assistantBudget = createBudgetPlanner({
|
|
25575
|
+
setting: () => assistantConfigStore.budget(),
|
|
25576
|
+
spendOf: (sessionId) => _assistantDispatcher?.providerSpend?.(sessionId) || { providers: {}, unpricedRuns: 0, liveRuns: [] },
|
|
25577
|
+
brainOf: (sessionId) => _assistantRuntime?.budgetSession?.(sessionId) || null,
|
|
25578
|
+
isOpen: (sessionId) => _assistantRuntime?.budgetSession?.(sessionId)?.open !== false,
|
|
25579
|
+
catalog: () => assistantCatalog.peek?.() || null,
|
|
25580
|
+
refreshCatalog: () => assistantCatalog.full(),
|
|
25581
|
+
accounts: () => accountUsage.list(),
|
|
25582
|
+
routing: () => assistantConfigStore.routing(),
|
|
25583
|
+
routeMode: (sessionId) => { try { return assistantRouter.modeFor(sessionId); } catch { return null; } },
|
|
25584
|
+
workers: () => assistantConfigStore.workers(),
|
|
25585
|
+
pricing: assistantPricing,
|
|
25586
|
+
// This session's spend by provider from the usage ledger (every model row it booked).
|
|
25587
|
+
recentSpend: (sessionId) => {
|
|
25588
|
+
try {
|
|
25589
|
+
const out = {};
|
|
25590
|
+
for (const row of assistantUsage.sessionView(sessionId)?.models || []) if (row?.provider) out[row.provider] = (out[row.provider] || 0) + (Number(row.costUsd) || 0);
|
|
25591
|
+
return out;
|
|
25592
|
+
} catch { return null; }
|
|
25593
|
+
},
|
|
25594
|
+
judge: assistantJev,
|
|
25595
|
+
emit: (sessionId, view, meta) => { try { _assistantRuntime?.onBudgetSplit?.(sessionId, view, meta); } catch {} },
|
|
25596
|
+
statePath: resolve(LOOP_DATA_DIR, 'assistant-budget-splits.json'),
|
|
25597
|
+
log: (tag, message) => assistantLog(tag, message),
|
|
25598
|
+
});
|
|
25044
25599
|
// DeepSeek-Harness-style routing: the brain proposes, the router enforces the
|
|
25045
25600
|
// session's route mode. Sinks reach the runtime/dispatcher lazily.
|
|
25046
25601
|
const assistantRouter = createAssistantRouter({
|
|
25602
|
+
accountUsage,
|
|
25603
|
+
budget: assistantBudget,
|
|
25047
25604
|
catalog: assistantCatalog,
|
|
25605
|
+
listAccounts: () => accountUsage.list(),
|
|
25048
25606
|
configStore: assistantConfigStore,
|
|
25049
25607
|
getSession: (id) => _assistantRuntime?.routerSession?.(id) || null,
|
|
25050
25608
|
sinks: {
|
|
@@ -25124,20 +25682,25 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25124
25682
|
assistantDesktop = null;
|
|
25125
25683
|
}
|
|
25126
25684
|
_assistantDispatcher = createAssistantDispatcher({
|
|
25685
|
+
accountUsage,
|
|
25127
25686
|
getRuntime: () => _nativeLoopRuntime,
|
|
25128
25687
|
dataDir: LOOP_DATA_DIR,
|
|
25129
25688
|
loopDir: LOOP_DIR,
|
|
25130
25689
|
broadcastSync: (message) => { broadcastSync(message); try { _assistantRuntime?.observeBroadcast?.(message); } catch {} },
|
|
25131
25690
|
log: (runId, tag, message, meta) => { try { loopLog(runId, tag, message, meta); } catch {} },
|
|
25132
25691
|
PACKAGE_ROOT,
|
|
25133
|
-
findCodexAccount, getCodexAccount, CODEX_DEFAULT_HOME, claudeAccounts,
|
|
25692
|
+
findCodexAccount, getCodexAccount, CODEX_DEFAULT_HOME, claudeAccounts, codexAccounts: _codexAccounts(),
|
|
25693
|
+
claudeWorkerAccount: () => assistantConfigStore.workers().claudeAccount,
|
|
25694
|
+
codexWorkerAccount: () => assistantConfigStore.workers().codexAccount,
|
|
25134
25695
|
isValidMcpProfileValue, normalizeMcpProfileName, readActiveMcpProfile,
|
|
25135
25696
|
acquireLoopBrowserAndTab, releaseSharedBrowserTab, reconcileKeepAwake,
|
|
25136
25697
|
memory: assistantMemory, detectProject: assistantDetectProject,
|
|
25137
|
-
// Limits
|
|
25698
|
+
// Limits are read live from assistant-config.json `limits`; the money is the session budget (the planner).
|
|
25138
25699
|
limits: {},
|
|
25139
|
-
readLimits: () => ({ ...(assistantConfigStore.read().limits || {})
|
|
25700
|
+
readLimits: () => ({ ...(assistantConfigStore.read().limits || {}) }),
|
|
25701
|
+
budget: assistantBudget,
|
|
25140
25702
|
brainSpend: (sessionId) => _assistantRuntime?.brainSpend?.(sessionId) || 0,
|
|
25703
|
+
brainSpendByProvider: (sessionId) => _assistantRuntime?.brainSpendByProvider?.(sessionId) || null,
|
|
25141
25704
|
currentTask: (sessionId) => _assistantRuntime?.currentTask?.(sessionId) || null,
|
|
25142
25705
|
pricing: assistantPricing,
|
|
25143
25706
|
usage: assistantUsage,
|
|
@@ -25148,9 +25711,11 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25148
25711
|
testDensity: (sessionId) => _assistantRuntime?.testDensityFor?.(sessionId) ?? null,
|
|
25149
25712
|
});
|
|
25150
25713
|
_assistantRuntime = createAssistantRuntime({
|
|
25714
|
+
accountUsage,
|
|
25151
25715
|
dispatcher: _assistantDispatcher,
|
|
25152
25716
|
phoneAuthority: _phoneAuthority.verify, // the verifier only
|
|
25153
|
-
claudeAccounts,
|
|
25717
|
+
claudeAccounts, codexAccounts: _codexAccounts(),
|
|
25718
|
+
codexHomeForAccount: (accountId) => _codexAccounts().homeFor(accountId),
|
|
25154
25719
|
ClaudeSession: claudeBridgeModule.ClaudeSession,
|
|
25155
25720
|
sdkVersion: claudeBridgeModule.SDK_VERSION,
|
|
25156
25721
|
handleCodexSkinWebSocket, ensureIsolatedServe, stopIsolatedServe, setupOpencodeSidepanelConfig,
|
|
@@ -25167,6 +25732,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25167
25732
|
gateUrl: `http://127.0.0.1:${PORT}/api/assistant/route-gate/check`,
|
|
25168
25733
|
codexBin: () => getCodexBin(),
|
|
25169
25734
|
pricing: assistantPricing,
|
|
25735
|
+
budget: assistantBudget,
|
|
25170
25736
|
usage: assistantUsage,
|
|
25171
25737
|
clarifier: assistantClarifier,
|
|
25172
25738
|
userContext: assistantUserContext,
|
|
@@ -25189,39 +25755,30 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25189
25755
|
if (unverified && attempt < 3) setTimeout(() => { repairRouteEfforts(attempt + 1).catch(() => {}); }, 60_000).unref?.();
|
|
25190
25756
|
};
|
|
25191
25757
|
repairRouteEfforts().catch(() => {});
|
|
25192
|
-
|
|
25193
|
-
|
|
25194
|
-
|
|
25195
|
-
|
|
25196
|
-
|
|
25197
|
-
if (!acct) throw Object.assign(new Error('Unknown Codex account'), { code: 'ACCOUNT_NOT_FOUND', status: 404 });
|
|
25198
|
-
const next = String(label || '').trim().slice(0, 60);
|
|
25199
|
-
if (next) acct.label = next;
|
|
25200
|
-
saveCodexAccounts(data);
|
|
25201
|
-
return acct;
|
|
25202
|
-
},
|
|
25203
|
-
remove: (id, { inUse = () => null } = {}) => {
|
|
25204
|
-
if (id === 'default') throw Object.assign(new Error('The Default account cannot be removed'), { code: 'DEFAULT_ACCOUNT', status: 409 });
|
|
25205
|
-
const data = loadCodexAccounts();
|
|
25206
|
-
const idx = data.accounts.findIndex((a) => a.id === id);
|
|
25207
|
-
if (idx === -1) throw Object.assign(new Error('Unknown Codex account'), { code: 'ACCOUNT_NOT_FOUND', status: 404 });
|
|
25208
|
-
const reason = inUse(id);
|
|
25209
|
-
if (reason) throw Object.assign(new Error(reason), { code: 'ACCOUNT_IN_USE', status: 409 });
|
|
25210
|
-
const [removed] = data.accounts.splice(idx, 1);
|
|
25211
|
-
saveCodexAccounts(data);
|
|
25212
|
-
try {
|
|
25213
|
-
if (removed.home && removed.home !== CODEX_DEFAULT_HOME && removed.home.startsWith(CODEX_ACCOUNTS_ROOT)) rmSync(removed.home, { recursive: true, force: true });
|
|
25214
|
-
} catch (e) { console.warn('[codex-accounts] Failed to delete home:', e.message); }
|
|
25215
|
-
return { removed: id };
|
|
25216
|
-
},
|
|
25217
|
-
};
|
|
25758
|
+
// The one Codex accounts instance (the sidepanel's, lib/codex-accounts.js): the brain picker, the
|
|
25759
|
+
// catalog, worker dispatch and the panel show one account state.
|
|
25760
|
+
const codexAccounts = _codexAccounts();
|
|
25761
|
+
// A Codex worker whose dispatch names no account runs under this one (Assistant → accounts).
|
|
25762
|
+
_assistantCodexWorkerAccount = () => assistantConfigStore.workers().codexAccount;
|
|
25218
25763
|
const createLoginTerminal = async ({ env = {}, cwd = null, profile = 'claude-code' } = {}) => {
|
|
25219
25764
|
const terminalSessionId = createTerminalSession(profile, 120, 30, cwd || getHomePath(), { extraEnv: env });
|
|
25220
25765
|
broadcastSync({ type: 'terminal:session-created', sessionId: terminalSessionId, profile });
|
|
25221
25766
|
return { terminalSessionId, profile };
|
|
25222
25767
|
};
|
|
25223
25768
|
app.use('/api/assistant', createAssistantApi({
|
|
25769
|
+
accountUsage,
|
|
25224
25770
|
dispatcher: _assistantDispatcher, runtime: _assistantRuntime, claudeAccounts, codexAccounts, buildCatalog, createLoginTerminal,
|
|
25771
|
+
// Claude accounts are the sidepanel's: sign-in is Claude Code's own `auth login` in a SynaBun terminal
|
|
25772
|
+
// under the account's config directory, a mode change ends the account's idle sessions first, and an
|
|
25773
|
+
// account a panel tab, the brain, a run or the worker choice uses cannot be removed.
|
|
25774
|
+
claudeSignIn: (accountId) => providerPanelTerminalAuth.start('claude', accountId),
|
|
25775
|
+
claudeSetMode: (accountId, mode) => panelClaudeAuth().setMode(accountId, mode),
|
|
25776
|
+
claudeAccountInUse,
|
|
25777
|
+
// Codex accounts are the sidepanel's too: sign-in is `codex login` in a SynaBun terminal under the
|
|
25778
|
+
// account's CODEX_HOME, a mode change restarts its idle panel processes, and an account something uses stays.
|
|
25779
|
+
codexSignIn: (accountId) => providerPanelTerminalAuth.start('codex', accountId),
|
|
25780
|
+
codexSetMode: (accountId, mode) => codexAccountAuth().setMode(accountId, mode),
|
|
25781
|
+
codexAccountInUse: (accountId) => codexAccountInUse(accountId),
|
|
25225
25782
|
broadcastSync, isGuestRequest, log: (tag, message) => assistantLog(tag, message),
|
|
25226
25783
|
assistantRouter, catalog: assistantCatalog, configStore: assistantConfigStore, taskClasses: TASK_CLASS_META,
|
|
25227
25784
|
pricing: assistantPricing,
|
|
@@ -25233,6 +25790,10 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25233
25790
|
userContext: assistantUserContext,
|
|
25234
25791
|
userContextError: () => assistantUserContextError,
|
|
25235
25792
|
defaultProject: () => assistantDetectProject(assistantConfigStore.read().defaultBrain?.cwd || PACKAGE_ROOT) || 'global',
|
|
25793
|
+
// whatsapp_send_file (POST /api/assistant/whatsapp/send-file): the WhatsApp service, built after this
|
|
25794
|
+
// router (startWhatsAppLink), and the port for that route's localhost Host check.
|
|
25795
|
+
whatsapp: () => _whatsapp,
|
|
25796
|
+
port: () => Number(PORT),
|
|
25236
25797
|
}));
|
|
25237
25798
|
const shutdownAssistant = () => {
|
|
25238
25799
|
try { assistantRouter.shutdown(); } catch {}
|
|
@@ -25241,6 +25802,8 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25241
25802
|
try { _assistantRuntime?.shutdown?.(); } catch {}
|
|
25242
25803
|
try { _assistantDispatcher?.shutdown?.(); } catch {}
|
|
25243
25804
|
try { _greeting?.shutdown?.(); } catch {}
|
|
25805
|
+
// The sessions' budget splits, written now (the planner debounces its writes).
|
|
25806
|
+
try { assistantBudget.flush(); } catch {}
|
|
25244
25807
|
// Usage rows whose append failed get their last try (synchronous: an 'exit' handler runs it too).
|
|
25245
25808
|
try { assistantUsage.flush(); } catch {}
|
|
25246
25809
|
};
|
|
@@ -25472,6 +26035,7 @@ function buildExecMcpEnvOverrides(loopState) {
|
|
|
25472
26035
|
function prepareExecWrapperFile(terminalSessionId, loopState, taskFile, sentinel) {
|
|
25473
26036
|
const config = loadCliConfig();
|
|
25474
26037
|
const profile = loopState.profile;
|
|
26038
|
+
if (profile === 'codex') ensureCanonicalCodexConfig({ strict: true });
|
|
25475
26039
|
const mcpEnvOverrides = buildExecMcpEnvOverrides(loopState);
|
|
25476
26040
|
|
|
25477
26041
|
// Surface the exact pins being injected per exec iteration so a stripped/missing
|
|
@@ -26212,6 +26776,38 @@ function handleSessionMonitorWebSocket(ws) {
|
|
|
26212
26776
|
ws.on('error', () => sessionMonitorClients.delete(ws));
|
|
26213
26777
|
}
|
|
26214
26778
|
|
|
26779
|
+
const providerPanelTerminalAuth = createProviderPanelTerminalAuth({
|
|
26780
|
+
resolveSpec: (provider, accountId) => providerLoginSpec({
|
|
26781
|
+
provider, accountId, claudeAccounts: _panelClaudeAccounts(), codexAccount: findCodexAccount,
|
|
26782
|
+
claudeExecutable: getClaudeSdkExecutable, codexExecutable: getCodexBin,
|
|
26783
|
+
env: withPathEnv(process.env, getAugmentedPath()),
|
|
26784
|
+
}),
|
|
26785
|
+
createSession: createTerminalSession,
|
|
26786
|
+
whenSpawned: id => terminalHost.whenSpawned(id),
|
|
26787
|
+
killSession: id => { try { terminalSessions.get(id)?.pty.kill(); } catch {} },
|
|
26788
|
+
// A sign-in that ended: ask the CLI what the account now holds. A subscription it confirms is what the
|
|
26789
|
+
// account uses from now on (every window hears it through claude:accounts-changed / codex:accounts-changed).
|
|
26790
|
+
// A Codex app-server reads its login only when it starts: the account's idle ones restart.
|
|
26791
|
+
onSettled: async (provider, accountId, status) => {
|
|
26792
|
+
if (provider === 'codex') {
|
|
26793
|
+
try {
|
|
26794
|
+
const accounts = _codexAccounts();
|
|
26795
|
+
await accounts.refreshStatus([accountId], { force: true });
|
|
26796
|
+
if (status === 'exited' && accounts.describe(accountId)?.subscription?.signedIn === true) accounts.noteSubscriptionSignIn(accountId);
|
|
26797
|
+
codexAccountStoreChanged(accountId);
|
|
26798
|
+
} catch {}
|
|
26799
|
+
return;
|
|
26800
|
+
}
|
|
26801
|
+
if (provider !== 'claude') return;
|
|
26802
|
+
try {
|
|
26803
|
+
const accounts = _panelClaudeAccounts();
|
|
26804
|
+
await accounts.refreshStatus([accountId], { force: true });
|
|
26805
|
+
if (status === 'exited' && accounts.describe(accountId)?.subscription?.signedIn === true) accounts.noteSubscriptionSignIn(accountId);
|
|
26806
|
+
} catch {}
|
|
26807
|
+
},
|
|
26808
|
+
});
|
|
26809
|
+
registerProviderPanelTerminalAuthRoutes(app, { guard: panelAuthOwnerGuard, auth: providerPanelTerminalAuth });
|
|
26810
|
+
|
|
26215
26811
|
// ── Terminal REST endpoints ──
|
|
26216
26812
|
|
|
26217
26813
|
app.get('/api/terminal/sessions', (req, res) => {
|
|
@@ -26272,6 +26868,7 @@ app.delete('/api/terminal/sessions/:id', (req, res) => {
|
|
|
26272
26868
|
const session = terminalSessions.get(req.params.id);
|
|
26273
26869
|
if (!session) return res.status(404).json({ error: 'Session not found' });
|
|
26274
26870
|
const wasClaudeCode = session.profile === 'claude-code';
|
|
26871
|
+
session.onProviderAuthClosed?.();
|
|
26275
26872
|
session.pty.kill();
|
|
26276
26873
|
// Clean up temp files from image paste
|
|
26277
26874
|
if (session.tempFiles) {
|
|
@@ -26284,7 +26881,7 @@ app.delete('/api/terminal/sessions/:id', (req, res) => {
|
|
|
26284
26881
|
});
|
|
26285
26882
|
|
|
26286
26883
|
app.get('/api/terminal/profiles', (req, res) => {
|
|
26287
|
-
const profileIds = ['claude-code', 'codex', 'gemini', 'shell'];
|
|
26884
|
+
const profileIds = ['claude-code', 'codex', 'gemini', 'opencode', 'shell'];
|
|
26288
26885
|
const profiles = profileIds.map(id => {
|
|
26289
26886
|
const p = getTerminalProfile(id);
|
|
26290
26887
|
return { id, shell: p.shell, args: p.args };
|
|
@@ -26343,8 +26940,10 @@ let _gitResolved = false;
|
|
|
26343
26940
|
|
|
26344
26941
|
// Resolve git's absolute path at first use. On Windows, `where git` lists all matches;
|
|
26345
26942
|
// prefer .exe so spawnSync works without cmd.exe quoting issues. On Unix, use `command -v`.
|
|
26943
|
+
// A Windows PC without git: the git.exe of SynaBun's copy of Git for Windows (lib/git-bash/), asked each time,
|
|
26944
|
+
// since a copy that is still being fetched at the first call arrives later.
|
|
26346
26945
|
function resolveGitPath() {
|
|
26347
|
-
if (_gitResolved) return _gitExePath;
|
|
26946
|
+
if (_gitResolved) return _gitExePath || gitBash.gitExecutable();
|
|
26348
26947
|
_gitResolved = true;
|
|
26349
26948
|
try {
|
|
26350
26949
|
const cmd = IS_WINDOWS ? 'where git' : 'command -v git';
|
|
@@ -26361,7 +26960,7 @@ function resolveGitPath() {
|
|
|
26361
26960
|
console.warn('[git] resolution failed:', err?.message || String(err));
|
|
26362
26961
|
_gitExePath = null;
|
|
26363
26962
|
}
|
|
26364
|
-
return _gitExePath;
|
|
26963
|
+
return _gitExePath || gitBash.gitExecutable();
|
|
26365
26964
|
}
|
|
26366
26965
|
|
|
26367
26966
|
// Cross-platform git invocation. Uses resolved absolute path when possible (spawnable
|
|
@@ -26820,16 +27419,16 @@ app.post('/api/git/commit', editorOwnerGuard, (req, res) => {
|
|
|
26820
27419
|
for (const f of files) {
|
|
26821
27420
|
if (typeof f !== 'string' || f.includes('..')) return res.status(400).json({ error: 'Invalid file path' });
|
|
26822
27421
|
}
|
|
26823
|
-
const result = spawnSync('git', ['add', '--', ...files], { cwd: dir, stdio: 'pipe', timeout: 10000 });
|
|
27422
|
+
const result = spawnSync('git', ['add', '--', ...files], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 10000 });
|
|
26824
27423
|
if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
|
|
26825
27424
|
} else {
|
|
26826
27425
|
// Stage all changes
|
|
26827
|
-
const result = spawnSync('git', ['add', '-A'], { cwd: dir, stdio: 'pipe', timeout: 10000 });
|
|
27426
|
+
const result = spawnSync('git', ['add', '-A'], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 10000 });
|
|
26828
27427
|
if (result.status !== 0) return res.status(500).json({ error: result.stderr?.toString?.()?.trim() || 'Stage failed' });
|
|
26829
27428
|
}
|
|
26830
27429
|
|
|
26831
27430
|
// Commit
|
|
26832
|
-
const result = spawnSync('git', ['commit', '-m', message.trim()], { cwd: dir, stdio: 'pipe', timeout: 15000 });
|
|
27431
|
+
const result = spawnSync('git', ['commit', '-m', message.trim()], { cwd: dir, env: withGitFallback(process.env), stdio: 'pipe', timeout: 15000 });
|
|
26833
27432
|
if (result.status !== 0) {
|
|
26834
27433
|
const stderr = result.stderr?.toString?.()?.trim() || '';
|
|
26835
27434
|
const stdout = result.stdout?.toString?.()?.trim() || '';
|
|
@@ -26985,14 +27584,10 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
|
|
|
26985
27584
|
return res.json({ ok: true, message: '', source: 'heuristic', summary: 'No changes to commit' });
|
|
26986
27585
|
}
|
|
26987
27586
|
|
|
26988
|
-
// Try Claude CLI
|
|
26989
|
-
|
|
26990
|
-
try {
|
|
26991
|
-
execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { stdio: 'pipe', timeout: 3000 });
|
|
26992
|
-
claudeAvailable = true;
|
|
26993
|
-
} catch {}
|
|
27587
|
+
// Try Claude CLI: the one the panels use (lib/cli-resolver.js), started without a shell.
|
|
27588
|
+
const claudeLaunch = cliResolver.launcher('claude-code');
|
|
26994
27589
|
|
|
26995
|
-
if (
|
|
27590
|
+
if (claudeLaunch.spec && claudeLaunch.state !== 'broken') {
|
|
26996
27591
|
try {
|
|
26997
27592
|
const prompt = [
|
|
26998
27593
|
'Generate a single-line git commit message for these changes.',
|
|
@@ -27007,14 +27602,16 @@ app.post('/api/git/generate-message', editorOwnerGuard, async (req, res) => {
|
|
|
27007
27602
|
untrackedList ? '\n## New untracked files\n' + untrackedList.split('\n').slice(0, 20).join('\n') : '',
|
|
27008
27603
|
].join('\n');
|
|
27009
27604
|
|
|
27010
|
-
const
|
|
27011
|
-
const result =
|
|
27605
|
+
const launch = spawnArgsFor(claudeLaunch.spec, ['-p', '--model', 'sonnet']);
|
|
27606
|
+
const result = spawnSync(launch.command, launch.args, {
|
|
27607
|
+
...launch.options,
|
|
27012
27608
|
input: prompt,
|
|
27013
27609
|
cwd: dir,
|
|
27014
27610
|
timeout: 30000,
|
|
27015
27611
|
maxBuffer: 64 * 1024,
|
|
27016
27612
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27017
|
-
|
|
27613
|
+
env: withPathEnv(process.env, getAugmentedPath()),
|
|
27614
|
+
windowsHide: true,
|
|
27018
27615
|
});
|
|
27019
27616
|
|
|
27020
27617
|
const output = (result.stdout || '').toString().trim();
|
|
@@ -27104,27 +27701,25 @@ app.put('/api/cli/config', (req, res) => {
|
|
|
27104
27701
|
}
|
|
27105
27702
|
});
|
|
27106
27703
|
|
|
27107
|
-
|
|
27704
|
+
// Settings > Terminal > Detect: what is installed, whatever Settings says now
|
|
27705
|
+
// (the button fills the field with it), found the way every launcher finds it
|
|
27706
|
+
// (lib/cli-resolver.js), with the OS PATH read afresh.
|
|
27707
|
+
app.post('/api/cli/detect/:profileId', async (req, res) => {
|
|
27108
27708
|
try {
|
|
27109
27709
|
const { profileId } = req.params;
|
|
27110
|
-
|
|
27111
|
-
const
|
|
27112
|
-
|
|
27113
|
-
|
|
27114
|
-
|
|
27115
|
-
|
|
27116
|
-
|
|
27117
|
-
|
|
27118
|
-
|
|
27119
|
-
|
|
27120
|
-
|
|
27121
|
-
|
|
27122
|
-
|
|
27123
|
-
const foundPath = result.split(/\r?\n/)[0].trim();
|
|
27124
|
-
res.json({ ok: true, found: true, path: foundPath });
|
|
27125
|
-
} catch {
|
|
27126
|
-
res.json({ ok: true, found: false, path: null });
|
|
27127
|
-
}
|
|
27710
|
+
if (!['claude-code', 'codex', 'gemini', 'opencode'].includes(profileId)) return res.status(400).json({ ok: false, error: 'Unknown profile' });
|
|
27711
|
+
const status = await cliResolver.resolve(profileId, { force: true, configured: null });
|
|
27712
|
+
res.json({
|
|
27713
|
+
ok: true,
|
|
27714
|
+
found: status.state !== 'missing',
|
|
27715
|
+
path: status.path,
|
|
27716
|
+
version: status.version,
|
|
27717
|
+
state: status.state,
|
|
27718
|
+
foundBy: status.foundBy,
|
|
27719
|
+
pathStatus: cliPathManager.inspect(status),
|
|
27720
|
+
installSource: status.installSource,
|
|
27721
|
+
error: status.error,
|
|
27722
|
+
});
|
|
27128
27723
|
} catch (err) {
|
|
27129
27724
|
res.status(500).json({ ok: false, error: err.message });
|
|
27130
27725
|
}
|
|
@@ -27297,9 +27892,13 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
27297
27892
|
const command = (cliConfig[profile]?.command || '').trim();
|
|
27298
27893
|
if (!command) throw new Error(`No CLI command configured for ${profile}`);
|
|
27299
27894
|
|
|
27300
|
-
|
|
27895
|
+
// Named by its plain name in Settings: the CLI the panels use (lib/cli-resolver.js), a program started
|
|
27896
|
+
// without a shell; otherwise the Settings value as written.
|
|
27897
|
+
const found = CLI_DEFAULTS[profile] && command === CLI_DEFAULTS[profile].command ? cliResolver.launcher(profile) : null;
|
|
27898
|
+
const direct = found?.spec?.kind === 'direct' ? found.launchPath : null;
|
|
27899
|
+
let spawnCmd = direct || command;
|
|
27301
27900
|
let spawnArgs = [...sidecarConfig.args];
|
|
27302
|
-
let useShell = IS_WIN;
|
|
27901
|
+
let useShell = IS_WIN && !direct;
|
|
27303
27902
|
let tmpFile = null;
|
|
27304
27903
|
|
|
27305
27904
|
if (sidecarConfig.stdin) {
|
|
@@ -27313,7 +27912,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
27313
27912
|
const flag = spawnArgs[0]; // e.g. '-p', '-q'
|
|
27314
27913
|
spawnCmd = 'powershell.exe';
|
|
27315
27914
|
spawnArgs = ['-NoProfile', '-NonInteractive', '-Command',
|
|
27316
|
-
`$msg = Get-Content -Raw '${tmpFile}'; & '${command}' ${flag} $msg`];
|
|
27915
|
+
`$msg = Get-Content -Raw '${tmpFile}'; & '${String(direct || command).replace(/'/g, "''")}' ${flag} $msg`];
|
|
27317
27916
|
useShell = false;
|
|
27318
27917
|
} else {
|
|
27319
27918
|
// Unix: pass directly as the flag's value arg (no shell quoting issues)
|
|
@@ -27324,7 +27923,7 @@ function spawnSidecarProcess(linkId, sessionId, message) {
|
|
|
27324
27923
|
|
|
27325
27924
|
const child = spawn(spawnCmd, spawnArgs, {
|
|
27326
27925
|
cwd: session.cwd || process.cwd(),
|
|
27327
|
-
env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
27926
|
+
env: { ...withPathEnv(process.env, getAugmentedPath()), FORCE_COLOR: '0', NO_COLOR: '1' },
|
|
27328
27927
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27329
27928
|
shell: useShell,
|
|
27330
27929
|
timeout: LINK_SIDECAR_TIMEOUT_MS,
|
|
@@ -32868,6 +33467,17 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
32868
33467
|
// first session runs on it (checkClaudeCliSkew in lib/claude-model-catalog.js).
|
|
32869
33468
|
setTimeout(() => { checkClaudeCliSkew(getClaudeBin()).catch(() => {}); }, 1000);
|
|
32870
33469
|
|
|
33470
|
+
// Windows: the Git Bash every Claude Code gets (lib/git-bash/). Looked for now (the settings of the user's own
|
|
33471
|
+
// claude follow it); a PC with none, and an app that carries none, gets SynaBun's copy, fetched in the background.
|
|
33472
|
+
setTimeout(() => {
|
|
33473
|
+
try {
|
|
33474
|
+
gitBash.refresh();
|
|
33475
|
+
if (gitBash.status().state === 'missing') gitBash.ensure({ reason: 'first run' });
|
|
33476
|
+
// What an earlier run left in the data home (a stopped attempt's work folder, an older version), off the event loop.
|
|
33477
|
+
gitBash.housekeep().catch(() => {});
|
|
33478
|
+
} catch (err) { console.warn('[git-bash] start check failed:', err?.message || err); }
|
|
33479
|
+
}, 2500);
|
|
33480
|
+
|
|
32871
33481
|
// Warm the session_cache + session_fts tables so the hybrid search has data
|
|
32872
33482
|
// on first keystroke. Each pass is synchronous and holds the main thread, so
|
|
32873
33483
|
// they run one per tick (requests are answered in between), and each only
|
|
@@ -33815,6 +34425,7 @@ process.on('exit', () => {
|
|
|
33815
34425
|
try { terminalHost.killNow(); } catch {}
|
|
33816
34426
|
try { _whatsapp?.killNow(); } catch {}
|
|
33817
34427
|
try { tunnel.killNow(); } catch {}
|
|
34428
|
+
try { gitBash.cancel(); } catch {}
|
|
33818
34429
|
try { stopOpencodeServer(); } catch {}
|
|
33819
34430
|
try { editorCompletionServe.stop('exit'); } catch {}
|
|
33820
34431
|
try { _claudeBridge?.shutdownAllBridges?.(); } catch {}
|