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.
- package/lib/finder-worker.mjs +2 -2
- package/lib/finder.mjs +96 -19
- package/lib/tui.mjs +27 -7
- package/package.json +1 -1
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
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
|