mcp-ticketer 0.1.8__py3-none-any.whl → 0.1.12__py3-none-any.whl

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.

Potentially problematic release.


This version of mcp-ticketer might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  """Version information for mcp-ticketer package."""
2
2
 
3
- __version__ = "0.1.8"
3
+ __version__ = "0.1.12"
4
4
  __version_info__ = tuple(int(part) for part in __version__.split("."))
5
5
 
6
6
  # Package metadata
@@ -4,5 +4,12 @@ from .aitrackdown import AITrackdownAdapter
4
4
  from .linear import LinearAdapter
5
5
  from .jira import JiraAdapter
6
6
  from .github import GitHubAdapter
7
+ from .hybrid import HybridAdapter
7
8
 
8
- __all__ = ["AITrackdownAdapter", "LinearAdapter", "JiraAdapter", "GitHubAdapter"]
9
+ __all__ = [
10
+ "AITrackdownAdapter",
11
+ "LinearAdapter",
12
+ "JiraAdapter",
13
+ "GitHubAdapter",
14
+ "HybridAdapter"
15
+ ]
@@ -168,7 +168,8 @@ class AITrackdownAdapter(BaseAdapter[Task]):
168
168
  """Create a new task."""
169
169
  # Generate ID if not provided
170
170
  if not ticket.id:
171
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
171
+ # Use microseconds to ensure uniqueness
172
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
172
173
  prefix = "epic" if isinstance(ticket, Epic) else "task"
173
174
  ticket.id = f"{prefix}-{timestamp}"
174
175
 
@@ -277,9 +278,9 @@ class AITrackdownAdapter(BaseAdapter[Task]):
277
278
  )
278
279
  tasks = [self._task_from_ai_ticket(t.__dict__) for t in tickets]
279
280
  else:
280
- # Direct file operation
281
+ # Direct file operation - read all files, filter, then paginate
281
282
  ticket_files = sorted(self.tickets_dir.glob("*.json"))
282
- for ticket_file in ticket_files[offset:offset + limit]:
283
+ for ticket_file in ticket_files:
283
284
  with open(ticket_file, "r") as f:
284
285
  ai_ticket = json.load(f)
285
286
  task = self._task_from_ai_ticket(ai_ticket)
@@ -303,7 +304,10 @@ class AITrackdownAdapter(BaseAdapter[Task]):
303
304
 
304
305
  tasks.append(task)
305
306
 
306
- return tasks[:limit]
307
+ # Apply pagination after filtering
308
+ tasks = tasks[offset:offset + limit]
309
+
310
+ return tasks
307
311
 
308
312
  async def search(self, query: SearchQuery) -> List[Task]:
309
313
  """Search tasks using query parameters."""
@@ -357,7 +361,7 @@ class AITrackdownAdapter(BaseAdapter[Task]):
357
361
  """Add comment to a task."""
358
362
  # Generate ID
359
363
  if not comment.id:
360
- timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
364
+ timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
361
365
  comment.id = f"comment-{timestamp}"
362
366
 
363
367
  comment.created_at = datetime.now()
@@ -965,6 +965,269 @@ class GitHubAdapter(BaseAdapter[Task]):
965
965
 
966
966
  return response.status_code == 201
967
967
 
968
+ async def create_pull_request(
969
+ self,
970
+ ticket_id: str,
971
+ base_branch: str = "main",
972
+ head_branch: Optional[str] = None,
973
+ title: Optional[str] = None,
974
+ body: Optional[str] = None,
975
+ draft: bool = False,
976
+ ) -> Dict[str, Any]:
977
+ """Create a pull request linked to an issue.
978
+
979
+ Args:
980
+ ticket_id: Issue number to link the PR to
981
+ base_branch: Target branch for the PR (default: main)
982
+ head_branch: Source branch name (auto-generated if not provided)
983
+ title: PR title (uses ticket title if not provided)
984
+ body: PR description (auto-generated with issue link if not provided)
985
+ draft: Create as draft PR
986
+
987
+ Returns:
988
+ Dictionary with PR details including number, url, and branch
989
+ """
990
+ try:
991
+ issue_number = int(ticket_id)
992
+ except ValueError:
993
+ raise ValueError(f"Invalid issue number: {ticket_id}")
994
+
995
+ # Get the issue details
996
+ issue = await self.read(ticket_id)
997
+ if not issue:
998
+ raise ValueError(f"Issue #{ticket_id} not found")
999
+
1000
+ # Auto-generate branch name if not provided
1001
+ if not head_branch:
1002
+ # Create branch name from issue number and title
1003
+ # e.g., "123-fix-authentication-bug"
1004
+ safe_title = "-".join(
1005
+ issue.title.lower()
1006
+ .replace("[", "")
1007
+ .replace("]", "")
1008
+ .replace("#", "")
1009
+ .replace("/", "-")
1010
+ .replace("\\", "-")
1011
+ .split()[:5] # Limit to 5 words
1012
+ )
1013
+ head_branch = f"{issue_number}-{safe_title}"
1014
+
1015
+ # Auto-generate title if not provided
1016
+ if not title:
1017
+ # Include issue number in PR title
1018
+ title = f"[#{issue_number}] {issue.title}"
1019
+
1020
+ # Auto-generate body if not provided
1021
+ if not body:
1022
+ body = f"""## Summary
1023
+
1024
+ This PR addresses issue #{issue_number}.
1025
+
1026
+ **Issue:** #{issue_number} - {issue.title}
1027
+ **Link:** {issue.metadata.get('github', {}).get('url', '')}
1028
+
1029
+ ## Description
1030
+
1031
+ {issue.description or 'No description provided.'}
1032
+
1033
+ ## Changes
1034
+
1035
+ - [ ] Implementation details to be added
1036
+
1037
+ ## Testing
1038
+
1039
+ - [ ] Tests have been added/updated
1040
+ - [ ] All tests pass
1041
+
1042
+ ## Checklist
1043
+
1044
+ - [ ] Code follows project style guidelines
1045
+ - [ ] Self-review completed
1046
+ - [ ] Documentation updated if needed
1047
+
1048
+ Fixes #{issue_number}
1049
+ """
1050
+
1051
+ # Check if the head branch exists
1052
+ try:
1053
+ branch_response = await self.client.get(
1054
+ f"/repos/{self.owner}/{self.repo}/branches/{head_branch}"
1055
+ )
1056
+ branch_exists = branch_response.status_code == 200
1057
+ except httpx.HTTPError:
1058
+ branch_exists = False
1059
+
1060
+ if not branch_exists:
1061
+ # Get the base branch SHA
1062
+ base_response = await self.client.get(
1063
+ f"/repos/{self.owner}/{self.repo}/branches/{base_branch}"
1064
+ )
1065
+ base_response.raise_for_status()
1066
+ base_sha = base_response.json()["commit"]["sha"]
1067
+
1068
+ # Create the new branch
1069
+ ref_response = await self.client.post(
1070
+ f"/repos/{self.owner}/{self.repo}/git/refs",
1071
+ json={
1072
+ "ref": f"refs/heads/{head_branch}",
1073
+ "sha": base_sha,
1074
+ }
1075
+ )
1076
+
1077
+ if ref_response.status_code != 201:
1078
+ # Branch might already exist on remote, try to use it
1079
+ pass
1080
+
1081
+ # Create the pull request
1082
+ pr_data = {
1083
+ "title": title,
1084
+ "body": body,
1085
+ "head": head_branch,
1086
+ "base": base_branch,
1087
+ "draft": draft,
1088
+ }
1089
+
1090
+ pr_response = await self.client.post(
1091
+ f"/repos/{self.owner}/{self.repo}/pulls",
1092
+ json=pr_data
1093
+ )
1094
+
1095
+ if pr_response.status_code == 422:
1096
+ # PR might already exist, try to get it
1097
+ search_response = await self.client.get(
1098
+ f"/repos/{self.owner}/{self.repo}/pulls",
1099
+ params={
1100
+ "head": f"{self.owner}:{head_branch}",
1101
+ "base": base_branch,
1102
+ "state": "open",
1103
+ }
1104
+ )
1105
+
1106
+ if search_response.status_code == 200:
1107
+ existing_prs = search_response.json()
1108
+ if existing_prs:
1109
+ pr = existing_prs[0]
1110
+ return {
1111
+ "number": pr["number"],
1112
+ "url": pr["html_url"],
1113
+ "api_url": pr["url"],
1114
+ "branch": head_branch,
1115
+ "state": pr["state"],
1116
+ "draft": pr.get("draft", False),
1117
+ "title": pr["title"],
1118
+ "existing": True,
1119
+ "linked_issue": issue_number,
1120
+ }
1121
+
1122
+ raise ValueError(f"Failed to create PR: {pr_response.text}")
1123
+
1124
+ pr_response.raise_for_status()
1125
+ pr = pr_response.json()
1126
+
1127
+ # Add a comment to the issue about the PR
1128
+ await self.add_comment(
1129
+ Comment(
1130
+ ticket_id=ticket_id,
1131
+ content=f"Pull request #{pr['number']} has been created: {pr['html_url']}",
1132
+ author="system",
1133
+ )
1134
+ )
1135
+
1136
+ return {
1137
+ "number": pr["number"],
1138
+ "url": pr["html_url"],
1139
+ "api_url": pr["url"],
1140
+ "branch": head_branch,
1141
+ "state": pr["state"],
1142
+ "draft": pr.get("draft", False),
1143
+ "title": pr["title"],
1144
+ "linked_issue": issue_number,
1145
+ }
1146
+
1147
+ async def link_existing_pull_request(
1148
+ self,
1149
+ ticket_id: str,
1150
+ pr_url: str,
1151
+ ) -> Dict[str, Any]:
1152
+ """Link an existing pull request to a ticket.
1153
+
1154
+ Args:
1155
+ ticket_id: Issue number to link the PR to
1156
+ pr_url: GitHub PR URL to link
1157
+
1158
+ Returns:
1159
+ Dictionary with link status and PR details
1160
+ """
1161
+ try:
1162
+ issue_number = int(ticket_id)
1163
+ except ValueError:
1164
+ raise ValueError(f"Invalid issue number: {ticket_id}")
1165
+
1166
+ # Parse PR URL to extract owner, repo, and PR number
1167
+ # Expected format: https://github.com/owner/repo/pull/123
1168
+ import re
1169
+ pr_pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)"
1170
+ match = re.search(pr_pattern, pr_url)
1171
+
1172
+ if not match:
1173
+ raise ValueError(f"Invalid GitHub PR URL format: {pr_url}")
1174
+
1175
+ pr_owner, pr_repo, pr_number = match.groups()
1176
+
1177
+ # Verify the PR is from the same repository
1178
+ if pr_owner != self.owner or pr_repo != self.repo:
1179
+ raise ValueError(
1180
+ f"PR must be from the same repository ({self.owner}/{self.repo})"
1181
+ )
1182
+
1183
+ # Get PR details
1184
+ pr_response = await self.client.get(
1185
+ f"/repos/{self.owner}/{self.repo}/pulls/{pr_number}"
1186
+ )
1187
+
1188
+ if pr_response.status_code == 404:
1189
+ raise ValueError(f"Pull request #{pr_number} not found")
1190
+
1191
+ pr_response.raise_for_status()
1192
+ pr = pr_response.json()
1193
+
1194
+ # Update PR body to include issue reference if not already present
1195
+ current_body = pr.get("body", "")
1196
+ issue_ref = f"#{issue_number}"
1197
+
1198
+ if issue_ref not in current_body:
1199
+ # Add issue reference to the body
1200
+ updated_body = current_body or ""
1201
+ if updated_body:
1202
+ updated_body += "\n\n"
1203
+ updated_body += f"Related to #{issue_number}"
1204
+
1205
+ # Update the PR
1206
+ update_response = await self.client.patch(
1207
+ f"/repos/{self.owner}/{self.repo}/pulls/{pr_number}",
1208
+ json={"body": updated_body}
1209
+ )
1210
+ update_response.raise_for_status()
1211
+
1212
+ # Add a comment to the issue about the PR
1213
+ await self.add_comment(
1214
+ Comment(
1215
+ ticket_id=ticket_id,
1216
+ content=f"Linked to pull request #{pr_number}: {pr_url}",
1217
+ author="system",
1218
+ )
1219
+ )
1220
+
1221
+ return {
1222
+ "success": True,
1223
+ "pr_number": pr["number"],
1224
+ "pr_url": pr["html_url"],
1225
+ "pr_title": pr["title"],
1226
+ "pr_state": pr["state"],
1227
+ "linked_issue": issue_number,
1228
+ "message": f"Successfully linked PR #{pr_number} to issue #{issue_number}",
1229
+ }
1230
+
968
1231
  async def close(self) -> None:
969
1232
  """Close the HTTP client connection."""
970
1233
  await self.client.aclose()