SLOPSHOPPER

todos

Offene Aufgaben des aktuellen Repos aus dem G-Brain im Pane und als Eintrag in der Leiste über dem Eingabefeld; ein Klick schreibt die Aufgabe ins Eingabefeld

newpanebandcommandprocesstimer
v0.3.4MITupdated 2026-10-16allorders1studio/claude-code-mods/todos
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · todos
│ ┃ Aufgaben ✕ › fix the failing auth test and add an audit log call │ ┃ G-Brain nicht lesbar: TypeError: undefined │ ┃ is not an object (evaluating ⏺ Read(src/auth.ts) │ ┃ 'r.content.find') ⎿ Read 6 lines │ ┃ [ Neu laden ] ⏺ 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 │ │ › /tickets │ ⎿ todos: Aufgaben-Pane geöffnet. │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Aufgaben
G-Brain nicht lesbar: TypeError: undefined is not an object (evaluating 'r.content.find') [ Neu laden ]
README

claude-code-mods

Four Claude Code mods (plugins that change the interface itself): a deploy status bar, a tasks pane, a RAM indicator and a Git worktree overview. The three that draw above the input field share one bar.

The UI texts of all mods are in German.

ModWhat it showsWorks for you if
worktreesAll Git worktrees with open workyou use Git (nothing else needed)
dokploy-statusbarLast Dokploy deploy per appyou deploy with Dokploy and have the private dokploy CLI
todosOpen tasks of the current repo from a G-Brainyou have an MCP server claude.ai G-Brain with get_page
ramUsed memory in the baryou use macOS

Installation

claude plugin marketplace add allorders1studio/claude-code-mods
claude plugin install worktrees@claude-code-mods

Replace worktrees with dokploy-statusbar, todos or ram as needed, then run /reload-plugins inside Claude Code.

worktrees

The only mod that needs nothing but Git. /worktrees lists every worktree of the current repo and tells you which ones can be deleted.

╭──────────────────────────────────────────────────────────╮
│ feature-search  feat/search                              │
│ 3 Dateien uncommittet  ·  letzter Commit heute           │
│ Pfad kopieren                                            │
╰──────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────╮
│ fix-login  fix/login                                     │
│ 2 Commits nicht in main, nicht gepusht  ·  letzter Commit gestern │
╰──────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────╮
│ old-experiment  exp/cache                                │
│ nichts offen – kann weg  ·  letzter Commit vor 12 Tagen  │
╰──────────────────────────────────────────────────────────╯
Neu laden  3 Worktrees · 2 mit offener Arbeit · Stand 23:20:56

Status line while worktrees hold open work:

2 von 3 Worktrees mit offener Arbeit · /worktrees
  • Uncommitted files per worktree
  • Commits not in main (compared against origin/<default branch>)
  • Pushed or not pushed
  • "kann weg" (can go) for worktrees with nothing left to lose; "Stand unklar" when the comparison with main fails
  • Card colors: red uncommitted, yellow not pushed, cyan pushed, green can go

Command: /worktrees. Requirements: Git.

dokploy-statusbar

A bar above the input field with the last Dokploy deploy of every app belonging to the current repo.

 ● api-app  ● web-app  ● worker running 1:20  ● docs ausstehend          23:20
 > _
  • Dot: green (done), red (error), yellow (running), grey (pending), magenta (unknown)
  • App name links to its Dokploy page; on error it links to the deployments tab
  • Button "Log" on error: writes the last 80 build-log lines into the input field
  • "ausstehend" (pending): a commit since the deployed one touched the service's watch paths
  • running m:ss while a build is in progress
  • After git push, polls every 5 s for 2 minutes instead of every 30 s
  • Toast when a deploy finishes; the last fetch time is shown faintly at the far right of the shared bar (see below)

Command: /deploys (refresh now).

Requirements, honestly:

  • The dokploy CLI from allorders1studio/dokploy-cli. That repository is currently private; it is not the npm package of the same name.
  • macOS with Homebrew paths: /opt/homebrew/bin/dokploy, /opt/homebrew/lib/node_modules/dokploy-cli and /opt/homebrew/bin/node are hard-coded in hooks/register.tsx and hooks/discover.cjs.

How it works

