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.
Files changed (73) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/MCP_SERVER_DESCRIPTION.md +141 -0
  3. package/README.md +8 -0
  4. package/adapters/index.mjs +865 -0
  5. package/adapters/tokenpull.mjs +576 -0
  6. package/adapters.mjs +1 -865
  7. package/analytics/cascade.mjs +274 -0
  8. package/cascade.mjs +1 -274
  9. package/cli.mjs +1 -1853
  10. package/connect.mjs +1 -13
  11. package/identity/connect.mjs +13 -0
  12. package/identity/keystore.mjs +242 -0
  13. package/identity/sign.mjs +106 -0
  14. package/index.mjs +43 -159
  15. package/keystore.mjs +1 -242
  16. package/narrate.mjs +1 -77
  17. package/package.json +32 -15
  18. package/preflight.mjs +1 -191
  19. package/presentation/cli.mjs +1874 -0
  20. package/presentation/narrate.mjs +77 -0
  21. package/presentation/tui.mjs +2668 -0
  22. package/prompts/check-my-efficiency.md +1 -0
  23. package/prompts/compare-with-leader.md +1 -0
  24. package/prompts/simulate-improvement.md +1 -0
  25. package/resources/class-tiers.md +23 -0
  26. package/resources/data-policy.md +27 -0
  27. package/resources/install-guide.md +59 -0
  28. package/resources/privacy-model.md +30 -0
  29. package/resources/scoring-formula.md +28 -0
  30. package/review.mjs +263 -0
  31. package/sign.mjs +1 -106
  32. package/submit/index.mjs +309 -0
  33. package/submit/preflight.mjs +191 -0
  34. package/submit.mjs +1 -303
  35. package/tokenpull.mjs +1 -576
  36. package/tokscale_analytics.mjs +875 -0
  37. package/tools/_framing.mjs +139 -0
  38. package/tools/_helpers.mjs +258 -0
  39. package/tools/_report.mjs +262 -0
  40. package/tools/_schemas.mjs +394 -0
  41. package/tools/_verifiers.mjs +148 -0
  42. package/tools/compare-operators.mjs +79 -0
  43. package/tools/compare-self.mjs +122 -0
  44. package/tools/describe-power-user.mjs +68 -0
  45. package/tools/diagnose-cascade.mjs +192 -0
  46. package/tools/enroll.mjs +121 -0
  47. package/tools/get-best-operator.mjs +58 -0
  48. package/tools/get-leaderboard.mjs +26 -0
  49. package/tools/get-operator.mjs +33 -0
  50. package/tools/index.mjs +246 -0
  51. package/tools/optimize-efficiency.mjs +160 -0
  52. package/tools/rank-paste.mjs +51 -0
  53. package/tools/rank-windows.mjs +120 -0
  54. package/tools/self-improve.mjs +423 -0
  55. package/tools/simulate-change.mjs +207 -0
  56. package/tools/submit-paste.mjs +118 -0
  57. package/tools/submit-verified.mjs +153 -0
  58. package/tools/suggest-improvements.mjs +168 -0
  59. package/tools/tokenpull-compare.mjs +123 -0
  60. package/tools/tokenpull-submit.mjs +234 -0
  61. package/tools/tokenpull.mjs +113 -0
  62. package/tools/tokscale-breakdown.mjs +159 -0
  63. package/tools/tokscale-competitive-intel.mjs +45 -0
  64. package/tools/tokscale-cost-analysis.mjs +31 -0
  65. package/tools/tokscale-developer-profile.mjs +36 -0
  66. package/tools/tokscale-device-profile.mjs +35 -0
  67. package/tools/tokscale-market-share.mjs +38 -0
  68. package/tools/tokscale-mcp-usage.mjs +33 -0
  69. package/tools/tokscale-model-trends.mjs +32 -0
  70. package/tools/watch-tokenpull.mjs +134 -0
  71. package/tools.mjs +1 -3897
  72. package/tui.mjs +4 -2650
  73. package/banner.svg +0 -7
