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
@@ -1,15 +1,19 @@
1
1
  // Worktree utility functions.
2
2
  // Wrapper for OpenCode worktree creation that also initializes git submodules.
3
- import { exec } from 'node:child_process';
3
+ // Also handles capturing and applying git diffs when creating worktrees from threads.
4
+ import { exec, spawn } from 'node:child_process';
4
5
  import { promisify } from 'node:util';
5
- import { createLogger } from './logger.js';
6
+ import { createLogger, LogPrefix } from './logger.js';
6
7
  export const execAsync = promisify(exec);
7
- const logger = createLogger('WORKTREE-UTILS');
8
+ const logger = createLogger(LogPrefix.WORKTREE);
8
9
  /**
9
10
  * Create a worktree using OpenCode SDK and initialize git submodules.
10
11
  * This wrapper ensures submodules are properly set up in new worktrees.
12
+ *
13
+ * If diff is provided, it's applied BEFORE submodule update to ensure
14
+ * any submodule pointer changes in the diff are respected.
11
15
  */
12
- export async function createWorktreeWithSubmodules({ clientV2, directory, name, }) {
16
+ export async function createWorktreeWithSubmodules({ clientV2, directory, name, diff, }) {
13
17
  // 1. Create worktree via OpenCode SDK
14
18
  const response = await clientV2.worktree.create({
15
19
  directory,
@@ -22,7 +26,17 @@ export async function createWorktreeWithSubmodules({ clientV2, directory, name,
22
26
  return new Error('No worktree data returned from SDK');
23
27
  }
24
28
  const worktreeDir = response.data.directory;
25
- // 2. Init submodules in new worktree (don't block on failure)
29
+ let diffApplied = false;
30
+ // 2. Apply diff BEFORE submodule update (if provided)
31
+ // This ensures any submodule pointer changes in the diff are applied first,
32
+ // so submodule update checks out the correct commits.
33
+ if (diff) {
34
+ logger.log(`Applying diff to ${worktreeDir} before submodule init`);
35
+ diffApplied = await applyGitDiff(worktreeDir, diff);
36
+ }
37
+ // 3. Init submodules in new worktree (don't block on failure)
38
+ // Uses --init to initialize, --recursive for nested submodules.
39
+ // Submodules will be checked out at the commit specified by the (possibly updated) index.
26
40
  try {
27
41
  logger.log(`Initializing submodules in ${worktreeDir}`);
28
42
  await execAsync('git submodule update --init --recursive', {
@@ -34,7 +48,7 @@ export async function createWorktreeWithSubmodules({ clientV2, directory, name,
34
48
  // Log but don't fail - submodules might not exist
35
49
  logger.warn(`Failed to init submodules in ${worktreeDir}: ${e instanceof Error ? e.message : String(e)}`);
36
50
  }
37
- // 3. Install dependencies using ni (detects package manager from lockfile)
51
+ // 4. Install dependencies using ni (detects package manager from lockfile)
38
52
  try {
39
53
  logger.log(`Installing dependencies in ${worktreeDir}`);
40
54
  await execAsync('npx -y ni', {
@@ -46,5 +60,75 @@ export async function createWorktreeWithSubmodules({ clientV2, directory, name,
46
60
  // Log but don't fail - might not be a JS project or might fail for various reasons
47
61
  logger.warn(`Failed to install dependencies in ${worktreeDir}: ${e instanceof Error ? e.message : String(e)}`);
48
62
  }
49
- return response.data;
63
+ return { ...response.data, diffApplied };
64
+ }
65
+ /**
66
+ * Capture git diff from a directory (both staged and unstaged changes).
67
+ * Returns null if no changes or on error.
68
+ */
69
+ export async function captureGitDiff(directory) {
70
+ try {
71
+ // Capture unstaged changes
72
+ const unstagedResult = await execAsync('git diff', { cwd: directory });
73
+ const unstaged = unstagedResult.stdout.trim();
74
+ // Capture staged changes
75
+ const stagedResult = await execAsync('git diff --staged', { cwd: directory });
76
+ const staged = stagedResult.stdout.trim();
77
+ if (!unstaged && !staged) {
78
+ return null;
79
+ }
80
+ return { unstaged, staged };
81
+ }
82
+ catch (e) {
83
+ logger.warn(`Failed to capture git diff from ${directory}: ${e instanceof Error ? e.message : String(e)}`);
84
+ return null;
85
+ }
86
+ }
87
+ /**
88
+ * Run a git command with stdin input.
89
+ * Uses spawn to pipe the diff content to git apply.
90
+ */
91
+ function runGitWithStdin(args, cwd, input) {
92
+ return new Promise((resolve, reject) => {
93
+ const child = spawn('git', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
94
+ let stderr = '';
95
+ child.stderr?.on('data', (data) => {
96
+ stderr += data.toString();
97
+ });
98
+ child.on('close', (code) => {
99
+ if (code === 0) {
100
+ resolve();
101
+ }
102
+ else {
103
+ reject(new Error(stderr || `git ${args.join(' ')} failed with code ${code}`));
104
+ }
105
+ });
106
+ child.on('error', reject);
107
+ child.stdin?.write(input);
108
+ child.stdin?.end();
109
+ });
110
+ }
111
+ /**
112
+ * Apply a captured git diff to a directory.
113
+ * Applies staged changes first, then unstaged.
114
+ */
115
+ export async function applyGitDiff(directory, diff) {
116
+ try {
117
+ // Apply staged changes first (and stage them)
118
+ if (diff.staged) {
119
+ logger.log(`Applying staged diff to ${directory}`);
120
+ await runGitWithStdin(['apply', '--index'], directory, diff.staged);
121
+ }
122
+ // Apply unstaged changes (don't stage them)
123
+ if (diff.unstaged) {
124
+ logger.log(`Applying unstaged diff to ${directory}`);
125
+ await runGitWithStdin(['apply'], directory, diff.unstaged);
126
+ }
127
+ logger.log(`Successfully applied diff to ${directory}`);
128
+ return true;
129
+ }
130
+ catch (e) {
131
+ logger.warn(`Failed to apply git diff to ${directory}: ${e instanceof Error ? e.message : String(e)}`);
132
+ return false;
133
+ }
50
134
  }
package/dist/xml.js CHANGED
@@ -2,8 +2,8 @@
2
2
  // Parses XML-like tags from strings (e.g., channel topics) to extract
3
3
  // Kimaki configuration like directory paths and app IDs.
4
4
  import { DomHandler, Parser, ElementType } from 'htmlparser2';
5
- import { createLogger } from './logger.js';
6
- const xmlLogger = createLogger('XML');
5
+ import { createLogger, LogPrefix } from './logger.js';
6
+ const xmlLogger = createLogger(LogPrefix.XML);
7
7
  export function extractTagsArrays({ xml, tags, }) {
8
8
  const result = {
9
9
  others: [],
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.45",
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({
@@ -88,6 +88,9 @@ export async function handleCreateNewProjectCommand({
88
88
  reason: 'New project session',
89
89
  })
90
90
 
91
+ // Add user to thread so it appears in their sidebar
92
+ await thread.members.add(command.user.id)
93
+
91
94
  await handleOpencodeSession({
92
95
  prompt: 'The project was just initialized. Say hi and ask what the user wants to build.',
93
96
  thread,
@@ -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
@@ -215,6 +215,9 @@ export async function handleForkSelectMenu(
215
215
  reason: `Forked from session ${sessionId}`,
216
216
  })
217
217
 
218
+ // Add user to thread so it appears in their sidebar
219
+ await thread.members.add(interaction.user.id)
220
+
218
221
  getDatabase()
219
222
  .prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
220
223
  .run(thread.id, forkedSession.id)
@@ -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 })
@@ -73,6 +73,9 @@ export async function handleResumeCommand({ command, appId }: CommandContext): P
73
73
  reason: `Resuming session ${sessionId}`,
74
74
  })
75
75
 
76
+ // Add user to thread so it appears in their sidebar
77
+ await thread.members.add(command.user.id)
78
+
76
79
  getDatabase()
77
80
  .prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
78
81
  .run(thread.id, sessionId)
@@ -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 })
@@ -75,6 +75,9 @@ export async function handleSessionCommand({ command, appId }: CommandContext):
75
75
  reason: 'OpenCode session',
76
76
  })
77
77
 
78
+ // Add user to thread so it appears in their sidebar
79
+ await thread.members.add(command.user.id)
80
+
78
81
  await command.editReply(`Created new session in ${thread.toString()}`)
79
82
 
80
83
  await handleOpencodeSession({