sigrank 1.0.39 → 1.0.41
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 +21 -0
- package/README.md +2 -2
- package/adapters/index.mjs +97 -1
- package/adapters/tokenpull.mjs +15 -11
- package/identity/keystore.mjs +8 -2
- package/index.mjs +20 -13
- package/lib/constants.mjs +20 -17
- package/manifest.json +1 -1
- package/omp-cache.mjs +25 -4
- package/package.json +1 -1
- package/presentation/cli.mjs +24 -18
- package/tools/_helpers.mjs +1 -1
- package/tools/tokscale-breakdown.mjs +2 -1
- package/tools/tokscale-competitive-intel.mjs +5 -1
- package/tui.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.37](https://github.com/SunrisesIllNeverSee/sigrank-mcp/releases/tag/v1.0.37) (2026-09-08)
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
* TUI: remove "multi" from Lifetime Tokens display (combined total, not a platform)
|
|
8
|
+
* TUI: Four Degrees "Top" column uses live operator cascade metrics instead of static snapshot
|
|
9
|
+
* TUI: "all" cascade row uses server batched data (from 1.0.36)
|
|
10
|
+
* Added Devin as first-class platform (P.09) across MCP + app
|
|
11
|
+
|
|
12
|
+
## [1.0.36](https://github.com/SunrisesIllNeverSee/sigrank-mcp/releases/tag/v1.0.36) (2026-09-08)
|
|
13
|
+
|
|
14
|
+
### Fixes
|
|
15
|
+
|
|
16
|
+
* TUI: "all" cascade row override uses server batched data to match Lifetime Tokens
|
|
17
|
+
|
|
18
|
+
## [1.0.35](https://github.com/SunrisesIllNeverSee/sigrank-mcp/releases/tag/v1.0.35) (2026-09-07)
|
|
19
|
+
|
|
20
|
+
### Fixes
|
|
21
|
+
|
|
22
|
+
* TUI presentation updates and audit fixes
|
|
23
|
+
|
|
3
24
|
## [0.0.203](https://github.com/SunrisesIllNeverSee/sigrank-mcp/compare/v0.0.202...v0.0.203) (2026-08-12)
|
|
4
25
|
|
|
5
26
|
### Chores
|
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ smithery tool call sigrank rank_paste '{"text": "1000000 500000 50000 800000"}'
|
|
|
161
161
|
## Commands
|
|
162
162
|
|
|
163
163
|
```
|
|
164
|
-
⊙ SigRank SignalAF CLI
|
|
164
|
+
⊙ SigRank SignalAF CLI v1.0.37
|
|
165
165
|
|
|
166
166
|
Default (no args)
|
|
167
167
|
sigrank unified dashboard: cascade + token pillars + board
|
|
@@ -407,7 +407,7 @@ All adapters are token-only (no message content, no cost fields, no credentials)
|
|
|
407
407
|
## Privacy
|
|
408
408
|
|
|
409
409
|
- **Token-only persistence and submission.** Local-log adapters read usage metadata only. The optional proxy necessarily handles provider-bound request and response bytes in memory, but never persists their content; it writes only token counts, model/backend metadata, and timestamps. Only token telemetry is submitted to SigRank SignalAF.
|
|
410
|
-
- **Local by default.** `tokenpull` reads only `~/.claude/projects`
|
|
410
|
+
- **Local by default.** `tokenpull` reads only the selected or detected adapter paths on your device (e.g. `~/.claude/projects` for Claude, `~/.codex` for Codex, plus additional paths for Amp, Devin, Gemini, Goose, and other supported platforms). Numbers stay on your machine unless you explicitly submit with a codename.
|
|
411
411
|
- **Background tooling excluded.** Memory plugins, observers, summarizers (e.g. `claude-mem`, `mem0`, `observer-sessions`) are filtered from both Claude and Codex reads. `subagents/` are kept — they represent real operator work.
|
|
412
412
|
- **Board reads are anonymous.** No account needed to browse, compare, or watch.
|
|
413
413
|
- **Ranked submissions are signed, not trusted.** `sigrank submit` requires a one-time `enroll` (device-bound ed25519 key — the private key never leaves your machine). Verify what's sent with `sigrank submit --dry-run`: the payload is four token counts, ratios, and a signature.
|
package/adapters/index.mjs
CHANGED
|
@@ -42,13 +42,16 @@
|
|
|
42
42
|
*/
|
|
43
43
|
|
|
44
44
|
import { readdir, readFile } from "node:fs/promises";
|
|
45
|
+
import { existsSync } from "node:fs";
|
|
45
46
|
import { join, dirname } from "node:path";
|
|
46
47
|
import { homedir } from "node:os";
|
|
47
48
|
import { execFile as execFileCb } from "node:child_process";
|
|
48
49
|
import { promisify } from "node:util";
|
|
50
|
+
import { fileURLToPath } from "node:url";
|
|
49
51
|
import { cachedOmpScan, ompCacheEnabled } from "../omp-cache.mjs";
|
|
50
52
|
|
|
51
53
|
const execFileP = promisify(execFileCb);
|
|
54
|
+
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
52
55
|
const DAY_MS = 86_400_000; // shared with tokenpull.mjs but kept local to avoid circular import
|
|
53
56
|
|
|
54
57
|
// ── File-system helpers ───────────────────────────────────────────────────────
|
|
@@ -1075,6 +1078,96 @@ export const proxyAdapter = {
|
|
|
1075
1078
|
},
|
|
1076
1079
|
};
|
|
1077
1080
|
|
|
1081
|
+
|
|
1082
|
+
// ── ccusage-backed adapters ──────────────────────────────────────────────────
|
|
1083
|
+
// ccusage maintains source-specific readers for these platforms and normalizes
|
|
1084
|
+
// its daily JSON output into the same four token buckets SigRank requires.
|
|
1085
|
+
// Keep these as thin adapters instead of duplicating upstream parsers here.
|
|
1086
|
+
// The bundled dependency is preferred; PATH is a fallback for development.
|
|
1087
|
+
async function execCcusage(args) {
|
|
1088
|
+
const localEntrypoint = join(
|
|
1089
|
+
PACKAGE_ROOT,
|
|
1090
|
+
"node_modules",
|
|
1091
|
+
"ccusage",
|
|
1092
|
+
"src",
|
|
1093
|
+
"cli.js",
|
|
1094
|
+
);
|
|
1095
|
+
const options = {
|
|
1096
|
+
timeout: 30_000,
|
|
1097
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
1098
|
+
};
|
|
1099
|
+
if (existsSync(localEntrypoint)) {
|
|
1100
|
+
return execFileP(process.execPath, [localEntrypoint, ...args], options);
|
|
1101
|
+
}
|
|
1102
|
+
return execFileP("ccusage", args, options);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function makeCcusageAdapter(platform) {
|
|
1106
|
+
return {
|
|
1107
|
+
platform,
|
|
1108
|
+
defaultRoot: () => homedir(),
|
|
1109
|
+
async *messages() {
|
|
1110
|
+
let data;
|
|
1111
|
+
try {
|
|
1112
|
+
const { stdout } = await execCcusage([
|
|
1113
|
+
platform,
|
|
1114
|
+
"daily",
|
|
1115
|
+
"--json",
|
|
1116
|
+
"--no-cost",
|
|
1117
|
+
]);
|
|
1118
|
+
data = JSON.parse(stdout || "{}");
|
|
1119
|
+
} catch {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const rows = Array.isArray(data?.daily)
|
|
1124
|
+
? data.daily
|
|
1125
|
+
: Array.isArray(data?.data)
|
|
1126
|
+
? data.data
|
|
1127
|
+
: Array.isArray(data)
|
|
1128
|
+
? data
|
|
1129
|
+
: [];
|
|
1130
|
+
|
|
1131
|
+
for (const row of rows) {
|
|
1132
|
+
if (!row || typeof row !== "object") continue;
|
|
1133
|
+
const date = row.date || row.day;
|
|
1134
|
+
if (!date) continue;
|
|
1135
|
+
const input = Number(row.inputTokens ?? row.input_tokens ?? 0);
|
|
1136
|
+
const output = Number(row.outputTokens ?? row.output_tokens ?? 0);
|
|
1137
|
+
const cacheCreate = Number(
|
|
1138
|
+
row.cacheCreationTokens ?? row.cache_create_tokens ?? 0,
|
|
1139
|
+
);
|
|
1140
|
+
const cacheRead = Number(
|
|
1141
|
+
row.cacheReadTokens ?? row.cache_read_tokens ?? 0,
|
|
1142
|
+
);
|
|
1143
|
+
if (
|
|
1144
|
+
![input, output, cacheCreate, cacheRead].every(
|
|
1145
|
+
(n) => Number.isFinite(n) && n >= 0,
|
|
1146
|
+
)
|
|
1147
|
+
) {
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (input + output + cacheCreate + cacheRead === 0) continue;
|
|
1151
|
+
|
|
1152
|
+
yield {
|
|
1153
|
+
id: `${platform}:${date}`,
|
|
1154
|
+
sid: null,
|
|
1155
|
+
ts: `${date}T12:00:00.000Z`,
|
|
1156
|
+
input,
|
|
1157
|
+
output,
|
|
1158
|
+
cacheCreate,
|
|
1159
|
+
cacheRead,
|
|
1160
|
+
file: `ccusage:${platform}`,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
},
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
export const antigravityAdapter = makeCcusageAdapter("antigravity");
|
|
1168
|
+
export const grokAdapter = makeCcusageAdapter("grok");
|
|
1169
|
+
export const zcodeAdapter = makeCcusageAdapter("zcode");
|
|
1170
|
+
|
|
1078
1171
|
// ── Registry ──────────────────────────────────────────────────────────────────
|
|
1079
1172
|
/** All non-Claude, non-Codex adapters keyed by platform ID. */
|
|
1080
1173
|
export const ADAPTERS = {
|
|
@@ -1095,6 +1188,9 @@ export const ADAPTERS = {
|
|
|
1095
1188
|
other: otherAdapter,
|
|
1096
1189
|
omp: ompAdapter,
|
|
1097
1190
|
proxy: proxyAdapter,
|
|
1191
|
+
antigravity: antigravityAdapter,
|
|
1192
|
+
grok: grokAdapter,
|
|
1193
|
+
zcode: zcodeAdapter,
|
|
1098
1194
|
};
|
|
1099
1195
|
|
|
1100
|
-
export const ALL_PLATFORMS = Object.keys(ADAPTERS).concat(["claude", "codex"
|
|
1196
|
+
export const ALL_PLATFORMS = Object.keys(ADAPTERS).concat(["claude", "codex"]);
|
package/adapters/tokenpull.mjs
CHANGED
|
@@ -18,19 +18,20 @@
|
|
|
18
18
|
import { readdir, readFile } from "node:fs/promises";
|
|
19
19
|
import { existsSync } from "node:fs";
|
|
20
20
|
import { execFile } from "node:child_process";
|
|
21
|
-
import { join, dirname } from "node:path";
|
|
21
|
+
import { join, dirname, delimiter } from "node:path";
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { fileURLToPath } from "node:url";
|
|
24
24
|
import { ADAPTERS, walkFiles } from "./index.mjs";
|
|
25
|
+
import { TOKSCALE_CLIENT_MAP } from "../lib/constants.mjs";
|
|
25
26
|
|
|
26
27
|
const DAY_MS = 86_400_000;
|
|
27
28
|
|
|
28
29
|
// Resolve the package root for finding bundled binaries (ccusage, tokscale, etc.)
|
|
29
|
-
const _pkgRoot = join(dirname(fileURLToPath(import.meta.url)));
|
|
30
|
+
const _pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
30
31
|
const _localBin = join(_pkgRoot, "node_modules", ".bin");
|
|
31
32
|
// Prepend local node_modules/.bin to PATH so bundled deps are found even when
|
|
32
33
|
// not globally installed (e.g., npx sigrank, local dev).
|
|
33
|
-
const _envPath = `${_localBin}${process.env.PATH ?
|
|
34
|
+
const _envPath = `${_localBin}${process.env.PATH ? delimiter + process.env.PATH : ""}`;
|
|
34
35
|
|
|
35
36
|
// ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync in the
|
|
36
37
|
// fresh verifier readers. execSync blocks the entire Node event loop (no key handling,
|
|
@@ -522,14 +523,17 @@ async function _freshTokscale(platform = "claude") {
|
|
|
522
523
|
: Array.isArray(data)
|
|
523
524
|
? data
|
|
524
525
|
: [];
|
|
525
|
-
const rows = entries.filter(
|
|
526
|
-
(e)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
e.
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
526
|
+
const rows = entries.filter((e) => {
|
|
527
|
+
if (!e || e.model === "<synthetic>" || e.model === "unknown") return false;
|
|
528
|
+
const mapped = Object.prototype.hasOwnProperty.call(
|
|
529
|
+
TOKSCALE_CLIENT_MAP,
|
|
530
|
+
e.client,
|
|
531
|
+
)
|
|
532
|
+
? TOKSCALE_CLIENT_MAP[e.client]
|
|
533
|
+
: e.client;
|
|
534
|
+
if (mapped !== platform) return false;
|
|
535
|
+
return (Number(e.input) || 0) > 0 || (Number(e.output) || 0) > 0;
|
|
536
|
+
});
|
|
533
537
|
if (!rows.length) return null;
|
|
534
538
|
const acc = rows.reduce(
|
|
535
539
|
(a, e) => ({
|
package/identity/keystore.mjs
CHANGED
|
@@ -149,7 +149,7 @@ export function ensureIdentity() {
|
|
|
149
149
|
// Regeneration path: a partial/corrupt record. The binding is tied to device_id —
|
|
150
150
|
// if a new device_id is generated, the old codename/operator MUST NOT carry over
|
|
151
151
|
// (bindingForFreshIdentity drops them). Only a reused device_id keeps its binding.
|
|
152
|
-
const fresh = generateIdentity(
|
|
152
|
+
const fresh = generateIdentity();
|
|
153
153
|
const binding = bindingForFreshIdentity(existing, fresh);
|
|
154
154
|
fresh.codename = binding.codename;
|
|
155
155
|
fresh.operator_id = binding.operator_id;
|
|
@@ -173,7 +173,13 @@ export function recordEnrollment({ codename, operator_id }) {
|
|
|
173
173
|
/** Clear the local identity (sign out). Next enroll provisions a fresh device_id. */
|
|
174
174
|
export function clearIdentity() {
|
|
175
175
|
try {
|
|
176
|
-
if (existsSync(
|
|
176
|
+
if (existsSync(DIR)) {
|
|
177
|
+
for (const file of readdirSync(DIR)) {
|
|
178
|
+
if (file === "identity.json" || file.startsWith(BACKUP_PREFIX)) {
|
|
179
|
+
unlinkSync(join(DIR, file));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
177
183
|
} catch {
|
|
178
184
|
/* best-effort */
|
|
179
185
|
}
|
package/index.mjs
CHANGED
|
@@ -54,21 +54,28 @@ function serverVersion() {
|
|
|
54
54
|
// Prevent silent crashes — log to stderr (MCP clients read stdout; stderr is safe for
|
|
55
55
|
// diagnostics). The process exits so the client can respawn with a clean slate rather
|
|
56
56
|
// than hanging on a broken connection.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
process.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
);
|
|
68
|
-
process.
|
|
69
|
-
|
|
57
|
+
// NOTE: These handlers are scoped to MCP mode only — see installMcpCrashHandlers()
|
|
58
|
+
// called inside startMcpServer(). Installing them at module load would break the
|
|
59
|
+
// TUI (process.exit(1) doesn't restore raw mode / alternate screen).
|
|
60
|
+
|
|
61
|
+
function installMcpCrashHandlers() {
|
|
62
|
+
process.on("uncaughtException", (err) => {
|
|
63
|
+
process.stderr.write(
|
|
64
|
+
`[sigrank-mcp] uncaughtException: ${err?.message || err}\n`,
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
});
|
|
68
|
+
process.on("unhandledRejection", (reason) => {
|
|
69
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
70
|
+
process.stderr.write(
|
|
71
|
+
`[sigrank-mcp] unhandledRejection: ${msg}\n`,
|
|
72
|
+
);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
70
76
|
|
|
71
77
|
async function startMcpServer() {
|
|
78
|
+
installMcpCrashHandlers();
|
|
72
79
|
const server = new Server(
|
|
73
80
|
{ name: "sigrank", version: serverVersion() },
|
|
74
81
|
{ capabilities: { tools: { listChanged: false }, prompts: { listChanged: false }, resources: { listChanged: false } } },
|
package/lib/constants.mjs
CHANGED
|
@@ -68,8 +68,9 @@ export const TOKSCALE_CLIENT_MAP = {
|
|
|
68
68
|
roocode: "other",
|
|
69
69
|
mux: "other",
|
|
70
70
|
crush: "other",
|
|
71
|
-
antigravity: "
|
|
72
|
-
"antigravity-cli": "
|
|
71
|
+
antigravity: "antigravity",
|
|
72
|
+
"antigravity-cli": "antigravity",
|
|
73
|
+
"antigravity-extension": "antigravity",
|
|
73
74
|
zed: "other",
|
|
74
75
|
kiro: "other",
|
|
75
76
|
trae: "other",
|
|
@@ -77,12 +78,12 @@ export const TOKSCALE_CLIENT_MAP = {
|
|
|
77
78
|
cline: "other",
|
|
78
79
|
"9router": "other",
|
|
79
80
|
gjc: "other",
|
|
80
|
-
grok: "
|
|
81
|
+
grok: "grok",
|
|
81
82
|
jcode: "other",
|
|
82
83
|
commandcode: "other",
|
|
83
84
|
micode: "other",
|
|
84
85
|
junie: "other",
|
|
85
|
-
zcode: "
|
|
86
|
+
zcode: "zcode",
|
|
86
87
|
opencodereview: "other",
|
|
87
88
|
codebuddy: "other",
|
|
88
89
|
workbuddy: "other",
|
|
@@ -92,25 +93,27 @@ export const TOKSCALE_CLIENT_MAP = {
|
|
|
92
93
|
reasonix: "other",
|
|
93
94
|
"prime-agent": "other",
|
|
94
95
|
freebuff: "other",
|
|
96
|
+
cherrystudio: "other",
|
|
97
|
+
dsh: "other",
|
|
98
|
+
mcode: "other",
|
|
99
|
+
fx: "other",
|
|
100
|
+
lmstudio: "other",
|
|
101
|
+
unsloth: "other",
|
|
102
|
+
hindsight: "other",
|
|
103
|
+
"micode-desktop": "other",
|
|
104
|
+
muse: "other",
|
|
95
105
|
synthetic: null,
|
|
96
106
|
};
|
|
97
107
|
|
|
98
108
|
/**
|
|
99
|
-
* Platforms whose local data
|
|
100
|
-
* never lists them as an active client. pullActivePlatforms() auto-detect must
|
|
101
|
-
* probe these directly or their cascade row can never appear, even with GBs of
|
|
102
|
-
* native local telemetry (that was exactly the omp bug: tokscale reports
|
|
103
|
-
* claude/codex/copilot/gemini/grok/kimi/kiro/opencode/pi and nothing else, so
|
|
104
|
-
* a tokscale-detected machine silently dropped oh-my-pi entirely).
|
|
109
|
+
* Platforms whose local data the pinned tokscale release cannot discover.
|
|
105
110
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* (TOKSCALE_CLIENT_MAP already maps the `omp` / `oh-my-pi` client names for
|
|
111
|
-
* when that happens).
|
|
111
|
+
* Tokscale 4.17.0 includes oh-my-pi (`omp`) directly, so there are currently
|
|
112
|
+
* no blind platforms that need forced probing. Keep the hook because future
|
|
113
|
+
* platform-specific discovery gaps can still be added here without changing
|
|
114
|
+
* pullActivePlatforms().
|
|
112
115
|
*/
|
|
113
|
-
export const TOKSCALE_BLIND_PLATFORMS = [
|
|
116
|
+
export const TOKSCALE_BLIND_PLATFORMS = [];
|
|
114
117
|
|
|
115
118
|
/**
|
|
116
119
|
* The "actually on the board" predicate. A submission is ranked only when the
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
|
|
3
3
|
"manifest_version": "0.4",
|
|
4
4
|
"name": "sigrank",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.40",
|
|
6
6
|
"description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read \u00d7 output / input\u00b2). 25 tools including SigRank Standard record, yield calculation, local token usage pulls, signed submission, and tokscale-powered analytics (market share, developer profiles, model trends, cost analysis, device profiles, MCP usage, competitive intelligence). Privacy-first: runs locally, only four token counts leave the machine.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "SunrisesIllNeverSee",
|
package/omp-cache.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* obtained, falls back to uncached scan without damaging the cache.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { execFile as execFileCb } from "node:child_process";
|
|
26
|
+
import { execFile as execFileCb, spawn } from "node:child_process";
|
|
27
27
|
import { promisify } from "node:util";
|
|
28
28
|
import { mkdir, stat, unlink } from "node:fs/promises";
|
|
29
29
|
import { join, dirname } from "node:path";
|
|
@@ -31,6 +31,22 @@ import { homedir } from "node:os";
|
|
|
31
31
|
|
|
32
32
|
const execFileP = promisify(execFileCb);
|
|
33
33
|
|
|
34
|
+
/** Run SQL via stdin to avoid OS argument-length limits (E2BIG on large transcripts). */
|
|
35
|
+
function sqliteExecStdin(dbPath, sql, timeoutMs = 30_000) {
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const child = spawn("sqlite3", [dbPath], {
|
|
38
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
39
|
+
timeout: timeoutMs,
|
|
40
|
+
});
|
|
41
|
+
let stderr = "";
|
|
42
|
+
child.stderr.on("data", (d) => { stderr += d; });
|
|
43
|
+
child.on("error", () => resolve(false));
|
|
44
|
+
child.on("close", (code) => resolve(code === 0));
|
|
45
|
+
child.stdin.write(`PRAGMA busy_timeout=${BUSY_TIMEOUT_MS};\n${sql}`);
|
|
46
|
+
child.stdin.end();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
const CACHE_SCHEMA_VERSION = "1";
|
|
35
51
|
const CACHE_PARSER_VERSION = "omp-stage1-v1";
|
|
36
52
|
const BUSY_TIMEOUT_MS = 5_000;
|
|
@@ -57,7 +73,7 @@ async function sqliteJson(dbPath, sql, timeoutMs = 10_000) {
|
|
|
57
73
|
/** Run a SQLite command that doesn't return rows (DDL/DML). Returns success boolean. */
|
|
58
74
|
async function sqliteExec(dbPath, sql, timeoutMs = 30_000) {
|
|
59
75
|
try {
|
|
60
|
-
await execFileP("sqlite3", [dbPath, sql], {
|
|
76
|
+
await execFileP("sqlite3", [dbPath, `PRAGMA busy_timeout=${BUSY_TIMEOUT_MS}; ${sql}`], {
|
|
61
77
|
timeout: timeoutMs,
|
|
62
78
|
maxBuffer: SQLITE_MAX_BUFFER,
|
|
63
79
|
});
|
|
@@ -67,8 +83,13 @@ async function sqliteExec(dbPath, sql, timeoutMs = 30_000) {
|
|
|
67
83
|
}
|
|
68
84
|
}
|
|
69
85
|
|
|
70
|
-
/** Run multiple SQL statements in a single sqlite3 call (semicolon-separated).
|
|
86
|
+
/** Run multiple SQL statements in a single sqlite3 call (semicolon-separated).
|
|
87
|
+
* Uses stdin for large SQL to avoid OS argument-length limits. */
|
|
71
88
|
async function sqliteExecBatch(dbPath, sql, timeoutMs = 60_000) {
|
|
89
|
+
// OS arg-length limit: ~256KB on macOS, ~2MB on Linux. Use stdin for safety.
|
|
90
|
+
if (sql.length > 200_000) {
|
|
91
|
+
return sqliteExecStdin(dbPath, sql, timeoutMs);
|
|
92
|
+
}
|
|
72
93
|
return sqliteExec(dbPath, sql, timeoutMs);
|
|
73
94
|
}
|
|
74
95
|
|
|
@@ -425,7 +446,7 @@ export async function* cachedOmpScan({
|
|
|
425
446
|
const cachedSum = sumFields(cachedRecords, field);
|
|
426
447
|
const uncachedSum = sumFields(uncachedRecords, field);
|
|
427
448
|
if (cachedSum !== uncachedSum) {
|
|
428
|
-
|
|
449
|
+
throw new Error(
|
|
429
450
|
`[omp-cache] PARITY DRIFT: ${field} cached=${cachedSum} uncached=${uncachedSum} (diff=${cachedSum - uncachedSum})`,
|
|
430
451
|
);
|
|
431
452
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.41",
|
|
4
4
|
"author": "SunrisesIllNeverSee (https://github.com/SunrisesIllNeverSee)",
|
|
5
5
|
"description": "Upsilon measurement engine distributed through the sigrank CLI/MCP compatibility package, with SigRank public leaderboard tools.",
|
|
6
6
|
"mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
|
package/presentation/cli.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import { cascade, classify, CLASS_TIERS, UNCLASSED, tierOf } from "../cascade.mj
|
|
|
29
29
|
import { ALL_PLATFORMS } from "../adapters.mjs";
|
|
30
30
|
import { ensureIdentity, keystorePath } from "../keystore.mjs";
|
|
31
31
|
import { submitSignedWindow } from "../submit.mjs";
|
|
32
|
-
import { LEADERBOARD_METRIC } from "../lib/constants.mjs";
|
|
32
|
+
import { LEADERBOARD_METRIC, TOKSCALE_CLIENT_MAP } from "../lib/constants.mjs";
|
|
33
33
|
import { execFile } from "child_process";
|
|
34
34
|
import { existsSync, readFileSync } from "fs";
|
|
35
35
|
import os from "os";
|
|
@@ -37,9 +37,9 @@ import path from "path";
|
|
|
37
37
|
import { fileURLToPath } from "url";
|
|
38
38
|
|
|
39
39
|
// Resolve local node_modules/.bin for bundled deps (ccusage, tokscale)
|
|
40
|
-
const _pkgRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
40
|
+
const _pkgRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
41
41
|
const _localBin = path.join(_pkgRoot, "node_modules", ".bin");
|
|
42
|
-
const _envPath = `${_localBin}${process.env.PATH ?
|
|
42
|
+
const _envPath = `${_localBin}${process.env.PATH ? path.delimiter + process.env.PATH : ""}`;
|
|
43
43
|
|
|
44
44
|
// ASYNC FIX (2026-06-27): execFile wrapped in a Promise — replaces execSync for
|
|
45
45
|
// defense-in-depth (shell injection prevention). execFile passes args as an
|
|
@@ -429,14 +429,17 @@ async function tokscalePillars(platform = "claude") {
|
|
|
429
429
|
: Array.isArray(data)
|
|
430
430
|
? data
|
|
431
431
|
: [];
|
|
432
|
-
const rows = entries.filter(
|
|
433
|
-
(e)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
e.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
432
|
+
const rows = entries.filter((e) => {
|
|
433
|
+
if (!e || e.model === "<synthetic>" || e.model === "unknown") return false;
|
|
434
|
+
const mapped = Object.prototype.hasOwnProperty.call(
|
|
435
|
+
TOKSCALE_CLIENT_MAP,
|
|
436
|
+
e.client,
|
|
437
|
+
)
|
|
438
|
+
? TOKSCALE_CLIENT_MAP[e.client]
|
|
439
|
+
: e.client;
|
|
440
|
+
if (mapped !== platform) return false;
|
|
441
|
+
return (Number(e.input) || 0) > 0 || (Number(e.output) || 0) > 0;
|
|
442
|
+
});
|
|
440
443
|
if (rows.length) {
|
|
441
444
|
const p = rows.reduce(
|
|
442
445
|
(acc, e) => ({
|
|
@@ -458,13 +461,16 @@ async function tokscalePillars(platform = "claude") {
|
|
|
458
461
|
try {
|
|
459
462
|
const data = JSON.parse(readFileSync(reportPath, "utf8"));
|
|
460
463
|
const entries = data.entries ?? [];
|
|
461
|
-
const rows = entries.filter(
|
|
462
|
-
(e)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
e.
|
|
466
|
-
|
|
467
|
-
|
|
464
|
+
const rows = entries.filter((e) => {
|
|
465
|
+
if (!e || e.model === "<synthetic>" || e.model === "unknown") return false;
|
|
466
|
+
const mapped = Object.prototype.hasOwnProperty.call(
|
|
467
|
+
TOKSCALE_CLIENT_MAP,
|
|
468
|
+
e.client,
|
|
469
|
+
)
|
|
470
|
+
? TOKSCALE_CLIENT_MAP[e.client]
|
|
471
|
+
: e.client;
|
|
472
|
+
return mapped === platform && (e.input > 0 || e.output > 0);
|
|
473
|
+
});
|
|
468
474
|
if (rows.length === 0) return null;
|
|
469
475
|
const p = rows.reduce(
|
|
470
476
|
(acc, e) => ({
|
package/tools/_helpers.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
// Resolve local node_modules/.bin for bundled deps (ccusage, tokscale)
|
|
22
22
|
const _pkgRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
23
23
|
const _localBin = path.join(_pkgRoot, "..", "node_modules", ".bin");
|
|
24
|
-
const _envPath = `${_localBin}${process.env.PATH ?
|
|
24
|
+
const _envPath = `${_localBin}${process.env.PATH ? path.delimiter + process.env.PATH : ""}`;
|
|
25
25
|
|
|
26
26
|
export const DEFAULT_API_BASE =
|
|
27
27
|
process.env.SIGRANK_API_BASE || "https://signalaf.com";
|
|
@@ -113,7 +113,8 @@ export async function tokscaleModelBreakdown(threshold = 0.01) {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export async function handleTokscaleBreakdown(args) {
|
|
116
|
-
const
|
|
116
|
+
const raw = Number(args?.threshold ?? 0.01);
|
|
117
|
+
const threshold = Number.isFinite(raw) ? Math.max(0, raw) : 0.01;
|
|
117
118
|
const platforms = await tokscaleModelBreakdown(threshold);
|
|
118
119
|
return { platforms };
|
|
119
120
|
}
|
|
@@ -39,7 +39,11 @@ export const TOOL_DEF = {
|
|
|
39
39
|
export async function handleTokscaleCompetitiveIntel(args) {
|
|
40
40
|
const target = String(args?.target || "").trim();
|
|
41
41
|
if (!target) {
|
|
42
|
-
|
|
42
|
+
return {
|
|
43
|
+
target: "",
|
|
44
|
+
found: false,
|
|
45
|
+
error: "tokscale_competitive_intel requires a non-empty `target` argument (a tokscale client slug like 'claude' or 'codex').",
|
|
46
|
+
};
|
|
43
47
|
}
|
|
44
48
|
return await tokscaleCompetitiveIntel(target);
|
|
45
49
|
}
|
package/tui.mjs
CHANGED
|
@@ -2,7 +2,8 @@ import { runTui } from './presentation/tui.mjs';
|
|
|
2
2
|
export * from './presentation/tui.mjs';
|
|
3
3
|
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
-
|
|
5
|
+
const entry = process.argv[1];
|
|
6
|
+
if (entry && import.meta.url === pathToFileURL(entry).href) {
|
|
6
7
|
runTui().catch((e) => {
|
|
7
8
|
console.error(e);
|
|
8
9
|
process.exit(1);
|