sigrank 0.12.3 → 0.12.5
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 +1 -1
- package/cli.mjs +24 -13
- package/package.json +1 -1
- package/tokenpull.mjs +1 -1
- package/tools.mjs +24 -12
- package/tui.mjs +6 -2
package/README.md
CHANGED
package/cli.mjs
CHANGED
|
@@ -28,11 +28,24 @@ import { callTool, DEFAULT_API_BASE } from './tools.mjs'
|
|
|
28
28
|
import { classify } from './cascade.mjs'
|
|
29
29
|
import { ensureIdentity, keystorePath } from './keystore.mjs'
|
|
30
30
|
import { submitSignedWindow } from './submit.mjs'
|
|
31
|
-
import {
|
|
31
|
+
import { execFile } from 'child_process'
|
|
32
32
|
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
33
33
|
import os from 'os'
|
|
34
34
|
import path from 'path'
|
|
35
35
|
|
|
36
|
+
// ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
|
|
37
|
+
// defense-in-depth (shell injection prevention). execFile passes args as an
|
|
38
|
+
// array, so no shell parsing occurs — even if platform contained special chars,
|
|
39
|
+
// they'd be treated as literal arguments, not shell commands.
|
|
40
|
+
function execFileAsync(cmd, args, timeoutMs) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
|
|
43
|
+
if (err) reject(err)
|
|
44
|
+
else resolve(stdout.toString())
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
// ── ANSI helpers (no chalk dep) ────────────────────────────────────────────
|
|
37
50
|
const ESC = '\x1b['
|
|
38
51
|
const c = {
|
|
@@ -407,11 +420,10 @@ async function runMe({ platform = 'claude', compare = false } = {}) {
|
|
|
407
420
|
// identical to _freshCcusage and could be consolidated in a future pass if compare
|
|
408
421
|
// switches to live data — but that's a behavior change, not a refactor.
|
|
409
422
|
|
|
410
|
-
function ccusagePillars(platform = 'claude') {
|
|
423
|
+
async function ccusagePillars(platform = 'claude') {
|
|
411
424
|
// ccusage <platform> daily --json → sum by window
|
|
412
425
|
try {
|
|
413
|
-
const
|
|
414
|
-
const raw = execSync(cmd, { timeout: 15000, stdio: ['ignore', 'pipe', 'ignore'] }).toString()
|
|
426
|
+
const raw = await execFileAsync('ccusage', [platform, 'daily', '--json'], 15000)
|
|
415
427
|
const data = JSON.parse(raw)
|
|
416
428
|
const rows = data.daily ?? data // ccusage may return {daily:[...]} or [...]
|
|
417
429
|
|
|
@@ -494,7 +506,7 @@ function appPillars() {
|
|
|
494
506
|
}
|
|
495
507
|
}
|
|
496
508
|
|
|
497
|
-
function tokenDashPillars() {
|
|
509
|
+
async function tokenDashPillars() {
|
|
498
510
|
const dbPath = path.join(os.homedir(), '.claude', 'token-dashboard.db')
|
|
499
511
|
if (!existsSync(dbPath)) return null
|
|
500
512
|
try {
|
|
@@ -513,9 +525,8 @@ r = db.execute(f"SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m
|
|
|
513
525
|
out['all'] = {'input':r[0] or 0,'output':r[1] or 0,'cacheCreate':r[2] or 0,'cacheRead':r[3] or 0}
|
|
514
526
|
print(json.dumps(out))
|
|
515
527
|
`)
|
|
516
|
-
const raw =
|
|
517
|
-
|
|
518
|
-
return JSON.parse(raw)
|
|
528
|
+
const raw = await execFileAsync('python3', [tmpScript, dbPath], 15000)
|
|
529
|
+
return JSON.parse(raw.trim())
|
|
519
530
|
} catch {
|
|
520
531
|
return null
|
|
521
532
|
}
|
|
@@ -560,12 +571,12 @@ function cascadeFromPillars(p) {
|
|
|
560
571
|
async function runCompare({ platform = 'claude' } = {}) {
|
|
561
572
|
write(HIDE_CURSOR)
|
|
562
573
|
|
|
563
|
-
// Pull all five sources in parallel
|
|
574
|
+
// Pull all five sources in parallel (verifiers now async via execFile)
|
|
564
575
|
writeln(` ${dim('reading all 5 sources…')}`)
|
|
565
576
|
const [ccPillars, tpData, tdPillars, tsPillars, apPillars] = await Promise.all([
|
|
566
|
-
|
|
577
|
+
ccusagePillars(platform).catch(() => null),
|
|
567
578
|
callTool('tokenpull', { platform }).catch(() => null),
|
|
568
|
-
|
|
579
|
+
(platform === 'claude' ? tokenDashPillars() : Promise.resolve(null)).catch(() => null),
|
|
569
580
|
Promise.resolve(tokscalePillars(platform)),
|
|
570
581
|
Promise.resolve(platform === 'claude' ? appPillars() : null),
|
|
571
582
|
])
|
|
@@ -1154,8 +1165,8 @@ async function runSigRank() {
|
|
|
1154
1165
|
if (k === 'q' || key === '\u0003') { // q or ctrl+c
|
|
1155
1166
|
resolve()
|
|
1156
1167
|
} else if (k === 'b') {
|
|
1157
|
-
|
|
1158
|
-
|
|
1168
|
+
// ASYNC FIX (2026-06-27): execFile instead of dynamic execSync import
|
|
1169
|
+
execFile('open', ['https://signalaf.com'], { stdio: 'ignore' }, () => {})
|
|
1159
1170
|
resolve()
|
|
1160
1171
|
} else if (k === 's') {
|
|
1161
1172
|
const id = ensureIdentity()
|
package/package.json
CHANGED
package/tokenpull.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { readdir, readFile, lstat } from 'node:fs/promises'
|
|
19
19
|
import { existsSync } from 'node:fs'
|
|
20
|
-
import {
|
|
20
|
+
import { 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'
|
package/tools.mjs
CHANGED
|
@@ -15,11 +15,24 @@ import { ALL_PLATFORMS } from './adapters.mjs'
|
|
|
15
15
|
import { ensureIdentity, recordEnrollment } from './keystore.mjs'
|
|
16
16
|
import { submitSignedWindow } from './submit.mjs'
|
|
17
17
|
import { createHash } from 'node:crypto'
|
|
18
|
-
import {
|
|
18
|
+
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
22
|
|
|
23
|
+
// ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
|
|
24
|
+
// defense-in-depth (shell injection prevention + non-blocking). The platform param
|
|
25
|
+
// is enum-validated at the MCP schema level, but execFile also prevents shell
|
|
26
|
+
// interpolation attacks by passing args as an array (no shell parsing).
|
|
27
|
+
function execFileAsync(cmd, args, timeoutMs) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
|
|
30
|
+
if (err) reject(err)
|
|
31
|
+
else resolve(stdout.toString())
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
23
36
|
// ── Verifier readers (sync, on-device, token-only) ────────────────────────────
|
|
24
37
|
// These mirror the implementations in cli.mjs / tui.mjs without the circular import.
|
|
25
38
|
//
|
|
@@ -29,9 +42,9 @@ import path from 'node:path'
|
|
|
29
42
|
// (bunx tokscale, scan+read tokendash) for the TUI/CLI dashboard. Different data sources =
|
|
30
43
|
// different behavior; do NOT merge without understanding the trade-off.
|
|
31
44
|
|
|
32
|
-
function _ccusagePillars(platform = 'claude') {
|
|
45
|
+
async function _ccusagePillars(platform = 'claude') {
|
|
33
46
|
try {
|
|
34
|
-
const raw =
|
|
47
|
+
const raw = await execFileAsync('ccusage', [platform, 'daily', '--json'], 15000)
|
|
35
48
|
const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw)
|
|
36
49
|
const now = Date.now()
|
|
37
50
|
const result = {}
|
|
@@ -57,15 +70,14 @@ function _ccusagePillars(platform = 'claude') {
|
|
|
57
70
|
} catch { return null }
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
function _tokenDashPillars() {
|
|
73
|
+
async function _tokenDashPillars() {
|
|
61
74
|
const dbPath = path.join(os.homedir(), '.claude', 'token-dashboard.db')
|
|
62
75
|
if (!existsSync(dbPath)) return null
|
|
63
76
|
try {
|
|
64
|
-
const raw =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
).
|
|
68
|
-
const [i,o,cw,cr] = raw.split('|').map(Number)
|
|
77
|
+
const raw = await execFileAsync('sqlite3', [dbPath,
|
|
78
|
+
'SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens)+SUM(cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages'
|
|
79
|
+
], 5000)
|
|
80
|
+
const [i,o,cw,cr] = raw.trim().split('|').map(Number)
|
|
69
81
|
return { all: { input: i||0, output: o||0, cacheCreate: cw||0, cacheRead: cr||0 } }
|
|
70
82
|
} catch { return null }
|
|
71
83
|
}
|
|
@@ -578,11 +590,11 @@ export async function callTool(name, args, opts = {}) {
|
|
|
578
590
|
const platform = args?.platform || 'claude'
|
|
579
591
|
const WINS = ['7d', '30d', '90d', 'all']
|
|
580
592
|
|
|
581
|
-
// Pull all four sources in parallel (verifiers are
|
|
593
|
+
// Pull all four sources in parallel (verifiers are now async via execFile)
|
|
582
594
|
const [tpResult, ccPillars, tdPillars, tsPillars] = await Promise.all([
|
|
583
595
|
pullByPlatform(platform, opts).catch(() => null),
|
|
584
|
-
|
|
585
|
-
|
|
596
|
+
_ccusagePillars(platform).catch(() => null),
|
|
597
|
+
(platform === 'claude' ? _tokenDashPillars() : Promise.resolve(null)).catch(() => null),
|
|
586
598
|
Promise.resolve(_tokscalePillars(platform)),
|
|
587
599
|
])
|
|
588
600
|
|
package/tui.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { callTool, DEFAULT_API_BASE } from './tools.mjs'
|
|
|
16
16
|
import { freshVerifierPillars } from './tokenpull.mjs'
|
|
17
17
|
import { isSignedIn, isCodeChar } from './connect.mjs'
|
|
18
18
|
import { loadIdentity, clearIdentity } from './keystore.mjs'
|
|
19
|
-
import {
|
|
19
|
+
import { execFile } from 'child_process'
|
|
20
20
|
import { existsSync, readFileSync } from 'fs'
|
|
21
21
|
import os from 'os'
|
|
22
22
|
import path from 'path'
|
|
@@ -57,6 +57,7 @@ const green = (s) => paint(c.green, s)
|
|
|
57
57
|
const red = (s) => paint(c.red, s)
|
|
58
58
|
|
|
59
59
|
const CLEAR = `${ESC}H${ESC}2J` // home + erase visible area
|
|
60
|
+
const CLEAR_SB = `${ESC}3J` // clear scrollback/saved lines (prevents scroll-up past TUI)
|
|
60
61
|
const ENTER_ALT = `${ESC}?1049h` // enter alternate screen buffer
|
|
61
62
|
const EXIT_ALT = `${ESC}?1049l` // exit alternate screen buffer (restores original)
|
|
62
63
|
const HIDE = `${ESC}?25l`
|
|
@@ -1334,6 +1335,7 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1334
1335
|
}
|
|
1335
1336
|
|
|
1336
1337
|
write(ENTER_ALT) // switch to alternate screen — original terminal state preserved on exit
|
|
1338
|
+
write(CLEAR_SB) // FIXED-WINDOW (2026-06-27): clear scrollback so nothing is above the TUI
|
|
1337
1339
|
write(HIDE)
|
|
1338
1340
|
write(CLEAR)
|
|
1339
1341
|
|
|
@@ -1756,7 +1758,9 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1756
1758
|
if (watchPlatform && watchPlatform !== 'all') watchCmd += ` --platform ${watchPlatform}`
|
|
1757
1759
|
if (watchWindow && watchWindow !== 'all-windows') watchCmd += ` --window ${watchWindow}`
|
|
1758
1760
|
watchCmd += ` --refresh ${watchRefresh}`
|
|
1759
|
-
|
|
1761
|
+
// ASYNC FIX (2026-06-27): execFile instead of execSync — no shell
|
|
1762
|
+
// interpolation. osascript args passed as array (defense-in-depth).
|
|
1763
|
+
execFile('osascript', ['-e', `tell application "Terminal" to do script "${watchCmd}"`], { stdio: 'ignore' }, () => {})
|
|
1760
1764
|
const scope = (!watchPlatform || watchPlatform === 'all') && (!watchWindow || watchWindow === 'all-windows')
|
|
1761
1765
|
? 'all platforms × all windows' : 'focused'
|
|
1762
1766
|
status = `watcher launched (${scope}, ${watchRefresh}s) in a new window`
|