mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 22:35:51 +00:00
* 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>
210 lines
8.3 KiB
TypeScript
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])
|
|
}
|