A tiny ASCII pet above your prompt (=^.^=) that cheers when tests pass, worries when commands fail, naps when you are idle, and levels up as you work.

hooks/register.tsx 293 lines1// Code Pet: a tiny pet above your prompt. It watches you code, cheers
2// when tests pass, worries when commands fail, naps when you are idle, and
3// earns XP as you work, hatching from an egg and growing over the weeks.
4//
5// /pet pats it; /pet name <name>, /pet species <cat|dog|bunny|bear|fox|panda|frog|chick>,
6// /pet stats. CODE_PET=status shows it in the status line, CODE_PET=off hides it.
7// It is plain ASCII by default (=^.^=); CODE_PET_STYLE=emoji uses emoji instead.
8
9import { atom, read, update } from 'claude-code'
10import type { EngineInterface, Register } from 'claude-code'
11
12import type { PetProfile } from '../types'
13import { animate, blink, day, DEFAULT_PROFILE, doingFor, face, isTestCommand, level, MARK, MOODS, passedCount, shown, SPECIES, stage, xpBar } from './pet'
14import type { Style } from './pet'
15
16const profile = atom({ plugin: 'code-pet', key: 'profile' } as const, null)
17const flash = atom({ plugin: 'code-pet', key: 'flash' } as const, null)
18const now = atom({ plugin: 'code-pet', key: 'now' } as const, 0)
19const lastActivity = atom({ plugin: 'code-pet', key: 'lastActivity' } as const, 0)
20const isWorking = atom({ plugin: 'code-pet', key: 'isWorking' } as const, false)
21const doing = atom({ plugin: 'code-pet', key: 'doing' } as const, '')
22const doingMood = atom({ plugin: 'code-pet', key: 'doingMood' } as const, 'working')
23const failStreak = atom({ plugin: 'code-pet', key: 'failStreak' } as const, 0)
24const contextPercent = atom({ plugin: 'code-pet', key: 'contextPercent' } as const, 0)
25const limitPercent = atom({ plugin: 'code-pet', key: 'limitPercent' } as const, 0)
26
27const SOUNDS: Record<string, string> = {
28 cat: 'purrs happily',
29 dog: 'wags its tail',
30 bunny: 'does a happy hop',
31 bear: 'gives you a hug',
32 fox: 'yips with joy',
33 panda: 'munches happily',
34 frog: 'ribbits',
35 chick: 'cheeps',
36}
37
38type Display = 'band' | 'status' | 'off'
39let display: Display = 'band'
40let style: Style = 'ascii'
41
42export const register: Register = on => {
43 on('session.start', async ($, e, next) => {
44 const result = await next(e)
45 const mode = ((await $.env.get('CODE_PET')) ?? 'band').toLowerCase()
46 display = mode === 'status' || mode === 'off' ? mode : 'band'
47 style = ((await $.env.get('CODE_PET_STYLE')) ?? '').toLowerCase() === 'emoji' ? 'emoji' : 'ascii'
48 const t = await $.clock.now()
49 const saved = await $.store.get('profile')
50 const loaded: PetProfile = saved && typeof saved === 'object' ? { ...DEFAULT_PROFILE(t), ...(saved as PetProfile) } : DEFAULT_PROFILE(t)
51 await update($, profile, () => loaded)
52 if (saved === undefined || saved === null) await $.store.set('profile', loaded)
53 await update($, now, () => t)
54 await update($, lastActivity, () => t)
55 try {
56 await $.command.register({
57 name: 'pet',
58 description: 'Pat your Code Pet (or: /pet name <name>, /pet species <kind>, /pet stats)',
59 argumentHint: '[name <name> | species <kind> | stats]',
60 immediate: true,
61 })
62 } catch (error) {
63 $.ui.log(`code-pet: could not register /pet (${String(error)})`, { to: 'debug' })
64 }
65 // One tick a second: the animation frame, idle naps, and the status line.
66 $.clock.every(1_000, () => tick($))
67 return result
68 })
69
70 on('turn.start', async ($, e, next) => {
71 const t = await $.clock.now()
72 await update($, isWorking, () => true)
73 await update($, doing, () => 'on it!')
74 await update($, doingMood, () => 'working')
75 await update($, lastActivity, () => t)
76 return next(e)
77 })
78
79 on('tool.call', async ($, e, next) => {
80 const tool = String(e.tool)
81 const input = e as unknown as Record<string, unknown>
82 const what = doingFor(tool, input)
83 await update($, doing, () => what.say)
84 await update($, doingMood, () => what.mood)
85 const started = await $.clock.now()
86 await update($, lastActivity, () => started)
87
88 const command = tool === 'Bash' && typeof input.command === 'string' ? input.command : undefined
89 const ran = await next(e)
90 if (command === undefined || ran.deny !== undefined) return ran
91
92 const ok = ran.isError !== true
93 const output = typeof ran.text === 'string' ? ran.text : ''
94 const t = await $.clock.now()
95 if (isTestCommand(command)) {
96 if (ok) {
97 const count = passedCount(output)
98 await gainXp($, 10, p => ({
99 ...p,
100 streak: p.streak + 1,
101 best: Math.max(p.best, p.streak + 1),
102 today: { date: day(t), tests: (p.today.date === day(t) ? p.today.tests : 0) + (count ?? 0) },
103 }))
104 await update($, failStreak, () => 0)
105 const p = await read($, profile)
106 const streak = p !== null && p.streak >= 3 ? ` ${MARK.streak![style]}${p.streak} in a row` : ''
107 await say($, 'happy', `${count !== undefined ? `${count} tests passed!` : 'tests passed!'} +10 XP${streak}`, 8_000)
108 } else {
109 await update($, profile, p => (p === null ? p : { ...p, streak: 0 }))
110 await saveProfile($)
111 const fails = (await read($, failStreak)) + 1
112 await update($, failStreak, () => fails)
113 await say($, fails >= 3 ? 'sad' : 'worried', fails >= 3 ? 'tests keep failing... we got this' : 'tests failed, let us fix them', 8_000)
114 }
115 } else if (!ok) {
116 const fails = (await read($, failStreak)) + 1
117 await update($, failStreak, () => fails)
118 const cmd = command.trim().split(/\s+/).slice(0, 3).join(' ')
119 await say($, fails >= 3 ? 'sad' : 'worried', `uh oh, \`${cmd.length > 30 ? cmd.slice(0, 27) + '...' : cmd}\` failed`, 8_000)
120 }
121 return ran
122 })
123
124 on('turn.complete', async ($, e, next) => {
125 const result = await next(e)
126 if (e.agentId !== undefined) return result
127 const t = await $.clock.now()
128 await update($, isWorking, () => false)
129 await update($, lastActivity, () => t)
130 if (!e.isAborted && e.reason === 'answer') {
131 await gainXp($, 5, p => p)
132 if (e.durationMs >= 60_000) await say($, 'done', `done! that took ${minutes(e.durationMs)}`, 6_000)
133 }
134 try {
135 const usage = await $.session.usage()
136 await update($, contextPercent, () => usage.context.percent ?? 0)
137 await update($, limitPercent, () => Math.max(0, ...usage.rateLimits.map(r => r.percentUsed)))
138 } catch {
139 // no readings: keep the old ones
140 }
141 return result
142 })
143
144 on('command.run', { command: 'pet' }, async ($, e) => {
145 const args = e.args.trim()
146 const p = await read($, profile)
147 if (p === null) return { text: 'Your pet is still waking up.' }
148
149 const named = /^name\s+(.+)$/i.exec(args)
150 if (named) {
151 const name = named[1]!.trim().slice(0, 20)
152 await update($, profile, q => (q === null ? q : { ...q, name }))
153 await saveProfile($)
154 await say($, 'petted', `my name is ${name}!`, 5_000)
155 return { text: `Your pet is now called ${name}.` }
156 }
157
158 const kind = /^species\s+(\w+)$/i.exec(args)
159 if (kind) {
160 const species = kind[1]!.toLowerCase()
161 if (!(species in SPECIES)) return { text: `Pick one of: ${Object.keys(SPECIES).join(', ')}` }
162 await update($, profile, q => (q === null ? q : { ...q, species }))
163 await saveProfile($)
164 return { text: `${p.name} is now a ${species} ${face({ ...p, species, xp: Math.max(p.xp, 25) }, style)} (eggs hatch at level 2).` }
165 }
166
167 if (/^stats$/i.test(args)) {
168 const t = await $.clock.now()
169 const tests = p.today.date === day(t) ? p.today.tests : 0
170 const days = Math.max(0, Math.floor((t - p.born) / 86_400_000))
171 return {
172 text: [
173 `${face(p, style)} ${p.name} the ${p.species}: level ${level(p.xp)} (${stage(p.xp)}), ${p.xp} XP`,
174 `Green test streak: ${p.streak} (best ${p.best}) | tests passed today: ${tests}`,
175 `Pats: ${p.pets} | friends for ${days} ${days === 1 ? 'day' : 'days'}`,
176 ].join('\n'),
177 }
178 }
179
180 await update($, profile, q => (q === null ? q : { ...q, pets: q.pets + 1 }))
181 await saveProfile($)
182 const sound = SOUNDS[p.species] ?? 'is happy'
183 await say($, 'petted', `${sound} ${MARK.heart![style]}`, 5_000)
184 return { text: `${face(p, style, 'petted')} ${p.name} ${sound}.` }
185 })
186
187 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
188 const below = await next(e)
189 if (display !== 'band' || e.props.hasSurvey) return below
190 const line = await current($)
191 if (line === null) return below
192 const { Box, Text } = $.ui.resolve(e)
193 const roomy = e.props.bodyColumns >= 70
194 return (
195 <Box flexDirection="column">
196 {below}
197 <Box key="code-pet" flexDirection="row" paddingX={1} justifyContent="space-between" width={e.props.bodyColumns}>
198 <Text wrap="truncate-end">
199 {line.face} {line.icon === '' ? '' : `${line.icon} `}
200 <Text bold>{line.name}</Text>
201 <Text color={line.color}>: {line.say}</Text>
202 </Text>
203 {roomy && (
204 <Text dimColor>
205 {' '}
206 Lv {line.level} {line.bar}
207 {line.streak >= 3 ? ` ${MARK.streak![style]}${line.streak}` : ''}
208 </Text>
209 )}
210 </Box>
211 </Box>
212 )
213 })
214}
215
216type Line = { face: string; icon: string; name: string; say: string; color: string; level: number; bar: string; streak: number }
217
218/** Everything the pet shows right now, from its state. */
219async function current($: EngineInterface): Promise<Line | null> {
220 const p = await read($, profile)
221 if (p === null) return null
222 const t = await read($, now)
223 const s = shown({
224 flash: await read($, flash),
225 now: t,
226 lastActivity: await read($, lastActivity),
227 isWorking: await read($, isWorking),
228 doing: await read($, doing),
229 doingMood: await read($, doingMood),
230 failStreak: await read($, failStreak),
231 contextPercent: await read($, contextPercent),
232 limitPercent: await read($, limitPercent),
233 hour: new Date(t).getHours(),
234 name: p.name,
235 })
236 const frame = Math.floor(t / 1000)
237 return {
238 face: face(p, style, blink(s.mood, frame, style)),
239 icon: animate(s.mood, frame, style),
240 name: p.name,
241 say: s.say,
242 color: MOODS[s.mood]?.color ?? 'white',
243 level: level(p.xp),
244 bar: xpBar(p.xp, style),
245 streak: p.streak,
246 }
247}
248
249async function tick($: EngineInterface) {
250 const t = await $.clock.now()
251 await update($, now, () => t)
252 if (display === 'status') {
253 const line = await current($)
254 if (line !== null) $.ui.status(`${line.face} ${line.icon === '' ? '' : `${line.icon} `}${line.name}: ${line.say}`)
255 }
256}
257
258/** A mood for a few seconds. A fresh level-up message is never overwritten. */
259async function say($: EngineInterface, mood: string, text: string, ms: number) {
260 const t = await $.clock.now()
261 const old = await read($, flash)
262 if (old !== null && old.mood === 'levelup' && old.until > t) return
263 await update($, flash, () => ({ mood, say: text, until: t + ms }))
264 await update($, now, () => t)
265}
266
267async function gainXp($: EngineInterface, amount: number, change: (p: PetProfile) => PetProfile) {
268 const before = await read($, profile)
269 if (before === null) return
270 const after = change({ ...before, xp: before.xp + amount })
271 await update($, profile, () => after)
272 await saveProfile($)
273 const from = level(before.xp)
274 const to = level(after.xp)
275 if (to > from) {
276 const t = await $.clock.now()
277 const hatched = from === 1
278 const text = hatched ? `${after.name} hatched!${MARK.hatch![style]} level ${to}` : `${after.name} grew to level ${to}!${to === 10 ? MARK.crown![style] : ''}`
279 await update($, flash, () => ({ mood: 'levelup', say: text, until: t + 8_000 }))
280 $.ui.toast(`Code Pet: ${text}`)
281 }
282}
283
284async function saveProfile($: EngineInterface) {
285 const p = await read($, profile)
286 if (p !== null) await $.store.set('profile', p)
287}
288
289function minutes(ms: number): string {
290 const m = Math.round(ms / 60_000)
291 return m < 1 ? `${Math.round(ms / 1000)}s` : `${m}m`
292}
293hooks/pet.ts 210 lines1// Code Pet's personality as plain functions: species, moods, levels, and
2// how to tell a test run from any other command. Nothing here touches $.
3//
4// Two looks: "ascii" (the default: plain ASCII, lines up in every terminal
5// and font) and "emoji" (CODE_PET_STYLE=emoji).
6
7import type { PetFlash, PetProfile } from '../types'
8
9export type Style = 'ascii' | 'emoji'
10
11export const SPECIES: Record<string, string> = {
12 cat: '🐱',
13 dog: '🐶',
14 bunny: '🐰',
15 bear: '🐻',
16 fox: '🦊',
17 panda: '🐼',
18 frog: '🐸',
19 chick: '🐥',
20}
21
22/** ASCII bodies: what goes left and right of the face. `=^.^=` is a cat, `U^.^U` a dog. */
23const ASCII_BODY: Record<string, [string, string]> = {
24 cat: ['=', '='],
25 dog: ['U', 'U'],
26 bunny: ['(\\', '/)'],
27 bear: ['(', ')'],
28 fox: ['<', '>'],
29 panda: ['[', ']'],
30 frog: ['@', '@'],
31 chick: ['{', '}'],
32}
33
34export const DEFAULT_PROFILE = (now: number): PetProfile => ({
35 name: 'Mochi',
36 species: 'cat',
37 xp: 0,
38 born: now,
39 streak: 0,
40 best: 0,
41 today: { date: day(now), tests: 0 },
42 pets: 0,
43})
44
45/** Per mood: the ASCII face (eyes and mouth), the icon beside it in each style, and the line's color. */
46export const MOODS: Record<string, { eyes: string; ascii: string; emoji: string; color: string }> = {
47 watching: { eyes: '^.^', ascii: '', emoji: '👀', color: 'white' },
48 working: { eyes: 'o_o', ascii: '...', emoji: '💻', color: 'cyan' },
49 building: { eyes: '>_<', ascii: '[#]', emoji: '🔨', color: 'cyan' },
50 reading: { eyes: 'o.o', ascii: '[=]', emoji: '📖', color: 'cyan' },
51 running: { eyes: '*_*', ascii: '[>]', emoji: '⚙️', color: 'cyan' },
52 happy: { eyes: '^o^', ascii: '\\o/', emoji: '🎉', color: 'green' },
53 done: { eyes: '^-^', ascii: '*', emoji: '✨', color: 'green' },
54 worried: { eyes: 'O.O', ascii: '!', emoji: '😰', color: 'yellow' },
55 sad: { eyes: 'T_T', ascii: ';(', emoji: '😢', color: 'red' },
56 crowded: { eyes: '@_@', ascii: '!!', emoji: '😵', color: 'magenta' },
57 tired: { eyes: '=_=', ascii: 'zz', emoji: '🥱', color: 'magenta' },
58 idle: { eyes: '-_-', ascii: '...', emoji: '💭', color: 'gray' },
59 sleeping: { eyes: '-.-', ascii: 'zZ', emoji: '💤', color: 'gray' },
60 late: { eyes: 'u_u', ascii: 'C', emoji: '🌙', color: 'blue' },
61 petted: { eyes: '^w^', ascii: '<3', emoji: '💕', color: 'magenta' },
62 levelup: { eyes: '*o*', ascii: '**', emoji: '⭐', color: 'yellow' },
63}
64
65// ---------- levels ----------
66
67/** Level from XP: 25 XP to level 2, then each level costs a little more. */
68export function level(xp: number): number {
69 return Math.floor(Math.sqrt(Math.max(0, xp) / 25)) + 1
70}
71
72/** XP where the current level starts and where the next one begins. */
73export function levelRange(xp: number): { from: number; to: number } {
74 const l = level(xp)
75 return { from: 25 * (l - 1) ** 2, to: 25 * l ** 2 }
76}
77
78/**
79 * The pet as it looks at its level, in its mood: an egg first, then the
80 * species, then a legend. ASCII: `(^)` egg, `=^o^=` cat, `*=^o^=*` legend.
81 */
82export function face(profile: PetProfile, style: Style = 'ascii', mood = 'watching'): string {
83 const l = level(profile.xp)
84 if (style === 'emoji') {
85 const animal = SPECIES[profile.species] ?? SPECIES.cat!
86 if (l <= 1) return '🥚'
87 return l >= 10 ? `${animal}👑` : animal
88 }
89 const eyes = MOODS[mood]?.eyes ?? '^.^'
90 if (l <= 1) return `(${eyes.charAt(0)})`
91 const [left, right] = ASCII_BODY[profile.species] ?? ASCII_BODY.cat!
92 const body = `${left}${eyes}${right}`
93 return l >= 10 ? `*${body}*` : body
94}
95
96export function stage(xp: number): string {
97 const l = level(xp)
98 return l <= 1 ? 'egg' : l <= 4 ? 'baby' : l <= 9 ? 'grown' : 'legendary'
99}
100
101export function xpBar(xp: number, style: Style = 'ascii', cells = 5): string {
102 const { from, to } = levelRange(xp)
103 const filled = Math.round(((xp - from) / Math.max(1, to - from)) * cells)
104 return style === 'emoji' ? '█'.repeat(filled) + '░'.repeat(cells - filled) : `[${'#'.repeat(filled)}${'.'.repeat(cells - filled)}]`
105}
106
107/** Little marks that differ by style. */
108export const MARK: Record<string, Record<Style, string>> = {
109 heart: { ascii: '<3', emoji: '♥' },
110 streak: { ascii: 'x', emoji: '🔥' },
111 hatch: { ascii: '', emoji: ' 🐣' },
112 crown: { ascii: ' legendary!', emoji: ' 👑 legendary!' },
113}
114
115// ---------- reading what happened ----------
116
117const TEST_RUNNER =
118 /\b(npm|pnpm|yarn|bun)\s+(run\s+)?test\b|\bnpx\s+(jest|vitest|mocha)\b|\b(pytest|vitest|jest|mocha|phpunit|rspec)\b|\bpython3?\s+-m\s+(pytest|unittest)\b|\b(go|cargo|deno|dart|flutter|mvn|swift)\s+test\b|\bgradlew?\s+test\b|\bdotnet\s+test\b|\bmix\s+test\b/
119
120export function isTestCommand(command: string): boolean {
121 return TEST_RUNNER.test(command)
122}
123
124/** How many tests passed, when the runner's output says so. */
125export function passedCount(output: string): number | undefined {
126 const m = /(\d+)\s+(?:tests?\s+)?(?:passed|passing)\b/i.exec(output) ?? /\bTests?:\s+(\d+)\s+passed/i.exec(output) ?? /\bok\s+\((\d+)\s+tests?\)/i.exec(output)
127 return m ? Number(m[1]) : undefined
128}
129
130/** A short "what I'm doing" line from a tool call. */
131export function doingFor(tool: string, input: Record<string, unknown>): { mood: string; say: string } {
132 const file = (p: unknown) => (typeof p === 'string' ? p.replace(/\\/g, '/').split('/').pop() ?? p : '')
133 if (tool === 'Read') return { mood: 'reading', say: `reading ${file(input.file_path)}` }
134 if (tool === 'Edit' || tool === 'Write' || tool === 'MultiEdit' || tool === 'NotebookEdit') {
135 return { mood: 'building', say: `building ${file(input.file_path ?? input.notebook_path)}` }
136 }
137 if (tool === 'Bash' && typeof input.command === 'string') {
138 const cmd = input.command.trim().split(/\s+/).slice(0, 3).join(' ')
139 return { mood: 'running', say: `running ${cmd.length > 28 ? cmd.slice(0, 25) + '...' : cmd}` }
140 }
141 if (tool === 'Grep' || tool === 'Glob') return { mood: 'reading', say: 'searching the code' }
142 if (tool === 'WebSearch' || tool === 'WebFetch') return { mood: 'reading', say: 'looking things up' }
143 if (tool === 'Agent') return { mood: 'working', say: 'calling a friend' }
144 return { mood: 'working', say: 'on it!' }
145}
146
147// ---------- what the pet shows right now ----------
148
149export type Shown = { mood: string; say: string }
150
151type Base = {
152 flash: PetFlash | null
153 now: number
154 lastActivity: number
155 isWorking: boolean
156 doing: string
157 doingMood?: string
158 failStreak: number
159 contextPercent: number
160 limitPercent: number
161 hour: number
162 name: string
163}
164
165export function shown(b: Base): Shown {
166 if (b.flash && b.flash.until > b.now) return { mood: b.flash.mood, say: b.flash.say }
167 if (b.isWorking) return { mood: b.doingMood ?? 'working', say: b.doing || 'on it!' }
168 const idleMin = (b.now - b.lastActivity) / 60_000
169 if (idleMin >= 15) return { mood: 'sleeping', say: `${b.name} is asleep` }
170 if (b.hour >= 1 && b.hour < 5) return { mood: 'late', say: "it's late... go to sleep?" }
171 if (b.limitPercent >= 90) return { mood: 'tired', say: "I'm tired... your limit is almost used" }
172 if (b.contextPercent >= 75) return { mood: 'crowded', say: "it's crowded in here... /compact?" }
173 if (b.failStreak >= 3) return { mood: 'sad', say: 'rough day... want to take a break?' }
174 if (idleMin >= 5) return { mood: 'idle', say: '...' }
175 return { mood: 'watching', say: `${b.name} is watching you code` }
176}
177
178/** A tiny animation: the icon beside the pet changes every second for some moods. */
179export function animate(mood: string, frame: number, style: Style = 'ascii'): string {
180 if (style === 'emoji') {
181 const icon = MOODS[mood]?.emoji ?? '👀'
182 if (mood === 'sleeping') return ['💤', ' ', '💤', 'z '][frame % 4]!
183 if (mood === 'happy' || mood === 'levelup' || mood === 'petted') return frame % 2 === 0 ? icon : icon === '🎉' ? '🥳' : icon === '💕' ? '💖' : '🌟'
184 if (mood === 'watching') return frame % 6 === 5 ? '😑' : icon
185 if (mood === 'building' || mood === 'running' || mood === 'working') return frame % 2 === 0 ? icon : '💦'
186 return icon
187 }
188 const icon = MOODS[mood]?.ascii ?? ''
189 if (mood === 'sleeping') return ['z', 'zZ', 'zZz', 'zZ'][frame % 4]!
190 if (mood === 'working' || mood === 'idle') return ['.', '..', '...'][frame % 3]!
191 if (mood === 'happy') return frame % 2 === 0 ? '\\o/' : '_o_'
192 if (mood === 'petted') return frame % 2 === 0 ? '<3' : '<3 <3'
193 if (mood === 'levelup') return frame % 2 === 0 ? '**' : '*'
194 return icon
195}
196
197/** In ASCII style the eyes blink now and then while watching. */
198export function blink(mood: string, frame: number, style: Style): string {
199 return style === 'ascii' && mood === 'watching' && frame % 6 === 5 ? 'idle' : mood
200}
201
202// ---------- helpers ----------
203
204const pad = (n: number) => String(n).padStart(2, '0')
205
206export function day(ms: number): string {
207 const d = new Date(ms)
208 return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
209}
210types/index.d.ts 39 lines1/** The pet itself: saved across sessions in the plugin store. */
2export type PetProfile = {
3 name: string
4 species: string
5 xp: number
6 /** When the pet hatched, ms since the epoch. */
7 born: number
8 /** Green test runs in a row, and the best run ever. */
9 streak: number
10 best: number
11 /** Tests passed today, by local date. */
12 today: { date: string; tests: number }
13 pets: number
14}
15
16/** A mood that lasts a few seconds (tests passed, a failure, a pat), then the pet goes back to its base mood. */
17export type PetFlash = { mood: string; say: string; until: number }
18
19declare module 'claude-code' {
20 interface PluginState {
21 'code-pet': {
22 profile: PetProfile | null
23 flash: PetFlash | null
24 /** Ticks every second: animation frame and idle checks. */
25 now: number
26 lastActivity: number
27 isWorking: boolean
28 /** What the pet says while Claude works: "reading app.js", "running npm test". */
29 doing: string
30 /** The mood that goes with it: reading, building, running, working. */
31 doingMood: string
32 failStreak: number
33 /** Context window and plan-limit fill, from the latest response. */
34 contextPercent: number
35 limitPercent: number
36 }
37 }
38}
39