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.
- package/package.json +1 -1
- package/tokenpull.mjs +37 -14
- package/tui.mjs +34 -8
package/package.json
CHANGED
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
).
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1684
|
-
|
|
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
|
-
|
|
1709
|
-
|
|
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
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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()
|