kimaki 0.4.1 → 0.4.4
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 +20 -37
- package/dist/discordBot.js +312 -147
- package/dist/escape-backticks.test.js +125 -0
- package/package.json +11 -13
- package/src/cli.ts +22 -49
- package/src/discordBot.ts +411 -145
- package/src/escape-backticks.test.ts +146 -0
|
@@ -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/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.4",
|
|
17
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
18
7
|
"bin": "bin.js",
|
|
19
8
|
"files": [
|
|
@@ -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": "pnpm tsc && 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'
|
|
@@ -97,6 +99,19 @@ async function registerCommands(token: string, appId: string) {
|
|
|
97
99
|
return option
|
|
98
100
|
})
|
|
99
101
|
.toJSON(),
|
|
102
|
+
new SlashCommandBuilder()
|
|
103
|
+
.setName('add-project')
|
|
104
|
+
.setDescription('Create Discord channels for a new OpenCode project')
|
|
105
|
+
.addStringOption((option) => {
|
|
106
|
+
option
|
|
107
|
+
.setName('project')
|
|
108
|
+
.setDescription('Select an OpenCode project')
|
|
109
|
+
.setRequired(true)
|
|
110
|
+
.setAutocomplete(true)
|
|
111
|
+
|
|
112
|
+
return option
|
|
113
|
+
})
|
|
114
|
+
.toJSON(),
|
|
100
115
|
]
|
|
101
116
|
|
|
102
117
|
const rest = new REST().setToken(token)
|
|
@@ -117,26 +132,7 @@ async function registerCommands(token: string, appId: string) {
|
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
134
|
|
|
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
135
|
|
|
127
|
-
return channel.name.toLowerCase() === 'kimaki'
|
|
128
|
-
},
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
if (existingCategory) {
|
|
132
|
-
return existingCategory
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return guild.channels.create({
|
|
136
|
-
name: 'Kimaki',
|
|
137
|
-
type: ChannelType.GuildCategory,
|
|
138
|
-
})
|
|
139
|
-
}
|
|
140
136
|
|
|
141
137
|
async function run({ restart, addChannels }: CliOptions) {
|
|
142
138
|
const forceSetup = Boolean(restart)
|
|
@@ -530,43 +526,20 @@ async function run({ restart, addChannels }: CliOptions) {
|
|
|
530
526
|
const project = projects.find((p) => p.id === projectId)
|
|
531
527
|
if (!project) continue
|
|
532
528
|
|
|
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
529
|
try {
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
type: ChannelType.GuildText,
|
|
545
|
-
parent: kimakiCategory,
|
|
546
|
-
topic: `<kimaki><directory>${project.worktree}</directory><app>${appId}</app></kimaki>`,
|
|
530
|
+
const { textChannelId, channelName } = await createProjectChannels({
|
|
531
|
+
guild: targetGuild,
|
|
532
|
+
projectDirectory: project.worktree,
|
|
533
|
+
appId,
|
|
547
534
|
})
|
|
548
535
|
|
|
549
|
-
const voiceChannel = await targetGuild.channels.create({
|
|
550
|
-
name: channelName,
|
|
551
|
-
type: ChannelType.GuildVoice,
|
|
552
|
-
parent: kimakiCategory,
|
|
553
|
-
})
|
|
554
|
-
|
|
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
536
|
createdChannels.push({
|
|
564
|
-
name:
|
|
565
|
-
id:
|
|
537
|
+
name: channelName,
|
|
538
|
+
id: textChannelId,
|
|
566
539
|
guildId: targetGuild.id,
|
|
567
540
|
})
|
|
568
541
|
} catch (error) {
|
|
569
|
-
cliLogger.error(`Failed to create channels for ${
|
|
542
|
+
cliLogger.error(`Failed to create channels for ${path.basename(project.worktree)}:`, error)
|
|
570
543
|
}
|
|
571
544
|
}
|
|
572
545
|
|