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 +12 -5
- package/lib/config.mjs +13 -7
- package/lib/preflight.mjs +125 -0
- package/lib/tui.mjs +28 -29
- package/package.json +1 -1
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 |
|
|
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
|
-
"
|
|
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`)
|
|
@@ -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
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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.'));
|