boomtick-cli 0.2.1__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.
Files changed (103) hide show
  1. boomtick_cli-0.2.1.dist-info/METADATA +20 -0
  2. boomtick_cli-0.2.1.dist-info/RECORD +103 -0
  3. boomtick_cli-0.2.1.dist-info/WHEEL +5 -0
  4. boomtick_cli-0.2.1.dist-info/entry_points.txt +3 -0
  5. boomtick_cli-0.2.1.dist-info/top_level.txt +1 -0
  6. dev_tools/__init__.py +1 -0
  7. dev_tools/cli-schema.json +2028 -0
  8. dev_tools/cli.py +1542 -0
  9. dev_tools/config.py +235 -0
  10. dev_tools/constants.py +14 -0
  11. dev_tools/daemon.py +146 -0
  12. dev_tools/dist/config.js +108 -0
  13. dev_tools/dist/index.js +10 -0
  14. dev_tools/dist/lib/error_utils.js +8 -0
  15. dev_tools/dist/lib/git.js +31 -0
  16. dev_tools/dist/lib/result.js +21 -0
  17. dev_tools/dist/lib/shell.js +91 -0
  18. dev_tools/dist/lib/shell.test.js +10 -0
  19. dev_tools/dist/lib/td-cli.js +27 -0
  20. dev_tools/dist/lib/test-utils.js +20 -0
  21. dev_tools/dist/mcp/definitions.js +242 -0
  22. dev_tools/dist/mcp/server.js +317 -0
  23. dev_tools/dist/mcp/tools.js +18 -0
  24. dev_tools/dist/tools/contract.js +2069 -0
  25. dev_tools/dist/tools/ddgs.search.js +50 -0
  26. dev_tools/dist/tools/ddgs.search.test.js +67 -0
  27. dev_tools/dist/tools/ddgs_search.py +23 -0
  28. dev_tools/dist/tools/github.checkout_branch.js +20 -0
  29. dev_tools/dist/tools/github.comment_triage_summary.js +25 -0
  30. dev_tools/dist/tools/github.create_issue.js +28 -0
  31. dev_tools/dist/tools/github.create_issue.test.js +54 -0
  32. dev_tools/dist/tools/github.create_pull_request.js +44 -0
  33. dev_tools/dist/tools/github.get_merge_conflict_files.js +26 -0
  34. dev_tools/dist/tools/github.get_pr.js +18 -0
  35. dev_tools/dist/tools/github.get_pr_diff.js +24 -0
  36. dev_tools/dist/tools/github.issue_comment.js +24 -0
  37. dev_tools/dist/tools/github.issue_update.js +29 -0
  38. dev_tools/dist/tools/github.issue_view.js +28 -0
  39. dev_tools/dist/tools/github.open_replacement_pr.js +41 -0
  40. dev_tools/dist/tools/github.search_open_prs.js +31 -0
  41. dev_tools/dist/tools/github.search_open_prs.test.js +51 -0
  42. dev_tools/dist/tools/jules/cancel-session.js +20 -0
  43. dev_tools/dist/tools/jules/create-session.js +20 -0
  44. dev_tools/dist/tools/jules/create-session.test.js +99 -0
  45. dev_tools/dist/tools/jules/get-messages.js +19 -0
  46. dev_tools/dist/tools/jules/get-messages.test.js +45 -0
  47. dev_tools/dist/tools/jules/get-pr.js +14 -0
  48. dev_tools/dist/tools/jules/get-pr.test.js +37 -0
  49. dev_tools/dist/tools/jules/get-session.js +10 -0
  50. dev_tools/dist/tools/jules/list-sessions.js +43 -0
  51. dev_tools/dist/tools/jules/send-message.js +22 -0
  52. dev_tools/dist/tools/jules/send-message.test.js +56 -0
  53. dev_tools/dist/tools/jules/shared.js +26 -0
  54. dev_tools/dist/tools/jules/trigger-feedback.js +16 -0
  55. dev_tools/dist/tools/jules/trigger-feedback.test.js +42 -0
  56. dev_tools/dist/tools/repo.commit_patch.js +33 -0
  57. dev_tools/dist/tools/repo.create_branch.js +21 -0
  58. dev_tools/dist/tools/repo.create_branch.test.js +42 -0
  59. dev_tools/dist/tools/repo.create_repair_branch.js +38 -0
  60. dev_tools/dist/tools/repo.get_changed_files.js +21 -0
  61. dev_tools/dist/tools/repo.get_command_schema.js +27 -0
  62. dev_tools/dist/tools/repo.get_package_scripts.js +35 -0
  63. dev_tools/dist/tools/repo.get_package_scripts.test.js +39 -0
  64. dev_tools/dist/tools/repo.get_route_map.js +37 -0
  65. dev_tools/dist/tools/repo.logs.js +16 -0
  66. dev_tools/dist/tools/repo.read_agent_context.js +20 -0
  67. dev_tools/dist/tools/repo.read_ci_logs.js +18 -0
  68. dev_tools/dist/tools/repo.run_lighthouse.js +37 -0
  69. dev_tools/dist/tools/repo.run_playwright.js +29 -0
  70. dev_tools/dist/tools/repo.run_tests.js +43 -0
  71. dev_tools/dist/tools/types.js +1 -0
  72. dev_tools/get_ai_context.py +63 -0
  73. dev_tools/handlers/__init__.py +1 -0
  74. dev_tools/handlers/command_handler.py +69 -0
  75. dev_tools/mcp_server.py +36 -0
  76. dev_tools/models.py +354 -0
  77. dev_tools/orchestrator.py +2841 -0
  78. dev_tools/pr_overlap.py +139 -0
  79. dev_tools/resources/__init__.py +1 -0
  80. dev_tools/resources/build-repo-context.py +182 -0
  81. dev_tools/resources/prompt_constants.json +5 -0
  82. dev_tools/resources/review_template.md +41 -0
  83. dev_tools/resources/ux-audit.config.json +15 -0
  84. dev_tools/resources/visual_guidelines.json +3 -0
  85. dev_tools/review_read_pass.py +232 -0
  86. dev_tools/schema_gen.py +55 -0
  87. dev_tools/schema_utils.py +125 -0
  88. dev_tools/scope_check.py +85 -0
  89. dev_tools/services/__init__.py +1 -0
  90. dev_tools/services/ai_service.py +816 -0
  91. dev_tools/services/dependency_graph.py +123 -0
  92. dev_tools/services/github.py +783 -0
  93. dev_tools/services/jules.py +181 -0
  94. dev_tools/services/repair_service.py +199 -0
  95. dev_tools/services/vector_store.py +82 -0
  96. dev_tools/services/vision_service.py +91 -0
  97. dev_tools/td_cli.py +28 -0
  98. dev_tools/utils/__init__.py +1035 -0
  99. dev_tools/utils/git.py +68 -0
  100. dev_tools/ux_report.py +213 -0
  101. dev_tools/verify_infra.py +91 -0
  102. dev_tools/verify_versions.py +191 -0
  103. dev_tools/version_utils.py +175 -0
