Adding a Google service
Existing services (Tasks, Calendar, Gmail) all follow the same structure: a scope ingoogle/auth.py, a module under google/, a CLI subcommand in main.py, and documentation in prompts.py.
Add the OAuth scope
Add the new scope to the
SCOPES list in src/ollim_bot/google/auth.py:src/ollim_bot/google/auth.py
Create the service module
Add a new module under Key conventions:
src/ollim_bot/google/. Follow the pattern established by tasks.py and calendar.py:src/ollim_bot/google/drive.py
_get_*_service()callsget_service(api, version)— credentials are obtained fresh on every callrun_*_command(argv: list[str])is the CLI entry point, usingargparsewith subparsers- Functions called from embed button actions (like
complete_taskanddelete_event) are module-level and return the affected item’s name for confirmation messages
Register the CLI subcommand
Add an entry to the Add the subcommand to the
routes dict in _dispatch_subcommand() in src/ollim_bot/main.py:src/ollim_bot/main.py
HELP string in the same file.Document in the system prompt
Add a section to
SYSTEM_PROMPT in src/ollim_bot/prompts.py describing the new CLI commands. Follow the format of the existing Google Tasks and Calendar sections — list available subcommands, flag names, and usage patterns so the agent knows how to use the integration.Adding an MCP tool
MCP tools are defined insrc/ollim_bot/agent_tools.py using the @tool decorator from claude_agent_sdk. All tools are registered on a single server named discord.
Define the tool function
Use the Tool functions are always
@tool decorator with a name, description, and JSON Schema for parameters:src/ollim_bot/agent_tools.py
async, take a single args dict, and return a dict with a content list of text blocks.Handle execution context
Use the If the tool sends visible output in a background fork, it must respect the ping budget and busy-check gates:For accessing the Discord channel, use the dual-pattern that works in both main and forked contexts:
_source() helper to determine if the tool is running in the main session, an interactive fork, or a background fork:The channel global
_channel is set by set_channel() (main session, protected by the agent lock), while _channel_var is a ContextVar set by set_fork_channel() (background forks, no lock needed). Every path into stream_chat must call both agent_tools.set_channel and permissions.set_channel — this is the channel-sync invariant.Adding a CLI command
CLI commands are dispatched fromsrc/ollim_bot/main.py. Each subcommand is a self-contained module with its own argparse setup.
Register in main.py
Add an entry to the Lazy imports keep startup fast — subcommand modules are only loaded when invoked via
routes dict in _dispatch_subcommand() in src/ollim_bot/main.py:src/ollim_bot/main.py
importlib.import_module.Update the HELP string to include the new subcommand.Adding a webhook spec
Webhook specs are markdown files with YAML frontmatter, stored in~/.ollim-bot/webhooks/. They define how external HTTP requests trigger background agent tasks.
Create the spec file
Add a markdown file in the webhooks directory:The
~/.ollim-bot/webhooks/deploy-notify.md
fields value is a JSON Schema that validates incoming payloads. A maxLength: 500 default is injected for any string field without an explicit limit.Configure optional behavior
The YAML frontmatter supports these optional fields:
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique webhook identifier (used in the URL path) |
message | string | — | Task instruction given to the agent |
fields | object | — | JSON Schema for payload validation |
isolated | boolean | false | Run in an isolated background fork (no main session context) |
model | string | null | Override the Claude model for this webhook |
thinking | boolean | true | Enable extended thinking |
allow_ping | boolean | true | Allow the agent to ping the user |
update_main_session | string | "on_ping" | freely, blocked, always, or on_ping |
Set the webhook secret
The
WEBHOOK_SECRET environment variable must be set. Incoming requests are authenticated via the Authorization header:Understand the dispatch pipeline
When a webhook fires:
- Validation — the payload is checked against the
fieldsJSON Schema. Invalid payloads return400. - Haiku screening — string fields are screened by a fast Claude Haiku call for prompt injection. If any fields are flagged, the webhook is not dispatched.
- Prompt construction —
build_webhook_promptcombines the scheduling preamble (active routines and reminders) with the webhook’smessageand fenced payload data. - Background fork — the prompt is dispatched as a background fork with the spec’s configured behavior (isolated, model, allow_ping, etc.).
Webhook data is fenced with explicit labels in the prompt —
WEBHOOK DATA (untrusted external input) is separated from TASK (from your webhook spec) — so the agent can distinguish data from instructions. The Haiku screening step runs before the main agent sees any payload content.