From ai-heartbeat-timer
Ziel: the user sagt einen Satz -- das Skill macht daraus die richtige Operation in `timer.md` und validiert. Drei Modi:
How this skill is triggered — by the user, by Claude, or both
Slash command
/ai-heartbeat-timer:ai-timerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Ziel: the user sagt einen Satz -- das Skill macht daraus die richtige Operation in `timer.md` und validiert. Drei Modi:
Ziel: the user sagt einen Satz -- das Skill macht daraus die richtige Operation in timer.md und validiert. Drei Modi:
| Intent | Beispiele | Abschnitt |
|---|---|---|
| ADD | "erinner mich jeden Werktag um 18 Uhr an X", "baue Timer Y" | ADD |
| UPDATE | "aendere morgen-check auf 8:00", "setze pulse-work auf every 30min", "verschiebe team-briefing auf Samstag", "benenne X um in Y", "aendere Prompt von X zu ..." | UPDATE |
| DELETE | "loesche Timer X", "entferne pulse-work", "kill team-briefing" | DELETE |
Vor jeder ADD/UPDATE/DELETE-Operation einmal kurz pruefen, ob die Heartbeat-Pipeline aktiviert ist. Ohne Channel-Flag tickt der Server, schreibt Log, feuert -- und Claude bekommt nichts davon mit. Reine "timer.md eingetragen"-Bestaetigung waere irrefuehrend.
Lies still (nicht reporten) folgende Signale aus dem Projekt-Root (pwd):
| Signal | Wert |
|---|---|
.ai-heartbeat-timer/heartbeat.log existiert? | ja/nein |
.ai-heartbeat-timer/state.json existiert? | ja/nein |
timer.md existiert? | ja/nein |
System-Reminder-Block ## plugin:ai-heartbeat-timer:heartbeat in dieser Session? | ja/nein (Indikator dass Channel-Flag gesetzt ist) |
Onboarding-Modus ausloesen wenn ALLE drei zutreffen:
.ai-heartbeat-timer/heartbeat.log fehlt und.ai-heartbeat-timer/state.json fehlt undtimer.md fehlt oder Channel-Reminder-Block fehlt in der Session)Andernfalls direkt zum Intent-Block weiter unten springen.
Zeige vor der Bearbeitung der User-Anfrage genau diese Sequenz (deutsch, knapp, kein Smalltalk):
Heartbeat-Timer ist neu in diesem Projekt. Drei Dinge musst du machen, sonst
feuert kein einziger Timer (silent failure):
1) timer.md anlegen
cp "$(claude plugin root ai-heartbeat-timer)/templates/timer.md" ./timer.md
2) Claude Code mit Channel-Flag starten -- ohne das verwirft Claude
alle Heartbeat-Notifications. Beende diese Session und starte neu:
claude --dangerously-load-development-channels plugin:ai-heartbeat-timer@ai-plugins
(plus deine ueblichen Flags wie --dangerously-skip-permissions)
Permanent als Alias in ~/.zshrc:
alias claude-heartbeat='claude --dangerously-load-development-channels plugin:ai-heartbeat-timer@ai-plugins --dangerously-skip-permissions'
3) Bei claude.ai eingeloggt sein (/login). Channels brauchen OAuth-Token,
API-Key allein reicht nicht.
Verifikation: Nach dem Neustart sollte in den System-Remindern dieser
Session ein Block `## plugin:ai-heartbeat-timer:heartbeat` auftauchen. Wenn ja:
Channel ist aktiv, Timer feuern.
Volle Doku + Troubleshooting:
$(claude plugin root ai-heartbeat-timer)/README.md
Ich kann jetzt trotzdem deinen Timer in timer.md eintragen -- er feuert
dann, sobald du mit der richtigen Kommandozeile neu startest. Soll ich?
Wenn der User bestaetigt ("ja", "mach", "klar"): weiter zum Intent-Block (ADD/UPDATE/DELETE) -- aber am Ende der Bestaetigung explizit anmerken: "Wirksam ab Neustart mit Channel-Flag, siehe oben."
Wenn der User ablehnt oder erst die Aktivierung machen will: nichts an timer.md aendern, kurz bestaetigen "OK, wir machen weiter sobald du neugestartet hast."
Wenn timer.md existiert, aber der Channel-Reminder-Block fehlt in der Session: kurze Warnung (eine Zeile) vor der Bestaetigung am Ende:
⚠ Channel-Flag fehlt in dieser Session. Timer landet in timer.md, feuert
aber erst nach Neustart mit:
claude --dangerously-load-development-channels plugin:ai-heartbeat-timer@ai-plugins
Dann normal weiterarbeiten.
Zuerst aus der Eingabe den Intent bestimmen:
Fuer UPDATE und DELETE muss ein existierender Timer-Name genannt oder eindeutig ableitbar sein:
morgen-check steht so in timer.md)morgencheck → morgen-check)bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/validate.ts" --verboseExtrahiere aus der Beschreibung vier Felder. Wenn etwas unklar ist, genau eine Rueckfrage stellen (nicht eine Liste, nicht iterativ) und dann weitermachen.
Zeit -- welche Variante:
18:00, 07:00, 14:00, 19:00every 40min, alle 2h, every 2dErkennungsmuster (Beispiele):
18:0007:00, 14:00, 19:00every 40minevery 2hevery 2devery 1h (wenn <15min Gefahr besteht, frag nach)Tage -- welche:
weekdaysweekendmo, mi (oder monday, wednesday)sundayDefault bei nicht genanntem Tag: leer (daily). Nicht nachfragen.
Name -- leite aus dem Verb/Thema ab, nicht aus der Zeit:
tagesabschlusspulse-checkemail-reminder oder email-checkRegel: [a-z0-9-]+, Kleinbuchstaben, Bindestriche statt Leerzeichen. Bei Konflikt mit existierendem Namen in timer.md: Suffix -2, -3, ... anhaengen ohne Rueckfrage.
Prompt -- formuliere einen handlungsorientierten Auftrag fuer Claude:
| (durch Komma/Klammer ersetzen)End-of-day: Ask the user how today went (what was finished, what is still open). Record open items in context/TODOs.md.Format exakt:
| <Zeit> | <Tage> | <Name> | <Prompt> |
Einfuegen in timer.md im Abschnitt "## Aktive Timer" -- hinter der letzten bestehenden Timer-Zeile, vor einer eventuellen Leerzeile oder Dateiende.
Nutze Edit mit dem letzten existierenden Timer als old_string-Anker und haenge die neue Zeile dahinter.
bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/validate.ts"
OK (Exit 0) → weiter zu Schritt 5. FAIL (Exit 1) → Schritt 4.
Lies die Fehlermeldungen. Typische Faelle:
| Fehler | Fix |
|---|---|
name '...' invalid (allowed: [a-z0-9-]+) | Umlaute ersetzen (ä→ae), Gross→Klein, Underscore→Bindestrich. |
duplicate name | Name-Suffix -2 (oder hochzaehlen). |
hour 'N' out of range | "19 Uhr abends" war als 19 gemeint, nicht 7. Re-parse und probiere 19:00. "Mittags" = 12:00. "Abends" = 18-20. Wenn immer noch ambig: eine Rueckfrage. |
prompt only N chars | Prompt war zu kurz (oft weil Pipe drin war und abgeschnitten hat). Pipe entfernen, ausfuehrlicher formulieren. |
interval Nmin too short | User wollte <15min. Sag ihm: "Min 15min erlaubt -- nehme ich every 15min?" und mach weiter mit 15min. |
unknown day 'X' | Tippfehler oder Uebersetzungsluecke (z.B. tuesdag). Versuche Kurzform (di) oder nachfragen wenn wirklich unklar. |
duplicate time in time list | User hat Zeit doppelt genannt. Dedupen. |
row has N columns | Pipe-Zeichen im Prompt. Durch Komma/Semikolon ersetzen. |
Maximal 2 Auto-Korrektur-Runden. Wenn danach immer noch FAIL: Zeile wieder raus-editieren, an the user Bescheid geben mit konkretem Fehler und Vorschlag.
Kurz, eine Zeile plus die neue Tabellen-Zeile:
Timer eingetragen:
| <Zeit> | <Tage> | <Name> | <Prompt> |
Wirkt beim naechsten Minuten-Tick, feuert erstmals <wann konkret>.
"Wann konkret" = erstes Fire-Fenster berechnen:
Lies timer.md und extrahiere den Tabellen-Abschnitt "## Aktive Timer".
Suche nach dem Ziel-Timer-Namen:
name in der Tabellemorgencheck → morgen-check, Morgen Check → morgen-checkbun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/validate.ts" --verbose aufrufen, kanonische Liste der existierenden Namen zeigen, eine Rueckfrage: "Meintest du einen davon?"Aus der Eingabe die Delta extrahieren. Moegliche Felder: zeit, tage, name, prompt. Meist wird nur eins geaendert.
Sprachmuster:
| Muster | Bedeutet |
|---|---|
| "aendere X auf 8:00" / "setze X auf 8:00" / "verschiebe X auf 8:00" | zeit = 08:00 |
| "X auf every 30min" / "setze X auf alle 2h" | Zeit wird zu Intervall |
| "X laeuft ab jetzt nur werktags" / "aendere X auf weekdays" | tage = weekdays |
| "aendere X auf Do und Fr" | tage = do, fr |
| "benenne X um in Y" / "rename X to Y" | name = Y |
| "aendere Prompt von X zu ..." / "setze Prompt von X auf ..." | prompt = ... |
Namens-Aenderung: Der Name ist Identitaet im State-File. Bei Rename MUSS der State mitgenommen werden -- sonst sieht der neue Name "frisch" aus und ein gerade gefeuertes Intervall feuert sofort nochmal. → siehe Schritt 3a unten.
Kind-Switch (time-list ↔ interval): Wenn der Timer heute schon gefeuert hat und auf das andere Schedule-Kind umgestellt wird, muss der State angepasst werden damit nicht sofort nochmal gefeuert wird. → siehe Schritt 3a unten.
Mehrere Felder gleichzeitig: Wenn the user "setze X auf Di 9:00" sagt → Zeit UND Tag.
Unveraenderte Felder bleiben exakt so wie in der Datei (inkl. Whitespace und Tabelle-Padding nicht veraendern -- Edit ersetzt immer die ganze Zeile).
Neue Zeile: | <neu-oder-alt Zeit> | <...> | <...> | <...> |
Nutze Edit-Tool mit:
old_string = die komplette alte Tabellenzeile (exakter Text, inkl. Whitespace). Lies dafuer timer.md und kopiere die Zeile verbatim.new_string = die neue Zeile mit gleichem Separator-Stil.Nur diese eine Zeile anfassen -- nichts anderes in timer.md.
Der Runtime matched State per Name. Wenn sich der Name oder das Schedule-Kind aendert, muss .ai-heartbeat-timer/state.json nachgezogen werden -- sonst gibt's ueberraschende Doppel-Fires.
Bedingungen + Befehle:
| Aenderung | Befehl |
|---|---|
| Rename (Name alt → Name neu) | bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/state.ts" rename <old> <new> |
| Kind-Switch time-list → interval, Timer hat heute gefeuert | bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/state.ts" touch <name> -- setzt lastFire auf jetzt, damit das neue Intervall ab jetzt zaehlt |
| Kind-Switch interval → time-list | kein State-Command noetig (neue firedToday-Pruefung ist leer, lastFire bleibt orphan aber inert) |
| Nur Zeit/Tage/Prompt geaendert, kein Name/Kind-Wechsel | kein State-Command noetig |
Wie erkennen "Timer hat heute gefeuert" vor dem Switch:
bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/state.ts" show
Wenn in firedToday.<name> irgendwas drin steht ODER lastFire.<name> heute gesetzt wurde → heute ist gefeuert, also touch noetig beim Kind-Switch.
bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/validate.ts". Bei FAIL: gleiche Fix-Tabelle wie im ADD-Flow (Schritt 4 oben) benutzen. Maximal 2 Runden.
Wenn Auto-Korrektur versagt: Alte Zeile per Edit wiederherstellen (also erneut tauschen, new_string ← original). the user sagen was nicht geht + Vorschlag.
Timer 'X' aktualisiert:
vorher: | 07:00 | weekdays | morgen-check | ... |
nachher: | 08:00 | weekdays | morgen-check | ... |
Wirkt beim naechsten Minuten-Tick.
Bei Rename oder Kind-Switch: zusaetzlich erwaehnen was mit dem State passiert ist, z.B. "State migriert: <old> → <new>" oder "lastFire auf jetzt gesetzt, naechster Fire in ~30min".
Wie in UPDATE Schritt 1: exact → fuzzy → Rueckfrage bei Ambiguitaet.
Edit mit:
old_string = \n + komplette alte Tabellenzeile (inkl. fuehrendem Newline, damit keine Leerzeile zurueckbleibt)new_string = `` (leer)Nur diese Zeile. Keine anderen Aenderungen.
bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/state.ts" delete <name>
Warum: wird der Timer spaeter mit gleichem Namen und gleichem Slot wieder angelegt, darf der Runtime nicht denken "der hat heute schon gefeuert, suppress". Ohne diesen Schritt bliebe firedToday[name] bzw. lastFire[name] stale im State-File.
bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/validate.ts" -- sollte gruen bleiben (weniger Timer).
Timer 'X' geloescht, State bereinigt.
Noch aktiv: N Timer.
User: /ai-timer jeden werktag 9 uhr check emails
Parsing:
09:00weekdaysemail-check (aus "check emails")Email-Check: Read new emails from the last 24h, flag anything urgent, ask the user about anything unclear.Zeile: | 09:00 | weekdays | email-check | Email-Check: Lies neue Emails der letzten 24h ... |
User: /ai-timer alle 2h mich fragen ob ich pause mache
Parsing:
every 2hpause-nudgePause-Nudge: Ask the user whether they took a break in the last 2h. If not: a short, friendly reminder — not pushy.User: /ai-timer morgens, mittags, abends kalender check
Parsing:
08:00, 12:00, 18:00 (morgens/mittags/abends sind ambig; waehle konservativ, erklaer im Output)kalender-checkCalendar-Check: Scan today + tomorrow's calendar, name the next appointment and what needs preparing.User: /ai-timer alle 5 min ping
Parsing: every 5min -- aber <15min.
Reaktion: Nicht validieren, direkt sagen: "Min-Intervall 15 Minuten (sonst laeuft Claude ueber). Ich nehme every 15min -- OK?" und bei "OK"/keinem Einspruch: mit 15min weitermachen.
User: /ai-timer jeden morgen 7 uhr tagescheck aber morgen-check existiert schon mit 07:00 weekdays.
Reaktion: Name-Suffix. morgen-check-2. Keine Rueckfrage, the user kann umbenennen wenn er will.
User: aendere timer morgen-check auf 8:00
Intent: UPDATE (Signal: "aendere ... auf").
Ziel: morgen-check -- exact match vorhanden.
Delta: nur Zeit → 08:00. Tage/Name/Prompt bleiben.
Aktion:
timer.md, finde Zeile mit morgen-check.Edit mit old_string = komplette alte Zeile, new_string = gleiche Zeile mit 07:00 → 08:00.bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/validate.ts" → OK.Timer 'morgen-check' aktualisiert:
vorher: | 07:00 | weekdays | morgen-check | Morgen-Check: (1) Lies context/TODOs.md ... |
nachher: | 08:00 | weekdays | morgen-check | Morgen-Check: (1) Lies context/TODOs.md ... |
Wirkt beim naechsten Minuten-Tick. Feuert heute erstmals morgen 08:00 -- falls der Timer heute schon gefeuert hat, bleibt er heute ruhig (State-File).
User: setze team-briefing auf samstag 10:00
Intent: UPDATE. Ziel: team-briefing. Delta: Zeit=10:00, Tage=saturday.
User: benenne pulse-work um in focus-pulse
Intent: UPDATE. Delta: Name. Schritte:
pulse-work → focus-pulsebun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/state.ts" rename pulse-work focus-pulse -- nimmt firedToday/lastFire mitOhne den state-rename wuerde focus-pulse als "frischer" Timer gesehen und bei Intervallen sofort feuern -- Doppelfire kurz nach Rename.
User: aendere morgencheck auf 9:00
Fuzzy match: morgencheck → morgen-check (eindeutig, nur eine Kandidatin). Direkt weitermachen ohne Rueckfrage.
User: aendere pulse auf 15min -- es gibt pulse-work und pulse-2h.
Reaktion: Liste zeigen (aus validate.ts --verbose), genau eine Rueckfrage: "Welchen? pulse-work oder pulse-2h?"
User: loesche timer pulse-2h
Intent: DELETE. Ziel: pulse-2h.
Aktion:
Edit mit old_string = \n| <zeile mit pulse-2h> |, new_string = "".bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/state.ts" delete pulse-2h -- entfernt State-Spuren.bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/validate.ts" → OK.Timer 'pulse-2h' geloescht, State bereinigt. Noch aktiv: 4 Timer.User: Um 07:30 (nach Fire um 07:00): aendere morgen-check auf every 2h
Intent: UPDATE. Delta: Zeit 07:00 → every 2h. Kind-Switch!
Check: bun state.ts show zeigt firedToday.morgen-check = ["07:00"] → heute schon gefeuert.
Aktion:
07:00 auf every 2h aendern.bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/state.ts" touch morgen-check -- setzt lastFire auf jetzt (07:31), damit das Intervall ab jetzt zaehlt.every 2h. lastFire auf jetzt gesetzt, naechster Fire ca. 09:31."Ohne den touch wuerde Runtime sehen: kind=interval, lastFire leer → sofortiger Fire um 07:32, dann 09:32, ... statt 09:31.
bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/validate.ts" -v reicht -- kein Skill noetig, nur der Befehl.CronCreate mit recurring: false als Session-Alternative vor.bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/state.ts" delete <name> raeumt firedToday + lastFire weg. Kein Skill noetig -- einfach den Befehl laufen lassen und bestaetigen.Nach Abschluss muss gelten:
bun "${CLAUDE_PLUGIN_ROOT}/mcp/heartbeat-channel/validate.ts" ist grün| Zeit | Tage | Name | Prompt |.timer.md unveraendert.npx claudepluginhub bennoloeffler/claude-code-personal-agent-plugins-skills --plugin ai-heartbeat-timerCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.