sigrank 0.18.5 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +158 -0
- package/MCP_SERVER_DESCRIPTION.md +172 -0
- package/README.md +94 -41
- package/adapters/index.mjs +1100 -0
- package/adapters/tokenpull.mjs +568 -0
- package/adapters.mjs +1 -857
- package/analytics/cascade.mjs +338 -0
- package/cascade.mjs +1 -274
- package/cli.mjs +1 -1853
- package/connect.mjs +1 -13
- package/identity/connect.mjs +13 -0
- package/identity/keystore.mjs +231 -0
- package/identity/sign.mjs +106 -0
- package/index.mjs +47 -162
- package/keystore.mjs +1 -242
- package/lib/constants.mjs +137 -0
- package/lib/pkg-version.mjs +59 -0
- package/manifest.json +46 -0
- package/narrate.mjs +1 -77
- package/package.json +41 -16
- package/preflight.mjs +1 -191
- package/presentation/cli.mjs +1926 -0
- package/presentation/narrate.mjs +219 -0
- package/presentation/tui-themes.mjs +189 -0
- package/presentation/tui.mjs +3316 -0
- package/prompts/check-my-efficiency.md +1 -0
- package/prompts/compare-with-leader.md +1 -0
- package/prompts/simulate-improvement.md +1 -0
- package/proxy.mjs +492 -0
- package/resources/class-tiers.md +81 -0
- package/resources/data-policy.md +31 -0
- package/resources/install-guide.md +59 -0
- package/resources/privacy-model.md +44 -0
- package/resources/scoring-formula.md +24 -0
- package/review.mjs +263 -0
- package/sign.mjs +1 -106
- package/submit/index.mjs +297 -0
- package/submit/preflight.mjs +191 -0
- package/submit.mjs +1 -303
- package/tokenpull.mjs +1 -576
- package/tokscale_analytics.mjs +864 -0
- package/tools/_framing.mjs +217 -0
- package/tools/_helpers.mjs +253 -0
- package/tools/_peer-matching.mjs +361 -0
- package/tools/_report.mjs +262 -0
- package/tools/_schemas.mjs +492 -0
- package/tools/_verifiers.mjs +148 -0
- package/tools/compare-operators.mjs +80 -0
- package/tools/compare-self.mjs +124 -0
- package/tools/describe-power-user.mjs +78 -0
- package/tools/diagnose-cascade.mjs +192 -0
- package/tools/discover-peers.mjs +198 -0
- package/tools/enroll.mjs +119 -0
- package/tools/get-best-operator.mjs +59 -0
- package/tools/get-leaderboard.mjs +27 -0
- package/tools/get-operator.mjs +33 -0
- package/tools/index.mjs +229 -0
- package/tools/optimize-efficiency.mjs +170 -0
- package/tools/rank-paste.mjs +51 -0
- package/tools/rank-windows.mjs +120 -0
- package/tools/self-improve.mjs +423 -0
- package/tools/simulate-change.mjs +207 -0
- package/tools/submit-paste.mjs +121 -0
- package/tools/submit-verified.mjs +153 -0
- package/tools/suggest-improvements.mjs +168 -0
- package/tools/tokenpull-compare.mjs +123 -0
- package/tools/tokenpull-submit.mjs +235 -0
- package/tools/tokenpull.mjs +113 -0
- package/tools/tokscale-breakdown.mjs +119 -0
- package/tools/tokscale-competitive-intel.mjs +45 -0
- package/tools/tokscale-cost-analysis.mjs +31 -0
- package/tools/tokscale-developer-profile.mjs +36 -0
- package/tools/tokscale-device-profile.mjs +35 -0
- package/tools/tokscale-market-share.mjs +38 -0
- package/tools/tokscale-mcp-usage.mjs +33 -0
- package/tools/tokscale-model-trends.mjs +32 -0
- package/tools/watch-tokenpull.mjs +134 -0
- package/tools.mjs +1 -3897
- package/tui.mjs +4 -2650
- package/banner.svg +0 -7
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/tokenpull-submit.mjs — tokenpull_submit tool.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cascade } from "../analytics/cascade.mjs";
|
|
6
|
+
import { narrate } from "../presentation/narrate.mjs";
|
|
7
|
+
import { ALL_PLATFORMS } from "../adapters/index.mjs";
|
|
8
|
+
import { SUBMIT_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
|
|
9
|
+
import {
|
|
10
|
+
MAX_INPUT,
|
|
11
|
+
pullByPlatform,
|
|
12
|
+
uploadStamp,
|
|
13
|
+
WINDOW_TYPE,
|
|
14
|
+
} from "./_helpers.mjs";
|
|
15
|
+
import { TERMS_VERSION, PRIVACY_VERSION, isRankedAck } from "../lib/constants.mjs";
|
|
16
|
+
|
|
17
|
+
export const TOOL_DEF = {
|
|
18
|
+
name: "tokenpull_submit",
|
|
19
|
+
description:
|
|
20
|
+
"Pull your LOCAL token usage from session logs AND publish it to the SigRank board in one call — the zero-paste flow. Reads the four canonical pillars (input, output, cacheCreate, cacheRead) per window from your local logs, computes the cascade, and submits each window to the board where it is re-scored server-side and tagged with the source platform. Requires a codename to publish; omit for a local preview only. Token-only — no prompt content is read or transmitted.",
|
|
21
|
+
annotations: {
|
|
22
|
+
title: "Pull and submit tokens",
|
|
23
|
+
...ANNOTATIONS.destructiveHint,
|
|
24
|
+
...ANNOTATIONS.idempotentHint,
|
|
25
|
+
...ANNOTATIONS.openWorldHint,
|
|
26
|
+
},
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
codename: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description:
|
|
33
|
+
'Operator codename to publish under on the leaderboard (e.g. "Iron Lotus"). Required to submit — omit for local preview only.',
|
|
34
|
+
},
|
|
35
|
+
window: {
|
|
36
|
+
type: "string",
|
|
37
|
+
enum: ["7d", "30d", "90d", "all"],
|
|
38
|
+
description:
|
|
39
|
+
'Submit only this time window (default: all 4 windows). Use "7d" for recent activity or "all" for all-time ranking.',
|
|
40
|
+
},
|
|
41
|
+
platform: {
|
|
42
|
+
type: "string",
|
|
43
|
+
enum: [...ALL_PLATFORMS, "multi"],
|
|
44
|
+
description: `Source platform to pull from (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}, multi. 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active). 'devin' reads from ~/.local/share/devin/cli/sessions.db (SQLite, all windows). 'other' reads from a user-supplied JSON file (set SIGRANK_OTHER_PATH). Each platform reads its own session logs locally.`,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
outputSchema: SUBMIT_OUTPUT,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export async function handleTokenpullSubmit(args, ctx) {
|
|
52
|
+
// Pull local usage, then publish each window's CANONICAL pillars to the board
|
|
53
|
+
// (server re-scores). The board stays platform-agnostic via the 4 pillars; the
|
|
54
|
+
// source platform rides along as a tag. Conversion already happened in the adapter.
|
|
55
|
+
const codename = String(args?.codename || "").trim();
|
|
56
|
+
const platform = args?.platform || "claude";
|
|
57
|
+
|
|
58
|
+
// MULTI: same combined cross-platform cascade as submit_verified. Includes Devin
|
|
59
|
+
// (cloud, via tokscale). Aggregate every locally-detected platform's pillars per
|
|
60
|
+
// window and publish as platform='multi'.
|
|
61
|
+
if (platform === "multi") {
|
|
62
|
+
const detected = [];
|
|
63
|
+
for (const p of ALL_PLATFORMS) {
|
|
64
|
+
const r = await pullByPlatform(p, ctx.opts).catch(() => null);
|
|
65
|
+
const live =
|
|
66
|
+
r &&
|
|
67
|
+
(r.windows || []).some(
|
|
68
|
+
(w) =>
|
|
69
|
+
w.pillars.input +
|
|
70
|
+
w.pillars.output +
|
|
71
|
+
w.pillars.cacheCreate +
|
|
72
|
+
w.pillars.cacheRead >
|
|
73
|
+
0,
|
|
74
|
+
);
|
|
75
|
+
if (live) detected.push(r);
|
|
76
|
+
}
|
|
77
|
+
if (detected.length < 2) {
|
|
78
|
+
return {
|
|
79
|
+
platform: "multi",
|
|
80
|
+
codename: codename || undefined,
|
|
81
|
+
status: "skipped",
|
|
82
|
+
reason: "need_2_platforms",
|
|
83
|
+
detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
|
|
84
|
+
windows: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const winKeys = args?.window ? [args.window] : ["7d", "30d", "90d", "all"];
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const wk of winKeys) {
|
|
90
|
+
const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
|
|
91
|
+
let msgs = 0;
|
|
92
|
+
for (const d of detected) {
|
|
93
|
+
const w = (d.windows || []).find((x) => x.window === wk);
|
|
94
|
+
if (!w) continue;
|
|
95
|
+
sum.input += w.pillars.input || 0;
|
|
96
|
+
sum.output += w.pillars.output || 0;
|
|
97
|
+
sum.cacheCreate += w.pillars.cacheCreate || 0;
|
|
98
|
+
sum.cacheRead += w.pillars.cacheRead || 0;
|
|
99
|
+
msgs += w.messages || 0;
|
|
100
|
+
}
|
|
101
|
+
if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
|
|
102
|
+
continue;
|
|
103
|
+
const c = cascade(sum);
|
|
104
|
+
const card = narrate(c, `${wk} multi`);
|
|
105
|
+
if (!codename) {
|
|
106
|
+
out.push({
|
|
107
|
+
window: wk,
|
|
108
|
+
pillars: sum,
|
|
109
|
+
cascade: c,
|
|
110
|
+
card,
|
|
111
|
+
submission: { status: "not_submitted", reason: "codename_required" },
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const rawPaste = `${sum.input} ${sum.output} ${sum.cacheCreate} ${sum.cacheRead}`;
|
|
116
|
+
const windowType = WINDOW_TYPE[wk] || wk;
|
|
117
|
+
const stamp = uploadStamp({
|
|
118
|
+
codename,
|
|
119
|
+
window: windowType,
|
|
120
|
+
pillars: sum,
|
|
121
|
+
platform: "multi",
|
|
122
|
+
});
|
|
123
|
+
const res = await ctx.doFetch(`${ctx.apiBase}/api/v1/ingest-paste`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"content-type": "application/json",
|
|
127
|
+
accept: "application/json",
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({
|
|
130
|
+
codename,
|
|
131
|
+
raw_paste: rawPaste,
|
|
132
|
+
window_type: windowType,
|
|
133
|
+
telemetry: { platform: { primary: "multi" } },
|
|
134
|
+
consent_acknowledged: true,
|
|
135
|
+
terms_version: TERMS_VERSION,
|
|
136
|
+
privacy_version: PRIVACY_VERSION,
|
|
137
|
+
...stamp,
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
let ack;
|
|
141
|
+
try {
|
|
142
|
+
ack = await res.json();
|
|
143
|
+
} catch {
|
|
144
|
+
ack = { status: "parse_error", httpStatus: res.status };
|
|
145
|
+
}
|
|
146
|
+
// Fix 4: was `ack?.ranked ?? ack?.accepted ?? false` — a looser predicate
|
|
147
|
+
// than the single-platform branch below, which would mark a non-persisted
|
|
148
|
+
// 202 as ranked. Now both branches use the shared isRankedAck() so every
|
|
149
|
+
// submit path agrees on what "ranked" means.
|
|
150
|
+
const ranked = isRankedAck(res, ack);
|
|
151
|
+
out.push({
|
|
152
|
+
window: wk,
|
|
153
|
+
pillars: sum,
|
|
154
|
+
cascade: c,
|
|
155
|
+
card,
|
|
156
|
+
submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
platform: "multi",
|
|
161
|
+
codename: codename || undefined,
|
|
162
|
+
sources: detected.map((d) => d.platform),
|
|
163
|
+
windows: out,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const pulled = await pullByPlatform(platform, ctx.opts);
|
|
168
|
+
const targets = args?.window
|
|
169
|
+
? pulled.windows.filter((w) => w.window === args.window)
|
|
170
|
+
: pulled.windows;
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const w of targets) {
|
|
173
|
+
const c = cascade(w.pillars);
|
|
174
|
+
const card = narrate(c, `${w.window} window`);
|
|
175
|
+
if (!codename) {
|
|
176
|
+
out.push({
|
|
177
|
+
window: w.window,
|
|
178
|
+
pillars: w.pillars,
|
|
179
|
+
cascade: c,
|
|
180
|
+
card,
|
|
181
|
+
submission: { status: "not_submitted", reason: "codename_required" },
|
|
182
|
+
});
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// canonical pillars → "input output cacheCreate cacheRead" (the parser's 4-bare-number form)
|
|
186
|
+
const rawPaste = `${w.pillars.input} ${w.pillars.output} ${w.pillars.cacheCreate} ${w.pillars.cacheRead}`;
|
|
187
|
+
const windowType = WINDOW_TYPE[w.window] || w.window;
|
|
188
|
+
const stamp = uploadStamp({
|
|
189
|
+
codename,
|
|
190
|
+
window: windowType,
|
|
191
|
+
pillars: w.pillars,
|
|
192
|
+
platform: pulled.platform,
|
|
193
|
+
});
|
|
194
|
+
const res = await ctx.doFetch(`${ctx.apiBase}/api/v1/ingest-paste`, {
|
|
195
|
+
method: "POST",
|
|
196
|
+
headers: {
|
|
197
|
+
"content-type": "application/json",
|
|
198
|
+
accept: "application/json",
|
|
199
|
+
},
|
|
200
|
+
body: JSON.stringify({
|
|
201
|
+
codename,
|
|
202
|
+
raw_paste: rawPaste,
|
|
203
|
+
window_type: windowType,
|
|
204
|
+
telemetry: { platform: { primary: pulled.platform } },
|
|
205
|
+
consent_acknowledged: true,
|
|
206
|
+
terms_version: TERMS_VERSION,
|
|
207
|
+
privacy_version: PRIVACY_VERSION,
|
|
208
|
+
...stamp,
|
|
209
|
+
}),
|
|
210
|
+
});
|
|
211
|
+
let ack;
|
|
212
|
+
try {
|
|
213
|
+
ack = await res.json();
|
|
214
|
+
} catch {
|
|
215
|
+
ack = { status: "error", detail: `HTTP ${res.status} (non-JSON)` };
|
|
216
|
+
}
|
|
217
|
+
// ranked = actually on the board (verified + persisted), not just received — parity
|
|
218
|
+
// with submit_verified (submit/index.mjs). An unenrolled/revoked device gets 202 but is NEVER ranked.
|
|
219
|
+
const ranked = isRankedAck(res, ack);
|
|
220
|
+
out.push({
|
|
221
|
+
window: w.window,
|
|
222
|
+
pillars: w.pillars,
|
|
223
|
+
cascade: c,
|
|
224
|
+
card,
|
|
225
|
+
ranked,
|
|
226
|
+
submission: { ...stamp, httpStatus: res.status, ranked, ...ack },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
platform: pulled.platform,
|
|
231
|
+
codename: codename || null,
|
|
232
|
+
generatedAt: pulled.generatedAt,
|
|
233
|
+
windows: out,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/tokenpull.mjs — tokenpull tool.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { cascade } from "../analytics/cascade.mjs";
|
|
6
|
+
import { narrate } from "../presentation/narrate.mjs";
|
|
7
|
+
import { ALL_PLATFORMS } from "../adapters/index.mjs";
|
|
8
|
+
import { TOKENPULL_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
|
|
9
|
+
import { pullByPlatform } from "./_helpers.mjs";
|
|
10
|
+
|
|
11
|
+
export const TOOL_DEF = {
|
|
12
|
+
name: "tokenpull",
|
|
13
|
+
description:
|
|
14
|
+
"Pull your LOCAL token usage from the platform's session logs and rank it across the four windows (7d/30d/90d/all-time) with the cascade — zero paste. Token-only: reads usage counts not message content. The numbers stay on your machine unless you submit them. Some platforms may have partial data (estimated=true when cacheCreate isn't available) or a dataGap note when the log format doesn't expose raw token counts.",
|
|
15
|
+
annotations: { title: "Pull local token usage", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
platform: {
|
|
20
|
+
type: "string",
|
|
21
|
+
enum: [...ALL_PLATFORMS, "multi"],
|
|
22
|
+
description: `source platform (default: claude). Supported: ${ALL_PLATFORMS.join(", ")}, multi. 'multi' = combined cascade summed across all locally-detected platforms (needs 2+ active). 'devin' reads from ~/.local/share/devin/cli/sessions.db (SQLite, all windows). 'codex' is estimated via io_ratio. 'other' reads from a user-supplied JSON file (set SIGRANK_OTHER_PATH). Some platforms need setup (e.g. copilot requires COPILOT_OTEL_ENABLED=true).`,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
outputSchema: TOKENPULL_OUTPUT,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function handleTokenpull(args, ctx) {
|
|
30
|
+
// Local read → 4 windows of pillars → cascade each. Token-only, on-device.
|
|
31
|
+
const platform = args?.platform || "claude";
|
|
32
|
+
|
|
33
|
+
// MULTI: sum every locally-detected platform's pillars per window. Same logic as
|
|
34
|
+
// submit_verified's multi flow — Devin (cloud, via tokscale) is included.
|
|
35
|
+
if (platform === "multi") {
|
|
36
|
+
const detected = [];
|
|
37
|
+
for (const p of ALL_PLATFORMS) {
|
|
38
|
+
const r = await pullByPlatform(p, ctx.opts).catch(() => null);
|
|
39
|
+
const live =
|
|
40
|
+
r &&
|
|
41
|
+
(r.windows || []).some(
|
|
42
|
+
(w) =>
|
|
43
|
+
w.pillars.input +
|
|
44
|
+
w.pillars.output +
|
|
45
|
+
w.pillars.cacheCreate +
|
|
46
|
+
w.pillars.cacheRead >
|
|
47
|
+
0,
|
|
48
|
+
);
|
|
49
|
+
if (live) detected.push(r);
|
|
50
|
+
}
|
|
51
|
+
if (detected.length < 2) {
|
|
52
|
+
return {
|
|
53
|
+
platform: "multi",
|
|
54
|
+
status: "skipped",
|
|
55
|
+
reason: "need_2_platforms",
|
|
56
|
+
detail: `multi needs 2+ active platforms; found ${detected.length} (${detected.map((d) => d.platform).join(", ") || "none"}).`,
|
|
57
|
+
windows: [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const winKeys = ["7d", "30d", "90d", "all"];
|
|
61
|
+
const windows = [];
|
|
62
|
+
for (const wk of winKeys) {
|
|
63
|
+
const sum = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
|
|
64
|
+
let msgs = 0;
|
|
65
|
+
for (const d of detected) {
|
|
66
|
+
const w = (d.windows || []).find((x) => x.window === wk);
|
|
67
|
+
if (!w) continue;
|
|
68
|
+
sum.input += w.pillars.input || 0;
|
|
69
|
+
sum.output += w.pillars.output || 0;
|
|
70
|
+
sum.cacheCreate += w.pillars.cacheCreate || 0;
|
|
71
|
+
sum.cacheRead += w.pillars.cacheRead || 0;
|
|
72
|
+
msgs += w.messages || 0;
|
|
73
|
+
}
|
|
74
|
+
if (sum.input + sum.output + sum.cacheCreate + sum.cacheRead <= 0)
|
|
75
|
+
continue;
|
|
76
|
+
const c = cascade(sum);
|
|
77
|
+
windows.push({
|
|
78
|
+
window: wk,
|
|
79
|
+
messages: msgs,
|
|
80
|
+
pillars: sum,
|
|
81
|
+
cascade: c,
|
|
82
|
+
card: narrate(c, `${wk} multi`),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
platform: "multi",
|
|
87
|
+
estimated: true,
|
|
88
|
+
sources: detected.map((d) => d.platform),
|
|
89
|
+
windows,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const pulled = await pullByPlatform(platform, ctx.opts);
|
|
94
|
+
const windows = pulled.windows.map((w) => {
|
|
95
|
+
const c = cascade(w.pillars);
|
|
96
|
+
return {
|
|
97
|
+
window: w.window,
|
|
98
|
+
messages: w.messages,
|
|
99
|
+
pillars: w.pillars,
|
|
100
|
+
cascade: c,
|
|
101
|
+
card: narrate(c, `${w.window} ${platform}`),
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
platform: pulled.platform,
|
|
106
|
+
estimated: pulled.estimated || false,
|
|
107
|
+
...(pulled.ioRatio ? { ioRatio: pulled.ioRatio } : {}),
|
|
108
|
+
generatedAt: pulled.generatedAt,
|
|
109
|
+
files: pulled.files,
|
|
110
|
+
totalMessages: pulled.totalMessages,
|
|
111
|
+
windows,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/tokscale-breakdown.mjs — tokscale_breakdown tool.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execFileAsync } from "./_helpers.mjs";
|
|
6
|
+
import { ANNOTATIONS } from "./_schemas.mjs";
|
|
7
|
+
import { TOKSCALE_CLIENT_MAP } from "../lib/constants.mjs";
|
|
8
|
+
|
|
9
|
+
export const TOOL_DEF = {
|
|
10
|
+
name: "tokscale_breakdown",
|
|
11
|
+
description:
|
|
12
|
+
"Show a per-model breakdown of your token usage across all platforms detected by tokscale. Models under the threshold (default 1%) are lumped into 'other' to keep the display clean. Useful for seeing which models you actually use per platform (e.g. claude-opus-4-8 76%, claude-sonnet-4-6 11%, other 0.3%). Returns { platform: [{ model, input, output, cacheRead, cacheCreate, pct }] }.",
|
|
13
|
+
annotations: {
|
|
14
|
+
title: "Model breakdown",
|
|
15
|
+
...ANNOTATIONS.readOnlyHint,
|
|
16
|
+
...ANNOTATIONS.openWorldHint,
|
|
17
|
+
},
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
threshold: {
|
|
22
|
+
type: "number",
|
|
23
|
+
description:
|
|
24
|
+
"Models below this fraction of their platform's total tokens are lumped into 'other'. Default 0.01 (1%). Set 0 to see every model.",
|
|
25
|
+
default: 0.01,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
outputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
platforms: {
|
|
33
|
+
type: "object",
|
|
34
|
+
description: "Map of platform name → array of { model, input, output, cacheRead, cacheCreate, pct }",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// TOKSCALE_CLIENT_MAP is imported from lib/constants.mjs (single source of truth).
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run `tokscale models --json` and return a model-level breakdown per platform.
|
|
44
|
+
* Models under `threshold` (default 1%) of their platform's total are lumped
|
|
45
|
+
* into "other" to keep the display clean.
|
|
46
|
+
*
|
|
47
|
+
* Returns: { platform: [{ model, input, output, cacheRead, cacheCreate, pct }] }
|
|
48
|
+
*/
|
|
49
|
+
export async function tokscaleModelBreakdown(threshold = 0.01) {
|
|
50
|
+
const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
|
|
51
|
+
const data = JSON.parse(raw);
|
|
52
|
+
const entries = Array.isArray(data?.entries)
|
|
53
|
+
? data.entries
|
|
54
|
+
: Array.isArray(data)
|
|
55
|
+
? data
|
|
56
|
+
: [];
|
|
57
|
+
if (!entries.length) return {};
|
|
58
|
+
|
|
59
|
+
// Group entries by our platform name
|
|
60
|
+
const byPlatform = {};
|
|
61
|
+
for (const e of entries) {
|
|
62
|
+
if (!e || !e.client) continue;
|
|
63
|
+
if (e.model === "<synthetic>" || e.model === "unknown") continue;
|
|
64
|
+
const input = Number(e.input) || 0;
|
|
65
|
+
const output = Number(e.output) || 0;
|
|
66
|
+
if (input + output === 0) continue;
|
|
67
|
+
const platform = TOKSCALE_CLIENT_MAP[e.client];
|
|
68
|
+
if (platform === null) continue;
|
|
69
|
+
const p = platform || "other";
|
|
70
|
+
if (!byPlatform[p]) byPlatform[p] = [];
|
|
71
|
+
byPlatform[p].push({
|
|
72
|
+
model: e.model,
|
|
73
|
+
input,
|
|
74
|
+
output,
|
|
75
|
+
cacheRead: Number(e.cacheRead) || 0,
|
|
76
|
+
cacheCreate: Number(e.cacheWrite) || 0,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// For each platform: sort by total tokens desc, lump small models into "other"
|
|
81
|
+
const result = {};
|
|
82
|
+
for (const [platform, models] of Object.entries(byPlatform)) {
|
|
83
|
+
const total = models.reduce(
|
|
84
|
+
(s, m) => s + m.input + m.output + m.cacheRead + m.cacheCreate,
|
|
85
|
+
0,
|
|
86
|
+
);
|
|
87
|
+
if (total === 0) continue;
|
|
88
|
+
const sorted = models.sort(
|
|
89
|
+
(a, b) =>
|
|
90
|
+
b.input + b.output + b.cacheRead + b.cacheCreate -
|
|
91
|
+
(a.input + a.output + a.cacheRead + a.cacheCreate),
|
|
92
|
+
);
|
|
93
|
+
const big = [];
|
|
94
|
+
const small = { model: "other", input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
95
|
+
for (const m of sorted) {
|
|
96
|
+
const t = m.input + m.output + m.cacheRead + m.cacheCreate;
|
|
97
|
+
if (t / total < threshold) {
|
|
98
|
+
small.input += m.input;
|
|
99
|
+
small.output += m.output;
|
|
100
|
+
small.cacheRead += m.cacheRead;
|
|
101
|
+
small.cacheCreate += m.cacheCreate;
|
|
102
|
+
} else {
|
|
103
|
+
big.push({ ...m, pct: t / total });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (small.input + small.output + small.cacheRead + small.cacheCreate > 0) {
|
|
107
|
+
const t = small.input + small.output + small.cacheRead + small.cacheCreate;
|
|
108
|
+
big.push({ ...small, pct: t / total });
|
|
109
|
+
}
|
|
110
|
+
result[platform] = big;
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function handleTokscaleBreakdown(args) {
|
|
116
|
+
const threshold = Number(args?.threshold ?? 0.01);
|
|
117
|
+
const platforms = await tokscaleModelBreakdown(threshold);
|
|
118
|
+
return { platforms };
|
|
119
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/tokscale-competitive-intel.mjs — tokscale_competitive_intel tool.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { tokscaleCompetitiveIntel } from "../tokscale_analytics.mjs";
|
|
6
|
+
import { ANNOTATIONS } from "./_schemas.mjs";
|
|
7
|
+
|
|
8
|
+
export const TOOL_DEF = {
|
|
9
|
+
name: "tokscale_competitive_intel",
|
|
10
|
+
description:
|
|
11
|
+
"Competitive intelligence for any AI tool company. Pass a target tool (by tokscale client slug like 'claude', 'codex', 'devin-cli' or canonical platform name like 'devin', 'other') and get: the target's rank by tokens among all detected tools, its full profile (tokens, cost, model mix, cache_read_pct, cost_per_million_tokens, market share), and a head-to-head comparison against every competitor (each competitor's tokens, cost, model_count, share, cost_per_million_tokens). Returns market_totals for context. If the target is not found, lists all detected clients. Use this to benchmark one AI tool against its competitors on this machine. All data is local — this is your own usage, not aggregate market data.",
|
|
12
|
+
annotations: { title: "Competitive intelligence", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
target: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description:
|
|
19
|
+
"The AI tool to analyze. Accepts a tokscale client slug (e.g. 'claude', 'codex', 'devin-cli', 'copilot') or a canonical platform name (e.g. 'devin', 'claude', 'other'). Case-insensitive. To discover valid slugs, call tokscale_market_share first.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ["target"],
|
|
23
|
+
description: "Requires the target tool name. No other parameters accepted.",
|
|
24
|
+
},
|
|
25
|
+
outputSchema: {
|
|
26
|
+
type: "object",
|
|
27
|
+
properties: {
|
|
28
|
+
target: { type: "string", description: "The resolved target slug(s)" },
|
|
29
|
+
found: { type: "boolean", description: "Whether the target was found in local data" },
|
|
30
|
+
rank_by_tokens: { type: "integer", description: "Target's rank among all detected tools by token volume" },
|
|
31
|
+
target_profile: { type: "object", description: "Target's full usage profile with model mix and market share" },
|
|
32
|
+
competitors: { type: "array", description: "All other detected tools with comparison metrics" },
|
|
33
|
+
market_totals: { type: "object", description: "Aggregate market context: { tokens, cost, messages, tool_count }" },
|
|
34
|
+
error: { type: "string", description: "Present if tokscale is unavailable or target is empty" },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export async function handleTokscaleCompetitiveIntel(args) {
|
|
40
|
+
const target = String(args?.target || "").trim();
|
|
41
|
+
if (!target) {
|
|
42
|
+
throw new Error("tokscale_competitive_intel requires a non-empty `target` argument (a tokscale client slug like 'claude' or 'codex').");
|
|
43
|
+
}
|
|
44
|
+
return await tokscaleCompetitiveIntel(target);
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/tokscale-cost-analysis.mjs — tokscale_cost_analysis tool.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { tokscaleCostAnalysis } from "../tokscale_analytics.mjs";
|
|
6
|
+
import { ANNOTATIONS } from "./_schemas.mjs";
|
|
7
|
+
|
|
8
|
+
export const TOOL_DEF = {
|
|
9
|
+
name: "tokscale_cost_analysis",
|
|
10
|
+
description:
|
|
11
|
+
"Cost analysis per developer per model from your local tokscale data. Returns a per-client × per-model cost breakdown with cost_per_million_tokens, cost_per_message, and share_cost. Includes a per-client cost rollup and totals: total_cost, total_tokens, avg_cost_per_million_tokens, most_expensive_model, cheapest_per_token. Use this to see exactly where your AI spend goes — which tools and models cost the most and which give the best value per token. Do NOT use this for market share — use tokscale_market_share for that.",
|
|
12
|
+
annotations: { title: "Cost analysis per model", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {},
|
|
16
|
+
description: "No parameters. Derives cost breakdown from tokscale models data.",
|
|
17
|
+
},
|
|
18
|
+
outputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
entries: { type: "array", description: "Per-client×per-model cost rows sorted by cost desc" },
|
|
22
|
+
client_rollup: { type: "array", description: "Per-client cost summary with share_cost and cost_per_million_tokens" },
|
|
23
|
+
totals: { type: "object", description: "Aggregate cost totals and extremes" },
|
|
24
|
+
error: { type: "string", description: "Present if tokscale is unavailable" },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function handleTokscaleCostAnalysis() {
|
|
30
|
+
return await tokscaleCostAnalysis();
|
|
31
|
+
}
|