sigrank 0.0.180 → 0.0.182

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/README.md CHANGED
@@ -332,28 +332,29 @@ Verifier numbers (ccusage/tokscale for codex) show **raw uncached input** (`inpu
332
332
 
333
333
  ## Platform adapters
334
334
 
335
- All adapters are token-only (no message content, no cost fields, no credentials).
336
-
337
- | Platform | Path | Notes |
338
- | ------------------ | ---------------------------------------------- | --------------------------------------------------------------- |
339
- | Claude Code | ✅ `~/.claude/projects` | Native; dedup by `(session_id, message_id)`; subagents included |
340
- | Codex | ✅ `~/.codex/sessions` | Estimated input via `io_ratio`; verified vs ccusage |
341
- | Devin CLI | ✅ `~/.local/share/devin/cli/sessions.db` | Estimated input via `io_ratio`; SQLite; same split as Codex |
342
- | Amp | ✅ `~/.local/share/amp/threads` | Full 4-pillar; per-message |
343
- | Kimi | ✅ `~/.kimi/sessions` | Full 4-pillar; `StatusUpdate` lines only |
344
- | pi-agent | ✅ `~/.pi/agent/sessions` | Full 4-pillar; per-message JSONL |
345
- | OpenClaw | ✅ `~/.openclaw` | Full 4-pillar; per-message JSONL |
346
- | Droid | ✅ `~/.factory/sessions/*.settings.json` | Full 4-pillar; thinking→output |
347
- | Codebuff | ✅ `~/.config/manicode` | Full 4-pillar; `chat-messages.json` |
348
- | Hermes | ✅ `~/.hermes/state.db` | Full 4-pillar; SQLite; reasoning→output |
349
- | Kilo | ✅ `~/.local/share/kilo/kilo.db` | Full 4-pillar; SQLite |
350
- | Qwen | ✅ `~/.qwen/projects` | `cacheCreate=0` estimated; thought→output |
351
- | Goose | ✅ `~/.local/share/goose/sessions/sessions.db` | `cacheCreate=cacheRead=0` estimated; SQLite |
352
- | Gemini CLI | ✅ `~/.gemini/tmp` | `cacheCreate=0` estimated; cache extracted from input field |
353
- | GitHub Copilot CLI | ✅ `~/.copilot/otel` | OTel JSONL; requires `COPILOT_OTEL_ENABLED=true` |
354
- | OpenCode | ⚠️ `~/.local/share/opencode` | Raw token counts not persisted in log format |
355
- | Cursor | 🔜 | Chat log path TBD |
356
- | Windsurf | 🔜 | Session logs at `~/.codeium/windsurf/` |
335
+ All adapters are token-only (no message content, no cost fields, no credentials). The table below is regenerated from the adapter file headers in `adapters/index.mjs` + `adapters/tokenpull.mjs` so the path, pillar shape, and notes match the actual code (a prior revision had drifted on Qwen/Codex/Copilot).
336
+
337
+ | Platform | Path | Pillar shape & notes |
338
+ | ------------------ | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
339
+ | Claude Code | ✅ `~/.claude/projects` (recursive, incl. `subagents/`) | Native 4-pillar; dedup by `(session_id, message_id)` — final snapshot wins |
340
+ | Codex | ✅ `~/.codex/sessions` (+ `archived_sessions`) | `input_tokens` incl. cached → `input` + `cacheCreate` split window-level via `io_ratio`; `cacheRead` native (`cached_input_tokens`); `reasoning_output`→output; verified vs ccusage (~1%) |
341
+ | Devin CLI | ✅ `~/.local/share/devin/cli/sessions.db` | SQLite; same `io_ratio` split as Codex (Alpha 2.0 default); `cacheRead` native |
342
+ | Amp | ✅ `~/.local/share/amp/threads` | Native 4-pillar; per-message |
343
+ | Kimi | ✅ `~/.kimi/sessions` | Native 4-pillar; `StatusUpdate` lines only |
344
+ | pi-agent | ✅ `~/.pi/agent/sessions` | Native 4-pillar; per-message JSONL |
345
+ | OpenClaw | ✅ `~/.openclaw` | Native 4-pillar; per-message JSONL |
346
+ | Droid | ✅ `~/.factory/sessions/*.settings.json` | Native 4-pillar; `thinking_tokens`→output |
347
+ | Codebuff | ✅ `~/.config/manicode` | Native 4-pillar; `chat-messages.json` |
348
+ | Hermes | ✅ `~/.hermes/state.db` | Native 4-pillar; SQLite; `reasoning_tokens`→output |
349
+ | Kilo | ✅ `~/.local/share/kilo/kilo.db` | Native 4-pillar; SQLite |
350
+ | Qwen | ✅ `~/.qwen/projects` | Estimated (`cacheCreate=0` — no field in logs); `cacheRead` from `cachedContentTokenCount`; `thoughtsTokenCount`→output |
351
+ | Goose | ✅ `~/.local/share/goose/sessions/sessions.db` (or `$GOOSE_PATH_ROOT/data/sessions/sessions.db`) | Estimated (`cacheCreate=cacheRead=0` — no cache fields); reasoning = `total−input−output`→output; cumulative-column dedup by session id |
352
+ | Gemini CLI | ✅ `~/.gemini/tmp` | Estimated (`cacheCreate=0`); `cacheRead` from `cached` field; `thought`→output; `input` = `input−cached` (fresh) |
353
+ | GitHub Copilot CLI | ✅ `~/.copilot/otel` | Native 4-pillar (OTel spans: `llm.token_count.{prompt,completion,cache_creation,cache_read}`); requires `COPILOT_OTEL_ENABLED=true` + `COPILOT_OTEL_EXPORTER_TYPE=file` set **before** session start |
354
+ | OpenCode | ⚠️ `~/.local/share/opencode` | Data gap — logs store `cost:0` and derive tokens via LiteLLM at runtime; raw token counts not persisted. No pillars readable with current format |
355
+ | Other (user JSON) | ✅ `$SIGRANK_OTHER_PATH` | User-supplied JSON `{ "windows": { "all": {input,output,cacheCreate,cacheRead} } }`; all-time only (no timestamps) |
356
+ | Cursor | 🔜 | Chat log path TBD |
357
+ | Windsurf | 🔜 | Session logs at `~/.codeium/windsurf/` |
357
358
 
358
359
  `estimated=true` means one or more pillars are derived, not native. The server re-scores all submitted pillars authoritatively; local preview Υ is indicative only.
359
360
 
@@ -51,8 +51,11 @@ const DAY_MS = 86_400_000; // shared with tokenpull.mjs but kept local to avoid
51
51
 
52
52
  // ── File-system helpers ───────────────────────────────────────────────────────
53
53
 
54
- /** Recursively yield every file whose name matches `pred` under dir (skips symlink dirs). */
55
- async function* walkFiles(dir, pred, counter = { n: 0 }, max = 10_000) {
54
+ /** Recursively yield every file whose name matches `pred` under dir (skips
55
+ * symlink dirs, stops after `max` files). Exported so adapters/tokenpull.mjs
56
+ * can reuse it instead of maintaining a second near-identical walker
57
+ * (_walkJsonl) — the two had drifted on the symlink-skip + max-files guards. */
58
+ export async function* walkFiles(dir, pred, counter = { n: 0 }, max = 10_000) {
56
59
  if (counter.n >= max) return;
57
60
  let entries;
58
61
  try {
@@ -743,7 +746,9 @@ export const hermesAdapter = {
743
746
  // Same combined-input problem as Codex: input_tokens INCLUDES cache write, so we
744
747
  // yield { ts, output, cacheRead, uncached } and let tokenpullCodex() do the
745
748
  // ioRatio split (input = output × ioRatio, cacheCreate = uncached − input).
746
- // ioRatio comes from Claude (Beta) or the 7:1:2 average (Alpha = 0.5).
749
+ // ioRatio comes from Claude (Beta = operator's Claude input/output ratio) or
750
+ // the Alpha 2.0 default (matches Codex; the "7:1:2 average → 0.5" note in a
751
+ // prior revision was wrong — tokenpullAny defaults ioRatio to 2.0 for both).
747
752
  export const devinAdapter = {
748
753
  platform: "devin",
749
754
  defaultRoot: () => join(homedir(), ".local", "share", "devin", "cli"),
@@ -795,9 +800,22 @@ export const devinAdapter = {
795
800
  // }
796
801
  // }
797
802
  //
798
- // The adapter yields one "message" per window with the summed pillars. Since
799
- // there's no per-message timestamp, all data lands in the "all" window only
800
- // (same as null-timestamp records in other adapters).
803
+ // The adapter yields ONE message carrying the all-time pillars. The generic
804
+ // tokenpull aggregator buckets by timestamp, and these records carry no
805
+ // timestamp (ts: null → "all" window only, same as null-ts records in other
806
+ // adapters). Yielding one message PER window key — as a prior version did —
807
+ // caused the "all" total to be the SUM of every window entry (all + 30d + 7d
808
+ // …), i.e. a multi-count. This is the only user-facing wrong-number bug in
809
+ // ingestion: an operator who filled in all four windows saw an "all" Υ
810
+ // computed from 2–4× the real pillars.
811
+ //
812
+ // Fix: yield exactly one record. If the JSON has an "all" entry, use it
813
+ // verbatim; otherwise sum the provided windows into a synthetic all-time
814
+ // total. The 7d/30d/90d entries are accepted (so the file format stays
815
+ // forward-compatible) but are NOT yielded as separate records — the generic
816
+ // aggregator has no way to route a timestamp-less record into a bounded
817
+ // window, so emitting them would only re-introduce the overcount. An operator
818
+ // who wants windowed data must use a real adapter that carries timestamps.
801
819
  export const otherAdapter = {
802
820
  platform: "other",
803
821
  defaultRoot: () => process.env.SIGRANK_OTHER_PATH || "",
@@ -822,23 +840,38 @@ export const otherAdapter = {
822
840
  throw new Error(`other adapter: invalid JSON in ${filePath}`);
823
841
  }
824
842
  const windows = data.windows || {};
825
- for (const [win, pillars] of Object.entries(windows)) {
826
- const input = Number(pillars.input || 0);
827
- const output = Number(pillars.output || 0);
828
- const cacheCreate = Number(pillars.cacheCreate || pillars.cache_create || 0);
829
- const cacheRead = Number(pillars.cacheRead || pillars.cache_read || 0);
830
- if (input + output + cacheCreate + cacheRead === 0) continue;
831
- yield {
832
- id: `other:${win}`,
833
- sid: `other-${win}`,
834
- ts: null, // no timestamp → lands in "all" window only
835
- input,
836
- output,
837
- cacheCreate,
838
- cacheRead,
839
- file: filePath,
840
- };
843
+ const norm = (p) => ({
844
+ input: Number(p?.input || 0),
845
+ output: Number(p?.output || 0),
846
+ cacheCreate: Number(p?.cacheCreate || p?.cache_create || 0),
847
+ cacheRead: Number(p?.cacheRead || p?.cache_read || 0),
848
+ });
849
+ let all;
850
+ if (windows.all) {
851
+ // Explicit all-time entry → use verbatim (do NOT add 7d/30d/90d on top).
852
+ all = norm(windows.all);
853
+ } else {
854
+ // No "all" key → fold the provided windows into a synthetic all-time.
855
+ all = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0 };
856
+ for (const pillars of Object.values(windows)) {
857
+ const n = norm(pillars);
858
+ all.input += n.input;
859
+ all.output += n.output;
860
+ all.cacheCreate += n.cacheCreate;
861
+ all.cacheRead += n.cacheRead;
862
+ }
841
863
  }
864
+ if (all.input + all.output + all.cacheCreate + all.cacheRead === 0) return;
865
+ yield {
866
+ id: `other:all`,
867
+ sid: `other-all`,
868
+ ts: null, // no timestamp → lands in "all" window only
869
+ input: all.input,
870
+ output: all.output,
871
+ cacheCreate: all.cacheCreate,
872
+ cacheRead: all.cacheRead,
873
+ file: filePath,
874
+ };
842
875
  },
843
876
  };
844
877
 
@@ -15,13 +15,13 @@
15
15
  * the same { platform, defaultRoot(), messages(root) } contract — Claude is just the first.
16
16
  */
17
17
 
18
- import { readdir, readFile, lstat } from "node:fs/promises";
18
+ import { readdir, readFile } from "node:fs/promises";
19
19
  import { existsSync } from "node:fs";
20
20
  import { execFile } from "node:child_process";
21
21
  import { join, dirname } from "node:path";
22
22
  import { homedir } from "node:os";
23
23
  import { fileURLToPath } from "node:url";
24
- import { ADAPTERS } from "./index.mjs";
24
+ import { ADAPTERS, walkFiles } from "./index.mjs";
25
25
 
26
26
  const DAY_MS = 86_400_000;
27
27
 
@@ -87,34 +87,26 @@ const MAX_JSONL_FILES = Number(process.env.SIGRANK_MAX_JSONL_FILES) || 10_000;
87
87
  * under-counts input (sub-agent runs are input-heavy). token-dashboard's rglob.
88
88
  *
89
89
  * Hardened: skips symlinked directories (prevents circular traversal) and stops
90
- * after MAX_JSONL_FILES files (prevents OOM on pathological trees). */
90
+ * after MAX_JSONL_FILES files (prevents OOM on pathological trees).
91
+ *
92
+ * Pass 2: now a thin sorted wrapper over the shared walkFiles() from
93
+ * adapters/index.mjs so the two walkers can't drift on the symlink-skip +
94
+ * max-files guards again. The sort (entries within each directory) is preserved
95
+ * by sorting the yielded paths; walkFiles already recurses in readdir order. */
91
96
  async function* _walkJsonl(dir, _counter = { n: 0 }) {
92
- if (_counter.n >= MAX_JSONL_FILES) return;
93
- let entries;
94
- try {
95
- entries = await readdir(dir, { withFileTypes: true });
96
- } catch {
97
- return;
98
- }
99
- for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : 1))) {
100
- if (_counter.n >= MAX_JSONL_FILES) return;
101
- const full = join(dir, e.name);
102
- if (e.isDirectory()) {
103
- // Skip symlinked directories — prevents circular traversal on machines where
104
- // ~/.claude/projects/ contains a symlink to a large or self-referential tree.
105
- let stat;
106
- try {
107
- stat = await lstat(full);
108
- } catch {
109
- continue;
110
- }
111
- if (stat.isSymbolicLink()) continue;
112
- yield* _walkJsonl(full, _counter);
113
- } else if (e.isFile() && e.name.endsWith(".jsonl")) {
114
- _counter.n++;
115
- yield full;
116
- }
97
+ // walkFiles yields in readdir order; we collect + sort to keep the
98
+ // deterministic ordering the previous implementation guaranteed.
99
+ const paths = [];
100
+ for await (const p of walkFiles(
101
+ dir,
102
+ (n) => n.endsWith(".jsonl"),
103
+ _counter,
104
+ MAX_JSONL_FILES,
105
+ )) {
106
+ paths.push(p);
117
107
  }
108
+ paths.sort((a, b) => (a < b ? -1 : 1));
109
+ for (const p of paths) yield p;
118
110
  }
119
111
 
120
112
  /**
@@ -68,9 +68,36 @@ export function cascade({ input, output, cacheCreate, cacheRead }) {
68
68
  return result;
69
69
  }
70
70
 
71
+ /**
72
+ * CLASS_TIERS — the canonical 8-tier dev10x taxonomy. SINGLE SOURCE OF TRUTH.
73
+ * Every consumer (presentation/tui.mjs, presentation/cli.mjs, tools/_schemas.mjs,
74
+ * resources/class-tiers.md) imports THIS list instead of maintaining its own.
75
+ * Order is descending (TRANSMITTER at top, IGNITER at bottom) — matches the
76
+ * classify() first-match-wins cut order.
77
+ */
78
+ export const CLASS_TIERS = [
79
+ "TRANSMITTER",
80
+ "ARCH+",
81
+ "ARCH",
82
+ "POWER",
83
+ "BASE",
84
+ "SEEKER",
85
+ "REFINER",
86
+ "IGNITER",
87
+ ];
88
+
89
+ /** The degenerate-case class returned when both yield and dev10x are null
90
+ * (all-zero / empty session). Distinct from IGNITER (which is a real low
91
+ * dev10x bucket) so callers can tell "no data" from "bottom tier". */
92
+ export const UNCLASSED = "UNCLASSED";
93
+
71
94
  /** MVP class tiering from Υ + 10xDEV (canon assigns from cascade SNR + 10xDEV; this
72
95
  * is the open-MVP approximation — proprietary threshold cuts stay server-side). */
73
96
  export function classify(yieldVal, dev10x) {
97
+ // Degenerate: all-null (empty session / no pillars) → UNCLASSED, not IGNITER.
98
+ // Falling through to IGNITER here would mislabel a no-data operator as the
99
+ // bottom tier; UNCLASSED lets the TUI / narrate surface "no data" instead.
100
+ if (yieldVal == null && dev10x == null) return UNCLASSED;
74
101
  if (yieldVal >= 1000 || dev10x >= 3) return "TRANSMITTER";
75
102
  if (dev10x >= 1.45) return "ARCH+";
76
103
  if (dev10x >= 1.35) return "ARCH";
@@ -23,6 +23,7 @@ import {
23
23
  copyFileSync,
24
24
  } from "node:fs";
25
25
  import { generateKeyPairSync, randomUUID } from "node:crypto";
26
+ import { agentVersion } from "../lib/pkg-version.mjs";
26
27
 
27
28
  const DIR = join(homedir(), ".sigrank-mcp");
28
29
  const PATH = join(DIR, "identity.json");
@@ -32,18 +33,6 @@ const MAX_BACKUPS = 5;
32
33
  /** SPKI DER prefix length for an ed25519 public key (12 bytes before the raw 32). */
33
34
  const SPKI_PREFIX_LEN = 12;
34
35
 
35
- /** Resolve this package's version for the agent_version stamp (best-effort). */
36
- function agentVersion() {
37
- try {
38
- const pkg = JSON.parse(
39
- readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
40
- );
41
- return `sigrank-mcp/${pkg.version}`;
42
- } catch {
43
- return "sigrank-mcp";
44
- }
45
- }
46
-
47
36
  export function keystorePath() {
48
37
  return PATH;
49
38
  }
@@ -0,0 +1,111 @@
1
+ // lib/constants.mjs — repo-wide constants that were previously inlined as
2
+ // literals across multiple files. Centralizing them here means a version bump
3
+ // or a client-map update lands in exactly one place.
4
+ //
5
+ // TERMS_VERSION / PRIVACY_VERSION — embedded in every enroll + submit
6
+ // payload (tools/enroll.mjs, tools/submit-paste.mjs,
7
+ // tools/tokenpull-submit.mjs, submit/index.mjs). They were inlined as
8
+ // "2026-07-21" in four files; a terms rev would have required finding
9
+ // and editing all four. Now: one export, imported everywhere.
10
+ //
11
+ // TOKSCALE_CLIENT_MAP — maps tokscale's raw client names to our platform
12
+ // IDs. Was duplicated verbatim in tools/index.mjs and
13
+ // tools/tokscale-breakdown.mjs (and again in tokscale_analytics.mjs);
14
+ // the three copies had already started to drift. One export here.
15
+ //
16
+ // isRankedAck — the "actually on the board" predicate (verified + persisted).
17
+ // Was inlined as `ack.verification_tier === "verified" && ack.persisted
18
+ // === true` in submit-paste, tokenpull-submit, and submit/index.mjs, with
19
+ // tokenpull-submit's multi-platform branch using a DIFFERENT looser
20
+ // predicate (`ack?.ranked ?? ack?.accepted ?? false`). One predicate here
21
+ // closes that loophole so every submit path agrees on what "ranked" means.
22
+
23
+ /** Current Terms of Service version the client attests to. */
24
+ export const TERMS_VERSION = "2026-07-21";
25
+
26
+ /** Current Privacy Policy version the client attests to. */
27
+ export const PRIVACY_VERSION = "2026-07-21";
28
+
29
+ /**
30
+ * The leaderboard sort metric the client requests from the server. Every
31
+ * leaderboard fetch site (tools/get-leaderboard.mjs, presentation/cli.mjs
32
+ * fetchBoard, presentation/tui.mjs, compare-self / optimize-efficiency /
33
+ * get-best-operator / compare-operators) MUST use this value — the server
34
+ * ranks by `yield_` and a different metric string would either 400 or return
35
+ * an unsorted board. Centralized here so a future metric rename lands in one
36
+ * place; the contract test in __tests__/contract/leaderboard-metric-contract.test.mjs
37
+ * guards against intra-repo drift and (in CI) cross-repo drift vs sigrank-app.
38
+ */
39
+ export const LEADERBOARD_METRIC = "yield_";
40
+
41
+ /**
42
+ * Map a raw tokscale `client` string to our canonical platform ID.
43
+ * Values mapped to `null` are skipped (synthetic / non-attributable).
44
+ * Values mapped to `"other"` have no native adapter and bucket together.
45
+ */
46
+ export const TOKSCALE_CLIENT_MAP = {
47
+ claude: "claude",
48
+ codex: "codex",
49
+ "devin-cli": "devin",
50
+ "devin-desktop": "devin",
51
+ gemini: "gemini",
52
+ amp: "amp",
53
+ kimi: "kimi",
54
+ qwen: "qwen",
55
+ goose: "goose",
56
+ kilo: "kilo",
57
+ kilocode: "kilo",
58
+ hermes: "hermes",
59
+ droid: "droid",
60
+ codebuff: "codebuff",
61
+ copilot: "copilot",
62
+ opencode: "opencode",
63
+ openclaw: "openclaw",
64
+ pi: "pi",
65
+ cursor: "other",
66
+ roocode: "other",
67
+ mux: "other",
68
+ crush: "other",
69
+ antigravity: "other",
70
+ "antigravity-cli": "other",
71
+ zed: "other",
72
+ kiro: "other",
73
+ trae: "other",
74
+ warp: "other",
75
+ cline: "other",
76
+ gjc: "other",
77
+ grok: "other",
78
+ jcode: "other",
79
+ commandcode: "other",
80
+ micode: "other",
81
+ junie: "other",
82
+ zcode: "other",
83
+ opencodereview: "other",
84
+ codebuddy: "other",
85
+ workbuddy: "other",
86
+ synthetic: null,
87
+ };
88
+
89
+ /**
90
+ * The "actually on the board" predicate. A submission is ranked only when the
91
+ * server returns HTTP ok AND verification_tier === "verified" AND persisted
92
+ * === true. An unenrolled/revoked device gets 202 but is NEVER ranked.
93
+ *
94
+ * Use this everywhere a submit path decides whether to surface `ranked: true`.
95
+ * The previous tokenpull-submit multi-platform branch used a looser
96
+ * `ack?.ranked ?? ack?.accepted ?? false` which would mark a non-persisted
97
+ * 202 as ranked — this predicate closes that loophole.
98
+ *
99
+ * @param {{ ok: boolean }} res the fetch Response (or { ok } shim)
100
+ * @param {Record<string, unknown>} ack the parsed JSON ack body
101
+ * @returns {boolean}
102
+ */
103
+ export function isRankedAck(res, ack) {
104
+ return !!(
105
+ res &&
106
+ res.ok &&
107
+ ack &&
108
+ ack.verification_tier === "verified" &&
109
+ ack.persisted === true
110
+ );
111
+ }
@@ -0,0 +1,59 @@
1
+ // lib/pkg-version.mjs — single source of truth for the package version.
2
+ //
3
+ // Resolves the repo-root package.json by walking up from this module until it
4
+ // finds the package named "sigrank". This is robust to callers in any
5
+ // subdirectory (identity/, submit/, presentation/, tools/, scripts/) — the
6
+ // previous per-caller `new URL("./package.json", import.meta.url)` lookups
7
+ // resolved relative to the CALLER's own directory and hit non-existent files,
8
+ // falling back to a bare string. The agent_version stamp rides inside the
9
+ // SIGNED submit payload (agent.agent_version), so a wrong/missing version
10
+ // there is a high-priority bug — it survived because no test guarded it.
11
+ //
12
+ // One implementation, imported everywhere. Never throws: returns "unknown" on
13
+ // any failure so a missing/renamed package.json never breaks a submit.
14
+
15
+ import { readFileSync } from "node:fs";
16
+ import { dirname, join } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ /** The published package name (package.json "name"). Used to identify the
20
+ * repo-root package.json unambiguously past any nested package.json. */
21
+ const REPO_PKG_NAME = "sigrank";
22
+
23
+ /** Resolve the repo-root package version (best-effort).
24
+ * Returns the version string (e.g. "0.0.181") or "unknown" on any failure. */
25
+ export function pkgVersion() {
26
+ try {
27
+ let dir = dirname(fileURLToPath(import.meta.url));
28
+ for (let i = 0; i < 20; i++) {
29
+ const pkgPath = join(dir, "package.json");
30
+ let pkg;
31
+ try {
32
+ pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
33
+ } catch {
34
+ // no package.json here — walk up
35
+ const parent = dirname(dir);
36
+ if (parent === dir) break;
37
+ dir = parent;
38
+ continue;
39
+ }
40
+ if (pkg.name === REPO_PKG_NAME) return pkg.version || "unknown";
41
+ // Found a package.json but it isn't ours (e.g. a nested dep) — keep walking.
42
+ const parent = dirname(dir);
43
+ if (parent === dir) {
44
+ // Reached the filesystem root without matching; use whatever we found.
45
+ return pkg.version || "unknown";
46
+ }
47
+ dir = parent;
48
+ }
49
+ return "unknown";
50
+ } catch {
51
+ return "unknown";
52
+ }
53
+ }
54
+
55
+ /** The full agent-version stamp, e.g. "sigrank-mcp/0.0.181".
56
+ * This is the value embedded in the signed payload's agent.agent_version. */
57
+ export function agentVersion() {
58
+ return `sigrank-mcp/${pkgVersion()}`;
59
+ }
package/manifest.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
3
+ "manifest_version": "0.4",
4
+ "name": "sigrank",
5
+ "version": "0.0.180",
6
+ "description": "Ranks AI coding operators by token cascade efficiency (Yield = cache_read × output / input²). 23 tools for 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
+ "author": {
8
+ "name": "SunrisesIllNeverSee",
9
+ "url": "https://github.com/SunrisesIllNeverSee"
10
+ },
11
+ "homepage": "https://signalaf.com",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/SunrisesIllNeverSee/sigrank-mcp"
15
+ },
16
+ "license": "MIT",
17
+ "server": {
18
+ "type": "node",
19
+ "entry_point": "index.mjs",
20
+ "mcp_config": {
21
+ "command": "npx",
22
+ "args": [
23
+ "sigrank"
24
+ ],
25
+ "env": {
26
+ "SIGRANK_API_BASE": "https://signalaf.com"
27
+ }
28
+ }
29
+ },
30
+ "user_config": {
31
+ "SIGRANK_API_BASE": {
32
+ "type": "string",
33
+ "title": "SigRank API Base URL",
34
+ "description": "Overrides the board host for the SigRank API. Defaults to https://signalaf.com.",
35
+ "default": "https://signalaf.com",
36
+ "required": false
37
+ }
38
+ },
39
+ "compatibility": {
40
+ "platforms": [
41
+ "darwin",
42
+ "win32",
43
+ "linux"
44
+ ]
45
+ }
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "0.0.180",
3
+ "version": "0.0.182",
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",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "files": [
34
34
  "index.mjs",
35
+ "lib/",
35
36
  "tools/",
36
37
  "analytics/",
37
38
  "identity/",
@@ -55,6 +56,7 @@
55
56
  "narrate.mjs",
56
57
  "tools.mjs",
57
58
  "tokscale_analytics.mjs",
59
+ "manifest.json",
58
60
  "README.md",
59
61
  "CHANGELOG.md",
60
62
  "MCP_SERVER_DESCRIPTION.md",