SLOPSHOPPER

clawd-pet

Clawd, the Claude pet, sits right above your prompt, on the right, with its laptop: it types while Claude works and falls asleep (Zzz) when you are idle.

newbandspinnerguardcommandprompt
★ 1v1.10.0no licenseupdated 2026-10-08Menendez2004/menendez-market/plugins/mods/clawd-pet
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · clawd-pet
› 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 › /pet ⎿ clawd-pet: Clawd went home. Run /pet to bring it back. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

menendez-market

Marketplace de plugins para Claude Code.

Instalación

# 1. Agregar el marketplace
/plugin marketplace add Menendez2004/menendez-market

# 2. Instalar los plugins que quieras
/plugin install dev-orchestrator@menendez-market
/plugin install qa@menendez-market
/plugin install unit-test-generator@menendez-market
/plugin install clawd-pet@menendez-market

(Desde la terminal también funciona: claude plugin marketplace add Menendez2004/menendez-market.)

Plugins

PluginDescripción
dev-orchestratorHuman-in-the-loop development orchestrator with a 2-level hierarchy: adopts the Lead's plan (or drafts one in plan mode under opusplan if none is given), launches one Task Agent per step in its own terminal (or inline for small steps) and its own git worktree, applying each finished step to the Lead's working tree as a checked, uncommitted patch, starting each step as soon as its dependencies finish, and each agent can use read-only research sub-agents (Sonnet 4.6) that prioritize graphify. Execution runs on the latest Sonnet. Never runs tests; runs checks once at the end. Escalates to the Lead on ambiguity or destructive actions.
qaSuite de QA (se instala completa). Incluye las skills: qa-orchestrator (orquesta sesiones de QA: lanza agentes de prueba en paralelo, recolecta resultados, hace triage de bugs y genera reportes), qa-happy-path (prueba los flujos principales de la UI con Playwright), qa-api-adversary (intenta romper la API: auth, validación, duplicados, condiciones de carrera, requests malformados), qa-debugger (aplica correcciones mínimas a los bugs reportados) y qa-personality-builder (crea personalidades de QA propias del proyecto en .qa/config.yml).
clawd-petMod (no es una skill): Clawd, la mascota de Claude, vive justo encima del prompt del CLI, a la derecha, con su laptop. Saluda cuando estás presente, teclea en su laptop mientras Claude trabaja, se pone casco en modo plan, queda noqueado cuando falla un comando y se duerme con un "Zzz" flotando cuando dejas de escribir (60 s por defecto; cámbialo con idleSeconds en /config). Escribe cualquier cosa para despertarlo; /pet lo oculta o lo muestra, y /pet color <nombre o #hex> (o /config) le cambia el color: orange, blue, green, purple, pink, red, yellow, gray o cualquier hexadecimal.
unit-test-generatorGenera unit tests estrictos y aislados con el runner, la librería de mocks y las factories que ya usa el repo. Los datos salen de Faker o de factories (nunca objetos escritos a mano), con semilla fija, patrón AAA y casos de éxito, ramas, entradas inválidas, límites, errores y efectos secundarios. Exige al menos 90% de coverage (líneas, sentencias, ramas y funciones) en la unidad probada. Nunca modifica código de producción: reporta los bugs que encuentra.

Estructura

.claude-plugin/
  marketplace.json          # catálogo: cada plugin apunta a sus skills
plugins/
  skills/
    dev-orchestrator/       # SKILL.md + references/, rules/
    qa/                     # todas las skills de QA
      qa-orchestrator/        # SKILL.md + references/, rules/, assets/
      qa-happy-path/          # SKILL.md + rules/
      qa-api-adversary/       # SKILL.md + rules/
      qa-debugger/            # SKILL.md + rules/
      qa-personality-builder/ # SKILL.md + references/, rules/
    unit-test-generator/    # SKILL.md + references/, rules/
  mods/
    clawd-pet/              # .claude-plugin/plugin.json + hooks/register.tsx, types/, tests/

