sigrank 0.16.0 → 0.16.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/adapters.mjs CHANGED
@@ -32,46 +32,65 @@
32
32
  * adapter object as `dataGap: string` so tokenpull() can surface them to the user.
33
33
  */
34
34
 
35
- import { readdir, readFile } from 'node:fs/promises'
36
- import { join, dirname } from 'node:path'
37
- import { homedir } from 'node:os'
38
- import { execFile as execFileCb } from 'node:child_process'
39
- import { promisify } from 'node:util'
35
+ import { readdir, readFile } from "node:fs/promises";
36
+ import { join, dirname } from "node:path";
37
+ import { homedir } from "node:os";
38
+ import { execFile as execFileCb } from "node:child_process";
39
+ import { promisify } from "node:util";
40
40
 
41
- const execFileP = promisify(execFileCb)
42
- const DAY_MS = 86_400_000 // shared with tokenpull.mjs but kept local to avoid circular import
41
+ const execFileP = promisify(execFileCb);
42
+ const DAY_MS = 86_400_000; // shared with tokenpull.mjs but kept local to avoid circular import
43
43
 
44
44
  // ── File-system helpers ───────────────────────────────────────────────────────
45
45
 
46
46
  /** Recursively yield every file whose name matches `pred` under dir (skips symlink dirs). */
47
47
  async function* walkFiles(dir, pred, counter = { n: 0 }, max = 10_000) {
48
- if (counter.n >= max) return
49
- let entries
50
- try { entries = await readdir(dir, { withFileTypes: true }) } catch { return }
48
+ if (counter.n >= max) return;
49
+ let entries;
50
+ try {
51
+ entries = await readdir(dir, { withFileTypes: true });
52
+ } catch {
53
+ return;
54
+ }
51
55
  for (const e of entries) {
52
- if (counter.n >= max) return
53
- const full = join(dir, e.name)
54
- if (e.isSymbolicLink()) continue
55
- if (e.isDirectory()) { yield* walkFiles(full, pred, counter, max) }
56
- else if (e.isFile() && pred(e.name)) { counter.n++; yield full }
56
+ if (counter.n >= max) return;
57
+ const full = join(dir, e.name);
58
+ if (e.isSymbolicLink()) continue;
59
+ if (e.isDirectory()) {
60
+ yield* walkFiles(full, pred, counter, max);
61
+ } else if (e.isFile() && pred(e.name)) {
62
+ counter.n++;
63
+ yield full;
64
+ }
57
65
  }
58
66
  }
59
67
 
60
- const isJsonl = (n) => n.endsWith('.jsonl') || n.endsWith('.jsonl.deleted') || n.match(/\.jsonl\.reset\.\d+$/)
61
- const isJson = (n) => n.endsWith('.json') && !n.endsWith('.jsonl')
68
+ const isJsonl = (n) =>
69
+ n.endsWith(".jsonl") ||
70
+ n.endsWith(".jsonl.deleted") ||
71
+ n.match(/\.jsonl\.reset\.\d+$/);
72
+ const isJson = (n) => n.endsWith(".json") && !n.endsWith(".jsonl");
62
73
 
63
74
  /** Read a file as UTF-8, return null on error. */
64
75
  async function readUtf8(path) {
65
- try { return await readFile(path, 'utf8') } catch { return null }
76
+ try {
77
+ return await readFile(path, "utf8");
78
+ } catch {
79
+ return null;
80
+ }
66
81
  }
67
82
 
68
83
  /** Parse each newline-delimited JSON line, yield parsed objects silently skipping bad lines. */
