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.
@@ -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.29",
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
+ }