@tokscale/cli 1.0.23 → 1.1.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/cli.js +127 -41
- package/dist/cli.js.map +1 -1
- package/dist/cursor.d.ts +38 -3
- package/dist/cursor.d.ts.map +1 -1
- package/dist/cursor.js +520 -46
- package/dist/cursor.js.map +1 -1
- package/dist/submit.d.ts.map +1 -1
- package/dist/submit.js +53 -26
- package/dist/submit.js.map +1 -1
- package/dist/tui/hooks/useData.d.ts.map +1 -1
- package/dist/tui/hooks/useData.js +9 -4
- package/dist/tui/hooks/useData.js.map +1 -1
- package/dist/wrapped.d.ts.map +1 -1
- package/dist/wrapped.js +8 -4
- package/dist/wrapped.js.map +1 -1
- package/package.json +2 -5
- package/src/cli.ts +143 -43
- package/src/cursor.ts +537 -51
- package/src/submit.ts +60 -28
- package/src/tui/hooks/useData.ts +10 -4
- package/src/wrapped.ts +9 -4
package/dist/cursor.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import * as os from "node:os";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
14
15
|
import { parse as parseCsv } from "csv-parse/sync";
|
|
15
16
|
// ============================================================================
|
|
16
17
|
// Credential Management
|
|
@@ -50,31 +51,384 @@ function migrateCursorFromOldPath() {
|
|
|
50
51
|
// Migration failed - continue with normal operation
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
export function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
54
|
+
export function ensureCursorMigration() {
|
|
55
|
+
// Best-effort: never throw
|
|
56
|
+
try {
|
|
57
|
+
migrateCursorFromOldPath();
|
|
58
|
+
}
|
|
59
|
+
catch { }
|
|
60
|
+
try {
|
|
61
|
+
migrateCursorCacheFromOldPath();
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
try {
|
|
65
|
+
// Triggers legacy schema -> v1 store migration if needed
|
|
66
|
+
loadCursorCredentialsStoreInternal();
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
59
69
|
}
|
|
60
|
-
|
|
70
|
+
function isStoreV1(data) {
|
|
71
|
+
if (!data || typeof data !== "object")
|
|
72
|
+
return false;
|
|
73
|
+
const obj = data;
|
|
74
|
+
return obj.version === 1 && typeof obj.activeAccountId === "string" && typeof obj.accounts === "object" && obj.accounts !== null;
|
|
75
|
+
}
|
|
76
|
+
function extractUserIdFromSessionToken(sessionToken) {
|
|
77
|
+
if (!sessionToken)
|
|
78
|
+
return null;
|
|
79
|
+
const token = sessionToken.trim();
|
|
80
|
+
if (token.includes("%3A%3A")) {
|
|
81
|
+
const userId = token.split("%3A%3A")[0]?.trim();
|
|
82
|
+
return userId ? userId : null;
|
|
83
|
+
}
|
|
84
|
+
if (token.includes("::")) {
|
|
85
|
+
const userId = token.split("::")[0]?.trim();
|
|
86
|
+
return userId ? userId : null;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
function sanitizeAccountIdForFilename(accountId) {
|
|
91
|
+
return accountId
|
|
92
|
+
.trim()
|
|
93
|
+
.toLowerCase()
|
|
94
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
95
|
+
.replace(/^-+|-+$/g, "")
|
|
96
|
+
.slice(0, 80) || "account";
|
|
97
|
+
}
|
|
98
|
+
function isCursorUsageCsvFilename(fileName) {
|
|
99
|
+
if (fileName === "usage.csv")
|
|
100
|
+
return true;
|
|
101
|
+
if (!fileName.startsWith("usage."))
|
|
102
|
+
return false;
|
|
103
|
+
if (!fileName.endsWith(".csv"))
|
|
104
|
+
return false;
|
|
105
|
+
// Exclude legacy backups (were previously written as usage.backup-<ts>.csv)
|
|
106
|
+
if (fileName.startsWith("usage.backup"))
|
|
107
|
+
return false;
|
|
108
|
+
const stem = fileName.slice("usage.".length, -".csv".length);
|
|
109
|
+
if (!stem)
|
|
110
|
+
return false;
|
|
111
|
+
return /^[a-z0-9._-]+$/i.test(stem);
|
|
112
|
+
}
|
|
113
|
+
function deriveAccountId(sessionToken) {
|
|
114
|
+
const userId = extractUserIdFromSessionToken(sessionToken);
|
|
115
|
+
if (userId)
|
|
116
|
+
return userId;
|
|
117
|
+
const hash = createHash("sha256").update(sessionToken).digest("hex").slice(0, 12);
|
|
118
|
+
return `anon-${hash}`;
|
|
119
|
+
}
|
|
120
|
+
function atomicWriteFile(filePath, data, mode) {
|
|
121
|
+
const dir = path.dirname(filePath);
|
|
122
|
+
const base = path.basename(filePath);
|
|
123
|
+
const tmp = path.join(dir, `.${base}.tmp-${process.pid}`);
|
|
124
|
+
fs.writeFileSync(tmp, data, { encoding: "utf-8", mode });
|
|
125
|
+
try {
|
|
126
|
+
fs.renameSync(tmp, filePath);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Best-effort for platforms where rename over an existing file can fail.
|
|
130
|
+
try {
|
|
131
|
+
if (fs.existsSync(filePath))
|
|
132
|
+
fs.rmSync(filePath);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// ignore
|
|
136
|
+
}
|
|
137
|
+
fs.renameSync(tmp, filePath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function loadCursorCredentialsStoreInternal() {
|
|
61
141
|
migrateCursorFromOldPath();
|
|
62
142
|
try {
|
|
63
|
-
if (!fs.existsSync(CURSOR_CREDENTIALS_FILE))
|
|
143
|
+
if (!fs.existsSync(CURSOR_CREDENTIALS_FILE))
|
|
64
144
|
return null;
|
|
65
|
-
}
|
|
66
145
|
const data = fs.readFileSync(CURSOR_CREDENTIALS_FILE, "utf-8");
|
|
67
146
|
const parsed = JSON.parse(data);
|
|
68
|
-
if (
|
|
69
|
-
|
|
147
|
+
if (isStoreV1(parsed)) {
|
|
148
|
+
const store = parsed;
|
|
149
|
+
if (!store.activeAccountId || !store.accounts[store.activeAccountId]) {
|
|
150
|
+
const firstId = Object.keys(store.accounts)[0];
|
|
151
|
+
if (!firstId)
|
|
152
|
+
return null;
|
|
153
|
+
store.activeAccountId = firstId;
|
|
154
|
+
ensureConfigDir();
|
|
155
|
+
atomicWriteFile(CURSOR_CREDENTIALS_FILE, JSON.stringify(store, null, 2), 0o600);
|
|
156
|
+
}
|
|
157
|
+
return store;
|
|
158
|
+
}
|
|
159
|
+
// Legacy single-account schema: { sessionToken, createdAt, ... }
|
|
160
|
+
if (parsed && typeof parsed === "object") {
|
|
161
|
+
const obj = parsed;
|
|
162
|
+
const sessionToken = typeof obj.sessionToken === "string" ? obj.sessionToken : "";
|
|
163
|
+
if (!sessionToken)
|
|
164
|
+
return null;
|
|
165
|
+
const accountId = deriveAccountId(sessionToken);
|
|
166
|
+
const migrated = {
|
|
167
|
+
version: 1,
|
|
168
|
+
activeAccountId: accountId,
|
|
169
|
+
accounts: {
|
|
170
|
+
[accountId]: {
|
|
171
|
+
sessionToken,
|
|
172
|
+
userId: typeof obj.userId === "string" ? obj.userId : extractUserIdFromSessionToken(sessionToken) || undefined,
|
|
173
|
+
createdAt: typeof obj.createdAt === "string" ? obj.createdAt : new Date().toISOString(),
|
|
174
|
+
expiresAt: typeof obj.expiresAt === "string" ? obj.expiresAt : undefined,
|
|
175
|
+
label: typeof obj.label === "string" ? obj.label : undefined,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
ensureConfigDir();
|
|
180
|
+
atomicWriteFile(CURSOR_CREDENTIALS_FILE, JSON.stringify(migrated, null, 2), 0o600);
|
|
181
|
+
return migrated;
|
|
70
182
|
}
|
|
71
|
-
return
|
|
183
|
+
return null;
|
|
72
184
|
}
|
|
73
185
|
catch {
|
|
74
186
|
return null;
|
|
75
187
|
}
|
|
76
188
|
}
|
|
189
|
+
function saveCursorCredentialsStoreInternal(store) {
|
|
190
|
+
ensureConfigDir();
|
|
191
|
+
atomicWriteFile(CURSOR_CREDENTIALS_FILE, JSON.stringify(store, null, 2), 0o600);
|
|
192
|
+
}
|
|
193
|
+
function resolveAccountId(store, nameOrId) {
|
|
194
|
+
const needle = nameOrId.trim();
|
|
195
|
+
if (!needle)
|
|
196
|
+
return null;
|
|
197
|
+
if (store.accounts[needle])
|
|
198
|
+
return needle;
|
|
199
|
+
const needleLower = needle.toLowerCase();
|
|
200
|
+
for (const [id, acct] of Object.entries(store.accounts)) {
|
|
201
|
+
if (acct.label && acct.label.toLowerCase() === needleLower)
|
|
202
|
+
return id;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
export function listCursorAccounts() {
|
|
207
|
+
const store = loadCursorCredentialsStoreInternal();
|
|
208
|
+
if (!store)
|
|
209
|
+
return [];
|
|
210
|
+
const accounts = Object.entries(store.accounts).map(([id, acct]) => ({
|
|
211
|
+
id,
|
|
212
|
+
label: acct.label,
|
|
213
|
+
userId: acct.userId,
|
|
214
|
+
createdAt: acct.createdAt,
|
|
215
|
+
isActive: id === store.activeAccountId,
|
|
216
|
+
}));
|
|
217
|
+
accounts.sort((a, b) => {
|
|
218
|
+
if (a.isActive !== b.isActive)
|
|
219
|
+
return a.isActive ? -1 : 1;
|
|
220
|
+
const la = (a.label || a.id).toLowerCase();
|
|
221
|
+
const lb = (b.label || b.id).toLowerCase();
|
|
222
|
+
return la.localeCompare(lb);
|
|
223
|
+
});
|
|
224
|
+
return accounts;
|
|
225
|
+
}
|
|
226
|
+
export function setActiveCursorAccount(nameOrId) {
|
|
227
|
+
const store = loadCursorCredentialsStoreInternal();
|
|
228
|
+
if (!store)
|
|
229
|
+
return { ok: false, error: "Not authenticated" };
|
|
230
|
+
const resolved = resolveAccountId(store, nameOrId);
|
|
231
|
+
if (!resolved)
|
|
232
|
+
return { ok: false, error: `Account not found: ${nameOrId}` };
|
|
233
|
+
const prev = store.activeAccountId;
|
|
234
|
+
store.activeAccountId = resolved;
|
|
235
|
+
saveCursorCredentialsStoreInternal(store);
|
|
236
|
+
// Best-effort cache reconcile (avoid double-counting)
|
|
237
|
+
try {
|
|
238
|
+
migrateCursorCacheFromOldPath();
|
|
239
|
+
ensureCacheDir();
|
|
240
|
+
const archiveDir = path.join(CURSOR_CACHE_DIR, "archive");
|
|
241
|
+
const ensureArchiveDir = () => {
|
|
242
|
+
ensureCacheDir();
|
|
243
|
+
if (!fs.existsSync(archiveDir)) {
|
|
244
|
+
fs.mkdirSync(archiveDir, { recursive: true, mode: 0o700 });
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
const archiveFile = (filePath, label) => {
|
|
248
|
+
ensureArchiveDir();
|
|
249
|
+
const safeLabel = sanitizeAccountIdForFilename(label);
|
|
250
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
251
|
+
const dest = path.join(archiveDir, `${safeLabel}-${ts}.csv`);
|
|
252
|
+
fs.renameSync(filePath, dest);
|
|
253
|
+
};
|
|
254
|
+
// Move current active cache to previous account file (preserve any existing file by archiving).
|
|
255
|
+
if (prev && fs.existsSync(CURSOR_CACHE_FILE)) {
|
|
256
|
+
const prevFile = getCursorCacheFilePathForAccount(prev, false);
|
|
257
|
+
if (fs.existsSync(prevFile)) {
|
|
258
|
+
try {
|
|
259
|
+
archiveFile(prevFile, `usage.${prev}.previous`);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// ignore
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
fs.renameSync(CURSOR_CACHE_FILE, prevFile);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// ignore
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Promote next account cache file into usage.csv.
|
|
273
|
+
const nextFile = getCursorCacheFilePathForAccount(resolved, false);
|
|
274
|
+
if (fs.existsSync(nextFile)) {
|
|
275
|
+
if (fs.existsSync(CURSOR_CACHE_FILE)) {
|
|
276
|
+
try {
|
|
277
|
+
archiveFile(CURSOR_CACHE_FILE, `usage.active.pre-switch`);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// ignore
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
fs.renameSync(nextFile, CURSOR_CACHE_FILE);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// ignore
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// If a per-account cache exists, it was promoted into usage.csv above.
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// ignore cache reconcile errors
|
|
294
|
+
}
|
|
295
|
+
return { ok: true };
|
|
296
|
+
}
|
|
297
|
+
export function saveCursorCredentials(credentials, options) {
|
|
298
|
+
const sessionToken = credentials.sessionToken;
|
|
299
|
+
const accountId = deriveAccountId(sessionToken);
|
|
300
|
+
const store = loadCursorCredentialsStoreInternal() || {
|
|
301
|
+
version: 1,
|
|
302
|
+
activeAccountId: accountId,
|
|
303
|
+
accounts: {},
|
|
304
|
+
};
|
|
305
|
+
if (options?.label) {
|
|
306
|
+
const needle = options.label.trim().toLowerCase();
|
|
307
|
+
if (needle) {
|
|
308
|
+
for (const [id, acct] of Object.entries(store.accounts)) {
|
|
309
|
+
if (id === accountId)
|
|
310
|
+
continue;
|
|
311
|
+
if (acct.label && acct.label.trim().toLowerCase() === needle) {
|
|
312
|
+
throw new Error(`Cursor account label already exists: ${options.label}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const next = {
|
|
318
|
+
...credentials,
|
|
319
|
+
userId: credentials.userId || extractUserIdFromSessionToken(sessionToken) || undefined,
|
|
320
|
+
label: options?.label ?? credentials.label,
|
|
321
|
+
};
|
|
322
|
+
store.accounts[accountId] = next;
|
|
323
|
+
if (options?.setActive !== false) {
|
|
324
|
+
store.activeAccountId = accountId;
|
|
325
|
+
}
|
|
326
|
+
saveCursorCredentialsStoreInternal(store);
|
|
327
|
+
return { accountId };
|
|
328
|
+
}
|
|
329
|
+
export function loadCursorCredentials(nameOrId) {
|
|
330
|
+
const store = loadCursorCredentialsStoreInternal();
|
|
331
|
+
if (!store)
|
|
332
|
+
return null;
|
|
333
|
+
if (nameOrId) {
|
|
334
|
+
const resolved = resolveAccountId(store, nameOrId);
|
|
335
|
+
return resolved ? store.accounts[resolved] : null;
|
|
336
|
+
}
|
|
337
|
+
return store.accounts[store.activeAccountId] || null;
|
|
338
|
+
}
|
|
339
|
+
export function loadCursorCredentialsStore() {
|
|
340
|
+
return loadCursorCredentialsStoreInternal();
|
|
341
|
+
}
|
|
342
|
+
// NOTE: implementation moved below to support cache archiving by default.
|
|
343
|
+
export function removeCursorAccount(nameOrId, options) {
|
|
344
|
+
const store = loadCursorCredentialsStoreInternal();
|
|
345
|
+
if (!store)
|
|
346
|
+
return { removed: false, error: "Not authenticated" };
|
|
347
|
+
const resolved = resolveAccountId(store, nameOrId);
|
|
348
|
+
if (!resolved)
|
|
349
|
+
return { removed: false, error: `Account not found: ${nameOrId}` };
|
|
350
|
+
const wasActive = resolved === store.activeAccountId;
|
|
351
|
+
// Cache behavior:
|
|
352
|
+
// - Default: keep history but remove from aggregation by archiving out of cursor-cache/.
|
|
353
|
+
// - purgeCache: delete cache files.
|
|
354
|
+
const CURSOR_CACHE_ARCHIVE_DIR = path.join(CURSOR_CACHE_DIR, "archive");
|
|
355
|
+
const ensureCacheArchiveDir = () => {
|
|
356
|
+
ensureCacheDir();
|
|
357
|
+
if (!fs.existsSync(CURSOR_CACHE_ARCHIVE_DIR)) {
|
|
358
|
+
fs.mkdirSync(CURSOR_CACHE_ARCHIVE_DIR, { recursive: true, mode: 0o700 });
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
const archiveFile = (filePath, label) => {
|
|
362
|
+
ensureCacheArchiveDir();
|
|
363
|
+
const safeLabel = sanitizeAccountIdForFilename(label);
|
|
364
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
365
|
+
const dest = path.join(CURSOR_CACHE_ARCHIVE_DIR, `${safeLabel}-${ts}.csv`);
|
|
366
|
+
fs.renameSync(filePath, dest);
|
|
367
|
+
};
|
|
368
|
+
try {
|
|
369
|
+
migrateCursorCacheFromOldPath();
|
|
370
|
+
if (fs.existsSync(CURSOR_CACHE_DIR)) {
|
|
371
|
+
const perAccount = getCursorCacheFilePathForAccount(resolved, false);
|
|
372
|
+
if (fs.existsSync(perAccount)) {
|
|
373
|
+
if (options?.purgeCache) {
|
|
374
|
+
fs.rmSync(perAccount);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
archiveFile(perAccount, `usage.${resolved}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (wasActive && fs.existsSync(CURSOR_CACHE_FILE)) {
|
|
381
|
+
if (options?.purgeCache) {
|
|
382
|
+
fs.rmSync(CURSOR_CACHE_FILE);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
archiveFile(CURSOR_CACHE_FILE, `usage.active.${resolved}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// ignore
|
|
392
|
+
}
|
|
393
|
+
delete store.accounts[resolved];
|
|
394
|
+
const remaining = Object.keys(store.accounts);
|
|
395
|
+
if (remaining.length === 0) {
|
|
396
|
+
try {
|
|
397
|
+
fs.unlinkSync(CURSOR_CREDENTIALS_FILE);
|
|
398
|
+
}
|
|
399
|
+
catch { }
|
|
400
|
+
return { removed: true };
|
|
401
|
+
}
|
|
402
|
+
if (wasActive) {
|
|
403
|
+
store.activeAccountId = remaining[0];
|
|
404
|
+
}
|
|
405
|
+
saveCursorCredentialsStoreInternal(store);
|
|
406
|
+
if (wasActive) {
|
|
407
|
+
// Best-effort: reconcile usage.csv for the new active account.
|
|
408
|
+
try {
|
|
409
|
+
migrateCursorCacheFromOldPath();
|
|
410
|
+
ensureCacheDir();
|
|
411
|
+
const nextId = store.activeAccountId;
|
|
412
|
+
const nextFile = getCursorCacheFilePathForAccount(nextId, false);
|
|
413
|
+
if (fs.existsSync(nextFile)) {
|
|
414
|
+
if (fs.existsSync(CURSOR_CACHE_FILE)) {
|
|
415
|
+
try {
|
|
416
|
+
fs.rmSync(CURSOR_CACHE_FILE);
|
|
417
|
+
}
|
|
418
|
+
catch { }
|
|
419
|
+
}
|
|
420
|
+
fs.renameSync(nextFile, CURSOR_CACHE_FILE);
|
|
421
|
+
}
|
|
422
|
+
// If nextFile existed, it was promoted into usage.csv above.
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// ignore
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { removed: true };
|
|
429
|
+
}
|
|
77
430
|
export function clearCursorCredentials() {
|
|
431
|
+
// Backward compatible: clears ALL accounts
|
|
78
432
|
try {
|
|
79
433
|
if (fs.existsSync(CURSOR_CREDENTIALS_FILE)) {
|
|
80
434
|
fs.unlinkSync(CURSOR_CREDENTIALS_FILE);
|
|
@@ -86,8 +440,51 @@ export function clearCursorCredentials() {
|
|
|
86
440
|
return false;
|
|
87
441
|
}
|
|
88
442
|
}
|
|
443
|
+
export function clearCursorCredentialsAndCache(options) {
|
|
444
|
+
const cleared = clearCursorCredentials();
|
|
445
|
+
if (!cleared)
|
|
446
|
+
return false;
|
|
447
|
+
try {
|
|
448
|
+
migrateCursorCacheFromOldPath();
|
|
449
|
+
if (!fs.existsSync(CURSOR_CACHE_DIR))
|
|
450
|
+
return true;
|
|
451
|
+
const archiveDir = path.join(CURSOR_CACHE_DIR, "archive");
|
|
452
|
+
const ensureArchiveDir = () => {
|
|
453
|
+
ensureCacheDir();
|
|
454
|
+
if (!fs.existsSync(archiveDir)) {
|
|
455
|
+
fs.mkdirSync(archiveDir, { recursive: true, mode: 0o700 });
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
const archiveFile = (filePath, label) => {
|
|
459
|
+
ensureArchiveDir();
|
|
460
|
+
const safeLabel = sanitizeAccountIdForFilename(label);
|
|
461
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
462
|
+
const dest = path.join(archiveDir, `${safeLabel}-${ts}.csv`);
|
|
463
|
+
fs.renameSync(filePath, dest);
|
|
464
|
+
};
|
|
465
|
+
for (const f of fs.readdirSync(CURSOR_CACHE_DIR)) {
|
|
466
|
+
if (!f.startsWith("usage") || !f.endsWith(".csv"))
|
|
467
|
+
continue;
|
|
468
|
+
const filePath = path.join(CURSOR_CACHE_DIR, f);
|
|
469
|
+
try {
|
|
470
|
+
if (options?.purgeCache) {
|
|
471
|
+
fs.rmSync(filePath);
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
archiveFile(filePath, `usage.all.${f}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
catch { }
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// ignore
|
|
482
|
+
}
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
89
485
|
export function isCursorLoggedIn() {
|
|
90
|
-
|
|
486
|
+
const store = loadCursorCredentialsStoreInternal();
|
|
487
|
+
return !!store && Object.keys(store.accounts).length > 0;
|
|
91
488
|
}
|
|
92
489
|
// ============================================================================
|
|
93
490
|
// API Client
|
|
@@ -302,8 +699,8 @@ export function cursorRowsToMessages(rows) {
|
|
|
302
699
|
* Fetch and parse Cursor usage data
|
|
303
700
|
* Requires valid credentials to be stored
|
|
304
701
|
*/
|
|
305
|
-
export async function readCursorUsage() {
|
|
306
|
-
const credentials = loadCursorCredentials();
|
|
702
|
+
export async function readCursorUsage(nameOrId) {
|
|
703
|
+
const credentials = loadCursorCredentials(nameOrId);
|
|
307
704
|
if (!credentials) {
|
|
308
705
|
throw new Error("Cursor not authenticated. Run 'tokscale cursor login' first.");
|
|
309
706
|
}
|
|
@@ -325,6 +722,12 @@ export function getCursorCredentialsPath() {
|
|
|
325
722
|
const OLD_CURSOR_CACHE_DIR = path.join(os.homedir(), ".tokscale", "cursor-cache");
|
|
326
723
|
const CURSOR_CACHE_DIR = path.join(CONFIG_DIR, "cursor-cache");
|
|
327
724
|
const CURSOR_CACHE_FILE = path.join(CURSOR_CACHE_DIR, "usage.csv");
|
|
725
|
+
function getCursorCacheFilePathForAccount(accountId, isActive) {
|
|
726
|
+
if (isActive)
|
|
727
|
+
return CURSOR_CACHE_FILE;
|
|
728
|
+
const safe = sanitizeAccountIdForFilename(accountId);
|
|
729
|
+
return path.join(CURSOR_CACHE_DIR, `usage.${safe}.csv`);
|
|
730
|
+
}
|
|
328
731
|
function ensureCacheDir() {
|
|
329
732
|
if (!fs.existsSync(CURSOR_CACHE_DIR)) {
|
|
330
733
|
fs.mkdirSync(CURSOR_CACHE_DIR, { recursive: true, mode: 0o700 });
|
|
@@ -358,17 +761,49 @@ function migrateCursorCacheFromOldPath() {
|
|
|
358
761
|
*/
|
|
359
762
|
export async function syncCursorCache() {
|
|
360
763
|
migrateCursorCacheFromOldPath();
|
|
361
|
-
const
|
|
362
|
-
if (!
|
|
764
|
+
const store = loadCursorCredentialsStoreInternal();
|
|
765
|
+
if (!store)
|
|
766
|
+
return { synced: false, rows: 0, error: "Not authenticated" };
|
|
767
|
+
const accounts = Object.entries(store.accounts);
|
|
768
|
+
if (accounts.length === 0)
|
|
363
769
|
return { synced: false, rows: 0, error: "Not authenticated" };
|
|
364
|
-
}
|
|
365
770
|
try {
|
|
366
|
-
const csvText = await fetchCursorUsageCsv(credentials.sessionToken);
|
|
367
771
|
ensureCacheDir();
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
772
|
+
// Ensure we don't double-count active account (usage.csv + usage.<active>.csv)
|
|
773
|
+
const activeId = store.activeAccountId;
|
|
774
|
+
if (activeId) {
|
|
775
|
+
const dup = getCursorCacheFilePathForAccount(activeId, false);
|
|
776
|
+
if (fs.existsSync(dup)) {
|
|
777
|
+
try {
|
|
778
|
+
fs.rmSync(dup);
|
|
779
|
+
}
|
|
780
|
+
catch { }
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
let totalRows = 0;
|
|
784
|
+
let successCount = 0;
|
|
785
|
+
const errors = [];
|
|
786
|
+
for (const [accountId, credentials] of accounts) {
|
|
787
|
+
const isActive = accountId === store.activeAccountId;
|
|
788
|
+
try {
|
|
789
|
+
const csvText = await fetchCursorUsageCsv(credentials.sessionToken);
|
|
790
|
+
const filePath = getCursorCacheFilePathForAccount(accountId, isActive);
|
|
791
|
+
atomicWriteFile(filePath, csvText, 0o600);
|
|
792
|
+
totalRows += parseCursorCsv(csvText).length;
|
|
793
|
+
successCount += 1;
|
|
794
|
+
}
|
|
795
|
+
catch (e) {
|
|
796
|
+
errors.push(`${accountId}: ${e.message}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (successCount === 0) {
|
|
800
|
+
return { synced: false, rows: 0, error: errors[0] || "Cursor sync failed" };
|
|
801
|
+
}
|
|
802
|
+
return {
|
|
803
|
+
synced: true,
|
|
804
|
+
rows: totalRows,
|
|
805
|
+
error: errors.length > 0 ? `Some accounts failed to sync (${errors.length}/${accounts.length})` : undefined,
|
|
806
|
+
};
|
|
372
807
|
}
|
|
373
808
|
catch (error) {
|
|
374
809
|
return { synced: false, rows: 0, error: error.message };
|
|
@@ -378,12 +813,15 @@ export async function syncCursorCache() {
|
|
|
378
813
|
* Get the cache file path
|
|
379
814
|
*/
|
|
380
815
|
export function getCursorCachePath() {
|
|
816
|
+
// Ensure legacy cache is migrated before reporting paths
|
|
817
|
+
migrateCursorCacheFromOldPath();
|
|
381
818
|
return CURSOR_CACHE_FILE;
|
|
382
819
|
}
|
|
383
820
|
/**
|
|
384
821
|
* Check if cache exists and when it was last updated
|
|
385
822
|
*/
|
|
386
823
|
export function getCursorCacheStatus() {
|
|
824
|
+
migrateCursorCacheFromOldPath();
|
|
387
825
|
const exists = fs.existsSync(CURSOR_CACHE_FILE);
|
|
388
826
|
let lastModified;
|
|
389
827
|
if (exists) {
|
|
@@ -397,36 +835,72 @@ export function getCursorCacheStatus() {
|
|
|
397
835
|
}
|
|
398
836
|
return { exists, lastModified, path: CURSOR_CACHE_FILE };
|
|
399
837
|
}
|
|
838
|
+
export function hasCursorUsageCache() {
|
|
839
|
+
migrateCursorCacheFromOldPath();
|
|
840
|
+
try {
|
|
841
|
+
if (!fs.existsSync(CURSOR_CACHE_DIR))
|
|
842
|
+
return false;
|
|
843
|
+
const files = fs.readdirSync(CURSOR_CACHE_DIR);
|
|
844
|
+
return files.some((f) => isCursorUsageCsvFilename(f));
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
400
850
|
export function readCursorMessagesFromCache() {
|
|
401
|
-
|
|
851
|
+
migrateCursorCacheFromOldPath();
|
|
852
|
+
if (!fs.existsSync(CURSOR_CACHE_DIR)) {
|
|
402
853
|
return [];
|
|
403
854
|
}
|
|
855
|
+
let files;
|
|
404
856
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const cacheWrite = Math.max(0, row.inputWithCacheWrite - row.inputWithoutCacheWrite);
|
|
409
|
-
const input = row.inputWithoutCacheWrite;
|
|
410
|
-
return {
|
|
411
|
-
source: "cursor",
|
|
412
|
-
modelId: row.model,
|
|
413
|
-
providerId: inferProvider(row.model),
|
|
414
|
-
sessionId: `cursor-${row.date}-${row.model}`,
|
|
415
|
-
timestamp: row.timestamp,
|
|
416
|
-
date: row.date,
|
|
417
|
-
tokens: {
|
|
418
|
-
input,
|
|
419
|
-
output: row.outputTokens,
|
|
420
|
-
cacheRead: row.cacheRead,
|
|
421
|
-
cacheWrite,
|
|
422
|
-
reasoning: 0,
|
|
423
|
-
},
|
|
424
|
-
cost: row.costToYou || row.apiCost,
|
|
425
|
-
};
|
|
426
|
-
});
|
|
857
|
+
files = fs
|
|
858
|
+
.readdirSync(CURSOR_CACHE_DIR)
|
|
859
|
+
.filter((f) => isCursorUsageCsvFilename(f));
|
|
427
860
|
}
|
|
428
861
|
catch {
|
|
429
862
|
return [];
|
|
430
863
|
}
|
|
864
|
+
const store = loadCursorCredentialsStoreInternal();
|
|
865
|
+
const activeId = store?.activeAccountId;
|
|
866
|
+
const all = [];
|
|
867
|
+
for (const file of files) {
|
|
868
|
+
const filePath = path.join(CURSOR_CACHE_DIR, file);
|
|
869
|
+
let accountId = "unknown";
|
|
870
|
+
if (file === "usage.csv") {
|
|
871
|
+
accountId = activeId || "active";
|
|
872
|
+
}
|
|
873
|
+
else if (file.startsWith("usage.") && file.endsWith(".csv")) {
|
|
874
|
+
accountId = file.slice("usage.".length, -".csv".length);
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
const csvText = fs.readFileSync(filePath, "utf-8");
|
|
878
|
+
const rows = parseCursorCsv(csvText);
|
|
879
|
+
for (const row of rows) {
|
|
880
|
+
const cacheWrite = Math.max(0, row.inputWithCacheWrite - row.inputWithoutCacheWrite);
|
|
881
|
+
const input = row.inputWithoutCacheWrite;
|
|
882
|
+
all.push({
|
|
883
|
+
source: "cursor",
|
|
884
|
+
modelId: row.model,
|
|
885
|
+
providerId: inferProvider(row.model),
|
|
886
|
+
sessionId: `cursor-${accountId}-${row.date}-${row.model}`,
|
|
887
|
+
timestamp: row.timestamp,
|
|
888
|
+
date: row.date,
|
|
889
|
+
tokens: {
|
|
890
|
+
input,
|
|
891
|
+
output: row.outputTokens,
|
|
892
|
+
cacheRead: row.cacheRead,
|
|
893
|
+
cacheWrite,
|
|
894
|
+
reasoning: 0,
|
|
895
|
+
},
|
|
896
|
+
cost: row.costToYou || row.apiCost,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
catch {
|
|
901
|
+
// ignore file
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return all;
|
|
431
905
|
}
|
|
432
906
|
//# sourceMappingURL=cursor.js.map
|