The buddy/Tamagotchi companion system in Claude Code is a sophisticated procedural generation system that creates persistent, deterministic companions for each user. The system is feature-gated, split across 6 TypeScript files, and implements a complete lifecycle from initial hatching through persistent state management.
Key Design Philosophy: Buddies are regenerated from user ID on every read (not fully persistent), allowing species renames and array changes without breaking stored companions. Only the "soul" (name, personality) persists in the config file.
| Component | File | Lines | Purpose |
|---|---|---|---|
| Type Definitions | types.ts |
~149 | Species, rarity tiers, stat names, and data structures |
| RNG & Determinism | companion.ts |
~134 | Mulberry32 PRNG, hash-based seeding, procedural generation |
| ASCII Art & Rendering | sprites.ts |
~515 | 18 species with 3-frame animations each, eye variants, hats |
| React Sprite Component | CompanionSprite.tsx |
~371 | Terminal UI animation, speech bubbles, pet reactions |
| System Prompt | prompt.ts |
~37 | Companion intro attachment system |
| Teaser & Notifications | useBuddyNotification.tsx |
~98 | Rainbow teaser UI, buddy discovery triggers |
Total: ~1,298 lines of implementation
All species are encoded using Unicode hex escapes to evade a canary string check in the build output. This prevents the literal string for one species (which collides with a model codename) from appearing in bundles:
const c = String.fromCharCode- duck —
0x64,0x75,0x63,0x6b - goose —
0x67,0x6f,0x6f,0x73,0x65 - blob —
0x62,0x6c,0x6f,0x62 - cat —
0x63,0x61,0x74 - dragon —
0x64,0x72,0x61,0x67,0x6f,0x6e - octopus —
0x6f,0x63,0x74,0x6f,0x70,0x75,0x73 - owl —
0x6f,0x77,0x6c - penguin —
0x70,0x65,0x6e,0x67,0x75,0x69,0x6e - turtle —
0x74,0x75,0x72,0x74,0x6c,0x65 - snail —
0x73,0x6e,0x61,0x69,0x6c - ghost —
0x67,0x68,0x6f,0x73,0x74 - axolotl —
0x61,0x78,0x6f,0x6c,0x6f,0x74,0x6c - capybara —
0x63,0x61,0x70,0x79,0x62,0x61,0x72,0x61 - cactus —
0x63,0x61,0x63,0x74,0x75,0x73 - robot —
0x72,0x6f,0x62,0x6f,0x74 - rabbit —
0x72,0x61,0x62,0x62,0x69,0x74 - mushroom —
0x6d,0x75,0x73,0x68,0x72,0x6f,0x6f,0x6d - chonk —
0x63,0x68,0x6f,0x6e,0x6b
Each species has unique ASCII art (12 chars wide × 5 lines). Example — duck has 3 frames for idle animation:
- Frame 0: Still duck
- Frame 1: Slight tail movement
- Frame 2: Different foot position
const RARITIES = [
'common',
'uncommon',
'rare',
'epic',
'legendary',
] as constconst RARITY_WEIGHTS = {
common: 60, // 60% (60÷100 = 60%)
uncommon: 25, // 25% (85% cumulative)
rare: 10, // 10% (95% cumulative)
epic: 4, // 4% (99% cumulative)
legendary: 1, // 1% (100% cumulative)
} // Total weight: 100Function: rollRarity(rng: () => number): Rarity
The system generates a random float [0, 1) via the PRNG, multiplies by total weight (100), then iterates through rarities, subtracting their weights until the roll goes negative:
function rollRarity(rng: () => number): Rarity {
const total = 100 // sum of all RARITY_WEIGHTS
let roll = rng() * total // roll is 0-100
// Iterator order matters: common → uncommon → rare → epic → legendary
for (const rarity of RARITIES) {
roll -= RARITY_WEIGHTS[rarity]
if (roll < 0) return rarity
}
return 'common' // fallback (should never reach)
}Example rolls:
- roll=15 → common (15-60 < 0)
- roll=70 → uncommon (70-60=10, 10-25 < 0)
- roll=92 → rare (92-60=32, 32-25=7, 7-10 < 0)
- roll=97 → epic (97-60=37, 37-25=12, 12-10=2, 2-4 < 0)
- roll=99 → legendary (99-60=39, 39-25=14, 14-10=4, 4-4=0, then 0-1 < 0)
const RARITY_STARS = {
common: '★', // 1 star
uncommon: '★★', // 2 stars
rare: '★★★', // 3 stars
epic: '★★★★', // 4 stars
legendary: '★★★★★', // 5 stars
}
const RARITY_COLORS = {
common: 'inactive', // Gray
uncommon: 'success', // Green
rare: 'permission', // Blue
epic: 'autoAccept', // Yellow/Gold
legendary: 'warning', // Red/Orange
}function mulberry32(seed: number): () => number {
let a = seed >>> 0 // Coerce to unsigned 32-bit
return function () {
a |= 0 // Keep as 32-bit signed
a = (a + 0x6d2b79f5) | 0 // Add constant, re-sign
let t = Math.imul(a ^ (a >>> 15), 1 | a) // Multiply + XOR rotation
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t // Secondary XOR folding
return ((t ^ (t >>> 14)) >>> 0) / 4294967296 // Normalize to [0,1)
}
}Salt constant: 'friend-2026-401' (hard-coded; date suggests April 1, 2026 launch)
Seed derivation:
const SALT = 'friend-2026-401'
export function roll(userId: string): Roll {
const key = userId + SALT
// On Bun: use native Bun.hash(key)
// On Node: use FNV-1a hash (below)
const seed = hashString(key)
const rng = mulberry32(seed)
return rollFrom(rng)
}Bun (native):
if (typeof Bun !== 'undefined') {
return Number(BigInt(Bun.hash(s)) & 0xffffffffn)
}Node.js (FNV-1a):
let h = 2166136261 // FNV offset basis
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i)
h = Math.imul(h, 16777619) // FNV prime
}
return h >>> 0CRITICAL SECURITY NOTE: The sequence is fully deterministic:
- Same
userId + SALT→ same seed - Same seed → same RNG sequence
- Same RNG sequence → same species, stats, eye, hat, shininess
Caching: Results are cached to avoid rehashing:
let rollCache: { key: string; value: Roll } | undefined
export function roll(userId: string): Roll {
const key = userId + SALT
if (rollCache?.key === key) return rollCache.value
const value = rollFrom(mulberry32(hashString(key)))
rollCache = { key, value }
return value
}This is called from three hot paths:
- 500ms sprite tick
- Per-keystroke PromptInput
- Per-turn observer
The cache prevents repeated hashing and PRNG generation.
const STAT_NAMES = [
'DEBUGGING', // Problem-solving prowess
'PATIENCE', // Tolerance for tedious tasks
'CHAOS', // Propensity for wild ideas
'WISDOM', // Long-term thinking
'SNARK', // Sarcastic commentary
] as constFunction: rollStats(rng: () => number, rarity: Rarity): Record<StatName, number>
Each companion has one peak stat, one dump stat, and three balanced stats. Rarity raises the floor:
const RARITY_FLOOR: Record<Rarity, number> = {
common: 5,
uncommon: 15,
rare: 25,
epic: 35,
legendary: 50,
}Generation logic:
- Randomly pick one stat as the peak (highest)
- Randomly pick a different stat as the dump (lowest)
- For each stat:
- Peak:
floor + 50 + [0,30)= capped at 100 - Dump:
floor - 10 + [0,15)= minimum 1 - Others:
floor + [0,40)
- Peak:
Rare legendary companion (floor=50):
- Peak (e.g., CHAOS): 50 + 50 + 28 = 128 → capped to 100
- Dump (e.g., PATIENCE): 50 - 10 + 8 = 48 (minimum enforced if negative)
- Balanced (e.g., DEBUGGING): 50 + 25 = 75
Common companion (floor=5):
- Peak: 5 + 50 + 18 = 73
- Dump: 5 - 10 + 12 = 7 (or at least 1)
- Balanced: 5 + 32 = 37
type CompanionBones = {
rarity: Rarity
species: Species
eye: Eye // ·, ✦, ×, ◉, @, °
hat: Hat // none, crown, tophat, propeller, halo, wizard, beanie, tinyduck
shiny: boolean // 1% chance (rng() < 0.01)
stats: Record<StatName, number>
}Bones are always regenerated from hash(userId + SALT) on every read. This design prevents:
- Config edits from faking a legendary companion
- Species list changes from breaking old companions
- Species renames from invalidating stored data
type CompanionSoul = {
name: string // User-supplied or model-generated
personality: string // Model-generated description
}type Companion = CompanionBones & CompanionSoul & {
hatchedAt: number // Timestamp
}type StoredCompanion = CompanionSoul & { hatchedAt: number }
// File: ~/.config/claude/config.json
{
"companion": {
"name": "Ziggy",
"personality": "A curious, playful spirit...",
"hatchedAt": 1712000000000
}
}- Dimensions: 12 characters wide × 5 lines tall
- Frames per species: 3 (idle animation cycle)
- Total sprites: 18 species × 3 frames = 54 distinct ASCII artworks
- Line 0 (special): Hat slot (blank in frames 0-1, can be used in frame 2)
const HATS = [
'none', // No hat (blank line 0)
'crown', // \\^^^/
'tophat', // [___]
'propeller', // -+-
'halo', // ( )
'wizard', // /^\\
'beanie', // (___)
'tinyduck', // ,>
] as constHat logic:
- Only non-common companions get random hats
- Hat only displays if line 0 is blank (no smoke/antenna in that frame)
- If all frames have blank line 0, that line is dropped to save space
const EYES = ['·', '✦', '×', '◉', '@', '°'] as constEach species has two eye slots (left and right) filled with the same eye type. Examples:
- Duck:
<(· )___(left eye) - Cat:
( · ω ·)(both eyes) - Blob:
( · · )(both eyes)
Function: renderSprite(bones: CompanionBones, frame = 0): string[]
- Select the frame (cycling through 0, 1, 2)
- Replace
{E}placeholders with the chosen eye - Optionally inject hat into line 0 (if blank and hat ≠ 'none')
- Drop line 0 if blank across all frames (optimization)
Example duck rendering (frame 1, eye='·', hat='crown'):
\^^^/ (hat injected)
__
<(· )___ (eye replaced)
( ._>
`--´~ (frame 1 variant)
Function: renderFace(bones: CompanionBones): string
Compact face representation for narrow terminals:
- Duck:
(·> - Blob:
(··) - Cat:
=·ω·= - Dragon:
<·~·> - Owl:
(·)(·) - Penguin:
(·>) - Capybara:
(·oo·) - Robot:
[··]
const TICK_MS = 500 // Animation frame interval
const BUBBLE_SHOW = 20 // Ticks (20 × 500ms = 10 seconds)
const FADE_WINDOW = 6 // Last 3 seconds the bubble dims
const PET_BURST_MS = 2500 // How long hearts float after /buddy pet
const MIN_COLS_FOR_FULL_SPRITE = 100 // Terminal width threshold
const SPRITE_BODY_WIDTH = 12 // ASCII art width
const BUBBLE_WIDTH = 36 // Speech bubble + tail
const NARROW_QUIP_CAP = 24 // Max chars for narrow mode quipconst IDLE_SEQUENCE = [
0, 0, 0, 0, 1, // Rest mostly
0, 0, 0, -1, // Blink on frame 0
0, 0, 2, 0, 0, 0
]
// -1 = "blink on frame 0" (replace eyes with '-')
// Cycles every 15 ticks (7.5 seconds)When user runs /buddy pet, hearts float up and fade over 5 ticks (~2.5s):
const H = figures.heart // ❤ character
const PET_HEARTS = [
` ❤ ❤ `, // Frame 0: wide
` ❤ ❤ ❤ `, // Frame 1: tighter
` ❤ ❤ ❤ `, // Frame 2
`❤ ❤ ❤ `, // Frame 3
'· · · ', // Frame 4: fade to dots
]Narrow terminals (< 100 columns):
- One-line face:
❤ (·) "quip or name" - Speech replaces name (no room for bubble)
- Quip truncated to 24 chars
Full terminals (≥ 100 columns):
- Multi-line sprite (body + name below)
- Speech bubble beside sprite (non-fullscreen mode)
- Floating bubble overlay (fullscreen mode)
type SpeechBubble = {
text: string
color: keyof Theme // e.g., 'success', 'warning'
fading: boolean // Dim if about to disappear
tail: 'right' | 'left' // Tail direction
}Rendered as Box with:
- Border style: 'round'
- Padding: 1 unit
- Width: 34 chars
- Text wrapping at 30 chars
- Italic formatting
- Colors fade when aging
// Per 500ms tick
const tick = useState(0)
const lastSpokeTick = useRef(0)
const [{petStartTick}, setPetStart] = useState({...})
// Age calculations
const bubbleAge = tick - lastSpokeTick
const fading = bubbleAge >= BUBBLE_SHOW - FADE_WINDOW
const petAge = tick - petStartTick
const petting = petAge * TICK_MS < PET_BURST_MSif (reaction || petting) {
// Excited: cycle all frames fast
spriteFrame = tick % frameCount
} else {
// Idle: use IDLE_SEQUENCE
const step = IDLE_SEQUENCE[tick % 15]
if (step === -1) {
spriteFrame = 0
blink = true // Replace eyes with '-'
} else {
spriteFrame = step % frameCount
}
}
// Blink post-processing
const body = renderSprite(companion, spriteFrame)
.map(line => blink ? line.replaceAll(eye, '-') : line)Function: companionReservedColumns(terminalColumns: number, speaking: boolean): number
Calculates how many columns the sprite occupies so PromptInput can wrap correctly:
if (terminalColumns < 100) return 0 // Not enough space
const nameWidth = stringWidth(companion.name)
const bubble = (speaking && !fullscreen) ? 36 : 0
return Math.max(12, nameWidth + 2) + 2 + bubbleTriggers the initial hatching flow:
- Generates deterministic bones from userId
- Calls Claude to generate a name + personality
- Stores the soul in config
- Displays the newly hatched companion
- Sets
AppState.companionPetAt = Date.now() - CompanionSprite renders hearts for 2.5 seconds
- No persistent effect (animation only)
// In config:
companionMuted?: booleanSuppresses all buddy UI rendering if set to true.
The buddy name is user-customizable post-hatch by editing the config file or through model generation on first contact (stored in CompanionSoul.name).
Buddies respond when:
- User addresses them directly by name in input
- System triggers reactions (prompts for the model to generate a speech bubble)
AppState.companionReactionis set to a string
The speech appears for 10 seconds, then fades for 3 seconds before disappearing.
-
Check: Does user have stored companion in config?
- Yes → Load and merge with regenerated bones
- No → Proceed to hatching
-
Generate Bones:
const userId = config.oauthAccount?.accountUuid ?? config.userID ?? 'anon' const {bones} = roll(userId) // Deterministic from userId + 'friend-2026-401'
-
Generate Soul (Claude model):
// Prompt generates: { name, personality } // Uses bones.stats and species as context
-
Store in config:
config.companion = { name: "Ziggy", personality: "...", hatchedAt: timestamp }
NOT random per user: Each user gets the same species/rarity/stats forever (unless they change accounts). The species is deterministic from their ID hash.
- Users can't "reroll" for a legendary by clearing config
- Changing species in the SPECIES array doesn't break old companions
- Users see the "same buddy" across sessions/devices (if they use the same account)
The buddy has no traditional level-up system. State is:
Immutable at creation:
- Species, rarity, stats, eyes, hat, shininess
- Generated once from userId, never updated
Mutable (user-controlled):
- Name (can be edited in config)
- Personality (generated once, doesn't change)
- Muted status (toggle in settings)
Transient (per-session):
companionReaction: Current speech bubble text (cleared after 10s)companionPetAt: Timestamp of last pet (used for heart animation)- Sprite frame: Which idle frame (cycles every 500ms)
Speech Bubble Triggers (sets companionReaction):
- User types
/buddydirectly (dedicated command) - User addresses companion by name in input
- System generates contextual reactions (via Claude prompt)
Pet Event (sets companionPetAt):
- User runs
/buddy petcommand - Displays hearts for 2.5 seconds (5 frames @ 500ms each)
Mute Event:
- User toggles
companionMutedin settings - Suppresses all UI rendering
11. Easter Eggs & Hidden Behaviors
shiny: rng() < 0.01 // Only 1 in 100 companionsCurrently, "shiny" flag is generated but not rendered in UI (no visual distinction implemented). It's a future expansion point.
Eyes are replaced with - characters when the idle sequence hits -1. Creates a periodic "blink" effect every ~15 ticks.
- Dragon: Frame 2 has
~ ~above (smoke/fire breath) - Octopus: Frame 2 has
oabove (bubble) - Penguin: Frame 2 has padding changes (subtle waddle)
- Capybara: Frame 2 has
~ ~above (water ripples) - Cactus: Frame 2 uses arms differently (stretch)
- Robot: Frame 2 has
*above (sparks/thoughts) - Mushroom: Frame 2 has
. o .above (spores)
One species name collides with a model codename in excluded-strings.txt. Rather than allow the literal string in the bundle, Anthropic:
- Encodes the species name as Unicode hex
- Runs a grep check on build output (not source)
- Allows the runtime-constructed value to pass (type erasure)
This is not security but obfuscation — the actual species name is readable in source.
On April 1-7, 2026 (teaser window), users without a buddy see:
/buddy (each character in a different color)
After April 7, the command stays live but teaser is hidden.
In narrow terminals, buddies display as:
❤ =·ω·= "quip"
A single-line face representation with optional speech.
The roll system returns:
{
bones: CompanionBones,
inspirationSeed: Math.floor(rng() * 1e9) // Max 10^9
}The inspirationSeed is passed to the Claude prompt to make name/personality generation "inspired" by the procedural stats/species. Currently generated but not used (future expansion).
-
User ID (hashed):
- SALT is public:
'friend-2026-401' - Hash function is public (FNV-1a or Bun.hash)
hash(userId + SALT)determines species/rarity/stats- Risk: If userId is known, species/stats are instantly predictable
- SALT is public:
-
Account UUID (if OAuth):
- Used as fallback userId:
config.oauthAccount?.accountUuid - More stable than local userID, but still hashed with known salt
- Risk: OAuth accounts have stable, guessable companions
- Used as fallback userId:
-
Mute Status:
- Stored in config:
companionMuted - Indicates user preferences (whether they find buddy annoying)
- Risk: Low
- Stored in config:
-
Hatch Timestamp:
hatchedAtstored in config- Reveals when user first set up buddy feature
- Risk: Low (could infer adoption timeline)
-
Name & Personality (user-supplied):
- Stored in config and sent in system prompts
- Could reveal user's taste/preferences
- Risk: Medium (stored locally only, sent to Claude API)
- NOT sent to external services: Buddy data is purely local + API bound
- Config file location:
~/.config/claude/config.json(user-controlled) - No telemetry: Buddy name/personality not logged analytically
- Deterministic by design: No RNG from externally-controlled sources
Can buddy state be faked?
// User edits config.json:
{
"companion": {
"name": "Legendary Dragon with 100 CHAOS",
"personality": "Super powerful"
}
}Result: The display name changes, but bones are always regenerated.
- Species: Still determined by userId hash (can't edit)
- Rarity: Still determined by userId hash (can't edit)
- Stats: Still determined by userId hash (can't edit)
- Name/personality: Edit takes effect immediately
Mitigation: Bones are immutable and regenerated on every read, so editing the soul doesn't grant fake stats.
type Rarity = 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'
type Species = 'duck' | 'goose' | 'blob' | 'cat' | ... | 'chonk' // 18 total
type Eye = '·' | '✦' | '×' | '◉' | '@' | '°' // 6 total
type Hat = 'none' | 'crown' | 'tophat' | 'propeller' | 'halo' | 'wizard' | 'beanie' | 'tinyduck' // 8 total
type StatName = 'DEBUGGING' | 'PATIENCE' | 'CHAOS' | 'WISDOM' | 'SNARK' // 5 total
// Deterministic bones
type CompanionBones = {
rarity: Rarity
species: Species
eye: Eye
hat: Hat
shiny: boolean
stats: Record<StatName, number> // Each 1-100
}
// User-generated soul
type CompanionSoul = {
name: string
personality: string
}
// Full companion (union)
type Companion = CompanionBones & CompanionSoul & {
hatchedAt: number // Timestamp ms
}
// Stored in config (bones excluded)
type StoredCompanion = CompanionSoul & { hatchedAt: number }
// RNG output
type Roll = {
bones: CompanionBones
inspirationSeed: number // 0-1e9
}// Bones never persist—regenerated on every read
export function getCompanion(): Companion | undefined {
const stored = getGlobalConfig().companion
if (!stored) return undefined
const { bones } = roll(companionUserId())
return { ...stored, ...bones } // Bones override stale fields
}Benefit: Species list changes, renames, and structure updates don't invalidate stored companions.
// Each call to roll() hashes and creates a fresh RNG
export function roll(userId: string): Roll {
const key = userId + SALT
const rng = mulberry32(hashString(key))
return rollFrom(rng)
}
// Separate function for seeding with arbitrary strings
export function rollWithSeed(seed: string): Roll {
return rollFrom(mulberry32(hashString(seed)))
}Benefit: Encapsulation; RNG is never exposed directly; seeding is consistent.
let rollCache: { key: string; value: Roll } | undefined
export function roll(userId: string): Roll {
const key = userId + SALT
if (rollCache?.key === key) return rollCache.value
const value = rollFrom(mulberry32(hashString(key)))
rollCache = { key, value }
return value
}Benefit: Avoids repeated hashing in 500ms sprite ticks, per-keystroke input, and observer loops.
if (!feature('BUDDY')) return null
const companion = getCompanion()
if (!companion || getGlobalConfig().companionMuted) return nullBenefit: Cleanly disables feature if flag is off or config unavailable.
function rollRarity(rng: () => number): Rarity {
let roll = rng() * 100
for (const rarity of RARITIES) {
roll -= RARITY_WEIGHTS[rarity]
if (roll < 0) return rarity
}
return 'common'
}Benefit: O(n) roll, fair distribution, easy to adjust weights.
const peak = pick(rng, STAT_NAMES)
let dump = pick(rng, STAT_NAMES)
while (dump === peak) dump = pick(rng, STAT_NAMES)
for (const name of STAT_NAMES) {
if (name === peak) stats[name] = high_value
else if (name === dump) stats[name] = low_value
else stats[name] = mid_value
}Benefit: Creates distinct personalities; no companions are "well-rounded" (always have a specialty and weakness).
- Persistent, deterministic companions — each user gets the same species/stats forever
- Lightweight procedural generation — 18 × 3 ASCII artworks, 5 stats, pre-computed
- Rich animation — 500ms ticks, idle sequences, pet reactions, speech bubbles
- No persistence complexity — bones regenerated, only soul stored
- Scalable display — adapts to narrow/full terminals
- Leveling/progression — stats are fixed at hatch
- Multiplayer interaction — companions are solo
- Shiny rendering — flag exists but not displayed
- Voice generation — speech is text-only (no audio)
- Buddy-specific commands — only
/buddy petand/buddyexist - Persistent buddy actions — no memory of past interactions
- Inspiration seed usage — generated but unused in prompts
- Narrow terminals: Collapses to face + quip
- Fullscreen mode: Floating bubble overlay instead of inline
- Blinks: Eyes replaced with
-periodically - Fading: Bubble dims in last 3 seconds before clearing
- Hat logic: Only injects if line 0 blank across all frames
- Stat floors by rarity: Legendary companions have higher minimum stats
| File | Key Responsibilities |
|---|---|
| types.ts | 18 species (hex-encoded), 5 rarities (60/25/10/4/1 weight), 8 hats, 6 eyes, 5 stats, type definitions |
| companion.ts | Mulberry32 PRNG, FNV-1a hash, seeding, rarity/stat rollout, caching, userId lookup |
| sprites.ts | ASCII art for 54 frames (18 × 3), hat injection, frame cycling, face rendering |
| CompanionSprite.tsx | React animation component, idle/pet/excited frame logic, speech bubbles, width reservation |
| prompt.ts | Companion intro attachment (sent to Claude on first hatch) |
| useBuddyNotification.tsx | Rainbow teaser (April 1-7), buddy discovery trigger, companion muting logic |
The Claude Code buddy system is a masterclass in deterministic, low-cost companion generation. By seeding from user ID, buddies are stable and reproducible without database storage. The procedural design (species × rarity × stats × eyes × hats) creates 18 × 5 × 6 × 8 × 2^1 ≈ 8,640 unique companion archetypes, yet requires only ~1,300 lines of code. The ASCII art is charming, the animation is snappy, and the system integrates cleanly with the modal architecture via React components and feature gates.
The buddy won't remember past interactions or learn from user behavior—but it doesn't need to. It's designed to be a presence, not a peer. A small creature that animates beside your input, nudges you with quips when you invoke it, and persists via a single JSON blob in your config file.