@tokentop/agent-cursor 1.0.0 → 1.2.0

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/index.js CHANGED
@@ -1,10 +1,861 @@
1
1
  // src/index.ts
2
- import * as fs from "fs";
3
- import * as os from "os";
4
- import * as path from "path";
2
+ import * as fs4 from "fs";
5
3
  import {
6
4
  createAgentPlugin
7
5
  } from "@tokentop/plugin-sdk";
6
+
7
+ // src/cache.ts
8
+ var sessionCache = {
9
+ lastCheck: 0,
10
+ lastResult: [],
11
+ lastLimit: 0,
12
+ lastSince: undefined
13
+ };
14
+ var CACHE_TTL_MS = 2000;
15
+ var SESSION_AGGREGATE_CACHE_MAX = 1e4;
16
+ var sessionAggregateCache = new Map;
17
+ function evictSessionAggregateCache() {
18
+ if (sessionAggregateCache.size <= SESSION_AGGREGATE_CACHE_MAX)
19
+ return;
20
+ const entries = Array.from(sessionAggregateCache.entries());
21
+ entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
22
+ const toEvict = entries.length - SESSION_AGGREGATE_CACHE_MAX;
23
+ for (let i = 0;i < toEvict; i++) {
24
+ sessionAggregateCache.delete(entries[i][0]);
25
+ }
26
+ }
27
+ var composerMetadataIndex = new Map;
28
+
29
+ // src/parser.ts
30
+ import * as fs3 from "fs/promises";
31
+
32
+ // src/paths.ts
33
+ import * as fs from "fs/promises";
34
+ import * as os from "os";
35
+ import * as path from "path";
36
+ var HOME = os.homedir();
37
+ var PLATFORM = os.platform();
38
+ var CURSOR_HOME = path.join(HOME, ".cursor");
39
+ function getCursorUserDataPath() {
40
+ switch (PLATFORM) {
41
+ case "darwin":
42
+ return path.join(HOME, "Library", "Application Support", "Cursor", "User");
43
+ case "win32":
44
+ return path.join(process.env.APPDATA ?? path.join(HOME, "AppData", "Roaming"), "Cursor", "User");
45
+ default:
46
+ return path.join(HOME, ".config", "Cursor", "User");
47
+ }
48
+ }
49
+ var CURSOR_USER_DATA = getCursorUserDataPath();
50
+ var CURSOR_GLOBAL_STORAGE_PATH = path.join(CURSOR_USER_DATA, "globalStorage");
51
+ var CURSOR_WORKSPACE_STORAGE_PATH = path.join(CURSOR_USER_DATA, "workspaceStorage");
52
+ var CURSOR_STATE_DB_PATH = path.join(CURSOR_GLOBAL_STORAGE_PATH, "state.vscdb");
53
+ async function getWorkspaceDirs() {
54
+ try {
55
+ const entries = await fs.readdir(CURSOR_WORKSPACE_STORAGE_PATH, { withFileTypes: true });
56
+ const dirs = [];
57
+ for (const entry of entries) {
58
+ if (entry.isDirectory()) {
59
+ dirs.push(path.join(CURSOR_WORKSPACE_STORAGE_PATH, entry.name));
60
+ }
61
+ }
62
+ return dirs;
63
+ } catch {
64
+ return [];
65
+ }
66
+ }
67
+
68
+ // src/utils.ts
69
+ import { Database } from "bun:sqlite";
70
+ import * as fs2 from "fs/promises";
71
+ import * as path2 from "path";
72
+ function openDatabase(dbPath) {
73
+ try {
74
+ return new Database(dbPath, { readonly: true, create: false });
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+ function openDatabaseForWatcher(dbPath) {
80
+ try {
81
+ return new Database(dbPath, { readwrite: true, create: false });
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ function readComposerData(db, composerId) {
87
+ try {
88
+ const row = db.query("SELECT value FROM cursorDiskKV WHERE key = ?").get(`composerData:${composerId}`);
89
+ if (!row)
90
+ return null;
91
+ return JSON.parse(row.value);
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+ function readBubbleData(db, composerId, bubbleId) {
97
+ try {
98
+ const row = db.query("SELECT value FROM cursorDiskKV WHERE key = ?").get(`bubbleId:${composerId}:${bubbleId}`);
99
+ if (!row)
100
+ return null;
101
+ return JSON.parse(row.value);
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+ function readAllComposerIds(db) {
107
+ try {
108
+ const PREFIX = "composerData:";
109
+ const rows = db.query("SELECT key FROM cursorDiskKV WHERE key LIKE 'composerData:%'").all();
110
+ return rows.map((row) => row.key.slice(PREFIX.length));
111
+ } catch {
112
+ return [];
113
+ }
114
+ }
115
+ function readAllBubbleKeys(db, composerId) {
116
+ try {
117
+ const PREFIX = `bubbleId:${composerId}:`;
118
+ const rows = db.query("SELECT key FROM cursorDiskKV WHERE key LIKE ?").all(`bubbleId:${composerId}:%`);
119
+ return rows.map((row) => row.key.slice(PREFIX.length));
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+ function readWorkspaceComposerIndex(workspaceDirPath) {
125
+ const dbPath = path2.join(workspaceDirPath, "state.vscdb");
126
+ const db = openDatabase(dbPath);
127
+ if (!db)
128
+ return null;
129
+ try {
130
+ const row = db.query("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
131
+ if (!row)
132
+ return null;
133
+ return JSON.parse(row.value);
134
+ } catch {
135
+ return null;
136
+ } finally {
137
+ db.close();
138
+ }
139
+ }
140
+ async function readWorkspaceJson(workspaceDirPath) {
141
+ try {
142
+ const content = await fs2.readFile(path2.join(workspaceDirPath, "workspace.json"), "utf-8");
143
+ return JSON.parse(content);
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+ function parseWorkspaceFolderUri(folderUri) {
149
+ if (folderUri.startsWith("file://")) {
150
+ try {
151
+ return decodeURIComponent(folderUri.slice(7));
152
+ } catch {
153
+ return folderUri.slice(7);
154
+ }
155
+ }
156
+ return folderUri;
157
+ }
158
+ function resolveProviderId(modelName) {
159
+ const lower = modelName.toLowerCase();
160
+ if (lower.startsWith("claude"))
161
+ return "anthropic";
162
+ if (lower.startsWith("gpt") || lower.startsWith("o1") || lower.startsWith("o3") || lower.startsWith("o4") || lower.startsWith("chatgpt") || lower.includes("codex"))
163
+ return "openai";
164
+ if (lower.startsWith("gemini"))
165
+ return "google";
166
+ if (lower.startsWith("deepseek"))
167
+ return "deepseek";
168
+ return "cursor";
169
+ }
170
+
171
+ // src/watcher.ts
172
+ import * as fsSync from "fs";
173
+ var RECONCILIATION_INTERVAL_MS = 10 * 60 * 1000;
174
+ var sessionWatcher = {
175
+ watchers: [],
176
+ dirty: false,
177
+ reconciliationTimer: null,
178
+ started: false
179
+ };
180
+ var activityWatcher = {
181
+ watchers: [],
182
+ callback: null,
183
+ lastBubbleRowId: 0,
184
+ pendingBubbles: new Map,
185
+ pollTimer: null,
186
+ pendingPollTimer: null,
187
+ debounceTimer: null,
188
+ started: false
189
+ };
190
+ var forceFullReconciliation = false;
191
+ var ASSISTANT_BUBBLE_TYPE = 2;
192
+ function toTimestamp(isoString) {
193
+ if (!isoString)
194
+ return Date.now();
195
+ const parsed = Date.parse(isoString);
196
+ return Number.isFinite(parsed) ? parsed : Date.now();
197
+ }
198
+ function extractComposerId(key) {
199
+ const firstColon = key.indexOf(":");
200
+ const secondColon = key.indexOf(":", firstColon + 1);
201
+ return key.slice(firstColon + 1, secondColon);
202
+ }
203
+ function extractBubbleId(key) {
204
+ const firstColon = key.indexOf(":");
205
+ const secondColon = key.indexOf(":", firstColon + 1);
206
+ return key.slice(secondColon + 1);
207
+ }
208
+ function initializeRowIdCursor() {
209
+ const db = openDatabaseForWatcher(CURSOR_STATE_DB_PATH);
210
+ if (!db) {
211
+ return;
212
+ }
213
+ try {
214
+ const row = db.query("SELECT MAX(rowid) as maxRowId FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'").get();
215
+ activityWatcher.lastBubbleRowId = row?.maxRowId ?? 0;
216
+ } finally {
217
+ db.close();
218
+ }
219
+ }
220
+ function tryFireBubble(db, composerId, bubbleId, callback) {
221
+ if (!db)
222
+ return false;
223
+ const row = db.query("SELECT value FROM cursorDiskKV WHERE key = ?").get(`bubbleId:${composerId}:${bubbleId}`);
224
+ if (!row)
225
+ return false;
226
+ let bubble;
227
+ try {
228
+ bubble = JSON.parse(row.value);
229
+ } catch {
230
+ return false;
231
+ }
232
+ if (bubble.type !== ASSISTANT_BUBBLE_TYPE)
233
+ return true;
234
+ const tc = bubble.tokenCount;
235
+ const hasRealTokens = tc && typeof tc.inputTokens === "number" && tc.inputTokens > 0 || tc && typeof tc.outputTokens === "number" && tc.outputTokens > 0;
236
+ const outputTokens = hasRealTokens && tc ? tc.outputTokens ?? 0 : estimateOutputTokens(bubble.text);
237
+ const inputTokens = hasRealTokens && tc ? tc.inputTokens ?? 0 : 0;
238
+ if (inputTokens === 0 && outputTokens === 0)
239
+ return false;
240
+ const tokens = {
241
+ input: inputTokens,
242
+ output: outputTokens
243
+ };
244
+ callback({
245
+ sessionId: composerId,
246
+ messageId: typeof bubble.bubbleId === "string" ? bubble.bubbleId : bubbleId,
247
+ tokens,
248
+ timestamp: toTimestamp(bubble.createdAt)
249
+ });
250
+ return true;
251
+ }
252
+ function processDbChange() {
253
+ const callback = activityWatcher.callback;
254
+ if (!callback)
255
+ return;
256
+ const db = openDatabaseForWatcher(CURSOR_STATE_DB_PATH);
257
+ if (!db)
258
+ return;
259
+ try {
260
+ const rows = db.query("SELECT rowid, key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%' AND rowid > ? ORDER BY rowid ASC").all(activityWatcher.lastBubbleRowId);
261
+ for (const row of rows) {
262
+ activityWatcher.lastBubbleRowId = row.rowid;
263
+ sessionWatcher.dirty = true;
264
+ let bubble;
265
+ try {
266
+ bubble = JSON.parse(row.value);
267
+ } catch {
268
+ continue;
269
+ }
270
+ if (bubble.type !== ASSISTANT_BUBBLE_TYPE)
271
+ continue;
272
+ const composerId = extractComposerId(row.key);
273
+ const bubbleId = typeof bubble.bubbleId === "string" ? bubble.bubbleId : extractBubbleId(row.key);
274
+ if (!tryFireBubble(db, composerId, bubbleId, callback)) {
275
+ const pendingKey = `${composerId}:${bubbleId}`;
276
+ activityWatcher.pendingBubbles.set(pendingKey, { composerId, bubbleId });
277
+ }
278
+ }
279
+ if (activityWatcher.pendingBubbles.size > 0) {
280
+ const resolved = [];
281
+ for (const [pendingKey, { composerId, bubbleId }] of activityWatcher.pendingBubbles) {
282
+ if (tryFireBubble(db, composerId, bubbleId, callback)) {
283
+ resolved.push(pendingKey);
284
+ }
285
+ }
286
+ for (const key of resolved) {
287
+ activityWatcher.pendingBubbles.delete(key);
288
+ }
289
+ }
290
+ if (activityWatcher.pendingBubbles.size > 0 && !activityWatcher.pendingPollTimer) {
291
+ activityWatcher.pendingPollTimer = setTimeout(() => {
292
+ activityWatcher.pendingPollTimer = null;
293
+ processDbChange();
294
+ }, PENDING_POLL_MS);
295
+ } else if (activityWatcher.pendingBubbles.size === 0 && activityWatcher.pendingPollTimer) {
296
+ clearTimeout(activityWatcher.pendingPollTimer);
297
+ activityWatcher.pendingPollTimer = null;
298
+ }
299
+ } catch {} finally {
300
+ db.close();
301
+ }
302
+ }
303
+ function startSessionWatcher() {
304
+ if (sessionWatcher.started)
305
+ return;
306
+ sessionWatcher.started = true;
307
+ const pathsToWatch = [CURSOR_STATE_DB_PATH, `${CURSOR_STATE_DB_PATH}-wal`];
308
+ for (const p of pathsToWatch) {
309
+ try {
310
+ const w = fsSync.watch(p, () => {
311
+ sessionWatcher.dirty = true;
312
+ });
313
+ sessionWatcher.watchers.push(w);
314
+ } catch {}
315
+ }
316
+ sessionWatcher.reconciliationTimer = setInterval(() => {
317
+ forceFullReconciliation = true;
318
+ }, RECONCILIATION_INTERVAL_MS);
319
+ }
320
+ function consumeForceFullReconciliation() {
321
+ const value = forceFullReconciliation;
322
+ if (forceFullReconciliation) {
323
+ forceFullReconciliation = false;
324
+ }
325
+ return value;
326
+ }
327
+ var ACTIVITY_DEBOUNCE_MS = 150;
328
+ var ACTIVITY_POLL_MS = 1000;
329
+ var PENDING_POLL_MS = 500;
330
+ function scheduleProcessDbChange() {
331
+ if (activityWatcher.debounceTimer)
332
+ return;
333
+ activityWatcher.debounceTimer = setTimeout(() => {
334
+ activityWatcher.debounceTimer = null;
335
+ processDbChange();
336
+ }, ACTIVITY_DEBOUNCE_MS);
337
+ }
338
+ function startActivityWatch(callback) {
339
+ activityWatcher.callback = callback;
340
+ if (activityWatcher.started)
341
+ return;
342
+ activityWatcher.started = true;
343
+ initializeRowIdCursor();
344
+ const pathsToWatch = [CURSOR_STATE_DB_PATH, `${CURSOR_STATE_DB_PATH}-wal`];
345
+ for (const p of pathsToWatch) {
346
+ try {
347
+ const w = fsSync.watch(p, () => {
348
+ scheduleProcessDbChange();
349
+ });
350
+ activityWatcher.watchers.push(w);
351
+ } catch {}
352
+ }
353
+ activityWatcher.pollTimer = setInterval(() => {
354
+ processDbChange();
355
+ }, ACTIVITY_POLL_MS);
356
+ }
357
+ function stopActivityWatch() {
358
+ activityWatcher.callback = null;
359
+ }
360
+
361
+ // src/auth.ts
362
+ function readItemTableValue(db, key) {
363
+ try {
364
+ const row = db.query("SELECT value FROM ItemTable WHERE key = ?").get(key);
365
+ return row?.value ?? null;
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+ function decodeJwtPayload(jwt) {
371
+ try {
372
+ const parts = jwt.split(".");
373
+ if (parts.length !== 3)
374
+ return null;
375
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
376
+ const json = atob(base64);
377
+ return JSON.parse(json);
378
+ } catch {
379
+ return null;
380
+ }
381
+ }
382
+ function buildSessionCookie() {
383
+ const db = openDatabase(CURSOR_STATE_DB_PATH);
384
+ if (!db)
385
+ return null;
386
+ try {
387
+ const accessToken = readItemTableValue(db, "cursorAuth/accessToken");
388
+ if (!accessToken)
389
+ return null;
390
+ const payload = decodeJwtPayload(accessToken);
391
+ if (!payload || typeof payload.sub !== "string")
392
+ return null;
393
+ const userId = payload.sub;
394
+ return `WorkosCursorSessionToken=${userId}%3A%3A${accessToken}`;
395
+ } finally {
396
+ db.close();
397
+ }
398
+ }
399
+
400
+ // src/storage.ts
401
+ var pluginStorage = null;
402
+ var ENRICHMENT_CACHE_KEY = "enrichment-cache";
403
+ function setPluginStorage(storage) {
404
+ pluginStorage = storage;
405
+ }
406
+ async function loadEnrichmentCache() {
407
+ if (!pluginStorage)
408
+ return null;
409
+ try {
410
+ const raw = await pluginStorage.get(ENRICHMENT_CACHE_KEY);
411
+ if (!raw)
412
+ return null;
413
+ const parsed = JSON.parse(raw);
414
+ if (!parsed.sessions || typeof parsed.sessions !== "object")
415
+ return null;
416
+ const map = new Map;
417
+ for (const [sessionId, rows] of Object.entries(parsed.sessions)) {
418
+ if (Array.isArray(rows) && rows.length > 0) {
419
+ map.set(sessionId, rows);
420
+ }
421
+ }
422
+ return map.size > 0 ? map : null;
423
+ } catch {
424
+ return null;
425
+ }
426
+ }
427
+ function saveEnrichmentCache(cache) {
428
+ if (!pluginStorage || cache.size === 0)
429
+ return;
430
+ const payload = {
431
+ savedAt: new Date().toISOString(),
432
+ sessions: Object.fromEntries(cache)
433
+ };
434
+ pluginStorage.set(ENRICHMENT_CACHE_KEY, JSON.stringify(payload)).catch(() => {});
435
+ }
436
+
437
+ // src/csv.ts
438
+ var USAGE_CSV_URL = "https://cursor.com/api/dashboard/export-usage-events-csv?strategy=tokens";
439
+ var CSV_CACHE_TTL_MS = 5 * 60 * 1000;
440
+ var MATCH_WINDOW_MS = 60000;
441
+ var csvCache = null;
442
+ var csvFetchInFlight = null;
443
+ var sessionEnrichmentCache = new Map;
444
+ var enrichmentCacheHydrated = false;
445
+ function parseUsageCsv(csvText) {
446
+ const lines = csvText.trim().split(`
447
+ `);
448
+ if (lines.length < 2)
449
+ return [];
450
+ const rows = [];
451
+ for (let i = 1;i < lines.length; i++) {
452
+ const line = lines[i].trim();
453
+ if (!line)
454
+ continue;
455
+ const cols = line.split(",").map((c) => c.replace(/^"|"$/g, ""));
456
+ if (cols.length < 10)
457
+ continue;
458
+ const timestamp = Date.parse(cols[0]);
459
+ if (!Number.isFinite(timestamp))
460
+ continue;
461
+ const outputTokens = parseInt(cols[7], 10);
462
+ if (!Number.isFinite(outputTokens) || outputTokens <= 0)
463
+ continue;
464
+ rows.push({
465
+ timestamp,
466
+ kind: cols[1].trim(),
467
+ model: cols[2].trim(),
468
+ maxMode: cols[3].trim().toLowerCase() === "true",
469
+ inputTokensWithCacheWrite: parseInt(cols[4], 10) || 0,
470
+ inputTokensWithoutCacheWrite: parseInt(cols[5], 10) || 0,
471
+ cacheReadTokens: parseInt(cols[6], 10) || 0,
472
+ outputTokens,
473
+ totalTokens: parseInt(cols[8], 10) || 0,
474
+ cost: parseFloat(cols[9]) || 0
475
+ });
476
+ }
477
+ return rows;
478
+ }
479
+ async function fetchCsvFromServer(ctx) {
480
+ const cookie = buildSessionCookie();
481
+ if (!cookie) {
482
+ ctx.logger.debug("Cursor CSV: no auth token available");
483
+ return [];
484
+ }
485
+ const response = await ctx.http.fetch(USAGE_CSV_URL, {
486
+ headers: { Cookie: cookie },
487
+ signal: ctx.signal
488
+ });
489
+ if (!response.ok) {
490
+ ctx.logger.debug("Cursor CSV: fetch failed", { status: response.status });
491
+ return [];
492
+ }
493
+ const csvText = await response.text();
494
+ return parseUsageCsv(csvText);
495
+ }
496
+ async function getCsvRows(ctx) {
497
+ if (ctx.config.csvEnrichment === false)
498
+ return [];
499
+ const refreshMinutes = typeof ctx.config.csvRefreshMinutes === "number" ? ctx.config.csvRefreshMinutes : 5;
500
+ const ttlMs = refreshMinutes * 60 * 1000;
501
+ const now = Date.now();
502
+ if (csvCache && now - csvCache.fetchedAt < ttlMs) {
503
+ return csvCache.rows;
504
+ }
505
+ if (!csvFetchInFlight) {
506
+ csvFetchInFlight = fetchCsvFromServer(ctx).then((rows) => {
507
+ csvCache = { rows, fetchedAt: Date.now() };
508
+ ctx.logger.debug("Cursor CSV: cached", { rowCount: rows.length });
509
+ }).catch((err) => {
510
+ ctx.logger.debug("Cursor CSV: fetch error", { error: String(err) });
511
+ }).finally(() => {
512
+ csvFetchInFlight = null;
513
+ });
514
+ }
515
+ if (csvFetchInFlight) {
516
+ await csvFetchInFlight;
517
+ }
518
+ return csvCache?.rows ?? [];
519
+ }
520
+ async function hydrateEnrichmentCache() {
521
+ if (enrichmentCacheHydrated)
522
+ return;
523
+ enrichmentCacheHydrated = true;
524
+ const persisted = await loadEnrichmentCache();
525
+ if (!persisted)
526
+ return;
527
+ for (const [sessionId, rows] of persisted) {
528
+ if (!sessionEnrichmentCache.has(sessionId)) {
529
+ sessionEnrichmentCache.set(sessionId, rows);
530
+ }
531
+ }
532
+ }
533
+ async function enrichWithCsvData(localRows, csvRows) {
534
+ await hydrateEnrichmentCache();
535
+ if (csvRows.length === 0) {
536
+ if (sessionEnrichmentCache.size === 0)
537
+ return localRows;
538
+ return applyEnrichmentCache(localRows);
539
+ }
540
+ const sessionGroups = new Map;
541
+ for (const row of localRows) {
542
+ const existing = sessionGroups.get(row.sessionId);
543
+ if (existing) {
544
+ existing.rows.push(row);
545
+ } else {
546
+ sessionGroups.set(row.sessionId, { rows: [row] });
547
+ }
548
+ }
549
+ const sortedCsv = [...csvRows].sort((a, b) => a.timestamp - b.timestamp);
550
+ const usedCsvIndices = new Set;
551
+ const sessionCsvMap = new Map;
552
+ for (const [sessionId, group] of sessionGroups) {
553
+ const updatedAt = group.rows.reduce((max, r) => {
554
+ const ts = r.sessionUpdatedAt ?? r.timestamp;
555
+ return ts > max ? ts : max;
556
+ }, 0);
557
+ let bestIdx = -1;
558
+ let bestDelta = Infinity;
559
+ for (let i = 0;i < sortedCsv.length; i++) {
560
+ if (usedCsvIndices.has(i))
561
+ continue;
562
+ const delta = Math.abs(sortedCsv[i].timestamp - updatedAt);
563
+ if (delta > MATCH_WINDOW_MS)
564
+ continue;
565
+ if (delta < bestDelta) {
566
+ bestDelta = delta;
567
+ bestIdx = i;
568
+ }
569
+ }
570
+ if (bestIdx !== -1) {
571
+ usedCsvIndices.add(bestIdx);
572
+ sessionCsvMap.set(sessionId, sortedCsv[bestIdx]);
573
+ }
574
+ }
575
+ const enrichedRows = localRows.map((row) => {
576
+ const csv = sessionCsvMap.get(row.sessionId);
577
+ if (!csv)
578
+ return row;
579
+ const group = sessionGroups.get(row.sessionId);
580
+ const totalEstOutput = group.rows.reduce((sum, r) => sum + r.tokens.output, 0);
581
+ const weight = totalEstOutput > 0 ? row.tokens.output / totalEstOutput : 1 / group.rows.length;
582
+ const cacheWrite = csv.inputTokensWithCacheWrite - csv.inputTokensWithoutCacheWrite;
583
+ return {
584
+ ...row,
585
+ tokens: {
586
+ input: Math.round(csv.inputTokensWithoutCacheWrite * weight),
587
+ output: Math.round(csv.outputTokens * weight),
588
+ ...csv.cacheReadTokens > 0 ? { cacheRead: Math.round(csv.cacheReadTokens * weight) } : {},
589
+ ...cacheWrite > 0 ? { cacheWrite: Math.round(cacheWrite * weight) } : {}
590
+ },
591
+ ...csv.cost > 0 ? { cost: Number((csv.cost * weight).toFixed(6)) } : {},
592
+ metadata: { isEstimated: false }
593
+ };
594
+ });
595
+ let cacheUpdated = false;
596
+ for (const [sessionId] of sessionCsvMap) {
597
+ const sessionRows = enrichedRows.filter((r) => r.sessionId === sessionId);
598
+ if (sessionRows.length > 0) {
599
+ sessionEnrichmentCache.set(sessionId, sessionRows);
600
+ cacheUpdated = true;
601
+ }
602
+ }
603
+ if (cacheUpdated) {
604
+ saveEnrichmentCache(sessionEnrichmentCache);
605
+ }
606
+ if (sessionCsvMap.size < sessionGroups.size && sessionEnrichmentCache.size > 0) {
607
+ return enrichedRows.map((row) => {
608
+ if (sessionCsvMap.has(row.sessionId))
609
+ return row;
610
+ return applyCachedEnrichmentToRow(row) ?? row;
611
+ });
612
+ }
613
+ return enrichedRows;
614
+ }
615
+ function applyEnrichmentCache(localRows) {
616
+ return localRows.map((row) => applyCachedEnrichmentToRow(row) ?? row);
617
+ }
618
+ function applyCachedEnrichmentToRow(row) {
619
+ const cachedRows = sessionEnrichmentCache.get(row.sessionId);
620
+ if (!cachedRows)
621
+ return null;
622
+ const match = cachedRows.find((c) => c.timestamp === row.timestamp);
623
+ if (!match)
624
+ return null;
625
+ return {
626
+ ...row,
627
+ tokens: match.tokens,
628
+ ...match.cost !== undefined ? { cost: match.cost } : {},
629
+ metadata: { isEstimated: false }
630
+ };
631
+ }
632
+ function resetCsvCache() {
633
+ csvCache = null;
634
+ csvFetchInFlight = null;
635
+ sessionEnrichmentCache.clear();
636
+ }
637
+
638
+ // src/parser.ts
639
+ var ASSISTANT_BUBBLE_TYPE2 = 2;
640
+ var ESTIMATED_CHARS_PER_TOKEN = 4;
641
+ function toTimestamp2(isoString, fallback) {
642
+ if (!isoString)
643
+ return fallback;
644
+ const parsed = Date.parse(isoString);
645
+ return Number.isFinite(parsed) ? parsed : fallback;
646
+ }
647
+ function isAssistantBubble(bubble) {
648
+ if (!bubble || typeof bubble !== "object")
649
+ return false;
650
+ const candidate = bubble;
651
+ if (candidate.type !== ASSISTANT_BUBBLE_TYPE2)
652
+ return false;
653
+ if (typeof candidate.bubbleId !== "string" || candidate.bubbleId.length === 0)
654
+ return false;
655
+ const hasTokens = candidate.tokenCount && typeof candidate.tokenCount === "object" && typeof candidate.tokenCount.inputTokens === "number" && typeof candidate.tokenCount.outputTokens === "number";
656
+ const hasText = typeof candidate.text === "string" && candidate.text.length > 0;
657
+ return hasTokens || hasText;
658
+ }
659
+ function estimateOutputTokens(text) {
660
+ if (!text || text.length === 0)
661
+ return 0;
662
+ return Math.ceil(text.length / ESTIMATED_CHARS_PER_TOKEN);
663
+ }
664
+ function resolveModelName(bubble, composer) {
665
+ const bubbleModel = bubble.modelInfo?.modelName;
666
+ if (bubbleModel && bubbleModel !== "default" && bubbleModel !== "?") {
667
+ return bubbleModel;
668
+ }
669
+ const composerModel = composer.modelConfig?.modelName;
670
+ if (composerModel && composerModel !== "default") {
671
+ return composerModel;
672
+ }
673
+ return "cursor-default";
674
+ }
675
+ function parseComposerBubbles(db, meta, composer, shouldEstimate = true) {
676
+ const deduped = new Map;
677
+ const bubbleIds = readAllBubbleKeys(db, meta.composerId);
678
+ for (const bubbleId of bubbleIds) {
679
+ const bubble = readBubbleData(db, meta.composerId, bubbleId);
680
+ if (!isAssistantBubble(bubble))
681
+ continue;
682
+ const modelId = resolveModelName(bubble, composer);
683
+ const providerId = resolveProviderId(modelId);
684
+ const timestamp = toTimestamp2(bubble.createdAt, meta.lastUpdatedAt);
685
+ const hasRealTokens = bubble.tokenCount.inputTokens > 0 || bubble.tokenCount.outputTokens > 0;
686
+ const outputTokens = hasRealTokens ? bubble.tokenCount.outputTokens : shouldEstimate ? estimateOutputTokens(bubble.text) : 0;
687
+ const inputTokens = hasRealTokens ? bubble.tokenCount.inputTokens : 0;
688
+ if (inputTokens === 0 && outputTokens === 0)
689
+ continue;
690
+ const usage = {
691
+ sessionId: meta.composerId,
692
+ providerId,
693
+ modelId,
694
+ tokens: {
695
+ input: inputTokens,
696
+ output: outputTokens
697
+ },
698
+ timestamp,
699
+ metadata: { isEstimated: true },
700
+ sessionUpdatedAt: meta.lastUpdatedAt
701
+ };
702
+ if (meta.sessionName) {
703
+ usage.sessionName = meta.sessionName;
704
+ }
705
+ if (meta.projectPath) {
706
+ usage.projectPath = meta.projectPath;
707
+ }
708
+ deduped.set(bubble.bubbleId, usage);
709
+ }
710
+ return Array.from(deduped.values());
711
+ }
712
+ async function buildWorkspaceMap() {
713
+ const workspaceDirs = await getWorkspaceDirs();
714
+ const composerToWorkspace = new Map;
715
+ for (const wsDir of workspaceDirs) {
716
+ const workspaceJson = await readWorkspaceJson(wsDir);
717
+ if (!workspaceJson?.folder)
718
+ continue;
719
+ const projectPath = parseWorkspaceFolderUri(workspaceJson.folder);
720
+ const workspaceHash = wsDir.split("/").pop() ?? wsDir.split("\\").pop() ?? "";
721
+ const index = readWorkspaceComposerIndex(wsDir);
722
+ if (!index?.allComposers)
723
+ continue;
724
+ const composerIds = index.allComposers.map((c) => c.composerId);
725
+ const info = {
726
+ workspaceHash,
727
+ projectPath,
728
+ composerIds
729
+ };
730
+ for (const cid of composerIds) {
731
+ composerToWorkspace.set(cid, info);
732
+ }
733
+ }
734
+ return composerToWorkspace;
735
+ }
736
+ async function parseSessionsFromWorkspaces(options, ctx) {
737
+ const limit = options.limit ?? 100;
738
+ const since = options.since;
739
+ try {
740
+ await fs3.access(CURSOR_STATE_DB_PATH);
741
+ } catch {
742
+ ctx.logger.debug("No Cursor state database found");
743
+ return [];
744
+ }
745
+ startSessionWatcher();
746
+ const now = Date.now();
747
+ if (!options.sessionId && limit === sessionCache.lastLimit && now - sessionCache.lastCheck < CACHE_TTL_MS && sessionCache.lastResult.length > 0 && sessionCache.lastSince === since) {
748
+ ctx.logger.debug("Cursor: using cached sessions (within TTL)", { count: sessionCache.lastResult.length });
749
+ return sessionCache.lastResult;
750
+ }
751
+ const isDirty = sessionWatcher.dirty;
752
+ sessionWatcher.dirty = false;
753
+ const needsFullStat = consumeForceFullReconciliation();
754
+ if (needsFullStat) {
755
+ ctx.logger.debug("Cursor: full reconciliation sweep triggered");
756
+ }
757
+ const db = openDatabase(CURSOR_STATE_DB_PATH);
758
+ if (!db) {
759
+ ctx.logger.debug("Cursor: failed to open state database");
760
+ return [];
761
+ }
762
+ try {
763
+ const composerToWorkspace = await buildWorkspaceMap();
764
+ const composerIds = options.sessionId ? [options.sessionId] : readAllComposerIds(db);
765
+ const composers = [];
766
+ const seenComposerIds = new Set;
767
+ let statCount = 0;
768
+ let statSkipCount = 0;
769
+ for (const composerId of composerIds) {
770
+ seenComposerIds.add(composerId);
771
+ const composerData = readComposerData(db, composerId);
772
+ if (!composerData)
773
+ continue;
774
+ const lastUpdatedAt = composerData.lastUpdatedAt || composerData.createdAt || 0;
775
+ const metadata = composerMetadataIndex.get(composerId);
776
+ if (!isDirty && !needsFullStat && metadata && metadata.lastUpdatedAt === lastUpdatedAt) {
777
+ statSkipCount++;
778
+ if (!since || lastUpdatedAt >= since) {
779
+ const wsInfo2 = composerToWorkspace.get(composerId);
780
+ composers.push({
781
+ composerId,
782
+ lastUpdatedAt,
783
+ projectPath: wsInfo2?.projectPath,
784
+ sessionName: composerData.name
785
+ });
786
+ }
787
+ continue;
788
+ }
789
+ statCount++;
790
+ composerMetadataIndex.set(composerId, { lastUpdatedAt, composerId });
791
+ if (since && lastUpdatedAt < since)
792
+ continue;
793
+ const wsInfo = composerToWorkspace.get(composerId);
794
+ composers.push({
795
+ composerId,
796
+ lastUpdatedAt,
797
+ projectPath: wsInfo?.projectPath,
798
+ sessionName: composerData.name
799
+ });
800
+ }
801
+ for (const cachedId of composerMetadataIndex.keys()) {
802
+ if (!seenComposerIds.has(cachedId)) {
803
+ composerMetadataIndex.delete(cachedId);
804
+ }
805
+ }
806
+ composers.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
807
+ const sessions = [];
808
+ let aggregateCacheHits = 0;
809
+ let aggregateCacheMisses = 0;
810
+ for (const meta of composers) {
811
+ const cached = sessionAggregateCache.get(meta.composerId);
812
+ const cacheStale = isDirty || needsFullStat || cached !== undefined && cached.usageRows.length === 0;
813
+ if (!cacheStale && cached && cached.updatedAt === meta.lastUpdatedAt) {
814
+ cached.lastAccessed = now;
815
+ aggregateCacheHits++;
816
+ sessions.push(...cached.usageRows);
817
+ continue;
818
+ }
819
+ aggregateCacheMisses++;
820
+ const composerData = readComposerData(db, meta.composerId);
821
+ if (!composerData)
822
+ continue;
823
+ const shouldEstimate = ctx.config.estimateTokens !== false;
824
+ const usageRows = parseComposerBubbles(db, meta, composerData, shouldEstimate);
825
+ sessionAggregateCache.set(meta.composerId, {
826
+ updatedAt: meta.lastUpdatedAt,
827
+ usageRows,
828
+ lastAccessed: now
829
+ });
830
+ sessions.push(...usageRows);
831
+ }
832
+ evictSessionAggregateCache();
833
+ const csvRows = await getCsvRows(ctx);
834
+ const enrichedSessions = await enrichWithCsvData(sessions, csvRows);
835
+ if (!options.sessionId) {
836
+ sessionCache.lastCheck = Date.now();
837
+ sessionCache.lastResult = enrichedSessions;
838
+ sessionCache.lastLimit = limit;
839
+ sessionCache.lastSince = since;
840
+ }
841
+ ctx.logger.debug("Cursor: parsed sessions", {
842
+ count: enrichedSessions.length,
843
+ composers: composers.length,
844
+ statChecks: statCount,
845
+ statSkips: statSkipCount,
846
+ aggregateCacheHits,
847
+ aggregateCacheMisses,
848
+ metadataIndexSize: composerMetadataIndex.size,
849
+ aggregateCacheSize: sessionAggregateCache.size,
850
+ csvEnriched: csvRows.length > 0
851
+ });
852
+ return enrichedSessions;
853
+ } finally {
854
+ db.close();
855
+ }
856
+ }
857
+
858
+ // src/index.ts
8
859
  var cursorAgentPlugin = createAgentPlugin({
9
860
  id: "cursor",
10
861
  type: "agent",
@@ -17,29 +868,80 @@ var cursorAgentPlugin = createAgentPlugin({
17
868
  permissions: {
18
869
  filesystem: {
19
870
  read: true,
20
- paths: ["~/.cursor"]
871
+ paths: ["~/.cursor", "~/Library/Application Support/Cursor", "~/.config/Cursor"]
872
+ },
873
+ network: {
874
+ enabled: true,
875
+ allowedDomains: ["cursor.com"]
21
876
  }
22
877
  },
23
878
  agent: {
24
879
  name: "Cursor",
25
880
  command: "cursor",
26
- configPath: path.join(os.homedir(), ".cursor"),
27
- sessionPath: path.join(os.homedir(), ".cursor")
881
+ configPath: CURSOR_HOME,
882
+ sessionPath: CURSOR_GLOBAL_STORAGE_PATH
28
883
  },
29
884
  capabilities: {
30
- sessionParsing: false,
885
+ sessionParsing: true,
31
886
  authReading: false,
32
- realTimeTracking: false,
887
+ realTimeTracking: true,
33
888
  multiProvider: false
34
889
  },
35
- async isInstalled(_ctx) {
36
- return fs.existsSync(path.join(os.homedir(), ".cursor"));
890
+ configSchema: {
891
+ csvEnrichment: {
892
+ type: "boolean",
893
+ label: "Server enrichment",
894
+ description: "Fetch accurate token counts and costs from Cursor's server. When enabled, local estimates are replaced with exact data on subsequent refreshes.",
895
+ default: true
896
+ },
897
+ csvRefreshMinutes: {
898
+ type: "number",
899
+ label: "Server refresh interval",
900
+ description: "How often to fetch updated token data from the server (minutes).",
901
+ default: 5,
902
+ min: 1,
903
+ max: 60
904
+ },
905
+ estimateTokens: {
906
+ type: "boolean",
907
+ label: "Estimate tokens",
908
+ description: "Estimate output tokens from response text when exact server counts are unavailable. Provides immediate visibility while server data loads.",
909
+ default: true
910
+ }
911
+ },
912
+ defaultConfig: {
913
+ csvEnrichment: true,
914
+ csvRefreshMinutes: 5,
915
+ estimateTokens: true
37
916
  },
38
- async parseSessions(_options, _ctx) {
39
- return [];
917
+ startActivityWatch(ctx, callback) {
918
+ setPluginStorage(ctx.storage);
919
+ startActivityWatch(callback);
920
+ },
921
+ stopActivityWatch(_ctx) {
922
+ stopActivityWatch();
923
+ },
924
+ async isInstalled(ctx) {
925
+ setPluginStorage(ctx.storage);
926
+ return fs4.existsSync(CURSOR_STATE_DB_PATH) || fs4.existsSync(CURSOR_HOME);
927
+ },
928
+ async parseSessions(options, ctx) {
929
+ return parseSessionsFromWorkspaces(options, ctx);
40
930
  }
41
931
  });
42
932
  var src_default = cursorAgentPlugin;
43
933
  export {
44
- src_default as default
934
+ sessionCache,
935
+ sessionAggregateCache,
936
+ resetCsvCache,
937
+ src_default as default,
938
+ composerMetadataIndex,
939
+ SESSION_AGGREGATE_CACHE_MAX,
940
+ RECONCILIATION_INTERVAL_MS,
941
+ CURSOR_WORKSPACE_STORAGE_PATH,
942
+ CURSOR_STATE_DB_PATH,
943
+ CURSOR_HOME,
944
+ CURSOR_GLOBAL_STORAGE_PATH,
945
+ CSV_CACHE_TTL_MS,
946
+ CACHE_TTL_MS
45
947
  };