kimaki 0.4.69 → 0.4.71

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 (94) hide show
  1. package/dist/channel-management.js +0 -22
  2. package/dist/cli.js +49 -102
  3. package/dist/commands/abort.js +2 -1
  4. package/dist/commands/action-buttons.js +2 -0
  5. package/dist/commands/file-upload.js +2 -0
  6. package/dist/commands/fork.js +11 -4
  7. package/dist/commands/merge-worktree.js +2 -0
  8. package/dist/commands/queue.js +3 -0
  9. package/dist/commands/restart-opencode-server.js +2 -1
  10. package/dist/commands/user-command.js +7 -2
  11. package/dist/commands/worktree.js +3 -0
  12. package/dist/config.js +0 -9
  13. package/dist/discord-bot.js +165 -110
  14. package/dist/errors.js +12 -1
  15. package/dist/forum-sync/watchers.js +1 -3
  16. package/dist/genai-worker-wrapper.js +2 -0
  17. package/dist/genai-worker.js +14 -3
  18. package/dist/interaction-handler.js +2 -0
  19. package/dist/ipc-polling.js +8 -1
  20. package/dist/kimaki-digital-twin.e2e.test.js +3 -6
  21. package/dist/logger.js +15 -0
  22. package/dist/markdown.js +13 -3
  23. package/dist/message-formatting.js +12 -1
  24. package/dist/opencode-plugin.js +69 -36
  25. package/dist/opencode-plugin.test.js +98 -0
  26. package/dist/opencode.js +20 -15
  27. package/dist/sentry.js +74 -0
  28. package/dist/session-handler/state.js +151 -0
  29. package/dist/session-handler/thread-session-runtime.js +73 -0
  30. package/dist/session-handler.js +184 -42
  31. package/dist/startup-service.js +153 -0
  32. package/dist/system-message.js +50 -73
  33. package/dist/task-runner.js +2 -0
  34. package/dist/thread-message-queue.e2e.test.js +781 -0
  35. package/dist/utils.js +11 -10
  36. package/dist/voice-handler.js +11 -0
  37. package/dist/voice.js +7 -2
  38. package/dist/worktree-utils.js +19 -12
  39. package/package.json +5 -2
  40. package/skills/critique/SKILL.md +129 -0
  41. package/skills/errore/SKILL.md +7 -7
  42. package/skills/goke/.prettierrc +5 -0
  43. package/skills/goke/CHANGELOG.md +40 -0
  44. package/skills/goke/LICENSE +21 -0
  45. package/skills/goke/README.md +666 -0
  46. package/skills/goke/SKILL.md +458 -0
  47. package/skills/goke/package.json +43 -0
  48. package/skills/goke/src/__test__/coerce.test.ts +411 -0
  49. package/skills/goke/src/__test__/index.test.ts +1798 -0
  50. package/skills/goke/src/__test__/types.test-d.ts +111 -0
  51. package/skills/goke/src/coerce.ts +547 -0
  52. package/skills/goke/src/goke.ts +1362 -0
  53. package/skills/goke/src/index.ts +16 -0
  54. package/skills/goke/src/mri.ts +164 -0
  55. package/skills/goke/tsconfig.json +15 -0
  56. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  57. package/skills/zustand-centralized-state/SKILL.md +582 -0
  58. package/src/channel-management.ts +0 -33
  59. package/src/cli.ts +50 -137
  60. package/src/commands/abort.ts +2 -1
  61. package/src/commands/action-buttons.ts +2 -0
  62. package/src/commands/file-upload.ts +2 -0
  63. package/src/commands/fork.ts +31 -22
  64. package/src/commands/merge-worktree.ts +2 -0
  65. package/src/commands/queue.ts +3 -0
  66. package/src/commands/restart-opencode-server.ts +2 -1
  67. package/src/commands/user-command.ts +9 -2
  68. package/src/commands/worktree.ts +3 -0
  69. package/src/config.ts +7 -13
  70. package/src/discord-bot.ts +197 -131
  71. package/src/errors.ts +13 -1
  72. package/src/forum-sync/watchers.ts +1 -3
  73. package/src/genai-worker-wrapper.ts +5 -0
  74. package/src/genai-worker.ts +19 -6
  75. package/src/interaction-handler.ts +2 -0
  76. package/src/ipc-polling.ts +8 -1
  77. package/src/kimaki-digital-twin.e2e.test.ts +3 -6
  78. package/src/logger.ts +16 -0
  79. package/src/markdown.ts +20 -3
  80. package/src/message-formatting.ts +13 -1
  81. package/src/opencode-plugin.test.ts +108 -0
  82. package/src/opencode-plugin.ts +73 -36
  83. package/src/opencode.ts +21 -15
  84. package/src/sentry.ts +83 -0
  85. package/src/session-handler/state.ts +232 -0
  86. package/src/session-handler.ts +255 -51
  87. package/src/startup-service.ts +200 -0
  88. package/src/system-message.ts +57 -72
  89. package/src/task-runner.ts +2 -0
  90. package/src/thread-message-queue.e2e.test.ts +997 -0
  91. package/src/utils.ts +10 -13
  92. package/src/voice-handler.ts +24 -9
  93. package/src/voice.ts +7 -2
  94. package/src/worktree-utils.ts +21 -13
