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.
- package/CHANGELOG.md +32 -0
- package/README.md +78 -28
- package/dist/claude/cli.d.ts +8 -0
- package/dist/claude/cli.js +16 -8
- package/dist/config/migration.d.ts +45 -0
- package/dist/config/migration.js +35 -0
- package/dist/config.d.ts +12 -18
- package/dist/config.js +7 -94
- package/dist/git/worktree.d.ts +0 -4
- package/dist/git/worktree.js +1 -1
- package/dist/index.js +31 -13
- package/dist/logo.d.ts +3 -20
- package/dist/logo.js +7 -23
- package/dist/mcp/permission-server.js +61 -112
- package/dist/onboarding.js +262 -137
- package/dist/persistence/session-store.d.ts +8 -2
- package/dist/persistence/session-store.js +41 -16
- package/dist/platform/client.d.ts +140 -0
- package/dist/platform/formatter.d.ts +74 -0
- package/dist/platform/index.d.ts +11 -0
- package/dist/platform/index.js +8 -0
- package/dist/platform/mattermost/client.d.ts +70 -0
- package/dist/{mattermost → platform/mattermost}/client.js +117 -34
- package/dist/platform/mattermost/formatter.d.ts +20 -0
- package/dist/platform/mattermost/formatter.js +46 -0
- package/dist/platform/mattermost/permission-api.d.ts +10 -0
- package/dist/platform/mattermost/permission-api.js +139 -0
- package/dist/platform/mattermost/types.js +1 -0
- package/dist/platform/permission-api-factory.d.ts +11 -0
- package/dist/platform/permission-api-factory.js +21 -0
- package/dist/platform/permission-api.d.ts +67 -0
- package/dist/platform/permission-api.js +8 -0
- package/dist/platform/types.d.ts +70 -0
- package/dist/platform/types.js +7 -0
- package/dist/session/commands.d.ts +52 -0
- package/dist/session/commands.js +323 -0
- package/dist/session/events.d.ts +25 -0
- package/dist/session/events.js +368 -0
- package/dist/session/index.d.ts +7 -0
- package/dist/session/index.js +6 -0
- package/dist/session/lifecycle.d.ts +70 -0
- package/dist/session/lifecycle.js +456 -0
- package/dist/session/manager.d.ts +96 -0
- package/dist/session/manager.js +537 -0
- package/dist/session/reactions.d.ts +25 -0
- package/dist/session/reactions.js +151 -0
- package/dist/session/streaming.d.ts +47 -0
- package/dist/session/streaming.js +152 -0
- package/dist/session/types.d.ts +78 -0
- package/dist/session/types.js +9 -0
- package/dist/session/worktree.d.ts +56 -0
- package/dist/session/worktree.js +339 -0
- package/dist/update-notifier.js +10 -0
- package/dist/{mattermost → utils}/emoji.d.ts +3 -3
- package/dist/{mattermost → utils}/emoji.js +3 -3
- package/dist/utils/emoji.test.d.ts +1 -0
- package/dist/utils/tool-formatter.d.ts +10 -13
- package/dist/utils/tool-formatter.js +48 -43
- package/dist/utils/tool-formatter.test.js +67 -52
- package/package.json +4 -3
- package/dist/claude/session.d.ts +0 -256
- package/dist/claude/session.js +0 -1964
- package/dist/mattermost/client.d.ts +0 -56
- /package/dist/{mattermost/emoji.test.d.ts → platform/client.js} +0 -0
- /package/dist/{mattermost/types.js → platform/formatter.js} +0 -0
- /package/dist/{mattermost → platform/mattermost}/types.d.ts +0 -0
- /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 '
|
|
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
|
-
|
|
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(
|
|
31
|
+
constructor(platformConfig) {
|
|
22
32
|
super();
|
|
23
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
51
|
-
|
|
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.
|
|
128
|
+
channel_id: this.channelId,
|
|
66
129
|
message,
|
|
67
130
|
root_id: threadId,
|
|
68
131
|
};
|
|
69
|
-
|
|
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
|
-
|
|
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.
|
|
178
|
+
const url = `${this.url}/api/v4/files/${fileId}`;
|
|
114
179
|
const response = await fetch(url, {
|
|
115
180
|
headers: {
|
|
116
|
-
Authorization: `Bearer ${this.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
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.
|
|
347
|
+
if (this.allowedUsers.length === 0) {
|
|
279
348
|
// If no allowlist configured, allow all
|
|
280
349
|
return true;
|
|
281
350
|
}
|
|
282
|
-
return this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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;
|