titan-cli 0.1.0__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 (146) hide show
  1. titan_cli/__init__.py +3 -0
  2. titan_cli/__main__.py +4 -0
  3. titan_cli/ai/__init__.py +0 -0
  4. titan_cli/ai/agents/__init__.py +15 -0
  5. titan_cli/ai/agents/base.py +152 -0
  6. titan_cli/ai/client.py +170 -0
  7. titan_cli/ai/constants.py +56 -0
  8. titan_cli/ai/exceptions.py +48 -0
  9. titan_cli/ai/models.py +34 -0
  10. titan_cli/ai/oauth_helper.py +120 -0
  11. titan_cli/ai/providers/__init__.py +9 -0
  12. titan_cli/ai/providers/anthropic.py +117 -0
  13. titan_cli/ai/providers/base.py +75 -0
  14. titan_cli/ai/providers/gemini.py +278 -0
  15. titan_cli/cli.py +59 -0
  16. titan_cli/clients/__init__.py +1 -0
  17. titan_cli/clients/gcloud_client.py +52 -0
  18. titan_cli/core/__init__.py +3 -0
  19. titan_cli/core/config.py +274 -0
  20. titan_cli/core/discovery.py +51 -0
  21. titan_cli/core/errors.py +81 -0
  22. titan_cli/core/models.py +52 -0
  23. titan_cli/core/plugins/available.py +36 -0
  24. titan_cli/core/plugins/models.py +67 -0
  25. titan_cli/core/plugins/plugin_base.py +108 -0
  26. titan_cli/core/plugins/plugin_registry.py +163 -0
  27. titan_cli/core/secrets.py +141 -0
  28. titan_cli/core/workflows/__init__.py +22 -0
  29. titan_cli/core/workflows/models.py +88 -0
  30. titan_cli/core/workflows/project_step_source.py +86 -0
  31. titan_cli/core/workflows/workflow_exceptions.py +17 -0
  32. titan_cli/core/workflows/workflow_filter_service.py +137 -0
  33. titan_cli/core/workflows/workflow_registry.py +419 -0
  34. titan_cli/core/workflows/workflow_sources.py +307 -0
  35. titan_cli/engine/__init__.py +39 -0
  36. titan_cli/engine/builder.py +159 -0
  37. titan_cli/engine/context.py +82 -0
  38. titan_cli/engine/mock_context.py +176 -0
  39. titan_cli/engine/results.py +91 -0
  40. titan_cli/engine/steps/ai_assistant_step.py +185 -0
  41. titan_cli/engine/steps/command_step.py +93 -0
  42. titan_cli/engine/utils/__init__.py +3 -0
  43. titan_cli/engine/utils/venv.py +31 -0
  44. titan_cli/engine/workflow_executor.py +187 -0
  45. titan_cli/external_cli/__init__.py +0 -0
  46. titan_cli/external_cli/configs.py +17 -0
  47. titan_cli/external_cli/launcher.py +65 -0
  48. titan_cli/messages.py +121 -0
  49. titan_cli/ui/tui/__init__.py +205 -0
  50. titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
  51. titan_cli/ui/tui/app.py +113 -0
  52. titan_cli/ui/tui/icons.py +70 -0
  53. titan_cli/ui/tui/screens/__init__.py +24 -0
  54. titan_cli/ui/tui/screens/ai_config.py +498 -0
  55. titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
  56. titan_cli/ui/tui/screens/base.py +110 -0
  57. titan_cli/ui/tui/screens/cli_launcher.py +151 -0
  58. titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
  59. titan_cli/ui/tui/screens/main_menu.py +162 -0
  60. titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
  61. titan_cli/ui/tui/screens/plugin_management.py +377 -0
  62. titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
  63. titan_cli/ui/tui/screens/workflow_execution.py +592 -0
  64. titan_cli/ui/tui/screens/workflows.py +249 -0
  65. titan_cli/ui/tui/textual_components.py +537 -0
  66. titan_cli/ui/tui/textual_workflow_executor.py +405 -0
  67. titan_cli/ui/tui/theme.py +102 -0
  68. titan_cli/ui/tui/widgets/__init__.py +40 -0
  69. titan_cli/ui/tui/widgets/button.py +108 -0
  70. titan_cli/ui/tui/widgets/header.py +116 -0
  71. titan_cli/ui/tui/widgets/panel.py +81 -0
  72. titan_cli/ui/tui/widgets/status_bar.py +115 -0
  73. titan_cli/ui/tui/widgets/table.py +77 -0
  74. titan_cli/ui/tui/widgets/text.py +177 -0
  75. titan_cli/utils/__init__.py +0 -0
  76. titan_cli/utils/autoupdate.py +155 -0
  77. titan_cli-0.1.0.dist-info/METADATA +149 -0
  78. titan_cli-0.1.0.dist-info/RECORD +146 -0
  79. titan_cli-0.1.0.dist-info/WHEEL +4 -0
  80. titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
  81. titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
  82. titan_plugin_git/__init__.py +1 -0
  83. titan_plugin_git/clients/__init__.py +8 -0
  84. titan_plugin_git/clients/git_client.py +772 -0
  85. titan_plugin_git/exceptions.py +40 -0
  86. titan_plugin_git/messages.py +112 -0
  87. titan_plugin_git/models.py +39 -0
  88. titan_plugin_git/plugin.py +118 -0
  89. titan_plugin_git/steps/__init__.py +1 -0
  90. titan_plugin_git/steps/ai_commit_message_step.py +171 -0
  91. titan_plugin_git/steps/branch_steps.py +104 -0
  92. titan_plugin_git/steps/commit_step.py +80 -0
  93. titan_plugin_git/steps/push_step.py +63 -0
  94. titan_plugin_git/steps/status_step.py +59 -0
  95. titan_plugin_git/workflows/__previews__/__init__.py +1 -0
  96. titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
  97. titan_plugin_git/workflows/commit-ai.yaml +28 -0
  98. titan_plugin_github/__init__.py +11 -0
  99. titan_plugin_github/agents/__init__.py +6 -0
  100. titan_plugin_github/agents/config_loader.py +130 -0
  101. titan_plugin_github/agents/issue_generator.py +353 -0
  102. titan_plugin_github/agents/pr_agent.py +528 -0
  103. titan_plugin_github/clients/__init__.py +8 -0
  104. titan_plugin_github/clients/github_client.py +1105 -0
  105. titan_plugin_github/config/__init__.py +0 -0
  106. titan_plugin_github/config/pr_agent.toml +85 -0
  107. titan_plugin_github/exceptions.py +28 -0
  108. titan_plugin_github/messages.py +88 -0
  109. titan_plugin_github/models.py +330 -0
  110. titan_plugin_github/plugin.py +131 -0
  111. titan_plugin_github/steps/__init__.py +12 -0
  112. titan_plugin_github/steps/ai_pr_step.py +172 -0
  113. titan_plugin_github/steps/create_pr_step.py +86 -0
  114. titan_plugin_github/steps/github_prompt_steps.py +171 -0
  115. titan_plugin_github/steps/issue_steps.py +143 -0
  116. titan_plugin_github/steps/preview_step.py +40 -0
  117. titan_plugin_github/utils.py +82 -0
  118. titan_plugin_github/workflows/__previews__/__init__.py +1 -0
  119. titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
  120. titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
  121. titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
  122. titan_plugin_jira/__init__.py +8 -0
  123. titan_plugin_jira/agents/__init__.py +6 -0
  124. titan_plugin_jira/agents/config_loader.py +154 -0
  125. titan_plugin_jira/agents/jira_agent.py +553 -0
  126. titan_plugin_jira/agents/prompts.py +364 -0
  127. titan_plugin_jira/agents/response_parser.py +435 -0
  128. titan_plugin_jira/agents/token_tracker.py +223 -0
  129. titan_plugin_jira/agents/validators.py +246 -0
  130. titan_plugin_jira/clients/jira_client.py +745 -0
  131. titan_plugin_jira/config/jira_agent.toml +92 -0
  132. titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
  133. titan_plugin_jira/exceptions.py +37 -0
  134. titan_plugin_jira/formatters/__init__.py +6 -0
  135. titan_plugin_jira/formatters/markdown_formatter.py +245 -0
  136. titan_plugin_jira/messages.py +115 -0
  137. titan_plugin_jira/models.py +89 -0
  138. titan_plugin_jira/plugin.py +264 -0
  139. titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
  140. titan_plugin_jira/steps/get_issue_step.py +82 -0
  141. titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
  142. titan_plugin_jira/steps/search_saved_query_step.py +238 -0
  143. titan_plugin_jira/utils/__init__.py +13 -0
  144. titan_plugin_jira/utils/issue_sorter.py +140 -0
  145. titan_plugin_jira/utils/saved_queries.py +150 -0
  146. titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
