sigrank 1.0.44 → 1.0.46

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/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.46
4
+
5
+ ### Security / privacy
6
+
7
+ * `enroll`: device label no longer defaults to the OS hostname; default is `sigrank <version> · <platform>` (opt-in override: `device_label` arg or `SIGRANK_DEVICE_LABEL`)
8
+ * `enroll` (MCP): refuses to re-bind an already-enrolled device; re-enroll from the CLI
9
+ * `submit_verified`: requires `confirm: true` to publish (not for `dry_run`); CLI/TUI pass it after their own consent
10
+ * `tokscale_developer_profile`: returns `workspace_count` only, no workspace/project names
11
+ * `validateApiBase`: https required except loopback hosts
12
+ * Pinned `ccusage` 20.0.30 and `tokscale` 4.18.0 exactly; removed stale `tokscale@latest` comment
13
+ * Proxy: rejects non-loopback Host headers and cross-site browser requests (403)
14
+ * Keystore: identity backups chmod 0600
15
+ * README / privacy-model corrected to list exactly what leaves the machine
16
+
17
+ ## [1.0.44](https://github.com/SunrisesIllNeverSee/sigrank-mcp/releases/tag/v1.0.44) (2026-10-05)
18
+
19
+ ### Features
20
+
21
+ * MCP: explicit opt-in support for protocol revision 2026-07-28 via `serveStdio` — a `server/discover` probe with the modern `_meta` envelope pins the connection to the modern era; `initialize` openings keep the 2025-era protocol unchanged (dual-era serving, `legacy: 'serve'`)
22
+ * Docs: `docs/PROTOCOL-COMPATIBILITY.md` — served eras, negotiation rules, host/client matrix
23
+
24
+ ### Tests
25
+
26
+ * stdio characterization suite 15 → 24: 9 modern-era tests (discover negotiation, era parity, envelope-claim requirement, `-32022` late-initialize, probe→initialize fallback, modern stdout purity + EOF)
27
+
28
+ ## [1.0.43](https://github.com/SunrisesIllNeverSee/sigrank-mcp/releases/tag/v1.0.43) (2026-10-05)
29
+
30
+ ### Breaking Changes
31
+
32
+ * Node.js >= 20 required (was >= 18)
33
+
34
+ ### Features
35
+
36
+ * MCP: migrated `@modelcontextprotocol/sdk` v1 → official v2 packages (`@modelcontextprotocol/server` 2.3.0); zero v1 imports remain
37
+ * `sigrank standard --json`: `sigrank/0.1-draft` now correctly labeled `legacy_alias` under TTEOP authority (`tteop-spec@0.1.5-draft`)
38
+
39
+ ### Fixes
40
+
41
+ * Release workflow: explicit releases only — no auto-publish after CI; real npm OIDC trusted publishing (dead NPM_TOKEN path removed); `--dry-run` + collision pre-gates; `--provenance`
42
+
43
+ ### Tests
44
+
45
+ * 15-test SDK-agnostic stdio behavior freeze locked identical across v1 → v2 (tools/prompts/resources, error classes, stdout purity, routing, EOF) on Node 20/22/26
46
+
3
47
  ## [1.0.37](https://github.com/SunrisesIllNeverSee/sigrank-mcp/releases/tag/v1.0.37) (2026-09-08)
4
48
 
5
49
  ### Fixes
@@ -1,6 +1,6 @@
1
1
  # SigRank MCP Server
2
2
 
3
- > Operator leaderboard measuring users, not models — 24 tools, TUI, signed submit, privacy-first.
3
+ > Operator leaderboard measuring users, not models — 25 tools, TUI, signed submit, privacy-first.
4
4
 
