From 8911a79d7f6194e184d38ce21b7c120552a85a2b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 22:28:32 +0000 Subject: [PATCH 1/6] docs: rewrite cron jobs guide and heartbeat notes --- CHANGELOG.md | 2 +- docs/clawd.md | 3 +- docs/configuration.md | 8 +- docs/cron-jobs.md | 132 +++++++++++ docs/cron.md | 385 ------------------------------- docs/heartbeat.md | 27 ++- docs/hubs.md | 2 +- docs/index.md | 2 +- docs/plans/cron-add-hardening.md | 2 +- docs/templates/AGENTS.md | 2 +- docs/templates/HEARTBEAT.md | 2 +- src/agents/workspace.ts | 5 +- 12 files changed, 166 insertions(+), 406 deletions(-) create mode 100644 docs/cron-jobs.md delete mode 100644 docs/cron.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c0a8ae13..31acd0c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ - Groups: `whatsapp.groups`, `telegram.groups`, and `imessage.groups` now act as allowlists when set. Add `"*"` to keep allow-all behavior. ### Fixes -- Heartbeat: default interval now 30m with a new default prompt + HEARTBEAT.md template. +- Heartbeat: default interval 30m; clarified default prompt usage and HEARTBEAT.md template behavior. - Onboarding: write auth profiles to the multi-agent path (`~/.clawdbot/agents/main/agent/`) so the gateway finds credentials on first startup. Thanks @minghinmatthewlam for PR #327. - Docs: add missing `ui:install` setup step in the README. Thanks @hugobarauna for PR #300. - Build: import tool-display JSON as a module instead of runtime file reads. Thanks @mukhtharcm for PR #312. diff --git a/docs/clawd.md b/docs/clawd.md index 1c62d396b..7f4115b70 100644 --- a/docs/clawd.md +++ b/docs/clawd.md @@ -166,6 +166,7 @@ By default, CLAWDBOT runs a heartbeat every 30 minutes with the prompt: Set `agent.heartbeat.every: "0m"` to disable. - If the agent replies with `HEARTBEAT_OK` (optionally with short padding; see `agent.heartbeat.ackMaxChars`), CLAWDBOT suppresses outbound delivery for that heartbeat. +- Heartbeats run full agent turns — shorter intervals burn more tokens. ```json5 { @@ -205,7 +206,7 @@ Logs live under `/tmp/clawdbot/` (default: `clawdbot-YYYY-MM-DD.log`). - WebChat: [WebChat](https://docs.clawd.bot/webchat) - Gateway ops: [Gateway runbook](https://docs.clawd.bot/gateway) -- Cron + wakeups: [Cron + wakeups](https://docs.clawd.bot/cron) +- Cron + wakeups: [Cron jobs](https://docs.clawd.bot/cron-jobs) - macOS menu bar companion: [Clawdbot macOS app](https://docs.clawd.bot/macos) - iOS node app: [iOS app](https://docs.clawd.bot/ios) - Android node app: [Android app](https://docs.clawd.bot/android) diff --git a/docs/configuration.md b/docs/configuration.md index 8fc8a76cd..ea954c48b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -791,11 +791,11 @@ Z.AI models are available as `zai/` (e.g. `zai/glm-4.7`) and require - `model`: optional override model for heartbeat runs (`provider/model`). - `target`: optional delivery provider (`last`, `whatsapp`, `telegram`, `discord`, `slack`, `signal`, `imessage`, `none`). Default: `last`. - `to`: optional recipient override (provider-specific id, e.g. E.164 for WhatsApp, chat id for Telegram). -- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`). +- `prompt`: optional override for the heartbeat body (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`). Overrides are sent verbatim; include a `Read HEARTBEAT.md if exists` line if you still want the file read. - `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30). -Heartbeats run full agent turns. Shorter intervals burn more tokens; adjust `every` -and/or `model` accordingly. +Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful +of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`. `agent.bash` configures background bash defaults: - `backgroundMs`: time before auto-background (ms, default 10000) @@ -1482,7 +1482,7 @@ Template placeholders are expanded in `routing.transcribeAudio.command` (and any ## Cron (Gateway scheduler) -Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron + wakeups](https://docs.clawd.bot/cron) for the full RFC and CLI examples. +Cron is a Gateway-owned scheduler for wakeups and scheduled jobs. See [Cron jobs](https://docs.clawd.bot/cron-jobs) for the feature overview and CLI examples. ```json5 { diff --git a/docs/cron-jobs.md b/docs/cron-jobs.md new file mode 100644 index 000000000..a4ad5d5c8 --- /dev/null +++ b/docs/cron-jobs.md @@ -0,0 +1,132 @@ +--- +summary: "Cron jobs + wakeups for the Gateway scheduler" +read_when: + - Scheduling background jobs or wakeups + - Wiring automation that should run with or alongside heartbeats +--- +# Cron jobs (Gateway scheduler) + +Cron runs inside the Gateway and schedules background work so Clawdbot can +wake itself up, run isolated agent jobs, and deliver reminders on time. + +## Update checklist (internal) +- [x] Audit cron + heartbeat behavior in code +- [x] Rewrite cron doc as user-facing feature +- [x] Update heartbeat docs + templates +- [x] Update cron links in docs +- [x] Update changelog +- [x] Run full gate (lint/build/test/docs) + +## What cron is +- **Gateway-owned scheduler** that persists jobs under `~/.clawdbot/cron/`. +- **Two execution modes**: + - **Main session jobs** enqueue `System:` events and rely on the heartbeat runner. + - **Isolated jobs** run a dedicated agent turn in `cron:` sessions. +- **Wakeups** are first-class: a job can trigger the next heartbeat or run it now. + +## When to use it +- Recurring reminders: “every weekday at 7:30” or “every 2h.” +- Background chores: summarize inboxes, check dashboards, watch logs. +- Automation that should not pollute the main chat history. +- Scheduled wakeups that drive the heartbeat pipeline. + +## Schedules +Cron supports three schedule kinds: +- `at`: one-shot timestamp in ms. +- `every`: fixed interval (ms). +- `cron`: 5-field cron expression, optional IANA timezone. + +Cron expressions use `croner` under the hood. If a timezone is omitted, the +server’s local timezone is used. + +## Job types + +### Main session jobs +Main jobs enqueue a system event and optionally wake the heartbeat runner. +They **must** use `payload.kind = "systemEvent"`. + +- **`wakeMode: "next-heartbeat"`** (default): the event waits for the next + scheduled heartbeat. +- **`wakeMode: "now"`**: the event triggers an immediate heartbeat run. + +### Isolated jobs +Isolated jobs run a dedicated agent turn in session `cron:` and can +optionally deliver a message. + +Key behaviors: +- Prompt is prefixed with `[cron: ]` for traceability. +- A summary is posted to the main session with prefix `Cron` (or + `isolation.postToMainPrefix`). +- `wakeMode: "now"` triggers an immediate heartbeat after posting the summary. +- `payload.deliver: true` sends output to a provider; otherwise it stays internal. + +## Storage & history +- Job store: `~/.clawdbot/cron/jobs.json` (JSON, Gateway-managed). +- Run history: `~/.clawdbot/cron/runs/.jsonl` (JSONL, auto-pruned). +- Override store path: `cron.store` in config. + +## Configuration + +```json5 +{ + cron: { + enabled: true, // default true + store: "~/.clawdbot/cron/jobs.json", + maxConcurrentRuns: 1 // default 1 + } +} +``` + +Disable cron entirely: +- `cron.enabled: false` (config) +- or `CLAWDBOT_SKIP_CRON=1` (env) + +## CLI quickstart + +One-shot reminder (main session, wake immediately): +```bash +clawdbot cron add \ + --name "Calendar check" \ + --at "20m" \ + --session main \ + --system-event "Next heartbeat: check calendar." \ + --wake now +``` + +Recurring isolated job (deliver to WhatsApp): +```bash +clawdbot cron add \ + --name "Morning status" \ + --cron "0 7 * * *" \ + --tz "America/Los_Angeles" \ + --session isolated \ + --message "Summarize inbox + calendar for today." \ + --deliver \ + --provider whatsapp \ + --to "+15551234567" +``` + +Manual run (debug): +```bash +clawdbot cron run --force +``` + +Run history: +```bash +clawdbot cron runs --id --limit 50 +``` + +Immediate wake without creating a job: +```bash +clawdbot wake --mode now --text "Next heartbeat: check battery." +``` + +## API surface (Gateway) +- `cron.list`, `cron.status`, `cron.add`, `cron.update`, `cron.remove` +- `cron.run` (force or due), `cron.runs` +- `wake` (enqueue system event + optional heartbeat) + +## Tips +- Use **main session jobs** when you want the heartbeat prompt + existing context. +- Use **isolated jobs** for noisy, frequent, or long-running work. +- Keep messages short; cron turns are full agent runs and can burn tokens. diff --git a/docs/cron.md b/docs/cron.md deleted file mode 100644 index 5c76c8b8d..000000000 --- a/docs/cron.md +++ /dev/null @@ -1,385 +0,0 @@ ---- -summary: "RFC: Cron jobs + wakeups for Clawd/Clawdbot (main vs isolated sessions)" -read_when: - - Designing scheduled jobs, alarms, or wakeups - - Adding Gateway methods or CLI commands for automation - - Adjusting heartbeat behavior or session routing ---- - -# RFC: Cron jobs + wakeups for Clawd - -Status: Draft -Last updated: 2025-12-13 - -## Context - -Clawdbot already has: -- A **gateway heartbeat runner** that runs the agent with the configured heartbeat prompt (default: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.`) and suppresses `HEARTBEAT_OK` ([`src/infra/heartbeat-runner.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/heartbeat-runner.ts)). -- A lightweight, in-memory **system event queue** (`enqueueSystemEvent`) that is injected into the next **main session** turn (`drainSystemEvents` in [`src/auto-reply/reply.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/auto-reply/reply.ts)). -- A WebSocket **Gateway** daemon that is intended to be always-on ([`docs/gateway.md`](https://docs.clawd.bot/gateway)). - -This RFC adds a small “cron job system” so Clawd can schedule future work and reliably wake itself up: -- **Delayed**: run on the *next* normal heartbeat tick -- **Immediate**: run *now* (trigger a heartbeat immediately) -- **Isolated jobs**: optionally run in their own session that does not pollute the main session and can run concurrently (within configured limits). - -## Goals - -- Provide a **persistent job store** and an **in-process scheduler** owned by the Gateway. -- Allow each job to target either: - - `sessionTarget: "main"`: inject as `System:` lines and rely on the main heartbeat (or trigger it immediately). - - `sessionTarget: "isolated"`: run an agent turn in a dedicated session key (job session), optionally delivering a message and/or posting a summary back to main. -- Expose a stable control surface: - - **Gateway methods** (`cron.*`, `wake`) for programmatic usage (mac app, CLI, agents). - - **CLI commands** (`clawdbot cron ...`) to add/remove/edit/list and to debug `run`. -- Produce clear, structured **logs** for job lifecycle and execution outcomes. - -## Non-goals (v1) - -- Multi-host distributed scheduling. -- Exactly-once semantics across crashes (we aim for “at-least-once with idempotency hooks”). -- A full Unix-cron parser as the only schedule format (we can support it, but v1 should not require complex cron features to be useful). - -## Terminology - -- **Wake**: a request to ensure the agent gets a turn soon (either right now or next heartbeat). -- **Main session**: the canonical session bucket (default key `"main"`) that receives `System:` events. -- **Isolated session**: a per-job session key (e.g. `cron:`) with its own session id / session file. - -## User stories - -- “Remind me in 20 minutes” → add a one-shot job that triggers an immediate heartbeat at T+20m. -- “Every weekday at 7:30, wake me up and start music” → recurring job, isolated session, deliver to WhatsApp. -- “Every hour, check battery; only interrupt me if < 20%” → isolated job that decides whether to deliver; may also post a brief status to main. -- “Next heartbeat, please check calendar” → delayed wake targeting main session. - -## Job model - -### Storage schema (v1) - -Each job is a JSON object with stable keys (unknown keys ignored for forward compatibility): - -- `id: string` (UUID) -- `name: string` (required) -- `description?: string` (optional) -- `enabled: boolean` -- `createdAtMs: number` -- `updatedAtMs: number` -- `schedule` (one of) - - `{"kind":"at","atMs":number}` (one-shot) - - `{"kind":"every","everyMs":number,"anchorMs"?:number}` (simple interval) - - `{"kind":"cron","expr":string,"tz"?:string}` (optional; see “Schedule parsing”) -- `sessionTarget: "main" | "isolated"` -- `wakeMode: "next-heartbeat" | "now"` - - For `sessionTarget:"isolated"`, `wakeMode:"now"` means “run immediately when due”. - - For `sessionTarget:"main"`, `wakeMode` controls whether we trigger the heartbeat immediately or just enqueue and wait. -- `payload` (one of) - - `{"kind":"systemEvent","text":string}` (enqueue as `System:`) - - `{"kind":"agentTurn","message":string,"deliver"?:boolean,"provider"?: "last"|"whatsapp"|"telegram"|"discord"|"signal"|"imessage","to"?:string,"timeoutSeconds"?:number}` -- `isolation` (optional; only meaningful for isolated jobs) - - `{"postToMainPrefix"?: string}` -- `runtime` (optional) - - `{"maxAttempts"?:number,"retryBackoffMs"?:number}` (best-effort retries; defaults off) -- `state` (runtime-maintained) - - `{"nextRunAtMs":number,"lastRunAtMs"?:number,"lastStatus"?: "ok"|"error"|"skipped","lastError"?:string,"lastDurationMs"?:number}` - -### Key behavior - -- `sessionTarget:"main"` jobs always enqueue `payload.kind:"systemEvent"` (directly or derived from `agentTurn` results; see below). -- `sessionTarget:"isolated"` jobs create/use a stable session key: `cron:`. - -## Storage location - -Cron persists everything under `~/.clawdbot/cron/`: -- Job store: `~/.clawdbot/cron/jobs.json` -- Run history: `~/.clawdbot/cron/runs/.jsonl` - -You can override the job store path via `cron.store` in config. - -The scheduler should never require additional configuration for the base directory (Clawdbot already treats `~/.clawdbot` as fixed). - -## Enabling - -Cron execution is enabled by default inside the Gateway. - -To disable it, set: - -```json5 -{ - cron: { - enabled: false, - // optional: - store: "~/.clawdbot/cron/jobs.json", - maxConcurrentRuns: 1 - } -} -``` - -You can also disable scheduling via the environment variable `CLAWDBOT_SKIP_CRON=1`. - -## Scheduler design - -### Ownership - -The Gateway owns: -- the scheduler timer, -- job store reads/writes, -- job execution (enqueue system events and/or agent turns). - -This keeps scheduling unified with the always-on process and prevents “two schedulers” when multiple CLIs run. - -### Timer strategy - -- Maintain an in-memory heap/array of enabled jobs keyed by `state.nextRunAtMs`. -- Use a **single `setTimeout`** to wake at the earliest next run. -- On wake: - - compute all due jobs (now >= nextRunAtMs), - - mark them “in flight” (in memory), - - persist updated `state` (at least bump `nextRunAtMs` / `lastRunAtMs`) before starting execution to minimize duplicate runs on crash, - - execute jobs (with concurrency limits), - - persist final `lastStatus/lastError/lastDurationMs`, - - re-arm timer for the next earliest run. - -### Schedule parsing - -V1 can ship with `at` + `every` without extra deps. - -If we add `"kind":"cron"`: -- Use a well-maintained parser (we use `croner`) and support: - - 5-field cron (`min hour dom mon dow`) at minimum - - optional `tz` -- Store `nextRunAtMs` computed by the parser; re-compute after each run. - -## Execution semantics - -### Main session jobs - -Main session jobs do not run the agent directly by default. - -When due: -1) `enqueueSystemEvent(job.payload.text)` (or a derived message) -2) If `wakeMode:"now"`, trigger an immediate heartbeat run (see “Heartbeat wake hook”). -3) Otherwise do nothing else (the next scheduled heartbeat will pick up the system event). - -Why: This keeps the main session’s “proactive” behavior centralized in the heartbeat rules and avoids ad-hoc agent turns that might fight with inbound message processing. - -### Isolated session jobs - -Isolated jobs run an agent turn in a dedicated session key, intended to be separate from main. - -When due: -- Build a message body that includes schedule metadata, e.g.: - - `"[cron:] : "` -- Execute via the same agent runner path as other command-mode runs, but pinned to: - - `sessionKey = cron:` - - `sessionId = store[sessionKey].sessionId` (create if missing) -- Optionally deliver output (`payload.deliver === true`) to the configured provider/to. -- Isolated jobs always enqueue a summary system event to the main session when they finish (derived from the last agent text output). - - Prefix defaults to `Cron`, and can be customized via `isolation.postToMainPrefix`. -- If `deliver` is omitted/false, nothing is sent to external providers; you still get the main-session summary and can inspect the full isolated transcript in `cron:`. - -### “Run in parallel to main” - -Clawdbot currently serializes command execution through a global in-process queue ([`src/process/command-queue.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/process/command-queue.ts)) to avoid collisions. - -To support isolated cron jobs running “in parallel”, we should introduce **lanes** (keyed queues) plus a global concurrency cap: -- Lane `"main"`: inbound auto-replies + main heartbeat. -- Lane `"cron"` (or `cron:`): isolated jobs. -- Configurable `cron.maxConcurrentRuns` (default 1 or 2). - -This yields: -- isolated jobs can overlap with the main lane (up to cap), -- each lane still preserves ordering for its own work (optional), -- we retain safety knobs to prevent runaway resource contention. - -## Heartbeat wake hook (immediate vs next heartbeat) - -We need a way for the Gateway (or the scheduler) to request an immediate heartbeat without duplicating heartbeat logic. - -Design: -- `startHeartbeatRunner` owns the real heartbeat execution and installs a wake handler. -- Wake hook lives in [`src/infra/heartbeat-wake.ts`](https://github.com/clawdbot/clawdbot/blob/main/src/infra/heartbeat-wake.ts): - - `setHeartbeatWakeHandler(fn | null)` installed by the heartbeat runner - - `requestHeartbeatNow({ reason, coalesceMs? })` -- If the handler is absent, the request is stored as “pending”; the next time the handler is installed, it runs once. -- Coalesce rapid calls and respect the “skip when queue busy” behavior (retry soon vs dropping). - -## Run history log (JSONL) - -In addition to normal structured logs, the Gateway writes an append-only run history “ledger” (JSONL) whenever a job finishes. This is intended for quick debugging (“did the job run, when, and what happened?”). - -Path rules: -- Run logs are stored per job next to the store: `.../runs/.jsonl`. - -Retention: -- Best-effort pruning when the file grows beyond ~2MB; keep the newest ~2000 lines. - -Each log line includes (at minimum) job id, status/error, timing, and a `summary` string (systemEvent text for main jobs, and the last agent text output for isolated jobs). - -## Compatibility policy (cron.add/cron.update) - -To keep older clients working, the Gateway applies **best-effort normalization** for `cron.add` and `cron.update`: -- Accepts wrapped payloads under `data` or `job` and unwraps them. -- Infers `schedule.kind` from `atMs`, `everyMs`, or `expr` if missing. -- Infers `payload.kind` from `text` (systemEvent) or `message` (agentTurn) if missing. -- Defaults `wakeMode` to `"next-heartbeat"` when omitted. -- Defaults `sessionTarget` based on payload kind (`systemEvent` → `"main"`, `agentTurn` → `"isolated"`). - -Normalization is **compat-only**. New clients should send the full schema (including `kind`, `sessionTarget`, and `wakeMode`) to avoid ambiguity. Unknown fields are still rejected by schema validation. - -## Gateway API - -New methods (names can be bikeshed; `cron.*` is suggested): - -- `wake` - - params: `{ mode: "now" | "next-heartbeat", text: string }` - - effect: `enqueueSystemEvent(text)`, plus optional immediate heartbeat trigger - -- `cron.list` - - params: optional `{ includeDisabled?: boolean }` - - returns: `{ jobs: CronJob[] }` - -- `cron.add` - - params: job payload without `id/state` (server generates and returns created job) - -- `cron.update` - - params: `{ id: string, patch: Partial }` - -- `cron.remove` - - params: `{ id: string }` - -- `cron.run` - - params: `{ id: string, mode?: "due" | "force" }` (debugging; does not change schedule unless `force` requires it) - -- `cron.runs` - - params: `{ id: string, limit?: number }` - - returns: `{ entries: CronRunLogEntry[] }` - - note: `id` is required (runs are stored per-job). - -The Gateway should broadcast a `cron` event for UI/debug: -- event: `cron` - - payload: `{ jobId, action: "added"|"updated"|"removed"|"started"|"finished", status?, error?, nextRunAtMs? }` - -## CLI surface - -Add a `cron` command group (all commands should also support `--json` where sensible): - -- `clawdbot cron list [--json] [--all]` -- `clawdbot cron add ...` - - schedule flags: - - `--at ` (one-shot) - - `--every ` (e.g. `10m`, `1h`) - - `--cron "" [--tz ""]` - - target flags: - - `--session main|isolated` - - `--wake now|next-heartbeat` - - payload flags (choose one): - - `--system-event ""` - - `--message "" [--deliver] [--provider last|whatsapp|telegram|discord|slack|signal|imessage] [--to ]` - -- `clawdbot cron edit ...` (patch-by-flags, non-interactive) -- `clawdbot cron rm ` -- `clawdbot cron enable ` / `clawdbot cron disable ` -- `clawdbot cron run [--force]` (debug) -- `clawdbot cron runs --id [--limit ]` (run history) -- `clawdbot cron status` (scheduler enabled + next wake) - -Additionally: -- `clawdbot wake --mode now|next-heartbeat --text ""` as a thin wrapper around `wake` for agents to call. - -## Examples - -### Run once at a specific time - -One-shot reminder that targets the main session and triggers a heartbeat immediately at the scheduled time: - -```bash -clawdbot cron add \ - --at "2025-12-14T07:00:00-08:00" \ - --session main \ - --wake now \ - --system-event "Alarm: wake up (meeting in 30 minutes)." -``` - -### Run daily (calendar-accurate) - -Daily at 07:00 in a specific timezone (preferred over “every 24h” to avoid DST drift): - -```bash -clawdbot cron add \ - --cron "0 7 * * *" \ - --tz "America/Los_Angeles" \ - --session isolated \ - --wake now \ - --message "Daily check: scan calendar + inbox; deliver only if urgent." \ - --deliver \ - --provider last -``` - -### Run weekly (every Wednesday) - -Every Wednesday at 09:00: - -```bash -clawdbot cron add \ - --cron "0 9 * * 3" \ - --tz "America/Los_Angeles" \ - --session isolated \ - --wake now \ - --message "Weekly: summarize status and remind me of goals." \ - --deliver \ - --provider last -``` - -### “Next heartbeat” - -Enqueue a note for the main session but let the existing heartbeat cadence pick it up: - -```bash -clawdbot wake --mode next-heartbeat --text "Next heartbeat: check battery + upcoming meetings." -``` - -## Logging & observability - -Logging requirements: -- Use `getChildLogger({ module: "cron", jobId, runId, name })` for every run. -- Log lifecycle: - - store load/save (debug; include job count) - - schedule recompute (debug; include nextRunAt) - - job start/end (info) - - job skipped (info; include reason) - - job error (warn; include error + stack where available) -- Emit a concise user-facing line to stdout when running in CLI mode (similar to heartbeat logs). - -Suggested log events: -- `cron: scheduler started` (jobCount, nextWakeAt) -- `cron: job started` (jobId, scheduleKind, sessionTarget, wakeMode) -- `cron: job finished` (status, durationMs, nextRunAtMs) -- When `cron.enabled` is false, the Gateway logs `cron: disabled` and jobs will not run automatically (the CLI warns on `cron add`/`cron edit`). -- Use `clawdbot cron status` to confirm the scheduler is enabled and see the next wake time. - -## Safety & security - -- Respect existing allowlists/routing rules: delivery defaults should not send to arbitrary destinations unless explicitly configured. -- Provide a global “kill switch”: - - `cron.enabled: boolean` (default `true`). - - `gateway method set-heartbeats` already exists; cron should have similar. -- Avoid persistence of sensitive payloads unless requested; job text may contain private content. - -## Testing plan (v1) - -- Unit tests: - - schedule computation for `at` and `every` - - job store read/write + migration behavior - - lane concurrency: main vs cron overlap is bounded - - “wake now” coalescing and pending behavior when provider not ready -- Integration tests: - - start Gateway with `CLAWDBOT_SKIP_PROVIDERS=1`, add jobs, list/edit/remove - - simulate due jobs and assert `enqueueSystemEvent` called + cron events broadcast - -## Rollout plan - -1) Add the `wake` primitive + heartbeat wake hook (no persistent jobs yet). -2) Add `cron.*` API and CLI wrappers with `at` + `every`. -3) Add optional cron expression parsing (`kind:"cron"`) if needed. -4) Add UI surfacing in WebChat/macOS app (optional). diff --git a/docs/heartbeat.md b/docs/heartbeat.md index 6fb021a4d..bb7b4f73b 100644 --- a/docs/heartbeat.md +++ b/docs/heartbeat.md @@ -8,17 +8,28 @@ read_when: Heartbeat runs periodic agent turns in the **main session** so the model can surface anything that needs attention without spamming the user. +## Defaults +- Interval: `30m` (set `agent.heartbeat.every` to change, `0m` disables). +- Prompt body (configurable via `agent.heartbeat.prompt`): + `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` +- Heartbeat prompt text is sent **verbatim** as the user message. Clawdbot does + not append extra body text. The system prompt includes a Heartbeats section + and the run is flagged as a heartbeat internally. + ## Prompt contract -- Heartbeat body defaults to: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` (configurable via `agent.heartbeat.prompt`). - If nothing needs attention, the model should reply `HEARTBEAT_OK`. - During heartbeat runs, Clawdbot treats `HEARTBEAT_OK` as an ack when it appears at the **start or end** of the reply. Clawdbot strips the token and discards the reply if the remaining content is **≤ `ackMaxChars`** (default: 30). - If `HEARTBEAT_OK` is in the **middle** of a reply, it is not treated specially. - For alerts, do **not** include `HEARTBEAT_OK`; return only the alert text. -- Heartbeat prompt text is sent **verbatim** as the user message. Clawdbot does - not append extra body text. The system prompt includes a Heartbeats section - and the run is flagged as a heartbeat internally. + +## Prompt overrides +- Overriding `agent.heartbeat.prompt` **replaces** the default body. Nothing is + merged for you. +- If you still want `HEARTBEAT.md` instructions, keep a line like + `Read HEARTBEAT.md if exists` in your custom prompt. +- `HEARTBEAT_OK` handling stays the same; changing the prompt won’t break acks. ### Stray `HEARTBEAT_OK` outside heartbeats If the model accidentally includes `HEARTBEAT_OK` at the start or end of a @@ -63,9 +74,9 @@ and final replies: - `ackMaxChars`: max chars allowed after `HEARTBEAT_OK` before delivery (default: 30). ## Cost awareness -Heartbeats run full agent turns. Shorter intervals burn more tokens. If you -don’t need frequent checks, increase `every`, pick a cheaper `model`, or set -`target: "none"` to keep results internal. +Heartbeats run full agent turns. Shorter intervals burn more tokens. Be +intentional about `every`, keep `HEARTBEAT.md` tiny, and consider a cheaper +`model` or `target: "none"` if you only want internal state updates. ## HEARTBEAT.md (optional) If a `HEARTBEAT.md` file exists in the workspace, the default prompt tells the @@ -85,6 +96,8 @@ bloat. - Handle mundane tasks (triage inboxes, summarize queues, refresh notes). - Nudge on open loops or reminders. - Background monitoring (health checks, status polling, low-priority alerts). +- Scheduled routines (use [Cron jobs](https://docs.clawd.bot/cron-jobs) when you + need exact schedules or isolated runs). ## Wake hook - The gateway exposes a heartbeat wake hook so cron/jobs/webhooks can request an diff --git a/docs/hubs.md b/docs/hubs.md index 87e74d053..4babee171 100644 --- a/docs/hubs.md +++ b/docs/hubs.md @@ -80,7 +80,7 @@ Use these hubs to discover every page, including deep dives and reference docs t - [Tools surface](https://docs.clawd.bot/tools) - [Bash tool](https://docs.clawd.bot/bash) - [Elevated mode](https://docs.clawd.bot/elevated) -- [Cron + wakeups](https://docs.clawd.bot/cron) +- [Cron jobs](https://docs.clawd.bot/cron-jobs) - [Thinking + verbose](https://docs.clawd.bot/thinking) - [Models](https://docs.clawd.bot/models) - [Agent send CLI](https://docs.clawd.bot/agent-send) diff --git a/docs/index.md b/docs/index.md index 862aa4b39..f39dfcc4e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -171,7 +171,7 @@ Example: - [Linux app](https://docs.clawd.bot/linux) - Ops and safety: - [Sessions](https://docs.clawd.bot/session) - - [Cron + wakeups](https://docs.clawd.bot/cron) + - [Cron jobs](https://docs.clawd.bot/cron-jobs) - [Security](https://docs.clawd.bot/security) - [Troubleshooting](https://docs.clawd.bot/troubleshooting) diff --git a/docs/plans/cron-add-hardening.md b/docs/plans/cron-add-hardening.md index 2ba67ea66..f3277e8e3 100644 --- a/docs/plans/cron-add-hardening.md +++ b/docs/plans/cron-add-hardening.md @@ -43,7 +43,7 @@ Recent gateway logs show repeated `cron.add` failures with invalid parameters (m - [x] Fix UI CronStatus type to match gateway (`jobs` instead of `jobCount`). - [x] Update cron UI provider select to include Discord/Slack/Signal/iMessage. - [x] Update macOS CronJobEditor provider picker + enum to include Slack/Signal/iMessage. -- [x] Document cron compatibility normalization policy in [`docs/cron.md`](https://docs.clawd.bot/cron). +- [x] Document cron compatibility normalization policy in [`docs/cron-jobs.md`](https://docs.clawd.bot/cron-jobs). ### Phase 2 — Input normalization + tooling hardening - [x] Add shared cron input normalization helpers (`normalizeCronJobCreate`/`normalizeCronJobPatch`). diff --git a/docs/templates/AGENTS.md b/docs/templates/AGENTS.md index 051f19c00..34dbd3465 100644 --- a/docs/templates/AGENTS.md +++ b/docs/templates/AGENTS.md @@ -120,7 +120,7 @@ When you receive a heartbeat poll (message matches the configured heartbeat prom Default heartbeat prompt: `Read HEARTBEAT.md if exists. Consider outstanding tasks. Checkup sometimes on your human during (user local) day time.` -You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it small. +You are free to edit `HEARTBEAT.md` with a short checklist or reminders. Keep it small to limit token burn. **Things to check (rotate through these, 2-4 times per day):** - **Emails** - Any urgent unread messages? diff --git a/docs/templates/HEARTBEAT.md b/docs/templates/HEARTBEAT.md index 4d300f421..45d86581f 100644 --- a/docs/templates/HEARTBEAT.md +++ b/docs/templates/HEARTBEAT.md @@ -5,4 +5,4 @@ read_when: --- # HEARTBEAT.md -Keep this file empty unless you want a tiny checklist for heartbeat runs. Keep it small. +Keep this file empty unless you want a tiny checklist. Keep it small. diff --git a/src/agents/workspace.ts b/src/agents/workspace.ts index a27dc7e5f..f29e97271 100644 --- a/src/agents/workspace.ts +++ b/src/agents/workspace.ts @@ -87,10 +87,9 @@ It does not define which tools exist; Clawdbot provides built-in tools internall Add whatever else you want the assistant to know about your local toolchain. `; -const DEFAULT_HEARTBEAT_TEMPLATE = `# HEARTBEAT.md - Optional heartbeat notes +const DEFAULT_HEARTBEAT_TEMPLATE = `# HEARTBEAT.md -Keep this file small. Leave it empty unless you want a short checklist or reminders -to follow during heartbeat runs. +Keep this file empty unless you want a tiny checklist. Keep it small. `; const DEFAULT_BOOTSTRAP_TEMPLATE = `# BOOTSTRAP.md - First Run Ritual (delete after) From 825a692390dcde5a26287172ee75e60cf4f9268c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 22:32:01 +0000 Subject: [PATCH 2/6] docs: add cron redirect --- docs/docs.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/docs.json b/docs/docs.json index 8802512e9..0bd5a3eb2 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -20,6 +20,16 @@ "url": "https://github.com/clawdbot/clawdbot/releases" } ], + "redirects": [ + { + "source": "/cron", + "destination": "/cron-jobs" + }, + { + "source": "/cron/", + "destination": "/cron-jobs" + } + ], "navigation": { "groups": [ { From 5939363eed48bc741ac152c7c7b021a395d732a7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 22:34:02 +0000 Subject: [PATCH 3/6] fix: include telegram group sender in envelope headers --- CHANGELOG.md | 1 + src/telegram/bot.test.ts | 43 ++++++++++++++++++++++++++++++++++++++++ src/telegram/bot.ts | 32 +++++++++++++++++++++++++----- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31acd0c15..5386ac4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Browser: fix `browser snapshot`/`browser act` timeouts under Bun by patching Playwright’s CDP WebSocket selection. Thanks @azade-c for PR #307. - Browser: add `--browser-profile` flag and honor profile in tabs routes + browser tool. Thanks @jamesgroat for PR #324. - Telegram: stop typing after tool results. Thanks @AbhisekBasu1 for PR #322. +- Telegram: include sender identity in group envelope headers. (#336) - Messages: stop defaulting ack reactions to 👀 when identity emoji is missing. - Auto-reply: require slash for control commands to avoid false triggers in normal text. - Auto-reply: flag error payloads and improve Bun socket error messaging. Thanks @emanuelst for PR #331. diff --git a/src/telegram/bot.test.ts b/src/telegram/bot.test.ts index e9553508e..99e6634c1 100644 --- a/src/telegram/bot.test.ts +++ b/src/telegram/bot.test.ts @@ -241,6 +241,49 @@ describe("createTelegramBot", () => { expect(replySpy).toHaveBeenCalledTimes(1); const payload = replySpy.mock.calls[0][0]; expect(payload.WasMentioned).toBe(true); + expect(payload.Body).toMatch( + /^\[Telegram Test Group id:7 from Ada id:9 2025-01-09T00:00Z\]/, + ); + }); + + it("includes sender identity in group envelope headers", async () => { + onSpy.mockReset(); + const replySpy = replyModule.__replySpy as unknown as ReturnType< + typeof vi.fn + >; + replySpy.mockReset(); + + loadConfig.mockReturnValue({ + telegram: { groups: { "*": { requireMention: false } } }, + }); + + createTelegramBot({ token: "tok" }); + const handler = onSpy.mock.calls[0][1] as ( + ctx: Record, + ) => Promise; + + await handler({ + message: { + chat: { id: 42, type: "group", title: "Ops" }, + text: "hello", + date: 1736380800, + message_id: 2, + from: { + id: 99, + first_name: "Ada", + last_name: "Lovelace", + username: "ada", + }, + }, + me: { username: "clawdbot_bot" }, + getFile: async () => ({ download: async () => new Uint8Array() }), + }); + + expect(replySpy).toHaveBeenCalledTimes(1); + const payload = replySpy.mock.calls[0][0]; + expect(payload.Body).toMatch( + /^\[Telegram Ops id:42 from Ada Lovelace \(@ada\) id:99 2025-01-09T00:00Z\]/, + ); }); it("reacts to mention-gated group messages when ackReaction is enabled", async () => { diff --git a/src/telegram/bot.ts b/src/telegram/bot.ts index 296f5b15c..650cae7b9 100644 --- a/src/telegram/bot.ts +++ b/src/telegram/bot.ts @@ -375,8 +375,8 @@ export function createTelegramBot(opts: TelegramBotOptions) { const body = formatAgentEnvelope({ provider: "Telegram", from: isGroup - ? buildGroupLabel(msg, chatId) - : buildSenderLabel(msg, chatId), + ? buildGroupFromLabel(msg, chatId, senderId) + : buildSenderLabel(msg, senderId || chatId), timestamp: msg.date ? msg.date * 1000 : undefined, body: `${bodyText}${replySuffix}`, }); @@ -874,7 +874,10 @@ function buildSenderName(msg: TelegramMessage) { return name || undefined; } -function buildSenderLabel(msg: TelegramMessage, chatId: number | string) { +function buildSenderLabel( + msg: TelegramMessage, + senderId?: number | string, +) { const name = buildSenderName(msg); const username = msg.from?.username ? `@${msg.from.username}` : undefined; let label = name; @@ -883,8 +886,17 @@ function buildSenderLabel(msg: TelegramMessage, chatId: number | string) { } else if (!name && username) { label = username; } - const idPart = `id:${chatId}`; - return label ? `${label} ${idPart}` : idPart; + const normalizedSenderId = + senderId != null && `${senderId}`.trim() + ? `${senderId}`.trim() + : undefined; + const fallbackId = + normalizedSenderId ?? + (msg.from?.id != null ? String(msg.from.id) : undefined); + const idPart = fallbackId ? `id:${fallbackId}` : undefined; + if (label && idPart) return `${label} ${idPart}`; + if (label) return label; + return idPart ?? "id:unknown"; } function buildGroupLabel(msg: TelegramMessage, chatId: number | string) { @@ -893,6 +905,16 @@ function buildGroupLabel(msg: TelegramMessage, chatId: number | string) { return `group:${chatId}`; } +function buildGroupFromLabel( + msg: TelegramMessage, + chatId: number | string, + senderId?: number | string, +) { + const groupLabel = buildGroupLabel(msg, chatId); + const senderLabel = buildSenderLabel(msg, senderId); + return `${groupLabel} from ${senderLabel}`; +} + function hasBotMention(msg: TelegramMessage, botUsername: string) { const text = (msg.text ?? msg.caption ?? "").toLowerCase(); if (text.includes(`@${botUsername}`)) return true; From e6d6c822c57a80c365f49561741bd419ba121117 Mon Sep 17 00:00:00 2001 From: Dante Lex Date: Tue, 6 Jan 2026 16:37:08 -0500 Subject: [PATCH 4/6] feat: add himalaya email CLI skill Add skill for Himalaya (https://github.com/pimalaya/himalaya), a CLI email client supporting IMAP, SMTP, Notmuch, and Sendmail backends. Includes: - SKILL.md with common operations (list, read, reply, forward, send) - Configuration reference for Gmail, iCloud, and generic IMAP/SMTP - MML (MIME Meta Language) composition guide for attachments Tested with iCloud IMAP account - verified folder listing, email reading, and sending work correctly. --- skills/himalaya/SKILL.md | 225 ++++++++++++++++++ skills/himalaya/references/configuration.md | 175 ++++++++++++++ .../references/message-composition.md | 182 ++++++++++++++ 3 files changed, 582 insertions(+) create mode 100644 skills/himalaya/SKILL.md create mode 100644 skills/himalaya/references/configuration.md create mode 100644 skills/himalaya/references/message-composition.md diff --git a/skills/himalaya/SKILL.md b/skills/himalaya/SKILL.md new file mode 100644 index 000000000..452aadc8c --- /dev/null +++ b/skills/himalaya/SKILL.md @@ -0,0 +1,225 @@ +--- +name: himalaya +description: "CLI to manage emails via IMAP/SMTP. Use `himalaya` to list, read, write, reply, forward, search, and organize emails from the terminal. Supports multiple accounts and message composition with MML (MIME Meta Language)." +homepage: https://github.com/pimalaya/himalaya +metadata: {"clawdbot":{"emoji":"📧","requires":{"bins":["himalaya"]},"install":[{"id":"brew","kind":"brew","formula":"himalaya","bins":["himalaya"],"label":"Install Himalaya (brew)"},{"id":"cargo","kind":"shell","cmd":"cargo install himalaya","bins":["himalaya"],"label":"Install Himalaya (cargo)"}]}} +--- + +# Himalaya Email CLI + +Himalaya is a CLI email client that lets you manage emails from the terminal using IMAP, SMTP, Notmuch, or Sendmail backends. + +## References + +- `references/configuration.md` (config file setup + IMAP/SMTP authentication) +- `references/message-composition.md` (MML syntax for composing emails) + +## Prerequisites + +1. Himalaya CLI installed (`himalaya --version` to verify) +2. A configuration file at `~/.config/himalaya/config.toml` +3. IMAP/SMTP credentials configured (password stored securely) + +## Configuration Setup + +Run the interactive wizard to set up an account: +```bash +himalaya account configure +``` + +Or create `~/.config/himalaya/config.toml` manually: +```toml +[accounts.personal] +email = "you@example.com" +display-name = "Your Name" +default = true + +backend.type = "imap" +backend.host = "imap.example.com" +backend.port = 993 +backend.encryption.type = "tls" +backend.login = "you@example.com" +backend.auth.type = "password" +backend.auth.cmd = "pass show email/imap" # or use keyring + +message.send.backend.type = "smtp" +message.send.backend.host = "smtp.example.com" +message.send.backend.port = 587 +message.send.backend.encryption.type = "start-tls" +message.send.backend.login = "you@example.com" +message.send.backend.auth.type = "password" +message.send.backend.auth.cmd = "pass show email/smtp" +``` + +## Common Operations + +### List Folders + +```bash +himalaya folder list +``` + +### List Emails + +List emails in INBOX (default): +```bash +himalaya envelope list +``` + +List emails in a specific folder: +```bash +himalaya envelope list --folder "Sent" +``` + +List with pagination: +```bash +himalaya envelope list --page 1 --page-size 20 +``` + +### Search Emails + +```bash +himalaya envelope list --query "from:john@example.com subject:meeting" +``` + +### Read an Email + +Read email by ID (shows plain text): +```bash +himalaya message read 42 +``` + +Read as raw MIME: +```bash +himalaya message read 42 --raw +``` + +### Reply to an Email + +Interactive reply (opens $EDITOR): +```bash +himalaya message reply 42 +``` + +Reply-all: +```bash +himalaya message reply 42 --all +``` + +### Forward an Email + +```bash +himalaya message forward 42 +``` + +### Write a New Email + +Interactive compose (opens $EDITOR): +```bash +himalaya message write +``` + +Send directly using template: +```bash +cat << 'EOF' | himalaya template send +From: you@example.com +To: recipient@example.com +Subject: Test Message + +Hello from Himalaya! +EOF +``` + +Or with headers flag: +```bash +himalaya message write -H "To:recipient@example.com" -H "Subject:Test" "Message body here" +``` + +### Move/Copy Emails + +Move to folder: +```bash +himalaya message move 42 "Archive" +``` + +Copy to folder: +```bash +himalaya message copy 42 "Important" +``` + +### Delete an Email + +```bash +himalaya message delete 42 +``` + +### Manage Flags + +Add flag: +```bash +himalaya flag add 42 --flag seen +``` + +Remove flag: +```bash +himalaya flag remove 42 --flag seen +``` + +## Multiple Accounts + +List accounts: +```bash +himalaya account list +``` + +Use a specific account: +```bash +himalaya --account work envelope list +``` + +## Attachments + +Save attachments from a message: +```bash +himalaya attachment download 42 +``` + +Save to specific directory: +```bash +himalaya attachment download 42 --dir ~/Downloads +``` + +## Output Formats + +Most commands support `--output` for structured output: +```bash +himalaya envelope list --output json +himalaya envelope list --output plain +``` + +## Sync Mode + +For faster access, enable local caching: +```bash +himalaya account sync personal +``` + +## Debugging + +Enable debug logging: +```bash +RUST_LOG=debug himalaya envelope list +``` + +Full trace with backtrace: +```bash +RUST_LOG=trace RUST_BACKTRACE=1 himalaya envelope list +``` + +## Tips + +- Use `himalaya --help` or `himalaya --help` for detailed usage. +- Message IDs are relative to the current folder; re-list after folder changes. +- For composing rich emails with attachments, use MML syntax (see `references/message-composition.md`). +- Store passwords securely using `pass`, system keyring, or a command that outputs the password. + diff --git a/skills/himalaya/references/configuration.md b/skills/himalaya/references/configuration.md new file mode 100644 index 000000000..42c252628 --- /dev/null +++ b/skills/himalaya/references/configuration.md @@ -0,0 +1,175 @@ +# Himalaya Configuration Reference + +Configuration file location: `~/.config/himalaya/config.toml` + +## Minimal IMAP + SMTP Setup + +```toml +[accounts.default] +email = "user@example.com" +display-name = "Your Name" +default = true + +# IMAP backend for reading emails +backend.type = "imap" +backend.host = "imap.example.com" +backend.port = 993 +backend.encryption.type = "tls" +backend.login = "user@example.com" +backend.auth.type = "password" +backend.auth.raw = "your-password" + +# SMTP backend for sending emails +message.send.backend.type = "smtp" +message.send.backend.host = "smtp.example.com" +message.send.backend.port = 587 +message.send.backend.encryption.type = "start-tls" +message.send.backend.login = "user@example.com" +message.send.backend.auth.type = "password" +message.send.backend.auth.raw = "your-password" +``` + +## Password Options + +### Raw password (testing only, not recommended) +```toml +backend.auth.raw = "your-password" +``` + +### Password from command (recommended) +```toml +backend.auth.cmd = "pass show email/imap" +backend.auth.cmd = "security find-generic-password -a user@example.com -s imap -w" +``` + +### System keyring (requires keyring feature) +```toml +backend.auth.keyring = "imap-example" +``` +Then run `himalaya configure -a ` to store the password. + +## Gmail Configuration + +```toml +[accounts.gmail] +email = "you@gmail.com" +display-name = "Your Name" +default = true + +backend.type = "imap" +backend.host = "imap.gmail.com" +backend.port = 993 +backend.encryption.type = "tls" +backend.login = "you@gmail.com" +backend.auth.type = "password" +backend.auth.cmd = "pass show google/app-password" + +message.send.backend.type = "smtp" +message.send.backend.host = "smtp.gmail.com" +message.send.backend.port = 587 +message.send.backend.encryption.type = "start-tls" +message.send.backend.login = "you@gmail.com" +message.send.backend.auth.type = "password" +message.send.backend.auth.cmd = "pass show google/app-password" +``` + +**Note:** Gmail requires an App Password if 2FA is enabled. + +## iCloud Configuration + +```toml +[accounts.icloud] +email = "you@icloud.com" +display-name = "Your Name" + +backend.type = "imap" +backend.host = "imap.mail.me.com" +backend.port = 993 +backend.encryption.type = "tls" +backend.login = "you@icloud.com" +backend.auth.type = "password" +backend.auth.cmd = "pass show icloud/app-password" + +message.send.backend.type = "smtp" +message.send.backend.host = "smtp.mail.me.com" +message.send.backend.port = 587 +message.send.backend.encryption.type = "start-tls" +message.send.backend.login = "you@icloud.com" +message.send.backend.auth.type = "password" +message.send.backend.auth.cmd = "pass show icloud/app-password" +``` + +**Note:** Generate an app-specific password at appleid.apple.com + +## Folder Aliases + +Map custom folder names: +```toml +[accounts.default.folder.alias] +inbox = "INBOX" +sent = "Sent" +drafts = "Drafts" +trash = "Trash" +``` + +## Multiple Accounts + +```toml +[accounts.personal] +email = "personal@example.com" +default = true +# ... backend config ... + +[accounts.work] +email = "work@company.com" +# ... backend config ... +``` + +Switch accounts with `--account`: +```bash +himalaya --account work envelope list +``` + +## Notmuch Backend (local mail) + +```toml +[accounts.local] +email = "user@example.com" + +backend.type = "notmuch" +backend.db-path = "~/.mail/.notmuch" +``` + +## OAuth2 Authentication (for providers that support it) + +```toml +backend.auth.type = "oauth2" +backend.auth.client-id = "your-client-id" +backend.auth.client-secret.cmd = "pass show oauth/client-secret" +backend.auth.access-token.cmd = "pass show oauth/access-token" +backend.auth.refresh-token.cmd = "pass show oauth/refresh-token" +backend.auth.auth-url = "https://provider.com/oauth/authorize" +backend.auth.token-url = "https://provider.com/oauth/token" +``` + +## Additional Options + +### Signature +```toml +[accounts.default] +signature = "Best regards,\nYour Name" +signature-delim = "-- \n" +``` + +### Downloads directory +```toml +[accounts.default] +downloads-dir = "~/Downloads/himalaya" +``` + +### Editor for composing +Set via environment variable: +```bash +export EDITOR="vim" +``` + diff --git a/skills/himalaya/references/message-composition.md b/skills/himalaya/references/message-composition.md new file mode 100644 index 000000000..9e22c1eec --- /dev/null +++ b/skills/himalaya/references/message-composition.md @@ -0,0 +1,182 @@ +# Message Composition with MML (MIME Meta Language) + +Himalaya uses MML for composing emails. MML is a simple XML-based syntax that compiles to MIME messages. + +## Basic Message Structure + +An email message is a list of **headers** followed by a **body**, separated by a blank line: + +``` +From: sender@example.com +To: recipient@example.com +Subject: Hello World + +This is the message body. +``` + +## Headers + +Common headers: +- `From`: Sender address +- `To`: Primary recipient(s) +- `Cc`: Carbon copy recipients +- `Bcc`: Blind carbon copy recipients +- `Subject`: Message subject +- `Reply-To`: Address for replies (if different from From) +- `In-Reply-To`: Message ID being replied to + +### Address Formats + +``` +To: user@example.com +To: John Doe +To: "John Doe" +To: user1@example.com, user2@example.com, "Jane" +``` + +## Plain Text Body + +Simple plain text email: +``` +From: alice@localhost +To: bob@localhost +Subject: Plain Text Example + +Hello, this is a plain text email. +No special formatting needed. + +Best, +Alice +``` + +## MML for Rich Emails + +### Multipart Messages + +Alternative text/html parts: +``` +From: alice@localhost +To: bob@localhost +Subject: Multipart Example + +<#multipart type=alternative> +This is the plain text version. +<#part type=text/html> +

