any-buddy 1.0.5 → 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.
@@ -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
- "type": "command",
206
- "command": "claude-code-any-buddy apply --silent"
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`)
@@ -255,7 +262,7 @@ This patches the salt back to the original, removes the SessionStart hook, and c
255
262
 
256
263
  ## Limitations
257
264
 
258
- - **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
259
266
  - **Requires Bun** — needed for matching Claude Code's wyhash implementation
260
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)
261
268
  - **Stats partially selectable** — you can pick which stat is highest (peak) and lowest (dump), but not exact values
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
- const hooks = settings.hooks?.SessionStart;
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.some(h => h.command === HOOK_COMMAND)) {
156
+ if (!findHookEntry(settings.hooks.SessionStart)) {
151
157
  settings.hooks.SessionStart.push({
152
- type: 'command',
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 !== HOOK_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;
@@ -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);
@@ -387,14 +380,20 @@ export async function runInteractive(flags = {}) {
387
380
 
388
381
  // ─── Hook setup ───
389
382
  if (!isHookInstalled() && !flags.noHook) {
390
- const setupHook = flags.yes || await confirm({
391
- message: 'Install SessionStart hook to auto-re-apply after updates?',
392
- default: true,
383
+ console.log(chalk.dim('\n Optional: install a SessionStart hook to auto-re-apply after Claude Code updates.'));
384
+ console.log(chalk.yellow(' Note: this modifies ~/.claude/settings.json. If you have issues, run:'));
385
+ console.log(chalk.yellow(' any-buddy restore'));
386
+
387
+ const setupHook = await confirm({
388
+ message: 'Install auto-patch hook?',
389
+ default: false,
393
390
  });
394
391
 
395
392
  if (setupHook) {
396
393
  installHook();
397
394
  console.log(chalk.green(' Hook installed in ~/.claude/settings.json'));
395
+ } else {
396
+ console.log(chalk.dim(' No hook installed. Run `any-buddy apply` manually after updates.'));
398
397
  }
399
398
  } else if (isHookInstalled()) {
400
399
  console.log(chalk.dim(' SessionStart hook already installed.'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "any-buddy",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Pick any Claude Code companion pet you want",
5
5
  "type": "module",
6
6
  "bin": {