kimaki 0.4.46 → 0.4.48

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 (85) hide show
  1. package/dist/cli.js +69 -21
  2. package/dist/commands/abort.js +4 -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 +60 -30
  8. package/dist/commands/fork.js +3 -3
  9. package/dist/commands/merge-worktree.js +23 -10
  10. package/dist/commands/model.js +5 -5
  11. package/dist/commands/permissions.js +5 -3
  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 +6 -3
  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 +5 -5
  20. package/dist/commands/worktree-settings.js +2 -2
  21. package/dist/commands/worktree.js +18 -8
  22. package/dist/config.js +7 -0
  23. package/dist/database.js +10 -7
  24. package/dist/discord-bot.js +30 -12
  25. package/dist/discord-utils.js +2 -2
  26. package/dist/genai-worker-wrapper.js +3 -3
  27. package/dist/genai-worker.js +2 -2
  28. package/dist/genai.js +2 -2
  29. package/dist/interaction-handler.js +6 -2
  30. package/dist/logger.js +57 -9
  31. package/dist/markdown.js +2 -2
  32. package/dist/message-formatting.js +91 -6
  33. package/dist/openai-realtime.js +2 -2
  34. package/dist/opencode.js +19 -25
  35. package/dist/session-handler.js +89 -29
  36. package/dist/system-message.js +11 -9
  37. package/dist/tools.js +3 -2
  38. package/dist/utils.js +1 -0
  39. package/dist/voice-handler.js +2 -2
  40. package/dist/voice.js +2 -2
  41. package/dist/worktree-utils.js +91 -7
  42. package/dist/xml.js +2 -2
  43. package/package.json +3 -3
  44. package/src/cli.ts +108 -21
  45. package/src/commands/abort.ts +4 -2
  46. package/src/commands/add-project.ts +2 -2
  47. package/src/commands/agent.ts +4 -4
  48. package/src/commands/ask-question.ts +9 -8
  49. package/src/commands/compact.ts +148 -0
  50. package/src/commands/create-new-project.ts +87 -36
  51. package/src/commands/fork.ts +3 -3
  52. package/src/commands/merge-worktree.ts +47 -10
  53. package/src/commands/model.ts +5 -5
  54. package/src/commands/permissions.ts +6 -2
  55. package/src/commands/queue.ts +2 -2
  56. package/src/commands/remove-project.ts +2 -2
  57. package/src/commands/resume.ts +2 -2
  58. package/src/commands/session.ts +6 -3
  59. package/src/commands/share.ts +2 -2
  60. package/src/commands/undo-redo.ts +2 -2
  61. package/src/commands/user-command.ts +2 -2
  62. package/src/commands/verbosity.ts +5 -5
  63. package/src/commands/worktree-settings.ts +2 -2
  64. package/src/commands/worktree.ts +20 -7
  65. package/src/config.ts +14 -0
  66. package/src/database.ts +13 -7
  67. package/src/discord-bot.ts +45 -12
  68. package/src/discord-utils.ts +2 -2
  69. package/src/genai-worker-wrapper.ts +3 -3
  70. package/src/genai-worker.ts +2 -2
  71. package/src/genai.ts +2 -2
  72. package/src/interaction-handler.ts +7 -2
  73. package/src/logger.ts +64 -10
  74. package/src/markdown.ts +2 -2
  75. package/src/message-formatting.ts +100 -6
  76. package/src/openai-realtime.ts +2 -2
  77. package/src/opencode.ts +19 -26
  78. package/src/session-handler.ts +102 -29
  79. package/src/system-message.ts +11 -9
  80. package/src/tools.ts +3 -2
  81. package/src/utils.ts +1 -0
  82. package/src/voice-handler.ts +2 -2
  83. package/src/voice.ts +2 -2
  84. package/src/worktree-utils.ts +111 -7
  85. package/src/xml.ts +2 -2
@@ -111,19 +111,21 @@ headings are discouraged anyway. instead try to use bold text for titles which r
111
111
 
112
112
  you can create diagrams wrapping them in code blocks.
113
113
 
114
- ## ending conversations with options
114
+ ## proactivity
115
115
 
116
- IMPORTANT: At the end of each response, especially after completing a task or presenting a plan, use the question tool to offer the user clear options for what to do next.
116
+ Be proactive. When the user asks you to do something, do it. Do NOT stop to ask for confirmation.
117
117
 
118
- IMPORTANT: The question tool must be called last, after all text parts. If it is called before your final text response, the user will not see the text.
118
+ Only ask questions when the request is genuinely ambiguous with multiple valid approaches, or the action is destructive and irreversible.
119
119
 
120
- Examples:
121
- - After showing a plan: offer "Start implementing?" with Yes/No options
122
- - After completing edits: offer "Commit changes?" with Yes/No options
123
- - After debugging: offer "How to proceed?" with options like "Apply fix", "Investigate further", "Try different approach"
120
+ ## ending conversations with options
124
121
 
