claude-threads 0.15.0 → 0.16.3
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/CHANGELOG.md +28 -0
- package/README.md +5 -5
- package/dist/index.js +20410 -387
- package/dist/mcp/permission-server.js +34038 -139
- package/package.json +14 -18
- package/dist/changelog.d.ts +0 -20
- package/dist/changelog.js +0 -134
- package/dist/claude/cli.d.ts +0 -50
- package/dist/claude/cli.js +0 -181
- package/dist/config/migration.d.ts +0 -45
- package/dist/config/migration.js +0 -35
- package/dist/config.d.ts +0 -21
- package/dist/config.js +0 -7
- package/dist/git/worktree.d.ts +0 -46
- package/dist/git/worktree.js +0 -228
- package/dist/index.d.ts +0 -2
- package/dist/logo.d.ts +0 -14
- package/dist/logo.js +0 -41
- package/dist/mattermost/api.d.ts +0 -85
- package/dist/mattermost/api.js +0 -124
- package/dist/mattermost/api.test.d.ts +0 -1
- package/dist/mattermost/api.test.js +0 -319
- package/dist/mcp/permission-server.d.ts +0 -2
- package/dist/onboarding.d.ts +0 -1
- package/dist/onboarding.js +0 -318
- package/dist/persistence/session-store.d.ts +0 -71
- package/dist/persistence/session-store.js +0 -152
- package/dist/platform/client.d.ts +0 -140
- package/dist/platform/client.js +0 -1
- package/dist/platform/formatter.d.ts +0 -74
- package/dist/platform/formatter.js +0 -1
- package/dist/platform/index.d.ts +0 -11
- package/dist/platform/index.js +0 -8
- package/dist/platform/mattermost/client.d.ts +0 -70
- package/dist/platform/mattermost/client.js +0 -404
- package/dist/platform/mattermost/formatter.d.ts +0 -20
- package/dist/platform/mattermost/formatter.js +0 -46
- package/dist/platform/mattermost/permission-api.d.ts +0 -10
- package/dist/platform/mattermost/permission-api.js +0 -139
- package/dist/platform/mattermost/types.d.ts +0 -71
- package/dist/platform/mattermost/types.js +0 -1
- package/dist/platform/permission-api-factory.d.ts +0 -11
- package/dist/platform/permission-api-factory.js +0 -21
- package/dist/platform/permission-api.d.ts +0 -67
- package/dist/platform/permission-api.js +0 -8
- package/dist/platform/types.d.ts +0 -70
- package/dist/platform/types.js +0 -7
- package/dist/session/commands.d.ts +0 -52
- package/dist/session/commands.js +0 -323
- package/dist/session/events.d.ts +0 -25
- package/dist/session/events.js +0 -368
- package/dist/session/index.d.ts +0 -7
- package/dist/session/index.js +0 -6
- package/dist/session/lifecycle.d.ts +0 -70
- package/dist/session/lifecycle.js +0 -456
- package/dist/session/manager.d.ts +0 -96
- package/dist/session/manager.js +0 -537
- package/dist/session/reactions.d.ts +0 -25
- package/dist/session/reactions.js +0 -151
- package/dist/session/streaming.d.ts +0 -47
- package/dist/session/streaming.js +0 -152
- package/dist/session/types.d.ts +0 -78
- package/dist/session/types.js +0 -9
- package/dist/session/worktree.d.ts +0 -56
- package/dist/session/worktree.js +0 -339
- package/dist/update-notifier.d.ts +0 -3
- package/dist/update-notifier.js +0 -41
- package/dist/utils/emoji.d.ts +0 -43
- package/dist/utils/emoji.js +0 -65
- package/dist/utils/emoji.test.d.ts +0 -1
- package/dist/utils/emoji.test.js +0 -131
- package/dist/utils/logger.d.ts +0 -34
- package/dist/utils/logger.js +0 -42
- package/dist/utils/logger.test.d.ts +0 -1
- package/dist/utils/logger.test.js +0 -121
- package/dist/utils/tool-formatter.d.ts +0 -53
- package/dist/utils/tool-formatter.js +0 -252
- package/dist/utils/tool-formatter.test.d.ts +0 -1
- package/dist/utils/tool-formatter.test.js +0 -372
|
@@ -1,404 +0,0 @@
|
|
|
1
|
-
import WebSocket from 'ws';
|
|
2
|
-
import { EventEmitter } from 'events';
|
|
3
|
-
import { wsLogger } from '../../utils/logger.js';
|
|
4
|
-
import { MattermostFormatter } from './formatter.js';
|
|
5
|
-
// Escape special regex characters to prevent regex injection
|
|
6
|
-
function escapeRegExp(string) {
|
|
7
|
-
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
-
}
|
|
9
|
-
export class MattermostClient extends EventEmitter {
|
|
10
|
-
// Platform identity (required by PlatformClient)
|
|
11
|
-
platformId;
|
|
12
|
-
platformType = 'mattermost';
|
|
13
|
-
displayName;
|
|
14
|
-
ws = null;
|
|
15
|
-
url;
|
|
16
|
-
token;
|
|
17
|
-
channelId;
|
|
18
|
-
botName;
|
|
19
|
-
allowedUsers;
|
|
20
|
-
reconnectAttempts = 0;
|
|
21
|
-
maxReconnectAttempts = 10;
|
|
22
|
-
reconnectDelay = 1000;
|
|
23
|
-
userCache = new Map();
|
|
24
|
-
botUserId = null;
|
|
25
|
-
formatter = new MattermostFormatter();
|
|
26
|
-
// Heartbeat to detect dead connections
|
|
27
|
-
pingInterval = null;
|
|
28
|
-
lastMessageAt = Date.now();
|
|
29
|
-
PING_INTERVAL_MS = 30000; // Send ping every 30s
|
|
30
|
-
PING_TIMEOUT_MS = 60000; // Reconnect if no message for 60s
|
|
31
|
-
constructor(platformConfig) {
|
|
32
|
-
super();
|
|
33
|
-
this.platformId = platformConfig.id;
|
|
34
|
-
this.displayName = platformConfig.displayName;
|
|
35
|
-
this.url = platformConfig.url;
|
|
36
|
-
this.token = platformConfig.token;
|
|
37
|
-
this.channelId = platformConfig.channelId;
|
|
38
|
-
this.botName = platformConfig.botName;
|
|
39
|
-
this.allowedUsers = platformConfig.allowedUsers;
|
|
40
|
-
}
|
|
41
|
-
// ============================================================================
|
|
42
|
-
// Type Normalization (Mattermost → Platform)
|
|
43
|
-
// ============================================================================
|
|
44
|
-
normalizePlatformUser(mattermostUser) {
|
|
45
|
-
return {
|
|
46
|
-
id: mattermostUser.id,
|
|
47
|
-
username: mattermostUser.username,
|
|
48
|
-
email: mattermostUser.email,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
normalizePlatformPost(mattermostPost) {
|
|
52
|
-
// Normalize metadata.files if present
|
|
53
|
-
const metadata = mattermostPost.metadata
|
|
54
|
-
? {
|
|
55
|
-
...mattermostPost.metadata,
|
|
56
|
-
files: mattermostPost.metadata.files?.map((f) => this.normalizePlatformFile(f)),
|
|
57
|
-
}
|
|
58
|
-
: undefined;
|
|
59
|
-
return {
|
|
60
|
-
id: mattermostPost.id,
|
|
61
|
-
platformId: this.platformId,
|
|
62
|
-
channelId: mattermostPost.channel_id,
|
|
63
|
-
userId: mattermostPost.user_id,
|
|
64
|
-
message: mattermostPost.message,
|
|
65
|
-
rootId: mattermostPost.root_id,
|
|
66
|
-
createAt: mattermostPost.create_at,
|
|
67
|
-
metadata,
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
normalizePlatformReaction(mattermostReaction) {
|
|
71
|
-
return {
|
|
72
|
-
userId: mattermostReaction.user_id,
|
|
73
|
-
postId: mattermostReaction.post_id,
|
|
74
|
-
emojiName: mattermostReaction.emoji_name,
|
|
75
|
-
createAt: mattermostReaction.create_at,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
normalizePlatformFile(mattermostFile) {
|
|
79
|
-
return {
|
|
80
|
-
id: mattermostFile.id,
|
|
81
|
-
name: mattermostFile.name,
|
|
82
|
-
size: mattermostFile.size,
|
|
83
|
-
mimeType: mattermostFile.mime_type,
|
|
84
|
-
extension: mattermostFile.extension,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
// REST API helper
|
|
88
|
-
async api(method, path, body) {
|
|
89
|
-
const url = `${this.url}/api/v4${path}`;
|
|
90
|
-
const response = await fetch(url, {
|
|
91
|
-
method,
|
|
92
|
-
headers: {
|
|
93
|
-
Authorization: `Bearer ${this.token}`,
|
|
94
|
-
'Content-Type': 'application/json',
|
|
95
|
-
},
|
|
96
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
97
|
-
});
|
|
98
|
-
if (!response.ok) {
|
|
99
|
-
const text = await response.text();
|
|
100
|
-
throw new Error(`Mattermost API error ${response.status}: ${text}`);
|
|
101
|
-
}
|
|
102
|
-
return response.json();
|
|
103
|
-
}
|
|
104
|
-
// Get current bot user info
|
|
105
|
-
async getBotUser() {
|
|
106
|
-
const user = await this.api('GET', '/users/me');
|
|
107
|
-
this.botUserId = user.id;
|
|
108
|
-
return this.normalizePlatformUser(user);
|
|
109
|
-
}
|
|
110
|
-
// Get user by ID (cached)
|
|
111
|
-
async getUser(userId) {
|
|
112
|
-
const cached = this.userCache.get(userId);
|
|
113
|
-
if (cached) {
|
|
114
|
-
return this.normalizePlatformUser(cached);
|
|
115
|
-
}
|
|
116
|
-
try {
|
|
117
|
-
const user = await this.api('GET', `/users/${userId}`);
|
|
118
|
-
this.userCache.set(userId, user);
|
|
119
|
-
return this.normalizePlatformUser(user);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
// Post a message
|
|
126
|
-
async createPost(message, threadId) {
|
|
127
|
-
const request = {
|
|
128
|
-
channel_id: this.channelId,
|
|
129
|
-
message,
|
|
130
|
-
root_id: threadId,
|
|
131
|
-
};
|
|
132
|
-
const post = await this.api('POST', '/posts', request);
|
|
133
|
-
return this.normalizePlatformPost(post);
|
|
134
|
-
}
|
|
135
|
-
// Update a message (for streaming updates)
|
|
136
|
-
async updatePost(postId, message) {
|
|
137
|
-
const request = {
|
|
138
|
-
id: postId,
|
|
139
|
-
message,
|
|
140
|
-
};
|
|
141
|
-
const post = await this.api('PUT', `/posts/${postId}`, request);
|
|
142
|
-
return this.normalizePlatformPost(post);
|
|
143
|
-
}
|
|
144
|
-
// Add a reaction to a post
|
|
145
|
-
async addReaction(postId, emojiName) {
|
|
146
|
-
await this.api('POST', '/reactions', {
|
|
147
|
-
user_id: this.botUserId,
|
|
148
|
-
post_id: postId,
|
|
149
|
-
emoji_name: emojiName,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Create a post with reaction options for user interaction
|
|
154
|
-
*
|
|
155
|
-
* This is a common pattern for interactive posts that need user response
|
|
156
|
-
* via reactions (e.g., approval prompts, questions, permission requests).
|
|
157
|
-
*
|
|
158
|
-
* @param message - Post message content
|
|
159
|
-
* @param reactions - Array of emoji names to add as reaction options
|
|
160
|
-
* @param threadId - Optional thread root ID
|
|
161
|
-
* @returns The created post
|
|
162
|
-
*/
|
|
163
|
-
async createInteractivePost(message, reactions, threadId) {
|
|
164
|
-
const post = await this.createPost(message, threadId);
|
|
165
|
-
// Add each reaction option, continuing even if some fail
|
|
166
|
-
for (const emoji of reactions) {
|
|
167
|
-
try {
|
|
168
|
-
await this.addReaction(post.id, emoji);
|
|
169
|
-
}
|
|
170
|
-
catch (err) {
|
|
171
|
-
console.error(` ⚠️ Failed to add reaction ${emoji}:`, err);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return post;
|
|
175
|
-
}
|
|
176
|
-
// Download a file attachment
|
|
177
|
-
async downloadFile(fileId) {
|
|
178
|
-
const url = `${this.url}/api/v4/files/${fileId}`;
|
|
179
|
-
const response = await fetch(url, {
|
|
180
|
-
headers: {
|
|
181
|
-
Authorization: `Bearer ${this.token}`,
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
if (!response.ok) {
|
|
185
|
-
throw new Error(`Failed to download file ${fileId}: ${response.status}`);
|
|
186
|
-
}
|
|
187
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
188
|
-
return Buffer.from(arrayBuffer);
|
|
189
|
-
}
|
|
190
|
-
// Get file info (metadata)
|
|
191
|
-
async getFileInfo(fileId) {
|
|
192
|
-
const file = await this.api('GET', `/files/${fileId}/info`);
|
|
193
|
-
return this.normalizePlatformFile(file);
|
|
194
|
-
}
|
|
195
|
-
// Get a post by ID (used to verify thread still exists on resume)
|
|
196
|
-
async getPost(postId) {
|
|
197
|
-
try {
|
|
198
|
-
const post = await this.api('GET', `/posts/${postId}`);
|
|
199
|
-
return this.normalizePlatformPost(post);
|
|
200
|
-
}
|
|
201
|
-
catch {
|
|
202
|
-
return null; // Post doesn't exist or was deleted
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
// Connect to WebSocket
|
|
206
|
-
async connect() {
|
|
207
|
-
// Get bot user first
|
|
208
|
-
await this.getBotUser();
|
|
209
|
-
wsLogger.debug(`Bot user ID: ${this.botUserId}`);
|
|
210
|
-
const wsUrl = this.url
|
|
211
|
-
.replace(/^http/, 'ws')
|
|
212
|
-
.concat('/api/v4/websocket');
|
|
213
|
-
return new Promise((resolve, reject) => {
|
|
214
|
-
this.ws = new WebSocket(wsUrl);
|
|
215
|
-
this.ws.on('open', () => {
|
|
216
|
-
wsLogger.debug('WebSocket connected');
|
|
217
|
-
// Authenticate
|
|
218
|
-
if (this.ws) {
|
|
219
|
-
this.ws.send(JSON.stringify({
|
|
220
|
-
seq: 1,
|
|
221
|
-
action: 'authentication_challenge',
|
|
222
|
-
data: { token: this.token },
|
|
223
|
-
}));
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
this.ws.on('message', (data) => {
|
|
227
|
-
this.lastMessageAt = Date.now(); // Track activity for heartbeat
|
|
228
|
-
try {
|
|
229
|
-
const event = JSON.parse(data.toString());
|
|
230
|
-
this.handleEvent(event);
|
|
231
|
-
// Authentication success
|
|
232
|
-
if (event.event === 'hello') {
|
|
233
|
-
this.reconnectAttempts = 0;
|
|
234
|
-
this.startHeartbeat();
|
|
235
|
-
this.emit('connected');
|
|
236
|
-
resolve();
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
catch (err) {
|
|
240
|
-
wsLogger.debug(`Failed to parse message: ${err}`);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
this.ws.on('close', () => {
|
|
244
|
-
wsLogger.debug('WebSocket disconnected');
|
|
245
|
-
this.stopHeartbeat();
|
|
246
|
-
this.emit('disconnected');
|
|
247
|
-
this.scheduleReconnect();
|
|
248
|
-
});
|
|
249
|
-
this.ws.on('error', (err) => {
|
|
250
|
-
wsLogger.debug(`WebSocket error: ${err}`);
|
|
251
|
-
this.emit('error', err);
|
|
252
|
-
reject(err);
|
|
253
|
-
});
|
|
254
|
-
this.ws.on('pong', () => {
|
|
255
|
-
this.lastMessageAt = Date.now(); // Pong received, connection is alive
|
|
256
|
-
wsLogger.debug('Pong received');
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
handleEvent(event) {
|
|
261
|
-
// Handle posted events
|
|
262
|
-
if (event.event === 'posted') {
|
|
263
|
-
const data = event.data;
|
|
264
|
-
if (!data.post)
|
|
265
|
-
return;
|
|
266
|
-
try {
|
|
267
|
-
const post = JSON.parse(data.post);
|
|
268
|
-
// Ignore messages from ourselves
|
|
269
|
-
if (post.user_id === this.botUserId)
|
|
270
|
-
return;
|
|
271
|
-
// Only handle messages in our channel
|
|
272
|
-
if (post.channel_id !== this.channelId)
|
|
273
|
-
return;
|
|
274
|
-
// Get user info and emit (with normalized types)
|
|
275
|
-
this.getUser(post.user_id).then((user) => {
|
|
276
|
-
this.emit('message', this.normalizePlatformPost(post), user);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
catch (err) {
|
|
280
|
-
wsLogger.debug(`Failed to parse post: ${err}`);
|
|
281
|
-
}
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
// Handle reaction_added events
|
|
285
|
-
if (event.event === 'reaction_added') {
|
|
286
|
-
const data = event.data;
|
|
287
|
-
if (!data.reaction)
|
|
288
|
-
return;
|
|
289
|
-
try {
|
|
290
|
-
const reaction = JSON.parse(data.reaction);
|
|
291
|
-
// Ignore reactions from ourselves
|
|
292
|
-
if (reaction.user_id === this.botUserId)
|
|
293
|
-
return;
|
|
294
|
-
// Get user info and emit (with normalized types)
|
|
295
|
-
this.getUser(reaction.user_id).then((user) => {
|
|
296
|
-
this.emit('reaction', this.normalizePlatformReaction(reaction), user);
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
catch (err) {
|
|
300
|
-
wsLogger.debug(`Failed to parse reaction: ${err}`);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
scheduleReconnect() {
|
|
305
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
306
|
-
console.error(' ⚠️ Max reconnection attempts reached');
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
this.reconnectAttempts++;
|
|
310
|
-
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
311
|
-
console.log(` 🔄 Reconnecting... (attempt ${this.reconnectAttempts})`);
|
|
312
|
-
setTimeout(() => {
|
|
313
|
-
this.connect().catch((err) => {
|
|
314
|
-
console.error(` ❌ Reconnection failed: ${err}`);
|
|
315
|
-
});
|
|
316
|
-
}, delay);
|
|
317
|
-
}
|
|
318
|
-
startHeartbeat() {
|
|
319
|
-
this.stopHeartbeat(); // Clear any existing
|
|
320
|
-
this.lastMessageAt = Date.now();
|
|
321
|
-
this.pingInterval = setInterval(() => {
|
|
322
|
-
const silentFor = Date.now() - this.lastMessageAt;
|
|
323
|
-
// If no message received for too long, connection is dead
|
|
324
|
-
if (silentFor > this.PING_TIMEOUT_MS) {
|
|
325
|
-
console.log(` 💔 Connection dead (no activity for ${Math.round(silentFor / 1000)}s), reconnecting...`);
|
|
326
|
-
this.stopHeartbeat();
|
|
327
|
-
if (this.ws) {
|
|
328
|
-
this.ws.terminate(); // Force close (triggers reconnect via 'close' event)
|
|
329
|
-
}
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
// Send ping to keep connection alive and verify it's working
|
|
333
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
334
|
-
this.ws.ping();
|
|
335
|
-
wsLogger.debug(`Ping sent (last activity ${Math.round(silentFor / 1000)}s ago)`);
|
|
336
|
-
}
|
|
337
|
-
}, this.PING_INTERVAL_MS);
|
|
338
|
-
}
|
|
339
|
-
stopHeartbeat() {
|
|
340
|
-
if (this.pingInterval) {
|
|
341
|
-
clearInterval(this.pingInterval);
|
|
342
|
-
this.pingInterval = null;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
// Check if user is allowed to use the bot
|
|
346
|
-
isUserAllowed(username) {
|
|
347
|
-
if (this.allowedUsers.length === 0) {
|
|
348
|
-
// If no allowlist configured, allow all
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
return this.allowedUsers.includes(username);
|
|
352
|
-
}
|
|
353
|
-
// Check if message mentions the bot
|
|
354
|
-
isBotMentioned(message) {
|
|
355
|
-
const botName = escapeRegExp(this.botName);
|
|
356
|
-
// Match @botname at start or with space before
|
|
357
|
-
const mentionPattern = new RegExp(`(^|\\s)@${botName}\\b`, 'i');
|
|
358
|
-
return mentionPattern.test(message);
|
|
359
|
-
}
|
|
360
|
-
// Extract prompt from message (remove bot mention)
|
|
361
|
-
extractPrompt(message) {
|
|
362
|
-
const botName = escapeRegExp(this.botName);
|
|
363
|
-
return message
|
|
364
|
-
.replace(new RegExp(`(^|\\s)@${botName}\\b`, 'gi'), ' ')
|
|
365
|
-
.trim();
|
|
366
|
-
}
|
|
367
|
-
// Get the bot name
|
|
368
|
-
getBotName() {
|
|
369
|
-
return this.botName;
|
|
370
|
-
}
|
|
371
|
-
// Get MCP config for permission server
|
|
372
|
-
getMcpConfig() {
|
|
373
|
-
return {
|
|
374
|
-
type: 'mattermost',
|
|
375
|
-
url: this.url,
|
|
376
|
-
token: this.token,
|
|
377
|
-
channelId: this.channelId,
|
|
378
|
-
allowedUsers: this.allowedUsers,
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
// Get platform-specific markdown formatter
|
|
382
|
-
getFormatter() {
|
|
383
|
-
return this.formatter;
|
|
384
|
-
}
|
|
385
|
-
// Send typing indicator via WebSocket
|
|
386
|
-
sendTyping(parentId) {
|
|
387
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN)
|
|
388
|
-
return;
|
|
389
|
-
this.ws.send(JSON.stringify({
|
|
390
|
-
action: 'user_typing',
|
|
391
|
-
seq: Date.now(),
|
|
392
|
-
data: {
|
|
393
|
-
channel_id: this.channelId,
|
|
394
|
-
parent_id: parentId || '',
|
|
395
|
-
},
|
|
396
|
-
}));
|
|
397
|
-
}
|
|
398
|
-
disconnect() {
|
|
399
|
-
if (this.ws) {
|
|
400
|
-
this.ws.close();
|
|
401
|
-
this.ws = null;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { PlatformFormatter } from '../formatter.js';
|
|
2
|
-
/**
|
|
3
|
-
* Mattermost markdown formatter
|
|
4
|
-
*
|
|
5
|
-
* Mattermost uses standard markdown syntax.
|
|
6
|
-
*/
|
|
7
|
-
export declare class MattermostFormatter implements PlatformFormatter {
|
|
8
|
-
formatBold(text: string): string;
|
|
9
|
-
formatItalic(text: string): string;
|
|
10
|
-
formatCode(text: string): string;
|
|
11
|
-
formatCodeBlock(code: string, language?: string): string;
|
|
12
|
-
formatUserMention(username: string): string;
|
|
13
|
-
formatLink(text: string, url: string): string;
|
|
14
|
-
formatListItem(text: string): string;
|
|
15
|
-
formatNumberedListItem(number: number, text: string): string;
|
|
16
|
-
formatBlockquote(text: string): string;
|
|
17
|
-
formatHorizontalRule(): string;
|
|
18
|
-
formatHeading(text: string, level: number): string;
|
|
19
|
-
escapeText(text: string): string;
|
|
20
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mattermost markdown formatter
|
|
3
|
-
*
|
|
4
|
-
* Mattermost uses standard markdown syntax.
|
|
5
|
-
*/
|
|
6
|
-
export class MattermostFormatter {
|
|
7
|
-
formatBold(text) {
|
|
8
|
-
return `**${text}**`;
|
|
9
|
-
}
|
|
10
|
-
formatItalic(text) {
|
|
11
|
-
return `_${text}_`;
|
|
12
|
-
}
|
|
13
|
-
formatCode(text) {
|
|
14
|
-
return `\`${text}\``;
|
|
15
|
-
}
|
|
16
|
-
formatCodeBlock(code, language) {
|
|
17
|
-
const lang = language || '';
|
|
18
|
-
return `\`\`\`${lang}\n${code}\n\`\`\``;
|
|
19
|
-
}
|
|
20
|
-
formatUserMention(username) {
|
|
21
|
-
return `@${username}`;
|
|
22
|
-
}
|
|
23
|
-
formatLink(text, url) {
|
|
24
|
-
return `[${text}](${url})`;
|
|
25
|
-
}
|
|
26
|
-
formatListItem(text) {
|
|
27
|
-
return `- ${text}`;
|
|
28
|
-
}
|
|
29
|
-
formatNumberedListItem(number, text) {
|
|
30
|
-
return `${number}. ${text}`;
|
|
31
|
-
}
|
|
32
|
-
formatBlockquote(text) {
|
|
33
|
-
return `> ${text}`;
|
|
34
|
-
}
|
|
35
|
-
formatHorizontalRule() {
|
|
36
|
-
return '---';
|
|
37
|
-
}
|
|
38
|
-
formatHeading(text, level) {
|
|
39
|
-
const hashes = '#'.repeat(Math.min(Math.max(level, 1), 6));
|
|
40
|
-
return `${hashes} ${text}`;
|
|
41
|
-
}
|
|
42
|
-
escapeText(text) {
|
|
43
|
-
// Escape markdown special characters
|
|
44
|
-
return text.replace(/([*_`[\]()#+\-.!])/g, '\\$1');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mattermost implementation of Permission API
|
|
3
|
-
*
|
|
4
|
-
* Handles permission requests via Mattermost API and WebSocket.
|
|
5
|
-
*/
|
|
6
|
-
import type { PermissionApi, PermissionApiConfig } from '../permission-api.js';
|
|
7
|
-
/**
|
|
8
|
-
* Create a Mattermost permission API instance
|
|
9
|
-
*/
|
|
10
|
-
export declare function createMattermostPermissionApi(config: PermissionApiConfig): PermissionApi;
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mattermost implementation of Permission API
|
|
3
|
-
*
|
|
4
|
-
* Handles permission requests via Mattermost API and WebSocket.
|
|
5
|
-
*/
|
|
6
|
-
import WebSocket from 'ws';
|
|
7
|
-
import { MattermostFormatter } from './formatter.js';
|
|
8
|
-
import { getMe, getUser, createInteractivePost, updatePost, isUserAllowed, } from '../../mattermost/api.js';
|
|
9
|
-
import { mcpLogger } from '../../utils/logger.js';
|
|
10
|
-
/**
|
|
11
|
-
* Mattermost Permission API implementation
|
|
12
|
-
*/
|
|
13
|
-
class MattermostPermissionApi {
|
|
14
|
-
apiConfig;
|
|
15
|
-
config;
|
|
16
|
-
formatter = new MattermostFormatter();
|
|
17
|
-
botUserIdCache = null;
|
|
18
|
-
constructor(config) {
|
|
19
|
-
this.config = config;
|
|
20
|
-
this.apiConfig = {
|
|
21
|
-
url: config.url,
|
|
22
|
-
token: config.token,
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
getFormatter() {
|
|
26
|
-
return this.formatter;
|
|
27
|
-
}
|
|
28
|
-
async getBotUserId() {
|
|
29
|
-
if (this.botUserIdCache)
|
|
30
|
-
return this.botUserIdCache;
|
|
31
|
-
const me = await getMe(this.apiConfig);
|
|
32
|
-
this.botUserIdCache = me.id;
|
|
33
|
-
return me.id;
|
|
34
|
-
}
|
|
35
|
-
async getUsername(userId) {
|
|
36
|
-
try {
|
|
37
|
-
const user = await getUser(this.apiConfig, userId);
|
|
38
|
-
return user?.username ?? null;
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
isUserAllowed(username) {
|
|
45
|
-
return isUserAllowed(username, this.config.allowedUsers);
|
|
46
|
-
}
|
|
47
|
-
async createInteractivePost(message, reactions, threadId) {
|
|
48
|
-
const botUserId = await this.getBotUserId();
|
|
49
|
-
const post = await createInteractivePost(this.apiConfig, this.config.channelId, message, reactions, threadId, botUserId);
|
|
50
|
-
return { id: post.id };
|
|
51
|
-
}
|
|
52
|
-
async updatePost(postId, message) {
|
|
53
|
-
await updatePost(this.apiConfig, postId, message);
|
|
54
|
-
}
|
|
55
|
-
async waitForReaction(postId, botUserId, timeoutMs) {
|
|
56
|
-
return new Promise((resolve) => {
|
|
57
|
-
// Parse WebSocket URL from HTTP URL
|
|
58
|
-
const wsUrl = this.config.url.replace(/^http/, 'ws') + '/api/v4/websocket';
|
|
59
|
-
mcpLogger.debug(`Connecting to WebSocket: ${wsUrl}`);
|
|
60
|
-
const ws = new WebSocket(wsUrl);
|
|
61
|
-
let resolved = false;
|
|
62
|
-
const cleanup = () => {
|
|
63
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
64
|
-
ws.close();
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
const timeout = setTimeout(() => {
|
|
68
|
-
if (!resolved) {
|
|
69
|
-
resolved = true;
|
|
70
|
-
cleanup();
|
|
71
|
-
resolve(null);
|
|
72
|
-
}
|
|
73
|
-
}, timeoutMs);
|
|
74
|
-
ws.on('open', () => {
|
|
75
|
-
mcpLogger.debug('WebSocket connected, sending auth...');
|
|
76
|
-
ws.send(JSON.stringify({
|
|
77
|
-
seq: 1,
|
|
78
|
-
action: 'authentication_challenge',
|
|
79
|
-
data: { token: this.config.token },
|
|
80
|
-
}));
|
|
81
|
-
});
|
|
82
|
-
ws.on('message', async (data) => {
|
|
83
|
-
if (resolved)
|
|
84
|
-
return;
|
|
85
|
-
try {
|
|
86
|
-
const event = JSON.parse(data.toString());
|
|
87
|
-
mcpLogger.debug(`WebSocket event: ${event.event}`);
|
|
88
|
-
if (event.event === 'reaction_added') {
|
|
89
|
-
// Mattermost sends reaction as JSON string
|
|
90
|
-
const reaction = typeof event.data.reaction === 'string'
|
|
91
|
-
? JSON.parse(event.data.reaction)
|
|
92
|
-
: event.data.reaction;
|
|
93
|
-
// Must be on our post
|
|
94
|
-
if (reaction.post_id !== postId)
|
|
95
|
-
return;
|
|
96
|
-
// Must not be the bot's own reaction (adding the options)
|
|
97
|
-
if (reaction.user_id === botUserId)
|
|
98
|
-
return;
|
|
99
|
-
mcpLogger.debug(`Reaction received: ${reaction.emoji_name} from user: ${reaction.user_id}`);
|
|
100
|
-
// Got a valid reaction
|
|
101
|
-
resolved = true;
|
|
102
|
-
clearTimeout(timeout);
|
|
103
|
-
cleanup();
|
|
104
|
-
resolve({
|
|
105
|
-
postId: reaction.post_id,
|
|
106
|
-
userId: reaction.user_id,
|
|
107
|
-
emojiName: reaction.emoji_name,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch (err) {
|
|
112
|
-
mcpLogger.debug(`Error parsing WebSocket message: ${err}`);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
ws.on('error', (error) => {
|
|
116
|
-
mcpLogger.error(`WebSocket error: ${error.message}`);
|
|
117
|
-
if (!resolved) {
|
|
118
|
-
resolved = true;
|
|
119
|
-
clearTimeout(timeout);
|
|
120
|
-
resolve(null);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
ws.on('close', () => {
|
|
124
|
-
mcpLogger.debug('WebSocket closed');
|
|
125
|
-
if (!resolved) {
|
|
126
|
-
resolved = true;
|
|
127
|
-
clearTimeout(timeout);
|
|
128
|
-
resolve(null);
|
|
129
|
-
}
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Create a Mattermost permission API instance
|
|
136
|
-
*/
|
|
137
|
-
export function createMattermostPermissionApi(config) {
|
|
138
|
-
return new MattermostPermissionApi(config);
|
|
139
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
export interface MattermostWebSocketEvent {
|
|
2
|
-
event: string;
|
|
3
|
-
data: Record<string, unknown>;
|
|
4
|
-
broadcast: {
|
|
5
|
-
channel_id?: string;
|
|
6
|
-
user_id?: string;
|
|
7
|
-
team_id?: string;
|
|
8
|
-
};
|
|
9
|
-
seq: number;
|
|
10
|
-
}
|
|
11
|
-
export interface MattermostFile {
|
|
12
|
-
id: string;
|
|
13
|
-
name: string;
|
|
14
|
-
size: number;
|
|
15
|
-
mime_type: string;
|
|
16
|
-
extension: string;
|
|
17
|
-
width?: number;
|
|
18
|
-
height?: number;
|
|
19
|
-
}
|
|
20
|
-
export interface MattermostPost {
|
|
21
|
-
id: string;
|
|
22
|
-
create_at: number;
|
|
23
|
-
update_at: number;
|
|
24
|
-
delete_at: number;
|
|
25
|
-
user_id: string;
|
|
26
|
-
channel_id: string;
|
|
27
|
-
root_id: string;
|
|
28
|
-
message: string;
|
|
29
|
-
type: string;
|
|
30
|
-
props: Record<string, unknown>;
|
|
31
|
-
metadata?: {
|
|
32
|
-
embeds?: unknown[];
|
|
33
|
-
files?: MattermostFile[];
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
export interface MattermostUser {
|
|
37
|
-
id: string;
|
|
38
|
-
username: string;
|
|
39
|
-
email: string;
|
|
40
|
-
first_name: string;
|
|
41
|
-
last_name: string;
|
|
42
|
-
nickname: string;
|
|
43
|
-
}
|
|
44
|
-
export interface PostedEventData {
|
|
45
|
-
channel_display_name: string;
|
|
46
|
-
channel_name: string;
|
|
47
|
-
channel_type: string;
|
|
48
|
-
post: string;
|
|
49
|
-
sender_name: string;
|
|
50
|
-
team_id: string;
|
|
51
|
-
}
|
|
52
|
-
export interface ReactionAddedEventData {
|
|
53
|
-
reaction: string;
|
|
54
|
-
}
|
|
55
|
-
export interface MattermostReaction {
|
|
56
|
-
user_id: string;
|
|
57
|
-
post_id: string;
|
|
58
|
-
emoji_name: string;
|
|
59
|
-
create_at: number;
|
|
60
|
-
}
|
|
61
|
-
export interface CreatePostRequest {
|
|
62
|
-
channel_id: string;
|
|
63
|
-
message: string;
|
|
64
|
-
root_id?: string;
|
|
65
|
-
props?: Record<string, unknown>;
|
|
66
|
-
}
|
|
67
|
-
export interface UpdatePostRequest {
|
|
68
|
-
id: string;
|
|
69
|
-
message: string;
|
|
70
|
-
props?: Record<string, unknown>;
|
|
71
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|