sigrank 0.18.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +158 -0
- package/MCP_SERVER_DESCRIPTION.md +172 -0
- package/README.md +94 -41
- package/adapters/index.mjs +1100 -0
- package/adapters/tokenpull.mjs +568 -0
- package/adapters.mjs +1 -857
- package/analytics/cascade.mjs +338 -0
- package/cascade.mjs +1 -274
- package/cli.mjs +1 -1853
- package/connect.mjs +1 -13
- package/identity/connect.mjs +13 -0
- package/identity/keystore.mjs +231 -0
- package/identity/sign.mjs +106 -0
- package/index.mjs +47 -162
- package/keystore.mjs +1 -242
- package/lib/constants.mjs +137 -0
- package/lib/pkg-version.mjs +59 -0
- package/manifest.json +46 -0
- package/narrate.mjs +1 -77
- package/package.json +41 -16
- package/preflight.mjs +1 -191
- package/presentation/cli.mjs +1926 -0
- package/presentation/narrate.mjs +219 -0
- package/presentation/tui-themes.mjs +189 -0
- package/presentation/tui.mjs +3316 -0
- package/prompts/check-my-efficiency.md +1 -0
- package/prompts/compare-with-leader.md +1 -0
- package/prompts/simulate-improvement.md +1 -0
- package/proxy.mjs +492 -0
- package/resources/class-tiers.md +81 -0
- package/resources/data-policy.md +31 -0
- package/resources/install-guide.md +59 -0
- package/resources/privacy-model.md +44 -0
- package/resources/scoring-formula.md +24 -0
- package/review.mjs +263 -0
- package/sign.mjs +1 -106
- package/submit/index.mjs +297 -0
- package/submit/preflight.mjs +191 -0
- package/submit.mjs +1 -303
- package/tokenpull.mjs +1 -576
- package/tokscale_analytics.mjs +864 -0
- package/tools/_framing.mjs +217 -0
- package/tools/_helpers.mjs +253 -0
- package/tools/_peer-matching.mjs +361 -0
- package/tools/_report.mjs +262 -0
- package/tools/_schemas.mjs +492 -0
- package/tools/_verifiers.mjs +148 -0
- package/tools/compare-operators.mjs +80 -0
- package/tools/compare-self.mjs +124 -0
- package/tools/describe-power-user.mjs +78 -0
- package/tools/diagnose-cascade.mjs +192 -0
- package/tools/discover-peers.mjs +198 -0
- package/tools/enroll.mjs +119 -0
- package/tools/get-best-operator.mjs +59 -0
- package/tools/get-leaderboard.mjs +27 -0
- package/tools/get-operator.mjs +33 -0
- package/tools/index.mjs +229 -0
- package/tools/optimize-efficiency.mjs +170 -0
- package/tools/rank-paste.mjs +51 -0
- package/tools/rank-windows.mjs +120 -0
- package/tools/self-improve.mjs +423 -0
- package/tools/simulate-change.mjs +207 -0
- package/tools/submit-paste.mjs +121 -0
- package/tools/submit-verified.mjs +153 -0
- package/tools/suggest-improvements.mjs +168 -0
- package/tools/tokenpull-compare.mjs +123 -0
- package/tools/tokenpull-submit.mjs +235 -0
- package/tools/tokenpull.mjs +113 -0
- package/tools/tokscale-breakdown.mjs +119 -0
- package/tools/tokscale-competitive-intel.mjs +45 -0
- package/tools/tokscale-cost-analysis.mjs +31 -0
- package/tools/tokscale-developer-profile.mjs +36 -0
- package/tools/tokscale-device-profile.mjs +35 -0
- package/tools/tokscale-market-share.mjs +38 -0
- package/tools/tokscale-mcp-usage.mjs +33 -0
- package/tools/tokscale-model-trends.mjs +32 -0
- package/tools/watch-tokenpull.mjs +134 -0
- package/tools.mjs +1 -3897
- package/tui.mjs +4 -2650
- package/banner.svg +0 -7
package/connect.mjs
CHANGED
|
@@ -1,13 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// No fs, no I/O, no side effects — unit-testable in isolation. Used by tui.mjs to
|
|
3
|
-
// decide sign-in state and to classify keystrokes while the Connect code field is focused.
|
|
4
|
-
|
|
5
|
-
/** True iff this device identity is signed in (bound to an operator). */
|
|
6
|
-
export function isSignedIn(id) {
|
|
7
|
-
return !!(id && id.codename && id.operator_id);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** True iff `s` is a single character a connect code can contain ([A-Za-z0-9-]). */
|
|
11
|
-
export function isCodeChar(s) {
|
|
12
|
-
return typeof s === "string" && /^[A-Za-z0-9-]$/.test(s);
|
|
13
|
-
}
|
|
1
|
+
export * from './identity/connect.mjs';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// connect.mjs — pure predicates for the TUI consolidation (the TUI is the whole app).
|
|
2
|
+
// No fs, no I/O, no side effects — unit-testable in isolation. Used by tui.mjs to
|
|
3
|
+
// decide sign-in state and to classify keystrokes while the Connect code field is focused.
|
|
4
|
+
|
|
5
|
+
/** True iff this device identity is signed in (bound to an operator). */
|
|
6
|
+
export function isSignedIn(id) {
|
|
7
|
+
return !!(id && id.codename && id.operator_id);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** True iff `s` is a single character a connect code can contain ([A-Za-z0-9-]). */
|
|
11
|
+
export function isCodeChar(s) {
|
|
12
|
+
return typeof s === "string" && /^[A-Za-z0-9-]$/.test(s);
|
|
13
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// keystore.mjs — local ed25519 device identity for SigRank signed submit (D7 §4.5).
|
|
2
|
+
//
|
|
3
|
+
// Persists a per-device keypair at ~/.sigrank-mcp/identity.json (dir 0700, file 0600).
|
|
4
|
+
// The PRIVATE key never leaves this machine; only the raw 32-byte public key (as
|
|
5
|
+
// "ed25519:<base64>") is sent to the server at enroll time. The device_id (uuid) is
|
|
6
|
+
// generated once and is the immutable binding target server-side.
|
|
7
|
+
//
|
|
8
|
+
// Public-key export MUST match the server's publicKeyFromAgent: generateKeyPairSync
|
|
9
|
+
// exports SPKI DER with a 12-byte prefix; we strip it to the raw 32 bytes, base64 it,
|
|
10
|
+
// and prefix "ed25519:". The server re-adds the SPKI prefix to verify. Round-trip is
|
|
11
|
+
// locked by sign.test.mjs + the canon-parity fixture.
|
|
12
|
+
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
chmodSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
readdirSync,
|
|
23
|
+
copyFileSync,
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import { generateKeyPairSync, randomUUID } from "node:crypto";
|
|
26
|
+
import { agentVersion } from "../lib/pkg-version.mjs";
|
|
27
|
+
|
|
28
|
+
const DIR = join(homedir(), ".sigrank-mcp");
|
|
29
|
+
const PATH = join(DIR, "identity.json");
|
|
30
|
+
const BACKUP_PREFIX = "identity.json.bak-";
|
|
31
|
+
const MAX_BACKUPS = 5;
|
|
32
|
+
|
|
33
|
+
/** SPKI DER prefix length for an ed25519 public key (12 bytes before the raw 32). */
|
|
34
|
+
const SPKI_PREFIX_LEN = 12;
|
|
35
|
+
|
|
36
|
+
export function keystorePath() {
|
|
37
|
+
return PATH;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read the stored identity, or null if absent/corrupt. */
|
|
41
|
+
export function loadIdentity() {
|
|
42
|
+
if (!existsSync(PATH)) return null;
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readFileSync(PATH, "utf-8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Write the identity with strict perms (dir 0700, file 0600). Backs up the existing
|
|
51
|
+
* file before overwriting so a corrupt write or accidental reset can be recovered. */
|
|
52
|
+
export function persistIdentity(identity) {
|
|
53
|
+
mkdirSync(DIR, { recursive: true });
|
|
54
|
+
try {
|
|
55
|
+
chmodSync(DIR, 0o700);
|
|
56
|
+
} catch {
|
|
57
|
+
/* best-effort on platforms without chmod */
|
|
58
|
+
}
|
|
59
|
+
// Back up the existing identity before overwriting (resilience: if the write
|
|
60
|
+
// fails or the file is later lost, the backup carries the binding + keys).
|
|
61
|
+
if (existsSync(PATH)) {
|
|
62
|
+
try {
|
|
63
|
+
const stamp = Date.now();
|
|
64
|
+
copyFileSync(PATH, join(DIR, `${BACKUP_PREFIX}${stamp}`));
|
|
65
|
+
pruneBackups();
|
|
66
|
+
} catch {
|
|
67
|
+
/* best-effort — don't block the write */
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
writeFileSync(PATH, `${JSON.stringify(identity, null, 2)}\n`, {
|
|
71
|
+
mode: 0o600,
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
chmodSync(PATH, 0o600);
|
|
75
|
+
} catch {
|
|
76
|
+
/* best-effort */
|
|
77
|
+
}
|
|
78
|
+
return identity;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Build a fresh keypair record (pure — no fs). Exposed for tests. */
|
|
82
|
+
export function generateIdentity({ device_id } = {}) {
|
|
83
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
84
|
+
const spki = publicKey.export({ type: "spki", format: "der" });
|
|
85
|
+
const rawPub = Buffer.from(spki).subarray(SPKI_PREFIX_LEN); // the raw 32 bytes
|
|
86
|
+
const pkcs8 = privateKey.export({ type: "pkcs8", format: "der" });
|
|
87
|
+
return {
|
|
88
|
+
device_id: device_id || randomUUID(),
|
|
89
|
+
codename: null,
|
|
90
|
+
operator_id: null,
|
|
91
|
+
public_key: `ed25519:${rawPub.toString("base64")}`,
|
|
92
|
+
private_key_pkcs8_b64: Buffer.from(pkcs8).toString("base64"),
|
|
93
|
+
agent_version: agentVersion(),
|
|
94
|
+
enrolled_at: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Decide the binding (codename/operator_id/enrolled_at) to carry onto a freshly
|
|
100
|
+
* generated identity. The binding is tied to device_id: if device_id is CHANGING
|
|
101
|
+
* (a new device, e.g. after a reset/re-enroll where the old file was deleted), the
|
|
102
|
+
* old binding is INVALIDATED — never preserve a stale codename/operator onto a
|
|
103
|
+
* different device. That mismatch is the "Frankenstein identity" root cause: a new
|
|
104
|
+
* device_id + an old codename → the server sees a mismatch → tags submissions
|
|
105
|
+
* `unverified` → never ranks, yet `isSignedIn` reads the local codename as present.
|
|
106
|
+
* Only reuse the binding when the SAME device_id is being kept. Pure — exposed for
|
|
107
|
+
* tests (no fs, so the owner's live identity is never at risk during a test run).
|
|
108
|
+
*/
|
|
109
|
+
export function bindingForFreshIdentity(existing, fresh) {
|
|
110
|
+
if (!existing)
|
|
111
|
+
return { codename: null, operator_id: null, enrolled_at: null };
|
|
112
|
+
if (existing.device_id && fresh.device_id === existing.device_id) {
|
|
113
|
+
return {
|
|
114
|
+
codename: existing.codename ?? null,
|
|
115
|
+
operator_id: existing.operator_id ?? null,
|
|
116
|
+
enrolled_at: existing.enrolled_at ?? null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// device_id changed → the old binding belongs to a different device; drop it.
|
|
120
|
+
return { codename: null, operator_id: null, enrolled_at: null };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load the existing identity, or generate + persist a fresh one. Idempotent: a
|
|
125
|
+
* complete existing identity is returned untouched (never rotates a live key).
|
|
126
|
+
* Self-healing: if the identity has keys + device_id but no codename (binding
|
|
127
|
+
* lost via partial write / version transition), scans backups for a matching
|
|
128
|
+
* device_id and restores the binding before returning.
|
|
129
|
+
*/
|
|
130
|
+
export function ensureIdentity() {
|
|
131
|
+
const existing = loadIdentity();
|
|
132
|
+
if (
|
|
133
|
+
existing?.private_key_pkcs8_b64 &&
|
|
134
|
+
existing?.public_key &&
|
|
135
|
+
existing?.device_id
|
|
136
|
+
) {
|
|
137
|
+
// Self-healing: keys present but binding lost → try to restore from backups.
|
|
138
|
+
if (!existing.codename || !existing.operator_id) {
|
|
139
|
+
const restored = restoreBindingFromBackups(existing.device_id);
|
|
140
|
+
if (restored) {
|
|
141
|
+
existing.codename = restored.codename;
|
|
142
|
+
existing.operator_id = restored.operator_id;
|
|
143
|
+
existing.enrolled_at = restored.enrolled_at;
|
|
144
|
+
return persistIdentity(existing);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return existing;
|
|
148
|
+
}
|
|
149
|
+
// Regeneration path: a partial/corrupt record. The binding is tied to device_id —
|
|
150
|
+
// if a new device_id is generated, the old codename/operator MUST NOT carry over
|
|
151
|
+
// (bindingForFreshIdentity drops them). Only a reused device_id keeps its binding.
|
|
152
|
+
const fresh = generateIdentity({ device_id: existing?.device_id });
|
|
153
|
+
const binding = bindingForFreshIdentity(existing, fresh);
|
|
154
|
+
fresh.codename = binding.codename;
|
|
155
|
+
fresh.operator_id = binding.operator_id;
|
|
156
|
+
fresh.enrolled_at = binding.enrolled_at;
|
|
157
|
+
return persistIdentity(fresh);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Record a successful enrollment (codename/operator_id + enrolled_at) into the keystore. */
|
|
161
|
+
export function recordEnrollment({ codename, operator_id }) {
|
|
162
|
+
const id = ensureIdentity();
|
|
163
|
+
// OVERWRITE from the server's enroll response — a new enroll is a new binding, full
|
|
164
|
+
// stop. Never `??`-preserve a stale codename/operator onto this device (the other
|
|
165
|
+
// half of the Frankenstein bug: a re-enroll used to keep the old codename when the
|
|
166
|
+
// new one was absent). A 201 enrolled always carries both.
|
|
167
|
+
id.codename = codename ?? null;
|
|
168
|
+
id.operator_id = operator_id ?? null;
|
|
169
|
+
id.enrolled_at = new Date().toISOString();
|
|
170
|
+
return persistIdentity(id);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Clear the local identity (sign out). Next enroll provisions a fresh device_id. */
|
|
174
|
+
export function clearIdentity() {
|
|
175
|
+
try {
|
|
176
|
+
if (existsSync(PATH)) unlinkSync(PATH);
|
|
177
|
+
} catch {
|
|
178
|
+
/* best-effort */
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Keep at most MAX_BACKUPS backup files (oldest deleted first). */
|
|
183
|
+
function pruneBackups() {
|
|
184
|
+
try {
|
|
185
|
+
const files = readdirSync(DIR)
|
|
186
|
+
.filter((f) => f.startsWith(BACKUP_PREFIX))
|
|
187
|
+
.sort(); // timestamp-suffixed → lexicographic = chronological
|
|
188
|
+
while (files.length > MAX_BACKUPS) {
|
|
189
|
+
unlinkSync(join(DIR, files.shift()));
|
|
190
|
+
}
|
|
191
|
+
} catch {
|
|
192
|
+
/* best-effort */
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Scan backup files for one with the same device_id that carries a codename +
|
|
197
|
+
* operator_id binding. Returns { codename, operator_id, enrolled_at } or null.
|
|
198
|
+
* Used by ensureIdentity() to self-heal when the live file lost its binding
|
|
199
|
+
* (e.g. a partial write or version transition that preserved keys but dropped
|
|
200
|
+
* the binding metadata). Only restores from a backup with the SAME device_id —
|
|
201
|
+
* never crosses device boundaries (Frankenstein-identity guard). */
|
|
202
|
+
export function restoreBindingFromBackups(device_id) {
|
|
203
|
+
if (!device_id) return null;
|
|
204
|
+
try {
|
|
205
|
+
const files = readdirSync(DIR)
|
|
206
|
+
.filter((f) => f.startsWith(BACKUP_PREFIX))
|
|
207
|
+
.sort()
|
|
208
|
+
.reverse(); // newest first — prefer the most recent binding
|
|
209
|
+
for (const f of files) {
|
|
210
|
+
try {
|
|
211
|
+
const bak = JSON.parse(readFileSync(join(DIR, f), "utf-8"));
|
|
212
|
+
if (
|
|
213
|
+
bak.device_id === device_id &&
|
|
214
|
+
bak.codename &&
|
|
215
|
+
bak.operator_id
|
|
216
|
+
) {
|
|
217
|
+
return {
|
|
218
|
+
codename: bak.codename,
|
|
219
|
+
operator_id: bak.operator_id,
|
|
220
|
+
enrolled_at: bak.enrolled_at ?? null,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
/* skip corrupt backups */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
/* best-effort */
|
|
229
|
+
}
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// sign.mjs — canonical JSON + ed25519 signing for SigRank signed snapshot submit.
|
|
2
|
+
//
|
|
3
|
+
// BYTE-COMPATIBLE PORT of sigrank-app lib/ingest/signature.ts. This is the highest-
|
|
4
|
+
// risk file in the client: the server VERIFIES the signature over ITS recomputation
|
|
5
|
+
// of these exact canonical bytes, so a single divergent byte hard-rejects (422) every
|
|
6
|
+
// verified submission. Keep this a line-for-line mirror of the server canonicalizer:
|
|
7
|
+
// canonical JSON = recursively sorted keys, compact separators, UTF-8, with the
|
|
8
|
+
// derived agent.signature + agent.snapshot_hash stripped before serialization.
|
|
9
|
+
// snapshot_hash = "sha256:" + hex(sha256(canonical_bytes)).
|
|
10
|
+
// public key = "ed25519:<base64 of the 32 raw verify-key bytes>".
|
|
11
|
+
// signature = base64 of the 64-byte ed25519 signature over canonical_bytes.
|
|
12
|
+
// The canon-parity fixture (tests/fixtures/canon_parity.json) + sign.test.mjs + the
|
|
13
|
+
// sigrank-app canon_parity gate together lock this byte-for-byte.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createHash,
|
|
17
|
+
createPrivateKey,
|
|
18
|
+
createPublicKey,
|
|
19
|
+
sign as edSign,
|
|
20
|
+
verify as edVerify,
|
|
21
|
+
} from "node:crypto";
|
|
22
|
+
|
|
23
|
+
/** Agent fields excluded from the canonical (signed/hashed) body — they derive from it. */
|
|
24
|
+
const DERIVED_AGENT_FIELDS = ["signature", "snapshot_hash"];
|
|
25
|
+
|
|
26
|
+
/** Recursively sort object keys (matches the server's Object.keys(...).sort()); arrays keep order. */
|
|
27
|
+
function sortDeep(v) {
|
|
28
|
+
if (Array.isArray(v)) return v.map(sortDeep);
|
|
29
|
+
if (v && typeof v === "object") {
|
|
30
|
+
const out = {};
|
|
31
|
+
for (const k of Object.keys(v).sort()) out[k] = sortDeep(v[k]);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
return v;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Canonical JSON string the agent signs/hashes (derived agent fields stripped). */
|
|
38
|
+
export function canonicalJson(payload) {
|
|
39
|
+
const clone = JSON.parse(JSON.stringify(payload));
|
|
40
|
+
const agent = clone.agent;
|
|
41
|
+
if (agent && typeof agent === "object") {
|
|
42
|
+
for (const f of DERIVED_AGENT_FIELDS) delete agent[f];
|
|
43
|
+
}
|
|
44
|
+
return JSON.stringify(sortDeep(clone));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** UTF-8 bytes of the canonical JSON — the exact bytes that get signed. */
|
|
48
|
+
export function canonicalBytes(payload) {
|
|
49
|
+
return Buffer.from(canonicalJson(payload), "utf-8");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** "sha256:<hex>" over the canonical bytes (matches the server snapshotHash). */
|
|
53
|
+
export function snapshotHash(payload) {
|
|
54
|
+
return `sha256:${createHash("sha256").update(canonicalBytes(payload)).digest("hex")}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** SPKI DER prefix for a raw 32-byte ed25519 public key. */
|
|
58
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
59
|
+
|
|
60
|
+
/** Build a node KeyObject from an "ed25519:<base64>" (or bare base64) public key, or null. */
|
|
61
|
+
function publicKeyFrom(pk) {
|
|
62
|
+
try {
|
|
63
|
+
const body = pk.startsWith("ed25519:") ? pk.slice("ed25519:".length) : pk;
|
|
64
|
+
const raw = Buffer.from(body, "base64");
|
|
65
|
+
if (raw.length !== 32) return null;
|
|
66
|
+
return createPublicKey({
|
|
67
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, raw]),
|
|
68
|
+
format: "der",
|
|
69
|
+
type: "spki",
|
|
70
|
+
});
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sign the payload's canonical bytes with the PKCS8-DER-base64 private key → base64 sig.
|
|
78
|
+
* The returned string is the X-Agent-Signature header value.
|
|
79
|
+
*/
|
|
80
|
+
export function signPayload(payload, privateKeyPkcs8B64) {
|
|
81
|
+
const key = createPrivateKey({
|
|
82
|
+
key: Buffer.from(privateKeyPkcs8B64, "base64"),
|
|
83
|
+
format: "der",
|
|
84
|
+
type: "pkcs8",
|
|
85
|
+
});
|
|
86
|
+
return edSign(null, canonicalBytes(payload), key).toString("base64");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Local verify (parity with the server's verifySignature) — used by the self-test
|
|
91
|
+
* and as a pre-send sanity check. Returns false on any malformation, never throws.
|
|
92
|
+
*/
|
|
93
|
+
export function verifyPayload(payload, signatureB64, publicKey) {
|
|
94
|
+
const key = publicKeyFrom(publicKey);
|
|
95
|
+
if (!key) return false;
|
|
96
|
+
try {
|
|
97
|
+
return edVerify(
|
|
98
|
+
null,
|
|
99
|
+
canonicalBytes(payload),
|
|
100
|
+
key,
|
|
101
|
+
Buffer.from(signatureB64, "base64"),
|
|
102
|
+
);
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
package/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* npx sigrank enroll sign in: redeem a connect code from signalaf.com
|
|
12
12
|
* npx sigrank submit publish your verified runs to the board
|
|
13
13
|
* npx sigrank board | compare | watch read / publish helpers
|
|
14
|
+
* npx sigrank proxy opt-in local Anthropic/OpenAI usage proxy
|
|
14
15
|
* npx sigrank --help full reference
|
|
15
16
|
*
|
|
16
17
|
* For AI clients (NOT human commands): in a piped/non-TTY context this starts an MCP
|
|
@@ -101,7 +102,7 @@ async function startMcpServer() {
|
|
|
101
102
|
{
|
|
102
103
|
name: "check-my-efficiency",
|
|
103
104
|
description:
|
|
104
|
-
"Pull your local token usage, compute your SigRank yield, and compare your efficiency against the leaderboard. Shows your class tier (
|
|
105
|
+
"Pull your local token usage, compute your SigRank yield, and compare your efficiency against the leaderboard. Shows your class tier (ARCH+ to IGNITER) and where you rank.",
|
|
105
106
|
arguments: [],
|
|
106
107
|
},
|
|
107
108
|
{
|
|
@@ -138,7 +139,10 @@ async function startMcpServer() {
|
|
|
138
139
|
role: "user",
|
|
139
140
|
content: {
|
|
140
141
|
type: "text",
|
|
141
|
-
text:
|
|
142
|
+
text: readFileSync(
|
|
143
|
+
new URL("./prompts/check-my-efficiency.md", import.meta.url),
|
|
144
|
+
"utf8",
|
|
145
|
+
).trim(),
|
|
142
146
|
},
|
|
143
147
|
},
|
|
144
148
|
],
|
|
@@ -146,13 +150,17 @@ async function startMcpServer() {
|
|
|
146
150
|
}
|
|
147
151
|
if (name === "simulate-improvement") {
|
|
148
152
|
const change = args.change || "increase cache reads by 50000";
|
|
153
|
+
const template = readFileSync(
|
|
154
|
+
new URL("./prompts/simulate-improvement.md", import.meta.url),
|
|
155
|
+
"utf8",
|
|
156
|
+
);
|
|
149
157
|
return {
|
|
150
158
|
messages: [
|
|
151
159
|
{
|
|
152
160
|
role: "user",
|
|
153
161
|
content: {
|
|
154
162
|
type: "text",
|
|
155
|
-
text:
|
|
163
|
+
text: template.replace(/\{\{change\}\}/g, change).trim(),
|
|
156
164
|
},
|
|
157
165
|
},
|
|
158
166
|
],
|
|
@@ -165,7 +173,10 @@ async function startMcpServer() {
|
|
|
165
173
|
role: "user",
|
|
166
174
|
content: {
|
|
167
175
|
type: "text",
|
|
168
|
-
text:
|
|
176
|
+
text: readFileSync(
|
|
177
|
+
new URL("./prompts/compare-with-leader.md", import.meta.url),
|
|
178
|
+
"utf8",
|
|
179
|
+
).trim(),
|
|
169
180
|
},
|
|
170
181
|
},
|
|
171
182
|
],
|
|
@@ -180,14 +191,14 @@ async function startMcpServer() {
|
|
|
180
191
|
uri: "sigrank://scoring-formula",
|
|
181
192
|
name: "SigRank Scoring Formula",
|
|
182
193
|
description:
|
|
183
|
-
"The yield cascade formula and all derived metrics: Υ (Yield = Cache Reads × Output / Input²), SNR (signal-to-noise), Leverage (Cr/I), Velocity (O/I), 10xDEV score, and class tier thresholds (
|
|
194
|
+
"The yield cascade formula and all derived metrics: Υ (Yield = Cache Reads × Output / Input²), SNR (signal-to-noise), Leverage (Cr/I), Velocity (O/I), 10xDEV score, and class tier thresholds (8-tier experience ladder, see class-tiers.md).",
|
|
184
195
|
mimeType: "text/markdown",
|
|
185
196
|
},
|
|
186
197
|
{
|
|
187
198
|
uri: "sigrank://class-tiers",
|
|
188
199
|
name: "Class Tier Definitions",
|
|
189
200
|
description:
|
|
190
|
-
"Definitions and thresholds for the
|
|
201
|
+
"Definitions and thresholds for the eight SigRank operator class tiers (ARCH+ through IGNITER) plus the TRANSMITTER peak badge. See class-tiers.md for the full ladder.",
|
|
191
202
|
mimeType: "text/markdown",
|
|
192
203
|
},
|
|
193
204
|
{
|
|
@@ -204,156 +215,21 @@ async function startMcpServer() {
|
|
|
204
215
|
"How SigRank protects user privacy: runs locally, only four token counts (input, output, cacheCreate, cacheRead) leave the machine, ed25519-signed submissions, no prompt content ever transmitted.",
|
|
205
216
|
mimeType: "text/markdown",
|
|
206
217
|
},
|
|
218
|
+
{
|
|
219
|
+
uri: "sigrank://data-policy",
|
|
220
|
+
name: "Data Policy",
|
|
221
|
+
description:
|
|
222
|
+
"Summary of SigRank data governance: what we collect, what we do not collect, consent requirements, and how to pause, delete, or export your data.",
|
|
223
|
+
mimeType: "text/markdown",
|
|
224
|
+
},
|
|
207
225
|
];
|
|
208
226
|
|
|
209
|
-
const
|
|
210
|
-
"sigrank://scoring-formula":
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
Υ = Cache Reads × Output / Input²
|
|
216
|
-
\`\`\`
|
|
217
|
-
|
|
218
|
-
Yield rewards operators who maximize output while minimizing input — and who build cache to amortize that input across sessions. A high yield means you're getting more done with less, efficiently.
|
|
219
|
-
|
|
220
|
-
## Derived Metrics
|
|
221
|
-
|
|
222
|
-
| Metric | Formula | Meaning |
|
|
223
|
-
|--------|---------|---------|
|
|
224
|
-
| **SNR** | Output / (Input + CacheCreate) | Signal-to-noise: how much of your token spend is productive output vs. overhead |
|
|
225
|
-
| **Leverage** | Cache Reads / Input | How well you reuse cached context — higher = better cache utilization |
|
|
226
|
-
| **Velocity** | Output / Input | Raw output efficiency — how much output you generate per token of input |
|
|
227
|
-
| **10xDEV** | Composite score | Weighted blend of yield, leverage, and velocity for cross-platform comparison |
|
|
228
|
-
|
|
229
|
-
## Class Tier Thresholds
|
|
230
|
-
|
|
231
|
-
| Tier | Yield Range | Profile |
|
|
232
|
-
|------|-------------|---------|
|
|
233
|
-
| **Burner** | Υ < 1.0 | Raw volume — high input, low cache reuse, brute-force output |
|
|
234
|
-
| **Builder** | 1.0 ≤ Υ < 10.0 | Balanced — moderate cache, decent output efficiency |
|
|
235
|
-
| **10xer** | Υ ≥ 10.0 | Cascade-optimized — high cache reads, minimal input, efficient output |
|
|
236
|
-
|
|
237
|
-
The formula is deterministic and computed locally. No network calls needed for scoring.
|
|
238
|
-
`,
|
|
239
|
-
|
|
240
|
-
"sigrank://class-tiers": `# SigRank Class Tiers
|
|
241
|
-
|
|
242
|
-
Every operator is classified into one of three tiers based on their Yield (Υ = Cache Reads × Output / Input²).
|
|
243
|
-
|
|
244
|
-
## Burner (Υ < 1.0)
|
|
245
|
-
- **Profile:** Raw volume operators. High input tokens, low cache reuse.
|
|
246
|
-
- **Behavior:** Brute-force — lots of context fed in, relatively little output back.
|
|
247
|
-
- **Typical:** New AI users, verbose prompters, no session continuity.
|
|
248
|
-
- **Fix:** Build cache across sessions. Stop re-explaining context. Use --continue.
|
|
249
|
-
|
|
250
|
-
## Builder (1.0 ≤ Υ < 10.0)
|
|
251
|
-
- **Profile:** Balanced operators. Moderate cache, decent output efficiency.
|
|
252
|
-
- **Behavior:** Productive — reasonable input-to-output ratio, some cache leverage.
|
|
253
|
-
- **Typical:** Experienced AI coders who use CLAUDE.md, project context, and session continuity.
|
|
254
|
-
- **Fix:** Increase cache reads by reusing sessions. Reduce input by trimming unnecessary context.
|
|
255
|
-
|
|
256
|
-
## 10xer (Υ ≥ 10.0)
|
|
257
|
-
- **Profile:** Cascade-optimized operators. High cache reads, minimal input, efficient output.
|
|
258
|
-
- **Behavior:** Surgical — minimal new input, maximum cache reuse, high-yield output.
|
|
259
|
-
- **Typical:** Power users with long-running sessions, tight context windows, and aggressive cache strategies.
|
|
260
|
-
- **Maintain:** Keep cache hit rate high. Avoid context bloat. Monitor with watch_tokenpull.
|
|
261
|
-
|
|
262
|
-
Tiers are recalculated on every submission. Your tier can change between windows (7d, 30d, 90d, all-time).
|
|
263
|
-
`,
|
|
264
|
-
|
|
265
|
-
"sigrank://install-guide": `# SigRank Quick Start
|
|
266
|
-
|
|
267
|
-
## Install
|
|
268
|
-
|
|
269
|
-
\`\`\`bash
|
|
270
|
-
npx sigrank
|
|
271
|
-
\`\`\`
|
|
272
|
-
|
|
273
|
-
No global install needed. The MCP server starts automatically when an AI client (Claude, Cursor, Cline) connects.
|
|
274
|
-
|
|
275
|
-
## First Submission
|
|
276
|
-
|
|
277
|
-
1. **Enroll** — register your operator codename:
|
|
278
|
-
\`\`\`bash
|
|
279
|
-
npx sigrank enroll
|
|
280
|
-
\`\`\`
|
|
281
|
-
|
|
282
|
-
2. **Pull + Submit** — scan your local AI session logs and publish:
|
|
283
|
-
\`\`\`bash
|
|
284
|
-
npx sigrank submit
|
|
285
|
-
\`\`\`
|
|
286
|
-
|
|
287
|
-
3. **Dry Run** — see exactly what would be sent before publishing:
|
|
288
|
-
\`\`\`bash
|
|
289
|
-
npx sigrank submit --dry-run
|
|
290
|
-
\`\`\`
|
|
291
|
-
|
|
292
|
-
## Token Pull Sources
|
|
293
|
-
|
|
294
|
-
SigRank bundles two token readers as npm dependencies:
|
|
295
|
-
- **ccusage** — Claude Code session logs
|
|
296
|
-
- **tokscale** — multi-platform token telemetry
|
|
297
|
-
|
|
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.
|
|
306
|
-
|
|
307
|
-
## MCP Client Setup
|
|
308
|
-
|
|
309
|
-
Add to your MCP client config:
|
|
310
|
-
\`\`\`json
|
|
311
|
-
{
|
|
312
|
-
"mcpServers": {
|
|
313
|
-
"sigrank": {
|
|
314
|
-
"command": "npx",
|
|
315
|
-
"args": ["sigrank"]
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
\`\`\`
|
|
320
|
-
|
|
321
|
-
## Live Board
|
|
322
|
-
|
|
323
|
-
Visit https://signalaf.com to see the global leaderboard.
|
|
324
|
-
`,
|
|
325
|
-
|
|
326
|
-
"sigrank://privacy-model": `# SigRank Privacy Model
|
|
327
|
-
|
|
328
|
-
## What leaves your machine
|
|
329
|
-
|
|
330
|
-
**Only four numbers per submission:**
|
|
331
|
-
- Input tokens
|
|
332
|
-
- Output tokens
|
|
333
|
-
- Cache create tokens
|
|
334
|
-
- Cache read tokens
|
|
335
|
-
|
|
336
|
-
**That's it.** No prompts, no code, no file contents, no conversation text.
|
|
337
|
-
|
|
338
|
-
## How it works
|
|
339
|
-
|
|
340
|
-
1. **Local-first:** All token pulling happens on your machine. SigRank reads session logs from ~/.claude, ~/.codex, ~/.local/share/amp, etc.
|
|
341
|
-
2. **Token counts only:** The MCP tools extract integer counts from log metadata. The actual content of your conversations is never read, parsed, or transmitted.
|
|
342
|
-
3. **Signed submission:** Submissions are ed25519-signed. The board verifies authenticity without seeing your data.
|
|
343
|
-
4. **No auth required:** No API keys, no OAuth, no account needed to read the leaderboard. Enrollment only requires a codename.
|
|
344
|
-
|
|
345
|
-
## What SigRank can NOT see
|
|
346
|
-
|
|
347
|
-
- Your prompts or messages
|
|
348
|
-
- Your code or file contents
|
|
349
|
-
- Your tool calls or their results
|
|
350
|
-
- Which AI platform you use (beyond token counts)
|
|
351
|
-
- Your identity (only your chosen codename)
|
|
352
|
-
|
|
353
|
-
## Verification
|
|
354
|
-
|
|
355
|
-
The submit_verified tool uses ed25519 signing. The board's source_attestations table records the signature for audit. You can verify your own submissions via get_operator.
|
|
356
|
-
`,
|
|
227
|
+
const RESOURCE_FILES = {
|
|
228
|
+
"sigrank://scoring-formula": "./resources/scoring-formula.md",
|
|
229
|
+
"sigrank://class-tiers": "./resources/class-tiers.md",
|
|
230
|
+
"sigrank://install-guide": "./resources/install-guide.md",
|
|
231
|
+
"sigrank://privacy-model": "./resources/privacy-model.md",
|
|
232
|
+
"sigrank://data-policy": "./resources/data-policy.md",
|
|
357
233
|
};
|
|
358
234
|
|
|
359
235
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
@@ -361,10 +237,11 @@ The submit_verified tool uses ed25519 signing. The board's source_attestations t
|
|
|
361
237
|
}));
|
|
362
238
|
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
363
239
|
const uri = req.params.uri;
|
|
364
|
-
const
|
|
365
|
-
if (!
|
|
240
|
+
const file = RESOURCE_FILES[uri];
|
|
241
|
+
if (!file) {
|
|
366
242
|
throw new McpError(ErrorCode.InvalidParams, `Unknown resource: ${uri}`);
|
|
367
243
|
}
|
|
244
|
+
const content = readFileSync(new URL(file, import.meta.url), "utf8");
|
|
368
245
|
return {
|
|
369
246
|
contents: [
|
|
370
247
|
{
|
|
@@ -380,15 +257,23 @@ The submit_verified tool uses ed25519 signing. The board's source_attestations t
|
|
|
380
257
|
}
|
|
381
258
|
|
|
382
259
|
// Route:
|
|
383
|
-
// any arg
|
|
384
|
-
//
|
|
385
|
-
// no args + TTY stdout → full tabbed TUI (interactive use)
|
|
386
|
-
// no args + piped
|
|
260
|
+
// any arg → the CLI (runCli shows help + exits non-zero on unknown commands,
|
|
261
|
+
// so a typo never silently starts a hung MCP stdio server on a TTY)
|
|
262
|
+
// no args + TTY stdin+stdout → full tabbed TUI (interactive use — needs keyboard input)
|
|
263
|
+
// no args + piped (either) → MCP stdio server (AI client / proxy / container use)
|
|
264
|
+
// SIGRANK_MCP_SERVER=1 → force MCP stdio server (manual override)
|
|
265
|
+
//
|
|
266
|
+
// The stdin check is critical for container/proxy environments (Glama, mcp-proxy,
|
|
267
|
+
// Docker with -t). These may allocate a pseudo-TTY for stdout, but stdin is piped
|
|
268
|
+
// because the proxy writes JSON-RPC messages to it. The TUI needs keyboard input
|
|
269
|
+
// from stdin, so if stdin is piped the TUI would hang — force MCP mode instead.
|
|
387
270
|
const cliArgs = process.argv.slice(2);
|
|
388
271
|
if (cliArgs.length > 0) {
|
|
389
272
|
runCli(process.argv);
|
|
390
|
-
} else if (process.
|
|
391
|
-
|
|
273
|
+
} else if (process.env.SIGRANK_MCP_SERVER === "1") {
|
|
274
|
+
startMcpServer();
|
|
275
|
+
} else if (process.stdout.isTTY && process.stdin.isTTY) {
|
|
276
|
+
// Interactive terminal with keyboard — launch the full tabbed TUI
|
|
392
277
|
const { runTui } = await import("./tui.mjs");
|
|
393
278
|
runTui();
|
|
394
279
|
} else {
|