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
|
@@ -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.
|