kimaki 0.4.2 → 0.4.6
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/cli.js +34 -38
- package/dist/discordBot.js +345 -145
- package/dist/escape-backticks.test.js +125 -0
- package/dist/tools.js +2 -1
- package/package.json +12 -14
- package/src/cli.ts +37 -50
- package/src/discordBot.ts +468 -159
- package/src/escape-backticks.test.ts +146 -0
- package/src/tools.ts +2 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { Lexer } from 'marked';
|
|
3
|
+
import { escapeBackticksInCodeBlocks } from './discordBot.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
|
+
});
|
package/dist/tools.js
CHANGED
|
@@ -95,7 +95,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
95
95
|
.optional()
|
|
96
96
|
.describe('Optional model to use for this session'),
|
|
97
97
|
}),
|
|
98
|
-
execute: async ({ message, title,
|
|
98
|
+
execute: async ({ message, title, }) => {
|
|
99
99
|
if (!message.trim()) {
|
|
100
100
|
throw new Error(`message must be a non empty string`);
|
|
101
101
|
}
|
|
@@ -114,6 +114,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
|
|
|
114
114
|
path: { id: session.data.id },
|
|
115
115
|
body: {
|
|
116
116
|
parts: [{ type: 'text', text: message }],
|
|
117
|
+
// model,
|
|
117
118
|
},
|
|
118
119
|
})
|
|
119
120
|
.then(async (response) => {
|
package/package.json
CHANGED
|
@@ -2,18 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.
|
|
6
|
-
"scripts": {
|
|
7
|
-
"dev": "pnpm tsc && tsx --env-file .env src/cli.ts",
|
|
8
|
-
"prepublishOnly": "pnpm tsc",
|
|
9
|
-
"dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
|
|
10
|
-
"test": "tsx scripts/test-opencode.ts",
|
|
11
|
-
"watch": "tsx scripts/watch-session.ts",
|
|
12
|
-
"test:events": "tsx test-events.ts",
|
|
13
|
-
"pcm-to-mp3": "bun scripts/pcm-to-mp3",
|
|
14
|
-
"test:send": "tsx send-test-message.ts",
|
|
15
|
-
"register-commands": "tsx scripts/register-commands.ts"
|
|
16
|
-
},
|
|
5
|
+
"version": "0.4.6",
|
|
17
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
18
7
|
"bin": "bin.js",
|
|
19
8
|
"files": [
|
|
@@ -35,7 +24,7 @@
|
|
|
35
24
|
"@discordjs/opus": "^0.10.0",
|
|
36
25
|
"@discordjs/voice": "^0.19.0",
|
|
37
26
|
"@google/genai": "^1.16.0",
|
|
38
|
-
"@opencode-ai/sdk": "^0.
|
|
27
|
+
"@opencode-ai/sdk": "^1.0.115",
|
|
39
28
|
"@purinton/resampler": "^1.0.4",
|
|
40
29
|
"@snazzah/davey": "^0.1.6",
|
|
41
30
|
"ai": "^5.0.29",
|
|
@@ -54,5 +43,14 @@
|
|
|
54
43
|
"string-dedent": "^3.0.2",
|
|
55
44
|
"undici": "^7.16.0",
|
|
56
45
|
"zod": "^4.0.17"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"dev": "tsx --env-file .env src/cli.ts",
|
|
49
|
+
"dev:bun": "DEBUG=1 bun --env-file .env src/cli.ts",
|
|
50
|
+
"watch": "tsx scripts/watch-session.ts",
|
|
51
|
+
"test:events": "tsx test-events.ts",
|
|
52
|
+
"pcm-to-mp3": "bun scripts/pcm-to-mp3",
|
|
53
|
+
"test:send": "tsx send-test-message.ts",
|
|
54
|
+
"register-commands": "tsx scripts/register-commands.ts"
|
|
57
55
|
}
|
|
58
|
-
}
|
|
56
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -20,6 +20,8 @@ import {
|
|
|
20
20
|
getDatabase,
|
|
21
21
|
startDiscordBot,
|
|
22
22
|
initializeOpencodeForDirectory,
|
|
23
|
+
ensureKimakiCategory,
|
|
24
|
+
createProjectChannels,
|
|
23
25
|
type ChannelWithTags,
|
|
24
26
|
} from './discordBot.js'
|
|
25
27
|
import type { OpencodeClient } from '@opencode-ai/sdk'
|
|
@@ -35,13 +37,27 @@ import {
|
|
|
35
37
|
import path from 'node:path'
|
|
36
38
|
import fs from 'node:fs'
|
|
37
39
|
import { createLogger } from './logger.js'
|
|
38
|
-
import { spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
|
|
40
|
+
import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
|
|
39
41
|
|
|
40
42
|
const cliLogger = createLogger('CLI')
|
|
41
43
|
const cli = cac('kimaki')
|
|
42
44
|
|
|
43
45
|
process.title = 'kimaki'
|
|
44
46
|
|
|
47
|
+
process.on('SIGUSR2', () => {
|
|
48
|
+
cliLogger.info('Received SIGUSR2, restarting process in 1000ms...')
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
cliLogger.info('Restarting...')
|
|
51
|
+
spawn(process.argv[0]!, [...process.execArgv, ...process.argv.slice(1)], {
|
|
52
|
+
stdio: 'inherit',
|
|
53
|
+
detached: true,
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
env: process.env,
|
|
56
|
+
}).unref()
|
|
57
|
+
process.exit(0)
|
|
58
|
+
}, 1000)
|
|
59
|
+
})
|
|
60
|
+
|
|
45
61
|
const EXIT_NO_RESTART = 64
|
|
46
62
|
|
|
47
63
|
type Project = {
|
|
@@ -97,6 +113,19 @@ async function registerCommands(token: string, appId: string) {
|
|
|
97
113
|
return option
|
|
98
114
|
})
|
|
99
115
|
.toJSON(),
|
|
116
|
+
new SlashCommandBuilder()
|
|
117
|
+
.setName('add-project')
|
|
118
|
+
.setDescription('Create Discord channels for a new OpenCode project')
|
|
119
|
+
.addStringOption((option) => {
|
|
120
|
+
option
|
|
121
|
+
.setName('project')
|
|
122
|
+
.setDescription('Select an OpenCode project')
|
|
123
|
+
.setRequired(true)
|
|
124
|
+
.setAutocomplete(true)
|
|
125
|
+
|
|
126
|
+
return option
|
|
127
|
+
})
|
|
128
|
+
.toJSON(),
|
|
100
129
|
]
|
|
101
130
|
|
|
102
131
|
const rest = new REST().setToken(token)
|
|
@@ -117,26 +146,7 @@ async function registerCommands(token: string, appId: string) {
|
|
|
117
146
|
}
|
|
118
147
|
}
|
|
119
148
|
|
|
120
|
-
async function ensureKimakiCategory(guild: Guild): Promise<CategoryChannel> {
|
|
121
|
-
const existingCategory = guild.channels.cache.find(
|
|
122
|
-
(channel): channel is CategoryChannel => {
|
|
123
|
-
if (channel.type !== ChannelType.GuildCategory) {
|
|
124
|
-
return false
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return channel.name.toLowerCase() === 'kimaki'
|
|
128
|
-
},
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
if (existingCategory) {
|
|
132
|
-
return existingCategory
|
|
133
|
-
}
|
|
134
149
|
|
|
135
|
-
return guild.channels.create({
|
|
136
|
-
name: 'Kimaki',
|
|
137
|
-
type: ChannelType.GuildCategory,
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
150
|
|
|
141
151
|
async function run({ restart, addChannels }: CliOptions) {
|
|
142
152
|
const forceSetup = Boolean(restart)
|
|
@@ -530,43 +540,20 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
530
540
|
const project = projects.find((p) => p.id === projectId)
|
|
531
541
|
if (!project) continue
|
|
532
542
|
|
|
533
|
-
const baseName = path.basename(project.worktree)
|
|
534
|
-
const channelName = `${baseName}`
|
|
535
|
-
.toLowerCase()
|
|
536
|
-
.replace(/[^a-z0-9-]/g, '-')
|
|
537
|
-
.slice(0, 100)
|
|
538
|
-
|
|
539
543
|
try {
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
type: ChannelType.GuildText,
|
|
545
|
-
parent: kimakiCategory,
|
|
546
|
-
topic: `<kimaki><directory>${project.worktree}</directory><app>${appId}</app></kimaki>`,
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
const voiceChannel = await targetGuild.channels.create({
|
|
550
|
-
name: channelName,
|
|
551
|
-
type: ChannelType.GuildVoice,
|
|
552
|
-
parent: kimakiCategory,
|
|
544
|
+
const { textChannelId, channelName } = await createProjectChannels({
|
|
545
|
+
guild: targetGuild,
|
|
546
|
+
projectDirectory: project.worktree,
|
|
547
|
+
appId,
|
|
553
548
|
})
|
|
554
549
|
|
|
555
|
-
db.prepare(
|
|
556
|
-
'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)',
|
|
557
|
-
).run(textChannel.id, project.worktree, 'text')
|
|
558
|
-
|
|
559
|
-
db.prepare(
|
|
560
|
-
'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type) VALUES (?, ?, ?)',
|
|
561
|
-
).run(voiceChannel.id, project.worktree, 'voice')
|
|
562
|
-
|
|
563
550
|
createdChannels.push({
|
|
564
|
-
name:
|
|
565
|
-
id:
|
|
551
|
+
name: channelName,
|
|
552
|
+
id: textChannelId,
|
|
566
553
|
guildId: targetGuild.id,
|
|
567
554
|
})
|
|
568
555
|
} catch (error) {
|
|
569
|
-
cliLogger.error(`Failed to create channels for ${
|
|
556
|
+
cliLogger.error(`Failed to create channels for ${path.basename(project.worktree)}:`, error)
|
|
570
557
|
}
|
|
571
558
|
}
|
|
572
559
|
|