@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.
Files changed (69) hide show
  1. package/.opencode/command/mattermost-connect.md +5 -0
  2. package/.opencode/command/mattermost-disconnect.md +5 -0
  3. package/.opencode/command/mattermost-monitor.md +12 -0
  4. package/.opencode/command/mattermost-status.md +5 -0
  5. package/.opencode/command/speckit.analyze.md +184 -0
  6. package/.opencode/command/speckit.checklist.md +294 -0
  7. package/.opencode/command/speckit.clarify.md +181 -0
  8. package/.opencode/command/speckit.constitution.md +82 -0
  9. package/.opencode/command/speckit.implement.md +135 -0
  10. package/.opencode/command/speckit.plan.md +89 -0
  11. package/.opencode/command/speckit.specify.md +258 -0
  12. package/.opencode/command/speckit.tasks.md +137 -0
  13. package/.opencode/command/speckit.taskstoissues.md +30 -0
  14. package/.opencode/plugin/mattermost-control/event-handlers/compaction.ts +61 -0
  15. package/.opencode/plugin/mattermost-control/event-handlers/file.ts +36 -0
  16. package/.opencode/plugin/mattermost-control/event-handlers/index.ts +14 -0
  17. package/.opencode/plugin/mattermost-control/event-handlers/message.ts +124 -0
  18. package/.opencode/plugin/mattermost-control/event-handlers/permission.ts +34 -0
  19. package/.opencode/plugin/mattermost-control/event-handlers/question.ts +92 -0
  20. package/.opencode/plugin/mattermost-control/event-handlers/session.ts +100 -0
  21. package/.opencode/plugin/mattermost-control/event-handlers/todo.ts +33 -0
  22. package/.opencode/plugin/mattermost-control/event-handlers/tool.ts +76 -0
  23. package/.opencode/plugin/mattermost-control/formatters.ts +202 -0
  24. package/.opencode/plugin/mattermost-control/index.ts +964 -0
  25. package/.opencode/plugin/mattermost-control/package.json +12 -0
  26. package/.opencode/plugin/mattermost-control/state.ts +180 -0
  27. package/.opencode/plugin/mattermost-control/timers.ts +96 -0
  28. package/.opencode/plugin/mattermost-control/tools/connect.ts +563 -0
  29. package/.opencode/plugin/mattermost-control/tools/file.ts +41 -0
  30. package/.opencode/plugin/mattermost-control/tools/index.ts +12 -0
  31. package/.opencode/plugin/mattermost-control/tools/monitor.ts +183 -0
  32. package/.opencode/plugin/mattermost-control/tools/schedule.ts +253 -0
  33. package/.opencode/plugin/mattermost-control/tools/session.ts +120 -0
  34. package/.opencode/plugin/mattermost-control/types.ts +107 -0
  35. package/LICENSE +21 -0
  36. package/README.md +1280 -0
  37. package/opencode-shared +359 -0
  38. package/opencode-shared-restart +495 -0
  39. package/opencode-shared-stop +90 -0
  40. package/package.json +65 -0
  41. package/src/clients/mattermost-client.ts +221 -0
  42. package/src/clients/websocket-client.ts +199 -0
  43. package/src/command-handler.ts +1035 -0
  44. package/src/config.ts +170 -0
  45. package/src/context-builder.ts +309 -0
  46. package/src/file-completion-handler.ts +521 -0
  47. package/src/file-handler.ts +242 -0
  48. package/src/guest-approval-handler.ts +223 -0
  49. package/src/logger.ts +73 -0
  50. package/src/merge-handler.ts +335 -0
  51. package/src/message-router.ts +151 -0
  52. package/src/models/index.ts +197 -0
  53. package/src/models/routing.ts +50 -0
  54. package/src/models/thread-mapping.ts +40 -0
  55. package/src/monitor-service.ts +222 -0
  56. package/src/notification-service.ts +118 -0
  57. package/src/opencode-session-registry.ts +370 -0
  58. package/src/persistence/team-store.ts +396 -0
  59. package/src/persistence/thread-mapping-store.ts +258 -0
  60. package/src/question-handler.ts +401 -0
  61. package/src/reaction-handler.ts +111 -0
  62. package/src/response-streamer.ts +364 -0
  63. package/src/scheduler/schedule-store.ts +261 -0
  64. package/src/scheduler/scheduler-service.ts +349 -0
  65. package/src/session-manager.ts +142 -0
  66. package/src/session-ownership-handler.ts +253 -0
  67. package/src/status-indicator.ts +279 -0
  68. package/src/thread-manager.ts +231 -0
  69. package/src/todo-manager.ts +162 -0
