fix: 删除 edit tool 中的旧逻辑处理, 现在已经不需要这些处理了, 大模型够屌 (#1251)
* refactor: remove tab/quote normalization from FileEditTool * fix: resolve pre-existing typecheck errors (zod v4 compat + RCS web exclude)
This commit is contained in:
parent
e6a5e9acdd
commit
75c6a5e22e
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Suspense fallback={<DiffFrame placeholder />}>
|
||||
<DiffBody promise={dataPromise} file_path={props.file_path} />
|
||||
</Suspense>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DiffBody({
|
||||
promise,
|
||||
file_path,
|
||||
}: {
|
||||
promise: Promise<DiffData>
|
||||
file_path: string
|
||||
}): React.ReactNode {
|
||||
const { patch, firstLine, fileContent } = use(promise)
|
||||
const { columns } = useTerminalSize()
|
||||
function DiffBody({ promise, file_path }: { promise: Promise<DiffData>; file_path: string }): React.ReactNode {
|
||||
const { patch, firstLine, fileContent } = use(promise);
|
||||
const { columns } = useTerminalSize();
|
||||
return (
|
||||
<DiffFrame>
|
||||
<StructuredDiffList
|
||||
|
|
@ -68,57 +48,42 @@ function DiffBody({
|
|||
fileContent={fileContent}
|
||||
/>
|
||||
</DiffFrame>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function DiffFrame({
|
||||
children,
|
||||
placeholder,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
placeholder?: boolean
|
||||
}): React.ReactNode {
|
||||
function DiffFrame({ children, placeholder }: { children?: React.ReactNode; placeholder?: boolean }): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box
|
||||
borderColor="subtle"
|
||||
borderStyle="dashed"
|
||||
flexDirection="column"
|
||||
borderLeft={false}
|
||||
borderRight={false}
|
||||
>
|
||||
<Box borderColor="subtle" borderStyle="dashed" flexDirection="column" borderLeft={false} borderRight={false}>
|
||||
{placeholder ? <Text dimColor>…</Text> : children}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function loadDiffData(
|
||||
file_path: string,
|
||||
edits: FileEdit[],
|
||||
): Promise<DiffData> {
|
||||
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<DiffData> {
|
||||
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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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=<kind>` (internal — supervisor spawns this).
|
||||
|
|
@ -121,10 +161,18 @@ async function main(): Promise<void> {
|
|||
// 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<void> {
|
|||
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<void>((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 <sub> (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 <subcommand>`: 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 <sub> (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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<ConnectedMCPServer | undefined>(undefined)
|
||||
const mcpClientRef = useRef<ConnectedMCPServer | undefined>(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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user