kimaki 0.4.63 → 0.4.65

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 (123) hide show
  1. package/dist/bin.js +2 -1
  2. package/dist/cli-parsing.test.js +7 -4
  3. package/dist/cli.js +92 -39
  4. package/dist/commands/abort.js +10 -14
  5. package/dist/commands/agent.js +9 -5
  6. package/dist/commands/ask-question.js +3 -3
  7. package/dist/commands/compact.js +10 -14
  8. package/dist/commands/context-usage.js +10 -15
  9. package/dist/commands/diff.js +7 -8
  10. package/dist/commands/file-upload.js +5 -7
  11. package/dist/commands/fork.js +27 -17
  12. package/dist/commands/gemini-apikey.js +3 -3
  13. package/dist/commands/login.js +3 -3
  14. package/dist/commands/mention-mode.js +5 -5
  15. package/dist/commands/merge-worktree.js +2 -2
  16. package/dist/commands/model.js +169 -74
  17. package/dist/commands/queue.js +16 -30
  18. package/dist/commands/remove-project.js +3 -3
  19. package/dist/commands/restart-opencode-server.js +61 -13
  20. package/dist/commands/resume.js +1 -1
  21. package/dist/commands/run-command.js +9 -12
  22. package/dist/commands/share.js +11 -16
  23. package/dist/commands/thinking.js +128 -0
  24. package/dist/commands/undo-redo.js +15 -19
  25. package/dist/commands/unset-model.js +7 -7
  26. package/dist/commands/user-command.js +7 -7
  27. package/dist/commands/verbosity.js +4 -4
  28. package/dist/commands/worktree-settings.js +5 -5
  29. package/dist/commands/worktree.js +16 -5
  30. package/dist/config.js +9 -0
  31. package/dist/database.js +55 -18
  32. package/dist/db.js +34 -1
  33. package/dist/db.test.js +11 -11
  34. package/dist/discord-bot.js +35 -16
  35. package/dist/discord-utils.js +8 -5
  36. package/dist/format-tables.test.js +1 -1
  37. package/dist/generated/internal/class.js +2 -2
  38. package/dist/generated/internal/prismaNamespace.js +3 -0
  39. package/dist/generated/internal/prismaNamespaceBrowser.js +3 -0
  40. package/dist/generated/models/session_thinking.js +1 -0
  41. package/dist/interaction-handler.js +13 -8
  42. package/dist/logger.js +1 -0
  43. package/dist/opencode-plugin.js +3 -7
  44. package/dist/opencode.js +1 -1
  45. package/dist/session-handler.js +121 -60
  46. package/dist/system-message.js +59 -33
  47. package/dist/thinking-utils.js +35 -0
  48. package/dist/thinking-utils.test.js +48 -0
  49. package/dist/unnest-code-blocks.js +36 -20
  50. package/dist/unnest-code-blocks.test.js +184 -1
  51. package/dist/wait-session.js +10 -6
  52. package/dist/worktree-utils.js +45 -21
  53. package/package.json +1 -1
  54. package/schema.prisma +3 -0
  55. package/src/__snapshots__/compact-session-context-no-system.md +2 -0
  56. package/src/__snapshots__/compact-session-context.md +2 -0
  57. package/src/__snapshots__/first-session-no-info.md +500 -498
  58. package/src/__snapshots__/first-session-with-info.md +500 -498
  59. package/src/__snapshots__/session-1.md +500 -498
  60. package/src/__snapshots__/session-2.md +1 -3
  61. package/src/__snapshots__/session-3.md +583 -574
  62. package/src/__snapshots__/session-with-tools.md +500 -498
  63. package/src/ai-tool-to-genai.ts +1 -4
  64. package/src/ai-tool.ts +1 -4
  65. package/src/bin.ts +4 -1
  66. package/src/cli-parsing.test.ts +7 -4
  67. package/src/cli.ts +524 -417
  68. package/src/commands/abort.ts +16 -16
  69. package/src/commands/agent.ts +19 -5
  70. package/src/commands/ask-question.ts +7 -3
  71. package/src/commands/compact.ts +14 -17
  72. package/src/commands/context-usage.ts +21 -24
  73. package/src/commands/diff.ts +13 -9
  74. package/src/commands/file-upload.ts +5 -6
  75. package/src/commands/fork.ts +54 -32
  76. package/src/commands/gemini-apikey.ts +4 -5
  77. package/src/commands/login.ts +4 -5
  78. package/src/commands/mention-mode.ts +10 -5
  79. package/src/commands/merge-worktree.ts +18 -4
  80. package/src/commands/model.ts +202 -72
  81. package/src/commands/queue.ts +21 -31
  82. package/src/commands/remove-project.ts +19 -6
  83. package/src/commands/restart-opencode-server.ts +70 -13
  84. package/src/commands/resume.ts +6 -1
  85. package/src/commands/run-command.ts +22 -13
  86. package/src/commands/share.ts +11 -16
  87. package/src/commands/undo-redo.ts +15 -19
  88. package/src/commands/unset-model.ts +7 -6
  89. package/src/commands/user-command.ts +8 -11
  90. package/src/commands/verbosity.ts +10 -4
  91. package/src/commands/worktree-settings.ts +10 -5
  92. package/src/commands/worktree.ts +27 -12
  93. package/src/config.ts +12 -0
  94. package/src/database.ts +470 -379
  95. package/src/db.test.ts +21 -21
  96. package/src/db.ts +41 -7
  97. package/src/discord-bot.ts +70 -33
  98. package/src/discord-utils.ts +27 -19
  99. package/src/errors.ts +1 -4
  100. package/src/format-tables.test.ts +11 -2
  101. package/src/format-tables.ts +1 -3
  102. package/src/generated/internal/class.ts +2 -2
  103. package/src/generated/internal/prismaNamespace.ts +3 -0
  104. package/src/generated/internal/prismaNamespaceBrowser.ts +3 -0
  105. package/src/generated/models/channel_models.ts +33 -1
  106. package/src/generated/models/global_models.ts +33 -1
  107. package/src/generated/models/session_models.ts +29 -1
  108. package/src/image-utils.ts +3 -1
  109. package/src/interaction-handler.ts +22 -9
  110. package/src/logger.ts +1 -0
  111. package/src/message-formatting.ts +3 -1
  112. package/src/opencode-plugin.ts +4 -9
  113. package/src/opencode.ts +8 -2
  114. package/src/schema.sql +3 -0
  115. package/src/session-handler.ts +301 -215
  116. package/src/system-message.ts +65 -37
  117. package/src/thinking-utils.ts +61 -0
  118. package/src/tools.ts +3 -1
  119. package/src/unnest-code-blocks.test.ts +196 -1
  120. package/src/unnest-code-blocks.ts +48 -18
  121. package/src/voice.ts +0 -1
  122. package/src/wait-session.ts +15 -7
  123. package/src/worktree-utils.ts +94 -43
