sigrank 0.0.178 → 0.0.180
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 +100 -0
- package/MCP_SERVER_DESCRIPTION.md +141 -0
- package/README.md +8 -0
- package/adapters/index.mjs +865 -0
- package/adapters/tokenpull.mjs +576 -0
- package/adapters.mjs +1 -865
- package/analytics/cascade.mjs +274 -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 +242 -0
- package/identity/sign.mjs +106 -0
- package/index.mjs +43 -159
- package/keystore.mjs +1 -242
- package/narrate.mjs +1 -77
- package/package.json +32 -15
- package/preflight.mjs +1 -191
- package/presentation/cli.mjs +1874 -0
- package/presentation/narrate.mjs +77 -0
- package/presentation/tui.mjs +2668 -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/resources/class-tiers.md +23 -0
- package/resources/data-policy.md +27 -0
- package/resources/install-guide.md +59 -0
- package/resources/privacy-model.md +30 -0
- package/resources/scoring-formula.md +28 -0
- package/review.mjs +263 -0
- package/sign.mjs +1 -106
- package/submit/index.mjs +309 -0
- package/submit/preflight.mjs +191 -0
- package/submit.mjs +1 -303
- package/tokenpull.mjs +1 -576
- package/tokscale_analytics.mjs +875 -0
- package/tools/_framing.mjs +139 -0
- package/tools/_helpers.mjs +258 -0
- package/tools/_report.mjs +262 -0
- package/tools/_schemas.mjs +394 -0
- package/tools/_verifiers.mjs +148 -0
- package/tools/compare-operators.mjs +79 -0
- package/tools/compare-self.mjs +122 -0
- package/tools/describe-power-user.mjs +68 -0
- package/tools/diagnose-cascade.mjs +192 -0
- package/tools/enroll.mjs +121 -0
- package/tools/get-best-operator.mjs +58 -0
- package/tools/get-leaderboard.mjs +26 -0
- package/tools/get-operator.mjs +33 -0
- package/tools/index.mjs +246 -0
- package/tools/optimize-efficiency.mjs +160 -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 +118 -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 +234 -0
- package/tools/tokenpull.mjs +113 -0
- package/tools/tokscale-breakdown.mjs +159 -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,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/_framing.mjs — behavioral framing helpers for intent-based tools.
|
|
3
|
+
*
|
|
4
|
+
* Power-user language + competitive layer shared by get_best_operator,
|
|
5
|
+
* compare_self, compare_operators, and optimize_efficiency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DEFAULT_API_BASE } from "./_helpers.mjs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Competitive layer for tool responses per SHARED_DESIGN_DECISIONS.md §3/§4/§5.
|
|
12
|
+
* Every tool response that includes operator data must show:
|
|
13
|
+
* rank, percentile, class_tier, delta_from_average, delta_from_top, shareable_url
|
|
14
|
+
* Response style: factual + competitive ("You rank #12 of 47 operators...")
|
|
15
|
+
*/
|
|
16
|
+
export function _competitiveLayer(op, board) {
|
|
17
|
+
const allOps = Array.isArray(board?.operators || board)
|
|
18
|
+
? board.operators || board
|
|
19
|
+
: [];
|
|
20
|
+
const yields = allOps.map((o) => o.yield_ || 0).sort((a, b) => a - b);
|
|
21
|
+
const yourYield = op.yield_ || 0;
|
|
22
|
+
const total = allOps.length;
|
|
23
|
+
|
|
24
|
+
// Rank: find operator's position (1-based)
|
|
25
|
+
let rank = op.rank || null;
|
|
26
|
+
if (!rank && total > 0) {
|
|
27
|
+
const sorted = [...allOps].sort((a, b) => (b.yield_ || 0) - (a.yield_ || 0));
|
|
28
|
+
const idx = sorted.findIndex((o) => o.codename === op.codename);
|
|
29
|
+
rank = idx >= 0 ? idx + 1 : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Percentile: % of operators with yield below this operator
|
|
33
|
+
const percentile = total > 0
|
|
34
|
+
? Math.round((yields.filter((y) => y < yourYield).length / total) * 100)
|
|
35
|
+
: 0;
|
|
36
|
+
|
|
37
|
+
// Delta from average
|
|
38
|
+
const avgYield = total > 0 ? yields.reduce((s, y) => s + y, 0) / total : 0;
|
|
39
|
+
const deltaFromAvg = avgYield > 0
|
|
40
|
+
? { absolute: Math.round(yourYield - avgYield),
|
|
41
|
+
percent: Math.round(((yourYield - avgYield) / avgYield) * 100) }
|
|
42
|
+
: { absolute: 0, percent: 0 };
|
|
43
|
+
|
|
44
|
+
// Delta from top operator
|
|
45
|
+
const topYield = total > 0 ? Math.max(...yields) : 0;
|
|
46
|
+
const deltaFromTop = topYield > 0
|
|
47
|
+
? { absolute: Math.round(topYield - yourYield),
|
|
48
|
+
percent: Math.round(((topYield - yourYield) / topYield) * 100) }
|
|
49
|
+
: { absolute: 0, percent: 0 };
|
|
50
|
+
|
|
51
|
+
// Shareable URL
|
|
52
|
+
const shareableUrl = op.codename && op.codename !== "you (local)"
|
|
53
|
+
? `${DEFAULT_API_BASE}/operator/${encodeURIComponent(op.codename)}`
|
|
54
|
+
: null;
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
rank,
|
|
58
|
+
total_operators: total,
|
|
59
|
+
percentile,
|
|
60
|
+
class_tier: op.class || "Burner",
|
|
61
|
+
delta_from_average: deltaFromAvg,
|
|
62
|
+
delta_from_top: deltaFromTop,
|
|
63
|
+
shareable_url: shareableUrl,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Factual + competitive summary line per SHARED_DESIGN_DECISIONS.md §4 */
|
|
68
|
+
export function _competitiveSummary(op, board) {
|
|
69
|
+
const cl = _competitiveLayer(op, board);
|
|
70
|
+
const parts = [];
|
|
71
|
+
|
|
72
|
+
if (cl.rank && cl.total_operators > 0) {
|
|
73
|
+
parts.push(`You rank #${cl.rank} of ${cl.total_operators} operators.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const topOp = (board?.operators || board || []).reduce(
|
|
77
|
+
(best, o) => ((o.yield_ || 0) > (best?.yield_ || 0) ? o : best),
|
|
78
|
+
null,
|
|
79
|
+
);
|
|
80
|
+
if (topOp && topOp.codename) {
|
|
81
|
+
parts.push(`Top operator is ${topOp.codename} with Υ ${(topOp.yield_ || 0).toLocaleString()}.`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (cl.delta_from_average.percent !== 0) {
|
|
85
|
+
const dir = cl.delta_from_average.percent > 0 ? "above" : "below";
|
|
86
|
+
parts.push(`You're ${Math.abs(cl.delta_from_average.percent)}% ${dir} average.`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (cl.delta_from_top.percent > 0) {
|
|
90
|
+
parts.push(`${cl.delta_from_top.percent}% below top.`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return parts.join(" ");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function _behavioralFraming(op) {
|
|
97
|
+
const y = op.yield_ || 0;
|
|
98
|
+
const l = op.leverage || 0;
|
|
99
|
+
const v = op.velocity || 0;
|
|
100
|
+
const klass = op.class || "Burner";
|
|
101
|
+
|
|
102
|
+
if (klass === "10xer")
|
|
103
|
+
return `Disciplined, system-level reuse: ${l.toFixed(1)}× leverage means heavy cache reuse over fresh input, ${v.toFixed(2)} velocity means more output per token spent. This is the AI power-user archetype.`;
|
|
104
|
+
if (klass === "Builder")
|
|
105
|
+
return `Building cascade momentum: moderate cache reuse (${l.toFixed(1)}× leverage) with ${v.toFixed(2)} output velocity. Approaching power-user patterns — increase cache reuse to push into 10xer territory.`;
|
|
106
|
+
return `Early-stage cascade: ${v.toFixed(2)} output velocity with ${l.toFixed(1)}× leverage. Tokens are being burned more than compounded. Focus on reusing prior context (templates, prompts, workflows) to build leverage.`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function _powerUserAssessment(klass, metrics) {
|
|
110
|
+
const l = metrics.leverage || 0;
|
|
111
|
+
const v = metrics.velocity || 0;
|
|
112
|
+
if (klass === "10xer")
|
|
113
|
+
return `You are an AI power user. Your SigRank class (10xer) indicates you reuse prior work heavily (${l.toFixed(1)}× leverage), get more out of each token (${v.toFixed(2)} velocity), and keep input lean. This is consistent with AI power-user behavior: iterative, efficient, multi-use patterns.`;
|
|
114
|
+
if (klass === "Builder")
|
|
115
|
+
return `You are becoming an AI power user. Your Builder class shows growing cache reuse (${l.toFixed(1)}× leverage) and ${v.toFixed(2)} output velocity. You're building the habits — increase context reuse to push into 10xer territory.`;
|
|
116
|
+
return `You are not yet an AI power user. Your Burner class means tokens are being spent without compounding. The power-user shift: reuse prior context (prompts, templates, cached results) instead of starting fresh each time. Your leverage (${l.toFixed(1)}×) is the key metric to improve.`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function _classMeaning(klass) {
|
|
120
|
+
if (klass === "10xer")
|
|
121
|
+
return "AI power user archetype — disciplined, system-level reuse, high output per input.";
|
|
122
|
+
if (klass === "Builder")
|
|
123
|
+
return "Building momentum — moderate reuse, approaching power-user patterns.";
|
|
124
|
+
return "Early-stage — tokens burned more than compounded. Focus on cache reuse.";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function _improvementSuggestion(klass, metrics) {
|
|
128
|
+
const l = metrics.leverage || 0;
|
|
129
|
+
const v = metrics.velocity || 0;
|
|
130
|
+
if (klass === "10xer")
|
|
131
|
+
return v < 1
|
|
132
|
+
? "Your leverage is excellent but velocity is under 1.0 — you're reading more cache than producing output. Push for more output per session."
|
|
133
|
+
: "You're at the top tier. Maintain your cache architecture and experiment with longer sessions to compound yield further.";
|
|
134
|
+
if (klass === "Builder")
|
|
135
|
+
return l < 5
|
|
136
|
+
? "Increase cache reuse: reuse prompts, templates, and workflows instead of starting from scratch. Each reused token multiplies your yield."
|
|
137
|
+
: "Your leverage is solid. Focus on output velocity — produce more per session to push your yield up.";
|
|
138
|
+
return "Start by reusing prior context. Instead of fresh prompts each time, build on cached results. Even a 2× increase in cache_read will dramatically improve your yield because input² is in the denominator.";
|
|
139
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/_helpers.mjs — shared helpers for the SigRank MCP tool suite.
|
|
3
|
+
*
|
|
4
|
+
* Transport-free utilities used across multiple tool handlers:
|
|
5
|
+
* exec wrapper, curl fallback, fetch builder, upload stamp, platform pull,
|
|
6
|
+
* parse-warning merge, and watch-tokenpull cooldown state.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import {
|
|
16
|
+
tokenpull as pullLocal,
|
|
17
|
+
tokenpullCodex as pullCodex,
|
|
18
|
+
tokenpullAny,
|
|
19
|
+
} from "../adapters/tokenpull.mjs";
|
|
20
|
+
|
|
21
|
+
// Resolve local node_modules/.bin for bundled deps (ccusage, tokscale)
|
|
22
|
+
const _pkgRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const _localBin = path.join(_pkgRoot, "..", "node_modules", ".bin");
|
|
24
|
+
const _envPath = `${_localBin}${process.env.PATH ? ":" + process.env.PATH : ""}`;
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_API_BASE =
|
|
27
|
+
process.env.SIGRANK_API_BASE || "https://signalaf.com";
|
|
28
|
+
/** Default network timeout in ms (override via opts.fetchTimeout or SIGRANK_FETCH_TIMEOUT). */
|
|
29
|
+
export const DEFAULT_FETCH_TIMEOUT =
|
|
30
|
+
Number(process.env.SIGRANK_FETCH_TIMEOUT) || 10_000;
|
|
31
|
+
/** Max accepted length for a single paste/string arg (chars). Token counts are tiny; anything
|
|
32
|
+
* past this is malformed or abusive — reject cleanly before parsing/POSTing (E2 hardening). */
|
|
33
|
+
export const MAX_INPUT = 1_000_000;
|
|
34
|
+
|
|
35
|
+
// E3: client-side auto-submit cooldown for watch_tokenpull. The server already dedups
|
|
36
|
+
// identical snapshots (exact hash → 422), but a noisy poll loop with submit:true could still
|
|
37
|
+
// churn the network/board with near-identical rows. Cap auto-submit to once per WATCH_SUBMIT_COOLDOWN_MS
|
|
38
|
+
// per platform+window (in-memory, per process). Keyed by platform:window so different
|
|
39
|
+
// platforms/windows don't block each other, and only armed on a non-error submit so a
|
|
40
|
+
// network failure doesn't lock out retries for 5 minutes.
|
|
41
|
+
export const WATCH_SUBMIT_COOLDOWN_MS = 5 * 60 * 1000;
|
|
42
|
+
export const _lastWatchSubmitAt = new Map();
|
|
43
|
+
|
|
44
|
+
// ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
|
|
45
|
+
// defense-in-depth (shell injection prevention + non-blocking). The platform param
|
|
46
|
+
// is enum-validated at the MCP schema level, but execFile also prevents shell
|
|
47
|
+
// interpolation attacks by passing args as an array (no shell parsing).
|
|
48
|
+
// BIN FIX (2026-06-27): PATH includes local node_modules/.bin so bundled deps
|
|
49
|
+
// are found even when sigrank isn't globally installed.
|
|
50
|
+
export function execFileAsync(cmd, args, timeoutMs) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
// NOTE: execFile does not accept a `stdio` option (it always pipes + buffers
|
|
53
|
+
// stdout/stderr against maxBuffer) — a previous `stdio` key here was silently ignored.
|
|
54
|
+
execFile(
|
|
55
|
+
cmd,
|
|
56
|
+
args,
|
|
57
|
+
{
|
|
58
|
+
timeout: timeoutMs,
|
|
59
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
60
|
+
env: { ...process.env, PATH: _envPath },
|
|
61
|
+
},
|
|
62
|
+
(err, stdout) => {
|
|
63
|
+
if (err) reject(err);
|
|
64
|
+
else resolve(stdout.toString());
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Resolve this package's version for the User-Agent stamp (best-effort). */
|
|
71
|
+
export function agentVersionStamp() {
|
|
72
|
+
try {
|
|
73
|
+
const pkg = JSON.parse(
|
|
74
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
|
|
75
|
+
);
|
|
76
|
+
return pkg.version;
|
|
77
|
+
} catch {
|
|
78
|
+
return "unknown";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* curl-based fetch fallback for when Node's fetch is blocked by Vercel's bot
|
|
84
|
+
* protection (TLS fingerprinting). Returns a Response-like object with .ok,
|
|
85
|
+
* .status, .json(), .text(). Uses execFileSync for synchronous curl execution.
|
|
86
|
+
*/
|
|
87
|
+
export function curlFetch(url, init = {}, timeoutMs = 10_000) {
|
|
88
|
+
const args = [
|
|
89
|
+
"-s",
|
|
90
|
+
"-S",
|
|
91
|
+
"-w",
|
|
92
|
+
"\n__HTTP_STATUS__%{http_code}",
|
|
93
|
+
"--max-time",
|
|
94
|
+
String(Math.ceil(timeoutMs / 1000)),
|
|
95
|
+
];
|
|
96
|
+
if (init.method) args.push("-X", init.method);
|
|
97
|
+
const headers = init.headers || {};
|
|
98
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
99
|
+
args.push("-H", `${k}: ${v}`);
|
|
100
|
+
}
|
|
101
|
+
if (init.body) {
|
|
102
|
+
args.push("-d", init.body);
|
|
103
|
+
}
|
|
104
|
+
args.push(url);
|
|
105
|
+
let stdout;
|
|
106
|
+
try {
|
|
107
|
+
stdout = execFileSync("curl", args, {
|
|
108
|
+
encoding: "utf-8",
|
|
109
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
110
|
+
timeout: timeoutMs + 2000,
|
|
111
|
+
});
|
|
112
|
+
} catch (e) {
|
|
113
|
+
throw new Error(`curl transport failed: ${e.message}`);
|
|
114
|
+
}
|
|
115
|
+
// Parse the status code from the trailer
|
|
116
|
+
const statusMatch = stdout.match(/__HTTP_STATUS__(\d+)$/);
|
|
117
|
+
const status = statusMatch ? parseInt(statusMatch[1], 10) : 0;
|
|
118
|
+
const bodyText = statusMatch ? stdout.slice(0, statusMatch.index) : stdout;
|
|
119
|
+
return {
|
|
120
|
+
ok: status >= 200 && status < 300,
|
|
121
|
+
status,
|
|
122
|
+
json: async () => {
|
|
123
|
+
try {
|
|
124
|
+
return JSON.parse(bodyText);
|
|
125
|
+
} catch {
|
|
126
|
+
return {};
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
text: async () => bodyText,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Every board upload from the MCP is hashed + timestamped (ddmmyy) — provenance + dedup.
|
|
134
|
+
export function uploadStamp(content) {
|
|
135
|
+
const hash = createHash("sha256")
|
|
136
|
+
.update(JSON.stringify(content))
|
|
137
|
+
.digest("hex");
|
|
138
|
+
const d = new Date();
|
|
139
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
140
|
+
const ddmmyy =
|
|
141
|
+
p(d.getUTCDate()) +
|
|
142
|
+
p(d.getUTCMonth() + 1) +
|
|
143
|
+
String(d.getUTCFullYear()).slice(-2);
|
|
144
|
+
return {
|
|
145
|
+
content_hash: hash,
|
|
146
|
+
submitted_ddmmyy: ddmmyy,
|
|
147
|
+
submitted_at: d.toISOString(),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Pull a platform's local usage → 4 windows of canonical pillars. Routes through
|
|
152
|
+
// tokenpullAny() which handles Claude (native), Codex (estimated io_ratio), and all
|
|
153
|
+
// other adapters from the registry. opts.adapter overrides for tests.
|
|
154
|
+
export async function pullByPlatform(platform, opts = {}) {
|
|
155
|
+
if (opts.adapter) {
|
|
156
|
+
// Test injection: bypass registry and use the mock adapter directly
|
|
157
|
+
if (platform === "codex") {
|
|
158
|
+
let ioRatio = 2.0;
|
|
159
|
+
try {
|
|
160
|
+
const c = await pullLocal({});
|
|
161
|
+
const all = c.windows.find((w) => w.window === "all");
|
|
162
|
+
if (all && all.pillars.output > 0)
|
|
163
|
+
ioRatio = all.pillars.input / all.pillars.output;
|
|
164
|
+
} catch {
|
|
165
|
+
/* no Claude data → Alpha 2.0 */
|
|
166
|
+
}
|
|
167
|
+
return pullCodex({ ioRatio, adapter: opts.adapter, now: opts.now });
|
|
168
|
+
}
|
|
169
|
+
return pullLocal({ adapter: opts.adapter, now: opts.now });
|
|
170
|
+
}
|
|
171
|
+
return tokenpullAny(platform || "claude", opts);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build a fetch wrapper with timeout + curl fallback + User-Agent stamp.
|
|
176
|
+
* Returns { doFetch, fetchJson } bound to the given opts.
|
|
177
|
+
*/
|
|
178
|
+
export function buildFetch(opts = {}) {
|
|
179
|
+
const apiBase = opts.apiBase || DEFAULT_API_BASE;
|
|
180
|
+
const timeoutMs = opts.fetchTimeout ?? DEFAULT_FETCH_TIMEOUT;
|
|
181
|
+
const rawFetch = opts.fetchImpl || fetch;
|
|
182
|
+
|
|
183
|
+
const doFetch = async (url, init = {}) => {
|
|
184
|
+
const ac = new AbortController();
|
|
185
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
186
|
+
const headers = {
|
|
187
|
+
"user-agent": `node/${process.version} sigrank-mcp/${agentVersionStamp()}`,
|
|
188
|
+
...(init.headers || {}),
|
|
189
|
+
};
|
|
190
|
+
try {
|
|
191
|
+
const res = await rawFetch(url, { ...init, headers, signal: ac.signal });
|
|
192
|
+
// Detect Vercel security checkpoint (403 + HTML body, not JSON)
|
|
193
|
+
if (res.status === 403) {
|
|
194
|
+
const text = await res.text();
|
|
195
|
+
if (
|
|
196
|
+
text.includes("Vercel Security Checkpoint") ||
|
|
197
|
+
text.includes("x-vercel-challenge")
|
|
198
|
+
) {
|
|
199
|
+
// Fall back to curl transport
|
|
200
|
+
return curlFetch(url, init, timeoutMs);
|
|
201
|
+
}
|
|
202
|
+
// Real 403 from the API — return a mock response object
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
status: 403,
|
|
206
|
+
json: async () => {
|
|
207
|
+
try {
|
|
208
|
+
return JSON.parse(text);
|
|
209
|
+
} catch {
|
|
210
|
+
return {};
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
text: async () => text,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return res;
|
|
217
|
+
} catch (e) {
|
|
218
|
+
if (e.name === "AbortError") throw e;
|
|
219
|
+
// Network error — try curl as fallback
|
|
220
|
+
try {
|
|
221
|
+
return await curlFetch(url, init, timeoutMs);
|
|
222
|
+
} catch {
|
|
223
|
+
throw e;
|
|
224
|
+
}
|
|
225
|
+
} finally {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const fetchJson = async (path) => {
|
|
231
|
+
const res = await doFetch(`${apiBase}${path}`, {
|
|
232
|
+
headers: { accept: "application/json" },
|
|
233
|
+
});
|
|
234
|
+
if (!res.ok) throw new Error(`SigRank API ${path} → HTTP ${res.status}`);
|
|
235
|
+
return res.json();
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return { apiBase, doFetch, fetchJson };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Helper: attach _parseWarnings from pillars onto the cascade result so they
|
|
242
|
+
// are always visible in the tool output for review.
|
|
243
|
+
export function withParseWarnings(pillars, cascadeResult) {
|
|
244
|
+
if (pillars._parseWarnings && pillars._parseWarnings.length > 0) {
|
|
245
|
+
const existing = cascadeResult.warnings || [];
|
|
246
|
+
return {
|
|
247
|
+
...cascadeResult,
|
|
248
|
+
warnings: [
|
|
249
|
+
...existing,
|
|
250
|
+
...pillars._parseWarnings.map((w) => `parse:${w}`),
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return cascadeResult;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// tokenpull window key → the board's window_type enum.
|
|
258
|
+
export const WINDOW_TYPE = { "7d": "7d", "30d": "30d", "90d": "90d", all: "all_time" };
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/_report.mjs — cascade report blocks + self-improvement scope helpers.
|
|
3
|
+
*
|
|
4
|
+
* Used by submit_verified (per-window report payload) and self_improve
|
|
5
|
+
* (weekly/trend scope analysis).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cascade, detectMode, qualityScore, MODE_EXPECTED_YIELD } from "../analytics/cascade.mjs";
|
|
9
|
+
import { computeBadges } from "../badges.mjs";
|
|
10
|
+
import { execFileAsync } from "./_helpers.mjs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Compute the cascade report block for a submission payload.
|
|
14
|
+
* Pure math — mode detection + badges + health score from the current pillars.
|
|
15
|
+
* The server stores this as-is (does NOT recompute modes).
|
|
16
|
+
* Weekly granularity is the privacy boundary — no daily modes in the report.
|
|
17
|
+
*/
|
|
18
|
+
export function _computeReportBlock(pillars) {
|
|
19
|
+
const cas = cascade(pillars);
|
|
20
|
+
const modeInfo = detectMode(pillars);
|
|
21
|
+
const badges = computeBadges({
|
|
22
|
+
pillars,
|
|
23
|
+
cascade: cas,
|
|
24
|
+
history: [], // no history available at submit time — badges computed from current pillars only
|
|
25
|
+
isVerified: true, // submit_verified is the signed agent path
|
|
26
|
+
rank: null,
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
current_mode: modeInfo.mode,
|
|
30
|
+
mode_confidence: modeInfo.confidence,
|
|
31
|
+
mode_distribution: { [modeInfo.mode]: 1.0 }, // single-window submission
|
|
32
|
+
mode_weighted_yield: cas.yield ?? 0,
|
|
33
|
+
peak_yield: cas.yield ?? 0,
|
|
34
|
+
health_score: _healthScore(
|
|
35
|
+
[{ mode: modeInfo.mode, yield: cas.yield ?? 0 }],
|
|
36
|
+
cas.yield ?? 0,
|
|
37
|
+
),
|
|
38
|
+
badges,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Pull daily rows from ccusage to build mode history. Returns array of { date, pillars, yield, mode }. */
|
|
43
|
+
export async function _pullDailyRows(opts = {}) {
|
|
44
|
+
try {
|
|
45
|
+
const raw = await execFileAsync(
|
|
46
|
+
"ccusage",
|
|
47
|
+
["claude", "daily", "--json"],
|
|
48
|
+
15000,
|
|
49
|
+
);
|
|
50
|
+
const rows = JSON.parse(raw)?.daily ?? JSON.parse(raw);
|
|
51
|
+
if (!Array.isArray(rows)) return [];
|
|
52
|
+
return rows
|
|
53
|
+
.map((r) => {
|
|
54
|
+
const pillars = {
|
|
55
|
+
input: r.inputTokens ?? r.input_tokens ?? 0,
|
|
56
|
+
output: r.outputTokens ?? r.output_tokens ?? 0,
|
|
57
|
+
cacheCreate: r.cacheCreationTokens ?? r.cache_create_tokens ?? 0,
|
|
58
|
+
cacheRead: r.cacheReadTokens ?? r.cache_read_tokens ?? 0,
|
|
59
|
+
};
|
|
60
|
+
const cas = cascade(pillars);
|
|
61
|
+
const mode = detectMode(pillars);
|
|
62
|
+
return {
|
|
63
|
+
date: r.date ?? r.day ?? null,
|
|
64
|
+
pillars,
|
|
65
|
+
yield: cas.yield ?? 0,
|
|
66
|
+
mode: mode.mode,
|
|
67
|
+
mode_confidence: mode.confidence,
|
|
68
|
+
};
|
|
69
|
+
})
|
|
70
|
+
.filter((r) => r.date);
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Compound daily rows into weekly snapshots. Each week = 7 days, starting Monday. */
|
|
77
|
+
export function _compoundWeekly(dailyRows) {
|
|
78
|
+
if (!dailyRows || dailyRows.length === 0) return [];
|
|
79
|
+
// Group by ISO week (week starts Monday)
|
|
80
|
+
const weeks = new Map();
|
|
81
|
+
for (const row of dailyRows) {
|
|
82
|
+
const d = new Date(row.date);
|
|
83
|
+
const day = d.getDay() || 7; // Sunday=0 → 7
|
|
84
|
+
const monday = new Date(d);
|
|
85
|
+
monday.setDate(d.getDate() - day + 1);
|
|
86
|
+
const weekKey = monday.toISOString().slice(0, 10);
|
|
87
|
+
if (!weeks.has(weekKey)) {
|
|
88
|
+
weeks.set(weekKey, { weekStart: weekKey, days: [] });
|
|
89
|
+
}
|
|
90
|
+
weeks.get(weekKey).days.push(row);
|
|
91
|
+
}
|
|
92
|
+
// Compound each week
|
|
93
|
+
return Array.from(weeks.values())
|
|
94
|
+
.map((w) => {
|
|
95
|
+
const pillars = w.days.reduce(
|
|
96
|
+
(acc, d) => ({
|
|
97
|
+
input: acc.input + d.pillars.input,
|
|
98
|
+
output: acc.output + d.pillars.output,
|
|
99
|
+
cacheCreate: acc.cacheCreate + d.pillars.cacheCreate,
|
|
100
|
+
cacheRead: acc.cacheRead + d.pillars.cacheRead,
|
|
101
|
+
}),
|
|
102
|
+
{ input: 0, output: 0, cacheCreate: 0, cacheRead: 0 },
|
|
103
|
+
);
|
|
104
|
+
const cas = cascade(pillars);
|
|
105
|
+
const mode = detectMode(pillars);
|
|
106
|
+
return {
|
|
107
|
+
weekStart: w.weekStart,
|
|
108
|
+
pillars,
|
|
109
|
+
yield: cas.yield ?? 0,
|
|
110
|
+
mode: mode.mode,
|
|
111
|
+
mode_confidence: mode.confidence,
|
|
112
|
+
dayCount: w.days.length,
|
|
113
|
+
dailyModes: w.days.map((d) => d.mode), // stays local — not in submitted report
|
|
114
|
+
};
|
|
115
|
+
})
|
|
116
|
+
.sort((a, b) => a.weekStart.localeCompare(b.weekStart));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Compute mode distribution from weekly snapshots (weekly granularity = privacy boundary). */
|
|
120
|
+
export function _modeDistribution(weeklySnapshots) {
|
|
121
|
+
if (!weeklySnapshots || weeklySnapshots.length === 0) return {};
|
|
122
|
+
const counts = { BUILD: 0, EDIT: 0, DEBUG: 0, MAINTAIN: 0, IDLE: 0 };
|
|
123
|
+
for (const w of weeklySnapshots) {
|
|
124
|
+
counts[w.mode] = (counts[w.mode] || 0) + 1;
|
|
125
|
+
}
|
|
126
|
+
const total = weeklySnapshots.length;
|
|
127
|
+
const dist = {};
|
|
128
|
+
for (const [mode, count] of Object.entries(counts)) {
|
|
129
|
+
if (count > 0) dist[mode] = Math.round((count / total) * 100) / 100;
|
|
130
|
+
}
|
|
131
|
+
return dist;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Mode-weighted yield — average yield weighted by mode distribution. */
|
|
135
|
+
export function _modeWeightedYield(weeklySnapshots) {
|
|
136
|
+
if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
|
|
137
|
+
const total = weeklySnapshots.length;
|
|
138
|
+
const sum = weeklySnapshots.reduce((acc, w) => acc + (w.yield || 0), 0);
|
|
139
|
+
return Math.round(sum / total);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Peak yield across all weekly snapshots. */
|
|
143
|
+
export function _peakYield(weeklySnapshots) {
|
|
144
|
+
if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
|
|
145
|
+
return Math.max(...weeklySnapshots.map((w) => w.yield || 0));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Health score (0-100) — weighted composite of consistency, momentum, quality. */
|
|
149
|
+
export function _healthScore(weeklySnapshots, modeWeightedYield) {
|
|
150
|
+
if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
|
|
151
|
+
// Simplified Phase 1 formula:
|
|
152
|
+
// 40% — consistency (how often in MAINTAIN)
|
|
153
|
+
// 30% — momentum (recent yield vs older yield)
|
|
154
|
+
// 30% — quality (mode-weighted yield relative to MAINTAIN expected)
|
|
155
|
+
const dist = _modeDistribution(weeklySnapshots);
|
|
156
|
+
const maintainShare = dist.MAINTAIN || 0;
|
|
157
|
+
const consistency = Math.min(maintainShare, 1) * 40;
|
|
158
|
+
|
|
159
|
+
// Momentum: compare last 3 weeks to first 3 weeks
|
|
160
|
+
const recent = weeklySnapshots.slice(-3);
|
|
161
|
+
const older = weeklySnapshots.slice(0, 3);
|
|
162
|
+
const recentAvg =
|
|
163
|
+
recent.reduce((a, w) => a + (w.yield || 0), 0) / (recent.length || 1);
|
|
164
|
+
const olderAvg =
|
|
165
|
+
older.reduce((a, w) => a + (w.yield || 0), 0) / (older.length || 1);
|
|
166
|
+
const momentumRatio = olderAvg > 0 ? recentAvg / olderAvg : 1;
|
|
167
|
+
const momentum = (Math.min(momentumRatio, 2) / 2) * 30;
|
|
168
|
+
|
|
169
|
+
// Quality: mode-weighted yield vs MAINTAIN expected (5000)
|
|
170
|
+
const quality = Math.min(modeWeightedYield / 5000, 1) * 30;
|
|
171
|
+
|
|
172
|
+
return Math.round(consistency + momentum + quality);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Yield for the last N days (from weekly snapshots — approximates by summing recent weeks). */
|
|
176
|
+
export function _yieldForDays(weeklySnapshots, days) {
|
|
177
|
+
if (!weeklySnapshots || weeklySnapshots.length === 0) return 0;
|
|
178
|
+
const weeksNeeded = Math.ceil(days / 7);
|
|
179
|
+
const recent = weeklySnapshots.slice(-weeksNeeded);
|
|
180
|
+
if (recent.length === 0) return 0;
|
|
181
|
+
const sum = recent.reduce((a, w) => a + (w.yield || 0), 0);
|
|
182
|
+
return Math.round(sum / recent.length); // average weekly yield
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Trajectory description from 7d/30d/90d yields. */
|
|
186
|
+
export function _trajectory(y7, y30, y90) {
|
|
187
|
+
if (y90 === 0) return "insufficient_data";
|
|
188
|
+
const r7v30 = y30 > 0 ? y7 / y30 : 1;
|
|
189
|
+
const r30v90 = y90 > 0 ? y30 / y90 : 1;
|
|
190
|
+
if (r7v30 > 1.2 && r30v90 > 1.0) return "accelerating";
|
|
191
|
+
if (r7v30 > 1.2) return "recent_surge";
|
|
192
|
+
if (r7v30 < 0.8 && r30v90 < 0.8) return "declining";
|
|
193
|
+
if (r7v30 < 0.8) return "recent_dip";
|
|
194
|
+
if (r30v90 > 1.1) return "steady_growth";
|
|
195
|
+
return "stable";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Phase pattern description from mode distribution over time. */
|
|
199
|
+
export function _phasePattern(weeklySnapshots) {
|
|
200
|
+
if (!weeklySnapshots || weeklySnapshots.length < 2)
|
|
201
|
+
return "insufficient_data";
|
|
202
|
+
const modes = weeklySnapshots.map((w) => w.mode);
|
|
203
|
+
const transitions = [];
|
|
204
|
+
for (let i = 1; i < modes.length; i++) {
|
|
205
|
+
if (modes[i] !== modes[i - 1]) {
|
|
206
|
+
transitions.push(`${modes[i - 1]}→${modes[i]}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (transitions.length === 0) return `consistent_${modes[0].toLowerCase()}`;
|
|
210
|
+
// Check for smooth transitions (BUILD→MAINTAIN without DEBUG)
|
|
211
|
+
const smooth = transitions.every((t) => !t.includes("DEBUG"));
|
|
212
|
+
if (smooth && transitions.some((t) => t === "BUILD→MAINTAIN"))
|
|
213
|
+
return "smooth_ramp";
|
|
214
|
+
if (transitions.filter((t) => t.includes("DEBUG")).length > 2)
|
|
215
|
+
return "erratic";
|
|
216
|
+
return "cyclical";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Daily assessment string. */
|
|
220
|
+
export function _dailyAssessment(mode, yieldVal, qualityScoreVal, expectedYield) {
|
|
221
|
+
const qsPct = Math.round(qualityScoreVal * 100);
|
|
222
|
+
if (mode === "IDLE")
|
|
223
|
+
return "You're idle — no significant token activity today.";
|
|
224
|
+
if (mode === "MAINTAIN") {
|
|
225
|
+
if (qualityScoreVal >= 0.5)
|
|
226
|
+
return `You're in MAINTAIN mode. Yield ${yieldVal} is ${qsPct}% of MAINTAIN norm. The cascade is compounding.`;
|
|
227
|
+
return `You're in MAINTAIN mode but yield ${yieldVal} is only ${qsPct}% of expected (${expectedYield}). You may be coasting — push for more output.`;
|
|
228
|
+
}
|
|
229
|
+
if (mode === "BUILD") {
|
|
230
|
+
if (qualityScoreVal >= 0.5)
|
|
231
|
+
return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of BUILD norm. Greenfield work — expected to be low.`;
|
|
232
|
+
return `You're in BUILD mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Building is slow — keep going.`;
|
|
233
|
+
}
|
|
234
|
+
if (mode === "EDIT") {
|
|
235
|
+
if (qualityScoreVal >= 0.5)
|
|
236
|
+
return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of EDIT norm. Fresh input but producing — good.`;
|
|
237
|
+
return `You're in EDIT mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Push for more output per input.`;
|
|
238
|
+
}
|
|
239
|
+
if (mode === "DEBUG") {
|
|
240
|
+
if (qualityScoreVal >= 0.5)
|
|
241
|
+
return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of DEBUG norm. Investigating — yield is expected to be low.`;
|
|
242
|
+
return `You're in DEBUG mode. Yield ${yieldVal} is ${qsPct}% of expected (${expectedYield}). Debugging is slow — consider loading prior context.`;
|
|
243
|
+
}
|
|
244
|
+
return `Mode: ${mode}. Yield: ${yieldVal}. Quality: ${qsPct}%.`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Daily advice string. */
|
|
248
|
+
export function _dailyAdvice(mode, yieldVal, qs) {
|
|
249
|
+
if (mode === "MAINTAIN" && qs >= 0.5)
|
|
250
|
+
return "Keep the cascade going. Don't reset context — let it compound.";
|
|
251
|
+
if (mode === "MAINTAIN")
|
|
252
|
+
return "You're in MAINTAIN but underperforming. Push the agent for more output per turn.";
|
|
253
|
+
if (mode === "BUILD")
|
|
254
|
+
return "When you're done building, load prior context to transition to MAINTAIN. The cascade rewards reuse.";
|
|
255
|
+
if (mode === "EDIT")
|
|
256
|
+
return "You're producing but using fresh input. Try to reuse prior context to boost leverage.";
|
|
257
|
+
if (mode === "DEBUG")
|
|
258
|
+
return "When you're done debugging, load prior context to return to MAINTAIN. Don't let the debug phase drag on.";
|
|
259
|
+
if (mode === "IDLE")
|
|
260
|
+
return "No activity detected. Start a session to build your cascade.";
|
|
261
|
+
return "Keep working — the cascade will compound as you build context.";
|
|
262
|
+
}
|