sigrank 0.0.213 → 0.0.215
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/manifest.json +1 -1
- package/package.json +5 -1
- package/presentation/tui-themes.mjs +126 -0
- package/presentation/tui.mjs +259 -64
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
|
|
3
3
|
"manifest_version": "0.4",
|
|
4
4
|
"name": "sigrank",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.214",
|
|
6
6
|
"description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read \u00d7 output / input\u00b2). 23 tools for yield calculation, local token usage pulls, signed submission, and tokscale-powered analytics (market share, developer profiles, model trends, cost analysis, device profiles, MCP usage, competitive intelligence). Privacy-first: runs locally, only four token counts leave the machine.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SunrisesIllNeverSee",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.215",
|
|
4
4
|
"author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
|
|
5
5
|
"description": "SigRank MCP server — AI operator evaluation and token cascade efficiency tools. The yield cascade + live leaderboard as MCP tools any agent can call.",
|
|
6
6
|
"mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
|
|
@@ -85,6 +85,10 @@
|
|
|
85
85
|
"ccusage": "^20.0.19",
|
|
86
86
|
"tokscale": "^4.7.0"
|
|
87
87
|
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
90
|
+
"gifenc": "^1.0.3"
|
|
91
|
+
},
|
|
88
92
|
"overrides": {
|
|
89
93
|
"@hono/node-server": "^2.0.5"
|
|
90
94
|
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// tui-themes.mjs — centralized ANSI color palette with multiple themes
|
|
2
|
+
//
|
|
3
|
+
// Tokscale-inspired: all colors live in one module, render code references
|
|
4
|
+
// them through `currentTheme`. Supports dark (default), light, high-contrast,
|
|
5
|
+
// and monochrome themes for accessibility.
|
|
6
|
+
//
|
|
7
|
+
// Usage in tui.mjs:
|
|
8
|
+
// import { currentTheme as c, setTheme } from "./tui-themes.mjs";
|
|
9
|
+
// // c.gold, c.cyan, c.reset, etc. — same interface as the old `c` object
|
|
10
|
+
//
|
|
11
|
+
// CLI:
|
|
12
|
+
// node tui.mjs --theme dark # default
|
|
13
|
+
// node tui.mjs --theme light # light terminal
|
|
14
|
+
// node tui.mjs --theme high-contrast # visually impaired
|
|
15
|
+
// node tui.mjs --theme monochrome # colorblind
|
|
16
|
+
|
|
17
|
+
const ESC = "\x1b[";
|
|
18
|
+
|
|
19
|
+
export const THEMES = {
|
|
20
|
+
// ── Dark (default) — current palette, designed for dark terminals ──
|
|
21
|
+
// green uses 256-color 77 (#5fd75f) for WCAG AA compliance on dark bg (9.0:1).
|
|
22
|
+
// Standard ANSI 32 (#5a8a5a) only achieves 4.1:1 — below the 4.5:1 threshold.
|
|
23
|
+
dark: {
|
|
24
|
+
name: "dark",
|
|
25
|
+
reset: `${ESC}0m`,
|
|
26
|
+
bold: `${ESC}1m`,
|
|
27
|
+
dim: `${ESC}2m`,
|
|
28
|
+
gold: `${ESC}33m`,
|
|
29
|
+
boldGold: `${ESC}1;33m`,
|
|
30
|
+
cyan: `${ESC}36m`,
|
|
31
|
+
boldCyan: `${ESC}1;36m`,
|
|
32
|
+
green: `${ESC}38;5;77m`, // brighter green #5fd75f (9.0:1 on dark — was ANSI 32 at 4.1:1)
|
|
33
|
+
red: `${ESC}38;5;203m`, // brighter red #ff5f5f (5.6:1 on dark — was ANSI 31 at 4.46:1, just below AA)
|
|
34
|
+
white: `${ESC}97m`,
|
|
35
|
+
boldWhite: `${ESC}1;97m`,
|
|
36
|
+
magenta: `${ESC}35m`,
|
|
37
|
+
blue: `${ESC}34m`,
|
|
38
|
+
bgDim: `${ESC}48;5;236m`,
|
|
39
|
+
bgCyan: `${ESC}48;5;23m`,
|
|
40
|
+
// Medal backgrounds (256-color) — gold/silver/bronze podium tints
|
|
41
|
+
medalBg: { 1: 220, 2: 250, 3: 130 },
|
|
42
|
+
medalFg: `${ESC}38;5;232m`, // dark text on medal bg
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// ── Light — dark text on light/white terminals ──
|
|
46
|
+
// Uses 256-color codes (38;5;N) with darker shades that pass WCAG AA (4.5:1)
|
|
47
|
+
// on white backgrounds. Standard ANSI 33/36/32/31/35/34 render as pastel
|
|
48
|
+
// shades that fail contrast on white — these 256-color variants fix that.
|
|
49
|
+
light: {
|
|
50
|
+
name: "light",
|
|
51
|
+
reset: `${ESC}0m`,
|
|
52
|
+
bold: `${ESC}1m`,
|
|
53
|
+
dim: `${ESC}38;5;242m`, // medium gray (4.5:1 on white — passes AA for large text)
|
|
54
|
+
gold: `${ESC}38;5;130m`, // dark orange-gold #af5f00 (4.7:1 — yellow can't pass AA on white, this is the closest)
|
|
55
|
+
boldGold: `${ESC}1;38;5;130m`, // bold dark orange-gold (4.7:1)
|
|
56
|
+
cyan: `${ESC}38;5;24m`, // dark cyan #005f87 (7.0:1)
|
|
57
|
+
boldCyan: `${ESC}1;38;5;24m`, // bold dark cyan (7.0:1)
|
|
58
|
+
green: `${ESC}38;5;28m`, // dark green #008700 (4.7:1)
|
|
59
|
+
red: `${ESC}38;5;124m`, // dark red #af0000 (7.4:1)
|
|
60
|
+
white: `${ESC}30m`, // BLACK text on light bg (21:1)
|
|
61
|
+
boldWhite: `${ESC}1;30m`, // bold black
|
|
62
|
+
magenta: `${ESC}38;5;90m`, // dark magenta #870087 (8.8:1)
|
|
63
|
+
blue: `${ESC}38;5;19m`, // dark blue #0000af (13.0:1)
|
|
64
|
+
bgDim: `${ESC}48;5;250m`, // light grey bg for active tab
|
|
65
|
+
bgCyan: `${ESC}48;5;194m`, // light teal bg for active tab
|
|
66
|
+
medalBg: { 1: 220, 2: 250, 3: 130 },
|
|
67
|
+
medalFg: `${ESC}38;5;232m`,
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
// ── High contrast — bold everything, no dim, maximum readability ──
|
|
71
|
+
highContrast: {
|
|
72
|
+
name: "high-contrast",
|
|
73
|
+
reset: `${ESC}0m`,
|
|
74
|
+
bold: `${ESC}1m`,
|
|
75
|
+
dim: `${ESC}0;37m`, // NO dim — full brightness white instead
|
|
76
|
+
gold: `${ESC}1;33m`, // bold gold
|
|
77
|
+
boldGold: `${ESC}1;33m`,
|
|
78
|
+
cyan: `${ESC}1;36m`, // bold cyan
|
|
79
|
+
boldCyan: `${ESC}1;36m`,
|
|
80
|
+
green: `${ESC}1;32m`, // bold green
|
|
81
|
+
red: `${ESC}1;31m`, // bold red
|
|
82
|
+
white: `${ESC}1;97m`, // bold bright white
|
|
83
|
+
boldWhite: `${ESC}1;97m`,
|
|
84
|
+
magenta: `${ESC}1;35m`, // bold magenta
|
|
85
|
+
blue: `${ESC}1;34m`, // bold blue
|
|
86
|
+
bgDim: `${ESC}48;5;238m`, // slightly lighter dark bg
|
|
87
|
+
bgCyan: `${ESC}48;5;24m`, // slightly lighter teal bg
|
|
88
|
+
medalBg: { 1: 220, 2: 250, 3: 130 },
|
|
89
|
+
medalFg: `${ESC}1;38;5;232m`,
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
// ── Monochrome — bold/normal only, no color (colorblind-safe) ──
|
|
93
|
+
monochrome: {
|
|
94
|
+
name: "monochrome",
|
|
95
|
+
reset: `${ESC}0m`,
|
|
96
|
+
bold: `${ESC}1m`,
|
|
97
|
+
dim: `${ESC}2m`, // dim is fine in monochrome (it's weight, not color)
|
|
98
|
+
gold: `${ESC}1m`, // bold only (no color)
|
|
99
|
+
boldGold: `${ESC}1m`,
|
|
100
|
+
cyan: `${ESC}0m`, // normal weight
|
|
101
|
+
boldCyan: `${ESC}1m`, // bold
|
|
102
|
+
green: `${ESC}0m`, // normal
|
|
103
|
+
red: `${ESC}1m`, // bold (errors stand out by weight)
|
|
104
|
+
white: `${ESC}0m`, // normal
|
|
105
|
+
boldWhite: `${ESC}1m`, // bold
|
|
106
|
+
magenta: `${ESC}0m`, // normal
|
|
107
|
+
blue: `${ESC}0m`, // normal
|
|
108
|
+
bgDim: `${ESC}7m`, // reverse video for active tab (no color)
|
|
109
|
+
bgCyan: `${ESC}7m`, // reverse video
|
|
110
|
+
medalBg: {}, // no medal colors in monochrome
|
|
111
|
+
medalFg: `${ESC}1m`, // bold for medal winners
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export let currentTheme = THEMES.dark;
|
|
116
|
+
|
|
117
|
+
export function setTheme(name) {
|
|
118
|
+
if (THEMES[name]) {
|
|
119
|
+
currentTheme = THEMES[name];
|
|
120
|
+
}
|
|
121
|
+
return currentTheme;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getThemeNames() {
|
|
125
|
+
return Object.keys(THEMES);
|
|
126
|
+
}
|
package/presentation/tui.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import { execFile } from "child_process";
|
|
|
22
22
|
import { existsSync, readFileSync } from "fs";
|
|
23
23
|
import os from "os";
|
|
24
24
|
import path from "path";
|
|
25
|
+
import { pathToFileURL } from "node:url";
|
|
25
26
|
import { pkgVersion } from "../lib/pkg-version.mjs";
|
|
26
27
|
import { LEADERBOARD_METRIC } from "../lib/constants.mjs";
|
|
27
28
|
|
|
@@ -49,24 +50,20 @@ export function dashboardEnabledPlatforms({
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
// ── ANSI ───────────────────────────────────────────────────────────────────
|
|
53
|
+
// Theme system: colors are centralized in tui-themes.mjs (tokscale-inspired).
|
|
54
|
+
// The `c` object is a live proxy to the current theme — changing the theme
|
|
55
|
+
// via setTheme() updates all colors. Supports dark/light/high-contrast/monochrome.
|
|
56
|
+
import * as _themeMod from "./tui-themes.mjs";
|
|
52
57
|
const ESC = "\x1b[";
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
red: `${ESC}31m`,
|
|
63
|
-
white: `${ESC}97m`,
|
|
64
|
-
boldWhite: `${ESC}1;97m`,
|
|
65
|
-
magenta: `${ESC}35m`,
|
|
66
|
-
blue: `${ESC}34m`,
|
|
67
|
-
bgDim: `${ESC}48;5;236m`, // dark grey bg for active tab
|
|
68
|
-
bgCyan: `${ESC}48;5;23m`, // dark teal bg for active tab
|
|
69
|
-
};
|
|
58
|
+
// `c` is a live proxy — reads currentTheme at access time so theme switches
|
|
59
|
+
// take effect immediately. The helper functions below (paint/bold/dim/etc.)
|
|
60
|
+
// read from `c` at call time, so they always use the active theme's colors.
|
|
61
|
+
const c = new Proxy({}, {
|
|
62
|
+
get(_target, prop) {
|
|
63
|
+
return _themeMod.currentTheme[prop];
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const setTheme = _themeMod.setTheme;
|
|
70
67
|
const paint = (col, s) => `${col}${s}${c.reset}`;
|
|
71
68
|
const bold = (s) => paint(c.bold, s);
|
|
72
69
|
const dim = (s) => paint(c.dim, s);
|
|
@@ -177,7 +174,7 @@ function ansiTrunc(s, w) {
|
|
|
177
174
|
vis++;
|
|
178
175
|
i++;
|
|
179
176
|
}
|
|
180
|
-
return out +
|
|
177
|
+
return out + c.reset; // reset so a cut mid-color doesn't bleed into next line
|
|
181
178
|
}
|
|
182
179
|
function padEnd(s, w) {
|
|
183
180
|
const v = stripAnsi(s).length;
|
|
@@ -190,6 +187,27 @@ function padStart(s, w) {
|
|
|
190
187
|
function trunc(s, w) {
|
|
191
188
|
return stripAnsi(s).length <= w ? s : s.slice(0, w - 1) + "…";
|
|
192
189
|
}
|
|
190
|
+
// Wrap a plain-text string into dim'd lines that fit within `maxW` columns.
|
|
191
|
+
// Used for no-data/empty-state messages that can be long on narrow terminals.
|
|
192
|
+
// Returns an array of strings (each already dim'd + indented) for emit().
|
|
193
|
+
function wrapDim(text, indent = " ", maxW) {
|
|
194
|
+
const w = maxW || W();
|
|
195
|
+
const avail = w - 2 - indent.length; // 2 = leading " " from emit caller
|
|
196
|
+
if (avail < 20) return [`${indent}${dim(text)}`]; // too narrow to wrap — let it be
|
|
197
|
+
const words = text.split(" ");
|
|
198
|
+
const lines = [];
|
|
199
|
+
let line = "";
|
|
200
|
+
for (const word of words) {
|
|
201
|
+
if ((line + " " + word).trim().length > avail) {
|
|
202
|
+
if (line) lines.push(`${indent}${dim(line.trim())}`);
|
|
203
|
+
line = word;
|
|
204
|
+
} else {
|
|
205
|
+
line = (line + " " + word).trim();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (line) lines.push(`${indent}${dim(line.trim())}`);
|
|
209
|
+
return lines;
|
|
210
|
+
}
|
|
193
211
|
function hr(ch = "─") {
|
|
194
212
|
return dim(ch.repeat(Math.max(0, W() - 4)));
|
|
195
213
|
}
|
|
@@ -644,8 +662,14 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
|
|
|
644
662
|
// Responsive: the full 12-col table is ~124 wide. On terminals narrower than
|
|
645
663
|
// that, drop the two derived columns (Vel, 10x) and tighten the inter-column
|
|
646
664
|
// gap from 2 spaces to 1, so the core pillars + Υ/SNR/Lev/Class still fit.
|
|
665
|
+
// On very narrow terminals (<100), also hide Class and shorten cache headers
|
|
666
|
+
// to CW/CR so the core pillars + Υ/SNR/Lev still fit at 80 cols.
|
|
647
667
|
const narrow = w < 124;
|
|
668
|
+
const veryNarrow = w < 100;
|
|
669
|
+
const ultraNarrow = w < 90;
|
|
648
670
|
const gap = narrow ? " " : " ";
|
|
671
|
+
const cwLabel = veryNarrow ? "CW" : "CacheW";
|
|
672
|
+
const crLabel = veryNarrow ? "CR" : "CacheR";
|
|
649
673
|
const renderRow = (label, colorFn, winKey, p, est = false) => {
|
|
650
674
|
const cas = cascadeFrom(p);
|
|
651
675
|
if (!cas) return;
|
|
@@ -665,15 +689,15 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
|
|
|
665
689
|
),
|
|
666
690
|
padStart((p.cacheRead ?? 0) > 0 ? fmtTok(p.cacheRead) : dim("—"), 9),
|
|
667
691
|
padStart(cas.yield > 10000 ? gold(fmtY(cas.yield)) : fmtY(cas.yield), 9),
|
|
668
|
-
padStart(fmtSNR(cas.snr), 7),
|
|
669
|
-
padStart(fmtLev(cas.leverage) + "×", 7),
|
|
692
|
+
...(ultraNarrow ? [] : [padStart(fmtSNR(cas.snr), 7)]),
|
|
693
|
+
...(ultraNarrow ? [] : [padStart(fmtLev(cas.leverage) + "×", 7)]),
|
|
670
694
|
...(narrow
|
|
671
695
|
? []
|
|
672
696
|
: [
|
|
673
697
|
padStart(cas.velocity?.toFixed(2) ?? "—", 6),
|
|
674
698
|
padStart(cas.dev10x?.toFixed(2) ?? "—", 6),
|
|
675
699
|
]),
|
|
676
|
-
padEnd(clsFn(cas.class), 13),
|
|
700
|
+
...(veryNarrow ? [] : [padEnd(clsFn(cas.class), 13)]),
|
|
677
701
|
];
|
|
678
702
|
emit(` ${cols.join(gap)}`);
|
|
679
703
|
};
|
|
@@ -689,16 +713,16 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
|
|
|
689
713
|
padEnd(hdr("Win"), 5),
|
|
690
714
|
padStart(hdr("Input"), 8),
|
|
691
715
|
padStart(hdr("Output"), 8),
|
|
692
|
-
padStart(hdr(
|
|
693
|
-
padStart(hdr(
|
|
716
|
+
padStart(hdr(cwLabel), 8),
|
|
717
|
+
padStart(hdr(crLabel), 9),
|
|
694
718
|
padStart(hdr("Υ Yield"), 9),
|
|
695
|
-
padStart(hdr("SNR"), 7),
|
|
696
|
-
padStart(hdr("Lev"), 7),
|
|
719
|
+
...(ultraNarrow ? [] : [padStart(hdr("SNR"), 7)]),
|
|
720
|
+
...(ultraNarrow ? [] : [padStart(hdr("Lev"), 7)]),
|
|
697
721
|
...(narrow ? [] : [padStart(hdr("Vel"), 6), padStart(hdr("10x"), 6)]),
|
|
698
|
-
padEnd(hdr("Class"), 13),
|
|
722
|
+
...(veryNarrow ? [] : [padEnd(hdr("Class"), 13)]),
|
|
699
723
|
];
|
|
700
724
|
emit(` ${CH.join(gap)}`);
|
|
701
|
-
emit(` ${dim("·".repeat(Math.max(0, Math.min(w - 4, narrow ? 96 : 114))))}`);
|
|
725
|
+
emit(` ${dim("·".repeat(Math.max(0, Math.min(w - 4, narrow ? (ultraNarrow ? 70 : veryNarrow ? 80 : 96) : 114))))}`);
|
|
702
726
|
|
|
703
727
|
if (active.length === 0) {
|
|
704
728
|
// Count is derived from the registry — a hardcoded number drifts every time an
|
|
@@ -1215,8 +1239,12 @@ function renderCompare(data) {
|
|
|
1215
1239
|
].filter((s) => Object.keys(s.pillars).length > 0);
|
|
1216
1240
|
|
|
1217
1241
|
emit();
|
|
1242
|
+
// Shorten the source list on narrow terminals to avoid header overflow.
|
|
1243
|
+
const sourceList = w < 90
|
|
1244
|
+
? `${SOURCES.length} sources`
|
|
1245
|
+
: "tokenpull vs ccusage vs token-dash vs tokscale";
|
|
1218
1246
|
emit(
|
|
1219
|
-
` ${bold("Source Comparison")} ${dim(`platform: ${platform}`)} ${dim(
|
|
1247
|
+
` ${bold("Source Comparison")} ${dim(`platform: ${platform}`)} ${dim(`· ${sourceList}`)}`,
|
|
1220
1248
|
);
|
|
1221
1249
|
|
|
1222
1250
|
if (!tpData) {
|
|
@@ -1239,16 +1267,17 @@ function renderCompare(data) {
|
|
|
1239
1267
|
if (!td) missing.push("token-dashboard (~/.claude/token-dashboard.db)");
|
|
1240
1268
|
if (!ts) missing.push("tokscale (npm i -g tokscale)");
|
|
1241
1269
|
if (missing.length)
|
|
1242
|
-
|
|
1270
|
+
for (const ln of wrapDim(`no data from: ${missing.join(" · ")}`)) emit(ln);
|
|
1243
1271
|
emit();
|
|
1244
1272
|
|
|
1245
1273
|
// FIX (2026-08-18): if ALL sources are empty (tokenpull has no windows AND all
|
|
1246
1274
|
// verifiers are null), show a clear empty-state message instead of bare headers
|
|
1247
1275
|
// with zero data rows.
|
|
1248
1276
|
if (SOURCES.length === 0) {
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
)
|
|
1277
|
+
for (const ln of wrapDim(
|
|
1278
|
+
"no source data available for this platform — install a verifier or check ~/.claude/projects/",
|
|
1279
|
+
))
|
|
1280
|
+
emit(ln);
|
|
1252
1281
|
return;
|
|
1253
1282
|
}
|
|
1254
1283
|
|
|
@@ -1305,6 +1334,28 @@ function renderCompare(data) {
|
|
|
1305
1334
|
emit();
|
|
1306
1335
|
}
|
|
1307
1336
|
|
|
1337
|
+
// Fill the dead-space between the source table and cascade metrics with a
|
|
1338
|
+
// log-scale Υ Yield comparison bar — owner wants more mini-graphs, and this
|
|
1339
|
+
// gap was flagged as dead-space by the audit.
|
|
1340
|
+
if (used < budget - 6 && SOURCES.length > 0) {
|
|
1341
|
+
const yields = SOURCES.map((s) => {
|
|
1342
|
+
const p = s.pillars["all"];
|
|
1343
|
+
const cas = p ? cascadeFrom(p) : null;
|
|
1344
|
+
return { name: s.name, color: s.color, yield: cas?.yield ?? 0 };
|
|
1345
|
+
}).filter((y) => y.yield > 0);
|
|
1346
|
+
if (yields.length > 0) {
|
|
1347
|
+
const maxYield = Math.max(...yields.map((y) => y.yield));
|
|
1348
|
+
const maxLog = Math.log10(maxYield) || 1;
|
|
1349
|
+
const barW = Math.min(Math.max(w - 40, 15), 30);
|
|
1350
|
+
emit(` ${dim("Υ Yield comparison (log scale)")}`);
|
|
1351
|
+
for (const y of yields) {
|
|
1352
|
+
const { bar } = logBar(y.yield, maxLog, barW, c.cyan);
|
|
1353
|
+
emit(` ${padEnd(y.color(y.name), 12)} ${bar} ${fmtY(y.yield)}`);
|
|
1354
|
+
}
|
|
1355
|
+
emit();
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1308
1359
|
if (used < budget - 4) {
|
|
1309
1360
|
emit(` ${hr()}`);
|
|
1310
1361
|
emit(
|
|
@@ -1387,8 +1438,12 @@ function renderSubmissions(
|
|
|
1387
1438
|
? `${dim(" · you highlighted")}`
|
|
1388
1439
|
: "";
|
|
1389
1440
|
emit();
|
|
1441
|
+
// Shorten the header on narrow terminals to avoid overflow.
|
|
1442
|
+
const boardHeader = w < 90
|
|
1443
|
+
? `window: ${window} · ranked by Υ Yield`
|
|
1444
|
+
: `window: ${window} · all submissions ranked by Υ Yield · signalaf.com`;
|
|
1390
1445
|
emit(
|
|
1391
|
-
` ${bold("Submissions")} ${dim(
|
|
1446
|
+
` ${bold("Submissions")} ${dim(boardHeader)}${modeLabel}`,
|
|
1392
1447
|
);
|
|
1393
1448
|
emit();
|
|
1394
1449
|
|
|
@@ -1419,15 +1474,26 @@ function renderSubmissions(
|
|
|
1419
1474
|
? v.toFixed(2)
|
|
1420
1475
|
: String(v);
|
|
1421
1476
|
|
|
1477
|
+
// Responsive column hiding — the full table is 106 cols wide. Hide columns
|
|
1478
|
+
// at narrower terminals so rows never overflow/wrap/garble.
|
|
1479
|
+
// w >= 120: all columns (106 cols fits)
|
|
1480
|
+
// w < 120: hide Tokens (saves 11) → 95 cols
|
|
1481
|
+
// w < 100: hide Tokens + Class (saves 14) → 81 cols
|
|
1482
|
+
// w < 90: hide Tokens + Class + Op Ratio, shorten Codename 20→16 → 61 cols
|
|
1483
|
+
const showTokens = w >= 120;
|
|
1484
|
+
const showClass = w >= 100;
|
|
1485
|
+
const showOpRatio = w >= 90;
|
|
1486
|
+
const nameWidth = w < 90 ? 16 : 20;
|
|
1487
|
+
|
|
1422
1488
|
const SH = [
|
|
1423
1489
|
padStart(hdr("#"), 4),
|
|
1424
|
-
padEnd(hdr("Codename"),
|
|
1490
|
+
padEnd(hdr("Codename"), nameWidth),
|
|
1425
1491
|
padEnd(hdr("Platform"), 10),
|
|
1426
1492
|
padEnd(hdr("Win"), 6),
|
|
1427
1493
|
padStart(hdr("Υ Yield"), 9),
|
|
1428
|
-
padEnd(hdr("Op Ratio"), 18),
|
|
1429
|
-
padEnd(hdr("Class"), 12),
|
|
1430
|
-
padStart(hdr("Tokens"), 9),
|
|
1494
|
+
...(showOpRatio ? [padEnd(hdr("Op Ratio"), 18)] : []),
|
|
1495
|
+
...(showClass ? [padEnd(hdr("Class"), 12)] : []),
|
|
1496
|
+
...(showTokens ? [padStart(hdr("Tokens"), 9)] : []),
|
|
1431
1497
|
];
|
|
1432
1498
|
emit(` ${SH.join(" ")}`);
|
|
1433
1499
|
emit(` ${dim("·".repeat(Math.min(w - 4, 100)))}`);
|
|
@@ -1445,10 +1511,10 @@ function renderSubmissions(
|
|
|
1445
1511
|
: idx < 3
|
|
1446
1512
|
? cyan(`#${idx + 1}`)
|
|
1447
1513
|
: `#${idx + 1}`;
|
|
1448
|
-
const nmRaw = trunc(gName(e),
|
|
1514
|
+
const nmRaw = trunc(gName(e), nameWidth);
|
|
1449
1515
|
const nm = isYou
|
|
1450
|
-
? `${c.bgCyan}${c.boldCyan}${padEnd(` ${trunc(gName(e),
|
|
1451
|
-
: padEnd(nmRaw,
|
|
1516
|
+
? `${c.bgCyan}${c.boldCyan}${padEnd(` ${trunc(gName(e), nameWidth - 2)} `, nameWidth)}${c.reset}`
|
|
1517
|
+
: padEnd(nmRaw, nameWidth);
|
|
1452
1518
|
const yv = gYield(e);
|
|
1453
1519
|
const yld = padStart(
|
|
1454
1520
|
yv != null ? (yv > 10000 ? gold(fmtY(yv)) : fmtY(yv)) : "—",
|
|
@@ -1457,9 +1523,17 @@ function renderSubmissions(
|
|
|
1457
1523
|
const cls = padEnd(colorCls(gClass(e)), 12);
|
|
1458
1524
|
const tk = gTokens(e);
|
|
1459
1525
|
const youMark = isYou ? ` ${c.bgCyan}${c.boldCyan}YOU${c.reset}` : "";
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1526
|
+
const cells = [
|
|
1527
|
+
padStart(rk, 4),
|
|
1528
|
+
nm,
|
|
1529
|
+
padEnd(cyan(trunc(gPlat(e), 10)), 10),
|
|
1530
|
+
padEnd(dim(trunc(gWin(e), 6)), 6),
|
|
1531
|
+
yld,
|
|
1532
|
+
...(showOpRatio ? [padEnd(dim(fmtOp(gOpRatio(e))), 18)] : []),
|
|
1533
|
+
...(showClass ? [cls] : []),
|
|
1534
|
+
...(showTokens ? [padStart(tk != null ? fmtTok(tk) : "—", 9)] : []),
|
|
1535
|
+
];
|
|
1536
|
+
emit(` ${cells.join(" ")}${youMark}`);
|
|
1463
1537
|
shown++;
|
|
1464
1538
|
}
|
|
1465
1539
|
// A3-style: if the height budget dropped rows, say so instead of silently truncating.
|
|
@@ -1547,10 +1621,16 @@ function renderBoard(
|
|
|
1547
1621
|
d10: top3((e) => e.dev10x),
|
|
1548
1622
|
};
|
|
1549
1623
|
// Wrap a padded cell in a medal bg-tint (dark fg on the medal color) when its value places top-3.
|
|
1624
|
+
// Theme-aware: monochrome uses bold instead of color backgrounds.
|
|
1550
1625
|
const medal = (map, val, padded) => {
|
|
1551
1626
|
const place = val == null ? null : map.get(val);
|
|
1552
1627
|
if (!place) return padded;
|
|
1553
|
-
|
|
1628
|
+
const bgCode = c.medalBg?.[place];
|
|
1629
|
+
if (bgCode != null) {
|
|
1630
|
+
return `${ESC}48;5;${bgCode}m${c.medalFg}${padded}${c.reset}`;
|
|
1631
|
+
}
|
|
1632
|
+
// Monochrome / no medal bg — use bold
|
|
1633
|
+
return `${c.bold}${padded}${c.reset}`;
|
|
1554
1634
|
};
|
|
1555
1635
|
|
|
1556
1636
|
const BH = [
|
|
@@ -1695,11 +1775,22 @@ function renderConnect(id, codeBuf = "", msg = "") {
|
|
|
1695
1775
|
);
|
|
1696
1776
|
writeln();
|
|
1697
1777
|
writeln(
|
|
1698
|
-
` ${dim('Need a new key? Click "New key" at signalaf.com → Settings, then paste it here.')}`,
|
|
1699
|
-
);
|
|
1700
|
-
writeln(
|
|
1701
|
-
` ${dim("Signed in on the wrong device, or want a fresh start?")} ${bold("[X]")} ${dim("signs out — next paste provisions a fresh device.")}`,
|
|
1778
|
+
` ${dim(W() < 90 ? 'Need a new key? signalaf.com → Settings → "New key", then paste here.' : 'Need a new key? Click "New key" at signalaf.com → Settings, then paste it here.')}`,
|
|
1702
1779
|
);
|
|
1780
|
+
// Wrap the sign-out help across 2 lines on narrow terminals (the full
|
|
1781
|
+
// sentence is 108 cols — overflows 100-col terminals by 8).
|
|
1782
|
+
if (W() >= 110) {
|
|
1783
|
+
writeln(
|
|
1784
|
+
` ${dim("Signed in on the wrong device, or want a fresh start?")} ${bold("[X]")} ${dim("signs out — next paste provisions a fresh device.")}`,
|
|
1785
|
+
);
|
|
1786
|
+
} else {
|
|
1787
|
+
writeln(
|
|
1788
|
+
` ${dim("Signed in on the wrong device, or want a fresh start?")}`,
|
|
1789
|
+
);
|
|
1790
|
+
writeln(
|
|
1791
|
+
` ${bold("[X]")} ${dim("signs out — next paste provisions a fresh device.")}`,
|
|
1792
|
+
);
|
|
1793
|
+
}
|
|
1703
1794
|
} else {
|
|
1704
1795
|
writeln(` ${bold("Log in to submit to board")}`);
|
|
1705
1796
|
writeln(` ${hr()}`);
|
|
@@ -1723,7 +1814,13 @@ function renderConnect(id, codeBuf = "", msg = "") {
|
|
|
1723
1814
|
// watch grid), instead of a single platform/window. `platform`/`win` are OPTIONAL focus filters —
|
|
1724
1815
|
// null/'all'/'all-windows' means "everything". Each row is a (platform, window) cascade cell:
|
|
1725
1816
|
// Υ Yield + SNR/Lev/Vel/class. Iterates active platforms (input+output > 0 in any window).
|
|
1726
|
-
|
|
1817
|
+
// Load Watch data once — scan all candidate platforms for active sessions.
|
|
1818
|
+
// Separated from rendering (renderWatchData) so the headless audit can load
|
|
1819
|
+
// once and render across many sizes/themes without re-scanning the filesystem
|
|
1820
|
+
// each frame (each full scan is ~8s across 16 platforms; 16 frames × 8s would
|
|
1821
|
+
// be ~130s of pure re-scan otherwise). Mirrors how Dashboard/Compare/Board
|
|
1822
|
+
// already separate load from render.
|
|
1823
|
+
async function loadWatchData(platform = "all", win = "all-windows") {
|
|
1727
1824
|
// ../tokenpull.mjs — this file lives in presentation/, so "./tokenpull.mjs" resolved
|
|
1728
1825
|
// to presentation/tokenpull.mjs (nonexistent) and threw ERR_MODULE_NOT_FOUND for
|
|
1729
1826
|
// every row. Matches the static `../tokenpull.mjs` import at the top of this file.
|
|
@@ -1748,17 +1845,6 @@ async function renderWatch(platform = "all", win = "all-windows") {
|
|
|
1748
1845
|
];
|
|
1749
1846
|
const platFilter = platform && platform !== "all" ? platform : null;
|
|
1750
1847
|
const winFilter = win && win !== "all-windows" ? win : null;
|
|
1751
|
-
const WINS = winFilter ? [winFilter] : ["7d", "30d", "90d", "all"];
|
|
1752
|
-
const w = W();
|
|
1753
|
-
const budget = H() - 4;
|
|
1754
|
-
let used = 0;
|
|
1755
|
-
const emit = (s = "") => {
|
|
1756
|
-
if (used < budget) {
|
|
1757
|
-
writeln(s);
|
|
1758
|
-
used++;
|
|
1759
|
-
}
|
|
1760
|
-
};
|
|
1761
|
-
|
|
1762
1848
|
// Detect active platforms (any window with input+output > 0), respecting an optional [P] focus.
|
|
1763
1849
|
const candidates = platFilter ? [platFilter] : ALL_PLATFORMS;
|
|
1764
1850
|
const settled = await Promise.allSettled(
|
|
@@ -1778,6 +1864,23 @@ async function renderWatch(platform = "all", win = "all-windows") {
|
|
|
1778
1864
|
(a, b) =>
|
|
1779
1865
|
ALL_PLATFORMS.indexOf(a.platform) - ALL_PLATFORMS.indexOf(b.platform),
|
|
1780
1866
|
);
|
|
1867
|
+
return { active, platFilter, winFilter };
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// Render the Watch tab from pre-loaded data. Sync — no scanning, safe to call
|
|
1871
|
+
// once per terminal size/theme in the audit.
|
|
1872
|
+
function renderWatchData(data) {
|
|
1873
|
+
const { active, platFilter, winFilter } = data;
|
|
1874
|
+
const WINS = winFilter ? [winFilter] : ["7d", "30d", "90d", "all"];
|
|
1875
|
+
const w = W();
|
|
1876
|
+
const budget = H() - 4;
|
|
1877
|
+
let used = 0;
|
|
1878
|
+
const emit = (s = "") => {
|
|
1879
|
+
if (used < budget) {
|
|
1880
|
+
writeln(s);
|
|
1881
|
+
used++;
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1781
1884
|
|
|
1782
1885
|
const scopeLabel =
|
|
1783
1886
|
(platFilter || "all active platforms") +
|
|
@@ -1789,9 +1892,10 @@ async function renderWatch(platform = "all", win = "all-windows") {
|
|
|
1789
1892
|
|
|
1790
1893
|
if (active.length === 0) {
|
|
1791
1894
|
emit();
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
)
|
|
1895
|
+
for (const ln of wrapDim(
|
|
1896
|
+
"no active platforms detected — run some sessions, this picks them up automatically",
|
|
1897
|
+
))
|
|
1898
|
+
emit(ln);
|
|
1795
1899
|
return;
|
|
1796
1900
|
}
|
|
1797
1901
|
|
|
@@ -1821,6 +1925,13 @@ async function renderWatch(platform = "all", win = "all-windows") {
|
|
|
1821
1925
|
}
|
|
1822
1926
|
}
|
|
1823
1927
|
|
|
1928
|
+
// Convenience: scan + render in one call (used by --render and --record, which
|
|
1929
|
+
// only render once). The audit uses loadWatchData + renderWatchData separately
|
|
1930
|
+
// to avoid re-scanning across its 16 frames.
|
|
1931
|
+
async function renderWatch(platform = "all", win = "all-windows") {
|
|
1932
|
+
renderWatchData(await loadWatchData(platform, win));
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1824
1935
|
// ── SUBMIT PREVIEW (FIX I1 / FIX E) — see→confirm→send ───────────────────────
|
|
1825
1936
|
// Renders a platform × window grid of what submit_verified WOULD send, using the
|
|
1826
1937
|
// already-loaded dashData. [Enter] confirms → fires submit_verified per platform;
|
|
@@ -2043,6 +2154,12 @@ export async function runTui({
|
|
|
2043
2154
|
platform: initPlatform = "claude",
|
|
2044
2155
|
window: win = "7d",
|
|
2045
2156
|
} = {}) {
|
|
2157
|
+
// Theme selection — --theme <name> sets the color palette (dark/light/high-contrast/monochrome)
|
|
2158
|
+
const themeIdx = process.argv.indexOf("--theme");
|
|
2159
|
+
if (themeIdx !== -1 && process.argv[themeIdx + 1]) {
|
|
2160
|
+
setTheme(process.argv[themeIdx + 1]);
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2046
2163
|
// FIX J: platform is mutable — [P] cycles it on Compare + Watch tabs. Was a const
|
|
2047
2164
|
// param (permanently locked to 'claude'); now a let so the user can switch + [S]
|
|
2048
2165
|
// submits the selected platform (needed for Phase 4 multi-platform submit).
|
|
@@ -2055,6 +2172,51 @@ export async function runTui({
|
|
|
2055
2172
|
return;
|
|
2056
2173
|
}
|
|
2057
2174
|
|
|
2175
|
+
// Audit mode — headless review of every tab × every terminal size. Checks
|
|
2176
|
+
// overflow, truncation, dead space, budget, color health, sparkline integrity,
|
|
2177
|
+
// responsive design, visual weight. Supports golden frame snapshots
|
|
2178
|
+
// (--golden-save / --golden-check), SVG export (--svg), and CI mode (--ci
|
|
2179
|
+
// exits non-zero on HIGH severity issues).
|
|
2180
|
+
const ai = process.argv.indexOf("--audit");
|
|
2181
|
+
if (ai !== -1) {
|
|
2182
|
+
let runAudit, formatAuditReport;
|
|
2183
|
+
try {
|
|
2184
|
+
({ runAudit, formatAuditReport } = await import("../dev/tui-audit.mjs"));
|
|
2185
|
+
} catch {
|
|
2186
|
+
console.error("--audit is a development tool, not available in the npm package.");
|
|
2187
|
+
console.error("Clone the repo to use it: https://github.com/SunrisesIllNeverSee/sigrank-mcp");
|
|
2188
|
+
process.exit(1);
|
|
2189
|
+
}
|
|
2190
|
+
const auditData = await runAudit({
|
|
2191
|
+
goldenSave: process.argv.includes("--golden-save"),
|
|
2192
|
+
goldenCheck: process.argv.includes("--golden-check"),
|
|
2193
|
+
svg: process.argv.includes("--svg"),
|
|
2194
|
+
png: process.argv.includes("--png"),
|
|
2195
|
+
ci: process.argv.includes("--ci"),
|
|
2196
|
+
});
|
|
2197
|
+
process.stdout.write(formatAuditReport(auditData) + "\n");
|
|
2198
|
+
// Always exit in audit mode — a data load that timed out may still have a
|
|
2199
|
+
// pending background promise (network fetch / local scan) that would keep
|
|
2200
|
+
// the event loop alive after runTui returns. The audit is headless and must
|
|
2201
|
+
// terminate; unconditional exit guarantees that regardless of ciExitCode.
|
|
2202
|
+
process.exit(auditData.ciExitCode);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// Record mode — capture a TUI session as an animated GIF (headless, no TTY needed)
|
|
2206
|
+
const recIdx = process.argv.indexOf("--record");
|
|
2207
|
+
if (recIdx !== -1) {
|
|
2208
|
+
let recordCli;
|
|
2209
|
+
try {
|
|
2210
|
+
({ recordCli } = await import("../dev/tui-record.mjs"));
|
|
2211
|
+
} catch {
|
|
2212
|
+
console.error("--record is a development tool, not available in the npm package.");
|
|
2213
|
+
console.error("Clone the repo to use it: https://github.com/SunrisesIllNeverSee/sigrank-mcp");
|
|
2214
|
+
process.exit(1);
|
|
2215
|
+
}
|
|
2216
|
+
await recordCli(process.argv);
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2058
2220
|
write(ENTER_ALT); // switch to alternate screen — original terminal state preserved on exit
|
|
2059
2221
|
write(CLEAR_SB); // FIXED-WINDOW (2026-06-27): clear scrollback so nothing is above the TUI
|
|
2060
2222
|
write(HIDE);
|
|
@@ -2905,8 +3067,41 @@ export async function runTui({
|
|
|
2905
3067
|
writeln();
|
|
2906
3068
|
}
|
|
2907
3069
|
|
|
3070
|
+
// ── Exports for the headless audit system (tui-audit.mjs) ──────────────────
|
|
3071
|
+
// These expose the render functions + buffer state so the audit module can
|
|
3072
|
+
// render tabs in-process (fast) instead of spawning child processes.
|
|
3073
|
+
export {
|
|
3074
|
+
renderDashboard,
|
|
3075
|
+
renderTrends,
|
|
3076
|
+
renderCompare,
|
|
3077
|
+
renderBoard,
|
|
3078
|
+
renderConnect,
|
|
3079
|
+
renderWatch,
|
|
3080
|
+
renderWatchData,
|
|
3081
|
+
loadWatchData,
|
|
3082
|
+
loadDashboardData,
|
|
3083
|
+
loadCompareData,
|
|
3084
|
+
loadBoardData,
|
|
3085
|
+
startBuffer,
|
|
3086
|
+
stripAnsi,
|
|
3087
|
+
};
|
|
3088
|
+
// _screenBuf is a `let` — exporting it snapshots the value, not the binding.
|
|
3089
|
+
// Expose getter/setter so the audit module can read the live buffer after
|
|
3090
|
+
// startBuffer() + render() populate it, and reset it after capturing.
|
|
3091
|
+
export function _getScreenBuf() {
|
|
3092
|
+
return _screenBuf;
|
|
3093
|
+
}
|
|
3094
|
+
export function _resetBuf() {
|
|
3095
|
+
_screenBuf = null;
|
|
3096
|
+
_footerBuf = null;
|
|
3097
|
+
}
|
|
3098
|
+
|
|
2908
3099
|
// Direct-run entry (e.g. `node tui.mjs --render 0`). Normal launch is via cli.mjs.
|
|
2909
|
-
|
|
3100
|
+
// pathToFileURL resolves relative argv[1] (e.g. `presentation/tui.mjs`) to the
|
|
3101
|
+
// same absolute file:// URL that import.meta.url always carries — without it,
|
|
3102
|
+
// `node presentation/tui.mjs --audit` never matched the guard and runTui never
|
|
3103
|
+
// ran (silent no-op). This is the standard Node ESM main-module detection.
|
|
3104
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
2910
3105
|
runTui().catch((e) => {
|
|
2911
3106
|
console.error(e);
|
|
2912
3107
|
process.exit(1);
|