@@ -81,28 +81,6 @@ export async function createProjectChannels({ guild, projectDirectory, appId, bo
81
81
  channelName,
82
82
  };
83
83
  }
84
- /**
85
- * Ensure a forum channel named "{botName}-memory" exists in the Kimaki category.
86
- * Creates it if missing. Returns the forum channel ID.
87
- */
88
- export async function ensureMemoryForumChannel({ guild, botName, }) {
89
- const isKimakiBot = botName?.toLowerCase() === 'kimaki';
90
- const forumName = botName && !isKimakiBot ? `${botName}-memory` : 'kimaki-memory';
91
- const existing = guild.channels.cache.find((channel) => {
92
- if (channel.type !== ChannelType.GuildForum)
93
- return false;
94
- return channel.name.toLowerCase() === forumName.toLowerCase();
95
- });
96
- if (existing)
97
- return existing;
98
- const kimakiCategory = await ensureKimakiCategory(guild, botName);
99
- return guild.channels.create({
100
- name: forumName,
101
- type: ChannelType.GuildForum,
102
- parent: kimakiCategory,
103
- topic: 'Persistent memory files synced from ~/.kimaki/memory/',
104
- });
105
- }
106
84
  export async function getChannelsWithDescriptions(guild) {
107
85
  const channels = [];
108
86
  const textChannels = guild.channels.cache.filter((channel) => channel.isTextBased());
package/dist/cli.js CHANGED
@@ -5,8 +5,8 @@
5
5
  import { goke } from 'goke';
6
6
  import { intro, outro, text, password, note, cancel, isCancel, confirm, log, multiselect, } from '@clack/prompts';
7
7
  import { deduplicateByKey, generateBotInstallUrl, abbreviatePath, } from './utils.js';
8
- import { getChannelsWithDescriptions, createDiscordClient, initDatabase, getChannelDirectory, startDiscordBot, initializeOpencodeForDirectory, ensureKimakiCategory, createProjectChannels, ensureMemoryForumChannel, } from './discord-bot.js';
9
- import { getBotToken, setBotToken, setChannelDirectory, findChannelsByDirectory, findChannelByAppId, getThreadSession, getThreadIdBySessionId, getPrisma, upsertForumSyncConfig, deleteStaleForumSyncConfigs, createScheduledTask, listScheduledTasks, cancelScheduledTask, getSessionStartSourcesBySessionIds, } from './database.js';
8
+ import { getChannelsWithDescriptions, createDiscordClient, initDatabase, getChannelDirectory, startDiscordBot, initializeOpencodeForDirectory, ensureKimakiCategory, createProjectChannels, } from './discord-bot.js';
9
+ import { getBotToken, setBotToken, setChannelDirectory, findChannelsByDirectory, findChannelByAppId, getThreadSession, getThreadIdBySessionId, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getSessionStartSourcesBySessionIds, } from './database.js';
10
10
  import { ShareMarkdown } from './markdown.js';
11
11
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts, } from './session-search.js';
12
12
  import { formatWorktreeName } from './commands/worktree.js';