package/dist/bin.js CHANGED
@@ -38,7 +38,8 @@ else {
38
38
  }
39
39
  const now = Date.now();
40
40
  restartTimestamps.push(now);
41
- while (restartTimestamps.length > 0 && restartTimestamps[0] < now - RAPID_RESTART_WINDOW_MS) {
41
+ while (restartTimestamps.length > 0 &&
42
+ restartTimestamps[0] < now - RAPID_RESTART_WINDOW_MS) {
42
43
  restartTimestamps.shift();
43
44
  }
44
45
  if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
@@ -8,8 +8,7 @@ function createCliForIdParsing() {
8
8
  .option('-c, --channel <channelId>', 'Discord channel ID')
9
9
  .option('--thread <threadId>', 'Thread ID')
10
10
  .option('--session <sessionId>', 'Session ID');
11
- cli
12
- .command('session archive <threadId>', 'Archive a thread');
11
+ cli.command('session archive <threadId>', 'Archive a thread');
13
12
  cli
14
13
  .command('add-project', 'Add a project')
15
14
  .option('-g, --guild <guildId>', 'Discord guild/server ID');
@@ -21,13 +20,17 @@ describe('goke CLI ID parsing', () => {
21
20
  const channelId = '1234567890123456789';
22
21
  const threadId = '9876543210987654321';
23
22
  const sessionId = '1111222233334444555';
24
- const channelResult = cli.parse(['node', 'kimaki', 'send', '--channel', channelId], { run: false });
23
+ const channelResult = cli.parse(['node', 'kimaki', 'send', '--channel', channelId], {
24
+ run: false,
25
+ });
25
26
  expect(channelResult.options.channel).toBe(channelId);
26
27
  expect(typeof channelResult.options.channel).toBe('string');
27
28
  const threadResult = cli.parse(['node', 'kimaki', 'send', '--thread', threadId], { run: false });
28
29
  expect(threadResult.options.thread).toBe(threadId);
29
30
  expect(typeof threadResult.options.thread).toBe('string');
30
- const sessionResult = cli.parse(['node', 'kimaki', 'send', '--session', sessionId], { run: false });
31
+ const sessionResult = cli.parse(['node', 'kimaki', 'send', '--session', sessionId], {
32
+ run: false,
33
+ });
31
34
  expect(sessionResult.options.session).toBe(sessionId);
32
35
  expect(typeof sessionResult.options.session).toBe('string');
33
36
  });
package/dist/cli.js CHANGED
@@ -19,9 +19,9 @@ import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
19
19
  import { archiveThread, uploadFilesToDiscord, stripMentions } from './discord-utils.js';
20
20
  import { spawn, spawnSync, execSync } from 'node:child_process';
21
21
  import http from 'node:http';
22
- import { setDataDir, getDataDir, getLockPort, setDefaultVerbosity, setDefaultMentionMode, getProjectsDir } from './config.js';
22
+ import { setDataDir, getDataDir, getLockPort, setDefaultVerbosity, setDefaultMentionMode, setCritiqueEnabled, getProjectsDir, } from './config.js';
23
23
  import { sanitizeAgentName } from './commands/agent.js';
24
- import { showFileUploadButton, } from './commands/file-upload.js';
24
+ import { showFileUploadButton } from './commands/file-upload.js';
25
25
  import { execAsync } from './worktree-utils.js';
26
26
  import { backgroundUpgradeKimaki, upgrade, getCurrentVersion } from './upgrade.js';
27
27
  const cliLogger = createLogger(LogPrefix.CLI);
@@ -32,7 +32,10 @@ function stripBracketedPaste(value) {
32
32
  if (!value) {
33
33
  return '';
34
34
  }
35
- return value.replace(/\x1b\[200~/g, '').replace(/\x1b\[201~/g, '').trim();
35
+ return value
36
+ .replace(/\x1b\[200~/g, '')
37
+ .replace(/\x1b\[201~/g, '')
38
+ .trim();
36
39
  }
37
40
  function isThreadChannelType(type) {
38
41
  return [
@@ -93,7 +96,11 @@ async function ensureCommandAvailable({ name, envPathKey, installUnix, installWi
93
96
  }
94
97
  const isWindows = process.platform === 'win32';
95
98
  const whichCmd = isWindows ? 'where' : 'which';
96
- const isInstalled = await execAsync(`${whichCmd} ${name}`, { env: process.env }).then(() => { return true; }, () => { return false; });
99
+ const isInstalled = await execAsync(`${whichCmd} ${name}`, { env: process.env }).then(() => {
100
+ return true;
101
+ }, () => {
102
+ return false;
103
+ });
97
104
  if (isInstalled) {
98
105
  return;
99
106
  }
@@ -134,7 +141,11 @@ async function ensureCommandAvailable({ name, envPathKey, installUnix, installWi
134
141
  process.exit(EXIT_NO_RESTART);
135
142
  }
136
143
  // After install, re-check PATH first (install script may have added it)
137
- const foundInPath = await execAsync(`${whichCmd} ${name}`, { env: process.env }).then((result) => { return result.stdout.trim(); }, () => { return ''; });
144
+ const foundInPath = await execAsync(`${whichCmd} ${name}`, { env: process.env }).then((result) => {
145
+ return result.stdout.trim();
146
+ }, () => {
147
+ return '';
148
+ });
138
149
  if (foundInPath) {
139
150
  process.env[envPathKey] = foundInPath;
140
151
  return;
@@ -143,8 +154,12 @@ async function ensureCommandAvailable({ name, envPathKey, installUnix, installWi
143
154
  const home = process.env.HOME || process.env.USERPROFILE || '';
144
155
  const accessFlag = isWindows ? fs.constants.F_OK : fs.constants.X_OK;
145
156
  const possiblePaths = (isWindows ? possiblePathsWindows : possiblePathsUnix)
146
- .filter((p) => { return !p.startsWith('~') || home; })
147
- .map((p) => { return p.replace('~', home); });
157
+ .filter((p) => {
158
+ return !p.startsWith('~') || home;
159
+ })
160
+ .map((p) => {
161
+ return p.replace('~', home);
162
+ });
148
163
  const installedPath = possiblePaths.find((p) => {
149
164
  try {
150
165
  fs.accessSync(p, accessFlag);
@@ -306,7 +321,9 @@ async function startLockServer() {
306
321
  };
307
322
  if (!request.sessionId || !request.threadId || !request.directory) {
308
323
  res.writeHead(400, { 'Content-Type': 'application/json' });
309
- res.end(JSON.stringify({ error: 'Missing required fields: sessionId, threadId, directory' }));
324
+ res.end(JSON.stringify({
325
+ error: 'Missing required fields: sessionId, threadId, directory',
326
+ }));
310
327
  return;
311
328
  }
312
329
  const thread = await discordClientRef.channels.fetch(request.threadId);
@@ -584,10 +601,7 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
584
601
  .setName('run-shell-command')
585
602
  .setDescription('Run a shell command in the project directory. Tip: prefix messages with ! as shortcut')
586
603
  .addStringOption((option) => {
587
- option
588
- .setName('command')
589
- .setDescription('Command to run')
590
- .setRequired(true);
604
+ option.setName('command').setDescription('Command to run').setRequired(true);
591
605
  return option;
592
606
  })
593
607
  .setDMPermission(false)
@@ -772,13 +786,8 @@ async function run({ restart, addChannels, useWorktrees, enableVoiceChannels })
772
786
  envPathKey: 'BUN_PATH',
773
787
  installUnix: 'curl -fsSL https://bun.sh/install | bash',
774
788
  installWindows: 'irm bun.sh/install.ps1 | iex',
775
- possiblePathsUnix: [
776
- '~/.bun/bin/bun',
777
- '/usr/local/bin/bun',
778
- ],
779
- possiblePathsWindows: [
780
- '~\\.bun\\bin\\bun.exe',
781
- ],
789
+ possiblePathsUnix: ['~/.bun/bin/bun', '/usr/local/bin/bun'],
790
+ possiblePathsWindows: ['~\\.bun\\bin\\bun.exe'],
782
791
  });
783
792
  backgroundUpgradeOpencode();
784
793
  backgroundUpgradeKimaki();
@@ -1105,6 +1114,7 @@ cli
1105
1114
  .option('--enable-voice-channels', 'Create voice channels for projects (disabled by default)')
1106
1115
  .option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text, text-and-essential-tools, or text-only)')
1107
1116
  .option('--mention-mode', 'Bot only responds when @mentioned (default for all channels)')
1117
+ .option('--no-critique', 'Disable automatic diff upload to critique.work in system prompts')
1108
1118
  .option('--auto-restart', 'Automatically restart the bot on crash or OOM kill')
1109
1119
  .action(async (options) => {
1110
1120
  try {
@@ -1126,6 +1136,10 @@ cli
1126
1136
  setDefaultMentionMode(true);
1127
1137
  cliLogger.log('Default mention mode: enabled (bot only responds when @mentioned)');
1128
1138
  }
1139
+ if (options.noCritique) {
1140
+ setCritiqueEnabled(false);
1141
+ cliLogger.log('Critique disabled: diffs will not be auto-uploaded to critique.work');
1142
+ }
1129
1143
  if (options.installUrl) {
1130
1144
  await initDatabase();
1131
1145
  const existingBot = await getBotToken();
@@ -1318,11 +1332,16 @@ cli
1318
1332
  try {
1319
1333
  // Helper to find channel for a path (prefers current bot's channel)
1320
1334
  const findChannelForPath = async (dirPath) => {
1321
- const withAppId = appId ? await findChannelsByDirectory({ directory: dirPath, channelType: 'text', appId }) : [];
1335
+ const withAppId = appId
1336
+ ? await findChannelsByDirectory({ directory: dirPath, channelType: 'text', appId })
1337
+ : [];
1322
1338
  if (withAppId.length > 0) {
1323
1339
  return withAppId[0];
1324
1340
  }
1325
- const withoutAppId = await findChannelsByDirectory({ directory: dirPath, channelType: 'text' });
1341
+ const withoutAppId = await findChannelsByDirectory({
1342
+ directory: dirPath,
1343
+ channelType: 'text',
1344
+ });
1326
1345
  return withoutAppId[0];
1327
1346
  };
1328
1347
  // Try exact match first, then walk up parent directories
@@ -1506,9 +1525,7 @@ cli
1506
1525
  const worktreeName = options.worktree
1507
1526
  ? formatWorktreeName(typeof options.worktree === 'string' ? options.worktree : baseThreadName)
1508
1527
  : undefined;
1509
- const threadName = worktreeName
1510
- ? `${WORKTREE_PREFIX}${baseThreadName}`
1511
- : baseThreadName;
1528
+ const threadName = worktreeName ? `${WORKTREE_PREFIX}${baseThreadName}` : baseThreadName;
1512
1529
  // Embed marker for auto-start sessions (unless --notify-only)
1513
1530
  // Bot parses this YAML to know it should start a session, optionally create a worktree, and set initial user
1514
1531
  const embedMarker = notifyOnly
@@ -1544,7 +1561,9 @@ cli
1544
1561
  await rest.put(Routes.threadMembers(threadData.id, resolvedUser.id));
1545
1562
  }
1546
1563
  const threadUrl = `https://discord.com/channels/${channelData.guild_id}/${threadData.id}`;
1547
- const worktreeNote = worktreeName ? `\nWorktree: ${worktreeName} (will be created by bot)` : '';
1564
+ const worktreeNote = worktreeName
1565
+ ? `\nWorktree: ${worktreeName} (will be created by bot)`
1566
+ : '';
1548
1567
  const successMessage = notifyOnly
1549
1568
  ? `Thread: ${threadData.name}\nDirectory: ${projectDirectory}\n\nNotification created. Reply to start a session.\n\nURL: ${threadUrl}`
1550
1569
  : `Thread: ${threadData.name}\nDirectory: ${projectDirectory}${worktreeNote}\n\nThe running bot will pick this up and start the session.\n\nURL: ${threadUrl}`;
@@ -1774,11 +1793,16 @@ cli
1774
1793
  const absolutePath = path.resolve('.');
1775
1794
  // Walk up parent directories to find a matching channel
1776
1795
  const findChannelForPath = async (dirPath) => {
1777
- const withAppId = appId ? await findChannelsByDirectory({ directory: dirPath, channelType: 'text', appId }) : [];
1796
+ const withAppId = appId
1797
+ ? await findChannelsByDirectory({ directory: dirPath, channelType: 'text', appId })
1798
+ : [];
1778
1799
  if (withAppId.length > 0) {
1779
1800
  return withAppId[0];
1780
1801
  }
1781
- const withoutAppId = await findChannelsByDirectory({ directory: dirPath, channelType: 'text' });
1802
+ const withoutAppId = await findChannelsByDirectory({
1803
+ directory: dirPath,
1804
+ channelType: 'text',
1805
+ });
1782
1806
  return withoutAppId[0];
1783
1807
  };
1784
1808
  let existingChannel;
@@ -1918,9 +1942,7 @@ cli
1918
1942
  command: command.length > 0 ? command : undefined,
1919
1943
  });
1920
1944
  });
1921
- cli
1922
- .command('sqlitedb', 'Show the location of the SQLite database file')
1923
- .action(() => {
1945
+ cli.command('sqlitedb', 'Show the location of the SQLite database file').action(() => {
1924
1946
  const dataDir = getDataDir();
1925
1947
  const dbPath = path.join(dataDir, 'discord-sessions.db');
1926
1948
  cliLogger.log(dbPath);
@@ -1992,15 +2014,48 @@ cli
1992
2014
  cliLogger.error('Failed to connect to OpenCode:', getClient.message);
1993
2015
  process.exit(EXIT_NO_RESTART);
1994
2016
  }
2017
+ // Try current project first (fast path)
1995
2018
  const markdown = new ShareMarkdown(getClient());
1996
2019
  const result = await markdown.generate({ sessionID: sessionId });
1997
- if (result instanceof Error) {
1998
- cliLogger.error(result.message);
1999
- process.exit(EXIT_NO_RESTART);
2020
+ if (!(result instanceof Error)) {
2021
+ process.stdout.write(result);
2022
+ process.exit(0);
2000
2023
  }
2001
- // Print to stdout so it can be piped: kimaki session read <id> > ./tmp/session.md
2002
- process.stdout.write(result);
2003
- process.exit(0);
2024
+ // Session not found in current project, search across all projects.
2025
+ // project.list() returns all known projects globally from any OpenCode server,
2026
+ // but session.list/get are scoped to the server's own project. So we try each.
2027
+ cliLogger.log('Session not in current project, searching all projects...');
2028
+ const projectsResponse = await getClient().project.list({});
2029
+ const projects = projectsResponse.data || [];
2030
+ const otherProjects = projects
2031
+ .filter((p) => path.resolve(p.worktree) !== projectDirectory)
2032
+ .filter((p) => {
2033
+ try {
2034
+ fs.accessSync(p.worktree, fs.constants.R_OK);
2035
+ return true;
2036
+ }
2037
+ catch {
2038
+ return false;
2039
+ }
2040
+ })
2041
+ // Sort by most recently created first to find sessions faster
2042
+ .sort((a, b) => b.time.created - a.time.created);
2043
+ for (const project of otherProjects) {
2044
+ const dir = project.worktree;
2045
+ cliLogger.log(`Trying project: ${dir}`);
2046
+ const otherClient = await initializeOpencodeForDirectory(dir);
2047
+ if (otherClient instanceof Error) {
2048
+ continue;
2049
+ }
2050
+ const otherMarkdown = new ShareMarkdown(otherClient());
2051
+ const otherResult = await otherMarkdown.generate({ sessionID: sessionId });
2052
+ if (!(otherResult instanceof Error)) {
2053
+ process.stdout.write(otherResult);
2054
+ process.exit(0);
2055
+ }
2056
+ }
2057
+ cliLogger.error(`Session ${sessionId} not found in any project`);
2058
+ process.exit(EXIT_NO_RESTART);
2004
2059
  }
2005
2060
  catch (error) {
2006
2061
  cliLogger.error('Error:', error instanceof Error ? error.message : String(error));
@@ -2060,9 +2115,7 @@ cli
2060
2115
  process.exit(EXIT_NO_RESTART);
2061
2116
  }
2062
2117
  });
2063
- cli
2064
- .command('upgrade', 'Upgrade kimaki to the latest version')
2065
- .action(async () => {
2118
+ cli.command('upgrade', 'Upgrade kimaki to the latest version').action(async () => {
2066
2119
  try {
2067
2120
  const current = getCurrentVersion();
2068
2121
  cliLogger.log(`Current version: v${current}`);
@@ -1,5 +1,5 @@
1
1
  // /abort command - Abort the current OpenCode request in this thread.
2
- import { ChannelType } from 'discord.js';
2
+ import { ChannelType, MessageFlags } from 'discord.js';
3
3
  import { getThreadSession } from '../database.js';
4
4
  import { initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
@@ -11,8 +11,7 @@ export async function handleAbortCommand({ command }) {
11
11
  if (!channel) {
12
12
  await command.reply({
13
13
  content: 'This command can only be used in a channel',
14
- ephemeral: true,
15
- flags: SILENT_MESSAGE_FLAGS,
14
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
16
15
  });
17
16
  return;
18
17
  }
@@ -24,17 +23,17 @@ export async function handleAbortCommand({ command }) {
24
23
  if (!isThread) {
25
24
  await command.reply({
26
25
  content: 'This command can only be used in a thread with an active session',
27
- ephemeral: true,
28
- flags: SILENT_MESSAGE_FLAGS,
26
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
29
27
  });
30
28
  return;
31
29
  }
32
- const resolved = await resolveWorkingDirectory({ channel: channel });
30
+ const resolved = await resolveWorkingDirectory({
31
+ channel: channel,
32
+ });
33
33
  if (!resolved) {
34
34
  await command.reply({
35
35
  content: 'Could not determine project directory for this channel',
36
- ephemeral: true,
37
- flags: SILENT_MESSAGE_FLAGS,
36
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
38
37
  });
39
38
  return;
40
39
  }
@@ -43,8 +42,7 @@ export async function handleAbortCommand({ command }) {
43
42
  if (!sessionId) {
44
43
  await command.reply({
45
44
  content: 'No active session in this thread',
46
- ephemeral: true,
47
- flags: SILENT_MESSAGE_FLAGS,
45
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
48
46
  });
49
47
  return;
50
48
  }
@@ -58,8 +56,7 @@ export async function handleAbortCommand({ command }) {
58
56
  if (getClient instanceof Error) {
59
57
  await command.reply({
60
58
  content: `Failed to abort: ${getClient.message}`,
61
- ephemeral: true,
62
- flags: SILENT_MESSAGE_FLAGS,
59
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
63
60
  });
64
61
  return;
65
62
  }
@@ -78,8 +75,7 @@ export async function handleAbortCommand({ command }) {
78
75
  logger.error('[ABORT] Error:', error);
79
76
  await command.reply({
80
77
  content: `Failed to abort: ${error instanceof Error ? error.message : 'Unknown error'}`,
81
- ephemeral: true,
82
- flags: SILENT_MESSAGE_FLAGS,
78
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
83
79
  });
84
80
  }
85
81
  }
@@ -1,8 +1,8 @@
1
1
  // /agent command - Set the preferred agent for this channel or session.
2
2
  // Also provides quick agent commands like /plan-agent, /build-agent that switch instantly.
3
- import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, } from 'discord.js';
3
+ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ChannelType, MessageFlags, } from 'discord.js';
4
4
  import crypto from 'node:crypto';
5
- import { setChannelAgent, setSessionAgent, clearSessionModel, getThreadSession, getSessionAgent, getChannelAgent } from '../database.js';
5
+ import { setChannelAgent, setSessionAgent, clearSessionModel, getThreadSession, getSessionAgent, getChannelAgent, } from '../database.js';
6
6
  import { initializeOpencodeForDirectory } from '../opencode.js';
7
7
  import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
8
8
  import { createLogger, LogPrefix } from '../logger.js';
@@ -32,7 +32,11 @@ export async function getCurrentAgentInfo({ sessionId, channelId, }) {
32
32
  * Lowercase, alphanumeric and hyphens only.
33
33
  */
34
34
  export function sanitizeAgentName(name) {
35
- return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
35
+ return name
36
+ .toLowerCase()
37
+ .replace(/[^a-z0-9-]/g, '-')
38
+ .replace(/-+/g, '-')
39
+ .replace(/^-|-$/g, '');
36
40
  }
37
41
  /**
38
42
  * Resolve the context for an agent command (directory, channel, session).
@@ -110,7 +114,7 @@ export async function setAgentForContext({ context, agentName, }) {
110
114
  }
111
115
  }
112
116
  export async function handleAgentCommand({ interaction, appId, }) {
113
- await interaction.deferReply({ ephemeral: true });
117
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
114
118
  const context = await resolveAgentCommandContext({ interaction, appId });
115
119
  if (!context) {
116
120
  return;
@@ -236,7 +240,7 @@ export async function handleAgentSelectMenu(interaction) {
236
240
  export async function handleQuickAgentCommand({ command, appId, }) {
237
241
  // Extract agent name from command: "plan-agent" → "plan"
238
242
  const sanitizedAgentName = command.commandName.replace(/-agent$/, '');
239
- await command.deferReply({ ephemeral: true });
243
+ await command.deferReply({ flags: MessageFlags.Ephemeral });
240
244
  const context = await resolveAgentCommandContext({ interaction: command, appId });
241
245
  if (!context) {
242
246
  return;
@@ -1,7 +1,7 @@
1
1
  // AskUserQuestion tool handler - Shows Discord dropdowns for AI questions.
2
2
  // When the AI uses the AskUserQuestion tool, this module renders dropdowns
3
3
  // for each question and collects user responses.
4
- import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder, } from 'discord.js';
4
+ import { StringSelectMenuBuilder, StringSelectMenuInteraction, ActionRowBuilder, MessageFlags, } from 'discord.js';
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';
@@ -77,7 +77,7 @@ export async function handleAskQuestionSelectMenu(interaction) {
77
77
  if (!contextHash) {
78
78
  await interaction.reply({
79
79
  content: 'Invalid selection.',
80
- ephemeral: true,
80
+ flags: MessageFlags.Ephemeral,
81
81
  });
82
82
  return;
83
83
  }
@@ -85,7 +85,7 @@ export async function handleAskQuestionSelectMenu(interaction) {
85
85
  if (!context) {
86
86
  await interaction.reply({
87
87
  content: 'This question has expired. Please ask the AI again.',
88
- ephemeral: true,
88
+ flags: MessageFlags.Ephemeral,
89
89
  });
90
90
  return;
91
91
  }
@@ -1,5 +1,5 @@
1
1
  // /compact command - Trigger context compaction (summarization) for the current session.
2
- import { ChannelType } from 'discord.js';
2
+ import { ChannelType, MessageFlags } from 'discord.js';
3
3
  import { getThreadSession } from '../database.js';
4
4
  import { initializeOpencodeForDirectory, getOpencodeClientV2 } from '../opencode.js';
5
5
  import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
@@ -10,8 +10,7 @@ export async function handleCompactCommand({ command }) {
10
10
  if (!channel) {
11
11
  await command.reply({
12
12
  content: 'This command can only be used in a channel',
13
- ephemeral: true,
14
- flags: SILENT_MESSAGE_FLAGS,
13
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
15
14
  });
16
15
  return;
17
16
  }
@@ -23,17 +22,17 @@ export async function handleCompactCommand({ command }) {
23
22
  if (!isThread) {
24
23
  await command.reply({
25
24
  content: 'This command can only be used in a thread with an active session',
26
- ephemeral: true,
27
- flags: SILENT_MESSAGE_FLAGS,
25
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
28
26
  });
29
27
  return;
30
28
  }
31
- const resolved = await resolveWorkingDirectory({ channel: channel });
29
+ const resolved = await resolveWorkingDirectory({
30
+ channel: channel,
31
+ });
32
32
  if (!resolved) {
33
33
  await command.reply({
34
34
  content: 'Could not determine project directory for this channel',
35
- ephemeral: true,
36
- flags: SILENT_MESSAGE_FLAGS,
35
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
37
36
  });
38
37
  return;
39
38
  }
@@ -42,8 +41,7 @@ export async function handleCompactCommand({ command }) {
42
41
  if (!sessionId) {
43
42
  await command.reply({
44
43
  content: 'No active session in this thread',
45
- ephemeral: true,
46
- flags: SILENT_MESSAGE_FLAGS,
44
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
47
45
  });
48
46
  return;
49
47
  }
@@ -52,8 +50,7 @@ export async function handleCompactCommand({ command }) {
52
50
  if (getClient instanceof Error) {
53
51
  await command.reply({
54
52
  content: `Failed to compact: ${getClient.message}`,
55
- ephemeral: true,
56
- flags: SILENT_MESSAGE_FLAGS,
53
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
57
54
  });
58
55
  return;
59
56
  }
@@ -61,8 +58,7 @@ export async function handleCompactCommand({ command }) {
61
58
  if (!clientV2) {
62
59
  await command.reply({
63
60
  content: 'Failed to get OpenCode client',
64
- ephemeral: true,
65
- flags: SILENT_MESSAGE_FLAGS,
61
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
66
62
  });
67
63
  return;
68
64
  }
@@ -1,5 +1,5 @@
1
1
  // /context-usage command - Show token usage and context window percentage for the current session.
2
- import { ChannelType } from 'discord.js';
2
+ import { ChannelType, MessageFlags } from 'discord.js';
3
3
  import { getThreadSession } from '../database.js';
4
4
  import { initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
@@ -14,8 +14,7 @@ export async function handleContextUsageCommand({ command }) {
14
14
  if (!channel) {
15
15
  await command.reply({
16
16
  content: 'This command can only be used in a channel',
17
- ephemeral: true,
18
- flags: SILENT_MESSAGE_FLAGS,
17
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
19
18
  });
20
19
  return;
21
20
  }
@@ -27,17 +26,17 @@ export async function handleContextUsageCommand({ command }) {
27
26
  if (!isThread) {
28
27
  await command.reply({
29
28
  content: 'This command can only be used in a thread with an active session',
30
- ephemeral: true,
31
- flags: SILENT_MESSAGE_FLAGS,
29
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
32
30
  });
33
31
  return;
34
32
  }
35
- const resolved = await resolveWorkingDirectory({ channel: channel });
33
+ const resolved = await resolveWorkingDirectory({
34
+ channel: channel,
35
+ });
36
36
  if (!resolved) {
37
37
  await command.reply({
38
38
  content: 'Could not determine project directory for this channel',
39
- ephemeral: true,
40
- flags: SILENT_MESSAGE_FLAGS,
39
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
41
40
  });
42
41
  return;
43
42
  }
@@ -46,8 +45,7 @@ export async function handleContextUsageCommand({ command }) {
46
45
  if (!sessionId) {
47
46
  await command.reply({
48
47
  content: 'No active session in this thread',
49
- ephemeral: true,
50
- flags: SILENT_MESSAGE_FLAGS,
48
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
51
49
  });
52
50
  return;
53
51
  }
@@ -55,8 +53,7 @@ export async function handleContextUsageCommand({ command }) {
55
53
  if (getClient instanceof Error) {
56
54
  await command.reply({
57
55
  content: `Failed to get context usage: ${getClient.message}`,
58
- ephemeral: true,
59
- flags: SILENT_MESSAGE_FLAGS,
56
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
60
57
  });
61
58
  return;
62
59
  }
@@ -74,9 +71,7 @@ export async function handleContextUsageCommand({ command }) {
74
71
  });
75
72
  return;
76
73
  }
77
- const lastAssistant = [...assistantMessages]
78
- .reverse()
79
- .find((m) => {
74
+ const lastAssistant = [...assistantMessages].reverse().find((m) => {
80
75
  if (m.info.role !== 'assistant') {
81
76
  return false;
82
77
  }
@@ -1,5 +1,5 @@
1
1
  // /diff command - Show git diff as a shareable URL.
2
- import { ChannelType, EmbedBuilder } from 'discord.js';
2
+ import { ChannelType, EmbedBuilder, MessageFlags, } from 'discord.js';
3
3
  import path from 'node:path';
4
4
  import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
5
5
  import { createLogger, LogPrefix } from '../logger.js';
@@ -10,8 +10,7 @@ export async function handleDiffCommand({ command }) {
10
10
  if (!channel) {
11
11
  await command.reply({
12
12
  content: 'This command can only be used in a channel',
13
- ephemeral: true,
14
- flags: SILENT_MESSAGE_FLAGS,
13
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
15
14
  });
16
15
  return;
17
16
  }
@@ -24,17 +23,17 @@ export async function handleDiffCommand({ command }) {
24
23
  if (!isThread && !isTextChannel) {
25
24
  await command.reply({
26
25
  content: 'This command can only be used in a text channel or thread',
27
- ephemeral: true,
28
- flags: SILENT_MESSAGE_FLAGS,
26
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
29
27
  });
30
28
  return;
31
29
  }
32
- const resolved = await resolveWorkingDirectory({ channel: channel });
30
+ const resolved = await resolveWorkingDirectory({
31
+ channel: channel,
32
+ });
33
33
  if (!resolved) {
34
34
  await command.reply({
35
35
  content: 'Could not determine project directory for this channel',
36
- ephemeral: true,
37
- flags: SILENT_MESSAGE_FLAGS,
36
+ flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
38
37
  });
39
38
  return;
40
39
  }
@@ -8,7 +8,7 @@
8
8
  // with the Discord bot via HTTP. The bot holds the HTTP response open until
9
9
  // the user completes the upload or the request is cancelled. This bridges the
10
10
  // gap between the plugin process and Discord's interaction-based UI.
11
- import { ButtonBuilder, ButtonStyle, ActionRowBuilder, ModalBuilder, FileUploadBuilder, LabelBuilder, ComponentType, } from 'discord.js';
11
+ import { ButtonBuilder, ButtonStyle, ActionRowBuilder, ModalBuilder, FileUploadBuilder, LabelBuilder, ComponentType, MessageFlags, } from 'discord.js';
12
12
  import crypto from 'node:crypto';
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
@@ -126,7 +126,7 @@ export async function handleFileUploadButton(interaction) {
126
126
  if (!context || context.resolved) {
127
127
  await interaction.reply({
128
128
  content: 'This file upload request has expired.',
129
- ephemeral: true,
129
+ flags: MessageFlags.Ephemeral,
130
130
  });
131
131
  return;
132
132
  }
@@ -157,12 +157,12 @@ export async function handleFileUploadModalSubmit(interaction) {
157
157
  if (!context || context.resolved) {
158
158
  await interaction.reply({
159
159
  content: 'This file upload request has expired.',
160
- ephemeral: true,
160
+ flags: MessageFlags.Ephemeral,
161
161
  });
162
162
  return;
163
163
  }
164
164
  try {
165
- await interaction.deferReply({ ephemeral: true });
165
+ await interaction.deferReply({ flags: MessageFlags.Ephemeral });
166
166
  // File upload data is nested in the LabelModalData -> FileUploadModalData
167
167
  const fileField = interaction.fields.getField('uploaded_files', ComponentType.FileUpload);
168
168
  const attachments = fileField.attachments;
@@ -208,9 +208,7 @@ export async function handleFileUploadModalSubmit(interaction) {
208
208
  const fileNames = downloadedPaths.map((p) => {
209
209
  return path.basename(p);
210
210
  });
211
- updateButtonMessage(context, downloadedPaths.length > 0
212
- ? `Uploaded: ${fileNames.join(', ')}`
213
- : '_Upload failed_');
211
+ updateButtonMessage(context, downloadedPaths.length > 0 ? `Uploaded: ${fileNames.join(', ')}` : '_Upload failed_');
214
212
  const summary = (() => {
215
213
  if (downloadedPaths.length > 0 && errors.length === 0) {
216
214
  return `Uploaded ${downloadedPaths.length} file(s) successfully.`;