any-buddy 1.0.4 → 1.0.6
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 +9 -2
- package/lib/config.mjs +13 -7
- package/lib/finder-worker.mjs +2 -2
- package/lib/finder.mjs +96 -19
- package/lib/tui.mjs +36 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,14 +202,21 @@ When you choose to install the hook, it adds this to `~/.claude/settings.json`:
|
|
|
202
202
|
"hooks": {
|
|
203
203
|
"SessionStart": [
|
|
204
204
|
{
|
|
205
|
-
"
|
|
206
|
-
"
|
|
205
|
+
"matcher": "",
|
|
206
|
+
"hooks": [
|
|
207
|
+
{
|
|
208
|
+
"type": "command",
|
|
209
|
+
"command": "claude-code-any-buddy apply --silent"
|
|
210
|
+
}
|
|
211
|
+
]
|
|
207
212
|
}
|
|
208
213
|
]
|
|
209
214
|
}
|
|
210
215
|
}
|
|
211
216
|
```
|
|
212
217
|
|
|
218
|
+
The hook is **optional and defaults to No** — you'll be asked during the interactive flow. If you prefer, just run `any-buddy apply` manually after Claude Code updates.
|
|
219
|
+
|
|
213
220
|
On every Claude Code session start, this runs `apply --silent` which:
|
|
214
221
|
1. Reads your saved salt from `~/.claude-code-any-buddy.json`
|
|
215
222
|
2. Checks if the current binary already has the correct salt (fast `Buffer.indexOf`)
|
package/lib/config.mjs
CHANGED
|
@@ -135,11 +135,17 @@ export function saveClaudeSettings(settings) {
|
|
|
135
135
|
|
|
136
136
|
const HOOK_COMMAND = 'claude-code-any-buddy apply --silent';
|
|
137
137
|
|
|
138
|
+
// Claude Code hooks schema: { "SessionStart": [{ "matcher": "", "hooks": [{ "type": "command", "command": "..." }] }] }
|
|
139
|
+
function findHookEntry(matchers) {
|
|
140
|
+
if (!Array.isArray(matchers)) return null;
|
|
141
|
+
return matchers.find(m =>
|
|
142
|
+
Array.isArray(m.hooks) && m.hooks.some(h => h.command === HOOK_COMMAND)
|
|
143
|
+
) ?? null;
|
|
144
|
+
}
|
|
145
|
+
|
|
138
146
|
export function isHookInstalled() {
|
|
139
147
|
const settings = getClaudeSettings();
|
|
140
|
-
|
|
141
|
-
if (!Array.isArray(hooks)) return false;
|
|
142
|
-
return hooks.some(h => h.command === HOOK_COMMAND);
|
|
148
|
+
return findHookEntry(settings.hooks?.SessionStart) !== null;
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
export function installHook() {
|
|
@@ -147,10 +153,10 @@ export function installHook() {
|
|
|
147
153
|
if (!settings.hooks) settings.hooks = {};
|
|
148
154
|
if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
|
|
149
155
|
|
|
150
|
-
if (!settings.hooks.SessionStart
|
|
156
|
+
if (!findHookEntry(settings.hooks.SessionStart)) {
|
|
151
157
|
settings.hooks.SessionStart.push({
|
|
152
|
-
|
|
153
|
-
command: HOOK_COMMAND,
|
|
158
|
+
matcher: '',
|
|
159
|
+
hooks: [{ type: 'command', command: HOOK_COMMAND }],
|
|
154
160
|
});
|
|
155
161
|
}
|
|
156
162
|
|
|
@@ -161,7 +167,7 @@ export function removeHook() {
|
|
|
161
167
|
const settings = getClaudeSettings();
|
|
162
168
|
if (!settings.hooks?.SessionStart) return;
|
|
163
169
|
settings.hooks.SessionStart = settings.hooks.SessionStart.filter(
|
|
164
|
-
h => h.command
|
|
170
|
+
m => !Array.isArray(m.hooks) || !m.hooks.some(h => h.command === HOOK_COMMAND)
|
|
165
171
|
);
|
|
166
172
|
if (settings.hooks.SessionStart.length === 0) delete settings.hooks.SessionStart;
|
|
167
173
|
if (Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
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
|
|
|
@@ -367,14 +387,20 @@ export async function runInteractive(flags = {}) {
|
|
|
367
387
|
|
|
368
388
|
// ─── Hook setup ───
|
|
369
389
|
if (!isHookInstalled() && !flags.noHook) {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
390
|
+
console.log(chalk.dim('\n Optional: install a SessionStart hook to auto-re-apply after Claude Code updates.'));
|
|
391
|
+
console.log(chalk.yellow(' Note: this modifies ~/.claude/settings.json. If you have issues, run:'));
|
|
392
|
+
console.log(chalk.yellow(' any-buddy restore'));
|
|
393
|
+
|
|
394
|
+
const setupHook = await confirm({
|
|
395
|
+
message: 'Install auto-patch hook?',
|
|
396
|
+
default: false,
|
|
373
397
|
});
|
|
374
398
|
|
|
375
399
|
if (setupHook) {
|
|
376
400
|
installHook();
|
|
377
401
|
console.log(chalk.green(' Hook installed in ~/.claude/settings.json'));
|
|
402
|
+
} else {
|
|
403
|
+
console.log(chalk.dim(' No hook installed. Run `any-buddy apply` manually after updates.'));
|
|
378
404
|
}
|
|
379
405
|
} else if (isHookInstalled()) {
|
|
380
406
|
console.log(chalk.dim(' SessionStart hook already installed.'));
|