From 280c5351e23944686530d5bd1e63624076b9783d Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Fri, 13 Mar 2026 12:04:42 -0500 Subject: [PATCH] feat(gateway): add plugin host module --- apps/gateway/package.json | 1 + apps/gateway/src/app.module.ts | 2 + apps/gateway/src/plugin/plugin.interface.ts | 5 ++ apps/gateway/src/plugin/plugin.module.ts | 90 +++++++++++++++++++++ apps/gateway/src/plugin/plugin.service.ts | 16 ++++ docs/scratchpads/41-plugin-host.md | 30 +++++++ pnpm-lock.yaml | 3 + 7 files changed, 147 insertions(+) create mode 100644 apps/gateway/src/plugin/plugin.interface.ts create mode 100644 apps/gateway/src/plugin/plugin.module.ts create mode 100644 apps/gateway/src/plugin/plugin.service.ts create mode 100644 docs/scratchpads/41-plugin-host.md diff --git a/apps/gateway/package.json b/apps/gateway/package.json index 6602939..3f36282 100644 --- a/apps/gateway/package.json +++ b/apps/gateway/package.json @@ -18,6 +18,7 @@ "@mosaic/brain": "workspace:^", "@mosaic/coord": "workspace:^", "@mosaic/db": "workspace:^", + "@mosaic/discord-plugin": "workspace:^", "@mosaic/log": "workspace:^", "@mosaic/memory": "workspace:^", "@mosaic/types": "workspace:^", diff --git a/apps/gateway/src/app.module.ts b/apps/gateway/src/app.module.ts index 17ab9b9..be30415 100644 --- a/apps/gateway/src/app.module.ts +++ b/apps/gateway/src/app.module.ts @@ -13,6 +13,7 @@ import { CoordModule } from './coord/coord.module.js'; import { MemoryModule } from './memory/memory.module.js'; import { LogModule } from './log/log.module.js'; import { SkillsModule } from './skills/skills.module.js'; +import { PluginModule } from './plugin/plugin.module.js'; @Module({ imports: [ @@ -29,6 +30,7 @@ import { SkillsModule } from './skills/skills.module.js'; MemoryModule, LogModule, SkillsModule, + PluginModule, ], controllers: [HealthController], }) diff --git a/apps/gateway/src/plugin/plugin.interface.ts b/apps/gateway/src/plugin/plugin.interface.ts new file mode 100644 index 0000000..80860d2 --- /dev/null +++ b/apps/gateway/src/plugin/plugin.interface.ts @@ -0,0 +1,5 @@ +export interface IChannelPlugin { + readonly name: string; + start(): Promise; + stop(): Promise; +} diff --git a/apps/gateway/src/plugin/plugin.module.ts b/apps/gateway/src/plugin/plugin.module.ts new file mode 100644 index 0000000..2d63f5f --- /dev/null +++ b/apps/gateway/src/plugin/plugin.module.ts @@ -0,0 +1,90 @@ +import { + Global, + Inject, + Logger, + Module, + type OnModuleDestroy, + type OnModuleInit, +} from '@nestjs/common'; +import { DiscordPlugin } from '@mosaic/discord-plugin'; +import { PluginService } from './plugin.service.js'; +import type { IChannelPlugin } from './plugin.interface.js'; + +export const PLUGIN_REGISTRY = Symbol('PLUGIN_REGISTRY'); + +class DiscordChannelPluginAdapter implements IChannelPlugin { + readonly name = 'discord'; + + constructor(private readonly plugin: DiscordPlugin) {} + + async start(): Promise { + await this.plugin.start(); + } + + async stop(): Promise { + await this.plugin.stop(); + } +} + +const DEFAULT_GATEWAY_URL = 'http://localhost:4000'; + +function createPluginRegistry(logger: Logger): IChannelPlugin[] { + const plugins: IChannelPlugin[] = []; + const discordToken = process.env['DISCORD_BOT_TOKEN']; + const discordGuildId = process.env['DISCORD_GUILD_ID']; + const discordGatewayUrl = process.env['DISCORD_GATEWAY_URL'] ?? DEFAULT_GATEWAY_URL; + + if (discordToken) { + plugins.push( + new DiscordChannelPluginAdapter( + new DiscordPlugin({ + token: discordToken, + guildId: discordGuildId, + gatewayUrl: discordGatewayUrl, + }), + ), + ); + } + + const telegramToken = process.env['TELEGRAM_BOT_TOKEN']; + const telegramGatewayUrl = process.env['TELEGRAM_GATEWAY_URL'] ?? DEFAULT_GATEWAY_URL; + + if (telegramToken) { + logger.warn( + `Telegram plugin requested for ${telegramGatewayUrl}, but @mosaic/telegram-plugin is not implemented yet.`, + ); + } + + return plugins; +} + +@Global() +@Module({ + providers: [ + { + provide: PLUGIN_REGISTRY, + useFactory: (): IChannelPlugin[] => createPluginRegistry(new Logger('PluginModule')), + }, + PluginService, + ], + exports: [PluginService, PLUGIN_REGISTRY], +}) +export class PluginModule implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(PluginModule.name); + + constructor(@Inject(PLUGIN_REGISTRY) private readonly plugins: IChannelPlugin[]) {} + + async onModuleInit(): Promise { + for (const plugin of this.plugins) { + this.logger.log(`Starting plugin: ${plugin.name}`); + await plugin.start(); + } + } + + async onModuleDestroy(): Promise { + for (const plugin of [...this.plugins].reverse()) { + this.logger.log(`Stopping plugin: ${plugin.name}`); + await plugin.stop(); + } + } +} diff --git a/apps/gateway/src/plugin/plugin.service.ts b/apps/gateway/src/plugin/plugin.service.ts new file mode 100644 index 0000000..9c839a8 --- /dev/null +++ b/apps/gateway/src/plugin/plugin.service.ts @@ -0,0 +1,16 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { PLUGIN_REGISTRY } from './plugin.module.js'; +import type { IChannelPlugin } from './plugin.interface.js'; + +@Injectable() +export class PluginService { + constructor(@Inject(PLUGIN_REGISTRY) private readonly plugins: IChannelPlugin[]) {} + + getPlugins(): IChannelPlugin[] { + return this.plugins; + } + + getPlugin(name: string): IChannelPlugin | undefined { + return this.plugins.find((plugin: IChannelPlugin) => plugin.name === name); + } +} diff --git a/docs/scratchpads/41-plugin-host.md b/docs/scratchpads/41-plugin-host.md new file mode 100644 index 0000000..fa0ed4a --- /dev/null +++ b/docs/scratchpads/41-plugin-host.md @@ -0,0 +1,30 @@ +# Scratchpad — P5-001 Plugin Host + +- Task: P5-001 / issue #41 +- Branch: feat/p5-plugin-host +- Objective: add global NestJS plugin host module, wire Discord import, register active plugins from env, and attach to AppModule. +- TDD: skipped as optional for module wiring/integration work; relying on targeted typecheck/lint and module-level situational verification. +- Constraints: ESM .js imports, explicit @Inject(), follow existing gateway patterns, do not touch TASKS.md. + +## Progress Log + +- 2026-03-13: session started in worktree; loading gateway/plugin package context. +- 2026-03-13: implemented initial plugin module, service, interface, and AppModule wiring; pending verification. +- 2026-03-13: added `@mosaic/discord-plugin` as a gateway workspace dependency and regenerated `pnpm-lock.yaml`. +- 2026-03-13: built gateway dependency chain so workspace packages exported `dist/*` for clean TypeScript resolution in this fresh worktree. +- 2026-03-13: verification complete. + +## Verification + +- `pnpm --filter @mosaic/gateway... build` ✅ +- `pnpm --filter @mosaic/gateway typecheck` ✅ +- `pnpm --filter @mosaic/gateway lint` ✅ +- `pnpm format:check` ✅ +- `pnpm typecheck` ✅ +- `pnpm lint` ✅ + +## Review + +- Automated review: `~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted` +- Manual review: diff inspection of gateway plugin host changes +- Result: no blocker findings diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48827c7..4bbb9de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: '@mosaic/db': specifier: workspace:^ version: link:../../packages/db + '@mosaic/discord-plugin': + specifier: workspace:^ + version: link:../../plugins/discord '@mosaic/log': specifier: workspace:^ version: link:../../packages/log