@jsayubi/ccgram 1.1.0 → 1.2.0

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 (55) hide show
  1. package/.env.example +3 -0
  2. package/README.md +45 -13
  3. package/dist/elicitation-notify.d.ts +20 -0
  4. package/dist/elicitation-notify.d.ts.map +1 -0
  5. package/dist/elicitation-notify.js +241 -0
  6. package/dist/elicitation-notify.js.map +1 -0
  7. package/dist/enhanced-hook-notify.d.ts +8 -1
  8. package/dist/enhanced-hook-notify.d.ts.map +1 -1
  9. package/dist/enhanced-hook-notify.js +119 -5
  10. package/dist/enhanced-hook-notify.js.map +1 -1
  11. package/dist/permission-denied-notify.d.ts +11 -0
  12. package/dist/permission-denied-notify.d.ts.map +1 -0
  13. package/dist/permission-denied-notify.js +193 -0
  14. package/dist/permission-denied-notify.js.map +1 -0
  15. package/dist/permission-hook.js +43 -18
  16. package/dist/permission-hook.js.map +1 -1
  17. package/dist/pre-compact-notify.d.ts +13 -0
  18. package/dist/pre-compact-notify.d.ts.map +1 -0
  19. package/dist/pre-compact-notify.js +197 -0
  20. package/dist/pre-compact-notify.js.map +1 -0
  21. package/dist/question-notify.d.ts +6 -5
  22. package/dist/question-notify.d.ts.map +1 -1
  23. package/dist/question-notify.js +107 -23
  24. package/dist/question-notify.js.map +1 -1
  25. package/dist/setup.js +26 -10
  26. package/dist/setup.js.map +1 -1
  27. package/dist/src/types/callbacks.d.ts +11 -1
  28. package/dist/src/types/callbacks.d.ts.map +1 -1
  29. package/dist/src/types/session.d.ts +13 -1
  30. package/dist/src/types/session.d.ts.map +1 -1
  31. package/dist/src/utils/callback-parser.d.ts +7 -5
  32. package/dist/src/utils/callback-parser.d.ts.map +1 -1
  33. package/dist/src/utils/callback-parser.js +11 -5
  34. package/dist/src/utils/callback-parser.js.map +1 -1
  35. package/dist/src/utils/deep-link.d.ts +22 -0
  36. package/dist/src/utils/deep-link.d.ts.map +1 -0
  37. package/dist/src/utils/deep-link.js +43 -0
  38. package/dist/src/utils/deep-link.js.map +1 -0
  39. package/dist/src/utils/ghostty-session-manager.d.ts +81 -0
  40. package/dist/src/utils/ghostty-session-manager.d.ts.map +1 -0
  41. package/dist/src/utils/ghostty-session-manager.js +370 -0
  42. package/dist/src/utils/ghostty-session-manager.js.map +1 -0
  43. package/dist/src/utils/transcript-reader.d.ts +57 -0
  44. package/dist/src/utils/transcript-reader.d.ts.map +1 -0
  45. package/dist/src/utils/transcript-reader.js +229 -0
  46. package/dist/src/utils/transcript-reader.js.map +1 -0
  47. package/dist/workspace-router.d.ts +19 -4
  48. package/dist/workspace-router.d.ts.map +1 -1
  49. package/dist/workspace-router.js +57 -1
  50. package/dist/workspace-router.js.map +1 -1
  51. package/dist/workspace-telegram-bot.js +515 -114
  52. package/dist/workspace-telegram-bot.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/types/callbacks.ts +15 -1
  55. package/src/types/session.ts +14 -1
package/.env.example CHANGED
@@ -16,4 +16,7 @@ TELEGRAM_CHAT_ID=
16
16
  # Suppress Telegram notifications when actively at terminal (seconds, default: 300)
17
17
  # ACTIVE_THRESHOLD_SECONDS=300
18
18
 
19
+ # Injection backend: tmux (default), ghostty, or pty
20
+ # INJECTION_MODE=ghostty
21
+
19
22
  # For advanced options (email relay, LINE, webhooks), see .env.full-example
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # CCGram
4
4
 
5
- **Control Claude Code from Telegram — approve permissions, answer questions, and manage AI sessions from your phone.**
5
+ **Control Claude Code from Telegram — approve permissions, answer questions, resume sessions, and manage AI coding agents from your phone.**
6
6
 
