@@ -54,6 +54,13 @@ type SyncState = {
5454 initialSync : boolean ;
5555} ;
5656
57+ type ViewerInfo = {
58+ linearId : string ;
59+ email : string ;
60+ name : string ;
61+ avatar ?: string ;
62+ } ;
63+
5764/**
5865 * Linear project management source
5966 *
@@ -104,6 +111,39 @@ export class Linear extends Connector<Linear> {
104111 return new LinearClient ( { accessToken : token . token } ) ;
105112 }
106113
114+ /**
115+ * Resolve author contact from a Linear user object.
116+ * Falls back to cached viewer info when the API doesn't return an email.
117+ */
118+ private async resolveAuthorContact (
119+ user : { id ?: string ; email ?: string ; name ?: string ; avatarUrl ?: string | null } | null | undefined ,
120+ projectId : string
121+ ) : Promise < NewContact | undefined > {
122+ if ( ! user ) return undefined ;
123+
124+ if ( user . email ) {
125+ return {
126+ email : user . email ,
127+ name : user . name ?? "" ,
128+ avatar : user . avatarUrl ?? undefined ,
129+ } ;
130+ }
131+
132+ // Linear API often omits email on creator relations — check if it's the authenticated user
133+ if ( user . id ) {
134+ const viewerInfo = await this . get < ViewerInfo > ( `viewer_info_${ projectId } ` ) ;
135+ if ( viewerInfo ?. email && user . id === viewerInfo . linearId ) {
136+ return {
137+ email : viewerInfo . email ,
138+ name : user . name || viewerInfo . name ,
139+ avatar : user . avatarUrl ?? viewerInfo . avatar ,
140+ } ;
141+ }
142+ }
143+
144+ return undefined ;
145+ }
146+
107147 /**
108148 * Returns available Linear teams as channel resources.
109149 */
@@ -263,6 +303,22 @@ export class Linear extends Connector<Linear> {
263303 projectId : string ,
264304 options ?: ProjectSyncOptions
265305 ) : Promise < void > {
306+ // Cache the authenticated user's info for author resolution
307+ try {
308+ const client = await this . getClient ( projectId ) ;
309+ const viewer = await client . viewer ;
310+ if ( viewer ) {
311+ await this . set ( `viewer_info_${ projectId } ` , {
312+ linearId : viewer . id ,
313+ email : viewer . email ,
314+ name : viewer . name ,
315+ avatar : viewer . avatarUrl ?? undefined ,
316+ } ) ;
317+ }
318+ } catch ( error ) {
319+ console . warn ( "Failed to fetch Linear viewer info:" , error ) ;
320+ }
321+
266322 // Initialize sync state
267323 await this . set ( `sync_state_${ projectId } ` , {
268324 after : null ,
@@ -392,16 +448,9 @@ export class Linear extends Connector<Linear> {
392448 }
393449
394450 // Prepare author and assignee contacts - will be passed directly as NewContact
395- let authorContact : NewContact | undefined ;
451+ const authorContact = await this . resolveAuthorContact ( creator , projectId ) ;
396452 let assigneeContact : NewContact | undefined ;
397453
398- if ( creator ?. email ) {
399- authorContact = {
400- email : creator . email ,
401- name : creator . name ,
402- avatar : creator . avatarUrl ?? undefined ,
403- } ;
404- }
405454 if ( assignee ?. email ) {
406455 assigneeContact = {
407456 email : assignee . email ,
@@ -688,16 +737,9 @@ export class Linear extends Connector<Linear> {
688737 const assignee = issue . assignee || null ;
689738
690739 // Build thread update with only issue fields (no notes)
691- let authorContact : NewContact | undefined ;
740+ const authorContact = await this . resolveAuthorContact ( creator , projectId ) ;
692741 let assigneeContact : NewContact | undefined ;
693742
694- if ( creator ?. email ) {
695- authorContact = {
696- email : creator . email ,
697- name : creator . name ,
698- avatar : creator . avatarUrl ?? undefined ,
699- } ;
700- }
701743 if ( assignee ?. email ) {
702744 assigneeContact = {
703745 email : assignee . email ,
0 commit comments