@vibe-cafe/vibe-usage 0.6.8 → 0.7.1
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 +28 -2
- package/package.json +1 -1
- package/src/daemon-service.js +272 -0
- package/src/index.js +14 -3
- package/src/init.js +4 -0
- package/src/parsers/antigravity.js +346 -0
- package/src/parsers/index.js +2 -0
- package/src/tools.js +5 -0
package/README.md
CHANGED
|
@@ -19,7 +19,12 @@ This will:
|
|
|
19
19
|
npx @vibe-cafe/vibe-usage # Init (first run) or sync (subsequent runs)
|
|
20
20
|
npx @vibe-cafe/vibe-usage init # Re-run setup
|
|
21
21
|
npx @vibe-cafe/vibe-usage sync # Manual sync
|
|
22
|
-
npx @vibe-cafe/vibe-usage daemon # Continuous sync (every
|
|
22
|
+
npx @vibe-cafe/vibe-usage daemon # Continuous sync (every 5m, foreground)
|
|
23
|
+
npx @vibe-cafe/vibe-usage daemon install # Install background service (systemd/launchd)
|
|
24
|
+
npx @vibe-cafe/vibe-usage daemon uninstall # Remove background service
|
|
25
|
+
npx @vibe-cafe/vibe-usage daemon status # Show background service status
|
|
26
|
+
npx @vibe-cafe/vibe-usage daemon stop # Stop background service
|
|
27
|
+
npx @vibe-cafe/vibe-usage daemon restart # Restart background service
|
|
23
28
|
npx @vibe-cafe/vibe-usage reset # Delete all data and re-upload from local logs
|
|
24
29
|
npx @vibe-cafe/vibe-usage reset --local # Delete this host's data only and re-upload
|
|
25
30
|
npx @vibe-cafe/vibe-usage skill # Install skill for AI coding assistants
|
|
@@ -89,13 +94,34 @@ Config stored at `~/.vibe-usage/config.json` (dev: `config.dev.json`). Contains
|
|
|
89
94
|
|
|
90
95
|
## Daemon Mode
|
|
91
96
|
|
|
97
|
+
### Background service (recommended)
|
|
98
|
+
|
|
99
|
+
Install as a system service for automatic background syncing:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx @vibe-cafe/vibe-usage daemon install
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This creates a user-level service (systemd on Linux, launchd on macOS) that syncs every 5 minutes and starts automatically on login. Manage with:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx @vibe-cafe/vibe-usage daemon status
|
|
109
|
+
npx @vibe-cafe/vibe-usage daemon stop
|
|
110
|
+
npx @vibe-cafe/vibe-usage daemon restart
|
|
111
|
+
npx @vibe-cafe/vibe-usage daemon uninstall
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
For reliable operation, install globally first: `npm install -g @vibe-cafe/vibe-usage`
|
|
115
|
+
|
|
116
|
+
### Foreground mode
|
|
117
|
+
|
|
92
118
|
Run continuous syncing in the foreground (every 5 minutes):
|
|
93
119
|
|
|
94
120
|
```bash
|
|
95
121
|
npx @vibe-cafe/vibe-usage daemon
|
|
96
122
|
```
|
|
97
123
|
|
|
98
|
-
Press Ctrl+C to stop.
|
|
124
|
+
Press Ctrl+C to stop.
|
|
99
125
|
|
|
100
126
|
## License
|
|
101
127
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { writeFileSync, unlinkSync, mkdirSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir, platform } from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const SERVICE_NAME = 'vibe-usage';
|
|
8
|
+
const LAUNCHD_LABEL = 'ai.vibecafe.vibe-usage';
|
|
9
|
+
|
|
10
|
+
function detectPlatform() {
|
|
11
|
+
const os = platform();
|
|
12
|
+
if (os === 'linux') {
|
|
13
|
+
if (existsSync('/run/systemd/system')) return 'systemd';
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (os === 'darwin') {
|
|
17
|
+
return 'launchd';
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolvePaths() {
|
|
23
|
+
const nodePath = process.execPath;
|
|
24
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
25
|
+
const binPath = join(thisFile, '..', '..', 'bin', 'vibe-usage.js');
|
|
26
|
+
|
|
27
|
+
// npx cache paths are unstable — service will break when cache is cleared
|
|
28
|
+
const isNpxCache = binPath.includes('.npm/_npx');
|
|
29
|
+
|
|
30
|
+
return { nodePath, binPath, isNpxCache };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getServicePaths(plat) {
|
|
34
|
+
if (plat === 'systemd') {
|
|
35
|
+
const dir = join(homedir(), '.config', 'systemd', 'user');
|
|
36
|
+
return { dir, file: join(dir, `${SERVICE_NAME}.service`) };
|
|
37
|
+
}
|
|
38
|
+
if (plat === 'launchd') {
|
|
39
|
+
const dir = join(homedir(), 'Library', 'LaunchAgents');
|
|
40
|
+
return { dir, file: join(dir, `${LAUNCHD_LABEL}.plist`) };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function generateSystemdUnit(nodePath, binPath) {
|
|
46
|
+
return `[Unit]
|
|
47
|
+
Description=VibeCafe Usage Tracker
|
|
48
|
+
After=network.target
|
|
49
|
+
|
|
50
|
+
[Service]
|
|
51
|
+
Type=simple
|
|
52
|
+
ExecStart=${nodePath} ${binPath} daemon
|
|
53
|
+
Restart=on-failure
|
|
54
|
+
RestartSec=10
|
|
55
|
+
Environment=NODE_ENV=production
|
|
56
|
+
WorkingDirectory=${homedir()}
|
|
57
|
+
|
|
58
|
+
[Install]
|
|
59
|
+
WantedBy=default.target
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function generateLaunchdPlist(nodePath, binPath) {
|
|
64
|
+
const logDir = join(homedir(), '.vibe-usage');
|
|
65
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
66
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
67
|
+
<plist version="1.0">
|
|
68
|
+
<dict>
|
|
69
|
+
<key>Label</key>
|
|
70
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
71
|
+
<key>ProgramArguments</key>
|
|
72
|
+
<array>
|
|
73
|
+
<string>${nodePath}</string>
|
|
74
|
+
<string>${binPath}</string>
|
|
75
|
+
<string>daemon</string>
|
|
76
|
+
</array>
|
|
77
|
+
<key>RunAtLoad</key>
|
|
78
|
+
<true/>
|
|
79
|
+
<key>KeepAlive</key>
|
|
80
|
+
<true/>
|
|
81
|
+
<key>WorkingDirectory</key>
|
|
82
|
+
<string>${homedir()}</string>
|
|
83
|
+
<key>StandardOutPath</key>
|
|
84
|
+
<string>${join(logDir, 'daemon.log')}</string>
|
|
85
|
+
<key>StandardErrorPath</key>
|
|
86
|
+
<string>${join(logDir, 'daemon.err')}</string>
|
|
87
|
+
<key>EnvironmentVariables</key>
|
|
88
|
+
<dict>
|
|
89
|
+
<key>NODE_ENV</key>
|
|
90
|
+
<string>production</string>
|
|
91
|
+
</dict>
|
|
92
|
+
</dict>
|
|
93
|
+
</plist>
|
|
94
|
+
`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function run(cmd, args) {
|
|
98
|
+
try {
|
|
99
|
+
const output = execFileSync(cmd, args, {
|
|
100
|
+
encoding: 'utf-8',
|
|
101
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
102
|
+
});
|
|
103
|
+
return { ok: true, output: output.trim() };
|
|
104
|
+
} catch (err) {
|
|
105
|
+
return { ok: false, output: (err.stderr || err.stdout || err.message || '').trim() };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function install() {
|
|
110
|
+
const plat = detectPlatform();
|
|
111
|
+
if (!plat) {
|
|
112
|
+
console.log('Daemon install is not supported on this platform.');
|
|
113
|
+
console.log('Supported: Linux (systemd), macOS (launchd).');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const { nodePath, binPath, isNpxCache } = resolvePaths();
|
|
118
|
+
|
|
119
|
+
if (isNpxCache) {
|
|
120
|
+
console.log('Warning: vibe-usage appears to be running from the npx cache.');
|
|
121
|
+
console.log('The daemon may break when the cache is cleared.');
|
|
122
|
+
console.log('For reliable operation, install globally first:');
|
|
123
|
+
console.log(' npm install -g @vibe-cafe/vibe-usage\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const paths = getServicePaths(plat);
|
|
127
|
+
|
|
128
|
+
if (existsSync(paths.file)) {
|
|
129
|
+
console.log('Service is already installed. Run `vibe-usage daemon restart` or `daemon uninstall` first.');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
mkdirSync(paths.dir, { recursive: true });
|
|
134
|
+
|
|
135
|
+
if (plat === 'systemd') {
|
|
136
|
+
writeFileSync(paths.file, generateSystemdUnit(nodePath, binPath), 'utf-8');
|
|
137
|
+
console.log(`Created ${paths.file}`);
|
|
138
|
+
|
|
139
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
140
|
+
const result = run('systemctl', ['--user', 'enable', '--now', `${SERVICE_NAME}.service`]);
|
|
141
|
+
if (!result.ok) {
|
|
142
|
+
console.error(`Failed to start service: ${result.output}`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
console.log('Service enabled and started.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (plat === 'launchd') {
|
|
149
|
+
mkdirSync(join(homedir(), '.vibe-usage'), { recursive: true });
|
|
150
|
+
writeFileSync(paths.file, generateLaunchdPlist(nodePath, binPath), 'utf-8');
|
|
151
|
+
console.log(`Created ${paths.file}`);
|
|
152
|
+
|
|
153
|
+
const result = run('launchctl', ['load', paths.file]);
|
|
154
|
+
if (!result.ok) {
|
|
155
|
+
console.error(`Failed to load service: ${result.output}`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log('Service loaded and started.');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log('\nDaemon installed. Usage data will sync automatically every 5 minutes.');
|
|
162
|
+
console.log('Run `vibe-usage daemon status` to check.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function uninstall() {
|
|
166
|
+
const plat = detectPlatform();
|
|
167
|
+
if (!plat) {
|
|
168
|
+
console.log('No supported service platform detected.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const paths = getServicePaths(plat);
|
|
173
|
+
|
|
174
|
+
if (!existsSync(paths.file)) {
|
|
175
|
+
console.log('No daemon service is installed.');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (plat === 'systemd') {
|
|
180
|
+
run('systemctl', ['--user', 'stop', `${SERVICE_NAME}.service`]);
|
|
181
|
+
run('systemctl', ['--user', 'disable', `${SERVICE_NAME}.service`]);
|
|
182
|
+
unlinkSync(paths.file);
|
|
183
|
+
run('systemctl', ['--user', 'daemon-reload']);
|
|
184
|
+
console.log('Service stopped, disabled, and removed.');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (plat === 'launchd') {
|
|
188
|
+
run('launchctl', ['unload', paths.file]);
|
|
189
|
+
unlinkSync(paths.file);
|
|
190
|
+
console.log('Service unloaded and removed.');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function status() {
|
|
195
|
+
const plat = detectPlatform();
|
|
196
|
+
if (!plat) {
|
|
197
|
+
console.log('No supported service platform detected.');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const paths = getServicePaths(plat);
|
|
202
|
+
|
|
203
|
+
if (!existsSync(paths.file)) {
|
|
204
|
+
console.log('No daemon service is installed.');
|
|
205
|
+
console.log('Run `vibe-usage daemon install` to set up.');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (plat === 'systemd') {
|
|
210
|
+
const result = run('systemctl', ['--user', 'status', `${SERVICE_NAME}.service`]);
|
|
211
|
+
console.log(result.output);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (plat === 'launchd') {
|
|
215
|
+
const result = run('launchctl', ['list', LAUNCHD_LABEL]);
|
|
216
|
+
if (result.ok) {
|
|
217
|
+
console.log(`Service: ${LAUNCHD_LABEL}`);
|
|
218
|
+
console.log(result.output);
|
|
219
|
+
} else {
|
|
220
|
+
console.log('Service is installed but not currently running.');
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function stop() {
|
|
226
|
+
const plat = detectPlatform();
|
|
227
|
+
if (!plat) {
|
|
228
|
+
console.log('No supported service platform detected.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (plat === 'systemd') {
|
|
233
|
+
const result = run('systemctl', ['--user', 'stop', `${SERVICE_NAME}.service`]);
|
|
234
|
+
console.log(result.ok ? 'Service stopped.' : `Failed: ${result.output}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (plat === 'launchd') {
|
|
238
|
+
const result = run('launchctl', ['stop', LAUNCHD_LABEL]);
|
|
239
|
+
console.log(result.ok ? 'Service stopped.' : `Failed: ${result.output}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function restart() {
|
|
244
|
+
const plat = detectPlatform();
|
|
245
|
+
if (!plat) {
|
|
246
|
+
console.log('No supported service platform detected.');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (plat === 'systemd') {
|
|
251
|
+
const result = run('systemctl', ['--user', 'restart', `${SERVICE_NAME}.service`]);
|
|
252
|
+
console.log(result.ok ? 'Service restarted.' : `Failed: ${result.output}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (plat === 'launchd') {
|
|
256
|
+
run('launchctl', ['stop', LAUNCHD_LABEL]);
|
|
257
|
+
const result = run('launchctl', ['start', LAUNCHD_LABEL]);
|
|
258
|
+
console.log(result.ok ? 'Service restarted.' : `Failed: ${result.output}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const SUBCOMMANDS = { install, uninstall, status, stop, restart };
|
|
263
|
+
|
|
264
|
+
export async function manageDaemon(subcommand) {
|
|
265
|
+
const fn = SUBCOMMANDS[subcommand];
|
|
266
|
+
if (!fn) {
|
|
267
|
+
console.error(`Unknown daemon subcommand: ${subcommand}`);
|
|
268
|
+
console.error('Usage: vibe-usage daemon <install|uninstall|status|stop|restart>');
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
fn();
|
|
272
|
+
}
|
package/src/index.js
CHANGED
|
@@ -109,8 +109,14 @@ export async function run(args) {
|
|
|
109
109
|
}
|
|
110
110
|
case 'daemon':
|
|
111
111
|
case '--daemon': {
|
|
112
|
-
const
|
|
113
|
-
|
|
112
|
+
const sub = args[1];
|
|
113
|
+
if (sub && ['install', 'uninstall', 'status', 'stop', 'restart'].includes(sub)) {
|
|
114
|
+
const { manageDaemon } = await import('./daemon-service.js');
|
|
115
|
+
await manageDaemon(sub);
|
|
116
|
+
} else {
|
|
117
|
+
const { runDaemon } = await import('./daemon.js');
|
|
118
|
+
await runDaemon();
|
|
119
|
+
}
|
|
114
120
|
break;
|
|
115
121
|
}
|
|
116
122
|
case 'skill': {
|
|
@@ -136,7 +142,12 @@ export async function run(args) {
|
|
|
136
142
|
npx @vibe-cafe/vibe-usage Init (first run) or sync
|
|
137
143
|
npx @vibe-cafe/vibe-usage init Set up API key
|
|
138
144
|
npx @vibe-cafe/vibe-usage sync Manually sync usage data
|
|
139
|
-
npx @vibe-cafe/vibe-usage daemon Continuous sync (every 5m)
|
|
145
|
+
npx @vibe-cafe/vibe-usage daemon Continuous sync (every 5m, foreground)
|
|
146
|
+
npx @vibe-cafe/vibe-usage daemon install Install background service (systemd/launchd)
|
|
147
|
+
npx @vibe-cafe/vibe-usage daemon uninstall Remove background service
|
|
148
|
+
npx @vibe-cafe/vibe-usage daemon status Show background service status
|
|
149
|
+
npx @vibe-cafe/vibe-usage daemon stop Stop background service
|
|
150
|
+
npx @vibe-cafe/vibe-usage daemon restart Restart background service
|
|
140
151
|
npx @vibe-cafe/vibe-usage reset Delete all data and re-upload
|
|
141
152
|
npx @vibe-cafe/vibe-usage reset --local Delete data for this host only and re-upload
|
|
142
153
|
npx @vibe-cafe/vibe-usage skill Install skill for AI coding tools
|
package/src/init.js
CHANGED
|
@@ -75,4 +75,8 @@ export async function runInit() {
|
|
|
75
75
|
await runSync();
|
|
76
76
|
|
|
77
77
|
console.log(`\nSetup complete! View your dashboard at: ${apiUrl}/usage`);
|
|
78
|
+
|
|
79
|
+
if (process.platform === 'linux' || process.platform === 'darwin') {
|
|
80
|
+
console.log('\nTip: Run `npx @vibe-cafe/vibe-usage daemon install` to sync automatically in the background.');
|
|
81
|
+
}
|
|
78
82
|
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { readdirSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { aggregateToBuckets, extractSessions } from './index.js';
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Antigravity parser (file-based).
|
|
11
|
+
* Scans .pb files in ~/.gemini/antigravity/conversations/ to discover cascade IDs.
|
|
12
|
+
* Calls GetCascadeTrajectory via a running language server to extract token usage
|
|
13
|
+
* (from generatorMetadata) and session events (from trajectory steps).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const SOURCE = 'antigravity';
|
|
17
|
+
const CONVERSATIONS_DIR = join(homedir(), '.gemini', 'antigravity', 'conversations');
|
|
18
|
+
|
|
19
|
+
// User sources → role 'user'; Model source → role 'assistant'; System sources → skip
|
|
20
|
+
const USER_SOURCES = new Set([
|
|
21
|
+
'CORTEX_STEP_SOURCE_USER_EXPLICIT',
|
|
22
|
+
'CORTEX_STEP_SOURCE_USER_IMPLICIT',
|
|
23
|
+
]);
|
|
24
|
+
const ASSISTANT_SOURCES = new Set([
|
|
25
|
+
'CORTEX_STEP_SOURCE_MODEL',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// ── Process discovery (single instance) ──────────────────────────────
|
|
29
|
+
|
|
30
|
+
const IS_WIN = process.platform === 'win32';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find ONE running language server process with a CSRF token.
|
|
34
|
+
* Returns { pid, csrfToken } or null.
|
|
35
|
+
*/
|
|
36
|
+
function findLanguageServer() {
|
|
37
|
+
try {
|
|
38
|
+
return IS_WIN ? findLanguageServerWin() : findLanguageServerUnix();
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function findLanguageServerUnix() {
|
|
45
|
+
const out = execSync("ps aux | grep 'antigravity/bin/language_server_'", { encoding: 'utf-8', timeout: 5000 });
|
|
46
|
+
for (const line of out.split('\n')) {
|
|
47
|
+
if (!line.trim()) continue;
|
|
48
|
+
if (line.includes('grep')) continue;
|
|
49
|
+
const parts = line.trim().split(/\s+/);
|
|
50
|
+
if (parts.length < 2) continue;
|
|
51
|
+
const pid = parts[1];
|
|
52
|
+
const csrfMatch = line.match(/--csrf_token\s+([0-9a-f-]+)/);
|
|
53
|
+
const csrfToken = csrfMatch ? csrfMatch[1] : '';
|
|
54
|
+
if (csrfToken) return { pid, csrfToken };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function findLanguageServerWin() {
|
|
60
|
+
const out = execSync(
|
|
61
|
+
'wmic process where "CommandLine like \'%antigravity%language_server%\'" get ProcessId,CommandLine /format:list',
|
|
62
|
+
{ encoding: 'utf-8', timeout: 5000, shell: 'cmd.exe' },
|
|
63
|
+
);
|
|
64
|
+
// wmic /format:list outputs lines like "CommandLine=..." and "ProcessId=..."
|
|
65
|
+
let cmdLine = '';
|
|
66
|
+
let pid = '';
|
|
67
|
+
for (const line of out.split('\n')) {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
if (trimmed.startsWith('CommandLine=')) {
|
|
70
|
+
const val = trimmed.slice('CommandLine='.length);
|
|
71
|
+
if (/WMIC\.exe/i.test(val)) continue; // skip wmic's own process
|
|
72
|
+
cmdLine = val;
|
|
73
|
+
}
|
|
74
|
+
if (trimmed.startsWith('ProcessId=')) pid = trimmed.slice('ProcessId='.length);
|
|
75
|
+
}
|
|
76
|
+
if (!pid || !cmdLine) return null;
|
|
77
|
+
const csrfMatch = cmdLine.match(/--csrf_token\s+([0-9a-f-]+)/);
|
|
78
|
+
const csrfToken = csrfMatch ? csrfMatch[1] : '';
|
|
79
|
+
if (!csrfToken) return null;
|
|
80
|
+
return { pid, csrfToken };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function findListeningPorts(pid) {
|
|
84
|
+
try {
|
|
85
|
+
return IS_WIN ? findListeningPortsWin(pid) : findListeningPortsUnix(pid);
|
|
86
|
+
} catch {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function findListeningPortsUnix(pid) {
|
|
92
|
+
const out = execSync(`lsof -iTCP -sTCP:LISTEN -nP -a -p ${pid}`, {
|
|
93
|
+
encoding: 'utf-8',
|
|
94
|
+
timeout: 5000,
|
|
95
|
+
});
|
|
96
|
+
const ports = [];
|
|
97
|
+
for (const line of out.split('\n')) {
|
|
98
|
+
const match = line.match(/:(\d+)\s+\(LISTEN\)/);
|
|
99
|
+
if (match) ports.push(parseInt(match[1], 10));
|
|
100
|
+
}
|
|
101
|
+
return ports;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function findListeningPortsWin(pid) {
|
|
105
|
+
// netstat output: TCP 127.0.0.1:49327 0.0.0.0:0 LISTENING 12345
|
|
106
|
+
const out = execSync('netstat -ano', { encoding: 'utf-8', timeout: 5000 });
|
|
107
|
+
const ports = [];
|
|
108
|
+
for (const line of out.split('\n')) {
|
|
109
|
+
if (!line.includes('LISTENING')) continue;
|
|
110
|
+
const parts = line.trim().split(/\s+/);
|
|
111
|
+
// parts: [TCP, local_addr:port, foreign_addr, LISTENING, pid]
|
|
112
|
+
const linePid = parts[parts.length - 1];
|
|
113
|
+
if (linePid !== String(pid)) continue;
|
|
114
|
+
const addrMatch = parts[1]?.match(/:(\d+)$/);
|
|
115
|
+
if (addrMatch) ports.push(parseInt(addrMatch[1], 10));
|
|
116
|
+
}
|
|
117
|
+
return ports;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function rpcPost(baseUrl, path, body, csrfToken, timeoutMs = 10000) {
|
|
121
|
+
const url = new URL(path, baseUrl);
|
|
122
|
+
const headers = {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
'Connect-Protocol-Version': '1',
|
|
125
|
+
};
|
|
126
|
+
if (csrfToken) headers['X-Codeium-Csrf-Token'] = csrfToken;
|
|
127
|
+
|
|
128
|
+
const res = await fetch(url, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers,
|
|
131
|
+
body: JSON.stringify(body),
|
|
132
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} from ${path}`);
|
|
135
|
+
return res.json();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function probeHttpPort(ports, csrfToken) {
|
|
139
|
+
for (const port of ports) {
|
|
140
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
141
|
+
try {
|
|
142
|
+
await rpcPost(
|
|
143
|
+
baseUrl,
|
|
144
|
+
'/exa.language_server_pb.LanguageServerService/GetWorkspaceInfos',
|
|
145
|
+
{},
|
|
146
|
+
csrfToken,
|
|
147
|
+
3000,
|
|
148
|
+
);
|
|
149
|
+
return baseUrl;
|
|
150
|
+
} catch {
|
|
151
|
+
// Not the right port, try next
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Normalize model names to canonical forms.
|
|
161
|
+
*/
|
|
162
|
+
const MODEL_NORMALIZE_MAP = {
|
|
163
|
+
'claude-opus-4-6-thinking': 'claude-opus-4-6',
|
|
164
|
+
'claude-sonnet-4-6-thinking': 'claude-sonnet-4-6',
|
|
165
|
+
'gemini-3-flash-c': 'gemini-3-flash',
|
|
166
|
+
"gemini-3.1-pro-high": "gemini-3.1-pro",
|
|
167
|
+
"gemini-3.1-pro-low": "gemini-3.1-pro",
|
|
168
|
+
"gemini-3-pro-high": "gemini-3-pro",
|
|
169
|
+
"gemini-3-pro-low": "gemini-3-pro",
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Map internal placeholder model IDs to canonical names.
|
|
174
|
+
* Used when responseModel is empty and only chatModel.model is available.
|
|
175
|
+
*/
|
|
176
|
+
const PLACEHOLDER_MODEL_MAP = {
|
|
177
|
+
'MODEL_PLACEHOLDER_M37': 'gemini-3.1-pro',
|
|
178
|
+
'MODEL_PLACEHOLDER_M36': 'gemini-3.1-pro',
|
|
179
|
+
'MODEL_PLACEHOLDER_M47': 'gemini-3-flash',
|
|
180
|
+
'MODEL_PLACEHOLDER_M35': 'claude-sonnet-4-6',
|
|
181
|
+
'MODEL_PLACEHOLDER_M26': 'claude-opus-4-6',
|
|
182
|
+
'MODEL_OPENAI_GPT_OSS_120B_MEDIUM': 'gpt-oss-120b',
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
function normalizeModel(raw) {
|
|
186
|
+
return MODEL_NORMALIZE_MAP[raw] || raw;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Resolve model name: prefer responseModel, fall back to placeholder map.
|
|
191
|
+
*/
|
|
192
|
+
function resolveModel(chatModel) {
|
|
193
|
+
if (chatModel.responseModel) return normalizeModel(chatModel.responseModel);
|
|
194
|
+
const placeholder = chatModel.model || '';
|
|
195
|
+
if (PLACEHOLDER_MODEL_MAP[placeholder]) return PLACEHOLDER_MODEL_MAP[placeholder];
|
|
196
|
+
return 'unknown';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function toSafeNumber(value) {
|
|
200
|
+
if (value == null) return 0;
|
|
201
|
+
const n = Number(value);
|
|
202
|
+
return Number.isFinite(n) ? n : 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Extract project name from a workspace URI (e.g. "file:///Users/x/myproject" → "myproject").
|
|
207
|
+
*/
|
|
208
|
+
function projectFromUri(uri) {
|
|
209
|
+
if (!uri) return null;
|
|
210
|
+
const parts = uri.replace(/\/$/, '').split('/');
|
|
211
|
+
return parts[parts.length - 1] || null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* List cascade IDs from .pb files in the conversations directory.
|
|
216
|
+
*/
|
|
217
|
+
function listCascades() {
|
|
218
|
+
try {
|
|
219
|
+
const files = readdirSync(CONVERSATIONS_DIR);
|
|
220
|
+
const results = [];
|
|
221
|
+
for (const f of files) {
|
|
222
|
+
if (!f.endsWith('.pb')) continue;
|
|
223
|
+
results.push(f.slice(0, -3)); // strip .pb → cascadeId
|
|
224
|
+
}
|
|
225
|
+
return results;
|
|
226
|
+
} catch {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Main parse ───────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
export async function parse() {
|
|
234
|
+
// Step 1: List cascade .pb files
|
|
235
|
+
const cascadeIds = listCascades();
|
|
236
|
+
if (cascadeIds.length === 0) return { buckets: [], sessions: [] };
|
|
237
|
+
|
|
238
|
+
// Step 2: Find a running language server to make RPC calls
|
|
239
|
+
const server = findLanguageServer();
|
|
240
|
+
if (!server) return { buckets: [], sessions: [] };
|
|
241
|
+
|
|
242
|
+
const ports = findListeningPorts(server.pid);
|
|
243
|
+
if (ports.length === 0) return { buckets: [], sessions: [] };
|
|
244
|
+
|
|
245
|
+
const baseUrl = await probeHttpPort(ports, server.csrfToken);
|
|
246
|
+
if (!baseUrl) return { buckets: [], sessions: [] };
|
|
247
|
+
|
|
248
|
+
const rpc = (method, body) =>
|
|
249
|
+
rpcPost(
|
|
250
|
+
baseUrl,
|
|
251
|
+
`/exa.language_server_pb.LanguageServerService/${method}`,
|
|
252
|
+
body,
|
|
253
|
+
server.csrfToken,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Step 3: Fetch trajectory for each changed cascade
|
|
257
|
+
const entries = [];
|
|
258
|
+
const sessionEvents = [];
|
|
259
|
+
const seenResponseIds = new Set();
|
|
260
|
+
|
|
261
|
+
for (const cascadeId of cascadeIds) {
|
|
262
|
+
let resp;
|
|
263
|
+
try {
|
|
264
|
+
resp = await rpc('GetCascadeTrajectory', { cascadeId });
|
|
265
|
+
} catch {
|
|
266
|
+
continue; // skip this cascade if RPC fails
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const trajectory = resp?.trajectory;
|
|
270
|
+
if (!trajectory) continue;
|
|
271
|
+
|
|
272
|
+
const steps = trajectory.steps || [];
|
|
273
|
+
const metadataList = trajectory.generatorMetadata || [];
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
// Extract project from trajectory metadata workspaces
|
|
277
|
+
let project = 'unknown';
|
|
278
|
+
const workspaces = trajectory.metadata?.workspaces || [];
|
|
279
|
+
if (workspaces.length > 0) {
|
|
280
|
+
project = workspaces[0].repository?.computedName || projectFromUri(workspaces[0].workspaceFolderAbsoluteUri) || 'unknown';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── Token entries from generatorMetadata ──
|
|
284
|
+
for (const meta of metadataList) {
|
|
285
|
+
const chatModel = meta?.chatModel;
|
|
286
|
+
if (!chatModel) continue;
|
|
287
|
+
|
|
288
|
+
const responseModel = resolveModel(chatModel);
|
|
289
|
+
const createdAt = chatModel?.chatStartMetadata?.createdAt;
|
|
290
|
+
const ts = createdAt ? new Date(createdAt) : null;
|
|
291
|
+
if (!ts || isNaN(ts.getTime())) continue;
|
|
292
|
+
|
|
293
|
+
const retryInfos = chatModel.retryInfos || [];
|
|
294
|
+
for (const retry of retryInfos) {
|
|
295
|
+
const usage = retry.usage;
|
|
296
|
+
if (!usage) continue;
|
|
297
|
+
|
|
298
|
+
const responseId = usage.responseId || '';
|
|
299
|
+
if (responseId && seenResponseIds.has(responseId)) continue;
|
|
300
|
+
if (responseId) seenResponseIds.add(responseId);
|
|
301
|
+
|
|
302
|
+
entries.push({
|
|
303
|
+
source: SOURCE,
|
|
304
|
+
model: responseModel,
|
|
305
|
+
project,
|
|
306
|
+
timestamp: ts,
|
|
307
|
+
inputTokens: toSafeNumber(usage.inputTokens),
|
|
308
|
+
outputTokens: toSafeNumber(usage.outputTokens),
|
|
309
|
+
cachedInputTokens: toSafeNumber(usage.cacheReadTokens),
|
|
310
|
+
reasoningOutputTokens: toSafeNumber(usage.thinkingOutputTokens),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Session events from trajectory steps ──
|
|
316
|
+
for (const step of steps) {
|
|
317
|
+
const stepSource = step?.metadata?.source || '';
|
|
318
|
+
let role;
|
|
319
|
+
if (USER_SOURCES.has(stepSource)) {
|
|
320
|
+
role = 'user';
|
|
321
|
+
} else if (ASSISTANT_SOURCES.has(stepSource)) {
|
|
322
|
+
role = 'assistant';
|
|
323
|
+
} else {
|
|
324
|
+
continue; // skip SYSTEM / SYSTEM_SDK / UNSPECIFIED
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const createdAt = step?.metadata?.createdAt;
|
|
328
|
+
const ts = createdAt ? new Date(createdAt) : null;
|
|
329
|
+
if (!ts || isNaN(ts.getTime())) continue;
|
|
330
|
+
|
|
331
|
+
sessionEvents.push({
|
|
332
|
+
sessionId: cascadeId,
|
|
333
|
+
source: SOURCE,
|
|
334
|
+
project,
|
|
335
|
+
timestamp: ts,
|
|
336
|
+
role,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
buckets: aggregateToBuckets(entries),
|
|
344
|
+
sessions: extractSessions(sessionEvents),
|
|
345
|
+
};
|
|
346
|
+
}
|
package/src/parsers/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { parse as parseQwenCode } from './qwen-code.js';
|
|
|
9
9
|
import { parse as parseKimiCode } from './kimi-code.js';
|
|
10
10
|
import { parse as parseAmp } from './amp.js';
|
|
11
11
|
import { parse as parseDroid } from './droid.js';
|
|
12
|
+
import { parse as parseAntigravity } from './antigravity.js';
|
|
12
13
|
import { parse as parsePiCodingAgent } from './pi-coding-agent.js';
|
|
13
14
|
|
|
14
15
|
export const parsers = {
|
|
@@ -22,6 +23,7 @@ export const parsers = {
|
|
|
22
23
|
'kimi-code': parseKimiCode,
|
|
23
24
|
'amp': parseAmp,
|
|
24
25
|
'droid': parseDroid,
|
|
26
|
+
'antigravity': parseAntigravity,
|
|
25
27
|
'pi-coding-agent': parsePiCodingAgent,
|
|
26
28
|
};
|
|
27
29
|
|
package/src/tools.js
CHANGED