ccgram 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. ccgram-2.0.0/.claude/rules/architecture.md +142 -0
  2. ccgram-2.0.0/.claude/rules/message-handling.md +40 -0
  3. ccgram-2.0.0/.claude/rules/topic-architecture.md +79 -0
  4. ccgram-2.0.0/.claude/skills/releasing/SKILL.md +38 -0
  5. ccgram-2.0.0/.env.example +32 -0
  6. ccgram-2.0.0/.github/workflows/ci.yml +33 -0
  7. ccgram-2.0.0/.github/workflows/release.yml +63 -0
  8. ccgram-2.0.0/.gitignore +55 -0
  9. ccgram-2.0.0/CHANGELOG.md +121 -0
  10. ccgram-2.0.0/CLAUDE.md +228 -0
  11. ccgram-2.0.0/LICENSE +22 -0
  12. ccgram-2.0.0/Makefile +40 -0
  13. ccgram-2.0.0/PKG-INFO +234 -0
  14. ccgram-2.0.0/README.md +193 -0
  15. ccgram-2.0.0/docs/ai-agents/README.md +58 -0
  16. ccgram-2.0.0/docs/ai-agents/architecture-map.md +101 -0
  17. ccgram-2.0.0/docs/ai-agents/codebase-index.md +149 -0
  18. ccgram-2.0.0/docs/ai-agents/extension-and-fix-playbook.md +80 -0
  19. ccgram-2.0.0/docs/ai-agents/tooling-and-tests.md +61 -0
  20. ccgram-2.0.0/docs/guides.md +347 -0
  21. ccgram-2.0.0/llm.txt +54 -0
  22. ccgram-2.0.0/pyproject.toml +141 -0
  23. ccgram-2.0.0/scripts/generate_homebrew_formula.py +167 -0
  24. ccgram-2.0.0/scripts/restart.sh +146 -0
  25. ccgram-2.0.0/src/ccgram/__init__.py +10 -0
  26. ccgram-2.0.0/src/ccgram/_version.py +34 -0
  27. ccgram-2.0.0/src/ccgram/bot.py +1780 -0
  28. ccgram-2.0.0/src/ccgram/cc_commands.py +285 -0
  29. ccgram-2.0.0/src/ccgram/cli.py +229 -0
  30. ccgram-2.0.0/src/ccgram/codex_status.py +237 -0
  31. ccgram-2.0.0/src/ccgram/command_catalog.py +171 -0
  32. ccgram-2.0.0/src/ccgram/config.py +133 -0
  33. ccgram-2.0.0/src/ccgram/doctor_cmd.py +334 -0
  34. ccgram-2.0.0/src/ccgram/fonts/JetBrainsMono-Regular.ttf +0 -0
  35. ccgram-2.0.0/src/ccgram/fonts/LICENSE-JetBrainsMono.txt +93 -0
  36. ccgram-2.0.0/src/ccgram/fonts/LICENSE-NotoSansMono.txt +92 -0
  37. ccgram-2.0.0/src/ccgram/fonts/LICENSE-Symbola.txt +8 -0
  38. ccgram-2.0.0/src/ccgram/fonts/NotoSansMonoCJKsc-Regular.otf +0 -0
  39. ccgram-2.0.0/src/ccgram/fonts/Symbola.ttf +0 -0
  40. ccgram-2.0.0/src/ccgram/handlers/__init__.py +13 -0
  41. ccgram-2.0.0/src/ccgram/handlers/callback_data.py +101 -0
  42. ccgram-2.0.0/src/ccgram/handlers/callback_helpers.py +28 -0
  43. ccgram-2.0.0/src/ccgram/handlers/cleanup.py +94 -0
  44. ccgram-2.0.0/src/ccgram/handlers/command_history.py +61 -0
  45. ccgram-2.0.0/src/ccgram/handlers/directory_browser.py +338 -0
  46. ccgram-2.0.0/src/ccgram/handlers/directory_callbacks.py +565 -0
  47. ccgram-2.0.0/src/ccgram/handlers/file_handler.py +272 -0
  48. ccgram-2.0.0/src/ccgram/handlers/history.py +201 -0
  49. ccgram-2.0.0/src/ccgram/handlers/history_callbacks.py +72 -0
  50. ccgram-2.0.0/src/ccgram/handlers/hook_events.py +269 -0
  51. ccgram-2.0.0/src/ccgram/handlers/interactive_callbacks.py +123 -0
  52. ccgram-2.0.0/src/ccgram/handlers/interactive_ui.py +327 -0
  53. ccgram-2.0.0/src/ccgram/handlers/message_queue.py +717 -0
  54. ccgram-2.0.0/src/ccgram/handlers/message_sender.py +223 -0
  55. ccgram-2.0.0/src/ccgram/handlers/recovery_callbacks.py +631 -0
  56. ccgram-2.0.0/src/ccgram/handlers/response_builder.py +95 -0
  57. ccgram-2.0.0/src/ccgram/handlers/restore_command.py +97 -0
  58. ccgram-2.0.0/src/ccgram/handlers/resume_command.py +434 -0
  59. ccgram-2.0.0/src/ccgram/handlers/screenshot_callbacks.py +389 -0
  60. ccgram-2.0.0/src/ccgram/handlers/sessions_dashboard.py +181 -0
  61. ccgram-2.0.0/src/ccgram/handlers/status_polling.py +1071 -0
  62. ccgram-2.0.0/src/ccgram/handlers/sync_command.py +262 -0
  63. ccgram-2.0.0/src/ccgram/handlers/text_handler.py +418 -0
  64. ccgram-2.0.0/src/ccgram/handlers/topic_emoji.py +220 -0
  65. ccgram-2.0.0/src/ccgram/handlers/upgrade.py +96 -0
  66. ccgram-2.0.0/src/ccgram/handlers/user_state.py +11 -0
  67. ccgram-2.0.0/src/ccgram/handlers/window_callbacks.py +180 -0
  68. ccgram-2.0.0/src/ccgram/hook.py +583 -0
  69. ccgram-2.0.0/src/ccgram/interactive_prompt_formatter.py +245 -0
  70. ccgram-2.0.0/src/ccgram/main.py +120 -0
  71. ccgram-2.0.0/src/ccgram/markdown_v2.py +168 -0
  72. ccgram-2.0.0/src/ccgram/monitor_state.py +110 -0
  73. ccgram-2.0.0/src/ccgram/providers/__init__.py +241 -0
  74. ccgram-2.0.0/src/ccgram/providers/_jsonl.py +244 -0
  75. ccgram-2.0.0/src/ccgram/providers/base.py +260 -0
  76. ccgram-2.0.0/src/ccgram/providers/claude.py +213 -0
  77. ccgram-2.0.0/src/ccgram/providers/codex.py +702 -0
  78. ccgram-2.0.0/src/ccgram/providers/gemini.py +752 -0
  79. ccgram-2.0.0/src/ccgram/providers/registry.py +62 -0
  80. ccgram-2.0.0/src/ccgram/screen_buffer.py +48 -0
  81. ccgram-2.0.0/src/ccgram/screenshot.py +338 -0
  82. ccgram-2.0.0/src/ccgram/session.py +1523 -0
  83. ccgram-2.0.0/src/ccgram/session_monitor.py +819 -0
  84. ccgram-2.0.0/src/ccgram/state_persistence.py +71 -0
  85. ccgram-2.0.0/src/ccgram/status_cmd.py +133 -0
  86. ccgram-2.0.0/src/ccgram/telegram_sender.py +43 -0
  87. ccgram-2.0.0/src/ccgram/terminal_parser.py +552 -0
  88. ccgram-2.0.0/src/ccgram/tmux_manager.py +907 -0
  89. ccgram-2.0.0/src/ccgram/transcript_parser.py +714 -0
  90. ccgram-2.0.0/src/ccgram/utils.py +223 -0
  91. ccgram-2.0.0/src/ccgram/window_resolver.py +200 -0
  92. ccgram-2.0.0/tests/ccgram/conftest.py +174 -0
  93. ccgram-2.0.0/tests/ccgram/handlers/__init__.py +0 -0
  94. ccgram-2.0.0/tests/ccgram/handlers/test_command_history.py +104 -0
  95. ccgram-2.0.0/tests/ccgram/handlers/test_history.py +33 -0
  96. ccgram-2.0.0/tests/ccgram/handlers/test_response_builder.py +58 -0
  97. ccgram-2.0.0/tests/ccgram/test_bot_callbacks.py +55 -0
  98. ccgram-2.0.0/tests/ccgram/test_callback_auth.py +31 -0
  99. ccgram-2.0.0/tests/ccgram/test_cc_commands.py +407 -0
  100. ccgram-2.0.0/tests/ccgram/test_claude_characterization.py +207 -0
  101. ccgram-2.0.0/tests/ccgram/test_cleanup.py +58 -0
  102. ccgram-2.0.0/tests/ccgram/test_cli.py +156 -0
  103. ccgram-2.0.0/tests/ccgram/test_codex_status.py +152 -0
  104. ccgram-2.0.0/tests/ccgram/test_command_catalog.py +125 -0
  105. ccgram-2.0.0/tests/ccgram/test_commands_command.py +292 -0
  106. ccgram-2.0.0/tests/ccgram/test_config.py +111 -0
  107. ccgram-2.0.0/tests/ccgram/test_directory_browser.py +123 -0
  108. ccgram-2.0.0/tests/ccgram/test_doctor_cmd.py +261 -0
  109. ccgram-2.0.0/tests/ccgram/test_emdash_integration.py +353 -0
  110. ccgram-2.0.0/tests/ccgram/test_file_handler.py +122 -0
  111. ccgram-2.0.0/tests/ccgram/test_forward_command.py +561 -0
  112. ccgram-2.0.0/tests/ccgram/test_group_filter.py +148 -0
  113. ccgram-2.0.0/tests/ccgram/test_handle_new_window.py +311 -0
  114. ccgram-2.0.0/tests/ccgram/test_hook.py +627 -0
  115. ccgram-2.0.0/tests/ccgram/test_hook_events.py +343 -0
  116. ccgram-2.0.0/tests/ccgram/test_interactive_prompt_formatter.py +153 -0
  117. ccgram-2.0.0/tests/ccgram/test_interactive_ui.py +114 -0
  118. ccgram-2.0.0/tests/ccgram/test_jsonl_providers.py +1648 -0
  119. ccgram-2.0.0/tests/ccgram/test_kill_command.py +101 -0
  120. ccgram-2.0.0/tests/ccgram/test_markdown_v2.py +126 -0
  121. ccgram-2.0.0/tests/ccgram/test_message_queue_properties.py +261 -0
  122. ccgram-2.0.0/tests/ccgram/test_message_sender.py +197 -0
  123. ccgram-2.0.0/tests/ccgram/test_monitor_state.py +145 -0
  124. ccgram-2.0.0/tests/ccgram/test_new_command.py +111 -0
  125. ccgram-2.0.0/tests/ccgram/test_new_window_sync.py +365 -0
  126. ccgram-2.0.0/tests/ccgram/test_provider_autodetect.py +331 -0
  127. ccgram-2.0.0/tests/ccgram/test_provider_contracts.py +424 -0
  128. ccgram-2.0.0/tests/ccgram/test_provider_registry.py +325 -0
  129. ccgram-2.0.0/tests/ccgram/test_provider_selection.py +305 -0
  130. ccgram-2.0.0/tests/ccgram/test_recovery_ui.py +1299 -0
  131. ccgram-2.0.0/tests/ccgram/test_restore_command.py +180 -0
  132. ccgram-2.0.0/tests/ccgram/test_resume_command.py +1009 -0
  133. ccgram-2.0.0/tests/ccgram/test_screen_buffer.py +133 -0
  134. ccgram-2.0.0/tests/ccgram/test_session.py +1035 -0
  135. ccgram-2.0.0/tests/ccgram/test_session_favorites.py +109 -0
  136. ccgram-2.0.0/tests/ccgram/test_session_monitor.py +740 -0
  137. ccgram-2.0.0/tests/ccgram/test_session_monitor_events.py +175 -0
  138. ccgram-2.0.0/tests/ccgram/test_session_notification_mode.py +132 -0
  139. ccgram-2.0.0/tests/ccgram/test_sessions_dashboard.py +261 -0
  140. ccgram-2.0.0/tests/ccgram/test_state_migration.py +32 -0
  141. ccgram-2.0.0/tests/ccgram/test_status_buttons.py +85 -0
  142. ccgram-2.0.0/tests/ccgram/test_status_cmd.py +131 -0
  143. ccgram-2.0.0/tests/ccgram/test_status_polling.py +1316 -0
  144. ccgram-2.0.0/tests/ccgram/test_status_recall_callback.py +101 -0
  145. ccgram-2.0.0/tests/ccgram/test_sync_command.py +390 -0
  146. ccgram-2.0.0/tests/ccgram/test_task_utils.py +96 -0
  147. ccgram-2.0.0/tests/ccgram/test_telegram_sender.py +81 -0
  148. ccgram-2.0.0/tests/ccgram/test_terminal_parser.py +865 -0
  149. ccgram-2.0.0/tests/ccgram/test_text_handler.py +483 -0
  150. ccgram-2.0.0/tests/ccgram/test_topic_edited.py +138 -0
  151. ccgram-2.0.0/tests/ccgram/test_topic_emoji.py +451 -0
  152. ccgram-2.0.0/tests/ccgram/test_transcript_parser.py +561 -0
  153. ccgram-2.0.0/tests/ccgram/test_utils.py +332 -0
  154. ccgram-2.0.0/tests/ccgram/test_vim_mode.py +457 -0
  155. ccgram-2.0.0/tests/ccgram/test_window_callbacks.py +181 -0
  156. ccgram-2.0.0/tests/conftest.py +14 -0
  157. ccgram-2.0.0/tests/e2e/__init__.py +0 -0
  158. ccgram-2.0.0/tests/e2e/_helpers.py +295 -0
  159. ccgram-2.0.0/tests/e2e/conftest.py +279 -0
  160. ccgram-2.0.0/tests/e2e/test_claude_lifecycle.py +326 -0
  161. ccgram-2.0.0/tests/e2e/test_codex_lifecycle.py +95 -0
  162. ccgram-2.0.0/tests/e2e/test_gemini_lifecycle.py +97 -0
  163. ccgram-2.0.0/tests/integration/conftest.py +86 -0
  164. ccgram-2.0.0/tests/integration/test_config_integration.py +39 -0
  165. ccgram-2.0.0/tests/integration/test_hook_pipeline.py +74 -0
  166. ccgram-2.0.0/tests/integration/test_message_dispatch.py +190 -0
  167. ccgram-2.0.0/tests/integration/test_monitor_flow.py +166 -0
  168. ccgram-2.0.0/tests/integration/test_monitor_state_integration.py +67 -0
  169. ccgram-2.0.0/tests/integration/test_state_roundtrip.py +146 -0
  170. ccgram-2.0.0/tests/integration/test_tmux_manager.py +222 -0
  171. ccgram-2.0.0/uv.lock +558 -0
