sigrank-mcp 0.6.4 → 0.6.6

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 (2) hide show
  1. package/cli.mjs +325 -21
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -3,23 +3,28 @@
3
3
  *
4
4
  * Commands (no external deps — pure Node.js ANSI escape codes):
5
5
  *
6
- * npx sigrank-mcp board live leaderboard, refreshes every 30s
7
- * npx sigrank-mcp board --window 7d board for a specific window
8
- * npx sigrank-mcp board --once print once and exit (no live refresh)
9
- * npx sigrank-mcp me your cascade across all 4 windows
10
- * npx sigrank-mcp me --platform amp use a different platform adapter
11
- * npx sigrank-mcp watch RT tune meter local cascade, refreshes
12
- * npx sigrank-mcp watch --window 7d watch a specific window
6
+ * npx sigrank-mcp board live leaderboard, refreshes every 30s
7
+ * npx sigrank-mcp board --window 7d board for a specific window
8
+ * npx sigrank-mcp board --once print once and exit (no live refresh)
9
+ * npx sigrank-mcp me your cascade across all 4 windows
10
+ * npx sigrank-mcp me --platform amp use a different platform adapter
11
+ * npx sigrank-mcp me --compare raw pillar comparison: ccusage vs tokenpull vs token-dashboard
12
+ * npx sigrank-mcp watch RT tune meter local cascade, refreshes
13
+ * npx sigrank-mcp watch --window 7d watch a specific window
13
14
  *
14
15
  * Color palette mirrors the SigRank web dark theme:
15
16
  * gold = class TRANSMITTER headline + rank #1
16
17
  * cyan = active metrics / your row highlight
17
18
  * dim = secondary data, separators
18
- * red = negative movement
19
+ * red = negative movement / delta
19
20
  * green = positive movement
20
21
  */
21
22
 
22
23
  import { callTool, DEFAULT_API_BASE } from './tools.mjs'
24
+ import { execSync } from 'child_process'
25
+ import { existsSync, readFileSync } from 'fs'
26
+ import os from 'os'
27
+ import path from 'path'
23
28
 
24
29
  // ── ANSI helpers (no chalk dep) ────────────────────────────────────────────
25
30
  const ESC = '\x1b['
