@shakudo/opencode-mattermost-control 0.3.45
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/.opencode/command/mattermost-connect.md +5 -0
- package/.opencode/command/mattermost-disconnect.md +5 -0
- package/.opencode/command/mattermost-monitor.md +12 -0
- package/.opencode/command/mattermost-status.md +5 -0
- package/.opencode/command/speckit.analyze.md +184 -0
- package/.opencode/command/speckit.checklist.md +294 -0
- package/.opencode/command/speckit.clarify.md +181 -0
- package/.opencode/command/speckit.constitution.md +82 -0
- package/.opencode/command/speckit.implement.md +135 -0
- package/.opencode/command/speckit.plan.md +89 -0
- package/.opencode/command/speckit.specify.md +258 -0
- package/.opencode/command/speckit.tasks.md +137 -0
- package/.opencode/command/speckit.taskstoissues.md +30 -0
- package/.opencode/plugin/mattermost-control/event-handlers/compaction.ts +61 -0
- package/.opencode/plugin/mattermost-control/event-handlers/file.ts +36 -0
- package/.opencode/plugin/mattermost-control/event-handlers/index.ts +14 -0
- package/.opencode/plugin/mattermost-control/event-handlers/message.ts +124 -0
- package/.opencode/plugin/mattermost-control/event-handlers/permission.ts +34 -0
- package/.opencode/plugin/mattermost-control/event-handlers/question.ts +92 -0
- package/.opencode/plugin/mattermost-control/event-handlers/session.ts +100 -0
- package/.opencode/plugin/mattermost-control/event-handlers/todo.ts +33 -0
- package/.opencode/plugin/mattermost-control/event-handlers/tool.ts +76 -0
- package/.opencode/plugin/mattermost-control/formatters.ts +202 -0
- package/.opencode/plugin/mattermost-control/index.ts +964 -0
- package/.opencode/plugin/mattermost-control/package.json +12 -0
- package/.opencode/plugin/mattermost-control/state.ts +180 -0
- package/.opencode/plugin/mattermost-control/timers.ts +96 -0
- package/.opencode/plugin/mattermost-control/tools/connect.ts +563 -0
- package/.opencode/plugin/mattermost-control/tools/file.ts +41 -0
- package/.opencode/plugin/mattermost-control/tools/index.ts +12 -0
- package/.opencode/plugin/mattermost-control/tools/monitor.ts +183 -0
- package/.opencode/plugin/mattermost-control/tools/schedule.ts +253 -0
- package/.opencode/plugin/mattermost-control/tools/session.ts +120 -0
- package/.opencode/plugin/mattermost-control/types.ts +107 -0
- package/LICENSE +21 -0
- package/README.md +1280 -0
- package/opencode-shared +359 -0
- package/opencode-shared-restart +495 -0
- package/opencode-shared-stop +90 -0
- package/package.json +65 -0
- package/src/clients/mattermost-client.ts +221 -0
- package/src/clients/websocket-client.ts +199 -0
- package/src/command-handler.ts +1035 -0
- package/src/config.ts +170 -0
- package/src/context-builder.ts +309 -0
- package/src/file-completion-handler.ts +521 -0
- package/src/file-handler.ts +242 -0
- package/src/guest-approval-handler.ts +223 -0
- package/src/logger.ts +73 -0
- package/src/merge-handler.ts +335 -0
- package/src/message-router.ts +151 -0
- package/src/models/index.ts +197 -0
- package/src/models/routing.ts +50 -0
- package/src/models/thread-mapping.ts +40 -0
- package/src/monitor-service.ts +222 -0
- package/src/notification-service.ts +118 -0
- package/src/opencode-session-registry.ts +370 -0
- package/src/persistence/team-store.ts +396 -0
- package/src/persistence/thread-mapping-store.ts +258 -0
- package/src/question-handler.ts +401 -0
- package/src/reaction-handler.ts +111 -0
- package/src/response-streamer.ts +364 -0
- package/src/scheduler/schedule-store.ts +261 -0
- package/src/scheduler/scheduler-service.ts +349 -0
- package/src/session-manager.ts +142 -0
- package/src/session-ownership-handler.ts +253 -0
- package/src/status-indicator.ts +279 -0
- package/src/thread-manager.ts +231 -0
- package/src/todo-manager.ts +162 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { log as fileLog } from "./logger.js";
|
|
3
|
+
|
|
4
|
+
// Configuration schema
|
|
5
|
+
const MattermostConfigSchema = z.object({
|
|
6
|
+
baseUrl: z.string().url(),
|
|
7
|
+
wsUrl: z.string(),
|
|
8
|
+
token: z.string().default(""),
|
|
9
|
+
botUsername: z.string().optional(),
|
|
10
|
+
defaultTeam: z.string().optional(),
|
|
11
|
+
debug: z.boolean().default(false),
|
|
12
|
+
reconnectInterval: z.number().default(5000),
|
|
13
|
+
maxReconnectAttempts: z.number().default(10),
|
|
14
|
+
autoConnect: z.boolean().default(true),
|
|
15
|
+
ownerUserId: z.string().optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const StreamingConfigSchema = z.object({
|
|
19
|
+
bufferSize: z.number().default(50),
|
|
20
|
+
maxDelay: z.number().default(500),
|
|
21
|
+
editRateLimit: z.number().default(10),
|
|
22
|
+
maxPostLength: z.number().default(15000), // Mattermost limit is 16383, leave buffer for formatting
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const NotificationsConfigSchema = z.object({
|
|
26
|
+
onCompletion: z.boolean().default(true),
|
|
27
|
+
onPermissionRequest: z.boolean().default(true),
|
|
28
|
+
onError: z.boolean().default(true),
|
|
29
|
+
onStatusUpdate: z.boolean().default(true),
|
|
30
|
+
statusInterval: z.number().default(30000),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const SessionsConfigSchema = z.object({
|
|
34
|
+
timeout: z.number().default(3600000),
|
|
35
|
+
maxSessions: z.number().default(50),
|
|
36
|
+
allowedUsers: z.array(z.string()).default([]),
|
|
37
|
+
allowedChannelTypes: z.array(z.enum(["D", "G", "O", "P"])).default(["D", "G", "O", "P"]),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const SessionSelectionConfigSchema = z.object({
|
|
41
|
+
commandPrefix: z.string().default("!"),
|
|
42
|
+
autoSelectSingle: z.boolean().default(true),
|
|
43
|
+
refreshIntervalMs: z.number().default(60000),
|
|
44
|
+
autoCreateSession: z.boolean().default(true),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const FilesConfigSchema = z.object({
|
|
48
|
+
tempDir: z.string().default("/tmp/opencode-mm-plugin"),
|
|
49
|
+
maxFileSize: z.number().default(10485760), // 10MB
|
|
50
|
+
allowedExtensions: z.array(z.string()).default(["*"]),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const PluginConfigSchema = z.object({
|
|
54
|
+
mattermost: MattermostConfigSchema,
|
|
55
|
+
streaming: StreamingConfigSchema,
|
|
56
|
+
notifications: NotificationsConfigSchema,
|
|
57
|
+
sessions: SessionsConfigSchema,
|
|
58
|
+
files: FilesConfigSchema,
|
|
59
|
+
sessionSelection: SessionSelectionConfigSchema,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export type MattermostConfig = z.infer<typeof MattermostConfigSchema>;
|
|
63
|
+
export type StreamingConfig = z.infer<typeof StreamingConfigSchema>;
|
|
64
|
+
export type NotificationsConfig = z.infer<typeof NotificationsConfigSchema>;
|
|
65
|
+
export type SessionsConfig = z.infer<typeof SessionsConfigSchema>;
|
|
66
|
+
export type FilesConfig = z.infer<typeof FilesConfigSchema>;
|
|
67
|
+
export type SessionSelectionConfig = z.infer<typeof SessionSelectionConfigSchema>;
|
|
68
|
+
export type PluginConfig = z.infer<typeof PluginConfigSchema>;
|
|
69
|
+
|
|
70
|
+
// Default Mattermost configuration
|
|
71
|
+
// NOTE: These are example URLs - you must configure your own Mattermost instance
|
|
72
|
+
const DEFAULT_MATTERMOST_CONFIG = {
|
|
73
|
+
baseUrl: "https://your-mattermost-instance.example.com/api/v4",
|
|
74
|
+
wsUrl: "wss://your-mattermost-instance.example.com/api/v4/websocket",
|
|
75
|
+
token: "", // REQUIRED: Set via MATTERMOST_TOKEN environment variable
|
|
76
|
+
defaultTeam: "",
|
|
77
|
+
debug: false,
|
|
78
|
+
reconnectInterval: 5000,
|
|
79
|
+
maxReconnectAttempts: 10,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Load configuration from environment variables
|
|
84
|
+
*/
|
|
85
|
+
export function loadConfig(): PluginConfig {
|
|
86
|
+
// Get baseUrl and ensure it has /api/v4 suffix
|
|
87
|
+
let baseUrl = process.env.MATTERMOST_URL || DEFAULT_MATTERMOST_CONFIG.baseUrl;
|
|
88
|
+
if (!baseUrl.includes("/api/v4")) {
|
|
89
|
+
baseUrl = baseUrl.replace(/\/$/, "") + "/api/v4";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Get wsUrl and ensure it has /api/v4/websocket suffix
|
|
93
|
+
let wsUrl = process.env.MATTERMOST_WS_URL || DEFAULT_MATTERMOST_CONFIG.wsUrl;
|
|
94
|
+
if (!wsUrl.includes("/api/v4/websocket")) {
|
|
95
|
+
wsUrl = wsUrl.replace(/\/$/, "").replace(/\/websocket$/, "");
|
|
96
|
+
wsUrl = wsUrl + "/api/v4/websocket";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const token = process.env.MATTERMOST_TOKEN || DEFAULT_MATTERMOST_CONFIG.token;
|
|
100
|
+
|
|
101
|
+
const config: PluginConfig = {
|
|
102
|
+
mattermost: {
|
|
103
|
+
baseUrl,
|
|
104
|
+
wsUrl,
|
|
105
|
+
token,
|
|
106
|
+
botUsername: process.env.MATTERMOST_BOT_USERNAME,
|
|
107
|
+
defaultTeam: process.env.MATTERMOST_TEAM || DEFAULT_MATTERMOST_CONFIG.defaultTeam,
|
|
108
|
+
debug: process.env.MATTERMOST_DEBUG === "true" || DEFAULT_MATTERMOST_CONFIG.debug,
|
|
109
|
+
reconnectInterval:
|
|
110
|
+
parseInt(process.env.MATTERMOST_RECONNECT_INTERVAL || "") ||
|
|
111
|
+
DEFAULT_MATTERMOST_CONFIG.reconnectInterval,
|
|
112
|
+
maxReconnectAttempts:
|
|
113
|
+
parseInt(process.env.MATTERMOST_MAX_RECONNECT_ATTEMPTS || "") ||
|
|
114
|
+
DEFAULT_MATTERMOST_CONFIG.maxReconnectAttempts,
|
|
115
|
+
autoConnect: process.env.MATTERMOST_AUTO_CONNECT !== "false",
|
|
116
|
+
ownerUserId: process.env.MATTERMOST_OWNER_USER_ID || undefined,
|
|
117
|
+
},
|
|
118
|
+
streaming: {
|
|
119
|
+
bufferSize: parseInt(process.env.OPENCODE_MM_BUFFER_SIZE || "") || 50,
|
|
120
|
+
maxDelay: parseInt(process.env.OPENCODE_MM_MAX_DELAY || "") || 500,
|
|
121
|
+
editRateLimit: parseInt(process.env.OPENCODE_MM_EDIT_RATE_LIMIT || "") || 10,
|
|
122
|
+
maxPostLength: parseInt(process.env.OPENCODE_MM_MAX_POST_LENGTH || "") || 15000,
|
|
123
|
+
},
|
|
124
|
+
notifications: {
|
|
125
|
+
onCompletion: process.env.OPENCODE_MM_NOTIFY_COMPLETION !== "false",
|
|
126
|
+
onPermissionRequest: process.env.OPENCODE_MM_NOTIFY_PERMISSION !== "false",
|
|
127
|
+
onError: process.env.OPENCODE_MM_NOTIFY_ERROR !== "false",
|
|
128
|
+
onStatusUpdate: process.env.OPENCODE_MM_NOTIFY_STATUS !== "false",
|
|
129
|
+
statusInterval: parseInt(process.env.OPENCODE_MM_STATUS_INTERVAL || "") || 30000,
|
|
130
|
+
},
|
|
131
|
+
sessions: {
|
|
132
|
+
timeout: parseInt(process.env.OPENCODE_MM_SESSION_TIMEOUT || "") || 3600000,
|
|
133
|
+
maxSessions: parseInt(process.env.OPENCODE_MM_MAX_SESSIONS || "") || 50,
|
|
134
|
+
allowedUsers: process.env.OPENCODE_MM_ALLOWED_USERS?.split(",").filter(Boolean) || [],
|
|
135
|
+
allowedChannelTypes: (process.env.OPENCODE_MM_ALLOWED_CHANNEL_TYPES?.split(",").filter(Boolean) as ("D" | "G" | "O" | "P")[]) || ["D", "G", "O", "P"],
|
|
136
|
+
},
|
|
137
|
+
files: {
|
|
138
|
+
tempDir: process.env.OPENCODE_MM_TEMP_DIR || "/tmp/opencode-mm-plugin",
|
|
139
|
+
maxFileSize: parseInt(process.env.OPENCODE_MM_MAX_FILE_SIZE || "") || 10485760,
|
|
140
|
+
allowedExtensions:
|
|
141
|
+
process.env.OPENCODE_MM_ALLOWED_EXTENSIONS?.split(",").filter(Boolean) || ["*"],
|
|
142
|
+
},
|
|
143
|
+
sessionSelection: {
|
|
144
|
+
commandPrefix: process.env.OPENCODE_MM_COMMAND_PREFIX || "!",
|
|
145
|
+
autoSelectSingle: process.env.OPENCODE_MM_AUTO_SELECT !== "false",
|
|
146
|
+
refreshIntervalMs: parseInt(process.env.OPENCODE_MM_SESSION_REFRESH_INTERVAL || "") || 60000,
|
|
147
|
+
autoCreateSession: process.env.OPENCODE_MM_AUTO_CREATE_SESSION !== "false",
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Validate configuration
|
|
152
|
+
return PluginConfigSchema.parse(config);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function createLogger(debug: boolean) {
|
|
156
|
+
return {
|
|
157
|
+
debug: (...args: unknown[]) => {
|
|
158
|
+
if (debug) fileLog.debug(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
|
|
159
|
+
},
|
|
160
|
+
info: (...args: unknown[]) => {
|
|
161
|
+
fileLog.info(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
|
|
162
|
+
},
|
|
163
|
+
warn: (...args: unknown[]) => {
|
|
164
|
+
fileLog.warn(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
|
|
165
|
+
},
|
|
166
|
+
error: (...args: unknown[]) => {
|
|
167
|
+
fileLog.error(args.map(a => typeof a === "object" ? JSON.stringify(a) : String(a)).join(" "));
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Builder for Group DM Thread Messages
|
|
3
|
+
*
|
|
4
|
+
* Fetches recent thread messages and optionally summarizes them with Claude Haiku
|
|
5
|
+
* to provide context when responding to @mentions in group DMs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { MattermostClient } from "./clients/mattermost-client.js";
|
|
9
|
+
import type { Post, PostList } from "./models/index.js";
|
|
10
|
+
import { log } from "./logger.js";
|
|
11
|
+
|
|
12
|
+
/** Maximum context length in characters before summarization */
|
|
13
|
+
const MAX_CONTEXT_LENGTH = 8000;
|
|
14
|
+
|
|
15
|
+
/** Default number of messages to fetch from thread */
|
|
16
|
+
const DEFAULT_MESSAGE_COUNT = 5;
|
|
17
|
+
|
|
18
|
+
export interface ThreadMessage {
|
|
19
|
+
userId: string;
|
|
20
|
+
username: string;
|
|
21
|
+
message: string;
|
|
22
|
+
timestamp: number;
|
|
23
|
+
fileIds?: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ThreadContext {
|
|
27
|
+
messages: ThreadMessage[];
|
|
28
|
+
totalCharacters: number;
|
|
29
|
+
wasSummarized: boolean;
|
|
30
|
+
summary?: string;
|
|
31
|
+
allFileIds: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build context from recent thread messages, excluding bot messages
|
|
36
|
+
*
|
|
37
|
+
* @param mmClient - Mattermost client instance
|
|
38
|
+
* @param threadRootPostId - Root post ID of the thread
|
|
39
|
+
* @param currentPostId - Current post ID (to exclude from context)
|
|
40
|
+
* @param botUserId - Bot user ID (to exclude bot messages)
|
|
41
|
+
* @param maxMessages - Maximum number of messages to fetch (default: 5)
|
|
42
|
+
* @returns ThreadContext with recent messages
|
|
43
|
+
*/
|
|
44
|
+
export async function buildThreadContext(
|
|
45
|
+
mmClient: MattermostClient,
|
|
46
|
+
threadRootPostId: string,
|
|
47
|
+
currentPostId: string,
|
|
48
|
+
botUserId: string,
|
|
49
|
+
maxMessages: number = DEFAULT_MESSAGE_COUNT
|
|
50
|
+
): Promise<ThreadContext> {
|
|
51
|
+
try {
|
|
52
|
+
log.info(`[ContextBuilder] Fetching thread context for root post: ${threadRootPostId}`);
|
|
53
|
+
|
|
54
|
+
// Fetch the full thread
|
|
55
|
+
const postList: PostList = await mmClient.getPostThread(threadRootPostId);
|
|
56
|
+
|
|
57
|
+
if (!postList.order || postList.order.length === 0) {
|
|
58
|
+
log.info(`[ContextBuilder] No posts found in thread`);
|
|
59
|
+
return { messages: [], totalCharacters: 0, wasSummarized: false, allFileIds: [] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sort posts by create_at (oldest first), then take the most recent ones
|
|
63
|
+
const sortedPostIds = [...postList.order].sort((a, b) => {
|
|
64
|
+
const postA = postList.posts[a];
|
|
65
|
+
const postB = postList.posts[b];
|
|
66
|
+
return (postA?.create_at || 0) - (postB?.create_at || 0);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Filter out:
|
|
70
|
+
// 1. Bot messages
|
|
71
|
+
// 2. The current post (the @mention we're responding to)
|
|
72
|
+
// 3. The root post ONLY if it's a bot message (session announcement)
|
|
73
|
+
// - If root is a user message (existing thread), include it for context
|
|
74
|
+
const relevantPosts: Post[] = sortedPostIds
|
|
75
|
+
.map(id => postList.posts[id])
|
|
76
|
+
.filter((post): post is Post => {
|
|
77
|
+
if (!post) return false;
|
|
78
|
+
if (post.user_id === botUserId) return false;
|
|
79
|
+
if (post.id === currentPostId) return false;
|
|
80
|
+
// Only exclude root post if it's from the bot (session announcement)
|
|
81
|
+
// User-created threads should have their root post included for context
|
|
82
|
+
if (post.id === threadRootPostId && post.user_id === botUserId) return false;
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Take the last N messages
|
|
87
|
+
const recentPosts = relevantPosts.slice(-maxMessages);
|
|
88
|
+
|
|
89
|
+
log.info(`[ContextBuilder] Found ${relevantPosts.length} relevant posts, using last ${recentPosts.length}`);
|
|
90
|
+
|
|
91
|
+
// Build thread messages - we need to fetch usernames
|
|
92
|
+
const messages: ThreadMessage[] = [];
|
|
93
|
+
const userCache: Record<string, string> = {};
|
|
94
|
+
|
|
95
|
+
for (const post of recentPosts) {
|
|
96
|
+
let username = userCache[post.user_id];
|
|
97
|
+
if (!username) {
|
|
98
|
+
try {
|
|
99
|
+
const user = await mmClient.getUserById(post.user_id);
|
|
100
|
+
username = user.username;
|
|
101
|
+
userCache[post.user_id] = username;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
username = "unknown";
|
|
104
|
+
log.warn(`[ContextBuilder] Failed to fetch username for user ${post.user_id}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const fileIds = post.file_ids && post.file_ids.length > 0 ? post.file_ids : undefined;
|
|
109
|
+
|
|
110
|
+
messages.push({
|
|
111
|
+
userId: post.user_id,
|
|
112
|
+
username,
|
|
113
|
+
message: post.message,
|
|
114
|
+
timestamp: post.create_at,
|
|
115
|
+
fileIds,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const totalCharacters = messages.reduce((sum, m) => sum + m.message.length, 0);
|
|
120
|
+
const allFileIds = messages.flatMap(m => m.fileIds || []);
|
|
121
|
+
|
|
122
|
+
log.info(`[ContextBuilder] Built context with ${messages.length} messages, ${totalCharacters} chars, ${allFileIds.length} files`);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
messages,
|
|
126
|
+
totalCharacters,
|
|
127
|
+
wasSummarized: false,
|
|
128
|
+
allFileIds,
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
log.error(`[ContextBuilder] Error building thread context: ${error}`);
|
|
132
|
+
return { messages: [], totalCharacters: 0, wasSummarized: false, allFileIds: [] };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Summarize context using Claude Haiku if it exceeds the character limit
|
|
138
|
+
*
|
|
139
|
+
* @param client - OpenCode SDK client
|
|
140
|
+
* @param sessionId - OpenCode session ID to use for summarization
|
|
141
|
+
* @param context - Thread context to potentially summarize
|
|
142
|
+
* @returns Updated ThreadContext with summary if needed
|
|
143
|
+
*/
|
|
144
|
+
export async function summarizeContextWithHaiku(
|
|
145
|
+
client: any, // OpenCode SDK client
|
|
146
|
+
sessionId: string,
|
|
147
|
+
context: ThreadContext
|
|
148
|
+
): Promise<ThreadContext> {
|
|
149
|
+
// Don't summarize if under the limit or no messages
|
|
150
|
+
if (context.totalCharacters <= MAX_CONTEXT_LENGTH || context.messages.length === 0) {
|
|
151
|
+
return context;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
log.info(`[ContextBuilder] Context exceeds ${MAX_CONTEXT_LENGTH} chars (${context.totalCharacters}), summarizing with Haiku`);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Format messages for summarization
|
|
158
|
+
const conversationText = context.messages
|
|
159
|
+
.map(m => `@${m.username}: ${m.message}`)
|
|
160
|
+
.join("\n\n");
|
|
161
|
+
|
|
162
|
+
const summaryPrompt = `Summarize the following conversation concisely, preserving key context, decisions, and any important details that would be needed to understand and respond to a follow-up message. Keep your summary under 500 words.
|
|
163
|
+
|
|
164
|
+
Conversation:
|
|
165
|
+
${conversationText}
|
|
166
|
+
|
|
167
|
+
Summary:`;
|
|
168
|
+
|
|
169
|
+
// Use Haiku for fast, cheap summarization
|
|
170
|
+
const result = await client.session.prompt({
|
|
171
|
+
sessionId,
|
|
172
|
+
prompt: summaryPrompt,
|
|
173
|
+
model: {
|
|
174
|
+
providerID: "anthropic",
|
|
175
|
+
modelID: "claude-3-5-haiku-20241022",
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Extract text from result
|
|
180
|
+
let summary = "";
|
|
181
|
+
if (result && typeof result === "object" && "text" in result) {
|
|
182
|
+
summary = String(result.text);
|
|
183
|
+
} else if (typeof result === "string") {
|
|
184
|
+
summary = result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (summary) {
|
|
188
|
+
log.info(`[ContextBuilder] Generated summary: ${summary.length} chars`);
|
|
189
|
+
return {
|
|
190
|
+
...context,
|
|
191
|
+
wasSummarized: true,
|
|
192
|
+
summary,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
log.warn(`[ContextBuilder] Haiku returned empty summary, using original context`);
|
|
197
|
+
return context;
|
|
198
|
+
} catch (error) {
|
|
199
|
+
log.error(`[ContextBuilder] Error summarizing with Haiku: ${error}`);
|
|
200
|
+
// Fall back to original context on error
|
|
201
|
+
return context;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Format context as a prefix for the user's prompt
|
|
207
|
+
*
|
|
208
|
+
* @param context - Thread context (possibly summarized)
|
|
209
|
+
* @param currentUsername - Username of the person who @mentioned the bot
|
|
210
|
+
* @returns Formatted context prefix string, or empty string if no context
|
|
211
|
+
*/
|
|
212
|
+
export function formatContextForPrompt(
|
|
213
|
+
context: ThreadContext,
|
|
214
|
+
currentUsername: string
|
|
215
|
+
): string {
|
|
216
|
+
if (context.messages.length === 0 && !context.summary) {
|
|
217
|
+
return "";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let contextBlock: string;
|
|
221
|
+
|
|
222
|
+
if (context.wasSummarized && context.summary) {
|
|
223
|
+
contextBlock = `[Previous conversation summary]
|
|
224
|
+
${context.summary}`;
|
|
225
|
+
if (context.allFileIds.length > 0) {
|
|
226
|
+
contextBlock += `\n[Note: ${context.allFileIds.length} file attachment(s) from this conversation are included below]`;
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
const formattedMessages = context.messages
|
|
230
|
+
.map(m => {
|
|
231
|
+
let text = `@${m.username}: ${m.message}`;
|
|
232
|
+
if (m.fileIds && m.fileIds.length > 0) {
|
|
233
|
+
text += ` [${m.fileIds.length} attachment(s)]`;
|
|
234
|
+
}
|
|
235
|
+
return text;
|
|
236
|
+
})
|
|
237
|
+
.join("\n\n");
|
|
238
|
+
|
|
239
|
+
contextBlock = `[Previous messages in this thread]
|
|
240
|
+
${formattedMessages}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return `${contextBlock}
|
|
244
|
+
|
|
245
|
+
[Current message from @${currentUsername}]
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if a message contains a bot @mention
|
|
251
|
+
*
|
|
252
|
+
* @param message - Message text to check
|
|
253
|
+
* @param botUsername - Bot username (without @)
|
|
254
|
+
* @param botUserId - Bot user ID
|
|
255
|
+
* @returns true if the bot is mentioned
|
|
256
|
+
*/
|
|
257
|
+
export function isBotMentioned(
|
|
258
|
+
message: string,
|
|
259
|
+
botUsername: string,
|
|
260
|
+
botUserId: string
|
|
261
|
+
): boolean {
|
|
262
|
+
// Check for @username mention (case-insensitive)
|
|
263
|
+
const atMentionRegex = new RegExp(`@${escapeRegExp(botUsername)}\\b`, "i");
|
|
264
|
+
if (atMentionRegex.test(message)) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check for user ID mention format used by some Mattermost clients
|
|
269
|
+
if (message.includes(`<@${botUserId}>`)) {
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Strip bot @mentions from a message to clean up the prompt
|
|
278
|
+
*
|
|
279
|
+
* @param message - Message text
|
|
280
|
+
* @param botUsername - Bot username (without @)
|
|
281
|
+
* @param botUserId - Bot user ID
|
|
282
|
+
* @returns Message with bot mentions removed
|
|
283
|
+
*/
|
|
284
|
+
export function stripBotMention(
|
|
285
|
+
message: string,
|
|
286
|
+
botUsername: string,
|
|
287
|
+
botUserId: string
|
|
288
|
+
): string {
|
|
289
|
+
// Remove @username mentions (case-insensitive)
|
|
290
|
+
let cleaned = message.replace(
|
|
291
|
+
new RegExp(`@${escapeRegExp(botUsername)}\\b`, "gi"),
|
|
292
|
+
""
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Remove user ID mention format
|
|
296
|
+
cleaned = cleaned.replace(new RegExp(`<@${escapeRegExp(botUserId)}>`, "g"), "");
|
|
297
|
+
|
|
298
|
+
// Clean up extra whitespace
|
|
299
|
+
cleaned = cleaned.replace(/\s+/g, " ").trim();
|
|
300
|
+
|
|
301
|
+
return cleaned;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Escape special regex characters in a string
|
|
306
|
+
*/
|
|
307
|
+
function escapeRegExp(str: string): string {
|
|
308
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
309
|
+
}
|