claude-notification-plugin 1.0.66 → 1.0.72

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-notification-plugin",
3
- "version": "1.0.66",
3
+ "version": "1.0.72",
4
4
  "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
5
5
  "author": {
6
6
  "name": "Viacheslav Makarov",
package/README.md CHANGED
@@ -73,7 +73,7 @@ Config file: `~/.claude/notifier.config.json`
73
73
  },
74
74
  "webhookUrl": "",
75
75
  "sendUserPromptToWebhook": false,
76
- "minSeconds": 15,
76
+ "notifyAfterSeconds": 15,
77
77
  "notifyOnWaiting": false,
78
78
  "debug": false
79
79
  }
@@ -149,9 +149,10 @@ Default: **false**
149
149
  ENV: `CLAUDE_NOTIFY_SEND_USER_PROMPT_TO_WEBHOOK`
150
150
 
151
151
 
152
- **minSeconds**
152
+ **notifyAfterSeconds**
153
153
  Skip notifications for tasks shorter than this (seconds).
154
154
  Default: **15**
155
+ ENV: `CLAUDE_NOTIFY_AFTER_SECONDS`
155
156
 
156
157
 
157
158
  **debug**
@@ -180,7 +181,8 @@ Add to `.claude/settings.local.json` in the project root to control channels per
180
181
  "CLAUDE_NOTIFY_DEBUG": 0,
181
182
  "CLAUDE_NOTIFY_INCLUDE_LAST_CC_MESSAGE_IN_TELEGRAM": 1,
182
183
  "CLAUDE_NOTIFY_WEBHOOK_URL": "",
183
- "CLAUDE_NOTIFY_SEND_USER_PROMPT_TO_WEBHOOK": 0
184
+ "CLAUDE_NOTIFY_SEND_USER_PROMPT_TO_WEBHOOK": 0,
185
+ "CLAUDE_NOTIFY_AFTER_SECONDS": 15
184
186
  }
185
187
  }
186
188
  ```
package/bin/install.js CHANGED
@@ -163,7 +163,7 @@ async function main () {
163
163
  },
164
164
  webhookUrl: '',
165
165
  sendUserPromptToWebhook: false,
166
- minSeconds: 15,
166
+ notifyAfterSeconds: 15,
167
167
  notifyOnWaiting: false,
168
168
  debug: false,
169
169
  };
package/commands/setup.md CHANGED
@@ -18,7 +18,7 @@ Help the user configure the notification plugin. The config file is `~/.claude/n
18
18
  - If auto-detection fails, ask the user to enter the Chat ID manually.
19
19
  - If Chat ID already exists, show it and ask if the user wants to keep it.
20
20
 
21
- 4. **Write the config** to `~/.claude/notifier.config.json`. Merge with existing config — do NOT overwrite settings that already exist (`minSeconds`, `notifyOnWaiting`, `debug`, channel `enabled` flags, etc.). Only update `telegram.token` and `telegram.chatId` with the values from this session.
21
+ 4. **Write the config** to `~/.claude/notifier.config.json`. Merge with existing config — do NOT overwrite settings that already exist (`notifyAfterSeconds`, `notifyOnWaiting`, `debug`, channel `enabled` flags, etc.). Only update `telegram.token` and `telegram.chatId` with the values from this session.
22
22
 
23
23
  Default config structure (for new installs):
24
24
  ```json
@@ -32,7 +32,7 @@ Help the user configure the notification plugin. The config file is `~/.claude/n
32
32
  "windowsNotification": { "enabled": true },
33
33
  "sound": { "enabled": true, "file": "C:/Windows/Media/notify.wav" },
34
34
  "voice": { "enabled": true },
35
- "minSeconds": 15,
35
+ "notifyAfterSeconds": 15,
36
36
  "notifyOnWaiting": false,
37
37
  "debug": false
38
38
  }
