@vibe-cafe/vibe-usage 0.10.4 → 0.10.5
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 +5 -1
- package/package.json +1 -1
- package/src/codex-roots.js +46 -0
- package/src/daemon-service.js +30 -10
- package/src/daemon.js +2 -2
- package/src/index.js +42 -10
- package/src/init.js +5 -4
- package/src/parsers/codex.js +55 -45
- package/src/parsers/dimagent.js +133 -0
- package/src/parsers/index.js +4 -0
- package/src/parsers/mimocode.js +90 -0
- package/src/sync.js +13 -2
- package/src/tools.js +50 -9
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ npx @vibe-cafe/vibe-usage # Init (first run, browser login) or sync
|
|
|
31
31
|
npx @vibe-cafe/vibe-usage init # Re-run setup via browser login
|
|
32
32
|
npx @vibe-cafe/vibe-usage init --manual-key <vbu_...> # Skip browser, use pre-issued key (CI/headless)
|
|
33
33
|
npx @vibe-cafe/vibe-usage sync # Manual sync
|
|
34
|
+
npx @vibe-cafe/vibe-usage sync --extra-codex-home /path/to/.codex # Add another Codex Home for this run only
|
|
34
35
|
npx @vibe-cafe/vibe-usage summary # Print last 7 days as markdown (cost / tokens / by model / by project)
|
|
35
36
|
npx @vibe-cafe/vibe-usage summary --days N # Same, over the last N days (1-90)
|
|
36
37
|
npx @vibe-cafe/vibe-usage daemon # Continuous sync (every 30m, foreground)
|
|
@@ -51,16 +52,18 @@ npx @vibe-cafe/vibe-usage status # Show config & detected tools
|
|
|
51
52
|
| Tool | Data Location |
|
|
52
53
|
|------|---------------|
|
|
53
54
|
| Claude Code | `~/.claude/projects/` (tokens + sessions), `~/.claude/transcripts/` (sessions only); also scans `$CLAUDE_CONFIG_DIR` and data-bearing `~/.claude-*` profiles, selects the most complete duplicate session, and streams logs so large active sessions are not silently omitted. Cache creation tokens are included in input usage. |
|
|
54
|
-
| Codex CLI | `$CODEX_HOME/sessions/` and `$CODEX_HOME/archived_sessions/` (default `~/.codex`)
|
|
55
|
+
| Codex CLI | `$CODEX_HOME/sessions/` and `$CODEX_HOME/archived_sessions/` (default `~/.codex`), plus an optional temporary `--extra-codex-home` or manually persisted `codexExtraHome`; a versioned local index avoids re-reading unchanged rollouts and reads only safe append tails for ordinary sessions, while fork/sub-agent replay matching, duplicate suppression, and live/archive/cross-root deduplication retain their existing semantics |
|
|
55
56
|
| Grok | `$GROK_HOME/sessions/<encoded-cwd>/<session-id>/` (default `~/.grok`); token usage from `updates.jsonl` `turn_completed.usage` (per-model `modelUsage`, cache reads, reasoning); project from `summary.json` cwd; honors `GROK_HOME` |
|
|
56
57
|
| GitHub Copilot CLI | `~/.copilot/session-state/*/events.jsonl` |
|
|
57
58
|
| Cursor | `state.vscdb` (SQLite, reads `cursorAuth/accessToken`, fetches CSV from `cursor.com`); cloud data is stamped with a fixed `cursor-cloud` hostname so multi-machine setups don't double-count |
|
|
59
|
+
| DimAgent | `$DIMCODE_HOME/dimcode.sqlite` (default `~/.dimcode/v2/dimcode.sqlite`); exact usage from `usage_ledger`, with forked ledger/history copies deduplicated |
|
|
58
60
|
| Gemini CLI | `~/.gemini/tmp/<project_hash>/chats/session-*.jsonl` (current line-delimited format) and legacy `session-*.json`; recurses into nested subagent sessions |
|
|
59
61
|
| OpenCode | `~/.local/share/opencode/opencode.db` (SQLite, `json_extract` query) |
|
|
60
62
|
| OpenClaw | `~/.openclaw/agents/`, `~/.openclaw-<profile>/agents/` (profile deployments) |
|
|
61
63
|
| pi | `~/.pi/agent/sessions/` |
|
|
62
64
|
| Qwen Code | `~/.qwen/tmp/` |
|
|
63
65
|
| Kimi Code | Current `~/.kimi-code/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), data root resolved via `$KIMI_CODE_HOME` like the CLI itself, with project names from `session_index.jsonl`; legacy `~/.kimi/sessions/` is parsed alongside (`kimi migrate` never carries usage over, so both stores are always merged) |
|
|
66
|
+
| 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) |
|
|
64
67
|
| Amp | `~/.local/share/amp/threads/` |
|
|
65
68
|
| Droid | `~/.factory/sessions/` |
|
|
66
69
|
| Hermes | `~/.hermes/state.db` + `~/.hermes/profiles/<name>/state.db` (SQLite, multi-profile) |
|
|
@@ -147,6 +150,7 @@ Config stored at `~/.vibe-usage/config.json` (dev: `config.dev.json`).
|
|
|
147
150
|
| `apiKey` | Your API key (starts with `vbu_`) |
|
|
148
151
|
| `apiUrl` | Server URL (default: `https://vibecafe.ai`) |
|
|
149
152
|
| `hostname` | Stable device name for usage tracking (set at init, reused across syncs) |
|
|
153
|
+
| `codexExtraHome` | Optional additional Codex Home scanned together with `$CODEX_HOME` / `~/.codex` |
|
|
150
154
|
|
|
151
155
|
The `hostname` is captured once during `init` and reused for all future syncs. This prevents macOS mDNS hostname changes (e.g., `MacBook-Pro` → `MacBook-Pro-2`) from creating duplicate device entries. To change it manually:
|
|
152
156
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function normalizeHomePath(value) {
|
|
6
|
+
const trimmed = value.trim();
|
|
7
|
+
if (trimmed === '~') return homedir();
|
|
8
|
+
if (trimmed.startsWith('~/') || trimmed.startsWith('~\\')) {
|
|
9
|
+
return resolve(homedir(), trimmed.slice(2));
|
|
10
|
+
}
|
|
11
|
+
return resolve(trimmed);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function primaryCodexHome() {
|
|
15
|
+
const configured = process.env.CODEX_HOME?.trim();
|
|
16
|
+
return configured ? normalizeHomePath(configured) : join(homedir(), '.codex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveCodexHomes(extraCodexHome) {
|
|
20
|
+
const roots = [primaryCodexHome()];
|
|
21
|
+
if (extraCodexHome?.trim()) roots.push(normalizeHomePath(extraCodexHome));
|
|
22
|
+
return [...new Set(roots)];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function codexSessionDirs(codexHome) {
|
|
26
|
+
return [
|
|
27
|
+
join(codexHome, 'sessions'),
|
|
28
|
+
join(codexHome, 'archived_sessions'),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isDirectory(path) {
|
|
33
|
+
try {
|
|
34
|
+
return statSync(path).isDirectory();
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateExtraCodexHome(value) {
|
|
41
|
+
const path = normalizeHomePath(value);
|
|
42
|
+
return {
|
|
43
|
+
ok: isDirectory(path) && codexSessionDirs(path).some(isDirectory),
|
|
44
|
+
path,
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/daemon-service.js
CHANGED
|
@@ -56,10 +56,25 @@ function escapeXml(value) {
|
|
|
56
56
|
.replace(/'/g, ''');
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
const PRESERVED_SERVICE_ENV = ['MIMOCODE_HOME', 'MIMOCODE_DB', 'XDG_DATA_HOME'];
|
|
60
|
+
|
|
61
|
+
function serviceEnvironment(claudeConfigDir, env) {
|
|
62
|
+
const values = {
|
|
63
|
+
CLAUDE_CONFIG_DIR: claudeConfigDir,
|
|
64
|
+
...Object.fromEntries(PRESERVED_SERVICE_ENV.map(key => [key, env[key]?.trim()])),
|
|
65
|
+
};
|
|
66
|
+
return Object.entries(values).filter(([, value]) => value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function generateSystemdUnit(
|
|
70
|
+
nodePath,
|
|
71
|
+
binPath,
|
|
72
|
+
claudeConfigDir = process.env.CLAUDE_CONFIG_DIR?.trim(),
|
|
73
|
+
env = process.env,
|
|
74
|
+
) {
|
|
75
|
+
const environment = serviceEnvironment(claudeConfigDir, env)
|
|
76
|
+
.map(([key, value]) => `Environment="${key}=${escapeSystemdEnvironment(value)}"\n`)
|
|
77
|
+
.join('');
|
|
63
78
|
return `[Unit]
|
|
64
79
|
Description=VibeCafe Usage Tracker
|
|
65
80
|
After=network.target
|
|
@@ -70,18 +85,23 @@ ExecStart=${nodePath} ${binPath} daemon
|
|
|
70
85
|
Restart=on-failure
|
|
71
86
|
RestartSec=10
|
|
72
87
|
Environment=NODE_ENV=production
|
|
73
|
-
${
|
|
88
|
+
${environment}WorkingDirectory=${homedir()}
|
|
74
89
|
|
|
75
90
|
[Install]
|
|
76
91
|
WantedBy=default.target
|
|
77
92
|
`;
|
|
78
93
|
}
|
|
79
94
|
|
|
80
|
-
export function generateLaunchdPlist(
|
|
95
|
+
export function generateLaunchdPlist(
|
|
96
|
+
nodePath,
|
|
97
|
+
binPath,
|
|
98
|
+
claudeConfigDir = process.env.CLAUDE_CONFIG_DIR?.trim(),
|
|
99
|
+
env = process.env,
|
|
100
|
+
) {
|
|
81
101
|
const logDir = join(homedir(), '.vibe-usage');
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
102
|
+
const environment = serviceEnvironment(claudeConfigDir, env)
|
|
103
|
+
.map(([key, value]) => ` <key>${key}</key>\n <string>${escapeXml(value)}</string>\n`)
|
|
104
|
+
.join('');
|
|
85
105
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
86
106
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
87
107
|
<plist version="1.0">
|
|
@@ -108,7 +128,7 @@ export function generateLaunchdPlist(nodePath, binPath, claudeConfigDir = proces
|
|
|
108
128
|
<dict>
|
|
109
129
|
<key>NODE_ENV</key>
|
|
110
130
|
<string>production</string>
|
|
111
|
-
${
|
|
131
|
+
${environment} </dict>
|
|
112
132
|
</dict>
|
|
113
133
|
</plist>
|
|
114
134
|
`;
|
package/src/daemon.js
CHANGED
|
@@ -13,7 +13,7 @@ function sleep(ms) {
|
|
|
13
13
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export async function runDaemon() {
|
|
16
|
+
export async function runDaemon({ codexExtraHome } = {}) {
|
|
17
17
|
const config = loadConfig();
|
|
18
18
|
if (!config?.apiKey) {
|
|
19
19
|
console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
|
|
@@ -34,7 +34,7 @@ export async function runDaemon() {
|
|
|
34
34
|
// eslint-disable-next-line no-constant-condition
|
|
35
35
|
while (true) {
|
|
36
36
|
try {
|
|
37
|
-
await runSync({ throws: true, quiet: true, surface: 'daemon' });
|
|
37
|
+
await runSync({ throws: true, quiet: true, surface: 'daemon', codexExtraHome });
|
|
38
38
|
consecutiveAuthFailures = 0;
|
|
39
39
|
} catch (err) {
|
|
40
40
|
if (err.message === 'UNAUTHORIZED') {
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { loadConfig, saveConfig, getConfigPath } from './config.js';
|
|
2
2
|
import { detectInstalledTools, TOOLS } from './tools.js';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
|
-
import {
|
|
4
|
+
import { validateExtraCodexHome } from './codex-roots.js';
|
|
5
|
+
import { failure, smallHeader } from './output.js';
|
|
5
6
|
|
|
6
7
|
function printSmallHeader() {
|
|
7
8
|
console.log();
|
|
@@ -20,10 +21,14 @@ async function showStatus() {
|
|
|
20
21
|
console.log(` Config: ${getConfigPath()}`);
|
|
21
22
|
console.log(` API key: ${config.apiKey.slice(0, 8)}...`);
|
|
22
23
|
console.log(` API URL: ${config.apiUrl || 'https://vibecafe.ai'}`);
|
|
24
|
+
if (config.codexExtraHome) {
|
|
25
|
+
console.log(` Extra Codex Home: ${config.codexExtraHome}`);
|
|
26
|
+
}
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
console.log('\n Detected tools:');
|
|
26
|
-
const
|
|
30
|
+
const toolOptions = { codexExtraHome: config?.codexExtraHome };
|
|
31
|
+
const detected = detectInstalledTools(toolOptions);
|
|
27
32
|
if (detected.length === 0) {
|
|
28
33
|
console.log(' (none)\n');
|
|
29
34
|
} else {
|
|
@@ -35,13 +40,15 @@ async function showStatus() {
|
|
|
35
40
|
|
|
36
41
|
console.log(' All supported tools:');
|
|
37
42
|
for (const tool of TOOLS) {
|
|
38
|
-
const installed =
|
|
43
|
+
const installed = (tool.detectDataDirs
|
|
44
|
+
? tool.detectDataDirs(toolOptions).length > 0
|
|
45
|
+
: existsSync(tool.dataDir)) ? 'installed' : 'not found';
|
|
39
46
|
console.log(` ${tool.name}: ${installed}`);
|
|
40
47
|
}
|
|
41
48
|
console.log();
|
|
42
49
|
}
|
|
43
50
|
|
|
44
|
-
const VALID_CONFIG_KEYS = ['apiKey', 'apiUrl', 'hostname'];
|
|
51
|
+
const VALID_CONFIG_KEYS = ['apiKey', 'apiUrl', 'hostname', 'codexExtraHome'];
|
|
45
52
|
|
|
46
53
|
function handleConfig(args) {
|
|
47
54
|
const sub = args[0];
|
|
@@ -64,7 +71,7 @@ function handleConfig(args) {
|
|
|
64
71
|
}
|
|
65
72
|
case 'set': {
|
|
66
73
|
const key = args[1];
|
|
67
|
-
|
|
74
|
+
let value = args[2];
|
|
68
75
|
if (!key || value === undefined) {
|
|
69
76
|
console.error('Usage: vibe-usage config set <key> <value>');
|
|
70
77
|
process.exit(1);
|
|
@@ -74,6 +81,14 @@ function handleConfig(args) {
|
|
|
74
81
|
console.error(`Valid keys: ${VALID_CONFIG_KEYS.join(', ')}`);
|
|
75
82
|
process.exit(1);
|
|
76
83
|
}
|
|
84
|
+
if (key === 'codexExtraHome' && value !== '') {
|
|
85
|
+
const validation = validateExtraCodexHome(value);
|
|
86
|
+
if (!validation.ok) {
|
|
87
|
+
console.error(failure(`额外 Codex Home 无效,需要包含 sessions/ 或 archived_sessions/: ${validation.path}`));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
value = validation.path;
|
|
91
|
+
}
|
|
77
92
|
const config = loadConfig() || {};
|
|
78
93
|
config[key] = value;
|
|
79
94
|
saveConfig(config);
|
|
@@ -117,19 +132,30 @@ export async function run(rawArgs) {
|
|
|
117
132
|
if (apiKey === undefined) {
|
|
118
133
|
({ args: stripped, value: apiKey } = extractOption(stripped, 'key'));
|
|
119
134
|
}
|
|
135
|
+
let codexExtraHome;
|
|
136
|
+
({ args: stripped, value: codexExtraHome } = extractOption(stripped, 'extra-codex-home'));
|
|
137
|
+
if (codexExtraHome !== undefined) {
|
|
138
|
+
const validation = validateExtraCodexHome(codexExtraHome);
|
|
139
|
+
if (!validation.ok) {
|
|
140
|
+
console.error(failure(`额外 Codex Home 无效,需要包含 sessions/ 或 archived_sessions/: ${validation.path}`));
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
codexExtraHome = validation.path;
|
|
144
|
+
}
|
|
145
|
+
|
|
120
146
|
const args = stripped;
|
|
121
147
|
const command = args[0];
|
|
122
148
|
|
|
123
149
|
switch (command) {
|
|
124
150
|
case 'init': {
|
|
125
151
|
const { runInit } = await import('./init.js');
|
|
126
|
-
await runInit({ apiKey });
|
|
152
|
+
await runInit({ apiKey, codexExtraHome });
|
|
127
153
|
break;
|
|
128
154
|
}
|
|
129
155
|
case 'sync': {
|
|
130
156
|
printSmallHeader();
|
|
131
157
|
const { runSync } = await import('./sync.js');
|
|
132
|
-
await runSync();
|
|
158
|
+
await runSync({ codexExtraHome });
|
|
133
159
|
break;
|
|
134
160
|
}
|
|
135
161
|
case 'summary': {
|
|
@@ -149,8 +175,12 @@ export async function run(rawArgs) {
|
|
|
149
175
|
if (sub === undefined) {
|
|
150
176
|
// Foreground daemon loop — no header, just start syncing
|
|
151
177
|
const { runDaemon } = await import('./daemon.js');
|
|
152
|
-
await runDaemon();
|
|
178
|
+
await runDaemon({ codexExtraHome });
|
|
153
179
|
} else {
|
|
180
|
+
if (codexExtraHome !== undefined) {
|
|
181
|
+
console.error(failure('后台 daemon 不接受临时 Codex Home,请先运行 `config set codexExtraHome <path>`。'));
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
154
184
|
// manageDaemon validates the subcommand and exits 1 on unknown ones —
|
|
155
185
|
// a typo (e.g. `daemon stauts`) must never fall through to the
|
|
156
186
|
// infinite foreground loop.
|
|
@@ -185,6 +215,7 @@ export async function run(rawArgs) {
|
|
|
185
215
|
npx @vibe-cafe/vibe-usage init Set up via browser login (default)
|
|
186
216
|
npx @vibe-cafe/vibe-usage init --manual-key <vbu_...> Skip browser, use a pre-issued key (CI/headless)
|
|
187
217
|
npx @vibe-cafe/vibe-usage sync Manually sync usage data
|
|
218
|
+
npx @vibe-cafe/vibe-usage sync --extra-codex-home <path> Use another Codex Home for this run
|
|
188
219
|
npx @vibe-cafe/vibe-usage summary Print last 7 days as markdown (cost/tokens/model/project)
|
|
189
220
|
npx @vibe-cafe/vibe-usage summary --days N Same, but over the last N days (1-90)
|
|
190
221
|
npx @vibe-cafe/vibe-usage daemon Continuous sync (every 30m, foreground)
|
|
@@ -201,6 +232,7 @@ export async function run(rawArgs) {
|
|
|
201
232
|
npx @vibe-cafe/vibe-usage config show Show full config as JSON
|
|
202
233
|
npx @vibe-cafe/vibe-usage config get <key> Get a config value
|
|
203
234
|
npx @vibe-cafe/vibe-usage config set <key> <value> Set a config value
|
|
235
|
+
npx @vibe-cafe/vibe-usage config set codexExtraHome <path> Persist another Codex Home
|
|
204
236
|
npx @vibe-cafe/vibe-usage help Show this help
|
|
205
237
|
`);
|
|
206
238
|
break;
|
|
@@ -212,12 +244,12 @@ export async function run(rawArgs) {
|
|
|
212
244
|
if (!config?.apiKey || apiKey) {
|
|
213
245
|
// First run OR user passed --key for a one-shot setup — init.js prints the big header
|
|
214
246
|
const { runInit } = await import('./init.js');
|
|
215
|
-
await runInit({ apiKey });
|
|
247
|
+
await runInit({ apiKey, codexExtraHome });
|
|
216
248
|
} else {
|
|
217
249
|
// Already configured: small header + sync
|
|
218
250
|
printSmallHeader();
|
|
219
251
|
const { runSync } = await import('./sync.js');
|
|
220
|
-
await runSync();
|
|
252
|
+
await runSync({ codexExtraHome });
|
|
221
253
|
}
|
|
222
254
|
break;
|
|
223
255
|
}
|
package/src/init.js
CHANGED
|
@@ -31,7 +31,7 @@ function isDaemonPlatform() {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export async function runInit(options = {}) {
|
|
34
|
-
const { apiKey: providedKey } = options;
|
|
34
|
+
const { apiKey: providedKey, codexExtraHome } = options;
|
|
35
35
|
|
|
36
36
|
console.log(bigHeader());
|
|
37
37
|
|
|
@@ -40,7 +40,7 @@ export async function runInit(options = {}) {
|
|
|
40
40
|
if (providedKey && existing.apiKey === providedKey) {
|
|
41
41
|
console.log(dim('已配置同一个 Key,直接同步数据。'));
|
|
42
42
|
console.log();
|
|
43
|
-
await runSync();
|
|
43
|
+
await runSync({ codexExtraHome });
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
const answer = await prompt('检测到已有配置,是否覆盖? (y/N) ');
|
|
@@ -80,10 +80,11 @@ export async function runInit(options = {}) {
|
|
|
80
80
|
apiKey,
|
|
81
81
|
apiUrl,
|
|
82
82
|
hostname: host,
|
|
83
|
+
...(existing?.codexExtraHome ? { codexExtraHome: existing.codexExtraHome } : {}),
|
|
83
84
|
};
|
|
84
85
|
saveConfig(config);
|
|
85
86
|
|
|
86
|
-
const tools = detectInstalledTools();
|
|
87
|
+
const tools = detectInstalledTools({ codexExtraHome: config.codexExtraHome });
|
|
87
88
|
if (tools.length > 0) {
|
|
88
89
|
console.log(success(`检测到 ${tools.length} 款工具: ${dim(tools.map(t => t.name).join(' · '))}`));
|
|
89
90
|
} else {
|
|
@@ -94,7 +95,7 @@ export async function runInit(options = {}) {
|
|
|
94
95
|
console.log(divider());
|
|
95
96
|
console.log();
|
|
96
97
|
|
|
97
|
-
await runSync();
|
|
98
|
+
await runSync({ codexExtraHome });
|
|
98
99
|
|
|
99
100
|
if (isDaemonPlatform()) {
|
|
100
101
|
if (process.stdin.isTTY) {
|
package/src/parsers/codex.js
CHANGED
|
@@ -8,10 +8,14 @@ import {
|
|
|
8
8
|
statSync,
|
|
9
9
|
} from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
-
import { homedir } from 'node:os';
|
|
12
11
|
import { createInterface } from 'node:readline';
|
|
13
12
|
import { createHash } from 'node:crypto';
|
|
14
13
|
import { aggregateToBuckets } from './index.js';
|
|
14
|
+
import {
|
|
15
|
+
codexSessionDirs,
|
|
16
|
+
resolveCodexHomes,
|
|
17
|
+
validateExtraCodexHome,
|
|
18
|
+
} from '../codex-roots.js';
|
|
15
19
|
import {
|
|
16
20
|
codexCacheEnabled,
|
|
17
21
|
fileSignature,
|
|
@@ -28,17 +32,6 @@ import {
|
|
|
28
32
|
// both, index them together so fork replay-skip works across directories, and
|
|
29
33
|
// select the most complete physical file when the same session briefly exists
|
|
30
34
|
// in both locations during an archive move.
|
|
31
|
-
function getCodexHome() {
|
|
32
|
-
return process.env.CODEX_HOME?.trim() || join(homedir(), '.codex');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function sessionsDirs(codexHome) {
|
|
36
|
-
return [
|
|
37
|
-
join(codexHome, 'sessions'),
|
|
38
|
-
join(codexHome, 'archived_sessions'),
|
|
39
|
-
];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
35
|
/**
|
|
43
36
|
* Recursively find all .jsonl files under a directory.
|
|
44
37
|
* Codex CLI stores sessions as: ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
|
|
@@ -456,10 +449,10 @@ function boundaryKey(boundary) {
|
|
|
456
449
|
return `${boundary.rawTokenCount}:${boundary.recordIndex ?? ''}`;
|
|
457
450
|
}
|
|
458
451
|
|
|
459
|
-
function updateFileCache(
|
|
452
|
+
function updateFileCache(file, changes) {
|
|
460
453
|
const data = cacheData(file.cache, changes);
|
|
461
454
|
try {
|
|
462
|
-
saveCodexFileCache(codexHome, file.filePath, file.signature, data);
|
|
455
|
+
saveCodexFileCache(file.codexHome, file.filePath, file.signature, data);
|
|
463
456
|
} catch {
|
|
464
457
|
// A read-only home, full disk, or antivirus race must only disable the
|
|
465
458
|
// optimization for this run. Raw-log parsing remains the source of truth.
|
|
@@ -803,10 +796,24 @@ function mergeFileResults(results) {
|
|
|
803
796
|
return { buckets: aggregateToBuckets(entries), sessions };
|
|
804
797
|
}
|
|
805
798
|
|
|
806
|
-
export async function parse() {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
799
|
+
export async function parse({ codexExtraHome } = {}) {
|
|
800
|
+
if (codexExtraHome?.trim()) {
|
|
801
|
+
const validation = validateExtraCodexHome(codexExtraHome);
|
|
802
|
+
if (!validation.ok) {
|
|
803
|
+
return {
|
|
804
|
+
buckets: [],
|
|
805
|
+
sessions: [],
|
|
806
|
+
skipped: true,
|
|
807
|
+
warnings: [`codex: 额外 Codex Home 不可用,已跳过本次 Codex 同步: ${validation.path}`],
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const codexHomes = resolveCodexHomes(codexExtraHome);
|
|
813
|
+
const dirs = codexHomes.flatMap(codexHome => (
|
|
814
|
+
codexSessionDirs(codexHome).map(dir => ({ codexHome, dir }))
|
|
815
|
+
));
|
|
816
|
+
if (!dirs.some(({ dir }) => existsSync(dir))) return { buckets: [], sessions: [] };
|
|
810
817
|
|
|
811
818
|
const startedAt = Date.now();
|
|
812
819
|
const budget = workBudgetMs();
|
|
@@ -820,29 +827,32 @@ export async function parse() {
|
|
|
820
827
|
audited: 0,
|
|
821
828
|
};
|
|
822
829
|
const files = [];
|
|
823
|
-
for (const
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
830
|
+
for (const { codexHome, dir } of dirs) {
|
|
831
|
+
for (const filePath of findJsonlFiles(dir)) {
|
|
832
|
+
try {
|
|
833
|
+
const stat = statSync(filePath);
|
|
834
|
+
if (stat.size <= 0) continue;
|
|
835
|
+
const signature = fileSignature(stat);
|
|
836
|
+
const cache = loadCodexFileCache(codexHome, filePath, signature);
|
|
837
|
+
const priorCache = cache || loadCodexFileCache(codexHome, filePath);
|
|
838
|
+
const priorTail = cache ? null : loadCodexFileTail(codexHome, filePath);
|
|
839
|
+
const file = {
|
|
840
|
+
codexHome,
|
|
841
|
+
filePath,
|
|
842
|
+
snapshotSize: stat.size,
|
|
843
|
+
signature,
|
|
844
|
+
cache,
|
|
845
|
+
priorCache,
|
|
846
|
+
priorTail,
|
|
847
|
+
header: null,
|
|
848
|
+
appendTail: null,
|
|
849
|
+
};
|
|
850
|
+
if (!cache && priorCache) file.appendTail = tailStateFor(file);
|
|
851
|
+
files.push(file);
|
|
852
|
+
} catch {
|
|
853
|
+
// The file may move to archived_sessions between discovery and stat.
|
|
854
|
+
// Its archived copy will be picked up on the next sync.
|
|
855
|
+
}
|
|
846
856
|
}
|
|
847
857
|
}
|
|
848
858
|
if (files.length === 0) return { buckets: [], sessions: [] };
|
|
@@ -875,7 +885,7 @@ export async function parse() {
|
|
|
875
885
|
try {
|
|
876
886
|
file.header = await readSessionHeader(file.filePath, file.snapshotSize);
|
|
877
887
|
cacheStats.filesRead++;
|
|
878
|
-
updateFileCache(
|
|
888
|
+
updateFileCache(file, { header: file.header });
|
|
879
889
|
} catch {
|
|
880
890
|
continue;
|
|
881
891
|
}
|
|
@@ -928,7 +938,7 @@ export async function parse() {
|
|
|
928
938
|
try {
|
|
929
939
|
meta = await indexSessionFile(file.filePath, file.snapshotSize);
|
|
930
940
|
cacheStats.filesRead++;
|
|
931
|
-
updateFileCache(
|
|
941
|
+
updateFileCache(file, { index: meta });
|
|
932
942
|
} catch {
|
|
933
943
|
continue;
|
|
934
944
|
}
|
|
@@ -984,14 +994,14 @@ export async function parse() {
|
|
|
984
994
|
const { tail, ...summary } = parsed;
|
|
985
995
|
if (tail) {
|
|
986
996
|
try {
|
|
987
|
-
saveCodexFileTail(codexHome, file.filePath, file.signature, tail);
|
|
997
|
+
saveCodexFileTail(file.codexHome, file.filePath, file.signature, tail);
|
|
988
998
|
} catch {
|
|
989
999
|
// Same fail-open rule as the summary cache: tail acceleration is
|
|
990
1000
|
// optional and a write failure must not fail the parser.
|
|
991
1001
|
}
|
|
992
1002
|
}
|
|
993
1003
|
result = { boundaryKey: key, ...summary };
|
|
994
|
-
updateFileCache(
|
|
1004
|
+
updateFileCache(file, { result, lastAuditedAt: Date.now() });
|
|
995
1005
|
file.appendTail = null;
|
|
996
1006
|
file.priorTail = null;
|
|
997
1007
|
if (auditPaths.has(file.filePath)) cacheStats.audited++;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
3
|
+
import { queryDbJson } from './sqlite.js';
|
|
4
|
+
import { getDimAgentDbPath } from '../tools.js';
|
|
5
|
+
|
|
6
|
+
const SOURCE = 'dimagent';
|
|
7
|
+
const FORKED_LEDGER_ID = /^ledger_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
+
|
|
9
|
+
function projectName(cwd) {
|
|
10
|
+
if (!cwd) return 'unknown';
|
|
11
|
+
const parts = String(cwd).replace(/[/\\]+$/, '').split(/[/\\]/);
|
|
12
|
+
return parts.at(-1) || 'unknown';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function tokenCount(value) {
|
|
16
|
+
const count = Number(value);
|
|
17
|
+
return Number.isFinite(count) && count > 0 ? count : 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function usageSignature(row) {
|
|
21
|
+
return [
|
|
22
|
+
row.runId || '',
|
|
23
|
+
row.providerId || '',
|
|
24
|
+
row.modelId || '',
|
|
25
|
+
row.usage || '',
|
|
26
|
+
row.cost ?? '',
|
|
27
|
+
row.createdAt || '',
|
|
28
|
+
].join('\0');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseUsageRows(rows) {
|
|
32
|
+
const entries = [];
|
|
33
|
+
const originalSignatures = new Set(
|
|
34
|
+
rows
|
|
35
|
+
.filter(row => !FORKED_LEDGER_ID.test(row.ledgerId || ''))
|
|
36
|
+
.map(usageSignature),
|
|
37
|
+
);
|
|
38
|
+
const keptOrphanClones = new Set();
|
|
39
|
+
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
const signature = usageSignature(row);
|
|
42
|
+
if (FORKED_LEDGER_ID.test(row.ledgerId || '')) {
|
|
43
|
+
if (originalSignatures.has(signature) || keptOrphanClones.has(signature)) continue;
|
|
44
|
+
keptOrphanClones.add(signature);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let usage;
|
|
48
|
+
try {
|
|
49
|
+
usage = typeof row.usage === 'string' ? JSON.parse(row.usage) : row.usage;
|
|
50
|
+
} catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!usage || typeof usage !== 'object') continue;
|
|
54
|
+
|
|
55
|
+
const timestamp = new Date(row.createdAt);
|
|
56
|
+
if (Number.isNaN(timestamp.getTime())) continue;
|
|
57
|
+
|
|
58
|
+
const promptTokens = tokenCount(usage.promptTokens);
|
|
59
|
+
const cachedInputTokens = tokenCount(usage.cacheReadTokens);
|
|
60
|
+
const inputTokens = Math.max(0, promptTokens - cachedInputTokens);
|
|
61
|
+
const outputTokens = tokenCount(usage.completionTokens);
|
|
62
|
+
if (inputTokens + outputTokens + cachedInputTokens === 0) continue;
|
|
63
|
+
|
|
64
|
+
entries.push({
|
|
65
|
+
source: SOURCE,
|
|
66
|
+
model: row.modelId || 'unknown',
|
|
67
|
+
project: projectName(row.cwd),
|
|
68
|
+
timestamp,
|
|
69
|
+
inputTokens,
|
|
70
|
+
outputTokens,
|
|
71
|
+
cachedInputTokens,
|
|
72
|
+
reasoningOutputTokens: 0,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return entries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function queryDb(dbPath, sql) {
|
|
80
|
+
try {
|
|
81
|
+
return queryDbJson(dbPath, sql);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err.code === 'ENOENT' || err.status === 127 || err.message?.includes('ENOENT')) {
|
|
84
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync DimAgent data.');
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function parse() {
|
|
91
|
+
const dbPath = getDimAgentDbPath();
|
|
92
|
+
if (!existsSync(dbPath)) return { buckets: [], sessions: [] };
|
|
93
|
+
|
|
94
|
+
const usageRows = queryDb(dbPath, `SELECT
|
|
95
|
+
u.ledgerId,
|
|
96
|
+
u.runId,
|
|
97
|
+
u.providerId,
|
|
98
|
+
u.modelId,
|
|
99
|
+
u.usage,
|
|
100
|
+
u.cost,
|
|
101
|
+
u.createdAt,
|
|
102
|
+
s.cwd
|
|
103
|
+
FROM usage_ledger u
|
|
104
|
+
LEFT JOIN sessions s ON s.sessionId = u.sessionId`);
|
|
105
|
+
|
|
106
|
+
const messageRows = queryDb(dbPath, `SELECT
|
|
107
|
+
m.sessionId,
|
|
108
|
+
m.role,
|
|
109
|
+
m.createdAt,
|
|
110
|
+
s.cwd
|
|
111
|
+
FROM messages m
|
|
112
|
+
LEFT JOIN sessions s ON s.sessionId = m.sessionId
|
|
113
|
+
WHERE m.role IN ('user', 'assistant')
|
|
114
|
+
AND m.messageId NOT LIKE 'msg_fork_%'`);
|
|
115
|
+
|
|
116
|
+
const sessionEvents = [];
|
|
117
|
+
for (const row of messageRows) {
|
|
118
|
+
const timestamp = new Date(row.createdAt);
|
|
119
|
+
if (Number.isNaN(timestamp.getTime())) continue;
|
|
120
|
+
sessionEvents.push({
|
|
121
|
+
sessionId: row.sessionId,
|
|
122
|
+
source: SOURCE,
|
|
123
|
+
project: projectName(row.cwd),
|
|
124
|
+
timestamp,
|
|
125
|
+
role: row.role,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
buckets: aggregateToBuckets(parseUsageRows(usageRows)),
|
|
131
|
+
sessions: extractSessions(sessionEvents),
|
|
132
|
+
};
|
|
133
|
+
}
|
package/src/parsers/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parse as parseCline } from './cline.js';
|
|
|
4
4
|
import { parse as parseCodex } from './codex.js';
|
|
5
5
|
import { parse as parseCopilotCli } from './copilot-cli.js';
|
|
6
6
|
import { parse as parseCursor } from './cursor.js';
|
|
7
|
+
import { parse as parseDimAgent } from './dimagent.js';
|
|
7
8
|
import { parse as parseRooCode } from './roo-code.js';
|
|
8
9
|
import { parse as parseGeminiCli } from './gemini-cli.js';
|
|
9
10
|
import { parse as parseGrok } from './grok.js';
|
|
@@ -16,6 +17,7 @@ import { parse as parseDroid } from './droid.js';
|
|
|
16
17
|
import { parse as parseAntigravity } from './antigravity.js';
|
|
17
18
|
import { parse as parseHermes } from './hermes.js';
|
|
18
19
|
import { parse as parseKiro } from './kiro.js';
|
|
20
|
+
import { parse as parseMimocode } from './mimocode.js';
|
|
19
21
|
import { parse as parsePiCodingAgent } from './pi-coding-agent.js';
|
|
20
22
|
import { parse as parseZcode } from './zcode.js';
|
|
21
23
|
import { parse as parseTraeCli } from './trae-cli.js';
|
|
@@ -26,6 +28,7 @@ export const parsers = {
|
|
|
26
28
|
'grok': parseGrok,
|
|
27
29
|
'copilot-cli': parseCopilotCli,
|
|
28
30
|
'cursor': parseCursor,
|
|
31
|
+
'dimagent': parseDimAgent,
|
|
29
32
|
'gemini-cli': parseGeminiCli,
|
|
30
33
|
'opencode': parseOpencode,
|
|
31
34
|
'openclaw': parseOpenclaw,
|
|
@@ -38,6 +41,7 @@ export const parsers = {
|
|
|
38
41
|
'trae-cli': parseTraeCli,
|
|
39
42
|
'hermes': parseHermes,
|
|
40
43
|
'kiro': parseKiro,
|
|
44
|
+
'mimocode': parseMimocode,
|
|
41
45
|
'cline': parseCline,
|
|
42
46
|
'roo-code': parseRooCode,
|
|
43
47
|
'zcode': parseZcode,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { getMimocodeDbPath } from '../tools.js';
|
|
4
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
5
|
+
import { queryDbJson } from './sqlite.js';
|
|
6
|
+
|
|
7
|
+
export { getMimocodeDbPath as resolveMimocodeDbPath };
|
|
8
|
+
|
|
9
|
+
export async function parse() {
|
|
10
|
+
const dbPath = getMimocodeDbPath();
|
|
11
|
+
if (!existsSync(dbPath)) return { buckets: [], sessions: [] };
|
|
12
|
+
|
|
13
|
+
let rows;
|
|
14
|
+
try {
|
|
15
|
+
const hasExternalImports = queryDbJson(dbPath, `
|
|
16
|
+
SELECT 1
|
|
17
|
+
FROM sqlite_master
|
|
18
|
+
WHERE type = 'table' AND name = 'external_import'
|
|
19
|
+
LIMIT 1
|
|
20
|
+
`).length > 0;
|
|
21
|
+
const externalImportJoin = hasExternalImports
|
|
22
|
+
? 'LEFT JOIN external_import ON external_import.session_id = message.session_id'
|
|
23
|
+
: '';
|
|
24
|
+
const externalImportFilter = hasExternalImports
|
|
25
|
+
? 'WHERE external_import.session_id IS NULL'
|
|
26
|
+
: '';
|
|
27
|
+
rows = queryDbJson(dbPath, `
|
|
28
|
+
SELECT
|
|
29
|
+
message.session_id AS sessionID,
|
|
30
|
+
message.time_created AS created,
|
|
31
|
+
message.data AS data,
|
|
32
|
+
session.directory AS directory
|
|
33
|
+
FROM message
|
|
34
|
+
JOIN session ON session.id = message.session_id
|
|
35
|
+
${externalImportJoin}
|
|
36
|
+
${externalImportFilter}
|
|
37
|
+
`);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
if (err.status === 127 || err.message?.includes('ENOENT')) {
|
|
40
|
+
throw new Error('sqlite3 CLI not found. Install sqlite3 (or use Node >= 22.5) to sync MiMoCode data.');
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const entries = [];
|
|
46
|
+
const events = [];
|
|
47
|
+
for (const row of rows) {
|
|
48
|
+
let data;
|
|
49
|
+
try {
|
|
50
|
+
data = typeof row.data === 'string' ? JSON.parse(row.data) : row.data;
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (data?.role !== 'user' && data?.role !== 'assistant') continue;
|
|
55
|
+
|
|
56
|
+
const timestamp = new Date(data.time?.created ?? row.created);
|
|
57
|
+
if (Number.isNaN(timestamp.getTime())) continue;
|
|
58
|
+
const project = row.directory ? basename(row.directory) : 'unknown';
|
|
59
|
+
events.push({
|
|
60
|
+
sessionId: row.sessionID || 'unknown',
|
|
61
|
+
source: 'mimocode',
|
|
62
|
+
project,
|
|
63
|
+
timestamp,
|
|
64
|
+
role: data.role,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const tokens = data.tokens;
|
|
68
|
+
if (data.role !== 'assistant' || !data.modelID || !tokens) continue;
|
|
69
|
+
const inputTokens = (Number(tokens.input) || 0) + (Number(tokens.cache?.write) || 0);
|
|
70
|
+
const outputTokens = Number(tokens.output) || 0;
|
|
71
|
+
const reasoningOutputTokens = Number(tokens.reasoning) || 0;
|
|
72
|
+
const cachedInputTokens = Number(tokens.cache?.read) || 0;
|
|
73
|
+
if (inputTokens + outputTokens + reasoningOutputTokens + cachedInputTokens <= 0) continue;
|
|
74
|
+
entries.push({
|
|
75
|
+
source: 'mimocode',
|
|
76
|
+
model: data.modelID,
|
|
77
|
+
project,
|
|
78
|
+
timestamp,
|
|
79
|
+
inputTokens,
|
|
80
|
+
outputTokens,
|
|
81
|
+
cachedInputTokens,
|
|
82
|
+
reasoningOutputTokens,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
buckets: aggregateToBuckets(entries),
|
|
88
|
+
sessions: extractSessions(events),
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/sync.js
CHANGED
|
@@ -27,7 +27,16 @@ export function resolveUploadProjectSetting(settings) {
|
|
|
27
27
|
return settings.uploadProject;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export
|
|
30
|
+
export function resolveCodexExtraHome(configured, temporary) {
|
|
31
|
+
return temporary ?? configured;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function runSync({
|
|
35
|
+
throws = false,
|
|
36
|
+
quiet = false,
|
|
37
|
+
surface = 'cli',
|
|
38
|
+
codexExtraHome,
|
|
39
|
+
} = {}) {
|
|
31
40
|
const config = loadConfig();
|
|
32
41
|
if (!config?.apiKey) {
|
|
33
42
|
console.error(failure('尚未配置,请先运行 `npx @vibe-cafe/vibe-usage init`。'));
|
|
@@ -72,7 +81,9 @@ export async function runSync({ throws = false, quiet = false, surface = 'cli' }
|
|
|
72
81
|
|
|
73
82
|
for (const [source, parse] of Object.entries(parsers)) {
|
|
74
83
|
try {
|
|
75
|
-
const result =
|
|
84
|
+
const result = source === 'codex'
|
|
85
|
+
? await parse({ codexExtraHome: resolveCodexExtraHome(config.codexExtraHome, codexExtraHome) })
|
|
86
|
+
: await parse();
|
|
76
87
|
const buckets = Array.isArray(result) ? result : result.buckets;
|
|
77
88
|
const sessions = Array.isArray(result) ? [] : (result.sessions || []);
|
|
78
89
|
if (!Array.isArray(buckets) || !Array.isArray(sessions)) {
|
package/src/tools.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { findClaudeCodeDataDirs } from './claude-roots.js';
|
|
5
|
+
import { codexSessionDirs, resolveCodexHomes } from './codex-roots.js';
|
|
5
6
|
|
|
6
7
|
function getCursorStateDbPath() {
|
|
7
8
|
const rel = join('User', 'globalStorage', 'state.vscdb');
|
|
@@ -84,11 +85,10 @@ function findOpenclawDataDirs() {
|
|
|
84
85
|
// Codex keeps live sessions in ~/.codex/sessions and moves completed ones to
|
|
85
86
|
// ~/.codex/archived_sessions. Detect Codex if either dir exists, so a user
|
|
86
87
|
// whose sessions have all been archived is still recognized.
|
|
87
|
-
function findCodexDataDirs() {
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
].filter(existsSync);
|
|
88
|
+
export function findCodexDataDirs(codexExtraHome) {
|
|
89
|
+
return resolveCodexHomes(codexExtraHome)
|
|
90
|
+
.flatMap(codexSessionDirs)
|
|
91
|
+
.filter(existsSync);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// Kimi Code moved its store from ~/.kimi to ~/.kimi-code; recognize either so
|
|
@@ -100,6 +100,17 @@ function findKimiCodeDataDirs() {
|
|
|
100
100
|
].filter(existsSync);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
export function getMimocodeDbPath(env = process.env) {
|
|
104
|
+
if (env.MIMOCODE_HOME && !isAbsolute(env.MIMOCODE_HOME)) {
|
|
105
|
+
throw new Error(`MIMOCODE_HOME must be an absolute path, got: ${JSON.stringify(env.MIMOCODE_HOME)}`);
|
|
106
|
+
}
|
|
107
|
+
const dataDir = env.MIMOCODE_HOME
|
|
108
|
+
? join(env.MIMOCODE_HOME, 'data')
|
|
109
|
+
: join(env.XDG_DATA_HOME || join(homedir(), '.local', 'share'), 'mimocode');
|
|
110
|
+
if (!env.MIMOCODE_DB) return join(dataDir, 'mimocode.db');
|
|
111
|
+
return isAbsolute(env.MIMOCODE_DB) ? env.MIMOCODE_DB : join(dataDir, env.MIMOCODE_DB);
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
function findAntigravityDataDirs() {
|
|
104
115
|
return [
|
|
105
116
|
join(homedir(), '.gemini', 'antigravity'),
|
|
@@ -145,6 +156,24 @@ export function findGrokDataDirs() {
|
|
|
145
156
|
return [join(getGrokHome(), 'sessions')].filter(existsSync);
|
|
146
157
|
}
|
|
147
158
|
|
|
159
|
+
export function getDimAgentDbPath() {
|
|
160
|
+
const override = process.env.VIBE_USAGE_DIMAGENT_DB?.trim();
|
|
161
|
+
if (override) return resolve(override);
|
|
162
|
+
|
|
163
|
+
const explicitHome = process.env.DIMCODE_HOME?.trim();
|
|
164
|
+
if (explicitHome) return join(resolve(explicitHome), 'dimcode.sqlite');
|
|
165
|
+
|
|
166
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME?.trim();
|
|
167
|
+
const home = xdgConfigHome
|
|
168
|
+
? resolve(xdgConfigHome, '.dimcode', 'v2')
|
|
169
|
+
: join(homedir(), '.dimcode', 'v2');
|
|
170
|
+
return join(home, 'dimcode.sqlite');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function findDimAgentDataDirs() {
|
|
174
|
+
return [getDimAgentDbPath()].filter(existsSync);
|
|
175
|
+
}
|
|
176
|
+
|
|
148
177
|
export const TOOLS = [
|
|
149
178
|
{
|
|
150
179
|
name: 'Claude Code',
|
|
@@ -156,7 +185,7 @@ export const TOOLS = [
|
|
|
156
185
|
name: 'Codex CLI',
|
|
157
186
|
id: 'codex',
|
|
158
187
|
dataDir: join(homedir(), '.codex', 'sessions'),
|
|
159
|
-
detectDataDirs: findCodexDataDirs,
|
|
188
|
+
detectDataDirs: ({ codexExtraHome } = {}) => findCodexDataDirs(codexExtraHome),
|
|
160
189
|
},
|
|
161
190
|
{
|
|
162
191
|
name: 'Grok',
|
|
@@ -174,6 +203,12 @@ export const TOOLS = [
|
|
|
174
203
|
id: 'cursor',
|
|
175
204
|
dataDir: getCursorStateDbPath(),
|
|
176
205
|
},
|
|
206
|
+
{
|
|
207
|
+
name: 'DimAgent',
|
|
208
|
+
id: 'dimagent',
|
|
209
|
+
dataDir: getDimAgentDbPath(),
|
|
210
|
+
detectDataDirs: findDimAgentDataDirs,
|
|
211
|
+
},
|
|
177
212
|
{
|
|
178
213
|
name: 'Gemini CLI',
|
|
179
214
|
id: 'gemini-cli',
|
|
@@ -208,6 +243,12 @@ export const TOOLS = [
|
|
|
208
243
|
dataDir: join(homedir(), '.kimi-code', 'sessions'),
|
|
209
244
|
detectDataDirs: findKimiCodeDataDirs,
|
|
210
245
|
},
|
|
246
|
+
{
|
|
247
|
+
name: 'MiMoCode',
|
|
248
|
+
id: 'mimocode',
|
|
249
|
+
dataDir: join(homedir(), '.local', 'share', 'mimocode', 'mimocode.db'),
|
|
250
|
+
detectDataDirs: () => [getMimocodeDbPath()].filter(existsSync),
|
|
251
|
+
},
|
|
211
252
|
{
|
|
212
253
|
name: 'Amp',
|
|
213
254
|
id: 'amp',
|
|
@@ -259,9 +300,9 @@ export const TOOLS = [
|
|
|
259
300
|
},
|
|
260
301
|
];
|
|
261
302
|
|
|
262
|
-
export function detectInstalledTools() {
|
|
303
|
+
export function detectInstalledTools(options = {}) {
|
|
263
304
|
return TOOLS.filter(t => {
|
|
264
|
-
if (t.detectDataDirs) return t.detectDataDirs().length > 0;
|
|
305
|
+
if (t.detectDataDirs) return t.detectDataDirs(options).length > 0;
|
|
265
306
|
return existsSync(t.dataDir);
|
|
266
307
|
});
|
|
267
308
|
}
|