quickcall-integrations 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/PKG-INFO +46 -2
  2. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/README.md +45 -1
  3. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/api_clients/github_client.py +543 -0
  4. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/tools/github_tools.py +398 -0
  5. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/plugins/quickcall/.claude-plugin/plugin.json +1 -1
  6. quickcall_integrations-0.5.0/plugins/quickcall/commands/pr-summary.md +51 -0
  7. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/pyproject.toml +1 -1
  8. quickcall_integrations-0.5.0/tests/test_pr_integration.py +354 -0
  9. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/uv.lock +1 -1
  10. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/.claude-plugin/marketplace.json +0 -0
  11. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  12. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  13. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/.github/ISSUE_TEMPLATE/task.yml +0 -0
  14. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/.github/workflows/publish-pypi.yml +0 -0
  15. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/.gitignore +0 -0
  16. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/.pre-commit-config.yaml +0 -0
  17. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/.quickcall-issue-template.yaml +0 -0
  18. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/Dockerfile +0 -0
  19. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/assets/logo.png +0 -0
  20. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/__init__.py +0 -0
  21. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/api_clients/__init__.py +0 -0
  22. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/api_clients/slack_client.py +0 -0
  23. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/auth/__init__.py +0 -0
  24. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/auth/credentials.py +0 -0
  25. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/auth/device_flow.py +0 -0
  26. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/resources/__init__.py +0 -0
  27. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/resources/github_resources.py +0 -0
  28. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/resources/slack_resources.py +0 -0
  29. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/server.py +0 -0
  30. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/tools/__init__.py +0 -0
  31. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/tools/auth_tools.py +0 -0
  32. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/tools/git_tools.py +0 -0
  33. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/tools/slack_tools.py +0 -0
  34. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/mcp_server/tools/utility_tools.py +0 -0
  35. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/plugins/quickcall/commands/appraisal.md +0 -0
  36. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/plugins/quickcall/commands/connect-github-pat.md +0 -0
  37. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/plugins/quickcall/commands/connect.md +0 -0
  38. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/plugins/quickcall/commands/projects.md +0 -0
  39. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/plugins/quickcall/commands/slack-summary.md +0 -0
  40. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/plugins/quickcall/commands/status.md +0 -0
  41. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/plugins/quickcall/commands/updates.md +0 -0
  42. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/requirements.txt +0 -0
  43. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/tests/README.md +0 -0
  44. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/tests/appraisal/__init__.py +0 -0
  45. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/tests/appraisal/setup_test_data.py +0 -0
  46. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/tests/test_appraisal_integration.py +0 -0
  47. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/tests/test_appraisal_tools.py +0 -0
  48. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/tests/test_integrations.py +0 -0
  49. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/tests/test_project_integration.py +0 -0
  50. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/tests/test_project_tools.py +0 -0
  51. {quickcall_integrations-0.4.0 → quickcall_integrations-0.5.0}/tests/test_tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickcall-integrations
3
- Version: 0.4.0
3
+ Version: 0.5.0
4
4
  Summary: MCP server with developer integrations for Claude Code and Cursor
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: fastmcp>=2.13.0
@@ -90,7 +90,7 @@ Add to MCP config (`~/.cursor/mcp.json` or `.cursor/mcp.json`):
90
90
  | **Slack** | Read/send messages, threads, channels | Yes |
91
91
 
92
92
  <details>
93
- <summary><strong>Available Tools (24)</strong></summary>
93
+ <summary><strong>Available Tools (26)</strong></summary>
94
94
 
95
95
  ### Git
96
96
  | Tool | Description |
@@ -107,6 +107,8 @@ Add to MCP config (`~/.cursor/mcp.json` or `.cursor/mcp.json`):
107
107
  | `get_commit` | Get commit details (message, stats, files) |
108
108
  | `list_branches` | List repository branches |
109
109
  | `manage_issues` | List, view, create, update, close, reopen, comment on issues + sub-issues |
