cribblecheck 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bobby Backlogs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # CribbleCheck
2
+
3
+ [![Version](https://img.shields.io/badge/version-1.0.0-gold.svg)](package.json)
4
+ [![Node](https://img.shields.io/badge/node-%3E%3D18-339933.svg)](package.json)
5
+ [![Platform](https://img.shields.io/badge/platform-win%20%7C%20mac%20%7C%20linux-007acc.svg)](package.json)
6
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![Cribble](https://img.shields.io/badge/integration-cribble.dev-black.svg)](https://cribble.dev)
8
+
9
+ A terminal companion for tracking live rankings, compute metrics, and activity across the Cribble arena.
10
+
11
+ ---
12
+
13
+ ## Overview
14
+
15
+ CribbleCheck provides instant terminal access to your public stats on Cribble. It reads live season standings, token throughput, AI tool breakdowns, and achievement badges directly from the command line without opening a browser.
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ Link the package globally using npm:
22
+
23
+ ```bash
24
+ git clone https://github.com/bobbybacklogs/CribbleCheck.git
25
+ cd CribbleCheck
26
+ npm link
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Quick Start
32
+
33
+ ### Check Personal Stats
34
+
35
+ Running without arguments displays your full performance dashboard:
36
+
37
+ ```bash
38
+ cribblecheck
39
+ ```
40
+
41
+ ### Leaderboard Exploration
42
+
43
+ ```bash
44
+ # View top developers on the global board
45
+ cribblecheck top 10
46
+
47
+ # View compute barons on the token board
48
+ cribblecheck tokens
49
+
50
+ # Inspect AI platform standings
51
+ cribblecheck ai
52
+
53
+ # Inspect squad rankings
54
+ cribblecheck teams
55
+ ```
56
+
57
+ ### Inspect Profiles & Badges
58
+
59
+ ```bash
60
+ # Look up any developer
61
+ cribblecheck <username>
62
+
63
+ # View all unlocked achievement badges
64
+ cribblecheck badges <username>
65
+ ```
66
+
67
+ ### Agent Integration
68
+
69
+ ```bash
70
+ # Verify local background sync status
71
+ cribblecheck local
72
+
73
+ # Change default handle
74
+ cribblecheck set-user <username>
75
+
76
+ # Export raw JSON for scripting and status bars
77
+ cribblecheck --json
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Commands Summary
83
+
84
+ | Command | Action |
85
+ | :--- | :--- |
86
+ | `cribblecheck` | Render personal arena dashboard |
87
+ | `cribblecheck top [n]` | Display global leaderboard standings |
88
+ | `cribblecheck tokens` | Display token leaderboard and compute rankings |
89
+ | `cribblecheck ai` | Display AI model and faction share |
90
+ | `cribblecheck teams` | Display team scores and rosters |
91
+ | `cribblecheck badges` | Display achievement badges dossier |
92
+ | `cribblecheck local` | Check local Cribble client and sync status |
93
+ | `cribblecheck set-user <handle>` | Update default saved username |
94
+
95
+ ---
96
+
97
+ ## References
98
+
99
+ CribbleCheck interfaces with the Cribble developer ecosystem:
100
+
101
+ - **Cribble Platform**: [cribble.dev](https://cribble.dev)
102
+ - **Global Leaderboard**: [cribble.dev/leaderboard](https://cribble.dev/leaderboard)
103
+ - **Official Agent Repository**: [Birdabo404/cribble-agent](https://github.com/Birdabo404/cribble-agent)
104
+ - **Token Tracker**: Powered by `ccusage` and the Cribble background collector
105
+
106
+ ---
107
+
108
+ ## License
109
+
110
+ MIT (c) Bobby Backlogs
@@ -0,0 +1,209 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const api = require("../src/api");
5
+ const config = require("../src/config");
6
+ const formatters = require("../src/formatters");
7
+ const local = require("../src/local");
8
+
9
+ function printHelp() {
10
+ const c = formatters.c;
11
+ console.log(`
12
+ ${c.goldBold}CribbleCheck · Live Cribble.dev Leaderboard CLI${c.reset}
13
+
14
+ ${c.neonCyan}Usage:${c.reset}
15
+ cribblecheck [command] [options]
16
+
17
+ ${c.neonCyan}Commands:${c.reset}
18
+ ${c.white}cribblecheck [username]${c.reset} Show stats dashboard for yourself or a user
19
+ ${c.white}cribblecheck top [limit]${c.reset} Show global developer leaderboard (default: 15)
20
+ ${c.white}cribblecheck tokens [username]${c.reset} Show token leaderboard or user token stats
21
+ ${c.white}cribblecheck ai${c.reset} Show AI faction war rankings (ChatGPT, Claude, etc.)
22
+ ${c.white}cribblecheck teams${c.reset} Show team leaderboard and scores
23
+ ${c.white}cribblecheck badges [username]${c.reset} Show all unlocked achievement badges
24
+ ${c.white}cribblecheck local${c.reset} Inspect your local Cribble Agent status & sync
25
+ ${c.white}cribblecheck set-user <handle>${c.reset} Set your default Cribble username (saved locally)
26
+
27
+ ${c.neonCyan}Options:${c.reset}
28
+ ${c.white}--json${c.reset} Output raw JSON for scripts/pipes
29
+ ${c.white}-h, --help${c.reset} Show this help message
30
+ ${c.white}-v, --version${c.reset} Show CLI version
31
+
32
+ ${c.charcoal}Default username: ${config.getDefaultUser()}${c.reset}
33
+ `);
34
+ }
35
+
36
+ async function main() {
37
+ const args = process.argv.slice(2);
38
+ const jsonMode = args.includes("--json");
39
+ const filteredArgs = args.filter(a => a !== "--json");
40
+
41
+ let cmd = filteredArgs[0] || "stats";
42
+ let targetUser = config.getDefaultUser();
43
+
44
+ if (cmd === "-h" || cmd === "--help" || cmd === "help") {
45
+ printHelp();
46
+ return;
47
+ }
48
+
49
+ if (cmd === "-v" || cmd === "--version" || cmd === "version") {
50
+ const pkg = require("../package.json");
51
+ console.log(`cribblecheck v${pkg.version}`);
52
+ return;
53
+ }
54
+
55
+ if (cmd === "set-user" || cmd === "config") {
56
+ const newUsername = cmd === "set-user" ? filteredArgs[1] : (filteredArgs[1] === "set-user" ? filteredArgs[2] : filteredArgs[1]);
57
+ if (!newUsername) {
58
+ console.error("Please specify a username: cribblecheck set-user <handle>");
59
+ process.exit(1);
60
+ }
61
+ config.setDefaultUser(newUsername);
62
+ console.log(`${formatters.c.neonGreen}✓ Default Cribble username set to:${formatters.c.reset} ${formatters.c.bold}${newUsername}${formatters.c.reset}`);
63
+ return;
64
+ }
65
+
66
+ try {
67
+ if (cmd === "top" || cmd === "leaderboard") {
68
+ const limit = parseInt(filteredArgs[1], 10) || 15;
69
+ const lb = await api.getLeaderboard();
70
+ if (jsonMode) {
71
+ console.log(JSON.stringify(lb.data?.slice(0, limit), null, 2));
72
+ return;
73
+ }
74
+ formatters.renderLeaderboardTable(lb.data || [], limit, targetUser);
75
+ return;
76
+ }
77
+
78
+ if (cmd === "ai" || cmd === "factions" || cmd === "models") {
79
+ const aiData = await api.getAiLeaderboard();
80
+ const tools = aiData.boards?.season?.tools || aiData.boards?.alltime?.tools || [];
81
+ if (jsonMode) {
82
+ console.log(JSON.stringify(tools, null, 2));
83
+ return;
84
+ }
85
+ formatters.renderAiTable(tools);
86
+ return;
87
+ }
88
+
89
+ if (cmd === "tokens") {
90
+ const specificUser = filteredArgs[1];
91
+ const tokData = await api.getTokenLeaderboard();
92
+ if (specificUser) {
93
+ const found = tokData.rows?.find(r => r.username?.toLowerCase() === specificUser.toLowerCase());
94
+ if (!found) {
95
+ console.error(`User "${specificUser}" not found on Token Leaderboard.`);
96
+ process.exit(1);
97
+ }
98
+ if (jsonMode) {
99
+ console.log(JSON.stringify(found, null, 2));
100
+ return;
101
+ }
102
+ // render stats
103
+ console.log(JSON.stringify(found, null, 2));
104
+ return;
105
+ }
106
+
107
+ if (jsonMode) {
108
+ console.log(JSON.stringify(tokData.rows, null, 2));
109
+ return;
110
+ }
111
+ formatters.renderTokenTable(tokData.rows || [], 15, targetUser);
112
+ return;
113
+ }
114
+
115
+ if (cmd === "teams") {
116
+ const teamsData = await api.getTeamsLeaderboard();
117
+ if (jsonMode) {
118
+ console.log(JSON.stringify(teamsData, null, 2));
119
+ return;
120
+ }
121
+ formatters.renderHeader("TEAMS LEADERBOARD", "Active Factions & Squads");
122
+ const teams = teamsData.data || [];
123
+ console.log(`${formatters.c.charcoal}RANK TEAM NAME MEMBERS TOTAL SCORE${formatters.c.reset}`);
124
+ console.log(`${formatters.c.charcoal}─────────────────────────────────────────────────────────────${formatters.c.reset}`);
125
+ teams.slice(0, 15).forEach((t, i) => {
126
+ const rankStr = (`#${i + 1}`).padEnd(5);
127
+ const nameStr = (t.name || t.username).slice(0, 26).padEnd(28);
128
+ const membersStr = (`${t.members?.length || t.memberCount || 1}`).padStart(7);
129
+ const scoreStr = formatters.formatNumber(t.score || t.totalScore || 0).padStart(14);
130
+ console.log(`${formatters.c.white}${rankStr}${formatters.c.reset} ${formatters.c.bold}${nameStr}${formatters.c.reset} ${formatters.c.cyan}${membersStr}${formatters.c.reset} ${formatters.c.neonGreen}${scoreStr}${formatters.c.reset}`);
131
+ });
132
+ console.log("");
133
+ return;
134
+ }
135
+
136
+ if (cmd === "badges") {
137
+ const user = filteredArgs[1] || targetUser;
138
+ const profile = await api.getUserProfile(user);
139
+ if (jsonMode) {
140
+ console.log(JSON.stringify(profile.profile?.badges || [], null, 2));
141
+ return;
142
+ }
143
+ formatters.renderBadges(profile);
144
+ return;
145
+ }
146
+
147
+ if (cmd === "local") {
148
+ const localInfo = local.readLocalStatus();
149
+ if (jsonMode) {
150
+ console.log(JSON.stringify(localInfo, null, 2));
151
+ return;
152
+ }
153
+ formatters.renderHeader("LOCAL AGENT STATUS", "Cribble CLI & Token Ledger");
154
+ console.log(`${formatters.c.brightWhite}Installed:${formatters.c.reset} ${localInfo.installed ? formatters.c.neonGreen + "Yes" : formatters.c.red + "No"}${formatters.c.reset}`);
155
+ console.log(`${formatters.c.brightWhite}Config Directory:${formatters.c.reset} ${localInfo.directory}`);
156
+ console.log(`${formatters.c.brightWhite}Client ID:${formatters.c.reset} ${localInfo.clientId || "None"}`);
157
+ console.log(`${formatters.c.brightWhite}API Key Storage:${formatters.c.reset} ${localInfo.hasKey ? formatters.c.neonGreen + "Protected in DPAPI" : formatters.c.yellow + "Not found"}${formatters.c.reset}`);
158
+ if (localInfo.syncState) {
159
+ console.log(`${formatters.c.brightWhite}Last Sync Attempt:${formatters.c.reset} ${localInfo.syncState.lastAttemptAt || "—"}`);
160
+ console.log(`${formatters.c.brightWhite}Last Sync Success:${formatters.c.reset} ${localInfo.syncState.lastSuccessAt || "—"}`);
161
+ if (localInfo.syncState.lastResult) {
162
+ const r = localInfo.syncState.lastResult;
163
+ console.log(`${formatters.c.brightWhite}Last Sync Result:${formatters.c.reset} ${r.inserted || 0} inserted, ${r.replaced || 0} replaced, ${r.stale || 0} stale`);
164
+ }
165
+ }
166
+ console.log("");
167
+ return;
168
+ }
169
+
170
+ // Default: stats or username
171
+ let userToCheck = targetUser;
172
+ if (cmd !== "stats" && !cmd.startsWith("-")) {
173
+ userToCheck = cmd;
174
+ } else if (filteredArgs[1]) {
175
+ userToCheck = filteredArgs[1];
176
+ }
177
+
178
+ const [profile, lbData, tokData] = await Promise.all([
179
+ api.getUserProfile(userToCheck),
180
+ api.getLeaderboard(),
181
+ api.getTokenLeaderboard()
182
+ ]);
183
+
184
+ const lbEntry = lbData.data?.find(u => u.username?.toLowerCase() === userToCheck.toLowerCase());
185
+ const tokenEntry = tokData.rows?.find(r => r.username?.toLowerCase() === userToCheck.toLowerCase());
186
+
187
+ if (jsonMode) {
188
+ console.log(JSON.stringify({
189
+ profile: profile.profile,
190
+ leaderboard: lbEntry,
191
+ tokenStats: tokenEntry,
192
+ season: lbData.season
193
+ }, null, 2));
194
+ return;
195
+ }
196
+
197
+ formatters.renderUserDashboard(profile, lbEntry, tokenEntry, lbData.season);
198
+
199
+ } catch (err) {
200
+ if (jsonMode) {
201
+ console.error(JSON.stringify({ error: err.message }));
202
+ } else {
203
+ console.error(`\n${formatters.c.red}${formatters.c.bold}Error:${formatters.c.reset} ${err.message}\n`);
204
+ }
205
+ process.exit(1);
206
+ }
207
+ }
208
+
209
+ main();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "cribblecheck",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to check your Cribble.dev leaderboard rankings, token stats, AI factions, and badges.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "cribblecheck": "bin/cribblecheck.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "preferGlobal": true,
16
+ "scripts": {
17
+ "start": "node bin/cribblecheck.js",
18
+ "test": "node bin/cribblecheck.js stats"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/bobbybacklogs/CribbleCheck.git"
23
+ },
24
+ "keywords": [
25
+ "cribble",
26
+ "cribble.dev",
27
+ "leaderboard",
28
+ "ai-usage",
29
+ "token-tracker",
30
+ "ccusage",
31
+ "cli"
32
+ ],
33
+ "author": "Bobby Backlogs",
34
+ "license": "MIT",
35
+ "bugs": {
36
+ "url": "https://github.com/bobbybacklogs/CribbleCheck/issues"
37
+ },
38
+ "homepage": "https://github.com/bobbybacklogs/CribbleCheck#readme",
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "type": "commonjs",
43
+ "engines": {
44
+ "node": ">=18"
45
+ }
46
+ }
package/src/api.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+
3
+ const BASE_URL = "https://cribble.dev";
4
+
5
+ async function fetchJson(endpoint) {
6
+ const url = `${BASE_URL}${endpoint}`;
7
+ const res = await fetch(url, {
8
+ headers: {
9
+ Accept: "application/json",
10
+ "User-Agent": "CribbleCheck-CLI/1.0"
11
+ }
12
+ });
13
+
14
+ if (!res.ok) {
15
+ throw new Error(`Cribble API returned HTTP ${res.status} for ${endpoint}`);
16
+ }
17
+
18
+ return await res.json();
19
+ }
20
+
21
+ async function getLeaderboard() {
22
+ return await fetchJson("/api/leaderboard");
23
+ }
24
+
25
+ async function getTokenLeaderboard() {
26
+ return await fetchJson("/api/leaderboard/tokens");
27
+ }
28
+
29
+ async function getAiLeaderboard() {
30
+ return await fetchJson("/api/leaderboard/ai");
31
+ }
32
+
33
+ async function getTeamsLeaderboard() {
34
+ return await fetchJson("/api/leaderboard/teams");
35
+ }
36
+
37
+ async function getUserProfile(username) {
38
+ if (!username) throw new Error("Username is required");
39
+ return await fetchJson(`/api/profile/${encodeURIComponent(username)}`);
40
+ }
41
+
42
+ async function getCursorAgentsLeaderboard() {
43
+ return await fetchJson("/api/leaderboard/cursor-agents");
44
+ }
45
+
46
+ async function getBillboard() {
47
+ return await fetchJson("/api/billboard/leaderboard");
48
+ }
49
+
50
+ module.exports = {
51
+ BASE_URL,
52
+ getLeaderboard,
53
+ getTokenLeaderboard,
54
+ getAiLeaderboard,
55
+ getTeamsLeaderboard,
56
+ getUserProfile,
57
+ getCursorAgentsLeaderboard,
58
+ getBillboard
59
+ };
package/src/config.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const os = require("node:os");
6
+
7
+ function getConfigPath() {
8
+ const home = os.homedir();
9
+ if (process.platform === "win32") {
10
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
11
+ const dir = path.join(localAppData, "CribbleCheck");
12
+ if (!fs.existsSync(dir)) {
13
+ try {
14
+ fs.mkdirSync(dir, { recursive: true });
15
+ } catch {}
16
+ }
17
+ return path.join(dir, "config.json");
18
+ }
19
+ return path.join(home, ".cribblecheckrc");
20
+ }
21
+
22
+ function loadConfig() {
23
+ const configPath = getConfigPath();
24
+ try {
25
+ if (fs.existsSync(configPath)) {
26
+ const raw = fs.readFileSync(configPath, "utf8");
27
+ return JSON.parse(raw);
28
+ }
29
+ } catch {}
30
+ return { username: "bobbybacklogs" };
31
+ }
32
+
33
+ function saveConfig(cfg) {
34
+ const configPath = getConfigPath();
35
+ try {
36
+ fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2), "utf8");
37
+ return true;
38
+ } catch (err) {
39
+ console.error("Failed to save config:", err.message);
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function getDefaultUser() {
45
+ const cfg = loadConfig();
46
+ return cfg.username || "bobbybacklogs";
47
+ }
48
+
49
+ function setDefaultUser(username) {
50
+ const cfg = loadConfig();
51
+ cfg.username = username.trim();
52
+ saveConfig(cfg);
53
+ }
54
+
55
+ module.exports = {
56
+ loadConfig,
57
+ saveConfig,
58
+ getDefaultUser,
59
+ setDefaultUser,
60
+ getConfigPath
61
+ };
@@ -0,0 +1,298 @@
1
+ "use strict";
2
+
3
+ // ANSI escape codes
4
+ const c = {
5
+ reset: "\x1b[0m",
6
+ bold: "\x1b[1m",
7
+ dim: "\x1b[2m",
8
+ italic: "\x1b[3m",
9
+ underline: "\x1b[4m",
10
+
11
+ // Colors
12
+ black: "\x1b[30m",
13
+ red: "\x1b[31m",
14
+ green: "\x1b[32m",
15
+ yellow: "\x1b[33m",
16
+ blue: "\x1b[34m",
17
+ magenta: "\x1b[35m",
18
+ cyan: "\x1b[36m",
19
+ white: "\x1b[37m",
20
+
21
+ // Bright colors
22
+ brightBlack: "\x1b[90m",
23
+ brightRed: "\x1b[91m",
24
+ brightGreen: "\x1b[92m",
25
+ brightYellow: "\x1b[93m",
26
+ brightBlue: "\x1b[94m",
27
+ brightMagenta: "\x1b[95m",
28
+ brightCyan: "\x1b[96m",
29
+ brightWhite: "\x1b[97m",
30
+
31
+ // Custom theme shades
32
+ gold: "\x1b[38;2;255;215;0m",
33
+ goldBold: "\x1b[1m\x1b[38;2;255;215;0m",
34
+ neonCyan: "\x1b[38;2;0;240;255m",
35
+ neonGreen: "\x1b[38;2;57;255;136m",
36
+ neonPink: "\x1b[38;2;255;92;138m",
37
+ neonPurple: "\x1b[38;2;170;110;255m",
38
+ neonOrange: "\x1b[38;2;255;128;0m",
39
+ charcoal: "\x1b[38;2;120;120;130m",
40
+ silver: "\x1b[38;2;210;210;225m",
41
+ bronze: "\x1b[38;2;205;127;50m",
42
+
43
+ bgBlack: "\x1b[40m",
44
+ bgDark: "\x1b[48;2;18;20;28m"
45
+ };
46
+
47
+ function formatNumber(num) {
48
+ if (num === null || num === undefined) return "0";
49
+ return Number(num).toLocaleString("en-US");
50
+ }
51
+
52
+ function formatCompactNumber(num) {
53
+ if (num === null || num === undefined) return "0";
54
+ const n = Number(num);
55
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
56
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
57
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "k";
58
+ return n.toLocaleString("en-US");
59
+ }
60
+
61
+ function formatDurationMs(ms) {
62
+ if (!ms || ms <= 0) return "0m";
63
+ const totalSeconds = Math.floor(ms / 1000);
64
+ const hours = Math.floor(totalSeconds / 3600);
65
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
66
+ if (hours > 0) {
67
+ return `${hours}h ${minutes}m`;
68
+ }
69
+ return `${minutes}m`;
70
+ }
71
+
72
+ function formatMoney(amount) {
73
+ if (!amount || amount === "0") return "$0.00";
74
+ const val = typeof amount === "string" ? parseFloat(amount) : amount;
75
+ if (isNaN(val)) return "$0.00";
76
+ return "$" + val.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
77
+ }
78
+
79
+ function progressBar(percent, width = 14) {
80
+ const p = Math.max(0, Math.min(100, percent || 0));
81
+ const filled = Math.round((p / 100) * width);
82
+ const empty = width - filled;
83
+ return `${c.neonCyan}${"█".repeat(filled)}${c.brightBlack}${"░".repeat(empty)}${c.reset} ${c.white}${p}%${c.reset}`;
84
+ }
85
+
86
+ function rankTag(rank) {
87
+ if (rank === 1) return `${c.goldBold}#1 👑${c.reset}`;
88
+ if (rank === 2) return `${c.silver}#2 🥈${c.reset}`;
89
+ if (rank === 3) return `${c.bronze}#3 🥉${c.reset}`;
90
+ if (rank <= 10) return `${c.brightMagenta}#${rank} ⚡${c.reset}`;
91
+ return `${c.brightCyan}#${rank}${c.reset}`;
92
+ }
93
+
94
+ function getRarityBadge(rarity) {
95
+ switch ((rarity || "").toLowerCase()) {
96
+ case "legendary":
97
+ return `${c.goldBold}[LEGENDARY]${c.reset}`;
98
+ case "epic":
99
+ return `${c.neonPurple}[EPIC]${c.reset}`;
100
+ case "rare":
101
+ return `${c.neonCyan}[RARE]${c.reset}`;
102
+ case "common":
103
+ default:
104
+ return `${c.brightBlack}[COMMON]${c.reset}`;
105
+ }
106
+ }
107
+
108
+ function renderHeader(title, subtitle = "") {
109
+ const line = "═".repeat(56);
110
+ console.log(`${c.goldBold}╔${line}╗${c.reset}`);
111
+ console.log(`${c.goldBold}║${c.reset} ${c.neonCyan}${c.bold}CRIBBLE · ${title.toUpperCase()}${c.reset}`.padEnd(68) + `${c.goldBold}║${c.reset}`);
112
+ if (subtitle) {
113
+ console.log(`${c.goldBold}║${c.reset} ${c.charcoal}${subtitle}${c.reset}`.padEnd(68) + `${c.goldBold}║${c.reset}`);
114
+ }
115
+ console.log(`${c.goldBold}╚${line}╝${c.reset}\n`);
116
+ }
117
+
118
+ function renderUserDashboard(profile, lbEntry, tokenEntry, season) {
119
+ const p = profile.profile || profile;
120
+ const username = p.username || "Unknown";
121
+ const displayName = p.display_name || p.displayName || username;
122
+ const rank = lbEntry?.rank || p.rank || "—";
123
+ const score = lbEntry?.score || p.score || 0;
124
+ const todayScore = lbEntry?.todayScore || p.todayScore || 0;
125
+ const weekScore = lbEntry?.weekScore || p.weekScore || 0;
126
+ const streak = p.longestStreak || p.streak || 0;
127
+ const activeDays = p.activeDays || 0;
128
+ const activeTime = formatDurationMs(p.totalActiveMs || p.active_ms);
129
+
130
+ const seasonName = season?.current?.name || "SEASON 01";
131
+ renderHeader(`LEADERBOARD STATS`, `${seasonName} · Live Arena`);
132
+
133
+ // Main Identity Card
134
+ console.log(`${c.bold}${c.white}${displayName}${c.reset} ${c.charcoal}(@${username})${c.reset} ${rankTag(rank)}`);
135
+ if (p.bio) console.log(`${c.italic}${c.charcoal}"${p.bio}"${c.reset}`);
136
+ console.log("");
137
+
138
+ // Key Metrics Grid
139
+ console.log(`${c.goldBold}┌─ ARENA PERFORMANCE ──────────────────────────────────┐${c.reset}`);
140
+ console.log(`${c.goldBold}│${c.reset} ${c.brightWhite}Current Score:${c.reset} ${c.neonGreen}${c.bold}${formatNumber(score)} pts${c.reset} ${c.brightWhite}Global Rank:${c.reset} ${rankTag(rank)}`.padEnd(72) + `${c.goldBold}│${c.reset}`);
141
+ console.log(`${c.goldBold}│${c.reset} ${c.brightWhite}Today's Gain:${c.reset} ${c.neonCyan}+${formatNumber(todayScore)} pts${c.reset} ${c.brightWhite}7-Day Gain:${c.reset} ${c.neonCyan}+${formatNumber(weekScore)} pts${c.reset}`.padEnd(72) + `${c.goldBold}│${c.reset}`);
142
+ console.log(`${c.goldBold}│${c.reset} ${c.brightWhite}Streak:${c.reset} ${c.neonOrange}${streak} days 🔥${c.reset} ${c.brightWhite}Active Days:${c.reset} ${c.white}${activeDays} days${c.reset}`.padEnd(70) + `${c.goldBold}│${c.reset}`);
143
+ console.log(`${c.goldBold}│${c.reset} ${c.brightWhite}Active Time:${c.reset} ${c.white}${activeTime}${c.reset}`.padEnd(61) + `${c.goldBold}│${c.reset}`);
144
+ console.log(`${c.goldBold}└───────────────────────────────────────────────────────┘${c.reset}\n`);
145
+
146
+ // Token Stats (if available)
147
+ if (tokenEntry) {
148
+ const totalTokens = formatCompactNumber(tokenEntry.totalTokens);
149
+ const inTokens = formatCompactNumber(tokenEntry.inputTokens);
150
+ const outTokens = formatCompactNumber(tokenEntry.outputTokens);
151
+ const cacheTokens = formatCompactNumber(tokenEntry.cacheTokens || tokenEntry.cacheReadTokens);
152
+ const burn = formatMoney(tokenEntry.burnUsd);
153
+ const persona = tokenEntry.persona ? `${tokenEntry.persona.label}` : "AI AGENT";
154
+ const cachePct = tokenEntry.cachePercent || 0;
155
+
156
+ console.log(`${c.neonPurple}┌─ TOKEN & COMPUTE STATS ───────────────────────────────┐${c.reset}`);
157
+ console.log(`${c.neonPurple}│${c.reset} ${c.brightWhite}Token Rank:${c.reset} ${rankTag(tokenEntry.rank)} ${c.brightWhite}Persona:${c.reset} ${c.neonPurple}${persona}${c.reset}`.padEnd(70) + `${c.neonPurple}│${c.reset}`);
158
+ console.log(`${c.neonPurple}│${c.reset} ${c.brightWhite}Total Tokens:${c.reset} ${c.neonGreen}${c.bold}${totalTokens}${c.reset} (${formatNumber(tokenEntry.totalTokens)})${c.reset}`.padEnd(61) + `${c.neonPurple}│${c.reset}`);
159
+ console.log(`${c.neonPurple}│${c.reset} ${c.brightWhite}Cache Ratio:${c.reset} ${progressBar(cachePct, 12)} (${cacheTokens} cached)`.padEnd(69) + `${c.neonPurple}│${c.reset}`);
160
+ console.log(`${c.neonPurple}│${c.reset} ${c.brightWhite}Input/Output:${c.reset} ${c.white}${inTokens} in / ${outTokens} out${c.reset}`.padEnd(61) + `${c.neonPurple}│${c.reset}`);
161
+ console.log(`${c.neonPurple}│${c.reset} ${c.brightWhite}Estimated Burn:${c.reset} ${c.neonOrange}${burn}${c.reset}`.padEnd(61) + `${c.neonPurple}│${c.reset}`);
162
+ if (tokenEntry.topAgent) {
163
+ console.log(`${c.neonPurple}│${c.reset} ${c.brightWhite}Top Agent:${c.reset} ${c.neonCyan}${tokenEntry.topAgent.toUpperCase()}${c.reset}`.padEnd(61) + `${c.neonPurple}│${c.reset}`);
164
+ }
165
+ console.log(`${c.neonPurple}└───────────────────────────────────────────────────────┘${c.reset}\n`);
166
+ }
167
+
168
+ // Top Tools Breakdown
169
+ if (p.topTools && p.topTools.length > 0) {
170
+ console.log(`${c.neonCyan}${c.bold}TOP AI TOOLS${c.reset}`);
171
+ for (const tool of p.topTools) {
172
+ const timeStr = formatDurationMs(tool.active_ms);
173
+ console.log(` ${c.white}${tool.name.padEnd(16)}${c.reset} ${progressBar(tool.percent, 16)} ${c.charcoal}${tool.visits} visits · ${timeStr}${c.reset}`);
174
+ }
175
+ console.log("");
176
+ }
177
+
178
+ // Top Agents Breakdown
179
+ if (p.topAgents && p.topAgents.length > 0) {
180
+ console.log(`${c.neonGreen}${c.bold}TOP CODING AGENTS${c.reset}`);
181
+ for (const agent of p.topAgents) {
182
+ const tokensStr = formatCompactNumber(agent.totalTokens);
183
+ console.log(` ${c.white}${agent.name.padEnd(16)}${c.reset} ${progressBar(agent.percent, 16)} ${c.charcoal}${tokensStr} tokens${c.reset}`);
184
+ }
185
+ console.log("");
186
+ }
187
+
188
+ // Badges Overview
189
+ if (p.badges && p.badges.length > 0) {
190
+ console.log(`${c.goldBold}BADGES UNLOCKED (${p.badges.length})${c.reset}`);
191
+ const preview = p.badges.slice(0, 4);
192
+ for (const b of preview) {
193
+ console.log(` ${getRarityBadge(b.rarity)} ${c.bold}${b.name}${c.reset} · ${c.charcoal}${b.description}${c.reset}`);
194
+ }
195
+ if (p.badges.length > 4) {
196
+ console.log(` ${c.dim}...and ${p.badges.length - 4} more. Run \`cribblecheck badges ${username}\` to view all.${c.reset}`);
197
+ }
198
+ console.log("");
199
+ }
200
+ }
201
+
202
+ function renderLeaderboardTable(users, limit = 15, currentUsername = "") {
203
+ renderHeader(`GLOBAL LEADERBOARD`, `Top ${limit} Developers by AI Activity`);
204
+ console.log(`${c.charcoal}RANK DEVELOPER SCORE TODAY 7-DAY STREAK${c.reset}`);
205
+ console.log(`${c.charcoal}─────────────────────────────────────────────────────────────────────────${c.reset}`);
206
+
207
+ users.slice(0, limit).forEach(u => {
208
+ const isYou = currentUsername && u.username?.toLowerCase() === currentUsername.toLowerCase();
209
+ const rankStr = (`#${u.rank}`).padEnd(6);
210
+ const nameStr = (`${u.display_name || u.username}`).slice(0, 26).padEnd(27);
211
+ const scoreStr = formatNumber(u.score).padStart(9);
212
+ const todayStr = (`+${formatNumber(u.todayScore || 0)}`).padStart(10);
213
+ const weekStr = (`+${formatNumber(u.weekScore || 0)}`).padStart(10);
214
+ const youTag = isYou ? ` ${c.goldBold}◄ YOU${c.reset}` : "";
215
+
216
+ let line = `${c.white}${rankStr}${c.reset} ${c.brightWhite}${nameStr}${c.reset} ${c.neonGreen}${scoreStr}${c.reset} ${c.cyan}${todayStr}${c.reset} ${c.charcoal}${weekStr}${c.reset}${youTag}`;
217
+ if (isYou) {
218
+ line = `${c.goldBold}${rankStr}${c.reset} ${c.goldBold}${nameStr}${c.reset} ${c.neonGreen}${c.bold}${scoreStr}${c.reset} ${c.neonCyan}${c.bold}${todayStr}${c.reset} ${c.white}${weekStr}${c.reset}${youTag}`;
219
+ }
220
+ console.log(line);
221
+ });
222
+ console.log("");
223
+ }
224
+
225
+ function renderTokenTable(rows, limit = 15, currentUsername = "") {
226
+ renderHeader(`TOKEN LEADERBOARD`, `Top Compute Barons by Token Volume`);
227
+ console.log(`${c.charcoal}RANK DEVELOPER TOTAL TOKENS CACHE % EST. BURN TOP AGENT${c.reset}`);
228
+ console.log(`${c.charcoal}───────────────────────────────────────────────────────────────────────────${c.reset}`);
229
+
230
+ rows.slice(0, limit).forEach(r => {
231
+ const isYou = currentUsername && r.username?.toLowerCase() === currentUsername.toLowerCase();
232
+ const rankStr = (`#${r.rank}`).padEnd(6);
233
+ const nameStr = (`${r.displayName || r.username}`).slice(0, 20).padEnd(21);
234
+ const tokenStr = formatCompactNumber(r.totalTokens).padStart(12);
235
+ const cacheStr = (`${r.cachePercent || 0}%`).padStart(8);
236
+ const burnStr = formatMoney(r.burnUsd).padStart(11);
237
+ const agentStr = (r.topAgent || "—").toUpperCase().padEnd(10);
238
+ const youTag = isYou ? ` ${c.goldBold}◄ YOU${c.reset}` : "";
239
+
240
+ let line = `${c.white}${rankStr}${c.reset} ${c.brightWhite}${nameStr}${c.reset} ${c.neonGreen}${tokenStr}${c.reset} ${c.neonCyan}${cacheStr}${c.reset} ${c.neonOrange}${burnStr}${c.reset} ${c.white}${agentStr}${c.reset}${youTag}`;
241
+ if (isYou) {
242
+ line = `${c.goldBold}${rankStr}${c.reset} ${c.goldBold}${nameStr}${c.reset} ${c.neonGreen}${c.bold}${tokenStr}${c.reset} ${c.neonCyan}${c.bold}${cacheStr}${c.reset} ${c.neonOrange}${c.bold}${burnStr}${c.reset} ${c.gold}${agentStr}${c.reset}${youTag}`;
243
+ }
244
+ console.log(line);
245
+ });
246
+ console.log("");
247
+ }
248
+
249
+ function renderAiTable(tools) {
250
+ renderHeader(`FACTION WARS`, `AI Tools & LLM Platform Market Share`);
251
+ console.log(`${c.charcoal}RANK TOOL SCORE FIELD SHARE VISITS ACTIVE PLAYERS${c.reset}`);
252
+ console.log(`${c.charcoal}──────────────────────────────────────────────────────────────────────────────${c.reset}`);
253
+
254
+ tools.slice(0, 15).forEach(t => {
255
+ const rankStr = (`#${t.rank}`).padEnd(5);
256
+ const nameStr = t.name.slice(0, 17).padEnd(18);
257
+ const scoreStr = formatNumber(t.score).padStart(10);
258
+ const bar = progressBar(t.percent, 10);
259
+ const visitsStr = formatNumber(t.visits).padStart(8);
260
+ const pilotsStr = formatNumber(t.pilots).padStart(8);
261
+
262
+ console.log(`${c.white}${rankStr}${c.reset} ${c.brightWhite}${c.bold}${nameStr}${c.reset} ${c.neonGreen}${scoreStr}${c.reset} ${bar} ${c.charcoal}${visitsStr}${c.reset} ${c.white}${pilotsStr}${c.reset}`);
263
+ });
264
+ console.log("");
265
+ }
266
+
267
+ function renderBadges(profile) {
268
+ const p = profile.profile || profile;
269
+ const badges = p.badges || [];
270
+ renderHeader(`BADGES DOSSIER`, `${p.display_name || p.username} · ${badges.length} Badges Unlocked`);
271
+
272
+ if (badges.length === 0) {
273
+ console.log(`${c.charcoal}No badges unlocked yet.${c.reset}\n`);
274
+ return;
275
+ }
276
+
277
+ for (const b of badges) {
278
+ const unlocked = b.unlockedAt ? new Date(b.unlockedAt).toLocaleDateString() : "";
279
+ console.log(`${getRarityBadge(b.rarity)} ${c.goldBold}${b.name}${c.reset} ${c.charcoal}(${unlocked})${c.reset}`);
280
+ console.log(` ${c.white}${b.description}${c.reset}\n`);
281
+ }
282
+ }
283
+
284
+ module.exports = {
285
+ c,
286
+ formatNumber,
287
+ formatCompactNumber,
288
+ formatDurationMs,
289
+ formatMoney,
290
+ progressBar,
291
+ rankTag,
292
+ renderHeader,
293
+ renderUserDashboard,
294
+ renderLeaderboardTable,
295
+ renderTokenTable,
296
+ renderAiTable,
297
+ renderBadges
298
+ };
package/src/index.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+
3
+ const api = require("./api");
4
+ const config = require("./config");
5
+ const formatters = require("./formatters");
6
+ const local = require("./local");
7
+
8
+ module.exports = {
9
+ api,
10
+ config,
11
+ formatters,
12
+ local
13
+ };
package/src/local.js ADDED
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const os = require("node:os");
6
+
7
+ function getLocalCribbleDir() {
8
+ const home = os.homedir();
9
+ if (process.platform === "win32") {
10
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
11
+ return path.join(localAppData, "Cribble");
12
+ }
13
+ if (process.platform === "darwin") {
14
+ return path.join(home, ".config", "cribble");
15
+ }
16
+ const xdg = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
17
+ return path.join(xdg, "cribble");
18
+ }
19
+
20
+ function readLocalStatus() {
21
+ const cribbleDir = getLocalCribbleDir();
22
+ if (!fs.existsSync(cribbleDir)) {
23
+ return { installed: false, error: "Local Cribble directory not found" };
24
+ }
25
+
26
+ const clientIdPath = path.join(cribbleDir, "client-id");
27
+ const syncStatePath = path.join(cribbleDir, "sync-state.json");
28
+ const scopePath = path.join(cribbleDir, "usage-scope");
29
+ const dpapiPath = path.join(cribbleDir, "agent-key.dpapi");
30
+
31
+ let clientId = null;
32
+ let syncState = null;
33
+ let usageScope = null;
34
+ let hasKey = false;
35
+
36
+ if (fs.existsSync(clientIdPath)) {
37
+ try {
38
+ clientId = fs.readFileSync(clientIdPath, "utf8").trim();
39
+ } catch {}
40
+ }
41
+
42
+ if (fs.existsSync(syncStatePath)) {
43
+ try {
44
+ syncState = JSON.parse(fs.readFileSync(syncStatePath, "utf8"));
45
+ } catch {}
46
+ }
47
+
48
+ if (fs.existsSync(scopePath)) {
49
+ try {
50
+ usageScope = fs.readFileSync(scopePath, "utf8").trim();
51
+ } catch {}
52
+ }
53
+
54
+ if (fs.existsSync(dpapiPath)) {
55
+ hasKey = true;
56
+ }
57
+
58
+ return {
59
+ installed: true,
60
+ directory: cribbleDir,
61
+ clientId,
62
+ syncState,
63
+ usageScope,
64
+ hasKey
65
+ };
66
+ }
67
+
68
+ module.exports = {
69
+ getLocalCribbleDir,
70
+ readLocalStatus
71
+ };