@vibe-cafe/vibe-usage 0.10.18 → 0.10.20
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 +27 -6
- package/package.json +1 -1
- package/src/daemon-service.js +347 -7
- package/src/extra-roots.js +107 -0
- package/src/index.js +70 -3
- package/src/init.js +6 -2
- package/src/parsers/antigravity-db.js +9 -8
- package/src/parsers/antigravity.js +88 -11
- package/src/parsers/codex-cache.js +1 -1
- package/src/parsers/codex.js +74 -42
- package/src/parsers/cursor.js +21 -2
- package/src/parsers/grok.js +102 -18
- package/src/parsers/index.js +2 -0
- package/src/parsers/mcode.js +182 -0
- package/src/parsers/pi-session-jsonl.js +4 -1
- package/src/pi-roots.js +33 -10
- package/src/sync.js +14 -3
- package/src/tools.js +43 -11
package/src/parsers/grok.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { createReadStream, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { createReadStream, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { findGrokDataDirs, getGrokSessionsDir } from '../tools.js';
|
|
5
|
+
import { grokSessionsDir, normalizeExtraRoot } from '../extra-roots.js';
|
|
5
6
|
import { aggregateToBuckets, extractSessions } from './aggregate.js';
|
|
6
7
|
import { readJsonSafe, projectFromPath } from './fs-utils.js';
|
|
7
8
|
|
|
@@ -25,14 +26,14 @@ const SOURCE = 'grok';
|
|
|
25
26
|
*/
|
|
26
27
|
|
|
27
28
|
/** Decode a sessions group dirname; fall back to basename after decode. */
|
|
28
|
-
function projectFromGroupDir(groupName, groupPath) {
|
|
29
|
+
function projectFromGroupDir(groupName, groupPath, strict = false) {
|
|
29
30
|
const cwdFile = join(groupPath, '.cwd');
|
|
30
31
|
if (existsSync(cwdFile)) {
|
|
31
32
|
try {
|
|
32
33
|
const raw = readFileSync(cwdFile, 'utf-8').trim();
|
|
33
34
|
if (raw) return projectFromPath(raw);
|
|
34
|
-
} catch {
|
|
35
|
-
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (strict) throw err;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
try {
|
|
@@ -115,12 +116,13 @@ function emitTurnUsage(entries, { usage, project, timestamp, fallbackModel }) {
|
|
|
115
116
|
});
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
async function forEachJsonlLine(filePath, onLine) {
|
|
119
|
+
async function forEachJsonlLine(filePath, onLine, strict = false) {
|
|
119
120
|
if (!existsSync(filePath)) return;
|
|
120
121
|
let stream;
|
|
121
122
|
try {
|
|
122
123
|
stream = createReadStream(filePath, { encoding: 'utf-8' });
|
|
123
|
-
} catch {
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (strict) throw err;
|
|
124
126
|
return;
|
|
125
127
|
}
|
|
126
128
|
|
|
@@ -137,7 +139,8 @@ async function forEachJsonlLine(filePath, onLine) {
|
|
|
137
139
|
}
|
|
138
140
|
onLine(obj);
|
|
139
141
|
}
|
|
140
|
-
} catch {
|
|
142
|
+
} catch (err) {
|
|
143
|
+
if (strict) throw err;
|
|
141
144
|
// unreadable / truncated mid-write — keep what we have
|
|
142
145
|
} finally {
|
|
143
146
|
rl.close();
|
|
@@ -145,14 +148,18 @@ async function forEachJsonlLine(filePath, onLine) {
|
|
|
145
148
|
}
|
|
146
149
|
}
|
|
147
150
|
|
|
148
|
-
function listSessionDirs(sessionsDir) {
|
|
151
|
+
function listSessionDirs(sessionsDir, strict = false) {
|
|
149
152
|
const results = [];
|
|
150
|
-
if (!existsSync(sessionsDir))
|
|
153
|
+
if (!existsSync(sessionsDir)) {
|
|
154
|
+
if (strict) throw new Error(`missing sessions directory: ${sessionsDir}`);
|
|
155
|
+
return results;
|
|
156
|
+
}
|
|
151
157
|
|
|
152
158
|
let groups;
|
|
153
159
|
try {
|
|
154
160
|
groups = readdirSync(sessionsDir, { withFileTypes: true });
|
|
155
|
-
} catch {
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (strict) throw err;
|
|
156
163
|
return results;
|
|
157
164
|
}
|
|
158
165
|
|
|
@@ -163,11 +170,12 @@ function listSessionDirs(sessionsDir) {
|
|
|
163
170
|
let children;
|
|
164
171
|
try {
|
|
165
172
|
children = readdirSync(groupPath, { withFileTypes: true });
|
|
166
|
-
} catch {
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (strict) throw err;
|
|
167
175
|
continue;
|
|
168
176
|
}
|
|
169
177
|
|
|
170
|
-
const projectFallback = projectFromGroupDir(group.name, groupPath);
|
|
178
|
+
const projectFallback = projectFromGroupDir(group.name, groupPath, strict);
|
|
171
179
|
|
|
172
180
|
for (const child of children) {
|
|
173
181
|
if (!child.isDirectory()) continue;
|
|
@@ -194,8 +202,29 @@ function listSessionDirs(sessionsDir) {
|
|
|
194
202
|
* Parse all Grok sessions under the configured sessions root(s).
|
|
195
203
|
* @returns {Promise<{ buckets: object[], sessions: object[] }>}
|
|
196
204
|
*/
|
|
197
|
-
export async function parse() {
|
|
198
|
-
|
|
205
|
+
export async function parse({ extraRoots = [] } = {}) {
|
|
206
|
+
if (!process.env.VIBE_USAGE_GROK_SESSIONS?.trim()) {
|
|
207
|
+
for (const root of extraRoots) {
|
|
208
|
+
const sessionsDir = grokSessionsDir(root);
|
|
209
|
+
try {
|
|
210
|
+
if (!statSync(sessionsDir).isDirectory()) throw new Error('not a directory');
|
|
211
|
+
} catch {
|
|
212
|
+
return {
|
|
213
|
+
buckets: [],
|
|
214
|
+
sessions: [],
|
|
215
|
+
skipped: true,
|
|
216
|
+
warnings: [`grok: 额外根目录不可用,已跳过本次 Grok 同步: ${normalizeExtraRoot(root)}`],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const strictRoots = process.env.VIBE_USAGE_GROK_SESSIONS?.trim()
|
|
222
|
+
? new Set()
|
|
223
|
+
: new Set(extraRoots.map(grokSessionsDir));
|
|
224
|
+
const sessionRoots = findGrokDataDirs(extraRoots);
|
|
225
|
+
for (const configuredRoot of strictRoots) {
|
|
226
|
+
if (!sessionRoots.includes(configuredRoot)) sessionRoots.push(configuredRoot);
|
|
227
|
+
}
|
|
199
228
|
// findGrokDataDirs returns sessions dirs; also allow empty → try default once
|
|
200
229
|
const roots = sessionRoots.length > 0 ? sessionRoots : [getGrokSessionsDir()].filter(existsSync);
|
|
201
230
|
if (roots.length === 0) return { buckets: [], sessions: [] };
|
|
@@ -203,9 +232,58 @@ export async function parse() {
|
|
|
203
232
|
const entries = [];
|
|
204
233
|
const sessionEvents = [];
|
|
205
234
|
|
|
235
|
+
const candidates = [];
|
|
206
236
|
for (const sessionsDir of roots) {
|
|
207
|
-
|
|
208
|
-
|
|
237
|
+
const strict = strictRoots.has(sessionsDir);
|
|
238
|
+
try {
|
|
239
|
+
for (const session of listSessionDirs(sessionsDir, strict)) {
|
|
240
|
+
candidates.push({ ...session, strict, configuredRoot: sessionsDir });
|
|
241
|
+
}
|
|
242
|
+
} catch {
|
|
243
|
+
return {
|
|
244
|
+
buckets: [], sessions: [], skipped: true,
|
|
245
|
+
warnings: [`grok: 额外根目录读取失败,已保留上次同步数据: ${sessionsDir}`],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let sessionsToParse = candidates;
|
|
251
|
+
if (roots.length > 1) {
|
|
252
|
+
const selectedSessions = new Map();
|
|
253
|
+
for (const session of candidates) {
|
|
254
|
+
const fileSize = (name) => {
|
|
255
|
+
try {
|
|
256
|
+
return statSync(join(session.sessionPath, name)).size;
|
|
257
|
+
} catch {
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
const score = [fileSize('updates.jsonl'), fileSize('events.jsonl'), fileSize('summary.json')];
|
|
262
|
+
const previous = selectedSessions.get(session.sessionId);
|
|
263
|
+
const moreComplete = !previous || score.some((value, index) => (
|
|
264
|
+
value !== previous.score[index] && value > previous.score[index]
|
|
265
|
+
&& score.slice(0, index).every((prior, priorIndex) => prior === previous.score[priorIndex])
|
|
266
|
+
));
|
|
267
|
+
if (moreComplete) selectedSessions.set(session.sessionId, { ...session, score });
|
|
268
|
+
}
|
|
269
|
+
sessionsToParse = [...selectedSessions.values()];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const {
|
|
273
|
+
sessionId,
|
|
274
|
+
sessionPath,
|
|
275
|
+
projectFallback,
|
|
276
|
+
strict,
|
|
277
|
+
configuredRoot,
|
|
278
|
+
} of sessionsToParse) {
|
|
279
|
+
try {
|
|
280
|
+
const summaryPath = join(sessionPath, 'summary.json');
|
|
281
|
+
let summary;
|
|
282
|
+
if (strict && existsSync(summaryPath)) {
|
|
283
|
+
summary = JSON.parse(readFileSync(summaryPath, 'utf-8'));
|
|
284
|
+
} else {
|
|
285
|
+
summary = readJsonSafe(summaryPath) || {};
|
|
286
|
+
}
|
|
209
287
|
const cwd = summary.info?.cwd || summary.git_root_dir || null;
|
|
210
288
|
const project = cwd ? projectFromPath(cwd) : projectFallback;
|
|
211
289
|
const fallbackModel = summary.current_model_id || 'unknown';
|
|
@@ -249,7 +327,7 @@ export async function parse() {
|
|
|
249
327
|
role: 'assistant',
|
|
250
328
|
});
|
|
251
329
|
}
|
|
252
|
-
});
|
|
330
|
+
}, strict);
|
|
253
331
|
|
|
254
332
|
// Fallback timing from events.jsonl when updates lack message chunks
|
|
255
333
|
// (short/aborted sessions, older builds).
|
|
@@ -274,7 +352,7 @@ export async function parse() {
|
|
|
274
352
|
role: 'assistant',
|
|
275
353
|
});
|
|
276
354
|
}
|
|
277
|
-
});
|
|
355
|
+
}, strict);
|
|
278
356
|
}
|
|
279
357
|
|
|
280
358
|
// Last-resort session envelope from summary timestamps so a session with
|
|
@@ -301,6 +379,12 @@ export async function parse() {
|
|
|
301
379
|
});
|
|
302
380
|
}
|
|
303
381
|
}
|
|
382
|
+
} catch (err) {
|
|
383
|
+
if (!strict) throw err;
|
|
384
|
+
return {
|
|
385
|
+
buckets: [], sessions: [], skipped: true,
|
|
386
|
+
warnings: [`grok: 额外根目录读取失败,已保留上次同步数据: ${configuredRoot}`],
|
|
387
|
+
};
|
|
304
388
|
}
|
|
305
389
|
}
|
|
306
390
|
|
package/src/parsers/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { parse as parseDsh } from './dsh.js';
|
|
|
20
20
|
import { parse as parseAntigravity } from './antigravity.js';
|
|
21
21
|
import { parse as parseHermes } from './hermes.js';
|
|
22
22
|
import { parse as parseKiro } from './kiro.js';
|
|
23
|
+
import { parse as parseMcode } from './mcode.js';
|
|
23
24
|
import { parse as parseMimocode } from './mimocode.js';
|
|
24
25
|
import { parse as parsePiCodingAgent } from './pi-coding-agent.js';
|
|
25
26
|
import { parse as parseZcode } from './zcode.js';
|
|
@@ -49,6 +50,7 @@ export const parsers = {
|
|
|
49
50
|
'trae-cli': parseTraeCli,
|
|
50
51
|
'hermes': parseHermes,
|
|
51
52
|
'kiro': parseKiro,
|
|
53
|
+
'mcode': parseMcode,
|
|
52
54
|
'mimocode': parseMimocode,
|
|
53
55
|
'cline': parseCline,
|
|
54
56
|
'roo-code': parseRooCode,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { projectFromPath } from './fs-utils.js';
|
|
3
|
+
import { aggregateToBuckets } from './aggregate.js';
|
|
4
|
+
import {
|
|
5
|
+
queryDbJsonSnapshotOnLock,
|
|
6
|
+
isSqliteUnavailableError,
|
|
7
|
+
sqliteUnavailableError,
|
|
8
|
+
} from './sqlite.js';
|
|
9
|
+
import { getMcodeDbPath } from '../tools.js';
|
|
10
|
+
|
|
11
|
+
const SOURCE = 'mcode';
|
|
12
|
+
|
|
13
|
+
// Strict column allow-list. The mcode token table also stores a `raw` JSON
|
|
14
|
+
// payload (and the sessions table stores `record_json` / `extra_data_json`)
|
|
15
|
+
// that contains message bodies — we never select those, neither in this
|
|
16
|
+
// parser nor in any test fixture.
|
|
17
|
+
const TOKEN_COLUMNS = [
|
|
18
|
+
'session_id',
|
|
19
|
+
'model',
|
|
20
|
+
'ts',
|
|
21
|
+
'input_tokens',
|
|
22
|
+
'output_tokens',
|
|
23
|
+
'reasoning_tokens',
|
|
24
|
+
'cache_read_tokens',
|
|
25
|
+
'cache_write_tokens',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// `local_runtime_sessions` carries both `workspace_dir` (per-session scratch
|
|
29
|
+
// dir, always present) and `project_workspace_dir` (the project root when one
|
|
30
|
+
// is known). We pick the project column first, then the workspace, then
|
|
31
|
+
// fall back to "unknown". Never read `record_json` / `extra_data_json`.
|
|
32
|
+
const SESSION_COLUMNS = ['session_id', 'workspace_dir', 'project_workspace_dir'];
|
|
33
|
+
|
|
34
|
+
// Keep token rows and their project metadata in one SQLite statement. Separate
|
|
35
|
+
// reads can observe different WAL snapshots while mcode is writing, causing a
|
|
36
|
+
// token row to be uploaded once as "unknown" and again under its real project.
|
|
37
|
+
const USAGE_SQL = `
|
|
38
|
+
SELECT
|
|
39
|
+
${TOKEN_COLUMNS.map(column => `token.${column}`).join(', ')},
|
|
40
|
+
session.workspace_dir,
|
|
41
|
+
session.project_workspace_dir
|
|
42
|
+
FROM local_runtime_token_usage AS token
|
|
43
|
+
LEFT JOIN local_runtime_sessions AS session
|
|
44
|
+
ON session.session_id = token.session_id
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the mcode runtime-state SQLite database. Mirrors the precedence used
|
|
49
|
+
* by sibling tools (MiMoCode, DimAgent): explicit env var wins, then a
|
|
50
|
+
* tool-specific HOME, then the default layout.
|
|
51
|
+
*
|
|
52
|
+
* Defaults to `<homedir()>/.minimax/v2/sqlite/runtime-state.sqlite`, which is
|
|
53
|
+
* where the mcode CLI keeps its WAL database on macOS / Linux.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveMcodeDbPath(env = process.env) {
|
|
56
|
+
return getMcodeDbPath(env);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Token count (ms vs seconds). The mcode runtime writes ts as integer
|
|
61
|
+
* milliseconds — confirmed against the live schema (`typeof(ts)=integer`,
|
|
62
|
+
* values ≈ 1.787e12 for 2026-08-29). Stay defensive: anything < 1e12 is
|
|
63
|
+
* treated as seconds and scaled up.
|
|
64
|
+
*/
|
|
65
|
+
function tsToDate(value) {
|
|
66
|
+
const n = Number(value);
|
|
67
|
+
if (!Number.isFinite(n)) return null;
|
|
68
|
+
const ms = n < 1e12 ? n * 1000 : n;
|
|
69
|
+
const d = new Date(ms);
|
|
70
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
function toNonNegative(value) {
|
|
76
|
+
const n = Number(value);
|
|
77
|
+
if (!Number.isFinite(n) || n < 0) return 0;
|
|
78
|
+
return n;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function dbHasColumns(dbPath, table, columns) {
|
|
82
|
+
const info = queryDbJsonSnapshotOnLock(
|
|
83
|
+
dbPath,
|
|
84
|
+
`PRAGMA table_info(${table})`,
|
|
85
|
+
{ tempPrefix: 'vibe-usage-mcode' },
|
|
86
|
+
);
|
|
87
|
+
const present = new Set(info.map(row => String(row.name)));
|
|
88
|
+
return columns.every(col => present.has(col));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function parse() {
|
|
92
|
+
const dbPath = resolveMcodeDbPath();
|
|
93
|
+
if (!existsSync(dbPath)) return { buckets: [], sessions: [] };
|
|
94
|
+
|
|
95
|
+
// Schema guard: every allow-listed column must exist. If the mcode
|
|
96
|
+
// runtime ever renames / drops a column, fail soft (skipped) so the
|
|
97
|
+
// incremental sync keeps the last good upload state for this source.
|
|
98
|
+
let schemaOk;
|
|
99
|
+
try {
|
|
100
|
+
schemaOk =
|
|
101
|
+
dbHasColumns(dbPath, 'local_runtime_token_usage', TOKEN_COLUMNS) &&
|
|
102
|
+
dbHasColumns(dbPath, 'local_runtime_sessions', SESSION_COLUMNS);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('mcode');
|
|
105
|
+
return { buckets: [], sessions: [], skipped: true };
|
|
106
|
+
}
|
|
107
|
+
if (!schemaOk) {
|
|
108
|
+
return { buckets: [], sessions: [], skipped: true };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Read tokens and session project metadata from one statement/snapshot.
|
|
112
|
+
let usageRows;
|
|
113
|
+
try {
|
|
114
|
+
usageRows = queryDbJsonSnapshotOnLock(dbPath, USAGE_SQL, {
|
|
115
|
+
tempPrefix: 'vibe-usage-mcode',
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('mcode');
|
|
119
|
+
return { buckets: [], sessions: [], skipped: true };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const entries = [];
|
|
123
|
+
|
|
124
|
+
for (const row of usageRows) {
|
|
125
|
+
const sessionId = row.session_id != null ? String(row.session_id) : '';
|
|
126
|
+
if (!sessionId) continue;
|
|
127
|
+
const ts = tsToDate(row.ts);
|
|
128
|
+
if (!ts) continue;
|
|
129
|
+
|
|
130
|
+
// MCode stores output and reasoning as separate counters. Its own
|
|
131
|
+
// summary code computes total = input + output + reasoning, so do not
|
|
132
|
+
// subtract reasoning from output here.
|
|
133
|
+
const inputRaw = toNonNegative(row.input_tokens);
|
|
134
|
+
const cacheWrite = toNonNegative(row.cache_write_tokens);
|
|
135
|
+
const outputRaw = toNonNegative(row.output_tokens);
|
|
136
|
+
const reasoningRaw = toNonNegative(row.reasoning_tokens);
|
|
137
|
+
const cacheRead = toNonNegative(row.cache_read_tokens);
|
|
138
|
+
|
|
139
|
+
const inputTokens = inputRaw + cacheWrite;
|
|
140
|
+
const reasoningOutputTokens = reasoningRaw;
|
|
141
|
+
const outputTokens = outputRaw;
|
|
142
|
+
const cachedInputTokens = cacheRead;
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
inputTokens +
|
|
146
|
+
outputTokens +
|
|
147
|
+
cachedInputTokens +
|
|
148
|
+
reasoningOutputTokens ===
|
|
149
|
+
0
|
|
150
|
+
) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const projectPath = row.project_workspace_dir || row.workspace_dir;
|
|
155
|
+
const project = projectPath ? projectFromPath(String(projectPath)) : 'unknown';
|
|
156
|
+
const model = row.model != null && String(row.model).trim()
|
|
157
|
+
? String(row.model).trim()
|
|
158
|
+
: 'unknown';
|
|
159
|
+
|
|
160
|
+
entries.push({
|
|
161
|
+
source: SOURCE,
|
|
162
|
+
model,
|
|
163
|
+
project,
|
|
164
|
+
timestamp: ts,
|
|
165
|
+
inputTokens,
|
|
166
|
+
outputTokens,
|
|
167
|
+
cachedInputTokens,
|
|
168
|
+
reasoningOutputTokens,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
buckets: aggregateToBuckets(entries),
|
|
174
|
+
// The token ledger contains assistant usage rows only. Reconstructing
|
|
175
|
+
// user prompts would require reading message payloads, which this parser
|
|
176
|
+
// deliberately never selects, so mcode emits buckets only like Alma.
|
|
177
|
+
sessions: [],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Re-export for tests / external consumers.
|
|
182
|
+
export { SOURCE as MCODE_SOURCE };
|
|
@@ -99,7 +99,10 @@ export async function parsePiSessionJsonl({
|
|
|
99
99
|
if (message.role !== 'assistant' || !message.usage) continue;
|
|
100
100
|
const usage = message.usage;
|
|
101
101
|
const inputTokens = toCount(usage.input) + toCount(usage.cacheWrite);
|
|
102
|
-
|
|
102
|
+
// Pi's Usage type names this field `reasoning` (a documented subset of
|
|
103
|
+
// `output`); older/adjacent stores wrote `reasoningTokens`. Reading only
|
|
104
|
+
// the latter left every Pi reasoning token inside outputTokens.
|
|
105
|
+
const reasoningOutputTokens = toCount(usage.reasoning ?? usage.reasoningTokens);
|
|
103
106
|
// OMP/Pi usage.output includes reasoning; the shared bucket contract
|
|
104
107
|
// stores non-reasoning output and reasoning separately.
|
|
105
108
|
const outputTokens = Math.max(0, toCount(usage.output) - reasoningOutputTokens);
|
package/src/pi-roots.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { delimiter, join } from 'node:path';
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { delimiter, isAbsolute, join } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
|
|
5
5
|
function expandHome(value) {
|
|
@@ -41,18 +41,41 @@ export function looksLikeOmpAgentDir(agentDir) {
|
|
|
41
41
|
|| existsSync(join(agentDir, 'agent.db'));
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Pi resolves a session directory from `--session-dir`, then
|
|
45
|
+
// PI_CODING_AGENT_SESSION_DIR, then `sessionDir` in settings.json. Only the
|
|
46
|
+
// last two are discoverable after the fact, and both name the sessions
|
|
47
|
+
// directory itself (no `sessions` segment is appended).
|
|
48
|
+
function settingsSessionDir(agentDir) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = readFileSync(join(agentDir, 'settings.json'), 'utf-8');
|
|
51
|
+
const value = JSON.parse(raw)?.sessionDir;
|
|
52
|
+
if (typeof value !== 'string') return null;
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
// Pi also accepts project-relative values. Those resolve against a cwd we
|
|
55
|
+
// do not have here, so only absolute and ~-anchored paths are scanned.
|
|
56
|
+
if (!trimmed || !(isAbsolute(trimmed) || trimmed.startsWith('~'))) return null;
|
|
57
|
+
return expandHome(trimmed);
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
export function getPiSessionDirs() {
|
|
45
64
|
const override = process.env.VIBE_USAGE_PI_SESSION_DIRS?.trim();
|
|
46
65
|
if (override) return uniqueExistingDirs(override.split(delimiter));
|
|
47
66
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
const envAgentDir = process.env.PI_CODING_AGENT_DIR?.trim();
|
|
68
|
+
const agentDir = envAgentDir ? expandHome(envAgentDir) : join(homedir(), '.pi', 'agent');
|
|
69
|
+
// OMP inherits PI_CODING_AGENT_DIR from Pi. Do not parse an identifiable
|
|
70
|
+
// OMP store again as source=pi-coding-agent.
|
|
71
|
+
if (envAgentDir && looksLikeOmpAgentDir(agentDir)) return [];
|
|
72
|
+
|
|
73
|
+
const dirs = [join(agentDir, 'sessions')];
|
|
74
|
+
const envSessionDir = process.env.PI_CODING_AGENT_SESSION_DIR?.trim();
|
|
75
|
+
if (envSessionDir) dirs.push(expandHome(envSessionDir));
|
|
76
|
+
const configured = settingsSessionDir(agentDir);
|
|
77
|
+
if (configured) dirs.push(configured);
|
|
78
|
+
return uniqueExistingDirs(dirs);
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
export function getOmpSessionDirs() {
|
package/src/sync.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createSyncClient, forBatch } from './client-meta.js';
|
|
|
9
9
|
import { parsers } from './parsers/index.js';
|
|
10
10
|
import { aggregateToBuckets } from './parsers/aggregate.js';
|
|
11
11
|
import { normalizeParserResult } from './parsers/contract.js';
|
|
12
|
+
import { extraRootList } from './extra-roots.js';
|
|
12
13
|
import { success, failure, warn, arrow, link, dim } from './output.js';
|
|
13
14
|
|
|
14
15
|
const BATCH_SIZE = 100;
|
|
@@ -20,6 +21,12 @@ function formatBytes(bytes) {
|
|
|
20
21
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
21
22
|
}
|
|
22
23
|
|
|
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
|
+
|
|
23
30
|
export function resolveUploadProjectSetting(settings) {
|
|
24
31
|
if (typeof settings?.uploadProject !== 'boolean') {
|
|
25
32
|
const error = new Error('SETTINGS_UNAVAILABLE');
|
|
@@ -147,9 +154,12 @@ export async function runSync({
|
|
|
147
154
|
PARSER_CONCURRENCY,
|
|
148
155
|
async ([source, parse]) => {
|
|
149
156
|
try {
|
|
150
|
-
const result =
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
const result = await parse({
|
|
158
|
+
extraRoots: extraRootList(config.extraRoots?.[source]),
|
|
159
|
+
...(source === 'codex' ? {
|
|
160
|
+
codexExtraHome: resolveCodexExtraHome(config.codexExtraHome, codexExtraHome),
|
|
161
|
+
} : {}),
|
|
162
|
+
});
|
|
153
163
|
return { source, result };
|
|
154
164
|
} catch (err) {
|
|
155
165
|
return { source, error: err };
|
|
@@ -175,6 +185,7 @@ export async function runSync({
|
|
|
175
185
|
parserProgress.push({ source, ...indexing });
|
|
176
186
|
}
|
|
177
187
|
for (const message of warnings) {
|
|
188
|
+
if (shouldSuppressParserWarning(source, message, quiet)) continue;
|
|
178
189
|
process.stderr.write(`${dim(` ${message}`)}\n`);
|
|
179
190
|
}
|
|
180
191
|
// A parser may deliberately suppress a transient error (Cursor network
|
package/src/tools.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { isAbsolute, join, posix, resolve, win32 } from 'node:path';
|
|
2
|
+
import { dirname, isAbsolute, join, posix, resolve, win32 } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { findClaudeCodeDataDirs } from './claude-roots.js';
|
|
5
5
|
import { findCindyDataDirs, getCindyDataRoots } from './cindy-roots.js';
|
|
6
6
|
import { codexSessionDirs, resolveCodexHomes } from './codex-roots.js';
|
|
7
|
+
import {
|
|
8
|
+
antigravityConversationDirs,
|
|
9
|
+
discoverCodexHomes,
|
|
10
|
+
extraRootList,
|
|
11
|
+
grokSessionsDir,
|
|
12
|
+
} from './extra-roots.js';
|
|
7
13
|
import { findClineDataDirs } from './cline-roots.js';
|
|
8
14
|
import { findCraftDataDirs } from './craft-roots.js';
|
|
9
15
|
import { findOmpDataDirs, findPiDataDirs } from './pi-roots.js';
|
|
@@ -106,8 +112,9 @@ function findOpenclawDataDirs() {
|
|
|
106
112
|
// Codex keeps live sessions in ~/.codex/sessions and moves completed ones to
|
|
107
113
|
// ~/.codex/archived_sessions. Detect Codex if either dir exists, so a user
|
|
108
114
|
// whose sessions have all been archived is still recognized.
|
|
109
|
-
export function findCodexDataDirs(codexExtraHome) {
|
|
110
|
-
|
|
115
|
+
export function findCodexDataDirs(codexExtraHome, extraRoots = []) {
|
|
116
|
+
const configuredHomes = extraRoots.flatMap(root => discoverCodexHomes(root).homes);
|
|
117
|
+
return [...new Set([...resolveCodexHomes(codexExtraHome), ...configuredHomes])]
|
|
111
118
|
.flatMap(codexSessionDirs)
|
|
112
119
|
.filter(existsSync);
|
|
113
120
|
}
|
|
@@ -143,6 +150,17 @@ export function findDshDataDirs() {
|
|
|
143
150
|
return [getDshSessionsDir()].filter(existsSync);
|
|
144
151
|
}
|
|
145
152
|
|
|
153
|
+
/** mcode runtime database: VIBE_USAGE_MCODE_DB wins, then MCODE_HOME, then ~/.minimax. */
|
|
154
|
+
export function getMcodeDbPath(env = process.env, home = homedir()) {
|
|
155
|
+
const override = env.VIBE_USAGE_MCODE_DB?.trim();
|
|
156
|
+
if (override) return isAbsolute(override) ? override : resolve(override);
|
|
157
|
+
if (env.MCODE_HOME && !isAbsolute(env.MCODE_HOME)) {
|
|
158
|
+
throw new Error(`MCODE_HOME must be an absolute path, got: ${JSON.stringify(env.MCODE_HOME)}`);
|
|
159
|
+
}
|
|
160
|
+
const root = env.MCODE_HOME || join(home, '.minimax');
|
|
161
|
+
return join(root, 'v2', 'sqlite', 'runtime-state.sqlite');
|
|
162
|
+
}
|
|
163
|
+
|
|
146
164
|
export function getMimocodeDbPath(env = process.env) {
|
|
147
165
|
if (env.MIMOCODE_HOME && !isAbsolute(env.MIMOCODE_HOME)) {
|
|
148
166
|
throw new Error(`MIMOCODE_HOME must be an absolute path, got: ${JSON.stringify(env.MIMOCODE_HOME)}`);
|
|
@@ -154,11 +172,12 @@ export function getMimocodeDbPath(env = process.env) {
|
|
|
154
172
|
return isAbsolute(env.MIMOCODE_DB) ? env.MIMOCODE_DB : join(dataDir, env.MIMOCODE_DB);
|
|
155
173
|
}
|
|
156
174
|
|
|
157
|
-
function findAntigravityDataDirs() {
|
|
158
|
-
return [
|
|
175
|
+
export function findAntigravityDataDirs(extraRoots = []) {
|
|
176
|
+
return [...new Set([
|
|
159
177
|
join(homedir(), '.gemini', 'antigravity'),
|
|
160
178
|
join(homedir(), '.gemini', 'antigravity-cli'),
|
|
161
|
-
|
|
179
|
+
...extraRoots.flatMap(root => antigravityConversationDirs(root).map(dirname)),
|
|
180
|
+
])].filter(existsSync);
|
|
162
181
|
}
|
|
163
182
|
|
|
164
183
|
export function findTraeCliDataDirs() {
|
|
@@ -193,10 +212,13 @@ export function getGrokSessionsDir() {
|
|
|
193
212
|
}
|
|
194
213
|
|
|
195
214
|
// Detect Grok when sessions/ exists under GROK_HOME (or the test override).
|
|
196
|
-
export function findGrokDataDirs() {
|
|
215
|
+
export function findGrokDataDirs(extraRoots = []) {
|
|
197
216
|
const testDir = process.env.VIBE_USAGE_GROK_SESSIONS?.trim();
|
|
198
217
|
if (testDir) return [testDir].filter(existsSync);
|
|
199
|
-
return [
|
|
218
|
+
return [...new Set([
|
|
219
|
+
join(getGrokHome(), 'sessions'),
|
|
220
|
+
...extraRoots.map(grokSessionsDir),
|
|
221
|
+
])].filter(existsSync);
|
|
200
222
|
}
|
|
201
223
|
|
|
202
224
|
export function getDimAgentDbPath() {
|
|
@@ -240,13 +262,15 @@ export const TOOLS = [
|
|
|
240
262
|
name: 'Codex CLI',
|
|
241
263
|
id: 'codex',
|
|
242
264
|
dataDir: join(homedir(), '.codex', 'sessions'),
|
|
243
|
-
detectDataDirs: ({ codexExtraHome } = {}) =>
|
|
265
|
+
detectDataDirs: ({ codexExtraHome, extraRoots } = {}) => (
|
|
266
|
+
findCodexDataDirs(codexExtraHome, extraRootList(extraRoots?.codex))
|
|
267
|
+
),
|
|
244
268
|
},
|
|
245
269
|
{
|
|
246
270
|
name: 'Grok',
|
|
247
271
|
id: 'grok',
|
|
248
272
|
dataDir: join(homedir(), '.grok', 'sessions'),
|
|
249
|
-
detectDataDirs: findGrokDataDirs,
|
|
273
|
+
detectDataDirs: ({ extraRoots } = {}) => findGrokDataDirs(extraRootList(extraRoots?.grok)),
|
|
250
274
|
},
|
|
251
275
|
{
|
|
252
276
|
name: 'GitHub Copilot CLI',
|
|
@@ -311,6 +335,12 @@ export const TOOLS = [
|
|
|
311
335
|
dataDir: join(homedir(), '.kimi-code', 'sessions'),
|
|
312
336
|
detectDataDirs: findKimiCodeDataDirs,
|
|
313
337
|
},
|
|
338
|
+
{
|
|
339
|
+
name: 'MiniMax Code',
|
|
340
|
+
id: 'mcode',
|
|
341
|
+
dataDir: join(homedir(), '.minimax', 'v2', 'sqlite', 'runtime-state.sqlite'),
|
|
342
|
+
detectDataDirs: () => [getMcodeDbPath()].filter(existsSync),
|
|
343
|
+
},
|
|
314
344
|
{
|
|
315
345
|
name: 'MiMoCode',
|
|
316
346
|
id: 'mimocode',
|
|
@@ -337,7 +367,9 @@ export const TOOLS = [
|
|
|
337
367
|
name: 'Antigravity',
|
|
338
368
|
id: 'antigravity',
|
|
339
369
|
dataDir: join(homedir(), '.gemini', 'antigravity'),
|
|
340
|
-
detectDataDirs:
|
|
370
|
+
detectDataDirs: ({ extraRoots } = {}) => (
|
|
371
|
+
findAntigravityDataDirs(extraRootList(extraRoots?.antigravity))
|
|
372
|
+
),
|
|
341
373
|
},
|
|
342
374
|
{
|
|
343
375
|
name: 'Trae CLI',
|