disunday 1.0.5 → 1.0.7
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/README.md +7 -1
- package/dist/cli.js +7 -1
- package/dist/commands/agent.js +8 -2
- package/dist/commands/schedule.js +22 -5
- package/dist/database.js +12 -0
- package/dist/scheduler.js +32 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
<img src="https://raw.githubusercontent.com/code-xhyun/disunday/main/assets/logo.png" alt="disunday" width="480" />
|
|
4
4
|
<br/>
|
|
5
5
|
<br/>
|
|
6
|
+
<a href="https://www.npmjs.com/package/disunday"><img src="https://img.shields.io/npm/v/disunday.svg" alt="npm version"></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/disunday"><img src="https://img.shields.io/npm/dm/disunday.svg" alt="npm downloads"></a>
|
|
8
|
+
<a href="https://github.com/code-xhyun/disunday/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/disunday.svg" alt="license"></a>
|
|
6
9
|
</div>
|
|
7
10
|
|
|
8
11
|
Disunday is a Discord bot that lets you control [OpenCode](https://opencode.ai) coding sessions from Discord. Send a message in a Discord channel → an AI agent edits code on your machine.
|
|
@@ -305,6 +308,7 @@ Schedule prompts to run at a specific time:
|
|
|
305
308
|
/schedule add prompt:"Run tests and deploy" time:3:00pm
|
|
306
309
|
/schedule add prompt:"Daily standup summary" time:30m
|
|
307
310
|
/schedule list
|
|
311
|
+
/schedule list all:true
|
|
308
312
|
/schedule cancel id:5
|
|
309
313
|
```
|
|
310
314
|
|
|
@@ -312,7 +316,7 @@ Schedule prompts to run at a specific time:
|
|
|
312
316
|
- Relative: `30m`, `2h`, `1d` (minutes, hours, days from now)
|
|
313
317
|
- Absolute: `3:00pm`, `14:30` (runs today, or tomorrow if time has passed)
|
|
314
318
|
|
|
315
|
-
Schedules persist across bot restarts. Use `/schedule list` to see pending schedules
|
|
319
|
+
Schedules persist across bot restarts. Use `/schedule list` to see pending schedules in the current channel, or `/schedule list all:true` to see all schedules across the server. When a hub channel is configured, schedule completions and failures are also reported there.
|
|
316
320
|
|
|
317
321
|
### Run Commands
|
|
318
322
|
|
|
@@ -596,6 +600,8 @@ Or use these Discord commands to change settings per channel/session:
|
|
|
596
600
|
- `/agent` - Select a different agent (if you have multiple agents configured in your project)
|
|
597
601
|
- `/login` - Authenticate with providers via OAuth or API key
|
|
598
602
|
|
|
603
|
+
When you switch agents in a session thread, the thread name is updated with the agent tag (e.g., `Fix login bug [hephaestus]`) so you can easily see which agent is active.
|
|
604
|
+
|
|
599
605
|
---
|
|
600
606
|
|
|
601
607
|
## Credits
|
package/dist/cli.js
CHANGED
|
@@ -554,7 +554,13 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
554
554
|
.addSubcommand((sub) => {
|
|
555
555
|
return sub
|
|
556
556
|
.setName('list')
|
|
557
|
-
.setDescription('List pending schedules in this channel')
|
|
557
|
+
.setDescription('List pending schedules in this channel')
|
|
558
|
+
.addBooleanOption((opt) => {
|
|
559
|
+
return opt
|
|
560
|
+
.setName('all')
|
|
561
|
+
.setDescription('List all pending schedules in the server')
|
|
562
|
+
.setRequired(false);
|
|
563
|
+
});
|
|
558
564
|
})
|
|
559
565
|
.addSubcommand((sub) => {
|
|
560
566
|
return sub
|
package/dist/commands/agent.js
CHANGED
|
@@ -4,7 +4,7 @@ import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectM
|
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import { getDatabase, setChannelAgent, setSessionAgent, clearSessionModel, runModelMigrations } from '../database.js';
|
|
6
6
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
7
|
-
import { resolveTextChannel, getDisundayMetadata } from '../discord-utils.js';
|
|
7
|
+
import { resolveTextChannel, getDisundayMetadata, SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
8
8
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
9
|
import * as errore from 'errore';
|
|
10
10
|
const agentLogger = createLogger(LogPrefix.AGENT);
|
|
@@ -200,7 +200,8 @@ export async function handleAgentSelectMenu(interaction) {
|
|
|
200
200
|
* These instantly switch to the specified agent without showing a dropdown.
|
|
201
201
|
*/
|
|
202
202
|
export async function handleQuickAgentCommand({ command, appId, }) {
|
|
203
|
-
|
|
203
|
+
const isThread = command.channel?.isThread() ?? false;
|
|
204
|
+
await command.deferReply({ ephemeral: !isThread });
|
|
204
205
|
runModelMigrations();
|
|
205
206
|
// Extract agent name from command: "plan-agent" → "plan"
|
|
206
207
|
const sanitizedAgentName = command.commandName.replace(/-agent$/, '');
|
|
@@ -231,6 +232,11 @@ export async function handleQuickAgentCommand({ command, appId, }) {
|
|
|
231
232
|
}
|
|
232
233
|
setAgentForContext({ context, agentName: matchingAgent.name });
|
|
233
234
|
if (context.isThread && context.sessionId) {
|
|
235
|
+
const thread = command.channel;
|
|
236
|
+
const currentName = thread.name;
|
|
237
|
+
const nameWithoutAgent = currentName.replace(/\s*\[[^\]]+\]$/, '');
|
|
238
|
+
const newName = `${nameWithoutAgent} [${matchingAgent.name}]`;
|
|
239
|
+
await errore.tryAsync(() => thread.setName(newName));
|
|
234
240
|
await command.editReply({
|
|
235
241
|
content: `Switched to **${matchingAgent.name}** agent for this session`,
|
|
236
242
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createScheduledMessage, getSchedulesByChannel, getScheduleById, cancelSchedule, } from '../database.js';
|
|
1
|
+
import { createScheduledMessage, getSchedulesByChannel, getSchedulesByChannelIds, getScheduleById, cancelSchedule, } from '../database.js';
|
|
2
2
|
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
3
3
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
4
4
|
const scheduleLogger = createLogger(LogPrefix.INTERACTION);
|
|
@@ -113,11 +113,26 @@ export async function handleScheduleCommand({ command, appId, }) {
|
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
115
|
case 'list': {
|
|
116
|
+
const showAll = command.options.getBoolean('all') ?? false;
|
|
116
117
|
const channelId = command.channelId;
|
|
117
|
-
const
|
|
118
|
+
const guild = command.guild;
|
|
119
|
+
let schedules;
|
|
120
|
+
if (showAll && guild) {
|
|
121
|
+
// Get all text channels in the guild that have disunday metadata
|
|
122
|
+
const channels = guild.channels.cache.filter((ch) => {
|
|
123
|
+
return ch.isTextBased() && 'topic' in ch && ch.topic?.includes('<disunday>');
|
|
124
|
+
});
|
|
125
|
+
const channelIds = channels.map((ch) => ch.id);
|
|
126
|
+
schedules = getSchedulesByChannelIds(channelIds);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
schedules = getSchedulesByChannel(channelId);
|
|
130
|
+
}
|
|
118
131
|
if (schedules.length === 0) {
|
|
119
132
|
await command.reply({
|
|
120
|
-
content:
|
|
133
|
+
content: showAll
|
|
134
|
+
? '📭 No pending schedules in this server'
|
|
135
|
+
: '📭 No pending schedules in this channel',
|
|
121
136
|
ephemeral: true,
|
|
122
137
|
});
|
|
123
138
|
return;
|
|
@@ -125,10 +140,12 @@ export async function handleScheduleCommand({ command, appId, }) {
|
|
|
125
140
|
const lines = schedules.map((s) => {
|
|
126
141
|
const time = formatScheduleTime(s.scheduled_at);
|
|
127
142
|
const preview = s.prompt.slice(0, 40) + (s.prompt.length > 40 ? '...' : '');
|
|
128
|
-
|
|
143
|
+
const channelMention = showAll ? `<#${s.channel_id}> ` : '';
|
|
144
|
+
return `**#${s.id}** ${channelMention}${time}\n└ ${preview}`;
|
|
129
145
|
});
|
|
146
|
+
const title = showAll ? '📋 **All Pending Schedules**' : '📋 **Pending Schedules**';
|
|
130
147
|
await command.reply({
|
|
131
|
-
content:
|
|
148
|
+
content: `${title}\n\n${lines.join('\n\n')}`,
|
|
132
149
|
ephemeral: true,
|
|
133
150
|
});
|
|
134
151
|
return;
|
package/dist/database.js
CHANGED
|
@@ -564,6 +564,18 @@ export function getSchedulesByChannel(channelId) {
|
|
|
564
564
|
ORDER BY scheduled_at ASC`)
|
|
565
565
|
.all(channelId, channelId);
|
|
566
566
|
}
|
|
567
|
+
export function getSchedulesByChannelIds(channelIds) {
|
|
568
|
+
if (channelIds.length === 0) {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
const db = getDatabase();
|
|
572
|
+
const placeholders = channelIds.map(() => '?').join(', ');
|
|
573
|
+
return db
|
|
574
|
+
.prepare(`SELECT * FROM scheduled_messages
|
|
575
|
+
WHERE channel_id IN (${placeholders}) AND status = 'pending'
|
|
576
|
+
ORDER BY scheduled_at ASC`)
|
|
577
|
+
.all(...channelIds);
|
|
578
|
+
}
|
|
567
579
|
export function getScheduleById(id) {
|
|
568
580
|
const db = getDatabase();
|
|
569
581
|
return db
|
package/dist/scheduler.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ChannelType } from 'discord.js';
|
|
2
2
|
import * as errore from 'errore';
|
|
3
|
-
import { getPendingSchedules, updateScheduleStatus, runScheduleMigrations, getChannelDirectory, } from './database.js';
|
|
3
|
+
import { getPendingSchedules, updateScheduleStatus, runScheduleMigrations, getChannelDirectory, getBotSettings, } from './database.js';
|
|
4
4
|
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';
|
|
@@ -84,10 +84,41 @@ async function processSchedules(client) {
|
|
|
84
84
|
if (result instanceof Error) {
|
|
85
85
|
schedulerLogger.error(`[SCHEDULER] Failed schedule #${schedule.id}:`, result);
|
|
86
86
|
updateScheduleStatus(schedule.id, 'failed', result.message);
|
|
87
|
+
await sendScheduleNotification(client, schedule, 'failed', result.message);
|
|
87
88
|
}
|
|
88
89
|
else {
|
|
89
90
|
schedulerLogger.log(`[SCHEDULER] Completed schedule #${schedule.id}`);
|
|
90
91
|
updateScheduleStatus(schedule.id, 'completed');
|
|
92
|
+
await sendScheduleNotification(client, schedule, 'completed');
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
}
|
|
96
|
+
async function sendScheduleNotification(client, schedule, status, errorMessage) {
|
|
97
|
+
const appId = client.application?.id;
|
|
98
|
+
if (!appId) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const settings = getBotSettings(appId);
|
|
102
|
+
if (!settings.hub_channel_id) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const hubChannel = await errore.tryAsync(() => {
|
|
106
|
+
return client.channels.fetch(settings.hub_channel_id);
|
|
107
|
+
});
|
|
108
|
+
if (hubChannel instanceof Error || !hubChannel?.isTextBased() || !('send' in hubChannel)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const promptPreview = schedule.prompt.slice(0, 50) + (schedule.prompt.length > 50 ? '...' : '');
|
|
112
|
+
const emoji = status === 'completed' ? '✅' : '❌';
|
|
113
|
+
const statusText = status === 'completed' ? 'completed' : 'failed';
|
|
114
|
+
const content = (() => {
|
|
115
|
+
if (status === 'failed') {
|
|
116
|
+
return `${emoji} Schedule **#${schedule.id}** ${statusText}\n📍 <#${schedule.channel_id}>\n💬 ${promptPreview}\n⚠️ ${errorMessage}`;
|
|
117
|
+
}
|
|
118
|
+
return `${emoji} Schedule **#${schedule.id}** ${statusText}\n📍 <#${schedule.channel_id}>\n💬 ${promptPreview}`;
|
|
119
|
+
})();
|
|
120
|
+
try {
|
|
121
|
+
await hubChannel.send({ content, flags: SILENT_MESSAGE_FLAGS });
|
|
122
|
+
}
|
|
123
|
+
catch { }
|
|
124
|
+
}
|