sigrank 0.0.177 → 0.0.178
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 +15 -3
- package/adapters.mjs +117 -2
- package/cli.mjs +4 -1
- package/index.mjs +11 -5
- package/package.json +5 -5
- package/tokenpull.mjs +7 -15
- package/tools.mjs +523 -11
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)
|
|
@@ -296,10 +303,10 @@ 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
|
+
**Non-Claude input is estimated** — most non-Claude systems (Codex, Devin, etc.) combine user input + cache write into a single `input_tokens` field, so true fresh input must be derived. The ruleset (applies to ALL non-Claude systems):
|
|
303
310
|
|
|
304
311
|
```
|
|
305
312
|
input = output × ioRatio (ioRatio derived from Claude ratio, else 2.0)
|
|
@@ -307,6 +314,10 @@ cacheCreate = uncached − input (uncached = input_tokens − cached_inp
|
|
|
307
314
|
cacheRead = exact (from logs)
|
|
308
315
|
```
|
|
309
316
|
|
|
317
|
+
- **Beta** = operator's Claude input/output ratio (if Claude data available)
|
|
318
|
+
- **Alpha** = 2.0 default (when no Claude data)
|
|
319
|
+
- Owner-stated average: 7:1:2 (cache:input:output) → input/output ≈ 0.5
|
|
320
|
+
|
|
310
321
|
Verifier numbers (ccusage/tokscale for codex) show **raw uncached input** (`input_tokens − cached`) — a different field than the estimated input above. The discrepancy is expected and explained inline in the dashboard.
|
|
311
322
|
|
|
312
323
|
---
|
|
@@ -319,6 +330,7 @@ All adapters are token-only (no message content, no cost fields, no credentials)
|
|
|
319
330
|
| ------------------ | ---------------------------------------------- | --------------------------------------------------------------- |
|
|
320
331
|
| Claude Code | ✅ `~/.claude/projects` | Native; dedup by `(session_id, message_id)`; subagents included |
|
|
321
332
|
| Codex | ✅ `~/.codex/sessions` | Estimated input via `io_ratio`; verified vs ccusage |
|
|
333
|
+
| Devin CLI | ✅ `~/.local/share/devin/cli/sessions.db` | Estimated input via `io_ratio`; SQLite; same split as Codex |
|
|
322
334
|
| Amp | ✅ `~/.local/share/amp/threads` | Full 4-pillar; per-message |
|
|
323
335
|
| Kimi | ✅ `~/.kimi/sessions` | Full 4-pillar; `StatusUpdate` lines only |
|
|
324
336
|
| pi-agent | ✅ `~/.pi/agent/sessions` | Full 4-pillar; per-message JSONL |
|
package/adapters.mjs
CHANGED
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
* messages(root): async generator → { id?, sid?, ts, input, output, cacheCreate, cacheRead, file }
|
|
16
16
|
*
|
|
17
17
|
* SigRank-specific mapping rules (applied consistently across all adapters):
|
|
18
|
+
* - Claude = parsed natively (4 pillars exact, no conversion)
|
|
19
|
+
* - Non-Claude systems = combined input → split via ioRatio:
|
|
20
|
+
* input = floor(output × ioRatio) // Beta from Claude, else Alpha 2.0
|
|
21
|
+
* cacheCreate = max(0, uncached − input)
|
|
22
|
+
* cacheRead = exact
|
|
23
|
+
* (Adapters that yield {ts, output, cacheRead, uncached} via records() go through
|
|
24
|
+
* tokenpullCodex(); adapters that yield native 4-pillar via messages() go through
|
|
25
|
+
* tokenpull() directly. The split happens at the tokenpullAny() routing layer.)
|
|
18
26
|
* - Reasoning / thinking tokens → folded into `output` (they are output-side spend)
|
|
19
27
|
* - No cache-creation data available → cacheCreate: 0 + adapter sets `estimated: true`
|
|
20
28
|
* - Cost fields (USD) → NEVER used or forwarded (SigRank scores cost efficiency from
|
|
@@ -97,10 +105,11 @@ function* parseJsonl(text, filePath) {
|
|
|
97
105
|
/** Run sqlite3 -json and return parsed rows, or [] on error/unavailability.
|
|
98
106
|
* execFile with an args array — no shell, so dbPath/sql need no quoting/escaping
|
|
99
107
|
* (matches the execFile hardening used in tools.mjs / tokenpull.mjs). */
|
|
100
|
-
async function sqliteJson(dbPath, sql) {
|
|
108
|
+
async function sqliteJson(dbPath, sql, timeoutMs = 10_000) {
|
|
101
109
|
try {
|
|
102
110
|
const { stdout } = await execFileP("sqlite3", ["-json", dbPath, sql], {
|
|
103
|
-
timeout:
|
|
111
|
+
timeout: timeoutMs,
|
|
112
|
+
maxBuffer: 256 * 1024 * 1024, // 256MB — Devin's sessions.db yields ~15MB JSON
|
|
104
113
|
});
|
|
105
114
|
return JSON.parse(stdout || "[]");
|
|
106
115
|
} catch {
|
|
@@ -729,6 +738,110 @@ export const hermesAdapter = {
|
|
|
729
738
|
},
|
|
730
739
|
};
|
|
731
740
|
|
|
741
|
+
// ── 14. Devin CLI ────────────────────────────────────────────────────────────
|
|
742
|
+
// SQLite: ~/.local/share/devin/cli/sessions.db
|
|
743
|
+
// Same combined-input problem as Codex: input_tokens INCLUDES cache write, so we
|
|
744
|
+
// yield { ts, output, cacheRead, uncached } and let tokenpullCodex() do the
|
|
745
|
+
// ioRatio split (input = output × ioRatio, cacheCreate = uncached − input).
|
|
746
|
+
// ioRatio comes from Claude (Beta) or the 7:1:2 average (Alpha = 0.5).
|
|
747
|
+
export const devinAdapter = {
|
|
748
|
+
platform: "devin",
|
|
749
|
+
defaultRoot: () => join(homedir(), ".local", "share", "devin", "cli"),
|
|
750
|
+
async *records(root) {
|
|
751
|
+
for (const r of roots("DEVIN_HOME", root)) {
|
|
752
|
+
const dbPath = join(r, "sessions.db");
|
|
753
|
+
const rows = await sqliteJson(
|
|
754
|
+
dbPath,
|
|
755
|
+
`SELECT row_id, session_id,
|
|
756
|
+
json_extract(chat_message, '$.metadata.metrics.input_tokens') as input_tokens,
|
|
757
|
+
json_extract(chat_message, '$.metadata.metrics.output_tokens') as output_tokens,
|
|
758
|
+
json_extract(chat_message, '$.metadata.metrics.cache_read_tokens') as cache_read_tokens,
|
|
759
|
+
json_extract(chat_message, '$.metadata.created_at') as created_at
|
|
760
|
+
FROM message_nodes
|
|
761
|
+
WHERE json_extract(chat_message, '$.role') = 'assistant'
|
|
762
|
+
AND json_extract(chat_message, '$.metadata.metrics.input_tokens') IS NOT NULL
|
|
763
|
+
ORDER BY created_at`,
|
|
764
|
+
60_000,
|
|
765
|
+
);
|
|
766
|
+
for (const row of rows) {
|
|
767
|
+
const inputIncl = Number(row.input_tokens || 0);
|
|
768
|
+
const cached = Number(row.cache_read_tokens || 0);
|
|
769
|
+
const output = Number(row.output_tokens || 0);
|
|
770
|
+
if (inputIncl + output + cached === 0) continue;
|
|
771
|
+
yield {
|
|
772
|
+
ts: row.created_at || null,
|
|
773
|
+
output,
|
|
774
|
+
cacheRead: cached,
|
|
775
|
+
uncached: Math.max(0, inputIncl - cached),
|
|
776
|
+
file: dbPath,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
// ── 15. Other (user-supplied JSON) ───────────────────────────────────────────
|
|
784
|
+
// For platforms/models not yet on the adapter list. The user writes a JSON file
|
|
785
|
+
// with the 4 pillars per window and points SIGRANK_OTHER_PATH at it. This lets
|
|
786
|
+
// anyone with a new tool submit without needing a custom adapter.
|
|
787
|
+
//
|
|
788
|
+
// JSON format (all fields optional, missing windows are skipped):
|
|
789
|
+
// {
|
|
790
|
+
// "platform": "my-tool", // optional, defaults to "other"
|
|
791
|
+
// "windows": {
|
|
792
|
+
// "all": { "input": 1000, "output": 2000, "cacheCreate": 500, "cacheRead": 8000 },
|
|
793
|
+
// "30d": { "input": 500, "output": 1000, "cacheCreate": 200, "cacheRead": 4000 },
|
|
794
|
+
// "7d": { "input": 100, "output": 200, "cacheCreate": 50, "cacheRead": 800 }
|
|
795
|
+
// }
|
|
796
|
+
// }
|
|
797
|
+
//
|
|
798
|
+
// The adapter yields one "message" per window with the summed pillars. Since
|
|
799
|
+
// there's no per-message timestamp, all data lands in the "all" window only
|
|
800
|
+
// (same as null-timestamp records in other adapters).
|
|
801
|
+
export const otherAdapter = {
|
|
802
|
+
platform: "other",
|
|
803
|
+
defaultRoot: () => process.env.SIGRANK_OTHER_PATH || "",
|
|
804
|
+
async *messages(root) {
|
|
805
|
+
const filePath = root || process.env.SIGRANK_OTHER_PATH;
|
|
806
|
+
if (!filePath) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
"other adapter: set SIGRANK_OTHER_PATH to a JSON file with your token pillars. " +
|
|
809
|
+
'Format: { "windows": { "all": { "input": N, "output": N, "cacheCreate": N, "cacheRead": N } } }',
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
let text;
|
|
813
|
+
try {
|
|
814
|
+
text = await readFile(filePath, "utf8");
|
|
815
|
+
} catch {
|
|
816
|
+
throw new Error(`other adapter: cannot read ${filePath} — check SIGRANK_OTHER_PATH`);
|
|
817
|
+
}
|
|
818
|
+
let data;
|
|
819
|
+
try {
|
|
820
|
+
data = JSON.parse(text);
|
|
821
|
+
} catch {
|
|
822
|
+
throw new Error(`other adapter: invalid JSON in ${filePath}`);
|
|
823
|
+
}
|
|
824
|
+
const windows = data.windows || {};
|
|
825
|
+
for (const [win, pillars] of Object.entries(windows)) {
|
|
826
|
+
const input = Number(pillars.input || 0);
|
|
827
|
+
const output = Number(pillars.output || 0);
|
|
828
|
+
const cacheCreate = Number(pillars.cacheCreate || pillars.cache_create || 0);
|
|
829
|
+
const cacheRead = Number(pillars.cacheRead || pillars.cache_read || 0);
|
|
830
|
+
if (input + output + cacheCreate + cacheRead === 0) continue;
|
|
831
|
+
yield {
|
|
832
|
+
id: `other:${win}`,
|
|
833
|
+
sid: `other-${win}`,
|
|
834
|
+
ts: null, // no timestamp → lands in "all" window only
|
|
835
|
+
input,
|
|
836
|
+
output,
|
|
837
|
+
cacheCreate,
|
|
838
|
+
cacheRead,
|
|
839
|
+
file: filePath,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
|
|
732
845
|
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
733
846
|
/** All non-Claude, non-Codex adapters keyed by platform ID. */
|
|
734
847
|
export const ADAPTERS = {
|
|
@@ -745,6 +858,8 @@ export const ADAPTERS = {
|
|
|
745
858
|
goose: gooseAdapter,
|
|
746
859
|
kilo: kiloAdapter,
|
|
747
860
|
hermes: hermesAdapter,
|
|
861
|
+
devin: devinAdapter,
|
|
862
|
+
other: otherAdapter,
|
|
748
863
|
};
|
|
749
864
|
|
|
750
865
|
export const ALL_PLATFORMS = Object.keys(ADAPTERS).concat(["claude", "codex"]);
|
package/cli.mjs
CHANGED
|
@@ -494,7 +494,7 @@ function appPillars() {
|
|
|
494
494
|
async function tokenDashPillars() {
|
|
495
495
|
const dbPath = path.join(os.homedir(), ".claude", "token-dashboard.db");
|
|
496
496
|
if (!existsSync(dbPath)) return null;
|
|
497
|
-
// Read directly with sqlite3 —
|
|
497
|
+
// Read directly with sqlite3 — DB created by Nate's token-dashboard (github.com/nateherkai/token-dashboard).
|
|
498
498
|
// Windowed queries using sqlite3's datetime() function.
|
|
499
499
|
const cf =
|
|
500
500
|
"(model LIKE '%claude%' OR model LIKE '%fable%' OR model LIKE '%sonnet%' OR model LIKE '%opus%' OR model LIKE '%haiku%')";
|
|
@@ -937,6 +937,7 @@ async function runWatch({
|
|
|
937
937
|
const ALL_PLATFORMS = [
|
|
938
938
|
"claude",
|
|
939
939
|
"codex",
|
|
940
|
+
"devin",
|
|
940
941
|
"amp",
|
|
941
942
|
"gemini",
|
|
942
943
|
"kimi",
|
|
@@ -947,8 +948,10 @@ const ALL_PLATFORMS = [
|
|
|
947
948
|
"droid",
|
|
948
949
|
"codebuff",
|
|
949
950
|
"copilot",
|
|
951
|
+
"opencode",
|
|
950
952
|
"openclaw",
|
|
951
953
|
"pi",
|
|
954
|
+
"other",
|
|
952
955
|
];
|
|
953
956
|
|
|
954
957
|
async function runSigRank() {
|
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.0.
|
|
3
|
+
"version": "0.0.178",
|
|
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",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"start": "node index.mjs",
|
|
27
|
-
"test": "node test.mjs && node sign.test.mjs && node --test __tests__/cascade.test.mjs __tests__/badges.test.mjs"
|
|
27
|
+
"test": "node test.mjs && node sign.test.mjs && node --test __tests__/cascade.test.mjs __tests__/badges.test.mjs",
|
|
28
|
+
"sync:platforms": "node scripts/sync-platforms.mjs"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
30
31
|
"index.mjs",
|
|
@@ -64,8 +65,7 @@
|
|
|
64
65
|
],
|
|
65
66
|
"dependencies": {
|
|
66
67
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"tokscale": "^4.0.4"
|
|
68
|
+
"ccusage": "^20.0.17",
|
|
69
|
+
"tokscale": "^4.5.2"
|
|
70
70
|
}
|
|
71
71
|
}
|
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
|
@@ -797,8 +797,8 @@ export const TOOLS = [
|
|
|
797
797
|
properties: {
|
|
798
798
|
platform: {
|
|
799
799
|
type: "string",
|
|
800
|
-
enum: ALL_PLATFORMS,
|
|
801
|
-
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).`,
|
|
802
802
|
},
|
|
803
803
|
},
|
|
804
804
|
},
|
|
@@ -830,8 +830,8 @@ export const TOOLS = [
|
|
|
830
830
|
},
|
|
831
831
|
platform: {
|
|
832
832
|
type: "string",
|
|
833
|
-
enum: ALL_PLATFORMS,
|
|
834
|
-
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.`,
|
|
835
835
|
},
|
|
836
836
|
},
|
|
837
837
|
},
|
|
@@ -1362,6 +1362,36 @@ export const TOOLS = [
|
|
|
1362
1362
|
},
|
|
1363
1363
|
},
|
|
1364
1364
|
},
|
|
1365
|
+
{
|
|
1366
|
+
name: "tokscale_breakdown",
|
|
1367
|
+
description:
|
|
1368
|
+
"Show a per-model breakdown of your token usage across all platforms detected by tokscale. Models under the threshold (default 1%) are lumped into 'other' to keep the display clean. Useful for seeing which models you actually use per platform (e.g. claude-opus-4-8 76%, claude-sonnet-4-6 11%, other 0.3%). Returns { platform: [{ model, input, output, cacheRead, cacheCreate, pct }] }.",
|
|
1369
|
+
annotations: {
|
|
1370
|
+
title: "Model breakdown",
|
|
1371
|
+
...ANNOTATIONS.readOnlyHint,
|
|
1372
|
+
...ANNOTATIONS.openWorldHint,
|
|
1373
|
+
},
|
|
1374
|
+
inputSchema: {
|
|
1375
|
+
type: "object",
|
|
1376
|
+
properties: {
|
|
1377
|
+
threshold: {
|
|
1378
|
+
type: "number",
|
|
1379
|
+
description:
|
|
1380
|
+
"Models below this fraction of their platform's total tokens are lumped into 'other'. Default 0.01 (1%). Set 0 to see every model.",
|
|
1381
|
+
default: 0.01,
|
|
1382
|
+
},
|
|
1383
|
+
},
|
|
1384
|
+
},
|
|
1385
|
+
outputSchema: {
|
|
1386
|
+
type: "object",
|
|
1387
|
+
properties: {
|
|
1388
|
+
platforms: {
|
|
1389
|
+
type: "object",
|
|
1390
|
+
description: "Map of platform name → array of { model, input, output, cacheRead, cacheCreate, pct }",
|
|
1391
|
+
},
|
|
1392
|
+
},
|
|
1393
|
+
},
|
|
1394
|
+
},
|
|
1365
1395
|
];
|
|
1366
1396
|
|
|
1367
1397
|
// tokenpull window key → the board's window_type enum.
|
|
@@ -1383,9 +1413,191 @@ const _lastWatchSubmitAt = new Map();
|
|
|
1383
1413
|
// keeps only platforms with real local data, and sorts claude → codex → rest.
|
|
1384
1414
|
// Pass `platforms` to scope it (e.g. ['claude'] for a fast first paint).
|
|
1385
1415
|
export async function pullActivePlatforms({ platforms } = {}, opts = {}) {
|
|
1386
|
-
|
|
1416
|
+
// If the caller specified explicit platforms, use those.
|
|
1417
|
+
if (platforms && platforms.length) {
|
|
1418
|
+
return _pullExplicit(platforms, opts);
|
|
1419
|
+
}
|
|
1420
|
+
// Auto-detect: run tokscale once to discover all active clients, then pull
|
|
1421
|
+
// only those. This is much faster than trying all 17 adapters (most fail
|
|
1422
|
+
// silently on a machine that doesn't have that tool installed).
|
|
1423
|
+
//
|
|
1424
|
+
// Flow: tokscale surfaces ALL clients → map to our platform names →
|
|
1425
|
+
// ccusage is primary for Claude (more accurate) → other platforms use
|
|
1426
|
+
// their adapters. Anything in tokscale NOT in ccusage gets included.
|
|
1427
|
+
const detected = await _tokscaleDetectClients().catch(() => null);
|
|
1428
|
+
if (detected && detected.length > 0) {
|
|
1429
|
+
return _pullExplicit(detected, opts);
|
|
1430
|
+
}
|
|
1431
|
+
// Fallback: tokscale not installed or failed → try all adapters (old behavior)
|
|
1432
|
+
return _pullExplicit(ALL_PLATFORMS, opts);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/** Run `tokscale models --json` and return the list of our platform names. */
|
|
1436
|
+
async function _tokscaleDetectClients() {
|
|
1437
|
+
const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
|
|
1438
|
+
const data = JSON.parse(raw);
|
|
1439
|
+
const entries = Array.isArray(data?.entries)
|
|
1440
|
+
? data.entries
|
|
1441
|
+
: Array.isArray(data)
|
|
1442
|
+
? data
|
|
1443
|
+
: [];
|
|
1444
|
+
if (!entries.length) return [];
|
|
1445
|
+
// Complete tokscale client → our platform name mapping.
|
|
1446
|
+
// Tokscale supports 40 clients (as of v3.1.0). We have native adapters for 16;
|
|
1447
|
+
// the rest map to "other" (user-supplied JSON via SIGRANK_OTHER_PATH).
|
|
1448
|
+
// Source: https://github.com/junhoyeo/tokscale
|
|
1449
|
+
const TOKSCALE_CLIENT_MAP = {
|
|
1450
|
+
// Native adapters
|
|
1451
|
+
claude: "claude",
|
|
1452
|
+
codex: "codex",
|
|
1453
|
+
"devin-cli": "devin",
|
|
1454
|
+
"devin-desktop": "devin",
|
|
1455
|
+
gemini: "gemini",
|
|
1456
|
+
amp: "amp",
|
|
1457
|
+
kimi: "kimi",
|
|
1458
|
+
qwen: "qwen",
|
|
1459
|
+
goose: "goose",
|
|
1460
|
+
kilo: "kilo",
|
|
1461
|
+
kilocode: "kilo",
|
|
1462
|
+
hermes: "hermes",
|
|
1463
|
+
droid: "droid",
|
|
1464
|
+
codebuff: "codebuff",
|
|
1465
|
+
copilot: "copilot",
|
|
1466
|
+
opencode: "opencode",
|
|
1467
|
+
openclaw: "openclaw",
|
|
1468
|
+
pi: "pi",
|
|
1469
|
+
// No native adapter yet → "other"
|
|
1470
|
+
cursor: "other",
|
|
1471
|
+
roocode: "other",
|
|
1472
|
+
mux: "other",
|
|
1473
|
+
crush: "other",
|
|
1474
|
+
antigravity: "other",
|
|
1475
|
+
"antigravity-cli": "other",
|
|
1476
|
+
zed: "other",
|
|
1477
|
+
kiro: "other",
|
|
1478
|
+
trae: "other",
|
|
1479
|
+
warp: "other",
|
|
1480
|
+
cline: "other",
|
|
1481
|
+
gjc: "other",
|
|
1482
|
+
grok: "other",
|
|
1483
|
+
jcode: "other",
|
|
1484
|
+
commandcode: "other",
|
|
1485
|
+
micode: "other",
|
|
1486
|
+
junie: "other",
|
|
1487
|
+
zcode: "other",
|
|
1488
|
+
opencodereview: "other",
|
|
1489
|
+
codebuddy: "other",
|
|
1490
|
+
workbuddy: "other",
|
|
1491
|
+
// synthetic = re-attributed from other sources, skip
|
|
1492
|
+
synthetic: null,
|
|
1493
|
+
};
|
|
1494
|
+
const clients = new Set();
|
|
1495
|
+
for (const e of entries) {
|
|
1496
|
+
if (!e || !e.client) continue;
|
|
1497
|
+
if (e.model === "<synthetic>" || e.model === "unknown") continue;
|
|
1498
|
+
const input = Number(e.input) || 0;
|
|
1499
|
+
const output = Number(e.output) || 0;
|
|
1500
|
+
if (input + output === 0) continue;
|
|
1501
|
+
const platform = TOKSCALE_CLIENT_MAP[e.client];
|
|
1502
|
+
if (platform === null) continue; // synthetic → skip
|
|
1503
|
+
clients.add(platform || "other");
|
|
1504
|
+
}
|
|
1505
|
+
return [...clients];
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Run `tokscale models --json` and return a model-level breakdown per platform.
|
|
1510
|
+
* Models under `threshold` (default 1%) of their platform's total are lumped
|
|
1511
|
+
* into "other" to keep the display clean.
|
|
1512
|
+
*
|
|
1513
|
+
* Returns: { platform: [{ model, input, output, cacheRead, cacheCreate, pct }] }
|
|
1514
|
+
*/
|
|
1515
|
+
export async function tokscaleModelBreakdown(threshold = 0.01) {
|
|
1516
|
+
const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
|
|
1517
|
+
const data = JSON.parse(raw);
|
|
1518
|
+
const entries = Array.isArray(data?.entries)
|
|
1519
|
+
? data.entries
|
|
1520
|
+
: Array.isArray(data)
|
|
1521
|
+
? data
|
|
1522
|
+
: [];
|
|
1523
|
+
if (!entries.length) return {};
|
|
1524
|
+
|
|
1525
|
+
const TOKSCALE_CLIENT_MAP = {
|
|
1526
|
+
claude: "claude", codex: "codex", "devin-cli": "devin",
|
|
1527
|
+
"devin-desktop": "devin", gemini: "gemini", amp: "amp",
|
|
1528
|
+
kimi: "kimi", qwen: "qwen", goose: "goose", kilo: "kilo",
|
|
1529
|
+
kilocode: "kilo", hermes: "hermes", droid: "droid",
|
|
1530
|
+
codebuff: "codebuff", copilot: "copilot", opencode: "opencode",
|
|
1531
|
+
openclaw: "openclaw", pi: "pi",
|
|
1532
|
+
cursor: "other", roocode: "other", mux: "other", crush: "other",
|
|
1533
|
+
antigravity: "other", "antigravity-cli": "other", zed: "other",
|
|
1534
|
+
kiro: "other", trae: "other", warp: "other", cline: "other",
|
|
1535
|
+
gjc: "other", grok: "other", jcode: "other", commandcode: "other",
|
|
1536
|
+
micode: "other", junie: "other", zcode: "other",
|
|
1537
|
+
opencodereview: "other", codebuddy: "other", workbuddy: "other",
|
|
1538
|
+
synthetic: null,
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
// Group entries by our platform name
|
|
1542
|
+
const byPlatform = {};
|
|
1543
|
+
for (const e of entries) {
|
|
1544
|
+
if (!e || !e.client) continue;
|
|
1545
|
+
if (e.model === "<synthetic>" || e.model === "unknown") continue;
|
|
1546
|
+
const input = Number(e.input) || 0;
|
|
1547
|
+
const output = Number(e.output) || 0;
|
|
1548
|
+
if (input + output === 0) continue;
|
|
1549
|
+
const platform = TOKSCALE_CLIENT_MAP[e.client];
|
|
1550
|
+
if (platform === null) continue;
|
|
1551
|
+
const p = platform || "other";
|
|
1552
|
+
if (!byPlatform[p]) byPlatform[p] = [];
|
|
1553
|
+
byPlatform[p].push({
|
|
1554
|
+
model: e.model,
|
|
1555
|
+
input,
|
|
1556
|
+
output,
|
|
1557
|
+
cacheRead: Number(e.cacheRead) || 0,
|
|
1558
|
+
cacheCreate: Number(e.cacheWrite) || 0,
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// For each platform: sort by total tokens desc, lump small models into "other"
|
|
1563
|
+
const result = {};
|
|
1564
|
+
for (const [platform, models] of Object.entries(byPlatform)) {
|
|
1565
|
+
const total = models.reduce(
|
|
1566
|
+
(s, m) => s + m.input + m.output + m.cacheRead + m.cacheCreate,
|
|
1567
|
+
0,
|
|
1568
|
+
);
|
|
1569
|
+
if (total === 0) continue;
|
|
1570
|
+
const sorted = models.sort(
|
|
1571
|
+
(a, b) =>
|
|
1572
|
+
b.input + b.output + b.cacheRead + b.cacheCreate -
|
|
1573
|
+
(a.input + a.output + a.cacheRead + a.cacheCreate),
|
|
1574
|
+
);
|
|
1575
|
+
const big = [];
|
|
1576
|
+
const small = { model: "other", input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
1577
|
+
for (const m of sorted) {
|
|
1578
|
+
const t = m.input + m.output + m.cacheRead + m.cacheCreate;
|
|
1579
|
+
if (t / total < threshold) {
|
|
1580
|
+
small.input += m.input;
|
|
1581
|
+
small.output += m.output;
|
|
1582
|
+
small.cacheRead += m.cacheRead;
|
|
1583
|
+
small.cacheCreate += m.cacheCreate;
|
|
1584
|
+
} else {
|
|
1585
|
+
big.push({ ...m, pct: t / total });
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
if (small.input + small.output + small.cacheRead + small.cacheCreate > 0) {
|
|
1589
|
+
const t = small.input + small.output + small.cacheRead + small.cacheCreate;
|
|
1590
|
+
big.push({ ...small, pct: t / total });
|
|
1591
|
+
}
|
|
1592
|
+
result[platform] = big;
|
|
1593
|
+
}
|
|
1594
|
+
return result;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
/** Pull a specific set of platforms in parallel, filter to active ones. */
|
|
1598
|
+
async function _pullExplicit(platforms, opts = {}) {
|
|
1387
1599
|
const settled = await Promise.allSettled(
|
|
1388
|
-
|
|
1600
|
+
platforms.map((p) => callTool("tokenpull", { platform: p }, opts)),
|
|
1389
1601
|
);
|
|
1390
1602
|
const active = settled
|
|
1391
1603
|
.filter((r) => r.status === "fulfilled" && r.value)
|
|
@@ -1487,8 +1699,12 @@ export async function callTool(name, args, opts = {}) {
|
|
|
1487
1699
|
const c = withParseWarnings(pillars, cascade(pillars));
|
|
1488
1700
|
return { ...c, card: narrate(c) };
|
|
1489
1701
|
}
|
|
1490
|
-
if (name === "get_leaderboard")
|
|
1491
|
-
|
|
1702
|
+
if (name === "get_leaderboard") {
|
|
1703
|
+
const params = new URLSearchParams({ metric: "yield_" });
|
|
1704
|
+
if (args?.limit) params.set("limit", String(args.limit));
|
|
1705
|
+
if (args?.window) params.set("window", args.window);
|
|
1706
|
+
return fetchJson(`/api/v1/leaderboard?${params}`);
|
|
1707
|
+
}
|
|
1492
1708
|
if (name === "get_operator") {
|
|
1493
1709
|
const codename = String(args?.codename || "").trim();
|
|
1494
1710
|
if (!codename)
|
|
@@ -1759,6 +1975,67 @@ export async function callTool(name, args, opts = {}) {
|
|
|
1759
1975
|
if (name === "tokenpull") {
|
|
1760
1976
|
// Local read → 4 windows of pillars → cascade each. Token-only, on-device.
|
|
1761
1977
|
const platform = args?.platform || "claude";
|
|
1978
|
+
|
|
1979
|
+
// MULTI: sum every locally-detected platform's pillars per window. Same logic as
|
|
1980
|
+
// submit_verified's multi flow — Devin (cloud, via tokscale) is included.
|
|
1981
|
+
if (platform === "multi") {
|
|
1982
|
+
const detected = [];
|
|
1983
|
+
for (const p of ALL_PLATFORMS) {
|
|
1984
|
+
const r = await pullByPlatform(p, opts).catch(() => null);
|
|
1985
|
+
const live =
|
|
1986
|
+
r &&
|
|
1987
|
+
(r.windows || []).some(
|
|
1988
|
+
(w) =>
|
|
1989
|
+
w.pillars.input +
|
|
1990
|
+
w.pillars.output +
|
|
1991
|
+
w.pillars.cacheCreate +
|
|
1992
|
+
w.pillars.cacheRead >
|
|
1993
|
+
0,
|
|
1994
|
+
);
|
|
1995
|
+
if (live) detected.push(r);
|
|
1996
|
+
}
|
|
1997
|
+
if (detected.length < 2) {
|
|
1998
|
+
return {
|
|
1999
|
+
platform: "multi",
|
|
2000
|
+
status: "skipped",
|
|
2001
|
+
reason: "need_2_platforms",
|
|
2002
|
+
detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
|
|
2003
|
+
windows: [],
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
2006
|
+
const winKeys = ["7d", "30d", "90d", "all"];
|
|
2007
|
+
const windows = [];
|
|
2008
|
+
for (const wk of winKeys) {
|
|
2009
|
+
const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
|
|
2010
|
+
let msgs = 0;
|
|
2011
|
+
for (const d of detected) {
|
|
2012
|
+
const w = (d.windows || []).find((x) => x.window === wk);
|
|
2013
|
+
if (!w) continue;
|
|
2014
|
+
sum.input += w.pillars.input || 0;
|
|
2015
|
+
sum.output += w.pillars.output || 0;
|
|
2016
|
+
sum.cacheCreate += w.pillars.cacheCreate || 0;
|
|
2017
|
+
sum.cacheRead += w.pillars.cacheRead || 0;
|
|
2018
|
+
msgs += w.messages || 0;
|
|
2019
|
+
}
|
|
2020
|
+
if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
|
|
2021
|
+
continue;
|
|
2022
|
+
const c = cascade(sum);
|
|
2023
|
+
windows.push({
|
|
2024
|
+
window: wk,
|
|
2025
|
+
messages: msgs,
|
|
2026
|
+
pillars: sum,
|
|
2027
|
+
cascade: c,
|
|
2028
|
+
card: narrate(c, `${wk} multi`),
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
return {
|
|
2032
|
+
platform: "multi",
|
|
2033
|
+
estimated: true,
|
|
2034
|
+
sources: detected.map((d) => d.platform),
|
|
2035
|
+
windows,
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1762
2039
|
const pulled = await pullByPlatform(platform, opts);
|
|
1763
2040
|
const windows = pulled.windows.map((w) => {
|
|
1764
2041
|
const c = cascade(w.pillars);
|
|
@@ -1786,7 +2063,111 @@ export async function callTool(name, args, opts = {}) {
|
|
|
1786
2063
|
// (server re-scores). The board stays platform-agnostic via the 4 pillars; the
|
|
1787
2064
|
// source platform rides along as a tag. Conversion already happened in the adapter.
|
|
1788
2065
|
const codename = String(args?.codename || "").trim();
|
|
1789
|
-
const
|
|
2066
|
+
const platform = args?.platform || "claude";
|
|
2067
|
+
|
|
2068
|
+
// MULTI: same combined cross-platform cascade as submit_verified. Includes Devin
|
|
2069
|
+
// (cloud, via tokscale). Aggregate every locally-detected platform's pillars per
|
|
2070
|
+
// window and publish as platform='multi'.
|
|
2071
|
+
if (platform === "multi") {
|
|
2072
|
+
const detected = [];
|
|
2073
|
+
for (const p of ALL_PLATFORMS) {
|
|
2074
|
+
const r = await pullByPlatform(p, opts).catch(() => null);
|
|
2075
|
+
const live =
|
|
2076
|
+
r &&
|
|
2077
|
+
(r.windows || []).some(
|
|
2078
|
+
(w) =>
|
|
2079
|
+
w.pillars.input +
|
|
2080
|
+
w.pillars.output +
|
|
2081
|
+
w.pillars.cacheCreate +
|
|
2082
|
+
w.pillars.cacheRead >
|
|
2083
|
+
0,
|
|
2084
|
+
);
|
|
2085
|
+
if (live) detected.push(r);
|
|
2086
|
+
}
|
|
2087
|
+
if (detected.length < 2) {
|
|
2088
|
+
return {
|
|
2089
|
+
platform: "multi",
|
|
2090
|
+
codename: codename || undefined,
|
|
2091
|
+
status: "skipped",
|
|
2092
|
+
reason: "need_2_platforms",
|
|
2093
|
+
detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
|
|
2094
|
+
windows: [],
|
|
2095
|
+
};
|
|
2096
|
+
}
|
|
2097
|
+
const winKeys = args?.window ? [args.window] : ["7d", "30d", "90d", "all"];
|
|
2098
|
+
const out = [];
|
|
2099
|
+
for (const wk of winKeys) {
|
|
2100
|
+
const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
|
|
2101
|
+
let msgs = 0;
|
|
2102
|
+
for (const d of detected) {
|
|
2103
|
+
const w = (d.windows || []).find((x) => x.window === wk);
|
|
2104
|
+
if (!w) continue;
|
|
2105
|
+
sum.input += w.pillars.input || 0;
|
|
2106
|
+
sum.output += w.pillars.output || 0;
|
|
2107
|
+
sum.cacheCreate += w.pillars.cacheCreate || 0;
|
|
2108
|
+
sum.cacheRead += w.pillars.cacheRead || 0;
|
|
2109
|
+
msgs += w.messages || 0;
|
|
2110
|
+
}
|
|
2111
|
+
if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
|
|
2112
|
+
continue;
|
|
2113
|
+
const c = cascade(sum);
|
|
2114
|
+
const card = narrate(c, `${wk} multi`);
|
|
2115
|
+
if (!codename) {
|
|
2116
|
+
out.push({
|
|
2117
|
+
window: wk,
|
|
2118
|
+
pillars: sum,
|
|
2119
|
+
cascade: c,
|
|
2120
|
+
card,
|
|
2121
|
+
submission: { status: "not_submitted", reason: "codename_required" },
|
|
2122
|
+
});
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
const rawPaste = `${sum.input} ${sum.output} ${sum.cacheCreate} ${sum.cacheRead}`;
|
|
2126
|
+
const windowType = WINDOW_TYPE[wk] || wk;
|
|
2127
|
+
const stamp = uploadStamp({
|
|
2128
|
+
codename,
|
|
2129
|
+
window: windowType,
|
|
2130
|
+
pillars: sum,
|
|
2131
|
+
platform: "multi",
|
|
2132
|
+
});
|
|
2133
|
+
const res = await doFetch(`${apiBase}/api/v1/ingest-paste`, {
|
|
2134
|
+
method: "POST",
|
|
2135
|
+
headers: {
|
|
2136
|
+
"content-type": "application/json",
|
|
2137
|
+
accept: "application/json",
|
|
2138
|
+
},
|
|
2139
|
+
body: JSON.stringify({
|
|
2140
|
+
codename,
|
|
2141
|
+
raw_paste: rawPaste,
|
|
2142
|
+
window_type: windowType,
|
|
2143
|
+
telemetry: { platform: { primary: "multi" } },
|
|
2144
|
+
...stamp,
|
|
2145
|
+
}),
|
|
2146
|
+
});
|
|
2147
|
+
let ack;
|
|
2148
|
+
try {
|
|
2149
|
+
ack = await res.json();
|
|
2150
|
+
} catch {
|
|
2151
|
+
ack = { status: "parse_error", httpStatus: res.status };
|
|
2152
|
+
}
|
|
2153
|
+
const ranked = ack?.ranked ?? ack?.accepted ?? false;
|
|
2154
|
+
out.push({
|
|
2155
|
+
window: wk,
|
|
2156
|
+
pillars: sum,
|
|
2157
|
+
cascade: c,
|
|
2158
|
+
card,
|
|
2159
|
+
submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
|
|
2160
|
+
});
|
|
2161
|
+
}
|
|
2162
|
+
return {
|
|
2163
|
+
platform: "multi",
|
|
2164
|
+
codename: codename || undefined,
|
|
2165
|
+
sources: detected.map((d) => d.platform),
|
|
2166
|
+
windows: out,
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
const pulled = await pullByPlatform(platform, opts);
|
|
1790
2171
|
const targets = args?.window
|
|
1791
2172
|
? pulled.windows.filter((w) => w.window === args.window)
|
|
1792
2173
|
: pulled.windows;
|
|
@@ -2856,6 +3237,7 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2856
3237
|
const top = ops.map((op) => ({
|
|
2857
3238
|
...op,
|
|
2858
3239
|
behavioral_framing: _behavioralFraming(op),
|
|
3240
|
+
competitive: _competitiveLayer(op, board),
|
|
2859
3241
|
}));
|
|
2860
3242
|
|
|
2861
3243
|
const best = top[0];
|
|
@@ -2863,7 +3245,13 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2863
3245
|
? `${best.codename} tops the SigRank leaderboard at Υ ${best.yield_?.toLocaleString?.() || best.yield_} — ${_behavioralFraming(best)}`
|
|
2864
3246
|
: "No operators on the board yet.";
|
|
2865
3247
|
|
|
2866
|
-
return {
|
|
3248
|
+
return {
|
|
3249
|
+
top_operators: top,
|
|
3250
|
+
total_operators: total,
|
|
3251
|
+
summary,
|
|
3252
|
+
cta: "Check my rank",
|
|
3253
|
+
shareable_url: best ? `${DEFAULT_API_BASE}/operator/${encodeURIComponent(best.codename)}` : null,
|
|
3254
|
+
};
|
|
2867
3255
|
}
|
|
2868
3256
|
|
|
2869
3257
|
if (name === "compare_self") {
|
|
@@ -2925,6 +3313,10 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2925
3313
|
|
|
2926
3314
|
const suggestion = _improvementSuggestion(klass, yourMetrics);
|
|
2927
3315
|
|
|
3316
|
+
// Competitive layer per SHARED_DESIGN_DECISIONS.md §3/§4/§5
|
|
3317
|
+
const competitive = _competitiveLayer(yourMetrics, board);
|
|
3318
|
+
const competitiveSummary = _competitiveSummary(yourMetrics, board);
|
|
3319
|
+
|
|
2928
3320
|
return {
|
|
2929
3321
|
your_metrics: yourMetrics,
|
|
2930
3322
|
power_user_assessment: powerUserAssessment,
|
|
@@ -2932,8 +3324,16 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2932
3324
|
your_yield_vs_avg: yieldVsAvg,
|
|
2933
3325
|
your_class_meaning: classMeaning,
|
|
2934
3326
|
percentile,
|
|
3327
|
+
rank: competitive.rank,
|
|
3328
|
+
total_operators: competitive.total_operators,
|
|
3329
|
+
class_tier: competitive.class_tier,
|
|
3330
|
+
delta_from_average: competitive.delta_from_average,
|
|
3331
|
+
delta_from_top: competitive.delta_from_top,
|
|
2935
3332
|
},
|
|
3333
|
+
competitive_summary: competitiveSummary,
|
|
3334
|
+
shareable_url: competitive.shareable_url,
|
|
2936
3335
|
suggestion,
|
|
3336
|
+
cta: "See where I stand",
|
|
2937
3337
|
};
|
|
2938
3338
|
}
|
|
2939
3339
|
|
|
@@ -2945,9 +3345,10 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2945
3345
|
"compare_operators requires both `codename_a` and `codename_b`.",
|
|
2946
3346
|
);
|
|
2947
3347
|
|
|
2948
|
-
const [opA, opB] = await Promise.all([
|
|
3348
|
+
const [opA, opB, board] = await Promise.all([
|
|
2949
3349
|
fetchJson(`/api/v1/operators/${encodeURIComponent(nameA)}`),
|
|
2950
3350
|
fetchJson(`/api/v1/operators/${encodeURIComponent(nameB)}`),
|
|
3351
|
+
fetchJson("/api/v1/leaderboard?metric=yield_"),
|
|
2951
3352
|
]);
|
|
2952
3353
|
|
|
2953
3354
|
const yieldA = opA.yield_ || 0;
|
|
@@ -2966,6 +3367,7 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2966
3367
|
velocity: opA.velocity,
|
|
2967
3368
|
class: opA.class,
|
|
2968
3369
|
rank: opA.rank,
|
|
3370
|
+
competitive: _competitiveLayer(opA, board),
|
|
2969
3371
|
},
|
|
2970
3372
|
operator_b: {
|
|
2971
3373
|
codename: opB.codename,
|
|
@@ -2974,9 +3376,11 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2974
3376
|
velocity: opB.velocity,
|
|
2975
3377
|
class: opB.class,
|
|
2976
3378
|
rank: opB.rank,
|
|
3379
|
+
competitive: _competitiveLayer(opB, board),
|
|
2977
3380
|
},
|
|
2978
3381
|
verdict,
|
|
2979
3382
|
yield_delta: delta,
|
|
3383
|
+
cta: "Compare me to others",
|
|
2980
3384
|
};
|
|
2981
3385
|
}
|
|
2982
3386
|
|
|
@@ -2997,6 +3401,8 @@ export async function callTool(name, args, opts = {}) {
|
|
|
2997
3401
|
{ class: "Burner", meaning: "Early-stage — tokens burned more than compounded. Low leverage, low velocity. The shift: reuse prior context." },
|
|
2998
3402
|
],
|
|
2999
3403
|
link: "https://signalaf.com/score — check your class tier and yield",
|
|
3404
|
+
shareable_url: `${DEFAULT_API_BASE}/score`,
|
|
3405
|
+
cta: "Learn the scoring",
|
|
3000
3406
|
};
|
|
3001
3407
|
}
|
|
3002
3408
|
|
|
@@ -3068,6 +3474,11 @@ export async function callTool(name, args, opts = {}) {
|
|
|
3068
3474
|
|
|
3069
3475
|
const summary = `Your Υ Yield is ${y.toLocaleString()} (${klass}). ${_improvementSuggestion(klass, metrics)}`;
|
|
3070
3476
|
|
|
3477
|
+
// Competitive layer per SHARED_DESIGN_DECISIONS.md §3/§4/§5
|
|
3478
|
+
const board = await fetchJson("/api/v1/leaderboard?metric=yield_");
|
|
3479
|
+
const competitive = _competitiveLayer(metrics, board);
|
|
3480
|
+
const competitiveSummary = _competitiveSummary(metrics, board);
|
|
3481
|
+
|
|
3071
3482
|
return {
|
|
3072
3483
|
your_metrics: {
|
|
3073
3484
|
yield_: y,
|
|
@@ -3075,16 +3486,117 @@ export async function callTool(name, args, opts = {}) {
|
|
|
3075
3486
|
velocity: v,
|
|
3076
3487
|
class: klass,
|
|
3077
3488
|
},
|
|
3489
|
+
competitive: {
|
|
3490
|
+
rank: competitive.rank,
|
|
3491
|
+
total_operators: competitive.total_operators,
|
|
3492
|
+
percentile: competitive.percentile,
|
|
3493
|
+
class_tier: competitive.class_tier,
|
|
3494
|
+
delta_from_average: competitive.delta_from_average,
|
|
3495
|
+
delta_from_top: competitive.delta_from_top,
|
|
3496
|
+
},
|
|
3497
|
+
competitive_summary: competitiveSummary,
|
|
3498
|
+
shareable_url: competitive.shareable_url,
|
|
3078
3499
|
suggestions,
|
|
3079
3500
|
summary,
|
|
3501
|
+
cta: "Improve my score",
|
|
3080
3502
|
};
|
|
3081
3503
|
}
|
|
3082
3504
|
|
|
3505
|
+
if (name === "tokscale_breakdown") {
|
|
3506
|
+
const threshold = Number(args?.threshold ?? 0.01);
|
|
3507
|
+
const platforms = await tokscaleModelBreakdown(threshold);
|
|
3508
|
+
return { platforms };
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3083
3511
|
throw new Error(`Unknown tool: ${name}`);
|
|
3084
3512
|
}
|
|
3085
3513
|
|
|
3086
3514
|
// ── Intent tool helpers — behavioral framing in power-user language ──────────
|
|
3087
3515
|
|
|
3516
|
+
/**
|
|
3517
|
+
* Competitive layer for tool responses per SHARED_DESIGN_DECISIONS.md §3/§4/§5.
|
|
3518
|
+
* Every tool response that includes operator data must show:
|
|
3519
|
+
* rank, percentile, class_tier, delta_from_average, delta_from_top, shareable_url
|
|
3520
|
+
* Response style: factual + competitive ("You rank #12 of 47 operators...")
|
|
3521
|
+
*/
|
|
3522
|
+
function _competitiveLayer(op, board) {
|
|
3523
|
+
const allOps = Array.isArray(board?.operators || board) ? (board.operators || board) : [];
|
|
3524
|
+
const yields = allOps.map((o) => o.yield_ || 0).sort((a, b) => a - b);
|
|
3525
|
+
const yourYield = op.yield_ || 0;
|
|
3526
|
+
const total = allOps.length;
|
|
3527
|
+
|
|
3528
|
+
// Rank: find operator's position (1-based)
|
|
3529
|
+
let rank = op.rank || null;
|
|
3530
|
+
if (!rank && total > 0) {
|
|
3531
|
+
const sorted = [...allOps].sort((a, b) => (b.yield_ || 0) - (a.yield_ || 0));
|
|
3532
|
+
const idx = sorted.findIndex((o) => o.codename === op.codename);
|
|
3533
|
+
rank = idx >= 0 ? idx + 1 : null;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
// Percentile: % of operators with yield below this operator
|
|
3537
|
+
const percentile = total > 0
|
|
3538
|
+
? Math.round((yields.filter((y) => y < yourYield).length / total) * 100)
|
|
3539
|
+
: 0;
|
|
3540
|
+
|
|
3541
|
+
// Delta from average
|
|
3542
|
+
const avgYield = total > 0 ? yields.reduce((s, y) => s + y, 0) / total : 0;
|
|
3543
|
+
const deltaFromAvg = avgYield > 0
|
|
3544
|
+
? { absolute: Math.round(yourYield - avgYield),
|
|
3545
|
+
percent: Math.round(((yourYield - avgYield) / avgYield) * 100) }
|
|
3546
|
+
: { absolute: 0, percent: 0 };
|
|
3547
|
+
|
|
3548
|
+
// Delta from top operator
|
|
3549
|
+
const topYield = total > 0 ? Math.max(...yields) : 0;
|
|
3550
|
+
const deltaFromTop = topYield > 0
|
|
3551
|
+
? { absolute: Math.round(topYield - yourYield),
|
|
3552
|
+
percent: Math.round(((topYield - yourYield) / topYield) * 100) }
|
|
3553
|
+
: { absolute: 0, percent: 0 };
|
|
3554
|
+
|
|
3555
|
+
// Shareable URL
|
|
3556
|
+
const shareableUrl = op.codename && op.codename !== "you (local)"
|
|
3557
|
+
? `${DEFAULT_API_BASE}/operator/${encodeURIComponent(op.codename)}`
|
|
3558
|
+
: null;
|
|
3559
|
+
|
|
3560
|
+
return {
|
|
3561
|
+
rank,
|
|
3562
|
+
total_operators: total,
|
|
3563
|
+
percentile,
|
|
3564
|
+
class_tier: op.class || "Burner",
|
|
3565
|
+
delta_from_average: deltaFromAvg,
|
|
3566
|
+
delta_from_top: deltaFromTop,
|
|
3567
|
+
shareable_url: shareableUrl,
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
/** Factual + competitive summary line per SHARED_DESIGN_DECISIONS.md §4 */
|
|
3572
|
+
function _competitiveSummary(op, board) {
|
|
3573
|
+
const cl = _competitiveLayer(op, board);
|
|
3574
|
+
const parts = [];
|
|
3575
|
+
|
|
3576
|
+
if (cl.rank && cl.total_operators > 0) {
|
|
3577
|
+
parts.push(`You rank #${cl.rank} of ${cl.total_operators} operators.`);
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
const topOp = (board?.operators || board || []).reduce(
|
|
3581
|
+
(best, o) => ((o.yield_ || 0) > (best?.yield_ || 0) ? o : best),
|
|
3582
|
+
null,
|
|
3583
|
+
);
|
|
3584
|
+
if (topOp && topOp.codename) {
|
|
3585
|
+
parts.push(`Top operator is ${topOp.codename} with Υ ${(topOp.yield_ || 0).toLocaleString()}.`);
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
if (cl.delta_from_average.percent !== 0) {
|
|
3589
|
+
const dir = cl.delta_from_average.percent > 0 ? "above" : "below";
|
|
3590
|
+
parts.push(`You're ${Math.abs(cl.delta_from_average.percent)}% ${dir} average.`);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
if (cl.delta_from_top.percent > 0) {
|
|
3594
|
+
parts.push(`${cl.delta_from_top.percent}% below top.`);
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
return parts.join(" ");
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3088
3600
|
function _behavioralFraming(op) {
|
|
3089
3601
|
const y = op.yield_ || 0;
|
|
3090
3602
|
const l = op.leverage || 0;
|