@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/README.md +92 -0
- package/dist/auth.d.ts +11 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/cache.d.ts +17 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/csv.d.ts +49 -0
- package/dist/csv.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +915 -13
- package/dist/parser.d.ts +33 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/paths.d.ts +6 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/storage.d.ts +15 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/watcher.d.ts +17 -0
- package/dist/watcher.d.ts.map +1 -0
- package/package.json +3 -4
package/dist/index.js
CHANGED
|
@@ -1,10 +1,861 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import * as
|
|
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:
|
|
27
|
-
sessionPath:
|
|
881
|
+
configPath: CURSOR_HOME,
|
|
882
|
+
sessionPath: CURSOR_GLOBAL_STORAGE_PATH
|
|
28
883
|
},
|
|
29
884
|
capabilities: {
|
|
30
|
-
sessionParsing:
|
|
885
|
+
sessionParsing: true,
|
|
31
886
|
authReading: false,
|
|
32
|
-
realTimeTracking:
|
|
887
|
+
realTimeTracking: true,
|
|
33
888
|
multiProvider: false
|
|
34
889
|
},
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
};
|