kimaki 0.4.29 → 0.4.31
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/LICENSE +21 -0
- package/dist/cli.js +215 -37
- package/dist/commands/ask-question.js +49 -16
- package/dist/discord-bot.js +83 -1
- package/dist/discord-utils.js +4 -1
- package/dist/escape-backticks.test.js +11 -3
- package/dist/session-handler.js +18 -4
- package/dist/system-message.js +14 -6
- package/dist/unnest-code-blocks.js +110 -0
- package/dist/unnest-code-blocks.test.js +213 -0
- package/dist/utils.js +1 -0
- package/package.json +11 -12
- package/src/cli.ts +282 -46
- package/src/commands/ask-question.ts +57 -22
- package/src/discord-bot.ts +97 -1
- package/src/discord-utils.ts +4 -1
- package/src/escape-backticks.test.ts +11 -3
- package/src/session-handler.ts +20 -4
- package/src/system-message.ts +14 -6
- package/src/unnest-code-blocks.test.ts +225 -0
- package/src/unnest-code-blocks.ts +127 -0
- package/src/utils.ts +1 -0
package/dist/system-message.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// OpenCode system prompt generator.
|
|
2
2
|
// Creates the system message injected into every OpenCode session,
|
|
3
3
|
// including Discord-specific formatting rules, diff commands, and permissions info.
|
|
4
|
-
export function getOpencodeSystemMessage({ sessionId }) {
|
|
4
|
+
export function getOpencodeSystemMessage({ sessionId, channelId }) {
|
|
5
5
|
return `
|
|
6
6
|
The user is reading your messages from inside Discord, via kimaki.xyz
|
|
7
7
|
|
|
8
8
|
The user cannot see bash tool outputs. If there is important information in bash output, include it in your text response.
|
|
9
9
|
|
|
10
|
-
Your current OpenCode session ID is: ${sessionId}
|
|
10
|
+
Your current OpenCode session ID is: ${sessionId}${channelId ? `\nYour current Discord channel ID is: ${channelId}` : ''}
|
|
11
11
|
|
|
12
12
|
## permissions
|
|
13
13
|
|
|
@@ -22,24 +22,32 @@ Only users with these Discord permissions can send messages to the bot:
|
|
|
22
22
|
To upload files to the Discord thread (images, screenshots, long files that would clutter the chat), run:
|
|
23
23
|
|
|
24
24
|
npx -y kimaki upload-to-discord --session ${sessionId} <file1> [file2] ...
|
|
25
|
+
${channelId ? `
|
|
26
|
+
## starting new sessions from CLI
|
|
25
27
|
|
|
28
|
+
To start a new thread/session in this channel programmatically, run:
|
|
29
|
+
|
|
30
|
+
npx -y kimaki start-session --channel ${channelId} --prompt "your prompt here"
|
|
31
|
+
|
|
32
|
+
This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
|
|
33
|
+
` : ''}
|
|
26
34
|
## showing diffs
|
|
27
35
|
|
|
28
36
|
IMPORTANT: After editing any files, you MUST execute the critique command using the Bash tool to get a diff URL, then share that URL with the user.
|
|
29
37
|
|
|
30
38
|
Execute this after making changes:
|
|
31
39
|
|
|
32
|
-
bunx critique web
|
|
40
|
+
bunx critique web --title "Add user authentication flow"
|
|
33
41
|
|
|
34
42
|
If there are other unrelated changes in the working directory, filter to only show the files you edited:
|
|
35
43
|
|
|
36
|
-
bunx critique web -- path/to/file1.ts path/to/file2.ts
|
|
44
|
+
bunx critique web --title "Fix database connection retry" -- path/to/file1.ts path/to/file2.ts
|
|
37
45
|
|
|
38
46
|
You can also show latest commit changes using:
|
|
39
47
|
|
|
40
|
-
bunx critique web HEAD
|
|
48
|
+
bunx critique web --title "Refactor API endpoints" HEAD
|
|
41
49
|
|
|
42
|
-
bunx critique web HEAD~1 to get the one before last
|
|
50
|
+
bunx critique web --title "Update dependencies" HEAD~1 to get the one before last
|
|
43
51
|
|
|
44
52
|
Do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
|
|
45
53
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Unnest code blocks from list items for Discord.
|
|
2
|
+
// Discord doesn't render code blocks inside lists, so this hoists them
|
|
3
|
+
// to root level while preserving list structure.
|
|
4
|
+
import { Lexer } from 'marked';
|
|
5
|
+
export function unnestCodeBlocksFromLists(markdown) {
|
|
6
|
+
const lexer = new Lexer();
|
|
7
|
+
const tokens = lexer.lex(markdown);
|
|
8
|
+
const result = [];
|
|
9
|
+
for (const token of tokens) {
|
|
10
|
+
if (token.type === 'list') {
|
|
11
|
+
const segments = processListToken(token);
|
|
12
|
+
result.push(renderSegments(segments));
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
result.push(token.raw);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return result.join('');
|
|
19
|
+
}
|
|
20
|
+
function processListToken(list) {
|
|
21
|
+
const segments = [];
|
|
22
|
+
const start = typeof list.start === 'number' ? list.start : parseInt(list.start, 10) || 1;
|
|
23
|
+
const prefix = list.ordered ? (i) => `${start + i}. ` : () => '- ';
|
|
24
|
+
for (let i = 0; i < list.items.length; i++) {
|
|
25
|
+
const item = list.items[i];
|
|
26
|
+
const itemSegments = processListItem(item, prefix(i));
|
|
27
|
+
segments.push(...itemSegments);
|
|
28
|
+
}
|
|
29
|
+
return segments;
|
|
30
|
+
}
|
|
31
|
+
function processListItem(item, prefix) {
|
|
32
|
+
const segments = [];
|
|
33
|
+
let currentText = [];
|
|
34
|
+
const flushText = () => {
|
|
35
|
+
const text = currentText.join('').trim();
|
|
36
|
+
if (text) {
|
|
37
|
+
segments.push({ type: 'list-item', prefix, content: text });
|
|
38
|
+
}
|
|
39
|
+
currentText = [];
|
|
40
|
+
};
|
|
41
|
+
for (const token of item.tokens) {
|
|
42
|
+
if (token.type === 'code') {
|
|
43
|
+
flushText();
|
|
44
|
+
const codeToken = token;
|
|
45
|
+
const lang = codeToken.lang || '';
|
|
46
|
+
segments.push({
|
|
47
|
+
type: 'code',
|
|
48
|
+
content: '```' + lang + '\n' + codeToken.text + '\n```\n',
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else if (token.type === 'list') {
|
|
52
|
+
flushText();
|
|
53
|
+
// Recursively process nested list - segments bubble up
|
|
54
|
+
const nestedSegments = processListToken(token);
|
|
55
|
+
segments.push(...nestedSegments);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
currentText.push(extractText(token));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
flushText();
|
|
62
|
+
// If no segments were created (empty item), return empty
|
|
63
|
+
if (segments.length === 0) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
// If item had no code blocks (all segments are list-items from this level),
|
|
67
|
+
// return original raw to preserve formatting
|
|
68
|
+
const hasCode = segments.some((s) => s.type === 'code');
|
|
69
|
+
if (!hasCode) {
|
|
70
|
+
return [{ type: 'list-item', prefix: '', content: item.raw }];
|
|
71
|
+
}
|
|
72
|
+
return segments;
|
|
73
|
+
}
|
|
74
|
+
function extractText(token) {
|
|
75
|
+
if (token.type === 'text') {
|
|
76
|
+
return token.text;
|
|
77
|
+
}
|
|
78
|
+
if (token.type === 'space') {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
if ('raw' in token) {
|
|
82
|
+
return token.raw;
|
|
83
|
+
}
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
function renderSegments(segments) {
|
|
87
|
+
const result = [];
|
|
88
|
+
for (let i = 0; i < segments.length; i++) {
|
|
89
|
+
const segment = segments[i];
|
|
90
|
+
const prev = segments[i - 1];
|
|
91
|
+
if (segment.type === 'code') {
|
|
92
|
+
// Add newline before code if previous was a list item
|
|
93
|
+
if (prev && prev.type === 'list-item') {
|
|
94
|
+
result.push('\n');
|
|
95
|
+
}
|
|
96
|
+
result.push(segment.content);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// list-item
|
|
100
|
+
if (segment.prefix) {
|
|
101
|
+
result.push(segment.prefix + segment.content + '\n');
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Raw content (no prefix means it's original raw)
|
|
105
|
+
result.push(segment.content);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return result.join('');
|
|
110
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { unnestCodeBlocksFromLists } from './unnest-code-blocks.js';
|
|
3
|
+
test('basic - single item with code block', () => {
|
|
4
|
+
const input = `- Item 1
|
|
5
|
+
\`\`\`js
|
|
6
|
+
const x = 1
|
|
7
|
+
\`\`\``;
|
|
8
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
9
|
+
expect(result).toMatchInlineSnapshot(`
|
|
10
|
+
"- Item 1
|
|
11
|
+
|
|
12
|
+
\`\`\`js
|
|
13
|
+
const x = 1
|
|
14
|
+
\`\`\`
|
|
15
|
+
"
|
|
16
|
+
`);
|
|
17
|
+
});
|
|
18
|
+
test('multiple items - code in middle item only', () => {
|
|
19
|
+
const input = `- Item 1
|
|
20
|
+
- Item 2
|
|
21
|
+
\`\`\`js
|
|
22
|
+
const x = 1
|
|
23
|
+
\`\`\`
|
|
24
|
+
- Item 3`;
|
|
25
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
26
|
+
expect(result).toMatchInlineSnapshot(`
|
|
27
|
+
"- Item 1
|
|
28
|
+
- Item 2
|
|
29
|
+
|
|
30
|
+
\`\`\`js
|
|
31
|
+
const x = 1
|
|
32
|
+
\`\`\`
|
|
33
|
+
- Item 3"
|
|
34
|
+
`);
|
|
35
|
+
});
|
|
36
|
+
test('multiple code blocks in one item', () => {
|
|
37
|
+
const input = `- Item with two code blocks
|
|
38
|
+
\`\`\`js
|
|
39
|
+
const a = 1
|
|
40
|
+
\`\`\`
|
|
41
|
+
\`\`\`python
|
|
42
|
+
b = 2
|
|
43
|
+
\`\`\``;
|
|
44
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
45
|
+
expect(result).toMatchInlineSnapshot(`
|
|
46
|
+
"- Item with two code blocks
|
|
47
|
+
|
|
48
|
+
\`\`\`js
|
|
49
|
+
const a = 1
|
|
50
|
+
\`\`\`
|
|
51
|
+
\`\`\`python
|
|
52
|
+
b = 2
|
|
53
|
+
\`\`\`
|
|
54
|
+
"
|
|
55
|
+
`);
|
|
56
|
+
});
|
|
57
|
+
test('nested list with code', () => {
|
|
58
|
+
const input = `- Item 1
|
|
59
|
+
- Nested item
|
|
60
|
+
\`\`\`js
|
|
61
|
+
const x = 1
|
|
62
|
+
\`\`\`
|
|
63
|
+
- Item 2`;
|
|
64
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
65
|
+
expect(result).toMatchInlineSnapshot(`
|
|
66
|
+
"- Item 1
|
|
67
|
+
- Nested item
|
|
68
|
+
|
|
69
|
+
\`\`\`js
|
|
70
|
+
const x = 1
|
|
71
|
+
\`\`\`
|
|
72
|
+
- Item 2"
|
|
73
|
+
`);
|
|
74
|
+
});
|
|
75
|
+
test('ordered list preserves numbering', () => {
|
|
76
|
+
const input = `1. First item
|
|
77
|
+
\`\`\`js
|
|
78
|
+
const a = 1
|
|
79
|
+
\`\`\`
|
|
80
|
+
2. Second item
|
|
81
|
+
3. Third item`;
|
|
82
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
83
|
+
expect(result).toMatchInlineSnapshot(`
|
|
84
|
+
"1. First item
|
|
85
|
+
|
|
86
|
+
\`\`\`js
|
|
87
|
+
const a = 1
|
|
88
|
+
\`\`\`
|
|
89
|
+
2. Second item
|
|
90
|
+
3. Third item"
|
|
91
|
+
`);
|
|
92
|
+
});
|
|
93
|
+
test('list without code blocks unchanged', () => {
|
|
94
|
+
const input = `- Item 1
|
|
95
|
+
- Item 2
|
|
96
|
+
- Item 3`;
|
|
97
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
98
|
+
expect(result).toMatchInlineSnapshot(`
|
|
99
|
+
"- Item 1
|
|
100
|
+
- Item 2
|
|
101
|
+
- Item 3"
|
|
102
|
+
`);
|
|
103
|
+
});
|
|
104
|
+
test('mixed - some items have code, some dont', () => {
|
|
105
|
+
const input = `- Normal item
|
|
106
|
+
- Item with code
|
|
107
|
+
\`\`\`js
|
|
108
|
+
const x = 1
|
|
109
|
+
\`\`\`
|
|
110
|
+
- Another normal item
|
|
111
|
+
- Another with code
|
|
112
|
+
\`\`\`python
|
|
113
|
+
y = 2
|
|
114
|
+
\`\`\``;
|
|
115
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
116
|
+
expect(result).toMatchInlineSnapshot(`
|
|
117
|
+
"- Normal item
|
|
118
|
+
- Item with code
|
|
119
|
+
|
|
120
|
+
\`\`\`js
|
|
121
|
+
const x = 1
|
|
122
|
+
\`\`\`
|
|
123
|
+
- Another normal item
|
|
124
|
+
- Another with code
|
|
125
|
+
|
|
126
|
+
\`\`\`python
|
|
127
|
+
y = 2
|
|
128
|
+
\`\`\`
|
|
129
|
+
"
|
|
130
|
+
`);
|
|
131
|
+
});
|
|
132
|
+
test('text before and after code in same item', () => {
|
|
133
|
+
const input = `- Start text
|
|
134
|
+
\`\`\`js
|
|
135
|
+
const x = 1
|
|
136
|
+
\`\`\`
|
|
137
|
+
End text`;
|
|
138
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
139
|
+
expect(result).toMatchInlineSnapshot(`
|
|
140
|
+
"- Start text
|
|
141
|
+
|
|
142
|
+
\`\`\`js
|
|
143
|
+
const x = 1
|
|
144
|
+
\`\`\`
|
|
145
|
+
- End text
|
|
146
|
+
"
|
|
147
|
+
`);
|
|
148
|
+
});
|
|
149
|
+
test('preserves content outside lists', () => {
|
|
150
|
+
const input = `# Heading
|
|
151
|
+
|
|
152
|
+
Some paragraph text.
|
|
153
|
+
|
|
154
|
+
- List item
|
|
155
|
+
\`\`\`js
|
|
156
|
+
const x = 1
|
|
157
|
+
\`\`\`
|
|
158
|
+
|
|
159
|
+
More text after.`;
|
|
160
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
161
|
+
expect(result).toMatchInlineSnapshot(`
|
|
162
|
+
"# Heading
|
|
163
|
+
|
|
164
|
+
Some paragraph text.
|
|
165
|
+
|
|
166
|
+
- List item
|
|
167
|
+
|
|
168
|
+
\`\`\`js
|
|
169
|
+
const x = 1
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
More text after."
|
|
174
|
+
`);
|
|
175
|
+
});
|
|
176
|
+
test('code block at root level unchanged', () => {
|
|
177
|
+
const input = `\`\`\`js
|
|
178
|
+
const x = 1
|
|
179
|
+
\`\`\``;
|
|
180
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
181
|
+
expect(result).toMatchInlineSnapshot(`
|
|
182
|
+
"\`\`\`js
|
|
183
|
+
const x = 1
|
|
184
|
+
\`\`\`"
|
|
185
|
+
`);
|
|
186
|
+
});
|
|
187
|
+
test('handles code block without language', () => {
|
|
188
|
+
const input = `- Item
|
|
189
|
+
\`\`\`
|
|
190
|
+
plain code
|
|
191
|
+
\`\`\``;
|
|
192
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
193
|
+
expect(result).toMatchInlineSnapshot(`
|
|
194
|
+
"- Item
|
|
195
|
+
|
|
196
|
+
\`\`\`
|
|
197
|
+
plain code
|
|
198
|
+
\`\`\`
|
|
199
|
+
"
|
|
200
|
+
`);
|
|
201
|
+
});
|
|
202
|
+
test('handles empty list item with code', () => {
|
|
203
|
+
const input = `- \`\`\`js
|
|
204
|
+
const x = 1
|
|
205
|
+
\`\`\``;
|
|
206
|
+
const result = unnestCodeBlocksFromLists(input);
|
|
207
|
+
expect(result).toMatchInlineSnapshot(`
|
|
208
|
+
"\`\`\`js
|
|
209
|
+
const x = 1
|
|
210
|
+
\`\`\`
|
|
211
|
+
"
|
|
212
|
+
`);
|
|
213
|
+
});
|
package/dist/utils.js
CHANGED
|
@@ -17,6 +17,7 @@ export function generateBotInstallUrl({ clientId, permissions = [
|
|
|
17
17
|
PermissionsBitField.Flags.AttachFiles,
|
|
18
18
|
PermissionsBitField.Flags.Connect,
|
|
19
19
|
PermissionsBitField.Flags.Speak,
|
|
20
|
+
PermissionsBitField.Flags.ManageRoles,
|
|
20
21
|
], scopes = ['bot'], guildId, disableGuildSelect = false, }) {
|
|
21
22
|
const permissionsBitField = new PermissionsBitField(permissions);
|
|
22
23
|
const permissionsValue = permissionsBitField.bitfield.toString();
|
package/package.json
CHANGED
|
@@ -2,17 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "tsx --env-file .env src/cli.ts",
|
|
8
|
-
"prepublishOnly": "pnpm tsc",
|
|
9
|
-
"dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
|
|
10
|
-
"watch": "tsx scripts/watch-session.ts",
|
|
11
|
-
"test:events": "tsx test-events.ts",
|
|
12
|
-
"pcm-to-mp3": "bun scripts/pcm-to-mp3",
|
|
13
|
-
"test:send": "tsx send-test-message.ts",
|
|
14
|
-
"register-commands": "tsx scripts/register-commands.ts"
|
|
15
|
-
},
|
|
5
|
+
"version": "0.4.31",
|
|
16
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
17
7
|
"bin": "bin.js",
|
|
18
8
|
"files": [
|
|
@@ -55,5 +45,14 @@
|
|
|
55
45
|
"string-dedent": "^3.0.2",
|
|
56
46
|
"undici": "^7.16.0",
|
|
57
47
|
"zod": "^4.2.1"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"dev": "tsx --env-file .env src/cli.ts",
|
|
51
|
+
"dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
|
|
52
|
+
"watch": "tsx scripts/watch-session.ts",
|
|
53
|
+
"test:events": "tsx test-events.ts",
|
|
54
|
+
"pcm-to-mp3": "bun scripts/pcm-to-mp3",
|
|
55
|
+
"test:send": "tsx send-test-message.ts",
|
|
56
|
+
"register-commands": "tsx scripts/register-commands.ts"
|
|
58
57
|
}
|
|
59
|
-
}
|
|
58
|
+
}
|