@@ -17,13 +17,13 @@ import path from 'node:path';
17
17
  import fs from 'node:fs';
18
18
  import * as errore from 'errore';
19
19
  import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
20
+ import { initSentry, notifyError } from './sentry.js';
20
21
  import { archiveThread, uploadFilesToDiscord, stripMentions, } from './discord-utils.js';
21
22
  import { spawn, execSync } from 'node:child_process';
22
- import { setDataDir, getDataDir, setDefaultVerbosity, setDefaultMentionMode, setCritiqueEnabled, setVerboseOpencodeServer, getMemoryEnabled, setMemoryEnabled, getProjectsDir, } from './config.js';
23
+ import { setDataDir, getDataDir, setDefaultVerbosity, setDefaultMentionMode, setCritiqueEnabled, setVerboseOpencodeServer, getProjectsDir, } from './config.js';
23
24
  import { sanitizeAgentName } from './commands/agent.js';
24
25
  import { execAsync } from './worktree-utils.js';
25
26
  import { backgroundUpgradeKimaki, upgrade, getCurrentVersion, } from './upgrade.js';
26
- import { startConfiguredForumSync } from './forum-sync/index.js';
27
27
  import { startHranaServer } from './hrana-server.js';
28
28
  import { startIpcPolling, stopIpcPolling } from './ipc-polling.js';
29
29
  import { getLocalTimeZone, getPromptPreview, parseSendAtValue, serializeScheduledTaskPayload, } from './task-schedule.js';
@@ -542,14 +542,32 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
542
542
  if (SKIP_USER_COMMANDS.includes(cmd.name)) {
543
543
  continue;
544
544
  }
545
- // Sanitize command name: oh-my-opencode uses MCP commands with colons, which Discord doesn't allow
546
- // Also convert to lowercase since Discord only allows lowercase in command names
547
- const sanitizedName = cmd.name.toLowerCase().replace(/:/g, '-');
548
- const commandName = `${sanitizedName}-cmd`;
545
+ // Sanitize command name: oh-my-opencode uses MCP commands with colons and slashes,
546
+ // which Discord doesn't allow in command names.
547
+ // Discord command names: lowercase, alphanumeric and hyphens only, must start with letter/number.
548
+ const sanitizedName = cmd.name
549
+ .toLowerCase()
550
+ .replace(/[:/]/g, '-') // Replace : and / with hyphens first
551
+ .replace(/[^a-z0-9-]/g, '-') // Replace any other non-alphanumeric chars
552
+ .replace(/-+/g, '-') // Collapse multiple hyphens
553
+ .replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
554
+ // Skip if sanitized name is empty - would create invalid command name like "-cmd"
555
+ if (!sanitizedName) {
556
+ continue;
557
+ }
558
+ // Truncate base name before appending suffix so the -cmd suffix is never
559
+ // lost to Discord's 32-char command name limit.
560
+ const cmdSuffix = '-cmd';
561
+ const baseName = sanitizedName.slice(0, 32 - cmdSuffix.length);
562
+ const commandName = `${baseName}${cmdSuffix}`;
549
563
  const description = cmd.description || `Run /${cmd.name} command`;
