sigrank 0.0.213 → 0.0.214
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 +3 -1
- package/presentation/tui-themes.mjs +122 -0
- package/presentation/tui.mjs +229 -57
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.213",
|
|
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.214",
|
|
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",
|
|
@@ -82,7 +82,9 @@
|
|
|
82
82
|
],
|
|
83
83
|
"dependencies": {
|
|
84
84
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
85
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
85
86
|
"ccusage": "^20.0.19",
|
|
87
|
+
"gifenc": "^1.0.3",
|
|
86
88
|
"tokscale": "^4.7.0"
|
|
87
89
|
},
|
|
88
90
|
"overrides": {
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
dark: {
|
|
22
|
+
name: "dark",
|
|
23
|
+
reset: `${ESC}0m`,
|
|
24
|
+
bold: `${ESC}1m`,
|
|
25
|
+
dim: `${ESC}2m`,
|
|
26
|
+
gold: `${ESC}33m`,
|
|
27
|
+
boldGold: `${ESC}1;33m`,
|
|
28
|
+
cyan: `${ESC}36m`,
|
|
29
|
+
boldCyan: `${ESC}1;36m`,
|
|
30
|
+
green: `${ESC}32m`,
|
|
31
|
+
red: `${ESC}31m`,
|
|
32
|
+
white: `${ESC}97m`,
|
|
33
|
+
boldWhite: `${ESC}1;97m`,
|
|
34
|
+
magenta: `${ESC}35m`,
|
|
35
|
+
blue: `${ESC}34m`,
|
|
36
|
+
bgDim: `${ESC}48;5;236m`,
|
|
37
|
+
bgCyan: `${ESC}48;5;23m`,
|
|
38
|
+
// Medal backgrounds (256-color) — gold/silver/bronze podium tints
|
|
39
|
+
medalBg: { 1: 220, 2: 250, 3: 130 },
|
|
40
|
+
medalFg: `${ESC}38;5;232m`, // dark text on medal bg
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// ── Light — dark text on light/white terminals ──
|
|
44
|
+
// Colors adjusted for contrast against white/light backgrounds.
|
|
45
|
+
light: {
|
|
46
|
+
name: "light",
|
|
47
|
+
reset: `${ESC}0m`,
|
|
48
|
+
bold: `${ESC}1m`,
|
|
49
|
+
dim: `${ESC}2m`,
|
|
50
|
+
gold: `${ESC}33m`, // yellow is readable on white
|
|
51
|
+
boldGold: `${ESC}1;33m`,
|
|
52
|
+
cyan: `${ESC}36m`, // cyan is readable on white
|
|
53
|
+
boldCyan: `${ESC}1;36m`,
|
|
54
|
+
green: `${ESC}32m`, // standard green (darker shade would be better but ANSI 32 is the option)
|
|
55
|
+
red: `${ESC}31m`, // standard red
|
|
56
|
+
white: `${ESC}30m`, // BLACK text on light bg (was bright white)
|
|
57
|
+
boldWhite: `${ESC}1;30m`, // bold black
|
|
58
|
+
magenta: `${ESC}35m`,
|
|
59
|
+
blue: `${ESC}34m`, // blue is readable on white
|
|
60
|
+
bgDim: `${ESC}48;5;250m`, // light grey bg for active tab
|
|
61
|
+
bgCyan: `${ESC}48;5;194m`,// light teal bg for active tab
|
|
62
|
+
medalBg: { 1: 220, 2: 250, 3: 130 },
|
|
63
|
+
medalFg: `${ESC}38;5;232m`,
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// ── High contrast — bold everything, no dim, maximum readability ──
|
|
67
|
+
highContrast: {
|
|
68
|
+
name: "high-contrast",
|
|
69
|
+
reset: `${ESC}0m`,
|
|
70
|
+
bold: `${ESC}1m`,
|
|
71
|
+
dim: `${ESC}0;37m`, // NO dim — full brightness white instead
|
|
72
|
+
gold: `${ESC}1;33m`, // bold gold
|
|
73
|
+
boldGold: `${ESC}1;33m`,
|
|
74
|
+
cyan: `${ESC}1;36m`, // bold cyan
|
|
75
|
+
boldCyan: `${ESC}1;36m`,
|
|
76
|
+
green: `${ESC}1;32m`, // bold green
|
|
77
|
+
red: `${ESC}1;31m`, // bold red
|
|
78
|
+
white: `${ESC}1;97m`, // bold bright white
|
|
79
|
+
boldWhite: `${ESC}1;97m`,
|
|
80
|
+
magenta: `${ESC}1;35m`, // bold magenta
|
|
81
|
+
blue: `${ESC}1;34m`, // bold blue
|
|
82
|
+
bgDim: `${ESC}48;5;238m`, // slightly lighter dark bg
|
|
83
|
+
bgCyan: `${ESC}48;5;24m`, // slightly lighter teal bg
|
|
84
|
+
medalBg: { 1: 220, 2: 250, 3: 130 },
|
|
85
|
+
medalFg: `${ESC}1;38;5;232m`,
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// ── Monochrome — bold/normal only, no color (colorblind-safe) ──
|
|
89
|
+
monochrome: {
|
|
90
|
+
name: "monochrome",
|
|
91
|
+
reset: `${ESC}0m`,
|
|
92
|
+
bold: `${ESC}1m`,
|
|
93
|
+
dim: `${ESC}2m`, // dim is fine in monochrome (it's weight, not color)
|
|
94
|
+
gold: `${ESC}1m`, // bold only (no color)
|
|
95
|
+
boldGold: `${ESC}1m`,
|
|
96
|
+
cyan: `${ESC}0m`, // normal weight
|
|
97
|
+
boldCyan: `${ESC}1m`, // bold
|
|
98
|
+
green: `${ESC}0m`, // normal
|
|
99
|
+
red: `${ESC}1m`, // bold (errors stand out by weight)
|
|
100
|
+
white: `${ESC}0m`, // normal
|
|
101
|
+
boldWhite: `${ESC}1m`, // bold
|
|
102
|
+
magenta: `${ESC}0m`, // normal
|
|
103
|
+
blue: `${ESC}0m`, // normal
|
|
104
|
+
bgDim: `${ESC}7m`, // reverse video for active tab (no color)
|
|
105
|
+
bgCyan: `${ESC}7m`, // reverse video
|
|
106
|
+
medalBg: {}, // no medal colors in monochrome
|
|
107
|
+
medalFg: `${ESC}1m`, // bold for medal winners
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export let currentTheme = THEMES.dark;
|
|
112
|
+
|
|
113
|
+
export function setTheme(name) {
|
|
114
|
+
if (THEMES[name]) {
|
|
115
|
+
currentTheme = THEMES[name];
|
|
116
|
+
}
|
|
117
|
+
return currentTheme;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getThemeNames() {
|
|
121
|
+
return Object.keys(THEMES);
|
|
122
|
+
}
|
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;
|
|
@@ -644,8 +641,14 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
|
|
|
644
641
|
// Responsive: the full 12-col table is ~124 wide. On terminals narrower than
|
|
645
642
|
// that, drop the two derived columns (Vel, 10x) and tighten the inter-column
|
|
646
643
|
// gap from 2 spaces to 1, so the core pillars + Υ/SNR/Lev/Class still fit.
|
|
644
|
+
// On very narrow terminals (<100), also hide Class and shorten cache headers
|
|
645
|
+
// to CW/CR so the core pillars + Υ/SNR/Lev still fit at 80 cols.
|
|
647
646
|
const narrow = w < 124;
|
|
647
|
+
const veryNarrow = w < 100;
|
|
648
|
+
const ultraNarrow = w < 90;
|
|
648
649
|
const gap = narrow ? " " : " ";
|
|
650
|
+
const cwLabel = veryNarrow ? "CW" : "CacheW";
|
|
651
|
+
const crLabel = veryNarrow ? "CR" : "CacheR";
|
|
649
652
|
const renderRow = (label, colorFn, winKey, p, est = false) => {
|
|
650
653
|
const cas = cascadeFrom(p);
|
|
651
654
|
if (!cas) return;
|
|
@@ -665,15 +668,15 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
|
|
|
665
668
|
),
|
|
666
669
|
padStart((p.cacheRead ?? 0) > 0 ? fmtTok(p.cacheRead) : dim("—"), 9),
|
|
667
670
|
padStart(cas.yield > 10000 ? gold(fmtY(cas.yield)) : fmtY(cas.yield), 9),
|
|
668
|
-
padStart(fmtSNR(cas.snr), 7),
|
|
669
|
-
padStart(fmtLev(cas.leverage) + "×", 7),
|
|
671
|
+
...(ultraNarrow ? [] : [padStart(fmtSNR(cas.snr), 7)]),
|
|
672
|
+
...(ultraNarrow ? [] : [padStart(fmtLev(cas.leverage) + "×", 7)]),
|
|
670
673
|
...(narrow
|
|
671
674
|
? []
|
|
672
675
|
: [
|
|
673
676
|
padStart(cas.velocity?.toFixed(2) ?? "—", 6),
|
|
674
677
|
padStart(cas.dev10x?.toFixed(2) ?? "—", 6),
|
|
675
678
|
]),
|
|
676
|
-
padEnd(clsFn(cas.class), 13),
|
|
679
|
+
...(veryNarrow ? [] : [padEnd(clsFn(cas.class), 13)]),
|
|
677
680
|
];
|
|
678
681
|
emit(` ${cols.join(gap)}`);
|
|
679
682
|
};
|
|
@@ -689,16 +692,16 @@ function renderDashboard(data, status = "", scrollOffset = 0) {
|
|
|
689
692
|
padEnd(hdr("Win"), 5),
|
|
690
693
|
padStart(hdr("Input"), 8),
|
|
691
694
|
padStart(hdr("Output"), 8),
|
|
692
|
-
padStart(hdr(
|
|
693
|
-
padStart(hdr(
|
|
695
|
+
padStart(hdr(cwLabel), 8),
|
|
696
|
+
padStart(hdr(crLabel), 9),
|
|
694
697
|
padStart(hdr("Υ Yield"), 9),
|
|
695
|
-
padStart(hdr("SNR"), 7),
|
|
696
|
-
padStart(hdr("Lev"), 7),
|
|
698
|
+
...(ultraNarrow ? [] : [padStart(hdr("SNR"), 7)]),
|
|
699
|
+
...(ultraNarrow ? [] : [padStart(hdr("Lev"), 7)]),
|
|
697
700
|
...(narrow ? [] : [padStart(hdr("Vel"), 6), padStart(hdr("10x"), 6)]),
|
|
698
|
-
padEnd(hdr("Class"), 13),
|
|
701
|
+
...(veryNarrow ? [] : [padEnd(hdr("Class"), 13)]),
|
|
699
702
|
];
|
|
700
703
|
emit(` ${CH.join(gap)}`);
|
|
701
|
-
emit(` ${dim("·".repeat(Math.max(0, Math.min(w - 4, narrow ? 96 : 114))))}`);
|
|
704
|
+
emit(` ${dim("·".repeat(Math.max(0, Math.min(w - 4, narrow ? (ultraNarrow ? 70 : veryNarrow ? 80 : 96) : 114))))}`);
|
|
702
705
|
|
|
703
706
|
if (active.length === 0) {
|
|
704
707
|
// Count is derived from the registry — a hardcoded number drifts every time an
|
|
@@ -1215,8 +1218,12 @@ function renderCompare(data) {
|
|
|
1215
1218
|
].filter((s) => Object.keys(s.pillars).length > 0);
|
|
1216
1219
|
|
|
1217
1220
|
emit();
|
|
1221
|
+
// Shorten the source list on narrow terminals to avoid header overflow.
|
|
1222
|
+
const sourceList = w < 90
|
|
1223
|
+
? `${SOURCES.length} sources`
|
|
1224
|
+
: "tokenpull vs ccusage vs token-dash vs tokscale";
|
|
1218
1225
|
emit(
|
|
1219
|
-
` ${bold("Source Comparison")} ${dim(`platform: ${platform}`)} ${dim(
|
|
1226
|
+
` ${bold("Source Comparison")} ${dim(`platform: ${platform}`)} ${dim(`· ${sourceList}`)}`,
|
|
1220
1227
|
);
|
|
1221
1228
|
|
|
1222
1229
|
if (!tpData) {
|
|
@@ -1305,6 +1312,28 @@ function renderCompare(data) {
|
|
|
1305
1312
|
emit();
|
|
1306
1313
|
}
|
|
1307
1314
|
|
|
1315
|
+
// Fill the dead-space between the source table and cascade metrics with a
|
|
1316
|
+
// log-scale Υ Yield comparison bar — owner wants more mini-graphs, and this
|
|
1317
|
+
// gap was flagged as dead-space by the audit.
|
|
1318
|
+
if (used < budget - 6 && SOURCES.length > 0) {
|
|
1319
|
+
const yields = SOURCES.map((s) => {
|
|
1320
|
+
const p = s.pillars["all"];
|
|
1321
|
+
const cas = p ? cascadeFrom(p) : null;
|
|
1322
|
+
return { name: s.name, color: s.color, yield: cas?.yield ?? 0 };
|
|
1323
|
+
}).filter((y) => y.yield > 0);
|
|
1324
|
+
if (yields.length > 0) {
|
|
1325
|
+
const maxYield = Math.max(...yields.map((y) => y.yield));
|
|
1326
|
+
const maxLog = Math.log10(maxYield) || 1;
|
|
1327
|
+
const barW = Math.min(Math.max(w - 40, 15), 30);
|
|
1328
|
+
emit(` ${dim("Υ Yield comparison (log scale)")}`);
|
|
1329
|
+
for (const y of yields) {
|
|
1330
|
+
const { bar } = logBar(y.yield, maxLog, barW, c.cyan);
|
|
1331
|
+
emit(` ${padEnd(y.color(y.name), 12)} ${bar} ${fmtY(y.yield)}`);
|
|
1332
|
+
}
|
|
1333
|
+
emit();
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1308
1337
|
if (used < budget - 4) {
|
|
1309
1338
|
emit(` ${hr()}`);
|
|
1310
1339
|
emit(
|
|
@@ -1387,8 +1416,12 @@ function renderSubmissions(
|
|
|
1387
1416
|
? `${dim(" · you highlighted")}`
|
|
1388
1417
|
: "";
|
|
1389
1418
|
emit();
|
|
1419
|
+
// Shorten the header on narrow terminals to avoid overflow.
|
|
1420
|
+
const boardHeader = w < 90
|
|
1421
|
+
? `window: ${window} · ranked by Υ Yield`
|
|
1422
|
+
: `window: ${window} · all submissions ranked by Υ Yield · signalaf.com`;
|
|
1390
1423
|
emit(
|
|
1391
|
-
` ${bold("Submissions")} ${dim(
|
|
1424
|
+
` ${bold("Submissions")} ${dim(boardHeader)}${modeLabel}`,
|
|
1392
1425
|
);
|
|
1393
1426
|
emit();
|
|
1394
1427
|
|
|
@@ -1419,15 +1452,26 @@ function renderSubmissions(
|
|
|
1419
1452
|
? v.toFixed(2)
|
|
1420
1453
|
: String(v);
|
|
1421
1454
|
|
|
1455
|
+
// Responsive column hiding — the full table is 106 cols wide. Hide columns
|
|
1456
|
+
// at narrower terminals so rows never overflow/wrap/garble.
|
|
1457
|
+
// w >= 120: all columns (106 cols fits)
|
|
1458
|
+
// w < 120: hide Tokens (saves 11) → 95 cols
|
|
1459
|
+
// w < 100: hide Tokens + Class (saves 14) → 81 cols
|
|
1460
|
+
// w < 90: hide Tokens + Class + Op Ratio, shorten Codename 20→16 → 61 cols
|
|
1461
|
+
const showTokens = w >= 120;
|
|
1462
|
+
const showClass = w >= 100;
|
|
1463
|
+
const showOpRatio = w >= 90;
|
|
1464
|
+
const nameWidth = w < 90 ? 16 : 20;
|
|
1465
|
+
|
|
1422
1466
|
const SH = [
|
|
1423
1467
|
padStart(hdr("#"), 4),
|
|
1424
|
-
padEnd(hdr("Codename"),
|
|
1468
|
+
padEnd(hdr("Codename"), nameWidth),
|
|
1425
1469
|
padEnd(hdr("Platform"), 10),
|
|
1426
1470
|
padEnd(hdr("Win"), 6),
|
|
1427
1471
|
padStart(hdr("Υ Yield"), 9),
|
|
1428
|
-
padEnd(hdr("Op Ratio"), 18),
|
|
1429
|
-
padEnd(hdr("Class"), 12),
|
|
1430
|
-
padStart(hdr("Tokens"), 9),
|
|
1472
|
+
...(showOpRatio ? [padEnd(hdr("Op Ratio"), 18)] : []),
|
|
1473
|
+
...(showClass ? [padEnd(hdr("Class"), 12)] : []),
|
|
1474
|
+
...(showTokens ? [padStart(hdr("Tokens"), 9)] : []),
|
|
1431
1475
|
];
|
|
1432
1476
|
emit(` ${SH.join(" ")}`);
|
|
1433
1477
|
emit(` ${dim("·".repeat(Math.min(w - 4, 100)))}`);
|
|
@@ -1445,10 +1489,10 @@ function renderSubmissions(
|
|
|
1445
1489
|
: idx < 3
|
|
1446
1490
|
? cyan(`#${idx + 1}`)
|
|
1447
1491
|
: `#${idx + 1}`;
|
|
1448
|
-
const nmRaw = trunc(gName(e),
|
|
1492
|
+
const nmRaw = trunc(gName(e), nameWidth);
|
|
1449
1493
|
const nm = isYou
|
|
1450
|
-
? `${c.bgCyan}${c.boldCyan}${padEnd(` ${trunc(gName(e),
|
|
1451
|
-
: padEnd(nmRaw,
|
|
1494
|
+
? `${c.bgCyan}${c.boldCyan}${padEnd(` ${trunc(gName(e), nameWidth - 2)} `, nameWidth)}${c.reset}`
|
|
1495
|
+
: padEnd(nmRaw, nameWidth);
|
|
1452
1496
|
const yv = gYield(e);
|
|
1453
1497
|
const yld = padStart(
|
|
1454
1498
|
yv != null ? (yv > 10000 ? gold(fmtY(yv)) : fmtY(yv)) : "—",
|
|
@@ -1457,9 +1501,17 @@ function renderSubmissions(
|
|
|
1457
1501
|
const cls = padEnd(colorCls(gClass(e)), 12);
|
|
1458
1502
|
const tk = gTokens(e);
|
|
1459
1503
|
const youMark = isYou ? ` ${c.bgCyan}${c.boldCyan}YOU${c.reset}` : "";
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1504
|
+
const cells = [
|
|
1505
|
+
padStart(rk, 4),
|
|
1506
|
+
nm,
|
|
1507
|
+
padEnd(cyan(trunc(gPlat(e), 10)), 10),
|
|
1508
|
+
padEnd(dim(trunc(gWin(e), 6)), 6),
|
|
1509
|
+
yld,
|
|
1510
|
+
...(showOpRatio ? [padEnd(dim(fmtOp(gOpRatio(e))), 18)] : []),
|
|
1511
|
+
...(showClass ? [cls] : []),
|
|
1512
|
+
...(showTokens ? [padStart(tk != null ? fmtTok(tk) : "—", 9)] : []),
|
|
1513
|
+
];
|
|
1514
|
+
emit(` ${cells.join(" ")}${youMark}`);
|
|
1463
1515
|
shown++;
|
|
1464
1516
|
}
|
|
1465
1517
|
// A3-style: if the height budget dropped rows, say so instead of silently truncating.
|
|
@@ -1547,10 +1599,16 @@ function renderBoard(
|
|
|
1547
1599
|
d10: top3((e) => e.dev10x),
|
|
1548
1600
|
};
|
|
1549
1601
|
// Wrap a padded cell in a medal bg-tint (dark fg on the medal color) when its value places top-3.
|
|
1602
|
+
// Theme-aware: monochrome uses bold instead of color backgrounds.
|
|
1550
1603
|
const medal = (map, val, padded) => {
|
|
1551
1604
|
const place = val == null ? null : map.get(val);
|
|
1552
1605
|
if (!place) return padded;
|
|
1553
|
-
|
|
1606
|
+
const bgCode = c.medalBg?.[place];
|
|
1607
|
+
if (bgCode != null) {
|
|
1608
|
+
return `${ESC}48;5;${bgCode}m${c.medalFg}${padded}${c.reset}`;
|
|
1609
|
+
}
|
|
1610
|
+
// Monochrome / no medal bg — use bold
|
|
1611
|
+
return `${c.bold}${padded}${c.reset}`;
|
|
1554
1612
|
};
|
|
1555
1613
|
|
|
1556
1614
|
const BH = [
|
|
@@ -1695,11 +1753,22 @@ function renderConnect(id, codeBuf = "", msg = "") {
|
|
|
1695
1753
|
);
|
|
1696
1754
|
writeln();
|
|
1697
1755
|
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.")}`,
|
|
1756
|
+
` ${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
1757
|
);
|
|
1758
|
+
// Wrap the sign-out help across 2 lines on narrow terminals (the full
|
|
1759
|
+
// sentence is 108 cols — overflows 100-col terminals by 8).
|
|
1760
|
+
if (W() >= 110) {
|
|
1761
|
+
writeln(
|
|
1762
|
+
` ${dim("Signed in on the wrong device, or want a fresh start?")} ${bold("[X]")} ${dim("signs out — next paste provisions a fresh device.")}`,
|
|
1763
|
+
);
|
|
1764
|
+
} else {
|
|
1765
|
+
writeln(
|
|
1766
|
+
` ${dim("Signed in on the wrong device, or want a fresh start?")}`,
|
|
1767
|
+
);
|
|
1768
|
+
writeln(
|
|
1769
|
+
` ${bold("[X]")} ${dim("signs out — next paste provisions a fresh device.")}`,
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1703
1772
|
} else {
|
|
1704
1773
|
writeln(` ${bold("Log in to submit to board")}`);
|
|
1705
1774
|
writeln(` ${hr()}`);
|
|
@@ -1723,7 +1792,13 @@ function renderConnect(id, codeBuf = "", msg = "") {
|
|
|
1723
1792
|
// watch grid), instead of a single platform/window. `platform`/`win` are OPTIONAL focus filters —
|
|
1724
1793
|
// null/'all'/'all-windows' means "everything". Each row is a (platform, window) cascade cell:
|
|
1725
1794
|
// Υ Yield + SNR/Lev/Vel/class. Iterates active platforms (input+output > 0 in any window).
|
|
1726
|
-
|
|
1795
|
+
// Load Watch data once — scan all candidate platforms for active sessions.
|
|
1796
|
+
// Separated from rendering (renderWatchData) so the headless audit can load
|
|
1797
|
+
// once and render across many sizes/themes without re-scanning the filesystem
|
|
1798
|
+
// each frame (each full scan is ~8s across 16 platforms; 16 frames × 8s would
|
|
1799
|
+
// be ~130s of pure re-scan otherwise). Mirrors how Dashboard/Compare/Board
|
|
1800
|
+
// already separate load from render.
|
|
1801
|
+
async function loadWatchData(platform = "all", win = "all-windows") {
|
|
1727
1802
|
// ../tokenpull.mjs — this file lives in presentation/, so "./tokenpull.mjs" resolved
|
|
1728
1803
|
// to presentation/tokenpull.mjs (nonexistent) and threw ERR_MODULE_NOT_FOUND for
|
|
1729
1804
|
// every row. Matches the static `../tokenpull.mjs` import at the top of this file.
|
|
@@ -1748,17 +1823,6 @@ async function renderWatch(platform = "all", win = "all-windows") {
|
|
|
1748
1823
|
];
|
|
1749
1824
|
const platFilter = platform && platform !== "all" ? platform : null;
|
|
1750
1825
|
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
1826
|
// Detect active platforms (any window with input+output > 0), respecting an optional [P] focus.
|
|
1763
1827
|
const candidates = platFilter ? [platFilter] : ALL_PLATFORMS;
|
|
1764
1828
|
const settled = await Promise.allSettled(
|
|
@@ -1778,6 +1842,23 @@ async function renderWatch(platform = "all", win = "all-windows") {
|
|
|
1778
1842
|
(a, b) =>
|
|
1779
1843
|
ALL_PLATFORMS.indexOf(a.platform) - ALL_PLATFORMS.indexOf(b.platform),
|
|
1780
1844
|
);
|
|
1845
|
+
return { active, platFilter, winFilter };
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// Render the Watch tab from pre-loaded data. Sync — no scanning, safe to call
|
|
1849
|
+
// once per terminal size/theme in the audit.
|
|
1850
|
+
function renderWatchData(data) {
|
|
1851
|
+
const { active, platFilter, winFilter } = data;
|
|
1852
|
+
const WINS = winFilter ? [winFilter] : ["7d", "30d", "90d", "all"];
|
|
1853
|
+
const w = W();
|
|
1854
|
+
const budget = H() - 4;
|
|
1855
|
+
let used = 0;
|
|
1856
|
+
const emit = (s = "") => {
|
|
1857
|
+
if (used < budget) {
|
|
1858
|
+
writeln(s);
|
|
1859
|
+
used++;
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1781
1862
|
|
|
1782
1863
|
const scopeLabel =
|
|
1783
1864
|
(platFilter || "all active platforms") +
|
|
@@ -1821,6 +1902,13 @@ async function renderWatch(platform = "all", win = "all-windows") {
|
|
|
1821
1902
|
}
|
|
1822
1903
|
}
|
|
1823
1904
|
|
|
1905
|
+
// Convenience: scan + render in one call (used by --render and --record, which
|
|
1906
|
+
// only render once). The audit uses loadWatchData + renderWatchData separately
|
|
1907
|
+
// to avoid re-scanning across its 16 frames.
|
|
1908
|
+
async function renderWatch(platform = "all", win = "all-windows") {
|
|
1909
|
+
renderWatchData(await loadWatchData(platform, win));
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1824
1912
|
// ── SUBMIT PREVIEW (FIX I1 / FIX E) — see→confirm→send ───────────────────────
|
|
1825
1913
|
// Renders a platform × window grid of what submit_verified WOULD send, using the
|
|
1826
1914
|
// already-loaded dashData. [Enter] confirms → fires submit_verified per platform;
|
|
@@ -2043,6 +2131,12 @@ export async function runTui({
|
|
|
2043
2131
|
platform: initPlatform = "claude",
|
|
2044
2132
|
window: win = "7d",
|
|
2045
2133
|
} = {}) {
|
|
2134
|
+
// Theme selection — --theme <name> sets the color palette (dark/light/high-contrast/monochrome)
|
|
2135
|
+
const themeIdx = process.argv.indexOf("--theme");
|
|
2136
|
+
if (themeIdx !== -1 && process.argv[themeIdx + 1]) {
|
|
2137
|
+
setTheme(process.argv[themeIdx + 1]);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2046
2140
|
// FIX J: platform is mutable — [P] cycles it on Compare + Watch tabs. Was a const
|
|
2047
2141
|
// param (permanently locked to 'claude'); now a let so the user can switch + [S]
|
|
2048
2142
|
// submits the selected platform (needed for Phase 4 multi-platform submit).
|
|
@@ -2055,6 +2149,51 @@ export async function runTui({
|
|
|
2055
2149
|
return;
|
|
2056
2150
|
}
|
|
2057
2151
|
|
|
2152
|
+
// Audit mode — headless review of every tab × every terminal size. Checks
|
|
2153
|
+
// overflow, truncation, dead space, budget, color health, sparkline integrity,
|
|
2154
|
+
// responsive design, visual weight. Supports golden frame snapshots
|
|
2155
|
+
// (--golden-save / --golden-check), SVG export (--svg), and CI mode (--ci
|
|
2156
|
+
// exits non-zero on HIGH severity issues).
|
|
2157
|
+
const ai = process.argv.indexOf("--audit");
|
|
2158
|
+
if (ai !== -1) {
|
|
2159
|
+
let runAudit, formatAuditReport;
|
|
2160
|
+
try {
|
|
2161
|
+
({ runAudit, formatAuditReport } = await import("../dev/tui-audit.mjs"));
|
|
2162
|
+
} catch {
|
|
2163
|
+
console.error("--audit is a development tool, not available in the npm package.");
|
|
2164
|
+
console.error("Clone the repo to use it: https://github.com/SunrisesIllNeverSee/sigrank-mcp");
|
|
2165
|
+
process.exit(1);
|
|
2166
|
+
}
|
|
2167
|
+
const auditData = await runAudit({
|
|
2168
|
+
goldenSave: process.argv.includes("--golden-save"),
|
|
2169
|
+
goldenCheck: process.argv.includes("--golden-check"),
|
|
2170
|
+
svg: process.argv.includes("--svg"),
|
|
2171
|
+
png: process.argv.includes("--png"),
|
|
2172
|
+
ci: process.argv.includes("--ci"),
|
|
2173
|
+
});
|
|
2174
|
+
process.stdout.write(formatAuditReport(auditData) + "\n");
|
|
2175
|
+
// Always exit in audit mode — a data load that timed out may still have a
|
|
2176
|
+
// pending background promise (network fetch / local scan) that would keep
|
|
2177
|
+
// the event loop alive after runTui returns. The audit is headless and must
|
|
2178
|
+
// terminate; unconditional exit guarantees that regardless of ciExitCode.
|
|
2179
|
+
process.exit(auditData.ciExitCode);
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
// Record mode — capture a TUI session as an animated GIF (headless, no TTY needed)
|
|
2183
|
+
const recIdx = process.argv.indexOf("--record");
|
|
2184
|
+
if (recIdx !== -1) {
|
|
2185
|
+
let recordCli;
|
|
2186
|
+
try {
|
|
2187
|
+
({ recordCli } = await import("../dev/tui-record.mjs"));
|
|
2188
|
+
} catch {
|
|
2189
|
+
console.error("--record is a development tool, not available in the npm package.");
|
|
2190
|
+
console.error("Clone the repo to use it: https://github.com/SunrisesIllNeverSee/sigrank-mcp");
|
|
2191
|
+
process.exit(1);
|
|
2192
|
+
}
|
|
2193
|
+
await recordCli(process.argv);
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2058
2197
|
write(ENTER_ALT); // switch to alternate screen — original terminal state preserved on exit
|
|
2059
2198
|
write(CLEAR_SB); // FIXED-WINDOW (2026-06-27): clear scrollback so nothing is above the TUI
|
|
2060
2199
|
write(HIDE);
|
|
@@ -2905,8 +3044,41 @@ export async function runTui({
|
|
|
2905
3044
|
writeln();
|
|
2906
3045
|
}
|
|
2907
3046
|
|
|
3047
|
+
// ── Exports for the headless audit system (tui-audit.mjs) ──────────────────
|
|
3048
|
+
// These expose the render functions + buffer state so the audit module can
|
|
3049
|
+
// render tabs in-process (fast) instead of spawning child processes.
|
|
3050
|
+
export {
|
|
3051
|
+
renderDashboard,
|
|
3052
|
+
renderTrends,
|
|
3053
|
+
renderCompare,
|
|
3054
|
+
renderBoard,
|
|
3055
|
+
renderConnect,
|
|
3056
|
+
renderWatch,
|
|
3057
|
+
renderWatchData,
|
|
3058
|
+
loadWatchData,
|
|
3059
|
+
loadDashboardData,
|
|
3060
|
+
loadCompareData,
|
|
3061
|
+
loadBoardData,
|
|
3062
|
+
startBuffer,
|
|
3063
|
+
stripAnsi,
|
|
3064
|
+
};
|
|
3065
|
+
// _screenBuf is a `let` — exporting it snapshots the value, not the binding.
|
|
3066
|
+
// Expose getter/setter so the audit module can read the live buffer after
|
|
3067
|
+
// startBuffer() + render() populate it, and reset it after capturing.
|
|
3068
|
+
export function _getScreenBuf() {
|
|
3069
|
+
return _screenBuf;
|
|
3070
|
+
}
|
|
3071
|
+
export function _resetBuf() {
|
|
3072
|
+
_screenBuf = null;
|
|
3073
|
+
_footerBuf = null;
|
|
3074
|
+
}
|
|
3075
|
+
|
|
2908
3076
|
// Direct-run entry (e.g. `node tui.mjs --render 0`). Normal launch is via cli.mjs.
|
|
2909
|
-
|
|
3077
|
+
// pathToFileURL resolves relative argv[1] (e.g. `presentation/tui.mjs`) to the
|
|
3078
|
+
// same absolute file:// URL that import.meta.url always carries — without it,
|
|
3079
|
+
// `node presentation/tui.mjs --audit` never matched the guard and runTui never
|
|
3080
|
+
// ran (silent no-op). This is the standard Node ESM main-module detection.
|
|
3081
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
2910
3082
|
runTui().catch((e) => {
|
|
2911
3083
|
console.error(e);
|
|
2912
3084
|
process.exit(1);
|