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
@@ -1,6 +1,7 @@
1
1
  // /remove-project command - Remove Discord channels for a project.
2
2
 
3
3
  import path from 'node:path'
4
+ import * as errore from 'errore'
4
5
  import type { CommandContext, AutocompleteContext } from './types.js'
5
6
  import { getDatabase } from '../database.js'
6
7
  import { createLogger } from '../logger.js'
@@ -36,19 +37,27 @@ export async function handleRemoveProjectCommand({ command, appId }: CommandCont
36
37
  const failedChannels: string[] = []
37
38
 
38
39
  for (const { channel_id, channel_type } of channels) {
39
- try {
40
- const channel = await guild.channels.fetch(channel_id).catch(() => null)
41
-
42
- if (channel) {
40
+ const channel = await errore.tryAsync({
41
+ try: () => guild.channels.fetch(channel_id),
42
+ catch: (e) => e as Error,
43
+ })
44
+
45
+ if (errore.isError(channel)) {
46
+ logger.error(`Failed to fetch channel ${channel_id}:`, channel)
47
+ failedChannels.push(`${channel_type}: ${channel_id}`)
48
+ continue
49
+ }
50
+
51
+ if (channel) {
52
+ try {
43
53
  await channel.delete(`Removed by /remove-project command`)
44
54
  deletedChannels.push(`${channel_type}: ${channel_id}`)
45
- } else {
46
- // Channel doesn't exist in this guild or was already deleted
47
- deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`)
55
+ } catch (error) {
56
+ logger.error(`Failed to delete channel ${channel_id}:`, error)
57
+ failedChannels.push(`${channel_type}: ${channel_id}`)
48
58
  }
49
- } catch (error) {
50
- logger.error(`Failed to delete channel ${channel_id}:`, error)
51
- failedChannels.push(`${channel_type}: ${channel_id}`)
59
+ } else {
60
+ deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`)
52
61
  }
53
62
  }
54
63
 
@@ -103,13 +112,16 @@ export async function handleRemoveProjectAutocomplete({
103
112
  const projectsInGuild: { directory: string; channelId: string }[] = []
104
113
 
105
114
  for (const { directory, channel_id } of allChannels) {
106
- try {
107
- const channel = await guild.channels.fetch(channel_id).catch(() => null)
108
- if (channel) {
109
- projectsInGuild.push({ directory, channelId: channel_id })
110
- }
111
- } catch {
115
+ const channel = await errore.tryAsync({
116
+ try: () => guild.channels.fetch(channel_id),
117
+ catch: (e) => e as Error,
118
+ })
119
+ if (errore.isError(channel)) {
112
120
  // Channel not in this guild, skip
121
+ continue
122
+ }
123
+ if (channel) {
124
+ projectsInGuild.push({ directory, channelId: channel_id })
113
125
  }
114
126
  }
115
127
 
@@ -14,6 +14,7 @@ import { sendThreadMessage, resolveTextChannel, getKimakiMetadata } from '../dis
14
14
  import { extractTagsArrays } from '../xml.js'
15
15
  import { collectLastAssistantParts } from '../message-formatting.js'
16
16
  import { createLogger } from '../logger.js'
17
+ import * as errore from 'errore'
17
18
 
18
19
  const logger = createLogger('RESUME')
19
20
 
@@ -60,6 +61,10 @@ export async function handleResumeCommand({ command, appId }: CommandContext): P
60
61
 
61
62
  try {
62
63
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
64
+ if (errore.isError(getClient)) {
65
+ await command.editReply(getClient.message)
66
+ return
67
+ }
63
68
 
64
69
  const sessionResponse = await getClient().session.get({
65
70
  path: { id: sessionId },
@@ -168,6 +173,10 @@ export async function handleResumeAutocomplete({
168
173
 
169
174
  try {
170
175
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
176
+ if (errore.isError(getClient)) {
177
+ await interaction.respond([])
178
+ return
179
+ }
171
180
 
172
181
  const sessionsResponse = await getClient().session.list()
173
182
  if (!sessionsResponse.data) {
@@ -10,6 +10,7 @@ import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
10
10
  import { extractTagsArrays } from '../xml.js'
11
11
  import { handleOpencodeSession } from '../session-handler.js'
12
12
  import { createLogger } from '../logger.js'
13
+ import * as errore from 'errore'
13
14
 
14
15
  const logger = createLogger('SESSION')
15
16
 
@@ -58,6 +59,10 @@ export async function handleSessionCommand({ command, appId }: CommandContext):
58
59
 
59
60
  try {
60
61
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
62
+ if (errore.isError(getClient)) {
63
+ await command.editReply(getClient.message)
64
+ return
65
+ }
61
66
 
62
67
  const files = filesString
63
68
  .split(',')
@@ -128,6 +133,10 @@ async function handleAgentAutocomplete({ interaction, appId }: AutocompleteConte
128
133
 
129
134
  try {
130
135
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
136
+ if (errore.isError(getClient)) {
137
+ await interaction.respond([])
138
+ return
139
+ }
131
140
 
132
141
  const agentsResponse = await getClient().app.agents({
133
142
  query: { directory: projectDirectory },
@@ -207,6 +216,10 @@ export async function handleSessionAutocomplete({
207
216
 
208
217
  try {
209
218
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
219
+ if (errore.isError(getClient)) {
220
+ await interaction.respond([])
221
+ return
222
+ }
210
223
 
211
224
  const response = await getClient().find.files({
212
225
  query: {
@@ -6,6 +6,7 @@ import { getDatabase } from '../database.js'
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
8
  import { createLogger } from '../logger.js'
9
+ import * as errore from 'errore'
9
10
 
10
11
  const logger = createLogger('SHARE')
11
12
 
@@ -63,8 +64,17 @@ export async function handleShareCommand({ command }: CommandContext): Promise<v
63
64
 
64
65
  const sessionId = row.session_id
65
66
 
67
+ const getClient = await initializeOpencodeForDirectory(directory)
68
+ if (errore.isError(getClient)) {
69
+ await command.reply({
70
+ content: `Failed to share session: ${getClient.message}`,
71
+ ephemeral: true,
72
+ flags: SILENT_MESSAGE_FLAGS,
73
+ })
74
+ return
75
+ }
76
+
66
77
  try {
67
- const getClient = await initializeOpencodeForDirectory(directory)
68
78
  const response = await getClient().session.share({
69
79
  path: { id: sessionId },
70
80
  })
@@ -6,6 +6,7 @@ import { getDatabase } from '../database.js'
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
8
  import { createLogger } from '../logger.js'
9
+ import * as errore from 'errore'
9
10
 
10
11
  const logger = createLogger('UNDO-REDO')
11
12
 
@@ -63,11 +64,15 @@ export async function handleUndoCommand({ command }: CommandContext): Promise<vo
63
64
 
64
65
  const sessionId = row.session_id
65
66
 
66
- try {
67
- await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
67
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
68
68
 
69
- const getClient = await initializeOpencodeForDirectory(directory)
69
+ const getClient = await initializeOpencodeForDirectory(directory)
70
+ if (errore.isError(getClient)) {
71
+ await command.editReply(`Failed to undo: ${getClient.message}`)
72
+ return
73
+ }
70
74
 
75
+ try {
71
76
  // Fetch messages to find the last assistant message
72
77
  const messagesResponse = await getClient().session.messages({
73
78
  path: { id: sessionId },
@@ -166,11 +171,15 @@ export async function handleRedoCommand({ command }: CommandContext): Promise<vo
166
171
 
167
172
  const sessionId = row.session_id
168
173
 
169
- try {
170
- await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
174
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
171
175
 
172
- const getClient = await initializeOpencodeForDirectory(directory)
176
+ const getClient = await initializeOpencodeForDirectory(directory)
177
+ if (errore.isError(getClient)) {
178
+ await command.editReply(`Failed to redo: ${getClient.message}`)
179
+ return
180
+ }
173
181
 
182
+ try {
174
183
  // Check if session has reverted state
175
184
  const sessionResponse = await getClient().session.get({
176
185
  path: { id: sessionId },
package/src/database.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import Database from 'better-sqlite3'
6
6
  import fs from 'node:fs'
7
7
  import path from 'node:path'
8
+ import * as errore from 'errore'
8
9
  import { createLogger } from './logger.js'
9
10
  import { getDataDir } from './config.js'
10
11
 
@@ -16,10 +17,14 @@ export function getDatabase(): Database.Database {
16
17
  if (!db) {
17
18
  const dataDir = getDataDir()
18
19
 
19
- try {
20
- fs.mkdirSync(dataDir, { recursive: true })
21
- } catch (error) {
22
- dbLogger.error(`Failed to create data directory ${dataDir}:`, error)
20
+ const mkdirError = errore.tryFn({
21
+ try: () => {
22
+ fs.mkdirSync(dataDir, { recursive: true })
23
+ },
24
+ catch: (e) => e as Error,
25
+ })
26
+ if (errore.isError(mkdirError)) {
27
+ dbLogger.error(`Failed to create data directory ${dataDir}:`, mkdirError.message)
23
28
  }
24
29
 
25
30
  const dbPath = path.join(dataDir, 'discord-sessions.db')
@@ -53,6 +53,7 @@ import {
53
53
  type ThreadChannel,
54
54
  } from 'discord.js'
55
55
  import fs from 'node:fs'
56
+ import * as errore from 'errore'
56
57
  import { extractTagsArrays } from './xml.js'
57
58
  import { createLogger } from './logger.js'
58
59
  import { setGlobalDispatcher, Agent } from 'undici'
@@ -149,10 +150,12 @@ export async function startDiscordBot({
149
150
  }
150
151
  if (message.partial) {
151
152
  discordLogger.log(`Fetching partial message ${message.id}`)
152
- try {
153
- await message.fetch()
154
- } catch (error) {
155
- discordLogger.log(`Failed to fetch partial message ${message.id}:`, error)
153
+ const fetched = await errore.tryAsync({
154
+ try: () => message.fetch(),
155
+ catch: (e) => e as Error,
156
+ })
157
+ if (errore.isError(fetched)) {
158
+ discordLogger.log(`Failed to fetch partial message ${message.id}:`, fetched.message)
156
159
  return
157
160
  }
158
161
  }
@@ -256,30 +259,41 @@ export async function startDiscordBot({
256
259
  if (projectDirectory) {
257
260
  try {
258
261
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
262
+ if (errore.isError(getClient)) {
263
+ voiceLogger.error(`[SESSION] Failed to initialize OpenCode client:`, getClient.message)
264
+ throw new Error(getClient.message)
265
+ }
259
266
  const client = getClient()
260
267
 
261
268
  // get current session context (without system prompt, it would be duplicated)
262
269
  if (row.session_id) {
263
- currentSessionContext = await getCompactSessionContext({
270
+ const result = await getCompactSessionContext({
264
271
  client,
265
272
  sessionId: row.session_id,
266
273
  includeSystemPrompt: false,
267
274
  maxMessages: 15,
268
275
  })
276
+ if (errore.isOk(result)) {
277
+ currentSessionContext = result
278
+ }
269
279
  }
270
280
 
271
281
  // get last session context (with system prompt for project context)
272
- const lastSessionId = await getLastSessionId({
282
+ const lastSessionResult = await getLastSessionId({
273
283
  client,
274
284
  excludeSessionId: row.session_id,
275
285
  })
286
+ const lastSessionId = errore.unwrapOr(lastSessionResult, null)
276
287
  if (lastSessionId) {
277
- lastSessionContext = await getCompactSessionContext({
288
+ const result = await getCompactSessionContext({
278
289
  client,
279
290
  sessionId: lastSessionId,
280
291
  includeSystemPrompt: true,
281
292
  maxMessages: 10,
282
293
  })
294
+ if (errore.isOk(result)) {
295
+ lastSessionContext = result
296
+ }
283
297
  }
284
298
  } catch (e) {
285
299
  voiceLogger.error(`Could not get session context:`, e)
package/src/errors.ts ADDED
@@ -0,0 +1,208 @@
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
+
5
+ import * as errore from 'errore'
6
+
7
+ // ═══════════════════════════════════════════════════════════════════════════
8
+ // INFRASTRUCTURE ERRORS - Server, filesystem, external services
9
+ // ═══════════════════════════════════════════════════════════════════════════
10
+
11
+ export class DirectoryNotAccessibleError extends errore.TaggedError('DirectoryNotAccessibleError')<{
12
+ directory: string
13
+ message: string
14
+ }>() {
15
+ constructor(args: { directory: string }) {
16
+ super({ ...args, message: `Directory does not exist or is not accessible: ${args.directory}` })
17
+ }
18
+ }
19
+
20
+ export class ServerStartError extends errore.TaggedError('ServerStartError')<{
21
+ port: number
22
+ reason: string
23
+ message: string
24
+ }>() {
25
+ constructor(args: { port: number; reason: string }) {
26
+ super({ ...args, message: `Server failed to start on port ${args.port}: ${args.reason}` })
27
+ }
28
+ }
29
+
30
+ export class ServerNotFoundError extends errore.TaggedError('ServerNotFoundError')<{
31
+ directory: string
32
+ message: string
33
+ }>() {
34
+ constructor(args: { directory: string }) {
35
+ super({ ...args, message: `OpenCode server not found for directory: ${args.directory}` })
36
+ }
37
+ }
38
+
39
+ export class ServerNotReadyError extends errore.TaggedError('ServerNotReadyError')<{
40
+ directory: string
41
+ message: string
42
+ }>() {
43
+ constructor(args: { directory: string }) {
44
+ super({
45
+ ...args,
46
+ message: `OpenCode server for directory "${args.directory}" is in an error state (no client available)`,
47
+ })
48
+ }
49
+ }
50
+
51
+ export class ApiKeyMissingError extends errore.TaggedError('ApiKeyMissingError')<{
52
+ service: string
53
+ message: string
54
+ }>() {
55
+ constructor(args: { service: string }) {
56
+ super({ ...args, message: `${args.service} API key is required` })
57
+ }
58
+ }
59
+
60
+ // ═══════════════════════════════════════════════════════════════════════════
61
+ // DOMAIN ERRORS - Sessions, messages, transcription
62
+ // ═══════════════════════════════════════════════════════════════════════════
63
+
64
+ export class SessionNotFoundError extends errore.TaggedError('SessionNotFoundError')<{
65
+ sessionId: string
66
+ message: string
67
+ }>() {
68
+ constructor(args: { sessionId: string }) {
69
+ super({ ...args, message: `Session ${args.sessionId} not found` })
70
+ }
71
+ }
72
+
73
+ export class SessionCreateError extends errore.TaggedError('SessionCreateError')<{
74
+ message: string
75
+ cause?: unknown
76
+ }>() {}
77
+
78
+ export class MessagesNotFoundError extends errore.TaggedError('MessagesNotFoundError')<{
79
+ sessionId: string
80
+ message: string
81
+ }>() {
82
+ constructor(args: { sessionId: string }) {
83
+ super({ ...args, message: `No messages found for session ${args.sessionId}` })
84
+ }
85
+ }
86
+
87
+ export class TranscriptionError extends errore.TaggedError('TranscriptionError')<{
88
+ reason: string
89
+ message: string
90
+ cause?: unknown
91
+ }>() {
92
+ constructor(args: { reason: string; cause?: unknown }) {
93
+ super({ ...args, message: `Transcription failed: ${args.reason}` })
94
+ }
95
+ }
96
+
97
+ export class GrepSearchError extends errore.TaggedError('GrepSearchError')<{
98
+ pattern: string
99
+ message: string
100
+ cause?: unknown
101
+ }>() {
102
+ constructor(args: { pattern: string; cause?: unknown }) {
103
+ super({ ...args, message: `Grep search failed for pattern: ${args.pattern}` })
104
+ }
105
+ }
106
+
107
+ export class GlobSearchError extends errore.TaggedError('GlobSearchError')<{
108
+ pattern: string
109
+ message: string
110
+ cause?: unknown
111
+ }>() {
112
+ constructor(args: { pattern: string; cause?: unknown }) {
113
+ super({ ...args, message: `Glob search failed for pattern: ${args.pattern}` })
114
+ }
115
+ }
116
+
117
+ // ═══════════════════════════════════════════════════════════════════════════
118
+ // VALIDATION ERRORS - Input validation, format checks
119
+ // ═══════════════════════════════════════════════════════════════════════════
120
+
121
+ export class InvalidAudioFormatError extends errore.TaggedError('InvalidAudioFormatError')<{
122
+ message: string
123
+ }>() {
124
+ constructor() {
125
+ super({ message: 'Invalid audio format' })
126
+ }
127
+ }
128
+
129
+ export class EmptyTranscriptionError extends errore.TaggedError('EmptyTranscriptionError')<{
130
+ message: string
131
+ }>() {
132
+ constructor() {
133
+ super({ message: 'Model returned empty transcription' })
134
+ }
135
+ }
136
+
137
+ export class NoResponseContentError extends errore.TaggedError('NoResponseContentError')<{
138
+ message: string
139
+ }>() {
140
+ constructor() {
141
+ super({ message: 'No response content from model' })
142
+ }
143
+ }
144
+
145
+ export class NoToolResponseError extends errore.TaggedError('NoToolResponseError')<{
146
+ message: string
147
+ }>() {
148
+ constructor() {
149
+ super({ message: 'No valid tool responses' })
150
+ }
151
+ }
152
+
153
+ // ═══════════════════════════════════════════════════════════════════════════
154
+ // NETWORK ERRORS - Fetch and HTTP
155
+ // ═══════════════════════════════════════════════════════════════════════════
156
+
157
+ export class FetchError extends errore.TaggedError('FetchError')<{
158
+ url: string
159
+ message: string
160
+ cause?: unknown
161
+ }>() {
162
+ constructor(args: { url: string; cause?: unknown }) {
163
+ const causeMsg = args.cause instanceof Error ? args.cause.message : String(args.cause)
164
+ super({ ...args, message: `Fetch failed for ${args.url}: ${causeMsg}` })
165
+ }
166
+ }
167
+
168
+ // ═══════════════════════════════════════════════════════════════════════════
169
+ // API ERRORS - External service responses
170
+ // ═══════════════════════════════════════════════════════════════════════════
171
+
172
+ export class DiscordApiError extends errore.TaggedError('DiscordApiError')<{
173
+ status: number
174
+ message: string
175
+ }>() {
176
+ constructor(args: { status: number; body?: string }) {
177
+ super({ ...args, message: `Discord API error: ${args.status}${args.body ? ` - ${args.body}` : ''}` })
178
+ }
179
+ }
180
+
181
+ export class OpenCodeApiError extends errore.TaggedError('OpenCodeApiError')<{
182
+ status: number
183
+ message: string
184
+ }>() {
185
+ constructor(args: { status: number; body?: string }) {
186
+ super({ ...args, message: `OpenCode API error (${args.status})${args.body ? `: ${args.body}` : ''}` })
187
+ }
188
+ }
189
+
190
+ // ═══════════════════════════════════════════════════════════════════════════
191
+ // UNION TYPES - For function signatures
192
+ // ═══════════════════════════════════════════════════════════════════════════
193
+
194
+ export type TranscriptionErrors =
195
+ | ApiKeyMissingError
196
+ | InvalidAudioFormatError
197
+ | TranscriptionError
198
+ | EmptyTranscriptionError
199
+ | NoResponseContentError
200
+ | NoToolResponseError
201
+
202
+ export type OpenCodeErrors =
203
+ | DirectoryNotAccessibleError
204
+ | ServerStartError
205
+ | ServerNotFoundError
206
+ | ServerNotReadyError
207
+
208
+ export type SessionErrors = SessionNotFoundError | MessagesNotFoundError | OpenCodeApiError
@@ -4,13 +4,14 @@
4
4
 
5
5
  import { parentPort, threadId } from 'node:worker_threads'
6
6
  import { createWriteStream, type WriteStream } from 'node:fs'
7
- import { mkdir } from 'node:fs/promises'
8
7
  import path from 'node:path'
8
+ import * as errore from 'errore'
9
9
  import { Resampler } from '@purinton/resampler'
10
10
  import * as prism from 'prism-media'
11
11
  import { startGenAiSession } from './genai.js'
12
12
  import type { Session } from '@google/genai'
13
13
  import { getTools } from './tools.js'
14
+ import { mkdir } from 'node:fs/promises'
14
15
  import type { WorkerInMessage, WorkerOutMessage } from './worker-types.js'
15
16
  import { createLogger } from './logger.js'
16
17
 
@@ -127,26 +128,28 @@ async function createAssistantAudioLogStream(
127
128
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
128
129
  const audioDir = path.join(process.cwd(), 'discord-audio-logs', guildId, channelId)
129
130
 
130
- try {
131
- await mkdir(audioDir, { recursive: true })
131
+ const mkdirError = await errore.tryAsync({
132
+ try: () => mkdir(audioDir, { recursive: true }),
133
+ catch: (e) => e as Error,
134
+ })
135
+ if (errore.isError(mkdirError)) {
136
+ workerLogger.error(`Failed to create audio log directory:`, mkdirError.message)
137
+ return null
138
+ }
132
139
 
133
- // Create stream for assistant audio (24kHz mono s16le PCM)
134
- const outputFileName = `assistant_${timestamp}.24.pcm`
135
- const outputFilePath = path.join(audioDir, outputFileName)
136
- const outputAudioStream = createWriteStream(outputFilePath)
140
+ // Create stream for assistant audio (24kHz mono s16le PCM)
141
+ const outputFileName = `assistant_${timestamp}.24.pcm`
142
+ const outputFilePath = path.join(audioDir, outputFileName)
143
+ const outputAudioStream = createWriteStream(outputFilePath)
137
144
 
138
- // Add error handler to prevent crashes
139
- outputAudioStream.on('error', (error) => {
140
- workerLogger.error(`Assistant audio log stream error:`, error)
141
- })
145
+ // Add error handler to prevent crashes
146
+ outputAudioStream.on('error', (error) => {
147
+ workerLogger.error(`Assistant audio log stream error:`, error)
148
+ })
142
149
 
143
- workerLogger.log(`Created assistant audio log: ${outputFilePath}`)
150
+ workerLogger.log(`Created assistant audio log: ${outputFilePath}`)
144
151
 
145
- return outputAudioStream
146
- } catch (error) {
147
- workerLogger.error(`Failed to create audio log directory:`, error)
148
- return null
149
- }
152
+ return outputAudioStream
150
153
  }
151
154
 
152
155
  // Handle encoded Opus packets
@@ -1,6 +1,7 @@
1
1
  import { test, expect, beforeAll, afterAll } from 'vitest'
2
2
  import { spawn, type ChildProcess } from 'child_process'
3
3
  import { OpencodeClient } from '@opencode-ai/sdk'
4
+ import * as errore from 'errore'
4
5
  import { ShareMarkdown, getCompactSessionContext } from './markdown.js'
5
6
 
6
7
  let serverProcess: ChildProcess
@@ -121,11 +122,14 @@ test('generate markdown from first available session', async () => {
121
122
  const exporter = new ShareMarkdown(client)
122
123
 
123
124
  // Generate markdown with system info
124
- const markdown = await exporter.generate({
125
+ const markdownResult = await exporter.generate({
125
126
  sessionID,
126
127
  includeSystemInfo: true,
127
128
  })
128
129
 
130
+ expect(errore.isOk(markdownResult)).toBe(true)
131
+ const markdown = errore.unwrap(markdownResult)
132
+
129
133
  console.log(`Generated markdown length: ${markdown.length} characters`)
130
134
 
131
135
  // Basic assertions
@@ -299,13 +303,16 @@ test('generate markdown from multiple sessions', async () => {
299
303
  test.skipIf(process.env.CI)('getCompactSessionContext generates compact format', async () => {
300
304
  const sessionId = 'ses_46c2205e8ffeOll1JUSuYChSAM'
301
305
 
302
- const context = await getCompactSessionContext({
306
+ const contextResult = await getCompactSessionContext({
303
307
  client,
304
308
  sessionId,
305
309
  includeSystemPrompt: true,
306
310
  maxMessages: 15,
307
311
  })
308
312
 
313
+ expect(errore.isOk(contextResult)).toBe(true)
314
+ const context = errore.unwrap(contextResult)
315
+
309
316
  console.log(`Generated compact context length: ${context.length} characters`)
310
317
 
311
318
  expect(context).toBeTruthy()
@@ -319,13 +326,16 @@ test.skipIf(process.env.CI)('getCompactSessionContext generates compact format',
319
326
  test.skipIf(process.env.CI)('getCompactSessionContext without system prompt', async () => {
320
327
  const sessionId = 'ses_46c2205e8ffeOll1JUSuYChSAM'
321
328
 
322
- const context = await getCompactSessionContext({
329
+ const contextResult = await getCompactSessionContext({
323
330
  client,
324
331
  sessionId,
325
332
  includeSystemPrompt: false,
326
333
  maxMessages: 10,
327
334
  })
328
335
 
336
+ expect(errore.isOk(contextResult)).toBe(true)
337
+ const context = errore.unwrap(contextResult)
338
+
329
339
  console.log(`Generated compact context (no system) length: ${context.length} characters`)
330
340
 
331
341
  expect(context).toBeTruthy()