@vibe-cafe/vibe-usage 0.13.1 → 0.13.2
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/daemon-service.js +2 -0
- package/src/parsers/mcode.js +112 -77
- package/src/tools.js +67 -7
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ Quota results are never uploaded or added to incremental sync state. Credential-
|
|
|
93
93
|
| pi | `~/.pi/agent/sessions/` or `$PI_CODING_AGENT_DIR/sessions/`, plus the session directory Pi itself was pointed at via `PI_CODING_AGENT_SESSION_DIR` or `sessionDir` in `~/.pi/agent/settings.json`, plus explicitly added `pi-coding-agent` roots for stores only reachable through `pi --session <file>` (fixture/relocation override: `VIBE_USAGE_PI_SESSION_DIRS`). Cache writes are included in input usage; reasoning is read from Pi's `usage.reasoning` (legacy `usage.reasoningTokens` still accepted) and split out of the inclusive output total |
|
|
94
94
|
| Qwen Code | `~/.qwen/tmp/` |
|
|
95
95
|
| Kimi Code | Current `<home>/sessions/wd_<slug>_<hash>/session_<id>/agents/<agent>/wire.jsonl` (`usage.record` deltas, including retry/compaction scope and cache creation; main/subagent wires form one session), with project names from `session_index.jsonl`. Every home with that layout is scanned: the CLI home (`$KIMI_CODE_HOME`, else `~/.kimi-code`) plus the Kimi Work desktop app's embedded runtime home (`<Electron userData>/kimi-desktop/daimon-share/daimon/runtime/kimi-code/home`), which the app never writes into the CLI home; legacy `~/.kimi/sessions/` is parsed alongside (`kimi migrate` never carries usage over, so both stores are always merged) |
|
|
96
|
-
| MiniMax Code (mcode) | `$MCODE_HOME/v2/sqlite/runtime-state.sqlite` (default `~/.minimax/v2/sqlite/runtime-state.sqlite`; fixture override: `VIBE_USAGE_MCODE_DB`). Reads only allow-listed token ledger fields and session workspace/project paths, uses basename-only projects, folds cache writes into input, keeps cache reads and reasoning separate, and never selects raw/message JSON payloads. WAL/lock reads use a disposable snapshot; malformed or incompatible databases are skipped to preserve incremental state. |
|
|
96
|
+
| MiniMax Code (mcode) | `$MCODE_HOME/v2/sqlite/runtime-state.sqlite` (default `~/.minimax/v2/sqlite/runtime-state.sqlite`; also reads the roots the CLI relocates to — `$MINIMAX_DATA_DIR` / `$MAVIS_DATA_DIR`, each `~/.minimax-<profile>`, the pre-npm `~/.minimax-code` user-data dir and the pre-rename `~/.mavis` home — and merges them, dropping rows a store only holds as a copy; fixture override: `VIBE_USAGE_MCODE_DB`). Reads only allow-listed token ledger fields and session workspace/project paths, uses basename-only projects, folds cache writes into input, keeps cache reads and reasoning separate, and never selects raw/message JSON payloads. WAL/lock reads use a disposable snapshot; malformed or incompatible databases are skipped to preserve incremental state (a broken extra store is reported as a warning instead of blanking the live one). |
|
|
97
97
|
| MiMoCode | `$MIMOCODE_HOME/data/mimocode.db`, `$XDG_DATA_HOME/mimocode/mimocode.db`, or `~/.local/share/mimocode/mimocode.db` (SQLite; exact input, output, reasoning, and cache-read tokens from assistant messages; honors `MIMOCODE_DB`; cache-write tokens are included in input usage) |
|
|
98
98
|
| Amp | `~/.local/share/amp/threads/`; cache-creation tokens are included in input usage |
|
|
99
99
|
| Droid | `~/.factory/sessions/` |
|
package/package.json
CHANGED
package/src/daemon-service.js
CHANGED
|
@@ -141,9 +141,11 @@ const PRESERVED_SERVICE_ENV = [
|
|
|
141
141
|
'KIRO_CLI_SESSIONS_DIR',
|
|
142
142
|
'KIRO_SESSIONS_DIR',
|
|
143
143
|
'KIRO_USER_PATH',
|
|
144
|
+
'MAVIS_DATA_DIR',
|
|
144
145
|
'MCODE_HOME',
|
|
145
146
|
'MIMOCODE_HOME',
|
|
146
147
|
'MIMOCODE_DB',
|
|
148
|
+
'MINIMAX_DATA_DIR',
|
|
147
149
|
'PI_CONFIG_DIR',
|
|
148
150
|
'PI_CODING_AGENT_DIR',
|
|
149
151
|
'PI_CODING_AGENT_SESSION_DIR',
|
package/src/parsers/mcode.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
isSqliteUnavailableError,
|
|
7
7
|
sqliteUnavailableError,
|
|
8
8
|
} from './sqlite.js';
|
|
9
|
-
import { getMcodeDbPath } from '../tools.js';
|
|
9
|
+
import { getMcodeDbPath, getMcodeDbPaths } from '../tools.js';
|
|
10
10
|
|
|
11
11
|
const SOURCE = 'mcode';
|
|
12
12
|
|
|
@@ -47,7 +47,8 @@ const USAGE_SQL = `
|
|
|
47
47
|
/**
|
|
48
48
|
* Resolve the mcode runtime-state SQLite database. Mirrors the precedence used
|
|
49
49
|
* by sibling tools (MiMoCode, DimAgent): explicit env var wins, then a
|
|
50
|
-
* tool-specific HOME, then the
|
|
50
|
+
* tool-specific HOME (MCODE_HOME), then the mcode CLI's own relocation
|
|
51
|
+
* variables (MINIMAX_DATA_DIR / MAVIS_DATA_DIR), then the default layout.
|
|
51
52
|
*
|
|
52
53
|
* Defaults to `<homedir()>/.minimax/v2/sqlite/runtime-state.sqlite`, which is
|
|
53
54
|
* where the mcode CLI keeps its WAL database on macOS / Linux.
|
|
@@ -56,6 +57,16 @@ export function resolveMcodeDbPath(env = process.env) {
|
|
|
56
57
|
return getMcodeDbPath(env);
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Every mcode store worth reading: the resolved default above, plus a
|
|
62
|
+
* `~/.minimax-<profile>` per active profile, the pre-npm `~/.minimax-code`
|
|
63
|
+
* and the pre-rename `~/.mavis` homes (and their profiles) when they hold a
|
|
64
|
+
* runtime-state database.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveMcodeDbPaths(env = process.env, home) {
|
|
67
|
+
return getMcodeDbPaths(env, home);
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
/**
|
|
60
71
|
* Token count (ms vs seconds). The mcode runtime writes ts as integer
|
|
61
72
|
* milliseconds — confirmed against the live schema (`typeof(ts)=integer`,
|
|
@@ -89,92 +100,116 @@ function dbHasColumns(dbPath, table, columns) {
|
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
export async function parse() {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
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
|
-
}
|
|
103
|
+
const dbPaths = getMcodeDbPaths();
|
|
104
|
+
// The first store that exists is the one the running CLI would use; schema
|
|
105
|
+
// or read failures there keep the fail-soft contract below. Later stores are
|
|
106
|
+
// best-effort extras (profiles, pre-npm / pre-rename homes) and a broken one
|
|
107
|
+
// must not suppress the live store.
|
|
108
|
+
const primary = dbPaths.find(path => existsSync(path));
|
|
121
109
|
|
|
122
110
|
const entries = [];
|
|
111
|
+
const warnings = [];
|
|
112
|
+
// mcode's own migration can COPY a relocated legacy tree into ~/.minimax, so
|
|
113
|
+
// both copies exist. Collapse rows that reappear unchanged in a later store;
|
|
114
|
+
// duplicates inside one store are left alone — there they are the ledger.
|
|
115
|
+
const seenAcrossStores = new Set();
|
|
116
|
+
|
|
117
|
+
for (const dbPath of dbPaths) {
|
|
118
|
+
if (!existsSync(dbPath)) continue;
|
|
119
|
+
const isPrimary = dbPath === primary;
|
|
120
|
+
|
|
121
|
+
// Schema guard: every allow-listed column must exist. If the mcode
|
|
122
|
+
// runtime ever renames / drops a column, fail soft (skipped) so the
|
|
123
|
+
// incremental sync keeps the last good upload state for this source.
|
|
124
|
+
let schemaOk;
|
|
125
|
+
try {
|
|
126
|
+
schemaOk =
|
|
127
|
+
dbHasColumns(dbPath, 'local_runtime_token_usage', TOKEN_COLUMNS) &&
|
|
128
|
+
dbHasColumns(dbPath, 'local_runtime_sessions', SESSION_COLUMNS);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('mcode');
|
|
131
|
+
schemaOk = false;
|
|
132
|
+
}
|
|
133
|
+
if (!schemaOk) {
|
|
134
|
+
if (isPrimary) return { buckets: [], sessions: [], skipped: true };
|
|
135
|
+
warnings.push(`MiniMax Code: 跳过结构不兼容的数据库 ${dbPath}`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
) {
|
|
139
|
+
// Read tokens and session project metadata from one statement/snapshot.
|
|
140
|
+
let usageRows;
|
|
141
|
+
try {
|
|
142
|
+
usageRows = queryDbJsonSnapshotOnLock(dbPath, USAGE_SQL, {
|
|
143
|
+
tempPrefix: 'vibe-usage-mcode',
|
|
144
|
+
});
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (isSqliteUnavailableError(err)) throw sqliteUnavailableError('mcode');
|
|
147
|
+
if (isPrimary) return { buckets: [], sessions: [], skipped: true };
|
|
148
|
+
warnings.push(`MiniMax Code: 无法读取 ${dbPath},已跳过: ${err.message}`);
|
|
151
149
|
continue;
|
|
152
150
|
}
|
|
153
151
|
|
|
154
|
-
const
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
152
|
+
const identitiesInStore = new Set();
|
|
153
|
+
for (const row of usageRows) {
|
|
154
|
+
// Identity covers every selected column; only a byte-identical ledger
|
|
155
|
+
// row can collapse, and only against an earlier store.
|
|
156
|
+
const identity = JSON.stringify(row);
|
|
157
|
+
if (seenAcrossStores.has(identity)) continue;
|
|
158
|
+
identitiesInStore.add(identity);
|
|
159
|
+
|
|
160
|
+
const sessionId = row.session_id != null ? String(row.session_id) : '';
|
|
161
|
+
if (!sessionId) continue;
|
|
162
|
+
const ts = tsToDate(row.ts);
|
|
163
|
+
if (!ts) continue;
|
|
164
|
+
|
|
165
|
+
// MCode stores output and reasoning as separate counters. Its own
|
|
166
|
+
// summary code computes total = input + output + reasoning, so do not
|
|
167
|
+
// subtract reasoning from output here.
|
|
168
|
+
const inputRaw = toNonNegative(row.input_tokens);
|
|
169
|
+
const cacheWrite = toNonNegative(row.cache_write_tokens);
|
|
170
|
+
const outputRaw = toNonNegative(row.output_tokens);
|
|
171
|
+
const reasoningRaw = toNonNegative(row.reasoning_tokens);
|
|
172
|
+
const cacheRead = toNonNegative(row.cache_read_tokens);
|
|
173
|
+
|
|
174
|
+
const inputTokens = inputRaw + cacheWrite;
|
|
175
|
+
const reasoningOutputTokens = reasoningRaw;
|
|
176
|
+
const outputTokens = outputRaw;
|
|
177
|
+
const cachedInputTokens = cacheRead;
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
inputTokens +
|
|
181
|
+
outputTokens +
|
|
182
|
+
cachedInputTokens +
|
|
183
|
+
reasoningOutputTokens ===
|
|
184
|
+
0
|
|
185
|
+
) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const projectPath = row.project_workspace_dir || row.workspace_dir;
|
|
190
|
+
const project = projectPath ? projectFromPath(String(projectPath)) : 'unknown';
|
|
191
|
+
const model = row.model != null && String(row.model).trim()
|
|
192
|
+
? String(row.model).trim()
|
|
193
|
+
: 'unknown';
|
|
194
|
+
|
|
195
|
+
entries.push({
|
|
196
|
+
source: SOURCE,
|
|
197
|
+
model,
|
|
198
|
+
project,
|
|
199
|
+
timestamp: ts,
|
|
200
|
+
inputTokens,
|
|
201
|
+
outputTokens,
|
|
202
|
+
cachedInputTokens,
|
|
203
|
+
reasoningOutputTokens,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
for (const identity of identitiesInStore) seenAcrossStores.add(identity);
|
|
170
207
|
}
|
|
171
208
|
|
|
172
209
|
return {
|
|
173
210
|
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
211
|
sessions: [],
|
|
212
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
178
213
|
};
|
|
179
214
|
}
|
|
180
215
|
|
package/src/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import { delimiter, dirname, isAbsolute, join, posix, resolve, win32 } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { getOpenCodeStores } from './opencode-roots.js';
|
|
@@ -176,15 +176,75 @@ export function findDshDataDirs() {
|
|
|
176
176
|
return [getDshSessionsDir()].filter(existsSync);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
const MCODE_DB_RELATIVE = join('v2', 'sqlite', 'runtime-state.sqlite');
|
|
180
|
+
|
|
181
|
+
// Data-root variables the mcode CLI itself resolves, in its own precedence.
|
|
182
|
+
// MCODE_HOME stays first as vibe-usage's pre-existing fixture override, then
|
|
183
|
+
// the two public relocation variables from the mcode README.
|
|
184
|
+
const MCODE_ROOT_ENV = ['MCODE_HOME', 'MINIMAX_DATA_DIR', 'MAVIS_DATA_DIR'];
|
|
185
|
+
|
|
186
|
+
function mcodeRootFromEnv(env) {
|
|
187
|
+
for (const key of MCODE_ROOT_ENV) {
|
|
188
|
+
const value = env[key]?.trim();
|
|
189
|
+
if (!value) continue;
|
|
190
|
+
if (!isAbsolute(value)) {
|
|
191
|
+
throw new Error(`${key} must be an absolute path, got: ${JSON.stringify(value)}`);
|
|
192
|
+
}
|
|
193
|
+
return value;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** mcode runtime database: VIBE_USAGE_MCODE_DB wins, then MCODE_HOME / MINIMAX_DATA_DIR / MAVIS_DATA_DIR, then ~/.minimax. */
|
|
180
199
|
export function getMcodeDbPath(env = process.env, home = homedir()) {
|
|
181
200
|
const override = env.VIBE_USAGE_MCODE_DB?.trim();
|
|
182
201
|
if (override) return isAbsolute(override) ? override : resolve(override);
|
|
183
|
-
|
|
184
|
-
|
|
202
|
+
const root = mcodeRootFromEnv(env) || join(home, '.minimax');
|
|
203
|
+
return join(root, MCODE_DB_RELATIVE);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Every mcode runtime database worth parsing.
|
|
208
|
+
*
|
|
209
|
+
* The mcode CLI relocates its whole data root with MINIMAX_DATA_DIR /
|
|
210
|
+
* MAVIS_DATA_DIR, scopes it to ~/.minimax-<profile> when a profile is active,
|
|
211
|
+
* and earlier source builds kept user data at ~/.minimax-code; the pre-rename
|
|
212
|
+
* home (~/.mavis) stays readable through the CLI's own compat link. Reading
|
|
213
|
+
* only ~/.minimax silently misses every session written in those layouts.
|
|
214
|
+
*
|
|
215
|
+
* Explicit roots keep returning their single unresolved path so a missing
|
|
216
|
+
* file still yields an empty result; the default layout only returns stores
|
|
217
|
+
* that exist, deduplicated by physical file so a compat symlink cannot make
|
|
218
|
+
* the same ledger count twice.
|
|
219
|
+
*/
|
|
220
|
+
export function getMcodeDbPaths(env = process.env, home = homedir()) {
|
|
221
|
+
if (env.VIBE_USAGE_MCODE_DB?.trim() || mcodeRootFromEnv(env)) return [getMcodeDbPath(env, home)];
|
|
222
|
+
const candidates = [
|
|
223
|
+
join(home, '.minimax'),
|
|
224
|
+
join(home, '.minimax-code'),
|
|
225
|
+
join(home, '.mavis'),
|
|
226
|
+
];
|
|
227
|
+
try {
|
|
228
|
+
const profiles = readdirSync(home, { withFileTypes: true })
|
|
229
|
+
.filter(entry => entry.isDirectory() && /^\.(?:minimax|mavis)-.+$/.test(entry.name))
|
|
230
|
+
.map(entry => join(home, entry.name))
|
|
231
|
+
.sort();
|
|
232
|
+
candidates.push(...profiles);
|
|
233
|
+
} catch {
|
|
234
|
+
// Unreadable home directory: fall back to the default layout below.
|
|
235
|
+
}
|
|
236
|
+
const paths = [];
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
for (const dir of candidates) {
|
|
239
|
+
const dbPath = join(dir, MCODE_DB_RELATIVE);
|
|
240
|
+
if (!existsSync(dbPath)) continue;
|
|
241
|
+
let identity = dbPath;
|
|
242
|
+
try { identity = realpathSync(dbPath); } catch { /* keep the literal path */ }
|
|
243
|
+
if (seen.has(identity)) continue;
|
|
244
|
+
seen.add(identity);
|
|
245
|
+
paths.push(dbPath);
|
|
185
246
|
}
|
|
186
|
-
|
|
187
|
-
return join(root, 'v2', 'sqlite', 'runtime-state.sqlite');
|
|
247
|
+
return paths;
|
|
188
248
|
}
|
|
189
249
|
|
|
190
250
|
// Devin (CLI and Desktop share one agent backend) keeps all sessions in a
|
|
@@ -417,7 +477,7 @@ export const TOOLS = [
|
|
|
417
477
|
name: 'MiniMax Code',
|
|
418
478
|
id: 'mcode',
|
|
419
479
|
dataDir: join(homedir(), '.minimax', 'v2', 'sqlite', 'runtime-state.sqlite'),
|
|
420
|
-
detectDataDirs: () =>
|
|
480
|
+
detectDataDirs: () => getMcodeDbPaths().filter(existsSync),
|
|
421
481
|
},
|
|
422
482
|
{
|
|
423
483
|
name: 'MiMoCode',
|