claude-threads 0.13.0 → 0.14.1

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +78 -28
  3. package/dist/claude/cli.d.ts +8 -0
  4. package/dist/claude/cli.js +16 -8
  5. package/dist/config/migration.d.ts +45 -0
  6. package/dist/config/migration.js +35 -0
  7. package/dist/config.d.ts +12 -18
  8. package/dist/config.js +7 -94
  9. package/dist/git/worktree.d.ts +0 -4
  10. package/dist/git/worktree.js +1 -1
  11. package/dist/index.js +31 -13
  12. package/dist/logo.d.ts +3 -20
  13. package/dist/logo.js +7 -23
  14. package/dist/mcp/permission-server.js +61 -112
  15. package/dist/onboarding.js +262 -137
  16. package/dist/persistence/session-store.d.ts +8 -2
  17. package/dist/persistence/session-store.js +41 -16
  18. package/dist/platform/client.d.ts +140 -0
  19. package/dist/platform/formatter.d.ts +74 -0
  20. package/dist/platform/index.d.ts +11 -0
  21. package/dist/platform/index.js +8 -0
  22. package/dist/platform/mattermost/client.d.ts +70 -0
  23. package/dist/{mattermost → platform/mattermost}/client.js +117 -34
  24. package/dist/platform/mattermost/formatter.d.ts +20 -0
  25. package/dist/platform/mattermost/formatter.js +46 -0
  26. package/dist/platform/mattermost/permission-api.d.ts +10 -0
  27. package/dist/platform/mattermost/permission-api.js +139 -0
  28. package/dist/platform/mattermost/types.js +1 -0
  29. package/dist/platform/permission-api-factory.d.ts +11 -0
  30. package/dist/platform/permission-api-factory.js +21 -0
  31. package/dist/platform/permission-api.d.ts +67 -0
  32. package/dist/platform/permission-api.js +8 -0
  33. package/dist/platform/types.d.ts +70 -0
  34. package/dist/platform/types.js +7 -0
  35. package/dist/session/commands.d.ts +52 -0
  36. package/dist/session/commands.js +323 -0
  37. package/dist/session/events.d.ts +25 -0
  38. package/dist/session/events.js +368 -0
  39. package/dist/session/index.d.ts +7 -0
  40. package/dist/session/index.js +6 -0
  41. package/dist/session/lifecycle.d.ts +70 -0
  42. package/dist/session/lifecycle.js +456 -0
  43. package/dist/session/manager.d.ts +96 -0
  44. package/dist/session/manager.js +537 -0
  45. package/dist/session/reactions.d.ts +25 -0
  46. package/dist/session/reactions.js +151 -0
  47. package/dist/session/streaming.d.ts +47 -0
  48. package/dist/session/streaming.js +152 -0
  49. package/dist/session/types.d.ts +78 -0
  50. package/dist/session/types.js +9 -0
  51. package/dist/session/worktree.d.ts +56 -0
  52. package/dist/session/worktree.js +339 -0
  53. package/dist/update-notifier.js +10 -0
  54. package/dist/{mattermost → utils}/emoji.d.ts +3 -3
  55. package/dist/{mattermost → utils}/emoji.js +3 -3
  56. package/dist/utils/emoji.test.d.ts +1 -0
  57. package/dist/utils/tool-formatter.d.ts +10 -13
  58. package/dist/utils/tool-formatter.js +48 -43
  59. package/dist/utils/tool-formatter.test.js +67 -52
  60. package/package.json +4 -3
  61. package/dist/claude/session.d.ts +0 -256
  62. package/dist/claude/session.js +0 -1964
  63. package/dist/mattermost/client.d.ts +0 -56
  64. /package/dist/{mattermost/emoji.test.d.ts → platform/client.js} +0 -0
  65. /package/dist/{mattermost/types.js → platform/formatter.js} +0 -0
  66. /package/dist/{mattermost → platform/mattermost}/types.d.ts +0 -0
  67. /package/dist/{mattermost → utils}/emoji.test.js +0 -0
