kimaki 0.4.38 → 0.4.40

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 (55) hide show
  1. package/dist/cli.js +27 -23
  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 +14 -1
  10. package/dist/commands/share.js +10 -1
  11. package/dist/commands/undo-redo.js +13 -4
  12. package/dist/commands/worktree.js +180 -0
  13. package/dist/database.js +57 -5
  14. package/dist/discord-bot.js +48 -10
  15. package/dist/discord-utils.js +36 -0
  16. package/dist/errors.js +109 -0
  17. package/dist/genai-worker.js +18 -16
  18. package/dist/interaction-handler.js +6 -2
  19. package/dist/markdown.js +100 -85
  20. package/dist/markdown.test.js +10 -3
  21. package/dist/message-formatting.js +50 -37
  22. package/dist/opencode.js +43 -46
  23. package/dist/session-handler.js +100 -2
  24. package/dist/system-message.js +2 -0
  25. package/dist/tools.js +18 -8
  26. package/dist/voice-handler.js +48 -25
  27. package/dist/voice.js +159 -131
  28. package/package.json +4 -2
  29. package/src/cli.ts +31 -32
  30. package/src/commands/abort.ts +17 -7
  31. package/src/commands/add-project.ts +9 -0
  32. package/src/commands/agent.ts +13 -1
  33. package/src/commands/fork.ts +18 -7
  34. package/src/commands/model.ts +12 -0
  35. package/src/commands/remove-project.ts +28 -16
  36. package/src/commands/resume.ts +9 -0
  37. package/src/commands/session.ts +14 -1
  38. package/src/commands/share.ts +11 -1
  39. package/src/commands/undo-redo.ts +15 -6
  40. package/src/commands/worktree.ts +243 -0
  41. package/src/database.ts +104 -4
  42. package/src/discord-bot.ts +49 -9
  43. package/src/discord-utils.ts +50 -0
  44. package/src/errors.ts +138 -0
  45. package/src/genai-worker.ts +20 -17
  46. package/src/interaction-handler.ts +7 -2
  47. package/src/markdown.test.ts +13 -3
  48. package/src/markdown.ts +112 -95
  49. package/src/message-formatting.ts +55 -38
  50. package/src/opencode.ts +52 -49
  51. package/src/session-handler.ts +118 -3
  52. package/src/system-message.ts +2 -0
  53. package/src/tools.ts +18 -8
  54. package/src/voice-handler.ts +48 -23
  55. package/src/voice.ts +195 -148
package/dist/voice.js CHANGED
@@ -1,49 +1,51 @@
1
1
  // Audio transcription service using Google Gemini.
2
2
  // Transcribes voice messages with code-aware context, using grep/glob tools
3
3
  // to verify technical terms, filenames, and function names in the codebase.
4
+ // Uses errore for type-safe error handling.
4
5
  import { GoogleGenAI, Type } from '@google/genai';
6
+ import * as errore from 'errore';
5
7
  import { createLogger } from './logger.js';
6
8
  import { glob } from 'glob';
7
9
  import { ripGrep } from 'ripgrep-js';
10
+ import { ApiKeyMissingError, InvalidAudioFormatError, TranscriptionError, EmptyTranscriptionError, NoResponseContentError, NoToolResponseError, GrepSearchError, GlobSearchError, } from './errors.js';
8
11
  const voiceLogger = createLogger('VOICE');
