Sign In

sigrank

Package Overview
Dependencies
Maintainers
1
Versions
76
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

sigrank - npm Package Compare versions

Comparing version
0.0.196
to
0.0.197
+492
proxy.mjs
/**
* Local, opt-in API proxy for provider-reported token usage.
*
* The proxy is inert until `sigrank proxy` starts it. It binds to loopback,
* forwards request/response bytes, and persists usage metadata only — never
* prompts, API keys, response text, or tool calls.
*/
import http from "node:http";
import https from "node:https";
import { appendFile, chmod, mkdir } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
const DEFAULT_HOST = "127.0.0.1";
const DEFAULT_PORT = 8787;
const DEFAULT_UPSTREAMS = Object.freeze({
anthropic: "https://api.anthropic.com",
openai: "https://api.openai.com",
});
const HOP_BY_HOP_HEADERS = new Set([
"connection",
"keep-alive",
"proxy-authenticate",
"proxy-authorization",
"te",
"trailer",
"transfer-encoding",
"upgrade",
]);
export function defaultProxyLogPath() {
return join(homedir(), ".sigrank-mcp", "proxy-sessions.jsonl");
}
export function parseProxyPort(value, { allowZero = false } = {}) {
const port = Number(value);
const min = allowZero ? 0 : 1;
if (!Number.isInteger(port) || port < min || port > 65_535) {
throw new Error(`Invalid proxy port "${value}" (expected ${min}-${65_535})`);
}
return port;
}
function routeFor(pathname) {
if (pathname === "/v1/messages") {
return { backend: "anthropic", endpoint: "messages" };
}
if (pathname === "/v1/chat/completions") {
return { backend: "openai", endpoint: "chat-completions" };
}
if (pathname === "/v1/responses") {
return { backend: "openai", endpoint: "responses" };
}
return null;
}
function finiteTokenCount(value) {
const n = Number(value);
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
}
function usageFields(backend, usage) {
if (!usage || typeof usage !== "object") return null;
if (backend === "anthropic") {
return {
input: finiteTokenCount(usage.input_tokens),
output: finiteTokenCount(usage.output_tokens),
cacheRead: finiteTokenCount(usage.cache_read_input_tokens),
cacheCreate: finiteTokenCount(usage.cache_creation_input_tokens),
};
}
const totalInput = finiteTokenCount(
usage.prompt_tokens ?? usage.input_tokens,
);
const cacheRead = finiteTokenCount(
usage.prompt_tokens_details?.cached_tokens ??
usage.input_tokens_details?.cached_tokens,
);
return {
// OpenAI's prompt/input total includes cached tokens. SigRank's input pillar
// is fresh input, so subtract the separately reported cache read amount.
input: Math.max(0, totalInput - cacheRead),
output: finiteTokenCount(
usage.completion_tokens ?? usage.output_tokens,
),
cacheRead,
cacheCreate: 0,
};
}
function mergeDefinedUsage(target, source) {
if (!source || typeof source !== "object") return false;
let found = false;
for (const key of [
"input_tokens",
"output_tokens",
"cache_read_input_tokens",
"cache_creation_input_tokens",
]) {
if (source[key] != null) {
target[key] = source[key];
found = true;
}
}
return found;
}
function createUsageTracker({ backend, endpoint, requestModel, warn }) {
let model = requestModel || null;
let usage = null;
const anthropicUsage = {};
function observe(payload) {
if (!payload || typeof payload !== "object") return;
if (backend === "anthropic") {
model = payload.message?.model || payload.model || model;
const next = payload.message?.usage || payload.usage;
if (mergeDefinedUsage(anthropicUsage, next)) usage = anthropicUsage;
return;
}
if (endpoint === "responses") {
const response = payload.response || payload;
model = response.model || model;
if (response.usage && typeof response.usage === "object") {
usage = response.usage;
}
return;
}
model = payload.model || model;
if (payload.usage && typeof payload.usage === "object") {
usage = payload.usage;
}
}
function observeJson(text) {
try {
observe(JSON.parse(text));
} catch (error) {
warn(`[proxy] usage parse warning: ${error.message}`);
}
}
function result() {
const fields = usageFields(backend, usage);
return fields ? { model: model || "unknown", ...fields } : null;
}
return { observe, observeJson, result };
}
/** Incremental SSE parser. Network chunks are not assumed to align with events. */
function createSseInspector(onData, warn) {
const decoder = new TextDecoder();
let buffer = "";
function parseFrame(frame) {
const data = frame
.split(/\r?\n/)
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).replace(/^ /, ""))
.join("\n");
if (!data || data === "[DONE]") return;
try {
onData(JSON.parse(data));
} catch (error) {
warn(`[proxy] SSE usage parse warning: ${error.message}`);
}
}
function drain(final = false) {
let match;
while ((match = /\r?\n\r?\n/.exec(buffer))) {
const frame = buffer.slice(0, match.index);
buffer = buffer.slice(match.index + match[0].length);
parseFrame(frame);
}
if (final && buffer.trim()) {
parseFrame(buffer);
buffer = "";
}
}
return {
push(chunk) {
buffer += decoder.decode(chunk, { stream: true });
drain(false);
},
end() {
buffer += decoder.decode();
drain(true);
},
};
}
function sanitizeRequestHeaders(headers, bodyLength) {
const out = { ...headers };
for (const name of HOP_BY_HOP_HEADERS) delete out[name];
delete out.host;
// Usage inspection operates on the raw response. Asking upstream for identity
// encoding avoids buffering/decompressing streamed provider responses.
out["accept-encoding"] = "identity";
out["content-length"] = String(bodyLength);
return out;
}
function sanitizeResponseHeaders(headers) {
const out = { ...headers };
for (const name of HOP_BY_HOP_HEADERS) delete out[name];
return out;
}
function prepareRequestBody(route, rawBody, injectOpenAIUsage, warn) {
let json = null;
try {
json = JSON.parse(rawBody.toString("utf8"));
} catch {
return { body: rawBody, json: null };
}
if (
injectOpenAIUsage &&
route.endpoint === "chat-completions" &&
json?.stream === true
) {
json.stream_options = {
...(json.stream_options || {}),
include_usage: true,
};
try {
return { body: Buffer.from(JSON.stringify(json)), json };
} catch (error) {
warn(`[proxy] request usage-option warning: ${error.message}`);
}
}
return { body: rawBody, json };
}
function createUsageWriter(logPath, logger) {
let queue = Promise.resolve();
const directory = dirname(logPath);
async function ensureDirectory() {
await mkdir(directory, { recursive: true, mode: 0o700 });
await chmod(directory, 0o700).catch(() => {});
}
function append(record) {
queue = queue
.then(async () => {
await ensureDirectory();
await appendFile(logPath, `${JSON.stringify(record)}\n`, {
encoding: "utf8",
mode: 0o600,
});
await chmod(logPath, 0o600).catch(() => {});
})
.catch((error) => {
logger.warn(`[proxy] usage log warning: ${error.message}`);
});
return queue;
}
return { ensureDirectory, append, flush: () => queue };
}
async function readRequestBody(req) {
const chunks = [];
for await (const chunk of req) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
return Buffer.concat(chunks);
}
function formatUsage(fields) {
return `input=${fields.input} output=${fields.output} cacheRead=${fields.cacheRead} cacheCreate=${fields.cacheCreate}`;
}
/**
* Start the opt-in loopback proxy.
*
* Test hooks (`upstreams`, `logPath`, `port: 0`, and `logger`) keep tests local;
* the CLI uses fixed provider origins and port 8787 by default.
*/
export async function startProxy({
host = DEFAULT_HOST,
port = DEFAULT_PORT,
logPath = defaultProxyLogPath(),
upstreams = DEFAULT_UPSTREAMS,
injectOpenAIUsage = true,
logger = console,
} = {}) {
if (host !== "127.0.0.1" && host !== "::1" && host !== "localhost") {
throw new Error("SigRank proxy must bind to a loopback host");
}
port = parseProxyPort(port, { allowZero: true });
const writer = createUsageWriter(logPath, logger);
await writer.ensureDirectory();
let lastCaptureMs = 0;
const nextCaptureTimestamp = () => {
// The adapter intentionally deduplicates by timestamp. Keep timestamps
// monotonic so distinct responses completing within one millisecond cannot
// collapse into one call.
lastCaptureMs = Math.max(Date.now(), lastCaptureMs + 1);
return new Date(lastCaptureMs).toISOString();
};
const server = http.createServer(async (req, res) => {
let parsedUrl;
try {
parsedUrl = new URL(req.url || "/", `http://${req.headers.host || host}`);
} catch {
res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
res.end("Bad request URL\n");
return;
}
const route = routeFor(parsedUrl.pathname);
if (!route) {
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
res.end("SigRank proxy supports /v1/messages, /v1/chat/completions, and /v1/responses\n");
return;
}
if (req.method !== "POST") {
res.writeHead(405, {
allow: "POST",
"content-type": "text/plain; charset=utf-8",
});
res.end("Method not allowed\n");
return;
}
let rawBody;
try {
rawBody = await readRequestBody(req);
} catch (error) {
if (!res.headersSent) {
res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
}
res.end("Could not read request body\n");
logger.warn(`[proxy] request read warning: ${error.message}`);
return;
}
const prepared = prepareRequestBody(
route,
rawBody,
injectOpenAIUsage,
logger.warn.bind(logger),
);
const upstreamBase = upstreams[route.backend];
if (!upstreamBase) {
res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
res.end(`No upstream configured for ${route.backend}\n`);
return;
}
const upstreamUrl = new URL(req.url, upstreamBase);
const transport = upstreamUrl.protocol === "http:" ? http : https;
const tracker = createUsageTracker({
backend: route.backend,
endpoint: route.endpoint,
requestModel: prepared.json?.model,
warn: logger.warn.bind(logger),
});
const upstreamReq = transport.request(
upstreamUrl,
{
method: "POST",
headers: sanitizeRequestHeaders(req.headers, prepared.body.length),
},
(upstreamRes) => {
const statusCode = upstreamRes.statusCode || 502;
const headers = sanitizeResponseHeaders(upstreamRes.headers);
res.writeHead(statusCode, headers);
const contentType = String(upstreamRes.headers["content-type"] || "");
const contentEncoding = String(
upstreamRes.headers["content-encoding"] || "identity",
);
const inspect = statusCode >= 200 && statusCode < 300;
const isSse = inspect && contentType.includes("text/event-stream");
const canInspect = contentEncoding === "identity";
const jsonChunks = [];
const inspector =
isSse && canInspect
? createSseInspector(tracker.observe, logger.warn.bind(logger))
: null;
upstreamRes.on("data", (chunk) => {
// Forward first, inspect second: parsing never holds back streamed tokens.
if (!res.write(chunk)) {
upstreamRes.pause();
res.once("drain", () => upstreamRes.resume());
}
if (inspector) inspector.push(chunk);
else if (inspect && canInspect) jsonChunks.push(Buffer.from(chunk));
});
upstreamRes.on("end", () => {
if (inspector) inspector.end();
else if (inspect && canInspect && jsonChunks.length) {
tracker.observeJson(Buffer.concat(jsonChunks).toString("utf8"));
}
const fields = tracker.result();
if (fields) {
writer.append({
ts: nextCaptureTimestamp(),
backend: route.backend,
model: fields.model,
input: fields.input,
output: fields.output,
cacheRead: fields.cacheRead,
cacheCreate: fields.cacheCreate,
});
logger.log(
`[proxy] ${statusCode} POST ${parsedUrl.pathname} → ${route.backend} (${formatUsage(fields)})`,
);
} else {
logger.log(
`[proxy] ${statusCode} POST ${parsedUrl.pathname} → ${route.backend}`,
);
}
res.end();
});
upstreamRes.on("error", (error) => {
logger.warn(`[proxy] upstream response warning: ${error.message}`);
if (!res.writableEnded) res.destroy(error);
});
},
);
res.on("close", () => {
if (!res.writableEnded) upstreamReq.destroy();
});
upstreamReq.on("error", (error) => {
logger.warn(`[proxy] upstream request warning: ${error.message}`);
if (!res.headersSent) {
res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
res.end("Upstream request failed\n");
} else if (!res.writableEnded) {
res.destroy(error);
}
});
upstreamReq.end(prepared.body);
});
server.on("clientError", (error, socket) => {
logger.warn(`[proxy] client warning: ${error.message}`);
if (socket.writable) socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
});
await new Promise((resolve, reject) => {
const onError = (error) => {
server.off("listening", onListening);
reject(error);
};
const onListening = () => {
server.off("error", onError);
resolve();
};
server.once("error", onError);
server.once("listening", onListening);
server.listen(port, host);
});
const address = server.address();
const listeningPort = typeof address === "object" ? address.port : port;
return {
server,
host,
port: listeningPort,
url: `http://${host}:${listeningPort}`,
logPath,
async close() {
if (server.listening) {
await new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
}
await writer.flush();
},
};
}
+112
-57

