sigrank-mcp 0.6.4 → 0.6.5

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 +243 -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 } 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,220 @@ 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 tokenDashPillars() {
435
+ // query token-dashboard SQLite directly
436
+ const dbPath = path.join(os.homedir(), '.claude', 'token-dashboard.db')
437
+ if (!existsSync(dbPath)) return null
438
+ try {
439
+ const claudeFilter = `(model LIKE '%claude%' OR model LIKE '%fable%' OR model LIKE '%sonnet%' OR model LIKE '%opus%' OR model LIKE '%haiku%')`
440
+ const now = new Date()
441
+ const result = {}
442
+ for (const [win, days] of [['7d', 7], ['30d', 30], ['90d', 90]]) {
443
+ const since = new Date(now - days * 86400000).toISOString()
444
+ const cmd = `python3 -c "
445
+ import sqlite3, json
446
+ db = sqlite3.connect('${dbPath}')
447
+ 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()
448
+ print(json.dumps({'input':r[0] or 0,'output':r[1] or 0,'cacheCreate':r[2] or 0,'cacheRead':r[3] or 0}))
449
+ "`
450
+ const raw = execSync(cmd, { timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
451
+ result[win] = JSON.parse(raw)
452
+ }
453
+ // all-time
454
+ const cmd = `python3 -c "
455
+ import sqlite3, json
456
+ db = sqlite3.connect('${dbPath}')
457
+ 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()
458
+ print(json.dumps({'input':r[0] or 0,'output':r[1] or 0,'cacheCreate':r[2] or 0,'cacheRead':r[3] or 0}))
459
+ "`
460
+ const raw = execSync(cmd, { timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
461
+ result['all'] = JSON.parse(raw)
462
+ return result
463
+ } catch {
464
+ return null
465
+ }
466
+ }
467
+
468
+ function fmtDelta(a, b) {
469
+ if (a == null || b == null) return dim(' —')
470
+ const d = b - a
471
+ if (d === 0) return dim(' =')
472
+ const pct = a !== 0 ? `${d > 0 ? '+' : ''}${((d / a) * 100).toFixed(1)}%` : ''
473
+ const abs = `${d > 0 ? '+' : ''}${fmtTokens(Math.abs(d))}`
474
+ const label = `${abs} ${pct}`
475
+ return d > 0 ? green(label) : red(label)
476
+ }
477
+
478
+ async function runCompare({ platform = 'claude' } = {}) {
479
+ write(HIDE_CURSOR)
480
+ writeln(` ${dim('reading ccusage…')}`)
481
+ const ccPillars = ccusagePillars(platform)
482
+ write(CURSOR_UP(1) + ERASE_LINE)
483
+
484
+ writeln(` ${dim('reading tokenpull…')}`)
485
+ let tpData
486
+ try { tpData = await callTool('tokenpull', { platform }) } catch { tpData = null }
487
+ write(CURSOR_UP(1) + ERASE_LINE)
488
+
489
+ writeln(` ${dim('reading token-dashboard…')}`)
490
+ const tdPillars = tokenDashPillars()
491
+ write(CURSOR_UP(1) + ERASE_LINE)
492
+
493
+ const w = termWidth()
494
+ const WINS = ['7d', '30d', '90d', 'all']
495
+ const WIN_LABEL = { '7d': '7d', '30d': '30d', '90d': '90d', 'all': 'all-time' }
496
+
497
+ // build tokenpull pillar lookup
498
+ const tpPillars = {}
499
+ for (const win of (tpData?.windows ?? [])) {
500
+ tpPillars[win.window] = win.pillars
501
+ }
502
+
503
+ writeln()
504
+ writeln(` ${gold('⊙ SigRank')} ${bold('Source Comparison')} ${dim(`platform: ${platform} · claude tokens only`)}`)
505
+ writeln(` ${dim('─'.repeat(w - 4))}`)
506
+
507
+ const PILLARS = [
508
+ { key: 'input', label: 'Input' },
509
+ { key: 'output', label: 'Output' },
510
+ { key: 'cacheCreate', label: 'Cache Create' },
511
+ { key: 'cacheRead', label: 'Cache Read' },
512
+ ]
513
+
514
+ for (const { key, label } of PILLARS) {
515
+ writeln()
516
+ writeln(` ${bold(label)}`)
517
+
518
+ // header row
519
+ const hcols = [
520
+ padEnd(dim('Source'), 16),
521
+ ...WINS.map(win => padStart(dim(WIN_LABEL[win]), 14))
522
+ ]
523
+ writeln(` ${hcols.join(' ')}`)
524
+ writeln(` ${dim('·'.repeat(Math.min(w - 4, 16 + WINS.length * 16)))}`)
525
+
526
+ // ccusage row
527
+ if (ccPillars) {
528
+ const cols = [padEnd(cyan('ccusage'), 16), ...WINS.map(win => padStart(fmtTokens(ccPillars[win]?.[key] ?? 0), 14))]
529
+ writeln(` ${cols.join(' ')}`)
530
+ } else {
531
+ writeln(` ${padEnd(dim('ccusage'), 16)} ${dim('not found')}`)
532
+ }
533
+
534
+ // tokenpull row
535
+ if (tpData) {
536
+ const cols = [padEnd(cyan('tokenpull'), 16), ...WINS.map(win => padStart(fmtTokens(tpPillars[win]?.[key] ?? 0), 14))]
537
+ writeln(` ${cols.join(' ')}`)
538
+ } else {
539
+ writeln(` ${padEnd(dim('tokenpull'), 16)} ${dim('not found')}`)
540
+ }
541
+
542
+ // token-dashboard row
543
+ if (tdPillars) {
544
+ const cols = [padEnd(cyan('token-dash'), 16), ...WINS.map(win => padStart(fmtTokens(tdPillars[win]?.[key] ?? 0), 14))]
545
+ writeln(` ${cols.join(' ')}`)
546
+ } else {
547
+ writeln(` ${padEnd(dim('token-dash'), 16)} ${dim('not found — run: python3 ~/token-dashboard/cli.py scan')}`)
548
+ }
549
+
550
+ // delta row: tokenpull vs token-dash (the two most comparable)
551
+ if (tpData && tdPillars) {
552
+ const cols = [
553
+ padEnd(dim('Δ tp→td'), 16),
554
+ ...WINS.map(win => {
555
+ const a = tpPillars[win]?.[key] ?? 0
556
+ const b = tdPillars[win]?.[key] ?? 0
557
+ return padStart(fmtDelta(a, b), 14)
558
+ })
559
+ ]
560
+ writeln(` ${cols.join(' ')}`)
561
+ }
562
+ }
563
+
564
+ // SigRank cascade section
565
+ writeln()
566
+ writeln(` ${dim('─'.repeat(w - 4))}`)
567
+ writeln(` ${bold('SigRank Cascade')} ${dim('(tokenpull → cascade math)')}`)
568
+ writeln()
569
+
570
+ const CASCADE_ROWS = [
571
+ { key: 'yield', label: 'Υ Yield', fmt: v => fmtYield(v) },
572
+ { key: 'snr', label: 'SNR', fmt: v => fmtSNR(v) },
573
+ { key: 'leverage', label: 'Leverage', fmt: v => `${fmtLev(v)}×` },
574
+ { key: 'velocity', label: 'Velocity', fmt: v => v.toFixed(2) },
575
+ { key: 'dev10x', label: '10xDEV', fmt: v => v.toFixed(2) },
576
+ { key: 'class', label: 'Class', fmt: v => colorClass(v) },
577
+ ]
578
+
579
+ const hcols = [padEnd(dim('Metric'), 12), ...WINS.map(win => padStart(dim(WIN_LABEL[win]), 12))]
580
+ writeln(` ${hcols.join(' ')}`)
581
+ writeln(` ${dim('·'.repeat(Math.min(w - 4, 12 + WINS.length * 14)))}`)
582
+
583
+ for (const { key, label, fmt } of CASCADE_ROWS) {
584
+ const cols = [
585
+ padEnd(dim(label), 12),
586
+ ...WINS.map(win => {
587
+ const cas = tpPillars[win] ? (tpData?.windows?.find(w => w.window === win)?.cascade) : null
588
+ const v = cas?.[key]
589
+ return padStart(v != null ? fmt(v) : dim('—'), 12)
590
+ })
591
+ ]
592
+ writeln(` ${cols.join(' ')}`)
593
+ }
594
+
595
+ // estimated rank hint
596
+ writeln()
597
+ writeln(` ${dim('─'.repeat(w - 4))}`)
598
+ writeln(` ${dim('note: token-dash has longer history (SQLite); tokenpull reads live JSONL only')}`)
599
+ writeln(` ${dim('note: ccusage = primary pillar source (all agents); tokenpull = claude-only cascade input')}`)
600
+ writeln()
601
+ write(SHOW_CURSOR)
602
+ }
603
+
383
604
  // ── WATCH command ─────────────────────────────────────────────────────────────
384
605
 
385
606
  async function runWatch({ platform = 'claude', window: win = '7d', refresh = 30 } = {}) {
@@ -446,16 +667,17 @@ async function runWatch({ platform = 'claude', window: win = '7d', refresh = 30
446
667
 
447
668
  function showHelp() {
448
669
  writeln()
449
- writeln(` ${gold('⊙ SigRank')} ${bold('CLI')} ${dim('v0.6.4')}`)
670
+ writeln(` ${gold('⊙ SigRank')} ${bold('CLI')} ${dim('v0.6.5')}`)
450
671
  writeln()
451
672
  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`)
673
+ writeln(` ${cyan('board')} live leaderboard (refreshes every 30s)`)
674
+ writeln(` ${cyan('board --window 7d')} board for a specific window (7d, 30d, 90d, all_time)`)
675
+ writeln(` ${cyan('board --once')} print once and exit`)
676
+ writeln(` ${cyan('me')} your cascade across all 4 time windows`)
677
+ writeln(` ${cyan('me --platform amp')} use a different platform adapter`)
678
+ writeln(` ${cyan('me --compare')} raw pillar comparison: ccusage vs tokenpull vs token-dashboard`)
679
+ writeln(` ${cyan('watch')} live tune meter re-reads local logs every 30s`)
680
+ writeln(` ${cyan('watch --window 7d')} watch a specific window`)
459
681
  writeln()
460
682
  writeln(` ${bold('Options')}`)
461
683
  writeln(` ${dim('--window')} 7d · 30d · 90d · all_time (default: 30d for board, 7d for watch)`)
@@ -498,7 +720,7 @@ export async function runCli(argv) {
498
720
  refresh: Number(flags.refresh) || 30,
499
721
  })
500
722
  } else if (cmd === 'me') {
501
- await runMe({ platform: flags.platform ?? 'claude' })
723
+ await runMe({ platform: flags.platform ?? 'claude', compare: flags.compare === true || flags.compare === 'true' })
502
724
  } else if (cmd === 'watch') {
503
725
  await runWatch({
504
726
  platform: flags.platform ?? 'claude',
@@ -508,7 +730,7 @@ export async function runCli(argv) {
508
730
  } else if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
509
731
  showHelp()
510
732
  } else if (cmd === '--version' || cmd === '-v') {
511
- writeln('0.6.4')
733
+ writeln('0.6.5')
512
734
  } else {
513
735
  // unknown command: show help
514
736
  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.5",
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": {