sigrank 0.11.5 → 0.12.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/cli.mjs +117 -49
- package/package.json +1 -1
- package/tokenpull.mjs +118 -0
- package/tools.mjs +36 -6
- package/tui.mjs +309 -114
package/cli.mjs
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* npx sigrank me your cascade across all 4 windows
|
|
13
13
|
* npx sigrank me --platform amp use a different platform adapter
|
|
14
14
|
* npx sigrank me --compare raw pillar comparison: ccusage vs tokenpull vs token-dashboard
|
|
15
|
-
* npx sigrank watch RT tune meter —
|
|
16
|
-
* npx sigrank watch --
|
|
15
|
+
* npx sigrank watch RT tune meter — ALL active platforms × all windows
|
|
16
|
+
* npx sigrank watch --platform X watch one platform only (optional filter)
|
|
17
|
+
* npx sigrank watch --window 7d watch one window only (optional filter)
|
|
17
18
|
*
|
|
18
19
|
* Color palette mirrors the SigRank web dark theme:
|
|
19
20
|
* gold = class TRANSMITTER headline + rank #1
|
|
@@ -684,37 +685,50 @@ async function runCompare({ platform = 'claude' } = {}) {
|
|
|
684
685
|
}
|
|
685
686
|
|
|
686
687
|
// ── WATCH command ─────────────────────────────────────────────────────────────
|
|
688
|
+
//
|
|
689
|
+
// A5: by default (no --platform / --window) the watcher auto-loads EVERYTHING — every
|
|
690
|
+
// active platform (input+output > 0) × every window (7d/30d/90d/all), refreshed each tick.
|
|
691
|
+
// --platform / --window remain OPTIONAL filters for back-compat (focus one cell). The
|
|
692
|
+
// refresh loop + --submit behavior are preserved; submit publishes whichever windows are
|
|
693
|
+
// shown whenever their Υ changes.
|
|
694
|
+
|
|
695
|
+
const WATCH_WINDOWS = ['7d', '30d', '90d', 'all']
|
|
696
|
+
|
|
697
|
+
// Detect which platforms have real local data (input+output > 0 in any window) via tokenpull.
|
|
698
|
+
// Returns an array of pull results (the same shape tokenpullAny yields), one per active platform.
|
|
699
|
+
async function detectActivePulls() {
|
|
700
|
+
const { tokenpullAny } = await import('./tokenpull.mjs')
|
|
701
|
+
const settled = await Promise.allSettled(ALL_PLATFORMS.map((p) => tokenpullAny(p)))
|
|
702
|
+
const active = []
|
|
703
|
+
for (const r of settled) {
|
|
704
|
+
if (r.status !== 'fulfilled' || !r.value) continue
|
|
705
|
+
const d = r.value
|
|
706
|
+
const live = (d.windows || []).some((w) => (w.pillars.input + w.pillars.output) > 0)
|
|
707
|
+
if (live) active.push(d)
|
|
708
|
+
}
|
|
709
|
+
return active
|
|
710
|
+
}
|
|
687
711
|
|
|
688
|
-
async function runWatch({ platform
|
|
689
|
-
let prev = null
|
|
712
|
+
async function runWatch({ platform, window: win, refresh = 30, submit = false } = {}) {
|
|
690
713
|
let lines = 0
|
|
691
|
-
let lastSubmit = null
|
|
692
714
|
const id = submit ? ensureIdentity() : null
|
|
693
715
|
const enrolled = !!(id && id.codename && id.operator_id && id.private_key_pkcs8_b64)
|
|
694
716
|
|
|
695
|
-
|
|
717
|
+
// Filters: when provided, narrow the grid; when absent, watch all active × all windows.
|
|
718
|
+
const winFilter = typeof win === 'string' && win ? win : null
|
|
719
|
+
const platFilter = typeof platform === 'string' && platform ? platform : null
|
|
720
|
+
const windows = winFilter ? [winFilter] : WATCH_WINDOWS
|
|
696
721
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
result = await callTool('watch_tokenpull', { platform, window: win })
|
|
701
|
-
} catch (e) {
|
|
702
|
-
writeln(red(` ✗ ${e.message}`))
|
|
703
|
-
return
|
|
704
|
-
}
|
|
722
|
+
// Per-cell Υ memory (key = `${platform}|${window}`) so each cell tracks its own change.
|
|
723
|
+
const prevY = new Map()
|
|
724
|
+
const lastSubmitMsg = new Map()
|
|
705
725
|
|
|
706
|
-
|
|
707
|
-
const changed = prev !== null && prev !== cas?.yield
|
|
726
|
+
write(HIDE_CURSOR)
|
|
708
727
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
lastSubmit = r.status === 'received' ? green(`✓ submitted · tier=${r.verification_tier || '—'}`) : red(`✗ ${r.reason || r.status}`)
|
|
714
|
-
} catch (e) {
|
|
715
|
-
lastSubmit = red(`✗ ${e.message}`)
|
|
716
|
-
}
|
|
717
|
-
}
|
|
728
|
+
const draw = async () => {
|
|
729
|
+
// Re-detect each tick so a platform that just started writing logs appears automatically.
|
|
730
|
+
let pulls = await detectActivePulls().catch(() => [])
|
|
731
|
+
if (platFilter) pulls = pulls.filter((d) => d.platform === platFilter)
|
|
718
732
|
|
|
719
733
|
if (lines > 0) write(CURSOR_UP(lines))
|
|
720
734
|
|
|
@@ -723,31 +737,64 @@ async function runWatch({ platform = 'claude', window: win = '7d', refresh = 30,
|
|
|
723
737
|
const w = termWidth()
|
|
724
738
|
const ts = new Date().toLocaleTimeString('en-US', { hour12: false })
|
|
725
739
|
|
|
740
|
+
const scopeLabel = (platFilter || 'all active platforms') + ' · ' + (winFilter ? `window: ${winFilter}` : 'all windows')
|
|
741
|
+
|
|
726
742
|
push()
|
|
727
|
-
push(` ${gold('⊙ SigRank')} ${bold('Watch')} ${dim(`${
|
|
743
|
+
push(` ${gold('⊙ SigRank')} ${bold('Watch')} ${dim(`${scopeLabel} · ${ts}`)}`)
|
|
728
744
|
push(` ${dim('─'.repeat(w - 4))}`)
|
|
729
|
-
push()
|
|
730
745
|
|
|
731
|
-
|
|
732
|
-
|
|
746
|
+
if (pulls.length === 0) {
|
|
747
|
+
push()
|
|
748
|
+
push(` ${dim('no active platforms detected — run some sessions, this will pick them up automatically')}`)
|
|
749
|
+
push()
|
|
750
|
+
}
|
|
733
751
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
752
|
+
for (const d of pulls) {
|
|
753
|
+
const plat = d.platform
|
|
754
|
+
const byWin = {}
|
|
755
|
+
for (const ww of (d.windows || [])) byWin[ww.window] = ww
|
|
756
|
+
push()
|
|
757
|
+
push(` ${cyan(plat)}${d.estimated ? dim(' (est)') : ''}`)
|
|
758
|
+
for (const wk of windows) {
|
|
759
|
+
const ww = byWin[wk]
|
|
760
|
+
const cas = ww ? cascadeFromPillars(ww.pillars) : null
|
|
761
|
+
const key = `${plat}|${wk}`
|
|
762
|
+
const prev = prevY.get(key)
|
|
763
|
+
const yNow = cas?.yield ?? null
|
|
764
|
+
const changed = prev !== undefined && prev !== yNow
|
|
765
|
+
|
|
766
|
+
// --submit: publish this cell on first observation + whenever its Υ changes.
|
|
767
|
+
if (submit && enrolled && ww && yNow != null && (prev === undefined || changed)) {
|
|
768
|
+
try {
|
|
769
|
+
const r = await submitSignedWindow(wk, ww.pillars, ww.messages, id, { platform: plat })
|
|
770
|
+
lastSubmitMsg.set(key, r.status === 'received' ? green(`✓ tier=${r.verification_tier || '—'}`) : red(`✗ ${r.reason || r.status}`))
|
|
771
|
+
} catch (e) {
|
|
772
|
+
lastSubmitMsg.set(key, red(`✗ ${e.message}`))
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
prevY.set(key, yNow)
|
|
776
|
+
|
|
777
|
+
const winLabel = wk === 'all' ? 'all-time' : wk
|
|
778
|
+
const yDisplay = cas?.yield != null ? gold(fmtYield(cas.yield)) : dim('—')
|
|
779
|
+
const indicator = changed ? green(' ▲') : dim(' ·')
|
|
780
|
+
const metrics = cas
|
|
781
|
+
? `${dim('SNR')} ${fmtSNR(cas.snr)} ${dim('Lev')} ${cas.leverage != null ? fmtLev(cas.leverage) + '×' : '—'} ${dim('Vel')} ${cas.velocity != null ? cas.velocity.toFixed(2) : '—'} ${colorClass(cas.class ?? '—')}`
|
|
782
|
+
: dim('no data')
|
|
783
|
+
const submitNote = (submit && enrolled) ? ' ' + (lastSubmitMsg.get(key) || dim('…')) : ''
|
|
784
|
+
push(` ${padEnd(winLabel, 8)} ${bold('Υ')} ${yDisplay}${indicator} ${metrics}${submitNote}`)
|
|
785
|
+
}
|
|
743
786
|
}
|
|
787
|
+
|
|
788
|
+
push()
|
|
744
789
|
push(` ${dim('─'.repeat(w - 4))}`)
|
|
790
|
+
if (submit && !enrolled) {
|
|
791
|
+
push(` ${red('not enrolled — run `npx sigrank enroll` to auto-submit')}`)
|
|
792
|
+
}
|
|
745
793
|
push(` ${dim(`polling every ${refresh}s · ${submit ? 'auto-submit ON · ' : ''}tokens stay on your machine · ctrl+c to exit`)}`)
|
|
746
794
|
push()
|
|
747
795
|
|
|
748
796
|
write(out.join('\n'))
|
|
749
797
|
lines = out.length
|
|
750
|
-
prev = cas?.yield ?? null
|
|
751
798
|
}
|
|
752
799
|
|
|
753
800
|
try {
|
|
@@ -1213,11 +1260,12 @@ async function showHelp() {
|
|
|
1213
1260
|
writeln(` ${cyan('compare --platform codex')} compare for a specific platform`)
|
|
1214
1261
|
writeln(` ${cyan('tui')} full tabbed TUI: Dashboard / Compare / Board / Watch`)
|
|
1215
1262
|
writeln(` ${cyan('tui --platform codex')} TUI with a different default platform`)
|
|
1216
|
-
writeln(` ${cyan('watch')} live tune meter —
|
|
1217
|
-
writeln(` ${cyan('watch --
|
|
1263
|
+
writeln(` ${cyan('watch')} live tune meter — ALL active platforms × all windows, every 30s`)
|
|
1264
|
+
writeln(` ${cyan('watch --platform codex')} watch only one platform (optional filter)`)
|
|
1265
|
+
writeln(` ${cyan('watch --window 7d')} watch only one window (optional filter)`)
|
|
1218
1266
|
writeln()
|
|
1219
1267
|
writeln(` ${bold('Options')}`)
|
|
1220
|
-
writeln(` ${dim('--window')} 7d · 30d · 90d · all (default: 30d for board
|
|
1268
|
+
writeln(` ${dim('--window')} 7d · 30d · 90d · all (default: 30d for board; all windows for watch)`)
|
|
1221
1269
|
writeln(` ${dim('--platform')} claude · codex · amp · gemini · opencode · goose · …`)
|
|
1222
1270
|
writeln(` ${dim('--refresh')} poll interval in seconds (default: 30)`)
|
|
1223
1271
|
writeln(` ${dim('--once')} print once and exit (board only)`)
|
|
@@ -1337,14 +1385,31 @@ export async function runCli(argv) {
|
|
|
1337
1385
|
const args = argv.slice(2) // strip 'node' + script path
|
|
1338
1386
|
const cmd = args[0]
|
|
1339
1387
|
|
|
1340
|
-
// parse --key value flags
|
|
1388
|
+
// parse --key value AND --key=value flags. E4: validate against the known set and warn
|
|
1389
|
+
// (don't crash) on anything unknown, so a typo'd flag is visible instead of silently ignored.
|
|
1390
|
+
const KNOWN_FLAGS = new Set(['window', 'platform', 'refresh', 'once', 'submit', 'compare', 'label'])
|
|
1341
1391
|
const flags = {}
|
|
1342
1392
|
for (let i = 1; i < args.length; i++) {
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1393
|
+
const a = args[i]
|
|
1394
|
+
if (!a.startsWith('--')) continue
|
|
1395
|
+
const body = a.slice(2)
|
|
1396
|
+
let key, val
|
|
1397
|
+
const eq = body.indexOf('=')
|
|
1398
|
+
if (eq !== -1) {
|
|
1399
|
+
// --flag=value form
|
|
1400
|
+
key = body.slice(0, eq)
|
|
1401
|
+
val = body.slice(eq + 1)
|
|
1402
|
+
} else {
|
|
1403
|
+
// --flag [value] form: consume the next token as the value unless it's another flag
|
|
1404
|
+
key = body
|
|
1405
|
+
val = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true
|
|
1406
|
+
}
|
|
1407
|
+
if (!key) continue
|
|
1408
|
+
if (!KNOWN_FLAGS.has(key)) {
|
|
1409
|
+
writeln(dim(` (ignoring unknown flag --${key}; known: ${[...KNOWN_FLAGS].join(', ')})`))
|
|
1410
|
+
continue
|
|
1347
1411
|
}
|
|
1412
|
+
flags[key] = val
|
|
1348
1413
|
}
|
|
1349
1414
|
|
|
1350
1415
|
try {
|
|
@@ -1362,9 +1427,12 @@ export async function runCli(argv) {
|
|
|
1362
1427
|
const { runTui } = await import('./tui.mjs')
|
|
1363
1428
|
await runTui({ platform: flags.platform ?? 'claude', window: flags.window ?? '7d' })
|
|
1364
1429
|
} else if (cmd === 'watch') {
|
|
1430
|
+
// A5: no --platform/--window → watch ALL active platforms × ALL windows. Pass the raw
|
|
1431
|
+
// flags through (undefined = no filter) so runWatch's auto-all mode engages by default;
|
|
1432
|
+
// a provided flag narrows the grid (back-compat).
|
|
1365
1433
|
await runWatch({
|
|
1366
|
-
platform: flags.platform
|
|
1367
|
-
window: flags.window
|
|
1434
|
+
platform: typeof flags.platform === 'string' ? flags.platform : undefined,
|
|
1435
|
+
window: typeof flags.window === 'string' ? flags.window : undefined,
|
|
1368
1436
|
refresh: Number(flags.refresh) || 30,
|
|
1369
1437
|
submit: flags.submit === true || flags.submit === 'true',
|
|
1370
1438
|
})
|
package/package.json
CHANGED
package/tokenpull.mjs
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { readdir, readFile, lstat } from 'node:fs/promises'
|
|
19
|
+
import { existsSync } from 'node:fs'
|
|
20
|
+
import { execSync } from 'node:child_process'
|
|
19
21
|
import { join } from 'node:path'
|
|
20
22
|
import { homedir } from 'node:os'
|
|
21
23
|
import { ADAPTERS } from './adapters.mjs'
|
|
@@ -280,3 +282,119 @@ export async function tokenpullAny(platform, opts = {}) {
|
|
|
280
282
|
if (adapter.setupNote) result.setupNote = adapter.setupNote
|
|
281
283
|
return result
|
|
282
284
|
}
|
|
285
|
+
|
|
286
|
+
// ── Fresh verifier pull (#9 / C1) ─────────────────────────────────────────────
|
|
287
|
+
// The owner-requested FRESH pull of the three external verifier sources. Unlike the
|
|
288
|
+
// stale snapshot readers (a 3-day-old token-dashboard.db, a static tokscale_report.json),
|
|
289
|
+
// these RUN the source tool on every call so the numbers are live. Token-only: each tool
|
|
290
|
+
// reports usage counts, never transcript content. Every external call is wrapped so a
|
|
291
|
+
// missing binary / parse error / timeout returns null and NEVER throws — a verifier the
|
|
292
|
+
// machine doesn't have simply doesn't appear.
|
|
293
|
+
//
|
|
294
|
+
// Returns { ccusage, tokscale, tokendash } where each value is either null or an object
|
|
295
|
+
// keyed by window: { '7d':{...}, '30d':{...}, '90d':{...}, all:{...} } (windows that can't
|
|
296
|
+
// be derived from a source are omitted; 'all' is present whenever the source has data).
|
|
297
|
+
|
|
298
|
+
// ccusage: run `ccusage <platform> daily --json` and bucket the daily rows into the four
|
|
299
|
+
// windows by date. Logic copied from tools.mjs `_ccusagePillars` (do NOT import private fns).
|
|
300
|
+
function _freshCcusage(platform = 'claude') {
|
|
301
|
+
try {
|
|
302
|
+
const raw = execSync(`ccusage ${platform} daily --json`, { timeout: 15000, stdio: ['ignore', 'pipe', 'ignore'] }).toString()
|
|
303
|
+
const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw)
|
|
304
|
+
if (!Array.isArray(rows)) return null
|
|
305
|
+
const now = Date.now()
|
|
306
|
+
const result = {}
|
|
307
|
+
for (const [win, days] of Object.entries({ '7d': 7, '30d': 30, '90d': 90 })) {
|
|
308
|
+
const since = new Date(now - days * DAY_MS)
|
|
309
|
+
let i = 0, o = 0, cw = 0, cr = 0
|
|
310
|
+
for (const r of rows) {
|
|
311
|
+
if (new Date(r.date ?? r.day ?? '1970') >= since) {
|
|
312
|
+
i += r.inputTokens ?? r.input_tokens ?? 0
|
|
313
|
+
o += r.outputTokens ?? r.output_tokens ?? 0
|
|
314
|
+
cw += r.cacheCreationTokens ?? r.cache_create_tokens ?? 0
|
|
315
|
+
cr += r.cacheReadTokens ?? r.cache_read_tokens ?? 0
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
result[win] = { input: i, output: o, cacheCreate: cw, cacheRead: cr }
|
|
319
|
+
}
|
|
320
|
+
let i = 0, o = 0, cw = 0, cr = 0
|
|
321
|
+
for (const r of rows) {
|
|
322
|
+
i += r.inputTokens ?? 0; o += r.outputTokens ?? 0; cw += r.cacheCreationTokens ?? 0; cr += r.cacheReadTokens ?? 0
|
|
323
|
+
}
|
|
324
|
+
result['all'] = { input: i, output: o, cacheCreate: cw, cacheRead: cr }
|
|
325
|
+
return result
|
|
326
|
+
} catch { return null }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// tokendash: REFRESH ~/.claude/token-dashboard.db by running the dashboard's scan, then read
|
|
330
|
+
// the live db via sqlite3. Claude only (the db only holds Claude Code sessions). The scan is
|
|
331
|
+
// best-effort: if it fails (tool missing, slow), we still read whatever the db currently has.
|
|
332
|
+
function _freshTokendash(platform = 'claude') {
|
|
333
|
+
if (platform !== 'claude') return null
|
|
334
|
+
const dbPath = join(homedir(), '.claude', 'token-dashboard.db')
|
|
335
|
+
const scanCli = join(homedir(), 'Desktop', 'token-dashboard', 'cli.py')
|
|
336
|
+
// Step 1 — refresh the db (best-effort; failures must not abort the read).
|
|
337
|
+
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 */ }
|
|
341
|
+
}
|
|
342
|
+
// Step 2 — read the (now-refreshed) db. all-time only; the db doesn't expose windowing here.
|
|
343
|
+
if (!existsSync(dbPath)) return null
|
|
344
|
+
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)
|
|
350
|
+
if (![i, o, cw, cr].some((n) => Number.isFinite(n) && n > 0)) return null
|
|
351
|
+
return { all: { input: i || 0, output: o || 0, cacheCreate: cw || 0, cacheRead: cr || 0 } }
|
|
352
|
+
} catch { return null }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// tokscale: run `bunx tokscale@latest models --json` and sum the per-(client,model) rows for
|
|
356
|
+
// the requested platform. The JSON shape (verified live) is:
|
|
357
|
+
// { entries: [ { client, model, input, output, cacheRead, cacheWrite, ... }, ... ], ... }
|
|
358
|
+
// `client` maps directly to our platform name. Synthetic/unknown model rows are dropped (they
|
|
359
|
+
// carry no real usage). all-time only — tokscale's models view is not windowed. If no row
|
|
360
|
+
// matches the platform, return null gracefully.
|
|
361
|
+
function _freshTokscale(platform = 'claude') {
|
|
362
|
+
try {
|
|
363
|
+
const raw = execSync(`bunx tokscale@latest models --json`, { timeout: 60000, stdio: ['ignore', 'pipe', 'ignore'] }).toString()
|
|
364
|
+
const data = JSON.parse(raw)
|
|
365
|
+
const entries = Array.isArray(data?.entries) ? data.entries : (Array.isArray(data) ? data : [])
|
|
366
|
+
const rows = entries.filter((e) =>
|
|
367
|
+
e && e.client === platform && e.model !== '<synthetic>' && e.model !== 'unknown' &&
|
|
368
|
+
((Number(e.input) || 0) > 0 || (Number(e.output) || 0) > 0),
|
|
369
|
+
)
|
|
370
|
+
if (!rows.length) return null
|
|
371
|
+
const acc = rows.reduce((a, e) => ({
|
|
372
|
+
input: a.input + (Number(e.input) || 0),
|
|
373
|
+
output: a.output + (Number(e.output) || 0),
|
|
374
|
+
cacheCreate: a.cacheCreate + (Number(e.cacheWrite) || 0),
|
|
375
|
+
cacheRead: a.cacheRead + (Number(e.cacheRead) || 0),
|
|
376
|
+
}), { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 })
|
|
377
|
+
return { all: acc }
|
|
378
|
+
} catch { return null }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Fresh-pull all three external verifier sources for a platform. Each is run live (no stale
|
|
383
|
+
* snapshot) and returned as { window → {input,output,cacheCreate,cacheRead} } or null when the
|
|
384
|
+
* source is unavailable / has no data for the platform. Never throws — a failing source is null.
|
|
385
|
+
*
|
|
386
|
+
* - ccusage : `ccusage <platform> daily --json` → 7d/30d/90d/all
|
|
387
|
+
* - tokendash: refresh + read ~/.claude/token-dashboard.db → all (claude only)
|
|
388
|
+
* - tokscale : `bunx tokscale@latest models --json` → all
|
|
389
|
+
*/
|
|
390
|
+
export async function freshVerifierPillars(platform = 'claude') {
|
|
391
|
+
const p = platform || 'claude'
|
|
392
|
+
// Each call is internally try/catch → null; await Promise.all of resolved values keeps the
|
|
393
|
+
// helper async (per the contract) without any individual failure rejecting the batch.
|
|
394
|
+
const [ccusage, tokendash, tokscale] = await Promise.all([
|
|
395
|
+
Promise.resolve().then(() => _freshCcusage(p)).catch(() => null),
|
|
396
|
+
Promise.resolve().then(() => _freshTokendash(p)).catch(() => null),
|
|
397
|
+
Promise.resolve().then(() => _freshTokscale(p)).catch(() => null),
|
|
398
|
+
])
|
|
399
|
+
return { ccusage, tokscale, tokendash }
|
|
400
|
+
}
|
package/tools.mjs
CHANGED
|
@@ -113,6 +113,9 @@ async function pullByPlatform(platform, opts = {}) {
|
|
|
113
113
|
export const DEFAULT_API_BASE = process.env.SIGRANK_API_BASE || 'https://signalaf.com'
|
|
114
114
|
/** Default network timeout in ms (override via opts.fetchTimeout or SIGRANK_FETCH_TIMEOUT). */
|
|
115
115
|
export const DEFAULT_FETCH_TIMEOUT = Number(process.env.SIGRANK_FETCH_TIMEOUT) || 10_000
|
|
116
|
+
/** Max accepted length for a single paste/string arg (chars). Token counts are tiny; anything
|
|
117
|
+
* past this is malformed or abusive — reject cleanly before parsing/POSTing (E2 hardening). */
|
|
118
|
+
const MAX_INPUT = 1_000_000
|
|
116
119
|
|
|
117
120
|
export const TOOLS = [
|
|
118
121
|
{
|
|
@@ -246,6 +249,13 @@ export const TOOLS = [
|
|
|
246
249
|
// tokenpull window key → the board's window_type enum.
|
|
247
250
|
const WINDOW_TYPE = { '7d': '7d', '30d': '30d', '90d': '90d', all: 'all_time' }
|
|
248
251
|
|
|
252
|
+
// E3: client-side auto-submit cooldown for watch_tokenpull. The server already dedups
|
|
253
|
+
// identical snapshots (exact hash → 422), but a noisy poll loop with submit:true could still
|
|
254
|
+
// churn the network/board with near-identical rows. Cap auto-submit to once per WATCH_SUBMIT_COOLDOWN_MS
|
|
255
|
+
// per window (in-memory, per process). Keyed by window so different windows don't block each other.
|
|
256
|
+
const WATCH_SUBMIT_COOLDOWN_MS = 5 * 60 * 1000
|
|
257
|
+
const _lastWatchSubmitAt = new Map()
|
|
258
|
+
|
|
249
259
|
export async function callTool(name, args, opts = {}) {
|
|
250
260
|
const apiBase = opts.apiBase || DEFAULT_API_BASE
|
|
251
261
|
const timeoutMs = opts.fetchTimeout ?? DEFAULT_FETCH_TIMEOUT
|
|
@@ -390,6 +400,9 @@ export async function callTool(name, args, opts = {}) {
|
|
|
390
400
|
|
|
391
401
|
if (name === 'submit_paste') {
|
|
392
402
|
if (!args?.text) throw new Error('submit_paste requires a non-empty `text` argument.')
|
|
403
|
+
if (typeof args.text === 'string' && args.text.length > MAX_INPUT) {
|
|
404
|
+
return { status: 'error', reason: 'input_too_large', detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.` }
|
|
405
|
+
}
|
|
393
406
|
// Local preview first — also validates the paste is parseable before any POST.
|
|
394
407
|
const pillars = parsePillars(args.text)
|
|
395
408
|
const c = withParseWarnings(pillars, cascade(pillars))
|
|
@@ -469,6 +482,13 @@ export async function callTool(name, args, opts = {}) {
|
|
|
469
482
|
// in the same shape as tokenpull output for easy follow-up with tokenpull_submit.
|
|
470
483
|
const WINDOW_KEYS = ['7d', '30d', '90d', 'all']
|
|
471
484
|
const sourceTool = args?.source_tool || null
|
|
485
|
+
// E2: reject any oversized window paste up front (token tables are tiny).
|
|
486
|
+
for (const wk of WINDOW_KEYS) {
|
|
487
|
+
const v = args?.[wk]
|
|
488
|
+
if (typeof v === 'string' && v.length > MAX_INPUT) {
|
|
489
|
+
return { status: 'error', reason: 'input_too_large', detail: `window '${wk}' exceeds ${MAX_INPUT} chars (${v.length}). Paste only the token-count table.` }
|
|
490
|
+
}
|
|
491
|
+
}
|
|
472
492
|
const windows = []
|
|
473
493
|
for (const wk of WINDOW_KEYS) {
|
|
474
494
|
const text = args?.[wk]
|
|
@@ -514,12 +534,22 @@ export async function callTool(name, args, opts = {}) {
|
|
|
514
534
|
if (args?.submit === true) {
|
|
515
535
|
const id = opts.identity || ensureIdentity()
|
|
516
536
|
if (id.codename && id.operator_id && id.private_key_pkcs8_b64) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
537
|
+
// E3: client-side cooldown — at most one auto-submit per window per 5 min. Prevents a
|
|
538
|
+
// fast poll loop from churning the board even before the server's hash-dedup kicks in.
|
|
539
|
+
const clockNow = typeof opts.now === 'number' ? opts.now : Date.now()
|
|
540
|
+
const last = _lastWatchSubmitAt.get(watchWindow)
|
|
541
|
+
if (last != null && clockNow - last < WATCH_SUBMIT_COOLDOWN_MS) {
|
|
542
|
+
const waitS = Math.ceil((WATCH_SUBMIT_COOLDOWN_MS - (clockNow - last)) / 1000)
|
|
543
|
+
auth_submit = { status: 'cooldown', detail: `auto-submit for '${watchWindow}' on cooldown — next in ~${waitS}s (max once / 5 min).`, retry_after_s: waitS }
|
|
544
|
+
} else {
|
|
545
|
+
_lastWatchSubmitAt.set(watchWindow, clockNow)
|
|
546
|
+
auth_submit = await submitSignedWindow(watchWindow, win.pillars, win.messages, id, {
|
|
547
|
+
apiBase,
|
|
548
|
+
fetchImpl: doFetch,
|
|
549
|
+
platform: pulled.platform,
|
|
550
|
+
now: opts.now,
|
|
551
|
+
})
|
|
552
|
+
}
|
|
523
553
|
} else {
|
|
524
554
|
auth_submit = { status: 'not_enrolled', detail: 'Run `npx sigrank-mcp enroll` to auto-submit verified runs.' }
|
|
525
555
|
}
|
package/tui.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { callTool, DEFAULT_API_BASE } from './tools.mjs'
|
|
16
|
+
import { freshVerifierPillars } from './tokenpull.mjs'
|
|
16
17
|
import { isSignedIn, isCodeChar } from './connect.mjs'
|
|
17
18
|
import { loadIdentity, clearIdentity } from './keystore.mjs'
|
|
18
19
|
import { execSync } from 'child_process'
|
|
@@ -301,10 +302,7 @@ async function loadDashboardData() {
|
|
|
301
302
|
// then fill the other 13 as they resolve. The caller passes an onFirst callback
|
|
302
303
|
// that fires when claude is ready so the user sees their cascade within ~1 read,
|
|
303
304
|
// not after a 14-platform barrier.
|
|
304
|
-
const
|
|
305
|
-
tokenpullAny('claude').catch(() => null),
|
|
306
|
-
Promise.resolve(tokenDashPillars()),
|
|
307
|
-
])
|
|
305
|
+
const claudeResult = await tokenpullAny('claude').catch(() => null)
|
|
308
306
|
|
|
309
307
|
const active = []
|
|
310
308
|
if (claudeResult) {
|
|
@@ -312,13 +310,13 @@ async function loadDashboardData() {
|
|
|
312
310
|
if (all && (all.pillars.input ?? 0) + (all.pillars.output ?? 0) > 0) active.push(claudeResult)
|
|
313
311
|
}
|
|
314
312
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
313
|
+
// FIX A1 (2026-06-27): removed the 14× SYNCHRONOUS ccusage `verifierMap` build (measured 39,816 ms)
|
|
314
|
+
// that blocked the very first Dashboard paint — and the render never read `verifierMap` or `tdPillars`.
|
|
315
|
+
// (Also dropped the discarded second `tokenDashPillars()` call above.) Verifier comparison
|
|
316
|
+
// (ccusage / tokscale / token-dashboard) now runs FRESH + on-demand in the Compare tab, scoped to
|
|
317
|
+
// active platforms — see loadCompareData. Dashboard paints from tokenpull in ~2s.
|
|
320
318
|
const boardData = await boardPromise
|
|
321
|
-
return { active, verifierMap, tdPillars:
|
|
319
|
+
return { active, verifierMap: {}, tdPillars: null, boardData, _loading: true, _remaining: ALL_PLATFORMS.slice(1) }
|
|
322
320
|
}
|
|
323
321
|
|
|
324
322
|
/** FIX 0: fill the remaining platforms after the primary render. Mutates dashData.active. */
|
|
@@ -342,20 +340,42 @@ async function fillDashboardRest(dashData) {
|
|
|
342
340
|
return true
|
|
343
341
|
}
|
|
344
342
|
|
|
343
|
+
// #9 / C1 (2026-06-27): Compare pulls the three external verifier sources FRESH + on-demand
|
|
344
|
+
// (ccusage + tokscale models + a token-dashboard rescan), via tokenpull.freshVerifierPillars —
|
|
345
|
+
// NOT the old stale snapshot readers (static tokscale_report.json / 3-day-old db). This runs ONLY
|
|
346
|
+
// when the Compare tab opens (it's a 5–60s scan), never on the Dashboard load path. Scoped to the
|
|
347
|
+
// current platform. tokenpull (the canonical board/cascade source) stays the fast/fresh tpData.
|
|
345
348
|
async function loadCompareData(platform = 'claude') {
|
|
346
349
|
const tpData = await callTool('tokenpull', { platform }).catch(() => null)
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
return { tpData, cc, ts, td, platform }
|
|
350
|
+
// Fresh pull — { ccusage, tokscale, tokendash }, each null or window-keyed pillars.
|
|
351
|
+
const fresh = await freshVerifierPillars(platform).catch(() => ({ ccusage: null, tokscale: null, tokendash: null }))
|
|
352
|
+
return { tpData, cc: fresh.ccusage, ts: fresh.tokscale, td: fresh.tokendash, platform }
|
|
351
353
|
}
|
|
352
354
|
|
|
355
|
+
// #8 (2026-06-27): the Board tab shows ALL SUBMISSIONS ranked (raw submission rows), not operator
|
|
356
|
+
// aggregates. Try the NEW GET /api/v1/submissions first; if it's not deployed yet (non-200 / 404),
|
|
357
|
+
// FALL BACK to the existing /api/v1/leaderboard so nothing breaks pre-deploy. The return is tagged
|
|
358
|
+
// with `_source` ('submissions' | 'leaderboard') so renderBoard picks the right column layout.
|
|
353
359
|
async function loadBoardData(window = '30d') {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
360
|
+
// Primary: submissions endpoint (ranked submission rows).
|
|
361
|
+
try {
|
|
362
|
+
const res = await fetch(`${DEFAULT_API_BASE}/api/v1/submissions?window=${window}&metric=yield_`, {
|
|
363
|
+
headers: { accept: 'application/json' },
|
|
364
|
+
})
|
|
365
|
+
if (res.ok) {
|
|
366
|
+
const data = await res.json()
|
|
367
|
+
return { ...data, _source: 'submissions' }
|
|
368
|
+
}
|
|
369
|
+
} catch { /* fall through to leaderboard */ }
|
|
370
|
+
// Fallback: existing leaderboard endpoint + current render (pre-deploy safety).
|
|
371
|
+
try {
|
|
372
|
+
const res = await fetch(`${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}&metric=yield_`, {
|
|
373
|
+
headers: { accept: 'application/json' },
|
|
374
|
+
})
|
|
375
|
+
if (!res.ok) return null
|
|
376
|
+
const data = await res.json()
|
|
377
|
+
return { ...data, _source: 'leaderboard' }
|
|
378
|
+
} catch { return null }
|
|
359
379
|
}
|
|
360
380
|
|
|
361
381
|
// ── TAB BAR ──────────────────────────────────────────────────────────────────
|
|
@@ -455,13 +475,26 @@ function yieldSpark(d) {
|
|
|
455
475
|
|
|
456
476
|
// ── TAB 1: DASHBOARD ─────────────────────────────────────────────────────────
|
|
457
477
|
function renderDashboard(data, status = '') {
|
|
458
|
-
const { active, verifierMap, tdPillars, boardData } = data
|
|
459
478
|
const w = W()
|
|
460
479
|
const budget = H() - 4 // 2 tab bar + 2 footer
|
|
461
480
|
const WINS = ['7d', '30d', '90d', 'all']
|
|
462
481
|
let used = 0
|
|
463
482
|
const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
|
|
464
483
|
|
|
484
|
+
// A2 (2026-06-27): tolerate null/empty dashData so the FIRST paint can happen BEFORE the
|
|
485
|
+
// initial load resolves (loadAll renders an empty frame + "reading your cascade…" immediately,
|
|
486
|
+
// then re-renders with real data). Guard the destructure: no data → header + status + return.
|
|
487
|
+
if (!data || !Array.isArray(data.active)) {
|
|
488
|
+
emit()
|
|
489
|
+
emit(` ${bold('Your Cascade')} ${dim('all platforms · all windows')}`)
|
|
490
|
+
emit()
|
|
491
|
+
emit(` ${dim(' reading token logs… (press [R] to refresh)')}`)
|
|
492
|
+
if (status && used < budget) emit(` ${dim(status)}`)
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const { active } = data
|
|
497
|
+
|
|
465
498
|
// Responsive: the full 12-col table is ~124 wide. On terminals narrower than
|
|
466
499
|
// that, drop the two derived columns (Vel, 10x) and tighten the inter-column
|
|
467
500
|
// gap from 2 spaces to 1, so the core pillars + Υ/SNR/Lev/Class still fit.
|
|
@@ -521,6 +554,15 @@ function renderDashboard(data, status = '') {
|
|
|
521
554
|
const sectionsBelow = barLines + boardLines
|
|
522
555
|
const maxCascadeRows = Math.max(8, budget - used - sectionsBelow)
|
|
523
556
|
|
|
557
|
+
// A3 (2026-06-27): count the cascade rows we WANT to show (every window with data across all
|
|
558
|
+
// active platforms) so we can tell the user how many were dropped by the height budget, instead
|
|
559
|
+
// of silently truncating them. (Fit logic below is unchanged — this is observation only.)
|
|
560
|
+
const totalCascadeRowsWanted = active.reduce((n, d) =>
|
|
561
|
+
n + WINS.filter(wk => {
|
|
562
|
+
const wd = d.windows?.find(x => x.window === wk)
|
|
563
|
+
return wd && (wd.pillars.input + wd.pillars.output) > 0
|
|
564
|
+
}).length, 0)
|
|
565
|
+
|
|
524
566
|
let firstWin = {}
|
|
525
567
|
let cascadeRowCount = 0
|
|
526
568
|
for (const d of active) {
|
|
@@ -538,6 +580,10 @@ function renderDashboard(data, status = '') {
|
|
|
538
580
|
emit()
|
|
539
581
|
}
|
|
540
582
|
|
|
583
|
+
// A3: if any cascade rows were dropped (height budget / row cap), say so instead of hiding them.
|
|
584
|
+
const cascadeDropped = totalCascadeRowsWanted - cascadeRowCount
|
|
585
|
+
if (cascadeDropped > 0) emit(` ${dim(` +${cascadeDropped} more — resize / scroll`)}`)
|
|
586
|
+
|
|
541
587
|
// combined
|
|
542
588
|
if (active.length > 1 && used < budget - sectionsBelow) {
|
|
543
589
|
const lbl = active.map(d => d.platform).join('+')
|
|
@@ -663,11 +709,14 @@ const TREND_METRICS = [
|
|
|
663
709
|
{ label: '10xDEV', pick: (c) => c.dev10x, fmt: (v) => v.toFixed(2) },
|
|
664
710
|
{ label: 'Eff', pick: (c) => c.yield != null ? c.yield / AA_EFF_BASELINE : null, fmt: (v) => v.toFixed(1) + '×' },
|
|
665
711
|
{ label: '§IGNA', pick: () => null, fmt: () => '—', stub: 'composite calibrating (needs user volume)' },
|
|
666
|
-
|
|
712
|
+
// FIX A4 (2026-06-27): operating ratio is INPUT-NORMALIZED `cache : 1 : output` (matches web canon —
|
|
713
|
+
// ThreeDegreesChart / OperatingRatioBar, e.g. "3.5 : 1 : 0.50"), not raw token counts (which also
|
|
714
|
+
// overflowed the Trends columns).
|
|
715
|
+
{ label: 'Op Ratio', pick: (c, p) => (p.input ?? 0) > 0 ? { cacheRead: p.cacheRead ?? 0, input: p.input ?? 0, output: p.output ?? 0 } : null, fmt: (v) => { const i = v.input || 1; return `${(v.cacheRead / i).toFixed(1)} : 1 : ${(v.output / i).toFixed(2)}` } },
|
|
667
716
|
{ label: '$/1M', pick: () => null, fmt: () => '—', stub: 'cost data wires post-ingest' },
|
|
668
717
|
]
|
|
669
718
|
function renderTrends(data, subIdx = 0) {
|
|
670
|
-
const { active } = data
|
|
719
|
+
const { active } = data ?? {}
|
|
671
720
|
const budget = H() - 4
|
|
672
721
|
let used = 0
|
|
673
722
|
const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
|
|
@@ -683,16 +732,23 @@ function renderTrends(data, subIdx = 0) {
|
|
|
683
732
|
|
|
684
733
|
if (sub === 'You' || sub === 'Platform') {
|
|
685
734
|
const rows = sub === 'You'
|
|
686
|
-
? (active
|
|
687
|
-
: active
|
|
735
|
+
? ((active?.length) ? [{ platform: 'you', windows: active[0]?.windows }] : []) // primary cascade
|
|
736
|
+
: (active ?? [])
|
|
688
737
|
if (!rows.length) { emit(` ${dim(' reading your cascade… (press [R] to refresh)')}`); }
|
|
689
|
-
|
|
690
|
-
|
|
738
|
+
// A3 (2026-06-27): count metric/rank rows dropped by the height budget so we can show a
|
|
739
|
+
// "+N more — resize / scroll" note instead of silently truncating. Each rendered block wants
|
|
740
|
+
// TREND_METRICS.length metric rows + 1 rank row. (Fit logic below unchanged — observe only.)
|
|
741
|
+
let trendsDropped = 0
|
|
742
|
+
for (let ri = 0; ri < rows.length; ri++) {
|
|
743
|
+
const d = rows[ri]
|
|
744
|
+
if (used >= budget - 2) { trendsDropped += (rows.length - ri) * (TREND_METRICS.length + 1); break }
|
|
691
745
|
emit(` ${bold(sub === 'You' ? 'Your trajectory' : cyan(d.platform))}`)
|
|
692
746
|
// FIX G1: window column headers (mirror renderCompare) — one header row, then
|
|
693
747
|
// values in fixed-width columns below. No per-value `w:` prefix.
|
|
694
748
|
emit(` ${trendHeader()}`)
|
|
695
|
-
|
|
749
|
+
let mi = 0
|
|
750
|
+
for (; mi < TREND_METRICS.length; mi++) {
|
|
751
|
+
const m = TREND_METRICS[mi]
|
|
696
752
|
if (used >= budget - 2) break
|
|
697
753
|
if (m.stub) {
|
|
698
754
|
emit(` ${padEnd(dim(m.label), 10)} ${dim('▁▁▁▁ calculating… (' + m.stub + ')')}`)
|
|
@@ -700,10 +756,14 @@ function renderTrends(data, subIdx = 0) {
|
|
|
700
756
|
emit(` ${padEnd(cyan(m.label), 10)} ${metricSpark(d, m.pick, m.fmt)}`)
|
|
701
757
|
}
|
|
702
758
|
}
|
|
759
|
+
trendsDropped += (TREND_METRICS.length - mi) // metrics this block couldn't fit
|
|
703
760
|
// rank trend — wired later; honest placeholder for now
|
|
704
761
|
if (used < budget - 1) emit(` ${padEnd(dim('Rank'), 10)} ${dim('▁▁▁▁ calculating… (rank history wires post-ingest)')}`)
|
|
762
|
+
else trendsDropped += 1
|
|
705
763
|
emit()
|
|
706
764
|
}
|
|
765
|
+
// A3: surface the dropped count instead of hiding it.
|
|
766
|
+
if (trendsDropped > 0) emit(` ${dim(` +${trendsDropped} more — resize / scroll`)}`)
|
|
707
767
|
} else { // Field
|
|
708
768
|
emit(` ${dim(' Field-wide trends — calculating…')}`)
|
|
709
769
|
emit(` ${dim(' (needs per-window board history; materializes once live ingest lands)')}`)
|
|
@@ -807,12 +867,95 @@ function renderCompare(data) {
|
|
|
807
867
|
}
|
|
808
868
|
}
|
|
809
869
|
|
|
870
|
+
// ── TAB 3: BOARD — submissions view (#8) ──────────────────────────────────────
|
|
871
|
+
// Renders ALL submissions ranked (raw submission rows from GET /api/v1/submissions), not operator
|
|
872
|
+
// aggregates. Columns: rank · codename · platform · window · Υ Yield · op_ratio · class · tokens.
|
|
873
|
+
// Field names are read defensively (the endpoint is new) so a slightly different server shape still
|
|
874
|
+
// renders. [Y] "just me" filtering + the you-highlight carry over from the leaderboard view.
|
|
875
|
+
function renderSubmissions(boardData, window = '30d', filterCodename = null, highlightCodename = null) {
|
|
876
|
+
const rows = boardData?.submissions ?? boardData?.entries ?? boardData?.rows ??
|
|
877
|
+
(Array.isArray(boardData) ? boardData : [])
|
|
878
|
+
const w = W()
|
|
879
|
+
const budget = H() - 4
|
|
880
|
+
let used = 0
|
|
881
|
+
const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
|
|
882
|
+
|
|
883
|
+
// Defensive field readers (server shape is new / not yet pinned).
|
|
884
|
+
const gName = (e) => e.codename ?? e.operator_codename ?? '—'
|
|
885
|
+
const gPlat = (e) => e.platform ?? e.platform_primary ?? '—'
|
|
886
|
+
const gWin = (e) => e.window ?? e.window_type ?? '—'
|
|
887
|
+
const gYield = (e) => e.yield_ ?? e.yield ?? e.y ?? null
|
|
888
|
+
const gOpRatio = (e) => e.op_ratio ?? e.operating_ratio ?? e.opRatio ?? null
|
|
889
|
+
const gClass = (e) => e.class_tier ?? e.class ?? e.class_ ?? '—'
|
|
890
|
+
const gTokens = (e) => e.tokens ?? e.total_tokens ?? e.token_total ?? null
|
|
891
|
+
|
|
892
|
+
const modeLabel = filterCodename ? `${dim(' · just you')}` : highlightCodename ? `${dim(' · you highlighted')}` : ''
|
|
893
|
+
emit()
|
|
894
|
+
emit(` ${bold('Submissions')} ${dim(`window: ${window} · all submissions ranked by Υ Yield · signalaf.com`)}${modeLabel}`)
|
|
895
|
+
emit()
|
|
896
|
+
|
|
897
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
898
|
+
emit(` ${dim(' no submissions for this window')}`)
|
|
899
|
+
return
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
let sorted = [...rows].sort((a, b) => (gYield(b) ?? 0) - (gYield(a) ?? 0))
|
|
903
|
+
if (filterCodename) {
|
|
904
|
+
sorted = sorted.filter(e => (gName(e) ?? '').toLowerCase() === filterCodename.toLowerCase())
|
|
905
|
+
if (sorted.length === 0) {
|
|
906
|
+
emit(` ${dim(` you have no submissions in the ${window} window yet`)}`)
|
|
907
|
+
emit(` ${dim(' press [S] from a read tab to submit your cascade')}`)
|
|
908
|
+
return
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// op_ratio may arrive as a preformatted string ("3.5 : 1 : 0.50") or a number — render either.
|
|
913
|
+
const fmtOp = (v) => v == null ? '—' : (typeof v === 'string' ? v : (typeof v === 'number' ? v.toFixed(2) : String(v)))
|
|
914
|
+
|
|
915
|
+
const SH = [
|
|
916
|
+
padStart(hdr('#'), 4), padEnd(hdr('Codename'), 20), padEnd(hdr('Platform'), 10),
|
|
917
|
+
padEnd(hdr('Win'), 6), padStart(hdr('Υ Yield'), 9), padEnd(hdr('Op Ratio'), 18),
|
|
918
|
+
padEnd(hdr('Class'), 12), padStart(hdr('Tokens'), 9),
|
|
919
|
+
]
|
|
920
|
+
emit(` ${SH.join(' ')}`)
|
|
921
|
+
emit(` ${dim('·'.repeat(Math.min(w - 4, 100)))}`)
|
|
922
|
+
|
|
923
|
+
let shown = 0
|
|
924
|
+
for (let idx = 0; idx < sorted.length; idx++) {
|
|
925
|
+
if (used >= budget) break
|
|
926
|
+
const e = sorted[idx]
|
|
927
|
+
const isYou = highlightCodename && (gName(e) ?? '').toLowerCase() === highlightCodename.toLowerCase()
|
|
928
|
+
const rk = idx === 0 ? gold(`#${idx+1}`) : idx < 3 ? cyan(`#${idx+1}`) : `#${idx+1}`
|
|
929
|
+
const nmRaw = trunc(gName(e), 20)
|
|
930
|
+
const nm = isYou
|
|
931
|
+
? `${c.bgCyan}${c.boldCyan}${padEnd(` ${trunc(gName(e), 18)} `, 20)}${c.reset}`
|
|
932
|
+
: padEnd(nmRaw, 20)
|
|
933
|
+
const yv = gYield(e)
|
|
934
|
+
const yld = padStart(yv != null ? (yv > 10000 ? gold(fmtY(yv)) : fmtY(yv)) : '—', 9)
|
|
935
|
+
const cls = padEnd(colorCls(gClass(e)), 12)
|
|
936
|
+
const tk = gTokens(e)
|
|
937
|
+
const youMark = isYou ? ` ${c.bgCyan}${c.boldCyan}YOU${c.reset}` : ''
|
|
938
|
+
emit(` ${padStart(rk, 4)} ${nm} ${padEnd(cyan(trunc(gPlat(e), 10)), 10)} ${padEnd(dim(trunc(gWin(e), 6)), 6)} ${yld} ${padEnd(dim(fmtOp(gOpRatio(e))), 18)} ${cls} ${padStart(tk != null ? fmtTok(tk) : '—', 9)}${youMark}`)
|
|
939
|
+
shown++
|
|
940
|
+
}
|
|
941
|
+
// A3-style: if the height budget dropped rows, say so instead of silently truncating.
|
|
942
|
+
const dropped = sorted.length - shown
|
|
943
|
+
if (dropped > 0 && used < budget) emit(` ${dim(` +${dropped} more — resize / scroll`)}`)
|
|
944
|
+
}
|
|
945
|
+
|
|
810
946
|
// ── TAB 3: BOARD ─────────────────────────────────────────────────────────────
|
|
811
947
|
// FIX I2: hybrid board model. `filterCodename` (non-null = "just me" mode) shows
|
|
812
948
|
// ONLY the signed-in operator's rows. `highlightCodename` (non-null = global mode)
|
|
813
949
|
// highlights the signed-in operator's row in the global board. Both come from the
|
|
814
950
|
// signed-in identity's codename; the [Y] key toggles between the two modes.
|
|
815
951
|
function renderBoard(boardData, window = '30d', filterCodename = null, highlightCodename = null) {
|
|
952
|
+
// #8 (2026-06-27): when the data came from the NEW /api/v1/submissions endpoint, render ALL
|
|
953
|
+
// submissions ranked (raw rows). Otherwise fall through to the existing leaderboard render
|
|
954
|
+
// (pre-deploy fallback). loadBoardData tags the payload with `_source`.
|
|
955
|
+
if (boardData?._source === 'submissions') {
|
|
956
|
+
renderSubmissions(boardData, window, filterCodename, highlightCodename)
|
|
957
|
+
return
|
|
958
|
+
}
|
|
816
959
|
const entries = boardData?.entries ?? boardData?.operators ?? boardData ?? []
|
|
817
960
|
const w = W()
|
|
818
961
|
const budget = H() - 4
|
|
@@ -897,13 +1040,20 @@ function renderBoard(boardData, window = '30d', filterCodename = null, highlight
|
|
|
897
1040
|
// Explains what the live watcher does and why it's the point of the agent: it
|
|
898
1041
|
// re-reads your local logs on an interval and feeds your verified cascade to the
|
|
899
1042
|
// leaderboard. Launched (in its own window) with [Enter]; interval is tunable.
|
|
1043
|
+
// A5 (2026-06-27): Watch defaults to ALL active platforms × ALL windows (mirror the cli.mjs watch
|
|
1044
|
+
// change). `platform`/`win` are OPTIONAL focus filters — null/'all' means "everything". [P]/[W]
|
|
1045
|
+
// cycle the focus (including back to "all"); the spawned watcher + this copy show everything by default.
|
|
900
1046
|
function renderWatchInfo(platform, win, refresh) {
|
|
1047
|
+
const platLabel = (!platform || platform === 'all') ? 'all active platforms' : platform
|
|
1048
|
+
const winLabel = (!win || win === 'all-windows') ? 'all windows' : win
|
|
1049
|
+
const allDefault = platLabel === 'all active platforms' && winLabel === 'all windows'
|
|
901
1050
|
writeln()
|
|
902
1051
|
writeln(` ${bold('Live Watch')} ${dim('the agent that keeps your rank current')}`)
|
|
903
1052
|
writeln()
|
|
904
1053
|
writeln(` ${dim('What it does')}`)
|
|
905
|
-
writeln(` Re-reads your local ${cyan(
|
|
906
|
-
writeln(`
|
|
1054
|
+
writeln(` Re-reads your local token logs for ${cyan(platLabel)} across ${cyan(winLabel)} every`)
|
|
1055
|
+
writeln(` ${gold(refresh + 's')} and recomputes each cascade (Υ Yield · SNR · Leverage · class) live, on`)
|
|
1056
|
+
writeln(` this machine. ${dim('By default it watches everything — every platform you actually use.')}`)
|
|
907
1057
|
writeln()
|
|
908
1058
|
writeln(` ${dim('Why it matters')}`)
|
|
909
1059
|
writeln(` Watch is how the board stays ${bold('current')}: each refresh submits your latest`)
|
|
@@ -911,9 +1061,10 @@ function renderWatchInfo(platform, win, refresh) {
|
|
|
911
1061
|
writeln(` no manual re-submit. Tokens never leave your machine; only the metrics post.`)
|
|
912
1062
|
writeln()
|
|
913
1063
|
writeln(` ${dim('Settings')}`)
|
|
914
|
-
writeln(` ${dim('Platform')} ${cyan(
|
|
1064
|
+
writeln(` ${dim('Platform')} ${cyan(platLabel)} ${dim('Window')} ${cyan(winLabel)} ${dim('Refresh')} ${gold(refresh + 's')}`)
|
|
1065
|
+
writeln(` ${dim(allDefault ? 'watching everything (default)' : 'focused — press [P]/[W] to widen back to all')}`)
|
|
915
1066
|
writeln()
|
|
916
|
-
writeln(` ${dim('[Enter]')} launch watcher (new window) ${dim('[+]/[-]')} refresh ±5s ${dim('[P]')} platform ${dim('[W]')} window`)
|
|
1067
|
+
writeln(` ${dim('[Enter]')} launch watcher (new window) ${dim('[+]/[-]')} refresh ±5s ${dim('[P]')} platform focus ${dim('[W]')} window focus`)
|
|
917
1068
|
writeln()
|
|
918
1069
|
}
|
|
919
1070
|
|
|
@@ -946,74 +1097,66 @@ function renderConnect(id, codeBuf = '', msg = '') {
|
|
|
946
1097
|
if (msg) { writeln(); writeln(` ${msg}`) }
|
|
947
1098
|
}
|
|
948
1099
|
|
|
949
|
-
// ── TAB 4: WATCH
|
|
950
|
-
|
|
1100
|
+
// ── TAB 4: WATCH (live grid) ──────────────────────────────────────────────────
|
|
1101
|
+
// A5 (2026-06-27): renders ALL active platforms × ALL windows by default (mirror the cli.mjs
|
|
1102
|
+
// watch grid), instead of a single platform/window. `platform`/`win` are OPTIONAL focus filters —
|
|
1103
|
+
// null/'all'/'all-windows' means "everything". Each row is a (platform, window) cascade cell:
|
|
1104
|
+
// Υ Yield + SNR/Lev/Vel/class. Iterates active platforms (input+output > 0 in any window).
|
|
1105
|
+
async function renderWatch(platform = 'all', win = 'all-windows') {
|
|
951
1106
|
const { tokenpullAny } = await import('./tokenpull.mjs')
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
const wdata = d.windows?.find(x => x.window === win)
|
|
956
|
-
if (!wdata) { writeln(` ${dim(`no data for ${win} window`)}`); return }
|
|
957
|
-
|
|
958
|
-
const p = wdata.pillars
|
|
959
|
-
const cas = cascadeFrom(p)
|
|
960
|
-
const w = W()
|
|
961
|
-
|
|
962
|
-
writeln()
|
|
963
|
-
writeln(` ${bold('Live Watch')} ${dim(`${platform} · ${win} window · ${d.files} files · ${wdata.messages?.toLocaleString()} msgs`)}`)
|
|
964
|
-
writeln()
|
|
965
|
-
|
|
966
|
-
if (!cas) { writeln(` ${dim(' insufficient data to compute cascade')}`); return }
|
|
967
|
-
|
|
968
|
-
const clsFn = CLS[cas.class] ?? ((s) => s)
|
|
969
|
-
|
|
970
|
-
// big numbers
|
|
971
|
-
const metrics = [
|
|
972
|
-
{ label: 'Υ Yield', val: fmtY(cas.yield), color: cas.yield > 1000 ? c.boldGold : c.boldCyan },
|
|
973
|
-
{ label: 'Class', val: cas.class, color: CLS[cas.class] ? c.boldGold : c.reset },
|
|
974
|
-
{ label: 'SNR', val: fmtSNR(cas.snr), color: c.green },
|
|
975
|
-
{ label: 'Leverage', val: fmtLev(cas.leverage)+'×', color: c.cyan },
|
|
976
|
-
{ label: 'Velocity', val: cas.velocity?.toFixed(2), color: c.white },
|
|
977
|
-
{ label: '10xDEV', val: cas.dev10x?.toFixed(2), color: c.magenta },
|
|
1107
|
+
const ALL_PLATFORMS = [
|
|
1108
|
+
'claude','codex','amp','gemini','kimi','qwen','goose','kilo',
|
|
1109
|
+
'hermes','droid','codebuff','copilot','openclaw','pi',
|
|
978
1110
|
]
|
|
1111
|
+
const platFilter = (platform && platform !== 'all') ? platform : null
|
|
1112
|
+
const winFilter = (win && win !== 'all-windows') ? win : null
|
|
1113
|
+
const WINS = winFilter ? [winFilter] : ['7d', '30d', '90d', 'all']
|
|
1114
|
+
const w = W()
|
|
1115
|
+
const budget = H() - 4
|
|
1116
|
+
let used = 0
|
|
1117
|
+
const emit = (s = '') => { if (used < budget) { writeln(s); used++ } }
|
|
979
1118
|
|
|
980
|
-
|
|
981
|
-
|
|
1119
|
+
// Detect active platforms (any window with input+output > 0), respecting an optional [P] focus.
|
|
1120
|
+
const candidates = platFilter ? [platFilter] : ALL_PLATFORMS
|
|
1121
|
+
const settled = await Promise.allSettled(candidates.map(p => tokenpullAny(p)))
|
|
1122
|
+
const active = []
|
|
1123
|
+
for (const r of settled) {
|
|
1124
|
+
if (r.status !== 'fulfilled' || !r.value) continue
|
|
1125
|
+
const d = r.value
|
|
1126
|
+
if ((d.windows || []).some(ww => (ww.pillars.input + ww.pillars.output) > 0)) active.push(d)
|
|
982
1127
|
}
|
|
983
|
-
|
|
1128
|
+
// stable display order
|
|
1129
|
+
active.sort((a, b) => ALL_PLATFORMS.indexOf(a.platform) - ALL_PLATFORMS.indexOf(b.platform))
|
|
984
1130
|
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
writeln()
|
|
1131
|
+
const scopeLabel = (platFilter || 'all active platforms') + ' · ' + (winFilter ? `${winFilter} window` : 'all windows')
|
|
1132
|
+
emit()
|
|
1133
|
+
emit(` ${bold('Live Watch')} ${dim(scopeLabel)}`)
|
|
1134
|
+
emit(` ${dim('·'.repeat(Math.min(w - 4, 80)))}`)
|
|
990
1135
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
{
|
|
994
|
-
|
|
995
|
-
{ label: 'Cache Read', val: p.cacheRead, color: c.boldGold,est: false },
|
|
996
|
-
]
|
|
997
|
-
// Log scale: token pillars span 2–3 orders of magnitude (input ~8M, cacheRead ~1.9B).
|
|
998
|
-
// Linear scale crushes input/output bars to near-invisible. Log10 gives equal visual
|
|
999
|
-
// weight per order-of-magnitude step.
|
|
1000
|
-
const maxLog = Math.log10(Math.max(...pillarsData.map(x => x.val ?? 0), 10))
|
|
1001
|
-
for (const item of pillarsData) {
|
|
1002
|
-
const v = item.val ?? 0
|
|
1003
|
-
const { bar } = logBar(v, maxLog, 40, item.color)
|
|
1004
|
-
const est = item.est ? dim('~') : ''
|
|
1005
|
-
writeln(` ${padEnd(dim(item.label), 12)} ${padEnd(bar, 40)} ${est}${fmtTok(v)}`)
|
|
1136
|
+
if (active.length === 0) {
|
|
1137
|
+
emit()
|
|
1138
|
+
emit(` ${dim(' no active platforms detected — run some sessions, this picks them up automatically')}`)
|
|
1139
|
+
return
|
|
1006
1140
|
}
|
|
1007
|
-
writeln(` ${dim('log₁₀ scale — each step = 10× · absolute values right')}`)
|
|
1008
|
-
writeln()
|
|
1009
1141
|
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1142
|
+
for (const d of active) {
|
|
1143
|
+
if (used >= budget - 1) break
|
|
1144
|
+
const byWin = {}
|
|
1145
|
+
for (const ww of (d.windows || [])) byWin[ww.window] = ww
|
|
1146
|
+
emit()
|
|
1147
|
+
emit(` ${cyan(d.platform)}${d.estimated ? dim(' (est)') : ''}`)
|
|
1148
|
+
for (const wk of WINS) {
|
|
1149
|
+
if (used >= budget - 1) break
|
|
1150
|
+
const ww = byWin[wk]
|
|
1151
|
+
const cas = ww ? cascadeFrom(ww.pillars) : null
|
|
1152
|
+
const winLabel = wk === 'all' ? 'all-time' : wk
|
|
1153
|
+
if (!cas) { emit(` ${padEnd(dim(winLabel), 9)} ${dim('no data')}`); continue }
|
|
1154
|
+
const clsFn = CLS[cas.class] ?? ((s) => s)
|
|
1155
|
+
const yDisplay = cas.yield > 1000 ? gold(fmtY(cas.yield)) : cyan(fmtY(cas.yield))
|
|
1156
|
+
const metrics = `${dim('SNR')} ${fmtSNR(cas.snr)} ${dim('Lev')} ${cas.leverage != null ? fmtLev(cas.leverage) + '×' : '—'} ${dim('Vel')} ${cas.velocity != null ? cas.velocity.toFixed(2) : '—'} ${clsFn(cas.class)}`
|
|
1157
|
+
emit(` ${padEnd(dim(winLabel), 9)} ${bold('Υ')} ${yDisplay} ${metrics}`)
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1017
1160
|
}
|
|
1018
1161
|
|
|
1019
1162
|
// ── SUBMIT PREVIEW (FIX I1 / FIX E) — see→confirm→send ───────────────────────
|
|
@@ -1070,6 +1213,7 @@ async function renderOnce(tabIdx = 0) {
|
|
|
1070
1213
|
if (tabIdx === 0) renderDashboard(data, 'debug')
|
|
1071
1214
|
else if (tabIdx === 1) renderCompare(data)
|
|
1072
1215
|
else if (tabIdx === 2) renderBoard(data, '30d')
|
|
1216
|
+
else if (tabIdx === 4) await renderWatch('all', 'all-windows')
|
|
1073
1217
|
else if (tabIdx === 5) renderConnect(loadIdentity(), '', '')
|
|
1074
1218
|
const lines = _screenBuf || []
|
|
1075
1219
|
_screenBuf = null; _footerBuf = null
|
|
@@ -1201,8 +1345,11 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1201
1345
|
let compareData = null
|
|
1202
1346
|
let boardData = null
|
|
1203
1347
|
let boardWindow = '30d'
|
|
1204
|
-
|
|
1205
|
-
|
|
1348
|
+
// A5 (2026-06-27): Watch defaults to "all" focus — all active platforms × all windows. [P]/[W]
|
|
1349
|
+
// optionally narrow the focus and cycle back to 'all'. ('all-windows' is the window sentinel so
|
|
1350
|
+
// it never collides with the real 'all' window bucket in the cli watcher's optional --window.)
|
|
1351
|
+
let watchPlatform = 'all'
|
|
1352
|
+
let watchWindow = 'all-windows'
|
|
1206
1353
|
let watchRefresh = 30 // [5] Watch poll interval (seconds) — [+]/[-] adjust
|
|
1207
1354
|
let trendSub = 0 // [2] Trends sub-view index (You/Platform/Field) — [T] cycles
|
|
1208
1355
|
let loading = true
|
|
@@ -1251,7 +1398,11 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1251
1398
|
setFooter(readFooter(`${hint} ${dim('[T]')} view${submitHint}`))
|
|
1252
1399
|
} else if (activeTab === 2) { // Compare
|
|
1253
1400
|
if (!compareData) {
|
|
1254
|
-
|
|
1401
|
+
// #9 (2026-06-27): Compare runs a FRESH on-demand pull of the external verifier sources
|
|
1402
|
+
// (ccusage + tokscale + token-dashboard rescan) — it can take several seconds. Show a clear
|
|
1403
|
+
// spinner line while it runs, instead of a bare "loading…".
|
|
1404
|
+
writeln(`\n ${bold('Source Comparison')} ${dim(`platform: ${platform}`)}`)
|
|
1405
|
+
writeln(`\n ${gold('◇')} ${dim(`pulling fresh sources… (ccusage · tokscale · token-dash — this can take several seconds)`)}`)
|
|
1255
1406
|
} else {
|
|
1256
1407
|
renderCompare(compareData)
|
|
1257
1408
|
}
|
|
@@ -1282,22 +1433,33 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1282
1433
|
|
|
1283
1434
|
// ── Initial data load
|
|
1284
1435
|
const loadAll = async () => {
|
|
1285
|
-
|
|
1436
|
+
// A2 (2026-06-27): paint the frame + a clear "reading your cascade…" status IMMEDIATELY,
|
|
1437
|
+
// BEFORE the await — so the very first thing on screen is the table header + a live status,
|
|
1438
|
+
// not a blank "loading dashboard…". renderDashboard tolerates this empty dashData (guarded).
|
|
1439
|
+
status = 'reading your cascade…'
|
|
1440
|
+
dashData = { active: [], _loading: true }
|
|
1441
|
+
await redraw()
|
|
1286
1442
|
// FIX 0: progressive Dashboard load — claude first (fast), render it, THEN fill
|
|
1287
1443
|
// the other 13 platforms in the background + redraw. The user sees their cascade
|
|
1288
1444
|
// within ~1 read instead of a 7s blank "loading dashboard…".
|
|
1289
1445
|
dashData = await loadDashboardData().catch(e => { status = `dashboard error: ${e.message}`; return null })
|
|
1290
1446
|
status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
|
|
1291
1447
|
await redraw()
|
|
1292
|
-
//
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1448
|
+
// Keep a "filling other platforms…" status visible while fillDashboardRest runs.
|
|
1449
|
+
// #9 (2026-06-27): Compare is NO LONGER eagerly loaded here — loadCompareData now runs the
|
|
1450
|
+
// FRESH verifier pull (ccusage + tokscale + token-dash rescan, 5–60s), which would re-introduce
|
|
1451
|
+
// a slow Dashboard load path. Compare loads lazily ON-DEMAND when the Compare tab is opened.
|
|
1452
|
+
// Board still preloads (fast, cached) in parallel with the platform fill.
|
|
1297
1453
|
if (dashData?._remaining) {
|
|
1298
|
-
|
|
1454
|
+
status = 'claude ready · filling other platforms…'
|
|
1299
1455
|
await redraw()
|
|
1300
1456
|
}
|
|
1457
|
+
;[, boardData] = await Promise.all([
|
|
1458
|
+
dashData?._remaining ? fillDashboardRest(dashData) : Promise.resolve(false),
|
|
1459
|
+
loadBoardData(boardWindow).catch(() => null),
|
|
1460
|
+
])
|
|
1461
|
+
status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
|
|
1462
|
+
await redraw()
|
|
1301
1463
|
}
|
|
1302
1464
|
|
|
1303
1465
|
// Always restore terminal on unexpected exit
|
|
@@ -1481,6 +1643,17 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1481
1643
|
if (k === '6') { activeTab = 5; switched = true } // Connect = sign in / switch device
|
|
1482
1644
|
if (k === 'c' && activeTab !== 5) { activeTab = 5; switched = true } // [C] → Connect from any read tab
|
|
1483
1645
|
|
|
1646
|
+
// #9 (2026-06-27): Compare loads its FRESH verifier pull ON-DEMAND when the tab opens
|
|
1647
|
+
// (NOT on the Dashboard load path — that pull is a 5–60s scan). Paint the "pulling fresh
|
|
1648
|
+
// sources…" spinner first (compareData null → redraw shows it), then run the pull + re-render.
|
|
1649
|
+
if (switched && activeTab === 2 && !compareData) {
|
|
1650
|
+
submitMsg = ''
|
|
1651
|
+
await redraw() // show the spinner line before the (slow) fresh pull
|
|
1652
|
+
compareData = await loadCompareData(platform).catch(() => null)
|
|
1653
|
+
await redraw()
|
|
1654
|
+
return
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1484
1657
|
// Trends tab: [T] cycles the sub-view (You · Platform · Field)
|
|
1485
1658
|
if (activeTab === 1 && k === 't') { trendSub = (trendSub + 1) % 3; await redraw(); return }
|
|
1486
1659
|
|
|
@@ -1492,36 +1665,48 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1492
1665
|
// Compare reloads compareData for the new platform; Watch just re-renders.
|
|
1493
1666
|
const CYCLE_PLATFORMS = ['claude','codex','amp','gemini','kimi','qwen','goose','kilo','hermes','droid','codebuff','copilot','openclaw','pi']
|
|
1494
1667
|
if (k === 'p' && (activeTab === 2 || activeTab === 4)) {
|
|
1495
|
-
const cur = activeTab === 2 ? platform : watchPlatform
|
|
1496
|
-
const idx = CYCLE_PLATFORMS.indexOf(cur)
|
|
1497
|
-
const next = CYCLE_PLATFORMS[(idx + 1) % CYCLE_PLATFORMS.length]
|
|
1498
1668
|
if (activeTab === 2) {
|
|
1499
|
-
platform
|
|
1669
|
+
// Compare: real platforms only (no 'all' — Compare is per-platform by design).
|
|
1670
|
+
const idx = CYCLE_PLATFORMS.indexOf(platform)
|
|
1671
|
+
platform = CYCLE_PLATFORMS[(idx + 1) % CYCLE_PLATFORMS.length]
|
|
1672
|
+
// #9: clearing compareData makes redraw show the "pulling fresh sources…" spinner
|
|
1673
|
+
// for the new platform while the fresh on-demand verifier pull runs.
|
|
1674
|
+
compareData = null
|
|
1500
1675
|
status = `loading ${platform}…`
|
|
1501
1676
|
await redraw()
|
|
1502
1677
|
compareData = await loadCompareData(platform).catch(() => null)
|
|
1503
1678
|
status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
|
|
1504
1679
|
} else {
|
|
1505
|
-
|
|
1680
|
+
// A5: Watch [P] cycles 'all' → claude → codex → … → 'all' (optional focus; default 'all').
|
|
1681
|
+
const WATCH_PLAT_CYCLE = ['all', ...CYCLE_PLATFORMS]
|
|
1682
|
+
const idx = WATCH_PLAT_CYCLE.indexOf(watchPlatform)
|
|
1683
|
+
watchPlatform = WATCH_PLAT_CYCLE[(idx + 1) % WATCH_PLAT_CYCLE.length]
|
|
1506
1684
|
}
|
|
1507
1685
|
await redraw(); return
|
|
1508
1686
|
}
|
|
1509
1687
|
|
|
1510
|
-
// FIX J: [W] cycles window on Watch tab (
|
|
1511
|
-
// [W] (activeTab === 3) is handled below
|
|
1688
|
+
// FIX J / A5: [W] cycles window FOCUS on the Watch tab. 'all-windows' (default) → 7d → 30d →
|
|
1689
|
+
// 90d → all → back to 'all-windows'. Board tab's [W] (activeTab === 3) is handled below.
|
|
1512
1690
|
if (k === 'w' && activeTab === 4) {
|
|
1513
|
-
const
|
|
1514
|
-
const idx =
|
|
1515
|
-
watchWindow =
|
|
1691
|
+
const WATCH_WIN_CYCLE = ['all-windows', '7d', '30d', '90d', 'all']
|
|
1692
|
+
const idx = WATCH_WIN_CYCLE.indexOf(watchWindow)
|
|
1693
|
+
watchWindow = WATCH_WIN_CYCLE[(idx + 1) % WATCH_WIN_CYCLE.length]
|
|
1516
1694
|
await redraw(); return
|
|
1517
1695
|
}
|
|
1518
1696
|
|
|
1519
1697
|
if (activeTab === 4 && (key === '\r' || key === '\n')) {
|
|
1520
|
-
//
|
|
1698
|
+
// A5 (2026-06-27): launch the live watcher in its own window. By default (focus = 'all')
|
|
1699
|
+
// pass NO --platform/--window so the cli watcher auto-loads ALL active platforms × ALL
|
|
1700
|
+
// windows. Only add a filter flag when the user has set an explicit [P]/[W] focus.
|
|
1521
1701
|
try {
|
|
1522
|
-
|
|
1702
|
+
let watchCmd = 'sigrank watch'
|
|
1703
|
+
if (watchPlatform && watchPlatform !== 'all') watchCmd += ` --platform ${watchPlatform}`
|
|
1704
|
+
if (watchWindow && watchWindow !== 'all-windows') watchCmd += ` --window ${watchWindow}`
|
|
1705
|
+
watchCmd += ` --refresh ${watchRefresh}`
|
|
1523
1706
|
execSync(`osascript -e 'tell application "Terminal" to do script "${watchCmd}"'`, { stdio: 'ignore' })
|
|
1524
|
-
|
|
1707
|
+
const scope = (!watchPlatform || watchPlatform === 'all') && (!watchWindow || watchWindow === 'all-windows')
|
|
1708
|
+
? 'all platforms × all windows' : 'focused'
|
|
1709
|
+
status = `watcher launched (${scope}, ${watchRefresh}s) in a new window`
|
|
1525
1710
|
} catch { status = 'could not open Terminal.app — run: sigrank watch' }
|
|
1526
1711
|
await redraw()
|
|
1527
1712
|
return
|
|
@@ -1529,6 +1714,16 @@ export async function runTui({ platform: initPlatform = 'claude', window: win =
|
|
|
1529
1714
|
|
|
1530
1715
|
if (k === 'r') {
|
|
1531
1716
|
status = 'refreshing…'
|
|
1717
|
+
// #9: on the Compare tab, [R] re-runs the FRESH verifier pull for the current platform
|
|
1718
|
+
// (loadAll no longer touches Compare). Clearing compareData shows the spinner first.
|
|
1719
|
+
if (activeTab === 2) {
|
|
1720
|
+
compareData = null
|
|
1721
|
+
await redraw()
|
|
1722
|
+
compareData = await loadCompareData(platform).catch(() => null)
|
|
1723
|
+
status = `last refreshed ${new Date().toLocaleTimeString('en-US', { hour12: false })}`
|
|
1724
|
+
await redraw()
|
|
1725
|
+
return
|
|
1726
|
+
}
|
|
1532
1727
|
await redraw()
|
|
1533
1728
|
await loadAll()
|
|
1534
1729
|
return
|