shuvmaki 0.4.26

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 (94) hide show
  1. package/bin.js +70 -0
  2. package/dist/ai-tool-to-genai.js +210 -0
  3. package/dist/ai-tool-to-genai.test.js +267 -0
  4. package/dist/channel-management.js +97 -0
  5. package/dist/cli.js +709 -0
  6. package/dist/commands/abort.js +78 -0
  7. package/dist/commands/add-project.js +98 -0
  8. package/dist/commands/agent.js +152 -0
  9. package/dist/commands/ask-question.js +183 -0
  10. package/dist/commands/create-new-project.js +78 -0
  11. package/dist/commands/fork.js +186 -0
  12. package/dist/commands/model.js +313 -0
  13. package/dist/commands/permissions.js +126 -0
  14. package/dist/commands/queue.js +129 -0
  15. package/dist/commands/resume.js +145 -0
  16. package/dist/commands/session.js +142 -0
  17. package/dist/commands/share.js +80 -0
  18. package/dist/commands/types.js +2 -0
  19. package/dist/commands/undo-redo.js +161 -0
  20. package/dist/commands/user-command.js +145 -0
  21. package/dist/database.js +184 -0
  22. package/dist/discord-bot.js +384 -0
  23. package/dist/discord-utils.js +217 -0
  24. package/dist/escape-backticks.test.js +410 -0
  25. package/dist/format-tables.js +96 -0
  26. package/dist/format-tables.test.js +418 -0
  27. package/dist/genai-worker-wrapper.js +109 -0
  28. package/dist/genai-worker.js +297 -0
  29. package/dist/genai.js +232 -0
  30. package/dist/interaction-handler.js +144 -0
  31. package/dist/logger.js +51 -0
  32. package/dist/markdown.js +310 -0
  33. package/dist/markdown.test.js +262 -0
  34. package/dist/message-formatting.js +273 -0
  35. package/dist/message-formatting.test.js +73 -0
  36. package/dist/openai-realtime.js +228 -0
  37. package/dist/opencode.js +216 -0
  38. package/dist/session-handler.js +580 -0
  39. package/dist/system-message.js +61 -0
  40. package/dist/tools.js +356 -0
  41. package/dist/utils.js +85 -0
  42. package/dist/voice-handler.js +541 -0
  43. package/dist/voice.js +314 -0
  44. package/dist/worker-types.js +4 -0
  45. package/dist/xml.js +92 -0
  46. package/dist/xml.test.js +32 -0
  47. package/package.json +60 -0
  48. package/src/__snapshots__/compact-session-context-no-system.md +35 -0
  49. package/src/__snapshots__/compact-session-context.md +47 -0
  50. package/src/ai-tool-to-genai.test.ts +296 -0
  51. package/src/ai-tool-to-genai.ts +255 -0
  52. package/src/channel-management.ts +161 -0
  53. package/src/cli.ts +1010 -0
  54. package/src/commands/abort.ts +94 -0
  55. package/src/commands/add-project.ts +139 -0
  56. package/src/commands/agent.ts +201 -0
  57. package/src/commands/ask-question.ts +276 -0
  58. package/src/commands/create-new-project.ts +111 -0
  59. package/src/commands/fork.ts +257 -0
  60. package/src/commands/model.ts +402 -0
  61. package/src/commands/permissions.ts +146 -0
  62. package/src/commands/queue.ts +181 -0
  63. package/src/commands/resume.ts +230 -0
  64. package/src/commands/session.ts +184 -0
  65. package/src/commands/share.ts +96 -0
  66. package/src/commands/types.ts +25 -0
  67. package/src/commands/undo-redo.ts +213 -0
  68. package/src/commands/user-command.ts +178 -0
  69. package/src/database.ts +220 -0
  70. package/src/discord-bot.ts +513 -0
  71. package/src/discord-utils.ts +282 -0
  72. package/src/escape-backticks.test.ts +447 -0
  73. package/src/format-tables.test.ts +440 -0
  74. package/src/format-tables.ts +110 -0
  75. package/src/genai-worker-wrapper.ts +160 -0
  76. package/src/genai-worker.ts +366 -0
  77. package/src/genai.ts +321 -0
  78. package/src/interaction-handler.ts +187 -0
  79. package/src/logger.ts +57 -0
  80. package/src/markdown.test.ts +358 -0
  81. package/src/markdown.ts +365 -0
  82. package/src/message-formatting.test.ts +81 -0
  83. package/src/message-formatting.ts +340 -0
  84. package/src/openai-realtime.ts +363 -0
  85. package/src/opencode.ts +277 -0
  86. package/src/session-handler.ts +758 -0
  87. package/src/system-message.ts +62 -0
  88. package/src/tools.ts +428 -0
  89. package/src/utils.ts +118 -0
  90. package/src/voice-handler.ts +760 -0
  91. package/src/voice.ts +432 -0
  92. package/src/worker-types.ts +66 -0
  93. package/src/xml.test.ts +37 -0
  94. package/src/xml.ts +121 -0
