@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,183 @@
|
|
|
1
|
+
import { tool, type ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
+
import { PluginState } from "../state.js";
|
|
3
|
+
import { MattermostClient } from "../../../../src/clients/mattermost-client.js";
|
|
4
|
+
import { MonitorService, type MonitoredSession } from "../../../../src/monitor-service.js";
|
|
5
|
+
import { loadConfig } from "../../../../src/config.js";
|
|
6
|
+
import { log } from "../../../../src/logger.js";
|
|
7
|
+
|
|
8
|
+
export interface MonitorContext {
|
|
9
|
+
client: any;
|
|
10
|
+
directory: string;
|
|
11
|
+
projectName: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createMonitorTool(ctx: MonitorContext): ToolDefinition {
|
|
15
|
+
return tool({
|
|
16
|
+
description: "Monitor an OpenCode session for events (permission requests, idle, questions). Sends DM alerts when the session needs attention.",
|
|
17
|
+
args: {
|
|
18
|
+
sessionId: tool.schema.string().optional().describe("Session ID to monitor. Defaults to current session if not specified."),
|
|
19
|
+
targetUser: tool.schema.string().optional().describe("Mattermost username to notify (required if not connected to Mattermost)."),
|
|
20
|
+
persistent: tool.schema.boolean().optional().describe("Keep monitoring after each alert (default: true). Set to false for one-time alerts."),
|
|
21
|
+
},
|
|
22
|
+
async execute(args) {
|
|
23
|
+
const config = loadConfig();
|
|
24
|
+
|
|
25
|
+
if (!config.mattermost.token) {
|
|
26
|
+
return "MATTERMOST_TOKEN environment variable is required.";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (config.mattermost.baseUrl.includes("your-mattermost-instance.example.com")) {
|
|
30
|
+
return "MATTERMOST_URL environment variable is required.";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let targetSessionId = args.sessionId;
|
|
34
|
+
let targetProjectName = ctx.projectName;
|
|
35
|
+
let targetDirectory = ctx.directory;
|
|
36
|
+
let targetSessionTitle: string | undefined;
|
|
37
|
+
|
|
38
|
+
if (!targetSessionId) {
|
|
39
|
+
targetSessionId = await resolveCurrentSession(ctx.client, ctx.directory);
|
|
40
|
+
} else if (PluginState.openCodeSessionRegistry) {
|
|
41
|
+
const session = PluginState.openCodeSessionRegistry.get(targetSessionId);
|
|
42
|
+
if (session) {
|
|
43
|
+
targetSessionId = session.id;
|
|
44
|
+
targetProjectName = session.projectName;
|
|
45
|
+
targetDirectory = session.directory;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!targetSessionId) {
|
|
50
|
+
return "No session ID provided and could not determine current session.";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const sessionDetails = await ctx.client.session.get({ path: { id: targetSessionId } });
|
|
55
|
+
if (sessionDetails.data) {
|
|
56
|
+
targetSessionTitle = sessionDetails.data.title;
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
log.debug(`[Monitor] Could not fetch session details: ${e}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (MonitorService.isMonitored(targetSessionId)) {
|
|
63
|
+
return `Session ${targetSessionId.substring(0, 8)} is already being monitored.`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let mattermostUserId: string;
|
|
67
|
+
let mattermostUsername: string;
|
|
68
|
+
|
|
69
|
+
if (args.targetUser) {
|
|
70
|
+
try {
|
|
71
|
+
const tempClient = new MattermostClient(config.mattermost);
|
|
72
|
+
const user = await tempClient.getUserByUsername(args.targetUser.replace(/^@/, ""));
|
|
73
|
+
mattermostUserId = user.id;
|
|
74
|
+
mattermostUsername = user.username;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
return `Could not find Mattermost user: ${args.targetUser}`;
|
|
77
|
+
}
|
|
78
|
+
} else if (PluginState.botUser) {
|
|
79
|
+
mattermostUserId = PluginState.botUser.id;
|
|
80
|
+
mattermostUsername = PluginState.botUser.username;
|
|
81
|
+
} else {
|
|
82
|
+
return "targetUser is required when not connected to Mattermost. Specify the Mattermost username to notify.";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isPersistent = args.persistent !== false;
|
|
86
|
+
|
|
87
|
+
const monitoredSession: MonitoredSession = {
|
|
88
|
+
sessionId: targetSessionId,
|
|
89
|
+
shortId: targetSessionId.substring(0, 8),
|
|
90
|
+
mattermostUserId,
|
|
91
|
+
mattermostUsername,
|
|
92
|
+
projectName: targetProjectName,
|
|
93
|
+
sessionTitle: targetSessionTitle,
|
|
94
|
+
directory: targetDirectory,
|
|
95
|
+
registeredAt: new Date(),
|
|
96
|
+
persistent: isPersistent,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
MonitorService.register(monitoredSession);
|
|
100
|
+
|
|
101
|
+
const modeText = isPersistent
|
|
102
|
+
? "_Persistent monitoring enabled. Use `mattermost_unmonitor` to stop._"
|
|
103
|
+
: "_One-time alert. After notification, monitoring stops._";
|
|
104
|
+
|
|
105
|
+
return `Monitoring session ${monitoredSession.shortId} (${targetProjectName})\nWill alert @${mattermostUsername} on permission request, idle, or question\n\n${modeText}`;
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createUnmonitorTool(client: any): ToolDefinition {
|
|
111
|
+
return tool({
|
|
112
|
+
description: "Stop monitoring an OpenCode session. Stops all alerts for the specified or current session.",
|
|
113
|
+
args: {
|
|
114
|
+
sessionId: tool.schema.string().optional().describe("Session ID to stop monitoring. Defaults to current session if not specified."),
|
|
115
|
+
},
|
|
116
|
+
async execute(args) {
|
|
117
|
+
let targetSessionId = args.sessionId;
|
|
118
|
+
|
|
119
|
+
if (!targetSessionId) {
|
|
120
|
+
try {
|
|
121
|
+
const statusResult = await client.session.status();
|
|
122
|
+
const statusMap = statusResult.data as Record<string, { type: string }> | undefined;
|
|
123
|
+
if (statusMap && Object.keys(statusMap).length > 0) {
|
|
124
|
+
const activeSessionIds = Object.keys(statusMap);
|
|
125
|
+
const busySessionId = activeSessionIds.find(id => statusMap[id]?.type === 'busy');
|
|
126
|
+
targetSessionId = busySessionId || activeSessionIds[0];
|
|
127
|
+
}
|
|
128
|
+
} catch (e) {
|
|
129
|
+
log.warn("Failed to get session status:", e);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (!targetSessionId) {
|
|
134
|
+
return "No session ID provided and could not determine current session.";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!MonitorService.isMonitored(targetSessionId)) {
|
|
138
|
+
return `Session ${targetSessionId.substring(0, 8)} is not being monitored.`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
MonitorService.unregister(targetSessionId);
|
|
142
|
+
return `Stopped monitoring session ${targetSessionId.substring(0, 8)}`;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function resolveCurrentSession(client: any, directory: string): Promise<string | undefined> {
|
|
148
|
+
if (PluginState.openCodeSessionRegistry) {
|
|
149
|
+
const defaultSession = PluginState.openCodeSessionRegistry.getDefault();
|
|
150
|
+
if (defaultSession) {
|
|
151
|
+
return defaultSession.id;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const statusResult = await client.session.status();
|
|
157
|
+
const statusMap = statusResult.data as Record<string, { type: string }> | undefined;
|
|
158
|
+
|
|
159
|
+
if (statusMap && Object.keys(statusMap).length > 0) {
|
|
160
|
+
const activeSessionIds = Object.keys(statusMap);
|
|
161
|
+
const busySessionId = activeSessionIds.find(id => statusMap[id]?.type === 'busy');
|
|
162
|
+
return busySessionId || activeSessionIds[0];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const sessions = await client.session.list();
|
|
166
|
+
if (sessions.data && sessions.data.length > 0) {
|
|
167
|
+
const sortedSessions = [...sessions.data]
|
|
168
|
+
.filter((s: any) => s.directory === directory)
|
|
169
|
+
.sort((a: any, b: any) => {
|
|
170
|
+
const timeA = a.time?.updated || a.time?.created || 0;
|
|
171
|
+
const timeB = b.time?.updated || b.time?.created || 0;
|
|
172
|
+
return timeB - timeA;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const currentSession = sortedSessions[0] || sessions.data[0];
|
|
176
|
+
return currentSession.id;
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
log.warn("Failed to get session:", e);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { tool, type ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
+
import { PluginState } from "../state.js";
|
|
3
|
+
import { MattermostClient } from "../../../../src/clients/mattermost-client.js";
|
|
4
|
+
import { getSchedulerService } from "../../../../src/scheduler/scheduler-service.js";
|
|
5
|
+
import { loadConfig } from "../../../../src/config.js";
|
|
6
|
+
import { log } from "../../../../src/logger.js";
|
|
7
|
+
|
|
8
|
+
export interface ScheduleToolContext {
|
|
9
|
+
client: any;
|
|
10
|
+
directory: string;
|
|
11
|
+
projectName: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createScheduleAddTool(ctx: ScheduleToolContext): ToolDefinition {
|
|
15
|
+
return tool({
|
|
16
|
+
description: "Create a scheduled task that runs a prompt at specified times and DMs you the response. The prompt is injected into the current session at each scheduled time.",
|
|
17
|
+
args: {
|
|
18
|
+
name: tool.schema.string().describe("Unique name for this schedule (e.g., 'morning-todos', 'pr-check')"),
|
|
19
|
+
cron: tool.schema.string().describe("Cron expression for when to run (e.g., '0 8,20 * * *' for 8am and 8pm, '0 */4 * * *' for every 4 hours)"),
|
|
20
|
+
prompt: tool.schema.string().describe("The prompt to send to the LLM at each scheduled time"),
|
|
21
|
+
timezone: tool.schema.string().optional().describe("Timezone for the schedule (default: UTC). Examples: 'America/New_York', 'Europe/London'"),
|
|
22
|
+
targetUser: tool.schema.string().optional().describe("Mattermost username to DM results to. Defaults to current user if connected."),
|
|
23
|
+
},
|
|
24
|
+
async execute(args) {
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
const scheduler = getSchedulerService();
|
|
27
|
+
|
|
28
|
+
if (!config.mattermost.token) {
|
|
29
|
+
return "MATTERMOST_TOKEN environment variable is required.";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let sessionId: string | undefined;
|
|
33
|
+
|
|
34
|
+
if (PluginState.openCodeSessionRegistry) {
|
|
35
|
+
const defaultSession = PluginState.openCodeSessionRegistry.getDefault();
|
|
36
|
+
if (defaultSession) {
|
|
37
|
+
sessionId = defaultSession.id;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!sessionId) {
|
|
42
|
+
try {
|
|
43
|
+
const sessions = await ctx.client.session.list();
|
|
44
|
+
if (sessions.data && sessions.data.length > 0) {
|
|
45
|
+
const dirSessions = sessions.data.filter((s: any) => s.directory === ctx.directory);
|
|
46
|
+
const session = dirSessions[0] || sessions.data[0];
|
|
47
|
+
sessionId = session.id;
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
log.warn("[ScheduleTool] Failed to get session:", e);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!sessionId) {
|
|
55
|
+
return "Could not determine current session. Please ensure an OpenCode session is active.";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let targetUserId: string;
|
|
59
|
+
let targetUsername: string;
|
|
60
|
+
|
|
61
|
+
if (args.targetUser) {
|
|
62
|
+
try {
|
|
63
|
+
const tempClient = new MattermostClient(config.mattermost);
|
|
64
|
+
const user = await tempClient.getUserByUsername(args.targetUser.replace(/^@/, ""));
|
|
65
|
+
targetUserId = user.id;
|
|
66
|
+
targetUsername = user.username;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
return `Could not find Mattermost user: ${args.targetUser}`;
|
|
69
|
+
}
|
|
70
|
+
} else if (PluginState.sessionManager) {
|
|
71
|
+
const sessions = Array.from((PluginState.sessionManager as any).sessions?.values() || []);
|
|
72
|
+
const userSession = sessions[0] as any;
|
|
73
|
+
if (userSession) {
|
|
74
|
+
targetUserId = userSession.mattermostUserId;
|
|
75
|
+
targetUsername = userSession.mattermostUsername;
|
|
76
|
+
} else {
|
|
77
|
+
return "No user session found. Please specify targetUser or ensure you're connected via Mattermost.";
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
return "targetUser is required when session manager is not available.";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const schedule = await scheduler.addSchedule({
|
|
85
|
+
name: args.name,
|
|
86
|
+
cron: args.cron,
|
|
87
|
+
timezone: args.timezone,
|
|
88
|
+
prompt: args.prompt,
|
|
89
|
+
sessionId,
|
|
90
|
+
targetUserId,
|
|
91
|
+
targetUsername,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const lines = [
|
|
95
|
+
`Schedule created: **${schedule.name}**`,
|
|
96
|
+
"",
|
|
97
|
+
`| Property | Value |`,
|
|
98
|
+
`|----------|-------|`,
|
|
99
|
+
`| Cron | \`${schedule.cron}\` |`,
|
|
100
|
+
`| Timezone | ${schedule.timezone} |`,
|
|
101
|
+
`| Session | \`${schedule.sessionId.slice(0, 8)}\` |`,
|
|
102
|
+
`| Notify | @${schedule.targetUsername} |`,
|
|
103
|
+
"",
|
|
104
|
+
`**Prompt:** ${schedule.prompt.slice(0, 100)}${schedule.prompt.length > 100 ? "..." : ""}`,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
110
|
+
return `Failed to create schedule: ${msg}`;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createScheduleListTool(): ToolDefinition {
|
|
117
|
+
return tool({
|
|
118
|
+
description: "List all scheduled tasks with their status and next run time.",
|
|
119
|
+
args: {},
|
|
120
|
+
async execute() {
|
|
121
|
+
const scheduler = getSchedulerService();
|
|
122
|
+
const schedules = scheduler.listSchedules();
|
|
123
|
+
|
|
124
|
+
if (schedules.length === 0) {
|
|
125
|
+
return "No schedules configured. Use `mattermost_schedule_add` to create one.";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const lines = [
|
|
129
|
+
`**Scheduled Tasks (${schedules.length})**`,
|
|
130
|
+
"",
|
|
131
|
+
"| Name | Cron | Status | Last Run | Notify |",
|
|
132
|
+
"|------|------|--------|----------|--------|",
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
for (const s of schedules) {
|
|
136
|
+
const status = s.enabled ? (scheduler.isRunning(s.id) ? ":green_circle: Active" : ":yellow_circle: Pending") : ":red_circle: Disabled";
|
|
137
|
+
const lastRun = s.lastRunAt
|
|
138
|
+
? new Date(s.lastRunAt).toLocaleString() + (s.lastRunSuccess === false ? " :x:" : " :white_check_mark:")
|
|
139
|
+
: "Never";
|
|
140
|
+
lines.push(`| ${s.name} | \`${s.cron}\` | ${status} | ${lastRun} | @${s.targetUsername} |`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return lines.join("\n");
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createScheduleRemoveTool(): ToolDefinition {
|
|
149
|
+
return tool({
|
|
150
|
+
description: "Remove a scheduled task by name or ID.",
|
|
151
|
+
args: {
|
|
152
|
+
name: tool.schema.string().describe("Name or ID of the schedule to remove"),
|
|
153
|
+
},
|
|
154
|
+
async execute(args) {
|
|
155
|
+
const scheduler = getSchedulerService();
|
|
156
|
+
|
|
157
|
+
let removed = scheduler.removeScheduleByName(args.name);
|
|
158
|
+
if (!removed) {
|
|
159
|
+
removed = scheduler.removeSchedule(args.name);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (removed) {
|
|
163
|
+
return `Schedule "${args.name}" removed.`;
|
|
164
|
+
} else {
|
|
165
|
+
return `Schedule "${args.name}" not found.`;
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function createScheduleEnableTool(): ToolDefinition {
|
|
172
|
+
return tool({
|
|
173
|
+
description: "Enable a disabled scheduled task.",
|
|
174
|
+
args: {
|
|
175
|
+
name: tool.schema.string().describe("Name or ID of the schedule to enable"),
|
|
176
|
+
},
|
|
177
|
+
async execute(args) {
|
|
178
|
+
const scheduler = getSchedulerService();
|
|
179
|
+
|
|
180
|
+
let schedule = scheduler.getScheduleByName(args.name);
|
|
181
|
+
if (!schedule) {
|
|
182
|
+
schedule = scheduler.getSchedule(args.name);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!schedule) {
|
|
186
|
+
return `Schedule "${args.name}" not found.`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (schedule.enabled) {
|
|
190
|
+
return `Schedule "${schedule.name}" is already enabled.`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
scheduler.enableSchedule(schedule.id);
|
|
194
|
+
return `Schedule "${schedule.name}" enabled.`;
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function createScheduleDisableTool(): ToolDefinition {
|
|
200
|
+
return tool({
|
|
201
|
+
description: "Disable a scheduled task without deleting it.",
|
|
202
|
+
args: {
|
|
203
|
+
name: tool.schema.string().describe("Name or ID of the schedule to disable"),
|
|
204
|
+
},
|
|
205
|
+
async execute(args) {
|
|
206
|
+
const scheduler = getSchedulerService();
|
|
207
|
+
|
|
208
|
+
let schedule = scheduler.getScheduleByName(args.name);
|
|
209
|
+
if (!schedule) {
|
|
210
|
+
schedule = scheduler.getSchedule(args.name);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!schedule) {
|
|
214
|
+
return `Schedule "${args.name}" not found.`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!schedule.enabled) {
|
|
218
|
+
return `Schedule "${schedule.name}" is already disabled.`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
scheduler.disableSchedule(schedule.id);
|
|
222
|
+
return `Schedule "${schedule.name}" disabled.`;
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function createScheduleRunTool(): ToolDefinition {
|
|
228
|
+
return tool({
|
|
229
|
+
description: "Run a scheduled task immediately for testing purposes.",
|
|
230
|
+
args: {
|
|
231
|
+
name: tool.schema.string().describe("Name or ID of the schedule to run"),
|
|
232
|
+
},
|
|
233
|
+
async execute(args) {
|
|
234
|
+
const scheduler = getSchedulerService();
|
|
235
|
+
|
|
236
|
+
let schedule = scheduler.getScheduleByName(args.name);
|
|
237
|
+
if (!schedule) {
|
|
238
|
+
schedule = scheduler.getSchedule(args.name);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!schedule) {
|
|
242
|
+
return `Schedule "${args.name}" not found.`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const ran = await scheduler.runNow(schedule.id);
|
|
246
|
+
if (ran) {
|
|
247
|
+
return `Schedule "${schedule.name}" executed. Check your Mattermost DMs for the result.`;
|
|
248
|
+
} else {
|
|
249
|
+
return `Failed to execute schedule "${schedule.name}".`;
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { tool, type ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
+
import { PluginState } from "../state.js";
|
|
3
|
+
import { log } from "../../../../src/logger.js";
|
|
4
|
+
|
|
5
|
+
export function createListSessionsTool() {
|
|
6
|
+
return {
|
|
7
|
+
description: "List available OpenCode sessions that can receive prompts from Mattermost",
|
|
8
|
+
args: {},
|
|
9
|
+
async execute() {
|
|
10
|
+
const { isConnected, openCodeSessionRegistry } = PluginState;
|
|
11
|
+
|
|
12
|
+
if (!isConnected || !openCodeSessionRegistry) {
|
|
13
|
+
return "Not connected to Mattermost. Use mattermost_connect first.";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await openCodeSessionRegistry.refresh();
|
|
18
|
+
} catch (e) {
|
|
19
|
+
log.warn("Failed to refresh sessions:", e);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sessions = openCodeSessionRegistry.listAvailable();
|
|
23
|
+
if (sessions.length === 0) {
|
|
24
|
+
return "No active OpenCode sessions found.";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const defaultSession = openCodeSessionRegistry.getDefault();
|
|
28
|
+
const lines = sessions.map((s, i) => {
|
|
29
|
+
const isDefault = s.id === defaultSession?.id;
|
|
30
|
+
return `${i + 1}. ${s.projectName} (${s.shortId})${isDefault ? " [default]" : ""}\n Directory: ${s.directory}`;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return `Available OpenCode Sessions:\n\n${lines.join("\n\n")}`;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createSelectSessionTool(): ToolDefinition {
|
|
39
|
+
return tool({
|
|
40
|
+
description: "Select which OpenCode session should receive prompts from a Mattermost user",
|
|
41
|
+
args: {
|
|
42
|
+
sessionId: tool.schema.string().describe("Session ID (full or short 6-char ID) or project name"),
|
|
43
|
+
mattermostUserId: tool.schema.string().optional().describe("Mattermost user ID to set session for (optional, defaults to all users)"),
|
|
44
|
+
},
|
|
45
|
+
async execute(args) {
|
|
46
|
+
const { isConnected, openCodeSessionRegistry, sessionManager } = PluginState;
|
|
47
|
+
|
|
48
|
+
if (!isConnected || !openCodeSessionRegistry || !sessionManager) {
|
|
49
|
+
return "Not connected to Mattermost. Use mattermost_connect first.";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const session = openCodeSessionRegistry.get(args.sessionId);
|
|
53
|
+
if (!session) {
|
|
54
|
+
return `Session not found: ${args.sessionId}. Use mattermost_list_sessions to see available sessions.`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!session.isAvailable) {
|
|
58
|
+
return `Session ${session.shortId} (${session.projectName}) is not available.`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (args.mattermostUserId) {
|
|
62
|
+
const userSession = sessionManager.getSession(args.mattermostUserId);
|
|
63
|
+
if (userSession) {
|
|
64
|
+
userSession.targetOpenCodeSessionId = session.id;
|
|
65
|
+
return `Set session ${session.shortId} (${session.projectName}) as target for Mattermost user.`;
|
|
66
|
+
}
|
|
67
|
+
return `Mattermost user session not found. User must DM the bot first.`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
openCodeSessionRegistry.setDefault(session.id);
|
|
71
|
+
return `Set ${session.shortId} (${session.projectName}) as the default OpenCode session for all Mattermost users.`;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createCurrentSessionTool(): ToolDefinition {
|
|
77
|
+
return tool({
|
|
78
|
+
description: "Show the currently targeted OpenCode session for a Mattermost user",
|
|
79
|
+
args: {
|
|
80
|
+
mattermostUserId: tool.schema.string().optional().describe("Mattermost user ID to check (optional, shows default if not specified)"),
|
|
81
|
+
},
|
|
82
|
+
async execute(args) {
|
|
83
|
+
const { isConnected, openCodeSessionRegistry, sessionManager } = PluginState;
|
|
84
|
+
|
|
85
|
+
if (!isConnected || !openCodeSessionRegistry || !sessionManager) {
|
|
86
|
+
return "Not connected to Mattermost. Use mattermost_connect first.";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (args.mattermostUserId) {
|
|
90
|
+
const userSession = sessionManager.getSession(args.mattermostUserId);
|
|
91
|
+
if (!userSession) {
|
|
92
|
+
return `No active Mattermost session for user ${args.mattermostUserId}. User must DM the bot first.`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const targetId = userSession.targetOpenCodeSessionId;
|
|
96
|
+
if (!targetId) {
|
|
97
|
+
const defaultSession = openCodeSessionRegistry.getDefault();
|
|
98
|
+
if (defaultSession) {
|
|
99
|
+
return `User @${userSession.mattermostUsername} has no explicit session selected.\nUsing default: ${defaultSession.projectName} (${defaultSession.shortId})\nDirectory: ${defaultSession.directory}`;
|
|
100
|
+
}
|
|
101
|
+
return `User @${userSession.mattermostUsername} has no session selected and no default is available.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const session = openCodeSessionRegistry.get(targetId);
|
|
105
|
+
if (!session || !session.isAvailable) {
|
|
106
|
+
return `User @${userSession.mattermostUsername}'s selected session is no longer available.`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return `User @${userSession.mattermostUsername} is targeting:\nProject: ${session.projectName}\nID: ${session.shortId}\nDirectory: ${session.directory}\nLast updated: ${session.lastUpdated.toISOString()}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const defaultSession = openCodeSessionRegistry.getDefault();
|
|
113
|
+
if (!defaultSession) {
|
|
114
|
+
return "No default OpenCode session is set. Use mattermost_list_sessions to see available sessions.";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return `Default OpenCode session:\nProject: ${defaultSession.projectName}\nID: ${defaultSession.shortId}\nDirectory: ${defaultSession.directory}\nLast updated: ${defaultSession.lastUpdated.toISOString()}`;
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the Mattermost Control Plugin
|
|
3
|
+
*
|
|
4
|
+
* This module contains all TypeScript interfaces used across the plugin.
|
|
5
|
+
* It has NO local imports to prevent circular dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Represents a currently executing tool
|
|
10
|
+
*/
|
|
11
|
+
export interface ActiveTool {
|
|
12
|
+
name: string;
|
|
13
|
+
startTime: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A task item from OpenCode's todo system
|
|
18
|
+
*/
|
|
19
|
+
export interface TodoItem {
|
|
20
|
+
id: string;
|
|
21
|
+
content: string;
|
|
22
|
+
status: string;
|
|
23
|
+
priority: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Token usage information
|
|
28
|
+
*/
|
|
29
|
+
export interface TokenInfo {
|
|
30
|
+
input: number;
|
|
31
|
+
output: number;
|
|
32
|
+
reasoning: number;
|
|
33
|
+
cache: {
|
|
34
|
+
read: number;
|
|
35
|
+
write: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Cost and token tracking for a message/session
|
|
41
|
+
*/
|
|
42
|
+
export interface CostInfo {
|
|
43
|
+
sessionTotal: number;
|
|
44
|
+
currentMessage: number;
|
|
45
|
+
tokens: TokenInfo;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Per-session context tracking an active response stream
|
|
50
|
+
*/
|
|
51
|
+
export interface ResponseContext {
|
|
52
|
+
opencodeSessionId: string;
|
|
53
|
+
mmSession: any;
|
|
54
|
+
streamCtx: any;
|
|
55
|
+
threadRootPostId?: string;
|
|
56
|
+
responseBuffer: string;
|
|
57
|
+
thinkingBuffer: string;
|
|
58
|
+
toolCalls: string[];
|
|
59
|
+
activeTool: ActiveTool | null;
|
|
60
|
+
shellOutput: string;
|
|
61
|
+
shellOutputLastUpdate: number;
|
|
62
|
+
lastUpdateTime: number;
|
|
63
|
+
textPartCount?: number;
|
|
64
|
+
reasoningPartCount?: number;
|
|
65
|
+
compactionCount: number;
|
|
66
|
+
todos: TodoItem[];
|
|
67
|
+
cost: CostInfo;
|
|
68
|
+
responseStartTime: number;
|
|
69
|
+
compactionPostId?: string;
|
|
70
|
+
awaitingContinuation: boolean;
|
|
71
|
+
inCompactionSummary: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Creates a new empty ResponseContext
|
|
76
|
+
*/
|
|
77
|
+
export function createEmptyResponseContext(
|
|
78
|
+
opencodeSessionId: string,
|
|
79
|
+
mmSession: any,
|
|
80
|
+
streamCtx: any,
|
|
81
|
+
threadRootPostId?: string,
|
|
82
|
+
sessionTotalCost: number = 0
|
|
83
|
+
): ResponseContext {
|
|
84
|
+
return {
|
|
85
|
+
opencodeSessionId,
|
|
86
|
+
mmSession,
|
|
87
|
+
streamCtx,
|
|
88
|
+
threadRootPostId,
|
|
89
|
+
responseBuffer: "",
|
|
90
|
+
thinkingBuffer: "",
|
|
91
|
+
toolCalls: [],
|
|
92
|
+
activeTool: null,
|
|
93
|
+
shellOutput: "",
|
|
94
|
+
shellOutputLastUpdate: 0,
|
|
95
|
+
lastUpdateTime: Date.now(),
|
|
96
|
+
compactionCount: 0,
|
|
97
|
+
todos: [],
|
|
98
|
+
cost: {
|
|
99
|
+
sessionTotal: sessionTotalCost,
|
|
100
|
+
currentMessage: 0,
|
|
101
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
102
|
+
},
|
|
103
|
+
responseStartTime: Date.now(),
|
|
104
|
+
awaitingContinuation: false,
|
|
105
|
+
inCompactionSummary: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shakudo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|