110
+ | `manage_prs` | Full PR lifecycle: create, update, merge, close, review, comment, labels, assignees |
111
+ | `manage_projects` | GitHub Projects V2: add issues, update fields (Status, Priority, etc.) |
110
112
  | `check_github_connection` | Verify GitHub connection |
111
113
 
112
114
  ### Slack
@@ -193,6 +195,8 @@ GITHUB_USERNAME=your-username # Optional: for better UX
193
195
  | `/quickcall:status` | Show connection status |
194
196
  | `/quickcall:updates` | Get git updates (default: 1 day) |
195
197
  | `/quickcall:updates 7d` | Get updates for last 7 days |
198
+ | `/quickcall:pr-summary` | List open PRs for a repo |
199
+ | `/quickcall:pr-summary owner/repo` | List PRs for specific repo |
196
200
  | `/quickcall:slack-summary` | Summarize Slack messages (default: 1 day) |
197
201
  | `/quickcall:slack-summary 7d` | Summarize last 7 days |
198
202
 
@@ -327,6 +331,46 @@ Create a bug report issue titled "Login fails on Safari"
327
331
  Create issue with feature_request template
328
332
  ```
329
333
 
334
+ ## PR Management
335
+
336
+ The `manage_prs` tool provides full pull request lifecycle management:
337
+
338
+ ### Actions
339
+
340
+ | Action | Description |
341
+ |--------|-------------|
342
+ | `list` | List PRs with state filter |
343
+ | `view` | View PR details |
344
+ | `create` | Create PR (auto-assigns to self) |
345
+ | `update` | Update title, body, base branch |
346
+ | `merge` | Merge PR (merge/squash/rebase) |
347
+ | `close` | Close PR without merging |
348
+ | `reopen` | Reopen closed PR |
349
+ | `comment` | Add comment to PR |
350
+ | `request_reviewers` | Request user/team reviewers |
351
+ | `review` | Submit review (APPROVE, REQUEST_CHANGES, COMMENT) |
352
+ | `to_draft` | Convert PR to draft |
353
+ | `ready_for_review` | Mark draft as ready |
354
+ | `add_labels` | Add labels to PR |
355
+ | `remove_labels` | Remove labels from PR |
356
+ | `add_assignees` | Add assignees |
357
+ | `remove_assignees` | Remove assignees |
358
+
359
+ ### Examples
360
+
361
+ ```
362
+ List open PRs on my-repo
363
+ Create a PR from feature-branch to main titled "Add new feature"
364
+ Merge PR #42 with squash
365
+ Request review from @alice on PR #42
366
+ Approve PR #42 with comment "LGTM!"
367
+ Add labels bug and urgent to PR #42
368
+ ```
369
+
370
+ ### Auto-assign
371
+
372
+ When creating a PR, it automatically assigns to the current user unless `assignees` is explicitly provided.
373
+
330
374
  ## Troubleshooting
331
375
 
332
376
  ### Clean Reinstall
@@ -77,7 +77,7 @@ Add to MCP config (`~/.cursor/mcp.json` or `.cursor/mcp.json`):
77
77
  | **Slack** | Read/send messages, threads, channels | Yes |
78
78
 
79
79
  <details>
80
- <summary><strong>Available Tools (24)</strong></summary>
80
+ <summary><strong>Available Tools (26)</strong></summary>
81
81
 
82
82
  ### Git
83
83
  | Tool | Description |
@@ -94,6 +94,8 @@ Add to MCP config (`~/.cursor/mcp.json` or `.cursor/mcp.json`):
94
94
  | `get_commit` | Get commit details (message, stats, files) |
95
95
  | `list_branches` | List repository branches |
96
96
  | `manage_issues` | List, view, create, update, close, reopen, comment on issues + sub-issues |
97
+ | `manage_prs` | Full PR lifecycle: create, update, merge, close, review, comment, labels, assignees |
98
+ | `manage_projects` | GitHub Projects V2: add issues, update fields (Status, Priority, etc.) |
97
99
  | `check_github_connection` | Verify GitHub connection |
98
100
 
99
101
  ### Slack
@@ -180,6 +182,8 @@ GITHUB_USERNAME=your-username # Optional: for better UX
180
182
  | `/quickcall:status` | Show connection status |
181
183
  | `/quickcall:updates` | Get git updates (default: 1 day) |
182
184
  | `/quickcall:updates 7d` | Get updates for last 7 days |
185
+ | `/quickcall:pr-summary` | List open PRs for a repo |
186
+ | `/quickcall:pr-summary owner/repo` | List PRs for specific repo |
183
187
  | `/quickcall:slack-summary` | Summarize Slack messages (default: 1 day) |
184
188
  | `/quickcall:slack-summary 7d` | Summarize last 7 days |
185
189
 
@@ -314,6 +318,46 @@ Create a bug report issue titled "Login fails on Safari"
314
318
  Create issue with feature_request template
315
319
  ```
