sigrank 0.19.0 → 1.0.2

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 (82) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/MCP_SERVER_DESCRIPTION.md +172 -0
  3. package/README.md +89 -41
  4. package/adapters/index.mjs +1100 -0
  5. package/adapters/tokenpull.mjs +568 -0
  6. package/adapters.mjs +1 -865
  7. package/analytics/cascade.mjs +338 -0
  8. package/bin/sigrank.mjs +137 -0
  9. package/cascade.mjs +1 -274
  10. package/cli.mjs +1 -1853
  11. package/connect.mjs +1 -13
  12. package/identity/connect.mjs +13 -0
  13. package/identity/keystore.mjs +231 -0
  14. package/identity/sign.mjs +106 -0
  15. package/index.mjs +47 -162
  16. package/keystore.mjs +1 -242
  17. package/lib/constants.mjs +137 -0
  18. package/lib/pkg-version.mjs +59 -0
  19. package/manifest.json +46 -0
  20. package/narrate.mjs +1 -77
  21. package/package.json +44 -18
  22. package/preflight.mjs +1 -191
  23. package/presentation/cli.mjs +1926 -0
  24. package/presentation/narrate.mjs +219 -0
  25. package/presentation/tui-themes.mjs +189 -0
  26. package/presentation/tui.mjs +3316 -0
  27. package/prompts/check-my-efficiency.md +1 -0
  28. package/prompts/compare-with-leader.md +1 -0
  29. package/prompts/simulate-improvement.md +1 -0
  30. package/proxy.mjs +492 -0
  31. package/resources/class-tiers.md +81 -0
  32. package/resources/data-policy.md +31 -0
  33. package/resources/install-guide.md +59 -0
  34. package/resources/privacy-model.md +44 -0
  35. package/resources/scoring-formula.md +24 -0
  36. package/review.mjs +263 -0
  37. package/sign.mjs +1 -106
  38. package/submit/index.mjs +297 -0
  39. package/submit/preflight.mjs +191 -0
  40. package/submit.mjs +1 -303
  41. package/tokenpull.mjs +1 -576
  42. package/tokscale_analytics.mjs +864 -0
  43. package/tools/_framing.mjs +217 -0
  44. package/tools/_helpers.mjs +253 -0
  45. package/tools/_peer-matching.mjs +361 -0
  46. package/tools/_report.mjs +262 -0
  47. package/tools/_schemas.mjs +492 -0
  48. package/tools/_verifiers.mjs +148 -0
  49. package/tools/compare-operators.mjs +80 -0
  50. package/tools/compare-self.mjs +124 -0
  51. package/tools/describe-power-user.mjs +78 -0
  52. package/tools/diagnose-cascade.mjs +192 -0
  53. package/tools/discover-peers.mjs +198 -0
  54. package/tools/enroll.mjs +119 -0
  55. package/tools/get-best-operator.mjs +59 -0
  56. package/tools/get-leaderboard.mjs +27 -0
  57. package/tools/get-operator.mjs +33 -0
  58. package/tools/index.mjs +233 -0
  59. package/tools/optimize-efficiency.mjs +170 -0
  60. package/tools/rank-paste.mjs +51 -0
  61. package/tools/rank-windows.mjs +120 -0
  62. package/tools/self-improve.mjs +423 -0
  63. package/tools/simulate-change.mjs +207 -0
  64. package/tools/standard-record.mjs +108 -0
  65. package/tools/submit-paste.mjs +121 -0
  66. package/tools/submit-verified.mjs +153 -0
  67. package/tools/suggest-improvements.mjs +168 -0
  68. package/tools/tokenpull-compare.mjs +123 -0
  69. package/tools/tokenpull-submit.mjs +235 -0
  70. package/tools/tokenpull.mjs +113 -0
  71. package/tools/tokscale-breakdown.mjs +119 -0
  72. package/tools/tokscale-competitive-intel.mjs +45 -0
  73. package/tools/tokscale-cost-analysis.mjs +31 -0
  74. package/tools/tokscale-developer-profile.mjs +36 -0
  75. package/tools/tokscale-device-profile.mjs +35 -0
  76. package/tools/tokscale-market-share.mjs +38 -0
  77. package/tools/tokscale-mcp-usage.mjs +33 -0
  78. package/tools/tokscale-model-trends.mjs +32 -0
  79. package/tools/watch-tokenpull.mjs +134 -0
  80. package/tools.mjs +1 -3897
  81. package/tui.mjs +4 -2650
  82. package/banner.svg +0 -7
