clarify

Ask the user a question when you need clarification, feedback, or a decision before proceeding. Supports two modes:

1. **Multiple choice** — provide up to 4 choices. The user picks one or types their own answer via a 5th 'Other' option.
2. **Open-ended** — omit choices entirely. The user types a free-form response.

Use this tool when:
- The task is ambiguous and you need the user to choose an approach
- You want post-task feedback ('How did that work out?')
- You want to offer to save a skill or update memory
- A decision has meaningful trade-offs the user should weigh in on

Do NOT use this tool for simple yes/no confirmation of dangerous commands (the terminal tool handles that). Prefer making a reasonable default choice yourself when the decision is low-stakes.

The question to present to the user.

Choices:
Up to 4 answer choices. Omit this parameter entirely to ask an open-ended question. When provided, the UI automatically appends an 'Other (type your answer)' option.

cronjob

Manage scheduled cron jobs with a single compressed tool.

Use action='create' to schedule a new job from a prompt or one or more skills.
Use action='list' to inspect jobs.
Use action='update', 'pause', 'resume', 'remove', or 'run' to manage an existing job.

To stop a job the user no longer wants: first action='list' to find the job_id, then action='remove' with that job_id. Never guess job IDs — always list first.

Jobs run in a fresh session with no current-chat context, so prompts must be self-contained.
If skills are provided on create, the future cron run loads those skills in order, then follows the prompt as the task instruction.
On update, passing skills=[] clears attached skills.

NOTE: The agent's final response is auto-delivered to the target. Put the primary
user-facing content in the final response. Cron jobs run autonomously with no user
present — they cannot ask questions or request clarification.

Important safety rule: cron-run sessions should not recursively schedule more cron jobs.

One of: create, list, update, pause, resume, remove, run

Required for update/pause/resume/remove/run

For create: the full self-contained prompt. If skills are also provided, this becomes the task instruction paired with those skills.

For create/update: '30m', 'every 2h', '0 9 * * *', or ISO timestamp

Optional human-friendly name

Optional repeat count. Omit for defaults (once for one-shot, forever for recurring).

Omit this parameter to auto-deliver back to the current chat and topic (recommended). Auto-detection preserves thread/topic context. Only set explicitly when the user asks to deliver somewhere OTHER than the current conversation. Values: 'origin' (same as omitting), 'local' (no delivery, save only), or platform:chat_id:thread_id for a specific destination. Examples: 'telegram:-1001234567890:17585', 'discord:#engineering', 'sms:+15551234567'. WARNING: 'platform:chat_id' without :thread_id loses topic targeting.

Optional ordered list of skill names to load before executing the cron prompt. On update, pass an empty array to clear attached skills.

Optional per-job model override. If provider is omitted, the current main provider is pinned at creation time so the job stays stable.

Provider name (e.g. 'openrouter', 'anthropic', or 'custom:<name>' for a provider defined in custom_providers config — always include the ':<name>' suffix, never pass the bare 'custom'). Omit to use and pin the current provider.

Model name (e.g. 'anthropic/claude-sonnet-4', 'claude-sonnet-4')

Optional path to a script that runs each tick. In the default mode its stdout is injected into the agent's prompt as context (data-collection / change-detection pattern). With no_agent=True, the script IS the job and its stdout is delivered verbatim (classic watchdog pattern). Relative paths resolve under ~/.hermes/scripts/. ``.sh``/``.bash`` extensions run via bash, everything else via Python. On update, pass empty string to clear.

Default: False (LLM-driven job — the agent runs the prompt each tick). Set True to skip the LLM entirely: the scheduler just runs ``script`` on schedule and delivers its stdout verbatim. No tokens, no agent loop, no model override honoured.

REQUIREMENTS when True: ``script`` MUST be set (``prompt`` and ``skills`` are ignored).

DELIVERY SEMANTICS when True: (a) non-empty stdout is sent verbatim as the message; (b) EMPTY stdout means SILENT — nothing is sent to the user and they won't see anything happened, so design your script to stay quiet when there's nothing to report (the watchdog pattern); (c) non-zero exit / timeout sends an error alert so a broken watchdog can't fail silently.

WHEN TO USE True: recurring script-only pings where the script itself produces the exact message text (memory/disk/GPU watchdogs, threshold alerts, heartbeats, CI notifications, API pollers with a fixed output shape). WHEN TO USE False (default): anything that needs reasoning — summarize a feed, draft a daily briefing, pick interesting items, rephrase data for a human, follow conditional logic based on content.

Optional job ID or list of job IDs whose most recent completed output is injected into the prompt as context before each run. Use this to chain cron jobs: job A collects data, job B processes it. Each entry must be a valid job ID (from cronjob action='list'). Note: injects the most recent completed output — does not wait for upstream jobs running in the same tick. On update, pass an empty array to clear.

Optional list of toolset names to restrict the job's agent to (e.g. ["web", "terminal", "file", "delegation"]). When set, only tools from these toolsets are loaded, significantly reducing input token overhead. When omitted, all default tools are loaded. Infer from the job's prompt — e.g. use "web" if it calls web_search, "terminal" if it runs scripts, "file" if it reads files, "delegation" if it calls delegate_task. On update, pass an empty array to clear.

Optional absolute path to run the job from. When set, AGENTS.md / CLAUDE.md / .cursorrules from that directory are injected into the system prompt, and the terminal/file/code_exec tools use it as their working directory — useful for running a job inside a specific project repo. Must be an absolute path that exists. When unset (default), preserves the original behaviour: no project context files, tools use the scheduler's cwd. On update, pass an empty string to clear. Jobs with workdir run sequentially (not parallel) to keep per-job directories isolated.

Spawn one or more subagents to work on tasks in isolated contexts. Each subagent gets its own conversation, terminal session, and toolset. Only the final summary is returned -- intermediate tool results never enter your context window.

TWO MODES (one of 'goal' or 'tasks' is required):
1. Single task: provide 'goal' (+ optional context, toolsets)
2. Batch (parallel): provide 'tasks' array with up to delegation.max_concurrent_children items (default 3, configurable via config.yaml, no hard ceiling). All run concurrently and results are returned together. Nested delegation requires role='orchestrator' and delegation.max_spawn_depth >= 2.

WHEN TO USE delegate_task:
- Reasoning-heavy subtasks (debugging, code review, research synthesis)
- Tasks that would flood your context with intermediate data
- Parallel independent workstreams (research A and B simultaneously)

WHEN NOT TO USE (use these instead):
- Mechanical multi-step work with no reasoning needed -> use execute_code
- Single tool call -> just call the tool directly
- Tasks needing user interaction -> subagents cannot use clarify
- Durable long-running work that must outlive the current turn -> use cronjob (action='create') or terminal(background=True, notify_on_complete=True) instead. delegate_task runs SYNCHRONOUSLY inside the parent turn: if the parent is interrupted (user sends a new message, /stop, /new) the child is cancelled with status='interrupted' and its work is discarded. Children cannot continue in the background.

IMPORTANT:
- Subagents have NO memory of your conversation. Pass all relevant info (file paths, error messages, constraints) via the 'context' field.
- If the user is writing in a non-English language, or asked for output in a specific language / tone / style, say so in 'context' (e.g. "respond in Chinese", "return output in Japanese"). Otherwise subagents default to English and their summaries will contaminate your final reply with the wrong language.
- Subagent summaries are SELF-REPORTS, not verified facts. A subagent that claims "uploaded successfully" or "file written" may be wrong. For operations with external side-effects (HTTP POST/PUT, remote writes, file creation at shared paths, publishing), require the subagent to return a verifiable handle (URL, ID, absolute path, HTTP status) and verify it yourself — fetch the URL, stat the file, read back the content — before telling the user the operation succeeded.
- Leaf subagents (role='leaf', the default) CANNOT call: delegate_task, clarify, memory, send_message, execute_code.
- Orchestrator subagents (role='orchestrator') retain delegate_task so they can spawn their own workers, but still cannot use clarify, memory, send_message, or execute_code. Orchestrators are bounded by delegation.max_spawn_depth (default 2) and can be disabled globally via delegation.orchestrator_enabled=false.
- Each subagent gets its own terminal session (separate working directory and state).
- Results are always returned as an array, one entry per task.

What the subagent should accomplish. Be specific and self-contained -- the subagent knows nothing about your conversation history.

Background information the subagent needs: file paths, error messages, project structure, constraints. The more specific you are, the better the subagent performs.

Toolsets to enable for this subagent. Default: inherits your enabled toolsets. Available toolsets: 'browser', 'cronjob', 'discord', 'discord_admin', 'feishu_doc', 'feishu_drive', 'file', 'homeassistant', 'image_gen', 'kanban', 'search', 'session_search', 'skills', 'spotify', 'terminal', 'todo', 'tts', 'video', 'vision', 'web', 'yuanbao'. Common patterns: ['terminal', 'file'] for code work, ['web'] for research, ['browser'] for web interaction, ['terminal', 'file', 'web'] for full-stack tasks.

Task goal

Task-specific context

Toolsets for this specific task. Available: 'browser', 'cronjob', 'discord', 'discord_admin', 'feishu_doc', 'feishu_drive', 'file', 'homeassistant', 'image_gen', 'kanban', 'search', 'session_search', 'skills', 'spotify', 'terminal', 'todo', 'tts', 'video', 'vision', 'web', 'yuanbao'. Use 'web' for network access, 'terminal' for shell, 'browser' for web interaction.

Per-task ACP command override (e.g. 'claude'). Overrides the top-level acp_command for this task only.

Per-task ACP args override.

Per-task role override. See top-level 'role' for semantics.

Batch mode: tasks to run in parallel (limit configurable via delegation.max_concurrent_children, default 3). Each gets its own subagent with isolated context and terminal session. When provided, top-level goal/context/toolsets are ignored.

Role of the child agent. 'leaf' (default) = focused worker, cannot delegate further. 'orchestrator' = can use delegate_task to spawn its own workers. Requires delegation.max_spawn_depth >= 2 in config; ignored (treated as 'leaf') when the child would exceed max_spawn_depth or when delegation.orchestrator_enabled=false.

Override ACP command for child agents (e.g. 'claude', 'copilot'). When set, children use ACP subprocess transport instead of inheriting the parent's transport. Enables spawning Claude Code (claude --acp --stdio) or other ACP-capable agents from any parent, including Discord/Telegram/CLI.

Arguments for the ACP command (default: ['--acp', '--stdio']). Only used when acp_command is set. Example: ['--acp', '--stdio', '--model', 'claude-opus-4-6']

Run a Python script that can call Hermes tools programmatically. Use this when you need 3+ tool calls with processing logic between them, need to filter/reduce large tool outputs before they enter your context, need conditional branching (if X then Y else Z), or need to loop (fetch N pages, process N files, retry on failure).

Use normal tool calls instead when: single tool call with no processing, you need to see the full result and apply complex reasoning, or the task requires interactive user input.

Available via `from hermes_tools import ...`:

  read_file(path: str, offset: int = 1, limit: int = 500) -> dict
    Lines are 1-indexed. Returns {"content": "...", "total_lines": N}
  write_file(path: str, content: str) -> dict
    Always overwrites the entire file.
  search_files(pattern: str, target="content", path=".", file_glob=None, limit=50) -> dict
    target: "content" (search inside files) or "files" (find files by name). Returns {"matches": [...]}
  patch(path: str, old_string: str, new_string: str, replace_all: bool = False) -> dict
    Replaces old_string with new_string in the file.
  terminal(command: str, timeout=None, workdir=None) -> dict
    Foreground only (no background/pty). Returns {"output": "...", "exit_code": N}

Limits: 5-minute timeout, 50KB stdout cap, max 50 tool calls per script. terminal() is foreground-only (no background or pty).

Scripts run in the session's working directory with the active venv's python, so project deps (pandas, etc.) and relative paths work like in terminal().

Print your final result to stdout. Use Python stdlib (json, re, math, csv, datetime, collections, etc.) for processing between tool calls.

Also available (no import needed — built into hermes_tools):
  json_parse(text: str) — json.loads with strict=False; use for terminal() output with control chars
  shell_quote(s: str) — shlex.quote(); use when interpolating dynamic strings into shell commands
  retry(fn, max_attempts=3, delay=2) — retry with exponential backoff for transient failures