316
320
 
321
+ ## PR Management
322
+
323
+ The `manage_prs` tool provides full pull request lifecycle management:
324
+
325
+ ### Actions
326
+
327
+ | Action | Description |
328
+ |--------|-------------|
329
+ | `list` | List PRs with state filter |
330
+ | `view` | View PR details |
331
+ | `create` | Create PR (auto-assigns to self) |
332
+ | `update` | Update title, body, base branch |
333
+ | `merge` | Merge PR (merge/squash/rebase) |
334
+ | `close` | Close PR without merging |
335
+ | `reopen` | Reopen closed PR |
336
+ | `comment` | Add comment to PR |
337
+ | `request_reviewers` | Request user/team reviewers |
338
+ | `review` | Submit review (APPROVE, REQUEST_CHANGES, COMMENT) |
339
+ | `to_draft` | Convert PR to draft |
340
+ | `ready_for_review` | Mark draft as ready |
341
+ | `add_labels` | Add labels to PR |
342
+ | `remove_labels` | Remove labels from PR |
343
+ | `add_assignees` | Add assignees |
344
+ | `remove_assignees` | Remove assignees |
345
+
346
+ ### Examples
347
+
348
+ ```
349
+ List open PRs on my-repo
350
+ Create a PR from feature-branch to main titled "Add new feature"
351
+ Merge PR #42 with squash
352
+ Request review from @alice on PR #42
353
+ Approve PR #42 with comment "LGTM!"
354
+ Add labels bug and urgent to PR #42
355
+ ```
356
+
357
+ ### Auto-assign
358
+
359
+ When creating a PR, it automatically assigns to the current user unless `assignees` is explicitly provided.
360
+
317
361
  ## Troubleshooting
318
362
 
319
363
  ### Clean Reinstall
@@ -417,6 +417,549 @@ class GitHubClient:
417
417
  html_url=pr.html_url,
418
418
  )
419
419
 
