kimaki 0.4.10 → 0.4.12
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.
- package/dist/discordBot.js +123 -47
- package/dist/escape-backticks.test.js +286 -1
- package/dist/tools.js +3 -2
- package/package.json +1 -1
- package/src/discordBot.ts +148 -56
- package/src/escape-backticks.test.ts +302 -1
- package/src/tools.ts +6 -3
package/dist/discordBot.js
CHANGED
|
@@ -22,6 +22,50 @@ import { isAbortError } from './utils.js';
|
|
|
22
22
|
import { setGlobalDispatcher, Agent } from 'undici';
|
|
23
23
|
// disables the automatic 5 minutes abort after no body
|
|
24
24
|
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 }));
|
|
25
|
+
export const OPENCODE_SYSTEM_MESSAGE = `
|
|
26
|
+
The user is reading your messages from inside Discord, via kimaki.xyz
|
|
27
|
+
|
|
28
|
+
After each message, if you implemented changes, you can show the user a diff via an url running the command, to show the changes in working directory:
|
|
29
|
+
|
|
30
|
+
bunx critique web
|
|
31
|
+
|
|
32
|
+
you can also show latest commit changes using
|
|
33
|
+
|
|
34
|
+
bunx critique web HEAD~1
|
|
35
|
+
|
|
36
|
+
do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
|
|
37
|
+
|
|
38
|
+
## markdown
|
|
39
|
+
|
|
40
|
+
discord does support basic markdown features like code blocks, code blocks languages, inline code, bold, italic, quotes, etc.
|
|
41
|
+
|
|
42
|
+
the max heading level is 3, so do not use ####
|
|
43
|
+
|
|
44
|
+
headings are discouraged anyway. instead try to use bold text for titles which renders more nicely in Discord
|
|
45
|
+
|
|
46
|
+
## tables
|
|
47
|
+
|
|
48
|
+
discord does NOT support markdown gfm tables.
|
|
49
|
+
|
|
50
|
+
so instead of using full markdown tables ALWAYS show code snippets with space aligned cells:
|
|
51
|
+
|
|
52
|
+
\`\`\`
|
|
53
|
+
Item Qty Price
|
|
54
|
+
---------- --- -----
|
|
55
|
+
Apples 10 $5
|
|
56
|
+
Oranges 3 $2
|
|
57
|
+
\`\`\`
|
|
58
|
+
|
|
59
|
+
Using code blocks will make the content use monospaced font so that space will be aligned correctly
|
|
60
|
+
|
|
61
|
+
IMPORTANT: add enough space characters to align the table! otherwise the content will not look good and will be difficult to understand for the user
|
|
62
|
+
|
|
63
|
+
code blocks for tables and diagrams MUST have Max length of 85 characters. otherwise the content will wrap
|
|
64
|
+
|
|
65
|
+
## diagrams
|
|
66
|
+
|
|
67
|
+
you can create diagrams wrapping them in code blocks too.
|
|
68
|
+
`;
|
|
25
69
|
const discordLogger = createLogger('DISCORD');
|
|
26
70
|
const voiceLogger = createLogger('VOICE');
|
|
27
71
|
const opencodeLogger = createLogger('OPENCODE');
|
|
@@ -451,60 +495,20 @@ async function getOpenPort() {
|
|
|
451
495
|
async function sendThreadMessage(thread, content) {
|
|
452
496
|
const MAX_LENGTH = 2000;
|
|
453
497
|
content = escapeBackticksInCodeBlocks(content);
|
|
454
|
-
|
|
455
|
-
if (
|
|
456
|
-
|
|
498
|
+
const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH });
|
|
499
|
+
if (chunks.length > 1) {
|
|
500
|
+
discordLogger.log(`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`);
|
|
457
501
|
}
|
|
458
|
-
// Use marked's lexer to tokenize markdown content
|
|
459
|
-
const lexer = new Lexer();
|
|
460
|
-
const tokens = lexer.lex(content);
|
|
461
|
-
const chunks = [];
|
|
462
|
-
let currentChunk = '';
|
|
463
|
-
// Process each token and add to chunks
|
|
464
|
-
for (const token of tokens) {
|
|
465
|
-
const tokenText = token.raw || '';
|
|
466
|
-
// If adding this token would exceed limit and we have content, flush current chunk
|
|
467
|
-
if (currentChunk && currentChunk.length + tokenText.length > MAX_LENGTH) {
|
|
468
|
-
chunks.push(currentChunk);
|
|
469
|
-
currentChunk = '';
|
|
470
|
-
}
|
|
471
|
-
// If this single token is longer than MAX_LENGTH, split it
|
|
472
|
-
if (tokenText.length > MAX_LENGTH) {
|
|
473
|
-
if (currentChunk) {
|
|
474
|
-
chunks.push(currentChunk);
|
|
475
|
-
currentChunk = '';
|
|
476
|
-
}
|
|
477
|
-
let remainingText = tokenText;
|
|
478
|
-
while (remainingText.length > MAX_LENGTH) {
|
|
479
|
-
// Try to split at a newline if possible
|
|
480
|
-
let splitIndex = MAX_LENGTH;
|
|
481
|
-
const newlineIndex = remainingText.lastIndexOf('\n', MAX_LENGTH - 1);
|
|
482
|
-
if (newlineIndex > MAX_LENGTH * 0.7) {
|
|
483
|
-
splitIndex = newlineIndex + 1;
|
|
484
|
-
}
|
|
485
|
-
chunks.push(remainingText.slice(0, splitIndex));
|
|
486
|
-
remainingText = remainingText.slice(splitIndex);
|
|
487
|
-
}
|
|
488
|
-
currentChunk = remainingText;
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
currentChunk += tokenText;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
// Add any remaining content
|
|
495
|
-
if (currentChunk) {
|
|
496
|
-
chunks.push(currentChunk);
|
|
497
|
-
}
|
|
498
|
-
// Send all chunks
|
|
499
|
-
discordLogger.log(`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`);
|
|
500
502
|
let firstMessage;
|
|
501
503
|
for (let i = 0; i < chunks.length; i++) {
|
|
502
504
|
const chunk = chunks[i];
|
|
503
|
-
if (!chunk)
|
|
505
|
+
if (!chunk) {
|
|
504
506
|
continue;
|
|
507
|
+
}
|
|
505
508
|
const message = await thread.send(chunk);
|
|
506
|
-
if (i === 0)
|
|
509
|
+
if (i === 0) {
|
|
507
510
|
firstMessage = message;
|
|
511
|
+
}
|
|
508
512
|
}
|
|
509
513
|
return firstMessage;
|
|
510
514
|
}
|
|
@@ -621,6 +625,77 @@ export function escapeBackticksInCodeBlocks(markdown) {
|
|
|
621
625
|
}
|
|
622
626
|
return result;
|
|
623
627
|
}
|
|
628
|
+
export function splitMarkdownForDiscord({ content, maxLength, }) {
|
|
629
|
+
if (content.length <= maxLength) {
|
|
630
|
+
return [content];
|
|
631
|
+
}
|
|
632
|
+
const lexer = new Lexer();
|
|
633
|
+
const tokens = lexer.lex(content);
|
|
634
|
+
const lines = [];
|
|
635
|
+
for (const token of tokens) {
|
|
636
|
+
if (token.type === 'code') {
|
|
637
|
+
const lang = token.lang || '';
|
|
638
|
+
lines.push({ text: '```' + lang + '\n', inCodeBlock: false, lang, isOpeningFence: true, isClosingFence: false });
|
|
639
|
+
const codeLines = token.text.split('\n');
|
|
640
|
+
for (const codeLine of codeLines) {
|
|
641
|
+
lines.push({ text: codeLine + '\n', inCodeBlock: true, lang, isOpeningFence: false, isClosingFence: false });
|
|
642
|
+
}
|
|
643
|
+
lines.push({ text: '```\n', inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: true });
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
const rawLines = token.raw.split('\n');
|
|
647
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
648
|
+
const isLast = i === rawLines.length - 1;
|
|
649
|
+
const text = isLast ? rawLines[i] : rawLines[i] + '\n';
|
|
650
|
+
if (text) {
|
|
651
|
+
lines.push({ text, inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: false });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const chunks = [];
|
|
657
|
+
let currentChunk = '';
|
|
658
|
+
let currentLang = null;
|
|
659
|
+
for (const line of lines) {
|
|
660
|
+
const wouldExceed = currentChunk.length + line.text.length > maxLength;
|
|
661
|
+
if (wouldExceed && currentChunk) {
|
|
662
|
+
if (currentLang !== null) {
|
|
663
|
+
currentChunk += '```\n';
|
|
664
|
+
}
|
|
665
|
+
chunks.push(currentChunk);
|
|
666
|
+
if (line.isClosingFence && currentLang !== null) {
|
|
667
|
+
currentChunk = '';
|
|
668
|
+
currentLang = null;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
672
|
+
const lang = line.lang;
|
|
673
|
+
currentChunk = '```' + lang + '\n';
|
|
674
|
+
if (!line.isOpeningFence) {
|
|
675
|
+
currentChunk += line.text;
|
|
676
|
+
}
|
|
677
|
+
currentLang = lang;
|
|
678
|
+
}
|
|
679
|
+
else {
|
|
680
|
+
currentChunk = line.text;
|
|
681
|
+
currentLang = null;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
currentChunk += line.text;
|
|
686
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
687
|
+
currentLang = line.lang;
|
|
688
|
+
}
|
|
689
|
+
else if (line.isClosingFence) {
|
|
690
|
+
currentLang = null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (currentChunk) {
|
|
695
|
+
chunks.push(currentChunk);
|
|
696
|
+
}
|
|
697
|
+
return chunks;
|
|
698
|
+
}
|
|
624
699
|
/**
|
|
625
700
|
* Escape Discord formatting characters to prevent breaking code blocks and inline code
|
|
626
701
|
*/
|
|
@@ -1265,6 +1340,7 @@ async function handleOpencodeSession({ prompt, thread, projectDirectory, origina
|
|
|
1265
1340
|
path: { id: session.id },
|
|
1266
1341
|
body: {
|
|
1267
1342
|
parts,
|
|
1343
|
+
system: OPENCODE_SYSTEM_MESSAGE,
|
|
1268
1344
|
},
|
|
1269
1345
|
signal: abortController.signal,
|
|
1270
1346
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test, expect } from 'vitest';
|
|
2
2
|
import { Lexer } from 'marked';
|
|
3
|
-
import { escapeBackticksInCodeBlocks } from './discordBot.js';
|
|
3
|
+
import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discordBot.js';
|
|
4
4
|
test('escapes single backticks in code blocks', () => {
|
|
5
5
|
const input = '```js\nconst x = `hello`\n```';
|
|
6
6
|
const result = escapeBackticksInCodeBlocks(input);
|
|
@@ -123,3 +123,288 @@ const x = \\\`hello\\\`
|
|
|
123
123
|
"
|
|
124
124
|
`);
|
|
125
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
|
+
});
|
package/dist/tools.js
CHANGED
|
@@ -8,7 +8,7 @@ const toolsLogger = createLogger('TOOLS');
|
|
|
8
8
|
import { formatDistanceToNow } from 'date-fns';
|
|
9
9
|
import { ShareMarkdown } from './markdown.js';
|
|
10
10
|
import pc from 'picocolors';
|
|
11
|
-
import { initializeOpencodeForDirectory } from './discordBot.js';
|
|
11
|
+
import { initializeOpencodeForDirectory, OPENCODE_SYSTEM_MESSAGE, } from './discordBot.js';
|
|
12
12
|
export async function getTools({ onMessageCompleted, directory, }) {
|
|
13
13
|
const getClient = await initializeOpencodeForDirectory(directory);
|
|
14
14
|
const client = getClient();
|
|
@@ -48,6 +48,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
48
48
|
body: {
|
|
49
49
|
parts: [{ type: 'text', text: message }],
|
|
50
50
|
model: sessionModel,
|
|
51
|
+
system: OPENCODE_SYSTEM_MESSAGE,
|
|
51
52
|
},
|
|
52
53
|
})
|
|
53
54
|
.then(async (response) => {
|
|
@@ -114,7 +115,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
114
115
|
path: { id: session.data.id },
|
|
115
116
|
body: {
|
|
116
117
|
parts: [{ type: 'text', text: message }],
|
|
117
|
-
|
|
118
|
+
system: OPENCODE_SYSTEM_MESSAGE,
|
|
118
119
|
},
|
|
119
120
|
})
|
|
120
121
|
.then(async (response) => {
|
package/package.json
CHANGED
package/src/discordBot.ts
CHANGED
|
@@ -54,6 +54,51 @@ import { setGlobalDispatcher, Agent } from 'undici'
|
|
|
54
54
|
// disables the automatic 5 minutes abort after no body
|
|
55
55
|
setGlobalDispatcher(new Agent({ headersTimeout: 0, bodyTimeout: 0 }))
|
|
56
56
|
|
|
57
|
+
export const OPENCODE_SYSTEM_MESSAGE = `
|
|
58
|
+
The user is reading your messages from inside Discord, via kimaki.xyz
|
|
59
|
+
|
|
60
|
+
After each message, if you implemented changes, you can show the user a diff via an url running the command, to show the changes in working directory:
|
|
61
|
+
|
|
62
|
+
bunx critique web
|
|
63
|
+
|
|
64
|
+
you can also show latest commit changes using
|
|
65
|
+
|
|
66
|
+
bunx critique web HEAD~1
|
|
67
|
+
|
|
68
|
+
do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
|
|
69
|
+
|
|
70
|
+
## markdown
|
|
71
|
+
|
|
72
|
+
discord does support basic markdown features like code blocks, code blocks languages, inline code, bold, italic, quotes, etc.
|
|
73
|
+
|
|
74
|
+
the max heading level is 3, so do not use ####
|
|
75
|
+
|
|
76
|
+
headings are discouraged anyway. instead try to use bold text for titles which renders more nicely in Discord
|
|
77
|
+
|
|
78
|
+
## tables
|
|
79
|
+
|
|
80
|
+
discord does NOT support markdown gfm tables.
|
|
81
|
+
|
|
82
|
+
so instead of using full markdown tables ALWAYS show code snippets with space aligned cells:
|
|
83
|
+
|
|
84
|
+
\`\`\`
|
|
85
|
+
Item Qty Price
|
|
86
|
+
---------- --- -----
|
|
87
|
+
Apples 10 $5
|
|
88
|
+
Oranges 3 $2
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
Using code blocks will make the content use monospaced font so that space will be aligned correctly
|
|
92
|
+
|
|
93
|
+
IMPORTANT: add enough space characters to align the table! otherwise the content will not look good and will be difficult to understand for the user
|
|
94
|
+
|
|
95
|
+
code blocks for tables and diagrams MUST have Max length of 85 characters. otherwise the content will wrap
|
|
96
|
+
|
|
97
|
+
## diagrams
|
|
98
|
+
|
|
99
|
+
you can create diagrams wrapping them in code blocks too.
|
|
100
|
+
`
|
|
101
|
+
|
|
57
102
|
const discordLogger = createLogger('DISCORD')
|
|
58
103
|
const voiceLogger = createLogger('VOICE')
|
|
59
104
|
const opencodeLogger = createLogger('OPENCODE')
|
|
@@ -629,69 +674,24 @@ async function sendThreadMessage(
|
|
|
629
674
|
|
|
630
675
|
content = escapeBackticksInCodeBlocks(content)
|
|
631
676
|
|
|
632
|
-
|
|
633
|
-
if (content.length <= MAX_LENGTH) {
|
|
634
|
-
return await thread.send(content)
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Use marked's lexer to tokenize markdown content
|
|
638
|
-
const lexer = new Lexer()
|
|
639
|
-
const tokens = lexer.lex(content)
|
|
677
|
+
const chunks = splitMarkdownForDiscord({ content, maxLength: MAX_LENGTH })
|
|
640
678
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
for (const token of tokens) {
|
|
646
|
-
const tokenText = token.raw || ''
|
|
647
|
-
|
|
648
|
-
// If adding this token would exceed limit and we have content, flush current chunk
|
|
649
|
-
if (currentChunk && currentChunk.length + tokenText.length > MAX_LENGTH) {
|
|
650
|
-
chunks.push(currentChunk)
|
|
651
|
-
currentChunk = ''
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// If this single token is longer than MAX_LENGTH, split it
|
|
655
|
-
if (tokenText.length > MAX_LENGTH) {
|
|
656
|
-
if (currentChunk) {
|
|
657
|
-
chunks.push(currentChunk)
|
|
658
|
-
currentChunk = ''
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
let remainingText = tokenText
|
|
662
|
-
while (remainingText.length > MAX_LENGTH) {
|
|
663
|
-
// Try to split at a newline if possible
|
|
664
|
-
let splitIndex = MAX_LENGTH
|
|
665
|
-
const newlineIndex = remainingText.lastIndexOf('\n', MAX_LENGTH - 1)
|
|
666
|
-
if (newlineIndex > MAX_LENGTH * 0.7) {
|
|
667
|
-
splitIndex = newlineIndex + 1
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
chunks.push(remainingText.slice(0, splitIndex))
|
|
671
|
-
remainingText = remainingText.slice(splitIndex)
|
|
672
|
-
}
|
|
673
|
-
currentChunk = remainingText
|
|
674
|
-
} else {
|
|
675
|
-
currentChunk += tokenText
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Add any remaining content
|
|
680
|
-
if (currentChunk) {
|
|
681
|
-
chunks.push(currentChunk)
|
|
679
|
+
if (chunks.length > 1) {
|
|
680
|
+
discordLogger.log(
|
|
681
|
+
`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`,
|
|
682
|
+
)
|
|
682
683
|
}
|
|
683
684
|
|
|
684
|
-
// Send all chunks
|
|
685
|
-
discordLogger.log(
|
|
686
|
-
`MESSAGE: Splitting ${content.length} chars into ${chunks.length} messages`,
|
|
687
|
-
)
|
|
688
|
-
|
|
689
685
|
let firstMessage: Message | undefined
|
|
690
686
|
for (let i = 0; i < chunks.length; i++) {
|
|
691
687
|
const chunk = chunks[i]
|
|
692
|
-
if (!chunk)
|
|
688
|
+
if (!chunk) {
|
|
689
|
+
continue
|
|
690
|
+
}
|
|
693
691
|
const message = await thread.send(chunk)
|
|
694
|
-
if (i === 0)
|
|
692
|
+
if (i === 0) {
|
|
693
|
+
firstMessage = message
|
|
694
|
+
}
|
|
695
695
|
}
|
|
696
696
|
|
|
697
697
|
return firstMessage!
|
|
@@ -852,6 +852,97 @@ export function escapeBackticksInCodeBlocks(markdown: string): string {
|
|
|
852
852
|
return result
|
|
853
853
|
}
|
|
854
854
|
|
|
855
|
+
type LineInfo = {
|
|
856
|
+
text: string
|
|
857
|
+
inCodeBlock: boolean
|
|
858
|
+
lang: string
|
|
859
|
+
isOpeningFence: boolean
|
|
860
|
+
isClosingFence: boolean
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
export function splitMarkdownForDiscord({
|
|
864
|
+
content,
|
|
865
|
+
maxLength,
|
|
866
|
+
}: {
|
|
867
|
+
content: string
|
|
868
|
+
maxLength: number
|
|
869
|
+
}): string[] {
|
|
870
|
+
if (content.length <= maxLength) {
|
|
871
|
+
return [content]
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const lexer = new Lexer()
|
|
875
|
+
const tokens = lexer.lex(content)
|
|
876
|
+
|
|
877
|
+
const lines: LineInfo[] = []
|
|
878
|
+
for (const token of tokens) {
|
|
879
|
+
if (token.type === 'code') {
|
|
880
|
+
const lang = token.lang || ''
|
|
881
|
+
lines.push({ text: '```' + lang + '\n', inCodeBlock: false, lang, isOpeningFence: true, isClosingFence: false })
|
|
882
|
+
const codeLines = token.text.split('\n')
|
|
883
|
+
for (const codeLine of codeLines) {
|
|
884
|
+
lines.push({ text: codeLine + '\n', inCodeBlock: true, lang, isOpeningFence: false, isClosingFence: false })
|
|
885
|
+
}
|
|
886
|
+
lines.push({ text: '```\n', inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: true })
|
|
887
|
+
} else {
|
|
888
|
+
const rawLines = token.raw.split('\n')
|
|
889
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
890
|
+
const isLast = i === rawLines.length - 1
|
|
891
|
+
const text = isLast ? rawLines[i]! : rawLines[i]! + '\n'
|
|
892
|
+
if (text) {
|
|
893
|
+
lines.push({ text, inCodeBlock: false, lang: '', isOpeningFence: false, isClosingFence: false })
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const chunks: string[] = []
|
|
900
|
+
let currentChunk = ''
|
|
901
|
+
let currentLang: string | null = null
|
|
902
|
+
|
|
903
|
+
for (const line of lines) {
|
|
904
|
+
const wouldExceed = currentChunk.length + line.text.length > maxLength
|
|
905
|
+
|
|
906
|
+
if (wouldExceed && currentChunk) {
|
|
907
|
+
if (currentLang !== null) {
|
|
908
|
+
currentChunk += '```\n'
|
|
909
|
+
}
|
|
910
|
+
chunks.push(currentChunk)
|
|
911
|
+
|
|
912
|
+
if (line.isClosingFence && currentLang !== null) {
|
|
913
|
+
currentChunk = ''
|
|
914
|
+
currentLang = null
|
|
915
|
+
continue
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
919
|
+
const lang = line.lang
|
|
920
|
+
currentChunk = '```' + lang + '\n'
|
|
921
|
+
if (!line.isOpeningFence) {
|
|
922
|
+
currentChunk += line.text
|
|
923
|
+
}
|
|
924
|
+
currentLang = lang
|
|
925
|
+
} else {
|
|
926
|
+
currentChunk = line.text
|
|
927
|
+
currentLang = null
|
|
928
|
+
}
|
|
929
|
+
} else {
|
|
930
|
+
currentChunk += line.text
|
|
931
|
+
if (line.inCodeBlock || line.isOpeningFence) {
|
|
932
|
+
currentLang = line.lang
|
|
933
|
+
} else if (line.isClosingFence) {
|
|
934
|
+
currentLang = null
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
if (currentChunk) {
|
|
940
|
+
chunks.push(currentChunk)
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
return chunks
|
|
944
|
+
}
|
|
945
|
+
|
|
855
946
|
/**
|
|
856
947
|
* Escape Discord formatting characters to prevent breaking code blocks and inline code
|
|
857
948
|
*/
|
|
@@ -1687,6 +1778,7 @@ async function handleOpencodeSession({
|
|
|
1687
1778
|
path: { id: session.id },
|
|
1688
1779
|
body: {
|
|
1689
1780
|
parts,
|
|
1781
|
+
system: OPENCODE_SYSTEM_MESSAGE,
|
|
1690
1782
|
},
|
|
1691
1783
|
signal: abortController.signal,
|
|
1692
1784
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test, expect } from 'vitest'
|
|
2
2
|
import { Lexer } from 'marked'
|
|
3
|
-
import { escapeBackticksInCodeBlocks } from './discordBot.js'
|
|
3
|
+
import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord } from './discordBot.js'
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
@@ -144,3 +144,304 @@ const x = \\\`hello\\\`
|
|
|
144
144
|
"
|
|
145
145
|
`)
|
|
146
146
|
})
|
|
147
|
+
|
|
148
|
+
test('splitMarkdownForDiscord returns single chunk for short content', () => {
|
|
149
|
+
const result = splitMarkdownForDiscord({
|
|
150
|
+
content: 'Hello world',
|
|
151
|
+
maxLength: 100,
|
|
152
|
+
})
|
|
153
|
+
expect(result).toMatchInlineSnapshot(`
|
|
154
|
+
[
|
|
155
|
+
"Hello world",
|
|
156
|
+
]
|
|
157
|
+
`)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('splitMarkdownForDiscord splits at line boundaries', () => {
|
|
161
|
+
const result = splitMarkdownForDiscord({
|
|
162
|
+
content: 'Line 1\nLine 2\nLine 3\nLine 4',
|
|
163
|
+
maxLength: 15,
|
|
164
|
+
})
|
|
165
|
+
expect(result).toMatchInlineSnapshot(`
|
|
166
|
+
[
|
|
167
|
+
"Line 1
|
|
168
|
+
Line 2
|
|
169
|
+
",
|
|
170
|
+
"Line 3
|
|
171
|
+
Line 4",
|
|
172
|
+
]
|
|
173
|
+
`)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('splitMarkdownForDiscord preserves code blocks when not split', () => {
|
|
177
|
+
const result = splitMarkdownForDiscord({
|
|
178
|
+
content: '```js\nconst x = 1\n```',
|
|
179
|
+
maxLength: 100,
|
|
180
|
+
})
|
|
181
|
+
expect(result).toMatchInlineSnapshot(`
|
|
182
|
+
[
|
|
183
|
+
"\`\`\`js
|
|
184
|
+
const x = 1
|
|
185
|
+
\`\`\`",
|
|
186
|
+
]
|
|
187
|
+
`)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('splitMarkdownForDiscord adds closing and opening fences when splitting code block', () => {
|
|
191
|
+
const result = splitMarkdownForDiscord({
|
|
192
|
+
content: '```js\nline1\nline2\nline3\nline4\n```',
|
|
193
|
+
maxLength: 20,
|
|
194
|
+
})
|
|
195
|
+
expect(result).toMatchInlineSnapshot(`
|
|
196
|
+
[
|
|
197
|
+
"\`\`\`js
|
|
198
|
+
line1
|
|
199
|
+
line2
|
|
200
|
+
\`\`\`
|
|
201
|
+
",
|
|
202
|
+
"\`\`\`js
|
|
203
|
+
line3
|
|
204
|
+
line4
|
|
205
|
+
\`\`\`
|
|
206
|
+
",
|
|
207
|
+
]
|
|
208
|
+
`)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
test('splitMarkdownForDiscord handles code block with language', () => {
|
|
212
|
+
const result = splitMarkdownForDiscord({
|
|
213
|
+
content: '```typescript\nconst a = 1\nconst b = 2\n```',
|
|
214
|
+
maxLength: 30,
|
|
215
|
+
})
|
|
216
|
+
expect(result).toMatchInlineSnapshot(`
|
|
217
|
+
[
|
|
218
|
+
"\`\`\`typescript
|
|
219
|
+
const a = 1
|
|
220
|
+
\`\`\`
|
|
221
|
+
",
|
|
222
|
+
"\`\`\`typescript
|
|
223
|
+
const b = 2
|
|
224
|
+
\`\`\`
|
|
225
|
+
",
|
|
226
|
+
]
|
|
227
|
+
`)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
test('splitMarkdownForDiscord handles mixed content with code blocks', () => {
|
|
231
|
+
const result = splitMarkdownForDiscord({
|
|
232
|
+
content: 'Text before\n```js\ncode\n```\nText after',
|
|
233
|
+
maxLength: 25,
|
|
234
|
+
})
|
|
235
|
+
expect(result).toMatchInlineSnapshot(`
|
|
236
|
+
[
|
|
237
|
+
"Text before
|
|
238
|
+
\`\`\`js
|
|
239
|
+
code
|
|
240
|
+
\`\`\`
|
|
241
|
+
",
|
|
242
|
+
"Text after",
|
|
243
|
+
]
|
|
244
|
+
`)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('splitMarkdownForDiscord handles code block without language', () => {
|
|
248
|
+
const result = splitMarkdownForDiscord({
|
|
249
|
+
content: '```\nline1\nline2\n```',
|
|
250
|
+
maxLength: 12,
|
|
251
|
+
})
|
|
252
|
+
expect(result).toMatchInlineSnapshot(`
|
|
253
|
+
[
|
|
254
|
+
"\`\`\`
|
|
255
|
+
line1
|
|
256
|
+
\`\`\`
|
|
257
|
+
",
|
|
258
|
+
"\`\`\`
|
|
259
|
+
line2
|
|
260
|
+
\`\`\`
|
|
261
|
+
",
|
|
262
|
+
]
|
|
263
|
+
`)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
test('splitMarkdownForDiscord handles multiple consecutive code blocks', () => {
|
|
267
|
+
const result = splitMarkdownForDiscord({
|
|
268
|
+
content: '```js\nfoo\n```\n```py\nbar\n```',
|
|
269
|
+
maxLength: 20,
|
|
270
|
+
})
|
|
271
|
+
expect(result).toMatchInlineSnapshot(`
|
|
272
|
+
[
|
|
273
|
+
"\`\`\`js
|
|
274
|
+
foo
|
|
275
|
+
\`\`\`
|
|
276
|
+
\`\`\`py
|
|
277
|
+
\`\`\`
|
|
278
|
+
",
|
|
279
|
+
"\`\`\`py
|
|
280
|
+
bar
|
|
281
|
+
\`\`\`
|
|
282
|
+
",
|
|
283
|
+
]
|
|
284
|
+
`)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('splitMarkdownForDiscord handles empty code block', () => {
|
|
288
|
+
const result = splitMarkdownForDiscord({
|
|
289
|
+
content: 'before\n```\n```\nafter',
|
|
290
|
+
maxLength: 50,
|
|
291
|
+
})
|
|
292
|
+
expect(result).toMatchInlineSnapshot(`
|
|
293
|
+
[
|
|
294
|
+
"before
|
|
295
|
+
\`\`\`
|
|
296
|
+
\`\`\`
|
|
297
|
+
after",
|
|
298
|
+
]
|
|
299
|
+
`)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('splitMarkdownForDiscord handles content exactly at maxLength', () => {
|
|
303
|
+
const result = splitMarkdownForDiscord({
|
|
304
|
+
content: '12345678901234567890',
|
|
305
|
+
maxLength: 20,
|
|
306
|
+
})
|
|
307
|
+
expect(result).toMatchInlineSnapshot(`
|
|
308
|
+
[
|
|
309
|
+
"12345678901234567890",
|
|
310
|
+
]
|
|
311
|
+
`)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
test('splitMarkdownForDiscord handles code block only', () => {
|
|
315
|
+
const result = splitMarkdownForDiscord({
|
|
316
|
+
content: '```ts\nconst x = 1\n```',
|
|
317
|
+
maxLength: 15,
|
|
318
|
+
})
|
|
319
|
+
expect(result).toMatchInlineSnapshot(`
|
|
320
|
+
[
|
|
321
|
+
"\`\`\`ts
|
|
322
|
+
\`\`\`
|
|
323
|
+
",
|
|
324
|
+
"\`\`\`ts
|
|
325
|
+
const x = 1
|
|
326
|
+
\`\`\`
|
|
327
|
+
",
|
|
328
|
+
]
|
|
329
|
+
`)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test('splitMarkdownForDiscord handles code block at start with text after', () => {
|
|
333
|
+
const result = splitMarkdownForDiscord({
|
|
334
|
+
content: '```js\ncode\n```\nSome text after',
|
|
335
|
+
maxLength: 20,
|
|
336
|
+
})
|
|
337
|
+
expect(result).toMatchInlineSnapshot(`
|
|
338
|
+
[
|
|
339
|
+
"\`\`\`js
|
|
340
|
+
code
|
|
341
|
+
\`\`\`
|
|
342
|
+
",
|
|
343
|
+
"Some text after",
|
|
344
|
+
]
|
|
345
|
+
`)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test('splitMarkdownForDiscord handles text before code block at end', () => {
|
|
349
|
+
const result = splitMarkdownForDiscord({
|
|
350
|
+
content: 'Some text before\n```js\ncode\n```',
|
|
351
|
+
maxLength: 25,
|
|
352
|
+
})
|
|
353
|
+
expect(result).toMatchInlineSnapshot(`
|
|
354
|
+
[
|
|
355
|
+
"Some text before
|
|
356
|
+
\`\`\`js
|
|
357
|
+
\`\`\`
|
|
358
|
+
",
|
|
359
|
+
"\`\`\`js
|
|
360
|
+
code
|
|
361
|
+
\`\`\`
|
|
362
|
+
",
|
|
363
|
+
]
|
|
364
|
+
`)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
test('splitMarkdownForDiscord handles very long line inside code block', () => {
|
|
368
|
+
const result = splitMarkdownForDiscord({
|
|
369
|
+
content: '```js\nshort\nveryverylonglinethatexceedsmaxlength\nshort\n```',
|
|
370
|
+
maxLength: 25,
|
|
371
|
+
})
|
|
372
|
+
expect(result).toMatchInlineSnapshot(`
|
|
373
|
+
[
|
|
374
|
+
"\`\`\`js
|
|
375
|
+
short
|
|
376
|
+
\`\`\`
|
|
377
|
+
",
|
|
378
|
+
"\`\`\`js
|
|
379
|
+
veryverylonglinethatexceedsmaxlength
|
|
380
|
+
\`\`\`
|
|
381
|
+
",
|
|
382
|
+
"\`\`\`js
|
|
383
|
+
short
|
|
384
|
+
\`\`\`
|
|
385
|
+
",
|
|
386
|
+
]
|
|
387
|
+
`)
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test('splitMarkdownForDiscord handles realistic long markdown with code block', () => {
|
|
391
|
+
const content = `Here is some explanation text before the code.
|
|
392
|
+
|
|
393
|
+
\`\`\`typescript
|
|
394
|
+
export function calculateTotal(items: Item[]): number {
|
|
395
|
+
let total = 0
|
|
396
|
+
for (const item of items) {
|
|
397
|
+
total += item.price * item.quantity
|
|
398
|
+
}
|
|
399
|
+
return total
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function formatCurrency(amount: number): string {
|
|
403
|
+
return new Intl.NumberFormat('en-US', {
|
|
404
|
+
style: 'currency',
|
|
405
|
+
currency: 'USD',
|
|
406
|
+
}).format(amount)
|
|
407
|
+
}
|
|
408
|
+
\`\`\`
|
|
409
|
+
|
|
410
|
+
And here is some text after the code block.`
|
|
411
|
+
|
|
412
|
+
const result = splitMarkdownForDiscord({
|
|
413
|
+
content,
|
|
414
|
+
maxLength: 200,
|
|
415
|
+
})
|
|
416
|
+
expect(result).toMatchInlineSnapshot(`
|
|
417
|
+
[
|
|
418
|
+
"Here is some explanation text before the code.
|
|
419
|
+
|
|
420
|
+
\`\`\`typescript
|
|
421
|
+
export function calculateTotal(items: Item[]): number {
|
|
422
|
+
let total = 0
|
|
423
|
+
for (const item of items) {
|
|
424
|
+
\`\`\`
|
|
425
|
+
",
|
|
426
|
+
"\`\`\`typescript
|
|
427
|
+
total += item.price * item.quantity
|
|
428
|
+
}
|
|
429
|
+
return total
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function formatCurrency(amount: number): string {
|
|
433
|
+
return new Intl.NumberFormat('en-US', {
|
|
434
|
+
style: 'currency',
|
|
435
|
+
\`\`\`
|
|
436
|
+
",
|
|
437
|
+
"\`\`\`typescript
|
|
438
|
+
currency: 'USD',
|
|
439
|
+
}).format(amount)
|
|
440
|
+
}
|
|
441
|
+
\`\`\`
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
And here is some text after the code block.",
|
|
445
|
+
]
|
|
446
|
+
`)
|
|
447
|
+
})
|
package/src/tools.ts
CHANGED
|
@@ -15,7 +15,10 @@ import { formatDistanceToNow } from 'date-fns'
|
|
|
15
15
|
|
|
16
16
|
import { ShareMarkdown } from './markdown.js'
|
|
17
17
|
import pc from 'picocolors'
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
initializeOpencodeForDirectory,
|
|
20
|
+
OPENCODE_SYSTEM_MESSAGE,
|
|
21
|
+
} from './discordBot.js'
|
|
19
22
|
|
|
20
23
|
export async function getTools({
|
|
21
24
|
onMessageCompleted,
|
|
@@ -72,10 +75,10 @@ export async function getTools({
|
|
|
72
75
|
getClient()
|
|
73
76
|
.session.prompt({
|
|
74
77
|
path: { id: sessionId },
|
|
75
|
-
|
|
76
78
|
body: {
|
|
77
79
|
parts: [{ type: 'text', text: message }],
|
|
78
80
|
model: sessionModel,
|
|
81
|
+
system: OPENCODE_SYSTEM_MESSAGE,
|
|
79
82
|
},
|
|
80
83
|
})
|
|
81
84
|
.then(async (response) => {
|
|
@@ -149,7 +152,7 @@ export async function getTools({
|
|
|
149
152
|
path: { id: session.data.id },
|
|
150
153
|
body: {
|
|
151
154
|
parts: [{ type: 'text', text: message }],
|
|
152
|
-
|
|
155
|
+
system: OPENCODE_SYSTEM_MESSAGE,
|
|
153
156
|
},
|
|
154
157
|
})
|
|
155
158
|
.then(async (response) => {
|