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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -3
- package/bin/install.js +1 -1
- package/commands/setup.md +2 -2
- package/listener/LISTENER-DETAILED.md +984 -0
- package/listener/listener.js +3 -2
- package/notifier/notifier.js +10 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-notification-plugin",
|
|
3
|
-
"version": "1.0.
|
|
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
|
-
"
|
|
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
|
-
**
|
|
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
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 (`
|
|
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
|
-
"
|
|
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
|
+
```
|
package/listener/listener.js
CHANGED
|
@@ -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
|
|
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}]
|
|
143
|
+
`⏰ [${label}] Task forcefully stopped — timeout exceeded (${Math.round(taskTimeout / 60000)} min): ${escapeHtml(task.text)}`,
|
|
143
144
|
task.telegramMessageId,
|
|
144
145
|
);
|
|
145
146
|
|
package/notifier/notifier.js
CHANGED
|
@@ -62,7 +62,7 @@ function loadConfig () {
|
|
|
62
62
|
},
|
|
63
63
|
webhookUrl: '',
|
|
64
64
|
sendUserPromptToWebhook: false,
|
|
65
|
-
|
|
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.
|
|
87
|
-
config.
|
|
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.
|
|
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.
|
|
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": {
|