hooks/discover.cjs asks Dokploy (through the CLI's API module) for all services and keeps those whose Git source matches git remote get-url origin. It also reads each service's watchPaths. No configuration per repo is needed. API keys stay inside the CLI; the mod never sees them.

todos

/tickets opens a pane "Aufgaben" with the open tasks of the current repo, read from a G-Brain (GBrain) knowledge store through its MCP server. The bar above the input field shows Aufgaben N (parked tasks not counted): red if one is overdue, yellow if one is due today or has high priority. A click on it opens the pane.

╭──────────────────────────────────────────────────────────╮
│ Abo-Bestellungen per Tag kennzeichnen                    │
│ überfällig · gestern                                     │
╰──────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────╮
│ HubSpot-Workflow für die Ankündigungsmail                │
│ seit 2026-10-14  ·  Prio hoch                            │
╰──────────────────────────────────────────────────────────╯
Neu laden  2 offen · Stand 23:20:56

Order: tasks with a due date first (by date), then high priority, then the rest in page order, parked tasks last and dimmed. A click on a task writes it, with its detail lines, into the input field; "Details ›" opens the detail view.

How it works: the mod reads the page todo, takes the list slugs from the lines - [[todo/<name>]] — … under ## Projekte, reads each list and keeps those whose Repos: line names the repo of git remote get-url origin. A repo may be on several lists; each is shown as its own section. Loaded at session start, every 5 minutes, on /tickets and on "Neu laden".

# To-do — My project

Repos: `owner/repo`, `owner/other-repo`

## Offen
- [ ] Task title · seit 2026-10-14 · fällig 2026-10-16 · Prio hoch
  - any number of detail lines
- [ ] Waiting task · wartet auf the supplier
- [ ] Parked task · geparkt

## Erledigt
- [x] …

Parts after · in the title line are read as seit, fällig (both YYYY-MM-DD), wartet auf …, Prio hoch and geparkt; anything else stays in the text. [[a/b]] is shown as a/b.

Requirements, honestly: an MCP server whose tools include get_page, put_page and remember (the mod finds it by these tools, since its name differs between terminal and desktop app); get_page takes { slug, content_only } and answers with JSON containing compiled_truth. In auto mode a mod's MCP call is refused unless get_page is allowed in permissions.allow, e.g. "mcp__<server>__get_page". Without such a store this mod is of no use to you. No credentials are configured here; the connection is the session's own.

Command: /tickets.

ram

Used memory in the bar, polled every 10 seconds from memory_pressure -Q (macOS only): RAM 66% is 100 minus the "free percentage". Grey below 80 %, yellow from 80 %, red from 90 %. Where the command is missing or fails, the entry is left out without a message.

The shared bar

dokploy-statusbar, todos and ram draw into the same band above the input field, which only one hook can fill. Each mod reads the tree of the hooks inside it (next(e)), adds its entry under a sorted key and rebuilds the bar, so the layout does not depend on plugin order. Apps on the left, the rest on the right, separated by · , the Dokploy fetch time last:

 ● api-app  ● web-app  ● worker running 1:20            Aufgaben 3 · RAM 66% · 23:20
 > _

Without Dokploy only Aufgaben and RAM appear. hooks/bar.tsx is the same file in all three mods.

Structure

.claude-plugin/marketplace.json   marketplace "claude-code-mods"
<plugin>/
  .claude-plugin/plugin.json      manifest (name, version)
  hooks/register.tsx              the mod: panes, bands, commands, atoms
  hooks/*.ts                      pure logic (parsing, scanning) plus tests
  types/index.d.ts                state types per plugin

Mods use the claude-code mod API. Tests of the pure logic sit next to the code (*.test.ts); UI behavior can be tested with claude-code/testing.

License

MIT, see LICENSE.

Source 5 files
hooks/register.tsx 184 lines
1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Item, TodoList } from '../types'
5import { addToBar } from './bar'
6import { due } from './format'
7import { parseList, parseOverview, repoOf, sortItems, summary, toPrompt } from './parse'
8
9const SERVER = 'claude.ai G-Brain'
10const PANE = 'todos'
11const EVERY = 300000
12const lists = atom({ plugin: 'todos', key: 'lists' } as const, [])
13const error = atom({ plugin: 'todos', key: 'error' } as const, '')
14const loadedAt = atom({ plugin: 'todos', key: 'loadedAt' } as const, '')
15const detail = atom({ plugin: 'todos', key: 'detail' } as const, null)
16
17// Der Servername hängt an der Oberfläche: im Terminal `claude.ai G-Brain`, in der Desktop-App die Connector-ID.
18// Daher den Server über seine Werkzeuge finden: einer, der `get_page`, `put_page` und `remember` anbietet.
19let server = ''
20async function brain($: EngineInterface) {
21  if (server) return server
22  const names = (await $.tool.list()).filter(t => t.mcp).map(t => t.name)
23  const has = (s: string, tool: string) => names.includes(`mcp__${s}__${tool}`)
24  const found = names
25    .map(n => n.match(/^mcp__(.+)__get_page$/)?.[1])
26    .find(s => s && has(s, 'put_page') && has(s, 'remember'))
27  // Nur einen Treffer merken; ohne Treffer beim nächsten Laden erneut suchen.
28  if (found) server = found
29  return found ?? SERVER
30}
31
32// Lesender Aufruf; Antwort ist JSON im ersten Text-Block, `compiled_truth` die Seite ohne Frontmatter.
33async function page($: EngineInterface, slug: string) {
34  const r = await $.mcp.call(await brain($), 'get_page', { slug, content_only: true })
35  const text = r.content.find(c => c.type === 'text')?.text ?? ''
36  if (r.isError) throw new Error(`${slug}: ${text.slice(0, 200) || 'Fehler'}`)
37  try {
38    const p = JSON.parse(text)
39    return { title: String(p.title ?? ''), md: String(p.compiled_truth ?? '') }
40  } catch {
41    throw new Error(`${slug}: Antwort nicht lesbar`)
42  }
43}
44
45// Repo bei jedem Lauf neu lesen, damit ein Projektwechsel in der Session greift.
46async function load($: EngineInterface) {
47  try {
48    const cwd = await $.session.cwd()
49    const remote = await $.process.run(['git', 'remote', 'get-url', 'origin'], { cwd })
50    const repo = remote.exitCode === 0 ? repoOf(remote.stdout) : ''
51    let found: TodoList[] = []
52    if (repo) {
53      const slugs = parseOverview((await page($, 'todo')).md)
54      const all = await Promise.all(
55        slugs.map(async slug => {
56          const p = await page($, slug)
57          const l = parseList(p.md)
58          return { ...l, title: l.title || p.title, slug }
59        }),
60      )
61      // Nur der Repo-Name zählt: die Listen im G-Brain nennen evtl. noch den alten GitHub-Owner.
62      const name = (r: string) => r.split('/')[1]
63      found = all.filter(l => l.repos.some(r => name(r) === name(repo)))
64    }
65    await update($, lists, () => found)
66    await update($, error, () => '')
67    await update($, loadedAt, () => new Date().toLocaleTimeString('de-DE'))
68  } catch (err) {
69    await update($, lists, () => [])
70    await update($, error, () => `G-Brain nicht lesbar: ${String(err)}`)
71  }
72}
73
74async function show($: EngineInterface) {
75  await update($, detail, () => null)
76  await $.ui.open({ id: PANE, title: 'Aufgaben' })
77  await load($)
78}
79
80// Backticks aus dem Markdown der Liste stören in Button und Text, dort wird nichts formatiert.
81const plain = (t: string) => t.replace(/`/g, '')
82
83const today = () => new Date().toLocaleDateString('sv-SE')
84
85export const register: Register = on => {
86  on('session.start', async ($, e, next) => {
87    await $.command.register({ name: 'tickets', description: 'Offene Aufgaben dieses Repos aus dem G-Brain zeigen' })
88    void load($)
89    $.clock.every(EVERY, () => void load($))
90    return next(e)
91  })
92
93  on('command.run', { command: 'tickets' }, async $ => {
94    await show($)
95    return { text: 'Aufgaben-Pane geöffnet.' }
96  })
97
98  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
99    const inner = await next(e)
100    const ls = await read($, lists)
101    if (e.props.hasSurvey || (await read($, error)) || ls.length === 0) return inner
102    const { Button, Text } = $.ui.resolve(e)
103    const s = summary(ls, today())
104    const entry = (
105      <Button key="todos" plain onPress={() => show($)}>
106        <Text color={s.color}>Aufgaben {s.n}</Text>
107      </Button>
108    )
109    return addToBar($.ui.resolve(e), inner, 'right', '1-todos', entry)
110  })
111
112  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
113    const { Box, Button, Markdown, Text } = $.ui.resolve(e)
114    const d = await read($, detail)
115    const now = new Date()
116    // Fälligkeit mit Farbe, sonst „seit“; dazu Wartezustand, Prio, Parkstatus.
117    const line = (it: Item) => {
118      const dl = it.meta.due && !it.meta.parked ? due(`${it.meta.due}T12:00:00`, false, now) : null
119      const rest = [it.meta.waitingFor && `wartet auf ${it.meta.waitingFor}`, it.meta.prio && 'Prio hoch', it.meta.parked && 'geparkt']
120      const first = dl ? <Text color={dl.color}>{dl.text}</Text> : <Text dimColor>{it.meta.since ? `seit ${it.meta.since}` : ''}</Text>
121      return (
122        <Text dimColor={!!it.meta.parked}>
123          {first}
124          {/* Trenner nur zwischen vorhandenen Teilen, sonst stünde bei Einträgen ohne „seit“ ein Punkt vorn */}
125          <Text dimColor>{rest.filter(Boolean).map((x, i) => (i || dl || it.meta.since ? `  ·  ${x}` : x)).join('')}</Text>
126        </Text>
127      )
128    }
129
130    if (d) {
131      const { item, list } = d
132      return (
133        <Box flexDirection="column" gap={1}>
134          <Box gap={1}>
135            <Button key="back" label="← Zurück" onPress={() => update($, detail, () => null)} />
136            <Button key="prompt" label="In Prompt übernehmen" onPress={() => $.prompt.fill({ text: toPrompt(list, item) })} />
137          </Box>
138          <Box flexDirection="column">
139            <Text bold>{plain(item.text)}</Text>
140            <Text dimColor>{list.title}</Text>
141            {line(item)}
142          </Box>
143          {item.details.length ? <Markdown text={item.details.map(x => `- ${x}`).join('\n')} /> : <Text dimColor>Keine Details.</Text>}
144        </Box>
145      )
146    }
147
148    const ls = await read($, lists)
149    const err = await read($, error)
150    const at = await read($, loadedAt)
151    const n = ls.reduce((sum, l) => sum + l.items.filter(i => !i.meta.parked).length, 0)
152    return (
153      <Box flexDirection="column">
154        {err && <Text color="red">{err}</Text>}
155        {!err && !at && <Text dimColor>Lade …</Text>}
156        {!err && at && ls.length === 0 && <Text dimColor>Für dieses Repo gibt es keine Aufgabenliste im G-Brain (Zeile `Repos:` auf einer todo-Seite).</Text>}
157        {ls.map(l => (
158          <Box key={`list:${l.slug}`} flexDirection="column" marginBottom={1}>
159            <Text bold>{l.title}</Text>
160            {l.items.length === 0 && <Text dimColor>Keine offenen Aufgaben.</Text>}
161            {sortItems(l.items).map((it, i) => {
162              const color = it.meta.due && !it.meta.parked ? due(`${it.meta.due}T12:00:00`, false, now)?.color : undefined
163              return (
164                <Box key={`card:${l.slug}:${i}`} flexDirection="column" borderStyle="round" borderColor={color} borderDimColor={!color} hover={{ borderColor: 'cyan', borderDimColor: false }} paddingX={1}>
165                  {/* Klick auf den Titel schreibt die Aufgabe ins Eingabefeld; die Details bleiben über „Details ›“ erreichbar */}
166                  <Button key={`task:${l.slug}:${i}`} plain dimColor={it.meta.parked} label={plain(it.text)} onPress={() => $.prompt.fill({ text: toPrompt(l, it) })} />
167                  <Box justifyContent="space-between">
168                    {line(it)}
169                    <Button key={`open:${l.slug}:${i}`} plain label="Details ›" onPress={() => update($, detail, () => ({ list: l, item: it }))} />
170                  </Box>
171                </Box>
172              )
173            })}
174          </Box>
175        ))}
176        <Box gap={1}>
177          <Button key="reload" label="Neu laden" onPress={() => load($)} />
178          {at && <Text dimColor>{n} offen · Stand {at}</Text>}
179        </Box>
180      </Box>
181    )
182  })
183}
184
hooks/bar.tsx 35 lines
1import type { EngineInterface, RenderElement } from 'claude-code'
2
3type Tree = { props?: { key?: string }; children?: Tree[] }
4type UI = ReturnType<EngineInterface['ui']['resolve']>
5
6// Gemeinsame Leiste über dem Eingabefeld, byte-gleich in dokploy-statusbar, todos und ram (Plugins sind getrennt).
7// `AbovePrompt` ist eine Instanz, die Hooks nesten (erster = äußerster) und deren `next(e)` den Baum der inneren liefert.
8// Jeder Mod liest daher den Baum der inneren, hängt seinen Eintrag unter einem Schlüssel ein und baut die Leiste neu;
9// links/rechts und Reihenfolge ergeben sich aus dem Schlüssel (Sortierung), nicht aus der Plugin-Reihenfolge.
10// shortcut: erkennt nur Bäume mit `key="bar"`; fremde Inhalte stehen darüber, bei Bedarf gezielter einordnen.
11export function addToBar(ui: UI, inner: RenderElement, side: 'left' | 'right', key: string, node: RenderElement): RenderElement {
12  const { Box, Text } = ui
13  const tree = (inner ?? {}) as Tree // falls `next(e)` doch nichts liefert
14  const isBar = tree.props?.key === 'bar'
15  const part = (k: string) => (isBar ? (tree.children?.find(c => c.props?.key === k)?.children ?? []) : [])
16  const entry = (<Box key={`bar:${key}`}>{node}</Box>) as Tree
17  const place = (k: string, items: Tree[]) =>
18    [...items.filter(c => c.props?.key !== `bar:${key}` && c.props?.key !== 'bar:sep'), ...(side === k ? [entry] : [])].sort((a, b) =>
19      String(a.props?.key).localeCompare(String(b.props?.key)),
20    ) as RenderElement[]
21  const left = place('left', part('bar:left'))
22  const right = place('right', part('bar:right'))
23  const bar = (
24    <Box key="bar" justifyContent="space-between">
25      <Box key="bar:left" gap={1}>
26        {left}
27      </Box>
28      <Box key="bar:right">
29        {right.flatMap((c, i) => (i ? [<Box key="bar:sep"><Text color="subtle"> · </Text></Box>, c] : [c]))}
30      </Box>
31    </Box>
32  )
33  return !isBar && (tree.children?.length ?? 0) > 0 ? <Box flexDirection="column">{inner}{bar}</Box> : bar
34}
35
hooks/format.ts 12 lines
1// Fälligkeit relativ zu heute, wie man sie auf einer Karte lesen will; Farbe: rot überfällig, gelb heute.
2export const due = (iso: string | null | undefined, hasTime: boolean | undefined, now: Date) => {
3  if (!iso) return null
4  const d = new Date(iso)
5  const day = (x: Date) => Date.UTC(x.getFullYear(), x.getMonth(), x.getDate()) / 86400000
6  const diff = day(d) - day(now)
7  const time = hasTime ? ` ${d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}` : ''
8  const text =
9    diff === 0 ? `heute${time}` : diff === 1 ? `morgen${time}` : diff === -1 ? `gestern${time}` : `${d.getDate()}.${d.getMonth() + 1}.${time}`
10  return { text: diff < 0 ? `überfällig · ${text}` : `fällig ${text}`, color: diff < 0 ? 'red' : diff === 0 ? 'yellow' : undefined }
11}
12
hooks/parse.ts 75 lines
1import type { Item, Meta, Parsed, TodoList } from '../types'
2
3export const repoOf = (remoteUrl: string) =>
4  remoteUrl.trim().toLowerCase().replace(/\.git$/, '').match(/github\.com[:/]([^/]+\/[^/]+)$/)?.[1] ?? ''
5
6// Listen-Slugs aus `- [[todo/<name>]] — …` unter `## Projekte` der Seite `todo`.
7export const parseOverview = (md: string): string[] => {
8  const slugs: string[] = []
9  let inside = false
10  for (const line of md.split('\n')) {
11    if (/^## /.test(line)) inside = /^## Projekte\s*$/.test(line)
12    else if (inside) {
13      const m = line.match(/^- \[\[(todo\/[^\]|]+)\]\]/)
14      if (m) slugs.push(m[1])
15    }
16  }
17  return slugs
18}
19
20const DATE = '(\\d{4}-\\d{2}-\\d{2})'
21const link = (s: string) => s.replace(/\[\[([^\]|]+)\]\]/g, '$1')
22
23// Metadaten stehen als ` · `-Teile hinter dem Text; ein unbekannter Teil bleibt im Text, damit nichts verschwindet.
24const parseTitle = (raw: string): { text: string; meta: Meta } => {
25  const [head, ...parts] = raw.split(' · ')
26  const meta: Meta = {}
27  const rest: string[] = []
28  for (const p of parts) {
29    const t = p.trim()
30    let m: RegExpMatchArray | null
31    if ((m = t.match(new RegExp(`^seit ${DATE}`)))) meta.since = m[1]
32    else if ((m = t.match(new RegExp(`^fällig ${DATE}`)))) meta.due = m[1]
33    else if ((m = t.match(/^wartet auf (.+)$/))) meta.waitingFor = m[1]
34    else if (/^Prio hoch\b/.test(t)) meta.prio = true
35    else if (/^geparkt\b/.test(t)) meta.parked = true
36    else rest.push(t)
37  }
38  return { text: link([head.trim(), ...rest].join(' · ')), meta }
39}
40
41// Nur Top-Level-Einträge `- [ ] ` unter `## Offen`; eingerückte `- `-Zeilen darunter sind Details.
42export const parseList = (md: string): Parsed => {
43  const lines = md.split('\n')
44  const title = lines.find(l => l.startsWith('# '))?.slice(2).trim() ?? ''
45  const repos = [...(md.match(/^Repos:(.*)$/m)?.[1] ?? '').matchAll(/`([^`]+)`/g)].map(m => m[1].trim().toLowerCase())
46  const items: Item[] = []
47  let open = false
48  for (const line of lines) {
49    if (/^## /.test(line)) open = /^## Offen\s*$/.test(line)
50    else if (!open) continue
51    else if (line.startsWith('- [ ] ')) items.push({ ...parseTitle(line.slice(6)), details: [] })
52    else if (/^\s+- /.test(line) && items.length) items[items.length - 1].details.push(link(line.replace(/^\s+- /, '').trim()))
53  }
54  return { title, repos, items }
55}
56
57// Mit Datum zuerst (nach Datum), dann Prio hoch, dann Rest in Seitenreihenfolge, geparkt zuletzt; sort ist stabil.
58const rank = (i: Item) => (i.meta.parked ? 3 : i.meta.due ? 0 : i.meta.prio ? 1 : 2)
59export const sortItems = (items: Item[]): Item[] =>
60  [...items].sort((a, b) => rank(a) - rank(b) || (a.meta.due ?? '').localeCompare(b.meta.due ?? ''))
61
62// Zahl und Farbe des Leisten-Eintrags; today als JJJJ-MM-TT.
63export const summary = (lists: TodoList[], today: string) => {
64  const open = lists.flatMap(l => l.items).filter(i => !i.meta.parked)
65  const color = open.some(i => i.meta.due && i.meta.due < today)
66    ? 'error'
67    : open.some(i => i.meta.due === today || i.meta.prio)
68      ? 'warning'
69      : 'subtle'
70  return { n: open.length, color }
71}
72
73export const toPrompt = (list: TodoList, item: Item) =>
74  [`Aufgabe aus ${list.slug}: ${item.text}`, ...item.details.map(d => `- ${d}`)].join('\n')
75
types/index.d.ts 11 lines
1export type Meta = { since?: string; due?: string; waitingFor?: string; prio?: boolean; parked?: boolean }
2export type Item = { text: string; meta: Meta; details: string[] }
3export type Parsed = { title: string; repos: string[]; items: Item[] }
4export type TodoList = Parsed & { slug: string }
5
6declare module 'claude-code' {
7  interface PluginState {
8    todos: { lists: TodoList[]; error: string; loadedAt: string; detail: { list: TodoList; item: Item } | null }
9  }
10}
11