sigrank 0.12.5 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,16 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- ```
4
- ███████ ██ ██████ ██████ █████ ███ ██ ██ ██
5
- ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ██
6
- ███████ ██ ██ ███ ██████ ███████ ██ ██ ██ █████
7
- ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
8
- ███████ ██ ██████ ██ ██ ██ ██ ██ ████ ██ ██
9
- ```
10
-
11
- ◈ ─────────────────────────────────────────── ◈
12
-
13
- # SigRank MCP
3
+ <p><img src="./.github/assets/og-card.png" alt="SigRank — the new standard in AI evaluation &amp; benchmarks" width="800"></p>
14
4
 
15
5
  **The yield cascade + live leaderboard as MCP tools any agent can call.**
16
6
 
@@ -25,21 +15,35 @@ For all builders, burners and 10xers.
25
15
 
26
16
  </div>
27
17
 
28
- ---
18
+ | The board | Your operator profile |
19
+ |:---:|:---:|
20
+ | [![SigRank leaderboard](./.github/assets/board.png)](https://signalaf.com/board/all) | [![SigRank operator profile](./.github/assets/profile.png)](https://signalaf.com) |
21
+ | Every operator ranked by **Υ Yield** — the architecture of the cascade, not raw spend | Cascade layer, class, and fingerprint — derived from four token counts |
29
22
 
30
- Token-only. No auth. No transcript content. Privacy-preserving by design.
23
+ > **Run [`sigrank enroll`](#sign-in--submit) then [`sigrank submit`](#sign-in--submit) to get ranked and claim your public profile at [signalaf.com](https://signalaf.com).**
31
24
 
32
- Dual-mode package: **interactive CLI dashboard** for operators, **MCP stdio server** for AI clients.
25
+ ---
26
+
27
+ ## Quickstart — 3 steps to the board
33
28
 
34
29
  ```bash
30
+ # 1. Install (pulls ccusage + tokscale + tokendash automatically — no separate installs)
35
31
  npm install -g sigrank
36
- sigrank # launches the full tabbed TUI (TTY detected automatically)
32
+
33
+ # 2. Sign in (paste a connect code from signalaf.com → Settings → New key)
34
+ sigrank enroll
35
+
36
+ # 3. Submit your cascade to the board
37
+ sigrank submit
37
38
  ```
38
39
 
39
- Or one-shot without installing:
40
+ That's it. The agent 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
+
42
+ Or just explore without signing in:
40
43
 
41
44
  ```bash
42
- npx sigrank board --once
45
+ sigrank # launches the full tabbed TUI (dashboard, compare, board, watch)
46
+ npx sigrank board --once # print the live leaderboard once
43
47
  ```
44
48
 
45
49
  ## Install from GitHub
@@ -67,7 +71,7 @@ sigrank
67
71
  ## Commands
68
72
 
69
73
  ```
70
- ⊙ SigRank CLI v0.12.5
74
+ ⊙ SigRank CLI v0.13.0
71
75
 
72
76
  Default (no args)
73
77
  sigrank unified dashboard: cascade + token pillars + board
@@ -202,9 +206,9 @@ The dashboard pulls from multiple sources and shows them side-by-side for verifi
202
206
  | Source | What | Platform |
203
207
  |---|---|---|
204
208
  | `tokenpull` | On-device JSONL scanner (canon source) | claude, codex, amp, … |
205
- | `ccusage` | `ccusage <platform> daily --json` CLI | claude, codex |
206
- | `token-dashboard` | `~/.claude/token-dashboard.db` SQLite | claude only |
207
- | `tokscale` | `~/tokscale_report.json` export | claude, codex |
209
+ | `ccusage` | `ccusage <platform> daily --json` CLI (bundled) | claude, codex |
210
+ | `token-dashboard` | `~/.claude/token-dashboard.db` SQLite (bundled) | claude only |
211
+ | `tokscale` | `tokscale models --json` CLI (bundled, falls back to `~/tokscale_report.json`) | claude, codex |
208
212
 
209
213
  **Codex input is estimated** — Codex logs don't expose true input tokens directly. The formula:
210
214
  ```
package/banner.svg ADDED
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="800" height="200" viewBox="0 0 800 200">
2
+ <rect width="800" height="200" rx="12" fill="#0a0805"/>
3
+ <rect x="0" y="0" width="800" height="3" rx="12" fill="#c4923a"/>
4
+ <text x="60" y="110" font-family="Georgia, serif" font-size="56" font-weight="700" fill="#e8b955">§ SigRank</text>
5
+ <text x="60" y="150" font-family="Helvetica, Arial, sans-serif" font-size="18" fill="#a89a7a">The AI operator leaderboard</text>
6
+ <text x="60" y="178" font-family="Helvetica, Arial, sans-serif" font-size="14" fill="#6b6354">signalaf.com</text>
7
+ </svg>
package/cli.mjs CHANGED
@@ -29,17 +29,25 @@ import { classify } from './cascade.mjs'
29
29
  import { ensureIdentity, keystorePath } from './keystore.mjs'
30
30
  import { submitSignedWindow } from './submit.mjs'
31
31
  import { execFile } from 'child_process'
32
- import { existsSync, readFileSync, writeFileSync } from 'fs'
32
+ import { existsSync, readFileSync } from 'fs'
33
33
  import os from 'os'
34
34
  import path from 'path'
35
+ import { fileURLToPath } from 'url'
36
+
37
+ // Resolve local node_modules/.bin for bundled deps (ccusage, tokscale)
38
+ const _pkgRoot = path.dirname(fileURLToPath(import.meta.url))
39
+ const _localBin = path.join(_pkgRoot, 'node_modules', '.bin')
40
+ const _envPath = `${_localBin}${process.env.PATH ? ':' + process.env.PATH : ''}`
35
41
 
36
42
  // ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
37
43
  // defense-in-depth (shell injection prevention). execFile passes args as an
38
44
  // array, so no shell parsing occurs — even if platform contained special chars,
39
45
  // they'd be treated as literal arguments, not shell commands.
46
+ // BIN FIX (2026-06-27): PATH includes local node_modules/.bin so bundled deps
47
+ // are found even when sigrank isn't globally installed.
40
48
  function execFileAsync(cmd, args, timeoutMs) {
41
49
  return new Promise((resolve, reject) => {
42
- execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
50
+ execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024, env: { ...process.env, PATH: _envPath } }, (err, stdout) => {
43
51
  if (err) reject(err)
44
52
  else resolve(stdout.toString())
45
53
  })
@@ -460,8 +468,28 @@ async function ccusagePillars(platform = 'claude') {
460
468
  }
461
469
  }
462
470
 
463
- function tokscalePillars(platform = 'claude') {
464
- // Read tokscale_report.json — all-time only (no timestamps in export)
471
+ async function tokscalePillars(platform = 'claude') {
472
+ // Try the bundled tokscale CLI first (fresh data), fall back to saved report file.
473
+ try {
474
+ const raw = await execFileAsync('tokscale', ['models', '--json'], 60000)
475
+ const data = JSON.parse(raw)
476
+ const entries = Array.isArray(data?.entries) ? data.entries : (Array.isArray(data) ? data : [])
477
+ const rows = entries.filter(e =>
478
+ e && e.client === platform &&
479
+ e.model !== '<synthetic>' && e.model !== 'unknown' &&
480
+ ((Number(e.input) || 0) > 0 || (Number(e.output) || 0) > 0)
481
+ )
482
+ if (rows.length) {
483
+ const p = rows.reduce((acc, e) => ({
484
+ input: acc.input + (Number(e.input) || 0),
485
+ output: acc.output + (Number(e.output) || 0),
486
+ cacheCreate: acc.cacheCreate + (Number(e.cacheWrite) || 0),
487
+ cacheRead: acc.cacheRead + (Number(e.cacheRead) || 0),
488
+ }), { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 })
489
+ return { all: p }
490
+ }
491
+ } catch { /* fall through to file-based read */ }
492
+ // Fallback: read saved tokscale_report.json — all-time only (no timestamps in export)
465
493
  const reportPath = path.join(os.homedir(), 'tokscale_report.json')
466
494
  if (!existsSync(reportPath)) return null
467
495
  try {
@@ -509,24 +537,34 @@ function appPillars() {
509
537
  async function tokenDashPillars() {
510
538
  const dbPath = path.join(os.homedir(), '.claude', 'token-dashboard.db')
511
539
  if (!existsSync(dbPath)) return null
540
+ // Read directly with sqlite3 — no python script needed (bundled dep).
541
+ // Windowed queries using sqlite3's datetime() function.
542
+ const cf = "(model LIKE '%claude%' OR model LIKE '%fable%' OR model LIKE '%sonnet%' OR model LIKE '%opus%' OR model LIKE '%haiku%')"
512
543
  try {
513
- const tmpScript = path.join(os.tmpdir(), 'sigrank_td_query.py')
514
- writeFileSync(tmpScript, `
515
- import sqlite3, json, sys
516
- from datetime import datetime, timezone, timedelta
517
- db = sqlite3.connect(sys.argv[1])
518
- cf = "(model LIKE '%claude%' OR model LIKE '%fable%' OR model LIKE '%sonnet%' OR model LIKE '%opus%' OR model LIKE '%haiku%')"
519
- out = {}
520
- for win, days in [('7d',7),('30d',30),('90d',90)]:
521
- since = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
522
- r = db.execute(f"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 {cf}",(since,)).fetchone()
523
- out[win] = {'input':r[0] or 0,'output':r[1] or 0,'cacheCreate':r[2] or 0,'cacheRead':r[3] or 0}
524
- r = db.execute(f"SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens+cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages WHERE {cf}").fetchone()
525
- out['all'] = {'input':r[0] or 0,'output':r[1] or 0,'cacheCreate':r[2] or 0,'cacheRead':r[3] or 0}
526
- print(json.dumps(out))
527
- `)
528
- const raw = await execFileAsync('python3', [tmpScript, dbPath], 15000)
529
- return JSON.parse(raw.trim())
544
+ const wins = [
545
+ ['7d', "datetime('now','-7 days')"],
546
+ ['30d', "datetime('now','-30 days')"],
547
+ ['90d', "datetime('now','-90 days')"],
548
+ ]
549
+ const sql = (cutoff) =>
550
+ `SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens+cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages WHERE timestamp>=${cutoff} AND ${cf}`
551
+ const allSql = `SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens+cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages WHERE ${cf}`
552
+ const [d7, d30, d90, dAll] = await Promise.all([
553
+ execFileAsync('sqlite3', [dbPath, sql(wins[0][1])], 5000),
554
+ execFileAsync('sqlite3', [dbPath, sql(wins[1][1])], 5000),
555
+ execFileAsync('sqlite3', [dbPath, sql(wins[2][1])], 5000),
556
+ execFileAsync('sqlite3', [dbPath, allSql], 5000),
557
+ ])
558
+ const parse = (raw) => {
559
+ const [i,o,cw,cr] = raw.trim().split('|').map(Number)
560
+ return { input: i||0, output: o||0, cacheCreate: cw||0, cacheRead: cr||0 }
561
+ }
562
+ return {
563
+ '7d': parse(d7),
564
+ '30d': parse(d30),
565
+ '90d': parse(d90),
566
+ all: parse(dAll),
567
+ }
530
568
  } catch {
531
569
  return null
532
570
  }
@@ -577,7 +615,7 @@ async function runCompare({ platform = 'claude' } = {}) {
577
615
  ccusagePillars(platform).catch(() => null),
578
616
  callTool('tokenpull', { platform }).catch(() => null),
579
617
  (platform === 'claude' ? tokenDashPillars() : Promise.resolve(null)).catch(() => null),
580
- Promise.resolve(tokscalePillars(platform)),
618
+ tokscalePillars(platform).catch(() => null),
581
619
  Promise.resolve(platform === 'claude' ? appPillars() : null),
582
620
  ])
583
621
  write(CURSOR_UP(1) + ERASE_LINE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "0.12.5",
3
+ "version": "0.13.1",
4
4
  "description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,10 +37,31 @@
37
37
  "keystore.mjs",
38
38
  "submit.mjs",
39
39
  "connect.mjs",
40
+ "banner.svg",
40
41
  "README.md",
41
42
  "LICENSE"
42
43
  ],
44
+ "keywords": [
45
+ "mcp",
46
+ "model-context-protocol",
47
+ "ai-agents",
48
+ "claude",
49
+ "anthropic",
50
+ "llm",
51
+ "token-telemetry",
52
+ "token-usage",
53
+ "leaderboard",
54
+ "cli",
55
+ "tui",
56
+ "yield-cascade",
57
+ "sigrank",
58
+ "agent-tools",
59
+ "on-device"
60
+ ],
43
61
  "dependencies": {
44
- "@modelcontextprotocol/sdk": "^1.0.0"
62
+ "@modelcontextprotocol/sdk": "^1.0.0",
63
+ "@zhangferry-dev/tokendash": "^1.7.5",
64
+ "ccusage": "^20.0.14",
65
+ "tokscale": "^4.0.4"
45
66
  }
46
67
  }
package/tokenpull.mjs CHANGED
@@ -18,20 +18,30 @@
18
18
  import { readdir, readFile, lstat } from 'node:fs/promises'
19
19
  import { existsSync } from 'node:fs'
20
20
  import { execFile } from 'node:child_process'
21
- import { join } from 'node:path'
21
+ import { join, dirname } from 'node:path'
22
22
  import { homedir } from 'node:os'
23
+ import { fileURLToPath } from 'node:url'
23
24
  import { ADAPTERS } from './adapters.mjs'
24
25
 
25
26
  const DAY_MS = 86_400_000
26
27
 
28
+ // Resolve the package root for finding bundled binaries (ccusage, tokscale, etc.)
29
+ const _pkgRoot = join(dirname(fileURLToPath(import.meta.url)))
30
+ const _localBin = join(_pkgRoot, 'node_modules', '.bin')
31
+ // Prepend local node_modules/.bin to PATH so bundled deps are found even when
32
+ // not globally installed (e.g., npx sigrank, local dev).
33
+ const _envPath = `${_localBin}${process.env.PATH ? ':' + process.env.PATH : ''}`
34
+
27
35
  // ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync in the
28
36
  // fresh verifier readers. execSync blocks the entire Node event loop (no key handling,
29
37
  // no screen repaint for up to 90s during the tokendash scan). execFile is async: the
30
38
  // event loop keeps running, so the TUI stays responsive while external commands run.
31
39
  // Returns stdout as a string. Rejects on error or timeout (caller catches → null).
40
+ // BIN FIX (2026-06-27): PATH includes local node_modules/.bin so bundled deps
41
+ // (ccusage, tokscale) are found even when sigrank isn't globally installed.
32
42
  function execFileAsync(cmd, args, timeoutMs) {
33
43
  return new Promise((resolve, reject) => {
34
- execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
44
+ execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024, env: { ...process.env, PATH: _envPath } }, (err, stdout) => {
35
45
  if (err) reject(err)
36
46
  else resolve(stdout.toString())
37
47
  })
@@ -354,13 +364,9 @@ async function _freshCcusage(platform = 'claude') {
354
364
  async function _freshTokendash(platform = 'claude') {
355
365
  if (platform !== 'claude') return null
356
366
  const dbPath = join(homedir(), '.claude', 'token-dashboard.db')
357
- const scanCli = join(homedir(), 'Desktop', 'token-dashboard', 'cli.py')
358
- // Step 1 — refresh the db (best-effort; failures must not abort the read).
359
- if (existsSync(scanCli)) {
360
- try { await execFileAsync('python3', [scanCli, 'scan'], 90000) }
361
- catch { /* continue: read whatever the db currently has */ }
362
- }
363
- // Step 2 — read the (now-refreshed) db. all-time only; the db doesn't expose windowing here.
367
+ // Read the token-dashboard DB directly with sqlite3 (no external python scan
368
+ // needed — the DB is created by the tokendash dashboard, now bundled as a dep).
369
+ // all-time only; the db doesn't expose windowing here.
364
370
  if (!existsSync(dbPath)) return null
365
371
  try {
366
372
  const raw = await execFileAsync('sqlite3', [dbPath,
@@ -372,8 +378,9 @@ async function _freshTokendash(platform = 'claude') {
372
378
  } catch { return null }
373
379
  }
374
380
 
375
- // tokscale: run `bunx tokscale@latest models --json` and sum the per-(client,model) rows for
376
- // the requested platform. The JSON shape (verified live) is:
381
+ // tokscale: run `tokscale models --json` (bundled as npm dep — no bunx needed)
382
+ // and sum the per-(client,model) rows for the requested platform. The JSON shape
383
+ // (verified live) is:
377
384
  // { entries: [ { client, model, input, output, cacheRead, cacheWrite, ... }, ... ], ... }
378
385
  // `client` maps directly to our platform name. Synthetic/unknown model rows are dropped (they
379
386
  // carry no real usage). all-time only — tokscale's models view is not windowed. If no row
@@ -383,7 +390,7 @@ async function _freshTokendash(platform = 'claude') {
383
390
  // Now uses execFile (async) so the TUI stays responsive.
384
391
  async function _freshTokscale(platform = 'claude') {
385
392
  try {
386
- const raw = await execFileAsync('bunx', ['tokscale@latest', 'models', '--json'], 60000)
393
+ const raw = await execFileAsync('tokscale', ['models', '--json'], 60000)
387
394
  const data = JSON.parse(raw)
388
395
  const entries = Array.isArray(data?.entries) ? data.entries : (Array.isArray(data) ? data : [])
389
396
  const rows = entries.filter((e) =>
package/tools.mjs CHANGED
@@ -19,14 +19,22 @@ import { execFile } from 'node:child_process'
19
19
  import { existsSync, readFileSync } from 'node:fs'
20
20
  import os from 'node:os'
21
21
  import path from 'node:path'
22
+ import { fileURLToPath } from 'node:url'
23
+
24
+ // Resolve local node_modules/.bin for bundled deps (ccusage, tokscale)
25
+ const _pkgRoot = path.dirname(fileURLToPath(import.meta.url))
26
+ const _localBin = path.join(_pkgRoot, 'node_modules', '.bin')
27
+ const _envPath = `${_localBin}${process.env.PATH ? ':' + process.env.PATH : ''}`
22
28
 
23
29
  // ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
24
30
  // defense-in-depth (shell injection prevention + non-blocking). The platform param
25
31
  // is enum-validated at the MCP schema level, but execFile also prevents shell
26
32
  // interpolation attacks by passing args as an array (no shell parsing).
33
+ // BIN FIX (2026-06-27): PATH includes local node_modules/.bin so bundled deps
34
+ // are found even when sigrank isn't globally installed.
27
35
  function execFileAsync(cmd, args, timeoutMs) {
28
36
  return new Promise((resolve, reject) => {
29
- execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
37
+ execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024, env: { ...process.env, PATH: _envPath } }, (err, stdout) => {
30
38
  if (err) reject(err)
31
39
  else resolve(stdout.toString())
32
40
  })
@@ -82,7 +90,24 @@ async function _tokenDashPillars() {
82
90
  } catch { return null }
83
91
  }
84
92
 
85
- function _tokscalePillars(platform = 'claude') {
93
+ async function _tokscalePillars(platform = 'claude') {
94
+ // Try the bundled tokscale CLI first (fresh data), fall back to saved report file.
95
+ try {
96
+ const raw = await execFileAsync('tokscale', ['models', '--json'], 60000)
97
+ const data = JSON.parse(raw)
98
+ const entries = Array.isArray(data?.entries) ? data.entries : (Array.isArray(data) ? data : [])
99
+ const rows = entries.filter(e =>
100
+ e && e.client === platform && e.model !== '<synthetic>' && e.model !== 'unknown' && ((Number(e.input) || 0) > 0 || (Number(e.output) || 0) > 0)
101
+ )
102
+ if (rows.length) {
103
+ const acc = rows.reduce((a,e) => ({
104
+ input: a.input+(Number(e.input)||0), output: a.output+(Number(e.output)||0),
105
+ cacheCreate: a.cacheCreate+(Number(e.cacheWrite)||0), cacheRead: a.cacheRead+(Number(e.cacheRead)||0),
106
+ }), { input:0, output:0, cacheCreate:0, cacheRead:0 })
107
+ return { all: acc }
108
+ }
109
+ } catch { /* fall through to file-based read */ }
110
+ // Fallback: read saved tokscale_report.json if it exists
86
111
  const p = path.join(os.homedir(), 'tokscale_report.json')
87
112
  if (!existsSync(p)) return null
88
113
  try {
@@ -595,7 +620,7 @@ export async function callTool(name, args, opts = {}) {
595
620
  pullByPlatform(platform, opts).catch(() => null),
596
621
  _ccusagePillars(platform).catch(() => null),
597
622
  (platform === 'claude' ? _tokenDashPillars() : Promise.resolve(null)).catch(() => null),
598
- Promise.resolve(_tokscalePillars(platform)),
623
+ _tokscalePillars(platform).catch(() => null),
599
624
  ])
600
625
 
601
626
  // Build tokenpull window lookup