Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .agents/skills/ccbot-send-file/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
name: ccbot-send-file
description: Deliver a file produced or selected by Claude Code or Codex to the current user's Telegram chat through the repository's built-in `ccbot send-file` relay. Use when an agent needs to attach an image, document, archive, report, or other local file to the CCBot conversation, verify outbound delivery, or troubleshoot target-chat resolution without exposing credentials.
---

# Send a file through CCBot

Use the built-in relay first. Do not ask for the bot token during normal
operation. Keep direct Telegram delivery as the reserve path.

## Deliver

1. Resolve the exact local file and verify that it is a regular file.
2. Run:

```bash
ccbot send-file "/absolute/path/to/file" --caption "Short description"
```

Omit `--caption` when it adds no value. If `ccbot` is not on `PATH`, use the
repository environment, for example `.venv/bin/ccbot send-file ...` or
`uv run ccbot send-file ...`.
3. Treat exit code `0` and the emitted `sent ...: ok` line as delivery proof.
Report a non-zero exit and its sanitized error; do not claim success.

`ccbot send-file` automatically switches from the filesystem relay to direct
Telegram delivery when the daemon relay is unavailable. Let that built-in
fallback finish before trying anything else.

Image extensions `.png`, `.jpg`, `.jpeg`, `.webp`, and `.gif` are sent as
Telegram photos. Other files are sent as documents with their filename.

## Resolve the target safely

Target precedence is:

1. Explicit `--chat-id ID` only when the user requested a specific allowed chat.
2. `CCBOT_CHAT_ID`, injected automatically into an owned Claude/Codex tmux
session. This is the normal path.
3. Every ID from `ALLOWED_USERS` when the session has no single owner.

Do not print IDs unless troubleshooting requires identifying the target and the
user authorized it. The daemon rejects IDs outside `ALLOWED_USERS`.

## Locate configuration without exposing it

Configuration lookup order is repository `.env`, then
`${CCBOT_DIR:-~/.ccbot}/.env`. Relevant variable names are:

- `CCBOT_CHAT_ID`: current session target; normally present in the process
environment, not stored manually.
- `ALLOWED_USERS`: allowed numeric Telegram user IDs.
- `TELEGRAM_BOT_TOKEN`: daemon credential originally obtained from BotFather.

The running daemon already owns `TELEGRAM_BOT_TOKEN`; `ccbot send-file` normally
uses its filesystem relay and does not need the agent to read the token. Check
only whether a variable or config file exists. Never echo, log, paste, commit,
or include token/ID values in a command transcript, caption, filename, or answer.

## Reserve direct channel

Use a manual direct call only when the `ccbot send-file` entry point itself
cannot run, not merely while it is waiting for its relay result. Prefer the
project implementation over handwritten `curl`: load `ccbot.config.config`,
resolve the same target precedence with `ccbot.send_file.resolve_chat_ids`, and
call `ccbot.send_file._send_all(path, caption, chat_ids)` from the repository's
Python environment. Pass the path and caption as arguments or constants; never
embed token or chat-ID values in the script or command line.

The direct channel still reads `TELEGRAM_BOT_TOKEN` and `ALLOWED_USERS` from the
normal configuration lookup. If configuration is absent, stop and report which
variable name is missing. Do not request or reveal its value in chat.

## Guardrails

