sigrank 0.19.0 → 1.0.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/MCP_SERVER_DESCRIPTION.md +172 -0
  3. package/README.md +89 -41
  4. package/adapters/index.mjs +1100 -0
  5. package/adapters/tokenpull.mjs +568 -0
  6. package/adapters.mjs +1 -865
  7. package/analytics/cascade.mjs +338 -0
  8. package/cascade.mjs +1 -274
  9. package/cli.mjs +1 -1853
  10. package/connect.mjs +1 -13
  11. package/identity/connect.mjs +13 -0
  12. package/identity/keystore.mjs +231 -0
  13. package/identity/sign.mjs +106 -0
  14. package/index.mjs +47 -162
  15. package/keystore.mjs +1 -242
  16. package/lib/constants.mjs +137 -0
  17. package/lib/pkg-version.mjs +59 -0
  18. package/manifest.json +46 -0
  19. package/narrate.mjs +1 -77
  20. package/package.json +41 -16
  21. package/preflight.mjs +1 -191
  22. package/presentation/cli.mjs +1926 -0
  23. package/presentation/narrate.mjs +219 -0
  24. package/presentation/tui-themes.mjs +189 -0
  25. package/presentation/tui.mjs +3316 -0
  26. package/prompts/check-my-efficiency.md +1 -0
  27. package/prompts/compare-with-leader.md +1 -0
  28. package/prompts/simulate-improvement.md +1 -0
  29. package/proxy.mjs +492 -0
  30. package/resources/class-tiers.md +81 -0
  31. package/resources/data-policy.md +31 -0
  32. package/resources/install-guide.md +59 -0
  33. package/resources/privacy-model.md +44 -0
  34. package/resources/scoring-formula.md +24 -0
  35. package/review.mjs +263 -0
  36. package/sign.mjs +1 -106
  37. package/submit/index.mjs +297 -0
  38. package/submit/preflight.mjs +191 -0
  39. package/submit.mjs +1 -303
  40. package/tokenpull.mjs +1 -576
  41. package/tokscale_analytics.mjs +864 -0
  42. package/tools/_framing.mjs +217 -0
  43. package/tools/_helpers.mjs +253 -0
  44. package/tools/_peer-matching.mjs +361 -0
  45. package/tools/_report.mjs +262 -0
  46. package/tools/_schemas.mjs +492 -0
  47. package/tools/_verifiers.mjs +148 -0
  48. package/tools/compare-operators.mjs +80 -0
  49. package/tools/compare-self.mjs +124 -0
  50. package/tools/describe-power-user.mjs +78 -0
  51. package/tools/diagnose-cascade.mjs +192 -0
  52. package/tools/discover-peers.mjs +198 -0
  53. package/tools/enroll.mjs +119 -0
  54. package/tools/get-best-operator.mjs +59 -0
  55. package/tools/get-leaderboard.mjs +27 -0
  56. package/tools/get-operator.mjs +33 -0
  57. package/tools/index.mjs +229 -0
  58. package/tools/optimize-efficiency.mjs +170 -0
  59. package/tools/rank-paste.mjs +51 -0
  60. package/tools/rank-windows.mjs +120 -0
  61. package/tools/self-improve.mjs +423 -0
  62. package/tools/simulate-change.mjs +207 -0
  63. package/tools/submit-paste.mjs +121 -0
  64. package/tools/submit-verified.mjs +153 -0
  65. package/tools/suggest-improvements.mjs +168 -0
  66. package/tools/tokenpull-compare.mjs +123 -0
  67. package/tools/tokenpull-submit.mjs +235 -0
  68. package/tools/tokenpull.mjs +113 -0
  69. package/tools/tokscale-breakdown.mjs +119 -0
  70. package/tools/tokscale-competitive-intel.mjs +45 -0
  71. package/tools/tokscale-cost-analysis.mjs +31 -0
  72. package/tools/tokscale-developer-profile.mjs +36 -0
  73. package/tools/tokscale-device-profile.mjs +35 -0
  74. package/tools/tokscale-market-share.mjs +38 -0
  75. package/tools/tokscale-mcp-usage.mjs +33 -0
  76. package/tools/tokscale-model-trends.mjs +32 -0
  77. package/tools/watch-tokenpull.mjs +134 -0
  78. package/tools.mjs +1 -3897
  79. package/tui.mjs +4 -2650
  80. package/banner.svg +0 -7