69
84
  function* parseJsonl(text, filePath) {
70
- if (!text) return
71
- for (const line of text.split('\n')) {
72
- const s = line.trim()
73
- if (!s) continue
74
- try { yield [JSON.parse(s), filePath] } catch { /* skip malformed */ }
85
+ if (!text) return;
86
+ for (const line of text.split("\n")) {
87
+ const s = line.trim();
88
+ if (!s) continue;
89
+ try {
90
+ yield [JSON.parse(s), filePath];
91
+ } catch {
92
+ /* skip malformed */
93
+ }
75
94
  }
76
95
  }
77
96
 
@@ -80,162 +99,236 @@ function* parseJsonl(text, filePath) {
80
99
  * (matches the execFile hardening used in tools.mjs / tokenpull.mjs). */
81
100
  async function sqliteJson(dbPath, sql) {
82
101
  try {
83
- const { stdout } = await execFileP('sqlite3', ['-json', dbPath, sql], { timeout: 10_000 })
84
- return JSON.parse(stdout || '[]')
85
- } catch { return [] }
102
+ const { stdout } = await execFileP("sqlite3", ["-json", dbPath, sql], {
103
+ timeout: 10_000,
104
+ });
105
+ return JSON.parse(stdout || "[]");
106
+ } catch {
107
+ return [];
108
+ }
86
109
  }
87
110
 
88
111
  // ── Env-var helper ────────────────────────────────────────────────────────────
89
112
  /** Resolve roots from env var or default. Supports comma-separated list. */
90
113
  function roots(envVar, defaultPath) {
91
- const v = process.env[envVar]
92
- if (v) return v.split(',').map((s) => s.trim()).filter(Boolean)
93
- return [defaultPath]
114
+ const v = process.env[envVar];
115
+ if (v)
116
+ return v
117
+ .split(",")
118
+ .map((s) => s.trim())
119
+ .filter(Boolean);
120
+ return [defaultPath];
94
121
  }
95
122
 
96
123
  // ── 1. Amp ────────────────────────────────────────────────────────────────────
97
124
  // ~/.local/share/amp/threads/**/*.json
98
125
  // Fields: assistant message usage: input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens
99
126
  export const ampAdapter = {
100
- platform: 'amp',
101
- defaultRoot: () => join(homedir(), '.local', 'share', 'amp'),
127
+ platform: "amp",
128
+ defaultRoot: () => join(homedir(), ".local", "share", "amp"),
102
129
  async *messages(root) {
103
- for (const r of roots('AMP_DATA_DIR', root)) {
104
- const threadsDir = join(r, 'threads')
130
+ for (const r of roots("AMP_DATA_DIR", root)) {
131
+ const threadsDir = join(r, "threads");
105
132
  for await (const path of walkFiles(threadsDir, isJson)) {
106
- const text = await readUtf8(path)
107
- if (!text) continue
108
- let thread
109
- try { thread = JSON.parse(text) } catch { continue }
133
+ const text = await readUtf8(path);
134
+ if (!text) continue;
135
+ let thread;
136
+ try {
137
+ thread = JSON.parse(text);
138
+ } catch {
139
+ continue;
140
+ }
110
141
  // Amp thread is an array or object with messages
111
- const msgs = Array.isArray(thread) ? thread : (thread.messages || [])
142
+ const msgs = Array.isArray(thread) ? thread : thread.messages || [];
112
143
  for (const msg of msgs) {
113
- if (!msg || msg.role !== 'assistant') continue
114
- const u = msg.usage || (msg.metadata && msg.metadata.usage) || {}
115
- const input = Number(u.input_tokens || u.inputTokens || 0)
116
- const output = Number(u.output_tokens || u.outputTokens || 0)
117
- const cacheCreate = Number(u.cache_creation_tokens || u.cacheCreationTokens || 0)
118
- const cacheRead = Number(u.cache_read_tokens || u.cacheReadTokens || 0)
119
- if (input + output + cacheCreate + cacheRead === 0) continue
120
- yield { id: msg.id || null, sid: thread.id || null, ts: msg.timestamp || msg.created_at || null, input, output, cacheCreate, cacheRead, file: path }
144
+ if (!msg || msg.role !== "assistant") continue;
145
+ const u = msg.usage || (msg.metadata && msg.metadata.usage) || {};
146
+ const input = Number(u.input_tokens || u.inputTokens || 0);
147
+ const output = Number(u.output_tokens || u.outputTokens || 0);
148
+ const cacheCreate = Number(
149
+ u.cache_creation_tokens || u.cacheCreationTokens || 0,
150
+ );
151
+ const cacheRead = Number(
152
+ u.cache_read_tokens || u.cacheReadTokens || 0,
153
+ );
154
+ if (input + output + cacheCreate + cacheRead === 0) continue;
155
+ yield {
156
+ id: msg.id || null,
157
+ sid: thread.id || null,
158
+ ts: msg.timestamp || msg.created_at || null,
159
+ input,
160
+ output,
161
+ cacheCreate,
162
+ cacheRead,
163
+ file: path,
164
+ };
121
165
  }
122
166
  }
123
167
  }
124
168
  },
125
- }
169
+ };
126
170
 
127
171
  // ── 2. Kimi ───────────────────────────────────────────────────────────────────
128
172
  // ~/.kimi/sessions/<group-id>/<session-id>/wire.jsonl
129
173
  // StatusUpdate lines only; token_usage: { input_other, output, input_cache_read, input_cache_creation }
130
174
  export const kimiAdapter = {
131
- platform: 'kimi',
132
- defaultRoot: () => join(homedir(), '.kimi'),
175
+ platform: "kimi",
176
+ defaultRoot: () => join(homedir(), ".kimi"),
133
177
  async *messages(root) {
134
- const sessionsDir = join(roots('KIMI_DATA_DIR', root)[0], 'sessions')
178
+ const sessionsDir = join(roots("KIMI_DATA_DIR", root)[0], "sessions");
135
179
  for await (const path of walkFiles(sessionsDir, isJsonl)) {
136
- const text = await readUtf8(path)
180
+ const text = await readUtf8(path);
137
181
  for (const [ev] of parseJsonl(text, path)) {
138
- if (!ev || ev.type !== 'StatusUpdate') continue
139
- const u = ev.token_usage || {}
140
- const input = Number(u.input_other || 0)
141
- const output = Number(u.output || 0)
142
- const cacheCreate = Number(u.input_cache_creation || 0)
143
- const cacheRead = Number(u.input_cache_read || 0)
144
- if (input + output + cacheCreate + cacheRead === 0) continue
145
- yield { id: ev.id || null, sid: null, ts: ev.timestamp || ev.created_at || null, input, output, cacheCreate, cacheRead, file: path }
182
+ if (!ev || ev.type !== "StatusUpdate") continue;
183
+ const u = ev.token_usage || {};
184
+ const input = Number(u.input_other || 0);
185
+ const output = Number(u.output || 0);
186
+ const cacheCreate = Number(u.input_cache_creation || 0);
187
+ const cacheRead = Number(u.input_cache_read || 0);
188
+ if (input + output + cacheCreate + cacheRead === 0) continue;
189
+ yield {
190
+ id: ev.id || null,
191
+ sid: null,
192
+ ts: ev.timestamp || ev.created_at || null,
193
+ input,
194
+ output,
195
+ cacheCreate,
196
+ cacheRead,
197
+ file: path,
198
+ };
146
199
  }
147
200
  }
148
201
  },
149
- }
202
+ };
150
203
 
151
204
  // ── 3. Qwen ───────────────────────────────────────────────────────────────────
152
205
  // ~/.qwen/projects/{project}/chats/*.jsonl
153
206
  // usageMetadata: { promptTokenCount, candidatesTokenCount, cachedContentTokenCount, thoughtsTokenCount }
154
207
  // No cache creation field. Reasoning (thoughtsTokenCount) → output.
