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
|
@@ -136,6 +136,44 @@
|
|
|
136
136
|
"stillOffline": "Still offline \u2014 start the server and try again",
|
|
137
137
|
"runCommand": "Run this command to start the server:",
|
|
138
138
|
"copyToClipboard": "Copy to clipboard",
|
|
139
|
+
"start": {
|
|
140
|
+
"asked": "Asked your system to start SynaBun. If your browser shows a prompt, choose Open.",
|
|
141
|
+
"unconfirmedMac": "No word from the launcher yet. If your browser is asking for permission, choose Open: a Terminal window then opens with the server.",
|
|
142
|
+
"unconfirmedWindows": "No word from the launcher yet. If your browser is asking for permission, choose Open: a console window then opens with the server.",
|
|
143
|
+
"unconfirmedLinux": "No word from the launcher yet. If your browser is asking for permission, choose Open: the server then starts in the background.",
|
|
144
|
+
"stalled": "Nothing has answered. If no prompt or window appeared, run the command below once: it also repairs this button.",
|
|
145
|
+
"launcherRunning": "The launcher is running.",
|
|
146
|
+
"terminalOpened": "A Terminal window was opened for the server.",
|
|
147
|
+
"terminalQuiet": "A Terminal window was opened {seconds} s ago, but the server has not started in it. Look at that window.",
|
|
148
|
+
"processStarted": "The server's supervisor was started.",
|
|
149
|
+
"checking": "Checking the install.",
|
|
150
|
+
"dependencies": "Installing dependencies with npm. A first start, or the first after an update, takes a few minutes.",
|
|
151
|
+
"browser": "Downloading the automation browser. This happens once.",
|
|
152
|
+
"build": "Building the MCP server.",
|
|
153
|
+
"snapshot": "Backing up your data before the first start of this version.",
|
|
154
|
+
"snapshotSized": "Backing up your data before the first start of this version: {files} files, {megabytes} MB.",
|
|
155
|
+
"server": "The server process is running. Waiting for it to answer.",
|
|
156
|
+
"listening": "The server is listening. Connecting.",
|
|
157
|
+
"answered": "The server answered the launcher. Connecting.",
|
|
158
|
+
"quiet": "Nothing new from the start for {seconds} s.",
|
|
159
|
+
"failed": "The start failed: {reason}",
|
|
160
|
+
"failedHint": "Run the command below to see the full output, or try again.",
|
|
161
|
+
"failedLog": "Launcher log: {path}",
|
|
162
|
+
"gaveUp": "The server was started but has not answered. Check its window, or run the command below.",
|
|
163
|
+
"online": "The server is up. Loading SynaBun.",
|
|
164
|
+
"trailLauncher": "Launcher started",
|
|
165
|
+
"trailTerminal": "Terminal opened",
|
|
166
|
+
"trailProcess": "Supervisor started",
|
|
167
|
+
"trailSupervisor": "Supervisor running",
|
|
168
|
+
"trailServer": "Server process started",
|
|
169
|
+
"trailListening": "Server listening",
|
|
170
|
+
"elapsedLabel": "Time since Start was pressed",
|
|
171
|
+
"headingStarting": "Starting the server",
|
|
172
|
+
"headingReady": "Server is up",
|
|
173
|
+
"headingFailed": "The server did not start"
|
|
174
|
+
},
|
|
175
|
+
"runCommandOr": "Or run this command:",
|
|
176
|
+
"runCommandSetup": "Run this command once. It also sets up the Start button:",
|
|
139
177
|
"serverOffline": "Server Offline",
|
|
140
178
|
"serverNotRunning": "The Neural Interface server is not running",
|
|
141
179
|
"connectionFailed": "Connection Failed",
|
|
@@ -525,7 +563,7 @@
|
|
|
525
563
|
"help": "Storage, search, backups"
|
|
526
564
|
},
|
|
527
565
|
"ai_decisions": {
|
|
528
|
-
"label": "
|
|
566
|
+
"label": "Jev",
|
|
529
567
|
"help": "Smart judgments and cost"
|
|
530
568
|
},
|
|
531
569
|
"projects": {
|
|
@@ -571,7 +609,7 @@
|
|
|
571
609
|
"purpose": "See where your memories live, choose how SynaBun finds them, and keep a copy you can restore."
|
|
572
610
|
},
|
|
573
611
|
"ai_decisions": {
|
|
574
|
-
"title": "
|
|
612
|
+
"title": "Jev",
|
|
575
613
|
"purpose": "Control where smart judgments help, what they may use, and how much they cost."
|
|
576
614
|
},
|
|
577
615
|
"projects": {
|
|
@@ -3597,6 +3635,7 @@
|
|
|
3597
3635
|
"judgments": {
|
|
3598
3636
|
"showEveryJudgmentAboutThisMemory": ": show every judgment about this memory in the log",
|
|
3599
3637
|
"checkingTypesafe": "Checking TypeSafe…",
|
|
3638
|
+
"learnMoreAtTypesafe": "Learn more at",
|
|
3600
3639
|
"jevTypesafeSystemOneReturnsTyped": "Jev (TypeSafe System One) returns typed judgments where SynaBun used to guess with a regex or a similarity constant. The key is stored in",
|
|
3601
3640
|
"andNeverLeavesTheServerOnly": "and never leaves the server; only its last four characters reach this page.",
|
|
3602
3641
|
"apiKey": "API key",
|
|
@@ -131,6 +131,44 @@
|
|
|
131
131
|
"stillOffline": "Ainda offline — inicie o servidor e tente novamente",
|
|
132
132
|
"runCommand": "Execute este comando para iniciar o servidor:",
|
|
133
133
|
"copyToClipboard": "Copiar para a área de transferência",
|
|
134
|
+
"start": {
|
|
135
|
+
"asked": "Pedimos ao sistema para iniciar o SynaBun. Se o navegador mostrar um aviso, escolha Abrir.",
|
|
136
|
+
"unconfirmedMac": "Ainda sem notícias do iniciador. Se o navegador estiver pedindo permissão, escolha Abrir: uma janela do Terminal abre em seguida com o servidor.",
|
|
137
|
+
"unconfirmedWindows": "Ainda sem notícias do iniciador. Se o navegador estiver pedindo permissão, escolha Abrir: uma janela de console abre em seguida com o servidor.",
|
|
138
|
+
"unconfirmedLinux": "Ainda sem notícias do iniciador. Se o navegador estiver pedindo permissão, escolha Abrir: o servidor inicia em seguida em segundo plano.",
|
|
139
|
+
"stalled": "Nada respondeu. Se nenhum aviso ou janela apareceu, execute o comando abaixo uma vez: ele também conserta este botão.",
|
|
140
|
+
"launcherRunning": "O iniciador está em execução.",
|
|
141
|
+
"terminalOpened": "Uma janela do Terminal foi aberta para o servidor.",
|
|
142
|
+
"terminalQuiet": "Uma janela do Terminal foi aberta há {seconds} s, mas o servidor não iniciou nela. Veja essa janela.",
|
|
143
|
+
"processStarted": "O supervisor do servidor foi iniciado.",
|
|
144
|
+
"checking": "Verificando a instalação.",
|
|
145
|
+
"dependencies": "Instalando dependências com o npm. A primeira inicialização, ou a primeira depois de uma atualização, leva alguns minutos.",
|
|
146
|
+
"browser": "Baixando o navegador de automação. Isso acontece uma única vez.",
|
|
147
|
+
"build": "Compilando o servidor MCP.",
|
|
148
|
+
"snapshot": "Fazendo backup dos seus dados antes da primeira inicialização desta versão.",
|
|
149
|
+
"snapshotSized": "Fazendo backup dos seus dados antes da primeira inicialização desta versão: {files} arquivos, {megabytes} MB.",
|
|
150
|
+
"server": "O processo do servidor está em execução. Aguardando a resposta dele.",
|
|
151
|
+
"listening": "O servidor está escutando. Conectando.",
|
|
152
|
+
"answered": "O servidor respondeu ao iniciador. Conectando.",
|
|
153
|
+
"quiet": "Nada de novo na inicialização há {seconds} s.",
|
|
154
|
+
"failed": "A inicialização falhou: {reason}",
|
|
155
|
+
"failedHint": "Execute o comando abaixo para ver a saída completa, ou tente novamente.",
|
|
156
|
+
"failedLog": "Log do iniciador: {path}",
|
|
157
|
+
"gaveUp": "O servidor foi iniciado, mas não respondeu. Verifique a janela dele ou execute o comando abaixo.",
|
|
158
|
+
"online": "O servidor está no ar. Carregando o SynaBun.",
|
|
159
|
+
"trailLauncher": "Iniciador acionado",
|
|
160
|
+
"trailTerminal": "Terminal aberto",
|
|
161
|
+
"trailProcess": "Supervisor iniciado",
|
|
162
|
+
"trailSupervisor": "Supervisor em execução",
|
|
163
|
+
"trailServer": "Processo do servidor iniciado",
|
|
164
|
+
"trailListening": "Servidor escutando",
|
|
165
|
+
"elapsedLabel": "Tempo desde que Iniciar foi pressionado",
|
|
166
|
+
"headingStarting": "Iniciando o servidor",
|
|
167
|
+
"headingReady": "Servidor no ar",
|
|
168
|
+
"headingFailed": "O servidor não iniciou"
|
|
169
|
+
},
|
|
170
|
+
"runCommandOr": "Ou execute este comando:",
|
|
171
|
+
"runCommandSetup": "Execute este comando uma vez. Ele também configura o botão Iniciar:",
|
|
134
172
|
"serverOffline": "Servidor offline",
|
|
135
173
|
"serverNotRunning": "O servidor do Neural Interface não está em execução",
|
|
136
174
|
"connectionFailed": "Falha na conexão",
|
|
@@ -511,7 +549,7 @@
|
|
|
511
549
|
"help": "Armazenamento, busca e backups"
|
|
512
550
|
},
|
|
513
551
|
"ai_decisions": {
|
|
514
|
-
"label": "
|
|
552
|
+
"label": "Jev",
|
|
515
553
|
"help": "Avaliações inteligentes e custo"
|
|
516
554
|
},
|
|
517
555
|
"projects": {
|
|
@@ -557,7 +595,7 @@
|
|
|
557
595
|
"purpose": "Veja onde suas memórias ficam, escolha como o SynaBun as encontra e mantenha uma cópia que você possa restaurar."
|
|
558
596
|
},
|
|
559
597
|
"ai_decisions": {
|
|
560
|
-
"title": "
|
|
598
|
+
"title": "Jev",
|
|
561
599
|
"purpose": "Controle onde as avaliações inteligentes ajudam, o que podem usar e quanto custam."
|
|
562
600
|
},
|
|
563
601
|
"projects": {
|
|
@@ -3583,6 +3621,7 @@
|
|
|
3583
3621
|
"judgments": {
|
|
3584
3622
|
"showEveryJudgmentAboutThisMemory": ": mostrar no registro todas as avaliações desta memória",
|
|
3585
3623
|
"checkingTypesafe": "Verificando TypeSafe…",
|
|
3624
|
+
"learnMoreAtTypesafe": "Saiba mais em",
|
|
3586
3625
|
"jevTypesafeSystemOneReturnsTyped": "Jev (TypeSafe System One) retorna avaliações tipadas onde o SynaBun antes fazia estimativas com regex ou uma constante de similaridade. A chave fica armazenada em",
|
|
3587
3626
|
"andNeverLeavesTheServerOnly": "e nunca sai do servidor; apenas seus quatro últimos caracteres chegam a esta página.",
|
|
3588
3627
|
"apiKey": "Chave de API",
|
|
@@ -158,13 +158,16 @@ export function codexControlResponse(pending, response = {}) {
|
|
|
158
158
|
// Codex matches a reply by id value and type: 0 and "0" are different requests to it.
|
|
159
159
|
const base = { requestId: native.rpcId ?? pending.request_id, responseToken: randomUUID(), threadId: native.threadId || null, turnId: native.turnId || null, toolCallId: native.toolCallId || null };
|
|
160
160
|
if (method === 'item/commandExecution/requestApproval' || method === 'item/fileChange/requestApproval') {
|
|
161
|
-
const available = Array.isArray(native.availableDecisions)
|
|
162
|
-
? native.availableDecisions
|
|
161
|
+
const available = Array.isArray(native.availableDecisions)
|
|
162
|
+
? native.availableDecisions
|
|
163
163
|
: ['accept', 'acceptForSession', 'decline', 'cancel'];
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
// The UI puts policy amendment objects in result.decision and only their
|
|
165
|
+
// label in decision. Preserve the wire value for strict server validation.
|
|
166
|
+
let decision = response.result?.decision ?? response.decision;
|
|
167
|
+
if (decision == null) {
|
|
168
|
+
if (allow) decision = response.always && available.includes('acceptForSession') ? 'acceptForSession' : 'accept';
|
|
169
|
+
else decision = response.cancel && available.includes('cancel') ? 'cancel' : 'decline';
|
|
170
|
+
}
|
|
168
171
|
return { ...base, result: { decision } };
|
|
169
172
|
}
|
|
170
173
|
if (method === 'item/permissions/requestApproval') {
|
|
@@ -9,14 +9,21 @@
|
|
|
9
9
|
// Extracted from server.js so the model catalog can share it: discovery spawns
|
|
10
10
|
// the CLI by name too, and without this a daemon-started server silently
|
|
11
11
|
// degrades to the static fallback model list. The version-skew guard has the
|
|
12
|
-
// same dependency — it compares the installed CLI against the
|
|
13
|
-
// and a CLI it cannot resolve reads as "nothing to compare"
|
|
12
|
+
// same dependency — it compares the installed CLI against the release the Agent
|
|
13
|
+
// SDK was built for, and a CLI it cannot resolve reads as "nothing to compare".
|
|
14
|
+
//
|
|
15
|
+
// SYNABUN_TOOL_DISCOVERY=path turns the additions off: the tools are then looked
|
|
16
|
+
// for on the PATH this process was given and nowhere else. It is what a check
|
|
17
|
+
// of "nothing is installed" needs on a machine where something is (the
|
|
18
|
+
// packaged application's own run), and what a user sets to make SynaBun see
|
|
19
|
+
// exactly their PATH.
|
|
14
20
|
|
|
15
21
|
import os from 'node:os';
|
|
16
22
|
import { join, dirname, delimiter } from 'node:path';
|
|
17
23
|
import { existsSync } from 'node:fs';
|
|
18
24
|
|
|
19
25
|
export function getAugmentedPath() {
|
|
26
|
+
if (process.env.SYNABUN_TOOL_DISCOVERY === 'path') return process.env.PATH || '';
|
|
20
27
|
const home = os.homedir();
|
|
21
28
|
const npmPrefix = process.env.NPM_CONFIG_PREFIX || process.env.npm_config_prefix || '';
|
|
22
29
|
const extra = process.platform === 'win32'
|
|
@@ -38,7 +45,20 @@ export function getAugmentedPath() {
|
|
|
38
45
|
join(home, '.npm-global', 'bin'),
|
|
39
46
|
npmPrefix ? join(npmPrefix, 'bin') : '',
|
|
40
47
|
];
|
|
41
|
-
|
|
48
|
+
// Where the tools' own installers put them, for a PATH that names none of it:
|
|
49
|
+
// Claude Code's native installer (also on Windows), its older local install,
|
|
50
|
+
// OpenCode's install script. After the user's PATH, so that they never
|
|
51
|
+
// outrank a choice the user made there.
|
|
52
|
+
const fallback = [
|
|
53
|
+
join(home, '.local', 'bin'),
|
|
54
|
+
join(home, '.claude', 'local'),
|
|
55
|
+
join(home, '.opencode', 'bin'),
|
|
56
|
+
];
|
|
57
|
+
const entries = [
|
|
58
|
+
...extra.filter(d => d && existsSync(d)),
|
|
59
|
+
...(process.env.PATH || '').split(delimiter),
|
|
60
|
+
...fallback.filter(d => existsSync(d)),
|
|
61
|
+
];
|
|
42
62
|
const seen = new Set();
|
|
43
63
|
return entries.filter((entry) => {
|
|
44
64
|
if (!entry) return false;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const DEFAULT_LOCALE = 'en-US';
|
|
2
|
+
const DEFAULT_ACCEPT_LANGUAGE = 'en-US,en;q=0.9';
|
|
3
|
+
// HTTP language ranges allow '*' and an optional qvalue with at most three decimals.
|
|
4
|
+
const LANGUAGE_RANGE = /^(\*|[a-z]{1,8}(?:-[a-z0-9]{1,8})*)(?:[ \t]*;[ \t]*q[ \t]*=[ \t]*(0(?:\.\d{0,3})?|1(?:\.0{0,3})?))?$/i;
|
|
5
|
+
|
|
6
|
+
function isAutomatic(value) {
|
|
7
|
+
return value == null || (typeof value === 'string' && !value.trim());
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function canonicalLocale(value) {
|
|
11
|
+
if (typeof value !== 'string' || !value.trim()) return null;
|
|
12
|
+
try {
|
|
13
|
+
return Intl.getCanonicalLocales(value.trim())[0] || null;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseAcceptLanguage(value) {
|
|
20
|
+
if (typeof value !== 'string' || /[\x00-\x08\x0a-\x1f\x7f]/.test(value)) return null;
|
|
21
|
+
const header = value.trim();
|
|
22
|
+
const entries = header.split(',').map(entry => entry.trim()).filter(Boolean);
|
|
23
|
+
if (!entries.length) return null;
|
|
24
|
+
|
|
25
|
+
const languages = new Set();
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const match = LANGUAGE_RANGE.exec(entry);
|
|
28
|
+
if (!match) return null;
|
|
29
|
+
const [, range, quality] = match;
|
|
30
|
+
if (range === '*' || (quality !== undefined && Number(quality) === 0)) continue;
|
|
31
|
+
// A valid HTTP language range need not be a locale supported by Intl (e.g. x-private).
|
|
32
|
+
const locale = canonicalLocale(range);
|
|
33
|
+
if (locale) languages.add(locale);
|
|
34
|
+
}
|
|
35
|
+
return { header, languages: [...languages] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Resolve legacy settings and request headers without rewriting the saved config. */
|
|
39
|
+
export function resolveBrowserLanguage(savedCfg = {}, requestAcceptLanguage) {
|
|
40
|
+
const savedLocale = canonicalLocale(savedCfg.locale);
|
|
41
|
+
const savedHeader = parseAcceptLanguage(savedCfg.acceptLanguage);
|
|
42
|
+
const requestHeader = savedHeader ? null : parseAcceptLanguage(requestAcceptLanguage);
|
|
43
|
+
const resolvedHeader = savedHeader || requestHeader || parseAcceptLanguage(DEFAULT_ACCEPT_LANGUAGE);
|
|
44
|
+
const locale = savedLocale || resolvedHeader.languages[0] || DEFAULT_LOCALE;
|
|
45
|
+
const warnings = [];
|
|
46
|
+
if (!isAutomatic(savedCfg.locale) && !savedLocale) {
|
|
47
|
+
warnings.push({ field: 'locale', value: locale });
|
|
48
|
+
}
|
|
49
|
+
if (!isAutomatic(savedCfg.acceptLanguage) && !savedHeader) {
|
|
50
|
+
warnings.push({ field: 'acceptLanguage', value: resolvedHeader.header });
|
|
51
|
+
}
|
|
52
|
+
if (!savedHeader && !isAutomatic(requestAcceptLanguage) && !requestHeader) {
|
|
53
|
+
warnings.push({ field: 'request Accept-Language', value: resolvedHeader.header });
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
locale,
|
|
57
|
+
acceptLanguage: resolvedHeader.header,
|
|
58
|
+
languages: resolvedHeader.languages.length ? resolvedHeader.languages : [locale],
|
|
59
|
+
warnings,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Return a normalized copy; invalid explicit language fields throw with status/field. */
|
|
64
|
+
export function validateBrowserLanguageConfig(config) {
|
|
65
|
+
const normalized = { ...config };
|
|
66
|
+
for (const field of ['locale', 'acceptLanguage']) {
|
|
67
|
+
if (!Object.hasOwn(config, field)) continue;
|
|
68
|
+
const value = config[field];
|
|
69
|
+
if (isAutomatic(value)) {
|
|
70
|
+
normalized[field] = null;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const validated = field === 'locale' ? canonicalLocale(value) : parseAcceptLanguage(value)?.header;
|
|
74
|
+
if (!validated) {
|
|
75
|
+
const help = field === 'locale'
|
|
76
|
+
? 'use a single locale tag such as pt-BR, or leave it blank for automatic detection'
|
|
77
|
+
: 'use comma-separated language ranges (or *) with optional q weights between 0 and 1 (up to three decimal places), or leave it blank';
|
|
78
|
+
throw Object.assign(new Error(`Invalid ${field}: ${help}.`), { status: 400, field });
|
|
79
|
+
}
|
|
80
|
+
normalized[field] = validated;
|
|
81
|
+
}
|
|
82
|
+
return normalized;
|
|
83
|
+
}
|
|
@@ -25,14 +25,9 @@ import { detectProject as memoryProject } from '../../mcp-server/dist/config.js'
|
|
|
25
25
|
// WS bound, so page-refresh reattach is the same object.
|
|
26
26
|
|
|
27
27
|
import { query, startup } from '@anthropic-ai/claude-agent-sdk';
|
|
28
|
-
import { CLAUDE_EFFORT_LEVELS
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
clearNativeBinaryCache,
|
|
32
|
-
isNativeBinaryLaunchFailure,
|
|
33
|
-
planRuntimeRecovery,
|
|
34
|
-
resolveClaudeExecutableOverride,
|
|
35
|
-
} from './native-binary-runtime.js';
|
|
28
|
+
import { CLAUDE_EFFORT_LEVELS } from './claude-model-catalog.js';
|
|
29
|
+
import { CLAUDE_NOT_INSTALLED, resolveClaudeSdkExecutable } from './claude-executable.js';
|
|
30
|
+
import { diagnoseLaunchFailure, isNativeBinaryLaunchFailure } from './native-binary-runtime.js';
|
|
36
31
|
import { normalizePanelSession, startSignature, applyPanelSessionOptions, liveSettingsPatch, normalizeMcpServers, mergeTabMcpServers } from './claude-panel-session.js';
|
|
37
32
|
import { SESSION_REQUESTS, runSessionRequest, slimRewind, slimMcpStatus } from './claude-panel-requests.js';
|
|
38
33
|
import { applyTemporaryOptions, plansDirsOf, planFileOfToolCall, removeTemporaryPlanFiles } from './claude-temporary.js';
|
|
@@ -348,15 +343,9 @@ export class ClaudeSession {
|
|
|
348
343
|
this.lastPrompt = null;
|
|
349
344
|
this.stallRetries = 0;
|
|
350
345
|
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
this._nativeRepairTried = false;
|
|
355
|
-
this._fallbackBin = null;
|
|
356
|
-
// The installed CLI the current Query runs on because it is newer than the
|
|
357
|
-
// bundled one (see ensureQuery). Per-Query, not a latch: a launch failure is
|
|
358
|
-
// latched process-wide in claude-model-catalog.js instead.
|
|
359
|
-
this._alignedBin = null;
|
|
346
|
+
// The Claude Code executable the current Query was started with (see
|
|
347
|
+
// ensureQuery): what a launch failure is diagnosed against.
|
|
348
|
+
this._claudeExecutable = null;
|
|
360
349
|
|
|
361
350
|
this.pendingPerms = new Map(); // requestId → { toolName, resolve, input, kind, wire }
|
|
362
351
|
this.alwaysAllowed = new Set();
|
|
@@ -499,6 +488,31 @@ export class ClaudeSession {
|
|
|
499
488
|
return null;
|
|
500
489
|
}
|
|
501
490
|
}
|
|
491
|
+
// Which Claude Code runs this session: the user's own installation, always.
|
|
492
|
+
// SynaBun carries none (lib/external-tools.js at the package root) and the
|
|
493
|
+
// SDK is never left to look for one. The host answers (deps.claudeExecutable,
|
|
494
|
+
// lib/claude-executable.js): an explicit override from cli-config.json
|
|
495
|
+
// ("claude-skin": { "sdkExecutable": "…" }), else the installed Claude Code.
|
|
496
|
+
// The model picker lists that same installation's models, so an alias means
|
|
497
|
+
// in the session what it meant in the picker.
|
|
498
|
+
//
|
|
499
|
+
// Nothing installed is an ordinary state: the start is refused with the
|
|
500
|
+
// sentence the panel shows its install help on, and nothing was started.
|
|
501
|
+
// The SDK itself is never called without an executable; only a host that
|
|
502
|
+
// brings its own query factory and no resolver (a test) starts without one.
|
|
503
|
+
let executable = null;
|
|
504
|
+
if (typeof deps.claudeExecutable === 'function') {
|
|
505
|
+
try { executable = deps.claudeExecutable() || null; } catch { executable = null; }
|
|
506
|
+
} else if (deps.sdkExecutable) {
|
|
507
|
+
executable = resolveClaudeSdkExecutable({ sdkExecutable: deps.sdkExecutable });
|
|
508
|
+
}
|
|
509
|
+
const ownFactory = typeof (warm ? deps.startupFactory : deps.queryFactory) === 'function';
|
|
510
|
+
if (!executable?.path && (typeof deps.claudeExecutable === 'function' || !ownFactory)) {
|
|
511
|
+
this._closeWarm();
|
|
512
|
+
this._startRefused = { code: 'claude_not_installed', message: executable?.reason || CLAUDE_NOT_INSTALLED };
|
|
513
|
+
log(`session create refused: no Claude Code to run (${executable?.reason || 'not installed'})`);
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
502
516
|
if (this._warm) {
|
|
503
517
|
const w = this._warm;
|
|
504
518
|
this._warm = null;
|
|
@@ -732,51 +746,20 @@ export class ClaudeSession {
|
|
|
732
746
|
this._tempPlansDirs = plansDirsOf(options.env, { cwd: workDir, settings: options.settings });
|
|
733
747
|
if (!this._tempSince) this._tempSince = Date.now();
|
|
734
748
|
}
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
this._alignedBin = null;
|
|
750
|
-
let runtimeLabel = 'bundled';
|
|
751
|
-
if (override.path) {
|
|
752
|
-
options.pathToClaudeCodeExecutable = override.path;
|
|
753
|
-
runtimeLabel = 'override';
|
|
754
|
-
} else if (sdkExec) {
|
|
755
|
-
log('ignoring sdkExecutable override:', override.reason);
|
|
756
|
-
this.send({ type: 'stderr', text: `Ignoring sdkExecutable override — ${override.reason}` });
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
if (!options.pathToClaudeCodeExecutable) {
|
|
760
|
-
let aligned = null;
|
|
761
|
-
if (!this._fallbackBin) {
|
|
762
|
-
try { aligned = deps.alignedClaudeBin?.() || null; } catch { aligned = null; }
|
|
763
|
-
}
|
|
764
|
-
if (this._fallbackBin) {
|
|
765
|
-
// A previous turn proved the bundled binary unusable; stay on the CLI we
|
|
766
|
-
// already fell back to rather than re-failing every turn.
|
|
767
|
-
options.pathToClaudeCodeExecutable = this._fallbackBin;
|
|
768
|
-
runtimeLabel = 'fallback';
|
|
769
|
-
} else if (aligned) {
|
|
770
|
-
options.pathToClaudeCodeExecutable = aligned;
|
|
771
|
-
this._alignedBin = aligned;
|
|
772
|
-
runtimeLabel = aligned;
|
|
773
|
-
} else {
|
|
774
|
-
// Called for its side effect only: this stats the binary the SDK is about
|
|
775
|
-
// to resolve and restores the execute bit if it is missing. We
|
|
776
|
-
// deliberately do NOT pass the resolved path on — letting the SDK resolve
|
|
777
|
-
// for itself keeps us correct even if its resolution logic changes.
|
|
778
|
-
claudeRuntime();
|
|
779
|
-
}
|
|
749
|
+
// The executable resolved at the top of this call. The SDK uses
|
|
750
|
+
// pathToClaudeCodeExecutable verbatim with zero validation, so it was vetted
|
|
751
|
+
// there — notably a bare command name, which would ENOENT under the SDK's
|
|
752
|
+
// spawn(shell:false).
|
|
753
|
+
let runtimeLabel = 'unset';
|
|
754
|
+
this._claudeExecutable = null;
|
|
755
|
+
if (executable?.ignored) {
|
|
756
|
+
log('ignoring sdkExecutable override:', executable.ignored);
|
|
757
|
+
this.send({ type: 'stderr', text: `Ignoring sdkExecutable override — ${executable.ignored}` });
|
|
758
|
+
}
|
|
759
|
+
if (executable?.path) {
|
|
760
|
+
options.pathToClaudeCodeExecutable = executable.path;
|
|
761
|
+
this._claudeExecutable = executable.path;
|
|
762
|
+
runtimeLabel = executable.source === 'override' ? 'override' : executable.path;
|
|
780
763
|
}
|
|
781
764
|
|
|
782
765
|
if (warm) {
|
|
@@ -1256,94 +1239,18 @@ export class ClaudeSession {
|
|
|
1256
1239
|
}
|
|
1257
1240
|
|
|
1258
1241
|
/**
|
|
1259
|
-
* The
|
|
1242
|
+
* The user's Claude Code would not start.
|
|
1260
1243
|
*
|
|
1261
1244
|
* The SDK's own message for this always blames a musl/glibc mismatch, which is
|
|
1262
|
-
* only ever right on Linux —
|
|
1263
|
-
*
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1266
|
-
* Recovery is: re-stat and repair → retry once → fall back to the user's
|
|
1267
|
-
* installed CLI with a visible notice → give up with an accurate diagnosis.
|
|
1245
|
+
* only ever right on Linux — elsewhere the cause is a missing execute bit, a
|
|
1246
|
+
* quarantined download, a build for another processor or a file that is gone.
|
|
1247
|
+
* There is no other runtime to move to (SynaBun carries none), so the turn
|
|
1248
|
+
* ends with what is wrong with that installation and how to fix it.
|
|
1268
1249
|
*/
|
|
1269
1250
|
_recoverNativeRuntime(err) {
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
// its own repair path below if it fails too.
|
|
1274
|
-
if (this._alignedBin) {
|
|
1275
|
-
const failed = this._alignedBin;
|
|
1276
|
-
this._alignedBin = null;
|
|
1277
|
-
markAlignedClaudeExecutableFailed(failed);
|
|
1278
|
-
log(`aligned CLI failed to launch (${failed}) — using the bundled runtime`);
|
|
1279
|
-
this.sendEvent({
|
|
1280
|
-
type: 'system',
|
|
1281
|
-
subtype: 'runtime_notice',
|
|
1282
|
-
level: 'warn',
|
|
1283
|
-
message: `Your installed Claude CLI (${failed}) failed to launch; using the bundled runtime instead.`,
|
|
1284
|
-
});
|
|
1285
|
-
this._recreateQuery(prompt);
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
// The cached verdict predates the failure; re-stat so a repair performed by
|
|
1289
|
-
// another session (or the boot sweep) is visible.
|
|
1290
|
-
clearNativeBinaryCache();
|
|
1291
|
-
const runtime = claudeRuntime();
|
|
1292
|
-
|
|
1293
|
-
let fallbackBin = this._fallbackBin;
|
|
1294
|
-
if (!fallbackBin) {
|
|
1295
|
-
// getClaudeBin()'s last resort is the bare string 'claude'. The SDK spawns
|
|
1296
|
-
// with shell:false, so a bare name is a guaranteed ENOENT — the override
|
|
1297
|
-
// validator rejects it, leaving fallbackBin null and giving the user a
|
|
1298
|
-
// real diagnosis instead of a second, more confusing failure.
|
|
1299
|
-
const candidate = resolveClaudeExecutableOverride(deps.getClaudeBin?.());
|
|
1300
|
-
if (candidate.ok && candidate.path) fallbackBin = candidate.path;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const plan = planRuntimeRecovery({
|
|
1304
|
-
err,
|
|
1305
|
-
runtime,
|
|
1306
|
-
fallbackBin,
|
|
1307
|
-
alreadyTried: this._nativeRepairTried,
|
|
1308
|
-
});
|
|
1309
|
-
log(`native runtime recovery: action=${plan.action} state=${runtime.state} kind=${plan.kind}`);
|
|
1310
|
-
|
|
1311
|
-
if (plan.action === 'retry') {
|
|
1312
|
-
this._nativeRepairTried = true;
|
|
1313
|
-
this.sendEvent({ type: 'system', subtype: 'runtime_notice', level: 'warn', message: plan.message });
|
|
1314
|
-
this._recreateQuery(prompt);
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
if (plan.action === 'fallback') {
|
|
1319
|
-
this._fallbackBin = fallbackBin;
|
|
1320
|
-
this.sendEvent({ type: 'system', subtype: 'runtime_notice', level: 'warn', message: plan.message });
|
|
1321
|
-
// Report-only: the fallback CLI may be a different version than the SDK
|
|
1322
|
-
// was built against, and alias ids like "opus[1m]" resolve per-binary — so
|
|
1323
|
-
// a picked model can silently launch as another version.
|
|
1324
|
-
this._announceSkew(fallbackBin);
|
|
1325
|
-
this._recreateQuery(prompt);
|
|
1326
|
-
return;
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
this._finishTurnWithError(plan.message);
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
_announceSkew(bin) {
|
|
1333
|
-
Promise.resolve()
|
|
1334
|
-
.then(() => checkClaudeCliSkew(bin, { log: () => {} }))
|
|
1335
|
-
.then((skew) => {
|
|
1336
|
-
if (!skew?.skewed || this.destroyed) return;
|
|
1337
|
-
this.sendEvent({
|
|
1338
|
-
type: 'system',
|
|
1339
|
-
subtype: 'runtime_notice',
|
|
1340
|
-
level: 'warn',
|
|
1341
|
-
message: `Version skew: your installed Claude CLI is ${skew.discovered}, but the bundled `
|
|
1342
|
-
+ `runtime is ${skew.bundled}. Model aliases and effort levels resolve per-binary, so `
|
|
1343
|
-
+ 'behaviour may differ until the bundled binary is repaired.',
|
|
1344
|
-
});
|
|
1345
|
-
})
|
|
1346
|
-
.catch(() => {});
|
|
1251
|
+
const dx = diagnoseLaunchFailure({ err, executable: this._claudeExecutable });
|
|
1252
|
+
log(`Claude Code failed to launch: kind=${dx.kind} executable=${this._claudeExecutable || 'unset'}`);
|
|
1253
|
+
this._finishTurnWithError(dx.message);
|
|
1347
1254
|
}
|
|
1348
1255
|
|
|
1349
1256
|
_pushUserText(text, images, { human = false } = {}) {
|
|
@@ -2222,7 +2129,7 @@ export class ClaudeSession {
|
|
|
2222
2129
|
// AskUserQuestion answers arrive as updatedInput {questions, answers} from the
|
|
2223
2130
|
// existing frontend card. The CLI's AskUserQuestion tool reads ONLY the
|
|
2224
2131
|
// top-level `answers` map, looked up by each question's exact `question` text
|
|
2225
|
-
// (verified against the
|
|
2132
|
+
// (verified against the CLI itself: missing/empty map → "The user did not
|
|
2226
2133
|
// answer the questions."). So: keep the original questions, pass the answers
|
|
2227
2134
|
// map through, and re-key any answer the panel stored under a fallback key
|
|
2228
2135
|
// (text/header) back onto the question text.
|