5
5
  SigRank is the first leaderboard that ranks **AI operators** — the humans — by token cascade efficiency, not the models. The yield metric (Υ = cache_read × output / input²) turns raw token telemetry into a competitive ranking on [signalaf.com](https://signalaf.com).
6
6
 
@@ -64,7 +64,7 @@ npx sigrank diagnose # Diagnose cascade inefficiencies
64
64
  npx sigrank improve # Get improvement suggestions
65
65
  ```
66
66
 
67
- ## The 24 MCP Tools
67
+ ## The 25 MCP Tools
68
68
 
69
69
  ### Read-only — pure math (no auth, no filesystem)
70
70
 
@@ -81,39 +81,40 @@ npx sigrank improve # Get improvement suggestions
81
81
 
82
82
  | # | Tool | What it does |
83
83
  |---|------|-------------|
84
- | 7 | `get_leaderboard` | Fetch the live public leaderboard from signalaf.com. Returns all ranked operators sorted by yield, with codename, Υ, leverage, velocity, class, and rank position. |
85
- | 8 | `get_operator` | Get any operator's full profile by codename. Returns detailed metrics + per-window breakdowns (7d/30d/90d/all-time) with the four token pillars per window. |
86
- | 9 | `discover_peers` | Find mentors, peers, and complementary operators for your operator on the leaderboard. |
84
+ | 7 | `get_sigrank_standard_record` | Build the portable `sigrank/0.1-draft` compatibility record (legacy alias under TTEOP authority) from token telemetry. Computes the canonical cascade locally — no network. |
85
+ | 8 | `get_leaderboard` | Fetch the live public leaderboard from signalaf.com. Returns all ranked operators sorted by yield, with codename, Υ, leverage, velocity, class, and rank position. |
86
+ | 9 | `get_operator` | Get any operator's full profile by codename. Returns detailed metrics + per-window breakdowns (7d/30d/90d/all-time) with the four token pillars per window. |
87
+ | 10 | `discover_peers` | Find mentors, peers, and complementary operators for your operator on the leaderboard. |
87
88
 
88
89
  ### Read-only — local filesystem (no auth, no network)
89
90
 
90
91
  | # | Tool | What it does |
91
92
  |---|------|-------------|
92
- | 10 | `tokenpull` | Pull your LOCAL token usage from session logs and rank it across all four windows — zero paste. Reads token counts, not message content. Numbers stay on your machine. 17 platform adapters. |
93
- | 11 | `tokenpull_compare` | Pull token usage from ALL four local sources in parallel (JSONL canon, ccusage CLI, token-dashboard SQLite, tokscale) and compare side-by-side with delta % vs baseline. |
94
- | 12 | `watch_tokenpull` | One poll per call — pulls local token logs and returns current cascade for the watched window. Re-call at your desired cadence to monitor. With `submit:true`, signs + publishes (rate-limited to once per 5 min). |
93
+ | 11 | `tokenpull` | Pull your LOCAL token usage from session logs and rank it across all four windows — zero paste. Reads token counts, not message content. Numbers stay on your machine. 17 platform adapters. |
94
+ | 12 | `tokenpull_compare` | Pull token usage from ALL four local sources in parallel (JSONL canon, ccusage CLI, token-dashboard SQLite, tokscale) and compare side-by-side with delta % vs baseline. |
95
+ | 13 | `watch_tokenpull` | One poll per call — pulls local token logs and returns current cascade for the watched window. Re-call at your desired cadence to monitor. With `submit:true`, signs + publishes (rate-limited to once per 5 min). |
95
96
 
96
97
  ### Write — submit to board (needs enrolled identity)
97
98
 
98
99
  | # | Tool | What it does |
99
100
  |---|------|-------------|
100
- | 13 | `submit_paste` | Ranks a paste of token counts AND publishes to the live board in one call. Computes local preview, then submits to server for authoritative scoring. Returns both local + server results. |
101
- | 14 | `tokenpull_submit` | Pull your LOCAL token usage AND publish to the board in one call — the zero-paste flow. Reads pillars per window, computes cascade, submits each window server-side. Token-only, no prompt content. |
102
- | 15 | `submit_verified` | Publish your LOCAL token runs as a VERIFIED operator — the enrolled, signed path. Reads pillars, builds Schema 1.0 snapshot, ed25519-signs with your device key, POSTs to board. Requires `enroll` first. |
103
- | 16 | `enroll` | Bind THIS device to your SigRank operator. Paste the key from signalaf.com → Settings → "New key". Generates + stores a local ed25519 keypair. Only the PUBLIC key is ever sent. |
101
+ | 14 | `submit_paste` | Ranks a paste of token counts AND publishes to the live board in one call. Computes local preview, then submits to server for authoritative scoring. Returns both local + server results. |
102
+ | 15 | `tokenpull_submit` | Pull your LOCAL token usage AND publish to the board in one call — the zero-paste flow. Reads pillars per window, computes cascade, submits each window server-side. Token-only, no prompt content. |
103
+ | 16 | `submit_verified` | Publish your LOCAL token runs as a VERIFIED operator — the enrolled, signed path. Reads pillars, builds Schema 1.0 snapshot, ed25519-signs with your device key, POSTs to board. Requires `enroll` first. |
104
+ | 17 | `enroll` | Bind THIS device to your SigRank operator. Paste the key from signalaf.com → Settings → "New key". Generates + stores a local ed25519 keypair. Only the PUBLIC key is ever sent. |
104
105
 
105
106
  ### Tokscale analytics — local tokscale data (no auth, no network)
106
107
 
107
108
  | # | Tool | What it does |
108
109
  |---|------|-------------|
109
- | 17 | `tokscale_breakdown` | Per-model token breakdown across platforms (models under threshold → "other"). |
110
- | 18 | `tokscale_market_share` | AI tool market share: each tool's % of tokens/cost/messages, ranked. From local tokscale data. |
111
- | 19 | `tokscale_developer_profile` | Per-developer usage profile across all detected tools: model mix, pillars, sessions, workspaces. Paths redacted. |
112
- | 20 | `tokscale_model_trends` | Model adoption over time: per-model first/last seen, active days, month-by-month adoption curve. |
113
- | 21 | `tokscale_cost_analysis` | Cost per developer per model: cost_per_million_tokens, cost_per_message, share_cost, client rollup. |
114
- | 22 | `tokscale_device_profile` | Device fingerprinting: installed tools, session counts, active days, day-of-week distribution, concurrency. Paths redacted. |
115
- | 23 | `tokscale_mcp_usage` | MCP server usage: detected servers, detection window, active days. |
116
- | 24 | `tokscale_competitive_intel` | Competitive intelligence for any AI tool: rank, model mix, cost efficiency, share vs all competitors. |
110
+ | 18 | `tokscale_breakdown` | Per-model token breakdown across platforms (models under threshold → "other"). |
111
+ | 19 | `tokscale_market_share` | AI tool market share: each tool's % of tokens/cost/messages, ranked. From local tokscale data. |
112
+ | 20 | `tokscale_developer_profile` | Per-developer usage profile across all detected tools: model mix, pillars, sessions, workspaces. Paths redacted. |
113
+ | 21 | `tokscale_model_trends` | Model adoption over time: per-model first/last seen, active days, month-by-month adoption curve. |
114
+ | 22 | `tokscale_cost_analysis` | Cost per developer per model: cost_per_million_tokens, cost_per_message, share_cost, client rollup. |
115
+ | 23 | `tokscale_device_profile` | Device fingerprinting: installed tools, session counts, active days, day-of-week distribution, concurrency. Paths redacted. |
116
+ | 24 | `tokscale_mcp_usage` | MCP server usage: detected servers, detection window, active days. |
117
+ | 25 | `tokscale_competitive_intel` | Competitive intelligence for any AI tool: rank, model mix, cost efficiency, share vs all competitors. |
117
118
 
118
119
  ### Moved to bestuser-router-mcp (5 intent tools)
119
120
 
@@ -160,7 +161,7 @@ The intent-based tools (in bestuser-router-mcp) are designed to map natural-lang
160
161
  | Transport | stdio |
161
162
  | Platform | Cross-platform (Node.js) |
162
163
  | Language | JavaScript |
163
- | Tools | 24 |
164
+ | Tools | 25 |
164
165
  | Auth | None for reads; Supabase anon key for submits |
165
166
  | Category | Developer Tools |
166
167
 
package/README.md CHANGED
@@ -95,7 +95,7 @@ sigrank submit
95
95
  sigrank submit --dry-run
96
96
  ```
97
97
 
98
- That's it. sigrank reads your local AI session logs on-device, derives your token cascade (Υ Yield, Leverage, Velocity, 10xDEV), and publishes to [signalaf.com](https://signalaf.com). No paste, no transcript content — only the four token counts leave your machine.
98
+ That's it. sigrank reads your local AI session logs on-device, derives your token cascade (Υ Yield, Leverage, Velocity, 10xDEV), and publishes to [signalaf.com](https://signalaf.com). No paste, no transcript content — what leaves your machine is limited to the four token pillars plus the identifiers listed under [Privacy](#privacy).
99
99
 
100
100
  Or just explore without signing in:
101
101
 
@@ -303,6 +303,8 @@ Or if installed globally:
303
303
  | `tokenpull_submit(codename, window?)` | `{codename?, window?}` | `tokenpull` → publish to board. Omit codename for preview |
304
304
  | `tokenpull_compare(platform?)` | `{platform?}` | All four sources side-by-side: tokenpull + ccusage + token-dash + tokscale. Returns pillars, cascade metrics, and delta % vs tokenpull per window |
305
305
  | `rank_windows` | `{platform?, window?}` | Multi-window cascade from local logs |
306
+ | `get_sigrank_standard_record` | `{input, output, cache_write?, cache_read?}` | Builds the portable `sigrank/0.1-draft` compatibility record (legacy alias under TTEOP authority) — canonical cascade, computed locally |
307
+ | `discover_peers` | `{codename?}` | Find mentors, peers, and complementary operators on the leaderboard |
306
308
  | `watch_tokenpull` | `{platform?, interval_s?}` | One cascade snapshot per call (interval_s advisory) |
307
309
  | `submit_verified` | `{window?, platform?, dry_run?}` | THE ranked path: builds + ed25519-signs Schema 1.0 snapshots and POSTs them. `platform:'multi'` sums all active platforms. `dry_run:true` returns the exact payload unsent |
308
310
  | `enroll` | `{code, device_label?}` | Bind this device with a connect code from signalaf.com → Settings |
@@ -310,20 +312,17 @@ Or if installed globally:
310
312
  | `simulate_change` | `{text?, changes}` | Prescriptive "what if" — test proposed pillar changes and see the exact Υ delta + class change before committing |
311
313
  | `suggest_improvements` | `{text?}` | Generates ranked, simulated improvement suggestions — tests strategies and returns them sorted by Υ yield impact |
312
314
  | `self_improve` | `{text?}` | One-click optimize: diagnoses, suggests, and simulates the best change in a single call |
313
- | `get_best_operator(n?)` | `{n?}` | Top N operators with behavioral framing in power-user language. Intent: "who is the best AI user?" |
314
- | `compare_self(codename? \| text?)` | `{codename?}` or `{text?}` | Your metrics vs board averages + power-user assessment + percentile + suggestion. Intent: "how do I measure up?" |
315
- | `compare_operators(a, b)` | `{codename_a, codename_b}` | Side-by-side comparison with behavioral verdict. Intent: "compare operator X vs Y" |
316
- | `describe_power_user()` | `{}` | Static explanation of AI power user archetype + metrics explained. Intent: "what is an AI power user?" |
317
- | `optimize_efficiency(codename? \| text?)` | `{codename?}` or `{text?}` | Ranked efficiency suggestions tied to your cascade shape. Intent: "how can I use AI more efficiently?" |
318
315
  | `tokscale_breakdown(threshold?)` | `{threshold?}` | Per-model token breakdown across platforms (models under threshold → "other") |
319
316
  | `tokscale_market_share()` | `{}` | AI tool market share: each tool's % of tokens/cost/messages, ranked. From local tokscale data |
320
- | `tokscale_developer_profile()` | `{}` | Per-developer usage profile across all detected tools: model mix, pillars, sessions, workspaces. Paths redacted |
317
+ | `tokscale_developer_profile()` | `{}` | Per-developer usage profile across all detected tools: model mix, pillars, sessions, workspace count (no names). Paths redacted |
321
318
  | `tokscale_model_trends()` | `{}` | Model adoption over time: per-model first/last seen, active days, month-by-month adoption curve |
322
319
  | `tokscale_cost_analysis()` | `{}` | Cost per developer per model: cost_per_million_tokens, cost_per_message, share_cost, client rollup |
323
320
  | `tokscale_device_profile()` | `{}` | Device fingerprinting: installed tools, session counts, active days, day-of-week distribution, concurrency. Paths redacted |
324
321
  | `tokscale_mcp_usage()` | `{}` | MCP server usage: detected servers, detection window, active days |
325
322
  | `tokscale_competitive_intel(target)` | `{target}` | Competitive intelligence for any AI tool: rank, model mix, cost efficiency, share vs all competitors |
326
323
 
324
+ Intent tools (`get_best_operator`, `compare_self`, `compare_operators`, `describe_power_user`, `optimize_efficiency`) moved to [bestuser-router-mcp](https://github.com/SunrisesIllNeverSee/bestuser-router-mcp) — `npx bestuser-router-mcp`.
325
+
327
326
  ---
328
327
 
329
328
  ## Cascade math
@@ -406,6 +405,8 @@ All adapters are token-only (no message content, no cost fields, no credentials)
406
405
 
407
406
  ## Privacy
408
407
 
408
+ - **Exactly what leaves your machine (signed submit):** the four token pillars (input, output, cache create, cache read) per window, your codename, device id, device public key, ed25519 signature, agent version, platform, message/session counts, and the weekly report block. `enroll` additionally sends a device label, which defaults to `sigrank <version> · <platform>` (your hostname is never sent unless you set `SIGRANK_DEVICE_LABEL` or pass `device_label`). Never sent: prompts, responses, file paths, repo/project names, or environment variables.
409
+ - **MCP safety rails.** The `enroll` tool refuses to re-bind a device that is already enrolled (re-enroll from the CLI), and `submit_verified` requires `confirm: true` to publish (not needed with `dry_run`). `tokscale_developer_profile` returns workspace counts only, never workspace or project names.
409
410
  - **Token-only persistence and submission.** Local-log adapters read usage metadata only. The optional proxy necessarily handles provider-bound request and response bytes in memory, but never persists their content; it writes only token counts, model/backend metadata, and timestamps. Only token telemetry is submitted to SigRank SignalAF.
410
411
  - **Local by default.** `tokenpull` reads only the selected or detected adapter paths on your device (e.g. `~/.claude/projects` for Claude, `~/.codex` for Codex, plus additional paths for Amp, Devin, Gemini, Goose, and other supported platforms). Numbers stay on your machine unless you explicitly submit with a codename.
411
412
  - **Background tooling excluded.** Memory plugins, observers, summarizers (e.g. `claude-mem`, `mem0`, `observer-sessions`) are filtered from both Claude and Codex reads. `subagents/` are kept — they represent real operator work.
@@ -418,7 +419,8 @@ All adapters are token-only (no message content, no cost fields, no credentials)
418
419
 
419
420
  | Var | Default | Description |
420
421
  | ----------------------- | ---------------------- | ---------------------------- |
421
- | `SIGRANK_API_BASE` | `https://signalaf.com` | Override the board host |
422
+ | `SIGRANK_API_BASE` | `https://signalaf.com` | Override the board host (https required; http only for loopback) |
423
+ | `SIGRANK_DEVICE_LABEL` | `sigrank <version> · <platform>` | Optional device label sent at enroll |
422
424
  | `SIGRANK_FETCH_TIMEOUT` | `10000` | Board API fetch timeout (ms) |
423
425
 
424
426
  ---
@@ -1099,6 +1099,8 @@ async function execCcusage(args) {
1099
1099
  if (existsSync(localEntrypoint)) {
1100
1100
  return execFileP(process.execPath, [localEntrypoint, ...args], options);
1101
1101
  }
1102
+ // Fallback only: the bundled (pinned) ccusage above is preferred; this resolves "ccusage"
1103
+ // from PATH when the bundled entrypoint is missing, so a PATH binary is trusted code here.
1102
1104
  return execFileP("ccusage", args, options);
1103
1105
  }
1104
1106
 
@@ -22,7 +22,10 @@ import { join, dirname, delimiter } from "node:path";
22
22
  import { homedir } from "node:os";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { ADAPTERS, walkFiles } from "./index.mjs";
25
- import { TOKSCALE_CLIENT_MAP } from "../lib/constants.mjs";
25
+ import {
26
+ TOKSCALE_CLIENT_MAP,
27
+ TOKSCALE_SCAN_TIMEOUT_MS,
28
+ } from "../lib/constants.mjs";
26
29
 
27
30
  const DAY_MS = 86_400_000;
28
31
 
@@ -516,7 +519,11 @@ async function _freshTokendash(platform = "claude") {
516
519
  // Now uses execFile (async) so the TUI stays responsive.
517
520
  async function _freshTokscale(platform = "claude") {
518
521
  try {
519
- const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
522
+ const raw = await execFileAsync(
523
+ "tokscale",
524
+ ["models", "--json"],
525
+ TOKSCALE_SCAN_TIMEOUT_MS,
526
+ );
520
527
  const data = JSON.parse(raw);
521
528
  const entries = Array.isArray(data?.entries)
522
529
  ? data.entries
@@ -557,7 +564,7 @@ async function _freshTokscale(platform = "claude") {
557
564
  *
558
565
  * - ccusage : `ccusage <platform> daily --json` → 7d/30d/90d/all
559
566
  * - tokendash: refresh + read ~/.claude/token-dashboard.db → all (claude only)
560
- * - tokscale : `bunx tokscale@latest models --json` → all
567
+ * - tokscale : `tokscale models --json` (bundled, pinned) → all
561
568
  */
562
569
  export async function freshVerifierPillars(platform = "claude") {
563
570
  const p = platform || "claude";
@@ -61,7 +61,9 @@ export function persistIdentity(identity) {
61
61
  if (existsSync(PATH)) {
62
62
  try {
63
63
  const stamp = Date.now();
64
- copyFileSync(PATH, join(DIR, `${BACKUP_PREFIX}${stamp}`));
64
+ const bakPath = join(DIR, `${BACKUP_PREFIX}${stamp}`);
65
+ copyFileSync(PATH, bakPath);
66
+ chmodSync(bakPath, 0o600);
65
67
  pruneBackups();
66
68
  } catch {
67
69
  /* best-effort — don't block the write */
package/lib/constants.mjs CHANGED
@@ -115,6 +115,18 @@ export const TOKSCALE_CLIENT_MAP = {
115
115
  */
116
116
  export const TOKSCALE_BLIND_PLATFORMS = [];
117
117
 
118
+ /**
119
+ * Timeout for `tokscale` CLI invocations (models/clients/graph/report scans).
120
+ * The scan walks every session log on the machine; on heavy machines it runs
121
+ * ~55–60s and GROWS with log volume, so the old hardcoded 60s ceiling flaked —
122
+ * the subprocess was killed mid-scan and the caller silently got null
123
+ * (TUI Compare showed "no data from tokscale"). 180s gives 3x headroom and
124
+ * stays under tokscale's own nativeTimeoutMs default (300s).
125
+ * Override via SIGRANK_TOKSCALE_TIMEOUT_MS.
126
+ */
127
+ export const TOKSCALE_SCAN_TIMEOUT_MS =
128
+ Number(process.env.SIGRANK_TOKSCALE_TIMEOUT_MS) || 180_000;
129
+
118
130
  /**
119
131
  * The "actually on the board" predicate. A submission is ranked only when the
120
132
  * server returns HTTP ok AND verification_tier === "verified" AND persisted
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": "1.0.43",
5
+ "version": "1.0.46",
6
6
  "description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read \u00d7 output / input\u00b2). 25 tools including SigRank Standard record, 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": "1.0.44",
3
+ "version": "1.0.46",
4
4
  "author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
5
5
  "description": "Upsilon measurement engine distributed through the sigrank CLI/MCP compatibility package, with SigRank public leaderboard tools.",
6
6
  "mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
@@ -81,10 +81,10 @@
81
81
  "on-device"
82
82
  ],
83
83
  "dependencies": {
84
- "token-cascade": "^0.2.1",
85
- "ccusage": "^20.0.19",
86
- "tokscale": "^4.7.0",
87
- "@modelcontextprotocol/server": "^2.3.0"
84
+ "token-cascade": "0.2.1",
85
+ "ccusage": "20.0.30",
86
+ "tokscale": "4.18.0",
87
+ "@modelcontextprotocol/server": "2.3.0"
88
88
  },
89
89
  "devDependencies": {
90
90
  "@resvg/resvg-js": "^2.6.2",
@@ -29,7 +29,11 @@ import { cascade, classify, CLASS_TIERS, UNCLASSED, tierOf } from "../cascade.mj
29
29
  import { ALL_PLATFORMS } from "../adapters.mjs";
30
30
  import { ensureIdentity, keystorePath } from "../keystore.mjs";
31
31
  import { submitSignedWindow } from "../submit.mjs";
32
- import { LEADERBOARD_METRIC, TOKSCALE_CLIENT_MAP } from "../lib/constants.mjs";
32
+ import {
33
+ LEADERBOARD_METRIC,
34
+ TOKSCALE_CLIENT_MAP,
35
+ TOKSCALE_SCAN_TIMEOUT_MS,
36
+ } from "../lib/constants.mjs";
33
37
  import { execFile } from "child_process";
34
38
  import { existsSync, readFileSync } from "fs";
35
39
  import os from "os";
@@ -422,7 +426,11 @@ async function ccusagePillars(platform = "claude") {
422
426
  async function tokscalePillars(platform = "claude") {
423
427
  // Try the bundled tokscale CLI first (fresh data), fall back to saved report file.
424
428
  try {
425
- const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
429
+ const raw = await execFileAsync(
430
+ "tokscale",
431
+ ["models", "--json"],
432
+ TOKSCALE_SCAN_TIMEOUT_MS,
433
+ );
426
434
  const data = JSON.parse(raw);
427
435
  const entries = Array.isArray(data?.entries)
428
436
  ? data.entries
@@ -1711,10 +1719,14 @@ async function runEnroll({ label } = {}) {
1711
1719
 
1712
1720
  let out;
1713
1721
  try {
1714
- out = await callTool("enroll", {
1715
- code,
1716
- device_label: typeof label === "string" ? label : undefined,
1717
- });
1722
+ out = await callTool(
1723
+ "enroll",
1724
+ {
1725
+ code,
1726
+ device_label: typeof label === "string" ? label : undefined,
1727
+ },
1728
+ { allowReenroll: true }, // human at a TTY already consented above
1729
+ );
1718
1730
  } catch (e) {
1719
1731
  writeln(red(` ✗ ${e.message}`));
1720
1732
  process.exitCode = 1;
@@ -1770,6 +1782,7 @@ async function runSubmit({ platform = "claude", window } = {}) {
1770
1782
  platform,
1771
1783
  window: typeof window === "string" ? window : undefined,
1772
1784
  dry_run: process.argv.includes("--dry-run"),
1785
+ confirm: true, // explicit CLI invocation is the confirmation
1773
1786
  });
1774
1787
  } catch (e) {
1775
1788
  writeln(red(` ✗ ${e.message}`));
@@ -1444,7 +1444,7 @@ function renderCompare(data) {
1444
1444
  const missing = [];
1445
1445
  if (!cc) missing.push("ccusage (npm i -g ccusage)");
1446
1446
  if (!td) missing.push("token-dashboard (~/.claude/token-dashboard.db)");
1447
- if (!ts) missing.push("tokscale (npm i -g tokscale)");
1447
+ if (!ts) missing.push("tokscale (not installed, or scan timed out)");
1448
1448
  if (missing.length)
1449
1449
  for (const ln of wrapDim(`no data from: ${missing.join(" · ")}`)) emit(ln);
1450
1450
  emit();
@@ -2904,7 +2904,7 @@ export async function runTui({
2904
2904
  didMulti = false;
2905
2905
  for (const p of platforms) {
2906
2906
  try {
2907
- const out = await callTool("submit_verified", { platform: p });
2907
+ const out = await callTool("submit_verified", { platform: p, confirm: true });
2908
2908
  const ws = out.windows || [];
2909
2909
  const received = ws.filter((w) => w.status === "received");
2910
2910
  const verified = received.filter(
@@ -2924,6 +2924,7 @@ export async function runTui({
2924
2924
  try {
2925
2925
  const out = await callTool("submit_verified", {
2926
2926
  platform: "multi",
2927
+ confirm: true,
2927
2928
  });
2928
2929
  const ws = out.windows || [];
2929
2930
  const received = ws.filter((w) => w.status === "received");
package/proxy.mjs CHANGED
@@ -311,6 +311,22 @@ export async function startProxy({
311
311
  };
312
312
 
313
313
  const server = http.createServer(async (req, res) => {
314
+ // DNS-rebinding / CSRF guard: only accept loopback Host headers on our own port, and
315
+ // refuse browser cross-site requests (Sec-Fetch-Site other than same-origin/none).
316
+ const hostHeader = String(req.headers.host || "");
317
+ const hm = /^(127\.0\.0\.1|localhost|\[::1\])(?::(\d+))?$/i.exec(hostHeader);
318
+ const boundPort = server.address()?.port;
319
+ const sfs = req.headers["sec-fetch-site"];
320
+ if (
321
+ !hm ||
322
+ (boundPort && Number(hm[2] || 80) !== boundPort) ||
323
+ (sfs !== undefined && sfs !== "same-origin" && sfs !== "none")
324
+ ) {
325
+ res.writeHead(403, { "content-type": "text/plain; charset=utf-8" });
326
+ res.end("Forbidden\n");
327
+ req.resume();
328
+ return;
329
+ }
314
330
  let parsedUrl;
315
331
  try {
316
332
  parsedUrl = new URL(req.url || "/", `http://${req.headers.host || host}`);
@@ -2,13 +2,15 @@
2
2
 
3
3
  ## What leaves your machine
4
4
 
5
- **Every submission path sends only four numbers:**
5
+ **The usage content of every submission is four numbers:**
6
6
  - Input tokens
7
7
  - Output tokens
8
8
  - Cache create tokens
9
9
  - Cache read tokens
10
10
 
11
- That's the entire payload — no prompts, no code, no file contents, no conversation text. The MCP parses your input locally and sends only the four token counts to the server, which re-parses and re-scores them authoritatively.
11
+ The signed path additionally carries: codename, device id, device public key, ed25519 signature, agent version, platform, message/session counts, and the weekly report block. `enroll` sends a device label (default `sigrank <version> · <platform>`; your hostname is not sent unless you set `SIGRANK_DEVICE_LABEL` or pass `device_label` yourself).
12
+
13
+ Never sent: prompts, responses, code, file contents, file paths, repo or project names, or environment variables. The server re-parses and re-scores the pillars authoritatively.
12
14
 
13
15
  The optional `sigrank proxy` is a local Upsilon transport path to Anthropic/OpenAI, not a
14
16
  submission path to the SigRank leaderboard. When explicitly enabled, it necessarily receives and
@@ -32,8 +34,9 @@ only provider-reported token counts, model/backend metadata, and timestamps to
32
34
  - Your prompts or messages
33
35
  - Your code or file contents
34
36
  - Your tool calls or their results
35
- - Which AI platform you use (beyond token counts)
36
- - Your identity (only your chosen codename)
37
+ - Your hostname (unless you opt in via a device label)
38
+ - File paths, repo/project names, or environment variables
39
+ - Your identity (only your chosen codename; the platform name, agent version, and message/session counts are sent)
37
40
 
38
41
  The local proxy process is different from the Upsilon service: if you opt in, it
39
42
  handles your provider traffic in memory solely to forward it to Anthropic or
@@ -33,7 +33,10 @@ import { existsSync, readFileSync } from "node:fs";
33
33
  import os from "node:os";
34
34
  import path from "node:path";
35
35
  import { fileURLToPath } from "node:url";
36
- import { TOKSCALE_CLIENT_MAP } from "./lib/constants.mjs";
36
+ import {
37
+ TOKSCALE_CLIENT_MAP,
38
+ TOKSCALE_SCAN_TIMEOUT_MS,
39
+ } from "./lib/constants.mjs";
37
40
 
38
41
  // Resolve local node_modules/.bin for the bundled tokscale binary.
39
42
  const _pkgRoot = path.dirname(fileURLToPath(import.meta.url));
@@ -61,13 +64,14 @@ function execFileAsync(cmd, args, timeoutMs) {
61
64
  });
62
65
  }
63
66
 
64
- // tokscale subcommand timeouts. `models` is fast (~5s); `graph` and `report`
65
- // walk every session log and can take ~30s on a heavy machine. 60s ceiling.
66
- const T_MODELS = 60_000;
67
- const T_MONTHLY = 60_000;
68
- const T_CLIENTS = 30_000;
69
- const T_GRAPH = 60_000;
70
- const T_REPORT = 60_000;
67
+ // tokscale subcommand timeouts. `models`/`clients`/`graph`/`report` all walk the
68
+ // session logs — on a heavy machine the scan runs ~55-60s and grows with log
69
+ // volume, so they share TOKSCALE_SCAN_TIMEOUT_MS (was a flat 60s that flaked).
70
+ const T_MODELS = TOKSCALE_SCAN_TIMEOUT_MS;
71
+ const T_MONTHLY = TOKSCALE_SCAN_TIMEOUT_MS;
72
+ const T_CLIENTS = TOKSCALE_SCAN_TIMEOUT_MS;
73
+ const T_GRAPH = TOKSCALE_SCAN_TIMEOUT_MS;
74
+ const T_REPORT = TOKSCALE_SCAN_TIMEOUT_MS;
71
75
 
72
76
  const HOME = os.homedir();
73
77
 
@@ -352,7 +356,6 @@ export async function tokscaleDeveloperProfile() {
352
356
  sessions_path_exists: c.sessions_path_exists ?? null,
353
357
  headless_supported: c.headless_supported ?? null,
354
358
  workspace_count: c.workspaces.size,
355
- workspaces: [...c.workspaces].sort(),
356
359
  models: c.models,
357
360
  };
358
361
  });
@@ -32,6 +32,8 @@ const ALLOWED_API_HOSTS = new Set([
32
32
  "sigeconomy.com",
33
33
  "localhost",
34
34
  "127.0.0.1",
35
+ "::1",
36
+ "[::1]",
35
37
  "test.local", // test fixtures only
36
38
  ]);
37
39
 
@@ -42,6 +44,10 @@ export function validateApiBase(urlStr) {
42
44
  if (!ALLOWED_API_HOSTS.has(u.hostname)) {
43
45
  throw new Error(`Blocked: API host '${u.hostname}' is not in the allowlist`);
44
46
  }
47
+ const loopback = ["localhost", "127.0.0.1", "::1", "[::1]", "test.local"].includes(u.hostname);
48
+ if (u.protocol !== "https:" && !(loopback && u.protocol === "http:")) {
49
+ throw new Error(`Blocked: API base must use https (http only for loopback): ${u.protocol}//${u.hostname}`);
50
+ }
45
51
  return urlStr;
46
52
  } catch (e) {
47
53
  if (e.message.startsWith("Blocked:")) throw e;
@@ -10,6 +10,7 @@ import { existsSync, readFileSync } from "node:fs";
10
10
  import os from "node:os";
11
11
  import path from "node:path";
12
12
  import { execFileAsync } from "./_helpers.mjs";
13
+ import { TOKSCALE_SCAN_TIMEOUT_MS } from "../lib/constants.mjs";
13
14
 
14
15
  async function _ccusagePillars(platform = "claude") {
15
16
  try {
@@ -87,7 +88,11 @@ async function _tokenDashPillars() {
87
88
  async function _tokscalePillars(platform = "claude") {
88
89
  // Try the bundled tokscale CLI first (fresh data), fall back to saved report file.
89
90
  try {
90
- const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
91
+ const raw = await execFileAsync(
92
+ "tokscale",
93
+ ["models", "--json"],
94
+ TOKSCALE_SCAN_TIMEOUT_MS,
95
+ );
91
96
  const data = JSON.parse(raw);
92
97
  const entries = Array.isArray(data?.entries)
93
98
  ? data.entries
package/tools/enroll.mjs CHANGED
@@ -2,7 +2,6 @@
2
2
  * tools/enroll.mjs — enroll tool.
3
3
  */
4
4
 
5
- import os from "node:os";
6
5
  import { ensureIdentity, recordEnrollment } from "../identity/keystore.mjs";
7
6
  import { ENROLL_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
8
7
  import { TERMS_VERSION, PRIVACY_VERSION } from "../lib/constants.mjs";
@@ -28,7 +27,7 @@ export const TOOL_DEF = {
28
27
  device_label: {
29
28
  type: "string",
30
29
  description:
31
- "optional label for this device (default: hostname · agent version)",
30
+ "optional label for this device (default: sigrank <version> · <platform>; hostnames are never sent unless you pass a label yourself)",
32
31
  },
33
32
  },
34
33
  required: ["code"],
@@ -45,8 +44,23 @@ export async function handleEnroll(args, ctx) {
45
44
  "enroll requires a `code` — paste your connect code from signalaf.com → Settings → Connect a device.",
46
45
  );
47
46
  const id = ctx.opts.identity || ensureIdentity();
47
+ // Re-enroll guard: a model-invoked MCP call must never silently re-bind a device that
48
+ // already has an operator. The human CLI/TUI path sets opts.allowReenroll after its own
49
+ // explicit consent prompt.
50
+ if (id.codename && !ctx.opts.allowReenroll) {
51
+ return {
52
+ status: "already_enrolled",
53
+ codename: id.codename,
54
+ operator_id: id.operator_id ?? null,
55
+ device_id: id.device_id,
56
+ detail:
57
+ "This device is already enrolled. To re-enroll, run `npx sigrank enroll` in a terminal yourself; the MCP tool will not re-bind an enrolled device.",
58
+ };
59
+ }
48
60
  const deviceLabel = String(
49
- args?.device_label || `${os.hostname()} · ${id.agent_version}`,
61
+ args?.device_label ||
62
+ process.env.SIGRANK_DEVICE_LABEL ||
63
+ `sigrank ${id.agent_version} · ${process.platform}`,
50
64
  ).slice(0, 120);
51
65
  const res = await ctx.doFetch(`${ctx.apiBase}/api/v1/devices/enroll`, {
52
66
  method: "POST",
package/tools/index.mjs CHANGED
@@ -12,6 +12,7 @@ import { buildFetch, DEFAULT_API_BASE, DEFAULT_FETCH_TIMEOUT } from "./_helpers.
12
12
  import {
13
13
  TOKSCALE_CLIENT_MAP,
14
14
  TOKSCALE_BLIND_PLATFORMS,
15
+ TOKSCALE_SCAN_TIMEOUT_MS,
15
16
  } from "../lib/constants.mjs";
16
17
 
17
18
  import { TOOL_DEF as rankPasteTool, handleRankPaste } from "./rank-paste.mjs";
@@ -83,7 +84,11 @@ export const TOOLS = [
83
84
 
84
85
  /** Run `tokscale models --json` and return the list of our platform names. */
85
86
  async function _tokscaleDetectClients() {
86
- const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
87
+ const raw = await execFileAsync(
88
+ "tokscale",
89
+ ["models", "--json"],
90
+ TOKSCALE_SCAN_TIMEOUT_MS,
91
+ );
87
92
  const data = JSON.parse(raw);
88
93
  const entries = Array.isArray(data?.entries)
89
94
  ? data.entries
@@ -12,7 +12,7 @@ import { _computeReportBlock } from "./_report.mjs";
12
12
  export const TOOL_DEF = {
13
13
  name: "submit_verified",
14
14
  description:
15
- "Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine. Pass dry_run:true to inspect the exact signed payload without publishing.",
15
+ "Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine. Publishing requires `confirm: true` (ask the user first); pass dry_run:true to inspect the exact signed payload without publishing and without confirm.",
16
16
  annotations: {
17
17
  title: "Submit verified score",
18
18
  ...ANNOTATIONS.destructiveHint,
@@ -33,6 +33,11 @@ export const TOOL_DEF = {
33
33
  description:
34
34
  "source platform (default: claude). 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active); empty windows are skipped.",
35
35
  },
36
+ confirm: {
37
+ type: "boolean",
38
+ description:
39
+ "REQUIRED to publish: must be true, set only after the user has explicitly approved submitting. Not needed with dry_run:true.",
40
+ },
36
41
  dry_run: {
37
42
  type: "boolean",
38
43
  description:
@@ -52,6 +57,13 @@ export async function handleSubmitVerified(args, ctx) {
52
57
  detail: "Run `npx sigrank-mcp enroll` to bind this device first.",
53
58
  };
54
59
  }
60
+ if (!args?.dry_run && args?.confirm !== true) {
61
+ return {
62
+ status: "confirmation_required",
63
+ detail:
64
+ "submit_verified publishes to the public board. Ask the user for approval, then call again with confirm: true (or use dry_run: true to preview).",
65
+ };
66
+ }
55
67
  const platform = args?.platform || "claude";
56
68
 
57
69
  // MULTI: the combined cross-platform cascade. The dashboard already SUMS every
@@ -4,7 +4,10 @@
4
4
 
5
5
  import { execFileAsync } from "./_helpers.mjs";
6
6
  import { ANNOTATIONS } from "./_schemas.mjs";
7
- import { TOKSCALE_CLIENT_MAP } from "../lib/constants.mjs";
7
+ import {
8
+ TOKSCALE_CLIENT_MAP,
9
+ TOKSCALE_SCAN_TIMEOUT_MS,
10
+ } from "../lib/constants.mjs";
8
11
 
9
12
  export const TOOL_DEF = {
10
13
  name: "tokscale_breakdown",
@@ -47,7 +50,11 @@ export const TOOL_DEF = {
47
50
  * Returns: { platform: [{ model, input, output, cacheRead, cacheCreate, pct }] }
48
51
  */
49
52
  export async function tokscaleModelBreakdown(threshold = 0.01) {
50
- const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
53
+ const raw = await execFileAsync(
54
+ "tokscale",
55
+ ["models", "--json"],
56
+ TOKSCALE_SCAN_TIMEOUT_MS,
57
+ );
51
58
  const data = JSON.parse(raw);
52
59
  const entries = Array.isArray(data?.entries)
53
60
  ? data.entries
@@ -20,7 +20,7 @@ export const TOOL_DEF = {
20
20
  properties: {
21
21
  tools: {
22
22
  type: "array",
23
- description: "Per-tool profile with model mix, token pillars, session counts, workspaces",
23
+ description: "Per-tool profile with model mix, token pillars, session counts, workspace count",
24
24
  },
25
25
  summary: {
26
26
  type: "object",