This is the HTML version

+<#/multipart> +``` + +### Attachments + +Attach a file: +``` +From: alice@localhost +To: bob@localhost +Subject: With Attachment + +Here is the document you requested. + +<#part filename=/path/to/document.pdf><#/part> +``` + +Attachment with custom name: +``` +<#part filename=/path/to/file.pdf name=report.pdf><#/part> +``` + +Multiple attachments: +``` +<#part filename=/path/to/doc1.pdf><#/part> +<#part filename=/path/to/doc2.pdf><#/part> +``` + +### Inline Images + +Embed an image inline: +``` +From: alice@localhost +To: bob@localhost +Subject: Inline Image + +<#multipart type=related> +<#part type=text/html> + +

Check out this image:

+ + +<#part disposition=inline id=image1 filename=/path/to/image.png><#/part> +<#/multipart> +``` + +### Mixed Content (Text + Attachments) + +``` +From: alice@localhost +To: bob@localhost +Subject: Mixed Content + +<#multipart type=mixed> +<#part type=text/plain> +Please find the attached files. + +Best, +Alice +<#part filename=/path/to/file1.pdf><#/part> +<#part filename=/path/to/file2.zip><#/part> +<#/multipart> +``` + +## MML Tag Reference + +### `<#multipart>` +Groups multiple parts together. +- `type=alternative`: Different representations of same content +- `type=mixed`: Independent parts (text + attachments) +- `type=related`: Parts that reference each other (HTML + images) + +### `<#part>` +Defines a message part. +- `type=`: Content type (e.g., `text/html`, `application/pdf`) +- `filename=`: File to attach +- `name=`: Display name for attachment +- `disposition=inline`: Display inline instead of as attachment +- `id=`: Content ID for referencing in HTML + +## Composing from CLI + +### Interactive compose +Opens your `$EDITOR`: +```bash +himalaya message write +``` + +### Reply (opens editor with quoted message) +```bash +himalaya message reply 42 +himalaya message reply 42 --all # reply-all +``` + +### Forward +```bash +himalaya message forward 42 +``` + +### Send from stdin +```bash +cat message.txt | himalaya message write +``` + +### Send with headers from CLI +```bash +echo "Message body here" | himalaya message write \ + --to "recipient@example.com" \ + --subject "Quick Message" +``` + +## Tips + +- The editor opens with a template; fill in headers and body. +- Save and exit the editor to send; exit without saving to cancel. +- MML parts are compiled to proper MIME when sending. +- Use `--raw` with `message read` to see the raw MIME structure of received emails. + From 16243b7edcc0d6ed4b28b7f9b13bb1d1c490f40a Mon Sep 17 00:00:00 2001 From: Dante Lex Date: Tue, 6 Jan 2026 17:03:12 -0500 Subject: [PATCH 5/6] fix: simplify install to brew-only Remove cargo install option to avoid confusing the model with multiple installation methods. --- skills/himalaya/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/himalaya/SKILL.md b/skills/himalaya/SKILL.md index 452aadc8c..4dfc68ab4 100644 --- a/skills/himalaya/SKILL.md +++ b/skills/himalaya/SKILL.md @@ -2,7 +2,7 @@ name: himalaya description: "CLI to manage emails via IMAP/SMTP. Use `himalaya` to list, read, write, reply, forward, search, and organize emails from the terminal. Supports multiple accounts and message composition with MML (MIME Meta Language)." homepage: https://github.com/pimalaya/himalaya -metadata: {"clawdbot":{"emoji":"📧","requires":{"bins":["himalaya"]},"install":[{"id":"brew","kind":"brew","formula":"himalaya","bins":["himalaya"],"label":"Install Himalaya (brew)"},{"id":"cargo","kind":"shell","cmd":"cargo install himalaya","bins":["himalaya"],"label":"Install Himalaya (cargo)"}]}} +metadata: {"clawdbot":{"emoji":"📧","requires":{"bins":["himalaya"]},"install":[{"id":"brew","kind":"brew","formula":"himalaya","bins":["himalaya"],"label":"Install Himalaya (brew)"}]}} --- # Himalaya Email CLI From ea216994a1166f17b22037e8eb95607e1f1927c8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 6 Jan 2026 23:44:06 +0100 Subject: [PATCH 6/6] docs: polish himalaya skill docs --- CHANGELOG.md | 1 + README.md | 2 +- skills/himalaya/SKILL.md | 7 +++---- skills/himalaya/references/configuration.md | 5 ++--- skills/himalaya/references/message-composition.md | 14 +++++++------- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5386ac4e2..5b3865e19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,7 @@ - Models: extend `clawdbot models` status output with a masked auth overview (profiles, env sources, and OAuth counts). ### Maintenance +- Skills: add Himalaya email CLI skill. Thanks @dantelex for PR #335. - Agent: add `skipBootstrap` config option. Thanks @onutc for PR #292. - UI: add favicon.ico derived from the macOS app icon. Thanks @jeffersonwarrior for PR #305. - Tooling: replace tsx with bun for TypeScript execution. Thanks @obviyus for PR #278. diff --git a/README.md b/README.md index 23d1827b9..b32dbac49 100644 --- a/README.md +++ b/README.md @@ -453,5 +453,5 @@ Thanks to all clawtributors: azade-c andranik-sahakyan adamgall jalehman jarvis-medmatic mneves75 regenrek tobiasbischoff MSch obviyus dbhurley Asleep123 Iamadig imfing kitze nachoiacovino VACInc cash-echo-bot claude kiranjd pcty-nextgen-service-account minghinmatthewlam - ngutman onutc oswalpalash snopoke ManuelHettich loukotal hugobarauna AbhisekBasu1 emanuelst + ngutman onutc oswalpalash snopoke ManuelHettich loukotal hugobarauna AbhisekBasu1 emanuelst dantelex