420
+ def create_pr(
421
+ self,
422
+ title: str,
423
+ head: str,
424
+ base: str,
425
+ body: Optional[str] = None,
426
+ draft: bool = False,
427
+ owner: Optional[str] = None,
428
+ repo: Optional[str] = None,
429
+ ) -> PullRequest:
430
+ """
431
+ Create a new pull request.
432
+
433
+ Args:
434
+ title: PR title
435
+ head: Branch containing changes (e.g., "feature-branch")
436
+ base: Branch to merge into (e.g., "main")
437
+ body: PR description
438
+ draft: Create as draft PR
439
+ owner: Repository owner
440
+ repo: Repository name
441
+
442
+ Returns:
443
+ Created PullRequest
444
+ """
445
+ gh_repo = self._get_repo(owner, repo)
446
+ pr = gh_repo.create_pull(
447
+ title=title,
448
+ head=head,
449
+ base=base,
450
+ body=body or "",
451
+ draft=draft,
452
+ )
453
+ return self._convert_pr(pr)
454
+
455
+ def update_pr(
456
+ self,
457
+ pr_number: int,
458
+ title: Optional[str] = None,
459
+ body: Optional[str] = None,
460
+ state: Optional[str] = None,
461
+ base: Optional[str] = None,
462
+ owner: Optional[str] = None,
463
+ repo: Optional[str] = None,
464
+ ) -> PullRequest:
465
+ """
466
+ Update an existing pull request.
467
+
468
+ Args:
469
+ pr_number: PR number
470
+ title: New title
471
+ body: New description
472
+ state: New state ('open' or 'closed')
473
+ base: New base branch
474
+ owner: Repository owner
475
+ repo: Repository name
476
+
477
+ Returns:
478
+ Updated PullRequest
479
+ """
480
+ gh_repo = self._get_repo(owner, repo)
481
+ pr = gh_repo.get_pull(pr_number)
482
+
483
+ kwargs = {}
484
+ if title is not None:
485
+ kwargs["title"] = title
486
+ if body is not None:
487
+ kwargs["body"] = body
488
+ if state is not None:
489
+ kwargs["state"] = state
490
+ if base is not None:
491
+ kwargs["base"] = base
492
+
493
+ if kwargs:
494
+ pr.edit(**kwargs)
495
+
496
+ return self._convert_pr(pr)
497
+
498
+ def merge_pr(
499
+ self,
500
+ pr_number: int,
501
+ commit_title: Optional[str] = None,
502
+ commit_message: Optional[str] = None,
503
+ merge_method: str = "merge",
504
+ owner: Optional[str] = None,
505
+ repo: Optional[str] = None,
506
+ ) -> Dict[str, Any]:
507
+ """
508
+ Merge a pull request.
509
+
510
+ Args:
511
+ pr_number: PR number
512
+ commit_title: Custom commit title (for squash/merge)
513
+ commit_message: Custom commit message
514
+ merge_method: 'merge', 'squash', or 'rebase'
515
+ owner: Repository owner
516
+ repo: Repository name
517
+
518
+ Returns:
519
+ Dict with merge status and SHA
520
+ """
521
+ gh_repo = self._get_repo(owner, repo)
522
+ pr = gh_repo.get_pull(pr_number)
523
+
524
+ # Check if PR is mergeable
525
+ if pr.merged:
526
+ return {
527
+ "merged": True,
528
+ "message": "PR was already merged",
529
+ "sha": pr.merge_commit_sha,
530
+ }
531
+
532
+ if not pr.mergeable:
533
+ return {
534
+ "merged": False,
535
+ "message": "PR is not mergeable (conflicts or checks failing)",
536
+ "sha": None,
537
+ }
538
+
539
+ # Merge the PR
540
+ result = pr.merge(
541
+ commit_title=commit_title,
542
+ commit_message=commit_message,
543
+ merge_method=merge_method,
544
+ )
545
+
546
+ return {
547
+ "merged": result.merged,
548
+ "message": result.message,
549
+ "sha": result.sha,
550
+ }
551
+
552
+ def close_pr(
553
+ self,
554
+ pr_number: int,
555
+ owner: Optional[str] = None,
556
+ repo: Optional[str] = None,
557
+ ) -> PullRequest:
558
+ """Close a pull request without merging."""
559
+ gh_repo = self._get_repo(owner, repo)
560
+ pr = gh_repo.get_pull(pr_number)
561
+ pr.edit(state="closed")
562
+ return self._convert_pr(pr)
563
+
564
+ def reopen_pr(
565
+ self,
566
+ pr_number: int,
567
+ owner: Optional[str] = None,
568
+ repo: Optional[str] = None,
569
+ ) -> PullRequest:
570
+ """Reopen a closed pull request."""
571
+ gh_repo = self._get_repo(owner, repo)
572
+ pr = gh_repo.get_pull(pr_number)
573
+ pr.edit(state="open")
574
+ return self._convert_pr(pr)
575
+
576
+ def add_pr_comment(
577
+ self,
578
+ pr_number: int,
579
+ body: str,
580
+ owner: Optional[str] = None,
581
+ repo: Optional[str] = None,
582
+ ) -> Dict[str, Any]:
583
+ """
584
+ Add a comment to a pull request.
585
+
586
+ Args:
587
+ pr_number: PR number
588
+ body: Comment text
589
+ owner: Repository owner
590
+ repo: Repository name
591
+
592
+ Returns:
593
+ Comment details
594
+ """
595
+ gh_repo = self._get_repo(owner, repo)
596
+ # PRs use the issue comment API
597
+ issue = gh_repo.get_issue(pr_number)
598
+ comment = issue.create_comment(body)
599
+ return {
600
+ "id": comment.id,
601
+ "body": comment.body,
602
+ "html_url": comment.html_url,
603
+ "created_at": comment.created_at.isoformat(),
604
+ "pr_number": pr_number,
605
+ }
606
+
607
+ def request_reviewers(
608
+ self,
609
+ pr_number: int,
610
+ reviewers: Optional[List[str]] = None,
611
+ team_reviewers: Optional[List[str]] = None,
612
+ owner: Optional[str] = None,
613
+ repo: Optional[str] = None,
614
+ ) -> Dict[str, Any]:
615
+ """
616
+ Request reviewers for a pull request.
617
+
618
+ Args:
619
+ pr_number: PR number
620
+ reviewers: List of GitHub usernames
621
+ team_reviewers: List of team slugs (org teams)
622
+ owner: Repository owner
623
+ repo: Repository name
624
+
625
+ Returns:
626
+ Dict with requested reviewers
627
+ """
628
+ gh_repo = self._get_repo(owner, repo)
629
+ pr = gh_repo.get_pull(pr_number)
630
+
631
+ # Create review request
632
+ pr.create_review_request(
633
+ reviewers=reviewers or [],
634
+ team_reviewers=team_reviewers or [],
635
+ )
636
+
637
+ # Re-fetch to get updated reviewers
638
+ pr = gh_repo.get_pull(pr_number)
639
+ return {
640
+ "pr_number": pr_number,
641
+ "requested_reviewers": [r.login for r in pr.requested_reviewers],
642
+ "requested_teams": [t.slug for t in pr.requested_teams],
643
+ }
644
+
645
+ def submit_pr_review(
646
+ self,
647
+ pr_number: int,
648
+ event: str,
649
+ body: Optional[str] = None,
650
+ owner: Optional[str] = None,
651
+ repo: Optional[str] = None,
652
+ ) -> Dict[str, Any]:
653
+ """
654
+ Submit a review on a pull request.
655
+
656
+ Args:
657
+ pr_number: PR number
658
+ event: Review event - 'APPROVE', 'REQUEST_CHANGES', or 'COMMENT'
659
+ body: Review comment (required for REQUEST_CHANGES)
660
+ owner: Repository owner
661
+ repo: Repository name
662
+
663
+ Returns:
664
+ Review details
665
+ """
666
+ gh_repo = self._get_repo(owner, repo)
667
+ pr = gh_repo.get_pull(pr_number)
668
+
669
+ review = pr.create_review(
670
+ body=body or "",
671
+ event=event,
672
+ )
673
+
674
+ return {
675
+ "id": review.id,
676
+ "state": review.state,
677
+ "body": review.body,
678
+ "html_url": review.html_url,
679
+ "pr_number": pr_number,
680
+ }
681
+
682
+ def convert_pr_to_draft(
683
+ self,
684
+ pr_number: int,
685
+ owner: Optional[str] = None,
686
+ repo: Optional[str] = None,
687
+ ) -> Dict[str, Any]:
688
+ """
689
+ Convert a PR to draft status using GraphQL API.
690
+
691
+ Args:
692
+ pr_number: PR number
693
+ owner: Repository owner
694
+ repo: Repository name
695
+
696
+ Returns:
697
+ Dict with success status
698
+ """
699
+ owner = owner or self.default_owner
700
+ repo = repo or self.default_repo
701
+
702
+ if not owner or not repo:
703
+ raise ValueError("Repository owner and name must be specified")
704
+
705
+ # Get PR node ID
706
+ query = """
707
+ query($owner: String!, $repo: String!, $number: Int!) {
708
+ repository(owner: $owner, name: $repo) {
709
+ pullRequest(number: $number) {
710
+ id
711
+ isDraft
712
+ }
713
+ }
714
+ }
715
+ """
716
+ data = self._graphql_request(
717
+ query, {"owner": owner, "repo": repo, "number": pr_number}
718
+ )
719
+ repository = data.get("repository")
720
+ if not repository:
721
+ raise GithubException(
722
+ 404, {"message": f"Repository {owner}/{repo} not found"}
723
+ )
724
+ pr_data = repository.get("pullRequest")
725
+ if not pr_data:
726
+ raise GithubException(404, {"message": f"PR #{pr_number} not found"})
727
+
728
+ if pr_data.get("isDraft"):
729
+ return {
730
+ "pr_number": pr_number,
731
+ "is_draft": True,
732
+ "message": "PR is already a draft",
733
+ }
734
+
735
+ pr_node_id = pr_data["id"]
736
+
737
+ # Convert to draft
738
+ mutation = """
739
+ mutation($pullRequestId: ID!) {
740
+ convertPullRequestToDraft(input: {pullRequestId: $pullRequestId}) {
741
+ pullRequest {
742
+ id
743
+ isDraft
744
+ }
745
+ }
746
+ }
747
+ """
748
+ result = self._graphql_request(mutation, {"pullRequestId": pr_node_id})
749
+ converted = result.get("convertPullRequestToDraft", {}).get("pullRequest", {})
750
+
751
+ return {
752
+ "pr_number": pr_number,
753
+ "is_draft": converted.get("isDraft", True),
754
+ "message": "PR converted to draft",
755
+ }
756
+
757
+ def mark_pr_ready_for_review(
758
+ self,
759
+ pr_number: int,
760
+ owner: Optional[str] = None,
761
+ repo: Optional[str] = None,
762
+ ) -> Dict[str, Any]:
763
+ """
764
+ Mark a draft PR as ready for review using GraphQL API.
765
+
766
+ Args:
767
+ pr_number: PR number
768
+ owner: Repository owner
769
+ repo: Repository name
770
+
771
+ Returns:
772
+ Dict with success status
773
+ """
774
+ owner = owner or self.default_owner
775
+ repo = repo or self.default_repo
776
+
777
+ if not owner or not repo:
778
+ raise ValueError("Repository owner and name must be specified")
779
+
780
+ # Get PR node ID
781
+ query = """
782
+ query($owner: String!, $repo: String!, $number: Int!) {
783
+ repository(owner: $owner, name: $repo) {
784
+ pullRequest(number: $number) {
785
+ id
786
+ isDraft
787
+ }
788
+ }
789
+ }
790
+ """
791
+ data = self._graphql_request(
792
+ query, {"owner": owner, "repo": repo, "number": pr_number}
793
+ )
794
+ repository = data.get("repository")
795
+ if not repository:
796
+ raise GithubException(
797
+ 404, {"message": f"Repository {owner}/{repo} not found"}
798
+ )
799
+ pr_data = repository.get("pullRequest")
800
+ if not pr_data:
801
+ raise GithubException(404, {"message": f"PR #{pr_number} not found"})
802
+
803
+ if not pr_data.get("isDraft"):
804
+ return {
805
+ "pr_number": pr_number,
806
+ "is_draft": False,
807
+ "message": "PR is already ready for review",
808
+ }
809
+
810
+ pr_node_id = pr_data["id"]
811
+
812
+ # Mark ready for review
813
+ mutation = """
814
+ mutation($pullRequestId: ID!) {
815
+ markPullRequestReadyForReview(input: {pullRequestId: $pullRequestId}) {
816
+ pullRequest {
817
+ id
818
+ isDraft
819
+ }
820
+ }
821
+ }
822
+ """
823
+ result = self._graphql_request(mutation, {"pullRequestId": pr_node_id})
824
+ converted = result.get("markPullRequestReadyForReview", {}).get(
825
+ "pullRequest", {}
826
+ )
827
+
828
+ return {
829
+ "pr_number": pr_number,
830
+ "is_draft": converted.get("isDraft", False),
831
+ "message": "PR marked ready for review",
832
+ }
833
+
834
+ def add_pr_labels(
835
+ self,
836
+ pr_number: int,
837
+ labels: List[str],
838
+ owner: Optional[str] = None,
839
+ repo: Optional[str] = None,
840
+ ) -> Dict[str, Any]:
841
+ """
842
+ Add labels to a pull request.
843
+
844
+ Args:
845
+ pr_number: PR number
846
+ labels: List of label names
847
+ owner: Repository owner
848
+ repo: Repository name
849
+
850
+ Returns:
851
+ Dict with updated labels
852
+ """
853
+ gh_repo = self._get_repo(owner, repo)
854
+ # PRs use the issue API for labels
855
+ issue = gh_repo.get_issue(pr_number)
856
+ issue.add_to_labels(*labels)
857
+
858
+ return {
859
+ "pr_number": pr_number,
860
+ "labels": [label.name for label in issue.labels],
861
+ }
862
+
863
+ def remove_pr_labels(
864
+ self,
865
+ pr_number: int,
866
+ labels: List[str],
867
+ owner: Optional[str] = None,
868
+ repo: Optional[str] = None,
869
+ ) -> Dict[str, Any]:
870
+ """
871
+ Remove labels from a pull request.
872
+
873
+ Args:
874
+ pr_number: PR number
875
+ labels: List of label names to remove
876
+ owner: Repository owner
877
+ repo: Repository name
878
+
879
+ Returns:
880
+ Dict with updated labels
881
+ """
882
+ gh_repo = self._get_repo(owner, repo)
883
+ issue = gh_repo.get_issue(pr_number)
884
+
885
+ for label in labels:
886
+ try:
887
+ issue.remove_from_labels(label)
888
+ except GithubException as e:
889
+ if e.status != 404: # Ignore if label not present
890
+ raise
891
+
892
+ # Re-fetch to get updated labels
893
+ issue = gh_repo.get_issue(pr_number)
894
+ return {
895
+ "pr_number": pr_number,
896
+ "labels": [label.name for label in issue.labels],
897
+ }
898
+
899
+ def add_pr_assignees(
900
+ self,
901
+ pr_number: int,
902
+ assignees: List[str],
903
+ owner: Optional[str] = None,
904
+ repo: Optional[str] = None,
905
+ ) -> Dict[str, Any]:
906
+ """
907
+ Add assignees to a pull request.
908
+
909
+ Args:
910
+ pr_number: PR number
911
+ assignees: List of GitHub usernames
912
+ owner: Repository owner
913
+ repo: Repository name
914
+
915
+ Returns:
916
+ Dict with updated assignees
917
+ """
918
+ gh_repo = self._get_repo(owner, repo)
919
+ issue = gh_repo.get_issue(pr_number)
920
+ issue.add_to_assignees(*assignees)
921
+
922
+ return {
923
+ "pr_number": pr_number,
924
+ "assignees": [a.login for a in issue.assignees],
925
+ }
926
+
927
+ def remove_pr_assignees(
928
+ self,
929
+ pr_number: int,
930
+ assignees: List[str],
931
+ owner: Optional[str] = None,
932
+ repo: Optional[str] = None,
933
+ ) -> Dict[str, Any]:
934
+ """
935
+ Remove assignees from a pull request.
936
+
937
+ Args:
938
+ pr_number: PR number
939
+ assignees: List of GitHub usernames to remove
940
+ owner: Repository owner
941
+ repo: Repository name
942
+
943
+ Returns:
944
+ Dict with updated assignees
945
+ """
946
+ gh_repo = self._get_repo(owner, repo)
947
+ issue = gh_repo.get_issue(pr_number)
948
+
949
+ for assignee in assignees:
950
+ try:
951
+ issue.remove_from_assignees(assignee)
952
+ except GithubException as e:
953
+ if e.status != 404: # Ignore if not assigned
954
+ raise
955
+
956
+ # Re-fetch to get updated assignees
957
+ issue = gh_repo.get_issue(pr_number)
958
+ return {
959
+ "pr_number": pr_number,
960
+ "assignees": [a.login for a in issue.assignees],
961
+ }
962
+
420
963
  # ========================================================================
421
964
  # Commit Operations
422
965
  # ========================================================================