7
7
  [![CI](https://github.com/jsayubi/ccgram/actions/workflows/ci.yml/badge.svg)](https://github.com/jsayubi/ccgram/actions/workflows/ci.yml)
8
8
  [![npm version](https://img.shields.io/npm/v/@jsayubi/ccgram)](https://www.npmjs.com/package/@jsayubi/ccgram)
@@ -13,7 +13,7 @@
13
13
 
14
14
  ---
15
15
 
16
- CCGram is a self-hosted Telegram bot that bridges Claude Code to your phone. When Claude needs a permission, has a question, or finishes a task — you get a Telegram message with inline buttons to respond. You never need to be at your keyboard.
16
+ CCGram is a self-hosted Telegram bot that bridges Claude Code to your phone. When Claude needs a permission, has a question, or finishes a task — you get a Telegram message with inline buttons to respond. Resume past conversations, start new sessions, and manage multiple AI coding agents — all without being at your keyboard.
17
17
 
18
18
  ```
19
19
  Claude Code → ccgram hooks → Telegram bot → 📱 your phone
@@ -28,6 +28,7 @@ Claude Code → ccgram hooks → Telegram bot → 📱 your phone
28
28
  - **Smart notifications** — Task completions, session start/end, and subagent activity — silent when you're at your terminal, instant when you're away
29
29
  - **Remote command routing** — Send any command to any Claude session from Telegram
30
30
  - **Session management** — List, switch between, and interrupt active sessions
31
+ - **Resume conversations** — `/resume` reads your full Claude Code session history with conversation snippets — pick up any past conversation in one tap
31
32
  - **Project launcher** — Start Claude in any project directory with `/new myproject`
32
33
  - **Smart routing** — Prefix matching, default workspace, reply-to routing
33
34
  - **Typing indicator** — See when the bot is waiting for Claude to respond
@@ -63,14 +64,22 @@ CCGram integrates with [Claude Code hooks](https://docs.anthropic.com/en/docs/cl
63
64
 
64
65
  | Hook | Event | What it does |
65
66
  |------|-------|-------------|
66
- | `permission-hook.js` | `PermissionRequest` | Sends a permission dialog with Allow / Deny / Always buttons. Blocks Claude until you respond. |
67
- | `question-notify.js` | `PreToolUse` (AskUserQuestion) | Sends Claude's question with selectable options. Your tap injects the selection via tmux/PTY. |
67
+ | `permission-hook.js` | `PermissionRequest` | Sends a permission dialog with Allow / Deny / Always / Defer buttons. Blocks Claude until you respond. |
68
+ | `question-notify.js` | `PreToolUse` (AskUserQuestion) | Sends Claude's question with selectable options. Returns answer directly via `updatedInput` works with any terminal. |
68
69
  | `enhanced-hook-notify.js completed` | `Stop` | Notifies you when Claude finishes a task, including the last response text. |
69
70
  | `enhanced-hook-notify.js waiting` | `Notification` | Notifies you when Claude is waiting for input. |
70
71
  | `user-prompt-hook.js` | `UserPromptSubmit` | Tracks terminal activity so notifications are suppressed when you're actively working. |
71
72
  | `enhanced-hook-notify.js session-start` | `SessionStart` | Notifies you when a new Claude session starts. |
72
- | `enhanced-hook-notify.js session-end` | `SessionEnd` | Notifies you when a Claude session ends, with the final response. |
73
+ | `enhanced-hook-notify.js session-end` | `SessionEnd` | Notifies you when a Claude session ends. |
73
74
  | `enhanced-hook-notify.js subagent-done` | `SubagentStop` | Notifies you when a subagent task completes. |
75
+ | `permission-denied-notify.js` | `PermissionDenied` | Notifies when auto mode blocks a tool. Retry button lets you override. |
76
+ | `enhanced-hook-notify.js stop-failure` | `StopFailure` | Notifies you on API errors (rate limits, network issues). |
77
+ | `enhanced-hook-notify.js post-compact` | `PostCompact` | Notifies when context compaction completes, with token savings. |
78
+ | `pre-compact-notify.js` | `PreCompact` | Warns before compaction starts. Block button lets you preserve context. |
79
+ | `elicitation-notify.js` | `Elicitation` | Forwards MCP server input requests to Telegram. Reply with your answer. |
80
+ | `enhanced-hook-notify.js task-created` | `TaskCreated` | Notifies when Claude creates a new task. |
81
+ | `enhanced-hook-notify.js cwd-changed` | `CwdChanged` | Notifies when Claude changes working directory. |
82
+ | `enhanced-hook-notify.js instructions-loaded` | `InstructionsLoaded` | Notifies when CLAUDE.md or rules are loaded. |
74
83
 
75
84
  > **Smart suppression** — all notifications (including permissions) are automatically silenced when you've sent a message to Claude within the last 5 minutes. The moment you step away, Telegram takes over. Telegram-injected commands always get their response back to Telegram regardless.
76
85
 
@@ -90,11 +99,11 @@ Claude requests permission
90
99
 
91
100
  ```
92
101
  Claude asks a question (AskUserQuestion)
93
- → Claude shows question UI in terminal
94
102
  → question-notify sends options to Telegram
95
103
  → you tap an option
96
- → bot injects arrow keys + Enter via tmux or PTY
97
- Claude's question UI captures the keystrokes
104
+ → bot writes response file with your selection
105
+ hook returns answer via updatedInput to Claude
106
+ → Claude receives answer directly (works with any terminal!)
98
107
  ```
99
108
 
100
109
  ## Bot Commands
@@ -113,9 +122,12 @@ Claude asks a question (AskUserQuestion)
113
122
  | Command | Description |
114
123
  |---------|-------------|
115
124
  | `/<workspace> <command>` | Send a command to a specific Claude session |
116
- | `/status [workspace]` | Show the last 20 lines of tmux pane output |
125
+ | `/status [workspace]` | Show the last 20 lines of output (includes rate limit info if available) |
117
126
  | `/stop [workspace]` | Send Ctrl+C to interrupt the running prompt |
118
127
  | `/compact [workspace]` | Run `/compact` and wait for it to complete |
128
+ | `/effort [workspace] low\|medium\|high` | Set Claude's thinking effort level |
129
+ | `/model [workspace] <model>` | Switch Claude model (sonnet, opus, haiku) |
130
+ | `/link <prompt>` | Generate a deep link to open Claude Code with your prompt |
119
131
 
120
132
  ### Project launcher
121
133
 
@@ -126,6 +138,22 @@ Claude asks a question (AskUserQuestion)
126
138
 
127
139
  The `/new` command searches your configured `PROJECT_DIRS`, finds exact or prefix-matched directories, creates a tmux session (or PTY session if tmux is unavailable), starts Claude, and sets it as the default workspace.
128
140
 
141
+ ### Resume past conversations
142
+
143
+ | Command | Description |
144
+ |---------|-------------|
145
+ | `/resume` | Show projects with past Claude sessions |
146
+ | `/resume myproject` | Jump straight to session picker for that project |
147
+
148
+ The `/resume` command reads directly from Claude Code's session storage (`~/.claude/projects/`), giving you access to your full conversation history — not just sessions started through the bot.
149
+
150
+ Each session shows a snippet of the first message for easy identification. Sessions are sorted by last activity.
151
+
152
+ **Active session protection:**
153
+ - If a session appears to be running in a terminal (JSONL file modified within 5 min), you get a warning before resuming to prevent dual-instance conflicts
154
+ - If a PTY session is running, you're warned that it will be terminated (PTY sessions can't be reattached)
155
+ - tmux sessions switch seamlessly — the bot injects `/exit` + `claude --resume` inline, keeping your terminal connected
156
+
129
157
  ### Smart routing
130
158
 
131
159
  **Prefix matching** — workspace names can be abbreviated. `/ass hello` routes to `assistant` if it's unique. Ambiguous prefixes show a list to choose from.
@@ -235,7 +263,7 @@ node dist/workspace-telegram-bot.js
235
263
  ```bash
236
264
  npm run build # Compile TypeScript → dist/
237
265
  npm run build:watch # Watch mode
238
- npm test # Run 57 tests (vitest)
266
+ npm test # Run 84 tests (vitest)
239
267
  ```
240
268
 
241
269
  **Note:** Claude Code hooks run from `~/.ccgram/dist/`, not the repo's `dist/`. After changing hook scripts during development, sync them:
@@ -276,8 +304,9 @@ cli.ts # ccgram CLI entry point
276
304
  ```
277
305
  test/
278
306
  ├── prompt-bridge.test.js # 15 tests — IPC write/read/update/clean/expiry
279
- ├── workspace-router.test.js # 28 tests — session map, prefix matching, defaults, reply-to
280
- └── callback-parser.test.js # 14 tests — all callback_data formats
307
+ ├── workspace-router.test.js # 38 tests — session map, prefix matching, defaults, reply-to, session history
308
+ ├── callback-parser.test.js # 23 tests — all callback_data formats (perm, opt, new, rp, rs, rc)
309
+ └── active-check.test.js # 8 tests — terminal activity detection, thresholds
281
310
  ```
282
311
 
283
312
  Tests use isolated temp directories and run with `npm test` (vitest, no configuration needed).
@@ -303,6 +332,9 @@ All notifications — including permission requests — are suppressed automatic
303
332
  **Can I use it with multiple projects at once?**
304
333
  Yes. Each Claude session maps to a named tmux or PTY session. Use `/sessions` to see all active sessions, or `/use <workspace>` to set a default for plain text routing.
305
334
 
335
+ **Can I resume a conversation I started in the terminal?**
336
+ Yes. `/resume` reads from Claude Code's own session storage, so it sees every conversation — not just ones started through the bot. If the session is still running in your terminal, you'll get a warning before resuming to prevent conflicts.
337
+
306
338
  **Do I need tmux?**
307
339
  No. When tmux is not detected, CCGram automatically falls back to headless PTY sessions powered by [`node-pty`](https://github.com/microsoft/node-pty). No configuration required — it activates on its own.
308
340
 
@@ -331,7 +363,7 @@ MIT — see [LICENSE](LICENSE).
331
363
 
332
364
  <div align="center">
333
365
 
334
- Built for developers who let Claude Code run overnight and want to stay in control from anywhere.
366
+ Built for developers who run Claude Code unattended approve permissions, resume conversations, and manage AI coding agents from anywhere.
335
367
 
336
368
  [Report a bug](https://github.com/jsayubi/ccgram/issues) · [Request a feature](https://github.com/jsayubi/ccgram/issues)
337
369
 
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Elicitation Notify — Called by Claude Code's Elicitation hook.
4
+ *
5
+ * BLOCKING mode: Forwards MCP server input requests to Telegram, polls for
6
+ * user responses (one per form field), then outputs the answer via stdout.
7
+ *
8
+ * Stdin JSON: { mcp_server_name, requested_schema, cwd, session_id, ... }
9
+ * requested_schema: JSON Schema object with `properties` describing form fields
10
+ *
11
+ * Stdout: {
12
+ * hookSpecificOutput: {
13
+ * hookEventName: "Elicitation",
14
+ * action: "accept" | "decline" | "cancel",
15
+ * content: { <fieldName>: <value> } // required when action === "accept"
16
+ * }
17
+ * }
18
+ */
19
+ export {};
20
+ //# sourceMappingURL=elicitation-notify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elicitation-notify.d.ts","sourceRoot":"","sources":["../elicitation-notify.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;GAgBG"}
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Elicitation Notify — Called by Claude Code's Elicitation hook.
5
+ *
6
+ * BLOCKING mode: Forwards MCP server input requests to Telegram, polls for
7
+ * user responses (one per form field), then outputs the answer via stdout.
8
+ *
9
+ * Stdin JSON: { mcp_server_name, requested_schema, cwd, session_id, ... }
10
+ * requested_schema: JSON Schema object with `properties` describing form fields
11
+ *
12
+ * Stdout: {
13
+ * hookSpecificOutput: {
14
+ * hookEventName: "Elicitation",
15
+ * action: "accept" | "decline" | "cancel",
16
+ * content: { <fieldName>: <value> } // required when action === "accept"
17
+ * }
18
+ * }
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ const path_1 = __importDefault(require("path"));
25
+ const paths_1 = require("./src/utils/paths");
26
+ require('dotenv').config({ path: path_1.default.join(paths_1.PROJECT_ROOT, '.env'), quiet: true });
27
+ const fs_1 = __importDefault(require("fs"));
28
+ const https_1 = __importDefault(require("https"));
29
+ const workspace_router_1 = require("./workspace-router");
30
+ const prompt_bridge_1 = require("./prompt-bridge");
31
+ const active_check_1 = require("./src/utils/active-check");
32
+ const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
33
+ const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
34
+ // Polling configuration
35
+ const POLL_INTERVAL_MS = 500;
36
+ const POLL_TIMEOUT_MS = 120000; // 2 minutes for user input
37
+ // ── Main ────────────────────────────────────────────────────────
38
+ async function main() {
39
+ const raw = await readStdin();
40
+ // Skip Telegram notification if user is at terminal AND this wasn't Telegram-injected
41
+ const typingActivePath = path_1.default.join(paths_1.PROJECT_ROOT, 'src/data', 'typing-active');
42
+ const isTelegramInjected = fs_1.default.existsSync(typingActivePath);
43
+ if (!isTelegramInjected && (0, active_check_1.isUserActiveAtTerminal)()) {
44
+ return;
45
+ }
46
+ let payload;
47
+ try {
48
+ payload = JSON.parse(raw);
49
+ }
50
+ catch {
51
+ return;
52
+ }
53
+ // Payload uses `mcp_server_name` (newer) — fall back to `mcp_server` for safety
54
+ const mcpServer = payload.mcp_server_name || payload.mcp_server || 'Unknown MCP';
55
+ const schema = payload.requested_schema || {};
56
+ const properties = schema.properties || {};
57
+ const fieldNames = Object.keys(properties);
58
+ const cwd = payload.cwd || process.cwd();
59
+ const workspace = (0, workspace_router_1.extractWorkspaceName)(cwd);
60
+ // If the schema has no fields, decline — we can't build a meaningful response.
61
+ if (fieldNames.length === 0) {
62
+ emitDecline();
63
+ return;
64
+ }
65
+ // Prompt the user one field at a time. Any field that doesn't get an answer
66
+ // (timeout) cancels the whole elicitation.
67
+ const content = {};
68
+ for (let i = 0; i < fieldNames.length; i++) {
69
+ const fieldName = fieldNames[i];
70
+ const prop = properties[fieldName];
71
+ const promptId = (0, prompt_bridge_1.generatePromptId)();
72
+ let messageText = `\u{1F50C} *MCP Input Required* — ${escapeMarkdown(workspace)}\n\n`;
73
+ messageText += `*Server:* \`${escapeMarkdown(mcpServer)}\`\n`;
74
+ if (fieldNames.length > 1) {
75
+ messageText += `*Field ${i + 1} of ${fieldNames.length}:* \`${escapeMarkdown(fieldName)}\`\n\n`;
76
+ }
77
+ else {
78
+ messageText += `*Field:* \`${escapeMarkdown(fieldName)}\`\n\n`;
79
+ }
80
+ if (prop.description) {
81
+ messageText += `${escapeMarkdown(prop.description)}\n\n`;
82
+ }
83
+ if (prop.enum && prop.enum.length > 0) {
84
+ messageText += `_Allowed values:_ ${prop.enum.map(v => `\`${escapeMarkdown(v)}\``).join(', ')}\n\n`;
85
+ }
86
+ messageText += `_Reply to this message with your answer_`;
87
+ (0, prompt_bridge_1.writePending)(promptId, {
88
+ type: 'elicitation',
89
+ workspace,
90
+ mcpServer,
91
+ fieldName,
92
+ });
93
+ try {
94
+ const result = await sendTelegram(messageText);
95
+ if (result && result.message_id) {
96
+ (0, workspace_router_1.trackNotificationMessage)(result.message_id, workspace, 'elicitation');
97
+ }
98
+ }
99
+ catch (err) {
100
+ process.stderr.write(`[elicitation-notify] Telegram send failed: ${err.message}\n`);
101
+ (0, prompt_bridge_1.cleanPrompt)(promptId);
102
+ emitCancel();
103
+ return;
104
+ }
105
+ const response = await pollForResponse(promptId);
106
+ (0, prompt_bridge_1.cleanPrompt)(promptId);
107
+ if (!response || typeof response.textAnswer !== 'string') {
108
+ // Timeout or missing answer — cancel the whole elicitation
109
+ emitCancel();
110
+ return;
111
+ }
112
+ content[fieldName] = response.textAnswer;
113
+ }
114
+ const output = {
115
+ hookSpecificOutput: {
116
+ hookEventName: 'Elicitation',
117
+ action: 'accept',
118
+ content,
119
+ },
120
+ };
121
+ process.stdout.write(JSON.stringify(output) + '\n');
122
+ }
123
+ function emitDecline() {
124
+ const output = {
125
+ hookSpecificOutput: {
126
+ hookEventName: 'Elicitation',
127
+ action: 'decline',
128
+ },
129
+ };
130
+ process.stdout.write(JSON.stringify(output) + '\n');
131
+ }
132
+ function emitCancel() {
133
+ const output = {
134
+ hookSpecificOutput: {
135
+ hookEventName: 'Elicitation',
136
+ action: 'cancel',
137
+ },
138
+ };
139
+ process.stdout.write(JSON.stringify(output) + '\n');
140
+ }
141
+ // ── Polling ─────────────────────────────────────────────────────
142
+ function pollForResponse(promptId) {
143
+ return new Promise((resolve) => {
144
+ const responseFile = path_1.default.join(prompt_bridge_1.PROMPTS_DIR, `response-${promptId}.json`);
145
+ const startTime = Date.now();
146
+ const interval = setInterval(() => {
147
+ if (Date.now() - startTime > POLL_TIMEOUT_MS) {
148
+ clearInterval(interval);
149
+ process.stderr.write(`[elicitation-notify] Timed out waiting for response\n`);
150
+ resolve(null);
151
+ return;
152
+ }
153
+ try {
154
+ if (fs_1.default.existsSync(responseFile)) {
155
+ const raw = fs_1.default.readFileSync(responseFile, 'utf8');
156
+ const data = JSON.parse(raw);
157
+ clearInterval(interval);
158
+ resolve(data);
159
+ }
160
+ }
161
+ catch {
162
+ // File not ready yet or parse error — keep polling
163
+ }
164
+ }, POLL_INTERVAL_MS);
165
+ });
166
+ }
167
+ // ── Telegram ────────────────────────────────────────────────────
168
+ function sendTelegram(text) {
169
+ return new Promise((resolve, reject) => {
170
+ const body = JSON.stringify({
171
+ chat_id: CHAT_ID,
172
+ text,
173
+ parse_mode: 'Markdown',
174
+ });
175
+ const options = {
176
+ hostname: 'api.telegram.org',
177
+ path: `/bot${BOT_TOKEN}/sendMessage`,
178
+ method: 'POST',
179
+ headers: {
180
+ 'Content-Type': 'application/json',
181
+ 'Content-Length': Buffer.byteLength(body),
182
+ },
183
+ timeout: 5000,
184
+ };
185
+ const req = https_1.default.request(options, (res) => {
186
+ let data = '';
187
+ res.on('data', (chunk) => { data += chunk; });
188
+ res.on('end', () => {
189
+ if (res.statusCode >= 200 && res.statusCode < 300) {
190
+ try {
191
+ const parsed = JSON.parse(data);
192
+ resolve(parsed.result || null);
193
+ }
194
+ catch {
195
+ resolve(null);
196
+ }
197
+ }
198
+ else {
199
+ reject(new Error(`Telegram API ${res.statusCode}: ${data}`));
200
+ }
201
+ });
202
+ });
203
+ req.on('error', reject);
204
+ req.on('timeout', () => {
205
+ req.destroy();
206
+ reject(new Error('Telegram request timed out'));
207
+ });
208
+ req.write(body);
209
+ req.end();
210
+ });
211
+ }
212
+ // ── Helpers ──────────────────────────────────────────────────────
213
+ function readStdin() {
214
+ return new Promise((resolve) => {
215
+ let data = '';
216
+ let resolved = false;
217
+ process.stdin.setEncoding('utf8');
218
+ process.stdin.on('data', (chunk) => { data += chunk; });
219
+ process.stdin.on('end', () => {
220
+ if (!resolved) {
221
+ resolved = true;
222
+ resolve(data);
223
+ }
224
+ });
225
+ setTimeout(() => {
226
+ if (!resolved) {
227
+ resolved = true;
228
+ process.stdin.destroy();
229
+ resolve(data || '{}');
230
+ }
231
+ }, 500);
232
+ });
233
+ }
234
+ function escapeMarkdown(text) {
235
+ return text.replace(/([_*`\[])/g, '\\$1');
236
+ }
237
+ // ── Run ─────────────────────────────────────────────────────────
238
+ main().catch((err) => {
239
+ process.stderr.write(`[elicitation-notify] Fatal: ${err.message}\n`);
240
+ });
241
+ //# sourceMappingURL=elicitation-notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"elicitation-notify.js","sourceRoot":"","sources":["../elicitation-notify.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,9 @@
3
3
  * Enhanced Hook Notifier — called by Claude Code hooks.
