diff --git a/src/commands.ts b/src/commands.ts index 10f03b2..2dd363a 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -115,11 +115,8 @@ const forkCmd = feature('FORK_SUBAGENT') require('./commands/fork/index.js') as typeof import('./commands/fork/index.js') ).default : null -const buddy = feature('BUDDY') - ? ( - require('./commands/buddy/index.js') as typeof import('./commands/buddy/index.js') - ).default - : null +// buddy loaded directly (not feature-gated) for this build +import buddy from './commands/buddy/index.js' /* eslint-enable @typescript-eslint/no-require-imports */ import thinkback from './commands/thinkback/index.js' import thinkbackPlay from './commands/thinkback-play/index.js' @@ -319,7 +316,7 @@ const COMMANDS = memoize((): Command[] => [ vim, ...(webCmd ? [webCmd] : []), ...(forkCmd ? [forkCmd] : []), - ...(buddy ? [buddy] : []), + buddy, ...(proactive ? [proactive] : []), ...(briefCommand ? [briefCommand] : []), ...(assistantCommand ? [assistantCommand] : []), diff --git a/src/commands/buddy/buddy.ts b/src/commands/buddy/buddy.ts new file mode 100644 index 0000000..2b371f2 --- /dev/null +++ b/src/commands/buddy/buddy.ts @@ -0,0 +1,254 @@ +import { + getCompanion, + roll, + type Roll, + companionUserId, +} from '../../buddy/companion.js' +import { + type StoredCompanion, + RARITY_STARS, + STAT_NAMES, + SPECIES, +} from '../../buddy/types.js' +import { renderSprite } from '../../buddy/sprites.js' +import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js' +import type { LocalCommandCall } from '../../types/command.js' + +// Species → default name fragments for hatch (no API needed) +const SPECIES_NAMES: Record = { + duck: 'Waddles', + goose: 'Goosberry', + blob: 'Gooey', + cat: 'Whiskers', + dragon: 'Ember', + octopus: 'Inky', + owl: 'Hoots', + penguin: 'Waddleford', + turtle: 'Shelly', + snail: 'Trailblazer', + ghost: 'Casper', + axolotl: 'Axie', + capybara: 'Chill', + cactus: 'Spike', + robot: 'Byte', + rabbit: 'Flops', + mushroom: 'Spore', + chonk: 'Chonk', +} + +const SPECIES_PERSONALITY: Record = { + duck: 'Quirky and easily amused. Leaves rubber duck debugging tips everywhere.', + goose: 'Assertive and honks at bad code. Takes no prisoners in code reviews.', + blob: 'Adaptable and goes with the flow. Sometimes splits into two when confused.', + cat: 'Independent and judgmental. Watches you type with mild disdain.', + dragon: 'Fiery and passionate about architecture. Hoards good variable names.', + octopus: 'Multitasker extraordinaire. Wraps tentacles around every problem at once.', + owl: 'Wise but verbose. Always says "let me think about that" for exactly 3 seconds.', + penguin: 'Cool under pressure. Slides gracefully through merge conflicts.', + turtle: 'Patient and thorough. Believes slow and steady wins the deploy.', + snail: 'Methodical and leaves a trail of useful comments. Never rushes.', + ghost: 'Ethereal and appears at the worst possible moments with spooky insights.', + axolotl: 'Regenerative and cheerful. Recovers from any bug with a smile.', + capybara: 'Zen master. Remains calm while everything around is on fire.', + cactus: 'Prickly on the outside but full of good intentions. Thrives on neglect.', + robot: 'Efficient and literal. Processes feedback in binary.', + rabbit: 'Energetic and hops between tasks. Finishes before you start.', + mushroom: 'Quietly insightful. Grows on you over time.', + chonk: 'Big, warm, and takes up the whole couch. Prioritizes comfort over elegance.', +} + +function speciesLabel(species: string): string { + return species.charAt(0).toUpperCase() + species.slice(1) +} + +function renderStats(stats: Record): string { + const lines = STAT_NAMES.map(name => { + const val = stats[name] ?? 0 + const filled = Math.round(val / 5) + const bar = '█'.repeat(filled) + '░'.repeat(20 - filled) + return ` ${name.padEnd(10)} ${bar} ${val}` + }) + return lines.join('\n') +} + +function companionInfoText(roll: Roll): string { + const { bones } = roll + const sprite = renderSprite(bones, 0) + const stars = RARITY_STARS[bones.rarity] + const name = SPECIES_NAMES[bones.species] ?? 'Buddy' + const shiny = bones.shiny ? ' ✨ Shiny!' : '' + + return [ + sprite.join('\n'), + '', + ` ${name} the ${speciesLabel(bones.species)}${shiny}`, + ` Rarity: ${stars} (${bones.rarity})`, + ` Eye: ${bones.eye} Hat: ${bones.hat}`, + '', + ' Stats:', + renderStats(bones.stats), + ].join('\n') +} + +export const call: LocalCommandCall = async (args, _context) => { + const sub = args.trim().toLowerCase() + const config = getGlobalConfig() + + // /buddy — show current companion or hint to hatch + if (sub === '') { + const companion = getCompanion() + if (!companion) { + return { + type: 'text', + value: + "You don't have a companion yet! Use /buddy hatch to get one.", + } + } + const stars = RARITY_STARS[companion.rarity] + const sprite = renderSprite(companion, 0) + const shiny = companion.shiny ? ' ✨ Shiny!' : '' + + const lines = [ + sprite.join('\n'), + '', + ` ${companion.name} the ${speciesLabel(companion.species)}${shiny}`, + ` Rarity: ${stars} (${companion.rarity})`, + ` Eye: ${companion.eye} Hat: ${companion.hat}`, + companion.personality ? `\n "${companion.personality}"` : '', + '', + ' Stats:', + renderStats(companion.stats), + '', + ' Commands: /buddy pet /buddy mute /buddy unmute /buddy hatch /buddy rehatch', + ] + return { type: 'text', value: lines.join('\n') } + } + + // /buddy hatch — create a new companion + if (sub === 'hatch') { + if (config.companion) { + return { + type: 'text', + value: `You already have a companion! Use /buddy to see it.\n(Tip: /buddy hatch again will re-roll a new one.)`, + } + } + + const r = roll(companionUserId()) + const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy' + const personality = + SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.' + + const stored: StoredCompanion = { + name, + personality, + hatchedAt: Date.now(), + } + + saveGlobalConfig(cfg => ({ ...cfg, companion: stored })) + + const stars = RARITY_STARS[r.bones.rarity] + const sprite = renderSprite(r.bones, 0) + const shiny = r.bones.shiny ? ' ✨ Shiny!' : '' + + const lines = [ + ' 🎉 A wild companion appeared!', + '', + sprite.join('\n'), + '', + ` ${name} the ${speciesLabel(r.bones.species)}${shiny}`, + ` Rarity: ${stars} (${r.bones.rarity})`, + ` "${personality}"`, + '', + ' Your companion will now appear beside your input box!', + ] + return { type: 'text', value: lines.join('\n') } + } + + // /buddy pet — trigger heart animation + if (sub === 'pet') { + const companion = getCompanion() + if (!companion) { + return { + type: 'text', + value: + "You don't have a companion yet! Use /buddy hatch to get one.", + } + } + + // Import setAppState dynamically to update companionPetAt + try { + const { setAppState } = await import('../../state/AppStateStore.js') + setAppState(prev => ({ + ...prev, + companionPetAt: Date.now(), + })) + } catch { + // If AppState is not available (non-interactive), just show text + } + + return { + type: 'text', + value: ` ${renderSprite(companion, 0).join('\n')}\n\n ${companion.name} purrs happily! ♥`, + } + } + + // /buddy mute + if (sub === 'mute') { + if (config.companionMuted) { + return { type: 'text', value: ' Companion is already muted.' } + } + saveGlobalConfig(cfg => ({ ...cfg, companionMuted: true })) + return { type: 'text', value: ' Companion muted. It will hide quietly. Use /buddy unmute to bring it back.' } + } + + // /buddy unmute + if (sub === 'unmute') { + if (!config.companionMuted) { + return { type: 'text', value: ' Companion is not muted.' } + } + saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false })) + return { type: 'text', value: ' Companion unmuted! Welcome back.' } + } + + // /buddy rehatch — re-roll a new companion (replaces existing) + if (sub === 'rehatch') { + const r = roll(companionUserId()) + const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy' + const personality = + SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.' + + const stored: StoredCompanion = { + name, + personality, + hatchedAt: Date.now(), + } + + saveGlobalConfig(cfg => ({ ...cfg, companion: stored })) + + const stars = RARITY_STARS[r.bones.rarity] + const sprite = renderSprite(r.bones, 0) + const shiny = r.bones.shiny ? ' ✨ Shiny!' : '' + + const lines = [ + ' 🎉 A new companion appeared!', + '', + sprite.join('\n'), + '', + ` ${name} the ${speciesLabel(r.bones.species)}${shiny}`, + ` Rarity: ${stars} (${r.bones.rarity})`, + ` "${personality}"`, + '', + ' Your old companion has been replaced!', + ] + return { type: 'text', value: lines.join('\n') } + } + + // Unknown subcommand + return { + type: 'text', + value: + ' Unknown command: /buddy ' + + sub + + '\n Commands: /buddy (info) /buddy hatch /buddy rehatch /buddy pet /buddy mute /buddy unmute', + } +} diff --git a/src/commands/buddy/index.ts b/src/commands/buddy/index.ts index 29ae609..dca9df8 100644 --- a/src/commands/buddy/index.ts +++ b/src/commands/buddy/index.ts @@ -1,3 +1,11 @@ -// Auto-generated stub — replace with real implementation -const _default: Record = {}; -export default _default; +import type { Command } from '../../commands.js' + +const buddy = { + type: 'local', + name: 'buddy', + description: 'View and manage your companion buddy', + supportsNonInteractive: false, + load: () => import('./buddy.js'), +} satisfies Command + +export default buddy diff --git a/src/entrypoints/cli.tsx b/src/entrypoints/cli.tsx index 2a008c5..d23b9f3 100644 --- a/src/entrypoints/cli.tsx +++ b/src/entrypoints/cli.tsx @@ -1,6 +1,6 @@ #!/usr/bin/env bun // Runtime polyfill for bun:bundle (build-time macros) -const feature = (_name: string) => false; +const feature = (name: string) => name === "BUDDY"; if (typeof globalThis.MACRO === "undefined") { (globalThis as any).MACRO = { VERSION: "2.1.888",