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.
@@ -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.1",
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 kimakiCategory = await ensureKimakiCategory(targetGuild)
541
-
542
- const textChannel = await targetGuild.channels.create({
543
- name: channelName,
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: textChannel.name,
565
- id: textChannel.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 ${baseName}:`, error)
542
+ cliLogger.error(`Failed to create channels for ${path.basename(project.worktree)}:`, error)
570
543
  }
571
544
  }
572
545