forked from getagentseal/codeburn
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontext-budget.ts
More file actions
149 lines (128 loc) · 4.78 KB
/
Copy pathcontext-budget.ts
File metadata and controls
149 lines (128 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import { readdir } from 'fs/promises'
import { existsSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
import { readSessionFile } from './fs-utils.js'
const CHARS_PER_TOKEN = 4
const SYSTEM_BASE_TOKENS = 10400
const TOOL_TOKENS_OVERHEAD = 400
const SKILL_FRONTMATTER_TOKENS = 80
export type ContextBudget = {
systemBase: number
mcpTools: { count: number; tokens: number }
skills: { count: number; tokens: number }
memory: { count: number; tokens: number; files: Array<{ name: string; tokens: number }> }
total: number
modelContext: number
}
function estimateTokens(text: string): number {
return Math.ceil(text.length / CHARS_PER_TOKEN)
}
async function readConfigFile(path: string): Promise<Record<string, unknown> | null> {
if (!existsSync(path)) return null
const raw = await readSessionFile(path)
if (raw === null) return null
try { return JSON.parse(raw) } catch { return null }
}
async function countMcpTools(projectPath?: string): Promise<number> {
const home = homedir()
const configPaths = [
join(home, '.claude', 'settings.json'),
join(home, '.claude', 'settings.local.json'),
]
if (projectPath) {
configPaths.push(join(projectPath, '.mcp.json'))
configPaths.push(join(projectPath, '.claude', 'settings.json'))
configPaths.push(join(projectPath, '.claude', 'settings.local.json'))
}
const servers = new Set<string>()
let toolCount = 0
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
}
}
return toolCount
}
async function countSkills(projectPath?: string): Promise<number> {
const dirs = [join(homedir(), '.claude', 'skills')]
if (projectPath) dirs.push(join(projectPath, '.claude', 'skills'))
let count = 0
for (const dir of dirs) {
if (!existsSync(dir)) continue
try {
const entries = await readdir(dir)
for (const entry of entries) {
const skillFile = join(dir, entry, 'SKILL.md')
if (existsSync(skillFile)) count++
}
} catch { continue }
}
return count
}
async function scanMemoryFiles(projectPath?: string): Promise<Array<{ name: string; tokens: number }>> {
const home = homedir()
const files: Array<{ name: string; tokens: number }> = []
const paths: Array<{ path: string; name: string }> = [
{ path: join(home, '.claude', 'CLAUDE.md'), name: '~/.claude/CLAUDE.md' },
]
if (projectPath) {
paths.push({ path: join(projectPath, 'CLAUDE.md'), name: 'CLAUDE.md' })
paths.push({ path: join(projectPath, '.claude', 'CLAUDE.md'), name: '.claude/CLAUDE.md' })
paths.push({ path: join(projectPath, 'CLAUDE.local.md'), name: 'CLAUDE.local.md' })
}
for (const { path, name } of paths) {
if (!existsSync(path)) continue
const content = await readSessionFile(path)
if (content === null) continue
files.push({ name, tokens: estimateTokens(content) })
}
return files
}
export async function estimateContextBudget(projectPath?: string, modelContext = 1_000_000): Promise<ContextBudget> {
const mcpToolCount = await countMcpTools(projectPath)
const skillCount = await countSkills(projectPath)
const memoryFiles = await scanMemoryFiles(projectPath)
const mcpTokens = mcpToolCount * 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 },
skills: { count: skillCount, tokens: skillTokens },
memory: { count: memoryFiles.length, tokens: memoryTokens, files: memoryFiles },
total,
modelContext,
}
}
export async function estimateBudgetsByProject(projectPaths: Map<string, string>): Promise<Map<string, ContextBudget>> {
const results = new Map<string, ContextBudget>()
for (const [project, cwd] of projectPaths) {
const budget = await estimateContextBudget(cwd)
results.set(project, budget)
}
return results
}
export async function discoverProjectCwd(sessionDir: string): Promise<string | null> {
let files: string[]
try {
files = (await readdir(sessionDir)).filter(f => f.endsWith('.jsonl'))
} catch { return null }
if (files.length === 0) return null
const content = await readSessionFile(join(sessionDir, files[0]))
if (content === null) return null
for (const line of content.split('\n')) {
if (!line.trim()) continue
try {
const entry = JSON.parse(line)
if (entry.cwd && typeof entry.cwd === 'string') return entry.cwd
} catch { continue }
}
return null
}