sigrank 0.0.181 → 0.0.183
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 +117 -12
- package/identity/keystore.mjs +1 -12
- package/lib/constants.mjs +111 -0
- package/lib/pkg-version.mjs +59 -0
- package/package.json +2 -1
- package/presentation/cli.mjs +51 -53
- package/presentation/tui.mjs +37 -40
- package/resources/class-tiers.md +79 -21
- package/resources/privacy-model.md +8 -5
- package/submit/index.mjs +8 -21
- package/tokscale_analytics.mjs +5 -16
- package/tools/_framing.mjs +95 -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 +16 -6
- package/tools/diagnose-cascade.mjs +1 -1
- 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/self-improve.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
|
@@ -61,24 +61,129 @@ export function cascade({ input, output, cacheCreate, cacheRead }) {
|
|
|
61
61
|
leverage: round(leverage, 1),
|
|
62
62
|
velocity: round(velocity, 3),
|
|
63
63
|
dev10x: round(dev10x, 2),
|
|
64
|
-
class: classify(
|
|
64
|
+
class: classify(total),
|
|
65
65
|
mode: detectMode({ input: i, output: o, cacheCreate: cw, cacheRead: cr }),
|
|
66
66
|
};
|
|
67
67
|
if (warnings.length > 0) result.warnings = warnings;
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
/**
|
|
72
|
-
*
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
71
|
+
/**
|
|
72
|
+
* CLASS_TIERS — the 8 base tier names (K.01–K.08) for display: glyph, color,
|
|
73
|
+
* meaning. SINGLE SOURCE OF TRUTH for tier-level display info.
|
|
74
|
+
*
|
|
75
|
+
* The permanent class is an EXPERIENCE ladder keyed on TOTAL TOKENS. Each tier
|
|
76
|
+
* has 3 sub-stages (I/II/III) — 24 stages total. The 24 thresholds live in
|
|
77
|
+
* RS05_CLASS_THRESHOLDS below. classify() returns the full sub-stage string
|
|
78
|
+
* (e.g. "REFINER II"). Use tierOf() to extract the base tier name.
|
|
79
|
+
*
|
|
80
|
+
* TRANSMITTER is NOT on this ladder — it is a temporary peak badge (RS.08).
|
|
81
|
+
* The client does not evaluate the badge (owner is still calibrating it);
|
|
82
|
+
* the server does it on read.
|
|
83
|
+
*
|
|
84
|
+
* Mirrors the server's canon-ids.ts CLASS_TIERS (display) + ruleset.ts
|
|
85
|
+
* RS05_CLASS_THRESHOLDS (thresholds). Order is descending (ARCH+ → IGNITER).
|
|
86
|
+
*/
|
|
87
|
+
export const CLASS_TIERS = [
|
|
88
|
+
"ARCH+",
|
|
89
|
+
"ARCH",
|
|
90
|
+
"POWER",
|
|
91
|
+
"BASE",
|
|
92
|
+
"SEEKER",
|
|
93
|
+
"REFINER",
|
|
94
|
+
"BEARER",
|
|
95
|
+
"IGNITER",
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* SIGNAL_CLASSES — the full 24 sub-stage names (8 tiers × 3 sub-stages I/II/III).
|
|
100
|
+
* Mirrors the server's SIGNAL_CLASSES set in lib/board/mappers.ts. The API
|
|
101
|
+
* returns class_tier as one of these 24 values. classify() returns one of these
|
|
102
|
+
* (or UNCLASSED). Use this for schema enums and validation.
|
|
103
|
+
*/
|
|
104
|
+
export const SIGNAL_CLASSES = [
|
|
105
|
+
"ARCH+ I", "ARCH+ II", "ARCH+ III",
|
|
106
|
+
"ARCH I", "ARCH II", "ARCH III",
|
|
107
|
+
"POWER I", "POWER II", "POWER III",
|
|
108
|
+
"BASE I", "BASE II", "BASE III",
|
|
109
|
+
"SEEKER I", "SEEKER II", "SEEKER III",
|
|
110
|
+
"REFINER I", "REFINER II", "REFINER III",
|
|
111
|
+
"BEARER I", "BEARER II", "BEARER III",
|
|
112
|
+
"IGNITER I", "IGNITER II", "IGNITER III",
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* RS05_CLASS_THRESHOLDS — 24 total-token breakpoints (8 tiers × 3 sub-stages).
|
|
117
|
+
* Mirrors the server's lib/analytics/ruleset.ts RS05_CLASS_THRESHOLDS exactly.
|
|
118
|
+
* Calibrated from the HCM cut (1,626 operators) using target distribution
|
|
119
|
+
* (Option C): IGNITER 10%, BEARER 12.5%, REFINER 15%, SEEKER 22.5%, BASE 20%,
|
|
120
|
+
* POWER 15%, ARCH 5%, ARCH+ 0% (aspirational). Each tier split into 3
|
|
121
|
+
* equal-population sub-stages. Update this one constant to stay in parity
|
|
122
|
+
* when the owner tweaks server-side.
|
|
123
|
+
*/
|
|
124
|
+
export const RS05_CLASS_THRESHOLDS = [
|
|
125
|
+
{ class: "ARCH+ I", totalMin: 7068201104627 },
|
|
126
|
+
{ class: "ARCH+ II", totalMin: 7068201104627 },
|
|
127
|
+
{ class: "ARCH+ III", totalMin: 7068201104627 },
|
|
128
|
+
{ class: "ARCH I", totalMin: 186207267611 },
|
|
129
|
+
{ class: "ARCH II", totalMin: 98543134083 },
|
|
130
|
+
{ class: "ARCH III", totalMin: 68766193943 },
|
|
131
|
+
{ class: "POWER I", totalMin: 39958782379 },
|
|
132
|
+
{ class: "POWER II", totalMin: 26955905621 },
|
|
133
|
+
{ class: "POWER III", totalMin: 19141226889 },
|
|
134
|
+
{ class: "BASE I", totalMin: 13960345961 },
|
|
135
|
+
{ class: "BASE II", totalMin: 10189224970 },
|
|
136
|
+
{ class: "BASE III", totalMin: 7747041813 },
|
|
137
|
+
{ class: "SEEKER I", totalMin: 5446673659 },
|
|
138
|
+
{ class: "SEEKER II", totalMin: 4014577247 },
|
|
139
|
+
{ class: "SEEKER III", totalMin: 2961798768 },
|
|
140
|
+
{ class: "REFINER I", totalMin: 2358346840 },
|
|
141
|
+
{ class: "REFINER II", totalMin: 1845750357 },
|
|
142
|
+
{ class: "REFINER III", totalMin: 1334876308 },
|
|
143
|
+
{ class: "BEARER I", totalMin: 984078167 },
|
|
144
|
+
{ class: "BEARER II", totalMin: 714619043 },
|
|
145
|
+
{ class: "BEARER III", totalMin: 431702990 },
|
|
146
|
+
{ class: "IGNITER I", totalMin: 216393332 },
|
|
147
|
+
{ class: "IGNITER II", totalMin: 88999166 },
|
|
148
|
+
{ class: "IGNITER III", totalMin: 0 },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
/** The degenerate-case class returned when totalTokens is null/undefined
|
|
152
|
+
* (all-zero / empty session). Distinct from IGNITER III (which is a real
|
|
153
|
+
* low-experience tier) so callers can tell "no data" from "bottom tier". */
|
|
154
|
+
export const UNCLASSED = "UNCLASSED";
|
|
155
|
+
|
|
156
|
+
/** Extract the base tier name from a sub-stage string (e.g. "ARCH+ I" → "ARCH+").
|
|
157
|
+
* Mirrors the server's tierOf() in components/sigrank/types.ts. Returns the
|
|
158
|
+
* input as-is if it's not a sub-stage string (e.g. UNCLASSED). */
|
|
159
|
+
export function tierOf(cls) {
|
|
160
|
+
if (cls === UNCLASSED || cls == null) return cls;
|
|
161
|
+
const parts = String(cls).split(" ");
|
|
162
|
+
if (parts.length >= 2 && ["I", "II", "III"].includes(parts[parts.length - 1])) {
|
|
163
|
+
return parts.slice(0, -1).join(" ");
|
|
164
|
+
}
|
|
165
|
+
return cls;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Extract the sub-stage from a sub-stage string (e.g. "ARCH+ I" → "I").
|
|
169
|
+
* Mirrors the server's stageOf(). Returns null for non-sub-stage strings. */
|
|
170
|
+
export function stageOf(cls) {
|
|
171
|
+
if (cls === UNCLASSED || cls == null) return null;
|
|
172
|
+
const parts = String(cls).split(" ");
|
|
173
|
+
const stage = parts[parts.length - 1];
|
|
174
|
+
return stage === "I" || stage === "II" || stage === "III" ? stage : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Classify an operator's experience stage from total tokens (input + output +
|
|
178
|
+
* cacheCreate + cacheRead). Descending first-match-wins scan over
|
|
179
|
+
* RS05_CLASS_THRESHOLDS — mirrors the server's assignClass(totalTokens).
|
|
180
|
+
* Returns a full sub-stage string (e.g. "REFINER II") or UNCLASSED. */
|
|
181
|
+
export function classify(totalTokens) {
|
|
182
|
+
if (totalTokens == null || !Number.isFinite(totalTokens)) return UNCLASSED;
|
|
183
|
+
for (const t of RS05_CLASS_THRESHOLDS) {
|
|
184
|
+
if (totalTokens >= t.totalMin) return t.class;
|
|
185
|
+
}
|
|
186
|
+
return "IGNITER III";
|
|
82
187
|
}
|
|
83
188
|
|
|
84
189
|
/**
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.183",
|
|
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/",
|