155
208
  export const qwenAdapter = {
156
- platform: 'qwen',
209
+ platform: "qwen",
157
210
  estimated: true, // no cacheCreate in logs
158
- defaultRoot: () => join(homedir(), '.qwen'),
211
+ defaultRoot: () => join(homedir(), ".qwen"),
159
212
  async *messages(root) {
160
- const projectsDir = join(roots('QWEN_DATA_DIR', root)[0], 'projects')
213
+ const projectsDir = join(roots("QWEN_DATA_DIR", root)[0], "projects");
161
214
  for await (const path of walkFiles(projectsDir, isJsonl)) {
162
- const text = await readUtf8(path)
215
+ const text = await readUtf8(path);
163
216
  for (const [ev] of parseJsonl(text, path)) {
164
- if (!ev || !ev.usageMetadata) continue
165
- const u = ev.usageMetadata
166
- const rawInput = Number(u.promptTokenCount || 0)
167
- const rawOutput = Number(u.candidatesTokenCount || 0)
168
- const thoughts = Number(u.thoughtsTokenCount || 0) // reasoning → output
169
- const cacheRead = Number(u.cachedContentTokenCount || 0)
217
+ if (!ev || !ev.usageMetadata) continue;
218
+ const u = ev.usageMetadata;
219
+ const rawInput = Number(u.promptTokenCount || 0);
220
+ const rawOutput = Number(u.candidatesTokenCount || 0);
221
+ const thoughts = Number(u.thoughtsTokenCount || 0); // reasoning → output
222
+ const cacheRead = Number(u.cachedContentTokenCount || 0);
170
223
  // promptTokenCount is inclusive of cached; subtract to get fresh input
171
- const input = Math.max(0, rawInput - cacheRead)
172
- const output = rawOutput + thoughts
173
- if (input + output + cacheRead === 0) continue
174
- yield { id: ev.id || null, sid: null, ts: ev.timestamp || null, input, output, cacheCreate: 0, cacheRead, file: path }
224
+ const input = Math.max(0, rawInput - cacheRead);
225
+ const output = rawOutput + thoughts;
226
+ if (input + output + cacheRead === 0) continue;
227
+ yield {
228
+ id: ev.id || null,
229
+ sid: null,
230
+ ts: ev.timestamp || null,
231
+ input,
232
+ output,
233
+ cacheCreate: 0,
234
+ cacheRead,
235
+ file: path,
236
+ };
175
237
  }
176
238
  }
177
239
  },
178
- }
240
+ };
179
241
 
180
242
  // ── 4. pi-agent ───────────────────────────────────────────────────────────────
181
243
  // ~/.pi/agent/sessions/**/*.jsonl
182
244
  // Fields: inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens
183
245
  export const piAdapter = {
184
- platform: 'pi',
185
- defaultRoot: () => join(homedir(), '.pi', 'agent', 'sessions'),
246
+ platform: "pi",
247
+ defaultRoot: () => join(homedir(), ".pi", "agent", "sessions"),
186
248
  async *messages(root) {
187
- for (const r of roots('PI_AGENT_DIR', root)) {
249
+ for (const r of roots("PI_AGENT_DIR", root)) {
188
250
  for await (const path of walkFiles(r, isJsonl)) {
189
- const text = await readUtf8(path)
251
+ const text = await readUtf8(path);
190
252
  for (const [ev] of parseJsonl(text, path)) {
191
- if (!ev) continue
253
+ if (!ev) continue;
192
254
  // pi-agent stores usage in assistant messages or usage events
193
- const u = ev.usage || ev
194
- const input = Number(u.inputTokens || u.input_tokens || 0)
195
- const output = Number(u.outputTokens || u.output_tokens || 0)
196
- const cacheCreate = Number(u.cacheCreationTokens || u.cache_creation_tokens || 0)
197
- const cacheRead = Number(u.cacheReadTokens || u.cache_read_tokens || 0)
198
- if (input + output + cacheCreate + cacheRead === 0) continue
199
- yield { id: ev.id || null, sid: ev.sessionId || null, ts: ev.timestamp || null, input, output, cacheCreate, cacheRead, file: path }
255
+ const u = ev.usage || ev;
256
+ const input = Number(u.inputTokens || u.input_tokens || 0);
257
+ const output = Number(u.outputTokens || u.output_tokens || 0);
258
+ const cacheCreate = Number(
259
+ u.cacheCreationTokens || u.cache_creation_tokens || 0,
260
+ );
261
+ const cacheRead = Number(
262
+ u.cacheReadTokens || u.cache_read_tokens || 0,
263
+ );
264
+ if (input + output + cacheCreate + cacheRead === 0) continue;
265
+ yield {
266
+ id: ev.id || null,
267
+ sid: ev.sessionId || null,
268
+ ts: ev.timestamp || null,
269
+ input,
270
+ output,
271
+ cacheCreate,
272
+ cacheRead,
273
+ file: path,
274
+ };
200
275
  }
201
276
  }
202
277
  }
203
278
  },
204
- }
279
+ };
205
280
 
206
281
  // ── 5. OpenClaw ───────────────────────────────────────────────────────────────
207
282
  // ~/.openclaw/ (also ~/.clawdbot/, ~/.moltbot/, ~/.moldbot/)
208
283
  // agents/<agentId>/sessions/<uuid>.jsonl (+ .deleted.<ts> and .reset.<ts> variants)
209
284
  // Per-message: input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens
