SLOPSHOPPER

buddy

Un compagnon ASCII qui vit au-dessus du prompt et réagit à la session

newbandguardcommandpromptmodel
v0.1.0MITupdated 2026-10-07bharismendy/claude-buddy
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · buddy
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /buddy ⎿ buddy: /\_/\_ ⎿ buddy: ( o o )> ⎿ buddy: / vv \ ⎿ buddy: ~(____) ⎿ buddy: ⎿ buddy: Brioche — dragon rare ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │ Classique. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │ Classique. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯
README

buddy

Un compagnon ASCII pour Claude Code : il vit au-dessus du prompt, gigote, réagit à la session (erreurs, tours longs, modifications), te répond quand tu l'appelles par son nom, et peut être libéré pour faire éclore un nouvel œuf. 8 espèces, 5 raretés, 1 % de shiny.

Installation

Dans une session Claude Code (terminal) :

/plugin install buddy --marketplace bharismendy/claude-buddy

Réponds y pour ajouter le marketplace, puis choisis la portée (user par défaut).

Commandes

/buddy                  sa carte et ses stats
/buddy <message>        lui parler (ou écris son nom dans un prompt)
/buddy pet              le caresser (ou ♥ / ctrl+x tab puis p)
/buddy mute | unmute    couper / rendre ses bulles spontanées
/buddy hide | show      le cacher / le faire revenir
/buddy rename <nom>     le renommer
/buddy release <nom>    le libérer pour de bon (laisse un œuf)
/buddy hatch            faire éclore l'œuf
/buddy dex              les espèces possibles et les raretés
/buddy help             l'aide

Les réponses quand on lui parle passent par Haiku ; ses autres réactions sont des phrases toutes faites.

Documentation

Licence

MIT

