sigrank 0.12.1 → 0.12.2
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 +21 -7
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
|
@@ -1677,11 +1677,17 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1677
1677
|
// #9 (2026-06-27): Compare loads its FRESH verifier pull ON-DEMAND when the tab opens
|
|
1678
1678
|
// (NOT on the Dashboard load path — that pull is a 5–60s scan). Paint the "pulling fresh
|
|
1679
1679
|
// sources…" spinner first (compareData null → redraw shows it), then run the pull + re-render.
|
|
1680
|
+
// ASYNC FIX (2026-06-27): with execFile (async), the event loop keeps running during the
|
|
1681
|
+
// pull — key presses are NOT blocked. Guard: only render the result if the user is still
|
|
1682
|
+
// on the Compare tab (they may have switched away during the pull).
|
|
1680
1683
|
if (switched && activeTab === 2 && !compareData) {
|
|
1681
1684
|
submitMsg = ''
|
|
1682
1685
|
await redraw() // show the spinner line before the (slow) fresh pull
|
|
1683
|
-
|
|
1684
|
-
|
|
1686
|
+
const result = await loadCompareData(platform).catch(() => null)
|
|
1687
|
+
if (activeTab === 2) { // still on Compare? render the result
|
|
1688
|
+
compareData = result
|
|
1689
|
+
await redraw()
|
|
1690
|
+
}
|
|
1685
1691
|
return
|
|
1686
1692
|
}
|
|
1687
1693
|
|
|
@@ -1705,8 +1711,12 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1705
1711
|
compareData = null
|
|
1706
1712
|
status = `loading ${platform}…`
|
|
1707
1713
|
await redraw()
|
|
1708
|
-
|
|
1709
|
-
|
|
1714
|
+
// ASYNC FIX: guard — only render if still on Compare (user may have switched away)
|
|
1715
|
+
const result = await loadCompareData(platform).catch(() => null)
|
|
1716
|
+
if (activeTab === 2) {
|
|
1717
|
+
compareData = result
|
|
1718
|
+
status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
|
|
1719
|
+
}
|
|
1710
1720
|
} else {
|
|
1711
1721
|
// A5: Watch [P] cycles 'all' → claude → codex → … → 'all' (optional focus; default 'all').
|
|
1712
1722
|
const WATCH_PLAT_CYCLE = ['all', ...CYCLE_PLATFORMS]
|
|
@@ -1751,9 +1761,13 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1751
1761
|
if (activeTab === 2) {
|
|
1752
1762
|
compareData = null
|
|
1753
1763
|
await redraw()
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1764
|
+
// ASYNC FIX: guard — only render if still on Compare
|
|
1765
|
+
const result = await loadCompareData(platform).catch(() => null)
|
|
1766
|
+
if (activeTab === 2) {
|
|
1767
|
+
compareData = result
|
|
1768
|
+
status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
|
|
1769
|
+
await redraw()
|
|
1770
|
+
}
|
|
1757
1771
|
return
|
|
1758
1772
|
}
|
|
1759
1773
|
await redraw()
|