@codeany/open-agent-sdk 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +2 -0
- package/dist/engine.js.map +1 -1
- package/package.json +5 -6
- package/src/agent.ts +0 -516
- package/src/engine.ts +0 -630
- package/src/hooks.ts +0 -261
- package/src/index.ts +0 -426
- package/src/mcp/client.ts +0 -150
- package/src/providers/anthropic.ts +0 -60
- package/src/providers/index.ts +0 -34
- package/src/providers/openai.ts +0 -315
- package/src/providers/types.ts +0 -85
- package/src/sdk-mcp-server.ts +0 -78
- package/src/session.ts +0 -227
- package/src/skills/bundled/commit.ts +0 -38
- package/src/skills/bundled/debug.ts +0 -48
- package/src/skills/bundled/index.ts +0 -28
- package/src/skills/bundled/review.ts +0 -41
- package/src/skills/bundled/simplify.ts +0 -51
- package/src/skills/bundled/test.ts +0 -43
- package/src/skills/index.ts +0 -25
- package/src/skills/registry.ts +0 -133
- package/src/skills/types.ts +0 -99
- package/src/tool-helper.ts +0 -127
- package/src/tools/agent-tool.ts +0 -164
- package/src/tools/ask-user.ts +0 -79
- package/src/tools/bash.ts +0 -75
- package/src/tools/config-tool.ts +0 -89
- package/src/tools/cron-tools.ts +0 -153
- package/src/tools/edit.ts +0 -74
- package/src/tools/glob.ts +0 -77
- package/src/tools/grep.ts +0 -168
- package/src/tools/index.ts +0 -240
- package/src/tools/lsp-tool.ts +0 -163
- package/src/tools/mcp-resource-tools.ts +0 -125
- package/src/tools/notebook-edit.ts +0 -93
- package/src/tools/plan-tools.ts +0 -88
- package/src/tools/read.ts +0 -73
- package/src/tools/send-message.ts +0 -96
- package/src/tools/skill-tool.ts +0 -133
- package/src/tools/task-tools.ts +0 -290
- package/src/tools/team-tools.ts +0 -128
- package/src/tools/todo-tool.ts +0 -112
- package/src/tools/tool-search.ts +0 -87
- package/src/tools/types.ts +0 -66
- package/src/tools/web-fetch.ts +0 -66
- package/src/tools/web-search.ts +0 -86
- package/src/tools/worktree-tools.ts +0 -140
- package/src/tools/write.ts +0 -42
- package/src/types.ts +0 -486
- package/src/utils/compact.ts +0 -207
- package/src/utils/context.ts +0 -191
- package/src/utils/fileCache.ts +0 -148
- package/src/utils/messages.ts +0 -195
- package/src/utils/retry.ts +0 -140
- package/src/utils/tokens.ts +0 -145
package/src/utils/compact.ts
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Context Compression / Auto-Compaction
|
|
3
|
-
*
|
|
4
|
-
* Summarizes long conversation histories when context window fills up.
|
|
5
|
-
* Three-tier system:
|
|
6
|
-
* 1. Auto-compact: triggered when tokens exceed threshold
|
|
7
|
-
* 2. Micro-compact: cache-aware per-request optimization
|
|
8
|
-
* 3. Session memory compaction: consolidates across sessions
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { LLMProvider } from '../providers/types.js'
|
|
12
|
-
import type { NormalizedMessageParam } from '../providers/types.js'
|
|
13
|
-
import {
|
|
14
|
-
estimateMessagesTokens,
|
|
15
|
-
getAutoCompactThreshold,
|
|
16
|
-
} from './tokens.js'
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* State for tracking auto-compaction across turns.
|
|
20
|
-
*/
|
|
21
|
-
export interface AutoCompactState {
|
|
22
|
-
compacted: boolean
|
|
23
|
-
turnCounter: number
|
|
24
|
-
consecutiveFailures: number
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Create initial auto-compact state.
|
|
29
|
-
*/
|
|
30
|
-
export function createAutoCompactState(): AutoCompactState {
|
|
31
|
-
return {
|
|
32
|
-
compacted: false,
|
|
33
|
-
turnCounter: 0,
|
|
34
|
-
consecutiveFailures: 0,
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Check if auto-compaction should trigger.
|
|
40
|
-
*/
|
|
41
|
-
export function shouldAutoCompact(
|
|
42
|
-
messages: any[],
|
|
43
|
-
model: string,
|
|
44
|
-
state: AutoCompactState,
|
|
45
|
-
): boolean {
|
|
46
|
-
if (state.consecutiveFailures >= 3) return false
|
|
47
|
-
|
|
48
|
-
const estimatedTokens = estimateMessagesTokens(messages)
|
|
49
|
-
const threshold = getAutoCompactThreshold(model)
|
|
50
|
-
|
|
51
|
-
return estimatedTokens >= threshold
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Compact conversation by summarizing with the LLM.
|
|
56
|
-
*
|
|
57
|
-
* Sends the entire conversation to the LLM for summarization,
|
|
58
|
-
* then replaces the history with a compact summary.
|
|
59
|
-
*/
|
|
60
|
-
export async function compactConversation(
|
|
61
|
-
provider: LLMProvider,
|
|
62
|
-
model: string,
|
|
63
|
-
messages: any[],
|
|
64
|
-
state: AutoCompactState,
|
|
65
|
-
): Promise<{
|
|
66
|
-
compactedMessages: NormalizedMessageParam[]
|
|
67
|
-
summary: string
|
|
68
|
-
state: AutoCompactState
|
|
69
|
-
}> {
|
|
70
|
-
try {
|
|
71
|
-
// Strip images before compacting to save tokens
|
|
72
|
-
const strippedMessages = stripImagesFromMessages(messages)
|
|
73
|
-
|
|
74
|
-
// Build compaction prompt
|
|
75
|
-
const compactionPrompt = buildCompactionPrompt(strippedMessages)
|
|
76
|
-
|
|
77
|
-
const response = await provider.createMessage({
|
|
78
|
-
model,
|
|
79
|
-
maxTokens: 8192,
|
|
80
|
-
system: 'You are a conversation summarizer. Create a detailed summary of the conversation that preserves all important context, decisions made, files modified, tool outputs, and current state. The summary should allow the conversation to continue seamlessly.',
|
|
81
|
-
messages: [
|
|
82
|
-
{
|
|
83
|
-
role: 'user',
|
|
84
|
-
content: compactionPrompt,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
const summary = response.content
|
|
90
|
-
.filter((b) => b.type === 'text')
|
|
91
|
-
.map((b) => (b as { type: 'text'; text: string }).text)
|
|
92
|
-
.join('\n')
|
|
93
|
-
|
|
94
|
-
// Replace messages with summary
|
|
95
|
-
const compactedMessages: NormalizedMessageParam[] = [
|
|
96
|
-
{
|
|
97
|
-
role: 'user',
|
|
98
|
-
content: `[Previous conversation summary]\n\n${summary}\n\n[End of summary - conversation continues below]`,
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
role: 'assistant',
|
|
102
|
-
content: 'I understand the context from the previous conversation. I\'ll continue from where we left off.',
|
|
103
|
-
},
|
|
104
|
-
]
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
compactedMessages,
|
|
108
|
-
summary,
|
|
109
|
-
state: {
|
|
110
|
-
compacted: true,
|
|
111
|
-
turnCounter: state.turnCounter,
|
|
112
|
-
consecutiveFailures: 0,
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
} catch (err: any) {
|
|
116
|
-
return {
|
|
117
|
-
compactedMessages: messages,
|
|
118
|
-
summary: '',
|
|
119
|
-
state: {
|
|
120
|
-
...state,
|
|
121
|
-
consecutiveFailures: state.consecutiveFailures + 1,
|
|
122
|
-
},
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Strip images from messages for compaction safety.
|
|
129
|
-
*/
|
|
130
|
-
function stripImagesFromMessages(
|
|
131
|
-
messages: any[],
|
|
132
|
-
): any[] {
|
|
133
|
-
return messages.map((msg: any) => {
|
|
134
|
-
if (typeof msg.content === 'string') return msg
|
|
135
|
-
|
|
136
|
-
const filtered = (msg.content as any[]).filter((block: any) => {
|
|
137
|
-
return block.type !== 'image'
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
return { ...msg, content: filtered.length > 0 ? filtered : '[content removed for compaction]' }
|
|
141
|
-
})
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Build compaction prompt from messages.
|
|
146
|
-
*/
|
|
147
|
-
function buildCompactionPrompt(messages: any[]): string {
|
|
148
|
-
const parts: string[] = ['Please summarize this conversation:\n']
|
|
149
|
-
|
|
150
|
-
for (const msg of messages) {
|
|
151
|
-
const role = msg.role === 'user' ? 'User' : 'Assistant'
|
|
152
|
-
|
|
153
|
-
if (typeof msg.content === 'string') {
|
|
154
|
-
parts.push(`${role}: ${msg.content.slice(0, 5000)}`)
|
|
155
|
-
} else if (Array.isArray(msg.content)) {
|
|
156
|
-
const texts: string[] = []
|
|
157
|
-
for (const block of msg.content as any[]) {
|
|
158
|
-
if (block.type === 'text') {
|
|
159
|
-
texts.push(block.text.slice(0, 3000))
|
|
160
|
-
} else if (block.type === 'tool_use') {
|
|
161
|
-
texts.push(`[Tool: ${block.name}]`)
|
|
162
|
-
} else if (block.type === 'tool_result') {
|
|
163
|
-
const content = typeof block.content === 'string'
|
|
164
|
-
? block.content.slice(0, 1000)
|
|
165
|
-
: '[tool result]'
|
|
166
|
-
texts.push(`[Tool Result: ${content}]`)
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (texts.length > 0) {
|
|
170
|
-
parts.push(`${role}: ${texts.join('\n')}`)
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return parts.join('\n\n')
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Micro-compact: optimize messages by truncating large tool results
|
|
180
|
-
* to fit within token budgets.
|
|
181
|
-
*/
|
|
182
|
-
export function microCompactMessages(
|
|
183
|
-
messages: any[],
|
|
184
|
-
maxToolResultChars: number = 50000,
|
|
185
|
-
): any[] {
|
|
186
|
-
return messages.map((msg: any) => {
|
|
187
|
-
if (typeof msg.content === 'string') return msg
|
|
188
|
-
if (!Array.isArray(msg.content)) return msg
|
|
189
|
-
|
|
190
|
-
const content = (msg.content as any[]).map((block: any) => {
|
|
191
|
-
if (block.type === 'tool_result' && typeof block.content === 'string') {
|
|
192
|
-
if (block.content.length > maxToolResultChars) {
|
|
193
|
-
return {
|
|
194
|
-
...block,
|
|
195
|
-
content:
|
|
196
|
-
block.content.slice(0, maxToolResultChars / 2) +
|
|
197
|
-
'\n...(truncated)...\n' +
|
|
198
|
-
block.content.slice(-maxToolResultChars / 2),
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return block
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
return { ...msg, content }
|
|
206
|
-
})
|
|
207
|
-
}
|
package/src/utils/context.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* System & User Context
|
|
3
|
-
*
|
|
4
|
-
* Builds context for the system prompt:
|
|
5
|
-
* - Git status injection (branch, commits, status)
|
|
6
|
-
* - AGENT.md / project context discovery and injection
|
|
7
|
-
* - Working directory info
|
|
8
|
-
* - Date injection
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { execSync } from 'child_process'
|
|
12
|
-
import { readFile, stat } from 'fs/promises'
|
|
13
|
-
import { join, resolve } from 'path'
|
|
14
|
-
|
|
15
|
-
// Memoization cache
|
|
16
|
-
let cachedGitStatus: string | null = null
|
|
17
|
-
let cachedGitStatusCwd: string | null = null
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Get git status info for system prompt.
|
|
21
|
-
* Memoized per cwd (cleared on new session).
|
|
22
|
-
*/
|
|
23
|
-
export async function getGitStatus(cwd: string): Promise<string> {
|
|
24
|
-
if (cachedGitStatus && cachedGitStatusCwd === cwd) {
|
|
25
|
-
return cachedGitStatus
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
const parts: string[] = []
|
|
30
|
-
|
|
31
|
-
const gitExec = (cmd: string, timeoutMs = 5000): string | null => {
|
|
32
|
-
try {
|
|
33
|
-
return execSync(cmd, {
|
|
34
|
-
cwd, timeout: timeoutMs, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
-
}).trim()
|
|
36
|
-
} catch {
|
|
37
|
-
return null
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Check if this is a git repo at all
|
|
42
|
-
if (!gitExec('git rev-parse --git-dir')) return ''
|
|
43
|
-
|
|
44
|
-
// Current branch
|
|
45
|
-
const branch = gitExec('git rev-parse --abbrev-ref HEAD')
|
|
46
|
-
if (branch) parts.push(`Current branch: ${branch}`)
|
|
47
|
-
|
|
48
|
-
// Main branch detection
|
|
49
|
-
const mainBranch = detectMainBranch(cwd)
|
|
50
|
-
if (mainBranch) parts.push(`Main branch: ${mainBranch}`)
|
|
51
|
-
|
|
52
|
-
// Git user
|
|
53
|
-
const user = gitExec('git config user.name', 3000)
|
|
54
|
-
if (user) parts.push(`Git user: ${user}`)
|
|
55
|
-
|
|
56
|
-
// Status (staged + unstaged)
|
|
57
|
-
const status = gitExec('git status --short')
|
|
58
|
-
if (status) {
|
|
59
|
-
const truncated = status.length > 2000
|
|
60
|
-
? status.slice(0, 2000) + '\n...(truncated)'
|
|
61
|
-
: status
|
|
62
|
-
parts.push(`Status:\n${truncated}`)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Recent commits (only if HEAD exists)
|
|
66
|
-
const hasHead = gitExec('git rev-parse HEAD')
|
|
67
|
-
if (hasHead) {
|
|
68
|
-
const log = gitExec('git log --oneline -5 --no-decorate')
|
|
69
|
-
if (log) parts.push(`Recent commits:\n${log}`)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
cachedGitStatus = parts.join('\n\n')
|
|
73
|
-
cachedGitStatusCwd = cwd
|
|
74
|
-
|
|
75
|
-
return cachedGitStatus
|
|
76
|
-
} catch {
|
|
77
|
-
return ''
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Detect the main branch name (main or master).
|
|
83
|
-
*/
|
|
84
|
-
function detectMainBranch(cwd: string): string | null {
|
|
85
|
-
try {
|
|
86
|
-
const branches = execSync('git branch -l main master', {
|
|
87
|
-
cwd, timeout: 3000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
88
|
-
}).trim()
|
|
89
|
-
if (branches.includes('main')) return 'main'
|
|
90
|
-
if (branches.includes('master')) return 'master'
|
|
91
|
-
return null
|
|
92
|
-
} catch {
|
|
93
|
-
return null
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Discover project context files (AGENT.md, CLAUDE.md) in the project.
|
|
99
|
-
*/
|
|
100
|
-
export async function discoverProjectContextFiles(cwd: string): Promise<string[]> {
|
|
101
|
-
const candidates = [
|
|
102
|
-
join(cwd, 'AGENT.md'),
|
|
103
|
-
join(cwd, 'CLAUDE.md'),
|
|
104
|
-
join(cwd, '.claude', 'CLAUDE.md'),
|
|
105
|
-
join(cwd, 'claude.md'),
|
|
106
|
-
]
|
|
107
|
-
|
|
108
|
-
// Also check home directory
|
|
109
|
-
const home = process.env.HOME || process.env.USERPROFILE || ''
|
|
110
|
-
if (home) {
|
|
111
|
-
candidates.push(
|
|
112
|
-
join(home, '.claude', 'CLAUDE.md'),
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const found: string[] = []
|
|
117
|
-
for (const path of candidates) {
|
|
118
|
-
try {
|
|
119
|
-
const s = await stat(path)
|
|
120
|
-
if (s.isFile()) {
|
|
121
|
-
found.push(path)
|
|
122
|
-
}
|
|
123
|
-
} catch {
|
|
124
|
-
// File doesn't exist
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return found
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Read project context file content from discovered files.
|
|
133
|
-
*/
|
|
134
|
-
export async function readProjectContextContent(cwd: string): Promise<string> {
|
|
135
|
-
const files = await discoverProjectContextFiles(cwd)
|
|
136
|
-
if (files.length === 0) return ''
|
|
137
|
-
|
|
138
|
-
const parts: string[] = []
|
|
139
|
-
for (const file of files) {
|
|
140
|
-
try {
|
|
141
|
-
const content = await readFile(file, 'utf-8')
|
|
142
|
-
if (content.trim()) {
|
|
143
|
-
parts.push(`# From ${file}:\n${content.trim()}`)
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
// Skip unreadable files
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return parts.join('\n\n')
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Get system context for the system prompt.
|
|
155
|
-
*/
|
|
156
|
-
export async function getSystemContext(cwd: string): Promise<string> {
|
|
157
|
-
const parts: string[] = []
|
|
158
|
-
|
|
159
|
-
const gitStatus = await getGitStatus(cwd)
|
|
160
|
-
if (gitStatus) {
|
|
161
|
-
parts.push(`gitStatus: ${gitStatus}`)
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return parts.join('\n\n')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Get user context (AGENT.md, date, etc).
|
|
169
|
-
*/
|
|
170
|
-
export async function getUserContext(cwd: string): Promise<string> {
|
|
171
|
-
const parts: string[] = []
|
|
172
|
-
|
|
173
|
-
// Current date
|
|
174
|
-
parts.push(`# currentDate\nToday's date is ${new Date().toISOString().split('T')[0]}.`)
|
|
175
|
-
|
|
176
|
-
// Project context files
|
|
177
|
-
const projectCtx = await readProjectContextContent(cwd)
|
|
178
|
-
if (projectCtx) {
|
|
179
|
-
parts.push(projectCtx)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return parts.join('\n\n')
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Clear memoized context (call between sessions).
|
|
187
|
-
*/
|
|
188
|
-
export function clearContextCache(): void {
|
|
189
|
-
cachedGitStatus = null
|
|
190
|
-
cachedGitStatusCwd = null
|
|
191
|
-
}
|
package/src/utils/fileCache.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File State LRU Cache
|
|
3
|
-
*
|
|
4
|
-
* Bounded cache for file contents with path normalization.
|
|
5
|
-
* Used to track file states for compaction diffs and
|
|
6
|
-
* avoiding redundant reads.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { normalize, resolve } from 'path'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Cached file state.
|
|
13
|
-
*/
|
|
14
|
-
export interface FileState {
|
|
15
|
-
content: string
|
|
16
|
-
timestamp: number
|
|
17
|
-
offset?: number
|
|
18
|
-
limit?: number
|
|
19
|
-
isPartialView?: boolean
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* LRU file state cache with size limits.
|
|
24
|
-
*/
|
|
25
|
-
export class FileStateCache {
|
|
26
|
-
private cache = new Map<string, FileState>()
|
|
27
|
-
private maxEntries: number
|
|
28
|
-
private maxSizeBytes: number
|
|
29
|
-
private currentSizeBytes = 0
|
|
30
|
-
|
|
31
|
-
constructor(maxEntries: number = 100, maxSizeBytes: number = 25 * 1024 * 1024) {
|
|
32
|
-
this.maxEntries = maxEntries
|
|
33
|
-
this.maxSizeBytes = maxSizeBytes
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Normalize a file path for cache lookup.
|
|
38
|
-
*/
|
|
39
|
-
private normalizePath(filePath: string): string {
|
|
40
|
-
return normalize(resolve(filePath))
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Get a cached file state.
|
|
45
|
-
*/
|
|
46
|
-
get(filePath: string): FileState | undefined {
|
|
47
|
-
const key = this.normalizePath(filePath)
|
|
48
|
-
const entry = this.cache.get(key)
|
|
49
|
-
if (entry) {
|
|
50
|
-
// Move to end (most recently used)
|
|
51
|
-
this.cache.delete(key)
|
|
52
|
-
this.cache.set(key, entry)
|
|
53
|
-
}
|
|
54
|
-
return entry
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Set a cached file state.
|
|
59
|
-
*/
|
|
60
|
-
set(filePath: string, state: FileState): void {
|
|
61
|
-
const key = this.normalizePath(filePath)
|
|
62
|
-
|
|
63
|
-
// Remove old entry if exists
|
|
64
|
-
const old = this.cache.get(key)
|
|
65
|
-
if (old) {
|
|
66
|
-
this.currentSizeBytes -= Buffer.byteLength(old.content, 'utf-8')
|
|
67
|
-
this.cache.delete(key)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const newSize = Buffer.byteLength(state.content, 'utf-8')
|
|
71
|
-
|
|
72
|
-
// Evict entries if necessary
|
|
73
|
-
while (
|
|
74
|
-
(this.cache.size >= this.maxEntries || this.currentSizeBytes + newSize > this.maxSizeBytes) &&
|
|
75
|
-
this.cache.size > 0
|
|
76
|
-
) {
|
|
77
|
-
const firstKey = this.cache.keys().next().value
|
|
78
|
-
if (firstKey) {
|
|
79
|
-
const entry = this.cache.get(firstKey)
|
|
80
|
-
if (entry) {
|
|
81
|
-
this.currentSizeBytes -= Buffer.byteLength(entry.content, 'utf-8')
|
|
82
|
-
}
|
|
83
|
-
this.cache.delete(firstKey)
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
this.cache.set(key, state)
|
|
88
|
-
this.currentSizeBytes += newSize
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Delete a cached entry.
|
|
93
|
-
*/
|
|
94
|
-
delete(filePath: string): boolean {
|
|
95
|
-
const key = this.normalizePath(filePath)
|
|
96
|
-
const entry = this.cache.get(key)
|
|
97
|
-
if (entry) {
|
|
98
|
-
this.currentSizeBytes -= Buffer.byteLength(entry.content, 'utf-8')
|
|
99
|
-
this.cache.delete(key)
|
|
100
|
-
return true
|
|
101
|
-
}
|
|
102
|
-
return false
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Clear all cached entries.
|
|
107
|
-
*/
|
|
108
|
-
clear(): void {
|
|
109
|
-
this.cache.clear()
|
|
110
|
-
this.currentSizeBytes = 0
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Get the number of cached entries.
|
|
115
|
-
*/
|
|
116
|
-
get size(): number {
|
|
117
|
-
return this.cache.size
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Get all cached file paths.
|
|
122
|
-
*/
|
|
123
|
-
keys(): string[] {
|
|
124
|
-
return Array.from(this.cache.keys())
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Clone the cache.
|
|
129
|
-
*/
|
|
130
|
-
clone(): FileStateCache {
|
|
131
|
-
const clone = new FileStateCache(this.maxEntries, this.maxSizeBytes)
|
|
132
|
-
for (const [key, value] of this.cache) {
|
|
133
|
-
clone.cache.set(key, { ...value })
|
|
134
|
-
}
|
|
135
|
-
clone.currentSizeBytes = this.currentSizeBytes
|
|
136
|
-
return clone
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Create a file state cache with default limits.
|
|
142
|
-
*/
|
|
143
|
-
export function createFileStateCache(
|
|
144
|
-
maxEntries: number = 100,
|
|
145
|
-
maxSizeBytes: number = 25 * 1024 * 1024,
|
|
146
|
-
): FileStateCache {
|
|
147
|
-
return new FileStateCache(maxEntries, maxSizeBytes)
|
|
148
|
-
}
|