kimaki 0.4.46 → 0.4.47

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 (79) hide show
  1. package/dist/cli.js +27 -2
  2. package/dist/commands/abort.js +2 -2
  3. package/dist/commands/add-project.js +2 -2
  4. package/dist/commands/agent.js +4 -4
  5. package/dist/commands/ask-question.js +9 -8
  6. package/dist/commands/compact.js +126 -0
  7. package/dist/commands/create-new-project.js +3 -3
  8. package/dist/commands/fork.js +3 -3
  9. package/dist/commands/merge-worktree.js +2 -2
  10. package/dist/commands/model.js +5 -5
  11. package/dist/commands/permissions.js +2 -2
  12. package/dist/commands/queue.js +2 -2
  13. package/dist/commands/remove-project.js +2 -2
  14. package/dist/commands/resume.js +2 -2
  15. package/dist/commands/session.js +2 -2
  16. package/dist/commands/share.js +2 -2
  17. package/dist/commands/undo-redo.js +2 -2
  18. package/dist/commands/user-command.js +2 -2
  19. package/dist/commands/verbosity.js +3 -3
  20. package/dist/commands/worktree-settings.js +2 -2
  21. package/dist/commands/worktree.js +18 -8
  22. package/dist/database.js +2 -2
  23. package/dist/discord-bot.js +3 -3
  24. package/dist/discord-utils.js +2 -2
  25. package/dist/genai-worker-wrapper.js +3 -3
  26. package/dist/genai-worker.js +2 -2
  27. package/dist/genai.js +2 -2
  28. package/dist/interaction-handler.js +6 -2
  29. package/dist/logger.js +57 -9
  30. package/dist/markdown.js +2 -2
  31. package/dist/message-formatting.js +69 -6
  32. package/dist/openai-realtime.js +2 -2
  33. package/dist/opencode.js +2 -2
  34. package/dist/session-handler.js +53 -14
  35. package/dist/tools.js +2 -2
  36. package/dist/voice-handler.js +2 -2
  37. package/dist/voice.js +2 -2
  38. package/dist/worktree-utils.js +91 -7
  39. package/dist/xml.js +2 -2
  40. package/package.json +1 -1
  41. package/src/cli.ts +28 -2
  42. package/src/commands/abort.ts +2 -2
  43. package/src/commands/add-project.ts +2 -2
  44. package/src/commands/agent.ts +4 -4
  45. package/src/commands/ask-question.ts +9 -8
  46. package/src/commands/compact.ts +148 -0
  47. package/src/commands/create-new-project.ts +3 -3
  48. package/src/commands/fork.ts +3 -3
  49. package/src/commands/merge-worktree.ts +2 -2
  50. package/src/commands/model.ts +5 -5
  51. package/src/commands/permissions.ts +2 -2
  52. package/src/commands/queue.ts +2 -2
  53. package/src/commands/remove-project.ts +2 -2
  54. package/src/commands/resume.ts +2 -2
  55. package/src/commands/session.ts +2 -2
  56. package/src/commands/share.ts +2 -2
  57. package/src/commands/undo-redo.ts +2 -2
  58. package/src/commands/user-command.ts +2 -2
  59. package/src/commands/verbosity.ts +3 -3
  60. package/src/commands/worktree-settings.ts +2 -2
  61. package/src/commands/worktree.ts +20 -7
  62. package/src/database.ts +2 -2
  63. package/src/discord-bot.ts +3 -3
  64. package/src/discord-utils.ts +2 -2
  65. package/src/genai-worker-wrapper.ts +3 -3
  66. package/src/genai-worker.ts +2 -2
  67. package/src/genai.ts +2 -2
  68. package/src/interaction-handler.ts +7 -2
  69. package/src/logger.ts +64 -10
  70. package/src/markdown.ts +2 -2
  71. package/src/message-formatting.ts +82 -6
  72. package/src/openai-realtime.ts +2 -2
  73. package/src/opencode.ts +2 -2
  74. package/src/session-handler.ts +62 -14
  75. package/src/tools.ts +2 -2
  76. package/src/voice-handler.ts +2 -2
  77. package/src/voice.ts +2 -2
  78. package/src/worktree-utils.ts +111 -7
  79. package/src/xml.ts +2 -2
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.46",
5
+ "version": "0.4.47",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
package/src/cli.ts CHANGED
@@ -43,14 +43,34 @@ import path from 'node:path'
43
43
  import fs from 'node:fs'
