claude-petpet 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 +22 -0
- package/README.md +90 -0
- package/package.json +57 -0
- package/src/cli.ts +286 -0
- package/src/color.ts +31 -0
- package/src/consts.ts +29 -0
- package/src/enums.ts +37 -0
- package/src/helpers.ts +104 -0
- package/src/index.ts +14 -0
- package/src/sprites.ts +54 -0
- package/src/types.ts +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Rayhan Noufal Arayilakath
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# claude-petpet
|
|
2
|
+
|
|
3
|
+
Find UUIDs that produce specific [Claude Code](https://docs.anthropic.com/en/docs/claude-code) buddy companions.
|
|
4
|
+
|
|
5
|
+
Claude Code assigns each user a procedurally generated buddy based on their
|
|
6
|
+
account UUID. This tool brute-force searches random UUIDs to find ones that
|
|
7
|
+
produce a buddy matching your desired species, rarity, stats, and cosmetics.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
Interactive mode (recommended):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun run start
|
|
21
|
+
# or
|
|
22
|
+
bun src/cli.ts
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The CLI walks you through selecting filters (species, rarity, eyes, hat, shiny,
|
|
26
|
+
peak/dump stats, minimum total) then searches for matching UUIDs.
|
|
27
|
+
|
|
28
|
+
### As a library
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { rollFrom, search } from "claude-petpet";
|
|
32
|
+
|
|
33
|
+
// Roll a specific UUID
|
|
34
|
+
const buddy = rollFrom("your-uuid-here");
|
|
35
|
+
console.log(buddy);
|
|
36
|
+
|
|
37
|
+
// Search with filters
|
|
38
|
+
const results = search({
|
|
39
|
+
species: "axolotl",
|
|
40
|
+
rarity: "legendary",
|
|
41
|
+
limit: 3,
|
|
42
|
+
max: 10_000_000,
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Applying a result
|
|
47
|
+
|
|
48
|
+
Once you find a UUID you like:
|
|
49
|
+
|
|
50
|
+
1. Open `~/.claude/.config.json`
|
|
51
|
+
2. Set `oauthAccount.accountUuid` to the UUID from the search results
|
|
52
|
+
3. Restart Claude Code
|
|
53
|
+
|
|
54
|
+
> **Note:** Re-authenticating will overwrite the UUID back to your real one.
|
|
55
|
+
|
|
56
|
+
## How it works
|
|
57
|
+
|
|
58
|
+
Claude Code's buddy system seeds a Mulberry32 PRNG with a hash of
|
|
59
|
+
`userId + salt`, then rolls rarity, species, eyes, hat, shiny status, and stat
|
|
60
|
+
distribution from that deterministic sequence. This tool replicates that
|
|
61
|
+
algorithm to predict the buddy for any given UUID without running Claude Code.
|
|
62
|
+
|
|
63
|
+
## Legal notice
|
|
64
|
+
|
|
65
|
+
The companion generation algorithm in this project was derived from source maps
|
|
66
|
+
that were inadvertently published alongside Claude Code's publicly distributed
|
|
67
|
+
client-side JavaScript. No access controls, obfuscation, or technological
|
|
68
|
+
protection measures were circumvented — the source maps were openly served to
|
|
69
|
+
end users via NPM. No proprietary source code was copied verbatim; the algorithm
|
|
70
|
+
was reimplemented from the observed logic.
|
|
71
|
+
|
|
72
|
+
Reading publicly served files does not constitute unauthorized access, and
|
|
73
|
+
reimplementing a functional algorithm from publicly available materials is
|
|
74
|
+
well-established as lawful under:
|
|
75
|
+
|
|
76
|
+
- **No DMCA §1201 issue** — no technological protection measure was bypassed;
|
|
77
|
+
the source maps were publicly accessible without authentication
|
|
78
|
+
- **Fair use doctrine** — functional algorithms are not copyrightable expression
|
|
79
|
+
(see _Oracle v. Google_, 593 U.S. 1 (2021))
|
|
80
|
+
- **DMCA §1201(f)** — reverse engineering for interoperability is independently
|
|
81
|
+
permitted even when protection measures are present (not applicable here)
|
|
82
|
+
- **EU Software Directive (2009/24/EC) Art. 6** — permits analysis for
|
|
83
|
+
interoperability
|
|
84
|
+
|
|
85
|
+
This project is not affiliated with or endorsed by Anthropic, PBC. "Claude" and
|
|
86
|
+
"Claude Code" are trademarks of Anthropic.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
[MIT](LICENSE) — Copyright (c) 2026 Rayhan Noufal Arayilakath
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-petpet",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Find UUIDs that produce specific Claude Code buddy companions",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"buddy",
|
|
7
|
+
"bun",
|
|
8
|
+
"claude",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"companion",
|
|
11
|
+
"petpet"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/rayhanadev/claude-petpet#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/rayhanadev/claude-petpet/issues"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Rayhan Noufal Arayilakath",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/rayhanadev/claude-petpet.git"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"claude-petpet": "src/cli.ts"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"src",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"type": "module",
|
|
32
|
+
"module": "src/index.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": "./src/index.ts"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"start": "bun src/cli.ts",
|
|
38
|
+
"lint": "oxlint --type-aware",
|
|
39
|
+
"format": "oxfmt",
|
|
40
|
+
"typecheck": "tsc --noEmit"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@clack/prompts": "^1.1.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/bun": "latest",
|
|
47
|
+
"oxfmt": "^0.43.0",
|
|
48
|
+
"oxlint": "^1.58.0",
|
|
49
|
+
"oxlint-tsgolint": "^0.18.1"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"typescript": "^6"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"bun": ">=1.2"
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// CLI for finding UUIDs that produce specific Claude Code buddy companions.
|
|
3
|
+
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
|
|
6
|
+
import { RARITIES, SPECIES, EYES, HATS, STAT_NAMES } from "./enums.ts";
|
|
7
|
+
import { STARS, YIELD_EVERY } from "./consts.ts";
|
|
8
|
+
import { rollFrom, matchesFilters } from "./helpers.ts";
|
|
9
|
+
import { renderSprite } from "./sprites.ts";
|
|
10
|
+
import { c } from "./color.ts";
|
|
11
|
+
import type { Rarity, StatName, SearchFilters, SearchResult } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
const NONE = "__none__";
|
|
14
|
+
|
|
15
|
+
async function promptFilters() {
|
|
16
|
+
return p.group(
|
|
17
|
+
{
|
|
18
|
+
species: () =>
|
|
19
|
+
p.select({
|
|
20
|
+
message: "Species?",
|
|
21
|
+
options: [
|
|
22
|
+
{ value: NONE, label: "Any", hint: "no filter" },
|
|
23
|
+
...SPECIES.map((s) => ({ value: s, label: s })),
|
|
24
|
+
],
|
|
25
|
+
initialValue: NONE,
|
|
26
|
+
}),
|
|
27
|
+
|
|
28
|
+
rarity: () =>
|
|
29
|
+
p.select({
|
|
30
|
+
message: "Rarity?",
|
|
31
|
+
options: [
|
|
32
|
+
{ value: NONE, label: "Any", hint: "no filter" },
|
|
33
|
+
...RARITIES.map((r) => ({ value: r, label: `${STARS[r]} ${r}` })),
|
|
34
|
+
],
|
|
35
|
+
initialValue: NONE,
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
eye: () =>
|
|
39
|
+
p.select({
|
|
40
|
+
message: "Eye style?",
|
|
41
|
+
options: [
|
|
42
|
+
{ value: NONE, label: "Any", hint: "no filter" },
|
|
43
|
+
...EYES.map((e) => ({ value: e, label: e })),
|
|
44
|
+
],
|
|
45
|
+
initialValue: NONE,
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
hat: () =>
|
|
49
|
+
p.select({
|
|
50
|
+
message: "Hat?",
|
|
51
|
+
options: [
|
|
52
|
+
{ value: NONE, label: "Any", hint: "no filter" },
|
|
53
|
+
...HATS.map((h) => ({ value: h, label: h })),
|
|
54
|
+
],
|
|
55
|
+
initialValue: NONE,
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
shiny: () =>
|
|
59
|
+
p.confirm({
|
|
60
|
+
message: "Must be shiny?",
|
|
61
|
+
initialValue: false,
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
peak: () =>
|
|
65
|
+
p.select({
|
|
66
|
+
message: "Peak stat?",
|
|
67
|
+
options: [
|
|
68
|
+
{ value: NONE, label: "Any", hint: "no filter" },
|
|
69
|
+
...STAT_NAMES.map((s) => ({ value: s, label: s })),
|
|
70
|
+
],
|
|
71
|
+
initialValue: NONE,
|
|
72
|
+
}),
|
|
73
|
+
|
|
74
|
+
dump: ({ results }) =>
|
|
75
|
+
p.select({
|
|
76
|
+
message: "Dump stat?",
|
|
77
|
+
options: [
|
|
78
|
+
{ value: NONE, label: "Any", hint: "no filter" },
|
|
79
|
+
...STAT_NAMES.filter(
|
|
80
|
+
(s) => s !== results.peak || (results.peak as string) === NONE,
|
|
81
|
+
).map((s) => ({
|
|
82
|
+
value: s,
|
|
83
|
+
label: s,
|
|
84
|
+
})),
|
|
85
|
+
],
|
|
86
|
+
initialValue: NONE,
|
|
87
|
+
}),
|
|
88
|
+
|
|
89
|
+
minTotal: () =>
|
|
90
|
+
p.text({
|
|
91
|
+
message: "Minimum total stats? (0-500)",
|
|
92
|
+
placeholder: "none",
|
|
93
|
+
validate: (v) => {
|
|
94
|
+
if (!v) return;
|
|
95
|
+
const n = parseInt(v, 10);
|
|
96
|
+
if (isNaN(n) || n < 0 || n > 500) return "Must be a number between 0 and 500";
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
limit: () =>
|
|
101
|
+
p.text({
|
|
102
|
+
message: "How many results?",
|
|
103
|
+
initialValue: "5",
|
|
104
|
+
validate: (v) => {
|
|
105
|
+
if (!v) return "Required";
|
|
106
|
+
const n = parseInt(v, 10);
|
|
107
|
+
if (isNaN(n) || n < 1 || n > 100) return "Must be a number between 1 and 100";
|
|
108
|
+
},
|
|
109
|
+
}),
|
|
110
|
+
|
|
111
|
+
max: () =>
|
|
112
|
+
p.text({
|
|
113
|
+
message: "Max seeds to search?",
|
|
114
|
+
initialValue: "100000000",
|
|
115
|
+
validate: (v) => {
|
|
116
|
+
if (!v) return "Required";
|
|
117
|
+
const n = parseInt(v, 10);
|
|
118
|
+
if (isNaN(n) || n < 1) return "Must be a positive number";
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
onCancel: () => {
|
|
124
|
+
p.cancel("Search cancelled.");
|
|
125
|
+
process.exit(0);
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseFilters(raw: Awaited<ReturnType<typeof promptFilters>>): SearchFilters {
|
|
132
|
+
return {
|
|
133
|
+
species: raw.species !== NONE ? raw.species : undefined,
|
|
134
|
+
rarity: (raw.rarity !== NONE ? raw.rarity : undefined) as Rarity | undefined,
|
|
135
|
+
eye: raw.eye !== NONE ? raw.eye : undefined,
|
|
136
|
+
hat: raw.hat !== NONE ? raw.hat : undefined,
|
|
137
|
+
shiny: raw.shiny || undefined,
|
|
138
|
+
peak: (raw.peak !== NONE ? raw.peak : undefined) as StatName | undefined,
|
|
139
|
+
dump: (raw.dump !== NONE ? raw.dump : undefined) as StatName | undefined,
|
|
140
|
+
minTotal: raw.minTotal ? parseInt(raw.minTotal, 10) : undefined,
|
|
141
|
+
limit: parseInt(raw.limit, 10),
|
|
142
|
+
max: parseInt(raw.max, 10),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatSummary(filters: SearchFilters): string {
|
|
147
|
+
const parts: string[] = [];
|
|
148
|
+
if (filters.species) parts.push(`species=${filters.species}`);
|
|
149
|
+
if (filters.rarity) parts.push(`rarity=${filters.rarity}`);
|
|
150
|
+
if (filters.eye) parts.push(`eye=${filters.eye}`);
|
|
151
|
+
if (filters.hat) parts.push(`hat=${filters.hat}`);
|
|
152
|
+
if (filters.shiny) parts.push("shiny=true");
|
|
153
|
+
if (filters.peak) parts.push(`peak=${filters.peak}`);
|
|
154
|
+
if (filters.dump) parts.push(`dump=${filters.dump}`);
|
|
155
|
+
if (filters.minTotal) parts.push(`min-total=${filters.minTotal}`);
|
|
156
|
+
return parts.join(", ") || "(any)";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function searchWithProgress(
|
|
160
|
+
filters: SearchFilters,
|
|
161
|
+
): Promise<{ results: SearchResult[]; searched: number }> {
|
|
162
|
+
const s = p.spinner({ indicator: "timer" });
|
|
163
|
+
s.start("Searching...");
|
|
164
|
+
|
|
165
|
+
const results: SearchResult[] = [];
|
|
166
|
+
let searched = 0;
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < filters.max; i++) {
|
|
169
|
+
if (i % YIELD_EVERY === 0 && i > 0) {
|
|
170
|
+
s.message(`Found ${results.length} match(es), ${i.toLocaleString()} searched`);
|
|
171
|
+
await Bun.sleep(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const uuid = crypto.randomUUID();
|
|
175
|
+
const roll = rollFrom(uuid);
|
|
176
|
+
|
|
177
|
+
if (!matchesFilters(roll, filters)) continue;
|
|
178
|
+
|
|
179
|
+
results.push({ ...roll, uuid });
|
|
180
|
+
results.sort((a, b) => b.total - a.total);
|
|
181
|
+
if (results.length > filters.limit) results.length = filters.limit;
|
|
182
|
+
|
|
183
|
+
searched = i + 1;
|
|
184
|
+
if (results.length >= filters.limit) break;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
searched = searched || filters.max;
|
|
188
|
+
s.stop(`Searched ${searched.toLocaleString()} seeds`);
|
|
189
|
+
|
|
190
|
+
return { results, searched };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function padVisual(s: string, width: number): string {
|
|
194
|
+
return s + " ".repeat(Math.max(0, width - Bun.stringWidth(s)));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function printResult(r: SearchResult) {
|
|
198
|
+
const rarityColor =
|
|
199
|
+
r.rarity === "legendary"
|
|
200
|
+
? c.yellow
|
|
201
|
+
: r.rarity === "epic"
|
|
202
|
+
? c.magenta
|
|
203
|
+
: r.rarity === "rare"
|
|
204
|
+
? c.blue
|
|
205
|
+
: r.rarity === "uncommon"
|
|
206
|
+
? c.green
|
|
207
|
+
: c.dim;
|
|
208
|
+
|
|
209
|
+
const shinyColor = r.shiny ? c.yellow : (s: string) => s;
|
|
210
|
+
|
|
211
|
+
const header = [
|
|
212
|
+
rarityColor(STARS[r.rarity]),
|
|
213
|
+
rarityColor(r.rarity.toUpperCase()),
|
|
214
|
+
c.bold(r.species),
|
|
215
|
+
r.eye,
|
|
216
|
+
r.hat !== "none" ? c.cyan(`[${r.hat}]`) : "",
|
|
217
|
+
r.shiny ? c.yellow("✨SHINY") : "",
|
|
218
|
+
]
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
.join(" ");
|
|
221
|
+
|
|
222
|
+
const statLine = STAT_NAMES.map((s) => {
|
|
223
|
+
const tag = s === r.peak ? c.green("↑") : s === r.dump ? c.red("↓") : " ";
|
|
224
|
+
return `${c.dim(s)}: ${String(r.stats[s]).padStart(3)}${tag}`;
|
|
225
|
+
}).join(" ");
|
|
226
|
+
|
|
227
|
+
const infoLines = [
|
|
228
|
+
header,
|
|
229
|
+
statLine,
|
|
230
|
+
`${c.dim("Total:")} ${r.total}/500`,
|
|
231
|
+
`${c.dim("UUID:")} ${c.cyan(r.uuid)}`,
|
|
232
|
+
];
|
|
233
|
+
|
|
234
|
+
// Render sprite and colorize it
|
|
235
|
+
const sprite = renderSprite(r).map((line) => shinyColor(line));
|
|
236
|
+
const spriteWidth = 14;
|
|
237
|
+
|
|
238
|
+
// Pad both sides to equal height
|
|
239
|
+
const height = Math.max(sprite.length, infoLines.length);
|
|
240
|
+
while (sprite.length < height) sprite.push(" ".repeat(12));
|
|
241
|
+
while (infoLines.length < height) infoLines.push("");
|
|
242
|
+
|
|
243
|
+
// Combine sprite + info side by side
|
|
244
|
+
const combined = sprite
|
|
245
|
+
.map((spriteLine, i) => {
|
|
246
|
+
const paddedSprite = padVisual(spriteLine, spriteWidth);
|
|
247
|
+
return ` ${paddedSprite}${infoLines[i]}`;
|
|
248
|
+
})
|
|
249
|
+
.join("\n");
|
|
250
|
+
|
|
251
|
+
console.log();
|
|
252
|
+
console.log(combined);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function main() {
|
|
256
|
+
p.intro(c.bgCyanBlack(" claude-petpet "));
|
|
257
|
+
|
|
258
|
+
const raw = await promptFilters();
|
|
259
|
+
const filters = parseFilters(raw);
|
|
260
|
+
|
|
261
|
+
p.log.info(
|
|
262
|
+
`Searching for: ${c.cyan(formatSummary(filters))} ${c.dim(`(top ${filters.limit}, up to ${filters.max.toLocaleString()} seeds)`)}`,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const { results } = await searchWithProgress(filters);
|
|
266
|
+
|
|
267
|
+
if (results.length === 0) {
|
|
268
|
+
p.log.warn("No matches found. Try relaxing filters or increasing max seeds.");
|
|
269
|
+
p.outro("Done.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
p.log.success(`Found ${c.green(String(results.length))} match${results.length > 1 ? "es" : ""}!`);
|
|
274
|
+
|
|
275
|
+
for (const r of results) {
|
|
276
|
+
printResult(r);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
p.log.info(
|
|
280
|
+
"To use: edit ~/.claude/.config.json → set oauthAccount.accountUuid to the UUID above.",
|
|
281
|
+
);
|
|
282
|
+
p.log.warn("Re-authenticating will overwrite it back to your real UUID.");
|
|
283
|
+
p.outro(c.green("Happy hunting!"));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
void main();
|
package/src/color.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const RESET = "\x1b[0m";
|
|
2
|
+
const BOLD = "\x1b[1m";
|
|
3
|
+
const DIM = "\x1b[2m";
|
|
4
|
+
|
|
5
|
+
function fg(cssColor: string) {
|
|
6
|
+
return (text: string) => {
|
|
7
|
+
const ansi = Bun.color(cssColor, "ansi");
|
|
8
|
+
return ansi ? `${ansi}${text}${RESET}` : text;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function bgFg(bgCss: string, fgCss: string) {
|
|
13
|
+
return (text: string) => {
|
|
14
|
+
const bgAnsi = Bun.color(bgCss, "ansi")?.replace("[38;", "[48;");
|
|
15
|
+
const fgAnsi = Bun.color(fgCss, "ansi");
|
|
16
|
+
if (!bgAnsi || !fgAnsi) return text;
|
|
17
|
+
return `${bgAnsi}${fgAnsi}${text}${RESET}`;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const c = {
|
|
22
|
+
bold: (text: string) => `${BOLD}${text}${RESET}`,
|
|
23
|
+
dim: (text: string) => `${DIM}${text}${RESET}`,
|
|
24
|
+
red: fg("red"),
|
|
25
|
+
green: fg("lime"),
|
|
26
|
+
blue: fg("dodgerblue"),
|
|
27
|
+
yellow: fg("yellow"),
|
|
28
|
+
magenta: fg("magenta"),
|
|
29
|
+
cyan: fg("cyan"),
|
|
30
|
+
bgCyanBlack: bgFg("cyan", "black"),
|
|
31
|
+
};
|
package/src/consts.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Rarity } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const SALT = "friend-2026-401";
|
|
4
|
+
|
|
5
|
+
export const RARITY_WEIGHTS: Record<Rarity, number> = {
|
|
6
|
+
common: 60,
|
|
7
|
+
uncommon: 25,
|
|
8
|
+
rare: 10,
|
|
9
|
+
epic: 4,
|
|
10
|
+
legendary: 1,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const RARITY_FLOOR: Record<Rarity, number> = {
|
|
14
|
+
common: 5,
|
|
15
|
+
uncommon: 15,
|
|
16
|
+
rare: 25,
|
|
17
|
+
epic: 35,
|
|
18
|
+
legendary: 50,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const STARS: Record<Rarity, string> = {
|
|
22
|
+
common: "★",
|
|
23
|
+
uncommon: "★★",
|
|
24
|
+
rare: "★★★",
|
|
25
|
+
epic: "★★★★",
|
|
26
|
+
legendary: "★★★★★",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const YIELD_EVERY = 50_000;
|
package/src/enums.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const RARITIES = ["common", "uncommon", "rare", "epic", "legendary"] as const;
|
|
2
|
+
|
|
3
|
+
export const SPECIES = [
|
|
4
|
+
"duck",
|
|
5
|
+
"goose",
|
|
6
|
+
"blob",
|
|
7
|
+
"cat",
|
|
8
|
+
"dragon",
|
|
9
|
+
"octopus",
|
|
10
|
+
"owl",
|
|
11
|
+
"penguin",
|
|
12
|
+
"turtle",
|
|
13
|
+
"snail",
|
|
14
|
+
"ghost",
|
|
15
|
+
"axolotl",
|
|
16
|
+
"capybara",
|
|
17
|
+
"cactus",
|
|
18
|
+
"robot",
|
|
19
|
+
"rabbit",
|
|
20
|
+
"mushroom",
|
|
21
|
+
"chonk",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export const EYES = ["·", "✦", "×", "◉", "@", "°"] as const;
|
|
25
|
+
|
|
26
|
+
export const HATS = [
|
|
27
|
+
"none",
|
|
28
|
+
"crown",
|
|
29
|
+
"tophat",
|
|
30
|
+
"propeller",
|
|
31
|
+
"halo",
|
|
32
|
+
"wizard",
|
|
33
|
+
"beanie",
|
|
34
|
+
"tinyduck",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export const STAT_NAMES = ["DEBUGGING", "PATIENCE", "CHAOS", "WISDOM", "SNARK"] as const;
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { RARITIES, SPECIES, HATS, EYES, STAT_NAMES } from "./enums.ts";
|
|
2
|
+
import { SALT, RARITY_WEIGHTS, RARITY_FLOOR } from "./consts.ts";
|
|
3
|
+
import type { Rarity, StatName, Roll, SearchFilters, SearchResult } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
// Mulberry32 PRNG — deterministic 32-bit PRNG matching the companion
|
|
6
|
+
// generation algorithm used by Claude Code's buddy system.
|
|
7
|
+
export function mulberry32(seed: number): () => number {
|
|
8
|
+
let a = seed >>> 0;
|
|
9
|
+
return function () {
|
|
10
|
+
a |= 0;
|
|
11
|
+
a = (a + 0x6d2b79f5) | 0;
|
|
12
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
13
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
14
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hashString(s: string): number {
|
|
19
|
+
if (typeof Bun !== "undefined") {
|
|
20
|
+
return Number(BigInt(Bun.hash(s)) & 0xffffffffn);
|
|
21
|
+
}
|
|
22
|
+
// FNV-1a fallback for non-Bun runtimes
|
|
23
|
+
let h = 2166136261;
|
|
24
|
+
for (let i = 0; i < s.length; i++) {
|
|
25
|
+
h ^= s.charCodeAt(i);
|
|
26
|
+
h = Math.imul(h, 16777619);
|
|
27
|
+
}
|
|
28
|
+
return h >>> 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function pick<T>(rng: () => number, arr: readonly T[]): T {
|
|
32
|
+
return arr[Math.floor(rng() * arr.length)]!;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function rollRarity(rng: () => number): Rarity {
|
|
36
|
+
const total = Object.values(RARITY_WEIGHTS).reduce((a, b) => a + b, 0);
|
|
37
|
+
let roll = rng() * total;
|
|
38
|
+
for (const rarity of RARITIES) {
|
|
39
|
+
roll -= RARITY_WEIGHTS[rarity];
|
|
40
|
+
if (roll < 0) return rarity;
|
|
41
|
+
}
|
|
42
|
+
return "common";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function rollStats(rng: () => number, rarity: Rarity) {
|
|
46
|
+
const floor = RARITY_FLOOR[rarity];
|
|
47
|
+
const peak = pick(rng, STAT_NAMES);
|
|
48
|
+
let dump = pick(rng, STAT_NAMES);
|
|
49
|
+
while (dump === peak) dump = pick(rng, STAT_NAMES);
|
|
50
|
+
|
|
51
|
+
const stats = {} as Record<StatName, number>;
|
|
52
|
+
for (const name of STAT_NAMES) {
|
|
53
|
+
if (name === peak) {
|
|
54
|
+
stats[name] = Math.min(100, floor + 50 + Math.floor(rng() * 30));
|
|
55
|
+
} else if (name === dump) {
|
|
56
|
+
stats[name] = Math.max(1, floor - 10 + Math.floor(rng() * 15));
|
|
57
|
+
} else {
|
|
58
|
+
stats[name] = floor + Math.floor(rng() * 40);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { stats, peak, dump };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function rollFrom(userId: string): Roll {
|
|
65
|
+
const rng = mulberry32(hashString(userId + SALT));
|
|
66
|
+
const rarity = rollRarity(rng);
|
|
67
|
+
const species = pick(rng, SPECIES);
|
|
68
|
+
const eye = pick(rng, EYES);
|
|
69
|
+
const hat = rarity === "common" ? "none" : pick(rng, HATS);
|
|
70
|
+
const shiny = rng() < 0.01;
|
|
71
|
+
const { stats, peak, dump } = rollStats(rng, rarity);
|
|
72
|
+
const total = Object.values(stats).reduce((a, b) => a + b, 0);
|
|
73
|
+
return { rarity, species, eye, hat, shiny, stats, peak, dump, total };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function matchesFilters(roll: Roll, filters: SearchFilters): boolean {
|
|
77
|
+
if (filters.species && roll.species !== filters.species) return false;
|
|
78
|
+
if (filters.rarity && roll.rarity !== filters.rarity) return false;
|
|
79
|
+
if (filters.eye && roll.eye !== filters.eye) return false;
|
|
80
|
+
if (filters.hat && roll.hat !== filters.hat) return false;
|
|
81
|
+
if (filters.shiny && !roll.shiny) return false;
|
|
82
|
+
if (filters.peak && roll.peak !== filters.peak) return false;
|
|
83
|
+
if (filters.dump && roll.dump !== filters.dump) return false;
|
|
84
|
+
if (filters.minTotal && roll.total < filters.minTotal) return false;
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function search(filters: SearchFilters): SearchResult[] {
|
|
89
|
+
const results: SearchResult[] = [];
|
|
90
|
+
|
|
91
|
+
for (let i = 0; i < filters.max; i++) {
|
|
92
|
+
const uuid = crypto.randomUUID();
|
|
93
|
+
const roll = rollFrom(uuid);
|
|
94
|
+
|
|
95
|
+
if (!matchesFilters(roll, filters)) continue;
|
|
96
|
+
|
|
97
|
+
results.push({ ...roll, uuid });
|
|
98
|
+
results.sort((a, b) => b.total - a.total);
|
|
99
|
+
if (results.length > filters.limit) results.length = filters.limit;
|
|
100
|
+
if (results.length >= filters.limit) break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return results;
|
|
104
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { RARITIES, SPECIES, EYES, HATS, STAT_NAMES } from "./enums.ts";
|
|
2
|
+
export { SALT, RARITY_WEIGHTS, RARITY_FLOOR, STARS } from "./consts.ts";
|
|
3
|
+
export {
|
|
4
|
+
mulberry32,
|
|
5
|
+
hashString,
|
|
6
|
+
pick,
|
|
7
|
+
rollRarity,
|
|
8
|
+
rollStats,
|
|
9
|
+
rollFrom,
|
|
10
|
+
matchesFilters,
|
|
11
|
+
search,
|
|
12
|
+
} from "./helpers.ts";
|
|
13
|
+
export { renderSprite } from "./sprites.ts";
|
|
14
|
+
export type { Rarity, StatName, Roll, SearchFilters, SearchResult } from "./types.ts";
|
package/src/sprites.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Roll } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
// Sprite bodies — frame 0 only (static display).
|
|
4
|
+
// Each sprite is 5 lines tall, 12 wide (after {E} → 1-char substitution).
|
|
5
|
+
// Line 0 is the hat slot — blank unless a hat is applied.
|
|
6
|
+
const BODIES: Record<string, string[]> = {
|
|
7
|
+
duck: [" ", " __ ", " <({E} )___ ", " ( ._> ", " `--´ "],
|
|
8
|
+
goose: [" ", " ({E}> ", " || ", " _(__)_ ", " ^^^^ "],
|
|
9
|
+
blob: [" ", " .----. ", " ( {E} {E} ) ", " ( ) ", " `----´ "],
|
|
10
|
+
cat: [" ", " /\\_/\\ ", " ( {E} {E}) ", " ( ω ) ", ' (")_(") '],
|
|
11
|
+
dragon: [" ", " /^\\ /^\\ ", " < {E} {E} > ", " ( ~~ ) ", " `-vvvv-´ "],
|
|
12
|
+
octopus: [" ", " .----. ", " ( {E} {E} ) ", " (______) ", " /\\/\\/\\/\\ "],
|
|
13
|
+
owl: [" ", " /\\ /\\ ", " (({E})({E})) ", " ( >< ) ", " `----´ "],
|
|
14
|
+
penguin: [" ", " .---. ", " ({E}>{E}) ", " /( )\\ ", " `---´ "],
|
|
15
|
+
turtle: [" ", " _,--._ ", " ( {E} {E} ) ", " /[______]\\ ", " `` `` "],
|
|
16
|
+
snail: [" ", " {E} .--. ", " \\ ( @ ) ", " \\_`--´ ", " ~~~~~~~ "],
|
|
17
|
+
ghost: [" ", " .----. ", " / {E} {E} \\ ", " | | ", " ~`~``~`~ "],
|
|
18
|
+
axolotl: [" ", "}~(______)~{", "}~({E} .. {E})~{", " ( .--. ) ", " (_/ \\_) "],
|
|
19
|
+
capybara: [" ", " n______n ", " ( {E} {E} ) ", " ( oo ) ", " `------´ "],
|
|
20
|
+
cactus: [" ", " n ____ n ", " | |{E} {E}| | ", " |_| |_| ", " | | "],
|
|
21
|
+
robot: [" ", " .[||]. ", " [ {E} {E} ] ", " [ ==== ] ", " `------´ "],
|
|
22
|
+
rabbit: [" ", " (\\__/) ", " ( {E} {E} ) ", " =( .. )= ", ' (")__(") '],
|
|
23
|
+
mushroom: [" ", " .-o-OO-o-. ", "(__________)", " |{E} {E}| ", " |____| "],
|
|
24
|
+
chonk: [" ", " /\\ /\\ ", " ( {E} {E} ) ", " ( .. ) ", " `------´ "],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const HAT_LINES: Record<string, string> = {
|
|
28
|
+
none: "",
|
|
29
|
+
crown: " \\^^^/ ",
|
|
30
|
+
tophat: " [___] ",
|
|
31
|
+
propeller: " -+- ",
|
|
32
|
+
halo: " ( ) ",
|
|
33
|
+
wizard: " /^\\ ",
|
|
34
|
+
beanie: " (___) ",
|
|
35
|
+
tinyduck: " ,> ",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function renderSprite(roll: Roll): string[] {
|
|
39
|
+
const body = (BODIES[roll.species] ?? BODIES["blob"]!).map((line) =>
|
|
40
|
+
line.replaceAll("{E}", roll.eye),
|
|
41
|
+
);
|
|
42
|
+
const lines = [...body];
|
|
43
|
+
|
|
44
|
+
if (roll.hat !== "none" && !lines[0]!.trim()) {
|
|
45
|
+
lines[0] = HAT_LINES[roll.hat] ?? "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Drop blank hat row when no hat is present
|
|
49
|
+
if (!lines[0]!.trim()) {
|
|
50
|
+
lines.shift();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return lines;
|
|
54
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RARITIES, STAT_NAMES } from "./enums.ts";
|
|
2
|
+
|
|
3
|
+
export type Rarity = (typeof RARITIES)[number];
|
|
4
|
+
export type StatName = (typeof STAT_NAMES)[number];
|
|
5
|
+
|
|
6
|
+
export type Roll = {
|
|
7
|
+
rarity: Rarity;
|
|
8
|
+
species: string;
|
|
9
|
+
eye: string;
|
|
10
|
+
hat: string;
|
|
11
|
+
shiny: boolean;
|
|
12
|
+
stats: Record<StatName, number>;
|
|
13
|
+
peak: StatName;
|
|
14
|
+
dump: StatName;
|
|
15
|
+
total: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SearchFilters = {
|
|
19
|
+
species?: string;
|
|
20
|
+
rarity?: Rarity;
|
|
21
|
+
eye?: string;
|
|
22
|
+
hat?: string;
|
|
23
|
+
shiny?: boolean;
|
|
24
|
+
peak?: StatName;
|
|
25
|
+
dump?: StatName;
|
|
26
|
+
minTotal?: number;
|
|
27
|
+
limit: number;
|
|
28
|
+
max: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type SearchResult = Roll & { uuid: string };
|