From 75c6a5e22e7665fcacc741ea65f18564c2307d4d Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Thu, 28 May 2026 21:52:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=88=A0=E9=99=A4=20edit=20tool=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=97=A7=E9=80=BB=E8=BE=91=E5=A4=84=E7=90=86?= =?UTF-8?q?=EF=BC=8C=20=E7=8E=B0=E5=9C=A8=E5=B7=B2=E7=BB=8F=E4=B8=8D?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E8=BF=99=E4=BA=9B=E5=A4=84=E7=90=86=E4=BA=86?= =?UTF-8?q?=EF=BC=8C=20=E5=A4=A7=E6=A8=A1=E5=9E=8B=E5=A4=9F=E5=B1=8C=20(#1?= =?UTF-8?q?251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: remove tab/quote normalization from FileEditTool * fix: resolve pre-existing typecheck errors (zod v4 compat + RCS web exclude) --- src/cli/print.ts | 4 +- src/components/FileEditToolDiff.tsx | 139 +++---- src/entrypoints/cli.tsx | 399 +++++++++++--------- src/hooks/useIdeAtMentioned.ts | 2 +- src/hooks/useIdeLogging.ts | 2 +- src/hooks/useIdeSelection.ts | 2 +- src/hooks/usePromptsFromClaudeInChrome.tsx | 137 +++---- src/services/mcp/useManageMCPConnections.ts | 269 +++++++------ src/services/mcp/vscodeSdkMcp.ts | 2 +- tsconfig.json | 52 ++- 10 files changed, 507 insertions(+), 501 deletions(-) diff --git a/src/cli/print.ts b/src/cli/print.ts index 1e19abff5..fd179d0ba 100644 --- a/src/cli/print.ts +++ b/src/cli/print.ts @@ -4749,7 +4749,7 @@ function handleChannelEnable( // channel messages queue at priority 'next' and are seen by the model on // the turn after they arrive. connection.client.setNotificationHandler( - ChannelMessageNotificationSchema(), + ChannelMessageNotificationSchema() as any, async notification => { const { content, meta } = notification.params logMCPDebug( @@ -4825,7 +4825,7 @@ function reregisterChannelHandlerAfterReconnect( 'Channel notifications re-registered after reconnect', ) connection.client.setNotificationHandler( - ChannelMessageNotificationSchema(), + ChannelMessageNotificationSchema() as any, async notification => { const { content, meta } = notification.params logMCPDebug( diff --git a/src/components/FileEditToolDiff.tsx b/src/components/FileEditToolDiff.tsx index 75efda0f5..f8ca04a0f 100644 --- a/src/components/FileEditToolDiff.tsx +++ b/src/components/FileEditToolDiff.tsx @@ -1,62 +1,42 @@ -import type { StructuredPatchHunk } from 'diff' -import * as React from 'react' -import { Suspense, use, useState } from 'react' -import { useTerminalSize } from '../hooks/useTerminalSize.js' -import { Box, Text } from '@anthropic/ink' -import type { FileEdit } from '../tools/FileEditTool/types.js' -import { - findActualString, - preserveQuoteStyle, -} from '../tools/FileEditTool/utils.js' -import { - adjustHunkLineNumbers, - CONTEXT_LINES, - getPatchForDisplay, -} from '../utils/diff.js' -import { logError } from '../utils/log.js' -import { - CHUNK_SIZE, - openForScan, - readCapped, - scanForContext, -} from '../utils/readEditContext.js' -import { firstLineOf } from '../utils/stringUtils.js' -import { StructuredDiffList } from './StructuredDiffList.js' +import type { StructuredPatchHunk } from 'diff'; +import * as React from 'react'; +import { Suspense, use, useState } from 'react'; +import { useTerminalSize } from '../hooks/useTerminalSize.js'; +import { Box, Text } from '@anthropic/ink'; +import type { FileEdit } from '@claude-code-best/builtin-tools/tools/FileEditTool/types.js'; +import { findActualString } from '@claude-code-best/builtin-tools/tools/FileEditTool/utils.js'; +import { adjustHunkLineNumbers, CONTEXT_LINES, getPatchForDisplay } from '../utils/diff.js'; +import { logError } from '../utils/log.js'; +import { CHUNK_SIZE, openForScan, readCapped, scanForContext } from '../utils/readEditContext.js'; +import { firstLineOf } from '../utils/stringUtils.js'; +import { StructuredDiffList } from './StructuredDiffList.js'; type Props = { - file_path: string - edits: FileEdit[] -} + file_path: string; + edits: FileEdit[]; +}; type DiffData = { - patch: StructuredPatchHunk[] - firstLine: string | null - fileContent: string | undefined -} + patch: StructuredPatchHunk[]; + firstLine: string | null; + fileContent: string | undefined; +}; export function FileEditToolDiff(props: Props): React.ReactNode { // Snapshot on mount — the diff must stay consistent even if the file changes // while the dialog is open. useMemo on props.edits would re-read the file on // every render because callers pass fresh array literals. - const [dataPromise] = useState(() => - loadDiffData(props.file_path, props.edits), - ) + const [dataPromise] = useState(() => loadDiffData(props.file_path, props.edits)); return ( }> - ) + ); } -function DiffBody({ - promise, - file_path, -}: { - promise: Promise - file_path: string -}): React.ReactNode { - const { patch, firstLine, fileContent } = use(promise) - const { columns } = useTerminalSize() +function DiffBody({ promise, file_path }: { promise: Promise; file_path: string }): React.ReactNode { + const { patch, firstLine, fileContent } = use(promise); + const { columns } = useTerminalSize(); return ( - ) + ); } -function DiffFrame({ - children, - placeholder, -}: { - children?: React.ReactNode - placeholder?: boolean -}): React.ReactNode { +function DiffFrame({ children, placeholder }: { children?: React.ReactNode; placeholder?: boolean }): React.ReactNode { return ( - + {placeholder ? : children} - ) + ); } -async function loadDiffData( - file_path: string, - edits: FileEdit[], -): Promise { - const valid = edits.filter(e => e.old_string != null && e.new_string != null) - const single = valid.length === 1 ? valid[0]! : undefined +async function loadDiffData(file_path: string, edits: FileEdit[]): Promise { + const valid = edits.filter(e => e.old_string != null && e.new_string != null); + const single = valid.length === 1 ? valid[0]! : undefined; // SedEditPermissionRequest passes the entire file as old_string. Scanning for // a needle ≥ CHUNK_SIZE allocates O(needle) for the overlap buffer — skip the // file read entirely and diff the inputs we already have. if (single && single.old_string.length >= CHUNK_SIZE) { - return diffToolInputsOnly(file_path, [single]) + return diffToolInputsOnly(file_path, [single]); } try { - const handle = await openForScan(file_path) - if (handle === null) return diffToolInputsOnly(file_path, valid) + const handle = await openForScan(file_path); + if (handle === null) return diffToolInputsOnly(file_path, valid); try { // Multi-edit and empty old_string genuinely need full-file for sequential // replacements — structuredPatch needs before/after strings. replace_all // routes through the chunked path below (shows first-occurrence window; // matches within the slice still replace via edit.replace_all). if (!single || single.old_string === '') { - const file = await readCapped(handle) - if (file === null) return diffToolInputsOnly(file_path, valid) - const normalized = valid.map(e => normalizeEdit(file, e)) + const file = await readCapped(handle); + if (file === null) return diffToolInputsOnly(file_path, valid); + const normalized = valid.map(e => normalizeEdit(file, e)); return { patch: getPatchForDisplay({ filePath: file_path, @@ -127,30 +92,30 @@ async function loadDiffData( }), firstLine: firstLineOf(file), fileContent: file, - } + }; } - const ctx = await scanForContext(handle, single.old_string, CONTEXT_LINES) + const ctx = await scanForContext(handle, single.old_string, CONTEXT_LINES); if (ctx.truncated || ctx.content === '') { - return diffToolInputsOnly(file_path, [single]) + return diffToolInputsOnly(file_path, [single]); } - const normalized = normalizeEdit(ctx.content, single) + const normalized = normalizeEdit(ctx.content, single); const hunks = getPatchForDisplay({ filePath: file_path, fileContents: ctx.content, edits: [normalized], - }) + }); return { patch: adjustHunkLineNumbers(hunks, ctx.lineOffset - 1), firstLine: ctx.lineOffset === 1 ? firstLineOf(ctx.content) : null, fileContent: ctx.content, - } + }; } finally { - await handle.close() + await handle.close(); } } catch (e) { - logError(e as Error) - return diffToolInputsOnly(file_path, valid) + logError(e as Error); + return diffToolInputsOnly(file_path, valid); } } @@ -165,16 +130,10 @@ function diffToolInputsOnly(filePath: string, edits: FileEdit[]): DiffData { ), firstLine: null, fileContent: undefined, - } + }; } function normalizeEdit(fileContent: string, edit: FileEdit): FileEdit { - const actualOld = - findActualString(fileContent, edit.old_string) || edit.old_string - const actualNew = preserveQuoteStyle( - edit.old_string, - actualOld, - edit.new_string, - ) - return { ...edit, old_string: actualOld, new_string: actualNew } + const actualOld = findActualString(fileContent, edit.old_string) || edit.old_string; + return { ...edit, old_string: actualOld }; } diff --git a/src/entrypoints/cli.tsx b/src/entrypoints/cli.tsx index 4cce50e35..7358e7ed2 100644 --- a/src/entrypoints/cli.tsx +++ b/src/entrypoints/cli.tsx @@ -1,10 +1,15 @@ #!/usr/bin/env bun -import { feature } from 'bun:bundle' +// Performance shim MUST be the first import — it replaces globalThis.performance +// with a JS-backed implementation before React/OTel capture the native reference. +// Without this, JSC's C++ Vector grows without bound in long-running sessions. +import '../utils/performanceShim.js'; +import { feature } from 'bun:bundle'; +import { isEnvTruthy } from '../utils/envUtils.js'; // Runtime fallback for MACRO.* when not injected by build/dev defines. // This happens when running cli.tsx directly (not via `bun run dev` or built dist/). if (typeof globalThis.MACRO === 'undefined') { - ;(globalThis as any).MACRO = { + (globalThis as any).MACRO = { VERSION: process.env.CLAUDE_CODE_VERSION || '2.1.888', BUILD_TIME: new Date().toISOString(), FEEDBACK_CHANNEL: '', @@ -12,22 +17,35 @@ if (typeof globalThis.MACRO === 'undefined') { NATIVE_PACKAGE_URL: '', PACKAGE_URL: '', VERSION_CHANGELOG: '', + }; +} + +if (isEnvTruthy(process.env.CLAUDE_CODE_FORCE_INTERACTIVE)) { + for (const stream of [process.stdin, process.stdout, process.stderr]) { + if (!stream.isTTY) { + try { + Object.defineProperty(stream, 'isTTY', { + value: true, + configurable: true, + }); + } catch { + // Best-effort dev-only override for nested bun launch on Windows. + } + } } } // Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons // eslint-disable-next-line custom-rules/no-top-level-side-effects -process.env.COREPACK_ENABLE_AUTO_PIN = '0' +process.env.COREPACK_ENABLE_AUTO_PIN = '0'; // Set max heap size for child processes in CCR environments (containers have 16GB) // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level, custom-rules/safe-env-boolean-check if (process.env.CLAUDE_CODE_REMOTE === 'true') { // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level - const existing = process.env.NODE_OPTIONS || '' + const existing = process.env.NODE_OPTIONS || ''; // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level - process.env.NODE_OPTIONS = existing - ? `${existing} --max-old-space-size=8192` - : '--max-old-space-size=8192' + process.env.NODE_OPTIONS = existing ? `${existing} --max-old-space-size=8192` : '--max-old-space-size=8192'; } // Harness-science L0 ablation baseline. Inlined here (not init.ts) because @@ -46,7 +64,7 @@ if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) { 'CLAUDE_CODE_DISABLE_BACKGROUND_TASKS', ]) { // eslint-disable-next-line custom-rules/no-top-level-side-effects, custom-rules/no-process-env-top-level - process.env[k] ??= '1' + process.env[k] ??= '1'; } } @@ -56,64 +74,86 @@ if (feature('ABLATION_BASELINE') && process.env.CLAUDE_CODE_ABLATION_BASELINE) { * Fast-path for --version has zero imports beyond this file. */ async function main(): Promise { - const args = process.argv.slice(2) + const args = process.argv.slice(2); // Fast-path for --version/-v: zero module loading needed - if ( - args.length === 1 && - (args[0] === '--version' || args[0] === '-v' || args[0] === '-V') - ) { + if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) { // MACRO.VERSION is inlined at build time - // biome-ignore lint/suspicious/noConsole:: intentional console output - console.log(`${MACRO.VERSION} (Claude Code)`) - return + console.log(`${MACRO.VERSION} (Claude Code)`); + return; } // For all other paths, load the startup profiler - const { profileCheckpoint } = await import('../utils/startupProfiler.js') - profileCheckpoint('cli_entry') + const { profileCheckpoint } = await import('../utils/startupProfiler.js'); + profileCheckpoint('cli_entry'); // Fast-path for --dump-system-prompt: output the rendered system prompt and exit. // Used by prompt sensitivity evals to extract the system prompt at a specific commit. // Ant-only: eliminated from external builds via feature flag. if (feature('DUMP_SYSTEM_PROMPT') && args[0] === '--dump-system-prompt') { - profileCheckpoint('cli_dump_system_prompt_path') - const { enableConfigs } = await import('../utils/config.js') - enableConfigs() - const { getMainLoopModel } = await import('../utils/model/model.js') - const modelIdx = args.indexOf('--model') - const model = (modelIdx !== -1 && args[modelIdx + 1]) || getMainLoopModel() - const { getSystemPrompt } = await import('../constants/prompts.js') - const prompt = await getSystemPrompt([], model) - // biome-ignore lint/suspicious/noConsole:: intentional console output - console.log(prompt.join('\n')) - return + profileCheckpoint('cli_dump_system_prompt_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const { getMainLoopModel } = await import('../utils/model/model.js'); + const modelIdx = args.indexOf('--model'); + const model = (modelIdx !== -1 && args[modelIdx + 1]) || getMainLoopModel(); + const { getSystemPrompt } = await import('../constants/prompts.js'); + const prompt = await getSystemPrompt([], model); + console.log(prompt.join('\n')); + return; } if (process.argv[2] === '--claude-in-chrome-mcp') { - profileCheckpoint('cli_claude_in_chrome_mcp_path') - const { runClaudeInChromeMcpServer } = await import( - '../utils/claudeInChrome/mcpServer.js' - ) - await runClaudeInChromeMcpServer() - return + profileCheckpoint('cli_claude_in_chrome_mcp_path'); + const { runClaudeInChromeMcpServer } = await import('../utils/claudeInChrome/mcpServer.js'); + await runClaudeInChromeMcpServer(); + return; } else if (process.argv[2] === '--chrome-native-host') { - profileCheckpoint('cli_chrome_native_host_path') - const { runChromeNativeHost } = await import( - '../utils/claudeInChrome/chromeNativeHost.js' - ) - await runChromeNativeHost() - return - } else if ( - feature('CHICAGO_MCP') && - process.argv[2] === '--computer-use-mcp' - ) { - profileCheckpoint('cli_computer_use_mcp_path') - const { runComputerUseMcpServer } = await import( - '../utils/computerUse/mcpServer.js' - ) - await runComputerUseMcpServer() - return + profileCheckpoint('cli_chrome_native_host_path'); + const { runChromeNativeHost } = await import('../utils/claudeInChrome/chromeNativeHost.js'); + await runChromeNativeHost(); + return; + } else if (feature('CHICAGO_MCP') && process.argv[2] === '--computer-use-mcp') { + profileCheckpoint('cli_computer_use_mcp_path'); + const { runComputerUseMcpServer } = await import('../utils/computerUse/mcpServer.js'); + await runComputerUseMcpServer(); + return; + } + + // Fast-path for `--acp` — ACP (Agent Client Protocol) agent mode over stdio. + if (feature('ACP') && process.argv[2] === '--acp') { + profileCheckpoint('cli_acp_path'); + const { runAcpAgent } = await import('../services/acp/entry.js'); + await runAcpAgent(); + return; + } + + if (args[0] === 'weixin') { + profileCheckpoint('cli_weixin_path'); + const { handleWeixinCli } = await import('@claude-code-best/weixin'); + const { enableConfigs } = await import('../utils/config.js'); + const { initializeAnalyticsSink } = await import('../services/analytics/sink.js'); + const { shutdownDatadog } = await import('../services/analytics/datadog.js'); + const { shutdown1PEventLogging } = await import('../services/analytics/firstPartyEventLogger.js'); + const { logForDebugging } = await import('../utils/debug.js'); + const { ChannelPermissionRequestNotificationSchema } = await import('../services/mcp/channelNotification.js'); + await handleWeixinCli( + args.slice(1), + { + enableConfigs, + initializeAnalyticsSink, + shutdownDatadog, + shutdown1PEventLogging, + logForDebugging, + registerPermissionHandler(server, handler) { + server.setNotificationHandler(ChannelPermissionRequestNotificationSchema() as any, async notification => + handler(notification.params), + ); + }, + }, + MACRO.VERSION, + ); + return; } // Fast-path for `--daemon-worker=` (internal — supervisor spawns this). @@ -121,10 +161,18 @@ async function main(): Promise { // perf-sensitive. No enableConfigs(), no analytics sinks at this layer — // workers are lean. If a worker kind needs configs/auth (assistant will), // it calls them inside its run() fn. - if (feature('DAEMON') && args[0] === '--daemon-worker') { - const { runDaemonWorker } = await import('../daemon/workerRegistry.js') - await runDaemonWorker(args[1]) - return + if (args[0] === '--daemon-worker' || args[0]?.startsWith('--daemon-worker=')) { + if (!feature('DAEMON')) { + console.error( + 'Error: --daemon-worker requires DAEMON feature to be enabled. Set FEATURE_DAEMON=1 or add DAEMON to DEFAULT_BUILD_FEATURES.', + ); + process.exitCode = 1; + return; + } + const kind = args[0] === '--daemon-worker' ? args[1] : args[0].split('=')[1]; + const { runDaemonWorker } = await import('../daemon/workerRegistry.js'); + await runDaemonWorker(kind); + return; } // Fast-path for `claude remote-control` (also accepts legacy `claude remote` / `claude sync` / `claude bridge`): @@ -139,185 +187,196 @@ async function main(): Promise { args[0] === 'sync' || args[0] === 'bridge') ) { - profileCheckpoint('cli_bridge_path') - const { enableConfigs } = await import('../utils/config.js') - enableConfigs() + profileCheckpoint('cli_bridge_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); - const { getBridgeDisabledReason, checkBridgeMinVersion } = await import( - '../bridge/bridgeEnabled.js' - ) - const { BRIDGE_LOGIN_ERROR } = await import('../bridge/types.js') - const { bridgeMain } = await import('../bridge/bridgeMain.js') - const { exitWithError } = await import('../utils/process.js') + const { getBridgeDisabledReason, checkBridgeMinVersion } = await import('../bridge/bridgeEnabled.js'); + const { BRIDGE_LOGIN_ERROR } = await import('../bridge/types.js'); + const { bridgeMain } = await import('../bridge/bridgeMain.js'); + const { exitWithError } = await import('../utils/process.js'); // Auth check must come before the GrowthBook gate check — without auth, // GrowthBook has no user context and would return a stale/default false. // getBridgeDisabledReason awaits GB init, so the returned value is fresh // (not the stale disk cache), but init still needs auth headers to work. - const { getClaudeAIOAuthTokens } = await import('../utils/auth.js') - const { getBridgeAccessToken } = await import('../bridge/bridgeConfig.js') + const { getClaudeAIOAuthTokens } = await import('../utils/auth.js'); + const { getBridgeAccessToken } = await import('../bridge/bridgeConfig.js'); if (!getClaudeAIOAuthTokens()?.accessToken && !getBridgeAccessToken()) { - exitWithError(BRIDGE_LOGIN_ERROR) + exitWithError(BRIDGE_LOGIN_ERROR); } - const disabledReason = await getBridgeDisabledReason() + const disabledReason = await getBridgeDisabledReason(); if (disabledReason) { - exitWithError(`Error: ${disabledReason}`) + exitWithError(`Error: ${disabledReason}`); } - const versionError = checkBridgeMinVersion() + const versionError = checkBridgeMinVersion(); if (versionError) { - exitWithError(versionError) + exitWithError(versionError); } // Bridge is a remote control feature - check policy limits - const { waitForPolicyLimitsToLoad, isPolicyAllowed } = await import( - '../services/policyLimits/index.js' - ) - await waitForPolicyLimitsToLoad() + const { waitForPolicyLimitsToLoad, isPolicyAllowed } = await import('../services/policyLimits/index.js'); + await waitForPolicyLimitsToLoad(); if (!isPolicyAllowed('allow_remote_control')) { - exitWithError( - "Error: Remote Control is disabled by your organization's policy.", - ) + exitWithError("Error: Remote Control is disabled by your organization's policy."); } - await bridgeMain(args.slice(1)) - return + await bridgeMain(args.slice(1)); + return; } - // Fast-path for `claude daemon [subcommand]`: long-running supervisor. - if (feature('DAEMON') && args[0] === 'daemon') { - profileCheckpoint('cli_daemon_path') - const { enableConfigs } = await import('../utils/config.js') - enableConfigs() - const { initSinks } = await import('../utils/sinks.js') - initSinks() - const { daemonMain } = await import('../daemon/main.js') - await daemonMain(args.slice(1)) - return + // Fast-path for `claude daemon [subcommand]`: unified daemon + session management. + // Handles both supervisor (start/stop) and background session (bg/attach/logs/kill) + // subcommands under one namespace. + if ((feature('DAEMON') || feature('BG_SESSIONS')) && args[0] === 'daemon') { + profileCheckpoint('cli_daemon_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const { setShellIfWindows } = await import('../utils/windowsPaths.js'); + setShellIfWindows(); + const { initSinks } = await import('../utils/sinks.js'); + initSinks(); + const { daemonMain } = await import('../daemon/main.js'); + await daemonMain(args.slice(1)); + return; } - // Fast-path for `claude ps|logs|attach|kill` and `--bg`/`--background`. - // Session management against the ~/.claude/sessions/ registry. Flag - // literals are inlined so bg.js only loads when actually dispatching. + // Fast-path for `claude autonomy ...`: state inspection/management commands + // do not need the full interactive CLI bootstrap. The full Commander path + // imports main.tsx and runs root preAction initialization before the autonomy + // action; under coverage/CI that leaves unrelated handles around simple + // state-only subprocess calls. + if (args[0] === 'autonomy') { + profileCheckpoint('cli_autonomy_path'); + const { getAutonomyCommandText } = await import('../cli/handlers/autonomy.js'); + const text = await getAutonomyCommandText(args.slice(1).join(' ')); + await new Promise((resolve, reject) => { + process.stdout.write(`${text}\n`, error => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + process.exit(0); + } + + // Fast-path for `--bg`/`--background` shortcut → daemon bg. + if (feature('BG_SESSIONS') && (args.includes('--bg') || args.includes('--background'))) { + profileCheckpoint('cli_daemon_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const { setShellIfWindows } = await import('../utils/windowsPaths.js'); + setShellIfWindows(); + const bg = await import('../cli/bg.js'); + await bg.handleBgStart(args.filter(a => a !== '--bg' && a !== '--background')); + return; + } + + // Backward-compat: ps/logs/attach/kill → daemon (deprecated) if ( feature('BG_SESSIONS') && - (args[0] === 'ps' || - args[0] === 'logs' || - args[0] === 'attach' || - args[0] === 'kill' || - args.includes('--bg') || - args.includes('--background')) + (args[0] === 'ps' || args[0] === 'logs' || args[0] === 'attach' || args[0] === 'kill') ) { - profileCheckpoint('cli_bg_path') - const { enableConfigs } = await import('../utils/config.js') - enableConfigs() - const bg = await import('../cli/bg.js') - switch (args[0]) { - case 'ps': - await bg.psHandler(args.slice(1)) - break - case 'logs': - await bg.logsHandler(args[1]) - break - case 'attach': - await bg.attachHandler(args[1]) - break - case 'kill': - await bg.killHandler(args[1]) - break - default: - await bg.handleBgFlag(args) - } - return + const mapped = args[0] === 'ps' ? 'status' : args[0]; + console.error(`[deprecated] Use: claude daemon ${mapped}${args[1] ? ' ' + args[1] : ''}`); + profileCheckpoint('cli_daemon_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const { setShellIfWindows } = await import('../utils/windowsPaths.js'); + setShellIfWindows(); + const { initSinks } = await import('../utils/sinks.js'); + initSinks(); + const { daemonMain } = await import('../daemon/main.js'); + await daemonMain([args[0] === 'ps' ? 'status' : args[0]!, ...args.slice(1)]); + return; } - // Fast-path for template job commands. - if ( - feature('TEMPLATES') && - (args[0] === 'new' || args[0] === 'list' || args[0] === 'reply') - ) { - profileCheckpoint('cli_templates_path') - const { templatesMain } = await import('../cli/handlers/templateJobs.js') - await templatesMain(args) + // Fast-path for `claude job `: template jobs. + if (feature('TEMPLATES') && args[0] === 'job') { + profileCheckpoint('cli_templates_path'); + const { templatesMain } = await import('../cli/handlers/templateJobs.js'); + await templatesMain(args.slice(1)); // process.exit (not return) — mountFleetView's Ink TUI can leave event // loop handles that prevent natural exit. // eslint-disable-next-line custom-rules/no-process-exit - process.exit(0) + process.exit(0); + } + + // Backward-compat: new/list/reply → job (deprecated) + if (feature('TEMPLATES') && (args[0] === 'new' || args[0] === 'list' || args[0] === 'reply')) { + console.error(`[deprecated] Use: claude job ${args[0]} ${args.slice(1).join(' ')}`.trim()); + profileCheckpoint('cli_templates_path'); + const { templatesMain } = await import('../cli/handlers/templateJobs.js'); + await templatesMain(args); + // eslint-disable-next-line custom-rules/no-process-exit + process.exit(0); } // Fast-path for `claude environment-runner`: headless BYOC runner. // feature() must stay inline for build-time dead code elimination. if (feature('BYOC_ENVIRONMENT_RUNNER') && args[0] === 'environment-runner') { - profileCheckpoint('cli_environment_runner_path') - const { environmentRunnerMain } = await import( - '../environment-runner/main.js' - ) - await environmentRunnerMain(args.slice(1)) - return + profileCheckpoint('cli_environment_runner_path'); + const { environmentRunnerMain } = await import('../environment-runner/main.js'); + await environmentRunnerMain(args.slice(1)); + return; } // Fast-path for `claude self-hosted-runner`: headless self-hosted-runner // targeting the SelfHostedRunnerWorkerService API (register + poll; poll IS // heartbeat). feature() must stay inline for build-time dead code elimination. if (feature('SELF_HOSTED_RUNNER') && args[0] === 'self-hosted-runner') { - profileCheckpoint('cli_self_hosted_runner_path') - const { selfHostedRunnerMain } = await import( - '../self-hosted-runner/main.js' - ) - await selfHostedRunnerMain(args.slice(1)) - return + profileCheckpoint('cli_self_hosted_runner_path'); + const { selfHostedRunnerMain } = await import('../self-hosted-runner/main.js'); + await selfHostedRunnerMain(args.slice(1)); + return; } // Fast-path for --worktree --tmux: exec into tmux before loading full CLI - const hasTmuxFlag = args.includes('--tmux') || args.includes('--tmux=classic') + const hasTmuxFlag = args.includes('--tmux') || args.includes('--tmux=classic'); if ( hasTmuxFlag && - (args.includes('-w') || - args.includes('--worktree') || - args.some(a => a.startsWith('--worktree='))) + (args.includes('-w') || args.includes('--worktree') || args.some(a => a.startsWith('--worktree='))) ) { - profileCheckpoint('cli_tmux_worktree_fast_path') - const { enableConfigs } = await import('../utils/config.js') - enableConfigs() - const { isWorktreeModeEnabled } = await import( - '../utils/worktreeModeEnabled.js' - ) + profileCheckpoint('cli_tmux_worktree_fast_path'); + const { enableConfigs } = await import('../utils/config.js'); + enableConfigs(); + const { isWorktreeModeEnabled } = await import('../utils/worktreeModeEnabled.js'); if (isWorktreeModeEnabled()) { - const { execIntoTmuxWorktree } = await import('../utils/worktree.js') - const result = await execIntoTmuxWorktree(args) + const { execIntoTmuxWorktree } = await import('../utils/worktree.js'); + const result = await execIntoTmuxWorktree(args); if (result.handled) { - return + return; } // If not handled (e.g., error), fall through to normal CLI if (result.error) { - const { exitWithError } = await import('../utils/process.js') - exitWithError(result.error) + const { exitWithError } = await import('../utils/process.js'); + exitWithError(result.error); } } } // Redirect common update flag mistakes to the update subcommand - if ( - args.length === 1 && - (args[0] === '--update' || args[0] === '--upgrade') - ) { - process.argv = [process.argv[0]!, process.argv[1]!, 'update'] + if (args.length === 1 && (args[0] === '--update' || args[0] === '--upgrade')) { + process.argv = [process.argv[0]!, process.argv[1]!, 'update']; } // --bare: set SIMPLE early so gates fire during module eval / commander // option building (not just inside the action handler). if (args.includes('--bare')) { - process.env.CLAUDE_CODE_SIMPLE = '1' + process.env.CLAUDE_CODE_SIMPLE = '1'; } // No special flags detected, load and run the full CLI - const { startCapturingEarlyInput } = await import('../utils/earlyInput.js') - startCapturingEarlyInput() - profileCheckpoint('cli_before_main_import') - const { main: cliMain } = await import('../main.jsx') - profileCheckpoint('cli_after_main_import') - await cliMain() - profileCheckpoint('cli_after_main_complete') + const { startCapturingEarlyInput } = await import('../utils/earlyInput.js'); + startCapturingEarlyInput(); + profileCheckpoint('cli_before_main_import'); + const { main: cliMain } = await import('../main.jsx'); + profileCheckpoint('cli_after_main_import'); + await cliMain(); + profileCheckpoint('cli_after_main_complete'); } // eslint-disable-next-line custom-rules/no-top-level-side-effects -void main() +await main(); diff --git a/src/hooks/useIdeAtMentioned.ts b/src/hooks/useIdeAtMentioned.ts index eb5977f91..7d79c6e5e 100644 --- a/src/hooks/useIdeAtMentioned.ts +++ b/src/hooks/useIdeAtMentioned.ts @@ -47,7 +47,7 @@ export function useIdeAtMentioned( // If we found a connected IDE client, register our handler if (ideClient) { ideClient.client.setNotificationHandler( - AtMentionedSchema(), + AtMentionedSchema() as any, notification => { if (ideClientRef.current !== ideClient) { return diff --git a/src/hooks/useIdeLogging.ts b/src/hooks/useIdeLogging.ts index e73c2301f..1fde54e0a 100644 --- a/src/hooks/useIdeLogging.ts +++ b/src/hooks/useIdeLogging.ts @@ -27,7 +27,7 @@ export function useIdeLogging(mcpClients: MCPServerConnection[]): void { if (ideClient) { // Register the log event handler ideClient.client.setNotificationHandler( - LogEventSchema(), + LogEventSchema() as any, notification => { const { eventName, eventData } = notification.params logEvent( diff --git a/src/hooks/useIdeSelection.ts b/src/hooks/useIdeSelection.ts index 9fb2f464f..1c678518a 100644 --- a/src/hooks/useIdeSelection.ts +++ b/src/hooks/useIdeSelection.ts @@ -110,7 +110,7 @@ export function useIdeSelection( // Register notification handler for selection_changed events ideClient.client.setNotificationHandler( - SelectionChangedSchema(), + SelectionChangedSchema() as any, notification => { if (currentIDERef.current !== ideClient) { return diff --git a/src/hooks/usePromptsFromClaudeInChrome.tsx b/src/hooks/usePromptsFromClaudeInChrome.tsx index 6b9d34def..94eee798a 100644 --- a/src/hooks/usePromptsFromClaudeInChrome.tsx +++ b/src/hooks/usePromptsFromClaudeInChrome.tsx @@ -1,19 +1,13 @@ -import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs' -import { useEffect, useRef } from 'react' -import { logError } from 'src/utils/log.js' -import { z } from 'zod/v4' -import { callIdeRpc } from '../services/mcp/client.js' -import type { - ConnectedMCPServer, - MCPServerConnection, -} from '../services/mcp/types.js' -import type { PermissionMode } from '../types/permissions.js' -import { - CLAUDE_IN_CHROME_MCP_SERVER_NAME, - isTrackedClaudeInChromeTabId, -} from '../utils/claudeInChrome/common.js' -import { lazySchema } from '../utils/lazySchema.js' -import { enqueuePendingNotification } from '../utils/messageQueueManager.js' +import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'; +import { useEffect, useRef } from 'react'; +import { logError } from 'src/utils/log.js'; +import { z } from 'zod/v4'; +import { callIdeRpc } from '../services/mcp/client.js'; +import type { ConnectedMCPServer, MCPServerConnection } from '../services/mcp/types.js'; +import type { PermissionMode } from '../types/permissions.js'; +import { CLAUDE_IN_CHROME_MCP_SERVER_NAME, isTrackedClaudeInChromeTabId } from '../utils/claudeInChrome/common.js'; +import { lazySchema } from '../utils/lazySchema.js'; +import { enqueuePendingNotification } from '../utils/messageQueueManager.js'; // Schema for the prompt notification from Chrome extension (JSON-RPC 2.0 format) const ClaudeInChromePromptNotificationSchema = lazySchema(() => @@ -24,19 +18,14 @@ const ClaudeInChromePromptNotificationSchema = lazySchema(() => image: z .object({ type: z.literal('base64'), - media_type: z.enum([ - 'image/jpeg', - 'image/png', - 'image/gif', - 'image/webp', - ]), + media_type: z.enum(['image/jpeg', 'image/png', 'image/gif', 'image/webp']), data: z.string(), }) .optional(), tabId: z.number().optional(), }), }), -) +); /** * A hook that listens for prompt notifications from the Claude for Chrome extension, @@ -46,84 +35,72 @@ export function usePromptsFromClaudeInChrome( mcpClients: MCPServerConnection[], toolPermissionMode: PermissionMode, ): void { - const mcpClientRef = useRef(undefined) + const mcpClientRef = useRef(undefined); useEffect(() => { if (process.env.USER_TYPE !== 'ant') { - return + return; } - const mcpClient = findChromeClient(mcpClients) + const mcpClient = findChromeClient(mcpClients); if (mcpClientRef.current !== mcpClient) { - mcpClientRef.current = mcpClient + mcpClientRef.current = mcpClient; } if (mcpClient) { - mcpClient.client.setNotificationHandler( - ClaudeInChromePromptNotificationSchema(), - notification => { - if (mcpClientRef.current !== mcpClient) { - return - } - const { tabId, prompt, image } = notification.params + mcpClient.client.setNotificationHandler(ClaudeInChromePromptNotificationSchema() as any, notification => { + if (mcpClientRef.current !== mcpClient) { + return; + } + const { tabId, prompt, image } = notification.params; - // Process notifications from tabs we're tracking since notifications are broadcasted - if ( - typeof tabId !== 'number' || - !isTrackedClaudeInChromeTabId(tabId) - ) { - return - } + // Process notifications from tabs we're tracking since notifications are broadcasted + if (typeof tabId !== 'number' || !isTrackedClaudeInChromeTabId(tabId)) { + return; + } - try { - // Build content blocks if there's an image, otherwise just use the prompt string - if (image) { - const contentBlocks: ContentBlockParam[] = [ - { type: 'text', text: prompt }, - { - type: 'image', - source: { - type: image.type, - media_type: image.media_type, - data: image.data, - }, + try { + // Build content blocks if there's an image, otherwise just use the prompt string + if (image) { + const contentBlocks: ContentBlockParam[] = [ + { type: 'text', text: prompt }, + { + type: 'image', + source: { + type: image.type, + media_type: image.media_type, + data: image.data, }, - ] - enqueuePendingNotification({ - value: contentBlocks, - mode: 'prompt', - }) - } else { - enqueuePendingNotification({ value: prompt, mode: 'prompt' }) - } - } catch (error) { - logError(error as Error) + }, + ]; + enqueuePendingNotification({ + value: contentBlocks, + mode: 'prompt', + }); + } else { + enqueuePendingNotification({ value: prompt, mode: 'prompt' }); } - }, - ) + } catch (error) { + logError(error as Error); + } + }); } - }, [mcpClients]) + }, [mcpClients]); // Sync permission mode with Chrome extension whenever it changes useEffect(() => { - const chromeClient = findChromeClient(mcpClients) - if (!chromeClient) return + const chromeClient = findChromeClient(mcpClients); + if (!chromeClient) return; - const chromeMode = - toolPermissionMode === 'bypassPermissions' - ? 'skip_all_permission_checks' - : 'ask' + const chromeMode = toolPermissionMode === 'bypassPermissions' ? 'skip_all_permission_checks' : 'ask'; - void callIdeRpc('set_permission_mode', { mode: chromeMode }, chromeClient) - }, [mcpClients, toolPermissionMode]) + void callIdeRpc('set_permission_mode', { mode: chromeMode }, chromeClient); + }, [mcpClients, toolPermissionMode]); } -function findChromeClient( - clients: MCPServerConnection[], -): ConnectedMCPServer | undefined { +function findChromeClient(clients: MCPServerConnection[]): ConnectedMCPServer | undefined { return clients.find( (client): client is ConnectedMCPServer => - client.type === 'connected' && - client.name === CLAUDE_IN_CHROME_MCP_SERVER_NAME, - ) + client.type === 'connected' && client.name === CLAUDE_IN_CHROME_MCP_SERVER_NAME, + ); } diff --git a/src/services/mcp/useManageMCPConnections.ts b/src/services/mcp/useManageMCPConnections.ts index a51bd27db..729621457 100644 --- a/src/services/mcp/useManageMCPConnections.ts +++ b/src/services/mcp/useManageMCPConnections.ts @@ -3,7 +3,6 @@ import { basename } from 'path' import { useCallback, useEffect, useRef } from 'react' import { getSessionId } from '../../bootstrap/state.js' import type { Command } from '../../commands.js' -import { clearSkillIndexCache as clearSkillIndexCacheValue } from '../skillSearch/localSearch.js' import type { Tool } from '../../Tool.js' import { clearServerCache, @@ -26,7 +25,9 @@ const fetchMcpSkillsForClient = feature('MCP_SKILLS') ).fetchMcpSkillsForClient : null const clearSkillIndexCache = feature('EXPERIMENTAL_SKILL_SEARCH') - ? clearSkillIndexCacheValue + ? ( + require('../skillSearch/localSearch.js') as typeof import('../skillSearch/localSearch.js') + ).clearSkillIndexCache : null import { @@ -444,7 +445,7 @@ export function useManageMCPConnections( // Schedule next retry with exponential backoff const backoffMs = Math.min( - INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1), + INITIAL_BACKOFF_MS * 2 ** (attempt - 1), MAX_BACKOFF_MS, ) logMCPDebug( @@ -469,147 +470,141 @@ export function useManageMCPConnections( // Channel push: notifications/claude/channel → enqueue(). // Gate decides whether to register the handler; connection stays // up either way (allowedMcpServers controls that). - if (feature('KAIROS') || feature('KAIROS_CHANNELS')) { - const gate = gateChannelServer( - client.name, - client.capabilities, - client.config.pluginSource, - ) - const entry = findChannelEntry(client.name, getAllowedChannels()) - // Plugin identifier for telemetry — log name@marketplace for any - // plugin-kind entry (same tier as tengu_plugin_installed, which - // logs arbitrary plugin_id+marketplace_name ungated). server-kind - // names are MCP-server-name tier; those are opt-in-only elsewhere - // (see isAnalyticsToolDetailsLoggingEnabled in metadata.ts) and - // stay unlogged here. is_dev/entry_kind segment the rest. - const pluginId = - entry?.kind === 'plugin' - ? (`${entry.name}@${entry.marketplace}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS) - : undefined - // Skip capability-miss — every non-channel MCP server trips it. - if (gate.action === 'register' || gate.kind !== 'capability') { - logEvent('tengu_mcp_channel_gate', { - registered: gate.action === 'register', - skip_kind: - gate.action === 'skip' - ? (gate.kind as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS) - : undefined, - entry_kind: - entry?.kind as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - is_dev: entry?.dev ?? false, - plugin: pluginId, - }) - } - switch (gate.action) { - case 'register': - logMCPDebug(client.name, 'Channel notifications registered') + const gate = gateChannelServer( + client.name, + client.capabilities, + client.config.pluginSource, + ) + const entry = findChannelEntry(client.name, getAllowedChannels()) + // Plugin identifier for telemetry — log name@marketplace for any + // plugin-kind entry (same tier as tengu_plugin_installed, which + // logs arbitrary plugin_id+marketplace_name ungated). server-kind + // names are MCP-server-name tier; those are opt-in-only elsewhere + // (see isAnalyticsToolDetailsLoggingEnabled in metadata.ts) and + // stay unlogged here. is_dev/entry_kind segment the rest. + const pluginId = + entry?.kind === 'plugin' + ? (`${entry.name}@${entry.marketplace}` as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS) + : undefined + // Skip capability-miss — every non-channel MCP server trips it. + if (gate.action === 'register' || gate.kind !== 'capability') { + logEvent('tengu_mcp_channel_gate', { + registered: gate.action === 'register', + skip_kind: + gate.action === 'skip' + ? (gate.kind as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS) + : undefined, + entry_kind: + entry?.kind as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + is_dev: entry?.dev ?? false, + plugin: pluginId, + }) + } + switch (gate.action) { + case 'register': + logMCPDebug(client.name, 'Channel notifications registered') + client.client.setNotificationHandler( + ChannelMessageNotificationSchema() as any, + async notification => { + const { content, meta } = notification.params + logMCPDebug( + client.name, + `notifications/claude/channel: ${content.slice(0, 80)}`, + ) + logEvent('tengu_mcp_channel_message', { + content_length: content.length, + meta_key_count: Object.keys(meta ?? {}).length, + entry_kind: + entry?.kind as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, + is_dev: entry?.dev ?? false, + plugin: pluginId, + }) + enqueue({ + mode: 'prompt', + value: wrapChannelMessage(client.name, content, meta), + priority: 'next', + isMeta: true, + origin: { kind: 'channel', server: client.name } as any, + skipSlashCommands: true, + }) + }, + ) + // Permission-reply handler — separate event, separate + // capability. Only registers if the server declares + // claude/channel/permission (same opt-in check as the send + // path in interactiveHandler.ts). Server parses the user's + // reply and emits {request_id, behavior}; no regex on our + // side, text in the general channel can't accidentally match. + if ( + client.capabilities?.experimental?.['claude/channel/permission'] + ) { client.client.setNotificationHandler( - ChannelMessageNotificationSchema(), + ChannelPermissionNotificationSchema() as any, async notification => { - const { content, meta } = notification.params + const { request_id, behavior } = notification.params + const resolved = + channelPermCallbacksRef.current?.resolve( + request_id, + behavior, + client.name, + ) ?? false logMCPDebug( client.name, - `notifications/claude/channel: ${content.slice(0, 80)}`, + `notifications/claude/channel/permission: ${request_id} → ${behavior} (${resolved ? 'matched pending' : 'no pending entry — stale or unknown ID'})`, ) - logEvent('tengu_mcp_channel_message', { - content_length: content.length, - meta_key_count: Object.keys(meta ?? {}).length, - entry_kind: - entry?.kind as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, - is_dev: entry?.dev ?? false, - plugin: pluginId, - }) - enqueue({ - mode: 'prompt', - value: wrapChannelMessage(client.name, content, meta), - priority: 'next', - isMeta: true, - origin: { kind: 'channel', server: client.name } as any, - skipSlashCommands: true, - }) }, ) - // Permission-reply handler — separate event, separate - // capability. Only registers if the server declares - // claude/channel/permission (same opt-in check as the send - // path in interactiveHandler.ts). Server parses the user's - // reply and emits {request_id, behavior}; no regex on our - // side, text in the general channel can't accidentally match. - if ( - client.capabilities?.experimental?.[ - 'claude/channel/permission' - ] !== undefined - ) { - client.client.setNotificationHandler( - ChannelPermissionNotificationSchema(), - async notification => { - const { request_id, behavior } = notification.params - const resolved = - channelPermCallbacksRef.current?.resolve( - request_id, - behavior, - client.name, - ) ?? false - logMCPDebug( - client.name, - `notifications/claude/channel/permission: ${request_id} → ${behavior} (${resolved ? 'matched pending' : 'no pending entry — stale or unknown ID'})`, - ) - }, - ) - } - break - case 'skip': - // Idempotent teardown so a register→skip re-gate (e.g. - // effect re-runs after /logout) actually removes the live - // handler. Without this, mid-session demotion is one-way: - // the gate says skip but the earlier handler keeps enqueuing. - // Map.delete — safe when never registered. - client.client.removeNotificationHandler( - 'notifications/claude/channel', - ) - client.client.removeNotificationHandler( - CHANNEL_PERMISSION_METHOD, - ) - logMCPDebug( - client.name, - `Channel notifications skipped: ${gate.reason}`, - ) - // Surface a once-per-kind toast when a channel server is - // blocked. This is the only - // user-visible signal (logMCPDebug above requires --debug). - // Capability/session skips are expected noise and stay - // debug-only. marketplace/allowlist run after session — if - // we're here with those kinds, the user asked for it. - if ( - gate.kind !== 'capability' && - gate.kind !== 'session' && - !channelWarnedKindsRef.current.has(gate.kind) && - (gate.kind === 'marketplace' || - gate.kind === 'allowlist' || - entry !== undefined) - ) { - channelWarnedKindsRef.current.add(gate.kind) - // disabled/auth/policy get custom toast copy (shorter, actionable); - // marketplace/allowlist reuse the gate's reason verbatim - // since it already names the mismatch. - const text = - gate.kind === 'disabled' - ? 'Channels are not currently available' - : gate.kind === 'auth' - ? 'Channels require claude.ai authentication · run /login' - : gate.kind === 'policy' - ? 'Channels are not enabled for your org · have an administrator set channelsEnabled: true in managed settings' - : gate.reason - addNotification({ - key: `channels-blocked-${gate.kind}`, - priority: 'high', - text, - color: 'warning', - timeoutMs: 12000, - }) - } - break - } + } + break + case 'skip': + // Idempotent teardown so a register→skip re-gate (e.g. + // effect re-runs after /logout) actually removes the live + // handler. Without this, mid-session demotion is one-way: + // the gate says skip but the earlier handler keeps enqueuing. + // Map.delete — safe when never registered. + client.client.removeNotificationHandler( + 'notifications/claude/channel', + ) + client.client.removeNotificationHandler(CHANNEL_PERMISSION_METHOD) + logMCPDebug( + client.name, + `Channel notifications skipped: ${gate.reason}`, + ) + // Surface a once-per-kind toast when a channel server is + // blocked. This is the only + // user-visible signal (logMCPDebug above requires --debug). + // Capability/session skips are expected noise and stay + // debug-only. marketplace/allowlist run after session — if + // we're here with those kinds, the user asked for it. + if ( + gate.kind !== 'capability' && + gate.kind !== 'session' && + !channelWarnedKindsRef.current.has(gate.kind) && + (gate.kind === 'marketplace' || + gate.kind === 'allowlist' || + entry !== undefined) + ) { + channelWarnedKindsRef.current.add(gate.kind) + // disabled/auth/policy get custom toast copy (shorter, actionable); + // marketplace/allowlist reuse the gate's reason verbatim + // since it already names the mismatch. + const text = + gate.kind === 'disabled' + ? 'Channels are not currently available' + : gate.kind === 'auth' + ? 'Channels require claude.ai authentication · run /login' + : gate.kind === 'policy' + ? 'Channels are not enabled for your org · have an administrator set channelsEnabled: true in managed settings' + : gate.reason + addNotification({ + key: `channels-blocked-${gate.kind}`, + priority: 'high', + text, + color: 'warning', + timeoutMs: 12000, + }) + } + break } // Register notification handlers for list_changed notifications diff --git a/src/services/mcp/vscodeSdkMcp.ts b/src/services/mcp/vscodeSdkMcp.ts index a15329cfb..27ee1c446 100644 --- a/src/services/mcp/vscodeSdkMcp.ts +++ b/src/services/mcp/vscodeSdkMcp.ts @@ -69,7 +69,7 @@ export function setupVscodeSdkMcp(sdkClients: MCPServerConnection[]): void { vscodeMcpClient = client client.client.setNotificationHandler( - LogEventNotificationSchema(), + LogEventNotificationSchema() as any, async notification => { const { eventName, eventData } = notification.params logEvent( diff --git a/tsconfig.json b/tsconfig.json index 49eda2f31..bcc029849 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,36 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "jsx": "react-jsx", - "strict": false, - "skipLibCheck": true, - "noEmit": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "resolveJsonModule": true, - "types": ["bun"], - "paths": { - "src/*": ["./src/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules"] + "extends": "./tsconfig.base.json", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "types": ["bun"], + "paths": { + "src/*": ["./src/*"], + "@claude-code-best/builtin-tools/*": ["./packages/builtin-tools/src/*"], + "@claude-code-best/builtin-tools": [ + "./packages/builtin-tools/src/index.ts" + ], + "@claude-code-best/mcp-client/*": ["./packages/mcp-client/src/*"], + "@claude-code-best/mcp-client": ["./packages/mcp-client/src/index.ts"], + "@claude-code-best/agent-tools/*": ["./packages/agent-tools/src/*"], + "@claude-code-best/agent-tools": ["./packages/agent-tools/src/index.ts"], + "@claude-code-best/weixin/*": ["./packages/weixin/src/*"], + "@claude-code-best/weixin": ["./packages/weixin/src/index.ts"] + } + }, + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "packages/**/*.ts", + "packages/**/*.tsx" + ], + "exclude": ["node_modules", "packages/remote-control-server/web"] }