#
tokens: 26349/50000 1/150 files (page 6/6)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 6 of 6. Use http://codebase.md/geropl/linear-mcp-go?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .clinerules
│   └── memory-bank.md
├── .devcontainer
│   ├── devcontainer.json
│   └── Dockerfile
├── .github
│   └── workflows
│       └── release.yml
├── .gitignore
├── .gitpod.yml
├── cmd
│   ├── root.go
│   ├── serve.go
│   ├── setup_test.go
│   ├── setup.go
│   └── version.go
├── docs
│   ├── design
│   │   ├── 001-mcp-go-upgrade.md
│   │   └── 002-project-milestone-initiative.md
│   └── prd
│       ├── 000-tool-standardization-overview.md
│       ├── 001-api-refresher.md
│       ├── 002-tool-standardization.md
│       ├── 003-tool-standardization-implementation.md
│       ├── 004-tool-standardization-tracking.md
│       ├── 005-sample-implementation.md
│       ├── 006-issue-comments-pagination.md
│       └── README.md
├── go.mod
├── go.sum
├── main.go
├── memory-bank
│   ├── activeContext.md
│   ├── developmentWorkflows.md
│   ├── productContext.md
│   ├── progress.md
│   ├── projectbrief.md
│   ├── systemPatterns.md
│   └── techContext.md
├── pkg
│   ├── linear
│   │   ├── client.go
│   │   ├── models.go
│   │   ├── rate_limiter.go
│   │   └── test_helpers.go
│   ├── server
│   │   ├── resources_test.go
│   │   ├── resources.go
│   │   ├── server.go
│   │   ├── test_helpers.go
│   │   └── tools_test.go
│   └── tools
│       ├── add_comment.go
│       ├── common.go
│       ├── create_issue.go
│       ├── get_issue_comments.go
│       ├── get_issue.go
│       ├── get_teams.go
│       ├── get_user_issues.go
│       ├── initiative_tools.go
│       ├── milestone_tools.go
│       ├── priority_test.go
│       ├── priority.go
│       ├── project_tools.go
│       ├── rendering.go
│       ├── reply_to_comment.go
│       ├── search_issues.go
│       ├── update_issue_comment.go
│       └── update_issue.go
├── README.md
├── scripts
│   └── register-cline.sh
└── testdata
    ├── fixtures
    │   ├── add_comment_handler_Missing body.yaml
    │   ├── add_comment_handler_Missing issue.yaml
    │   ├── add_comment_handler_Missing issueId.yaml
    │   ├── add_comment_handler_Reply with shorthand.yaml
    │   ├── add_comment_handler_Reply with URL.yaml
    │   ├── add_comment_handler_Reply_to_comment.yaml
    │   ├── add_comment_handler_Valid comment.yaml
    │   ├── create_initiative_handler_Missing name.yaml
    │   ├── create_initiative_handler_Valid initiative.yaml
    │   ├── create_initiative_handler_With description.yaml
    │   ├── create_issue_handler_Create issue with invalid project.yaml
    │   ├── create_issue_handler_Create issue with labels.yaml
    │   ├── create_issue_handler_Create issue with project ID.yaml
    │   ├── create_issue_handler_Create issue with project name.yaml
    │   ├── create_issue_handler_Create issue with project slug.yaml
    │   ├── create_issue_handler_Create sub issue from identifier.yaml
    │   ├── create_issue_handler_Create sub issue with labels.yaml
    │   ├── create_issue_handler_Create sub issue.yaml
    │   ├── create_issue_handler_Invalid team.yaml
    │   ├── create_issue_handler_Missing team.yaml
    │   ├── create_issue_handler_Missing teamId.yaml
    │   ├── create_issue_handler_Missing title.yaml
    │   ├── create_issue_handler_Valid issue with team key.yaml
    │   ├── create_issue_handler_Valid issue with team name.yaml
    │   ├── create_issue_handler_Valid issue with team UUID.yaml
    │   ├── create_issue_handler_Valid issue with team.yaml
    │   ├── create_issue_handler_Valid issue with teamId.yaml
    │   ├── create_issue_handler_Valid issue.yaml
    │   ├── create_milestone_handler_Invalid project ID.yaml
    │   ├── create_milestone_handler_Missing name.yaml
    │   ├── create_milestone_handler_Valid milestone.yaml
    │   ├── create_milestone_handler_With all optional fields.yaml
    │   ├── create_project_handler_Invalid team ID.yaml
    │   ├── create_project_handler_Missing name.yaml
    │   ├── create_project_handler_Valid project.yaml
    │   ├── create_project_handler_With all optional fields.yaml
    │   ├── get_initiative_handler_By name.yaml
    │   ├── get_initiative_handler_Non-existent name.yaml
    │   ├── get_initiative_handler_Valid initiative.yaml
    │   ├── get_issue_comments_handler_Invalid issue.yaml
    │   ├── get_issue_comments_handler_Missing issue.yaml
    │   ├── get_issue_comments_handler_Thread_with_pagination.yaml
    │   ├── get_issue_comments_handler_Valid issue.yaml
    │   ├── get_issue_comments_handler_With limit.yaml
    │   ├── get_issue_comments_handler_With_thread_parameter.yaml
    │   ├── get_issue_handler_Get comment issue.yaml
    │   ├── get_issue_handler_Missing issue.yaml
    │   ├── get_issue_handler_Missing issueId.yaml
    │   ├── get_issue_handler_Valid issue.yaml
    │   ├── get_milestone_handler_By name.yaml
    │   ├── get_milestone_handler_Non-existent milestone.yaml
    │   ├── get_milestone_handler_Valid milestone.yaml
    │   ├── get_project_handler_By ID.yaml
    │   ├── get_project_handler_By name.yaml
    │   ├── get_project_handler_By slug.yaml
    │   ├── get_project_handler_Invalid project.yaml
    │   ├── get_project_handler_Missing project param.yaml
    │   ├── get_project_handler_Non-existent slug.yaml
    │   ├── get_teams_handler_Get Teams.yaml
    │   ├── get_user_issues_handler_Current user issues.yaml
    │   ├── get_user_issues_handler_Specific user issues.yaml
    │   ├── reply_to_comment_handler_Missing body.yaml
    │   ├── reply_to_comment_handler_Missing thread.yaml
    │   ├── reply_to_comment_handler_Reply with URL.yaml
    │   ├── reply_to_comment_handler_Valid reply.yaml
    │   ├── resource_TeamResourceHandler_Fetch By ID.yaml
    │   ├── resource_TeamResourceHandler_Fetch By Key.yaml
    │   ├── resource_TeamResourceHandler_Fetch By Name.yaml
    │   ├── resource_TeamResourceHandler_Invalid ID.yaml
    │   ├── resource_TeamResourceHandler_Missing ID.yaml
    │   ├── resource_TeamsResourceHandler_List All.yaml
    │   ├── search_issues_handler_Search by query.yaml
    │   ├── search_issues_handler_Search by team.yaml
    │   ├── search_projects_handler_Empty query.yaml
    │   ├── search_projects_handler_Multiple results.yaml
    │   ├── search_projects_handler_No results.yaml
    │   ├── search_projects_handler_Search by query.yaml
    │   ├── update_comment_handler_Invalid comment identifier.yaml
    │   ├── update_comment_handler_Missing body.yaml
    │   ├── update_comment_handler_Missing comment.yaml
    │   ├── update_comment_handler_Valid comment update with hash only.yaml
    │   ├── update_comment_handler_Valid comment update with shorthand.yaml
    │   ├── update_comment_handler_Valid comment update.yaml
    │   ├── update_initiative_handler_Non-existent initiative.yaml
    │   ├── update_initiative_handler_Valid update.yaml
    │   ├── update_issue_handler_Missing id.yaml
    │   ├── update_issue_handler_Valid update.yaml
    │   ├── update_milestone_handler_Non-existent milestone.yaml
    │   ├── update_milestone_handler_Valid update.yaml
    │   ├── update_project_handler_Non-existent project.yaml
    │   ├── update_project_handler_Update name and description.yaml
    │   ├── update_project_handler_Update only description.yaml
    │   └── update_project_handler_Valid update.yaml
    └── golden
        ├── add_comment_handler_Missing body.golden
        ├── add_comment_handler_Missing issue.golden
        ├── add_comment_handler_Missing issueId.golden
        ├── add_comment_handler_Reply with shorthand.golden
        ├── add_comment_handler_Reply with URL.golden
        ├── add_comment_handler_Reply_to_comment.golden
        ├── add_comment_handler_Valid comment.golden
        ├── create_initiative_handler_Missing name.golden
        ├── create_initiative_handler_Valid initiative.golden
        ├── create_initiative_handler_With description.golden
        ├── create_issue_handler_Create issue with invalid project.golden
        ├── create_issue_handler_Create issue with labels.golden
        ├── create_issue_handler_Create issue with project ID.golden
        ├── create_issue_handler_Create issue with project name.golden
        ├── create_issue_handler_Create issue with project slug.golden
        ├── create_issue_handler_Create sub issue from identifier.golden
        ├── create_issue_handler_Create sub issue with labels.golden
        ├── create_issue_handler_Create sub issue.golden
        ├── create_issue_handler_Invalid team.golden
        ├── create_issue_handler_Missing team.golden
        ├── create_issue_handler_Missing teamId.golden
        ├── create_issue_handler_Missing title.golden
        ├── create_issue_handler_Valid issue with team key.golden
        ├── create_issue_handler_Valid issue with team name.golden
        ├── create_issue_handler_Valid issue with team UUID.golden
        ├── create_issue_handler_Valid issue with team.golden
        ├── create_issue_handler_Valid issue with teamId.golden
        ├── create_issue_handler_Valid issue.golden
        ├── create_milestone_handler_Invalid project ID.golden
        ├── create_milestone_handler_Missing name.golden
        ├── create_milestone_handler_Valid milestone.golden
        ├── create_milestone_handler_With all optional fields.golden
        ├── create_project_handler_Invalid team ID.golden
        ├── create_project_handler_Missing name.golden
        ├── create_project_handler_Valid project.golden
        ├── create_project_handler_With all optional fields.golden
        ├── get_initiative_handler_By name.golden
        ├── get_initiative_handler_Non-existent name.golden
        ├── get_initiative_handler_Valid initiative.golden
        ├── get_issue_comments_handler_Invalid issue.golden
        ├── get_issue_comments_handler_Missing issue.golden
        ├── get_issue_comments_handler_Thread_with_pagination.golden
        ├── get_issue_comments_handler_Valid issue.golden
        ├── get_issue_comments_handler_With limit.golden
        ├── get_issue_comments_handler_With_thread_parameter.golden
        ├── get_issue_handler_Get comment issue.golden
        ├── get_issue_handler_Missing issue.golden
        ├── get_issue_handler_Missing issueId.golden
        ├── get_issue_handler_Valid issue.golden
        ├── get_milestone_handler_By name.golden
        ├── get_milestone_handler_Non-existent milestone.golden
        ├── get_milestone_handler_Valid milestone.golden
        ├── get_project_handler_By ID.golden
        ├── get_project_handler_By name.golden
        ├── get_project_handler_By slug.golden
        ├── get_project_handler_Invalid project.golden
        ├── get_project_handler_Missing project param.golden
        ├── get_project_handler_Non-existent slug.golden
        ├── get_teams_handler_Get Teams.golden
        ├── get_user_issues_handler_Current user issues.golden
        ├── get_user_issues_handler_Specific user issues.golden
        ├── reply_to_comment_handler_Missing body.golden
        ├── reply_to_comment_handler_Missing thread.golden
        ├── reply_to_comment_handler_Reply with URL.golden
        ├── reply_to_comment_handler_Valid reply.golden
        ├── resource_TeamResourceHandler_Fetch By ID.golden
        ├── resource_TeamResourceHandler_Fetch By Key.golden
        ├── resource_TeamResourceHandler_Fetch By Name.golden
        ├── resource_TeamResourceHandler_Invalid ID.golden
        ├── resource_TeamResourceHandler_Missing ID.golden
        ├── resource_TeamsResourceHandler_List All.golden
        ├── search_issues_handler_Search by query.golden
        ├── search_issues_handler_Search by team.golden
        ├── search_projects_handler_Empty query.golden
        ├── search_projects_handler_Multiple results.golden
        ├── search_projects_handler_No results.golden
        ├── search_projects_handler_Search by query.golden
        ├── update_comment_handler_Invalid comment identifier.golden
        ├── update_comment_handler_Missing body.golden
        ├── update_comment_handler_Missing comment.golden
        ├── update_comment_handler_Valid comment update with hash only.golden
        ├── update_comment_handler_Valid comment update with shorthand.golden
        ├── update_comment_handler_Valid comment update.golden
        ├── update_initiative_handler_Non-existent initiative.golden
        ├── update_initiative_handler_Valid update.golden
        ├── update_issue_handler_Missing id.golden
        ├── update_issue_handler_Valid update.golden
        ├── update_milestone_handler_Non-existent milestone.golden
        ├── update_milestone_handler_Valid update.golden
        ├── update_project_handler_Non-existent project.golden
        ├── update_project_handler_Update name and description.golden
        ├── update_project_handler_Update only description.golden
        └── update_project_handler_Valid update.golden