210
285
  export const openclawAdapter = {
211
- platform: 'openclaw',
286
+ platform: "openclaw",
212
287
  defaultRoot: () => {
213
- const env = process.env.OPENCLAW_DIR
214
- if (env) return env.split(',')[0].trim()
215
- return join(homedir(), '.openclaw')
288
+ const env = process.env.OPENCLAW_DIR;
289
+ if (env) return env.split(",")[0].trim();
290
+ return join(homedir(), ".openclaw");
216
291
  },
217
292
  async *messages(root) {
218
293
  const dirs = process.env.OPENCLAW_DIR
219
- ? process.env.OPENCLAW_DIR.split(',').map((s) => s.trim())
220
- : [join(homedir(), '.openclaw'), join(homedir(), '.clawdbot'), join(homedir(), '.moltbot'), join(homedir(), '.moldbot')]
221
- const pred = (n) => isJsonl(n) || n.endsWith('.json') // covers archived variants
294
+ ? process.env.OPENCLAW_DIR.split(",").map((s) => s.trim())
295
+ : [
296
+ join(homedir(), ".openclaw"),
297
+ join(homedir(), ".clawdbot"),
298
+ join(homedir(), ".moltbot"),
299
+ join(homedir(), ".moldbot"),
300
+ ];
301
+ const pred = (n) => isJsonl(n) || n.endsWith(".json"); // covers archived variants
222
302
  for (const dir of dirs) {
223
303
  for await (const path of walkFiles(dir, pred)) {
224
- const text = await readUtf8(path)
304
+ const text = await readUtf8(path);
225
305
  for (const [ev] of parseJsonl(text, path)) {
226
- if (!ev || ev.role !== 'assistant') continue
227
- const u = ev.usage || ev.tokens || {}
228
- const input = Number(u.input_tokens || u.inputTokens || 0)
229
- const output = Number(u.output_tokens || u.outputTokens || 0)
230
- const cacheCreate = Number(u.cache_creation_tokens || u.cacheCreationTokens || 0)
231
- const cacheRead = Number(u.cache_read_tokens || u.cacheReadTokens || 0)
232
- if (input + output + cacheCreate + cacheRead === 0) continue
233
- yield { id: ev.id || null, sid: null, ts: ev.timestamp || null, input, output, cacheCreate, cacheRead, file: path }
306
+ if (!ev || ev.role !== "assistant") continue;
307
+ const u = ev.usage || ev.tokens || {};
308
+ const input = Number(u.input_tokens || u.inputTokens || 0);
309
+ const output = Number(u.output_tokens || u.outputTokens || 0);
310
+ const cacheCreate = Number(
311
+ u.cache_creation_tokens || u.cacheCreationTokens || 0,
312
+ );
313
+ const cacheRead = Number(
314
+ u.cache_read_tokens || u.cacheReadTokens || 0,
315
+ );
316
+ if (input + output + cacheCreate + cacheRead === 0) continue;
317
+ yield {
318
+ id: ev.id || null,
319
+ sid: null,
320
+ ts: ev.timestamp || null,
321
+ input,
322
+ output,
323
+ cacheCreate,
324
+ cacheRead,
325
+ file: path,
326
+ };
234
327
  }
235
328
  }
236
329
  }
237
330
  },
238
- }
331
+ };
239
332
 
240
333
  // ── 6. Droid ──────────────────────────────────────────────────────────────────
241
334
  // ~/.factory/sessions/**/*.settings.json
