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 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 and `/schedule cancel` to remove them.
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
@@ -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
- await command.deferReply({ ephemeral: true });
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 schedules = getSchedulesByChannel(channelId);
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: '📭 No pending schedules in this channel',
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
- return `**#${s.id}** ${time}\n└ ${preview}`;
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: `📋 **Pending Schedules**\n\n${lines.join('\n\n')}`,
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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "disunday",
3
3
  "type": "module",
4
- "version": "1.0.5",
4
+ "version": "1.0.7",
5
5
  "description": "Discord bot for controlling OpenCode coding sessions",
6
6
  "author": "code-xhyun",
7
7
  "license": "MIT",