claude-notification-plugin 1.1.20 → 1.1.26

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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const COMMANDS = [
4
- '/status', '/queue', '/cancel', '/drop', '/clear',
4
+ '/status', '/queue', '/cancel', '/drop', '/clear', '/newsession',
5
5
  '/projects', '/worktrees', '/worktree', '/rmworktree',
6
6
  '/history', '/stop', '/help',
7
7
  ];
@@ -1,159 +1,195 @@
1
- #!/usr/bin/env node
2
-
3
- import { spawn } from 'child_process';
4
- import { EventEmitter } from 'events';
5
-
6
- const DEFAULT_TIMEOUT = 600_000; // 10 minutes
7
-
8
- /**
9
- * Runs claude CLI tasks and emits events on completion.
10
- */
11
- export class TaskRunner extends EventEmitter {
12
- constructor (logger, timeout, taskLogger) {
13
- super();
14
- this.logger = logger;
15
- this.timeout = timeout || DEFAULT_TIMEOUT;
16
- this.taskLogger = taskLogger || null;
17
- this.activeProcesses = new Map(); // workDir { child, timer, task }
18
- }
19
-
20
- /**
21
- * Run a task in a specific workDir.
22
- * @param {string} workDir - Working directory
23
- * @param {object} task - Task object { id, text, telegramMessageId, ... }
24
- * @returns {object} task with pid
25
- */
26
- run (workDir, task) {
27
- if (this.activeProcesses.has(workDir)) {
28
- throw new Error(`Already running a task in ${workDir}`);
29
- }
30
-
31
- this.logger.info(`Running task "${task.text}" in ${workDir}`);
32
- if (this.taskLogger) {
33
- this.taskLogger.logQuestion(task.project || 'unknown', task.branch || 'main', workDir, task.text);
34
- }
35
-
36
- const args = ['-p', task.text, '--output-format', 'text'];
37
- const child = spawn('claude', args, {
38
- cwd: workDir,
39
- stdio: ['ignore', 'pipe', 'pipe'],
40
- windowsHide: true,
41
- shell: process.platform === 'win32',
42
- env: { ...process.env, CLAUDE_NOTIFY_FROM_LISTENER: '1' },
43
- });
44
-
45
- let stdout = '';
46
- let stderr = '';
47
-
48
- child.stdout.on('data', (chunk) => {
49
- stdout += chunk.toString();
50
- });
51
-
52
- child.stderr.on('data', (chunk) => {
53
- stderr += chunk.toString();
54
- });
55
-
56
- const timer = setTimeout(() => {
57
- this.logger.warn(`Task "${task.id}" timed out in ${workDir}`);
58
- this._killProcess(workDir);
59
- this.emit('timeout', workDir, task);
60
- }, this.timeout);
61
-
62
- child.on('close', (code) => {
63
- clearTimeout(timer);
64
- this.activeProcesses.delete(workDir);
65
-
66
- if (code === null) {
67
- // Process was killed (timeout or cancel)
68
- return;
69
- }
70
-
71
- if (code === 0) {
72
- this.logger.info(`Task "${task.id}" completed in ${workDir}`);
73
- if (this.taskLogger) {
74
- this.taskLogger.logAnswer(task.project || 'unknown', task.branch || 'main', stdout.trim(), 0);
75
- }
76
- this.emit('complete', workDir, task, stdout.trim());
77
- } else {
78
- const errorMsg = stderr.trim() || `Process exited with code ${code}`;
79
- this.logger.error(`Task "${task.id}" failed in ${workDir}: ${errorMsg}`);
80
- if (this.taskLogger) {
81
- this.taskLogger.logAnswer(task.project || 'unknown', task.branch || 'main', errorMsg, code);
82
- }
83
- this.emit('error', workDir, task, errorMsg);
84
- }
85
- });
86
-
87
- child.on('error', (err) => {
88
- clearTimeout(timer);
89
- this.activeProcesses.delete(workDir);
90
- this.logger.error(`Task "${task.id}" spawn error: ${err.message}`);
91
- this.emit('error', workDir, task, err.message);
92
- });
93
-
94
- task.pid = child.pid;
95
- task.startedAt = new Date().toISOString();
96
- this.activeProcesses.set(workDir, { child, timer, task });
97
-
98
- return task;
99
- }
100
-
101
- /**
102
- * Cancel the active task in a workDir.
103
- */
104
- cancel (workDir) {
105
- this._killProcess(workDir);
106
- }
107
-
108
- /**
109
- * Check if a task is running in a workDir.
110
- */
111
- isRunning (workDir) {
112
- return this.activeProcesses.has(workDir);
113
- }
114
-
115
- /**
116
- * Get active task info for a workDir.
117
- */
118
- getActive (workDir) {
119
- const entry = this.activeProcesses.get(workDir);
120
- return entry?.task || null;
121
- }
122
-
123
- /**
124
- * Cancel all active tasks (for graceful shutdown).
125
- */
126
- cancelAll () {
127
- for (const workDir of this.activeProcesses.keys()) {
128
- this._killProcess(workDir);
129
- }
130
- }
131
-
132
- _killProcess (workDir) {
133
- const entry = this.activeProcesses.get(workDir);
134
- if (!entry) {
135
- return;
136
- }
137
- clearTimeout(entry.timer);
138
- try {
139
- if (process.platform === 'win32') {
140
- spawn('taskkill', ['/PID', String(entry.child.pid), '/T', '/F'], {
141
- stdio: 'ignore',
142
- windowsHide: true,
143
- });
144
- } else {
145
- entry.child.kill('SIGTERM');
146
- setTimeout(() => {
147
- try {
148
- entry.child.kill('SIGKILL');
149
- } catch {
150
- // already dead
151
- }
152
- }, 3000);
153
- }
154
- } catch {
155
- // process already dead
156
- }
157
- this.activeProcesses.delete(workDir);
158
- }
159
- }
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'child_process';
4
+ import { EventEmitter } from 'events';
5
+
6
+ const DEFAULT_TIMEOUT = 600_000; // 10 minutes
7
+
8
+ /**
9
+ * Parse JSON output from claude --output-format json.
10
+ * Returns structured result or fallback with raw text.
11
+ */
12
+ function parseClaudeOutput (raw) {
13
+ try {
14
+ const data = JSON.parse(raw);
15
+ const modelUsage = data.modelUsage || {};
16
+ const model = Object.keys(modelUsage)[0];
17
+ const mu = model ? modelUsage[model] : {};
18
+ const totalTokens = (mu.inputTokens || 0)
19
+ + (mu.cacheReadInputTokens || 0)
20
+ + (mu.cacheCreationInputTokens || 0)
21
+ + (mu.outputTokens || 0);
22
+ return {
23
+ text: data.result || '',
24
+ sessionId: data.session_id || null,
25
+ cost: data.total_cost_usd || 0,
26
+ numTurns: data.num_turns || 0,
27
+ durationMs: data.duration_ms || 0,
28
+ contextWindow: mu.contextWindow || 0,
29
+ totalTokens,
30
+ isError: !!data.is_error,
31
+ };
32
+ } catch {
33
+ return { text: raw.trim(), sessionId: null };
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Runs claude CLI tasks and emits events on completion.
39
+ */
40
+ export class TaskRunner extends EventEmitter {
41
+ constructor (logger, timeout, taskLogger) {
42
+ super();
43
+ this.logger = logger;
44
+ this.timeout = timeout || DEFAULT_TIMEOUT;
45
+ this.taskLogger = taskLogger || null;
46
+ this.activeProcesses = new Map(); // workDir -> { child, timer, task }
47
+ }
48
+
49
+ /**
50
+ * Run a task in a specific workDir.
51
+ * @param {string} workDir - Working directory
52
+ * @param {object} task - Task object { id, text, telegramMessageId, ... }
53
+ * @param {string[]} claudeArgs - Extra CLI args
54
+ * @param {boolean} continueSession - Add --continue flag
55
+ * @returns {object} task with pid
56
+ */
57
+ run (workDir, task, claudeArgs = [], continueSession = false) {
58
+ if (this.activeProcesses.has(workDir)) {
59
+ throw new Error(`Already running a task in ${workDir}`);
60
+ }
61
+
62
+ this.logger.info(`Running task "${task.text}" in ${workDir}${continueSession ? ' (continue session)' : ' (new session)'}`);
63
+ if (this.taskLogger) {
64
+ this.taskLogger.logQuestion(task.project || 'unknown', task.branch || 'main', workDir, task.text);
65
+ }
66
+
67
+ const args = ['-p', task.text, '--output-format', 'json', ...claudeArgs];
68
+ if (continueSession) {
69
+ args.push('--continue');
70
+ }
71
+ const child = spawn('claude', args, {
72
+ cwd: workDir,
73
+ stdio: ['ignore', 'pipe', 'pipe'],
74
+ windowsHide: true,
75
+ shell: process.platform === 'win32',
76
+ env: { ...process.env, CLAUDE_NOTIFY_FROM_LISTENER: '1' },
77
+ });
78
+
79
+ let stdout = '';
80
+ let stderr = '';
81
+
82
+ child.stdout.on('data', (chunk) => {
83
+ stdout += chunk.toString();
84
+ });
85
+
86
+ child.stderr.on('data', (chunk) => {
87
+ stderr += chunk.toString();
88
+ });
89
+
90
+ const timer = setTimeout(() => {
91
+ this.logger.warn(`Task "${task.id}" timed out in ${workDir}`);
92
+ this._killProcess(workDir);
93
+ this.emit('timeout', workDir, task);
94
+ }, this.timeout);
95
+
96
+ child.on('close', (code) => {
97
+ clearTimeout(timer);
98
+ this.activeProcesses.delete(workDir);
99
+
100
+ if (code === null) {
101
+ // Process was killed (timeout or cancel)
102
+ return;
103
+ }
104
+
105
+ if (code === 0) {
106
+ const result = parseClaudeOutput(stdout);
107
+ this.logger.info(`Task "${task.id}" completed in ${workDir} (session: ${result.sessionId || 'unknown'})`);
108
+ if (this.taskLogger) {
109
+ this.taskLogger.logAnswer(task.project || 'unknown', task.branch || 'main', result.text, 0);
110
+ }
111
+ this.emit('complete', workDir, task, result);
112
+ } else {
113
+ const errorMsg = stderr.trim() || `Process exited with code ${code}`;
114
+ this.logger.error(`Task "${task.id}" failed in ${workDir}: ${errorMsg}`);
115
+ if (this.taskLogger) {
116
+ this.taskLogger.logAnswer(task.project || 'unknown', task.branch || 'main', errorMsg, code);
117
+ }
118
+ this.emit('error', workDir, task, errorMsg);
119
+ }
120
+ });
121
+
122
+ child.on('error', (err) => {
123
+ clearTimeout(timer);
124
+ this.activeProcesses.delete(workDir);
125
+ this.logger.error(`Task "${task.id}" spawn error: ${err.message}`);
126
+ this.emit('error', workDir, task, err.message);
127
+ });
128
+
129
+ task.pid = child.pid;
130
+ task.startedAt = new Date().toISOString();
131
+ task.continueSession = continueSession;
132
+ this.activeProcesses.set(workDir, { child, timer, task });
133
+
134
+ return task;
135
+ }
136
+
137
+ /**
138
+ * Cancel the active task in a workDir.
139
+ */
140
+ cancel (workDir) {
141
+ this._killProcess(workDir);
142
+ }
143
+
144
+ /**
145
+ * Check if a task is running in a workDir.
146
+ */
147
+ isRunning (workDir) {
148
+ return this.activeProcesses.has(workDir);
149
+ }
150
+
151
+ /**
152
+ * Get active task info for a workDir.
153
+ */
154
+ getActive (workDir) {
155
+ const entry = this.activeProcesses.get(workDir);
156
+ return entry?.task || null;
157
+ }
158
+
159
+ /**
160
+ * Cancel all active tasks (for graceful shutdown).
161
+ */
162
+ cancelAll () {
163
+ for (const workDir of this.activeProcesses.keys()) {
164
+ this._killProcess(workDir);
165
+ }
166
+ }
167
+
168
+ _killProcess (workDir) {
169
+ const entry = this.activeProcesses.get(workDir);
170
+ if (!entry) {
171
+ return;
172
+ }
173
+ clearTimeout(entry.timer);
174
+ try {
175
+ if (process.platform === 'win32') {
176
+ spawn('taskkill', ['/PID', String(entry.child.pid), '/T', '/F'], {
177
+ stdio: 'ignore',
178
+ windowsHide: true,
179
+ });
180
+ } else {
181
+ entry.child.kill('SIGTERM');
182
+ setTimeout(() => {
183
+ try {
184
+ entry.child.kill('SIGKILL');
185
+ } catch {
186
+ // already dead
187
+ }
188
+ }, 3000);
189
+ }
190
+ } catch {
191
+ // process already dead
192
+ }
193
+ this.activeProcesses.delete(workDir);
194
+ }
195
+ }
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.1.20",
4
+ "version": "1.1.26",
5
5
  "description": "Claude Code task-completion notifications: Telegram, desktop notifications (Windows/macOS/Linux), sound, and voice",
6
6
  "type": "module",
7
7
  "engines": {