sigrank 0.12.1 → 0.12.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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/tokenpull.mjs +37 -14
  3. package/tui.mjs +34 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
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",
package/tokenpull.mjs CHANGED
@@ -17,13 +17,27 @@
17
17
 
18
18
  import { readdir, readFile, lstat } from 'node:fs/promises'
19
19
  import { existsSync } from 'node:fs'
20
- import { execSync } from 'node:child_process'
20
+ import { execSync, execFile } from 'node:child_process'
21
21
  import { join } from 'node:path'
22
22
  import { homedir } from 'node:os'
23
23
  import { ADAPTERS } from './adapters.mjs'
24
24
 
25
25
  const DAY_MS = 86_400_000
26
26
 
27
+ // ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync in the
28
+ // fresh verifier readers. execSync blocks the entire Node event loop (no key handling,
29
+ // no screen repaint for up to 90s during the tokendash scan). execFile is async: the
30
+ // event loop keeps running, so the TUI stays responsive while external commands run.
31
+ // Returns stdout as a string. Rejects on error or timeout (caller catches → null).
32
+ function execFileAsync(cmd, args, timeoutMs) {
33
+ return new Promise((resolve, reject) => {
34
+ execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
35
+ if (err) reject(err)
36
+ else resolve(stdout.toString())
37
+ })
38
+ })
39
+ }
40
+
27
41
  // Background tooling that runs under your account but is NOT your work — memory plugins,
28
42
  // observers, summarizers, sub-agent fleets. They pad output/Υ (claude-mem did ~27%).
29
43
  // Excluded so pillars reflect the operator, not their tools. EXTENSIBLE — add tool