- Send only the file the user requested or a clearly identified task artifact.
- Inspect filenames and intended contents for credentials, private keys, `.env`
data, access tokens, cookies, personal data, and unrelated workspace content.
- Ask before sending when the file's sensitivity or target is ambiguous.
- Prefer an absolute, explicitly quoted path; do not use broad globs.
- Do not switch to the direct channel or retry blindly after a timeout:
Telegram may have accepted the first delivery. Check the command result and
bot logs first.
- Keep generated artifacts in the task/repository scope; do not copy secrets
into a new file merely to send them.
4 changes: 4 additions & 0 deletions .agents/skills/ccbot-send-file/agents/openai.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface:
display_name: "CCBot Send File"
short_description: "Send generated files through the CCBot relay"
default_prompt: "Use $ccbot-send-file to deliver a generated file to my Telegram chat."
60 changes: 44 additions & 16 deletions .claude/rules/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
Additional modules:
screenshot.py ─ Terminal text → PNG rendering (ANSI color, font fallback)
transcribe.py ─ Voice-to-text transcription via whisper.cpp / Apple Speech
i18n.py ─ Per-user UI strings (en / ru / zh)
i18n.py ─ Compatibility facade for per-user translations
i18n_locales/ ─ English / Russian / Chinese translation catalogs
naming.py ─ lightweight-model-generated session names
usage.py ─ Token usage aggregator + per-session token alerts
main.py ─ CLI entry point (ccbot / ccbot hook / ccbot send-file)
Expand All @@ -83,12 +84,19 @@ Additional modules:
--chat-id > $CCBOT_CHAT_ID > all ALLOWED_USERS)
utils.py ─ Shared utilities (ccbot_dir, atomic_write_json)
session_models.py ─ Session / WindowState / ClaudeSession dataclasses
session_state.py ─ active routing, lifecycle, settings, persistence helpers
session_map.py ─ hook map reconciliation and transcript resolution
session_recovery.py ─ Startup hygiene: reconcile w/ tmux + resolve stale window IDs
session_claude_io.py─ Read-only Claude transcript discovery (encode_cwd, list, get)
transcript_format.py─ Tool-summary + tool-result formatting (was inside TranscriptParser)
transcript_types.py ─ ParsedEntry / ParsedMessage / pending-tool DTOs
transcript_message.py / transcript_codex.py ─ backend-specific normalization
terminal_usage.py ─ /usage models and terminal-output parsing
tmux_process.py ─ orphan process cleanup
tmux_window.py ─ backend command assembly and tmux window creation
logging_setup.py ─ Logging config (level via LOG_LEVEL, JSON via CCBOT_LOG_FORMAT)
metrics.py ─ In-process counters → metrics.json
rich.py ─ Bot API 10.1 rich messages via raw Bot._post
rich.py ─ Bot API 10.2 rich messages via raw Bot._post
(sendRichMessage / rich edit; to_rich_markdown
escapes bare < and maps expandable-quote sentinels
to <details>); safe_* try rich first, fall back to
Expand All @@ -106,7 +114,9 @@ Additional modules:

bot/ package (was bot.py before A1, split per CLAUDE.md size budget):
__init__.py ─ Re-exports create_bot, forward_command_handler
app.py ─ create_bot, post_init/shutdown, handler registration
app.py ─ Compatibility facade + watchdog/error handling
_app_lifecycle.py ─ post_init/post_shutdown orchestration
_app_routes.py ─ Application construction and handler registration
_common.py ─ is_user_allowed, active_window, resolve_ident,
render_session_preview, set_view, open_more_in_place,
is_window_busy, shorten_workdir, CC_COMMANDS
Expand All @@ -116,8 +126,11 @@ bot/ package (was bot.py before A1, split per CLAUDE.md size budget):
modal body; parser picks the LAST modal header
in the buffer to ignore stale prior attempts)
_session_create.py ─ create_and_activate_session (dir-browser → tmux flow)
messages.py ─ text/voice/photo/document handlers, forward_command_handler,
bash !cmd capture
messages.py ─ Compatibility facade for inbound message handlers
_messages_shared.py ─ delivery proof, card bracketing, prompt interception
_messages_text.py ─ text routing and bash capture
_messages_voice.py ─ voice checkpoint/transcription routing
_messages_media.py ─ photo/document/forwarded content handling
session_events.py ─ handle_new_message — claude → TG dispatch
commands/lifecycle.py ─ /new /kill /done /stop /menu /archive
(+ archive_session shared helper)
Expand All @@ -141,11 +154,11 @@ bot/ package (was bot.py before A1, split per CLAUDE.md size budget):