9
- async function runGrep({ pattern, directory, }) {
10
- try {
11
- const results = await ripGrep(directory, {
12
- string: pattern,
13
- globs: ['!node_modules/**', '!.git/**', '!dist/**', '!build/**'],
14
- });
15
- if (results.length === 0) {
16
- return 'No matches found';
17
- }
18
- const output = results
19
- .slice(0, 10)
20
- .map((match) => {
21
- return `${match.path.text}:${match.line_number}: ${match.lines.text.trim()}`;
22
- })
23
- .join('\n');
24
- return output.slice(0, 2000);
25
- }
26
- catch (e) {
27
- voiceLogger.error('grep search failed:', e);
28
- return 'grep search failed';
29
- }
12
+ function runGrep({ pattern, directory }) {
13
+ return errore.tryAsync({
14
+ try: async () => {
15
+ const results = await ripGrep(directory, {
16
+ string: pattern,
17
+ globs: ['!node_modules/**', '!.git/**', '!dist/**', '!build/**'],
18
+ });
19
+ if (results.length === 0) {
20
+ return 'No matches found';
21
+ }
22
+ const output = results
23
+ .slice(0, 10)
24
+ .map((match) => {
25
+ return `${match.path.text}:${match.line_number}: ${match.lines.text.trim()}`;
26
+ })
27
+ .join('\n');
28
+ return output.slice(0, 2000);
29
+ },
30
+ catch: (e) => new GrepSearchError({ pattern, cause: e }),
31
+ });
30
32
  }
31
- async function runGlob({ pattern, directory, }) {
32
- try {
33
- const files = await glob(pattern, {
34
- cwd: directory,
35
- nodir: false,
36
- ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
37
- maxDepth: 10,
38
- });
39
- if (files.length === 0) {
40
- return 'No files found';
41
- }
42
- return files.slice(0, 30).join('\n');
43
- }
44
- catch (error) {
45
- return `Glob search failed: ${error instanceof Error ? error.message : 'Unknown error'}`;
46
- }
33
+ function runGlob({ pattern, directory }) {
34
+ return errore.tryAsync({
35
+ try: async () => {
36
+ const files = await glob(pattern, {
37
+ cwd: directory,
38
+ nodir: false,
39
+ ignore: ['node_modules/**', '.git/**', 'dist/**', 'build/**'],
40
+ maxDepth: 10,
41
+ });
42
+ if (files.length === 0) {
43
+ return 'No files found';
44
+ }
45
+ return files.slice(0, 30).join('\n');
46
+ },
47
+ catch: (e) => new GlobSearchError({ pattern, cause: e }),
48
+ });
47
49
  }
48
50
  const grepToolDeclaration = {
49
51
  name: 'grep',
@@ -99,14 +101,28 @@ function createToolRunner({ directory }) {
99
101
  if (name === 'grep' && hasDirectory) {
100
102
  const pattern = args?.pattern || '';
101
103
  voiceLogger.log(`Grep search: "${pattern}"`);
102
- const output = await runGrep({ pattern, directory });
104
+ const result = await runGrep({ pattern, directory });
105
+ const output = (() => {
106
+ if (result instanceof Error) {
107
+ voiceLogger.error('grep search failed:', result);
108
+ return 'grep search failed';
109
+ }
110
+ return result;
111
+ })();
103
112
  voiceLogger.log(`Grep result: ${output.slice(0, 100)}...`);
104
113
  return { type: 'toolResponse', name: 'grep', output };
105
114
  }
106
115
  if (name === 'glob' && hasDirectory) {
107
116
  const pattern = args?.pattern || '';
108
117
  voiceLogger.log(`Glob search: "${pattern}"`);
109
- const output = await runGlob({ pattern, directory });
118
+ const result = await runGlob({ pattern, directory });
119
+ const output = (() => {
120
+ if (result instanceof Error) {
121
+ voiceLogger.error('glob search failed:', result);
122
+ return 'glob search failed';
123
+ }
124
+ return result;
125
+ })();
110
126
  voiceLogger.log(`Glob result: ${output.slice(0, 100)}...`);
111
127
  return { type: 'toolResponse', name: 'glob', output };
112
128
  }
@@ -114,17 +130,25 @@ function createToolRunner({ directory }) {
114
130
  };
115
131
  }
116
132
  export async function runTranscriptionLoop({ genAI, model, initialContents, tools, temperature, toolRunner, maxSteps = 10, }) {
117
- let response = await genAI.models.generateContent({
118
- model,
119
- contents: initialContents,
120
- config: {
121
- temperature,
122
- thinkingConfig: {
123
- thinkingBudget: 1024,
133
+ // Wrap external API call that can throw
134
+ const initialResponse = await errore.tryAsync({
135
+ try: () => genAI.models.generateContent({
136
+ model,
137
+ contents: initialContents,
138
+ config: {
139
+ temperature,
140
+ thinkingConfig: {
141
+ thinkingBudget: 1024,
142
+ },
143
+ tools,
124
144
  },
125
- tools,
126
- },
145
+ }),
146
+ catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
127
147
  });
148
+ if (initialResponse instanceof Error) {
149
+ return initialResponse;
150
+ }
151
+ let response = initialResponse;
128
152
  const conversationHistory = [...initialContents];
129
153
  let stepsRemaining = maxSteps;
130
154
  while (true) {
@@ -135,7 +159,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
135
159
  voiceLogger.log(`No parts but got text response: "${text.slice(0, 100)}..."`);
136
160
  return text;
137
161
  }
138
- throw new Error('Transcription failed: No response content from model');
162
+ return new NoResponseContentError();
139
163
  }
140
164
  const functionCalls = candidate.content.parts.filter((part) => 'functionCall' in part && !!part.functionCall);
141
165
  if (functionCalls.length === 0) {
@@ -144,7 +168,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
144
168
  voiceLogger.log(`No function calls but got text: "${text.slice(0, 100)}..."`);
145
169
  return text;
146
170
  }
147
- throw new Error('Transcription failed: Model did not produce a transcription');
171
+ return new TranscriptionError({ reason: 'Model did not produce a transcription' });
148
172
  }
149
173
  conversationHistory.push({
150
174
  role: 'model',
@@ -159,7 +183,7 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
159
183
  const transcription = result.transcription?.trim() || '';
160
184
  voiceLogger.log(`Transcription result received: "${transcription.slice(0, 100)}..."`);
161
185
  if (!transcription) {
162
- throw new Error('Transcription failed: Model returned empty transcription');
186
+ return new EmptyTranscriptionError();
163
187
  }
164
188
  return transcription;
165
189
  }
@@ -186,67 +210,76 @@ export async function runTranscriptionLoop({ genAI, model, initialContents, tool
186
210
  }
187
211
  }
188
212
  if (functionResponseParts.length === 0) {
189
- throw new Error('Transcription failed: No valid tool responses');
213
+ return new NoToolResponseError();
190
214
  }
191
215
  conversationHistory.push({
192
216
  role: 'user',
193
217
  parts: functionResponseParts,
194
218
  });
195
- response = await genAI.models.generateContent({
196
- model,
197
- contents: conversationHistory,
198
- config: {
199
- temperature,
200
- thinkingConfig: {
201
- thinkingBudget: 512,
219
+ // Wrap external API call that can throw
220
+ const nextResponse = await errore.tryAsync({
221
+ try: () => genAI.models.generateContent({
222
+ model,
223
+ contents: conversationHistory,
224
+ config: {
225
+ temperature,
226
+ thinkingConfig: {
227
+ thinkingBudget: 512,
228
+ },
229
+ tools: stepsRemaining <= 0
230
+ ? [{ functionDeclarations: [transcriptionResultToolDeclaration] }]
231
+ : tools,
202
232
  },
203
- tools: stepsRemaining <= 0
204
- ? [{ functionDeclarations: [transcriptionResultToolDeclaration] }]
205
- : tools,
206
- },
233
+ }),
234
+ catch: (e) => new TranscriptionError({ reason: `API call failed: ${String(e)}`, cause: e }),
207
235
  });
236
+ if (nextResponse instanceof Error) {
237
+ return nextResponse;
238
+ }
239
+ response = nextResponse;
208
240
  }
209
241
  }
210
- export async function transcribeAudio({ audio, prompt, language, temperature, geminiApiKey, directory, currentSessionContext, lastSessionContext, }) {
211
- try {
212
- const apiKey = geminiApiKey || process.env.GEMINI_API_KEY;
213
- if (!apiKey) {
214
- throw new Error('Gemini API key is required for audio transcription');
215
- }
216
- const genAI = new GoogleGenAI({ apiKey });
217
- let audioBase64;
242
+ export function transcribeAudio({ audio, prompt, language, temperature, geminiApiKey, directory, currentSessionContext, lastSessionContext, }) {
243
+ const apiKey = geminiApiKey || process.env.GEMINI_API_KEY;
244
+ if (!apiKey) {
245
+ return Promise.resolve(new ApiKeyMissingError({ service: 'Gemini' }));
246
+ }
247
+ const genAI = new GoogleGenAI({ apiKey });
248
+ const audioBase64 = (() => {
218
249
  if (typeof audio === 'string') {
219
- audioBase64 = audio;
220
- }
221
- else if (audio instanceof Buffer) {
222
- audioBase64 = audio.toString('base64');
250
+ return audio;
223
251
  }
224
- else if (audio instanceof Uint8Array) {
225
- audioBase64 = Buffer.from(audio).toString('base64');
252
+ if (audio instanceof Buffer) {
253
+ return audio.toString('base64');
226
254
  }
227
- else if (audio instanceof ArrayBuffer) {
228
- audioBase64 = Buffer.from(audio).toString('base64');
255
+ if (audio instanceof Uint8Array) {
256
+ return Buffer.from(audio).toString('base64');
229
257
  }
230
- else {
231
- throw new Error('Invalid audio format');
258
+ if (audio instanceof ArrayBuffer) {
259
+ return Buffer.from(audio).toString('base64');
232
260
  }
233
- const languageHint = language ? `The audio is in ${language}.\n\n` : '';
234
- // build session context section
235
- const sessionContextParts = [];
236
- if (lastSessionContext) {
237
- sessionContextParts.push(`<last_session>
261
+ return '';
262
+ })();
263
+ if (!audioBase64) {
264
+ return Promise.resolve(new InvalidAudioFormatError());
265
+ }
266
+ const languageHint = language ? `The audio is in ${language}.\n\n` : '';
267
+ // build session context section
268
+ const sessionContextParts = [];
269
+ if (lastSessionContext) {
270
+ sessionContextParts.push(`<last_session>
238
271
  ${lastSessionContext}
239
272
  </last_session>`);
240
- }
241
- if (currentSessionContext) {
242
- sessionContextParts.push(`<current_session>
273
+ }
274
+ if (currentSessionContext) {
275
+ sessionContextParts.push(`<current_session>
243
276
  ${currentSessionContext}
244
277
  </current_session>`);
245
- }
246
- const sessionContextSection = sessionContextParts.length > 0
247
- ? `\nSession context (use to understand references to files, functions, tools used):\n${sessionContextParts.join('\n\n')}`
248
- : '';
249
- const transcriptionPrompt = `${languageHint}Transcribe this audio for a coding agent (like Claude Code or OpenCode).
278
+ }
279
+ const sessionContextSection = sessionContextParts.length > 0
280
+ ? `\nSession context (use to understand references to files, functions, tools used):\n${sessionContextParts.join('\n\n')}`
281
+ : '';
282
+ const transcriptionPrompt = `${languageHint}Transcribe this audio for a coding agent (like Claude Code or OpenCode).
250
283
 
251
284
  CRITICAL REQUIREMENT: You MUST call the "transcriptionResult" tool to complete this task.
252
285
  - The transcriptionResult tool is the ONLY way to return results
@@ -275,42 +308,37 @@ ${sessionContextSection}
275
308
  REMEMBER: Call "transcriptionResult" tool with your transcription. This is mandatory.
276
309
 
277
310
  Note: "critique" is a CLI tool for showing diffs in the browser.`;
278
- // const hasDirectory = directory && directory.trim().length > 0
279
- const tools = [
280
- {
281
- functionDeclarations: [
282
- transcriptionResultToolDeclaration,
283
- // grep/glob disabled - was causing transcription to hang
284
- // ...(hasDirectory ? [grepToolDeclaration, globToolDeclaration] : []),
285
- ],
286
- },
287
- ];
288
- const initialContents = [
289
- {
290
- role: 'user',
291
- parts: [
292
- { text: transcriptionPrompt },
293
- {
294
- inlineData: {
295
- data: audioBase64,
296
- mimeType: 'audio/mpeg',
297
- },
311
+ // const hasDirectory = directory && directory.trim().length > 0
312
+ const tools = [
313
+ {
314
+ functionDeclarations: [
315
+ transcriptionResultToolDeclaration,
316
+ // grep/glob disabled - was causing transcription to hang
317
+ // ...(hasDirectory ? [grepToolDeclaration, globToolDeclaration] : []),
318
+ ],
319
+ },
320
+ ];
321
+ const initialContents = [
322
+ {
323
+ role: 'user',
324
+ parts: [
325
+ { text: transcriptionPrompt },
326
+ {
327
+ inlineData: {
328
+ data: audioBase64,
329
+ mimeType: 'audio/mpeg',
298
330
  },
299
- ],
300
- },
301
- ];
302
- const toolRunner = createToolRunner({ directory });
303
- return await runTranscriptionLoop({
304
- genAI,
305
- model: 'gemini-2.5-flash',
306
- initialContents,
307
- tools,
308
- temperature: temperature ?? 0.3,
309
- toolRunner,
310
- });
311
- }
312
- catch (error) {
313
- voiceLogger.error('Failed to transcribe audio:', error);
314
- throw new Error(`Audio transcription failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
315
- }
331
+ },
332
+ ],
333
+ },
334
+ ];
335
+ const toolRunner = createToolRunner({ directory });
336
+ return runTranscriptionLoop({
337
+ genAI,
338
+ model: 'gemini-2.5-flash',
339
+ initialContents,
340
+ tools,
341
+ temperature: temperature ?? 0.3,
342
+ toolRunner,
343
+ });
316
344
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.38",
5
+ "version": "0.4.40",
6
6
  "scripts": {
7
7
  "dev": "tsx --env-file .env src/cli.ts",
8
8
  "prepublishOnly": "pnpm tsc",
@@ -34,17 +34,19 @@
34
34
  "@clack/prompts": "^0.11.0",
35
35
  "@discordjs/voice": "^0.19.0",
36
36
  "@google/genai": "^1.34.0",
37
- "@opencode-ai/sdk": "^1.1.12",
37
+ "@opencode-ai/sdk": "^1.1.31",
38
38
  "@purinton/resampler": "^1.0.4",
39
39
  "ai": "^5.0.114",
40
40
  "better-sqlite3": "^12.3.0",
41
41
  "cac": "^6.7.14",
42
42
  "discord.js": "^14.16.3",
43
43
  "domhandler": "^5.0.3",
44
+ "errore": "workspace:^",
44
45
  "glob": "^13.0.0",
45
46
  "htmlparser2": "^10.0.0",
46
47
  "js-yaml": "^4.1.0",
47
48
  "marked": "^16.3.0",
49
+ "mime": "^4.1.0",
48
50
  "picocolors": "^1.1.1",
49
51
  "pretty-ms": "^9.3.0",
50
52
  "ripgrep-js": "^3.0.0",
package/src/cli.ts CHANGED
@@ -40,8 +40,10 @@ import {
40
40
  } from 'discord.js'
41
41
  import path from 'node:path'
42
42
  import fs from 'node:fs'
43
+ import * as errore from 'errore'
43
44
 
44
45
  import { createLogger } from './logger.js'
46
+ import { uploadFilesToDiscord } from './discord-utils.js'
45
47
  import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
46
48
  import http from 'node:http'
47
49
  import { setDataDir, getDataDir, getLockPort } from './config.js'
@@ -181,6 +183,7 @@ type AgentInfo = {
181
183
  name: string
182
184
  description?: string
183
185
  mode: string
186
+ hidden?: boolean
184
187
  }
185
188
 
186
189
  async function registerCommands({
@@ -209,7 +212,7 @@ async function registerCommands({
209
212
  })
210
213
  .toJSON(),
211
214
  new SlashCommandBuilder()
212
- .setName('session')
215
+ .setName('new-session')
213
216
  .setDescription('Start a new OpenCode session')
214
217
  .addStringOption((option) => {
215
218
  option.setName('prompt').setDescription('Prompt content for the session').setRequired(true)
@@ -234,6 +237,18 @@ async function registerCommands({
234
237
  return option
235
238
  })
236
239
  .toJSON(),
240
+ new SlashCommandBuilder()
241
+ .setName('new-worktree')
242
+ .setDescription('Create a new git worktree and start a session thread')
243
+ .addStringOption((option) => {
244
+ option
245
+ .setName('name')
246
+ .setDescription('Name for the worktree (will be formatted: lowercase, spaces to dashes)')
247
+ .setRequired(true)
248
+
249
+ return option
250
+ })
251
+ .toJSON(),
237
252
  new SlashCommandBuilder()
238
253
  .setName('add-project')
239
254
  .setDescription('Create Discord channels for a new OpenCode project')
@@ -343,8 +358,10 @@ async function registerCommands({
343
358
  }
344
359
 
345
360
  // Add agent-specific quick commands like /plan-agent, /build-agent
346
- // Filter to primary/all mode agents (same as /agent command shows)
347
- const primaryAgents = agents.filter((a) => a.mode === 'primary' || a.mode === 'all')
361
+ // Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
362
+ const primaryAgents = agents.filter(
363
+ (a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden,
364
+ )
348
365
  for (const agent of primaryAgents) {
349
366
  const sanitizedName = sanitizeAgentName(agent.name)
350
367
  const commandName = `${sanitizedName}-agent`
@@ -577,7 +594,12 @@ async function run({ restart, addChannels }: CliOptions) {
577
594
  // This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
578
595
  const currentDir = process.cwd()
579
596
  s.start('Starting OpenCode server...')
580
- const opencodePromise = initializeOpencodeForDirectory(currentDir)
597
+ const opencodePromise = initializeOpencodeForDirectory(currentDir).then((result) => {
598
+ if (result instanceof Error) {
599
+ throw new Error(result.message)
600
+ }
601
+ return result
602
+ })
581
603
 
582
604
  s.message('Connecting to Discord...')
583
605
  const discordClient = await createDiscordClient()
@@ -989,34 +1011,11 @@ cli
989
1011
  const s = spinner()
990
1012
  s.start(`Uploading ${resolvedFiles.length} file(s)...`)
991
1013
 
992
- for (const file of resolvedFiles) {
993
- const buffer = fs.readFileSync(file)
994
-
995
- const formData = new FormData()
996
- formData.append(
997
- 'payload_json',
998
- JSON.stringify({
999
- attachments: [{ id: 0, filename: path.basename(file) }],
1000
- }),
1001
- )
1002
- formData.append('files[0]', new Blob([buffer]), path.basename(file))
1003
-
1004
- const response = await fetch(
1005
- `https://discord.com/api/v10/channels/${threadRow.thread_id}/messages`,
1006
- {
1007
- method: 'POST',
1008
- headers: {
1009
- Authorization: `Bot ${botRow.token}`,
1010
- },
1011
- body: formData,
1012
- },
1013
- )
1014
-
1015
- if (!response.ok) {
1016
- const error = await response.text()
1017
- throw new Error(`Discord API error: ${response.status} - ${error}`)
1018
- }
1019
- }
1014
+ await uploadFilesToDiscord({
1015
+ threadId: threadRow.thread_id,
1016
+ botToken: botRow.token,
1017
+ files: resolvedFiles,
1018
+ })
1020
1019
 
1021
1020
  s.stop(`Uploaded ${resolvedFiles.length} file(s)!`)
1022
1021
 
@@ -7,6 +7,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
8
  import { abortControllers } from '../session-handler.js'
9
9
  import { createLogger } from '../logger.js'
10
+ import * as errore from 'errore'
10
11
 
11
12
  const logger = createLogger('ABORT')
12
13
 
@@ -64,14 +65,23 @@ export async function handleAbortCommand({ command }: CommandContext): Promise<v
64
65
 
65
66
  const sessionId = row.session_id
66
67
 
67
- try {
68
- const existingController = abortControllers.get(sessionId)
69
- if (existingController) {
70
- existingController.abort(new Error('User requested abort'))
71
- abortControllers.delete(sessionId)
72
- }
68
+ const existingController = abortControllers.get(sessionId)
69
+ if (existingController) {
70
+ existingController.abort(new Error('User requested abort'))
71
+ abortControllers.delete(sessionId)
72
+ }
73
+
74
+ const getClient = await initializeOpencodeForDirectory(directory)
75
+ if (getClient instanceof Error) {
76
+ await command.reply({
77
+ content: `Failed to abort: ${getClient.message}`,
78
+ ephemeral: true,
79
+ flags: SILENT_MESSAGE_FLAGS,
80
+ })
81
+ return
82
+ }
73
83
 
74
- const getClient = await initializeOpencodeForDirectory(directory)
84
+ try {
75
85
  await getClient().session.abort({
76
86
  path: { id: sessionId },
77
87
  })
@@ -8,6 +8,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js'
8
8
  import { createProjectChannels } from '../channel-management.js'
9
9
  import { createLogger } from '../logger.js'
10
10
  import { abbreviatePath } from '../utils.js'
11
+ import * as errore from 'errore'
11
12
 
12
13
  const logger = createLogger('ADD-PROJECT')
13
14
 
@@ -25,6 +26,10 @@ export async function handleAddProjectCommand({ command, appId }: CommandContext
25
26
  try {
26
27
  const currentDir = process.cwd()
27
28
  const getClient = await initializeOpencodeForDirectory(currentDir)
29
+ if (getClient instanceof Error) {
30
+ await command.editReply(getClient.message)
31
+ return
32
+ }
28
33
 
29
34
  const projectsResponse = await getClient().project.list({})
30
35
  if (!projectsResponse.data) {
@@ -89,6 +94,10 @@ export async function handleAddProjectAutocomplete({
89
94
  try {
90
95
  const currentDir = process.cwd()
91
96
  const getClient = await initializeOpencodeForDirectory(currentDir)
97
+ if (getClient instanceof Error) {
98
+ await interaction.respond([])
99
+ return
100
+ }
92
101
 
93
102
  const projectsResponse = await getClient().project.list({})
94
103
  if (!projectsResponse.data) {
@@ -15,6 +15,7 @@ import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runMo
15
15
  import { initializeOpencodeForDirectory } from '../opencode.js'
16
16
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
17
17
  import { createLogger } from '../logger.js'
18
+ import * as errore from 'errore'
18
19
 
19
20
  const agentLogger = createLogger('AGENT')
20
21
 
@@ -161,6 +162,10 @@ export async function handleAgentCommand({
161
162
 
162
163
  try {
163
164
  const getClient = await initializeOpencodeForDirectory(context.dir)
165
+ if (getClient instanceof Error) {
166
+ await interaction.editReply({ content: getClient.message })
167
+ return
168
+ }
164
169
 
165
170
  const agentsResponse = await getClient().app.agents({
166
171
  query: { directory: context.dir },
@@ -172,7 +177,10 @@ export async function handleAgentCommand({
172
177
  }
173
178
 
174
179
  const agents = agentsResponse.data
175
- .filter((a) => a.mode === 'primary' || a.mode === 'all')
180
+ .filter((agent) => {
181
+ const hidden = (agent as { hidden?: boolean }).hidden
182
+ return (agent.mode === 'primary' || agent.mode === 'all') && !hidden
183
+ })
176
184
  .slice(0, 25)
177
185
 
178
186
  if (agents.length === 0) {
@@ -289,6 +297,10 @@ export async function handleQuickAgentCommand({
289
297
 
290
298
  try {
291
299
  const getClient = await initializeOpencodeForDirectory(context.dir)
300
+ if (getClient instanceof Error) {
301
+ await command.editReply({ content: getClient.message })
302
+ return
303
+ }
292
304
 
293
305
  const agentsResponse = await getClient().app.agents({
294
306
  query: { directory: context.dir },