hacklab 0.22.3 → 26.921.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 +39 -415
- package/dist/index.js +317 -23
- package/package.json +22 -40
- package/LICENSE +0 -21
- package/dist/agent-handoff.d.ts +0 -51
- package/dist/agent-handoff.d.ts.map +0 -1
- package/dist/agent-handoff.js +0 -91
- package/dist/agent-handoff.js.map +0 -1
- package/dist/api-client.d.ts +0 -18
- package/dist/api-client.d.ts.map +0 -1
- package/dist/api-client.js +0 -40
- package/dist/api-client.js.map +0 -1
- package/dist/belt.d.ts +0 -25
- package/dist/belt.d.ts.map +0 -1
- package/dist/belt.js +0 -113
- package/dist/belt.js.map +0 -1
- package/dist/card.d.ts +0 -66
- package/dist/card.d.ts.map +0 -1
- package/dist/card.js +0 -151
- package/dist/card.js.map +0 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -238
- package/dist/cli.js.map +0 -1
- package/dist/commands/book.d.ts +0 -14
- package/dist/commands/book.d.ts.map +0 -1
- package/dist/commands/book.js +0 -90
- package/dist/commands/book.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -36
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js +0 -642
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -84
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/daemon.d.ts +0 -19
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/daemon.js +0 -72
- package/dist/commands/daemon.js.map +0 -1
- package/dist/commands/drop.d.ts +0 -12
- package/dist/commands/drop.d.ts.map +0 -1
- package/dist/commands/drop.js +0 -103
- package/dist/commands/drop.js.map +0 -1
- package/dist/commands/drops.d.ts +0 -9
- package/dist/commands/drops.d.ts.map +0 -1
- package/dist/commands/drops.js +0 -96
- package/dist/commands/drops.js.map +0 -1
- package/dist/commands/essay.d.ts +0 -40
- package/dist/commands/essay.d.ts.map +0 -1
- package/dist/commands/essay.js +0 -651
- package/dist/commands/essay.js.map +0 -1
- package/dist/commands/hackathon.d.ts +0 -2
- package/dist/commands/hackathon.d.ts.map +0 -1
- package/dist/commands/hackathon.js +0 -893
- package/dist/commands/hackathon.js.map +0 -1
- package/dist/commands/hacker.d.ts +0 -2
- package/dist/commands/hacker.d.ts.map +0 -1
- package/dist/commands/hacker.js +0 -87
- package/dist/commands/hacker.js.map +0 -1
- package/dist/commands/jobs.d.ts +0 -18
- package/dist/commands/jobs.d.ts.map +0 -1
- package/dist/commands/jobs.js +0 -138
- package/dist/commands/jobs.js.map +0 -1
- package/dist/commands/keys.d.ts +0 -2
- package/dist/commands/keys.d.ts.map +0 -1
- package/dist/commands/keys.js +0 -138
- package/dist/commands/keys.js.map +0 -1
- package/dist/commands/login.d.ts +0 -83
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js +0 -236
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts +0 -2
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js +0 -22
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/org-jobs.d.ts +0 -50
- package/dist/commands/org-jobs.d.ts.map +0 -1
- package/dist/commands/org-jobs.js +0 -383
- package/dist/commands/org-jobs.js.map +0 -1
- package/dist/commands/org.d.ts +0 -67
- package/dist/commands/org.d.ts.map +0 -1
- package/dist/commands/org.js +0 -1244
- package/dist/commands/org.js.map +0 -1
- package/dist/commands/ping.d.ts +0 -2
- package/dist/commands/ping.d.ts.map +0 -1
- package/dist/commands/ping.js +0 -50
- package/dist/commands/ping.js.map +0 -1
- package/dist/commands/profile.d.ts +0 -72
- package/dist/commands/profile.d.ts.map +0 -1
- package/dist/commands/profile.js +0 -672
- package/dist/commands/profile.js.map +0 -1
- package/dist/commands/project.d.ts +0 -2
- package/dist/commands/project.d.ts.map +0 -1
- package/dist/commands/project.js +0 -583
- package/dist/commands/project.js.map +0 -1
- package/dist/commands/referral.d.ts +0 -6
- package/dist/commands/referral.d.ts.map +0 -1
- package/dist/commands/referral.js +0 -93
- package/dist/commands/referral.js.map +0 -1
- package/dist/commands/rtfm.d.ts +0 -9
- package/dist/commands/rtfm.d.ts.map +0 -1
- package/dist/commands/rtfm.js +0 -114
- package/dist/commands/rtfm.js.map +0 -1
- package/dist/commands/scan.d.ts +0 -13
- package/dist/commands/scan.d.ts.map +0 -1
- package/dist/commands/scan.js +0 -286
- package/dist/commands/scan.js.map +0 -1
- package/dist/commands/scout.d.ts +0 -18
- package/dist/commands/scout.d.ts.map +0 -1
- package/dist/commands/scout.js +0 -232
- package/dist/commands/scout.js.map +0 -1
- package/dist/commands/setup-rail.d.ts +0 -58
- package/dist/commands/setup-rail.d.ts.map +0 -1
- package/dist/commands/setup-rail.js +0 -135
- package/dist/commands/setup-rail.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -442
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/sync.d.ts +0 -9
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js +0 -345
- package/dist/commands/sync.js.map +0 -1
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js +0 -92
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/whoami.d.ts +0 -2
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js +0 -41
- package/dist/commands/whoami.js.map +0 -1
- package/dist/config.d.ts +0 -78
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -93
- package/dist/config.js.map +0 -1
- package/dist/daily-sync.d.ts +0 -150
- package/dist/daily-sync.d.ts.map +0 -1
- package/dist/daily-sync.js +0 -665
- package/dist/daily-sync.js.map +0 -1
- package/dist/dossier.d.ts +0 -73
- package/dist/dossier.d.ts.map +0 -1
- package/dist/dossier.js +0 -141
- package/dist/dossier.js.map +0 -1
- package/dist/field-normalize.d.ts +0 -4
- package/dist/field-normalize.d.ts.map +0 -1
- package/dist/field-normalize.js +0 -8
- package/dist/field-normalize.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/job-fields.d.ts +0 -47
- package/dist/job-fields.d.ts.map +0 -1
- package/dist/job-fields.js +0 -148
- package/dist/job-fields.js.map +0 -1
- package/dist/machine.d.ts +0 -15
- package/dist/machine.d.ts.map +0 -1
- package/dist/machine.js +0 -64
- package/dist/machine.js.map +0 -1
- package/dist/org-fields.d.ts +0 -57
- package/dist/org-fields.d.ts.map +0 -1
- package/dist/org-fields.js +0 -167
- package/dist/org-fields.js.map +0 -1
- package/dist/posthog.d.ts +0 -38
- package/dist/posthog.d.ts.map +0 -1
- package/dist/posthog.js +0 -224
- package/dist/posthog.js.map +0 -1
- package/dist/project-fields.d.ts +0 -40
- package/dist/project-fields.d.ts.map +0 -1
- package/dist/project-fields.js +0 -79
- package/dist/project-fields.js.map +0 -1
- package/dist/prompt-consent.d.ts +0 -73
- package/dist/prompt-consent.d.ts.map +0 -1
- package/dist/prompt-consent.js +0 -191
- package/dist/prompt-consent.js.map +0 -1
- package/dist/prompt-stats.d.ts +0 -226
- package/dist/prompt-stats.d.ts.map +0 -1
- package/dist/prompt-stats.js +0 -561
- package/dist/prompt-stats.js.map +0 -1
- package/dist/referral.d.ts +0 -14
- package/dist/referral.d.ts.map +0 -1
- package/dist/referral.js +0 -28
- package/dist/referral.js.map +0 -1
- package/dist/registry.d.ts +0 -15
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -180
- package/dist/registry.js.map +0 -1
- package/dist/resolve-command.d.ts +0 -20
- package/dist/resolve-command.d.ts.map +0 -1
- package/dist/resolve-command.js +0 -21
- package/dist/resolve-command.js.map +0 -1
- package/dist/scanners/antigravity.d.ts +0 -41
- package/dist/scanners/antigravity.d.ts.map +0 -1
- package/dist/scanners/antigravity.js +0 -210
- package/dist/scanners/antigravity.js.map +0 -1
- package/dist/scanners/github-copilot-telemetry.d.ts +0 -23
- package/dist/scanners/github-copilot-telemetry.d.ts.map +0 -1
- package/dist/scanners/github-copilot-telemetry.js +0 -220
- package/dist/scanners/github-copilot-telemetry.js.map +0 -1
- package/dist/scanners/github-copilot.d.ts +0 -68
- package/dist/scanners/github-copilot.d.ts.map +0 -1
- package/dist/scanners/github-copilot.js +0 -262
- package/dist/scanners/github-copilot.js.map +0 -1
- package/dist/scanners/incremental.d.ts +0 -282
- package/dist/scanners/incremental.d.ts.map +0 -1
- package/dist/scanners/incremental.js +0 -788
- package/dist/scanners/incremental.js.map +0 -1
- package/dist/scanners/index.d.ts +0 -116
- package/dist/scanners/index.d.ts.map +0 -1
- package/dist/scanners/index.js +0 -726
- package/dist/scanners/index.js.map +0 -1
- package/dist/scanners/sqlite.d.ts +0 -18
- package/dist/scanners/sqlite.d.ts.map +0 -1
- package/dist/scanners/sqlite.js +0 -63
- package/dist/scanners/sqlite.js.map +0 -1
- package/dist/scanners/util.d.ts +0 -112
- package/dist/scanners/util.d.ts.map +0 -1
- package/dist/scanners/util.js +0 -167
- package/dist/scanners/util.js.map +0 -1
- package/dist/session.d.ts +0 -106
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -191
- package/dist/session.js.map +0 -1
- package/dist/share-card-colors.d.ts +0 -19
- package/dist/share-card-colors.d.ts.map +0 -1
- package/dist/share-card-colors.js +0 -36
- package/dist/share-card-colors.js.map +0 -1
- package/dist/share-card.d.ts +0 -31
- package/dist/share-card.d.ts.map +0 -1
- package/dist/share-card.js +0 -380
- package/dist/share-card.js.map +0 -1
- package/dist/share.d.ts +0 -17
- package/dist/share.d.ts.map +0 -1
- package/dist/share.js +0 -75
- package/dist/share.js.map +0 -1
- package/dist/sync.d.ts +0 -100
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -304
- package/dist/sync.js.map +0 -1
- package/dist/ui.d.ts +0 -33
- package/dist/ui.d.ts.map +0 -1
- package/dist/ui.js +0 -127
- package/dist/ui.js.map +0 -1
- package/dist/utils/npmGlobal.d.ts +0 -30
- package/dist/utils/npmGlobal.d.ts.map +0 -1
- package/dist/utils/npmGlobal.js +0 -69
- package/dist/utils/npmGlobal.js.map +0 -1
- package/dist/utils/openBrowser.d.ts +0 -14
- package/dist/utils/openBrowser.d.ts.map +0 -1
- package/dist/utils/openBrowser.js +0 -47
- package/dist/utils/openBrowser.js.map +0 -1
- package/dist/utils/updateCheck.d.ts +0 -16
- package/dist/utils/updateCheck.d.ts.map +0 -1
- package/dist/utils/updateCheck.js +0 -111
- package/dist/utils/updateCheck.js.map +0 -1
- package/dist/utils/waitForEnter.d.ts +0 -12
- package/dist/utils/waitForEnter.d.ts.map +0 -1
- package/dist/utils/waitForEnter.js +0 -43
- package/dist/utils/waitForEnter.js.map +0 -1
package/dist/scanners/index.js
DELETED
|
@@ -1,726 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { resolveCursorAuth } from '../config.js';
|
|
5
|
-
import { scanAntigravity } from './antigravity.js';
|
|
6
|
-
import { scanGitHubCopilot } from './github-copilot.js';
|
|
7
|
-
import { num, queryDb, str } from './sqlite.js';
|
|
8
|
-
import { emptyResult, findFiles, TokenCollector, toDateStr, } from './util.js';
|
|
9
|
-
export * from './util.js';
|
|
10
|
-
// One self-contained scanner per tool. Each returns a uniform ScanResult and
|
|
11
|
-
// keeps its own state (via TokenCollector), so they're pure and run in parallel.
|
|
12
|
-
// Where each harness keeps its logs. Resolved per call rather than at import
|
|
13
|
-
// time so a test (or a run with a different HOME) sees the right home dir.
|
|
14
|
-
export function claudeProjectsDir() {
|
|
15
|
-
return join(homedir(), '.claude', 'projects');
|
|
16
|
-
}
|
|
17
|
-
export function codexSessionsDir() {
|
|
18
|
-
return join(homedir(), '.codex', 'sessions');
|
|
19
|
-
}
|
|
20
|
-
export function openclawAgentsDir() {
|
|
21
|
-
return join(homedir(), '.openclaw', 'agents');
|
|
22
|
-
}
|
|
23
|
-
export function hermesDbPath() {
|
|
24
|
-
return join(homedir(), '.hermes', 'state.db');
|
|
25
|
-
}
|
|
26
|
-
export function opencodeDir() {
|
|
27
|
-
return join(homedir(), '.local', 'share', 'opencode');
|
|
28
|
-
}
|
|
29
|
-
export function opencodeDbPath() {
|
|
30
|
-
return join(opencodeDir(), 'opencode.db');
|
|
31
|
-
}
|
|
32
|
-
/** Grok Build home. `GROK_HOME` wins so tests (and unusual installs) can point elsewhere. */
|
|
33
|
-
export function grokHome() {
|
|
34
|
-
const override = process.env.GROK_HOME?.trim();
|
|
35
|
-
return override ? override : join(homedir(), '.grok');
|
|
36
|
-
}
|
|
37
|
-
export function grokLogsDir() {
|
|
38
|
-
return join(grokHome(), 'logs');
|
|
39
|
-
}
|
|
40
|
-
export function parseClaudeCodeLine(line) {
|
|
41
|
-
if (!line.trim())
|
|
42
|
-
return null;
|
|
43
|
-
try {
|
|
44
|
-
const parsed = JSON.parse(line);
|
|
45
|
-
const usage = parsed.message?.usage ?? parsed.usage ?? null;
|
|
46
|
-
if (!usage)
|
|
47
|
-
return null;
|
|
48
|
-
const tokens = (usage.input_tokens ?? 0) +
|
|
49
|
-
(usage.output_tokens ?? 0) +
|
|
50
|
-
(usage.cache_creation_input_tokens ?? 0) +
|
|
51
|
-
(usage.cache_read_input_tokens ?? 0);
|
|
52
|
-
if (tokens <= 0)
|
|
53
|
-
return null;
|
|
54
|
-
let date = null;
|
|
55
|
-
if (parsed.timestamp) {
|
|
56
|
-
const d = new Date(typeof parsed.timestamp === 'string' && /^\d+$/.test(parsed.timestamp)
|
|
57
|
-
? Number(parsed.timestamp)
|
|
58
|
-
: parsed.timestamp);
|
|
59
|
-
date = toDateStr(d);
|
|
60
|
-
}
|
|
61
|
-
return { tokens, model: parsed.message?.model ?? '', date };
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// malformed line (or an unparseable timestamp) — skip it
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
/** Every Claude Code transcript on this machine. */
|
|
69
|
-
export async function claudeCodeFiles() {
|
|
70
|
-
return findFiles(claudeProjectsDir(), '.jsonl');
|
|
71
|
-
}
|
|
72
|
-
export async function scanClaudeCode() {
|
|
73
|
-
const collector = new TokenCollector('claude_code');
|
|
74
|
-
for (const filePath of await claudeCodeFiles()) {
|
|
75
|
-
try {
|
|
76
|
-
const content = await readFile(filePath, 'utf8');
|
|
77
|
-
let fallbackDate = null;
|
|
78
|
-
for (const line of content.split('\n')) {
|
|
79
|
-
const usage = parseClaudeCodeLine(line);
|
|
80
|
-
if (!usage)
|
|
81
|
-
continue;
|
|
82
|
-
let date = usage.date;
|
|
83
|
-
if (!date) {
|
|
84
|
-
fallbackDate ??= toDateStr((await stat(filePath)).mtime);
|
|
85
|
-
date = fallbackDate;
|
|
86
|
-
}
|
|
87
|
-
collector.addDaily(date, usage.model, usage.tokens, 1);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
// skip unreadable files
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return collector.result();
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Extract the session date from a Codex session path laid out as
|
|
98
|
-
* `YYYY/MM/DD/file.jsonl`, or null if the path doesn't carry one. Splits on
|
|
99
|
-
* either separator so it works on Windows (backslashes) too.
|
|
100
|
-
*/
|
|
101
|
-
export function codexDateFromRelPath(relPath) {
|
|
102
|
-
const parts = relPath.split(/[\\/]/);
|
|
103
|
-
if (parts.length >= 3) {
|
|
104
|
-
return `${parts[0]}-${parts[1]?.padStart(2, '0')}-${parts[2]?.padStart(2, '0')}`;
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
/** Session date for a Codex log path (.codex/sessions/YYYY/MM/DD/file.jsonl). */
|
|
109
|
-
export function codexDateForFile(filePath) {
|
|
110
|
-
return codexDateFromRelPath(filePath.slice(codexSessionsDir().length + 1));
|
|
111
|
-
}
|
|
112
|
-
/** Every Codex session log on this machine. */
|
|
113
|
-
export async function codexFiles() {
|
|
114
|
-
return findFiles(codexSessionsDir(), '.jsonl');
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* The running token total (and last model) a chunk of a Codex session log
|
|
118
|
-
* reports. Codex writes cumulative totals rather than per-message deltas, so a
|
|
119
|
-
* file's contribution is the max it ever reports — which also means an appended
|
|
120
|
-
* chunk can only ever raise that max, and the incremental tick can take the
|
|
121
|
-
* difference without re-reading the whole file.
|
|
122
|
-
*/
|
|
123
|
-
export function codexFileTotals(content) {
|
|
124
|
-
let maxTotal = 0;
|
|
125
|
-
let model = '';
|
|
126
|
-
for (const line of content.split('\n')) {
|
|
127
|
-
if (!line.trim())
|
|
128
|
-
continue;
|
|
129
|
-
try {
|
|
130
|
-
const parsed = JSON.parse(line);
|
|
131
|
-
if (parsed.payload?.model)
|
|
132
|
-
model = parsed.payload.model;
|
|
133
|
-
const usage = parsed.payload?.info?.total_token_usage;
|
|
134
|
-
if (usage) {
|
|
135
|
-
const t = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
136
|
-
if (t > maxTotal)
|
|
137
|
-
maxTotal = t;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
// skip malformed lines
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
return { maxTotal, model };
|
|
145
|
-
}
|
|
146
|
-
export async function scanCodex() {
|
|
147
|
-
const collector = new TokenCollector('codex');
|
|
148
|
-
for (const filePath of await codexFiles()) {
|
|
149
|
-
try {
|
|
150
|
-
const { maxTotal, model } = codexFileTotals(await readFile(filePath, 'utf8'));
|
|
151
|
-
if (maxTotal <= 0)
|
|
152
|
-
continue;
|
|
153
|
-
const date = codexDateForFile(filePath) ?? toDateStr((await stat(filePath)).mtime);
|
|
154
|
-
collector.addDaily(date, model, maxTotal, 1);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
// skip unreadable files
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
return collector.result();
|
|
161
|
-
}
|
|
162
|
-
export async function scanHermes() {
|
|
163
|
-
const dbPath = hermesDbPath();
|
|
164
|
-
try {
|
|
165
|
-
await stat(dbPath);
|
|
166
|
-
}
|
|
167
|
-
catch {
|
|
168
|
-
return emptyResult('hermes');
|
|
169
|
-
}
|
|
170
|
-
const collector = new TokenCollector('hermes');
|
|
171
|
-
try {
|
|
172
|
-
const rows = await queryDb(dbPath, `select s.id, coalesce(s.model,'') as model, s.started_at, s.input_tokens, s.output_tokens, s.cache_read_tokens, s.cache_write_tokens, s.reasoning_tokens, coalesce((select count(*) from messages m where m.session_id = s.id and m.role = 'assistant'), 0) as msg_count from sessions s`);
|
|
173
|
-
if (rows.length === 0)
|
|
174
|
-
return emptyResult('hermes');
|
|
175
|
-
for (const row of rows) {
|
|
176
|
-
const [, model, startedAt, inputTokens, outputTokens, cacheRead, cacheWrite, reasoning, msgCount,] = row;
|
|
177
|
-
const tokens = num(inputTokens) +
|
|
178
|
-
num(outputTokens) +
|
|
179
|
-
num(cacheRead) +
|
|
180
|
-
num(cacheWrite) +
|
|
181
|
-
num(reasoning);
|
|
182
|
-
if (tokens <= 0)
|
|
183
|
-
continue;
|
|
184
|
-
const tsSec = num(startedAt);
|
|
185
|
-
if (!Number.isFinite(tsSec) || tsSec <= 0)
|
|
186
|
-
continue;
|
|
187
|
-
const d = new Date(Math.round(tsSec * 1000));
|
|
188
|
-
const date = toDateStr(d);
|
|
189
|
-
const m = str(model).trim();
|
|
190
|
-
const messages = num(msgCount) || 1;
|
|
191
|
-
collector.addDaily(date, m, tokens, messages);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch {
|
|
195
|
-
return emptyResult('hermes');
|
|
196
|
-
}
|
|
197
|
-
return collector.result();
|
|
198
|
-
}
|
|
199
|
-
export function parseOpenclawLine(line) {
|
|
200
|
-
if (!line.trim())
|
|
201
|
-
return null;
|
|
202
|
-
try {
|
|
203
|
-
const parsed = JSON.parse(line);
|
|
204
|
-
const usage = parsed.usage ??
|
|
205
|
-
parsed.tokenUsage ??
|
|
206
|
-
parsed.message?.usage ??
|
|
207
|
-
parsed.response?.usage ??
|
|
208
|
-
null;
|
|
209
|
-
if (!usage || typeof usage !== 'object')
|
|
210
|
-
return null;
|
|
211
|
-
const tokens = (usage.total ??
|
|
212
|
-
(usage.input ?? usage.inputTokens ?? 0) +
|
|
213
|
-
(usage.output ?? usage.outputTokens ?? 0) +
|
|
214
|
-
(usage.cacheRead ?? usage.cacheReadTokens ?? 0) +
|
|
215
|
-
(usage.cacheWrite ?? usage.cacheWriteTokens ?? 0));
|
|
216
|
-
if (!tokens || tokens <= 0)
|
|
217
|
-
return null;
|
|
218
|
-
let date = null;
|
|
219
|
-
const ts = parsed.timestamp ?? parsed.t ?? parsed.time ?? null;
|
|
220
|
-
if (ts) {
|
|
221
|
-
const tsMs = typeof ts === 'string' && /^\d+$/.test(ts) ? Number(ts) : ts;
|
|
222
|
-
const d = new Date(tsMs);
|
|
223
|
-
if (!Number.isNaN(d.getTime()))
|
|
224
|
-
date = toDateStr(d);
|
|
225
|
-
}
|
|
226
|
-
const model = parsed.model ?? parsed.response?.model ?? '';
|
|
227
|
-
return { tokens, model, date };
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
230
|
-
// skip malformed lines
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/** Every OpenClaw session log, across all of its agents. */
|
|
235
|
-
export async function openclawFiles() {
|
|
236
|
-
const agentsDir = openclawAgentsDir();
|
|
237
|
-
const sessionFiles = [];
|
|
238
|
-
try {
|
|
239
|
-
const agentIds = await readdir(agentsDir, { withFileTypes: true });
|
|
240
|
-
for (const entry of agentIds) {
|
|
241
|
-
if (!entry.isDirectory())
|
|
242
|
-
continue;
|
|
243
|
-
const discovered = await findFiles(join(agentsDir, entry.name, 'sessions'), '.jsonl');
|
|
244
|
-
sessionFiles.push(...discovered);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
// no OpenClaw on this machine
|
|
249
|
-
}
|
|
250
|
-
return sessionFiles;
|
|
251
|
-
}
|
|
252
|
-
export async function scanOpenclaw() {
|
|
253
|
-
const collector = new TokenCollector('openclaw');
|
|
254
|
-
for (const filePath of await openclawFiles()) {
|
|
255
|
-
try {
|
|
256
|
-
const content = await readFile(filePath, 'utf8');
|
|
257
|
-
let fileFallbackDate = null;
|
|
258
|
-
for (const line of content.split('\n')) {
|
|
259
|
-
const usage = parseOpenclawLine(line);
|
|
260
|
-
if (!usage)
|
|
261
|
-
continue;
|
|
262
|
-
let date = usage.date;
|
|
263
|
-
if (!date) {
|
|
264
|
-
fileFallbackDate ??= toDateStr((await stat(filePath)).mtime);
|
|
265
|
-
date = fileFallbackDate;
|
|
266
|
-
}
|
|
267
|
-
collector.addDaily(date, usage.model, usage.tokens, 1);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
catch {
|
|
271
|
-
// skip unreadable files
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
return collector.result();
|
|
275
|
-
}
|
|
276
|
-
export async function scanOpenCode() {
|
|
277
|
-
const baseDir = opencodeDir();
|
|
278
|
-
try {
|
|
279
|
-
await stat(baseDir);
|
|
280
|
-
}
|
|
281
|
-
catch {
|
|
282
|
-
return emptyResult('opencode');
|
|
283
|
-
}
|
|
284
|
-
const collector = new TokenCollector('opencode');
|
|
285
|
-
// Preferred: SQLite. Schema: message.data JSON with
|
|
286
|
-
// { role, modelID, time: { created: ms }, tokens: { input, output, reasoning, cache: { read, write } } }
|
|
287
|
-
const dbPath = opencodeDbPath();
|
|
288
|
-
try {
|
|
289
|
-
await stat(dbPath);
|
|
290
|
-
const rows = await queryDb(dbPath, `SELECT json_extract(data,'$.time.created'), coalesce(json_extract(data,'$.modelID'),''), coalesce(json_extract(data,'$.tokens.input'),0), coalesce(json_extract(data,'$.tokens.output'),0), coalesce(json_extract(data,'$.tokens.cache.read'),0), coalesce(json_extract(data,'$.tokens.cache.write'),0), coalesce(json_extract(data,'$.tokens.reasoning'),0) FROM message WHERE json_extract(data,'$.role')='assistant'`);
|
|
291
|
-
if (rows.length > 0) {
|
|
292
|
-
for (const row of rows) {
|
|
293
|
-
const [time, model, inp, out, cr, cw, reason] = row;
|
|
294
|
-
const tokens = num(inp) + num(out) + num(cr) + num(cw) + num(reason);
|
|
295
|
-
if (tokens <= 0)
|
|
296
|
-
continue;
|
|
297
|
-
const tsMs = num(time);
|
|
298
|
-
if (!Number.isFinite(tsMs) || tsMs <= 0)
|
|
299
|
-
continue;
|
|
300
|
-
const d = new Date(tsMs);
|
|
301
|
-
const m = str(model).trim();
|
|
302
|
-
collector.addDaily(toDateStr(d), m, tokens, 1);
|
|
303
|
-
}
|
|
304
|
-
const dbResult = collector.result();
|
|
305
|
-
if (dbResult.daily.length > 0)
|
|
306
|
-
return dbResult;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
catch {
|
|
310
|
-
// DB unavailable; fall through to JSON files
|
|
311
|
-
}
|
|
312
|
-
// Fallback: storage/message/{sessionID}/msg_*.json
|
|
313
|
-
const messageDir = join(baseDir, 'storage', 'message');
|
|
314
|
-
try {
|
|
315
|
-
await stat(messageDir);
|
|
316
|
-
}
|
|
317
|
-
catch {
|
|
318
|
-
return collector.result();
|
|
319
|
-
}
|
|
320
|
-
try {
|
|
321
|
-
const sessionDirs = await readdir(messageDir, { withFileTypes: true });
|
|
322
|
-
for (const sessionEntry of sessionDirs) {
|
|
323
|
-
if (!sessionEntry.isDirectory())
|
|
324
|
-
continue;
|
|
325
|
-
const msgFiles = await findFiles(join(messageDir, sessionEntry.name), '.json');
|
|
326
|
-
for (const filePath of msgFiles) {
|
|
327
|
-
try {
|
|
328
|
-
const parsed = JSON.parse(await readFile(filePath, 'utf8'));
|
|
329
|
-
if (parsed.role !== 'assistant')
|
|
330
|
-
continue;
|
|
331
|
-
const t = parsed.tokens;
|
|
332
|
-
if (!t || typeof t !== 'object')
|
|
333
|
-
continue;
|
|
334
|
-
const tokens = (t.input ?? 0) +
|
|
335
|
-
(t.output ?? 0) +
|
|
336
|
-
(t.cache?.read ?? 0) +
|
|
337
|
-
(t.cache?.write ?? 0) +
|
|
338
|
-
(t.reasoning ?? 0);
|
|
339
|
-
if (tokens <= 0)
|
|
340
|
-
continue;
|
|
341
|
-
const tsMs = parsed.time?.created ?? 0;
|
|
342
|
-
const date = tsMs > 0
|
|
343
|
-
? toDateStr(new Date(tsMs))
|
|
344
|
-
: toDateStr((await stat(filePath)).mtime);
|
|
345
|
-
const model = String(parsed.modelID ?? '');
|
|
346
|
-
collector.addDaily(date, model, tokens, 1);
|
|
347
|
-
}
|
|
348
|
-
catch {
|
|
349
|
-
// skip malformed files
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
catch {
|
|
355
|
-
// skip
|
|
356
|
-
}
|
|
357
|
-
return collector.result();
|
|
358
|
-
}
|
|
359
|
-
/**
|
|
360
|
-
* Grok Build writes one `shell.turn.inference_done` line per model round to
|
|
361
|
-
* `~/.grok/logs/unified.jsonl`. `prompt_tokens` is the full prompt (cache hits
|
|
362
|
-
* included); `cached_prompt_tokens` and `reasoning_tokens` are subsets, so the
|
|
363
|
-
* billed total is prompt + completion — same shape as counting Claude's
|
|
364
|
-
* input+output+cache as one number.
|
|
365
|
-
*/
|
|
366
|
-
export function parseGrokLine(line) {
|
|
367
|
-
if (!line.includes('shell.turn.inference_done'))
|
|
368
|
-
return null;
|
|
369
|
-
try {
|
|
370
|
-
const parsed = JSON.parse(line);
|
|
371
|
-
if (parsed.msg !== 'shell.turn.inference_done')
|
|
372
|
-
return null;
|
|
373
|
-
const ctx = parsed.ctx ?? {};
|
|
374
|
-
const tokens = Number(ctx.prompt_tokens ?? 0) + Number(ctx.completion_tokens ?? 0);
|
|
375
|
-
if (!Number.isFinite(tokens) || tokens <= 0)
|
|
376
|
-
return null;
|
|
377
|
-
let date = null;
|
|
378
|
-
if (parsed.ts) {
|
|
379
|
-
const d = new Date(parsed.ts);
|
|
380
|
-
if (!Number.isNaN(d.getTime()))
|
|
381
|
-
date = toDateStr(d);
|
|
382
|
-
}
|
|
383
|
-
const model = String(ctx.model_id ?? ctx.model ?? parsed.model ?? '').trim() || 'grok';
|
|
384
|
-
return { tokens, model, date };
|
|
385
|
-
}
|
|
386
|
-
catch {
|
|
387
|
-
return null;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
/** Grok Build's local usage log (and any rotated `unified*.jsonl` siblings). */
|
|
391
|
-
export async function grokLogFiles() {
|
|
392
|
-
return findFiles(grokLogsDir(), '.jsonl');
|
|
393
|
-
}
|
|
394
|
-
export async function scanGrok() {
|
|
395
|
-
const collector = new TokenCollector('grok');
|
|
396
|
-
for (const filePath of await grokLogFiles()) {
|
|
397
|
-
try {
|
|
398
|
-
const content = await readFile(filePath, 'utf8');
|
|
399
|
-
let fallbackDate = null;
|
|
400
|
-
for (const line of content.split('\n')) {
|
|
401
|
-
const usage = parseGrokLine(line);
|
|
402
|
-
if (!usage)
|
|
403
|
-
continue;
|
|
404
|
-
let date = usage.date;
|
|
405
|
-
if (!date) {
|
|
406
|
-
fallbackDate ??= toDateStr((await stat(filePath)).mtime);
|
|
407
|
-
date = fallbackDate;
|
|
408
|
-
}
|
|
409
|
-
collector.addDaily(date, usage.model, usage.tokens, 1);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
// skip unreadable files
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return collector.result();
|
|
417
|
-
}
|
|
418
|
-
const CURSOR_TRACKING_DB = join(homedir(), '.cursor', 'ai-tracking', 'ai-code-tracking.db');
|
|
419
|
-
/** Cursor's Electron state dir, per platform. Not where the tracking db lives. */
|
|
420
|
-
function cursorStateDirs() {
|
|
421
|
-
const home = homedir();
|
|
422
|
-
if (process.platform === 'darwin') {
|
|
423
|
-
return [join(home, 'Library', 'Application Support', 'Cursor')];
|
|
424
|
-
}
|
|
425
|
-
if (process.platform === 'win32') {
|
|
426
|
-
return process.env.APPDATA ? [join(process.env.APPDATA, 'Cursor')] : [];
|
|
427
|
-
}
|
|
428
|
-
return [join(home, '.config', 'Cursor')];
|
|
429
|
-
}
|
|
430
|
-
async function pathExists(p) {
|
|
431
|
-
try {
|
|
432
|
-
await stat(p);
|
|
433
|
-
return true;
|
|
434
|
-
}
|
|
435
|
-
catch {
|
|
436
|
-
return false;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
async function dirHasEntries(p) {
|
|
440
|
-
try {
|
|
441
|
-
return (await readdir(p)).length > 0;
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
/**
|
|
448
|
-
* Does this machine look like a Cursor machine? `scan` uses this to decide
|
|
449
|
-
* whether asking for a Cursor API key is worth a prompt.
|
|
450
|
-
*
|
|
451
|
-
* A false positive costs a question the user has to skip, so this looks for
|
|
452
|
-
* evidence Cursor has been *used* — the AI-tracking db, editor session state,
|
|
453
|
-
* per-workspace storage, or local file history — rather than merely installed.
|
|
454
|
-
* Every probe is a filesystem existence check, so it stays cheap enough to run
|
|
455
|
-
* inline in the scan flow.
|
|
456
|
-
*/
|
|
457
|
-
export async function detectCursorUsage() {
|
|
458
|
-
if (await pathExists(CURSOR_TRACKING_DB))
|
|
459
|
-
return true;
|
|
460
|
-
for (const dir of cursorStateDirs()) {
|
|
461
|
-
if (await pathExists(join(dir, 'User', 'globalStorage', 'state.vscdb'))) {
|
|
462
|
-
return true;
|
|
463
|
-
}
|
|
464
|
-
if (await dirHasEntries(join(dir, 'User', 'workspaceStorage')))
|
|
465
|
-
return true;
|
|
466
|
-
if (await dirHasEntries(join(dir, 'User', 'History')))
|
|
467
|
-
return true;
|
|
468
|
-
}
|
|
469
|
-
return false;
|
|
470
|
-
}
|
|
471
|
-
export async function scanCursorLocal() {
|
|
472
|
-
const dbPath = CURSOR_TRACKING_DB;
|
|
473
|
-
try {
|
|
474
|
-
await stat(dbPath);
|
|
475
|
-
}
|
|
476
|
-
catch {
|
|
477
|
-
return { ...emptyResult('cursor'), cursorStats: null };
|
|
478
|
-
}
|
|
479
|
-
const collector = new TokenCollector('cursor');
|
|
480
|
-
try {
|
|
481
|
-
const rows = await queryDb(dbPath, `select commitHash, linesAdded, linesDeleted, composerLinesAdded, composerLinesDeleted, humanLinesAdded, humanLinesDeleted, v2AiPercentage from scored_commits;`);
|
|
482
|
-
if (rows.length === 0)
|
|
483
|
-
return { ...emptyResult('cursor'), cursorStats: null };
|
|
484
|
-
let totalTokens = 0;
|
|
485
|
-
let totalAiLines = 0;
|
|
486
|
-
let totalHumanLines = 0;
|
|
487
|
-
let aiPctSum = 0;
|
|
488
|
-
for (const row of rows) {
|
|
489
|
-
const composerAdded = num(row[3]);
|
|
490
|
-
const composerDeleted = num(row[4]);
|
|
491
|
-
const humanAdded = num(row[5]);
|
|
492
|
-
const aiLines = composerAdded + composerDeleted;
|
|
493
|
-
totalAiLines += composerAdded;
|
|
494
|
-
totalHumanLines += humanAdded;
|
|
495
|
-
aiPctSum += num(row[7]);
|
|
496
|
-
totalTokens += aiLines * 30;
|
|
497
|
-
}
|
|
498
|
-
let models = [];
|
|
499
|
-
try {
|
|
500
|
-
const modelRows = await queryDb(dbPath, `select model, count(*) from ai_code_hashes where model is not null and model != '' group by model order by count(*) desc;`);
|
|
501
|
-
models = modelRows.map((row) => ({
|
|
502
|
-
model: str(row[0]),
|
|
503
|
-
uses: num(row[1]),
|
|
504
|
-
}));
|
|
505
|
-
}
|
|
506
|
-
catch {
|
|
507
|
-
// optional
|
|
508
|
-
}
|
|
509
|
-
if (totalTokens > 0)
|
|
510
|
-
collector.addDaily(toDateStr(new Date()), '', totalTokens, 1);
|
|
511
|
-
const stats = {
|
|
512
|
-
totalCommits: rows.length,
|
|
513
|
-
aiLinesAdded: totalAiLines,
|
|
514
|
-
humanLinesAdded: totalHumanLines,
|
|
515
|
-
avgAiPercent: rows.length > 0 ? Math.round((aiPctSum / rows.length) * 10) / 10 : 0,
|
|
516
|
-
models,
|
|
517
|
-
};
|
|
518
|
-
return collector.result({ cursorStats: stats });
|
|
519
|
-
}
|
|
520
|
-
catch {
|
|
521
|
-
return { ...emptyResult('cursor'), cursorStats: null };
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
export async function scanCursorApi() {
|
|
525
|
-
const { apiKey, email } = await resolveCursorAuth();
|
|
526
|
-
if (!apiKey) {
|
|
527
|
-
return { ...emptyResult('cursor'), cursorScanStatus: { source: 'none' } };
|
|
528
|
-
}
|
|
529
|
-
const collector = new TokenCollector('cursor');
|
|
530
|
-
const now = Date.now();
|
|
531
|
-
const startDate = now - 5 * 365 * 24 * 60 * 60 * 1000;
|
|
532
|
-
const endDate = now;
|
|
533
|
-
let page = 1;
|
|
534
|
-
let hasMore = true;
|
|
535
|
-
let retries = 0;
|
|
536
|
-
const MAX_RETRIES = 3;
|
|
537
|
-
let eventsProcessed = 0;
|
|
538
|
-
let partialReason = null;
|
|
539
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
540
|
-
while (hasMore) {
|
|
541
|
-
const body = {
|
|
542
|
-
startDate,
|
|
543
|
-
endDate,
|
|
544
|
-
page,
|
|
545
|
-
pageSize: 100,
|
|
546
|
-
};
|
|
547
|
-
if (email)
|
|
548
|
-
body.email = email;
|
|
549
|
-
let res;
|
|
550
|
-
try {
|
|
551
|
-
res = await fetch('https://api.cursor.com/teams/filtered-usage-events', {
|
|
552
|
-
method: 'POST',
|
|
553
|
-
headers: {
|
|
554
|
-
'Content-Type': 'application/json',
|
|
555
|
-
Authorization: `Bearer ${apiKey}`,
|
|
556
|
-
},
|
|
557
|
-
body: JSON.stringify(body),
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
catch (err) {
|
|
561
|
-
partialReason = `network: ${err instanceof Error ? err.message : 'unknown'}`;
|
|
562
|
-
break;
|
|
563
|
-
}
|
|
564
|
-
if (!res.ok) {
|
|
565
|
-
if (res.status === 429 && retries < MAX_RETRIES) {
|
|
566
|
-
await sleep(2000 * 2 ** retries);
|
|
567
|
-
retries++;
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
|
-
if (res.status === 401 || res.status === 403) {
|
|
571
|
-
if (eventsProcessed === 0) {
|
|
572
|
-
return {
|
|
573
|
-
...emptyResult('cursor'),
|
|
574
|
-
cursorScanStatus: {
|
|
575
|
-
source: 'api-failed',
|
|
576
|
-
reason: `Cursor rejected the API key (auth ${res.status})`,
|
|
577
|
-
},
|
|
578
|
-
};
|
|
579
|
-
}
|
|
580
|
-
partialReason = `auth ${res.status} mid-scan`;
|
|
581
|
-
break;
|
|
582
|
-
}
|
|
583
|
-
partialReason = `HTTP ${res.status}`;
|
|
584
|
-
break;
|
|
585
|
-
}
|
|
586
|
-
retries = 0;
|
|
587
|
-
let data;
|
|
588
|
-
try {
|
|
589
|
-
data = await res.json();
|
|
590
|
-
}
|
|
591
|
-
catch {
|
|
592
|
-
partialReason = 'invalid json';
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
for (const event of data.usageEvents ?? []) {
|
|
596
|
-
if (!event.isTokenBasedCall)
|
|
597
|
-
continue;
|
|
598
|
-
const tu = event.tokenUsage ??
|
|
599
|
-
event;
|
|
600
|
-
const tokens = (tu.inputTokens ?? 0) +
|
|
601
|
-
(tu.outputTokens ?? 0) +
|
|
602
|
-
(tu.cacheWriteTokens ?? 0) +
|
|
603
|
-
(tu.cacheReadTokens ?? 0);
|
|
604
|
-
if (tokens > 0 && event.timestamp) {
|
|
605
|
-
const rawTs = event.timestamp;
|
|
606
|
-
const tsMs = typeof rawTs === 'string' && /^\d+$/.test(rawTs)
|
|
607
|
-
? Number(rawTs)
|
|
608
|
-
: rawTs;
|
|
609
|
-
const d = new Date(tsMs);
|
|
610
|
-
const model = event.model ?? '';
|
|
611
|
-
collector.addDaily(toDateStr(d), model, tokens, 1);
|
|
612
|
-
eventsProcessed++;
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
hasMore = data.pagination?.hasNextPage ?? false;
|
|
616
|
-
page++;
|
|
617
|
-
if (page > 500) {
|
|
618
|
-
partialReason = 'pagination cap (500 pages)';
|
|
619
|
-
break;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
const cursorScanStatus = partialReason
|
|
623
|
-
? { source: 'api-partial', events: eventsProcessed, reason: partialReason }
|
|
624
|
-
: { source: 'api', events: eventsProcessed };
|
|
625
|
-
return collector.result({ cursorScanStatus });
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Choose between the two Cursor sources: the API scan when it produced data,
|
|
629
|
-
* else the local `lines × 30` estimate.
|
|
630
|
-
*
|
|
631
|
-
* Either way the winner carries the local commit stats (the API has no notion of
|
|
632
|
-
* commits, and the share card renders them) and the API's status. Propagating
|
|
633
|
-
* the status onto the local fallback is what lets a rejected key surface as
|
|
634
|
-
* `api-failed` — otherwise a bad key is indistinguishable from no key at all,
|
|
635
|
-
* and the user silently gets the estimate while believing they get exact counts.
|
|
636
|
-
*/
|
|
637
|
-
function resolveCursor(api, local) {
|
|
638
|
-
if (api.daily.length > 0) {
|
|
639
|
-
return { ...api, cursorStats: local.cursorStats ?? null };
|
|
640
|
-
}
|
|
641
|
-
return {
|
|
642
|
-
...local,
|
|
643
|
-
cursorScanStatus: api.cursorScanStatus ?? { source: 'none' },
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
/** Run every scanner in parallel, returning one resolved result per tool. */
|
|
647
|
-
export async function collectToolScans() {
|
|
648
|
-
const [claude, codex, cursorApi, cursorLocal, openclaw, hermes, opencode, grok, copilot, antigravity,] = await Promise.all([
|
|
649
|
-
scanClaudeCode(),
|
|
650
|
-
scanCodex(),
|
|
651
|
-
scanCursorApi(),
|
|
652
|
-
scanCursorLocal(),
|
|
653
|
-
scanOpenclaw(),
|
|
654
|
-
scanHermes(),
|
|
655
|
-
scanOpenCode(),
|
|
656
|
-
scanGrok(),
|
|
657
|
-
scanGitHubCopilot(),
|
|
658
|
-
scanAntigravity(),
|
|
659
|
-
]);
|
|
660
|
-
const cursor = resolveCursor(cursorApi, cursorLocal);
|
|
661
|
-
return [
|
|
662
|
-
claude,
|
|
663
|
-
codex,
|
|
664
|
-
cursor,
|
|
665
|
-
openclaw,
|
|
666
|
-
hermes,
|
|
667
|
-
opencode,
|
|
668
|
-
grok,
|
|
669
|
-
copilot,
|
|
670
|
-
antigravity,
|
|
671
|
-
];
|
|
672
|
-
}
|
|
673
|
-
/**
|
|
674
|
-
* Re-run only the Cursor API scanner and swap the result into an existing set of
|
|
675
|
-
* scans. `scan` calls this after the user supplies a key mid-flow, so the exact
|
|
676
|
-
* API numbers replace the local estimate without re-walking every other tool's
|
|
677
|
-
* logs (which for a heavy Claude Code user means re-parsing thousands of files).
|
|
678
|
-
*/
|
|
679
|
-
export async function rescanCursorWithApi(results) {
|
|
680
|
-
const api = await scanCursorApi();
|
|
681
|
-
return results.map((r) => (r.tool === 'cursor' ? resolveCursor(api, r) : r));
|
|
682
|
-
}
|
|
683
|
-
/** Merge per-tool results into a single submit-ready payload. */
|
|
684
|
-
export function mergeToolScans(results) {
|
|
685
|
-
const toolTotals = {};
|
|
686
|
-
const dailyTotals = [];
|
|
687
|
-
const modelTotals = {};
|
|
688
|
-
const modelsByTool = {};
|
|
689
|
-
let grandTotal = 0;
|
|
690
|
-
for (const r of results) {
|
|
691
|
-
let toolSum = 0;
|
|
692
|
-
for (const d of r.daily) {
|
|
693
|
-
dailyTotals.push(d);
|
|
694
|
-
toolSum += d.tokens;
|
|
695
|
-
}
|
|
696
|
-
toolTotals[r.tool] = (toolTotals[r.tool] ?? 0) + toolSum;
|
|
697
|
-
grandTotal += toolSum;
|
|
698
|
-
let byTool = modelsByTool[r.tool];
|
|
699
|
-
if (!byTool) {
|
|
700
|
-
byTool = {};
|
|
701
|
-
modelsByTool[r.tool] = byTool;
|
|
702
|
-
}
|
|
703
|
-
for (const [model, tokens] of Object.entries(r.models)) {
|
|
704
|
-
modelTotals[model] = (modelTotals[model] ?? 0) + tokens;
|
|
705
|
-
byTool[model] = (byTool[model] ?? 0) + tokens;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
const cursor = results.find((r) => r.tool === 'cursor');
|
|
709
|
-
return {
|
|
710
|
-
toolTotals,
|
|
711
|
-
dailyTotals,
|
|
712
|
-
modelTotals,
|
|
713
|
-
modelsByTool,
|
|
714
|
-
grandTotal,
|
|
715
|
-
cursorStats: cursor?.cursorStats ?? null,
|
|
716
|
-
cursorScanStatus: cursor?.cursorScanStatus ?? { source: 'none' },
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Run every scanner and merge the results — the one-shot entry point for callers
|
|
721
|
-
* that don't need to re-scan a single tool afterwards (`sync`, the daily job).
|
|
722
|
-
*/
|
|
723
|
-
export async function scanAllTools() {
|
|
724
|
-
return mergeToolScans(await collectToolScans());
|
|
725
|
-
}
|
|
726
|
-
//# sourceMappingURL=index.js.map
|