remote-opencode 1.0.8 β 1.0.10
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/README.md +72 -27
- package/dist/src/__tests__/queueManager.test.js +72 -0
- package/dist/src/__tests__/serveManager.test.js +85 -5
- package/dist/src/__tests__/sessionManager.test.js +17 -2
- package/dist/src/__tests__/sseClient.test.js +13 -13
- package/dist/src/cli.js +11 -1
- package/dist/src/commands/index.js +6 -0
- package/dist/src/commands/model.js +85 -0
- package/dist/src/commands/opencode.js +11 -172
- package/dist/src/commands/queue.js +85 -0
- package/dist/src/commands/setports.js +33 -0
- package/dist/src/handlers/buttonHandler.js +20 -13
- package/dist/src/handlers/interactionHandler.js +17 -5
- package/dist/src/handlers/messageHandler.js +9 -174
- package/dist/src/services/configStore.js +8 -0
- package/dist/src/services/dataStore.js +66 -2
- package/dist/src/services/executionService.js +202 -0
- package/dist/src/services/queueManager.js +20 -0
- package/dist/src/services/serveManager.js +134 -37
- package/dist/src/services/sessionManager.js +26 -9
- package/package.json +2 -2
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import { SlashCommandBuilder,
|
|
1
|
+
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
|
2
2
|
import * as dataStore from '../services/dataStore.js';
|
|
3
|
-
import * as sessionManager from '../services/sessionManager.js';
|
|
4
|
-
import * as serveManager from '../services/serveManager.js';
|
|
5
|
-
import * as worktreeManager from '../services/worktreeManager.js';
|
|
6
|
-
import { SSEClient } from '../services/sseClient.js';
|
|
7
3
|
import { getOrCreateThread } from '../utils/threadHelper.js';
|
|
8
|
-
import {
|
|
4
|
+
import { runPrompt } from '../services/executionService.js';
|
|
5
|
+
import { isBusy } from '../services/queueManager.js';
|
|
9
6
|
function getParentChannelId(interaction) {
|
|
10
7
|
const channel = interaction.channel;
|
|
11
8
|
if (channel?.isThread()) {
|
|
@@ -49,50 +46,14 @@ export const opencode = {
|
|
|
49
46
|
return;
|
|
50
47
|
}
|
|
51
48
|
const threadId = thread.id;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const branchName = worktreeManager.sanitizeBranchName(`auto/${threadId.slice(0, 8)}-${Date.now()}`);
|
|
59
|
-
const worktreePath = await worktreeManager.createWorktree(projectPath, branchName);
|
|
60
|
-
const newMapping = {
|
|
61
|
-
threadId,
|
|
62
|
-
branchName,
|
|
63
|
-
worktreePath,
|
|
64
|
-
projectPath,
|
|
65
|
-
description: prompt.slice(0, 50) + (prompt.length > 50 ? '...' : ''),
|
|
66
|
-
createdAt: Date.now()
|
|
67
|
-
};
|
|
68
|
-
dataStore.setWorktreeMapping(newMapping);
|
|
69
|
-
worktreeMapping = newMapping;
|
|
70
|
-
const embed = new EmbedBuilder()
|
|
71
|
-
.setTitle(`π³ Auto-Worktree: ${branchName}`)
|
|
72
|
-
.setDescription('Automatically created for this session')
|
|
73
|
-
.addFields({ name: 'Branch', value: branchName, inline: true }, { name: 'Path', value: worktreePath, inline: true })
|
|
74
|
-
.setColor(0x2ecc71);
|
|
75
|
-
const worktreeButtons = new ActionRowBuilder()
|
|
76
|
-
.addComponents(new ButtonBuilder()
|
|
77
|
-
.setCustomId(`delete_${threadId}`)
|
|
78
|
-
.setLabel('Delete')
|
|
79
|
-
.setStyle(ButtonStyle.Danger), new ButtonBuilder()
|
|
80
|
-
.setCustomId(`pr_${threadId}`)
|
|
81
|
-
.setLabel('Create PR')
|
|
82
|
-
.setStyle(ButtonStyle.Primary));
|
|
83
|
-
await thread.send({ embeds: [embed], components: [worktreeButtons] });
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
console.error('Auto-worktree creation failed:', error);
|
|
87
|
-
// Continue with main project path (graceful degradation)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
const effectivePath = worktreeMapping?.worktreePath ?? projectPath;
|
|
92
|
-
const existingClient = sessionManager.getSseClient(threadId);
|
|
93
|
-
if (existingClient && existingClient.isConnected()) {
|
|
49
|
+
if (isBusy(threadId)) {
|
|
50
|
+
dataStore.addToQueue(threadId, {
|
|
51
|
+
prompt,
|
|
52
|
+
userId: interaction.user.id,
|
|
53
|
+
timestamp: Date.now()
|
|
54
|
+
});
|
|
94
55
|
await interaction.reply({
|
|
95
|
-
content: '
|
|
56
|
+
content: 'π₯ Prompt added to queue.',
|
|
96
57
|
flags: MessageFlags.Ephemeral
|
|
97
58
|
});
|
|
98
59
|
return;
|
|
@@ -100,128 +61,6 @@ export const opencode = {
|
|
|
100
61
|
await interaction.reply({
|
|
101
62
|
content: `π **Prompt**: ${prompt}`
|
|
102
63
|
});
|
|
103
|
-
|
|
104
|
-
.addComponents(new ButtonBuilder()
|
|
105
|
-
.setCustomId(`interrupt_${threadId}`)
|
|
106
|
-
.setLabel('βΈοΈ Interrupt')
|
|
107
|
-
.setStyle(ButtonStyle.Secondary));
|
|
108
|
-
let streamMessage;
|
|
109
|
-
try {
|
|
110
|
-
streamMessage = await thread.send({
|
|
111
|
-
content: 'π Starting OpenCode server...',
|
|
112
|
-
components: [buttons]
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
await interaction.editReply({
|
|
117
|
-
content: `π **Prompt**: ${prompt}\n\nβ Cannot send message to thread.`
|
|
118
|
-
});
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
let port;
|
|
122
|
-
let sessionId;
|
|
123
|
-
let updateInterval = null;
|
|
124
|
-
let accumulatedText = '';
|
|
125
|
-
let lastContent = '';
|
|
126
|
-
let tick = 0;
|
|
127
|
-
const spinner = ['β ', 'β ', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ', 'β '];
|
|
128
|
-
const updateStreamMessage = async (content, components) => {
|
|
129
|
-
try {
|
|
130
|
-
await streamMessage.edit({ content, components });
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
}
|
|
134
|
-
};
|
|
135
|
-
try {
|
|
136
|
-
port = await serveManager.spawnServe(effectivePath);
|
|
137
|
-
await updateStreamMessage('β³ Waiting for OpenCode server...', [buttons]);
|
|
138
|
-
await serveManager.waitForReady(port);
|
|
139
|
-
const existingSession = sessionManager.getSessionForThread(threadId);
|
|
140
|
-
if (existingSession && existingSession.projectPath === effectivePath) {
|
|
141
|
-
const isValid = await sessionManager.validateSession(port, existingSession.sessionId);
|
|
142
|
-
if (isValid) {
|
|
143
|
-
sessionId = existingSession.sessionId;
|
|
144
|
-
sessionManager.updateSessionLastUsed(threadId);
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
sessionId = await sessionManager.createSession(port);
|
|
148
|
-
sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
else {
|
|
152
|
-
sessionId = await sessionManager.createSession(port);
|
|
153
|
-
sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
|
|
154
|
-
}
|
|
155
|
-
const sseClient = new SSEClient();
|
|
156
|
-
sseClient.connect(`http://localhost:${port}`);
|
|
157
|
-
sessionManager.setSseClient(threadId, sseClient);
|
|
158
|
-
sseClient.onPartUpdated((part) => {
|
|
159
|
-
accumulatedText = part.text;
|
|
160
|
-
});
|
|
161
|
-
sseClient.onSessionIdle(() => {
|
|
162
|
-
if (updateInterval) {
|
|
163
|
-
clearInterval(updateInterval);
|
|
164
|
-
updateInterval = null;
|
|
165
|
-
}
|
|
166
|
-
(async () => {
|
|
167
|
-
try {
|
|
168
|
-
const formatted = formatOutput(accumulatedText);
|
|
169
|
-
const disabledButtons = new ActionRowBuilder()
|
|
170
|
-
.addComponents(new ButtonBuilder()
|
|
171
|
-
.setCustomId(`interrupt_${threadId}`)
|
|
172
|
-
.setLabel('βΈοΈ Interrupt')
|
|
173
|
-
.setStyle(ButtonStyle.Secondary)
|
|
174
|
-
.setDisabled(true));
|
|
175
|
-
await updateStreamMessage(`\`\`\`\n${formatted}\n\`\`\``, [disabledButtons]);
|
|
176
|
-
await thread.send({ content: 'β
Done' });
|
|
177
|
-
sseClient.disconnect();
|
|
178
|
-
sessionManager.clearSseClient(threadId);
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
}
|
|
182
|
-
})();
|
|
183
|
-
});
|
|
184
|
-
sseClient.onError((error) => {
|
|
185
|
-
if (updateInterval) {
|
|
186
|
-
clearInterval(updateInterval);
|
|
187
|
-
updateInterval = null;
|
|
188
|
-
}
|
|
189
|
-
(async () => {
|
|
190
|
-
try {
|
|
191
|
-
await updateStreamMessage(`β Connection error: ${error.message}`, []);
|
|
192
|
-
}
|
|
193
|
-
catch {
|
|
194
|
-
}
|
|
195
|
-
})();
|
|
196
|
-
});
|
|
197
|
-
updateInterval = setInterval(async () => {
|
|
198
|
-
tick++;
|
|
199
|
-
try {
|
|
200
|
-
const formatted = formatOutput(accumulatedText);
|
|
201
|
-
const spinnerChar = spinner[tick % spinner.length];
|
|
202
|
-
const newContent = formatted || 'Processing...';
|
|
203
|
-
if (newContent !== lastContent || tick % 2 === 0) {
|
|
204
|
-
lastContent = newContent;
|
|
205
|
-
await updateStreamMessage(`${spinnerChar} **Running...**\n\`\`\`\n${newContent}\n\`\`\``, [buttons]);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
}
|
|
210
|
-
}, 1000);
|
|
211
|
-
await updateStreamMessage('π Sending prompt...', [buttons]);
|
|
212
|
-
await sessionManager.sendPrompt(port, sessionId, prompt);
|
|
213
|
-
}
|
|
214
|
-
catch (error) {
|
|
215
|
-
if (updateInterval) {
|
|
216
|
-
clearInterval(updateInterval);
|
|
217
|
-
}
|
|
218
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
219
|
-
await updateStreamMessage(`β OpenCode execution failed: ${errorMessage}`, []);
|
|
220
|
-
const client = sessionManager.getSseClient(threadId);
|
|
221
|
-
if (client) {
|
|
222
|
-
client.disconnect();
|
|
223
|
-
sessionManager.clearSseClient(threadId);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
64
|
+
await runPrompt(thread, threadId, prompt, channelId);
|
|
226
65
|
}
|
|
227
66
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
|
2
|
+
import * as dataStore from '../services/dataStore.js';
|
|
3
|
+
import { processNextInQueue } from '../services/queueManager.js';
|
|
4
|
+
export const queue = {
|
|
5
|
+
data: new SlashCommandBuilder()
|
|
6
|
+
.setName('queue')
|
|
7
|
+
.setDescription('Manage the job queue for this thread')
|
|
8
|
+
.addSubcommand(sub => sub.setName('list')
|
|
9
|
+
.setDescription('List all pending prompts in the queue'))
|
|
10
|
+
.addSubcommand(sub => sub.setName('clear')
|
|
11
|
+
.setDescription('Clear all pending prompts in the queue'))
|
|
12
|
+
.addSubcommand(sub => sub.setName('pause')
|
|
13
|
+
.setDescription('Pause the queue (current job will finish)'))
|
|
14
|
+
.addSubcommand(sub => sub.setName('resume')
|
|
15
|
+
.setDescription('Resume the queue and start next task if idle'))
|
|
16
|
+
.addSubcommand(sub => sub.setName('settings')
|
|
17
|
+
.setDescription('Configure queue behavior')
|
|
18
|
+
.addBooleanOption(opt => opt.setName('continue_on_failure')
|
|
19
|
+
.setDescription('Whether to continue to next task if current one fails'))
|
|
20
|
+
.addBooleanOption(opt => opt.setName('fresh_context')
|
|
21
|
+
.setDescription('Whether to clear AI conversation context between tasks'))),
|
|
22
|
+
async execute(interaction) {
|
|
23
|
+
const thread = interaction.channel;
|
|
24
|
+
if (!thread?.isThread()) {
|
|
25
|
+
await interaction.reply({
|
|
26
|
+
content: 'β This command can only be used in a thread.',
|
|
27
|
+
flags: MessageFlags.Ephemeral
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const threadId = thread.id;
|
|
32
|
+
const subcommand = interaction.options.getSubcommand();
|
|
33
|
+
if (subcommand === 'list') {
|
|
34
|
+
const q = dataStore.getQueue(threadId);
|
|
35
|
+
if (q.length === 0) {
|
|
36
|
+
await interaction.reply({ content: 'π The queue is empty.', flags: MessageFlags.Ephemeral });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const list = q.map((m, i) => `${i + 1}. ${m.prompt.slice(0, 100)}${m.prompt.length > 100 ? '...' : ''}`).join('\n');
|
|
40
|
+
const settings = dataStore.getQueueSettings(threadId);
|
|
41
|
+
const status = settings.paused ? 'βΈοΈ Paused' : 'βΆοΈ Running';
|
|
42
|
+
await interaction.reply({
|
|
43
|
+
content: `π **Queue Status**: ${status}\n\n**Pending Tasks**:\n${list}`,
|
|
44
|
+
flags: MessageFlags.Ephemeral
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else if (subcommand === 'clear') {
|
|
48
|
+
dataStore.clearQueue(threadId);
|
|
49
|
+
await interaction.reply({ content: 'ποΈ Queue cleared.', flags: MessageFlags.Ephemeral });
|
|
50
|
+
}
|
|
51
|
+
else if (subcommand === 'pause') {
|
|
52
|
+
dataStore.updateQueueSettings(threadId, { paused: true });
|
|
53
|
+
await interaction.reply({ content: 'βΈοΈ Queue paused. Current job will finish, but next one won\'t start automatically.', flags: MessageFlags.Ephemeral });
|
|
54
|
+
}
|
|
55
|
+
else if (subcommand === 'resume') {
|
|
56
|
+
dataStore.updateQueueSettings(threadId, { paused: false });
|
|
57
|
+
await interaction.reply({ content: 'βΆοΈ Queue resumed.', flags: MessageFlags.Ephemeral });
|
|
58
|
+
// Try to trigger next if idle
|
|
59
|
+
const parentChannelId = thread.parentId;
|
|
60
|
+
if (parentChannelId) {
|
|
61
|
+
const sseClient = (await import('../services/sessionManager.js')).getSseClient(threadId);
|
|
62
|
+
if (!sseClient || !sseClient.isConnected()) {
|
|
63
|
+
await processNextInQueue(thread, threadId, parentChannelId);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else if (subcommand === 'settings') {
|
|
68
|
+
const continueOnFailure = interaction.options.getBoolean('continue_on_failure');
|
|
69
|
+
const freshContext = interaction.options.getBoolean('fresh_context');
|
|
70
|
+
const updates = {};
|
|
71
|
+
if (continueOnFailure !== null)
|
|
72
|
+
updates.continueOnFailure = continueOnFailure;
|
|
73
|
+
if (freshContext !== null)
|
|
74
|
+
updates.freshContext = freshContext;
|
|
75
|
+
if (Object.keys(updates).length > 0) {
|
|
76
|
+
dataStore.updateQueueSettings(threadId, updates);
|
|
77
|
+
}
|
|
78
|
+
const settings = dataStore.getQueueSettings(threadId);
|
|
79
|
+
await interaction.reply({
|
|
80
|
+
content: `βοΈ **Queue Settings Updated**:\n- Continue on failure: \`${settings.continueOnFailure}\`\n- Fresh context: \`${settings.freshContext}\``,
|
|
81
|
+
flags: MessageFlags.Ephemeral
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SlashCommandBuilder } from 'discord.js';
|
|
2
|
+
import { setPortConfig } from '../services/configStore.js';
|
|
3
|
+
export const setports = {
|
|
4
|
+
data: new SlashCommandBuilder()
|
|
5
|
+
.setName('setports')
|
|
6
|
+
.setDescription('Configure the port range for OpenCode server instances')
|
|
7
|
+
.addIntegerOption(option => option.setName('min')
|
|
8
|
+
.setDescription('Minimum port number')
|
|
9
|
+
.setRequired(true))
|
|
10
|
+
.addIntegerOption(option => option.setName('max')
|
|
11
|
+
.setDescription('Maximum port number')
|
|
12
|
+
.setRequired(true)),
|
|
13
|
+
async execute(interaction) {
|
|
14
|
+
const min = interaction.options.getInteger('min', true);
|
|
15
|
+
const max = interaction.options.getInteger('max', true);
|
|
16
|
+
if (min >= max) {
|
|
17
|
+
await interaction.reply({
|
|
18
|
+
content: 'β Minimum port must be less than maximum port.',
|
|
19
|
+
ephemeral: true
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (min < 1024 || max > 65535) {
|
|
24
|
+
await interaction.reply({
|
|
25
|
+
content: 'β Ports must be between 1024 and 65535.',
|
|
26
|
+
ephemeral: true
|
|
27
|
+
});
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
setPortConfig({ min, max });
|
|
31
|
+
await interaction.reply(`β
Port range updated: ${min}-${max}`);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { MessageFlags } from 'discord.js';
|
|
1
2
|
import * as sessionManager from '../services/sessionManager.js';
|
|
2
3
|
import * as serveManager from '../services/serveManager.js';
|
|
3
4
|
import * as dataStore from '../services/dataStore.js';
|
|
@@ -8,7 +9,7 @@ export async function handleButton(interaction) {
|
|
|
8
9
|
if (!threadId) {
|
|
9
10
|
await interaction.reply({
|
|
10
11
|
content: 'β Invalid button.',
|
|
11
|
-
|
|
12
|
+
flags: MessageFlags.Ephemeral
|
|
12
13
|
});
|
|
13
14
|
return;
|
|
14
15
|
}
|
|
@@ -24,7 +25,7 @@ export async function handleButton(interaction) {
|
|
|
24
25
|
else {
|
|
25
26
|
await interaction.reply({
|
|
26
27
|
content: 'β Unknown action.',
|
|
27
|
-
|
|
28
|
+
flags: MessageFlags.Ephemeral
|
|
28
29
|
});
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -33,19 +34,22 @@ async function handleInterrupt(interaction, threadId) {
|
|
|
33
34
|
if (!session) {
|
|
34
35
|
await interaction.reply({
|
|
35
36
|
content: 'β οΈ Session not found.',
|
|
36
|
-
|
|
37
|
+
flags: MessageFlags.Ephemeral
|
|
37
38
|
});
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
|
-
const
|
|
41
|
+
const channel = interaction.channel;
|
|
42
|
+
const parentChannelId = channel?.isThread() ? channel.parentId : channel?.id;
|
|
43
|
+
const preferredModel = parentChannelId ? dataStore.getChannelModel(parentChannelId) : undefined;
|
|
44
|
+
const port = serveManager.getPort(session.projectPath, preferredModel);
|
|
41
45
|
if (!port) {
|
|
42
46
|
await interaction.reply({
|
|
43
47
|
content: 'β οΈ Server is not running.',
|
|
44
|
-
|
|
48
|
+
flags: MessageFlags.Ephemeral
|
|
45
49
|
});
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
48
|
-
await interaction.deferReply({
|
|
52
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
49
53
|
const success = await sessionManager.abortSession(port, session.sessionId);
|
|
50
54
|
if (success) {
|
|
51
55
|
await interaction.editReply({ content: 'βΈοΈ Interrupt request sent.' });
|
|
@@ -57,10 +61,10 @@ async function handleInterrupt(interaction, threadId) {
|
|
|
57
61
|
async function handleWorktreeDelete(interaction, threadId) {
|
|
58
62
|
const mapping = dataStore.getWorktreeMapping(threadId);
|
|
59
63
|
if (!mapping) {
|
|
60
|
-
await interaction.reply({ content: 'β οΈ Worktree mapping not found.',
|
|
64
|
+
await interaction.reply({ content: 'β οΈ Worktree mapping not found.', flags: MessageFlags.Ephemeral });
|
|
61
65
|
return;
|
|
62
66
|
}
|
|
63
|
-
await interaction.deferReply({
|
|
67
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
64
68
|
try {
|
|
65
69
|
if (worktreeManager.worktreeExists(mapping.worktreePath)) {
|
|
66
70
|
await worktreeManager.removeWorktree(mapping.worktreePath, false);
|
|
@@ -79,13 +83,16 @@ async function handleWorktreeDelete(interaction, threadId) {
|
|
|
79
83
|
async function handleWorktreePR(interaction, threadId) {
|
|
80
84
|
const mapping = dataStore.getWorktreeMapping(threadId);
|
|
81
85
|
if (!mapping) {
|
|
82
|
-
await interaction.reply({ content: 'β οΈ Worktree mapping not found.',
|
|
86
|
+
await interaction.reply({ content: 'β οΈ Worktree mapping not found.', flags: MessageFlags.Ephemeral });
|
|
83
87
|
return;
|
|
84
88
|
}
|
|
85
|
-
|
|
89
|
+
const channel = interaction.channel;
|
|
90
|
+
const parentChannelId = channel?.isThread() ? channel.parentId : channel?.id;
|
|
91
|
+
const preferredModel = parentChannelId ? dataStore.getChannelModel(parentChannelId) : undefined;
|
|
92
|
+
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
|
86
93
|
try {
|
|
87
|
-
const port = await serveManager.spawnServe(mapping.worktreePath);
|
|
88
|
-
await serveManager.waitForReady(port);
|
|
94
|
+
const port = await serveManager.spawnServe(mapping.worktreePath, preferredModel);
|
|
95
|
+
await serveManager.waitForReady(port, 30000, mapping.worktreePath, preferredModel);
|
|
89
96
|
let sessionId;
|
|
90
97
|
const existingSession = sessionManager.getSessionForThread(threadId);
|
|
91
98
|
if (existingSession) {
|
|
@@ -97,7 +104,7 @@ async function handleWorktreePR(interaction, threadId) {
|
|
|
97
104
|
}
|
|
98
105
|
sessionManager.setSessionForThread(threadId, sessionId, mapping.worktreePath, port);
|
|
99
106
|
const prPrompt = `Create a pull request for the current branch. Include a clear title and description summarizing all changes.`;
|
|
100
|
-
await sessionManager.sendPrompt(port, sessionId, prPrompt);
|
|
107
|
+
await sessionManager.sendPrompt(port, sessionId, prPrompt, preferredModel);
|
|
101
108
|
await interaction.editReply({ content: 'π PR creation started! Check the thread for progress.' });
|
|
102
109
|
}
|
|
103
110
|
catch (error) {
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import { MessageFlags } from 'discord.js';
|
|
1
2
|
import { commands } from '../commands/index.js';
|
|
2
3
|
import { handleButton } from './buttonHandler.js';
|
|
3
4
|
export async function handleInteraction(interaction) {
|
|
4
5
|
if (interaction.isButton()) {
|
|
5
|
-
|
|
6
|
+
try {
|
|
7
|
+
await handleButton(interaction);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
console.error('Error handling button:', error);
|
|
11
|
+
}
|
|
6
12
|
return;
|
|
7
13
|
}
|
|
8
14
|
if (!interaction.isChatInputCommand())
|
|
@@ -15,12 +21,18 @@ export async function handleInteraction(interaction) {
|
|
|
15
21
|
await command.execute(interaction);
|
|
16
22
|
}
|
|
17
23
|
catch (error) {
|
|
24
|
+
console.error(`Error executing command ${interaction.commandName}:`, error);
|
|
18
25
|
const content = 'β An error occurred while executing the command.';
|
|
19
|
-
|
|
20
|
-
|
|
26
|
+
try {
|
|
27
|
+
if (interaction.replied || interaction.deferred) {
|
|
28
|
+
await interaction.followUp({ content, flags: MessageFlags.Ephemeral });
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
await interaction.reply({ content, flags: MessageFlags.Ephemeral });
|
|
32
|
+
}
|
|
21
33
|
}
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
catch (replyError) {
|
|
35
|
+
console.error('Failed to send error response to user:', replyError);
|
|
24
36
|
}
|
|
25
37
|
}
|
|
26
38
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
|
|
2
1
|
import * as dataStore from '../services/dataStore.js';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import * as worktreeManager from '../services/worktreeManager.js';
|
|
6
|
-
import { SSEClient } from '../services/sseClient.js';
|
|
7
|
-
import { formatOutput } from '../utils/messageFormatter.js';
|
|
2
|
+
import { runPrompt } from '../services/executionService.js';
|
|
3
|
+
import { isBusy } from '../services/queueManager.js';
|
|
8
4
|
export async function handleMessageCreate(message) {
|
|
9
5
|
if (message.author.bot)
|
|
10
6
|
return;
|
|
@@ -19,178 +15,17 @@ export async function handleMessageCreate(message) {
|
|
|
19
15
|
const parentChannelId = channel.parentId;
|
|
20
16
|
if (!parentChannelId)
|
|
21
17
|
return;
|
|
22
|
-
const projectPath = dataStore.getChannelProjectPath(parentChannelId);
|
|
23
|
-
if (!projectPath) {
|
|
24
|
-
await message.reply('β No project bound to parent channel. Disable passthrough and use `/use` first.');
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
let worktreeMapping = dataStore.getWorktreeMapping(threadId);
|
|
28
|
-
// Auto-create worktree if enabled and no mapping exists for this thread
|
|
29
|
-
if (!worktreeMapping) {
|
|
30
|
-
const projectAlias = dataStore.getChannelBinding(parentChannelId);
|
|
31
|
-
if (projectAlias && dataStore.getProjectAutoWorktree(projectAlias)) {
|
|
32
|
-
try {
|
|
33
|
-
const branchName = worktreeManager.sanitizeBranchName(`auto/${threadId.slice(0, 8)}-${Date.now()}`);
|
|
34
|
-
const worktreePath = await worktreeManager.createWorktree(projectPath, branchName);
|
|
35
|
-
const prompt = message.content.trim();
|
|
36
|
-
const newMapping = {
|
|
37
|
-
threadId,
|
|
38
|
-
branchName,
|
|
39
|
-
worktreePath,
|
|
40
|
-
projectPath,
|
|
41
|
-
description: prompt.slice(0, 50) + (prompt.length > 50 ? '...' : ''),
|
|
42
|
-
createdAt: Date.now()
|
|
43
|
-
};
|
|
44
|
-
dataStore.setWorktreeMapping(newMapping);
|
|
45
|
-
worktreeMapping = newMapping;
|
|
46
|
-
const embed = new EmbedBuilder()
|
|
47
|
-
.setTitle(`π³ Auto-Worktree: ${branchName}`)
|
|
48
|
-
.setDescription('Automatically created for this session')
|
|
49
|
-
.addFields({ name: 'Branch', value: branchName, inline: true }, { name: 'Path', value: worktreePath, inline: true })
|
|
50
|
-
.setColor(0x2ecc71);
|
|
51
|
-
const worktreeButtons = new ActionRowBuilder()
|
|
52
|
-
.addComponents(new ButtonBuilder()
|
|
53
|
-
.setCustomId(`delete_${threadId}`)
|
|
54
|
-
.setLabel('Delete')
|
|
55
|
-
.setStyle(ButtonStyle.Danger), new ButtonBuilder()
|
|
56
|
-
.setCustomId(`pr_${threadId}`)
|
|
57
|
-
.setLabel('Create PR')
|
|
58
|
-
.setStyle(ButtonStyle.Primary));
|
|
59
|
-
await channel.send({ embeds: [embed], components: [worktreeButtons] });
|
|
60
|
-
}
|
|
61
|
-
catch (error) {
|
|
62
|
-
console.error('Auto-worktree creation failed:', error);
|
|
63
|
-
// Continue with main project path (graceful degradation)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
const effectivePath = worktreeMapping?.worktreePath ?? projectPath;
|
|
68
|
-
const existingClient = sessionManager.getSseClient(threadId);
|
|
69
|
-
if (existingClient && existingClient.isConnected()) {
|
|
70
|
-
await message.react('β³');
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
18
|
const prompt = message.content.trim();
|
|
74
19
|
if (!prompt)
|
|
75
20
|
return;
|
|
76
|
-
|
|
77
|
-
.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
let streamMessage;
|
|
82
|
-
try {
|
|
83
|
-
streamMessage = await channel.send({
|
|
84
|
-
content: `π **Prompt**: ${prompt}\n\nπ Starting OpenCode server...`,
|
|
85
|
-
components: [buttons]
|
|
21
|
+
if (isBusy(threadId)) {
|
|
22
|
+
dataStore.addToQueue(threadId, {
|
|
23
|
+
prompt,
|
|
24
|
+
userId: message.author.id,
|
|
25
|
+
timestamp: Date.now()
|
|
86
26
|
});
|
|
87
|
-
|
|
88
|
-
catch {
|
|
27
|
+
await message.react('π₯');
|
|
89
28
|
return;
|
|
90
29
|
}
|
|
91
|
-
|
|
92
|
-
let sessionId;
|
|
93
|
-
let updateInterval = null;
|
|
94
|
-
let accumulatedText = '';
|
|
95
|
-
let lastContent = '';
|
|
96
|
-
let tick = 0;
|
|
97
|
-
const spinner = ['β ', 'β ', 'β Ή', 'β Έ', 'β Ό', 'β ΄', 'β ¦', 'β §', 'β ', 'β '];
|
|
98
|
-
const updateStreamMessage = async (content, components) => {
|
|
99
|
-
try {
|
|
100
|
-
await streamMessage.edit({ content, components });
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
try {
|
|
106
|
-
port = await serveManager.spawnServe(effectivePath);
|
|
107
|
-
await updateStreamMessage(`π **Prompt**: ${prompt}\n\nβ³ Waiting for OpenCode server...`, [buttons]);
|
|
108
|
-
await serveManager.waitForReady(port);
|
|
109
|
-
const existingSession = sessionManager.getSessionForThread(threadId);
|
|
110
|
-
if (existingSession && existingSession.projectPath === effectivePath) {
|
|
111
|
-
const isValid = await sessionManager.validateSession(port, existingSession.sessionId);
|
|
112
|
-
if (isValid) {
|
|
113
|
-
sessionId = existingSession.sessionId;
|
|
114
|
-
sessionManager.updateSessionLastUsed(threadId);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
sessionId = await sessionManager.createSession(port);
|
|
118
|
-
sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
sessionId = await sessionManager.createSession(port);
|
|
123
|
-
sessionManager.setSessionForThread(threadId, sessionId, effectivePath, port);
|
|
124
|
-
}
|
|
125
|
-
const sseClient = new SSEClient();
|
|
126
|
-
sseClient.connect(`http://localhost:${port}`);
|
|
127
|
-
sessionManager.setSseClient(threadId, sseClient);
|
|
128
|
-
sseClient.onPartUpdated((part) => {
|
|
129
|
-
accumulatedText = part.text;
|
|
130
|
-
});
|
|
131
|
-
sseClient.onSessionIdle(() => {
|
|
132
|
-
if (updateInterval) {
|
|
133
|
-
clearInterval(updateInterval);
|
|
134
|
-
updateInterval = null;
|
|
135
|
-
}
|
|
136
|
-
(async () => {
|
|
137
|
-
try {
|
|
138
|
-
const formatted = formatOutput(accumulatedText);
|
|
139
|
-
const disabledButtons = new ActionRowBuilder()
|
|
140
|
-
.addComponents(new ButtonBuilder()
|
|
141
|
-
.setCustomId(`interrupt_${threadId}`)
|
|
142
|
-
.setLabel('βΈοΈ Interrupt')
|
|
143
|
-
.setStyle(ButtonStyle.Secondary)
|
|
144
|
-
.setDisabled(true));
|
|
145
|
-
await updateStreamMessage(`π **Prompt**: ${prompt}\n\n\`\`\`\n${formatted}\n\`\`\``, [disabledButtons]);
|
|
146
|
-
await channel.send({ content: 'β
Done' });
|
|
147
|
-
sseClient.disconnect();
|
|
148
|
-
sessionManager.clearSseClient(threadId);
|
|
149
|
-
}
|
|
150
|
-
catch {
|
|
151
|
-
}
|
|
152
|
-
})();
|
|
153
|
-
});
|
|
154
|
-
sseClient.onError((error) => {
|
|
155
|
-
if (updateInterval) {
|
|
156
|
-
clearInterval(updateInterval);
|
|
157
|
-
updateInterval = null;
|
|
158
|
-
}
|
|
159
|
-
(async () => {
|
|
160
|
-
try {
|
|
161
|
-
await updateStreamMessage(`π **Prompt**: ${prompt}\n\nβ Connection error: ${error.message}`, []);
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
}
|
|
165
|
-
})();
|
|
166
|
-
});
|
|
167
|
-
updateInterval = setInterval(async () => {
|
|
168
|
-
tick++;
|
|
169
|
-
try {
|
|
170
|
-
const formatted = formatOutput(accumulatedText);
|
|
171
|
-
const spinnerChar = spinner[tick % spinner.length];
|
|
172
|
-
const newContent = formatted || 'Processing...';
|
|
173
|
-
if (newContent !== lastContent || tick % 2 === 0) {
|
|
174
|
-
lastContent = newContent;
|
|
175
|
-
await updateStreamMessage(`π **Prompt**: ${prompt}\n\n${spinnerChar} **Running...**\n\`\`\`\n${newContent}\n\`\`\``, [buttons]);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
catch {
|
|
179
|
-
}
|
|
180
|
-
}, 1000);
|
|
181
|
-
await updateStreamMessage(`π **Prompt**: ${prompt}\n\nπ Sending prompt...`, [buttons]);
|
|
182
|
-
await sessionManager.sendPrompt(port, sessionId, prompt);
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
if (updateInterval) {
|
|
186
|
-
clearInterval(updateInterval);
|
|
187
|
-
}
|
|
188
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
189
|
-
await updateStreamMessage(`π **Prompt**: ${prompt}\n\nβ OpenCode execution failed: ${errorMessage}`, []);
|
|
190
|
-
const client = sessionManager.getSseClient(threadId);
|
|
191
|
-
if (client) {
|
|
192
|
-
client.disconnect();
|
|
193
|
-
sessionManager.clearSseClient(threadId);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
30
|
+
await runPrompt(channel, threadId, prompt, parentChannelId);
|
|
196
31
|
}
|
|
@@ -36,6 +36,14 @@ export function setBotConfig(bot) {
|
|
|
36
36
|
config.bot = bot;
|
|
37
37
|
saveConfig(config);
|
|
38
38
|
}
|
|
39
|
+
export function getPortConfig() {
|
|
40
|
+
return loadConfig().ports;
|
|
41
|
+
}
|
|
42
|
+
export function setPortConfig(ports) {
|
|
43
|
+
const config = loadConfig();
|
|
44
|
+
config.ports = ports;
|
|
45
|
+
saveConfig(config);
|
|
46
|
+
}
|
|
39
47
|
export function hasBotConfig() {
|
|
40
48
|
const bot = getBotConfig();
|
|
41
49
|
return !!(bot?.discordToken && bot?.clientId && bot?.guildId);
|