Files
claude-code/src/components/permissions/hooks.ts
claude-code-best 2fb1c9dcd8 feat: 工具层及 mcp 大重构 (#252)
* feat: 第一版大重构

* fix: 修复类型问题

* chore: 更新版本到 1.3.2

* Add brave as alternative WebSearchTool

* fix: 修正顺序

* fix: 修复对穷鬼模式的 auto dream 和 session memory 越过

* feat: 穷鬼模式去除 session-summary

* feat: 创建 builtin-tools 包,搬运所有工具实现

将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/,
内部导入路径已更新为 src/ alias 模式。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/

- src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/
- 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock

- tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射
- 新增 packages/builtin-tools/src 至 include

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀

所有包名及 import 路径统一添加 @claude-code-best/ 前缀:
- builtin-tools → @claude-code-best/builtin-tools
- mcp-client → @claude-code-best/mcp-client
- agent-tools → @claude-code-best/agent-tools

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复 node 环境没有 bun 的问题

---------

Co-authored-by: Eric-Guo <eric.guocz@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 09:52:05 +08:00

210 lines
8.3 KiB
TypeScript

import { feature } from 'bun:bundle'
import { useEffect, useRef } from 'react'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js'
import { BashTool } from '@claude-code-best/builtin-tools/tools/BashTool/BashTool.js'
import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js'
import type {
PermissionDecisionReason,
PermissionResult,
} from 'src/utils/permissions/PermissionResult.js'
import {
extractRules,
hasRules,
} from 'src/utils/permissions/PermissionUpdate.js'
import { permissionRuleValueToString } from 'src/utils/permissions/permissionRuleParser.js'
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'
import type { ToolUseConfirm } from '../../components/permissions/PermissionRequest.js'
import { useSetAppState } from '../../state/AppState.js'
import { env } from '../../utils/env.js'
import { jsonStringify } from '../../utils/slowOperations.js'
import { type CompletionType, logUnaryEvent } from '../../utils/unaryLogging.js'
export type UnaryEvent = {
completion_type: CompletionType
language_name: string | Promise<string>
}
function permissionResultToLog(permissionResult: PermissionResult): string {
switch (permissionResult.behavior) {
case 'allow':
return 'allow'
case 'ask': {
const rules = extractRules(permissionResult.suggestions)
const suggestions =
rules.length > 0
? rules.map(r => permissionRuleValueToString(r)).join(', ')
: 'none'
return `ask: ${permissionResult.message},
suggestions: ${suggestions}
reason: ${decisionReasonToString(permissionResult.decisionReason)}`
}
case 'deny':
return `deny: ${permissionResult.message},
reason: ${decisionReasonToString(permissionResult.decisionReason)}`
case 'passthrough': {
const rules = extractRules(permissionResult.suggestions)
const suggestions =
rules.length > 0
? rules.map(r => permissionRuleValueToString(r)).join(', ')
: 'none'
return `passthrough: ${permissionResult.message},
suggestions: ${suggestions}
reason: ${decisionReasonToString(permissionResult.decisionReason)}`
}
}
}
function decisionReasonToString(
decisionReason: PermissionDecisionReason | undefined,
): string {
if (!decisionReason) {
return 'No decision reason'
}
if (
(feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) &&
decisionReason.type === 'classifier'
) {
return `Classifier: ${decisionReason.classifier}, Reason: ${decisionReason.reason}`
}
switch (decisionReason.type) {
case 'rule':
return `Rule: ${permissionRuleValueToString(decisionReason.rule.ruleValue)}`
case 'mode':
return `Mode: ${decisionReason.mode}`
case 'subcommandResults':
return `Subcommand Results: ${Array.from(decisionReason.reasons.entries())
.map(([key, value]) => `${key}: ${permissionResultToLog(value)}`)
.join(', \n')}`
case 'permissionPromptTool':
return `Permission Tool: ${decisionReason.permissionPromptToolName}, Result: ${jsonStringify(decisionReason.toolResult)}`
case 'hook':
return `Hook: ${decisionReason.hookName}${decisionReason.reason ? `, Reason: ${decisionReason.reason}` : ''}`
case 'workingDir':
return `Working Directory: ${decisionReason.reason}`
case 'safetyCheck':
return `Safety check: ${decisionReason.reason}`
case 'other':
return `Other: ${decisionReason.reason}`
default:
return jsonStringify(decisionReason, null, 2)
}
}
/**
* Logs permission request events using analytics and unary logging.
* Handles both the analytics event and the unary event logging.
*/
export function usePermissionRequestLogging(
toolUseConfirm: ToolUseConfirm,
unaryEvent: UnaryEvent,
): void {
const setAppState = useSetAppState()
// Guard against effect re-firing if toolUseConfirm's object reference
// changes during a single dialog's lifetime (e.g., parent re-renders with a
// fresh object). Without this, the unconditional setAppState below can
// cascade into an infinite microtask loop — each re-fire does another
// setAppState spread + (ant builds) splitCommand → shell-quote regex,
// pegging CPU at 100% and leaking ~500MB/min in JSRopeString/RegExp allocs.
// The component is keyed by toolUseID, so this ref resets on remount —
// we only need to dedupe re-fires WITHIN one dialog instance.
const loggedToolUseID = useRef<string | null>(null)
useEffect(() => {
if (loggedToolUseID.current === toolUseConfirm.toolUseID) {
return
}
loggedToolUseID.current = toolUseConfirm.toolUseID
// Increment permission prompt count for attribution tracking
setAppState(prev => ({
...prev,
attribution: {
...prev.attribution,
permissionPromptCount: prev.attribution.permissionPromptCount + 1,
},
}))
// Log analytics event
logEvent('tengu_tool_use_show_permission_request', {
messageID: toolUseConfirm.assistantMessage.message
.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name),
isMcp: toolUseConfirm.tool.isMcp ?? false,
decisionReasonType: toolUseConfirm.permissionResult.decisionReason
?.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
sandboxEnabled: SandboxManager.isSandboxingEnabled(),
})
if (process.env.USER_TYPE === 'ant') {
const permissionResult = toolUseConfirm.permissionResult
if (
toolUseConfirm.tool.name === BashTool.name &&
permissionResult.behavior === 'ask' &&
!hasRules(permissionResult.suggestions)
) {
// Log if no rule suggestions ("always allow") are provided
logEvent('tengu_internal_tool_use_permission_request_no_always_allow', {
messageID: toolUseConfirm.assistantMessage.message
.id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name),
isMcp: toolUseConfirm.tool.isMcp ?? false,
decisionReasonType: (permissionResult.decisionReason?.type ??
'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
sandboxEnabled: SandboxManager.isSandboxingEnabled(),
// This DOES contain code/filepaths and should not be logged in the public build!
decisionReasonDetails: decisionReasonToString(
permissionResult.decisionReason,
) as never,
})
}
}
// [ANT-ONLY] Log bash tool calls, so we can categorize
// & burn down calls that should have been allowed
if (process.env.USER_TYPE === 'ant') {
const parsedInput = BashTool.inputSchema.safeParse(toolUseConfirm.input)
if (
toolUseConfirm.tool.name === BashTool.name &&
toolUseConfirm.permissionResult.behavior === 'ask' &&
parsedInput.success
) {
// Note: All metadata fields in this event contain code/filepaths
let split = [parsedInput.data.command]
try {
split = splitCommand_DEPRECATED(parsedInput.data.command)
} catch {
// Ignore parse errors here - just log the full command
}
logEvent('tengu_internal_bash_tool_use_permission_request', {
parts: jsonStringify(
split,
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
input: jsonStringify(
toolUseConfirm.input,
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
decisionReasonType: toolUseConfirm.permissionResult.decisionReason
?.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
decisionReason: decisionReasonToString(
toolUseConfirm.permissionResult.decisionReason,
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
}
}
void logUnaryEvent({
completion_type: unaryEvent.completion_type,
event: 'response',
metadata: {
language_name: unaryEvent.language_name,
message_id: toolUseConfirm.assistantMessage.message.id!,
platform: env.platform,
},
})
}, [toolUseConfirm, unaryEvent, setAppState])
}