Source 2 files
hooks/register.tsx 473 lines
1import { atom, read, update } from 'claude-code'
2import type { Color, EngineInterface, Register } from 'claude-code'
3
4import type { Pet, Rarity, Released, StatName } from '../types'
5
6const pet = atom({ plugin: 'buddy', key: 'pet' } as const, null)
7const tick = atom({ plugin: 'buddy', key: 'tick' } as const, 0)
8const bubble = atom({ plugin: 'buddy', key: 'bubble' } as const, null)
9const heartsUntil = atom({ plugin: 'buddy', key: 'heartsUntil' } as const, 0)
10const isHidden = atom({ plugin: 'buddy', key: 'isHidden' } as const, false)
11const isEgg = atom({ plugin: 'buddy', key: 'isEgg' } as const, false)
12const hatchingUntil = atom({ plugin: 'buddy', key: 'hatchingUntil' } as const, 0)
13const isMuted = atom({ plugin: 'buddy', key: 'isMuted' } as const, false)
14// Message à qui il doit répondre ; traité par la boucle du ticker, qui survit aux dispatchs.
15const ask = atom({ plugin: 'buddy', key: 'ask' } as const, null)
16
17const TICK_MS = 500
18const BUBBLE_TICKS = 14
19const HEART_TICKS = 6
20const CHATTER_COOLDOWN_TICKS = 40
21const HATCH_TICKS = 8
22const IDLE_TICKS = 1200
23const SUBCOMMANDS = ['help', 'dex', 'card', 'pet', 'hide', 'show', 'mute', 'unmute', 'rename', 'release', 'hatch', 'talk']
24
25const HELP = [
26  '/buddy                  sa carte et ses stats',
27  '/buddy <message>        lui parler (ou écris son nom dans un prompt)',
28  '/buddy pet              le caresser (ou ♥ / ctrl+x tab puis p)',
29  '/buddy mute | unmute    couper / rendre ses bulles spontanées',
30  '/buddy hide | show      le cacher / le faire revenir',
31  '/buddy rename <nom>     le renommer',
32  '/buddy release <nom>    le libérer pour de bon (laisse un œuf)',
33  '/buddy hatch            faire éclore l’œuf',
34  '/buddy dex              les espèces possibles et les raretés',
35  '/buddy help             cette aide',
36].join('\n')
37
38const EGG = {
39  idle: ['   .--.   ', '  /    \  ', ' |      | ', '  \____/  '],
40  wobble: ['  .--.    ', ' /    \   ', '|      |  ', ' \____/   '],
41  // L'œuf se fissure sur HATCH_TICKS ticks, une frame par quart.
42  cracks: [
43    ['   .--.   ', '  /    \  ', ' |  /\  | ', '  \____/  '],
44    ['   .--.   ', '  / /  \  ', ' |  \/\ | ', '  \____/  '],
45    ['   .,-.   ', '  /\/\/\  ', ' | \/  \| ', '  \____/  '],
46    ['  _    _  ', '  /\/\/\  ', ' |      | ', '  \____/  '],
47  ],
48}
49
50// E = l'œil, remplacé au rendu ; deux frames par espèce, la seconde sert à gigoter.
51const SPECIES: Record<string, [string[], string[]]> = {
52  canard: [
53    ['   __    ', ' <(E )___', '  (  ._> ', '   `--´  '],
54    ['   __    ', ' <(E )___', '  (  ._> ', '   `--´~ '],
55  ],
56  chat: [
57    [' /\\_/\\  ', '( E.E ) ', ' > ^ <  ', ' (_ _)  '],
58    [' /\\_/\\  ', '( E.E ) ', ' > ^ <  ', ' (_ _)~ '],
59  ],
60  blob: [
61    ['  .--.   ', ' ( E E )  ', ' (  ‿  )  ', '  `--´   '],
62    [' .----.  ', '( E  E ) ', '(   ‿  ) ', ' `----´  '],
63  ],
64  hibou: [
65    ['  ,___,  ', '  {E,E}  ', '  /)__)  ', '  -"-"-  '],
66    ['  ,___,  ', '  {E.E}  ', '  /)__)  ', '  -"-"-  '],
67  ],
68  fantôme: [
69    ['  .-.   ', ' (E E)  ', ' | O |  ', " '~~~'  "],
70    ['  .-.   ', ' (E E)  ', ' | o |  ', " '~'~'  "],
71  ],
72  axolotl: [
73    ['}\\(E‿E)/{', '  (    )  ', '   U  U   ', '          '],
74    ['}/(E‿E)\\{', '  (    )  ', '   U  U   ', '          '],
75  ],
76  robot: [
77    ['  [###]  ', '  [E_E]  ', '  /|=|\\  ', '   d b   '],
78    ['  [#*#]  ', '  [E_E]  ', '  \\|=|/  ', '   b d   '],
79  ],
80  dragon: [
81    ['  /\\_/\\_  ', ' ( E  E )>', ' /  vv  \\ ', ' ~(____)  '],
82    ['  /\\_/\\_  ', ' ( E  E )>', ' /  ^^  \\ ', '  (____)~ '],
83  ],
84}
85
86const EYES = ['·', 'o', '°', '^', '✦', '@', '•']
87const NAMES = ['Pixel', 'Grumly', 'Biscotte', 'Nougat', 'Kernel', 'Mochi', 'Tofu', 'Segfault', 'Patate', 'Lambda', 'Brioche', 'Zéphyr']
88const STATS: StatName[] = ['DEBUG', 'PATIENCE', 'CHAOS', 'SAGESSE', 'SARCASME']
89const RARITIES: [Rarity, number, number][] = [
90  // [rareté, poids, plancher des stats]
91  ['commun', 60, 1],
92  ['peu commun', 25, 15],
93  ['rare', 10, 30],
94  ['épique', 4, 45],
95  ['légendaire', 1, 60],
96]
97const RARITY_COLOR: Record<Rarity, Color> = {
98  commun: 'inactive',
99  'peu commun': 'success',
100  rare: 'suggestion',
101  épique: 'permission',
102  légendaire: 'warning',
103}
104
105const QUIPS = {
106  submit: ['Allez, on s’y met !', 'Je t’écoute.', 'Hmm, intéressant…', 'C’est parti !'],
107  error: ['Aïe. Ça a planté.', 'Rouge partout…', 'On respire. On relit la stack.', 'Classique.'],
108  edit: ['Joli diff.', 'Ça compile ? 👀', 'Propre.', 'Hop, un fichier de plus.'],
109  done: ['Fini !', 'Et voilà.', 'Mission accomplie.', 'Pas mal du tout.'],
110  long: ['Ouf, c’était long.', 'J’ai failli m’endormir.', 'Ça valait le coup d’attendre ?'],
111  aborted: ['Interrompu…', 'On change de plan ?', 'Ok, ok, j’arrête.'],
112  called: ['Oui ?', 'Présent !', 'Tu m’as appelé ?', 'Je suis là !'],
113  pet: ['♥', 'Rrrr…', 'Encore !', 'Merci ♥'],
114  idle: ['Tu es toujours là ?', 'Pause café ?', 'Je m’ennuie un peu…', 'On reprend quand tu veux.'],
115  farewell: ['Adieu… 👋', 'Merci pour tout !', 'Je m’envole !', 'Prends soin du code.'],
116}
117
118const pick = <T,>(list: readonly T[]): T => list[Math.floor(Math.random() * list.length)] as T
119
120function hatch(now: number): Pet {
121  const roll = Math.random() * 100
122  let acc = 0
123  let rarity: Rarity = 'commun'
124  let floor = 1
125  for (const [name, weight, min] of RARITIES) {
126    acc += weight
127    if (roll < acc) {
128      rarity = name
129      floor = min
130      break
131    }
132  }
133  const stats = Object.fromEntries(
134    STATS.map(stat => [stat, floor + Math.floor(Math.random() * (101 - floor))]),
135  ) as Record<StatName, number>
136
137  return {
138    species: pick(Object.keys(SPECIES)),
139    name: pick(NAMES),
140    rarity,
141    eye: pick(EYES),
142    isShiny: Math.random() < 0.01,
143    stats,
144    hatchedAt: now,
145  }
146}
147
148// Les espèces en grille de 4, celles déjà rencontrées cochées, puis les chances par rareté.
149function dex(seen: Set<string>): string {
150  const COL = 14
151  const names = Object.keys(SPECIES)
152  const rows: string[] = []
153  for (let i = 0; i < names.length; i += 4) {
154    const group = names.slice(i, i + 4)
155    const sprites = group.map(name => SPECIES[name]![0].map(line => line.replaceAll('E', 'o')))
156    for (let l = 0; l < 4; l++) rows.push(sprites.map(sp => (sp[l] ?? '').padEnd(COL)).join('').trimEnd())
157    rows.push(group.map(name => `${seen.has(name) ? '✓' : '·'} ${name}`.padEnd(COL)).join('').trimEnd(), '')
158  }
159  const odds = RARITIES.map(([rarity, weight]) => `${rarity} ${weight} %`).join(' · ')
160
161  return [...rows, `Raretés : ${odds} · shiny 1 %`, `${seen.size}/${names.length} espèces rencontrées`].join('\n')
162}
163
164function memorial(released: Released[]): string[] {
165  if (released.length === 0) return []
166
167  return ['', 'Anciens compagnons :', ...released.slice(-5).map(r => `  ${r.name} — ${r.species} ${r.rarity}`)]
168}
169
170function card(p: Pet): string {
171  const bar = (n: number) => '█'.repeat(Math.round(n / 10)).padEnd(10, '░')
172  const sprite = SPECIES[p.species]?.[0] ?? []
173
174  return [
175    ...sprite.map(line => '    ' + line.replaceAll('E', p.eye)),
176    '',
177    `${p.isShiny ? '✨ ' : ''}${p.name} — ${p.species} ${p.rarity}`,
178    `Éclos le ${new Date(p.hatchedAt).toLocaleDateString('fr-FR')}`,
179    '',
180    ...STATS.map(stat => `${stat.padEnd(9)} ${bar(p.stats[stat])} ${p.stats[stat]}`),
181  ].join('\n')
182}
183
184let lastChatter = -CHATTER_COOLDOWN_TICKS
185let lastActivity = 0
186let isReplying = false
187
188// chatter : bavardage limité par le cooldown ; event : réaction à la session ; direct : réponse à une demande explicite.
189// Le mute coupe chatter et event, jamais direct.
190async function say($: EngineInterface, text: string, mode: 'chatter' | 'event' | 'direct' = 'event') {
191  const now = await read($, tick)
192  if (mode !== 'direct' && (await read($, isMuted))) return
193  if (mode === 'chatter' && now - lastChatter < CHATTER_COOLDOWN_TICKS) return
194  lastChatter = now
195  await update($, bubble, () => ({ text, until: now + BUBBLE_TICKS }))
196}
197
198function greeting(hour: number): string {
199  if (hour < 6) return 'Tu codes encore à cette heure ?'
200  if (hour < 12) return 'Bonjour ! ☀'
201  if (hour < 18) return 'Re ! On s’y remet ?'
202  return 'Bonsoir ! Session du soir ?'
203}
204
205async function reply($: EngineInterface, p: Pet, message: string): Promise<string> {
206  const traits = STATS.map(stat => `${stat} ${p.stats[stat]}/100`).join(', ')
207  // Le moteur rejette une requête qu'il refuse d'envoyer (modèle bloqué…) : il répond alors une phrase toute faite.
208  const r = await $.model.complete({
209    model: 'haiku',
210    system:
211      `Tu es ${p.name}, un petit ${p.species} en ASCII (${p.rarity}) qui vit au-dessus du prompt ` +
212      `du terminal de ton humain, un développeur. Traits : ${traits} ; plus un trait est haut, plus il colore ` +
213      `ta réponse. Réponds en français, en une seule phrase de 20 mots maximum, sans guillemets ni markdown.`,
214    prompt: message.slice(0, 1000),
215    maxTokens: 80,
216    timeoutMs: 8000,
217  }).catch(() => null)
218
219  return r?.isAnswered ? r.text.trim().split('\n')[0]!.slice(0, 160) : pick(QUIPS.called)
220}
221
222// Appelé par le ticker : répond au message en attente, un à la fois.
223async function answerPending($: EngineInterface) {
224  const message = await read($, ask)
225  const current = await read($, pet)
226  if (message === null || current === null || isReplying) return
227  isReplying = true
228  try {
229    await update($, ask, () => null)
230    if (await read($, isMuted)) return
231    await say($, '…', 'direct')
232    await say($, await reply($, current, message), 'direct')
233  } finally {
234    isReplying = false
235  }
236}
237
238async function onTick($: EngineInterface) {
239  if (await read($, isHidden)) return
240  const now = await read($, tick)
241  await update($, tick, n => n + 1)
242  if (now - lastActivity === IDLE_TICKS) await say($, pick(QUIPS.idle), 'chatter')
243  await answerPending($)
244}
245
246async function caress($: EngineInterface) {
247  const now = await read($, tick)
248  await update($, heartsUntil, () => now + HEART_TICKS)
249  await say($, pick(QUIPS.pet), 'direct')
250}
251
252export const register: Register = on => {
253  on('session.start', async ($, e, next) => {
254    // undefined : premier lancement, on fait éclore ; null : relâché, un œuf attend.
255    let stored = (await $.store.get('pet')) as Pet | null | undefined
256    if (stored === undefined) {
257      stored = hatch(await $.clock.now())
258      await $.store.set('pet', stored)
259    }
260    const hatched = stored
261    await update($, pet, () => hatched)
262    await update($, isEgg, () => hatched === null)
263    const muted = (await $.store.get('muted')) === true
264    await update($, isMuted, () => muted)
265    if (hatched !== null) await say($, greeting(new Date(await $.clock.now()).getHours()))
266    await $.command.register({
267      name: 'buddy',
268      description: 'Ton compagnon (/buddy help pour les commandes)',
269    })
270    $.clock.every(TICK_MS, () => void onTick($))
271
272    return next(e)
273  })
274
275  on('command.run', { command: 'buddy' }, async ($, e) => {
276    const words = e.args.trim().split(/\s+/).filter(Boolean)
277    // /buddy <texte libre> = lui parler.
278    const [sub = 'card', ...rest] = words[0] !== undefined && !SUBCOMMANDS.includes(words[0]) ? ['talk', ...words] : words
279    if (sub === 'help') return { text: HELP }
280    const released = ((await $.store.get('released')) ?? []) as Released[]
281    if (sub === 'dex') {
282      const owned = await read($, pet)
283      const seen = new Set([...released.map(r => r.species), ...(owned === null ? [] : [owned.species])])
284      return { text: dex(seen) }
285    }
286
287    if (sub === 'hatch') {
288      if (!(await read($, isEgg))) return { text: 'Il n’y a pas d’œuf à faire éclore. /buddy release d’abord.' }
289      const now = await read($, tick)
290      const fresh = hatch(await $.clock.now())
291      await $.store.set('pet', fresh)
292      await update($, isHidden, () => false)
293      await update($, hatchingUntil, () => now + HATCH_TICKS)
294      await update($, bubble, () => ({ text: 'Bonjour le monde !', until: now + HATCH_TICKS + BUBBLE_TICKS }))
295      await update($, pet, () => fresh)
296      await update($, isEgg, () => false)
297      return { text: `L’œuf éclot…\n\n${card(fresh)}` }
298    }
299
300    const current = await read($, pet)
301    if (current === null) {
302      const egg = 'Un œuf attend au-dessus du prompt. /buddy hatch pour le faire éclore.'
303      return { text: [egg, ...memorial(released)].join('\n') }
304    }
305
306    switch (sub) {
307      case 'pet':
308        await update($, isHidden, () => false)
309        await caress($)
310        return { text: `Tu caresses ${current.name}. ♥` }
311      case 'talk': {
312        const message = rest.join(' ')
313        if (message === '') return { text: 'Usage : /buddy <message>' }
314        const answer = await reply($, current, message)
315        await update($, isHidden, () => false)
316        await say($, answer, 'direct')
317        return { text: `${current.name} : ${answer}` }
318      }
319      case 'mute':
320        await update($, isMuted, () => true)
321        await $.store.set('muted', true)
322        return { text: `${current.name} se tait. /buddy unmute pour lui rendre la parole.` }
323      case 'unmute':
324        await update($, isMuted, () => false)
325        await $.store.set('muted', false)
326        await say($, 'Enfin ! J’avais des choses à dire.', 'direct')
327        return { text: `${current.name} peut de nouveau parler.` }
328      case 'hide':
329        await update($, isHidden, () => true)
330        return { text: `${current.name} fait une sieste. /buddy show pour le réveiller.` }
331      case 'show':
332        await update($, isHidden, () => false)
333        await say($, 'Coucou !', 'direct')
334        return { text: `${current.name} est de retour.` }
335      case 'rename': {
336        const name = rest.join(' ').slice(0, 20)
337        if (name === '') return { text: 'Usage : /buddy rename <nom>' }
338        const renamed = { ...current, name }
339        await update($, pet, () => renamed)
340        await $.store.set('pet', renamed)
341        await say($, `Je m’appelle ${name} maintenant !`, 'direct')
342        return { text: `Renommé en ${name}.` }
343      }
344      case 'release': {
345        if (rest.join(' ').toLowerCase() !== current.name.toLowerCase()) {
346          return { text: `${current.name} partira pour de bon. Tape /buddy release ${current.name} pour confirmer.` }
347        }
348        const gone: Released = {
349          name: current.name,
350          species: current.species,
351          rarity: current.rarity,
352          releasedAt: await $.clock.now(),
353        }
354        await $.store.set('released', [...released, gone].slice(-20))
355        await $.store.set('pet', null)
356        await update($, isHidden, () => false)
357        await update($, pet, () => null)
358        await update($, isEgg, () => true)
359        await say($, `${current.name} : ${pick(QUIPS.farewell)}`, 'direct')
360        return { text: `${current.name} s’est envolé. Un œuf est apparu : /buddy hatch pour le faire éclore.` }
361      }
362      default:
363        return { text: [card(current), ...memorial(released)].join('\n') }
364    }
365  })
366
367  on('prompt.submit', async ($, e, next) => {
368    lastActivity = await read($, tick)
369    const current = await read($, pet)
370    if (current !== null && e.text.toLowerCase().includes(current.name.toLowerCase())) {
371      await update($, ask, () => e.text)
372    } else if (Math.random() < 0.3) {
373      await say($, pick(QUIPS.submit), 'chatter')
374    }
375
376    return next(e)
377  })
378
379  on('tool.call', async ($, e, next) => {
380    const ran = await next(e)
381    if (e.agentId !== undefined) return ran
382
383    if ('isError' in ran && ran.isError === true) {
384      await say($, pick(QUIPS.error))
385    } else if ((e.tool === 'Edit' || e.tool === 'Write') && Math.random() < 0.25) {
386      await say($, pick(QUIPS.edit), 'chatter')
387    }
388
389    return ran
390  })
391
392  on('turn.complete', async ($, e, next) => {
393    if (e.agentId === undefined) {
394      lastActivity = await read($, tick)
395      if (e.isAborted) await say($, pick(QUIPS.aborted))
396      else if (e.durationMs > 120_000) await say($, pick(QUIPS.long))
397      else if (Math.random() < 0.3) await say($, pick(QUIPS.done), 'chatter')
398    }
399
400    return next(e)
401  })
402
403  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
404    const current = await read($, pet)
405    const egg = await read($, isEgg)
406    if (e.props.hasSurvey || (current === null && !egg) || (await read($, isHidden))) {
407      return next(e)
408    }
409
410    const { Box, Button, Text } = $.ui.resolve(e)
411    const now = await read($, tick)
412    const said = await read($, bubble)
413    const showBubble = said !== null && said.until > now && e.props.bodyColumns >= 40
414    const left = await read($, hatchingUntil)
415
416    if (current === null || left > now) {
417      const step = HATCH_TICKS - (left - now)
418      const sprite = current === null
419        ? (now % 6 === 5 ? EGG.wobble : EGG.idle)
420        : (EGG.cracks[Math.min(EGG.cracks.length - 1, Math.floor((step * EGG.cracks.length) / HATCH_TICKS))] ?? EGG.idle)
421
422      return (
423        <Box flexDirection="row" justifyContent="flex-end" alignItems="flex-end" width={e.props.bodyColumns}>
424          {showBubble && (
425            <Box borderStyle="round" borderColor="subtle" paddingX={1} marginRight={1} marginBottom={1}>
426              <Text wrap="wrap">{said.text}</Text>
427            </Box>
428          )}
429          <Box flexDirection="column" alignItems="center">
430            {sprite.map(line => (
431              <Text>{line}</Text>
432            ))}
433            <Text dimColor>{current === null ? '/buddy hatch' : 'crac…'}</Text>
434          </Box>
435        </Box>
436      )
437    }
438
439    const hearts = (await read($, heartsUntil)) > now
440    const frames = SPECIES[current.species] ?? SPECIES.blob!
441    // Gigote vite pendant un tour, lentement au repos ; cligne de temps en temps.
442    const isFidget = e.props.isWorking ? now % 2 === 1 : now % 8 === 7
443    const eye = now % 13 === 0 ? '-' : current.eye
444    const sprite = frames[isFidget ? 1 : 0].map(line => line.replaceAll('E', eye))
445    const color = RARITY_COLOR[current.rarity]
446    const muted = await read($, isMuted)
447
448    return (
449      <Box flexDirection="row" justifyContent="flex-end" alignItems="flex-end" width={e.props.bodyColumns}>
450        {showBubble && (
451          <Box borderStyle="round" borderColor={color} paddingX={1} marginRight={1} marginBottom={1}>
452            <Text wrap="wrap">{said.text}</Text>
453          </Box>
454        )}
455        <Box flexDirection="column" alignItems="center">
456          <Text color="error">{hearts ? (now % 2 ? ' ♥   ♥ ' : '   ♥   ') : ' '}</Text>
457          {sprite.map(line => (
458            <Text color={current.isShiny ? 'warning' : color}>{line}</Text>
459          ))}
460          <Box flexDirection="row">
461            <Text bold color={color}>
462              {current.isShiny ? '✨' : ''}
463              {current.name}{' '}
464            </Text>
465            {muted && <Text dimColor>🔇 </Text>}
466            <Button key="pet" label="♥" plain hotkey="p" onPress={() => caress($)} />
467          </Box>
468        </Box>
469      </Box>
470    )
471  })
472}
473
types/index.d.ts 33 lines
1export type Rarity = 'commun' | 'peu commun' | 'rare' | 'épique' | 'légendaire'
2export type StatName = 'DEBUG' | 'PATIENCE' | 'CHAOS' | 'SAGESSE' | 'SARCASME'
3
4export type Pet = {
5  species: string
6  name: string
7  rarity: Rarity
8  eye: string
9  isShiny: boolean
10  stats: Record<StatName, number>
11  hatchedAt: number
12}
13
14export type Released = { name: string; species: string; rarity: Rarity; releasedAt: number }
15
16export type Bubble = { text: string; until: number }
17
18declare module 'claude-code' {
19  interface PluginState {
20    buddy: {
21      pet: Pet | null
22      tick: number
23      bubble: Bubble | null
24      heartsUntil: number
25      isHidden: boolean
26      isEgg: boolean
27      hatchingUntil: number
28      isMuted: boolean
29      ask: string | null
30    }
31  }
32}
33