sigrank-mcp 0.6.3 → 0.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.mjs +743 -0
- package/index.mjs +27 -13
- package/package.json +8 -3
package/cli.mjs
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli.mjs — SigRank terminal UI.
|
|
3
|
+
*
|
|
4
|
+
* Commands (no external deps — pure Node.js ANSI escape codes):
|
|
5
|
+
*
|
|
6
|
+
* npx sigrank-mcp board live leaderboard, refreshes every 30s
|
|
7
|
+
* npx sigrank-mcp board --window 7d board for a specific window
|
|
8
|
+
* npx sigrank-mcp board --once print once and exit (no live refresh)
|
|
9
|
+
* npx sigrank-mcp me your cascade across all 4 windows
|
|
10
|
+
* npx sigrank-mcp me --platform amp use a different platform adapter
|
|
11
|
+
* npx sigrank-mcp me --compare raw pillar comparison: ccusage vs tokenpull vs token-dashboard
|
|
12
|
+
* npx sigrank-mcp watch RT tune meter — local cascade, refreshes
|
|
13
|
+
* npx sigrank-mcp watch --window 7d watch a specific window
|
|
14
|
+
*
|
|
15
|
+
* Color palette mirrors the SigRank web dark theme:
|
|
16
|
+
* gold = class TRANSMITTER headline + rank #1
|
|
17
|
+
* cyan = active metrics / your row highlight
|
|
18
|
+
* dim = secondary data, separators
|
|
19
|
+
* red = negative movement / delta
|
|
20
|
+
* green = positive movement
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { callTool, DEFAULT_API_BASE } from './tools.mjs'
|
|
24
|
+
import { execSync } from 'child_process'
|
|
25
|
+
import { existsSync } from 'fs'
|
|
26
|
+
import os from 'os'
|
|
27
|
+
import path from 'path'
|
|
28
|
+
|
|
29
|
+
// ── ANSI helpers (no chalk dep) ────────────────────────────────────────────
|
|
30
|
+
const ESC = '\x1b['
|
|
31
|
+
const c = {
|
|
32
|
+
reset: `${ESC}0m`,
|
|
33
|
+
bold: `${ESC}1m`,
|
|
34
|
+
dim: `${ESC}2m`,
|
|
35
|
+
gold: `${ESC}33m`,
|
|
36
|
+
boldGold: `${ESC}1;33m`,
|
|
37
|
+
cyan: `${ESC}36m`,
|
|
38
|
+
boldCyan: `${ESC}1;36m`,
|
|
39
|
+
green: `${ESC}32m`,
|
|
40
|
+
red: `${ESC}31m`,
|
|
41
|
+
white: `${ESC}97m`,
|
|
42
|
+
boldWhite:`${ESC}1;97m`,
|
|
43
|
+
magenta: `${ESC}35m`,
|
|
44
|
+
blue: `${ESC}34m`,
|
|
45
|
+
}
|
|
46
|
+
const paint = (color, str) => `${color}${str}${c.reset}`
|
|
47
|
+
const bold = (str) => paint(c.bold, str)
|
|
48
|
+
const dim = (str) => paint(c.dim, str)
|
|
49
|
+
const gold = (str) => paint(c.boldGold, str)
|
|
50
|
+
const cyan = (str) => paint(c.boldCyan, str)
|
|
51
|
+
const green = (str) => paint(c.green, str)
|
|
52
|
+
const red = (str) => paint(c.red, str)
|
|
53
|
+
|
|
54
|
+
// ── Class tier → color ─────────────────────────────────────────────────────
|
|
55
|
+
const CLASS_COLOR = {
|
|
56
|
+
TRANSMITTER: (s) => paint(c.boldGold, s),
|
|
57
|
+
'ARCH+': (s) => paint(c.boldCyan, s),
|
|
58
|
+
ARCH: (s) => paint(c.cyan, s),
|
|
59
|
+
POWER: (s) => paint(c.boldWhite, s),
|
|
60
|
+
BASE: (s) => paint(c.white, s),
|
|
61
|
+
SEEKER: (s) => paint(c.magenta, s),
|
|
62
|
+
REFINER: (s) => paint(c.blue, s),
|
|
63
|
+
BEARER: (s) => paint(c.dim, s),
|
|
64
|
+
IGNITER: (s) => paint(c.dim, s),
|
|
65
|
+
}
|
|
66
|
+
const colorClass = (cls) => (CLASS_COLOR[cls] ?? ((s) => s))(cls)
|
|
67
|
+
|
|
68
|
+
// ── Terminal utils ──────────────────────────────────────────────────────────
|
|
69
|
+
const CLEAR_SCREEN = `${ESC}2J${ESC}H`
|
|
70
|
+
const CURSOR_UP = (n) => `${ESC}${n}A`
|
|
71
|
+
const ERASE_LINE = `${ESC}2K`
|
|
72
|
+
const HIDE_CURSOR = `${ESC}?25l`
|
|
73
|
+
const SHOW_CURSOR = `${ESC}?25h`
|
|
74
|
+
const termWidth = () => process.stdout.columns || 80
|
|
75
|
+
const write = (s) => process.stdout.write(s)
|
|
76
|
+
const writeln = (s = '') => process.stdout.write(s + '\n')
|
|
77
|
+
|
|
78
|
+
// Right-pad or truncate to exact width (ANSI-escape-aware via strip helper)
|
|
79
|
+
function stripAnsi(s) { return s.replace(/\x1b\[[0-9;]*m/g, '') }
|
|
80
|
+
function padEnd(s, w) {
|
|
81
|
+
const vis = stripAnsi(s).length
|
|
82
|
+
return vis >= w ? s : s + ' '.repeat(w - vis)
|
|
83
|
+
}
|
|
84
|
+
function padStart(s, w) {
|
|
85
|
+
const vis = stripAnsi(s).length
|
|
86
|
+
return vis >= w ? s : ' '.repeat(w - vis) + s
|
|
87
|
+
}
|
|
88
|
+
function trunc(s, w) {
|
|
89
|
+
const stripped = stripAnsi(s)
|
|
90
|
+
if (stripped.length <= w) return s
|
|
91
|
+
// truncate the raw string, not the escape-aware one — safe for plain strings
|
|
92
|
+
return s.slice(0, w - 1) + '…'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Number formatters ───────────────────────────────────────────────────────
|
|
96
|
+
const fmtYield = (y) => {
|
|
97
|
+
if (y == null) return '—'
|
|
98
|
+
if (y >= 10000) return `${(y / 1000).toFixed(1)}K`
|
|
99
|
+
if (y >= 1000) return `${(y / 1000).toFixed(2)}K`
|
|
100
|
+
return y.toFixed(1)
|
|
101
|
+
}
|
|
102
|
+
const fmtLev = (l) => {
|
|
103
|
+
if (l == null) return '—'
|
|
104
|
+
if (l >= 1000) return `${(l / 1000).toFixed(1)}K`
|
|
105
|
+
return l.toFixed(0)
|
|
106
|
+
}
|
|
107
|
+
const fmtPct = (n) => n != null ? `${(n * 100).toFixed(0)}%` : '—'
|
|
108
|
+
const fmtSNR = (n) => n != null ? `${(n * 100).toFixed(1)}%` : '—'
|
|
109
|
+
const fmtMove = (n) => {
|
|
110
|
+
if (n == null || n === 0) return dim(' —')
|
|
111
|
+
return n > 0 ? green(`+${n}`) : red(`${n}`)
|
|
112
|
+
}
|
|
113
|
+
const fmtTokens = (n) => {
|
|
114
|
+
if (n == null) return '—'
|
|
115
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`
|
|
116
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`
|
|
117
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`
|
|
118
|
+
return String(n)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Header / footer ─────────────────────────────────────────────────────────
|
|
122
|
+
function renderHeader(title, subtitle = '') {
|
|
123
|
+
const w = termWidth()
|
|
124
|
+
const ts = new Date().toLocaleTimeString('en-US', { hour12: false })
|
|
125
|
+
const right = dim(`signalaf.com ${ts}`)
|
|
126
|
+
const rightVis = stripAnsi(right).length
|
|
127
|
+
const leftVis = stripAnsi(title).length
|
|
128
|
+
const gap = Math.max(1, w - leftVis - rightVis)
|
|
129
|
+
writeln()
|
|
130
|
+
writeln(` ${title}${' '.repeat(gap)}${right}`)
|
|
131
|
+
if (subtitle) writeln(` ${dim(subtitle)}`)
|
|
132
|
+
writeln(` ${dim('─'.repeat(w - 4))}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function renderFooter(hint = '') {
|
|
136
|
+
const w = termWidth()
|
|
137
|
+
writeln(` ${dim('─'.repeat(w - 4))}`)
|
|
138
|
+
if (hint) writeln(` ${dim(hint)}`)
|
|
139
|
+
writeln()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── BOARD command ────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
const BOARD_COLS = [
|
|
145
|
+
{ key: 'rank', label: '#', w: 4, align: 'r' },
|
|
146
|
+
{ key: 'codename', label: 'Operator',w: 20, align: 'l' },
|
|
147
|
+
{ key: 'class_tier', label: 'Class', w: 13, align: 'l' },
|
|
148
|
+
{ key: 'signa_rate', label: 'SIGNA', w: 7, align: 'r' },
|
|
149
|
+
{ key: 'compression_ratio', label: 'SNR', w: 6, align: 'r' },
|
|
150
|
+
{ key: 'session_depth', label: 'Depth', w: 6, align: 'r' },
|
|
151
|
+
{ key: 'token_throughput', label: 'Tokens', w: 8, align: 'r' },
|
|
152
|
+
{ key: 'movement_7d', label: '7d Δ', w: 6, align: 'r' },
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
function renderBoardRow(entry, highlight = false) {
|
|
156
|
+
const rank = entry.rank === 1 ? gold(`#${entry.rank}`) : `#${entry.rank}`
|
|
157
|
+
const name = highlight
|
|
158
|
+
? cyan(trunc(entry.codename, 19))
|
|
159
|
+
: trunc(entry.codename, 19)
|
|
160
|
+
const cls = colorClass(entry.class_tier ?? '—')
|
|
161
|
+
const sna = (entry.signa_rate ?? 0).toFixed(1)
|
|
162
|
+
const snr = fmtSNR(entry.compression_ratio)
|
|
163
|
+
const dep = entry.session_depth != null ? entry.session_depth.toFixed(1) : '—'
|
|
164
|
+
const tok = fmtTokens(entry.token_throughput)
|
|
165
|
+
const mv = fmtMove(entry.movement_7d)
|
|
166
|
+
|
|
167
|
+
const cols = [
|
|
168
|
+
padStart(rank, 4),
|
|
169
|
+
padEnd(name, 20),
|
|
170
|
+
padEnd(cls, 13),
|
|
171
|
+
padStart(sna, 7),
|
|
172
|
+
padStart(snr, 6),
|
|
173
|
+
padStart(dep, 6),
|
|
174
|
+
padStart(tok, 8),
|
|
175
|
+
padStart(mv, 6),
|
|
176
|
+
]
|
|
177
|
+
const prefix = highlight ? `${c.boldCyan}▶${c.reset} ` : ' '
|
|
178
|
+
writeln(prefix + cols.join(' '))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function renderBoardHeader(window = '30d') {
|
|
182
|
+
renderHeader(
|
|
183
|
+
`${gold('⊙ SigRank')} ${bold('Leaderboard')}`,
|
|
184
|
+
`window: ${window} · sorted by SIGNA rate · top 25 operators`
|
|
185
|
+
)
|
|
186
|
+
// column headers
|
|
187
|
+
const headers = BOARD_COLS.map(col =>
|
|
188
|
+
col.align === 'r'
|
|
189
|
+
? padStart(dim(col.label), col.w)
|
|
190
|
+
: padEnd(dim(col.label), col.w)
|
|
191
|
+
)
|
|
192
|
+
writeln(` ${headers.join(' ')}`)
|
|
193
|
+
writeln(` ${dim('·'.repeat(termWidth() - 4))}`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function fetchBoard(window = '30d') {
|
|
197
|
+
const res = await fetch(`${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}`, {
|
|
198
|
+
headers: { accept: 'application/json' }
|
|
199
|
+
})
|
|
200
|
+
if (!res.ok) throw new Error(`Board API → HTTP ${res.status}`)
|
|
201
|
+
return res.json()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function runBoard({ window = '30d', once = false, refresh = 30 } = {}) {
|
|
205
|
+
let lines = 0
|
|
206
|
+
|
|
207
|
+
const draw = async () => {
|
|
208
|
+
let data
|
|
209
|
+
try { data = await fetchBoard(window) }
|
|
210
|
+
catch (e) {
|
|
211
|
+
writeln(red(` ✗ Could not reach signalaf.com: ${e.message}`))
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!once && lines > 0) {
|
|
216
|
+
// move cursor up and redraw in-place
|
|
217
|
+
write(CURSOR_UP(lines))
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const out = []
|
|
221
|
+
const push = (s = '') => out.push(s)
|
|
222
|
+
|
|
223
|
+
push()
|
|
224
|
+
const ts = new Date().toLocaleTimeString('en-US', { hour12: false })
|
|
225
|
+
const right = `signalaf.com ${ts}`
|
|
226
|
+
const title = `⊙ SigRank Leaderboard`
|
|
227
|
+
const w = termWidth()
|
|
228
|
+
const gap = Math.max(1, w - 2 - title.length - right.length)
|
|
229
|
+
push(` ${gold('⊙ SigRank')} ${bold('Leaderboard')}${' '.repeat(gap)}${dim(right)}`)
|
|
230
|
+
push(` ${dim(`window: ${data.window ?? window} · ${data.total_operators ?? (data.entries?.length ?? 0)} operators`)}`)
|
|
231
|
+
push(` ${dim('─'.repeat(w - 4))}`)
|
|
232
|
+
|
|
233
|
+
// column header row
|
|
234
|
+
const headers = BOARD_COLS.map(col =>
|
|
235
|
+
col.align === 'r'
|
|
236
|
+
? padStart(dim(col.label), col.w)
|
|
237
|
+
: padEnd(dim(col.label), col.w)
|
|
238
|
+
).join(' ')
|
|
239
|
+
push(` ${headers}`)
|
|
240
|
+
push(` ${dim('·'.repeat(w - 4))}`)
|
|
241
|
+
|
|
242
|
+
const entries = data.entries ?? []
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
const rank = entry.rank === 1 ? gold(`#${entry.rank}`) : `#${entry.rank}`
|
|
245
|
+
const name = trunc(entry.codename ?? '—', 19)
|
|
246
|
+
const cls = colorClass(entry.class_tier ?? '—')
|
|
247
|
+
const sna = (entry.signa_rate ?? 0).toFixed(1)
|
|
248
|
+
const snr = fmtSNR(entry.compression_ratio)
|
|
249
|
+
const dep = entry.session_depth != null ? entry.session_depth.toFixed(1) : '—'
|
|
250
|
+
const tok = fmtTokens(entry.token_throughput)
|
|
251
|
+
const mv = fmtMove(entry.movement_7d)
|
|
252
|
+
const cols = [
|
|
253
|
+
padStart(rank, 4),
|
|
254
|
+
padEnd(name, 20),
|
|
255
|
+
padEnd(cls, 13),
|
|
256
|
+
padStart(sna, 7),
|
|
257
|
+
padStart(snr, 6),
|
|
258
|
+
padStart(dep, 6),
|
|
259
|
+
padStart(tok, 8),
|
|
260
|
+
padStart(mv, 6),
|
|
261
|
+
]
|
|
262
|
+
push(` ${cols.join(' ')}`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
push(` ${dim('─'.repeat(w - 4))}`)
|
|
266
|
+
if (!once) push(` ${dim(`auto-refresh every ${refresh}s · ctrl+c to exit`)}`)
|
|
267
|
+
push()
|
|
268
|
+
|
|
269
|
+
// write all at once to minimize flicker
|
|
270
|
+
const rendered = out.join('\n')
|
|
271
|
+
write(rendered)
|
|
272
|
+
lines = out.length
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!once) write(HIDE_CURSOR)
|
|
276
|
+
try {
|
|
277
|
+
await draw()
|
|
278
|
+
if (!once) {
|
|
279
|
+
const iv = setInterval(draw, refresh * 1000)
|
|
280
|
+
await new Promise((resolve) => {
|
|
281
|
+
process.on('SIGINT', () => { clearInterval(iv); resolve() })
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
} finally {
|
|
285
|
+
if (!once) write(SHOW_CURSOR + '\n')
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── ME command ───────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
async function runMe({ platform = 'claude', compare = false } = {}) {
|
|
292
|
+
if (compare) return runCompare({ platform })
|
|
293
|
+
|
|
294
|
+
write(HIDE_CURSOR)
|
|
295
|
+
writeln(` ${dim('reading local token logs…')}`)
|
|
296
|
+
|
|
297
|
+
let pulled
|
|
298
|
+
try {
|
|
299
|
+
pulled = await callTool('tokenpull', { platform })
|
|
300
|
+
} catch (e) {
|
|
301
|
+
write(SHOW_CURSOR)
|
|
302
|
+
write(CURSOR_UP(1) + ERASE_LINE)
|
|
303
|
+
writeln(red(` ✗ ${e.message}`))
|
|
304
|
+
process.exit(1)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// clear the "reading…" line
|
|
308
|
+
write(CURSOR_UP(1) + ERASE_LINE)
|
|
309
|
+
|
|
310
|
+
const w = termWidth()
|
|
311
|
+
writeln()
|
|
312
|
+
writeln(` ${gold('⊙ SigRank')} ${bold('Your Cascade')} ${dim(`platform: ${pulled.platform ?? platform}`)}`)
|
|
313
|
+
if (pulled.estimated) writeln(` ${dim('⚠ estimated values (cache data unavailable for this platform)')}`)
|
|
314
|
+
writeln(` ${dim('─'.repeat(w - 4))}`)
|
|
315
|
+
|
|
316
|
+
// column headers
|
|
317
|
+
const cols_h = [
|
|
318
|
+
padEnd(dim('Window'), 8),
|
|
319
|
+
padStart(dim('Υ Yield'), 10),
|
|
320
|
+
padStart(dim('SNR'), 7),
|
|
321
|
+
padStart(dim('Leverage'), 9),
|
|
322
|
+
padStart(dim('Velocity'), 9),
|
|
323
|
+
padStart(dim('10xDEV'), 8),
|
|
324
|
+
padStart(dim('Class'), 13),
|
|
325
|
+
padStart(dim('Tokens'), 8),
|
|
326
|
+
]
|
|
327
|
+
writeln(` ${cols_h.join(' ')}`)
|
|
328
|
+
writeln(` ${dim('·'.repeat(w - 4))}`)
|
|
329
|
+
|
|
330
|
+
const windows = pulled.windows ?? []
|
|
331
|
+
for (const win of windows) {
|
|
332
|
+
const cas = win.cascade
|
|
333
|
+
const isAll = win.window === 'all'
|
|
334
|
+
const wLabel = isAll ? bold('all-time') : win.window
|
|
335
|
+
const yVal = cas?.yield != null ? fmtYield(cas.yield) : '—'
|
|
336
|
+
const snrVal = cas?.snr != null ? fmtSNR(cas.snr) : '—'
|
|
337
|
+
const levVal = cas?.leverage != null ? `${fmtLev(cas.leverage)}×` : '—'
|
|
338
|
+
const velVal = cas?.velocity != null ? cas.velocity.toFixed(2) : '—'
|
|
339
|
+
const devVal = cas?.dev10x != null ? cas.dev10x.toFixed(2) : '—'
|
|
340
|
+
const cls = colorClass(cas?.class ?? '—')
|
|
341
|
+
const tok = fmtTokens(win.pillars?.total ?? (
|
|
342
|
+
(win.pillars?.input ?? 0) + (win.pillars?.output ?? 0) +
|
|
343
|
+
(win.pillars?.cacheCreate ?? 0) + (win.pillars?.cacheRead ?? 0)
|
|
344
|
+
))
|
|
345
|
+
|
|
346
|
+
const row = [
|
|
347
|
+
padEnd(wLabel, 8),
|
|
348
|
+
padStart(isAll ? gold(yVal) : yVal, 10),
|
|
349
|
+
padStart(snrVal, 7),
|
|
350
|
+
padStart(levVal, 9),
|
|
351
|
+
padStart(velVal, 9),
|
|
352
|
+
padStart(devVal, 8),
|
|
353
|
+
padEnd(cls, 13),
|
|
354
|
+
padStart(tok, 8),
|
|
355
|
+
]
|
|
356
|
+
writeln(` ${row.join(' ')}`)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
writeln(` ${dim('─'.repeat(w - 4))}`)
|
|
360
|
+
|
|
361
|
+
// card for the best window (all-time if present, else first)
|
|
362
|
+
const best = windows.find(w => w.window === 'all') ?? windows[0]
|
|
363
|
+
if (best?.card) {
|
|
364
|
+
writeln()
|
|
365
|
+
writeln(` ${dim('cascade read:')}`)
|
|
366
|
+
// wrap card text at terminal width
|
|
367
|
+
const cardText = best.card
|
|
368
|
+
const maxW = w - 6
|
|
369
|
+
const words = cardText.split(' ')
|
|
370
|
+
let line = ''
|
|
371
|
+
for (const word of words) {
|
|
372
|
+
if (line.length + word.length + 1 > maxW) {
|
|
373
|
+
writeln(` ${line}`)
|
|
374
|
+
line = word
|
|
375
|
+
} else {
|
|
376
|
+
line = line ? `${line} ${word}` : word
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (line) writeln(` ${line}`)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
writeln()
|
|
383
|
+
|
|
384
|
+
// submit hint
|
|
385
|
+
writeln(` ${dim('to publish:')} ${cyan('npx sigrank-mcp board')} ${dim('after')} ${cyan('tokenpull_submit')} ${dim('via your MCP client')}`)
|
|
386
|
+
writeln()
|
|
387
|
+
write(SHOW_CURSOR)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── COMPARE command ───────────────────────────────────────────────────────────
|
|
391
|
+
// Side-by-side: ccusage (JSON) vs tokenpull vs token-dashboard (SQLite)
|
|
392
|
+
|
|
393
|
+
function ccusagePillars(platform = 'claude') {
|
|
394
|
+
// ccusage <platform> daily --json → sum by window
|
|
395
|
+
try {
|
|
396
|
+
const cmd = platform === 'claude' ? 'ccusage claude daily --json' : `ccusage ${platform} daily --json`
|
|
397
|
+
const raw = execSync(cmd, { timeout: 15000, stdio: ['ignore', 'pipe', 'ignore'] }).toString()
|
|
398
|
+
const data = JSON.parse(raw)
|
|
399
|
+
const rows = data.daily ?? data // ccusage may return {daily:[...]} or [...]
|
|
400
|
+
|
|
401
|
+
const now = Date.now()
|
|
402
|
+
const cutoff = { '7d': 7, '30d': 30, '90d': 90 }
|
|
403
|
+
const result = {}
|
|
404
|
+
|
|
405
|
+
for (const [win, days] of Object.entries(cutoff)) {
|
|
406
|
+
const since = new Date(now - days * 86400000)
|
|
407
|
+
let input = 0, output = 0, cacheCreate = 0, cacheRead = 0
|
|
408
|
+
for (const row of rows) {
|
|
409
|
+
const d = new Date(row.date ?? row.day ?? row.week ?? '1970-01-01')
|
|
410
|
+
if (d >= since) {
|
|
411
|
+
input += row.inputTokens ?? row.input_tokens ?? 0
|
|
412
|
+
output += row.outputTokens ?? row.output_tokens ?? 0
|
|
413
|
+
cacheCreate += row.cacheCreationTokens ?? row.cache_create_tokens ?? 0
|
|
414
|
+
cacheRead += row.cacheReadTokens ?? row.cache_read_tokens ?? 0
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
result[win] = { input, output, cacheCreate, cacheRead }
|
|
418
|
+
}
|
|
419
|
+
// all-time = sum everything
|
|
420
|
+
let input = 0, output = 0, cacheCreate = 0, cacheRead = 0
|
|
421
|
+
for (const row of rows) {
|
|
422
|
+
input += row.inputTokens ?? row.input_tokens ?? 0
|
|
423
|
+
output += row.outputTokens ?? row.output_tokens ?? 0
|
|
424
|
+
cacheCreate += row.cacheCreationTokens ?? row.cache_create_tokens ?? 0
|
|
425
|
+
cacheRead += row.cacheReadTokens ?? row.cache_read_tokens ?? 0
|
|
426
|
+
}
|
|
427
|
+
result['all'] = { input, output, cacheCreate, cacheRead }
|
|
428
|
+
return result
|
|
429
|
+
} catch {
|
|
430
|
+
return null
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function tokenDashPillars() {
|
|
435
|
+
// query token-dashboard SQLite directly
|
|
436
|
+
const dbPath = path.join(os.homedir(), '.claude', 'token-dashboard.db')
|
|
437
|
+
if (!existsSync(dbPath)) return null
|
|
438
|
+
try {
|
|
439
|
+
const claudeFilter = `(model LIKE '%claude%' OR model LIKE '%fable%' OR model LIKE '%sonnet%' OR model LIKE '%opus%' OR model LIKE '%haiku%')`
|
|
440
|
+
const now = new Date()
|
|
441
|
+
const result = {}
|
|
442
|
+
for (const [win, days] of [['7d', 7], ['30d', 30], ['90d', 90]]) {
|
|
443
|
+
const since = new Date(now - days * 86400000).toISOString()
|
|
444
|
+
const cmd = `python3 -c "
|
|
445
|
+
import sqlite3, json
|
|
446
|
+
db = sqlite3.connect('${dbPath}')
|
|
447
|
+
r = db.execute('''SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens+cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages WHERE timestamp>=? AND ${claudeFilter}''',(('${since}',))).fetchone()
|
|
448
|
+
print(json.dumps({'input':r[0] or 0,'output':r[1] or 0,'cacheCreate':r[2] or 0,'cacheRead':r[3] or 0}))
|
|
449
|
+
"`
|
|
450
|
+
const raw = execSync(cmd, { timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
|
|
451
|
+
result[win] = JSON.parse(raw)
|
|
452
|
+
}
|
|
453
|
+
// all-time
|
|
454
|
+
const cmd = `python3 -c "
|
|
455
|
+
import sqlite3, json
|
|
456
|
+
db = sqlite3.connect('${dbPath}')
|
|
457
|
+
r = db.execute('SELECT SUM(input_tokens),SUM(output_tokens),SUM(cache_create_5m_tokens+cache_create_1h_tokens),SUM(cache_read_tokens) FROM messages WHERE ${claudeFilter}').fetchone()
|
|
458
|
+
print(json.dumps({'input':r[0] or 0,'output':r[1] or 0,'cacheCreate':r[2] or 0,'cacheRead':r[3] or 0}))
|
|
459
|
+
"`
|
|
460
|
+
const raw = execSync(cmd, { timeout: 10000, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim()
|
|
461
|
+
result['all'] = JSON.parse(raw)
|
|
462
|
+
return result
|
|
463
|
+
} catch {
|
|
464
|
+
return null
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function fmtDelta(a, b) {
|
|
469
|
+
if (a == null || b == null) return dim(' —')
|
|
470
|
+
const d = b - a
|
|
471
|
+
if (d === 0) return dim(' =')
|
|
472
|
+
const pct = a !== 0 ? `${d > 0 ? '+' : ''}${((d / a) * 100).toFixed(1)}%` : ''
|
|
473
|
+
const abs = `${d > 0 ? '+' : ''}${fmtTokens(Math.abs(d))}`
|
|
474
|
+
const label = `${abs} ${pct}`
|
|
475
|
+
return d > 0 ? green(label) : red(label)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function runCompare({ platform = 'claude' } = {}) {
|
|
479
|
+
write(HIDE_CURSOR)
|
|
480
|
+
writeln(` ${dim('reading ccusage…')}`)
|
|
481
|
+
const ccPillars = ccusagePillars(platform)
|
|
482
|
+
write(CURSOR_UP(1) + ERASE_LINE)
|
|
483
|
+
|
|
484
|
+
writeln(` ${dim('reading tokenpull…')}`)
|
|
485
|
+
let tpData
|
|
486
|
+
try { tpData = await callTool('tokenpull', { platform }) } catch { tpData = null }
|
|
487
|
+
write(CURSOR_UP(1) + ERASE_LINE)
|
|
488
|
+
|
|
489
|
+
writeln(` ${dim('reading token-dashboard…')}`)
|
|
490
|
+
const tdPillars = tokenDashPillars()
|
|
491
|
+
write(CURSOR_UP(1) + ERASE_LINE)
|
|
492
|
+
|
|
493
|
+
const w = termWidth()
|
|
494
|
+
const WINS = ['7d', '30d', '90d', 'all']
|
|
495
|
+
const WIN_LABEL = { '7d': '7d', '30d': '30d', '90d': '90d', 'all': 'all-time' }
|
|
496
|
+
|
|
497
|
+
// build tokenpull pillar lookup
|
|
498
|
+
const tpPillars = {}
|
|
499
|
+
for (const win of (tpData?.windows ?? [])) {
|
|
500
|
+
tpPillars[win.window] = win.pillars
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
writeln()
|
|
504
|
+
writeln(` ${gold('⊙ SigRank')} ${bold('Source Comparison')} ${dim(`platform: ${platform} · claude tokens only`)}`)
|
|
505
|
+
writeln(` ${dim('─'.repeat(w - 4))}`)
|
|
506
|
+
|
|
507
|
+
const PILLARS = [
|
|
508
|
+
{ key: 'input', label: 'Input' },
|
|
509
|
+
{ key: 'output', label: 'Output' },
|
|
510
|
+
{ key: 'cacheCreate', label: 'Cache Create' },
|
|
511
|
+
{ key: 'cacheRead', label: 'Cache Read' },
|
|
512
|
+
]
|
|
513
|
+
|
|
514
|
+
for (const { key, label } of PILLARS) {
|
|
515
|
+
writeln()
|
|
516
|
+
writeln(` ${bold(label)}`)
|
|
517
|
+
|
|
518
|
+
// header row
|
|
519
|
+
const hcols = [
|
|
520
|
+
padEnd(dim('Source'), 16),
|
|
521
|
+
...WINS.map(win => padStart(dim(WIN_LABEL[win]), 14))
|
|
522
|
+
]
|
|
523
|
+
writeln(` ${hcols.join(' ')}`)
|
|
524
|
+
writeln(` ${dim('·'.repeat(Math.min(w - 4, 16 + WINS.length * 16)))}`)
|
|
525
|
+
|
|
526
|
+
// ccusage row
|
|
527
|
+
if (ccPillars) {
|
|
528
|
+
const cols = [padEnd(cyan('ccusage'), 16), ...WINS.map(win => padStart(fmtTokens(ccPillars[win]?.[key] ?? 0), 14))]
|
|
529
|
+
writeln(` ${cols.join(' ')}`)
|
|
530
|
+
} else {
|
|
531
|
+
writeln(` ${padEnd(dim('ccusage'), 16)} ${dim('not found')}`)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// tokenpull row
|
|
535
|
+
if (tpData) {
|
|
536
|
+
const cols = [padEnd(cyan('tokenpull'), 16), ...WINS.map(win => padStart(fmtTokens(tpPillars[win]?.[key] ?? 0), 14))]
|
|
537
|
+
writeln(` ${cols.join(' ')}`)
|
|
538
|
+
} else {
|
|
539
|
+
writeln(` ${padEnd(dim('tokenpull'), 16)} ${dim('not found')}`)
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// token-dashboard row
|
|
543
|
+
if (tdPillars) {
|
|
544
|
+
const cols = [padEnd(cyan('token-dash'), 16), ...WINS.map(win => padStart(fmtTokens(tdPillars[win]?.[key] ?? 0), 14))]
|
|
545
|
+
writeln(` ${cols.join(' ')}`)
|
|
546
|
+
} else {
|
|
547
|
+
writeln(` ${padEnd(dim('token-dash'), 16)} ${dim('not found — run: python3 ~/token-dashboard/cli.py scan')}`)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// delta row: tokenpull vs token-dash (the two most comparable)
|
|
551
|
+
if (tpData && tdPillars) {
|
|
552
|
+
const cols = [
|
|
553
|
+
padEnd(dim('Δ tp→td'), 16),
|
|
554
|
+
...WINS.map(win => {
|
|
555
|
+
const a = tpPillars[win]?.[key] ?? 0
|
|
556
|
+
const b = tdPillars[win]?.[key] ?? 0
|
|
557
|
+
return padStart(fmtDelta(a, b), 14)
|
|
558
|
+
})
|
|
559
|
+
]
|
|
560
|
+
writeln(` ${cols.join(' ')}`)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// SigRank cascade section
|
|
565
|
+
writeln()
|
|
566
|
+
writeln(` ${dim('─'.repeat(w - 4))}`)
|
|
567
|
+
writeln(` ${bold('SigRank Cascade')} ${dim('(tokenpull → cascade math)')}`)
|
|
568
|
+
writeln()
|
|
569
|
+
|
|
570
|
+
const CASCADE_ROWS = [
|
|
571
|
+
{ key: 'yield', label: 'Υ Yield', fmt: v => fmtYield(v) },
|
|
572
|
+
{ key: 'snr', label: 'SNR', fmt: v => fmtSNR(v) },
|
|
573
|
+
{ key: 'leverage', label: 'Leverage', fmt: v => `${fmtLev(v)}×` },
|
|
574
|
+
{ key: 'velocity', label: 'Velocity', fmt: v => v.toFixed(2) },
|
|
575
|
+
{ key: 'dev10x', label: '10xDEV', fmt: v => v.toFixed(2) },
|
|
576
|
+
{ key: 'class', label: 'Class', fmt: v => colorClass(v) },
|
|
577
|
+
]
|
|
578
|
+
|
|
579
|
+
const hcols = [padEnd(dim('Metric'), 12), ...WINS.map(win => padStart(dim(WIN_LABEL[win]), 12))]
|
|
580
|
+
writeln(` ${hcols.join(' ')}`)
|
|
581
|
+
writeln(` ${dim('·'.repeat(Math.min(w - 4, 12 + WINS.length * 14)))}`)
|
|
582
|
+
|
|
583
|
+
for (const { key, label, fmt } of CASCADE_ROWS) {
|
|
584
|
+
const cols = [
|
|
585
|
+
padEnd(dim(label), 12),
|
|
586
|
+
...WINS.map(win => {
|
|
587
|
+
const cas = tpPillars[win] ? (tpData?.windows?.find(w => w.window === win)?.cascade) : null
|
|
588
|
+
const v = cas?.[key]
|
|
589
|
+
return padStart(v != null ? fmt(v) : dim('—'), 12)
|
|
590
|
+
})
|
|
591
|
+
]
|
|
592
|
+
writeln(` ${cols.join(' ')}`)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// estimated rank hint
|
|
596
|
+
writeln()
|
|
597
|
+
writeln(` ${dim('─'.repeat(w - 4))}`)
|
|
598
|
+
writeln(` ${dim('note: token-dash has longer history (SQLite); tokenpull reads live JSONL only')}`)
|
|
599
|
+
writeln(` ${dim('note: ccusage = primary pillar source (all agents); tokenpull = claude-only cascade input')}`)
|
|
600
|
+
writeln()
|
|
601
|
+
write(SHOW_CURSOR)
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ── WATCH command ─────────────────────────────────────────────────────────────
|
|
605
|
+
|
|
606
|
+
async function runWatch({ platform = 'claude', window: win = '7d', refresh = 30 } = {}) {
|
|
607
|
+
let prev = null
|
|
608
|
+
let lines = 0
|
|
609
|
+
|
|
610
|
+
write(HIDE_CURSOR)
|
|
611
|
+
|
|
612
|
+
const draw = async () => {
|
|
613
|
+
let result
|
|
614
|
+
try {
|
|
615
|
+
result = await callTool('watch_tokenpull', { platform, window: win })
|
|
616
|
+
} catch (e) {
|
|
617
|
+
writeln(red(` ✗ ${e.message}`))
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const cas = result.cascade
|
|
622
|
+
const changed = prev !== null && prev !== cas?.yield
|
|
623
|
+
|
|
624
|
+
if (lines > 0) write(CURSOR_UP(lines))
|
|
625
|
+
|
|
626
|
+
const out = []
|
|
627
|
+
const push = (s = '') => out.push(s)
|
|
628
|
+
const w = termWidth()
|
|
629
|
+
const ts = new Date().toLocaleTimeString('en-US', { hour12: false })
|
|
630
|
+
|
|
631
|
+
push()
|
|
632
|
+
push(` ${gold('⊙ SigRank')} ${bold('Watch')} ${dim(`${platform} · window: ${win} · ${ts}`)}`)
|
|
633
|
+
push(` ${dim('─'.repeat(w - 4))}`)
|
|
634
|
+
push()
|
|
635
|
+
|
|
636
|
+
const yDisplay = cas?.yield != null ? fmtYield(cas.yield) : '—'
|
|
637
|
+
const indicator = changed ? green(' ▲ updated') : dim(' · no change')
|
|
638
|
+
|
|
639
|
+
push(` ${bold('Υ Yield')} ${cas?.yield != null ? gold(yDisplay) : '—'}${indicator}`)
|
|
640
|
+
push(` ${bold('SNR')} ${fmtSNR(cas?.snr)}`)
|
|
641
|
+
push(` ${bold('Leverage')} ${cas?.leverage != null ? `${fmtLev(cas.leverage)}×` : '—'}`)
|
|
642
|
+
push(` ${bold('Velocity')} ${cas?.velocity != null ? cas.velocity.toFixed(2) : '—'}`)
|
|
643
|
+
push(` ${bold('10xDEV')} ${cas?.dev10x != null ? cas.dev10x.toFixed(2) : '—'}`)
|
|
644
|
+
push(` ${bold('Class')} ${colorClass(cas?.class ?? '—')}`)
|
|
645
|
+
push()
|
|
646
|
+
push(` ${dim('─'.repeat(w - 4))}`)
|
|
647
|
+
push(` ${dim(`polling every ${refresh}s · tokens stay on your machine · ctrl+c to exit`)}`)
|
|
648
|
+
push()
|
|
649
|
+
|
|
650
|
+
write(out.join('\n'))
|
|
651
|
+
lines = out.length
|
|
652
|
+
prev = cas?.yield ?? null
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
await draw()
|
|
657
|
+
const iv = setInterval(draw, refresh * 1000)
|
|
658
|
+
await new Promise((resolve) => {
|
|
659
|
+
process.on('SIGINT', () => { clearInterval(iv); resolve() })
|
|
660
|
+
})
|
|
661
|
+
} finally {
|
|
662
|
+
write(SHOW_CURSOR + '\n')
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// ── HELP ─────────────────────────────────────────────────────────────────────
|
|
667
|
+
|
|
668
|
+
function showHelp() {
|
|
669
|
+
writeln()
|
|
670
|
+
writeln(` ${gold('⊙ SigRank')} ${bold('CLI')} ${dim('v0.6.5')}`)
|
|
671
|
+
writeln()
|
|
672
|
+
writeln(` ${bold('Commands')}`)
|
|
673
|
+
writeln(` ${cyan('board')} live leaderboard (refreshes every 30s)`)
|
|
674
|
+
writeln(` ${cyan('board --window 7d')} board for a specific window (7d, 30d, 90d, all_time)`)
|
|
675
|
+
writeln(` ${cyan('board --once')} print once and exit`)
|
|
676
|
+
writeln(` ${cyan('me')} your cascade across all 4 time windows`)
|
|
677
|
+
writeln(` ${cyan('me --platform amp')} use a different platform adapter`)
|
|
678
|
+
writeln(` ${cyan('me --compare')} raw pillar comparison: ccusage vs tokenpull vs token-dashboard`)
|
|
679
|
+
writeln(` ${cyan('watch')} live tune meter — re-reads local logs every 30s`)
|
|
680
|
+
writeln(` ${cyan('watch --window 7d')} watch a specific window`)
|
|
681
|
+
writeln()
|
|
682
|
+
writeln(` ${bold('Options')}`)
|
|
683
|
+
writeln(` ${dim('--window')} 7d · 30d · 90d · all_time (default: 30d for board, 7d for watch)`)
|
|
684
|
+
writeln(` ${dim('--platform')} claude · codex · amp · gemini · opencode · goose · …`)
|
|
685
|
+
writeln(` ${dim('--refresh')} poll interval in seconds (default: 30)`)
|
|
686
|
+
writeln(` ${dim('--once')} print once and exit (board only)`)
|
|
687
|
+
writeln()
|
|
688
|
+
writeln(` ${bold('MCP server mode')} (default when no command given)`)
|
|
689
|
+
writeln(` ${dim('npx sigrank-mcp')} starts the MCP server on stdio`)
|
|
690
|
+
writeln()
|
|
691
|
+
writeln(` ${bold('Examples')}`)
|
|
692
|
+
writeln(` ${dim('npx sigrank-mcp board')}`)
|
|
693
|
+
writeln(` ${dim('npx sigrank-mcp me')}`)
|
|
694
|
+
writeln(` ${dim('npx sigrank-mcp watch --window 7d --refresh 60')}`)
|
|
695
|
+
writeln(` ${dim('npx sigrank-mcp board --window all_time --once')}`)
|
|
696
|
+
writeln()
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// ── ENTRY POINT ───────────────────────────────────────────────────────────────
|
|
700
|
+
|
|
701
|
+
export async function runCli(argv) {
|
|
702
|
+
const args = argv.slice(2) // strip 'node' + script path
|
|
703
|
+
const cmd = args[0]
|
|
704
|
+
|
|
705
|
+
// parse --key value flags
|
|
706
|
+
const flags = {}
|
|
707
|
+
for (let i = 1; i < args.length; i++) {
|
|
708
|
+
if (args[i].startsWith('--')) {
|
|
709
|
+
const key = args[i].slice(2)
|
|
710
|
+
const val = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true
|
|
711
|
+
flags[key] = val
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
if (cmd === 'board') {
|
|
717
|
+
await runBoard({
|
|
718
|
+
window: flags.window ?? '30d',
|
|
719
|
+
once: flags.once === true || flags.once === 'true',
|
|
720
|
+
refresh: Number(flags.refresh) || 30,
|
|
721
|
+
})
|
|
722
|
+
} else if (cmd === 'me') {
|
|
723
|
+
await runMe({ platform: flags.platform ?? 'claude', compare: flags.compare === true || flags.compare === 'true' })
|
|
724
|
+
} else if (cmd === 'watch') {
|
|
725
|
+
await runWatch({
|
|
726
|
+
platform: flags.platform ?? 'claude',
|
|
727
|
+
window: flags.window ?? '7d',
|
|
728
|
+
refresh: Number(flags.refresh) || 30,
|
|
729
|
+
})
|
|
730
|
+
} else if (cmd === '--help' || cmd === '-h' || cmd === 'help') {
|
|
731
|
+
showHelp()
|
|
732
|
+
} else if (cmd === '--version' || cmd === '-v') {
|
|
733
|
+
writeln('0.6.5')
|
|
734
|
+
} else {
|
|
735
|
+
// unknown command: show help
|
|
736
|
+
showHelp()
|
|
737
|
+
}
|
|
738
|
+
} catch (e) {
|
|
739
|
+
write(SHOW_CURSOR)
|
|
740
|
+
writeln(red(`\n ✗ ${e.message}`))
|
|
741
|
+
process.exit(1)
|
|
742
|
+
}
|
|
743
|
+
}
|
package/index.mjs
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* SigRank MCP server
|
|
4
|
-
* any client (Claude Code, Cursor, …) can call. Token-only, Claude/ccusage-first.
|
|
3
|
+
* SigRank MCP server + CLI entry point.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
5
|
+
* CLI mode — triggered when any command arg is passed:
|
|
6
|
+
* npx sigrank-mcp board live leaderboard (auto-refresh)
|
|
7
|
+
* npx sigrank-mcp me your local cascade across 4 windows
|
|
8
|
+
* npx sigrank-mcp watch RT tune meter, re-reads local logs
|
|
9
|
+
* npx sigrank-mcp --help full command reference
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* MCP server mode — triggered when no args (the default for MCP clients):
|
|
12
|
+
* npx sigrank-mcp starts the MCP stdio server
|
|
13
|
+
*
|
|
14
|
+
* Tools exposed in MCP mode (see ./tools.mjs):
|
|
15
|
+
* rank_paste · get_leaderboard · get_operator · submit_paste
|
|
16
|
+
* tokenpull · tokenpull_submit · rank_windows · watch_tokenpull
|
|
17
|
+
*
|
|
18
|
+
* Pure cascade math: ./cascade.mjs | narration card: ./narrate.mjs
|
|
19
|
+
* Tool table + dispatcher: ./tools.mjs | Terminal UI: ./cli.mjs
|
|
20
|
+
* Token-only — no transcript content, no auth.
|
|
15
21
|
*/
|
|
16
22
|
|
|
17
23
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
18
24
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
19
25
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
20
26
|
import { TOOLS, callTool } from './tools.mjs'
|
|
27
|
+
import { runCli } from './cli.mjs'
|
|
21
28
|
|
|
22
29
|
// Prevent silent crashes — log to stderr (MCP clients read stdout; stderr is safe for
|
|
23
30
|
// diagnostics). The process exits so the client can respawn with a clean slate rather
|
|
@@ -31,8 +38,8 @@ process.on('unhandledRejection', (reason) => {
|
|
|
31
38
|
process.exit(1)
|
|
32
39
|
})
|
|
33
40
|
|
|
34
|
-
async function
|
|
35
|
-
const server = new Server({ name: 'sigrank', version: '0.6.
|
|
41
|
+
async function startMcpServer() {
|
|
42
|
+
const server = new Server({ name: 'sigrank', version: '0.6.4' }, { capabilities: { tools: {} } })
|
|
36
43
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
37
44
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
38
45
|
try {
|
|
@@ -45,4 +52,11 @@ async function main() {
|
|
|
45
52
|
await server.connect(new StdioServerTransport())
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
// Route: CLI commands → terminal UI; no args → MCP server
|
|
56
|
+
const cliArgs = process.argv.slice(2)
|
|
57
|
+
const CLI_COMMANDS = new Set(['board', 'me', 'watch', 'help', '--help', '-h', '--version', '-v'])
|
|
58
|
+
if (cliArgs.length > 0 && (CLI_COMMANDS.has(cliArgs[0]) || cliArgs[0].startsWith('--'))) {
|
|
59
|
+
runCli(process.argv)
|
|
60
|
+
} else {
|
|
61
|
+
startMcpServer()
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"bin": {
|
|
6
|
+
"bin": {
|
|
7
|
+
"sigrank-mcp": "index.mjs"
|
|
8
|
+
},
|
|
7
9
|
"main": "index.mjs",
|
|
8
|
-
"engines": {
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18"
|
|
12
|
+
},
|
|
9
13
|
"scripts": {
|
|
10
14
|
"start": "node index.mjs",
|
|
11
15
|
"test": "node test.mjs"
|
|
12
16
|
},
|
|
13
17
|
"files": [
|
|
14
18
|
"index.mjs",
|
|
19
|
+
"cli.mjs",
|
|
15
20
|
"tools.mjs",
|
|
16
21
|
"cascade.mjs",
|
|
17
22
|
"narrate.mjs",
|