any-buddy 1.0.3 → 1.0.5
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 +3 -0
- package/bin/cli.mjs +5 -1
- package/lib/config.mjs +11 -0
- package/lib/finder-worker.mjs +2 -2
- package/lib/finder.mjs +96 -19
- package/lib/tui.mjs +70 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,9 @@ claude-code-any-buddy apply --silent
|
|
|
85
85
|
# Restore original pet
|
|
86
86
|
claude-code-any-buddy restore
|
|
87
87
|
|
|
88
|
+
# Delete companion so Claude Code re-hatches a fresh one on next /buddy
|
|
89
|
+
claude-code-any-buddy rehatch
|
|
90
|
+
|
|
88
91
|
# Non-interactive with flags (skip prompts you already know the answer to)
|
|
89
92
|
claude-code-any-buddy --species dragon --rarity legendary --eye '✦' --hat wizard --name Draco --yes
|
|
90
93
|
```
|
package/bin/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { runInteractive, runPreview, runCurrent, runApply, runRestore } from '../lib/tui.mjs';
|
|
3
|
+
import { runInteractive, runPreview, runCurrent, runApply, runRestore, runRehatch } from '../lib/tui.mjs';
|
|
4
4
|
|
|
5
5
|
function parseArgs(argv) {
|
|
6
6
|
const args = argv.slice(2);
|
|
@@ -43,6 +43,9 @@ try {
|
|
|
43
43
|
case 'restore':
|
|
44
44
|
await runRestore();
|
|
45
45
|
break;
|
|
46
|
+
case 'rehatch':
|
|
47
|
+
await runRehatch();
|
|
48
|
+
break;
|
|
46
49
|
case 'help':
|
|
47
50
|
printHelp();
|
|
48
51
|
break;
|
|
@@ -78,6 +81,7 @@ Usage:
|
|
|
78
81
|
claude-code-any-buddy current Show your current pet
|
|
79
82
|
claude-code-any-buddy apply [--silent] Re-apply saved pet after update
|
|
80
83
|
claude-code-any-buddy restore Restore original pet
|
|
84
|
+
claude-code-any-buddy rehatch Delete companion to re-hatch via /buddy
|
|
81
85
|
|
|
82
86
|
Options:
|
|
83
87
|
-s, --species <name> Species (duck, goose, blob, cat, dragon, octopus, owl,
|
package/lib/config.mjs
CHANGED
|
@@ -104,6 +104,17 @@ export function setCompanionPersonality(personality) {
|
|
|
104
104
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Delete the companion from ~/.claude.json so Claude Code re-hatches on next /buddy
|
|
108
|
+
export function deleteCompanion() {
|
|
109
|
+
const configPath = getClaudeConfigPath();
|
|
110
|
+
if (!existsSync(configPath)) return false;
|
|
111
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
112
|
+
if (!config.companion) return false;
|
|
113
|
+
delete config.companion;
|
|
114
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
// Read or write Claude Code's settings.json for hooks
|
|
108
119
|
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
109
120
|
|
package/lib/finder-worker.mjs
CHANGED
|
@@ -109,7 +109,7 @@ while (true) {
|
|
|
109
109
|
process.exit(0);
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
if (attempts %
|
|
113
|
-
process.stderr.write(
|
|
112
|
+
if (attempts % 25000 === 0) {
|
|
113
|
+
process.stderr.write(JSON.stringify({ attempts, elapsed: Date.now() - start }) + '\n');
|
|
114
114
|
}
|
|
115
115
|
}
|
package/lib/finder.mjs
CHANGED
|
@@ -1,28 +1,105 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import { dirname, join } from 'path';
|
|
4
|
+
import { RARITY_WEIGHTS } from './constants.mjs';
|
|
4
5
|
|
|
5
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const WORKER_PATH = join(__dirname, 'finder-worker.mjs');
|
|
7
8
|
|
|
9
|
+
// Calculate expected attempts based on probability of matching all desired traits.
|
|
10
|
+
export function estimateAttempts(desired) {
|
|
11
|
+
// Species: 1/18
|
|
12
|
+
let p = 1 / 18;
|
|
13
|
+
|
|
14
|
+
// Rarity: weight / 100
|
|
15
|
+
p *= RARITY_WEIGHTS[desired.rarity] / 100;
|
|
16
|
+
|
|
17
|
+
// Eye: 1/6
|
|
18
|
+
p *= 1 / 6;
|
|
19
|
+
|
|
20
|
+
// Hat: common is always 'none' (guaranteed), otherwise 1/8
|
|
21
|
+
if (desired.rarity !== 'common') {
|
|
22
|
+
p *= 1 / 8;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Shiny: 1/100
|
|
26
|
+
if (desired.shiny) {
|
|
27
|
+
p *= 0.01;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Peak stat: 1/5
|
|
31
|
+
if (desired.peak) {
|
|
32
|
+
p *= 1 / 5;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Dump stat: ~1/4 (picked from remaining 4, but rerolls on collision)
|
|
36
|
+
if (desired.dump) {
|
|
37
|
+
p *= 1 / 4;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Expected attempts = 1/p (geometric distribution)
|
|
41
|
+
return Math.round(1 / p);
|
|
42
|
+
}
|
|
43
|
+
|
|
8
44
|
// Spawns a Bun subprocess that brute-forces salts using native Bun.hash.
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
45
|
+
// Calls onProgress with { attempts, elapsed, rate, expected, pct, eta } on each tick.
|
|
46
|
+
// Returns a promise resolving to { salt, attempts, elapsed }.
|
|
47
|
+
export function findSalt(userId, desired, { onProgress } = {}) {
|
|
48
|
+
const expected = estimateAttempts(desired);
|
|
49
|
+
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const child = spawn('bun', [
|
|
52
|
+
WORKER_PATH,
|
|
53
|
+
userId,
|
|
54
|
+
desired.species,
|
|
55
|
+
desired.rarity,
|
|
56
|
+
desired.eye,
|
|
57
|
+
desired.hat,
|
|
58
|
+
String(desired.shiny ?? false),
|
|
59
|
+
desired.peak ?? 'any',
|
|
60
|
+
desired.dump ?? 'any',
|
|
61
|
+
], {
|
|
62
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
63
|
+
timeout: 300000,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let stdout = '';
|
|
26
67
|
|
|
27
|
-
|
|
68
|
+
child.stdout.on('data', (chunk) => {
|
|
69
|
+
stdout += chunk.toString();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
child.stderr.on('data', (chunk) => {
|
|
73
|
+
if (!onProgress) return;
|
|
74
|
+
// Worker writes JSON progress lines to stderr
|
|
75
|
+
const lines = chunk.toString().split('\n').filter(Boolean);
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
try {
|
|
78
|
+
const progress = JSON.parse(line);
|
|
79
|
+
const rate = progress.attempts / (progress.elapsed / 1000); // attempts/sec
|
|
80
|
+
const pct = Math.min(100, (progress.attempts / expected) * 100);
|
|
81
|
+
// ETA based on expected remaining attempts at current rate
|
|
82
|
+
const remaining = Math.max(0, expected - progress.attempts);
|
|
83
|
+
const eta = rate > 0 ? remaining / rate : Infinity;
|
|
84
|
+
onProgress({ ...progress, rate, expected, pct, eta });
|
|
85
|
+
} catch {
|
|
86
|
+
// Not JSON — ignore
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
child.on('close', (code) => {
|
|
92
|
+
if (code === 0 && stdout.trim()) {
|
|
93
|
+
try {
|
|
94
|
+
resolve(JSON.parse(stdout.trim()));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
reject(new Error(`Failed to parse finder result: ${stdout.trim()}`));
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
reject(new Error(`Salt finder exited with code ${code}`));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
child.on('error', reject);
|
|
104
|
+
});
|
|
28
105
|
}
|
package/lib/tui.mjs
CHANGED
|
@@ -3,11 +3,23 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { SPECIES, EYES, HATS, RARITIES, RARITY_STARS, RARITY_WEIGHTS, STAT_NAMES, ORIGINAL_SALT } from './constants.mjs';
|
|
4
4
|
import { roll } from './generation.mjs';
|
|
5
5
|
import { renderSprite, renderFace } from './sprites.mjs';
|
|
6
|
-
import { findSalt } from './finder.mjs';
|
|
6
|
+
import { findSalt, estimateAttempts } from './finder.mjs';
|
|
7
7
|
import { findClaudeBinary, getCurrentSalt, patchBinary, verifySalt, restoreBinary, isClaudeRunning } from './patcher.mjs';
|
|
8
|
-
import { getClaudeUserId, savePetConfig, loadPetConfig, isHookInstalled, installHook, removeHook, getCompanionName, renameCompanion, getCompanionPersonality, setCompanionPersonality } from './config.mjs';
|
|
8
|
+
import { getClaudeUserId, savePetConfig, loadPetConfig, isHookInstalled, installHook, removeHook, getCompanionName, renameCompanion, getCompanionPersonality, setCompanionPersonality, deleteCompanion } from './config.mjs';
|
|
9
9
|
import { DEFAULT_PERSONALITIES } from './personalities.mjs';
|
|
10
10
|
|
|
11
|
+
function progressBar(pct, width) {
|
|
12
|
+
const filled = Math.min(width, Math.round((pct / 100) * width));
|
|
13
|
+
const empty = width - filled;
|
|
14
|
+
return chalk.cyan('█'.repeat(filled)) + chalk.dim('░'.repeat(empty));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function formatCount(n) {
|
|
18
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
19
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(0)}k`;
|
|
20
|
+
return String(n);
|
|
21
|
+
}
|
|
22
|
+
|
|
11
23
|
const RARITY_CHALK = {
|
|
12
24
|
common: chalk.gray,
|
|
13
25
|
uncommon: chalk.green,
|
|
@@ -180,6 +192,37 @@ export async function runRestore() {
|
|
|
180
192
|
console.log();
|
|
181
193
|
}
|
|
182
194
|
|
|
195
|
+
export async function runRehatch() {
|
|
196
|
+
banner();
|
|
197
|
+
|
|
198
|
+
const name = getCompanionName();
|
|
199
|
+
if (!name) {
|
|
200
|
+
console.log(chalk.dim(' No companion found — nothing to delete.\n'));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const personality = getCompanionPersonality();
|
|
205
|
+
console.log(chalk.dim(` Current companion: "${name}"`));
|
|
206
|
+
if (personality) {
|
|
207
|
+
console.log(chalk.dim(` Personality: "${personality}"`));
|
|
208
|
+
}
|
|
209
|
+
console.log();
|
|
210
|
+
|
|
211
|
+
const proceed = await confirm({
|
|
212
|
+
message: `Delete "${name}" so Claude Code generates a fresh companion on next /buddy?`,
|
|
213
|
+
default: false,
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (!proceed) {
|
|
217
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
deleteCompanion();
|
|
222
|
+
console.log(chalk.green(`\n Companion "${name}" deleted.`));
|
|
223
|
+
console.log(chalk.dim(' Run /buddy in Claude Code to hatch a new one.\n'));
|
|
224
|
+
}
|
|
225
|
+
|
|
183
226
|
export async function runInteractive(flags = {}) {
|
|
184
227
|
banner();
|
|
185
228
|
|
|
@@ -241,15 +284,23 @@ export async function runInteractive(flags = {}) {
|
|
|
241
284
|
}
|
|
242
285
|
|
|
243
286
|
// ─── Find salt ───
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
287
|
+
const expected = estimateAttempts(desired);
|
|
288
|
+
console.log(chalk.dim(`\n Searching (~${formatCount(expected)} expected attempts)...`));
|
|
289
|
+
|
|
290
|
+
const result = await findSalt(userId, desired, {
|
|
291
|
+
onProgress: ({ attempts, elapsed, rate, pct, eta }) => {
|
|
292
|
+
const bar = progressBar(pct, 20);
|
|
293
|
+
const etaStr = eta < 1 ? '<1s' : eta < 60 ? `${Math.ceil(eta)}s` : `${(eta / 60).toFixed(1)}m`;
|
|
294
|
+
const rateStr = rate > 1e6 ? `${(rate / 1e6).toFixed(1)}M/s` : `${(rate / 1e3).toFixed(0)}k/s`;
|
|
295
|
+
process.stdout.write(
|
|
296
|
+
`\r ${bar} ${chalk.dim(`${Math.min(99, Math.floor(pct))}%`)} ${chalk.cyan(formatCount(attempts))} tried ${chalk.dim(rateStr)} ${chalk.dim(`ETA ${etaStr}`)} `
|
|
297
|
+
);
|
|
249
298
|
},
|
|
250
299
|
});
|
|
251
300
|
|
|
252
|
-
|
|
301
|
+
// Clear the progress line and show result
|
|
302
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
303
|
+
console.log(chalk.green(` Found in ${result.attempts.toLocaleString()} attempts (${(result.elapsed / 1000).toFixed(1)}s)`));
|
|
253
304
|
const foundBones = roll(userId, result.salt).bones;
|
|
254
305
|
showPet(foundBones, 'Your new pet');
|
|
255
306
|
|
|
@@ -352,8 +403,10 @@ export async function runInteractive(flags = {}) {
|
|
|
352
403
|
// ─── Rename & Personality ───
|
|
353
404
|
const currentName = getCompanionName();
|
|
354
405
|
const currentPersonality = getCompanionPersonality();
|
|
406
|
+
const hasCompanion = !!(currentName && currentPersonality);
|
|
355
407
|
|
|
356
|
-
if (
|
|
408
|
+
if (hasCompanion) {
|
|
409
|
+
// ── Name ──
|
|
357
410
|
const newName = flags.name ?? await input({
|
|
358
411
|
message: `Rename your companion? (current: "${currentName}", leave blank to keep)`,
|
|
359
412
|
default: '',
|
|
@@ -367,14 +420,10 @@ export async function runInteractive(flags = {}) {
|
|
|
367
420
|
console.log(chalk.yellow(` Could not rename: ${err.message}`));
|
|
368
421
|
}
|
|
369
422
|
}
|
|
370
|
-
} else if (flags.name) {
|
|
371
|
-
console.log(chalk.dim(' No companion hatched yet — name will be set when you run /buddy'));
|
|
372
|
-
}
|
|
373
423
|
|
|
374
|
-
|
|
424
|
+
// ── Personality ──
|
|
375
425
|
console.log(chalk.dim(`\n Current personality: "${currentPersonality}"`));
|
|
376
426
|
|
|
377
|
-
// Determine the species — use the desired species from the selection above
|
|
378
427
|
const selectedSpecies = desired.species;
|
|
379
428
|
const speciesDefault = DEFAULT_PERSONALITIES[selectedSpecies] || null;
|
|
380
429
|
|
|
@@ -407,8 +456,13 @@ export async function runInteractive(flags = {}) {
|
|
|
407
456
|
console.log(chalk.yellow(` Could not update personality: ${err.message}`));
|
|
408
457
|
}
|
|
409
458
|
}
|
|
410
|
-
} else
|
|
411
|
-
console.log(chalk.dim(' No companion hatched yet —
|
|
459
|
+
} else {
|
|
460
|
+
console.log(chalk.dim('\n No companion hatched yet — the visual patch has been applied.'));
|
|
461
|
+
console.log(chalk.dim(' Run /buddy in Claude Code to hatch your companion and get a name & personality.'));
|
|
462
|
+
console.log(chalk.dim(' Then run any-buddy again to customize the name and personality.'));
|
|
463
|
+
if (flags.name || flags.personality) {
|
|
464
|
+
console.log(chalk.yellow(' --name and --personality are ignored until after hatching.'));
|
|
465
|
+
}
|
|
412
466
|
}
|
|
413
467
|
|
|
414
468
|
if (running) {
|