@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibe-cafe/vibe-usage",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Track your AI coding tool token usage and sync to vibecafe.ai",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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',
@@ -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 default layout.
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 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
- }
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
- 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
- ) {
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 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
- });
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);
@@ -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
- /** mcode runtime database: VIBE_USAGE_MCODE_DB wins, then MCODE_HOME, then ~/.minimax. */
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
- if (env.MCODE_HOME && !isAbsolute(env.MCODE_HOME)) {
184
- throw new Error(`MCODE_HOME must be an absolute path, got: ${JSON.stringify(env.MCODE_HOME)}`);
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
- const root = env.MCODE_HOME || join(home, '.minimax');
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: () => [getMcodeDbPath()].filter(existsSync),
480
+ detectDataDirs: () => getMcodeDbPaths().filter(existsSync),
421
481
  },
422
482
  {
423
483
  name: 'MiMoCode',