@@ -0,0 +1,140 @@
1
+ import { EventEmitter } from 'events';
2
+ import type { PlatformUser, PlatformPost, PlatformReaction, PlatformFile } from './types.js';
3
+ import type { PlatformFormatter } from './formatter.js';
4
+ /**
5
+ * Events emitted by PlatformClient
6
+ */
7
+ export interface PlatformClientEvents {
8
+ connected: () => void;
9
+ disconnected: () => void;
10
+ error: (error: Error) => void;
11
+ message: (post: PlatformPost, user: PlatformUser | null) => void;
12
+ reaction: (reaction: PlatformReaction, user: PlatformUser | null) => void;
13
+ }
14
+ /**
15
+ * Platform-agnostic client interface
16
+ *
17
+ * All platform implementations (Mattermost, Slack) must implement this interface.
18
+ * This allows SessionManager and other code to work with any platform without
19
+ * knowing the specific implementation details.
20
+ */
21
+ export interface PlatformClient extends EventEmitter {
22
+ /**
23
+ * Unique identifier for this platform instance
24
+ * e.g., 'mattermost-internal', 'slack-eng'
25
+ */
26
+ readonly platformId: string;
27
+ /**
28
+ * Platform type
29
+ * e.g., 'mattermost', 'slack'
30
+ */
31
+ readonly platformType: string;
32
+ /**
33
+ * Human-readable display name
34
+ * e.g., 'Internal Team', 'Engineering Slack'
35
+ */
36
+ readonly displayName: string;
37
+ /**
38
+ * Connect to the platform (WebSocket, Socket Mode, etc.)
39
+ */
40
+ connect(): Promise<void>;
41
+ /**
42
+ * Disconnect from the platform
43
+ */
44
+ disconnect(): void;
45
+ /**
46
+ * Get the bot's own user info
47
+ */
48
+ getBotUser(): Promise<PlatformUser>;
49
+ /**
50
+ * Get a user by their ID
51
+ */
52
+ getUser(userId: string): Promise<PlatformUser | null>;
53
+ /**
54
+ * Check if a username is in the allowed users list
55
+ */
56
+ isUserAllowed(username: string): boolean;
57
+ /**
58
+ * Get the bot's mention name (e.g., 'claude-code')
59
+ */
60
+ getBotName(): string;
61
+ /**
62
+ * Get platform config for MCP permission server
63
+ */
64
+ getMcpConfig(): {
65
+ type: string;
66
+ url: string;
67
+ token: string;
68
+ channelId: string;
69
+ allowedUsers: string[];
70
+ };
71
+ /**
72
+ * Get the platform-specific markdown formatter
73
+ * Use this to format bold, code, etc. in a platform-appropriate way.
74
+ */
75
+ getFormatter(): PlatformFormatter;
76
+ /**
77
+ * Create a new post/message
78
+ * @param message - Message text
79
+ * @param threadId - Optional thread parent ID
80
+ * @returns The created post
81
+ */
82
+ createPost(message: string, threadId?: string): Promise<PlatformPost>;
83
+ /**
84
+ * Update an existing post/message
85
+ * @param postId - Post ID to update
86
+ * @param message - New message text
87
+ * @returns The updated post
88
+ */
89
+ updatePost(postId: string, message: string): Promise<PlatformPost>;
90
+ /**
91
+ * Create a post with reaction options (for interactive prompts)
92
+ * @param message - Message text
93
+ * @param reactions - Array of emoji names to add as options
94
+ * @param threadId - Optional thread parent ID
95
+ * @returns The created post
96
+ */
97
+ createInteractivePost(message: string, reactions: string[], threadId?: string): Promise<PlatformPost>;
98
+ /**
99
+ * Get a post by ID
100
+ * @param postId - Post ID
101
+ * @returns The post, or null if not found/deleted
102
+ */
103
+ getPost(postId: string): Promise<PlatformPost | null>;
104
+ /**
105
+ * Add a reaction to a post
106
+ * @param postId - Post ID
107
+ * @param emojiName - Emoji name (e.g., '+1', 'white_check_mark')
108
+ */
109
+ addReaction(postId: string, emojiName: string): Promise<void>;
110
+ /**
111
+ * Check if a message mentions the bot
112
+ * @param message - Message text
113
+ */
114
+ isBotMentioned(message: string): boolean;
115
+ /**
116
+ * Extract the prompt from a message (remove bot mention)
117
+ * @param message - Message text
118
+ * @returns The message with bot mention removed
119
+ */
120
+ extractPrompt(message: string): string;
121
+ /**
122
+ * Send typing indicator to show bot is "thinking"
123
+ * @param threadId - Optional thread ID
124
+ */
125
+ sendTyping(threadId?: string): void;
126
+ /**
127
+ * Download a file attachment
128
+ * @param fileId - File ID
129
+ * @returns File contents as Buffer
130
+ */
131
+ downloadFile?(fileId: string): Promise<Buffer>;
132
+ /**
133
+ * Get file metadata
134
+ * @param fileId - File ID
135
+ * @returns File metadata
136
+ */
137
+ getFileInfo?(fileId: string): Promise<PlatformFile>;
138
+ on<K extends keyof PlatformClientEvents>(event: K, listener: PlatformClientEvents[K]): this;
139
+ emit<K extends keyof PlatformClientEvents>(event: K, ...args: Parameters<PlatformClientEvents[K]>): boolean;
140
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Platform-agnostic markdown formatter interface
3
+ *
4
+ * Different platforms have slightly different markdown dialects:
5
+ * - Mattermost: Standard markdown (**bold**, _italic_, @username)
6
+ * - Slack: mrkdwn (*bold*, _italic_, <@U123> for mentions)
7
+ *
8
+ * This interface abstracts those differences.
9
+ */
10
+ export interface PlatformFormatter {
11
+ /**
12
+ * Format text as bold
13
+ * Mattermost: **text**
14
+ * Slack: *text*
15
+ */
16
+ formatBold(text: string): string;
17
+ /**
18
+ * Format text as italic
19
+ * Mattermost: _text_ or *text*
20
+ * Slack: _text_
21
+ */
22
+ formatItalic(text: string): string;
23
+ /**
24
+ * Format text as inline code
25
+ * Both: `code`
26
+ */
27
+ formatCode(text: string): string;
28
+ /**
29
+ * Format text as code block with optional language
30
+ * Both: ```lang\ncode\n```
31
+ */
32
+ formatCodeBlock(code: string, language?: string): string;
33
+ /**
34
+ * Format a user mention
35
+ * Mattermost: @username
36
+ * Slack: <@U123456> (requires user ID)
37
+ */
38
+ formatUserMention(username: string, userId?: string): string;
39
+ /**
40
+ * Format a hyperlink
41
+ * Mattermost: [text](url)
42
+ * Slack: <url|text>
43
+ */
44
+ formatLink(text: string, url: string): string;
45
+ /**
46
+ * Format a bulleted list item
47
+ * Both: - item or * item
48
+ */
49
+ formatListItem(text: string): string;
50
+ /**
51
+ * Format a numbered list item
52
+ * Both: 1. item
53
+ */
54
+ formatNumberedListItem(number: number, text: string): string;
55
+ /**
56
+ * Format a blockquote
57
+ * Both: > text
58
+ */
59
+ formatBlockquote(text: string): string;
60
+ /**
61
+ * Format a horizontal rule
62
+ * Both: ---
63
+ */
64
+ formatHorizontalRule(): string;
65
+ /**
66
+ * Format a heading
67
+ * Both: # Heading (level 1), ## Heading (level 2), etc.
68
+ */
69
+ formatHeading(text: string, level: number): string;
70
+ /**
71
+ * Escape special characters in text to prevent formatting
72
+ */
73
+ escapeText(text: string): string;
74
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Platform abstraction layer
3
+ *
4
+ * This module provides platform-agnostic interfaces and types that allow
5
+ * claude-threads to work with multiple chat platforms (Mattermost, Slack, etc.)
6
+ * without coupling the core logic to any specific platform.
7
+ */
8
+ export type { PlatformClient, PlatformClientEvents } from './client.js';
9
+ export type { PlatformFormatter } from './formatter.js';
10
+ export type { PermissionApi, PermissionApiConfig, ReactionEvent, PostedMessage, } from './permission-api.js';
11
+ export type { PlatformUser, PlatformPost, PlatformReaction, PlatformFile, CreatePostRequest, UpdatePostRequest, AddReactionRequest, } from './types.js';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Platform abstraction layer
3
+ *
4
+ * This module provides platform-agnostic interfaces and types that allow
5
+ * claude-threads to work with multiple chat platforms (Mattermost, Slack, etc.)
6
+ * without coupling the core logic to any specific platform.
7
+ */
8
+ export {};
@@ -0,0 +1,70 @@
1
+ import { EventEmitter } from 'events';
2
+ import type { MattermostPlatformConfig } from '../../config/migration.js';
3
+ import type { PlatformClient, PlatformUser, PlatformPost, PlatformFile } from '../index.js';
4
+ import type { PlatformFormatter } from '../formatter.js';
5
+ export declare class MattermostClient extends EventEmitter implements PlatformClient {
6
+ readonly platformId: string;
7
+ readonly platformType: "mattermost";
8
+ readonly displayName: string;
9
+ private ws;
10
+ private url;
11
+ private token;
12
+ private channelId;
13
+ private botName;
14
+ private allowedUsers;
15
+ private reconnectAttempts;
16
+ private maxReconnectAttempts;
17
+ private reconnectDelay;
18
+ private userCache;
19
+ private botUserId;
20
+ private readonly formatter;
21
+ private pingInterval;
22
+ private lastMessageAt;
23
+ private readonly PING_INTERVAL_MS;
24
+ private readonly PING_TIMEOUT_MS;
25
+ constructor(platformConfig: MattermostPlatformConfig);
26
+ private normalizePlatformUser;
27
+ private normalizePlatformPost;
28
+ private normalizePlatformReaction;
29
+ private normalizePlatformFile;
30
+ private api;
31
+ getBotUser(): Promise<PlatformUser>;
32
+ getUser(userId: string): Promise<PlatformUser | null>;
33
+ createPost(message: string, threadId?: string): Promise<PlatformPost>;
34
+ updatePost(postId: string, message: string): Promise<PlatformPost>;
35
+ addReaction(postId: string, emojiName: string): Promise<void>;
36
+ /**
37
+ * Create a post with reaction options for user interaction
38
+ *
39
+ * This is a common pattern for interactive posts that need user response
40
+ * via reactions (e.g., approval prompts, questions, permission requests).
41
+ *
42
+ * @param message - Post message content
43
+ * @param reactions - Array of emoji names to add as reaction options
44
+ * @param threadId - Optional thread root ID
45
+ * @returns The created post
46
+ */
47
+ createInteractivePost(message: string, reactions: string[], threadId?: string): Promise<PlatformPost>;
48
+ downloadFile(fileId: string): Promise<Buffer>;
49
+ getFileInfo(fileId: string): Promise<PlatformFile>;
50
+ getPost(postId: string): Promise<PlatformPost | null>;
51
+ connect(): Promise<void>;
52
+ private handleEvent;
53
+ private scheduleReconnect;
54
+ private startHeartbeat;
55
+ private stopHeartbeat;
56
+ isUserAllowed(username: string): boolean;
57
+ isBotMentioned(message: string): boolean;
58
+ extractPrompt(message: string): string;
59
+ getBotName(): string;
60
+ getMcpConfig(): {
61
+ type: string;
62
+ url: string;
63
+ token: string;
64
+ channelId: string;
65
+ allowedUsers: string[];
66
+ };
67
+ getFormatter(): PlatformFormatter;
68
+ sendTyping(parentId?: string): void;
69
+ disconnect(): void;
70
+ }
@@ -1,34 +1,96 @@
1
1
  import WebSocket from 'ws';
2
2
  import { EventEmitter } from 'events';
3
- import { wsLogger } from '../utils/logger.js';
3
+ import { wsLogger } from '../../utils/logger.js';
4
+ import { MattermostFormatter } from './formatter.js';
4
5
  // Escape special regex characters to prevent regex injection
5
6
  function escapeRegExp(string) {
6
7
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
8
  }
8
9
  export class MattermostClient extends EventEmitter {
10
+ // Platform identity (required by PlatformClient)
11
+ platformId;
12
+ platformType = 'mattermost';
13
+ displayName;
9
14
  ws = null;
10
- config;
15
+ url;
16
+ token;
17
+ channelId;
18
+ botName;
19
+ allowedUsers;
11
20
  reconnectAttempts = 0;
12
21
  maxReconnectAttempts = 10;
13
22
  reconnectDelay = 1000;
14
23
  userCache = new Map();
15
24
  botUserId = null;
25
+ formatter = new MattermostFormatter();
16
26
  // Heartbeat to detect dead connections
17
27
  pingInterval = null;
18
28
  lastMessageAt = Date.now();
19
29
  PING_INTERVAL_MS = 30000; // Send ping every 30s
20
30
  PING_TIMEOUT_MS = 60000; // Reconnect if no message for 60s
21
- constructor(config) {
31
+ constructor(platformConfig) {
22
32
  super();
23
- this.config = config;
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
+ };
24
86
  }
25
87
  // REST API helper
26
88
  async api(method, path, body) {
27
- const url = `${this.config.mattermost.url}/api/v4${path}`;
89
+ const url = `${this.url}/api/v4${path}`;
28
90
  const response = await fetch(url, {
29
91
  method,
30
92
  headers: {
31
- Authorization: `Bearer ${this.config.mattermost.token}`,
93
+ Authorization: `Bearer ${this.token}`,
32
94
  'Content-Type': 'application/json',
33
95
  },
34
96
  body: body ? JSON.stringify(body) : undefined,
@@ -43,17 +105,18 @@ export class MattermostClient extends EventEmitter {
43
105
  async getBotUser() {
44
106
  const user = await this.api('GET', '/users/me');
45
107
  this.botUserId = user.id;
46
- return user;
108
+ return this.normalizePlatformUser(user);
47
109
  }
48
110
  // Get user by ID (cached)
49
111
  async getUser(userId) {
50
- if (this.userCache.has(userId)) {
51
- return this.userCache.get(userId);
112
+ const cached = this.userCache.get(userId);
113
+ if (cached) {
114
+ return this.normalizePlatformUser(cached);
52
115
  }
53
116
  try {
54
117
  const user = await this.api('GET', `/users/${userId}`);
55
118
  this.userCache.set(userId, user);
56
- return user;
119
+ return this.normalizePlatformUser(user);
57
120
  }
58
121
  catch {
59
122
  return null;
@@ -62,11 +125,12 @@ export class MattermostClient extends EventEmitter {
62
125
  // Post a message
63
126
  async createPost(message, threadId) {
64
127
  const request = {
65
- channel_id: this.config.mattermost.channelId,
128
+ channel_id: this.channelId,
66
129
  message,
67
130
  root_id: threadId,
68
131
  };
69
- return this.api('POST', '/posts', request);
132
+ const post = await this.api('POST', '/posts', request);
133
+ return this.normalizePlatformPost(post);
70
134
  }
71
135
  // Update a message (for streaming updates)
72
136
  async updatePost(postId, message) {
@@ -74,7 +138,8 @@ export class MattermostClient extends EventEmitter {
74
138
  id: postId,
75
139
  message,
76
140
  };
77
- return this.api('PUT', `/posts/${postId}`, request);
141
+ const post = await this.api('PUT', `/posts/${postId}`, request);
142
+ return this.normalizePlatformPost(post);
78
143
  }
79
144
  // Add a reaction to a post
80
145
  async addReaction(postId, emojiName) {
@@ -110,10 +175,10 @@ export class MattermostClient extends EventEmitter {
110
175
  }
111
176
  // Download a file attachment
112
177
  async downloadFile(fileId) {
113
- const url = `${this.config.mattermost.url}/api/v4/files/${fileId}`;
178
+ const url = `${this.url}/api/v4/files/${fileId}`;
114
179
  const response = await fetch(url, {
115
180
  headers: {
116
- Authorization: `Bearer ${this.config.mattermost.token}`,
181
+ Authorization: `Bearer ${this.token}`,
117
182
  },
118
183
  });
119
184
  if (!response.ok) {
@@ -124,12 +189,14 @@ export class MattermostClient extends EventEmitter {
124
189
  }
125
190
  // Get file info (metadata)
126
191
  async getFileInfo(fileId) {
127
- return this.api('GET', `/files/${fileId}/info`);
192
+ const file = await this.api('GET', `/files/${fileId}/info`);
193
+ return this.normalizePlatformFile(file);
128
194
  }
129
195
  // Get a post by ID (used to verify thread still exists on resume)
130
196
  async getPost(postId) {
131
197
  try {
132
- return await this.api('GET', `/posts/${postId}`);
198
+ const post = await this.api('GET', `/posts/${postId}`);
199
+ return this.normalizePlatformPost(post);
133
200
  }
134
201
  catch {
135
202
  return null; // Post doesn't exist or was deleted
@@ -140,7 +207,7 @@ export class MattermostClient extends EventEmitter {
140
207
  // Get bot user first
141
208
  await this.getBotUser();
142
209
  wsLogger.debug(`Bot user ID: ${this.botUserId}`);
143
- const wsUrl = this.config.mattermost.url
210
+ const wsUrl = this.url
144
211
  .replace(/^http/, 'ws')
145
212
  .concat('/api/v4/websocket');
146
213
  return new Promise((resolve, reject) => {
@@ -148,11 +215,13 @@ export class MattermostClient extends EventEmitter {
148
215
  this.ws.on('open', () => {
149
216
  wsLogger.debug('WebSocket connected');
150
217
  // Authenticate
151
- this.ws.send(JSON.stringify({
152
- seq: 1,
153
- action: 'authentication_challenge',
154
- data: { token: this.config.mattermost.token },
155
- }));
218
+ if (this.ws) {
219
+ this.ws.send(JSON.stringify({
220
+ seq: 1,
221
+ action: 'authentication_challenge',
222
+ data: { token: this.token },
223
+ }));
224
+ }
156
225
  });
157
226
  this.ws.on('message', (data) => {
158
227
  this.lastMessageAt = Date.now(); // Track activity for heartbeat
@@ -200,11 +269,11 @@ export class MattermostClient extends EventEmitter {
200
269
  if (post.user_id === this.botUserId)
201
270
  return;
202
271
  // Only handle messages in our channel
203
- if (post.channel_id !== this.config.mattermost.channelId)
272
+ if (post.channel_id !== this.channelId)
204
273
  return;
205
- // Get user info and emit
274
+ // Get user info and emit (with normalized types)
206
275
  this.getUser(post.user_id).then((user) => {
207
- this.emit('message', post, user);
276
+ this.emit('message', this.normalizePlatformPost(post), user);
208
277
  });
209
278
  }
210
279
  catch (err) {
@@ -222,9 +291,9 @@ export class MattermostClient extends EventEmitter {
222
291
  // Ignore reactions from ourselves
223
292
  if (reaction.user_id === this.botUserId)
224
293
  return;
225
- // Get user info and emit
294
+ // Get user info and emit (with normalized types)
226
295
  this.getUser(reaction.user_id).then((user) => {
227
- this.emit('reaction', reaction, user);
296
+ this.emit('reaction', this.normalizePlatformReaction(reaction), user);
228
297
  });
229
298
  }
230
299
  catch (err) {
@@ -275,29 +344,43 @@ export class MattermostClient extends EventEmitter {
275
344
  }
276
345
  // Check if user is allowed to use the bot
277
346
  isUserAllowed(username) {
278
- if (this.config.allowedUsers.length === 0) {
347
+ if (this.allowedUsers.length === 0) {
279
348
  // If no allowlist configured, allow all
280
349
  return true;
281
350
  }
282
- return this.config.allowedUsers.includes(username);
351
+ return this.allowedUsers.includes(username);
283
352
  }
284
353
  // Check if message mentions the bot
285
354
  isBotMentioned(message) {
286
- const botName = escapeRegExp(this.config.mattermost.botName);
355
+ const botName = escapeRegExp(this.botName);
287
356
  // Match @botname at start or with space before
288
357
  const mentionPattern = new RegExp(`(^|\\s)@${botName}\\b`, 'i');
289
358
  return mentionPattern.test(message);
290
359
  }
291
360
  // Extract prompt from message (remove bot mention)
292
361
  extractPrompt(message) {
293
- const botName = escapeRegExp(this.config.mattermost.botName);
362
+ const botName = escapeRegExp(this.botName);
294
363
  return message
295
364
  .replace(new RegExp(`(^|\\s)@${botName}\\b`, 'gi'), ' ')
296
365
  .trim();
297
366
  }
298
367
  // Get the bot name
299
368
  getBotName() {
300
- return this.config.mattermost.botName;
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;
301
384
  }
302
385
  // Send typing indicator via WebSocket
303
386
  sendTyping(parentId) {
@@ -307,7 +390,7 @@ export class MattermostClient extends EventEmitter {
307
390
  action: 'user_typing',
308
391
  seq: Date.now(),
309
392
  data: {
310
- channel_id: this.config.mattermost.channelId,
393
+ channel_id: this.channelId,
311
394
  parent_id: parentId || '',
312
395
  },
313
396
  }));
@@ -0,0 +1,20 @@
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
+ }
@@ -0,0 +1,46 @@
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
+ }
@@ -0,0 +1,10 @@
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;