@@ -0,0 +1,142 @@
1
+ # System Architecture
2
+
3
+ ```mermaid
4
+ graph TB
5
+ subgraph bot["Telegram Bot — bot.py"]
6
+ direction TB
7
+ BotCore["Topic routing · /history · /sessions\nStatus messages · Interactive UI\nMessage queue + worker · MarkdownV2"]
8
+ BotSub1["markdown_v2.py\nMD → MarkdownV2 + expandable quotes"]
9
+ BotSub2["telegram_sender.py\nsplit_message — 4096 limit"]
10
+ Terminal["terminal_parser.py + screen_buffer.py\npyte VT100 · interactive UI detection\nspinner parsing · separator detection"]
11
+ end
12
+
13
+ subgraph monitor["SessionMonitor — session_monitor.py"]
14
+ Mon["Poll JSONL every 2s · mtime cache\nParse new lines · track pending tools\nRead events.jsonl incrementally"]
15
+ end
16
+
17
+ subgraph tmux["TmuxManager — tmux_manager.py"]
18
+ Tmux["list/find/create/kill windows\nsend_keys · capture_pane\nlist_panes · send_keys_to_pane"]
19
+ end
20
+
21
+ subgraph parsing["TranscriptParser — transcript_parser.py"]
22
+ TP["Parse JSONL · pair tool_use ↔ tool_result\nExpandable quotes for thinking · history"]
23
+ end
24
+
25
+ subgraph windows["Tmux Windows"]
26
+ Win["One window per topic/session\nClaude Code · Codex · Gemini"]
27
+ end
28
+
29
+ subgraph hook["Hook — hook.py"]
30
+ Hook["Receive hook stdin\nWrite session_map.json\nWrite events.jsonl"]
31
+ end
32
+
33
+ subgraph session["SessionManager — session.py"]
34
+ SM["Window ↔ Session resolution\nThread bindings · message history"]
35
+ end
36
+
37
+ subgraph state["State Files — ~/.ccgram/"]
38
+ MonState["MonitorState\nbyte offsets per session"]
39
+ Sessions["Claude Sessions\n~/.claude/projects/\nsessions-index + *.jsonl"]
40
+ end
41
+
42
+ bot -- "Notify\n(NewMessage callback)" --> monitor
43
+ bot -- "Send\n(tmux keys)" --> tmux
44
+ monitor --> parsing
45
+ tmux --> windows
46
+ windows -- "Claude Code hooks\n(7 event types)" --> hook
47
+ hook -- "session_map.json\n+ events.jsonl" --> session
48
+ session -- "reads JSONL" --> Sessions
49
+ monitor -- "reads" --> MonState
50
+
51
+ style bot fill:#e8f4fd,stroke:#0088cc,stroke-width:2px,color:#333
52
+ style monitor fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#333
53
+ style tmux fill:#f0faf0,stroke:#2ea44f,stroke-width:2px,color:#333
54
+ style parsing fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#333
55
+ style windows fill:#f0faf0,stroke:#2ea44f,stroke-width:2px,color:#333
56
+ style hook fill:#fce4ec,stroke:#c62828,stroke-width:2px,color:#333
57
+ style session fill:#e8eaf6,stroke:#283593,stroke-width:2px,color:#333
58
+ style state fill:#f5f5f5,stroke:#616161,stroke-width:2px,color:#333
59
+ ```
60
+
61
+ ## Module Inventory
62
+
63
+ ### Provider modules (`providers/`)
64
+
65
+ | Module | Description |
66
+ | ------------- | ---------------------------------------------------------------------------------------- |
67
+ | `base.py` | AgentProvider protocol, ProviderCapabilities, event types |
68
+ | `registry.py` | ProviderRegistry (name→factory map, singleton cache) |
69
+ | `_jsonl.py` | Shared JSONL parsing base class for Codex + Gemini |
70
+ | `claude.py` | ClaudeProvider (hook, resume, continue, JSONL transcripts) |
71
+ | `codex.py` | CodexProvider (resume, continue, JSONL transcripts, no hook) |
72
+ | `gemini.py` | GeminiProvider (resume, continue, whole-file JSON transcripts, no hook) |
73
+ | `__init__.py` | `get_provider_for_window()`, `detect_provider_from_command()`, `get_provider()` fallback |
74
+
75
+ ### Core modules (`src/ccgram/`)
76
+
77
+ | Module | Description |
78
+ | ------------------ | -------------------------------------------------------------------- |
79
+ | `cli.py` | Click-based CLI entry point (run subcommand + all bot-config flags) |
80
+ | `config.py` | Application configuration singleton (env vars, .env files, defaults) |
81
+ | `doctor_cmd.py` | `ccgram doctor [--fix]` — validate setup without bot token |
82
+ | `status_cmd.py` | `ccgram status` — show running state without bot token |
83
+ | `screen_buffer.py` | pyte VT100 screen buffer (ANSI→clean lines, separator detection) |
84
+ | `cc_commands.py` | CC command discovery (skills, custom commands) + menu registration |
85
+ | `screenshot.py` | Terminal text → PNG rendering (ANSI color, font fallback) |
86
+ | `main.py` | Application entry point (Click dispatcher, run_bot bootstrap) |
87
+ | `utils.py` | Shared utilities (ccgram_dir, tmux_session_name, atomic_write_json) |
88
+
89
+ ### Handler modules (`handlers/`)
90
+
91
+ | Module | Description |
92
+ | -------------------------- | ------------------------------------------------------------------ |
93
+ | `text_handler.py` | Text message routing (UI guards → unbound → dead → forward) |
94
+ | `message_sender.py` | safe_reply/safe_edit/safe_send + rate_limit_send |
95
+ | `message_queue.py` | Per-user queue + worker (merge, status dedup) |
96
+ | `status_polling.py` | Background status polling (1s), auto-close, multi-pane scanning |
97
+ | `response_builder.py` | Response pagination and formatting |
98
+ | `interactive_ui.py` | AskUserQuestion / ExitPlanMode / Permission UI rendering |
99
+ | `interactive_callbacks.py` | Callbacks for interactive UI (arrow keys, enter, esc) |
100
+ | `directory_browser.py` | Directory selection UI for new topics |
101
+ | `directory_callbacks.py` | Callbacks for directory browser (navigate, confirm, provider pick) |
102
+ | `window_callbacks.py` | Window picker callbacks (bind, new, cancel) |
103
+ | `recovery_callbacks.py` | Dead window recovery callbacks (fresh, continue, resume) |
104
+ | `screenshot_callbacks.py` | Screenshot refresh, Esc, quick-key, pane screenshot callbacks |
105
+ | `history.py` | Message history display with pagination |
106
+ | `history_callbacks.py` | History pagination callbacks (prev/next) |
107
+ | `sessions_dashboard.py` | /sessions command: active session overview + kill |
108
+ | `restore_command.py` | /restore command: recover dead topics via recovery keyboard |
109
+ | `resume_command.py` | /resume command: scan past sessions, paginated picker |
110
+ | `upgrade.py` | /upgrade command: uv tool upgrade + process restart |
111
+ | `file_handler.py` | Photo/document handler (save to .ccgram-uploads/, notify agent) |
112
+ | `command_history.py` | Per-user/per-topic in-memory command recall (max 20) |
113
+ | `topic_emoji.py` | Topic name emoji updates (active/idle/done/dead), debounced |
114
+ | `hook_events.py` | Hook event dispatcher (Notification, Stop, Subagent*, Team*) |
115
+ | `cleanup.py` | Centralized topic state cleanup on close/delete |
116
+ | `callback_data.py` | CB\_\* callback data constants for inline keyboard routing |
117
+ | `callback_helpers.py` | Shared helpers (user_owns_window, get_thread_id) |
118
+ | `user_state.py` | context.user_data string key constants |
119
+
120
+ ### State files (`~/.ccgram/` or `$CCBOT_DIR/`)
121
+
122
+ | File | Description |
123
+ | -------------------- | -------------------------------------------------------------- |
124
+ | `state.json` | Thread bindings + window states + display names + read offsets |
125
+ | `session_map.json` | Hook-generated window_id→session mapping |
126
+ | `events.jsonl` | Append-only hook event log (all 7 event types) |
127
+ | `monitor_state.json` | Poll progress (byte offset) per JSONL file |
128
+
129
+ ## Key Design Decisions
130
+
131
+ - **Topic-centric** — Each Telegram topic binds to one tmux window. No centralized session list; topics _are_ the session list.
132
+ - **Window ID-centric** — All internal state keyed by tmux window ID (e.g. `@0`, `@12`), not window names. Window IDs are guaranteed unique within a tmux server session. Window names are kept as display names via `window_display_names` map. Same directory can have multiple windows.
133
+ - **Hook-based event system** — Claude Code hooks (SessionStart, Notification, Stop, SubagentStart, SubagentStop, TeammateIdle, TaskCompleted) write to `session_map.json` and `events.jsonl`. SessionMonitor reads both: session_map for session tracking, events.jsonl for instant event dispatch (interactive UI, done detection, subagent status, team notifications). Terminal scraping remains as fallback. Missing hooks are detected at startup with an actionable warning.
134
+ - **Multi-pane awareness** — Windows with multiple panes (e.g. Claude Code agent teams) are scanned for interactive prompts in non-active panes. Blocked panes are auto-surfaced as inline keyboard alerts. `/panes` command lists all panes with status and per-pane screenshot buttons. Callback data format extended to include pane_id: `"aq:enter:@12:%5"`.
135
+ - **Tool use ↔ tool result pairing** — `tool_use_id` tracked across poll cycles; tool result edits the original tool_use Telegram message in-place.
136
+ - **MarkdownV2 with fallback** — All messages go through `safe_reply`/`safe_edit`/`safe_send` which convert via `telegramify-markdown` and fall back to plain text on parse failure.
137
+ - **No truncation at parse layer** — Full content preserved; splitting at send layer respects Telegram's 4096 char limit with expandable quote atomicity.
138
+ - Only sessions registered in `session_map.json` (via hook) are monitored.
139
+ - Notifications delivered to users via thread bindings (topic → window_id → session).
140
+ - **Startup re-resolution** — Window IDs reset on tmux server restart. On startup, `resolve_stale_ids()` matches persisted display names against live windows to re-map IDs. Old state.json files keyed by window name are auto-migrated.
141
+ - **Per-window provider** — All CLI-specific behavior (launch args, transcript parsing, terminal status, command discovery) is delegated to an `AgentProvider` protocol. Providers declare capabilities (`ProviderCapabilities`) that gate UX features per-window: hook checks, resume/continue buttons, and command registration. Each window stores its `provider_name` in `WindowState`; `get_provider_for_window(window_id)` resolves the correct provider instance, falling back to the config default. Externally created windows are auto-detected via `detect_provider_from_command(pane_current_command)`. The global `get_provider()` singleton remains for CLI commands (`doctor`, `status`) that lack window context.
142
+ - **Foreign window support (emdash)** — Windows owned by external tools (emdash) use qualified IDs like `emdash-claude-main-abc123:@0` which are valid tmux `-t` targets. Foreign windows are marked `WindowState.external=True` and are never killed by ccgram. Discovery via `tmux list-sessions` filtered by `emdash-` prefix. The `window_resolver` preserves foreign entries during startup re-resolution. All tmux operations (send_keys, capture_pane) route foreign IDs through subprocess instead of libtmux.
@@ -0,0 +1,40 @@
1
+ # Message Handling
2
+
3
+ ## Message Queue Architecture
4
+
5
+ Per-user message queues + worker pattern for all send tasks:
6
+ - Messages are sent in receive order (FIFO)
7
+ - Status messages always follow content messages
8
+ - Multi-user concurrent processing without interference
9
+
10
+ **Message merging**: The worker automatically merges consecutive mergeable content messages on dequeue:
11
+ - Content messages for the same window can be merged (including text, thinking)
12
+ - tool_use breaks the merge chain and is sent separately (message ID recorded for later editing)
13
+ - tool_result breaks the merge chain and is edited into the tool_use message (preventing order confusion)
14
+ - Merging stops when combined length exceeds 3800 characters (to avoid pagination)
15
+
16
+ ## Status Message Handling
17
+
18
+ **Conversion**: The status message is edited into the first content message, reducing message count:
19
+ - When a status message exists, the first content message updates it via edit
20
+ - Subsequent content messages are sent as new messages
21
+
22
+ **Polling**: Background task polls terminal status for all active windows at 1-second intervals. Send-layer rate limiting ensures flood control is not triggered.
23
+
24
+ **Deduplication**: The worker compares `last_text` when processing status updates; identical content skips the edit, reducing API calls.
25
+
26
+ ## Rate Limiting
27
+
28
+ - Minimum 1.1-second interval between messages per user
29
+ - Status polling interval: 1 second (send layer has rate limiting protection)
30
+ - Automated outbound messages (queue worker, status updates) go through `rate_limit_send()`
31
+
32
+ ## Performance Optimizations
33
+
34
+ **mtime cache**: The monitoring loop maintains an in-memory file mtime cache, skipping reads for unchanged files.
35
+
36
+ **Byte offset incremental reads**: Each tracked session records `last_byte_offset`, reading only new content. File truncation (offset > file_size) is detected and offset is auto-reset.
37
+
38
+ ## No Message Truncation
39
+
40
+ Historical messages (tool_use summaries, tool_result text, user/assistant messages) are always kept in full — no character-level truncation at the parsing layer. Long text is handled exclusively at the send layer: `split_message` splits by Telegram's 4096-character limit; real-time messages get `[1/N]` text suffixes, history pages get inline keyboard navigation.
@@ -0,0 +1,79 @@
1
+ # Topic-Only Architecture
2
+
3
+ The bot operates exclusively in Telegram Forum (topics) mode. There is **no** `active_sessions` mapping, **no** `/list` command, **no** General topic routing, and **no** backward-compatibility logic for older non-topic modes. Every code path assumes named topics.
4
+
5
+ ## 1 Topic = 1 Window = 1 Session
6
+
7
+ ```mermaid
8
+ graph LR
9
+ Topic["Topic ID\n(Telegram)"]
10
+ Window["Window ID\n(tmux @id)"]
11
+ Session["Session ID\n(Claude)"]
12
+
13
+ Topic -- "thread_bindings\n(state.json)" --> Window
14
+ Window -- "session_map.json\n(written by hook)" --> Session
15
+
16
+ style Topic fill:#e8f4fd,stroke:#0088cc,stroke-width:2px,color:#333
17
+ style Window fill:#f0faf0,stroke:#2ea44f,stroke-width:2px,color:#333
18
+ style Session fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px,color:#333
19
+ ```
20
+
21
+ Window IDs (e.g. `@0`, `@12`) are guaranteed unique within a tmux server session. Window names are stored separately as display names (`window_display_names` map).
22
+
23
+ ## Mapping 1: Topic → Window ID (thread_bindings)
24
+
25
+ ```python
26
+ # session.py: SessionManager
27
+ thread_bindings: dict[int, dict[int, str]] # user_id → {thread_id → window_id}
28
+ window_display_names: dict[str, str] # window_id → window_name (for display)
29
+ ```
30
+
31
+ - Storage: memory + `state.json`
32
+ - Written when: user creates a new session via the directory browser in a topic
33
+ - Purpose: route user messages to the correct tmux window
34
+
35
+ ## Mapping 2: Window ID → Session (session_map.json)
36
+
37
+ ```python
38
+ # session_map.json (key format: "tmux_session:window_id")
39
+ {
40
+ "ccgram:@0": {"session_id": "uuid-xxx", "cwd": "/path/to/project", "window_name": "project", "provider_name": "claude", "transcript_path": "..."},
41
+ "ccgram:@5": {"session_id": "uuid-yyy", "cwd": "/path/to/project", "window_name": "project-2", "provider_name": "codex", "transcript_path": "..."}
42
+ }
43
+ ```
44
+
45
+ - Storage: `session_map.json`
46
+ - Written when: Claude Code's `SessionStart` hook fires (always sets `provider_name: "claude"`; other providers have no hook). All 7 hook events also append to `events.jsonl` for instant dispatch.
47
+ - Property: one window maps to one session; session_id changes after `/clear`
48
+ - Purpose: SessionMonitor reads session_map to decide which sessions to watch, and reads events.jsonl for instant event notifications (interactive UI, done detection, subagent status)
49
+
50
+ ## Message Flows
51
+
52
+ **Outbound** (user → Claude):
53
+
54
+ ```
55
+ User sends "hello" in topic (thread_id=42)
56
+ → thread_bindings[user_id][42] → "@0"
57
+ → send_to_window("@0", "hello") # resolves via find_window_by_id
58
+ ```
59
+
60
+ **Inbound** (Claude → user):
61
+
62
+ ```
63
+ SessionMonitor reads new message (session_id = "uuid-xxx")
64
+ → Iterate thread_bindings, find (user, thread) whose window_id maps to this session
65
+ → Deliver message to user in the correct topic (thread_id)
66
+ ```
67
+
68
+ **New topic flow**: First message in an unbound topic → directory browser → select directory → select provider → create window with chosen provider → bind topic → forward pending message.
69
+
70
+ **Topic lifecycle**: Closing/deleting a topic auto-kills the associated tmux window and unbinds the thread. Stale bindings (window deleted externally) are cleaned up by the status polling loop.
71
+
72
+ ## Session Lifecycle
73
+
74
+ **Startup cleanup**: On bot startup, all tracked sessions not present in session_map are cleaned up, preventing monitoring of closed sessions.
75
+
76
+ **Runtime change detection**: Each polling cycle checks for session_map changes:
77
+
78
+ - Window's session_id changed (e.g., after `/clear`) → clean up old session
79
+ - Window deleted → clean up corresponding session
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: releasing
3
+ description: Tag version and trigger PyPI + Homebrew release. Use when user says "release", "tag and release", "publish version".
4
+ disable-model-invocation: true
5
+ user-invocable: true
6
+ allowed-tools: Bash(git *), Bash(gh *), Bash(make check), Read
7
+ argument-hint: <version> (e.g., 0.3.0)
8
+ model: haiku
9
+ ---
10
+
11
+ # Release v$ARGUMENTS
12
+
13
+ Tag and publish a new version to PyPI and Homebrew.
14
+
15
+ ## Pre-flight
16
+
17
+ 1. Verify on `main` branch: `git branch --show-current`
18
+ 2. Verify working tree is clean: `git status`
19
+ 3. Run `make check` — all must pass
20
+ 4. Confirm no unpushed commits: `git log origin/main..HEAD --oneline`
21
+
22
+ ## Tag and Push
23
+
24
+ 5. Tag: `git tag v$ARGUMENTS`
25
+ 6. Push tag: `git push origin v$ARGUMENTS`
26
+
27
+ ## Monitor
28
+
29
+ 7. Wait 30s, then check: `gh api repos/alexei-led/ccbot/actions/runs --jq '.workflow_runs[0] | "\(.name): \(.status) \(.conclusion // "running")"'`
30
+ 8. If publish job failed, show logs and stop
31
+ 9. If update-homebrew failed, show logs and stop
32
+ 10. Report final status
33
+
34
+ ## Notes
35
+
36
+ - hatch-vcs generates version from tag: `v0.3.0` → PyPI `0.3.0`
37
+ - Release workflow: `.github/workflows/release.yml`
38
+ - Re-tag if needed: `git tag -d v$ARGUMENTS && git push origin :refs/tags/v$ARGUMENTS && git tag v$ARGUMENTS && git push origin v$ARGUMENTS`
@@ -0,0 +1,32 @@
1
+ # Telegram Bot Token (required)
2
+ TELEGRAM_BOT_TOKEN=your_bot_token_here
3
+
4
+ # Allowed user IDs (comma-separated, required)
5
+ ALLOWED_USERS=123456789,987654321
6
+
7
+ # Tmux session name (optional, defaults to "ccgram")
8
+ TMUX_SESSION_NAME=ccgram
9
+
10
+ # Monitor polling interval in seconds (optional, defaults to 2.0)
11
+ MONITOR_POLL_INTERVAL=2.0
12
+
13
+ # Multi-instance: Telegram group chat ID this instance owns (optional)
14
+ # When set, this instance only processes updates from this group.
15
+ # Get the ID via @userinfobot or @RawDataBot in your group.
16
+ # CCGRAM_GROUP_ID=-1001234567890
17
+
18
+ # Multi-instance: display name for this instance (optional, defaults to hostname)
19
+ # CCGRAM_INSTANCE_NAME=bot-1
20
+
21
+ # Per-provider launch command overrides (optional)
22
+ # CCGRAM_CLAUDE_COMMAND=my-claude-wrapper (defaults to "claude")
23
+ # CCGRAM_CODEX_COMMAND=my-codex-wrapper (defaults to "codex")
24
+ # CCGRAM_GEMINI_COMMAND=my-gemini-wrapper (defaults to "gemini")
25
+ #
26
+ # Claude config directory (optional, defaults to ~/.claude)
27
+ # For Claude wrappers (ce, cc-mirror, zai) that use a different config location.
28
+ # Affects hook install, command discovery, and session monitoring.
29
+ # CLAUDE_CONFIG_DIR=~/.claude-custom
30
+
31
+ # Show hidden (dot) directories in the directory browser (optional, defaults to false)
32
+ # CCGRAM_SHOW_HIDDEN_DIRS=false
@@ -0,0 +1,33 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ pull_request:
7
+ branches:
8
+ - main
9
+ jobs:
10
+ check:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version:
15
+ - "3.14"
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+ with:
19
+ fetch-depth: 0
20
+ - uses: astral-sh/setup-uv@v7
21
+ with:
22
+ python-version: ${{ matrix.python-version }}
23
+ - run: uv sync --all-extras
24
+ - name: Format check
25
+ run: uv run ruff format --check src/ tests/
26
+ - name: Lint
27
+ run: uv run ruff check src/ tests/
28
+ - name: Type check
29
+ run: uv run pyright src/ccgram/
30
+ - name: Dependency check
31
+ run: uv run deptry src
32
+ - name: Test
33
+ run: uv run pytest --tb=short -q
@@ -0,0 +1,63 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ tags:
5
+ - v*
6
+ jobs:
7
+ publish:
8
+ runs-on: ubuntu-latest
9
+ environment: pypi
10
+ permissions:
11
+ id-token: write
12
+ steps:
13
+ - uses: actions/checkout@v6
14
+ with:
15
+ fetch-depth: 0
16
+ - uses: astral-sh/setup-uv@v7
17
+ with:
18
+ python-version: "3.14"
19
+ - run: uv build
20
+ - name: Publish to PyPI
21
+ uses: pypa/gh-action-pypi-publish@release/v1
22
+ update-homebrew:
23
+ needs: publish
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v6
27
+ - uses: astral-sh/setup-uv@v7
28
+ with:
29
+ python-version: "3.14"
30
+ - name: Extract version
31
+ id: version
32
+ run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
33
+ - name: Generate formula
34
+ run: uv run scripts/generate_homebrew_formula.py "${{ steps.version.outputs.VERSION }}" > ccgram.rb
35
+ - name: Push to homebrew-tap
36
+ env:
37
+ GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
38
+ run: |
39
+ git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/alexei-led/homebrew-tap.git" tap
40
+ cp ccgram.rb tap/Formula/ccgram.rb
41
+ cd tap
42
+ git config user.name "github-actions[bot]"
43
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
44
+ git add Formula/ccgram.rb
45
+ git commit -m "ccgram ${{ steps.version.outputs.VERSION }}"
46
+ git push
47
+ github-release:
48
+ needs:
49
+ - publish
50
+ - update-homebrew
51
+ runs-on: ubuntu-latest
52
+ permissions:
53
+ contents: write
54
+ steps:
55
+ - name: Publish GitHub Release
56
+ uses: softprops/action-gh-release@v2
57
+ with:
58
+ tag_name: ${{ github.ref_name }}
59
+ name: ${{ github.ref_name }}
60
+ generate_release_notes: true
61
+ body: |
62
+ Automated release for `${{ github.ref_name }}`.
63
+ Includes published PyPI package and Homebrew formula updates.
@@ -0,0 +1,55 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+ *.egg-info/
6
+ *.egg
7
+
8
+ # Build & Distribution
9
+ build/
10
+ dist/
11
+ sdist/
12
+ wheels/
13
+ src/ccgram/_version.py
14
+
15
+ # Virtual Environments
16
+ .venv/
17
+ .env
18
+
19
+ # Testing & Coverage
20
+ .hypothesis/
21
+ .pytest_cache/
22
+ htmlcov/
23
+ .coverage
24
+ .coverage.*
25
+ coverage.xml
26
+ coverage.json
27
+
28
+ # Type Checking & Linting
29
+ .mypy_cache/
30
+ .ruff_cache/
31
+
32
+ # Go module cache (external)
33
+ pkg/
34
+
35
+ # IDE
36
+ .idea/
37
+ .vscode/
38
+ *.swp
39
+ *.swo
40
+ *~
41
+
42
+ # Claude Code (keep .claude/ tracked)
43
+ .claude/settings.local.json
44
+
45
+ # Spec (keep .spec/ tracked)
46
+
47
+ # ai artificats to skip
48
+ .brainstorming/
49
+
50
+
51
+ # ralphex progress logs
52
+ progress*.txt
53
+
54
+ # Dev run lock
55
+ .ccgram-dev-run.lock.d/
@@ -0,0 +1,121 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [2.0.0] - 2026-03-16
11
+
12
+ ### Changed
13
+
14
+ - **BREAKING**: Renamed project from `ccbot` to `ccgram` (CCGram)
15
+ - **BREAKING**: CLI command renamed from `ccbot` to `ccgram`
16
+ - **BREAKING**: Config directory changed from `~/.ccbot/` to `~/.ccgram/`
17
+ - **BREAKING**: Environment variables renamed from `CCBOT_*` to `CCGRAM_*`
18
+ - Hook command changed from `ccbot hook` to `ccgram hook`
19
+ - PyPI package name changed from `ccbot` to `ccgram`
20
+ - Default tmux session name changed from `ccbot` to `ccgram`
21
+
22
+ ### Migration
23
+
24
+ - Old `CCBOT_*` environment variables still work as fallback with deprecation warnings
25
+ - `ccgram hook --install` detects and replaces legacy `ccbot hook` entries
26
+ - `ccgram hook --uninstall` removes both old and new hook entries
27
+ - Session map keys with `ccbot:` prefix are auto-migrated on load
28
+ - If `~/.ccgram/` doesn't exist but `~/.ccbot/` does, a migration hint is logged
29
+
30
+ ## [1.0.0] - 2026-02-22
31
+
32
+ ### Added
33
+
34
+ - Multi-provider support: Claude Code, OpenAI Codex CLI, and Google Gemini CLI as agent backends
35
+ - Per-topic provider selection via directory browser (Claude default, Codex, Gemini)
36
+ - Auto-detection of provider from externally created tmux windows
37
+ - Provider-aware recovery UI (Fresh/Continue/Resume adapt to each provider's capabilities)
38
+ - Gemini CLI terminal status detection via pane title and interactive UI patterns
39
+ - Codex and Gemini transcript parsing with provider-specific formats
40
+ - Provider capability matrix gating UX features per-window
41
+
42
+ ### Fixed
43
+
44
+ - Codex resume syntax corrected to `resume <id>` subcommand (was `exec resume`)
45
+ - Gemini resume accepts index numbers and "latest" (not just UUIDs)
46
+ - Both Codex and Gemini now correctly support Continue (resume last session)
47
+
48
+ ## [0.2.11] - 2026-02-17
49
+
50
+ ### Fixed
51
+
52
+ - Preserved window display names when the SessionStart hook map is stale, preventing topic/session labels from regressing during state resolution
53
+
54
+ ## [0.2.10] - 2026-02-17
55
+
56
+ ### Added
57
+
58
+ - Directory favorites sidebar plus starred MRU controls for faster session bootstrapping
59
+ - File handler uploads that forward captions to Claude Code alongside the document payload
60
+ - Notification toggle to pause/resume Telegram alerts per topic
61
+
62
+ ### Changed
63
+
64
+ - Directory browser now shows status keyboard tweaks for clarity when picking working directories
65
+ - Status keyboard refreshed to better expose screenshot shortcuts and live indicators
66
+
67
+ ### Fixed
68
+
69
+ - Session polling stability improvements that cover status, screenshot, and message filtering edge cases
70
+
71
+ ## [0.2.0] - 2026-02-12
72
+
73
+ Major rewrite as an independent fork of [six-ddc/ccbot](https://github.com/six-ddc/ccbot).
74
+
75
+ ### Added
76
+
77
+ - Topic-based sessions: 1 topic = 1 tmux window = 1 Claude session
78
+ - Interactive UI for AskUserQuestion, ExitPlanMode, and Permission prompts
79
+ - Sessions dashboard with per-session status and kill buttons
80
+ - Message history with paginated browsing (newest first)
81
+ - Auto-discovery of Claude Code skills and custom commands in Telegram menu
82
+ - Hook-based session tracking (SessionStart hook writes session_map.json)
83
+ - Per-user message queue with FIFO ordering and message merging
84
+ - Rate limiting (1.1s minimum interval per user)
85
+ - Multi-instance support via CCBOT_GROUP_ID and CCBOT_INSTANCE_NAME
86
+ - Auto-topic creation for manually created tmux windows (including cold-start)
87
+ - Fresh/Continue/Resume recovery flows for dead sessions
88
+ - /resume command to browse and resume past sessions
89
+ - Directory browser for new topic session creation
90
+ - MarkdownV2 output with automatic plain text fallback
91
+ - Terminal screenshot rendering (ANSI color support)
92
+ - Status line polling with spinner and working text
93
+ - Expandable quote formatting for thinking content
94
+ - Persistent state (thread bindings, read offsets survive restarts)
95
+ - Topic emoji status updates reflecting session state
96
+ - Configurable config directory via CCBOT_DIR env var
97
+
98
+ ### Changed
99
+
100
+ - Internal routing keyed by tmux window ID instead of window name
101
+ - Python 3.14 required (up from 3.12)
102
+ - Replaced broad exception handlers with specific types
103
+ - Normalized variable naming (full names instead of short aliases)
104
+ - Enabled C901, PLR, N ruff quality gate rules
105
+
106
+ ### Removed
107
+
108
+ - Non-topic mode (active_sessions, /list, General topic routing)
109
+ - Message truncation at parse layer (splitting only at send layer)
110
+
111
+ ## [0.1.0] - 2026-02-07
112
+
113
+ Initial release by [six-ddc](https://github.com/six-ddc).
114
+
115
+ [Unreleased]: https://github.com/alexei-led/ccgram/compare/v2.0.0...HEAD
116
+ [2.0.0]: https://github.com/alexei-led/ccgram/compare/v1.0.0...v2.0.0
117
+ [1.0.0]: https://github.com/alexei-led/ccbot/compare/v0.2.11...v1.0.0
118
+ [0.2.11]: https://github.com/alexei-led/ccbot/compare/v0.2.10...v0.2.11
119
+ [0.2.10]: https://github.com/alexei-led/ccbot/compare/v0.2.0...v0.2.10
120
+ [0.2.0]: https://github.com/alexei-led/ccbot/compare/v0.1.0...v0.2.0
121
+ [0.1.0]: https://github.com/alexei-led/ccbot/releases/tag/v0.1.0