@@ -245,58 +338,101 @@ export const openclawAdapter = {
245
338
  // appears in multiple files, summing double-counts. We set both id and sid to the session id
246
339
  // so tokenpull()'s (sid|id) keep-last dedup collapses duplicates to the latest values.
247
340
  export const droidAdapter = {
248
- platform: 'droid',
249
- defaultRoot: () => join(homedir(), '.factory', 'sessions'),
341
+ platform: "droid",
342
+ defaultRoot: () => join(homedir(), ".factory", "sessions"),
250
343
  async *messages(root) {
251
- for (const r of roots('DROID_SESSIONS_DIR', root)) {
252
- for await (const path of walkFiles(r, (n) => n.endsWith('.settings.json'))) {
253
- const text = await readUtf8(path)
254
- if (!text) continue
255
- let s
256
- try { s = JSON.parse(text) } catch { continue }
257
- const input = Number(s.input_tokens || 0)
258
- const output = Number(s.output_tokens || 0) + Number(s.thinking_tokens || 0)
259
- const cacheCreate = Number(s.cache_creation_tokens || 0)
260
- const cacheRead = Number(s.cache_read_tokens || 0)
261
- if (input + output + cacheCreate + cacheRead === 0) continue
262
- const sessionId = String(s.session_id || s.id || '')
263
- yield { id: sessionId, sid: sessionId, ts: s.updated_at || s.created_at || null, input, output, cacheCreate, cacheRead, file: path }
344
+ for (const r of roots("DROID_SESSIONS_DIR", root)) {
345
+ for await (const path of walkFiles(r, (n) =>
346
+ n.endsWith(".settings.json"),
347
+ )) {
348
+ const text = await readUtf8(path);
349
+ if (!text) continue;
350
+ let s;
351
+ try {
352
+ s = JSON.parse(text);
353
+ } catch {
354
+ continue;
355
+ }
356
+ const input = Number(s.input_tokens || 0);
357
+ const output =
358
+ Number(s.output_tokens || 0) + Number(s.thinking_tokens || 0);
359
+ const cacheCreate = Number(s.cache_creation_tokens || 0);
360
+ const cacheRead = Number(s.cache_read_tokens || 0);
361
+ if (input + output + cacheCreate + cacheRead === 0) continue;
362
+ const sessionId = String(s.session_id || s.id || "");
363
+ yield {
364
+ id: sessionId,
365
+ sid: sessionId,
366
+ ts: s.updated_at || s.created_at || null,
367
+ input,
368
+ output,
369
+ cacheCreate,
370
+ cacheRead,
371
+ file: path,
372
+ };
264
373
  }
265
374
  }
266
375
  },
267
- }
376
+ };
268
377
 
269
378
  // ── 7. Codebuff ───────────────────────────────────────────────────────────────
270
379
  // ~/.config/manicode/projects/<project>/chats/<chat-id>/chat-messages.json
271
380
  // assistant messages: metadata.usage or metadata.codebuff.usage
272
381
  export const codebuffAdapter = {
273
- platform: 'codebuff',
274
- defaultRoot: () => join(homedir(), '.config', 'manicode'),
382
+ platform: "codebuff",
383
+ defaultRoot: () => join(homedir(), ".config", "manicode"),
275
384
  async *messages(root) {
276
385
  const dirs = process.env.CODEBUFF_DATA_DIR
277
- ? process.env.CODEBUFF_DATA_DIR.split(',').map((s) => s.trim())
278
- : [join(homedir(), '.config', 'manicode'), join(homedir(), '.config', 'manicode-dev'), join(homedir(), '.config', 'manicode-staging')]
386
+ ? process.env.CODEBUFF_DATA_DIR.split(",").map((s) => s.trim())
387
+ : [
388
+ join(homedir(), ".config", "manicode"),
389
+ join(homedir(), ".config", "manicode-dev"),
390
+ join(homedir(), ".config", "manicode-staging"),
391
+ ];
279
392
  for (const dir of dirs) {
280
- for await (const path of walkFiles(dir, (n) => n === 'chat-messages.json')) {
281
- const text = await readUtf8(path)
282
- if (!text) continue
283
- let msgs
284
- try { msgs = JSON.parse(text) } catch { continue }
285
- if (!Array.isArray(msgs)) continue
393
+ for await (const path of walkFiles(
394
+ dir,
395
+ (n) => n === "chat-messages.json",
396
+ )) {
397
+ const text = await readUtf8(path);
398
+ if (!text) continue;
399
+ let msgs;
400
+ try {
401
+ msgs = JSON.parse(text);
402
+ } catch {
403
+ continue;
404
+ }
405
+ if (!Array.isArray(msgs)) continue;
286
406
  for (const msg of msgs) {
287
- if (!msg || msg.role !== 'assistant') continue
288
- const u = (msg.metadata && (msg.metadata.usage || msg.metadata.codebuff?.usage)) || {}
289
- const input = Number(u.input_tokens || u.inputTokens || 0)
290
- const output = Number(u.output_tokens || u.outputTokens || 0)
291
- const cacheCreate = Number(u.cache_creation_tokens || u.cacheCreationTokens || 0)
292
- const cacheRead = Number(u.cache_read_tokens || u.cacheReadTokens || 0)
293
- if (input + output + cacheCreate + cacheRead === 0) continue
294
- yield { id: msg.id || null, sid: null, ts: msg.timestamp || msg.created_at || null, input, output, cacheCreate, cacheRead, file: path }
407
+ if (!msg || msg.role !== "assistant") continue;
408
+ const u =
409
+ (msg.metadata &&
410
+ (msg.metadata.usage || msg.metadata.codebuff?.usage)) ||
411
+ {};
412
+ const input = Number(u.input_tokens || u.inputTokens || 0);
413
+ const output = Number(u.output_tokens || u.outputTokens || 0);
414
+ const cacheCreate = Number(
415
+ u.cache_creation_tokens || u.cacheCreationTokens || 0,
416
+ );
417
+ const cacheRead = Number(
418
+ u.cache_read_tokens || u.cacheReadTokens || 0,
419
+ );
420
+ if (input + output + cacheCreate + cacheRead === 0) continue;
421
+ yield {
422
+ id: msg.id || null,
423
+ sid: null,
424
+ ts: msg.timestamp || msg.created_at || null,
425
+ input,
426
+ output,
427
+ cacheCreate,
428
+ cacheRead,
429
+ file: path,
430
+ };
295
431
  }
296
432
  }
297
433
  }
298
434
  },
299
- }
435
+ };
300
436
 
301
437
  // ── 8. Gemini CLI ─────────────────────────────────────────────────────────────
302
438
  // ~/.gemini/tmp/*/chats/*.json and *.jsonl
@@ -304,84 +440,135 @@ export const codebuffAdapter = {
304
440
  // SigRank mapping: input = input−cached (fresh), cacheRead = cached, cacheCreate = 0 (not exposed),
305
441
  // output = output + thought (reasoning→output)
306
442
  export const geminiAdapter = {
307
- platform: 'gemini',
443
+ platform: "gemini",
308
444
  estimated: true, // no cacheCreate field in Gemini logs
309
- defaultRoot: () => join(homedir(), '.gemini', 'tmp'),
445
+ defaultRoot: () => join(homedir(), ".gemini", "tmp"),
310
446
  async *messages(root) {
311
- for (const r of roots('GEMINI_DATA_DIR', root)) {
312
- for await (const path of walkFiles(r, (n) => n.endsWith('.json') || n.endsWith('.jsonl'))) {
313
- const text = await readUtf8(path)
314
- if (!text) continue
447
+ for (const r of roots("GEMINI_DATA_DIR", root)) {
448
+ for await (const path of walkFiles(
449
+ r,
450
+ (n) => n.endsWith(".json") || n.endsWith(".jsonl"),
451
+ )) {
452
+ const text = await readUtf8(path);
453
+ if (!text) continue;
315
454
  // Try JSONL first, then single JSON
316
- let parsed = []
455
+ let parsed = [];
317
456
  try {
318
- parsed = text.trim().split('\n').filter(Boolean).map((l) => JSON.parse(l))
457
+ parsed = text
458
+ .trim()
459
+ .split("\n")
460
+ .filter(Boolean)
461
+ .map((l) => JSON.parse(l));
319
462
  } catch {
320
- try { parsed = [JSON.parse(text)] } catch { continue }
463
+ try {
464
+ parsed = [JSON.parse(text)];
465
+ } catch {
466
+ continue;
467
+ }
321
468
  }
322
469
  for (const ev of parsed) {
323
- if (!ev) continue
470
+ if (!ev) continue;
324
471
  // Gemini usage may be at top level or nested in usageMetadata
325
- const u = ev.usageMetadata || ev.usage || ev
326
- const rawInput = Number(u.input || u.promptTokenCount || 0)
327
- const rawOutput = Number(u.output || u.candidatesTokenCount || 0)
328
- const cached = Number(u.cached || u.cachedContentTokenCount || 0)
329
- const thought = Number(u.thought || u.thoughtsTokenCount || 0)
330
- if (rawInput + rawOutput + cached + thought === 0) continue
331
- const input = Math.max(0, rawInput - cached) // strip cached from input
332
- const output = rawOutput + thought // reasoning → output
333
- yield { id: ev.id || null, sid: null, ts: ev.timestamp || null, input, output, cacheCreate: 0, cacheRead: cached, file: path }
472
+ const u = ev.usageMetadata || ev.usage || ev;
473
+ const rawInput = Number(u.input || u.promptTokenCount || 0);
474
+ const rawOutput = Number(u.output || u.candidatesTokenCount || 0);
475
+ const cached = Number(u.cached || u.cachedContentTokenCount || 0);
476
+ const thought = Number(u.thought || u.thoughtsTokenCount || 0);
477
+ if (rawInput + rawOutput + cached + thought === 0) continue;
478
+ const input = Math.max(0, rawInput - cached); // strip cached from input
479
+ const output = rawOutput + thought; // reasoning → output
480
+ yield {
481
+ id: ev.id || null,
482
+ sid: null,
483
+ ts: ev.timestamp || null,
484
+ input,
485
+ output,
486
+ cacheCreate: 0,
487
+ cacheRead: cached,
488
+ file: path,
489
+ };
334
490
  }
335
491
  }
336
492
  }
337
493
  },
338
- }
494
+ };
339
495
 
340
496
  // ── 9. GitHub Copilot CLI ─────────────────────────────────────────────────────
341
497
  // ~/.copilot/otel/*.jsonl (requires COPILOT_OTEL_ENABLED=true before session start)
342
498
  // OpenTelemetry spans; looks for llm.token_count.prompt / completion / cached attributes
343
499
  export const copilotAdapter = {
344
- platform: 'copilot',
345
- defaultRoot: () => join(homedir(), '.copilot', 'otel'),
500
+ platform: "copilot",
501
+ defaultRoot: () => join(homedir(), ".copilot", "otel"),
346
502
  async *messages(root) {
347
503
  const dir = process.env.COPILOT_OTEL_FILE_EXPORTER_PATH
348
504
  ? dirname(process.env.COPILOT_OTEL_FILE_EXPORTER_PATH)
349
- : root
505
+ : root;
350
506
  for await (const path of walkFiles(dir, isJsonl)) {
351
- const text = await readUtf8(path)
507
+ const text = await readUtf8(path);
352
508
  for (const [ev] of parseJsonl(text, path)) {
353
- if (!ev) continue
509
+ if (!ev) continue;
354
510
  // OTel span: attributes may be an object or array of {key,value} pairs
355
- const attrs = ev.attributes || ev.resource?.attributes || {}
511
+ const attrs = ev.attributes || ev.resource?.attributes || {};
356
512
  const get = (k) => {
357
- if (typeof attrs === 'object' && !Array.isArray(attrs)) return attrs[k]
358
- if (Array.isArray(attrs)) { const a = attrs.find((x) => x.key === k); return a?.value?.intValue ?? a?.value?.stringValue ?? null }
359
- return null
360
- }
361
- const input = Number(get('llm.token_count.prompt') || get('gen_ai.usage.input_tokens') || 0)
362
- const output = Number(get('llm.token_count.completion') || get('gen_ai.usage.output_tokens') || 0)
363
- const cacheCreate = Number(get('llm.token_count.cache_creation') || 0)
364
- const cacheRead = Number(get('llm.token_count.cache_read') || get('gen_ai.usage.cache_read_input_tokens') || 0)
365
- if (input + output + cacheCreate + cacheRead === 0) continue
366
- yield { id: ev.traceId || ev.spanId || null, sid: null, ts: ev.startTimeUnixNano ? new Date(Number(ev.startTimeUnixNano) / 1e6).toISOString() : null, input, output, cacheCreate, cacheRead, file: path }
513
+ if (typeof attrs === "object" && !Array.isArray(attrs))
514
+ return attrs[k];
515
+ if (Array.isArray(attrs)) {
516
+ const a = attrs.find((x) => x.key === k);
517
+ return a?.value?.intValue ?? a?.value?.stringValue ?? null;
518
+ }
519
+ return null;
520
+ };
521
+ const input = Number(
522
+ get("llm.token_count.prompt") ||
523
+ get("gen_ai.usage.input_tokens") ||
524
+ 0,
525
+ );
526
+ const output = Number(
527
+ get("llm.token_count.completion") ||
528
+ get("gen_ai.usage.output_tokens") ||
529
+ 0,
530
+ );
531
+ const cacheCreate = Number(get("llm.token_count.cache_creation") || 0);
532
+ const cacheRead = Number(
533
+ get("llm.token_count.cache_read") ||
534
+ get("gen_ai.usage.cache_read_input_tokens") ||
535
+ 0,
536
+ );
537
+ if (input + output + cacheCreate + cacheRead === 0) continue;
538
+ yield {
539
+ id: ev.traceId || ev.spanId || null,
540
+ sid: null,
541
+ ts: ev.startTimeUnixNano
542
+ ? new Date(Number(ev.startTimeUnixNano) / 1e6).toISOString()
543
+ : null,
544
+ input,
545
+ output,
546
+ cacheCreate,
547
+ cacheRead,
548
+ file: path,
549
+ };
367
550
  }
368
551
  }
369
552
  },
370
- setupNote: 'Requires COPILOT_OTEL_ENABLED=true and COPILOT_OTEL_EXPORTER_TYPE=file set BEFORE starting the Copilot session. Without this, no local token logs are written.',
371
- }
553
+ setupNote:
554
+ "Requires COPILOT_OTEL_ENABLED=true and COPILOT_OTEL_EXPORTER_TYPE=file set BEFORE starting the Copilot session. Without this, no local token logs are written.",
555
+ };
372
556
 
373
557
  // ── 10. OpenCode ──────────────────────────────────────────────────────────────
374
558
  // ~/.local/share/opencode — JSON message files, but costs stored as 0 and token fields
375
559
  // are calculated via LiteLLM pricing (not stored in the log). No raw token fields.
376
560
  // SigRank cannot derive pillars from OpenCode logs with current log format.
377
561
  export const opencodeAdapter = {
378
- platform: 'opencode',
379
- defaultRoot: () => join(homedir(), '.local', 'share', 'opencode'),
562
+ platform: "opencode",
563
+ defaultRoot: () => join(homedir(), ".local", "share", "opencode"),
380
564
  estimated: true,
381
- dataGap: 'OpenCode logs store cost:0 and derive tokens via LiteLLM at runtime — raw token counts are not persisted. SigRank cannot read pillars from OpenCode logs with the current format. Track https://github.com/ccusage/ccusage for format changes.',
565
+ dataGap:
566
+ "OpenCode logs store cost:0 and derive tokens via LiteLLM at runtime — raw token counts are not persisted. SigRank cannot read pillars from OpenCode logs with the current format. Track https://github.com/ccusage/ccusage for format changes.",
382
567
  // eslint-disable-next-line require-yield
383
- async *messages() { /* no data available */ },
384
- }
568
+ async *messages() {
569
+ /* no data available */
570
+ },
571
+ };
385
572
 
386
573
  // ── 11. Goose ─────────────────────────────────────────────────────────────────
387
574
  // SQLite: sessions.db at standard Goose data roots or $GOOSE_PATH_ROOT/data/sessions/sessions.db
@@ -392,58 +579,114 @@ export const opencodeAdapter = {
392
579
  // and id to the session id and ORDER BY updated_at so tokenpull()'s (sid|id) keep-last dedup
393
580
  // collapses duplicate session rows to the latest (max accumulated) values.
394
581
  export const gooseAdapter = {
395
- platform: 'goose',
582
+ platform: "goose",
396
583
  estimated: true, // no cacheCreate or cacheRead
397
584
  defaultRoot: () => {
398
- const env = process.env.GOOSE_PATH_ROOT
399
- if (env) return env
585
+ const env = process.env.GOOSE_PATH_ROOT;
586
+ if (env) return env;
400
587
  // Standard locations (macOS first, then XDG)
401
- return join(homedir(), 'Library', 'Application Support', 'goose')
588
+ return join(homedir(), "Library", "Application Support", "goose");
402
589
  },
403
590
  async *messages(root) {
404
591
  const dbCandidates = process.env.GOOSE_PATH_ROOT
405
- ? [join(process.env.GOOSE_PATH_ROOT, 'data', 'sessions', 'sessions.db')]
592
+ ? [join(process.env.GOOSE_PATH_ROOT, "data", "sessions", "sessions.db")]
406
593
  : [
407
- join(homedir(), 'Library', 'Application Support', 'goose', 'sessions', 'sessions.db'),
408
- join(homedir(), '.local', 'share', 'goose', 'sessions', 'sessions.db'),
409
- join(homedir(), '.local', 'share', 'Block', 'goose', 'sessions', 'sessions.db'),
410
- ]
594
+ join(
595
+ homedir(),
596
+ "Library",
597
+ "Application Support",
598
+ "goose",
599
+ "sessions",
600
+ "sessions.db",
601
+ ),
602
+ join(
603
+ homedir(),
604
+ ".local",
605
+ "share",
606
+ "goose",
607
+ "sessions",
608
+ "sessions.db",
609
+ ),
610
+ join(
611
+ homedir(),
612
+ ".local",
613
+ "share",
614
+ "Block",
615
+ "goose",
616
+ "sessions",
617
+ "sessions.db",
618
+ ),
619
+ ];
411
620
  for (const db of dbCandidates) {
412
621
  // ORDER BY updated_at so the latest cumulative row per session is yielded last
413
622
  // (tokenpull keep-last dedup picks the final row = max accumulated values).
414
- const rows = await sqliteJson(db, 'SELECT * FROM sessions ORDER BY updated_at')
623
+ const rows = await sqliteJson(
624
+ db,
625
+ "SELECT * FROM sessions ORDER BY updated_at",
626
+ );
415
627
  for (const row of rows) {
416
- const input = Number(row.accumulated_input_tokens || row.input_tokens || 0)
417
- const output = Number(row.accumulated_output_tokens || row.output_tokens || 0)
418
- const total = Number(row.accumulated_total_tokens || row.total_tokens || 0)
419
- const reasoning = Math.max(0, total - input - output) // folded into output
420
- if (input + output === 0) continue
421
- const sessionId = String(row.id || row.session_id || '')
422
- yield { id: sessionId, sid: sessionId, ts: row.created_at || row.updated_at || null, input, output: output + reasoning, cacheCreate: 0, cacheRead: 0, file: db }
628
+ const input = Number(
629
+ row.accumulated_input_tokens || row.input_tokens || 0,
630
+ );
631
+ const output = Number(
632
+ row.accumulated_output_tokens || row.output_tokens || 0,
633
+ );
634
+ const total = Number(
635
+ row.accumulated_total_tokens || row.total_tokens || 0,
636
+ );
637
+ const reasoning = Math.max(0, total - input - output); // folded into output
638
+ if (input + output === 0) continue;
639
+ const sessionId = String(row.id || row.session_id || "");
640
+ yield {
641
+ id: sessionId,
642
+ sid: sessionId,
643
+ ts: row.created_at || row.updated_at || null,
644
+ input,
645
+ output: output + reasoning,
646
+ cacheCreate: 0,
647
+ cacheRead: 0,
648
+ file: db,
649
+ };
423
650
  }
424
651
  }
425
652
  },
426
- }
653
+ };
427
654
 