Handler modules (handlers/):
message_sender.py ─ safe_reply/safe_edit/safe_send + send_with_fallback
message_queue.py ─ Per-user queue + worker (merge, status dedup)
status_polling.py ─ Background status line polling (1s interval) +
auto-approve hook for interactive prompts +
bg-window interactive-UI detection (suppress + stash)
notifications.py ─ Live card per session + push events + completion +
status_approval.py ─ pure auto-approval parsing/signature helpers
notifications.py ─ Compatibility facade for live-card orchestration +
bg-status panel injection +
refresh_panel + repost_card (always-repost behaviour:
every user-msg replaces the card by a fresh one below)
Expand All @@ -156,7 +169,9 @@ Handler modules (handlers/):
Persisted in state.json (status/last_change/context_pct;
pending UI re-detected after restart by terminal_parser).
archive.py ─ /archive page rendering + restore + idle/purge sweeps
history.py ─ Paginated /history rendering (with optional extra rows)
archive_blurb.py ─ archive summary text cleanup and formatting
history.py ─ Live paginated /history cache and presentation
history_archive.py ─ archived transcript/card page rendering
quota_alerts.py ─ Background /usage modal poll (default 10 min) →
5h/weekly band crossings 50/75/90 %
inbox.py ─ photo/document inbox under <workdir>/.ccbot-inbox/
Expand All @@ -166,21 +181,34 @@ Handler modules (handlers/):
via notifications.enter_kb_mode on the claimed carrier.
directory_browser.py─ Directory + session picker UI builders
switcher.py ─ Inline session-switcher keyboard
menu.py ─ Footer / More / Settings keyboard composition;
menu.py ─ Footer / More keyboard composition and settings facade;
[+ new] [≡ Menu] share the bottom row on screen="main"
cleanup.py ─ Per-window state cleanup on archive
callback_data.py ─ Callback data prefix constants
tg_format.py ─ Table/code overflow → file attachment
card_model.py ─ Event/CardState dataclasses + render/paginate/seed
helpers (pure model layer split from notifications.py;
notifications.py re-exports its names as a facade)
card_model.py ─ Compatibility facade for card types/render/pagination
card_types.py ─ Event/CardState data only
card_events.py ─ monitor message → card event conversion
card_text.py ─ transcript text parsing and sanitization
card_budget.py ─ line/byte budgeting and chunking
card_event_render.py─ individual event rendering
card_pagination.py ─ page boundaries and user-specific page sizing
card_layout.py ─ complete card body composition
card_registry.py ─ mutable card ownership, locks, and message registry
card_seed.py ─ JSONL seeding
card_carrier.py ─ pause/transfer/restore carrier lifecycle
card_transport.py ─ Telegram send/edit/photo operations
card_rich_media.py ─ RUNNING-only inline pane placement and Rich Markdown
photo reuse (body → pane → context → bg panel; removed
on idle/final/clear; legacy photo/text fallback)
card_updates.py ─ event application/finalization/attachments
card_stall.py ─ silent-turn pane refresh + bg ⚠️ state
card_surface.py ─ timers, panel refresh, and receipt scheduling
kb_mode.py ─ kb-mode keyboard builder + pane-capture-to-PNG helper
typing.py ─ Per-user throttle in front of send_chat_action(TYPING)
(status_polling + session_events share one timer)
response_builder.py ─ Paginated response builder (display truncation)
context_poll.py ─ Background /context poller — PRESENT but DISABLED;
JSONL math (usage.context_pct_for_session) is the live
path (see bot/app.py)

Responsibility-level extension guide: `doc/refactor-architecture.md`.

State files (~/.ccbot/ or $CCBOT_DIR/):
state.json ─ window states + display names + read offsets + user
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/ccbot-send-file
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: ruff format
run: uv run ruff format --check src/ tests/

- name: module size budgets
run: uv run python scripts/check_module_size.py

- name: pyright
run: uv run pyright src/ccbot/

Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ ccbot hook --install # Auto-install Claude Code SessionStart ho
## Code Conventions

- Every `.py` file starts with a module-level docstring: purpose clear within 10 lines, one-sentence summary first line, then core responsibilities and key components.
- Hard module budgets: `src/ccbot/bot/**/*.py` is capped at 600 physical
lines; all other `src/ccbot/**/*.py` modules are capped at 800. Run
`python scripts/check_module_size.py`; split by reason to change rather than
creating generic helper dumps.
- Telegram interaction: prefer inline keyboards over reply keyboards; use `edit_message_text` for in-place updates; keep callback data under 64 bytes; use `answer_callback_query` for instant feedback.

## Configuration
Expand Down
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ fix the offending content — never bypass with `--no-verify`.
This is what the optional `CCBOT_LOG_FORMAT=json` mode uses to
produce one JSON line per record. One-shot logs from rare paths can
stay as plain strings.
- **600-LOC ceiling** under `src/ccbot/bot/`. Files in `src/ccbot/` may
go up to 800 LOC temporarily; over that, decompose in the same PR.
- **Hard module budgets.** Files under `src/ccbot/bot/` are capped at
600 physical lines; every other Python module under `src/ccbot/` is capped
at 800. `python scripts/check_module_size.py` enforces both limits in CI.
Prefer splitting around one reason to change before a file reaches its cap.
- **No comments explaining "what"** — the code says that. Only write
comments when the *why* is non-obvious (a hidden constraint, a
workaround, surprising behaviour).
Expand Down
38 changes: 29 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,20 @@ Card knobs live under *Settings → 🃏 Card / view*:
| ------- | ------- | ------ |
| `Card history` | `20` | end-of-turn boundaries seeded into a fresh card from the JSONL (survives bot restarts) |
| `Page size` | `20` lines | max lines per card page; longer bodies chunk across pages on paragraph/sentence boundaries |
| `Inline screenshots` | `off` | card becomes photo + caption — the photo is the live pane render (caption limit is 1024 chars, so shrink page size to compensate) |
| `Inline screenshots` | `off` | shows the live terminal pane inside the active card while a turn is running |
| `Live lag` | `4s` | coalescing window for preview updates |

