any-buddy 1.0.6 → 1.0.7

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
@@ -2,7 +2,7 @@
2
2
  Pick any Claude Code companion pet you want.
3
3
 
4
4
  ```bash
5
- npx any-buddy
5
+ npx any-buddy@latest
6
6
  ```
7
7
 
8
8
  That's it. Follow the prompts to choose your species, rarity, eyes, hat, and name.
@@ -37,7 +37,7 @@ The patch is safe — it uses `rename()` to atomically swap the binary, which is
37
37
  | Platform | Status | Binary location (auto-detected) |
38
38
  |---|---|---|
39
39
  | Linux | Tested | `~/.local/share/claude/versions/<ver>` |
40
- | macOS | Should work | `~/.local/bin/claude`, `/opt/homebrew/bin/claude`, `~/.claude/local/claude` |
40
+ | macOS | Tested | `~/.local/bin/claude`, `/opt/homebrew/bin/claude`, `~/.claude/local/claude` |
41
41
  | Windows | Should work | `%LOCALAPPDATA%\Programs\claude\claude.exe`, npm global shim |
42
42
 
43
43
  The binary is found automatically via `which`/`where` and platform-specific known paths. If auto-detection fails, set `CLAUDE_BINARY=/path/to/binary` manually.
@@ -262,7 +262,7 @@ This patches the salt back to the original, removes the SessionStart hook, and c
262
262
 
263
263
  ## Limitations
264
264
 