Cada entrada de marketplace.json usa "strict": false y declara sus skills con "skills": [...]. dev-orchestrator carga solo su skill (y su agente orch-researcher), unit-test-generator carga solo la suya, y el plugin qa carga las cinco skills de plugins/skills/qa/ juntas, porque qa-orchestrator lanza a las demás.

Para agregar una skill nueva: crea plugins/skills/<nombre>/SKILL.md y agrega una entrada en .claude-plugin/marketplace.json. Una skill de QA nueva va en plugins/skills/qa/<nombre>/ y se agrega a la lista skills del plugin qa. Valida con:

claude plugin validate .

Los mods (plugins de function hooks, como clawd-pet) viven en plugins/mods/<nombre>/ con su propio .claude-plugin/plugin.json, y su entrada en marketplace.json usa "source": "./plugins/mods/<nombre>". Cada cambio a un mod debe subir su version (en su plugin.json y en su entrada de marketplace.json): Claude Code guarda la copia instalada por versión, y si la versión no cambia /plugin update no la reemplaza. Para probarlos:

claude plugin validate plugins/mods/clawd-pet
claude plugin test plugins/mods/clawd-pet
claude --plugin-dir plugins/mods/clawd-pet   # sesión de prueba con el mod cargado
Source 2 files
hooks/register.tsx 616 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { PetMood } from '../types'
5
6const mood = atom({ plugin: 'clawd-pet', key: 'mood' } as const, 'awake')
7const frame = atom({ plugin: 'clawd-pet', key: 'frame' } as const, 0)
8const isHidden = atom({ plugin: 'clawd-pet', key: 'isHidden' } as const, false)
9const laptopStep = atom({ plugin: 'clawd-pet', key: 'laptopStep' } as const, 0)
10const isPlanMode = atom({ plugin: 'clawd-pet', key: 'isPlanMode' } as const, false)
11
12// How long Clawd stays knocked out after a command fails.
13const ERROR_MS = 3000
14
15// A quarter second a frame, so the typing looks quick; the laptop moves on
16// every other frame, so it still takes about two seconds to come out.
17const TICK_MS = 250
18
19// Where the laptop is, one step every other tick toward OPEN while Claude
20// works and back toward STOWED once it stops.
21const STOWED = 0 // Clawd faces you, the laptop behind its back, one corner peeking out
22const TURNED = 1 // Clawd turns sideways, facing left, and grabs the laptop
23const LIFTED = 2 // pulled out, held up closed in front of it
24const SET_DOWN = 3 // set down closed on the ground in front of it
25const OPEN = 4 // opened sideways, the screen tilted back; Clawd types
26
27// Fixed colors, so Clawd stays vivid whatever the terminal theme.
28const DEFAULT_COLOR = 'orange'
29const EYE_BLACK = '#000000'
30const LAPTOP_GREY = '#A8A8A8'
31const LAPTOP_DIM = '#5C5C5C'
32const SPARK_YELLOW = '#FFD166'
33const Z_FADED = '#7A5A4A'
34const SCREEN_GLOW = '#CFE3F7'
35const SCREEN_DIM = '#8FB4DA'
36const HAT_YELLOW = '#E8B04B'
37const HAT_BADGE = '#B07A1E'
38
39// The colors Clawd can be, by the name the person picks in /config or types
40// after `/pet color`; any #RRGGBB (or #RGB) works too.
41const PALETTE: Record<string, string> = {
42  orange: '#E8713A',
43  blue: '#4A90E2',
44  green: '#4CAF50',
45  purple: '#9B59B6',
46  pink: '#E86A9B',
47  red: '#E74C3C',
48  yellow: '#F1C40F',
49  gray: '#95A5A6',
50}
51
52// The hex a color name or hex code stands for, as #RRGGBB, or undefined when
53// it is neither a palette name nor a well-formed hex code.
54function toHex(color: string): string | undefined {
55  const value = color.trim().toLowerCase()
56  const named = PALETTE[value]
57  if (named !== undefined) {
58    return named
59  }
60  const short = /^#?([0-9a-f])([0-9a-f])([0-9a-f])$/.exec(value)
61  if (short) {
62    return `#${short[1]}${short[1]}${short[2]}${short[2]}${short[3]}${short[3]}`
63  }
64  const long = /^#?([0-9a-f]{6})$/.exec(value)
65  return long ? `#${long[1]}` : undefined
66}
67
68// The same color at 60% brightness: shut eyes are drawn in it, so they read
69// as a darker line on any body color.
70function darken(hex: string): string {
71  const n = Number.parseInt(hex.slice(1), 16)
72  const channel = (shift: number) => Math.round(((n >> shift) & 0xff) * 0.6)
73  return `#${[16, 8, 0].map(shift => channel(shift).toString(16).padStart(2, '0')).join('')}`
74}
75
76// Clawd's body color and its darker shade, set from the person's choice when
77// the module loads (and again when they change it with /pet color).
78let BODY = PALETTE[DEFAULT_COLOR] ?? '#E8713A'
79let BODY_DARK = darken(BODY)
80
81function paint(hex: string) {
82  BODY = hex
83  BODY_DARK = darken(hex)
84}
85
86// One run of characters in a single style.
87type Seg = { text: string; color?: string; backgroundColor?: string; bold?: boolean }
88
89// Clawd as a 9×5 pixel sprite, two pixel rows per terminal row (half
90// blocks), so it is two and a half rows tall, close to the sticker's shape;
91// the head starts halfway down the top row:
92//
93//   . # # # # # # # .     row 0  head
94//   # # E # # # E # #     row 1  arms and eyes
95//   # # # # # # # # #     row 2  arms
96//   . # # # # # # # .     row 3  body
97//   . # . # . # . # .     row 4  legs
98//
99// An eye is one pixel: black when open (`▀`, black over orange), a dark
100// orange line when it blinks or while asleep. Facing you, Clawd only idles
101// and sleeps; asleep it lies still and only the Zs above it move. To work it
102// turns sideways (below).
103type Eyes = 'open' | 'blink' | 'asleep'
104
105function petRows(eyes: Eyes): Seg[][] {
106  const body = (text: string): Seg => ({ text, color: BODY, bold: true })
107  const eye: Seg =
108    eyes === 'open'
109      ? { text: '▀', color: EYE_BLACK, backgroundColor: BODY }
110      : { text: '▀', color: BODY_DARK, backgroundColor: BODY }
111
112  return [
113    [body(' ▄▄▄▄▄▄▄ ')],
114    [body('██'), eye, body('███'), eye, body('██')],
115    [body(' █▀█▀█▀█ ')],
116  ]
117}
118
119// Clawd in profile, facing left, as it works: the same 9×5 body with both
120// eyes toward the front (left) and one arm, the front one, reaching out to
121// the keyboard; the back arm is hidden behind its body.
122//
123//   . # # # # # # # .     row 0  head
124//   . # E # # E # # .     row 1  eyes
125//   A # # # # # # # .     row 2  front arm (at rest)
126//   a # # # # # # # .     row 3  front arm (on the keys)
127//   . # . # . # . # .     row 4  legs
128//
129// Typing, the front arm drops one pixel onto the keys and back up.
130function sidePetRows(isArmDown: boolean): Seg[][] {
131  const body = (text: string): Seg => ({ text, color: BODY, bold: true })
132  const eye: Seg = { text: '▀', color: EYE_BLACK, backgroundColor: BODY }
133
134  return [
135    [body(' ▄▄▄▄▄▄▄ ')],
136    [body(`${isArmDown ? ' ' : '▄'}█`), eye, body('██'), eye, body('██ ')],
137    [body(`${isArmDown ? '▀' : ' '}█▀█▀█▀█ `)],
138  ]
139}
140
141// The corner of the laptop peeking out from behind Clawd's back, one column
142// right of its head: dim while stowed, bright while Clawd turns and grabs it,
143// gone once out.
144function behindRows(step: number): Seg[][] {
145  const none: Seg[] = [{ text: ' ' }]
146  if (step === STOWED) {
147    return [[{ text: '▐', color: LAPTOP_DIM }], none, none]
148  }
149  if (step === TURNED) {
150    return [[{ text: '█', color: LAPTOP_GREY }], none, none]
151  }
152  return [none, none, none]
153}
154
155// The laptop, seen from the side like Clawd, five columns wide at every step
156// so Clawd never shifts: held up closed at arm height, set down closed, then
157// opened with the screen tilted back toward the left and the keyboard
158// running up to Clawd's front arm. While Clawd types the screen flickers.
159function laptopRows(step: number, isWorking: boolean, tick: number): Seg[][] {
160  const empty: Seg[] = [{ text: '     ' }]
161  const grey = (text: string): Seg => ({ text, color: LAPTOP_GREY, bold: true })
162
163  if (step === LIFTED) {
164    return [empty, [{ text: '  ' }, grey('▄▄▄')], empty]
165  }
166  if (step === SET_DOWN) {
167    return [empty, empty, [{ text: '  ' }, grey('▀▀▀')]]
168  }
169  if (step === OPEN) {
170    const screen: Seg = {
171      text: '╲',
172      color: isWorking && tick % 2 === 1 ? SCREEN_DIM : SCREEN_GLOW,
173      bold: true,
174    }
175    return [
176      [screen, { text: '    ' }],
177      [{ text: ' ' }, screen, { text: '   ' }],
178      [{ text: '  ' }, grey('▀▀▀')],
179    ]
180  }
181  return [empty, empty, empty]
182}
183
184// The hard hat Clawd wears while Claude is in plan mode: a yellow dome with
185// a darker badge in the row above its head, and the brim painted over the
186// empty top half of its head row, so it sits right on top of the head.
187function hatRow(): Seg[] {
188  const hat = (text: string, color = HAT_YELLOW): Seg => ({ text, color, bold: true })
189  return [{ text: '  ' }, hat('▄▄'), hat('▄', HAT_BADGE), hat('▄▄'), { text: '  ' }]
190}
191
192function brimRow(): Seg[] {
193  return [
194    { text: '▀', color: HAT_YELLOW, bold: true },
195    { text: '▀▀▀▀▀▀▀', color: HAT_YELLOW, backgroundColor: BODY, bold: true },
196    { text: '▀', color: HAT_YELLOW, bold: true },
197  ]
198}
199
200// Planning, Clawd faces you under its hard hat and marches in place: its
201// legs step between two stances every half second.
202function marchingLegs(tick: number): Seg[] {
203  const legs = Math.floor(tick / 2) % 2 === 0 ? ' █▀█▀█▀█ ' : ' ▀█▀█▀█▀ '
204  return [{ text: legs, color: BODY, bold: true }]
205}
206
207// Knocked out after a command fails: Clawd lies flat on its back, its legs
208// in the air, its eyes crossed out (×), with stars spinning over it and the
209// laptop tumbling off behind it.
210//
211//   . . . . . . . . .     legs up
212//   . # . # . # . # .
213//   # # X # # # X # #     flat body
214//   # # # # # # # # #
215function knockedOutRows(): Seg[][] {
216  const body = (text: string): Seg => ({ text, color: BODY, bold: true })
217  const eye: Seg = { text: '×', color: EYE_BLACK, backgroundColor: BODY, bold: true }
218  return [
219    [body(' ▄ ▄ ▄ ▄ ')],
220    [body('██'), eye, body('███'), eye, body('██')],
221  ]
222}
223
224const STARS = ['  ✦   ✧  ', '   ✧ ✦   ', '  ✧   ✦  ', '   ✦ ✧   ']
225
226function starRow(tick: number): Seg[] {
227  const stars = STARS[Math.floor(tick / 2) % STARS.length] ?? ''
228  return [...stars].map(ch => (ch === ' ' ? { text: ch } : { text: ch, color: SPARK_YELLOW, bold: true }))
229}
230
231// The sparks that pop out of the laptop while Clawd types, one frame per
232// tick, in the row above the laptop and as wide as it.
233const SPARKS = ['  ✻  ', ' · ✻ ', '✻  · ', ' ✻  ·']
234
235function sparkRow(tick: number): Seg[] {
236  const sparks = SPARKS[tick % SPARKS.length] ?? ''
237  return [...sparks].map(ch =>
238    ch === '✻' ? { text: ch, color: BODY, bold: true } : ch === '·' ? { text: ch, color: SPARK_YELLOW, bold: true } : { text: ch },
239  )
240}
241
242// The Zs that float up out of Clawd's head while it sleeps, in the row above
243// it: each is born small over the top of its head and drifts right one column
244// every half second, growing from `·` to `z` to `Z` and fading on its last
245// step. Two are in the air at once, three steps apart.
246const Z_SHAPES = ['·', 'z', 'z', 'Z', 'Z']
247const Z_CYCLE = 6
248
249function sleepRow(tick: number): Seg[] {
250  const cells: Seg[] = Array.from({ length: 9 }, () => ({ text: ' ' }))
251  const beat = Math.floor(tick / 2)
252  for (const offset of [0, 3]) {
253    const age = (beat + offset) % Z_CYCLE
254    const shape = Z_SHAPES[age]
255    if (shape !== undefined) {
256      cells[4 + age] = { text: shape, color: age === Z_SHAPES.length - 1 ? Z_FADED : BODY, bold: age >= 3 }
257    }
258  }
259  return cells
260}
261
262// Writes the mood only when it changed, so an idle tick redraws nothing new.
263async function setMood($: EngineInterface, next: PetMood) {
264  if ((await read($, mood)) !== next) {
265    await update($, mood, () => next)
266  }
267}
268
269// What moodNow needs to know beyond the plugin's state.
270type Clock = {
271  lastActivityAt: number
272  isTurnRunning: boolean
273  errorUntil: number
274  idleMs: number
275  // How many Plan subagents are running: Claude planning without plan mode.
276  planAgents: number
277  // What the footer under the prompt last said about plan mode, and what of
278  // it the plugin has already recorded (see the PromptHint hook).
279  footerSaysPlan: boolean
280  footerEverSaidPlan: boolean
281  footerApplied: boolean | undefined
282}
283
284// Clawd's mood right now: knocked out for a few seconds after a failed
285// command, planning or working while a turn runs, asleep once the person has
286// been idle long enough, awake otherwise.
287async function moodNow($: EngineInterface, c: Clock): Promise<PetMood> {
288  const now = await $.clock.now()
289  if (now < c.errorUntil) {
290    return 'error'
291  }
292  if (c.isTurnRunning) {
293    return c.planAgents > 0 || (await read($, isPlanMode)) ? 'planning' : 'working'
294  }
295  return now - c.lastActivityAt >= c.idleMs ? 'sleeping' : 'awake'
296}
297
298// Records whether plan mode is on. The engine's plan-mode notes switch it
299// both ways; the settings-hook events' `permission_mode` only ever switches it
300// on, so a field they leave stale cannot take the hat off mid-plan.
301async function notePermissionMode($: EngineInterface, permissionMode: string | undefined) {
302  if (permissionMode !== undefined && (await read($, isPlanMode)) !== (permissionMode === 'plan')) {
303    await update($, isPlanMode, () => permissionMode === 'plan')
304  }
305}
306
307export const register: Register = (on, options) => {
308  const idleMs = Math.max(5, Number(options.idleSeconds ?? 60)) * 1000
309  // A custom hex in /config wins over the palette pick; a malformed one is
310  // ignored and the pick is used.
311  paint(toHex(String(options.customColor ?? '')) ?? toHex(String(options.color ?? DEFAULT_COLOR)) ?? BODY)
312  const c: Clock = {
313    lastActivityAt: 0,
314    isTurnRunning: false,
315    errorUntil: 0,
316    idleMs,
317    planAgents: 0,
318    footerSaysPlan: false,
319    footerEverSaidPlan: false,
320    footerApplied: undefined,
321  }
322
323  on('session.start', async ($, e, next) => {
324    c.lastActivityAt = await $.clock.now()
325    await update($, mood, () => 'awake')
326
327    $.clock.every(TICK_MS, async () => {
328      // Shift+Tab: the footer is the first to show the mode changed. Its word
329      // is recorded once each time it changes; it only switches plan mode off
330      // once it has been seen saying it is on, so a footer that never shows
331      // the mode leaves the other signals in charge.
332      if (c.footerApplied !== c.footerSaysPlan && (c.footerSaysPlan || c.footerEverSaidPlan)) {
333        await notePermissionMode($, c.footerSaysPlan ? 'plan' : 'default')
334        c.footerApplied = c.footerSaysPlan
335      }
336      const current = await moodNow($, c)
337      await setMood($, current)
338      if ((await read($, frame)) % 2 === 0) {
339        // Working, the laptop comes out; knocked out, it stays where it was;
340        // otherwise (planning included) it goes back behind Clawd's back.
341        await update($, laptopStep, step =>
342          current === 'working'
343            ? Math.min(OPEN, (step ?? STOWED) + 1)
344            : current === 'error'
345              ? (step ?? STOWED)
346              : Math.max(STOWED, (step ?? STOWED) - 1),
347        )
348      }
349      await update($, frame, n => ((n ?? 0) + 1) % 1000)
350    })
351
352    // The pet keeps living even if the command cannot be registered.
353    try {
354      await $.command.register({
355        name: 'pet',
356        description: 'Show or hide Clawd, the Claude pet above the prompt; /pet color changes its color.',
357        argumentHint: '[color <name|#hex>]',
358      })
359    } catch {}
360
361    return next(e)
362  })
363
364  // `/pet` shows or hides Clawd; `/pet color` lists the colors; `/pet color
365  // <name|#hex>` repaints it now and saves the choice in the plugin's /config
366  // options (a name in "color", a hex in "customColor"), so it lasts.
367  on('command.run', { command: 'pet' }, async ($, e) => {
368    const [sub, ...rest] = e.args.trim().split(/\s+/)
369    if (sub !== 'color') {
370      const hidden = await update($, isHidden, h => !h)
371      return { text: hidden ? 'Clawd went home. Run /pet to bring it back.' : 'Clawd is back!' }
372    }
373
374    const names = Object.keys(PALETTE).join(', ')
375    const wanted = rest.join(' ')
376    if (wanted === '') {
377      return { text: `Clawd's colors: ${names}, or any hex like #00BCD4. Now: ${BODY}.` }
378    }
379
380    const hex = toHex(wanted)
381    if (hex === undefined) {
382      return { text: `"${wanted}" is not a color I know. Use one of: ${names}, or a hex like #00BCD4.` }
383    }
384
385    paint(hex)
386    const name = wanted.trim().toLowerCase()
387    const saves =
388      PALETTE[name] !== undefined
389        ? [
390            { key: 'clawd-pet.color', value: name },
391            { key: 'clawd-pet.customColor', value: '' },
392          ]
393        : [{ key: 'clawd-pet.customColor', value: hex }]
394    for (const save of saves) {
395      const { deny } = await $.config.set(save)
396      if (deny !== undefined) {
397        return { text: `Clawd is ${hex} for now, but the color could not be saved: ${deny}` }
398      }
399    }
400    return { text: `Clawd is now ${PALETTE[name] !== undefined ? name : hex}.` }
401  })
402
403  // Anything the person does in the prompt box wakes Clawd up.
404  on('prompt.edit', async ($, e, next) => {
405    c.lastActivityAt = await $.clock.now()
406    await setMood($, await moodNow($, c))
407    return next(e)
408  })
409
410  on('prompt.submit', async ($, e, next) => {
411    c.isTurnRunning = true
412    c.lastActivityAt = await $.clock.now()
413    await setMood($, await moodNow($, c))
414    return next(e)
415  })
416
417  on('turn.start', async ($, e, next) => {
418    c.isTurnRunning = true
419    c.lastActivityAt = await $.clock.now()
420    await setMood($, await moodNow($, c))
421    return next(e)
422  })
423
424  on('turn.complete', async ($, e, next) => {
425    c.isTurnRunning = false
426    c.lastActivityAt = await $.clock.now()
427    await setMood($, await moodNow($, c))
428    return next(e)
429  })
430
431  on('classic.UserPromptSubmit', async ($, e, next) => {
432    if (e.permission_mode === 'plan') {
433      await notePermissionMode($, 'plan')
434    }
435    return next(e)
436  })
437
438  on('classic.PostToolUse', async ($, e, next) => {
439    if (e.permission_mode === 'plan') {
440      await notePermissionMode($, 'plan')
441    }
442    return next(e)
443  })
444
445  // The engine's own record of plan mode: while it is on, every request of
446  // the main conversation carries a `plan_mode` reminder (and a
447  // `plan_mode_reentry` note when Claude goes back into it); when it ends, a
448  // `plan_mode_exit` note is made once. A subagent's rows are left out: only
449  // the main conversation's mode puts the hat on.
450  on('prompt.attachment', { type: 'plan_mode' }, async ($, e, next) => {
451    if (e.agentId === undefined) {
452      await notePermissionMode($, 'plan')
453    }
454    return next(e)
455  })
456
457  on('prompt.attachment', { type: 'plan_mode_reentry' }, async ($, e, next) => {
458    if (e.agentId === undefined) {
459      await notePermissionMode($, 'plan')
460    }
461    return next(e)
462  })
463
464  on('prompt.attachment', { type: 'plan_mode_exit' }, async ($, e, next) => {
465    if (e.agentId === undefined) {
466      await notePermissionMode($, 'default')
467    }
468    return next(e)
469  })
470
471  // Entering plan mode puts the hat on; a plan accepted takes it off.
472  on('tool.call', { tool: 'EnterPlanMode' }, async ($, e, next) => {
473    const result = await next(e)
474    if (!result.deny && !result.isError) {
475      await update($, isPlanMode, () => true)
476    }
477    return result
478  })
479
480  on('tool.call', { tool: 'ExitPlanMode' }, async ($, e, next) => {
481    const result = await next(e)
482    if (!result.deny && !result.isError) {
483      await update($, isPlanMode, () => false)
484    }
485    return result
486  })
487
488  // Asked to plan, Claude may hand the work to its Plan subagent without
489  // entering plan mode: Clawd plans (hard hat on) while that agent runs.
490  on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
491    if (e.subagent_type !== 'Plan') {
492      return next(e)
493    }
494    c.planAgents += 1
495    try {
496      return await next(e)
497    } finally {
498      c.planAgents -= 1
499    }
500  })
501
502  // The footer under the prompt says "plan mode on" while plan mode is on, the
503  // moment Shift+Tab switches it. A drawing never writes state, so this only
504  // notes what the line says; the timer records it.
505  on('ui.render', { component: 'PromptHint' }, ($, e, next) => {
506    c.footerSaysPlan = /plan mode on/i.test(e.props.hint)
507    c.footerEverSaidPlan = c.footerEverSaidPlan || c.footerSaysPlan
508    return next(e)
509  })
510
511  // A failing command (a build, a test run, a script) knocks Clawd out for a
512  // few seconds; an interrupt does not count.
513  on('classic.PostToolUseFailure', async ($, e, next) => {
514    if (e.permission_mode === 'plan') {
515      await notePermissionMode($, 'plan')
516    }
517    if (e.tool_name === 'Bash' && !e.is_interrupt) {
518      c.errorUntil = (await $.clock.now()) + ERROR_MS
519      await setMood($, 'error')
520    }
521    return next(e)
522  })
523
524  // Clawd sits in the band right above the prompt, against its right edge,
525  // on rows of its own so nothing squeezes it: four where the band has them
526  // (the top one for the sparks and the floating Zs), three otherwise. The
527  // row count never changes with the mood, so the prompt never jumps.
528  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
529    if (e.props.hasSurvey || e.props.maxRows < 3 || (await read($, isHidden))) {
530      return next(e)
531    }
532
533    const current = await read($, mood)
534    const tick = await read($, frame)
535    const step = await read($, laptopStep)
536    const { Box, Text } = $.ui.resolve(e)
537
538    const isWorking = current === 'working'
539    const isTyping = isWorking && step === OPEN
540    const isAsleep = current === 'sleeping'
541    const isPlanning = current === 'planning'
542    const isKnockedOut = current === 'error'
543    const wearsHat = !isAsleep && !isKnockedOut && (isPlanning || (await read($, isPlanMode)))
544    const eyes: Eyes = isAsleep ? 'asleep' : current === 'awake' && tick % 28 === 27 ? 'blink' : 'open'
545    // From the moment it turns to grab the laptop until it has put it back,
546    // Clawd is in profile; otherwise it faces you.
547    const isSideways = step > STOWED && !isKnockedOut
548    const bubble =
549      isKnockedOut
550        ? ['', '', '', 'oops!']
551        : isPlanning
552          ? ['', '', '', `planning${'.'.repeat((Math.floor(tick / 2) % 3) + 1)}`]
553          : isAsleep
554        ? ['', '', '', 'Zzz…']
555        : isTyping
556          ? ['', '', '', '']
557          : isWorking
558            ? ['', '', '', step <= TURNED ? 'hmm…' : 'got it!']
559            : step > STOWED
560              ? ['', '', '', 'done!']
561              : ['', '', '', 'hi!']
562    const blank: Seg[] = [{ text: ' ' }]
563    const laptop = [isTyping ? sparkRow(tick) : [{ text: '     ' }], ...laptopRows(step, isWorking, tick)]
564    const front = petRows(eyes)
565    const body: Seg[][] = isKnockedOut
566      ? [[{ text: '         ' }], ...knockedOutRows()]
567      : isSideways
568        ? sidePetRows(isTyping && tick % 2 === 1)
569        : isPlanning
570          ? [front[0] ?? blank, front[1] ?? blank, marchingLegs(tick)]
571          : front
572    const head = body[0] ?? blank
573    const pet = [
574      isKnockedOut ? starRow(tick) : isAsleep ? sleepRow(tick) : wearsHat ? hatRow() : blank,
575      wearsHat ? brimRow() : head,
576      ...body.slice(1),
577    ]
578    const tumblingLaptop: Seg[][] = [
579      [{ text: ' ' }],
580      [{ text: '▞', color: LAPTOP_GREY, bold: true }],
581      [{ text: '▀', color: LAPTOP_DIM }],
582    ]
583    const behind = [blank, ...(isKnockedOut ? tumblingLaptop : behindRows(step))]
584    const rows = e.props.maxRows >= 4 ? 4 : 3
585    const fit = <T,>(column: T[]): T[] => column.slice(column.length - rows)
586
587    const drawRow = (segs: Seg[], key: string) => (
588      <Text key={key}>
589        {segs.map((seg, i) => (
590          <Text key={`${key}-${i}`} color={seg.color} backgroundColor={seg.backgroundColor} bold={seg.bold}>
591            {seg.text}
592          </Text>
593        ))}
594      </Text>
595    )
596
597    // As wide as the band, everything pushed to its right end.
598    return (
599      <Box flexDirection="row" alignItems="flex-end" justifyContent="flex-end" width={e.props.bodyColumns}>
600        <Box flexDirection="column" alignItems="flex-end">
601          {fit(bubble).map((row, i) => (
602            <Text key={`bubble-${i}`} color={isAsleep ? Z_FADED : BODY} bold={!isAsleep}>
603              {row || ' '}
604            </Text>
605          ))}
606        </Box>
607        <Box flexDirection="column" marginLeft={1}>
608          {fit(laptop).map((r, i) => drawRow(r, `laptop-${i}`))}
609        </Box>
610        <Box flexDirection="column">{fit(pet).map((r, i) => drawRow(r, `pet-${i}`))}</Box>
611        <Box flexDirection="column">{fit(behind).map((r, i) => drawRow(r, `behind-${i}`))}</Box>
612      </Box>
613    )
614  })
615}
616
types/index.d.ts 8 lines
1export type PetMood = 'awake' | 'working' | 'planning' | 'error' | 'sleeping'
2
3declare module 'claude-code' {
4  interface PluginState {
5    'clawd-pet': { mood: PetMood; frame: number; isHidden: boolean; laptopStep: number; isPlanMode: boolean }
6  }
7}
8