mattermost-claude-code 0.3.2 → 0.3.4
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/dist/claude/session.d.ts +2 -0
- package/dist/claude/session.js +16 -0
- package/dist/index.js +7 -0
- package/dist/mattermost/client.d.ts +6 -0
- package/dist/mattermost/client.js +39 -0
- package/package.json +1 -1
package/dist/claude/session.d.ts
CHANGED
|
@@ -87,6 +87,8 @@ export declare class SessionManager {
|
|
|
87
87
|
sendFollowUp(threadId: string, message: string): Promise<void>;
|
|
88
88
|
/** Kill a specific session */
|
|
89
89
|
killSession(threadId: string): void;
|
|
90
|
+
/** Cancel a session with user feedback */
|
|
91
|
+
cancelSession(threadId: string, username: string): Promise<void>;
|
|
90
92
|
/** Kill all active sessions (for graceful shutdown) */
|
|
91
93
|
killAllSessions(): void;
|
|
92
94
|
/** Cleanup idle sessions that have exceeded timeout */
|
package/dist/claude/session.js
CHANGED
|
@@ -129,6 +129,7 @@ export class SessionManager {
|
|
|
129
129
|
};
|
|
130
130
|
// Register session
|
|
131
131
|
this.sessions.set(actualThreadId, session);
|
|
132
|
+
this.registerPost(post.id, actualThreadId); // For cancel reactions on session start post
|
|
132
133
|
const shortId = actualThreadId.substring(0, 8);
|
|
133
134
|
console.log(` ▶ Session #${this.sessions.size} started (${shortId}…) by @${username}`);
|
|
134
135
|
// Start typing indicator immediately so user sees activity
|
|
@@ -388,6 +389,11 @@ export class SessionManager {
|
|
|
388
389
|
const session = this.getSessionByPost(postId);
|
|
389
390
|
if (!session)
|
|
390
391
|
return;
|
|
392
|
+
// Handle cancel reactions (❌ or 🛑) on any post in the session
|
|
393
|
+
if (emojiName === 'x' || emojiName === 'octagonal_sign' || emojiName === 'stop_sign') {
|
|
394
|
+
await this.cancelSession(session.threadId, username);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
391
397
|
// Handle approval reactions
|
|
392
398
|
if (session.pendingApproval && session.pendingApproval.postId === postId) {
|
|
393
399
|
await this.handleApprovalReaction(session, emojiName, username);
|
|
@@ -708,6 +714,16 @@ export class SessionManager {
|
|
|
708
714
|
const shortId = threadId.substring(0, 8);
|
|
709
715
|
console.log(` ✖ Session killed (${shortId}…) — ${this.sessions.size} active`);
|
|
710
716
|
}
|
|
717
|
+
/** Cancel a session with user feedback */
|
|
718
|
+
async cancelSession(threadId, username) {
|
|
719
|
+
const session = this.sessions.get(threadId);
|
|
720
|
+
if (!session)
|
|
721
|
+
return;
|
|
722
|
+
const shortId = threadId.substring(0, 8);
|
|
723
|
+
console.log(` 🛑 Session (${shortId}…) cancelled by @${username}`);
|
|
724
|
+
await this.mattermost.createPost(`🛑 **Session cancelled** by @${username}`, threadId);
|
|
725
|
+
this.killSession(threadId);
|
|
726
|
+
}
|
|
711
727
|
/** Kill all active sessions (for graceful shutdown) */
|
|
712
728
|
killAllSessions() {
|
|
713
729
|
const count = this.sessions.size;
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,13 @@ Usage: cd /your/project && mm-claude`);
|
|
|
50
50
|
const content = mattermost.isBotMentioned(message)
|
|
51
51
|
? mattermost.extractPrompt(message)
|
|
52
52
|
: message.trim();
|
|
53
|
+
// Check for stop/cancel commands
|
|
54
|
+
const lowerContent = content.toLowerCase();
|
|
55
|
+
if (lowerContent === '/stop' || lowerContent === 'stop' ||
|
|
56
|
+
lowerContent === '/cancel' || lowerContent === 'cancel') {
|
|
57
|
+
await session.cancelSession(threadRoot, username);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
53
60
|
if (content)
|
|
54
61
|
await session.sendFollowUp(threadRoot, content);
|
|
55
62
|
return;
|
|
@@ -17,6 +17,10 @@ export declare class MattermostClient extends EventEmitter {
|
|
|
17
17
|
private userCache;
|
|
18
18
|
private botUserId;
|
|
19
19
|
private debug;
|
|
20
|
+
private pingInterval;
|
|
21
|
+
private lastMessageAt;
|
|
22
|
+
private readonly PING_INTERVAL_MS;
|
|
23
|
+
private readonly PING_TIMEOUT_MS;
|
|
20
24
|
constructor(config: Config);
|
|
21
25
|
private log;
|
|
22
26
|
private api;
|
|
@@ -28,6 +32,8 @@ export declare class MattermostClient extends EventEmitter {
|
|
|
28
32
|
connect(): Promise<void>;
|
|
29
33
|
private handleEvent;
|
|
30
34
|
private scheduleReconnect;
|
|
35
|
+
private startHeartbeat;
|
|
36
|
+
private stopHeartbeat;
|
|
31
37
|
isUserAllowed(username: string): boolean;
|
|
32
38
|
isBotMentioned(message: string): boolean;
|
|
33
39
|
extractPrompt(message: string): string;
|
|
@@ -9,6 +9,11 @@ export class MattermostClient extends EventEmitter {
|
|
|
9
9
|
userCache = new Map();
|
|
10
10
|
botUserId = null;
|
|
11
11
|
debug = process.env.DEBUG === '1' || process.argv.includes('--debug');
|
|
12
|
+
// Heartbeat to detect dead connections
|
|
13
|
+
pingInterval = null;
|
|
14
|
+
lastMessageAt = Date.now();
|
|
15
|
+
PING_INTERVAL_MS = 30000; // Send ping every 30s
|
|
16
|
+
PING_TIMEOUT_MS = 60000; // Reconnect if no message for 60s
|
|
12
17
|
constructor(config) {
|
|
13
18
|
super();
|
|
14
19
|
this.config = config;
|
|
@@ -99,12 +104,14 @@ export class MattermostClient extends EventEmitter {
|
|
|
99
104
|
}));
|
|
100
105
|
});
|
|
101
106
|
this.ws.on('message', (data) => {
|
|
107
|
+
this.lastMessageAt = Date.now(); // Track activity for heartbeat
|
|
102
108
|
try {
|
|
103
109
|
const event = JSON.parse(data.toString());
|
|
104
110
|
this.handleEvent(event);
|
|
105
111
|
// Authentication success
|
|
106
112
|
if (event.event === 'hello') {
|
|
107
113
|
this.reconnectAttempts = 0;
|
|
114
|
+
this.startHeartbeat();
|
|
108
115
|
this.emit('connected');
|
|
109
116
|
resolve();
|
|
110
117
|
}
|
|
@@ -115,6 +122,7 @@ export class MattermostClient extends EventEmitter {
|
|
|
115
122
|
});
|
|
116
123
|
this.ws.on('close', () => {
|
|
117
124
|
this.log('WebSocket disconnected');
|
|
125
|
+
this.stopHeartbeat();
|
|
118
126
|
this.emit('disconnected');
|
|
119
127
|
this.scheduleReconnect();
|
|
120
128
|
});
|
|
@@ -123,6 +131,10 @@ export class MattermostClient extends EventEmitter {
|
|
|
123
131
|
this.emit('error', err);
|
|
124
132
|
reject(err);
|
|
125
133
|
});
|
|
134
|
+
this.ws.on('pong', () => {
|
|
135
|
+
this.lastMessageAt = Date.now(); // Pong received, connection is alive
|
|
136
|
+
this.log('Pong received');
|
|
137
|
+
});
|
|
126
138
|
});
|
|
127
139
|
}
|
|
128
140
|
handleEvent(event) {
|
|
@@ -183,6 +195,33 @@ export class MattermostClient extends EventEmitter {
|
|
|
183
195
|
});
|
|
184
196
|
}, delay);
|
|
185
197
|
}
|
|
198
|
+
startHeartbeat() {
|
|
199
|
+
this.stopHeartbeat(); // Clear any existing
|
|
200
|
+
this.lastMessageAt = Date.now();
|
|
201
|
+
this.pingInterval = setInterval(() => {
|
|
202
|
+
const silentFor = Date.now() - this.lastMessageAt;
|
|
203
|
+
// If no message received for too long, connection is dead
|
|
204
|
+
if (silentFor > this.PING_TIMEOUT_MS) {
|
|
205
|
+
console.log(` 💔 Connection dead (no activity for ${Math.round(silentFor / 1000)}s), reconnecting...`);
|
|
206
|
+
this.stopHeartbeat();
|
|
207
|
+
if (this.ws) {
|
|
208
|
+
this.ws.terminate(); // Force close (triggers reconnect via 'close' event)
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
// Send ping to keep connection alive and verify it's working
|
|
213
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
214
|
+
this.ws.ping();
|
|
215
|
+
this.log(`Ping sent (last activity ${Math.round(silentFor / 1000)}s ago)`);
|
|
216
|
+
}
|
|
217
|
+
}, this.PING_INTERVAL_MS);
|
|
218
|
+
}
|
|
219
|
+
stopHeartbeat() {
|
|
220
|
+
if (this.pingInterval) {
|
|
221
|
+
clearInterval(this.pingInterval);
|
|
222
|
+
this.pingInterval = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
186
225
|
// Check if user is allowed to use the bot
|
|
187
226
|
isUserAllowed(username) {
|
|
188
227
|
if (this.config.allowedUsers.length === 0) {
|