@@ -94,6 +94,51 @@ interface GrainTranscriptSegment {
9494 */
9595const RECORDING_INCLUDE = { participants : true } as const
9696
97+ /** Number of milliseconds in a day, used to convert the lookback window to a timestamp. */
98+ const MS_PER_DAY = 24 * 60 * 60 * 1000
99+
100+ /**
101+ * Valid values for the recordings list `participant_scope` filter (verified against the
102+ * Grain Public API recordings list request body, which the in-repo Grain tools also use).
103+ */
104+ const PARTICIPANT_SCOPES = [ 'internal' , 'external' ] as const
105+ type ParticipantScope = ( typeof PARTICIPANT_SCOPES ) [ number ]
106+
107+ function isParticipantScope ( value : unknown ) : value is ParticipantScope {
108+ return typeof value === 'string' && PARTICIPANT_SCOPES . includes ( value as ParticipantScope )
109+ }
110+
111+ /**
112+ * Builds the recordings list `filter` object from the connector's scoping config. Only
113+ * documented Grain filter keys are emitted, and only when configured, so an empty config
114+ * produces no `filter` (full sync). Returns undefined when no scoping is configured.
115+ *
116+ * Supported keys (verified against the in-repo Grain list_recordings tool / Public API):
117+ * - `after_datetime` — derived from `lookbackDays`; recordings on/after the window start
118+ * - `participant_scope` — `internal` or `external`
119+ * - `title_search` — substring match against recording titles
120+ */
121+ function buildRecordingFilter (
122+ sourceConfig : Record < string , unknown >
123+ ) : Record < string , unknown > | undefined {
124+ const filter : Record < string , unknown > = { }
125+
126+ const lookbackDays = sourceConfig . lookbackDays ? Number ( sourceConfig . lookbackDays ) : 0
127+ if ( Number . isFinite ( lookbackDays ) && lookbackDays > 0 ) {
128+ filter . after_datetime = new Date ( Date . now ( ) - lookbackDays * MS_PER_DAY ) . toISOString ( )
129+ }
130+
131+ if ( isParticipantScope ( sourceConfig . participantScope ) ) {
132+ filter . participant_scope = sourceConfig . participantScope
133+ }
134+
135+ const titleSearch =
136+ typeof sourceConfig . titleSearch === 'string' ? sourceConfig . titleSearch . trim ( ) : ''
137+ if ( titleSearch ) filter . title_search = titleSearch
138+
139+ return Object . keys ( filter ) . length > 0 ? filter : undefined
140+ }
141+
97142/**
98143 * Builds the auth + version headers shared by every Grain API request.
99144 */
@@ -284,6 +329,39 @@ export const grainConnector: ConnectorConfig = {
284329 type : 'short-input' ,
285330 required : false ,
286331 placeholder : 'e.g. 200 (default: unlimited)' ,
332+ description : 'Cap the total number of recordings synced. Leave blank to sync all.' ,
333+ } ,
334+ {
335+ id : 'lookbackDays' ,
336+ title : 'Lookback Window (days)' ,
337+ type : 'short-input' ,
338+ required : false ,
339+ mode : 'advanced' ,
340+ placeholder : 'e.g. 90 (default: all time)' ,
341+ description : 'Only sync recordings from the last N days. Leave blank to sync any age.' ,
342+ } ,
343+ {
344+ id : 'participantScope' ,
345+ title : 'Participant Scope' ,
346+ type : 'dropdown' ,
347+ required : false ,
348+ mode : 'advanced' ,
349+ description :
350+ 'Limit to internal-only meetings or meetings that include an external participant. Leave as Any to sync both.' ,
351+ options : [
352+ { label : 'Any' , id : '' } ,
353+ { label : 'Internal only' , id : 'internal' } ,
354+ { label : 'External (has external participant)' , id : 'external' } ,
355+ ] ,
356+ } ,
357+ {
358+ id : 'titleSearch' ,
359+ title : 'Title Search' ,
360+ type : 'short-input' ,
361+ required : false ,
362+ mode : 'advanced' ,
363+ placeholder : 'e.g. weekly standup' ,
364+ description : 'Only sync recordings whose title matches this text. Leave blank to sync all.' ,
287365 } ,
288366 ] ,
289367
@@ -298,7 +376,13 @@ export const grainConnector: ConnectorConfig = {
298376 const body : Record < string , unknown > = { include : RECORDING_INCLUDE }
299377 if ( cursor ) body . cursor = cursor
300378
301- logger . info ( 'Listing Grain recordings' , { hasCursor : Boolean ( cursor ) } )
379+ const filter = buildRecordingFilter ( sourceConfig )
380+ if ( filter ) body . filter = filter
381+
382+ logger . info ( 'Listing Grain recordings' , {
383+ hasCursor : Boolean ( cursor ) ,
384+ hasFilter : Boolean ( filter ) ,
385+ } )
302386
303387 const response = await fetchWithRetry ( `${ GRAIN_API_BASE } /recordings` , {
304388 method : 'POST' ,
0 commit comments