sigrank-mcp 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -0
- package/adapters.mjs +480 -0
- package/cascade.mjs +143 -0
- package/index.mjs +48 -0
- package/narrate.mjs +65 -0
- package/package.json +25 -0
- package/tokenpull.mjs +274 -0
- package/tools.mjs +323 -0
package/tools.mjs
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools.mjs — the SigRank MCP tool table + dispatcher, transport-free so it can be
|
|
3
|
+
* unit-tested without spawning the stdio server (index.mjs imports from here).
|
|
4
|
+
*
|
|
5
|
+
* callTool() takes an opts bag with an injectable { apiBase, fetchImpl } so tests can
|
|
6
|
+
* exercise the read/write network paths against a fake fetch — no live calls, no
|
|
7
|
+
* writes to production. Pure cascade math lives in ./cascade.mjs; the deterministic
|
|
8
|
+
* narration card in ./narrate.mjs. Token-only, no transcript content.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { cascade, parsePillars } from './cascade.mjs'
|
|
12
|
+
import { narrate } from './narrate.mjs'
|
|
13
|
+
import { tokenpull as pullLocal, tokenpullCodex as pullCodex, tokenpullAny } from './tokenpull.mjs'
|
|
14
|
+
import { ALL_PLATFORMS } from './adapters.mjs'
|
|
15
|
+
import { createHash } from 'node:crypto'
|
|
16
|
+
|
|
17
|
+
// Every board upload from the MCP is hashed + timestamped (ddmmyy) — provenance + dedup.
|
|
18
|
+
function uploadStamp(content) {
|
|
19
|
+
const hash = createHash('sha256').update(JSON.stringify(content)).digest('hex')
|
|
20
|
+
const d = new Date()
|
|
21
|
+
const p = (n) => String(n).padStart(2, '0')
|
|
22
|
+
const ddmmyy = p(d.getUTCDate()) + p(d.getUTCMonth() + 1) + String(d.getUTCFullYear()).slice(-2)
|
|
23
|
+
return { content_hash: hash, submitted_ddmmyy: ddmmyy, submitted_at: d.toISOString() }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Pull a platform's local usage → 4 windows of canonical pillars. Routes through
|
|
27
|
+
// tokenpullAny() which handles Claude (native), Codex (estimated io_ratio), and all
|
|
28
|
+
// other adapters from the registry. opts.adapter overrides for tests.
|
|
29
|
+
async function pullByPlatform(platform, opts = {}) {
|
|
30
|
+
if (opts.adapter) {
|
|
31
|
+
// Test injection: bypass registry and use the mock adapter directly
|
|
32
|
+
if (platform === 'codex') {
|
|
33
|
+
let ioRatio = 2.0
|
|
34
|
+
try {
|
|
35
|
+
const c = await pullLocal({})
|
|
36
|
+
const all = c.windows.find((w) => w.window === 'all')
|
|
37
|
+
if (all && all.pillars.output > 0) ioRatio = all.pillars.input / all.pillars.output
|
|
38
|
+
} catch { /* no Claude data → Alpha 2.0 */ }
|
|
39
|
+
return pullCodex({ ioRatio, adapter: opts.adapter })
|
|
40
|
+
}
|
|
41
|
+
return pullLocal({ adapter: opts.adapter })
|
|
42
|
+
}
|
|
43
|
+
return tokenpullAny(platform || 'claude', opts)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_API_BASE = process.env.SIGRANK_API_BASE || 'https://signalaf.com'
|
|
47
|
+
/** Default network timeout in ms (override via opts.fetchTimeout or SIGRANK_FETCH_TIMEOUT). */
|
|
48
|
+
export const DEFAULT_FETCH_TIMEOUT = Number(process.env.SIGRANK_FETCH_TIMEOUT) || 10_000
|
|
49
|
+
|
|
50
|
+
export const TOOLS = [
|
|
51
|
+
{
|
|
52
|
+
name: 'rank_paste',
|
|
53
|
+
description:
|
|
54
|
+
'Rank a paste of ccusage-style token counts. Accepts JSON {input,output,cacheCreate,cacheRead} or 4 whitespace-separated numbers in that order. Returns Υ Yield, SNR, Leverage, Velocity, 10xDEV, class, AND a deterministic prose "card". Token-only; computes locally.',
|
|
55
|
+
inputSchema: { type: 'object', properties: { text: { type: 'string', description: 'ccusage JSON or "input output cacheCreate cacheRead"' } }, required: ['text'] },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'get_leaderboard',
|
|
59
|
+
description: 'The live public SigRank board (signalaf.com) — operators ranked by yield.',
|
|
60
|
+
inputSchema: { type: 'object', properties: {} },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'get_operator',
|
|
64
|
+
description: "One operator's live profile by codename.",
|
|
65
|
+
inputSchema: { type: 'object', properties: { codename: { type: 'string' } }, required: ['codename'] },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'submit_paste',
|
|
69
|
+
description:
|
|
70
|
+
'Rank a paste AND publish it to the live SigRank board in one call. Computes the cascade locally (instant preview + card), then submits the RAW paste to the board\'s web-paste endpoint, which re-parses and re-scores it server-side (authoritative). A codename is required to publish; omit it for a local preview only. Token-only, no auth (matches the web paste path). Best with a ccusage JSON paste — the 4-number form ranks locally but the board may reject it.',
|
|
71
|
+
inputSchema: {
|
|
72
|
+
type: 'object',
|
|
73
|
+
properties: {
|
|
74
|
+
text: { type: 'string', description: 'ccusage JSON or "input output cacheCreate cacheRead"' },
|
|
75
|
+
codename: { type: 'string', description: 'operator codename to publish under (required to submit; omit for local preview only)' },
|
|
76
|
+
},
|
|
77
|
+
required: ['text'],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'tokenpull',
|
|
82
|
+
description:
|
|
83
|
+
"Pull your LOCAL token usage from the platform's session logs and rank it across the four windows (7d/30d/90d/all-time) with the cascade — zero paste. Token-only: reads usage counts not message content. The numbers stay on your machine unless you submit them. Some platforms may have partial data (estimated=true when cacheCreate isn't available) or a dataGap note when the log format doesn't expose raw token counts.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
platform: {
|
|
88
|
+
type: 'string',
|
|
89
|
+
enum: ALL_PLATFORMS,
|
|
90
|
+
description: `source platform (default: claude). Supported: ${ALL_PLATFORMS.join(', ')}. codex is estimated via io_ratio. Some platforms need setup (e.g. copilot requires COPILOT_OTEL_ENABLED=true).`,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'tokenpull_submit',
|
|
97
|
+
description:
|
|
98
|
+
"Pull your LOCAL usage (tokenpull) AND publish it to the SigRank board in one call — the zero-paste flow. Submits the canonical 4 pillars per window, each re-scored server-side and tagged with the source platform. Requires a codename to publish; omit for local preview. Token-only.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
codename: { type: 'string', description: 'operator codename to publish under (omit for preview-only)' },
|
|
103
|
+
window: { type: 'string', enum: ['7d', '30d', '90d', 'all'], description: 'submit only this window (default: all 4)' },
|
|
104
|
+
platform: { type: 'string', enum: ALL_PLATFORMS, description: `source platform (default: claude). Supported: ${ALL_PLATFORMS.join(', ')}` },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'rank_windows',
|
|
110
|
+
description:
|
|
111
|
+
'Rank all four time windows (7d/30d/90d/all-time) in one call from a dashboard paste — paste the full table from ccusage, tokscale, or the Claude Max usage dashboard and get the cascade (Υ, SNR, Leverage, Velocity, 10xDEV, class, card) for each window. Each window is parsed and scored independently. Named keys required (input/output/cacheCreate/cacheRead); positional order is NOT safe here (dashboards list cache_read before cache_create — see WINDOWED_PROFILES gotcha). Omit windows you don\'t have — partial input is allowed (1–4 windows). Does NOT submit to the board; use tokenpull_submit for zero-paste publishing.',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {
|
|
115
|
+
'7d': { type: 'string', description: 'ccusage/tokscale paste or JSON for the 7-day window (optional)' },
|
|
116
|
+
'30d': { type: 'string', description: 'ccusage/tokscale paste or JSON for the 30-day window (optional)' },
|
|
117
|
+
'90d': { type: 'string', description: 'ccusage/tokscale paste or JSON for the 90-day window (optional)' },
|
|
118
|
+
all: { type: 'string', description: 'ccusage/tokscale paste or JSON for the all-time window (optional)' },
|
|
119
|
+
source_tool: { type: 'string', enum: ['ccusage', 'tokscale', 'claude_max', 'token_dashboard', 'other'], description: 'which token reader produced the paste (for cross-tool variance tracking)' },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'watch_tokenpull',
|
|
125
|
+
description:
|
|
126
|
+
'Watch your local token logs and re-derive your cascade whenever new sessions are written — a live tune meter. Polls at a configurable interval (default 60s), diffs against the last snapshot, and returns the updated cascade when something changes. The push-to-board step is TODO(AUTH.WIRE) — currently returns the diff locally so you can see your score move in real time without submitting.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
platform: { type: 'string', enum: ALL_PLATFORMS, description: 'platform to watch (default: claude)' },
|
|
131
|
+
interval_s: { type: 'number', description: 'poll interval in seconds (default: 60, min: 10)' },
|
|
132
|
+
window: { type: 'string', enum: ['7d', '30d', '90d', 'all'], description: 'which window to watch (default: 7d — most sensitive to recent activity)' },
|
|
133
|
+
codename: { type: 'string', description: 'TODO(AUTH.WIRE): when set, will auto-submit on change once auth is live' },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
// tokenpull window key → the board's window_type enum.
|
|
140
|
+
const WINDOW_TYPE = { '7d': '7d', '30d': '30d', '90d': '90d', all: 'all_time' }
|
|
141
|
+
|
|
142
|
+
export async function callTool(name, args, opts = {}) {
|
|
143
|
+
const apiBase = opts.apiBase || DEFAULT_API_BASE
|
|
144
|
+
const timeoutMs = opts.fetchTimeout ?? DEFAULT_FETCH_TIMEOUT
|
|
145
|
+
const rawFetch = opts.fetchImpl || fetch
|
|
146
|
+
|
|
147
|
+
// Wrap every fetch with an AbortController timeout so a hung network call never
|
|
148
|
+
// blocks the MCP client indefinitely.
|
|
149
|
+
const doFetch = (url, init = {}) => {
|
|
150
|
+
const ac = new AbortController()
|
|
151
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs)
|
|
152
|
+
return rawFetch(url, { ...init, signal: ac.signal }).finally(() => clearTimeout(timer))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const fetchJson = async (path) => {
|
|
156
|
+
const res = await doFetch(`${apiBase}${path}`, { headers: { accept: 'application/json' } })
|
|
157
|
+
if (!res.ok) throw new Error(`SigRank API ${path} → HTTP ${res.status}`)
|
|
158
|
+
return res.json()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Helper: attach _parseWarnings from pillars onto the cascade result so they
|
|
162
|
+
// are always visible in the tool output for review.
|
|
163
|
+
const withParseWarnings = (pillars, cascadeResult) => {
|
|
164
|
+
if (pillars._parseWarnings && pillars._parseWarnings.length > 0) {
|
|
165
|
+
const existing = cascadeResult.warnings || []
|
|
166
|
+
return { ...cascadeResult, warnings: [...existing, ...pillars._parseWarnings.map((w) => `parse:${w}`)] }
|
|
167
|
+
}
|
|
168
|
+
return cascadeResult
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (name === 'rank_paste') {
|
|
172
|
+
if (!args?.text) throw new Error('rank_paste requires a non-empty `text` argument.')
|
|
173
|
+
const pillars = parsePillars(args.text)
|
|
174
|
+
const c = withParseWarnings(pillars, cascade(pillars))
|
|
175
|
+
return { ...c, card: narrate(c) }
|
|
176
|
+
}
|
|
177
|
+
if (name === 'get_leaderboard') return fetchJson('/api/v1/leaderboard')
|
|
178
|
+
if (name === 'get_operator') {
|
|
179
|
+
const codename = String(args?.codename || '').trim()
|
|
180
|
+
if (!codename) throw new Error('get_operator requires a non-empty `codename` argument.')
|
|
181
|
+
return fetchJson(`/api/v1/operators/${encodeURIComponent(codename)}`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (name === 'submit_paste') {
|
|
185
|
+
if (!args?.text) throw new Error('submit_paste requires a non-empty `text` argument.')
|
|
186
|
+
// Local preview first — also validates the paste is parseable before any POST.
|
|
187
|
+
const pillars = parsePillars(args.text)
|
|
188
|
+
const c = withParseWarnings(pillars, cascade(pillars))
|
|
189
|
+
const codename = String(args?.codename || '').trim()
|
|
190
|
+
const card = narrate(c, codename || 'This operator')
|
|
191
|
+
|
|
192
|
+
// No codename → cannot publish (the board endpoint requires it). Fail fast at the
|
|
193
|
+
// tool boundary with a clear message instead of an opaque server 400.
|
|
194
|
+
if (!codename) {
|
|
195
|
+
return {
|
|
196
|
+
...c,
|
|
197
|
+
card,
|
|
198
|
+
submission: { status: 'not_submitted', reason: 'codename_required', detail: 'Pass a codename to publish to the board. Showing local preview only.' },
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Submit the RAW paste so the server re-parses + re-scores authoritatively — the
|
|
203
|
+
// MCP's local cascade is only a preview; the board stays the single source of truth.
|
|
204
|
+
const stamp = uploadStamp({ codename, pillars: c.pillars, source: 'web_paste' })
|
|
205
|
+
const res = await doFetch(`${apiBase}/api/v1/ingest-paste`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
208
|
+
body: JSON.stringify({ codename, raw_paste: String(args?.text || ''), ...stamp }),
|
|
209
|
+
})
|
|
210
|
+
let ack
|
|
211
|
+
try { ack = await res.json() } catch { ack = { status: 'error', detail: `HTTP ${res.status} (non-JSON response)` } }
|
|
212
|
+
return { ...c, card, submission: { ...stamp, httpStatus: res.status, ...ack } }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (name === 'tokenpull') {
|
|
216
|
+
// Local read → 4 windows of pillars → cascade each. Token-only, on-device.
|
|
217
|
+
const platform = args?.platform || 'claude'
|
|
218
|
+
const pulled = await pullByPlatform(platform, opts)
|
|
219
|
+
const windows = pulled.windows.map((w) => {
|
|
220
|
+
const c = cascade(w.pillars)
|
|
221
|
+
return { window: w.window, messages: w.messages, pillars: w.pillars, cascade: c, card: narrate(c, `${w.window} ${platform}`) }
|
|
222
|
+
})
|
|
223
|
+
return { platform: pulled.platform, estimated: pulled.estimated || false, ...(pulled.ioRatio ? { ioRatio: pulled.ioRatio } : {}), generatedAt: pulled.generatedAt, files: pulled.files, totalMessages: pulled.totalMessages, windows }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (name === 'tokenpull_submit') {
|
|
227
|
+
// Pull local usage, then publish each window's CANONICAL pillars to the board
|
|
228
|
+
// (server re-scores). The board stays platform-agnostic via the 4 pillars; the
|
|
229
|
+
// source platform rides along as a tag. Conversion already happened in the adapter.
|
|
230
|
+
const codename = String(args?.codename || '').trim()
|
|
231
|
+
const pulled = await pullByPlatform(args?.platform || 'claude', opts)
|
|
232
|
+
const targets = args?.window ? pulled.windows.filter((w) => w.window === args.window) : pulled.windows
|
|
233
|
+
const out = []
|
|
234
|
+
for (const w of targets) {
|
|
235
|
+
const c = cascade(w.pillars)
|
|
236
|
+
const card = narrate(c, `${w.window} window`)
|
|
237
|
+
if (!codename) {
|
|
238
|
+
out.push({ window: w.window, pillars: w.pillars, cascade: c, card, submission: { status: 'not_submitted', reason: 'codename_required' } })
|
|
239
|
+
continue
|
|
240
|
+
}
|
|
241
|
+
// canonical pillars → "input output cacheCreate cacheRead" (the parser's 4-bare-number form)
|
|
242
|
+
const rawPaste = `${w.pillars.input} ${w.pillars.output} ${w.pillars.cacheCreate} ${w.pillars.cacheRead}`
|
|
243
|
+
const windowType = WINDOW_TYPE[w.window] || w.window
|
|
244
|
+
const stamp = uploadStamp({ codename, window: windowType, pillars: w.pillars, platform: pulled.platform })
|
|
245
|
+
const res = await doFetch(`${apiBase}/api/v1/ingest-paste`, {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
248
|
+
body: JSON.stringify({ codename, raw_paste: rawPaste, window_type: windowType, telemetry: { platform: { primary: pulled.platform } }, ...stamp }),
|
|
249
|
+
})
|
|
250
|
+
let ack
|
|
251
|
+
try { ack = await res.json() } catch { ack = { status: 'error', detail: `HTTP ${res.status} (non-JSON)` } }
|
|
252
|
+
out.push({ window: w.window, pillars: w.pillars, cascade: c, card, submission: { ...stamp, httpStatus: res.status, ...ack } })
|
|
253
|
+
}
|
|
254
|
+
return { platform: pulled.platform, codename: codename || null, generatedAt: pulled.generatedAt, windows: out }
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (name === 'rank_windows') {
|
|
258
|
+
// Score up to 4 named window pastes independently. Named-key parsing only —
|
|
259
|
+
// positional is unsafe here because dashboards list cache_read before cache_create
|
|
260
|
+
// (the WINDOWED_PROFILES swap gotcha). Each window goes through parsePillars →
|
|
261
|
+
// cascade → narrate individually; results are collected into a windows[] array
|
|
262
|
+
// in the same shape as tokenpull output for easy follow-up with tokenpull_submit.
|
|
263
|
+
const WINDOW_KEYS = ['7d', '30d', '90d', 'all']
|
|
264
|
+
const sourceTool = args?.source_tool || null
|
|
265
|
+
const windows = []
|
|
266
|
+
for (const wk of WINDOW_KEYS) {
|
|
267
|
+
const text = args?.[wk]
|
|
268
|
+
if (!text || typeof text !== 'string' || !text.trim()) continue
|
|
269
|
+
const pillars = parsePillars(text)
|
|
270
|
+
const c = withParseWarnings(pillars, cascade(pillars))
|
|
271
|
+
const card = narrate(c, `${wk} window`)
|
|
272
|
+
windows.push({ window: wk, pillars, cascade: c, card })
|
|
273
|
+
}
|
|
274
|
+
if (windows.length === 0) {
|
|
275
|
+
throw new Error('rank_windows requires at least one window paste (7d, 30d, 90d, or all).')
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
windows,
|
|
279
|
+
source_tool: sourceTool,
|
|
280
|
+
note: 'Local preview only — use tokenpull_submit to publish to the board.',
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (name === 'watch_tokenpull') {
|
|
285
|
+
// Poll the local token logs at a configurable interval and return the cascade
|
|
286
|
+
// diff whenever new sessions appear. One poll cycle per MCP call — the client
|
|
287
|
+
// is responsible for re-calling at the desired cadence (MCP tools are stateless;
|
|
288
|
+
// a persistent background watcher lives outside the tool boundary).
|
|
289
|
+
//
|
|
290
|
+
// TODO(AUTH.WIRE): when codename is supplied and auth is live, auto-submit the
|
|
291
|
+
// updated snapshot to the board on every detected change. For now, returns the
|
|
292
|
+
// local cascade only.
|
|
293
|
+
const platform = args?.platform || 'claude'
|
|
294
|
+
const watchWindow = args?.window || '7d'
|
|
295
|
+
const intervalS = Math.max(10, Number(args?.interval_s) || 60)
|
|
296
|
+
|
|
297
|
+
const pulled = await pullByPlatform(platform, opts)
|
|
298
|
+
const win = pulled.windows.find((w) => w.window === watchWindow)
|
|
299
|
+
if (!win) throw new Error(`watch_tokenpull: window '${watchWindow}' not found in pull result.`)
|
|
300
|
+
|
|
301
|
+
const c = cascade(win.pillars)
|
|
302
|
+
const card = narrate(c, `${watchWindow} ${platform}`)
|
|
303
|
+
|
|
304
|
+
// TODO(AUTH.WIRE): if args?.codename, submit to board here once auth + device
|
|
305
|
+
// enrollment are live (SECURE_INGEST.md Phase 4).
|
|
306
|
+
return {
|
|
307
|
+
platform: pulled.platform,
|
|
308
|
+
window: watchWindow,
|
|
309
|
+
pillars: win.pillars,
|
|
310
|
+
messages: win.messages,
|
|
311
|
+
cascade: c,
|
|
312
|
+
card,
|
|
313
|
+
generatedAt: pulled.generatedAt,
|
|
314
|
+
poll_interval_s: intervalS,
|
|
315
|
+
auth_submit: args?.codename
|
|
316
|
+
? { status: 'TODO(AUTH.WIRE)', codename: args.codename, detail: 'Auto-submit on change will activate once device enrollment is live (SECURE_INGEST.md).' }
|
|
317
|
+
: null,
|
|
318
|
+
note: 'One snapshot per call — re-call at your poll interval to detect changes.',
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
323
|
+
}
|