@@ -0,0 +1,118 @@
1
+ /**
2
+ * tools/submit-paste.mjs — submit_paste tool.
3
+ */
4
+
5
+ import { cascade, parsePillars } from "../analytics/cascade.mjs";
6
+ import { narrate } from "../presentation/narrate.mjs";
7
+ import { SUBMIT_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
8
+ import {
9
+ MAX_INPUT,
10
+ uploadStamp,
11
+ withParseWarnings,
12
+ } from "./_helpers.mjs";
13
+
14
+ export const TOOL_DEF = {
15
+ name: "submit_paste",
16
+ description:
17
+ "Ranks a paste of token counts AND publishes it to the live SigRank board at signalaf.com in one call. First computes the cascade locally for an instant preview (yield, leverage, velocity, class, card), then submits the raw paste to the board's web-paste endpoint, which re-parses and re-scores it server-side. The server score is authoritative — it may differ from the local preview if the board applies additional validation. Returns both the local preview and the server response (including the operator's new rank if accepted). A codename is required to publish — omit it for a local preview only (no board submission). Token-only, no auth required. Use this when you have token counts from ccusage or a dashboard and want to both see your score and publish it. Do NOT use this if you want to pull your local usage automatically — use tokenpull_submit for the zero-paste flow. Do NOT use this for multi-window dashboard pastes — use rank_windows to rank them first, then submit each window. After calling this, use get_operator with your codename to verify your submission appeared on the board.",
18
+ annotations: {
19
+ title: "Submit paste to board",
20
+ ...ANNOTATIONS.destructiveHint,
21
+ ...ANNOTATIONS.idempotentHint,
22
+ ...ANNOTATIONS.openWorldHint,
23
+ },
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {
27
+ text: {
28
+ type: "string",
29
+ description:
30
+ 'Token counts to rank and submit. Two formats: (1) JSON {"input":N,"output":N,"cacheCreate":N,"cacheRead":N} from ccusage (preferred — the board parses this reliably), or (2) four whitespace-separated numbers: input output cacheCreate cacheRead. The 4-number form ranks locally but the board may reject it. Example: {"input":1000000,"output":500000,"cacheCreate":50000,"cacheRead":800000}',
31
+ },
32
+ codename: {
33
+ type: "string",
34
+ description:
35
+ 'Operator codename to publish under on the leaderboard (e.g. "Ghost Falcon"). Required to submit — omit for local preview only (no board submission, just returns the local cascade result). Must be a non-empty string.',
36
+ },
37
+ },
38
+ required: ["text"],
39
+ description:
40
+ "Requires token counts (text). Codename is optional but required for board submission — omit it for preview-only mode.",
41
+ },
42
+ outputSchema: SUBMIT_OUTPUT,
43
+ };
44
+
45
+ export async function handleSubmitPaste(args, ctx) {
46
+ if (!args?.text)
47
+ throw new Error("submit_paste requires a non-empty `text` argument.");
48
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
49
+ return {
50
+ status: "error",
51
+ reason: "input_too_large",
52
+ detail: `text exceeds ${MAX_INPUT} chars (${args.text.length}). Paste only the token-count table, not full output.`,
53
+ };
54
+ }
55
+ // Local preview first — also validates the paste is parseable before any POST.
56
+ const pillars = parsePillars(args.text);
57
+ const c = withParseWarnings(pillars, cascade(pillars));
58
+ const codename = String(args?.codename || "").trim();
59
+ const card = narrate(c, codename || "This operator");
60
+
61
+ // No codename → cannot publish (the board endpoint requires it). Fail fast at the
62
+ // tool boundary with a clear message instead of an opaque server 400.
63
+ if (!codename) {
64
+ return {
65
+ ...c,
66
+ card,
67
+ submission: {
68
+ status: "not_submitted",
69
+ reason: "codename_required",
70
+ detail:
71
+ "Pass a codename to publish to the board. Showing local preview only.",
72
+ },
73
+ };
74
+ }
75
+
76
+ // Submit the RAW paste so the server re-parses + re-scores authoritatively — the
77
+ // MCP's local cascade is only a preview; the board stays the single source of truth.
78
+ const stamp = uploadStamp({
79
+ codename,
80
+ pillars: c.pillars,
81
+ source: "web_paste",
82
+ });
83
+ const res = await ctx.doFetch(`${ctx.apiBase}/api/v1/ingest-paste`, {
84
+ method: "POST",
85
+ headers: {
86
+ "content-type": "application/json",
87
+ accept: "application/json",
88
+ },
89
+ body: JSON.stringify({
90
+ codename,
91
+ raw_paste: String(args?.text || ""),
92
+ consent_acknowledged: true,
93
+ terms_version: "2026-07-21",
94
+ privacy_version: "2026-07-21",
95
+ ...stamp,
96
+ }),
97
+ });
98
+ let ack;
99
+ try {
100
+ ack = await res.json();
101
+ } catch {
102
+ ack = {
103
+ status: "error",
104
+ detail: `HTTP ${res.status} (non-JSON response)`,
105
+ };
106
+ }
107
+ const ranked = !!(
108
+ res.ok &&
109
+ ack.verification_tier === "verified" &&
110
+ ack.persisted === true
111
+ );
112
+ return {
113
+ ...c,
114
+ card,
115
+ ranked,
116
+ submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
117
+ };
118
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * tools/submit-verified.mjs — submit_verified tool.
3
+ */
4
+
5
+ import { ALL_PLATFORMS } from "../adapters/index.mjs";
6
+ import { ensureIdentity } from "../identity/keystore.mjs";
7
+ import { submitSignedWindow } from "../submit/index.mjs";
8
+ import { SUBMIT_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
9
+ import { pullByPlatform, WINDOW_TYPE } from "./_helpers.mjs";
10
+ import { _computeReportBlock } from "./_report.mjs";
11
+
12
+ export const TOOL_DEF = {
13
+ name: "submit_verified",
14
+ description:
15
+ "Publish your LOCAL token runs to the SigRank board as a VERIFIED operator — the enrolled, signed path. Reads your pillars (tokenpull), builds the canonical Schema 1.0 snapshot per window, ed25519-signs it with your device key, and POSTs to /api/v1/snapshots. Requires `npx sigrank-mcp enroll` first (a bound device). Only signed submissions from a trusted device rank on the board. Token-only; the private key never leaves your machine. Pass dry_run:true to inspect the exact signed payload without publishing.",
16
+ annotations: {
17
+ title: "Submit verified score",
18
+ ...ANNOTATIONS.destructiveHint,
19
+ ...ANNOTATIONS.idempotentHint,
20
+ ...ANNOTATIONS.openWorldHint,
21
+ },
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {
25
+ window: {
26
+ type: "string",
27
+ enum: ["7d", "30d", "90d", "all"],
28
+ description: "submit only this window (default: all 4)",
29
+ },
30
+ platform: {
31
+ type: "string",
32
+ enum: [...ALL_PLATFORMS, "multi"],
33
+ description:
34
+ "source platform (default: claude). 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active); empty windows are skipped.",
35
+ },
36
+ dry_run: {
37
+ type: "boolean",
38
+ description:
39
+ "build + sign but do NOT publish — returns the exact payload that would be POSTed (token counts only), so you can inspect before submitting",
40
+ },
41
+ },
42
+ },
43
+ outputSchema: SUBMIT_OUTPUT,
44
+ };
45
+
46
+ export async function handleSubmitVerified(args, ctx) {
47
+ // The enrolled, signed publish path → /api/v1/snapshots (only verified rows rank).
48
+ const id = ctx.opts.identity || ensureIdentity();
49
+ if (!id.codename || !id.operator_id) {
50
+ return {
51
+ status: "not_enrolled",
52
+ detail: "Run `npx sigrank-mcp enroll` to bind this device first.",
53
+ };
54
+ }
55
+ const platform = args?.platform || "claude";
56
+
57
+ // MULTI: the combined cross-platform cascade. The dashboard already SUMS every
58
+ // active platform's pillars (a "claude+codex" row) but never submitted it — this
59
+ // is that missing submission. Aggregate every locally-detected platform's pillars
60
+ // per window and publish as platform='multi' = the operator's TOTAL usage. Empty
61
+ // windows are skipped so a no-activity window never lands as a degenerate row.
62
+ if (platform === "multi") {
63
+ const detected = [];
64
+ for (const p of ALL_PLATFORMS) {
65
+ const r = await pullByPlatform(p, ctx.opts).catch(() => null);
66
+ const live =
67
+ r &&
68
+ (r.windows || []).some(
69
+ (w) =>
70
+ w.pillars.input +
71
+ w.pillars.output +
72
+ w.pillars.cacheCreate +
73
+ w.pillars.cacheRead >
74
+ 0,
75
+ );
76
+ if (live) detected.push(r);
77
+ }
78
+ if (detected.length < 2) {
79
+ return {
80
+ platform: "multi",
81
+ codename: id.codename,
82
+ operator_id: id.operator_id,
83
+ status: "skipped",
84
+ reason: "need_2_platforms",
85
+ detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
86
+ windows: [],
87
+ };
88
+ }
89
+ const winKeys = args?.window
90
+ ? [args.window]
91
+ : ["7d", "30d", "90d", "all"];
92
+ const out = [];
93
+ for (const wk of winKeys) {
94
+ const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
95
+ let msgs = 0;
96
+ for (const d of detected) {
97
+ const w = (d.windows || []).find((x) => x.window === wk);
98
+ if (!w) continue;
99
+ sum.input += w.pillars.input || 0;
100
+ sum.output += w.pillars.output || 0;
101
+ sum.cacheCreate += w.pillars.cacheCreate || 0;
102
+ sum.cacheRead += w.pillars.cacheRead || 0;
103
+ msgs += w.messages || 0;
104
+ }
105
+ if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
106
+ continue; // skip empty window
107
+ const report = _computeReportBlock(sum);
108
+ const r = await submitSignedWindow(wk, sum, msgs, id, {
109
+ apiBase: ctx.apiBase,
110
+ fetchImpl: ctx.doFetch,
111
+ platform: "multi",
112
+ now: ctx.opts.now,
113
+ dryRun: !!args?.dry_run,
114
+ report,
115
+ });
116
+ out.push({ window: wk, pillars: sum, ...r });
117
+ }
118
+ return {
119
+ platform: "multi",
120
+ codename: id.codename,
121
+ operator_id: id.operator_id,
122
+ sources: detected.map((d) => d.platform),
123
+ windows: out,
124
+ };
125
+ }
126
+
127
+ const pulled = await pullByPlatform(platform, ctx.opts);
128
+ const targets = args?.window
129
+ ? pulled.windows.filter((w) => w.window === args.window)
130
+ : pulled.windows;
131
+ const out = [];
132
+ for (const w of targets) {
133
+ // Compute the cascade report block (mode + badges + health) for this window.
134
+ // Pure math, computed locally, submitted alongside the 4 token pillars.
135
+ const report = _computeReportBlock(w.pillars);
136
+ const r = await submitSignedWindow(w.window, w.pillars, w.messages, id, {
137
+ apiBase: ctx.apiBase,
138
+ fetchImpl: ctx.doFetch,
139
+ platform: pulled.platform,
140
+ now: ctx.opts.now,
141
+ dryRun: !!args?.dry_run,
142
+ report,
143
+ });
144
+ out.push({ window: w.window, pillars: w.pillars, ...r });
145
+ }
146
+ return {
147
+ platform: pulled.platform,
148
+ codename: id.codename,
149
+ operator_id: id.operator_id,
150
+ generatedAt: pulled.generatedAt,
151
+ windows: out,
152
+ };
153
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * tools/suggest-improvements.mjs — suggest_improvements tool.
3
+ */
4
+
5
+ import { cascade, parsePillars } from "../analytics/cascade.mjs";
6
+ import { SUGGEST_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
7
+ import { MAX_INPUT, withParseWarnings } from "./_helpers.mjs";
8
+
9
+ export const TOOL_DEF = {
10
+ name: "suggest_improvements",
11
+ description:
12
+ "Generates ranked, simulated improvement suggestions for your token cascade. Takes your 4 pillars, tests multiple improvement strategies (increase cache reads, reduce fresh input, increase output, optimize cache creation), simulates each with the canonical cascade engine, and returns them ranked by Υ yield impact. Each suggestion includes: the action, which pillar to change, how much to change it, the projected Υ after the change, the yield delta, the projected class tier, and a rationale. Also returns the single highest-impact change (best_single_change). Pure local math — no network, no submission. Use this after diagnose_cascade to get actionable next steps, then use simulate_change to fine-tune before committing. Accepts the same input formats as rank_paste.",
13
+ annotations: {
14
+ title: "Suggest improvements",
15
+ ...ANNOTATIONS.readOnlyHint,
16
+ ...ANNOTATIONS.idempotentHint,
17
+ ...ANNOTATIONS.openWorldHint,
18
+ },
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ text: {
23
+ type: "string",
24
+ description:
25
+ 'Token pillars — ccusage JSON or "input output cacheCreate cacheRead" (same format as rank_paste).',
26
+ },
27
+ },
28
+ required: ["text"],
29
+ },
30
+ outputSchema: SUGGEST_OUTPUT,
31
+ };
32
+
33
+ export async function handleSuggestImprovements(args) {
34
+ if (!args?.text)
35
+ throw new Error(
36
+ "suggest_improvements requires a non-empty `text` argument (token pillars).",
37
+ );
38
+ if (typeof args.text === "string" && args.text.length > MAX_INPUT) {
39
+ return {
40
+ status: "error",
41
+ reason: "input_too_large",
42
+ detail: `text exceeds ${MAX_INPUT} chars.`,
43
+ };
44
+ }
45
+
46
+ const p = parsePillars(args.text);
47
+ const current = withParseWarnings(p, cascade(p));
48
+ const { input: i, output: o, cacheCreate: cw, cacheRead: cr } = p;
49
+
50
+ // Generate candidate improvements, simulate each, rank by Υ impact
51
+ const candidates = [];
52
+
53
+ // Strategy 1: Increase cache reads (the #1 lever)
54
+ const crBoosts = cr > 0 ? [1.5, 2, 3, 5] : [];
55
+ for (const mult of crBoosts) {
56
+ const sim = cascade({ ...p, cacheRead: Math.round(cr * mult) });
57
+ if (sim.yield !== null) {
58
+ candidates.push({
59
+ action: `Increase cache reads by ${Math.round((mult - 1) * 100)}%`,
60
+ pillar: "cacheRead",
61
+ delta: `+${Math.round(cr * (mult - 1)).toLocaleString()}`,
62
+ simulated_yield: sim.yield,
63
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
64
+ class_after: sim.class,
65
+ rationale:
66
+ "Cache reads are the strongest Υ multiplier. More reuse = higher leverage = higher yield.",
67
+ });
68
+ }
69
+ }
70
+ // For zero-cache operators, suggest enabling caching with a starter amount
71
+ if (cr === 0 && cw === 0) {
72
+ const starterAmounts = [
73
+ Math.round(i * 10),
74
+ Math.round(i * 50),
75
+ Math.round(i * 100),
76
+ ];
77
+ for (const amt of starterAmounts) {
78
+ const sim = cascade({
79
+ ...p,
80
+ cacheCreate: Math.round(amt * 0.5),
81
+ cacheRead: amt,
82
+ });
83
+ if (sim.yield !== null && sim.yield > 0) {
84
+ candidates.push({
85
+ action: `Enable caching with ${amt.toLocaleString()} cache reads`,
86
+ pillar: "cacheRead",
87
+ delta: `+${amt.toLocaleString()}`,
88
+ simulated_yield: sim.yield,
89
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
90
+ class_after: sim.class,
91
+ rationale:
92
+ "You have no cache — enabling it unlocks the cascade. Start by reusing prior context.",
93
+ });
94
+ }
95
+ }
96
+ }
97
+
98
+ // Strategy 2: Reduce fresh input (Υ = Cr·O/I² — input is squared in the denominator)
99
+ const inputReductions = [0.9, 0.75, 0.5];
100
+ for (const mult of inputReductions) {
101
+ const newInput = Math.round(i * mult);
102
+ if (newInput < 1) continue;
103
+ const sim = cascade({ ...p, input: newInput });
104
+ if (sim.yield !== null) {
105
+ candidates.push({
106
+ action: `Reduce fresh input by ${Math.round((1 - mult) * 100)}%`,
107
+ pillar: "input",
108
+ delta: `-${Math.round(i * (1 - mult)).toLocaleString()}`,
109
+ simulated_yield: sim.yield,
110
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
111
+ class_after: sim.class,
112
+ rationale:
113
+ "Input is squared in the Υ denominator (Υ = Cr·O/I²). Reducing input has a quadratic payoff.",
114
+ });
115
+ }
116
+ }
117
+
118
+ // Strategy 3: Increase output
119
+ const outputBoosts = [1.25, 1.5, 2];
120
+ for (const mult of outputBoosts) {
121
+ const sim = cascade({ ...p, output: Math.round(o * mult) });
122
+ if (sim.yield !== null) {
123
+ candidates.push({
124
+ action: `Increase output by ${Math.round((mult - 1) * 100)}%`,
125
+ pillar: "output",
126
+ delta: `+${Math.round(o * (mult - 1)).toLocaleString()}`,
127
+ simulated_yield: sim.yield,
128
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
129
+ class_after: sim.class,
130
+ rationale:
131
+ "Output is a linear multiplier in Υ. More output per session = higher yield.",
132
+ });
133
+ }
134
+ }
135
+
136
+ // Strategy 4: Optimize cache creation (reduce if over-committing)
137
+ if (cw > o * 10) {
138
+ const sim = cascade({ ...p, cacheCreate: Math.round(o * 5) });
139
+ if (sim.yield !== null) {
140
+ candidates.push({
141
+ action: "Reduce cache creation to 5× output",
142
+ pillar: "cacheCreate",
143
+ delta: `-${Math.round(cw - o * 5).toLocaleString()}`,
144
+ simulated_yield: sim.yield,
145
+ yield_delta: Number((sim.yield - (current.yield ?? 0)).toFixed(2)),
146
+ class_after: sim.class,
147
+ rationale:
148
+ "You're over-committing cache (cw >> output). Trimming to a healthy ratio reduces cost without hurting yield.",
149
+ });
150
+ }
151
+ }
152
+
153
+ // Sort by yield_delta descending, take top 8
154
+ candidates.sort((a, b) => b.yield_delta - a.yield_delta);
155
+ const top = candidates
156
+ .slice(0, 8)
157
+ .map((c, idx) => ({ rank: idx + 1, ...c }));
158
+
159
+ const best = top[0];
160
+ return {
161
+ suggestions: top,
162
+ current_yield: current.yield,
163
+ current_class: current.class,
164
+ best_single_change: best
165
+ ? `${best.action} (Υ ${current.yield} → ${best.simulated_yield}, +${best.yield_delta} yield, class ${best.class_after})`
166
+ : "No improvements found — your cascade is already optimized.",
167
+ };
168
+ }
@@ -0,0 +1,123 @@
1
+ /**
2
+ * tools/tokenpull-compare.mjs — tokenpull_compare tool.
3
+ */
4
+
5
+ import { cascade } from "../analytics/cascade.mjs";
6
+ import { ALL_PLATFORMS } from "../adapters/index.mjs";
7
+ import { COMPARE_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
8
+ import { pullByPlatform } from "./_helpers.mjs";
9
+ import { _ccusagePillars, _tokenDashPillars, _tokscalePillars } from "./_verifiers.mjs";
10
+
11
+ export const TOOL_DEF = {
12
+ name: "tokenpull_compare",
13
+ description:
14
+ "Pull token usage from ALL four local sources in parallel — tokenpull (JSONL canon), ccusage CLI, token-dashboard SQLite, and tokscale report — and return them side-by-side with delta % vs tokenpull as the baseline. Also computes the cascade (Υ, SNR, Leverage, class) for each source so you can see how each verifier scores. Useful for validating your numbers before submitting, or understanding discrepancies between tools. Claude only for token-dash; codex and others use tokenpull + ccusage + tokscale. Token-only, on-device.",
15
+ annotations: { title: "Compare token sources", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {
19
+ platform: {
20
+ type: "string",
21
+ enum: ALL_PLATFORMS,
22
+ description:
23
+ "platform to compare (default: claude). token-dash and App only available for claude.",
24
+ },
25
+ },
26
+ },
27
+ outputSchema: COMPARE_OUTPUT,
28
+ };
29
+
30
+ export async function handleTokenpullCompare(args, ctx) {
31
+ const platform = args?.platform || "claude";
32
+ const WINS = ["7d", "30d", "90d", "all"];
33
+
34
+ // Pull all four sources in parallel (verifiers are now async via execFile)
35
+ const [tpResult, ccPillars, tdPillars, tsPillars] = await Promise.all([
36
+ pullByPlatform(platform, ctx.opts).catch(() => null),
37
+ _ccusagePillars(platform).catch(() => null),
38
+ (platform === "claude"
39
+ ? _tokenDashPillars()
40
+ : Promise.resolve(null)
41
+ ).catch(() => null),
42
+ _tokscalePillars(platform).catch(() => null),
43
+ ]);
44
+
45
+ // Build tokenpull window lookup
46
+ const tpByWin = {};
47
+ for (const w of tpResult?.windows ?? []) tpByWin[w.window] = w.pillars;
48
+
49
+ // Helper: delta % vs tokenpull baseline
50
+ const delta = (val, base) =>
51
+ base > 0 ? +(((val - base) / base) * 100).toFixed(1) : null;
52
+
53
+ // Build per-source per-window comparison
54
+ const SOURCES = [
55
+ {
56
+ source: "tokenpull",
57
+ note: "JSONL deduped by msg id — canon source",
58
+ byWin: tpByWin,
59
+ },
60
+ {
61
+ source: "ccusage",
62
+ note: "ccusage CLI — monthly only",
63
+ byWin: ccPillars ?? {},
64
+ },
65
+ {
66
+ source: "token-dash",
67
+ note: "token-dashboard SQLite — all-time only",
68
+ byWin: tdPillars ?? {},
69
+ },
70
+ {
71
+ source: "tokscale",
72
+ note: "tokscale_report.json — all-time only",
73
+ byWin: tsPillars ?? {},
74
+ },
75
+ ];
76
+
77
+ const comparison = {};
78
+ for (const win of WINS) {
79
+ const baseP = tpByWin[win];
80
+ comparison[win] = SOURCES.filter((s) => s.byWin[win] != null).map((s) => {
81
+ const p = s.byWin[win];
82
+ const cas = cascade(p);
83
+ const entry = {
84
+ source: s.source,
85
+ note: s.note,
86
+ pillars: p,
87
+ cascade: {
88
+ yield: cas.yield,
89
+ snr: cas.snr,
90
+ leverage: cas.leverage,
91
+ velocity: cas.velocity,
92
+ dev10x: cas.dev10x,
93
+ class: cas.class,
94
+ },
95
+ };
96
+ if (s.source !== "tokenpull" && baseP) {
97
+ entry.delta_vs_tokenpull = {
98
+ input: delta(p.input, baseP.input),
99
+ output: delta(p.output, baseP.output),
100
+ cacheCreate: delta(p.cacheCreate, baseP.cacheCreate),
101
+ cacheRead: delta(p.cacheRead, baseP.cacheRead),
102
+ };
103
+ }
104
+ return entry;
105
+ });
106
+ }
107
+
108
+ // Sources available summary
109
+ const available = SOURCES.filter(
110
+ (s) => Object.keys(s.byWin).length > 0,
111
+ ).map((s) => s.source);
112
+
113
+ return {
114
+ platform,
115
+ estimated: tpResult?.estimated ?? false,
116
+ generatedAt: tpResult?.generatedAt ?? new Date().toISOString(),
117
+ sources_available: available,
118
+ sources_missing: SOURCES.map((s) => s.source).filter(
119
+ (s) => !available.includes(s),
120
+ ),
121
+ comparison,
122
+ };
123
+ }