428
655
  // ── 12. Kilo ──────────────────────────────────────────────────────────────────
429
656
  // SQLite: ~/.local/share/kilo/kilo.db
430
657
  // Per-message rows with model, input/output/cache token columns.
431
658
  export const kiloAdapter = {
432
- platform: 'kilo',
433
- defaultRoot: () => join(homedir(), '.local', 'share', 'kilo'),
659
+ platform: "kilo",
660
+ defaultRoot: () => join(homedir(), ".local", "share", "kilo"),
434
661
  async *messages(root) {
435
- const dbPath = join(roots('KILO_DATA_DIR', root)[0], 'kilo.db')
436
- const rows = await sqliteJson(dbPath, 'SELECT * FROM messages WHERE role="assistant"')
662
+ const dbPath = join(roots("KILO_DATA_DIR", root)[0], "kilo.db");
663
+ const rows = await sqliteJson(
664
+ dbPath,
665
+ 'SELECT * FROM messages WHERE role="assistant"',
666
+ );
437
667
  for (const row of rows) {
438
- const input = Number(row.input_tokens || row.inputTokens || 0)
439
- const output = Number(row.output_tokens || row.outputTokens || 0)
440
- const cacheCreate = Number(row.cache_creation_tokens || row.cacheCreationTokens || 0)
441
- const cacheRead = Number(row.cache_read_tokens || row.cacheReadTokens || 0)
442
- if (input + output + cacheCreate + cacheRead === 0) continue
443
- yield { id: String(row.id || ''), sid: String(row.session_id || row.sessionId || ''), ts: row.created_at || row.timestamp || null, input, output, cacheCreate, cacheRead, file: dbPath }
668
+ const input = Number(row.input_tokens || row.inputTokens || 0);
669
+ const output = Number(row.output_tokens || row.outputTokens || 0);
670
+ const cacheCreate = Number(
671
+ row.cache_creation_tokens || row.cacheCreationTokens || 0,
672
+ );
673
+ const cacheRead = Number(
674
+ row.cache_read_tokens || row.cacheReadTokens || 0,
675
+ );
676
+ if (input + output + cacheCreate + cacheRead === 0) continue;
677
+ yield {
678
+ id: String(row.id || ""),
679
+ sid: String(row.session_id || row.sessionId || ""),
680
+ ts: row.created_at || row.timestamp || null,
681
+ input,
682
+ output,
683
+ cacheCreate,
684
+ cacheRead,
685
+ file: dbPath,
686
+ };
444
687
  }
445
688
  },
446
- }
689
+ };
447
690
 
