sigrank 0.17.1 → 0.17.3

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
@@ -25,6 +25,10 @@ For all builders, burners and 10xers.
25
25
 
26
26
  </div>
27
27
 
28
+ <p align="center">
29
+ <a href="https://www.npmjs.com/package/sigrank"><img src="https://img.shields.io/badge/$%20npx%20sigrank-gold?style=for-the-badge&logo=npm&logoColor=white&labelColor=1a1a1a&color=daa520" alt="npx sigrank" /></a>
30
+ </p>
31
+
28
32
  ## Table of Contents
29
33
 
30
34
  - [The SigRank ecosystem](#the-sigrank-ecosystem)
@@ -143,7 +147,7 @@ smithery tool call sigrank rank_paste '{"text": "1000000 500000 50000 800000"}'
143
147
  ## Commands
144
148
 
145
149
  ```
146
- ⊙ SigRank CLI v0.17.0
150
+ ⊙ SigRank CLI v0.17.3
147
151
 
148
152
  Default (no args)
149
153
  sigrank unified dashboard: cascade + token pillars + board
@@ -242,7 +246,7 @@ Or if installed globally:
242
246
  }
243
247
  ```
244
248
 
245
- ### MCP Tools
249
+ ## Tools
246
250
 
247
251
  | Tool | Args | What |
248
252
  | ------------------------------------- | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -261,6 +265,11 @@ Or if installed globally:
261
265
  | `simulate_change` | `{text?, changes}` | Prescriptive "what if" — test proposed pillar changes and see the exact Υ delta + class change before committing |
262
266
  | `suggest_improvements` | `{text?}` | Generates ranked, simulated improvement suggestions — tests strategies and returns them sorted by Υ yield impact |
263
267
  | `self_improve` | `{text?}` | One-click optimize: diagnoses, suggests, and simulates the best change in a single call |
268
+ | `get_best_operator(n?)` | `{n?}` | Top N operators with behavioral framing in power-user language. Intent: "who is the best AI user?" |
269
+ | `compare_self(codename? \| text?)` | `{codename?}` or `{text?}` | Your metrics vs board averages + power-user assessment + percentile + suggestion. Intent: "how do I measure up?" |
270
+ | `compare_operators(a, b)` | `{codename_a, codename_b}` | Side-by-side comparison with behavioral verdict. Intent: "compare operator X vs Y" |
271
+ | `describe_power_user()` | `{}` | Static explanation of AI power user archetype + metrics explained. Intent: "what is an AI power user?" |
272
+ | `optimize_efficiency(codename? \| text?)` | `{codename?}` or `{text?}` | Ranked efficiency suggestions tied to your cascade shape. Intent: "how can I use AI more efficiently?" |
264
273
 
265
274
  ---
266
275
 
package/index.mjs CHANGED
@@ -69,7 +69,7 @@ process.on("unhandledRejection", (reason) => {
69
69
  async function startMcpServer() {
70
70
  const server = new Server(
71
71
  { name: "sigrank", version: serverVersion() },
72
- { capabilities: { tools: { listChanged: false } } },
72
+ { capabilities: { tools: { listChanged: false }, prompts: { listChanged: false }, resources: { listChanged: false } } },
73
73
  );
74
74
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
75
75
  tools: TOOLS,
package/keystore.mjs CHANGED
@@ -19,11 +19,15 @@ import {
19
19
  existsSync,
20
20
  chmodSync,
21
21
  unlinkSync,
22
+ readdirSync,
23
+ copyFileSync,
22
24
  } from "node:fs";
23
25
  import { generateKeyPairSync, randomUUID } from "node:crypto";
24
26
 
25
27
  const DIR = join(homedir(), ".sigrank-mcp");
26
28
  const PATH = join(DIR, "identity.json");
29
+ const BACKUP_PREFIX = "identity.json.bak-";
30
+ const MAX_BACKUPS = 5;
27
31
 
28
32
  /** SPKI DER prefix length for an ed25519 public key (12 bytes before the raw 32). */
29
33
  const SPKI_PREFIX_LEN = 12;
@@ -54,7 +58,8 @@ export function loadIdentity() {
54
58
  }
55
59
  }
56
60
 
57
- /** Write the identity with strict perms (dir 0700, file 0600). */
61
+ /** Write the identity with strict perms (dir 0700, file 0600). Backs up the existing
62
+ * file before overwriting so a corrupt write or accidental reset can be recovered. */
58
63
  export function persistIdentity(identity) {
59
64
  mkdirSync(DIR, { recursive: true });
60
65
  try {
@@ -62,6 +67,17 @@ export function persistIdentity(identity) {
62
67
  } catch {
63
68
  /* best-effort on platforms without chmod */
64
69
  }
70
+ // Back up the existing identity before overwriting (resilience: if the write
71
+ // fails or the file is later lost, the backup carries the binding + keys).
72
+ if (existsSync(PATH)) {
73
+ try {
74
+ const stamp = Date.now();
75
+ copyFileSync(PATH, join(DIR, `${BACKUP_PREFIX}${stamp}`));
76
+ pruneBackups();
77
+ } catch {
78
+ /* best-effort — don't block the write */
79
+ }
80
+ }
65
81
  writeFileSync(PATH, `${JSON.stringify(identity, null, 2)}\n`, {
66
82
  mode: 0o600,
67
83
  });
@@ -118,6 +134,9 @@ export function bindingForFreshIdentity(existing, fresh) {
118
134
  /**
119
135
  * Load the existing identity, or generate + persist a fresh one. Idempotent: a
120
136
  * complete existing identity is returned untouched (never rotates a live key).
137
+ * Self-healing: if the identity has keys + device_id but no codename (binding
138
+ * lost via partial write / version transition), scans backups for a matching
139
+ * device_id and restores the binding before returning.
121
140
  */
122
141
  export function ensureIdentity() {
123
142
  const existing = loadIdentity();
@@ -126,6 +145,16 @@ export function ensureIdentity() {
126
145
  existing?.public_key &&
127
146
  existing?.device_id
128
147
  ) {
148
+ // Self-healing: keys present but binding lost → try to restore from backups.
149
+ if (!existing.codename || !existing.operator_id) {
150
+ const restored = restoreBindingFromBackups(existing.device_id);
151
+ if (restored) {
152
+ existing.codename = restored.codename;
153
+ existing.operator_id = restored.operator_id;
154
+ existing.enrolled_at = restored.enrolled_at;
155
+ return persistIdentity(existing);
156
+ }
157
+ }
129
158
  return existing;
130
159
  }
131
160
  // Regeneration path: a partial/corrupt record. The binding is tied to device_id —
@@ -160,3 +189,54 @@ export function clearIdentity() {
160
189
  /* best-effort */
161
190
  }
162
191
  }
192
+
193
+ /** Keep at most MAX_BACKUPS backup files (oldest deleted first). */
194
+ function pruneBackups() {
195
+ try {
196
+ const files = readdirSync(DIR)
197
+ .filter((f) => f.startsWith(BACKUP_PREFIX))
198
+ .sort(); // timestamp-suffixed → lexicographic = chronological
199
+ while (files.length > MAX_BACKUPS) {
200
+ unlinkSync(join(DIR, files.shift()));
201
+ }
202
+ } catch {
203
+ /* best-effort */
204
+ }
205
+ }
206
+
207
+ /** Scan backup files for one with the same device_id that carries a codename +
208
+ * operator_id binding. Returns { codename, operator_id, enrolled_at } or null.
209
+ * Used by ensureIdentity() to self-heal when the live file lost its binding
210
+ * (e.g. a partial write or version transition that preserved keys but dropped
211
+ * the binding metadata). Only restores from a backup with the SAME device_id —
212
+ * never crosses device boundaries (Frankenstein-identity guard). */
213
+ export function restoreBindingFromBackups(device_id) {
214
+ if (!device_id) return null;
215
+ try {
216
+ const files = readdirSync(DIR)
217
+ .filter((f) => f.startsWith(BACKUP_PREFIX))
218
+ .sort()
219
+ .reverse(); // newest first — prefer the most recent binding
220
+ for (const f of files) {
221
+ try {
222
+ const bak = JSON.parse(readFileSync(join(DIR, f), "utf-8"));
223
+ if (
224
+ bak.device_id === device_id &&
225
+ bak.codename &&
226
+ bak.operator_id
227
+ ) {
228
+ return {
229
+ codename: bak.codename,
230
+ operator_id: bak.operator_id,
231
+ enrolled_at: bak.enrolled_at ?? null,
232
+ };
233
+ }
234
+ } catch {
235
+ /* skip corrupt backups */
236
+ }
237
+ }
238
+ } catch {
239
+ /* best-effort */
240
+ }
241
+ return null;
242
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigrank",
3
- "version": "0.17.1",
3
+ "version": "0.17.3",
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
@@ -351,6 +351,106 @@ const OPERATOR_OUTPUT = {
351
351
  },
352
352
  };
353
353
 
354
+ const BEST_OPERATOR_OUTPUT = {
355
+ type: "object",
356
+ properties: {
357
+ top_operators: {
358
+ type: "array",
359
+ description: "Top N operators ranked by yield",
360
+ items: {
361
+ type: "object",
362
+ properties: {
363
+ codename: { type: "string", description: "Public display name" },
364
+ yield_: { type: "number", description: "Υ Yield metric" },
365
+ leverage: { type: "number", description: "Cr/I ratio" },
366
+ velocity: { type: "number", description: "O/I ratio" },
367
+ class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
368
+ rank: { type: "integer", description: "1-based rank position" },
369
+ behavioral_framing: {
370
+ type: "string",
371
+ description: "Plain-language interpretation of the operator's cascade in power-user terms",
372
+ },
373
+ },
374
+ },
375
+ },
376
+ total_operators: { type: "integer", description: "Total operators on the board" },
377
+ summary: {
378
+ type: "string",
379
+ description: "One-line summary of the top operator's achievement in behavioral terms",
380
+ },
381
+ },
382
+ };
383
+
384
+ const COMPARE_SELF_OUTPUT = {
385
+ type: "object",
386
+ properties: {
387
+ your_metrics: {
388
+ type: "object",
389
+ description: "Your cascade metrics",
390
+ properties: {
391
+ codename: { type: "string" },
392
+ yield_: { type: "number", description: "Υ Yield metric" },
393
+ leverage: { type: "number", description: "Cr/I ratio" },
394
+ velocity: { type: "number", description: "O/I ratio" },
395
+ class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
396
+ rank: { type: "integer", description: "1-based rank position" },
397
+ },
398
+ },
399
+ power_user_assessment: {
400
+ type: "string",
401
+ description: "Behavioral interpretation: are you an AI power user? Maps class tier to power-user language.",
402
+ },
403
+ comparison: {
404
+ type: "object",
405
+ description: "How you compare to board averages and archetypes",
406
+ properties: {
407
+ your_yield_vs_avg: { type: "string", description: "Your yield vs board average" },
408
+ your_class_meaning: { type: "string", description: "What your class tier means in power-user terms" },
409
+ percentile: { type: "number", description: "Your percentile rank (0-100)" },
410
+ },
411
+ },
412
+ suggestion: {
413
+ type: "string",
414
+ description: "One actionable suggestion to improve your cascade efficiency",
415
+ },
416
+ },
417
+ };
418
+
419
+ const COMPARE_OPERATORS_OUTPUT = {
420
+ type: "object",
421
+ properties: {
422
+ operator_a: {
423
+ type: "object",
424
+ description: "First operator's metrics",
425
+ properties: {
426
+ codename: { type: "string" },
427
+ yield_: { type: "number" },
428
+ leverage: { type: "number" },
429
+ velocity: { type: "number" },
430
+ class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
431
+ rank: { type: "integer" },
432
+ },
433
+ },
434
+ operator_b: {
435
+ type: "object",
436
+ description: "Second operator's metrics",
437
+ properties: {
438
+ codename: { type: "string" },
439
+ yield_: { type: "number" },
440
+ leverage: { type: "number" },
441
+ velocity: { type: "number" },
442
+ class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
443
+ rank: { type: "integer" },
444
+ },
445
+ },
446
+ verdict: {
447
+ type: "string",
448
+ description: "Who is more efficient and why, in behavioral terms",
449
+ },
450
+ yield_delta: { type: "number", description: "Yield difference (A - B)" },
451
+ },
452
+ };
453
+
354
454
  const SUBMIT_OUTPUT = {
355
455
  type: "object",
356
456
  properties: {
@@ -1052,6 +1152,166 @@ export const TOOLS = [
1052
1152
  },
1053
1153
  },
1054
1154
  },
1155
+ {
1156
+ name: "get_best_operator",
1157
+ description:
1158
+ "Returns the top N operators on the SigRank leaderboard with behavioral framing in power-user language. Wraps get_leaderboard and adds plain-language interpretation of each top operator's cascade: what their yield, leverage, and velocity mean in terms of AI power-user behavior (cache reuse, input economy, output productivity). Use this when users ask 'who is the best AI user?' or 'who tops the SigRank leaderboard?' or 'show me the AI user leaderboard'. Do NOT use get_leaderboard if you want the raw array without interpretation — use this for the power-user framing. Intent: BEST_OPERATOR.",
1159
+ annotations: { title: "Get best operator", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
1160
+ inputSchema: {
1161
+ type: "object",
1162
+ properties: {
1163
+ n: {
1164
+ type: "integer",
1165
+ description:
1166
+ "Number of top operators to return (default: 5, max: 20). Returns the top N by yield.",
1167
+ minimum: 1,
1168
+ maximum: 20,
1169
+ },
1170
+ },
1171
+ description:
1172
+ "Optional: how many top operators to return. Defaults to 5.",
1173
+ },
1174
+ outputSchema: BEST_OPERATOR_OUTPUT,
1175
+ },
1176
+ {
1177
+ name: "compare_self",
1178
+ description:
1179
+ "Compares an operator's metrics against board averages and power-user archetypes, returning a behavioral assessment. Accepts either a codename (fetches from the board) or raw token pillars (computes locally). Returns: your yield/leverage/velocity/class/rank, a power-user assessment mapping your class tier to AI power-user language, comparison vs board averages (your percentile), and one actionable suggestion to improve. Use this when users ask 'how do I measure up to other AI users?' or 'am I a power user?' or 'compare me to others'. Intent: COMPARE_SELF.",
1180
+ annotations: { title: "Compare self to board", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
1181
+ inputSchema: {
1182
+ type: "object",
1183
+ properties: {
1184
+ codename: {
1185
+ type: "string",
1186
+ description:
1187
+ "Your codename on the SigRank leaderboard. If provided, fetches your live profile from the board. Case-insensitive.",
1188
+ },
1189
+ text: {
1190
+ type: "string",
1191
+ description:
1192
+ 'Alternative: raw token pillars to score locally (ccusage JSON or "input output cacheCreate cacheRead"). Use this if you are not on the board yet but want to see how you would compare.',
1193
+ },
1194
+ },
1195
+ description:
1196
+ "Provide either `codename` (to fetch from the board) or `text` (to score locally). At least one is required.",
1197
+ },
1198
+ outputSchema: COMPARE_SELF_OUTPUT,
1199
+ },
1200
+ {
1201
+ name: "compare_operators",
1202
+ description:
1203
+ "Compares two operators side-by-side with a behavioral verdict. Fetches both profiles from the board and returns their yield, leverage, velocity, class, and rank side-by-side, plus a verdict explaining who is more efficient and why in power-user language. Use this when users ask 'compare operator X vs Y' or 'who is more efficient' or 'how do two AI users compare'. Intent: COMPARE_OPERATORS.",
1204
+ annotations: { title: "Compare two operators", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.openWorldHint },
1205
+ inputSchema: {
1206
+ type: "object",
1207
+ properties: {
1208
+ codename_a: {
1209
+ type: "string",
1210
+ description:
1211
+ "First operator's codename from the SigRank leaderboard. Case-insensitive.",
1212
+ },
1213
+ codename_b: {
1214
+ type: "string",
1215
+ description:
1216
+ "Second operator's codename from the SigRank leaderboard. Case-insensitive.",
1217
+ },
1218
+ },
1219
+ required: ["codename_a", "codename_b"],
1220
+ description:
1221
+ "Requires both codenames. Both must exist on the board.",
1222
+ },
1223
+ outputSchema: COMPARE_OPERATORS_OUTPUT,
1224
+ },
1225
+ {
1226
+ name: "describe_power_user",
1227
+ description:
1228
+ "Returns an explanatory description of what makes an AI power user, anchored in SigRank's metrics and operator classes. Explains the yield metric, leverage, velocity, and how class tiers (Burner/Builder/10xer) map to power-user behavior patterns. Use this when users ask 'what is an AI power user?' or 'what makes a good AI user?' or 'describe advanced AI user behavior'. Intent: DESCRIBE_POWER_USER (Informational).",
1229
+ annotations: { title: "Describe power user", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint },
1230
+ inputSchema: {
1231
+ type: "object",
1232
+ properties: {},
1233
+ description:
1234
+ "This tool takes no parameters. It returns a static explanatory response about AI power users.",
1235
+ },
1236
+ outputSchema: {
1237
+ type: "object",
1238
+ properties: {
1239
+ description: { type: "string", description: "What is an AI power user" },
1240
+ metrics_explained: {
1241
+ type: "object",
1242
+ description: "How SigRank metrics map to power-user behavior",
1243
+ properties: {
1244
+ yield_: { type: "string", description: "What yield measures in power-user terms" },
1245
+ leverage: { type: "string", description: "What leverage means for power users" },
1246
+ velocity: { type: "string", description: "What velocity means for power users" },
1247
+ },
1248
+ },
1249
+ class_tiers: {
1250
+ type: "array",
1251
+ description: "Operator class tiers and their power-user meaning",
1252
+ items: {
1253
+ type: "object",
1254
+ properties: {
1255
+ class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
1256
+ meaning: { type: "string" },
1257
+ },
1258
+ },
1259
+ },
1260
+ link: { type: "string", description: "URL to learn more" },
1261
+ },
1262
+ },
1263
+ },
1264
+ {
1265
+ name: "optimize_efficiency",
1266
+ description:
1267
+ "Returns actionable suggestions for improving your token cascade efficiency, tied to your current metrics. Accepts either a codename (fetches from board) or raw token pillars (computes locally). Returns: your current metrics, ranked efficiency suggestions tied to cascade shape (increase cache reuse, reduce input, increase output), and references to power-user practices. Use this when users ask 'how can I use AI more efficiently?' or 'reduce token burn' or 'optimize token usage' or 'stop tokenmaxxing'. Intent: OPTIMIZE_EFFICIENCY (Informational + Transactional).",
1268
+ annotations: { title: "Optimize efficiency", ...ANNOTATIONS.readOnlyHint, ...ANNOTATIONS.idempotentHint },
1269
+ inputSchema: {
1270
+ type: "object",
1271
+ properties: {
1272
+ codename: {
1273
+ type: "string",
1274
+ description:
1275
+ "Your codename on the SigRank leaderboard. If provided, fetches your live profile from the board.",
1276
+ },
1277
+ text: {
1278
+ type: "string",
1279
+ description:
1280
+ 'Alternative: raw token pillars to score locally (ccusage JSON or "input output cacheCreate cacheRead"). Use this if you are not on the board yet.',
1281
+ },
1282
+ },
1283
+ description:
1284
+ "Provide either `codename` (to fetch from the board) or `text` (to score locally). At least one is required.",
1285
+ },
1286
+ outputSchema: {
1287
+ type: "object",
1288
+ properties: {
1289
+ your_metrics: {
1290
+ type: "object",
1291
+ description: "Your current cascade metrics",
1292
+ properties: {
1293
+ yield_: { type: "number" },
1294
+ leverage: { type: "number" },
1295
+ velocity: { type: "number" },
1296
+ class: { type: "string", enum: ["Burner", "Builder", "10xer"] },
1297
+ },
1298
+ },
1299
+ suggestions: {
1300
+ type: "array",
1301
+ description: "Ranked efficiency suggestions",
1302
+ items: {
1303
+ type: "object",
1304
+ properties: {
1305
+ action: { type: "string", description: "What to change" },
1306
+ why: { type: "string", description: "Why this helps your yield" },
1307
+ power_user_practice: { type: "string", description: "The power-user practice this maps to" },
1308
+ },
1309
+ },
1310
+ },
1311
+ summary: { type: "string", description: "One-line summary of your efficiency status" },
1312
+ },
1313
+ },
1314
+ },
1055
1315
  ];
1056
1316
 
1057
1317
  // tokenpull window key → the board's window_type enum.
@@ -1201,6 +1461,30 @@ export async function callTool(name, args, opts = {}) {
1201
1461
  trust_status: ack.trust_status ?? "trusted",
1202
1462
  };
1203
1463
  }
1464
+ // Recovery: if the server says device_already_enrolled but includes the
1465
+ // codename/operator_id, the device IS bound server-side — record it locally
1466
+ // and return enrolled instead of erroring. This handles the case where the
1467
+ // local binding was lost (partial write, version transition) but the device
1468
+ // is still enrolled server-side.
1469
+ if (
1470
+ ack.reason === "device_already_enrolled" &&
1471
+ ack.codename &&
1472
+ ack.operator_id
1473
+ ) {
1474
+ if (!opts.identity)
1475
+ recordEnrollment({
1476
+ codename: ack.codename,
1477
+ operator_id: ack.operator_id,
1478
+ });
1479
+ return {
1480
+ status: "enrolled",
1481
+ codename: ack.codename,
1482
+ operator_id: ack.operator_id,
1483
+ device_id: id.device_id,
1484
+ trust_status: ack.trust_status ?? "trusted",
1485
+ recovered: true,
1486
+ };
1487
+ }
1204
1488
  return {
1205
1489
  status: "error",
1206
1490
  httpStatus: res.status,
@@ -2474,9 +2758,296 @@ export async function callTool(name, args, opts = {}) {
2474
2758
  };
2475
2759
  }
2476
2760
 
2761
+ // ── Intent-based tools (get_best_operator, compare_self, compare_operators) ──
2762
+ // These wrap existing primitives with behavioral framing in power-user language.
2763
+ // See artifacts/004_sigrank-mcp-intent-tools-spec.md for the intent taxonomy.
2764
+
2765
+ if (name === "get_best_operator") {
2766
+ const rawN = args?.n;
2767
+ const n = Math.min(20, Math.max(1, rawN == null ? 5 : Number(rawN)));
2768
+ const board = await fetchJson("/api/v1/leaderboard?metric=yield_");
2769
+ const ops = (board.operators || board || []).slice(0, n);
2770
+ const total = Array.isArray(board.operators || board)
2771
+ ? (board.operators || board).length
2772
+ : 0;
2773
+
2774
+ const top = ops.map((op) => ({
2775
+ ...op,
2776
+ behavioral_framing: _behavioralFraming(op),
2777
+ }));
2778
+
2779
+ const best = top[0];
2780
+ const summary = best
2781
+ ? `${best.codename} tops the SigRank leaderboard at Υ ${best.yield_?.toLocaleString?.() || best.yield_} — ${_behavioralFraming(best)}`
2782
+ : "No operators on the board yet.";
2783
+
2784
+ return { top_operators: top, total_operators: total, summary };
2785
+ }
2786
+
2787
+ if (name === "compare_self") {
2788
+ const codename = String(args?.codename || "").trim();
2789
+ const text = String(args?.text || "").trim();
2790
+
2791
+ if (!codename && !text)
2792
+ throw new Error(
2793
+ "compare_self requires either `codename` (to fetch from the board) or `text` (raw token pillars to score locally).",
2794
+ );
2795
+
2796
+ let yourMetrics;
2797
+ if (codename) {
2798
+ yourMetrics = await fetchJson(
2799
+ `/api/v1/operators/${encodeURIComponent(codename)}`,
2800
+ );
2801
+ } else {
2802
+ if (text.length > MAX_INPUT) {
2803
+ return {
2804
+ error: "input_too_large",
2805
+ detail: `text exceeds ${MAX_INPUT} chars.`,
2806
+ };
2807
+ }
2808
+ const pillars = parsePillars(text);
2809
+ const c = withParseWarnings(pillars, cascade(pillars));
2810
+ yourMetrics = {
2811
+ codename: "you (local)",
2812
+ yield_: c.yield,
2813
+ leverage: c.leverage,
2814
+ velocity: c.velocity,
2815
+ class: c.class,
2816
+ rank: null,
2817
+ };
2818
+ }
2819
+
2820
+ // Fetch board for comparison
2821
+ const board = await fetchJson("/api/v1/leaderboard?metric=yield_");
2822
+ const allOps = board.operators || board || [];
2823
+ const yields = allOps.map((o) => o.yield_ || 0).sort((a, b) => a - b);
2824
+ const avgYield = yields.length
2825
+ ? yields.reduce((s, y) => s + y, 0) / yields.length
2826
+ : 0;
2827
+ const yourYield = yourMetrics.yield_ || 0;
2828
+ const percentile = yields.length
2829
+ ? Math.round(
2830
+ (yields.filter((y) => y < yourYield).length / yields.length) * 100,
2831
+ )
2832
+ : 0;
2833
+
2834
+ const klass = yourMetrics.class || "Burner";
2835
+ const powerUserAssessment = _powerUserAssessment(klass, yourMetrics);
2836
+ const classMeaning = _classMeaning(klass);
2837
+
2838
+ const yieldVsAvg = yields.length
2839
+ ? yourYield > avgYield
2840
+ ? `${(yourYield / avgYield).toFixed(1)}× the board average`
2841
+ : `${((yourYield / avgYield) * 100).toFixed(0)}% of the board average`
2842
+ : "board has no other operators";
2843
+
2844
+ const suggestion = _improvementSuggestion(klass, yourMetrics);
2845
+
2846
+ return {
2847
+ your_metrics: yourMetrics,
2848
+ power_user_assessment: powerUserAssessment,
2849
+ comparison: {
2850
+ your_yield_vs_avg: yieldVsAvg,
2851
+ your_class_meaning: classMeaning,
2852
+ percentile,
2853
+ },
2854
+ suggestion,
2855
+ };
2856
+ }
2857
+
2858
+ if (name === "compare_operators") {
2859
+ const nameA = String(args?.codename_a || "").trim();
2860
+ const nameB = String(args?.codename_b || "").trim();
2861
+ if (!nameA || !nameB)
2862
+ throw new Error(
2863
+ "compare_operators requires both `codename_a` and `codename_b`.",
2864
+ );
2865
+
2866
+ const [opA, opB] = await Promise.all([
2867
+ fetchJson(`/api/v1/operators/${encodeURIComponent(nameA)}`),
2868
+ fetchJson(`/api/v1/operators/${encodeURIComponent(nameB)}`),
2869
+ ]);
2870
+
2871
+ const yieldA = opA.yield_ || 0;
2872
+ const yieldB = opB.yield_ || 0;
2873
+ const delta = yieldA - yieldB;
2874
+
2875
+ const winner = yieldA > yieldB ? opA : opB;
2876
+ const loser = yieldA > yieldB ? opB : opA;
2877
+ const verdict = `${winner.codename} is more token-efficient (${winner.yield_?.toLocaleString?.() || winner.yield_} vs ${loser.yield_?.toLocaleString?.() || loser.yield_} Υ). ${_behavioralFraming(winner)} ${loser.codename} ${_classMeaning(loser.class).toLowerCase()}`;
2878
+
2879
+ return {
2880
+ operator_a: {
2881
+ codename: opA.codename,
2882
+ yield_: opA.yield_,
2883
+ leverage: opA.leverage,
2884
+ velocity: opA.velocity,
2885
+ class: opA.class,
2886
+ rank: opA.rank,
2887
+ },
2888
+ operator_b: {
2889
+ codename: opB.codename,
2890
+ yield_: opB.yield_,
2891
+ leverage: opB.leverage,
2892
+ velocity: opB.velocity,
2893
+ class: opB.class,
2894
+ rank: opB.rank,
2895
+ },
2896
+ verdict,
2897
+ yield_delta: delta,
2898
+ };
2899
+ }
2900
+
2901
+ if (name === "describe_power_user") {
2902
+ return {
2903
+ description:
2904
+ "An AI power user isn't someone who sends the most tokens — it's someone who compounds signal. " +
2905
+ "Power users build workflows where cached context does the heavy lifting, fresh input stays lean, " +
2906
+ "and output per session is high. SigRank quantifies this with the yield metric (Υ = cache_read × output / input²).",
2907
+ metrics_explained: {
2908
+ yield_: "Yield (Υ) measures how well you compound signal, not how much you burn. High yield = your cached context is doing work for you.",
2909
+ leverage: "Leverage (Cr/I) measures how much you reuse prior work vs starting fresh. High leverage = you're building on cached results, not re-explaining everything.",
2910
+ velocity: "Velocity (O/I) measures how much output you get per token spent. High velocity = you're productive, not just active.",
2911
+ },
2912
+ class_tiers: [
2913
+ { class: "10xer", meaning: "AI power user archetype — disciplined, system-level reuse, high output per input. Leverage > 10×, high velocity." },
2914
+ { class: "Builder", meaning: "Building momentum — moderate cache reuse, approaching power-user patterns. Growing leverage and velocity." },
2915
+ { class: "Burner", meaning: "Early-stage — tokens burned more than compounded. Low leverage, low velocity. The shift: reuse prior context." },
2916
+ ],
2917
+ link: "https://signalaf.com/score — check your class tier and yield",
2918
+ };
2919
+ }
2920
+
2921
+ if (name === "optimize_efficiency") {
2922
+ const codename = String(args?.codename || "").trim();
2923
+ const text = String(args?.text || "").trim();
2924
+
2925
+ if (!codename && !text)
2926
+ throw new Error(
2927
+ "optimize_efficiency requires either `codename` (to fetch from the board) or `text` (raw token pillars to score locally).",
2928
+ );
2929
+
2930
+ let metrics;
2931
+ if (codename) {
2932
+ metrics = await fetchJson(
2933
+ `/api/v1/operators/${encodeURIComponent(codename)}`,
2934
+ );
2935
+ } else {
2936
+ if (text.length > MAX_INPUT) {
2937
+ return { error: "input_too_large", detail: `text exceeds ${MAX_INPUT} chars.` };
2938
+ }
2939
+ const pillars = parsePillars(text);
2940
+ const c = withParseWarnings(pillars, cascade(pillars));
2941
+ metrics = {
2942
+ codename: "you (local)",
2943
+ yield_: c.yield,
2944
+ leverage: c.leverage,
2945
+ velocity: c.velocity,
2946
+ class: c.class,
2947
+ };
2948
+ }
2949
+
2950
+ const klass = metrics.class || "Burner";
2951
+ const l = metrics.leverage || 0;
2952
+ const v = metrics.velocity || 0;
2953
+ const y = metrics.yield_ || 0;
2954
+
2955
+ // Build ranked suggestions based on current cascade shape
2956
+ const suggestions = [];
2957
+
2958
+ if (l < 5) {
2959
+ suggestions.push({
2960
+ action: "Increase cache reuse — reuse prompts, templates, and workflows instead of starting from scratch",
2961
+ why: "Your leverage is " + l.toFixed(1) + "×, meaning most of your context is fresh input. Each reused cached token multiplies your yield because input² is in the denominator.",
2962
+ power_user_practice: "Power users build template libraries and workflow patterns they invoke repeatedly, letting cached context accumulate.",
2963
+ });
2964
+ }
2965
+ if (v < 1) {
2966
+ suggestions.push({
2967
+ action: "Increase output per session — produce more, don't just read",
2968
+ why: "Your velocity is " + v.toFixed(2) + ", meaning you're consuming more input than producing output. Yield rewards output production.",
2969
+ power_user_practice: "Power users maximize output per session — they ask AI to generate, transform, and produce, not just explain.",
2970
+ });
2971
+ }
2972
+ if (l >= 5 && v >= 1 && klass !== "10xer") {
2973
+ suggestions.push({
2974
+ action: "Extend session length to compound cached context further",
2975
+ why: "Your leverage (" + l.toFixed(1) + "×) and velocity (" + v.toFixed(2) + ") are solid. Longer sessions with consistent context will push your yield higher.",
2976
+ power_user_practice: "Power users maintain long, context-rich sessions where the cache grows and compounds.",
2977
+ });
2978
+ }
2979
+ if (suggestions.length === 0) {
2980
+ suggestions.push({
2981
+ action: "Maintain your cascade architecture — you're at the top tier",
2982
+ why: "Your yield (" + y.toLocaleString() + "), leverage (" + l.toFixed(1) + "×), and velocity (" + v.toFixed(2) + ") are all strong. Keep doing what you're doing.",
2983
+ power_user_practice: "Power users don't rest on their metrics — they experiment with new workflow patterns and measure the impact.",
2984
+ });
2985
+ }
2986
+
2987
+ const summary = `Your Υ Yield is ${y.toLocaleString()} (${klass}). ${_improvementSuggestion(klass, metrics)}`;
2988
+
2989
+ return {
2990
+ your_metrics: {
2991
+ yield_: y,
2992
+ leverage: l,
2993
+ velocity: v,
2994
+ class: klass,
2995
+ },
2996
+ suggestions,
2997
+ summary,
2998
+ };
2999
+ }
3000
+
2477
3001
  throw new Error(`Unknown tool: ${name}`);
2478
3002
  }
2479
3003
 
3004
+ // ── Intent tool helpers — behavioral framing in power-user language ──────────
3005
+
3006
+ function _behavioralFraming(op) {
3007
+ const y = op.yield_ || 0;
3008
+ const l = op.leverage || 0;
3009
+ const v = op.velocity || 0;
3010
+ const klass = op.class || "Burner";
3011
+
3012
+ if (klass === "10xer")
3013
+ return `Disciplined, system-level reuse: ${l.toFixed(1)}× leverage means heavy cache reuse over fresh input, ${v.toFixed(2)} velocity means more output per token spent. This is the AI power-user archetype.`;
3014
+ if (klass === "Builder")
3015
+ return `Building cascade momentum: moderate cache reuse (${l.toFixed(1)}× leverage) with ${v.toFixed(2)} output velocity. Approaching power-user patterns — increase cache reuse to push into 10xer territory.`;
3016
+ return `Early-stage cascade: ${v.toFixed(2)} output velocity with ${l.toFixed(1)}× leverage. Tokens are being burned more than compounded. Focus on reusing prior context (templates, prompts, workflows) to build leverage.`;
3017
+ }
3018
+
3019
+ function _powerUserAssessment(klass, metrics) {
3020
+ const l = metrics.leverage || 0;
3021
+ const v = metrics.velocity || 0;
3022
+ if (klass === "10xer")
3023
+ return `You are an AI power user. Your SigRank class (10xer) indicates you reuse prior work heavily (${l.toFixed(1)}× leverage), get more out of each token (${v.toFixed(2)} velocity), and keep input lean. This is consistent with AI power-user behavior: iterative, efficient, multi-use patterns.`;
3024
+ if (klass === "Builder")
3025
+ return `You are becoming an AI power user. Your Builder class shows growing cache reuse (${l.toFixed(1)}× leverage) and ${v.toFixed(2)} output velocity. You're building the habits — increase context reuse to push into 10xer territory.`;
3026
+ return `You are not yet an AI power user. Your Burner class means tokens are being spent without compounding. The power-user shift: reuse prior context (prompts, templates, cached results) instead of starting fresh each time. Your leverage (${l.toFixed(1)}×) is the key metric to improve.`;
3027
+ }
3028
+
3029
+ function _classMeaning(klass) {
3030
+ if (klass === "10xer")
3031
+ return "AI power user archetype — disciplined, system-level reuse, high output per input.";
3032
+ if (klass === "Builder")
3033
+ return "Building momentum — moderate reuse, approaching power-user patterns.";
3034
+ return "Early-stage — tokens burned more than compounded. Focus on cache reuse.";
3035
+ }
3036
+
3037
+ function _improvementSuggestion(klass, metrics) {
3038
+ const l = metrics.leverage || 0;
3039
+ const v = metrics.velocity || 0;
3040
+ if (klass === "10xer")
3041
+ return v < 1
3042
+ ? "Your leverage is excellent but velocity is under 1.0 — you're reading more cache than producing output. Push for more output per session."
3043
+ : "You're at the top tier. Maintain your cache architecture and experiment with longer sessions to compound yield further.";
3044
+ if (klass === "Builder")
3045
+ return l < 5
3046
+ ? "Increase cache reuse: reuse prompts, templates, and workflows instead of starting from scratch. Each reused token multiplies your yield."
3047
+ : "Your leverage is solid. Focus on output velocity — produce more per session to push your yield up.";
3048
+ return "Start by reusing prior context. Instead of fresh prompts each time, build on cached results. Even a 2× increase in cache_read will dramatically improve your yield because input² is in the denominator.";
3049
+ }
3050
+
2480
3051
  // ── self_improve scope helpers ──────────────────────────────────────────────
2481
3052
 
2482
3053
  /** Pull daily rows from ccusage to build mode history. Returns array of { date, pillars, yield, mode }. */
package/tui.mjs CHANGED
@@ -2049,7 +2049,7 @@ export async function runTui({
2049
2049
  renderConnect(connectId, codeBuf, connectMsg);
2050
2050
  const signOutHint = isSignedIn(connectId)
2051
2051
  ? ` ${dim("[X]")} sign out`
2052
- : "";
2052
+ : " " + dim("[X]") + " reset device";
2053
2053
  setFooter([
2054
2054
  ` ${hr()}`,
2055
2055
  ` ${dim("[Enter]")} sign in ${dim("[Esc]")} clear/back${signOutHint} ${dim("← →")} tabs ${dim("[Q]")} quit`,
@@ -2166,14 +2166,16 @@ export async function runTui({
2166
2166
  try {
2167
2167
  const out = await callTool("enroll", { code });
2168
2168
  if (out.status === "enrolled") {
2169
- connectMsg = `${green("✓")} signed in as ${cyan(out.codename || "(operator)")}`;
2169
+ connectMsg = out.recovered
2170
+ ? `${green("✓")} recovered account: ${cyan(out.codename || "(operator)")} ${dim("(device was already bound)")}`
2171
+ : `${green("✓")} signed in as ${cyan(out.codename || "(operator)")}`;
2170
2172
  codeBuf = "";
2171
2173
  } else {
2172
2174
  const reasons = {
2173
2175
  code_invalid:
2174
2176
  "that code is invalid, expired, or already used — generate a fresh one.",
2175
2177
  device_already_enrolled:
2176
- 'this device is already signed in. Need a new key? Click "New key" at signalaf.com → Settings, then paste it here.',
2178
+ 'this device is already bound. Press [X] to reset the device, then paste a fresh code from signalaf.com → Settings → "New key".',
2177
2179
  bad_request: "the code or device key was malformed.",
2178
2180
  rate_limited:
2179
2181
  "too many attempts — wait a few minutes and retry.",
@@ -2214,14 +2216,20 @@ export async function runTui({
2214
2216
  await redraw();
2215
2217
  return;
2216
2218
  } // backspace
2217
- // [X] sign out / reset — only when signed in AND the code field is empty (so an
2218
- // 'x' typed mid-code still feeds the buffer). Drops the stale local credential so
2219
- // a server-revoked device stops reading as "signed in" + the next enroll provisions
2220
- // a fresh device_id (escapes the Frankenstein-identity / 409-re-enroll deadlock).
2221
- if (k === "x" && !codeBuf && isSignedIn(loadIdentity())) {
2219
+ // [X] sign out / reset — when the code field is empty (so an 'x' typed mid-code
2220
+ // still feeds the buffer). Drops the local credential so a server-revoked OR
2221
+ // server-enrolled-but-locally-unbound device stops holding a stale device_id,
2222
+ // and the next enroll provisions a fresh device_id (escapes the Frankenstein-
2223
+ // identity / 409-re-enroll deadlock). Works whether or not isSignedIn() —
2224
+ // otherwise a device enrolled server-side but unbound locally is un-escapable
2225
+ // (can't sign in: device_already_enrolled; can't sign out: X gated off).
2226
+ if (k === "x" && !codeBuf) {
2227
+ const wasSignedIn = isSignedIn(loadIdentity());
2222
2228
  clearIdentity();
2223
2229
  codeBuf = "";
2224
- connectMsg = `${green("✓")} signed out — paste a new code to sign in`;
2230
+ connectMsg = wasSignedIn
2231
+ ? `${green("✓")} signed out — paste a new code to sign in`
2232
+ : `${green("✓")} device reset — paste a new code to sign in`;
2225
2233
  await redraw();
2226
2234
  return;
2227
2235
  }