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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// ── Which Claude Code runs: the user's own installation ──
|
|
2
|
+
//
|
|
3
|
+
// Claude Code is installed by the user, separately from SynaBun (the native
|
|
4
|
+
// installer, Homebrew, npm). Nothing in SynaBun carries a copy: the Claude
|
|
5
|
+
// Agent SDK is installed without the executable it would otherwise bring (see
|
|
6
|
+
// lib/external-tools.js at the package root), so every SDK call has to be told
|
|
7
|
+
// which executable to start. This module is where that answer comes from, for
|
|
8
|
+
// the side panel, the Assistant's Claude brain, native loops, session titles
|
|
9
|
+
// and the model probe alike:
|
|
10
|
+
//
|
|
11
|
+
// resolveClaudeLauncher what a terminal or a shell would start: the
|
|
12
|
+
// user's configured command, else the first
|
|
13
|
+
// `claude` on PATH that is not inside SynaBun
|
|
14
|
+
// resolveClaudeSdkExecutable what the SDK can start. It spawns with
|
|
15
|
+
// shell:false, so an npm command file on Windows
|
|
16
|
+
// is followed to the executable behind it
|
|
17
|
+
//
|
|
18
|
+
// A missing installation is an ordinary state, not a failure: both answer with
|
|
19
|
+
// `path: null` and the caller says CLAUDE_NOT_INSTALLED.
|
|
20
|
+
//
|
|
21
|
+
// Every dependency is injectable (platform, PATH, the file system), so Windows
|
|
22
|
+
// resolution is tested from any host. Nothing here throws.
|
|
23
|
+
|
|
24
|
+
import { accessSync, constants, existsSync, statSync } from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { getAugmentedPath } from './augmented-path.js';
|
|
27
|
+
import { resolveClaudeExecutableOverride } from './native-binary-runtime.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What a session says when no Claude Code is installed. The words "Claude CLI
|
|
31
|
+
* not found" are what the side panel shows its install help on.
|
|
32
|
+
*/
|
|
33
|
+
export const CLAUDE_NOT_INSTALLED = 'Claude CLI not found. Claude Code is installed separately from SynaBun: install it, then send your message again.';
|
|
34
|
+
|
|
35
|
+
/** A real executable first, then the command files a shell can run. */
|
|
36
|
+
const WINDOWS_LAUNCHER_ORDER = ['.exe', '.cmd', '.bat', '.ps1'];
|
|
37
|
+
|
|
38
|
+
const pathFor = (platform) => (platform === 'win32' ? path.win32 : path.posix);
|
|
39
|
+
|
|
40
|
+
function unique(values, pathImpl, insensitive) {
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
const out = [];
|
|
43
|
+
for (const value of values) {
|
|
44
|
+
const text = String(value || '').trim();
|
|
45
|
+
if (!text) continue;
|
|
46
|
+
const normal = pathImpl.normalize(text);
|
|
47
|
+
const key = insensitive ? normal.toLowerCase() : normal;
|
|
48
|
+
if (seen.has(key)) continue;
|
|
49
|
+
seen.add(key);
|
|
50
|
+
out.push(normal);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The launcher Windows should start, from a list in PATH order (as `where`
|
|
57
|
+
* prints it, or findOnPath below). PATH order decides between folders; inside
|
|
58
|
+
* one folder a real executable beats a command file, and npm's extensionless
|
|
59
|
+
* shell script (which Windows cannot start) is only ever the last resort.
|
|
60
|
+
*/
|
|
61
|
+
export function selectWindowsLauncher(launchers = [], accept = () => true) {
|
|
62
|
+
const accepted = unique(launchers, path.win32, true).filter(accept);
|
|
63
|
+
const folders = new Map();
|
|
64
|
+
for (const launcher of accepted) {
|
|
65
|
+
const folder = path.win32.dirname(launcher).toLowerCase();
|
|
66
|
+
if (!folders.has(folder)) folders.set(folder, []);
|
|
67
|
+
folders.get(folder).push(launcher);
|
|
68
|
+
}
|
|
69
|
+
for (const group of folders.values()) {
|
|
70
|
+
for (const extension of WINDOWS_LAUNCHER_ORDER) {
|
|
71
|
+
const hit = group.find(launcher => launcher.toLowerCase().endsWith(extension));
|
|
72
|
+
if (hit) return hit;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return accepted[0] || null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function defaultIsLaunchable(file, platform) {
|
|
79
|
+
try {
|
|
80
|
+
if (!statSync(file).isFile()) return false;
|
|
81
|
+
if (platform !== 'win32') accessSync(file, constants.X_OK);
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** PATH without any `node_modules/.bin` folder: a dependency's command is never the user's tool. */
|
|
89
|
+
export function withoutPackageBins(pathValue, platform = process.platform) {
|
|
90
|
+
const delimiter = platform === 'win32' ? ';' : ':';
|
|
91
|
+
return String(pathValue || '').split(delimiter)
|
|
92
|
+
.filter(entry => entry && !/[\\/]node_modules[\\/]\.bin[\\/]?$/i.test(entry))
|
|
93
|
+
.join(delimiter);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Every file a command name resolves to, in PATH order, read from the file
|
|
98
|
+
* system (no `which`, no shell: a folder with a space in its name is just a
|
|
99
|
+
* folder). On Windows each folder is tried with the launcher extensions, then
|
|
100
|
+
* bare.
|
|
101
|
+
*/
|
|
102
|
+
export function findOnPath(command, {
|
|
103
|
+
platform = process.platform,
|
|
104
|
+
pathValue = getAugmentedPath(),
|
|
105
|
+
isLaunchable = defaultIsLaunchable,
|
|
106
|
+
} = {}) {
|
|
107
|
+
const name = String(command || '').trim();
|
|
108
|
+
if (!name) return [];
|
|
109
|
+
const pathImpl = pathFor(platform);
|
|
110
|
+
const windows = platform === 'win32';
|
|
111
|
+
const names = windows && !pathImpl.extname(name) ? [...WINDOWS_LAUNCHER_ORDER.map(extension => name + extension), name] : [name];
|
|
112
|
+
const found = [];
|
|
113
|
+
for (const folder of String(pathValue || '').split(windows ? ';' : ':')) {
|
|
114
|
+
if (!folder) continue;
|
|
115
|
+
for (const candidate of names) {
|
|
116
|
+
const file = pathImpl.join(folder, candidate);
|
|
117
|
+
if (isLaunchable(file, platform)) found.push(file);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return unique(found, pathImpl, windows);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The Claude Code a terminal would start.
|
|
125
|
+
*
|
|
126
|
+
* configured cli-config.json → "claude-code": { "command": "…" } (Settings > Terminal):
|
|
127
|
+
* a path is taken as it is, a name is looked up on PATH
|
|
128
|
+
* path the first `claude` on PATH
|
|
129
|
+
*
|
|
130
|
+
* Never a file inside SynaBun (`isInsideSynabun`) and never one from a
|
|
131
|
+
* `node_modules/.bin` folder on PATH: a copy that came with a dependency must
|
|
132
|
+
* not stand in for the user's own.
|
|
133
|
+
*
|
|
134
|
+
* @returns {{path: string|null, source: 'configured'|'path'|'missing', rejected: string[]}}
|
|
135
|
+
*/
|
|
136
|
+
export function resolveClaudeLauncher({
|
|
137
|
+
configured = '',
|
|
138
|
+
platform = process.platform,
|
|
139
|
+
pathValue = getAugmentedPath(),
|
|
140
|
+
isInsideSynabun = () => false,
|
|
141
|
+
exists = existsSync,
|
|
142
|
+
isLaunchable = defaultIsLaunchable,
|
|
143
|
+
} = {}) {
|
|
144
|
+
const pathImpl = pathFor(platform);
|
|
145
|
+
const windows = platform === 'win32';
|
|
146
|
+
const rejected = [];
|
|
147
|
+
const outside = (file) => {
|
|
148
|
+
if (!isInsideSynabun(file)) return true;
|
|
149
|
+
rejected.push(file);
|
|
150
|
+
return false;
|
|
151
|
+
};
|
|
152
|
+
const lookup = (command) => {
|
|
153
|
+
const hits = findOnPath(command, { platform, pathValue: withoutPackageBins(pathValue, platform), isLaunchable }).filter(outside);
|
|
154
|
+
return (windows ? selectWindowsLauncher(hits) : hits[0]) || null;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const wanted = String(configured || '').trim();
|
|
158
|
+
if (wanted && wanted !== 'claude') {
|
|
159
|
+
const isPath = wanted.includes('/') || (windows && wanted.includes('\\'));
|
|
160
|
+
if (isPath) {
|
|
161
|
+
if (exists(wanted) && outside(wanted)) return { path: pathImpl.normalize(wanted), source: 'configured', rejected };
|
|
162
|
+
} else {
|
|
163
|
+
const hit = lookup(wanted);
|
|
164
|
+
if (hit) return { path: hit, source: 'configured', rejected };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const hit = lookup('claude');
|
|
169
|
+
return hit ? { path: hit, source: 'path', rejected } : { path: null, source: 'missing', rejected };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Where the executable behind an npm command file is, most likely first. npm
|
|
174
|
+
* puts `claude.cmd` next to `node_modules\@anthropic-ai\claude-code`, whose
|
|
175
|
+
* `bin\claude.exe` is the program (older releases: `cli.js`, run by Node). A
|
|
176
|
+
* `.bin` folder has the package one level up instead.
|
|
177
|
+
*/
|
|
178
|
+
export function windowsClaudeTargets(launcher) {
|
|
179
|
+
const pathImpl = path.win32;
|
|
180
|
+
const folder = pathImpl.dirname(String(launcher || ''));
|
|
181
|
+
const roots = [pathImpl.join(folder, 'node_modules')];
|
|
182
|
+
if (pathImpl.basename(folder).toLowerCase() === '.bin') roots.push(pathImpl.dirname(folder));
|
|
183
|
+
return roots.flatMap((root) => {
|
|
184
|
+
const packageDir = pathImpl.join(root, '@anthropic-ai', 'claude-code');
|
|
185
|
+
return [pathImpl.join(packageDir, 'bin', 'claude.exe'), pathImpl.join(packageDir, 'cli.js')];
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The executable an Agent SDK call is given as `pathToClaudeCodeExecutable`.
|
|
191
|
+
*
|
|
192
|
+
* override cli-config.json → "claude-skin": { "sdkExecutable": "…" }, when it is usable
|
|
193
|
+
* installed the user's Claude Code (`launcher`, from resolveClaudeLauncher)
|
|
194
|
+
*
|
|
195
|
+
* The SDK starts this path itself, without a shell, so it has to be a program
|
|
196
|
+
* or a script: on Windows a `.cmd`, `.bat` or `.ps1` launcher is followed to
|
|
197
|
+
* what it runs. `path: null` means there is nothing to start; `reason` says
|
|
198
|
+
* why in a sentence a person can act on, `ignored` why an override was passed
|
|
199
|
+
* over.
|
|
200
|
+
*
|
|
201
|
+
* @returns {{path: string|null, source: 'override'|'installed'|'missing', kind: string|null, reason: string|null, ignored: string|null, launcher: string|null}}
|
|
202
|
+
*/
|
|
203
|
+
export function resolveClaudeSdkExecutable({
|
|
204
|
+
launcher = null,
|
|
205
|
+
sdkExecutable = null,
|
|
206
|
+
platform = process.platform,
|
|
207
|
+
exists = existsSync,
|
|
208
|
+
isInsideSynabun = () => false,
|
|
209
|
+
validate = resolveClaudeExecutableOverride,
|
|
210
|
+
} = {}) {
|
|
211
|
+
const answer = (fields) => ({ path: null, source: 'missing', kind: null, reason: null, ignored: null, launcher: launcher || null, ...fields });
|
|
212
|
+
let ignored = null;
|
|
213
|
+
|
|
214
|
+
const explicit = typeof sdkExecutable === 'string' ? sdkExecutable.trim() : '';
|
|
215
|
+
if (explicit) {
|
|
216
|
+
const verdict = validate(explicit, { platform });
|
|
217
|
+
if (verdict.ok && verdict.path) return answer({ path: verdict.path, source: 'override', kind: verdict.kind });
|
|
218
|
+
ignored = verdict.reason || `Not usable: ${explicit}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const installed = typeof launcher === 'string' ? launcher.trim() : '';
|
|
222
|
+
// A bare name is what "nothing was found" looks like to older callers.
|
|
223
|
+
if (!installed || !(installed.includes('/') || installed.includes('\\'))) return answer({ reason: CLAUDE_NOT_INSTALLED, ignored });
|
|
224
|
+
|
|
225
|
+
const candidates = platform === 'win32' && !/\.exe$/i.test(installed) && !/\.[cm]?js$/i.test(installed)
|
|
226
|
+
? windowsClaudeTargets(installed).filter(file => exists(file))
|
|
227
|
+
: [installed];
|
|
228
|
+
let refusal = null;
|
|
229
|
+
for (const candidate of candidates) {
|
|
230
|
+
if (isInsideSynabun(candidate)) { refusal = `${candidate} is inside SynaBun, not an installation of Claude Code`; continue; }
|
|
231
|
+
const verdict = validate(candidate, { platform });
|
|
232
|
+
if (verdict.ok && verdict.path) return answer({ path: verdict.path, source: 'installed', kind: verdict.kind, ignored });
|
|
233
|
+
refusal = verdict.reason || `Not usable: ${candidate}`;
|
|
234
|
+
}
|
|
235
|
+
return answer({
|
|
236
|
+
reason: refusal
|
|
237
|
+
? `Claude Code was found at ${installed}, but it cannot be started from there: ${refusal}.`
|
|
238
|
+
: `Claude Code was found at ${installed}, but not the program that command runs. Reinstall Claude Code, or name its executable in cli-config.json ("claude-skin": { "sdkExecutable": "…" }).`,
|
|
239
|
+
ignored,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
@@ -15,7 +15,6 @@ import { dirname, join, sep } from 'node:path';
|
|
|
15
15
|
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
16
16
|
import { createRequire } from 'node:module';
|
|
17
17
|
import { getAugmentedPath } from './augmented-path.js';
|
|
18
|
-
import { resolveClaudeExecutableOverride } from './native-binary-runtime.js';
|
|
19
18
|
|
|
20
19
|
// Effort ladder the Claude Code CLI accepts for --effort. Models advertise their
|
|
21
20
|
// own subset via supportedEffortLevels; this is the superset used as a fallback
|
|
@@ -155,10 +154,23 @@ export function normalizeClaudeModels(models = []) {
|
|
|
155
154
|
return out;
|
|
156
155
|
}
|
|
157
156
|
|
|
157
|
+
// With a shell, Node joins the command and its arguments into one line and
|
|
158
|
+
// quotes nothing. An installation under a folder with a space in its name
|
|
159
|
+
// (C:\Users\Jane Doe\AppData\Roaming\npm\claude.cmd) would be cut at the
|
|
160
|
+
// space, so the command is quoted here whenever a shell runs it. Where the
|
|
161
|
+
// value may also be a command line the user configured ("wsl codex"), pass
|
|
162
|
+
// `isFile`: only what is a file on disk is then treated as one word.
|
|
163
|
+
export function shellCommand(bin, useShell, { isFile = null } = {}) {
|
|
164
|
+
const value = String(bin);
|
|
165
|
+
if (!useShell || !/\s/.test(value) || /^".*"$/.test(value)) return value;
|
|
166
|
+
if (isFile && !isFile(value)) return value;
|
|
167
|
+
return `"${value}"`;
|
|
168
|
+
}
|
|
169
|
+
|
|
158
170
|
// Ask the CLI for its model list. Resolves to [] on any failure — callers fall
|
|
159
171
|
// back to CLAUDE_FALLBACK_MODELS rather than surfacing an error, because a
|
|
160
172
|
// missing model list should degrade the picker, not break the panel.
|
|
161
|
-
export function queryClaudeCliModels(claudeBin, { cwd, timeoutMs = DISCOVERY_TIMEOUT_MS } = {}) {
|
|
173
|
+
export function queryClaudeCliModels(claudeBin, { cwd, timeoutMs = DISCOVERY_TIMEOUT_MS, spawnImpl = spawn } = {}) {
|
|
162
174
|
return new Promise((resolveResult) => {
|
|
163
175
|
let bin = claudeBin;
|
|
164
176
|
const args = [
|
|
@@ -200,7 +212,7 @@ export function queryClaudeCliModels(claudeBin, { cwd, timeoutMs = DISCOVERY_TIM
|
|
|
200
212
|
timer.unref?.();
|
|
201
213
|
|
|
202
214
|
try {
|
|
203
|
-
child =
|
|
215
|
+
child = spawnImpl(shellCommand(bin, useShell), args, {
|
|
204
216
|
cwd: cwd || process.cwd(),
|
|
205
217
|
env: { ...process.env, PATH: getAugmentedPath(), CLAUDE_CODE_ENTRYPOINT: 'sdk-cli' },
|
|
206
218
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
@@ -212,6 +224,11 @@ export function queryClaudeCliModels(claudeBin, { cwd, timeoutMs = DISCOVERY_TIM
|
|
|
212
224
|
|
|
213
225
|
child.on('error', () => finish([]));
|
|
214
226
|
child.on('exit', () => finish([]));
|
|
227
|
+
// A CLI that is not installed is a shell that exits at once, sometimes
|
|
228
|
+
// before the request below is written. The write then fails on the stream
|
|
229
|
+
// (EPIPE), not in the try around it, and an unhandled stream error ends the
|
|
230
|
+
// whole server. Seen on Linux, where `sh` is fast.
|
|
231
|
+
child.stdin.on('error', () => finish([]));
|
|
215
232
|
|
|
216
233
|
let buf = '';
|
|
217
234
|
child.stdout.setEncoding('utf-8');
|
|
@@ -274,27 +291,26 @@ export function clearClaudeModelCache() {
|
|
|
274
291
|
_cache = { at: 0, key: '', models: [] };
|
|
275
292
|
_skewCache = { key: null, result: undefined };
|
|
276
293
|
_skewInflight.clear();
|
|
277
|
-
_alignFailures.clear();
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
// ── CLI version skew ──────────────────────────────────────────────────────────
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
// the
|
|
284
|
-
// "opus[1m]"
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
297
|
+
// One Claude Code is in play: the user's own installation. This module asks it
|
|
298
|
+
// for its models, and the Agent SDK starts that same executable for sidepanel
|
|
299
|
+
// chats, the Assistant and native loops (lib/claude-executable.js), so an alias
|
|
300
|
+
// such as "opus[1m]" means in a session what it meant in the picker.
|
|
301
|
+
//
|
|
302
|
+
// What can still drift is the SDK itself: each release is built against one
|
|
303
|
+
// Claude Code version (its manifest names it) and may send options an older
|
|
304
|
+
// installation does not know. That is worth one line in the log, never a
|
|
305
|
+
// refusal: the installed Claude Code is the only one there is to run.
|
|
290
306
|
|
|
291
307
|
const _require = createRequire(import.meta.url);
|
|
292
308
|
|
|
293
|
-
// The Claude Code version the installed Agent SDK
|
|
294
|
-
// manifest (`manifest.json` → "version": "2.1.220"). Not in the
|
|
295
|
-
// exports map, so walk up from the resolved entry file. null when the
|
|
296
|
-
//
|
|
297
|
-
export function
|
|
309
|
+
// The Claude Code version the installed Agent SDK was built against, from the
|
|
310
|
+
// SDK's own manifest (`manifest.json` → "version": "2.1.220"). Not in the
|
|
311
|
+
// package's exports map, so walk up from the resolved entry file. null when the
|
|
312
|
+
// SDK is absent.
|
|
313
|
+
export function sdkReferenceCliVersion() {
|
|
298
314
|
try {
|
|
299
315
|
let dir = dirname(_require.resolve('@anthropic-ai/claude-agent-sdk'));
|
|
300
316
|
for (let i = 0; i < 5; i++) {
|
|
@@ -355,7 +371,7 @@ export function queryClaudeCliVersion(claudeBin, { timeoutMs = 10000 } = {}) {
|
|
|
355
371
|
timer.unref?.();
|
|
356
372
|
|
|
357
373
|
try {
|
|
358
|
-
child = spawn(bin, args, {
|
|
374
|
+
child = spawn(shellCommand(bin, useShell), args, {
|
|
359
375
|
env: { ...process.env, PATH: getAugmentedPath() },
|
|
360
376
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
361
377
|
shell: useShell,
|
|
@@ -371,33 +387,25 @@ export function queryClaudeCliVersion(claudeBin, { timeoutMs = 10000 } = {}) {
|
|
|
371
387
|
}
|
|
372
388
|
|
|
373
389
|
// Never blocks discovery. The result is cached per binary identity — the model
|
|
374
|
-
// endpoints ask on every request
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
// distinct pair so repeated checks don't spam the log.
|
|
390
|
+
// endpoints ask on every request — so a `claude --version` spawn happens once
|
|
391
|
+
// per installed CLI, not once per request. A CLI update changes the identity and
|
|
392
|
+
// re-probes. `null` (nothing to compare: no SDK, no CLI, unreadable version) is
|
|
393
|
+
// cached too. Warns once per distinct pair so repeated checks don't spam the log.
|
|
379
394
|
const _warnedSkew = new Set();
|
|
380
395
|
let _skewCache = { key: null, result: undefined };
|
|
381
396
|
const _skewInflight = new Map(); // binary identity → pending probe
|
|
382
397
|
|
|
383
398
|
function warnSkewOnce(result, claudeBin, log) {
|
|
384
|
-
|
|
399
|
+
// A newer installation than the SDK was built against is the ordinary state
|
|
400
|
+
// between two SynaBun releases, and nothing to report.
|
|
401
|
+
if (!result.installedOlder) return;
|
|
402
|
+
const pair = `${result.discovered}→${result.reference}`;
|
|
385
403
|
if (_warnedSkew.has(pair)) return;
|
|
386
404
|
_warnedSkew.add(pair);
|
|
387
|
-
if (result.installedNewer) {
|
|
388
|
-
log(
|
|
389
|
-
`[claude-models] CLI version skew: the installed CLI ${result.discovered} (${claudeBin}) is newer `
|
|
390
|
-
+ `than the ${result.bundled} bundled with @anthropic-ai/claude-agent-sdk. Sidepanel sessions and `
|
|
391
|
-
+ 'native loops will run the installed CLI, so a picked model launches as the version the picker '
|
|
392
|
-
+ 'names. Bump @anthropic-ai/claude-agent-sdk to the release that bundles it to re-align the runtime.',
|
|
393
|
-
);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
405
|
log(
|
|
397
|
-
`[claude-models]
|
|
398
|
-
+
|
|
399
|
-
+
|
|
400
|
-
+ `different version. Align them: update the installed Claude CLI to ${result.bundled} or newer.`,
|
|
406
|
+
`[claude-models] The installed Claude Code is ${result.discovered} (${claudeBin}); `
|
|
407
|
+
+ `@anthropic-ai/claude-agent-sdk was built against ${result.reference}. Sessions run the `
|
|
408
|
+
+ `installed one. If a session misbehaves, update Claude Code to ${result.reference} or newer.`,
|
|
401
409
|
);
|
|
402
410
|
}
|
|
403
411
|
|
|
@@ -405,7 +413,7 @@ export async function checkClaudeCliSkew(claudeBin, {
|
|
|
405
413
|
log = console.warn,
|
|
406
414
|
force = false,
|
|
407
415
|
queryVersion = queryClaudeCliVersion,
|
|
408
|
-
|
|
416
|
+
referenceVersion = sdkReferenceCliVersion,
|
|
409
417
|
} = {}) {
|
|
410
418
|
const key = binaryIdentity(claudeBin);
|
|
411
419
|
let result;
|
|
@@ -415,14 +423,14 @@ export async function checkClaudeCliSkew(claudeBin, {
|
|
|
415
423
|
let probe = _skewInflight.get(key);
|
|
416
424
|
if (!probe) {
|
|
417
425
|
probe = (async () => {
|
|
418
|
-
const
|
|
419
|
-
const discovered =
|
|
420
|
-
const order = compareCliVersions(discovered,
|
|
426
|
+
const reference = referenceVersion();
|
|
427
|
+
const discovered = reference ? await queryVersion(claudeBin) : null;
|
|
428
|
+
const order = compareCliVersions(discovered, reference);
|
|
421
429
|
const value = order === null ? null : {
|
|
422
430
|
discovered,
|
|
423
|
-
|
|
431
|
+
reference,
|
|
424
432
|
skewed: order !== 0,
|
|
425
|
-
|
|
433
|
+
installedOlder: order < 0,
|
|
426
434
|
};
|
|
427
435
|
_skewCache = { key, result: value };
|
|
428
436
|
return value;
|
|
@@ -434,50 +442,3 @@ export async function checkClaudeCliSkew(claudeBin, {
|
|
|
434
442
|
if (result?.skewed) warnSkewOnce(result, claudeBin, log);
|
|
435
443
|
return result;
|
|
436
444
|
}
|
|
437
|
-
|
|
438
|
-
// Synchronous view of the last probe for this binary: the result, null (probed,
|
|
439
|
-
// nothing to compare), or undefined (not probed at this identity — typically
|
|
440
|
-
// because the CLI was updated since).
|
|
441
|
-
export function cachedClaudeCliSkew(claudeBin) {
|
|
442
|
-
return _skewCache.key === binaryIdentity(claudeBin) ? _skewCache.result : undefined;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// ── Runtime alignment ─────────────────────────────────────────────────────────
|
|
446
|
-
// The picker's model table comes from the installed CLI; SDK sessions (sidepanel)
|
|
447
|
-
// and native loops default to the CLI the SDK bundles. When the installed one is
|
|
448
|
-
// newer, the same alias resolves to a newer model there — "opus[1m]" is Opus 5.5 on
|
|
449
|
-
// 2.1.280 but Opus 5 on 2.1.278 — so the picker would promise a model the session
|
|
450
|
-
// cannot launch. In that case sessions run the installed CLI instead. Everywhere
|
|
451
|
-
// else the bundled, version-matched binary stays the default: when the versions
|
|
452
|
-
// match, when the bundled one is the newer of the two, and when no usable CLI is
|
|
453
|
-
// installed.
|
|
454
|
-
const _alignFailures = new Set(); // binary identities whose aligned launch failed
|
|
455
|
-
|
|
456
|
-
// A launch failure of the aligned CLI sends every later session back to the bundled
|
|
457
|
-
// runtime. Keyed by identity, so the next CLI update gets a fresh chance.
|
|
458
|
-
export function markAlignedClaudeExecutableFailed(claudeBin) {
|
|
459
|
-
const key = binaryIdentity(claudeBin);
|
|
460
|
-
if (key) _alignFailures.add(key);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// The path SDK sessions should run instead of the bundled CLI, or null to keep the
|
|
464
|
-
// bundled one. Never blocks: when this binary has not been compared yet, the check
|
|
465
|
-
// starts in the background and this call answers null, so no session waits on a
|
|
466
|
-
// version probe — the next one aligns.
|
|
467
|
-
export function alignedClaudeExecutable(claudeBin, {
|
|
468
|
-
skew = cachedClaudeCliSkew(claudeBin),
|
|
469
|
-
refresh = true,
|
|
470
|
-
resolveOverride = resolveClaudeExecutableOverride,
|
|
471
|
-
} = {}) {
|
|
472
|
-
if (!claudeBin) return null;
|
|
473
|
-
if (skew === undefined) {
|
|
474
|
-
if (refresh) checkClaudeCliSkew(claudeBin).catch(() => {});
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
if (!skew?.installedNewer) return null;
|
|
478
|
-
if (_alignFailures.has(binaryIdentity(claudeBin))) return null;
|
|
479
|
-
// The SDK spawns this path verbatim with shell:false; the shared override
|
|
480
|
-
// contract rejects bare command names and anything that is not executable.
|
|
481
|
-
const candidate = resolveOverride(claudeBin);
|
|
482
|
-
return candidate?.ok && candidate.path ? candidate.path : null;
|
|
483
|
-
}
|