sigrank 0.18.2 → 0.18.4

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 (2) hide show
  1. package/package.json +1 -1
  2. package/tools.mjs +220 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "0.18.2",
3
+ "version": "0.18.4",
4
4
  "author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
5
5
  "description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
6
6
  "mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
package/tools.mjs CHANGED
@@ -1362,6 +1362,36 @@ export const TOOLS = [
1362
1362
  },
1363
1363
  },
1364
1364
  },
1365
+ {
1366
+ name: "tokscale_breakdown",
1367
+ description:
1368
+ "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 }] }.",
1369
+ annotations: {
1370
+ title: "Model breakdown",
1371
+ ...ANNOTATIONS.readOnlyHint,
1372
+ ...ANNOTATIONS.openWorldHint,
1373
+ },
1374
+ inputSchema: {
1375
+ type: "object",
1376
+ properties: {
1377
+ threshold: {
1378
+ type: "number",
1379
+ description:
1380
+ "Models below this fraction of their platform's total tokens are lumped into 'other'. Default 0.01 (1%). Set 0 to see every model.",
1381
+ default: 0.01,
1382
+ },
1383
+ },
1384
+ },
1385
+ outputSchema: {
1386
+ type: "object",
1387
+ properties: {
1388
+ platforms: {
1389
+ type: "object",
1390
+ description: "Map of platform name → array of { model, input, output, cacheRead, cacheCreate, pct }",
1391
+ },
1392
+ },
1393
+ },
1394
+ },
1365
1395
  ];
1366
1396
 
1367
1397
  // tokenpull window key → the board's window_type enum.
@@ -1383,9 +1413,191 @@ const _lastWatchSubmitAt = new Map();
1383
1413
  // keeps only platforms with real local data, and sorts claude → codex → rest.
1384
1414
  // Pass `platforms` to scope it (e.g. ['claude'] for a fast first paint).
