disunday 1.0.0

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 (83) hide show
  1. package/dist/ai-tool-to-genai.js +208 -0
  2. package/dist/ai-tool-to-genai.test.js +267 -0
  3. package/dist/channel-management.js +96 -0
  4. package/dist/cli.js +1674 -0
  5. package/dist/commands/abort.js +89 -0
  6. package/dist/commands/add-project.js +117 -0
  7. package/dist/commands/agent.js +250 -0
  8. package/dist/commands/ask-question.js +219 -0
  9. package/dist/commands/compact.js +126 -0
  10. package/dist/commands/context-menu.js +171 -0
  11. package/dist/commands/context.js +89 -0
  12. package/dist/commands/cost.js +93 -0
  13. package/dist/commands/create-new-project.js +111 -0
  14. package/dist/commands/diff.js +77 -0
  15. package/dist/commands/export.js +100 -0
  16. package/dist/commands/files.js +73 -0
  17. package/dist/commands/fork.js +199 -0
  18. package/dist/commands/help.js +54 -0
  19. package/dist/commands/login.js +488 -0
  20. package/dist/commands/merge-worktree.js +165 -0
  21. package/dist/commands/model.js +325 -0
  22. package/dist/commands/permissions.js +140 -0
  23. package/dist/commands/ping.js +13 -0
  24. package/dist/commands/queue.js +133 -0
  25. package/dist/commands/remove-project.js +119 -0
  26. package/dist/commands/rename.js +70 -0
  27. package/dist/commands/restart-opencode-server.js +77 -0
  28. package/dist/commands/resume.js +276 -0
  29. package/dist/commands/run-config.js +79 -0
  30. package/dist/commands/run.js +240 -0
  31. package/dist/commands/schedule.js +170 -0
  32. package/dist/commands/session-info.js +58 -0
  33. package/dist/commands/session.js +191 -0
  34. package/dist/commands/settings.js +84 -0
  35. package/dist/commands/share.js +89 -0
  36. package/dist/commands/status.js +79 -0
  37. package/dist/commands/sync.js +119 -0
  38. package/dist/commands/theme.js +53 -0
  39. package/dist/commands/types.js +2 -0
  40. package/dist/commands/undo-redo.js +170 -0
  41. package/dist/commands/user-command.js +135 -0
  42. package/dist/commands/verbosity.js +59 -0
  43. package/dist/commands/worktree-settings.js +50 -0
  44. package/dist/commands/worktree.js +288 -0
  45. package/dist/config.js +139 -0
  46. package/dist/database.js +585 -0
  47. package/dist/discord-bot.js +700 -0
  48. package/dist/discord-utils.js +336 -0
  49. package/dist/discord-utils.test.js +20 -0
  50. package/dist/errors.js +193 -0
  51. package/dist/escape-backticks.test.js +429 -0
  52. package/dist/format-tables.js +96 -0
  53. package/dist/format-tables.test.js +418 -0
  54. package/dist/genai-worker-wrapper.js +109 -0
  55. package/dist/genai-worker.js +299 -0
  56. package/dist/genai.js +230 -0
  57. package/dist/image-utils.js +107 -0
  58. package/dist/interaction-handler.js +289 -0
  59. package/dist/limit-heading-depth.js +25 -0
  60. package/dist/limit-heading-depth.test.js +105 -0
  61. package/dist/logger.js +111 -0
  62. package/dist/markdown.js +323 -0
  63. package/dist/markdown.test.js +269 -0
  64. package/dist/message-formatting.js +447 -0
  65. package/dist/message-formatting.test.js +73 -0
  66. package/dist/openai-realtime.js +226 -0
  67. package/dist/opencode.js +224 -0
  68. package/dist/reaction-handler.js +128 -0
  69. package/dist/scheduler.js +93 -0
  70. package/dist/security.js +200 -0
  71. package/dist/session-handler.js +1436 -0
  72. package/dist/system-message.js +138 -0
  73. package/dist/tools.js +354 -0
  74. package/dist/unnest-code-blocks.js +117 -0
  75. package/dist/unnest-code-blocks.test.js +432 -0
  76. package/dist/utils.js +95 -0
  77. package/dist/voice-handler.js +569 -0
  78. package/dist/voice.js +344 -0
  79. package/dist/worker-types.js +4 -0
  80. package/dist/worktree-utils.js +134 -0
  81. package/dist/xml.js +90 -0
  82. package/dist/xml.test.js +32 -0
  83. package/package.json +84 -0
