@@ -3,6 +3,7 @@ import { CommandOptions, getApiToken } from "./auth.js";
33import {
44 CreateCommentArgs ,
55 LinearComment ,
6+ LinearInitiative ,
67 LinearIssue ,
78 LinearLabel ,
89 LinearProject ,
@@ -687,6 +688,222 @@ export class LinearService {
687688
688689 return projectsConnection . nodes [ 0 ] . id ;
689690 }
691+
692+ /**
693+ * Get all initiatives
694+ *
695+ * @param statusFilter - Optional status filter (Planned/Active/Completed)
696+ * @param ownerFilter - Optional owner ID filter
697+ * @param limit - Maximum initiatives to fetch (default 50)
698+ * @returns Array of initiatives with owner information
699+ */
700+ async getInitiatives (
701+ statusFilter ?: string ,
702+ ownerFilter ?: string ,
703+ limit : number = 50 ,
704+ ) : Promise < LinearInitiative [ ] > {
705+ const filter : any = { } ;
706+
707+ if ( statusFilter ) {
708+ filter . status = { eq : statusFilter } ;
709+ }
710+
711+ if ( ownerFilter ) {
712+ filter . owner = { id : { eq : ownerFilter } } ;
713+ }
714+
715+ const initiativesConnection = await this . client . initiatives ( {
716+ filter : Object . keys ( filter ) . length > 0 ? filter : undefined ,
717+ first : limit ,
718+ } ) ;
719+
720+ // Fetch owner relationship in parallel for all initiatives
721+ const initiativesWithData = await Promise . all (
722+ initiativesConnection . nodes . map ( async ( initiative ) => {
723+ const owner = await initiative . owner ;
724+ return {
725+ id : initiative . id ,
726+ name : initiative . name ,
727+ description : initiative . description || undefined ,
728+ content : initiative . content || undefined ,
729+ status : initiative . status as "Planned" | "Active" | "Completed" ,
730+ health : initiative . health as
731+ | "onTrack"
732+ | "atRisk"
733+ | "offTrack"
734+ | undefined ,
735+ targetDate : initiative . targetDate
736+ ? new Date ( initiative . targetDate ) . toISOString ( )
737+ : undefined ,
738+ owner : owner
739+ ? {
740+ id : owner . id ,
741+ name : owner . name ,
742+ }
743+ : undefined ,
744+ createdAt : initiative . createdAt
745+ ? new Date ( initiative . createdAt ) . toISOString ( )
746+ : new Date ( ) . toISOString ( ) ,
747+ updatedAt : initiative . updatedAt
748+ ? new Date ( initiative . updatedAt ) . toISOString ( )
749+ : new Date ( ) . toISOString ( ) ,
750+ } ;
751+ } ) ,
752+ ) ;
753+
754+ return initiativesWithData ;
755+ }
756+
757+ /**
758+ * Get single initiative by ID with projects and sub-initiatives
759+ *
760+ * @param initiativeId - Initiative UUID
761+ * @param projectsLimit - Maximum projects to fetch (default 50)
762+ * @returns Initiative with projects and sub-initiatives
763+ */
764+ async getInitiativeById (
765+ initiativeId : string ,
766+ projectsLimit : number = 50 ,
767+ ) : Promise < LinearInitiative > {
768+ const initiative = await this . client . initiative ( initiativeId ) ;
769+
770+ const [
771+ owner ,
772+ projectsConnection ,
773+ parentInitiative ,
774+ subInitiativesConnection ,
775+ ] = await Promise . all ( [
776+ initiative . owner ,
777+ initiative . projects ( { first : projectsLimit } ) ,
778+ initiative . parentInitiative ,
779+ initiative . subInitiatives ( { first : 50 } ) ,
780+ ] ) ;
781+
782+ // Map projects with basic info
783+ const projects = projectsConnection . nodes . map ( ( project ) => ( {
784+ id : project . id ,
785+ name : project . name ,
786+ state : project . state ,
787+ progress : project . progress ,
788+ } ) ) ;
789+
790+ // Map sub-initiatives
791+ const subInitiatives = subInitiativesConnection . nodes . map ( ( sub ) => ( {
792+ id : sub . id ,
793+ name : sub . name ,
794+ status : sub . status as "Planned" | "Active" | "Completed" ,
795+ } ) ) ;
796+
797+ return {
798+ id : initiative . id ,
799+ name : initiative . name ,
800+ description : initiative . description || undefined ,
801+ content : initiative . content || undefined ,
802+ status : initiative . status as "Planned" | "Active" | "Completed" ,
803+ health : initiative . health as
804+ | "onTrack"
805+ | "atRisk"
806+ | "offTrack"
807+ | undefined ,
808+ targetDate : initiative . targetDate
809+ ? new Date ( initiative . targetDate ) . toISOString ( )
810+ : undefined ,
811+ owner : owner
812+ ? {
813+ id : owner . id ,
814+ name : owner . name ,
815+ }
816+ : undefined ,
817+ createdAt : initiative . createdAt
818+ ? new Date ( initiative . createdAt ) . toISOString ( )
819+ : new Date ( ) . toISOString ( ) ,
820+ updatedAt : initiative . updatedAt
821+ ? new Date ( initiative . updatedAt ) . toISOString ( )
822+ : new Date ( ) . toISOString ( ) ,
823+ projects : projects . length > 0 ? projects : undefined ,
824+ parentInitiative : parentInitiative
825+ ? {
826+ id : parentInitiative . id ,
827+ name : parentInitiative . name ,
828+ }
829+ : undefined ,
830+ subInitiatives : subInitiatives . length > 0 ? subInitiatives : undefined ,
831+ } ;
832+ }
833+
834+ /**
835+ * Resolve initiative by name or ID
836+ *
837+ * @param initiativeNameOrId - Initiative name or UUID
838+ * @returns Initiative UUID
839+ * @throws Error if initiative not found or multiple matches
840+ */
841+ async resolveInitiativeId ( initiativeNameOrId : string ) : Promise < string > {
842+ // Return UUID as-is
843+ if ( isUuid ( initiativeNameOrId ) ) {
844+ return initiativeNameOrId ;
845+ }
846+
847+ // Search by name (case-insensitive)
848+ const initiativesConnection = await this . client . initiatives ( {
849+ filter : { name : { eqIgnoreCase : initiativeNameOrId } } ,
850+ first : 10 ,
851+ } ) ;
852+
853+ const nodes = initiativesConnection . nodes ;
854+
855+ if ( nodes . length === 0 ) {
856+ throw notFoundError ( "Initiative" , initiativeNameOrId ) ;
857+ }
858+
859+ if ( nodes . length === 1 ) {
860+ return nodes [ 0 ] . id ;
861+ }
862+
863+ // Multiple matches - prefer Active, then Planned
864+ let chosen = nodes . find ( ( n ) => n . status === "Active" ) ;
865+ if ( ! chosen ) chosen = nodes . find ( ( n ) => n . status === "Planned" ) ;
866+ if ( ! chosen ) chosen = nodes [ 0 ] ;
867+
868+ return chosen . id ;
869+ }
870+
871+ /**
872+ * Update an initiative
873+ *
874+ * @param initiativeId - Initiative UUID
875+ * @param updates - Fields to update
876+ * @returns Updated initiative
877+ */
878+ async updateInitiative (
879+ initiativeId : string ,
880+ updates : {
881+ name ?: string ;
882+ description ?: string ;
883+ content ?: string ;
884+ status ?: "Planned" | "Active" | "Completed" ;
885+ ownerId ?: string ;
886+ targetDate ?: string ;
887+ } ,
888+ ) : Promise < LinearInitiative > {
889+ // Build update input with only provided fields
890+ const input : Record < string , any > = { } ;
891+ if ( updates . name !== undefined ) input . name = updates . name ;
892+ if ( updates . description !== undefined ) input . description = updates . description ;
893+ if ( updates . content !== undefined ) input . content = updates . content ;
894+ if ( updates . status !== undefined ) input . status = updates . status ;
895+ if ( updates . ownerId !== undefined ) input . ownerId = updates . ownerId ;
896+ if ( updates . targetDate !== undefined ) input . targetDate = updates . targetDate ;
897+
898+ const payload = await this . client . updateInitiative ( initiativeId , input ) ;
899+
900+ if ( ! payload . success ) {
901+ throw new Error ( "Failed to update initiative" ) ;
902+ }
903+
904+ // Re-fetch to get complete data
905+ return this . getInitiativeById ( initiativeId ) ;
906+ }
690907}
691908
692909/**
0 commit comments