The inline pane exists only in the **RUNNING** state. Its order is
`body → gap → pane → gap → context → background panel`; it is removed on
**IDLE**, final answer, and `/clear`, then appears again when the next turn
starts. Rich-capable Bot API servers keep text and media in one Rich Markdown
message. Older servers use photo + caption. A failed rich send falls back to
legacy photo, then text-only; transient edits retry on the next update, and a
lost carrier is recreated without posting an immediate duplicate.
If an unfinished turn goes silent, the active card keeps its pane instead of
inserting a warning or sending a push. The same condition on a background
session is shown only as `⚠️` beside that session in the background panel.

Telegram's chat-header **`typing…` indicator** is driven by real
claude events. As long as the active session keeps emitting (tool
calls, thinking, text), `typing…` stays on; an idle session lets it
Expand Down Expand Up @@ -437,20 +448,25 @@ half-rendered modal can't fire a phantom alert.

## Architecture

The full module map is `.claude/rules/architecture.md`. At a glance:
The full module map is `.claude/rules/architecture.md`; the responsibility-level
extension guide is `doc/refactor-architecture.md`. At a glance:

```
src/ccbot/
├── main.py — CLI entry point (`ccbot`, `ccbot hook`, `ccbot send-file`)
├── config.py — env-var loader (singleton)
├── session.py — Session + SessionManager (state.json)
├── session.py — SessionManager compatibility facade / resume flow
├── session_state.py — routing, lifecycle, settings, state.json
├── session_map.py — hook/window binding reconciliation
├── session_monitor.py — JSONL polling, NewMessage callbacks
├── codex_session_io.py — Codex rollout JSONL discovery and reading
├── codex_auth.py — account/read + device-code login
├── codex_usage.py — Codex app-server rate limits
├── session_import.py — cross-agent restore handoff
├── transcript_parser.py — JSONL turn parsing
├── transcript_parser.py — JSONL parser compatibility facade
├── transcript_*.py — message/Codex/type-specific parsing
├── terminal_parser.py — interactive-UI + status-line detection
├── terminal_usage.py — /usage models and parsing
├── tmux_manager.py — libtmux wrapper
├── rich.py — Bot API 10.1 rich messages (native markdown)
├── markdown_v2.py — MD → Telegram MarkdownV2 (fallback path)
Expand All @@ -460,16 +476,20 @@ src/ccbot/
├── send_file.py — `ccbot send-file` outbound delivery
├── local_terminal.py — native-terminal attach helper
├── usage.py — token aggregator, context %, alert logic
├── i18n.py — en / ru / zh UI strings
├── i18n.py — translation service compatibility facade
├── i18n_locales/ — en / ru / zh UI catalogs
├── bot/ — Telegram-facing handlers (≤ 600 LOC each)
│ ├── app.py — Application bootstrap, post_init / post_shutdown
│ ├── messages.py — text / voice / photo / document / forward
│ ├── app.py — bootstrap/watchdog compatibility facade
│ ├── _app_*.py — lifecycle and handler registration
│ ├── messages.py — inbound compatibility facade
│ ├── _messages_*.py — text / voice / media / shared delivery
│ ├── session_events.py — claude → TG dispatch
│ ├── commands/ — slash command bodies
│ └── callbacks/ — one file per CB_* prefix
└── handlers/
├── notifications.py — live cards + push events
├── card_model.py — card state / render / paginate model layer
├── notifications.py — live-card compatibility facade
├── card_model.py — card-model compatibility facade
├── card_*.py — state / render / paginate / transport / lifecycle
├── bg_status.py — background-session status panel
├── archive.py — /archive page rendering + idle sweeps
├── quota_alerts.py — background /usage poll
Expand Down
Loading
Loading