mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-04-28 06:59:37 +00:00
Fix Cursor provider for newer versions that store data in agentKv
Newer Cursor versions store conversation data in agentKv entries instead of populating tokenCount in bubbleId entries. This adds a fallback parser that reads agentKv blobs and estimates tokens from content length. Closes #114 Closes #102
This commit is contained in:
parent
f893c964cc
commit
681afdbece
1 changed files with 150 additions and 1 deletions
|
|
@ -34,6 +34,27 @@ type BubbleRow = {
|
||||||
code_blocks: string | null
|
code_blocks: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentKvRow = {
|
||||||
|
key: string
|
||||||
|
role: string | null
|
||||||
|
content: string | null
|
||||||
|
request_id: string | null
|
||||||
|
content_length: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentKvContent = {
|
||||||
|
type?: string
|
||||||
|
text?: string
|
||||||
|
providerOptions?: {
|
||||||
|
cursor?: {
|
||||||
|
modelName?: string
|
||||||
|
requestId?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHARS_PER_TOKEN = 4
|
||||||
|
|
||||||
function getCursorDbPath(): string {
|
function getCursorDbPath(): string {
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
return join(homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb')
|
return join(homedir(), 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb')
|
||||||
|
|
@ -87,6 +108,19 @@ const BUBBLE_QUERY_BASE = `
|
||||||
AND json_extract(value, '$.tokenCount.inputTokens') > 0
|
AND json_extract(value, '$.tokenCount.inputTokens') > 0
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const AGENTKV_QUERY = `
|
||||||
|
SELECT
|
||||||
|
key,
|
||||||
|
json_extract(value, '$.role') as role,
|
||||||
|
json_extract(value, '$.content') as content,
|
||||||
|
json_extract(value, '$.providerOptions.cursor.requestId') as request_id,
|
||||||
|
length(value) as content_length
|
||||||
|
FROM cursorDiskKV
|
||||||
|
WHERE key LIKE 'agentKv:blob:%'
|
||||||
|
AND hex(substr(value, 1, 1)) = '7B'
|
||||||
|
ORDER BY ROWID ASC
|
||||||
|
`
|
||||||
|
|
||||||
const USER_MESSAGES_QUERY = `
|
const USER_MESSAGES_QUERY = `
|
||||||
SELECT
|
SELECT
|
||||||
json_extract(value, '$.conversationId') as conversation_id,
|
json_extract(value, '$.conversationId') as conversation_id,
|
||||||
|
|
@ -207,6 +241,116 @@ function parseBubbles(db: SqliteDatabase, seenKeys: Set<string>): { calls: Parse
|
||||||
return { calls: results }
|
return { calls: results }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractModelFromContent(content: AgentKvContent[]): string | null {
|
||||||
|
for (const c of content) {
|
||||||
|
if (c.providerOptions?.cursor?.modelName) {
|
||||||
|
return c.providerOptions.cursor.modelName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTextLength(content: AgentKvContent[]): number {
|
||||||
|
let total = 0
|
||||||
|
for (const c of content) {
|
||||||
|
if (c.text) total += c.text.length
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAgentKv(db: SqliteDatabase, seenKeys: Set<string>): { calls: ParsedProviderCall[] } {
|
||||||
|
const results: ParsedProviderCall[] = []
|
||||||
|
|
||||||
|
let rows: AgentKvRow[]
|
||||||
|
try {
|
||||||
|
rows = db.query<AgentKvRow>(AGENTKV_QUERY)
|
||||||
|
} catch {
|
||||||
|
return { calls: results }
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessions: Map<string, { inputChars: number; outputChars: number; model: string | null; userText: string }> = new Map()
|
||||||
|
let currentRequestId = 'unknown'
|
||||||
|
let turnIndex = 0
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
if (!row.role || !row.content) continue
|
||||||
|
|
||||||
|
let content: AgentKvContent[]
|
||||||
|
try {
|
||||||
|
content = JSON.parse(row.content)
|
||||||
|
if (!Array.isArray(content)) continue
|
||||||
|
} catch {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = row.request_id ?? currentRequestId
|
||||||
|
if (requestId !== currentRequestId) {
|
||||||
|
currentRequestId = requestId
|
||||||
|
turnIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const textLength = extractTextLength(content)
|
||||||
|
const model = extractModelFromContent(content)
|
||||||
|
|
||||||
|
if (row.role === 'user') {
|
||||||
|
const existing = sessions.get(requestId) ?? { inputChars: 0, outputChars: 0, model: null, userText: '' }
|
||||||
|
existing.inputChars += textLength
|
||||||
|
if (!existing.userText && content[0]?.text) {
|
||||||
|
const text = content[0].text
|
||||||
|
const queryMatch = text.match(/<user_query>([\s\S]*?)<\/user_query>/)
|
||||||
|
existing.userText = queryMatch ? queryMatch[1].trim().slice(0, 500) : text.slice(0, 500)
|
||||||
|
}
|
||||||
|
sessions.set(requestId, existing)
|
||||||
|
} else if (row.role === 'assistant') {
|
||||||
|
const existing = sessions.get(requestId) ?? { inputChars: 0, outputChars: 0, model: null, userText: '' }
|
||||||
|
existing.outputChars += textLength
|
||||||
|
if (model) existing.model = model
|
||||||
|
sessions.set(requestId, existing)
|
||||||
|
} else if (row.role === 'tool' || row.role === 'system') {
|
||||||
|
const existing = sessions.get(requestId) ?? { inputChars: 0, outputChars: 0, model: null, userText: '' }
|
||||||
|
existing.inputChars += textLength
|
||||||
|
sessions.set(requestId, existing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [requestId, session] of sessions) {
|
||||||
|
if (session.inputChars === 0 && session.outputChars === 0) continue
|
||||||
|
|
||||||
|
const inputTokens = Math.ceil(session.inputChars / CHARS_PER_TOKEN)
|
||||||
|
const outputTokens = Math.ceil(session.outputChars / CHARS_PER_TOKEN)
|
||||||
|
const dedupKey = `cursor:agentKv:${requestId}`
|
||||||
|
|
||||||
|
if (seenKeys.has(dedupKey)) continue
|
||||||
|
seenKeys.add(dedupKey)
|
||||||
|
|
||||||
|
const pricingModel = resolveModel(session.model)
|
||||||
|
const displayModel = modelForDisplay(session.model)
|
||||||
|
const costUSD = calculateCost(pricingModel, inputTokens, outputTokens, 0, 0, 0)
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
provider: 'cursor',
|
||||||
|
model: displayModel,
|
||||||
|
inputTokens,
|
||||||
|
outputTokens,
|
||||||
|
cacheCreationInputTokens: 0,
|
||||||
|
cacheReadInputTokens: 0,
|
||||||
|
cachedInputTokens: 0,
|
||||||
|
reasoningTokens: 0,
|
||||||
|
webSearchRequests: 0,
|
||||||
|
costUSD,
|
||||||
|
tools: [],
|
||||||
|
bashCommands: [],
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
speed: 'standard',
|
||||||
|
deduplicationKey: dedupKey,
|
||||||
|
userMessage: session.userText,
|
||||||
|
sessionId: requestId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { calls: results }
|
||||||
|
}
|
||||||
|
|
||||||
function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
|
function createParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
|
||||||
return {
|
return {
|
||||||
async *parse(): AsyncGenerator<ParsedProviderCall> {
|
async *parse(): AsyncGenerator<ParsedProviderCall> {
|
||||||
|
|
@ -239,7 +383,12 @@ function createParser(source: SessionSource, seenKeys: Set<string>): SessionPars
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const { calls } = parseBubbles(db, seenKeys)
|
let { calls } = parseBubbles(db, seenKeys)
|
||||||
|
|
||||||
|
if (calls.length === 0) {
|
||||||
|
const agentKvResult = parseAgentKv(db, seenKeys)
|
||||||
|
calls = agentKvResult.calls
|
||||||
|
}
|
||||||
|
|
||||||
await writeCachedResults(source.path, calls)
|
await writeCachedResults(source.path, calls)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue