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.
Files changed (80) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/MCP_SERVER_DESCRIPTION.md +172 -0
  3. package/README.md +94 -41
  4. package/adapters/index.mjs +1100 -0
  5. package/adapters/tokenpull.mjs +568 -0
  6. package/adapters.mjs +1 -857
  7. package/analytics/cascade.mjs +338 -0
  8. package/cascade.mjs +1 -274
  9. package/cli.mjs +1 -1853
  10. package/connect.mjs +1 -13
  11. package/identity/connect.mjs +13 -0
  12. package/identity/keystore.mjs +231 -0
  13. package/identity/sign.mjs +106 -0
  14. package/index.mjs +47 -162
  15. package/keystore.mjs +1 -242
  16. package/lib/constants.mjs +137 -0
  17. package/lib/pkg-version.mjs +59 -0
  18. package/manifest.json +46 -0
  19. package/narrate.mjs +1 -77
  20. package/package.json +41 -16
  21. package/preflight.mjs +1 -191
  22. package/presentation/cli.mjs +1926 -0
  23. package/presentation/narrate.mjs +219 -0
  24. package/presentation/tui-themes.mjs +189 -0
  25. package/presentation/tui.mjs +3316 -0
  26. package/prompts/check-my-efficiency.md +1 -0
  27. package/prompts/compare-with-leader.md +1 -0
  28. package/prompts/simulate-improvement.md +1 -0
  29. package/proxy.mjs +492 -0
  30. package/resources/class-tiers.md +81 -0
  31. package/resources/data-policy.md +31 -0
  32. package/resources/install-guide.md +59 -0
  33. package/resources/privacy-model.md +44 -0
  34. package/resources/scoring-formula.md +24 -0
  35. package/review.mjs +263 -0
  36. package/sign.mjs +1 -106
  37. package/submit/index.mjs +297 -0
  38. package/submit/preflight.mjs +191 -0
  39. package/submit.mjs +1 -303
  40. package/tokenpull.mjs +1 -576
  41. package/tokscale_analytics.mjs +864 -0
  42. package/tools/_framing.mjs +217 -0
  43. package/tools/_helpers.mjs +253 -0
  44. package/tools/_peer-matching.mjs +361 -0
  45. package/tools/_report.mjs +262 -0
  46. package/tools/_schemas.mjs +492 -0
  47. package/tools/_verifiers.mjs +148 -0
  48. package/tools/compare-operators.mjs +80 -0
  49. package/tools/compare-self.mjs +124 -0
  50. package/tools/describe-power-user.mjs +78 -0
  51. package/tools/diagnose-cascade.mjs +192 -0
  52. package/tools/discover-peers.mjs +198 -0
  53. package/tools/enroll.mjs +119 -0
  54. package/tools/get-best-operator.mjs +59 -0
  55. package/tools/get-leaderboard.mjs +27 -0
  56. package/tools/get-operator.mjs +33 -0
  57. package/tools/index.mjs +229 -0
  58. package/tools/optimize-efficiency.mjs +170 -0
  59. package/tools/rank-paste.mjs +51 -0
  60. package/tools/rank-windows.mjs +120 -0
  61. package/tools/self-improve.mjs +423 -0
  62. package/tools/simulate-change.mjs +207 -0
  63. package/tools/submit-paste.mjs +121 -0
  64. package/tools/submit-verified.mjs +153 -0
  65. package/tools/suggest-improvements.mjs +168 -0
  66. package/tools/tokenpull-compare.mjs +123 -0
  67. package/tools/tokenpull-submit.mjs +235 -0
  68. package/tools/tokenpull.mjs +113 -0
  69. package/tools/tokscale-breakdown.mjs +119 -0
  70. package/tools/tokscale-competitive-intel.mjs +45 -0
  71. package/tools/tokscale-cost-analysis.mjs +31 -0
  72. package/tools/tokscale-developer-profile.mjs +36 -0
  73. package/tools/tokscale-device-profile.mjs +35 -0
  74. package/tools/tokscale-market-share.mjs +38 -0
  75. package/tools/tokscale-mcp-usage.mjs +33 -0
  76. package/tools/tokscale-model-trends.mjs +32 -0
  77. package/tools/watch-tokenpull.mjs +134 -0
  78. package/tools.mjs +1 -3897
  79. package/tui.mjs +4 -2650
  80. package/banner.svg +0 -7
@@ -0,0 +1 @@
1
+ Pull my local token usage with tokenpull, compute my yield, and then fetch the leaderboard so I can see where I rank. Show me my class tier and my key metrics (Yield, Leverage, Velocity).
@@ -0,0 +1 @@
1
+ Fetch the SigRank leaderboard with get_leaderboard, then pull my local token usage with tokenpull. Compare my yield, leverage, and velocity against the top operator. What's the gap and what would I need to change to close it?
@@ -0,0 +1 @@
1
+ First pull my local token usage with tokenpull to get my current pillars. Then use simulate_change to answer: "what if I {{change}}?" Show me the yield delta and whether my class tier would change.
package/proxy.mjs ADDED
@@ -0,0 +1,492 @@
1
+ /**
2
+ * Local, opt-in API proxy for provider-reported token usage.
3
+ *
4
+ * The proxy is inert until `sigrank proxy` starts it. It binds to loopback,
5
+ * forwards request/response bytes, and persists usage metadata only — never
6
+ * prompts, API keys, response text, or tool calls.
7
+ */
8
+
9
+ import http from "node:http";
10
+ import https from "node:https";
11
+ import { appendFile, chmod, mkdir } from "node:fs/promises";
12
+ import { homedir } from "node:os";
13
+ import { dirname, join } from "node:path";
14
+
15
+ const DEFAULT_HOST = "127.0.0.1";
16
+ const DEFAULT_PORT = 8787;
17
+ const DEFAULT_UPSTREAMS = Object.freeze({
18
+ anthropic: "https://api.anthropic.com",
19
+ openai: "https://api.openai.com",
20
+ });
21
+
22
+ const HOP_BY_HOP_HEADERS = new Set([
23
+ "connection",
24
+ "keep-alive",
25
+ "proxy-authenticate",
26
+ "proxy-authorization",
27
+ "te",
28
+ "trailer",
29
+ "transfer-encoding",
30
+ "upgrade",
31
+ ]);
32
+
33
+ export function defaultProxyLogPath() {
34
+ return join(homedir(), ".sigrank-mcp", "proxy-sessions.jsonl");
35
+ }
36
+
37
+ export function parseProxyPort(value, { allowZero = false } = {}) {
38
+ const port = Number(value);
39
+ const min = allowZero ? 0 : 1;
40
+ if (!Number.isInteger(port) || port < min || port > 65_535) {
41
+ throw new Error(`Invalid proxy port "${value}" (expected ${min}-${65_535})`);
42
+ }
43
+ return port;
44
+ }
45
+
46
+ function routeFor(pathname) {
47
+ if (pathname === "/v1/messages") {
48
+ return { backend: "anthropic", endpoint: "messages" };
49
+ }
50
+ if (pathname === "/v1/chat/completions") {
51
+ return { backend: "openai", endpoint: "chat-completions" };
52
+ }
53
+ if (pathname === "/v1/responses") {
54
+ return { backend: "openai", endpoint: "responses" };
55
+ }
56
+ return null;
57
+ }
58
+
59
+ function finiteTokenCount(value) {
60
+ const n = Number(value);
61
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
62
+ }
63
+
64
+ function usageFields(backend, usage) {
65
+ if (!usage || typeof usage !== "object") return null;
66
+ if (backend === "anthropic") {
67
+ return {
68
+ input: finiteTokenCount(usage.input_tokens),
69
+ output: finiteTokenCount(usage.output_tokens),
70
+ cacheRead: finiteTokenCount(usage.cache_read_input_tokens),
71
+ cacheCreate: finiteTokenCount(usage.cache_creation_input_tokens),
72
+ };
73
+ }
74
+
75
+ const totalInput = finiteTokenCount(
76
+ usage.prompt_tokens ?? usage.input_tokens,
77
+ );
78
+ const cacheRead = finiteTokenCount(
79
+ usage.prompt_tokens_details?.cached_tokens ??
80
+ usage.input_tokens_details?.cached_tokens,
81
+ );
82
+ return {
83
+ // OpenAI's prompt/input total includes cached tokens. SigRank's input pillar
84
+ // is fresh input, so subtract the separately reported cache read amount.
85
+ input: Math.max(0, totalInput - cacheRead),
86
+ output: finiteTokenCount(
87
+ usage.completion_tokens ?? usage.output_tokens,
88
+ ),
89
+ cacheRead,
90
+ cacheCreate: 0,
91
+ };
92
+ }
93
+
94
+ function mergeDefinedUsage(target, source) {
95
+ if (!source || typeof source !== "object") return false;
96
+ let found = false;
97
+ for (const key of [
98
+ "input_tokens",
99
+ "output_tokens",
100
+ "cache_read_input_tokens",
101
+ "cache_creation_input_tokens",
102
+ ]) {
103
+ if (source[key] != null) {
104
+ target[key] = source[key];
105
+ found = true;
106
+ }
107
+ }
108
+ return found;
109
+ }
110
+
111
+ function createUsageTracker({ backend, endpoint, requestModel, warn }) {
112
+ let model = requestModel || null;
113
+ let usage = null;
114
+ const anthropicUsage = {};
115
+
116
+ function observe(payload) {
117
+ if (!payload || typeof payload !== "object") return;
118
+
119
+ if (backend === "anthropic") {
120
+ model = payload.message?.model || payload.model || model;
121
+ const next = payload.message?.usage || payload.usage;
122
+ if (mergeDefinedUsage(anthropicUsage, next)) usage = anthropicUsage;
123
+ return;
124
+ }
125
+
126
+ if (endpoint === "responses") {
127
+ const response = payload.response || payload;
128
+ model = response.model || model;
129
+ if (response.usage && typeof response.usage === "object") {
130
+ usage = response.usage;
131
+ }
132
+ return;
133
+ }
134
+
135
+ model = payload.model || model;
136
+ if (payload.usage && typeof payload.usage === "object") {
137
+ usage = payload.usage;
138
+ }
139
+ }
140
+
141
+ function observeJson(text) {
142
+ try {
143
+ observe(JSON.parse(text));
144
+ } catch (error) {
145
+ warn(`[proxy] usage parse warning: ${error.message}`);
146
+ }
147
+ }
148
+
149
+ function result() {
150
+ const fields = usageFields(backend, usage);
151
+ return fields ? { model: model || "unknown", ...fields } : null;
152
+ }
153
+
154
+ return { observe, observeJson, result };
155
+ }
156
+
157
+ /** Incremental SSE parser. Network chunks are not assumed to align with events. */
158
+ function createSseInspector(onData, warn) {
159
+ const decoder = new TextDecoder();
160
+ let buffer = "";
161
+
162
+ function parseFrame(frame) {
163
+ const data = frame
164
+ .split(/\r?\n/)
165
+ .filter((line) => line.startsWith("data:"))
166
+ .map((line) => line.slice(5).replace(/^ /, ""))
167
+ .join("\n");
168
+ if (!data || data === "[DONE]") return;
169
+ try {
170
+ onData(JSON.parse(data));
171
+ } catch (error) {
172
+ warn(`[proxy] SSE usage parse warning: ${error.message}`);
173
+ }
174
+ }
175
+
176
+ function drain(final = false) {
177
+ let match;
178
+ while ((match = /\r?\n\r?\n/.exec(buffer))) {
179
+ const frame = buffer.slice(0, match.index);
180
+ buffer = buffer.slice(match.index + match[0].length);
181
+ parseFrame(frame);
182
+ }
183
+ if (final && buffer.trim()) {
184
+ parseFrame(buffer);
185
+ buffer = "";
186
+ }
187
+ }
188
+
189
+ return {
190
+ push(chunk) {
191
+ buffer += decoder.decode(chunk, { stream: true });
192
+ drain(false);
193
+ },
194
+ end() {
195
+ buffer += decoder.decode();
196
+ drain(true);
197
+ },
198
+ };
199
+ }
200
+
201
+ function sanitizeRequestHeaders(headers, bodyLength) {
202
+ const out = { ...headers };
203
+ for (const name of HOP_BY_HOP_HEADERS) delete out[name];
204
+ delete out.host;
205
+ // Usage inspection operates on the raw response. Asking upstream for identity
206
+ // encoding avoids buffering/decompressing streamed provider responses.
207
+ out["accept-encoding"] = "identity";
208
+ out["content-length"] = String(bodyLength);
209
+ return out;
210
+ }
211
+
212
+ function sanitizeResponseHeaders(headers) {
213
+ const out = { ...headers };
214
+ for (const name of HOP_BY_HOP_HEADERS) delete out[name];
215
+ return out;
216
+ }
217
+
218
+ function prepareRequestBody(route, rawBody, injectOpenAIUsage, warn) {
219
+ let json = null;
220
+ try {
221
+ json = JSON.parse(rawBody.toString("utf8"));
222
+ } catch {
223
+ return { body: rawBody, json: null };
224
+ }
225
+
226
+ if (
227
+ injectOpenAIUsage &&
228
+ route.endpoint === "chat-completions" &&
229
+ json?.stream === true
230
+ ) {
231
+ json.stream_options = {
232
+ ...(json.stream_options || {}),
233
+ include_usage: true,
234
+ };
235
+ try {
236
+ return { body: Buffer.from(JSON.stringify(json)), json };
237
+ } catch (error) {
238
+ warn(`[proxy] request usage-option warning: ${error.message}`);
239
+ }
240
+ }
241
+ return { body: rawBody, json };
242
+ }
243
+
244
+ function createUsageWriter(logPath, logger) {
245
+ let queue = Promise.resolve();
246
+ const directory = dirname(logPath);
247
+
248
+ async function ensureDirectory() {
249
+ await mkdir(directory, { recursive: true, mode: 0o700 });
250
+ await chmod(directory, 0o700).catch(() => {});
251
+ }
252
+
253
+ function append(record) {
254
+ queue = queue
255
+ .then(async () => {
256
+ await ensureDirectory();
257
+ await appendFile(logPath, `${JSON.stringify(record)}\n`, {
258
+ encoding: "utf8",
259
+ mode: 0o600,
260
+ });
261
+ await chmod(logPath, 0o600).catch(() => {});
262
+ })
263
+ .catch((error) => {
264
+ logger.warn(`[proxy] usage log warning: ${error.message}`);
265
+ });
266
+ return queue;
267
+ }
268
+
269
+ return { ensureDirectory, append, flush: () => queue };
270
+ }
271
+
272
+ async function readRequestBody(req) {
273
+ const chunks = [];
274
+ for await (const chunk of req) {
275
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
276
+ }
277
+ return Buffer.concat(chunks);
278
+ }
279
+
280
+ function formatUsage(fields) {
281
+ return `input=${fields.input} output=${fields.output} cacheRead=${fields.cacheRead} cacheCreate=${fields.cacheCreate}`;
282
+ }
283
+
284
+ /**
285
+ * Start the opt-in loopback proxy.
286
+ *
287
+ * Test hooks (`upstreams`, `logPath`, `port: 0`, and `logger`) keep tests local;
288
+ * the CLI uses fixed provider origins and port 8787 by default.
289
+ */
290
+ export async function startProxy({
291
+ host = DEFAULT_HOST,
292
+ port = DEFAULT_PORT,
293
+ logPath = defaultProxyLogPath(),
294
+ upstreams = DEFAULT_UPSTREAMS,
295
+ injectOpenAIUsage = true,
296
+ logger = console,
297
+ } = {}) {
298
+ if (host !== "127.0.0.1" && host !== "::1" && host !== "localhost") {
299
+ throw new Error("SigRank proxy must bind to a loopback host");
300
+ }
301
+ port = parseProxyPort(port, { allowZero: true });
302
+ const writer = createUsageWriter(logPath, logger);
303
+ await writer.ensureDirectory();
304
+ let lastCaptureMs = 0;
305
+ const nextCaptureTimestamp = () => {
306
+ // The adapter intentionally deduplicates by timestamp. Keep timestamps
307
+ // monotonic so distinct responses completing within one millisecond cannot
308
+ // collapse into one call.
309
+ lastCaptureMs = Math.max(Date.now(), lastCaptureMs + 1);
310
+ return new Date(lastCaptureMs).toISOString();
311
+ };
312
+
313
+ const server = http.createServer(async (req, res) => {
314
+ let parsedUrl;
315
+ try {
316
+ parsedUrl = new URL(req.url || "/", `http://${req.headers.host || host}`);
317
+ } catch {
318
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
319
+ res.end("Bad request URL\n");
320
+ return;
321
+ }
322
+
323
+ const route = routeFor(parsedUrl.pathname);
324
+ if (!route) {
325
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
326
+ res.end("SigRank proxy supports /v1/messages, /v1/chat/completions, and /v1/responses\n");
327
+ return;
328
+ }
329
+ if (req.method !== "POST") {
330
+ res.writeHead(405, {
331
+ allow: "POST",
332
+ "content-type": "text/plain; charset=utf-8",
333
+ });
334
+ res.end("Method not allowed\n");
335
+ return;
336
+ }
337
+
338
+ let rawBody;
339
+ try {
340
+ rawBody = await readRequestBody(req);
341
+ } catch (error) {
342
+ if (!res.headersSent) {
343
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
344
+ }
345
+ res.end("Could not read request body\n");
346
+ logger.warn(`[proxy] request read warning: ${error.message}`);
347
+ return;
348
+ }
349
+
350
+ const prepared = prepareRequestBody(
351
+ route,
352
+ rawBody,
353
+ injectOpenAIUsage,
354
+ logger.warn.bind(logger),
355
+ );
356
+ const upstreamBase = upstreams[route.backend];
357
+ if (!upstreamBase) {
358
+ res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
359
+ res.end(`No upstream configured for ${route.backend}\n`);
360
+ return;
361
+ }
362
+
363
+ const upstreamUrl = new URL(req.url, upstreamBase);
364
+ const transport = upstreamUrl.protocol === "http:" ? http : https;
365
+ const tracker = createUsageTracker({
366
+ backend: route.backend,
367
+ endpoint: route.endpoint,
368
+ requestModel: prepared.json?.model,
369
+ warn: logger.warn.bind(logger),
370
+ });
371
+
372
+ const upstreamReq = transport.request(
373
+ upstreamUrl,
374
+ {
375
+ method: "POST",
376
+ headers: sanitizeRequestHeaders(req.headers, prepared.body.length),
377
+ },
378
+ (upstreamRes) => {
379
+ const statusCode = upstreamRes.statusCode || 502;
380
+ const headers = sanitizeResponseHeaders(upstreamRes.headers);
381
+ res.writeHead(statusCode, headers);
382
+
383
+ const contentType = String(upstreamRes.headers["content-type"] || "");
384
+ const contentEncoding = String(
385
+ upstreamRes.headers["content-encoding"] || "identity",
386
+ );
387
+ const inspect = statusCode >= 200 && statusCode < 300;
388
+ const isSse = inspect && contentType.includes("text/event-stream");
389
+ const canInspect = contentEncoding === "identity";
390
+ const jsonChunks = [];
391
+ const inspector =
392
+ isSse && canInspect
393
+ ? createSseInspector(tracker.observe, logger.warn.bind(logger))
394
+ : null;
395
+
396
+ upstreamRes.on("data", (chunk) => {
397
+ // Forward first, inspect second: parsing never holds back streamed tokens.
398
+ if (!res.write(chunk)) {
399
+ upstreamRes.pause();
400
+ res.once("drain", () => upstreamRes.resume());
401
+ }
402
+ if (inspector) inspector.push(chunk);
403
+ else if (inspect && canInspect) jsonChunks.push(Buffer.from(chunk));
404
+ });
405
+
406
+ upstreamRes.on("end", () => {
407
+ if (inspector) inspector.end();
408
+ else if (inspect && canInspect && jsonChunks.length) {
409
+ tracker.observeJson(Buffer.concat(jsonChunks).toString("utf8"));
410
+ }
411
+
412
+ const fields = tracker.result();
413
+ if (fields) {
414
+ writer.append({
415
+ ts: nextCaptureTimestamp(),
416
+ backend: route.backend,
417
+ model: fields.model,
418
+ input: fields.input,
419
+ output: fields.output,
420
+ cacheRead: fields.cacheRead,
421
+ cacheCreate: fields.cacheCreate,
422
+ });
423
+ logger.log(
424
+ `[proxy] ${statusCode} POST ${parsedUrl.pathname} → ${route.backend} (${formatUsage(fields)})`,
425
+ );
426
+ } else {
427
+ logger.log(
428
+ `[proxy] ${statusCode} POST ${parsedUrl.pathname} → ${route.backend}`,
429
+ );
430
+ }
431
+ res.end();
432
+ });
433
+
434
+ upstreamRes.on("error", (error) => {
435
+ logger.warn(`[proxy] upstream response warning: ${error.message}`);
436
+ if (!res.writableEnded) res.destroy(error);
437
+ });
438
+ },
439
+ );
440
+
441
+ res.on("close", () => {
442
+ if (!res.writableEnded) upstreamReq.destroy();
443
+ });
444
+ upstreamReq.on("error", (error) => {
445
+ logger.warn(`[proxy] upstream request warning: ${error.message}`);
446
+ if (!res.headersSent) {
447
+ res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
448
+ res.end("Upstream request failed\n");
449
+ } else if (!res.writableEnded) {
450
+ res.destroy(error);
451
+ }
452
+ });
453
+ upstreamReq.end(prepared.body);
454
+ });
455
+
456
+ server.on("clientError", (error, socket) => {
457
+ logger.warn(`[proxy] client warning: ${error.message}`);
458
+ if (socket.writable) socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
459
+ });
460
+
461
+ await new Promise((resolve, reject) => {
462
+ const onError = (error) => {
463
+ server.off("listening", onListening);
464
+ reject(error);
465
+ };
466
+ const onListening = () => {
467
+ server.off("error", onError);
468
+ resolve();
469
+ };
470
+ server.once("error", onError);
471
+ server.once("listening", onListening);
472
+ server.listen(port, host);
473
+ });
474
+
475
+ const address = server.address();
476
+ const listeningPort = typeof address === "object" ? address.port : port;
477
+ return {
478
+ server,
479
+ host,
480
+ port: listeningPort,
481
+ url: `http://${host}:${listeningPort}`,
482
+ logPath,
483
+ async close() {
484
+ if (server.listening) {
485
+ await new Promise((resolve, reject) =>
486
+ server.close((error) => (error ? reject(error) : resolve())),
487
+ );
488
+ }
489
+ await writer.flush();
490
+ },
491
+ };
492
+ }
@@ -0,0 +1,81 @@
1
+ # SigRank Class Tiers
2
+
3
+ Every operator is classified into one of **24 experience stages** — 8 tiers ×
4
+ 3 sub-stages (I/II/III) — based on their **total tokens** (input + output +
5
+ cacheCreate + cacheRead). The ladder mirrors the server's
6
+ `RS05_CLASS_THRESHOLDS` (`lib/analytics/ruleset.ts` in sigrank-app) — the
7
+ client's `classify()` in `analytics/cascade.mjs` is the canonical local source
8
+ of truth. The TUI, CLI, MCP output schemas, and this doc all read from that
9
+ one list.
10
+
11
+ The class is an **experience** axis — it measures how much volume the operator
12
+ has accumulated. It is separate from the **build archetype** (the operator's
13
+ composition shape: CONVERGENT, KINETIC, INPUT-BOUND, ARCHIVIST, AMPLIFIER, etc.)
14
+ and from the **TRANSMITTER peak badge** (a temporary state — see below).
15
+
16
+ A 25th value, **UNCLASSED**, is returned when total tokens are null or
17
+ non-finite (empty / all-zero session). It is distinct from IGNITER III so
18
+ callers can tell "no data" from "bottom tier".
19
+
20
+ ## The 8 base tiers (K.01–K.08)
21
+
22
+ Each tier has 3 sub-stages (I/II/III) split by total-token thresholds. The
23
+ sub-stage is the granular experience level; the base tier is the display
24
+ grouping (color, glyph, meaning).
25
+
26
+ ### ARCH+ (K.01) — total ≥ 7.07T
27
+ - **Sub-stages:** ARCH+ I (≥ 7.07T), ARCH+ II (≥ 7.07T), ARCH+ III (≥ 7.07T)
28
+ - **Profile:** Deepest field experience. Volume that became architecture.
29
+ - **Note:** Aspirational — currently 1 operator in the static board.
30
+
31
+ ### ARCH (K.02) — total ≥ 68.8B
32
+ - **Sub-stages:** ARCH I (≥ 186B), ARCH II (≥ 98.5B), ARCH III (≥ 68.8B)
33
+ - **Profile:** System builders. Sustained volume, coherent output.
34
+
35
+ ### POWER (K.03) — total ≥ 19.1B
36
+ - **Sub-stages:** POWER I (≥ 40.0B), POWER II (≥ 27.0B), POWER III (≥ 19.1B)
37
+ - **Profile:** Above the center. Volume compounding.
38
+
39
+ ### BASE (K.04) — total ≥ 7.7B
40
+ - **Sub-stages:** BASE I (≥ 14.0B), BASE II (≥ 10.2B), BASE III (≥ 7.7B)
41
+ - **Profile:** The center of the field. The average operator's experience.
42
+
43
+ ### SEEKER (K.05) — total ≥ 3.0B
44
+ - **Sub-stages:** SEEKER I (≥ 5.4B), SEEKER II (≥ 4.0B), SEEKER III (≥ 3.0B)
45
+ - **Profile:** Approaching the center. Experience accumulating.
46
+
47
+ ### REFINER (K.06) — total ≥ 1.3B
48
+ - **Sub-stages:** REFINER I (≥ 2.4B), REFINER II (≥ 1.8B), REFINER III (≥ 1.3B)
49
+ - **Profile:** Practicing with purpose. Early sustained volume.
50
+
51
+ ### BEARER (K.07) — total ≥ 432M
52
+ - **Sub-stages:** BEARER I (≥ 984M), BEARER II (≥ 715M), BEARER III (≥ 432M)
53
+ - **Profile:** Quiet accumulation. The first real volume.
54
+
55
+ ### IGNITER (K.08) — total ≥ 0
56
+ - **Sub-stages:** IGNITER I (≥ 216M), IGNITER II (≥ 89.0M), IGNITER III (≥ 0)
57
+ - **Profile:** Dormant potential. The still soul. Waiting.
58
+
59
+ ## UNCLASSED (no data)
60
+ - **Profile:** Empty session — all four pillars are zero.
61
+ - **Fix:** Run some sessions; the stage will resolve once there is data.
62
+
63
+ ## TRANSMITTER (peak badge, not a class)
64
+
65
+ TRANSMITTER is **not** on the experience ladder. It is a temporary peak badge
66
+ (RS.08) that any experience tier can earn during a high-frequency,
67
+ high-resonance window. An operator "transmits" when they hit both:
68
+
69
+ - **High frequency:** token throughput (total tokens in window) ≥ 1B
70
+ - **High resonance:** SIGNA RATE (composite signal quality) ≥ 85
71
+
72
+ The badge is per-window (daily/weekly) and lapses when frequency or resonance
73
+ drops. The server evaluates it via `evaluateTransmitterBadge()` in
74
+ `lib/analytics/transmitter-badge.ts`; the client does not evaluate the badge
75
+ locally (the owner is still calibrating the thresholds). The server is the
76
+ authority.
77
+
78
+ ---
79
+
80
+ Tiers are recalculated on every submission. Your stage can change between
81
+ windows (7d, 30d, 90d, all-time) as total tokens accumulate.
@@ -0,0 +1,31 @@
1
+ # SigRank Data Policy
2
+
3
+ SigRank is a public AI-operator leaderboard. This policy summarizes what data we collect, how we use it, and how you can delete it.
4
+
5
+ ## What we collect
6
+
7
+ - **Token telemetry** from your local agent: token counts, content lengths, model identifiers, and platform/source tags. We use these to compute your score on the leaderboard.
8
+ - **Account identifiers** when you sign in (email from your OAuth provider) so we can link your operator profile to your login.
9
+ - **Optional profile fields** you choose to add: display name, handle, bio, location, links, avatar.
10
+
11
+ ## What we never collect
12
+
13
+ We do **not** collect or receive the content of your prompts or AI conversations
14
+ at the SigRank service. Local-log adapters extract only numeric usage metadata.
15
+ If you explicitly run the optional local API proxy, it forwards provider-bound
16
+ requests and responses in memory but does not persist their content; only usage
17
+ metadata is written locally or submitted to SigRank.
18
+
19
+ ## Consent
20
+
21
+ Before you connect a device, you must agree to the current Terms of Service and Privacy Policy. We record the timestamp and version of your consent.
22
+
23
+ ## Your choices
24
+
25
+ - **Pause data collection** at any time from [signalaf.com/settings](https://signalaf.com/settings). While paused, your agent submissions are rejected and no new telemetry is stored.
26
+ - **Delete your data** from Settings to erase all snapshots, scored metrics, ranks, devices, and enrollment codes while keeping your account and profile.
27
+ - **Delete your account** from Settings to fully sever your identity. This runs Delete your data first, then anonymizes your board row (kept as aggregate history), removes your login, and cancels billing.
28
+
29
+ ## Contact
30
+
31
+ Questions: hello@signalaf.com
@@ -0,0 +1,59 @@
1
+ # SigRank Quick Start
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npx sigrank
7
+ ```
8
+
9
+ No global install needed. The MCP server starts automatically when an AI client (Claude, Cursor, Cline) connects.
10
+
11
+ ## First Submission
12
+
13
+ 1. **Enroll** — register your operator codename:
14
+ ```bash
15
+ npx sigrank enroll
16
+ ```
17
+
18
+ 2. **Pull + Submit** — scan your local AI session logs and publish:
19
+ ```bash
20
+ npx sigrank submit
21
+ ```
22
+
23
+ 3. **Dry Run** — see exactly what would be sent before publishing:
24
+ ```bash
25
+ npx sigrank submit --dry-run
26
+ ```
27
+
28
+ ## Token Pull Sources
29
+
30
+ SigRank bundles two token readers as npm dependencies:
31
+ - **ccusage** — Claude Code session logs
32
+ - **tokscale** — multi-platform token telemetry
33
+
34
+ A third source, **token-dashboard**, is read from `~/.claude/token-dashboard.db` (SQLite).
35
+ That DB is created by [Nate's token-dashboard](https://github.com/nateherkai/token-dashboard) (Python):
36
+ ```bash
37
+ git clone https://github.com/nateherkai/token-dashboard.git
38
+ cd token-dashboard
39
+ python3 cli.py dashboard
40
+ ```
41
+ Run the scan once to create the DB; SigRank reads it directly via sqlite3 after that.
42
+
43
+ ## MCP Client Setup
44
+
45
+ Add to your MCP client config:
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "sigrank": {
50
+ "command": "npx",
51
+ "args": ["sigrank"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Live Board
58
+
59
+ Visit https://signalaf.com to see the global leaderboard.