synabun 2.0.0-beta → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/THIRD-PARTY-LICENSES.md +5 -0
- package/launcher.mjs +40 -0
- package/lib/claude-hooks.js +61 -8
- package/lib/client-config-repair.js +4 -3
- package/lib/data-home-migration.js +4 -0
- package/lib/desktop-pwa.js +206 -0
- package/lib/external-tools.js +221 -0
- package/lib/first-launch-protection.js +5 -0
- package/lib/packaged-runtime.js +110 -0
- package/lib/start-launcher.js +1309 -0
- package/neural-interface/i18n/en.json +41 -2
- package/neural-interface/i18n/pt-BR.json +41 -2
- package/neural-interface/lib/assistant-envelope.js +9 -6
- package/neural-interface/lib/augmented-path.js +23 -3
- package/neural-interface/lib/browser-language.js +83 -0
- package/neural-interface/lib/claude-agent-bridge.js +54 -147
- package/neural-interface/lib/claude-executable.js +241 -0
- package/neural-interface/lib/claude-model-catalog.js +52 -91
- package/neural-interface/lib/native-binary-runtime.js +69 -337
- package/neural-interface/lib/native-loop-providers.js +30 -44
- package/neural-interface/lib/offline-page.js +54 -0
- package/neural-interface/lib/session-title-generator.js +5 -0
- package/neural-interface/lib/synabun-update-plan.js +19 -0
- package/neural-interface/package-lock.json +0 -251
- package/neural-interface/public/install-app.html +31 -0
- package/neural-interface/public/manifest.json +2 -0
- package/neural-interface/public/offline.html +401 -66
- package/neural-interface/public/onboarding.html +252 -146
- package/neural-interface/public/shared/assistant/asst-mascot.js +12 -10
- package/neural-interface/public/shared/assistant/asst-rack-styles.js +0 -3
- package/neural-interface/public/shared/assistant/asst-render.js +58 -49
- package/neural-interface/public/shared/assistant/asst-styles.js +4 -2
- package/neural-interface/public/shared/command-runner-model.js +30 -0
- package/neural-interface/public/shared/html-shell.js +9 -5
- package/neural-interface/public/shared/pwa-install-page.js +34 -0
- package/neural-interface/public/shared/pwa-install.css +12 -0
- package/neural-interface/public/shared/pwa-install.js +28 -0
- package/neural-interface/public/shared/settings/settings-rules.js +160 -0
- package/neural-interface/public/shared/start-bridge.js +583 -0
- package/neural-interface/public/shared/styleguide/sg-colors.js +5 -2
- package/neural-interface/public/shared/styleguide/sg-import-export.js +4 -3
- package/neural-interface/public/shared/styleguide/sg-kit.js +4 -1
- package/neural-interface/public/shared/styleguide/sg-panel.js +9 -1
- package/neural-interface/public/shared/styleguide/sg-typography.js +1 -0
- package/neural-interface/public/shared/styles.css +801 -392
- package/neural-interface/public/shared/term-ready.js +307 -0
- package/neural-interface/public/shared/ui-claude-panel.js +3 -3
- package/neural-interface/public/shared/ui-command-runner.js +251 -468
- package/neural-interface/public/shared/ui-judgments.js +1 -1
- package/neural-interface/public/shared/ui-loading.js +221 -10
- package/neural-interface/public/shared/ui-menubar.js +11 -27
- package/neural-interface/public/shared/ui-settings.js +36 -59
- package/neural-interface/public/shared/ui-terminal.js +130 -144
- package/neural-interface/public/sw.js +3 -1
- package/neural-interface/public/variant/2d/main.js +2 -1
- package/neural-interface/public/variant/3d/main.js +2 -1
- package/neural-interface/scripts/rebuild-pty.js +15 -12
- package/neural-interface/server.js +220 -185
- package/package.json +13 -1
- package/postinstall.js +11 -81
- package/setup.js +72 -4
- package/docs/browser-v2.md +0 -109
- package/docs/hook-integrity.md +0 -89
- package/docs/judgments.md +0 -182
- package/docs/style-guide.md +0 -246
- package/scripts/audit-claude-hook-usage.mjs +0 -12
- package/scripts/audit-memory-duplicates.mjs +0 -44
- package/scripts/measure-claude-hook-overhead.mjs +0 -77
- package/scripts/recover-user-config.mjs +0 -254
|
@@ -10,7 +10,7 @@ import { findSessionTranscript, sessionProjects, confineArtifact, confineSubPath
|
|
|
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
12
|
import { claudeLegacyPricing, claudeUsageCostUsd } from './lib/assistant-pricing.js';
|
|
13
|
-
import { HOOK_SCRIPTS, hookDefinitionsForEvent, hookCommandString, isSynaBunHookCommand, sweepSettingsHooks, ensureHookRoot } from '../lib/claude-hooks.js';
|
|
13
|
+
import { HOOK_SCRIPTS, hookDefinitionsForEvent, hookCommandString, hookInstallBlocker, hookInstallPlan, isSynaBunHookCommand, sweepSettingsHooks, ensureHookRoot } from '../lib/claude-hooks.js';
|
|
14
14
|
import { browserV2Enabled, browserRequestMiddleware, assertBrowserRequestActive, browserActionTimeout, markBrowserActionStarted, runBrowserRequest, cancellableBrowserDelay, measureBrowserPhase, createEngagementScheduler, engagementTargetSelector, isEngagementWriteSelector, isXShortcutPublish, isXScriptedWrite } from './lib/browser-execution.js';
|
|
15
15
|
import { describeLocatorMatches, healLocatorByText, validNthMatch, captureTargetIdentity, matchesTargetIdentity } from './lib/browser-targets.js';
|
|
16
16
|
import { getInteractiveHints, collectSemanticContext, buildAssist, describeTargetFacts, verifyHealTarget, createAssistContextStore } from './lib/browser-semantic-context.js';
|
|
@@ -22,6 +22,7 @@ import { createMemoryMapService, createMemoryMapApi } from './lib/memory-map-api
|
|
|
22
22
|
import { createAiSnapshotStore, settleBrowserPage } from './lib/browser-snapshots.js';
|
|
23
23
|
import { runBrowserExtraction, readBrowserContentHtml } from './lib/browser-extraction.js';
|
|
24
24
|
import { attachConsoleBuffer, readConsoleBuffer, parseConsoleSince } from './lib/browser-console.js';
|
|
25
|
+
import { resolveBrowserLanguage, validateBrowserLanguageConfig } from './lib/browser-language.js';
|
|
25
26
|
import { parseScreenshotOptions, captureScreenshot, screenshotPathRefusal, defaultScreenshotPath, writeScreenshotFile } from './lib/browser-screenshot.js';
|
|
26
27
|
import { memoryRevision } from '../mcp-server/dist/services/memory-maintenance.js';
|
|
27
28
|
import { startGatedMemoryMaintenance } from './lib/memory-timers.js';
|
|
@@ -155,10 +156,7 @@ import {
|
|
|
155
156
|
resolveAutomationOverrides,
|
|
156
157
|
validAutomationContextMode,
|
|
157
158
|
} from './public/shared/automation-context.js';
|
|
158
|
-
import {
|
|
159
|
-
claudeRuntime,
|
|
160
|
-
ensureVendoredExecutables,
|
|
161
|
-
} from './lib/native-binary-runtime.js';
|
|
159
|
+
import { ensureVendoredExecutables } from './lib/native-binary-runtime.js';
|
|
162
160
|
import {
|
|
163
161
|
buildExecInvocation,
|
|
164
162
|
execWrapperExtension,
|
|
@@ -225,10 +223,11 @@ import { generateSessionTitle } from './lib/session-title-generator.js';
|
|
|
225
223
|
import {
|
|
226
224
|
CLAUDE_EFFORT_LEVELS,
|
|
227
225
|
CLAUDE_FALLBACK_MODELS,
|
|
228
|
-
alignedClaudeExecutable,
|
|
229
226
|
checkClaudeCliSkew,
|
|
230
227
|
discoverClaudeModels,
|
|
228
|
+
shellCommand,
|
|
231
229
|
} from './lib/claude-model-catalog.js';
|
|
230
|
+
import { resolveClaudeLauncher, resolveClaudeSdkExecutable } from './lib/claude-executable.js';
|
|
232
231
|
import { getAugmentedPath } from './lib/augmented-path.js';
|
|
233
232
|
import { semverNewer, synabunReleaseChannel } from './lib/synabun-version.js';
|
|
234
233
|
import { selectSynabunUpdate, buildSynabunInstallPlan, refuseStaleUpdateClick } from './lib/synabun-update-plan.js';
|
|
@@ -256,8 +255,12 @@ import {
|
|
|
256
255
|
convertPctInPlace as wbConvertPctInPlace,
|
|
257
256
|
} from '../mcp-server/dist/services/whiteboard-geometry.js';
|
|
258
257
|
import { ensureProjectCategories } from '../hooks/claude-code/shared.mjs';
|
|
259
|
-
import { getDataHome, getDataHomeDiagnostics, pathIsInside } from '../lib/paths.js';
|
|
258
|
+
import { getDataHome, getDataHomeDiagnostics, isGlobalInstall, pathIsInside } from '../lib/paths.js';
|
|
260
259
|
import { launcherLogPath } from '../lib/first-launch-protection.js';
|
|
260
|
+
import { registerStartLauncher, startLauncherState } from '../lib/start-launcher.js';
|
|
261
|
+
import { durableEntry, mcpStdioCommand, packagedRuntime } from '../lib/packaged-runtime.js';
|
|
262
|
+
import { pruneExternalTools } from '../lib/external-tools.js';
|
|
263
|
+
import { renderOfflinePage } from './lib/offline-page.js';
|
|
261
264
|
import {
|
|
262
265
|
getDb, closeDb, getDbPath, getEmbedding, getEmbeddingBatch, getEmbeddingDims, warmupEmbeddings,
|
|
263
266
|
encodeVector, decodeVector, cosineSimilarity,
|
|
@@ -295,11 +298,11 @@ const execFileAsync = promisify(execFile);
|
|
|
295
298
|
const __filename = fileURLToPath(import.meta.url);
|
|
296
299
|
const __dirname = dirname(__filename);
|
|
297
300
|
|
|
298
|
-
// Restore execute permissions on
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
301
|
+
// Restore execute permissions on SynaBun's own native helpers (node-pty's
|
|
302
|
+
// spawn-helper). npm and cpSync drop the execute bit, and a checkout with
|
|
303
|
+
// core.filemode=false materializes them 0644, so terminals failed to spawn.
|
|
304
|
+
// The agent tools are not here: Claude Code and Codex are the user's own
|
|
305
|
+
// installations (lib/claude-executable.js, getCodexBin).
|
|
303
306
|
//
|
|
304
307
|
// This sweeps ALL platform directories, not just this host's: a vendored
|
|
305
308
|
// checkout can carry several, and repairing only the current one left e.g.
|
|
@@ -746,13 +749,34 @@ app.get('/', (req, res, next) => {
|
|
|
746
749
|
res.redirect('/onboarding.html');
|
|
747
750
|
});
|
|
748
751
|
|
|
749
|
-
//
|
|
752
|
+
// The Start Server button on the offline page hands the OS a synabun:// link.
|
|
753
|
+
// Whether the handler for it is registered is read once (after the boot-time
|
|
754
|
+
// repair below) and told to the pages, which remember it for when we are down.
|
|
755
|
+
let _startLauncherState = null;
|
|
756
|
+
function getStartLauncherState() {
|
|
757
|
+
if (_startLauncherState === null) {
|
|
758
|
+
try { _startLauncherState = startLauncherState({ packageRoot: PACKAGE_ROOT }); } catch { _startLauncherState = 'missing'; }
|
|
759
|
+
}
|
|
760
|
+
return _startLauncherState;
|
|
761
|
+
}
|
|
762
|
+
// Set when this server runs inside a packaged application (packaging/): the
|
|
763
|
+
// code is then replaced by installing a newer build, never by npm or git.
|
|
764
|
+
const PACKAGED_RUNTIME = packagedRuntime();
|
|
765
|
+
const INSTALL_KIND = PACKAGED_RUNTIME ? 'app' : isGlobalInstall() ? 'npm' : 'git';
|
|
766
|
+
// What a person types to start a packaged application: its entry executable,
|
|
767
|
+
// when that is a path that will still be there once this run is over.
|
|
768
|
+
const START_ENTRY = durableEntry(PACKAGED_RUNTIME) || '';
|
|
769
|
+
|
|
770
|
+
// Serve offline.html with what it needs once we are down: the install path,
|
|
771
|
+
// how it was installed, the launcher's state and the bridge code itself.
|
|
750
772
|
app.get('/offline.html', (req, res) => {
|
|
751
773
|
const html = readFileSync(join(__dirname, 'public', 'offline.html'), 'utf-8');
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
774
|
+
let bridgeSource = '';
|
|
775
|
+
try { bridgeSource = readFileSync(join(__dirname, 'public', 'shared', 'start-bridge.js'), 'utf-8'); } catch {}
|
|
776
|
+
res.type('html').send(renderOfflinePage(html, {
|
|
777
|
+
bridgeSource,
|
|
778
|
+
facts: { projectDir: PACKAGE_ROOT, install: INSTALL_KIND, launcher: getStartLauncherState(), entry: START_ENTRY },
|
|
779
|
+
}));
|
|
756
780
|
});
|
|
757
781
|
|
|
758
782
|
// The standalone Claude chat page was retired (the sidepanel replaced it): its
|
|
@@ -2428,6 +2452,15 @@ function cancelUpdateHandoff(handoffPath, reason) {
|
|
|
2428
2452
|
app.post('/api/system/run-update', async (req, res) => {
|
|
2429
2453
|
let preparedHandoffPath = null;
|
|
2430
2454
|
try {
|
|
2455
|
+
// Nothing is ever installed into the resources of a packaged application.
|
|
2456
|
+
if (PACKAGED_RUNTIME) {
|
|
2457
|
+
const openUrl = `${SYNABUN_GITHUB_REPO_URL}/releases`;
|
|
2458
|
+
return res.status(409).json({
|
|
2459
|
+
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}`,
|
|
2460
|
+
packaged: true,
|
|
2461
|
+
openUrl,
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2431
2464
|
// Auto-restart is always on — running the click-to-update flow implies the
|
|
2432
2465
|
// user wants the new version live without a manual relaunch step.
|
|
2433
2466
|
const autoRestart = true;
|
|
@@ -3207,88 +3240,53 @@ function _heartbeatLock(sessionId, windowId) {
|
|
|
3207
3240
|
}
|
|
3208
3241
|
}
|
|
3209
3242
|
|
|
3210
|
-
// Cache resolved claude binary path (avoids
|
|
3243
|
+
// Cache resolved claude binary path (avoids a PATH walk on every query)
|
|
3211
3244
|
let _claudeBinPath = null;
|
|
3212
|
-
|
|
3245
|
+
// A tool that was not found is looked for again after a moment: installing it
|
|
3246
|
+
// is what a user does next, and that must not take a restart of SynaBun.
|
|
3247
|
+
const MISSING_TOOL_RECHECK_MS = 15_000;
|
|
3248
|
+
let _claudeBinMissingAt = 0;
|
|
3249
|
+
|
|
3250
|
+
// The Claude Code a terminal would start: the command from cli-config.json
|
|
3251
|
+
// (Settings > Terminal), else the first `claude` on the augmented PATH. Always
|
|
3252
|
+
// the user's own installation: never a file inside SynaBun, never a command
|
|
3253
|
+
// from a node_modules/.bin folder (lib/claude-executable.js).
|
|
3213
3254
|
function getClaudeBin() {
|
|
3214
|
-
if (_claudeBinPath) return _claudeBinPath;
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
// Might be a bare command in PATH — try which/where
|
|
3226
|
-
try {
|
|
3227
|
-
const envWithPath = { ...process.env, PATH: getAugmentedPath() };
|
|
3228
|
-
let lookup;
|
|
3229
|
-
if (process.platform === 'win32') {
|
|
3230
|
-
const lines = execSync(`where "${userCmd}"`, { encoding: 'utf-8', env: envWithPath })
|
|
3231
|
-
.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
3232
|
-
lookup = lines.find(l => /\.(cmd|exe|bat|ps1)$/i.test(l)) || lines[0];
|
|
3233
|
-
} else {
|
|
3234
|
-
lookup = execSync(`which "${userCmd}"`, { encoding: 'utf-8', env: envWithPath }).trim();
|
|
3235
|
-
}
|
|
3236
|
-
if (lookup) { _claudeBinPath = lookup; return _claudeBinPath; }
|
|
3237
|
-
} catch {}
|
|
3238
|
-
}
|
|
3239
|
-
} catch {}
|
|
3240
|
-
// 1. Try bundled @anthropic-ai/claude-code in node_modules/.bin
|
|
3241
|
-
const bundledBase = resolve(__dirname, 'node_modules', '.bin', 'claude');
|
|
3242
|
-
// On Windows npm creates .cmd shims — check for those too
|
|
3243
|
-
const bundledCandidates = process.platform === 'win32'
|
|
3244
|
-
? [bundledBase + '.cmd', bundledBase + '.ps1', bundledBase]
|
|
3245
|
-
: [bundledBase];
|
|
3246
|
-
for (const bundled of bundledCandidates) {
|
|
3247
|
-
if (!existsSync(bundled)) continue;
|
|
3248
|
-
const st = statSync(bundled);
|
|
3249
|
-
// npm sometimes creates a plain text stub instead of a symlink — resolve through to cli.js
|
|
3250
|
-
if (st.isFile() && st.size < 256) {
|
|
3251
|
-
const target = readFileSync(bundled, 'utf-8').trim();
|
|
3252
|
-
if (target && !target.startsWith('#')) {
|
|
3253
|
-
const resolved = resolve(__dirname, 'node_modules', '.bin', target);
|
|
3254
|
-
if (existsSync(resolved)) {
|
|
3255
|
-
if (process.platform !== 'win32') {
|
|
3256
|
-
try { const rs = statSync(resolved); if (!(rs.mode & 0o111)) chmodSync(resolved, rs.mode | 0o755); } catch {}
|
|
3257
|
-
}
|
|
3258
|
-
_claudeBinPath = resolved;
|
|
3259
|
-
return _claudeBinPath;
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
}
|
|
3263
|
-
// Normal symlink or executable — ensure +x
|
|
3264
|
-
if (process.platform !== 'win32' && !(st.mode & 0o111)) {
|
|
3265
|
-
try { chmodSync(bundled, st.mode | 0o755); } catch {}
|
|
3266
|
-
}
|
|
3267
|
-
_claudeBinPath = bundled;
|
|
3268
|
-
return _claudeBinPath;
|
|
3269
|
-
}
|
|
3270
|
-
// 2. Fall back to global install via which/where (with augmented PATH)
|
|
3271
|
-
const envWithPath = { ...process.env, PATH: getAugmentedPath() };
|
|
3272
|
-
if (process.platform === 'win32') {
|
|
3273
|
-
try {
|
|
3274
|
-
const lines = execSync('where claude', { encoding: 'utf-8', env: envWithPath })
|
|
3275
|
-
.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
|
|
3276
|
-
// Prefer .cmd/.exe/.bat/.ps1 over the bare extensionless shim (which is a sh script Windows can't spawn)
|
|
3277
|
-
_claudeBinPath = lines.find(l => /\.(cmd|exe|bat|ps1)$/i.test(l)) || lines[0] || null;
|
|
3278
|
-
} catch { _claudeBinPath = null; }
|
|
3279
|
-
} else {
|
|
3280
|
-
try { _claudeBinPath = execSync('which claude', { encoding: 'utf-8', env: envWithPath }).trim(); }
|
|
3281
|
-
catch { _claudeBinPath = null; }
|
|
3282
|
-
}
|
|
3283
|
-
if (!_claudeBinPath) {
|
|
3284
|
-
console.warn('[claude-skin] Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code');
|
|
3255
|
+
if (_claudeBinPath && _claudeBinPath !== 'claude') return _claudeBinPath;
|
|
3256
|
+
if (_claudeBinPath && Date.now() - _claudeBinMissingAt < MISSING_TOOL_RECHECK_MS) return _claudeBinPath;
|
|
3257
|
+
let configured = '';
|
|
3258
|
+
try { configured = loadCliConfig()['claude-code']?.command?.trim() || ''; } catch {}
|
|
3259
|
+
const found = resolveClaudeLauncher({ configured, isInsideSynabun });
|
|
3260
|
+
for (const rejected of found.rejected) {
|
|
3261
|
+
console.warn(`[claude-skin] Ignoring Claude CLI inside the SynaBun install: ${rejected}`);
|
|
3262
|
+
}
|
|
3263
|
+
if (!found.path) {
|
|
3264
|
+
if (!_claudeBinMissingAt) console.warn('[claude-skin] Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code');
|
|
3265
|
+
_claudeBinMissingAt = Date.now();
|
|
3285
3266
|
_claudeBinPath = 'claude'; // last resort — will fail at spawn time with a clear error
|
|
3267
|
+
return _claudeBinPath;
|
|
3286
3268
|
}
|
|
3269
|
+
if (_claudeBinMissingAt) console.log(`[claude-skin] Claude CLI found: ${found.path}`);
|
|
3270
|
+
_claudeBinMissingAt = 0;
|
|
3271
|
+
_claudeBinPath = found.path;
|
|
3287
3272
|
return _claudeBinPath;
|
|
3288
3273
|
}
|
|
3289
3274
|
|
|
3275
|
+
// The executable every Agent SDK call is given (pathToClaudeCodeExecutable):
|
|
3276
|
+
// the override from cli-config.json ("claude-skin": { "sdkExecutable": "…" }),
|
|
3277
|
+
// else the installation above, followed through an npm command file on
|
|
3278
|
+
// Windows. `path: null` when no Claude Code is installed; SynaBun carries none.
|
|
3279
|
+
function getClaudeSdkExecutable() {
|
|
3280
|
+
return resolveClaudeSdkExecutable({
|
|
3281
|
+
launcher: getClaudeBin(),
|
|
3282
|
+
sdkExecutable: _readClaudeSkinConfig().sdkExecutable || null,
|
|
3283
|
+
isInsideSynabun,
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3290
3287
|
let _codexBinPath = null;
|
|
3291
3288
|
let _codexBinSource = 'missing';
|
|
3289
|
+
let _codexBinMissingAt = 0;
|
|
3292
3290
|
let _nativeCodexBinPath = null;
|
|
3293
3291
|
// The SDK is statically imported at module load; reaching this point is the
|
|
3294
3292
|
// authoritative capability check. Its package.json is intentionally not an
|
|
@@ -3337,10 +3335,11 @@ function resolveWindowsGlobalCodexRuntime() {
|
|
|
3337
3335
|
}
|
|
3338
3336
|
|
|
3339
3337
|
function getCodexBin() {
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
//
|
|
3343
|
-
//
|
|
3338
|
+
// (Not found is looked for again after a moment, like Claude Code above.)
|
|
3339
|
+
if (_codexBinPath && (_codexBinSource !== 'missing' || Date.now() - _codexBinMissingAt < MISSING_TOOL_RECHECK_MS)) return _codexBinPath;
|
|
3340
|
+
// All Codex surfaces follow the user's trusted global install. SynaBun
|
|
3341
|
+
// carries no Codex of its own (lib/external-tools.js): the SDK is always
|
|
3342
|
+
// handed this path, and without one Codex is reported as not installed.
|
|
3344
3343
|
const globalPath = getAugmentedPath().split(delimiter)
|
|
3345
3344
|
.filter((entry) => !/[\\/]node_modules[\\/]\.bin[\\/]?$/i.test(entry))
|
|
3346
3345
|
.join(delimiter);
|
|
@@ -3360,14 +3359,17 @@ function getCodexBin() {
|
|
|
3360
3359
|
try { _codexBinPath = execSync('which codex', { encoding: 'utf-8', env: envWithPath }).trim(); }
|
|
3361
3360
|
catch { _codexBinPath = null; }
|
|
3362
3361
|
}
|
|
3363
|
-
if (_codexBinPath && !isInsideSynabun(_codexBinPath)) {
|
|
3362
|
+
if (_codexBinPath && _codexBinPath !== 'codex' && !isInsideSynabun(_codexBinPath)) {
|
|
3363
|
+
if (_codexBinMissingAt) console.log(`[codex-skin] Codex CLI found: ${_codexBinPath}`);
|
|
3364
|
+
_codexBinMissingAt = 0;
|
|
3364
3365
|
_codexBinSource = 'global';
|
|
3365
3366
|
return _codexBinPath;
|
|
3366
3367
|
}
|
|
3367
|
-
if (
|
|
3368
|
-
console.warn(`[codex-skin] Ignoring Codex CLI inside the SynaBun install: ${_codexBinPath}`);
|
|
3368
|
+
if (!_codexBinMissingAt) {
|
|
3369
|
+
if (_codexBinPath && _codexBinPath !== 'codex') console.warn(`[codex-skin] Ignoring Codex CLI inside the SynaBun install: ${_codexBinPath}`);
|
|
3370
|
+
console.warn('[codex-skin] Codex CLI not found. Install: npm install -g @openai/codex');
|
|
3369
3371
|
}
|
|
3370
|
-
|
|
3372
|
+
_codexBinMissingAt = Date.now();
|
|
3371
3373
|
_codexBinPath = 'codex';
|
|
3372
3374
|
_codexBinSource = 'missing';
|
|
3373
3375
|
return _codexBinPath;
|
|
@@ -5044,7 +5046,7 @@ CRITICAL — When you need clarification during planning:
|
|
|
5044
5046
|
};
|
|
5045
5047
|
delete env.FORCE_COLOR;
|
|
5046
5048
|
delete env.CLICOLOR_FORCE;
|
|
5047
|
-
const proc = spawn(codexBin, args, {
|
|
5049
|
+
const proc = spawn(shellCommand(codexBin, useShell, { isFile: existsSync }), args, {
|
|
5048
5050
|
cwd: PACKAGE_ROOT,
|
|
5049
5051
|
env,
|
|
5050
5052
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -7226,7 +7228,8 @@ async function queryCodexAppServerModels(codexHome = CODEX_DEFAULT_HOME) {
|
|
|
7226
7228
|
delete env.CLICOLOR_FORCE;
|
|
7227
7229
|
|
|
7228
7230
|
return new Promise((resolve, reject) => {
|
|
7229
|
-
|
|
7231
|
+
// (A configured command may be a command line; a file with a space in its path is one word.)
|
|
7232
|
+
const child = spawn(shellCommand(codexBin, useShell, { isFile: existsSync }), args, {
|
|
7230
7233
|
cwd: PACKAGE_ROOT,
|
|
7231
7234
|
env,
|
|
7232
7235
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -7265,6 +7268,10 @@ async function queryCodexAppServerModels(codexHome = CODEX_DEFAULT_HOME) {
|
|
|
7265
7268
|
child.stderr.setEncoding('utf-8');
|
|
7266
7269
|
child.stderr.on('data', chunk => { stderrBuf += chunk; });
|
|
7267
7270
|
child.on('error', fail);
|
|
7271
|
+
// Codex not installed: the shell asked to run it is gone before the first
|
|
7272
|
+
// request is written, and that write fails on the stream (EPIPE). Unhandled,
|
|
7273
|
+
// it ends the whole server.
|
|
7274
|
+
child.stdin.on('error', fail);
|
|
7268
7275
|
child.on('exit', (code) => {
|
|
7269
7276
|
if (pending.size) {
|
|
7270
7277
|
const err = new Error(`Codex app-server exited while listing models (code ${code ?? 'unknown'})`);
|
|
@@ -8861,6 +8868,7 @@ app.post('/api/sidepanel/session-title', async (req, res) => {
|
|
|
8861
8868
|
input.cwd = cwd;
|
|
8862
8869
|
}
|
|
8863
8870
|
const result = await generateSessionTitle(input, {
|
|
8871
|
+
claudeExecutable: getClaudeSdkExecutable,
|
|
8864
8872
|
execFile: execFileAsync,
|
|
8865
8873
|
getCodexBin,
|
|
8866
8874
|
getCodexAccountHome,
|
|
@@ -9394,10 +9402,10 @@ app.post('/api/opencode/mcp', async (req, res) => {
|
|
|
9394
9402
|
let { name, config: mcpConfig } = req.body || {};
|
|
9395
9403
|
if (!name) {
|
|
9396
9404
|
name = 'SynaBun';
|
|
9397
|
-
const {
|
|
9405
|
+
const { envPath, mcpCommand, mcpArgs } = getMcpPaths();
|
|
9398
9406
|
mcpConfig = {
|
|
9399
|
-
command:
|
|
9400
|
-
args:
|
|
9407
|
+
command: mcpCommand,
|
|
9408
|
+
args: mcpArgs,
|
|
9401
9409
|
env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
|
|
9402
9410
|
};
|
|
9403
9411
|
}
|
|
@@ -9522,17 +9530,12 @@ const _claudeBridgeReady = (async () => {
|
|
|
9522
9530
|
if (typeof cfg.partials === 'boolean') return cfg.partials;
|
|
9523
9531
|
return process.platform !== 'win32';
|
|
9524
9532
|
},
|
|
9525
|
-
//
|
|
9526
|
-
//
|
|
9527
|
-
//
|
|
9528
|
-
|
|
9529
|
-
//
|
|
9530
|
-
|
|
9531
|
-
// Lazy so the (memoized) resolution happens on demand, not at boot.
|
|
9532
|
-
getClaudeBin: () => getClaudeBin(),
|
|
9533
|
-
// The installed CLI when it is newer than the bundled one, else null — the
|
|
9534
|
-
// model picker lists the installed CLI's models, so sessions must run it.
|
|
9535
|
-
alignedClaudeBin: () => alignedClaudeExecutable(getClaudeBin()),
|
|
9533
|
+
// The Claude Code every session runs: the user's own installation, or the
|
|
9534
|
+
// explicit override from cli-config.json ("claude-skin": { "sdkExecutable":
|
|
9535
|
+
// "…" }, a .js entrypoint or a native binary path). SynaBun carries none, so
|
|
9536
|
+
// a session without one is refused with the install hint. Lazy, so the
|
|
9537
|
+
// (memoized) resolution happens on demand, not at boot.
|
|
9538
|
+
claudeExecutable: () => getClaudeSdkExecutable(),
|
|
9536
9539
|
// Sidepanel tabs can run under another Claude account (its own config
|
|
9537
9540
|
// directory). The default account adds nothing to the environment.
|
|
9538
9541
|
claudeAccountEnv: (accountId) => _panelClaudeAccounts().envFor(accountId),
|
|
@@ -9617,10 +9620,10 @@ function handleClaudeSkinWebSocket(ws) {
|
|
|
9617
9620
|
async function getClaudeModelsForClient(force = false) {
|
|
9618
9621
|
try {
|
|
9619
9622
|
const bin = getClaudeBin();
|
|
9620
|
-
// Sidepanel sessions and native loops
|
|
9621
|
-
//
|
|
9622
|
-
//
|
|
9623
|
-
//
|
|
9623
|
+
// Sidepanel sessions and native loops run this same installation. The Agent
|
|
9624
|
+
// SDK that starts it was built against one Claude Code release; an older
|
|
9625
|
+
// installation is worth one line in the log. Cached per binary (a spawn only
|
|
9626
|
+
// when the CLI changes).
|
|
9624
9627
|
checkClaudeCliSkew(bin).catch(() => {});
|
|
9625
9628
|
const result = await discoverClaudeModels(bin, { force, cwd: PACKAGE_ROOT });
|
|
9626
9629
|
if (result?.models?.length) return result;
|
|
@@ -15460,7 +15463,7 @@ function spawnAgentProcess(agent, prompt, extraEnv = {}) {
|
|
|
15460
15463
|
|
|
15461
15464
|
console.log(`[agent] ${agent.id} spawn: ${claudeBin} ${args.map(a => a.length > 80 ? a.slice(0, 80) + '…' : a).join(' ')}`);
|
|
15462
15465
|
|
|
15463
|
-
const child = spawn(claudeBin, args, {
|
|
15466
|
+
const child = spawn(shellCommand(claudeBin, AGENT_IS_WIN), args, {
|
|
15464
15467
|
cwd: agent.cwd,
|
|
15465
15468
|
env: { ...cleanEnv, FORCE_COLOR: '0', NO_COLOR: '1', TERM: 'dumb', ...extraEnv },
|
|
15466
15469
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -16094,6 +16097,7 @@ function isSynabunGitHubRemote(remoteText) {
|
|
|
16094
16097
|
}
|
|
16095
16098
|
|
|
16096
16099
|
function detectSynabunInstallSource() {
|
|
16100
|
+
if (PACKAGED_RUNTIME) return { kind: 'packaged-app' };
|
|
16097
16101
|
const normalizedRoot = PACKAGE_ROOT.replace(/\\/g, '/');
|
|
16098
16102
|
if (/\/node_modules\/synabun$/i.test(normalizedRoot)) {
|
|
16099
16103
|
return { kind: 'npm-global' };
|
|
@@ -16345,6 +16349,10 @@ const SYNABUN_ROOTS = (() => {
|
|
|
16345
16349
|
const roots = new Set();
|
|
16346
16350
|
try { roots.add(realpathSync(__dirname).replace(/\\/g, '/').toLowerCase()); } catch {}
|
|
16347
16351
|
try { roots.add(realpathSync(resolve(__dirname, '..')).replace(/\\/g, '/').toLowerCase()); } catch {}
|
|
16352
|
+
// A packaged application: everything it ships, its own Node and npm included.
|
|
16353
|
+
if (PACKAGED_RUNTIME?.resources) {
|
|
16354
|
+
try { roots.add(realpathSync(PACKAGED_RUNTIME.resources).replace(/\\/g, '/').toLowerCase()); } catch {}
|
|
16355
|
+
}
|
|
16348
16356
|
return [...roots];
|
|
16349
16357
|
})();
|
|
16350
16358
|
|
|
@@ -16366,35 +16374,23 @@ function isInsideSynabun(binPath) {
|
|
|
16366
16374
|
// to come from. Add new versions here if SynaBun ever ships claude-code again.
|
|
16367
16375
|
const KNOWN_STALE_CLAUDE_VERSIONS = new Set(['2.1.71', '2.1.89', '2.1.91']);
|
|
16368
16376
|
|
|
16369
|
-
//
|
|
16370
|
-
//
|
|
16371
|
-
//
|
|
16372
|
-
//
|
|
16373
|
-
//
|
|
16377
|
+
// Remove any external tool that ended up in our own node_modules: Claude Code,
|
|
16378
|
+
// Codex, OpenCode and Gemini CLI are installed by the user, never by SynaBun,
|
|
16379
|
+
// and the agent SDKs are installed without the executables they would carry
|
|
16380
|
+
// (the list and the reasons: lib/external-tools.js at the package root).
|
|
16381
|
+
// Idempotent, runs on every startup, and touches nothing when there is nothing
|
|
16382
|
+
// to remove — a packaged application ships without them and is never written to.
|
|
16383
|
+
// Catches what `npm install` leaves behind: packages an older SynaBun depended
|
|
16384
|
+
// on, and payloads a regenerated lockfile brought back. Logs each removal so a
|
|
16385
|
+
// user can see the prune fire on their machine.
|
|
16374
16386
|
function pruneBundledCliTools() {
|
|
16375
|
-
// @openai/codex is a live transitive dependency of @openai/codex-sdk, so it
|
|
16376
|
-
// must remain installed even though SynaBun never executes its bundled binary.
|
|
16377
|
-
const stalePkgs = ['@anthropic-ai/claude-code', 'opencode-ai', '@google/gemini-cli'];
|
|
16378
16387
|
const moduleRoots = [
|
|
16379
16388
|
join(__dirname, 'node_modules'),
|
|
16380
16389
|
join(__dirname, '..', 'node_modules'),
|
|
16381
16390
|
];
|
|
16382
16391
|
for (const root of moduleRoots) {
|
|
16383
|
-
|
|
16384
|
-
|
|
16385
|
-
try {
|
|
16386
|
-
if (existsSync(target)) {
|
|
16387
|
-
rmSync(target, { recursive: true, force: true });
|
|
16388
|
-
console.log(` Pruned stale bundled CLI: ${target}`);
|
|
16389
|
-
}
|
|
16390
|
-
} catch { /* permission or in-use — ignore */ }
|
|
16391
|
-
// Also remove .bin shims that point at the deleted package
|
|
16392
|
-
const binBase = pkg.split('/').pop().replace(/-cli$|-ai$/, '');
|
|
16393
|
-
for (const ext of ['', '.cmd', '.ps1', '.exe']) {
|
|
16394
|
-
const shim = join(root, '.bin', binBase + ext);
|
|
16395
|
-
try { if (existsSync(shim)) rmSync(shim, { force: true }); } catch {}
|
|
16396
|
-
}
|
|
16397
|
-
}
|
|
16392
|
+
const result = pruneExternalTools(root);
|
|
16393
|
+
for (const item of result.removed) console.log(` Removed ${item.name} from SynaBun's own node_modules (${item.tool} is installed separately): ${item.path}`);
|
|
16398
16394
|
}
|
|
16399
16395
|
}
|
|
16400
16396
|
|
|
@@ -17928,6 +17924,7 @@ app.get('/api/setup/onboarding', async (req, res) => {
|
|
|
17928
17924
|
const setupComplete = vars.SETUP_COMPLETE === 'true';
|
|
17929
17925
|
const dbExists = existsSync(getDbPath());
|
|
17930
17926
|
const mcpBuilt = existsSync(resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'index.js'));
|
|
17927
|
+
const { mcpCommand, mcpArgs } = getMcpPaths();
|
|
17931
17928
|
|
|
17932
17929
|
res.json({
|
|
17933
17930
|
setupComplete,
|
|
@@ -17938,6 +17935,8 @@ app.get('/api/setup/onboarding', async (req, res) => {
|
|
|
17938
17935
|
projectDir: PACKAGE_ROOT,
|
|
17939
17936
|
dataHome: DATA_HOME,
|
|
17940
17937
|
packageRoot: PACKAGE_ROOT,
|
|
17938
|
+
mcpCommand,
|
|
17939
|
+
mcpArgs,
|
|
17941
17940
|
dataHomeInsidePackage: DATA_HOME_DIAGNOSTICS.dataHomeInsidePackage,
|
|
17942
17941
|
recommendedDataHome: DATA_HOME_DIAGNOSTICS.recommendedDataHome,
|
|
17943
17942
|
dataHomeExplicitOverride: DATA_HOME_DIAGNOSTICS.explicitOverride,
|
|
@@ -18072,8 +18071,7 @@ app.post('/api/setup/write-mcp-json', (req, res) => {
|
|
|
18072
18071
|
const envPath = resolve(DATA_HOME, '.env').replace(/\\/g, '/');
|
|
18073
18072
|
|
|
18074
18073
|
const mcpEntry = {
|
|
18075
|
-
|
|
18076
|
-
args: [mcpIndexPath],
|
|
18074
|
+
...mcpStdioCommand(mcpIndexPath),
|
|
18077
18075
|
env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
|
|
18078
18076
|
};
|
|
18079
18077
|
|
|
@@ -20152,7 +20150,7 @@ app.get('/api/claude-code/integrations', (req, res) => {
|
|
|
20152
20150
|
|
|
20153
20151
|
// Auto-sync: ensure all registered projects have globally-enabled hooks
|
|
20154
20152
|
const globallyEnabled = Object.entries(globalHooks).filter(([, on]) => on).map(([ev]) => ev);
|
|
20155
|
-
for (const p of projects) {
|
|
20153
|
+
for (const p of hookInstallBlocker() ? [] : projects) {
|
|
20156
20154
|
const projFile = getClaudeSettingsPath(p.path);
|
|
20157
20155
|
let projSettings;
|
|
20158
20156
|
try {
|
|
@@ -20198,7 +20196,8 @@ app.get('/api/claude-code/integrations', (req, res) => {
|
|
|
20198
20196
|
|
|
20199
20197
|
const mcpIndexPath = resolve(PACKAGE_ROOT, 'mcp-server', 'run.mjs').replace(/\\/g, '/');
|
|
20200
20198
|
const envPath = resolve(DATA_HOME, '.env').replace(/\\/g, '/');
|
|
20201
|
-
const
|
|
20199
|
+
const { command: mcpCommand, args: mcpArgs } = mcpStdioCommand(mcpIndexPath);
|
|
20200
|
+
const cliCommand = claudeMcpAddCommand({ mcpIndexPath, envPath, mcpCommand, mcpArgs });
|
|
20202
20201
|
|
|
20203
20202
|
res.json({
|
|
20204
20203
|
ok: true,
|
|
@@ -20220,7 +20219,11 @@ app.post('/api/claude-code/integrations', (req, res) => {
|
|
|
20220
20219
|
const { target, projectPath, label, hook } = req.body;
|
|
20221
20220
|
// target: 'global' | 'project'
|
|
20222
20221
|
|
|
20222
|
+
// Null unless this run has no path of its own to register (lib/claude-hooks.js).
|
|
20223
|
+
const blocker = hookInstallBlocker();
|
|
20224
|
+
|
|
20223
20225
|
if (target === 'global') {
|
|
20226
|
+
if (blocker) return res.status(409).json({ error: blocker });
|
|
20224
20227
|
const filePath = getGlobalClaudeSettingsPath();
|
|
20225
20228
|
let settings = readClaudeSettingsForWrite(filePath);
|
|
20226
20229
|
settings = addHookToSettings(settings, hook || undefined);
|
|
@@ -20246,15 +20249,23 @@ app.post('/api/claude-code/integrations', (req, res) => {
|
|
|
20246
20249
|
const normalized = resolve(projectPath);
|
|
20247
20250
|
if (!existsSync(normalized)) return res.status(400).json({ error: `Directory not found: ${normalized}` });
|
|
20248
20251
|
|
|
20249
|
-
|
|
20250
|
-
|
|
20251
|
-
|
|
20252
|
-
|
|
20252
|
+
// Adding a project is more than its hooks: a run that cannot write them
|
|
20253
|
+
// still registers it, and refuses only a request that asks for nothing else.
|
|
20254
|
+
const projects = loadHookProjects();
|
|
20255
|
+
const registered = projects.some(p => resolve(p.path) === normalized);
|
|
20256
|
+
const plan = hookInstallPlan({ target, registered, hook });
|
|
20257
|
+
if (plan === 'refuse') return res.status(409).json({ error: blocker });
|
|
20258
|
+
|
|
20259
|
+
if (plan === 'write') {
|
|
20260
|
+
const filePath = getClaudeSettingsPath(normalized);
|
|
20261
|
+
let settings = readClaudeSettingsForWrite(filePath);
|
|
20262
|
+
settings = addHookToSettings(settings, hook || undefined, normalized);
|
|
20263
|
+
writeClaudeSettings(filePath, settings);
|
|
20264
|
+
}
|
|
20253
20265
|
|
|
20254
20266
|
// Save to registered projects list
|
|
20255
20267
|
const projectLabel = label || basename(normalized);
|
|
20256
|
-
|
|
20257
|
-
if (!projects.some(p => resolve(p.path) === normalized)) {
|
|
20268
|
+
if (!registered) {
|
|
20258
20269
|
projects.push({ path: normalized, label: projectLabel });
|
|
20259
20270
|
saveHookProjects(projects);
|
|
20260
20271
|
}
|
|
@@ -20293,6 +20304,9 @@ app.post('/api/claude-code/integrations', (req, res) => {
|
|
|
20293
20304
|
}
|
|
20294
20305
|
} catch { /* non-critical */ }
|
|
20295
20306
|
|
|
20307
|
+
if (plan !== 'write') {
|
|
20308
|
+
return res.json({ ok: true, message: `${basename(normalized)} was added without hooks. ${blocker}`, hooksSkipped: true, dubiousOwnership, trustPath });
|
|
20309
|
+
}
|
|
20296
20310
|
return res.json({ ok: true, message: `Hook enabled for ${basename(normalized)}.`, dubiousOwnership, trustPath });
|
|
20297
20311
|
}
|
|
20298
20312
|
|
|
@@ -21319,7 +21333,18 @@ function syncCodexToolPermissions(allowedKeys) {
|
|
|
21319
21333
|
function getMcpPaths() {
|
|
21320
21334
|
const mcpIndexPath = resolve(PACKAGE_ROOT, 'mcp-server', 'dist', 'preload.js').replace(/\\/g, '/');
|
|
21321
21335
|
const envPath = resolve(DATA_HOME, '.env').replace(/\\/g, '/');
|
|
21322
|
-
|
|
21336
|
+
// What a client outside this process runs: `node <preload.js>`, or the
|
|
21337
|
+
// entry executable of a packaged application (`<entry> mcp`).
|
|
21338
|
+
const { command: mcpCommand, args: mcpArgs } = mcpStdioCommand(mcpIndexPath);
|
|
21339
|
+
return { mcpIndexPath, envPath, mcpCommand, mcpArgs };
|
|
21340
|
+
}
|
|
21341
|
+
|
|
21342
|
+
/** The `claude mcp add` line for the registration getMcpPaths() describes. */
|
|
21343
|
+
function claudeMcpAddCommand({ mcpIndexPath, envPath, mcpCommand, mcpArgs }) {
|
|
21344
|
+
// Inside double quotes a POSIX shell still reads \ " $ and `; a Windows path has none of them to escape.
|
|
21345
|
+
const quoted = (value) => (/^[A-Za-z]:[\\/]/.test(value) ? `"${value}"` : `"${String(value).replace(/[\\"$`]/g, '\\$&')}"`);
|
|
21346
|
+
const launch = mcpCommand === 'node' ? `node "${mcpIndexPath}"` : `${quoted(mcpCommand)} ${mcpArgs.join(' ')}`;
|
|
21347
|
+
return `claude mcp add SynaBun ${launch} -s user -e "DOTENV_PATH=${envPath}"`;
|
|
21323
21348
|
}
|
|
21324
21349
|
|
|
21325
21350
|
function getHomePath() {
|
|
@@ -21340,8 +21365,8 @@ function buildCodexMcpEnv() {
|
|
|
21340
21365
|
}
|
|
21341
21366
|
|
|
21342
21367
|
function buildCodexMcpDefinition() {
|
|
21343
|
-
const {
|
|
21344
|
-
return { command:
|
|
21368
|
+
const { mcpCommand, mcpArgs } = getMcpPaths();
|
|
21369
|
+
return { command: mcpCommand, args: mcpArgs, env: buildCodexMcpEnv() };
|
|
21345
21370
|
}
|
|
21346
21371
|
|
|
21347
21372
|
function buildCodexMcpToml() {
|
|
@@ -21374,13 +21399,13 @@ app.post('/api/setup/gemini/mcp', (req, res) => {
|
|
|
21374
21399
|
try {
|
|
21375
21400
|
const dir = join(getHomePath(), '.gemini');
|
|
21376
21401
|
const settingsPath = join(dir, 'settings.json');
|
|
21377
|
-
const {
|
|
21402
|
+
const { envPath, mcpCommand, mcpArgs } = getMcpPaths();
|
|
21378
21403
|
mkdirSync(dir, { recursive: true });
|
|
21379
21404
|
let data = {};
|
|
21380
21405
|
try { data = JSON.parse(readFileSync(settingsPath, 'utf-8')); } catch {}
|
|
21381
21406
|
data = mergeGeminiMcpConfig(data, {
|
|
21382
|
-
command:
|
|
21383
|
-
args:
|
|
21407
|
+
command: mcpCommand,
|
|
21408
|
+
args: mcpArgs,
|
|
21384
21409
|
env: { DOTENV_PATH: envPath, SYNABUN_DATA_HOME: DATA_HOME, MEMORY_DATA_DIR: resolve(DATA_HOME, 'mcp-data') },
|
|
21385
21410
|
});
|
|
21386
21411
|
writeFileSync(settingsPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
|
|
@@ -21450,7 +21475,8 @@ app.delete('/api/setup/codex/mcp', (req, res) => {
|
|
|
21450
21475
|
app.get('/api/setup/status', (req, res) => {
|
|
21451
21476
|
try {
|
|
21452
21477
|
const home = getHomePath();
|
|
21453
|
-
const
|
|
21478
|
+
const mcpPaths = getMcpPaths();
|
|
21479
|
+
const { mcpIndexPath, envPath, mcpCommand, mcpArgs } = mcpPaths;
|
|
21454
21480
|
|
|
21455
21481
|
// Claude
|
|
21456
21482
|
let claude = { connected: false };
|
|
@@ -21458,7 +21484,7 @@ app.get('/api/setup/status', (req, res) => {
|
|
|
21458
21484
|
const cj = JSON.parse(readFileSync(join(home, '.claude.json'), 'utf-8'));
|
|
21459
21485
|
claude.connected = !!(cj.mcpServers && cj.mcpServers.SynaBun);
|
|
21460
21486
|
} catch {}
|
|
21461
|
-
claude.cliCommand =
|
|
21487
|
+
claude.cliCommand = claudeMcpAddCommand(mcpPaths);
|
|
21462
21488
|
|
|
21463
21489
|
// Gemini
|
|
21464
21490
|
let gemini = { connected: false };
|
|
@@ -21488,7 +21514,7 @@ app.get('/api/setup/status', (req, res) => {
|
|
|
21488
21514
|
let rules = null;
|
|
21489
21515
|
try { rules = rulesetInstaller.status(); } catch (err) { rules = { ok: false, error: err.message }; }
|
|
21490
21516
|
|
|
21491
|
-
res.json({ ok: true, claude, gemini, codex, opencode, rules, paths: { mcpIndexPath, envPath } });
|
|
21517
|
+
res.json({ ok: true, claude, gemini, codex, opencode, rules, paths: { mcpIndexPath, envPath, mcpCommand, mcpArgs } });
|
|
21492
21518
|
} catch (err) {
|
|
21493
21519
|
res.status(500).json({ error: err.message });
|
|
21494
21520
|
}
|
|
@@ -22743,7 +22769,7 @@ app.get('/api/health', (req, res) => {
|
|
|
22743
22769
|
// getDb() auto-creates the directory, file, and schema if missing
|
|
22744
22770
|
getDb();
|
|
22745
22771
|
const count = countMemories();
|
|
22746
|
-
res.json({ ok: true, storage: 'sqlite', memories: count, projectDir: PACKAGE_ROOT });
|
|
22772
|
+
res.json({ ok: true, storage: 'sqlite', memories: count, projectDir: PACKAGE_ROOT, install: INSTALL_KIND, startLauncher: getStartLauncherState(), entry: START_ENTRY });
|
|
22747
22773
|
} catch (err) {
|
|
22748
22774
|
res.json({ ok: false, reason: 'db_error', detail: err.message });
|
|
22749
22775
|
}
|
|
@@ -25248,14 +25274,11 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25248
25274
|
? `${state.model}[1m]`
|
|
25249
25275
|
: state.model && state.model.includes(':') ? toCliModelName(state.model) : state.model,
|
|
25250
25276
|
mcpUrl: `http://localhost:${PORT}/mcp`,
|
|
25251
|
-
|
|
25252
|
-
//
|
|
25253
|
-
//
|
|
25254
|
-
//
|
|
25255
|
-
|
|
25256
|
-
// Run the installed CLI instead when it is newer than the bundled one, so
|
|
25257
|
-
// the loop's model resolves the way the picker it was chosen from named it.
|
|
25258
|
-
alignedClaudeBin: alignedClaudeExecutable(getClaudeBin()),
|
|
25277
|
+
// The user's Claude Code (or the cli-config override): the same
|
|
25278
|
+
// executable the model picker read its list from, so the loop's model
|
|
25279
|
+
// resolves the way it was named there. Without one the run fails at
|
|
25280
|
+
// once with the install hint, before anything is started.
|
|
25281
|
+
claudeExecutable: getClaudeSdkExecutable(),
|
|
25259
25282
|
includePartialMessages: typeof config.partials === 'boolean'
|
|
25260
25283
|
? config.partials
|
|
25261
25284
|
: process.platform !== 'win32',
|
|
@@ -25271,7 +25294,7 @@ _nativeLoopRuntime = new NativeLoopRuntime({
|
|
|
25271
25294
|
...state,
|
|
25272
25295
|
codexHome: state.codexHome || getCodexAccountHome(state.codexAccountId || 'default'),
|
|
25273
25296
|
// Native schedules use the same trusted global CLI as the interactive
|
|
25274
|
-
// sidepanel
|
|
25297
|
+
// sidepanel; SynaBun carries no Codex for the SDK to find by itself.
|
|
25275
25298
|
codexPath: getNativeCodexBin(),
|
|
25276
25299
|
});
|
|
25277
25300
|
},
|
|
@@ -29916,9 +29939,11 @@ async function createBrowserSession(options = {}) {
|
|
|
29916
29939
|
// detection on sites like Twitter. Let the automated Chrome use its own default UA.
|
|
29917
29940
|
const realHeaders = options._realHeaders || {};
|
|
29918
29941
|
const userAgent = savedCfg.userAgent || undefined; // only override if explicitly configured
|
|
29919
|
-
const
|
|
29920
|
-
|
|
29921
|
-
const
|
|
29942
|
+
const { locale, acceptLanguage, languages, warnings: languageWarnings } =
|
|
29943
|
+
resolveBrowserLanguage(savedCfg, realHeaders['accept-language']);
|
|
29944
|
+
for (const warning of languageWarnings) {
|
|
29945
|
+
console.warn(`[browser] Invalid ${warning.field}; using ${JSON.stringify(warning.value)}`);
|
|
29946
|
+
}
|
|
29922
29947
|
|
|
29923
29948
|
// Build context options from saved config
|
|
29924
29949
|
const contextOpts = {
|
|
@@ -30311,7 +30336,7 @@ async function createBrowserSession(options = {}) {
|
|
|
30311
30336
|
],
|
|
30312
30337
|
});
|
|
30313
30338
|
Object.defineProperty(navigator, 'languages', {
|
|
30314
|
-
get: () => ${JSON.stringify(
|
|
30339
|
+
get: () => ${JSON.stringify(languages)},
|
|
30315
30340
|
});
|
|
30316
30341
|
const origQuery = window.Permissions.prototype.query;
|
|
30317
30342
|
window.Permissions.prototype.query = function(params) {
|
|
@@ -32824,7 +32849,7 @@ app.get('/api/browser/config', (req, res) => {
|
|
|
32824
32849
|
app.put('/api/browser/config', (req, res) => {
|
|
32825
32850
|
try {
|
|
32826
32851
|
// Full replacement — the frontend sends the complete config object
|
|
32827
|
-
const body = req.body || {};
|
|
32852
|
+
const body = validateBrowserLanguageConfig(req.body || {});
|
|
32828
32853
|
if (body.connectMode !== undefined) body.connectMode = ['attach', 'morelogin'].includes(body.connectMode) ? body.connectMode : 'mirror';
|
|
32829
32854
|
if (body.attachSeed !== undefined) body.attachSeed = body.attachSeed === 'fresh' ? 'fresh' : 'real';
|
|
32830
32855
|
if (body.moreloginEnvId !== undefined && body.moreloginEnvId !== null && body.moreloginEnvId !== '') body.moreloginEnvId = String(body.moreloginEnvId);
|
|
@@ -32836,7 +32861,8 @@ app.put('/api/browser/config', (req, res) => {
|
|
|
32836
32861
|
.catch(() => {});
|
|
32837
32862
|
res.json({ ok: true });
|
|
32838
32863
|
} catch (err) {
|
|
32839
|
-
|
|
32864
|
+
const invalidLanguage = err.status === 400 && ['locale', 'acceptLanguage'].includes(err.field);
|
|
32865
|
+
res.status(invalidLanguage ? 400 : 500).json({ error: err.message, ...(invalidLanguage && { field: err.field }) });
|
|
32840
32866
|
}
|
|
32841
32867
|
});
|
|
32842
32868
|
|
|
@@ -32966,6 +32992,16 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
32966
32992
|
setKvConfig('embedding_dims', String(EMBEDDING_DIMS));
|
|
32967
32993
|
} catch {}
|
|
32968
32994
|
|
|
32995
|
+
// Keep the Start Server launcher (synabun://) pointing at this install even
|
|
32996
|
+
// when the server was started directly rather than through setup.js. Writes
|
|
32997
|
+
// only what differs; skipped for test runs and throwaway data homes.
|
|
32998
|
+
try {
|
|
32999
|
+
const launcher = registerStartLauncher({ packageRoot: PACKAGE_ROOT });
|
|
33000
|
+
_startLauncherState = launcher.state === 'skipped' ? startLauncherState({ packageRoot: PACKAGE_ROOT }) : launcher.state;
|
|
33001
|
+
if (launcher.changed) console.log('[server] Start Server launcher registered (synabun://)');
|
|
33002
|
+
else if (!launcher.ok) console.warn(`[server] Start Server launcher not registered: ${launcher.errors[0]}`);
|
|
33003
|
+
} catch {}
|
|
33004
|
+
|
|
32969
33005
|
// Auto-heal legacy synabun-symlink plugin state and reinstall via CC CLI
|
|
32970
33006
|
try {
|
|
32971
33007
|
const repair = pluginInstaller.repairSynabunPlugins();
|
|
@@ -33013,10 +33049,9 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
33013
33049
|
// opencode-<sid> node_modules that never got their per-loop cleanup.
|
|
33014
33050
|
setTimeout(() => { try { cleanupStaleLoopConfigs(); } catch {} }, 1500);
|
|
33015
33051
|
|
|
33016
|
-
// Compare the installed Claude CLI with the
|
|
33017
|
-
//
|
|
33018
|
-
//
|
|
33019
|
-
// alignedClaudeExecutable in lib/claude-model-catalog.js).
|
|
33052
|
+
// Compare the installed Claude CLI with the release the Agent SDK was built
|
|
33053
|
+
// against, once, so an older installation is named in the log before the
|
|
33054
|
+
// first session runs on it (checkClaudeCliSkew in lib/claude-model-catalog.js).
|
|
33020
33055
|
setTimeout(() => { checkClaudeCliSkew(getClaudeBin()).catch(() => {}); }, 1000);
|
|
33021
33056
|
|
|
33022
33057
|
// Warm the session_cache + session_fts tables so the hybrid search has data
|
|
@@ -33130,10 +33165,10 @@ const httpServer = app.listen(PORT, async () => {
|
|
|
33130
33165
|
// Check for SynaBun updates (npm + github releases)
|
|
33131
33166
|
try { await checkSynabunUpdate(); } catch {}
|
|
33132
33167
|
|
|
33133
|
-
//
|
|
33134
|
-
//
|
|
33135
|
-
//
|
|
33136
|
-
//
|
|
33168
|
+
// Prune any external CLI tool left in our node_modules before the version
|
|
33169
|
+
// check runs — defends against `npm install` not auto-pruning packages that
|
|
33170
|
+
// were removed from package.json (recurring source of phantom-version
|
|
33171
|
+
// badges, e.g. v2.1.89 from a stale @anthropic-ai/claude-code).
|
|
33137
33172
|
try { pruneBundledCliTools(); } catch {}
|
|
33138
33173
|
|
|
33139
33174
|
// Check for CLI tool updates — fire-and-forget so boot isn't serialized
|