any-buddy 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/README.md +246 -0
- package/assets/current.svg +44 -0
- package/assets/demo.svg +44 -0
- package/assets/options.svg +44 -0
- package/assets/species.svg +44 -0
- package/bin/cli.mjs +88 -0
- package/lib/config.mjs +132 -0
- package/lib/constants.mjs +41 -0
- package/lib/finder-worker.mjs +96 -0
- package/lib/finder.mjs +25 -0
- package/lib/generation.mjs +96 -0
- package/lib/patcher.mjs +168 -0
- package/lib/sprites.mjs +147 -0
- package/lib/tui.mjs +420 -0
- package/package.json +38 -0
package/lib/sprites.mjs
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Each sprite is 5 lines tall, ~12 wide. {E} = eye placeholder.
|
|
2
|
+
// 3 frames per species for idle animation.
|
|
3
|
+
const BODIES = {
|
|
4
|
+
duck: [
|
|
5
|
+
[' ', ' __ ', ' <({E} )___ ', ' ( ._> ', ' `--´ '],
|
|
6
|
+
[' ', ' __ ', ' <({E} )___ ', ' ( ._> ', ' `--´~ '],
|
|
7
|
+
[' ', ' __ ', ' <({E} )___ ', ' ( .__> ', ' `--´ '],
|
|
8
|
+
],
|
|
9
|
+
goose: [
|
|
10
|
+
[' ', ' ({E}> ', ' || ', ' _(__)_ ', ' ^^^^ '],
|
|
11
|
+
[' ', ' ({E}> ', ' || ', ' _(__)_ ', ' ^^^^ '],
|
|
12
|
+
[' ', ' ({E}>> ', ' || ', ' _(__)_ ', ' ^^^^ '],
|
|
13
|
+
],
|
|
14
|
+
blob: [
|
|
15
|
+
[' ', ' .----. ', ' ( {E} {E} ) ', ' ( ) ', ' `----´ '],
|
|
16
|
+
[' ', ' .------. ', ' ( {E} {E} ) ', ' ( ) ', ' `------´ '],
|
|
17
|
+
[' ', ' .--. ', ' ({E} {E}) ', ' ( ) ', ' `--´ '],
|
|
18
|
+
],
|
|
19
|
+
cat: [
|
|
20
|
+
[' ', ' /\\_/\\ ', ' ( {E} {E}) ', ' ( ω ) ', ' (")_(") '],
|
|
21
|
+
[' ', ' /\\_/\\ ', ' ( {E} {E}) ', ' ( ω ) ', ' (")_(")~ '],
|
|
22
|
+
[' ', ' /\\-/\\ ', ' ( {E} {E}) ', ' ( ω ) ', ' (")_(") '],
|
|
23
|
+
],
|
|
24
|
+
dragon: [
|
|
25
|
+
[' ', ' /^\\ /^\\ ', ' < {E} {E} > ', ' ( ~~ ) ', ' `-vvvv-´ '],
|
|
26
|
+
[' ', ' /^\\ /^\\ ', ' < {E} {E} > ', ' ( ) ', ' `-vvvv-´ '],
|
|
27
|
+
[' ~ ~ ', ' /^\\ /^\\ ', ' < {E} {E} > ', ' ( ~~ ) ', ' `-vvvv-´ '],
|
|
28
|
+
],
|
|
29
|
+
octopus: [
|
|
30
|
+
[' ', ' .----. ', ' ( {E} {E} ) ', ' (______) ', ' /\\/\\/\\/\\ '],
|
|
31
|
+
[' ', ' .----. ', ' ( {E} {E} ) ', ' (______) ', ' \\/\\/\\/\\/ '],
|
|
32
|
+
[' o ', ' .----. ', ' ( {E} {E} ) ', ' (______) ', ' /\\/\\/\\/\\ '],
|
|
33
|
+
],
|
|
34
|
+
owl: [
|
|
35
|
+
[' ', ' /\\ /\\ ', ' (({E})({E})) ', ' ( >< ) ', ' `----´ '],
|
|
36
|
+
[' ', ' /\\ /\\ ', ' (({E})({E})) ', ' ( >< ) ', ' .----. '],
|
|
37
|
+
[' ', ' /\\ /\\ ', ' (({E})(-)) ', ' ( >< ) ', ' `----´ '],
|
|
38
|
+
],
|
|
39
|
+
penguin: [
|
|
40
|
+
[' ', ' .---. ', ' ({E}>{E}) ', ' /( )\\ ', ' `---´ '],
|
|
41
|
+
[' ', ' .---. ', ' ({E}>{E}) ', ' |( )| ', ' `---´ '],
|
|
42
|
+
[' .---. ', ' ({E}>{E}) ', ' /( )\\ ', ' `---´ ', ' ~ ~ '],
|
|
43
|
+
],
|
|
44
|
+
turtle: [
|
|
45
|
+
[' ', ' _,--._ ', ' ( {E} {E} ) ', ' /[______]\\ ', ' `` `` '],
|
|
46
|
+
[' ', ' _,--._ ', ' ( {E} {E} ) ', ' /[______]\\ ', ' `` `` '],
|
|
47
|
+
[' ', ' _,--._ ', ' ( {E} {E} ) ', ' /[======]\\ ', ' `` `` '],
|
|
48
|
+
],
|
|
49
|
+
snail: [
|
|
50
|
+
[' ', ' {E} .--. ', ' \\ ( @ ) ', ' \\_`--´ ', ' ~~~~~~~ '],
|
|
51
|
+
[' ', ' {E} .--. ', ' | ( @ ) ', ' \\_`--´ ', ' ~~~~~~~ '],
|
|
52
|
+
[' ', ' {E} .--. ', ' \\ ( @ ) ', ' \\_`--´ ', ' ~~~~~~ '],
|
|
53
|
+
],
|
|
54
|
+
ghost: [
|
|
55
|
+
[' ', ' .----. ', ' / {E} {E} \\ ', ' | | ', ' ~`~``~`~ '],
|
|
56
|
+
[' ', ' .----. ', ' / {E} {E} \\ ', ' | | ', ' `~`~~`~` '],
|
|
57
|
+
[' ~ ~ ', ' .----. ', ' / {E} {E} \\ ', ' | | ', ' ~~`~~`~~ '],
|
|
58
|
+
],
|
|
59
|
+
axolotl: [
|
|
60
|
+
[' ', '}~(______)~{', '}~({E} .. {E})~{', ' ( .--. ) ', ' (_/ \\_) '],
|
|
61
|
+
[' ', '~}(______){~', '~}({E} .. {E}){~', ' ( .--. ) ', ' (_/ \\_) '],
|
|
62
|
+
[' ', '}~(______)~{', '}~({E} .. {E})~{', ' ( -- ) ', ' ~_/ \\_~ '],
|
|
63
|
+
],
|
|
64
|
+
capybara: [
|
|
65
|
+
[' ', ' n______n ', ' ( {E} {E} ) ', ' ( oo ) ', ' `------´ '],
|
|
66
|
+
[' ', ' n______n ', ' ( {E} {E} ) ', ' ( Oo ) ', ' `------´ '],
|
|
67
|
+
[' ~ ~ ', ' u______n ', ' ( {E} {E} ) ', ' ( oo ) ', ' `------´ '],
|
|
68
|
+
],
|
|
69
|
+
cactus: [
|
|
70
|
+
[' ', ' n ____ n ', ' | |{E} {E}| | ', ' |_| |_| ', ' | | '],
|
|
71
|
+
[' ', ' ____ ', ' n |{E} {E}| n ', ' |_| |_| ', ' | | '],
|
|
72
|
+
[' n n ', ' | ____ | ', ' | |{E} {E}| | ', ' |_| |_| ', ' | | '],
|
|
73
|
+
],
|
|
74
|
+
robot: [
|
|
75
|
+
[' ', ' .[||]. ', ' [ {E} {E} ] ', ' [ ==== ] ', ' `------´ '],
|
|
76
|
+
[' ', ' .[||]. ', ' [ {E} {E} ] ', ' [ -==- ] ', ' `------´ '],
|
|
77
|
+
[' * ', ' .[||]. ', ' [ {E} {E} ] ', ' [ ==== ] ', ' `------´ '],
|
|
78
|
+
],
|
|
79
|
+
rabbit: [
|
|
80
|
+
[' ', ' (\\__/) ', ' ( {E} {E} ) ', ' =( .. )= ', ' (")__(") '],
|
|
81
|
+
[' ', ' (|__/) ', ' ( {E} {E} ) ', ' =( .. )= ', ' (")__(") '],
|
|
82
|
+
[' ', ' (\\__/) ', ' ( {E} {E} ) ', ' =( . . )= ', ' (")__(") '],
|
|
83
|
+
],
|
|
84
|
+
mushroom: [
|
|
85
|
+
[' ', ' .-o-OO-o-. ', '(__________)', ' |{E} {E}| ', ' |____| '],
|
|
86
|
+
[' ', ' .-O-oo-O-. ', '(__________)', ' |{E} {E}| ', ' |____| '],
|
|
87
|
+
[' . o . ', ' .-o-OO-o-. ', '(__________)', ' |{E} {E}| ', ' |____| '],
|
|
88
|
+
],
|
|
89
|
+
chonk: [
|
|
90
|
+
[' ', ' /\\ /\\ ', ' ( {E} {E} ) ', ' ( .. ) ', ' `------´ '],
|
|
91
|
+
[' ', ' /\\ /| ', ' ( {E} {E} ) ', ' ( .. ) ', ' `------´ '],
|
|
92
|
+
[' ', ' /\\ /\\ ', ' ( {E} {E} ) ', ' ( .. ) ', ' `------´~ '],
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const HAT_LINES = {
|
|
97
|
+
none: '',
|
|
98
|
+
crown: ' \\^^^/ ',
|
|
99
|
+
tophat: ' [___] ',
|
|
100
|
+
propeller: ' -+- ',
|
|
101
|
+
halo: ' ( ) ',
|
|
102
|
+
wizard: ' /^\\ ',
|
|
103
|
+
beanie: ' (___) ',
|
|
104
|
+
tinyduck: ' ,> ',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function renderSprite(bones, frame = 0) {
|
|
108
|
+
const frames = BODIES[bones.species];
|
|
109
|
+
const body = frames[frame % frames.length].map(line =>
|
|
110
|
+
line.replaceAll('{E}', bones.eye),
|
|
111
|
+
);
|
|
112
|
+
const lines = [...body];
|
|
113
|
+
if (bones.hat !== 'none' && !lines[0].trim()) {
|
|
114
|
+
lines[0] = HAT_LINES[bones.hat];
|
|
115
|
+
}
|
|
116
|
+
if (!lines[0].trim() && frames.every(f => !f[0].trim())) lines.shift();
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function spriteFrameCount(species) {
|
|
121
|
+
return BODIES[species].length;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function renderFace(bones) {
|
|
125
|
+
const e = bones.eye;
|
|
126
|
+
switch (bones.species) {
|
|
127
|
+
case 'duck':
|
|
128
|
+
case 'goose': return `(${e}>`;
|
|
129
|
+
case 'blob': return `(${e}${e})`;
|
|
130
|
+
case 'cat': return `=${e}ω${e}=`;
|
|
131
|
+
case 'dragon': return `<${e}~${e}>`;
|
|
132
|
+
case 'octopus': return `~(${e}${e})~`;
|
|
133
|
+
case 'owl': return `(${e})(${e})`;
|
|
134
|
+
case 'penguin': return `(${e}>)`;
|
|
135
|
+
case 'turtle': return `[${e}_${e}]`;
|
|
136
|
+
case 'snail': return `${e}(@)`;
|
|
137
|
+
case 'ghost': return `/${e}${e}\\`;
|
|
138
|
+
case 'axolotl': return `}${e}.${e}{`;
|
|
139
|
+
case 'capybara': return `(${e}oo${e})`;
|
|
140
|
+
case 'cactus': return `|${e} ${e}|`;
|
|
141
|
+
case 'robot': return `[${e}${e}]`;
|
|
142
|
+
case 'rabbit': return `(${e}..${e})`;
|
|
143
|
+
case 'mushroom': return `|${e} ${e}|`;
|
|
144
|
+
case 'chonk': return `(${e}.${e})`;
|
|
145
|
+
default: return `(${e}${e})`;
|
|
146
|
+
}
|
|
147
|
+
}
|
package/lib/tui.mjs
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { select, confirm, input } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { SPECIES, EYES, HATS, RARITIES, RARITY_STARS, RARITY_WEIGHTS, ORIGINAL_SALT } from './constants.mjs';
|
|
4
|
+
import { roll } from './generation.mjs';
|
|
5
|
+
import { renderSprite, renderFace } from './sprites.mjs';
|
|
6
|
+
import { findSalt } from './finder.mjs';
|
|
7
|
+
import { findClaudeBinary, getCurrentSalt, patchBinary, verifySalt, restoreBinary, isClaudeRunning } from './patcher.mjs';
|
|
8
|
+
import { getClaudeUserId, savePetConfig, loadPetConfig, isHookInstalled, installHook, removeHook, getCompanionName, renameCompanion } from './config.mjs';
|
|
9
|
+
|
|
10
|
+
const RARITY_CHALK = {
|
|
11
|
+
common: chalk.gray,
|
|
12
|
+
uncommon: chalk.green,
|
|
13
|
+
rare: chalk.blue,
|
|
14
|
+
epic: chalk.magenta,
|
|
15
|
+
legendary: chalk.yellow,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function formatSprite(bones, frame = 0) {
|
|
19
|
+
return renderSprite(bones, frame).join('\n');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function spritePreview(species, eye, hat, rarity) {
|
|
23
|
+
const bones = { species, eye, hat: rarity === 'common' ? 'none' : hat, rarity, shiny: false, stats: {} };
|
|
24
|
+
const lines = renderSprite(bones, 0);
|
|
25
|
+
return lines.map(l => l.trimEnd()).join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function colorize(text, rarity) {
|
|
29
|
+
return (RARITY_CHALK[rarity] || chalk.white)(text);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function banner() {
|
|
33
|
+
console.log(chalk.bold('\n claude-code-any-buddy'));
|
|
34
|
+
console.log(chalk.dim(' Pick any Claude Code companion pet\n'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function showPet(bones, label = 'Your pet') {
|
|
38
|
+
const rarityColor = RARITY_CHALK[bones.rarity] || chalk.white;
|
|
39
|
+
console.log(rarityColor(`\n ${label}: ${bones.species} ${RARITY_STARS[bones.rarity]}`));
|
|
40
|
+
console.log(rarityColor(` Rarity: ${bones.rarity} Eyes: ${bones.eye} Hat: ${bones.hat} Shiny: ${bones.shiny ? 'YES' : 'no'}`));
|
|
41
|
+
const lines = renderSprite(bones, 0);
|
|
42
|
+
console.log();
|
|
43
|
+
for (const line of lines) {
|
|
44
|
+
console.log(rarityColor(' ' + line));
|
|
45
|
+
}
|
|
46
|
+
console.log();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Subcommands ───
|
|
50
|
+
|
|
51
|
+
export async function runCurrent() {
|
|
52
|
+
banner();
|
|
53
|
+
const userId = getClaudeUserId();
|
|
54
|
+
console.log(chalk.dim(` User ID: ${userId.slice(0, 12)}...`));
|
|
55
|
+
|
|
56
|
+
// Show what the original salt produces
|
|
57
|
+
const origResult = roll(userId, ORIGINAL_SALT);
|
|
58
|
+
showPet(origResult.bones, 'Default pet (original salt)');
|
|
59
|
+
|
|
60
|
+
// Show patched pet if applicable
|
|
61
|
+
const config = loadPetConfig();
|
|
62
|
+
if (config?.salt && config.salt !== ORIGINAL_SALT) {
|
|
63
|
+
const patchedResult = roll(userId, config.salt);
|
|
64
|
+
showPet(patchedResult.bones, 'Active pet (patched)');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runPreview(flags = {}) {
|
|
69
|
+
banner();
|
|
70
|
+
|
|
71
|
+
const species = validateFlag('species', flags.species, SPECIES) ?? await selectSpecies();
|
|
72
|
+
const eye = validateFlag('eye', flags.eye, EYES) ?? await selectEyes(species);
|
|
73
|
+
const rarity = validateFlag('rarity', flags.rarity, RARITIES) ?? await selectRarity();
|
|
74
|
+
const hat = rarity === 'common' ? 'none'
|
|
75
|
+
: validateFlag('hat', flags.hat, HATS) ?? await selectHat(species, eye, rarity);
|
|
76
|
+
|
|
77
|
+
const bones = { species, eye, hat, rarity, shiny: false, stats: {} };
|
|
78
|
+
showPet(bones, 'Preview');
|
|
79
|
+
console.log(chalk.dim(' (Preview only - no changes made)\n'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function runApply({ silent = false } = {}) {
|
|
83
|
+
const config = loadPetConfig();
|
|
84
|
+
if (!config?.salt) {
|
|
85
|
+
if (!silent) console.error('No saved pet config. Run claude-code-any-buddy first.');
|
|
86
|
+
process.exit(silent ? 0 : 1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let binaryPath;
|
|
90
|
+
try {
|
|
91
|
+
binaryPath = findClaudeBinary();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (!silent) console.error(err.message);
|
|
94
|
+
process.exit(silent ? 0 : 1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check if already patched with our salt
|
|
98
|
+
const check = verifySalt(binaryPath, config.salt);
|
|
99
|
+
if (check.found >= 3) {
|
|
100
|
+
if (!silent) console.log(chalk.green(' Pet already applied.'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Find what salt is currently in the binary
|
|
105
|
+
const current = getCurrentSalt(binaryPath);
|
|
106
|
+
const oldSalt = current.patched ? null : ORIGINAL_SALT;
|
|
107
|
+
|
|
108
|
+
if (!oldSalt) {
|
|
109
|
+
// Binary has unknown salt — check if it's a previous any-buddy salt
|
|
110
|
+
// Try to find the salt from our config's previous application
|
|
111
|
+
if (config.previousSalt) {
|
|
112
|
+
const prevCheck = verifySalt(binaryPath, config.previousSalt);
|
|
113
|
+
if (prevCheck.found >= 3) {
|
|
114
|
+
const result = patchBinary(binaryPath, config.previousSalt, config.salt);
|
|
115
|
+
if (!silent) console.log(chalk.green(` Re-patched (${result.replacements} replacements).`));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Try original salt as fallback (maybe Claude updated)
|
|
120
|
+
const origCheck = verifySalt(binaryPath, ORIGINAL_SALT);
|
|
121
|
+
if (origCheck.found >= 3) {
|
|
122
|
+
const result = patchBinary(binaryPath, ORIGINAL_SALT, config.salt);
|
|
123
|
+
if (!silent) console.log(chalk.green(` Patched after update (${result.replacements} replacements).`));
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!silent) console.error('Could not find known salt in binary. Claude Code may have changed the salt string.');
|
|
127
|
+
process.exit(silent ? 0 : 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = patchBinary(binaryPath, oldSalt, config.salt);
|
|
131
|
+
if (!silent) {
|
|
132
|
+
console.log(chalk.green(` Applied (${result.replacements} replacements).`));
|
|
133
|
+
if (isClaudeRunning(binaryPath)) {
|
|
134
|
+
console.log(chalk.yellow(' Restart Claude Code for the change to take effect.'));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function runRestore() {
|
|
140
|
+
banner();
|
|
141
|
+
const binaryPath = findClaudeBinary();
|
|
142
|
+
|
|
143
|
+
const config = loadPetConfig();
|
|
144
|
+
if (config?.salt && config.salt !== ORIGINAL_SALT) {
|
|
145
|
+
// Try to patch back to original
|
|
146
|
+
const check = verifySalt(binaryPath, config.salt);
|
|
147
|
+
if (check.found >= 3) {
|
|
148
|
+
patchBinary(binaryPath, config.salt, ORIGINAL_SALT);
|
|
149
|
+
console.log(chalk.green(' Restored original pet salt.'));
|
|
150
|
+
} else {
|
|
151
|
+
// Try backup
|
|
152
|
+
try {
|
|
153
|
+
restoreBinary(binaryPath);
|
|
154
|
+
console.log(chalk.green(' Restored from backup.'));
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error(err.message);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
console.log(chalk.dim(' Already using original salt.'));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Clean up hook if installed
|
|
165
|
+
if (isHookInstalled()) {
|
|
166
|
+
removeHook();
|
|
167
|
+
console.log(chalk.dim(' Removed SessionStart hook.'));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Remove our config
|
|
171
|
+
savePetConfig({ salt: ORIGINAL_SALT, restored: true });
|
|
172
|
+
console.log();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function runInteractive(flags = {}) {
|
|
176
|
+
banner();
|
|
177
|
+
|
|
178
|
+
const userId = getClaudeUserId();
|
|
179
|
+
if (userId === 'anon') {
|
|
180
|
+
console.log(chalk.yellow(' Warning: No Claude Code user ID found. Using "anon".'));
|
|
181
|
+
console.log(chalk.yellow(' Make sure Claude Code is installed and you\'ve logged in.\n'));
|
|
182
|
+
} else {
|
|
183
|
+
console.log(chalk.dim(` User ID: ${userId.slice(0, 12)}...\n`));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Show current pet
|
|
187
|
+
const currentBones = roll(userId, ORIGINAL_SALT).bones;
|
|
188
|
+
showPet(currentBones, 'Your current default pet');
|
|
189
|
+
|
|
190
|
+
// Check if already patched
|
|
191
|
+
const existingConfig = loadPetConfig();
|
|
192
|
+
if (existingConfig?.salt && existingConfig.salt !== ORIGINAL_SALT) {
|
|
193
|
+
const patchedBones = roll(userId, existingConfig.salt).bones;
|
|
194
|
+
showPet(patchedBones, 'Your active patched pet');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Selection flow ───
|
|
198
|
+
console.log(chalk.bold(' Choose your new pet:\n'));
|
|
199
|
+
|
|
200
|
+
// Use flags if provided, otherwise prompt interactively
|
|
201
|
+
const species = validateFlag('species', flags.species, SPECIES) ?? await selectSpecies();
|
|
202
|
+
const eye = validateFlag('eye', flags.eye, EYES) ?? await selectEyes(species);
|
|
203
|
+
const rarity = validateFlag('rarity', flags.rarity, RARITIES) ?? await selectRarity();
|
|
204
|
+
const hat = rarity === 'common' ? 'none'
|
|
205
|
+
: validateFlag('hat', flags.hat, HATS) ?? await selectHat(species, eye, rarity);
|
|
206
|
+
|
|
207
|
+
// Final preview
|
|
208
|
+
const desired = { species, eye, hat, rarity };
|
|
209
|
+
const previewBones = { ...desired, shiny: false, stats: {} };
|
|
210
|
+
showPet(previewBones, 'Your selection');
|
|
211
|
+
|
|
212
|
+
const proceed = flags.yes || await confirm({
|
|
213
|
+
message: 'Find a matching salt and apply?',
|
|
214
|
+
default: true,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (!proceed) {
|
|
218
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ─── Find salt ───
|
|
223
|
+
console.log(chalk.dim('\n Searching for matching salt...'));
|
|
224
|
+
|
|
225
|
+
const result = findSalt(userId, desired, {
|
|
226
|
+
onProgress: (attempts, ms) => {
|
|
227
|
+
process.stdout.write(chalk.dim(`\r Tried ${(attempts / 1000).toFixed(0)}k seeds (${(ms / 1000).toFixed(1)}s)...`));
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
console.log(chalk.green(`\r Found salt "${result.salt}" in ${result.attempts.toLocaleString()} attempts (${(result.elapsed / 1000).toFixed(1)}s)`));
|
|
232
|
+
const foundBones = roll(userId, result.salt).bones;
|
|
233
|
+
showPet(foundBones, 'Your new pet');
|
|
234
|
+
|
|
235
|
+
// ─── Patch binary ───
|
|
236
|
+
let binaryPath;
|
|
237
|
+
try {
|
|
238
|
+
binaryPath = findClaudeBinary();
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(chalk.red(`\n ${err.message}`));
|
|
241
|
+
console.log(chalk.dim(` Salt saved. You can manually apply later with: claude-code-any-buddy apply\n`));
|
|
242
|
+
savePetConfig({
|
|
243
|
+
salt: result.salt,
|
|
244
|
+
species: desired.species,
|
|
245
|
+
rarity: desired.rarity,
|
|
246
|
+
eye: desired.eye,
|
|
247
|
+
hat: desired.hat,
|
|
248
|
+
appliedAt: new Date().toISOString(),
|
|
249
|
+
});
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
console.log(chalk.dim(` Binary: ${binaryPath}`));
|
|
254
|
+
|
|
255
|
+
// Find what's currently in the binary
|
|
256
|
+
const current = getCurrentSalt(binaryPath);
|
|
257
|
+
let oldSalt;
|
|
258
|
+
if (!current.patched) {
|
|
259
|
+
oldSalt = ORIGINAL_SALT;
|
|
260
|
+
} else if (existingConfig?.salt) {
|
|
261
|
+
oldSalt = existingConfig.salt;
|
|
262
|
+
const check = verifySalt(binaryPath, oldSalt);
|
|
263
|
+
if (check.found < 3) {
|
|
264
|
+
console.error(chalk.red(' Cannot find current salt in binary. Try restoring first.'));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
console.error(chalk.red(' Binary appears patched but no previous salt on record. Try restoring first.'));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const running = isClaudeRunning(binaryPath);
|
|
273
|
+
if (running) {
|
|
274
|
+
console.log(chalk.yellow('\n Claude Code is currently running.'));
|
|
275
|
+
console.log(chalk.yellow(' The patch is safe (uses atomic rename — the running process'));
|
|
276
|
+
console.log(chalk.yellow(' keeps using the old binary in memory), but the change won\'t'));
|
|
277
|
+
console.log(chalk.yellow(' take effect until you restart Claude Code.\n'));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const applyNow = flags.yes || await confirm({
|
|
281
|
+
message: running
|
|
282
|
+
? 'Patch binary? (you\'ll need to restart Claude Code after)'
|
|
283
|
+
: 'Patch binary? (backup will be created)',
|
|
284
|
+
default: true,
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (!applyNow) {
|
|
288
|
+
savePetConfig({
|
|
289
|
+
salt: result.salt,
|
|
290
|
+
species: desired.species,
|
|
291
|
+
rarity: desired.rarity,
|
|
292
|
+
eye: desired.eye,
|
|
293
|
+
hat: desired.hat,
|
|
294
|
+
appliedAt: new Date().toISOString(),
|
|
295
|
+
});
|
|
296
|
+
console.log(chalk.dim(' Salt saved. Apply later with: claude-code-any-buddy apply\n'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const patchResult = patchBinary(binaryPath, oldSalt, result.salt);
|
|
301
|
+
console.log(chalk.green(` Patched! ${patchResult.replacements} replacements, verified: ${patchResult.verified}`));
|
|
302
|
+
console.log(chalk.dim(` Backup: ${patchResult.backupPath}`));
|
|
303
|
+
|
|
304
|
+
// Save config
|
|
305
|
+
savePetConfig({
|
|
306
|
+
salt: result.salt,
|
|
307
|
+
previousSalt: oldSalt,
|
|
308
|
+
species: desired.species,
|
|
309
|
+
rarity: desired.rarity,
|
|
310
|
+
eye: desired.eye,
|
|
311
|
+
hat: desired.hat,
|
|
312
|
+
appliedTo: binaryPath,
|
|
313
|
+
appliedAt: new Date().toISOString(),
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ─── Hook setup ───
|
|
317
|
+
if (!isHookInstalled() && !flags.noHook) {
|
|
318
|
+
const setupHook = flags.yes || await confirm({
|
|
319
|
+
message: 'Install SessionStart hook to auto-re-apply after updates?',
|
|
320
|
+
default: true,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (setupHook) {
|
|
324
|
+
installHook();
|
|
325
|
+
console.log(chalk.green(' Hook installed in ~/.claude/settings.json'));
|
|
326
|
+
}
|
|
327
|
+
} else if (isHookInstalled()) {
|
|
328
|
+
console.log(chalk.dim(' SessionStart hook already installed.'));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ─── Rename ───
|
|
332
|
+
const currentName = getCompanionName();
|
|
333
|
+
if (currentName) {
|
|
334
|
+
const newName = flags.name ?? await input({
|
|
335
|
+
message: `Rename your companion? (current: "${currentName}", leave blank to keep)`,
|
|
336
|
+
default: '',
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (newName && newName !== currentName) {
|
|
340
|
+
try {
|
|
341
|
+
renameCompanion(newName);
|
|
342
|
+
console.log(chalk.green(` Renamed "${currentName}" → "${newName}"`));
|
|
343
|
+
} catch (err) {
|
|
344
|
+
console.log(chalk.yellow(` Could not rename: ${err.message}`));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
} else if (flags.name) {
|
|
348
|
+
console.log(chalk.dim(' No companion hatched yet — name will be set when you run /buddy'));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (running) {
|
|
352
|
+
console.log(chalk.bold.yellow('\n Done! Quit all Claude Code sessions and relaunch to see your new pet.'));
|
|
353
|
+
console.log(chalk.dim(' Then run /buddy to meet your new companion.\n'));
|
|
354
|
+
} else {
|
|
355
|
+
console.log(chalk.bold.green('\n Done! Launch Claude Code and run /buddy to see your new pet.\n'));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ─── Flag validation ───
|
|
360
|
+
|
|
361
|
+
function validateFlag(name, value, allowed) {
|
|
362
|
+
if (value === undefined) return undefined;
|
|
363
|
+
if (allowed.includes(value)) return value;
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Invalid --${name} "${value}". Must be one of: ${allowed.join(', ')}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── Selection helpers ───
|
|
370
|
+
|
|
371
|
+
async function selectSpecies() {
|
|
372
|
+
return select({
|
|
373
|
+
message: 'Species',
|
|
374
|
+
choices: SPECIES.map(s => {
|
|
375
|
+
const face = renderFace({ species: s, eye: '·' });
|
|
376
|
+
return { name: `${s.padEnd(10)} ${face}`, value: s };
|
|
377
|
+
}),
|
|
378
|
+
pageSize: 18,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function selectEyes(species) {
|
|
383
|
+
return select({
|
|
384
|
+
message: 'Eyes',
|
|
385
|
+
choices: EYES.map(e => {
|
|
386
|
+
const face = renderFace({ species, eye: e });
|
|
387
|
+
return { name: `${e} ${face}`, value: e };
|
|
388
|
+
}),
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function selectRarity() {
|
|
393
|
+
return select({
|
|
394
|
+
message: 'Rarity',
|
|
395
|
+
choices: RARITIES.map(r => {
|
|
396
|
+
const color = RARITY_CHALK[r] || chalk.white;
|
|
397
|
+
const pct = RARITY_WEIGHTS[r];
|
|
398
|
+
return {
|
|
399
|
+
name: color(`${r.padEnd(12)} ${RARITY_STARS[r].padEnd(6)} (normally ${pct}%)`),
|
|
400
|
+
value: r,
|
|
401
|
+
};
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function selectHat(species, eye, rarity) {
|
|
407
|
+
if (rarity === 'common') {
|
|
408
|
+
console.log(chalk.dim(' Common rarity = no hat (this is how Claude Code works)\n'));
|
|
409
|
+
return 'none';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return select({
|
|
413
|
+
message: 'Hat',
|
|
414
|
+
choices: HATS.filter(h => h !== 'none').map(h => {
|
|
415
|
+
const preview = renderSprite({ species, eye, hat: h, rarity }, 0);
|
|
416
|
+
const topLine = preview[0]?.trim() || h;
|
|
417
|
+
return { name: `${h.padEnd(12)} ${topLine}`, value: h };
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
420
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "any-buddy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pick any Claude Code companion pet you want",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"any-buddy": "./bin/cli.mjs",
|
|
8
|
+
"claude-code-any-buddy": "./bin/cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"assets/",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/youruser/claude-code-any-buddy"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"claude",
|
|
25
|
+
"claude-code",
|
|
26
|
+
"buddy",
|
|
27
|
+
"companion",
|
|
28
|
+
"pet",
|
|
29
|
+
"customizer",
|
|
30
|
+
"cli"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@inquirer/prompts": "^7.0.0",
|
|
36
|
+
"chalk": "^5.3.0"
|
|
37
|
+
}
|
|
38
|
+
}
|