@@ -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
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * tools/tokscale-developer-profile.mjs — tokscale_developer_profile tool.
3
+ */
4
+
5
+ import { tokscaleDeveloperProfile } from "../tokscale_analytics.mjs";
6
+ import { ANNOTATIONS } from "./_schemas.mjs";
7
+
8
+ export const TOOL_DEF = {
9
+ name: "tokscale_developer_profile",
10
+ description:
11
+ "Per-developer usage profile across all 20+ AI tools detected by tokscale on this machine. For each tool: model mix (per-model tokens/cost/messages/performance), token pillars (input/output/cache_read/cache_write/reasoning), cache_read_pct, session count, scan path (redacted to ~), workspace breakdown, and headless support flag. Returns a summary with tool_count, total_cost, dominant_tool. All filesystem paths are redacted (home dir → ~). Use this to understand your full AI tool footprint — which tools you use, which models per tool, and how your usage is distributed. Do NOT use this for cost-only analysis — use tokscale_cost_analysis for that.",
12
+ annotations: { title: "Developer usage profile", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {},
16
+ description: "No parameters. Profiles all locally-detected AI tools via tokscale.",
17
+ },
18
+ outputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ tools: {
22
+ type: "array",
23
+ description: "Per-tool profile with model mix, token pillars, session counts, workspaces",
24
+ },
25
+ summary: {
26
+ type: "object",
27
+ description: "Aggregate summary: { tool_count, total_cost, total_tokens, total_messages, avg_cost_per_tool, dominant_tool }",
28
+ },
29
+ error: { type: "string", description: "Present if tokscale is unavailable" },
30
+ },
31
+ },
32
+ };
33
+
34
+ export async function handleTokscaleDeveloperProfile() {
35
+ return await tokscaleDeveloperProfile();
36
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * tools/tokscale-device-profile.mjs — tokscale_device_profile tool.
3
+ */
4
+
5
+ import { tokscaleDeviceProfile } from "../tokscale_analytics.mjs";
6
+ import { ANNOTATIONS } from "./_schemas.mjs";
7
+
8
+ export const TOOL_DEF = {
9
+ name: "tokscale_device_profile",
10
+ description:
11
+ "Device fingerprinting: profiles this machine's AI tool footprint — which AI tools are installed, where their session logs live (paths redacted to ~), how many sessions and messages each has, when the machine was active (daily activity + day-of-week distribution), session concurrency, and longest continuous session. Combines tokscale clients + graph data. All filesystem paths are redacted so no local username leaks. Use this to audit your own machine's AI tool installation and activity pattern. This is local-only — it profiles the current machine, not remote devices.",
12
+ annotations: { title: "Device profile", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {},
16
+ description: "No parameters. Profiles the local machine via tokscale.",
17
+ },
18
+ outputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ installed_tools: { type: "array", description: "Detected AI tools with redacted session paths and message counts" },
22
+ installed_tool_count: { type: "integer" },
23
+ active_tool_count: { type: "integer", description: "Tools whose session path exists on disk" },
24
+ date_range: { type: "object", description: "{ start, end } of the activity data" },
25
+ activity: { type: "object", description: "Daily activity + day-of-week token distribution" },
26
+ sessions: { type: "object", description: "Session metrics: count, total/longest active time, max concurrent" },
27
+ summary: { type: "object", description: "Cost summary from graph: total_tokens, total_cost, avg_per_day, max_single_day" },
28
+ error: { type: "string", description: "Present if tokscale is unavailable" },
29
+ },
30
+ },
31
+ };
32
+
33
+ export async function handleTokscaleDeviceProfile() {
34
+ return await tokscaleDeviceProfile();
35
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * tools/tokscale-market-share.mjs — tokscale_market_share tool.
3
+ */
4
+
5
+ import {
6
+ tokscaleMarketShare,
7
+ } from "../tokscale_analytics.mjs";
8
+ import { ANNOTATIONS } from "./_schemas.mjs";
9
+
10
+ export const TOOL_DEF = {
11
+ name: "tokscale_market_share",
12
+ description:
13
+ "Complete AI tool market share analysis from your local tokscale data. Aggregates per-model usage by client (AI tool) and computes each tool's share of total tokens, cost, and messages. Returns each tool ranked by token share, with share_tokens / share_cost / share_messages percentages and a totals rollup. All data is read locally from tokscale's scan of your session logs — no network calls, no PII. Use this to see which AI coding tools dominate your workflow by volume, spend, or activity. Do NOT use this for per-model detail — use tokscale_developer_profile for that.",
14
+ annotations: { title: "AI tool market share", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {},
18
+ description: "No parameters. Scans all locally-detected AI tools via tokscale.",
19
+ },
20
+ outputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ tools: {
24
+ type: "array",
25
+ description: "AI tools ranked by token share, each with { client, label, tokens, cost, messages, model_count, share_tokens, share_cost, share_messages }",
26
+ },
27
+ totals: {
28
+ type: "object",
29
+ description: "Aggregate totals: { tokens, cost, messages, tool_count }",
30
+ },
31
+ error: { type: "string", description: "Present if tokscale is unavailable" },
32
+ },
33
+ },
34
+ };
35
+
36
+ export async function handleTokscaleMarketShare() {
37
+ return await tokscaleMarketShare();
38
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * tools/tokscale-mcp-usage.mjs — tokscale_mcp_usage tool.
3
+ */
4
+
5
+ import { tokscaleMcpUsage } from "../tokscale_analytics.mjs";
6
+ import { ANNOTATIONS } from "./_schemas.mjs";
7
+
8
+ export const TOOL_DEF = {
9
+ name: "tokscale_mcp_usage",
10
+ description:
11
+ "MCP server usage patterns from your local tokscale data. Reports which MCP servers tokscale detected on this machine, the detection window, and active days in that window. tokscale currently exposes detected MCP servers as a set (not per-session attribution), so the report notes the detection window. Use this to see which MCP servers are active on your machine. If no servers are detected, the response explains that MCP server tracking requires a tokscale version that records per-session MCP attribution.",
12
+ annotations: { title: "MCP server usage", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {},
16
+ description: "No parameters. Reads MCP server data from tokscale graph output.",
17
+ },
18
+ outputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ servers: { type: "array", description: "Detected MCP servers: [{ name, detected: true }]" },
22
+ server_count: { type: "integer" },
23
+ detection_window: { type: "object", description: "{ start, end } date range" },
24
+ active_days_in_window: { type: "integer" },
25
+ note: { type: "string", description: "Explanation of the MCP server data granularity" },
26
+ error: { type: "string", description: "Present if tokscale is unavailable" },
27
+ },
28
+ },
29
+ };
30
+
31
+ export async function handleTokscaleMcpUsage() {
32
+ return await tokscaleMcpUsage();
33
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * tools/tokscale-model-trends.mjs — tokscale_model_trends tool.
3
+ */
4
+
5
+ import { tokscaleModelTrends } from "../tokscale_analytics.mjs";
6
+ import { ANNOTATIONS } from "./_schemas.mjs";
7
+
8
+ export const TOOL_DEF = {
9
+ name: "tokscale_model_trends",
10
+ description:
11
+ "Model adoption trends over time from your local tokscale data. Combines monthly aggregates with per-day contribution data to build a model-level adoption timeline: each model's first_seen / last_seen / active_days / tokens / clients, plus a month-by-month adoption curve showing how many new models appeared each month. Returns months[], models[], and adoption_curve[]. Use this to track which AI models you adopted when, and how your model mix evolved over time. Do NOT use this for cost trends — use tokscale_cost_analysis for current cost breakdown.",
12
+ annotations: { title: "Model adoption trends", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {},
16
+ description: "No parameters. Derives trends from tokscale monthly + graph data.",
17
+ },
18
+ outputSchema: {
19
+ type: "object",
20
+ properties: {
21
+ months: { type: "array", description: "Per-month aggregates: { month, models, model_count, input, output, cache_read, messages, cost }" },
22
+ models: { type: "array", description: "Per-model adoption: { model, first_seen, last_seen, active_days, tokens, cost, messages, client_count, clients }" },
23
+ adoption_curve: { type: "array", description: "Per-month new-model count: { month, new_models, new_model_count, total_models }" },
24
+ date_range: { type: "object", description: "{ start, end } date range of the data" },
25
+ error: { type: "string", description: "Present if tokscale is unavailable" },
26
+ },
27
+ },
28
+ };
29
+
30
+ export async function handleTokscaleModelTrends() {
31
+ return await tokscaleModelTrends();
32
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * tools/watch-tokenpull.mjs — watch_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 { ensureIdentity } from "../identity/keystore.mjs";
9
+ import { submitSignedWindow } from "../submit/index.mjs";
10
+ import { TOKENPULL_OUTPUT, ANNOTATIONS } from "./_schemas.mjs";
11
+ import {
12
+ pullByPlatform,
13
+ WATCH_SUBMIT_COOLDOWN_MS,
14
+ _lastWatchSubmitAt,
15
+ } from "./_helpers.mjs";
16
+
17
+ export const TOOL_DEF = {
18
+ name: "watch_tokenpull",
19
+ description:
20
+ "One poll per call: pulls your local token logs and returns the current cascade for the watched window — the tool never blocks or loops. Re-call at your desired cadence to watch for changes (interval_s is advisory only and echoed back as poll_interval_s). With submit:true (and an enrolled device) each call may also sign + publish the watched window to the board, rate-limited to once per 5 min per platform+window; default is preview-only (no submit).",
21
+ annotations: {
22
+ title: "Watch token pull",
23
+ ...ANNOTATIONS.idempotentHint,
24
+ ...ANNOTATIONS.openWorldHint,
25
+ },
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ platform: {
30
+ type: "string",
31
+ enum: ALL_PLATFORMS,
32
+ description: "platform to watch (default: claude)",
33
+ },
34
+ interval_s: {
35
+ type: "number",
36
+ description:
37
+ "advisory poll cadence in seconds (default: 60, min: 10) — echoed back as poll_interval_s; does not make the call block or loop",
38
+ },
39
+ window: {
40
+ type: "string",
41
+ enum: ["7d", "30d", "90d", "all"],
42
+ description:
43
+ "which window to watch (default: 7d — most sensitive to recent activity)",
44
+ },
45
+ submit: {
46
+ type: "boolean",
47
+ description:
48
+ "auto-submit the watched window to the board as a VERIFIED operator each poll (requires `enroll`; default false = preview only)",
49
+ },
50
+ },
51
+ },
52
+ outputSchema: TOKENPULL_OUTPUT,
53
+ };
54
+
55
+ export async function handleWatchTokenpull(args, ctx) {
56
+ // Poll the local token logs at a configurable interval and return the cascade
57
+ // diff whenever new sessions appear. One poll cycle per MCP call — the client
58
+ // is responsible for re-calling at the desired cadence (MCP tools are stateless;
59
+ // a persistent background watcher lives outside the tool boundary).
60
+ //
61
+ // With submit:true + an enrolled device, this also signs + POSTs the watched window
62
+ // to the verified ingest path each poll (the server dedups identical re-submits).
63
+ const platform = args?.platform || "claude";
64
+ const watchWindow = args?.window || "7d";
65
+ const intervalS = Math.max(10, Number(args?.interval_s) || 60);
66
+
67
+ const pulled = await pullByPlatform(platform, ctx.opts);
68
+ const win = pulled.windows.find((w) => w.window === watchWindow);
69
+ if (!win)
70
+ throw new Error(
71
+ `watch_tokenpull: window '${watchWindow}' not found in pull result.`,
72
+ );
73
+
74
+ const c = cascade(win.pillars);
75
+ const card = narrate(c, `${watchWindow} ${platform}`);
76
+
77
+ // AUTH.WIRE (D7 §7): when submit is on AND the device is enrolled, sign + POST the
78
+ // watched window to the verified ingest path. Default OFF = preview only. The server
79
+ // dedups identical re-submits (exact snapshot_hash → 422), so re-calling is safe.
80
+ let auth_submit = null;
81
+ if (args?.submit === true) {
82
+ const id = ctx.opts.identity || ensureIdentity();
83
+ if (id.codename && id.operator_id && id.private_key_pkcs8_b64) {
84
+ // E3: client-side cooldown — at most one auto-submit per platform+window per 5 min.
85
+ // Prevents a fast poll loop from churning the board even before the server's
86
+ // hash-dedup kicks in. Armed only on a non-error outcome so failed submits retry.
87
+ const clockNow = typeof ctx.opts.now === "number" ? ctx.opts.now : Date.now();
88
+ const cdKey = `${pulled.platform}:${watchWindow}`;
89
+ const last = _lastWatchSubmitAt.get(cdKey);
90
+ if (last != null && clockNow - last < WATCH_SUBMIT_COOLDOWN_MS) {
91
+ const waitS = Math.ceil(
92
+ (WATCH_SUBMIT_COOLDOWN_MS - (clockNow - last)) / 1000,
93
+ );
94
+ auth_submit = {
95
+ status: "cooldown",
96
+ detail: `auto-submit for '${cdKey}' on cooldown — next in ~${waitS}s (max once / 5 min).`,
97
+ retry_after_s: waitS,
98
+ };
99
+ } else {
100
+ auth_submit = await submitSignedWindow(
101
+ watchWindow,
102
+ win.pillars,
103
+ win.messages,
104
+ id,
105
+ {
106
+ apiBase: ctx.apiBase,
107
+ fetchImpl: ctx.doFetch,
108
+ platform: pulled.platform,
109
+ now: ctx.opts.now,
110
+ },
111
+ );
112
+ if (auth_submit?.status !== "error")
113
+ _lastWatchSubmitAt.set(cdKey, clockNow);
114
+ }
115
+ } else {
116
+ auth_submit = {
117
+ status: "not_enrolled",
118
+ detail: "Run `npx sigrank-mcp enroll` to auto-submit verified runs.",
119
+ };
120
+ }
121
+ }
122
+ return {
123
+ platform: pulled.platform,
124
+ window: watchWindow,
125
+ pillars: win.pillars,
126
+ messages: win.messages,
127
+ cascade: c,
128
+ card,
129
+ generatedAt: pulled.generatedAt,
130
+ poll_interval_s: intervalS,
131
+ auth_submit,
132
+ note: "One snapshot per call — re-call at your poll interval to detect changes.",
133
+ };
134
+ }