any-buddy 1.0.4 → 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.
@@ -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
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,
@@ -272,15 +284,23 @@ export async function runInteractive(flags = {}) {
272
284
  }
273
285
 
274
286
  // ─── Find salt ───
275
- console.log(chalk.dim('\n Searching for matching salt...'));
276
-
277
- const result = findSalt(userId, desired, {
278
- onProgress: (attempts, ms) => {
279
- 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
+ );
280
298
  },
281
299
  });
282
300
 
283
- 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)`));
284
304
  const foundBones = roll(userId, result.salt).bones;
285
305
  showPet(foundBones, 'Your new pet');
286
306
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "any-buddy",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Pick any Claude Code companion pet you want",
5
5
  "type": "module",
6
6
  "bin": {