@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,370 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Information about an available OpenCode session
|
|
5
|
+
*/
|
|
6
|
+
export interface OpenCodeSessionInfo {
|
|
7
|
+
id: string;
|
|
8
|
+
projectName: string;
|
|
9
|
+
directory: string;
|
|
10
|
+
shortId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
lastUpdated: Date;
|
|
13
|
+
isAvailable: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* OpenCode SDK Session type (matches SDK types.gen.d.ts Session type)
|
|
18
|
+
*/
|
|
19
|
+
export interface OpenCodeSession {
|
|
20
|
+
id: string;
|
|
21
|
+
slug?: string;
|
|
22
|
+
projectID?: string;
|
|
23
|
+
directory: string;
|
|
24
|
+
parentID?: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
time: {
|
|
27
|
+
created: number;
|
|
28
|
+
updated: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Interface for OpenCode client session operations
|
|
34
|
+
*/
|
|
35
|
+
export interface OpenCodeClientSession {
|
|
36
|
+
list(): Promise<{ data: OpenCodeSession[] | undefined }>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type NewSessionCallback = (session: OpenCodeSessionInfo) => void | Promise<void>;
|
|
40
|
+
export type SessionDeletedCallback = (sessionId: string, sessionInfo: OpenCodeSessionInfo) => void | Promise<void>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Registry for tracking available OpenCode sessions.
|
|
44
|
+
* Provides session discovery, lookup, and availability tracking.
|
|
45
|
+
*/
|
|
46
|
+
export class OpenCodeSessionRegistry {
|
|
47
|
+
private sessions: Map<string, OpenCodeSessionInfo> = new Map();
|
|
48
|
+
private defaultSessionId: string | null = null;
|
|
49
|
+
private refreshIntervalMs: number;
|
|
50
|
+
private refreshTimer: ReturnType<typeof setInterval> | null = null;
|
|
51
|
+
private client: OpenCodeClientSession | null = null;
|
|
52
|
+
private newSessionCallbacks: NewSessionCallback[] = [];
|
|
53
|
+
private sessionDeletedCallbacks: SessionDeletedCallback[] = [];
|
|
54
|
+
|
|
55
|
+
constructor(refreshIntervalMs: number = 60000) {
|
|
56
|
+
this.refreshIntervalMs = refreshIntervalMs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onNewSession(callback: NewSessionCallback): void {
|
|
60
|
+
this.newSessionCallbacks.push(callback);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onSessionDeleted(callback: SessionDeletedCallback): void {
|
|
64
|
+
this.sessionDeletedCallbacks.push(callback);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async notifyNewSession(session: OpenCodeSessionInfo): Promise<void> {
|
|
68
|
+
for (const cb of this.newSessionCallbacks) {
|
|
69
|
+
try {
|
|
70
|
+
await cb(session);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
log.error("[OpenCodeSessionRegistry] New session callback error:", e);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private async notifySessionDeleted(sessionId: string, sessionInfo: OpenCodeSessionInfo): Promise<void> {
|
|
78
|
+
for (const cb of this.sessionDeletedCallbacks) {
|
|
79
|
+
try {
|
|
80
|
+
await cb(sessionId, sessionInfo);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
log.error("[OpenCodeSessionRegistry] Session deleted callback error:", e);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Initialize the registry with an OpenCode client
|
|
89
|
+
*/
|
|
90
|
+
initialize(client: OpenCodeClientSession): void {
|
|
91
|
+
this.client = client;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Start automatic background refresh of session list
|
|
96
|
+
*/
|
|
97
|
+
startAutoRefresh(): void {
|
|
98
|
+
if (this.refreshTimer) return;
|
|
99
|
+
|
|
100
|
+
this.refreshTimer = setInterval(async () => {
|
|
101
|
+
try {
|
|
102
|
+
await this.refresh();
|
|
103
|
+
} catch (e) {
|
|
104
|
+
log.error("[OpenCodeSessionRegistry] Auto-refresh failed:", e);
|
|
105
|
+
}
|
|
106
|
+
}, this.refreshIntervalMs);
|
|
107
|
+
|
|
108
|
+
log.debug(`[OpenCodeSessionRegistry] Auto-refresh started (interval: ${this.refreshIntervalMs}ms)`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Stop automatic background refresh
|
|
113
|
+
*/
|
|
114
|
+
stopAutoRefresh(): void {
|
|
115
|
+
if (this.refreshTimer) {
|
|
116
|
+
clearInterval(this.refreshTimer);
|
|
117
|
+
this.refreshTimer = null;
|
|
118
|
+
log.debug("[OpenCodeSessionRegistry] Auto-refresh stopped");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Refresh the session list from OpenCode API
|
|
124
|
+
*/
|
|
125
|
+
async refresh(): Promise<void> {
|
|
126
|
+
if (!this.client) {
|
|
127
|
+
throw new Error("Registry not initialized - call initialize() first");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const result = await this.client.list();
|
|
132
|
+
const sessions = result.data;
|
|
133
|
+
|
|
134
|
+
if (!sessions || !Array.isArray(sessions) || sessions.length === 0) {
|
|
135
|
+
log.debug("[OpenCodeSessionRegistry] No sessions found");
|
|
136
|
+
this.sessions.clear();
|
|
137
|
+
this.defaultSessionId = null;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const currentIds = new Set<string>();
|
|
142
|
+
|
|
143
|
+
for (const session of sessions) {
|
|
144
|
+
if (session.parentID) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
currentIds.add(session.id);
|
|
149
|
+
|
|
150
|
+
const projectName = this.extractProjectName(session.directory);
|
|
151
|
+
const shortId = session.slug || session.id.substring(0, 8);
|
|
152
|
+
|
|
153
|
+
const existing = this.sessions.get(session.id);
|
|
154
|
+
|
|
155
|
+
const sessionInfo: OpenCodeSessionInfo = {
|
|
156
|
+
id: session.id,
|
|
157
|
+
projectName,
|
|
158
|
+
directory: session.directory,
|
|
159
|
+
shortId,
|
|
160
|
+
title: session.title || projectName,
|
|
161
|
+
lastUpdated: new Date(session.time.updated),
|
|
162
|
+
isAvailable: true,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
this.sessions.set(session.id, sessionInfo);
|
|
166
|
+
|
|
167
|
+
if (!existing) {
|
|
168
|
+
log.info(`[OpenCodeSessionRegistry] New session discovered: ${shortId} (${projectName})`);
|
|
169
|
+
this.notifyNewSession(sessionInfo).catch((e) =>
|
|
170
|
+
log.error("[OpenCodeSessionRegistry] Failed to notify new session:", e)
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const [id, info] of this.sessions.entries()) {
|
|
176
|
+
if (!currentIds.has(id) && info.isAvailable) {
|
|
177
|
+
info.isAvailable = false;
|
|
178
|
+
log.info(`[OpenCodeSessionRegistry] Session no longer available: ${info.shortId} (${info.projectName})`);
|
|
179
|
+
this.notifySessionDeleted(id, info).catch((e) =>
|
|
180
|
+
log.error("[OpenCodeSessionRegistry] Failed to notify session unavailable:", e)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const available = this.list().filter(s => s.isAvailable);
|
|
186
|
+
if (available.length > 0) {
|
|
187
|
+
const sorted = available.sort((a, b) => b.lastUpdated.getTime() - a.lastUpdated.getTime());
|
|
188
|
+
this.defaultSessionId = sorted[0].id;
|
|
189
|
+
} else {
|
|
190
|
+
this.defaultSessionId = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
log.debug(`[OpenCodeSessionRegistry] Refreshed: ${available.length} available sessions`);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
log.error("[OpenCodeSessionRegistry] Failed to refresh sessions:", e);
|
|
196
|
+
throw e;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get all tracked sessions
|
|
202
|
+
*/
|
|
203
|
+
list(): OpenCodeSessionInfo[] {
|
|
204
|
+
return Array.from(this.sessions.values());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get all available sessions (excludes unavailable ones)
|
|
209
|
+
*/
|
|
210
|
+
listAvailable(): OpenCodeSessionInfo[] {
|
|
211
|
+
return this.list().filter(s => s.isAvailable);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Get session by full ID or short ID (first 6 chars)
|
|
216
|
+
*/
|
|
217
|
+
get(idOrShortId: string): OpenCodeSessionInfo | null {
|
|
218
|
+
const exactMatch = this.sessions.get(idOrShortId);
|
|
219
|
+
if (exactMatch) return exactMatch;
|
|
220
|
+
|
|
221
|
+
const normalized = idOrShortId.toLowerCase();
|
|
222
|
+
|
|
223
|
+
for (const session of this.sessions.values()) {
|
|
224
|
+
const matchesShortId = session.shortId.toLowerCase() === normalized;
|
|
225
|
+
const matchesIdPrefix = session.id.toLowerCase().startsWith(normalized);
|
|
226
|
+
if (matchesShortId || matchesIdPrefix) {
|
|
227
|
+
return session;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const session of this.sessions.values()) {
|
|
232
|
+
if (session.projectName.toLowerCase().includes(normalized)) {
|
|
233
|
+
return session;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get the default session (most recently updated available session)
|
|
242
|
+
*/
|
|
243
|
+
getDefault(): OpenCodeSessionInfo | null {
|
|
244
|
+
if (!this.defaultSessionId) return null;
|
|
245
|
+
return this.sessions.get(this.defaultSessionId) || null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Explicitly set the default session
|
|
250
|
+
*/
|
|
251
|
+
setDefault(sessionId: string): boolean {
|
|
252
|
+
const session = this.get(sessionId);
|
|
253
|
+
if (!session) return false;
|
|
254
|
+
|
|
255
|
+
this.defaultSessionId = session.id;
|
|
256
|
+
log.info(`[OpenCodeSessionRegistry] Default session set to: ${session.shortId} (${session.projectName})`);
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Mark a session as unavailable (e.g., when a prompt fails)
|
|
262
|
+
*/
|
|
263
|
+
markUnavailable(sessionId: string): void {
|
|
264
|
+
const session = this.sessions.get(sessionId);
|
|
265
|
+
if (session) {
|
|
266
|
+
session.isAvailable = false;
|
|
267
|
+
log.info(`[OpenCodeSessionRegistry] Session marked unavailable: ${session.shortId} (${session.projectName})`);
|
|
268
|
+
|
|
269
|
+
if (this.defaultSessionId === sessionId) {
|
|
270
|
+
const available = this.listAvailable();
|
|
271
|
+
this.defaultSessionId = available.length > 0 ? available[0].id : null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
handleSessionCreated(session: OpenCodeSession): void {
|
|
277
|
+
if (session.parentID) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const existing = this.sessions.get(session.id);
|
|
282
|
+
if (existing) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const projectName = this.extractProjectName(session.directory);
|
|
287
|
+
const shortId = session.slug || session.id.substring(0, 8);
|
|
288
|
+
|
|
289
|
+
const sessionInfo: OpenCodeSessionInfo = {
|
|
290
|
+
id: session.id,
|
|
291
|
+
projectName,
|
|
292
|
+
directory: session.directory,
|
|
293
|
+
shortId,
|
|
294
|
+
title: session.title || projectName,
|
|
295
|
+
lastUpdated: new Date(session.time.updated),
|
|
296
|
+
isAvailable: true,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
this.sessions.set(session.id, sessionInfo);
|
|
300
|
+
|
|
301
|
+
log.info(`[OpenCodeSessionRegistry] Session created: ${shortId} (${projectName})`);
|
|
302
|
+
|
|
303
|
+
if (!this.defaultSessionId) {
|
|
304
|
+
this.defaultSessionId = session.id;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.notifyNewSession(sessionInfo).catch((e) =>
|
|
308
|
+
log.error("[OpenCodeSessionRegistry] Failed to notify new session:", e)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Handle session.deleted event from OpenCode
|
|
314
|
+
*/
|
|
315
|
+
handleSessionDeleted(sessionId: string): void {
|
|
316
|
+
const session = this.sessions.get(sessionId);
|
|
317
|
+
if (session) {
|
|
318
|
+
this.sessions.delete(sessionId);
|
|
319
|
+
log.info(`[OpenCodeSessionRegistry] Session deleted: ${session.shortId} (${session.projectName})`);
|
|
320
|
+
|
|
321
|
+
if (this.defaultSessionId === sessionId) {
|
|
322
|
+
const available = this.listAvailable();
|
|
323
|
+
this.defaultSessionId = available.length > 0 ? available[0].id : null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.notifySessionDeleted(sessionId, session).catch((e) =>
|
|
327
|
+
log.error("[OpenCodeSessionRegistry] Failed to notify session deleted:", e)
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Check if a session exists and is available
|
|
334
|
+
*/
|
|
335
|
+
isAvailable(sessionId: string): boolean {
|
|
336
|
+
const session = this.get(sessionId);
|
|
337
|
+
return session?.isAvailable ?? false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Get session count
|
|
342
|
+
*/
|
|
343
|
+
count(): number {
|
|
344
|
+
return this.sessions.size;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get available session count
|
|
349
|
+
*/
|
|
350
|
+
countAvailable(): number {
|
|
351
|
+
return this.listAvailable().length;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Clear all sessions (for disconnect)
|
|
356
|
+
*/
|
|
357
|
+
clear(): void {
|
|
358
|
+
this.sessions.clear();
|
|
359
|
+
this.defaultSessionId = null;
|
|
360
|
+
this.stopAutoRefresh();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Extract project name from directory path
|
|
365
|
+
*/
|
|
366
|
+
private extractProjectName(directory: string): string {
|
|
367
|
+
const parts = directory.split("/").filter(Boolean);
|
|
368
|
+
return parts[parts.length - 1] || "unknown";
|
|
369
|
+
}
|
|
370
|
+
}
|