448
691
  // ── 13. Hermes Agent ─────────────────────────────────────────────────────────
449
692
  // SQLite: ~/.hermes/state.db
@@ -453,43 +696,55 @@ export const kiloAdapter = {
453
696
  // and ORDER BY updated_at so tokenpull()'s (sid|id) keep-last dedup collapses duplicates
454
697
  // to the latest values.
455
698
  export const hermesAdapter = {
456
- platform: 'hermes',
457
- defaultRoot: () => join(homedir(), '.hermes'),
699
+ platform: "hermes",
700
+ defaultRoot: () => join(homedir(), ".hermes"),
458
701
  async *messages(root) {
459
- for (const r of roots('HERMES_HOME', root)) {
460
- const dbPath = join(r, 'state.db')
702
+ for (const r of roots("HERMES_HOME", root)) {
703
+ const dbPath = join(r, "state.db");
461
704
  // ORDER BY updated_at so the latest cumulative row per session is yielded last.
462
- const rows = await sqliteJson(dbPath, 'SELECT * FROM sessions ORDER BY updated_at')
705
+ const rows = await sqliteJson(
706
+ dbPath,
707
+ "SELECT * FROM sessions ORDER BY updated_at",
708
+ );
463
709
  for (const row of rows) {
464
- const input = Number(row.input || 0)
465
- const reasoning = Number(row.reasoning_tokens || 0)
466
- const output = Number(row.output || 0) + reasoning
467
- const cacheCreate = Number(row.cache_write || row.cache_creation || 0)
468
- const cacheRead = Number(row.cache_read || 0)
469
- if (input + output + cacheCreate + cacheRead === 0) continue
470
- const sessionId = String(row.id || row.session_id || '')
471
- yield { id: sessionId, sid: sessionId, ts: row.created_at || row.updated_at || null, input, output, cacheCreate, cacheRead, file: dbPath }
710
+ const input = Number(row.input || 0);
711
+ const reasoning = Number(row.reasoning_tokens || 0);
712
+ const output = Number(row.output || 0) + reasoning;
713
+ const cacheCreate = Number(row.cache_write || row.cache_creation || 0);
714
+ const cacheRead = Number(row.cache_read || 0);
715
+ if (input + output + cacheCreate + cacheRead === 0) continue;
716
+ const sessionId = String(row.id || row.session_id || "");
717
+ yield {
718
+ id: sessionId,
719
+ sid: sessionId,
720
+ ts: row.created_at || row.updated_at || null,
721
+ input,
722
+ output,
723
+ cacheCreate,
724
+ cacheRead,
725
+ file: dbPath,
726
+ };
472
727
  }
473
728
  }
474
729
  },
475
- }
730
+ };
476
731
 
477
732
  // ── Registry ──────────────────────────────────────────────────────────────────
478
733
  /** All non-Claude, non-Codex adapters keyed by platform ID. */
479
734
  export const ADAPTERS = {
480
- amp: ampAdapter,
481
- kimi: kimiAdapter,
482
- qwen: qwenAdapter,
483
- pi: piAdapter,
735
+ amp: ampAdapter,
736
+ kimi: kimiAdapter,
737
+ qwen: qwenAdapter,
738
+ pi: piAdapter,
484
739
  openclaw: openclawAdapter,
485
- droid: droidAdapter,
740
+ droid: droidAdapter,
486
741
  codebuff: codebuffAdapter,
487
- gemini: geminiAdapter,
488
- copilot: copilotAdapter,
742
+ gemini: geminiAdapter,
743
+ copilot: copilotAdapter,
489
744
  opencode: opencodeAdapter,
490
- goose: gooseAdapter,
491
- kilo: kiloAdapter,
492
- hermes: hermesAdapter,
493
- }
745
+ goose: gooseAdapter,
746
+ kilo: kiloAdapter,
747
+ hermes: hermesAdapter,
748
+ };
494
749
 
495
- export const ALL_PLATFORMS = Object.keys(ADAPTERS).concat(['claude', 'codex'])
750
+ export const ALL_PLATFORMS = Object.keys(ADAPTERS).concat(["claude", "codex"]);