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.
Files changed (49) hide show
  1. package/dist/cli.js +9 -3
  2. package/dist/commands/abort.js +15 -6
  3. package/dist/commands/add-project.js +9 -0
  4. package/dist/commands/agent.js +13 -1
  5. package/dist/commands/fork.js +13 -2
  6. package/dist/commands/model.js +12 -0
  7. package/dist/commands/remove-project.js +26 -16
  8. package/dist/commands/resume.js +9 -0
  9. package/dist/commands/session.js +13 -0
  10. package/dist/commands/share.js +10 -1
  11. package/dist/commands/undo-redo.js +13 -4
  12. package/dist/database.js +9 -5
  13. package/dist/discord-bot.js +21 -8
  14. package/dist/errors.js +110 -0
  15. package/dist/genai-worker.js +18 -16
  16. package/dist/markdown.js +96 -85
  17. package/dist/markdown.test.js +10 -3
  18. package/dist/message-formatting.js +50 -37
  19. package/dist/opencode.js +43 -46
  20. package/dist/session-handler.js +100 -2
  21. package/dist/system-message.js +2 -0
  22. package/dist/tools.js +18 -8
  23. package/dist/voice-handler.js +48 -25
  24. package/dist/voice.js +159 -131
  25. package/package.json +2 -1
  26. package/src/cli.ts +12 -3
  27. package/src/commands/abort.ts +17 -7
  28. package/src/commands/add-project.ts +9 -0
  29. package/src/commands/agent.ts +13 -1
  30. package/src/commands/fork.ts +18 -7
  31. package/src/commands/model.ts +12 -0
  32. package/src/commands/remove-project.ts +28 -16
  33. package/src/commands/resume.ts +9 -0
  34. package/src/commands/session.ts +13 -0
  35. package/src/commands/share.ts +11 -1
  36. package/src/commands/undo-redo.ts +15 -6
  37. package/src/database.ts +9 -4
  38. package/src/discord-bot.ts +21 -7
  39. package/src/errors.ts +208 -0
  40. package/src/genai-worker.ts +20 -17
  41. package/src/markdown.test.ts +13 -3
  42. package/src/markdown.ts +111 -95
  43. package/src/message-formatting.ts +55 -38
  44. package/src/opencode.ts +52 -49
  45. package/src/session-handler.ts +118 -3
  46. package/src/system-message.ts +2 -0
  47. package/src/tools.ts +18 -8
  48. package/src/voice-handler.ts +48 -23
  49. 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 = [];
@@ -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([]);
@@ -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((a) => a.mode === 'primary' || a.mode === 'all')
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
  });
@@ -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 },
@@ -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
- try {
29
- const channel = await guild.channels.fetch(channel_id).catch(() => null);
30
- if (channel) {
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
- else {
35
- // Channel doesn't exist in this guild or was already deleted
36
- deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`);
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
- catch (error) {
40
- logger.error(`Failed to delete channel ${channel_id}:`, error);
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
- try {
80
- const channel = await guild.channels.fetch(channel_id).catch(() => null);
81
- if (channel) {
82
- projectsInGuild.push({ directory, channelId: channel_id });
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
@@ -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([]);
@@ -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 || '',
@@ -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
- try {
15
- fs.mkdirSync(dataDir, { recursive: true });
16
- }
17
- catch (error) {
18
- dbLogger.error(`Failed to create data directory ${dataDir}:`, error);
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}`);
@@ -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
- try {
93
- await message.fetch();
94
- }
95
- catch (error) {
96
- discordLogger.log(`Failed to fetch partial message ${message.id}:`, error);
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
- currentSessionContext = await getCompactSessionContext({
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 lastSessionId = await getLastSessionId({
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
- lastSessionContext = await getCompactSessionContext({
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
+ }