hacklab 0.6.1 → 0.6.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/dist/commands/brag.js +4 -4
- package/dist/commands/brag.js.map +1 -1
- package/dist/commands/chat.d.ts.map +1 -1
- package/dist/commands/chat.js +16 -12
- package/dist/commands/chat.js.map +1 -1
- package/dist/commands/drop.d.ts +9 -0
- package/dist/commands/drop.d.ts.map +1 -1
- package/dist/commands/drop.js +33 -4
- package/dist/commands/drop.js.map +1 -1
- package/dist/commands/join.d.ts +3 -1
- package/dist/commands/join.d.ts.map +1 -1
- package/dist/commands/join.js +31 -3
- package/dist/commands/join.js.map +1 -1
- package/dist/commands/login.d.ts +18 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +156 -19
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +2 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +15 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/org.js +5 -5
- package/dist/commands/org.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +9 -1
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +10 -2
- package/dist/commands/whoami.js.map +1 -1
- package/dist/index.js +27 -88
- package/dist/index.js.map +1 -1
- package/dist/registry.d.ts +15 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +77 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolve-command.d.ts +0 -1
- package/dist/resolve-command.d.ts.map +1 -1
- package/dist/resolve-command.js +2 -17
- package/dist/resolve-command.js.map +1 -1
- package/dist/session.d.ts +26 -5
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +55 -8
- package/dist/session.js.map +1 -1
- package/dist/share.d.ts +2 -2
- package/dist/share.js +2 -2
- package/dist/sync.d.ts +18 -34
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +47 -686
- package/dist/sync.js.map +1 -1
- package/dist/ui.d.ts +2 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +4 -0
- package/dist/ui.js.map +1 -1
- package/dist/utils/openBrowser.d.ts +12 -0
- package/dist/utils/openBrowser.d.ts.map +1 -1
- package/dist/utils/openBrowser.js +30 -12
- package/dist/utils/openBrowser.js.map +1 -1
- package/package.json +3 -2
- package/dist/commands/company.d.ts +0 -2
- package/dist/commands/company.d.ts.map +0 -1
- package/dist/commands/company.js +0 -214
- package/dist/commands/company.js.map +0 -1
- package/dist/commands/exam.d.ts +0 -5
- package/dist/commands/exam.d.ts.map +0 -1
- package/dist/commands/exam.js +0 -224
- package/dist/commands/exam.js.map +0 -1
- package/dist/commands/scan-profile.d.ts +0 -33
- package/dist/commands/scan-profile.d.ts.map +0 -1
- package/dist/commands/scan-profile.js +0 -279
- package/dist/commands/scan-profile.js.map +0 -1
- package/dist/dd.d.ts +0 -3
- package/dist/dd.d.ts.map +0 -1
- package/dist/dd.js +0 -28
- package/dist/dd.js.map +0 -1
package/dist/sync.js
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { loadConfig } from './config.js';
|
|
1
|
+
import { formatBytes, formatTokens, scanAllTools, } from './scanners/index.js';
|
|
2
|
+
import { resolveAppUrl } from './session.js';
|
|
5
3
|
import { dim } from './ui.js';
|
|
4
|
+
// Token scanning lives in one place — the scanners/ module (used by both `join`
|
|
5
|
+
// and `sync`). This file is just the `sync` command's API layer: session
|
|
6
|
+
// checks, the claim upload, and error formatting. Re-export the shared scan
|
|
7
|
+
// helpers so existing importers don't have to reach into scanners/ directly.
|
|
8
|
+
export { formatBytes, formatTokens };
|
|
6
9
|
export const LOGIN_EXPIRED_MESSAGE = 'login expired. run hacklab login again';
|
|
7
|
-
const HOURLY_WINDOW_DAYS = 90;
|
|
8
|
-
const modelAccumulator = new Map();
|
|
9
|
-
const hourlyAccumulator = new Map();
|
|
10
|
-
const _cursorScanStatus = {
|
|
11
|
-
value: { source: 'none' },
|
|
12
|
-
};
|
|
13
10
|
function buildApiUrl(session, path) {
|
|
14
|
-
return `${session
|
|
11
|
+
return `${resolveAppUrl(session)}${path}`;
|
|
15
12
|
}
|
|
16
13
|
function isLoopbackAppUrl(value) {
|
|
17
14
|
try {
|
|
@@ -37,7 +34,7 @@ function getCauseMessage(err) {
|
|
|
37
34
|
return err.message === 'fetch failed' ? null : err.message;
|
|
38
35
|
}
|
|
39
36
|
function formatFetchFailure(session, err) {
|
|
40
|
-
const app = session
|
|
37
|
+
const app = resolveAppUrl(session);
|
|
41
38
|
const cause = getCauseMessage(err);
|
|
42
39
|
const details = cause ? ` (${cause})` : '';
|
|
43
40
|
if (isLoopbackAppUrl(app)) {
|
|
@@ -74,688 +71,54 @@ export async function checkSession(session) {
|
|
|
74
71
|
return { status: 'ok' };
|
|
75
72
|
return {
|
|
76
73
|
status: 'failed',
|
|
77
|
-
message: `Hacklab API not found at ${session
|
|
74
|
+
message: `Hacklab API not found at ${resolveAppUrl(session)}. check ${dim('HACKLAB_APP_URL')} or run ${dim('hacklab login')} again`,
|
|
78
75
|
};
|
|
79
76
|
}
|
|
80
77
|
if (res.status >= 500) {
|
|
81
78
|
return {
|
|
82
79
|
status: 'failed',
|
|
83
|
-
message: `Hacklab API failed at ${session
|
|
80
|
+
message: `Hacklab API failed at ${resolveAppUrl(session)} (${res.status}). try again later`,
|
|
84
81
|
};
|
|
85
82
|
}
|
|
86
83
|
return { status: 'ok' };
|
|
87
84
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
function
|
|
94
|
-
if (date < cutoff)
|
|
95
|
-
return;
|
|
96
|
-
const key = `${date}|${hour}|${tool}|${model ?? ''}`;
|
|
97
|
-
const existing = hourlyAccumulator.get(key);
|
|
98
|
-
if (existing) {
|
|
99
|
-
existing.tokens += tokens;
|
|
100
|
-
existing.messages += messages;
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
hourlyAccumulator.set(key, { tokens, messages });
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function flushHourly() {
|
|
107
|
-
const out = [];
|
|
108
|
-
for (const [key, { tokens, messages }] of hourlyAccumulator.entries()) {
|
|
109
|
-
const [date, hourStr, tool, model] = key.split('|');
|
|
110
|
-
if (!date || hourStr === undefined || !tool)
|
|
111
|
-
continue;
|
|
112
|
-
out.push({
|
|
113
|
-
date,
|
|
114
|
-
hour: Number(hourStr),
|
|
115
|
-
tool,
|
|
116
|
-
model: model || undefined,
|
|
117
|
-
tokens,
|
|
118
|
-
messages,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
return out;
|
|
122
|
-
}
|
|
123
|
-
function addToDaily(map, date, model, tokens, messages) {
|
|
124
|
-
const key = `${date}|${model}`;
|
|
125
|
-
const existing = map.get(key);
|
|
126
|
-
if (existing) {
|
|
127
|
-
existing.tokens += tokens;
|
|
128
|
-
existing.messages += messages;
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
map.set(key, { tokens, messages });
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
function dailyMapToEntries(map, tool) {
|
|
135
|
-
return Array.from(map.entries()).map(([key, { tokens, messages }]) => {
|
|
136
|
-
const [date, model] = key.split('|');
|
|
137
|
-
return {
|
|
138
|
-
date: date ?? '',
|
|
139
|
-
tool,
|
|
140
|
-
tokens,
|
|
141
|
-
messages,
|
|
142
|
-
model: model || undefined,
|
|
143
|
-
};
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
function toDateStr(ts) {
|
|
147
|
-
const v = typeof ts === 'string' && /^\d+$/.test(ts) ? Number(ts) : ts;
|
|
148
|
-
const d = new Date(v);
|
|
149
|
-
return d.toISOString().slice(0, 10);
|
|
150
|
-
}
|
|
151
|
-
export function formatTokens(n) {
|
|
152
|
-
if (n >= 1_000_000_000)
|
|
153
|
-
return `${(n / 1_000_000_000).toFixed(1)}B`;
|
|
154
|
-
if (n >= 1_000_000)
|
|
155
|
-
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
156
|
-
if (n >= 1_000)
|
|
157
|
-
return `${(n / 1_000).toFixed(0)}K`;
|
|
158
|
-
return String(n);
|
|
159
|
-
}
|
|
160
|
-
export function formatBytes(n) {
|
|
161
|
-
if (n >= 1_073_741_824)
|
|
162
|
-
return `${(n / 1_073_741_824).toFixed(1)}GB`;
|
|
163
|
-
if (n >= 1_048_576)
|
|
164
|
-
return `${(n / 1_048_576).toFixed(1)}MB`;
|
|
165
|
-
if (n >= 1_024)
|
|
166
|
-
return `${(n / 1_024).toFixed(0)}KB`;
|
|
167
|
-
return `${n}B`;
|
|
168
|
-
}
|
|
169
|
-
async function findFiles(dir, ext) {
|
|
170
|
-
const results = [];
|
|
171
|
-
async function walk(current) {
|
|
172
|
-
try {
|
|
173
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
174
|
-
for (const entry of entries) {
|
|
175
|
-
const fullPath = join(current, entry.name);
|
|
176
|
-
if (entry.isDirectory()) {
|
|
177
|
-
await walk(fullPath);
|
|
178
|
-
}
|
|
179
|
-
else if (entry.name.endsWith(ext)) {
|
|
180
|
-
results.push(fullPath);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
catch {
|
|
185
|
-
// skip unreadable directories
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
await walk(dir);
|
|
189
|
-
return results;
|
|
190
|
-
}
|
|
191
|
-
async function scanClaudeCode() {
|
|
192
|
-
const claudeDir = join(homedir(), '.claude', 'projects');
|
|
193
|
-
try {
|
|
194
|
-
await stat(claudeDir);
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
return [];
|
|
198
|
-
}
|
|
199
|
-
const files = await findFiles(claudeDir, '.jsonl');
|
|
200
|
-
const dailyByModel = new Map();
|
|
201
|
-
const cutoff = hourlyCutoff();
|
|
202
|
-
for (const filePath of files) {
|
|
203
|
-
try {
|
|
204
|
-
const content = await readFile(filePath, 'utf8');
|
|
205
|
-
for (const line of content.split('\n')) {
|
|
206
|
-
if (!line.trim())
|
|
207
|
-
continue;
|
|
208
|
-
try {
|
|
209
|
-
const parsed = JSON.parse(line);
|
|
210
|
-
const usage = parsed.message?.usage ?? parsed.usage ?? null;
|
|
211
|
-
if (!usage)
|
|
212
|
-
continue;
|
|
213
|
-
const tokens = (usage.input_tokens ?? 0) +
|
|
214
|
-
(usage.output_tokens ?? 0) +
|
|
215
|
-
(usage.cache_creation_input_tokens ?? 0) +
|
|
216
|
-
(usage.cache_read_input_tokens ?? 0);
|
|
217
|
-
if (tokens <= 0)
|
|
218
|
-
continue;
|
|
219
|
-
let date;
|
|
220
|
-
let hour = null;
|
|
221
|
-
if (parsed.timestamp) {
|
|
222
|
-
const d = new Date(typeof parsed.timestamp === 'string' &&
|
|
223
|
-
/^\d+$/.test(parsed.timestamp)
|
|
224
|
-
? Number(parsed.timestamp)
|
|
225
|
-
: parsed.timestamp);
|
|
226
|
-
date = toDateStr(d);
|
|
227
|
-
hour = d.getHours();
|
|
228
|
-
}
|
|
229
|
-
else {
|
|
230
|
-
const s = await stat(filePath);
|
|
231
|
-
date = toDateStr(s.mtime);
|
|
232
|
-
}
|
|
233
|
-
const model = parsed.message?.model ?? '';
|
|
234
|
-
addToDaily(dailyByModel, date, model, tokens, 1);
|
|
235
|
-
if (model) {
|
|
236
|
-
modelAccumulator.set(model, (modelAccumulator.get(model) ?? 0) + tokens);
|
|
237
|
-
}
|
|
238
|
-
if (hour !== null) {
|
|
239
|
-
addHourly(date, hour, 'claude_code', model, tokens, cutoff, 1);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
// skip malformed lines
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
// skip unreadable files
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return dailyMapToEntries(dailyByModel, 'claude_code');
|
|
252
|
-
}
|
|
253
|
-
async function scanCodex() {
|
|
254
|
-
const codexDir = join(homedir(), '.codex', 'sessions');
|
|
255
|
-
try {
|
|
256
|
-
await stat(codexDir);
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
return [];
|
|
260
|
-
}
|
|
261
|
-
const files = await findFiles(codexDir, '.jsonl');
|
|
262
|
-
const dailyByModel = new Map();
|
|
263
|
-
for (const filePath of files) {
|
|
264
|
-
try {
|
|
265
|
-
const content = await readFile(filePath, 'utf8');
|
|
266
|
-
const relPath = filePath.slice(codexDir.length + 1);
|
|
267
|
-
const parts = relPath.split('/');
|
|
268
|
-
let date;
|
|
269
|
-
if (parts.length >= 3) {
|
|
270
|
-
date = `${parts[0]}-${parts[1]?.padStart(2, '0')}-${parts[2]?.padStart(2, '0')}`;
|
|
271
|
-
}
|
|
272
|
-
else {
|
|
273
|
-
const s = await stat(filePath);
|
|
274
|
-
date = toDateStr(s.mtime);
|
|
275
|
-
}
|
|
276
|
-
let maxTotal = 0;
|
|
277
|
-
let fileModel = '';
|
|
278
|
-
for (const line of content.split('\n')) {
|
|
279
|
-
if (!line.trim())
|
|
280
|
-
continue;
|
|
281
|
-
try {
|
|
282
|
-
const parsed = JSON.parse(line);
|
|
283
|
-
if (parsed.payload?.model)
|
|
284
|
-
fileModel = parsed.payload.model;
|
|
285
|
-
const usage = parsed.payload?.info?.total_token_usage;
|
|
286
|
-
if (usage) {
|
|
287
|
-
const t = (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0);
|
|
288
|
-
if (t > maxTotal)
|
|
289
|
-
maxTotal = t;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
// skip
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (maxTotal > 0) {
|
|
297
|
-
addToDaily(dailyByModel, date, fileModel, maxTotal, 1);
|
|
298
|
-
if (fileModel) {
|
|
299
|
-
modelAccumulator.set(fileModel, (modelAccumulator.get(fileModel) ?? 0) + maxTotal);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
// skip
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return dailyMapToEntries(dailyByModel, 'codex');
|
|
308
|
-
}
|
|
309
|
-
async function scanHermes() {
|
|
310
|
-
const dbPath = join(homedir(), '.hermes', 'state.db');
|
|
311
|
-
try {
|
|
312
|
-
await stat(dbPath);
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
return [];
|
|
316
|
-
}
|
|
317
|
-
const { execSync } = await import('node:child_process');
|
|
318
|
-
const dailyByModel = new Map();
|
|
319
|
-
const cutoff = hourlyCutoff();
|
|
320
|
-
try {
|
|
321
|
-
const raw = execSync(`sqlite3 -readonly "${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"`, { encoding: 'utf8', timeout: 10000 }).trim();
|
|
322
|
-
if (!raw)
|
|
323
|
-
return [];
|
|
324
|
-
for (const row of raw.split('\n')) {
|
|
325
|
-
const [_id, model, startedAt, inputTokens, outputTokens, cacheRead, cacheWrite, reasoning, msgCount,] = row.split('|');
|
|
326
|
-
const tokens = (Number.parseInt(inputTokens ?? '0', 10) || 0) +
|
|
327
|
-
(Number.parseInt(outputTokens ?? '0', 10) || 0) +
|
|
328
|
-
(Number.parseInt(cacheRead ?? '0', 10) || 0) +
|
|
329
|
-
(Number.parseInt(cacheWrite ?? '0', 10) || 0) +
|
|
330
|
-
(Number.parseInt(reasoning ?? '0', 10) || 0);
|
|
331
|
-
if (tokens <= 0)
|
|
332
|
-
continue;
|
|
333
|
-
const tsSec = Number.parseFloat(startedAt ?? '0');
|
|
334
|
-
if (!Number.isFinite(tsSec) || tsSec <= 0)
|
|
335
|
-
continue;
|
|
336
|
-
const tsMs = Math.round(tsSec * 1000);
|
|
337
|
-
const d = new Date(tsMs);
|
|
338
|
-
const date = toDateStr(d);
|
|
339
|
-
const hour = d.getHours();
|
|
340
|
-
const m = (model ?? '').trim();
|
|
341
|
-
const messages = Number.parseInt(msgCount ?? '0', 10) || 1;
|
|
342
|
-
addToDaily(dailyByModel, date, m, tokens, messages);
|
|
343
|
-
if (m) {
|
|
344
|
-
modelAccumulator.set(m, (modelAccumulator.get(m) ?? 0) + tokens);
|
|
345
|
-
}
|
|
346
|
-
addHourly(date, hour, 'hermes', m, tokens, cutoff, messages);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
catch {
|
|
350
|
-
return [];
|
|
351
|
-
}
|
|
352
|
-
return dailyMapToEntries(dailyByModel, 'hermes');
|
|
353
|
-
}
|
|
354
|
-
async function scanOpenclaw() {
|
|
355
|
-
const agentsDir = join(homedir(), '.openclaw', 'agents');
|
|
356
|
-
try {
|
|
357
|
-
await stat(agentsDir);
|
|
358
|
-
}
|
|
359
|
-
catch {
|
|
360
|
-
return [];
|
|
361
|
-
}
|
|
362
|
-
const sessionFiles = [];
|
|
85
|
+
/**
|
|
86
|
+
* Ask the server to mirror the user's pinned GitHub repos into their projects
|
|
87
|
+
* (and drop ones that are no longer pinned/public). Best-effort and non-fatal —
|
|
88
|
+
* returns null on any failure so a GitHub hiccup never fails `sync`/`join`.
|
|
89
|
+
*/
|
|
90
|
+
export async function syncGithubRepos(session) {
|
|
363
91
|
try {
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
catch {
|
|
374
|
-
return [];
|
|
375
|
-
}
|
|
376
|
-
if (sessionFiles.length === 0)
|
|
377
|
-
return [];
|
|
378
|
-
const dailyByModel = new Map();
|
|
379
|
-
const cutoff = hourlyCutoff();
|
|
380
|
-
for (const filePath of sessionFiles) {
|
|
381
|
-
try {
|
|
382
|
-
const content = await readFile(filePath, 'utf8');
|
|
383
|
-
let fileFallbackDate = null;
|
|
384
|
-
for (const line of content.split('\n')) {
|
|
385
|
-
if (!line.trim())
|
|
386
|
-
continue;
|
|
387
|
-
try {
|
|
388
|
-
const parsed = JSON.parse(line);
|
|
389
|
-
const usage = parsed.usage ??
|
|
390
|
-
parsed.tokenUsage ??
|
|
391
|
-
parsed.message?.usage ??
|
|
392
|
-
parsed.response?.usage ??
|
|
393
|
-
null;
|
|
394
|
-
if (!usage || typeof usage !== 'object')
|
|
395
|
-
continue;
|
|
396
|
-
const tokens = (usage.total ??
|
|
397
|
-
(usage.input ?? usage.inputTokens ?? 0) +
|
|
398
|
-
(usage.output ?? usage.outputTokens ?? 0) +
|
|
399
|
-
(usage.cacheRead ?? usage.cacheReadTokens ?? 0) +
|
|
400
|
-
(usage.cacheWrite ?? usage.cacheWriteTokens ?? 0));
|
|
401
|
-
if (!tokens || tokens <= 0)
|
|
402
|
-
continue;
|
|
403
|
-
let date;
|
|
404
|
-
let hour = null;
|
|
405
|
-
const ts = parsed.timestamp ?? parsed.t ?? parsed.time ?? null;
|
|
406
|
-
if (ts) {
|
|
407
|
-
const tsMs = typeof ts === 'string' && /^\d+$/.test(ts) ? Number(ts) : ts;
|
|
408
|
-
const d = new Date(tsMs);
|
|
409
|
-
if (!Number.isNaN(d.getTime())) {
|
|
410
|
-
date = toDateStr(d);
|
|
411
|
-
hour = d.getHours();
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
if (!fileFallbackDate) {
|
|
415
|
-
const s = await stat(filePath);
|
|
416
|
-
fileFallbackDate = toDateStr(s.mtime);
|
|
417
|
-
}
|
|
418
|
-
date = fileFallbackDate;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
if (!fileFallbackDate) {
|
|
423
|
-
const s = await stat(filePath);
|
|
424
|
-
fileFallbackDate = toDateStr(s.mtime);
|
|
425
|
-
}
|
|
426
|
-
date = fileFallbackDate;
|
|
427
|
-
}
|
|
428
|
-
const model = parsed.model ?? parsed.response?.model ?? '';
|
|
429
|
-
addToDaily(dailyByModel, date, model, tokens, 1);
|
|
430
|
-
if (model) {
|
|
431
|
-
modelAccumulator.set(model, (modelAccumulator.get(model) ?? 0) + tokens);
|
|
432
|
-
}
|
|
433
|
-
if (hour !== null) {
|
|
434
|
-
addHourly(date, hour, 'openclaw', model, tokens, cutoff, 1);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
catch {
|
|
438
|
-
// skip malformed lines
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
// skip unreadable files
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
return dailyMapToEntries(dailyByModel, 'openclaw');
|
|
447
|
-
}
|
|
448
|
-
async function scanCursorLocal() {
|
|
449
|
-
const dbPath = join(homedir(), '.cursor', 'ai-tracking', 'ai-code-tracking.db');
|
|
450
|
-
try {
|
|
451
|
-
await stat(dbPath);
|
|
452
|
-
}
|
|
453
|
-
catch {
|
|
454
|
-
return { entries: [], stats: null };
|
|
455
|
-
}
|
|
456
|
-
const { execSync } = await import('node:child_process');
|
|
457
|
-
try {
|
|
458
|
-
const raw = execSync(`sqlite3 "${dbPath}" "select commitHash, linesAdded, linesDeleted, composerLinesAdded, composerLinesDeleted, humanLinesAdded, humanLinesDeleted, v2AiPercentage from scored_commits;"`, { encoding: 'utf8', timeout: 10000 }).trim();
|
|
459
|
-
if (!raw)
|
|
460
|
-
return { entries: [], stats: null };
|
|
461
|
-
const rows = raw.split('\n');
|
|
462
|
-
let totalTokens = 0;
|
|
463
|
-
let totalAiLines = 0;
|
|
464
|
-
let totalHumanLines = 0;
|
|
465
|
-
let aiPctSum = 0;
|
|
466
|
-
for (const row of rows) {
|
|
467
|
-
const [, , , composerAdded, composerDeleted, humanAdded, , aiPct] = row.split('|');
|
|
468
|
-
const aiLines = (parseInt(composerAdded ?? '0', 10) || 0) +
|
|
469
|
-
(parseInt(composerDeleted ?? '0', 10) || 0);
|
|
470
|
-
totalAiLines += parseInt(composerAdded ?? '0', 10) || 0;
|
|
471
|
-
totalHumanLines += parseInt(humanAdded ?? '0', 10) || 0;
|
|
472
|
-
aiPctSum += parseFloat(aiPct ?? '0') || 0;
|
|
473
|
-
totalTokens += aiLines * 30;
|
|
474
|
-
}
|
|
475
|
-
let models = [];
|
|
476
|
-
try {
|
|
477
|
-
const modelRaw = execSync(`sqlite3 "${dbPath}" "select model, count(*) from ai_code_hashes where model is not null and model != '' group by model order by count(*) desc;"`, { encoding: 'utf8', timeout: 5000 }).trim();
|
|
478
|
-
if (modelRaw) {
|
|
479
|
-
models = modelRaw.split('\n').map((line) => {
|
|
480
|
-
const [model, count] = line.split('|');
|
|
481
|
-
return { model: model ?? '', uses: parseInt(count ?? '0', 10) || 0 };
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
// optional
|
|
487
|
-
}
|
|
488
|
-
const entries = totalTokens > 0
|
|
489
|
-
? [{ date: toDateStr(new Date()), tool: 'cursor', tokens: totalTokens }]
|
|
490
|
-
: [];
|
|
491
|
-
return {
|
|
492
|
-
entries,
|
|
493
|
-
stats: {
|
|
494
|
-
totalCommits: rows.length,
|
|
495
|
-
aiLinesAdded: totalAiLines,
|
|
496
|
-
humanLinesAdded: totalHumanLines,
|
|
497
|
-
avgAiPercent: rows.length > 0 ? Math.round((aiPctSum / rows.length) * 10) / 10 : 0,
|
|
498
|
-
models,
|
|
499
|
-
},
|
|
500
|
-
};
|
|
501
|
-
}
|
|
502
|
-
catch {
|
|
503
|
-
return { entries: [], stats: null };
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
async function scanCursorApi() {
|
|
507
|
-
const config = await loadConfig();
|
|
508
|
-
if (!config.cursorApiKey) {
|
|
509
|
-
_cursorScanStatus.value = { source: 'none' };
|
|
510
|
-
return [];
|
|
511
|
-
}
|
|
512
|
-
const email = config.cursorEmail;
|
|
513
|
-
const now = Date.now();
|
|
514
|
-
const fiveYearsMs = 5 * 365 * 24 * 60 * 60 * 1000;
|
|
515
|
-
const startDate = now - fiveYearsMs;
|
|
516
|
-
const endDate = now;
|
|
517
|
-
const dailyByModel = new Map();
|
|
518
|
-
const cursorCutoff = hourlyCutoff();
|
|
519
|
-
let page = 1;
|
|
520
|
-
let hasMore = true;
|
|
521
|
-
let retries = 0;
|
|
522
|
-
const MAX_RETRIES = 3;
|
|
523
|
-
let eventsProcessed = 0;
|
|
524
|
-
let partialReason = null;
|
|
525
|
-
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
526
|
-
while (hasMore) {
|
|
527
|
-
const body = {
|
|
528
|
-
startDate,
|
|
529
|
-
endDate,
|
|
530
|
-
page,
|
|
531
|
-
pageSize: 100,
|
|
532
|
-
};
|
|
533
|
-
if (email)
|
|
534
|
-
body.email = email;
|
|
535
|
-
let res;
|
|
536
|
-
try {
|
|
537
|
-
res = await fetch('https://api.cursor.com/teams/filtered-usage-events', {
|
|
538
|
-
method: 'POST',
|
|
539
|
-
headers: {
|
|
540
|
-
'Content-Type': 'application/json',
|
|
541
|
-
Authorization: `Bearer ${config.cursorApiKey}`,
|
|
542
|
-
},
|
|
543
|
-
body: JSON.stringify(body),
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
catch (err) {
|
|
547
|
-
partialReason = `network: ${err instanceof Error ? err.message : 'unknown'}`;
|
|
548
|
-
break;
|
|
549
|
-
}
|
|
550
|
-
if (!res.ok) {
|
|
551
|
-
if (res.status === 429 && retries < MAX_RETRIES) {
|
|
552
|
-
const waitMs = 2000 * 2 ** retries;
|
|
553
|
-
await sleep(waitMs);
|
|
554
|
-
retries++;
|
|
555
|
-
continue;
|
|
556
|
-
}
|
|
557
|
-
if (res.status === 401 || res.status === 403) {
|
|
558
|
-
if (eventsProcessed === 0) {
|
|
559
|
-
_cursorScanStatus.value = {
|
|
560
|
-
source: 'api-failed',
|
|
561
|
-
reason: `auth ${res.status} — check CURSOR_API_KEY`,
|
|
562
|
-
};
|
|
563
|
-
return [];
|
|
564
|
-
}
|
|
565
|
-
partialReason = `auth ${res.status} mid-scan`;
|
|
566
|
-
break;
|
|
567
|
-
}
|
|
568
|
-
partialReason = `HTTP ${res.status}`;
|
|
569
|
-
break;
|
|
570
|
-
}
|
|
571
|
-
retries = 0;
|
|
572
|
-
let data;
|
|
573
|
-
try {
|
|
574
|
-
data = await res.json();
|
|
575
|
-
}
|
|
576
|
-
catch {
|
|
577
|
-
partialReason = 'invalid json';
|
|
578
|
-
break;
|
|
579
|
-
}
|
|
580
|
-
const events = data.usageEvents ?? [];
|
|
581
|
-
for (const event of events) {
|
|
582
|
-
if (!event.isTokenBasedCall)
|
|
583
|
-
continue;
|
|
584
|
-
const tu = event.tokenUsage ??
|
|
585
|
-
event;
|
|
586
|
-
const tokens = (tu.inputTokens ?? 0) +
|
|
587
|
-
(tu.outputTokens ?? 0) +
|
|
588
|
-
(tu.cacheWriteTokens ?? 0) +
|
|
589
|
-
(tu.cacheReadTokens ?? 0);
|
|
590
|
-
if (tokens > 0 && event.timestamp) {
|
|
591
|
-
const rawTs = event.timestamp;
|
|
592
|
-
const tsMs = typeof rawTs === 'string' && /^\d+$/.test(rawTs)
|
|
593
|
-
? Number(rawTs)
|
|
594
|
-
: rawTs;
|
|
595
|
-
const eventDate = new Date(tsMs);
|
|
596
|
-
const date = toDateStr(eventDate);
|
|
597
|
-
const hour = eventDate.getHours();
|
|
598
|
-
const model = event.model ?? '';
|
|
599
|
-
addToDaily(dailyByModel, date, model, tokens, 1);
|
|
600
|
-
if (model) {
|
|
601
|
-
modelAccumulator.set(model, (modelAccumulator.get(model) ?? 0) + tokens);
|
|
602
|
-
}
|
|
603
|
-
addHourly(date, hour, 'cursor', model, tokens, cursorCutoff, 1);
|
|
604
|
-
eventsProcessed++;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
hasMore = data.pagination?.hasNextPage ?? false;
|
|
608
|
-
page++;
|
|
609
|
-
if (page > 500) {
|
|
610
|
-
partialReason = 'pagination cap (500 pages)';
|
|
611
|
-
break;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
_cursorScanStatus.value = partialReason
|
|
615
|
-
? { source: 'api-partial', events: eventsProcessed, reason: partialReason }
|
|
616
|
-
: { source: 'api', events: eventsProcessed };
|
|
617
|
-
return dailyMapToEntries(dailyByModel, 'cursor');
|
|
618
|
-
}
|
|
619
|
-
async function scanOpenCode() {
|
|
620
|
-
const openCodeDir = join(homedir(), '.local', 'share', 'opencode');
|
|
621
|
-
try {
|
|
622
|
-
await stat(openCodeDir);
|
|
623
|
-
}
|
|
624
|
-
catch {
|
|
625
|
-
return [];
|
|
626
|
-
}
|
|
627
|
-
const cutoff = hourlyCutoff();
|
|
628
|
-
const dailyByModel = new Map();
|
|
629
|
-
// Schema: data column is JSON with shape:
|
|
630
|
-
// { role, modelID, time: { created: ms }, tokens: { input, output, reasoning, cache: { read, write } } }
|
|
631
|
-
const dbPath = join(openCodeDir, 'opencode.db');
|
|
632
|
-
try {
|
|
633
|
-
await stat(dbPath);
|
|
634
|
-
const { execSync } = await import('node:child_process');
|
|
635
|
-
const raw = execSync(`sqlite3 -readonly "${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'"`, { encoding: 'utf8', timeout: 15000, stdio: 'pipe' }).trim();
|
|
636
|
-
if (raw) {
|
|
637
|
-
for (const row of raw.split('\n')) {
|
|
638
|
-
const [timeStr, model, inputStr, outputStr, cacheReadStr, cacheWriteStr, reasoningStr,] = row.split('|');
|
|
639
|
-
const tokens = (Number.parseInt(inputStr ?? '0', 10) || 0) +
|
|
640
|
-
(Number.parseInt(outputStr ?? '0', 10) || 0) +
|
|
641
|
-
(Number.parseInt(cacheReadStr ?? '0', 10) || 0) +
|
|
642
|
-
(Number.parseInt(cacheWriteStr ?? '0', 10) || 0) +
|
|
643
|
-
(Number.parseInt(reasoningStr ?? '0', 10) || 0);
|
|
644
|
-
if (tokens <= 0)
|
|
645
|
-
continue;
|
|
646
|
-
const tsMs = Number(timeStr ?? '0'); // already milliseconds
|
|
647
|
-
if (!Number.isFinite(tsMs) || tsMs <= 0)
|
|
648
|
-
continue;
|
|
649
|
-
const d = new Date(tsMs);
|
|
650
|
-
const date = toDateStr(d);
|
|
651
|
-
const hour = d.getHours();
|
|
652
|
-
const m = (model ?? '').trim();
|
|
653
|
-
addToDaily(dailyByModel, date, m, tokens, 1);
|
|
654
|
-
if (m)
|
|
655
|
-
modelAccumulator.set(m, (modelAccumulator.get(m) ?? 0) + tokens);
|
|
656
|
-
addHourly(date, hour, 'opencode', m, tokens, cutoff, 1);
|
|
657
|
-
}
|
|
658
|
-
if (dailyByModel.size > 0) {
|
|
659
|
-
return dailyMapToEntries(dailyByModel, 'opencode');
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
catch {
|
|
664
|
-
// DB unavailable; fall through to JSON files
|
|
665
|
-
}
|
|
666
|
-
// Fallback: scan ~/.local/share/opencode/storage/message/{sessionID}/msg_*.json
|
|
667
|
-
const messageDir = join(openCodeDir, 'storage', 'message');
|
|
668
|
-
try {
|
|
669
|
-
await stat(messageDir);
|
|
670
|
-
}
|
|
671
|
-
catch {
|
|
672
|
-
return [];
|
|
673
|
-
}
|
|
674
|
-
try {
|
|
675
|
-
const sessionDirs = await readdir(messageDir, { withFileTypes: true });
|
|
676
|
-
for (const sessionEntry of sessionDirs) {
|
|
677
|
-
if (!sessionEntry.isDirectory())
|
|
678
|
-
continue;
|
|
679
|
-
const msgFiles = await findFiles(join(messageDir, sessionEntry.name), '.json');
|
|
680
|
-
for (const filePath of msgFiles) {
|
|
681
|
-
try {
|
|
682
|
-
const parsed = JSON.parse(await readFile(filePath, 'utf8'));
|
|
683
|
-
if (parsed.role !== 'assistant')
|
|
684
|
-
continue;
|
|
685
|
-
const t = parsed.tokens;
|
|
686
|
-
if (!t || typeof t !== 'object')
|
|
687
|
-
continue;
|
|
688
|
-
const tokens = (t.input ?? 0) +
|
|
689
|
-
(t.output ?? 0) +
|
|
690
|
-
(t.cache?.read ?? 0) +
|
|
691
|
-
(t.cache?.write ?? 0) +
|
|
692
|
-
(t.reasoning ?? 0);
|
|
693
|
-
if (tokens <= 0)
|
|
694
|
-
continue;
|
|
695
|
-
const tsMs = parsed.time?.created ?? 0;
|
|
696
|
-
let date;
|
|
697
|
-
let hour = null;
|
|
698
|
-
if (tsMs > 0) {
|
|
699
|
-
const d = new Date(tsMs);
|
|
700
|
-
date = toDateStr(d);
|
|
701
|
-
hour = d.getHours();
|
|
702
|
-
}
|
|
703
|
-
else {
|
|
704
|
-
date = toDateStr((await stat(filePath)).mtime);
|
|
705
|
-
}
|
|
706
|
-
const model = String(parsed.modelID ?? '');
|
|
707
|
-
addToDaily(dailyByModel, date, model, tokens, 1);
|
|
708
|
-
if (model)
|
|
709
|
-
modelAccumulator.set(model, (modelAccumulator.get(model) ?? 0) + tokens);
|
|
710
|
-
if (hour !== null)
|
|
711
|
-
addHourly(date, hour, 'opencode', model, tokens, cutoff, 1);
|
|
712
|
-
}
|
|
713
|
-
catch {
|
|
714
|
-
// skip malformed files
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
}
|
|
92
|
+
const res = await fetchApi(session, '/api/cli/github-sync', {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { Authorization: `Bearer ${session.token}` },
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok)
|
|
97
|
+
return null;
|
|
98
|
+
return (await res.json());
|
|
718
99
|
}
|
|
719
100
|
catch {
|
|
720
|
-
|
|
101
|
+
return null;
|
|
721
102
|
}
|
|
722
|
-
return dailyMapToEntries(dailyByModel, 'opencode');
|
|
723
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Scan local AI usage (via the shared scanners) and upload it to the claim
|
|
106
|
+
* endpoint. Uses the exact same scan path as `join`, so the two commands report
|
|
107
|
+
* identical numbers for the same machine.
|
|
108
|
+
*/
|
|
724
109
|
export async function runSync(session) {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
scanOpenclaw(),
|
|
734
|
-
scanHermes(),
|
|
735
|
-
scanOpenCode(),
|
|
736
|
-
]);
|
|
737
|
-
const cursorEntries = cursorApiEntries.length > 0 ? cursorApiEntries : cursorLocal.entries;
|
|
738
|
-
const claudeTotal = claudeEntries.reduce((s, e) => s + e.tokens, 0);
|
|
739
|
-
const codexTotal = codexEntries.reduce((s, e) => s + e.tokens, 0);
|
|
740
|
-
const cursorTotal = cursorEntries.reduce((s, e) => s + e.tokens, 0);
|
|
741
|
-
const openclawTotal = openclawEntries.reduce((s, e) => s + e.tokens, 0);
|
|
742
|
-
const hermesTotal = hermesEntries.reduce((s, e) => s + e.tokens, 0);
|
|
743
|
-
const opencodeTotal = opencodeEntries.reduce((s, e) => s + e.tokens, 0);
|
|
744
|
-
const allEntries = [
|
|
745
|
-
...claudeEntries,
|
|
746
|
-
...codexEntries,
|
|
747
|
-
...cursorEntries,
|
|
748
|
-
...openclawEntries,
|
|
749
|
-
...hermesEntries,
|
|
750
|
-
...opencodeEntries,
|
|
751
|
-
];
|
|
110
|
+
const scan = await scanAllTools();
|
|
111
|
+
const claudeTotal = scan.toolTotals.claude_code ?? 0;
|
|
112
|
+
const codexTotal = scan.toolTotals.codex ?? 0;
|
|
113
|
+
const cursorTotal = scan.toolTotals.cursor ?? 0;
|
|
114
|
+
const openclawTotal = scan.toolTotals.openclaw ?? 0;
|
|
115
|
+
const hermesTotal = scan.toolTotals.hermes ?? 0;
|
|
116
|
+
const opencodeTotal = scan.toolTotals.opencode ?? 0;
|
|
117
|
+
const allEntries = scan.dailyTotals;
|
|
752
118
|
const messagesTotal = allEntries.reduce((s, e) => s + (e.messages ?? 0), 0);
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
modelTotals[name] = tokens;
|
|
757
|
-
}
|
|
758
|
-
const hourlyTotals = flushHourly();
|
|
119
|
+
const models = Object.entries(scan.modelTotals)
|
|
120
|
+
.map(([name, tokens]) => ({ name, tokens }))
|
|
121
|
+
.sort((a, b) => b.tokens - a.tokens);
|
|
759
122
|
const res = await fetchApi(session, '/api/claim/sync', {
|
|
760
123
|
method: 'POST',
|
|
761
124
|
headers: {
|
|
@@ -772,8 +135,8 @@ export async function runSync(session) {
|
|
|
772
135
|
hermes: hermesTotal,
|
|
773
136
|
opencode: opencodeTotal,
|
|
774
137
|
},
|
|
775
|
-
modelTotals,
|
|
776
|
-
hourlyTotals,
|
|
138
|
+
modelTotals: scan.modelTotals,
|
|
139
|
+
hourlyTotals: scan.hourlyTotals,
|
|
777
140
|
}),
|
|
778
141
|
});
|
|
779
142
|
if (res.status === 401) {
|
|
@@ -784,7 +147,7 @@ export async function runSync(session) {
|
|
|
784
147
|
throw new Error(data?.error ??
|
|
785
148
|
`sync failed (${res.status})`);
|
|
786
149
|
}
|
|
787
|
-
const result = await res.json();
|
|
150
|
+
const result = (await res.json());
|
|
788
151
|
return {
|
|
789
152
|
claudeTotal,
|
|
790
153
|
codexTotal,
|
|
@@ -794,12 +157,10 @@ export async function runSync(session) {
|
|
|
794
157
|
opencodeTotal,
|
|
795
158
|
messagesTotal,
|
|
796
159
|
allEntries,
|
|
797
|
-
cursorStats:
|
|
160
|
+
cursorStats: scan.cursorStats,
|
|
798
161
|
result,
|
|
799
|
-
models
|
|
800
|
-
|
|
801
|
-
.sort((a, b) => b.tokens - a.tokens),
|
|
802
|
-
cursorScanStatus: _cursorScanStatus.value,
|
|
162
|
+
models,
|
|
163
|
+
cursorScanStatus: scan.cursorScanStatus,
|
|
803
164
|
};
|
|
804
165
|
}
|
|
805
166
|
//# sourceMappingURL=sync.js.map
|