kimaki 0.4.45 → 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 +5 -3
  8. package/dist/commands/fork.js +5 -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 +4 -2
  15. package/dist/commands/session.js +4 -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 +4 -2
  19. package/dist/commands/verbosity.js +3 -3
  20. package/dist/commands/worktree-settings.js +2 -2
  21. package/dist/commands/worktree.js +20 -8
  22. package/dist/database.js +2 -2
  23. package/dist/discord-bot.js +5 -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 +93 -15
  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 +6 -3
  48. package/src/commands/fork.ts +6 -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 +5 -2
  55. package/src/commands/session.ts +5 -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 +5 -2
  59. package/src/commands/verbosity.ts +3 -3
  60. package/src/commands/worktree-settings.ts +2 -2
  61. package/src/commands/worktree.ts +23 -7
  62. package/src/database.ts +2 -2
  63. package/src/discord-bot.ts +6 -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 +105 -15
  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/dist/cli.js CHANGED
@@ -10,13 +10,33 @@ import { Events, ChannelType, REST, Routes, SlashCommandBuilder, AttachmentBuild
10
10
  import path from 'node:path';
11
11
  import fs from 'node:fs';
12
12
  import * as errore from 'errore';
13
- import { createLogger } from './logger.js';
13
+ import { createLogger, LogPrefix } from './logger.js';
14
14
  import { uploadFilesToDiscord } from './discord-utils.js';
15
15
  import { spawn, spawnSync, execSync } from 'node:child_process';
16
16
  import http from 'node:http';
17
17
  import { setDataDir, getDataDir, getLockPort } from './config.js';
18
18
  import { sanitizeAgentName } from './commands/agent.js';
19
- const cliLogger = createLogger('CLI');
19
+ const cliLogger = createLogger(LogPrefix.CLI);
20
+ // Spawn caffeinate on macOS to prevent system sleep while bot is running.
21
+ // Not detached, so it dies automatically with the parent process.
22
+ function startCaffeinate() {
23
+ if (process.platform !== 'darwin') {
24
+ return;
25
+ }
26
+ try {
27
+ const proc = spawn('caffeinate', ['-i'], {
28
+ stdio: 'ignore',
29
+ detached: false,
30
+ });
31
+ proc.on('error', (err) => {
32
+ cliLogger.warn('Failed to start caffeinate:', err.message);
33
+ });
34
+ cliLogger.log('Started caffeinate to prevent system sleep');
35
+ }
36
+ catch (err) {
37
+ cliLogger.warn('Failed to spawn caffeinate:', err instanceof Error ? err.message : String(err));
38
+ }
39
+ }
20
40
  const cli = cac('kimaki');
21
41
  process.title = 'kimaki';
22
42
  async function killProcessOnPort(port) {
@@ -217,6 +237,10 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
217
237
  .setName('abort')
218
238
  .setDescription('Abort the current OpenCode request in this thread')
219
239
  .toJSON(),
240
+ new SlashCommandBuilder()
241
+ .setName('compact')
242
+ .setDescription('Compact the session context by summarizing conversation history')
243
+ .toJSON(),
220
244
  new SlashCommandBuilder()
221
245
  .setName('stop')
222
246
  .setDescription('Abort the current OpenCode request in this thread')
@@ -389,6 +413,7 @@ async function backgroundInit({ currentDir, token, appId, }) {
389
413
  }
390
414
  }
391
415
  async function run({ restart, addChannels, useWorktrees }) {
416
+ startCaffeinate();
392
417
  const forceSetup = Boolean(restart);
393
418
  intro('🤖 Discord Bot Setup');
394
419
  // Step 0: Check if OpenCode CLI is available
@@ -4,9 +4,9 @@ import { getDatabase } from '../database.js';
4
4
  import { initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
6
6
  import { abortControllers } from '../session-handler.js';
7
- import { createLogger } from '../logger.js';
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
8
  import * as errore from 'errore';
9
- const logger = createLogger('ABORT');
9
+ const logger = createLogger(LogPrefix.ABORT);
10
10
  export async function handleAbortCommand({ command }) {
11
11
  const channel = command.channel;
12
12
  if (!channel) {
@@ -4,10 +4,10 @@ import path from 'node:path';
4
4
  import { getDatabase } from '../database.js';
5
5
  import { initializeOpencodeForDirectory } from '../opencode.js';
6
6
  import { createProjectChannels } from '../channel-management.js';
7
- import { createLogger } from '../logger.js';
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
8
  import { abbreviatePath } from '../utils.js';
9
9
  import * as errore from 'errore';
10
- const logger = createLogger('ADD-PROJECT');
10
+ const logger = createLogger(LogPrefix.ADD_PROJECT);
11
11
  export async function handleAddProjectCommand({ command, appId }) {
12
12
  await command.deferReply({ ephemeral: false });
13
13
  const projectId = command.options.getString('project', true);
@@ -5,9 +5,9 @@ import crypto from 'node:crypto';
5
5
  import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runModelMigrations } from '../database.js';
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js';
7
7
  import { resolveTextChannel, getKimakiMetadata } 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
- const agentLogger = createLogger('AGENT');
10
+ const agentLogger = createLogger(LogPrefix.AGENT);
11
11
  const pendingAgentContexts = new Map();
12
12
  /**
13
13
  * Sanitize an agent name to be a valid Discord command name component.
@@ -181,7 +181,7 @@ export async function handleAgentSelectMenu(interaction) {
181
181
  }
182
182
  else {
183
183
  await interaction.editReply({
184
- content: `Agent preference set for this channel: **${selectedAgent}**\n\nAll new sessions in this channel will use this agent.`,
184
+ content: `Agent preference set for this channel: **${selectedAgent}**\nAll new sessions in this channel will use this agent.`,
185
185
  components: [],
186
186
  });
187
187
  }
@@ -237,7 +237,7 @@ export async function handleQuickAgentCommand({ command, appId, }) {
237
237
  }
238
238
  else {
239
239
  await command.editReply({
240
- content: `Switched to **${matchingAgent.name}** agent for this channel\n\nAll new sessions will use this agent.`,
240
+ content: `Switched to **${matchingAgent.name}** agent for this channel\nAll new sessions will use this agent.`,
241
241
  });
242
242
  }
243
243
  }
@@ -5,8 +5,8 @@ import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder,
5
5
  import crypto from 'node:crypto';
6
6
  import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
7
7
  import { getOpencodeClientV2 } from '../opencode.js';
8
- import { createLogger } from '../logger.js';
9
- const logger = createLogger('ASK_QUESTION');
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ const logger = createLogger(LogPrefix.ASK_QUESTION);
10
10
  // Store pending question contexts by hash
11
11
  export const pendingQuestionContexts = new Map();
12
12
  /**
@@ -178,9 +178,9 @@ export function parseAskUserQuestionTool(part) {
178
178
  }
179
179
  /**
180
180
  * Cancel a pending question for a thread (e.g., when user sends a new message).
181
- * Sends cancellation response to OpenCode so the session can continue.
181
+ * Sends the user's message as the answer to OpenCode so the model sees their actual response.
182
182
  */
183
- export async function cancelPendingQuestion(threadId) {
183
+ export async function cancelPendingQuestion(threadId, userMessage) {
184
184
  // Find pending question for this thread
185
185
  let contextHash;
186
186
  let context;
@@ -199,18 +199,19 @@ export async function cancelPendingQuestion(threadId) {
199
199
  if (!clientV2) {
200
200
  throw new Error('OpenCode server not found for directory');
201
201
  }
202
- // Preserve already-answered questions, mark unanswered as cancelled
202
+ // Use user's message as answer if provided, otherwise mark as "Other"
203
+ const customAnswer = userMessage || 'Other';
203
204
  const answers = context.questions.map((_, i) => {
204
- return context.answers[i] || ['(cancelled - user sent new message)'];
205
+ return context.answers[i] || [customAnswer];
205
206
  });
206
207
  await clientV2.question.reply({
207
208
  requestID: context.requestId,
208
209
  answers,
209
210
  });
210
- logger.log(`Cancelled question ${context.requestId} due to new user message`);
211
+ logger.log(`Answered question ${context.requestId} with user message`);
211
212
  }
212
213
  catch (error) {
213
- logger.error('Failed to cancel question:', error);
214
+ logger.error('Failed to answer question:', error);
214
215
  }
215
216
  // Clean up regardless of whether the API call succeeded
216
217
  pendingQuestionContexts.delete(contextHash);
@@ -0,0 +1,126 @@
1
+ // /compact command - Trigger context compaction (summarization) for the current session.
2
+ import { ChannelType } from 'discord.js';
3
+ import { getDatabase } from '../database.js';
4
+ import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js';
5
+ import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.COMPACT);
8
+ export async function handleCompactCommand({ command }) {
9
+ const channel = command.channel;
10
+ if (!channel) {
11
+ await command.reply({
12
+ content: 'This command can only be used in a channel',
13
+ ephemeral: true,
14
+ flags: SILENT_MESSAGE_FLAGS,
15
+ });
16
+ return;
17
+ }
18
+ const isThread = [
19
+ ChannelType.PublicThread,
20
+ ChannelType.PrivateThread,
21
+ ChannelType.AnnouncementThread,
22
+ ].includes(channel.type);
23
+ if (!isThread) {
24
+ await command.reply({
25
+ content: 'This command can only be used in a thread with an active session',
26
+ ephemeral: true,
27
+ flags: SILENT_MESSAGE_FLAGS,
28
+ });
29
+ return;
30
+ }
31
+ const textChannel = await resolveTextChannel(channel);
32
+ const { projectDirectory: directory } = getKimakiMetadata(textChannel);
33
+ if (!directory) {
34
+ await command.reply({
35
+ content: 'Could not determine project directory for this channel',
36
+ ephemeral: true,
37
+ flags: SILENT_MESSAGE_FLAGS,
38
+ });
39
+ return;
40
+ }
41
+ const row = getDatabase()
42
+ .prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
43
+ .get(channel.id);
44
+ if (!row?.session_id) {
45
+ await command.reply({
46
+ content: 'No active session in this thread',
47
+ ephemeral: true,
48
+ flags: SILENT_MESSAGE_FLAGS,
49
+ });
50
+ return;
51
+ }
52
+ const sessionId = row.session_id;
53
+ // Ensure server is running for this directory
54
+ const getClient = await initializeOpencodeForDirectory(directory);
55
+ if (getClient instanceof Error) {
56
+ await command.reply({
57
+ content: `Failed to compact: ${getClient.message}`,
58
+ ephemeral: true,
59
+ flags: SILENT_MESSAGE_FLAGS,
60
+ });
61
+ return;
62
+ }
63
+ const clientV2 = getOpencodeClientV2(directory);
64
+ if (!clientV2) {
65
+ await command.reply({
66
+ content: 'Failed to get OpenCode client',
67
+ ephemeral: true,
68
+ flags: SILENT_MESSAGE_FLAGS,
69
+ });
70
+ return;
71
+ }
72
+ // Defer reply since compaction may take a moment
73
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
74
+ try {
75
+ // Get session messages to find the model from the last user message
76
+ const messagesResult = await clientV2.session.messages({
77
+ sessionID: sessionId,
78
+ directory,
79
+ });
80
+ if (messagesResult.error || !messagesResult.data) {
81
+ logger.error('[COMPACT] Failed to get messages:', messagesResult.error);
82
+ await command.editReply({
83
+ content: 'Failed to compact: Could not retrieve session messages',
84
+ });
85
+ return;
86
+ }
87
+ // Find the last user message to get the model
88
+ const lastUserMessage = [...messagesResult.data]
89
+ .reverse()
90
+ .find((msg) => msg.info.role === 'user');
91
+ if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
92
+ await command.editReply({
93
+ content: 'Failed to compact: No user message found in session',
94
+ });
95
+ return;
96
+ }
97
+ const { providerID, modelID } = lastUserMessage.info.model;
98
+ const result = await clientV2.session.summarize({
99
+ sessionID: sessionId,
100
+ directory,
101
+ providerID,
102
+ modelID,
103
+ auto: false,
104
+ });
105
+ if (result.error) {
106
+ logger.error('[COMPACT] Error:', result.error);
107
+ const errorMessage = 'data' in result.error && result.error.data
108
+ ? result.error.data.message || 'Unknown error'
109
+ : 'Unknown error';
110
+ await command.editReply({
111
+ content: `Failed to compact: ${errorMessage}`,
112
+ });
113
+ return;
114
+ }
115
+ await command.editReply({
116
+ content: `📦 Session **compacted** successfully`,
117
+ });
118
+ logger.log(`Session ${sessionId} compacted by user`);
119
+ }
120
+ catch (error) {
121
+ logger.error('[COMPACT] Error:', error);
122
+ await command.editReply({
123
+ content: `Failed to compact: ${error instanceof Error ? error.message : 'Unknown error'}`,
124
+ });
125
+ }
126
+ }
@@ -6,8 +6,8 @@ import { getProjectsDir } from '../config.js';
6
6
  import { createProjectChannels } from '../channel-management.js';
7
7
  import { handleOpencodeSession } from '../session-handler.js';
8
8
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
9
- import { createLogger } from '../logger.js';
10
- const logger = createLogger('CREATE-NEW-PROJECT');
9
+ import { createLogger, LogPrefix } from '../logger.js';
10
+ const logger = createLogger(LogPrefix.CREATE_PROJECT);
11
11
  export async function handleCreateNewProjectCommand({ command, appId, }) {
12
12
  await command.deferReply({ ephemeral: false });
13
13
  const projectName = command.options.getString('name', true);
@@ -53,7 +53,7 @@ export async function handleCreateNewProjectCommand({ command, appId, }) {
53
53
  appId,
54
54
  });
55
55
  const textChannel = (await guild.channels.fetch(textChannelId));
56
- await command.editReply(`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n\n_Starting session..._`);
56
+ await command.editReply(`✅ Created new project **${sanitizedName}**\n📁 Directory: \`${projectDirectory}\`\n📝 Text: <#${textChannelId}>\n🔊 Voice: <#${voiceChannelId}>\n_Starting session..._`);
57
57
  const starterMessage = await textChannel.send({
58
58
  content: `🚀 **New project initialized**\n📁 \`${projectDirectory}\``,
59
59
  flags: SILENT_MESSAGE_FLAGS,
@@ -63,6 +63,8 @@ export async function handleCreateNewProjectCommand({ command, appId, }) {
63
63
  autoArchiveDuration: 1440,
64
64
  reason: 'New project session',
65
65
  });
66
+ // Add user to thread so it appears in their sidebar
67
+ await thread.members.add(command.user.id);
66
68
  await handleOpencodeSession({
67
69
  prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
68
70
  thread,
@@ -4,10 +4,10 @@ import { getDatabase } from '../database.js';
4
4
  import { initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveTextChannel, getKimakiMetadata, sendThreadMessage } from '../discord-utils.js';
6
6
  import { collectLastAssistantParts } from '../message-formatting.js';
7
- import { createLogger } from '../logger.js';
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
8
  import * as errore from 'errore';
9
- const sessionLogger = createLogger('SESSION');
10
- const forkLogger = createLogger('FORK');
9
+ const sessionLogger = createLogger(LogPrefix.SESSION);
10
+ const forkLogger = createLogger(LogPrefix.FORK);
11
11
  export async function handleForkCommand(interaction) {
12
12
  const channel = interaction.channel;
13
13
  if (!channel) {
@@ -162,6 +162,8 @@ export async function handleForkSelectMenu(interaction) {
162
162
  autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
163
163
  reason: `Forked from session ${sessionId}`,
164
164
  });
165
+ // Add user to thread so it appears in their sidebar
166
+ await thread.members.add(interaction.user.id);
165
167
  getDatabase()
166
168
  .prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
167
169
  .run(thread.id, forkedSession.id);
@@ -3,9 +3,9 @@
3
3
  // After merge, switches to detached HEAD at main so user can keep working.
4
4
  import {} from 'discord.js';
5
5
  import { getThreadWorktree } from '../database.js';
6
- import { createLogger } from '../logger.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
7
  import { execAsync } from '../worktree-utils.js';
8
- const logger = createLogger('MERGE-WORKTREE');
8
+ const logger = createLogger(LogPrefix.WORKTREE);
9
9
  /** Worktree thread title prefix - indicates unmerged worktree */
10
10
  export const WORKTREE_PREFIX = '⬦ ';
11
11
  /**
@@ -5,9 +5,9 @@ import { getDatabase, setChannelModel, setSessionModel, runModelMigrations } fro
5
5
  import { initializeOpencodeForDirectory } from '../opencode.js';
6
6
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
7
7
  import { abortAndRetrySession } from '../session-handler.js';
8
- import { createLogger } from '../logger.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
9
  import * as errore from 'errore';
10
- const modelLogger = createLogger('MODEL');
10
+ const modelLogger = createLogger(LogPrefix.MODEL);
11
11
  // Store context by hash to avoid customId length limits (Discord max: 100 chars)
12
12
  const pendingModelContexts = new Map();
13
13
  /**
@@ -292,13 +292,13 @@ export async function handleModelSelectMenu(interaction) {
292
292
  }
293
293
  if (retried) {
294
294
  await interaction.editReply({
295
- content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\`\n\n_Retrying current request with new model..._`,
295
+ content: `Model changed for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\n_Retrying current request with new model..._`,
296
296
  components: [],
297
297
  });
298
298
  }
299
299
  else {
300
300
  await interaction.editReply({
301
- content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n\n\`${fullModelId}\``,
301
+ content: `Model preference set for this session:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\``,
302
302
  components: [],
303
303
  });
304
304
  }
@@ -308,7 +308,7 @@ export async function handleModelSelectMenu(interaction) {
308
308
  setChannelModel(context.channelId, fullModelId);
309
309
  modelLogger.log(`Set model ${fullModelId} for channel ${context.channelId}`);
310
310
  await interaction.editReply({
311
- 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.`,
311
+ content: `Model preference set for this channel:\n**${context.providerName}** / **${selectedModelId}**\n\`${fullModelId}\`\nAll new sessions in this channel will use this model.`,
312
312
  components: [],
313
313
  });
314
314
  }
@@ -5,8 +5,8 @@ import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder,
5
5
  import crypto from 'node:crypto';
6
6
  import { getOpencodeClientV2 } from '../opencode.js';
7
7
  import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
8
- import { createLogger } from '../logger.js';
9
- const logger = createLogger('PERMISSIONS');
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
+ const logger = createLogger(LogPrefix.PERMISSIONS);
10
10
  // Store pending permission contexts by hash
11
11
  export const pendingPermissionContexts = new Map();
12
12
  /**
@@ -3,8 +3,8 @@ import { ChannelType } from 'discord.js';
3
3
  import { getDatabase } from '../database.js';
4
4
  import { resolveTextChannel, getKimakiMetadata, sendThreadMessage, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
5
5
  import { handleOpencodeSession, abortControllers, addToQueue, getQueueLength, clearQueue, } from '../session-handler.js';
6
- import { createLogger } from '../logger.js';
7
- const logger = createLogger('QUEUE');
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.QUEUE);
8
8
  export async function handleQueueCommand({ command }) {
9
9
  const message = command.options.getString('message', true);
10
10
  const channel = command.channel;
@@ -2,9 +2,9 @@
2
2
  import path from 'node:path';
3
3
  import * as errore from 'errore';
4
4
  import { getDatabase } from '../database.js';
5
- import { createLogger } from '../logger.js';
5
+ import { createLogger, LogPrefix } from '../logger.js';
6
6
  import { abbreviatePath } from '../utils.js';
7
- const logger = createLogger('REMOVE-PROJECT');
7
+ const logger = createLogger(LogPrefix.REMOVE_PROJECT);
8
8
  export async function handleRemoveProjectCommand({ command, appId }) {
9
9
  await command.deferReply({ ephemeral: false });
10
10
  const directory = command.options.getString('project', true);
@@ -5,9 +5,9 @@ import { getDatabase, getChannelDirectory } from '../database.js';
5
5
  import { initializeOpencodeForDirectory } from '../opencode.js';
6
6
  import { sendThreadMessage, resolveTextChannel } from '../discord-utils.js';
7
7
  import { collectLastAssistantParts } from '../message-formatting.js';
8
- import { createLogger } from '../logger.js';
8
+ import { createLogger, LogPrefix } from '../logger.js';
9
9
  import * as errore from 'errore';
10
- const logger = createLogger('RESUME');
10
+ const logger = createLogger(LogPrefix.RESUME);
11
11
  export async function handleResumeCommand({ command, appId }) {
12
12
  await command.deferReply({ ephemeral: false });
13
13
  const sessionId = command.options.getString('session', true);
@@ -51,6 +51,8 @@ export async function handleResumeCommand({ command, appId }) {
51
51
  autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
52
52
  reason: `Resuming session ${sessionId}`,
53
53
  });
54
+ // Add user to thread so it appears in their sidebar
55
+ await thread.members.add(command.user.id);
54
56
  getDatabase()
55
57
  .prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
56
58
  .run(thread.id, sessionId);
@@ -6,9 +6,9 @@ import { getDatabase, getChannelDirectory } from '../database.js';
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js';
7
7
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
8
8
  import { handleOpencodeSession } 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
- const logger = createLogger('SESSION');
11
+ const logger = createLogger(LogPrefix.SESSION);
12
12
  export async function handleSessionCommand({ command, appId }) {
13
13
  await command.deferReply({ ephemeral: false });
14
14
  const prompt = command.options.getString('prompt', true);
@@ -58,6 +58,8 @@ export async function handleSessionCommand({ command, appId }) {
58
58
  autoArchiveDuration: 1440,
59
59
  reason: 'OpenCode session',
60
60
  });
61
+ // Add user to thread so it appears in their sidebar
62
+ await thread.members.add(command.user.id);
61
63
  await command.editReply(`Created new session in ${thread.toString()}`);
62
64
  await handleOpencodeSession({
63
65
  prompt: fullPrompt,
@@ -3,9 +3,9 @@ import { ChannelType } from 'discord.js';
3
3
  import { getDatabase } from '../database.js';
4
4
  import { initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
6
- import { createLogger } from '../logger.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
7
  import * as errore from 'errore';
8
- const logger = createLogger('SHARE');
8
+ const logger = createLogger(LogPrefix.SHARE);
9
9
  export async function handleShareCommand({ command }) {
10
10
  const channel = command.channel;
11
11
  if (!channel) {
@@ -3,9 +3,9 @@ import { ChannelType } from 'discord.js';
3
3
  import { getDatabase } from '../database.js';
4
4
  import { initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveTextChannel, getKimakiMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
6
- import { createLogger } from '../logger.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
7
  import * as errore from 'errore';
8
- const logger = createLogger('UNDO-REDO');
8
+ const logger = createLogger(LogPrefix.UNDO_REDO);
9
9
  export async function handleUndoCommand({ command }) {
10
10
  const channel = command.channel;
11
11
  if (!channel) {
@@ -3,10 +3,10 @@
3
3
  import { ChannelType } from 'discord.js';
4
4
  import { handleOpencodeSession } from '../session-handler.js';
5
5
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
6
- import { createLogger } from '../logger.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
7
  import { getDatabase, getChannelDirectory } from '../database.js';
8
8
  import fs from 'node:fs';
9
- const userCommandLogger = createLogger('USER_CMD');
9
+ const userCommandLogger = createLogger(LogPrefix.USER_CMD);
10
10
  export const handleUserCommand = async ({ command, appId }) => {
11
11
  const discordCommandName = command.commandName;
12
12
  // Strip the -cmd suffix to get the actual OpenCode command name
@@ -105,6 +105,8 @@ export const handleUserCommand = async ({ command, appId }) => {
105
105
  autoArchiveDuration: 1440,
106
106
  reason: `OpenCode command: ${commandName}`,
107
107
  });
108
+ // Add user to thread so it appears in their sidebar
109
+ await newThread.members.add(command.user.id);
108
110
  await command.editReply(`Started /${commandName} in ${newThread.toString()}`);
109
111
  await handleOpencodeSession({
110
112
  prompt: '', // Not used when command is set
@@ -4,8 +4,8 @@
4
4
  // 'text-only': only shows text responses (⬥ diamond parts)
5
5
  import { ChatInputCommandInteraction, ChannelType } from 'discord.js';
6
6
  import { getChannelVerbosity, setChannelVerbosity } from '../database.js';
7
- import { createLogger } from '../logger.js';
8
- const verbosityLogger = createLogger('VERBOSITY');
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
+ const verbosityLogger = createLogger(LogPrefix.VERBOSITY);
9
9
  /**
10
10
  * Handle the /verbosity slash command.
11
11
  * Sets output verbosity for the channel (applies to new sessions).
@@ -47,7 +47,7 @@ export async function handleVerbosityCommand({ command, appId, }) {
47
47
  ? 'Only text responses will be shown. Tool executions, status messages, and thinking will be hidden.'
48
48
  : 'All output will be shown, including tool executions and status messages.';
49
49
  await command.reply({
50
- content: `Verbosity set to **${level}**.\n\n${description}\n\nThis applies to all new sessions in this channel.`,
50
+ content: `Verbosity set to **${level}**.\n${description}\nThis applies to all new sessions in this channel.`,
51
51
  ephemeral: true,
52
52
  });
53
53
  }
@@ -4,8 +4,8 @@
4
4
  import { ChatInputCommandInteraction, ChannelType } from 'discord.js';
5
5
  import { getChannelWorktreesEnabled, setChannelWorktreesEnabled } from '../database.js';
6
6
  import { getKimakiMetadata } from '../discord-utils.js';
7
- import { createLogger } from '../logger.js';
8
- const worktreeSettingsLogger = createLogger('WORKTREE_SETTINGS');
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
+ const worktreeSettingsLogger = createLogger(LogPrefix.WORKTREE);
9
9
  /**
10
10
  * Handle the /enable-worktrees slash command.
11
11
  * Enables automatic worktree creation for new sessions in this channel.
@@ -6,11 +6,11 @@ import fs from 'node:fs';
6
6
  import { createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelDirectory, getThreadWorktree, } from '../database.js';
7
7
  import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js';
8
8
  import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
9
- import { createLogger } from '../logger.js';
10
- import { createWorktreeWithSubmodules } from '../worktree-utils.js';
9
+ import { createLogger, LogPrefix } from '../logger.js';
10
+ import { createWorktreeWithSubmodules, captureGitDiff } from '../worktree-utils.js';
11
11
  import { WORKTREE_PREFIX } from './merge-worktree.js';
12
12
  import * as errore from 'errore';
13
- const logger = createLogger('WORKTREE');
13
+ const logger = createLogger(LogPrefix.WORKTREE);
14
14
  class WorktreeError extends Error {
15
15
  constructor(message, options) {
16
16
  super(message, options);
@@ -69,14 +69,16 @@ function getProjectDirectoryFromChannel(channel, appId) {
69
69
  }
70
70
  /**
71
71
  * Create worktree in background and update starter message when done.
72
+ * If diff is provided, it's applied during worktree creation (before submodule init).
72
73
  */
73
- async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, clientV2, }) {
74
- // Create worktree using SDK v2 and init submodules
74
+ async function createWorktreeInBackground({ thread, starterMessage, worktreeName, projectDirectory, clientV2, diff, }) {
75
+ // Create worktree using SDK v2, apply diff, then init submodules
75
76
  logger.log(`Creating worktree "${worktreeName}" for project ${projectDirectory}`);
76
77
  const worktreeResult = await createWorktreeWithSubmodules({
77
78
  clientV2,
78
79
  directory: projectDirectory,
79
80
  name: worktreeName,
81
+ diff,
80
82
  });
81
83
  if (worktreeResult instanceof Error) {
82
84
  const errorMsg = worktreeResult.message;
@@ -87,9 +89,11 @@ async function createWorktreeInBackground({ thread, starterMessage, worktreeName
87
89
  }
88
90
  // Success - update database and edit starter message
89
91
  setWorktreeReady({ threadId: thread.id, worktreeDirectory: worktreeResult.directory });
92
+ const diffStatus = diff ? (worktreeResult.diffApplied ? '\n✅ Changes applied' : '\n⚠️ Failed to apply changes') : '';
90
93
  await starterMessage.edit(`🌳 **Worktree: ${worktreeName}**\n` +
91
94
  `📁 \`${worktreeResult.directory}\`\n` +
92
- `🌿 Branch: \`${worktreeResult.branch}\``);
95
+ `🌿 Branch: \`${worktreeResult.branch}\`` +
96
+ diffStatus);
93
97
  }
94
98
  export async function handleNewWorktreeCommand({ command, appId, }) {
95
99
  await command.deferReply({ ephemeral: false });
@@ -167,6 +171,8 @@ export async function handleNewWorktreeCommand({ command, appId, }) {
167
171
  autoArchiveDuration: 1440,
168
172
  reason: 'Worktree session',
169
173
  });
174
+ // Add user to thread so it appears in their sidebar
175
+ await thread.members.add(command.user.id);
170
176
  return { thread, starterMessage };
171
177
  },
172
178
  catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
@@ -251,6 +257,10 @@ async function handleWorktreeInThread({ command, appId, thread, }) {
251
257
  await command.editReply(`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``);
252
258
  return;
253
259
  }
260
+ // Capture git diff from project directory before creating worktree.
261
+ // This allows transferring uncommitted changes to the new worktree.
262
+ const diff = await captureGitDiff(projectDirectory);
263
+ const hasDiff = diff && (diff.staged || diff.unstaged);
254
264
  // Store pending worktree in database for this existing thread
255
265
  createPendingWorktree({
256
266
  threadId: thread.id,
@@ -258,18 +268,20 @@ async function handleWorktreeInThread({ command, appId, thread, }) {
258
268
  projectDirectory,
259
269
  });
260
270
  // Send status message in thread
271
+ const diffNote = hasDiff ? '\n📋 Will transfer uncommitted changes' : '';
261
272
  const statusMessage = await thread.send({
262
- content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`,
273
+ content: `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...${diffNote}`,
263
274
  flags: SILENT_MESSAGE_FLAGS,
264
275
  });
265
276
  await command.editReply(`Creating worktree \`${worktreeName}\` for this thread...`);
266
- // Create worktree in background
277
+ // Create worktree in background, passing diff to apply after creation
267
278
  createWorktreeInBackground({
268
279
  thread,
269
280
  starterMessage: statusMessage,
270
281
  worktreeName,
271
282
  projectDirectory,
272
283
  clientV2,
284
+ diff,
273
285
  }).catch((e) => {
274
286
  logger.error('[NEW-WORKTREE] Background error:', e);
275
287
  });