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