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.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/adapters/__init__.py +8 -1
- mcp_ticketer/adapters/aitrackdown.py +9 -5
- mcp_ticketer/adapters/github.py +263 -0
- mcp_ticketer/adapters/hybrid.py +505 -0
- mcp_ticketer/adapters/linear.py +220 -0
- mcp_ticketer/cli/configure.py +532 -0
- mcp_ticketer/cli/main.py +107 -0
- mcp_ticketer/cli/migrate_config.py +204 -0
- mcp_ticketer/core/project_config.py +553 -0
- mcp_ticketer/mcp/server.py +349 -15
- mcp_ticketer/queue/queue.py +4 -1
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/METADATA +6 -5
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/RECORD +18 -14
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.8.dist-info → mcp_ticketer-0.1.12.dist-info}/top_level.txt +0 -0
mcp_ticketer/__version__.py
CHANGED
|
@@ -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__ = [
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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()
|
mcp_ticketer/adapters/github.py
CHANGED
|
@@ -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()
|