Files
claude-code/src/utils/plugins/cacheUtils.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

197 lines
6.5 KiB
TypeScript

import { readdir, rm, stat, unlink, writeFile } from 'fs/promises'
import { join } from 'path'
import { clearCommandsCache } from '../../commands.js'
import { clearAllOutputStylesCache } from '../../constants/outputStyles.js'
import { clearAgentDefinitionsCache } from '@claude-code-best/builtin-tools/tools/AgentTool/loadAgentsDir.js'
import { clearPromptCache } from '@claude-code-best/builtin-tools/tools/SkillTool/prompt.js'
import { resetSentSkillNames } from '../attachments.js'
import { logForDebugging } from '../debug.js'
import { getErrnoCode } from '../errors.js'
import { logError } from '../log.js'
import { loadInstalledPluginsFromDisk } from './installedPluginsManager.js'
import { clearPluginAgentCache } from './loadPluginAgents.js'
import { clearPluginCommandCache } from './loadPluginCommands.js'
import {
clearPluginHookCache,
pruneRemovedPluginHooks,
} from './loadPluginHooks.js'
import { clearPluginOutputStyleCache } from './loadPluginOutputStyles.js'
import { clearPluginCache, getPluginCachePath } from './pluginLoader.js'
import { clearPluginOptionsCache } from './pluginOptionsStorage.js'
import { isPluginZipCacheEnabled } from './zipCache.js'
const ORPHANED_AT_FILENAME = '.orphaned_at'
const CLEANUP_AGE_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
export function clearAllPluginCaches(): void {
clearPluginCache()
clearPluginCommandCache()
clearPluginAgentCache()
clearPluginHookCache()
// Prune hooks from plugins no longer in the enabled set so uninstalled/
// disabled plugins stop firing immediately (gh-36995). Prune-only: hooks
// from newly-enabled plugins are NOT added here — they wait for
// /reload-plugins like commands/agents/MCP do. Fire-and-forget: old hooks
// stay valid until the prune completes (preserves gh-29767). No-op when
// STATE.registeredHooks is empty (test/preload.ts beforeEach clears it via
// resetStateForTests before reaching here).
pruneRemovedPluginHooks().catch(e => logError(e))
clearPluginOptionsCache()
clearPluginOutputStyleCache()
clearAllOutputStylesCache()
}
export function clearAllCaches(): void {
clearAllPluginCaches()
clearCommandsCache()
clearAgentDefinitionsCache()
clearPromptCache()
resetSentSkillNames()
}
/**
* Mark a plugin version as orphaned.
* Called when a plugin is uninstalled or updated to a new version.
*/
export async function markPluginVersionOrphaned(
versionPath: string,
): Promise<void> {
try {
await writeFile(getOrphanedAtPath(versionPath), `${Date.now()}`, 'utf-8')
} catch (error) {
logForDebugging(`Failed to write .orphaned_at: ${versionPath}: ${error}`)
}
}
/**
* Clean up orphaned plugin versions that have been orphaned for more than 7 days.
*
* Pass 1: Remove .orphaned_at from installed versions (clears stale markers)
* Pass 2: For each cached version not in installed_plugins.json:
* - If no .orphaned_at exists: create it (handles old CC versions, manual edits)
* - If .orphaned_at exists and > 7 days old: delete the version
*/
export async function cleanupOrphanedPluginVersionsInBackground(): Promise<void> {
// Zip cache mode stores plugins as .zip files, not directories. readSubdirs
// filters to directories only, so removeIfEmpty would see plugin dirs as empty
// and delete them (including the ZIPs). Skip cleanup entirely in zip mode.
if (isPluginZipCacheEnabled()) {
return
}
try {
const installedVersions = getInstalledVersionPaths()
if (!installedVersions) return
const cachePath = getPluginCachePath()
const now = Date.now()
// Pass 1: Remove .orphaned_at from installed versions
// This handles cases where a plugin was reinstalled after being orphaned
await Promise.all(
[...installedVersions].map(p => removeOrphanedAtMarker(p)),
)
// Pass 2: Process orphaned versions
for (const marketplace of await readSubdirs(cachePath)) {
const marketplacePath = join(cachePath, marketplace)
for (const plugin of await readSubdirs(marketplacePath)) {
const pluginPath = join(marketplacePath, plugin)
for (const version of await readSubdirs(pluginPath)) {
const versionPath = join(pluginPath, version)
if (installedVersions.has(versionPath)) continue
await processOrphanedPluginVersion(versionPath, now)
}
await removeIfEmpty(pluginPath)
}
await removeIfEmpty(marketplacePath)
}
} catch (error) {
logForDebugging(`Plugin cache cleanup failed: ${error}`)
}
}
function getOrphanedAtPath(versionPath: string): string {
return join(versionPath, ORPHANED_AT_FILENAME)
}
async function removeOrphanedAtMarker(versionPath: string): Promise<void> {
const orphanedAtPath = getOrphanedAtPath(versionPath)
try {
await unlink(orphanedAtPath)
} catch (error) {
const code = getErrnoCode(error)
if (code === 'ENOENT') return
logForDebugging(`Failed to remove .orphaned_at: ${versionPath}: ${error}`)
}
}
function getInstalledVersionPaths(): Set<string> | null {
try {
const paths = new Set<string>()
const diskData = loadInstalledPluginsFromDisk()
for (const installations of Object.values(diskData.plugins)) {
for (const entry of installations) {
paths.add(entry.installPath)
}
}
return paths
} catch (error) {
logForDebugging(`Failed to load installed plugins: ${error}`)
return null
}
}
async function processOrphanedPluginVersion(
versionPath: string,
now: number,
): Promise<void> {
const orphanedAtPath = getOrphanedAtPath(versionPath)
let orphanedAt: number
try {
orphanedAt = (await stat(orphanedAtPath)).mtimeMs
} catch (error) {
const code = getErrnoCode(error)
if (code === 'ENOENT') {
await markPluginVersionOrphaned(versionPath)
return
}
logForDebugging(`Failed to stat orphaned marker: ${versionPath}: ${error}`)
return
}
if (now - orphanedAt > CLEANUP_AGE_MS) {
try {
await rm(versionPath, { recursive: true, force: true })
} catch (error) {
logForDebugging(
`Failed to delete orphaned version: ${versionPath}: ${error}`,
)
}
}
}
async function removeIfEmpty(dirPath: string): Promise<void> {
if ((await readSubdirs(dirPath)).length === 0) {
try {
await rm(dirPath, { recursive: true, force: true })
} catch (error) {
logForDebugging(`Failed to remove empty dir: ${dirPath}: ${error}`)
}
}
}
async function readSubdirs(dirPath: string): Promise<string[]> {
try {
const entries = await readdir(dirPath, { withFileTypes: true })
return entries.filter(d => d.isDirectory()).map(d => d.name)
} catch {
return []
}
}