@@ -0,0 +1,353 @@
1
+ from pathlib import Path
2
+ from typing import Dict, Optional, Tuple
3
+ import re
4
+ import json
5
+ from dataclasses import dataclass
6
+ from titan_cli.ai.agents.base import BaseAIAgent, AgentRequest, AIGenerator
7
+
8
+
9
+ @dataclass
10
+ class IssueSizeEstimation:
11
+ """
12
+ Issue size estimation with token limits.
13
+
14
+ Attributes:
15
+ complexity: Size category (simple, moderate, complex, very_complex)
16
+ max_tokens: Maximum tokens for issue description generation
17
+ description_length: Length of user's description
18
+ """
19
+ complexity: str
20
+ max_tokens: int
21
+ description_length: int
22
+
23
+
24
+ class IssueGeneratorAgent(BaseAIAgent):
25
+ def __init__(self, ai_client: AIGenerator, template_dir: Optional[Path] = None):
26
+ super().__init__(ai_client)
27
+ self.template_dir = template_dir or Path(".github/ISSUE_TEMPLATE")
28
+ self.max_tokens = 8192 # Add token limit configuration
29
+ self.categories = {
30
+ "feature": {
31
+ "template": "feature.md",
32
+ "labels": ["feature"],
33
+ "prefix": "feat"
34
+ },
35
+ "improvement": {
36
+ "template": "improvement.md",
37
+ "labels": ["improvement"],
38
+ "prefix": "improve"
39
+ },
40
+ "bug": {
41
+ "template": "bug.md",
42
+ "labels": ["bug"],
43
+ "prefix": "fix"
44
+ },
45
+ "refactor": {
46
+ "template": "refactor.md",
47
+ "labels": ["refactor", "technical-debt"],
48
+ "prefix": "refactor"
49
+ },
50
+ "chore": {
51
+ "template": "chore.md",
52
+ "labels": ["chore", "maintenance"],
53
+ "prefix": "chore"
54
+ },
55
+ "documentation": {
56
+ "template": "documentation.md",
57
+ "labels": ["documentation"],
58
+ "prefix": "docs"
59
+ }
60
+ }
61
+
62
+ # Label aliases for mapping to different repository label conventions
63
+ self.label_aliases = {
64
+ "feature": ["feature", "enhancement", "new-feature", "feat"],
65
+ "improvement": ["improvement", "enhancement", "optimization", "perf"],
66
+ "bug": ["bug", "fix", "defect", "error"],
67
+ "refactor": ["refactor", "refactoring", "technical-debt", "tech-debt"],
68
+ "chore": ["chore", "maintenance", "housekeeping"],
69
+ "documentation": ["documentation", "docs", "doc"]
70
+ }
71
+
72
+ def get_system_prompt(self) -> str:
73
+ return """You are an expert at creating highly professional, descriptive, and useful GitHub issues.
74
+ Your task is to:
75
+ 1. Analyze the user's description and categorize it
76
+ 2. Generate an issue title and detailed description following the appropriate template (if available)
77
+ 3. Ensure the title follows the Conventional Commits specification (e.g., "feat(scope): brief description")
78
+ 4. Use English for all content
79
+ 5. Prioritize clarity, conciseness, and actionable detail
80
+ 6. Preserve any code snippets exactly as provided, formatted in markdown code blocks
81
+ 7. Always return your response as valid JSON format for reliable parsing
82
+ """
83
+
84
+ def _load_template(self, template_name: str) -> Optional[str]:
85
+ """
86
+ Load a template file from the template directory.
87
+ Returns None if template doesn't exist or can't be read.
88
+ """
89
+ try:
90
+ template_path = self.template_dir / template_name
91
+ if template_path.exists() and template_path.is_file():
92
+ return template_path.read_text(encoding="utf-8")
93
+ except (IOError, FileNotFoundError, PermissionError, OSError):
94
+ pass
95
+ return None
96
+
97
+ def _load_all_templates(self) -> Dict[str, Optional[str]]:
98
+ """
99
+ Load all available templates.
100
+ Returns a dict mapping category name to template content.
101
+ """
102
+ templates = {}
103
+ for category, info in self.categories.items():
104
+ template_content = self._load_template(info["template"])
105
+ templates[category] = template_content
106
+ return templates
107
+
108
+ def _estimate_issue_complexity(self, description: str) -> IssueSizeEstimation:
109
+ """
110
+ Estimate issue complexity based on description length and content.
111
+
112
+ Args:
113
+ description: User's issue description
114
+
115
+ Returns:
116
+ IssueSizeEstimation with complexity and token limits
117
+ """
118
+ desc_length = len(description)
119
+ # Count newlines as indicator of detail
120
+ lines = description.count('\n') + 1
121
+ # Count code blocks as indicator of technical complexity
122
+ code_blocks = description.count('```')
123
+
124
+ # Determine complexity and set appropriate token limits
125
+ if desc_length < 200 and lines <= 3 and code_blocks == 0:
126
+ # Simple issue: brief request, quick fix
127
+ complexity = "simple"
128
+ max_tokens = 3000 # Increased to ensure complete JSON
129
+ elif desc_length < 500 and lines <= 10:
130
+ # Moderate issue: standard feature or bug with some detail
131
+ complexity = "moderate"
132
+ max_tokens = 4000 # Increased to ensure complete JSON
133
+ elif desc_length < 1000 and lines <= 25:
134
+ # Complex issue: detailed requirements, multiple aspects
135
+ complexity = "complex"
136
+ max_tokens = 6000 # Increased to ensure complete JSON
137
+ else:
138
+ # Very complex: comprehensive spec, architectural changes
139
+ complexity = "very_complex"
140
+ max_tokens = 8000 # Increased to ensure complete JSON
141
+
142
+ return IssueSizeEstimation(
143
+ complexity=complexity,
144
+ max_tokens=max_tokens,
145
+ description_length=desc_length
146
+ )
147
+
148
+ def _parse_ai_response(self, content: str) -> Tuple[str, str, str]:
149
+ """
150
+ Parse AI response to extract category, title, and body.
151
+ Tries JSON parsing first, falls back to regex for robustness.
152
+ Handles incomplete JSON by attempting to fix it.
153
+
154
+ Returns:
155
+ Tuple of (category, title, body)
156
+ """
157
+ # Try JSON parsing first (more robust, avoids conflicts with user text)
158
+ try:
159
+ # Remove ONLY the outer markdown code block wrapper (```json ... ```)
160
+ # but preserve code blocks inside the JSON body content
161
+ cleaned_content = content.strip()
162
+
163
+ # Remove opening ```json or ``` if present at the start
164
+ if cleaned_content.startswith('```json'):
165
+ cleaned_content = cleaned_content[7:].lstrip()
166
+ elif cleaned_content.startswith('```'):
167
+ cleaned_content = cleaned_content[3:].lstrip()
168
+
169
+ # Remove closing ``` if present at the end
170
+ if cleaned_content.endswith('```'):
171
+ cleaned_content = cleaned_content[:-3].rstrip()
172
+
173
+ # Try to find JSON in the content (handle incomplete JSON)
174
+ json_match = re.search(r'\{[\s\S]*\}', cleaned_content)
175
+
176
+ # If no complete JSON found, try to fix incomplete JSON
177
+ if not json_match and cleaned_content.strip().startswith('{'):
178
+ # JSON might be incomplete (missing closing quote and brace)
179
+ json_str = cleaned_content.strip()
180
+ # Try to close the JSON properly
181
+ if not json_str.endswith('}'):
182
+ # Close any unclosed string first
183
+ if json_str.count('"') % 2 != 0:
184
+ json_str = json_str + '"'
185
+ # Then close the JSON object
186
+ json_str = json_str + '\n}'
187
+ elif json_match:
188
+ json_str = json_match.group(0)
189
+ else:
190
+ json_str = None
191
+
192
+ if json_str:
193
+ data = json.loads(json_str)
194
+
195
+ category = data.get("category", "feature").lower()
196
+ title = data.get("title", "New issue")
197
+ body = data.get("body", "")
198
+
199
+ # Validate category
200
+ if category not in self.categories:
201
+ category = "feature"
202
+
203
+ return category, title, body
204
+ except (json.JSONDecodeError, AttributeError):
205
+ # JSON parsing failed, fall back to regex
206
+ pass
207
+
208
+ # Fallback: regex parsing for backwards compatibility
209
+ # Extract category
210
+ category_match = re.search(r'CATEGORY:\s*(\w+)', content, re.IGNORECASE)
211
+ category = category_match.group(1).strip().lower() if category_match else "feature"
212
+
213
+ # Validate category
214
+ if category not in self.categories:
215
+ category = "feature"
216
+
217
+ # Extract title
218
+ title_match = re.search(r'TITLE:\s*(.+?)(?=\n|DESCRIPTION:|$)', content, re.IGNORECASE)
219
+ title = title_match.group(1).strip() if title_match else "New issue"
220
+
221
+ # Extract description/body
222
+ desc_match = re.search(r'DESCRIPTION:\s*(.+)', content, re.IGNORECASE | re.DOTALL)
223
+ body = desc_match.group(1).strip() if desc_match else content
224
+
225
+ return category, title, body
226
+
227
+ def _map_labels_to_available(self, category: str, available_labels: Optional[list] = None) -> list:
228
+ """
229
+ Map category labels to available repository labels using aliases.
230
+
231
+ Args:
232
+ category: The issue category
233
+ available_labels: List of labels available in the repository (optional)
234
+
235
+ Returns:
236
+ List of labels that exist in the repository, or default labels if no filtering
237
+ """
238
+ default_labels = self.categories.get(category, self.categories["feature"])["labels"]
239
+
240
+ # If no available_labels provided, return defaults (no filtering)
241
+ if available_labels is None:
242
+ return default_labels
243
+
244
+ # Get aliases for this category
245
+ aliases = self.label_aliases.get(category, [])
246
+
247
+ # Find matching labels in the repository
248
+ matched_labels = []
249
+ for alias in aliases:
250
+ # Case-insensitive matching
251
+ if alias.lower() in [label.lower() for label in available_labels]:
252
+ # Find the exact case from available_labels
253
+ matched_label = next(label for label in available_labels if label.lower() == alias.lower())
254
+ if matched_label not in matched_labels:
255
+ matched_labels.append(matched_label)
256
+
257
+ # Fallback: if no matches found, return empty list (graceful degradation)
258
+ return matched_labels
259
+
260
+ def generate_issue(self, user_description: str, available_labels: Optional[list] = None) -> Dict[str, any]:
261
+ """
262
+ Generate a complete issue with auto-categorization in a single AI call.
263
+
264
+ Returns:
265
+ dict with keys: title, body, category, labels, template_used, tokens_used, complexity
266
+ """
267
+ # Estimate issue complexity to determine appropriate token allocation
268
+ estimation = self._estimate_issue_complexity(user_description)
269
+
270
+ # Load all available templates
271
+ all_templates = self._load_all_templates()
272
+
273
+ # Build prompt that includes all templates and asks AI to categorize + generate
274
+ templates_section = self._format_templates_for_prompt(all_templates)
275
+
276
+ prompt = f"""
277
+ Analyze the following issue description and:
278
+ 1. Categorize it into ONE category: feature, improvement, bug, refactor, chore, or documentation
279
+ 2. Generate a complete GitHub issue following the appropriate template
280
+
281
+ User description:
282
+ ---
283
+ {user_description}
284
+ ---
285
+
286
+ {templates_section}
287
+
288
+ Instructions:
289
+ - Choose the most appropriate category based on the description
290
+ - Follow the template structure for that category (if available)
291
+ - Remove all HTML comments (<!-- -->)
292
+ - Keep only section headers (##) that have meaningful content
293
+ - IMPORTANT: Completely omit optional sections (marked with "Optional") if they don't apply or have no meaningful content
294
+ - Never include a section header followed by just a language identifier (e.g., "python", "yaml") without actual code
295
+ - Fill in meaningful content for each section you include
296
+ - Preserve any code snippets exactly as provided in markdown code blocks
297
+ - Use the correct conventional commit prefix for the title
298
+ - Adjust detail level based on issue complexity ({estimation.complexity}):
299
+ * Simple: Brief, direct descriptions (1-2 sentences per section)
300
+ * Moderate: Standard detail (2-3 sentences per section)
301
+ * Complex: Comprehensive detail with examples
302
+ * Very Complex: Full context, edge cases, and implementation notes
303
+
304
+ Output format (REQUIRED - JSON):
305
+ {{
306
+ "category": "<category>",
307
+ "title": "<prefix>(scope): brief description",
308
+ "body": "<complete markdown-formatted description>"
309
+ }}
310
+ """
311
+
312
+ # Single AI call for categorization + generation with appropriate token limit
313
+ request = AgentRequest(
314
+ context=prompt,
315
+ max_tokens=estimation.max_tokens
316
+ )
317
+ response = self.generate(request)
318
+
319
+ # Parse response using robust regex parsing
320
+ category, title, body = self._parse_ai_response(response.content)
321
+
322
+ template_used = all_templates.get(category) is not None
323
+
324
+ # Map labels to available repository labels (with fallback to empty list)
325
+ labels = self._map_labels_to_available(category, available_labels)
326
+
327
+ return {
328
+ "title": title,
329
+ "body": body,
330
+ "category": category,
331
+ "labels": labels,
332
+ "template_used": template_used,
333
+ "tokens_used": response.tokens_used,
334
+ "complexity": estimation.complexity
335
+ }
336
+
337
+ def _format_templates_for_prompt(self, templates: Dict[str, Optional[str]]) -> str:
338
+ """
339
+ Format all templates into a string for the AI prompt.
340
+ """
341
+ if not any(templates.values()):
342
+ return "No templates available. Generate structured content based on category best practices."
343
+
344
+ formatted = "Available templates:\n\n"
345
+ for category, template_content in templates.items():
346
+ if template_content:
347
+ category_info = self.categories[category]
348
+ formatted += f"### {category.upper()} (prefix: {category_info['prefix']})\n"
349
+ formatted += f"```\n{template_content}\n```\n\n"
350
+ else:
351
+ formatted += f"### {category.upper()} (no template available)\n\n"
352
+
353
+ return formatted