125
- The user can always select "Other" to type a custom response if the provided options don't fit their needs, or if the plan needs updating.
122
+ After **completing** a task, use the question tool to offer follow-up options. The question tool must be called last, after all text parts.
126
123
 
127
- This makes the interaction more guided and reduces friction for the user.
124
+ IMPORTANT: Do NOT use the question tool to ask permission before doing work. Do the work first, then offer follow-ups.
125
+
126
+ Examples:
127
+ - After completing edits: offer "Commit changes?" or "Run tests?"
128
+ - After debugging: offer "Apply fix", "Investigate further", "Try different approach"
129
+ - After a genuinely ambiguous request where you cannot infer intent: offer the different approaches
128
130
  `;
129
131
  }
package/dist/tools.js CHANGED
@@ -6,9 +6,9 @@ import { z } from 'zod';
6
6
  import { spawn } from 'node:child_process';
7
7
  import net from 'node:net';
8
8
  import { createOpencodeClient, } from '@opencode-ai/sdk';
9
- import { createLogger } from './logger.js';
9
+ import { createLogger, LogPrefix } from './logger.js';
10
10
  import * as errore from 'errore';
11
- const toolsLogger = createLogger('TOOLS');
11
+ const toolsLogger = createLogger(LogPrefix.TOOLS);
12
12
  import { ShareMarkdown } from './markdown.js';
13
13
  import { formatDistanceToNow } from './utils.js';
14
14
  import pc from 'picocolors';
@@ -289,6 +289,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
289
289
  }),
290
290
  execute: async ({ sessionId }) => {
291
291
  try {
292
+ toolsLogger.log(`[ABORT] reason=voice-tool sessionId=${sessionId} - user requested abort via voice assistant tool`);
292
293
  const result = await getClient().session.abort({
293
294
  path: { id: sessionId },
294
295
  });
package/dist/utils.js CHANGED
@@ -50,6 +50,7 @@ export function isAbortError(error, signal) {
50
50
  error.name === 'Aborterror' ||
51
51
  error.name === 'aborterror' ||
52
52
  error.name.toLowerCase() === 'aborterror' ||
53
+ error.name === 'MessageAbortedError' ||
53
54
  error.message?.includes('aborted') ||
54
55
  (signal?.aborted ?? false))) ||
55
56
  (error instanceof DOMException && error.name === 'AbortError'));
@@ -17,8 +17,8 @@ import { getDatabase } from './database.js';
17
17
  import { sendThreadMessage, escapeDiscordFormatting, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
18
18
  import { transcribeAudio } from './voice.js';
19
19
  import { FetchError } from './errors.js';
20
- import { createLogger } from './logger.js';
21
- const voiceLogger = createLogger('VOICE');
20
+ import { createLogger, LogPrefix } from './logger.js';
21
+ const voiceLogger = createLogger(LogPrefix.VOICE);
22
22
  export const voiceConnections = new Map();
23
23
  export function convertToMono16k(buffer) {
24
24
  const inputSampleRate = 48000;
package/dist/voice.js CHANGED
@@ -4,11 +4,11 @@
4
4
  // Uses errore for type-safe error handling.
5
5
  import { GoogleGenAI, Type } from '@google/genai';
6
6
  import * as errore from 'errore';
7
- import { createLogger } from './logger.js';
7
+ import { createLogger, LogPrefix } from './logger.js';
8
8
  import { glob } from 'glob';
9
9
  import { ripGrep } from 'ripgrep-js';
10
10
  import { ApiKeyMissingError, InvalidAudioFormatError, TranscriptionError, EmptyTranscriptionError, NoResponseContentError, NoToolResponseError, GrepSearchError, GlobSearchError, } from './errors.js';
11
- const voiceLogger = createLogger('VOICE');
11
+ const voiceLogger = createLogger(LogPrefix.VOICE);
12
12
  function runGrep({ pattern, directory }) {
13
13
  return errore.tryAsync({
14
14
  try: async () => {
@@ -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.46",
5
+ "version": "0.4.48",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
@@ -20,7 +20,7 @@
20
20
  "tsx": "^4.20.5"
21
21
  },
22
22
  "dependencies": {
23
- "@clack/prompts": "^0.11.0",
23
+ "@clack/prompts": "^1.0.0",
24
24
  "@discordjs/voice": "^0.19.0",
25
25
  "@google/genai": "^1.34.0",
26
26
  "@opencode-ai/sdk": "^1.1.31",
@@ -41,7 +41,7 @@
41
41
  "string-dedent": "^3.0.2",
42
42
  "undici": "^7.16.0",
43
43
  "zod": "^4.2.1",
44
- "errore": "^0.9.0"
44
+ "errore": "^0.10.0"
45
45
  },
46
46
  "optionalDependencies": {
47
47
  "@discordjs/opus": "^0.10.0",
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
- import { setDataDir, getDataDir, getLockPort } from './config.js'
50
+ import { setDataDir, getDataDir, getLockPort, setDefaultVerbosity } 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'
@@ -125,7 +145,11 @@ async function checkSingleInstance(): Promise<void> {
125
145
  setTimeout(resolve, 500)
126
146
  })
127
147
  }
128
- } catch {
148
+ } catch (error) {
149
+ cliLogger.debug(
150
+ 'Lock port check failed:',
151
+ error instanceof Error ? error.message : String(error),
152
+ )
129
153
  cliLogger.debug('No other kimaki instance detected on lock port')
130
154
  }
131
155
  }
@@ -301,6 +325,10 @@ async function registerCommands({
301
325
  .setName('abort')
302
326
  .setDescription('Abort the current OpenCode request in this thread')
303
327
  .toJSON(),
328
+ new SlashCommandBuilder()
329
+ .setName('compact')
330
+ .setDescription('Compact the session context by summarizing conversation history')
331
+ .toJSON(),
304
332
  new SlashCommandBuilder()
305
333
  .setName('stop')
306
334
  .setDescription('Abort the current OpenCode request in this thread')
@@ -527,11 +555,23 @@ async function backgroundInit({
527
555
  getClient()
528
556
  .command.list({ query: { directory: currentDir } })
529
557
  .then((r) => r.data || [])
530
- .catch(() => []),
558
+ .catch((error) => {
559
+ cliLogger.warn(
560
+ 'Failed to load user commands during background init:',
561
+ error instanceof Error ? error.message : String(error),
562
+ )
563
+ return []
564
+ }),
531
565
  getClient()
532
566
  .app.agents({ query: { directory: currentDir } })
533
567
  .then((r) => r.data || [])
534
- .catch(() => []),
568
+ .catch((error) => {
569
+ cliLogger.warn(
570
+ 'Failed to load agents during background init:',
571
+ error instanceof Error ? error.message : String(error),
572
+ )
573
+ return []
574
+ }),
535
575
  ])
536
576
 
537
577
  await registerCommands({ token, appId, userCommands, agents })
@@ -545,6 +585,8 @@ async function backgroundInit({
545
585
  }
546
586
 
547
587
  async function run({ restart, addChannels, useWorktrees }: CliOptions) {
588
+ startCaffeinate()
589
+
548
590
  const forceSetup = Boolean(restart)
549
591
 
550
592
  intro('🤖 Discord Bot Setup')
@@ -587,7 +629,11 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
587
629
  try {
588
630
  fs.accessSync(p, fs.constants.F_OK)
589
631
  return true
590
- } catch {
632
+ } catch (error) {
633
+ cliLogger.debug(
634
+ `OpenCode path not found at ${p}:`,
635
+ error instanceof Error ? error.message : String(error),
636
+ )
591
637
  return false
592
638
  }
593
639
  })
@@ -888,11 +934,23 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
888
934
  getClient()
889
935
  .command.list({ query: { directory: currentDir } })
890
936
  .then((r) => r.data || [])
891
- .catch(() => []),
937
+ .catch((error) => {
938
+ cliLogger.warn(
939
+ 'Failed to load user commands during setup:',
940
+ error instanceof Error ? error.message : String(error),
941
+ )
942
+ return []
943
+ }),
892
944
  getClient()
893
945
  .app.agents({ query: { directory: currentDir } })
894
946
  .then((r) => r.data || [])
895
- .catch(() => []),
947
+ .catch((error) => {
948
+ cliLogger.warn(
949
+ 'Failed to load agents during setup:',
950
+ error instanceof Error ? error.message : String(error),
951
+ )
952
+ return []
953
+ }),
896
954
  ])
897
955
 
898
956
  s.stop(`Found ${projects.length} OpenCode project(s)`)
@@ -1040,6 +1098,7 @@ cli
1040
1098
  .option('--data-dir <path>', 'Data directory for config and database (default: ~/.kimaki)')
1041
1099
  .option('--install-url', 'Print the bot install URL and exit')
1042
1100
  .option('--use-worktrees', 'Create git worktrees for all new sessions started from channel messages')
1101
+ .option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text or text-only)')
1043
1102
  .action(
1044
1103
  async (options: {
1045
1104
  restart?: boolean
@@ -1047,6 +1106,7 @@ cli
1047
1106
  dataDir?: string
1048
1107
  installUrl?: boolean
1049
1108
  useWorktrees?: boolean
1109
+ verbosity?: string
1050
1110
  }) => {
1051
1111
  try {
1052
1112
  // Set data directory early, before any database access
@@ -1055,6 +1115,15 @@ cli
1055
1115
  cliLogger.log(`Using data directory: ${getDataDir()}`)
1056
1116
  }
1057
1117
 
1118
+ if (options.verbosity) {
1119
+ if (options.verbosity !== 'tools-and-text' && options.verbosity !== 'text-only') {
1120
+ cliLogger.error(`Invalid verbosity level: ${options.verbosity}. Use "tools-and-text" or "text-only".`)
1121
+ process.exit(EXIT_NO_RESTART)
1122
+ }
1123
+ setDefaultVerbosity(options.verbosity)
1124
+ cliLogger.log(`Default verbosity: ${options.verbosity}`)
1125
+ }
1126
+
1058
1127
  if (options.installUrl) {
1059
1128
  const db = getDatabase()
1060
1129
  const existingBot = db
@@ -1214,8 +1283,11 @@ cli
1214
1283
  .prepare('SELECT app_id FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
1215
1284
  .get() as { app_id: string } | undefined
1216
1285
  appId = botRow?.app_id
1217
- } catch {
1218
- // Database might not exist in CI, that's ok
1286
+ } catch (error) {
1287
+ cliLogger.debug(
1288
+ 'Database lookup failed while resolving app ID:',
1289
+ error instanceof Error ? error.message : String(error),
1290
+ )
1219
1291
  }
1220
1292
  }
1221
1293
  } else {
@@ -1330,8 +1402,11 @@ cli
1330
1402
  if (ch && 'guild' in ch && ch.guild) {
1331
1403
  return ch.guild
1332
1404
  }
1333
- } catch {
1334
- // Channel might be deleted, continue
1405
+ } catch (error) {
1406
+ cliLogger.debug(
1407
+ 'Failed to fetch existing channel while selecting guild:',
1408
+ error instanceof Error ? error.message : String(error),
1409
+ )
1335
1410
  }
1336
1411
  }
1337
1412
  // Fall back to first guild the bot is in
@@ -1571,8 +1646,11 @@ cli
1571
1646
  .prepare('SELECT app_id FROM bot_tokens ORDER BY created_at DESC LIMIT 1')
1572
1647
  .get() as { app_id: string } | undefined
1573
1648
  appId = botRow?.app_id
1574
- } catch {
1575
- // Database might not exist in CI
1649
+ } catch (error) {
1650
+ cliLogger.debug(
1651
+ 'Database lookup failed while resolving app ID:',
1652
+ error instanceof Error ? error.message : String(error),
1653
+ )
1576
1654
  }
1577
1655
  }
1578
1656
  } else {
@@ -1651,8 +1729,11 @@ cli
1651
1729
  } else {
1652
1730
  throw new Error('Channel has no guild')
1653
1731
  }
1654
- } catch {
1655
- // Channel might be deleted, fall back to first guild
1732
+ } catch (error) {
1733
+ cliLogger.debug(
1734
+ 'Failed to fetch existing channel while selecting guild:',
1735
+ error instanceof Error ? error.message : String(error),
1736
+ )
1656
1737
  const firstGuild = client.guilds.cache.first()
1657
1738
  if (!firstGuild) {
1658
1739
  s.stop('No guild found')
@@ -1696,12 +1777,18 @@ cli
1696
1777
  client.destroy()
1697
1778
  process.exit(0)
1698
1779
  }
1699
- } catch {
1700
- // Channel might be deleted, continue checking
1780
+ } catch (error) {
1781
+ cliLogger.debug(
1782
+ `Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`,
1783
+ error instanceof Error ? error.message : String(error),
1784
+ )
1701
1785
  }
1702
1786
  }
1703
- } catch {
1704
- // Database might not exist, continue to create
1787
+ } catch (error) {
1788
+ cliLogger.debug(
1789
+ 'Database lookup failed while checking existing channels:',
1790
+ error instanceof Error ? error.message : String(error),
1791
+ )
1705
1792
  }
1706
1793
 
1707
1794
  s.message(`Creating channels in ${guild.name}...`)
@@ -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
@@ -67,6 +67,7 @@ export async function handleAbortCommand({ command }: CommandContext): Promise<v
67
67
 
68
68
  const existingController = abortControllers.get(sessionId)
69
69
  if (existingController) {
70
+ logger.log(`[ABORT] reason=user-requested sessionId=${sessionId} channelId=${channel.id} - user ran /abort command`)
70
71
  existingController.abort(new Error('User requested abort'))
71
72
  abortControllers.delete(sessionId)
72
73
  }
@@ -82,6 +83,7 @@ export async function handleAbortCommand({ command }: CommandContext): Promise<v
82
83
  }
83
84
 
84
85
  try {
86
+ logger.log(`[ABORT-API] reason=user-requested sessionId=${sessionId} channelId=${channel.id} - sending API abort from /abort command`)
85
87
  await getClient().session.abort({
86
88
  path: { id: sessionId },
87
89
  })
@@ -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