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,410 @@
1
+ import { test, expect } from 'vitest';
2
+ import { Lexer } from 'marked';
3
+ import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discord-utils.js';
4
+ test('escapes single backticks in code blocks', () => {
5
+ const input = '```js\nconst x = `hello`\n```';
6
+ const result = escapeBackticksInCodeBlocks(input);
7
+ expect(result).toMatchInlineSnapshot(`
8
+ "\`\`\`js
9
+ const x = \\\`hello\\\`
10
+ \`\`\`
11
+ "
12
+ `);
13
+ });
14
+ test('escapes backticks in code blocks with language', () => {
15
+ const input = '```typescript\nconst greeting = `Hello, ${name}!`\nconst inline = `test`\n```';
16
+ const result = escapeBackticksInCodeBlocks(input);
17
+ expect(result).toMatchInlineSnapshot(`
18
+ "\`\`\`typescript
19
+ const greeting = \\\`Hello, \${name}!\\\`
20
+ const inline = \\\`test\\\`
21
+ \`\`\`
22
+ "
23
+ `);
24
+ });
25
+ test('does not escape backticks outside code blocks', () => {
26
+ const input = 'This is `inline code` and this is a code block:\n```\nconst x = `template`\n```';
27
+ const result = escapeBackticksInCodeBlocks(input);
28
+ expect(result).toMatchInlineSnapshot(`
29
+ "This is \`inline code\` and this is a code block:
30
+ \`\`\`
31
+ const x = \\\`template\\\`
32
+ \`\`\`
33
+ "
34
+ `);
35
+ });
36
+ test('handles multiple code blocks', () => {
37
+ const input = `First block:
38
+ \`\`\`js
39
+ const a = \`test\`
40
+ \`\`\`
41
+
42
+ Some text with \`inline\` code
43
+
44
+ Second block:
45
+ \`\`\`python
46
+ name = f\`hello {world}\`
47
+ \`\`\``;
48
+ const result = escapeBackticksInCodeBlocks(input);
49
+ expect(result).toMatchInlineSnapshot(`
50
+ "First block:
51
+ \`\`\`js
52
+ const a = \\\`test\\\`
53
+ \`\`\`
54
+
55
+
56
+ Some text with \`inline\` code
57
+
58
+ Second block:
59
+ \`\`\`python
60
+ name = f\\\`hello {world}\\\`
61
+ \`\`\`
62
+ "
63
+ `);
64
+ });
65
+ test('handles code blocks without language', () => {
66
+ const input = '```\nconst x = `value`\n```';
67
+ const result = escapeBackticksInCodeBlocks(input);
68
+ expect(result).toMatchInlineSnapshot(`
69
+ "\`\`\`
70
+ const x = \\\`value\\\`
71
+ \`\`\`
72
+ "
73
+ `);
74
+ });
75
+ test('handles nested backticks in code blocks', () => {
76
+ const input = '```js\nconst nested = `outer ${`inner`} text`\n```';
77
+ const result = escapeBackticksInCodeBlocks(input);
78
+ expect(result).toMatchInlineSnapshot(`
79
+ "\`\`\`js
80
+ const nested = \\\`outer \${\\\`inner\\\`} text\\\`
81
+ \`\`\`
82
+ "
83
+ `);
84
+ });
85
+ test('preserves markdown outside code blocks', () => {
86
+ const input = `# Heading
87
+
88
+ This is **bold** and *italic* text
89
+
90
+ \`\`\`js
91
+ const code = \`with template\`
92
+ \`\`\`
93
+
94
+ - List item 1
95
+ - List item 2`;
96
+ const result = escapeBackticksInCodeBlocks(input);
97
+ expect(result).toMatchInlineSnapshot(`
98
+ "# Heading
99
+
100
+ This is **bold** and *italic* text
101
+
102
+ \`\`\`js
103
+ const code = \\\`with template\\\`
104
+ \`\`\`
105
+
106
+
107
+ - List item 1
108
+ - List item 2"
109
+ `);
110
+ });
111
+ test('does not escape code block delimiter backticks', () => {
112
+ const input = '```js\nconst x = `hello`\n```';
113
+ const result = escapeBackticksInCodeBlocks(input);
114
+ expect(result.startsWith('```')).toBe(true);
115
+ expect(result.endsWith('```\n')).toBe(true);
116
+ expect(result).toContain('\\`hello\\`');
117
+ expect(result).not.toContain('\\`\\`\\`js');
118
+ expect(result).not.toContain('\\`\\`\\`\n');
119
+ expect(result).toMatchInlineSnapshot(`
120
+ "\`\`\`js
121
+ const x = \\\`hello\\\`
122
+ \`\`\`
123
+ "
124
+ `);
125
+ });
126
+ test('splitMarkdownForDiscord returns single chunk for short content', () => {
127
+ const result = splitMarkdownForDiscord({
128
+ content: 'Hello world',
129
+ maxLength: 100,
130
+ });
131
+ expect(result).toMatchInlineSnapshot(`
132
+ [
133
+ "Hello world",
134
+ ]
135
+ `);
136
+ });
137
+ test('splitMarkdownForDiscord splits at line boundaries', () => {
138
+ const result = splitMarkdownForDiscord({
139
+ content: 'Line 1\nLine 2\nLine 3\nLine 4',
140
+ maxLength: 15,
141
+ });
142
+ expect(result).toMatchInlineSnapshot(`
143
+ [
144
+ "Line 1
145
+ Line 2
146
+ ",
147
+ "Line 3
148
+ Line 4",
149
+ ]
150
+ `);
151
+ });
152
+ test('splitMarkdownForDiscord preserves code blocks when not split', () => {
153
+ const result = splitMarkdownForDiscord({
154
+ content: '```js\nconst x = 1\n```',
155
+ maxLength: 100,
156
+ });
157
+ expect(result).toMatchInlineSnapshot(`
158
+ [
159
+ "\`\`\`js
160
+ const x = 1
161
+ \`\`\`",
162
+ ]
163
+ `);
164
+ });
165
+ test('splitMarkdownForDiscord adds closing and opening fences when splitting code block', () => {
166
+ const result = splitMarkdownForDiscord({
167
+ content: '```js\nline1\nline2\nline3\nline4\n```',
168
+ maxLength: 20,
169
+ });
170
+ expect(result).toMatchInlineSnapshot(`
171
+ [
172
+ "\`\`\`js
173
+ line1
174
+ line2
175
+ \`\`\`
176
+ ",
177
+ "\`\`\`js
178
+ line3
179
+ line4
180
+ \`\`\`
181
+ ",
182
+ ]
183
+ `);
184
+ });
185
+ test('splitMarkdownForDiscord handles code block with language', () => {
186
+ const result = splitMarkdownForDiscord({
187
+ content: '```typescript\nconst a = 1\nconst b = 2\n```',
188
+ maxLength: 30,
189
+ });
190
+ expect(result).toMatchInlineSnapshot(`
191
+ [
192
+ "\`\`\`typescript
193
+ const a = 1
194
+ \`\`\`
195
+ ",
196
+ "\`\`\`typescript
197
+ const b = 2
198
+ \`\`\`
199
+ ",
200
+ ]
201
+ `);
202
+ });
203
+ test('splitMarkdownForDiscord handles mixed content with code blocks', () => {
204
+ const result = splitMarkdownForDiscord({
205
+ content: 'Text before\n```js\ncode\n```\nText after',
206
+ maxLength: 25,
207
+ });
208
+ expect(result).toMatchInlineSnapshot(`
209
+ [
210
+ "Text before
211
+ \`\`\`js
212
+ code
213
+ \`\`\`
214
+ ",
215
+ "Text after",
216
+ ]
217
+ `);
218
+ });
219
+ test('splitMarkdownForDiscord handles code block without language', () => {
220
+ const result = splitMarkdownForDiscord({
221
+ content: '```\nline1\nline2\n```',
222
+ maxLength: 12,
223
+ });
224
+ expect(result).toMatchInlineSnapshot(`
225
+ [
226
+ "\`\`\`
227
+ line1
228
+ \`\`\`
229
+ ",
230
+ "\`\`\`
231
+ line2
232
+ \`\`\`
233
+ ",
234
+ ]
235
+ `);
236
+ });
237
+ test('splitMarkdownForDiscord handles multiple consecutive code blocks', () => {
238
+ const result = splitMarkdownForDiscord({
239
+ content: '```js\nfoo\n```\n```py\nbar\n```',
240
+ maxLength: 20,
241
+ });
242
+ expect(result).toMatchInlineSnapshot(`
243
+ [
244
+ "\`\`\`js
245
+ foo
246
+ \`\`\`
247
+ \`\`\`py
248
+ \`\`\`
249
+ ",
250
+ "\`\`\`py
251
+ bar
252
+ \`\`\`
253
+ ",
254
+ ]
255
+ `);
256
+ });
257
+ test('splitMarkdownForDiscord handles empty code block', () => {
258
+ const result = splitMarkdownForDiscord({
259
+ content: 'before\n```\n```\nafter',
260
+ maxLength: 50,
261
+ });
262
+ expect(result).toMatchInlineSnapshot(`
263
+ [
264
+ "before
265
+ \`\`\`
266
+ \`\`\`
267
+ after",
268
+ ]
269
+ `);
270
+ });
271
+ test('splitMarkdownForDiscord handles content exactly at maxLength', () => {
272
+ const result = splitMarkdownForDiscord({
273
+ content: '12345678901234567890',
274
+ maxLength: 20,
275
+ });
276
+ expect(result).toMatchInlineSnapshot(`
277
+ [
278
+ "12345678901234567890",
279
+ ]
280
+ `);
281
+ });
282
+ test('splitMarkdownForDiscord handles code block only', () => {
283
+ const result = splitMarkdownForDiscord({
284
+ content: '```ts\nconst x = 1\n```',
285
+ maxLength: 15,
286
+ });
287
+ expect(result).toMatchInlineSnapshot(`
288
+ [
289
+ "\`\`\`ts
290
+ \`\`\`
291
+ ",
292
+ "\`\`\`ts
293
+ const x = 1
294
+ \`\`\`
295
+ ",
296
+ ]
297
+ `);
298
+ });
299
+ test('splitMarkdownForDiscord handles code block at start with text after', () => {
300
+ const result = splitMarkdownForDiscord({
301
+ content: '```js\ncode\n```\nSome text after',
302
+ maxLength: 20,
303
+ });
304
+ expect(result).toMatchInlineSnapshot(`
305
+ [
306
+ "\`\`\`js
307
+ code
308
+ \`\`\`
309
+ ",
310
+ "Some text after",
311
+ ]
312
+ `);
313
+ });
314
+ test('splitMarkdownForDiscord handles text before code block at end', () => {
315
+ const result = splitMarkdownForDiscord({
316
+ content: 'Some text before\n```js\ncode\n```',
317
+ maxLength: 25,
318
+ });
319
+ expect(result).toMatchInlineSnapshot(`
320
+ [
321
+ "Some text before
322
+ \`\`\`js
323
+ \`\`\`
324
+ ",
325
+ "\`\`\`js
326
+ code
327
+ \`\`\`
328
+ ",
329
+ ]
330
+ `);
331
+ });
332
+ test('splitMarkdownForDiscord handles very long line inside code block', () => {
333
+ const result = splitMarkdownForDiscord({
334
+ content: '```js\nshort\nveryverylonglinethatexceedsmaxlength\nshort\n```',
335
+ maxLength: 25,
336
+ });
337
+ expect(result).toMatchInlineSnapshot(`
338
+ [
339
+ "\`\`\`js
340
+ short
341
+ \`\`\`
342
+ ",
343
+ "\`\`\`js
344
+ veryverylonglinethatexceedsmaxlength
345
+ \`\`\`
346
+ ",
347
+ "\`\`\`js
348
+ short
349
+ \`\`\`
350
+ ",
351
+ ]
352
+ `);
353
+ });
354
+ test('splitMarkdownForDiscord handles realistic long markdown with code block', () => {
355
+ const content = `Here is some explanation text before the code.
356
+
357
+ \`\`\`typescript
358
+ export function calculateTotal(items: Item[]): number {
359
+ let total = 0
360
+ for (const item of items) {
361
+ total += item.price * item.quantity
362
+ }
363
+ return total
364
+ }
365
+
366
+ export function formatCurrency(amount: number): string {
367
+ return new Intl.NumberFormat('en-US', {
368
+ style: 'currency',
369
+ currency: 'USD',
370
+ }).format(amount)
371
+ }
372
+ \`\`\`
373
+
374
+ And here is some text after the code block.`;
375
+ const result = splitMarkdownForDiscord({
376
+ content,
377
+ maxLength: 200,
378
+ });
379
+ expect(result).toMatchInlineSnapshot(`
380
+ [
381
+ "Here is some explanation text before the code.
382
+
383
+ \`\`\`typescript
384
+ export function calculateTotal(items: Item[]): number {
385
+ let total = 0
386
+ for (const item of items) {
387
+ \`\`\`
388
+ ",
389
+ "\`\`\`typescript
390
+ total += item.price * item.quantity
391
+ }
392
+ return total
393
+ }
394
+
395
+ export function formatCurrency(amount: number): string {
396
+ return new Intl.NumberFormat('en-US', {
397
+ style: 'currency',
398
+ \`\`\`
399
+ ",
400
+ "\`\`\`typescript
401
+ currency: 'USD',
402
+ }).format(amount)
403
+ }
404
+ \`\`\`
405
+
406
+
407
+ And here is some text after the code block.",
408
+ ]
409
+ `);
410
+ });
@@ -0,0 +1,96 @@
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
+ import { Lexer } from 'marked';
5
+ export function formatMarkdownTables(markdown) {
6
+ const lexer = new Lexer();
7
+ const tokens = lexer.lex(markdown);
8
+ let result = '';
9
+ for (const token of tokens) {
10
+ if (token.type === 'table') {
11
+ result += formatTableToken(token);
12
+ }
13
+ else {
14
+ result += token.raw;
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+ function formatTableToken(table) {
20
+ const headers = table.header.map((cell) => {
21
+ return extractCellText(cell.tokens);
22
+ });
23
+ const rows = table.rows.map((row) => {
24
+ return row.map((cell) => {
25
+ return extractCellText(cell.tokens);
26
+ });
27
+ });
28
+ const columnWidths = calculateColumnWidths(headers, rows);
29
+ const lines = [];
30
+ lines.push(formatRow(headers, columnWidths));
31
+ lines.push(formatSeparator(columnWidths));
32
+ for (const row of rows) {
33
+ lines.push(formatRow(row, columnWidths));
34
+ }
35
+ return '```\n' + lines.join('\n') + '\n```\n';
36
+ }
37
+ function extractCellText(tokens) {
38
+ const parts = [];
39
+ for (const token of tokens) {
40
+ parts.push(extractTokenText(token));
41
+ }
42
+ return parts.join('').trim();
43
+ }
44
+ function extractTokenText(token) {
45
+ switch (token.type) {
46
+ case 'text':
47
+ case 'codespan':
48
+ case 'escape':
49
+ return token.text;
50
+ case 'link':
51
+ return token.href;
52
+ case 'image':
53
+ return token.href;
54
+ case 'strong':
55
+ case 'em':
56
+ case 'del':
57
+ return token.tokens ? extractCellText(token.tokens) : token.text;
58
+ case 'br':
59
+ return ' ';
60
+ default: {
61
+ const tokenAny = token;
62
+ if (tokenAny.tokens && Array.isArray(tokenAny.tokens)) {
63
+ return extractCellText(tokenAny.tokens);
64
+ }
65
+ if (typeof tokenAny.text === 'string') {
66
+ return tokenAny.text;
67
+ }
68
+ return '';
69
+ }
70
+ }
71
+ }
72
+ function calculateColumnWidths(headers, rows) {
73
+ const widths = headers.map((h) => {
74
+ return h.length;
75
+ });
76
+ for (const row of rows) {
77
+ for (let i = 0; i < row.length; i++) {
78
+ const cell = row[i] ?? '';
79
+ widths[i] = Math.max(widths[i] ?? 0, cell.length);
80
+ }
81
+ }
82
+ return widths;
83
+ }
84
+ function formatRow(cells, widths) {
85
+ const paddedCells = cells.map((cell, i) => {
86
+ return cell.padEnd(widths[i] ?? 0);
87
+ });
88
+ return paddedCells.join(' ');
89
+ }
90
+ function formatSeparator(widths) {
91
+ return widths
92
+ .map((w) => {
93
+ return '-'.repeat(w);
94
+ })
95
+ .join(' ');
96
+ }