sigrank 0.13.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/adapters.mjs +6 -4
- package/cli.mjs +26 -126
- package/index.mjs +11 -6
- package/package.json +1 -1
- package/submit.mjs +17 -0
- package/tokenpull.mjs +7 -2
- package/tools.mjs +54 -15
- package/tui.mjs +14 -34
package/README.md
CHANGED
|
@@ -35,9 +35,12 @@ sigrank enroll
|
|
|
35
35
|
|
|
36
36
|
# 3. Submit your cascade to the board
|
|
37
37
|
sigrank submit
|
|
38
|
+
|
|
39
|
+
# (cautious? see exactly what would be sent — four counts + a signature — sending nothing)
|
|
40
|
+
sigrank submit --dry-run
|
|
38
41
|
```
|
|
39
42
|
|
|
40
|
-
That's it.
|
|
43
|
+
That's it. sigrank reads your local AI session logs on-device, derives your token cascade (Υ Yield, Leverage, Velocity, 10xDEV), and publishes to [signalaf.com](https://signalaf.com). No paste, no transcript content — only the four token counts leave your machine.
|
|
41
44
|
|
|
42
45
|
Or just explore without signing in:
|
|
43
46
|
|
|
@@ -71,7 +74,7 @@ sigrank
|
|
|
71
74
|
## Commands
|
|
72
75
|
|
|
73
76
|
```
|
|
74
|
-
⊙ SigRank CLI v0.
|
|
77
|
+
⊙ SigRank CLI v0.14.2
|
|
75
78
|
|
|
76
79
|
Default (no args)
|
|
77
80
|
sigrank unified dashboard: cascade + token pillars + board
|
|
@@ -82,8 +85,6 @@ Commands
|
|
|
82
85
|
board live leaderboard (refreshes every 30s)
|
|
83
86
|
board --window 7d board for a specific window (7d, 30d, 90d, all)
|
|
84
87
|
board --once print once and exit
|
|
85
|
-
me single-platform cascade (claude by default)
|
|
86
|
-
me --platform codex use a different platform adapter
|
|
87
88
|
compare raw pillar audit: tokenpull vs ccusage vs token-dash vs tokscale
|
|
88
89
|
compare --platform codex compare for a specific platform
|
|
89
90
|
tui full tabbed TUI: Dashboard / Trends / Compare / Board / Watch / Connect
|
|
@@ -107,7 +108,6 @@ Examples
|
|
|
107
108
|
sigrank board # live leaderboard
|
|
108
109
|
sigrank compare # pillar audit (claude)
|
|
109
110
|
sigrank compare --platform codex
|
|
110
|
-
sigrank me --platform codex
|
|
111
111
|
sigrank watch --window 7d --refresh 60
|
|
112
112
|
sigrank board --window all --once
|
|
113
113
|
```
|
|
@@ -136,6 +136,7 @@ Six tabs. Keys: `1`-`6` or `←` `→` to switch · `R` refresh · `Q` quit.
|
|
|
136
136
|
```bash
|
|
137
137
|
sigrank enroll # sign in: paste a connect code (get one at signalaf.com → Settings)
|
|
138
138
|
sigrank submit # publish your verified runs to the board (sign in first)
|
|
139
|
+
sigrank submit --dry-run # inspect the exact signed payload without sending anything
|
|
139
140
|
```
|
|
140
141
|
|
|
141
142
|
Or do it inside the TUI on the **Connect** tab (`6`), then press `[S]` to submit.
|
|
@@ -180,7 +181,9 @@ Or if installed globally:
|
|
|
180
181
|
| `tokenpull_submit(codename, window?)` | `{codename?, window?}` | `tokenpull` → publish to board. Omit codename for preview |
|
|
181
182
|
| `tokenpull_compare(platform?)` | `{platform?}` | All four sources side-by-side: tokenpull + ccusage + token-dash + tokscale. Returns pillars, cascade metrics, and delta % vs tokenpull per window |
|
|
182
183
|
| `rank_windows` | `{platform?, window?}` | Multi-window cascade from local logs |
|
|
183
|
-
| `watch_tokenpull` | `{platform?, interval_s?}` |
|
|
184
|
+
| `watch_tokenpull` | `{platform?, interval_s?}` | One cascade snapshot per call (interval_s advisory) |
|
|
185
|
+
| `submit_verified` | `{window?, platform?, dry_run?}` | THE ranked path: builds + ed25519-signs Schema 1.0 snapshots and POSTs them. `platform:'multi'` sums all active platforms. `dry_run:true` returns the exact payload unsent |
|
|
186
|
+
| `enroll` | `{code, device_label?}` | Bind this device with a connect code from signalaf.com → Settings |
|
|
184
187
|
|
|
185
188
|
---
|
|
186
189
|
|
|
@@ -253,7 +256,8 @@ All adapters are token-only (no message content, no cost fields, no credentials)
|
|
|
253
256
|
- **Token-only, always.** No message content is ever read, logged, or transmitted — only token counts (`input`, `output`, `cache_creation`, `cache_read`), message IDs, and timestamps.
|
|
254
257
|
- **Local by default.** `tokenpull` reads only `~/.claude/projects` (Claude) or `~/.codex` (Codex) on your device. Numbers stay on your machine unless you explicitly submit with a codename.
|
|
255
258
|
- **Background tooling excluded.** Memory plugins, observers, summarizers (e.g. `claude-mem`, `mem0`, `observer-sessions`) are filtered from both Claude and Codex reads. `subagents/` are kept — they represent real operator work.
|
|
256
|
-
- **
|
|
259
|
+
- **Board reads are anonymous.** No account needed to browse, compare, or watch.
|
|
260
|
+
- **Ranked submissions are signed, not trusted.** `sigrank submit` requires a one-time `enroll` (device-bound ed25519 key — the private key never leaves your machine). Verify what's sent with `sigrank submit --dry-run`: the payload is four token counts, ratios, and a signature.
|
|
257
261
|
|
|
258
262
|
---
|
|
259
263
|
|
|
@@ -291,7 +295,7 @@ Tests verify (29 assertions):
|
|
|
291
295
|
| File | Responsibility |
|
|
292
296
|
|---|---|
|
|
293
297
|
| `index.mjs` | Entry point — TTY detection, routes to CLI or MCP server |
|
|
294
|
-
| `cli.mjs` | CLI commands: board,
|
|
298
|
+
| `cli.mjs` | CLI commands: board, compare, watch, enroll, submit, help |
|
|
295
299
|
| `tui.mjs` | Full tabbed TUI: Dashboard / Trends / Compare / Board / Watch / Connect |
|
|
296
300
|
| `cascade.mjs` | Pure cascade math (Υ, SNR, leverage, velocity, 10xDEV, class) |
|
|
297
301
|
| `tokenpull.mjs` | On-device log scanner — Claude, Codex, multi-platform |
|
package/adapters.mjs
CHANGED
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
import { readdir, readFile } from 'node:fs/promises'
|
|
36
36
|
import { join, extname } from 'node:path'
|
|
37
37
|
import { homedir } from 'node:os'
|
|
38
|
-
import {
|
|
38
|
+
import { execFile as execFileCb } from 'node:child_process'
|
|
39
39
|
import { promisify } from 'node:util'
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const execFileP = promisify(execFileCb)
|
|
42
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 ───────────────────────────────────────────────────────
|
|
@@ -75,10 +75,12 @@ function* parseJsonl(text, filePath) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
78
|
+
/** Run sqlite3 -json and return parsed rows, or [] on error/unavailability.
|
|
79
|
+
* execFile with an args array — no shell, so dbPath/sql need no quoting/escaping
|
|
80
|
+
* (matches the execFile hardening used in tools.mjs / tokenpull.mjs). */
|
|
79
81
|
async function sqliteJson(dbPath, sql) {
|
|
80
82
|
try {
|
|
81
|
-
const { stdout } = await
|
|
83
|
+
const { stdout } = await execFileP('sqlite3', ['-json', dbPath, sql], { timeout: 10_000 })
|
|
82
84
|
return JSON.parse(stdout || '[]')
|
|
83
85
|
} catch { return [] }
|
|
84
86
|
}
|
package/cli.mjs
CHANGED
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
* npx sigrank board live leaderboard, refreshes every 30s
|
|
10
10
|
* npx sigrank board --window 7d board for a specific window
|
|
11
11
|
* npx sigrank board --once print once and exit (no live refresh)
|
|
12
|
-
* npx sigrank
|
|
13
|
-
* npx sigrank me --platform amp use a different platform adapter
|
|
14
|
-
* npx sigrank me --compare raw pillar comparison: ccusage vs tokenpull vs token-dashboard
|
|
12
|
+
* npx sigrank compare raw pillar comparison: ccusage vs tokenpull vs token-dashboard
|
|
15
13
|
* npx sigrank watch RT tune meter — ALL active platforms × all windows
|
|
16
14
|
* npx sigrank watch --platform X watch one platform only (optional filter)
|
|
17
15
|
* npx sigrank watch --window 7d watch one window only (optional filter)
|
|
@@ -24,7 +22,7 @@
|
|
|
24
22
|
* green = positive movement
|
|
25
23
|
*/
|
|
26
24
|
|
|
27
|
-
import { callTool, DEFAULT_API_BASE } from './tools.mjs'
|
|
25
|
+
import { callTool, DEFAULT_API_BASE, pullActivePlatforms } from './tools.mjs'
|
|
28
26
|
import { classify } from './cascade.mjs'
|
|
29
27
|
import { ensureIdentity, keystorePath } from './keystore.mjs'
|
|
30
28
|
import { submitSignedWindow } from './submit.mjs'
|
|
@@ -314,107 +312,6 @@ async function runBoard({ window = '30d', once = false, refresh = 30 } = {}) {
|
|
|
314
312
|
}
|
|
315
313
|
}
|
|
316
314
|
|
|
317
|
-
// ── ME command ───────────────────────────────────────────────────────────────
|
|
318
|
-
|
|
319
|
-
async function runMe({ platform = 'claude', compare = false } = {}) {
|
|
320
|
-
if (compare) return runCompare({ platform })
|
|
321
|
-
|
|
322
|
-
write(HIDE_CURSOR)
|
|
323
|
-
writeln(` ${dim('reading local token logs…')}`)
|
|
324
|
-
|
|
325
|
-
let pulled
|
|
326
|
-
try {
|
|
327
|
-
pulled = await callTool('tokenpull', { platform })
|
|
328
|
-
} catch (e) {
|
|
329
|
-
write(SHOW_CURSOR)
|
|
330
|
-
write(CURSOR_UP(1) + ERASE_LINE)
|
|
331
|
-
writeln(red(` ✗ ${e.message}`))
|
|
332
|
-
process.exit(1)
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
// clear the "reading…" line
|
|
336
|
-
write(CURSOR_UP(1) + ERASE_LINE)
|
|
337
|
-
|
|
338
|
-
const w = termWidth()
|
|
339
|
-
writeln()
|
|
340
|
-
writeln(` ${gold('⊙ SigRank')} ${bold('Your Cascade')} ${dim(`platform: ${pulled.platform ?? platform}`)}`)
|
|
341
|
-
if (pulled.estimated) writeln(` ${dim('⚠ estimated values (cache data unavailable for this platform)')}`)
|
|
342
|
-
writeln(` ${dim('─'.repeat(w - 4))}`)
|
|
343
|
-
|
|
344
|
-
// column headers
|
|
345
|
-
const cols_h = [
|
|
346
|
-
padEnd(dim('Window'), 8),
|
|
347
|
-
padStart(dim('Υ Yield'), 10),
|
|
348
|
-
padStart(dim('SNR'), 7),
|
|
349
|
-
padStart(dim('Leverage'), 9),
|
|
350
|
-
padStart(dim('Velocity'), 9),
|
|
351
|
-
padStart(dim('10xDEV'), 8),
|
|
352
|
-
padStart(dim('Class'), 13),
|
|
353
|
-
padStart(dim('Tokens'), 8),
|
|
354
|
-
]
|
|
355
|
-
writeln(` ${cols_h.join(' ')}`)
|
|
356
|
-
writeln(` ${dim('·'.repeat(w - 4))}`)
|
|
357
|
-
|
|
358
|
-
const windows = pulled.windows ?? []
|
|
359
|
-
for (const win of windows) {
|
|
360
|
-
const cas = win.cascade
|
|
361
|
-
const isAll = win.window === 'all'
|
|
362
|
-
const wLabel = isAll ? bold('all-time') : win.window
|
|
363
|
-
const yVal = cas?.yield != null ? fmtYield(cas.yield) : '—'
|
|
364
|
-
const snrVal = cas?.snr != null ? fmtSNR(cas.snr) : '—'
|
|
365
|
-
const levVal = cas?.leverage != null ? `${fmtLev(cas.leverage)}×` : '—'
|
|
366
|
-
const velVal = cas?.velocity != null ? cas.velocity.toFixed(2) : '—'
|
|
367
|
-
const devVal = cas?.dev10x != null ? cas.dev10x.toFixed(2) : '—'
|
|
368
|
-
const cls = colorClass(cas?.class ?? '—')
|
|
369
|
-
const tok = fmtTokens(win.pillars?.total ?? (
|
|
370
|
-
(win.pillars?.input ?? 0) + (win.pillars?.output ?? 0) +
|
|
371
|
-
(win.pillars?.cacheCreate ?? 0) + (win.pillars?.cacheRead ?? 0)
|
|
372
|
-
))
|
|
373
|
-
|
|
374
|
-
const row = [
|
|
375
|
-
padEnd(wLabel, 8),
|
|
376
|
-
padStart(isAll ? gold(yVal) : yVal, 10),
|
|
377
|
-
padStart(snrVal, 7),
|
|
378
|
-
padStart(levVal, 9),
|
|
379
|
-
padStart(velVal, 9),
|
|
380
|
-
padStart(devVal, 8),
|
|
381
|
-
padEnd(cls, 13),
|
|
382
|
-
padStart(tok, 8),
|
|
383
|
-
]
|
|
384
|
-
writeln(` ${row.join(' ')}`)
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
writeln(` ${dim('─'.repeat(w - 4))}`)
|
|
388
|
-
|
|
389
|
-
// card for the best window (all-time if present, else first)
|
|
390
|
-
const best = windows.find(w => w.window === 'all') ?? windows[0]
|
|
391
|
-
if (best?.card) {
|
|
392
|
-
writeln()
|
|
393
|
-
writeln(` ${dim('cascade read:')}`)
|
|
394
|
-
// wrap card text at terminal width
|
|
395
|
-
const cardText = best.card
|
|
396
|
-
const maxW = w - 6
|
|
397
|
-
const words = cardText.split(' ')
|
|
398
|
-
let line = ''
|
|
399
|
-
for (const word of words) {
|
|
400
|
-
if (line.length + word.length + 1 > maxW) {
|
|
401
|
-
writeln(` ${line}`)
|
|
402
|
-
line = word
|
|
403
|
-
} else {
|
|
404
|
-
line = line ? `${line} ${word}` : word
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (line) writeln(` ${line}`)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
writeln()
|
|
411
|
-
|
|
412
|
-
// submit hint
|
|
413
|
-
writeln(` ${dim('to publish:')} ${cyan('npx sigrank submit')} ${dim('(sign in first:')} ${cyan('npx sigrank enroll')}${dim(' — get a code at signalaf.com → Settings)')}`)
|
|
414
|
-
writeln()
|
|
415
|
-
write(SHOW_CURSOR)
|
|
416
|
-
}
|
|
417
|
-
|
|
418
315
|
// ── COMPARE command ───────────────────────────────────────────────────────────
|
|
419
316
|
// Side-by-side: ccusage (JSON) vs tokenpull vs token-dashboard (SQLite)
|
|
420
317
|
//
|
|
@@ -756,16 +653,8 @@ const WATCH_WINDOWS = ['7d', '30d', '90d', 'all']
|
|
|
756
653
|
// Detect which platforms have real local data (input+output > 0 in any window) via tokenpull.
|
|
757
654
|
// Returns an array of pull results (the same shape tokenpullAny yields), one per active platform.
|
|
758
655
|
async function detectActivePulls() {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
const active = []
|
|
762
|
-
for (const r of settled) {
|
|
763
|
-
if (r.status !== 'fulfilled' || !r.value) continue
|
|
764
|
-
const d = r.value
|
|
765
|
-
const live = (d.windows || []).some((w) => (w.pillars.input + w.pillars.output) > 0)
|
|
766
|
-
if (live) active.push(d)
|
|
767
|
-
}
|
|
768
|
-
return active
|
|
656
|
+
// Unified: one loader for me / watch / the TUI Dashboard (was a separate tokenpullAny loop).
|
|
657
|
+
return pullActivePlatforms()
|
|
769
658
|
}
|
|
770
659
|
|
|
771
660
|
async function runWatch({ platform, window: win, refresh = 30, submit = false } = {}) {
|
|
@@ -1320,8 +1209,6 @@ async function showHelp() {
|
|
|
1320
1209
|
writeln(` ${cyan('board')} live leaderboard (refreshes every 30s)`)
|
|
1321
1210
|
writeln(` ${cyan('board --window 7d')} board for a specific window (7d, 30d, 90d, all)`)
|
|
1322
1211
|
writeln(` ${cyan('board --once')} print once and exit`)
|
|
1323
|
-
writeln(` ${cyan('me')} single-platform cascade (claude by default)`)
|
|
1324
|
-
writeln(` ${cyan('me --platform codex')} use a different platform adapter`)
|
|
1325
1212
|
writeln(` ${cyan('compare')} raw pillar audit: tokenpull vs ccusage vs token-dash vs tokscale`)
|
|
1326
1213
|
writeln(` ${cyan('compare --platform codex')} compare for a specific platform`)
|
|
1327
1214
|
writeln(` ${cyan('tui')} full tabbed TUI: Dashboard / Compare / Board / Watch`)
|
|
@@ -1346,7 +1233,6 @@ async function showHelp() {
|
|
|
1346
1233
|
writeln(` ${dim('sigrank board')} # live leaderboard`)
|
|
1347
1234
|
writeln(` ${dim('sigrank compare')} # pillar audit (claude)`)
|
|
1348
1235
|
writeln(` ${dim('sigrank compare --platform codex')}`)
|
|
1349
|
-
writeln(` ${dim('sigrank me --platform codex')}`)
|
|
1350
1236
|
writeln(` ${dim('sigrank watch --window 7d --refresh 60')}`)
|
|
1351
1237
|
writeln(` ${dim('sigrank board --window all --once')}`)
|
|
1352
1238
|
writeln()
|
|
@@ -1423,7 +1309,7 @@ async function runSubmit({ platform = 'claude', window } = {}) {
|
|
|
1423
1309
|
|
|
1424
1310
|
let out
|
|
1425
1311
|
try {
|
|
1426
|
-
out = await callTool('submit_verified', { platform, window: typeof window === 'string' ? window : undefined })
|
|
1312
|
+
out = await callTool('submit_verified', { platform, window: typeof window === 'string' ? window : undefined, dry_run: process.argv.includes('--dry-run') })
|
|
1427
1313
|
} catch (e) {
|
|
1428
1314
|
writeln(red(` ✗ ${e.message}`))
|
|
1429
1315
|
process.exitCode = 1
|
|
@@ -1436,15 +1322,28 @@ async function runSubmit({ platform = 'claude', window } = {}) {
|
|
|
1436
1322
|
}
|
|
1437
1323
|
|
|
1438
1324
|
writeln()
|
|
1325
|
+
let anyRanked = false
|
|
1326
|
+
let anyReceivedNotRanked = false
|
|
1439
1327
|
for (const w of out.windows || []) {
|
|
1440
1328
|
const label = String(w.window).padEnd(8)
|
|
1441
|
-
if (w.status === '
|
|
1442
|
-
writeln(` ${
|
|
1329
|
+
if (w.status === 'dry_run') {
|
|
1330
|
+
writeln(` ${gold('◇')} ${bold(label)} ${gold('DRY RUN')} ${dim('· nothing sent')}`)
|
|
1331
|
+
} else if (w.ranked) {
|
|
1332
|
+
anyRanked = true
|
|
1333
|
+
writeln(` ${green('✓')} ${bold(label)} ${green('RANKED')} ${dim('· live on the board')}`)
|
|
1334
|
+
} else if (w.status === 'received') {
|
|
1335
|
+
anyReceivedNotRanked = true
|
|
1336
|
+
writeln(` ${gold('⚠')} ${bold(label)} ${gold('received · NOT ranked')} ${dim(`(tier=${w.verification_tier || '—'})`)}`)
|
|
1443
1337
|
} else {
|
|
1444
1338
|
writeln(` ${red('✗')} ${bold(label)} ${dim(w.reason || w.status || 'failed')}`)
|
|
1445
1339
|
}
|
|
1446
1340
|
}
|
|
1447
|
-
writeln(
|
|
1341
|
+
writeln()
|
|
1342
|
+
if (anyRanked) {
|
|
1343
|
+
writeln(` ${dim('Your board row:')} ${cyan(`signalaf.com/user/${id.codename}`)}`)
|
|
1344
|
+
} else if (anyReceivedNotRanked) {
|
|
1345
|
+
writeln(` ${gold('Received, but NOT ranked')} ${dim('— this device is unenrolled or revoked. Re-enroll to rank:')} ${cyan('npx sigrank enroll')}`)
|
|
1346
|
+
}
|
|
1448
1347
|
}
|
|
1449
1348
|
|
|
1450
1349
|
export async function runCli(argv) {
|
|
@@ -1453,7 +1352,7 @@ export async function runCli(argv) {
|
|
|
1453
1352
|
|
|
1454
1353
|
// parse --key value AND --key=value flags. E4: validate against the known set and warn
|
|
1455
1354
|
// (don't crash) on anything unknown, so a typo'd flag is visible instead of silently ignored.
|
|
1456
|
-
const KNOWN_FLAGS = new Set(['window', 'platform', 'refresh', 'once', 'submit', 'compare', 'label'])
|
|
1355
|
+
const KNOWN_FLAGS = new Set(['window', 'platform', 'refresh', 'once', 'submit', 'compare', 'label', 'dry-run'])
|
|
1457
1356
|
const flags = {}
|
|
1458
1357
|
for (let i = 1; i < args.length; i++) {
|
|
1459
1358
|
const a = args[i]
|
|
@@ -1485,8 +1384,6 @@ export async function runCli(argv) {
|
|
|
1485
1384
|
once: flags.once === true || flags.once === 'true',
|
|
1486
1385
|
refresh: Number(flags.refresh) || 30,
|
|
1487
1386
|
})
|
|
1488
|
-
} else if (cmd === 'me') {
|
|
1489
|
-
await runMe({ platform: flags.platform ?? 'claude', compare: flags.compare === true || flags.compare === 'true' })
|
|
1490
1387
|
} else if (cmd === 'compare') {
|
|
1491
1388
|
await runCompare({ platform: flags.platform ?? 'claude' })
|
|
1492
1389
|
} else if (cmd === 'tui') {
|
|
@@ -1517,8 +1414,11 @@ export async function runCli(argv) {
|
|
|
1517
1414
|
// default: full unified view
|
|
1518
1415
|
await runSigRank()
|
|
1519
1416
|
} else {
|
|
1520
|
-
// unknown command: show help
|
|
1417
|
+
// unknown command: show help + exit non-zero so typos fail loudly (index.mjs now
|
|
1418
|
+
// routes ALL arg'd invocations here instead of falling through to the MCP server)
|
|
1419
|
+
writeln(red(`\n ✗ unknown command: ${cmd}`))
|
|
1521
1420
|
await showHelp()
|
|
1421
|
+
process.exit(2)
|
|
1522
1422
|
}
|
|
1523
1423
|
} catch (e) {
|
|
1524
1424
|
write(SHOW_CURSOR)
|
package/index.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* CLI shortcuts (optional — the TUI never needs them):
|
|
11
11
|
* npx sigrank enroll sign in: redeem a connect code from signalaf.com
|
|
12
12
|
* npx sigrank submit publish your verified runs to the board
|
|
13
|
-
* npx sigrank board |
|
|
13
|
+
* npx sigrank board | compare | watch read / publish helpers
|
|
14
14
|
* npx sigrank --help full reference
|
|
15
15
|
*
|
|
16
16
|
* For AI clients (NOT human commands): in a piped/non-TTY context this starts an MCP
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
25
25
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
26
|
-
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
26
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'
|
|
27
27
|
import { TOOLS, callTool } from './tools.mjs'
|
|
28
28
|
import { runCli } from './cli.mjs'
|
|
29
29
|
import { readFileSync } from 'node:fs'
|
|
@@ -55,6 +55,11 @@ async function startMcpServer() {
|
|
|
55
55
|
const server = new Server({ name: 'sigrank', version: serverVersion() }, { capabilities: { tools: {} } })
|
|
56
56
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
57
57
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
58
|
+
// Unknown tool name = a client/host bug → JSON-RPC -32602 protocol error, per the MCP
|
|
59
|
+
// spec. isError results (below) are reserved for tools that ran and failed.
|
|
60
|
+
if (!TOOLS.some((t) => t.name === req.params.name)) {
|
|
61
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${req.params.name}`)
|
|
62
|
+
}
|
|
58
63
|
try {
|
|
59
64
|
const out = await callTool(req.params.name, req.params.arguments)
|
|
60
65
|
return { content: [{ type: 'text', text: JSON.stringify(out, null, 2) }] }
|
|
@@ -66,14 +71,14 @@ async function startMcpServer() {
|
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
// Route:
|
|
69
|
-
// any CLI
|
|
74
|
+
// any arg → the CLI (runCli shows help + exits non-zero on unknown commands,
|
|
75
|
+
// so a typo never silently starts a hung MCP stdio server on a TTY)
|
|
70
76
|
// no args + TTY stdout → full tabbed TUI (interactive use)
|
|
71
77
|
// no args + piped → MCP stdio server (AI client use)
|
|
72
78
|
const cliArgs = process.argv.slice(2)
|
|
73
|
-
|
|
74
|
-
if (cliArgs.length > 0 && (CLI_COMMANDS.has(cliArgs[0]) || cliArgs[0].startsWith('--'))) {
|
|
79
|
+
if (cliArgs.length > 0) {
|
|
75
80
|
runCli(process.argv)
|
|
76
|
-
} else if (
|
|
81
|
+
} else if (process.stdout.isTTY) {
|
|
77
82
|
// Interactive terminal — launch the full tabbed TUI
|
|
78
83
|
const { runTui } = await import('./tui.mjs')
|
|
79
84
|
runTui()
|
package/package.json
CHANGED
package/submit.mjs
CHANGED
|
@@ -130,6 +130,20 @@ export async function submitSignedWindow(windowKey, pillars, messages, identity,
|
|
|
130
130
|
const payload = buildPayload(windowKey, pillars, messages, identity, opts.platform || 'claude', opts)
|
|
131
131
|
const signature = signPayload(payload, identity.private_key_pkcs8_b64)
|
|
132
132
|
|
|
133
|
+
// Dry run: build + sign exactly as a real publish, then STOP before the POST.
|
|
134
|
+
// Returns the exact payload that would be sent — the privacy proof ("token
|
|
135
|
+
// counts only") is inspectable, and nothing touches the network.
|
|
136
|
+
if (opts.dryRun) {
|
|
137
|
+
return {
|
|
138
|
+
status: 'dry_run',
|
|
139
|
+
window: WINDOW_TYPE[windowKey] || windowKey,
|
|
140
|
+
would_post: `${apiBase}/api/v1/snapshots`,
|
|
141
|
+
payload,
|
|
142
|
+
signature,
|
|
143
|
+
detail: 'Nothing sent. Re-run without dry_run to publish.',
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
let res
|
|
134
148
|
let ack
|
|
135
149
|
try {
|
|
@@ -157,6 +171,9 @@ export async function submitSignedWindow(windowKey, pillars, messages, identity,
|
|
|
157
171
|
window: WINDOW_TYPE[windowKey] || windowKey,
|
|
158
172
|
verification_tier: ack.verification_tier ?? null,
|
|
159
173
|
persisted: ack.persisted ?? null,
|
|
174
|
+
// ranked = actually on the board (verified + written), not just "received".
|
|
175
|
+
// An unenrolled/revoked device gets HTTP 202 received but is NEVER ranked.
|
|
176
|
+
ranked: !!(res.ok && ack.verification_tier === 'verified' && ack.persisted === true),
|
|
160
177
|
snapshot_hash: payload.agent.snapshot_hash,
|
|
161
178
|
reason: res.ok ? null : ack.reason || ack.status || `http_${res.status}`,
|
|
162
179
|
detail: ack.detail ?? null,
|
package/tokenpull.mjs
CHANGED
|
@@ -41,7 +41,9 @@ const _envPath = `${_localBin}${process.env.PATH ? ':' + process.env.PATH : ''}`
|
|
|
41
41
|
// (ccusage, tokscale) are found even when sigrank isn't globally installed.
|
|
42
42
|
function execFileAsync(cmd, args, timeoutMs) {
|
|
43
43
|
return new Promise((resolve, reject) => {
|
|
44
|
-
|
|
44
|
+
// NOTE: execFile does not accept a `stdio` option (it always pipes + buffers
|
|
45
|
+
// stdout/stderr against maxBuffer) — a previous `stdio` key here was silently ignored.
|
|
46
|
+
execFile(cmd, args, { timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, env: { ...process.env, PATH: _envPath } }, (err, stdout) => {
|
|
45
47
|
if (err) reject(err)
|
|
46
48
|
else resolve(stdout.toString())
|
|
47
49
|
})
|
|
@@ -347,7 +349,10 @@ async function _freshCcusage(platform = 'claude') {
|
|
|
347
349
|
}
|
|
348
350
|
let i = 0, o = 0, cw = 0, cr = 0
|
|
349
351
|
for (const r of rows) {
|
|
350
|
-
i
|
|
352
|
+
i += r.inputTokens ?? r.input_tokens ?? 0
|
|
353
|
+
o += r.outputTokens ?? r.output_tokens ?? 0
|
|
354
|
+
cw += r.cacheCreationTokens ?? r.cache_create_tokens ?? 0
|
|
355
|
+
cr += r.cacheReadTokens ?? r.cache_read_tokens ?? 0
|
|
351
356
|
}
|
|
352
357
|
result['all'] = { input: i, output: o, cacheCreate: cw, cacheRead: cr }
|
|
353
358
|
return result
|
package/tools.mjs
CHANGED
|
@@ -34,7 +34,9 @@ const _envPath = `${_localBin}${process.env.PATH ? ':' + process.env.PATH : ''}`
|
|
|
34
34
|
// are found even when sigrank isn't globally installed.
|
|
35
35
|
function execFileAsync(cmd, args, timeoutMs) {
|
|
36
36
|
return new Promise((resolve, reject) => {
|
|
37
|
-
|
|
37
|
+
// NOTE: execFile does not accept a `stdio` option (it always pipes + buffers
|
|
38
|
+
// stdout/stderr against maxBuffer) — a previous `stdio` key here was silently ignored.
|
|
39
|
+
execFile(cmd, args, { timeout: timeoutMs, maxBuffer: 10 * 1024 * 1024, env: { ...process.env, PATH: _envPath } }, (err, stdout) => {
|
|
38
40
|
if (err) reject(err)
|
|
39
41
|
else resolve(stdout.toString())
|
|
40
42
|
})
|
|
@@ -71,7 +73,10 @@ async function _ccusagePillars(platform = 'claude') {
|
|
|
71
73
|
}
|
|
72
74
|
let i=0,o=0,cw=0,cr=0
|
|
73
75
|
for (const r of rows) {
|
|
74
|
-
i+=r.inputTokens??
|
|
76
|
+
i += r.inputTokens ?? r.input_tokens ?? 0
|
|
77
|
+
o += r.outputTokens ?? r.output_tokens ?? 0
|
|
78
|
+
cw += r.cacheCreationTokens ?? r.cache_create_tokens ?? 0
|
|
79
|
+
cr += r.cacheReadTokens ?? r.cache_read_tokens ?? 0
|
|
75
80
|
}
|
|
76
81
|
result['all'] = { input: i, output: o, cacheCreate: cw, cacheRead: cr }
|
|
77
82
|
return result
|
|
@@ -231,17 +236,19 @@ export const TOOLS = [
|
|
|
231
236
|
all: { type: 'string', description: 'ccusage/tokscale paste or JSON for the all-time window (optional)' },
|
|
232
237
|
source_tool: { type: 'string', enum: ['ccusage', 'tokscale', 'claude_max', 'token_dashboard', 'other'], description: 'which token reader produced the paste (for cross-tool variance tracking)' },
|
|
233
238
|
},
|
|
239
|
+
// at least one window paste is required (runtime check backs this up)
|
|
240
|
+
anyOf: [{ required: ['7d'] }, { required: ['30d'] }, { required: ['90d'] }, { required: ['all'] }],
|
|
234
241
|
},
|
|
235
242
|
},
|
|
236
243
|
{
|
|
237
244
|
name: 'watch_tokenpull',
|
|
238
245
|
description:
|
|
239
|
-
'
|
|
246
|
+
'One poll per call: pulls your local token logs and returns the current cascade for the watched window — the tool never blocks or loops. Re-call at your desired cadence to watch for changes (interval_s is advisory only and echoed back as poll_interval_s). With submit:true (and an enrolled device) each call may also sign + publish the watched window to the board, rate-limited to once per 5 min per platform+window; default is preview-only (no submit).',
|
|
240
247
|
inputSchema: {
|
|
241
248
|
type: 'object',
|
|
242
249
|
properties: {
|
|
243
250
|
platform: { type: 'string', enum: ALL_PLATFORMS, description: 'platform to watch (default: claude)' },
|
|
244
|
-
interval_s: { type: 'number', description: 'poll
|
|
251
|
+
interval_s: { type: 'number', description: 'advisory poll cadence in seconds (default: 60, min: 10) — echoed back as poll_interval_s; does not make the call block or loop' },
|
|
245
252
|
window: { type: 'string', enum: ['7d', '30d', '90d', 'all'], description: 'which window to watch (default: 7d — most sensitive to recent activity)' },
|
|
246
253
|
submit: { type: 'boolean', description: 'auto-submit the watched window to the board as a VERIFIED operator each poll (requires `enroll`; default false = preview only)' },
|
|
247
254
|
},
|
|
@@ -278,12 +285,13 @@ export const TOOLS = [
|
|
|
278
285
|
{
|
|
279
286
|
name: 'submit_verified',
|
|
280
287
|
description:
|
|
281
|
-
'Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine.',
|
|
288
|
+
'Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine. Pass dry_run:true to inspect the exact signed payload without publishing.',
|
|
282
289
|
inputSchema: {
|
|
283
290
|
type: 'object',
|
|
284
291
|
properties: {
|
|
285
292
|
window: { type: 'string', enum: ['7d', '30d', '90d', 'all'], description: 'submit only this window (default: all 4)' },
|
|
286
|
-
platform: { type: 'string', enum: ALL_PLATFORMS, description:
|
|
293
|
+
platform: { type: 'string', enum: [...ALL_PLATFORMS, 'multi'], description: "source platform (default: claude). 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active); empty windows are skipped." },
|
|
294
|
+
dry_run: { type: 'boolean', description: 'build + sign but do NOT publish — returns the exact payload that would be POSTed (token counts only), so you can inspect before submitting' },
|
|
287
295
|
},
|
|
288
296
|
},
|
|
289
297
|
},
|
|
@@ -295,10 +303,30 @@ const WINDOW_TYPE = { '7d': '7d', '30d': '30d', '90d': '90d', all: 'all_time' }
|
|
|
295
303
|
// E3: client-side auto-submit cooldown for watch_tokenpull. The server already dedups
|
|
296
304
|
// identical snapshots (exact hash → 422), but a noisy poll loop with submit:true could still
|
|
297
305
|
// churn the network/board with near-identical rows. Cap auto-submit to once per WATCH_SUBMIT_COOLDOWN_MS
|
|
298
|
-
// per window (in-memory, per process). Keyed by window so different
|
|
306
|
+
// per platform+window (in-memory, per process). Keyed by platform:window so different
|
|
307
|
+
// platforms/windows don't block each other, and only armed on a non-error submit so a
|
|
308
|
+
// network failure doesn't lock out retries for 5 minutes.
|
|
299
309
|
const WATCH_SUBMIT_COOLDOWN_MS = 5 * 60 * 1000
|
|
300
310
|
const _lastWatchSubmitAt = new Map()
|
|
301
311
|
|
|
312
|
+
// ── Shared active-platform loader ───────────────────────────────────────────
|
|
313
|
+
// THE single data path for "show my cascade across platforms" — used by `me`,
|
|
314
|
+
// `watch`, and the TUI Dashboard so they can't drift apart again. Pulls each
|
|
315
|
+
// target platform via the tokenpull tool (enriched: pillars + cascade + card),
|
|
316
|
+
// keeps only platforms with real local data, and sorts claude → codex → rest.
|
|
317
|
+
// Pass `platforms` to scope it (e.g. ['claude'] for a fast first paint).
|
|
318
|
+
export async function pullActivePlatforms({ platforms } = {}, opts = {}) {
|
|
319
|
+
const targets = platforms && platforms.length ? platforms : ALL_PLATFORMS
|
|
320
|
+
const settled = await Promise.allSettled(targets.map((p) => callTool('tokenpull', { platform: p }, opts)))
|
|
321
|
+
const active = settled
|
|
322
|
+
.filter((r) => r.status === 'fulfilled' && r.value)
|
|
323
|
+
.map((r) => r.value)
|
|
324
|
+
.filter((d) => (d.windows || []).some((w) => ((w.pillars?.input ?? 0) + (w.pillars?.output ?? 0)) > 0))
|
|
325
|
+
const rank = (p) => (p === 'claude' ? -2 : p === 'codex' ? -1 : ALL_PLATFORMS.indexOf(p))
|
|
326
|
+
active.sort((a, b) => rank(a.platform) - rank(b.platform))
|
|
327
|
+
return active
|
|
328
|
+
}
|
|
329
|
+
|
|
302
330
|
export async function callTool(name, args, opts = {}) {
|
|
303
331
|
const apiBase = opts.apiBase || DEFAULT_API_BASE
|
|
304
332
|
const timeoutMs = opts.fetchTimeout ?? DEFAULT_FETCH_TIMEOUT
|
|
@@ -330,6 +358,10 @@ export async function callTool(name, args, opts = {}) {
|
|
|
330
358
|
|
|
331
359
|
if (name === 'rank_paste') {
|
|
332
360
|
if (!args?.text) throw new Error('rank_paste requires a non-empty `text` argument.')
|
|
361
|
+
// E2: reject oversized pastes before parsing (parity with submit_paste / rank_windows).
|
|
362
|
+
if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
|
|
363
|
+
return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.` }
|
|
364
|
+
}
|
|
333
365
|
const pillars = parsePillars(args.text)
|
|
334
366
|
const c = withParseWarnings(pillars, cascade(pillars))
|
|
335
367
|
return { ...c, card: narrate(c) }
|
|
@@ -420,7 +452,7 @@ export async function callTool(name, args, opts = {}) {
|
|
|
420
452
|
msgs += w.messages || 0
|
|
421
453
|
}
|
|
422
454
|
if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0) continue // skip empty window
|
|
423
|
-
const r = await submitSignedWindow(wk, sum, msgs, id, { apiBase, fetchImpl: doFetch, platform: 'multi', now: opts.now })
|
|
455
|
+
const r = await submitSignedWindow(wk, sum, msgs, id, { apiBase, fetchImpl: doFetch, platform: 'multi', now: opts.now, dryRun: !!args?.dry_run })
|
|
424
456
|
out.push({ window: wk, pillars: sum, ...r })
|
|
425
457
|
}
|
|
426
458
|
return { platform: 'multi', codename: id.codename, operator_id: id.operator_id, sources: detected.map((d) => d.platform), windows: out }
|
|
@@ -435,6 +467,7 @@ export async function callTool(name, args, opts = {}) {
|
|
|
435
467
|
fetchImpl: doFetch,
|
|
436
468
|
platform: pulled.platform,
|
|
437
469
|
now: opts.now,
|
|
470
|
+
dryRun: !!args?.dry_run,
|
|
438
471
|
})
|
|
439
472
|
out.push({ window: w.window, pillars: w.pillars, ...r })
|
|
440
473
|
}
|
|
@@ -472,7 +505,8 @@ export async function callTool(name, args, opts = {}) {
|
|
|
472
505
|
})
|
|
473
506
|
let ack
|
|
474
507
|
try { ack = await res.json() } catch { ack = { status: 'error', detail: `HTTP ${res.status} (non-JSON response)` } }
|
|
475
|
-
|
|
508
|
+
const ranked = !!(res.ok && ack.verification_tier === 'verified' && ack.persisted === true)
|
|
509
|
+
return { ...c, card, ranked, submission: { ...stamp, httpStatus: res.status, ranked, ...ack } }
|
|
476
510
|
}
|
|
477
511
|
|
|
478
512
|
if (name === 'tokenpull') {
|
|
@@ -512,7 +546,10 @@ export async function callTool(name, args, opts = {}) {
|
|
|
512
546
|
})
|
|
513
547
|
let ack
|
|
514
548
|
try { ack = await res.json() } catch { ack = { status: 'error', detail: `HTTP ${res.status} (non-JSON)` } }
|
|
515
|
-
|
|
549
|
+
// ranked = actually on the board (verified + persisted), not just received — parity
|
|
550
|
+
// with submit_verified (submit.mjs). An unenrolled/revoked device gets 202 but is NEVER ranked.
|
|
551
|
+
const ranked = !!(res.ok && ack.verification_tier === 'verified' && ack.persisted === true)
|
|
552
|
+
out.push({ window: w.window, pillars: w.pillars, cascade: c, card, ranked, submission: { ...stamp, httpStatus: res.status, ranked, ...ack } })
|
|
516
553
|
}
|
|
517
554
|
return { platform: pulled.platform, codename: codename || null, generatedAt: pulled.generatedAt, windows: out }
|
|
518
555
|
}
|
|
@@ -577,21 +614,23 @@ export async function callTool(name, args, opts = {}) {
|
|
|
577
614
|
if (args?.submit === true) {
|
|
578
615
|
const id = opts.identity || ensureIdentity()
|
|
579
616
|
if (id.codename && id.operator_id && id.private_key_pkcs8_b64) {
|
|
580
|
-
// E3: client-side cooldown — at most one auto-submit per window per 5 min.
|
|
581
|
-
// fast poll loop from churning the board even before the server's
|
|
617
|
+
// E3: client-side cooldown — at most one auto-submit per platform+window per 5 min.
|
|
618
|
+
// Prevents a fast poll loop from churning the board even before the server's
|
|
619
|
+
// hash-dedup kicks in. Armed only on a non-error outcome so failed submits retry.
|
|
582
620
|
const clockNow = typeof opts.now === 'number' ? opts.now : Date.now()
|
|
583
|
-
const
|
|
621
|
+
const cdKey = `${pulled.platform}:${watchWindow}`
|
|
622
|
+
const last = _lastWatchSubmitAt.get(cdKey)
|
|
584
623
|
if (last != null && clockNow - last < WATCH_SUBMIT_COOLDOWN_MS) {
|
|
585
624
|
const waitS = Math.ceil((WATCH_SUBMIT_COOLDOWN_MS - (clockNow - last)) / 1000)
|
|
586
|
-
auth_submit = { status: 'cooldown', detail: `auto-submit for '${
|
|
625
|
+
auth_submit = { status: 'cooldown', detail: `auto-submit for '${cdKey}' on cooldown — next in ~${waitS}s (max once / 5 min).`, retry_after_s: waitS }
|
|
587
626
|
} else {
|
|
588
|
-
_lastWatchSubmitAt.set(watchWindow, clockNow)
|
|
589
627
|
auth_submit = await submitSignedWindow(watchWindow, win.pillars, win.messages, id, {
|
|
590
628
|
apiBase,
|
|
591
629
|
fetchImpl: doFetch,
|
|
592
630
|
platform: pulled.platform,
|
|
593
631
|
now: opts.now,
|
|
594
632
|
})
|
|
633
|
+
if (auth_submit?.status !== 'error') _lastWatchSubmitAt.set(cdKey, clockNow)
|
|
595
634
|
}
|
|
596
635
|
} else {
|
|
597
636
|
auth_submit = { status: 'not_enrolled', detail: 'Run `npx sigrank-mcp enroll` to auto-submit verified runs.' }
|
package/tui.mjs
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* - Dashboard + Compare load once, refresh on [R].
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { callTool, DEFAULT_API_BASE } from './tools.mjs'
|
|
15
|
+
import { callTool, DEFAULT_API_BASE, pullActivePlatforms } from './tools.mjs'
|
|
16
|
+
import { ALL_PLATFORMS } from './adapters.mjs'
|
|
16
17
|
import { freshVerifierPillars } from './tokenpull.mjs'
|
|
17
18
|
import { isSignedIn, isCodeChar } from './connect.mjs'
|
|
18
19
|
import { loadIdentity, clearIdentity } from './keystore.mjs'
|
|
@@ -231,54 +232,33 @@ function sparkline(values) {
|
|
|
231
232
|
// still used by the `tokenpull_compare` MCP tool.)
|
|
232
233
|
|
|
233
234
|
async function loadDashboardData() {
|
|
234
|
-
const { tokenpullAny } = await import('./tokenpull.mjs')
|
|
235
|
-
const ALL_PLATFORMS = [
|
|
236
|
-
'claude','codex','amp','gemini','kimi','qwen','goose','kilo',
|
|
237
|
-
'hermes','droid','codebuff','copilot','openclaw','pi',
|
|
238
|
-
]
|
|
239
235
|
const boardPromise = Promise.race([
|
|
240
236
|
fetch(`${DEFAULT_API_BASE}/api/v1/leaderboard?window=30d&metric=yield_`, { headers: { accept: 'application/json' } })
|
|
241
237
|
.then(r => r.ok ? r.json() : null).catch(() => null),
|
|
242
238
|
new Promise(r => setTimeout(() => r(null), 5000)),
|
|
243
239
|
])
|
|
244
240
|
|
|
245
|
-
// FIX 0: progressive load
|
|
246
|
-
// then fill the
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
const
|
|
241
|
+
// FIX 0 + unify (0.14): progressive load via the SHARED loader (pullActivePlatforms) — paint the
|
|
242
|
+
// primary platform (claude) FAST, then fill the rest in fillDashboardRest. This is the SAME loader
|
|
243
|
+
// `me` and `watch` use, so the three views can't drift apart. (Verifier comparison still runs
|
|
244
|
+
// FRESH + on-demand in the Compare tab — see loadCompareData — never on this Dashboard path.)
|
|
245
|
+
const active = await pullActivePlatforms({ platforms: ['claude'] }).catch(() => [])
|
|
250
246
|
|
|
251
|
-
const active = []
|
|
252
|
-
if (claudeResult) {
|
|
253
|
-
const all = claudeResult.windows?.find(w => w.window === 'all')
|
|
254
|
-
if (all && (all.pillars.input ?? 0) + (all.pillars.output ?? 0) > 0) active.push(claudeResult)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// FIX A1 (2026-06-27): removed the 14× SYNCHRONOUS ccusage `verifierMap` build (measured 39,816 ms)
|
|
258
|
-
// that blocked the very first Dashboard paint — and the render never read `verifierMap` or `tdPillars`.
|
|
259
|
-
// (Also dropped the discarded second `tokenDashPillars()` call above.) Verifier comparison
|
|
260
|
-
// (ccusage / tokscale / token-dashboard) now runs FRESH + on-demand in the Compare tab, scoped to
|
|
261
|
-
// active platforms — see loadCompareData. Dashboard paints from tokenpull in ~2s.
|
|
262
247
|
const boardData = await boardPromise
|
|
263
|
-
return { active, verifierMap: {}, tdPillars: null, boardData, _loading: true, _remaining: ALL_PLATFORMS.
|
|
248
|
+
return { active, verifierMap: {}, tdPillars: null, boardData, _loading: true, _remaining: ALL_PLATFORMS.filter(p => p !== 'claude') }
|
|
264
249
|
}
|
|
265
250
|
|
|
266
251
|
/** FIX 0: fill the remaining platforms after the primary render. Mutates dashData.active. */
|
|
267
252
|
async function fillDashboardRest(dashData) {
|
|
268
253
|
if (!dashData?._remaining) return false
|
|
269
|
-
|
|
270
|
-
const rest = await
|
|
271
|
-
for (const
|
|
272
|
-
if (r.status !== 'fulfilled') continue
|
|
273
|
-
const d = r.value
|
|
274
|
-
const all = d.windows?.find(w => w.window === 'all')
|
|
275
|
-
if (!all) continue
|
|
276
|
-
if ((all.pillars.input ?? 0) + (all.pillars.output ?? 0) === 0) continue
|
|
254
|
+
// Unify: fill the remaining platforms through the SAME shared loader.
|
|
255
|
+
const rest = await pullActivePlatforms({ platforms: dashData._remaining }).catch(() => [])
|
|
256
|
+
for (const d of rest) {
|
|
277
257
|
if (!dashData.active.find(a => a.platform === d.platform)) dashData.active.push(d)
|
|
278
258
|
}
|
|
279
|
-
//
|
|
280
|
-
const
|
|
281
|
-
dashData.active.sort((a, b) =>
|
|
259
|
+
// stable display order: claude → codex → rest (matches the shared loader)
|
|
260
|
+
const rank = (p) => (p === 'claude' ? -2 : p === 'codex' ? -1 : ALL_PLATFORMS.indexOf(p))
|
|
261
|
+
dashData.active.sort((a, b) => rank(a.platform) - rank(b.platform))
|
|
282
262
|
dashData._loading = false
|
|
283
263
|
dashData._remaining = null
|
|
284
264
|
return true
|