mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-19 16:13:56 +00:00
Merge 3357df40e6 into 151d24fb26
This commit is contained in:
commit
4417e08d14
4 changed files with 319 additions and 26 deletions
|
|
@ -9,10 +9,18 @@ const CHARS_PER_TOKEN = 4
|
|||
const SYSTEM_BASE_TOKENS = 10400
|
||||
const TOOL_TOKENS_OVERHEAD = 400
|
||||
const SKILL_FRONTMATTER_TOKENS = 80
|
||||
const TOOLS_PER_MCP_SERVER = 5
|
||||
|
||||
export type ContextBudget = {
|
||||
systemBase: number
|
||||
mcpTools: { count: number; tokens: number }
|
||||
mcpTools: {
|
||||
count: number
|
||||
tokens: number
|
||||
declared: number
|
||||
used: number
|
||||
unused: string[]
|
||||
unusedTokens: number
|
||||
}
|
||||
skills: { count: number; tokens: number }
|
||||
memory: { count: number; tokens: number; files: Array<{ name: string; tokens: number }> }
|
||||
total: number
|
||||
|
|
@ -30,7 +38,15 @@ async function readConfigFile(path: string): Promise<Record<string, unknown> | n
|
|||
try { return JSON.parse(raw) } catch { return null }
|
||||
}
|
||||
|
||||
async function countMcpTools(projectPath?: string): Promise<number> {
|
||||
type McpCount = {
|
||||
toolCount: number
|
||||
declared: number
|
||||
used: number
|
||||
unused: string[]
|
||||
unusedTokens: number
|
||||
}
|
||||
|
||||
async function countMcpTools(projectPath?: string, calledServers?: Set<string>): Promise<McpCount> {
|
||||
const home = homedir()
|
||||
const configPaths = [
|
||||
join(home, '.claude', 'settings.json'),
|
||||
|
|
@ -42,21 +58,63 @@ async function countMcpTools(projectPath?: string): Promise<number> {
|
|||
configPaths.push(join(projectPath, '.claude', 'settings.local.json'))
|
||||
}
|
||||
|
||||
const servers = new Set<string>()
|
||||
let toolCount = 0
|
||||
const normalizedSeen = new Set<string>()
|
||||
const serverNames: string[] = []
|
||||
|
||||
const pushServers = (serversObj: Record<string, unknown>): void => {
|
||||
for (const name of Object.keys(serversObj)) {
|
||||
const normalized = name.replace(/:/g, '_')
|
||||
if (normalizedSeen.has(normalized)) continue
|
||||
normalizedSeen.add(normalized)
|
||||
serverNames.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of configPaths) {
|
||||
const config = await readConfigFile(p)
|
||||
if (!config) continue
|
||||
const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>
|
||||
for (const name of Object.keys(mcpServers)) {
|
||||
if (servers.has(name)) continue
|
||||
servers.add(name)
|
||||
toolCount += 5
|
||||
pushServers((config.mcpServers ?? {}) as Record<string, unknown>)
|
||||
}
|
||||
|
||||
// `claude mcp add` writes to ~/.claude.json (top-level for user-scope,
|
||||
// projects[cwd].mcpServers for project-local scope). This is the common config
|
||||
// path and was missed by settings.json-only discovery.
|
||||
const claudeJson = await readConfigFile(join(home, '.claude.json'))
|
||||
if (claudeJson) {
|
||||
pushServers((claudeJson.mcpServers ?? {}) as Record<string, unknown>)
|
||||
if (projectPath) {
|
||||
const projects = (claudeJson.projects ?? {}) as Record<string, { mcpServers?: Record<string, unknown> }>
|
||||
const projectEntry = projects[projectPath] ?? projects[projectPath.replace(/\\/g, '/')]
|
||||
if (projectEntry?.mcpServers) {
|
||||
pushServers(projectEntry.mcpServers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return toolCount
|
||||
const toolCount = normalizedSeen.size * TOOLS_PER_MCP_SERVER
|
||||
const declared = normalizedSeen.size
|
||||
|
||||
if (!calledServers) {
|
||||
return { toolCount, declared, used: 0, unused: [], unusedTokens: 0 }
|
||||
}
|
||||
if (calledServers.size === 0) {
|
||||
const unusedTokens = serverNames.length * TOOLS_PER_MCP_SERVER * TOOL_TOKENS_OVERHEAD
|
||||
return { toolCount, declared, used: 0, unused: serverNames, unusedTokens }
|
||||
}
|
||||
|
||||
let usedCount = 0
|
||||
const unused: string[] = []
|
||||
for (const name of serverNames) {
|
||||
const normalized = name.replace(/:/g, '_')
|
||||
if (calledServers.has(normalized)) {
|
||||
usedCount++
|
||||
} else {
|
||||
unused.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
const unusedTokens = unused.length * TOOLS_PER_MCP_SERVER * TOOL_TOKENS_OVERHEAD
|
||||
return { toolCount, declared, used: usedCount, unused, unusedTokens }
|
||||
}
|
||||
|
||||
async function countSkills(projectPath?: string): Promise<number> {
|
||||
|
|
@ -101,19 +159,30 @@ async function scanMemoryFiles(projectPath?: string): Promise<Array<{ name: stri
|
|||
return files
|
||||
}
|
||||
|
||||
export async function estimateContextBudget(projectPath?: string, modelContext = 1_000_000): Promise<ContextBudget> {
|
||||
const mcpToolCount = await countMcpTools(projectPath)
|
||||
export async function estimateContextBudget(
|
||||
projectPath?: string,
|
||||
modelContext = 1_000_000,
|
||||
calledServers?: Set<string>,
|
||||
): Promise<ContextBudget> {
|
||||
const mcpCount = await countMcpTools(projectPath, calledServers)
|
||||
const skillCount = await countSkills(projectPath)
|
||||
const memoryFiles = await scanMemoryFiles(projectPath)
|
||||
|
||||
const mcpTokens = mcpToolCount * TOOL_TOKENS_OVERHEAD
|
||||
const mcpTokens = mcpCount.toolCount * TOOL_TOKENS_OVERHEAD
|
||||
const skillTokens = skillCount * SKILL_FRONTMATTER_TOKENS
|
||||
const memoryTokens = memoryFiles.reduce((s, f) => s + f.tokens, 0)
|
||||
const total = SYSTEM_BASE_TOKENS + mcpTokens + skillTokens + memoryTokens
|
||||
|
||||
return {
|
||||
systemBase: SYSTEM_BASE_TOKENS,
|
||||
mcpTools: { count: mcpToolCount, tokens: mcpTokens },
|
||||
mcpTools: {
|
||||
count: mcpCount.toolCount,
|
||||
tokens: mcpTokens,
|
||||
declared: mcpCount.declared,
|
||||
used: mcpCount.used,
|
||||
unused: mcpCount.unused,
|
||||
unusedTokens: mcpCount.unusedTokens,
|
||||
},
|
||||
skills: { count: skillCount, tokens: skillTokens },
|
||||
memory: { count: memoryFiles.length, tokens: memoryTokens, files: memoryFiles },
|
||||
total,
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ function ProjectBreakdown({ projects, pw, bw, budgets }: { projects: ProjectSumm
|
|||
<Text color={GOLD}>{formatCost(project.totalCostUSD).padStart(8)}</Text>
|
||||
<Text color={GOLD}>{avgCost.padStart(PROJECT_COL_AVG)}</Text>
|
||||
<Text>{String(project.sessions.length).padStart(6)}</Text>
|
||||
{hasBudgets && <Text color="#7B9EF5">{(budget ? formatTokens(budget.total) : '-').padStart(10)}</Text>}
|
||||
{hasBudgets && <Text color={budget?.mcpTools.unused.length ? ORANGE : '#7B9EF5'}>{(budget ? formatTokens(budget.total) : '-').padStart(10)}</Text>}
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
|
|
@ -755,7 +755,13 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
|
|||
if (cancelled) return
|
||||
const cwd = await discoverProjectCwd(join(claudeDir, project.project))
|
||||
if (!cwd) continue
|
||||
budgets.set(project.project, await estimateContextBudget(cwd))
|
||||
const calledServers = new Set<string>()
|
||||
for (const session of project.sessions) {
|
||||
for (const server of Object.keys(session.mcpBreakdown)) {
|
||||
calledServers.add(server)
|
||||
}
|
||||
}
|
||||
budgets.set(project.project, await estimateContextBudget(cwd, 1_000_000, calledServers))
|
||||
}
|
||||
if (!cancelled) setProjectBudgets(budgets)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -397,19 +397,15 @@ export function loadMcpConfigs(projectCwds: Iterable<string>): Map<string, McpCo
|
|||
join(homedir(), '.claude', 'settings.json'),
|
||||
join(homedir(), '.claude', 'settings.local.json'),
|
||||
]
|
||||
const projectCwdList: string[] = []
|
||||
for (const cwd of projectCwds) {
|
||||
projectCwdList.push(cwd)
|
||||
configPaths.push(join(cwd, '.mcp.json'))
|
||||
configPaths.push(join(cwd, '.claude', 'settings.json'))
|
||||
configPaths.push(join(cwd, '.claude', 'settings.local.json'))
|
||||
}
|
||||
|
||||
for (const p of configPaths) {
|
||||
if (!existsSync(p)) continue
|
||||
const config = readJsonFile(p)
|
||||
if (!config) continue
|
||||
let mtime = 0
|
||||
try { mtime = statSync(p).mtimeMs } catch {}
|
||||
const serversObj = (config.mcpServers ?? {}) as Record<string, unknown>
|
||||
const pushServers = (serversObj: Record<string, unknown>, mtime: number): void => {
|
||||
for (const name of Object.keys(serversObj)) {
|
||||
const normalized = name.replace(/:/g, '_')
|
||||
const existing = servers.get(normalized)
|
||||
|
|
@ -418,6 +414,36 @@ export function loadMcpConfigs(projectCwds: Iterable<string>): Map<string, McpCo
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const p of configPaths) {
|
||||
if (!existsSync(p)) continue
|
||||
const config = readJsonFile(p)
|
||||
if (!config) continue
|
||||
let mtime = 0
|
||||
try { mtime = statSync(p).mtimeMs } catch {}
|
||||
pushServers((config.mcpServers ?? {}) as Record<string, unknown>, mtime)
|
||||
}
|
||||
|
||||
// `claude mcp add` writes to ~/.claude.json (top-level for user-scope,
|
||||
// projects[cwd].mcpServers for project-local scope). This is the common config
|
||||
// path and was missed by settings.json-only discovery.
|
||||
const claudeJsonPath = join(homedir(), '.claude.json')
|
||||
if (existsSync(claudeJsonPath)) {
|
||||
const config = readJsonFile(claudeJsonPath)
|
||||
if (config) {
|
||||
let mtime = 0
|
||||
try { mtime = statSync(claudeJsonPath).mtimeMs } catch {}
|
||||
pushServers((config.mcpServers ?? {}) as Record<string, unknown>, mtime)
|
||||
const projectsObj = (config.projects ?? {}) as Record<string, { mcpServers?: Record<string, unknown> }>
|
||||
for (const cwd of projectCwdList) {
|
||||
const entry = projectsObj[cwd] ?? projectsObj[cwd.replace(/\\/g, '/')]
|
||||
if (entry?.mcpServers) {
|
||||
pushServers(entry.mcpServers, mtime)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return servers
|
||||
}
|
||||
|
||||
|
|
@ -901,12 +927,12 @@ export function detectUnusedMcp(
|
|||
if (unused.length === 0) return null
|
||||
|
||||
const totalSessions = projects.reduce((s, p) => s + p.sessions.length, 0)
|
||||
const schemaTokensPerSession = unused.length * TOOLS_PER_MCP_SERVER * TOKENS_PER_MCP_TOOL
|
||||
const tokensSaved = schemaTokensPerSession * Math.max(totalSessions, 1)
|
||||
const perSessionTokens = unused.length * TOOLS_PER_MCP_SERVER * TOKENS_PER_MCP_TOOL
|
||||
const tokensSaved = perSessionTokens * Math.max(totalSessions, 1)
|
||||
|
||||
return {
|
||||
title: `${unused.length} MCP server${unused.length > 1 ? 's' : ''} configured but never used`,
|
||||
explanation: `Never called in this period: ${unused.join(', ')}. Each server loads ~${TOOLS_PER_MCP_SERVER * TOKENS_PER_MCP_TOOL} tokens of tool schema into every session.`,
|
||||
explanation: `Never called in this period: ${unused.join(', ')}. Estimated overhead: ~${formatTokens(perSessionTokens)} tokens/session (${formatTokens(tokensSaved)} tokens total across ${totalSessions} session${totalSessions !== 1 ? 's' : ''}).`,
|
||||
impact: unused.length >= UNUSED_MCP_HIGH_THRESHOLD ? 'high' : 'medium',
|
||||
tokensSaved,
|
||||
fix: {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,10 @@ function writeFile(path: string, content: string): void {
|
|||
writeFileSync(path, content)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
rmSync(join(FAKE_HOME_FOR_MOCK, '.claude.json'), { force: true })
|
||||
})
|
||||
|
||||
function touchOld(path: string, daysAgo: number): void {
|
||||
const past = new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000)
|
||||
utimesSync(path, past, past)
|
||||
|
|
@ -170,6 +174,58 @@ describe('loadMcpConfigs', () => {
|
|||
expect(() => loadMcpConfigs([projectDir])).not.toThrow()
|
||||
expect(loadMcpConfigs([projectDir]).size).toBe(0)
|
||||
})
|
||||
|
||||
describe('~/.claude.json support (claude mcp add flow)', () => {
|
||||
const claudeJsonPath = join(FAKE_HOME_FOR_MOCK, '.claude.json')
|
||||
|
||||
beforeEach(() => {
|
||||
rmSync(claudeJsonPath, { force: true })
|
||||
})
|
||||
|
||||
it('reads user-scope mcpServers from ~/.claude.json top-level', () => {
|
||||
writeFile(claudeJsonPath, JSON.stringify({
|
||||
mcpServers: { docker: { command: 'x' }, vault: { command: 'y' } },
|
||||
}))
|
||||
const servers = loadMcpConfigs([])
|
||||
expect(servers.has('docker')).toBe(true)
|
||||
expect(servers.has('vault')).toBe(true)
|
||||
})
|
||||
|
||||
it('reads project-scope mcpServers from ~/.claude.json projects map', () => {
|
||||
const projectDir = join(makeFixtureRoot(), 'proj')
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
writeFile(claudeJsonPath, JSON.stringify({
|
||||
projects: { [projectDir]: { mcpServers: { local: { command: 'x' } } } },
|
||||
}))
|
||||
const servers = loadMcpConfigs([projectDir])
|
||||
expect(servers.has('local')).toBe(true)
|
||||
})
|
||||
|
||||
it('matches projects map key with forward slashes on Windows paths', () => {
|
||||
const projectDir = join(makeFixtureRoot(), 'proj')
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
writeFile(claudeJsonPath, JSON.stringify({
|
||||
projects: { [projectDir.replace(/\\/g, '/')]: { mcpServers: { win: { command: 'x' } } } },
|
||||
}))
|
||||
const servers = loadMcpConfigs([projectDir])
|
||||
expect(servers.has('win')).toBe(true)
|
||||
})
|
||||
|
||||
it('merges ~/.claude.json user-scope with project .mcp.json', () => {
|
||||
const projectDir = join(makeFixtureRoot(), 'proj')
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
writeFile(join(projectDir, '.mcp.json'), JSON.stringify({
|
||||
mcpServers: { proj_only: { command: 'x' } },
|
||||
}))
|
||||
writeFile(claudeJsonPath, JSON.stringify({
|
||||
mcpServers: { user_wide: { command: 'y' } },
|
||||
}))
|
||||
const servers = loadMcpConfigs([projectDir])
|
||||
expect(servers.has('proj_only')).toBe(true)
|
||||
expect(servers.has('user_wide')).toBe(true)
|
||||
expect(servers.size).toBe(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('detectUnusedMcp', () => {
|
||||
|
|
@ -210,6 +266,19 @@ describe('detectUnusedMcp', () => {
|
|||
]
|
||||
expect(detectUnusedMcp(calls, [], new Set([projectDir]))).toBeNull()
|
||||
})
|
||||
|
||||
it('explanation mentions tokens/session', () => {
|
||||
const root = makeFixtureRoot()
|
||||
const projectDir = join(root, 'myapp')
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
writeFile(join(projectDir, '.mcp.json'), JSON.stringify({
|
||||
mcpServers: { ghost: { command: 'x' } },
|
||||
}))
|
||||
touchOld(join(projectDir, '.mcp.json'), 30)
|
||||
const finding = detectUnusedMcp([], [], new Set([projectDir]))
|
||||
expect(finding).not.toBeNull()
|
||||
expect(finding!.explanation).toMatch(/tokens\/session/)
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -435,3 +504,126 @@ describe('discoverProjectCwd', () => {
|
|||
expect(await discoverProjectCwd(root)).toBe('/Users/test/project')
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// estimateContextBudget with calledServers
|
||||
// ============================================================================
|
||||
|
||||
describe('estimateContextBudget with calledServers', () => {
|
||||
it('reports unused servers when calledServers provided', async () => {
|
||||
const root = makeFixtureRoot()
|
||||
writeFile(join(root, '.mcp.json'), JSON.stringify({
|
||||
mcpServers: { used: { command: 'x' }, ghost: { command: 'y' } },
|
||||
}))
|
||||
const budget = await estimateContextBudget(root, 1_000_000, new Set(['used']))
|
||||
expect(budget.mcpTools.declared).toBe(2)
|
||||
expect(budget.mcpTools.used).toBe(1)
|
||||
expect(budget.mcpTools.unused).toEqual(['ghost'])
|
||||
expect(budget.mcpTools.unusedTokens).toBe(1 * 5 * 400)
|
||||
})
|
||||
|
||||
it('reports zero unused when all called', async () => {
|
||||
const root = makeFixtureRoot()
|
||||
writeFile(join(root, '.mcp.json'), JSON.stringify({
|
||||
mcpServers: { a: { command: 'x' }, b: { command: 'y' } },
|
||||
}))
|
||||
const budget = await estimateContextBudget(root, 1_000_000, new Set(['a', 'b']))
|
||||
expect(budget.mcpTools.unused).toEqual([])
|
||||
expect(budget.mcpTools.unusedTokens).toBe(0)
|
||||
})
|
||||
|
||||
it('treats calledServers=undefined as no usage data (backward compat)', async () => {
|
||||
const root = makeFixtureRoot()
|
||||
writeFile(join(root, '.mcp.json'), JSON.stringify({
|
||||
mcpServers: { x: { command: 'x' } },
|
||||
}))
|
||||
const budget = await estimateContextBudget(root)
|
||||
expect(budget.mcpTools.declared).toBe(1)
|
||||
expect(budget.mcpTools.used).toBe(0)
|
||||
expect(budget.mcpTools.unused).toEqual([])
|
||||
expect(budget.mcpTools.count).toBe(5)
|
||||
expect(budget.mcpTools.tokens).toBe(2000)
|
||||
})
|
||||
|
||||
it('reports all servers unused when calledServers is empty set', async () => {
|
||||
const root = makeFixtureRoot()
|
||||
writeFile(join(root, '.mcp.json'), JSON.stringify({
|
||||
mcpServers: { alpha: { command: 'a' }, beta: { command: 'b' } },
|
||||
}))
|
||||
const budget = await estimateContextBudget(root, 1_000_000, new Set())
|
||||
expect(budget.mcpTools.declared).toBe(2)
|
||||
expect(budget.mcpTools.used).toBe(0)
|
||||
expect(budget.mcpTools.unused).toEqual(['alpha', 'beta'])
|
||||
expect(budget.mcpTools.unusedTokens).toBe(2 * 5 * 400)
|
||||
})
|
||||
|
||||
it('normalizes plugin:foo:bar names before comparison', async () => {
|
||||
const root = makeFixtureRoot()
|
||||
writeFile(join(root, '.mcp.json'), JSON.stringify({
|
||||
mcpServers: { 'plugin:context7:context7': { command: 'ctx' } },
|
||||
}))
|
||||
const budget = await estimateContextBudget(root, 1_000_000, new Set(['plugin_context7_context7']))
|
||||
expect(budget.mcpTools.used).toBe(1)
|
||||
expect(budget.mcpTools.unused).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('estimateContextBudget reading ~/.claude.json (claude mcp add flow)', () => {
|
||||
const claudeJsonPath = join(FAKE_HOME_FOR_MOCK, '.claude.json')
|
||||
|
||||
beforeEach(() => {
|
||||
rmSync(claudeJsonPath, { force: true })
|
||||
})
|
||||
|
||||
it('reads user-scope mcpServers from ~/.claude.json top-level', async () => {
|
||||
writeFile(claudeJsonPath, JSON.stringify({
|
||||
mcpServers: { docker: { command: 'x' }, vault: { command: 'y' } },
|
||||
}))
|
||||
const budget = await estimateContextBudget(undefined, 1_000_000, new Set(['docker']))
|
||||
expect(budget.mcpTools.declared).toBe(2)
|
||||
expect(budget.mcpTools.used).toBe(1)
|
||||
expect(budget.mcpTools.unused).toEqual(['vault'])
|
||||
})
|
||||
|
||||
it('reads project-scope mcpServers from ~/.claude.json projects map', async () => {
|
||||
const projectDir = join(makeFixtureRoot(), 'proj')
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
writeFile(claudeJsonPath, JSON.stringify({
|
||||
mcpServers: {},
|
||||
projects: { [projectDir]: { mcpServers: { local: { command: 'x' } } } },
|
||||
}))
|
||||
const budget = await estimateContextBudget(projectDir, 1_000_000, new Set(['other']))
|
||||
expect(budget.mcpTools.declared).toBe(1)
|
||||
expect(budget.mcpTools.unused).toEqual(['local'])
|
||||
})
|
||||
|
||||
it('matches projects map key with forward slashes on Windows paths', async () => {
|
||||
const projectDir = join(makeFixtureRoot(), 'proj')
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
writeFile(claudeJsonPath, JSON.stringify({
|
||||
projects: { [projectDir.replace(/\\/g, '/')]: { mcpServers: { win: { command: 'x' } } } },
|
||||
}))
|
||||
const budget = await estimateContextBudget(projectDir, 1_000_000, new Set(['other']))
|
||||
expect(budget.mcpTools.declared).toBe(1)
|
||||
})
|
||||
|
||||
it('merges ~/.claude.json top-level with .mcp.json at project root', async () => {
|
||||
const projectDir = join(makeFixtureRoot(), 'proj')
|
||||
mkdirSync(projectDir, { recursive: true })
|
||||
writeFile(join(projectDir, '.mcp.json'), JSON.stringify({
|
||||
mcpServers: { proj_only: { command: 'x' } },
|
||||
}))
|
||||
writeFile(claudeJsonPath, JSON.stringify({
|
||||
mcpServers: { user_wide: { command: 'y' } },
|
||||
}))
|
||||
const budget = await estimateContextBudget(projectDir, 1_000_000, new Set(['proj_only']))
|
||||
expect(budget.mcpTools.declared).toBe(2)
|
||||
expect(budget.mcpTools.used).toBe(1)
|
||||
expect(budget.mcpTools.unused).toEqual(['user_wide'])
|
||||
})
|
||||
|
||||
it('does nothing when ~/.claude.json is missing', async () => {
|
||||
const budget = await estimateContextBudget(undefined, 1_000_000, new Set())
|
||||
expect(budget.mcpTools.declared).toBe(0)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue