@@ -3,11 +3,32 @@ import { createCoreRuntime } from "@/core/runtime";
33import type { IMAdapter } from "@/core/types" ;
44import { createAgentAdapter } from "@/agents/adapter" ;
55import type { OpenCodeMessageContext } from "@/agents" ;
6- import { getChannelSystemMessage , getDiscordBotTokens , getDiscordTargetChannels , getGitHubInfoForUser } from "@/config" ;
6+ import {
7+ getChannelSystemMessage ,
8+ getDiscordBotTokens ,
9+ getDiscordTargetChannels ,
10+ getGitHubInfoForUser ,
11+ getWebHost ,
12+ getWebPort ,
13+ } from "@/config" ;
714import { isThreadActive , markThreadActive } from "@/config/local/settings" ;
815import { log } from "@/utils" ;
916
1017const DISCORD_MESSAGE_LIMIT = 2000 ;
18+ const DISCORD_LAUNCHER_COMMANDS = [
19+ {
20+ name : "setting" ,
21+ description : "Open Ode settings" ,
22+ } ,
23+ {
24+ name : "channel" ,
25+ description : "Open channel settings" ,
26+ } ,
27+ {
28+ name : "gh" ,
29+ description : "Check GitHub token status" ,
30+ } ,
31+ ] as const ;
1132
1233let discordClient : Client | null = null ;
1334const statusMessageThreadMap = new Map < string , string > ( ) ;
@@ -143,6 +164,63 @@ function isStopCommand(text: string): boolean {
143164 return text . trim ( ) . toLowerCase ( ) === "stop" ;
144165}
145166
167+ function parseLauncherCommand ( text : string ) : "setting" | "channel" | "gh" | null {
168+ const trimmed = text . trim ( ) . toLowerCase ( ) ;
169+ if ( / ^ \/ s e t t i n g \b / . test ( trimmed ) ) return "setting" ;
170+ if ( / ^ \/ c h a n n e l \b / . test ( trimmed ) ) return "channel" ;
171+ if ( / ^ \/ g h \b / . test ( trimmed ) ) return "gh" ;
172+ return null ;
173+ }
174+
175+ function getLocalSettingsUrl ( ) : string {
176+ return `http://${ getWebHost ( ) } :${ getWebPort ( ) } /local-setting` ;
177+ }
178+
179+ function buildLauncherCommandText ( params : {
180+ command : "setting" | "channel" | "gh" ;
181+ userId : string ;
182+ channelId : string ;
183+ } ) : string {
184+ const { command, userId, channelId } = params ;
185+ const settingsUrl = getLocalSettingsUrl ( ) ;
186+
187+ if ( command === "gh" ) {
188+ const hasToken = Boolean ( getGitHubInfoForUser ( userId ) ?. token ) ;
189+ return hasToken
190+ ? `GitHub token is set for your account. You can update it at ${ settingsUrl } .`
191+ : `No GitHub token set yet. Add it at ${ settingsUrl } .` ;
192+ }
193+
194+ if ( command === "channel" ) {
195+ return `Open channel settings for channel ${ channelId } : ${ settingsUrl } ` ;
196+ }
197+
198+ return `Open settings: ${ settingsUrl } ` ;
199+ }
200+
201+ async function postLauncherCommandReply ( params : {
202+ channel : any ;
203+ command : "setting" | "channel" | "gh" ;
204+ userId : string ;
205+ channelId : string ;
206+ } ) : Promise < void > {
207+ const text = buildLauncherCommandText ( params ) ;
208+ await params . channel . send ( text ) ;
209+ }
210+
211+ async function registerDiscordCommands ( client : Client ) : Promise < void > {
212+ try {
213+ const guilds = await client . guilds . fetch ( ) ;
214+ for ( const [ , guildPreview ] of guilds ) {
215+ const guild = await client . guilds . fetch ( guildPreview . id ) ;
216+ await guild . commands . set ( [ ...DISCORD_LAUNCHER_COMMANDS ] ) ;
217+ }
218+ log . info ( "Discord slash commands registered" , { count : DISCORD_LAUNCHER_COMMANDS . length } ) ;
219+ } catch ( error ) {
220+ log . warn ( "Failed to register Discord slash commands" , { error : String ( error ) } ) ;
221+ }
222+ }
223+
146224export async function startDiscordRuntime ( reason : string ) : Promise < boolean > {
147225 if ( discordClient ) return true ;
148226 const configuredTokens = getDiscordBotTokens ( ) ;
@@ -176,6 +254,16 @@ export async function startDiscordRuntime(reason: string): Promise<boolean> {
176254
177255 const threadId = message . channel . id ;
178256 const text = message . content . trim ( ) ;
257+ const launcherCommand = parseLauncherCommand ( text ) ;
258+ if ( launcherCommand ) {
259+ await postLauncherCommandReply ( {
260+ channel : message . channel ,
261+ command : launcherCommand ,
262+ userId : message . author . id ,
263+ channelId : parentId ,
264+ } ) ;
265+ return ;
266+ }
179267 const mentioned = message . mentions . users . has ( client . user . id ) ;
180268 const active = isThreadActive ( parentId , threadId ) ;
181269 if ( ! mentioned && ! active ) return ;
@@ -203,10 +291,31 @@ export async function startDiscordRuntime(reason: string): Promise<boolean> {
203291 const parentId = message . channel . id ;
204292 if ( configuredChannels && ! configuredChannels . includes ( parentId ) ) return ;
205293
294+ const parentLauncherCommand = parseLauncherCommand ( message . content ) ;
295+ if ( parentLauncherCommand ) {
296+ await postLauncherCommandReply ( {
297+ channel : message . channel ,
298+ command : parentLauncherCommand ,
299+ userId : message . author . id ,
300+ channelId : parentId ,
301+ } ) ;
302+ return ;
303+ }
304+
206305 const isMentioned = message . mentions . users . has ( client . user . id ) ;
207306 if ( ! isMentioned ) return ;
208307
209308 const cleaned = cleanBotMention ( message . content , client . user . id ) ;
309+ const cleanedLauncherCommand = parseLauncherCommand ( cleaned ) ;
310+ if ( cleanedLauncherCommand ) {
311+ await postLauncherCommandReply ( {
312+ channel : message . channel ,
313+ command : cleanedLauncherCommand ,
314+ userId : message . author . id ,
315+ channelId : parentId ,
316+ } ) ;
317+ return ;
318+ }
210319 if ( ! cleaned ) {
211320 await message . reply ( "Please include a request after mentioning me." ) ;
212321 return ;
@@ -230,7 +339,28 @@ export async function startDiscordRuntime(reason: string): Promise<boolean> {
230339 }
231340 } ) ;
232341
342+ client . on ( "interactionCreate" , async ( interaction : any ) => {
343+ try {
344+ if ( ! interaction . isChatInputCommand || ! interaction . isChatInputCommand ( ) ) return ;
345+ const commandName = String ( interaction . commandName || "" ) . toLowerCase ( ) ;
346+ if ( ! [ "setting" , "channel" , "gh" ] . includes ( commandName ) ) return ;
347+
348+ const channel = interaction . channel ;
349+ const resolvedChannelId = channel ?. isThread ?.( ) ? ( channel . parentId ?? interaction . channelId ) : interaction . channelId ;
350+ const text = buildLauncherCommandText ( {
351+ command : commandName as "setting" | "channel" | "gh" ,
352+ userId : interaction . user . id ,
353+ channelId : resolvedChannelId ,
354+ } ) ;
355+
356+ await interaction . reply ( { content : text , ephemeral : true } ) ;
357+ } catch ( error ) {
358+ log . error ( "Discord interaction handler failed" , { error : String ( error ) } ) ;
359+ }
360+ } ) ;
361+
233362 await client . login ( token ) ;
363+ await registerDiscordCommands ( client ) ;
234364 discordClient = client ;
235365 log . info ( "Discord runtime started" , { reason, botUserId : client . user ?. id ?? "unknown" } ) ;
236366 return true ;
0 commit comments