550
- registeredUserCommands.push({ name: cmd.name, description });
564
+ registeredUserCommands.push({
565
+ name: cmd.name,
566
+ discordName: baseName,
567
+ description,
568
+ });
551
569
  commands.push(new SlashCommandBuilder()
552
- .setName(commandName.slice(0, 32)) // Discord limits to 32 chars
570
+ .setName(commandName)
553
571
  .setDescription(description.slice(0, 100)) // Discord limits to 100 chars
554
572
  .addStringOption((option) => {
555
573
  option
@@ -566,10 +584,19 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
566
584
  const primaryAgents = agents.filter((a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden);
567
585
  for (const agent of primaryAgents) {
568
586
  const sanitizedName = sanitizeAgentName(agent.name);
569
- const commandName = `${sanitizedName}-agent`;
587
+ // Skip if sanitized name is empty or would create invalid command name
588
+ // Discord command names must start with a lowercase letter or number
589
+ if (!sanitizedName || !/^[a-z0-9]/.test(sanitizedName)) {
590
+ continue;
591
+ }
592
+ // Truncate base name before appending suffix so the -agent suffix is never
593
+ // lost to Discord's 32-char command name limit.
594
+ const agentSuffix = '-agent';
595
+ const agentBaseName = sanitizedName.slice(0, 32 - agentSuffix.length);
596
+ const commandName = `${agentBaseName}${agentSuffix}`;
570
597
  const description = agent.description || `Switch to ${agent.name} agent`;
571
598
  commands.push(new SlashCommandBuilder()
572
- .setName(commandName.slice(0, 32)) // Discord limits to 32 chars
599
+ .setName(commandName)
573
600
  .setDescription(description.slice(0, 100))
574
601
  .setDMPermission(false)
575
602
  .toJSON());
@@ -675,49 +702,6 @@ function showReadyMessage({ kimakiChannels, createdChannels, appId, }) {
675
702
  }
676
703
  note('Leave this process running to keep the bot active.\n\nIf you close this process or restart your machine, run `npx kimaki` again to start the bot.', '⚠️ Keep Running');
677
704
  }
678
- /**
679
- * Ensure the memory forum channel exists and register it for forum sync.
680
- * Runs in background -- errors are logged but don't block startup.
681
- */
682
- async function ensureMemoryForumSync({ guilds, appId, botName, }) {
683
- const guild = guilds[0];
684
- if (!guild) {
685
- cliLogger.warn('No guild available, skipping memory forum setup');
686
- return;
687
- }
688
- const ensureGlobalMemoryTag = async ({ forumChannel, }) => {
689
- const hasGlobalTag = forumChannel.availableTags.some((tag) => tag.name.toLowerCase().trim() === 'global');
690
- if (hasGlobalTag) {
691
- return;
692
- }
693
- const setTagResult = await forumChannel
694
- .setAvailableTags([...forumChannel.availableTags, { name: 'global' }], 'Ensure global memory tag')
695
- .catch((cause) => cause);
696
- if (setTagResult instanceof Error) {
697
- cliLogger.warn('Failed to add global memory tag:', setTagResult.message);
698
- return;
699
- }
700
- cliLogger.log(`Added global tag to memory forum: #${forumChannel.name}`);
701
- };
702
- const forumChannel = await ensureMemoryForumChannel({ guild, botName });
703
- await ensureGlobalMemoryTag({ forumChannel });
704
- const memoryDir = path.join(getDataDir(), 'memory');
705
- await upsertForumSyncConfig({
706
- appId,
707
- forumChannelId: forumChannel.id,
708
- outputDir: memoryDir,
709
- direction: 'bidirectional',
710
- });
711
- // Clean up stale configs left behind when the forum channel was deleted and recreated.
712
- // Without this, startConfiguredForumSync would iterate over the stale config first,
713
- // fail to resolve the deleted channel, and skip starting the watcher entirely.
714
- await deleteStaleForumSyncConfigs({
715
- appId,
716
- forumChannelId: forumChannel.id,
717
- outputDir: memoryDir,
718
- });
719
- cliLogger.log(`Memory forum sync configured: #${forumChannel.name} (${forumChannel.id})`);
720
- }
721
705
  /**
722
706
  * Background initialization for quick start mode.
723
707
  * Starts OpenCode server and registers slash commands without blocking bot startup.
@@ -753,11 +737,11 @@ async function backgroundInit({ currentDir, token, appId, }) {
753
737
  }
754
738
  catch (error) {
755
739
  cliLogger.error('Background init failed:', error instanceof Error ? error.message : String(error));
740
+ void notifyError(error, 'Background init failed');
756
741
  }
757
742
  }
758
743
  async function run({ restart, addChannels, useWorktrees, enableVoiceChannels, }) {
759
744
  startCaffeinate();
760
- const memoryEnabled = getMemoryEnabled();
761
745
  const forceSetup = Boolean(restart);
762
746
  // Step 0: Ensure required CLI tools are installed (OpenCode + Bun)
763
747
  await ensureCommandAvailable({
@@ -955,26 +939,6 @@ async function run({ restart, addChannels, useWorktrees, enableVoiceChannels, })
955
939
  })();
956
940
  // Background: OpenCode init + slash command registration (non-blocking)
957
941
  void backgroundInit({ currentDir, token, appId });
958
- if (memoryEnabled) {
959
- // Background: create memory forum channel + start forum sync
960
- void ensureMemoryForumSync({
961
- guilds,
962
- appId,
963
- botName: discordClient.user?.username,
964
- })
965
- .then(() => startConfiguredForumSync({ discordClient, appId }))
966
- .then((result) => {
967
- if (!result)
968
- return;
969
- cliLogger.warn(`Forum sync startup failed: ${result.message}`);
970
- })
971
- .catch((error) => {
972
- cliLogger.warn('Memory forum setup failed:', error instanceof Error ? error.message : String(error));
973
- });
974
- }
975
- else {
976
- cliLogger.log('Memory disabled (--memory not provided)');
977
- }
978
942
  showReadyMessage({ kimakiChannels: [], createdChannels, appId });
979
943
  outro('✨ Bot ready! Listening for messages...');
980
944
  return;
@@ -1132,26 +1096,6 @@ async function run({ restart, addChannels, useWorktrees, enableVoiceChannels, })
1132
1096
  cliLogger.log('Starting Discord bot...');
1133
1097
  await startDiscordBot({ token, appId, discordClient, useWorktrees });
1134
1098
  cliLogger.log('Discord bot is running!');
1135
- if (memoryEnabled) {
1136
- // Background: create memory forum channel + start forum sync
1137
- void ensureMemoryForumSync({
1138
- guilds,
1139
- appId,
1140
- botName: discordClient.user?.username,
1141
- })
1142
- .then(() => startConfiguredForumSync({ discordClient, appId }))
1143
- .then((result) => {
1144
- if (!result)
1145
- return;
1146
- cliLogger.warn(`Forum sync startup failed: ${result.message}`);
1147
- })
1148
- .catch((error) => {
1149
- cliLogger.warn('Memory forum setup failed:', error instanceof Error ? error.message : String(error));
1150
- });
1151
- }
1152
- else {
1153
- cliLogger.log('Memory disabled (--memory not provided)');
1154
- }
1155
1099
  showReadyMessage({ kimakiChannels, createdChannels, appId });
1156
1100
  outro('✨ Setup complete! Listening for new messages... do not close this process.');
1157
1101
  }
@@ -1165,10 +1109,10 @@ cli
1165
1109
  .option('--enable-voice-channels', 'Create voice channels for projects (disabled by default)')
1166
1110
  .option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text, text-and-essential-tools, or text-only)')
1167
1111
  .option('--mention-mode', 'Bot only responds when @mentioned (default for all channels)')
1168
- .option('--memory', 'Enable memory sync and persistent memory features')
1169
1112
  .option('--no-critique', 'Disable automatic diff upload to critique.work in system prompts')
1170
1113
  .option('--auto-restart', 'Automatically restart the bot on crash or OOM kill')
1171
1114
  .option('--verbose-opencode-server', 'Forward OpenCode server stdout/stderr to kimaki.log')
1115
+ .option('--no-sentry', 'Disable Sentry error reporting')
1172
1116
  .action(async (options) => {
1173
1117
  try {
1174
1118
  // Set data directory early, before any database access
@@ -1193,10 +1137,6 @@ cli
1193
1137
  setDefaultMentionMode(true);
1194
1138
  cliLogger.log('Default mention mode: enabled (bot only responds when @mentioned)');
1195
1139
  }
1196
- setMemoryEnabled(Boolean(options.memory));
1197
- if (options.memory) {
1198
- cliLogger.log('Memory enabled');
1199
- }
1200
1140
  if (options.noCritique) {
1201
1141
  setCritiqueEnabled(false);
1202
1142
  cliLogger.log('Critique disabled: diffs will not be auto-uploaded to critique.work');
@@ -1205,6 +1145,13 @@ cli
1205
1145
  setVerboseOpencodeServer(true);
1206
1146
  cliLogger.log('Verbose OpenCode server: stdout/stderr will be forwarded to kimaki.log');
1207
1147
  }
1148
+ if (options.noSentry) {
1149
+ process.env.KIMAKI_SENTRY_DISABLED = '1';
1150
+ cliLogger.log('Sentry error reporting disabled (--no-sentry)');
1151
+ }
1152
+ else {
1153
+ initSentry();
1154
+ }
1208
1155
  if (options.installUrl) {
1209
1156
  await initDatabase();
1210
1157
  const existingBot = await getBotToken();
@@ -2373,7 +2320,7 @@ cli
2373
2320
  : message.info.role === 'user'
2374
2321
  ? 'user'
2375
2322
  : 'message';
2376
- return message.parts.flatMap((part) => {
2323
+ return message.parts.filter((p) => !(p.type === 'text' && p.synthetic)).flatMap((part) => {
2377
2324
  return getPartSearchTexts(part).flatMap((text) => {
2378
2325
  const hit = findFirstSessionSearchHit({
2379
2326
  text,
@@ -4,6 +4,7 @@ import { getThreadSession } from '../database.js';
4
4
  import { initializeOpencodeForDirectory } from '../opencode.js';
5
5
  import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
6
  import { abortControllers } from '../session-handler.js';
7
+ import { SessionAbortError } from '../errors.js';
7
8
  import { createLogger, LogPrefix } from '../logger.js';
8
9
  const logger = createLogger(LogPrefix.ABORT);
9
10
  export async function handleAbortCommand({ command, }) {
@@ -49,7 +50,7 @@ export async function handleAbortCommand({ command, }) {
49
50
  const existingController = abortControllers.get(sessionId);
50
51
  if (existingController) {
51
52
  logger.log(`[ABORT] reason=user-requested sessionId=${sessionId} channelId=${channel.id} - user ran /abort command`);
52
- existingController.abort(new Error('User requested abort'));
53
+ existingController.abort(new SessionAbortError({ reason: 'user-requested' }));
53
54
  abortControllers.delete(sessionId);
54
55
  }
55
56
  const getClient = await initializeOpencodeForDirectory(projectDirectory);
@@ -6,6 +6,7 @@ import crypto from 'node:crypto';
6
6
  import { getThreadSession } from '../database.js';
7
7
  import { NOTIFY_MESSAGE_FLAGS, resolveWorkingDirectory, sendThreadMessage, } from '../discord-utils.js';
8
8
  import { createLogger } from '../logger.js';
9
+ import { notifyError } from '../sentry.js';
9
10
  import { abortControllers, addToQueue, handleOpencodeSession, } from '../session-handler.js';
10
11
  const logger = createLogger('ACT_BTN');
11
12
  const PENDING_TTL_MS = 24 * 60 * 60 * 1000;
@@ -233,6 +234,7 @@ export async function handleActionButton(interaction) {
233
234
  }
234
235
  catch (error) {
235
236
  logger.error('[ACTION] Failed to send click to model:', error);
237
+ void notifyError(error, 'Action button click send to model failed');
236
238
  await sendThreadMessage(thread, `Failed to send action click: ${error instanceof Error ? error.message : String(error)}`);
237
239
  }
238
240
  }
@@ -10,6 +10,7 @@ import crypto from 'node:crypto';
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
12
  import { createLogger, LogPrefix } from '../logger.js';
13
+ import { notifyError } from '../sentry.js';
13
14
  import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
14
15
  const logger = createLogger(LogPrefix.FILE_UPLOAD);
15
16
  // 5 minute TTL for pending contexts - if user doesn't click within this time,
@@ -225,6 +226,7 @@ export async function handleFileUploadModalSubmit(interaction) {
225
226
  // Ensure context is always resolved even on unexpected errors
226
227
  // so the plugin tool doesn't hang indefinitely
227
228
  logger.error('Error in file upload modal submit:', err);
229
+ void notifyError(err, 'File upload modal submit error');
228
230
  resolveContext(context, []);
229
231
  }
230
232
  }
@@ -75,9 +75,15 @@ export async function handleForkCommand(interaction) {
75
75
  return;
76
76
  }
77
77
  const recentMessages = userMessages.slice(-25);
78
- const options = recentMessages.map((m, index) => {
79
- const textPart = m.parts.find((p) => p.type === 'text');
80
- const preview = textPart?.text?.slice(0, 80) || '(no text)';
78
+ // Filter out synthetic parts (branch context, memory reminders, etc.)
79
+ // injected by the opencode plugin they clutter the dropdown preview.
80
+ const options = recentMessages
81
+ .map((m, index) => {
82
+ const textPart = m.parts.find((p) => p.type === 'text' && !p.synthetic);
83
+ if (!textPart?.text) {
84
+ return null;
85
+ }
86
+ const preview = textPart.text.slice(0, 80);
81
87
  const label = `${index + 1}. ${preview}${preview.length >= 80 ? '...' : ''}`;
82
88
  return {
83
89
  label: label.slice(0, 100),
@@ -86,7 +92,8 @@ export async function handleForkCommand(interaction) {
86
92
  .toLocaleString()
87
93
  .slice(0, 50),
88
94
  };
89
- });
95
+ })
96
+ .filter((o) => o !== null);
90
97
  const selectMenu = new StringSelectMenuBuilder()
91
98
  // Discord component custom_id max length is 100 chars.
92
99
  // Avoid embedding long directory paths (or base64 of them) in the custom ID.
@@ -4,6 +4,7 @@
4
4
  import {} from 'discord.js';
5
5
  import { getThreadWorktree, getThreadSession } from '../database.js';
6
6
  import { createLogger, LogPrefix } from '../logger.js';
7
+ import { notifyError } from '../sentry.js';
7
8
  import { mergeWorktree } from '../worktree-utils.js';
8
9
  import { sendThreadMessage, resolveWorkingDirectory } from '../discord-utils.js';
9
10
  import { handleOpencodeSession, abortControllers, addToQueue, } from '../session-handler.js';
@@ -62,6 +63,7 @@ async function sendPromptToModel({ prompt, thread, projectDirectory, command, ap
62
63
  appId,
63
64
  }).catch((e) => {
64
65
  logger.error(`[merge] Failed to send prompt to model:`, e);
66
+ void notifyError(e, 'Merge-worktree prompt send failed');
65
67
  sendThreadMessage(thread, `Failed to send prompt: ${(e instanceof Error ? e.message : String(e)).slice(0, 1900)}`).catch(() => { });
66
68
  });
67
69
  }
@@ -4,6 +4,7 @@ import { getThreadSession } from '../database.js';
4
4
  import { resolveWorkingDirectory, sendThreadMessage, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
5
5
  import { handleOpencodeSession, abortControllers, addToQueue, getQueueLength, clearQueue, } from '../session-handler.js';
6
6
  import { createLogger, LogPrefix } from '../logger.js';
7
+ import { notifyError } from '../sentry.js';
7
8
  import { registeredUserCommands } from '../config.js';
8
9
  const logger = createLogger(LogPrefix.QUEUE);
9
10
  export async function handleQueueCommand({ command, appId, }) {
@@ -67,6 +68,7 @@ export async function handleQueueCommand({ command, appId, }) {
67
68
  appId,
68
69
  }).catch(async (e) => {
69
70
  logger.error(`[QUEUE] Failed to send message:`, e);
71
+ void notifyError(e, 'Queue: failed to send message');
70
72
  const errorMsg = e instanceof Error ? e.message : String(e);
71
73
  await sendThreadMessage(channel, `✗ Failed: ${errorMsg.slice(0, 200)}`);
72
74
  });
@@ -200,6 +202,7 @@ export async function handleQueueCommandCommand({ command, appId, }) {
200
202
  appId,
201
203
  }).catch(async (e) => {
202
204
  logger.error(`[QUEUE] Failed to send command:`, e);
205
+ void notifyError(e, 'Queue: failed to send command');
203
206
  const errorMsg = e instanceof Error ? e.message : String(e);
204
207
  await sendThreadMessage(channel, `Failed: ${errorMsg.slice(0, 200)}`);
205
208
  });
@@ -7,6 +7,7 @@ import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils
7
7
  import { createLogger, LogPrefix } from '../logger.js';
8
8
  import { getAllThreadSessionIds, getThreadIdBySessionId } from '../database.js';
9
9
  import { abortControllers } from '../session-handler.js';
10
+ import { SessionAbortError } from '../errors.js';
10
11
  import * as errore from 'errore';
11
12
  const logger = createLogger(LogPrefix.OPENCODE);
12
13
  export async function handleRestartOpencodeServerCommand({ command, appId, }) {
@@ -82,7 +83,7 @@ export async function handleRestartOpencodeServerCommand({ command, appId, }) {
82
83
  const controller = abortControllers.get(sessionId);
83
84
  if (controller) {
84
85
  logger.log(`[RESTART] Aborting session ${sessionId} in thread ${threadId}`);
85
- controller.abort(new Error('Server restart requested'));
86
+ controller.abort(new SessionAbortError({ reason: 'server-restart' }));
86
87
  abortControllers.delete(sessionId);
87
88
  abortedCount++;
88
89
  }
@@ -5,12 +5,17 @@ import { handleOpencodeSession } from '../session-handler.js';
5
5
  import { sendThreadMessage, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
6
6
  import { createLogger, LogPrefix } from '../logger.js';
7
7
  import { getChannelDirectory, getThreadSession } from '../database.js';
8
+ import { registeredUserCommands } from '../config.js';
8
9
  import fs from 'node:fs';
9
10
  const userCommandLogger = createLogger(LogPrefix.USER_CMD);
10
11
  export const handleUserCommand = async ({ command, appId, }) => {
11
12
  const discordCommandName = command.commandName;
12
- // Strip the -cmd suffix to get the actual OpenCode command name
13
- const commandName = discordCommandName.replace(/-cmd$/, '');
13
+ // Look up the original OpenCode command name from the mapping populated at registration.
14
+ // The sanitized Discord name is lossy (e.g. foo:bar → foo-bar), so stripping -cmd
15
+ // would give the wrong name for commands with special characters.
16
+ const sanitizedBase = discordCommandName.replace(/-cmd$/, '');
17
+ const registered = registeredUserCommands.find((c) => c.discordName === sanitizedBase);
18
+ const commandName = registered?.name || sanitizedBase;
14
19
  const args = command.options.getString('arguments') || '';
15
20
  userCommandLogger.log(`Executing /${commandName} (from /${discordCommandName}) argsLength=${args.length}`);
16
21
  const channel = command.channel;
@@ -6,6 +6,7 @@ import fs from 'node:fs';
6
6
  import { createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelDirectory, getThreadWorktree, } from '../database.js';
7
7
  import { SILENT_MESSAGE_FLAGS, reactToThread } from '../discord-utils.js';
8
8
  import { createLogger, LogPrefix } from '../logger.js';
9
+ import { notifyError } from '../sentry.js';
9
10
  import { createWorktreeWithSubmodules, captureGitDiff, execAsync, } from '../worktree-utils.js';
10
11
  import { WORKTREE_PREFIX } from './merge-worktree.js';
11
12
  import * as errore from 'errore';
@@ -221,6 +222,7 @@ export async function handleNewWorktreeCommand({ command, appId, }) {
221
222
  rest: command.client.rest,
222
223
  }).catch((e) => {
223
224
  logger.error('[NEW-WORKTREE] Background error:', e);
225
+ void notifyError(e, 'Background worktree creation failed');
224
226
  });
225
227
  }
226
228
  /**
@@ -292,5 +294,6 @@ async function handleWorktreeInThread({ command, appId, thread, }) {
292
294
  rest: command.client.rest,
293
295
  }).catch((e) => {
294
296
  logger.error('[NEW-WORKTREE] Background error:', e);
297
+ void notifyError(e, 'Background worktree creation failed (in-thread)');
295
298
  });
296
299
  }
package/dist/config.js CHANGED
@@ -77,15 +77,6 @@ export function getVerboseOpencodeServer() {
77
77
  export function setVerboseOpencodeServer(enabled) {
78
78
  verboseOpencodeServer = enabled;
79
79
  }
80
- // Whether memory sync/instructions are enabled.
81
- // Disabled by default; enabled via --memory CLI flag.
82
- let memoryEnabled = false;
83
- export function getMemoryEnabled() {
84
- return memoryEnabled;
85
- }
86
- export function setMemoryEnabled(enabled) {
87
- memoryEnabled = enabled;
88
- }
89
80
  export const registeredUserCommands = [];
90
81
  const DEFAULT_LOCK_PORT = 29988;
91
82
  /**