From 6ef99ee727ca3c80116fba3ebfa567b757f453fe Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Fri, 27 Mar 2026 15:17:53 -0600 Subject: [PATCH] =?UTF-8?q?test:=20Wired=20EventBridge=20into=20Daemon=20l?= =?UTF-8?q?ifecycle=20with=20/gsd-verbose=20slash=20c=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "packages/daemon/src/commands.ts" - "packages/daemon/src/discord-bot.ts" - "packages/daemon/src/daemon.ts" - "packages/daemon/src/index.ts" - "packages/daemon/src/discord-bot.test.ts" GSD-Task: S04/T04 --- packages/daemon/src/commands.ts | 15 ++++++++++ packages/daemon/src/daemon.ts | 32 ++++++++++++++++++++ packages/daemon/src/discord-bot.test.ts | 3 +- packages/daemon/src/discord-bot.ts | 39 ++++++++++++++++++++++++- packages/daemon/src/index.ts | 20 +++++++++++++ 5 files changed, 107 insertions(+), 2 deletions(-) diff --git a/packages/daemon/src/commands.ts b/packages/daemon/src/commands.ts index 515b56728..d46d92269 100644 --- a/packages/daemon/src/commands.ts +++ b/packages/daemon/src/commands.ts @@ -35,6 +35,21 @@ export function buildCommands(): RESTPostAPIChatInputApplicationCommandsJSONBody .setName('gsd-stop') .setDescription('Stop a running GSD session') .toJSON(), + new SlashCommandBuilder() + .setName('gsd-verbose') + .setDescription('Set event verbosity level for this channel') + .addStringOption((option) => + option + .setName('level') + .setDescription('Verbosity level') + .setRequired(false) + .addChoices( + { name: 'default', value: 'default' }, + { name: 'verbose', value: 'verbose' }, + { name: 'quiet', value: 'quiet' }, + ), + ) + .toJSON(), ]; } diff --git a/packages/daemon/src/daemon.ts b/packages/daemon/src/daemon.ts index 727b583b7..5d94097ca 100644 --- a/packages/daemon/src/daemon.ts +++ b/packages/daemon/src/daemon.ts @@ -3,6 +3,7 @@ import type { Logger } from './logger.js'; import { SessionManager } from './session-manager.js'; import { scanForProjects } from './project-scanner.js'; import { DiscordBot, validateDiscordConfig } from './discord-bot.js'; +import { EventBridge } from './event-bridge.js'; /** * Core daemon class — ties config + logger together with lifecycle management. @@ -15,6 +16,7 @@ export class Daemon { private readonly onSigint: () => void; private sessionManager: SessionManager | undefined; private discordBot: DiscordBot | undefined; + private eventBridge: EventBridge | undefined; constructor( private readonly config: DaemonConfig, @@ -51,6 +53,25 @@ export class Daemon { sessionManager: this.sessionManager, }); await this.discordBot.login(); + + // Wire up EventBridge after bot is ready + const channelManager = this.discordBot.getChannelManager(); + const client = this.discordBot.getClient(); + if (channelManager && client) { + this.eventBridge = new EventBridge({ + sessionManager: this.sessionManager, + channelManager, + client, + config: this.config, + logger: this.logger, + ownerId: this.config.discord.owner_id, + }); + this.discordBot.setEventBridge(this.eventBridge); + this.eventBridge.start(); + this.logger.info('event bridge wired'); + } else { + this.logger.warn('event bridge skipped — channel manager or client not available'); + } } catch (err) { // Log error but don't abort daemon startup — bot is optional this.logger.error('discord bot login failed', { @@ -74,6 +95,11 @@ export class Daemon { return this.sessionManager; } + /** Accessor for the event bridge (available after start() with Discord configured). */ + getEventBridge(): EventBridge | undefined { + return this.eventBridge; + } + /** Idempotent shutdown: log, cleanup sessions, close logger, exit. */ async shutdown(): Promise { if (this.shuttingDown) return; @@ -91,6 +117,12 @@ export class Daemon { this.keepaliveTimer = undefined; } + // Stop EventBridge before Discord bot destroy + if (this.eventBridge) { + await this.eventBridge.stop(); + this.eventBridge = undefined; + } + // Destroy Discord bot before session cleanup if (this.discordBot) { await this.discordBot.destroy(); diff --git a/packages/daemon/src/discord-bot.test.ts b/packages/daemon/src/discord-bot.test.ts index 156568cdb..648a355c2 100644 --- a/packages/daemon/src/discord-bot.test.ts +++ b/packages/daemon/src/discord-bot.test.ts @@ -440,11 +440,12 @@ describe('ChannelManager', () => { describe('buildCommands', () => { it('returns array with correct command names', () => { const commands = buildCommands(); - assert.equal(commands.length, 3); + assert.equal(commands.length, 4); const names = commands.map((c) => c.name); assert.ok(names.includes('gsd-status'), 'should include gsd-status'); assert.ok(names.includes('gsd-start'), 'should include gsd-start'); assert.ok(names.includes('gsd-stop'), 'should include gsd-stop'); + assert.ok(names.includes('gsd-verbose'), 'should include gsd-verbose'); }); it('each command has a description', () => { diff --git a/packages/daemon/src/discord-bot.ts b/packages/daemon/src/discord-bot.ts index 5a1614f71..1340dde5c 100644 --- a/packages/daemon/src/discord-bot.ts +++ b/packages/daemon/src/discord-bot.ts @@ -13,11 +13,12 @@ import { type Interaction, type Guild, } from 'discord.js'; -import type { DaemonConfig } from './types.js'; +import type { DaemonConfig, VerbosityLevel } from './types.js'; import type { Logger } from './logger.js'; import type { SessionManager } from './session-manager.js'; import { ChannelManager } from './channel-manager.js'; import { buildCommands, registerGuildCommands, formatSessionStatus } from './commands.js'; +import type { EventBridge } from './event-bridge.js'; // --------------------------------------------------------------------------- // Pure helpers — exported for testability @@ -67,6 +68,7 @@ export class DiscordBot { private client: Client | null = null; private destroyed = false; private channelManager: ChannelManager | null = null; + private eventBridge: EventBridge | null = null; private readonly config: NonNullable; private readonly logger: Logger; @@ -171,6 +173,22 @@ export class DiscordBot { return this.channelManager; } + /** + * Return the underlying discord.js Client, or null if not logged in. + * Used by Daemon to pass to EventBridge as BridgeClient. + */ + getClient(): Client | null { + return this.client; + } + + /** + * Set the EventBridge reference so the bot can dispatch /gsd-verbose commands. + * Called by Daemon after creating the EventBridge. + */ + setEventBridge(bridge: EventBridge): void { + this.eventBridge = bridge; + } + // --------------------------------------------------------------------------- // Private: interaction handling // --------------------------------------------------------------------------- @@ -212,6 +230,25 @@ export class DiscordBot { }); }); break; + case 'gsd-verbose': { + if (!this.eventBridge) { + interaction.reply({ content: 'Event bridge not available.', ephemeral: true }).catch((err) => { + this.logger.warn('gsd-verbose reply failed', { + error: err instanceof Error ? err.message : String(err), + }); + }); + break; + } + const level = (interaction.options.getString('level') ?? 'default') as VerbosityLevel; + const channelId = interaction.channelId; + this.eventBridge.getVerbosityManager().setLevel(channelId, level); + interaction.reply({ content: `Verbosity set to **${level}** for this channel.`, ephemeral: true }).catch((err) => { + this.logger.warn('gsd-verbose reply failed', { + error: err instanceof Error ? err.message : String(err), + }); + }); + break; + } default: interaction.reply({ content: 'Unknown command', ephemeral: true }).catch((err) => { this.logger.warn('unknown command reply failed', { diff --git a/packages/daemon/src/index.ts b/packages/daemon/src/index.ts index 571d18da6..5bce1c226 100644 --- a/packages/daemon/src/index.ts +++ b/packages/daemon/src/index.ts @@ -9,6 +9,8 @@ export type { ProjectInfo, ProjectMarker, StartSessionOptions, + FormattedEvent, + VerbosityLevel, } from './types.js'; export { MAX_EVENTS, INIT_TIMEOUT_MS } from './types.js'; export { resolveConfigPath, loadConfig, validateConfig } from './config.js'; @@ -22,3 +24,21 @@ export type { DiscordBotOptions } from './discord-bot.js'; export { ChannelManager, sanitizeChannelName } from './channel-manager.js'; export type { ChannelManagerOptions } from './channel-manager.js'; export { buildCommands, formatSessionStatus, registerGuildCommands } from './commands.js'; +export { EventBridge } from './event-bridge.js'; +export type { BridgeClient, EventBridgeOptions } from './event-bridge.js'; +export { MessageBatcher } from './message-batcher.js'; +export type { SendPayload, SendFn, BatcherLogger, BatcherOptions } from './message-batcher.js'; +export { VerbosityManager, shouldShowAtLevel } from './verbosity.js'; +export { + formatToolStart, + formatToolEnd, + formatMessage, + formatBlocker, + formatCompletion, + formatError, + formatCostUpdate, + formatSessionStarted, + formatTaskTransition, + formatGenericEvent, + formatEvent, +} from './event-formatter.js';