kimaki 0.28.0 → 0.30.0
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/dist/agent-model.e2e.test.js +33 -33
- package/dist/anthropic-account-identity.js +1 -1
- package/dist/anthropic-auth-plugin.js +5 -5
- package/dist/anthropic-auth-plugin.test.js +30 -2
- package/dist/anthropic-auth-state.js +1 -1
- package/dist/bash-tool-schema-plugin.js +18 -0
- package/dist/bash-tool-schema-plugin.test.js +20 -1
- package/dist/bin.js +7 -3
- package/dist/bot-lifecycle.test.js +206 -0
- package/dist/cache-drift-plugin.js +25 -18
- package/dist/cache-rewrite.js +39 -0
- package/dist/cache-rewrite.test.js +41 -0
- package/dist/channel-management.js +113 -51
- package/dist/cli-commands/misc.js +4 -1
- package/dist/cli-commands/project.js +7 -4
- package/dist/cli-commands/session.js +118 -36
- package/dist/cli-commands/thread.js +63 -0
- package/dist/cli-parsing.test.js +3 -3
- package/dist/cli-runner.js +11 -1
- package/dist/cli-send-thread.e2e.test.js +2 -2
- package/dist/cli.js +6 -4
- package/dist/commands/ask-question.js +72 -23
- package/dist/commands/queue.js +84 -36
- package/dist/commands/queue.test.js +15 -0
- package/dist/commands/restart-opencode-server.js +7 -1
- package/dist/commands/session-id.js +5 -5
- package/dist/database.js +149 -6
- package/dist/db.js +34 -0
- package/dist/db.test.js +199 -1
- package/dist/default-channel-provisioning.test.js +2 -1
- package/dist/discord-bot.js +220 -146
- package/dist/discord-command-registration.js +1 -1
- package/dist/external-directory-permissions.e2e.test.js +3 -3
- package/dist/external-directory-project-deny.e2e.test.js +1 -1
- package/dist/forum-sync/watchers.js +15 -0
- package/dist/gateway-proxy.e2e.test.js +9 -9
- package/dist/guild-categories.test.js +225 -0
- package/dist/hrana-server.js +68 -14
- package/dist/interaction-handler.js +5 -1
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/kimaki-opencode-plugin.test.js +28 -0
- package/dist/list-channel-threads.js +163 -0
- package/dist/list-channel-threads.test.js +80 -0
- package/dist/markdown.js +164 -102
- package/dist/markdown.test.js +88 -20
- package/dist/message-formatting.js +29 -18
- package/dist/message-formatting.test.js +111 -3
- package/dist/opencode.js +27 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-resume.e2e.test.js +100 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +1 -1
- package/dist/question-text-before-dropdown.e2e.test.js +1 -1
- package/dist/queue-advanced-abort.e2e.test.js +3 -3
- package/dist/queue-advanced-action-buttons.e2e.test.js +5 -5
- package/dist/queue-advanced-e2e-setup.js +63 -0
- package/dist/queue-advanced-footer.e2e.test.js +18 -18
- package/dist/queue-advanced-model-switch.e2e.test.js +3 -4
- package/dist/queue-advanced-permissions-typing.e2e.test.js +2 -2
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +3 -3
- package/dist/queue-delete-message.e2e.test.js +4 -4
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +3 -3
- package/dist/queue-interrupt-drain.e2e.test.js +10 -10
- package/dist/queue-question-select-drain.e2e.test.js +5 -5
- package/dist/runtime-lifecycle.e2e.test.js +95 -11
- package/dist/schema.js +21 -0
- package/dist/session-handler/event-stream-state.js +240 -6
- package/dist/session-handler/event-stream-state.test.js +1116 -130
- package/dist/session-handler/thread-runtime-state.js +4 -0
- package/dist/session-handler/thread-session-runtime.js +499 -89
- package/dist/sleep.e2e.test.js +7 -7
- package/dist/store.js +1 -1
- package/dist/system-message.js +64 -23
- package/dist/system-message.test.js +69 -23
- package/dist/task-id-plugin.js +16 -0
- package/dist/task-runner.js +7 -0
- package/dist/task-schedule.test.js +1 -1
- package/dist/test-utils.js +5 -3
- package/dist/thread-message-queue.e2e.test.js +30 -31
- package/dist/undo-redo.e2e.test.js +3 -3
- package/dist/voice-message.e2e.test.js +64 -26
- package/dist/voice-routing-input.e2e.test.js +4 -4
- package/dist/voice-routing-question.e2e.test.js +4 -4
- package/dist/voice.js +107 -21
- package/dist/voice.test.js +52 -1
- package/dist/wait-session.js +24 -2
- package/dist/worktree-lifecycle.e2e.test.js +8 -8
- package/package.json +7 -7
- package/skills/egaki/SKILL.md +14 -0
- package/skills/holocron/SKILL.md +125 -42
- package/skills/opensrc/SKILL.md +20 -3
- package/skills/spiceflow/SKILL.md +53 -172
- package/skills/strada/SKILL.md +61 -232
- package/src/agent-model.e2e.test.ts +33 -33
- package/src/anthropic-account-identity.ts +1 -1
- package/src/anthropic-auth-plugin.test.ts +37 -2
- package/src/anthropic-auth-plugin.ts +9 -7
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +29 -0
- package/src/bash-tool-schema-plugin.ts +31 -0
- package/src/bin.ts +7 -3
- package/src/bot-lifecycle.test.ts +252 -0
- package/src/cache-drift-plugin.ts +34 -24
- package/src/cache-rewrite.test.ts +49 -0
- package/src/cache-rewrite.ts +96 -0
- package/src/channel-management.ts +144 -65
- package/src/cli-commands/misc.ts +4 -1
- package/src/cli-commands/project.ts +9 -4
- package/src/cli-commands/session.ts +167 -38
- package/src/cli-commands/thread.ts +86 -0
- package/src/cli-parsing.test.ts +3 -3
- package/src/cli-runner.ts +11 -0
- package/src/cli-send-thread.e2e.test.ts +2 -2
- package/src/cli.ts +8 -6
- package/src/commands/ask-question.ts +88 -25
- package/src/commands/queue.test.ts +19 -0
- package/src/commands/queue.ts +111 -45
- package/src/commands/restart-opencode-server.ts +12 -1
- package/src/commands/session-id.ts +5 -5
- package/src/database.ts +219 -6
- package/src/db.test.ts +236 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +2 -1
- package/src/discord-bot.ts +272 -185
- package/src/discord-command-registration.ts +3 -1
- package/src/external-directory-permissions.e2e.test.ts +3 -3
- package/src/external-directory-project-deny.e2e.test.ts +1 -1
- package/src/forum-sync/watchers.ts +20 -0
- package/src/gateway-proxy.e2e.test.ts +9 -9
- package/src/guild-categories.test.ts +260 -0
- package/src/hrana-server.ts +96 -16
- package/src/interaction-handler.ts +7 -0
- package/src/kimaki-opencode-plugin.test.ts +34 -0
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/list-channel-threads.test.ts +90 -0
- package/src/list-channel-threads.ts +252 -0
- package/src/markdown.test.ts +108 -22
- package/src/markdown.ts +216 -103
- package/src/message-formatting.test.ts +129 -2
- package/src/message-formatting.ts +28 -14
- package/src/opencode.ts +28 -13
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-resume.e2e.test.ts +121 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +1 -1
- package/src/question-text-before-dropdown.e2e.test.ts +1 -1
- package/src/queue-advanced-abort.e2e.test.ts +3 -3
- package/src/queue-advanced-action-buttons.e2e.test.ts +5 -5
- package/src/queue-advanced-e2e-setup.ts +65 -0
- package/src/queue-advanced-footer.e2e.test.ts +18 -18
- package/src/queue-advanced-model-switch.e2e.test.ts +3 -4
- package/src/queue-advanced-permissions-typing.e2e.test.ts +2 -2
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +3 -3
- package/src/queue-delete-message.e2e.test.ts +4 -4
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +3 -3
- package/src/queue-interrupt-drain.e2e.test.ts +10 -10
- package/src/queue-question-select-drain.e2e.test.ts +5 -5
- package/src/runtime-lifecycle.e2e.test.ts +104 -10
- package/src/schema.sql +16 -0
- package/src/schema.ts +23 -0
- package/src/session-handler/event-stream-state.test.ts +1184 -133
- package/src/session-handler/event-stream-state.ts +322 -8
- package/src/session-handler/thread-runtime-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +597 -95
- package/src/sleep.e2e.test.ts +7 -7
- package/src/store.ts +2 -2
- package/src/system-message.test.ts +70 -23
- package/src/system-message.ts +67 -22
- package/src/task-id-plugin.ts +17 -0
- package/src/task-runner.ts +8 -0
- package/src/task-schedule.test.ts +1 -1
- package/src/test-utils.ts +5 -3
- package/src/thread-message-queue.e2e.test.ts +30 -31
- package/src/undo-redo.e2e.test.ts +3 -3
- package/src/voice-message.e2e.test.ts +70 -26
- package/src/voice-routing-input.e2e.test.ts +4 -4
- package/src/voice-routing-question.e2e.test.ts +4 -4
- package/src/voice.test.ts +56 -0
- package/src/voice.ts +135 -25
- package/src/wait-session.ts +25 -2
- package/src/worktree-lifecycle.e2e.test.ts +8 -8
package/dist/commands/queue.js
CHANGED
|
@@ -1,44 +1,94 @@
|
|
|
1
1
|
// Queue commands - /queue, /queue-command
|
|
2
2
|
// Clear/remove actions live as buttons on each queue confirmation message.
|
|
3
3
|
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, ChannelType, MessageFlags, } from 'discord.js';
|
|
4
|
-
import { getThreadSession } from '../database.js';
|
|
4
|
+
import { deleteThreadQueueItem, getThreadSession } from '../database.js';
|
|
5
5
|
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
6
6
|
import { getOrCreateRuntime, getRuntime, } from '../session-handler/thread-session-runtime.js';
|
|
7
|
-
import { buildHtmlActionCustomId, registerHtmlAction, } from '../html-actions.js';
|
|
8
7
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
|
-
import { QUEUE_PREFIX } from '../message-formatting.js';
|
|
8
|
+
import { asSystemLine, QUEUE_PREFIX } from '../message-formatting.js';
|
|
10
9
|
import { store } from '../store.js';
|
|
11
10
|
const logger = createLogger(LogPrefix.QUEUE);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const removed = getRuntime(threadId)?.removeQueueItemById(queueId);
|
|
18
|
-
if (!removed) {
|
|
19
|
-
await interaction.editReply({
|
|
20
|
-
content: position
|
|
21
|
-
? `Queued message at position ${position} is no longer in the queue`
|
|
22
|
-
: 'Queued message is no longer in the queue',
|
|
23
|
-
components: [],
|
|
24
|
-
});
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const label = removed.command
|
|
28
|
-
? `/${removed.command.name}`
|
|
29
|
-
: removed.prompt.slice(0, 120);
|
|
30
|
-
await interaction.editReply({
|
|
31
|
-
content: `Removed queued message${position ? ` (was position ${position})` : ''}: ${label}`,
|
|
32
|
-
components: [],
|
|
33
|
-
});
|
|
34
|
-
logger.log(`[QUEUE] User ${interaction.user.displayName} removed queued item ${queueId} in thread ${threadId}`);
|
|
35
|
-
},
|
|
11
|
+
export const QUEUE_REMOVE_CUSTOM_ID_PREFIX = 'queue_remove:';
|
|
12
|
+
async function deleteThreadQueueItemWithoutRuntime(queueId) {
|
|
13
|
+
const row = await deleteThreadQueueItem(queueId).catch((error) => {
|
|
14
|
+
logger.error(`[QUEUE] Failed to delete persisted queue item ${queueId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
15
|
+
return undefined;
|
|
36
16
|
});
|
|
17
|
+
if (!row) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(row.payload_json);
|
|
22
|
+
return {
|
|
23
|
+
prompt: typeof parsed.prompt === 'string' ? parsed.prompt : '',
|
|
24
|
+
username: typeof parsed.username === 'string' ? parsed.username : '',
|
|
25
|
+
command: parsed.command,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { prompt: '', username: '' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function buildQueueRemoveCustomId({ threadId, queueId, }) {
|
|
33
|
+
return `${QUEUE_REMOVE_CUSTOM_ID_PREFIX}${threadId}:${queueId}`;
|
|
34
|
+
}
|
|
35
|
+
export function parseQueueRemoveCustomId(customId) {
|
|
36
|
+
if (!customId.startsWith(QUEUE_REMOVE_CUSTOM_ID_PREFIX)) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const rest = customId.slice(QUEUE_REMOVE_CUSTOM_ID_PREFIX.length);
|
|
40
|
+
const separator = rest.indexOf(':');
|
|
41
|
+
if (separator <= 0 || separator >= rest.length - 1) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
threadId: rest.slice(0, separator),
|
|
46
|
+
queueId: rest.slice(separator + 1),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function buildQueueRemoveRow({ threadId, queueId, }) {
|
|
37
50
|
return new ActionRowBuilder().addComponents(new ButtonBuilder()
|
|
38
|
-
.setCustomId(
|
|
51
|
+
.setCustomId(buildQueueRemoveCustomId({ threadId, queueId }))
|
|
39
52
|
.setLabel('Remove from queue')
|
|
40
53
|
.setStyle(ButtonStyle.Secondary));
|
|
41
54
|
}
|
|
55
|
+
export async function handleQueueRemoveButton(interaction) {
|
|
56
|
+
const parsed = parseQueueRemoveCustomId(interaction.customId);
|
|
57
|
+
if (!parsed) {
|
|
58
|
+
await interaction.reply({
|
|
59
|
+
content: 'Invalid queue remove button',
|
|
60
|
+
flags: MessageFlags.Ephemeral,
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (interaction.channelId && interaction.channelId !== parsed.threadId) {
|
|
65
|
+
await interaction.reply({
|
|
66
|
+
content: 'This queue button does not belong to this thread',
|
|
67
|
+
flags: MessageFlags.Ephemeral,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await interaction.deferUpdate();
|
|
72
|
+
const runtime = getRuntime(parsed.threadId);
|
|
73
|
+
const removed = runtime
|
|
74
|
+
? await runtime.removeQueueItemById(parsed.queueId)
|
|
75
|
+
: await deleteThreadQueueItemWithoutRuntime(parsed.queueId);
|
|
76
|
+
if (!removed) {
|
|
77
|
+
await interaction.editReply({
|
|
78
|
+
content: asSystemLine('Queued message is no longer in the queue'),
|
|
79
|
+
components: [],
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const label = removed.command
|
|
84
|
+
? `/${removed.command.name}`
|
|
85
|
+
: removed.prompt.slice(0, 120);
|
|
86
|
+
await interaction.editReply({
|
|
87
|
+
content: asSystemLine(`Removed queued message: ${label}`),
|
|
88
|
+
components: [],
|
|
89
|
+
});
|
|
90
|
+
logger.log(`[QUEUE] User ${interaction.user.displayName} removed queued item ${parsed.queueId} in thread ${parsed.threadId}`);
|
|
91
|
+
}
|
|
42
92
|
export async function handleQueueCommand({ command, appId, }) {
|
|
43
93
|
const message = command.options.getString('message', true);
|
|
44
94
|
const channel = command.channel;
|
|
@@ -95,14 +145,13 @@ export async function handleQueueCommand({ command, appId, }) {
|
|
|
95
145
|
mode: 'local-queue',
|
|
96
146
|
});
|
|
97
147
|
if (enqueueResult.queued && enqueueResult.queueId) {
|
|
98
|
-
const responseText = `Queued message${enqueueResult.position ? ` (position ${enqueueResult.position})` : ''}
|
|
148
|
+
const responseText = asSystemLine(`Queued message${enqueueResult.position ? ` (position ${enqueueResult.position})` : ''}`);
|
|
99
149
|
await command.reply({
|
|
100
150
|
content: responseText,
|
|
101
151
|
components: [
|
|
102
152
|
buildQueueRemoveRow({
|
|
103
153
|
threadId: thread.id,
|
|
104
154
|
queueId: enqueueResult.queueId,
|
|
105
|
-
position: enqueueResult.position,
|
|
106
155
|
}),
|
|
107
156
|
],
|
|
108
157
|
flags: SILENT_MESSAGE_FLAGS,
|
|
@@ -147,7 +196,7 @@ export async function handleClearQueueCommand({ command, }) {
|
|
|
147
196
|
return;
|
|
148
197
|
}
|
|
149
198
|
if (position !== undefined) {
|
|
150
|
-
const removed = runtime?.removeQueuePosition(position);
|
|
199
|
+
const removed = await runtime?.removeQueuePosition(position);
|
|
151
200
|
if (!removed) {
|
|
152
201
|
await command.reply({
|
|
153
202
|
content: `No queued message at position ${position}`,
|
|
@@ -156,13 +205,13 @@ export async function handleClearQueueCommand({ command, }) {
|
|
|
156
205
|
return;
|
|
157
206
|
}
|
|
158
207
|
await command.reply({
|
|
159
|
-
content: `Cleared queued message at position ${position}
|
|
208
|
+
content: asSystemLine(`Cleared queued message at position ${position}`),
|
|
160
209
|
flags: SILENT_MESSAGE_FLAGS,
|
|
161
210
|
});
|
|
162
211
|
logger.log(`[QUEUE] User ${command.user.displayName} cleared queued position ${position} in thread ${channel.id}`);
|
|
163
212
|
return;
|
|
164
213
|
}
|
|
165
|
-
const cleared = runtime?.clearQueue() ?? [];
|
|
214
|
+
const cleared = await runtime?.clearQueue() ?? [];
|
|
166
215
|
const lines = cleared.map((item, i) => {
|
|
167
216
|
const label = item.command
|
|
168
217
|
? `/${item.command.name}`
|
|
@@ -174,7 +223,7 @@ export async function handleClearQueueCommand({ command, }) {
|
|
|
174
223
|
list = list.slice(0, 597) + '...';
|
|
175
224
|
}
|
|
176
225
|
await command.reply({
|
|
177
|
-
content: `Cleared ${cleared.length} queued message${cleared.length > 1 ? 's' : ''}:\n${list}
|
|
226
|
+
content: asSystemLine(`Cleared ${cleared.length} queued message${cleared.length > 1 ? 's' : ''}:\n${list}`),
|
|
178
227
|
flags: SILENT_MESSAGE_FLAGS,
|
|
179
228
|
});
|
|
180
229
|
logger.log(`[QUEUE] User ${command.user.displayName} cleared queue in thread ${channel.id}`);
|
|
@@ -250,14 +299,13 @@ export async function handleQueueCommandCommand({ command, appId, }) {
|
|
|
250
299
|
mode: 'local-queue',
|
|
251
300
|
});
|
|
252
301
|
if (enqueueResult.queued && enqueueResult.queueId) {
|
|
253
|
-
const responseText = `Queued message${enqueueResult.position ? ` (position ${enqueueResult.position})` : ''}
|
|
302
|
+
const responseText = asSystemLine(`Queued message${enqueueResult.position ? ` (position ${enqueueResult.position})` : ''}`);
|
|
254
303
|
await command.reply({
|
|
255
304
|
content: responseText,
|
|
256
305
|
components: [
|
|
257
306
|
buildQueueRemoveRow({
|
|
258
307
|
threadId: thread.id,
|
|
259
308
|
queueId: enqueueResult.queueId,
|
|
260
|
-
position: enqueueResult.position,
|
|
261
309
|
}),
|
|
262
310
|
],
|
|
263
311
|
flags: SILENT_MESSAGE_FLAGS,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { buildQueueRemoveCustomId, parseQueueRemoveCustomId, } from './queue.js';
|
|
3
|
+
describe('queue remove custom id', () => {
|
|
4
|
+
test('round-trips thread and queue ids under Discord custom_id limit', () => {
|
|
5
|
+
const customId = buildQueueRemoveCustomId({
|
|
6
|
+
threadId: '1550106184055398531',
|
|
7
|
+
queueId: 'a1b2c3d4e5f60789',
|
|
8
|
+
});
|
|
9
|
+
expect(customId.length).toBeLessThanOrEqual(100);
|
|
10
|
+
expect(parseQueueRemoveCustomId(customId)).toEqual({
|
|
11
|
+
threadId: '1550106184055398531',
|
|
12
|
+
queueId: 'a1b2c3d4e5f60789',
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -9,7 +9,7 @@ import { ChannelType, MessageFlags, } from 'discord.js';
|
|
|
9
9
|
import { initializeOpencodeForDirectory, restartOpencodeServer } from '../opencode.js';
|
|
10
10
|
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
11
11
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
12
|
-
import { disposeRuntimesForDirectory } from '../session-handler/thread-session-runtime.js';
|
|
12
|
+
import { disposeRuntimesForDirectory, restorePersistedLocalQueues, } from '../session-handler/thread-session-runtime.js';
|
|
13
13
|
import { registerCommands } from '../discord-command-registration.js';
|
|
14
14
|
const logger = createLogger(LogPrefix.OPENCODE);
|
|
15
15
|
export async function handleRestartOpencodeServerCommand({ command, appId, }) {
|
|
@@ -74,6 +74,12 @@ export async function handleRestartOpencodeServerCommand({ command, appId, }) {
|
|
|
74
74
|
content: `Opencode server **restarted** successfully${abortMsg}. Re-registering slash commands...`,
|
|
75
75
|
});
|
|
76
76
|
logger.log('[RESTART] Shared opencode server restarted');
|
|
77
|
+
await restorePersistedLocalQueues({
|
|
78
|
+
discordClient: command.client,
|
|
79
|
+
appId,
|
|
80
|
+
}).catch((error) => {
|
|
81
|
+
logger.warn(`[RESTART] Failed to restore persisted queues: ${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
+
});
|
|
77
83
|
// Re-register Discord slash commands after restart so new/changed
|
|
78
84
|
// commands, agents, and plugins are picked up immediately.
|
|
79
85
|
const token = command.client.token;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// /session-id command - Show current session ID and an opencode attach command.
|
|
1
|
+
// /session-id command - Show current session ID, Discord thread ID, and an opencode attach command.
|
|
2
2
|
import { ChannelType, MessageFlags, } from 'discord.js';
|
|
3
3
|
import { getThreadSession } from '../database.js';
|
|
4
4
|
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
@@ -46,7 +46,7 @@ export async function handleSessionIdCommand({ command, }) {
|
|
|
46
46
|
const sessionId = await getThreadSession(channel.id);
|
|
47
47
|
if (!sessionId) {
|
|
48
48
|
await command.reply({
|
|
49
|
-
content:
|
|
49
|
+
content: `No active session in this thread\n**Thread ID:** \`${channel.id}\``,
|
|
50
50
|
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
51
51
|
});
|
|
52
52
|
return;
|
|
@@ -57,7 +57,7 @@ export async function handleSessionIdCommand({ command, }) {
|
|
|
57
57
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
58
58
|
if (getClient instanceof Error) {
|
|
59
59
|
await command.editReply({
|
|
60
|
-
content:
|
|
60
|
+
content: `**Session ID:** \`${sessionId}\`\n**Thread ID:** \`${channel.id}\`\nFailed to resolve OpenCode server port: ${getClient.message}`,
|
|
61
61
|
});
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
@@ -65,14 +65,14 @@ export async function handleSessionIdCommand({ command, }) {
|
|
|
65
65
|
}
|
|
66
66
|
if (!port) {
|
|
67
67
|
await command.editReply({
|
|
68
|
-
content:
|
|
68
|
+
content: `**Session ID:** \`${sessionId}\`\n**Thread ID:** \`${channel.id}\`\nCould not determine OpenCode server port`,
|
|
69
69
|
});
|
|
70
70
|
return;
|
|
71
71
|
}
|
|
72
72
|
const attachUrl = `http://127.0.0.1:${port}`;
|
|
73
73
|
const attachCommand = `opencode attach ${attachUrl} --session ${sessionId} --dir ${shellQuote(workingDirectory)}`;
|
|
74
74
|
await command.editReply({
|
|
75
|
-
content: `**Session ID:** \`${sessionId}\`\n**Attach command:**\n\`\`\`bash\n${attachCommand}\n\`\`\``,
|
|
75
|
+
content: `**Session ID:** \`${sessionId}\`\n**Thread ID:** \`${channel.id}\`\n**Attach command:**\n\`\`\`bash\n${attachCommand}\n\`\`\``,
|
|
76
76
|
});
|
|
77
77
|
logger.log(`Session ID shown for thread ${channel.id}: ${sessionId}`);
|
|
78
78
|
}
|
package/dist/database.js
CHANGED
|
@@ -174,13 +174,13 @@ export async function recoverStaleRunningScheduledTasks({ staleBefore }) {
|
|
|
174
174
|
export async function deleteScheduledTask(taskId) {
|
|
175
175
|
const db = await getDb();
|
|
176
176
|
await db.delete(schema.scheduled_tasks)
|
|
177
|
-
.where(orm.eq(schema.scheduled_tasks.id, taskId));
|
|
177
|
+
.where(orm.and(orm.eq(schema.scheduled_tasks.id, taskId), orm.eq(schema.scheduled_tasks.status, 'running')));
|
|
178
178
|
}
|
|
179
179
|
export async function markScheduledTaskCronRescheduled({ taskId, completedAt, nextRunAt }) {
|
|
180
180
|
const db = await getDb();
|
|
181
181
|
await db.update(schema.scheduled_tasks)
|
|
182
182
|
.set({ status: 'planned', last_run_at: completedAt, running_started_at: null, last_error: null, next_run_at: nextRunAt })
|
|
183
|
-
.where(orm.eq(schema.scheduled_tasks.id, taskId));
|
|
183
|
+
.where(orm.and(orm.eq(schema.scheduled_tasks.id, taskId), orm.eq(schema.scheduled_tasks.status, 'running')));
|
|
184
184
|
}
|
|
185
185
|
export async function markScheduledTaskFailed({ taskId, failedAt, errorMessage }) {
|
|
186
186
|
const db = await getDb();
|
|
@@ -192,7 +192,7 @@ export async function markScheduledTaskFailed({ taskId, failedAt, errorMessage }
|
|
|
192
192
|
last_error: errorMessage,
|
|
193
193
|
attempts: orm.sql `${schema.scheduled_tasks.attempts} + 1`,
|
|
194
194
|
})
|
|
195
|
-
.where(orm.eq(schema.scheduled_tasks.id, taskId));
|
|
195
|
+
.where(orm.and(orm.eq(schema.scheduled_tasks.id, taskId), orm.eq(schema.scheduled_tasks.status, 'running')));
|
|
196
196
|
}
|
|
197
197
|
export async function markScheduledTaskCronRetry({ taskId, failedAt, errorMessage, nextRunAt }) {
|
|
198
198
|
const db = await getDb();
|
|
@@ -205,7 +205,7 @@ export async function markScheduledTaskCronRetry({ taskId, failedAt, errorMessag
|
|
|
205
205
|
last_error: errorMessage,
|
|
206
206
|
attempts: orm.sql `${schema.scheduled_tasks.attempts} + 1`,
|
|
207
207
|
})
|
|
208
|
-
.where(orm.eq(schema.scheduled_tasks.id, taskId));
|
|
208
|
+
.where(orm.and(orm.eq(schema.scheduled_tasks.id, taskId), orm.eq(schema.scheduled_tasks.status, 'running')));
|
|
209
209
|
}
|
|
210
210
|
export async function setSessionStartSource({ sessionId, scheduleKind, scheduledTaskId }) {
|
|
211
211
|
const db = await getDb();
|
|
@@ -542,6 +542,42 @@ export async function getThreadSession(threadId) {
|
|
|
542
542
|
const db = await getDb();
|
|
543
543
|
return (await db.query.thread_sessions.findFirst({ where: { thread_id: threadId } }))?.session_id;
|
|
544
544
|
}
|
|
545
|
+
export async function insertThreadQueueItem({ queueId, threadId, payloadJson, }) {
|
|
546
|
+
const db = await getDb();
|
|
547
|
+
await db.insert(schema.thread_queue_items).values({
|
|
548
|
+
queue_id: queueId,
|
|
549
|
+
thread_id: threadId,
|
|
550
|
+
payload_json: payloadJson,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
export async function listThreadQueueItems(threadId) {
|
|
554
|
+
const db = await getDb();
|
|
555
|
+
return db.query.thread_queue_items.findMany({
|
|
556
|
+
where: { thread_id: threadId },
|
|
557
|
+
orderBy: { id: 'asc' },
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
export async function listAllThreadQueueItems() {
|
|
561
|
+
const db = await getDb();
|
|
562
|
+
return db.query.thread_queue_items.findMany({
|
|
563
|
+
orderBy: { id: 'asc' },
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
export async function deleteThreadQueueItem(queueId) {
|
|
567
|
+
const db = await getDb();
|
|
568
|
+
const rows = await db.delete(schema.thread_queue_items).where(orm.eq(schema.thread_queue_items.queue_id, queueId)).returning();
|
|
569
|
+
return rows[0];
|
|
570
|
+
}
|
|
571
|
+
export async function deleteThreadQueueItems(threadId) {
|
|
572
|
+
const db = await getDb();
|
|
573
|
+
await db.delete(schema.thread_queue_items).where(orm.eq(schema.thread_queue_items.thread_id, threadId));
|
|
574
|
+
}
|
|
575
|
+
export async function updateThreadQueueItemPayload({ queueId, payloadJson, }) {
|
|
576
|
+
const db = await getDb();
|
|
577
|
+
await db.update(schema.thread_queue_items)
|
|
578
|
+
.set({ payload_json: payloadJson })
|
|
579
|
+
.where(orm.eq(schema.thread_queue_items.queue_id, queueId));
|
|
580
|
+
}
|
|
545
581
|
export async function setThreadSession(threadId, sessionId) {
|
|
546
582
|
await upsertThreadSession({ threadId, sessionId, source: 'kimaki' });
|
|
547
583
|
}
|
|
@@ -833,24 +869,131 @@ export async function listTrackedTextChannels() {
|
|
|
833
869
|
columns: { channel_id: true, directory: true, created_at: true },
|
|
834
870
|
});
|
|
835
871
|
}
|
|
872
|
+
export async function getGuildCategories(guildId) {
|
|
873
|
+
const db = await getDb();
|
|
874
|
+
const row = await db.query.guild_categories.findFirst({ where: { guild_id: guildId } });
|
|
875
|
+
if (!row)
|
|
876
|
+
return null;
|
|
877
|
+
return {
|
|
878
|
+
guild_id: row.guild_id,
|
|
879
|
+
category_id: row.category_id,
|
|
880
|
+
audio_category_id: row.audio_category_id,
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
export async function listGuildCategoryIds() {
|
|
884
|
+
const db = await getDb();
|
|
885
|
+
const rows = await db.query.guild_categories.findMany({
|
|
886
|
+
columns: { category_id: true, audio_category_id: true },
|
|
887
|
+
});
|
|
888
|
+
const ids = new Set();
|
|
889
|
+
for (const row of rows) {
|
|
890
|
+
if (row.category_id)
|
|
891
|
+
ids.add(row.category_id);
|
|
892
|
+
if (row.audio_category_id)
|
|
893
|
+
ids.add(row.audio_category_id);
|
|
894
|
+
}
|
|
895
|
+
return [...ids];
|
|
896
|
+
}
|
|
897
|
+
export async function setGuildCategoryId({ guildId, kind, categoryId, }) {
|
|
898
|
+
const db = await getDb();
|
|
899
|
+
const values = kind === 'audio'
|
|
900
|
+
? { guild_id: guildId, audio_category_id: categoryId }
|
|
901
|
+
: { guild_id: guildId, category_id: categoryId };
|
|
902
|
+
const set = kind === 'audio'
|
|
903
|
+
? { audio_category_id: categoryId }
|
|
904
|
+
: { category_id: categoryId };
|
|
905
|
+
await db.insert(schema.guild_categories)
|
|
906
|
+
.values(values)
|
|
907
|
+
.onConflictDoUpdate({
|
|
908
|
+
target: schema.guild_categories.guild_id,
|
|
909
|
+
set,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
836
912
|
export async function deleteChannelDirectoriesByDirectory(directory) {
|
|
837
913
|
const db = await getDb();
|
|
838
914
|
await db.delete(schema.channel_directories).where(orm.eq(schema.channel_directories.directory, directory));
|
|
839
915
|
}
|
|
840
|
-
export async function deleteChannelDirectoryById(channelId) {
|
|
916
|
+
export async function deleteChannelDirectoryById(channelId, { preserveMapping = false } = {}) {
|
|
841
917
|
const db = await getDb();
|
|
842
918
|
await db.batch([
|
|
919
|
+
db.update(schema.scheduled_tasks)
|
|
920
|
+
.set({
|
|
921
|
+
status: 'cancelled',
|
|
922
|
+
running_started_at: null,
|
|
923
|
+
last_error: 'Discord channel was deleted',
|
|
924
|
+
})
|
|
925
|
+
.where(orm.and(orm.eq(schema.scheduled_tasks.channel_id, channelId), orm.inArray(schema.scheduled_tasks.status, ['planned', 'running']))),
|
|
843
926
|
db.delete(schema.channel_models).where(orm.eq(schema.channel_models.channel_id, channelId)),
|
|
844
927
|
db.delete(schema.channel_agents).where(orm.eq(schema.channel_agents.channel_id, channelId)),
|
|
845
928
|
db.delete(schema.channel_worktrees).where(orm.eq(schema.channel_worktrees.channel_id, channelId)),
|
|
846
929
|
db.delete(schema.channel_verbosity).where(orm.eq(schema.channel_verbosity.channel_id, channelId)),
|
|
847
930
|
db.delete(schema.channel_mention_mode).where(orm.eq(schema.channel_mention_mode.channel_id, channelId)),
|
|
848
931
|
]);
|
|
932
|
+
if (preserveMapping) {
|
|
933
|
+
return Boolean(await db.query.channel_directories.findFirst({
|
|
934
|
+
where: { channel_id: channelId },
|
|
935
|
+
columns: { channel_id: true },
|
|
936
|
+
}));
|
|
937
|
+
}
|
|
849
938
|
const rows = await db.delete(schema.channel_directories)
|
|
850
939
|
.where(orm.eq(schema.channel_directories.channel_id, channelId))
|
|
851
940
|
.returning({ channel_id: schema.channel_directories.channel_id });
|
|
852
941
|
return rows.length > 0;
|
|
853
942
|
}
|
|
943
|
+
export async function cleanupDeletedThread(threadId) {
|
|
944
|
+
const db = await getDb();
|
|
945
|
+
const binding = await db.query.thread_sessions.findFirst({
|
|
946
|
+
where: { thread_id: threadId },
|
|
947
|
+
columns: { session_id: true },
|
|
948
|
+
});
|
|
949
|
+
const currentThreadId = binding
|
|
950
|
+
? await getThreadIdBySessionId(binding.session_id)
|
|
951
|
+
: undefined;
|
|
952
|
+
const currentSessionId = currentThreadId === threadId
|
|
953
|
+
? binding?.session_id
|
|
954
|
+
: undefined;
|
|
955
|
+
await db.batch([
|
|
956
|
+
db.delete(schema.thread_queue_items)
|
|
957
|
+
.where(orm.eq(schema.thread_queue_items.thread_id, threadId)),
|
|
958
|
+
db.update(schema.session_sleeps)
|
|
959
|
+
.set({ status: 'cancelled' })
|
|
960
|
+
.where(orm.and(orm.eq(schema.session_sleeps.status, 'planned'), orm.eq(schema.session_sleeps.session_id, currentSessionId ?? ''))),
|
|
961
|
+
db.update(schema.scheduled_tasks)
|
|
962
|
+
.set({
|
|
963
|
+
status: 'cancelled',
|
|
964
|
+
running_started_at: null,
|
|
965
|
+
last_error: 'Discord thread was deleted',
|
|
966
|
+
})
|
|
967
|
+
.where(orm.and(orm.eq(schema.scheduled_tasks.thread_id, threadId), orm.inArray(schema.scheduled_tasks.status, ['planned', 'running']))),
|
|
968
|
+
db.update(schema.ipc_requests)
|
|
969
|
+
.set({
|
|
970
|
+
status: 'cancelled',
|
|
971
|
+
response: JSON.stringify({ error: 'Discord thread was deleted' }),
|
|
972
|
+
})
|
|
973
|
+
.where(orm.and(orm.eq(schema.ipc_requests.thread_id, threadId), orm.inArray(schema.ipc_requests.status, ['pending', 'processing']))),
|
|
974
|
+
]);
|
|
975
|
+
}
|
|
976
|
+
export async function isCurrentThreadSessionBinding(threadId) {
|
|
977
|
+
const db = await getDb();
|
|
978
|
+
const binding = await db.query.thread_sessions.findFirst({
|
|
979
|
+
where: { thread_id: threadId },
|
|
980
|
+
columns: { session_id: true },
|
|
981
|
+
});
|
|
982
|
+
if (!binding)
|
|
983
|
+
return false;
|
|
984
|
+
return await getThreadIdBySessionId(binding.session_id) === threadId;
|
|
985
|
+
}
|
|
986
|
+
export async function clearGuildCategoryByChannelId(channelId) {
|
|
987
|
+
const db = await getDb();
|
|
988
|
+
await db.batch([
|
|
989
|
+
db.update(schema.guild_categories)
|
|
990
|
+
.set({ category_id: null })
|
|
991
|
+
.where(orm.eq(schema.guild_categories.category_id, channelId)),
|
|
992
|
+
db.update(schema.guild_categories)
|
|
993
|
+
.set({ audio_category_id: null })
|
|
994
|
+
.where(orm.eq(schema.guild_categories.audio_category_id, channelId)),
|
|
995
|
+
]);
|
|
996
|
+
}
|
|
854
997
|
export async function getVoiceChannelDirectory(channelId) {
|
|
855
998
|
const db = await getDb();
|
|
856
999
|
return (await db.query.channel_directories.findFirst({ where: { channel_id: channelId, channel_type: 'voice' } }))?.directory;
|
|
@@ -906,7 +1049,7 @@ export async function completeIpcRequest({ id, response }) {
|
|
|
906
1049
|
const db = await getDb();
|
|
907
1050
|
const [row] = await db.update(schema.ipc_requests)
|
|
908
1051
|
.set({ response, status: 'completed' })
|
|
909
|
-
.where(orm.eq(schema.ipc_requests.id, id))
|
|
1052
|
+
.where(orm.and(orm.eq(schema.ipc_requests.id, id), orm.inArray(schema.ipc_requests.status, ['pending', 'processing'])))
|
|
910
1053
|
.returning();
|
|
911
1054
|
return row;
|
|
912
1055
|
}
|
package/dist/db.js
CHANGED
|
@@ -89,6 +89,37 @@ async function initializeDb() {
|
|
|
89
89
|
dbInstance = db;
|
|
90
90
|
return db;
|
|
91
91
|
}
|
|
92
|
+
async function rebuildLegacyThreadQueueItems(client) {
|
|
93
|
+
const info = await client.execute('PRAGMA table_info(thread_queue_items)').catch(() => undefined);
|
|
94
|
+
if (!info) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const columns = new Set(info.rows.map((row) => String(row.name)));
|
|
98
|
+
if (columns.has('id')) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!columns.has('queue_id')) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
await client.execute('ALTER TABLE thread_queue_items RENAME TO thread_queue_items_legacy');
|
|
105
|
+
await client.execute(`
|
|
106
|
+
CREATE TABLE IF NOT EXISTS thread_queue_items (
|
|
107
|
+
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
108
|
+
queue_id text NOT NULL UNIQUE,
|
|
109
|
+
thread_id text NOT NULL,
|
|
110
|
+
payload_json text NOT NULL,
|
|
111
|
+
created_at datetime DEFAULT CURRENT_TIMESTAMP
|
|
112
|
+
)
|
|
113
|
+
`);
|
|
114
|
+
await client.execute(`
|
|
115
|
+
INSERT INTO thread_queue_items (queue_id, thread_id, payload_json, created_at)
|
|
116
|
+
SELECT queue_id, thread_id, payload_json, created_at
|
|
117
|
+
FROM thread_queue_items_legacy
|
|
118
|
+
ORDER BY created_at, queue_id
|
|
119
|
+
`);
|
|
120
|
+
await client.execute('DROP TABLE thread_queue_items_legacy');
|
|
121
|
+
await client.execute('CREATE INDEX IF NOT EXISTS thread_queue_items_thread_id_id_idx ON thread_queue_items (thread_id, id)');
|
|
122
|
+
}
|
|
92
123
|
// CREATE TABLE IF NOT EXISTS cannot drop columns. The first two session_sleeps
|
|
93
124
|
// shapes kept thread_id NOT NULL and posted_at. Inserts now omit thread_id, so
|
|
94
125
|
// existing DBs fail with SQLITE_CONSTRAINT_NOTNULL until those columns go away.
|
|
@@ -104,6 +135,9 @@ async function dropLegacySessionSleepsColumns(client) {
|
|
|
104
135
|
await client.execute("UPDATE session_sleeps SET status = 'planned' WHERE status = 'posted'");
|
|
105
136
|
}
|
|
106
137
|
async function migrateSchema({ db, client, }) {
|
|
138
|
+
// schema.sql CREATE TABLE IF NOT EXISTS cannot change the primary key.
|
|
139
|
+
// Rebuild first so CREATE INDEX (... id) does not hit the old queue_id table.
|
|
140
|
+
await rebuildLegacyThreadQueueItems(client);
|
|
107
141
|
const schemaPath = path.join(__dirname, '../src/schema.sql');
|
|
108
142
|
const sql = fs.readFileSync(schemaPath, 'utf-8');
|
|
109
143
|
const statements = sql
|