kimaki 0.4.38 → 0.4.39
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/cli.js +9 -3
- package/dist/commands/abort.js +15 -6
- package/dist/commands/add-project.js +9 -0
- package/dist/commands/agent.js +13 -1
- package/dist/commands/fork.js +13 -2
- package/dist/commands/model.js +12 -0
- package/dist/commands/remove-project.js +26 -16
- package/dist/commands/resume.js +9 -0
- package/dist/commands/session.js +13 -0
- package/dist/commands/share.js +10 -1
- package/dist/commands/undo-redo.js +13 -4
- package/dist/database.js +9 -5
- package/dist/discord-bot.js +21 -8
- package/dist/errors.js +110 -0
- package/dist/genai-worker.js +18 -16
- package/dist/markdown.js +96 -85
- package/dist/markdown.test.js +10 -3
- package/dist/message-formatting.js +50 -37
- package/dist/opencode.js +43 -46
- package/dist/session-handler.js +100 -2
- package/dist/system-message.js +2 -0
- package/dist/tools.js +18 -8
- package/dist/voice-handler.js +48 -25
- package/dist/voice.js +159 -131
- package/package.json +2 -1
- package/src/cli.ts +12 -3
- package/src/commands/abort.ts +17 -7
- package/src/commands/add-project.ts +9 -0
- package/src/commands/agent.ts +13 -1
- package/src/commands/fork.ts +18 -7
- package/src/commands/model.ts +12 -0
- package/src/commands/remove-project.ts +28 -16
- package/src/commands/resume.ts +9 -0
- package/src/commands/session.ts +13 -0
- package/src/commands/share.ts +11 -1
- package/src/commands/undo-redo.ts +15 -6
- package/src/database.ts +9 -4
- package/src/discord-bot.ts +21 -7
- package/src/errors.ts +208 -0
- package/src/genai-worker.ts +20 -17
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +111 -95
- package/src/message-formatting.ts +55 -38
- package/src/opencode.ts +52 -49
- package/src/session-handler.ts +118 -3
- package/src/system-message.ts +2 -0
- package/src/tools.ts +18 -8
- package/src/voice-handler.ts +48 -23
- package/src/voice.ts +195 -148
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getChannelsWithDescriptions, createDiscordClient, getDatabase, startDis
|
|
|
9
9
|
import { Events, ChannelType, REST, Routes, SlashCommandBuilder, AttachmentBuilder, } from 'discord.js';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
|
+
import * as errore from 'errore';
|
|
12
13
|
import { createLogger } from './logger.js';
|
|
13
14
|
import { spawn, spawnSync, execSync } from 'node:child_process';
|
|
14
15
|
import http from 'node:http';
|
|
@@ -256,8 +257,8 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
256
257
|
.toJSON());
|
|
257
258
|
}
|
|
258
259
|
// Add agent-specific quick commands like /plan-agent, /build-agent
|
|
259
|
-
// Filter to primary/all mode agents (same as /agent command shows)
|
|
260
|
-
const primaryAgents = agents.filter((a) => a.mode === 'primary' || a.mode === 'all');
|
|
260
|
+
// Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
|
|
261
|
+
const primaryAgents = agents.filter((a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden);
|
|
261
262
|
for (const agent of primaryAgents) {
|
|
262
263
|
const sanitizedName = sanitizeAgentName(agent.name);
|
|
263
264
|
const commandName = `${sanitizedName}-agent`;
|
|
@@ -432,7 +433,12 @@ async function run({ restart, addChannels }) {
|
|
|
432
433
|
// This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
|
|
433
434
|
const currentDir = process.cwd();
|
|
434
435
|
s.start('Starting OpenCode server...');
|
|
435
|
-
const opencodePromise = initializeOpencodeForDirectory(currentDir)
|
|
436
|
+
const opencodePromise = initializeOpencodeForDirectory(currentDir).then((result) => {
|
|
437
|
+
if (errore.isError(result)) {
|
|
438
|
+
throw new Error(result.message);
|
|
439
|
+
}
|
|
440
|
+
return result;
|
|
441
|
+
});
|
|
436
442
|
s.message('Connecting to Discord...');
|
|
437
443
|
const discordClient = await createDiscordClient();
|
|
438
444
|
const guilds = [];
|
package/dist/commands/abort.js
CHANGED
|
@@ -5,6 +5,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { abortControllers } from '../session-handler.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
|
+
import * as errore from 'errore';
|
|
8
9
|
const logger = createLogger('ABORT');
|
|
9
10
|
export async function handleAbortCommand({ command }) {
|
|
10
11
|
const channel = command.channel;
|
|
@@ -51,13 +52,21 @@ export async function handleAbortCommand({ command }) {
|
|
|
51
52
|
return;
|
|
52
53
|
}
|
|
53
54
|
const sessionId = row.session_id;
|
|
55
|
+
const existingController = abortControllers.get(sessionId);
|
|
56
|
+
if (existingController) {
|
|
57
|
+
existingController.abort(new Error('User requested abort'));
|
|
58
|
+
abortControllers.delete(sessionId);
|
|
59
|
+
}
|
|
60
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
61
|
+
if (errore.isError(getClient)) {
|
|
62
|
+
await command.reply({
|
|
63
|
+
content: `Failed to abort: ${getClient.message}`,
|
|
64
|
+
ephemeral: true,
|
|
65
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
54
69
|
try {
|
|
55
|
-
const existingController = abortControllers.get(sessionId);
|
|
56
|
-
if (existingController) {
|
|
57
|
-
existingController.abort(new Error('User requested abort'));
|
|
58
|
-
abortControllers.delete(sessionId);
|
|
59
|
-
}
|
|
60
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
61
70
|
await getClient().session.abort({
|
|
62
71
|
path: { id: sessionId },
|
|
63
72
|
});
|
|
@@ -6,6 +6,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
6
6
|
import { createProjectChannels } from '../channel-management.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
8
|
import { abbreviatePath } from '../utils.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
9
10
|
const logger = createLogger('ADD-PROJECT');
|
|
10
11
|
export async function handleAddProjectCommand({ command, appId }) {
|
|
11
12
|
await command.deferReply({ ephemeral: false });
|
|
@@ -18,6 +19,10 @@ export async function handleAddProjectCommand({ command, appId }) {
|
|
|
18
19
|
try {
|
|
19
20
|
const currentDir = process.cwd();
|
|
20
21
|
const getClient = await initializeOpencodeForDirectory(currentDir);
|
|
22
|
+
if (errore.isError(getClient)) {
|
|
23
|
+
await command.editReply(getClient.message);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
21
26
|
const projectsResponse = await getClient().project.list({});
|
|
22
27
|
if (!projectsResponse.data) {
|
|
23
28
|
await command.editReply('Failed to fetch projects');
|
|
@@ -60,6 +65,10 @@ export async function handleAddProjectAutocomplete({ interaction, appId, }) {
|
|
|
60
65
|
try {
|
|
61
66
|
const currentDir = process.cwd();
|
|
62
67
|
const getClient = await initializeOpencodeForDirectory(currentDir);
|
|
68
|
+
if (errore.isError(getClient)) {
|
|
69
|
+
await interaction.respond([]);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
63
72
|
const projectsResponse = await getClient().project.list({});
|
|
64
73
|
if (!projectsResponse.data) {
|
|
65
74
|
await interaction.respond([]);
|
package/dist/commands/agent.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runMo
|
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
7
7
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
9
10
|
const agentLogger = createLogger('AGENT');
|
|
10
11
|
const pendingAgentContexts = new Map();
|
|
11
12
|
/**
|
|
@@ -102,6 +103,10 @@ export async function handleAgentCommand({ interaction, appId, }) {
|
|
|
102
103
|
}
|
|
103
104
|
try {
|
|
104
105
|
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
106
|
+
if (errore.isError(getClient)) {
|
|
107
|
+
await interaction.editReply({ content: getClient.message });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
105
110
|
const agentsResponse = await getClient().app.agents({
|
|
106
111
|
query: { directory: context.dir },
|
|
107
112
|
});
|
|
@@ -110,7 +115,10 @@ export async function handleAgentCommand({ interaction, appId, }) {
|
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
112
117
|
const agents = agentsResponse.data
|
|
113
|
-
.filter((
|
|
118
|
+
.filter((agent) => {
|
|
119
|
+
const hidden = agent.hidden;
|
|
120
|
+
return (agent.mode === 'primary' || agent.mode === 'all') && !hidden;
|
|
121
|
+
})
|
|
114
122
|
.slice(0, 25);
|
|
115
123
|
if (agents.length === 0) {
|
|
116
124
|
await interaction.editReply({ content: 'No primary agents available' });
|
|
@@ -202,6 +210,10 @@ export async function handleQuickAgentCommand({ command, appId, }) {
|
|
|
202
210
|
}
|
|
203
211
|
try {
|
|
204
212
|
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
213
|
+
if (errore.isError(getClient)) {
|
|
214
|
+
await command.editReply({ content: getClient.message });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
205
217
|
const agentsResponse = await getClient().app.agents({
|
|
206
218
|
query: { directory: context.dir },
|
|
207
219
|
});
|
package/dist/commands/fork.js
CHANGED
|
@@ -5,6 +5,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, sendThreadMessage } from '../discord-utils.js';
|
|
6
6
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
|
+
import * as errore from 'errore';
|
|
8
9
|
const sessionLogger = createLogger('SESSION');
|
|
9
10
|
const forkLogger = createLogger('FORK');
|
|
10
11
|
export async function handleForkCommand(interaction) {
|
|
@@ -50,8 +51,14 @@ export async function handleForkCommand(interaction) {
|
|
|
50
51
|
// Defer reply before API calls to avoid 3-second timeout
|
|
51
52
|
await interaction.deferReply({ ephemeral: true });
|
|
52
53
|
const sessionId = row.session_id;
|
|
54
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
|
+
if (errore.isError(getClient)) {
|
|
56
|
+
await interaction.editReply({
|
|
57
|
+
content: `Failed to load messages: ${getClient.message}`,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
53
61
|
try {
|
|
54
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
62
|
const messagesResponse = await getClient().session.messages({
|
|
56
63
|
path: { id: sessionId },
|
|
57
64
|
});
|
|
@@ -120,8 +127,12 @@ export async function handleForkSelectMenu(interaction) {
|
|
|
120
127
|
return;
|
|
121
128
|
}
|
|
122
129
|
await interaction.deferReply({ ephemeral: false });
|
|
130
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
131
|
+
if (errore.isError(getClient)) {
|
|
132
|
+
await interaction.editReply(`Failed to fork session: ${getClient.message}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
123
135
|
try {
|
|
124
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
125
136
|
const forkResponse = await getClient().session.fork({
|
|
126
137
|
path: { id: sessionId },
|
|
127
138
|
body: { messageID: selectedMessageId },
|
package/dist/commands/model.js
CHANGED
|
@@ -6,6 +6,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
6
6
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
7
7
|
import { abortAndRetrySession } from '../session-handler.js';
|
|
8
8
|
import { createLogger } from '../logger.js';
|
|
9
|
+
import * as errore from 'errore';
|
|
9
10
|
const modelLogger = createLogger('MODEL');
|
|
10
11
|
// Store context by hash to avoid customId length limits (Discord max: 100 chars)
|
|
11
12
|
const pendingModelContexts = new Map();
|
|
@@ -77,6 +78,10 @@ export async function handleModelCommand({ interaction, appId, }) {
|
|
|
77
78
|
}
|
|
78
79
|
try {
|
|
79
80
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
81
|
+
if (errore.isError(getClient)) {
|
|
82
|
+
await interaction.editReply({ content: getClient.message });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
80
85
|
const providersResponse = await getClient().provider.list({
|
|
81
86
|
query: { directory: projectDirectory },
|
|
82
87
|
});
|
|
@@ -162,6 +167,13 @@ export async function handleProviderSelectMenu(interaction) {
|
|
|
162
167
|
}
|
|
163
168
|
try {
|
|
164
169
|
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
170
|
+
if (errore.isError(getClient)) {
|
|
171
|
+
await interaction.editReply({
|
|
172
|
+
content: getClient.message,
|
|
173
|
+
components: [],
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
165
177
|
const providersResponse = await getClient().provider.list({
|
|
166
178
|
query: { directory: context.dir },
|
|
167
179
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// /remove-project command - Remove Discord channels for a project.
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import * as errore from 'errore';
|
|
3
4
|
import { getDatabase } from '../database.js';
|
|
4
5
|
import { createLogger } from '../logger.js';
|
|
5
6
|
import { abbreviatePath } from '../utils.js';
|
|
@@ -25,20 +26,27 @@ export async function handleRemoveProjectCommand({ command, appId }) {
|
|
|
25
26
|
const deletedChannels = [];
|
|
26
27
|
const failedChannels = [];
|
|
27
28
|
for (const { channel_id, channel_type } of channels) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const channel = await errore.tryAsync({
|
|
30
|
+
try: () => guild.channels.fetch(channel_id),
|
|
31
|
+
catch: (e) => e,
|
|
32
|
+
});
|
|
33
|
+
if (errore.isError(channel)) {
|
|
34
|
+
logger.error(`Failed to fetch channel ${channel_id}:`, channel);
|
|
35
|
+
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (channel) {
|
|
39
|
+
try {
|
|
31
40
|
await channel.delete(`Removed by /remove-project command`);
|
|
32
41
|
deletedChannels.push(`${channel_type}: ${channel_id}`);
|
|
33
42
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.error(`Failed to delete channel ${channel_id}:`, error);
|
|
45
|
+
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
failedChannels.push(`${channel_type}: ${channel_id}`);
|
|
48
|
+
else {
|
|
49
|
+
deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`);
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
// Remove from database
|
|
@@ -76,14 +84,16 @@ export async function handleRemoveProjectAutocomplete({ interaction, appId, }) {
|
|
|
76
84
|
// Filter to only channels that exist in this guild
|
|
77
85
|
const projectsInGuild = [];
|
|
78
86
|
for (const { directory, channel_id } of allChannels) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
87
|
+
const channel = await errore.tryAsync({
|
|
88
|
+
try: () => guild.channels.fetch(channel_id),
|
|
89
|
+
catch: (e) => e,
|
|
90
|
+
});
|
|
91
|
+
if (errore.isError(channel)) {
|
|
86
92
|
// Channel not in this guild, skip
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (channel) {
|
|
96
|
+
projectsInGuild.push({ directory, channelId: channel_id });
|
|
87
97
|
}
|
|
88
98
|
}
|
|
89
99
|
const projects = projectsInGuild
|
package/dist/commands/resume.js
CHANGED
|
@@ -7,6 +7,7 @@ import { sendThreadMessage, resolveTextChannel, getKimakiMetadata } from '../dis
|
|
|
7
7
|
import { extractTagsArrays } from '../xml.js';
|
|
8
8
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
9
9
|
import { createLogger } from '../logger.js';
|
|
10
|
+
import * as errore from 'errore';
|
|
10
11
|
const logger = createLogger('RESUME');
|
|
11
12
|
export async function handleResumeCommand({ command, appId }) {
|
|
12
13
|
await command.deferReply({ ephemeral: false });
|
|
@@ -41,6 +42,10 @@ export async function handleResumeCommand({ command, appId }) {
|
|
|
41
42
|
}
|
|
42
43
|
try {
|
|
43
44
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
45
|
+
if (errore.isError(getClient)) {
|
|
46
|
+
await command.editReply(getClient.message);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
44
49
|
const sessionResponse = await getClient().session.get({
|
|
45
50
|
path: { id: sessionId },
|
|
46
51
|
});
|
|
@@ -111,6 +116,10 @@ export async function handleResumeAutocomplete({ interaction, appId, }) {
|
|
|
111
116
|
}
|
|
112
117
|
try {
|
|
113
118
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
119
|
+
if (errore.isError(getClient)) {
|
|
120
|
+
await interaction.respond([]);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
114
123
|
const sessionsResponse = await getClient().session.list();
|
|
115
124
|
if (!sessionsResponse.data) {
|
|
116
125
|
await interaction.respond([]);
|
package/dist/commands/session.js
CHANGED
|
@@ -8,6 +8,7 @@ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
|
8
8
|
import { extractTagsArrays } from '../xml.js';
|
|
9
9
|
import { handleOpencodeSession } from '../session-handler.js';
|
|
10
10
|
import { createLogger } from '../logger.js';
|
|
11
|
+
import * as errore from 'errore';
|
|
11
12
|
const logger = createLogger('SESSION');
|
|
12
13
|
export async function handleSessionCommand({ command, appId }) {
|
|
13
14
|
await command.deferReply({ ephemeral: false });
|
|
@@ -44,6 +45,10 @@ export async function handleSessionCommand({ command, appId }) {
|
|
|
44
45
|
}
|
|
45
46
|
try {
|
|
46
47
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
48
|
+
if (errore.isError(getClient)) {
|
|
49
|
+
await command.editReply(getClient.message);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
47
52
|
const files = filesString
|
|
48
53
|
.split(',')
|
|
49
54
|
.map((f) => f.trim())
|
|
@@ -102,6 +107,10 @@ async function handleAgentAutocomplete({ interaction, appId }) {
|
|
|
102
107
|
}
|
|
103
108
|
try {
|
|
104
109
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
110
|
+
if (errore.isError(getClient)) {
|
|
111
|
+
await interaction.respond([]);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
105
114
|
const agentsResponse = await getClient().app.agents({
|
|
106
115
|
query: { directory: projectDirectory },
|
|
107
116
|
});
|
|
@@ -165,6 +174,10 @@ export async function handleSessionAutocomplete({ interaction, appId, }) {
|
|
|
165
174
|
}
|
|
166
175
|
try {
|
|
167
176
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
177
|
+
if (errore.isError(getClient)) {
|
|
178
|
+
await interaction.respond([]);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
168
181
|
const response = await getClient().find.files({
|
|
169
182
|
query: {
|
|
170
183
|
query: currentQuery || '',
|
package/dist/commands/share.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getDatabase } from '../database.js';
|
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { createLogger } from '../logger.js';
|
|
7
|
+
import * as errore from 'errore';
|
|
7
8
|
const logger = createLogger('SHARE');
|
|
8
9
|
export async function handleShareCommand({ command }) {
|
|
9
10
|
const channel = command.channel;
|
|
@@ -50,8 +51,16 @@ export async function handleShareCommand({ command }) {
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
const sessionId = row.session_id;
|
|
54
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
|
+
if (errore.isError(getClient)) {
|
|
56
|
+
await command.reply({
|
|
57
|
+
content: `Failed to share session: ${getClient.message}`,
|
|
58
|
+
ephemeral: true,
|
|
59
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
53
63
|
try {
|
|
54
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
55
64
|
const response = await getClient().session.share({
|
|
56
65
|
path: { id: sessionId },
|
|
57
66
|
});
|
|
@@ -4,6 +4,7 @@ import { getDatabase } from '../database.js';
|
|
|
4
4
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
6
6
|
import { createLogger } from '../logger.js';
|
|
7
|
+
import * as errore from 'errore';
|
|
7
8
|
const logger = createLogger('UNDO-REDO');
|
|
8
9
|
export async function handleUndoCommand({ command }) {
|
|
9
10
|
const channel = command.channel;
|
|
@@ -50,9 +51,13 @@ export async function handleUndoCommand({ command }) {
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
const sessionId = row.session_id;
|
|
54
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
55
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
56
|
+
if (errore.isError(getClient)) {
|
|
57
|
+
await command.editReply(`Failed to undo: ${getClient.message}`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
53
60
|
try {
|
|
54
|
-
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
55
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
56
61
|
// Fetch messages to find the last assistant message
|
|
57
62
|
const messagesResponse = await getClient().session.messages({
|
|
58
63
|
path: { id: sessionId },
|
|
@@ -133,9 +138,13 @@ export async function handleRedoCommand({ command }) {
|
|
|
133
138
|
return;
|
|
134
139
|
}
|
|
135
140
|
const sessionId = row.session_id;
|
|
141
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
142
|
+
const getClient = await initializeOpencodeForDirectory(directory);
|
|
143
|
+
if (errore.isError(getClient)) {
|
|
144
|
+
await command.editReply(`Failed to redo: ${getClient.message}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
136
147
|
try {
|
|
137
|
-
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
138
|
-
const getClient = await initializeOpencodeForDirectory(directory);
|
|
139
148
|
// Check if session has reverted state
|
|
140
149
|
const sessionResponse = await getClient().session.get({
|
|
141
150
|
path: { id: sessionId },
|
package/dist/database.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import Database from 'better-sqlite3';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import * as errore from 'errore';
|
|
7
8
|
import { createLogger } from './logger.js';
|
|
8
9
|
import { getDataDir } from './config.js';
|
|
9
10
|
const dbLogger = createLogger('DB');
|
|
@@ -11,11 +12,14 @@ let db = null;
|
|
|
11
12
|
export function getDatabase() {
|
|
12
13
|
if (!db) {
|
|
13
14
|
const dataDir = getDataDir();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const mkdirError = errore.tryFn({
|
|
16
|
+
try: () => {
|
|
17
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
18
|
+
},
|
|
19
|
+
catch: (e) => e,
|
|
20
|
+
});
|
|
21
|
+
if (errore.isError(mkdirError)) {
|
|
22
|
+
dbLogger.error(`Failed to create data directory ${dataDir}:`, mkdirError.message);
|
|
19
23
|
}
|
|
20
24
|
const dbPath = path.join(dataDir, 'discord-sessions.db');
|
|
21
25
|
dbLogger.log(`Opening database at: ${dbPath}`);
|
package/dist/discord-bot.js
CHANGED
|
@@ -18,6 +18,7 @@ export { getOpencodeSystemMessage } from './system-message.js';
|
|
|
18
18
|
export { ensureKimakiCategory, ensureKimakiAudioCategory, createProjectChannels, getChannelsWithDescriptions, } from './channel-management.js';
|
|
19
19
|
import { ChannelType, Client, Events, GatewayIntentBits, Partials, PermissionsBitField, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
20
20
|
import fs from 'node:fs';
|
|
21
|
+
import * as errore from 'errore';
|
|
21
22
|
import { extractTagsArrays } from './xml.js';
|
|
22
23
|
import { createLogger } from './logger.js';
|
|
23
24
|
import { setGlobalDispatcher, Agent } from 'undici';
|
|
@@ -89,11 +90,12 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
89
90
|
}
|
|
90
91
|
if (message.partial) {
|
|
91
92
|
discordLogger.log(`Fetching partial message ${message.id}`);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
const fetched = await errore.tryAsync({
|
|
94
|
+
try: () => message.fetch(),
|
|
95
|
+
catch: (e) => e,
|
|
96
|
+
});
|
|
97
|
+
if (errore.isError(fetched)) {
|
|
98
|
+
discordLogger.log(`Failed to fetch partial message ${message.id}:`, fetched.message);
|
|
97
99
|
return;
|
|
98
100
|
}
|
|
99
101
|
}
|
|
@@ -173,28 +175,39 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
|
|
|
173
175
|
if (projectDirectory) {
|
|
174
176
|
try {
|
|
175
177
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
178
|
+
if (errore.isError(getClient)) {
|
|
179
|
+
voiceLogger.error(`[SESSION] Failed to initialize OpenCode client:`, getClient.message);
|
|
180
|
+
throw new Error(getClient.message);
|
|
181
|
+
}
|
|
176
182
|
const client = getClient();
|
|
177
183
|
// get current session context (without system prompt, it would be duplicated)
|
|
178
184
|
if (row.session_id) {
|
|
179
|
-
|
|
185
|
+
const result = await getCompactSessionContext({
|
|
180
186
|
client,
|
|
181
187
|
sessionId: row.session_id,
|
|
182
188
|
includeSystemPrompt: false,
|
|
183
189
|
maxMessages: 15,
|
|
184
190
|
});
|
|
191
|
+
if (errore.isOk(result)) {
|
|
192
|
+
currentSessionContext = result;
|
|
193
|
+
}
|
|
185
194
|
}
|
|
186
195
|
// get last session context (with system prompt for project context)
|
|
187
|
-
const
|
|
196
|
+
const lastSessionResult = await getLastSessionId({
|
|
188
197
|
client,
|
|
189
198
|
excludeSessionId: row.session_id,
|
|
190
199
|
});
|
|
200
|
+
const lastSessionId = errore.unwrapOr(lastSessionResult, null);
|
|
191
201
|
if (lastSessionId) {
|
|
192
|
-
|
|
202
|
+
const result = await getCompactSessionContext({
|
|
193
203
|
client,
|
|
194
204
|
sessionId: lastSessionId,
|
|
195
205
|
includeSystemPrompt: true,
|
|
196
206
|
maxMessages: 10,
|
|
197
207
|
});
|
|
208
|
+
if (errore.isOk(result)) {
|
|
209
|
+
lastSessionContext = result;
|
|
210
|
+
}
|
|
198
211
|
}
|
|
199
212
|
}
|
|
200
213
|
catch (e) {
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// TaggedError definitions for type-safe error handling with errore.
|
|
2
|
+
// Errors are grouped by category: infrastructure, domain, and validation.
|
|
3
|
+
// Use errore.matchError() for exhaustive error handling in command handlers.
|
|
4
|
+
import * as errore from 'errore';
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
// INFRASTRUCTURE ERRORS - Server, filesystem, external services
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
export class DirectoryNotAccessibleError extends errore.TaggedError('DirectoryNotAccessibleError')() {
|
|
9
|
+
constructor(args) {
|
|
10
|
+
super({ ...args, message: `Directory does not exist or is not accessible: ${args.directory}` });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class ServerStartError extends errore.TaggedError('ServerStartError')() {
|
|
14
|
+
constructor(args) {
|
|
15
|
+
super({ ...args, message: `Server failed to start on port ${args.port}: ${args.reason}` });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export class ServerNotFoundError extends errore.TaggedError('ServerNotFoundError')() {
|
|
19
|
+
constructor(args) {
|
|
20
|
+
super({ ...args, message: `OpenCode server not found for directory: ${args.directory}` });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class ServerNotReadyError extends errore.TaggedError('ServerNotReadyError')() {
|
|
24
|
+
constructor(args) {
|
|
25
|
+
super({
|
|
26
|
+
...args,
|
|
27
|
+
message: `OpenCode server for directory "${args.directory}" is in an error state (no client available)`,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class ApiKeyMissingError extends errore.TaggedError('ApiKeyMissingError')() {
|
|
32
|
+
constructor(args) {
|
|
33
|
+
super({ ...args, message: `${args.service} API key is required` });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
// DOMAIN ERRORS - Sessions, messages, transcription
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
export class SessionNotFoundError extends errore.TaggedError('SessionNotFoundError')() {
|
|
40
|
+
constructor(args) {
|
|
41
|
+
super({ ...args, message: `Session ${args.sessionId} not found` });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export class SessionCreateError extends errore.TaggedError('SessionCreateError')() {
|
|
45
|
+
}
|
|
46
|
+
export class MessagesNotFoundError extends errore.TaggedError('MessagesNotFoundError')() {
|
|
47
|
+
constructor(args) {
|
|
48
|
+
super({ ...args, message: `No messages found for session ${args.sessionId}` });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export class TranscriptionError extends errore.TaggedError('TranscriptionError')() {
|
|
52
|
+
constructor(args) {
|
|
53
|
+
super({ ...args, message: `Transcription failed: ${args.reason}` });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export class GrepSearchError extends errore.TaggedError('GrepSearchError')() {
|
|
57
|
+
constructor(args) {
|
|
58
|
+
super({ ...args, message: `Grep search failed for pattern: ${args.pattern}` });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export class GlobSearchError extends errore.TaggedError('GlobSearchError')() {
|
|
62
|
+
constructor(args) {
|
|
63
|
+
super({ ...args, message: `Glob search failed for pattern: ${args.pattern}` });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
// VALIDATION ERRORS - Input validation, format checks
|
|
68
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
+
export class InvalidAudioFormatError extends errore.TaggedError('InvalidAudioFormatError')() {
|
|
70
|
+
constructor() {
|
|
71
|
+
super({ message: 'Invalid audio format' });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export class EmptyTranscriptionError extends errore.TaggedError('EmptyTranscriptionError')() {
|
|
75
|
+
constructor() {
|
|
76
|
+
super({ message: 'Model returned empty transcription' });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export class NoResponseContentError extends errore.TaggedError('NoResponseContentError')() {
|
|
80
|
+
constructor() {
|
|
81
|
+
super({ message: 'No response content from model' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export class NoToolResponseError extends errore.TaggedError('NoToolResponseError')() {
|
|
85
|
+
constructor() {
|
|
86
|
+
super({ message: 'No valid tool responses' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
90
|
+
// NETWORK ERRORS - Fetch and HTTP
|
|
91
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
92
|
+
export class FetchError extends errore.TaggedError('FetchError')() {
|
|
93
|
+
constructor(args) {
|
|
94
|
+
const causeMsg = args.cause instanceof Error ? args.cause.message : String(args.cause);
|
|
95
|
+
super({ ...args, message: `Fetch failed for ${args.url}: ${causeMsg}` });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
99
|
+
// API ERRORS - External service responses
|
|
100
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
101
|
+
export class DiscordApiError extends errore.TaggedError('DiscordApiError')() {
|
|
102
|
+
constructor(args) {
|
|
103
|
+
super({ ...args, message: `Discord API error: ${args.status}${args.body ? ` - ${args.body}` : ''}` });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export class OpenCodeApiError extends errore.TaggedError('OpenCodeApiError')() {
|
|
107
|
+
constructor(args) {
|
|
108
|
+
super({ ...args, message: `OpenCode API error (${args.status})${args.body ? `: ${args.body}` : ''}` });
|
|
109
|
+
}
|
|
110
|
+
}
|