@@ -297,9 +311,13 @@ export async function tokenpullAny(platform, opts = {}) {
297
311
 
298
312
  // ccusage: run `ccusage <platform> daily --json` and bucket the daily rows into the four
299
313
  // windows by date. Logic copied from tools.mjs `_ccusagePillars` (do NOT import private fns).
300
- function _freshCcusage(platform = 'claude') {
314
+ //
315
+ // ASYNC FIX (2026-06-27): was execSync (blocked the entire event loop for up to 15s,
316
+ // freezing the TUI locked frame + key handling). Now uses execFile (async) so the
317
+ // event loop keeps running — the TUI stays responsive while the external command runs.
318
+ async function _freshCcusage(platform = 'claude') {
301
319
  try {
302
- const raw = execSync(`ccusage ${platform} daily --json`, { timeout: 15000, stdio: ['ignore', 'pipe', 'ignore'] }).toString()
320
+ const raw = await execFileAsync('ccusage', [platform, 'daily', '--json'], 15000)
303
321
  const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw)
304
322
  if (!Array.isArray(rows)) return null
305
323
  const now = Date.now()
@@ -329,24 +347,26 @@ function _freshCcusage(platform = 'claude') {
329
347
  // tokendash: REFRESH ~/.claude/token-dashboard.db by running the dashboard's scan, then read
330
348
  // the live db via sqlite3. Claude only (the db only holds Claude Code sessions). The scan is
331
349
  // best-effort: if it fails (tool missing, slow), we still read whatever the db currently has.
332
- function _freshTokendash(platform = 'claude') {
350
+ //
351
+ // ASYNC FIX (2026-06-27): was execSync with a 90s timeout — that blocked the ENTIRE event
352
+ // loop for up to 90 seconds, freezing the TUI completely (no key handling, no frame repaint).
353
+ // Now uses execFile (async) so the TUI stays responsive during the scan.
354
+ async function _freshTokendash(platform = 'claude') {
333
355
  if (platform !== 'claude') return null
334
356
  const dbPath = join(homedir(), '.claude', 'token-dashboard.db')
335
357
  const scanCli = join(homedir(), 'Desktop', 'token-dashboard', 'cli.py')
336
358
  // Step 1 — refresh the db (best-effort; failures must not abort the read).
337
359
  if (existsSync(scanCli)) {
338
- try {
339
- execSync(`python3 "${scanCli}" scan`, { timeout: 90000, stdio: ['ignore', 'ignore', 'ignore'] })
340
- } catch { /* continue: read whatever the db currently has */ }
360
+ try { await execFileAsync('python3', [scanCli, 'scan'], 90000) }
361
+ catch { /* continue: read whatever the db currently has */ }
341
362
  }
342
363
  // Step 2 — read the (now-refreshed) db. all-time only; the db doesn't expose windowing here.
343
364
  if (!existsSync(dbPath)) return null
344
365
  try {
345
- const raw = execSync(
346
- `sqlite3 "${dbPath}" "SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens)+SUM(cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages"`,
347
- { timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] },
348
- ).toString().trim()
349
- const [i, o, cw, cr] = raw.split('|').map(Number)
366
+ const raw = await execFileAsync('sqlite3', [dbPath,
367
+ 'SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens)+SUM(cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages'
368
+ ], 5000)
369
+ const [i, o, cw, cr] = raw.trim().split('|').map(Number)
350
370
  if (![i, o, cw, cr].some((n) => Number.isFinite(n) && n > 0)) return null
351
371
  return { all: { input: i || 0, output: o || 0, cacheCreate: cw || 0, cacheRead: cr || 0 } }
352
372
  } catch { return null }
@@ -358,9 +378,12 @@ function _freshTokendash(platform = 'claude') {
358
378
  // `client` maps directly to our platform name. Synthetic/unknown model rows are dropped (they
359
379
  // carry no real usage). all-time only — tokscale's models view is not windowed. If no row
360
380
  // matches the platform, return null gracefully.
361
- function _freshTokscale(platform = 'claude') {
381
+ //
382
+ // ASYNC FIX (2026-06-27): was execSync with a 60s timeout — blocked the event loop entirely.
383
+ // Now uses execFile (async) so the TUI stays responsive.
384
+ async function _freshTokscale(platform = 'claude') {
362
385
  try {
363
- const raw = execSync(`bunx tokscale@latest models --json`, { timeout: 60000, stdio: ['ignore', 'pipe', 'ignore'] }).toString()
386
+ const raw = await execFileAsync('bunx', ['tokscale@latest', 'models', '--json'], 60000)
364
387
  const data = JSON.parse(raw)
365
388
  const entries = Array.isArray(data?.entries) ? data.entries : (Array.isArray(data) ? data : [])
366
389
  const rows = entries.filter((e) =>
package/tui.mjs CHANGED
@@ -1275,7 +1275,19 @@ function showSplash() {
1275
1275
  const hint = () => interactive
1276
1276
  ? `${dim(' [P]')} ${paused ? 'resume' : 'pause'} ${dim('[Enter]')} enter`
1277
1277
  : dim(' loading…')
1278
- const paint = () => { write(CLEAR); write(splashFrame(mode)); write(`\n${ctr(hint())}`) }
1278
+ // FIXED-WINDOW FIX (2026-06-27): paint the splash via the buffered flushScreen
1279
+ // path (GOTO positioning), NOT raw write('\n'). Raw \n scrolls the alt-screen
1280
+ // buffer, creating scrollback above the TUI — that's why you could scroll up
1281
+ // to see what was above it. flushScreen uses absolute cursor positioning so
1282
+ // nothing ever scrolls the buffer. This matches how tokscale renders its frame.
1283
+ const paint = () => {
1284
+ startBuffer()
1285
+ const lines = splashFrame(mode).split('\n')
1286
+ lines.forEach((ln) => writeln(ln))
1287
+ writeln(ctr(hint()))
1288
+ setFooter([])
1289
+ flushScreen()
1290
+ }
1279
1291
  paint()
1280
1292
  let timer = setInterval(paint, 110)
1281
1293
 
@@ -1677,11 +1689,17 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
1677
1689
  // #9 (2026-06-27): Compare loads its FRESH verifier pull ON-DEMAND when the tab opens
1678
1690
  // (NOT on the Dashboard load path — that pull is a 5–60s scan). Paint the "pulling fresh
1679
1691
  // sources…" spinner first (compareData null → redraw shows it), then run the pull + re-render.
1692
+ // ASYNC FIX (2026-06-27): with execFile (async), the event loop keeps running during the
1693
+ // pull — key presses are NOT blocked. Guard: only render the result if the user is still
1694
+ // on the Compare tab (they may have switched away during the pull).
1680
1695
  if (switched && activeTab === 2 && !compareData) {
1681
1696
  submitMsg = ''
1682
1697
  await redraw() // show the spinner line before the (slow) fresh pull
1683
- compareData = await loadCompareData(platform).catch(() => null)
1684
- await redraw()
1698
+ const result = await loadCompareData(platform).catch(() => null)
1699
+ if (activeTab === 2) { // still on Compare? render the result
1700
+ compareData = result
1701
+ await redraw()
1702
+ }
1685
1703
  return
1686
1704
  }
1687
1705
 
@@ -1705,8 +1723,12 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
1705
1723
  compareData = null
1706
1724
  status = `loading ${platform}…`
1707
1725
  await redraw()
1708
- compareData = await loadCompareData(platform).catch(() => null)
1709
- status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
1726
+ // ASYNC FIX: guard — only render if still on Compare (user may have switched away)
1727
+ const result = await loadCompareData(platform).catch(() => null)
1728
+ if (activeTab === 2) {
1729
+ compareData = result
1730
+ status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
1731
+ }
1710
1732
  } else {
1711
1733
  // A5: Watch [P] cycles 'all' → claude → codex → … → 'all' (optional focus; default 'all').
1712
1734
  const WATCH_PLAT_CYCLE = ['all', ...CYCLE_PLATFORMS]
@@ -1751,9 +1773,13 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
1751
1773
  if (activeTab === 2) {
1752
1774
  compareData = null
1753
1775
  await redraw()
1754
- compareData = await loadCompareData(platform).catch(() => null)
1755
- status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
1756
- await redraw()
1776
+ // ASYNC FIX: guard — only render if still on Compare
1777
+ const result = await loadCompareData(platform).catch(() => null)
1778
+ if (activeTab === 2) {
1779
+ compareData = result
1780
+ status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
1781
+ await redraw()
1782
+ }
1757
1783
  return
1758
1784
  }
1759
1785
  await redraw()