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.
- package/dist/channel-management.js +0 -22
- package/dist/cli.js +49 -102
- package/dist/commands/abort.js +2 -1
- package/dist/commands/action-buttons.js +2 -0
- package/dist/commands/file-upload.js +2 -0
- package/dist/commands/fork.js +11 -4
- package/dist/commands/merge-worktree.js +2 -0
- package/dist/commands/queue.js +3 -0
- package/dist/commands/restart-opencode-server.js +2 -1
- package/dist/commands/user-command.js +7 -2
- package/dist/commands/worktree.js +3 -0
- package/dist/config.js +0 -9
- package/dist/discord-bot.js +165 -110
- package/dist/errors.js +12 -1
- package/dist/forum-sync/watchers.js +1 -3
- package/dist/genai-worker-wrapper.js +2 -0
- package/dist/genai-worker.js +14 -3
- package/dist/interaction-handler.js +2 -0
- package/dist/ipc-polling.js +8 -1
- package/dist/kimaki-digital-twin.e2e.test.js +3 -6
- package/dist/logger.js +15 -0
- package/dist/markdown.js +13 -3
- package/dist/message-formatting.js +12 -1
- package/dist/opencode-plugin.js +69 -36
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +20 -15
- package/dist/sentry.js +74 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler/thread-session-runtime.js +73 -0
- package/dist/session-handler.js +184 -42
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +50 -73
- package/dist/task-runner.js +2 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/utils.js +11 -10
- package/dist/voice-handler.js +11 -0
- package/dist/voice.js +7 -2
- package/dist/worktree-utils.js +19 -12
- package/package.json +5 -2
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +7 -7
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/channel-management.ts +0 -33
- package/src/cli.ts +50 -137
- package/src/commands/abort.ts +2 -1
- package/src/commands/action-buttons.ts +2 -0
- package/src/commands/file-upload.ts +2 -0
- package/src/commands/fork.ts +31 -22
- package/src/commands/merge-worktree.ts +2 -0
- package/src/commands/queue.ts +3 -0
- package/src/commands/restart-opencode-server.ts +2 -1
- package/src/commands/user-command.ts +9 -2
- package/src/commands/worktree.ts +3 -0
- package/src/config.ts +7 -13
- package/src/discord-bot.ts +197 -131
- package/src/errors.ts +13 -1
- package/src/forum-sync/watchers.ts +1 -3
- package/src/genai-worker-wrapper.ts +5 -0
- package/src/genai-worker.ts +19 -6
- package/src/interaction-handler.ts +2 -0
- package/src/ipc-polling.ts +8 -1
- package/src/kimaki-digital-twin.e2e.test.ts +3 -6
- package/src/logger.ts +16 -0
- package/src/markdown.ts +20 -3
- package/src/message-formatting.ts +13 -1
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +73 -36
- package/src/opencode.ts +21 -15
- package/src/sentry.ts +83 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +255 -51
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +57 -72
- package/src/task-runner.ts +2 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/utils.ts +10 -13
- package/src/voice-handler.ts +24 -9
- package/src/voice.ts +7 -2
- 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,
|
|
9
|
-
import { getBotToken, setBotToken, setChannelDirectory, findChannelsByDirectory, findChannelByAppId, getThreadSession, getThreadIdBySessionId, getPrisma,
|
|
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,
|
|
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
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
const
|
|
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({
|
|
564
|
+
registeredUserCommands.push({
|
|
565
|
+
name: cmd.name,
|
|
566
|
+
discordName: baseName,
|
|
567
|
+
description,
|
|
568
|
+
});
|
|
551
569
|
commands.push(new SlashCommandBuilder()
|
|
552
|
-
.setName(commandName
|
|
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
|
-
|
|
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
|
|
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,
|
package/dist/commands/abort.js
CHANGED
|
@@ -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
|
|
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
|
}
|
package/dist/commands/fork.js
CHANGED
|
@@ -75,9 +75,15 @@ export async function handleForkCommand(interaction) {
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
const recentMessages = userMessages.slice(-25);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
}
|
package/dist/commands/queue.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
13
|
-
|
|
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
|
/**
|