sigrank 0.15.5 → 0.16.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/README.md +132 -77
- package/adapters.mjs +508 -240
- package/badges.mjs +264 -0
- package/cascade.mjs +185 -54
- package/cli.mjs +1311 -893
- package/connect.mjs +2 -2
- package/index.mjs +78 -59
- package/keystore.mjs +64 -44
- package/narrate.mjs +34 -22
- package/package.json +4 -2
- package/preflight.mjs +191 -0
- package/sign.mjs +46 -27
- package/submit.mjs +166 -62
- package/tokenpull.mjs +337 -188
- package/tools.mjs +2154 -534
- package/tui.mjs +1860 -1081
package/connect.mjs
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
/** True iff this device identity is signed in (bound to an operator). */
|
|
6
6
|
export function isSignedIn(id) {
|
|
7
|
-
return !!(id && id.codename && id.operator_id)
|
|
7
|
+
return !!(id && id.codename && id.operator_id);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
/** True iff `s` is a single character a connect code can contain ([A-Za-z0-9-]). */
|
|
11
11
|
export function isCodeChar(s) {
|
|
12
|
-
return typeof s ===
|
|
12
|
+
return typeof s === "string" && /^[A-Za-z0-9-]$/.test(s);
|
|
13
13
|
}
|
package/index.mjs
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
* Token-only — no transcript content.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { Server } from
|
|
25
|
-
import { StdioServerTransport } from
|
|
24
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
25
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
26
26
|
import {
|
|
27
27
|
CallToolRequestSchema,
|
|
28
28
|
ListToolsRequestSchema,
|
|
@@ -30,130 +30,149 @@ import {
|
|
|
30
30
|
GetPromptRequestSchema,
|
|
31
31
|
McpError,
|
|
32
32
|
ErrorCode,
|
|
33
|
-
} from
|
|
34
|
-
import { TOOLS, callTool } from
|
|
35
|
-
import { runCli } from
|
|
36
|
-
import { readFileSync } from
|
|
33
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
34
|
+
import { TOOLS, callTool } from "./tools.mjs";
|
|
35
|
+
import { runCli } from "./cli.mjs";
|
|
36
|
+
import { readFileSync } from "node:fs";
|
|
37
37
|
|
|
38
38
|
// FIX K: read the version from package.json (was hardcoded '0.7.0' — drifted from
|
|
39
39
|
// 0.11.0). The MCP server identity now tracks the published package version.
|
|
40
40
|
function serverVersion() {
|
|
41
41
|
try {
|
|
42
|
-
const pkg = JSON.parse(
|
|
43
|
-
|
|
42
|
+
const pkg = JSON.parse(
|
|
43
|
+
readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
|
|
44
|
+
);
|
|
45
|
+
return pkg.version || "0.0.0";
|
|
44
46
|
} catch {
|
|
45
|
-
return
|
|
47
|
+
return "0.0.0";
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// Prevent silent crashes — log to stderr (MCP clients read stdout; stderr is safe for
|
|
50
52
|
// diagnostics). The process exits so the client can respawn with a clean slate rather
|
|
51
53
|
// than hanging on a broken connection.
|
|
52
|
-
process.on(
|
|
53
|
-
process.stderr.write(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
process.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
process.on("uncaughtException", (err) => {
|
|
55
|
+
process.stderr.write(
|
|
56
|
+
`[sigrank-mcp] uncaughtException: ${err?.stack || err}\n`,
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
});
|
|
60
|
+
process.on("unhandledRejection", (reason) => {
|
|
61
|
+
process.stderr.write(
|
|
62
|
+
`[sigrank-mcp] unhandledRejection: ${reason?.stack || reason}\n`,
|
|
63
|
+
);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
60
66
|
|
|
61
67
|
async function startMcpServer() {
|
|
62
68
|
const server = new Server(
|
|
63
|
-
{ name:
|
|
69
|
+
{ name: "sigrank", version: serverVersion() },
|
|
64
70
|
{ capabilities: { tools: {}, prompts: {} } },
|
|
65
|
-
)
|
|
66
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
71
|
+
);
|
|
72
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
73
|
+
tools: TOOLS,
|
|
74
|
+
}));
|
|
67
75
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
68
76
|
// Unknown tool name = a client/host bug → JSON-RPC -32602 protocol error, per the MCP
|
|
69
77
|
// spec. isError results (below) are reserved for tools that ran and failed.
|
|
70
78
|
if (!TOOLS.some((t) => t.name === req.params.name)) {
|
|
71
|
-
throw new McpError(
|
|
79
|
+
throw new McpError(
|
|
80
|
+
ErrorCode.InvalidParams,
|
|
81
|
+
`Unknown tool: ${req.params.name}`,
|
|
82
|
+
);
|
|
72
83
|
}
|
|
73
84
|
try {
|
|
74
|
-
const out = await callTool(req.params.name, req.params.arguments)
|
|
75
|
-
return {
|
|
85
|
+
const out = await callTool(req.params.name, req.params.arguments);
|
|
86
|
+
return {
|
|
87
|
+
content: [{ type: "text", text: JSON.stringify(out, null, 2) }],
|
|
88
|
+
};
|
|
76
89
|
} catch (e) {
|
|
77
|
-
return {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "text", text: `Error: ${e.message}` }],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
78
94
|
}
|
|
79
|
-
})
|
|
95
|
+
});
|
|
80
96
|
|
|
81
97
|
// --- Prompts: interactive templates that agents can offer to users ---
|
|
82
98
|
const PROMPTS = [
|
|
83
99
|
{
|
|
84
|
-
name:
|
|
100
|
+
name: "check-my-efficiency",
|
|
85
101
|
description:
|
|
86
|
-
|
|
102
|
+
"Pull your local token usage, compute your SigRank yield, and compare your efficiency against the leaderboard. Shows your class tier (Burner/Builder/10xer) and where you rank.",
|
|
87
103
|
arguments: [],
|
|
88
104
|
},
|
|
89
105
|
{
|
|
90
|
-
name:
|
|
106
|
+
name: "simulate-improvement",
|
|
91
107
|
description:
|
|
92
108
|
'Run a what-if analysis on your token mix: "if I increased my cache reads by 50k, how would my yield change?" Uses simulate_change to show the payoff before you change your workflow.',
|
|
93
109
|
arguments: [
|
|
94
110
|
{
|
|
95
|
-
name:
|
|
96
|
-
description:
|
|
111
|
+
name: "change",
|
|
112
|
+
description:
|
|
113
|
+
'The change to simulate (e.g. "increase cache reads by 50000" or "reduce input by 100000")',
|
|
97
114
|
required: true,
|
|
98
115
|
},
|
|
99
116
|
],
|
|
100
117
|
},
|
|
101
118
|
{
|
|
102
|
-
name:
|
|
119
|
+
name: "compare-with-leader",
|
|
103
120
|
description:
|
|
104
|
-
|
|
121
|
+
"Fetch the top operator from the SigRank leaderboard and compare their token efficiency metrics against yours. Shows the gap and what you'd need to change to close it.",
|
|
105
122
|
arguments: [],
|
|
106
123
|
},
|
|
107
|
-
]
|
|
124
|
+
];
|
|
108
125
|
|
|
109
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
126
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
127
|
+
prompts: PROMPTS,
|
|
128
|
+
}));
|
|
110
129
|
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
111
|
-
const name = req.params.name
|
|
112
|
-
const args = req.params.arguments || {}
|
|
113
|
-
if (name ===
|
|
130
|
+
const name = req.params.name;
|
|
131
|
+
const args = req.params.arguments || {};
|
|
132
|
+
if (name === "check-my-efficiency") {
|
|
114
133
|
return {
|
|
115
134
|
messages: [
|
|
116
135
|
{
|
|
117
|
-
role:
|
|
136
|
+
role: "user",
|
|
118
137
|
content: {
|
|
119
|
-
type:
|
|
120
|
-
text:
|
|
138
|
+
type: "text",
|
|
139
|
+
text: "Pull my local token usage with tokenpull, compute my yield, and then fetch the leaderboard so I can see where I rank. Show me my class tier and my key metrics (Yield, Leverage, Velocity).",
|
|
121
140
|
},
|
|
122
141
|
},
|
|
123
142
|
],
|
|
124
|
-
}
|
|
143
|
+
};
|
|
125
144
|
}
|
|
126
|
-
if (name ===
|
|
127
|
-
const change = args.change ||
|
|
145
|
+
if (name === "simulate-improvement") {
|
|
146
|
+
const change = args.change || "increase cache reads by 50000";
|
|
128
147
|
return {
|
|
129
148
|
messages: [
|
|
130
149
|
{
|
|
131
|
-
role:
|
|
150
|
+
role: "user",
|
|
132
151
|
content: {
|
|
133
|
-
type:
|
|
152
|
+
type: "text",
|
|
134
153
|
text: `First pull my local token usage with tokenpull to get my current pillars. Then use simulate_change to answer: "what if I ${change}?" Show me the yield delta and whether my class tier would change.`,
|
|
135
154
|
},
|
|
136
155
|
},
|
|
137
156
|
],
|
|
138
|
-
}
|
|
157
|
+
};
|
|
139
158
|
}
|
|
140
|
-
if (name ===
|
|
159
|
+
if (name === "compare-with-leader") {
|
|
141
160
|
return {
|
|
142
161
|
messages: [
|
|
143
162
|
{
|
|
144
|
-
role:
|
|
163
|
+
role: "user",
|
|
145
164
|
content: {
|
|
146
|
-
type:
|
|
147
|
-
text:
|
|
165
|
+
type: "text",
|
|
166
|
+
text: "Fetch the SigRank leaderboard with get_leaderboard, then pull my local token usage with tokenpull. Compare my yield, leverage, and velocity against the top operator. What's the gap and what would I need to change to close it?",
|
|
148
167
|
},
|
|
149
168
|
},
|
|
150
169
|
],
|
|
151
|
-
}
|
|
170
|
+
};
|
|
152
171
|
}
|
|
153
|
-
throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${name}`)
|
|
154
|
-
})
|
|
172
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown prompt: ${name}`);
|
|
173
|
+
});
|
|
155
174
|
|
|
156
|
-
await server.connect(new StdioServerTransport())
|
|
175
|
+
await server.connect(new StdioServerTransport());
|
|
157
176
|
}
|
|
158
177
|
|
|
159
178
|
// Route:
|
|
@@ -161,13 +180,13 @@ async function startMcpServer() {
|
|
|
161
180
|
// so a typo never silently starts a hung MCP stdio server on a TTY)
|
|
162
181
|
// no args + TTY stdout → full tabbed TUI (interactive use)
|
|
163
182
|
// no args + piped → MCP stdio server (AI client use)
|
|
164
|
-
const cliArgs = process.argv.slice(2)
|
|
183
|
+
const cliArgs = process.argv.slice(2);
|
|
165
184
|
if (cliArgs.length > 0) {
|
|
166
|
-
runCli(process.argv)
|
|
185
|
+
runCli(process.argv);
|
|
167
186
|
} else if (process.stdout.isTTY) {
|
|
168
187
|
// Interactive terminal — launch the full tabbed TUI
|
|
169
|
-
const { runTui } = await import(
|
|
170
|
-
runTui()
|
|
188
|
+
const { runTui } = await import("./tui.mjs");
|
|
189
|
+
runTui();
|
|
171
190
|
} else {
|
|
172
|
-
startMcpServer()
|
|
191
|
+
startMcpServer();
|
|
173
192
|
}
|
package/keystore.mjs
CHANGED
|
@@ -10,73 +10,84 @@
|
|
|
10
10
|
// and prefix "ed25519:". The server re-adds the SPKI prefix to verify. Round-trip is
|
|
11
11
|
// locked by sign.test.mjs + the canon-parity fixture.
|
|
12
12
|
|
|
13
|
-
import { homedir } from
|
|
14
|
-
import { join } from
|
|
15
|
-
import {
|
|
16
|
-
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
mkdirSync,
|
|
17
|
+
readFileSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
chmodSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { generateKeyPairSync, randomUUID } from "node:crypto";
|
|
17
24
|
|
|
18
|
-
const DIR = join(homedir(),
|
|
19
|
-
const PATH = join(DIR,
|
|
25
|
+
const DIR = join(homedir(), ".sigrank-mcp");
|
|
26
|
+
const PATH = join(DIR, "identity.json");
|
|
20
27
|
|
|
21
28
|
/** SPKI DER prefix length for an ed25519 public key (12 bytes before the raw 32). */
|
|
22
|
-
const SPKI_PREFIX_LEN = 12
|
|
29
|
+
const SPKI_PREFIX_LEN = 12;
|
|
23
30
|
|
|
24
31
|
/** Resolve this package's version for the agent_version stamp (best-effort). */
|
|
25
32
|
function agentVersion() {
|
|
26
33
|
try {
|
|
27
|
-
const pkg = JSON.parse(
|
|
28
|
-
|
|
34
|
+
const pkg = JSON.parse(
|
|
35
|
+
readFileSync(new URL("./package.json", import.meta.url), "utf-8"),
|
|
36
|
+
);
|
|
37
|
+
return `sigrank-mcp/${pkg.version}`;
|
|
29
38
|
} catch {
|
|
30
|
-
return
|
|
39
|
+
return "sigrank-mcp";
|
|
31
40
|
}
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
export function keystorePath() {
|
|
35
|
-
return PATH
|
|
44
|
+
return PATH;
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/** Read the stored identity, or null if absent/corrupt. */
|
|
39
48
|
export function loadIdentity() {
|
|
40
|
-
if (!existsSync(PATH)) return null
|
|
49
|
+
if (!existsSync(PATH)) return null;
|
|
41
50
|
try {
|
|
42
|
-
return JSON.parse(readFileSync(PATH,
|
|
51
|
+
return JSON.parse(readFileSync(PATH, "utf-8"));
|
|
43
52
|
} catch {
|
|
44
|
-
return null
|
|
53
|
+
return null;
|
|
45
54
|
}
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
/** Write the identity with strict perms (dir 0700, file 0600). */
|
|
49
58
|
export function persistIdentity(identity) {
|
|
50
|
-
mkdirSync(DIR, { recursive: true })
|
|
59
|
+
mkdirSync(DIR, { recursive: true });
|
|
51
60
|
try {
|
|
52
|
-
chmodSync(DIR, 0o700)
|
|
61
|
+
chmodSync(DIR, 0o700);
|
|
53
62
|
} catch {
|
|
54
63
|
/* best-effort on platforms without chmod */
|
|
55
64
|
}
|
|
56
|
-
writeFileSync(PATH, `${JSON.stringify(identity, null, 2)}\n`, {
|
|
65
|
+
writeFileSync(PATH, `${JSON.stringify(identity, null, 2)}\n`, {
|
|
66
|
+
mode: 0o600,
|
|
67
|
+
});
|
|
57
68
|
try {
|
|
58
|
-
chmodSync(PATH, 0o600)
|
|
69
|
+
chmodSync(PATH, 0o600);
|
|
59
70
|
} catch {
|
|
60
71
|
/* best-effort */
|
|
61
72
|
}
|
|
62
|
-
return identity
|
|
73
|
+
return identity;
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
/** Build a fresh keypair record (pure — no fs). Exposed for tests. */
|
|
66
77
|
export function generateIdentity({ device_id } = {}) {
|
|
67
|
-
const { publicKey, privateKey } = generateKeyPairSync(
|
|
68
|
-
const spki = publicKey.export({ type:
|
|
69
|
-
const rawPub = Buffer.from(spki).subarray(SPKI_PREFIX_LEN) // the raw 32 bytes
|
|
70
|
-
const pkcs8 = privateKey.export({ type:
|
|
78
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
79
|
+
const spki = publicKey.export({ type: "spki", format: "der" });
|
|
80
|
+
const rawPub = Buffer.from(spki).subarray(SPKI_PREFIX_LEN); // the raw 32 bytes
|
|
81
|
+
const pkcs8 = privateKey.export({ type: "pkcs8", format: "der" });
|
|
71
82
|
return {
|
|
72
83
|
device_id: device_id || randomUUID(),
|
|
73
84
|
codename: null,
|
|
74
85
|
operator_id: null,
|
|
75
|
-
public_key: `ed25519:${rawPub.toString(
|
|
76
|
-
private_key_pkcs8_b64: Buffer.from(pkcs8).toString(
|
|
86
|
+
public_key: `ed25519:${rawPub.toString("base64")}`,
|
|
87
|
+
private_key_pkcs8_b64: Buffer.from(pkcs8).toString("base64"),
|
|
77
88
|
agent_version: agentVersion(),
|
|
78
89
|
enrolled_at: null,
|
|
79
|
-
}
|
|
90
|
+
};
|
|
80
91
|
}
|
|
81
92
|
|
|
82
93
|
/**
|
|
@@ -91,16 +102,17 @@ export function generateIdentity({ device_id } = {}) {
|
|
|
91
102
|
* tests (no fs, so the owner's live identity is never at risk during a test run).
|
|
92
103
|
*/
|
|
93
104
|
export function bindingForFreshIdentity(existing, fresh) {
|
|
94
|
-
if (!existing)
|
|
105
|
+
if (!existing)
|
|
106
|
+
return { codename: null, operator_id: null, enrolled_at: null };
|
|
95
107
|
if (existing.device_id && fresh.device_id === existing.device_id) {
|
|
96
108
|
return {
|
|
97
109
|
codename: existing.codename ?? null,
|
|
98
110
|
operator_id: existing.operator_id ?? null,
|
|
99
111
|
enrolled_at: existing.enrolled_at ?? null,
|
|
100
|
-
}
|
|
112
|
+
};
|
|
101
113
|
}
|
|
102
114
|
// device_id changed → the old binding belongs to a different device; drop it.
|
|
103
|
-
return { codename: null, operator_id: null, enrolled_at: null }
|
|
115
|
+
return { codename: null, operator_id: null, enrolled_at: null };
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
/**
|
|
@@ -108,35 +120,43 @@ export function bindingForFreshIdentity(existing, fresh) {
|
|
|
108
120
|
* complete existing identity is returned untouched (never rotates a live key).
|
|
109
121
|
*/
|
|
110
122
|
export function ensureIdentity() {
|
|
111
|
-
const existing = loadIdentity()
|
|
112
|
-
if (
|
|
113
|
-
|
|
123
|
+
const existing = loadIdentity();
|
|
124
|
+
if (
|
|
125
|
+
existing?.private_key_pkcs8_b64 &&
|
|
126
|
+
existing?.public_key &&
|
|
127
|
+
existing?.device_id
|
|
128
|
+
) {
|
|
129
|
+
return existing;
|
|
114
130
|
}
|
|
115
131
|
// Regeneration path: a partial/corrupt record. The binding is tied to device_id —
|
|
116
132
|
// if a new device_id is generated, the old codename/operator MUST NOT carry over
|
|
117
133
|
// (bindingForFreshIdentity drops them). Only a reused device_id keeps its binding.
|
|
118
|
-
const fresh = generateIdentity({ device_id: existing?.device_id })
|
|
119
|
-
const binding = bindingForFreshIdentity(existing, fresh)
|
|
120
|
-
fresh.codename = binding.codename
|
|
121
|
-
fresh.operator_id = binding.operator_id
|
|
122
|
-
fresh.enrolled_at = binding.enrolled_at
|
|
123
|
-
return persistIdentity(fresh)
|
|
134
|
+
const fresh = generateIdentity({ device_id: existing?.device_id });
|
|
135
|
+
const binding = bindingForFreshIdentity(existing, fresh);
|
|
136
|
+
fresh.codename = binding.codename;
|
|
137
|
+
fresh.operator_id = binding.operator_id;
|
|
138
|
+
fresh.enrolled_at = binding.enrolled_at;
|
|
139
|
+
return persistIdentity(fresh);
|
|
124
140
|
}
|
|
125
141
|
|
|
126
142
|
/** Record a successful enrollment (codename/operator_id + enrolled_at) into the keystore. */
|
|
127
143
|
export function recordEnrollment({ codename, operator_id }) {
|
|
128
|
-
const id = ensureIdentity()
|
|
144
|
+
const id = ensureIdentity();
|
|
129
145
|
// OVERWRITE from the server's enroll response — a new enroll is a new binding, full
|
|
130
146
|
// stop. Never `??`-preserve a stale codename/operator onto this device (the other
|
|
131
147
|
// half of the Frankenstein bug: a re-enroll used to keep the old codename when the
|
|
132
148
|
// new one was absent). A 201 enrolled always carries both.
|
|
133
|
-
id.codename = codename ?? null
|
|
134
|
-
id.operator_id = operator_id ?? null
|
|
135
|
-
id.enrolled_at = new Date().toISOString()
|
|
136
|
-
return persistIdentity(id)
|
|
149
|
+
id.codename = codename ?? null;
|
|
150
|
+
id.operator_id = operator_id ?? null;
|
|
151
|
+
id.enrolled_at = new Date().toISOString();
|
|
152
|
+
return persistIdentity(id);
|
|
137
153
|
}
|
|
138
154
|
|
|
139
155
|
/** Clear the local identity (sign out). Next enroll provisions a fresh device_id. */
|
|
140
156
|
export function clearIdentity() {
|
|
141
|
-
try {
|
|
157
|
+
try {
|
|
158
|
+
if (existsSync(PATH)) unlinkSync(PATH);
|
|
159
|
+
} catch {
|
|
160
|
+
/* best-effort */
|
|
161
|
+
}
|
|
142
162
|
}
|
package/narrate.mjs
CHANGED
|
@@ -11,55 +11,67 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
// Safe formatters: never emit NaN/Infinity/undefined into the card.
|
|
14
|
-
const safeNum = (n) => (Number.isFinite(Number(n)) ? Number(n) : null)
|
|
14
|
+
const safeNum = (n) => (Number.isFinite(Number(n)) ? Number(n) : null);
|
|
15
15
|
const comma = (n, dec) => {
|
|
16
|
-
const v = safeNum(n)
|
|
17
|
-
return v !== null
|
|
18
|
-
|
|
16
|
+
const v = safeNum(n);
|
|
17
|
+
return v !== null
|
|
18
|
+
? v.toLocaleString("en-US", {
|
|
19
|
+
minimumFractionDigits: dec,
|
|
20
|
+
maximumFractionDigits: dec,
|
|
21
|
+
})
|
|
22
|
+
: "—";
|
|
23
|
+
};
|
|
19
24
|
const plain = (n, dec) => {
|
|
20
|
-
const v = safeNum(n)
|
|
21
|
-
return v !== null ? v.toFixed(dec) :
|
|
22
|
-
}
|
|
25
|
+
const v = safeNum(n);
|
|
26
|
+
return v !== null ? v.toFixed(dec) : "—";
|
|
27
|
+
};
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Given a cascade result ({ velocity, leverage, dev10x, pillars, class }) and an
|
|
26
31
|
* optional subject name, return "**CLASS.** <one or two sentences>". Deterministic.
|
|
27
32
|
*/
|
|
28
|
-
export function narrate(cascade, name =
|
|
29
|
-
const klass = cascade.class || cascade.klass ||
|
|
30
|
-
const v = safeNum(cascade.velocity)
|
|
31
|
-
const l = safeNum(cascade.leverage)
|
|
33
|
+
export function narrate(cascade, name = "This operator") {
|
|
34
|
+
const klass = cascade.class || cascade.klass || "UNCLASSED";
|
|
35
|
+
const v = safeNum(cascade.velocity);
|
|
36
|
+
const l = safeNum(cascade.leverage);
|
|
32
37
|
|
|
33
38
|
// "non-compounding" = a stateless pipe: no cache commits, so the cascade can't
|
|
34
39
|
// form. cascade.mjs leaves dev10x null when cacheCreate is 0 (the cw/o term
|
|
35
40
|
// collapses), which is exactly metrics.py's non_compounding flag.
|
|
36
41
|
// Also catches zero-input sessions where velocity/leverage are null.
|
|
37
|
-
const cw = cascade.pillars ? Number(cascade.pillars.cacheCreate) : NaN
|
|
38
|
-
const nonCompounding =
|
|
42
|
+
const cw = cascade.pillars ? Number(cascade.pillars.cacheCreate) : NaN;
|
|
43
|
+
const nonCompounding =
|
|
44
|
+
cascade.dev10x == null || !(cw > 0) || v === null || l === null;
|
|
39
45
|
|
|
40
|
-
let body
|
|
46
|
+
let body;
|
|
41
47
|
if (nonCompounding) {
|
|
42
|
-
const leverageStr =
|
|
43
|
-
|
|
48
|
+
const leverageStr =
|
|
49
|
+
l !== null
|
|
50
|
+
? `Leverage ${comma(l, 1)}x comes from reuse alone.`
|
|
51
|
+
: "Leverage is undefined (no fresh input recorded).";
|
|
52
|
+
const dev10xNote =
|
|
53
|
+
cascade.dev10x == null
|
|
54
|
+
? " 10xDEV is undefined — the compounding loop has not formed yet."
|
|
55
|
+
: "";
|
|
44
56
|
body =
|
|
45
57
|
`${name} runs a stateless pipe — no cache commits, so the cascade can't form. ` +
|
|
46
|
-
`High read volume, but nothing is being built forward. ${leverageStr}${dev10xNote}
|
|
58
|
+
`High read volume, but nothing is being built forward. ${leverageStr}${dev10xNote}`;
|
|
47
59
|
} else if (v >= 1 && l >= 100) {
|
|
48
60
|
body =
|
|
49
61
|
`${name} holds both axes at once: ${plain(v, 1)}x generation AND ${comma(l, 0)}x memory leverage. ` +
|
|
50
|
-
`A closed kinetic loop — the rare operator the leverage/generation tradeoff says shouldn't exist
|
|
62
|
+
`A closed kinetic loop — the rare operator the leverage/generation tradeoff says shouldn't exist.`;
|
|
51
63
|
} else if (l >= 10 && v < 1) {
|
|
52
64
|
body =
|
|
53
65
|
`${name} is an archival sponge — ${comma(l, 0)}x reuse but only ${plain(v, 2)}x generation. ` +
|
|
54
|
-
`Holds context beautifully, executes little with it. The reuse number is inflated by a weak commitment stage
|
|
66
|
+
`Holds context beautifully, executes little with it. The reuse number is inflated by a weak commitment stage.`;
|
|
55
67
|
} else if (v >= 0.8 && l < 2) {
|
|
56
68
|
body =
|
|
57
69
|
`${name} is a volatile ingestor — ${plain(v, 2)}x generation but ${plain(l, 1)}x leverage. ` +
|
|
58
|
-
`Fast on single shots, resets between turns. Memory doesn't persist into a compounding loop
|
|
70
|
+
`Fast on single shots, resets between turns. Memory doesn't persist into a compounding loop.`;
|
|
59
71
|
} else {
|
|
60
72
|
body =
|
|
61
73
|
`${name} sits low on both axes: ${plain(v, 2)}x generation, ${plain(l, 1)}x leverage. ` +
|
|
62
|
-
`A transient profile — neither building state nor converting input to output efficiently
|
|
74
|
+
`A transient profile — neither building state nor converting input to output efficiently.`;
|
|
63
75
|
}
|
|
64
|
-
return `**${klass}.** ${body}
|
|
76
|
+
return `**${klass}.** ${body}`;
|
|
65
77
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigrank",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "SigRank MCP server — the yield cascade + live leaderboard as MCP tools any agent can call",
|
|
5
5
|
"mcpName": "io.github.SunrisesIllNeverSee/sigrank-mcp",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
25
|
"start": "node index.mjs",
|
|
26
|
-
"test": "node test.mjs && node sign.test.mjs"
|
|
26
|
+
"test": "node test.mjs && node sign.test.mjs && node --test __tests__/cascade.test.mjs __tests__/badges.test.mjs"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"index.mjs",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"keystore.mjs",
|
|
39
39
|
"submit.mjs",
|
|
40
40
|
"connect.mjs",
|
|
41
|
+
"badges.mjs",
|
|
42
|
+
"preflight.mjs",
|
|
41
43
|
"banner.svg",
|
|
42
44
|
"README.md",
|
|
43
45
|
"LICENSE"
|