@vibe-cafe/vibe-usage 0.10.10 → 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 +1 -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 +1 -1
- 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 +1 -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/parsers/dimagent.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
3
|
-
import {
|
|
2
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
3
|
+
import { toCount } from './fs-utils.js';
|
|
4
|
+
import { queryDbJson, sqliteUnavailableError, isSqliteUnavailableError } from './sqlite.js';
|
|
4
5
|
import { getDimAgentDbPath } from '../tools.js';
|
|
5
6
|
|
|
6
7
|
const SOURCE = 'dimagent';
|
|
@@ -12,11 +13,6 @@ function projectName(cwd) {
|
|
|
12
13
|
return parts.at(-1) || 'unknown';
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
function tokenCount(value) {
|
|
16
|
-
const count = Number(value);
|
|
17
|
-
return Number.isFinite(count) && count > 0 ? count : 0;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
16
|
function usageSignature(row) {
|
|
21
17
|
return [
|
|
22
18
|
row.runId || '',
|
|
@@ -55,10 +51,10 @@ function parseUsageRows(rows) {
|
|
|
55
51
|
const timestamp = new Date(row.createdAt);
|
|
56
52
|
if (Number.isNaN(timestamp.getTime())) continue;
|
|
57
53
|
|
|
58
|
-
const promptTokens =
|
|
59
|
-
const cachedInputTokens =
|
|
54
|
+
const promptTokens = toCount(usage.promptTokens);
|
|
55
|
+
const cachedInputTokens = toCount(usage.cacheReadTokens);
|
|
60
56
|
const inputTokens = Math.max(0, promptTokens - cachedInputTokens);
|
|
61
|
-
const outputTokens =
|
|
57
|
+
const outputTokens = toCount(usage.completionTokens);
|
|
62
58
|
if (inputTokens + outputTokens + cachedInputTokens === 0) continue;
|
|
63
59
|
|
|
64
60
|
entries.push({
|
|
@@ -80,9 +76,7 @@ function queryDb(dbPath, sql) {
|
|
|
80
76
|
try {
|
|
81
77
|
return queryDbJson(dbPath, sql);
|
|
82
78
|
} catch (err) {
|
|
83
|
-
if (err
|
|
84
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync DimAgent data.');
|
|
85
|
-
}
|
|
79
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('DimAgent');
|
|
86
80
|
throw err;
|
|
87
81
|
}
|
|
88
82
|
}
|
package/src/parsers/droid.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join, basename, dirname } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
5
|
|
|
6
6
|
const DROID_SESSIONS_DIR = join(homedir(), '.factory', 'sessions');
|
|
7
7
|
|
package/src/parsers/dsh.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
3
3
|
import { basename, join, relative } from 'node:path';
|
|
4
4
|
import zlib from 'node:zlib';
|
|
5
5
|
import { getDshSessionsDir } from '../tools.js';
|
|
6
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
6
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
7
7
|
|
|
8
8
|
const SOURCE = 'dsh';
|
|
9
9
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Small shared filesystem/parsing helpers used across parsers. Keeping them in
|
|
5
|
+
// one place removes the same ~10-line functions copied into every parser.
|
|
6
|
+
|
|
7
|
+
/** Read and parse a JSON file, returning null on any failure. */
|
|
8
|
+
export function readJsonSafe(path) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Last path component (project name), or 'unknown'. */
|
|
17
|
+
export function projectFromPath(absPath) {
|
|
18
|
+
if (!absPath || typeof absPath !== 'string') return 'unknown';
|
|
19
|
+
const trimmed = absPath.replace(/[\\/]+$/, '');
|
|
20
|
+
const name = basename(trimmed);
|
|
21
|
+
return name || 'unknown';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Last path component of a cwd value (works for both Unix and Windows paths). */
|
|
25
|
+
export function projectFromCwd(cwd, fallback = 'unknown') {
|
|
26
|
+
if (typeof cwd !== 'string') return fallback;
|
|
27
|
+
const trimmed = cwd.trim().replace(/[\\/]+$/, '');
|
|
28
|
+
if (!trimmed) return fallback;
|
|
29
|
+
return trimmed.split(/[\\/]/).filter(Boolean).at(-1) || fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Coerce a token count to a finite positive number, else 0. */
|
|
33
|
+
export function toCount(value) {
|
|
34
|
+
const n = Number(value);
|
|
35
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
36
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join, basename } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
5
|
|
|
6
6
|
const TMP_DIR = join(homedir(), '.gemini', 'tmp');
|
|
7
7
|
|
package/src/parsers/grok.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createReadStream, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import {
|
|
3
|
+
import { join } from 'node:path';
|
|
4
4
|
import { findGrokDataDirs, getGrokSessionsDir } from '../tools.js';
|
|
5
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
5
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
6
|
+
import { readJsonSafe, projectFromPath } from './fs-utils.js';
|
|
6
7
|
|
|
7
8
|
const SOURCE = 'grok';
|
|
8
9
|
|
|
@@ -23,21 +24,6 @@ const SOURCE = 'grok';
|
|
|
23
24
|
* reads), matching Codex/Copilot so totalTokens does not double-count cache.
|
|
24
25
|
*/
|
|
25
26
|
|
|
26
|
-
function readJsonSafe(path) {
|
|
27
|
-
try {
|
|
28
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
29
|
-
} catch {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function projectFromPath(absPath) {
|
|
35
|
-
if (!absPath || typeof absPath !== 'string') return 'unknown';
|
|
36
|
-
const trimmed = absPath.replace(/[\\/]+$/, '');
|
|
37
|
-
const name = basename(trimmed);
|
|
38
|
-
return name || 'unknown';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
27
|
/** Decode a sessions group dirname; fall back to basename after decode. */
|
|
42
28
|
function projectFromGroupDir(groupName, groupPath) {
|
|
43
29
|
const cwdFile = join(groupPath, '.cwd');
|
package/src/parsers/hermes.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
5
|
-
import { queryDbJson } from './sqlite.js';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
|
+
import { queryDbJson, sqliteUnavailableError, isSqliteUnavailableError } from './sqlite.js';
|
|
6
6
|
|
|
7
7
|
const HERMES_HOME = process.env.HERMES_HOME || join(homedir(), '.hermes');
|
|
8
8
|
|
|
@@ -37,9 +37,7 @@ export async function parse() {
|
|
|
37
37
|
FROM sessions
|
|
38
38
|
WHERE input_tokens > 0 OR output_tokens > 0`);
|
|
39
39
|
} catch (err) {
|
|
40
|
-
if (err
|
|
41
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Hermes data.');
|
|
42
|
-
}
|
|
40
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('Hermes');
|
|
43
41
|
throw err;
|
|
44
42
|
}
|
|
45
43
|
|
package/src/parsers/index.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
import { parse as parseClaudeCode } from './claude-code.js';
|
|
3
2
|
import { parse as parseCline } from './cline.js';
|
|
4
3
|
import { parse as parseCodex } from './codex.js';
|
|
@@ -57,150 +56,4 @@ export const parsers = {
|
|
|
57
56
|
'zcode': parseZcode,
|
|
58
57
|
};
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
export function roundToHalfHour(date) {
|
|
62
|
-
const d = new Date(date);
|
|
63
|
-
d.setMinutes(d.getMinutes() < 30 ? 0 : 30, 0, 0);
|
|
64
|
-
return d;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Server column limits (usage_buckets: model varchar(100), project varchar(200)).
|
|
68
|
-
// Anything longer aborts the whole INSERT chunk with 22001, so clamp here.
|
|
69
|
-
const MODEL_MAX_LENGTH = 100;
|
|
70
|
-
const PROJECT_MAX_LENGTH = 200;
|
|
71
|
-
|
|
72
|
-
// Server token columns are bigint — a single fractional/NaN value aborts the
|
|
73
|
-
// whole INSERT chunk with 22P02, taking every other tool's rows in the batch
|
|
74
|
-
// down with it.
|
|
75
|
-
function toTokenCount(value) {
|
|
76
|
-
const n = Number(value);
|
|
77
|
-
if (!Number.isFinite(n) || n <= 0) return 0;
|
|
78
|
-
return Math.round(n);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function aggregateToBuckets(entries) {
|
|
82
|
-
const map = new Map();
|
|
83
|
-
|
|
84
|
-
for (const e of entries) {
|
|
85
|
-
const model = String(e.model || 'unknown').slice(0, MODEL_MAX_LENGTH);
|
|
86
|
-
const project = String(e.project || 'unknown').slice(0, PROJECT_MAX_LENGTH);
|
|
87
|
-
const bucketStart = roundToHalfHour(e.timestamp).toISOString();
|
|
88
|
-
const key = `${e.source}|${model}|${project}|${e.hostname || ''}|${bucketStart}`;
|
|
89
|
-
|
|
90
|
-
if (!map.has(key)) {
|
|
91
|
-
map.set(key, {
|
|
92
|
-
source: e.source,
|
|
93
|
-
model,
|
|
94
|
-
project,
|
|
95
|
-
// Cloud-sourced parsers (cursor) pre-set a fixed hostname sentinel; it
|
|
96
|
-
// must survive aggregation, or sync.js stamps the machine hostname and
|
|
97
|
-
// every machine gets its own duplicate row server-side.
|
|
98
|
-
...(e.hostname ? { hostname: e.hostname } : {}),
|
|
99
|
-
bucketStart,
|
|
100
|
-
inputTokens: 0,
|
|
101
|
-
outputTokens: 0,
|
|
102
|
-
cachedInputTokens: 0,
|
|
103
|
-
reasoningOutputTokens: 0,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const b = map.get(key);
|
|
108
|
-
b.inputTokens += e.inputTokens || 0;
|
|
109
|
-
b.outputTokens += e.outputTokens || 0;
|
|
110
|
-
b.cachedInputTokens += e.cachedInputTokens || 0;
|
|
111
|
-
b.reasoningOutputTokens += e.reasoningOutputTokens || 0;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Clamp after summation, not per entry — rounding each entry first would
|
|
115
|
-
// discard sub-integer values instead of letting them accumulate.
|
|
116
|
-
return Array.from(map.values()).map((b) => {
|
|
117
|
-
const inputTokens = toTokenCount(b.inputTokens);
|
|
118
|
-
const outputTokens = toTokenCount(b.outputTokens);
|
|
119
|
-
const cachedInputTokens = toTokenCount(b.cachedInputTokens);
|
|
120
|
-
const reasoningOutputTokens = toTokenCount(b.reasoningOutputTokens);
|
|
121
|
-
return {
|
|
122
|
-
...b,
|
|
123
|
-
inputTokens,
|
|
124
|
-
outputTokens,
|
|
125
|
-
cachedInputTokens,
|
|
126
|
-
reasoningOutputTokens,
|
|
127
|
-
totalTokens: inputTokens + outputTokens + reasoningOutputTokens,
|
|
128
|
-
};
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Extract session metadata from timing events.
|
|
134
|
-
* Each event: { sessionId, source, project, timestamp: Date, role: 'user'|'assistant' }
|
|
135
|
-
*
|
|
136
|
-
* Turn = first AI response → last AI response before next user prompt.
|
|
137
|
-
* activeSeconds = sum(generation durations), excluding queue/TTFT wait.
|
|
138
|
-
* durationSeconds = wall clock from first to last message.
|
|
139
|
-
*/
|
|
140
|
-
export function extractSessions(events) {
|
|
141
|
-
const groups = new Map();
|
|
142
|
-
for (const e of events) {
|
|
143
|
-
if (!groups.has(e.sessionId)) groups.set(e.sessionId, []);
|
|
144
|
-
groups.get(e.sessionId).push(e);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const sessions = [];
|
|
148
|
-
for (const [sessionId, sessionEvents] of groups) {
|
|
149
|
-
sessionEvents.sort((a, b) => a.timestamp - b.timestamp);
|
|
150
|
-
|
|
151
|
-
const first = sessionEvents[0];
|
|
152
|
-
const last = sessionEvents[sessionEvents.length - 1];
|
|
153
|
-
const durationSeconds = Math.round((last.timestamp - first.timestamp) / 1000);
|
|
154
|
-
|
|
155
|
-
let activeSeconds = 0;
|
|
156
|
-
let turnStart = null;
|
|
157
|
-
let turnEnd = null;
|
|
158
|
-
let waitingForFirstResponse = false;
|
|
159
|
-
|
|
160
|
-
for (const event of sessionEvents) {
|
|
161
|
-
if (event.role === 'user') {
|
|
162
|
-
if (turnStart !== null && turnEnd !== null && turnEnd > turnStart) {
|
|
163
|
-
activeSeconds += Math.round((turnEnd - turnStart) / 1000);
|
|
164
|
-
}
|
|
165
|
-
turnStart = null;
|
|
166
|
-
turnEnd = null;
|
|
167
|
-
waitingForFirstResponse = true;
|
|
168
|
-
} else if (waitingForFirstResponse) {
|
|
169
|
-
turnStart = event.timestamp;
|
|
170
|
-
turnEnd = event.timestamp;
|
|
171
|
-
waitingForFirstResponse = false;
|
|
172
|
-
} else if (turnStart !== null) {
|
|
173
|
-
turnEnd = event.timestamp;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
if (turnStart !== null && turnEnd !== null && turnEnd > turnStart) {
|
|
177
|
-
activeSeconds += Math.round((turnEnd - turnStart) / 1000);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const userPromptHours = new Array(24).fill(0);
|
|
181
|
-
let userMessageCount = 0;
|
|
182
|
-
for (const event of sessionEvents) {
|
|
183
|
-
if (event.role === 'user') {
|
|
184
|
-
userMessageCount++;
|
|
185
|
-
userPromptHours[event.timestamp.getUTCHours()]++;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const sessionHash = createHash('sha256').update(sessionId).digest('hex').slice(0, 16);
|
|
190
|
-
|
|
191
|
-
sessions.push({
|
|
192
|
-
source: first.source,
|
|
193
|
-
project: first.project || 'unknown',
|
|
194
|
-
sessionHash,
|
|
195
|
-
firstMessageAt: first.timestamp.toISOString(),
|
|
196
|
-
lastMessageAt: last.timestamp.toISOString(),
|
|
197
|
-
durationSeconds,
|
|
198
|
-
activeSeconds,
|
|
199
|
-
messageCount: sessionEvents.length,
|
|
200
|
-
userMessageCount,
|
|
201
|
-
userPromptHours,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return sessions;
|
|
206
|
-
}
|
|
59
|
+
export { roundToHalfHour, aggregateToBuckets, extractSessions } from './aggregate.js';
|
package/src/parsers/kimi-code.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
|
2
2
|
import { join, basename } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
5
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Kimi Code CLI parser. MoonshotAI/kimi-cli (a.k.a. "Kimi Code").
|
package/src/parsers/kiro.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
|
-
copyFileSync,
|
|
3
2
|
createReadStream,
|
|
4
3
|
existsSync,
|
|
5
|
-
mkdtempSync,
|
|
6
4
|
readFileSync,
|
|
7
5
|
readdirSync,
|
|
8
|
-
rmSync,
|
|
9
6
|
statSync,
|
|
10
7
|
} from 'node:fs';
|
|
11
8
|
import { createInterface } from 'node:readline';
|
|
12
9
|
import { dirname, join, resolve } from 'node:path';
|
|
13
|
-
import { homedir
|
|
14
|
-
import { aggregateToBuckets } from './
|
|
15
|
-
import {
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { aggregateToBuckets } from './aggregate.js';
|
|
12
|
+
import { queryDbJsonSnapshotOnLock, sqliteUnavailableError, isSqliteUnavailableError } from './sqlite.js';
|
|
16
13
|
|
|
17
14
|
const KIRO_AGENT_RELATIVE = join('User', 'globalStorage', 'kiro.kiroagent');
|
|
18
15
|
const KIRO_USER_RELATIVE = 'User';
|
|
@@ -122,37 +119,9 @@ export function getKiroCliSessionsDir() {
|
|
|
122
119
|
return existsSync(def) ? def : null;
|
|
123
120
|
}
|
|
124
121
|
|
|
125
|
-
function isLockError(err) {
|
|
126
|
-
return err && typeof err.message === 'string' && /database is locked/i.test(err.message);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function queryDb(dbPath, sql) {
|
|
130
|
-
return queryDbJson(dbPath, sql);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function queryDbSnapshotOnLock(dbPath, sql) {
|
|
134
|
-
try {
|
|
135
|
-
return queryDb(dbPath, sql);
|
|
136
|
-
} catch (err) {
|
|
137
|
-
if (!isLockError(err)) throw err;
|
|
138
|
-
const snapshotDir = mkdtempSync(join(tmpdir(), 'vibe-usage-kiro-'));
|
|
139
|
-
const queryPath = join(snapshotDir, 'data.sqlite3');
|
|
140
|
-
copyFileSync(dbPath, queryPath);
|
|
141
|
-
for (const suffix of ['-shm', '-wal']) {
|
|
142
|
-
const companion = `${dbPath}${suffix}`;
|
|
143
|
-
if (existsSync(companion)) copyFileSync(companion, `${queryPath}${suffix}`);
|
|
144
|
-
}
|
|
145
|
-
try {
|
|
146
|
-
return queryDb(queryPath, sql);
|
|
147
|
-
} finally {
|
|
148
|
-
rmSync(snapshotDir, { recursive: true, force: true });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
122
|
function queryOptionalDb(dbPath, sql) {
|
|
154
123
|
try {
|
|
155
|
-
return
|
|
124
|
+
return queryDbJsonSnapshotOnLock(dbPath, sql, { tempPrefix: 'vibe-usage-kiro-' });
|
|
156
125
|
} catch (err) {
|
|
157
126
|
const msg = err && typeof err.message === 'string' ? err.message : '';
|
|
158
127
|
if (/no such table|no such column/i.test(msg)) return [];
|
|
@@ -167,7 +136,7 @@ const TOKENS_SQL =
|
|
|
167
136
|
'ORDER BY id ASC';
|
|
168
137
|
|
|
169
138
|
function readLegacyDb(dbPath) {
|
|
170
|
-
return
|
|
139
|
+
return queryDbJsonSnapshotOnLock(dbPath, TOKENS_SQL, { tempPrefix: 'vibe-usage-kiro-' });
|
|
171
140
|
}
|
|
172
141
|
|
|
173
142
|
// Legacy Kiro dev telemetry fallback. This is opt-in because recent Kiro builds
|
|
@@ -790,9 +759,7 @@ export async function parse() {
|
|
|
790
759
|
return { buckets: aggregateToBuckets(estimateEntries), sessions: [] };
|
|
791
760
|
}
|
|
792
761
|
} catch (err) {
|
|
793
|
-
if (err
|
|
794
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync Kiro CLI data.');
|
|
795
|
-
}
|
|
762
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('Kiro CLI');
|
|
796
763
|
throw err;
|
|
797
764
|
}
|
|
798
765
|
|
package/src/parsers/mimocode.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { basename } from 'node:path';
|
|
3
3
|
import { getMimocodeDbPath } from '../tools.js';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
5
|
-
import { queryDbJson } from './sqlite.js';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
|
+
import { queryDbJson, sqliteUnavailableError, isSqliteUnavailableError } from './sqlite.js';
|
|
6
6
|
|
|
7
7
|
export { getMimocodeDbPath as resolveMimocodeDbPath };
|
|
8
8
|
|
|
@@ -36,9 +36,7 @@ export async function parse() {
|
|
|
36
36
|
${externalImportFilter}
|
|
37
37
|
`);
|
|
38
38
|
} catch (err) {
|
|
39
|
-
if (err
|
|
40
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync MiMoCode data.');
|
|
41
|
-
}
|
|
39
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('MiMoCode');
|
|
42
40
|
throw err;
|
|
43
41
|
}
|
|
44
42
|
|
package/src/parsers/openclaw.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
5
|
|
|
6
6
|
// OpenClaw stores data at ~/.openclaw/agents/<agentId>/sessions/*.jsonl
|
|
7
7
|
// Profile deployments use ~/.openclaw-<profile>/agents/...
|
package/src/parsers/opencode.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join, basename } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
5
|
-
import { queryDbJson } from './sqlite.js';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
|
+
import { queryDbJson, sqliteUnavailableError, isSqliteUnavailableError } from './sqlite.js';
|
|
6
6
|
|
|
7
7
|
const DATA_DIR = join(homedir(), '.local', 'share', 'opencode');
|
|
8
8
|
const DB_PATH = join(DATA_DIR, 'opencode.db');
|
|
@@ -37,9 +37,7 @@ function parseFromSqlite() {
|
|
|
37
37
|
try {
|
|
38
38
|
rows = queryDbJson(DB_PATH, query);
|
|
39
39
|
} catch (err) {
|
|
40
|
-
if (
|
|
41
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync opencode data.');
|
|
42
|
-
}
|
|
40
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('OpenCode');
|
|
43
41
|
throw err;
|
|
44
42
|
}
|
|
45
43
|
if (!rows.length) return { buckets: [], sessions: [] };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { basename, join, relative } from 'node:path';
|
|
3
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
3
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
4
|
+
import { projectFromCwd, toCount } from './fs-utils.js';
|
|
4
5
|
|
|
5
6
|
const MAX_WARNINGS = 20;
|
|
6
7
|
|
|
@@ -28,17 +29,6 @@ function findJsonlFiles(dir, includeFile, ctx) {
|
|
|
28
29
|
return files;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function tokenCount(value) {
|
|
32
|
-
const number = Number(value);
|
|
33
|
-
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function projectFromCwd(cwd) {
|
|
37
|
-
if (typeof cwd !== 'string') return 'unknown';
|
|
38
|
-
const parts = cwd.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
39
|
-
return parts.at(-1) || 'unknown';
|
|
40
|
-
}
|
|
41
|
-
|
|
42
32
|
export function projectFromFirstDir(filePath, sessionsDir) {
|
|
43
33
|
const first = relative(sessionsDir, filePath).split(/[\\/]/)[0];
|
|
44
34
|
if (!first) return 'unknown';
|
|
@@ -105,12 +95,12 @@ export async function parsePiSessionJsonl({
|
|
|
105
95
|
|
|
106
96
|
if (message.role !== 'assistant' || !message.usage) continue;
|
|
107
97
|
const usage = message.usage;
|
|
108
|
-
const inputTokens =
|
|
109
|
-
const reasoningOutputTokens =
|
|
98
|
+
const inputTokens = toCount(usage.input) + toCount(usage.cacheWrite);
|
|
99
|
+
const reasoningOutputTokens = toCount(usage.reasoningTokens);
|
|
110
100
|
// OMP/Pi usage.output includes reasoning; the shared bucket contract
|
|
111
101
|
// stores non-reasoning output and reasoning separately.
|
|
112
|
-
const outputTokens = Math.max(0,
|
|
113
|
-
const cachedInputTokens =
|
|
102
|
+
const outputTokens = Math.max(0, toCount(usage.output) - reasoningOutputTokens);
|
|
103
|
+
const cachedInputTokens = toCount(usage.cacheRead);
|
|
114
104
|
const score = inputTokens + outputTokens + cachedInputTokens + reasoningOutputTokens;
|
|
115
105
|
if (score === 0) continue;
|
|
116
106
|
|
package/src/parsers/qwen-code.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { join, basename, sep } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Qwen Code parser (Gemini CLI fork).
|
package/src/parsers/roo-code.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
|
+
import { readJsonSafe, projectFromPath } from './fs-utils.js';
|
|
5
6
|
|
|
6
7
|
const EXTENSION_ID = 'rooveterinaryinc.roo-cline';
|
|
7
8
|
|
|
@@ -35,17 +36,6 @@ export function findRooCodeExtensionDirs() {
|
|
|
35
36
|
return dirs;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function readJsonSafe(path) {
|
|
39
|
-
try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function projectFromPath(absPath) {
|
|
43
|
-
if (!absPath || typeof absPath !== 'string') return 'unknown';
|
|
44
|
-
const trimmed = absPath.replace(/[\\/]+$/, '');
|
|
45
|
-
const name = basename(trimmed);
|
|
46
|
-
return name || 'unknown';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
39
|
// Read all HistoryItems from `_index.json` if present, else fall back to
|
|
50
40
|
// scanning per-task `history_item.json` files (Roo migrated to per-task
|
|
51
41
|
// files in 2025; the index is a cache).
|
package/src/parsers/sqlite.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
+
import { copyFileSync, existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { basename, join } from 'node:path';
|
|
3
6
|
|
|
4
7
|
const require = createRequire(import.meta.url);
|
|
5
8
|
|
|
@@ -72,3 +75,46 @@ function queryViaCli(dbPath, sql, { timeout, maxBuffer }) {
|
|
|
72
75
|
if (!trimmed || trimmed === '[]') return [];
|
|
73
76
|
return JSON.parse(trimmed);
|
|
74
77
|
}
|
|
78
|
+
|
|
79
|
+
/** Standard "sqlite3 unavailable" hint, reused by every SQLite-backed parser. */
|
|
80
|
+
export function sqliteUnavailableError(label) {
|
|
81
|
+
return new Error(`sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync ${label} data.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** True when the error is the "no sqlite3" hint (node:sqlite absent + CLI absent). */
|
|
85
|
+
export function isSqliteUnavailableError(err) {
|
|
86
|
+
return !!err && (
|
|
87
|
+
err.code === 'ENOENT'
|
|
88
|
+
|| err.status === 127
|
|
89
|
+
|| /ENOENT|sqlite3.*not found/i.test(err?.message || '')
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isLockError(err) {
|
|
94
|
+
return !!err && typeof err.message === 'string' && /database is locked/i.test(err.message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Run a query, and if the source app holds a write lock on the database, copy
|
|
99
|
+
* the DB (plus its -wal/-shm companions) to a temp dir and re-query the
|
|
100
|
+
* snapshot. Shared by Cursor / Antigravity / Kiro.
|
|
101
|
+
*/
|
|
102
|
+
export function queryDbJsonSnapshotOnLock(dbPath, sql, { tempPrefix = 'vibe-usage-sqlite', opts } = {}) {
|
|
103
|
+
try {
|
|
104
|
+
return queryDbJson(dbPath, sql, opts);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (!isLockError(err)) throw err;
|
|
107
|
+
const snapshotDir = mkdtempSync(join(tmpdir(), tempPrefix));
|
|
108
|
+
const queryPath = join(snapshotDir, basename(dbPath));
|
|
109
|
+
try {
|
|
110
|
+
copyFileSync(dbPath, queryPath);
|
|
111
|
+
for (const suffix of ['-shm', '-wal']) {
|
|
112
|
+
const companion = `${dbPath}${suffix}`;
|
|
113
|
+
if (existsSync(companion)) copyFileSync(companion, `${queryPath}${suffix}`);
|
|
114
|
+
}
|
|
115
|
+
return queryDbJson(queryPath, sql, opts);
|
|
116
|
+
} finally {
|
|
117
|
+
rmSync(snapshotDir, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/parsers/trae-cli.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { join } from 'node:path';
|
|
3
3
|
import { findTraeCliDataDirs } from '../tools.js';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
5
|
-
|
|
6
|
-
function readJsonSafe(path) {
|
|
7
|
-
try { return JSON.parse(readFileSync(path, 'utf-8')); } catch { return null; }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function projectFromPath(absPath) {
|
|
11
|
-
if (!absPath || typeof absPath !== 'string') return 'unknown';
|
|
12
|
-
const trimmed = absPath.replace(/[\\/]+$/, '');
|
|
13
|
-
const name = basename(trimmed);
|
|
14
|
-
return name || 'unknown';
|
|
15
|
-
}
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
|
+
import { readJsonSafe, projectFromPath } from './fs-utils.js';
|
|
16
6
|
|
|
17
7
|
function parseJsonlSafe(path) {
|
|
18
8
|
try {
|
package/src/parsers/workbuddy.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createReadStream, readdirSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
3
|
import { basename, join, relative, sep } from 'node:path';
|
|
4
4
|
import { findWorkbuddyDataDirs } from '../workbuddy-roots.js';
|
|
5
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
5
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
6
6
|
|
|
7
7
|
const SOURCE = 'workbuddy';
|
|
8
8
|
const MAX_WARNINGS = 20;
|
package/src/parsers/zcode.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join, basename } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { aggregateToBuckets, extractSessions } from './
|
|
5
|
-
import { queryDbJson } from './sqlite.js';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
5
|
+
import { queryDbJson, sqliteUnavailableError, isSqliteUnavailableError } from './sqlite.js';
|
|
6
6
|
|
|
7
7
|
// ZCode (z.ai / Zhipu's coding agent) stores everything in a SQLite database
|
|
8
8
|
// at ~/.zcode/cli/db/db.sqlite. The `message` table is the canonical source:
|
|
@@ -46,9 +46,7 @@ export async function parse() {
|
|
|
46
46
|
try {
|
|
47
47
|
rows = queryDbJson(DB_PATH, query);
|
|
48
48
|
} catch (err) {
|
|
49
|
-
if (
|
|
50
|
-
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync ZCode data.');
|
|
51
|
-
}
|
|
49
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('ZCode');
|
|
52
50
|
throw err;
|
|
53
51
|
}
|
|
54
52
|
if (!rows.length) return { buckets: [], sessions: [] };
|