@vibe-cafe/vibe-usage 0.13.0 → 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 +2 -1
- package/package.json +1 -1
- package/src/daemon-service.js +2 -0
- package/src/parsers/mcode.js +112 -77
- package/src/quotas/providers/opencode-go.js +4 -1
- package/src/quotas/schema.js +20 -0
- package/src/tools.js +67 -7
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ npx @vibe-cafe/vibe-usage help --all # Full help (plain `help` shows the short
|
|
|
62
62
|
|
|
63
63
|
The versioned `quota` JSON contract is designed for local desktop clients. `discover` only checks ordinary app, config-directory, and executable presence signals: it does not open credentials or use the network. `fetch` invokes only products explicitly named with `--product`, and one provider failure does not prevent results for the others.
|
|
64
64
|
|
|
65
|
+
- A quota result may carry a machine-readable `emptyReason` (`limitReached` / `noWindow` / `notEntitled` / `sessionWithoutPlanLimits`) alongside the human `message`; it is additive within schema v1, so clients that do not know it ignore it, and it says *why* there was no window instead of leaving the caller to parse prose. OpenCode Go sets `notEntitled` for a key without the Go subscription.
|
|
65
66
|
- **Kimi Code** reads the official OAuth file from `$KIMI_CODE_HOME/credentials/kimi-code.json` (otherwise `~/.kimi-code/credentials/kimi-code.json`) and falls back to the legacy `$KIMI_SHARE_DIR` / `~/.kimi` location — the first login that exists wins. It calls `https://api.kimi.com/coding/v1/usages` and supports both the current 2.x response and the legacy quota response. When the short-lived access token is close to expiry, it uses Kimi's standard OAuth refresh flow and atomically rotates the official credential with owner-only permissions. Refreshes are serialized across Vibe Usage processes and re-check the file before writing so a concurrent Kimi CLI refresh wins safely.
|
|
66
67
|
- **ZCode / GLM Coding Plan** accepts only a caller-supplied regional key: `BIGMODEL_API_KEY` calls the domestic `https://open.bigmodel.cn/api/monitor/usage/quota/limit`, while the existing `Z_AI_API_KEY` keeps using `https://api.z.ai/api/monitor/usage/quota/limit`. If both are present, the explicitly named BigModel key wins. It does not read ZCode's private OAuth state.
|
|
67
68
|
- **Grok** reads at most the final 2 MiB of the official CLI's ordinary `$GROK_HOME/logs/unified.jsonl` (default `~/.grok/logs/unified.jsonl`). It accepts only the structured `billing: fetched credits config` event and projects the current utilization, period bounds, subscription tier, and event timestamp. It performs no network request, reads no credentials, and never returns or retains other log fields.
|
|
@@ -92,7 +93,7 @@ Quota results are never uploaded or added to incremental sync state. Credential-
|
|
|
92
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 |
|
|
93
94
|
| Qwen Code | `~/.qwen/tmp/` |
|
|
94
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) |
|
|
95
|
-
| 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). |
|
|
96
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) |
|
|
97
98
|
| Amp | `~/.local/share/amp/threads/`; cache-creation tokens are included in input usage |
|
|
98
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
|
|
|
@@ -184,10 +184,13 @@ export async function fetchOpenCodeGoQuota({
|
|
|
184
184
|
}), token);
|
|
185
185
|
}
|
|
186
186
|
if (response.status === 403) {
|
|
187
|
-
// A valid OpenCode key without an active Go subscription.
|
|
187
|
+
// A valid OpenCode key without an active Go subscription. The reason
|
|
188
|
+
// travels machine-readably so a client can say "not subscribed" instead
|
|
189
|
+
// of showing a neutral empty state.
|
|
188
190
|
return attachCacheScope(quotaResult({
|
|
189
191
|
id: PRODUCT_ID,
|
|
190
192
|
status: 'no_data',
|
|
193
|
+
emptyReason: 'notEntitled',
|
|
191
194
|
message: 'OpenCode Go subscription is not active',
|
|
192
195
|
fetchedAt: now,
|
|
193
196
|
}), token);
|
package/src/quotas/schema.js
CHANGED
|
@@ -15,6 +15,19 @@ export const FETCHABLE_QUOTA_PRODUCT_IDS = Object.freeze([
|
|
|
15
15
|
'opencode-go',
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Machine-readable "why there was no window" values, shared with the desktop
|
|
20
|
+
* clients' `EmptyReason` (macOS `RateLimit.swift`) and additive within schema
|
|
21
|
+
* v1: a client that does not know the field ignores it, and a client that does
|
|
22
|
+
* can render the reason instead of a neutral empty state.
|
|
23
|
+
*/
|
|
24
|
+
export const QUOTA_EMPTY_REASONS = Object.freeze([
|
|
25
|
+
'limitReached',
|
|
26
|
+
'noWindow',
|
|
27
|
+
'notEntitled',
|
|
28
|
+
'sessionWithoutPlanLimits',
|
|
29
|
+
]);
|
|
30
|
+
|
|
18
31
|
const FETCH_STATUSES = new Set([
|
|
19
32
|
'ok',
|
|
20
33
|
'no_data',
|
|
@@ -129,6 +142,7 @@ export function quotaResult({
|
|
|
129
142
|
dataAsOf = fetchedAt,
|
|
130
143
|
message,
|
|
131
144
|
source = 'live',
|
|
145
|
+
emptyReason,
|
|
132
146
|
}) {
|
|
133
147
|
if (!FETCHABLE_QUOTA_PRODUCT_IDS.includes(id)) {
|
|
134
148
|
throw new TypeError(`unsupported quota product: ${id}`);
|
|
@@ -147,6 +161,12 @@ export function quotaResult({
|
|
|
147
161
|
if (normalizedDataAsOf) result.dataAsOf = normalizedDataAsOf;
|
|
148
162
|
if (typeof planLabel === 'string' && planLabel.trim()) result.planLabel = planLabel.trim();
|
|
149
163
|
if (typeof message === 'string' && message.trim()) result.message = message.trim();
|
|
164
|
+
if (emptyReason !== undefined && emptyReason !== null) {
|
|
165
|
+
if (!QUOTA_EMPTY_REASONS.includes(emptyReason)) {
|
|
166
|
+
throw new TypeError(`invalid quota emptyReason: ${emptyReason}`);
|
|
167
|
+
}
|
|
168
|
+
result.emptyReason = emptyReason;
|
|
169
|
+
}
|
|
150
170
|
return result;
|
|
151
171
|
}
|
|
152
172
|
|
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',
|