kimaki 0.4.24 → 0.4.25

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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/bin.js +6 -1
  3. package/dist/ai-tool-to-genai.js +3 -0
  4. package/dist/channel-management.js +3 -0
  5. package/dist/cli.js +93 -14
  6. package/dist/commands/abort.js +78 -0
  7. package/dist/commands/add-project.js +97 -0
  8. package/dist/commands/create-new-project.js +78 -0
  9. package/dist/commands/fork.js +186 -0
  10. package/dist/commands/model.js +294 -0
  11. package/dist/commands/permissions.js +126 -0
  12. package/dist/commands/queue.js +129 -0
  13. package/dist/commands/resume.js +145 -0
  14. package/dist/commands/session.js +144 -0
  15. package/dist/commands/share.js +80 -0
  16. package/dist/commands/types.js +2 -0
  17. package/dist/commands/undo-redo.js +161 -0
  18. package/dist/database.js +3 -0
  19. package/dist/discord-bot.js +3 -0
  20. package/dist/discord-utils.js +10 -1
  21. package/dist/format-tables.js +3 -0
  22. package/dist/genai-worker-wrapper.js +3 -0
  23. package/dist/genai-worker.js +3 -0
  24. package/dist/genai.js +3 -0
  25. package/dist/interaction-handler.js +71 -697
  26. package/dist/logger.js +3 -0
  27. package/dist/markdown.js +3 -0
  28. package/dist/message-formatting.js +41 -6
  29. package/dist/opencode.js +3 -0
  30. package/dist/session-handler.js +47 -3
  31. package/dist/system-message.js +16 -0
  32. package/dist/tools.js +3 -0
  33. package/dist/utils.js +3 -0
  34. package/dist/voice-handler.js +3 -0
  35. package/dist/voice.js +3 -0
  36. package/dist/worker-types.js +3 -0
  37. package/dist/xml.js +3 -0
  38. package/package.json +11 -12
  39. package/src/ai-tool-to-genai.ts +4 -0
  40. package/src/channel-management.ts +4 -0
  41. package/src/cli.ts +93 -14
  42. package/src/commands/abort.ts +94 -0
  43. package/src/commands/add-project.ts +138 -0
  44. package/src/commands/create-new-project.ts +111 -0
  45. package/src/{fork.ts → commands/fork.ts} +39 -5
  46. package/src/{model-command.ts → commands/model.ts} +7 -5
  47. package/src/commands/permissions.ts +146 -0
  48. package/src/commands/queue.ts +181 -0
  49. package/src/commands/resume.ts +230 -0
  50. package/src/commands/session.ts +186 -0
  51. package/src/commands/share.ts +96 -0
  52. package/src/commands/types.ts +25 -0
  53. package/src/commands/undo-redo.ts +213 -0
  54. package/src/database.ts +4 -0
  55. package/src/discord-bot.ts +4 -0
  56. package/src/discord-utils.ts +12 -0
  57. package/src/format-tables.ts +4 -0
  58. package/src/genai-worker-wrapper.ts +4 -0
  59. package/src/genai-worker.ts +4 -0
  60. package/src/genai.ts +4 -0
  61. package/src/interaction-handler.ts +81 -919
  62. package/src/logger.ts +4 -0
  63. package/src/markdown.ts +4 -0
  64. package/src/message-formatting.ts +52 -7
  65. package/src/opencode.ts +4 -0
  66. package/src/session-handler.ts +70 -3
  67. package/src/system-message.ts +17 -0
  68. package/src/tools.ts +4 -0
  69. package/src/utils.ts +4 -0
  70. package/src/voice-handler.ts +4 -0
  71. package/src/voice.ts +4 -0
  72. package/src/worker-types.ts +4 -0
  73. package/src/xml.ts +4 -0
  74. package/README.md +0 -48
package/src/logger.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // Prefixed logging utility using @clack/prompts.
2
+ // Creates loggers with consistent prefixes for different subsystems
3
+ // (DISCORD, VOICE, SESSION, etc.) for easier debugging.
4
+
1
5
  import { log } from '@clack/prompts'
2
6
 