44
44
  import * as errore from 'errore'
45
45
 
46
- import { createLogger } from './logger.js'
46
+ import { createLogger, LogPrefix } from './logger.js'
47
47
  import { uploadFilesToDiscord } from './discord-utils.js'
48
48
  import { spawn, spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
49
49
  import http from 'node:http'
50
50
  import { setDataDir, getDataDir, getLockPort } from './config.js'
51
51
  import { sanitizeAgentName } from './commands/agent.js'
52
52
 
53
- const cliLogger = createLogger('CLI')
53
+ const cliLogger = createLogger(LogPrefix.CLI)
54
+
55
+ // Spawn caffeinate on macOS to prevent system sleep while bot is running.
56
+ // Not detached, so it dies automatically with the parent process.
57
+ function startCaffeinate() {
58
+ if (process.platform !== 'darwin') {
59
+ return
60
+ }
61
+ try {
62
+ const proc = spawn('caffeinate', ['-i'], {
63
+ stdio: 'ignore',
64
+ detached: false,
65
+ })
66
+ proc.on('error', (err) => {
67
+ cliLogger.warn('Failed to start caffeinate:', err.message)
68
+ })
69
+ cliLogger.log('Started caffeinate to prevent system sleep')
70
+ } catch (err) {
71
+ cliLogger.warn('Failed to spawn caffeinate:', err instanceof Error ? err.message : String(err))
72
+ }
73
+ }
54
74
  const cli = cac('kimaki')
55
75
 
56
76
  process.title = 'kimaki'
@@ -301,6 +321,10 @@ async function registerCommands({
301
321
  .setName('abort')
302
322
  .setDescription('Abort the current OpenCode request in this thread')
303
323
  .toJSON(),
324
+ new SlashCommandBuilder()
325
+ .setName('compact')
326
+ .setDescription('Compact the session context by summarizing conversation history')
327
+ .toJSON(),
304
328
  new SlashCommandBuilder()
305
329
  .setName('stop')
306
330
  .setDescription('Abort the current OpenCode request in this thread')
@@ -545,6 +569,8 @@ async function backgroundInit({
545
569
  }
546
570
 
547
571
  async function run({ restart, addChannels, useWorktrees }: CliOptions) {
572
+ startCaffeinate()
573
+
548
574
  const forceSetup = Boolean(restart)
549
575
 
550
576
  intro('🤖 Discord Bot Setup')
@@ -6,10 +6,10 @@ import { getDatabase } from '../database.js'
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
8
  import { abortControllers } from '../session-handler.js'
9
- import { createLogger } from '../logger.js'
9
+ import { createLogger, LogPrefix } from '../logger.js'
10
10
  import * as errore from 'errore'
11
11
 
12
- const logger = createLogger('ABORT')
12
+ const logger = createLogger(LogPrefix.ABORT)
13
13
 
14
14
  export async function handleAbortCommand({ command }: CommandContext): Promise<void> {
15
15
  const channel = command.channel
@@ -6,11 +6,11 @@ import type { CommandContext, AutocompleteContext } from './types.js'
6
6
  import { getDatabase } from '../database.js'
7
7
  import { initializeOpencodeForDirectory } from '../opencode.js'
8
8
  import { createProjectChannels } from '../channel-management.js'
9
- import { createLogger } from '../logger.js'
9
+ import { createLogger, LogPrefix } from '../logger.js'
10
10
  import { abbreviatePath } from '../utils.js'
11
11
  import * as errore from 'errore'
12
12
 
13
- const logger = createLogger('ADD-PROJECT')
13
+ const logger = createLogger(LogPrefix.ADD_PROJECT)
14
14
 
15
15
  export async function handleAddProjectCommand({ command, appId }: CommandContext): Promise<void> {
16
16
  await command.deferReply({ ephemeral: false })
@@ -14,10 +14,10 @@ import crypto from 'node:crypto'
14
14
  import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runModelMigrations } from '../database.js'
15
15
  import { initializeOpencodeForDirectory } from '../opencode.js'
16
16
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
17
- import { createLogger } from '../logger.js'
17
+ import { createLogger, LogPrefix } from '../logger.js'
18
18
  import * as errore from 'errore'
19
19
 
20
- const agentLogger = createLogger('AGENT')
20
+ const agentLogger = createLogger(LogPrefix.AGENT)
21
21
 
22
22
  const pendingAgentContexts = new Map<
23
23
  string,
@@ -257,7 +257,7 @@ export async function handleAgentSelectMenu(
257
257
  })
258
258
  } else {
259
259
  await interaction.editReply({
260
- content: `Agent preference set for this channel: **${selectedAgent}**\n\nAll new sessions in this channel will use this agent.`,
260
+ content: `Agent preference set for this channel: **${selectedAgent}**\nAll new sessions in this channel will use this agent.`,
261
261
  components: [],
262
262
  })
263
263
  }
@@ -331,7 +331,7 @@ export async function handleQuickAgentCommand({
331
331
  })
332
332
  } else {
333
333
  await command.editReply({
334
- content: `Switched to **${matchingAgent.name}** agent for this channel\n\nAll new sessions will use this agent.`,
334
+ content: `Switched to **${matchingAgent.name}** agent for this channel\nAll new sessions will use this agent.`,
335
335
  })
336
336
  }
337
337
  } catch (error) {
@@ -11,9 +11,9 @@ import {
11
11
  import crypto from 'node:crypto'
12
12
  import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
13
13
  import { getOpencodeClientV2 } from '../opencode.js'
14
- import { createLogger } from '../logger.js'
14
+ import { createLogger, LogPrefix } from '../logger.js'
15
15
 
16
- const logger = createLogger('ASK_QUESTION')
16
+ const logger = createLogger(LogPrefix.ASK_QUESTION)
17
17
 
18
18
  // Schema matching the question tool input
19
19
  export type AskUserQuestionInput = {
@@ -268,9 +268,9 @@ export function parseAskUserQuestionTool(part: {
268
268
 
269
269
  /**
270
270
  * Cancel a pending question for a thread (e.g., when user sends a new message).
271
- * Sends cancellation response to OpenCode so the session can continue.
271
+ * Sends the user's message as the answer to OpenCode so the model sees their actual response.
272
272
  */
273
- export async function cancelPendingQuestion(threadId: string): Promise<boolean> {
273
+ export async function cancelPendingQuestion(threadId: string, userMessage?: string): Promise<boolean> {
274
274
  // Find pending question for this thread
275
275
  let contextHash: string | undefined
276
276
  let context: PendingQuestionContext | undefined
@@ -292,9 +292,10 @@ export async function cancelPendingQuestion(threadId: string): Promise<boolean>
292
292
  throw new Error('OpenCode server not found for directory')
293
293
  }
294
294
 
295
- // Preserve already-answered questions, mark unanswered as cancelled
295
+ // Use user's message as answer if provided, otherwise mark as "Other"
296
+ const customAnswer = userMessage || 'Other'
296
297
  const answers = context.questions.map((_, i) => {
297
- return context.answers[i] || ['(cancelled - user sent new message)']
298
+ return context.answers[i] || [customAnswer]
298
299
  })
299
300
 
300
301
  await clientV2.question.reply({
@@ -302,9 +303,9 @@ export async function cancelPendingQuestion(threadId: string): Promise<boolean>
302
303
  answers,
303
304
  })
304
305
 
305
- logger.log(`Cancelled question ${context.requestId} due to new user message`)
306
+ logger.log(`Answered question ${context.requestId} with user message`)
306
307
  } catch (error) {
307
- logger.error('Failed to cancel question:', error)
308
+ logger.error('Failed to answer question:', error)
308
309
  }
309
310
 
310
311
  // Clean up regardless of whether the API call succeeded
@@ -0,0 +1,148 @@
1
+ // /compact command - Trigger context compaction (summarization) for the current session.
2
+
3
+ import { ChannelType, type ThreadChannel } from 'discord.js'
4
+ import type { CommandContext } from './types.js'
5
+ import { getDatabase } from '../database.js'
6
+ import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js'
7
+ import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
+
10
+ const logger = createLogger(LogPrefix.COMPACT)
11
+
12
+ export async function handleCompactCommand({ command }: CommandContext): Promise<void> {
13
+ const channel = command.channel
14
+
15
+ if (!channel) {
16
+ await command.reply({
17
+ content: 'This command can only be used in a channel',
18
+ ephemeral: true,
19
+ flags: SILENT_MESSAGE_FLAGS,
20
+ })
21
+ return
22
+ }
23
+
24
+ const isThread = [
25
+ ChannelType.PublicThread,
26
+ ChannelType.PrivateThread,
27
+ ChannelType.AnnouncementThread,
28
+ ].includes(channel.type)
29
+
30
+ if (!isThread) {
31
+ await command.reply({
32
+ content: 'This command can only be used in a thread with an active session',
33
+ ephemeral: true,
34
+ flags: SILENT_MESSAGE_FLAGS,
35
+ })
36
+ return
37
+ }
38
+
39
+ const textChannel = await resolveTextChannel(channel as ThreadChannel)
40
+ const { projectDirectory: directory } = getKimakiMetadata(textChannel)
41
+
42
+ if (!directory) {
43
+ await command.reply({
44
+ content: 'Could not determine project directory for this channel',
45
+ ephemeral: true,
46
+ flags: SILENT_MESSAGE_FLAGS,
47
+ })
48
+ return
49
+ }
50
+
51
+ const row = getDatabase()
52
+ .prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
53
+ .get(channel.id) as { session_id: string } | undefined
54
+
55
+ if (!row?.session_id) {
56
+ await command.reply({
57
+ content: 'No active session in this thread',
58
+ ephemeral: true,
59
+ flags: SILENT_MESSAGE_FLAGS,
60
+ })
61
+ return
62
+ }
63
+
64
+ const sessionId = row.session_id
65
+
66
+ // Ensure server is running for this directory
67
+ const getClient = await initializeOpencodeForDirectory(directory)
68
+ if (getClient instanceof Error) {
69
+ await command.reply({
70
+ content: `Failed to compact: ${getClient.message}`,
71
+ ephemeral: true,
72
+ flags: SILENT_MESSAGE_FLAGS,
73
+ })
74
+ return
75
+ }
76
+
77
+ const clientV2 = getOpencodeClientV2(directory)
78
+ if (!clientV2) {
79
+ await command.reply({
80
+ content: 'Failed to get OpenCode client',
81
+ ephemeral: true,
82
+ flags: SILENT_MESSAGE_FLAGS,
83
+ })
84
+ return
85
+ }
86
+
87
+ // Defer reply since compaction may take a moment
88
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
89
+
90
+ try {
91
+ // Get session messages to find the model from the last user message
92
+ const messagesResult = await clientV2.session.messages({
93
+ sessionID: sessionId,
94
+ directory,
95
+ })
96
+
97
+ if (messagesResult.error || !messagesResult.data) {
98
+ logger.error('[COMPACT] Failed to get messages:', messagesResult.error)
99
+ await command.editReply({
100
+ content: 'Failed to compact: Could not retrieve session messages',
101
+ })
102
+ return
103
+ }
104
+
105
+ // Find the last user message to get the model
106
+ const lastUserMessage = [...messagesResult.data]
107
+ .reverse()
108
+ .find((msg) => msg.info.role === 'user')
109
+
110
+ if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
111
+ await command.editReply({
112
+ content: 'Failed to compact: No user message found in session',
113
+ })
114
+ return
115
+ }
116
+
117
+ const { providerID, modelID } = lastUserMessage.info.model
118
+
119
+ const result = await clientV2.session.summarize({
120
+ sessionID: sessionId,
121
+ directory,
122
+ providerID,
123
+ modelID,
124
+ auto: false,
125
+ })
126
+
127
+ if (result.error) {
128
+ logger.error('[COMPACT] Error:', result.error)
129
+ const errorMessage = 'data' in result.error && result.error.data
130
+ ? (result.error.data as { message?: string }).message || 'Unknown error'
131
+ : 'Unknown error'
132
+ await command.editReply({
133
+ content: `Failed to compact: ${errorMessage}`,
134
+ })
135
+ return
136
+ }
137
+
138
+ await command.editReply({
139
+ content: `📦 Session **compacted** successfully`,
140
+ })
141
+ logger.log(`Session ${sessionId} compacted by user`)
142
+ } catch (error) {
143
+ logger.error('[COMPACT] Error:', error)
144
+ await command.editReply({
145
+ content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
146
+ })
147
+ }
148
+ }
@@ -8,9 +8,9 @@ import { getProjectsDir } from '../config.js'
8
8
  import { createProjectChannels } from '../channel-management.js'
9
9
  import { handleOpencodeSession } from '../session-handler.js'
10
10
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
11
- import { createLogger } from '../logger.js'
11
+ import { createLogger, LogPrefix } from '../logger.js'
12
12
 
13
- const logger = createLogger('CREATE-NEW-PROJECT')
13
+ const logger = createLogger(LogPrefix.CREATE_PROJECT)
14
14
 
15
15
  export async function handleCreateNewProjectCommand({
16
16
  command,
@@ -74,7 +74,7 @@ export async function handleCreateNewProjectCommand({
74
74
  const textChannel = (await guild.channels.fetch(textChannelId)) as TextChannel
75
75
 
76
76
  await command.editReply(
77
- `✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n\n_Starting session..._`,
77
+ `✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n_Starting session..._`,
78
78
  )
79
79
 
80
80
  const starterMessage = await textChannel.send({
@@ -13,11 +13,11 @@ import { getDatabase } from '../database.js'
13
13
  import { initializeOpencodeForDirectory } from '../opencode.js'
14
14
  import { resolveTextChannel, getKimakiMetadata, sendThreadMessage } from '../discord-utils.js'
15
15
  import { collectLastAssistantParts } from '../message-formatting.js'
16
- import { createLogger } from '../logger.js'
16
+ import { createLogger, LogPrefix } from '../logger.js'
17
17
  import * as errore from 'errore'
18
18
 
19
- const sessionLogger = createLogger('SESSION')
20
- const forkLogger = createLogger('FORK')
19
+ const sessionLogger = createLogger(LogPrefix.SESSION)
20
+ const forkLogger = createLogger(LogPrefix.FORK)
21
21
 
22
22
  export async function handleForkCommand(interaction: ChatInputCommandInteraction): Promise<void> {
23
23
  const channel = interaction.channel
@@ -5,10 +5,10 @@
5
5
  import { type ThreadChannel } from 'discord.js'
6
6
  import type { CommandContext } from './types.js'
7
7
  import { getThreadWorktree } from '../database.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import { execAsync } from '../worktree-utils.js'
10
10
 
11
- const logger = createLogger('MERGE-WORKTREE')
11
+ const logger = createLogger(LogPrefix.WORKTREE)
12
12
 
13
13
  /** Worktree thread title prefix - indicates unmerged worktree */
14
14
  export const WORKTREE_PREFIX = '⬦ '
@@ -14,10 +14,10 @@ import { getDatabase, setChannelModel, setSessionModel, runModelMigrations } fro
14
14
  import { initializeOpencodeForDirectory } from '../opencode.js'
15
15
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js'
16
16
  import { abortAndRetrySession } from '../session-handler.js'
17
- import { createLogger } from '../logger.js'
17
+ import { createLogger, LogPrefix } from '../logger.js'
18
18
  import * as errore from 'errore'
19
19
 
20
- const modelLogger = createLogger('MODEL')
20
+ const modelLogger = createLogger(LogPrefix.MODEL)
21
21
 
22
22
  // Store context by hash to avoid customId length limits (Discord max: 100 chars)
23
23
  const pendingModelContexts = new Map<
@@ -385,12 +385,12 @@ export async function handleModelSelectMenu(
385
385
 
386
386
  if (retried) {
387
387
  await interaction.editReply({
388
- content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\`\n\n_Retrying current request with new model..._`,
388
+ content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\n_Retrying current request with new model..._`,
389
389
  components: [],
390
390
  })
391
391
  } else {
392
392
  await interaction.editReply({
393
- content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\``,
393
+ content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\``,
394
394
  components: [],
395
395
  })
396
396
  }
@@ -400,7 +400,7 @@ export async function handleModelSelectMenu(
400
400
  modelLogger.log(`Set model ${fullModelId} for channel ${context.channelId}`)
401
401
 
402
402
  await interaction.editReply({
403
- content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\`\n\nAll new sessions in this channel will use this model.`,
403
+ content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\nAll new sessions in this channel will use this model.`,
404
404
  components: [],
405
405
  })
406
406
  }
@@ -12,9 +12,9 @@ import crypto from 'node:crypto'
12
12
  import type { PermissionRequest } from '@opencode-ai/sdk/v2'
13
13
  import { getOpencodeClientV2 } from '../opencode.js'
14
14
  import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
15
- import { createLogger } from '../logger.js'
15
+ import { createLogger, LogPrefix } from '../logger.js'
16
16
 
17
- const logger = createLogger('PERMISSIONS')
17
+ const logger = createLogger(LogPrefix.PERMISSIONS)
18
18
 
19
19
  type PendingPermissionContext = {
20
20
  permission: PermissionRequest
@@ -16,9 +16,9 @@ import {
16
16
  getQueueLength,
17
17
  clearQueue,
18
18
  } from '../session-handler.js'
19
- import { createLogger } from '../logger.js'
19
+ import { createLogger, LogPrefix } from '../logger.js'
20
20
 
21
- const logger = createLogger('QUEUE')
21
+ const logger = createLogger(LogPrefix.QUEUE)
22
22
 
23
23
  export async function handleQueueCommand({ command }: CommandContext): Promise<void> {
24
24
  const message = command.options.getString('message', true)
@@ -4,10 +4,10 @@ import path from 'node:path'
4
4
  import * as errore from 'errore'
5
5
  import type { CommandContext, AutocompleteContext } from './types.js'
6
6
  import { getDatabase } from '../database.js'
7
- import { createLogger } from '../logger.js'
7
+ import { createLogger, LogPrefix } from '../logger.js'
8
8
  import { abbreviatePath } from '../utils.js'
9
9
 
10
- const logger = createLogger('REMOVE-PROJECT')
10
+ const logger = createLogger(LogPrefix.REMOVE_PROJECT)
11
11
 
12
12
  export async function handleRemoveProjectCommand({ command, appId }: CommandContext): Promise<void> {
13
13
  await command.deferReply({ ephemeral: false })
@@ -12,10 +12,10 @@ import { getDatabase, getChannelDirectory } from '../database.js'
12
12
  import { initializeOpencodeForDirectory } from '../opencode.js'
13
13
  import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js'
14
14
  import { collectLastAssistantParts } from '../message-formatting.js'
15
- import { createLogger } from '../logger.js'
15
+ import { createLogger, LogPrefix } from '../logger.js'
16
16
  import * as errore from 'errore'
17
17
 
18
- const logger = createLogger('RESUME')
18
+ const logger = createLogger(LogPrefix.RESUME)
19
19
 
20
20
  export async function handleResumeCommand({ command, appId }: CommandContext): Promise<void> {
21
21
  await command.deferReply({ ephemeral: false })
@@ -8,10 +8,10 @@ import { getDatabase, getChannelDirectory } from '../database.js'
8
8
  import { initializeOpencodeForDirectory } from '../opencode.js'
9
9
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
10
10
  import { handleOpencodeSession } from '../session-handler.js'
11
- import { createLogger } from '../logger.js'
11
+ import { createLogger, LogPrefix } from '../logger.js'
12
12
  import * as errore from 'errore'
13
13
 
14
- const logger = createLogger('SESSION')
14
+ const logger = createLogger(LogPrefix.SESSION)
15
15
 
16
16
  export async function handleSessionCommand({ command, appId }: CommandContext): Promise<void> {
17
17
  await command.deferReply({ ephemeral: false })
@@ -5,10 +5,10 @@ import type { CommandContext } from './types.js'
5
5
  import { getDatabase } from '../database.js'
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import * as errore from 'errore'
10
10
 
11
- const logger = createLogger('SHARE')
11
+ const logger = createLogger(LogPrefix.SHARE)
12
12
 
13
13
  export async function handleShareCommand({ command }: CommandContext): Promise<void> {
14
14
  const channel = command.channel
@@ -5,10 +5,10 @@ import type { CommandContext } from './types.js'
5
5
  import { getDatabase } from '../database.js'
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js'
7
7
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import * as errore from 'errore'
10
10
 
11
- const logger = createLogger('UNDO-REDO')
11
+ const logger = createLogger(LogPrefix.UNDO_REDO)
12
12
 
13
13
  export async function handleUndoCommand({ command }: CommandContext): Promise<void> {
14
14
  const channel = command.channel
@@ -5,11 +5,11 @@ import type { CommandContext, CommandHandler } from './types.js'
5
5
  import { ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
6
6
  import { handleOpencodeSession } from '../session-handler.js'
7
7
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
  import { getDatabase, getChannelDirectory } from '../database.js'
10
10
  import fs from 'node:fs'
11
11
 
12
- const userCommandLogger = createLogger('USER_CMD')
12
+ const userCommandLogger = createLogger(LogPrefix.USER_CMD)
13
13
 
14
14
  export const handleUserCommand: CommandHandler = async ({ command, appId }: CommandContext) => {
15
15
  const discordCommandName = command.commandName
@@ -5,9 +5,9 @@
5
5
 
6
6
  import { ChatInputCommandInteraction, ChannelType, type TextChannel, type ThreadChannel } from 'discord.js'
7
7
  import { getChannelVerbosity, setChannelVerbosity, type VerbosityLevel } from '../database.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
 
10
- const verbosityLogger = createLogger('VERBOSITY')
10
+ const verbosityLogger = createLogger(LogPrefix.VERBOSITY)
11
11
 
12
12
  /**
13
13
  * Handle the /verbosity slash command.
@@ -65,7 +65,7 @@ export async function handleVerbosityCommand({
65
65
  : 'All output will be shown, including tool executions and status messages.'
66
66
 
67
67
  await command.reply({
68
- content: `Verbosity set to **${level}**.\n\n${description}\n\nThis applies to all new sessions in this channel.`,
68
+ content: `Verbosity set to **${level}**.\n${description}\nThis applies to all new sessions in this channel.`,
69
69
  ephemeral: true,
70
70
  })
71
71
  }
@@ -5,9 +5,9 @@
5
5
  import { ChatInputCommandInteraction, ChannelType, type TextChannel } from 'discord.js'
6
6
  import { getChannelWorktreesEnabled, setChannelWorktreesEnabled } from '../database.js'
7
7
  import { getKimakiMetadata } from '../discord-utils.js'
8
- import { createLogger } from '../logger.js'
8
+ import { createLogger, LogPrefix } from '../logger.js'
9
9
 
10
- const worktreeSettingsLogger = createLogger('WORKTREE_SETTINGS')
10
+ const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE)
11
11
 
12
12
  /**
13
13
  * Handle the /enable-worktrees slash command.
@@ -14,12 +14,12 @@ import {
14
14
  } from '../database.js'
15
15
  import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js'
16
16
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
17
- import { createLogger } from '../logger.js'
18
- import { createWorktreeWithSubmodules } from '../worktree-utils.js'
17
+ import { createLogger, LogPrefix } from '../logger.js'
18
+ import { createWorktreeWithSubmodules, captureGitDiff, type CapturedDiff } from '../worktree-utils.js'
19
19
  import { WORKTREE_PREFIX } from './merge-worktree.js'
20
20
  import * as errore from 'errore'
21
21
 
22
- const logger = createLogger('WORKTREE')
22
+ const logger = createLogger(LogPrefix.WORKTREE)
23
23
 
24
24
  class WorktreeError extends Error {
25
25
  constructor(message: string, options?: { cause?: unknown }) {
@@ -91,6 +91,7 @@ function getProjectDirectoryFromChannel(
91
91
 
92
92
  /**
93
93
  * Create worktree in background and update starter message when done.
94
+ * If diff is provided, it's applied during worktree creation (before submodule init).
94
95
  */
95
96
  async function createWorktreeInBackground({
96
97
  thread,
@@ -98,19 +99,22 @@ async function createWorktreeInBackground({
98
99
  worktreeName,
99
100
  projectDirectory,
100
101
  clientV2,
102
+ diff,
101
103
  }: {
102
104
  thread: ThreadChannel
103
105
  starterMessage: Message
104
106
  worktreeName: string
105
107
  projectDirectory: string
106
108
  clientV2: ReturnType<typeof getOpencodeClientV2> & {}
109
+ diff?: CapturedDiff | null
107
110
  }): Promise<void> {
108
- // Create worktree using SDK v2 and init submodules
111
+ // Create worktree using SDK v2, apply diff, then init submodules
109
112
  logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}`)
110
113
  const worktreeResult = await createWorktreeWithSubmodules({
111
114
  clientV2,
112
115
  directory: projectDirectory,
113
116
  name: worktreeName,
117
+ diff,
114
118
  })
115
119
 
116
120
  if (worktreeResult instanceof Error) {
@@ -123,10 +127,12 @@ async function createWorktreeInBackground({
123
127
 
124
128
  // Success - update database and edit starter message
125
129
  setWorktreeReady({ threadId: thread.id, worktreeDirectory: worktreeResult.directory })
130
+ const diffStatus = diff ? (worktreeResult.diffApplied ? '\n✅ Changes applied' : '\n⚠️ Failed to apply changes') : ''
126
131
  await starterMessage.edit(
127
132
  `🌳 **Worktree: ${worktreeName}**\n` +
128
133
  `📁 \`${worktreeResult.directory}\`\n` +
129
- `🌿 Branch: \`${worktreeResult.branch}\``
134
+ `🌿 Branch: \`${worktreeResult.branch}\`` +
135
+ diffStatus
130
136
  )
131
137
  }
132
138
 
@@ -337,6 +343,11 @@ async function handleWorktreeInThread({
337
343
  return
338
344
  }
339
345
 
346
+ // Capture git diff from project directory before creating worktree.
347
+ // This allows transferring uncommitted changes to the new worktree.
348
+ const diff = await captureGitDiff(projectDirectory)
349
+ const hasDiff = diff && (diff.staged || diff.unstaged)
350
+
340
351
  // Store pending worktree in database for this existing thread
341
352
  createPendingWorktree({
342
353
  threadId: thread.id,
@@ -345,20 +356,22 @@ async function handleWorktreeInThread({
345
356
  })
346
357
 
347
358
  // Send status message in thread
359
+ const diffNote = hasDiff ? '\n📋 Will transfer uncommitted changes' : ''
348
360
  const statusMessage = await thread.send({
349
- content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`,
361
+ content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...${diffNote}`,
350
362
  flags: SILENT_MESSAGE_FLAGS,
351
363
  })
352
364
 
353
365
  await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`)
354
366
 
355
- // Create worktree in background
367
+ // Create worktree in background, passing diff to apply after creation
356
368
  createWorktreeInBackground({
357
369
  thread,
358
370
  starterMessage: statusMessage,
359
371
  worktreeName,
360
372
  projectDirectory,
361
373
  clientV2,
374
+ diff,
362
375
  }).catch((e) => {
363
376
  logger.error('[NEW-WORKTREE] Background error:', e)
364
377
  })