costhawk 1.5.10 → 1.5.12
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/build-info.d.ts +1 -1
- package/dist/build-info.js +1 -1
- package/dist/cursor-parser.d.ts +86 -0
- package/dist/cursor-parser.d.ts.map +1 -0
- package/dist/cursor-parser.js +355 -0
- package/dist/cursor-parser.js.map +1 -0
- package/dist/index.js +168 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/build-info.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const BUILD_COMMIT_SHA = "
|
|
1
|
+
export declare const BUILD_COMMIT_SHA = "d2dc17c";
|
|
2
2
|
//# sourceMappingURL=build-info.d.ts.map
|
package/dist/build-info.js
CHANGED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Local SQLite Parser (PR1 — dry-run only)
|
|
3
|
+
*
|
|
4
|
+
* Parses Cursor IDE chat history from the local SQLite database to extract
|
|
5
|
+
* token usage data. Read-only. Does not push to any backend.
|
|
6
|
+
*
|
|
7
|
+
* Storage:
|
|
8
|
+
* macOS: ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
|
|
9
|
+
* Linux: ~/.config/Cursor/User/globalStorage/state.vscdb
|
|
10
|
+
* Windows: %APPDATA%/Cursor/User/globalStorage/state.vscdb
|
|
11
|
+
*
|
|
12
|
+
* Schema:
|
|
13
|
+
* Table cursorDiskKV (key TEXT, value BLOB)
|
|
14
|
+
* Conversations: composerData:<composerId>
|
|
15
|
+
* Messages: bubbleId:<composerId>:<bubbleId>
|
|
16
|
+
*
|
|
17
|
+
* Token data lives at $.tokenCount.inputTokens and $.tokenCount.outputTokens
|
|
18
|
+
* on bubble rows. Model name at $.modelInfo.modelName. Server-side dedup id
|
|
19
|
+
* at $.serverBubbleId.
|
|
20
|
+
*
|
|
21
|
+
* NOTE (PR1 scope): Cursor message timestamps are not yet verified across
|
|
22
|
+
* versions, so this dry-run parser does NOT return startTime/endTime/dailyUsage.
|
|
23
|
+
* PR2 will add timestamp support after verification on real Cursor data.
|
|
24
|
+
*
|
|
25
|
+
* Workspace metadata fields (workspaceHash/workspaceName) are also unverified
|
|
26
|
+
* and return null until PR2 confirms the stable field source.
|
|
27
|
+
*/
|
|
28
|
+
import type { TokenUsage } from "./transcript-parser.js";
|
|
29
|
+
/**
|
|
30
|
+
* Single Cursor session in dry-run output.
|
|
31
|
+
*
|
|
32
|
+
* Shape is intentionally distinct from Claude/Codex SessionUsage because
|
|
33
|
+
* Cursor message timestamps are not yet verified — startTime/endTime are
|
|
34
|
+
* deliberately absent until PR2 verifies the stable timestamp source.
|
|
35
|
+
*/
|
|
36
|
+
export interface CursorSessionUsageDryRun {
|
|
37
|
+
sessionId: string;
|
|
38
|
+
workspaceHash: string | null;
|
|
39
|
+
workspaceName: string | null;
|
|
40
|
+
model: string;
|
|
41
|
+
tokens: TokenUsage;
|
|
42
|
+
messageCount: number;
|
|
43
|
+
filePath: string;
|
|
44
|
+
}
|
|
45
|
+
export interface CursorParserError {
|
|
46
|
+
code: "CURSOR_DB_NOT_FOUND" | "CURSOR_SQLITE3_NOT_FOUND" | "CURSOR_SQLITE_QUERY_FAILED";
|
|
47
|
+
message: string;
|
|
48
|
+
}
|
|
49
|
+
export interface CursorParserResult {
|
|
50
|
+
sessions: CursorSessionUsageDryRun[];
|
|
51
|
+
filePath: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the default Cursor SQLite path for the current platform, honoring
|
|
55
|
+
* the COSTHAWK_CURSOR_DB_PATH environment override.
|
|
56
|
+
*/
|
|
57
|
+
export declare function getCursorDbPath(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Check whether the Cursor SQLite database exists at the resolved path.
|
|
60
|
+
*/
|
|
61
|
+
export declare function cursorDbExists(): boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Type guard — narrows an unknown error to a CursorParserError.
|
|
64
|
+
*/
|
|
65
|
+
declare function isCursorParserError(value: unknown): value is CursorParserError;
|
|
66
|
+
/**
|
|
67
|
+
* Parse Cursor usage from local SQLite. Read-only dry run — does NOT push
|
|
68
|
+
* anything to the costcanary backend.
|
|
69
|
+
*
|
|
70
|
+
* Returns aggregated session data per composer with per-session token totals
|
|
71
|
+
* and message counts. Throws CursorParserError on unrecoverable failures
|
|
72
|
+
* (missing DB, missing sqlite3 binary, malformed SQLite output).
|
|
73
|
+
*
|
|
74
|
+
* Dedup strategy: per composer, keep one entry per (serverBubbleId ?? bubbleId).
|
|
75
|
+
* On collision, keep the candidate with the larger token total.
|
|
76
|
+
*
|
|
77
|
+
* Mixed-model handling: if a composer contains multiple non-empty model names,
|
|
78
|
+
* the returned `model` field is "mixed". If no model info is present on any
|
|
79
|
+
* bubble, the field is "unknown".
|
|
80
|
+
*
|
|
81
|
+
* Sort order: total tokens descending. NOT chronological — message timestamps
|
|
82
|
+
* are not yet verified for Cursor.
|
|
83
|
+
*/
|
|
84
|
+
export declare function parseCursorUsageDryRun(): CursorParserResult;
|
|
85
|
+
export { isCursorParserError };
|
|
86
|
+
//# sourceMappingURL=cursor-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-parser.d.ts","sourceRoot":"","sources":["../src/cursor-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAOH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAOzD;;;;;;GAMG;AACH,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EACA,qBAAqB,GACrB,0BAA0B,GAC1B,4BAA4B,CAAC;IACjC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,wBAAwB,EAAE,CAAC;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAmCxC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAUD;;GAEG;AACH,iBAAS,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAYvE;AA+KD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,sBAAsB,IAAI,kBAAkB,CAuH3D;AAID,OAAO,EAAE,mBAAmB,EAAE,CAAC"}
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Local SQLite Parser (PR1 — dry-run only)
|
|
3
|
+
*
|
|
4
|
+
* Parses Cursor IDE chat history from the local SQLite database to extract
|
|
5
|
+
* token usage data. Read-only. Does not push to any backend.
|
|
6
|
+
*
|
|
7
|
+
* Storage:
|
|
8
|
+
* macOS: ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
|
|
9
|
+
* Linux: ~/.config/Cursor/User/globalStorage/state.vscdb
|
|
10
|
+
* Windows: %APPDATA%/Cursor/User/globalStorage/state.vscdb
|
|
11
|
+
*
|
|
12
|
+
* Schema:
|
|
13
|
+
* Table cursorDiskKV (key TEXT, value BLOB)
|
|
14
|
+
* Conversations: composerData:<composerId>
|
|
15
|
+
* Messages: bubbleId:<composerId>:<bubbleId>
|
|
16
|
+
*
|
|
17
|
+
* Token data lives at $.tokenCount.inputTokens and $.tokenCount.outputTokens
|
|
18
|
+
* on bubble rows. Model name at $.modelInfo.modelName. Server-side dedup id
|
|
19
|
+
* at $.serverBubbleId.
|
|
20
|
+
*
|
|
21
|
+
* NOTE (PR1 scope): Cursor message timestamps are not yet verified across
|
|
22
|
+
* versions, so this dry-run parser does NOT return startTime/endTime/dailyUsage.
|
|
23
|
+
* PR2 will add timestamp support after verification on real Cursor data.
|
|
24
|
+
*
|
|
25
|
+
* Workspace metadata fields (workspaceHash/workspaceName) are also unverified
|
|
26
|
+
* and return null until PR2 confirms the stable field source.
|
|
27
|
+
*/
|
|
28
|
+
import { execFileSync } from "child_process";
|
|
29
|
+
import { existsSync } from "fs";
|
|
30
|
+
import { homedir, platform } from "os";
|
|
31
|
+
import { join } from "path";
|
|
32
|
+
// Defaults — overridable via env vars
|
|
33
|
+
const DEFAULT_SQLITE3_PATH = "/usr/bin/sqlite3";
|
|
34
|
+
const SQLITE_TIMEOUT_MS = 10_000;
|
|
35
|
+
const SQLITE_MAX_BUFFER_BYTES = 32 * 1024 * 1024;
|
|
36
|
+
/**
|
|
37
|
+
* Get the default Cursor SQLite path for the current platform, honoring
|
|
38
|
+
* the COSTHAWK_CURSOR_DB_PATH environment override.
|
|
39
|
+
*/
|
|
40
|
+
export function getCursorDbPath() {
|
|
41
|
+
const envOverride = process.env.COSTHAWK_CURSOR_DB_PATH;
|
|
42
|
+
if (envOverride && envOverride.length > 0) {
|
|
43
|
+
return envOverride;
|
|
44
|
+
}
|
|
45
|
+
const home = homedir();
|
|
46
|
+
if (platform() === "darwin") {
|
|
47
|
+
return join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
48
|
+
}
|
|
49
|
+
if (platform() === "win32") {
|
|
50
|
+
const appData = process.env.APPDATA;
|
|
51
|
+
if (appData) {
|
|
52
|
+
return join(appData, "Cursor", "User", "globalStorage", "state.vscdb");
|
|
53
|
+
}
|
|
54
|
+
return join(home, "AppData", "Roaming", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
55
|
+
}
|
|
56
|
+
// Linux and other unix-likes
|
|
57
|
+
return join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Check whether the Cursor SQLite database exists at the resolved path.
|
|
61
|
+
*/
|
|
62
|
+
export function cursorDbExists() {
|
|
63
|
+
return existsSync(getCursorDbPath());
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the sqlite3 binary path. Defaults to /usr/bin/sqlite3, honoring
|
|
67
|
+
* the COSTHAWK_SQLITE3_PATH environment override.
|
|
68
|
+
*/
|
|
69
|
+
function getSqlite3Path() {
|
|
70
|
+
return process.env.COSTHAWK_SQLITE3_PATH ?? DEFAULT_SQLITE3_PATH;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Type guard — narrows an unknown error to a CursorParserError.
|
|
74
|
+
*/
|
|
75
|
+
function isCursorParserError(value) {
|
|
76
|
+
if (typeof value !== "object" || value === null) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const obj = value;
|
|
80
|
+
return (typeof obj.code === "string" &&
|
|
81
|
+
(obj.code === "CURSOR_DB_NOT_FOUND" ||
|
|
82
|
+
obj.code === "CURSOR_SQLITE3_NOT_FOUND" ||
|
|
83
|
+
obj.code === "CURSOR_SQLITE_QUERY_FAILED") &&
|
|
84
|
+
typeof obj.message === "string");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run a SQL query against the Cursor SQLite via shell-out to the system
|
|
88
|
+
* sqlite3 binary. Returns parsed rows as an array of {key, value} objects,
|
|
89
|
+
* or throws CursorParserError on unrecoverable failures.
|
|
90
|
+
*
|
|
91
|
+
* Uses execFileSync with an arg array (not shell strings) to avoid shell
|
|
92
|
+
* injection. Sets explicit timeout and maxBuffer to defend against runaway
|
|
93
|
+
* queries or oversized state.vscdb files.
|
|
94
|
+
*/
|
|
95
|
+
function runCursorQuery(sql) {
|
|
96
|
+
const sqlite3Path = getSqlite3Path();
|
|
97
|
+
const dbPath = getCursorDbPath();
|
|
98
|
+
if (!existsSync(dbPath)) {
|
|
99
|
+
const error = {
|
|
100
|
+
code: "CURSOR_DB_NOT_FOUND",
|
|
101
|
+
message: `Cursor SQLite database not found at ${dbPath}. Make sure Cursor is installed and you have used it at least once. Set COSTHAWK_CURSOR_DB_PATH to override.`,
|
|
102
|
+
};
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
let stdout;
|
|
106
|
+
try {
|
|
107
|
+
stdout = execFileSync(sqlite3Path, ["-readonly", "-batch", "-json", "--", dbPath, sql], {
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
timeout: SQLITE_TIMEOUT_MS,
|
|
110
|
+
maxBuffer: SQLITE_MAX_BUFFER_BYTES,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const errno = err.code;
|
|
115
|
+
if (errno === "ENOENT") {
|
|
116
|
+
const error = {
|
|
117
|
+
code: "CURSOR_SQLITE3_NOT_FOUND",
|
|
118
|
+
message: `sqlite3 binary not found at ${sqlite3Path}. Set COSTHAWK_SQLITE3_PATH to override the default path.`,
|
|
119
|
+
};
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
123
|
+
const error = {
|
|
124
|
+
code: "CURSOR_SQLITE_QUERY_FAILED",
|
|
125
|
+
message,
|
|
126
|
+
};
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
if (!stdout || stdout.trim().length === 0) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
// sqlite3 -json output is a JSON array of objects when rows exist,
|
|
133
|
+
// or empty / whitespace when no rows. Anything else is a real failure
|
|
134
|
+
// and must surface as CURSOR_SQLITE_QUERY_FAILED — silently returning
|
|
135
|
+
// [] would mask a parser failure as "no sessions found".
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = JSON.parse(stdout);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
const error = {
|
|
142
|
+
code: "CURSOR_SQLITE_QUERY_FAILED",
|
|
143
|
+
message: "sqlite3 returned invalid JSON output",
|
|
144
|
+
};
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
if (!Array.isArray(parsed)) {
|
|
148
|
+
const error = {
|
|
149
|
+
code: "CURSOR_SQLITE_QUERY_FAILED",
|
|
150
|
+
message: "sqlite3 returned a non-array JSON payload",
|
|
151
|
+
};
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
return parsed.filter((row) => typeof row === "object" &&
|
|
155
|
+
row !== null &&
|
|
156
|
+
"key" in row &&
|
|
157
|
+
"value" in row &&
|
|
158
|
+
typeof row.key === "string" &&
|
|
159
|
+
typeof row.value === "string");
|
|
160
|
+
}
|
|
161
|
+
function hasTokenUsage(bubble) {
|
|
162
|
+
return bubble.inputTokens > 0 || bubble.outputTokens > 0;
|
|
163
|
+
}
|
|
164
|
+
const BUBBLE_KEY_REGEX = /^bubbleId:([^:]+):(.+)$/;
|
|
165
|
+
/**
|
|
166
|
+
* Parse a single bubbleId row into structured BubbleData.
|
|
167
|
+
*
|
|
168
|
+
* Returns null if the row key is malformed, the value is not parseable JSON,
|
|
169
|
+
* or the row contains neither a non-empty model name nor any positive token
|
|
170
|
+
* counts. Cursor can store model metadata and token usage on different rows
|
|
171
|
+
* (model name typically lives on user-prompt bubbles, token counts live on
|
|
172
|
+
* assistant-response bubbles), so the parser must accept either signal in
|
|
173
|
+
* isolation and let the per-composer aggregation merge them.
|
|
174
|
+
*/
|
|
175
|
+
function parseBubble(row) {
|
|
176
|
+
const match = BUBBLE_KEY_REGEX.exec(row.key);
|
|
177
|
+
if (!match) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const [, composerId, bubbleId] = match;
|
|
181
|
+
let value;
|
|
182
|
+
try {
|
|
183
|
+
value = JSON.parse(row.value);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (typeof value !== "object" || value === null) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
const obj = value;
|
|
192
|
+
let inputTokens = 0;
|
|
193
|
+
let outputTokens = 0;
|
|
194
|
+
const tokenCount = obj.tokenCount;
|
|
195
|
+
if (typeof tokenCount === "object" && tokenCount !== null) {
|
|
196
|
+
const tc = tokenCount;
|
|
197
|
+
inputTokens = typeof tc.inputTokens === "number" ? tc.inputTokens : 0;
|
|
198
|
+
outputTokens = typeof tc.outputTokens === "number" ? tc.outputTokens : 0;
|
|
199
|
+
}
|
|
200
|
+
let modelName;
|
|
201
|
+
const modelInfo = obj.modelInfo;
|
|
202
|
+
if (typeof modelInfo === "object" && modelInfo !== null) {
|
|
203
|
+
const mi = modelInfo;
|
|
204
|
+
if (typeof mi.modelName === "string" && mi.modelName.length > 0) {
|
|
205
|
+
modelName = mi.modelName;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Skip rows with no usable signal at all — neither model metadata nor
|
|
209
|
+
// positive token counts. These are typically system messages, empty
|
|
210
|
+
// bubbles, or tool-call bookkeeping rows.
|
|
211
|
+
if (!modelName && inputTokens === 0 && outputTokens === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
let serverBubbleId;
|
|
215
|
+
if (typeof obj.serverBubbleId === "string" &&
|
|
216
|
+
obj.serverBubbleId.length > 0) {
|
|
217
|
+
serverBubbleId = obj.serverBubbleId;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
composerId,
|
|
221
|
+
bubbleId,
|
|
222
|
+
serverBubbleId,
|
|
223
|
+
modelName,
|
|
224
|
+
inputTokens,
|
|
225
|
+
outputTokens,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Parse Cursor usage from local SQLite. Read-only dry run — does NOT push
|
|
230
|
+
* anything to the costcanary backend.
|
|
231
|
+
*
|
|
232
|
+
* Returns aggregated session data per composer with per-session token totals
|
|
233
|
+
* and message counts. Throws CursorParserError on unrecoverable failures
|
|
234
|
+
* (missing DB, missing sqlite3 binary, malformed SQLite output).
|
|
235
|
+
*
|
|
236
|
+
* Dedup strategy: per composer, keep one entry per (serverBubbleId ?? bubbleId).
|
|
237
|
+
* On collision, keep the candidate with the larger token total.
|
|
238
|
+
*
|
|
239
|
+
* Mixed-model handling: if a composer contains multiple non-empty model names,
|
|
240
|
+
* the returned `model` field is "mixed". If no model info is present on any
|
|
241
|
+
* bubble, the field is "unknown".
|
|
242
|
+
*
|
|
243
|
+
* Sort order: total tokens descending. NOT chronological — message timestamps
|
|
244
|
+
* are not yet verified for Cursor.
|
|
245
|
+
*/
|
|
246
|
+
export function parseCursorUsageDryRun() {
|
|
247
|
+
const dbPath = getCursorDbPath();
|
|
248
|
+
// Throws CursorParserError on missing DB / missing sqlite3 / query failure
|
|
249
|
+
const rows = runCursorQuery("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'");
|
|
250
|
+
// Cursor splits model metadata and token usage across different bubble
|
|
251
|
+
// rows: model names typically live on user-prompt bubbles (type 1) with
|
|
252
|
+
// zero token counts, and token counts live on assistant-response bubbles
|
|
253
|
+
// (type 2) with no model info. We collect them separately and merge per
|
|
254
|
+
// composer.
|
|
255
|
+
//
|
|
256
|
+
// - tokenBubblesByComposer: per-composer dedup map for bubbles that carry
|
|
257
|
+
// positive token counts. Dedup key is (serverBubbleId ?? bubbleId).
|
|
258
|
+
// Collision rule: keep the candidate with the larger token total.
|
|
259
|
+
// - modelsByComposer: per-composer set of all distinct non-empty model
|
|
260
|
+
// names found on ANY bubble row in the composer. No dedup needed since
|
|
261
|
+
// models are categorical, not additive.
|
|
262
|
+
//
|
|
263
|
+
// Model harvesting is intentionally not gated on type or on token presence
|
|
264
|
+
// — if Cursor ever stores model names on assistant rows in a future
|
|
265
|
+
// schema, this code already supports it.
|
|
266
|
+
const tokenBubblesByComposer = new Map();
|
|
267
|
+
const modelsByComposer = new Map();
|
|
268
|
+
for (const row of rows) {
|
|
269
|
+
const bubble = parseBubble(row);
|
|
270
|
+
if (!bubble) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (bubble.modelName) {
|
|
274
|
+
let composerModels = modelsByComposer.get(bubble.composerId);
|
|
275
|
+
if (!composerModels) {
|
|
276
|
+
composerModels = new Set();
|
|
277
|
+
modelsByComposer.set(bubble.composerId, composerModels);
|
|
278
|
+
}
|
|
279
|
+
composerModels.add(bubble.modelName);
|
|
280
|
+
}
|
|
281
|
+
if (!hasTokenUsage(bubble)) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
let composerMap = tokenBubblesByComposer.get(bubble.composerId);
|
|
285
|
+
if (!composerMap) {
|
|
286
|
+
composerMap = new Map();
|
|
287
|
+
tokenBubblesByComposer.set(bubble.composerId, composerMap);
|
|
288
|
+
}
|
|
289
|
+
const dedupKey = bubble.serverBubbleId ?? bubble.bubbleId;
|
|
290
|
+
const existing = composerMap.get(dedupKey);
|
|
291
|
+
if (existing) {
|
|
292
|
+
const existingTotal = existing.inputTokens + existing.outputTokens;
|
|
293
|
+
const newTotal = bubble.inputTokens + bubble.outputTokens;
|
|
294
|
+
if (newTotal > existingTotal) {
|
|
295
|
+
composerMap.set(dedupKey, bubble);
|
|
296
|
+
}
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
composerMap.set(dedupKey, bubble);
|
|
300
|
+
}
|
|
301
|
+
// Aggregate per composer into the dry-run output shape.
|
|
302
|
+
const sessions = [];
|
|
303
|
+
for (const [composerId, composerMap] of tokenBubblesByComposer) {
|
|
304
|
+
let inputTokens = 0;
|
|
305
|
+
let outputTokens = 0;
|
|
306
|
+
let messageCount = 0;
|
|
307
|
+
const modelsSeen = modelsByComposer.get(composerId) ?? new Set();
|
|
308
|
+
for (const bubble of composerMap.values()) {
|
|
309
|
+
inputTokens += bubble.inputTokens;
|
|
310
|
+
outputTokens += bubble.outputTokens;
|
|
311
|
+
messageCount += 1;
|
|
312
|
+
}
|
|
313
|
+
if (messageCount === 0) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
let model;
|
|
317
|
+
if (modelsSeen.size === 0) {
|
|
318
|
+
model = "unknown";
|
|
319
|
+
}
|
|
320
|
+
else if (modelsSeen.size === 1) {
|
|
321
|
+
model = Array.from(modelsSeen)[0];
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
model = "mixed";
|
|
325
|
+
}
|
|
326
|
+
sessions.push({
|
|
327
|
+
sessionId: composerId,
|
|
328
|
+
workspaceHash: null, // Unverified in PR1 — set in PR2
|
|
329
|
+
workspaceName: null, // Unverified in PR1 — set in PR2
|
|
330
|
+
model,
|
|
331
|
+
tokens: {
|
|
332
|
+
inputTokens,
|
|
333
|
+
outputTokens,
|
|
334
|
+
cacheCreationTokens: 0, // Cursor does not have prompt cache tokens
|
|
335
|
+
cacheReadTokens: 0,
|
|
336
|
+
},
|
|
337
|
+
messageCount,
|
|
338
|
+
filePath: dbPath,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
// Sort by total tokens descending. NOT recency — no verified timestamps.
|
|
342
|
+
sessions.sort((a, b) => {
|
|
343
|
+
const aTotal = a.tokens.inputTokens + a.tokens.outputTokens;
|
|
344
|
+
const bTotal = b.tokens.inputTokens + b.tokens.outputTokens;
|
|
345
|
+
return bTotal - aTotal;
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
sessions,
|
|
349
|
+
filePath: dbPath,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
// Re-export the type guard so the MCP tool registration in index.ts can
|
|
353
|
+
// distinguish CursorParserError from generic Error in its catch block.
|
|
354
|
+
export { isCursorParserError };
|
|
355
|
+
//# sourceMappingURL=cursor-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor-parser.js","sourceRoot":"","sources":["../src/cursor-parser.ts"],"names":[],"mappings":"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"}
|