package/adapters.mjs CHANGED
@@ -1,865 +1 @@
1
- /**
2
- * adapters.mjs — SigRank tokenpull adapters for all supported platforms.
3
- *
4
- * OKF (code-file form — fields live in this comment, not raw frontmatter, so the .mjs
5
- * still parses as valid JS):
6
- * type: Reference
7
- * title: SigRank tokenpull adapters
8
- * description: Per-platform adapters implementing the tokenpull contract — async-generate
9
- * {id,sid,ts,input,output,cacheCreate,cacheRead,file} from local logs. Reasoning→output,
10
- * cost fields dropped, missing cacheCreate → estimated:true. Token-only, read-only.
11
- * tags: [sigrank, mcp, tokenpull, adapters, reference]
12
- * timestamp: 2026-06-23
13
- *
14
- * Each adapter implements the tokenpull contract:
15
- * messages(root): async generator → { id?, sid?, ts, input, output, cacheCreate, cacheRead, file }
16
- *
17
- * SigRank-specific mapping rules (applied consistently across all adapters):
18
- * - Claude = parsed natively (4 pillars exact, no conversion)
19
- * - Non-Claude systems = combined input → split via ioRatio:
20
- * input = floor(output × ioRatio) // Beta from Claude, else Alpha 2.0
21
- * cacheCreate = max(0, uncached − input)
22
- * cacheRead = exact
23
- * (Adapters that yield {ts, output, cacheRead, uncached} via records() go through
24
- * tokenpullCodex(); adapters that yield native 4-pillar via messages() go through
25
- * tokenpull() directly. The split happens at the tokenpullAny() routing layer.)
26
- * - Reasoning / thinking tokens → folded into `output` (they are output-side spend)
27
- * - No cache-creation data available → cacheCreate: 0 + adapter sets `estimated: true`
28
- * - Cost fields (USD) → NEVER used or forwarded (SigRank scores cost efficiency from
29
- * token ratios, not from dollar amounts — cost efficiency is derived, not ingested)
30
- * - Credits / provider-specific fields → dropped
31
- *
32
- * "estimated" flag: set on the ADAPTER OBJECT (not per-record) when the adapter
33
- * cannot provide native cacheCreate values. tokenpull() and tokenpullCodex() already
34
- * handle this pattern; new adapters with estimated=true get the same treatment.
35
- *
36
- * SQLite adapters shell out to `sqlite3 -json` (macOS/Linux system tool, no npm dep).
37
- * If sqlite3 is unavailable the adapter returns an empty generator with a dataGap note.
38
- *
39
- * Data-gap notes (sources that can't provide full 4-pillar data) are attached on the
40
- * adapter object as `dataGap: string` so tokenpull() can surface them to the user.
41
- */
42
-
43
- import { readdir, readFile } from "node:fs/promises";
44
- import { join, dirname } from "node:path";
45
- import { homedir } from "node:os";
46
- import { execFile as execFileCb } from "node:child_process";
47
- import { promisify } from "node:util";
48
-
49
- const execFileP = promisify(execFileCb);
50
- const DAY_MS = 86_400_000; // shared with tokenpull.mjs but kept local to avoid circular import
51
-
52
- // ── File-system helpers ───────────────────────────────────────────────────────
53
-
54
- /** Recursively yield every file whose name matches `pred` under dir (skips symlink dirs). */
55
- async function* walkFiles(dir, pred, counter = { n: 0 }, max = 10_000) {
56
- if (counter.n >= max) return;
57
- let entries;
58
- try {
59
- entries = await readdir(dir, { withFileTypes: true });
60
- } catch {
61
- return;
62
- }
63
- for (const e of entries) {
64
- if (counter.n >= max) return;
65
- const full = join(dir, e.name);
66
- if (e.isSymbolicLink()) continue;
67
- if (e.isDirectory()) {
68
- yield* walkFiles(full, pred, counter, max);
69
- } else if (e.isFile() && pred(e.name)) {
70
- counter.n++;
71
- yield full;
72
- }
73
- }
74
- }
75
-
76
- const isJsonl = (n) =>
77
- n.endsWith(".jsonl") ||
78
- n.endsWith(".jsonl.deleted") ||
79
- n.match(/\.jsonl\.reset\.\d+$/);
80
- const isJson = (n) => n.endsWith(".json") && !n.endsWith(".jsonl");
81
-
82
- /** Read a file as UTF-8, return null on error. */
83
- async function readUtf8(path) {
84
- try {
85
- return await readFile(path, "utf8");
86
- } catch {
87
- return null;
88
- }
89
- }
90
-
91
- /** Parse each newline-delimited JSON line, yield parsed objects silently skipping bad lines. */
92
- function* parseJsonl(text, filePath) {
93
- if (!text) return;
94
- for (const line of text.split("\n")) {
95
- const s = line.trim();
96
- if (!s) continue;
97
- try {
98
- yield [JSON.parse(s), filePath];
99
- } catch {
100
- /* skip malformed */
101
- }
102
- }
103
- }
104
-
105
- /** Run sqlite3 -json and return parsed rows, or [] on error/unavailability.
106
- * execFile with an args array — no shell, so dbPath/sql need no quoting/escaping
107
- * (matches the execFile hardening used in tools.mjs / tokenpull.mjs). */
108
- async function sqliteJson(dbPath, sql, timeoutMs = 10_000) {
109
- try {
110
- const { stdout } = await execFileP("sqlite3", ["-json", dbPath, sql], {
111
- timeout: timeoutMs,
112
- maxBuffer: 256 * 1024 * 1024, // 256MB — Devin's sessions.db yields ~15MB JSON
113
- });
114
- return JSON.parse(stdout || "[]");
115
- } catch {
116
- return [];
117
- }
118
- }
119
-
120
- // ── Env-var helper ────────────────────────────────────────────────────────────
121
- /** Resolve roots from env var or default. Supports comma-separated list. */
122
- function roots(envVar, defaultPath) {
123
- const v = process.env[envVar];
124
- if (v)
125
- return v
126
- .split(",")
127
- .map((s) => s.trim())
128
- .filter(Boolean);
129
- return [defaultPath];
130
- }
131
-
132
- // ── 1. Amp ────────────────────────────────────────────────────────────────────
133
- // ~/.local/share/amp/threads/**/*.json
134
- // Fields: assistant message usage: input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens
135
- export const ampAdapter = {
136
- platform: "amp",
137
- defaultRoot: () => join(homedir(), ".local", "share", "amp"),
138
- async *messages(root) {
139
- for (const r of roots("AMP_DATA_DIR", root)) {
140
- const threadsDir = join(r, "threads");
141
- for await (const path of walkFiles(threadsDir, isJson)) {
142
- const text = await readUtf8(path);
143
- if (!text) continue;
144
- let thread;
145
- try {
146
- thread = JSON.parse(text);
147
- } catch {
148
- continue;
149
- }
150
- // Amp thread is an array or object with messages
151
- const msgs = Array.isArray(thread) ? thread : thread.messages || [];
152
- for (const msg of msgs) {
153
- if (!msg || msg.role !== "assistant") continue;
154
- const u = msg.usage || (msg.metadata && msg.metadata.usage) || {};
155
- const input = Number(u.input_tokens || u.inputTokens || 0);
156
- const output = Number(u.output_tokens || u.outputTokens || 0);
157
- const cacheCreate = Number(
158
- u.cache_creation_tokens || u.cacheCreationTokens || 0,
159
- );
160
- const cacheRead = Number(
161
- u.cache_read_tokens || u.cacheReadTokens || 0,
162
- );
163
- if (input + output + cacheCreate + cacheRead === 0) continue;
164
- yield {
165
- id: msg.id || null,
166
- sid: thread.id || null,
167
- ts: msg.timestamp || msg.created_at || null,
168
- input,
169
- output,
170
- cacheCreate,
171
- cacheRead,
172
- file: path,
173
- };
174
- }
175
- }
176
- }
177
- },
178
- };
179
-
180
- // ── 2. Kimi ───────────────────────────────────────────────────────────────────
181
- // ~/.kimi/sessions/<group-id>/<session-id>/wire.jsonl
182
- // StatusUpdate lines only; token_usage: { input_other, output, input_cache_read, input_cache_creation }
183
- export const kimiAdapter = {
184
- platform: "kimi",
185
- defaultRoot: () => join(homedir(), ".kimi"),
186
- async *messages(root) {
187
- const sessionsDir = join(roots("KIMI_DATA_DIR", root)[0], "sessions");
188
- for await (const path of walkFiles(sessionsDir, isJsonl)) {
189
- const text = await readUtf8(path);
190
- for (const [ev] of parseJsonl(text, path)) {
191
- if (!ev || ev.type !== "StatusUpdate") continue;
192
- const u = ev.token_usage || {};
193
- const input = Number(u.input_other || 0);
194
- const output = Number(u.output || 0);
195
- const cacheCreate = Number(u.input_cache_creation || 0);
196
- const cacheRead = Number(u.input_cache_read || 0);
197
- if (input + output + cacheCreate + cacheRead === 0) continue;
198
- yield {
199
- id: ev.id || null,
200
- sid: null,
201
- ts: ev.timestamp || ev.created_at || null,
202
- input,
203
- output,
204
- cacheCreate,
205
- cacheRead,
206
- file: path,
207
- };
208
- }
209
- }
210
- },
211
- };
212
-
213
- // ── 3. Qwen ───────────────────────────────────────────────────────────────────
214
- // ~/.qwen/projects/{project}/chats/*.jsonl
215
- // usageMetadata: { promptTokenCount, candidatesTokenCount, cachedContentTokenCount, thoughtsTokenCount }
216
- // No cache creation field. Reasoning (thoughtsTokenCount) → output.
217
- export const qwenAdapter = {
218
- platform: "qwen",
219
- estimated: true, // no cacheCreate in logs
220
- defaultRoot: () => join(homedir(), ".qwen"),
221
- async *messages(root) {
222
- const projectsDir = join(roots("QWEN_DATA_DIR", root)[0], "projects");
223
- for await (const path of walkFiles(projectsDir, isJsonl)) {
224
- const text = await readUtf8(path);
225
- for (const [ev] of parseJsonl(text, path)) {
226
- if (!ev || !ev.usageMetadata) continue;
227
- const u = ev.usageMetadata;
228
- const rawInput = Number(u.promptTokenCount || 0);
229
- const rawOutput = Number(u.candidatesTokenCount || 0);
230
- const thoughts = Number(u.thoughtsTokenCount || 0); // reasoning → output
231
- const cacheRead = Number(u.cachedContentTokenCount || 0);
232
- // promptTokenCount is inclusive of cached; subtract to get fresh input
233
- const input = Math.max(0, rawInput - cacheRead);
234
- const output = rawOutput + thoughts;
235
- if (input + output + cacheRead === 0) continue;
236
- yield {
237
- id: ev.id || null,
238
- sid: null,
239
- ts: ev.timestamp || null,
240
- input,
241
- output,
242
- cacheCreate: 0,
243
- cacheRead,
244
- file: path,
245
- };
246
- }
247
- }
248
- },
249
- };
250
-
251
- // ── 4. pi-agent ───────────────────────────────────────────────────────────────
252
- // ~/.pi/agent/sessions/**/*.jsonl
253
- // Fields: inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens
254
- export const piAdapter = {
255
- platform: "pi",
256
- defaultRoot: () => join(homedir(), ".pi", "agent", "sessions"),
257
- async *messages(root) {
258
- for (const r of roots("PI_AGENT_DIR", root)) {
259
- for await (const path of walkFiles(r, isJsonl)) {
260
- const text = await readUtf8(path);
261
- for (const [ev] of parseJsonl(text, path)) {
262
- if (!ev) continue;
263
- // pi-agent stores usage in assistant messages or usage events
264
- const u = ev.usage || ev;
265
- const input = Number(u.inputTokens || u.input_tokens || 0);
266
- const output = Number(u.outputTokens || u.output_tokens || 0);
267
- const cacheCreate = Number(
268
- u.cacheCreationTokens || u.cache_creation_tokens || 0,
269
- );
270
- const cacheRead = Number(
271
- u.cacheReadTokens || u.cache_read_tokens || 0,
272
- );
273
- if (input + output + cacheCreate + cacheRead === 0) continue;
274
- yield {
275
- id: ev.id || null,
276
- sid: ev.sessionId || null,
277
- ts: ev.timestamp || null,
278
- input,
279
- output,
280
- cacheCreate,
281
- cacheRead,
282
- file: path,
283
- };
284
- }
285
- }
286
- }
287
- },
288
- };
289
-
290
- // ── 5. OpenClaw ───────────────────────────────────────────────────────────────
291
- // ~/.openclaw/ (also ~/.clawdbot/, ~/.moltbot/, ~/.moldbot/)
292
- // agents/<agentId>/sessions/<uuid>.jsonl (+ .deleted.<ts> and .reset.<ts> variants)
293
- // Per-message: input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens
294
- export const openclawAdapter = {
295
- platform: "openclaw",
296
- defaultRoot: () => {
297
- const env = process.env.OPENCLAW_DIR;
298
- if (env) return env.split(",")[0].trim();
299
- return join(homedir(), ".openclaw");
300
- },
301
- async *messages(root) {
302
- const dirs = process.env.OPENCLAW_DIR
303
- ? process.env.OPENCLAW_DIR.split(",").map((s) => s.trim())
304
- : [
305
- join(homedir(), ".openclaw"),
306
- join(homedir(), ".clawdbot"),
307
- join(homedir(), ".moltbot"),
308
- join(homedir(), ".moldbot"),
309
- ];
310
- const pred = (n) => isJsonl(n) || n.endsWith(".json"); // covers archived variants
311
- for (const dir of dirs) {
312
- for await (const path of walkFiles(dir, pred)) {
313
- const text = await readUtf8(path);
314
- for (const [ev] of parseJsonl(text, path)) {
315
- if (!ev || ev.role !== "assistant") continue;
316
- const u = ev.usage || ev.tokens || {};
317
- const input = Number(u.input_tokens || u.inputTokens || 0);
318
- const output = Number(u.output_tokens || u.outputTokens || 0);
319
- const cacheCreate = Number(
320
- u.cache_creation_tokens || u.cacheCreationTokens || 0,
321
- );
322
- const cacheRead = Number(
323
- u.cache_read_tokens || u.cacheReadTokens || 0,
324
- );
325
- if (input + output + cacheCreate + cacheRead === 0) continue;
326
- yield {
327
- id: ev.id || null,
328
- sid: null,
329
- ts: ev.timestamp || null,
330
- input,
331
- output,
332
- cacheCreate,
333
- cacheRead,
334
- file: path,
335
- };
336
- }
337
- }
338
- }
339
- },
340
- };
341
-
342
- // ── 6. Droid ──────────────────────────────────────────────────────────────────
343
- // ~/.factory/sessions/**/*.settings.json
344
- // Fields: input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, thinking_tokens
345
- // Thinking tokens → output. Per-settings-file (session-level granularity).
346
- // CUMULATIVE-COLUMN GUARD: settings files contain session-level totals. If the same session
347
- // appears in multiple files, summing double-counts. We set both id and sid to the session id
348
- // so tokenpull()'s (sid|id) keep-last dedup collapses duplicates to the latest values.
349
- export const droidAdapter = {
350
- platform: "droid",
351
- defaultRoot: () => join(homedir(), ".factory", "sessions"),
352
- async *messages(root) {
353
- for (const r of roots("DROID_SESSIONS_DIR", root)) {
354
- for await (const path of walkFiles(r, (n) =>
355
- n.endsWith(".settings.json"),
356
- )) {
357
- const text = await readUtf8(path);
358
- if (!text) continue;
359
- let s;
360
- try {
361
- s = JSON.parse(text);
362
- } catch {
363
- continue;
364
- }
365
- const input = Number(s.input_tokens || 0);
366
- const output =
367
- Number(s.output_tokens || 0) + Number(s.thinking_tokens || 0);
368
- const cacheCreate = Number(s.cache_creation_tokens || 0);
369
- const cacheRead = Number(s.cache_read_tokens || 0);
370
- if (input + output + cacheCreate + cacheRead === 0) continue;
371
- const sessionId = String(s.session_id || s.id || "");
372
- yield {
373
- id: sessionId,
374
- sid: sessionId,
375
- ts: s.updated_at || s.created_at || null,
376
- input,
377
- output,
378
- cacheCreate,
379
- cacheRead,
380
- file: path,
381
- };
382
- }
383
- }
384
- },
385
- };
386
-
387
- // ── 7. Codebuff ───────────────────────────────────────────────────────────────
388
- // ~/.config/manicode/projects/<project>/chats/<chat-id>/chat-messages.json
389
- // assistant messages: metadata.usage or metadata.codebuff.usage
390
- export const codebuffAdapter = {
391
- platform: "codebuff",
392
- defaultRoot: () => join(homedir(), ".config", "manicode"),
393
- async *messages(root) {
394
- const dirs = process.env.CODEBUFF_DATA_DIR
395
- ? process.env.CODEBUFF_DATA_DIR.split(",").map((s) => s.trim())
396
- : [
397
- join(homedir(), ".config", "manicode"),
398
- join(homedir(), ".config", "manicode-dev"),
399
- join(homedir(), ".config", "manicode-staging"),
400
- ];
401
- for (const dir of dirs) {
402
- for await (const path of walkFiles(
403
- dir,
404
- (n) => n === "chat-messages.json",
405
- )) {
406
- const text = await readUtf8(path);
407
- if (!text) continue;
408
- let msgs;
409
- try {
410
- msgs = JSON.parse(text);
411
- } catch {
412
- continue;
413
- }
414
- if (!Array.isArray(msgs)) continue;
415
- for (const msg of msgs) {
416
- if (!msg || msg.role !== "assistant") continue;
417
- const u =
418
- (msg.metadata &&
419
- (msg.metadata.usage || msg.metadata.codebuff?.usage)) ||
420
- {};
421
- const input = Number(u.input_tokens || u.inputTokens || 0);
422
- const output = Number(u.output_tokens || u.outputTokens || 0);
423
- const cacheCreate = Number(
424
- u.cache_creation_tokens || u.cacheCreationTokens || 0,
425
- );
426
- const cacheRead = Number(
427
- u.cache_read_tokens || u.cacheReadTokens || 0,
428
- );
429
- if (input + output + cacheCreate + cacheRead === 0) continue;
430
- yield {
431
- id: msg.id || null,
432
- sid: null,
433
- ts: msg.timestamp || msg.created_at || null,
434
- input,
435
- output,
436
- cacheCreate,
437
- cacheRead,
438
- file: path,
439
- };
440
- }
441
- }
442
- }
443
- },
444
- };
445
-
446
- // ── 8. Gemini CLI ─────────────────────────────────────────────────────────────
447
- // ~/.gemini/tmp/*/chats/*.json and *.jsonl
448
- // Fields: input, output, cached, thought (reasoning), tool, total
449
- // SigRank mapping: input = input−cached (fresh), cacheRead = cached, cacheCreate = 0 (not exposed),
450
- // output = output + thought (reasoning→output)
451
- export const geminiAdapter = {
452
- platform: "gemini",
453
- estimated: true, // no cacheCreate field in Gemini logs
454
- defaultRoot: () => join(homedir(), ".gemini", "tmp"),
455
- async *messages(root) {
456
- for (const r of roots("GEMINI_DATA_DIR", root)) {
457
- for await (const path of walkFiles(
458
- r,
459
- (n) => n.endsWith(".json") || n.endsWith(".jsonl"),
460
- )) {
461
- const text = await readUtf8(path);
462
- if (!text) continue;
463
- // Try JSONL first, then single JSON
464
- let parsed = [];
465
- try {
466
- parsed = text
467
- .trim()
468
- .split("\n")
469
- .filter(Boolean)
470
- .map((l) => JSON.parse(l));
471
- } catch {
472
- try {
473
- parsed = [JSON.parse(text)];
474
- } catch {
475
- continue;
476
- }
477
- }
478
- for (const ev of parsed) {
479
- if (!ev) continue;
480
- // Gemini usage may be at top level or nested in usageMetadata
481
- const u = ev.usageMetadata || ev.usage || ev;
482
- const rawInput = Number(u.input || u.promptTokenCount || 0);
483
- const rawOutput = Number(u.output || u.candidatesTokenCount || 0);
484
- const cached = Number(u.cached || u.cachedContentTokenCount || 0);
485
- const thought = Number(u.thought || u.thoughtsTokenCount || 0);
486
- if (rawInput + rawOutput + cached + thought === 0) continue;
487
- const input = Math.max(0, rawInput - cached); // strip cached from input
488
- const output = rawOutput + thought; // reasoning → output
489
- yield {
490
- id: ev.id || null,
491
- sid: null,
492
- ts: ev.timestamp || null,
493
- input,
494
- output,
495
- cacheCreate: 0,
496
- cacheRead: cached,
497
- file: path,
498
- };
499
- }
500
- }
501
- }
502
- },
503
- };
504
-
505
- // ── 9. GitHub Copilot CLI ─────────────────────────────────────────────────────
506
- // ~/.copilot/otel/*.jsonl (requires COPILOT_OTEL_ENABLED=true before session start)
507
- // OpenTelemetry spans; looks for llm.token_count.prompt / completion / cached attributes
508
- export const copilotAdapter = {
509
- platform: "copilot",
510
- defaultRoot: () => join(homedir(), ".copilot", "otel"),
511
- async *messages(root) {
512
- const dir = process.env.COPILOT_OTEL_FILE_EXPORTER_PATH
513
- ? dirname(process.env.COPILOT_OTEL_FILE_EXPORTER_PATH)
514
- : root;
515
- for await (const path of walkFiles(dir, isJsonl)) {
516
- const text = await readUtf8(path);
517
- for (const [ev] of parseJsonl(text, path)) {
518
- if (!ev) continue;
519
- // OTel span: attributes may be an object or array of {key,value} pairs
520
- const attrs = ev.attributes || ev.resource?.attributes || {};
521
- const get = (k) => {
522
- if (typeof attrs === "object" && !Array.isArray(attrs))
523
- return attrs[k];
524
- if (Array.isArray(attrs)) {
525
- const a = attrs.find((x) => x.key === k);
526
- return a?.value?.intValue ?? a?.value?.stringValue ?? null;
527
- }
528
- return null;
529
- };
530
- const input = Number(
531
- get("llm.token_count.prompt") ||
532
- get("gen_ai.usage.input_tokens") ||
533
- 0,
534
- );
535
- const output = Number(
536
- get("llm.token_count.completion") ||
537
- get("gen_ai.usage.output_tokens") ||
538
- 0,
539
- );
540
- const cacheCreate = Number(get("llm.token_count.cache_creation") || 0);
541
- const cacheRead = Number(
542
- get("llm.token_count.cache_read") ||
543
- get("gen_ai.usage.cache_read_input_tokens") ||
544
- 0,
545
- );
546
- if (input + output + cacheCreate + cacheRead === 0) continue;
547
- yield {
548
- id: ev.traceId || ev.spanId || null,
549
- sid: null,
550
- ts: ev.startTimeUnixNano
551
- ? new Date(Number(ev.startTimeUnixNano) / 1e6).toISOString()
552
- : null,
553
- input,
554
- output,
555
- cacheCreate,
556
- cacheRead,
557
- file: path,
558
- };
559
- }
560
- }
561
- },
562
- setupNote:
563
- "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.",
564
- };
565
-
566
- // ── 10. OpenCode ──────────────────────────────────────────────────────────────
567
- // ~/.local/share/opencode — JSON message files, but costs stored as 0 and token fields
568
- // are calculated via LiteLLM pricing (not stored in the log). No raw token fields.
569
- // SigRank cannot derive pillars from OpenCode logs with current log format.
570
- export const opencodeAdapter = {
571
- platform: "opencode",
572
- defaultRoot: () => join(homedir(), ".local", "share", "opencode"),
573
- estimated: true,
574
- dataGap:
575
- "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.",
576
- // eslint-disable-next-line require-yield
577
- async *messages() {
578
- /* no data available */
579
- },
580
- };
581
-
582
- // ── 11. Goose ─────────────────────────────────────────────────────────────────
583
- // SQLite: sessions.db at standard Goose data roots or $GOOSE_PATH_ROOT/data/sessions/sessions.db
584
- // Columns: accumulated_input_tokens (or input_tokens), accumulated_output_tokens (or output_tokens),
585
- // accumulated_total_tokens (or total_tokens). NO cache fields. Reasoning = total-input-output.
586
- // CUMULATIVE-COLUMN GUARD: accumulated_* columns are per-session running totals. If the sessions
587
- // table has multiple rows per session id, summing them downstream double-counts. We set both sid
588
- // and id to the session id and ORDER BY updated_at so tokenpull()'s (sid|id) keep-last dedup
589
- // collapses duplicate session rows to the latest (max accumulated) values.
590
- export const gooseAdapter = {
591
- platform: "goose",
592
- estimated: true, // no cacheCreate or cacheRead
593
- defaultRoot: () => {
594
- const env = process.env.GOOSE_PATH_ROOT;
595
- if (env) return env;
596
- // Standard locations (macOS first, then XDG)
597
- return join(homedir(), "Library", "Application Support", "goose");
598
- },
599
- async *messages(root) {
600
- const dbCandidates = process.env.GOOSE_PATH_ROOT
601
- ? [join(process.env.GOOSE_PATH_ROOT, "data", "sessions", "sessions.db")]
602
- : [
603
- join(
604
- homedir(),
605
- "Library",
606
- "Application Support",
607
- "goose",
608
- "sessions",
609
- "sessions.db",
610
- ),
611
- join(
612
- homedir(),
613
- ".local",
614
- "share",
615
- "goose",
616
- "sessions",
617
- "sessions.db",
618
- ),
619
- join(
620
- homedir(),
621
- ".local",
622
- "share",
623
- "Block",
624
- "goose",
625
- "sessions",
626
- "sessions.db",
627
- ),
628
- ];
629
- for (const db of dbCandidates) {
630
- // ORDER BY updated_at so the latest cumulative row per session is yielded last
631
- // (tokenpull keep-last dedup picks the final row = max accumulated values).
632
- const rows = await sqliteJson(
633
- db,
634
- "SELECT * FROM sessions ORDER BY updated_at",
635
- );
636
- for (const row of rows) {
637
- const input = Number(
638
- row.accumulated_input_tokens || row.input_tokens || 0,
639
- );
640
- const output = Number(
641
- row.accumulated_output_tokens || row.output_tokens || 0,
642
- );
643
- const total = Number(
644
- row.accumulated_total_tokens || row.total_tokens || 0,
645
- );
646
- const reasoning = Math.max(0, total - input - output); // folded into output
647
- if (input + output === 0) continue;
648
- const sessionId = String(row.id || row.session_id || "");
649
- yield {
650
- id: sessionId,
651
- sid: sessionId,
652
- ts: row.created_at || row.updated_at || null,
653
- input,
654
- output: output + reasoning,
655
- cacheCreate: 0,
656
- cacheRead: 0,
657
- file: db,
658
- };
659
- }
660
- }
661
- },
662
- };
663
-
664
- // ── 12. Kilo ──────────────────────────────────────────────────────────────────
665
- // SQLite: ~/.local/share/kilo/kilo.db
666
- // Per-message rows with model, input/output/cache token columns.
667
- export const kiloAdapter = {
668
- platform: "kilo",
669
- defaultRoot: () => join(homedir(), ".local", "share", "kilo"),
670
- async *messages(root) {
671
- const dbPath = join(roots("KILO_DATA_DIR", root)[0], "kilo.db");
672
- const rows = await sqliteJson(
673
- dbPath,
674
- 'SELECT * FROM messages WHERE role="assistant"',
675
- );
676
- for (const row of rows) {
677
- const input = Number(row.input_tokens || row.inputTokens || 0);
678
- const output = Number(row.output_tokens || row.outputTokens || 0);
679
- const cacheCreate = Number(
680
- row.cache_creation_tokens || row.cacheCreationTokens || 0,
681
- );
682
- const cacheRead = Number(
683
- row.cache_read_tokens || row.cacheReadTokens || 0,
684
- );
685
- if (input + output + cacheCreate + cacheRead === 0) continue;
686
- yield {
687
- id: String(row.id || ""),
688
- sid: String(row.session_id || row.sessionId || ""),
689
- ts: row.created_at || row.timestamp || null,
690
- input,
691
- output,
692
- cacheCreate,
693
- cacheRead,
694
- file: dbPath,
695
- };
696
- }
697
- },
698
- };
699
-
700
- // ── 13. Hermes Agent ─────────────────────────────────────────────────────────
701
- // SQLite: ~/.hermes/state.db
702
- // Per-session rows: input, output, cache_read, cache_write (=cacheCreate), reasoning_tokens → output
703
- // CUMULATIVE-COLUMN GUARD: session rows may contain cumulative totals. If the same session
704
- // appears in multiple rows, summing double-counts. We set both id and sid to the session id
705
- // and ORDER BY updated_at so tokenpull()'s (sid|id) keep-last dedup collapses duplicates
706
- // to the latest values.
707
- export const hermesAdapter = {
708
- platform: "hermes",
709
- defaultRoot: () => join(homedir(), ".hermes"),
710
- async *messages(root) {
711
- for (const r of roots("HERMES_HOME", root)) {
712
- const dbPath = join(r, "state.db");
713
- // ORDER BY updated_at so the latest cumulative row per session is yielded last.
714
- const rows = await sqliteJson(
715
- dbPath,
716
- "SELECT * FROM sessions ORDER BY updated_at",
717
- );
718
- for (const row of rows) {
719
- const input = Number(row.input || 0);
720
- const reasoning = Number(row.reasoning_tokens || 0);
721
- const output = Number(row.output || 0) + reasoning;
722
- const cacheCreate = Number(row.cache_write || row.cache_creation || 0);
723
- const cacheRead = Number(row.cache_read || 0);
724
- if (input + output + cacheCreate + cacheRead === 0) continue;
725
- const sessionId = String(row.id || row.session_id || "");
726
- yield {
727
- id: sessionId,
728
- sid: sessionId,
729
- ts: row.created_at || row.updated_at || null,
730
- input,
731
- output,
732
- cacheCreate,
733
- cacheRead,
734
- file: dbPath,
735
- };
736
- }
737
- }
738
- },
739
- };
740
-
741
- // ── 14. Devin CLI ────────────────────────────────────────────────────────────
742
- // SQLite: ~/.local/share/devin/cli/sessions.db
743
- // Same combined-input problem as Codex: input_tokens INCLUDES cache write, so we
744
- // yield { ts, output, cacheRead, uncached } and let tokenpullCodex() do the
745
- // ioRatio split (input = output × ioRatio, cacheCreate = uncached − input).
746
- // ioRatio comes from Claude (Beta) or the 7:1:2 average (Alpha = 0.5).
747
- export const devinAdapter = {
748
- platform: "devin",
749
- defaultRoot: () => join(homedir(), ".local", "share", "devin", "cli"),
750
- async *records(root) {
751
- for (const r of roots("DEVIN_HOME", root)) {
752
- const dbPath = join(r, "sessions.db");
753
- const rows = await sqliteJson(
754
- dbPath,
755
- `SELECT row_id, session_id,
756
- json_extract(chat_message, '$.metadata.metrics.input_tokens') as input_tokens,
757
- json_extract(chat_message, '$.metadata.metrics.output_tokens') as output_tokens,
758
- json_extract(chat_message, '$.metadata.metrics.cache_read_tokens') as cache_read_tokens,
759
- json_extract(chat_message, '$.metadata.created_at') as created_at
760
- FROM message_nodes
761
- WHERE json_extract(chat_message, '$.role') = 'assistant'
762
- AND json_extract(chat_message, '$.metadata.metrics.input_tokens') IS NOT NULL
763
- ORDER BY created_at`,
764
- 60_000,
765
- );
766
- for (const row of rows) {
767
- const inputIncl = Number(row.input_tokens || 0);
768
- const cached = Number(row.cache_read_tokens || 0);
769
- const output = Number(row.output_tokens || 0);
770
- if (inputIncl + output + cached === 0) continue;
771
- yield {
772
- ts: row.created_at || null,
773
- output,
774
- cacheRead: cached,
775
- uncached: Math.max(0, inputIncl - cached),
776
- file: dbPath,
777
- };
778
- }
779
- }
780
- },
781
- };
782
-
783
- // ── 15. Other (user-supplied JSON) ───────────────────────────────────────────
784
- // For platforms/models not yet on the adapter list. The user writes a JSON file
785
- // with the 4 pillars per window and points SIGRANK_OTHER_PATH at it. This lets
786
- // anyone with a new tool submit without needing a custom adapter.
787
- //
788
- // JSON format (all fields optional, missing windows are skipped):
789
- // {
790
- // "platform": "my-tool", // optional, defaults to "other"
791
- // "windows": {
792
- // "all": { "input": 1000, "output": 2000, "cacheCreate": 500, "cacheRead": 8000 },
793
- // "30d": { "input": 500, "output": 1000, "cacheCreate": 200, "cacheRead": 4000 },
794
- // "7d": { "input": 100, "output": 200, "cacheCreate": 50, "cacheRead": 800 }
795
- // }
796
- // }
797
- //
798
- // The adapter yields one "message" per window with the summed pillars. Since
799
- // there's no per-message timestamp, all data lands in the "all" window only
800
- // (same as null-timestamp records in other adapters).
801
- export const otherAdapter = {
802
- platform: "other",
803
- defaultRoot: () => process.env.SIGRANK_OTHER_PATH || "",
804
- async *messages(root) {
805
- const filePath = root || process.env.SIGRANK_OTHER_PATH;
806
- if (!filePath) {
807
- throw new Error(
808
- "other adapter: set SIGRANK_OTHER_PATH to a JSON file with your token pillars. " +
809
- 'Format: { "windows": { "all": { "input": N, "output": N, "cacheCreate": N, "cacheRead": N } } }',
810
- );
811
- }
812
- let text;
813
- try {
814
- text = await readFile(filePath, "utf8");
815
- } catch {
816
- throw new Error(`other adapter: cannot read ${filePath} — check SIGRANK_OTHER_PATH`);
817
- }
818
- let data;
819
- try {
820
- data = JSON.parse(text);
821
- } catch {
822
- throw new Error(`other adapter: invalid JSON in ${filePath}`);
823
- }
824
- const windows = data.windows || {};
825
- for (const [win, pillars] of Object.entries(windows)) {
826
- const input = Number(pillars.input || 0);
827
- const output = Number(pillars.output || 0);
828
- const cacheCreate = Number(pillars.cacheCreate || pillars.cache_create || 0);
829
- const cacheRead = Number(pillars.cacheRead || pillars.cache_read || 0);
830
- if (input + output + cacheCreate + cacheRead === 0) continue;
831
- yield {
832
- id: `other:${win}`,
833
- sid: `other-${win}`,
834
- ts: null, // no timestamp → lands in "all" window only
835
- input,
836
- output,
837
- cacheCreate,
838
- cacheRead,
839
- file: filePath,
840
- };
841
- }
842
- },
843
- };
844
-
845
- // ── Registry ──────────────────────────────────────────────────────────────────
846
- /** All non-Claude, non-Codex adapters keyed by platform ID. */
847
- export const ADAPTERS = {
848
- amp: ampAdapter,
849
- kimi: kimiAdapter,
850
- qwen: qwenAdapter,
851
- pi: piAdapter,
852
- openclaw: openclawAdapter,
853
- droid: droidAdapter,
854
- codebuff: codebuffAdapter,
855
- gemini: geminiAdapter,
856
- copilot: copilotAdapter,
857
- opencode: opencodeAdapter,
858
- goose: gooseAdapter,
859
- kilo: kiloAdapter,
860
- hermes: hermesAdapter,
861
- devin: devinAdapter,
862
- other: otherAdapter,
863
- };
864
-
865
- export const ALL_PLATFORMS = Object.keys(ADAPTERS).concat(["claude", "codex"]);
1
+ export * from './adapters/index.mjs';