Install tzdata so TZ resolves, default SECRETARY_TIMEZONE to
Europe/Warsaw, and pass it through as the container-wide TZ so OS
timestamps (logs, date, cron) match the app's scheduling zone.
pi-claude-code-provider fences off Claude Code's MCP servers by design,
so managed MCPs like Google Calendar can't reach the bot through that
provider. Implement calendar access as a first-class Pi tool instead —
it lives alongside `schedule` and calls Google Calendar API v3 directly
with a subscription-free refresh-token grant.
Config: three new optional env vars (GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET, GOOGLE_REFRESH_TOKEN) — set all three to enable
the tool, leave all three blank to disable. Partial config raises a
ConfigError at startup so mis-set env is caught early.
Tool: list / create / update / delete on any calendar (default
'primary'). Access tokens are cached until expiry and refreshed on
demand from the long-lived refresh token. Invalid_grant surfaces a
clear "re-run device flow" error.
Switch from named volumes to bind mounts under one host directory so
the workspace, pi-agent, and claude state is easy to back up / inspect
on the Coolify host.
Also re-adds the sleep-infinity override so the container stays up
while claude auth is redone against the new (empty) claude bind mount.
Override the compose command with `sleep infinity` for now — the deploy
failed because Pi has no auth for pi.dev yet. Once the user has run
`pi login` (or dropped an auth.json in /pi-agent) inside the container,
remove the `command:` line to restore normal app startup.
- main.ts: unhandledRejection/uncaughtException also print the stack (or
message) via console.error so it is not lost when the logger only prints a
single line; bot.start(...) is no longer fire-and-forget -- a polling
failure is logged and exits the process instead of failing silently.
- bridge.ts: /start now greets before showing the same status block as
/status (split back into two command handlers). Command detection now
checks for a bot_command entity at offset 0 instead of a leading "/", so a
message that merely starts with a slash (e.g. a path) reaches the agent
instead of getting "Unknown command.". An unsupported media message with
no text and no other notes (sticker, location, video note, ...) now gets
an explicit "I can handle text, photos, voice notes, and files." reply
instead of silently doing nothing.
- outbound.ts sendMarkdown: only falls back to plain text for Telegram's
400 "can't parse entities" style errors; any other error (network, etc.)
propagates instead of being masked as a silent plain-text resend.
- runner.ts resolveShell: added the per-user Git for Windows bash.exe under
%LOCALAPPDATA% as a third candidate; README's Windows section now
mentions SECRETARY_SHELL for that case.
- docker-compose.yml: added init: true, since Node does not reap zombies
left by killed script process groups.
Claude-Session: https://claude.ai/code/session_01JtuZTCMW9TyogEmBgGf8JT
Docker build verified locally (docker build -t volcanic-secretary .
succeeded on this machine). Step 6's manual end-to-end checklist
against a real Telegram bot was not run in this environment (no bot
token or owner id available); the checklist is kept in the README's
Tests section for the owner to run before relying on this in
production.
Claude-Session: https://claude.ai/code/session_01JtuZTCMW9TyogEmBgGf8JT