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 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
 
@@ -109,7 +109,7 @@ while (true) {
109
109
  process.exit(0);
110
110
  }
111
111
 
112
- if (attempts % 500000 === 0) {
113
- process.stderr.write(`${(attempts / 1000).toFixed(0)}k seeds tried...\n`);
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 { execFileSync } from 'child_process';
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
- // Returns { salt, attempts, elapsed }.
10
- export function findSalt(userId, desired) {
11
- const result = execFileSync('bun', [
12
- WORKER_PATH,
13
- userId,
14
- desired.species,
15
- desired.rarity,
16
- desired.eye,
17
- desired.hat,
18
- String(desired.shiny ?? false),
19
- desired.peak ?? 'any',
20
- desired.dump ?? 'any',
21
- ], {
22
- encoding: 'utf-8',
23
- timeout: 120000, // 2 minute timeout
24
- stdio: ['pipe', 'pipe', 'inherit'], // stderr passes through for progress
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
- return JSON.parse(result.trim());
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
- console.log(chalk.dim('\n Searching for matching salt...'));
245
-
246
- const result = findSalt(userId, desired, {
247
- onProgress: (attempts, ms) => {
248
- process.stdout.write(chalk.dim(`\r Tried ${(attempts / 1000).toFixed(0)}k seeds (${(ms / 1000).toFixed(1)}s)...`));
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
- console.log(chalk.green(`\r Found salt "${result.salt}" in ${result.attempts.toLocaleString()} attempts (${(result.elapsed / 1000).toFixed(1)}s)`));
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 (currentName) {
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
- if (currentPersonality) {
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 if (flags.personality) {
411
- console.log(chalk.dim(' No companion hatched yet — personality will be set when you run /buddy'));
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "any-buddy",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Pick any Claude Code companion pet you want",
5
5
  "type": "module",
6
6
  "bin": {