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 +25 -21
- package/banner.svg +7 -0
- package/cli.mjs +60 -22
- package/package.json +23 -2
- package/tokenpull.mjs +19 -12
- package/tools.mjs +28 -3
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 & 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
|
+
| [](https://signalaf.com/board/all) | [](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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`
|
|
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
|
|
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
|
-
//
|
|
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
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
358
|
-
//
|
|
359
|
-
|
|
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 `
|
|
376
|
-
// the requested platform. The JSON shape
|
|
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('
|
|
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
|
-
|
|
623
|
+
_tokscalePillars(platform).catch(() => null),
|
|
599
624
|
])
|
|
600
625
|
|
|
601
626
|
// Build tokenpull window lookup
|