Python code to execute. Import tools with `from hermes_tools import terminal, ...` and print your final result to stdout.

memory

Save durable information to persistent memory that survives across sessions. Memory is injected into future turns, so keep it compact and focused on facts that will still matter later.

WHEN TO SAVE (do this proactively, don't wait to be asked):
- User corrects you or says 'remember this' / 'don't do that again'
- User shares a preference, habit, or personal detail (name, role, timezone, coding style)
- You discover something about the environment (OS, installed tools, project structure)
- You learn a convention, API quirk, or workflow specific to this user's setup
- You identify a stable fact that will be useful again in future sessions

PRIORITY: User preferences and corrections > environment facts > procedural knowledge. The most valuable memory prevents the user from having to repeat themselves.

Do NOT save task progress, session outcomes, completed-work logs, or temporary TODO state to memory; use session_search to recall those from past transcripts.
If you've discovered a new way to do something, solved a problem that could be necessary later, save it as a skill with the skill tool.

TWO TARGETS:
- 'user': who the user is -- name, role, preferences, communication style, pet peeves
- 'memory': your notes -- environment facts, project conventions, tool quirks, lessons learned

ACTIONS: add (new entry), replace (update existing -- old_text identifies it), remove (delete -- old_text identifies it).

SKIP: trivial/obvious info, things easily re-discovered, raw data dumps, and temporary task state.

The action to perform.

Which memory store: 'memory' for personal notes, 'user' for user profile.

The entry content. Required for 'add' and 'replace'.

Short unique substring identifying the entry to replace or remove.

patch

Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal. Uses fuzzy matching (9 strategies) so minor whitespace/indentation differences won't break it. Returns a unified diff. Auto-runs syntax checks after editing.

Replace mode (default): find a unique string and replace it.
Patch mode: apply V4A multi-file patches for bulk changes.

Edit mode: 'replace' for targeted find-and-replace, 'patch' for V4A multi-file patches

File path to edit (required for 'replace' mode)

Text to find in the file (required for 'replace' mode). Must be unique in the file unless replace_all=true. Include enough surrounding context to ensure uniqueness.

Replacement text (required for 'replace' mode). Can be empty string to delete the matched text.

Replace all occurrences instead of requiring a unique match (default: false)

V4A format patch content (required for 'patch' mode). Format:
*** Begin Patch
*** Update File: path/to/file
@@ context hint @@
 context line
-removed line
+added line
*** End Patch

process

Manage background processes started with terminal(background=true). Actions: 'list' (show all), 'poll' (check status + new output), 'log' (full output with pagination), 'wait' (block until done or timeout), 'kill' (terminate), 'write' (send raw stdin data without newline), 'submit' (send data + Enter, for answering prompts), 'close' (close stdin/send EOF).

list poll log wait kill write submit close

Action to perform on background processes

Process session ID (from terminal background output). Required for all actions except 'list'.

Text to send to process stdin (for 'write' and 'submit' actions)

Max seconds to block for 'wait' action. Returns partial output on timeout.

Line offset for 'log' action (default: last 200 lines)

Max lines to return for 'log' action

Read a text file with line numbers and pagination. Use this instead of cat/head/tail in terminal. Output format: 'LINE_NUM|CONTENT'. Suggests similar filenames if not found. Use offset and limit for large files. Reads exceeding ~100K characters are rejected; use offset and limit to read specific sections of large files. NOTE: Cannot read images or binary files — use vision_analyze for images.

Path to the file to read (absolute, relative, or ~/path)

Line number to start reading from (1-indexed, default: 1)

Maximum number of lines to read (default: 500, max: 2000)

Search file contents or find files by name. Use this instead of grep/rg/find/ls in terminal. Ripgrep-backed, faster than shell equivalents.

Content search (target='content'): Regex search inside files. Output modes: full matches with line numbers, file paths only, or match counts.

File search (target='files'): Find files by glob pattern (e.g., '*.py', '*config*'). Also use this instead of ls — results sorted by modification time.

Regex pattern for content search, or glob pattern (e.g., '*.py') for file search

'content' searches inside file contents, 'files' searches for files by name

Directory or file to search in (default: current working directory)

Filter files by pattern in grep mode (e.g., '*.py' to only search Python files)

Maximum number of results to return (default: 50)

Skip first N results for pagination (default: 0)

Output format for grep mode: 'content' shows matching lines with line numbers, 'files_only' lists file paths, 'count' shows match counts per file

Number of context lines before and after each match (grep mode only)

Send a message to a connected messaging platform, or list available targets.

IMPORTANT: When the user asks to send to a specific channel or person (not just a bare platform name), call send_message(action='list') FIRST to see available targets, then send to the correct one.
If the user just says a platform name like 'send to telegram', send directly to the home channel without listing first.

Action to perform. 'send' (default) sends a message. 'list' returns all available channels/contacts across connected platforms.

Delivery target. Format: 'platform' (uses home channel), 'platform:#channel-name', 'platform:chat_id', or 'platform:chat_id:thread_id' for Telegram topics and Discord threads. Examples: 'telegram', 'telegram:-1001234567890:17585', 'discord:999888777:555444333', 'discord:#bot-home', 'slack:#engineering', 'signal:+155****4567', 'matrix:!roomid:server.org', 'matrix:@user:server.org', 'yuanbao:direct:<account_id>' (DM), 'yuanbao:group:<group_code>' (group chat)

The message text to send. To send an image or file, include MEDIA:<local_path> (e.g. 'MEDIA:/tmp/hermes/cache/img_xxx.jpg') in the message — the platform will deliver it as a native media attachment.

Search your long-term memory of past conversations, or browse recent sessions. This is your recall -- every past session is searchable, and this tool summarizes what happened.

TWO MODES:
1. Recent sessions (no query): Call with no arguments to see what was worked on recently. Returns titles, previews, and timestamps. Zero LLM cost, instant. Start here when the user asks what were we working on or what did we do recently.
2. Keyword search (with query): Search for specific topics across all past sessions. Returns LLM-generated summaries of matching sessions.

USE THIS PROACTIVELY when:
- The user says 'we did this before', 'remember when', 'last time', 'as I mentioned'
- The user asks about a topic you worked on before but don't have in current context
- The user references a project, person, or concept that seems familiar but isn't in memory
- You want to check if you've solved a similar problem before
- The user asks 'what did we do about X?' or 'how did we fix Y?'

Don't hesitate to search when it is actually cross-session -- it's fast and cheap. Better to search and confirm than to guess or ask the user to repeat themselves.

Search syntax: keywords joined with OR for broad recall (elevenlabs OR baseten OR funding), phrases for exact match ("docker networking"), boolean (python NOT java), prefix (deploy*). IMPORTANT: Use OR between keywords for best results — FTS5 defaults to AND which misses sessions that only mention some terms. If a broad OR query returns nothing, try individual keyword searches in parallel. Returns summaries of the top matching sessions.

Search query — keywords, phrases, or boolean expressions to find in past sessions. Omit this parameter entirely to browse recent sessions instead (returns titles, previews, timestamps with no LLM cost).

Optional: only search messages from specific roles (comma-separated). E.g. 'user,assistant' to skip tool outputs.

Max sessions to summarize (default: 3, max: 5).

Manage skills (create, update, delete). Skills are your procedural memory — reusable approaches for recurring task types. New skills go to ~/.hermes/skills/; existing skills can be modified wherever they live.

Actions: create (full SKILL.md + optional category), patch (old_string/new_string — preferred for fixes), edit (full SKILL.md rewrite — major overhauls only), delete, write_file, remove_file.

On delete, pass `absorbed_into=<umbrella>` when you're merging this skill's content into another one, or `absorbed_into=""` when you're pruning it with no forwarding target. This lets the curator tell consolidation from pruning without guessing, so downstream consumers (cron jobs that reference the old skill name, etc.) get updated correctly. The target you name in `absorbed_into` must already exist — create/patch the umbrella first, then delete.

Create when: complex task succeeded (5+ calls), errors overcome, user-corrected approach worked, non-trivial workflow discovered, or user asks you to remember a procedure.
Update when: instructions stale/wrong, OS-specific failures, missing steps or pitfalls found during use. If you used a skill and hit issues not covered by it, patch it immediately.

After difficult/iterative tasks, offer to save as a skill. Skip for simple one-offs. Confirm with user before creating/deleting.

Good skills: trigger conditions, numbered steps with exact commands, pitfalls section, verification steps. Use skill_view() to see format examples.

Pinned skills are protected from deletion only — skill_manage(action='delete') will refuse with a message pointing the user to `hermes curator unpin <name>`. Patches and edits go through on pinned skills so you can still improve them as pitfalls come up; pin only guards against irrecoverable loss.

create patch edit delete write_file remove_file

The action to perform.

Skill name (lowercase, hyphens/underscores, max 64 chars). Must match an existing skill for patch/edit/delete/write_file/remove_file.

Full SKILL.md content (YAML frontmatter + markdown body). Required for 'create' and 'edit'. For 'edit', read the skill first with skill_view() and provide the complete updated text.

Text to find in the file (required for 'patch'). Must be unique unless replace_all=true. Include enough surrounding context to ensure uniqueness.

Replacement text (required for 'patch'). Can be empty string to delete the matched text.

For 'patch': replace all occurrences instead of requiring a unique match (default: false).

Optional category/domain for organizing the skill (e.g., 'devops', 'data-science', 'mlops'). Creates a subdirectory grouping. Only used with 'create'.

Path to a supporting file within the skill directory. For 'write_file'/'remove_file': required, must be under references/, templates/, scripts/, or assets/. For 'patch': optional, defaults to SKILL.md if omitted.

Content for the file. Required for 'write_file'.

For 'delete' only — declares intent so the curator can tell consolidation from pruning without guessing. Pass the umbrella skill name when this skill's content was merged into another (the target must already exist). Pass an empty string when the skill is truly stale and being pruned with no forwarding target. Omitting the arg on delete is supported for backward compatibility but downstream tooling (e.g. cron-job skill reference rewriting) will have to guess at intent.

Skills allow for loading information about specific tasks and workflows, as well as scripts and templates. Load a skill's full content or access its linked files (references, templates, scripts). First call returns SKILL.md content plus a 'linked_files' dict showing available references/templates/scripts. To access those, call again with file_path parameter.

The skill name (use skills_list to see available skills). For plugin-provided skills, use the qualified form 'plugin:skill' (e.g. 'superpowers:writing-plans').

OPTIONAL: Path to a linked file within the skill (e.g., 'references/api.md', 'templates/config.yaml', 'scripts/validate.py'). Omit to get the main SKILL.md content.

List available skills (name + description). Use skill_view(name) to load full content.

Optional category filter to narrow results

terminal

Execute shell commands on a Linux environment. Filesystem usually persists between calls.

Do NOT use cat/head/tail to read files — use read_file instead.
Do NOT use grep/rg/find to search — use search_files instead.
Do NOT use ls to list directories — use search_files(target='files') instead.
Do NOT use sed/awk to edit files — use patch instead.
Do NOT use echo/cat heredoc to create files — use write_file instead.
Reserve terminal for: builds, installs, git, processes, scripts, network, package managers, and anything that needs a shell.

Foreground (default): Commands return INSTANTLY when done, even if the timeout is high. Set timeout=300 for long builds/scripts — you'll still get the result in seconds if it's fast. Prefer foreground for short commands.
Background: Set background=true to get a session_id. Two patterns:
  (1) Long-lived processes that never exit (servers, watchers).
  (2) Long-running tasks with notify_on_complete=true — you can keep working on other things and the system auto-notifies you when the task finishes. Great for test suites, builds, deployments, or anything that takes more than a minute.
For servers/watchers, do NOT use shell-level background wrappers (nohup/disown/setsid/trailing '&') in foreground mode. Use background=true so Hermes can track lifecycle and output.
After starting a server, verify readiness with a health check or log signal, then run tests in a separate terminal() call. Avoid blind sleep loops.
Use process(action="poll") for progress checks, process(action="wait") to block until done.
Working directory: Use 'workdir' for per-command cwd.
PTY mode: Set pty=true for interactive CLI tools (Codex, Claude Code, Python REPL).

Do NOT use vim/nano/interactive tools without pty=true — they hang without a pseudo-terminal. Pipe git output to cat if it might page.

The command to execute on the VM

Run the command in the background. Two patterns: (1) Long-lived processes that never exit (servers, watchers). (2) Long-running tasks paired with notify_on_complete=true — you can keep working and get notified when the task finishes. For short commands, prefer foreground with a generous timeout instead.

Max seconds to wait (default: 180, foreground max: 600). Returns INSTANTLY when command finishes — set high for long tasks, you won't wait unnecessarily. Foreground timeout above 600s is rejected; use background=true for longer commands.

Working directory for this command (absolute path). Defaults to the session working directory.

Run in pseudo-terminal (PTY) mode for interactive CLI tools like Codex, Claude Code, or Python REPL. Only works with local and SSH backends. Default: false.

When true (and background=true), you'll be automatically notified exactly once when the process finishes. **This is the right choice for almost every long-running task** — tests, builds, deployments, multi-item batch jobs, anything that takes over a minute and has a defined end. Use this and keep working on other things; the system notifies you on exit. MUTUALLY EXCLUSIVE with watch_patterns — when both are set, watch_patterns is dropped.

Strings to watch for in background process output. HARD RATE LIMIT: at most 1 notification per 15 seconds per process — matches arriving inside the cooldown are dropped. After 3 consecutive 15-second windows with dropped matches, watch_patterns is automatically disabled for that process and promoted to notify_on_complete behavior (one notification on exit, no more mid-process spam). USE ONLY for truly rare, one-shot mid-process signals on LONG-LIVED processes that will never exit on their own — e.g. ['Application startup complete'] on a server so you know when to hit its endpoint, or ['migration done'] on a daemon. DO NOT use for: (1) end-of-run markers like 'DONE'/'PASS' — use notify_on_complete instead; (2) error patterns like 'ERROR'/'Traceback' in loops or multi-item batch jobs — they fire on every iteration and you'll hit the strike limit fast; (3) anything you'd ever combine with notify_on_complete. When in doubt, choose notify_on_complete. MUTUALLY EXCLUSIVE with notify_on_complete — set one, not both.

Convert text to speech audio. Returns a MEDIA: path that the platform delivers as native audio. Compatible providers render as a voice bubble on Telegram; otherwise audio is sent as a regular attachment. In CLI mode, saves to ~/voice-memos/. Voice and provider are user-configured (built-in providers like edge/openai or custom command providers under tts.providers.<name>), not model-selected.

The text to convert to speech. Provider-specific character caps apply and are enforced automatically (OpenAI 4096, xAI 15000, MiniMax 10000, ElevenLabs 5k-40k depending on model); over-long input is truncated.

Optional custom file path to save the audio. Defaults to ~/.hermes/audio_cache/<timestamp>.mp3

todo

Manage your task list for the current session. Use for complex tasks with 3+ steps or when the user provides multiple tasks. Call with no parameters to read the current list.

Writing:
- Provide 'todos' array to create/update items
- merge=false (default): replace the entire list with a fresh plan
- merge=true: update existing items by id, add any new ones

Each item: {id: string, content: string, status: pending|in_progress|completed|cancelled}
List order is priority. Only ONE item in_progress at a time.
Mark items completed immediately when done. If something fails, cancel it and add a revised item.

Always returns the full current list.

Task items to write. Omit to read current list.

Unique item identifier

Task description

Current status

true: update existing items by id, add new ones. false (default): replace the entire list.

Inspect an image from a URL, file path, or tool output when you need closer detail than what's visible in the conversation. If the user's image is already attached to the conversation and you can see it, just answer directly — only call this tool for images referenced by URL/path, images returned inside other tool results (browser screenshots, search thumbnails), or when you need a deeper look at a specific region the main model's vision may have missed.

Image URL (http/https) or local file path to analyze.

Your specific question or request about the image to resolve. The AI will automatically provide a complete image description AND answer your specific question.

Write content to a file, completely replacing existing content. Use this instead of echo/cat heredoc in terminal. Creates parent directories automatically. OVERWRITES the entire file — use 'patch' for targeted edits. Auto-runs syntax checks on .py/.json/.yaml/.toml and other linted languages; only NEW errors introduced by this write are surfaced (pre-existing errors are filtered out).

Path to the file to write (will be created if it doesn't exist, overwritten if it does)

Complete content to write to the file


Kannst du bei Daily Dose jeden Tag bitte einfügen Körperpflege mit 30 min, das soll ab jetzt bitte automatisch jeden Tag sein


Explanation:
The user wants me to add "Körperpflege" (personal hygiene) with 30 minutes to their "Daily Dose" every day, automatically. Let me first search for any existing "Daily Dose" setup — this could be a cron job, a routine, a calendar entry, or something in their Obsidian vault.

Let me check:
1. Existing cron jobs
2. Session history for "Daily Dose"
3. Maybe it's an Obsidian note or a Google Calendar thing

Let me search in parallel.

Question: Daily Dose OR Körperpflege OR routine


cronjob

DailyDose Reminder

Sende eine freundliche Erinnerungs-Nachricht auf Deutsch: Erinnere Zeyd daran, seine DailyDose für h...


Question: Daily Dose OR Körperpflege OR routine

Summary:
**Factual Recap — May 15, 2026 at 12:45 PM (Telegram)**

**1. User’s goal (as it relates to the search topic)**
The conversation did not contain any discussion of **Körperpflege** or **routine**. The only search-term match was **DailyDose**, which appeared at the very end of the session. After spending most of the conversation cleaning up movie and series filenames on the HDD, the user asked the assistant to set up a **cronjob** that would remind them to fill out **DailyDose** every day at **09:00, 16:00, and 22:00**.

**2. Actions taken and outcomes**
- **Media-library cleanup (preceding context).** The user first asked (via voice) whether there were new movies whose titles did not match other posts. The assistant searched prior sessions and the local filesystem, found no post history, and then located the actual media on the HDD:
  - Movies: `/media/HDD_1TB/Medien/Filme/` (30 files)
  - Series: `/media/HDD_1TB/Medien/Serien/` (3 shows: *Gibi*, *Sherlock*, *Typisch Andy*)
- **Filename standardization.** The assistant renamed 9 films to remove `T00` rip-artifacts, replace umlauts with ASCII equivalents (e.g., `ü` → `ue`), and swap spaces for underscores. Example: `Alter_Weißer_Mann.mp4` → `Alter_Weisser_Mann.mp4`; `Catch Me If You Can T00.mp4` → `Catch_Me_If_You_Can.mp4`. The remaining films were already in a clean format.
- **Series folder restructuring.** Series were reorganized into per-season subfolders with underscores in filenames:
  - `Sherlock/` → `Sherlock/S01/Sherlock_S01E01.mp4`
  - `Typisch Andy/` → `Typisch Andy/S01/…`, `S02/…`, `S03/…` (all renamed to `Typisch_Andy_SxxExx.mp4`)
  - `Gibi/` already had `S01/`, `S05/`, `S06/` structure and was left as-is.
- **DailyDose investigation.** After the media work, the user requested the DailyDose reminder cronjob. The assistant inspected the existing DailyDose project folder at **`/media/HDD_1TB/DailyDose/`**, finding:
  - `index.html`
  - `manifest.json`
  - `icon.svg` & `icon.png`
  - `nginx.conf`
- **Session history lookup.** The assistant ran `session_search` with the query **`DailyDose OR daily dose OR Tagebuch OR journal`**. One prior session was returned—**`20260511_094554_273678fc` (May 11, 2026 at 09:45 AM)**—but its summary was marked **“Raw preview — summarization unavailable”**, so no actionable prior context could be extracted.

**3. Key decisions, solutions found, or conclusions reached**
- Media naming convention agreed upon: strict ASCII, underscores as word separators, no rip-tool artifacts (`T00`) or umlauts.
- DailyDose is an existing local web application (evidenced by `index.html`, `manifest.json`, and `nginx.conf`), but its purpose or prior configuration could not be retrieved from the truncated session history.

**4. Important technical details preserved**
- **Paths:**
  - `/media/HDD_1TB/Medien/Filme/`
  - `/media/HDD_1TB/Medien/Serien/`
  - `/media/HDD_1TB/DailyDose/`
- **DailyDose directory contents:** `index.html`, `manifest.json`, `icon.svg`, `icon.png`, `nginx.conf`
- **Requested cronjob schedule:** 9:00, 16:00, 22:00 daily (reminder to fill out DailyDose).

**5. Anything left unresolved or notable**
- The transcript was **truncated immediately after** the DailyDose filesystem inspection and session search. It is **not shown whether the cronjob was successfully created** or what the DailyDose application actually does.
- No mention of **Körperpflege** or **routine** appeared anywhere in the conversation.

Summary:
**Search topic match:** None. The transcript did not contain references to "Daily Dose," "Körperpflege," or any daily/routine-related topic.

---

**What the user wanted to accomplish**
The user (Ahmed, via Telegram on May 13, 2026) asked about two recurring system error messages ("⚠ Auxiliary title generation failed: Request timed out" and "⏳ Still working... (3 min elapsed — iteration 2/90, waiting for stream response)"). In a voice message, he then asked whether the assistant could be made more efficient to prevent such timeouts. Later, he sent a selfie and asked "Wer ist das?".

**Actions taken and outcomes**
- The assistant loaded the `hermes-agent` skill and inspected the user's `~/.hermes/config.yaml`.
- It identified that auxiliary tasks were either set to `auto` (causing hangs) or misconfigured to use the heavy main model `kimi-k2.6` for lightweight background jobs.
- The assistant patched the config directly (the `hermes` CLI was not in PATH) to bind every auxiliary task to explicit, small Ollama models:
  - **Main chat:** kept as `kimi-k2.6` via custom provider at `https://ollama.com/v1`.
  - **Compression:** reverted to `kimi-k2.6` after an internal warning that `qwen2.5:1.5b`'s 32k context window was below the 64k minimum required by Hermes.
  - **All other auxiliaries** (title_generation, session_search, skills_hub, approval, mcp, web_extract, curator): set to `qwen2.5:1.5b` with 60–300s timeouts.
  - **Vision:** initially set to `llava-phi3`, but when the user later uploaded an image, `vision_analyze` returned a 404 (`"model \"llava-phi3\" not found"`). The assistant queried the Ollama endpoint's model list, confirmed `llava-phi3` was missing, and switched vision to `gemma3:4b`, after which image analysis succeeded.
  - **Approvals:** changed from `manual` to `smart`.
- The assistant also queried the remote Ollama API (`curl https://ollama.com/v1/models`) to verify available models after a local Ollama check (`localhost:11434`) failed because the local service was not running.
- Finally, the assistant analyzed the user's selfie (`/DATA/.hermes/cache/images/img_0b280c8d0ba3.jpg`) using `gemma3:4b` and described a young man with dark curly hair, a mustache, and a coral-colored shirt in a room with framed pictures on the wall.

**Key decisions, solutions, and conclusions**
- Root cause of timeouts: the `auto` auxiliary provider setting and using a large model for trivial background tasks.
- Decision: keep the large model only for the main chat and compression; route all other auxiliaries through `qwen2.5:1.5b` for speed.
- Decision: substitute `gemma3:4b` for the missing `llava-phi3` vision model.
- Conclusion: after the config rewrite and a pending session restart, the timeout messages were expected to disappear.

**Important technical details**
- **Config file:** `~/.hermes/config.yaml`
- **API endpoint:** `https://ollama.com/v1`
- **API key visible in config output:** `eaf9ec548a9f4f789cc2eeb0d7cc61e3.VI7OQTAxEEGB1fqyil_1oJob`
- **Final auxiliary assignments:**
  - `compression`: `kimi-k2.6`
  - `vision`: `gemma3:4b`
  - `title_generation`, `session_search`, `skills_hub`, `approval`, `mcp`, `web_extract`, `curator`: `qwen2.5:1.5b`
- **Approval mode:** `smart`
- **Image analyzed:** `/DATA/.hermes/cache/images/img_0b280c8d0ba3.jpg`
- **Execution note:** `hermes` CLI was not found in PATH (`/usr/bin/bash: line 3: hermes: command not found`), so changes were applied via inline Python config edits.

**Unresolved / notable**
- A session restart (`/restart` or gateway restart) was still required for all auxiliary changes to take full effect.
- The user's Ollama endpoint did not host `llava-phi3`, forcing a last-minute model substitution for vision tasks.
- The conversation ended with the successful image analysis after all optimization tasks had been confirmed complete.

Summary:
**Factual Recap — May 11, 2026 Telegram Session (~08:49–09:45)**

**1. User’s Goal**
Ahmed Zeyd asked Kiwi (the Hermes Agent assistant) to read and analyze the **“Official Hermes Agent AMA”** Reddit thread that was stored locally at `/tmp/reddit_api.json`. He wanted the key takeaways extracted, with special interest in two items from a preliminary summary:
- **Point 3:** Honcho Memory (behavior-observing memory system)
- **Point 6:** Operating rules posted by Reddit user `u/setthatonfire` for autonomous agent behavior

**2. Actions & Outcomes**
- **Reddit thread parsing:** Kiwi first read only the first 80 comment blocks and reported them. Ahmed noticed the truncation (“178 total comment blocks shown”). Kiwi then re-parsed the JSON exhaustively and confirmed **189 total comment blocks** were extracted, including deeply nested replies.
- **Top-10 learnings delivered:** Kiwi summarized ten actionable findings (best local model = Qwen3.6-27B, vision routing fix, curator system, auxiliary-model configuration, AGENTS.md rules, Tailscale usage, self-dogfooding stats, model-collapse risk, upcoming Web UI, etc.).
- **AGENTS.md rules written:** Ahmed approved the rules. Kiwi created `/DATA/AppData/hermes/AGENTS.md` containing the Reddit rules plus an added “Non-technical user rule” requiring proactive plain-language explanation of any system-resource changes (storage, RAM, installs). File size: 835 bytes.
- **Honcho Memory investigated:**
  - `hermes memory status` showed Honcho installed as a plugin but **not active**; provider was `(none — built-in only)`.
  - Ahmed asked to activate it. Kiwi ran `hermes config set memory.provider honcho`, which updated `config.yaml`.
  - Subsequent status check revealed Honcho was now marked as the active provider, but **unavailable** because the `HONCHO_API_KEY` was missing (or a local DB container was not running). Activation therefore stalled.
- **Full environment reconnaissance:** At the end of the visible transcript Kiwi performed a broad read of all memories, skills, sessions, and the work directory to establish complete context.

**3. Key Decisions & Solutions**
- Adopted the Reddit “stop-only-for” safety rules into the persistent system prompt (`AGENTS.md`), customized for a non-technical user who wants autonomous action but with guardrails on destructive/config changes.
- Determined that **Honcho cannot be used yet** without an API key or local backend setup.
- Confirmed the agent’s working environment: Hermes Home at `/DATA/AppData/hermes`, running on ZimaOS (Linux 6.12.25), user `az-a`, with a user-local venv-based installation and a systemd gateway service.

**4. Important Technical Details**
- **Paths:** `/DATA/AppData/hermes/AGENTS.md`, `/DATA/AppData/hermes/config.yaml`, `/tmp/reddit_api.json`
- **Commands used:**
  - `hermes memory status`
  - `hermes config set memory.provider honcho`
  - `find /DATA/AppData/hermes -name "AGENTS.md"`
  - `execute_code` Python blocks for JSON parsing of Reddit comments
- **Configuration:** Model provider set to `kimi-k2.6` via `kimi-coding` provider; Telegram gateway enabled for user `8744435286`.
- **Work-folder contents noted by Kiwi:** `gridfinity_pick_holder` (3D model), `bathroom_3d.html` / `2d.svg`, `Sukh-Podcast-Transkript`, `KW 20/26 Word-Dokument`.

**5. Unresolved / Notable**
- **Honcho Memory remains inactive** pending a `HONCHO_API_KEY` or local Docker/DB setup. Kiwi explicitly told Ahmed the built-in memory will have to suffice for now.
- The transcript is **truncated at both ends** (“earlier conversation truncated” / “later conversation truncated”), so the exact start of the May 11 session and any final follow-ups are missing.
- **Search-topic mismatch:** The terms **“Daily Dose,” “Körperpflege,”** or **“routine”** do not appear in the visible portion of this transcript. The conversation was exclusively focused on Hermes Agent configuration, Reddit AMA analysis, and memory/behavior rules. If those terms were relevant, they likely appeared in a different session or in the truncated segments not included here.


Explanation:
Let me understand what the user wants. They have a DailyDose reminder cronjob that fires at 9:00, 16:00, and 22:00. They want me to add "Körperpflege mit 30 min" (personal hygiene/body care for 30 minutes) to the Daily Dose every day, and they want this to be automatic going forward.

Let me first look at the existing cronjob's prompt to understand what it currently does, and then figure out how to modify it.

The cronjob has:
- job_id: "3efe39511aff"
- name: "DailyDose Reminder"
- schedule: "0 9,16,22 * * *"
- prompt_preview: "Sende eine freundliche Erinnerungs-Nachricht auf Deutsch: Erinnere Zeyd daran, seine DailyDose f\u00fcr h..."

I need to see the full prompt to know what exactly is being sent. Let me also look at what "DailyDose" is - the session search shows it's a web app at /media/HDD_1TB/DailyDose/.

But wait - the user says "bei Daily Dose jeden Tag einfügen Körperpflege mit 30 min". This could mean two things:
1. Modify the DailyDose reminder message to include Körperpflege
2. Modify the DailyDose web app to include Körperpflege as a default/template entry

Let me first check the full prompt of the cronjob and also look at the DailyDose app.


terminal

{"output": "cat: /DATA/AppData/hermes/.cron/jobs/3efe39511aff.json: No such file or directory", "exit_code": 1, "error": null}

[Subdirectory context discovered: AppData/hermes/AGENTS.md]
# Operating Rules — Kiwi

## Default operating rule
If the path is clear, do the work and report after.
Do not make the user keep re-approving momentum.

## Stop only for
- destructive actions with real blast radius
- real product decisions
- conflicting requirements
- missing context that tools cannot retrieve
- any deletion of files, branches, data, or code until the user explicitly says ok via y/n
- any config edit until the user explicitly says ok via y/n

## Execution rule
- Bundle obvious next steps.
- Prefer action over recap.
- Use tools instead of describing what you would do.

## Non-technical user rule
For anything affecting system resources (storage, RAM, installs, containers),
proactively explain in plain language what you're doing and why.
Do not silently consume disk space or RAM. Ask if a path is unclear.


     1|<!DOCTYPE html>
     2|<html lang="de">
     3|<head>
     4|  <meta charset="UTF-8" />
     5|  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
     6|  <link rel="manifest" href="/manifest.json" />
     7|  <meta name="apple-mobile-web-app-capable" content="yes" />
     8|  <meta name="apple-mobile-web-app-status-bar-style" content="default" />
     9|  <meta name="apple-mobile-web-app-title" content="DailyDose" />
    10|  <title>DailyDose</title>
    11|  <link rel="apple-touch-icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='22' fill='%23F5C518'/><text y='73' x='50' font-size='64' font-weight='bold' text-anchor='middle' fill='%231C1C1E' font-family='system-ui,-apple-system'>D</text></svg>" />
    12|  <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='22' fill='%23F5C518'/><text y='73' x='50' font-size='64' font-weight='bold' text-anchor='middle' fill='%231C1C1E' font-family='system-ui,-apple-system'>D</text></svg>">
    13|  <style>
    14|    :root {
    15|      --accent:       #F5C518;
    16|      --accent-glow:  rgba(245,197,24,0.18);
    17|      --accent-dim:   rgba(245,197,24,0.10);
    18|
    19|      --bg:           #F2F2F7;
    20|      --bg-card:      rgba(255,255,255,0.75);
    21|      --bg-input:     rgba(255,255,255,0.92);
    22|      --bg-entry:     rgba(255,255,255,0.62);
    23|      --bg-hole:      #FFFFFF;
    24|
    25|      --border:       rgba(0,0,0,0.07);
    26|      --text-primary: #1C1C1E;
    27|      --text-secondary:#6E6E73;
    28|      --text-muted:   #AEAEB2;
    29|      --text-del:     #C7C7CC;
    30|      --sep:          #F2F2F7;
    31|
    32|      --shadow-card:  0 2px 28px rgba(0,0,0,0.07), 0 1px 4px rgba(0,0,0,0.04);
    33|      --shadow-btn:   0 4px 16px rgba(245,197,24,0.38);
    34|      --r-card: 20px;
    35|      --r-el:   12px;
    36|    }
    37|
    38|    @media (prefers-color-scheme: dark) {
    39|      :root {
    40|        --bg:           #1C1C1E;
    41|        --bg-card:      rgba(44,44,46,0.82);
    42|        --bg-input:     rgba(58,58,60,0.92);
    43|        --bg-entry:     rgba(58,58,60,0.62);
    44|        --bg-hole:      #2C2C2E;
    45|        --border:       rgba(255,255,255,0.07);
    46|        --text-primary: #F5F5F7;
    47|        --text-secondary:#98989D;
    48|        --text-muted:   #636366;
    49|        --text-del:     #48484A;
    50|        --sep:          #1C1C1E;
    51|        --shadow-card:  0 2px 28px rgba(0,0,0,0.38), 0 1px 4px rgba(0,0,0,0.22);
    52|        --shadow-btn:   0 4px 18px rgba(245,197,24,0.26);
    53|      }
    54|    }
    55|
    56|    * { box-sizing: border-box; margin: 0; padding: 0; }
    57|
    58|    body {
    59|      font-family: -apple-system, 'SF Pro Display', BlinkMacSystemFont, system-ui, sans-serif;
    60|      background: var(--bg);
    61|      color: var(--text-primary);
    62|      min-height: 100vh;
    63|      min-height: -webkit-fill-available;
    64|      -webkit-font-smoothing: antialiased;
    65|      display: flex;
    66|      flex-direction: column;
    67|      overscroll-behavior-y: none;
    68|      -webkit-tap-highlight-color: transparent;
    69|      padding-top: env(safe-area-inset-top);
    70|    }
    71|
    72|    main { flex: 1; padding: 24px 16px 24px; padding-left: max(16px, env(safe-area-inset-left)); padding-right: max(16px, env(safe-area-inset-right)); }
    73|
    74|    .container, .footer-container {
    75|      max-width: 480px;
    76|      margin: 0 auto;
    77|    }
    78|
    79|    /* ─── Header ─── */
    80|    .app-header {
    81|      text-align: center;
    82|      margin-bottom: 28px;
    83|    }
    84|
    85|    .header-pill {
    86|      display: inline-flex;
    87|      align-items: center;
    88|      gap: 6px;
    89|      background: var(--accent);
    90|      color: #1C1C1E;
    91|      font-size: 0.68rem;
    92|      font-weight: 700;
    93|      letter-spacing: 1.1px;
    94|      text-transform: uppercase;
    95|      padding: 4px 12px;
    96|      border-radius: 50px;
    97|      margin-bottom: 12px;
    98|      box-shadow: var(--shadow-btn);
    99|    }
   100|
   101|    .app-title {
   102|      font-size: 2.3rem;
   103|      font-weight: 700;
   104|      letter-spacing: -1px;
   105|      line-height: 1.1;
   106|    }
   107|
   108|    .app-title span { color: var(--accent); }
   109|
   110|    /* ─── Date Nav ─── */
   111|    .date-nav {
   112|      display: flex;
   113|      align-items: center;
   114|      justify-content: center;
   115|      gap: 10px;
   116|      margin: 18px 0 0;
   117|    }
   118|
   119|    .nav-btn {
   120|      width: 44px;
   121|      height: 44px;
   122|      border-radius: 50%;
   123|      border: 1.5px solid var(--border);
   124|      background: var(--bg-card);
   125|      backdrop-filter: blur(12px);
   126|      -webkit-backdrop-filter: blur(12px);
   127|      color: var(--text-primary);
   128|      font-size: 1.1rem;
   129|      cursor: pointer;
   130|      display: flex;
   131|      align-items: center;
   132|      justify-content: center;
   133|      transition: background 0.15s, opacity 0.15s, transform 0.12s;
   134|      box-shadow: var(--shadow-card);
   135|    }
   136|
   137|    .nav-btn:hover:not(:disabled) { background: var(--accent-dim); }
   138|    .nav-btn:active:not(:disabled) { transform: scale(0.91); }
   139|    .nav-btn:disabled { opacity: 0.22; cursor: default; }
   140|
   141|    .date-label {
   142|      font-size: 0.92rem;
   143|      font-weight: 600;
   144|      color: var(--text-primary);
   145|      min-width: 220px;
   146|      text-align: center;
   147|      letter-spacing: -0.2px;
   148|    }
   149|
   150|    .date-label .date-sub {
   151|      display: block;
   152|      font-size: 0.7rem;
   153|      font-weight: 500;
   154|      color: var(--text-muted);
   155|      margin-top: 2px;
   156|    }
   157|
   158|    /* ─── Card ─── */
   159|    .card {
   160|      background: var(--bg-card);
   161|      backdrop-filter: blur(24px);
   162|      -webkit-backdrop-filter: blur(24px);
   163|      border: 1px solid var(--border);
   164|      border-radius: var(--r-card);
   165|      padding: 20px 18px;
   166|      margin-bottom: 14px;
   167|      box-shadow: var(--shadow-card);
   168|    }
   169|
   170|    .card-title {
   171|      font-size: 0.7rem;
   172|      font-weight: 600;
   173|      text-transform: uppercase;
   174|      letter-spacing: 0.9px;
   175|      color: var(--text-muted);
   176|      margin-bottom: 14px;
   177|    }
   178|
   179|    /* ─── Input ─── */
   180|    select, input[type="number"], input[type="text"] {
   181|      background: var(--bg-input);
   182|      border: 1.5px solid var(--border);
   183|      border-radius: var(--r-el);
   184|      color: var(--text-primary);
   185|      font-size: 16px;
   186|      font-family: inherit;
   187|      padding: 12px 14px;
   188|      outline: none;
   189|      transition: border-color 0.18s, box-shadow 0.18s;
   190|      -webkit-appearance: none;
   191|    }
   192|
   193|    select:focus, input[type="number"]:focus, input[type="text"]:focus {
   194|      border-color: var(--accent);
   195|      box-shadow: 0 0 0 3px var(--accent-glow);
   196|    }
   197|
   198|    /* Zeile 1: Kategorie + neue-Kategorie-Button */
   199|    .input-top-row {
   200|      display: flex;
   201|      gap: 8px;
   202|      align-items: stretch;
   203|      margin-bottom: 10px;
   204|    }
   205|
   206|    select {
   207|      flex: 1;
   208|      cursor: pointer;
   209|      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='7' viewBox='0 0 11 7'%3E%3Cpath d='M1 1l4.5 4.5L10 1' stroke='%23AEAEB2' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
   210|      background-repeat: no-repeat;
   211|      background-position: right 13px center;
   212|      padding-right: 34px;
   213|    }
   214|
   215|    .new-cat-btn {
   216|      width: 48px;
   217|      flex-shrink: 0;
   218|      background: var(--bg-input);
   219|      border: 1.5px solid var(--border);
   220|      border-radius: var(--r-el);
   221|      color: var(--text-secondary);
   222|      font-size: 1.4rem;
   223|      cursor: pointer;
   224|      display: flex;
   225|      align-items: center;
   226|      justify-content: center;
   227|      transition: background 0.15s, color 0.15s;
   228|    }
   229|    .new-cat-btn:active { background: var(--accent-dim); color: var(--accent); }
   230|
   231|    /* Zeile 2: Zeit + Add */
   232|    .input-bottom-row {
   233|      display: flex;
   234|      gap: 8px;
   235|      align-items: stretch;
   236|    }
   237|
   238|    .time-grp {
   239|      flex: 1;
   240|      display: flex;
   241|      align-items: center;
   242|      background: var(--bg-input);
   243|      border: 1.5px solid var(--border);
   244|      border-radius: var(--r-el);
   245|      transition: border-color 0.18s, box-shadow 0.18s;
   246|      overflow: hidden;
   247|    }
   248|
   249|    .time-grp:focus-within {
   250|      border-color: var(--accent);
   251|      box-shadow: 0 0 0 3px var(--accent-glow);
   252|    }
   253|
   254|    .time-grp input[type="number"] {
   255|      flex: 1;
   256|      min-width: 0;
   257|      text-align: center;
   258|      font-weight: 600;
   259|      background: none;
   260|      border: none;
   261|      box-shadow: none;
   262|      padding: 12px 4px 12px 12px;
   263|    }
   264|
   265|    .time-grp input[type="number"]:focus { border-color: transparent; box-shadow: none; }
   266|
   267|    .time-lbl {
   268|      font-size: 0.8rem;
   269|      font-weight: 600;
   270|      color: var(--text-muted);
   271|      padding-right: 12px;
   272|      flex-shrink: 0;
   273|    }
   274|
   275|    input[type="number"]::-webkit-inner-spin-button,
   276|    input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; }
   277|    input[type="number"] { -moz-appearance: textfield; }
   278|    input[type="number"]::placeholder { color: var(--text-muted); font-weight: 400; }
   279|
   280|    .add-btn {
   281|      background: var(--accent);
   282|      border: none;
   283|      border-radius: var(--r-el);
   284|      color: #1C1C1E;
   285|      font-size: 1.5rem;
   286|      font-weight: 700;
   287|      width: 56px;
   288|      flex-shrink: 0;
   289|      display: flex;
   290|      align-items: center;
   291|      justify-content: center;
   292|      box-shadow: var(--shadow-btn);
   293|      transition: transform 0.13s, opacity 0.15s;
   294|    }
   295|
   296|    .add-btn:active { transform: scale(0.91); }
   297|    .add-btn:hover  { opacity: 0.86; }
   298|
   299|    /* ─── Custom-Kategorie Bottom Sheet ─── */
   300|    .sheet-overlay {
   301|      display: none;
   302|      position: fixed;
   303|      inset: 0;
   304|      background: rgba(0,0,0,0.4);
   305|      z-index: 100;
   306|      backdrop-filter: blur(4px);
   307|      -webkit-backdrop-filter: blur(4px);
   308|    }
   309|
   310|    .sheet-overlay.open { display: block; }
   311|
   312|    .cat-sheet {
   313|      position: fixed;
   314|      bottom: 0;
   315|      left: 0; right: 0;
   316|      background: var(--bg-card);
   317|      backdrop-filter: blur(24px);
   318|      -webkit-backdrop-filter: blur(24px);
   319|      border-radius: 24px 24px 0 0;
   320|      padding: 12px 20px calc(24px + env(safe-area-inset-bottom));
   321|      z-index: 101;
   322|      transform: translateY(100%);
   323|      transition: transform 0.35s cubic-bezier(0.4,0,0.2,1);
   324|      box-shadow: 0 -4px 40px rgba(0,0,0,0.18);
   325|    }
   326|
   327|    .cat-sheet.open { transform: translateY(0); }
   328|
   329|    .sheet-handle {
   330|      width: 36px; height: 4px;
   331|      background: var(--border);
   332|      border-radius: 99px;
   333|      margin: 0 auto 20px;
   334|    }
   335|
   336|    .sheet-title {
   337|      font-size: 1.05rem;
   338|      font-weight: 700;
   339|      text-align: center;
   340|      margin-bottom: 20px;
   341|      letter-spacing: -0.3px;
   342|    }
   343|
   344|    .emoji-row {
   345|      display: flex;
   346|      align-items: center;
   347|      gap: 12px;
   348|      margin-bottom: 12px;
   349|    }
   350|
   351|    .emoji-input {
   352|      width: 64px !important;
   353|      height: 64px !important;
   354|      text-align: center !important;
   355|      font-size: 2rem !important;
   356|      padding: 0 !important;
   357|      border-radius: 16px !important;
   358|      flex-shrink: 0;
   359|    }
   360|
   361|    .name-input { flex: 1; }
   362|
   363|    .color-grid {
   364|      display: grid;
   365|      grid-template-columns: repeat(10, 1fr);
   366|      gap: 8px;
   367|      margin: 14px 0 20px;
   368|    }
   369|
   370|    .color-swatch {
   371|      width: 100%;
   372|      aspect-ratio: 1;
   373|      border-radius: 50%;
   374|      border: 2.5px solid transparent;
   375|      cursor: pointer;
   376|      transition: transform 0.12s, border-color 0.12s;
   377|    }
   378|
   379|    .color-swatch.selected { border-color: var(--text-primary); transform: scale(1.15); }
   380|    .color-swatch:active   { transform: scale(0.9); }
   381|
   382|    .sheet-actions {
   383|      display: flex;
   384|      gap: 10px;
   385|    }
   386|
   387|    .sheet-btn {
   388|      flex: 1;
   389|      padding: 14px;
   390|      border: none;
   391|      border-radius: var(--r-el);
   392|      font-size: 1rem;
   393|      font-weight: 600;
   394|      font-family: inherit;
   395|      cursor: pointer;
   396|      transition: opacity 0.15s;
   397|    }
   398|
   399|    .sheet-btn:active { opacity: 0.75; }
   400|    .sheet-btn.cancel { background: var(--bg-input); color: var(--text-secondary); }
   401|    .sheet-btn.save   { background: var(--accent); color: #1C1C1E; box-shadow: var(--shadow-btn); }
   402|
   403|    /* ─── Entries ─── */
   404|    .entries {
   405|      display: flex;
   406|      flex-direction: column;
   407|      margin-top: 13px;
   408|      border-radius: var(--r-el);
   409|      overflow: hidden;
   410|      border: 1px solid var(--border);
   411|    }
   412|
   413|    .entries:empty { display: none; }
   414|
   415|    .entry {
   416|      display: flex;
   417|      align-items: center;
   418|      gap: 9px;
   419|      background: var(--bg-entry);
   420|      padding: 10px 13px;
   421|      border-bottom: 1px solid var(--border);
   422|      animation: slideIn 0.17s ease;
   423|    }
   424|
   425|    .entry:last-child { border-bottom: none; }
   426|
   427|    .entry-bar { width: 3px; height: 26px; border-radius: 2px; flex-shrink: 0; }
   428|    .entry-icon { font-size: 0.95rem; width: 20px; text-align: center; }
   429|
   430|    .entry-label { flex: 1; font-size: 0.88rem; font-weight: 500; }
   431|
   432|    .entry-hours {
   433|      font-size: 0.8rem;
   434|      color: var(--text-secondary);
   435|      font-variant-numeric: tabular-nums;
   436|    }
   437|
   438|    .entry-pct {
   439|      font-size: 0.76rem;
   440|      font-weight: 600;
   441|      width: 38px;
   442|      text-align: right;
   443|      font-variant-numeric: tabular-nums;
   444|    }
   445|
   446|    .entry-del {
   447|      background: none;
   448|      border: none;
   449|      color: var(--text-del);
   450|      cursor: pointer;
   451|      font-size: 0.82rem;
   452|      width: 36px;
   453|      height: 36px;
   454|      border-radius: 50%;
   455|      margin: -8px -4px -8px 0;
   456|      display: flex;
   457|      align-items: center;
   458|      justify-content: center;
   459|      transition: background 0.13s, color 0.13s;
   460|      flex-shrink: 0;
   461|    }
   462|
   463|    .entry-del:hover { background: rgba(255,59,48,0.12); color: #FF3B30; }
   464|
   465|    /* ─── Total ─── */
   466|    .total-bar-row {
   467|      margin-top: 13px;
   468|      display: flex;
   469|      justify-content: space-between;
   470|      align-items: center;
   471|      font-size: 0.78rem;
   472|    }
   473|
   474|    .total-bar-row .used { color: var(--text-secondary); }
   475|    .total-bar-row .left { color: var(--accent); font-weight: 600; }
   476|    .total-bar-row .over { color: #FF3B30; font-weight: 600; }
   477|
   478|    .progress-track {
   479|      margin-top: 9px;
   480|      height: 3px;
   481|      background: var(--border);
   482|      border-radius: 99px;
   483|      overflow: hidden;
   484|    }
   485|
   486|    .progress-fill {
   487|      height: 100%;
   488|      border-radius: 99px;
   489|      background: var(--accent);
   490|      transition: width 0.4s cubic-bezier(0.4,0,0.2,1);
   491|    }
   492|
   493|    .progress-fill.over { background: #FF3B30; }
   494|
   495|    /* ─── Chart ─── */
   496|    .chart-wrap {
   497|      display: flex;
   498|      flex-direction: column;
   499|      align-items: center;
   500|      gap: 22px;
   501|

Use offset=501 to continue reading (showing 1-500 of 1480 lines)


Explanation:
Let me understand the situation:

1. There's an existing DailyDose Reminder cron job (ID: `3efe39511aff`) that runs at 9:00, 16:00, and 22:00 daily
2. The user wants "Körperpflege" (personal hygiene) with 30 minutes to be added every day automatically

The DailyDose app is a time-tracking web app at `/media/HDD_1TB/DailyDose/`. The user tracks time spent on activities. He wants Körperpflege (30 min) to be added automatically every day.

Let me look deeper into the DailyDose app to understand its data storage mechanism and the cron job's full prompt to understand what it currently does. Then I need to figure out how to add Körperpflege with 30 min to it.

Let me check:
1. How DailyDose stores data (localStorage? IndexedDB?)
2. The full prompt of the cron job
3. The manifest.json for more context

Let me also check the cron job's stored data more carefully.


501|    }
   502|
   503|    .canvas-container {
   504|      position: relative;
   505|      width: min(290px, 80vw);
   506|      height: min(290px, 80vw);
   507|    }
   508|
   509|    canvas#pie {
   510|      width: 100%;
   511|      height: 100%;
   512|      display: block;
   513|    }
   514|
   515|    .center-label {
   516|      position: absolute;
   517|      inset: 0;
   518|      display: flex;
   519|      flex-direction: column;
   520|      align-items: center;
   521|      justify-content: center;
   522|      pointer-events: none;
   523|    }
   524|
   525|    .center-h {
   526|      font-size: 1.6rem;
   527|      font-weight: 700;
   528|      letter-spacing: -0.5px;
   529|      line-height: 1;
   530|    }
   531|
   532|    .center-sub {
   533|      font-size: 0.68rem;
   534|      color: var(--text-muted);
   535|      margin-top: 4px;
   536|    }
   537|
   538|    /* ─── Legend ─── */
   539|    .legend {
   540|      display: flex;
   541|      flex-direction: column;
   542|      width: 100%;
   543|      border-radius: var(--r-el);
   544|      overflow: hidden;
   545|      border: 1px solid var(--border);
   546|    }
   547|
   548|    .legend-item {
   549|      display: flex;
   550|      align-items: center;
   551|      gap: 9px;
   552|      padding: 9px 13px;
   553|      background: var(--bg-entry);
   554|      border-bottom: 1px solid var(--border);
   555|      font-size: 0.83rem;
   556|    }
   557|
   558|    .legend-item:last-child { border-bottom: none; }
   559|
   560|    .legend-dot  { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
   561|    .legend-name { flex: 1; font-weight: 500; }
   562|    .legend-val  { color: var(--text-secondary); font-size: 0.77rem; font-variant-numeric: tabular-nums; }
   563|    .legend-pct  { font-size: 0.77rem; font-weight: 600; width: 38px; text-align: right; font-variant-numeric: tabular-nums; }
   564|
   565|    /* ─── Empty ─── */
   566|    .empty-state { text-align: center; padding: 32px 0 14px; }
   567|    .empty-icon  { font-size: 2.2rem; opacity: 0.3; margin-bottom: 8px; }
   568|    .empty-state p { color: var(--text-muted); font-size: 0.85rem; }
   569|
   570|    /* ─── Stats Footer ─── */
   571|    footer {
   572|      padding: 0 16px calc(32px + env(safe-area-inset-bottom));
   573|      padding-left: max(16px, env(safe-area-inset-left));
   574|      padding-right: max(16px, env(safe-area-inset-right));
   575|    }
   576|
   577|    .stats-header {
   578|      display: flex;
   579|      align-items: center;
   580|      justify-content: space-between;
   581|      margin-bottom: 16px;
   582|      gap: 12px;
   583|    }
   584|
   585|    .stats-title {
   586|      font-size: 0.7rem;
   587|      font-weight: 600;
   588|      text-transform: uppercase;
   589|      letter-spacing: 0.9px;
   590|      color: var(--text-muted);
   591|      white-space: nowrap;
   592|    }
   593|
   594|    /* ─── Segmented Control ─── */
   595|    .seg-ctrl {
   596|      display: flex;
   597|      background: var(--bg-input);
   598|      border: 1px solid var(--border);
   599|      border-radius: 10px;
   600|      padding: 3px;
   601|      gap: 2px;
   602|    }
   603|
   604|    .seg-btn {
   605|      background: none;
   606|      border: none;
   607|      border-radius: 8px;
   608|      font-size: 0.76rem;
   609|      font-weight: 500;
   610|      font-family: inherit;
   611|      color: var(--text-secondary);
   612|      padding: 5px 13px;
   613|      cursor: pointer;
   614|      transition: background 0.15s, color 0.15s;
   615|      white-space: nowrap;
   616|    }
   617|
   618|    .seg-btn.active {
   619|      background: var(--accent);
   620|      color: #1C1C1E;
   621|      font-weight: 700;
   622|      box-shadow: 0 1px 4px rgba(0,0,0,0.1);
   623|    }
   624|
   625|    .seg-btn:not(.active):hover {
   626|      background: var(--accent-dim);
   627|      color: var(--text-primary);
   628|    }
   629|
   630|    /* ─── Stats summary row ─── */
   631|    .stats-summary {
   632|      font-size: 0.75rem;
   633|      color: var(--text-muted);
   634|      margin-bottom: 14px;
   635|    }
   636|
   637|    .stats-summary b { color: var(--accent); font-weight: 700; }
   638|
   639|    /* ─── Stats rows ─── */
   640|    .stats-rows {
   641|      display: flex;
   642|      flex-direction: column;
   643|      gap: 11px;
   644|    }
   645|
   646|    .stats-row {
   647|      display: flex;
   648|      align-items: center;
   649|      gap: 9px;
   650|    }
   651|
   652|    .stats-icon { font-size: 0.9rem; width: 20px; text-align: center; flex-shrink: 0; }
   653|
   654|    .stats-name {
   655|      font-size: 0.82rem;
   656|      font-weight: 500;
   657|      width: 80px;
   658|      flex-shrink: 0;
   659|      color: var(--text-primary);
   660|    }
   661|
   662|    .stats-bar-wrap {
   663|      flex: 1;
   664|      height: 6px;
   665|      background: var(--border);
   666|      border-radius: 99px;
   667|      overflow: hidden;
   668|      min-width: 0;
   669|    }
   670|
   671|    .stats-bar-fill {
   672|      height: 100%;
   673|      border-radius: 99px;
   674|      transition: width 0.5s cubic-bezier(0.4,0,0.2,1);
   675|    }
   676|
   677|    .stats-val {
   678|      font-size: 0.73rem;
   679|      color: var(--text-secondary);
   680|      width: 40px;
   681|      text-align: right;
   682|      font-variant-numeric: tabular-nums;
   683|      flex-shrink: 0;
   684|    }
   685|
   686|    .stats-pct {
   687|      font-size: 0.73rem;
   688|      font-weight: 600;
   689|      width: 34px;
   690|      text-align: right;
   691|      font-variant-numeric: tabular-nums;
   692|      flex-shrink: 0;
   693|    }
   694|
   695|    .stats-empty {
   696|      text-align: center;
   697|      color: var(--text-muted);
   698|      font-size: 0.83rem;
   699|      padding: 14px 0 6px;
   700|    }
   701|
   702|    /* ─── Footer credit ─── */
   703|    .divider {
   704|      height: 1px;
   705|      background: var(--border);
   706|      margin: 20px 0;
   707|    }
   708|
   709|    .credit {
   710|      text-align: center;
   711|      font-size: 0.75rem;
   712|      color: var(--text-muted);
   713|    }
   714|
   715|    .credit strong { color: var(--text-secondary); font-weight: 600; }
   716|
   717|    /* ─── Zeitgeist Sync Badge ─── */
   718|    .card-title-row {
   719|      display: flex;
   720|      align-items: center;
   721|      justify-content: space-between;
   722|      margin-bottom: 14px;
   723|    }
   724|    .card-title-row .card-title { margin-bottom: 0; }
   725|
   726|    .zg-badge {
   727|      font-size: 0.67rem;
   728|      font-weight: 600;
   729|      padding: 3px 9px;
   730|      border-radius: 99px;
   731|      letter-spacing: 0.4px;
   732|      transition: all 0.2s;
   733|      display: flex;
   734|      align-items: center;
   735|      gap: 4px;
   736|    }
   737|    .zg-badge:empty { display: none; }
   738|    .zg-badge.syncing { background: var(--accent-dim); color: var(--accent); }
   739|    .zg-badge.ok      { background: rgba(50,215,75,0.12); color: #32D74B; }
   740|    .zg-badge.error   { background: rgba(255,59,48,0.10); color: #FF3B30; }
   741|
   742|    /* ─── Animations ─── */
   743|    @keyframes slideIn {
   744|      from { opacity: 0; transform: translateY(5px); }
   745|      to   { opacity: 1; transform: translateY(0); }
   746|    }
   747|
   748|    @keyframes fadeSwap {
   749|      from { opacity: 0; transform: translateX(10px); }
   750|      to   { opacity: 1; transform: translateX(0); }
   751|    }
   752|
   753|    .date-anim { animation: fadeSwap 0.2s ease; }
   754|  </style>
   755|
   756|
   757|
   758|
   759|

   760|
   761|
   762|
   763|
✦ Daily Tracker

   764|
DailyDose

   765|
   766|

   767|      <button class="nav-btn" id="btn-prev" onclick="changeDate(-1)" title="Vorheriger Tag">‹</button>
   768|

   769|
   770|
   771|

   772|      <button class="nav-btn" id="btn-next" onclick="changeDate(1)" title="Nächster Tag">›</button>
   773|

   774|
   775|
   776|
   777|

   778|

   779|      Aktivität hinzufügen
   780|
   781|

   782|
   783|
   784|

   785|      <select id="category"></select>
   786|      <button class="new-cat-btn" onclick="openCatSheet()" title="Neue Kategorie">＋</button>
   787|

   788|
   789|
   790|

   791|

   792|        <input type="number" id="hours" min="0" max="24" step="1" placeholder="0" />
   793|        h
   794|

   795|

   796|        <input type="number" id="minutes" min="0" max="59" step="5" placeholder="0" />
   797|        min
   798|

   799|      <button class="add-btn" onclick="addEntry()">+</button>
   800|

   801|
   802|

   803|
   804|

   805|

   806|
   807|
   808|

   809|

   810|

   811|

   812|

   813|

   814|
   815|
   816|

   817|
Tagesübersicht

   818|

   819|

   820|
🌅

   821|
Noch keine Aktivitäten für diesen Tag.

   822|

   823|

   824|        <canvas id="pie" width="580" height="580"></canvas>
   825|

   826|          0h
   827|          eingetragen
   828|

   829|

   830|

   831|

   832|

   833|
   834|

   835|
   836|
   837|
   838|
   839|

   840|

   841|

   842|        Statistik
   843|

   844|          <button class="seg-btn active" onclick="setPeriod(7,this)">Woche</button>
   845|          <button class="seg-btn" onclick="setPeriod(30,this)">Monat</button>
   846|          <button class="seg-btn" onclick="setPeriod(365,this)">Jahr</button>
   847|

   848|

   849|

   850|

   851|
Noch keine Daten.

   852|

   853|

   854|
   855|

   856|
Made with ♥ by Ahmed Zeyd Aytac

   857|

   858|
   859|
   860|
   861|

   862|

   863|

   864|
Neue Kategorie

   865|

   866|    <input type="text" class="emoji-input" id="cat-emoji" placeholder="😊" maxlength="2" />
   867|    <input type="text" class="name-input" id="cat-name" placeholder="Name" />
   868|

   869|

   870|

   871|    <button class="sheet-btn cancel" onclick="closeCatSheet()">Abbrechen</button>
   872|    <button class="sheet-btn save" onclick="saveNewCat()">Speichern</button>
   873|

   874|

   875|
   876|

Use offset=1001 to continue reading (showing 501-1000 of 1480 lines)


     1|{
     2|  "name": "DailyDose",
     3|  "short_name": "DailyDose",
     4|  "description": "Täglicher Aktivitäten-Tracker",
     5|  "start_url": "/",
     6|  "display": "standalone",
     7|  "orientation": "portrait",
     8|  "theme_color": "#F5C518",
     9|  "background_color": "#F2F2F7",
    10|  "icons": [
    11|    {
    12|      "src": "/icon.svg",
    13|      "sizes": "any",
    14|      "type": "image/svg+xml",
    15|      "purpose": "any maskable"
    16|    }
    17|  ]
    18|}
    19|


Explanation:
Let me understand the situation:

1. DailyDose is a PWA (web app) at `/media/HDD_1TB/DailyDose/` - a daily activity tracker with categories like Schlafen, Lernen, Work, Hausarbeit, Kochen, Islam, Sport, Unterwegs, Sonstiges.

2. There's an existing cronjob `3efe39511aff` (DailyDose Reminder) that sends reminders at 9:00, 16:00, and 22:00 to fill out DailyDose.

3. The user wants "Körperpflege" (personal hygiene) added with 30 minutes to be automatically logged every day.

Now, the DailyDose app stores data in localStorage (browser-based PWA). I can't directly modify localStorage from the server side. But I can look at how the app works.

The app has a DD_API fetch endpoint for syncing custom categories. Data is stored in localStorage. The entries for each day are likely stored as `dd_YYYY-MM-DD` or similar keys.

The user wants me to set up a system where "Körperpflege" with 30 minutes is automatically added every day.

Options:
1. I can modify the HTML app to auto-add Körperpflege 30min every day
2. I can set up a script that somehow injects the data
3. I can create a cronjob that modifies the localStorage-equivalent data

Since DailyDose is a client-side PWA, the data lives in the browser's localStorage. The sync is done via a POST to DD_API. Let me look at the rest of the index.html to understand the API and data format better.

Let me check what the full app does, particularly the API endpoints and data storage format.


1001|  let dateOffset  = 0;
  1002|  let statsPeriod = 7;
  1003|
  1004|  /* ─── Date helpers ─── */
  1005|  function getDate(offset) {
  1006|    const d = new Date();
  1007|    d.setHours(0,0,0,0);
  1008|    d.setDate(d.getDate() + offset);
  1009|    return d;
  1010|  }
  1011|
  1012|  function toKey(d) {
  1013|    return `dailydose_${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  1014|  }
  1015|
  1016|  function dateKey(offset) { return toKey(getDate(offset)); }
  1017|
  1018|  function formatDateLabel(offset) {
  1019|    const d    = getDate(offset);
  1020|    const day  = WEEKDAYS[d.getDay()];
  1021|    const date = `${d.getDate()}. ${MONTHS[d.getMonth()]} ${d.getFullYear()}`;
  1022|    if (offset === 0)  return { main: 'Heute',   sub: `${day}, ${date}` };
  1023|    if (offset === -1) return { main: 'Gestern',  sub: `${day}, ${date}` };
  1024|    return { main: day, sub: date };
  1025|  }
  1026|
  1027|  /* ─── Storage ─── */
  1028|  const DD_API = `http://${location.hostname}:8765/api/data`;
  1029|
  1030|  /* ─── Defaults per Wochentag ─── */
  1031|  function getDefaults(offset) {
  1032|    const d   = getDate(offset);
  1033|    const dow = d.getDay(); // 0=So, 1=Mo, 2=Di, 3=Mi, 4=Do, 5=Fr, 6=Sa
  1034|    const defaults = [];
  1035|
  1036|    // Freitag: 45 min Islam · alle anderen Tage: 15 min Islam
  1037|    defaults.push({ category: 'Islam', hours: dow === 5 ? 0.75 : 0.25 });
  1038|
  1039|    // Montag + Dienstag: 1h Unterwegs
  1040|    if (dow === 1 || dow === 2) {
  1041|      defaults.push({ category: 'Unterwegs', hours: 1 });
  1042|    }
  1043|
  1044|    return defaults;
  1045|  }
  1046|
  1047|  // Nur gespeicherte Daten, ohne Defaults (für Statistik)
  1048|  function loadSavedEntries(offset) {
  1049|    const raw = localStorage.getItem(dateKey(offset));
  1050|    return raw ? JSON.parse(raw) : [];
  1051|  }
  1052|
  1053|  // Mit Defaults als Fallback (für Tagesansicht)
  1054|  function loadEntries(offset) {
  1055|    const raw = localStorage.getItem(dateKey(offset));
  1056|    if (raw) return JSON.parse(raw);
  1057|    return getDefaults(offset);
  1058|  }
  1059|
  1060|  function saveEntries(offset, data) {
  1061|    const key = dateKey(offset);
  1062|    const val = JSON.stringify(data);
  1063|    localStorage.setItem(key, val);
  1064|    // Sync to server so all devices share the same data
  1065|    fetch(DD_API, {
  1066|      method: 'POST',
  1067|      headers: { 'Content-Type': 'application/json' },
  1068|      body: JSON.stringify({ [key]: val })
  1069|    }).catch(() => {});
  1070|  }
  1071|
  1072|  // Pull all DailyDose data from server into localStorage (cross-device sync)
  1073|  async function loadFromServer() {
  1074|    try {
  1075|      const res = await fetch(DD_API, { signal: AbortSignal.timeout(5000) });
  1076|      if (!res.ok) return;
  1077|      const data = await res.json();
  1078|      Object.entries(data).forEach(([k, v]) => {
  1079|        if (k.startsWith('dailydose_') || k === 'dd_custom_cats') localStorage.setItem(k, v);
  1080|      });
  1081|    } catch {}
  1082|  }
  1083|
  1084|  /* ─── Date navigation ─── */
  1085|  function changeDate(delta) {
  1086|    dateOffset += delta;
  1087|    const label = document.getElementById('date-label');
  1088|    label.classList.remove('date-anim');
  1089|    void label.offsetWidth;
  1090|    label.classList.add('date-anim');
  1091|    const { main, sub } = formatDateLabel(dateOffset);
  1092|    document.getElementById('date-main').textContent = main;
  1093|    document.getElementById('date-sub').textContent  = sub;
  1094|    document.getElementById('btn-next').disabled = (dateOffset >= 0);
  1095|    render();
  1096|    renderStats();
  1097|    syncZeitgeist(dateOffset);
  1098|  }
  1099|
  1100|  /* ─── Entry actions ─── */
  1101|  function addEntry() {
  1102|    const cat = document.getElementById('category').value;
  1103|    const h   = parseInt(document.getElementById('hours').value)  || 0;
  1104|    const m   = parseInt(document.getElementById('minutes').value) || 0;
  1105|    if (h === 0 && m === 0) { document.getElementById('hours').focus(); return; }
  1106|    const total = Math.round((h + m / 60) * 100) / 100;
  1107|
  1108|    const entries = loadEntries(dateOffset);
  1109|    const existing = entries.find(e => e.category === cat);
  1110|    if (existing) {
  1111|      existing.hours = Math.round((existing.hours + total) * 100) / 100;
  1112|    } else {
  1113|      entries.push({ category: cat, hours: total });
  1114|    }
  1115|    saveEntries(dateOffset, entries);
  1116|    document.getElementById('hours').value   = '';
  1117|    document.getElementById('minutes').value = '';
  1118|    render();
  1119|    renderStats();
  1120|  }
  1121|
  1122|  function removeEntry(idx) {
  1123|    const entries = loadEntries(dateOffset);
  1124|    entries.splice(idx, 1);
  1125|    saveEntries(dateOffset, entries);
  1126|    render();
  1127|    renderStats();
  1128|  }
  1129|
  1130|  /* ─── Build chart data (merges remaining into Sonstiges) ─── */
  1131|  function buildChartData(entries) {
  1132|    const totalUsed = entries.reduce((s, e) => s + e.hours, 0);
  1133|    const remaining = Math.max(0, 24 - totalUsed);
  1134|
  1135|    // Clone entries into chart items (alte Kategorien mappen)
  1136|    const data = entries.map(e => {
  1137|      const cat = e.category === 'Beten'     ? 'Islam'
  1138|                : e.category === 'Essen'     ? 'Kochen'
  1139|                : e.category === 'Buildtech' ? 'Hausarbeit'
  1140|                : e.category;
  1141|      return { label: cat, hours: e.hours, color: COLORS[cat] };
  1142|    });
  1143|
  1144|    if (remaining > 0) {
  1145|      const existingSonstiges = data.find(d => d.label === 'Sonstiges');
  1146|      if (existingSonstiges) {
  1147|        existingSonstiges.hours = Math.round((existingSonstiges.hours + remaining) * 100) / 100;
  1148|      } else {
  1149|        data.push({ label: 'Sonstiges', hours: remaining, color: COLORS.Sonstiges });
  1150|      }
  1151|    }
  1152|
  1153|    // Absteigend sortieren, Sonstiges immer ans Ende
  1154|    data.sort((a, b) => {
  1155|      if (a.label === 'Sonstiges') return 1;
  1156|      if (b.label === 'Sonstiges') return -1;
  1157|      return b.hours - a.hours;
  1158|    });
  1159|
  1160|    return data;
  1161|  }
  1162|
  1163|  /* ─── Render day view ─── */
  1164|  function render() {
  1165|    const entries   = loadEntries(dateOffset);
  1166|    const totalUsed = entries.reduce((s, e) => s + e.hours, 0);
  1167|    const remaining = Math.max(0, 24 - totalUsed);
  1168|    const pctUsed   = Math.min(1, totalUsed / 24);
  1169|
  1170|    // Entries list – absteigend nach Stunden, Original-Index für Löschen
  1171|    const sortedEntries = entries
  1172|      .map((e, i) => ({ ...e, origIdx: i }))
  1173|      .sort((a, b) => b.hours - a.hours);
  1174|
  1175|    document.getElementById('entries').innerHTML = sortedEntries.map(e => {
  1176|      const pct = ((e.hours / 24) * 100).toFixed(1);
  1177|      return `
  1178|

  1179|

  1180|          ${ICONS[e.category] || '✦'}
  1181|          ${e.category}
  1182|          ${fmtH(e.hours)}
  1183|          ${pct}%
  1184|          <button class="entry-del" onclick="removeEntry(${e.origIdx})" title="Entfernen">✕</button>
  1185|
`;
  1186|    }).join('');
  1187|
  1188|    // Total / progress
  1189|    const totSec = document.getElementById('total-section');
  1190|    if (entries.length > 0) {
  1191|      totSec.style.display = 'block';
  1192|      document.getElementById('total-used').textContent = `${fmtH(totalUsed)} eingetragen`;
  1193|      const leftEl = document.getElementById('total-left');
  1194|      const fillEl = document.getElementById('progress-fill');
  1195|      if (totalUsed > 24) {
  1196|        leftEl.className   = 'over';
  1197|        leftEl.textContent = `⚠ ${fmtH(totalUsed - 24)} über 24h`;
  1198|        fillEl.className   = 'progress-fill over';
  1199|      } else {
  1200|        leftEl.className   = 'left';
  1201|        leftEl.textContent = `${fmtH(remaining)} verbleibend`;
  1202|        fillEl.className   = 'progress-fill';
  1203|      }
  1204|      fillEl.style.width = (pctUsed * 100).toFixed(1) + '%';
  1205|    } else {
  1206|      totSec.style.display = 'none';
  1207|    }
  1208|
  1209|    // Chart
  1210|    const canvas    = document.getElementById('pie');
  1211|    const container = document.getElementById('canvas-container');
  1212|    const empty     = document.getElementById('empty-state');
  1213|    const legendEl  = document.getElementById('legend');
  1214|    const centerH   = document.getElementById('center-h');
  1215|
  1216|    if (entries.length === 0) {
  1217|      container.style.display = 'none';
  1218|      legendEl.style.display  = 'none';
  1219|      empty.style.display     = 'block';
  1220|      return;
  1221|    }
  1222|
  1223|    container.style.display = 'block';
  1224|    legendEl.style.display  = 'flex';
  1225|    empty.style.display     = 'none';
  1226|    centerH.textContent     = fmtH(totalUsed);
  1227|
  1228|    const chartData = buildChartData(entries);
  1229|    drawPie(canvas, chartData);
  1230|
  1231|    legendEl.innerHTML = chartData.map(d => {
  1232|      const pct = ((d.hours / 24) * 100).toFixed(1);
  1233|      return `
  1234|

  1235|
  1236|          ${ICONS[d.label] || '✦'} ${d.label}
  1237|          ${fmtH(d.hours)}
  1238|          ${pct}%
  1239|
`;
  1240|    }).join('');
  1241|  }
  1242|
  1243|  /* ─── Draw Pie Chart ─── */
  1244|  function drawPie(canvas, data) {
  1245|    const ctx   = canvas.getContext('2d');
  1246|    const W     = canvas.width, H = canvas.height;
  1247|    const cx    = W / 2, cy = H / 2;
  1248|    const r     = Math.min(W, H) / 2 - 14;
  1249|    const holeR = r * 0.42;
  1250|    const total = data.reduce((s, d) => s + d.hours, 0);
  1251|    const sep   = isDark() ? '#1C1C1E' : '#F2F2F7';
  1252|    const hole  = isDark() ? '#2C2C2E' : '#FFFFFF';
  1253|
  1254|    ctx.clearRect(0, 0, W, H);
  1255|
  1256|    // Slices
  1257|    let startAngle = -Math.PI / 2;
  1258|    data.forEach(d => {
  1259|      const slice = (d.hours / total) * 2 * Math.PI;
  1260|      ctx.beginPath();
  1261|      ctx.moveTo(cx, cy);
  1262|      ctx.arc(cx, cy, r, startAngle, startAngle + slice);
  1263|      ctx.closePath();
  1264|      ctx.fillStyle = d.color;
  1265|      ctx.fill();
  1266|      ctx.strokeStyle = sep;
  1267|      ctx.lineWidth = 4;
  1268|      ctx.stroke();
  1269|      startAngle += slice;
  1270|    });
  1271|
  1272|    // Donut hole
  1273|    ctx.beginPath();
  1274|    ctx.arc(cx, cy, holeR, 0, 2 * Math.PI);
  1275|    ctx.fillStyle = hole;
  1276|    ctx.fill();
  1277|
  1278|    // Labels inside slices (≥ 7% of 24h)
  1279|    startAngle = -Math.PI / 2;
  1280|    data.forEach(d => {
  1281|      const slice  = (d.hours / total) * 2 * Math.PI;
  1282|      const pct24  = d.hours / 24;
  1283|
  1284|      if (pct24 >= 0.07) {
  1285|        const mid    = startAngle + slice / 2;
  1286|        const labelR = holeR + (r - holeR) * 0.55;
  1287|        const tx = cx + labelR * Math.cos(mid);
  1288|        const ty = cy + labelR * Math.sin(mid);
  1289|
  1290|        ctx.save();
  1291|        ctx.font         = `bold ${Math.round(W * 0.048)}px -apple-system, system-ui, sans-serif`;
  1292|        ctx.textAlign    = 'center';
  1293|        ctx.textBaseline = 'middle';
  1294|        ctx.fillStyle    = 'rgba(255,255,255,0.95)';
  1295|        ctx.shadowColor  = 'rgba(0,0,0,0.25)';
  1296|        ctx.shadowBlur   = 6;
  1297|        ctx.fillText((pct24 * 100).toFixed(0) + '%', tx, ty);
  1298|        ctx.restore();
  1299|      }
  1300|
  1301|      startAngle += slice;
  1302|    });
  1303|  }
  1304|
  1305|  /* ─── Period tab switch ─── */
  1306|  function setPeriod(days, btn) {
  1307|    statsPeriod = days;
  1308|    document.querySelectorAll('.seg-btn').forEach(b => b.classList.remove('active'));
  1309|    btn.classList.add('active');
  1310|    renderStats();
  1311|  }
  1312|
  1313|  /* ─── Render Stats ─── */
  1314|  function renderStats() {
  1315|    const totals = {};
  1316|    let daysWithData = 0;
  1317|
  1318|    for (let i = 0; i > -statsPeriod; i--) {
  1319|      if (!localStorage.getItem(dateKey(i))) continue;
  1320|      const dayEntries = loadEntries(i);
  1321|      if (dayEntries.length === 0) continue;
  1322|
  1323|      daysWithData++;
  1324|      const dayTotal  = dayEntries.reduce((s, e) => s + e.hours, 0);
  1325|      const remaining = Math.max(0, 24 - dayTotal);
  1326|
  1327|      dayEntries.forEach(e => {
  1328|        const cat = CAT_ALIAS[e.category] || e.category;
  1329|        totals[cat] = (totals[cat] || 0) + e.hours;
  1330|      });
  1331|      if (remaining > 0) totals['Sonstiges'] = (totals['Sonstiges'] || 0) + remaining;
  1332|    }
  1333|
  1334|    const summaryEl = document.getElementById('stats-summary');
  1335|    const contentEl = document.getElementById('stats-content');
  1336|
  1337|    // Datumsbereich berechnen
  1338|    const today = getDate(0);
  1339|    const from  = getDate(-(statsPeriod - 1));
  1340|    const fmtD  = d => `${d.getDate()}. ${MONTHS[d.getMonth()].slice(0,3)}`;
  1341|    const fmtDY = d => `${d.getDate()}. ${MONTHS[d.getMonth()].slice(0,3)} ${d.getFullYear()}`;
  1342|    const sameYear = today.getFullYear() === from.getFullYear();
  1343|    const dateRange = `${sameYear ? fmtD(from) : fmtDY(from)} – ${fmtDY(today)}`;
  1344|
  1345|    if (Object.keys(totals).length === 0) {
  1346|      summaryEl.innerHTML = `<span style="color:var(--accent);font-weight:600">${dateRange}</span>`;
  1347|      contentEl.innerHTML = '
Keine Daten in diesem Zeitraum.
';
  1348|      return;
  1349|    }
  1350|
  1351|    // Durchschnitt pro Tag berechnen
  1352|    const avgs   = {};
  1353|    Object.entries(totals).forEach(([cat, hrs]) => avgs[cat] = hrs / daysWithData);
  1354|
  1355|    const maxAvg = Math.max(...Object.values(avgs));
  1356|    const sorted = Object.entries(avgs).sort((a, b) => b[1] - a[1]);
  1357|
  1358|    summaryEl.innerHTML = `<span style="color:var(--accent);font-weight:600">${dateRange}</span>`;
  1359|
  1360|    contentEl.innerHTML = `<div class="stats-rows">` + sorted.map(([cat, avg]) => {
  1361|      const barW = (avg / maxAvg) * 100;
  1362|      const pct  = ((avg / 24) * 100).toFixed(1);
  1363|      return `
  1364|

  1365|          ${ICONS[cat] || '✦'}
  1366|          ${cat}
  1367|

  1368|

  1369|

  1370|          Ø ${fmtH(avg)}
  1371|          ${pct}%
  1372|
`;
  1373|    }).join('') + `</div>`;
  1374|  }
  1375|
  1376|  /* ─── Utils ─── */
  1377|  function isDark() {
  1378|    return window.matchMedia('(prefers-color-scheme: dark)').matches;
  1379|  }
  1380|
  1381|  function fmtH(h) {
  1382|    const hrs  = Math.floor(h);
  1383|    const mins = Math.round((h - hrs) * 60);
  1384|    if (mins === 0) return `${hrs}h`;
  1385|    if (hrs === 0)  return `${mins}min`;
  1386|    return `${hrs}h ${mins}m`;
  1387|  }
  1388|
  1389|  /* ─── Zeitgeist Sync ─── */
  1390|  const ZG_URL = `http://${location.hostname}:8765/api/data`;
  1391|  const ZG_KEY = 'zeitgeist-v2';
  1392|
  1393|  function zgSetBadge(status, text) {
  1394|    const b = document.getElementById('zg-badge');
  1395|    if (!b) return;
  1396|    b.className = 'zg-badge ' + status;
  1397|    b.textContent = text;
  1398|  }
  1399|
  1400|  async function syncZeitgeist(offset) {
  1401|    zgSetBadge('syncing', '⟳ Zeitgeist');
  1402|    try {
  1403|      const res = await fetch(ZG_URL, { signal: AbortSignal.timeout(4000) });
  1404|      if (!res.ok) throw new Error();
  1405|      const api  = await res.json();
  1406|      const raw  = api[ZG_KEY];
  1407|      if (!raw) { zgSetBadge('', ''); return; }
  1408|
  1409|      const zgData = JSON.parse(raw);
  1410|      const d      = getDate(offset);
  1411|      const key    = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
  1412|
  1413|      const slots    = zgData[key] || [];
  1414|      const totalMin = slots.reduce((s, v) => s + v, 0);
  1415|      const hours    = Math.round(totalMin / 60 * 100) / 100;
  1416|
  1417|      const entries = loadEntries(offset);
  1418|      const idx     = entries.findIndex(e => e.category === 'Lernen');
  1419|
  1420|      if (hours > 0) {
  1421|        if (idx !== -1) entries[idx].hours = hours;
  1422|        else entries.push({ category: 'Lernen', hours });
  1423|      } else if (idx !== -1) {
  1424|        entries.splice(idx, 1);
  1425|      }
  1426|
  1427|      saveEntries(offset, entries);
  1428|      render();
  1429|      renderStats();
  1430|      zgSetBadge('ok', `✓ Zeitgeist · ${totalMin} min`);
  1431|    } catch {
  1432|      zgSetBadge('error', '✗ Zeitgeist');
  1433|    }
  1434|  }
  1435|
  1436|  /* ─── Init ─── */
  1437|  document.getElementById('hours').addEventListener('keydown', e => {
  1438|    if (e.key === 'Enter') addEntry();
  1439|  });
  1440|  document.getElementById('minutes').addEventListener('keydown', e => {
  1441|    if (e.key === 'Enter') addEntry();
  1442|  });
  1443|
  1444|  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
  1445|    render(); renderStats();
  1446|  });
  1447|
  1448|  // Kategorien aufbauen und Select befüllen
  1449|  buildCategoryMaps();
  1450|  buildCategorySelect();
  1451|  renderColorGrid();
  1452|
  1453|  // Initial render
  1454|  const { main, sub } = formatDateLabel(0);
  1455|  document.getElementById('date-main').textContent = main;
  1456|  document.getElementById('date-sub').textContent  = sub;
  1457|  document.getElementById('btn-next').disabled     = true;
  1458|
  1459|  // Sofort mit lokalen Daten rendern
  1460|  render();
  1461|  renderStats();
  1462|
  1463|  // Server-Daten laden (inkl. custom cats), neu rendern, Zeitgeist sync
  1464|  loadFromServer().then(() => {
  1465|    // Custom cats vom Server ebenfalls laden
  1466|    const serverCats = localStorage.getItem('dd_custom_cats');
  1467|    if (serverCats) {
  1468|      buildCategoryMaps();
  1469|      buildCategorySelect();
  1470|    }
  1471|    render();
  1472|    renderStats();
  1473|    syncZeitgeist(0);
  1474|  });
  1475|
  1476|  // Auto-refresh alle 60s für den heutigen Tag
  1477|  setInterval(() => { if (dateOffset === 0) syncZeitgeist(0); }, 60_000);
  1478|</script>
  1479|
  1480|
  1481|