@@ -283,10 +288,10 @@ async function runBoard({ window = '30d', once = false, refresh = 30 } = {}) {
283
288
 
284
289
  // ── ME command ───────────────────────────────────────────────────────────────
285
290
 
286
- async function runMe({ platform = 'claude' } = {}) {
291
+ async function runMe({ platform = 'claude', compare = false } = {}) {
292
+ if (compare) return runCompare({ platform })
293
+
287
294
  write(HIDE_CURSOR)
288
- writeln()
289
- writeln(` ${gold('⊙ SigRank')} ${bold('Your Cascade')} ${dim(`platform: ${platform}`)}`)
290
295
  writeln(` ${dim('reading local token logs…')}`)
291
296
 
292
297
  let pulled
@@ -294,6 +299,7 @@ async function runMe({ platform = 'claude' } = {}) {
294
299
  pulled = await callTool('tokenpull', { platform })
295
300
  } catch (e) {
296
301
  write(SHOW_CURSOR)
302
+ write(CURSOR_UP(1) + ERASE_LINE)
297
303
  writeln(red(` ✗ ${e.message}`))
298
304
  process.exit(1)
299
305
  }
@@ -302,6 +308,7 @@ async function runMe({ platform = 'claude' } = {}) {
302
308
  write(CURSOR_UP(1) + ERASE_LINE)
303
309
 
304
310
  const w = termWidth()
311
+ writeln()
305
312
  writeln(` ${gold('⊙ SigRank')} ${bold('Your Cascade')} ${dim(`platform: ${pulled.platform ?? platform}`)}`)
306
313
  if (pulled.estimated) writeln(` ${dim('⚠ estimated values (cache data unavailable for this platform)')}`)
307
314
  writeln(` ${dim('─'.repeat(w - 4))}`)
@@ -380,6 +387,302 @@ async function runMe({ platform = 'claude' } = {}) {
380
387
  write(SHOW_CURSOR)
381
388
  }
382
389
 
390
+ // ── COMPARE command ───────────────────────────────────────────────────────────
391
+ // Side-by-side: ccusage (JSON) vs tokenpull vs token-dashboard (SQLite)
392
+
393
+ function ccusagePillars(platform = 'claude') {
394
+ // ccusage <platform> daily --json → sum by window
395
+ try {
396
+ const cmd = platform === 'claude' ? 'ccusage claude daily --json' : `ccusage ${platform} daily --json`
397
+ const raw = execSync(cmd, { timeout: 15000, stdio: ['ignore', 'pipe', 'ignore'] }).toString()
398
+ const data = JSON.parse(raw)
399
+ const rows = data.daily ?? data // ccusage may return {daily:[...]} or [...]
400
+
401
+ const now = Date.now()
402
+ const cutoff = { '7d': 7, '30d': 30, '90d': 90 }
403
+ const result = {}
404
+
405
+ for (const [win, days] of Object.entries(cutoff)) {
406
+ const since = new Date(now - days * 86400000)
407
+ let input = 0, output = 0, cacheCreate = 0, cacheRead = 0
408
+ for (const row of rows) {
409
+ const d = new Date(row.date ?? row.day ?? row.week ?? '1970-01-01')
410
+ if (d >= since) {
411
+ input += row.inputTokens ?? row.input_tokens ?? 0
412
+ output += row.outputTokens ?? row.output_tokens ?? 0
413
+ cacheCreate += row.cacheCreationTokens ?? row.cache_create_tokens ?? 0
414
+ cacheRead += row.cacheReadTokens ?? row.cache_read_tokens ?? 0
415
+ }
416
+ }
417
+ result[win] = { input, output, cacheCreate, cacheRead }
418
+ }
419
+ // all-time = sum everything
420
+ let input = 0, output = 0, cacheCreate = 0, cacheRead = 0
421
+ for (const row of rows) {
422
+ input += row.inputTokens ?? row.input_tokens ?? 0
423
+ output += row.outputTokens ?? row.output_tokens ?? 0
424
+ cacheCreate += row.cacheCreationTokens ?? row.cache_create_tokens ?? 0
425
+ cacheRead += row.cacheReadTokens ?? row.cache_read_tokens ?? 0
426
+ }
427
+ result['all'] = { input, output, cacheCreate, cacheRead }
428
+ return result
429
+ } catch {
430
+ return null
431
+ }
432
+ }
433
+
434
+ function tokscalePillars() {
435
+ // Read tokscale_report.json — claude client only, all-time only (no timestamps in export)
436
+ const reportPath = path.join(os.homedir(), 'tokscale_report.json')
437
+ if (!existsSync(reportPath)) return null
438
+ try {
439
+ const data = JSON.parse(readFileSync(reportPath, 'utf8'))
440
+ const entries = data.entries ?? []
441
+ const claude = entries.filter(e =>
442
+ e.client === 'claude' &&
443
+ e.model !== '<synthetic>' && e.model !== 'unknown' &&
444
+ e.provider !== 'unknown'
445
+ )
446
+ const p = claude.reduce((acc, e) => ({
447
+ input: acc.input + (e.input ?? 0),
448
+ output: acc.output + (e.output ?? 0),
449
+ cacheCreate: acc.cacheCreate + (e.cacheWrite ?? 0),
450
+ cacheRead: acc.cacheRead + (e.cacheRead ?? 0),
451
+ }), { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 })
452
+ // tokscale export has no timestamps → only all-time available
453
+ return { all: p }
454
+ } catch { return null }
455
+ }
456
+
457
+ function appPillars() {
458
+ // App numbers from screenshots — all-time, per model (no cache fields)
459
+ // Hard-coded from 2026-06-23 screenshot capture (update when re-screenshotted)
460
+ return {
461
+ all: {
462
+ input: 6_378_000, // sum of all models: 5.6M + 102.1K + 92.9K + 130.3K + 418.9K + 33.5K
463
+ output: 38_682_400, // sum: 19.6M + 6.5M + 5.4M + 6.6M + 292.4K + 290.4K
464
+ cacheCreate: null, // not shown in App UI
465
+ cacheRead: null, // not shown in App UI
466
+ },
467
+ _note: 'App UI — all-time, per-model sum from screenshots 2026-06-23. No cache fields. Update when re-screenshotted.',
468
+ _perModel: [
469
+ { model: 'claude-opus-4-8', input: 5_600_000, output: 19_600_000 },
470
+ { model: 'claude-sonnet-4-5',input: 102_100, output: 6_500_000 },
471
+ { model: 'claude-sonnet-4-6',input: 92_900, output: 5_400_000 },
472
+ { model: 'claude-opus-4-7', input: 130_300, output: 6_600_000 },
473
+ { model: 'claude-fable-5', input: 418_900, output: 292_400 },
474
+ { model: 'claude-haiku-4-5', input: 33_500, output: 290_400 },
475
+ ],
476
+ }
477
+ }
478
+
479
+ function tokenDashPillars() {
480
+ // query token-dashboard SQLite directly
481
+ const dbPath = path.join(os.homedir(), '.claude', 'token-dashboard.db')
482
+ if (!existsSync(dbPath)) return null
483
+ try {
484
+ const claudeFilter = `(model LIKE '%claude%' OR model LIKE '%fable%' OR model LIKE '%sonnet%' OR model LIKE '%opus%' OR model LIKE '%haiku%')`
485
+ const now = new Date()
486
+ const result = {}
487
+ for (const [win, days] of [['7d', 7], ['30d', 30], ['90d', 90]]) {
488
+ const since = new Date(now - days * 86400000).toISOString()
489
+ const cmd = `python3 -c "
490
+ import sqlite3, json
491
+ db = sqlite3.connect('${dbPath}')
492
+ r = db.execute('''SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens+cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages WHERE timestamp>=? AND ${claudeFilter}''',(('${since}',))).fetchone()
493
+ print(json.dumps({'input':r[0] or 0,'output':r[1] or 0,'cacheCreate':r[2] or 0,'cacheRead':r[3] or 0}))
494
+ "`
495
+ const raw = execSync(cmd, { timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
496
+ result[win] = JSON.parse(raw)
497
+ }
498
+ // all-time
499
+ const cmd = `python3 -c "
500
+ import sqlite3, json
501
+ db = sqlite3.connect('${dbPath}')
502
+ r = db.execute('SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens+cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages WHERE ${claudeFilter}').fetchone()
503
+ print(json.dumps({'input':r[0] or 0,'output':r[1] or 0,'cacheCreate':r[2] or 0,'cacheRead':r[3] or 0}))
504
+ "`
505
+ const raw = execSync(cmd, { timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
506
+ result['all'] = JSON.parse(raw)
507
+ return result
508
+ } catch {
509
+ return null
510
+ }
511
+ }
512
+
513
+ function fmtDelta(a, b) {
514
+ if (a == null || b == null) return dim(' —')
515
+ const d = b - a
516
+ if (d === 0) return dim(' =')
517
+ const pct = a !== 0 ? `${d > 0 ? '+' : ''}${((d / a) * 100).toFixed(1)}%` : ''
518
+ const abs = `${d > 0 ? '+' : ''}${fmtTokens(Math.abs(d))}`
519
+ const label = `${abs} ${pct}`
520
+ return d > 0 ? green(label) : red(label)
521
+ }
522
+
523
+ // Compute cascade metrics from raw pillars (mirrors bridge.ts computeCascadeMetrics)
524
+ function cascadeFromPillars(p) {
525
+ if (!p) return null
526
+ const i = p.input ?? 0
527
+ const o = p.output ?? 0
528
+ const cw = p.cacheCreate ?? 0
529
+ const cr = p.cacheRead ?? 0
530
+ if (i === 0 && o === 0) return null
531
+ const safeI = Math.max(i, 1)
532
+ const total = i + o + cw + cr
533
+ const velocity = o / safeI
534
+ const leverage = cr / safeI
535
+ const yield_ = leverage * velocity
536
+ const snr = (i + o) > 0 ? o / (i + o) : 0
537
+ // dev10x = log10(T × C × R) — only when all four pillars present
538
+ let dev10x = null
539
+ if (cw > 0 && o > 0 && i > 0 && cr > 0) {
540
+ const T = o / i, C = cw / o, R = cr / cw
541
+ dev10x = Math.log10(T * C * R)
542
+ }
543
+ // efficiency = ((cr+cw+o)/i) / 4.0
544
+ const efficiency = ((cr + cw + o) / safeI) / 4.0
545
+ const cls = yield_ > 500 ? 'TRANSMITTER' : yield_ > 400 ? 'ARCH+' : yield_ > 300 ? 'ARCH' : yield_ > 150 ? 'POWER' : 'BASE'
546
+ return { yield: yield_, velocity, leverage, snr, dev10x, efficiency, class: cls, total }
547
+ }
548
+
549
+ async function runCompare({ platform = 'claude' } = {}) {
550
+ write(HIDE_CURSOR)
551
+
552
+ // Pull all five sources in parallel
553
+ writeln(` ${dim('reading all 5 sources…')}`)
554
+ const [ccPillars, tpData, tdPillars, tsPillars, apPillars] = await Promise.all([
555
+ Promise.resolve(ccusagePillars(platform)),
556
+ callTool('tokenpull', { platform }).catch(() => null),
557
+ Promise.resolve(tokenDashPillars()),
558
+ Promise.resolve(tokscalePillars()),
559
+ Promise.resolve(appPillars()),
560
+ ])
561
+ write(CURSOR_UP(1) + ERASE_LINE)
562
+
563
+ const w = termWidth()
564
+ const WINS = ['7d', '30d', '90d', 'all']
565
+ const WIN_LABEL = { '7d': '7d', '30d': '30d', '90d': '90d', 'all': 'all-time' }
566
+
567
+ // build tokenpull pillar lookup
568
+ const tpPillars = {}
569
+ for (const win of (tpData?.windows ?? [])) {
570
+ tpPillars[win.window] = win.pillars
571
+ }
572
+
573
+ // sources: name, color, pillars-by-window, note
574
+ const SOURCES = [
575
+ { name: 'tokenpull', color: cyan, pillars: tpPillars, note: 'JSONL deduped by msg id · canon source' },
576
+ { name: 'ccusage', color: (s) => paint(c.green, s), pillars: ccPillars ?? {}, note: 'ccusage claude subcommand · monthly only' },
577
+ { name: 'token-dash', color: (s) => paint(c.magenta,s), pillars: tdPillars ?? {}, note: 'SQLite — double-counts sessions · use with caution' },
578
+ { name: 'tokscale', color: (s) => paint(c.blue, s), pillars: tsPillars ?? {}, note: 'all-time only · partial export (~5% of opus-4-8)' },
579
+ { name: 'App', color: gold, pillars: apPillars ?? {}, note: 'screenshots 2026-06-23 · no cache fields · update manually' },
580
+ ]
581
+
582
+ writeln()
583
+ writeln(` ${gold('⊙ SigRank')} ${bold('5-Source Comparison')} ${dim(`platform: ${platform} · claude only`)}`)
584
+ writeln(` ${dim('─'.repeat(w - 4))}`)
585
+
586
+ // ── PILLARS TABLE ──────────────────────────────────────────────────────────
587
+ const PILLARS = [
588
+ { key: 'input', label: 'Input' },
589
+ { key: 'output', label: 'Output' },
590
+ { key: 'cacheCreate', label: 'Cache Write' },
591
+ { key: 'cacheRead', label: 'Cache Read' },
592
+ ]
593
+
594
+ for (const { key, label } of PILLARS) {
595
+ writeln()
596
+ writeln(` ${bold(label)}`)
597
+ const hcols = [padEnd(dim('Source'), 14), ...WINS.map(win => padStart(dim(WIN_LABEL[win]), 13))]
598
+ writeln(` ${hcols.join(' ')}`)
599
+ writeln(` ${dim('·'.repeat(Math.min(w - 4, 14 + WINS.length * 15)))}`)
600
+
601
+ for (const src of SOURCES) {
602
+ const vals = WINS.map(win => {
603
+ const p = src.pillars[win]
604
+ const v = p?.[key]
605
+ if (v == null) return padStart(dim('—'), 13)
606
+ return padStart(fmtTokens(v), 13)
607
+ })
608
+ writeln(` ${padEnd(src.color(src.name), 14)} ${vals.join(' ')}`)
609
+ }
610
+ }
611
+
612
+ // ── SIGNATURE TABLE ────────────────────────────────────────────────────────
613
+ writeln()
614
+ writeln(` ${dim('─'.repeat(w - 4))}`)
615
+ writeln(` ${bold('Cascade Signature')} ${dim('per source · all windows where data available')}`)
616
+ writeln()
617
+
618
+ const SIG_METRICS = [
619
+ { key: 'yield', label: 'Υ Yield', fmt: v => fmtYield(v), w: 9 },
620
+ { key: 'velocity', label: 'Vel', fmt: v => v.toFixed(2), w: 6 },
621
+ { key: 'leverage', label: 'Lev', fmt: v => `${fmtLev(v)}×`, w: 7 },
622
+ { key: 'snr', label: 'SNR', fmt: v => fmtSNR(v), w: 6 },
623
+ { key: 'dev10x', label: '10x', fmt: v => v.toFixed(2), w: 5 },
624
+ { key: 'efficiency', label: 'Eff', fmt: v => v.toFixed(1), w: 6 },
625
+ { key: 'class', label: 'Class', fmt: v => colorClass(v), w: 12 },
626
+ ]
627
+
628
+ // header
629
+ const sigHdr = [
630
+ padEnd(dim('Source'), 14),
631
+ padEnd(dim('Window'), 8),
632
+ ...SIG_METRICS.map(m => padStart(dim(m.label), m.w)),
633
+ ]
634
+ writeln(` ${sigHdr.join(' ')}`)
635
+ writeln(` ${dim('·'.repeat(Math.min(w - 4, 80)))}`)
636
+
637
+ for (const src of SOURCES) {
638
+ const availWins = WINS.filter(win => src.pillars[win] != null)
639
+ if (availWins.length === 0) {
640
+ writeln(` ${padEnd(src.color(src.name), 14)} ${dim('no data')}`)
641
+ continue
642
+ }
643
+ let first = true
644
+ for (const win of availWins) {
645
+ const p = src.pillars[win]
646
+ // For tokenpull, use the pre-computed cascade from the tool if available
647
+ let cas
648
+ if (src.name === 'tokenpull') {
649
+ const tpWin = tpData?.windows?.find(ww => ww.window === win)
650
+ cas = tpWin?.cascade ? {
651
+ yield: tpWin.cascade.yield,
652
+ velocity: tpWin.cascade.velocity,
653
+ leverage: tpWin.cascade.leverage,
654
+ snr: tpWin.cascade.snr,
655
+ dev10x: tpWin.cascade.dev10x,
656
+ efficiency: null,
657
+ class: tpWin.cascade.class,
658
+ } : cascadeFromPillars(p)
659
+ } else {
660
+ cas = cascadeFromPillars(p)
661
+ }
662
+
663
+ const srcLabel = first ? src.color(src.name) : ' '.repeat(stripAnsi(src.name).length)
664
+ const winLabel = win === 'all' ? bold('all-time') : win
665
+ const sigCols = SIG_METRICS.map(m => {
666
+ const v = cas?.[m.key]
667
+ return padStart(v != null ? m.fmt(v) : dim('—'), m.w)
668
+ })
669
+ writeln(` ${padEnd(srcLabel, 14)} ${padEnd(winLabel, 8)} ${sigCols.join(' ')}`)
670
+ first = false
671
+ }
672
+ }
673
+
674
+ // ── NOTES ─────────────────────────────────────────────────────────────────
675
+ writeln()
676
+ writeln(` ${dim('─'.repeat(w - 4))}`)
677
+ for (const src of SOURCES) {
678
+ writeln(` ${src.color(src.name.padEnd(12))} ${dim(src.note)}`)
679
+ }
680
+ writeln(` ${dim('Eff = ((cacheRead+cacheWrite+output)/input)/4.0 vs AA baseline')}`)
681
+ writeln(` ${dim('App has no cache fields → Υ/Lev/Eff/10x unavailable from App source')}`)
682
+ writeln()
683
+ write(SHOW_CURSOR)
684
+ }
685
+
383
686
  // ── WATCH command ─────────────────────────────────────────────────────────────
384
687
 
385
688
  async function runWatch({ platform = 'claude', window: win = '7d', refresh = 30 } = {}) {
@@ -446,16 +749,17 @@ async function runWatch({ platform = 'claude', window: win = '7d', refresh = 30
446
749
 
447
750
  function showHelp() {
448
751
  writeln()
449
- writeln(` ${gold('⊙ SigRank')} ${bold('CLI')} ${dim('v0.6.4')}`)
752
+ writeln(` ${gold('⊙ SigRank')} ${bold('CLI')} ${dim('v0.6.5')}`)
450
753
  writeln()
451
754
  writeln(` ${bold('Commands')}`)
452
- writeln(` ${cyan('board')} live leaderboard (refreshes every 30s)`)
453
- writeln(` ${cyan('board --window 7d')} board for a specific window (7d, 30d, 90d, all_time)`)
454
- writeln(` ${cyan('board --once')} print once and exit`)
455
- writeln(` ${cyan('me')} your cascade across all 4 time windows`)
456
- writeln(` ${cyan('me --platform amp')} use a different platform adapter`)
457
- writeln(` ${cyan('watch')} live tune meter re-reads local logs every 30s`)
458
- writeln(` ${cyan('watch --window 7d')} watch a specific window`)
755
+ writeln(` ${cyan('board')} live leaderboard (refreshes every 30s)`)
756
+ writeln(` ${cyan('board --window 7d')} board for a specific window (7d, 30d, 90d, all_time)`)
757
+ writeln(` ${cyan('board --once')} print once and exit`)
758
+ writeln(` ${cyan('me')} your cascade across all 4 time windows`)
759
+ writeln(` ${cyan('me --platform amp')} use a different platform adapter`)
760
+ writeln(` ${cyan('me --compare')} raw pillar comparison: ccusage vs tokenpull vs token-dashboard`)
761
+ writeln(` ${cyan('watch')} live tune meter re-reads local logs every 30s`)
762
+ writeln(` ${cyan('watch --window 7d')} watch a specific window`)
459
763
  writeln()
460
764
  writeln(` ${bold('Options')}`)
461
765
  writeln(` ${dim('--window')} 7d · 30d · 90d · all_time (default: 30d for board, 7d for watch)`)
@@ -498,7 +802,7 @@ export async function runCli(argv) {
498
802
  refresh: Number(flags.refresh) || 30,
499
803
  })
500
804
  } else if (cmd === 'me') {
501
- await runMe({ platform: flags.platform ?? 'claude' })
805
+ await runMe({ platform: flags.platform ?? 'claude', compare: flags.compare === true || flags.compare === 'true' })
502
806
  } else if (cmd === 'watch') {
503
807
  await runWatch({
504
808
  platform: flags.platform ?? 'claude',
@@ -508,7 +812,7 @@ export async function runCli(argv) {
508
812
  } else if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
509
813
  showHelp()
510
814
  } else if (cmd === '--version' || cmd === '-v') {
511
- writeln('0.6.4')
815
+ writeln('0.6.5')
512
816
  } else {
513
817
  // unknown command: show help
514
818
  showHelp()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank-mcp",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
5
5
  "type": "module",
6
6
  "bin": {