265
- - **Tested on Linux** macOS and Windows should work but are not yet tested. Please [open an issue](https://github.com/cpaczek/any-buddy/issues) if you hit problems
265
+ - **Tested on Linux and macOS** Windows should work but is not yet tested. Please [open an issue](https://github.com/cpaczek/any-buddy/issues) if you hit problems
266
266
  - **Requires Bun** — needed for matching Claude Code's wyhash implementation
267
267
  - **Salt string dependent** — if Anthropic changes the salt from `friend-2026-401` in a future version, the patch logic would need updating (but the tool will detect this and warn you)
268
268
  - **Stats partially selectable** — you can pick which stat is highest (peak) and lowest (dump), but not exact values
@@ -0,0 +1,125 @@
1
+ import { existsSync, readFileSync, statSync } from 'fs';
2
+ import { execSync } from 'child_process';
3
+ import { platform } from 'os';
4
+ import chalk from 'chalk';
5
+ import { ORIGINAL_SALT } from './constants.mjs';
6
+ import { findClaudeBinary, verifySalt } from './patcher.mjs';
7
+ import { getClaudeUserId } from './config.mjs';
8
+
9
+ const ISSUE_URL = 'https://github.com/cpaczek/any-buddy/issues';
10
+
11
+ // Run all preflight checks before doing anything destructive.
12
+ // Returns { ok, binaryPath, userId, saltCount } or throws with a helpful message.
13
+ export function runPreflight({ requireBinary = true } = {}) {
14
+ const errors = [];
15
+ const warnings = [];
16
+
17
+ // ── 1. Check bun is installed ──
18
+ let bunVersion = null;
19
+ try {
20
+ bunVersion = execSync('bun --version', { encoding: 'utf-8', timeout: 5000 }).trim();
21
+ } catch {
22
+ errors.push(
23
+ 'Bun is not installed or not on PATH.\n' +
24
+ ' any-buddy needs Bun to compute the correct hash (Claude Code uses Bun.hash/wyhash).\n' +
25
+ ' Install Bun: https://bun.sh'
26
+ );
27
+ }
28
+
29
+ // ── 2. Check Claude config exists and has a userId ──
30
+ const userId = getClaudeUserId();
31
+ if (userId === 'anon') {
32
+ warnings.push(
33
+ 'No user ID found in ~/.claude.json (using "anon").\n' +
34
+ ' This usually means Claude Code hasn\'t been set up yet.\n' +
35
+ ' The generated pet may not match what Claude Code shows.'
36
+ );
37
+ }
38
+
39
+ // ── 3. Find and validate the binary ──
40
+ let binaryPath = null;
41
+ let saltCount = 0;
42
+
43
+ if (requireBinary) {
44
+ try {
45
+ binaryPath = findClaudeBinary();
46
+ } catch (err) {
47
+ errors.push(err.message);
48
+ }
49
+
50
+ if (binaryPath) {
51
+ // Check binary size — should be substantial (>1MB), not a shell script or shim
52
+ try {
53
+ const size = statSync(binaryPath).size;
54
+ if (size < 1_000_000) {
55
+ warnings.push(
56
+ `Binary at ${binaryPath} is only ${(size / 1024).toFixed(0)}KB.\n` +
57
+ ' This might be a shell script, symlink wrapper, or npm shim rather than the actual binary.\n' +
58
+ ' If patching fails, try setting CLAUDE_BINARY to the real compiled binary.'
59
+ );
60
+ }
61
+ } catch { /* ignore */ }
62
+
63
+ // Check that the salt exists in the binary
64
+ try {
65
+ const result = verifySalt(binaryPath, ORIGINAL_SALT);
66
+ saltCount = result.found;
67
+
68
+ if (saltCount === 0) {
69
+ // Check if it might already be patched with a different salt
70
+ errors.push(
71
+ `Salt "${ORIGINAL_SALT}" not found in ${binaryPath}.\n` +
72
+ ' Possible reasons:\n' +
73
+ ' - Binary is already patched with a custom salt (run `any-buddy restore` first)\n' +
74
+ ' - This binary format doesn\'t contain the salt as a plain string\n' +
75
+ ' - Claude Code changed the salt in a new version\n' +
76
+ `\n Platform: ${platform()}, binary: ${binaryPath}` +
77
+ `\n Please report this at: ${ISSUE_URL}`
78
+ );
79
+ } else if (saltCount < 2) {
80
+ warnings.push(
81
+ `Salt found only ${saltCount} time(s) in binary (expected 3 on Linux).\n` +
82
+ ' This might work but the patch may be incomplete.\n' +
83
+ ` Platform: ${platform()}`
84
+ );
85
+ }
86
+ } catch (err) {
87
+ errors.push(
88
+ `Could not read binary at ${binaryPath}: ${err.message}\n` +
89
+ ' Check file permissions.'
90
+ );
91
+ }
92
+
93
+ // Platform-specific warnings
94
+ const plat = platform();
95
+ if (plat === 'win32') {
96
+ warnings.push(
97
+ 'Windows support is experimental.\n' +
98
+ ' You must close all Claude Code windows before patching.\n' +
99
+ ' If you encounter issues, please report them at: ' + ISSUE_URL
100
+ );
101
+ } else if (plat === 'darwin') {
102
+ warnings.push(
103
+ 'macOS support is experimental.\n' +
104
+ ' If the binary is code-signed, patching may invalidate the signature.\n' +
105
+ ' If Claude Code won\'t launch after patching, run `any-buddy restore`.\n' +
106
+ ' Please report issues at: ' + ISSUE_URL
107
+ );
108
+ }
109
+ }
110
+ }
111
+
112
+ // ── Print results ──
113
+ for (const w of warnings) {
114
+ console.log(chalk.yellow(` Warning: ${w}\n`));
115
+ }
116
+
117
+ if (errors.length > 0) {
118
+ for (const e of errors) {
119
+ console.log(chalk.red(` Error: ${e}\n`));
120
+ }
121
+ return { ok: false, binaryPath, userId, saltCount, bunVersion };
122
+ }
123
+
124
+ return { ok: true, binaryPath, userId, saltCount, bunVersion };
125
+ }
package/lib/tui.mjs CHANGED
@@ -5,6 +5,7 @@ import { roll } from './generation.mjs';
5
5
  import { renderSprite, renderFace } from './sprites.mjs';
6
6
  import { findSalt, estimateAttempts } from './finder.mjs';
7
7
  import { findClaudeBinary, getCurrentSalt, patchBinary, verifySalt, restoreBinary, isClaudeRunning } from './patcher.mjs';
8
+ import { runPreflight } from './preflight.mjs';
8
9
  import { getClaudeUserId, savePetConfig, loadPetConfig, isHookInstalled, installHook, removeHook, getCompanionName, renameCompanion, getCompanionPersonality, setCompanionPersonality, deleteCompanion } from './config.mjs';
9
10
  import { DEFAULT_PERSONALITIES } from './personalities.mjs';
10
11
 
@@ -69,7 +70,9 @@ function showPet(bones, label = 'Your pet') {
69
70
 
70
71
  export async function runCurrent() {
71
72
  banner();
72
- const userId = getClaudeUserId();
73
+ const preflight = runPreflight({ requireBinary: false });
74
+ if (!preflight.ok) process.exit(1);
75
+ const userId = preflight.userId;
73
76
  console.log(chalk.dim(` User ID: ${userId.slice(0, 12)}...`));
74
77
 
75
78
  // Show what the original salt produces
@@ -86,6 +89,8 @@ export async function runCurrent() {
86
89
 
87
90
  export async function runPreview(flags = {}) {
88
91
  banner();
92
+ const preflight = runPreflight({ requireBinary: false });
93
+ if (!preflight.ok) process.exit(1);
89
94
 
90
95
  const species = validateFlag('species', flags.species, SPECIES) ?? await selectSpecies();
91
96
  const eye = validateFlag('eye', flags.eye, EYES) ?? await selectEyes(species);
@@ -226,13 +231,18 @@ export async function runRehatch() {
226
231
  export async function runInteractive(flags = {}) {
227
232
  banner();
228
233
 
229
- const userId = getClaudeUserId();
230
- if (userId === 'anon') {
231
- console.log(chalk.yellow(' Warning: No Claude Code user ID found. Using "anon".'));
232
- console.log(chalk.yellow(' Make sure Claude Code is installed and you\'ve logged in.\n'));
233
- } else {
234
- console.log(chalk.dim(` User ID: ${userId.slice(0, 12)}...\n`));
234
+ // ─── Preflight checks ───
235
+ const preflight = runPreflight({ requireBinary: true });
236
+ if (!preflight.ok) {
237
+ process.exit(1);
238
+ }
239
+ const userId = preflight.userId;
240
+ console.log(chalk.dim(` User ID: ${userId.slice(0, 12)}...`));
241
+ console.log(chalk.dim(` Binary: ${preflight.binaryPath} (salt found ${preflight.saltCount}x)`));
242
+ if (preflight.bunVersion) {
243
+ console.log(chalk.dim(` Bun: v${preflight.bunVersion}`));
235
244
  }
245
+ console.log();
236
246
 
237
247
  // Show current pet
238
248
  const currentBones = roll(userId, ORIGINAL_SALT).bones;
@@ -304,25 +314,8 @@ export async function runInteractive(flags = {}) {
304
314
  const foundBones = roll(userId, result.salt).bones;
305
315
  showPet(foundBones, 'Your new pet');
306
316
 
307
- // ─── Patch binary ───
308
- let binaryPath;
309
- try {
310
- binaryPath = findClaudeBinary();
311
- } catch (err) {
312
- console.error(chalk.red(`\n ${err.message}`));
313
- console.log(chalk.dim(` Salt saved. You can manually apply later with: claude-code-any-buddy apply\n`));
314
- savePetConfig({
315
- salt: result.salt,
316
- species: desired.species,
317
- rarity: desired.rarity,
318
- eye: desired.eye,
319
- hat: desired.hat,
320
- appliedAt: new Date().toISOString(),
321
- });
322
- return;
323
- }
324
-
325
- console.log(chalk.dim(` Binary: ${binaryPath}`));
317
+ // ─── Patch binary (path already validated by preflight) ───
318
+ const binaryPath = preflight.binaryPath;
326
319
 
327
320
  // Find what's currently in the binary
328
321
  const current = getCurrentSalt(binaryPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "any-buddy",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Pick any Claude Code companion pet you want",
5
5
  "type": "module",
6
6
  "bin": {