@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
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import type { MattermostClient } from "./clients/mattermost-client.js";
|
|
2
|
+
import type { ThreadMappingStore } from "./persistence/thread-mapping-store.js";
|
|
3
|
+
import type { ThreadSessionMapping } from "./models/index.js";
|
|
4
|
+
import { log } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
export interface MergeResult {
|
|
7
|
+
success: boolean;
|
|
8
|
+
message: string;
|
|
9
|
+
sourceSessionId?: string;
|
|
10
|
+
summary?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SessionMessage {
|
|
14
|
+
role: "user" | "assistant";
|
|
15
|
+
content: string;
|
|
16
|
+
timestamp?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class MergeHandler {
|
|
20
|
+
private mmClient: MattermostClient;
|
|
21
|
+
private threadMappingStore: ThreadMappingStore;
|
|
22
|
+
private opencodeClient: any;
|
|
23
|
+
private mattermostBaseUrl: string;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
mmClient: MattermostClient,
|
|
27
|
+
threadMappingStore: ThreadMappingStore,
|
|
28
|
+
opencodeClient: any,
|
|
29
|
+
mattermostBaseUrl: string
|
|
30
|
+
) {
|
|
31
|
+
this.mmClient = mmClient;
|
|
32
|
+
this.threadMappingStore = threadMappingStore;
|
|
33
|
+
this.opencodeClient = opencodeClient;
|
|
34
|
+
this.mattermostBaseUrl = mattermostBaseUrl.replace(/\/api\/v4$/, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
parseThreadUrl(url: string): string | null {
|
|
38
|
+
const patterns = [
|
|
39
|
+
/\/pl\/([a-z0-9]+)$/i,
|
|
40
|
+
/\/pl\/([a-z0-9]+)\?/i,
|
|
41
|
+
/postId=([a-z0-9]+)/i,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
for (const pattern of patterns) {
|
|
45
|
+
const match = url.match(pattern);
|
|
46
|
+
if (match?.[1]) {
|
|
47
|
+
return match[1];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getThreadRootPostId(postId: string): Promise<string | null> {
|
|
55
|
+
try {
|
|
56
|
+
const post = await this.mmClient.getPost(postId);
|
|
57
|
+
return post.root_id || post.id;
|
|
58
|
+
} catch (e) {
|
|
59
|
+
log.error(`[MergeHandler] Failed to fetch post ${postId}: ${e}`);
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async fetchSessionMessages(sessionId: string): Promise<SessionMessage[]> {
|
|
65
|
+
try {
|
|
66
|
+
const messagesResult = await this.opencodeClient.session.messages({
|
|
67
|
+
path: { id: sessionId },
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const messages: SessionMessage[] = [];
|
|
71
|
+
const rawMessages = messagesResult.data || [];
|
|
72
|
+
|
|
73
|
+
for (const msg of rawMessages) {
|
|
74
|
+
if (!msg.info?.role) continue;
|
|
75
|
+
|
|
76
|
+
let content = "";
|
|
77
|
+
if (msg.parts) {
|
|
78
|
+
for (const part of msg.parts) {
|
|
79
|
+
if (part.type === "text" && part.text) {
|
|
80
|
+
content += part.text + "\n";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (content.trim()) {
|
|
86
|
+
messages.push({
|
|
87
|
+
role: msg.info.role as "user" | "assistant",
|
|
88
|
+
content: content.trim(),
|
|
89
|
+
timestamp: msg.info.createdAt,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return messages;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
log.error(`[MergeHandler] Failed to fetch session messages for ${sessionId}: ${e}`);
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async summarizeSession(sessionId: string, sourceMapping: ThreadSessionMapping): Promise<string | null> {
|
|
102
|
+
const messages = await this.fetchSessionMessages(sessionId);
|
|
103
|
+
|
|
104
|
+
if (messages.length === 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const conversationText = messages
|
|
109
|
+
.map((m) => `**${m.role === "user" ? "User" : "Assistant"}**: ${m.content}`)
|
|
110
|
+
.join("\n\n---\n\n");
|
|
111
|
+
|
|
112
|
+
const summaryPrompt = `You are summarizing a conversation from a Mattermost thread to be merged into another thread. The conversation was about "${sourceMapping.sessionTitle || sourceMapping.projectName}".
|
|
113
|
+
|
|
114
|
+
Summarize the following conversation concisely. Focus on:
|
|
115
|
+
- Key decisions made
|
|
116
|
+
- Important context or requirements
|
|
117
|
+
- Actions taken or planned
|
|
118
|
+
- Any unfinished work or next steps
|
|
119
|
+
|
|
120
|
+
Keep your summary under 800 words. Use bullet points for clarity.
|
|
121
|
+
|
|
122
|
+
Conversation:
|
|
123
|
+
${conversationText}
|
|
124
|
+
|
|
125
|
+
Summary:`;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const result = await this.opencodeClient.session.prompt({
|
|
129
|
+
sessionId,
|
|
130
|
+
prompt: summaryPrompt,
|
|
131
|
+
model: {
|
|
132
|
+
providerID: "anthropic",
|
|
133
|
+
modelID: "claude-3-5-haiku-20241022",
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
let summary = "";
|
|
138
|
+
if (result && typeof result === "object" && "text" in result) {
|
|
139
|
+
summary = String(result.text);
|
|
140
|
+
} else if (typeof result === "string") {
|
|
141
|
+
summary = result;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return summary || null;
|
|
145
|
+
} catch (e) {
|
|
146
|
+
log.error(`[MergeHandler] Failed to summarize session ${sessionId}: ${e}`);
|
|
147
|
+
return this.createFallbackSummary(messages, sourceMapping);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private createFallbackSummary(messages: SessionMessage[], sourceMapping: ThreadSessionMapping): string {
|
|
152
|
+
const userMessages = messages.filter((m) => m.role === "user").slice(-5);
|
|
153
|
+
const assistantMessages = messages.filter((m) => m.role === "assistant").slice(-3);
|
|
154
|
+
|
|
155
|
+
let summary = `**Context from ${sourceMapping.projectName}** (${messages.length} messages)\n\n`;
|
|
156
|
+
summary += `**Recent user requests:**\n`;
|
|
157
|
+
for (const msg of userMessages) {
|
|
158
|
+
const truncated = msg.content.length > 200 ? msg.content.slice(0, 200) + "..." : msg.content;
|
|
159
|
+
summary += `- ${truncated}\n`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (assistantMessages.length > 0) {
|
|
163
|
+
summary += `\n**Key responses/actions:**\n`;
|
|
164
|
+
for (const msg of assistantMessages) {
|
|
165
|
+
const truncated = msg.content.length > 300 ? msg.content.slice(0, 300) + "..." : msg.content;
|
|
166
|
+
summary += `- ${truncated}\n`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return summary;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async executeMerge(
|
|
174
|
+
sourceUrl: string,
|
|
175
|
+
targetSessionId: string,
|
|
176
|
+
targetThreadRootPostId: string,
|
|
177
|
+
targetChannelId: string,
|
|
178
|
+
userId: string
|
|
179
|
+
): Promise<MergeResult> {
|
|
180
|
+
log.info(`[MergeHandler] Starting merge from URL ${sourceUrl} into session ${targetSessionId}`);
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const postId = this.parseThreadUrl(sourceUrl);
|
|
184
|
+
log.debug(`[MergeHandler] Parsed postId: ${postId}`);
|
|
185
|
+
if (!postId) {
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
message: "Invalid URL format. Please provide a valid Mattermost thread link (e.g., `https://mattermost.example.com/team/pl/postid123`).",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
log.debug(`[MergeHandler] Fetching thread root post ID for ${postId}`);
|
|
193
|
+
const sourceThreadRootPostId = await this.getThreadRootPostId(postId);
|
|
194
|
+
log.debug(`[MergeHandler] Source thread root: ${sourceThreadRootPostId}`);
|
|
195
|
+
if (!sourceThreadRootPostId) {
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
message: "Could not resolve thread from the provided URL. The post may have been deleted.",
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (sourceThreadRootPostId === targetThreadRootPostId) {
|
|
203
|
+
return {
|
|
204
|
+
success: false,
|
|
205
|
+
message: "Cannot merge a thread into itself.",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
log.debug(`[MergeHandler] Looking up source mapping for thread ${sourceThreadRootPostId}`);
|
|
210
|
+
const sourceMapping = this.threadMappingStore.getByThreadRootPostId(sourceThreadRootPostId);
|
|
211
|
+
log.debug(`[MergeHandler] Source mapping found: ${sourceMapping ? sourceMapping.sessionId : 'null'}`);
|
|
212
|
+
if (!sourceMapping) {
|
|
213
|
+
return {
|
|
214
|
+
success: false,
|
|
215
|
+
message: "Thread not found in this OpenCode instance. Only threads from your current OpenCode sessions can be merged.",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (sourceMapping.status === "merged") {
|
|
220
|
+
const destMapping = sourceMapping.mergedInto
|
|
221
|
+
? this.threadMappingStore.getBySessionId(sourceMapping.mergedInto)
|
|
222
|
+
: null;
|
|
223
|
+
const destLink = destMapping
|
|
224
|
+
? `[here](${this.mattermostBaseUrl}/_redirect/pl/${destMapping.threadRootPostId})`
|
|
225
|
+
: "another thread";
|
|
226
|
+
return {
|
|
227
|
+
success: false,
|
|
228
|
+
message: `This thread was already merged into ${destLink} on ${sourceMapping.mergedAt || "unknown date"}.`,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
log.debug(`[MergeHandler] Summarizing session ${sourceMapping.sessionId}`);
|
|
233
|
+
const summary = await this.summarizeSession(sourceMapping.sessionId, sourceMapping);
|
|
234
|
+
log.debug(`[MergeHandler] Summary generated: ${summary ? summary.substring(0, 100) + '...' : 'null'}`)
|
|
235
|
+
if (!summary) {
|
|
236
|
+
return {
|
|
237
|
+
success: false,
|
|
238
|
+
message: "Could not summarize the source thread. It may be empty or inaccessible.",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const sourceLink = `${this.mattermostBaseUrl}/_redirect/pl/${sourceThreadRootPostId}`;
|
|
243
|
+
const mergeTimestamp = new Date().toISOString();
|
|
244
|
+
|
|
245
|
+
let username = "unknown";
|
|
246
|
+
try {
|
|
247
|
+
const user = await this.mmClient.getUserById(userId);
|
|
248
|
+
username = user.username;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
log.warn(`[MergeHandler] Could not fetch username for ${userId}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const mergeMessage = [
|
|
254
|
+
`:twisted_rightwards_arrows: **Merged Thread Context**`,
|
|
255
|
+
"",
|
|
256
|
+
`Merged conversation from [${sourceMapping.projectName} (${sourceMapping.shortId})](${sourceLink}):`,
|
|
257
|
+
"",
|
|
258
|
+
"---",
|
|
259
|
+
"",
|
|
260
|
+
summary,
|
|
261
|
+
"",
|
|
262
|
+
"---",
|
|
263
|
+
`_Merged by @${username} at ${new Date(mergeTimestamp).toLocaleString()}_`,
|
|
264
|
+
].join("\n");
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
await this.mmClient.createPost(targetChannelId, mergeMessage, targetThreadRootPostId);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
log.error(`[MergeHandler] Failed to post merge summary: ${e}`);
|
|
270
|
+
return {
|
|
271
|
+
success: false,
|
|
272
|
+
message: `Failed to post merge summary: ${e instanceof Error ? e.message : String(e)}`,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
sourceMapping.status = "merged";
|
|
277
|
+
sourceMapping.mergedInto = targetSessionId;
|
|
278
|
+
sourceMapping.mergedAt = mergeTimestamp;
|
|
279
|
+
sourceMapping.lastActivityAt = mergeTimestamp;
|
|
280
|
+
this.threadMappingStore.update(sourceMapping);
|
|
281
|
+
|
|
282
|
+
const farewellMessage = [
|
|
283
|
+
`:lock: **Thread Merged**`,
|
|
284
|
+
"",
|
|
285
|
+
`This conversation has been merged into another thread.`,
|
|
286
|
+
`Continue the conversation [here](${this.mattermostBaseUrl}/_redirect/pl/${targetThreadRootPostId}).`,
|
|
287
|
+
"",
|
|
288
|
+
`_Merged at ${new Date(mergeTimestamp).toLocaleString()}_`,
|
|
289
|
+
].join("\n");
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await this.mmClient.createPost(
|
|
293
|
+
sourceMapping.channelId || sourceMapping.dmChannelId,
|
|
294
|
+
farewellMessage,
|
|
295
|
+
sourceThreadRootPostId
|
|
296
|
+
);
|
|
297
|
+
} catch (e) {
|
|
298
|
+
log.warn(`[MergeHandler] Could not post farewell message to source thread: ${e}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
log.info(`[MergeHandler] Successfully merged session ${sourceMapping.shortId} into ${targetSessionId.substring(0, 8)}`);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
success: true,
|
|
305
|
+
message: [
|
|
306
|
+
`:white_check_mark: **Thread Merged Successfully**`,
|
|
307
|
+
"",
|
|
308
|
+
`Conversation from **${sourceMapping.projectName}** (\`${sourceMapping.shortId}\`) has been merged.`,
|
|
309
|
+
`The source thread has been marked as merged and locked.`,
|
|
310
|
+
].join("\n"),
|
|
311
|
+
sourceSessionId: sourceMapping.sessionId,
|
|
312
|
+
summary,
|
|
313
|
+
};
|
|
314
|
+
} catch (e) {
|
|
315
|
+
log.error(`[MergeHandler] Unhandled error in executeMerge: ${e}`);
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
message: `Merge failed due to an unexpected error: ${e instanceof Error ? e.message : String(e)}`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
isMergedThread(threadRootPostId: string): boolean {
|
|
324
|
+
const mapping = this.threadMappingStore.getByThreadRootPostId(threadRootPostId);
|
|
325
|
+
return mapping?.status === "merged";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
getMergeDestination(threadRootPostId: string): ThreadSessionMapping | null {
|
|
329
|
+
const mapping = this.threadMappingStore.getByThreadRootPostId(threadRootPostId);
|
|
330
|
+
if (mapping?.status === "merged" && mapping.mergedInto) {
|
|
331
|
+
return this.threadMappingStore.getBySessionId(mapping.mergedInto);
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { Post, ThreadSessionMapping } from "./models/index.js";
|
|
2
|
+
import type {
|
|
3
|
+
InboundRouteResult,
|
|
4
|
+
ThreadPromptRoute,
|
|
5
|
+
MainDmCommandRoute,
|
|
6
|
+
MainDmPromptRoute,
|
|
7
|
+
UnknownThreadRoute,
|
|
8
|
+
EndedSessionRoute,
|
|
9
|
+
MergedSessionRoute,
|
|
10
|
+
} from "./models/routing.js";
|
|
11
|
+
|
|
12
|
+
export type MessageType = "command" | "prompt";
|
|
13
|
+
|
|
14
|
+
export interface ParsedCommand {
|
|
15
|
+
name: string;
|
|
16
|
+
args: string[];
|
|
17
|
+
rawArgs: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RouteResult {
|
|
21
|
+
type: MessageType;
|
|
22
|
+
command?: ParsedCommand;
|
|
23
|
+
promptText?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type ThreadLookupFn = (threadRootPostId: string) => ThreadSessionMapping | null;
|
|
27
|
+
|
|
28
|
+
export class MessageRouter {
|
|
29
|
+
private commandPrefix: string;
|
|
30
|
+
private threadLookup: ThreadLookupFn | null = null;
|
|
31
|
+
|
|
32
|
+
constructor(commandPrefix: string = "!") {
|
|
33
|
+
this.commandPrefix = commandPrefix;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setThreadLookup(fn: ThreadLookupFn): void {
|
|
37
|
+
this.threadLookup = fn;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
route(post: Post): RouteResult {
|
|
41
|
+
const message = post.message.trim();
|
|
42
|
+
|
|
43
|
+
if (this.isCommand(message)) {
|
|
44
|
+
return {
|
|
45
|
+
type: "command",
|
|
46
|
+
command: this.parseCommand(message)!,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
type: "prompt",
|
|
52
|
+
promptText: message,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
routeWithThreads(post: Post): InboundRouteResult {
|
|
57
|
+
const message = post.message.trim();
|
|
58
|
+
const threadRootPostId = post.root_id;
|
|
59
|
+
|
|
60
|
+
if (threadRootPostId && this.threadLookup) {
|
|
61
|
+
const mapping = this.threadLookup(threadRootPostId);
|
|
62
|
+
|
|
63
|
+
if (!mapping) {
|
|
64
|
+
return {
|
|
65
|
+
type: "unknown_thread",
|
|
66
|
+
threadRootPostId,
|
|
67
|
+
errorMessage: "This thread is not associated with any OpenCode session.",
|
|
68
|
+
} as UnknownThreadRoute;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (mapping.status === "ended") {
|
|
72
|
+
return {
|
|
73
|
+
type: "ended_session",
|
|
74
|
+
sessionId: mapping.sessionId,
|
|
75
|
+
threadRootPostId,
|
|
76
|
+
errorMessage: "This session has ended. Start a new OpenCode session to create a new thread.",
|
|
77
|
+
} as EndedSessionRoute;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (mapping.status === "disconnected") {
|
|
81
|
+
return {
|
|
82
|
+
type: "ended_session",
|
|
83
|
+
sessionId: mapping.sessionId,
|
|
84
|
+
threadRootPostId,
|
|
85
|
+
errorMessage: "This session is disconnected. Please wait for reconnection or start a new session.",
|
|
86
|
+
} as EndedSessionRoute;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (mapping.status === "merged") {
|
|
90
|
+
return {
|
|
91
|
+
type: "merged_session",
|
|
92
|
+
sessionId: mapping.sessionId,
|
|
93
|
+
threadRootPostId,
|
|
94
|
+
mergedInto: mapping.mergedInto || "",
|
|
95
|
+
errorMessage: "This thread has been merged into another session.",
|
|
96
|
+
} as MergedSessionRoute;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (mapping.status === "orphaned") {
|
|
100
|
+
return {
|
|
101
|
+
type: "ended_session",
|
|
102
|
+
sessionId: mapping.sessionId,
|
|
103
|
+
threadRootPostId,
|
|
104
|
+
errorMessage: "This session is no longer available (OpenCode may have restarted). Start a new conversation to create a new session.",
|
|
105
|
+
} as EndedSessionRoute;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
type: "thread_prompt",
|
|
110
|
+
sessionId: mapping.sessionId,
|
|
111
|
+
threadRootPostId,
|
|
112
|
+
promptText: message,
|
|
113
|
+
fileIds: post.file_ids,
|
|
114
|
+
} as ThreadPromptRoute;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (this.isCommand(message)) {
|
|
118
|
+
return {
|
|
119
|
+
type: "main_dm_command",
|
|
120
|
+
command: this.parseCommand(message)!,
|
|
121
|
+
} as MainDmCommandRoute;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
type: "main_dm_prompt",
|
|
126
|
+
errorMessage: "Prompts must be sent in a session thread, not the main DM.",
|
|
127
|
+
suggestedAction: "Use `!sessions` to see available sessions and their threads.",
|
|
128
|
+
} as MainDmPromptRoute;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private isCommand(message: string): boolean {
|
|
132
|
+
return message.startsWith(this.commandPrefix);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
parseCommand(message: string): ParsedCommand | null {
|
|
136
|
+
if (!message.startsWith(this.commandPrefix)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const withoutPrefix = message.slice(this.commandPrefix.length);
|
|
140
|
+
const parts = withoutPrefix.split(/\s+/);
|
|
141
|
+
const name = parts[0]?.toLowerCase() || "";
|
|
142
|
+
const args = parts.slice(1);
|
|
143
|
+
const rawArgs = parts.slice(1).join(" ");
|
|
144
|
+
|
|
145
|
+
return { name, args, rawArgs };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getCommandPrefix(): string {
|
|
149
|
+
return this.commandPrefix;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
export interface User {
|
|
2
|
+
id: string;
|
|
3
|
+
create_at: number;
|
|
4
|
+
update_at: number;
|
|
5
|
+
delete_at: number;
|
|
6
|
+
username: string;
|
|
7
|
+
auth_service: string;
|
|
8
|
+
email: string;
|
|
9
|
+
nickname: string;
|
|
10
|
+
first_name: string;
|
|
11
|
+
last_name: string;
|
|
12
|
+
position: string;
|
|
13
|
+
roles: string;
|
|
14
|
+
allow_marketing: boolean;
|
|
15
|
+
props: Record<string, any>;
|
|
16
|
+
notify_props: {
|
|
17
|
+
email: string;
|
|
18
|
+
push: string;
|
|
19
|
+
desktop: string;
|
|
20
|
+
desktop_sound: string;
|
|
21
|
+
mention_keys: string;
|
|
22
|
+
channel: string;
|
|
23
|
+
first_name: string;
|
|
24
|
+
};
|
|
25
|
+
last_password_update: number;
|
|
26
|
+
locale: string;
|
|
27
|
+
timezone?: {
|
|
28
|
+
automaticTimezone: string;
|
|
29
|
+
manualTimezone: string;
|
|
30
|
+
useAutomaticTimezone: string;
|
|
31
|
+
};
|
|
32
|
+
is_bot: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Team {
|
|
36
|
+
id: string;
|
|
37
|
+
create_at: number;
|
|
38
|
+
update_at: number;
|
|
39
|
+
delete_at: number;
|
|
40
|
+
display_name: string;
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
email: string;
|
|
44
|
+
type: string;
|
|
45
|
+
company_name: string;
|
|
46
|
+
allowed_domains: string;
|
|
47
|
+
invite_id: string;
|
|
48
|
+
allow_open_invite: boolean;
|
|
49
|
+
scheme_id: string;
|
|
50
|
+
group_constrained: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface Channel {
|
|
54
|
+
id: string;
|
|
55
|
+
create_at: number;
|
|
56
|
+
update_at: number;
|
|
57
|
+
delete_at: number;
|
|
58
|
+
team_id: string;
|
|
59
|
+
type: string;
|
|
60
|
+
display_name: string;
|
|
61
|
+
name: string;
|
|
62
|
+
header: string;
|
|
63
|
+
purpose: string;
|
|
64
|
+
last_post_at: number;
|
|
65
|
+
total_msg_count: number;
|
|
66
|
+
extra_update_at: number;
|
|
67
|
+
creator_id: string;
|
|
68
|
+
scheme_id: string;
|
|
69
|
+
group_constrained: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Post {
|
|
73
|
+
id: string;
|
|
74
|
+
create_at: number;
|
|
75
|
+
update_at: number;
|
|
76
|
+
delete_at: number;
|
|
77
|
+
edit_at: number;
|
|
78
|
+
user_id: string;
|
|
79
|
+
channel_id: string;
|
|
80
|
+
root_id: string;
|
|
81
|
+
parent_id: string;
|
|
82
|
+
original_id: string;
|
|
83
|
+
message: string;
|
|
84
|
+
type: string;
|
|
85
|
+
props: Record<string, any>;
|
|
86
|
+
hashtags: string;
|
|
87
|
+
file_ids: string[];
|
|
88
|
+
pending_post_id: string;
|
|
89
|
+
metadata: {
|
|
90
|
+
embeds?: any[];
|
|
91
|
+
emojis?: any[];
|
|
92
|
+
files?: any[];
|
|
93
|
+
images?: {
|
|
94
|
+
height: number;
|
|
95
|
+
width: number;
|
|
96
|
+
format: string;
|
|
97
|
+
frame_count: number;
|
|
98
|
+
}[];
|
|
99
|
+
reactions?: any[];
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface PostList {
|
|
104
|
+
order: string[];
|
|
105
|
+
posts: Record<string, Post>;
|
|
106
|
+
next_post_id?: string;
|
|
107
|
+
prev_post_id?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface Reaction {
|
|
111
|
+
user_id: string;
|
|
112
|
+
post_id: string;
|
|
113
|
+
emoji_name: string;
|
|
114
|
+
create_at: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface FileInfo {
|
|
118
|
+
id: string;
|
|
119
|
+
user_id: string;
|
|
120
|
+
post_id: string;
|
|
121
|
+
create_at: number;
|
|
122
|
+
update_at: number;
|
|
123
|
+
delete_at: number;
|
|
124
|
+
name: string;
|
|
125
|
+
extension: string;
|
|
126
|
+
size: number;
|
|
127
|
+
mime_type: string;
|
|
128
|
+
width: number;
|
|
129
|
+
height: number;
|
|
130
|
+
has_preview_image: boolean;
|
|
131
|
+
mini_preview?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface WebSocketEvent {
|
|
135
|
+
event: string;
|
|
136
|
+
data: any;
|
|
137
|
+
broadcast: {
|
|
138
|
+
omit_users?: Record<string, boolean>;
|
|
139
|
+
user_id?: string;
|
|
140
|
+
channel_id?: string;
|
|
141
|
+
team_id?: string;
|
|
142
|
+
};
|
|
143
|
+
seq: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface PostedEvent {
|
|
147
|
+
channel_display_name: string;
|
|
148
|
+
channel_name: string;
|
|
149
|
+
channel_type: string;
|
|
150
|
+
mentioned?: Record<string, boolean>;
|
|
151
|
+
post: string;
|
|
152
|
+
sender_name: string;
|
|
153
|
+
set_online?: boolean;
|
|
154
|
+
team_id: string;
|
|
155
|
+
event_name?: string;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export type ThreadMappingStatus = "active" | "ended" | "disconnected" | "orphaned" | "merged";
|
|
159
|
+
|
|
160
|
+
export interface ModelSelection {
|
|
161
|
+
providerID: string;
|
|
162
|
+
modelID: string;
|
|
163
|
+
displayName?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ThreadSessionMapping {
|
|
167
|
+
sessionId: string;
|
|
168
|
+
threadRootPostId: string;
|
|
169
|
+
shortId: string;
|
|
170
|
+
mattermostUserId: string;
|
|
171
|
+
dmChannelId: string;
|
|
172
|
+
channelId?: string;
|
|
173
|
+
projectName: string;
|
|
174
|
+
directory: string;
|
|
175
|
+
sessionTitle?: string;
|
|
176
|
+
status: ThreadMappingStatus;
|
|
177
|
+
createdAt: string;
|
|
178
|
+
lastActivityAt: string;
|
|
179
|
+
endedAt?: string;
|
|
180
|
+
model?: ModelSelection;
|
|
181
|
+
pendingModelSelection?: boolean;
|
|
182
|
+
approvedUsers?: string[];
|
|
183
|
+
approveAllUsers?: boolean;
|
|
184
|
+
approveNextMessage?: boolean;
|
|
185
|
+
mergedInto?: string;
|
|
186
|
+
mergedAt?: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface ThreadRootPostContent {
|
|
190
|
+
projectName: string;
|
|
191
|
+
directory: string;
|
|
192
|
+
sessionId: string;
|
|
193
|
+
shortId: string;
|
|
194
|
+
startedAt: Date;
|
|
195
|
+
sessionTitle?: string;
|
|
196
|
+
ownerUsername?: string;
|
|
197
|
+
}
|