@vibe-cafe/vibe-usage 0.10.9 → 0.10.11
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 +2 -1
- package/package.json +1 -1
- package/src/api.js +56 -42
- package/src/parsers/aggregate.js +157 -0
- package/src/parsers/alma.js +18 -15
- package/src/parsers/amp.js +1 -1
- package/src/parsers/antigravity-db.js +8 -31
- package/src/parsers/antigravity.js +1 -1
- package/src/parsers/claude-code.js +2 -14
- package/src/parsers/cline.js +4 -14
- package/src/parsers/codex.js +1 -1
- package/src/parsers/contract.js +55 -0
- package/src/parsers/copilot-cli.js +1 -1
- package/src/parsers/cursor.js +12 -34
- package/src/parsers/dimagent.js +7 -13
- package/src/parsers/droid.js +1 -1
- package/src/parsers/dsh.js +454 -0
- package/src/parsers/fs-utils.js +36 -0
- package/src/parsers/gemini-cli.js +1 -1
- package/src/parsers/grok.js +3 -17
- package/src/parsers/hermes.js +3 -5
- package/src/parsers/index.js +3 -148
- package/src/parsers/kimi-code.js +1 -1
- package/src/parsers/kiro.js +6 -39
- package/src/parsers/mimocode.js +3 -5
- package/src/parsers/openclaw.js +1 -1
- package/src/parsers/opencode.js +3 -5
- package/src/parsers/pi-session-jsonl.js +6 -16
- package/src/parsers/qwen-code.js +1 -1
- package/src/parsers/roo-code.js +4 -14
- package/src/parsers/sqlite.js +46 -0
- package/src/parsers/trae-cli.js +3 -13
- package/src/parsers/workbuddy.js +1 -1
- package/src/parsers/zcode.js +3 -5
- package/src/state.js +14 -3
- package/src/summary.js +12 -42
- package/src/sync.js +110 -38
- package/src/tools.js +28 -0
package/src/sync.js
CHANGED
|
@@ -6,8 +6,10 @@ import {
|
|
|
6
6
|
} from './state.js';
|
|
7
7
|
import { ingest, fetchSettings } from './api.js';
|
|
8
8
|
import { createSyncClient, forBatch } from './client-meta.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { parsers } from './parsers/index.js';
|
|
10
|
+
import { aggregateToBuckets } from './parsers/aggregate.js';
|
|
11
|
+
import { normalizeParserResult } from './parsers/contract.js';
|
|
12
|
+
import { success, failure, warn, arrow, link, dim } from './output.js';
|
|
11
13
|
|
|
12
14
|
const BATCH_SIZE = 100;
|
|
13
15
|
const SESSION_BATCH_SIZE = 500;
|
|
@@ -27,6 +29,13 @@ export function resolveUploadProjectSetting(settings) {
|
|
|
27
29
|
return settings.uploadProject;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
export function resolveCachedUploadProjectSetting(config, apiUrl) {
|
|
33
|
+
if (config?.lastUploadProjectApiUrl !== apiUrl) return undefined;
|
|
34
|
+
return typeof config.lastUploadProject === 'boolean'
|
|
35
|
+
? config.lastUploadProject
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
export function resolveCodexExtraHome(configured, temporary) {
|
|
31
40
|
return temporary ?? configured;
|
|
32
41
|
}
|
|
@@ -41,6 +50,27 @@ export function reaggregateHiddenProjectBuckets(buckets) {
|
|
|
41
50
|
})));
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
// Parser execution is I/O bound (log reads, occasional network calls). Run a
|
|
54
|
+
// bounded number at once to cut wall-clock sync time without the memory spike
|
|
55
|
+
// of loading every tool's logs simultaneously.
|
|
56
|
+
export const PARSER_CONCURRENCY = 4;
|
|
57
|
+
|
|
58
|
+
// Run `fn` over `items` with at most `limit` in flight, preserving order.
|
|
59
|
+
export async function mapWithConcurrency(items, limit, fn) {
|
|
60
|
+
const results = new Array(items.length);
|
|
61
|
+
let nextIndex = 0;
|
|
62
|
+
const workerCount = Math.max(1, Math.min(limit, items.length));
|
|
63
|
+
const workers = Array.from({ length: workerCount }, async () => {
|
|
64
|
+
while (true) {
|
|
65
|
+
const index = nextIndex++;
|
|
66
|
+
if (index >= items.length) return;
|
|
67
|
+
results[index] = await fn(items[index], index);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
await Promise.all(workers);
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
export async function runSync({
|
|
45
75
|
throws = false,
|
|
46
76
|
quiet = false,
|
|
@@ -70,14 +100,34 @@ export async function runSync({
|
|
|
70
100
|
try {
|
|
71
101
|
const settings = await fetchSettings(apiUrl, config.apiKey);
|
|
72
102
|
uploadProject = resolveUploadProjectSetting(settings);
|
|
103
|
+
// Scope the cached privacy choice to the server that returned it. Reusing
|
|
104
|
+
// the value after `apiUrl` changes could expose project names to a
|
|
105
|
+
// different server during its first settings outage.
|
|
106
|
+
if (
|
|
107
|
+
config.lastUploadProject !== uploadProject
|
|
108
|
+
|| config.lastUploadProjectApiUrl !== apiUrl
|
|
109
|
+
) {
|
|
110
|
+
config.lastUploadProject = uploadProject;
|
|
111
|
+
config.lastUploadProjectApiUrl = apiUrl;
|
|
112
|
+
saveConfig(config);
|
|
113
|
+
}
|
|
73
114
|
} catch (err) {
|
|
74
115
|
if (err.message === 'UNAUTHORIZED') {
|
|
75
116
|
console.error(failure('API Key 无效,请运行 `npx @vibe-cafe/vibe-usage init` 重新配置。'));
|
|
117
|
+
if (throws) throw err;
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
// Settings endpoint unreachable (not auth): degrade to the last confirmed
|
|
121
|
+
// choice for this same server rather than hard-aborting every upload.
|
|
122
|
+
const cachedUploadProject = resolveCachedUploadProjectSetting(config, apiUrl);
|
|
123
|
+
if (typeof cachedUploadProject === 'boolean') {
|
|
124
|
+
uploadProject = cachedUploadProject;
|
|
125
|
+
if (!quiet) console.log(warn('设置接口不可用,沿用上次的项目名设置。'));
|
|
76
126
|
} else {
|
|
77
127
|
console.error(failure('暂时无法读取上传设置,本次同步已安全取消(未上传数据)。请稍后重试。'));
|
|
128
|
+
if (throws) throw err;
|
|
129
|
+
process.exit(1);
|
|
78
130
|
}
|
|
79
|
-
if (throws) throw err;
|
|
80
|
-
process.exit(1);
|
|
81
131
|
}
|
|
82
132
|
|
|
83
133
|
let allBuckets = [];
|
|
@@ -89,36 +139,52 @@ export async function runSync({
|
|
|
89
139
|
// state and force a full re-upload next run.
|
|
90
140
|
const okSources = new Set();
|
|
91
141
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
142
|
+
// Run parsers concurrently (bounded) so one slow parser (Cursor's network
|
|
143
|
+
// fetch, a cold Codex index) can't stall the rest. Results are collected in
|
|
144
|
+
// registry order so output and merged arrays stay deterministic.
|
|
145
|
+
const parserOutcomes = await mapWithConcurrency(
|
|
146
|
+
Object.entries(parsers),
|
|
147
|
+
PARSER_CONCURRENCY,
|
|
148
|
+
async ([source, parse]) => {
|
|
149
|
+
try {
|
|
150
|
+
const result = source === 'codex'
|
|
151
|
+
? await parse({ codexExtraHome: resolveCodexExtraHome(config.codexExtraHome, codexExtraHome) })
|
|
152
|
+
: await parse();
|
|
153
|
+
return { source, result };
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return { source, error: err };
|
|
101
156
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
process.stderr.write(`${dim(` ${message}`)}\n`);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// A parser may deliberately suppress a transient error (Cursor network
|
|
111
|
-
// timeout) to keep daemon logs quiet. Its empty result is not proof that
|
|
112
|
-
// its prior data disappeared, so it must not be pruned this run.
|
|
113
|
-
if (!result?.skipped) okSources.add(source);
|
|
114
|
-
if (buckets.length > 0) allBuckets.push(...buckets);
|
|
115
|
-
if (sessions.length > 0) allSessions.push(...sessions);
|
|
116
|
-
if (buckets.length > 0 || sessions.length > 0) {
|
|
117
|
-
parserResults.push({ source, buckets: buckets.length, sessions: sessions.length });
|
|
118
|
-
}
|
|
119
|
-
} catch (err) {
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
for (const { source, result, error } of parserOutcomes) {
|
|
161
|
+
if (error) {
|
|
120
162
|
// Parser errors are non-fatal — pass-through in dim gray (no translation).
|
|
163
|
+
process.stderr.write(`${dim(` ${source}: ${error.message}`)}\n`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
let normalized;
|
|
167
|
+
try {
|
|
168
|
+
normalized = normalizeParserResult(source, result);
|
|
169
|
+
} catch (err) {
|
|
121
170
|
process.stderr.write(`${dim(` ${source}: ${err.message}`)}\n`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const { buckets, sessions, skipped, warnings, indexing } = normalized;
|
|
174
|
+
if (indexing) {
|
|
175
|
+
parserProgress.push({ source, ...indexing });
|
|
176
|
+
}
|
|
177
|
+
for (const message of warnings) {
|
|
178
|
+
process.stderr.write(`${dim(` ${message}`)}\n`);
|
|
179
|
+
}
|
|
180
|
+
// A parser may deliberately suppress a transient error (Cursor network
|
|
181
|
+
// timeout) to keep daemon logs quiet. Its empty result is not proof that
|
|
182
|
+
// its prior data disappeared, so it must not be pruned this run.
|
|
183
|
+
if (!skipped) okSources.add(source);
|
|
184
|
+
if (buckets.length > 0) allBuckets.push(...buckets);
|
|
185
|
+
if (sessions.length > 0) allSessions.push(...sessions);
|
|
186
|
+
if (buckets.length > 0 || sessions.length > 0) {
|
|
187
|
+
parserResults.push({ source, buckets: buckets.length, sessions: sessions.length });
|
|
122
188
|
}
|
|
123
189
|
}
|
|
124
190
|
|
|
@@ -271,10 +337,10 @@ export async function runSync({
|
|
|
271
337
|
}
|
|
272
338
|
totalProtectedBuckets += Number(result.protected?.buckets) || 0;
|
|
273
339
|
|
|
274
|
-
// Commit only
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
340
|
+
// Commit only hashes from this successful batch. Persist before starting
|
|
341
|
+
// the next batch: if a later upload fails or the process exits abruptly,
|
|
342
|
+
// the next sync retries only the uncommitted suffix.
|
|
343
|
+
let batchStateChanged = false;
|
|
278
344
|
for (const b of batch) {
|
|
279
345
|
// A source unknown to an older backend may become valid after deploy.
|
|
280
346
|
// Leave those hashes uncommitted so the next sync retries them instead
|
|
@@ -282,14 +348,20 @@ export async function runSync({
|
|
|
282
348
|
if (batchUnknownSources.has(b.source)) continue;
|
|
283
349
|
const key = bucketKey(b);
|
|
284
350
|
const entry = pendingBucketState.get(key);
|
|
285
|
-
if (entry)
|
|
351
|
+
if (entry) {
|
|
352
|
+
state.buckets[key] = entry;
|
|
353
|
+
batchStateChanged = true;
|
|
354
|
+
}
|
|
286
355
|
}
|
|
287
356
|
for (const s of batchSessions) {
|
|
288
357
|
const key = sessionKey(s);
|
|
289
358
|
const entry = pendingSessionState.get(key);
|
|
290
|
-
if (entry)
|
|
359
|
+
if (entry) {
|
|
360
|
+
state.sessions[key] = entry;
|
|
361
|
+
batchStateChanged = true;
|
|
362
|
+
}
|
|
291
363
|
}
|
|
292
|
-
saveState(state);
|
|
364
|
+
if (batchStateChanged) saveState(state);
|
|
293
365
|
}
|
|
294
366
|
|
|
295
367
|
if (totalBatches > 1 || allBucketsToSend.length > 0) {
|
package/src/tools.js
CHANGED
|
@@ -120,6 +120,28 @@ function findKimiCodeDataDirs() {
|
|
|
120
120
|
].filter(existsSync);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** DeepSeek Harness home: DSH_HOME env (same as the dsh CLI) or ~/.dsh. */
|
|
124
|
+
export function getDshHome(env = process.env) {
|
|
125
|
+
const explicit = env.DSH_HOME?.trim();
|
|
126
|
+
if (!explicit) return join(homedir(), '.dsh');
|
|
127
|
+
if (explicit === '~') return homedir();
|
|
128
|
+
if (explicit.startsWith('~/') || explicit.startsWith('~\\')) {
|
|
129
|
+
return resolve(homedir(), explicit.slice(2));
|
|
130
|
+
}
|
|
131
|
+
return resolve(explicit);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function getDshSessionsDir() {
|
|
135
|
+
const testDir = process.env.VIBE_USAGE_DSH_SESSIONS?.trim();
|
|
136
|
+
if (testDir) return testDir;
|
|
137
|
+
return join(getDshHome(), 'sessions');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Detect DeepSeek Harness when its sessions tree exists (or the test override).
|
|
141
|
+
export function findDshDataDirs() {
|
|
142
|
+
return [getDshSessionsDir()].filter(existsSync);
|
|
143
|
+
}
|
|
144
|
+
|
|
123
145
|
export function getMimocodeDbPath(env = process.env) {
|
|
124
146
|
if (env.MIMOCODE_HOME && !isAbsolute(env.MIMOCODE_HOME)) {
|
|
125
147
|
throw new Error(`MIMOCODE_HOME must be an absolute path, got: ${JSON.stringify(env.MIMOCODE_HOME)}`);
|
|
@@ -298,6 +320,12 @@ export const TOOLS = [
|
|
|
298
320
|
id: 'droid',
|
|
299
321
|
dataDir: join(homedir(), '.factory', 'sessions'),
|
|
300
322
|
},
|
|
323
|
+
{
|
|
324
|
+
name: 'DeepSeek Harness',
|
|
325
|
+
id: 'dsh',
|
|
326
|
+
dataDir: getDshSessionsDir(),
|
|
327
|
+
detectDataDirs: findDshDataDirs,
|
|
328
|
+
},
|
|
301
329
|
{
|
|
302
330
|
name: 'Antigravity',
|
|
303
331
|
id: 'antigravity',
|