@@ -0,0 +1,440 @@
1
+ import { test, expect } from 'vitest'
2
+ import { formatMarkdownTables } from './format-tables.js'
3
+
4
+ test('formats simple table', () => {
5
+ const input = `| Name | Age |
6
+ | --- | --- |
7
+ | Alice | 30 |
8
+ | Bob | 25 |`
9
+ const result = formatMarkdownTables(input)
10
+ expect(result).toMatchInlineSnapshot(`
11
+ "\`\`\`
12
+ Name Age
13
+ ----- ---
14
+ Alice 30
15
+ Bob 25
16
+ \`\`\`
17
+ "
18
+ `)
19
+ })
20
+
21
+ test('formats table with varying column widths', () => {
22
+ const input = `| Item | Quantity | Price |
23
+ | --- | --- | --- |
24
+ | Apples | 10 | $5 |
25
+ | Oranges | 3 | $2 |
26
+ | Bananas with long name | 100 | $15.99 |`
27
+ const result = formatMarkdownTables(input)
28
+ expect(result).toMatchInlineSnapshot(`
29
+ "\`\`\`
30
+ Item Quantity Price
31
+ ---------------------- -------- ------
32
+ Apples 10 $5
33
+ Oranges 3 $2
34
+ Bananas with long name 100 $15.99
35
+ \`\`\`
36
+ "
37
+ `)
38
+ })
39
+
40
+ test('strips bold formatting from cells', () => {
41
+ const input = `| Header | Value |
42
+ | --- | --- |
43
+ | **Bold text** | Normal |
44
+ | Mixed **bold** text | Another |`
45
+ const result = formatMarkdownTables(input)
46
+ expect(result).toMatchInlineSnapshot(`
47
+ "\`\`\`
48
+ Header Value
49
+ --------------- -------
50
+ Bold text Normal
51
+ Mixed bold text Another
52
+ \`\`\`
53
+ "
54
+ `)
55
+ })
56
+
57
+ test('strips italic formatting from cells', () => {
58
+ const input = `| Header | Value |
59
+ | --- | --- |
60
+ | *Italic text* | Normal |
61
+ | _Also italic_ | Another |`
62
+ const result = formatMarkdownTables(input)
63
+ expect(result).toMatchInlineSnapshot(`
64
+ "\`\`\`
65
+ Header Value
66
+ ----------- -------
67
+ Italic text Normal
68
+ Also italic Another
69
+ \`\`\`
70
+ "
71
+ `)
72
+ })
73
+
74
+ test('extracts URL from links', () => {
75
+ const input = `| Name | Link |
76
+ | --- | --- |
77
+ | Google | [Click here](https://google.com) |
78
+ | GitHub | [GitHub Home](https://github.com) |`
79
+ const result = formatMarkdownTables(input)
80
+ expect(result).toMatchInlineSnapshot(`
81
+ "\`\`\`
82
+ Name Link
83
+ ------ ------------------
84
+ Google https://google.com
85
+ GitHub https://github.com
86
+ \`\`\`
87
+ "
88
+ `)
89
+ })
90
+
91
+ test('handles inline code in cells', () => {
92
+ const input = `| Function | Description |
93
+ | --- | --- |
94
+ | \`console.log\` | Logs to console |
95
+ | \`Array.map\` | Maps array items |`
96
+ const result = formatMarkdownTables(input)
97
+ expect(result).toMatchInlineSnapshot(`
98
+ "\`\`\`
99
+ Function Description
100
+ ----------- ----------------
101
+ console.log Logs to console
102
+ Array.map Maps array items
103
+ \`\`\`
104
+ "
105
+ `)
106
+ })
107
+
108
+ test('handles mixed formatting in single cell', () => {
109
+ const input = `| Description |
110
+ | --- |
111
+ | This has **bold**, *italic*, and \`code\` |
112
+ | Also [a link](https://example.com) here |`
113
+ const result = formatMarkdownTables(input)
114
+ expect(result).toMatchInlineSnapshot(`
115
+ "\`\`\`
116
+ Description
117
+ -------------------------------
118
+ This has bold, italic, and code
119
+ Also https://example.com here
120
+ \`\`\`
121
+ "
122
+ `)
123
+ })
124
+
125
+ test('handles strikethrough text', () => {
126
+ const input = `| Status | Item |
127
+ | --- | --- |
128
+ | Done | ~~Deleted item~~ |
129
+ | Active | Normal item |`
130
+ const result = formatMarkdownTables(input)
131
+ expect(result).toMatchInlineSnapshot(`
132
+ "\`\`\`
133
+ Status Item
134
+ ------ ------------
135
+ Done Deleted item
136
+ Active Normal item
137
+ \`\`\`
138
+ "
139
+ `)
140
+ })
141
+
142
+ test('preserves content before table', () => {
143
+ const input = `Here is some text before the table.
144
+
145
+ | Col A | Col B |
146
+ | --- | --- |
147
+ | 1 | 2 |`
148
+ const result = formatMarkdownTables(input)
149
+ expect(result).toMatchInlineSnapshot(`
150
+ "Here is some text before the table.
151
+
152
+ \`\`\`
153
+ Col A Col B
154
+ ----- -----
155
+ 1 2
156
+ \`\`\`
157
+ "
158
+ `)
159
+ })
160
+
161
+ test('preserves content after table', () => {
162
+ const input = `| Col A | Col B |
163
+ | --- | --- |
164
+ | 1 | 2 |
165
+
166
+ And here is text after.`
167
+ const result = formatMarkdownTables(input)
168
+ expect(result).toMatchInlineSnapshot(`
169
+ "\`\`\`
170
+ Col A Col B
171
+ ----- -----
172
+ 1 2
173
+ \`\`\`
174
+ And here is text after."
175
+ `)
176
+ })
177
+
178
+ test('preserves content before and after table', () => {
179
+ const input = `Some intro text.
180
+
181
+ | Name | Value |
182
+ | --- | --- |
183
+ | Key | 123 |
184
+
185
+ Some outro text.`
186
+ const result = formatMarkdownTables(input)
187
+ expect(result).toMatchInlineSnapshot(`
188
+ "Some intro text.
189
+
190
+ \`\`\`
191
+ Name Value
192
+ ---- -----
193
+ Key 123
194
+ \`\`\`
195
+ Some outro text."
196
+ `)
197
+ })
198
+
199
+ test('handles multiple tables in same content', () => {
200
+ const input = `First table:
201
+
202
+ | A | B |
203
+ | --- | --- |
204
+ | 1 | 2 |
205
+
206
+ Some text between.
207
+
208
+ Second table:
209
+
210
+ | X | Y | Z |
211
+ | --- | --- | --- |
212
+ | a | b | c |`
213
+ const result = formatMarkdownTables(input)
214
+ expect(result).toMatchInlineSnapshot(`
215
+ "First table:
216
+
217
+ \`\`\`
218
+ A B
219
+ - -
220
+ 1 2
221
+ \`\`\`
222
+ Some text between.
223
+
224
+ Second table:
225
+
226
+ \`\`\`
227
+ X Y Z
228
+ - - -
229
+ a b c
230
+ \`\`\`
231
+ "
232
+ `)
233
+ })
234
+
235
+ test('handles empty cells', () => {
236
+ const input = `| Name | Optional |
237
+ | --- | --- |
238
+ | Alice | |
239
+ | | Bob |
240
+ | | |`
241
+ const result = formatMarkdownTables(input)
242
+ expect(result).toMatchInlineSnapshot(`
243
+ "\`\`\`
244
+ Name Optional
245
+ ----- --------
246
+ Alice
247
+ Bob
248
+
249
+ \`\`\`
250
+ "
251
+ `)
252
+ })
253
+
254
+ test('handles single column table', () => {
255
+ const input = `| Items |
256
+ | --- |
257
+ | Apple |
258
+ | Banana |
259
+ | Cherry |`
260
+ const result = formatMarkdownTables(input)
261
+ expect(result).toMatchInlineSnapshot(`
262
+ "\`\`\`
263
+ Items
264
+ ------
265
+ Apple
266
+ Banana
267
+ Cherry
268
+ \`\`\`
269
+ "
270
+ `)
271
+ })
272
+
273
+ test('handles single row table', () => {
274
+ const input = `| A | B | C | D |
275
+ | --- | --- | --- | --- |
276
+ | 1 | 2 | 3 | 4 |`
277
+ const result = formatMarkdownTables(input)
278
+ expect(result).toMatchInlineSnapshot(`
279
+ "\`\`\`
280
+ A B C D
281
+ - - - -
282
+ 1 2 3 4
283
+ \`\`\`
284
+ "
285
+ `)
286
+ })
287
+
288
+ test('handles nested formatting', () => {
289
+ const input = `| Description |
290
+ | --- |
291
+ | **Bold with *nested italic* inside** |
292
+ | *Italic with **nested bold** inside* |`
293
+ const result = formatMarkdownTables(input)
294
+ expect(result).toMatchInlineSnapshot(`
295
+ "\`\`\`
296
+ Description
297
+ ------------------------------
298
+ Bold with nested italic inside
299
+ Italic with nested bold inside
300
+ \`\`\`
301
+ "
302
+ `)
303
+ })
304
+
305
+ test('handles image references', () => {
306
+ const input = `| Icon | Name |
307
+ | --- | --- |
308
+ | ![alt](https://example.com/icon.png) | Item 1 |
309
+ | ![](https://cdn.test.com/img.jpg) | Item 2 |`
310
+ const result = formatMarkdownTables(input)
311
+ expect(result).toMatchInlineSnapshot(`
312
+ "\`\`\`
313
+ Icon Name
314
+ ---------------------------- ------
315
+ https://example.com/icon.png Item 1
316
+ https://cdn.test.com/img.jpg Item 2
317
+ \`\`\`
318
+ "
319
+ `)
320
+ })
321
+
322
+ test('preserves code blocks alongside tables', () => {
323
+ const input = `Some code:
324
+
325
+ \`\`\`js
326
+ const x = 1
327
+ \`\`\`
328
+
329
+ A table:
330
+
331
+ | Key | Value |
332
+ | --- | --- |
333
+ | a | 1 |
334
+
335
+ More code:
336
+
337
+ \`\`\`python
338
+ print("hello")
339
+ \`\`\``
340
+ const result = formatMarkdownTables(input)
341
+ expect(result).toMatchInlineSnapshot(`
342
+ "Some code:
343
+
344
+ \`\`\`js
345
+ const x = 1
346
+ \`\`\`
347
+
348
+ A table:
349
+
350
+ \`\`\`
351
+ Key Value
352
+ --- -----
353
+ a 1
354
+ \`\`\`
355
+ More code:
356
+
357
+ \`\`\`python
358
+ print("hello")
359
+ \`\`\`"
360
+ `)
361
+ })
362
+
363
+ test('handles content without tables', () => {
364
+ const input = `Just some regular markdown.
365
+
366
+ - List item 1
367
+ - List item 2
368
+
369
+ **Bold text** and *italic*.`
370
+ const result = formatMarkdownTables(input)
371
+ expect(result).toMatchInlineSnapshot(`
372
+ "Just some regular markdown.
373
+
374
+ - List item 1
375
+ - List item 2
376
+
377
+ **Bold text** and *italic*."
378
+ `)
379
+ })
380
+
381
+ test('handles complex real-world table', () => {
382
+ const input = `## API Endpoints
383
+
384
+ | Method | Endpoint | Description | Auth |
385
+ | --- | --- | --- | --- |
386
+ | GET | \`/api/users\` | List all users | [Bearer token](https://docs.example.com/auth) |
387
+ | POST | \`/api/users\` | Create **new** user | Required |
388
+ | DELETE | \`/api/users/:id\` | ~~Remove~~ *Deactivate* user | Admin only |`
389
+ const result = formatMarkdownTables(input)
390
+ expect(result).toMatchInlineSnapshot(`
391
+ "## API Endpoints
392
+
393
+ \`\`\`
394
+ Method Endpoint Description Auth
395
+ ------ -------------- ---------------------- -----------------------------
396
+ GET /api/users List all users https://docs.example.com/auth
397
+ POST /api/users Create new user Required
398
+ DELETE /api/users/:id Remove Deactivate user Admin only
399
+ \`\`\`
400
+ "
401
+ `)
402
+ })
403
+
404
+ test('handles unicode content', () => {
405
+ const input = `| Emoji | Name | Country |
406
+ | --- | --- | --- |
407
+ | 🍎 | Apple | 日本 |
408
+ | 🍊 | Orange | España |
409
+ | 🍌 | Banana | Ελλάδα |`
410
+ const result = formatMarkdownTables(input)
411
+ expect(result).toMatchInlineSnapshot(`
412
+ "\`\`\`
413
+ Emoji Name Country
414
+ ----- ------ -------
415
+ 🍎 Apple 日本
416
+ 🍊 Orange España
417
+ 🍌 Banana Ελλάδα
418
+ \`\`\`
419
+ "
420
+ `)
421
+ })
422
+
423
+ test('handles numbers and special characters', () => {
424
+ const input = `| Price | Discount | Final |
425
+ | --- | --- | --- |
426
+ | $100.00 | -15% | $85.00 |
427
+ | €50,00 | -10% | €45,00 |
428
+ | £75.99 | N/A | £75.99 |`
429
+ const result = formatMarkdownTables(input)
430
+ expect(result).toMatchInlineSnapshot(`
431
+ "\`\`\`
432
+ Price Discount Final
433
+ ------- -------- ------
434
+ $100.00 -15% $85.00
435
+ €50,00 -10% €45,00
436
+ £75.99 N/A £75.99
437
+ \`\`\`
438
+ "
439
+ `)
440
+ })
@@ -0,0 +1,110 @@
1
+ // Markdown table to code block converter.
2
+ // Discord doesn't render GFM tables, so this converts them to
3
+ // space-aligned code blocks for proper monospace display.
4
+
5
+ import { Lexer, type Token, type Tokens } from 'marked'
6
+
7
+ export function formatMarkdownTables(markdown: string): string {
8
+ const lexer = new Lexer()
9
+ const tokens = lexer.lex(markdown)
10
+
11
+ let result = ''
12
+ for (const token of tokens) {
13
+ if (token.type === 'table') {
14
+ result += formatTableToken(token as Tokens.Table)
15
+ } else {
16
+ result += token.raw
17
+ }
18
+ }
19
+ return result
20
+ }
21
+
22
+ function formatTableToken(table: Tokens.Table): string {
23
+ const headers = table.header.map((cell) => {
24
+ return extractCellText(cell.tokens)
25
+ })
26
+ const rows = table.rows.map((row) => {
27
+ return row.map((cell) => {
28
+ return extractCellText(cell.tokens)
29
+ })
30
+ })
31
+
32
+ const columnWidths = calculateColumnWidths(headers, rows)
33
+ const lines: string[] = []
34
+
35
+ lines.push(formatRow(headers, columnWidths))
36
+ lines.push(formatSeparator(columnWidths))
37
+ for (const row of rows) {
38
+ lines.push(formatRow(row, columnWidths))
39
+ }
40
+
41
+ return '```\n' + lines.join('\n') + '\n```\n'
42
+ }
43
+
44
+ function extractCellText(tokens: Token[]): string {
45
+ const parts: string[] = []
46
+ for (const token of tokens) {
47
+ parts.push(extractTokenText(token))
48
+ }
49
+ return parts.join('').trim()
50
+ }
51
+
52
+ function extractTokenText(token: Token): string {
53
+ switch (token.type) {
54
+ case 'text':
55
+ case 'codespan':
56
+ case 'escape':
57
+ return token.text
58
+ case 'link':
59
+ return token.href
60
+ case 'image':
61
+ return token.href
62
+ case 'strong':
63
+ case 'em':
64
+ case 'del':
65
+ return token.tokens ? extractCellText(token.tokens) : token.text
66
+ case 'br':
67
+ return ' '
68
+ default: {
69
+ const tokenAny = token as { tokens?: Token[]; text?: string }
70
+ if (tokenAny.tokens && Array.isArray(tokenAny.tokens)) {
71
+ return extractCellText(tokenAny.tokens)
72
+ }
73
+ if (typeof tokenAny.text === 'string') {
74
+ return tokenAny.text
75
+ }
76
+ return ''
77
+ }
78
+ }
79
+ }
80
+
81
+ function calculateColumnWidths(
82
+ headers: string[],
83
+ rows: string[][],
84
+ ): number[] {
85
+ const widths = headers.map((h) => {
86
+ return h.length
87
+ })
88
+ for (const row of rows) {
89
+ for (let i = 0; i < row.length; i++) {
90
+ const cell = row[i] ?? ''
91
+ widths[i] = Math.max(widths[i] ?? 0, cell.length)
92
+ }
93
+ }
94
+ return widths
95
+ }
96
+
97
+ function formatRow(cells: string[], widths: number[]): string {
98
+ const paddedCells = cells.map((cell, i) => {
99
+ return cell.padEnd(widths[i] ?? 0)
100
+ })
101
+ return paddedCells.join(' ')
102
+ }
103
+
104
+ function formatSeparator(widths: number[]): string {
105
+ return widths
106
+ .map((w) => {
107
+ return '-'.repeat(w)
108
+ })
109
+ .join(' ')
110
+ }
@@ -0,0 +1,160 @@
1
+ // Main thread interface for the GenAI worker.
2
+ // Spawns and manages the worker thread, handling message passing for
3
+ // audio input/output, tool call completions, and graceful shutdown.
4
+
5
+ import { Worker } from 'node:worker_threads'
6
+ import type { WorkerInMessage, WorkerOutMessage } from './worker-types.js'
7
+ import type { Tool as AITool } from 'ai'
8
+ import { createLogger } from './logger.js'
9
+
10
+ const genaiWorkerLogger = createLogger('GENAI WORKER')
11
+ const genaiWrapperLogger = createLogger('GENAI WORKER WRAPPER')
12
+
13
+ export interface GenAIWorkerOptions {
14
+ directory: string
15
+ systemMessage?: string
16
+ guildId: string
17
+ channelId: string
18
+ appId: string
19
+ geminiApiKey?: string | null
20
+ onAssistantOpusPacket: (packet: ArrayBuffer) => void
21
+ onAssistantStartSpeaking?: () => void
22
+ onAssistantStopSpeaking?: () => void
23
+ onAssistantInterruptSpeaking?: () => void
24
+ onToolCallCompleted?: (params: {
25
+ sessionId: string
26
+ messageId: string
27
+ data?: any
28
+ error?: any
29
+ markdown?: string
30
+ }) => void
31
+ onError?: (error: string) => void
32
+ }
33
+
34
+ export interface GenAIWorker {
35
+ sendRealtimeInput(params: {
36
+ audio?: { mimeType: string; data: string }
37
+ audioStreamEnd?: boolean
38
+ }): void
39
+ sendTextInput(text: string): void
40
+ interrupt(): void
41
+ stop(): Promise<void>
42
+ }
43
+
44
+ export function createGenAIWorker(
45
+ options: GenAIWorkerOptions,
46
+ ): Promise<GenAIWorker> {
47
+ return new Promise((resolve, reject) => {
48
+ const worker = new Worker(
49
+ new URL('../dist/genai-worker.js', import.meta.url),
50
+ )
51
+
52
+ // Handle messages from worker
53
+ worker.on('message', (message: WorkerOutMessage) => {
54
+ switch (message.type) {
55
+ case 'assistantOpusPacket':
56
+ options.onAssistantOpusPacket(message.packet)
57
+ break
58
+ case 'assistantStartSpeaking':
59
+ options.onAssistantStartSpeaking?.()
60
+ break
61
+ case 'assistantStopSpeaking':
62
+ options.onAssistantStopSpeaking?.()
63
+ break
64
+ case 'assistantInterruptSpeaking':
65
+ options.onAssistantInterruptSpeaking?.()
66
+ break
67
+ case 'toolCallCompleted':
68
+ options.onToolCallCompleted?.(message)
69
+ break
70
+ case 'error':
71
+ genaiWorkerLogger.error('Error:', message.error)
72
+ options.onError?.(message.error)
73
+ break
74
+ case 'ready':
75
+ genaiWorkerLogger.log('Ready')
76
+ // Resolve with the worker interface
77
+ resolve({
78
+ sendRealtimeInput({ audio, audioStreamEnd }) {
79
+ worker.postMessage({
80
+ type: 'sendRealtimeInput',
81
+ audio,
82
+ audioStreamEnd,
83
+ } satisfies WorkerInMessage)
84
+ },
85
+ sendTextInput(text) {
86
+ worker.postMessage({
87
+ type: 'sendTextInput',
88
+ text,
89
+ } satisfies WorkerInMessage)
90
+ },
91
+ interrupt() {
92
+ worker.postMessage({
93
+ type: 'interrupt',
94
+ } satisfies WorkerInMessage)
95
+ },
96
+ async stop() {
97
+ genaiWrapperLogger.log('Stopping worker...')
98
+ // Send stop message to trigger graceful shutdown
99
+ worker.postMessage({ type: 'stop' } satisfies WorkerInMessage)
100
+
101
+ // Wait for worker to exit gracefully (with timeout)
102
+ await new Promise<void>((resolve) => {
103
+ let resolved = false
104
+
105
+ // Listen for worker exit
106
+ worker.once('exit', (code) => {
107
+ if (!resolved) {
108
+ resolved = true
109
+ genaiWrapperLogger.log(
110
+ `[GENAI WORKER WRAPPER] Worker exited with code ${code}`,
111
+ )
112
+ resolve()
113
+ }
114
+ })
115
+
116
+ // Timeout after 5 seconds and force terminate
117
+ setTimeout(() => {
118
+ if (!resolved) {
119
+ resolved = true
120
+ genaiWrapperLogger.log(
121
+ '[GENAI WORKER WRAPPER] Worker did not exit gracefully, terminating...',
122
+ )
123
+ worker.terminate().then(() => {
124
+ genaiWrapperLogger.log('Worker terminated')
125
+ resolve()
126
+ })
127
+ }
128
+ }, 5000)
129
+ })
130
+ },
131
+ })
132
+ break
133
+ }
134
+ })
135
+
136
+ // Handle worker errors
137
+ worker.on('error', (error) => {
138
+ genaiWorkerLogger.error('Worker error:', error)
139
+ reject(error)
140
+ })
141
+
142
+ worker.on('exit', (code) => {
143
+ if (code !== 0) {
144
+ genaiWorkerLogger.error(`Worker stopped with exit code ${code}`)
145
+ }
146
+ })
147
+
148
+ // Send initialization message
149
+ const initMessage: WorkerInMessage = {
150
+ type: 'init',
151
+ directory: options.directory,
152
+ systemMessage: options.systemMessage,
153
+ guildId: options.guildId,
154
+ channelId: options.channelId,
155
+ appId: options.appId,
156
+ geminiApiKey: options.geminiApiKey,
157
+ }
158
+ worker.postMessage(initMessage)
159
+ })
160
+ }