sigrank 0.0.180 → 0.0.182
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 +23 -22
- package/adapters/index.mjs +55 -22
- package/adapters/tokenpull.mjs +20 -28
- package/analytics/cascade.mjs +27 -0
- package/identity/keystore.mjs +1 -12
- package/lib/constants.mjs +111 -0
- package/lib/pkg-version.mjs +59 -0
- package/manifest.json +46 -0
- package/package.json +3 -1
- package/presentation/cli.mjs +49 -51
- package/presentation/tui.mjs +36 -39
- package/resources/class-tiers.md +51 -12
- package/resources/privacy-model.md +8 -5
- package/submit/index.mjs +8 -21
- package/tokscale_analytics.mjs +5 -16
- package/tools/_framing.mjs +94 -17
- package/tools/_helpers.mjs +5 -10
- package/tools/_schemas.mjs +68 -38
- package/tools/compare-operators.mjs +2 -1
- package/tools/compare-self.mjs +4 -2
- package/tools/describe-power-user.mjs +17 -6
- package/tools/enroll.mjs +1 -3
- package/tools/get-best-operator.mjs +2 -1
- package/tools/get-leaderboard.mjs +3 -2
- package/tools/get-operator.mjs +1 -1
- package/tools/index.mjs +4 -42
- package/tools/optimize-efficiency.mjs +15 -5
- package/tools/rank-paste.mjs +1 -1
- package/tools/submit-paste.mjs +13 -10
- package/tools/tokenpull-submit.mjs +11 -10
- package/tools/tokscale-breakdown.mjs +2 -42
package/README.md
CHANGED
|
@@ -332,28 +332,29 @@ Verifier numbers (ccusage/tokscale for codex) show **raw uncached input** (`inpu
|
|
|
332
332
|
|
|
333
333
|
## Platform adapters
|
|
334
334
|
|
|
335
|
-
All adapters are token-only (no message content, no cost fields, no credentials).
|
|
336
|
-
|
|
337
|
-
| Platform | Path |
|
|
338
|
-
| ------------------ | ---------------------------------------------- |
|
|
339
|
-
| Claude Code | ✅ `~/.claude/projects`
|
|
340
|
-
| Codex | ✅ `~/.codex/sessions`
|
|
341
|
-
| Devin CLI | ✅ `~/.local/share/devin/cli/sessions.db` |
|
|
342
|
-
| Amp | ✅ `~/.local/share/amp/threads` |
|
|
343
|
-
| Kimi | ✅ `~/.kimi/sessions` |
|
|
344
|
-
| pi-agent | ✅ `~/.pi/agent/sessions` |
|
|
345
|
-
| OpenClaw | ✅ `~/.openclaw` |
|
|
346
|
-
| Droid | ✅ `~/.factory/sessions/*.settings.json` |
|
|
347
|
-
| Codebuff | ✅ `~/.config/manicode` |
|
|
348
|
-
| Hermes | ✅ `~/.hermes/state.db` |
|
|
349
|
-
| Kilo | ✅ `~/.local/share/kilo/kilo.db` |
|
|
350
|
-
| Qwen | ✅ `~/.qwen/projects` | `cacheCreate=0`
|
|
351
|
-
| Goose | ✅ `~/.local/share/goose/sessions/sessions.db` | `cacheCreate=cacheRead=0`
|
|
352
|
-
| Gemini CLI | ✅ `~/.gemini/tmp` | `cacheCreate=0`
|
|
353
|
-
| GitHub Copilot CLI | ✅ `~/.copilot/otel` | OTel
|
|
354
|
-
| OpenCode | ⚠️ `~/.local/share/opencode` |
|
|
355
|
-
|
|
|
356
|
-
|
|
|
335
|
+
All adapters are token-only (no message content, no cost fields, no credentials). The table below is regenerated from the adapter file headers in `adapters/index.mjs` + `adapters/tokenpull.mjs` so the path, pillar shape, and notes match the actual code (a prior revision had drifted on Qwen/Codex/Copilot).
|
|
336
|
+
|
|
337
|
+
| Platform | Path | Pillar shape & notes |
|
|
338
|
+
| ------------------ | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
339
|
+
| Claude Code | ✅ `~/.claude/projects` (recursive, incl. `subagents/`) | Native 4-pillar; dedup by `(session_id, message_id)` — final snapshot wins |
|
|
340
|
+
| Codex | ✅ `~/.codex/sessions` (+ `archived_sessions`) | `input_tokens` incl. cached → `input` + `cacheCreate` split window-level via `io_ratio`; `cacheRead` native (`cached_input_tokens`); `reasoning_output`→output; verified vs ccusage (~1%) |
|
|
341
|
+
| Devin CLI | ✅ `~/.local/share/devin/cli/sessions.db` | SQLite; same `io_ratio` split as Codex (Alpha 2.0 default); `cacheRead` native |
|
|
342
|
+
| Amp | ✅ `~/.local/share/amp/threads` | Native 4-pillar; per-message |
|
|
343
|
+
| Kimi | ✅ `~/.kimi/sessions` | Native 4-pillar; `StatusUpdate` lines only |
|
|
344
|
+
| pi-agent | ✅ `~/.pi/agent/sessions` | Native 4-pillar; per-message JSONL |
|
|
345
|
+
| OpenClaw | ✅ `~/.openclaw` | Native 4-pillar; per-message JSONL |
|
|
346
|
+
| Droid | ✅ `~/.factory/sessions/*.settings.json` | Native 4-pillar; `thinking_tokens`→output |
|
|
347
|
+
| Codebuff | ✅ `~/.config/manicode` | Native 4-pillar; `chat-messages.json` |
|
|
348
|
+
| Hermes | ✅ `~/.hermes/state.db` | Native 4-pillar; SQLite; `reasoning_tokens`→output |
|
|
349
|
+
| Kilo | ✅ `~/.local/share/kilo/kilo.db` | Native 4-pillar; SQLite |
|
|
350
|
+
| Qwen | ✅ `~/.qwen/projects` | Estimated (`cacheCreate=0` — no field in logs); `cacheRead` from `cachedContentTokenCount`; `thoughtsTokenCount`→output |
|
|
351
|
+
| Goose | ✅ `~/.local/share/goose/sessions/sessions.db` (or `$GOOSE_PATH_ROOT/data/sessions/sessions.db`) | Estimated (`cacheCreate=cacheRead=0` — no cache fields); reasoning = `total−input−output`→output; cumulative-column dedup by session id |
|
|
352
|
+
| Gemini CLI | ✅ `~/.gemini/tmp` | Estimated (`cacheCreate=0`); `cacheRead` from `cached` field; `thought`→output; `input` = `input−cached` (fresh) |
|
|
353
|
+
| GitHub Copilot CLI | ✅ `~/.copilot/otel` | Native 4-pillar (OTel spans: `llm.token_count.{prompt,completion,cache_creation,cache_read}`); requires `COPILOT_OTEL_ENABLED=true` + `COPILOT_OTEL_EXPORTER_TYPE=file` set **before** session start |
|
|
354
|
+
| OpenCode | ⚠️ `~/.local/share/opencode` | Data gap — logs store `cost:0` and derive tokens via LiteLLM at runtime; raw token counts not persisted. No pillars readable with current format |
|
|
355
|
+
| Other (user JSON) | ✅ `$SIGRANK_OTHER_PATH` | User-supplied JSON `{ "windows": { "all": {input,output,cacheCreate,cacheRead} } }`; all-time only (no timestamps) |
|
|
356
|
+
| Cursor | 🔜 | Chat log path TBD |
|
|
357
|
+
| Windsurf | 🔜 | Session logs at `~/.codeium/windsurf/` |
|
|
357
358
|
|
|
358
359
|
`estimated=true` means one or more pillars are derived, not native. The server re-scores all submitted pillars authoritatively; local preview Υ is indicative only.
|
|
359
360
|
|
package/adapters/index.mjs
CHANGED
|
@@ -51,8 +51,11 @@ const DAY_MS = 86_400_000; // shared with tokenpull.mjs but kept local to avoid
|
|
|
51
51
|
|
|
52
52
|
// ── File-system helpers ───────────────────────────────────────────────────────
|
|
53
53
|
|
|
54
|
-
/** Recursively yield every file whose name matches `pred` under dir (skips
|
|
55
|
-
|
|
54
|
+
/** Recursively yield every file whose name matches `pred` under dir (skips
|
|
55
|
+
* symlink dirs, stops after `max` files). Exported so adapters/tokenpull.mjs
|
|
56
|
+
* can reuse it instead of maintaining a second near-identical walker
|
|
57
|
+
* (_walkJsonl) — the two had drifted on the symlink-skip + max-files guards. */
|
|
58
|
+
export async function* walkFiles(dir, pred, counter = { n: 0 }, max = 10_000) {
|
|
56
59
|
if (counter.n >= max) return;
|
|
57
60
|
let entries;
|
|
58
61
|
try {
|
|
@@ -743,7 +746,9 @@ export const hermesAdapter = {
|
|
|
743
746
|
// Same combined-input problem as Codex: input_tokens INCLUDES cache write, so we
|
|
744
747
|
// yield { ts, output, cacheRead, uncached } and let tokenpullCodex() do the
|
|
745
748
|
// ioRatio split (input = output × ioRatio, cacheCreate = uncached − input).
|
|
746
|
-
// ioRatio comes from Claude (Beta
|
|
749
|
+
// ioRatio comes from Claude (Beta = operator's Claude input/output ratio) or
|
|
750
|
+
// the Alpha 2.0 default (matches Codex; the "7:1:2 average → 0.5" note in a
|
|
751
|
+
// prior revision was wrong — tokenpullAny defaults ioRatio to 2.0 for both).
|
|
747
752
|
export const devinAdapter = {
|
|
748
753
|
platform: "devin",
|
|
749
754
|
defaultRoot: () => join(homedir(), ".local", "share", "devin", "cli"),
|
|
@@ -795,9 +800,22 @@ export const devinAdapter = {
|
|
|
795
800
|
// }
|
|
796
801
|
// }
|
|
797
802
|
//
|
|
798
|
-
// The adapter yields
|
|
799
|
-
//
|
|
800
|
-
// (same as null-
|
|
803
|
+
// The adapter yields ONE message carrying the all-time pillars. The generic
|
|
804
|
+
// tokenpull aggregator buckets by timestamp, and these records carry no
|
|
805
|
+
// timestamp (ts: null → "all" window only, same as null-ts records in other
|
|
806
|
+
// adapters). Yielding one message PER window key — as a prior version did —
|
|
807
|
+
// caused the "all" total to be the SUM of every window entry (all + 30d + 7d
|
|
808
|
+
// …), i.e. a multi-count. This is the only user-facing wrong-number bug in
|
|
809
|
+
// ingestion: an operator who filled in all four windows saw an "all" Υ
|
|
810
|
+
// computed from 2–4× the real pillars.
|
|
811
|
+
//
|
|
812
|
+
// Fix: yield exactly one record. If the JSON has an "all" entry, use it
|
|
813
|
+
// verbatim; otherwise sum the provided windows into a synthetic all-time
|
|
814
|
+
// total. The 7d/30d/90d entries are accepted (so the file format stays
|
|
815
|
+
// forward-compatible) but are NOT yielded as separate records — the generic
|
|
816
|
+
// aggregator has no way to route a timestamp-less record into a bounded
|
|
817
|
+
// window, so emitting them would only re-introduce the overcount. An operator
|
|
818
|
+
// who wants windowed data must use a real adapter that carries timestamps.
|
|
801
819
|
export const otherAdapter = {
|
|
802
820
|
platform: "other",
|
|
803
821
|
defaultRoot: () => process.env.SIGRANK_OTHER_PATH || "",
|
|
@@ -822,23 +840,38 @@ export const otherAdapter = {
|
|
|
822
840
|
throw new Error(`other adapter: invalid JSON in ${filePath}`);
|
|
823
841
|
}
|
|
824
842
|
const windows = data.windows || {};
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
843
|
+
const norm = (p) => ({
|
|
844
|
+
input: Number(p?.input || 0),
|
|
845
|
+
output: Number(p?.output || 0),
|
|
846
|
+
cacheCreate: Number(p?.cacheCreate || p?.cache_create || 0),
|
|
847
|
+
cacheRead: Number(p?.cacheRead || p?.cache_read || 0),
|
|
848
|
+
});
|
|
849
|
+
let all;
|
|
850
|
+
if (windows.all) {
|
|
851
|
+
// Explicit all-time entry → use verbatim (do NOT add 7d/30d/90d on top).
|
|
852
|
+
all = norm(windows.all);
|
|
853
|
+
} else {
|
|
854
|
+
// No "all" key → fold the provided windows into a synthetic all-time.
|
|
855
|
+
all = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
|
|
856
|
+
for (const pillars of Object.values(windows)) {
|
|
857
|
+
const n = norm(pillars);
|
|
858
|
+
all.input += n.input;
|
|
859
|
+
all.output += n.output;
|
|
860
|
+
all.cacheCreate += n.cacheCreate;
|
|
861
|
+
all.cacheRead += n.cacheRead;
|
|
862
|
+
}
|
|
841
863
|
}
|
|
864
|
+
if (all.input + all.output + all.cacheCreate + all.cacheRead === 0) return;
|
|
865
|
+
yield {
|
|
866
|
+
id: `other:all`,
|
|
867
|
+
sid: `other-all`,
|
|
868
|
+
ts: null, // no timestamp → lands in "all" window only
|
|
869
|
+
input: all.input,
|
|
870
|
+
output: all.output,
|
|
871
|
+
cacheCreate: all.cacheCreate,
|
|
872
|
+
cacheRead: all.cacheRead,
|
|
873
|
+
file: filePath,
|
|
874
|
+
};
|
|
842
875
|
},
|
|
843
876
|
};
|
|
844
877
|
|
package/adapters/tokenpull.mjs
CHANGED
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
* the same { platform, defaultRoot(), messages(root) } contract — Claude is just the first.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { readdir, readFile
|
|
18
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
19
19
|
import { existsSync } from "node:fs";
|
|
20
20
|
import { execFile } from "node:child_process";
|
|
21
21
|
import { join, dirname } from "node:path";
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
|
-
import { ADAPTERS } from "./index.mjs";
|
|
24
|
+
import { ADAPTERS, walkFiles } from "./index.mjs";
|
|
25
25
|
|
|
26
26
|
const DAY_MS = 86_400_000;
|
|
27
27
|
|
|
@@ -87,34 +87,26 @@ const MAX_JSONL_FILES = Number(process.env.SIGRANK_MAX_JSONL_FILES) || 10_000;
|
|
|
87
87
|
* under-counts input (sub-agent runs are input-heavy). token-dashboard's rglob.
|
|
88
88
|
*
|
|
89
89
|
* Hardened: skips symlinked directories (prevents circular traversal) and stops
|
|
90
|
-
* after MAX_JSONL_FILES files (prevents OOM on pathological trees).
|
|
90
|
+
* after MAX_JSONL_FILES files (prevents OOM on pathological trees).
|
|
91
|
+
*
|
|
92
|
+
* Pass 2: now a thin sorted wrapper over the shared walkFiles() from
|
|
93
|
+
* adapters/index.mjs so the two walkers can't drift on the symlink-skip +
|
|
94
|
+
* max-files guards again. The sort (entries within each directory) is preserved
|
|
95
|
+
* by sorting the yielded paths; walkFiles already recurses in readdir order. */
|
|
91
96
|
async function* _walkJsonl(dir, _counter = { n: 0 }) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (e.isDirectory()) {
|
|
103
|
-
// Skip symlinked directories — prevents circular traversal on machines where
|
|
104
|
-
// ~/.claude/projects/ contains a symlink to a large or self-referential tree.
|
|
105
|
-
let stat;
|
|
106
|
-
try {
|
|
107
|
-
stat = await lstat(full);
|
|
108
|
-
} catch {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (stat.isSymbolicLink()) continue;
|
|
112
|
-
yield* _walkJsonl(full, _counter);
|
|
113
|
-
} else if (e.isFile() && e.name.endsWith(".jsonl")) {
|
|
114
|
-
_counter.n++;
|
|
115
|
-
yield full;
|
|
116
|
-
}
|
|
97
|
+
// walkFiles yields in readdir order; we collect + sort to keep the
|
|
98
|
+
// deterministic ordering the previous implementation guaranteed.
|
|
99
|
+
const paths = [];
|
|
100
|
+
for await (const p of walkFiles(
|
|
101
|
+
dir,
|
|
102
|
+
(n) => n.endsWith(".jsonl"),
|
|
103
|
+
_counter,
|
|
104
|
+
MAX_JSONL_FILES,
|
|
105
|
+
)) {
|
|
106
|
+
paths.push(p);
|
|
117
107
|
}
|
|
108
|
+
paths.sort((a, b) => (a < b ? -1 : 1));
|
|
109
|
+
for (const p of paths) yield p;
|
|
118
110
|
}
|
|
119
111
|
|
|
120
112
|
/**
|
package/analytics/cascade.mjs
CHANGED
|
@@ -68,9 +68,36 @@ export function cascade({ input, output, cacheCreate, cacheRead }) {
|
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* CLASS_TIERS — the canonical 8-tier dev10x taxonomy. SINGLE SOURCE OF TRUTH.
|
|
73
|
+
* Every consumer (presentation/tui.mjs, presentation/cli.mjs, tools/_schemas.mjs,
|
|
74
|
+
* resources/class-tiers.md) imports THIS list instead of maintaining its own.
|
|
75
|
+
* Order is descending (TRANSMITTER at top, IGNITER at bottom) — matches the
|
|
76
|
+
* classify() first-match-wins cut order.
|
|
77
|
+
*/
|
|
78
|
+
export const CLASS_TIERS = [
|
|
79
|
+
"TRANSMITTER",
|
|
80
|
+
"ARCH+",
|
|
81
|
+
"ARCH",
|
|
82
|
+
"POWER",
|
|
83
|
+
"BASE",
|
|
84
|
+
"SEEKER",
|
|
85
|
+
"REFINER",
|
|
86
|
+
"IGNITER",
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
/** The degenerate-case class returned when both yield and dev10x are null
|
|
90
|
+
* (all-zero / empty session). Distinct from IGNITER (which is a real low
|
|
91
|
+
* dev10x bucket) so callers can tell "no data" from "bottom tier". */
|
|
92
|
+
export const UNCLASSED = "UNCLASSED";
|
|
93
|
+
|
|
71
94
|
/** MVP class tiering from Υ + 10xDEV (canon assigns from cascade SNR + 10xDEV; this
|
|
72
95
|
* is the open-MVP approximation — proprietary threshold cuts stay server-side). */
|
|
73
96
|
export function classify(yieldVal, dev10x) {
|
|
97
|
+
// Degenerate: all-null (empty session / no pillars) → UNCLASSED, not IGNITER.
|
|
98
|
+
// Falling through to IGNITER here would mislabel a no-data operator as the
|
|
99
|
+
// bottom tier; UNCLASSED lets the TUI / narrate surface "no data" instead.
|
|
100
|
+
if (yieldVal == null && dev10x == null) return UNCLASSED;
|
|
74
101
|
if (yieldVal >= 1000 || dev10x >= 3) return "TRANSMITTER";
|
|
75
102
|
if (dev10x >= 1.45) return "ARCH+";
|
|
76
103
|
if (dev10x >= 1.35) return "ARCH";
|
package/identity/keystore.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
copyFileSync,
|
|
24
24
|
} from "node:fs";
|
|
25
25
|
import { generateKeyPairSync, randomUUID } from "node:crypto";
|
|
26
|
+
import { agentVersion } from "../lib/pkg-version.mjs";
|
|
26
27
|
|
|
27
28
|
const DIR = join(homedir(), ".sigrank-mcp");
|
|
28
29
|
const PATH = join(DIR, "identity.json");
|
|
@@ -32,18 +33,6 @@ const MAX_BACKUPS = 5;
|
|
|
32
33
|
/** SPKI DER prefix length for an ed25519 public key (12 bytes before the raw 32). */
|
|
33
34
|
const SPKI_PREFIX_LEN = 12;
|
|
34
35
|
|
|
35
|
-
/** Resolve this package's version for the agent_version stamp (best-effort). */
|
|
36
|
-
function agentVersion() {
|
|
37
|
-
try {
|
|
38
|
-
const pkg = JSON.parse(
|
|
39
|
-
readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
|
|
40
|
-
);
|
|
41
|
-
return `sigrank-mcp/${pkg.version}`;
|
|
42
|
-
} catch {
|
|
43
|
-
return "sigrank-mcp";
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
36
|
export function keystorePath() {
|
|
48
37
|
return PATH;
|
|
49
38
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// lib/constants.mjs — repo-wide constants that were previously inlined as
|
|
2
|
+
// literals across multiple files. Centralizing them here means a version bump
|
|
3
|
+
// or a client-map update lands in exactly one place.
|
|
4
|
+
//
|
|
5
|
+
// TERMS_VERSION / PRIVACY_VERSION — embedded in every enroll + submit
|
|
6
|
+
// payload (tools/enroll.mjs, tools/submit-paste.mjs,
|
|
7
|
+
// tools/tokenpull-submit.mjs, submit/index.mjs). They were inlined as
|
|
8
|
+
// "2026-07-21" in four files; a terms rev would have required finding
|
|
9
|
+
// and editing all four. Now: one export, imported everywhere.
|
|
10
|
+
//
|
|
11
|
+
// TOKSCALE_CLIENT_MAP — maps tokscale's raw client names to our platform
|
|
12
|
+
// IDs. Was duplicated verbatim in tools/index.mjs and
|
|
13
|
+
// tools/tokscale-breakdown.mjs (and again in tokscale_analytics.mjs);
|
|
14
|
+
// the three copies had already started to drift. One export here.
|
|
15
|
+
//
|
|
16
|
+
// isRankedAck — the "actually on the board" predicate (verified + persisted).
|
|
17
|
+
// Was inlined as `ack.verification_tier === "verified" && ack.persisted
|
|
18
|
+
// === true` in submit-paste, tokenpull-submit, and submit/index.mjs, with
|
|
19
|
+
// tokenpull-submit's multi-platform branch using a DIFFERENT looser
|
|
20
|
+
// predicate (`ack?.ranked ?? ack?.accepted ?? false`). One predicate here
|
|
21
|
+
// closes that loophole so every submit path agrees on what "ranked" means.
|
|
22
|
+
|
|
23
|
+
/** Current Terms of Service version the client attests to. */
|
|
24
|
+
export const TERMS_VERSION = "2026-07-21";
|
|
25
|
+
|
|
26
|
+
/** Current Privacy Policy version the client attests to. */
|
|
27
|
+
export const PRIVACY_VERSION = "2026-07-21";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The leaderboard sort metric the client requests from the server. Every
|
|
31
|
+
* leaderboard fetch site (tools/get-leaderboard.mjs, presentation/cli.mjs
|
|
32
|
+
* fetchBoard, presentation/tui.mjs, compare-self / optimize-efficiency /
|
|
33
|
+
* get-best-operator / compare-operators) MUST use this value — the server
|
|
34
|
+
* ranks by `yield_` and a different metric string would either 400 or return
|
|
35
|
+
* an unsorted board. Centralized here so a future metric rename lands in one
|
|
36
|
+
* place; the contract test in __tests__/contract/leaderboard-metric-contract.test.mjs
|
|
37
|
+
* guards against intra-repo drift and (in CI) cross-repo drift vs sigrank-app.
|
|
38
|
+
*/
|
|
39
|
+
export const LEADERBOARD_METRIC = "yield_";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Map a raw tokscale `client` string to our canonical platform ID.
|
|
43
|
+
* Values mapped to `null` are skipped (synthetic / non-attributable).
|
|
44
|
+
* Values mapped to `"other"` have no native adapter and bucket together.
|
|
45
|
+
*/
|
|
46
|
+
export const TOKSCALE_CLIENT_MAP = {
|
|
47
|
+
claude: "claude",
|
|
48
|
+
codex: "codex",
|
|
49
|
+
"devin-cli": "devin",
|
|
50
|
+
"devin-desktop": "devin",
|
|
51
|
+
gemini: "gemini",
|
|
52
|
+
amp: "amp",
|
|
53
|
+
kimi: "kimi",
|
|
54
|
+
qwen: "qwen",
|
|
55
|
+
goose: "goose",
|
|
56
|
+
kilo: "kilo",
|
|
57
|
+
kilocode: "kilo",
|
|
58
|
+
hermes: "hermes",
|
|
59
|
+
droid: "droid",
|
|
60
|
+
codebuff: "codebuff",
|
|
61
|
+
copilot: "copilot",
|
|
62
|
+
opencode: "opencode",
|
|
63
|
+
openclaw: "openclaw",
|
|
64
|
+
pi: "pi",
|
|
65
|
+
cursor: "other",
|
|
66
|
+
roocode: "other",
|
|
67
|
+
mux: "other",
|
|
68
|
+
crush: "other",
|
|
69
|
+
antigravity: "other",
|
|
70
|
+
"antigravity-cli": "other",
|
|
71
|
+
zed: "other",
|
|
72
|
+
kiro: "other",
|
|
73
|
+
trae: "other",
|
|
74
|
+
warp: "other",
|
|
75
|
+
cline: "other",
|
|
76
|
+
gjc: "other",
|
|
77
|
+
grok: "other",
|
|
78
|
+
jcode: "other",
|
|
79
|
+
commandcode: "other",
|
|
80
|
+
micode: "other",
|
|
81
|
+
junie: "other",
|
|
82
|
+
zcode: "other",
|
|
83
|
+
opencodereview: "other",
|
|
84
|
+
codebuddy: "other",
|
|
85
|
+
workbuddy: "other",
|
|
86
|
+
synthetic: null,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The "actually on the board" predicate. A submission is ranked only when the
|
|
91
|
+
* server returns HTTP ok AND verification_tier === "verified" AND persisted
|
|
92
|
+
* === true. An unenrolled/revoked device gets 202 but is NEVER ranked.
|
|
93
|
+
*
|
|
94
|
+
* Use this everywhere a submit path decides whether to surface `ranked: true`.
|
|
95
|
+
* The previous tokenpull-submit multi-platform branch used a looser
|
|
96
|
+
* `ack?.ranked ?? ack?.accepted ?? false` which would mark a non-persisted
|
|
97
|
+
* 202 as ranked — this predicate closes that loophole.
|
|
98
|
+
*
|
|
99
|
+
* @param {{ ok: boolean }} res the fetch Response (or { ok } shim)
|
|
100
|
+
* @param {Record<string, unknown>} ack the parsed JSON ack body
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
export function isRankedAck(res, ack) {
|
|
104
|
+
return !!(
|
|
105
|
+
res &&
|
|
106
|
+
res.ok &&
|
|
107
|
+
ack &&
|
|
108
|
+
ack.verification_tier === "verified" &&
|
|
109
|
+
ack.persisted === true
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// lib/pkg-version.mjs — single source of truth for the package version.
|
|
2
|
+
//
|
|
3
|
+
// Resolves the repo-root package.json by walking up from this module until it
|
|
4
|
+
// finds the package named "sigrank". This is robust to callers in any
|
|
5
|
+
// subdirectory (identity/, submit/, presentation/, tools/, scripts/) — the
|
|
6
|
+
// previous per-caller `new URL("./package.json", import.meta.url)` lookups
|
|
7
|
+
// resolved relative to the CALLER's own directory and hit non-existent files,
|
|
8
|
+
// falling back to a bare string. The agent_version stamp rides inside the
|
|
9
|
+
// SIGNED submit payload (agent.agent_version), so a wrong/missing version
|
|
10
|
+
// there is a high-priority bug — it survived because no test guarded it.
|
|
11
|
+
//
|
|
12
|
+
// One implementation, imported everywhere. Never throws: returns "unknown" on
|
|
13
|
+
// any failure so a missing/renamed package.json never breaks a submit.
|
|
14
|
+
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
/** The published package name (package.json "name"). Used to identify the
|
|
20
|
+
* repo-root package.json unambiguously past any nested package.json. */
|
|
21
|
+
const REPO_PKG_NAME = "sigrank";
|
|
22
|
+
|
|
23
|
+
/** Resolve the repo-root package version (best-effort).
|
|
24
|
+
* Returns the version string (e.g. "0.0.181") or "unknown" on any failure. */
|
|
25
|
+
export function pkgVersion() {
|
|
26
|
+
try {
|
|
27
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
for (let i = 0; i < 20; i++) {
|
|
29
|
+
const pkgPath = join(dir, "package.json");
|
|
30
|
+
let pkg;
|
|
31
|
+
try {
|
|
32
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
33
|
+
} catch {
|
|
34
|
+
// no package.json here — walk up
|
|
35
|
+
const parent = dirname(dir);
|
|
36
|
+
if (parent === dir) break;
|
|
37
|
+
dir = parent;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (pkg.name === REPO_PKG_NAME) return pkg.version || "unknown";
|
|
41
|
+
// Found a package.json but it isn't ours (e.g. a nested dep) — keep walking.
|
|
42
|
+
const parent = dirname(dir);
|
|
43
|
+
if (parent === dir) {
|
|
44
|
+
// Reached the filesystem root without matching; use whatever we found.
|
|
45
|
+
return pkg.version || "unknown";
|
|
46
|
+
}
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
return "unknown";
|
|
50
|
+
} catch {
|
|
51
|
+
return "unknown";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The full agent-version stamp, e.g. "sigrank-mcp/0.0.181".
|
|
56
|
+
* This is the value embedded in the signed payload's agent.agent_version. */
|
|
57
|
+
export function agentVersion() {
|
|
58
|
+
return `sigrank-mcp/${pkgVersion()}`;
|
|
59
|
+
}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
|
|
3
|
+
"manifest_version": "0.4",
|
|
4
|
+
"name": "sigrank",
|
|
5
|
+
"version": "0.0.180",
|
|
6
|
+
"description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read × output / input²). 23 tools for yield calculation, local token usage pulls, signed submission, and tokscale-powered analytics (market share, developer profiles, model trends, cost analysis, device profiles, MCP usage, competitive intelligence). Privacy-first: runs locally, only four token counts leave the machine.",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "SunrisesIllNeverSee",
|
|
9
|
+
"url": "https://github.com/SunrisesIllNeverSee"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://signalaf.com",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/SunrisesIllNeverSee/sigrank-mcp"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"server": {
|
|
18
|
+
"type": "node",
|
|
19
|
+
"entry_point": "index.mjs",
|
|
20
|
+
"mcp_config": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": [
|
|
23
|
+
"sigrank"
|
|
24
|
+
],
|
|
25
|
+
"env": {
|
|
26
|
+
"SIGRANK_API_BASE": "https://signalaf.com"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"user_config": {
|
|
31
|
+
"SIGRANK_API_BASE": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"title": "SigRank API Base URL",
|
|
34
|
+
"description": "Overrides the board host for the SigRank API. Defaults to https://signalaf.com.",
|
|
35
|
+
"default": "https://signalaf.com",
|
|
36
|
+
"required": false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"compatibility": {
|
|
40
|
+
"platforms": [
|
|
41
|
+
"darwin",
|
|
42
|
+
"win32",
|
|
43
|
+
"linux"
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.182",
|
|
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",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
34
|
"index.mjs",
|
|
35
|
+
"lib/",
|
|
35
36
|
"tools/",
|
|
36
37
|
"analytics/",
|
|
37
38
|
"identity/",
|
|
@@ -55,6 +56,7 @@
|
|
|
55
56
|
"narrate.mjs",
|
|
56
57
|
"tools.mjs",
|
|
57
58
|
"tokscale_analytics.mjs",
|
|
59
|
+
"manifest.json",
|
|
58
60
|
"README.md",
|
|
59
61
|
"CHANGELOG.md",
|
|
60
62
|
"MCP_SERVER_DESCRIPTION.md",
|