@@ -0,0 +1,50 @@
1
+ import type { ParsedCommand } from "../message-router.js";
2
+
3
+ export interface ThreadPromptRoute {
4
+ type: "thread_prompt";
5
+ sessionId: string;
6
+ threadRootPostId: string;
7
+ promptText: string;
8
+ fileIds?: string[];
9
+ }
10
+
11
+ export interface MainDmCommandRoute {
12
+ type: "main_dm_command";
13
+ command: ParsedCommand;
14
+ }
15
+
16
+ export interface MainDmPromptRoute {
17
+ type: "main_dm_prompt";
18
+ errorMessage: string;
19
+ suggestedAction: string;
20
+ }
21
+
22
+ export interface UnknownThreadRoute {
23
+ type: "unknown_thread";
24
+ threadRootPostId: string;
25
+ errorMessage: string;
26
+ }
27
+
28
+ export interface EndedSessionRoute {
29
+ type: "ended_session";
30
+ sessionId: string;
31
+ threadRootPostId: string;
32
+ errorMessage: string;
33
+ }
34
+
35
+ export interface MergedSessionRoute {
36
+ type: "merged_session";
37
+ sessionId: string;
38
+ threadRootPostId: string;
39
+ mergedInto: string;
40
+ errorMessage: string;
41
+ redirectLink?: string;
42
+ }
43
+
44
+ export type InboundRouteResult =
45
+ | ThreadPromptRoute
46
+ | MainDmCommandRoute
47
+ | MainDmPromptRoute
48
+ | UnknownThreadRoute
49
+ | EndedSessionRoute
50
+ | MergedSessionRoute;
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+
3
+ export const ModelSelectionSchema = z.object({
4
+ providerID: z.string().min(1),
5
+ modelID: z.string().min(1),
6
+ displayName: z.string().optional(),
7
+ });
8
+
9
+ export type ModelSelection = z.infer<typeof ModelSelectionSchema>;
10
+
11
+ export const ThreadSessionMappingSchema = z.object({
12
+ sessionId: z.string().min(1),
13
+ threadRootPostId: z.string().min(1),
14
+ shortId: z.string().min(6).max(10),
15
+ mattermostUserId: z.string().min(1),
16
+ dmChannelId: z.string().min(1),
17
+ channelId: z.string().min(1).optional(),
18
+ projectName: z.string().min(1),
19
+ directory: z.string().min(1),
20
+ sessionTitle: z.string().optional(),
21
+ status: z.enum(["active", "ended", "disconnected", "orphaned", "merged"]),
22
+ createdAt: z.string().datetime(),
23
+ lastActivityAt: z.string().datetime(),
24
+ endedAt: z.string().datetime().optional(),
25
+ model: ModelSelectionSchema.optional(),
26
+ pendingModelSelection: z.boolean().optional(),
27
+ approvedUsers: z.array(z.string()).optional(),
28
+ approveAllUsers: z.boolean().optional(),
29
+ approveNextMessage: z.boolean().optional(),
30
+ mergedInto: z.string().optional(),
31
+ mergedAt: z.string().datetime().optional(),
32
+ });
33
+
34
+ export const ThreadMappingFileSchema = z.object({
35
+ version: z.literal(1),
36
+ mappings: z.array(ThreadSessionMappingSchema),
37
+ lastModified: z.string().datetime(),
38
+ });
39
+
40
+ export type ThreadMappingFileV1 = z.infer<typeof ThreadMappingFileSchema>;
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Monitor Service for Mattermost Alerts
3
+ *
4
+ * Enables ephemeral monitoring of OpenCode sessions. When a monitored session
5
+ * triggers an event (permission request, idle, question), sends a one-time
6
+ * DM alert to the specified Mattermost user.
7
+ */
8
+ import { MattermostClient } from "./clients/mattermost-client.js";
9
+ import { loadConfig, type MattermostConfig } from "./config.js";
10
+ import { log } from "./logger.js";
11
+
12
+ export interface MonitoredSession {
13
+ sessionId: string;
14
+ shortId: string;
15
+ mattermostUserId: string;
16
+ mattermostUsername: string;
17
+ projectName: string;
18
+ sessionTitle?: string;
19
+ directory: string;
20
+ registeredAt: Date;
21
+ persistent: boolean;
22
+ }
23
+
24
+ /**
25
+ * Alert types that trigger notifications
26
+ */
27
+ export type AlertType = "permission.asked" | "session.idle" | "question";
28
+
29
+ /**
30
+ * Alert context passed to formatAlertMessage
31
+ */
32
+ export interface AlertContext {
33
+ type: AlertType;
34
+ session: MonitoredSession;
35
+ /** Additional details about what's waiting (e.g., permission description) */
36
+ details?: string;
37
+ }
38
+
39
+ /**
40
+ * In-memory registry of monitored sessions.
41
+ * Key: sessionId (full), Value: MonitoredSession
42
+ */
43
+ class MonitorServiceImpl {
44
+ private monitoredSessions: Map<string, MonitoredSession> = new Map();
45
+
46
+ /**
47
+ * Register a session for monitoring
48
+ */
49
+ register(session: MonitoredSession): void {
50
+ this.monitoredSessions.set(session.sessionId, session);
51
+ log.info(`[Monitor] Registered session ${session.shortId} (${session.projectName}) for user @${session.mattermostUsername} [fullId=${session.sessionId}]`);
52
+ }
53
+
54
+ /**
55
+ * Unregister a session from monitoring
56
+ */
57
+ unregister(sessionId: string): boolean {
58
+ const session = this.monitoredSessions.get(sessionId);
59
+ if (session) {
60
+ this.monitoredSessions.delete(sessionId);
61
+ log.info(`[Monitor] Unregistered session ${session.shortId}`);
62
+ return true;
63
+ }
64
+ return false;
65
+ }
66
+
67
+ /**
68
+ * Check if a session is being monitored
69
+ */
70
+ isMonitored(sessionId: string): boolean {
71
+ return this.monitoredSessions.has(sessionId);
72
+ }
73
+
74
+ /**
75
+ * Get a monitored session by ID
76
+ */
77
+ get(sessionId: string): MonitoredSession | undefined {
78
+ return this.monitoredSessions.get(sessionId);
79
+ }
80
+
81
+ /**
82
+ * Get all monitored sessions
83
+ */
84
+ getAll(): MonitoredSession[] {
85
+ return Array.from(this.monitoredSessions.values());
86
+ }
87
+
88
+ /**
89
+ * Clear all monitored sessions
90
+ */
91
+ clear(): void {
92
+ this.monitoredSessions.clear();
93
+ log.info("[Monitor] Cleared all monitored sessions");
94
+ }
95
+
96
+ /**
97
+ * Get count of monitored sessions
98
+ */
99
+ count(): number {
100
+ return this.monitoredSessions.size;
101
+ }
102
+ }
103
+
104
+ export const MonitorService = new MonitorServiceImpl();
105
+
106
+ /**
107
+ * Format an alert message for Mattermost
108
+ */
109
+ export function formatAlertMessage(context: AlertContext): string {
110
+ const { type, session, details } = context;
111
+
112
+ const header = `:bell: **OpenCode Session Alert**`;
113
+ const projectLine = `**Project:** ${session.projectName}`;
114
+ const sessionLine = session.sessionTitle
115
+ ? `**Session:** \`${session.shortId}\` - ${session.sessionTitle}`
116
+ : `**Session:** \`${session.shortId}\``;
117
+ const directoryLine = `**Directory:** \`${session.directory}\``;
118
+
119
+ let alertTypeText: string;
120
+ let icon: string;
121
+
122
+ switch (type) {
123
+ case "permission.asked":
124
+ icon = ":lock:";
125
+ alertTypeText = "Permission requested";
126
+ break;
127
+ case "session.idle":
128
+ icon = ":hourglass:";
129
+ alertTypeText = "Session is idle (waiting for input)";
130
+ break;
131
+ case "question":
132
+ icon = ":question:";
133
+ alertTypeText = "Question awaiting answer";
134
+ break;
135
+ default:
136
+ icon = ":bell:";
137
+ alertTypeText = "Session needs attention";
138
+ }
139
+
140
+ const alertLine = `${icon} **Alert:** ${alertTypeText}`;
141
+ const detailsLine = details ? `**Details:** ${details}` : "";
142
+ const actionLine = `\n_Use \`!use ${session.shortId}\` in DM to connect to this session._`;
143
+
144
+ const parts = [header, "", projectLine, sessionLine, directoryLine, "", alertLine];
145
+ if (detailsLine) parts.push(detailsLine);
146
+ parts.push(actionLine);
147
+
148
+ return parts.join("\n");
149
+ }
150
+
151
+ /**
152
+ * Send an ephemeral alert to a Mattermost user.
153
+ * Creates a new MattermostClient, sends the DM, then discards the client.
154
+ *
155
+ * @param mattermostUserId - The Mattermost user ID to send the alert to
156
+ * @param message - The formatted message to send
157
+ * @returns true if sent successfully, false otherwise
158
+ */
159
+ export async function sendEphemeralAlert(
160
+ mattermostUserId: string,
161
+ message: string
162
+ ): Promise<boolean> {
163
+ const config = loadConfig();
164
+
165
+ if (!config.mattermost.token) {
166
+ log.error("[Monitor] Cannot send alert: MATTERMOST_TOKEN not configured");
167
+ return false;
168
+ }
169
+
170
+ if (config.mattermost.baseUrl.includes("your-mattermost-instance.example.com")) {
171
+ log.error("[Monitor] Cannot send alert: MATTERMOST_URL not configured");
172
+ return false;
173
+ }
174
+
175
+ let client: MattermostClient | null = null;
176
+
177
+ try {
178
+ client = new MattermostClient(config.mattermost);
179
+ const dmChannel = await client.createDirectChannel(mattermostUserId);
180
+ await client.createPost(dmChannel.id, message);
181
+
182
+ log.info(`[Monitor] Sent ephemeral alert to user ${mattermostUserId}`);
183
+ return true;
184
+ } catch (error) {
185
+ const errorMsg = error instanceof Error ? error.message : String(error);
186
+ log.error(`[Monitor] Failed to send ephemeral alert: ${errorMsg}`);
187
+ return false;
188
+ }
189
+ }
190
+
191
+ export async function handleMonitorAlert(
192
+ sessionId: string,
193
+ alertType: AlertType,
194
+ details?: string,
195
+ connectedSessionId?: string
196
+ ): Promise<boolean> {
197
+ if (connectedSessionId && sessionId === connectedSessionId) {
198
+ log.debug(`[Monitor] Skipping alert for connected session ${sessionId.slice(0, 6)}`);
199
+ return false;
200
+ }
201
+
202
+ const monitoredSession = MonitorService.get(sessionId);
203
+ if (!monitoredSession) {
204
+ return false;
205
+ }
206
+
207
+ log.info(`[Monitor] Handling ${alertType} alert for session ${monitoredSession.shortId} (persistent: ${monitoredSession.persistent})`);
208
+
209
+ const message = formatAlertMessage({
210
+ type: alertType,
211
+ session: monitoredSession,
212
+ details,
213
+ });
214
+
215
+ const sent = await sendEphemeralAlert(monitoredSession.mattermostUserId, message);
216
+
217
+ if (sent && !monitoredSession.persistent) {
218
+ MonitorService.unregister(sessionId);
219
+ }
220
+
221
+ return sent;
222
+ }
@@ -0,0 +1,118 @@
1
+ import type { MattermostClient } from "./clients/mattermost-client.js";
2
+ import type { NotificationsConfig } from "./config.js";
3
+ import type { UserSession, PermissionRequest } from "./session-manager.js";
4
+ import { log } from "./logger.js";
5
+
6
+ export type StatusType = "thinking" | "tool_execution" | "waiting" | "idle";
7
+
8
+ export interface StatusUpdate {
9
+ type: StatusType;
10
+ details?: string;
11
+ }
12
+
13
+ export class NotificationService {
14
+ private mmClient: MattermostClient;
15
+ private config: NotificationsConfig;
16
+
17
+ constructor(mmClient: MattermostClient, config: NotificationsConfig) {
18
+ this.mmClient = mmClient;
19
+ this.config = config;
20
+ }
21
+
22
+ async notifyCompletion(session: UserSession, summary: string, threadRootPostId?: string): Promise<void> {
23
+ if (!this.config.onCompletion) return;
24
+
25
+ const message = `:white_check_mark: **Task Completed**\n\n> ${summary}`;
26
+
27
+ try {
28
+ await this.mmClient.createPost(session.dmChannelId, message, threadRootPostId);
29
+ } catch (error) {
30
+ log.error("[NotificationService] Failed to send completion notification:", error);
31
+ }
32
+ }
33
+
34
+ async notifyPermissionRequest(session: UserSession, request: PermissionRequest): Promise<void> {
35
+ if (!this.config.onPermissionRequest) return;
36
+
37
+ const riskEmoji = {
38
+ low: ":large_blue_circle:",
39
+ medium: ":warning:",
40
+ high: ":red_circle:",
41
+ }[request.risk];
42
+
43
+ const argsDisplay = JSON.stringify(request.args, null, 2);
44
+ const message = `:warning: **Permission Required**
45
+
46
+ OpenCode wants to execute:
47
+ \`\`\`
48
+ ${request.tool}(${argsDisplay})
49
+ \`\`\`
50
+
51
+ **Risk Level**: ${request.risk} ${riskEmoji}
52
+ **Description**: ${request.description}
53
+
54
+ React to respond:
55
+ - :white_check_mark: Approve
56
+ - :x: Deny`;
57
+
58
+ try {
59
+ const post = await this.mmClient.createPost(session.dmChannelId, message);
60
+ session.pendingPermission = request;
61
+ session.currentResponsePostId = post.id;
62
+ } catch (error) {
63
+ log.error("[NotificationService] Failed to send permission request:", error);
64
+ }
65
+ }
66
+
67
+ async notifyError(session: UserSession, error: Error): Promise<void> {
68
+ if (!this.config.onError) return;
69
+
70
+ const message = `:x: **Error Occurred**
71
+
72
+ \`\`\`
73
+ ${error.message}
74
+ \`\`\`
75
+
76
+ React :arrows_counterclockwise: to retry or send a new message.`;
77
+
78
+ try {
79
+ await this.mmClient.createPost(session.dmChannelId, message);
80
+ } catch (err) {
81
+ log.error("[NotificationService] Failed to send error notification:", err);
82
+ }
83
+ }
84
+
85
+ async notifyStatus(session: UserSession, status: StatusUpdate): Promise<void> {
86
+ if (!this.config.onStatusUpdate) return;
87
+
88
+ const statusEmoji = {
89
+ thinking: ":thought_balloon:",
90
+ tool_execution: ":hammer_and_wrench:",
91
+ waiting: ":hourglass_flowing_sand:",
92
+ idle: ":zzz:",
93
+ }[status.type];
94
+
95
+ const message = `${statusEmoji} **${status.type.replace("_", " ")}**${status.details ? `\n${status.details}` : ""}`;
96
+
97
+ try {
98
+ await this.mmClient.createPost(session.dmChannelId, message);
99
+ } catch (error) {
100
+ log.error("[NotificationService] Failed to send status notification:", error);
101
+ }
102
+ }
103
+
104
+ async notifySessionTakeover(session: UserSession, newProject: string): Promise<void> {
105
+ const message = `:information_source: **Session Transferred**
106
+
107
+ Mattermost control has been transferred to another OpenCode instance.
108
+ New project: \`${newProject}\`
109
+
110
+ DMs will now be handled by the new session.`;
111
+
112
+ try {
113
+ await this.mmClient.createPost(session.dmChannelId, message);
114
+ } catch (error) {
115
+ log.error("[NotificationService] Failed to send takeover notification:", error);
116
+ }
117
+ }
118
+ }