dev_tools/models.py ADDED
@@ -0,0 +1,354 @@
1
+ # pylint: disable=invalid-name,missing-docstring,no-member
2
+ import re
3
+ from typing import Any, Dict, List, Literal, Optional, Union
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
6
+
7
+
8
+ class IssueSummary(BaseModel):
9
+ number: int
10
+ title: str
11
+ html_url: str
12
+ state: str
13
+
14
+
15
+ class PRSummary(BaseModel):
16
+ number: int
17
+ title: str
18
+ author: Dict[str, str] = Field(default_factory=dict)
19
+ headRefName: Optional[str] = None
20
+ baseRefName: Optional[str] = None
21
+ isDraft: bool = False
22
+ mergeStateStatus: Optional[str] = None
23
+ updatedAt: Optional[str] = None
24
+ url: Optional[str] = None
25
+
26
+
27
+ class CreateIssueInput(BaseModel):
28
+ title: str = Field(..., min_length=1)
29
+ body: Optional[str] = Field(None, min_length=1)
30
+ file: Optional[str] = Field(None, min_length=1)
31
+
32
+ @model_validator(mode="after")
33
+ def check_body_or_file(self) -> "CreateIssueInput":
34
+ # pylint: disable=no-member
35
+ if (self.body is None or not self.body.strip()) and (self.file is None or not self.file.strip()):
36
+ raise ValueError("Provide either --file or --body (cannot be empty)")
37
+ if self.body and self.file:
38
+ raise ValueError("Provide --file or --body, not both")
39
+ return self
40
+
41
+
42
+ class SearchPRsInput(BaseModel):
43
+ model_config = ConfigDict(populate_by_name=True)
44
+ state: str = Field("open", description="The state of the PRs to search for (open, closed, all).")
45
+ limit: int = Field(100, description="The maximum number of PRs to return (default: 100, range: 1-100).")
46
+ includeDrafts: bool = Field(True, alias="includeDrafts", description="Whether to include draft PRs in the results.")
47
+ labels: Optional[List[str]] = Field(None, description="Filter PRs by labels.")
48
+
49
+
50
+ class IssueUpdateInput(BaseModel):
51
+ model_config = ConfigDict(populate_by_name=True)
52
+ issueNumber: int = Field(..., alias="issueNumber", description="The number of the issue to update.")
53
+ body: Optional[str] = Field(None, description="The new body content for the issue.")
54
+ file: Optional[str] = Field(None, description="Path to file containing new issue body.")
55
+ labels: Optional[List[str]] = Field(None, description="Comma-separated list of labels to set.")
56
+ addLabels: Optional[List[str]] = Field(
57
+ None, alias="addLabels", description="Comma-separated list of labels to add."
58
+ )
59
+ removeLabels: Optional[List[str]] = Field(
60
+ None, alias="removeLabels", description="Comma-separated list of labels to remove."
61
+ )
62
+ state: Optional[str] = Field(None, description="The state to set the issue to (open or closed).")
63
+
64
+ @model_validator(mode="after")
65
+ def check_updates(self) -> "IssueUpdateInput":
66
+ # pylint: disable=no-member
67
+ if not any([self.body, self.file, self.labels, self.addLabels, self.removeLabels, self.state]):
68
+ raise ValueError("Provide --file, --body, --labels, --addLabels, --removeLabels, or --state")
69
+ if self.body and self.file:
70
+ raise ValueError("Provide --file or --body, not both")
71
+ return self
72
+
73
+
74
+ class CLIResponse(BaseModel):
75
+ status: str = "success"
76
+ message: Optional[str] = None
77
+ data: Optional[Any] = None
78
+
79
+
80
+ class CreateIssueResponse(CLIResponse):
81
+ issue: Optional[IssueSummary] = None
82
+
83
+
84
+ class SearchPRsResponse(CLIResponse):
85
+ prs: List[PRSummary] = Field(default_factory=list)
86
+
87
+
88
+ class IssueUpdateResponse(CLIResponse):
89
+ issue: Optional[IssueSummary] = None
90
+
91
+
92
+ class ReadPRCommentsInput(BaseModel):
93
+ model_config = ConfigDict(populate_by_name=True)
94
+ prNumber: int = Field(..., alias="prNumber", gt=0, description="The PR number to fetch comments for.")
95
+
96
+
97
+ class PRComment(BaseModel):
98
+ user: str
99
+ body: str
100
+ created_at: str
101
+
102
+
103
+ class ReviewComment(BaseModel):
104
+ user: str
105
+ path: str
106
+ line: Optional[int] = None
107
+ body: str
108
+ created_at: str
109
+
110
+
111
+ class ReadPRCommentsResponse(CLIResponse):
112
+ pr: IssueSummary
113
+ comments: List[PRComment] = Field(default_factory=list)
114
+ review_comments: List[ReviewComment] = Field(default_factory=list)
115
+
116
+
117
+ # MCP Tool Inputs
118
+
119
+
120
+ class HealthCheckInput(BaseModel):
121
+ checkDeep: bool = Field(
122
+ False, description="Whether to perform a deep health check including external dependencies."
123
+ )
124
+
125
+
126
+ class GetPrDiffInput(BaseModel):
127
+ prNumber: int = Field(..., description="The number of the pull request to get the diff for.")
128
+
129
+
130
+ class CheckoutBranchInput(BaseModel):
131
+ branch: str = Field(..., description="The name of the branch to checkout.")
132
+ worktreePath: Optional[str] = Field(None, description="Optional path to the worktree to perform the checkout in.")
133
+
134
+
135
+ class GetMergeConflictFilesInput(BaseModel):
136
+ prNumber: int = Field(..., description="The number of the pull request to check for conflicts.")
137
+ baseBranch: str = Field("main", description="The base branch to check against (default: 'main').")
138
+
139
+
140
+ class GetChangedFilesInput(BaseModel):
141
+ base: str = Field("main", description="The base ref to compare from (default: 'main').")
142
+ head: str = Field("HEAD", description="The head ref to compare to (default: 'HEAD').")
143
+
144
+
145
+ class GetPackageScriptsInput(BaseModel):
146
+ filter: Optional[str] = Field(None, description="Optional glob pattern to filter script names.")
147
+
148
+
149
+ class GetRouteMapInput(BaseModel):
150
+ includeStatic: Optional[bool] = Field(None, description="Whether to include static assets in the route map.")
151
+
152
+
153
+ class ReadCiLogsInput(BaseModel):
154
+ prNumber: int = Field(..., description="The number of the pull request to read logs for.")
155
+ all: bool = Field(False, description="Include logs for successful runs (default: false).")
156
+
157
+
158
+ class RepoLogsInput(BaseModel):
159
+ prNumber: int = Field(..., description="The number of the pull request to stream logs for.")
160
+ grep: Optional[str] = Field(None, description="Optional pattern to filter log lines.")
161
+
162
+
163
+ class CreateBranchInput(BaseModel):
164
+ branchName: str = Field(..., description="The name of the new branch")
165
+ baseBranch: str = Field("main", description="Branch to branch off from")
166
+
167
+
168
+ class CreateRepairBranchInput(BaseModel):
169
+ prNumber: int = Field(..., description="The original pull request number to repair.")
170
+ repairBranchName: Optional[str] = Field(None, description="Optional custom name for the new repair branch.")
171
+ writeMode: bool = Field(..., description="Safety gate: Must be true to perform branch creation and worktree setup.")
172
+
173
+
174
+ class RunTestsInput(BaseModel):
175
+ commands: Optional[List[str]] = Field(
176
+ None,
177
+ description="Optional list of commands to run (default includes install, lint, test, build).",
178
+ )
179
+ timeoutSeconds: int = Field(300, description="Maximum time in seconds to wait for tests (default: 300).")
180
+ worktreePath: Optional[str] = Field(None, description="Optional path to the worktree to run tests in.")
181
+
182
+
183
+ class RunLighthouseInput(BaseModel):
184
+ route: str = Field("/", description="The route to audit (default: '/').")
185
+ worktreePath: Optional[str] = Field(None, description="Optional path to the worktree to run the audit in.")
186
+
187
+
188
+ class RunPlaywrightInput(BaseModel):
189
+ grep: Optional[str] = Field(None, description="Optional pattern to filter tests by name.")
190
+ worktreePath: Optional[str] = Field(None, description="Optional path to the worktree to run tests in.")
191
+
192
+
193
+ class CommitPatchInput(BaseModel):
194
+ worktreePath: str = Field(..., description="Path to the worktree where changes are made.")
195
+ message: str = Field(..., description="Commit message.")
196
+ allowedFiles: List[str] = Field(..., description="List of files that are allowed to be committed.")
197
+ writeMode: bool = Field(..., description="Safety gate: Must be true to perform the commit.")
198
+
199
+
200
+ class OpenReplacementPrInput(BaseModel):
201
+ originalPrNumber: int = Field(..., description="The number of the pull request being replaced.")
202
+ repairBranch: str = Field(..., description="The branch containing the fixes.")
203
+ baseBranch: str = Field(..., description="The branch to merge the fixes into.")
204
+ title: str = Field(..., description="The title of the new PR.")
205
+ body: str = Field(..., description="The body/description of the new PR.")
206
+ draft: bool = Field(True, description="Whether to create the PR as a draft (default: true).")
207
+ worktreePath: Optional[str] = Field(None, description="Optional path to the worktree where the PR is created from.")
208
+ pushMode: bool = Field(..., description="Safety gate: Must be true to push the branch and open the PR.")
209
+
210
+
211
+ class CreatePullRequestInput(BaseModel):
212
+ title: str = Field(..., description="PR Title.")
213
+ body: str = Field(..., description="Description of changes.")
214
+ head: str = Field(..., description="The branch containing changes to merge.")
215
+ base: str = Field("main", description="The target branch to merge into.")
216
+ draft: bool = Field(False, description="Whether to create the PR as a draft.")
217
+
218
+
219
+ class CommentTriageSummaryInput(BaseModel):
220
+ prNumber: int = Field(..., description="The number of the original PR to comment on.")
221
+ body: str = Field(..., description="The content of the comment.")
222
+
223
+
224
+ class GetPrInput(BaseModel):
225
+ prNumber: int = Field(..., description="The number of the PR to view.")
226
+
227
+
228
+ class IssueViewInput(BaseModel):
229
+ issueNumber: int = Field(..., description="The number of the issue to view.")
230
+
231
+
232
+ class IssueCommentInput(BaseModel):
233
+ issueNumber: int = Field(..., description="The number of the issue to comment on.")
234
+ body: str = Field(..., description="The content of the comment.")
235
+
236
+
237
+ class CreateJulesSessionInput(BaseModel):
238
+ task: str = Field(..., description="The instructions for Jules.")
239
+ branch: Optional[str] = Field(None, description="The base branch to start from (e.g., 'main').")
240
+ pr: Optional[int] = Field(None, description="The PR number to use as the base branch context.")
241
+
242
+
243
+ class JulesSessionIdInput(BaseModel):
244
+ sessionId: str = Field(..., description="The unique ID of the Jules session.")
245
+
246
+
247
+ class JulesSendMessageInput(BaseModel):
248
+ sessionId: Union[str, List[str]] = Field(..., description="The unique ID or IDs of the Jules session(s).")
249
+ message: str = Field(..., min_length=1, description="The message content to send.")
250
+
251
+ @model_validator(mode="after")
252
+ def validate_send_message(self) -> "JulesSendMessageInput":
253
+ ids = [self.sessionId] if isinstance(self.sessionId, str) else self.sessionId
254
+
255
+ # Enforce batch cap of 50
256
+ BATCH_CAP = 50
257
+ if not ids:
258
+ raise ValueError("sessionId list cannot be empty")
259
+ if len(ids) > BATCH_CAP:
260
+ raise ValueError(f"Batch size exceeds maximum limit of {BATCH_CAP}")
261
+
262
+ # Allow alphanumeric, hyphens, underscores, and forward slashes (for sessions/ prefix)
263
+ pattern = re.compile(r"^[a-zA-Z0-9/_-]+$")
264
+
265
+ for sid in ids:
266
+ if not sid.strip():
267
+ raise ValueError("Session ID cannot be empty or whitespace")
268
+ if not pattern.match(sid):
269
+ raise ValueError(f"Invalid characters in session ID: {sid}")
270
+
271
+ if not self.message.strip():
272
+ raise ValueError("Message cannot be empty or whitespace")
273
+
274
+ return self
275
+
276
+
277
+ class JulesListSessionsInput(BaseModel):
278
+ pageSize: Optional[int] = Field(None, description="Maximum number of sessions to return.")
279
+ pageToken: Optional[str] = Field(None, description="Token for pagination.")
280
+
281
+
282
+ class SearchDdgsInput(BaseModel):
283
+ query: str = Field(..., description="The search query.")
284
+ maxResults: Optional[int] = Field(None, description="Maximum number of results to return.")
285
+
286
+
287
+ class ReadAgentContextInput(BaseModel):
288
+ pass
289
+
290
+
291
+ class GetCommandSchemaInput(BaseModel):
292
+ commandPath: str = Field(..., description="The CLI command path to retrieve the schema for (e.g. 'gh audit-pr')")
293
+
294
+
295
+ # AI Review Models
296
+
297
+
298
+ class AIReviewIssue(BaseModel):
299
+ model_config = ConfigDict(populate_by_name=True)
300
+ id: Optional[str] = Field(None, description="Unique identifier for the finding.")
301
+ line: Optional[int] = Field(None, description="Line number where the issue occurs.")
302
+ file: Optional[str] = Field(None, description="File path (if not already scoped by AIFileReview).")
303
+ severity: Literal["error", "warn", "info"] = Field(..., description="The severity level.")
304
+ issue: Optional[str] = Field(None, description="Description of the issue (alternative to 'comment').")
305
+ comment: Optional[str] = Field(None, description="Description of the issue.")
306
+ status: Optional[str] = Field("open", description="Status of the finding.")
307
+ confidence: Literal["high", "medium", "low"] = Field(..., description="Confidence level of the AI.")
308
+ counterexample: Optional[str] = Field(None, description="Example of how to fix or why it fails.")
309
+ snippet: Optional[str] = Field(None, description="The exact code snippet from the diff.")
310
+
311
+ @field_validator("severity", "confidence", mode="before")
312
+ @classmethod
313
+ def normalize_enums(cls, v: Any) -> Any:
314
+ if isinstance(v, str):
315
+ return v.lower().strip()
316
+ return v
317
+
318
+ @model_validator(mode="before")
319
+ @classmethod
320
+ def reconcile_issue_fields(cls, data: Any) -> Any:
321
+ if isinstance(data, dict):
322
+ # Sync issue and comment fields
323
+ issue = data.get("issue")
324
+ comment = data.get("comment")
325
+ if issue and not comment:
326
+ data["comment"] = issue
327
+ elif comment and not issue:
328
+ data["issue"] = comment
329
+ return data
330
+
331
+ @model_validator(mode="after")
332
+ def validate_content(self) -> "AIReviewIssue":
333
+ if not self.issue and not self.comment:
334
+ raise ValueError("AI review issue must have either an 'issue' or 'comment' field.")
335
+ return self
336
+
337
+
338
+ class AIFileReview(BaseModel):
339
+ file: str = Field(..., description="The path of the file being reviewed.")
340
+ issues: List[AIReviewIssue] = Field(default_factory=list, description="List of findings in this file.")
341
+ verdict: str = Field(..., description="The verdict for this file (ok, needs_changes, blocking).")
342
+
343
+
344
+ class AIFullReview(BaseModel):
345
+ file_reviews: List[AIFileReview] = Field(default_factory=list, description="Per-file review results.")
346
+ reviewComment: str = Field(..., description="Overall summary comment for the PR.")
347
+ labels: List[str] = Field(default_factory=list, description="Suggested labels for the PR.")
348
+ recommendation: str = Field(..., description="Final recommendation (Approved, Not Approved, etc).")
349
+
350
+
351
+ class AISynthesisReview(BaseModel):
352
+ reviewComment: str = Field(..., description="Overall summary comment for the PR.")
353
+ labels: List[str] = Field(default_factory=list, description="Suggested labels for the PR.")
354
+ recommendation: str = Field(..., description="Final recommendation.")