```

# Files

--------------------------------------------------------------------------------
/pkg/linear/client.go:
--------------------------------------------------------------------------------

```go
   1 | package linear
   2 | 
   3 | import (
   4 | 	"bytes"
   5 | 	"encoding/json"
   6 | 	"errors"
   7 | 	"fmt"
   8 | 	"io"
   9 | 	"net/http"
  10 | 	"os"
  11 | 	"strconv"
  12 | 	"strings"
  13 | 	"time"
  14 | )
  15 | 
  16 | const (
  17 | 	LinearAPIEndpoint = "https://api.linear.app/graphql"
  18 | )
  19 | 
  20 | // LinearClient is a client for the Linear API
  21 | type LinearClient struct {
  22 | 	apiKey      string
  23 | 	httpClient  *http.Client
  24 | 	rateLimiter *RateLimiter
  25 | 
  26 | 	serverVersion string
  27 | }
  28 | 
  29 | // NewLinearClient creates a new Linear API client
  30 | func NewLinearClient(apiKey string, serverVersion string) (*LinearClient, error) {
  31 | 	if apiKey == "" {
  32 | 		return nil, errors.New("LINEAR_API_KEY environment variable is required")
  33 | 	}
  34 | 
  35 | 	return &LinearClient{
  36 | 		apiKey: apiKey,
  37 | 		httpClient: &http.Client{
  38 | 			Timeout: 30 * time.Second,
  39 | 		},
  40 | 		rateLimiter:   NewRateLimiter(1400), // Linear API limit is 1400 requests per hour
  41 | 		serverVersion: serverVersion,
  42 | 	}, nil
  43 | }
  44 | 
  45 | // NewLinearClientFromEnv creates a new Linear API client from environment variables
  46 | func NewLinearClientFromEnv(serverVersion string) (*LinearClient, error) {
  47 | 	apiKey := os.Getenv("LINEAR_API_KEY")
  48 | 	return NewLinearClient(apiKey, serverVersion)
  49 | }
  50 | 
  51 | // executeGraphQL executes a GraphQL query against the Linear API
  52 | func (c *LinearClient) executeGraphQL(query string, variables map[string]interface{}) (*GraphQLResponse, error) {
  53 | 	// Create the request body
  54 | 	reqBody := GraphQLRequest{
  55 | 		Query:     query,
  56 | 		Variables: variables,
  57 | 	}
  58 | 
  59 | 	// Marshal the request body to JSON
  60 | 	reqBodyBytes, err := json.Marshal(reqBody)
  61 | 	if err != nil {
  62 | 		return nil, fmt.Errorf("failed to marshal request body: %w", err)
  63 | 	}
  64 | 
  65 | 	// Create the HTTP request
  66 | 	req, err := http.NewRequest("POST", LinearAPIEndpoint, bytes.NewBuffer(reqBodyBytes))
  67 | 	if err != nil {
  68 | 		return nil, fmt.Errorf("failed to create request: %w", err)
  69 | 	}
  70 | 
  71 | 	// Set headers
  72 | 	req.Header.Set("Content-Type", "application/json")
  73 | 	req.Header.Set("Authorization", c.apiKey)
  74 | 	req.Header.Set("User-Agent", fmt.Sprintf("linear-mcp-go/%s", c.serverVersion))
  75 | 
  76 | 	// Execute the request with rate limiting
  77 | 	var resp *http.Response
  78 | 	err = c.rateLimiter.Enqueue(func() error {
  79 | 		var reqErr error
  80 | 		resp, reqErr = c.httpClient.Do(req)
  81 | 		return reqErr
  82 | 	}, "graphql")
  83 | 
  84 | 	if err != nil {
  85 | 		return nil, fmt.Errorf("failed to execute request: %w", err)
  86 | 	}
  87 | 	defer resp.Body.Close()
  88 | 
  89 | 	// Read the response body
  90 | 	respBody, err := io.ReadAll(resp.Body)
  91 | 	if err != nil {
  92 | 		return nil, fmt.Errorf("failed to read response body: %w", err)
  93 | 	}
  94 | 
  95 | 	// Check for HTTP errors
  96 | 	if resp.StatusCode != http.StatusOK {
  97 | 		return nil, fmt.Errorf("API returned non-200 status code: %d, body: %s", resp.StatusCode, string(respBody))
  98 | 	}
  99 | 
 100 | 	// Parse the response
 101 | 	var graphQLResp GraphQLResponse
 102 | 	if err := json.Unmarshal(respBody, &graphQLResp); err != nil {
 103 | 		return nil, fmt.Errorf("failed to unmarshal response: %w", err)
 104 | 	}
 105 | 
 106 | 	// Check for GraphQL errors
 107 | 	if len(graphQLResp.Errors) > 0 {
 108 | 		return nil, fmt.Errorf("GraphQL error: %s", graphQLResp.Errors[0].Message)
 109 | 	}
 110 | 
 111 | 	return &graphQLResp, nil
 112 | }
 113 | 
 114 | // GetIssue gets an issue by ID
 115 | func (c *LinearClient) GetIssue(issueID string) (*Issue, error) {
 116 | 	query := `
 117 | 		query GetIssue($id: String!) {
 118 | 			issue(id: $id) {
 119 | 				id
 120 | 				identifier
 121 | 				title
 122 | 				description
 123 | 				priority
 124 | 				url
 125 | 				createdAt
 126 | 				updatedAt
 127 | 				state {
 128 | 					id
 129 | 					name
 130 | 				}
 131 | 				assignee {
 132 | 					id
 133 | 					name
 134 | 					email
 135 | 				}
 136 | 				team {
 137 | 					id
 138 | 					name
 139 | 					key
 140 | 				}
 141 | 				project {
 142 | 					id
 143 | 					name
 144 | 				}
 145 | 				projectMilestone {
 146 | 					id
 147 | 					name
 148 | 				}
 149 | 				relations(first: 20) {
 150 | 					nodes {
 151 | 						id
 152 | 						type
 153 | 						relatedIssue {
 154 | 							id
 155 | 							identifier
 156 | 							title
 157 | 							url
 158 | 						}
 159 | 					}
 160 | 				}
 161 | 				inverseRelations(first: 20) {
 162 | 					nodes {
 163 | 						id
 164 | 						type
 165 | 						issue {
 166 | 							id
 167 | 							identifier
 168 | 							title
 169 | 							url
 170 | 						}
 171 | 					}
 172 | 				}
 173 | 				attachments(first: 50) {
 174 | 					nodes {
 175 | 						id
 176 | 						title
 177 | 						subtitle
 178 | 						url
 179 | 						sourceType
 180 | 						metadata
 181 | 						createdAt
 182 | 					}
 183 | 				}
 184 | 			}
 185 | 		}
 186 | 	`
 187 | 
 188 | 	variables := map[string]interface{}{
 189 | 		"id": issueID,
 190 | 	}
 191 | 
 192 | 	resp, err := c.executeGraphQL(query, variables)
 193 | 	if err != nil {
 194 | 		return nil, err
 195 | 	}
 196 | 
 197 | 	// Extract the issue from the response
 198 | 	issueData, ok := resp.Data["issue"].(map[string]interface{})
 199 | 	if !ok || issueData == nil {
 200 | 		return nil, fmt.Errorf("issue %s not found", issueID)
 201 | 	}
 202 | 
 203 | 	// Parse the issue data
 204 | 	var issue Issue
 205 | 	issueBytes, err := json.Marshal(issueData)
 206 | 	if err != nil {
 207 | 		return nil, fmt.Errorf("failed to marshal issue data: %w", err)
 208 | 	}
 209 | 
 210 | 	if err := json.Unmarshal(issueBytes, &issue); err != nil {
 211 | 		return nil, fmt.Errorf("failed to unmarshal issue data: %w", err)
 212 | 	}
 213 | 
 214 | 	return &issue, nil
 215 | }
 216 | 
 217 | // GetProject gets a project by identifier (ID, name, or slug)
 218 | func (c *LinearClient) GetProject(identifier string) (*Project, error) {
 219 | 	// First, try to get the project by ID
 220 | 	project, err := c.getProjectByID(identifier)
 221 | 	if err == nil {
 222 | 		return project, nil
 223 | 	}
 224 | 
 225 | 	// If not found by ID, try to get by name or slug
 226 | 	return c.getProjectByNameOrSlug(identifier)
 227 | }
 228 | 
 229 | // getProjectByID gets a project by its UUID
 230 | func (c *LinearClient) getProjectByID(id string) (*Project, error) {
 231 | 	query := `
 232 | 		query GetProject($id: String!) {
 233 | 			project(id: $id) {
 234 | 				id
 235 | 				name
 236 | 				description
 237 | 				slugId
 238 | 				state
 239 | 				url
 240 | 				createdAt
 241 | 				updatedAt
 242 | 				lead {
 243 | 					id
 244 | 					name
 245 | 					email
 246 | 				}
 247 | 				members {
 248 | 					nodes {
 249 | 						id
 250 | 						name
 251 | 						email
 252 | 					}
 253 | 				}
 254 | 				teams {
 255 | 					nodes {
 256 | 						id
 257 | 						name
 258 | 						key
 259 | 					}
 260 | 				}
 261 | 				initiatives(first: 10) {
 262 | 					nodes {
 263 | 						id
 264 | 						name
 265 | 					}
 266 | 				}
 267 | 				startDate
 268 | 				targetDate
 269 | 			}
 270 | 		}
 271 | 	`
 272 | 
 273 | 	variables := map[string]interface{}{
 274 | 		"id": id,
 275 | 	}
 276 | 
 277 | 	resp, err := c.executeGraphQL(query, variables)
 278 | 	if err != nil {
 279 | 		return nil, err
 280 | 	}
 281 | 
 282 | 	projectData, ok := resp.Data["project"].(map[string]interface{})
 283 | 	if !ok || projectData == nil {
 284 | 		return nil, fmt.Errorf("project with ID %s not found", id)
 285 | 	}
 286 | 
 287 | 	var project Project
 288 | 	projectBytes, err := json.Marshal(projectData)
 289 | 	if err != nil {
 290 | 		return nil, fmt.Errorf("failed to marshal project data: %w", err)
 291 | 	}
 292 | 
 293 | 	if err := json.Unmarshal(projectBytes, &project); err != nil {
 294 | 		return nil, fmt.Errorf("failed to unmarshal project data: %w", err)
 295 | 	}
 296 | 
 297 | 	return &project, nil
 298 | }
 299 | 
 300 | // getProjectByNameOrSlug gets a project by its name or slug
 301 | func (c *LinearClient) getProjectByNameOrSlug(identifier string) (*Project, error) {
 302 | 	query := `
 303 | 		query GetProjectByNameOrSlug($filter: ProjectFilter) {
 304 | 			projects(filter: $filter, first: 1) {
 305 | 				nodes {
 306 | 					id
 307 | 					name
 308 | 					description
 309 | 					slugId
 310 | 					state
 311 | 					url
 312 | 					createdAt
 313 | 					updatedAt
 314 | 					lead {
 315 | 						id
 316 | 						name
 317 | 						email
 318 | 					}
 319 | 					members {
 320 | 						nodes {
 321 | 							id
 322 | 							name
 323 | 							email
 324 | 						}
 325 | 					}
 326 | 					teams {
 327 | 						nodes {
 328 | 							id
 329 | 							name
 330 | 							key
 331 | 						}
 332 | 					}
 333 | 					initiatives(first: 1) {
 334 | 						nodes {
 335 | 							id
 336 | 							name
 337 | 						}
 338 | 					}
 339 | 					startDate
 340 | 					targetDate
 341 | 				}
 342 | 			}
 343 | 		}
 344 | 	`
 345 | 
 346 | 	// Check if the identifier is a slug and extract the slugId
 347 | 	parts := strings.Split(identifier, "-")
 348 | 	slugID := ""
 349 | 	if len(parts) > 1 {
 350 | 		slugID = parts[len(parts)-1]
 351 | 	}
 352 | 
 353 | 	filter := map[string]interface{}{
 354 | 		"or": []map[string]interface{}{
 355 | 			{
 356 | 				"name": map[string]interface{}{"eq": identifier},
 357 | 			},
 358 | 			{
 359 | 				"slugId": map[string]interface{}{"eq": slugID},
 360 | 			},
 361 | 		},
 362 | 	}
 363 | 
 364 | 	variables := map[string]interface{}{
 365 | 		"filter": filter,
 366 | 	}
 367 | 
 368 | 	resp, err := c.executeGraphQL(query, variables)
 369 | 	if err != nil {
 370 | 		return nil, err
 371 | 	}
 372 | 
 373 | 	projectsData, ok := resp.Data["projects"].(map[string]interface{})
 374 | 	if !ok || projectsData == nil {
 375 | 		return nil, fmt.Errorf("project with identifier '%s' not found", identifier)
 376 | 	}
 377 | 
 378 | 	nodes, ok := projectsData["nodes"].([]interface{})
 379 | 	if !ok || len(nodes) == 0 {
 380 | 		return nil, fmt.Errorf("project with identifier '%s' not found", identifier)
 381 | 	}
 382 | 
 383 | 	projectData, ok := nodes[0].(map[string]interface{})
 384 | 	if !ok {
 385 | 		return nil, fmt.Errorf("failed to parse project data for identifier '%s'", identifier)
 386 | 	}
 387 | 
 388 | 	var project Project
 389 | 	projectBytes, err := json.Marshal(projectData)
 390 | 	if err != nil {
 391 | 		return nil, fmt.Errorf("failed to marshal project data: %w", err)
 392 | 	}
 393 | 
 394 | 	if err := json.Unmarshal(projectBytes, &project); err != nil {
 395 | 		return nil, fmt.Errorf("failed to unmarshal project data: %w", err)
 396 | 	}
 397 | 
 398 | 	return &project, nil
 399 | }
 400 | 
 401 | // SearchProjects searches for projects
 402 | func (c *LinearClient) SearchProjects(query string) ([]Project, error) {
 403 | 	graphqlQuery := `
 404 | 		query SearchProjects($filter: ProjectFilter) {
 405 | 			projects(filter: $filter) {
 406 | 				nodes {
 407 | 					id
 408 | 					name
 409 | 					description
 410 | 					slugId
 411 | 					state
 412 | 					url
 413 | 					initiatives(first: 1) {
 414 | 						nodes {
 415 | 							id
 416 | 							name
 417 | 						}
 418 | 					}
 419 | 					lead {
 420 | 						id
 421 | 						name
 422 | 					}
 423 | 					startDate
 424 | 					targetDate
 425 | 				}
 426 | 			}
 427 | 		}
 428 | 	`
 429 | 
 430 | 	filter := map[string]interface{}{
 431 | 		"name": map[string]interface{}{"containsIgnoreCase": query},
 432 | 	}
 433 | 
 434 | 	variables := map[string]interface{}{
 435 | 		"filter": filter,
 436 | 	}
 437 | 
 438 | 	resp, err := c.executeGraphQL(graphqlQuery, variables)
 439 | 	if err != nil {
 440 | 		return nil, err
 441 | 	}
 442 | 
 443 | 	projectsData, ok := resp.Data["projects"].(map[string]interface{})
 444 | 	if !ok || projectsData == nil {
 445 | 		return []Project{}, nil
 446 | 	}
 447 | 
 448 | 	nodes, ok := projectsData["nodes"].([]interface{})
 449 | 	if !ok {
 450 | 		return []Project{}, nil
 451 | 	}
 452 | 
 453 | 	var projects []Project
 454 | 	for _, node := range nodes {
 455 | 		projectData, ok := node.(map[string]interface{})
 456 | 		if !ok {
 457 | 			continue
 458 | 		}
 459 | 
 460 | 		var project Project
 461 | 		projectBytes, err := json.Marshal(projectData)
 462 | 		if err != nil {
 463 | 			return nil, fmt.Errorf("failed to marshal project data: %w", err)
 464 | 		}
 465 | 
 466 | 		if err := json.Unmarshal(projectBytes, &project); err != nil {
 467 | 			return nil, fmt.Errorf("failed to unmarshal project data: %w", err)
 468 | 		}
 469 | 		projects = append(projects, project)
 470 | 	}
 471 | 
 472 | 	return projects, nil
 473 | }
 474 | 
 475 | // CreateProject creates a new project.
 476 | func (c *LinearClient) CreateProject(input ProjectCreateInput) (*Project, error) {
 477 | 	query := `
 478 | 		mutation ProjectCreate($input: ProjectCreateInput!) {
 479 | 			projectCreate(input: $input) {
 480 | 				success
 481 | 				project {
 482 | 					id
 483 | 					name
 484 | 					description
 485 | 					slugId
 486 | 					state
 487 | 					url
 488 | 				}
 489 | 			}
 490 | 		}
 491 | 	`
 492 | 
 493 | 	variables := map[string]interface{}{
 494 | 		"input": input,
 495 | 	}
 496 | 
 497 | 	resp, err := c.executeGraphQL(query, variables)
 498 | 	if err != nil {
 499 | 		return nil, err
 500 | 	}
 501 | 
 502 | 	projectCreateData, ok := resp.Data["projectCreate"].(map[string]interface{})
 503 | 	if !ok || projectCreateData == nil {
 504 | 		return nil, errors.New("failed to create project")
 505 | 	}
 506 | 
 507 | 	success, ok := projectCreateData["success"].(bool)
 508 | 	if !ok || !success {
 509 | 		return nil, errors.New("failed to create project")
 510 | 	}
 511 | 
 512 | 	projectData, ok := projectCreateData["project"].(map[string]interface{})
 513 | 	if !ok || projectData == nil {
 514 | 		return nil, errors.New("failed to create project")
 515 | 	}
 516 | 
 517 | 	var project Project
 518 | 	projectBytes, err := json.Marshal(projectData)
 519 | 	if err != nil {
 520 | 		return nil, fmt.Errorf("failed to marshal project data: %w", err)
 521 | 	}
 522 | 
 523 | 	if err := json.Unmarshal(projectBytes, &project); err != nil {
 524 | 		return nil, fmt.Errorf("failed to unmarshal project data: %w", err)
 525 | 	}
 526 | 
 527 | 	return &project, nil
 528 | }
 529 | 
 530 | // UpdateProject updates an existing project.
 531 | func (c *LinearClient) UpdateProject(id string, input ProjectUpdateInput) (*Project, error) {
 532 | 	query := `
 533 | 		mutation ProjectUpdate($id: String!, $input: ProjectUpdateInput!) {
 534 | 			projectUpdate(id: $id, input: $input) {
 535 | 				success
 536 | 				project {
 537 | 					id
 538 | 					name
 539 | 					description
 540 | 					slugId
 541 | 					state
 542 | 					url
 543 | 				}
 544 | 			}
 545 | 		}
 546 | 	`
 547 | 
 548 | 	variables := map[string]interface{}{
 549 | 		"id":    id,
 550 | 		"input": input,
 551 | 	}
 552 | 
 553 | 	resp, err := c.executeGraphQL(query, variables)
 554 | 	if err != nil {
 555 | 		return nil, err
 556 | 	}
 557 | 
 558 | 	projectUpdateData, ok := resp.Data["projectUpdate"].(map[string]interface{})
 559 | 	if !ok || projectUpdateData == nil {
 560 | 		return nil, errors.New("failed to update project")
 561 | 	}
 562 | 
 563 | 	success, ok := projectUpdateData["success"].(bool)
 564 | 	if !ok || !success {
 565 | 		return nil, errors.New("failed to update project")
 566 | 	}
 567 | 
 568 | 	projectData, ok := projectUpdateData["project"].(map[string]interface{})
 569 | 	if !ok || projectData == nil {
 570 | 		return nil, errors.New("failed to update project")
 571 | 	}
 572 | 
 573 | 	var project Project
 574 | 	projectBytes, err := json.Marshal(projectData)
 575 | 	if err != nil {
 576 | 		return nil, fmt.Errorf("failed to marshal project data: %w", err)
 577 | 	}
 578 | 
 579 | 	if err := json.Unmarshal(projectBytes, &project); err != nil {
 580 | 		return nil, fmt.Errorf("failed to unmarshal project data: %w", err)
 581 | 	}
 582 | 
 583 | 	return &project, nil
 584 | }
 585 | 
 586 | // GetMilestone gets a project milestone by identifier (ID or name).
 587 | func (c *LinearClient) GetMilestone(identifier string) (*ProjectMilestone, error) {
 588 | 	// First, try to get the milestone by ID
 589 | 	milestone, err := c.getMilestoneByID(identifier)
 590 | 	if err == nil {
 591 | 		return milestone, nil
 592 | 	}
 593 | 
 594 | 	// If not found by ID, try to get by name
 595 | 	return c.getMilestoneByName(identifier)
 596 | }
 597 | 
 598 | // getMilestoneByID gets a project milestone by its UUID.
 599 | func (c *LinearClient) getMilestoneByID(id string) (*ProjectMilestone, error) {
 600 | 	query := `
 601 | 		query ProjectMilestone($id: String!) {
 602 | 			projectMilestone(id: $id) {
 603 | 				id
 604 | 				name
 605 | 				description
 606 | 				targetDate
 607 | 				project {
 608 | 					id
 609 | 					name
 610 | 				}
 611 | 			}
 612 | 		}
 613 | 	`
 614 | 
 615 | 	variables := map[string]interface{}{
 616 | 		"id": id,
 617 | 	}
 618 | 
 619 | 	resp, err := c.executeGraphQL(query, variables)
 620 | 	if err != nil {
 621 | 		return nil, err
 622 | 	}
 623 | 
 624 | 	milestoneData, ok := resp.Data["projectMilestone"].(map[string]interface{})
 625 | 	if !ok || milestoneData == nil {
 626 | 		return nil, fmt.Errorf("milestone with ID %s not found", id)
 627 | 	}
 628 | 
 629 | 	var milestone ProjectMilestone
 630 | 	milestoneBytes, err := json.Marshal(milestoneData)
 631 | 	if err != nil {
 632 | 		return nil, fmt.Errorf("failed to marshal milestone data: %w", err)
 633 | 	}
 634 | 
 635 | 	if err := json.Unmarshal(milestoneBytes, &milestone); err != nil {
 636 | 		return nil, fmt.Errorf("failed to unmarshal milestone data: %w", err)
 637 | 	}
 638 | 
 639 | 	return &milestone, nil
 640 | }
 641 | 
 642 | // getMilestoneByName gets a project milestone by its name.
 643 | func (c *LinearClient) getMilestoneByName(name string) (*ProjectMilestone, error) {
 644 | 	query := `
 645 | 		query GetMilestoneByName($filter: ProjectMilestoneFilter) {
 646 | 			projectMilestones(filter: $filter, first: 1) {
 647 | 				nodes {
 648 | 					id
 649 | 					name
 650 | 					description
 651 | 					targetDate
 652 | 					project {
 653 | 						id
 654 | 						name
 655 | 					}
 656 | 				}
 657 | 			}
 658 | 		}
 659 | 	`
 660 | 
 661 | 	filter := map[string]interface{}{
 662 | 		"name": map[string]interface{}{"eq": name},
 663 | 	}
 664 | 
 665 | 	variables := map[string]interface{}{
 666 | 		"filter": filter,
 667 | 	}
 668 | 
 669 | 	resp, err := c.executeGraphQL(query, variables)
 670 | 	if err != nil {
 671 | 		return nil, err
 672 | 	}
 673 | 
 674 | 	milestonesData, ok := resp.Data["projectMilestones"].(map[string]interface{})
 675 | 	if !ok || milestonesData == nil {
 676 | 		return nil, fmt.Errorf("milestone with name '%s' not found", name)
 677 | 	}
 678 | 
 679 | 	nodes, ok := milestonesData["nodes"].([]interface{})
 680 | 	if !ok || len(nodes) == 0 {
 681 | 		return nil, fmt.Errorf("milestone with name '%s' not found", name)
 682 | 	}
 683 | 
 684 | 	milestoneData, ok := nodes[0].(map[string]interface{})
 685 | 	if !ok {
 686 | 		return nil, fmt.Errorf("failed to parse milestone data for name '%s'", name)
 687 | 	}
 688 | 
 689 | 	var milestone ProjectMilestone
 690 | 	milestoneBytes, err := json.Marshal(milestoneData)
 691 | 	if err != nil {
 692 | 		return nil, fmt.Errorf("failed to marshal milestone data: %w", err)
 693 | 	}
 694 | 
 695 | 	if err := json.Unmarshal(milestoneBytes, &milestone); err != nil {
 696 | 		return nil, fmt.Errorf("failed to unmarshal milestone data: %w", err)
 697 | 	}
 698 | 
 699 | 	return &milestone, nil
 700 | }
 701 | 
 702 | // UpdateMilestone updates an existing project milestone.
 703 | func (c *LinearClient) UpdateMilestone(id string, input ProjectMilestoneUpdateInput) (*ProjectMilestone, error) {
 704 | 	query := `
 705 | 		mutation ProjectMilestoneUpdate($id: String!, $input: ProjectMilestoneUpdateInput!) {
 706 | 			projectMilestoneUpdate(id: $id, input: $input) {
 707 | 				success
 708 | 				projectMilestone {
 709 | 					id
 710 | 					name
 711 | 					description
 712 | 					targetDate
 713 | 					project {
 714 | 						id
 715 | 						name
 716 | 					}
 717 | 				}
 718 | 			}
 719 | 		}
 720 | 	`
 721 | 
 722 | 	variables := map[string]interface{}{
 723 | 		"id":    id,
 724 | 		"input": input,
 725 | 	}
 726 | 
 727 | 	resp, err := c.executeGraphQL(query, variables)
 728 | 	if err != nil {
 729 | 		return nil, err
 730 | 	}
 731 | 
 732 | 	milestoneUpdateData, ok := resp.Data["projectMilestoneUpdate"].(map[string]interface{})
 733 | 	if !ok || milestoneUpdateData == nil {
 734 | 		return nil, errors.New("failed to update milestone")
 735 | 	}
 736 | 
 737 | 	success, ok := milestoneUpdateData["success"].(bool)
 738 | 	if !ok || !success {
 739 | 		return nil, errors.New("failed to update milestone")
 740 | 	}
 741 | 
 742 | 	milestoneData, ok := milestoneUpdateData["projectMilestone"].(map[string]interface{})
 743 | 	if !ok || milestoneData == nil {
 744 | 		return nil, errors.New("failed to update milestone")
 745 | 	}
 746 | 
 747 | 	var milestone ProjectMilestone
 748 | 	milestoneBytes, err := json.Marshal(milestoneData)
 749 | 	if err != nil {
 750 | 		return nil, fmt.Errorf("failed to marshal milestone data: %w", err)
 751 | 	}
 752 | 
 753 | 	if err := json.Unmarshal(milestoneBytes, &milestone); err != nil {
 754 | 		return nil, fmt.Errorf("failed to unmarshal milestone data: %w", err)
 755 | 	}
 756 | 
 757 | 	return &milestone, nil
 758 | }
 759 | 
 760 | // CreateMilestone creates a new project milestone.
 761 | func (c *LinearClient) CreateMilestone(input ProjectMilestoneCreateInput) (*ProjectMilestone, error) {
 762 | 	query := `
 763 | 		mutation ProjectMilestoneCreate($input: ProjectMilestoneCreateInput!) {
 764 | 			projectMilestoneCreate(input: $input) {
 765 | 				success
 766 | 				projectMilestone {
 767 | 					id
 768 | 					name
 769 | 					description
 770 | 					targetDate
 771 | 					project {
 772 | 						id
 773 | 						name
 774 | 					}
 775 | 				}
 776 | 			}
 777 | 		}
 778 | 	`
 779 | 
 780 | 	variables := map[string]interface{}{
 781 | 		"input": input,
 782 | 	}
 783 | 
 784 | 	resp, err := c.executeGraphQL(query, variables)
 785 | 	if err != nil {
 786 | 		return nil, err
 787 | 	}
 788 | 
 789 | 	milestoneCreateData, ok := resp.Data["projectMilestoneCreate"].(map[string]interface{})
 790 | 	if !ok || milestoneCreateData == nil {
 791 | 		return nil, errors.New("failed to create milestone")
 792 | 	}
 793 | 
 794 | 	success, ok := milestoneCreateData["success"].(bool)
 795 | 	if !ok || !success {
 796 | 		return nil, errors.New("failed to create milestone")
 797 | 	}
 798 | 
 799 | 	milestoneData, ok := milestoneCreateData["projectMilestone"].(map[string]interface{})
 800 | 	if !ok || milestoneData == nil {
 801 | 		return nil, errors.New("failed to create milestone")
 802 | 	}
 803 | 
 804 | 	var milestone ProjectMilestone
 805 | 	milestoneBytes, err := json.Marshal(milestoneData)
 806 | 	if err != nil {
 807 | 		return nil, fmt.Errorf("failed to marshal milestone data: %w", err)
 808 | 	}
 809 | 
 810 | 	if err := json.Unmarshal(milestoneBytes, &milestone); err != nil {
 811 | 		return nil, fmt.Errorf("failed to unmarshal milestone data: %w", err)
 812 | 	}
 813 | 
 814 | 	return &milestone, nil
 815 | }
 816 | 
 817 | // GetInitiative gets an initiative by identifier (ID or name)
 818 | func (c *LinearClient) GetInitiative(identifier string) (*Initiative, error) {
 819 | 	// First, try to get the initiative by ID
 820 | 	initiative, err := c.getInitiativeByID(identifier)
 821 | 	if err == nil {
 822 | 		return initiative, nil
 823 | 	}
 824 | 
 825 | 	// If not found by ID, try to get by name
 826 | 	return c.getInitiativeByName(identifier)
 827 | }
 828 | 
 829 | // getInitiativeByID gets an initiative by its UUID
 830 | func (c *LinearClient) getInitiativeByID(id string) (*Initiative, error) {
 831 | 	query := `
 832 | 		query GetInitiative($id: String!) {
 833 | 			initiative(id: $id) {
 834 | 				id
 835 | 				name
 836 | 				description
 837 | 				url
 838 | 			}
 839 | 		}
 840 | 	`
 841 | 
 842 | 	variables := map[string]interface{}{
 843 | 		"id": id,
 844 | 	}
 845 | 
 846 | 	resp, err := c.executeGraphQL(query, variables)
 847 | 	if err != nil {
 848 | 		return nil, err
 849 | 	}
 850 | 
 851 | 	initiativeData, ok := resp.Data["initiative"].(map[string]interface{})
 852 | 	if !ok || initiativeData == nil {
 853 | 		return nil, fmt.Errorf("initiative with ID %s not found", id)
 854 | 	}
 855 | 
 856 | 	var initiative Initiative
 857 | 	initiativeBytes, err := json.Marshal(initiativeData)
 858 | 	if err != nil {
 859 | 		return nil, fmt.Errorf("failed to marshal initiative data: %w", err)
 860 | 	}
 861 | 
 862 | 	if err := json.Unmarshal(initiativeBytes, &initiative); err != nil {
 863 | 		return nil, fmt.Errorf("failed to unmarshal initiative data: %w", err)
 864 | 	}
 865 | 
 866 | 	return &initiative, nil
 867 | }
 868 | 
 869 | // getInitiativeByName gets an initiative by its name
 870 | func (c *LinearClient) getInitiativeByName(name string) (*Initiative, error) {
 871 | 	query := `
 872 | 		query GetInitiativeByName($filter: InitiativeFilter) {
 873 | 			initiatives(filter: $filter, first: 1) {
 874 | 				nodes {
 875 | 					id
 876 | 					name
 877 | 					description
 878 | 					url
 879 | 				}
 880 | 			}
 881 | 		}
 882 | 	`
 883 | 
 884 | 	filter := map[string]interface{}{
 885 | 		"name": map[string]interface{}{"eq": name},
 886 | 	}
 887 | 
 888 | 	variables := map[string]interface{}{
 889 | 		"filter": filter,
 890 | 	}
 891 | 
 892 | 	resp, err := c.executeGraphQL(query, variables)
 893 | 	if err != nil {
 894 | 		return nil, err
 895 | 	}
 896 | 
 897 | 	initiativesData, ok := resp.Data["initiatives"].(map[string]interface{})
 898 | 	if !ok || initiativesData == nil {
 899 | 		return nil, fmt.Errorf("initiative with name '%s' not found", name)
 900 | 	}
 901 | 
 902 | 	nodes, ok := initiativesData["nodes"].([]interface{})
 903 | 	if !ok || len(nodes) == 0 {
 904 | 		return nil, fmt.Errorf("initiative with name '%s' not found", name)
 905 | 	}
 906 | 
 907 | 	initiativeData, ok := nodes[0].(map[string]interface{})
 908 | 	if !ok {
 909 | 		return nil, fmt.Errorf("failed to parse initiative data for name '%s'", name)
 910 | 	}
 911 | 
 912 | 	var initiative Initiative
 913 | 	initiativeBytes, err := json.Marshal(initiativeData)
 914 | 	if err != nil {
 915 | 		return nil, fmt.Errorf("failed to marshal initiative data: %w", err)
 916 | 	}
 917 | 
 918 | 	if err := json.Unmarshal(initiativeBytes, &initiative); err != nil {
 919 | 		return nil, fmt.Errorf("failed to unmarshal initiative data: %w", err)
 920 | 	}
 921 | 
 922 | 	return &initiative, nil
 923 | }
 924 | 
 925 | // UpdateInitiative updates an existing initiative.
 926 | func (c *LinearClient) UpdateInitiative(id string, input InitiativeUpdateInput) (*Initiative, error) {
 927 | 	query := `
 928 | 		mutation InitiativeUpdate($id: String!, $input: InitiativeUpdateInput!) {
 929 | 			initiativeUpdate(id: $id, input: $input) {
 930 | 				success
 931 | 				initiative {
 932 | 					id
 933 | 					name
 934 | 					description
 935 | 					url
 936 | 				}
 937 | 			}
 938 | 		}
 939 | 	`
 940 | 
 941 | 	variables := map[string]interface{}{
 942 | 		"id":    id,
 943 | 		"input": input,
 944 | 	}
 945 | 
 946 | 	resp, err := c.executeGraphQL(query, variables)
 947 | 	if err != nil {
 948 | 		return nil, err
 949 | 	}
 950 | 
 951 | 	initiativeUpdateData, ok := resp.Data["initiativeUpdate"].(map[string]interface{})
 952 | 	if !ok || initiativeUpdateData == nil {
 953 | 		return nil, errors.New("failed to update initiative")
 954 | 	}
 955 | 
 956 | 	success, ok := initiativeUpdateData["success"].(bool)
 957 | 	if !ok || !success {
 958 | 		return nil, errors.New("failed to update initiative")
 959 | 	}
 960 | 
 961 | 	initiativeData, ok := initiativeUpdateData["initiative"].(map[string]interface{})
 962 | 	if !ok || initiativeData == nil {
 963 | 		return nil, errors.New("failed to update initiative")
 964 | 	}
 965 | 
 966 | 	var initiative Initiative
 967 | 	initiativeBytes, err := json.Marshal(initiativeData)
 968 | 	if err != nil {
 969 | 		return nil, fmt.Errorf("failed to marshal initiative data: %w", err)
 970 | 	}
 971 | 
 972 | 	if err := json.Unmarshal(initiativeBytes, &initiative); err != nil {
 973 | 		return nil, fmt.Errorf("failed to unmarshal initiative data: %w", err)
 974 | 	}
 975 | 
 976 | 	return &initiative, nil
 977 | }
 978 | 
 979 | // CreateInitiative creates a new initiative.
 980 | func (c *LinearClient) CreateInitiative(input InitiativeCreateInput) (*Initiative, error) {
 981 | 	query := `
 982 | 		mutation InitiativeCreate($input: InitiativeCreateInput!) {
 983 | 			initiativeCreate(input: $input) {
 984 | 				success
 985 | 				initiative {
 986 | 					id
 987 | 					name
 988 | 					description
 989 | 					url
 990 | 				}
 991 | 			}
 992 | 		}
 993 | 	`
 994 | 
 995 | 	variables := map[string]interface{}{
 996 | 		"input": input,
 997 | 	}
 998 | 
 999 | 	resp, err := c.executeGraphQL(query, variables)
1000 | 	if err != nil {
1001 | 		return nil, err
1002 | 	}
1003 | 
1004 | 	initiativeCreateData, ok := resp.Data["initiativeCreate"].(map[string]interface{})
1005 | 	if !ok || initiativeCreateData == nil {
1006 | 		return nil, errors.New("failed to create initiative")
1007 | 	}
1008 | 
1009 | 	success, ok := initiativeCreateData["success"].(bool)
1010 | 	if !ok || !success {
1011 | 		return nil, errors.New("failed to create initiative")
1012 | 	}
1013 | 
1014 | 	initiativeData, ok := initiativeCreateData["initiative"].(map[string]interface{})
1015 | 	if !ok || initiativeData == nil {
1016 | 		return nil, errors.New("failed to create initiative")
1017 | 	}
1018 | 
1019 | 	var initiative Initiative
1020 | 	initiativeBytes, err := json.Marshal(initiativeData)
1021 | 	if err != nil {
1022 | 		return nil, fmt.Errorf("failed to marshal initiative data: %w", err)
1023 | 	}
1024 | 
1025 | 	if err := json.Unmarshal(initiativeBytes, &initiative); err != nil {
1026 | 		return nil, fmt.Errorf("failed to unmarshal initiative data: %w", err)
1027 | 	}
1028 | 
1029 | 	return &initiative, nil
1030 | }
1031 | 
1032 | // GetIssueComments gets paginated comments for an issue
1033 | func (c *LinearClient) GetIssueComments(input GetIssueCommentsInput) (*PaginatedCommentConnection, error) {
1034 | 	query := `
1035 | 		query GetIssueComments($issueId: String!, $parentId: ID, $first: Int!, $after: String) {
1036 | 			issue(id: $issueId) {
1037 | 				comments(
1038 | 					first: $first,
1039 | 					after: $after,
1040 | 					filter: { parent: { id: { eq: $parentId } } }
1041 | 				) {
1042 | 					nodes {
1043 | 						id
1044 | 						body
1045 | 						createdAt
1046 | 						user {
1047 | 							id
1048 | 							name
1049 | 						}
1050 | 						parent {
1051 | 							id
1052 | 						}
1053 | 						children(first: 1) {
1054 | 							nodes {
1055 | 								id
1056 | 							}
1057 | 						}
1058 | 					}
1059 | 					pageInfo {
1060 | 						hasNextPage
1061 | 						endCursor
1062 | 					}
1063 | 				}
1064 | 			}
1065 | 		}
1066 | 	`
1067 | 
1068 | 	// Set default limit if not provided
1069 | 	limit := 10
1070 | 	if input.Limit > 0 {
1071 | 		limit = input.Limit
1072 | 	}
1073 | 
1074 | 	variables := map[string]interface{}{
1075 | 		"issueId": input.IssueID,
1076 | 		"first":   limit,
1077 | 	}
1078 | 
1079 | 	// Add optional parameters if provided
1080 | 	if input.ParentID != "" {
1081 | 		variables["parentId"] = input.ParentID
1082 | 	}
1083 | 
1084 | 	if input.AfterCursor != "" {
1085 | 		variables["after"] = input.AfterCursor
1086 | 	}
1087 | 
1088 | 	resp, err := c.executeGraphQL(query, variables)
1089 | 	if err != nil {
1090 | 		return nil, err
1091 | 	}
1092 | 
1093 | 	// Extract the issue from the response
1094 | 	issueData, ok := resp.Data["issue"].(map[string]interface{})
1095 | 	if !ok || issueData == nil {
1096 | 		return nil, fmt.Errorf("issue %s not found", input.IssueID)
1097 | 	}
1098 | 
1099 | 	// Extract the comments
1100 | 	commentsData, ok := issueData["comments"].(map[string]interface{})
1101 | 	if !ok || commentsData == nil {
1102 | 		return &PaginatedCommentConnection{
1103 | 			Nodes:    []Comment{},
1104 | 			PageInfo: PageInfo{HasNextPage: false},
1105 | 		}, nil
1106 | 	}
1107 | 
1108 | 	// Parse the comments data
1109 | 	var paginatedComments PaginatedCommentConnection
1110 | 	commentsBytes, err := json.Marshal(commentsData)
1111 | 	if err != nil {
1112 | 		return nil, fmt.Errorf("failed to marshal comments data: %w", err)
1113 | 	}
1114 | 
1115 | 	if err := json.Unmarshal(commentsBytes, &paginatedComments); err != nil {
1116 | 		return nil, fmt.Errorf("failed to unmarshal comments data: %w", err)
1117 | 	}
1118 | 
1119 | 	return &paginatedComments, nil
1120 | }
1121 | 
1122 | // GetIssueByIdentifier gets an issue by its identifier (e.g., "TEAM-123")
1123 | func (c *LinearClient) GetIssueByIdentifier(identifier string) (*Issue, error) {
1124 | 	// Split the identifier into team key and number parts
1125 | 	parts := strings.Split(identifier, "-")
1126 | 	if len(parts) != 2 {
1127 | 		return nil, fmt.Errorf("invalid issue identifier format: %s (expected format: TEAM-123)", identifier)
1128 | 	}
1129 | 
1130 | 	teamKey := parts[0]
1131 | 	numberStr := parts[1]
1132 | 
1133 | 	// Convert the number part to an integer
1134 | 	number, err := strconv.Atoi(numberStr)
1135 | 	if err != nil {
1136 | 		return nil, fmt.Errorf("invalid issue number in identifier: %s", identifier)
1137 | 	}
1138 | 
1139 | 	// Use the issues query with filters for team key and number
1140 | 	query := `
1141 | 		query GetIssueByIdentifier($teamKey: String!, $number: Float!) {
1142 | 			issues(filter: { team: { key: { eq: $teamKey } }, number: { eq: $number } }, first: 1) {
1143 | 				nodes {
1144 | 					id
1145 | 					identifier
1146 | 					title
1147 | 				}
1148 | 			}
1149 | 		}
1150 | 	`
1151 | 
1152 | 	variables := map[string]interface{}{
1153 | 		"teamKey": teamKey,
1154 | 		"number":  float64(number),
1155 | 	}
1156 | 
1157 | 	resp, err := c.executeGraphQL(query, variables)
1158 | 	if err != nil {
1159 | 		return nil, err
1160 | 	}
1161 | 
1162 | 	// Extract the issues from the response
1163 | 	issuesData, ok := resp.Data["issues"].(map[string]interface{})
1164 | 	if !ok || issuesData == nil {
1165 | 		return nil, fmt.Errorf("issue search failed for identifier %s", identifier)
1166 | 	}
1167 | 
1168 | 	nodesData, ok := issuesData["nodes"].([]interface{})
1169 | 	if !ok || nodesData == nil || len(nodesData) == 0 {
1170 | 		return nil, fmt.Errorf("no issue found with identifier %s", identifier)
1171 | 	}
1172 | 
1173 | 	// Get the first issue
1174 | 	issueData, ok := nodesData[0].(map[string]interface{})
1175 | 	if !ok || issueData == nil {
1176 | 		return nil, fmt.Errorf("invalid issue data for identifier %s", identifier)
1177 | 	}
1178 | 
1179 | 	// Parse the issue data
1180 | 	var issue Issue
1181 | 	issueBytes, err := json.Marshal(issueData)
1182 | 	if err != nil {
1183 | 		return nil, fmt.Errorf("failed to marshal issue data: %w", err)
1184 | 	}
1185 | 
1186 | 	if err := json.Unmarshal(issueBytes, &issue); err != nil {
1187 | 		return nil, fmt.Errorf("failed to unmarshal issue data: %w", err)
1188 | 	}
1189 | 
1190 | 	return &issue, nil
1191 | }
1192 | 
1193 | // GetLabelsByName gets labels by name for a team
1194 | func (c *LinearClient) GetLabelsByName(teamID string, labelNames []string) ([]Label, error) {
1195 | 	query := `
1196 | 		query GetLabelsByName($teamId: String!, $names: [String!]!) {
1197 | 			team(id: $teamId) {
1198 | 				labels(filter: { name: { in: $names } }) {
1199 | 					nodes {
1200 | 						id
1201 | 						name
1202 | 					}
1203 | 				}
1204 | 			}
1205 | 		}
1206 | 	`
1207 | 
1208 | 	variables := map[string]interface{}{
1209 | 		"teamId": teamID,
1210 | 		"names":  labelNames,
1211 | 	}
1212 | 
1213 | 	resp, err := c.executeGraphQL(query, variables)
1214 | 	if err != nil {
1215 | 		return nil, err
1216 | 	}
1217 | 
1218 | 	// Extract the team from the response
1219 | 	teamData, ok := resp.Data["team"].(map[string]interface{})
1220 | 	if !ok || teamData == nil {
1221 | 		return nil, fmt.Errorf("team %s not found", teamID)
1222 | 	}
1223 | 
1224 | 	// Extract the labels
1225 | 	labelsData, ok := teamData["labels"].(map[string]interface{})
1226 | 	if !ok || labelsData == nil {
1227 | 		return []Label{}, nil
1228 | 	}
1229 | 
1230 | 	nodesData, ok := labelsData["nodes"].([]interface{})
1231 | 	if !ok || nodesData == nil {
1232 | 		return []Label{}, nil
1233 | 	}
1234 | 
1235 | 	// Parse the labels data
1236 | 	labels := make([]Label, 0, len(nodesData))
1237 | 	for _, nodeData := range nodesData {
1238 | 		labelData, ok := nodeData.(map[string]interface{})
1239 | 		if !ok {
1240 | 			continue
1241 | 		}
1242 | 
1243 | 		label := Label{
1244 | 			ID:   getStringValue(labelData, "id"),
1245 | 			Name: getStringValue(labelData, "name"),
1246 | 		}
1247 | 
1248 | 		labels = append(labels, label)
1249 | 	}
1250 | 
1251 | 	return labels, nil
1252 | }
1253 | 
1254 | // CreateIssue creates a new issue
1255 | func (c *LinearClient) CreateIssue(input CreateIssueInput) (*Issue, error) {
1256 | 	query := `
1257 | 		mutation CreateIssue($input: IssueCreateInput!) {
1258 | 			issueCreate(input: $input) {
1259 | 				success
1260 | 				issue {
1261 | 					id
1262 | 					identifier
1263 | 					title
1264 | 					description
1265 | 					priority
1266 | 					url
1267 | 					createdAt
1268 | 					updatedAt
1269 | 					state {
1270 | 						id
1271 | 						name
1272 | 					}
1273 | 					team {
1274 | 						id
1275 | 						name
1276 | 						key
1277 | 					}
1278 | 					labels {
1279 | 						nodes {
1280 | 							id
1281 | 							name
1282 | 						}
1283 | 					}
1284 | 					project {
1285 | 						id
1286 | 						name
1287 | 					}
1288 | 					projectMilestone {
1289 | 						id
1290 | 						name
1291 | 					}
1292 | 				}
1293 | 			}
1294 | 		}
1295 | 	`
1296 | 
1297 | 	// Prepare variables
1298 | 	variables := map[string]interface{}{
1299 | 		"input": map[string]interface{}{
1300 | 			"title":       input.Title,
1301 | 			"teamId":      input.TeamID,
1302 | 			"description": input.Description,
1303 | 		},
1304 | 	}
1305 | 
1306 | 	if input.Priority != nil {
1307 | 		variables["input"].(map[string]interface{})["priority"] = *input.Priority
1308 | 	}
1309 | 
1310 | 	if input.Status != "" {
1311 | 		variables["input"].(map[string]interface{})["stateId"] = input.Status
1312 | 	}
1313 | 
1314 | 	if input.ParentID != nil && *input.ParentID != "" {
1315 | 		variables["input"].(map[string]interface{})["parentId"] = *input.ParentID
1316 | 	}
1317 | 
1318 | 	if len(input.LabelIDs) > 0 {
1319 | 		variables["input"].(map[string]interface{})["labelIds"] = input.LabelIDs
1320 | 	}
1321 | 
1322 | 	if input.ProjectID != "" {
1323 | 		variables["input"].(map[string]interface{})["projectId"] = input.ProjectID
1324 | 	}
1325 | 
1326 | 	resp, err := c.executeGraphQL(query, variables)
1327 | 	if err != nil {
1328 | 		return nil, err
1329 | 	}
1330 | 
1331 | 	// Extract the issue from the response
1332 | 	issueCreateData, ok := resp.Data["issueCreate"].(map[string]interface{})
1333 | 	if !ok || issueCreateData == nil {
1334 | 		return nil, errors.New("failed to create issue")
1335 | 	}
1336 | 
1337 | 	success, ok := issueCreateData["success"].(bool)
1338 | 	if !ok || !success {
1339 | 		return nil, errors.New("failed to create issue")
1340 | 	}
1341 | 
1342 | 	issueData, ok := issueCreateData["issue"].(map[string]interface{})
1343 | 	if !ok || issueData == nil {
1344 | 		return nil, errors.New("failed to create issue")
1345 | 	}
1346 | 
1347 | 	// Parse the issue data
1348 | 	var issue Issue
1349 | 	issueBytes, err := json.Marshal(issueData)
1350 | 	if err != nil {
1351 | 		return nil, fmt.Errorf("failed to marshal issue data: %w", err)
1352 | 	}
1353 | 
1354 | 	if err := json.Unmarshal(issueBytes, &issue); err != nil {
1355 | 		return nil, fmt.Errorf("failed to unmarshal issue data: %w", err)
1356 | 	}
1357 | 
1358 | 	return &issue, nil
1359 | }
1360 | 
1361 | // UpdateIssue updates an existing issue
1362 | func (c *LinearClient) UpdateIssue(input UpdateIssueInput) (*Issue, error) {
1363 | 	query := `
1364 | 		mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
1365 | 			issueUpdate(id: $id, input: $input) {
1366 | 				success
1367 | 				issue {
1368 | 					id
1369 | 					identifier
1370 | 					title
1371 | 					description
1372 | 					priority
1373 | 					url
1374 | 					createdAt
1375 | 					updatedAt
1376 | 					state {
1377 | 						id
1378 | 						name
1379 | 					}
1380 | 					team {
1381 | 						id
1382 | 						name
1383 | 						key
1384 | 					}
1385 | 				}
1386 | 			}
1387 | 		}
1388 | 	`
1389 | 
1390 | 	// Prepare variables
1391 | 	updateInput := map[string]interface{}{}
1392 | 
1393 | 	if input.Title != "" {
1394 | 		updateInput["title"] = input.Title
1395 | 	}
1396 | 
1397 | 	if input.Description != "" {
1398 | 		updateInput["description"] = input.Description
1399 | 	}
1400 | 
1401 | 	if input.Priority != nil {
1402 | 		updateInput["priority"] = *input.Priority
1403 | 	}
1404 | 
1405 | 	if input.Status != "" {
1406 | 		updateInput["stateId"] = input.Status
1407 | 	}
1408 | 
1409 | 	if input.Status != "" {
1410 | 		updateInput["teamId"] = input.TeamID
1411 | 	}
1412 | 
1413 | 	if input.ProjectID != "" {
1414 | 		updateInput["projectId"] = input.ProjectID
1415 | 	}
1416 | 
1417 | 	if input.MilestoneID != "" {
1418 | 		updateInput["milestoneId"] = input.MilestoneID
1419 | 	}
1420 | 
1421 | 	variables := map[string]interface{}{
1422 | 		"id":    input.ID,
1423 | 		"input": updateInput,
1424 | 	}
1425 | 
1426 | 	resp, err := c.executeGraphQL(query, variables)
1427 | 	if err != nil {
1428 | 		return nil, err
1429 | 	}
1430 | 
1431 | 	// Extract the issue from the response
1432 | 	issueUpdateData, ok := resp.Data["issueUpdate"].(map[string]interface{})
1433 | 	if !ok || issueUpdateData == nil {
1434 | 		return nil, errors.New("failed to update issue")
1435 | 	}
1436 | 
1437 | 	success, ok := issueUpdateData["success"].(bool)
1438 | 	if !ok || !success {
1439 | 		return nil, errors.New("failed to update issue")
1440 | 	}
1441 | 
1442 | 	issueData, ok := issueUpdateData["issue"].(map[string]interface{})
1443 | 	if !ok || issueData == nil {
1444 | 		return nil, errors.New("failed to update issue")
1445 | 	}
1446 | 
1447 | 	// Parse the issue data
1448 | 	var issue Issue
1449 | 	issueBytes, err := json.Marshal(issueData)
1450 | 	if err != nil {
1451 | 		return nil, fmt.Errorf("failed to marshal issue data: %w", err)
1452 | 	}
1453 | 
1454 | 	if err := json.Unmarshal(issueBytes, &issue); err != nil {
1455 | 		return nil, fmt.Errorf("failed to unmarshal issue data: %w", err)
1456 | 	}
1457 | 
1458 | 	return &issue, nil
1459 | }
1460 | 
1461 | // SearchIssues searches for issues with filters
1462 | func (c *LinearClient) SearchIssues(input SearchIssuesInput) ([]LinearIssueResponse, error) {
1463 | 	query := `
1464 | 		query SearchIssues($filter: IssueFilter, $first: Int, $includeArchived: Boolean) {
1465 | 			issues(filter: $filter, first: $first, includeArchived: $includeArchived) {
1466 | 				nodes {
1467 | 					id
1468 | 					identifier
1469 | 					title
1470 | 					description
1471 | 					priority
1472 | 					url
1473 | 					state {
1474 | 						id
1475 | 						name
1476 | 					}
1477 | 					assignee {
1478 | 						id
1479 | 						name
1480 | 					}
1481 | 					labels {
1482 | 						nodes {
1483 | 							id
1484 | 							name
1485 | 						}
1486 | 					}
1487 | 				}
1488 | 			}
1489 | 		}
1490 | 	`
1491 | 
1492 | 	// Build the filter
1493 | 	filter := map[string]interface{}{}
1494 | 
1495 | 	if input.Query != "" {
1496 | 		filter["or"] = []map[string]interface{}{
1497 | 			{"title": map[string]interface{}{"contains": input.Query}},
1498 | 			{"description": map[string]interface{}{"contains": input.Query}},
1499 | 		}
1500 | 	}
1501 | 
1502 | 	if input.TeamID != "" {
1503 | 		filter["team"] = map[string]interface{}{
1504 | 			"id": map[string]interface{}{"eq": input.TeamID},
1505 | 		}
1506 | 	}
1507 | 
1508 | 	if input.Status != "" {
1509 | 		filter["state"] = map[string]interface{}{
1510 | 			"name": map[string]interface{}{"eq": input.Status},
1511 | 		}
1512 | 	}
1513 | 
1514 | 	if input.AssigneeID != "" {
1515 | 		filter["assignee"] = map[string]interface{}{
1516 | 			"id": map[string]interface{}{"eq": input.AssigneeID},
1517 | 		}
1518 | 	}
1519 | 
1520 | 	if len(input.Labels) > 0 {
1521 | 		filter["labels"] = map[string]interface{}{
1522 | 			"some": map[string]interface{}{
1523 | 				"name": map[string]interface{}{"in": input.Labels},
1524 | 			},
1525 | 		}
1526 | 	}
1527 | 
1528 | 	if input.Priority != nil {
1529 | 		filter["priority"] = map[string]interface{}{"eq": *input.Priority}
1530 | 	}
1531 | 
1532 | 	if input.Estimate != nil {
1533 | 		filter["estimate"] = map[string]interface{}{"eq": *input.Estimate}
1534 | 	}
1535 | 
1536 | 	// Set default limit if not provided
1537 | 	limit := 10
1538 | 	if input.Limit > 0 {
1539 | 		limit = input.Limit
1540 | 	}
1541 | 
1542 | 	variables := map[string]interface{}{
1543 | 		"filter":          filter,
1544 | 		"first":           limit,
1545 | 		"includeArchived": input.IncludeArchived,
1546 | 	}
1547 | 
1548 | 	resp, err := c.executeGraphQL(query, variables)
1549 | 	if err != nil {
1550 | 		return nil, err
1551 | 	}
1552 | 
1553 | 	// Extract the issues from the response
1554 | 	issuesData, ok := resp.Data["issues"].(map[string]interface{})
1555 | 	if !ok || issuesData == nil {
1556 | 		return []LinearIssueResponse{}, nil
1557 | 	}
1558 | 
1559 | 	nodesData, ok := issuesData["nodes"].([]interface{})
1560 | 	if !ok || nodesData == nil {
1561 | 		return []LinearIssueResponse{}, nil
1562 | 	}
1563 | 
1564 | 	// Parse the issues data
1565 | 	issues := make([]LinearIssueResponse, 0, len(nodesData))
1566 | 	for _, nodeData := range nodesData {
1567 | 		issueData, ok := nodeData.(map[string]interface{})
1568 | 		if !ok {
1569 | 			continue
1570 | 		}
1571 | 
1572 | 		// Extract state name
1573 | 		var stateName string
1574 | 		if stateData, ok := issueData["state"].(map[string]interface{}); ok && stateData != nil {
1575 | 			if name, ok := stateData["name"].(string); ok {
1576 | 				stateName = name
1577 | 			}
1578 | 		}
1579 | 
1580 | 		// Create the issue response
1581 | 		issue := LinearIssueResponse{
1582 | 			ID:         getStringValue(issueData, "id"),
1583 | 			Identifier: getStringValue(issueData, "identifier"),
1584 | 			Title:      getStringValue(issueData, "title"),
1585 | 			URL:        getStringValue(issueData, "url"),
1586 | 			StateName:  stateName,
1587 | 		}
1588 | 
1589 | 		// Extract priority
1590 | 		if priority, ok := issueData["priority"].(float64); ok {
1591 | 			issue.Priority = int(priority)
1592 | 		}
1593 | 
1594 | 		issues = append(issues, issue)
1595 | 	}
1596 | 
1597 | 	return issues, nil
1598 | }
1599 | 
1600 | // GetUserIssues gets issues assigned to a user
1601 | func (c *LinearClient) GetUserIssues(input GetUserIssuesInput) ([]LinearIssueResponse, error) {
1602 | 	var userID string
1603 | 	var err error
1604 | 
1605 | 	if input.UserID == "" {
1606 | 		// Get the current user's ID
1607 | 		userID, err = c.getCurrentUserID()
1608 | 		if err != nil {
1609 | 			return nil, err
1610 | 		}
1611 | 	} else {
1612 | 		userID = input.UserID
1613 | 	}
1614 | 
1615 | 	query := `
1616 | 		query GetUserIssues($userId: String!, $first: Int, $includeArchived: Boolean) {
1617 | 			user(id: $userId) {
1618 | 				assignedIssues(first: $first, includeArchived: $includeArchived) {
1619 | 					nodes {
1620 | 						id
1621 | 						identifier
1622 | 						title
1623 | 						description
1624 | 						priority
1625 | 						url
1626 | 						state {
1627 | 							id
1628 | 							name
1629 | 						}
1630 | 					}
1631 | 				}
1632 | 			}
1633 | 		}
1634 | 	`
1635 | 
1636 | 	// Set default limit if not provided
1637 | 	limit := 50
1638 | 	if input.Limit > 0 {
1639 | 		limit = input.Limit
1640 | 	}
1641 | 
1642 | 	variables := map[string]interface{}{
1643 | 		"userId":          userID,
1644 | 		"first":           limit,
1645 | 		"includeArchived": input.IncludeArchived,
1646 | 	}
1647 | 
1648 | 	resp, err := c.executeGraphQL(query, variables)
1649 | 	if err != nil {
1650 | 		return nil, err
1651 | 	}
1652 | 
1653 | 	// Extract the user from the response
1654 | 	userData, ok := resp.Data["user"].(map[string]interface{})
1655 | 	if !ok || userData == nil {
1656 | 		return nil, fmt.Errorf("user %s not found", userID)
1657 | 	}
1658 | 
1659 | 	// Extract the assigned issues
1660 | 	assignedIssuesData, ok := userData["assignedIssues"].(map[string]interface{})
1661 | 	if !ok || assignedIssuesData == nil {
1662 | 		return []LinearIssueResponse{}, nil
1663 | 	}
1664 | 
1665 | 	nodesData, ok := assignedIssuesData["nodes"].([]interface{})
1666 | 	if !ok || nodesData == nil {
1667 | 		return []LinearIssueResponse{}, nil
1668 | 	}
1669 | 
1670 | 	// Parse the issues data
1671 | 	issues := make([]LinearIssueResponse, 0, len(nodesData))
1672 | 	for _, nodeData := range nodesData {
1673 | 		issueData, ok := nodeData.(map[string]interface{})
1674 | 		if !ok {
1675 | 			continue
1676 | 		}
1677 | 
1678 | 		// Extract state name
1679 | 		var stateName string
1680 | 		if stateData, ok := issueData["state"].(map[string]interface{}); ok && stateData != nil {
1681 | 			if name, ok := stateData["name"].(string); ok {
1682 | 				stateName = name
1683 | 			}
1684 | 		}
1685 | 
1686 | 		// Create the issue response
1687 | 		issue := LinearIssueResponse{
1688 | 			ID:         getStringValue(issueData, "id"),
1689 | 			Identifier: getStringValue(issueData, "identifier"),
1690 | 			Title:      getStringValue(issueData, "title"),
1691 | 			URL:        getStringValue(issueData, "url"),
1692 | 			StateName:  stateName,
1693 | 		}
1694 | 
1695 | 		// Extract priority
1696 | 		if priority, ok := issueData["priority"].(float64); ok {
1697 | 			issue.Priority = int(priority)
1698 | 		}
1699 | 
1700 | 		issues = append(issues, issue)
1701 | 	}
1702 | 
1703 | 	return issues, nil
1704 | }
1705 | 
1706 | // AddComment adds a comment to an issue
1707 | func (c *LinearClient) AddComment(input AddCommentInput) (*Comment, *Issue, error) {
1708 | 	query := `
1709 | 		mutation AddComment($input: CommentCreateInput!) {
1710 | 			commentCreate(input: $input) {
1711 | 				success
1712 | 				comment {
1713 | 					id
1714 | 					body
1715 | 					url
1716 | 					createdAt
1717 | 					user {
1718 | 						id
1719 | 						name
1720 | 					}
1721 | 					issue {
1722 | 						id
1723 | 						identifier
1724 | 						title
1725 | 						url
1726 | 					}
1727 | 				}
1728 | 			}
1729 | 		}
1730 | 	`
1731 | 
1732 | 	// Prepare variables
1733 | 	commentInput := map[string]interface{}{
1734 | 		"issueId": input.IssueID,
1735 | 		"body":    input.Body,
1736 | 	}
1737 | 
1738 | 	if input.CreateAsUser != "" {
1739 | 		commentInput["createAsUser"] = input.CreateAsUser
1740 | 	}
1741 | 
1742 | 	if input.ParentID != "" {
1743 | 		commentInput["parentId"] = input.ParentID
1744 | 	}
1745 | 
1746 | 	variables := map[string]interface{}{
1747 | 		"input": commentInput,
1748 | 	}
1749 | 
1750 | 	resp, err := c.executeGraphQL(query, variables)
1751 | 	if err != nil {
1752 | 		return nil, nil, err
1753 | 	}
1754 | 
1755 | 	// Extract the comment from the response
1756 | 	commentCreateData, ok := resp.Data["commentCreate"].(map[string]interface{})
1757 | 	if !ok || commentCreateData == nil {
1758 | 		return nil, nil, errors.New("failed to create comment")
1759 | 	}
1760 | 
1761 | 	success, ok := commentCreateData["success"].(bool)
1762 | 	if !ok || !success {
1763 | 		return nil, nil, errors.New("failed to create comment")
1764 | 	}
1765 | 
1766 | 	commentData, ok := commentCreateData["comment"].(map[string]interface{})
1767 | 	if !ok || commentData == nil {
1768 | 		return nil, nil, errors.New("failed to create comment")
1769 | 	}
1770 | 
1771 | 	issueData, ok := commentData["issue"].(map[string]interface{})
1772 | 	if !ok || issueData == nil {
1773 | 		return nil, nil, errors.New("failed to get issue for comment")
1774 | 	}
1775 | 
1776 | 	// Parse the comment data
1777 | 	var comment Comment
1778 | 	commentBytes, err := json.Marshal(commentData)
1779 | 	if err != nil {
1780 | 		return nil, nil, fmt.Errorf("failed to marshal comment data: %w", err)
1781 | 	}
1782 | 
1783 | 	if err := json.Unmarshal(commentBytes, &comment); err != nil {
1784 | 		return nil, nil, fmt.Errorf("failed to unmarshal comment data: %w", err)
1785 | 	}
1786 | 
1787 | 	// Parse the issue data
1788 | 	var issue Issue
1789 | 	issueBytes, err := json.Marshal(issueData)
1790 | 	if err != nil {
1791 | 		return nil, nil, fmt.Errorf("failed to marshal issue data: %w", err)
1792 | 	}
1793 | 
1794 | 	if err := json.Unmarshal(issueBytes, &issue); err != nil {
1795 | 		return nil, nil, fmt.Errorf("failed to unmarshal issue data: %w", err)
1796 | 	}
1797 | 
1798 | 	return &comment, &issue, nil
1799 | }
1800 | 
1801 | // UpdateComment updates an existing comment
1802 | func (c *LinearClient) UpdateComment(input UpdateCommentInput) (*Comment, *Issue, error) {
1803 | 	query := `
1804 | 		mutation UpdateComment($id: String!, $input: CommentUpdateInput!) {
1805 | 			commentUpdate(id: $id, input: $input) {
1806 | 				success
1807 | 				comment {
1808 | 					id
1809 | 					body
1810 | 					url
1811 | 					createdAt
1812 | 					user {
1813 | 						id
1814 | 						name
1815 | 					}
1816 | 					issue {
1817 | 						id
1818 | 						identifier
1819 | 						title
1820 | 						url
1821 | 					}
1822 | 				}
1823 | 			}
1824 | 		}
1825 | 	`
1826 | 
1827 | 	// Prepare variables
1828 | 	commentInput := map[string]interface{}{
1829 | 		"body": input.Body,
1830 | 	}
1831 | 
1832 | 	variables := map[string]interface{}{
1833 | 		"id":    input.CommentID,
1834 | 		"input": commentInput,
1835 | 	}
1836 | 
1837 | 	resp, err := c.executeGraphQL(query, variables)
1838 | 	if err != nil {
1839 | 		return nil, nil, err
1840 | 	}
1841 | 
1842 | 	// Extract the comment from the response
1843 | 	commentUpdateData, ok := resp.Data["commentUpdate"].(map[string]interface{})
1844 | 	if !ok || commentUpdateData == nil {
1845 | 		return nil, nil, errors.New("failed to update comment")
1846 | 	}
1847 | 
1848 | 	success, ok := commentUpdateData["success"].(bool)
1849 | 	if !ok || !success {
1850 | 		return nil, nil, errors.New("failed to update comment")
1851 | 	}
1852 | 
1853 | 	commentData, ok := commentUpdateData["comment"].(map[string]interface{})
1854 | 	if !ok || commentData == nil {
1855 | 		return nil, nil, errors.New("failed to update comment")
1856 | 	}
1857 | 
1858 | 	issueData, ok := commentData["issue"].(map[string]interface{})
1859 | 	if !ok || issueData == nil {
1860 | 		return nil, nil, errors.New("failed to get issue for comment")
1861 | 	}
1862 | 
1863 | 	// Parse the comment data
1864 | 	var comment Comment
1865 | 	commentBytes, err := json.Marshal(commentData)
1866 | 	if err != nil {
1867 | 		return nil, nil, fmt.Errorf("failed to marshal comment data: %w", err)
1868 | 	}
1869 | 
1870 | 	if err := json.Unmarshal(commentBytes, &comment); err != nil {
1871 | 		return nil, nil, fmt.Errorf("failed to unmarshal comment data: %w", err)
1872 | 	}
1873 | 
1874 | 	// Parse the issue data
1875 | 	var issue Issue
1876 | 	issueBytes, err := json.Marshal(issueData)
1877 | 	if err != nil {
1878 | 		return nil, nil, fmt.Errorf("failed to marshal issue data: %w", err)
1879 | 	}
1880 | 
1881 | 	if err := json.Unmarshal(issueBytes, &issue); err != nil {
1882 | 		return nil, nil, fmt.Errorf("failed to unmarshal issue data: %w", err)
1883 | 	}
1884 | 
1885 | 	return &comment, &issue, nil
1886 | }
1887 | 
1888 | // GetComment gets a comment by its ID
1889 | func (c *LinearClient) GetComment(commentID string) (*Comment, error) {
1890 | 	query := `
1891 | 		query GetComment($id: String!) {
1892 | 			comment(id: $id) {
1893 | 				id
1894 | 				body
1895 | 				url
1896 | 				createdAt
1897 | 				user {
1898 | 					id
1899 | 					name
1900 | 				}
1901 | 				issue {
1902 | 					id
1903 | 					identifier
1904 | 				}
1905 | 			}
1906 | 		}
1907 | 	`
1908 | 
1909 | 	variables := map[string]interface{}{
1910 | 		"id": commentID,
1911 | 	}
1912 | 
1913 | 	resp, err := c.executeGraphQL(query, variables)
1914 | 	if err != nil {
1915 | 		return nil, err
1916 | 	}
1917 | 
1918 | 	// Extract the comment from the response
1919 | 	commentData, ok := resp.Data["comment"].(map[string]interface{})
1920 | 	if !ok || commentData == nil {
1921 | 		return nil, errors.New("comment not found")
1922 | 	}
1923 | 
1924 | 	// Parse the comment data
1925 | 	var comment Comment
1926 | 	commentBytes, err := json.Marshal(commentData)
1927 | 	if err != nil {
1928 | 		return nil, fmt.Errorf("failed to marshal comment data: %w", err)
1929 | 	}
1930 | 
1931 | 	if err := json.Unmarshal(commentBytes, &comment); err != nil {
1932 | 		return nil, fmt.Errorf("failed to unmarshal comment data: %w", err)
1933 | 	}
1934 | 
1935 | 	return &comment, nil
1936 | }
1937 | 
1938 | // GetCommentByHash gets a comment by its hash (shorthand ID)
1939 | func (c *LinearClient) GetCommentByHash(hash string) (*Comment, error) {
1940 | 	query := `
1941 | 		query GetCommentByHash($hash: String!) {
1942 | 			comment(hash: $hash) {
1943 | 				id
1944 | 				body
1945 | 				url
1946 | 				createdAt
1947 | 				user {
1948 | 					id
1949 | 					name
1950 | 				}
1951 | 			}
1952 | 		}
1953 | 	`
1954 | 
1955 | 	variables := map[string]interface{}{
1956 | 		"hash": hash,
1957 | 	}
1958 | 
1959 | 	resp, err := c.executeGraphQL(query, variables)
1960 | 	if err != nil {
1961 | 		return nil, err
1962 | 	}
1963 | 
1964 | 	// Extract the comment from the response
1965 | 	commentData, ok := resp.Data["comment"].(map[string]interface{})
1966 | 	if !ok || commentData == nil {
1967 | 		return nil, errors.New("comment not found")
1968 | 	}
1969 | 
1970 | 	// Parse the comment data
1971 | 	var comment Comment
1972 | 	commentBytes, err := json.Marshal(commentData)
1973 | 	if err != nil {
1974 | 		return nil, fmt.Errorf("failed to marshal comment data: %w", err)
1975 | 	}
1976 | 
1977 | 	if err := json.Unmarshal(commentBytes, &comment); err != nil {
1978 | 		return nil, fmt.Errorf("failed to unmarshal comment data: %w", err)
1979 | 	}
1980 | 
1981 | 	return &comment, nil
1982 | }
1983 | 
1984 | // GetTeamIssues gets issues for a team
1985 | func (c *LinearClient) GetTeamIssues(teamID string) ([]LinearIssueResponse, error) {
1986 | 	query := `
1987 | 		query GetTeamIssues($teamId: ID!) {
1988 | 			team(id: $teamId) {
1989 | 				issues {
1990 | 					nodes {
1991 | 						id
1992 | 						identifier
1993 | 						title
1994 | 						description
1995 | 						priority
1996 | 						url
1997 | 						state {
1998 | 							id
1999 | 							name
2000 | 						}
2001 | 						assignee {
2002 | 							id
2003 | 							name
2004 | 						}
2005 | 					}
2006 | 				}
2007 | 			}
2008 | 		}
2009 | 	`
2010 | 
2011 | 	variables := map[string]interface{}{
2012 | 		"teamId": teamID,
2013 | 	}
2014 | 
2015 | 	resp, err := c.executeGraphQL(query, variables)
2016 | 	if err != nil {
2017 | 		return nil, err
2018 | 	}
2019 | 
2020 | 	// Extract the team from the response
2021 | 	teamData, ok := resp.Data["team"].(map[string]interface{})
2022 | 	if !ok || teamData == nil {
2023 | 		return nil, fmt.Errorf("team %s not found", teamID)
2024 | 	}
2025 | 
2026 | 	// Extract the issues
2027 | 	issuesData, ok := teamData["issues"].(map[string]interface{})
2028 | 	if !ok || issuesData == nil {
2029 | 		return []LinearIssueResponse{}, nil
2030 | 	}
2031 | 
2032 | 	nodesData, ok := issuesData["nodes"].([]interface{})
2033 | 	if !ok || nodesData == nil {
2034 | 		return []LinearIssueResponse{}, nil
2035 | 	}
2036 | 
2037 | 	// Parse the issues data
2038 | 	issues := make([]LinearIssueResponse, 0, len(nodesData))
2039 | 	for _, nodeData := range nodesData {
2040 | 		issueData, ok := nodeData.(map[string]interface{})
2041 | 		if !ok {
2042 | 			continue
2043 | 		}
2044 | 
2045 | 		// Extract state name
2046 | 		var stateName string
2047 | 		if stateData, ok := issueData["state"].(map[string]interface{}); ok && stateData != nil {
2048 | 			if name, ok := stateData["name"].(string); ok {
2049 | 				stateName = name
2050 | 			}
2051 | 		}
2052 | 
2053 | 		// Create the issue response
2054 | 		issue := LinearIssueResponse{
2055 | 			ID:         getStringValue(issueData, "id"),
2056 | 			Identifier: getStringValue(issueData, "identifier"),
2057 | 			Title:      getStringValue(issueData, "title"),
2058 | 			URL:        getStringValue(issueData, "url"),
2059 | 			StateName:  stateName,
2060 | 		}
2061 | 
2062 | 		// Extract priority
2063 | 		if priority, ok := issueData["priority"].(float64); ok {
2064 | 			issue.Priority = int(priority)
2065 | 		}
2066 | 
2067 | 		issues = append(issues, issue)
2068 | 	}
2069 | 
2070 | 	return issues, nil
2071 | }
2072 | 
2073 | // GetViewer gets the current user
2074 | func (c *LinearClient) GetViewer() (*User, []Team, *Organization, error) {
2075 | 	query := `
2076 | 		query GetViewer {
2077 | 			viewer {
2078 | 				id
2079 | 				name
2080 | 				email
2081 | 				admin
2082 | 				teams {
2083 | 					nodes {
2084 | 						id
2085 | 						name
2086 | 						key
2087 | 					}
2088 | 				}
2089 | 				organization {
2090 | 					id
2091 | 					name
2092 | 					urlKey
2093 | 				}
2094 | 			}
2095 | 		}
2096 | 	`
2097 | 
2098 | 	resp, err := c.executeGraphQL(query, nil)
2099 | 	if err != nil {
2100 | 		return nil, nil, nil, err
2101 | 	}
2102 | 
2103 | 	// Extract the viewer from the response
2104 | 	viewerData, ok := resp.Data["viewer"].(map[string]interface{})
2105 | 	if !ok || viewerData == nil {
2106 | 		return nil, nil, nil, errors.New("failed to get viewer")
2107 | 	}
2108 | 
2109 | 	// Parse the user data
2110 | 	var user User
2111 | 	user.ID = getStringValue(viewerData, "id")
2112 | 	user.Name = getStringValue(viewerData, "name")
2113 | 	user.Email = getStringValue(viewerData, "email")
2114 | 	if admin, ok := viewerData["admin"].(bool); ok {
2115 | 		user.Admin = admin
2116 | 	}
2117 | 
2118 | 	// Extract teams
2119 | 	var teams []Team
2120 | 	if teamsData, ok := viewerData["teams"].(map[string]interface{}); ok && teamsData != nil {
2121 | 		if nodesData, ok := teamsData["nodes"].([]interface{}); ok && nodesData != nil {
2122 | 			teams = make([]Team, 0, len(nodesData))
2123 | 			for _, nodeData := range nodesData {
2124 | 				teamData, ok := nodeData.(map[string]interface{})
2125 | 				if !ok {
2126 | 					continue
2127 | 				}
2128 | 
2129 | 				team := Team{
2130 | 					ID:   getStringValue(teamData, "id"),
2131 | 					Name: getStringValue(teamData, "name"),
2132 | 					Key:  getStringValue(teamData, "key"),
2133 | 				}
2134 | 				teams = append(teams, team)
2135 | 			}
2136 | 		}
2137 | 	}
2138 | 
2139 | 	// Extract organization
2140 | 	var org Organization
2141 | 	if orgData, ok := viewerData["organization"].(map[string]interface{}); ok && orgData != nil {
2142 | 		org.ID = getStringValue(orgData, "id")
2143 | 		org.Name = getStringValue(orgData, "name")
2144 | 		org.URLKey = getStringValue(orgData, "urlKey")
2145 | 	}
2146 | 
2147 | 	return &user, teams, &org, nil
2148 | }
2149 | 
2150 | // GetOrganization gets the organization
2151 | func (c *LinearClient) GetOrganization() (*Organization, error) {
2152 | 	query := `
2153 | 		query GetOrganization {
2154 | 			organization {
2155 | 				id
2156 | 				name
2157 | 				urlKey
2158 | 				teams {
2159 | 					nodes {
2160 | 						id
2161 | 						name
2162 | 						key
2163 | 					}
2164 | 				}
2165 | 				users {
2166 | 					nodes {
2167 | 						id
2168 | 						name
2169 | 						email
2170 | 						admin
2171 | 						active
2172 | 					}
2173 | 				}
2174 | 			}
2175 | 		}
2176 | 	`
2177 | 
2178 | 	resp, err := c.executeGraphQL(query, nil)
2179 | 	if err != nil {
2180 | 		return nil, err
2181 | 	}
2182 | 
2183 | 	// Extract the organization from the response
2184 | 	orgData, ok := resp.Data["organization"].(map[string]interface{})
2185 | 	if !ok || orgData == nil {
2186 | 		return nil, errors.New("failed to get organization")
2187 | 	}
2188 | 
2189 | 	// Parse the organization data
2190 | 	var org Organization
2191 | 	org.ID = getStringValue(orgData, "id")
2192 | 	org.Name = getStringValue(orgData, "name")
2193 | 	org.URLKey = getStringValue(orgData, "urlKey")
2194 | 
2195 | 	// Extract teams
2196 | 	if teamsData, ok := orgData["teams"].(map[string]interface{}); ok && teamsData != nil {
2197 | 		if nodesData, ok := teamsData["nodes"].([]interface{}); ok && nodesData != nil {
2198 | 			org.Teams = make([]Team, 0, len(nodesData))
2199 | 			for _, nodeData := range nodesData {
2200 | 				teamData, ok := nodeData.(map[string]interface{})
2201 | 				if !ok {
2202 | 					continue
2203 | 				}
2204 | 
2205 | 				team := Team{
2206 | 					ID:   getStringValue(teamData, "id"),
2207 | 					Name: getStringValue(teamData, "name"),
2208 | 					Key:  getStringValue(teamData, "key"),
2209 | 				}
2210 | 				org.Teams = append(org.Teams, team)
2211 | 			}
2212 | 		}
2213 | 	}
2214 | 
2215 | 	// Extract users
2216 | 	if usersData, ok := orgData["users"].(map[string]interface{}); ok && usersData != nil {
2217 | 		if nodesData, ok := usersData["nodes"].([]interface{}); ok && nodesData != nil {
2218 | 			org.Users = make([]User, 0, len(nodesData))
2219 | 			for _, nodeData := range nodesData {
2220 | 				userData, ok := nodeData.(map[string]interface{})
2221 | 				if !ok {
2222 | 					continue
2223 | 				}
2224 | 
2225 | 				user := User{
2226 | 					ID:    getStringValue(userData, "id"),
2227 | 					Name:  getStringValue(userData, "name"),
2228 | 					Email: getStringValue(userData, "email"),
2229 | 				}
2230 | 
2231 | 				if admin, ok := userData["admin"].(bool); ok {
2232 | 					user.Admin = admin
2233 | 				}
2234 | 
2235 | 				org.Users = append(org.Users, user)
2236 | 			}
2237 | 		}
2238 | 	}
2239 | 
2240 | 	return &org, nil
2241 | }
2242 | 
2243 | // ListIssues lists issues
2244 | func (c *LinearClient) ListIssues() ([]LinearIssueResponse, error) {
2245 | 	query := `
2246 | 		query ListIssues {
2247 | 			issues(first: 50, orderBy: updatedAt) {
2248 | 				nodes {
2249 | 					id
2250 | 					identifier
2251 | 					title
2252 | 					priority
2253 | 					url
2254 | 					state {
2255 | 						name
2256 | 					}
2257 | 					assignee {
2258 | 						name
2259 | 					}
2260 | 					team {
2261 | 						name
2262 | 					}
2263 | 				}
2264 | 			}
2265 | 		}
2266 | 	`
2267 | 
2268 | 	resp, err := c.executeGraphQL(query, nil)
2269 | 	if err != nil {
2270 | 		return nil, err
2271 | 	}
2272 | 
2273 | 	// Extract the issues from the response
2274 | 	issuesData, ok := resp.Data["issues"].(map[string]interface{})
2275 | 	if !ok || issuesData == nil {
2276 | 		return []LinearIssueResponse{}, nil
2277 | 	}
2278 | 
2279 | 	nodesData, ok := issuesData["nodes"].([]interface{})
2280 | 	if !ok || nodesData == nil {
2281 | 		return []LinearIssueResponse{}, nil
2282 | 	}
2283 | 
2284 | 	// Parse the issues data
2285 | 	issues := make([]LinearIssueResponse, 0, len(nodesData))
2286 | 	for _, nodeData := range nodesData {
2287 | 		issueData, ok := nodeData.(map[string]interface{})
2288 | 		if !ok {
2289 | 			continue
2290 | 		}
2291 | 
2292 | 		// Extract state name
2293 | 		var stateName string
2294 | 		if stateData, ok := issueData["state"].(map[string]interface{}); ok && stateData != nil {
2295 | 			if name, ok := stateData["name"].(string); ok {
2296 | 				stateName = name
2297 | 			}
2298 | 		}
2299 | 
2300 | 		// Create the issue response
2301 | 		issue := LinearIssueResponse{
2302 | 			ID:         getStringValue(issueData, "id"),
2303 | 			Identifier: getStringValue(issueData, "identifier"),
2304 | 			Title:      getStringValue(issueData, "title"),
2305 | 			URL:        getStringValue(issueData, "url"),
2306 | 			StateName:  stateName,
2307 | 		}
2308 | 
2309 | 		// Extract priority
2310 | 		if priority, ok := issueData["priority"].(float64); ok {
2311 | 			issue.Priority = int(priority)
2312 | 		}
2313 | 
2314 | 		issues = append(issues, issue)
2315 | 	}
2316 | 
2317 | 	return issues, nil
2318 | }
2319 | 
2320 | // getCurrentUserID gets the current user's ID
2321 | func (c *LinearClient) getCurrentUserID() (string, error) {
2322 | 	query := `
2323 | 		query GetCurrentUser {
2324 | 			viewer {
2325 | 				id
2326 | 			}
2327 | 		}
2328 | 	`
2329 | 
2330 | 	resp, err := c.executeGraphQL(query, nil)
2331 | 	if err != nil {
2332 | 		return "", err
2333 | 	}
2334 | 
2335 | 	// Extract the viewer from the response
2336 | 	viewerData, ok := resp.Data["viewer"].(map[string]interface{})
2337 | 	if !ok || viewerData == nil {
2338 | 		return "", errors.New("failed to get current user")
2339 | 	}
2340 | 
2341 | 	// Extract the ID
2342 | 	id, ok := viewerData["id"].(string)
2343 | 	if !ok || id == "" {
2344 | 		return "", errors.New("failed to get current user ID")
2345 | 	}
2346 | 
2347 | 	return id, nil
2348 | }
2349 | 
2350 | // GetTeams gets teams by name (optional filter)
2351 | func (c *LinearClient) GetTeams(name string) ([]Team, error) {
2352 | 	query := `
2353 | 		query GetTeams($filter: TeamFilter) {
2354 | 			teams(filter: $filter) {
2355 | 				nodes {
2356 | 					id
2357 | 					name
2358 | 					key
2359 | 					description
2360 | 					states {
2361 | 						nodes {
2362 | 							id
2363 | 							name
2364 | 						}
2365 | 					}
2366 | 				}
2367 | 			}
2368 | 		}
2369 | 	`
2370 | 
2371 | 	// Build the filter
2372 | 	variables := map[string]interface{}{}
2373 | 
2374 | 	if name != "" {
2375 | 		variables["filter"] = map[string]interface{}{
2376 | 			"name": map[string]interface{}{
2377 | 				"contains": name,
2378 | 			},
2379 | 		}
2380 | 	}
2381 | 
2382 | 	resp, err := c.executeGraphQL(query, variables)
2383 | 	if err != nil {
2384 | 		return nil, err
2385 | 	}
2386 | 
2387 | 	// Extract the teams from the response
2388 | 	teamsData, ok := resp.Data["teams"].(map[string]interface{})
2389 | 	if !ok || teamsData == nil {
2390 | 		return []Team{}, nil
2391 | 	}
2392 | 
2393 | 	nodesData, ok := teamsData["nodes"].([]interface{})
2394 | 	if !ok || nodesData == nil {
2395 | 		return []Team{}, nil
2396 | 	}
2397 | 
2398 | 	// Parse the teams data
2399 | 	teams := make([]Team, 0, len(nodesData))
2400 | 	for _, nodeData := range nodesData {
2401 | 		teamData, ok := nodeData.(map[string]interface{})
2402 | 		if !ok {
2403 | 			continue
2404 | 		}
2405 | 
2406 | 		team := Team{
2407 | 			ID:   getStringValue(teamData, "id"),
2408 | 			Name: getStringValue(teamData, "name"),
2409 | 			Key:  getStringValue(teamData, "key"),
2410 | 		}
2411 | 
2412 | 		teams = append(teams, team)
2413 | 	}
2414 | 
2415 | 	return teams, nil
2416 | }
2417 | 
2418 | // GetMetrics returns metrics about the API usage
2419 | func (c *LinearClient) GetMetrics() APIMetrics {
2420 | 	metrics := c.rateLimiter.GetMetrics()
2421 | 
2422 | 	return APIMetrics{
2423 | 		RequestsInLastHour: metrics.RequestsInLastHour,
2424 | 		RemainingRequests:  c.rateLimiter.requestsPerHour - metrics.RequestsInLastHour,
2425 | 		AverageRequestTime: fmt.Sprintf("%dms", metrics.AverageRequestTime),
2426 | 		QueueLength:        metrics.QueueLength,
2427 | 		LastRequestTime:    time.Unix(0, metrics.LastRequestTime*int64(time.Millisecond)).Format(time.RFC3339),
2428 | 	}
2429 | }
2430 | 
2431 | // Helper function to safely extract string values from maps
2432 | func getStringValue(data map[string]interface{}, key string) string {
2433 | 	if value, ok := data[key].(string); ok {
2434 | 		return value
2435 | 	}
2436 | 	return ""
2437 | }
2438 | 
```
Page 6/6FirstPrevNextLast