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.
@@ -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, model }) => {
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.2",
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.11.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 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>`,
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: textChannel.name,
565
- id: textChannel.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 ${baseName}:`, error)
556
+ cliLogger.error(`Failed to create channels for ${path.basename(project.worktree)}:`, error)
570
557
  }
571
558
  }
572
559