@@ -0,0 +1,119 @@
1
+ // /remove-project command - Remove Discord channels for a project.
2
+ import path from 'node:path';
3
+ import * as errore from 'errore';
4
+ import { getDatabase } from '../database.js';
5
+ import { createLogger, LogPrefix } from '../logger.js';
6
+ import { abbreviatePath } from '../utils.js';
7
+ const logger = createLogger(LogPrefix.REMOVE_PROJECT);
8
+ export async function handleRemoveProjectCommand({ command, appId }) {
9
+ await command.deferReply({ ephemeral: false });
10
+ const directory = command.options.getString('project', true);
11
+ const guild = command.guild;
12
+ if (!guild) {
13
+ await command.editReply('This command can only be used in a guild');
14
+ return;
15
+ }
16
+ try {
17
+ const db = getDatabase();
18
+ // Get channel IDs for this directory
19
+ const channels = db
20
+ .prepare('SELECT channel_id, channel_type FROM channel_directories WHERE directory = ?')
21
+ .all(directory);
22
+ if (channels.length === 0) {
23
+ await command.editReply(`No channels found for directory: \`${directory}\``);
24
+ return;
25
+ }
26
+ const deletedChannels = [];
27
+ const failedChannels = [];
28
+ for (const { channel_id, channel_type } of channels) {
29
+ const channel = await errore.tryAsync({
30
+ try: () => guild.channels.fetch(channel_id),
31
+ catch: (e) => e,
32
+ });
33
+ if (channel instanceof Error) {
34
+ logger.error(`Failed to fetch channel ${channel_id}:`, channel);
35
+ failedChannels.push(`${channel_type}: ${channel_id}`);
36
+ continue;
37
+ }
38
+ if (channel) {
39
+ try {
40
+ await channel.delete(`Removed by /remove-project command`);
41
+ deletedChannels.push(`${channel_type}: ${channel_id}`);
42
+ }
43
+ catch (error) {
44
+ logger.error(`Failed to delete channel ${channel_id}:`, error);
45
+ failedChannels.push(`${channel_type}: ${channel_id}`);
46
+ }
47
+ }
48
+ else {
49
+ deletedChannels.push(`${channel_type}: ${channel_id} (already deleted)`);
50
+ }
51
+ }
52
+ // Remove from database
53
+ db.prepare('DELETE FROM channel_directories WHERE directory = ?').run(directory);
54
+ const projectName = path.basename(directory);
55
+ let message = `Removed project **${projectName}**\n`;
56
+ message += `Directory: \`${directory}\`\n\n`;
57
+ if (deletedChannels.length > 0) {
58
+ message += `Deleted channels:\n${deletedChannels.map((c) => `- ${c}`).join('\n')}`;
59
+ }
60
+ if (failedChannels.length > 0) {
61
+ message += `\n\nFailed to delete (may be in another server):\n${failedChannels.map((c) => `- ${c}`).join('\n')}`;
62
+ }
63
+ await command.editReply(message);
64
+ logger.log(`Removed project ${projectName} at ${directory}`);
65
+ }
66
+ catch (error) {
67
+ logger.error('[REMOVE-PROJECT] Error:', error);
68
+ await command.editReply(`Failed to remove project: ${error instanceof Error ? error.message : 'Unknown error'}`);
69
+ }
70
+ }
71
+ export async function handleRemoveProjectAutocomplete({ interaction, appId, }) {
72
+ const focusedValue = interaction.options.getFocused();
73
+ const guild = interaction.guild;
74
+ if (!guild) {
75
+ await interaction.respond([]);
76
+ return;
77
+ }
78
+ try {
79
+ const db = getDatabase();
80
+ // Get all directories with channels
81
+ const allChannels = db
82
+ .prepare('SELECT DISTINCT directory, channel_id FROM channel_directories WHERE channel_type = ?')
83
+ .all('text');
84
+ // Filter to only channels that exist in this guild
85
+ const projectsInGuild = [];
86
+ for (const { directory, channel_id } of allChannels) {
87
+ const channel = await errore.tryAsync({
88
+ try: () => guild.channels.fetch(channel_id),
89
+ catch: (e) => e,
90
+ });
91
+ if (channel instanceof Error) {
92
+ // Channel not in this guild, skip
93
+ continue;
94
+ }
95
+ if (channel) {
96
+ projectsInGuild.push({ directory, channelId: channel_id });
97
+ }
98
+ }
99
+ const projects = projectsInGuild
100
+ .filter(({ directory }) => {
101
+ const baseName = path.basename(directory);
102
+ const searchText = `${baseName} ${directory}`.toLowerCase();
103
+ return searchText.includes(focusedValue.toLowerCase());
104
+ })
105
+ .slice(0, 25)
106
+ .map(({ directory }) => {
107
+ const name = `${path.basename(directory)} (${abbreviatePath(directory)})`;
108
+ return {
109
+ name: name.length > 100 ? name.slice(0, 99) + '...' : name,
110
+ value: directory,
111
+ };
112
+ });
113
+ await interaction.respond(projects);
114
+ }
115
+ catch (error) {
116
+ logger.error('[AUTOCOMPLETE] Error fetching projects:', error);
117
+ await interaction.respond([]);
118
+ }
119
+ }
@@ -0,0 +1,70 @@
1
+ import { ChannelType } from 'discord.js';
2
+ import { getDatabase, getThreadWorktree } from '../database.js';
3
+ import { initializeOpencodeForDirectory } from '../opencode.js';
4
+ import { resolveTextChannel, getDisundayMetadata, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
5
+ import { createLogger, LogPrefix } from '../logger.js';
6
+ const logger = createLogger(LogPrefix.SESSION);
7
+ export async function handleRenameCommand({ command, }) {
8
+ const newTitle = command.options.getString('title', true);
9
+ const channel = command.channel;
10
+ if (!channel) {
11
+ await command.reply({
12
+ content: 'This command can only be used in a channel',
13
+ ephemeral: true,
14
+ flags: SILENT_MESSAGE_FLAGS,
15
+ });
16
+ return;
17
+ }
18
+ const isThread = [
19
+ ChannelType.PublicThread,
20
+ ChannelType.PrivateThread,
21
+ ChannelType.AnnouncementThread,
22
+ ].includes(channel.type);
23
+ if (!isThread) {
24
+ await command.reply({
25
+ content: 'This command can only be used in a thread with an active session',
26
+ ephemeral: true,
27
+ flags: SILENT_MESSAGE_FLAGS,
28
+ });
29
+ return;
30
+ }
31
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
32
+ const thread = channel;
33
+ const textChannel = await resolveTextChannel(thread);
34
+ const { projectDirectory: directory } = getDisundayMetadata(textChannel);
35
+ if (!directory) {
36
+ await command.editReply('Could not determine project directory for this channel');
37
+ return;
38
+ }
39
+ const row = getDatabase()
40
+ .prepare('SELECT session_id FROM thread_sessions WHERE thread_id = ?')
41
+ .get(channel.id);
42
+ if (!row?.session_id) {
43
+ await command.editReply('No active session in this thread');
44
+ return;
45
+ }
46
+ const sessionId = row.session_id;
47
+ const worktreeInfo = getThreadWorktree(channel.id);
48
+ const sdkDirectory = worktreeInfo?.status === 'ready' && worktreeInfo.worktree_directory
49
+ ? worktreeInfo.worktree_directory
50
+ : directory;
51
+ try {
52
+ const getClient = await initializeOpencodeForDirectory(directory);
53
+ if (getClient instanceof Error) {
54
+ await command.editReply(`Failed to rename: ${getClient.message}`);
55
+ return;
56
+ }
57
+ await getClient().session.update({
58
+ path: { id: sessionId },
59
+ body: { title: newTitle },
60
+ query: { directory: sdkDirectory },
61
+ });
62
+ await thread.setName(newTitle.slice(0, 100)).catch(() => { });
63
+ await command.editReply(`✅ Session renamed to: **${newTitle}**`);
64
+ logger.log(`[RENAME] Session ${sessionId} renamed to "${newTitle}"`);
65
+ }
66
+ catch (error) {
67
+ logger.error('[RENAME] Error:', error);
68
+ await command.editReply(`Failed to rename: ${error instanceof Error ? error.message : 'Unknown error'}`);
69
+ }
70
+ }
@@ -0,0 +1,77 @@
1
+ // /restart-opencode-server command - Restart the opencode server for the current channel.
2
+ // Used for resolving opencode state issues, internal bugs, refreshing auth state, plugins, etc.
3
+ import { ChannelType } from 'discord.js';
4
+ import { restartOpencodeServer } from '../opencode.js';
5
+ import { resolveTextChannel, getDisundayMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
6
+ import { createLogger, LogPrefix } from '../logger.js';
7
+ const logger = createLogger(LogPrefix.OPENCODE);
8
+ export async function handleRestartOpencodeServerCommand({ command, appId }) {
9
+ const channel = command.channel;
10
+ if (!channel) {
11
+ await command.reply({
12
+ content: 'This command can only be used in a channel',
13
+ ephemeral: true,
14
+ flags: SILENT_MESSAGE_FLAGS,
15
+ });
16
+ return;
17
+ }
18
+ const isThread = [
19
+ ChannelType.PublicThread,
20
+ ChannelType.PrivateThread,
21
+ ChannelType.AnnouncementThread,
22
+ ].includes(channel.type);
23
+ let projectDirectory;
24
+ let channelAppId;
25
+ if (isThread) {
26
+ const thread = channel;
27
+ const textChannel = await resolveTextChannel(thread);
28
+ const metadata = getDisundayMetadata(textChannel);
29
+ projectDirectory = metadata.projectDirectory;
30
+ channelAppId = metadata.channelAppId;
31
+ }
32
+ else if (channel.type === ChannelType.GuildText) {
33
+ const textChannel = channel;
34
+ const metadata = getDisundayMetadata(textChannel);
35
+ projectDirectory = metadata.projectDirectory;
36
+ channelAppId = metadata.channelAppId;
37
+ }
38
+ else {
39
+ await command.reply({
40
+ content: 'This command can only be used in text channels or threads',
41
+ ephemeral: true,
42
+ flags: SILENT_MESSAGE_FLAGS,
43
+ });
44
+ return;
45
+ }
46
+ if (channelAppId && channelAppId !== appId) {
47
+ await command.reply({
48
+ content: 'This channel is not configured for this bot',
49
+ ephemeral: true,
50
+ flags: SILENT_MESSAGE_FLAGS,
51
+ });
52
+ return;
53
+ }
54
+ if (!projectDirectory) {
55
+ await command.reply({
56
+ content: 'Could not determine project directory for this channel',
57
+ ephemeral: true,
58
+ flags: SILENT_MESSAGE_FLAGS,
59
+ });
60
+ return;
61
+ }
62
+ // Defer reply since restart may take a moment
63
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
64
+ logger.log(`[RESTART] Restarting opencode server for directory: ${projectDirectory}`);
65
+ const result = await restartOpencodeServer(projectDirectory);
66
+ if (result instanceof Error) {
67
+ logger.error('[RESTART] Failed:', result);
68
+ await command.editReply({
69
+ content: `Failed to restart opencode server: ${result.message}`,
70
+ });
71
+ return;
72
+ }
73
+ await command.editReply({
74
+ content: `🔄 Opencode server **restarted** successfully`,
75
+ });
76
+ logger.log(`[RESTART] Opencode server restarted for directory: ${projectDirectory}`);
77
+ }
@@ -0,0 +1,276 @@
1
+ import { ChannelType, ThreadAutoArchiveDuration, } from 'discord.js';
2
+ import fs from 'node:fs';
3
+ import { getDatabase, getChannelDirectory } from '../database.js';
4
+ import { initializeOpencodeForDirectory } from '../opencode.js';
5
+ import { sendThreadMessage, resolveTextChannel, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
6
+ import { collectLastAssistantParts } from '../message-formatting.js';
7
+ import { createLogger, LogPrefix } from '../logger.js';
8
+ import * as errore from 'errore';
9
+ const logger = createLogger(LogPrefix.RESUME);
10
+ const sessionCache = new Map();
11
+ const CACHE_TTL = 30_000;
12
+ export async function refreshSessionCache(projectDirectory) {
13
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
14
+ if (getClient instanceof Error) {
15
+ return [];
16
+ }
17
+ try {
18
+ const response = await getClient().session.list({
19
+ query: { directory: projectDirectory },
20
+ });
21
+ const sessions = (response.data || []).map((s) => ({
22
+ id: s.id,
23
+ title: s.title,
24
+ updated: new Date(s.time.updated).getTime(),
25
+ }));
26
+ sessionCache.set(projectDirectory, { sessions, timestamp: Date.now() });
27
+ return sessions;
28
+ }
29
+ catch {
30
+ return sessionCache.get(projectDirectory)?.sessions || [];
31
+ }
32
+ }
33
+ export async function handleResumeCommand({ command, appId, }) {
34
+ await command.deferReply({ ephemeral: false });
35
+ const sessionId = command.options.getString('session', true);
36
+ if (sessionId === '__refresh__') {
37
+ const channelConfig = getChannelDirectory(command.channelId);
38
+ if (channelConfig?.directory) {
39
+ await refreshSessionCache(channelConfig.directory);
40
+ }
41
+ await command.editReply('🔄 Session list refreshed! Run `/resume` again to see updated list.');
42
+ return;
43
+ }
44
+ if (sessionId.startsWith('__header_')) {
45
+ await command.editReply('Please select a session, not a header.');
46
+ return;
47
+ }
48
+ const channel = command.channel;
49
+ const isThread = channel &&
50
+ [
51
+ ChannelType.PublicThread,
52
+ ChannelType.PrivateThread,
53
+ ChannelType.AnnouncementThread,
54
+ ].includes(channel.type);
55
+ if (isThread) {
56
+ await command.editReply('This command can only be used in project channels, not threads');
57
+ return;
58
+ }
59
+ if (!channel || channel.type !== ChannelType.GuildText) {
60
+ await command.editReply('This command can only be used in text channels');
61
+ return;
62
+ }
63
+ const textChannel = channel;
64
+ const channelConfig = getChannelDirectory(textChannel.id);
65
+ const projectDirectory = channelConfig?.directory;
66
+ const channelAppId = channelConfig?.appId || undefined;
67
+ if (channelAppId && channelAppId !== appId) {
68
+ await command.editReply('This channel is not configured for this bot');
69
+ return;
70
+ }
71
+ if (!projectDirectory) {
72
+ await command.editReply('This channel is not configured with a project directory');
73
+ return;
74
+ }
75
+ if (!fs.existsSync(projectDirectory)) {
76
+ await command.editReply(`Directory does not exist: ${projectDirectory}`);
77
+ return;
78
+ }
79
+ try {
80
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
81
+ if (getClient instanceof Error) {
82
+ await command.editReply(getClient.message);
83
+ return;
84
+ }
85
+ const sessionResponse = await getClient().session.get({
86
+ path: { id: sessionId },
87
+ });
88
+ if (!sessionResponse.data) {
89
+ await command.editReply(`Session not found: \`${sessionId}\`\n\nMake sure the session ID is correct. You can find it with \`opencode session list\` in terminal.`);
90
+ return;
91
+ }
92
+ const sessionTitle = sessionResponse.data.title;
93
+ const thread = await textChannel.threads.create({
94
+ name: `Resume: ${sessionTitle}`.slice(0, 100),
95
+ autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
96
+ reason: `Resuming session ${sessionId}`,
97
+ });
98
+ await thread.members.add(command.user.id);
99
+ getDatabase()
100
+ .prepare('INSERT OR REPLACE INTO thread_sessions (thread_id, session_id) VALUES (?, ?)')
101
+ .run(thread.id, sessionId);
102
+ logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`);
103
+ const terminalCmd = `opencode -s ${sessionId} ${projectDirectory}`;
104
+ const sessionInfoContent = `📋 **Session Info**\n**ID:** \`${sessionId}\`\n**Terminal:**\n\`\`\`\n${terminalCmd}\n\`\`\``;
105
+ const infoMessage = await sendThreadMessage(thread, sessionInfoContent);
106
+ await infoMessage.pin().catch(() => { });
107
+ const messagesResponse = await getClient().session.messages({
108
+ path: { id: sessionId },
109
+ });
110
+ if (!messagesResponse.data) {
111
+ throw new Error('Failed to fetch session messages');
112
+ }
113
+ const messages = messagesResponse.data;
114
+ await command.editReply(`Resumed session "${sessionTitle}" in ${thread.toString()}`);
115
+ await sendThreadMessage(thread, `📂 **Resumed session:** ${sessionTitle}\n📅 **Created:** ${new Date(sessionResponse.data.time.created).toLocaleString()}\n\n*Loading ${messages.length} messages...*`);
116
+ const { partIds, content, skippedCount } = collectLastAssistantParts({
117
+ messages,
118
+ });
119
+ if (skippedCount > 0) {
120
+ await sendThreadMessage(thread, `*Skipped ${skippedCount} older assistant parts...*`);
121
+ }
122
+ if (content.trim()) {
123
+ const discordMessage = await sendThreadMessage(thread, content);
124
+ const stmt = getDatabase().prepare('INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)');
125
+ const transaction = getDatabase().transaction((ids) => {
126
+ for (const partId of ids) {
127
+ stmt.run(partId, discordMessage.id, thread.id);
128
+ }
129
+ });
130
+ transaction(partIds);
131
+ }
132
+ const messageCount = messages.length;
133
+ await sendThreadMessage(thread, `✅ **Session resumed!** Loaded ${messageCount} messages.\n\nYou can now continue the conversation by sending messages in this thread.`);
134
+ }
135
+ catch (error) {
136
+ logger.error('[RESUME] Error:', error);
137
+ await command.editReply(`Failed to resume session: ${error instanceof Error ? error.message : 'Unknown error'}`);
138
+ }
139
+ }
140
+ function getDateGroup(timestamp) {
141
+ const now = new Date();
142
+ const date = new Date(timestamp);
143
+ const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
144
+ const yesterdayStart = new Date(todayStart.getTime() - 24 * 60 * 60 * 1000);
145
+ const weekStart = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000);
146
+ if (date >= todayStart) {
147
+ return 'today';
148
+ }
149
+ if (date >= yesterdayStart) {
150
+ return 'yesterday';
151
+ }
152
+ if (date >= weekStart) {
153
+ return 'thisWeek';
154
+ }
155
+ return 'older';
156
+ }
157
+ function formatRelativeTime(timestamp) {
158
+ const now = Date.now();
159
+ const diff = now - timestamp;
160
+ const minutes = Math.floor(diff / 60000);
161
+ const hours = Math.floor(diff / 3600000);
162
+ if (minutes < 1) {
163
+ return 'just now';
164
+ }
165
+ if (minutes < 60) {
166
+ return `${minutes}m ago`;
167
+ }
168
+ if (hours < 24) {
169
+ return `${hours}h ago`;
170
+ }
171
+ const date = new Date(timestamp);
172
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
173
+ }
174
+ const GROUP_LABELS = {
175
+ today: '📅 Today',
176
+ yesterday: '📆 Yesterday',
177
+ thisWeek: '🗓️ This Week',
178
+ older: '📁 Older',
179
+ };
180
+ export async function handleResumeAutocomplete({ interaction, appId, }) {
181
+ const focusedValue = interaction.options.getFocused();
182
+ let projectDirectory;
183
+ if (interaction.channel) {
184
+ const textChannel = await resolveTextChannel(interaction.channel);
185
+ if (textChannel) {
186
+ const channelConfig = getChannelDirectory(textChannel.id);
187
+ if (channelConfig?.appId && channelConfig.appId !== appId) {
188
+ await interaction.respond([]);
189
+ return;
190
+ }
191
+ projectDirectory = channelConfig?.directory;
192
+ }
193
+ }
194
+ if (!projectDirectory) {
195
+ await interaction.respond([]);
196
+ return;
197
+ }
198
+ try {
199
+ const cached = sessionCache.get(projectDirectory);
200
+ let sessions;
201
+ if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
202
+ sessions = cached.sessions;
203
+ }
204
+ else {
205
+ const timeoutPromise = new Promise((resolve) => {
206
+ setTimeout(() => {
207
+ resolve(cached?.sessions || []);
208
+ }, 2000);
209
+ });
210
+ sessions = await Promise.race([
211
+ refreshSessionCache(projectDirectory),
212
+ timeoutPromise,
213
+ ]);
214
+ }
215
+ const filtered = sessions
216
+ .filter((session) => {
217
+ return session.title.toLowerCase().includes(focusedValue.toLowerCase());
218
+ })
219
+ .sort((a, b) => {
220
+ return b.updated - a.updated;
221
+ });
222
+ const grouped = new Map();
223
+ for (const session of filtered) {
224
+ const group = getDateGroup(session.updated);
225
+ if (!grouped.has(group)) {
226
+ grouped.set(group, []);
227
+ }
228
+ grouped.get(group).push(session);
229
+ }
230
+ const choices = [
231
+ {
232
+ name: '🔄 Refresh list (type session ID if not shown)',
233
+ value: '__refresh__',
234
+ },
235
+ ];
236
+ const groupOrder = ['today', 'yesterday', 'thisWeek', 'older'];
237
+ let totalItems = 1;
238
+ for (const group of groupOrder) {
239
+ const groupSessions = grouped.get(group);
240
+ if (!groupSessions || groupSessions.length === 0) {
241
+ continue;
242
+ }
243
+ if (totalItems >= 25) {
244
+ break;
245
+ }
246
+ choices.push({
247
+ name: `── ${GROUP_LABELS[group]} ──`,
248
+ value: `__header_${group}__`,
249
+ });
250
+ totalItems++;
251
+ for (const session of groupSessions) {
252
+ if (totalItems >= 25) {
253
+ break;
254
+ }
255
+ const timeStr = formatRelativeTime(session.updated);
256
+ const prefix = ' ';
257
+ const suffix = ` (${timeStr})`;
258
+ const maxTitleLength = 100 - prefix.length - suffix.length;
259
+ let title = session.title;
260
+ if (title.length > maxTitleLength) {
261
+ title = title.slice(0, Math.max(0, maxTitleLength - 1)) + '…';
262
+ }
263
+ choices.push({
264
+ name: `${prefix}${title}${suffix}`,
265
+ value: session.id,
266
+ });
267
+ totalItems++;
268
+ }
269
+ }
270
+ await interaction.respond(choices);
271
+ }
272
+ catch (error) {
273
+ logger.error('[AUTOCOMPLETE] Error fetching sessions:', error);
274
+ await interaction.respond([]).catch(() => { });
275
+ }
276
+ }
@@ -0,0 +1,79 @@
1
+ // /run-config command - Configure notification settings for /run command.
2
+ import { ChannelType } from 'discord.js';
3
+ import { getRunConfig, setRunConfig } from '../database.js';
4
+ import { resolveTextChannel, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
5
+ export async function handleRunConfigCommand({ command, }) {
6
+ const channel = command.channel;
7
+ if (!channel) {
8
+ await command.reply({
9
+ content: 'This command can only be used in a channel',
10
+ ephemeral: true,
11
+ });
12
+ return;
13
+ }
14
+ const subcommand = command.options.getSubcommand();
15
+ const isThread = [
16
+ ChannelType.PublicThread,
17
+ ChannelType.PrivateThread,
18
+ ChannelType.AnnouncementThread,
19
+ ].includes(channel.type);
20
+ let channelId = channel.id;
21
+ if (isThread) {
22
+ const textChannel = await resolveTextChannel(channel);
23
+ if (textChannel) {
24
+ channelId = textChannel.id;
25
+ }
26
+ }
27
+ const currentConfig = getRunConfig(channelId);
28
+ if (subcommand === 'show') {
29
+ const discordStatus = currentConfig.notify_discord ? '✅' : '❌';
30
+ const systemStatus = currentConfig.notify_system ? '✅' : '❌';
31
+ const webhookStatus = currentConfig.webhook_url
32
+ ? `\`${currentConfig.webhook_url.slice(0, 30)}...\``
33
+ : '(not set)';
34
+ await command.reply({
35
+ content: [
36
+ '**Run Notification Settings**',
37
+ '',
38
+ `${discordStatus} Discord notifications`,
39
+ `${systemStatus} System notifications`,
40
+ `🔗 Webhook: ${webhookStatus}`,
41
+ ].join('\n'),
42
+ ephemeral: true,
43
+ flags: SILENT_MESSAGE_FLAGS,
44
+ });
45
+ return;
46
+ }
47
+ if (subcommand === 'discord') {
48
+ const enabled = command.options.getBoolean('enabled', true);
49
+ setRunConfig(channelId, { notify_discord: enabled ? 1 : 0 });
50
+ await command.reply({
51
+ content: `${enabled ? '✅' : '❌'} Discord notifications ${enabled ? 'enabled' : 'disabled'}`,
52
+ ephemeral: true,
53
+ flags: SILENT_MESSAGE_FLAGS,
54
+ });
55
+ return;
56
+ }
57
+ if (subcommand === 'system') {
58
+ const enabled = command.options.getBoolean('enabled', true);
59
+ setRunConfig(channelId, { notify_system: enabled ? 1 : 0 });
60
+ await command.reply({
61
+ content: `${enabled ? '✅' : '❌'} System notifications ${enabled ? 'enabled' : 'disabled'}`,
62
+ ephemeral: true,
63
+ flags: SILENT_MESSAGE_FLAGS,
64
+ });
65
+ return;
66
+ }
67
+ if (subcommand === 'webhook') {
68
+ const url = command.options.getString('url');
69
+ setRunConfig(channelId, { webhook_url: url || null });
70
+ await command.reply({
71
+ content: url
72
+ ? `🔗 Webhook URL set to \`${url.slice(0, 30)}...\``
73
+ : '🔗 Webhook URL cleared',
74
+ ephemeral: true,
75
+ flags: SILENT_MESSAGE_FLAGS,
76
+ });
77
+ return;
78
+ }
79
+ }