3
7
  export function createLogger(prefix: string) {
package/src/markdown.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // Session-to-markdown renderer for sharing.
2
+ // Generates shareable markdown from OpenCode sessions, formatting
3
+ // user messages, assistant responses, tool calls, and reasoning blocks.
4
+
1
5
  import type { OpencodeClient } from '@opencode-ai/sdk'
2
6
  import * as yaml from 'js-yaml'
3
7
  import { formatDateTime } from './utils.js'
@@ -1,9 +1,45 @@
1
- import type { Part, FilePartInput } from '@opencode-ai/sdk'
1
+ // OpenCode message part formatting for Discord.
2
+ // Converts SDK message parts (text, tools, reasoning) to Discord-friendly format,
3
+ // handles file attachments, and provides tool summary generation.
4
+
5
+ import type { Part, FilePartInput, SessionMessagesResponse } from '@opencode-ai/sdk'
2
6
  import type { Message } from 'discord.js'
3
7
  import { createLogger } from './logger.js'
4
8
 
5
9
  const logger = createLogger('FORMATTING')
6
10
 
11
+ /**
12
+ * Collects and formats the last N assistant parts from session messages.
13
+ * Used by both /resume and /fork to show recent assistant context.
14
+ */
15
+ export function collectLastAssistantParts({
16
+ messages,
17
+ limit = 30,
18
+ }: {
19
+ messages: SessionMessagesResponse
20
+ limit?: number
21
+ }): { partIds: string[]; content: string; skippedCount: number } {
22
+ const allAssistantParts: { id: string; content: string }[] = []
23
+
24
+ for (const message of messages) {
25
+ if (message.info.role === 'assistant') {
26
+ for (const part of message.parts) {
27
+ const content = formatPart(part)
28
+ if (content.trim()) {
29
+ allAssistantParts.push({ id: part.id, content: content.trimEnd() })
30
+ }
31
+ }
32
+ }
33
+ }
34
+
35
+ const partsToRender = allAssistantParts.slice(-limit)
36
+ const partIds = partsToRender.map((p) => p.id)
37
+ const content = partsToRender.map((p) => p.content).join('\n')
38
+ const skippedCount = allAssistantParts.length - partsToRender.length
39
+
40
+ return { partIds, content, skippedCount }
41
+ }
42
+
7
43
  export const TEXT_MIME_TYPES = [
8
44
  'text/',
9
45
  'application/json',
@@ -136,7 +172,7 @@ export function getToolSummaryText(part: Part): string {
136
172
  .map(([key, value]) => {
137
173
  if (value === null || value === undefined) return null
138
174
  const stringValue = typeof value === 'string' ? value : JSON.stringify(value)
139
- const truncatedValue = stringValue.length > 300 ? stringValue.slice(0, 300) + '…' : stringValue
175
+ const truncatedValue = stringValue.length > 50 ? stringValue.slice(0, 50) + '…' : stringValue
140
176
  return `${key}: ${truncatedValue}`
141
177
  })
142
178
  .filter(Boolean)
@@ -163,12 +199,13 @@ export function formatTodoList(part: Part): string {
163
199
 
164
200
  export function formatPart(part: Part): string {
165
201
  if (part.type === 'text') {
166
- return part.text || ''
202
+ if (!part.text?.trim()) return ''
203
+ return `⬥ ${part.text}`
167
204
  }
168
205
 
169
206
  if (part.type === 'reasoning') {
170
207
  if (!part.text?.trim()) return ''
171
- return `◼︎ thinking`
208
+ return `┣ thinking`
172
209
  }
173
210
 
174
211
  if (part.type === 'file') {
@@ -180,11 +217,11 @@ export function formatPart(part: Part): string {
180
217
  }
181
218
 
182
219
  if (part.type === 'agent') {
183
- return `◼︎ agent ${part.id}`
220
+ return `┣ agent ${part.id}`
184
221
  }
185
222
 
186
223
  if (part.type === 'snapshot') {
187
- return `◼︎ snapshot ${part.snapshot}`
224
+ return `┣ snapshot ${part.snapshot}`
188
225
  }
189
226
 
190
227
  if (part.type === 'tool') {
@@ -218,7 +255,15 @@ export function formatPart(part: Part): string {
218
255
  toolTitle = `_${stateTitle}_`
219
256
  }
220
257
 
221
- const icon = part.state.status === 'error' ? '⨯' : '◼︎'
258
+ const icon = (() => {
259
+ if (part.state.status === 'error') {
260
+ return '⨯'
261
+ }
262
+ if (part.tool === 'edit' || part.tool === 'write') {
263
+ return '◼︎'
264
+ }
265
+ return '┣'
266
+ })()
222
267
  return `${icon} ${part.tool} ${toolTitle} ${summaryText}`
223
268
  }
224
269
 
package/src/opencode.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // OpenCode server process manager.
2
+ // Spawns and maintains OpenCode API servers per project directory,
3
+ // handles automatic restarts on failure, and provides typed SDK clients.
4
+
1
5
  import { spawn, type ChildProcess } from 'node:child_process'
2
6
  import net from 'node:net'
3
7
  import {
@@ -1,9 +1,13 @@
1
+ // OpenCode session lifecycle manager.
2
+ // Creates, maintains, and sends prompts to OpenCode sessions from Discord threads.
3
+ // Handles streaming events, permissions, abort signals, and message queuing.
4
+
1
5
  import type { Part, FilePartInput, Permission } from '@opencode-ai/sdk'
2
6
  import type { Message, ThreadChannel } from 'discord.js'
3
7
  import prettyMilliseconds from 'pretty-ms'
4
8
  import { getDatabase, getSessionModel, getChannelModel } from './database.js'
5
9
  import { initializeOpencodeForDirectory, getOpencodeServers } from './opencode.js'
6
- import { sendThreadMessage } from './discord-utils.js'
10
+ import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from './discord-utils.js'
7
11
  import { formatPart } from './message-formatting.js'
8
12
  import { getOpencodeSystemMessage } from './system-message.js'
9
13
  import { createLogger } from './logger.js'
@@ -42,6 +46,39 @@ export const pendingPermissions = new Map<
42
46
  { permission: Permission; messageId: string; directory: string }
43
47
  >()
44
48
 
49
+ export type QueuedMessage = {
50
+ prompt: string
51
+ userId: string
52
+ username: string
53
+ queuedAt: number
54
+ images?: FilePartInput[]
55
+ }
56
+
57
+ // Queue of messages waiting to be sent after current response finishes
58
+ // Key is threadId, value is array of queued messages
59
+ export const messageQueue = new Map<string, QueuedMessage[]>()
60
+
61
+ export function addToQueue({
62
+ threadId,
63
+ message,
64
+ }: {
65
+ threadId: string
66
+ message: QueuedMessage
67
+ }): number {
68
+ const queue = messageQueue.get(threadId) || []
69
+ queue.push(message)
70
+ messageQueue.set(threadId, queue)
71
+ return queue.length
72
+ }
73
+
74
+ export function getQueueLength(threadId: string): number {
75
+ return messageQueue.get(threadId)?.length || 0
76
+ }
77
+
78
+ export function clearQueue(threadId: string): void {
79
+ messageQueue.delete(threadId)
80
+ }
81
+
45
82
  export async function handleOpencodeSession({
46
83
  prompt,
47
84
  thread,
@@ -296,7 +333,7 @@ export async function handleOpencodeSession({
296
333
  const thresholdCrossed = Math.floor(currentPercentage / 10) * 10
297
334
  if (thresholdCrossed > lastDisplayedContextPercentage && thresholdCrossed >= 10) {
298
335
  lastDisplayedContextPercentage = thresholdCrossed
299
- await sendThreadMessage(thread, `◼︎ context usage ${currentPercentage}%`)
336
+ await sendThreadMessage(thread, `⬥ context usage ${currentPercentage}%`)
300
337
  }
301
338
  }
302
339
  }
@@ -467,8 +504,38 @@ export async function handleOpencodeSession({
467
504
  sessionLogger.error('Failed to fetch provider info for context percentage:', e)
468
505
  }
469
506
 
470
- await sendThreadMessage(thread, `_Completed in ${sessionDuration}${contextInfo}_${attachCommand}${modelInfo}`)
507
+ await sendThreadMessage(thread, `_Completed in ${sessionDuration}${contextInfo}_${attachCommand}${modelInfo}`, { flags: NOTIFY_MESSAGE_FLAGS })
471
508
  sessionLogger.log(`DURATION: Session completed in ${sessionDuration}, port ${port}, model ${usedModel}, tokens ${tokensUsedInSession}`)
509
+
510
+ // Process queued messages after completion
511
+ const queue = messageQueue.get(thread.id)
512
+ if (queue && queue.length > 0) {
513
+ const nextMessage = queue.shift()!
514
+ if (queue.length === 0) {
515
+ messageQueue.delete(thread.id)
516
+ }
517
+
518
+ sessionLogger.log(`[QUEUE] Processing queued message from ${nextMessage.username}`)
519
+
520
+ // Show that queued message is being sent
521
+ await sendThreadMessage(thread, `» **${nextMessage.username}:** ${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`)
522
+
523
+ // Send the queued message as a new prompt (recursive call)
524
+ // Use setImmediate to avoid blocking and allow this finally to complete
525
+ setImmediate(() => {
526
+ handleOpencodeSession({
527
+ prompt: nextMessage.prompt,
528
+ thread,
529
+ projectDirectory,
530
+ images: nextMessage.images,
531
+ channelId,
532
+ }).catch(async (e) => {
533
+ sessionLogger.error(`[QUEUE] Failed to process queued message:`, e)
534
+ const errorMsg = e instanceof Error ? e.message : String(e)
535
+ await sendThreadMessage(thread, `✗ Queued message failed: ${errorMsg.slice(0, 200)}`)
536
+ })
537
+ })
538
+ }
472
539
  } else {
473
540
  sessionLogger.log(
474
541
  `Session was aborted (reason: ${abortController.signal.reason}), skipping duration message`,
@@ -1,3 +1,7 @@
1
+ // OpenCode system prompt generator.
2
+ // Creates the system message injected into every OpenCode session,
3
+ // including Discord-specific formatting rules, diff commands, and permissions info.
4
+
1
5
  export function getOpencodeSystemMessage({ sessionId }: { sessionId: string }) {
2
6
  return `
3
7
  The user is reading your messages from inside Discord, via kimaki.xyz
@@ -66,6 +70,19 @@ the max heading level is 3, so do not use ####
66
70
 
67
71
  headings are discouraged anyway. instead try to use bold text for titles which renders more nicely in Discord
68
72
 
73
+ ## capitalization
74
+
75
+ write casually like a discord user. never capitalize the initials of phrases or acronyms in your messages. use all lowercase instead.
76
+
77
+ examples:
78
+ - write "api" not "API"
79
+ - write "url" not "URL"
80
+ - write "json" not "JSON"
81
+ - write "cli" not "CLI"
82
+ - write "sdk" not "SDK"
83
+
84
+ this makes your messages blend in naturally with how people actually type on discord.
85
+
69
86
  ## tables
70
87
 
71
88
  discord does NOT support markdown gfm tables.
package/src/tools.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // Voice assistant tool definitions for the GenAI worker.
2
+ // Provides tools for managing OpenCode sessions (create, submit, abort),
3
+ // listing chats, searching files, and reading session messages.
4
+
1
5
  import { tool } from 'ai'
2
6
  import { z } from 'zod'
3
7
  import { spawn, type ChildProcess } from 'node:child_process'
package/src/utils.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // General utility functions for the bot.
2
+ // Includes Discord OAuth URL generation, array deduplication,
3
+ // abort error detection, and date/time formatting helpers.
4
+
1
5
  import { PermissionsBitField } from 'discord.js'
2
6
 
3
7
  type GenerateInstallUrlOptions = {
@@ -1,3 +1,7 @@
1
+ // Discord voice channel connection and audio stream handler.
2
+ // Manages joining/leaving voice channels, captures user audio, resamples to 16kHz,
3
+ // and routes audio to the GenAI worker for real-time voice assistant interactions.
4
+
1
5
  import {
2
6
  VoiceConnectionStatus,
3
7
  EndBehaviorType,
package/src/voice.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // Audio transcription service using Google Gemini.
2
+ // Transcribes voice messages with code-aware context, using grep/glob tools
3
+ // to verify technical terms, filenames, and function names in the codebase.
4
+
1
5
  import {
2
6
  GoogleGenAI,
3
7
  Type,
@@ -1,3 +1,7 @@
1
+ // Type definitions for worker thread message passing.
2
+ // Defines the protocol between main thread and GenAI worker for
3
+ // audio streaming, tool calls, and session lifecycle management.
4
+
1
5
  import type { Tool as AITool } from 'ai'
2
6
 
3
7
  // Messages sent from main thread to worker
package/src/xml.ts CHANGED
@@ -1,3 +1,7 @@
1
+ // XML/HTML tag content extractor.
2
+ // Parses XML-like tags from strings (e.g., channel topics) to extract
3
+ // Kimaki configuration like directory paths and app IDs.
4
+
1
5
  import { DomHandler, Parser, ElementType } from 'htmlparser2'
2
6
  import type { ChildNode, Element, Text } from 'domhandler'
3
7
  import { createLogger } from './logger.js'
package/README.md DELETED
@@ -1,48 +0,0 @@
1
- # Kimaki Discord Bot
2
-
3
- A Discord bot that integrates OpenCode coding sessions with Discord channels and voice.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install -g kimaki
9
- ```
10
-
11
- ## Setup
12
-
13
- Run the interactive setup:
14
-
15
- ```bash
16
- kimaki
17
- ```
18
-
19
- This will guide you through:
20
- 1. Creating a Discord application at https://discord.com/developers/applications
21
- 2. Getting your bot token
22
- 3. Installing the bot to your Discord server
23
- 4. Creating channels for your OpenCode projects
24
-
25
- ## Commands
26
-
27
- ### Start the bot
28
-
29
- ```bash
30
- kimaki
31
- ```
32
-
33
- ## Discord Slash Commands
34
-
35
- Once the bot is running, you can use these commands in Discord:
36
-
37
- - `/session <prompt>` - Start a new OpenCode session
38
- - `/resume <session>` - Resume an existing session
39
- - `/add-project <project>` - Add a new project to Discord
40
- - `/accept` - Accept a permission request
41
- - `/accept-always` - Accept and auto-approve similar requests
42
- - `/reject` - Reject a permission request
43
-
44
- ## Voice Support
45
-
46
- Join a voice channel that has an associated project directory, and the bot will join with Jarvis-like voice interaction powered by Gemini.
47
-
48
- Requires a Gemini API key (prompted during setup).