diff --git a/skills/himalaya/SKILL.md b/skills/himalaya/SKILL.md index 4dfc68ab4..5823e31b1 100644 --- a/skills/himalaya/SKILL.md +++ b/skills/himalaya/SKILL.md @@ -79,7 +79,7 @@ himalaya envelope list --page 1 --page-size 20 ### Search Emails ```bash -himalaya envelope list --query "from:john@example.com subject:meeting" +himalaya envelope list --query "from john@example.com subject meeting" ``` ### Read an Email @@ -89,9 +89,9 @@ Read email by ID (shows plain text): himalaya message read 42 ``` -Read as raw MIME: +Export raw MIME: ```bash -himalaya message read 42 --raw +himalaya message export 42 --full ``` ### Reply to an Email @@ -222,4 +222,3 @@ RUST_LOG=trace RUST_BACKTRACE=1 himalaya envelope list - Message IDs are relative to the current folder; re-list after folder changes. - For composing rich emails with attachments, use MML syntax (see `references/message-composition.md`). - Store passwords securely using `pass`, system keyring, or a command that outputs the password. - diff --git a/skills/himalaya/references/configuration.md b/skills/himalaya/references/configuration.md index 42c252628..015049203 100644 --- a/skills/himalaya/references/configuration.md +++ b/skills/himalaya/references/configuration.md @@ -39,14 +39,14 @@ backend.auth.raw = "your-password" ### Password from command (recommended) ```toml backend.auth.cmd = "pass show email/imap" -backend.auth.cmd = "security find-generic-password -a user@example.com -s imap -w" +# backend.auth.cmd = "security find-generic-password -a user@example.com -s imap -w" ``` ### System keyring (requires keyring feature) ```toml backend.auth.keyring = "imap-example" ``` -Then run `himalaya configure -a ` to store the password. +Then run `himalaya account configure ` to store the password. ## Gmail Configuration @@ -172,4 +172,3 @@ Set via environment variable: ```bash export EDITOR="vim" ``` - diff --git a/skills/himalaya/references/message-composition.md b/skills/himalaya/references/message-composition.md index 9e22c1eec..17e40ef37 100644 --- a/skills/himalaya/references/message-composition.md +++ b/skills/himalaya/references/message-composition.md @@ -163,14 +163,15 @@ himalaya message forward 42 ### Send from stdin ```bash -cat message.txt | himalaya message write +cat message.txt | himalaya template send ``` -### Send with headers from CLI +### Prefill headers from CLI ```bash -echo "Message body here" | himalaya message write \ - --to "recipient@example.com" \ - --subject "Quick Message" +himalaya message write \ + -H "To:recipient@example.com" \ + -H "Subject:Quick Message" \ + "Message body here" ``` ## Tips @@ -178,5 +179,4 @@ echo "Message body here" | himalaya message write \ - The editor opens with a template; fill in headers and body. - Save and exit the editor to send; exit without saving to cancel. - MML parts are compiled to proper MIME when sending. -- Use `--raw` with `message read` to see the raw MIME structure of received emails. - +- Use `himalaya message export --full` to inspect the raw MIME structure of received emails.