any-buddy 1.0.2 → 1.0.4
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 +8 -1
- package/bin/cli.mjs +11 -1
- package/lib/config.mjs +37 -0
- package/lib/finder-worker.mjs +22 -7
- package/lib/finder.mjs +2 -0
- package/lib/personalities.mjs +24 -0
- package/lib/tui.mjs +109 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,6 +85,9 @@ claude-code-any-buddy apply --silent
|
|
|
85
85
|
# Restore original pet
|
|
86
86
|
claude-code-any-buddy restore
|
|
87
87
|
|
|
88
|
+
# Delete companion so Claude Code re-hatches a fresh one on next /buddy
|
|
89
|
+
claude-code-any-buddy rehatch
|
|
90
|
+
|
|
88
91
|
# Non-interactive with flags (skip prompts you already know the answer to)
|
|
89
92
|
claude-code-any-buddy --species dragon --rarity legendary --eye '✦' --hat wizard --name Draco --yes
|
|
90
93
|
```
|
|
@@ -98,8 +101,11 @@ claude-code-any-buddy --species dragon --rarity legendary --eye '✦' --hat wiza
|
|
|
98
101
|
| `--eye <char>` | `-e` | Pre-select eye style |
|
|
99
102
|
| `--hat <name>` | `-t` | Pre-select hat |
|
|
100
103
|
| `--name <name>` | `-n` | Rename your companion |
|
|
104
|
+
| `--personality <desc>` | `-p` | Set companion personality (controls speech bubble tone) |
|
|
101
105
|
| `--yes` | `-y` | Skip all confirmation prompts |
|
|
102
106
|
| `--shiny` | | Require shiny variant (~100x longer search) |
|
|
107
|
+
| `--peak <stat>` | | Best stat: DEBUGGING, PATIENCE, CHAOS, WISDOM, or SNARK |
|
|
108
|
+
| `--dump <stat>` | | Worst stat (~20x longer search with both) |
|
|
103
109
|
| `--no-hook` | | Don't offer to install the auto-patch hook |
|
|
104
110
|
| `--silent` | | Suppress output (for `apply` in hooks) |
|
|
105
111
|
|
|
@@ -252,8 +258,9 @@ This patches the salt back to the original, removes the SessionStart hook, and c
|
|
|
252
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
|
|
253
259
|
- **Requires Bun** — needed for matching Claude Code's wyhash implementation
|
|
254
260
|
- **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)
|
|
255
|
-
- **Stats
|
|
261
|
+
- **Stats partially selectable** — you can pick which stat is highest (peak) and lowest (dump), but not exact values
|
|
256
262
|
- **Personality** — generated by Claude on first `/buddy` run after patching, not controlled by this tool. Delete the `companion` key from `~/.claude.json` to re-hatch with a new personality
|
|
263
|
+
- **Speech bubble** — your buddy's speech bubble reactions are generated by Claude based on the personality and name stored in `~/.claude.json`. After patching the visual, the buddy will still *talk* like the original personality unless you update it. Use the interactive prompt or `--personality "your description here"` to change what your buddy says
|
|
257
264
|
- **Name** — can be changed at any time via the interactive flow or `--name` flag (edits `~/.claude.json` directly)
|
|
258
265
|
|
|
259
266
|
## License
|
package/bin/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { runInteractive, runPreview, runCurrent, runApply, runRestore } from '../lib/tui.mjs';
|
|
3
|
+
import { runInteractive, runPreview, runCurrent, runApply, runRestore, runRehatch } from '../lib/tui.mjs';
|
|
4
4
|
|
|
5
5
|
function parseArgs(argv) {
|
|
6
6
|
const args = argv.slice(2);
|
|
@@ -14,7 +14,10 @@ function parseArgs(argv) {
|
|
|
14
14
|
else if (arg === '--eye' || arg === '-e') { flags.eye = args[++i]; }
|
|
15
15
|
else if (arg === '--hat' || arg === '-t') { flags.hat = args[++i]; }
|
|
16
16
|
else if (arg === '--name' || arg === '-n') { flags.name = args[++i]; }
|
|
17
|
+
else if (arg === '--personality' || arg === '-p') { flags.personality = args[++i]; }
|
|
17
18
|
else if (arg === '--shiny') { flags.shiny = true; }
|
|
19
|
+
else if (arg === '--peak') { flags.peak = args[++i]; }
|
|
20
|
+
else if (arg === '--dump') { flags.dump = args[++i]; }
|
|
18
21
|
else if (arg === '--silent') { flags.silent = true; }
|
|
19
22
|
else if (arg === '--no-hook') { flags.noHook = true; }
|
|
20
23
|
else if (arg === '--yes' || arg === '-y') { flags.yes = true; }
|
|
@@ -40,6 +43,9 @@ try {
|
|
|
40
43
|
case 'restore':
|
|
41
44
|
await runRestore();
|
|
42
45
|
break;
|
|
46
|
+
case 'rehatch':
|
|
47
|
+
await runRehatch();
|
|
48
|
+
break;
|
|
43
49
|
case 'help':
|
|
44
50
|
printHelp();
|
|
45
51
|
break;
|
|
@@ -75,6 +81,7 @@ Usage:
|
|
|
75
81
|
claude-code-any-buddy current Show your current pet
|
|
76
82
|
claude-code-any-buddy apply [--silent] Re-apply saved pet after update
|
|
77
83
|
claude-code-any-buddy restore Restore original pet
|
|
84
|
+
claude-code-any-buddy rehatch Delete companion to re-hatch via /buddy
|
|
78
85
|
|
|
79
86
|
Options:
|
|
80
87
|
-s, --species <name> Species (duck, goose, blob, cat, dragon, octopus, owl,
|
|
@@ -85,7 +92,10 @@ Options:
|
|
|
85
92
|
-t, --hat <name> Hat (none, crown, tophat, propeller, halo, wizard,
|
|
86
93
|
beanie, tinyduck)
|
|
87
94
|
-n, --name <name> Rename your companion
|
|
95
|
+
-p, --personality <desc> Set companion personality
|
|
88
96
|
--shiny Require shiny (~100x longer search)
|
|
97
|
+
--peak <stat> Best stat (DEBUGGING, PATIENCE, CHAOS, WISDOM, SNARK)
|
|
98
|
+
--dump <stat> Worst stat (~20x longer search with both)
|
|
89
99
|
-y, --yes Skip confirmation prompts
|
|
90
100
|
--no-hook Don't offer to install the SessionStart hook
|
|
91
101
|
--silent Suppress output (for apply command in hooks)
|
package/lib/config.mjs
CHANGED
|
@@ -78,6 +78,43 @@ export function renameCompanion(newName) {
|
|
|
78
78
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Read the companion's personality from ~/.claude.json
|
|
82
|
+
export function getCompanionPersonality() {
|
|
83
|
+
const configPath = getClaudeConfigPath();
|
|
84
|
+
if (!existsSync(configPath)) return null;
|
|
85
|
+
try {
|
|
86
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
87
|
+
return config.companion?.personality ?? null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Update the companion's personality in ~/.claude.json
|
|
94
|
+
export function setCompanionPersonality(personality) {
|
|
95
|
+
const configPath = getClaudeConfigPath();
|
|
96
|
+
if (!existsSync(configPath)) {
|
|
97
|
+
throw new Error(`Claude config not found at ${configPath}`);
|
|
98
|
+
}
|
|
99
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
100
|
+
if (!config.companion) {
|
|
101
|
+
throw new Error('No companion found in config. Run /buddy in Claude Code first to hatch one.');
|
|
102
|
+
}
|
|
103
|
+
config.companion.personality = personality;
|
|
104
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Delete the companion from ~/.claude.json so Claude Code re-hatches on next /buddy
|
|
108
|
+
export function deleteCompanion() {
|
|
109
|
+
const configPath = getClaudeConfigPath();
|
|
110
|
+
if (!existsSync(configPath)) return false;
|
|
111
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
112
|
+
if (!config.companion) return false;
|
|
113
|
+
delete config.companion;
|
|
114
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
81
118
|
// Read or write Claude Code's settings.json for hooks
|
|
82
119
|
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
83
120
|
|
package/lib/finder-worker.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// This script runs under Bun for fast native Bun.hash access.
|
|
3
3
|
// Called by finder.mjs as a subprocess.
|
|
4
|
-
// Args: <userId> <species> <rarity> <eye> <hat>
|
|
4
|
+
// Args: <userId> <species> <rarity> <eye> <hat> <shiny> <peak> <dump>
|
|
5
5
|
// Outputs JSON: { salt, attempts, elapsed }
|
|
6
6
|
|
|
7
7
|
const RARITIES = ['common', 'uncommon', 'rare', 'epic', 'legendary'];
|
|
@@ -13,6 +13,7 @@ const SPECIES = [
|
|
|
13
13
|
];
|
|
14
14
|
const EYES = ['·', '✦', '×', '◉', '@', '°'];
|
|
15
15
|
const HATS = ['none', 'crown', 'tophat', 'propeller', 'halo', 'wizard', 'beanie', 'tinyduck'];
|
|
16
|
+
const STAT_NAMES = ['DEBUGGING', 'PATIENCE', 'CHAOS', 'WISDOM', 'SNARK'];
|
|
16
17
|
|
|
17
18
|
function mulberry32(seed) {
|
|
18
19
|
let a = seed >>> 0;
|
|
@@ -39,7 +40,7 @@ function rollRarity(rng) {
|
|
|
39
40
|
return 'common';
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function quickRoll(userId, salt) {
|
|
43
|
+
function quickRoll(userId, salt, needStats) {
|
|
43
44
|
const key = userId + salt;
|
|
44
45
|
const seed = Number(BigInt(Bun.hash(key)) & 0xffffffffn);
|
|
45
46
|
const rng = mulberry32(seed);
|
|
@@ -48,7 +49,16 @@ function quickRoll(userId, salt) {
|
|
|
48
49
|
const eye = pick(rng, EYES);
|
|
49
50
|
const hat = rarity === 'common' ? 'none' : pick(rng, HATS);
|
|
50
51
|
const shiny = rng() < 0.01;
|
|
51
|
-
|
|
52
|
+
|
|
53
|
+
let peak = null, dump = null;
|
|
54
|
+
if (needStats) {
|
|
55
|
+
// These mirror the rollStats() calls exactly — same RNG consumption order
|
|
56
|
+
peak = pick(rng, STAT_NAMES);
|
|
57
|
+
dump = pick(rng, STAT_NAMES);
|
|
58
|
+
while (dump === peak) dump = pick(rng, STAT_NAMES);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { rarity, species, eye, hat, shiny, peak, dump };
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
const SALT_LEN = 15;
|
|
@@ -62,14 +72,17 @@ function randomSalt() {
|
|
|
62
72
|
return s;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
|
-
const [userId, wantSpecies, wantRarity, wantEye, wantHat, wantShiny] = process.argv.slice(2);
|
|
75
|
+
const [userId, wantSpecies, wantRarity, wantEye, wantHat, wantShiny, wantPeak, wantDump] = process.argv.slice(2);
|
|
66
76
|
|
|
67
77
|
if (!userId || !wantSpecies || !wantRarity || !wantEye || !wantHat) {
|
|
68
|
-
console.error('Usage: finder-worker.mjs <userId> <species> <rarity> <eye> <hat> [shiny]');
|
|
78
|
+
console.error('Usage: finder-worker.mjs <userId> <species> <rarity> <eye> <hat> [shiny] [peak] [dump]');
|
|
69
79
|
process.exit(1);
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
const requireShiny = wantShiny === 'true';
|
|
83
|
+
const requirePeak = wantPeak && wantPeak !== 'any' ? wantPeak : null;
|
|
84
|
+
const requireDump = wantDump && wantDump !== 'any' ? wantDump : null;
|
|
85
|
+
const needStats = !!(requirePeak || requireDump);
|
|
73
86
|
|
|
74
87
|
const start = Date.now();
|
|
75
88
|
let attempts = 0;
|
|
@@ -77,14 +90,16 @@ let attempts = 0;
|
|
|
77
90
|
while (true) {
|
|
78
91
|
attempts++;
|
|
79
92
|
const salt = randomSalt();
|
|
80
|
-
const bones = quickRoll(userId, salt);
|
|
93
|
+
const bones = quickRoll(userId, salt, needStats);
|
|
81
94
|
|
|
82
95
|
if (
|
|
83
96
|
bones.species === wantSpecies &&
|
|
84
97
|
bones.rarity === wantRarity &&
|
|
85
98
|
bones.eye === wantEye &&
|
|
86
99
|
bones.hat === wantHat &&
|
|
87
|
-
(!requireShiny || bones.shiny)
|
|
100
|
+
(!requireShiny || bones.shiny) &&
|
|
101
|
+
(!requirePeak || bones.peak === requirePeak) &&
|
|
102
|
+
(!requireDump || bones.dump === requireDump)
|
|
88
103
|
) {
|
|
89
104
|
console.log(JSON.stringify({
|
|
90
105
|
salt,
|
package/lib/finder.mjs
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Default personality suggestions for each species.
|
|
2
|
+
// Claude Code generates these via LLM on first hatch — these are hand-written
|
|
3
|
+
// defaults the user can pick from or use as inspiration.
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PERSONALITIES = {
|
|
6
|
+
duck: "A cheerful quacker who celebrates your wins with enthusiastic honks and judges your variable names with quiet side-eye.",
|
|
7
|
+
goose: "An agent of chaos who thrives on your merge conflicts and honks menacingly whenever you write a TODO comment.",
|
|
8
|
+
blob: "A formless, chill companion who absorbs your stress and responds to everything with gentle, unhurried wisdom.",
|
|
9
|
+
cat: "An aloof code reviewer who pretends not to care about your bugs but quietly bats at syntax errors when you're not looking.",
|
|
10
|
+
dragon: "A fierce guardian of clean code who breathes fire at spaghetti logic and hoards well-written functions.",
|
|
11
|
+
octopus: "A multitasking genius who juggles eight concerns at once and offers tentacle-loads of unsolicited architectural advice.",
|
|
12
|
+
owl: "A nocturnal sage who comes alive during late-night debugging sessions and asks annoyingly insightful questions.",
|
|
13
|
+
penguin: "A tuxedo-wearing professional who waddles through your codebase with dignified concern and dry wit.",
|
|
14
|
+
turtle: "A patient mentor who reminds you that slow, steady refactoring beats heroic rewrites every time.",
|
|
15
|
+
snail: "A zen minimalist who moves at their own pace and leaves a trail of thoughtful, unhurried observations.",
|
|
16
|
+
ghost: "A spectral presence who haunts your dead code and whispers about the bugs you thought you fixed.",
|
|
17
|
+
axolotl: "A regenerative optimist who believes every broken build can be healed and every test can be unflaked.",
|
|
18
|
+
capybara: "The most relaxed companion possible — nothing fazes them, not even production outages at 3am.",
|
|
19
|
+
cactus: "A prickly but lovable desert dweller who thrives on neglect and offers sharp, pointed feedback.",
|
|
20
|
+
robot: "A logical companion who speaks in precise technical observations and occasionally glitches endearingly.",
|
|
21
|
+
rabbit: "A fast-moving, hyperactive buddy who speed-reads your diffs and bounces between topics at alarming pace.",
|
|
22
|
+
mushroom: "A wry fungal sage who speaks in meandering tangents about your bugs while secretly enjoying the chaos.",
|
|
23
|
+
chonk: "An absolute unit of a companion who sits on your terminal with maximum gravitational presence and minimal urgency.",
|
|
24
|
+
};
|
package/lib/tui.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { select, confirm, input } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { SPECIES, EYES, HATS, RARITIES, RARITY_STARS, RARITY_WEIGHTS, ORIGINAL_SALT } from './constants.mjs';
|
|
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
6
|
import { findSalt } from './finder.mjs';
|
|
7
7
|
import { findClaudeBinary, getCurrentSalt, patchBinary, verifySalt, restoreBinary, isClaudeRunning } from './patcher.mjs';
|
|
8
|
-
import { getClaudeUserId, savePetConfig, loadPetConfig, isHookInstalled, installHook, removeHook, getCompanionName, renameCompanion } from './config.mjs';
|
|
8
|
+
import { getClaudeUserId, savePetConfig, loadPetConfig, isHookInstalled, installHook, removeHook, getCompanionName, renameCompanion, getCompanionPersonality, setCompanionPersonality, deleteCompanion } from './config.mjs';
|
|
9
|
+
import { DEFAULT_PERSONALITIES } from './personalities.mjs';
|
|
9
10
|
|
|
10
11
|
const RARITY_CHALK = {
|
|
11
12
|
common: chalk.gray,
|
|
@@ -37,7 +38,13 @@ function banner() {
|
|
|
37
38
|
function showPet(bones, label = 'Your pet') {
|
|
38
39
|
const rarityColor = RARITY_CHALK[bones.rarity] || chalk.white;
|
|
39
40
|
console.log(rarityColor(`\n ${label}: ${bones.species} ${RARITY_STARS[bones.rarity]}`));
|
|
40
|
-
|
|
41
|
+
let info = ` Rarity: ${bones.rarity} Eyes: ${bones.eye} Hat: ${bones.hat} Shiny: ${bones.shiny ? 'YES' : 'no'}`;
|
|
42
|
+
if (bones.stats && Object.keys(bones.stats).length) {
|
|
43
|
+
const sorted = Object.entries(bones.stats).sort((a, b) => b[1] - a[1]);
|
|
44
|
+
const best = sorted[0], worst = sorted[sorted.length - 1];
|
|
45
|
+
info += `\n Best: ${best[0]} ${best[1]} Worst: ${worst[0]} ${worst[1]}`;
|
|
46
|
+
}
|
|
47
|
+
console.log(rarityColor(info));
|
|
41
48
|
const lines = renderSprite(bones, 0);
|
|
42
49
|
console.log();
|
|
43
50
|
for (const line of lines) {
|
|
@@ -173,6 +180,37 @@ export async function runRestore() {
|
|
|
173
180
|
console.log();
|
|
174
181
|
}
|
|
175
182
|
|
|
183
|
+
export async function runRehatch() {
|
|
184
|
+
banner();
|
|
185
|
+
|
|
186
|
+
const name = getCompanionName();
|
|
187
|
+
if (!name) {
|
|
188
|
+
console.log(chalk.dim(' No companion found — nothing to delete.\n'));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const personality = getCompanionPersonality();
|
|
193
|
+
console.log(chalk.dim(` Current companion: "${name}"`));
|
|
194
|
+
if (personality) {
|
|
195
|
+
console.log(chalk.dim(` Personality: "${personality}"`));
|
|
196
|
+
}
|
|
197
|
+
console.log();
|
|
198
|
+
|
|
199
|
+
const proceed = await confirm({
|
|
200
|
+
message: `Delete "${name}" so Claude Code generates a fresh companion on next /buddy?`,
|
|
201
|
+
default: false,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (!proceed) {
|
|
205
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
deleteCompanion();
|
|
210
|
+
console.log(chalk.green(`\n Companion "${name}" deleted.`));
|
|
211
|
+
console.log(chalk.dim(' Run /buddy in Claude Code to hatch a new one.\n'));
|
|
212
|
+
}
|
|
213
|
+
|
|
176
214
|
export async function runInteractive(flags = {}) {
|
|
177
215
|
banner();
|
|
178
216
|
|
|
@@ -208,9 +246,18 @@ export async function runInteractive(flags = {}) {
|
|
|
208
246
|
message: 'Shiny? (1% normally — search takes ~100x longer)',
|
|
209
247
|
default: false,
|
|
210
248
|
});
|
|
249
|
+
const wantStats = flags.peak || flags.dump || await confirm({
|
|
250
|
+
message: 'Customize stats? (best/worst stat — search takes ~20x longer)',
|
|
251
|
+
default: false,
|
|
252
|
+
});
|
|
253
|
+
let peak = null, dump = null;
|
|
254
|
+
if (wantStats) {
|
|
255
|
+
peak = validateFlag('peak', flags.peak, STAT_NAMES) ?? await selectStat('Best stat');
|
|
256
|
+
dump = validateFlag('dump', flags.dump, STAT_NAMES) ?? await selectStat('Worst stat', peak);
|
|
257
|
+
}
|
|
211
258
|
|
|
212
259
|
// Final preview
|
|
213
|
-
const desired = { species, eye, hat, rarity, shiny };
|
|
260
|
+
const desired = { species, eye, hat, rarity, shiny, peak, dump };
|
|
214
261
|
const previewBones = { ...desired, stats: {} };
|
|
215
262
|
showPet(previewBones, 'Your selection');
|
|
216
263
|
|
|
@@ -333,9 +380,13 @@ export async function runInteractive(flags = {}) {
|
|
|
333
380
|
console.log(chalk.dim(' SessionStart hook already installed.'));
|
|
334
381
|
}
|
|
335
382
|
|
|
336
|
-
// ─── Rename ───
|
|
383
|
+
// ─── Rename & Personality ───
|
|
337
384
|
const currentName = getCompanionName();
|
|
338
|
-
|
|
385
|
+
const currentPersonality = getCompanionPersonality();
|
|
386
|
+
const hasCompanion = !!(currentName && currentPersonality);
|
|
387
|
+
|
|
388
|
+
if (hasCompanion) {
|
|
389
|
+
// ── Name ──
|
|
339
390
|
const newName = flags.name ?? await input({
|
|
340
391
|
message: `Rename your companion? (current: "${currentName}", leave blank to keep)`,
|
|
341
392
|
default: '',
|
|
@@ -349,8 +400,49 @@ export async function runInteractive(flags = {}) {
|
|
|
349
400
|
console.log(chalk.yellow(` Could not rename: ${err.message}`));
|
|
350
401
|
}
|
|
351
402
|
}
|
|
352
|
-
|
|
353
|
-
|
|
403
|
+
|
|
404
|
+
// ── Personality ──
|
|
405
|
+
console.log(chalk.dim(`\n Current personality: "${currentPersonality}"`));
|
|
406
|
+
|
|
407
|
+
const selectedSpecies = desired.species;
|
|
408
|
+
const speciesDefault = DEFAULT_PERSONALITIES[selectedSpecies] || null;
|
|
409
|
+
|
|
410
|
+
let newPersonality = flags.personality;
|
|
411
|
+
if (!newPersonality) {
|
|
412
|
+
const choices = [
|
|
413
|
+
{ name: 'Keep current', value: 'keep' },
|
|
414
|
+
];
|
|
415
|
+
if (speciesDefault) {
|
|
416
|
+
choices.push({ name: `Use ${selectedSpecies} default: "${speciesDefault.slice(0, 60)}..."`, value: 'default' });
|
|
417
|
+
}
|
|
418
|
+
choices.push({ name: 'Write custom', value: 'custom' });
|
|
419
|
+
|
|
420
|
+
const choice = await select({ message: 'Personality', choices });
|
|
421
|
+
|
|
422
|
+
if (choice === 'default') {
|
|
423
|
+
newPersonality = speciesDefault;
|
|
424
|
+
} else if (choice === 'custom') {
|
|
425
|
+
newPersonality = await input({
|
|
426
|
+
message: 'Describe your companion\'s personality',
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (newPersonality && newPersonality !== currentPersonality) {
|
|
432
|
+
try {
|
|
433
|
+
setCompanionPersonality(newPersonality);
|
|
434
|
+
console.log(chalk.green(' Personality updated.'));
|
|
435
|
+
} catch (err) {
|
|
436
|
+
console.log(chalk.yellow(` Could not update personality: ${err.message}`));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} else {
|
|
440
|
+
console.log(chalk.dim('\n No companion hatched yet — the visual patch has been applied.'));
|
|
441
|
+
console.log(chalk.dim(' Run /buddy in Claude Code to hatch your companion and get a name & personality.'));
|
|
442
|
+
console.log(chalk.dim(' Then run any-buddy again to customize the name and personality.'));
|
|
443
|
+
if (flags.name || flags.personality) {
|
|
444
|
+
console.log(chalk.yellow(' --name and --personality are ignored until after hatching.'));
|
|
445
|
+
}
|
|
354
446
|
}
|
|
355
447
|
|
|
356
448
|
if (running) {
|
|
@@ -365,6 +457,7 @@ export async function runInteractive(flags = {}) {
|
|
|
365
457
|
|
|
366
458
|
function validateFlag(name, value, allowed) {
|
|
367
459
|
if (value === undefined) return undefined;
|
|
460
|
+
if (value === 'any') return undefined; // treat 'any' as unset
|
|
368
461
|
if (allowed.includes(value)) return value;
|
|
369
462
|
throw new Error(
|
|
370
463
|
`Invalid --${name} "${value}". Must be one of: ${allowed.join(', ')}`
|
|
@@ -423,3 +516,11 @@ async function selectHat(species, eye, rarity) {
|
|
|
423
516
|
}),
|
|
424
517
|
});
|
|
425
518
|
}
|
|
519
|
+
|
|
520
|
+
async function selectStat(label, exclude) {
|
|
521
|
+
const choices = STAT_NAMES
|
|
522
|
+
.filter(s => s !== exclude)
|
|
523
|
+
.map(s => ({ name: s, value: s }));
|
|
524
|
+
|
|
525
|
+
return select({ message: label, choices });
|
|
526
|
+
}
|