1385
1415
  export async function pullActivePlatforms({ platforms } = {}, opts = {}) {
1386
- const targets = platforms && platforms.length ? platforms : ALL_PLATFORMS;
1416
+ // If the caller specified explicit platforms, use those.
1417
+ if (platforms && platforms.length) {
1418
+ return _pullExplicit(platforms, opts);
1419
+ }
1420
+ // Auto-detect: run tokscale once to discover all active clients, then pull
1421
+ // only those. This is much faster than trying all 17 adapters (most fail
1422
+ // silently on a machine that doesn't have that tool installed).
1423
+ //
1424
+ // Flow: tokscale surfaces ALL clients → map to our platform names →
1425
+ // ccusage is primary for Claude (more accurate) → other platforms use
1426
+ // their adapters. Anything in tokscale NOT in ccusage gets included.
1427
+ const detected = await _tokscaleDetectClients().catch(() => null);
1428
+ if (detected && detected.length > 0) {
1429
+ return _pullExplicit(detected, opts);
1430
+ }
1431
+ // Fallback: tokscale not installed or failed → try all adapters (old behavior)
1432
+ return _pullExplicit(ALL_PLATFORMS, opts);
1433
+ }
1434
+
1435
+ /** Run `tokscale models --json` and return the list of our platform names. */
1436
+ async function _tokscaleDetectClients() {
1437
+ const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
1438
+ const data = JSON.parse(raw);
1439
+ const entries = Array.isArray(data?.entries)
1440
+ ? data.entries
1441
+ : Array.isArray(data)
1442
+ ? data
1443
+ : [];
1444
+ if (!entries.length) return [];
1445
+ // Complete tokscale client → our platform name mapping.
1446
+ // Tokscale supports 40 clients (as of v3.1.0). We have native adapters for 16;
1447
+ // the rest map to "other" (user-supplied JSON via SIGRANK_OTHER_PATH).
1448
+ // Source: https://github.com/junhoyeo/tokscale
1449
+ const TOKSCALE_CLIENT_MAP = {
1450
+ // Native adapters
1451
+ claude: "claude",
1452
+ codex: "codex",
1453
+ "devin-cli": "devin",
1454
+ "devin-desktop": "devin",
1455
+ gemini: "gemini",
1456
+ amp: "amp",
1457
+ kimi: "kimi",
1458
+ qwen: "qwen",
1459
+ goose: "goose",
1460
+ kilo: "kilo",
1461
+ kilocode: "kilo",
1462
+ hermes: "hermes",
1463
+ droid: "droid",
1464
+ codebuff: "codebuff",
1465
+ copilot: "copilot",
1466
+ opencode: "opencode",
1467
+ openclaw: "openclaw",
1468
+ pi: "pi",
1469
+ // No native adapter yet → "other"
1470
+ cursor: "other",
1471
+ roocode: "other",
1472
+ mux: "other",
1473
+ crush: "other",
1474
+ antigravity: "other",
1475
+ "antigravity-cli": "other",
1476
+ zed: "other",
1477
+ kiro: "other",
1478
+ trae: "other",
1479
+ warp: "other",
1480
+ cline: "other",
1481
+ gjc: "other",
1482
+ grok: "other",
1483
+ jcode: "other",
1484
+ commandcode: "other",
1485
+ micode: "other",
1486
+ junie: "other",
1487
+ zcode: "other",
1488
+ opencodereview: "other",
1489
+ codebuddy: "other",
1490
+ workbuddy: "other",
1491
+ // synthetic = re-attributed from other sources, skip
1492
+ synthetic: null,
1493
+ };
1494
+ const clients = new Set();
1495
+ for (const e of entries) {
1496
+ if (!e || !e.client) continue;
1497
+ if (e.model === "<synthetic>" || e.model === "unknown") continue;
1498
+ const input = Number(e.input) || 0;
1499
+ const output = Number(e.output) || 0;
1500
+ if (input + output === 0) continue;
1501
+ const platform = TOKSCALE_CLIENT_MAP[e.client];
1502
+ if (platform === null) continue; // synthetic → skip
1503
+ clients.add(platform || "other");
1504
+ }
1505
+ return [...clients];
1506
+ }
1507
+
1508
+ /**
1509
+ * Run `tokscale models --json` and return a model-level breakdown per platform.
1510
+ * Models under `threshold` (default 1%) of their platform's total are lumped
1511
+ * into "other" to keep the display clean.
1512
+ *
1513
+ * Returns: { platform: [{ model, input, output, cacheRead, cacheCreate, pct }] }
1514
+ */
1515
+ export async function tokscaleModelBreakdown(threshold = 0.01) {
1516
+ const raw = await execFileAsync("tokscale", ["models", "--json"], 60000);
1517
+ const data = JSON.parse(raw);
1518
+ const entries = Array.isArray(data?.entries)
1519
+ ? data.entries
1520
+ : Array.isArray(data)
1521
+ ? data
1522
+ : [];
1523
+ if (!entries.length) return {};
1524
+
1525
+ const TOKSCALE_CLIENT_MAP = {
1526
+ claude: "claude", codex: "codex", "devin-cli": "devin",
1527
+ "devin-desktop": "devin", gemini: "gemini", amp: "amp",
1528
+ kimi: "kimi", qwen: "qwen", goose: "goose", kilo: "kilo",
1529
+ kilocode: "kilo", hermes: "hermes", droid: "droid",
1530
+ codebuff: "codebuff", copilot: "copilot", opencode: "opencode",
1531
+ openclaw: "openclaw", pi: "pi",
1532
+ cursor: "other", roocode: "other", mux: "other", crush: "other",
1533
+ antigravity: "other", "antigravity-cli": "other", zed: "other",
1534
+ kiro: "other", trae: "other", warp: "other", cline: "other",
1535
+ gjc: "other", grok: "other", jcode: "other", commandcode: "other",
1536
+ micode: "other", junie: "other", zcode: "other",
1537
+ opencodereview: "other", codebuddy: "other", workbuddy: "other",
1538
+ synthetic: null,
1539
+ };
1540
+
1541
+ // Group entries by our platform name
1542
+ const byPlatform = {};
1543
+ for (const e of entries) {
1544
+ if (!e || !e.client) continue;
1545
+ if (e.model === "<synthetic>" || e.model === "unknown") continue;
1546
+ const input = Number(e.input) || 0;
1547
+ const output = Number(e.output) || 0;
1548
+ if (input + output === 0) continue;
1549
+ const platform = TOKSCALE_CLIENT_MAP[e.client];
1550
+ if (platform === null) continue;
1551
+ const p = platform || "other";
1552
+ if (!byPlatform[p]) byPlatform[p] = [];
1553
+ byPlatform[p].push({
1554
+ model: e.model,
1555
+ input,
1556
+ output,
1557
+ cacheRead: Number(e.cacheRead) || 0,
1558
+ cacheCreate: Number(e.cacheWrite) || 0,
1559
+ });
1560
+ }
1561
+
1562
+ // For each platform: sort by total tokens desc, lump small models into "other"
1563
+ const result = {};
1564
+ for (const [platform, models] of Object.entries(byPlatform)) {
1565
+ const total = models.reduce(
1566
+ (s, m) => s + m.input + m.output + m.cacheRead + m.cacheCreate,
1567
+ 0,
1568
+ );
1569
+ if (total === 0) continue;
1570
+ const sorted = models.sort(
1571
+ (a, b) =>
1572
+ b.input + b.output + b.cacheRead + b.cacheCreate -
1573
+ (a.input + a.output + a.cacheRead + a.cacheCreate),
1574
+ );
1575
+ const big = [];
1576
+ const small = { model: "other", input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
1577
+ for (const m of sorted) {
1578
+ const t = m.input + m.output + m.cacheRead + m.cacheCreate;
1579
+ if (t / total < threshold) {
1580
+ small.input += m.input;
1581
+ small.output += m.output;
1582
+ small.cacheRead += m.cacheRead;
1583
+ small.cacheCreate += m.cacheCreate;
1584
+ } else {
1585
+ big.push({ ...m, pct: t / total });
1586
+ }
1587
+ }
1588
+ if (small.input + small.output + small.cacheRead + small.cacheCreate > 0) {
1589
+ const t = small.input + small.output + small.cacheRead + small.cacheCreate;
1590
+ big.push({ ...small, pct: t / total });
1591
+ }
1592
+ result[platform] = big;
1593
+ }
1594
+ return result;
1595
+ }
1596
+
1597
+ /** Pull a specific set of platforms in parallel, filter to active ones. */
1598
+ async function _pullExplicit(platforms, opts = {}) {
1387
1599
  const settled = await Promise.allSettled(
1388
- targets.map((p) => callTool("tokenpull", { platform: p }, opts)),
1600
+ platforms.map((p) => callTool("tokenpull", { platform: p }, opts)),
1389
1601
  );
1390
1602
  const active = settled
1391
1603
  .filter((r) => r.status === "fulfilled" && r.value)
@@ -3290,6 +3502,12 @@ export async function callTool(name, args, opts = {}) {
3290
3502
  };
3291
3503
  }
3292
3504
 
3505
+ if (name === "tokscale_breakdown") {
3506
+ const threshold = Number(args?.threshold ?? 0.01);
3507
+ const platforms = await tokscaleModelBreakdown(threshold);
3508
+ return { platforms };
3509
+ }
3510
+
3293
3511
  throw new Error(`Unknown tool: ${name}`);
3294
3512
  }
3295
3513