4
4
  *
5
5
  * Handles: Stop (completed), Notification (waiting), SessionStart,
6
- * SessionEnd, SubagentStop (subagent-done).
6
+ * SessionEnd, SubagentStop (subagent-done), StopFailure (stop-failure),
7
+ * PostCompact (post-compact), TaskCreated (task-created),
8
+ * CwdChanged (cwd-changed), InstructionsLoaded (instructions-loaded).
7
9
  *
8
10
  * Usage (in ~/.claude/settings.json hooks):
9
11
  * node /path/to/ccgram/dist/enhanced-hook-notify.js completed
@@ -11,6 +13,11 @@
11
13
  * node /path/to/ccgram/dist/enhanced-hook-notify.js session-start
12
14
  * node /path/to/ccgram/dist/enhanced-hook-notify.js session-end
13
15
  * node /path/to/ccgram/dist/enhanced-hook-notify.js subagent-done
16
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js stop-failure
17
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js post-compact
18
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js task-created
19
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js cwd-changed
20
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js instructions-loaded
14
21
  */
15
22
  export {};
16
23
  //# sourceMappingURL=enhanced-hook-notify.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"enhanced-hook-notify.d.ts","sourceRoot":"","sources":["../enhanced-hook-notify.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;GAYG"}
1
+ {"version":3,"file":"enhanced-hook-notify.d.ts","sourceRoot":"","sources":["../enhanced-hook-notify.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;GAmBG"}
@@ -4,7 +4,9 @@
4
4
  * Enhanced Hook Notifier — called by Claude Code hooks.
5
5
  *
6
6
  * Handles: Stop (completed), Notification (waiting), SessionStart,
7
- * SessionEnd, SubagentStop (subagent-done).
7
+ * SessionEnd, SubagentStop (subagent-done), StopFailure (stop-failure),
8
+ * PostCompact (post-compact), TaskCreated (task-created),
9
+ * CwdChanged (cwd-changed), InstructionsLoaded (instructions-loaded).
8
10
  *
9
11
  * Usage (in ~/.claude/settings.json hooks):
10
12
  * node /path/to/ccgram/dist/enhanced-hook-notify.js completed
@@ -12,6 +14,11 @@
12
14
  * node /path/to/ccgram/dist/enhanced-hook-notify.js session-start
13
15
  * node /path/to/ccgram/dist/enhanced-hook-notify.js session-end
14
16
  * node /path/to/ccgram/dist/enhanced-hook-notify.js subagent-done
17
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js stop-failure
18
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js post-compact
19
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js task-created
20
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js cwd-changed
21
+ * node /path/to/ccgram/dist/enhanced-hook-notify.js instructions-loaded
15
22
  */
16
23
  var __importDefault = (this && this.__importDefault) || function (mod) {
17
24
  return (mod && mod.__esModule) ? mod : { "default": mod };
@@ -37,6 +44,12 @@ const STATUS_CONFIG = {
37
44
  'session-start': { icon: '\u{1F7E2}', label: 'Session started', alwaysNotify: false, upsertSession: true },
38
45
  'session-end': { icon: '\u{1F534}', label: 'Session ended', alwaysNotify: false, upsertSession: false },
39
46
  'subagent-done': { icon: '\u{1F916}', label: 'Subagent finished', alwaysNotify: false, upsertSession: true },
47
+ // Phase 2 hook events
48
+ 'stop-failure': { icon: '\u26A0\uFE0F', label: 'API error', alwaysNotify: true, upsertSession: true },
49
+ 'post-compact': { icon: '\u{1F4E6}', label: 'Context compacted', alwaysNotify: false, upsertSession: true },
50
+ 'task-created': { icon: '\u{1F4CB}', label: 'Task created', alwaysNotify: false, upsertSession: true },
51
+ 'cwd-changed': { icon: '\u{1F4C2}', label: 'Directory changed', alwaysNotify: false, upsertSession: true },
52
+ 'instructions-loaded': { icon: '\u{1F4D6}', label: 'Instructions loaded', alwaysNotify: false, upsertSession: false },
40
53
  };
41
54
  // ── Main ────────────────────────────────────────────────────────
42
55
  async function main() {
@@ -50,23 +63,38 @@ async function main() {
50
63
  }
51
64
  const cwd = payload.cwd || process.env.CLAUDE_CWD || process.cwd();
52
65
  const workspace = (0, workspace_router_1.extractWorkspaceName)(cwd);
66
+ const sessionTitle = payload.session_title || null; // v2.1.94+
53
67
  const tmuxSession = detectSessionName(cwd);
54
68
  const sessionId = payload.session_id || null;
55
69
  const config = STATUS_CONFIG[STATUS_ARG] ?? STATUS_CONFIG['waiting'];
70
+ // Extract rate limit info from payload (Claude Code v2.1.80+)
71
+ const rateLimit = extractRateLimitInfo(payload);
56
72
  // Update session map (skip for session-end — session is over)
57
73
  if (config.upsertSession) {
58
74
  try {
75
+ const sessionType = process.env.TMUX ? 'tmux'
76
+ : process.env.TERM_PROGRAM === 'ghostty' ? 'ghostty'
77
+ : undefined;
59
78
  (0, workspace_router_1.upsertSession)({
60
79
  cwd,
61
80
  tmuxSession: tmuxSession || `claude-${workspace}`,
62
81
  status: STATUS_ARG,
63
82
  sessionId,
83
+ sessionType,
84
+ rateLimit,
64
85
  });
65
86
  }
66
87
  catch (err) {
67
88
  logger.error(`Failed to update session map: ${err.message}`);
68
89
  }
69
90
  }
91
+ else if (rateLimit) {
92
+ // Even for session-end, update rate limit if present
93
+ try {
94
+ (0, workspace_router_1.updateSessionRateLimit)(workspace, rateLimit);
95
+ }
96
+ catch { }
97
+ }
70
98
  // If the bot injected this command from Telegram, typing-active exists — always notify
71
99
  // so the response goes back to Telegram regardless of terminal activity.
72
100
  const typingActivePath = path_1.default.join(paths_1.PROJECT_ROOT, 'src/data', 'typing-active');
@@ -90,9 +118,62 @@ async function main() {
90
118
  catch { }
91
119
  return;
92
120
  }
93
- let message = `${config.icon} ${config.label} in <b>${escapeHtml(workspace)}</b>`;
94
- // Append Claude's last response text (skip for session-start — nothing said yet)
95
- if (STATUS_ARG !== 'session-start') {
121
+ // Include session title if available (v2.1.94+)
122
+ const titleSuffix = sessionTitle ? ` (${escapeHtml(sessionTitle)})` : '';
123
+ let message = `${config.icon} ${config.label} in <b>${escapeHtml(workspace)}</b>${titleSuffix}`;
124
+ // Status-specific message additions
125
+ if (STATUS_ARG === 'stop-failure') {
126
+ // Include error details from StopFailure hook
127
+ const errorType = payload.error_type;
128
+ const errorMessage = payload.error_message;
129
+ if (errorType || errorMessage) {
130
+ message += `\n\n<b>Error:</b> ${escapeHtml(errorType || 'Unknown')}`;
131
+ if (errorMessage) {
132
+ message += `\n${escapeHtml(errorMessage)}`;
133
+ }
134
+ }
135
+ }
136
+ else if (STATUS_ARG === 'post-compact') {
137
+ // Include compaction stats if available
138
+ const beforeTokens = payload.tokens_before;
139
+ const afterTokens = payload.tokens_after;
140
+ if (beforeTokens && afterTokens) {
141
+ const saved = beforeTokens - afterTokens;
142
+ const percent = Math.round((saved / beforeTokens) * 100);
143
+ message += `\n\n<i>${beforeTokens.toLocaleString()} → ${afterTokens.toLocaleString()} tokens (${percent}% saved)</i>`;
144
+ }
145
+ }
146
+ else if (STATUS_ARG === 'task-created') {
147
+ // Include task description
148
+ const taskDescription = payload.task_description;
149
+ const taskId = payload.task_id;
150
+ if (taskDescription) {
151
+ message += `\n\n${escapeHtml(taskDescription)}`;
152
+ }
153
+ if (taskId) {
154
+ message += `\n<i>ID: ${escapeHtml(taskId)}</i>`;
155
+ }
156
+ }
157
+ else if (STATUS_ARG === 'cwd-changed') {
158
+ // Include old and new directory
159
+ const oldCwd = payload.old_cwd;
160
+ const newCwd = payload.new_cwd;
161
+ if (oldCwd && newCwd) {
162
+ const oldWorkspace = (0, workspace_router_1.extractWorkspaceName)(oldCwd);
163
+ const newWorkspace = (0, workspace_router_1.extractWorkspaceName)(newCwd);
164
+ message += `\n\n<i>${escapeHtml(oldWorkspace || oldCwd)} → ${escapeHtml(newWorkspace || newCwd)}</i>`;
165
+ }
166
+ }
167
+ else if (STATUS_ARG === 'instructions-loaded') {
168
+ // Include instructions file path
169
+ const instructionsPath = payload.instructions_path;
170
+ if (instructionsPath) {
171
+ message += `\n\n<i>${escapeHtml(instructionsPath)}</i>`;
172
+ }
173
+ }
174
+ // Append Claude's last response text (skip for session-start and simple notifications)
175
+ const skipResponseText = ['session-start', 'post-compact', 'task-created', 'cwd-changed', 'instructions-loaded'];
176
+ if (!skipResponseText.includes(STATUS_ARG)) {
96
177
  const responseText = getResponseText(payload);
97
178
  if (responseText) {
98
179
  const truncated = responseText.length > 3500
@@ -200,6 +281,34 @@ function sendTelegram(text, parseMode = 'Markdown') {
200
281
  });
201
282
  }
202
283
  // ── Helpers ──────────────────────────────────────────────────────
284
+ /**
285
+ * Extract rate limit info from hook payload (Claude Code v2.1.80+).
286
+ * Rate limits may appear in statusline data or direct payload fields.
287
+ */
288
+ function extractRateLimitInfo(payload) {
289
+ // Try direct rate_limits field
290
+ const rateLimits = payload.rate_limits;
291
+ if (rateLimits) {
292
+ return {
293
+ remaining: rateLimits.remaining,
294
+ limit: rateLimits.limit,
295
+ resetsAt: rateLimits.resets_at,
296
+ updatedAt: Date.now(),
297
+ };
298
+ }
299
+ // Try statusline object (may contain rate limit info)
300
+ const statusline = payload.statusline;
301
+ if (statusline?.rate_limits) {
302
+ const sl = statusline.rate_limits;
303
+ return {
304
+ remaining: sl.remaining,
305
+ limit: sl.limit,
306
+ resetsAt: sl.resets_at,
307
+ updatedAt: Date.now(),
308
+ };
309
+ }
310
+ return undefined;
311
+ }
203
312
  function extractLastResponse(transcriptPath) {
204
313
  const data = fs_1.default.readFileSync(transcriptPath, 'utf8').trimEnd();
205
314
  const lines = data.split('\n');
@@ -248,7 +357,12 @@ function detectSessionName(cwd) {
248
357
  }
249
358
  catch { }
250
359
  }
251
- // 2. Derive from CWD sanitize so it matches PTY handle key format
360
+ // 2. Ghosttyderive from CWD (same sanitization as tmux-less path)
361
+ if (process.env.TERM_PROGRAM === 'ghostty') {
362
+ const raw = (0, workspace_router_1.extractWorkspaceName)(cwd);
363
+ return raw ? raw.replace(/[.:\s]/g, '-') : null;
364
+ }
365
+ // 3. Derive from CWD — sanitize so it matches PTY handle key format
252
366
  const raw = (0, workspace_router_1.extractWorkspaceName)(cwd);
253
367
  if (!raw)
254
368
  return null;