@vibe-cafe/vibe-usage 0.10.25 → 0.10.27
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 +1 -1
- package/package.json +1 -1
- package/src/parsers/cursor.js +9 -3
- package/src/sync.js +62 -18
package/README.md
CHANGED
|
@@ -111,7 +111,7 @@ Cursor 用量需要从 `cursor.com` 下载 CSV。`Cursor usage export skipped (n
|
|
|
111
111
|
- `ENOTFOUND` / `EAI_AGAIN`:检查 DNS 和终端网络。
|
|
112
112
|
- `UND_ERR_CONNECT_TIMEOUT` / `ETIMEDOUT` / `ECONNRESET`:检查到 Cursor 的连接及终端代理。浏览器或 Cursor 应用能联网,不代表 Node.js 使用了相同代理。
|
|
113
113
|
- `CERT_*` / `UNABLE_TO_VERIFY_LEAF_SIGNATURE` 等:检查系统时间和代理或公司网络的 CA 证书配置;自定义 CA 可通过 `NODE_EXTRA_CA_CERTS` 指定。
|
|
114
|
-
- `timeout after …ms
|
|
114
|
+
- `timeout after …ms`:导出超时。`cursor.com` 的导出是按整个账号现算的,**账号用量越大越慢**,所以重度用户会每次都超时、而不是偶尔。默认等待 120 秒;仍不够就调大,例如 `VIBE_USAGE_CURSOR_FETCH_TIMEOUT_MS=300000`,然后重跑一次 `sync` 补传历史。
|
|
115
115
|
- `Cursor session rejected`:在 Cursor 的 Account 设置中重新登录,再同步。
|
|
116
116
|
|
|
117
117
|
需要 HTTP/HTTPS 代理时,Node.js **22.21+ 或 24.5+** 可用 `NODE_USE_ENV_PROXY=1` 启用环境变量代理([Node.js 官方说明](https://nodejs.org/en/learn/http/enterprise-network-configuration))。以下为 macOS/Linux 终端示例,代理地址须替换为实际地址:
|
package/package.json
CHANGED
package/src/parsers/cursor.js
CHANGED
|
@@ -68,9 +68,15 @@ function decodeJwtSub(token) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
71
|
+
// cursor.com's CSV export is computed over the whole account, so it gets
|
|
72
|
+
// slower the more usage the account has: heavy users time out on every run,
|
|
73
|
+
// not intermittently. 10s (v0.7.11) and then 30s (#72) were both still short
|
|
74
|
+
// enough to lock those accounts out permanently -- a user with ~13B tokens/30d
|
|
75
|
+
// needed 120s to complete a single export, and every default-timeout run of
|
|
76
|
+
// his silently uploaded nothing. Budget for the slow accounts; a healthy
|
|
77
|
+
// export returns in a second or two, so this ceiling only costs wall-clock
|
|
78
|
+
// time on the runs that were failing anyway.
|
|
79
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 120_000;
|
|
74
80
|
const MAX_FETCH_TIMEOUT_MS = 2_147_483_647;
|
|
75
81
|
|
|
76
82
|
export function resolveCursorFetchTimeout(value) {
|
package/src/sync.js
CHANGED
|
@@ -15,18 +15,34 @@ import { success, failure, warn, arrow, link, dim } from './output.js';
|
|
|
15
15
|
const BATCH_SIZE = 100;
|
|
16
16
|
const SESSION_BATCH_SIZE = 500;
|
|
17
17
|
|
|
18
|
+
/** Coarse human duration: "45s" / "2m10s" / "1h 20m". */
|
|
19
|
+
export function formatDuration(seconds) {
|
|
20
|
+
const secs = Math.max(0, Math.round(seconds));
|
|
21
|
+
if (secs < 60) return `${secs}s`;
|
|
22
|
+
const h = Math.floor(secs / 3600);
|
|
23
|
+
const m = Math.floor((secs % 3600) / 60);
|
|
24
|
+
const s = secs % 60;
|
|
25
|
+
if (h > 0) return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
26
|
+
return s > 0 ? `${m}m${s}s` : `${m}m`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Remaining upload time from batches already finished. Measured, never guessed:
|
|
31
|
+
* returns null until at least one batch has completed, because a first-sync
|
|
32
|
+
* backlog and a steady-state trickle differ by three orders of magnitude and
|
|
33
|
+
* any a-priori rate would be wrong for one of them.
|
|
34
|
+
*/
|
|
35
|
+
export function estimateRemainingSeconds({ elapsedMs, doneBatches, totalBatches }) {
|
|
36
|
+
if (!(elapsedMs > 0) || doneBatches < 1 || totalBatches <= doneBatches) return null;
|
|
37
|
+
return ((elapsedMs / doneBatches) * (totalBatches - doneBatches)) / 1000;
|
|
38
|
+
}
|
|
39
|
+
|
|
18
40
|
function formatBytes(bytes) {
|
|
19
41
|
if (bytes < 1024) return `${bytes}B`;
|
|
20
42
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
21
43
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
22
44
|
}
|
|
23
45
|
|
|
24
|
-
/** Hide only Cursor's intentional transient fetch soft-skip in quiet (daemon) syncs. */
|
|
25
|
-
export function shouldSuppressParserWarning(source, message, quiet) {
|
|
26
|
-
if (!quiet) return false;
|
|
27
|
-
return source === 'cursor' && message.startsWith('cursor: Cursor usage export skipped (');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
46
|
export function resolveUploadProjectSetting(settings) {
|
|
31
47
|
if (typeof settings?.uploadProject !== 'boolean') {
|
|
32
48
|
const error = new Error('SETTINGS_UNAVAILABLE');
|
|
@@ -184,13 +200,17 @@ export async function runSync({
|
|
|
184
200
|
if (indexing) {
|
|
185
201
|
parserProgress.push({ source, ...indexing });
|
|
186
202
|
}
|
|
203
|
+
// Parser warnings always reach stderr, including quiet (daemon) runs: the
|
|
204
|
+
// daemon log is the only trail a background failure leaves. Cursor's fetch
|
|
205
|
+
// soft-skip used to be filtered out here to keep that log tidy, which made
|
|
206
|
+
// a permanently failing export indistinguishable from a healthy one -- the
|
|
207
|
+
// tool still listed as "installed" while it had never uploaded a byte.
|
|
187
208
|
for (const message of warnings) {
|
|
188
|
-
if (shouldSuppressParserWarning(source, message, quiet)) continue;
|
|
189
209
|
process.stderr.write(`${dim(` ${message}`)}\n`);
|
|
190
210
|
}
|
|
191
|
-
// A parser may
|
|
192
|
-
//
|
|
193
|
-
//
|
|
211
|
+
// A parser may downgrade a transient error (Cursor network timeout) to a
|
|
212
|
+
// warning instead of throwing. Its empty result is not proof that its prior
|
|
213
|
+
// data disappeared, so it must not be pruned this run.
|
|
194
214
|
if (!skipped) okSources.add(source);
|
|
195
215
|
for (const bucket of buckets) allBuckets.push(bucket);
|
|
196
216
|
for (const session of sessions) allSessions.push(session);
|
|
@@ -323,6 +343,20 @@ export async function runSync({
|
|
|
323
343
|
const totalBatches = Math.max(bucketBatches, sessionBatches, 1);
|
|
324
344
|
const syncClient = createSyncClient({ defaultSurface: surface, hostname: host });
|
|
325
345
|
|
|
346
|
+
// Say up front how much is about to go up. A first sync (or one that
|
|
347
|
+
// backfills after a parser was broken) can be thousands of batches, and with
|
|
348
|
+
// only a per-batch progress line the user cannot tell a long upload from a
|
|
349
|
+
// hung one -- which is exactly how a silently failing parser stayed hidden.
|
|
350
|
+
if (!quiet) {
|
|
351
|
+
const pending = [`${allBucketsToSend.length} buckets`];
|
|
352
|
+
if (allSessionsToSend.length > 0) pending.push(`${allSessionsToSend.length} sessions`);
|
|
353
|
+
const batchNote = totalBatches > 1 ? `,分 ${totalBatches} 批` : '';
|
|
354
|
+
console.log(dim(` 待上传 ${pending.join(' · ')}${batchNote}`));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let uploadedBytes = 0;
|
|
358
|
+
const uploadStartedAt = Date.now();
|
|
359
|
+
|
|
326
360
|
try {
|
|
327
361
|
for (let batchIdx = 0; batchIdx < totalBatches; batchIdx++) {
|
|
328
362
|
const batch = allBucketsToSend.slice(batchIdx * BATCH_SIZE, (batchIdx + 1) * BATCH_SIZE);
|
|
@@ -330,13 +364,25 @@ export async function runSync({
|
|
|
330
364
|
const batchNum = batchIdx + 1;
|
|
331
365
|
const prefix = totalBatches > 1 ? ` ${dim(`[${batchNum}/${totalBatches}]`)} 上传中 ` : ' 上传中 ';
|
|
332
366
|
|
|
367
|
+
// Only measured batches feed the estimate, so the first batch shows no
|
|
368
|
+
// ETA rather than a made-up one.
|
|
369
|
+
const remaining = estimateRemainingSeconds({
|
|
370
|
+
elapsedMs: Date.now() - uploadStartedAt,
|
|
371
|
+
doneBatches: batchIdx,
|
|
372
|
+
totalBatches,
|
|
373
|
+
});
|
|
374
|
+
const etaNote = remaining === null ? '' : ` · 预计还需 ${formatDuration(remaining)}`;
|
|
375
|
+
|
|
376
|
+
let batchBytes = 0;
|
|
333
377
|
const result = await ingest(apiUrl, config.apiKey, batch, {
|
|
334
378
|
client: forBatch(syncClient, batchIdx, totalBatches),
|
|
335
379
|
onProgress(sent, total) {
|
|
380
|
+
batchBytes = total;
|
|
336
381
|
const pct = Math.round((sent / total) * 100);
|
|
337
|
-
process.stdout.write(`\r${prefix}${dim(`${formatBytes(sent)}/${formatBytes(total)} (${pct}%)`)}\x1b[K`);
|
|
382
|
+
process.stdout.write(`\r${prefix}${dim(`${formatBytes(sent)}/${formatBytes(total)} (${pct}%)${etaNote}`)}\x1b[K`);
|
|
338
383
|
},
|
|
339
384
|
}, batchSessions.length > 0 ? batchSessions : undefined);
|
|
385
|
+
uploadedBytes += batchBytes;
|
|
340
386
|
totalIngested += result.ingested ?? batch.length;
|
|
341
387
|
totalSessionsSynced += result.sessions ?? 0;
|
|
342
388
|
const batchUnknownSources = new Set(result.dropped?.unknownSources || []);
|
|
@@ -381,6 +427,10 @@ export async function runSync({
|
|
|
381
427
|
const syncParts = [`${totalIngested} buckets`];
|
|
382
428
|
if (totalSessionsSynced > 0) syncParts.push(`${totalSessionsSynced} sessions`);
|
|
383
429
|
console.log(success(`已同步 ${syncParts.join(' · ')}`));
|
|
430
|
+
if (!quiet && uploadedBytes > 0) {
|
|
431
|
+
const elapsed = (Date.now() - uploadStartedAt) / 1000;
|
|
432
|
+
console.log(dim(` 上传 ${formatBytes(uploadedBytes)}(已压缩),用时 ${formatDuration(elapsed)}`));
|
|
433
|
+
}
|
|
384
434
|
|
|
385
435
|
if (totalDroppedBuckets > 0) {
|
|
386
436
|
const reasons = [];
|
|
@@ -401,13 +451,7 @@ export async function runSync({
|
|
|
401
451
|
const totalActive = allSessionsToSend.reduce((s, x) => s + x.activeSeconds, 0);
|
|
402
452
|
const totalDuration = allSessionsToSend.reduce((s, x) => s + x.durationSeconds, 0);
|
|
403
453
|
const totalMsgs = allSessionsToSend.reduce((s, x) => s + x.messageCount, 0);
|
|
404
|
-
|
|
405
|
-
if (secs < 60) return `${secs}s`;
|
|
406
|
-
const h = Math.floor(secs / 3600);
|
|
407
|
-
const m = Math.floor((secs % 3600) / 60);
|
|
408
|
-
return h > 0 ? (m > 0 ? `${h}h ${m}m` : `${h}h`) : `${m}m`;
|
|
409
|
-
};
|
|
410
|
-
console.log(dim(` 活跃 ${fmtTime(totalActive)} / 总时长 ${fmtTime(totalDuration)} · ${totalMsgs} 条消息`));
|
|
454
|
+
console.log(dim(` 活跃 ${formatDuration(totalActive)} / 总时长 ${formatDuration(totalDuration)} · ${totalMsgs} 条消息`));
|
|
411
455
|
}
|
|
412
456
|
|
|
413
457
|
if (!quiet) {
|