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.
- titan_cli/__init__.py +3 -0
- titan_cli/__main__.py +4 -0
- titan_cli/ai/__init__.py +0 -0
- titan_cli/ai/agents/__init__.py +15 -0
- titan_cli/ai/agents/base.py +152 -0
- titan_cli/ai/client.py +170 -0
- titan_cli/ai/constants.py +56 -0
- titan_cli/ai/exceptions.py +48 -0
- titan_cli/ai/models.py +34 -0
- titan_cli/ai/oauth_helper.py +120 -0
- titan_cli/ai/providers/__init__.py +9 -0
- titan_cli/ai/providers/anthropic.py +117 -0
- titan_cli/ai/providers/base.py +75 -0
- titan_cli/ai/providers/gemini.py +278 -0
- titan_cli/cli.py +59 -0
- titan_cli/clients/__init__.py +1 -0
- titan_cli/clients/gcloud_client.py +52 -0
- titan_cli/core/__init__.py +3 -0
- titan_cli/core/config.py +274 -0
- titan_cli/core/discovery.py +51 -0
- titan_cli/core/errors.py +81 -0
- titan_cli/core/models.py +52 -0
- titan_cli/core/plugins/available.py +36 -0
- titan_cli/core/plugins/models.py +67 -0
- titan_cli/core/plugins/plugin_base.py +108 -0
- titan_cli/core/plugins/plugin_registry.py +163 -0
- titan_cli/core/secrets.py +141 -0
- titan_cli/core/workflows/__init__.py +22 -0
- titan_cli/core/workflows/models.py +88 -0
- titan_cli/core/workflows/project_step_source.py +86 -0
- titan_cli/core/workflows/workflow_exceptions.py +17 -0
- titan_cli/core/workflows/workflow_filter_service.py +137 -0
- titan_cli/core/workflows/workflow_registry.py +419 -0
- titan_cli/core/workflows/workflow_sources.py +307 -0
- titan_cli/engine/__init__.py +39 -0
- titan_cli/engine/builder.py +159 -0
- titan_cli/engine/context.py +82 -0
- titan_cli/engine/mock_context.py +176 -0
- titan_cli/engine/results.py +91 -0
- titan_cli/engine/steps/ai_assistant_step.py +185 -0
- titan_cli/engine/steps/command_step.py +93 -0
- titan_cli/engine/utils/__init__.py +3 -0
- titan_cli/engine/utils/venv.py +31 -0
- titan_cli/engine/workflow_executor.py +187 -0
- titan_cli/external_cli/__init__.py +0 -0
- titan_cli/external_cli/configs.py +17 -0
- titan_cli/external_cli/launcher.py +65 -0
- titan_cli/messages.py +121 -0
- titan_cli/ui/tui/__init__.py +205 -0
- titan_cli/ui/tui/__previews__/statusbar_preview.py +88 -0
- titan_cli/ui/tui/app.py +113 -0
- titan_cli/ui/tui/icons.py +70 -0
- titan_cli/ui/tui/screens/__init__.py +24 -0
- titan_cli/ui/tui/screens/ai_config.py +498 -0
- titan_cli/ui/tui/screens/ai_config_wizard.py +882 -0
- titan_cli/ui/tui/screens/base.py +110 -0
- titan_cli/ui/tui/screens/cli_launcher.py +151 -0
- titan_cli/ui/tui/screens/global_setup_wizard.py +363 -0
- titan_cli/ui/tui/screens/main_menu.py +162 -0
- titan_cli/ui/tui/screens/plugin_config_wizard.py +550 -0
- titan_cli/ui/tui/screens/plugin_management.py +377 -0
- titan_cli/ui/tui/screens/project_setup_wizard.py +686 -0
- titan_cli/ui/tui/screens/workflow_execution.py +592 -0
- titan_cli/ui/tui/screens/workflows.py +249 -0
- titan_cli/ui/tui/textual_components.py +537 -0
- titan_cli/ui/tui/textual_workflow_executor.py +405 -0
- titan_cli/ui/tui/theme.py +102 -0
- titan_cli/ui/tui/widgets/__init__.py +40 -0
- titan_cli/ui/tui/widgets/button.py +108 -0
- titan_cli/ui/tui/widgets/header.py +116 -0
- titan_cli/ui/tui/widgets/panel.py +81 -0
- titan_cli/ui/tui/widgets/status_bar.py +115 -0
- titan_cli/ui/tui/widgets/table.py +77 -0
- titan_cli/ui/tui/widgets/text.py +177 -0
- titan_cli/utils/__init__.py +0 -0
- titan_cli/utils/autoupdate.py +155 -0
- titan_cli-0.1.0.dist-info/METADATA +149 -0
- titan_cli-0.1.0.dist-info/RECORD +146 -0
- titan_cli-0.1.0.dist-info/WHEEL +4 -0
- titan_cli-0.1.0.dist-info/entry_points.txt +9 -0
- titan_cli-0.1.0.dist-info/licenses/LICENSE +201 -0
- titan_plugin_git/__init__.py +1 -0
- titan_plugin_git/clients/__init__.py +8 -0
- titan_plugin_git/clients/git_client.py +772 -0
- titan_plugin_git/exceptions.py +40 -0
- titan_plugin_git/messages.py +112 -0
- titan_plugin_git/models.py +39 -0
- titan_plugin_git/plugin.py +118 -0
- titan_plugin_git/steps/__init__.py +1 -0
- titan_plugin_git/steps/ai_commit_message_step.py +171 -0
- titan_plugin_git/steps/branch_steps.py +104 -0
- titan_plugin_git/steps/commit_step.py +80 -0
- titan_plugin_git/steps/push_step.py +63 -0
- titan_plugin_git/steps/status_step.py +59 -0
- titan_plugin_git/workflows/__previews__/__init__.py +1 -0
- titan_plugin_git/workflows/__previews__/commit_ai_preview.py +124 -0
- titan_plugin_git/workflows/commit-ai.yaml +28 -0
- titan_plugin_github/__init__.py +11 -0
- titan_plugin_github/agents/__init__.py +6 -0
- titan_plugin_github/agents/config_loader.py +130 -0
- titan_plugin_github/agents/issue_generator.py +353 -0
- titan_plugin_github/agents/pr_agent.py +528 -0
- titan_plugin_github/clients/__init__.py +8 -0
- titan_plugin_github/clients/github_client.py +1105 -0
- titan_plugin_github/config/__init__.py +0 -0
- titan_plugin_github/config/pr_agent.toml +85 -0
- titan_plugin_github/exceptions.py +28 -0
- titan_plugin_github/messages.py +88 -0
- titan_plugin_github/models.py +330 -0
- titan_plugin_github/plugin.py +131 -0
- titan_plugin_github/steps/__init__.py +12 -0
- titan_plugin_github/steps/ai_pr_step.py +172 -0
- titan_plugin_github/steps/create_pr_step.py +86 -0
- titan_plugin_github/steps/github_prompt_steps.py +171 -0
- titan_plugin_github/steps/issue_steps.py +143 -0
- titan_plugin_github/steps/preview_step.py +40 -0
- titan_plugin_github/utils.py +82 -0
- titan_plugin_github/workflows/__previews__/__init__.py +1 -0
- titan_plugin_github/workflows/__previews__/create_pr_ai_preview.py +140 -0
- titan_plugin_github/workflows/create-issue-ai.yaml +32 -0
- titan_plugin_github/workflows/create-pr-ai.yaml +49 -0
- titan_plugin_jira/__init__.py +8 -0
- titan_plugin_jira/agents/__init__.py +6 -0
- titan_plugin_jira/agents/config_loader.py +154 -0
- titan_plugin_jira/agents/jira_agent.py +553 -0
- titan_plugin_jira/agents/prompts.py +364 -0
- titan_plugin_jira/agents/response_parser.py +435 -0
- titan_plugin_jira/agents/token_tracker.py +223 -0
- titan_plugin_jira/agents/validators.py +246 -0
- titan_plugin_jira/clients/jira_client.py +745 -0
- titan_plugin_jira/config/jira_agent.toml +92 -0
- titan_plugin_jira/config/templates/issue_analysis.md.j2 +78 -0
- titan_plugin_jira/exceptions.py +37 -0
- titan_plugin_jira/formatters/__init__.py +6 -0
- titan_plugin_jira/formatters/markdown_formatter.py +245 -0
- titan_plugin_jira/messages.py +115 -0
- titan_plugin_jira/models.py +89 -0
- titan_plugin_jira/plugin.py +264 -0
- titan_plugin_jira/steps/ai_analyze_issue_step.py +105 -0
- titan_plugin_jira/steps/get_issue_step.py +82 -0
- titan_plugin_jira/steps/prompt_select_issue_step.py +80 -0
- titan_plugin_jira/steps/search_saved_query_step.py +238 -0
- titan_plugin_jira/utils/__init__.py +13 -0
- titan_plugin_jira/utils/issue_sorter.py +140 -0
- titan_plugin_jira/utils/saved_queries.py +150 -0
- titan_plugin_jira/workflows/analyze-jira-issues.yaml +34 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, TypedDict, List
|
|
3
|
+
from pydantic import ValidationError
|
|
4
|
+
from titan_cli.core.plugins.models import JiraPluginConfig
|
|
5
|
+
from titan_cli.core.plugins.plugin_base import TitanPlugin
|
|
6
|
+
from titan_cli.core.config import TitanConfig
|
|
7
|
+
from titan_cli.core.secrets import SecretManager
|
|
8
|
+
from .clients.jira_client import JiraClient
|
|
9
|
+
from .exceptions import JiraConfigurationError, JiraClientError
|
|
10
|
+
from .messages import msg
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TokenValidationResult(TypedDict):
|
|
14
|
+
"""Result of token validation."""
|
|
15
|
+
valid: bool
|
|
16
|
+
error: Optional[str]
|
|
17
|
+
user: Optional[str]
|
|
18
|
+
email: Optional[str]
|
|
19
|
+
token_source: dict
|
|
20
|
+
warnings: List[str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class JiraPlugin(TitanPlugin):
|
|
24
|
+
"""
|
|
25
|
+
Titan CLI Plugin for JIRA operations.
|
|
26
|
+
Provides a JiraClient for interacting with JIRA REST API.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def name(self) -> str:
|
|
31
|
+
return "jira"
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def description(self) -> str:
|
|
35
|
+
return "Provides JIRA API integration with AI-powered issue management."
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def dependencies(self) -> list[str]:
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
def initialize(self, config: TitanConfig, secrets: SecretManager) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Initialize with configuration.
|
|
44
|
+
|
|
45
|
+
Configuration cascade (project overrides global):
|
|
46
|
+
1. Global credentials (~/.titan/config.toml): base_url, email
|
|
47
|
+
2. Project settings (.titan/config.toml): default_project (optional)
|
|
48
|
+
|
|
49
|
+
Note: TitanConfig automatically merges global and project configs,
|
|
50
|
+
so _get_plugin_config() returns the already-merged configuration.
|
|
51
|
+
|
|
52
|
+
Reads API token from secrets:
|
|
53
|
+
JIRA_API_TOKEN or {email}_jira_api_token
|
|
54
|
+
"""
|
|
55
|
+
# Get plugin-specific configuration data (already merged by TitanConfig)
|
|
56
|
+
plugin_config_data = self._get_plugin_config(config)
|
|
57
|
+
|
|
58
|
+
# Validate configuration using Pydantic model
|
|
59
|
+
# Pydantic validators will check base_url and email during construction
|
|
60
|
+
try:
|
|
61
|
+
validated_config = JiraPluginConfig(**plugin_config_data)
|
|
62
|
+
except ValidationError as e:
|
|
63
|
+
raise JiraConfigurationError(str(e)) from e
|
|
64
|
+
|
|
65
|
+
# Get API token from secrets
|
|
66
|
+
# Try multiple secret keys for backwards compatibility
|
|
67
|
+
# Priority: project-specific → plugin-specific → env var → email-specific
|
|
68
|
+
project_name = config.get_project_name()
|
|
69
|
+
project_key = f"{project_name}_jira_api_token" if project_name else None
|
|
70
|
+
|
|
71
|
+
api_token = (
|
|
72
|
+
(secrets.get(project_key) if project_key else None) or # Project-specific keychain
|
|
73
|
+
secrets.get("jira_api_token") or # Standard: plugin_fieldname
|
|
74
|
+
secrets.get("JIRA_API_TOKEN") or # Environment variable
|
|
75
|
+
secrets.get(f"{validated_config.email}_jira_api_token") # Email-specific
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not api_token:
|
|
79
|
+
raise JiraConfigurationError(
|
|
80
|
+
"JIRA API token not found in secrets. "
|
|
81
|
+
"Please configure the JIRA plugin to set the API token."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Initialize client with validated configuration
|
|
85
|
+
self._client = JiraClient(
|
|
86
|
+
base_url=validated_config.base_url,
|
|
87
|
+
email=validated_config.email,
|
|
88
|
+
api_token=api_token,
|
|
89
|
+
project_key=validated_config.default_project,
|
|
90
|
+
timeout=validated_config.timeout,
|
|
91
|
+
enable_cache=validated_config.enable_cache,
|
|
92
|
+
cache_ttl=validated_config.cache_ttl
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Store token source info for diagnostics (without exposing token value)
|
|
96
|
+
self._token_source = self._identify_token_source(
|
|
97
|
+
secrets, project_name, validated_config.email, api_token
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def _identify_token_source(
|
|
101
|
+
self, secrets: SecretManager, project_name: Optional[str],
|
|
102
|
+
email: str, token: str
|
|
103
|
+
) -> dict:
|
|
104
|
+
"""
|
|
105
|
+
Identify which source the token came from for diagnostics.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Dict with source info (name, type, details)
|
|
109
|
+
"""
|
|
110
|
+
project_key = f"{project_name}_jira_api_token" if project_name else None
|
|
111
|
+
|
|
112
|
+
if project_key and secrets.get(project_key) == token:
|
|
113
|
+
return {
|
|
114
|
+
"name": project_key,
|
|
115
|
+
"type": "project-specific",
|
|
116
|
+
"details": f"Token for project '{project_name}'"
|
|
117
|
+
}
|
|
118
|
+
elif secrets.get("jira_api_token") == token:
|
|
119
|
+
return {
|
|
120
|
+
"name": "jira_api_token",
|
|
121
|
+
"type": "global",
|
|
122
|
+
"details": "Global JIRA token (recommended)"
|
|
123
|
+
}
|
|
124
|
+
elif secrets.get("JIRA_API_TOKEN") == token:
|
|
125
|
+
return {
|
|
126
|
+
"name": "JIRA_API_TOKEN",
|
|
127
|
+
"type": "environment",
|
|
128
|
+
"details": "Environment variable"
|
|
129
|
+
}
|
|
130
|
+
elif secrets.get(f"{email}_jira_api_token") == token:
|
|
131
|
+
return {
|
|
132
|
+
"name": f"{email}_jira_api_token",
|
|
133
|
+
"type": "email-specific",
|
|
134
|
+
"details": f"Token for email '{email}'"
|
|
135
|
+
}
|
|
136
|
+
else:
|
|
137
|
+
return {
|
|
138
|
+
"name": "unknown",
|
|
139
|
+
"type": "unknown",
|
|
140
|
+
"details": "Token source could not be identified"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def has_default_project(self) -> bool:
|
|
145
|
+
"""Check if a default project is configured."""
|
|
146
|
+
return hasattr(self, '_client') and self._client.project_key is not None
|
|
147
|
+
|
|
148
|
+
def validate_token(self) -> TokenValidationResult:
|
|
149
|
+
"""
|
|
150
|
+
Validate that the current token works by making a test API call.
|
|
151
|
+
|
|
152
|
+
Also checks configuration completeness and returns warnings.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
TokenValidationResult with validation results
|
|
156
|
+
"""
|
|
157
|
+
warnings = []
|
|
158
|
+
|
|
159
|
+
# Check if default project is configured
|
|
160
|
+
if not self.has_default_project:
|
|
161
|
+
warnings.append(
|
|
162
|
+
"No default_project configured. "
|
|
163
|
+
"Some operations (like create_subtask) will fail without a project."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if not self.is_available():
|
|
167
|
+
return {
|
|
168
|
+
"valid": False,
|
|
169
|
+
"error": "JIRA client not initialized",
|
|
170
|
+
"user": None,
|
|
171
|
+
"email": None,
|
|
172
|
+
"token_source": getattr(self, '_token_source', {}),
|
|
173
|
+
"warnings": warnings
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
# Test token with /rest/api/2/myself endpoint
|
|
178
|
+
myself = self._client.get_current_user()
|
|
179
|
+
return {
|
|
180
|
+
"valid": True,
|
|
181
|
+
"error": None,
|
|
182
|
+
"user": myself.get("displayName", "Unknown"),
|
|
183
|
+
"email": myself.get("emailAddress", "Unknown"),
|
|
184
|
+
"token_source": getattr(self, '_token_source', {}),
|
|
185
|
+
"warnings": warnings
|
|
186
|
+
}
|
|
187
|
+
except Exception as e:
|
|
188
|
+
return {
|
|
189
|
+
"valid": False,
|
|
190
|
+
"error": str(e),
|
|
191
|
+
"user": None,
|
|
192
|
+
"email": None,
|
|
193
|
+
"token_source": getattr(self, '_token_source', {}),
|
|
194
|
+
"warnings": warnings
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
def _get_plugin_config(self, config: TitanConfig) -> dict:
|
|
198
|
+
"""
|
|
199
|
+
Extract plugin-specific configuration.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
config: TitanConfig instance
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Plugin config dict (empty if not configured)
|
|
206
|
+
"""
|
|
207
|
+
if "jira" not in config.config.plugins:
|
|
208
|
+
return {}
|
|
209
|
+
|
|
210
|
+
plugin_entry = config.config.plugins["jira"]
|
|
211
|
+
return plugin_entry.config if hasattr(plugin_entry, 'config') else {}
|
|
212
|
+
|
|
213
|
+
def get_config_schema(self) -> dict:
|
|
214
|
+
"""
|
|
215
|
+
Return JSON schema for plugin configuration.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
JSON schema dict with api_token marked as required (even though it's stored in secrets)
|
|
219
|
+
"""
|
|
220
|
+
schema = JiraPluginConfig.model_json_schema()
|
|
221
|
+
# Ensure api_token is in required list for interactive configuration
|
|
222
|
+
# (even though it's Optional in the model since it's stored in secrets)
|
|
223
|
+
if "api_token" not in schema.get("required", []):
|
|
224
|
+
schema.setdefault("required", []).append("api_token")
|
|
225
|
+
return schema
|
|
226
|
+
|
|
227
|
+
def is_available(self) -> bool:
|
|
228
|
+
"""
|
|
229
|
+
Checks if the JIRA client is initialized and ready.
|
|
230
|
+
"""
|
|
231
|
+
return hasattr(self, '_client') and self._client is not None
|
|
232
|
+
|
|
233
|
+
def get_client(self) -> JiraClient:
|
|
234
|
+
"""
|
|
235
|
+
Returns the initialized JiraClient instance.
|
|
236
|
+
"""
|
|
237
|
+
if not hasattr(self, '_client') or self._client is None:
|
|
238
|
+
raise JiraClientError(msg.Plugin.JIRA_CLIENT_NOT_AVAILABLE)
|
|
239
|
+
return self._client
|
|
240
|
+
|
|
241
|
+
def get_steps(self) -> dict:
|
|
242
|
+
"""
|
|
243
|
+
Returns a dictionary of available workflow steps.
|
|
244
|
+
"""
|
|
245
|
+
from .steps.search_saved_query_step import search_saved_query_step
|
|
246
|
+
from .steps.prompt_select_issue_step import prompt_select_issue_step
|
|
247
|
+
from .steps.get_issue_step import get_issue_step
|
|
248
|
+
from .steps.ai_analyze_issue_step import ai_analyze_issue_requirements_step
|
|
249
|
+
return {
|
|
250
|
+
"search_saved_query": search_saved_query_step,
|
|
251
|
+
"prompt_select_issue": prompt_select_issue_step,
|
|
252
|
+
"get_issue": get_issue_step,
|
|
253
|
+
"ai_analyze_issue_requirements": ai_analyze_issue_requirements_step,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def workflows_path(self) -> Optional[Path]:
|
|
258
|
+
"""
|
|
259
|
+
Returns the path to the workflows directory.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Path to workflows directory containing YAML workflow definitions
|
|
263
|
+
"""
|
|
264
|
+
return Path(__file__).parent / "workflows"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI-powered JIRA issue analysis step
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error, Skip
|
|
6
|
+
from titan_cli.ui.tui.widgets import Panel
|
|
7
|
+
from ..messages import msg
|
|
8
|
+
from ..agents import JiraAgent
|
|
9
|
+
from ..formatters import IssueAnalysisMarkdownFormatter
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def ai_analyze_issue_requirements_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
13
|
+
"""
|
|
14
|
+
Analyze JIRA issue requirements using AI.
|
|
15
|
+
|
|
16
|
+
Provides intelligent analysis of:
|
|
17
|
+
- Summary and description breakdown
|
|
18
|
+
- Acceptance criteria extraction
|
|
19
|
+
- Technical requirements identification
|
|
20
|
+
- Potential challenges and risks
|
|
21
|
+
- Implementation suggestions
|
|
22
|
+
- Missing information detection
|
|
23
|
+
|
|
24
|
+
Inputs (from ctx.data):
|
|
25
|
+
jira_issue (JiraTicket): JIRA issue object to analyze
|
|
26
|
+
selected_issue (JiraTicket, optional): Alternative source
|
|
27
|
+
|
|
28
|
+
Outputs (saved to ctx.data):
|
|
29
|
+
ai_analysis (str): AI-generated analysis
|
|
30
|
+
analysis_sections (dict): Structured analysis breakdown
|
|
31
|
+
"""
|
|
32
|
+
if not ctx.textual:
|
|
33
|
+
return Error("Textual UI context is not available for this step.")
|
|
34
|
+
|
|
35
|
+
# Check if AI is available
|
|
36
|
+
if not ctx.ai or not ctx.ai.is_available():
|
|
37
|
+
ctx.textual.mount(Panel(msg.Steps.AIIssue.AI_NOT_CONFIGURED_SKIP, panel_type="info"))
|
|
38
|
+
return Skip(msg.Steps.AIIssue.AI_NOT_CONFIGURED)
|
|
39
|
+
|
|
40
|
+
# Get issue to analyze
|
|
41
|
+
issue = ctx.get("jira_issue") or ctx.get("selected_issue")
|
|
42
|
+
if not issue:
|
|
43
|
+
ctx.textual.mount(Panel(msg.Steps.AIIssue.NO_ISSUE_FOUND, panel_type="error"))
|
|
44
|
+
return Error(msg.Steps.AIIssue.NO_ISSUE_FOUND)
|
|
45
|
+
|
|
46
|
+
# Create JiraAgent instance and analyze issue with loading indicator
|
|
47
|
+
with ctx.textual.loading(msg.Steps.AIIssue.ANALYZING):
|
|
48
|
+
jira_agent = JiraAgent(ctx.ai, ctx.jira)
|
|
49
|
+
analysis = jira_agent.analyze_issue(
|
|
50
|
+
issue_key=issue.key,
|
|
51
|
+
include_subtasks=True,
|
|
52
|
+
include_comments=False,
|
|
53
|
+
include_linked_issues=False
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Build formatted analysis for display
|
|
57
|
+
# Use template from config if specified, otherwise use built-in formatter
|
|
58
|
+
template_name = jira_agent.config.template if jira_agent.config.template else None
|
|
59
|
+
formatter = IssueAnalysisMarkdownFormatter(template_path=template_name)
|
|
60
|
+
ai_analysis = formatter.format(analysis)
|
|
61
|
+
|
|
62
|
+
# Display analysis
|
|
63
|
+
ctx.textual.text("")
|
|
64
|
+
ctx.textual.text("AI Analysis Results", markup="bold cyan")
|
|
65
|
+
ctx.textual.text("")
|
|
66
|
+
|
|
67
|
+
# Show issue header
|
|
68
|
+
ctx.textual.text(f"{issue.key}: {issue.summary}", markup="bold")
|
|
69
|
+
ctx.textual.text(f"Type: {issue.issue_type} | Status: {issue.status} | Priority: {issue.priority}", markup="dim")
|
|
70
|
+
ctx.textual.text("")
|
|
71
|
+
|
|
72
|
+
# Show AI analysis as markdown
|
|
73
|
+
ctx.textual.markdown(ai_analysis)
|
|
74
|
+
|
|
75
|
+
# Show token usage
|
|
76
|
+
if analysis.total_tokens_used > 0:
|
|
77
|
+
ctx.textual.text(f"Tokens used: {analysis.total_tokens_used}", markup="dim")
|
|
78
|
+
|
|
79
|
+
# Save structured analysis to context
|
|
80
|
+
ctx.set("ai_analysis_structured", {
|
|
81
|
+
"functional_requirements": analysis.functional_requirements,
|
|
82
|
+
"non_functional_requirements": analysis.non_functional_requirements,
|
|
83
|
+
"acceptance_criteria": analysis.acceptance_criteria,
|
|
84
|
+
"technical_approach": analysis.technical_approach,
|
|
85
|
+
"dependencies": analysis.dependencies,
|
|
86
|
+
"risks": analysis.risks,
|
|
87
|
+
"edge_cases": analysis.edge_cases,
|
|
88
|
+
"suggested_subtasks": analysis.suggested_subtasks,
|
|
89
|
+
"complexity_score": analysis.complexity_score,
|
|
90
|
+
"estimated_effort": analysis.estimated_effort
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return Success(
|
|
94
|
+
"AI analysis completed",
|
|
95
|
+
metadata={
|
|
96
|
+
"ai_analysis": ai_analysis,
|
|
97
|
+
"analyzed_issue_key": issue.key,
|
|
98
|
+
"tokens_used": analysis.total_tokens_used,
|
|
99
|
+
"complexity": analysis.complexity_score,
|
|
100
|
+
"effort": analysis.estimated_effort
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = ["ai_analyze_issue_requirements_step"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Get JIRA issue details step
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
+
from titan_cli.ui.tui.widgets import Panel
|
|
7
|
+
from ..exceptions import JiraAPIError
|
|
8
|
+
from ..messages import msg
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
12
|
+
"""
|
|
13
|
+
Get JIRA issue details by key.
|
|
14
|
+
|
|
15
|
+
Inputs (from ctx.data):
|
|
16
|
+
jira_issue_key (str): JIRA issue key (e.g., "PROJ-123")
|
|
17
|
+
expand (list[str], optional): Additional fields to expand
|
|
18
|
+
|
|
19
|
+
Outputs (saved to ctx.data):
|
|
20
|
+
jira_issue (JiraTicket): Issue details
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Success: Issue retrieved
|
|
24
|
+
Error: Failed to get issue
|
|
25
|
+
"""
|
|
26
|
+
if not ctx.textual:
|
|
27
|
+
return Error("Textual UI context is not available for this step.")
|
|
28
|
+
|
|
29
|
+
# Check if JIRA client is available
|
|
30
|
+
if not ctx.jira:
|
|
31
|
+
ctx.textual.mount(Panel(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT, panel_type="error"))
|
|
32
|
+
return Error(msg.Plugin.CLIENT_NOT_AVAILABLE_IN_CONTEXT)
|
|
33
|
+
|
|
34
|
+
# Get issue key
|
|
35
|
+
issue_key = ctx.get("jira_issue_key")
|
|
36
|
+
if not issue_key:
|
|
37
|
+
ctx.textual.mount(Panel("JIRA issue key is required", panel_type="error"))
|
|
38
|
+
return Error("JIRA issue key is required")
|
|
39
|
+
|
|
40
|
+
# Get optional expand fields
|
|
41
|
+
expand = ctx.get("expand")
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Get issue with loading indicator
|
|
45
|
+
with ctx.textual.loading(msg.Steps.GetIssue.GETTING_ISSUE.format(issue_key=issue_key)):
|
|
46
|
+
issue = ctx.jira.get_ticket(ticket_key=issue_key, expand=expand)
|
|
47
|
+
|
|
48
|
+
# Show success
|
|
49
|
+
ctx.textual.mount(
|
|
50
|
+
Panel(
|
|
51
|
+
text=msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key),
|
|
52
|
+
panel_type="success"
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Show issue details
|
|
57
|
+
ctx.textual.text(f" Title: {issue.summary}", markup="cyan")
|
|
58
|
+
ctx.textual.text(f" Status: {issue.status}")
|
|
59
|
+
ctx.textual.text(f" Type: {issue.issue_type}")
|
|
60
|
+
ctx.textual.text(f" Assignee: {issue.assignee or 'Unassigned'}")
|
|
61
|
+
ctx.textual.text("")
|
|
62
|
+
|
|
63
|
+
return Success(
|
|
64
|
+
msg.Steps.GetIssue.GET_SUCCESS.format(issue_key=issue_key),
|
|
65
|
+
metadata={"jira_issue": issue}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
except JiraAPIError as e:
|
|
69
|
+
if e.status_code == 404:
|
|
70
|
+
error_msg = msg.Steps.GetIssue.ISSUE_NOT_FOUND.format(issue_key=issue_key)
|
|
71
|
+
ctx.textual.mount(Panel(error_msg, panel_type="error"))
|
|
72
|
+
return Error(error_msg)
|
|
73
|
+
error_msg = msg.Steps.GetIssue.GET_FAILED.format(e=e)
|
|
74
|
+
ctx.textual.mount(Panel(error_msg, panel_type="error"))
|
|
75
|
+
return Error(error_msg)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
error_msg = f"Unexpected error getting issue: {e}"
|
|
78
|
+
ctx.textual.mount(Panel(error_msg, panel_type="error"))
|
|
79
|
+
return Error(error_msg)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = ["get_issue_step"]
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt user to select an issue from search results
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from titan_cli.engine import WorkflowContext, WorkflowResult, Success, Error
|
|
6
|
+
from titan_cli.ui.tui.widgets import Panel
|
|
7
|
+
from ..messages import msg
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def prompt_select_issue_step(ctx: WorkflowContext) -> WorkflowResult:
|
|
11
|
+
"""
|
|
12
|
+
Prompt user to select a JIRA issue from search results.
|
|
13
|
+
|
|
14
|
+
Inputs (from ctx.data):
|
|
15
|
+
jira_issues (List[JiraTicket]): List of issues from search
|
|
16
|
+
|
|
17
|
+
Outputs (saved to ctx.data):
|
|
18
|
+
jira_issue_key (str): Selected issue key
|
|
19
|
+
selected_issue (JiraTicket): Selected issue object
|
|
20
|
+
"""
|
|
21
|
+
if not ctx.textual:
|
|
22
|
+
return Error("Textual UI context is not available for this step.")
|
|
23
|
+
|
|
24
|
+
# Get issues from previous search
|
|
25
|
+
issues = ctx.get("jira_issues")
|
|
26
|
+
if not issues:
|
|
27
|
+
return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
|
|
28
|
+
|
|
29
|
+
if len(issues) == 0:
|
|
30
|
+
return Error(msg.Steps.PromptSelectIssue.NO_ISSUES_AVAILABLE)
|
|
31
|
+
|
|
32
|
+
# Prompt user to select issue (issues already displayed in table from previous step)
|
|
33
|
+
ctx.textual.text("")
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Ask for selection using text input and validate
|
|
37
|
+
response = ctx.textual.ask_text(
|
|
38
|
+
msg.Steps.PromptSelectIssue.ASK_ISSUE_NUMBER,
|
|
39
|
+
default=""
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if not response or not response.strip():
|
|
43
|
+
return Error(msg.Steps.PromptSelectIssue.NO_ISSUE_SELECTED)
|
|
44
|
+
|
|
45
|
+
# Validate it's a number
|
|
46
|
+
try:
|
|
47
|
+
selected_index = int(response.strip())
|
|
48
|
+
except ValueError:
|
|
49
|
+
return Error(f"Invalid input: '{response}' is not a number")
|
|
50
|
+
|
|
51
|
+
# Validate it's in range
|
|
52
|
+
if selected_index < 1 or selected_index > len(issues):
|
|
53
|
+
return Error(f"Invalid selection: must be between 1 and {len(issues)}")
|
|
54
|
+
|
|
55
|
+
# Convert to 0-based index
|
|
56
|
+
selected_issue = issues[selected_index - 1]
|
|
57
|
+
|
|
58
|
+
ctx.textual.text("")
|
|
59
|
+
ctx.textual.mount(
|
|
60
|
+
Panel(
|
|
61
|
+
text=msg.Steps.PromptSelectIssue.ISSUE_SELECTION_CONFIRM.format(
|
|
62
|
+
key=selected_issue.key,
|
|
63
|
+
summary=selected_issue.summary
|
|
64
|
+
),
|
|
65
|
+
panel_type="success"
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return Success(
|
|
70
|
+
msg.Steps.PromptSelectIssue.SELECT_SUCCESS.format(key=selected_issue.key),
|
|
71
|
+
metadata={
|
|
72
|
+
"jira_issue_key": selected_issue.key,
|
|
73
|
+
"selected_issue": selected_issue
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
except (KeyboardInterrupt, EOFError):
|
|
77
|
+
return Error("User cancelled issue selection")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
__all__ = ["prompt_select_issue_step"]
|