@@ -70,6 +70,9 @@ export function registerIpcHandlers(
7070
7171 // Skill config handlers (direct file access, no Gateway RPC)
7272 registerSkillConfigHandlers ( ) ;
73+
74+ // Cron task handlers (proxy to Gateway RPC)
75+ registerCronHandlers ( gatewayManager ) ;
7376}
7477
7578/**
@@ -100,6 +103,183 @@ function registerSkillConfigHandlers(): void {
100103 } ) ;
101104}
102105
106+ /**
107+ * Gateway CronJob type (as returned by cron.list RPC)
108+ */
109+ interface GatewayCronJob {
110+ id : string ;
111+ name : string ;
112+ description ?: string ;
113+ enabled : boolean ;
114+ createdAtMs : number ;
115+ updatedAtMs : number ;
116+ schedule : { kind : string ; expr ?: string ; everyMs ?: number ; at ?: string ; tz ?: string } ;
117+ payload : { kind : string ; message ?: string ; text ?: string } ;
118+ delivery ?: { mode : string ; channel ?: string ; to ?: string } ;
119+ state : {
120+ nextRunAtMs ?: number ;
121+ lastRunAtMs ?: number ;
122+ lastStatus ?: string ;
123+ lastError ?: string ;
124+ lastDurationMs ?: number ;
125+ } ;
126+ }
127+
128+ /**
129+ * Transform a Gateway CronJob to the frontend CronJob format
130+ */
131+ function transformCronJob ( job : GatewayCronJob ) {
132+ // Extract message from payload
133+ const message = job . payload ?. message || job . payload ?. text || '' ;
134+
135+ // Build target from delivery info
136+ const channelType = job . delivery ?. channel || 'unknown' ;
137+ const target = {
138+ channelType,
139+ channelId : channelType ,
140+ channelName : channelType ,
141+ } ;
142+
143+ // Build lastRun from state
144+ const lastRun = job . state ?. lastRunAtMs
145+ ? {
146+ time : new Date ( job . state . lastRunAtMs ) . toISOString ( ) ,
147+ success : job . state . lastStatus === 'ok' ,
148+ error : job . state . lastError ,
149+ duration : job . state . lastDurationMs ,
150+ }
151+ : undefined ;
152+
153+ // Build nextRun from state
154+ const nextRun = job . state ?. nextRunAtMs
155+ ? new Date ( job . state . nextRunAtMs ) . toISOString ( )
156+ : undefined ;
157+
158+ return {
159+ id : job . id ,
160+ name : job . name ,
161+ message,
162+ schedule : job . schedule , // Pass the object through; frontend parseCronSchedule handles it
163+ target,
164+ enabled : job . enabled ,
165+ createdAt : new Date ( job . createdAtMs ) . toISOString ( ) ,
166+ updatedAt : new Date ( job . updatedAtMs ) . toISOString ( ) ,
167+ lastRun,
168+ nextRun,
169+ } ;
170+ }
171+
172+ /**
173+ * Cron task IPC handlers
174+ * Proxies cron operations to the Gateway RPC service.
175+ * The frontend works with plain cron expression strings, but the Gateway
176+ * expects CronSchedule objects ({ kind: "cron", expr: "..." }).
177+ * These handlers bridge the two formats.
178+ */
179+ function registerCronHandlers ( gatewayManager : GatewayManager ) : void {
180+ // List all cron jobs — transforms Gateway CronJob format to frontend CronJob format
181+ ipcMain . handle ( 'cron:list' , async ( ) => {
182+ try {
183+ const result = await gatewayManager . rpc ( 'cron.list' , { includeDisabled : true } ) ;
184+ const data = result as { jobs ?: GatewayCronJob [ ] } ;
185+ const jobs = data ?. jobs ?? [ ] ;
186+ // Transform Gateway format to frontend format
187+ return jobs . map ( transformCronJob ) ;
188+ } catch ( error ) {
189+ console . error ( 'Failed to list cron jobs:' , error ) ;
190+ throw error ;
191+ }
192+ } ) ;
193+
194+ // Create a new cron job
195+ ipcMain . handle ( 'cron:create' , async ( _ , input : {
196+ name : string ;
197+ message : string ;
198+ schedule : string ;
199+ target : { channelType : string ; channelId : string ; channelName : string } ;
200+ enabled ?: boolean ;
201+ } ) => {
202+ try {
203+ // Transform frontend input to Gateway cron.add format
204+ const gatewayInput = {
205+ name : input . name ,
206+ schedule : { kind : 'cron' , expr : input . schedule } ,
207+ payload : { kind : 'agentTurn' , message : input . message } ,
208+ enabled : input . enabled ?? true ,
209+ wakeMode : 'next-heartbeat' ,
210+ sessionTarget : 'isolated' ,
211+ delivery : {
212+ mode : 'announce' ,
213+ channel : input . target . channelType ,
214+ } ,
215+ } ;
216+ const result = await gatewayManager . rpc ( 'cron.add' , gatewayInput ) ;
217+ // Transform the returned job to frontend format
218+ if ( result && typeof result === 'object' ) {
219+ return transformCronJob ( result as GatewayCronJob ) ;
220+ }
221+ return result ;
222+ } catch ( error ) {
223+ console . error ( 'Failed to create cron job:' , error ) ;
224+ throw error ;
225+ }
226+ } ) ;
227+
228+ // Update an existing cron job
229+ ipcMain . handle ( 'cron:update' , async ( _ , id : string , input : Record < string , unknown > ) => {
230+ try {
231+ // Transform schedule string to CronSchedule object if present
232+ const patch = { ...input } ;
233+ if ( typeof patch . schedule === 'string' ) {
234+ patch . schedule = { kind : 'cron' , expr : patch . schedule } ;
235+ }
236+ // Transform message to payload format if present
237+ if ( typeof patch . message === 'string' ) {
238+ patch . payload = { kind : 'agentTurn' , message : patch . message } ;
239+ delete patch . message ;
240+ }
241+ const result = await gatewayManager . rpc ( 'cron.update' , { id, patch } ) ;
242+ return result ;
243+ } catch ( error ) {
244+ console . error ( 'Failed to update cron job:' , error ) ;
245+ throw error ;
246+ }
247+ } ) ;
248+
249+ // Delete a cron job
250+ ipcMain . handle ( 'cron:delete' , async ( _ , id : string ) => {
251+ try {
252+ const result = await gatewayManager . rpc ( 'cron.remove' , { id } ) ;
253+ return result ;
254+ } catch ( error ) {
255+ console . error ( 'Failed to delete cron job:' , error ) ;
256+ throw error ;
257+ }
258+ } ) ;
259+
260+ // Toggle a cron job enabled/disabled
261+ ipcMain . handle ( 'cron:toggle' , async ( _ , id : string , enabled : boolean ) => {
262+ try {
263+ const result = await gatewayManager . rpc ( 'cron.update' , { id, patch : { enabled } } ) ;
264+ return result ;
265+ } catch ( error ) {
266+ console . error ( 'Failed to toggle cron job:' , error ) ;
267+ throw error ;
268+ }
269+ } ) ;
270+
271+ // Trigger a cron job manually
272+ ipcMain . handle ( 'cron:trigger' , async ( _ , id : string ) => {
273+ try {
274+ const result = await gatewayManager . rpc ( 'cron.run' , { id, mode : 'force' } ) ;
275+ return result ;
276+ } catch ( error ) {
277+ console . error ( 'Failed to trigger cron job:' , error ) ;
278+ throw error ;
279+ }
280+ } ) ;
281+ }
282+
103283/**
104284 * UV-related IPC handlers
105285 */
0 commit comments