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 +21 -0
- package/README.md +110 -0
- package/bin/cribblecheck.js +209 -0
- package/package.json +46 -0
- package/src/api.js +59 -0
- package/src/config.js +61 -0
- package/src/formatters.js +298 -0
- package/src/index.js +13 -0
- package/src/local.js +71 -0
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
|
+
[](package.json)
|
|
4
|
+
[](package.json)
|
|
5
|
+
[](package.json)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](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
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
|
+
};
|