@@ -0,0 +1,984 @@
1
+ # Telegram Listener - Detailed Guide
2
+
3
+ Telegram Listener is a background daemon that receives tasks from a Telegram chat
4
+ and executes them on your machine via `claude -p`. The result is sent back to Telegram.
5
+
6
+ **[Quick Start here](../LISTENER.md)**
7
+
8
+ # Detailed Guide
9
+
10
+ ## Table of Contents
11
+
12
+ - [What is the Listener](#what-is-the-listener)
13
+ - [Long polling: how it works](#long-polling-how-it-works)
14
+ - [Detached process: why the listener lives without a terminal](#detached-process-why-the-listener-lives-without-a-terminal)
15
+ - [PID file and duplicate protection](#pid-file-and-duplicate-protection)
16
+ - [Listener components](#listener-components)
17
+ - [Message processing flow](#message-processing-flow)
18
+ - [Configuration](#configuration)
19
+ - [Sending tasks](#sending-tasks)
20
+ - [Projects and worktrees](#projects-and-worktrees)
21
+ - [Task queues](#task-queues)
22
+ - [Bot commands](#bot-commands)
23
+ - [Task lifecycle](#task-lifecycle)
24
+ - [State files](#state-files)
25
+ - [Security](#security)
26
+ - [Troubleshooting](#troubleshooting)
27
+ - [Full session example](#full-session-example)
28
+
29
+ ---
30
+
31
+ ## What is the Listener
32
+
33
+ The Listener is **not a web server**. It does not listen on ports, does not accept incoming connections, and does not require a public IP, domain, or SSL.
34
+
35
+ The Listener is a regular Node.js program that runs an infinite loop:
36
+
37
+ ```
38
+ while (true) {
39
+ 1. Send an HTTP request to Telegram: "Any new messages?"
40
+ 2. Telegram responds: "Yes, here are 3 messages" (or "No, nothing")
41
+ 3. Process each message
42
+ 4. goto 1
43
+ }
44
+ ```
45
+
46
+ The Listener fetches data from Telegram itself (outgoing requests), rather than Telegram connecting to it (incoming). That's why it works behind any NAT, firewall, or VPN — from anywhere with internet access.
47
+
48
+ ---
49
+
50
+ ## Long polling: how it works
51
+
52
+ ```
53
+ Listener (your computer) Telegram server
54
+ ──────────────────────── ──────────────
55
+
56
+ GET /getUpdates?timeout=30 ────► "Let's wait up to 30 seconds,
57
+ maybe someone will write..."
58
+
59
+ (listener hangs and waits,
60
+ connection is open)
61
+
62
+ After 15 sec a user
63
+ wrote "@proj1 fix bug"
64
+
65
+ ◄──── {"result": [{"message":...}]} "There's a message, here it is!"
66
+
67
+ Processing... launching claude...
68
+
69
+ GET /getUpdates?timeout=30 ────► "Waiting again..."
70
+
71
+ (30 seconds of silence, nobody writes)
72
+
73
+ ◄──── {"result": []} "Nothing in 30 sec"
74
+
75
+ GET /getUpdates?timeout=30 ────► ...and so on in a loop
76
+ ```
77
+
78
+ **How `timeout=30` works**: this is not a polling interval of once every 30 seconds. It tells Telegram: "keep the connection open for up to 30 seconds. If a message arrives during this time — respond **immediately**. If not — respond with an empty result."
79
+
80
+ In practice, delivery latency is:
81
+ - If a message arrives while waiting → **instant** (less than a second)
82
+ - Worst case (message arrives right after a response) → up to 30 seconds
83
+
84
+ The `offset` parameter in the request ensures each message is processed exactly once: after receiving messages, the listener remembers `update_id + 1` and passes it in the next request, so Telegram doesn't return already-processed messages.
85
+
86
+ ---
87
+
88
+ ## Detached process: why the listener lives without a terminal
89
+
90
+ When you run `claude-notify-listener start`, the following happens:
91
+
92
+ ```
93
+ Your terminal
94
+
95
+ └─► listener-cli.js start
96
+
97
+ ├─ Check: is it already running?
98
+ ├─ Check config
99
+
100
+ ├─ spawn("node listener.js", { detached: true })
101
+ │ │
102
+ │ └─► listener.js ← SEPARATE OS PROCESS
103
+ │ Not attached to the terminal.
104
+ │ Lives on its own.
105
+ │ PID written to ~/.claude/.listener.pid
106
+
107
+ ├─ child.unref() ← "don't wait for child process to finish"
108
+ ├─ console.log("Started PID: 12345")
109
+ └─ exit
110
+
111
+ Terminal is free (or closed)
112
+
113
+ listener.js continues running.
114
+ ```
115
+
116
+ `detached: true` — creates a process not tied to the parent.
117
+ `child.unref()` — allows `listener-cli.js` to exit without waiting for the child.
118
+
119
+ Result: the listener runs as a background OS process. It is not tied to a terminal or to Claude Code — only to the operating system. It will only stop when:
120
+ - The `claude-notify-listener stop` command is issued (or `/stop` in Telegram)
121
+ - The computer is shut down or restarted
122
+ - It crashes due to an error
123
+
124
+ ---
125
+
126
+ ## PID file and duplicate protection
127
+
128
+ The PID file (`~/.claude/.listener.pid`) is simply a text file with the process number:
129
+
130
+ ```
131
+ 12345
132
+ ```
133
+
134
+ Why it's needed:
135
+ - **`start`** — check whether the listener is already running
136
+ - **`stop`** — determine which process to kill
137
+ - **`status`** — show whether it's running and with which PID
138
+
139
+ ### What if the listener crashes but the PID file remains?
140
+
141
+ This is a normal situation. Every `start` and `status` performs a check:
142
+
143
+ ```
144
+ 1. Read PID from file → 12345
145
+ 2. Check: is process 12345 alive?
146
+ Windows: tasklist /FI "PID eq 12345"
147
+ Linux: kill -0 12345
148
+
149
+ 3a. Process is ALIVE
150
+ → "Listener is already running (PID: 12345)"
151
+ → A new one is not started
152
+
153
+ 3b. Process is DEAD
154
+ → PID file is stale, delete it
155
+ → Start a new listener normally
156
+ ```
157
+
158
+ The scenario where the OS reuses the PID for another process is extremely unlikely, and even if it happens, the listener will simply show "already running" and you can delete the PID file manually (`rm ~/.claude/.listener.pid`) and start again.
159
+
160
+ ### Two listener instances
161
+
162
+ Running two listeners is impossible — the PID file prevents it. And this is important: two listeners on the same bot break long polling. Whichever calls `getUpdates` first gets the messages. The second gets an empty response. Messages would be randomly lost between the two processes.
163
+
164
+ ---
165
+
166
+ ## Listener components
167
+
168
+ ```
169
+ ┌────────────────────────────────────────────┐
170
+ │ listener.js (OS process) │
171
+ │ │
172
+ │ ┌────────────────┐ ┌───────────────┐ │
173
+ │ │ TelegramPoller │ │ WorkQueue │ │
174
+ │ │ │ │ (per-workDir) │ │
175
+ │ │ long polling │ │ active + FIFO │ │
176
+ │ │ getUpdates() │ │ .json on disk │ │
177
+ │ │ sendMessage() │ └───────┬───────┘ │
178
+ │ └───────┬────────┘ │ │
179
+ │ │ │ │
180
+ │ ┌───────┴────────┐ ┌───────┴───────┐ │
181
+ │ │ MessageParser │ │ TaskRunner │ │
182
+ │ │ │ │ │ │
183
+ │ │ @proj/branch │ │ spawn claude │ │
184
+ │ │ /commands │ │ timeouts │ │
185
+ │ └────────────────┘ │ kill │ │
186
+ │ └───────────────┘ │
187
+ │ ┌────────────────┐ ┌───────────────┐ │
188
+ │ │WorktreeManager │ │ Logger │ │
189
+ │ │ │ │ │ │
190
+ │ │ git worktree │ │ .log file │ │
191
+ │ │ add/remove │ │ rotation 5MB │ │
192
+ │ │ auto-discover │ └───────────────┘ │
193
+ │ └────────────────┘ │
194
+ └────────────────────────────────────────────┘
195
+ ```
196
+
197
+ | Module | File | Description |
198
+ |---|---|---|
199
+ | **TelegramPoller** | `telegram-poller.js` | Long polling to the Telegram API. Receives messages, sends replies. Splits long messages into chunks |
200
+ | **MessageParser** | `message-parser.js` | Parses message text: is it a command (`/status`) or a task (`@proj1 fix bug`)? Extracts project, branch, task text |
201
+ | **WorkQueue** | `work-queue.js` | Manages task queues. Each working directory has a separate FIFO queue. Guarantees: one `claude` process per directory. Persists state to disk |
202
+ | **TaskRunner** | `task-runner.js` | Runs `claude -p "task"` as a child process. Monitors timeouts. Can kill the process on cancellation. Emits events: complete, error, timeout |
203
+ | **WorktreeManager** | `worktree-manager.js` | Creates and removes git worktrees. Auto-discovery via `git worktree list`. Maps `@project/branch` to a path on disk |
204
+ | **Logger** | `logger.js` | Writes log to `~/.claude/.listener.log`. Rotation when exceeding 5 MB (old file → `.log.old`) |
205
+
206
+ ---
207
+
208
+ ## Message processing flow
209
+
210
+ ```
211
+ Telegram message
212
+
213
+
214
+ TelegramPoller.getUpdates()
215
+
216
+ ├─ chat_id matches config? ── No ──► ignore + log warning
217
+
218
+ ▼ Yes
219
+ MessageParser.parse(text)
220
+
221
+ ├─ Starts with "/"? ──► Command
222
+ │ ├─ /status, /queue, /cancel, /drop, /clear
223
+ │ ├─ /projects, /worktrees, /worktree, /rmworktree
224
+ │ ├─ /history, /help, /stop
225
+ │ └─ Execute → reply in Telegram
226
+
227
+ └─ Otherwise ──► Task
228
+
229
+ ├─ "@proj1/feature/auth fix bug"
230
+ │ → project = "proj1"
231
+ │ → branch = "feature/auth"
232
+ │ → text = "fix bug"
233
+
234
+ ├─ "@proj1 fix bug"
235
+ │ → project = "proj1"
236
+ │ → branch = null (main)
237
+ │ → text = "fix bug"
238
+
239
+ └─ "fix bug"
240
+ → project = "default"
241
+ → branch = null (main)
242
+ → text = "fix bug"
243
+
244
+
245
+ WorktreeManager.resolveWorkDir(project, branch)
246
+
247
+ ├─ branch = null → path from config (main worktree)
248
+ ├─ branch found in worktrees → worktree path
249
+ ├─ branch not found + autoCreate = true → git worktree add
250
+ └─ branch not found + autoCreate = false → error
251
+
252
+
253
+ WorkQueue.enqueue(workDir, task)
254
+
255
+ ├─ workDir is free (active = null)
256
+ │ → active = task
257
+ │ → TaskRunner.run(workDir, task)
258
+ │ → claude -p "fix bug" --output-format text
259
+ │ → Telegram: "⏳ Running: fix bug"
260
+
261
+ └─ workDir is busy (active != null)
262
+ → queue.push(task)
263
+ → Telegram: "📋 Queued (position N)"
264
+
265
+ ▼ (when claude finishes)
266
+ TaskRunner emit 'complete'/'error'/'timeout'
267
+
268
+ ├─ Send result to Telegram
269
+ └─ WorkQueue.onTaskComplete(workDir)
270
+ ├─ Queue is empty → active = null
271
+ └─ More tasks → shift() → TaskRunner.run()
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Configuration
277
+
278
+ Full example of `~/.claude/notifier.config.json` with the listener section:
279
+
280
+ ```json
281
+ {
282
+ "telegram": {
283
+ "token": "123456789:ABCdefGHIjklMNO...",
284
+ "chatId": "987654321",
285
+ "enabled": true,
286
+ "deleteAfterHours": 24
287
+ },
288
+ "listener": {
289
+ "projects": {
290
+ "default": {
291
+ "path": "/home/user/main-project"
292
+ },
293
+ "api": {
294
+ "path": "/home/user/projects/api-server",
295
+ "worktrees": {
296
+ "feature/auth": "/home/user/projects/api-wt-auth"
297
+ }
298
+ },
299
+ "web": {
300
+ "path": "/home/user/projects/web-app"
301
+ }
302
+ },
303
+ "worktreeBaseDir": "~/.claude/worktrees",
304
+ "autoCreateWorktree": true,
305
+ "taskTimeoutMinutes": 30,
306
+ "maxQueuePerWorkDir": 10,
307
+ "maxTotalTasks": 50
308
+ }
309
+ }
310
+ ```
311
+
312
+ ### Parameters
313
+
314
+ | Parameter | Default | Description |
315
+ |---|---|---|
316
+ | `projects` | — (required) | Map of projects: `alias → { path, worktrees? }` |
317
+ | `worktreeBaseDir` | `~/.claude/worktrees` | Where auto-created worktrees are stored |
318
+ | `autoCreateWorktree` | `true` | Automatically create a worktree if the branch is not found |
319
+ | `taskTimeoutMinutes` | `30` | Maximum task execution time in minutes. Force-stopped when exceeded |
320
+ | `maxQueuePerWorkDir` | `10` | Maximum tasks in the queue for a single working directory |
321
+ | `maxTotalTasks` | `50` | Maximum tasks across all queues combined |
322
+
323
+ ### What is `projects`?
324
+
325
+ Each project is an alias (short name) + path to a directory on disk:
326
+
327
+ ```json
328
+ {
329
+ "api": {
330
+ "path": "/home/user/projects/api-server"
331
+ }
332
+ }
333
+ ```
334
+
335
+ Now in Telegram you can write `@api refactor the code`, and Claude will run in the `/home/user/projects/api-server` directory.
336
+
337
+ The **`default`** alias is special. Messages without `@project` go to it:
338
+
339
+ ```json
340
+ {
341
+ "default": {
342
+ "path": "/home/user/main-project"
343
+ }
344
+ }
345
+ ```
346
+
347
+ ---
348
+
349
+ ## Sending tasks
350
+
351
+ ### Message format
352
+
353
+ In the Telegram chat with the bot:
354
+
355
+ ```
356
+ @project task ← task in the main worktree of the project
357
+ @project/branch task ← task in the worktree of a specific branch
358
+ task without @ ← task in the "default" project
359
+ ```
360
+
361
+ ### Examples
362
+
363
+ ```
364
+ add a README to the project
365
+ ```
366
+ → runs in the `default` project (if configured)
367
+
368
+ ```
369
+ @api fix the authentication bug
370
+ ```
371
+ → runs in `/home/user/projects/api-server`
372
+
373
+ ```
374
+ @api/feature/payments add Stripe integration
375
+ ```
376
+ → runs in the `feature/payments` worktree of the `api` project.
377
+ If the worktree doesn't exist, it will be created automatically.
378
+
379
+ ```
380
+ @web update dependencies
381
+ ```
382
+ → runs in `/home/user/projects/web-app`
383
+
384
+ ### What happens when a task is sent
385
+
386
+ 1. The Listener receives the message from Telegram
387
+ 2. Parses `@project/branch` from the beginning of the message
388
+ 3. Determines the working directory (workDir)
389
+ 4. Checks: is this workDir busy with another task?
390
+ - **No** → runs `claude -p "task"` immediately, replies with `⏳ Running...`
391
+ - **Yes** → adds to the queue, replies with `📋 Queued (position N)...`
392
+ 5. When Claude finishes → sends the result to Telegram
393
+ 6. If there's a next task in the queue → starts it
394
+
395
+ ---
396
+
397
+ ## Projects and worktrees
398
+
399
+ ### Why worktrees?
400
+
401
+ Git worktrees allow you to have multiple working copies of the same repository in different directories, each on its own branch.
402
+
403
+ Without worktrees: one repository = one directory = one task at a time.
404
+
405
+ With worktrees: one repository, but 3 directories on different branches = 3 tasks in parallel.
406
+
407
+ ```
408
+ api-server/ ← main worktree, branch main
409
+ └─ src/...
410
+
411
+ ~/.claude/worktrees/api/
412
+ ├─ feature-auth/ ← worktree, branch feature/auth
413
+ │ └─ src/...
414
+ └─ feature-payments/ ← worktree, branch feature/payments
415
+ └─ src/...
416
+ ```
417
+
418
+ ### How the listener works with worktrees
419
+
420
+ **The queue is tied to the working directory, not to the project name.**
421
+
422
+ This means:
423
+ - `@api task` and `@api/feature/auth task` are **different queues**, because they're different directories. They run **in parallel**.
424
+ - `@api task1` and `@api task2` are **the same queue** (both go to the main worktree). `task2` will wait for `task1` to complete.
425
+
426
+ ```
427
+ Project "api"
428
+
429
+ ├─ main worktree (/home/user/projects/api)
430
+ │ Queue: [task1] → [task2] → ... ← strictly sequential
431
+
432
+ ├─ feature/auth (~/.claude/worktrees/api/feature-auth)
433
+ │ Queue: [task3] → [task4] → ... ← strictly sequential
434
+
435
+ └─ feature/payments (~/.claude/worktrees/api/feature-payments)
436
+ Queue: [task5] → ... ← strictly sequential
437
+
438
+ All three queues run IN PARALLEL.
439
+ Within each — strictly one task at a time.
440
+ ```
441
+
442
+ ### Auto-creation of worktrees
443
+
444
+ When you write `@api/feature/new task`, and a worktree for the `feature/new` branch doesn't exist:
445
+
446
+ 1. The Listener checks: does the `feature/new` branch exist in git?
447
+ - Yes → `git worktree add ~/.claude/worktrees/api/feature-new feature/new`
448
+ - No → `git worktree add -b feature/new ~/.claude/worktrees/api/feature-new`
449
+ 2. Registers the new worktree in the config
450
+ 3. Replies in Telegram: `🌿 Created worktree feature/new for project "api"`
451
+ 4. Starts the task in the new worktree
452
+
453
+ This behavior is controlled by the `autoCreateWorktree` parameter (default: `true`).
454
+
455
+ ### Auto-discovery of worktrees
456
+
457
+ On startup, the listener scans each project with `git worktree list` and picks up all worktrees that were created manually (via `git worktree add`). You don't need to manually specify each worktree in the config.
458
+
459
+ ### Manual worktree management from Telegram
460
+
461
+ ```
462
+ /worktree @api feature/payments ← create a worktree
463
+ /worktrees @api ← list all worktrees for a project
464
+ /rmworktree @api feature/payments ← remove a worktree
465
+ ```
466
+
467
+ ---
468
+
469
+ ## Task queues
470
+
471
+ ### How it works
472
+
473
+ Each working directory (workDir) has:
474
+ - **active** — the task currently being executed (or `null`)
475
+ - **queue** — an array of tasks waiting to be executed (FIFO)
476
+
477
+ While `active !== null`, all new tasks for this workDir go into the `queue`.
478
+
479
+ ### Example: 4 tasks, 2 projects
480
+
481
+ ```
482
+ 10:00 You: @api fix the router bug
483
+ Bot: ⏳ [@api] Running: fix the router bug
484
+ (api/main: active = "fix the router bug", queue = [])
485
+
486
+ 10:01 You: @web update dependencies
487
+ Bot: ⏳ [@web] Running: update dependencies
488
+ (web/main: active = "update dependencies", queue = [])
489
+ (api and web are running in parallel!)
490
+
491
+ 10:02 You: @api add tests
492
+ Bot: 📋 [@api] Queued (position 1).
493
+ Currently running: fix the router bug
494
+ (api/main: active = "fix the router bug", queue = ["add tests"])
495
+
496
+ 10:03 You: @api refactor the code
497
+ Bot: 📋 [@api] Queued (position 2).
498
+ Currently running: fix the router bug
499
+ (api/main: active = "fix the router bug", queue = ["add tests", "refactor"])
500
+
501
+ 10:05 Bot: ✅ [@web] Done: update dependencies
502
+ <result>
503
+ (web/main: active = null, queue = [])
504
+
505
+ 10:08 Bot: ✅ [@api] Done: fix the router bug
506
+ <result>
507
+ Bot: ⏳ [@api] Running: add tests
508
+ (api/main: active = "add tests", queue = ["refactor"])
509
+ (next task started automatically!)
510
+
511
+ 10:15 Bot: ✅ [@api] Done: add tests
512
+ <result>
513
+ Bot: ⏳ [@api] Running: refactor the code
514
+ (api/main: active = "refactor", queue = [])
515
+
516
+ 10:25 Bot: ✅ [@api] Done: refactor the code
517
+ <result>
518
+ (api/main: active = null, queue = [])
519
+ (all tasks completed)
520
+ ```
521
+
522
+ ### Limits
523
+
524
+ - Maximum **10** tasks in the queue per workDir (configurable: `maxQueuePerWorkDir`)
525
+ - Maximum **50** tasks across all queues combined (configurable: `maxTotalTasks`)
526
+ - If the limit is exceeded, the bot will reply with an error
527
+
528
+ ### Timeout
529
+
530
+ If a task runs longer than 30 minutes (configurable: `taskTimeoutMinutes`), it is forcefully stopped:
531
+
532
+ ```
533
+ Bot: ⏰ [@api] Task forcefully stopped — timeout exceeded (30 min): refactor the code
534
+ ```
535
+
536
+ After a timeout, the next task from the queue starts automatically.
537
+
538
+ ---
539
+
540
+ ## Bot commands
541
+
542
+ All commands start with `/` and execute instantly (they are not queued).
543
+
544
+ ### /status — project status
545
+
546
+ ```
547
+ You: /status
548
+ Bot: 📊 Status:
549
+ Uptime: 2h 15m
550
+
551
+ api:
552
+ main: ▶ fix the router bug (3m 42s) +2 queued
553
+ feature/auth: ✅ idle
554
+ web:
555
+ main: ✅ idle
556
+ ```
557
+
558
+ ```
559
+ You: /status @api
560
+ Bot: 📊 Project "api":
561
+
562
+ main:
563
+ ▶ fix the router bug (3m 42s)
564
+ Queue: 2 tasks
565
+ feature/auth:
566
+ ✅ idle
567
+ Queue: 0 tasks
568
+ ```
569
+
570
+ ### /queue — queue contents
571
+
572
+ ```
573
+ You: /queue
574
+ Bot: 📋 Queues:
575
+
576
+ @api:
577
+ ▶ fix the router bug
578
+ 1. add tests
579
+ 2. refactor the code
580
+ ```
581
+
582
+ ### /cancel — cancel a task
583
+
584
+ ```
585
+ You: /cancel @api
586
+ Bot: 🛑 [@api] Task cancelled. Starting next.
587
+ ⏳ [@api] Running: add tests
588
+ ```
589
+
590
+ Cancelling a task in a worktree:
591
+
592
+ ```
593
+ You: /cancel @api/feature/auth
594
+ Bot: 🛑 [@api/feature/auth] Task cancelled
595
+ ```
596
+
597
+ ### /drop — remove from queue
598
+
599
+ Removes a task that **hasn't started executing yet** (waiting in the queue):
600
+
601
+ ```
602
+ You: /drop @api 2
603
+ Bot: 🗑 Removed from queue: refactor the code
604
+ ```
605
+
606
+ The task number is the position in the queue (starting from 1). You can check numbers with `/queue`.
607
+
608
+ ### /clear — clear the queue
609
+
610
+ Removes all tasks from the queue (the active task continues running):
611
+
612
+ ```
613
+ You: /clear @api
614
+ Bot: 🧹 [@api] Queue cleared (3 tasks)
615
+ ```
616
+
617
+ ### /projects — list projects
618
+
619
+ ```
620
+ You: /projects
621
+ Bot: 📂 Projects:
622
+
623
+ @default → /home/user/main-project
624
+ @api → /home/user/projects/api-server
625
+ /feature/auth → ~/.claude/worktrees/api/feature-auth
626
+ @web → /home/user/projects/web-app
627
+ ```
628
+
629
+ ### /worktrees — project worktrees
630
+
631
+ ```
632
+ You: /worktrees @api
633
+ Bot: 🌳 Worktrees for project "api":
634
+ • main → /home/user/projects/api-server
635
+ • feature/auth → ~/.claude/worktrees/api/feature-auth
636
+ • feature/payments → ~/.claude/worktrees/api/feature-payments
637
+ ```
638
+
639
+ ### /worktree — create a worktree
640
+
641
+ ```
642
+ You: /worktree @api feature/payments
643
+ Bot: 🌿 Created worktree for project "api":
644
+ Branch: feature/payments
645
+ Path: ~/.claude/worktrees/api/feature-payments
646
+ ```
647
+
648
+ ### /rmworktree — remove a worktree
649
+
650
+ ```
651
+ You: /rmworktree @api feature/payments
652
+ Bot: 🗑 Worktree feature/payments removed from project "api"
653
+ ```
654
+
655
+ If a task is running in the worktree, removal will be rejected:
656
+
657
+ ```
658
+ Bot: ❌ Cannot remove worktree: a task is running in it.
659
+ First /cancel @api/feature/payments
660
+ ```
661
+
662
+ ### /history — history
663
+
664
+ ```
665
+ You: /history
666
+ Bot: 📜 Recent tasks:
667
+
668
+ ✅ [@api] fix the router bug
669
+ ✅ [@web] update dependencies
670
+ 🛑 [@api/feature/auth] implement OAuth2
671
+ ✅ [@api] add tests
672
+ ```
673
+
674
+ ### /stop — stop the listener
675
+
676
+ ```
677
+ You: /stop
678
+ Bot: 👋 Listener is shutting down...
679
+ ```
680
+
681
+ All active tasks will be terminated. Queues are saved to disk and will be restored on the next startup.
682
+
683
+ ### /help — help
684
+
685
+ Shows a brief reference for all commands.
686
+
687
+ ---
688
+
689
+ ## Task lifecycle
690
+
691
+ ### Path of a task from message to result
692
+
693
+ ```
694
+ 1. RECEIPT
695
+ Telegram message → getUpdates() → parsing
696
+
697
+ 2. ROUTING
698
+ "@api/feature/auth task"
699
+ → project = "api"
700
+ → branch = "feature/auth"
701
+ → workDir = ~/.claude/worktrees/api/feature-auth
702
+
703
+ 3. QUEUING
704
+ workDir busy?
705
+ → No: active = task, start immediately
706
+ → Yes: queue.push(task), reply with position
707
+
708
+ 4. EXECUTION
709
+ claude -p "task" --output-format text
710
+ cwd = workDir
711
+ timeout = 30 min
712
+ Telegram: "⏳ Running: <task>"
713
+
714
+ 5. WAITING
715
+ The claude process is working...
716
+ (listener continues accepting other messages)
717
+
718
+ 6. COMPLETION
719
+ claude finished:
720
+ exit 0 → "✅ Done" + stdout
721
+ exit N → "❌ Error" + stderr
722
+ timeout → "⏰ Timeout"
723
+
724
+ 7. NEXT TASK
725
+ queue not empty?
726
+ → Yes: shift() → goto 4
727
+ → No: active = null, workDir is free
728
+ ```
729
+
730
+ ### What Claude receives
731
+
732
+ The command that gets executed:
733
+
734
+ ```bash
735
+ claude -p "your message text from Telegram" --output-format text
736
+ ```
737
+
738
+ With the working directory (`cwd`) = project/worktree workDir.
739
+
740
+ Claude sees the project files, CLAUDE.md, .claude/settings.json, and everything else as if you had launched it manually in that directory.
741
+
742
+ ### What is returned to Telegram
743
+
744
+ All stdout from claude. This is Claude's text response to your task.
745
+
746
+ Handling long responses:
747
+ - Up to 4096 characters — a single message
748
+ - 4096–20000 characters — multiple messages (split by lines)
749
+ - Over 20000 — first 2000 and last 2000 characters + full text as a file
750
+
751
+ ---
752
+
753
+ ## State files
754
+
755
+ All files are stored in `~/.claude/`:
756
+
757
+ | File | Description |
758
+ |---|---|
759
+ | `.listener.pid` | PID of the running daemon. On `start`, it checks whether the process is alive |
760
+ | `.listener.log` | Operation log. Rotation when exceeding 5 MB (old file → `.log.old`) |
761
+ | `.task_queues.json` | Current state of all queues. Persisted to disk after every change |
762
+ | `.task_history.json` | Last 50 completed tasks (for `/history`) |
763
+
764
+ ### Recovery after reboot
765
+
766
+ On startup, the listener:
767
+
768
+ 1. Loads `.task_queues.json`
769
+ 2. Watchdog checks all `active` tasks:
770
+ - Process PID is dead → clears active, starts the next one from the queue
771
+ - Task exceeded timeout → clears active, starts the next one
772
+ 3. Tasks waiting in the queue remain and will be executed
773
+
774
+ This means: if the computer reboots, tasks in the queue won't be lost. But an active task that didn't finish will be marked as stale and skipped.
775
+
776
+ ---
777
+
778
+ ## Security
779
+
780
+ ### Authorization
781
+
782
+ The Listener processes **only** messages from the `chatId` specified in the config. All other messages are ignored and logged as warnings.
783
+
784
+ ### No shell injection
785
+
786
+ Task text is passed to claude as an array argument, not through the shell:
787
+
788
+ ```js
789
+ // Like this (safe):
790
+ spawn('claude', ['-p', userText], { ... })
791
+
792
+ // NOT like this (dangerous):
793
+ exec(`claude -p "${userText}"`)
794
+ ```
795
+
796
+ ### Isolation
797
+
798
+ - One claude process per working directory
799
+ - Strictly one task at a time in a single directory
800
+ - Different directories run in parallel but don't interfere with each other
801
+
802
+ ### Limits
803
+
804
+ - 10 tasks in the queue per workDir (spam protection)
805
+ - 50 tasks total (overload protection)
806
+ - 10-minute timeout per task (hang protection)
807
+
808
+ ---
809
+
810
+ ## Troubleshooting
811
+
812
+ ### Listener won't start
813
+
814
+ ```bash
815
+ claude-notify-listener status
816
+ # → Status: not running
817
+ ```
818
+
819
+ Check:
820
+
821
+ 1. Does the config exist? `cat ~/.claude/notifier.config.json`
822
+ 2. Are `telegramToken` and `telegramChatId` present?
823
+ 3. Is there a `listener.projects` section?
824
+ 4. Logs: `claude-notify-listener logs`
825
+
826
+ ### Bot doesn't respond
827
+
828
+ 1. Is the listener running? `claude-notify-listener status`
829
+ 2. Is the chatId correct? Messages from other chats are ignored (check the log: `WARN Ignored message from chat ...`)
830
+ 3. Is the bot added to the chat? Write `/help` to the bot — if there's no response, check the token
831
+
832
+ ### Task is stuck
833
+
834
+ ```
835
+ /cancel @project
836
+ ```
837
+
838
+ Or restart the listener:
839
+
840
+ ```bash
841
+ claude-notify-listener restart
842
+ ```
843
+
844
+ The watchdog will automatically clear stale tasks on the next startup.
845
+
846
+ ### Claude can't find project files
847
+
848
+ Check the path in the config:
849
+
850
+ ```
851
+ /projects
852
+ ```
853
+
854
+ Make sure the path exists and contains the correct repository.
855
+
856
+ ---
857
+
858
+ ## Full session example
859
+
860
+ Suppose you have two projects: an API server and a web application.
861
+
862
+ ### Configuration
863
+
864
+ ```json
865
+ {
866
+ "telegram": {
867
+ "token": "123456789:ABCdef...",
868
+ "chatId": "987654321"
869
+ },
870
+ "listener": {
871
+ "projects": {
872
+ "api": { "path": "/home/user/projects/api" },
873
+ "web": { "path": "/home/user/projects/web" }
874
+ }
875
+ }
876
+ }
877
+ ```
878
+
879
+ ### Telegram session
880
+
881
+ ```
882
+ === 10:00 — Startup ===
883
+
884
+ You (terminal): claude-notify-listener start
885
+ → Listener started (PID: 12345)
886
+
887
+ === 10:01 — First task ===
888
+
889
+ You: @api add endpoint GET /users with pagination
890
+ Bot: ⏳ [@api] Running: add endpoint GET /users with pagination
891
+
892
+ Behind the scenes: process started
893
+ claude -p "add endpoint GET /users with pagination" --output-format text
894
+ cwd = /home/user/projects/api
895
+
896
+ === 10:02 — Task to another project (in parallel!) ===
897
+
898
+ You: @web add a /users page that calls GET /users
899
+ Bot: ⏳ [@web] Running: add a /users page that calls GET /users
900
+
901
+ Now two claude processes are running in parallel:
902
+ one in /home/user/projects/api, another in /home/user/projects/web
903
+
904
+ === 10:03 — Another task for api (queued) ===
905
+
906
+ You: @api add tests for /users
907
+ Bot: 📋 [@api] Queued (position 1).
908
+ Currently running: add endpoint GET /users with pagination
909
+
910
+ === 10:04 — Task in a worktree (in parallel with api/main!) ===
911
+
912
+ You: @api/feature/auth add JWT authorization middleware
913
+ Bot: 🌿 Created worktree feature/auth for project "api"
914
+ ⏳ [@api/feature/auth] Running: add JWT authorization middleware
915
+
916
+ Three claude processes running in parallel:
917
+ 1. api/main → GET /users
918
+ 2. api/auth → JWT middleware
919
+ 3. web/main → /users page
920
+
921
+ === 10:05 — Check status ===
922
+
923
+ You: /status
924
+ Bot: 📊 Status:
925
+ Uptime: 5m 12s
926
+
927
+ api:
928
+ main: ▶ add endpoint GET /users with pagination (4m 30s) +1 queued
929
+ feature/auth: ▶ add JWT authorization middleware (1m 05s)
930
+ web:
931
+ main: ▶ add a /users page... (3m 20s)
932
+
933
+ === 10:07 — web finished ===
934
+
935
+ Bot: ✅ [@web] Done: add a /users page that calls GET /users
936
+
937
+ Created file src/pages/Users.vue with a user table.
938
+ Added route in src/router.js.
939
+ Uses fetch to /api/users with page and limit parameters.
940
+
941
+ === 10:09 — api/main finished, automatically starts the next task ===
942
+
943
+ Bot: ✅ [@api] Done: add endpoint GET /users with pagination
944
+
945
+ Created controller src/controllers/users.js.
946
+ Added route GET /users in src/routes.js.
947
+ Supports query parameters: page, limit, sort.
948
+
949
+ Bot: ⏳ [@api] Running: add tests for /users
950
+
951
+ Next task from the queue started automatically!
952
+
953
+ === 10:12 — Cancel a worktree task ===
954
+
955
+ You: /cancel @api/feature/auth
956
+ Bot: 🛑 [@api/feature/auth] Task cancelled
957
+
958
+ === 10:15 — api/main (tests) finished ===
959
+
960
+ Bot: ✅ [@api] Done: add tests for /users
961
+
962
+ Created tests/users.test.js.
963
+ Covered cases: pagination, sorting, empty result, errors.
964
+
965
+ === 10:16 — Check history ===
966
+
967
+ You: /history
968
+ Bot: 📜 Recent tasks:
969
+
970
+ ✅ [@api] add tests for /users
971
+ 🛑 [@api/feature/auth] add JWT authorization middleware
972
+ ✅ [@api] add endpoint GET /users with pagination
973
+ ✅ [@web] add a /users page...
974
+
975
+ === 10:17 — Remove unneeded worktree ===
976
+
977
+ You: /rmworktree @api feature/auth
978
+ Bot: 🗑 Worktree feature/auth removed from project "api"
979
+
980
+ === Evening — Shut down ===
981
+
982
+ You: /stop
983
+ Bot: 👋 Listener is shutting down...
984
+ ```
@@ -52,7 +52,8 @@ if (!config.listener?.projects || Object.keys(config.listener.projects).length =
52
52
  }
53
53
 
54
54
  const listenerConfig = config.listener;
55
- const taskTimeout = listenerConfig.taskTimeout || 600_000;
55
+ const taskTimeoutMinutes = listenerConfig.taskTimeoutMinutes || 30;
56
+ const taskTimeout = taskTimeoutMinutes * 60_000;
56
57
 
57
58
  const poller = new TelegramPoller(token, chatId, logger);
58
59
  const queue = new WorkQueue(
@@ -139,7 +140,7 @@ runner.on('timeout', async (workDir, task) => {
139
140
  const entry = queue.queues[workDir];
140
141
  const label = formatLabel(entry);
141
142
  await poller.sendMessage(
142
- `⏰ [${label}] Timeout (${Math.round(taskTimeout / 60000)} min): ${escapeHtml(task.text)}`,
143
+ `⏰ [${label}] Task forcefully stopped — timeout exceeded (${Math.round(taskTimeout / 60000)} min): ${escapeHtml(task.text)}`,
143
144
  task.telegramMessageId,
144
145
  );
145
146
 
@@ -62,7 +62,7 @@ function loadConfig () {
62
62
  },
63
63
  webhookUrl: '',
64
64
  sendUserPromptToWebhook: false,
65
- minSeconds: 15,
65
+ notifyAfterSeconds: 15,
66
66
  notifyOnWaiting: false,
67
67
  debug: false,
68
68
  };
@@ -83,8 +83,8 @@ function loadConfig () {
83
83
  if (user.voice) {
84
84
  config.voice = { ...config.voice, ...user.voice };
85
85
  }
86
- if (typeof user.minSeconds === 'number') {
87
- config.minSeconds = user.minSeconds;
86
+ if (typeof user.notifyAfterSeconds === 'number') {
87
+ config.notifyAfterSeconds = user.notifyAfterSeconds;
88
88
  }
89
89
  if (typeof user.notifyOnWaiting === 'boolean') {
90
90
  config.notifyOnWaiting = user.notifyOnWaiting;
@@ -138,6 +138,12 @@ function loadConfig () {
138
138
  if (process.env.CLAUDE_NOTIFY_SEND_USER_PROMPT_TO_WEBHOOK !== undefined) {
139
139
  config.sendUserPromptToWebhook = process.env.CLAUDE_NOTIFY_SEND_USER_PROMPT_TO_WEBHOOK === '1';
140
140
  }
141
+ if (process.env.CLAUDE_NOTIFY_AFTER_SECONDS !== undefined) {
142
+ const val = Number(process.env.CLAUDE_NOTIFY_AFTER_SECONDS);
143
+ if (!Number.isNaN(val)) {
144
+ config.notifyAfterSeconds = val;
145
+ }
146
+ }
141
147
 
142
148
  return config;
143
149
  }
@@ -666,7 +672,7 @@ process.stdin.on('end', async () => {
666
672
  duration = Math.round((Date.now() - session.start) / 1000);
667
673
  }
668
674
 
669
- if (duration < config.minSeconds) {
675
+ if (duration < config.notifyAfterSeconds) {
670
676
  process.exit(0);
671
677
  }
672
678
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "claude-notification-plugin",
3
3
  "productName": "claude-notification-plugin",
4
- "version": "1.0.66",
4
+ "version": "1.0.72",
5
5
  "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
6
  "type": "module",
7
7
  "engines": {