@@ -49,2 +49,3 @@ /**

import { promisify } from "node:util";
import { cachedOmpScan, ompCacheEnabled } from "../omp-cache.mjs";

@@ -81,3 +82,3 @@ const execFileP = promisify(execFileCb);

const isJsonl = (n) =>
export const isJsonl = (n) =>
n.endsWith(".jsonl") ||

@@ -749,19 +750,12 @@ n.endsWith(".jsonl.deleted") ||

// SQLite: ~/.local/share/devin/cli/sessions.db
// Native 4-pillar: metrics.input_tokens is FRESH input (does NOT include cache
// read or cache creation — verified against live data where input=3 alongside
// cache_creation_tokens=23500). cache_read_tokens and cache_creation_tokens are
// separate fields. output_tokens includes any reasoning tokens.
//
// CACHE_CREATION FALLBACK: the Devin CLI records cache_creation_tokens natively
// for Claude models (claude-sonnet-4-6, claude-opus-4-6) but writes NULL for
// GLM-5-2 and other non-Claude models. When the field is NULL, estimate
// cacheCreate from cache_read using the construction ratio (cw/cr = 0.0635)
// calibrated from the 8K Claude-era rows (50.3M cw / 792M cr). This preserves
// the compounding signal — without it, cacheCreate=0 makes every window look
// like a "stateless pipe" (no cache commits), which is a data gap, not reality.
const DEVIN_CONSTRUCTION_RATIO = 0.0635;
// Same combined-input problem as Codex: input_tokens INCLUDES cache write, so we
// yield { ts, output, cacheRead, uncached } and let tokenpullCodex() do the
// ioRatio split (input = output × ioRatio, cacheCreate = uncached − input).
// ioRatio comes from Claude (Beta = operator's Claude input/output ratio) or
// the Alpha 2.0 default (matches Codex; the "7:1:2 average → 0.5" note in a
// prior revision was wrong — tokenpullAny defaults ioRatio to 2.0 for both).
export const devinAdapter = {
platform: "devin",
defaultRoot: () => join(homedir(), ".local", "share", "devin", "cli"),
async *messages(root) {
async *records(root) {
for (const r of roots("DEVIN_HOME", root)) {

@@ -775,3 +769,2 @@ const dbPath = join(r, "sessions.db");

json_extract(chat_message, '$.metadata.metrics.cache_read_tokens') as cache_read_tokens,
json_extract(chat_message, '$.metadata.metrics.cache_creation_tokens') as cache_creation_tokens,
json_extract(chat_message, '$.metadata.created_at') as created_at

@@ -785,20 +778,11 @@ FROM message_nodes

for (const row of rows) {
const input = Number(row.input_tokens || 0);
const inputIncl = Number(row.input_tokens || 0);
const cached = Number(row.cache_read_tokens || 0);
const output = Number(row.output_tokens || 0);
const cacheRead = Number(row.cache_read_tokens || 0);
// Use native cache_creation_tokens when present; estimate from cache_read
// when NULL (GLM-5-2 era doesn't record the field).
const cacheCreate =
row.cache_creation_tokens != null
? Number(row.cache_creation_tokens || 0)
: Math.round(cacheRead * DEVIN_CONSTRUCTION_RATIO);
if (input + output + cacheRead + cacheCreate === 0) continue;
if (inputIncl + output + cached === 0) continue;
yield {
id: `devin:${row.row_id}`,
sid: row.session_id || null,
ts: row.created_at || null,
input,
output,
cacheCreate,
cacheRead,
cacheRead: cached,
uncached: Math.max(0, inputIncl - cached),
file: dbPath,

@@ -949,4 +933,5 @@ };

/** Parse a single omp transcript file and yield usage records.
* Isolated so it can be called concurrently from the bounded pool. */
function* parseOmpFile(text, path) {
* Isolated so it can be called concurrently from the bounded pool.
* Exported for omp-cache.mjs and tests. */
export function* parseOmpFile(text, path) {
if (!text) return;

@@ -997,31 +982,100 @@ let sid = null;

for (const r of roots("OMP_DATA_DIR", root)) {
// Collect all file paths first (readdir is 0.8% of scan time — negligible)
const paths = [];
for await (const path of walkFiles(r, isJsonl, { n: 0 }, OMP_MAX_FILES)) {
paths.push(path);
// Stage 2: if SIGRANK_OMP_CACHE is set, use the incremental SQLite cache.
// The cache wraps the Stage 1 scanner — on a warm run with no changed
// files, it serves records from SQLite without reading transcripts.
// Falls back to uncached on any error.
if (ompCacheEnabled()) {
yield* cachedOmpScan({
rootDir: r,
uncachedScan: async function* () {
yield* ompUncachedScan(r);
},
parseOmpFile,
readUtf8,
walkFiles,
isJsonl,
maxFiles: OMP_MAX_FILES,
});
continue;
}
// Bounded-concurrency read+parse: George-RD measured c=8 as optimal
// (1.29x speedup, 45s → 35s). Order-safe: all 504 duplicate keys carry
// byte-identical pillar tuples, so completion order can't change totals.
const concurrency = OMP_CONCURRENCY;
const inflight = new Set();
const queue = [...paths];
// Default: uncached Stage 1 scan
yield* ompUncachedScan(r);
}
},
};
while (queue.length > 0 || inflight.size > 0) {
while (inflight.size < concurrency && queue.length > 0) {
const path = queue.shift();
const p = (async () => {
const text = await readUtf8(path);
return { path, records: [...parseOmpFile(text, path)] };
})();
inflight.add(p);
p.finally(() => inflight.delete(p));
}
const done = await Promise.race(inflight);
inflight.delete(done);
for (const record of done.records) {
yield record;
}
/** Stage 1 uncached scan — bounded concurrency + line guard.
* Isolated so the cache wrapper can call it as the fallback/parity reference. */
async function* ompUncachedScan(rootDir) {
// Collect all file paths first (readdir is 0.8% of scan time — negligible)
const paths = [];
for await (const path of walkFiles(rootDir, isJsonl, { n: 0 }, OMP_MAX_FILES)) {
paths.push(path);
}
// Bounded-concurrency read+parse: George-RD measured c=8 as optimal
// (1.29x speedup, 45s → 35s). Order-safe: all 504 duplicate keys carry
// byte-identical pillar tuples, so completion order can't change totals.
const concurrency = OMP_CONCURRENCY;
const inflight = new Set();
const queue = [...paths];
while (queue.length > 0 || inflight.size > 0) {
while (inflight.size < concurrency && queue.length > 0) {
const path = queue.shift();
const p = (async () => {
const text = await readUtf8(path);
return { path, records: [...parseOmpFile(text, path)] };
})();
inflight.add(p);
p.finally(() => inflight.delete(p));
}
const done = await Promise.race(inflight);
inflight.delete(done);
for (const record of done.records) {
yield record;
}
}
}
// ── SigRank local API proxy ──────────────────────────────────────────────────
// ~/.sigrank-mcp/proxy-sessions.jsonl — one provider-reported usage record per
// API call. Native 4-pillar data; OpenAI's inclusive input count is normalized
// by proxy.mjs before it reaches this adapter.
export const proxyAdapter = {
platform: "proxy",
defaultRoot: () =>
join(homedir(), ".sigrank-mcp", "proxy-sessions.jsonl"),
async *messages(root) {
const path = root || this.defaultRoot();
const text = await readUtf8(path);
for (const [ev] of parseJsonl(text, path)) {
if (!ev || typeof ev !== "object") continue;
const input = Number(ev.input);
const output = Number(ev.output);
const cacheCreate = Number(ev.cacheCreate);
const cacheRead = Number(ev.cacheRead);
if (
![input, output, cacheCreate, cacheRead].every(
(n) => Number.isFinite(n) && n >= 0,
)
) {
continue;
}
if (input + output + cacheCreate + cacheRead === 0) continue;
yield {
// tokenpull() keeps the final record for a duplicate id, which gives
// this adapter the requested same-timestamp keep-last behavior.
id: typeof ev.ts === "string" && ev.ts ? ev.ts : null,
sid: null,
ts: typeof ev.ts === "string" ? ev.ts : null,
input,
output,
cacheCreate,
cacheRead,
model: typeof ev.model === "string" ? ev.model : null,
backend: typeof ev.backend === "string" ? ev.backend : null,
file: path,
};
}

@@ -1050,4 +1104,5 @@ },

omp: ompAdapter,
proxy: proxyAdapter,
};
export const ALL_PLATFORMS = Object.keys(ADAPTERS).concat(["claude", "codex"]);

@@ -362,8 +362,5 @@ /**

return tokenpull({ adapter: claudeAdapter, ...opts });
// Codex: input_tokens includes cache write, so it goes through tokenpullCodex()
// which splits via ioRatio (Beta from Claude, Alpha 2.0). Devin was previously
// routed here too, but its metrics object carries native cache_creation_tokens
// and cache_read_tokens as separate fields with input_tokens as fresh input —
// no estimation needed. Devin now goes through the standard tokenpull() path.
if (platform === "codex") {
// Codex + Devin: both combine input + cache write in input_tokens, so both go
// through tokenpullCodex() which splits via ioRatio (Beta from Claude, Alpha 2.0).
if (platform === "codex" || platform === "devin") {
let ioRatio = opts.ioRatio || 2.0;

@@ -370,0 +367,0 @@ if (!opts.ioRatio) {

@@ -14,2 +14,3 @@ #!/usr/bin/env node

* npx sigrank board | compare | watch read / publish helpers
* npx sigrank proxy opt-in local Anthropic/OpenAI usage proxy
* npx sigrank --help full reference

@@ -16,0 +17,0 @@ *

{
"name": "sigrank",
"version": "0.0.196",
"version": "0.0.197",
"author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",

@@ -35,2 +35,3 @@ "description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",

"index.mjs",
"proxy.mjs",
"lib/",

@@ -37,0 +38,0 @@ "tools/",

/**
* narrate.mjs — deterministic prose "card" for a cascade result.
*
* Port of _template() from ~/Desktop/moses-sigrank/narrate.py. The model path
* (MiniCPM4-0.5B) is intentionally SKIPPED: the template is the trustworthy,
* instant, auditable fallback — same numbers in → same card out, and it can never
* emit a metric the cascade didn't produce. A model hook can layer behind this same
* narrate() interface later without a rewrite.
* Uses the 10 build archetype composition classifier, synced with the app's
* lib/analytics/build-archetypes.ts. Every operator lands in exactly one
* archetype that describes their operating shape — not their rank.
*
* Classification precedence (first match wins):
* 1. CONVERGENT — P80+ on all 3 axes (leverage + velocity + construction)
* 2. KINETIC — velocity >= 0.80
* 3. Construction — construction >= 0.02 (BUILDER / RECURSIVE / AMPLIFIER)
* 4. Reuse depth — else (INPUT-BOUND / PRIMING / CONTEXTUAL / DEEP READER / ARCHIVIST)
*
* The three derived dimensions:
* leverage = cache_read / input
* velocity = output / input
* construction = cache_write / cache_read
*
* Token-only. No network, no randomness.

@@ -29,5 +38,142 @@ */

// P80 thresholds calibrated from HCM cut (1,586 operators).
const CONVERGENT_T = { levP80: 74.6, velP80: 0.34, constrP80: 0.0431 };
const VEL_KINETIC = 0.8;
const LEV_INPUT_BOUND = 5;
const LEV_PRIMING = 10;
const LEV_CONTEXTUAL = 15;
const LEV_DEEP_READER = 23;
const CONSTR_ACTIVE = 0.02;
const LEV_BUILDER = 30;
const LEV_RECURSIVE = 50;
/** Classify an operator's cascade into one of 10 build archetypes.
* Returns { key, name, family, familyLabel, blurb }. */
export function classifyArchetype(cascade) {
const v = safeNum(cascade.velocity);
const l = safeNum(cascade.leverage);
const cw = cascade.pillars ? Number(cascade.pillars.cacheCreate) : NaN;
const cr = cascade.pillars ? Number(cascade.pillars.cacheRead) : NaN;
const input = cascade.pillars ? Number(cascade.pillars.input) : NaN;
// Derive construction = cache_write / cache_read
const constr = Number.isFinite(cr) && cr > 0 && Number.isFinite(cw) ? cw / cr : 0;
const lev = l ?? 0;
const vel = v ?? 0;
// 1. CONVERGENT — all three axes elevated (P80+)
if (
lev > CONVERGENT_T.levP80 &&
vel > CONVERGENT_T.velP80 &&
constr > CONVERGENT_T.constrP80
) {
return {
key: "convergent",
name: "CONVERGENT",
family: "convergence",
familyLabel: "Convergence",
blurb:
"Deep reuse, active construction, and high generation rise together. A rare composition where all three operating axes are elevated without the usual tradeoffs.",
};
}
// 2. KINETIC — generation breakout
if (vel >= VEL_KINETIC) {
return {
key: "kinetic",
name: "KINETIC",
family: "generation",
familyLabel: "Generation",
blurb:
"Generation has broken out. Output approaches or exceeds fresh input, making transmission the defining feature of the composition.",
};
}
// 3. Construction branch — active context construction
if (constr >= CONSTR_ACTIVE) {
if (lev >= LEV_RECURSIVE) {
return {
key: "amplifier",
name: "AMPLIFIER",
family: "construction",
familyLabel: "Active Construction",
blurb:
"Deep reuse and active construction are operating together at scale. Existing context produces new work that expands the context available for future cycles.",
};
}
if (lev >= LEV_BUILDER) {
return {
key: "recursive",
name: "RECURSIVE",
family: "construction",
familyLabel: "Active Construction",
blurb:
"New context is being built on top of an already substantial reusable base. Construction and reuse are now feeding the same operating loop.",
};
}
return {
key: "builder",
name: "BUILDER",
family: "construction",
familyLabel: "Active Construction",
blurb:
"Active context construction has begun. The system is creating material for future reuse while leverage is still developing.",
};
}
// 4. Reuse depth branch — passive (construction < 0.02)
if (lev >= LEV_DEEP_READER) {
return {
key: "archivist",
name: "ARCHIVIST",
family: "reuse",
familyLabel: "Reuse Depth",
blurb:
"Extreme reuse of accumulated context. A deep context library carries the system while new construction remains limited.",
};
}
if (lev >= LEV_CONTEXTUAL) {
return {
key: "deep-reader",
name: "DEEP READER",
family: "reuse",
familyLabel: "Reuse Depth",
blurb:
"Strong accumulated context is carrying the workflow. The operator draws deeply from retained context while creating relatively little new context.",
};
}
if (lev >= LEV_PRIMING) {
return {
key: "contextual",
name: "CONTEXTUAL",
family: "reuse",
familyLabel: "Reuse Depth",
blurb:
"Retained context is now materially supporting the workflow. Reuse is established, while active construction remains limited.",
};
}
if (lev >= LEV_INPUT_BOUND) {
return {
key: "priming",
name: "PRIMING",
family: "reuse",
familyLabel: "Reuse Depth",
blurb:
"Reuse is beginning to form. Prior context is returning, but the system has not yet developed deep leverage.",
};
}
return {
key: "input-bound",
name: "INPUT-BOUND",
family: "reuse",
familyLabel: "Reuse Depth",
blurb:
"Fresh input still carries most of the workload. Little prior context is returning, so each cycle depends heavily on new input.",
};
}
/**
* Given a cascade result ({ velocity, leverage, dev10x, pillars, class }) and an
* optional subject name, return "**CLASS.** <one or two sentences>". Deterministic.
* optional subject name, return "**CLASS.** <archetype blurb with context>".
* Deterministic — same numbers in → same card out.
*/

@@ -38,12 +184,9 @@ export function narrate(cascade, name = "This operator") {

const l = safeNum(cascade.leverage);
const cw = cascade.pillars ? Number(cascade.pillars.cacheCreate) : NaN;
// "non-compounding" = a stateless pipe: no cache commits, so the cascade can't
// form. cascade.mjs leaves dev10x null when cacheCreate is 0 (the cw/o term
// collapses), which is exactly metrics.py's non_compounding flag.
// Also catches zero-input sessions where velocity/leverage are null.
const cw = cascade.pillars ? Number(cascade.pillars.cacheCreate) : NaN;
// Handle non-compounding (stateless pipe) as a special case before the
// archetype classifier — cascade.mjs leaves dev10x null when cacheCreate is 0.
const nonCompounding =
cascade.dev10x == null || !(cw > 0) || v === null || l === null;
let body;
if (nonCompounding) {

@@ -58,23 +201,22 @@ const leverageStr =

: "";
body =
const body =
`${name} runs a stateless pipe — no cache commits, so the cascade can't form. ` +
`High read volume, but nothing is being built forward. ${leverageStr}${dev10xNote}`;
} else if (v >= 1 && l >= 100) {
body =
`${name} holds both axes at once: ${plain(v, 1)}x generation AND ${comma(l, 0)}x memory leverage. ` +
`A closed kinetic loop — the rare operator the leverage/generation tradeoff says shouldn't exist.`;
} else if (l >= 10 && v < 1) {
body =
`${name} is an archival sponge — ${comma(l, 0)}x reuse but only ${plain(v, 2)}x generation. ` +
`Holds context beautifully, executes little with it. The reuse number is inflated by a weak commitment stage.`;
} else if (v >= 0.8 && l < 2) {
body =
`${name} is a volatile ingestor — ${plain(v, 2)}x generation but ${plain(l, 1)}x leverage. ` +
`Fast on single shots, resets between turns. Memory doesn't persist into a compounding loop.`;
} else {
body =
`${name} sits low on both axes: ${plain(v, 2)}x generation, ${plain(l, 1)}x leverage. ` +
`A transient profile — neither building state nor converting input to output efficiently.`;
return `**${klass}.** ${body}`;
}
// Classify into one of 10 build archetypes
const arch = classifyArchetype(cascade);
// Build the card with archetype name, family, and context-specific numbers
const levStr = l !== null ? `${comma(l, 0)}x leverage` : "undefined leverage";
const velStr = v !== null ? `${plain(v, 2)}x generation` : "undefined generation";
const cr = cascade.pillars ? Number(cascade.pillars.cacheRead) : NaN;
const constrStr = `construction ${plain(Number.isFinite(cr) && cr > 0 ? cw / cr : 0, 4)}`;
const body =
`${name} is ${arch.name} — ${arch.blurb} ` +
`${levStr}, ${velStr}, ${constrStr}.`;
return `**${klass}.** ${body}`;
}

@@ -176,2 +176,4 @@ # SigRank MCP

watch --window 7d watch only one window (optional filter)
proxy opt-in local Anthropic/OpenAI usage proxy
proxy --port 9000 run the proxy on a custom loopback port

@@ -183,2 +185,3 @@ Options

--once print once and exit (board only)
--port proxy port (default: 8787)

@@ -199,2 +202,30 @@ For AI clients (not typeable)

### Optional API usage proxy
Some desktop coding agents receive provider usage in API responses but do not
persist it in their local session files. SigRank can capture those
provider-reported counts through a manually started loopback proxy:
```bash
sigrank proxy # http://localhost:8787
sigrank proxy --port 9000 # custom port
```
Then point a compatible tool's API base URL at the displayed local URL. The
first release supports Anthropic Messages (`/v1/messages`), OpenAI Chat
Completions (`/v1/chat/completions`), and OpenAI Responses (`/v1/responses`).
The tool must support a custom API base URL; this is not guaranteed for every
desktop client.
The proxy is **off by default**: it opens no port and observes no traffic unless
you explicitly run `sigrank proxy`. It binds only to loopback and stops when the
command exits. Request and response content, API keys, and tool calls are
forwarded transiently but never written to disk. Only usage metadata is appended
to `~/.sigrank-mcp/proxy-sessions.jsonl` (directory `0700`, file `0600`).
Anthropic and OpenAI calls are currently grouped under one `proxy` platform row.
For streamed Chat Completions, SigRank sets OpenAI's
`stream_options.include_usage=true` so the provider includes the final usage
chunk; response chunks are still forwarded immediately.
### The TUI is the whole app

@@ -360,2 +391,3 @@

| OpenCode | ⚠️ `~/.local/share/opencode` | Data gap — logs store `cost:0` and derive tokens via LiteLLM at runtime; raw token counts not persisted. No pillars readable with current format |
| SigRank proxy | ✅ `~/.sigrank-mcp/proxy-sessions.jsonl` | Opt-in native 4-pillar usage reported by Anthropic/OpenAI; same-timestamp records keep the last call; OpenAI cached input is separated from fresh input |
| Other (user JSON) | ✅ `$SIGRANK_OTHER_PATH` | User-supplied JSON `{ "windows": { "all": {input,output,cacheCreate,cacheRead} } }`; all-time only (no timestamps) |

@@ -371,3 +403,3 @@ | Cursor | 🔜 | Chat log path TBD |

- **Token-only, always.** No message content is ever read, logged, or transmitted — only token counts (`input`, `output`, `cache_creation`, `cache_read`), message IDs, and timestamps.
- **Token-only persistence and submission.** Local-log adapters read usage metadata only. The optional proxy necessarily handles provider-bound request and response bytes in memory, but never persists their content; it writes only token counts, model/backend metadata, and timestamps. Only token telemetry is submitted to SigRank.
- **Local by default.** `tokenpull` reads only `~/.claude/projects` (Claude) or `~/.codex` (Codex) on your device. Numbers stay on your machine unless you explicitly submit with a codename.

@@ -392,3 +424,3 @@ - **Background tooling excluded.** Memory plugins, observers, summarizers (e.g. `claude-mem`, `mem0`, `observer-sessions`) are filtered from both Claude and Codex reads. `subagents/` are kept — they represent real operator work.

```bash
node test.mjs # 14 test groups, 313 assertions (no network; fs writes confined to OS-tmpdir fixtures)
node test.mjs # 313-assertion baseline + proxy tests (local mocks only; temp filesystem)
node sign.test.mjs # ed25519 signing + canon parity

@@ -405,3 +437,4 @@ node index.mjs # stdio MCP server directly (pipe to MCP client)

- `tokenpullCodex` io_ratio conversion per-window
- Adapter registry (16 platforms) + per-adapter shape contracts
- Adapter registry (17 platforms) + per-adapter shape contracts
- Local proxy: Anthropic/OpenAI JSON + fragmented SSE, live pass-through, secure JSONL, error forwarding
- `rank_windows` 4-window paste scoring, partial input, no-network

@@ -422,2 +455,3 @@ - `watch_tokenpull` cascade snapshot, interval_s, submit path

| `index.mjs` | Entry point — TTY detection, routes to CLI or MCP server |
| `proxy.mjs` | Opt-in loopback Anthropic/OpenAI proxy and usage capture |
| `cli.mjs` | CLI commands: board, compare, watch, enroll, submit, help |

@@ -424,0 +458,0 @@ | `tui.mjs` | Full tabbed TUI: Dashboard / Trends / Compare / Board / Watch / Connect |

@@ -12,5 +12,5 @@ # SigRank Class Tiers

The class is an **experience** axis — it measures how much volume the operator
has accumulated. It is separate from the **cascade archetype** (the operator's
token-flow shape: kinetic loop, archival sponge, volatile ingestor, etc.) and
from the **TRANSMITTER peak badge** (a temporary state — see below).
has accumulated. It is separate from the **build archetype** (the operator's
composition shape: CONVERGENT, KINETIC, INPUT-BOUND, ARCHIVIST, AMPLIFIER, etc.)
and from the **TRANSMITTER peak badge** (a temporary state — see below).

@@ -17,0 +17,0 @@ A 25th value, **UNCLASSED**, is returned when total tokens are null or

@@ -13,3 +13,7 @@ # SigRank Data Policy

We do **not** read or transmit the content of your prompts or AI conversations. The local agent extracts only numeric token counts and metadata; transcripts never leave your device.
We do **not** collect or receive the content of your prompts or AI conversations
at the SigRank service. Local-log adapters extract only numeric usage metadata.
If you explicitly run the optional local API proxy, it forwards provider-bound
requests and responses in memory but does not persist their content; only usage
metadata is written locally or submitted to SigRank.

@@ -16,0 +20,0 @@ ## Consent

@@ -13,2 +13,9 @@ # SigRank Privacy Model

The optional `sigrank proxy` is a local transport path to Anthropic/OpenAI, not a
submission path to SigRank. When explicitly enabled, it necessarily receives and
forwards provider-bound API keys, prompts, tool calls, and responses in memory.
It does not persist that content or send it to the SigRank service; it appends
only provider-reported token counts, model/backend metadata, and timestamps to
`~/.sigrank-mcp/proxy-sessions.jsonl`.
- **Signed path** (`submit_verified`, `watch_tokenpull` with `submit:true`): the four numbers travel with the device's public key, codename, window, and an ed25519 signature. The board verifies the signature without seeing your data.

@@ -19,8 +26,8 @@ - **Paste path** (`submit_paste`, `tokenpull_submit`): the MCP parses your pasted token counts locally, then sends only the four canonical numbers to the server's web-paste endpoint. Even if you paste mixed text (prose + numbers), only the extracted token counts are transmitted — the raw text never leaves your machine.

1. **Local-first:** All token pulling happens on your machine. SigRank reads session logs from ~/.claude, ~/.codex, ~/.local/share/amp, etc.
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. On the paste path, the MCP parses the paste locally and sends only the four extracted numbers — the raw paste text stays on your machine.
1. **Local-first:** All token pulling happens on your machine. SigRank reads session logs from ~/.claude, ~/.codex, ~/.local/share/amp, etc. The optional proxy also runs only on loopback and only when manually started.
2. **Token-only persistence:** Local-log adapters extract integer counts from log metadata without reading conversation content. The optional proxy forwards request/response bytes transiently but persists only usage metadata. On the paste path, the MCP parses the paste locally and sends only the four extracted numbers — the raw paste text stays on your machine.
3. **Signed submission:** Ranked submissions (`submit_verified`, `watch_tokenpull` with `submit:true`) are ed25519-signed with a device-bound key generated locally at enrollment. The board verifies the signature without seeing your data. Paste submissions (`submit_paste`, `tokenpull_submit`) are unsigned and go through the web-paste endpoint with a codename only — but still send only the four token counts, not the raw paste.
4. **Read tools need no auth:** No API keys, no OAuth, no account needed to read the leaderboard or operator profiles. **Enrollment requires a connect code** from signalaf.com → Settings → New key (the code binds your device's public key to your operator server-side); a codename alone is not enough for the signed/ranked path.
## What SigRank can NOT see
## What the SigRank service can NOT see

@@ -33,4 +40,8 @@ - Your prompts or messages

The local proxy process is different from the SigRank service: if you opt in, it
handles your provider traffic in memory solely to forward it to Anthropic or
OpenAI. That content is never stored in the proxy JSONL file.
## Verification
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.

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display