SLOPSHOPPER

deriva

Deriva: tenete in rotta la nave del team, insieme, mentre i vostri Claude lavorano

newpaneguardcommandtoastprompt
v0.1.0no licenseupdated 2026-10-06Bayway/LLM-Game/plugins/deriva
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · deriva
│ ┃ deriva ✕ › fix the failing auth test and add an audit log call │ ┃ 🚀 Deriva │ ┃ Non sei ancora su una nave. ⏺ Read(src/auth.ts) │ ┃ /deriva nuova <nome> per crearne una, ⎿ Read 6 lines │ ┃ /deriva entra <CODICE> <nome> per salire su ⏺ Update(src/auth.ts) │ ┃ quella del team. ⎿ 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 │ │ › /deriva │ ⎿ deriva: Deriva: tenete in rotta la nave del team mentre i vostri │ ⎿ deriva: /deriva nuova <nome> crea una nave e ti dà il codice da │ ⎿ deriva: /deriva entra <CODICE> <nome> [ruolo] sali sulla nave d │ ⎿ deriva: /deriva apre il pannello │ ⎿ deriva: /deriva ruolo <ruolo> cambia ruolo (costa 3 di energia) │ ⎿ deriva: /deriva auto on|off apertura automatica a ogni prompt │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · deriva
🚀 Deriva Non sei ancora su una nave. /deriva nuova <nome> per crearne una, /deriva entra <CODICE> <nome> per salire su quella del team.
Source 3 files
hooks/register.tsx 459 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, Timer } from 'claude-code'
3
4import type { Membership, Reply, View } from '../types'
5import { ago, bar, COLORS, countdown, HAZARD_NAMES, levelColor, movesFor, parseJoin } from './format'
6
7const SERVER = 'https://mqhxhdhdmexkbhlmkjvu.supabase.co/functions/v1/deriva'
8const PANE = 'deriva'
9const TITLE = 'Deriva'
10const PANE_ROWS = 28
11const POLL_MS = 2000
12/** With the pane closed but Claude working, one sync every this many polls (about 14 s). */
13const CLOSED_POLL_EVERY = 7
14const CLOSE_DELAY_MS = 800
15const LOG_LINES = 6
16
17/** Tools that stop the turn until the person answers. */
18const ASKING_TOOLS = new Set(['AskUserQuestion', 'ExitPlanMode'])
19const ASKING_NOTIFICATIONS = new Set(['permission_prompt', 'elicitation_dialog'])
20
21const SIGNAL_BUTTONS = [
22  { key: 'sig-ingegnere', hotkey: 'i', label: 'serve ingegnere', id: 'serve_ingegnere' },
23  { key: 'sig-navigatore', hotkey: 'n', label: 'serve navigatore', id: 'serve_navigatore' },
24  { key: 'sig-tecnico', hotkey: 't', label: 'serve tecnico', id: 'serve_tecnico' },
25  { key: 'sig-bravi', hotkey: 'b', label: 'bravi!', id: 'bravi' },
26]
27
28const HELP = [
29  'Deriva: tenete in rotta la nave del team mentre i vostri Claude lavorano.',
30  '/deriva nuova <nome>  crea una nave e ti dà il codice da condividere',
31  '/deriva entra <CODICE> <nome> [ruolo]  sali sulla nave del team (ingegnere, navigatore, tecnico)',
32  '/deriva  apre il pannello',
33  '/deriva ruolo <ruolo>  cambia ruolo (costa 3 di energia)',
34  '/deriva auto on|off  apertura automatica a ogni prompt',
35  '/deriva esci  lascia la nave su questo computer',
36].join('\n')
37
38const phase = atom({ plugin: 'deriva', key: 'phase' } as const, 'idle')
39const isAutoOpen = atom({ plugin: 'deriva', key: 'isAutoOpen' } as const, true)
40
41// Turns running now, the main loop's and its subagents': the room is open while any runs.
42const running = new Set<string>()
43let pendingClose: Timer | undefined
44let poller: Timer | undefined
45let pollCount = 0
46let isSyncing = false
47/** Undefined until read from the store. */
48let membership: Membership | null | undefined
49let view: View | null = null
50let receivedAt = 0
51let lastError: string | null = null
52let alertedJump = 0
53
54async function memberOf($: EngineInterface): Promise<Membership | null> {
55  if (membership === undefined) {
56    membership = ((await $.store.get('membership')) as Membership | undefined) ?? null
57  }
58
59  return membership
60}
61
62async function call($: EngineInterface, body: Record<string, unknown>): Promise<Reply> {
63  try {
64    const response = await $.http.fetch(SERVER, {
65      method: 'POST',
66      headers: { 'content-type': 'application/json' },
67      body: JSON.stringify(body),
68    })
69    const reply = JSON.parse(response.text) as Reply
70
71    return response.ok || reply.error ? reply : { error: `La nave non risponde (${response.status}).` }
72  } catch {
73    return { error: 'La nave non risponde: controlla la connessione.' }
74  }
75}
76
77async function apply($: EngineInterface, reply: Reply): Promise<void> {
78  if (reply.view) {
79    view = reply.view
80    receivedAt = await $.clock.now()
81    const jump = reply.view.jump
82    if (jump && !jump.isYours && jump.expiresAt !== alertedJump && (await read($, phase)) === 'working') {
83      alertedJump = jump.expiresAt
84      $.ui.toast(`🚀 ${jump.by} ha avviato un salto: premi 9 su Deriva per confermare entro 20 secondi!`)
85    }
86  }
87  lastError = reply.error ?? null
88  $.ui.invalidate('ui.render')
89}
90
91async function sync($: EngineInterface): Promise<void> {
92  const member = await memberOf($)
93  if (!member || isSyncing) return
94
95  isSyncing = true
96  try {
97    const isWaiting = (await read($, phase)) === 'working'
98    const { code, playerId, token } = member
99    await apply($, await call($, { op: 'sync', code, playerId, token, isWaiting }))
100  } finally {
101    isSyncing = false
102  }
103}
104
105async function poll($: EngineInterface): Promise<void> {
106  if (!(await memberOf($))) return
107
108  pollCount += 1
109  const isOpen = (await $.ui.panes()).some(pane => pane.id === PANE)
110  const isWorking = (await read($, phase)) === 'working'
111  if (isOpen || (isWorking && pollCount % CLOSED_POLL_EVERY === 0)) await sync($)
112}
113
114function startPolling($: EngineInterface): void {
115  poller ??= $.clock.every(POLL_MS, () => void poll($))
116}
117
118async function perform($: EngineInterface, action: string, target?: number): Promise<void> {
119  const member = await memberOf($)
120  if (!member) return
121
122  const current = await read($, phase)
123  if (current !== 'working') {
124    $.ui.toast(current === 'needs-you' ? 'Prima rispondi a Claude: ti sta aspettando.' : 'Si agisce solo mentre Claude lavora.')
125    return
126  }
127
128  const { code, playerId, token } = member
129  const reply = await call($, { op: 'act', code, playerId, token, action, target, isWaiting: true })
130  await apply($, reply)
131  if (reply.error) $.ui.toast(reply.error)
132}
133
134async function sendSignal($: EngineInterface, id: string): Promise<void> {
135  const member = await memberOf($)
136  if (!member) return
137
138  const { code, playerId, token } = member
139  const reply = await call($, { op: 'signal', code, playerId, token, signal: id })
140  await apply($, reply)
141  $.ui.toast(reply.error ?? "Segnale inviato all'equipaggio.")
142}
143
144/** Keeps the membership a create or join handed out and opens the bridge. */
145async function board($: EngineInterface, reply: Reply): Promise<string | null> {
146  if (!reply.code || !reply.playerId || !reply.token || !reply.view) return null
147
148  membership = {
149    code: reply.code,
150    playerId: reply.playerId,
151    token: reply.token,
152    nickname: reply.view.you.nickname,
153  }
154  await $.store.set('membership', membership)
155  await apply($, reply)
156  startPolling($)
157  await $.ui.open({ id: PANE, title: TITLE, focus: true, rows: PANE_ROWS })
158
159  return reply.view.you.role
160}
161
162async function needsYou($: EngineInterface): Promise<void> {
163  if (running.size > 0) await update($, phase, () => 'needs-you')
164}
165
166async function backToWork($: EngineInterface): Promise<void> {
167  if (running.size > 0) await update($, phase, current => (current === 'needs-you' ? 'working' : current))
168}
169
170export const register: Register = on => {
171  on('session.start', async ($, e, next) => {
172    await $.command.register({
173      name: 'deriva',
174      description: 'Deriva: la nave del team, si gioca mentre Claude lavora (/deriva nuova | entra | ruolo | auto | esci)',
175    })
176    const isAuto = (await $.store.get('isAutoOpen')) !== false
177    await update($, isAutoOpen, () => isAuto)
178    if (await memberOf($)) startPolling($)
179
180    return next(e)
181  })
182
183  on('prompt.submit', async ($, e, next) => {
184    const result = await next(e)
185    pendingClose?.cancel()
186    pendingClose = undefined
187    if ((await memberOf($)) && (await read($, isAutoOpen))) {
188      void $.ui.open({ id: PANE, title: TITLE, rows: PANE_ROWS })
189    }
190
191    return result
192  })
193
194  on('turn.start', async ($, e, next) => {
195    running.add(e.turnId)
196    await update($, phase, () => 'working')
197    if (await memberOf($)) {
198      startPolling($)
199      void sync($)
200    }
201
202    return next(e)
203  })
204
205  on('turn.complete', async ($, e, next) => {
206    const result = await next(e)
207    running.delete(e.turnId)
208    if (running.size > 0 || e.agentId !== undefined) return result
209
210    await update($, phase, () => 'idle')
211    const isOpen = (await $.ui.panes()).some(pane => pane.id === PANE)
212    if (isOpen && view) {
213      const threats = view.hazards.length
214      $.ui.toast(
215        `Deriva: nave a ${Math.floor(view.ship.distance)}/${view.destination} anni luce, ${threats} ${threats === 1 ? 'minaccia' : 'minacce'} in arrivo`,
216      )
217    }
218    if (isOpen && (await read($, isAutoOpen))) {
219      pendingClose = $.clock.after(CLOSE_DELAY_MS, () => {
220        pendingClose = undefined
221        void $.ui.close({ id: PANE })
222      })
223    }
224
225    return result
226  })
227
228  on('classic.PermissionRequest', async ($, e, next) => {
229    await needsYou($)
230
231    return next(e)
232  })
233
234  on('classic.Notification', async ($, e, next) => {
235    if (ASKING_NOTIFICATIONS.has(e.notification_type)) await needsYou($)
236
237    return next(e)
238  })
239
240  on('tool.call', async ($, e, next) => {
241    if (ASKING_TOOLS.has(String(e.tool))) await needsYou($)
242    const ran = await next(e)
243    await backToWork($)
244
245    return ran
246  })
247
248  on('command.run', { command: 'deriva' }, async ($, e) => {
249    const [sub = '', ...words] = e.args.trim().split(/\s+/).filter(Boolean)
250
251    if (sub === 'nuova') {
252      const reply = await call($, { op: 'create', nickname: words.join(' ') })
253      if (!(await board($, reply))) return { text: reply.error ?? 'Non sono riuscito a creare la nave.' }
254
255      return {
256        text: `Nave creata: codice ${reply.code}. I compagni salgono con /deriva entra ${reply.code} <nome>. Tu sei l'ingegnere.`,
257      }
258    }
259
260    if (sub === 'entra') {
261      const parsed = parseJoin(words)
262      if (!parsed) return { text: 'Uso: /deriva entra <CODICE> <nome> [ingegnere|navigatore|tecnico]' }
263      const reply = await call($, { op: 'join', ...parsed })
264      const role = await board($, reply)
265      if (!role) return { text: reply.error ?? 'Non sono riuscito a salire a bordo.' }
266
267      return { text: `Sei a bordo della nave ${reply.code} come ${role}. Si gioca mentre Claude lavora.` }
268    }
269
270    const member = await memberOf($)
271
272    if (sub === 'ruolo') {
273      if (!member) return { text: 'Prima sali su una nave: /deriva entra <CODICE> <nome>' }
274      const { code, playerId, token } = member
275      const reply = await call($, { op: 'role', code, playerId, token, role: words[0] ?? '' })
276      await apply($, reply)
277
278      return { text: reply.error ?? `Ora sei ${reply.view?.you.role ?? words[0]}.` }
279    }
280
281    if (sub === 'esci') {
282      membership = null
283      view = null
284      await $.store.delete('membership')
285      await $.ui.close({ id: PANE })
286
287      return { text: 'Hai lasciato la nave su questo computer. Per tornare: /deriva entra <CODICE> <nome>' }
288    }
289
290    if (sub === 'auto' && (words[0] === 'on' || words[0] === 'off')) {
291      const isAuto = words[0] === 'on'
292      await update($, isAutoOpen, () => isAuto)
293      await $.store.set('isAutoOpen', isAuto)
294
295      return { text: isAuto ? 'Deriva si aprirà a ogni prompt.' : 'Deriva non si aprirà più da solo: usa /deriva.' }
296    }
297
298    if (sub !== '' || !member) return { text: HELP }
299
300    await $.ui.open({ id: PANE, title: TITLE, focus: true, rows: PANE_ROWS })
301    void sync($)
302
303    return { text: `Plancia della nave ${member.code} aperta.` }
304  })
305
306  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
307    const { Box, Text, Button } = $.ui.resolve(e)
308    const current = await read($, phase)
309    const member = await memberOf($)
310
311    if (!member) {
312      return (
313        <Box flexDirection="column">
314          <Text bold>🚀 Deriva</Text>
315          <Text>Non sei ancora su una nave.</Text>
316          <Text dimColor wrap="wrap">
317            {'/deriva nuova <nome> per crearne una, /deriva entra <CODICE> <nome> per salire su quella del team.'}
318          </Text>
319        </Box>
320      )
321    }
322    const shown = view
323    if (!shown) {
324      return <Text dimColor>Collegamento alla nave {member.code}…</Text>
325    }
326
327    const now = shown.now + ((await $.clock.now()) - receivedAt)
328    const ship = shown.ship
329    const isActive = current === 'working'
330    const gauges = [
331      ['Reattore', ship.reactor],
332      ['Scafo', ship.hull],
333      ['Ossigeno', ship.oxygen],
334      ['Scudi', ship.shields],
335    ] as const
336    const jump = shown.jump && shown.jump.expiresAt > now ? shown.jump : null
337    const status =
338      current === 'working'
339        ? "Claude sta lavorando: sei in sala d'attesa, +1 energia ogni 30 secondi."
340        : current === 'needs-you'
341          ? '⚠ Claude ha bisogno di te: rispondi, poi torni in plancia.'
342          : '🔒 Claude è fermo: puoi guardare la nave, ma si agisce solo mentre lavora.'
343
344    return (
345      <Box flexDirection="column">
346        <Box gap={2}>
347          <Text bold>🚀 DERIVA</Text>
348          <Text>{shown.code}</Text>
349          <Text dimColor>missione {shown.mission}</Text>
350        </Box>
351        <Box gap={1}>
352          <Text>{'Rotta'.padEnd(9)}</Text>
353          <Text color={COLORS.accent}>{bar(ship.distance, shown.destination, 20)}</Text>
354          <Text>
355            {Math.floor(ship.distance)}/{shown.destination} AL
356          </Text>
357          {ship.courseUntil > now && <Text color={COLORS.accent}>⚡ {countdown(ship.courseUntil - now)}</Text>}
358        </Box>
359        {gauges.map(([label, value]) => (
360          <Box key={`gauge-${label}`} gap={1}>
361            <Text>{label.padEnd(9)}</Text>
362            <Text color={levelColor(value)}>{bar(value, 100, 20)}</Text>
363            <Text>{String(Math.round(value)).padStart(3)}</Text>
364          </Box>
365        ))}
366
367        <Text bold>Minacce</Text>
368        {shown.hazards.length === 0 && <Text dimColor>  nessun contatto sui sensori</Text>}
369        {shown.hazards.map(hazard => (
370          <Text color={hazard.kind === null ? COLORS.warn : hazard.kind === 'relitto' ? COLORS.accent : COLORS.bad}>
371            {'  '}
372            {hazard.kind === null ? '? Contatto sconosciuto' : `${hazard.kind === 'relitto' ? '🛰' : '⚠'} ${HAZARD_NAMES[hazard.kind]}`} tra{' '}
373            {countdown(hazard.hitsAt - now)}
374          </Text>
375        ))}
376
377        <Text bold>Equipaggio</Text>
378        <Box gap={2} flexWrap="wrap">
379          {shown.crew.map(c => (
380            <Text color={c.isWaiting ? COLORS.good : COLORS.dim}>
381              {c.isWaiting ? '●' : '○'} {c.nickname} {c.role}
382              {c.isYou ? ' (tu)' : ''}
383            </Text>
384          ))}
385        </Box>
386
387        <Box gap={1}>
388          <Text>{'Energia'.padEnd(9)}</Text>
389          <Text color={COLORS.accent}>{bar(shown.you.energy, shown.you.maxEnergy, shown.you.maxEnergy)}</Text>
390          <Text>
391            {shown.you.energy}/{shown.you.maxEnergy}
392          </Text>
393        </Box>
394        <Box gap={2} flexWrap="wrap">
395          {movesFor(shown).map(move => (
396            <Button
397              key={move.key}
398              label={`${move.label} (${move.cost})`}
399              hotkey={move.hotkey}
400              plain
401              dimColor={!isActive || shown.you.energy < move.cost}
402              onPress={() => void perform($, move.action, move.target)}
403            />
404          ))}
405          {!jump && (
406            <Button
407              key="jump"
408              label="salto (2)"
409              hotkey="8"
410              plain
411              dimColor={!isActive || ship.reactor < 60}
412              onPress={() => void perform($, 'salto')}
413            />
414          )}
415          {jump && !jump.isYours && (
416            <Button
417              key="confirm-jump"
418              label={`conferma il salto di ${jump.by} (1)`}
419              hotkey="9"
420              plain
421              variant="primary"
422              onPress={() => void perform($, 'conferma_salto')}
423            />
424          )}
425        </Box>
426        {jump?.isYours && (
427          <Text color={COLORS.warn}>Salto avviato: serve la conferma di un compagno entro {countdown(jump.expiresAt - now)}.</Text>
428        )}
429        <Box gap={2} flexWrap="wrap">
430          <Text dimColor>Segnali</Text>
431          {SIGNAL_BUTTONS.map(signal => (
432            <Button
433              key={signal.key}
434              label={signal.label}
435              hotkey={signal.hotkey}
436              plain
437              dimColor
438              onPress={() => void sendSignal($, signal.id)}
439            />
440          ))}
441        </Box>
442
443        <Text bold>Diario di bordo</Text>
444        {shown.log.slice(-LOG_LINES).map((entry, index) => (
445          <Box key={`log-${index}`} gap={1}>
446            <Text dimColor>{ago(now - entry.at).padEnd(6)}</Text>
447            <Text wrap="wrap">{entry.text}</Text>
448          </Box>
449        ))}
450
451        <Text color={current === 'needs-you' ? COLORS.warn : undefined} dimColor={current === 'idle'} wrap="wrap">
452          {status}
453        </Text>
454        {lastError && <Text color={COLORS.bad}>{lastError}</Text>}
455      </Box>
456    )
457  })
458}
459
hooks/format.ts 98 lines
1import type { HazardKind, Role, View } from '../types'
2
3export const HAZARD_NAMES: Record<HazardKind, string> = {
4  asteroidi: 'Campo di asteroidi',
5  tempesta: 'Tempesta ionica',
6  relitto: 'Relitto alla deriva',
7}
8
9export const COLORS = {
10  good: '#22c55e',
11  warn: '#eab308',
12  bad: '#ef4444',
13  dim: '#6b7280',
14  accent: '#38bdf8',
15}
16
17export type Move = {
18  key: string
19  hotkey: string
20  label: string
21  action: string
22  target?: number
23  cost: number
24}
25
26export function bar(value: number, max: number, width: number): string {
27  const filled = Math.round((Math.max(0, Math.min(max, value)) / max) * width)
28
29  return '█'.repeat(filled) + '░'.repeat(width - filled)
30}
31
32export function levelColor(value: number): string {
33  return value < 25 ? COLORS.bad : value < 50 ? COLORS.warn : COLORS.good
34}
35
36export function countdown(ms: number): string {
37  const seconds = Math.max(0, Math.round(ms / 1000))
38
39  return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`
40}
41
42export function ago(ms: number): string {
43  const minutes = Math.floor(ms / 60_000)
44  if (minutes < 1) return 'ora'
45  if (minutes < 60) return `${minutes}m fa`
46  const hours = Math.floor(minutes / 60)
47
48  return hours < 24 ? `${hours}h fa` : `${Math.floor(hours / 24)}g fa`
49}
50
51/** What the player's role can do on this view, in hotkey order. */
52export function movesFor(view: View): Move[] {
53  const role: Role = view.you.role
54  if (role === 'ingegnere') {
55    return [
56      { key: 'act-reattore', hotkey: '1', label: 'ripara reattore +20', action: 'ripara_reattore', cost: 1 },
57      { key: 'act-scafo', hotkey: '2', label: 'ripara scafo +15', action: 'ripara_scafo', cost: 1 },
58      { key: 'act-scudi', hotkey: '3', label: 'carica scudi +25', action: 'carica_scudi', cost: 1 },
59    ]
60  }
61  if (role === 'tecnico') {
62    return [
63      { key: 'act-scansiona', hotkey: '1', label: 'scansiona', action: 'scansiona', cost: 1 },
64      { key: 'act-ossigeno', hotkey: '2', label: 'ricicla ossigeno +20', action: 'ricicla_ossigeno', cost: 1 },
65    ]
66  }
67
68  const moves: Move[] = [{ key: 'act-rotta', hotkey: '1', label: 'rotta veloce 60m', action: 'traccia_rotta', cost: 2 }]
69  const threat = view.hazards.find(hazard => hazard.kind === 'asteroidi' || hazard.kind === 'tempesta')
70  if (threat?.kind) {
71    moves.push({
72      key: 'act-evita',
73      hotkey: '2',
74      label: `evita: ${HAZARD_NAMES[threat.kind].toLowerCase()}`,
75      action: 'evita',
76      target: threat.id,
77      cost: 2,
78    })
79  }
80  const wreck = view.hazards.find(hazard => hazard.kind === 'relitto')
81  if (wreck) {
82    moves.push({ key: 'act-aggancia', hotkey: '3', label: 'aggancia relitto', action: 'aggancia', target: wreck.id, cost: 2 })
83  }
84
85  return moves
86}
87
88/** Splits `/deriva entra DRV-AB12 Anna Rossi tecnico` into code, nickname and role. */
89export function parseJoin(words: string[]): { code: string; nickname: string; role?: Role } | null {
90  const [code, ...rest] = words
91  if (!code || rest.length === 0) return null
92  const last = rest[rest.length - 1]
93  const role = last === 'ingegnere' || last === 'navigatore' || last === 'tecnico' ? last : undefined
94  const nickname = (role ? rest.slice(0, -1) : rest).join(' ')
95
96  return nickname ? { code: code.toUpperCase(), nickname, role } : null
97}
98
types/index.d.ts 46 lines
1// The deriva edge function's answers, as engine.ts `toView` builds them.
2
3export type Phase = 'idle' | 'working' | 'needs-you'
4
5export type Role = 'ingegnere' | 'navigatore' | 'tecnico'
6
7export type HazardKind = 'asteroidi' | 'tempesta' | 'relitto'
8
9export type Ship = {
10  reactor: number
11  hull: number
12  oxygen: number
13  shields: number
14  distance: number
15  courseUntil: number
16}
17
18export type View = {
19  code: string
20  now: number
21  mission: number
22  destination: number
23  ship: Ship
24  hazards: { id: number; kind: HazardKind | null; hitsAt: number }[]
25  crew: { nickname: string; role: Role; isWaiting: boolean; isYou: boolean }[]
26  you: { nickname: string; role: Role; energy: number; maxEnergy: number }
27  jump: { by: string; isYours: boolean; expiresAt: number } | null
28  log: { at: number; text: string }[]
29}
30
31export type Reply = {
32  view?: View
33  error?: string
34  code?: string
35  playerId?: string
36  token?: string
37}
38
39export type Membership = { code: string; playerId: string; token: string; nickname: string }
40
41declare module 'claude-code' {
42  interface PluginState {
43    deriva: { phase: Phase; isAutoOpen: boolean }
44  }
45}
46