sigrank 0.17.4 → 0.18.2
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 +10 -3
- package/adapters.mjs +109 -2
- package/cli.mjs +9 -2
- package/index.mjs +11 -5
- package/package.json +3 -4
- package/submit.mjs +18 -0
- package/tokenpull.mjs +7 -15
- package/tools.mjs +391 -15
- package/tui.mjs +9 -7
package/README.md
CHANGED
|
@@ -66,10 +66,17 @@ For all builders, burners and 10xers.
|
|
|
66
66
|
| **[sigrank-vscode](https://github.com/SunrisesIllNeverSee/sigrank-vscode)** | The IDE extension — see your cascade metrics inline in VS Code. | `code --install-extension sigrank.sigrank` |
|
|
67
67
|
| **[fundscore](https://github.com/SunrisesIllNeverSee/fundscore)** | The repo scorer — investor-readiness scoring for GitHub repos. CLI + MCP server. | `npx fundscore` |
|
|
68
68
|
|
|
69
|
+
### Also in the MO§ES™ suite
|
|
70
|
+
|
|
71
|
+
| Site | What it is |
|
|
72
|
+
| ---- | ---------- |
|
|
73
|
+
| **[SIGNOMY](https://signomy.xyz)** | Governed AI agent marketplace where ranked agents form teams, fill slots, run missions, and earn revenue under constitutional protocol. Agents are free. Operators pay. |
|
|
74
|
+
| **[MO§ES](https://mos2es.com)** | The governance framework that underpins SigRank, SIGNOMY, and all governed agent operations. Structural accountability for agentic systems. |
|
|
75
|
+
|
|
69
76
|
## Quickstart — 3 steps to the board
|
|
70
77
|
|
|
71
78
|
```bash
|
|
72
|
-
# 1. Install (pulls ccusage + tokscale
|
|
79
|
+
# 1. Install (pulls ccusage + tokscale automatically — no separate installs)
|
|
73
80
|
npm install -g sigrank
|
|
74
81
|
|
|
75
82
|
# 2. Sign in (paste a connect code from signalaf.com → Settings → New key)
|
|
@@ -147,7 +154,7 @@ smithery tool call sigrank rank_paste '{"text": "1000000 500000 50000 800000"}'
|
|
|
147
154
|
## Commands
|
|
148
155
|
|
|
149
156
|
```
|
|
150
|
-
⊙ SigRank CLI v0.
|
|
157
|
+
⊙ SigRank CLI v0.0.177
|
|
151
158
|
|
|
152
159
|
Default (no args)
|
|
153
160
|
sigrank unified dashboard: cascade + token pillars + board
|
|
@@ -296,7 +303,7 @@ The dashboard pulls from multiple sources and shows them side-by-side for verifi
|
|
|
296
303
|
| ----------------- | ------------------------------------------------------------------------------ | --------------------- |
|
|
297
304
|
| `tokenpull` | On-device JSONL scanner (canon source) | claude, codex, amp, … |
|
|
298
305
|
| `ccusage` | `ccusage <platform> daily --json` CLI (bundled) | claude, codex |
|
|
299
|
-
| `token-dashboard` | `~/.claude/token-dashboard.db` SQLite (
|
|
306
|
+
| `token-dashboard` | `~/.claude/token-dashboard.db` SQLite ([Nate's](https://github.com/nateherkai/token-dashboard)) | claude only |
|
|
300
307
|
| `tokscale` | `tokscale models --json` CLI (bundled, falls back to `~/tokscale_report.json`) | claude, codex |
|
|
301
308
|
|
|
302
309
|
**Codex input is estimated** — Codex logs don't expose true input tokens directly. The formula:
|
package/adapters.mjs
CHANGED
|
@@ -97,10 +97,11 @@ function* parseJsonl(text, filePath) {
|
|
|
97
97
|
/** Run sqlite3 -json and return parsed rows, or [] on error/unavailability.
|
|
98
98
|
* execFile with an args array — no shell, so dbPath/sql need no quoting/escaping
|
|
99
99
|
* (matches the execFile hardening used in tools.mjs / tokenpull.mjs). */
|
|
100
|
-
async function sqliteJson(dbPath, sql) {
|
|
100
|
+
async function sqliteJson(dbPath, sql, timeoutMs = 10_000) {
|
|
101
101
|
try {
|
|
102
102
|
const { stdout } = await execFileP("sqlite3", ["-json", dbPath, sql], {
|
|
103
|
-
timeout:
|
|
103
|
+
timeout: timeoutMs,
|
|
104
|
+
maxBuffer: 256 * 1024 * 1024, // 256MB — Devin's sessions.db yields ~15MB JSON
|
|
104
105
|
});
|
|
105
106
|
return JSON.parse(stdout || "[]");
|
|
106
107
|
} catch {
|
|
@@ -729,6 +730,110 @@ export const hermesAdapter = {
|
|
|
729
730
|
},
|
|
730
731
|
};
|
|
731
732
|
|
|
733
|
+
// ── 14. Devin CLI ────────────────────────────────────────────────────────────
|
|
734
|
+
// SQLite: ~/.local/share/devin/cli/sessions.db
|
|
735
|
+
// Same combined-input problem as Codex: input_tokens INCLUDES cache write, so we
|
|
736
|
+
// yield { ts, output, cacheRead, uncached } and let tokenpullCodex() do the
|
|
737
|
+
// ioRatio split (input = output × ioRatio, cacheCreate = uncached − input).
|
|
738
|
+
// ioRatio comes from Claude (Beta) or the 7:1:2 average (Alpha = 0.5).
|
|
739
|
+
export const devinAdapter = {
|
|
740
|
+
platform: "devin",
|
|
741
|
+
defaultRoot: () => join(homedir(), ".local", "share", "devin", "cli"),
|
|
742
|
+
async *records(root) {
|
|
743
|
+
for (const r of roots("DEVIN_HOME", root)) {
|
|
744
|
+
const dbPath = join(r, "sessions.db");
|
|
745
|
+
const rows = await sqliteJson(
|
|
746
|
+
dbPath,
|
|
747
|
+
`SELECT row_id, session_id,
|
|
748
|
+
json_extract(chat_message, '$.metadata.metrics.input_tokens') as input_tokens,
|
|
749
|
+
json_extract(chat_message, '$.metadata.metrics.output_tokens') as output_tokens,
|
|
750
|
+
json_extract(chat_message, '$.metadata.metrics.cache_read_tokens') as cache_read_tokens,
|
|
751
|
+
json_extract(chat_message, '$.metadata.created_at') as created_at
|
|
752
|
+
FROM message_nodes
|
|
753
|
+
WHERE json_extract(chat_message, '$.role') = 'assistant'
|
|
754
|
+
AND json_extract(chat_message, '$.metadata.metrics.input_tokens') IS NOT NULL
|
|
755
|
+
ORDER BY created_at`,
|
|
756
|
+
60_000,
|
|
757
|
+
);
|
|
758
|
+
for (const row of rows) {
|
|
759
|
+
const inputIncl = Number(row.input_tokens || 0);
|
|
760
|
+
const cached = Number(row.cache_read_tokens || 0);
|
|
761
|
+
const output = Number(row.output_tokens || 0);
|
|
762
|
+
if (inputIncl + output + cached === 0) continue;
|
|
763
|
+
yield {
|
|
764
|
+
ts: row.created_at || null,
|
|
765
|
+
output,
|
|
766
|
+
cacheRead: cached,
|
|
767
|
+
uncached: Math.max(0, inputIncl - cached),
|
|
768
|
+
file: dbPath,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// ── 15. Other (user-supplied JSON) ───────────────────────────────────────────
|
|
776
|
+
// For platforms/models not yet on the adapter list. The user writes a JSON file
|
|
777
|
+
// with the 4 pillars per window and points SIGRANK_OTHER_PATH at it. This lets
|
|
778
|
+
// anyone with a new tool submit without needing a custom adapter.
|
|
779
|
+
//
|
|
780
|
+
// JSON format (all fields optional, missing windows are skipped):
|
|
781
|
+
// {
|
|
782
|
+
// "platform": "my-tool", // optional, defaults to "other"
|
|
783
|
+
// "windows": {
|
|
784
|
+
// "all": { "input": 1000, "output": 2000, "cacheCreate": 500, "cacheRead": 8000 },
|
|
785
|
+
// "30d": { "input": 500, "output": 1000, "cacheCreate": 200, "cacheRead": 4000 },
|
|
786
|
+
// "7d": { "input": 100, "output": 200, "cacheCreate": 50, "cacheRead": 800 }
|
|
787
|
+
// }
|
|
788
|
+
// }
|
|
789
|
+
//
|
|
790
|
+
// The adapter yields one "message" per window with the summed pillars. Since
|
|
791
|
+
// there's no per-message timestamp, all data lands in the "all" window only
|
|
792
|
+
// (same as null-timestamp records in other adapters).
|
|
793
|
+
export const otherAdapter = {
|
|
794
|
+
platform: "other",
|
|
795
|
+
defaultRoot: () => process.env.SIGRANK_OTHER_PATH || "",
|
|
796
|
+
async *messages(root) {
|
|
797
|
+
const filePath = root || process.env.SIGRANK_OTHER_PATH;
|
|
798
|
+
if (!filePath) {
|
|
799
|
+
throw new Error(
|
|
800
|
+
"other adapter: set SIGRANK_OTHER_PATH to a JSON file with your token pillars. " +
|
|
801
|
+
'Format: { "windows": { "all": { "input": N, "output": N, "cacheCreate": N, "cacheRead": N } } }',
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
let text;
|
|
805
|
+
try {
|
|
806
|
+
text = await readFile(filePath, "utf8");
|
|
807
|
+
} catch {
|
|
808
|
+
throw new Error(`other adapter: cannot read ${filePath} — check SIGRANK_OTHER_PATH`);
|
|
809
|
+
}
|
|
810
|
+
let data;
|
|
811
|
+
try {
|
|
812
|
+
data = JSON.parse(text);
|
|
813
|
+
} catch {
|
|
814
|
+
throw new Error(`other adapter: invalid JSON in ${filePath}`);
|
|
815
|
+
}
|
|
816
|
+
const windows = data.windows || {};
|
|
817
|
+
for (const [win, pillars] of Object.entries(windows)) {
|
|
818
|
+
const input = Number(pillars.input || 0);
|
|
819
|
+
const output = Number(pillars.output || 0);
|
|
820
|
+
const cacheCreate = Number(pillars.cacheCreate || pillars.cache_create || 0);
|
|
821
|
+
const cacheRead = Number(pillars.cacheRead || pillars.cache_read || 0);
|
|
822
|
+
if (input + output + cacheCreate + cacheRead === 0) continue;
|
|
823
|
+
yield {
|
|
824
|
+
id: `other:${win}`,
|
|
825
|
+
sid: `other-${win}`,
|
|
826
|
+
ts: null, // no timestamp → lands in "all" window only
|
|
827
|
+
input,
|
|
828
|
+
output,
|
|
829
|
+
cacheCreate,
|
|
830
|
+
cacheRead,
|
|
831
|
+
file: filePath,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
};
|
|
836
|
+
|
|
732
837
|
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
733
838
|
/** All non-Claude, non-Codex adapters keyed by platform ID. */
|
|
734
839
|
export const ADAPTERS = {
|
|
@@ -745,6 +850,8 @@ export const ADAPTERS = {
|
|
|
745
850
|
goose: gooseAdapter,
|
|
746
851
|
kilo: kiloAdapter,
|
|
747
852
|
hermes: hermesAdapter,
|
|
853
|
+
devin: devinAdapter,
|
|
854
|
+
other: otherAdapter,
|
|
748
855
|
};
|
|
749
856
|
|
|
750
857
|
export const ALL_PLATFORMS = Object.keys(ADAPTERS).concat(["claude", "codex"]);
|
package/cli.mjs
CHANGED
|
@@ -236,7 +236,10 @@ async function fetchBoard(window = "30d") {
|
|
|
236
236
|
const res = await fetch(
|
|
237
237
|
`${DEFAULT_API_BASE}/api/v1/leaderboard?window=${window}&metric=yield_`,
|
|
238
238
|
{
|
|
239
|
-
headers: {
|
|
239
|
+
headers: {
|
|
240
|
+
accept: "application/json",
|
|
241
|
+
"user-agent": `node/${process.version} sigrank-mcp/cli`,
|
|
242
|
+
},
|
|
240
243
|
},
|
|
241
244
|
);
|
|
242
245
|
if (!res.ok) throw new Error(`Board API → HTTP ${res.status}`);
|
|
@@ -491,7 +494,7 @@ function appPillars() {
|
|
|
491
494
|
async function tokenDashPillars() {
|
|
492
495
|
const dbPath = path.join(os.homedir(), ".claude", "token-dashboard.db");
|
|
493
496
|
if (!existsSync(dbPath)) return null;
|
|
494
|
-
// Read directly with sqlite3 —
|
|
497
|
+
// Read directly with sqlite3 — DB created by Nate's token-dashboard (github.com/nateherkai/token-dashboard).
|
|
495
498
|
// Windowed queries using sqlite3's datetime() function.
|
|
496
499
|
const cf =
|
|
497
500
|
"(model LIKE '%claude%' OR model LIKE '%fable%' OR model LIKE '%sonnet%' OR model LIKE '%opus%' OR model LIKE '%haiku%')";
|
|
@@ -934,6 +937,7 @@ async function runWatch({
|
|
|
934
937
|
const ALL_PLATFORMS = [
|
|
935
938
|
"claude",
|
|
936
939
|
"codex",
|
|
940
|
+
"devin",
|
|
937
941
|
"amp",
|
|
938
942
|
"gemini",
|
|
939
943
|
"kimi",
|
|
@@ -944,8 +948,10 @@ const ALL_PLATFORMS = [
|
|
|
944
948
|
"droid",
|
|
945
949
|
"codebuff",
|
|
946
950
|
"copilot",
|
|
951
|
+
"opencode",
|
|
947
952
|
"openclaw",
|
|
948
953
|
"pi",
|
|
954
|
+
"other",
|
|
949
955
|
];
|
|
950
956
|
|
|
951
957
|
async function runSigRank() {
|
|
@@ -1472,6 +1478,7 @@ async function runSigRank() {
|
|
|
1472
1478
|
headers: {
|
|
1473
1479
|
"content-type": "application/json",
|
|
1474
1480
|
accept: "application/json",
|
|
1481
|
+
"user-agent": `node/${process.version} sigrank-mcp/cli`,
|
|
1475
1482
|
},
|
|
1476
1483
|
body: JSON.stringify({
|
|
1477
1484
|
codename,
|
package/index.mjs
CHANGED
|
@@ -194,7 +194,7 @@ async function startMcpServer() {
|
|
|
194
194
|
uri: "sigrank://install-guide",
|
|
195
195
|
name: "Quick Start Guide",
|
|
196
196
|
description:
|
|
197
|
-
"Step-by-step install and first submission: npx sigrank → enroll → submit. Covers ccusage/tokscale
|
|
197
|
+
"Step-by-step install and first submission: npx sigrank → enroll → submit. Covers ccusage/tokscale bundled deps + token-dashboard (Nate's, github.com/nateherkai/token-dashboard), privacy model (token counts only, never prompts), and dry-run workflow.",
|
|
198
198
|
mimeType: "text/markdown",
|
|
199
199
|
},
|
|
200
200
|
{
|
|
@@ -289,14 +289,20 @@ No global install needed. The MCP server starts automatically when an AI client
|
|
|
289
289
|
npx sigrank submit --dry-run
|
|
290
290
|
\`\`\`
|
|
291
291
|
|
|
292
|
-
## Token Pull Sources
|
|
292
|
+
## Token Pull Sources
|
|
293
293
|
|
|
294
|
-
SigRank bundles
|
|
294
|
+
SigRank bundles two token readers as npm dependencies:
|
|
295
295
|
- **ccusage** — Claude Code session logs
|
|
296
296
|
- **tokscale** — multi-platform token telemetry
|
|
297
|
-
- **tokendash** — dashboard + TUI for token usage
|
|
298
297
|
|
|
299
|
-
|
|
298
|
+
A third source, **token-dashboard**, is read from \`~/.claude/token-dashboard.db\` (SQLite).
|
|
299
|
+
That DB is created by [Nate's token-dashboard](https://github.com/nateherkai/token-dashboard) (Python):
|
|
300
|
+
\`\`\`bash
|
|
301
|
+
git clone https://github.com/nateherkai/token-dashboard.git
|
|
302
|
+
cd token-dashboard
|
|
303
|
+
python3 cli.py dashboard
|
|
304
|
+
\`\`\`
|
|
305
|
+
Run the scan once to create the DB; SigRank reads it directly via sqlite3 after that.
|
|
300
306
|
|
|
301
307
|
## MCP Client Setup
|
|
302
308
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
|
|
5
5
|
"description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
|
|
6
6
|
"mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
|
|
@@ -64,8 +64,7 @@
|
|
|
64
64
|
],
|
|
65
65
|
"dependencies": {
|
|
66
66
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"tokscale": "^4.0.4"
|
|
67
|
+
"ccusage": "^20.0.17",
|
|
68
|
+
"tokscale": "^4.5.2"
|
|
70
69
|
}
|
|
71
70
|
}
|
package/submit.mjs
CHANGED
|
@@ -11,9 +11,22 @@
|
|
|
11
11
|
|
|
12
12
|
import { snapshotHash, signPayload } from "./sign.mjs";
|
|
13
13
|
import { preflight } from "./preflight.mjs";
|
|
14
|
+
import { readFileSync } from "node:fs";
|
|
14
15
|
|
|
15
16
|
const WINDOW_TYPE = { "7d": "7d", "30d": "30d", "90d": "90d", all: "all_time" };
|
|
16
17
|
const WINDOW_SPAN_DAYS = { "7d": 7, "30d": 30, "90d": 90, all_time: 3650 };
|
|
18
|
+
|
|
19
|
+
/** Resolve this package's version for the User-Agent stamp (best-effort). */
|
|
20
|
+
function _pkgVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(
|
|
23
|
+
readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
|
|
24
|
+
);
|
|
25
|
+
return pkg.version;
|
|
26
|
+
} catch {
|
|
27
|
+
return "unknown";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
17
30
|
const PLATFORM_ENUM = new Set([
|
|
18
31
|
"claude",
|
|
19
32
|
"chatgpt",
|
|
@@ -181,8 +194,13 @@ export async function submitSignedWindow(
|
|
|
181
194
|
((url, init = {}) => {
|
|
182
195
|
const ctrl = new AbortController();
|
|
183
196
|
const timer = setTimeout(() => ctrl.abort(), 15_000);
|
|
197
|
+
const headers = {
|
|
198
|
+
"user-agent": `node/${process.version} sigrank-mcp/${_pkgVersion()}`,
|
|
199
|
+
...(init.headers || {}),
|
|
200
|
+
};
|
|
184
201
|
return fetch(url, {
|
|
185
202
|
...init,
|
|
203
|
+
headers,
|
|
186
204
|
signal: init.signal || ctrl.signal,
|
|
187
205
|
}).finally(() => clearTimeout(timer));
|
|
188
206
|
});
|
package/tokenpull.mjs
CHANGED
|
@@ -343,7 +343,7 @@ export async function tokenpullCodex({
|
|
|
343
343
|
});
|
|
344
344
|
|
|
345
345
|
return {
|
|
346
|
-
platform: "codex",
|
|
346
|
+
platform: adapter.platform || "codex",
|
|
347
347
|
root: r,
|
|
348
348
|
generatedAt: new Date(nowMs).toISOString(),
|
|
349
349
|
files: files.size,
|
|
@@ -367,13 +367,12 @@ export async function tokenpullCodex({
|
|
|
367
367
|
export async function tokenpullAny(platform, opts = {}) {
|
|
368
368
|
if (!platform || platform === "claude")
|
|
369
369
|
return tokenpull({ adapter: claudeAdapter, ...opts });
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
// Codex + Devin: both combine input + cache write in input_tokens, so both go
|
|
371
|
+
// through tokenpullCodex() which splits via ioRatio (Beta from Claude, Alpha 2.0).
|
|
372
|
+
if (platform === "codex" || platform === "devin") {
|
|
372
373
|
let ioRatio = opts.ioRatio || 2.0;
|
|
373
374
|
if (!opts.ioRatio) {
|
|
374
375
|
try {
|
|
375
|
-
// Forward root/now from opts so the Beta-ratio derivation is testable
|
|
376
|
-
// (without this, it always reads the real ~/.claude and can't be injected).
|
|
377
376
|
const c = await tokenpull({
|
|
378
377
|
adapter: claudeAdapter,
|
|
379
378
|
root: opts.root,
|
|
@@ -386,15 +385,8 @@ export async function tokenpullAny(platform, opts = {}) {
|
|
|
386
385
|
/* no Claude data → Alpha 2.0 */
|
|
387
386
|
}
|
|
388
387
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
// Cloud agents (Devin, etc.) run server-side — no local JSONL to read
|
|
392
|
-
const CLOUD_AGENTS = { devin: "Cognition/Devin" };
|
|
393
|
-
if (platform in CLOUD_AGENTS) {
|
|
394
|
-
throw new Error(
|
|
395
|
-
`"${platform}" (${CLOUD_AGENTS[platform]}) runs in the cloud — sessions are not written to local JSONL files. ` +
|
|
396
|
-
`There is no local data source to read. If ${CLOUD_AGENTS[platform]} exposes a usage API in future, an adapter can be added.`,
|
|
397
|
-
);
|
|
388
|
+
const adapter = platform === "codex" ? codexAdapter : ADAPTERS.devin;
|
|
389
|
+
return tokenpullCodex({ adapter, ioRatio, ...opts });
|
|
398
390
|
}
|
|
399
391
|
const adapter = ADAPTERS[platform];
|
|
400
392
|
if (!adapter)
|
|
@@ -486,7 +478,7 @@ async function _freshTokendash(platform = "claude") {
|
|
|
486
478
|
if (platform !== "claude") return null;
|
|
487
479
|
const dbPath = join(homedir(), ".claude", "token-dashboard.db");
|
|
488
480
|
// Read the token-dashboard DB directly with sqlite3 (no external python scan
|
|
489
|
-
// needed — the DB is created by
|
|
481
|
+
// needed — the DB is created by Nate's token-dashboard, github.com/nateherkai/token-dashboard).
|
|
490
482
|
// all-time only; the db doesn't expose windowing here.
|
|
491
483
|
if (!existsSync(dbPath)) return null;
|
|
492
484
|
try {
|
package/tools.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { ALL_PLATFORMS } from "./adapters.mjs";
|
|
|
26
26
|
import { ensureIdentity, recordEnrollment } from "./keystore.mjs";
|
|
27
27
|
import { submitSignedWindow } from "./submit.mjs";
|
|
28
28
|
import { createHash } from "node:crypto";
|
|
29
|
-
import { execFile } from "node:child_process";
|
|
29
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
30
30
|
import { existsSync, readFileSync } from "node:fs";
|
|
31
31
|
import os from "node:os";
|
|
32
32
|
import path from "node:path";
|
|
@@ -256,6 +256,56 @@ export const DEFAULT_FETCH_TIMEOUT =
|
|
|
256
256
|
* past this is malformed or abusive — reject cleanly before parsing/POSTing (E2 hardening). */
|
|
257
257
|
const MAX_INPUT = 1_000_000;
|
|
258
258
|
|
|
259
|
+
/** Resolve this package's version for the User-Agent stamp (best-effort). */
|
|
260
|
+
function agentVersionStamp() {
|
|
261
|
+
try {
|
|
262
|
+
const pkg = JSON.parse(
|
|
263
|
+
readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
|
|
264
|
+
);
|
|
265
|
+
return pkg.version;
|
|
266
|
+
} catch {
|
|
267
|
+
return "unknown";
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* curl-based fetch fallback for when Node's fetch is blocked by Vercel's bot
|
|
273
|
+
* protection (TLS fingerprinting). Returns a Response-like object with .ok,
|
|
274
|
+
* .status, .json(), .text(). Uses execFileSync for synchronous curl execution.
|
|
275
|
+
*/
|
|
276
|
+
function curlFetch(url, init = {}, timeoutMs = 10_000) {
|
|
277
|
+
const args = ["-s", "-S", "-w", "\n__HTTP_STATUS__%{http_code}", "--max-time", String(Math.ceil(timeoutMs / 1000))];
|
|
278
|
+
if (init.method) args.push("-X", init.method);
|
|
279
|
+
const headers = init.headers || {};
|
|
280
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
281
|
+
args.push("-H", `${k}: ${v}`);
|
|
282
|
+
}
|
|
283
|
+
if (init.body) {
|
|
284
|
+
args.push("-d", init.body);
|
|
285
|
+
}
|
|
286
|
+
args.push(url);
|
|
287
|
+
let stdout;
|
|
288
|
+
try {
|
|
289
|
+
stdout = execFileSync("curl", args, {
|
|
290
|
+
encoding: "utf-8",
|
|
291
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
292
|
+
timeout: timeoutMs + 2000,
|
|
293
|
+
});
|
|
294
|
+
} catch (e) {
|
|
295
|
+
throw new Error(`curl transport failed: ${e.message}`);
|
|
296
|
+
}
|
|
297
|
+
// Parse the status code from the trailer
|
|
298
|
+
const statusMatch = stdout.match(/__HTTP_STATUS__(\d+)$/);
|
|
299
|
+
const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
|
|
300
|
+
const bodyText = statusMatch ? stdout.slice(0, statusMatch.index) : stdout;
|
|
301
|
+
return {
|
|
302
|
+
ok: status >= 200 && status < 300,
|
|
303
|
+
status,
|
|
304
|
+
json: async () => { try { return JSON.parse(bodyText); } catch { return {}; } },
|
|
305
|
+
text: async () => bodyText,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
259
309
|
// ── Smithery quality: output schemas + annotations ──────────────────────────
|
|
260
310
|
// MCP tool annotations hint to clients about side-effects, read-only status, etc.
|
|
261
311
|
const ANNOTATIONS = {
|
|
@@ -747,8 +797,8 @@ export const TOOLS = [
|
|
|
747
797
|
properties: {
|
|
748
798
|
platform: {
|
|
749
799
|
type: "string",
|
|
750
|
-
enum: ALL_PLATFORMS,
|
|
751
|
-
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).`,
|
|
800
|
+
enum: [...ALL_PLATFORMS, "multi"],
|
|
801
|
+
description: `source platform (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}, multi. 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active). 'devin' reads from ~/.local/share/devin/cli/sessions.db (SQLite, all windows). 'codex' is estimated via io_ratio. 'other' reads from a user-supplied JSON file (set SIGRANK_OTHER_PATH). Some platforms need setup (e.g. copilot requires COPILOT_OTEL_ENABLED=true).`,
|
|
752
802
|
},
|
|
753
803
|
},
|
|
754
804
|
},
|
|
@@ -780,8 +830,8 @@ export const TOOLS = [
|
|
|
780
830
|
},
|
|
781
831
|
platform: {
|
|
782
832
|
type: "string",
|
|
783
|
-
enum: ALL_PLATFORMS,
|
|
784
|
-
description: `Source platform to pull from (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}. Each platform reads its own session logs locally.`,
|
|
833
|
+
enum: [...ALL_PLATFORMS, "multi"],
|
|
834
|
+
description: `Source platform to pull from (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}, multi. 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active). 'devin' reads from ~/.local/share/devin/cli/sessions.db (SQLite, all windows). 'other' reads from a user-supplied JSON file (set SIGRANK_OTHER_PATH). Each platform reads its own session logs locally.`,
|
|
785
835
|
},
|
|
786
836
|
},
|
|
787
837
|
},
|
|
@@ -1357,13 +1407,45 @@ export async function callTool(name, args, opts = {}) {
|
|
|
1357
1407
|
const rawFetch = opts.fetchImpl || fetch;
|
|
1358
1408
|
|
|
1359
1409
|
// Wrap every fetch with an AbortController timeout so a hung network call never
|
|
1360
|
-
// blocks the MCP client indefinitely.
|
|
1361
|
-
|
|
1410
|
+
// blocks the MCP client indefinitely. Vercel's bot protection (security
|
|
1411
|
+
// checkpoint) blocks Node's fetch via TLS fingerprinting — no UA fix helps.
|
|
1412
|
+
// When fetch gets a 403 Vercel challenge, fall back to curl (which passes).
|
|
1413
|
+
const doFetch = async (url, init = {}) => {
|
|
1362
1414
|
const ac = new AbortController();
|
|
1363
1415
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1416
|
+
const headers = {
|
|
1417
|
+
"user-agent": `node/${process.version} sigrank-mcp/${agentVersionStamp()}`,
|
|
1418
|
+
...(init.headers || {}),
|
|
1419
|
+
};
|
|
1420
|
+
try {
|
|
1421
|
+
const res = await rawFetch(url, { ...init, headers, signal: ac.signal });
|
|
1422
|
+
// Detect Vercel security checkpoint (403 + HTML body, not JSON)
|
|
1423
|
+
if (res.status === 403) {
|
|
1424
|
+
const text = await res.text();
|
|
1425
|
+
if (text.includes("Vercel Security Checkpoint") || text.includes("x-vercel-challenge")) {
|
|
1426
|
+
// Fall back to curl transport
|
|
1427
|
+
return curlFetch(url, init, timeoutMs);
|
|
1428
|
+
}
|
|
1429
|
+
// Real 403 from the API — return a mock response object
|
|
1430
|
+
return {
|
|
1431
|
+
ok: false,
|
|
1432
|
+
status: 403,
|
|
1433
|
+
json: async () => { try { return JSON.parse(text); } catch { return {}; } },
|
|
1434
|
+
text: async () => text,
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
return res;
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
if (e.name === "AbortError") throw e;
|
|
1440
|
+
// Network error — try curl as fallback
|
|
1441
|
+
try {
|
|
1442
|
+
return await curlFetch(url, init, timeoutMs);
|
|
1443
|
+
} catch {
|
|
1444
|
+
throw e;
|
|
1445
|
+
}
|
|
1446
|
+
} finally {
|
|
1447
|
+
clearTimeout(timer);
|
|
1448
|
+
}
|
|
1367
1449
|
};
|
|
1368
1450
|
|
|
1369
1451
|
const fetchJson = async (path) => {
|
|
@@ -1405,8 +1487,12 @@ export async function callTool(name, args, opts = {}) {
|
|
|
1405
1487
|
const c = withParseWarnings(pillars, cascade(pillars));
|
|
1406
1488
|
return { ...c, card: narrate(c) };
|
|
1407
1489
|
}
|
|
1408
|
-
if (name === "get_leaderboard")
|
|
1409
|
-
|
|
1490
|
+
if (name === "get_leaderboard") {
|
|
1491
|
+
const params = new URLSearchParams({ metric: "yield_" });
|
|
1492
|
+
if (args?.limit) params.set("limit", String(args.limit));
|
|
1493
|
+
if (args?.window) params.set("window", args.window);
|
|
1494
|
+
return fetchJson(`/api/v1/leaderboard?${params}`);
|
|
1495
|
+
}
|
|
1410
1496
|
if (name === "get_operator") {
|
|
1411
1497
|
const codename = String(args?.codename || "").trim();
|
|
1412
1498
|
if (!codename)
|
|
@@ -1677,6 +1763,67 @@ export async function callTool(name, args, opts = {}) {
|
|
|
1677
1763
|
if (name === "tokenpull") {
|
|
1678
1764
|
// Local read → 4 windows of pillars → cascade each. Token-only, on-device.
|
|
1679
1765
|
const platform = args?.platform || "claude";
|
|
1766
|
+
|
|
1767
|
+
// MULTI: sum every locally-detected platform's pillars per window. Same logic as
|
|
1768
|
+
// submit_verified's multi flow — Devin (cloud, via tokscale) is included.
|
|
1769
|
+
if (platform === "multi") {
|
|
1770
|
+
const detected = [];
|
|
1771
|
+
for (const p of ALL_PLATFORMS) {
|
|
1772
|
+
const r = await pullByPlatform(p, opts).catch(() => null);
|
|
1773
|
+
const live =
|
|
1774
|
+
r &&
|
|
1775
|
+
(r.windows || []).some(
|
|
1776
|
+
(w) =>
|
|
1777
|
+
w.pillars.input +
|
|
1778
|
+
w.pillars.output +
|
|
1779
|
+
w.pillars.cacheCreate +
|
|
1780
|
+
w.pillars.cacheRead >
|
|
1781
|
+
0,
|
|
1782
|
+
);
|
|
1783
|
+
if (live) detected.push(r);
|
|
1784
|
+
}
|
|
1785
|
+
if (detected.length < 2) {
|
|
1786
|
+
return {
|
|
1787
|
+
platform: "multi",
|
|
1788
|
+
status: "skipped",
|
|
1789
|
+
reason: "need_2_platforms",
|
|
1790
|
+
detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
|
|
1791
|
+
windows: [],
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
const winKeys = ["7d", "30d", "90d", "all"];
|
|
1795
|
+
const windows = [];
|
|
1796
|
+
for (const wk of winKeys) {
|
|
1797
|
+
const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
|
|
1798
|
+
let msgs = 0;
|
|
1799
|
+
for (const d of detected) {
|
|
1800
|
+
const w = (d.windows || []).find((x) => x.window === wk);
|
|
1801
|
+
if (!w) continue;
|
|
1802
|
+
sum.input += w.pillars.input || 0;
|
|
1803
|
+
sum.output += w.pillars.output || 0;
|
|
1804
|
+
sum.cacheCreate += w.pillars.cacheCreate || 0;
|
|
1805
|
+
sum.cacheRead += w.pillars.cacheRead || 0;
|
|
1806
|
+
msgs += w.messages || 0;
|
|
1807
|
+
}
|
|
1808
|
+
if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
|
|
1809
|
+
continue;
|
|
1810
|
+
const c = cascade(sum);
|
|
1811
|
+
windows.push({
|
|
1812
|
+
window: wk,
|
|
1813
|
+
messages: msgs,
|
|
1814
|
+
pillars: sum,
|
|
1815
|
+
cascade: c,
|
|
1816
|
+
card: narrate(c, `${wk} multi`),
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
return {
|
|
1820
|
+
platform: "multi",
|
|
1821
|
+
estimated: true,
|
|
1822
|
+
sources: detected.map((d) => d.platform),
|
|
1823
|
+
windows,
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1680
1827
|
const pulled = await pullByPlatform(platform, opts);
|
|
1681
1828
|
const windows = pulled.windows.map((w) => {
|
|
1682
1829
|
const c = cascade(w.pillars);
|
|
@@ -1704,7 +1851,111 @@ export async function callTool(name, args, opts = {}) {
|
|
|
1704
1851
|
// (server re-scores). The board stays platform-agnostic via the 4 pillars; the
|
|
1705
1852
|
// source platform rides along as a tag. Conversion already happened in the adapter.
|
|
1706
1853
|
const codename = String(args?.codename || "").trim();
|
|
1707
|
-
const
|
|
1854
|
+
const platform = args?.platform || "claude";
|
|
1855
|
+
|
|
1856
|
+
// MULTI: same combined cross-platform cascade as submit_verified. Includes Devin
|
|
1857
|
+
// (cloud, via tokscale). Aggregate every locally-detected platform's pillars per
|
|
1858
|
+
// window and publish as platform='multi'.
|
|
1859
|
+
if (platform === "multi") {
|
|
1860
|
+
const detected = [];
|
|
1861
|
+
for (const p of ALL_PLATFORMS) {
|
|
1862
|
+
const r = await pullByPlatform(p, opts).catch(() => null);
|
|
1863
|
+
const live =
|
|
1864
|
+
r &&
|
|
1865
|
+
(r.windows || []).some(
|
|
1866
|
+
(w) =>
|
|
1867
|
+
w.pillars.input +
|
|
1868
|
+
w.pillars.output +
|
|
1869
|
+
w.pillars.cacheCreate +
|
|
1870
|
+
w.pillars.cacheRead >
|
|
1871
|
+
0,
|
|
1872
|
+
);
|
|
1873
|
+
if (live) detected.push(r);
|
|
1874
|
+
}
|
|
1875
|
+
if (detected.length < 2) {
|
|
1876
|
+
return {
|
|
1877
|
+
platform: "multi",
|
|
1878
|
+
codename: codename || undefined,
|
|
1879
|
+
status: "skipped",
|
|
1880
|
+
reason: "need_2_platforms",
|
|
1881
|
+
detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
|
|
1882
|
+
windows: [],
|
|
1883
|
+
};
|
|
1884
|
+
}
|
|
1885
|
+
const winKeys = args?.window ? [args.window] : ["7d", "30d", "90d", "all"];
|
|
1886
|
+
const out = [];
|
|
1887
|
+
for (const wk of winKeys) {
|
|
1888
|
+
const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
|
|
1889
|
+
let msgs = 0;
|
|
1890
|
+
for (const d of detected) {
|
|
1891
|
+
const w = (d.windows || []).find((x) => x.window === wk);
|
|
1892
|
+
if (!w) continue;
|
|
1893
|
+
sum.input += w.pillars.input || 0;
|
|
1894
|
+
sum.output += w.pillars.output || 0;
|
|
1895
|
+
sum.cacheCreate += w.pillars.cacheCreate || 0;
|
|
1896
|
+
sum.cacheRead += w.pillars.cacheRead || 0;
|
|
1897
|
+
msgs += w.messages || 0;
|
|
1898
|
+
}
|
|
1899
|
+
if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
|
|
1900
|
+
continue;
|
|
1901
|
+
const c = cascade(sum);
|
|
1902
|
+
const card = narrate(c, `${wk} multi`);
|
|
1903
|
+
if (!codename) {
|
|
1904
|
+
out.push({
|
|
1905
|
+
window: wk,
|
|
1906
|
+
pillars: sum,
|
|
1907
|
+
cascade: c,
|
|
1908
|
+
card,
|
|
1909
|
+
submission: { status: "not_submitted", reason: "codename_required" },
|
|
1910
|
+
});
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
const rawPaste = `${sum.input} ${sum.output} ${sum.cacheCreate} ${sum.cacheRead}`;
|
|
1914
|
+
const windowType = WINDOW_TYPE[wk] || wk;
|
|
1915
|
+
const stamp = uploadStamp({
|
|
1916
|
+
codename,
|
|
1917
|
+
window: windowType,
|
|
1918
|
+
pillars: sum,
|
|
1919
|
+
platform: "multi",
|
|
1920
|
+
});
|
|
1921
|
+
const res = await doFetch(`${apiBase}/api/v1/ingest-paste`, {
|
|
1922
|
+
method: "POST",
|
|
1923
|
+
headers: {
|
|
1924
|
+
"content-type": "application/json",
|
|
1925
|
+
accept: "application/json",
|
|
1926
|
+
},
|
|
1927
|
+
body: JSON.stringify({
|
|
1928
|
+
codename,
|
|
1929
|
+
raw_paste: rawPaste,
|
|
1930
|
+
window_type: windowType,
|
|
1931
|
+
telemetry: { platform: { primary: "multi" } },
|
|
1932
|
+
...stamp,
|
|
1933
|
+
}),
|
|
1934
|
+
});
|
|
1935
|
+
let ack;
|
|
1936
|
+
try {
|
|
1937
|
+
ack = await res.json();
|
|
1938
|
+
} catch {
|
|
1939
|
+
ack = { status: "parse_error", httpStatus: res.status };
|
|
1940
|
+
}
|
|
1941
|
+
const ranked = ack?.ranked ?? ack?.accepted ?? false;
|
|
1942
|
+
out.push({
|
|
1943
|
+
window: wk,
|
|
1944
|
+
pillars: sum,
|
|
1945
|
+
cascade: c,
|
|
1946
|
+
card,
|
|
1947
|
+
submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
return {
|
|
1951
|
+
platform: "multi",
|
|
1952
|
+
codename: codename || undefined,
|
|
1953
|
+
sources: detected.map((d) => d.platform),
|
|
1954
|
+
windows: out,
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
const pulled = await pullByPlatform(platform, opts);
|
|
1708
1959
|
const targets = args?.window
|
|
1709
1960
|
? pulled.windows.filter((w) => w.window === args.window)
|
|
1710
1961
|
: pulled.windows;
|
|
@@ -2774,6 +3025,7 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2774
3025
|
const top = ops.map((op) => ({
|
|
2775
3026
|
...op,
|
|
2776
3027
|
behavioral_framing: _behavioralFraming(op),
|
|
3028
|
+
competitive: _competitiveLayer(op, board),
|
|
2777
3029
|
}));
|
|
2778
3030
|
|
|
2779
3031
|
const best = top[0];
|
|
@@ -2781,7 +3033,13 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2781
3033
|
? `${best.codename} tops the SigRank leaderboard at Υ ${best.yield_?.toLocaleString?.() || best.yield_} — ${_behavioralFraming(best)}`
|
|
2782
3034
|
: "No operators on the board yet.";
|
|
2783
3035
|
|
|
2784
|
-
return {
|
|
3036
|
+
return {
|
|
3037
|
+
top_operators: top,
|
|
3038
|
+
total_operators: total,
|
|
3039
|
+
summary,
|
|
3040
|
+
cta: "Check my rank",
|
|
3041
|
+
shareable_url: best ? `${DEFAULT_API_BASE}/operator/${encodeURIComponent(best.codename)}` : null,
|
|
3042
|
+
};
|
|
2785
3043
|
}
|
|
2786
3044
|
|
|
2787
3045
|
if (name === "compare_self") {
|
|
@@ -2843,6 +3101,10 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2843
3101
|
|
|
2844
3102
|
const suggestion = _improvementSuggestion(klass, yourMetrics);
|
|
2845
3103
|
|
|
3104
|
+
// Competitive layer per SHARED_DESIGN_DECISIONS.md §3/§4/§5
|
|
3105
|
+
const competitive = _competitiveLayer(yourMetrics, board);
|
|
3106
|
+
const competitiveSummary = _competitiveSummary(yourMetrics, board);
|
|
3107
|
+
|
|
2846
3108
|
return {
|
|
2847
3109
|
your_metrics: yourMetrics,
|
|
2848
3110
|
power_user_assessment: powerUserAssessment,
|
|
@@ -2850,8 +3112,16 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2850
3112
|
your_yield_vs_avg: yieldVsAvg,
|
|
2851
3113
|
your_class_meaning: classMeaning,
|
|
2852
3114
|
percentile,
|
|
3115
|
+
rank: competitive.rank,
|
|
3116
|
+
total_operators: competitive.total_operators,
|
|
3117
|
+
class_tier: competitive.class_tier,
|
|
3118
|
+
delta_from_average: competitive.delta_from_average,
|
|
3119
|
+
delta_from_top: competitive.delta_from_top,
|
|
2853
3120
|
},
|
|
3121
|
+
competitive_summary: competitiveSummary,
|
|
3122
|
+
shareable_url: competitive.shareable_url,
|
|
2854
3123
|
suggestion,
|
|
3124
|
+
cta: "See where I stand",
|
|
2855
3125
|
};
|
|
2856
3126
|
}
|
|
2857
3127
|
|
|
@@ -2863,9 +3133,10 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2863
3133
|
"compare_operators requires both `codename_a` and `codename_b`.",
|
|
2864
3134
|
);
|
|
2865
3135
|
|
|
2866
|
-
const [opA, opB] = await Promise.all([
|
|
3136
|
+
const [opA, opB, board] = await Promise.all([
|
|
2867
3137
|
fetchJson(`/api/v1/operators/${encodeURIComponent(nameA)}`),
|
|
2868
3138
|
fetchJson(`/api/v1/operators/${encodeURIComponent(nameB)}`),
|
|
3139
|
+
fetchJson("/api/v1/leaderboard?metric=yield_"),
|
|
2869
3140
|
]);
|
|
2870
3141
|
|
|
2871
3142
|
const yieldA = opA.yield_ || 0;
|
|
@@ -2884,6 +3155,7 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2884
3155
|
velocity: opA.velocity,
|
|
2885
3156
|
class: opA.class,
|
|
2886
3157
|
rank: opA.rank,
|
|
3158
|
+
competitive: _competitiveLayer(opA, board),
|
|
2887
3159
|
},
|
|
2888
3160
|
operator_b: {
|
|
2889
3161
|
codename: opB.codename,
|
|
@@ -2892,9 +3164,11 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2892
3164
|
velocity: opB.velocity,
|
|
2893
3165
|
class: opB.class,
|
|
2894
3166
|
rank: opB.rank,
|
|
3167
|
+
competitive: _competitiveLayer(opB, board),
|
|
2895
3168
|
},
|
|
2896
3169
|
verdict,
|
|
2897
3170
|
yield_delta: delta,
|
|
3171
|
+
cta: "Compare me to others",
|
|
2898
3172
|
};
|
|
2899
3173
|
}
|
|
2900
3174
|
|
|
@@ -2915,6 +3189,8 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2915
3189
|
{ class: "Burner", meaning: "Early-stage — tokens burned more than compounded. Low leverage, low velocity. The shift: reuse prior context." },
|
|
2916
3190
|
],
|
|
2917
3191
|
link: "https://signalaf.com/score — check your class tier and yield",
|
|
3192
|
+
shareable_url: `${DEFAULT_API_BASE}/score`,
|
|
3193
|
+
cta: "Learn the scoring",
|
|
2918
3194
|
};
|
|
2919
3195
|
}
|
|
2920
3196
|
|
|
@@ -2986,6 +3262,11 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2986
3262
|
|
|
2987
3263
|
const summary = `Your Υ Yield is ${y.toLocaleString()} (${klass}). ${_improvementSuggestion(klass, metrics)}`;
|
|
2988
3264
|
|
|
3265
|
+
// Competitive layer per SHARED_DESIGN_DECISIONS.md §3/§4/§5
|
|
3266
|
+
const board = await fetchJson("/api/v1/leaderboard?metric=yield_");
|
|
3267
|
+
const competitive = _competitiveLayer(metrics, board);
|
|
3268
|
+
const competitiveSummary = _competitiveSummary(metrics, board);
|
|
3269
|
+
|
|
2989
3270
|
return {
|
|
2990
3271
|
your_metrics: {
|
|
2991
3272
|
yield_: y,
|
|
@@ -2993,8 +3274,19 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2993
3274
|
velocity: v,
|
|
2994
3275
|
class: klass,
|
|
2995
3276
|
},
|
|
3277
|
+
competitive: {
|
|
3278
|
+
rank: competitive.rank,
|
|
3279
|
+
total_operators: competitive.total_operators,
|
|
3280
|
+
percentile: competitive.percentile,
|
|
3281
|
+
class_tier: competitive.class_tier,
|
|
3282
|
+
delta_from_average: competitive.delta_from_average,
|
|
3283
|
+
delta_from_top: competitive.delta_from_top,
|
|
3284
|
+
},
|
|
3285
|
+
competitive_summary: competitiveSummary,
|
|
3286
|
+
shareable_url: competitive.shareable_url,
|
|
2996
3287
|
suggestions,
|
|
2997
3288
|
summary,
|
|
3289
|
+
cta: "Improve my score",
|
|
2998
3290
|
};
|
|
2999
3291
|
}
|
|
3000
3292
|
|
|
@@ -3003,6 +3295,90 @@ export async function callTool(name, args, opts = {}) {
|
|
|
3003
3295
|
|
|
3004
3296
|
// ── Intent tool helpers — behavioral framing in power-user language ──────────
|
|
3005
3297
|
|
|
3298
|
+
/**
|
|
3299
|
+
* Competitive layer for tool responses per SHARED_DESIGN_DECISIONS.md §3/§4/§5.
|
|
3300
|
+
* Every tool response that includes operator data must show:
|
|
3301
|
+
* rank, percentile, class_tier, delta_from_average, delta_from_top, shareable_url
|
|
3302
|
+
* Response style: factual + competitive ("You rank #12 of 47 operators...")
|
|
3303
|
+
*/
|
|
3304
|
+
function _competitiveLayer(op, board) {
|
|
3305
|
+
const allOps = Array.isArray(board?.operators || board) ? (board.operators || board) : [];
|
|
3306
|
+
const yields = allOps.map((o) => o.yield_ || 0).sort((a, b) => a - b);
|
|
3307
|
+
const yourYield = op.yield_ || 0;
|
|
3308
|
+
const total = allOps.length;
|
|
3309
|
+
|
|
3310
|
+
// Rank: find operator's position (1-based)
|
|
3311
|
+
let rank = op.rank || null;
|
|
3312
|
+
if (!rank && total > 0) {
|
|
3313
|
+
const sorted = [...allOps].sort((a, b) => (b.yield_ || 0) - (a.yield_ || 0));
|
|
3314
|
+
const idx = sorted.findIndex((o) => o.codename === op.codename);
|
|
3315
|
+
rank = idx >= 0 ? idx + 1 : null;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
// Percentile: % of operators with yield below this operator
|
|
3319
|
+
const percentile = total > 0
|
|
3320
|
+
? Math.round((yields.filter((y) => y < yourYield).length / total) * 100)
|
|
3321
|
+
: 0;
|
|
3322
|
+
|
|
3323
|
+
// Delta from average
|
|
3324
|
+
const avgYield = total > 0 ? yields.reduce((s, y) => s + y, 0) / total : 0;
|
|
3325
|
+
const deltaFromAvg = avgYield > 0
|
|
3326
|
+
? { absolute: Math.round(yourYield - avgYield),
|
|
3327
|
+
percent: Math.round(((yourYield - avgYield) / avgYield) * 100) }
|
|
3328
|
+
: { absolute: 0, percent: 0 };
|
|
3329
|
+
|
|
3330
|
+
// Delta from top operator
|
|
3331
|
+
const topYield = total > 0 ? Math.max(...yields) : 0;
|
|
3332
|
+
const deltaFromTop = topYield > 0
|
|
3333
|
+
? { absolute: Math.round(topYield - yourYield),
|
|
3334
|
+
percent: Math.round(((topYield - yourYield) / topYield) * 100) }
|
|
3335
|
+
: { absolute: 0, percent: 0 };
|
|
3336
|
+
|
|
3337
|
+
// Shareable URL
|
|
3338
|
+
const shareableUrl = op.codename && op.codename !== "you (local)"
|
|
3339
|
+
? `${DEFAULT_API_BASE}/operator/${encodeURIComponent(op.codename)}`
|
|
3340
|
+
: null;
|
|
3341
|
+
|
|
3342
|
+
return {
|
|
3343
|
+
rank,
|
|
3344
|
+
total_operators: total,
|
|
3345
|
+
percentile,
|
|
3346
|
+
class_tier: op.class || "Burner",
|
|
3347
|
+
delta_from_average: deltaFromAvg,
|
|
3348
|
+
delta_from_top: deltaFromTop,
|
|
3349
|
+
shareable_url: shareableUrl,
|
|
3350
|
+
};
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
/** Factual + competitive summary line per SHARED_DESIGN_DECISIONS.md §4 */
|
|
3354
|
+
function _competitiveSummary(op, board) {
|
|
3355
|
+
const cl = _competitiveLayer(op, board);
|
|
3356
|
+
const parts = [];
|
|
3357
|
+
|
|
3358
|
+
if (cl.rank && cl.total_operators > 0) {
|
|
3359
|
+
parts.push(`You rank #${cl.rank} of ${cl.total_operators} operators.`);
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
const topOp = (board?.operators || board || []).reduce(
|
|
3363
|
+
(best, o) => ((o.yield_ || 0) > (best?.yield_ || 0) ? o : best),
|
|
3364
|
+
null,
|
|
3365
|
+
);
|
|
3366
|
+
if (topOp && topOp.codename) {
|
|
3367
|
+
parts.push(`Top operator is ${topOp.codename} with Υ ${(topOp.yield_ || 0).toLocaleString()}.`);
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
if (cl.delta_from_average.percent !== 0) {
|
|
3371
|
+
const dir = cl.delta_from_average.percent > 0 ? "above" : "below";
|
|
3372
|
+
parts.push(`You're ${Math.abs(cl.delta_from_average.percent)}% ${dir} average.`);
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
if (cl.delta_from_top.percent > 0) {
|
|
3376
|
+
parts.push(`${cl.delta_from_top.percent}% below top.`);
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
return parts.join(" ");
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3006
3382
|
function _behavioralFraming(op) {
|
|
3007
3383
|
const y = op.yield_ || 0;
|
|
3008
3384
|
const l = op.leverage || 0;
|
package/tui.mjs
CHANGED
|
@@ -2192,9 +2192,11 @@ export async function runTui({
|
|
|
2192
2192
|
"sign-in is temporarily unavailable — try again shortly.",
|
|
2193
2193
|
};
|
|
2194
2194
|
connectMsg = `${red("✗")} ${reasons[out.reason] || `sign-in failed (${out.reason || "unknown"}).`}`;
|
|
2195
|
+
codeBuf = ""; // auto-clear on failure — the code is consumed/invalid
|
|
2195
2196
|
}
|
|
2196
2197
|
} catch (e) {
|
|
2197
2198
|
connectMsg = `${red("✗")} ${e.message}`;
|
|
2199
|
+
codeBuf = ""; // auto-clear on error too
|
|
2198
2200
|
}
|
|
2199
2201
|
await redraw();
|
|
2200
2202
|
return;
|
|
@@ -2225,13 +2227,13 @@ export async function runTui({
|
|
|
2225
2227
|
await redraw();
|
|
2226
2228
|
return;
|
|
2227
2229
|
} // backspace
|
|
2228
|
-
// [X] sign out / reset — when the code field is empty (so
|
|
2229
|
-
// still feeds the buffer
|
|
2230
|
-
//
|
|
2231
|
-
//
|
|
2232
|
-
//
|
|
2233
|
-
//
|
|
2234
|
-
// (
|
|
2230
|
+
// [X] sign out / reset — only when the code field is empty (so 'x'/'X' typed
|
|
2231
|
+
// mid-code still feeds the buffer — codes can contain X). The auto-clear on
|
|
2232
|
+
// sign-in failure (above) ensures the buffer is empty after a failed attempt,
|
|
2233
|
+
// so X works immediately when the user wants to reset. Drops the local
|
|
2234
|
+
// credential so a server-revoked OR server-enrolled-but-locally-unbound device
|
|
2235
|
+
// stops holding a stale device_id, and the next enroll provisions a fresh
|
|
2236
|
+
// device_id (escapes the Frankenstein-identity / 409-re-enroll deadlock).
|
|
2235
2237
|
if (k === "x" && !codeBuf) {
|
|
2236
2238
|
const wasSignedIn = isSignedIn(loadIdentity());
|
|
2237
2239
|
clearIdentity();
|