sigrank 0.12.4 → 0.13.0
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 +81 -32
- package/package.json +5 -2
- package/tokenpull.mjs +20 -13
- package/tools.mjs +51 -14
- package/tui.mjs +4 -2
package/README.md
CHANGED
package/cli.mjs
CHANGED
|
@@ -28,10 +28,31 @@ 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 {
|
|
32
|
-
import { existsSync, readFileSync
|
|
31
|
+
import { execFile } from 'child_process'
|
|
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 : ''}`
|
|
41
|
+
|
|
42
|
+
// ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
|
|
43
|
+
// defense-in-depth (shell injection prevention). execFile passes args as an
|
|
44
|
+
// array, so no shell parsing occurs — even if platform contained special chars,
|
|
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.
|
|
48
|
+
function execFileAsync(cmd, args, timeoutMs) {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024, env: { ...process.env, PATH: _envPath } }, (err, stdout) => {
|
|
51
|
+
if (err) reject(err)
|
|
52
|
+
else resolve(stdout.toString())
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
}
|
|
35
56
|
|
|
36
57
|
// ── ANSI helpers (no chalk dep) ────────────────────────────────────────────
|
|
37
58
|
const ESC = '\x1b['
|
|
@@ -407,11 +428,10 @@ async function runMe({ platform = 'claude', compare = false } = {}) {
|
|
|
407
428
|
// identical to _freshCcusage and could be consolidated in a future pass if compare
|
|
408
429
|
// switches to live data — but that's a behavior change, not a refactor.
|
|
409
430
|
|
|
410
|
-
function ccusagePillars(platform = 'claude') {
|
|
431
|
+
async function ccusagePillars(platform = 'claude') {
|
|
411
432
|
// ccusage <platform> daily --json → sum by window
|
|
412
433
|
try {
|
|
413
|
-
const
|
|
414
|
-
const raw = execSync(cmd, { timeout: 15000, stdio: ['ignore', 'pipe', 'ignore'] }).toString()
|
|
434
|
+
const raw = await execFileAsync('ccusage', [platform, 'daily', '--json'], 15000)
|
|
415
435
|
const data = JSON.parse(raw)
|
|
416
436
|
const rows = data.daily ?? data // ccusage may return {daily:[...]} or [...]
|
|
417
437
|
|
|
@@ -448,8 +468,28 @@ function ccusagePillars(platform = 'claude') {
|
|
|
448
468
|
}
|
|
449
469
|
}
|
|
450
470
|
|
|
451
|
-
function tokscalePillars(platform = 'claude') {
|
|
452
|
-
//
|
|
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)
|
|
453
493
|
const reportPath = path.join(os.homedir(), 'tokscale_report.json')
|
|
454
494
|
if (!existsSync(reportPath)) return null
|
|
455
495
|
try {
|
|
@@ -494,28 +534,37 @@ function appPillars() {
|
|
|
494
534
|
}
|
|
495
535
|
}
|
|
496
536
|
|
|
497
|
-
function tokenDashPillars() {
|
|
537
|
+
async function tokenDashPillars() {
|
|
498
538
|
const dbPath = path.join(os.homedir(), '.claude', 'token-dashboard.db')
|
|
499
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%')"
|
|
500
543
|
try {
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
{
|
|
518
|
-
|
|
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
|
+
}
|
|
519
568
|
} catch {
|
|
520
569
|
return null
|
|
521
570
|
}
|
|
@@ -560,13 +609,13 @@ function cascadeFromPillars(p) {
|
|
|
560
609
|
async function runCompare({ platform = 'claude' } = {}) {
|
|
561
610
|
write(HIDE_CURSOR)
|
|
562
611
|
|
|
563
|
-
// Pull all five sources in parallel
|
|
612
|
+
// Pull all five sources in parallel (verifiers now async via execFile)
|
|
564
613
|
writeln(` ${dim('reading all 5 sources…')}`)
|
|
565
614
|
const [ccPillars, tpData, tdPillars, tsPillars, apPillars] = await Promise.all([
|
|
566
|
-
|
|
615
|
+
ccusagePillars(platform).catch(() => null),
|
|
567
616
|
callTool('tokenpull', { platform }).catch(() => null),
|
|
568
|
-
|
|
569
|
-
|
|
617
|
+
(platform === 'claude' ? tokenDashPillars() : Promise.resolve(null)).catch(() => null),
|
|
618
|
+
tokscalePillars(platform).catch(() => null),
|
|
570
619
|
Promise.resolve(platform === 'claude' ? appPillars() : null),
|
|
571
620
|
])
|
|
572
621
|
write(CURSOR_UP(1) + ERASE_LINE)
|
|
@@ -1154,8 +1203,8 @@ async function runSigRank() {
|
|
|
1154
1203
|
if (k === 'q' || key === '\u0003') { // q or ctrl+c
|
|
1155
1204
|
resolve()
|
|
1156
1205
|
} else if (k === 'b') {
|
|
1157
|
-
|
|
1158
|
-
|
|
1206
|
+
// ASYNC FIX (2026-06-27): execFile instead of dynamic execSync import
|
|
1207
|
+
execFile('open', ['https://signalaf.com'], { stdio: 'ignore' }, () => {})
|
|
1159
1208
|
resolve()
|
|
1160
1209
|
} else if (k === 's') {
|
|
1161
1210
|
const id = ensureIdentity()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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",
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
"LICENSE"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
45
|
+
"@zhangferry-dev/tokendash": "^1.7.5",
|
|
46
|
+
"ccusage": "^20.0.14",
|
|
47
|
+
"tokscale": "^4.0.4"
|
|
45
48
|
}
|
|
46
49
|
}
|
package/tokenpull.mjs
CHANGED
|
@@ -17,21 +17,31 @@
|
|
|
17
17
|
|
|
18
18
|
import { readdir, readFile, lstat } from 'node:fs/promises'
|
|
19
19
|
import { existsSync } from 'node:fs'
|
|
20
|
-
import {
|
|
21
|
-
import { join } from 'node:path'
|
|
20
|
+
import { execFile } from 'node:child_process'
|
|
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
|
@@ -15,10 +15,31 @@ 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
|
+
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 : ''}`
|
|
28
|
+
|
|
29
|
+
// ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
|
|
30
|
+
// defense-in-depth (shell injection prevention + non-blocking). The platform param
|
|
31
|
+
// is enum-validated at the MCP schema level, but execFile also prevents shell
|
|
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.
|
|
35
|
+
function execFileAsync(cmd, args, timeoutMs) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
execFile(cmd, args, { timeout: timeoutMs, stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 10 * 1024 * 1024, env: { ...process.env, PATH: _envPath } }, (err, stdout) => {
|
|
38
|
+
if (err) reject(err)
|
|
39
|
+
else resolve(stdout.toString())
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
}
|
|
22
43
|
|
|
23
44
|
// ── Verifier readers (sync, on-device, token-only) ────────────────────────────
|
|
24
45
|
// These mirror the implementations in cli.mjs / tui.mjs without the circular import.
|
|
@@ -29,9 +50,9 @@ import path from 'node:path'
|
|
|
29
50
|
// (bunx tokscale, scan+read tokendash) for the TUI/CLI dashboard. Different data sources =
|
|
30
51
|
// different behavior; do NOT merge without understanding the trade-off.
|
|
31
52
|
|
|
32
|
-
function _ccusagePillars(platform = 'claude') {
|
|
53
|
+
async function _ccusagePillars(platform = 'claude') {
|
|
33
54
|
try {
|
|
34
|
-
const raw =
|
|
55
|
+
const raw = await execFileAsync('ccusage', [platform, 'daily', '--json'], 15000)
|
|
35
56
|
const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw)
|
|
36
57
|
const now = Date.now()
|
|
37
58
|
const result = {}
|
|
@@ -57,20 +78,36 @@ function _ccusagePillars(platform = 'claude') {
|
|
|
57
78
|
} catch { return null }
|
|
58
79
|
}
|
|
59
80
|
|
|
60
|
-
function _tokenDashPillars() {
|
|
81
|
+
async function _tokenDashPillars() {
|
|
61
82
|
const dbPath = path.join(os.homedir(), '.claude', 'token-dashboard.db')
|
|
62
83
|
if (!existsSync(dbPath)) return null
|
|
63
84
|
try {
|
|
64
|
-
const raw =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
).
|
|
68
|
-
const [i,o,cw,cr] = raw.split('|').map(Number)
|
|
85
|
+
const raw = await execFileAsync('sqlite3', [dbPath,
|
|
86
|
+
'SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens)+SUM(cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages'
|
|
87
|
+
], 5000)
|
|
88
|
+
const [i,o,cw,cr] = raw.trim().split('|').map(Number)
|
|
69
89
|
return { all: { input: i||0, output: o||0, cacheCreate: cw||0, cacheRead: cr||0 } }
|
|
70
90
|
} catch { return null }
|
|
71
91
|
}
|
|
72
92
|
|
|
73
|
-
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
|
|
74
111
|
const p = path.join(os.homedir(), 'tokscale_report.json')
|
|
75
112
|
if (!existsSync(p)) return null
|
|
76
113
|
try {
|
|
@@ -578,12 +615,12 @@ export async function callTool(name, args, opts = {}) {
|
|
|
578
615
|
const platform = args?.platform || 'claude'
|
|
579
616
|
const WINS = ['7d', '30d', '90d', 'all']
|
|
580
617
|
|
|
581
|
-
// Pull all four sources in parallel (verifiers are
|
|
618
|
+
// Pull all four sources in parallel (verifiers are now async via execFile)
|
|
582
619
|
const [tpResult, ccPillars, tdPillars, tsPillars] = await Promise.all([
|
|
583
620
|
pullByPlatform(platform, opts).catch(() => null),
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
621
|
+
_ccusagePillars(platform).catch(() => null),
|
|
622
|
+
(platform === 'claude' ? _tokenDashPillars() : Promise.resolve(null)).catch(() => null),
|
|
623
|
+
_tokscalePillars(platform).catch(() => null),
|
|
587
624
|
])
|
|
588
625
|
|
|
589
626
|
// Build tokenpull window lookup
|
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'
|
|
@@ -1758,7 +1758,9 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1758
1758
|
if (watchPlatform && watchPlatform !== 'all') watchCmd += ` --platform ${watchPlatform}`
|
|
1759
1759
|
if (watchWindow && watchWindow !== 'all-windows') watchCmd += ` --window ${watchWindow}`
|
|
1760
1760
|
watchCmd += ` --refresh ${watchRefresh}`
|
|
1761
|
-
|
|
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' }, () => {})
|
|
1762
1764
|
const scope = (!watchPlatform || watchPlatform === 'all') && (!watchWindow || watchWindow === 'all-windows')
|
|
1763
1765
|
? 'all platforms × all windows' : 'focused'
|
|
1764
1766
|
status = `watcher launched (${scope}, ${watchRefresh}s) in a new window`
|