@techwavedev/agi-agent-kit 1.1.7 → 1.2.1
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 @techwavedev/agi-agent-kit might be problematic. Click here for more details.
- package/CHANGELOG.md +82 -1
- package/README.md +190 -12
- package/bin/init.js +30 -2
- package/package.json +6 -3
- package/templates/base/AGENTS.md +54 -23
- package/templates/base/README.md +325 -0
- package/templates/base/directives/memory_integration.md +95 -0
- package/templates/base/execution/memory_manager.py +309 -0
- package/templates/base/execution/session_boot.py +218 -0
- package/templates/base/execution/session_init.py +320 -0
- package/templates/base/skill-creator/SKILL_skillcreator.md +23 -36
- package/templates/base/skill-creator/scripts/init_skill.py +18 -135
- package/templates/skills/ec/README.md +31 -0
- package/templates/skills/ec/aws/SKILL.md +1020 -0
- package/templates/skills/ec/aws/defaults.yaml +13 -0
- package/templates/skills/ec/aws/references/common_patterns.md +80 -0
- package/templates/skills/ec/aws/references/mcp_servers.md +98 -0
- package/templates/skills/ec/aws-terraform/SKILL.md +349 -0
- package/templates/skills/ec/aws-terraform/references/best_practices.md +394 -0
- package/templates/skills/ec/aws-terraform/references/checkov_reference.md +337 -0
- package/templates/skills/ec/aws-terraform/scripts/configure_mcp.py +150 -0
- package/templates/skills/ec/confluent-kafka/SKILL.md +655 -0
- package/templates/skills/ec/confluent-kafka/references/ansible_playbooks.md +792 -0
- package/templates/skills/ec/confluent-kafka/references/ec_deployment.md +579 -0
- package/templates/skills/ec/confluent-kafka/references/kraft_migration.md +490 -0
- package/templates/skills/ec/confluent-kafka/references/troubleshooting.md +778 -0
- package/templates/skills/ec/confluent-kafka/references/upgrade_7x_to_8x.md +488 -0
- package/templates/skills/ec/confluent-kafka/scripts/kafka_health_check.py +435 -0
- package/templates/skills/ec/confluent-kafka/scripts/upgrade_preflight.py +568 -0
- package/templates/skills/ec/confluent-kafka/scripts/validate_config.py +455 -0
- package/templates/skills/ec/consul/SKILL.md +427 -0
- package/templates/skills/ec/consul/references/acl_setup.md +168 -0
- package/templates/skills/ec/consul/references/ha_config.md +196 -0
- package/templates/skills/ec/consul/references/troubleshooting.md +267 -0
- package/templates/skills/ec/consul/references/upgrades.md +213 -0
- package/templates/skills/ec/consul/scripts/consul_health_report.py +530 -0
- package/templates/skills/ec/consul/scripts/consul_status.py +264 -0
- package/templates/skills/ec/consul/scripts/generate_values.py +170 -0
- package/templates/skills/ec/documentation/SKILL.md +351 -0
- package/templates/skills/ec/documentation/references/best_practices.md +201 -0
- package/templates/skills/ec/documentation/scripts/analyze_code.py +307 -0
- package/templates/skills/ec/documentation/scripts/detect_changes.py +460 -0
- package/templates/skills/ec/documentation/scripts/generate_changelog.py +312 -0
- package/templates/skills/ec/documentation/scripts/sync_docs.py +272 -0
- package/templates/skills/ec/documentation/scripts/update_skill_docs.py +366 -0
- package/templates/skills/ec/gitlab/SKILL.md +529 -0
- package/templates/skills/ec/gitlab/references/agent_installation.md +416 -0
- package/templates/skills/ec/gitlab/references/api_reference.md +508 -0
- package/templates/skills/ec/gitlab/references/gitops_flux.md +465 -0
- package/templates/skills/ec/gitlab/references/troubleshooting.md +518 -0
- package/templates/skills/ec/gitlab/scripts/generate_agent_values.py +329 -0
- package/templates/skills/ec/gitlab/scripts/gitlab_agent_status.py +414 -0
- package/templates/skills/ec/jira/SKILL.md +484 -0
- package/templates/skills/ec/jira/references/jql_reference.md +148 -0
- package/templates/skills/ec/jira/scripts/add_comment.py +91 -0
- package/templates/skills/ec/jira/scripts/bulk_log_work.py +124 -0
- package/templates/skills/ec/jira/scripts/create_ticket.py +162 -0
- package/templates/skills/ec/jira/scripts/get_ticket.py +191 -0
- package/templates/skills/ec/jira/scripts/jira_client.py +383 -0
- package/templates/skills/ec/jira/scripts/log_work.py +154 -0
- package/templates/skills/ec/jira/scripts/search_tickets.py +104 -0
- package/templates/skills/ec/jira/scripts/update_comment.py +67 -0
- package/templates/skills/ec/jira/scripts/update_ticket.py +161 -0
- package/templates/skills/ec/karpenter/SKILL.md +301 -0
- package/templates/skills/ec/karpenter/references/ec2nodeclasses.md +421 -0
- package/templates/skills/ec/karpenter/references/migration.md +396 -0
- package/templates/skills/ec/karpenter/references/nodepools.md +400 -0
- package/templates/skills/ec/karpenter/references/troubleshooting.md +359 -0
- package/templates/skills/ec/karpenter/scripts/generate_ec2nodeclass.py +187 -0
- package/templates/skills/ec/karpenter/scripts/generate_nodepool.py +245 -0
- package/templates/skills/ec/karpenter/scripts/karpenter_status.py +359 -0
- package/templates/skills/ec/opensearch/SKILL.md +720 -0
- package/templates/skills/ec/opensearch/references/ml_neural_search.md +576 -0
- package/templates/skills/ec/opensearch/references/operator.md +532 -0
- package/templates/skills/ec/opensearch/references/query_dsl.md +532 -0
- package/templates/skills/ec/opensearch/scripts/configure_mcp.py +148 -0
- package/templates/skills/ec/victoriametrics/SKILL.md +598 -0
- package/templates/skills/ec/victoriametrics/references/kubernetes.md +531 -0
- package/templates/skills/ec/victoriametrics/references/prometheus_migration.md +333 -0
- package/templates/skills/ec/victoriametrics/references/troubleshooting.md +442 -0
- package/templates/skills/knowledge/SKILLS_CATALOG.md +274 -4
- package/templates/skills/knowledge/intelligent-routing/SKILL.md +237 -164
- package/templates/skills/knowledge/parallel-agents/SKILL.md +345 -73
- package/templates/skills/knowledge/plugin-discovery/SKILL.md +582 -0
- package/templates/skills/knowledge/plugin-discovery/scripts/platform_setup.py +1083 -0
- package/templates/skills/knowledge/design-md/README.md +0 -34
- package/templates/skills/knowledge/design-md/SKILL.md +0 -193
- package/templates/skills/knowledge/design-md/examples/DESIGN.md +0 -154
- package/templates/skills/knowledge/notebooklm-mcp/SKILL.md +0 -71
- package/templates/skills/knowledge/notebooklm-mcp/assets/example_asset.txt +0 -24
- package/templates/skills/knowledge/notebooklm-mcp/references/api_reference.md +0 -34
- package/templates/skills/knowledge/notebooklm-mcp/scripts/example.py +0 -19
- package/templates/skills/knowledge/react-components/README.md +0 -36
- package/templates/skills/knowledge/react-components/SKILL.md +0 -53
- package/templates/skills/knowledge/react-components/examples/gold-standard-card.tsx +0 -80
- package/templates/skills/knowledge/react-components/package-lock.json +0 -231
- package/templates/skills/knowledge/react-components/package.json +0 -16
- package/templates/skills/knowledge/react-components/resources/architecture-checklist.md +0 -15
- package/templates/skills/knowledge/react-components/resources/component-template.tsx +0 -37
- package/templates/skills/knowledge/react-components/resources/stitch-api-reference.md +0 -14
- package/templates/skills/knowledge/react-components/resources/style-guide.json +0 -27
- package/templates/skills/knowledge/react-components/scripts/fetch-stitch.sh +0 -30
- package/templates/skills/knowledge/react-components/scripts/validate.js +0 -68
- package/templates/skills/knowledge/self-update/SKILL.md +0 -60
- package/templates/skills/knowledge/self-update/scripts/update_kit.py +0 -103
- package/templates/skills/knowledge/stitch-loop/README.md +0 -54
- package/templates/skills/knowledge/stitch-loop/SKILL.md +0 -235
- package/templates/skills/knowledge/stitch-loop/examples/SITE.md +0 -73
- package/templates/skills/knowledge/stitch-loop/examples/next-prompt.md +0 -25
- package/templates/skills/knowledge/stitch-loop/resources/baton-schema.md +0 -61
- package/templates/skills/knowledge/stitch-loop/resources/site-template.md +0 -104
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Jira API Client Utility
|
|
4
|
+
|
|
5
|
+
Provides a reusable client for Jira REST API operations.
|
|
6
|
+
Handles authentication, error handling, and common operations.
|
|
7
|
+
|
|
8
|
+
This module is used by all other Jira scripts.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
from jira_client import JiraClient
|
|
12
|
+
|
|
13
|
+
client = JiraClient()
|
|
14
|
+
issue = client.get_issue("PROJ-123")
|
|
15
|
+
|
|
16
|
+
Environment Variables Required:
|
|
17
|
+
JIRA_URL - Base URL (e.g., https://company.atlassian.net)
|
|
18
|
+
JIRA_EMAIL - User email
|
|
19
|
+
JIRA_API_TOKEN - API token
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
28
|
+
from urllib.parse import urljoin
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import requests
|
|
32
|
+
except ImportError:
|
|
33
|
+
print("❌ Error: requests library required. Install with: pip install requests", file=sys.stderr)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
# Try to load dotenv if available
|
|
37
|
+
try:
|
|
38
|
+
from dotenv import load_dotenv
|
|
39
|
+
# Look for .env in common locations
|
|
40
|
+
for env_path in ['.env', '../.env', '../../.env', Path.home() / '.env']:
|
|
41
|
+
if Path(env_path).exists():
|
|
42
|
+
load_dotenv(env_path)
|
|
43
|
+
break
|
|
44
|
+
except ImportError:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class JiraClient:
|
|
49
|
+
"""
|
|
50
|
+
Jira REST API client with authentication and error handling.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __init__(self, url: str = None, email: str = None, token: str = None):
|
|
54
|
+
"""
|
|
55
|
+
Initialize the Jira client.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
url: Jira instance URL (or JIRA_URL env var)
|
|
59
|
+
email: User email (or JIRA_EMAIL env var)
|
|
60
|
+
token: API token (or JIRA_API_TOKEN env var)
|
|
61
|
+
"""
|
|
62
|
+
self.base_url = (url or os.getenv('JIRA_URL', '')).rstrip('/')
|
|
63
|
+
self.email = email or os.getenv('JIRA_EMAIL', '')
|
|
64
|
+
self.token = token or os.getenv('JIRA_API_TOKEN', '')
|
|
65
|
+
|
|
66
|
+
if not all([self.base_url, self.token]):
|
|
67
|
+
missing = []
|
|
68
|
+
if not self.base_url:
|
|
69
|
+
missing.append('JIRA_URL')
|
|
70
|
+
if not self.token:
|
|
71
|
+
missing.append('JIRA_API_TOKEN')
|
|
72
|
+
raise ValueError(f"Missing required environment variables: {', '.join(missing)}")
|
|
73
|
+
|
|
74
|
+
# Detect Jira Server vs Cloud and set appropriate API version
|
|
75
|
+
# Jira Server uses /rest/api/2, Jira Cloud uses /rest/api/3
|
|
76
|
+
self.is_server = 'atlassian.net' not in self.base_url.lower()
|
|
77
|
+
api_version = '2' if self.is_server else '3'
|
|
78
|
+
|
|
79
|
+
# Clean base URL (remove /secure/Dashboard.jspa or similar paths)
|
|
80
|
+
import re
|
|
81
|
+
self.base_url = re.sub(r'/secure/.*$', '', self.base_url)
|
|
82
|
+
self.base_url = re.sub(r'/browse/.*$', '', self.base_url)
|
|
83
|
+
self.base_url = self.base_url.rstrip('/')
|
|
84
|
+
|
|
85
|
+
self.api_url = f"{self.base_url}/rest/api/{api_version}"
|
|
86
|
+
self.session = requests.Session()
|
|
87
|
+
|
|
88
|
+
# Jira Server can use token directly as Bearer or basic auth
|
|
89
|
+
# Try Bearer token first for server, basic auth for cloud
|
|
90
|
+
if self.is_server:
|
|
91
|
+
self.session.headers.update({
|
|
92
|
+
'Authorization': f'Bearer {self.token}'
|
|
93
|
+
})
|
|
94
|
+
else:
|
|
95
|
+
if self.email:
|
|
96
|
+
self.session.auth = (self.email, self.token)
|
|
97
|
+
else:
|
|
98
|
+
self.session.headers.update({
|
|
99
|
+
'Authorization': f'Bearer {self.token}'
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
self.session.headers.update({
|
|
103
|
+
'Accept': 'application/json',
|
|
104
|
+
'Content-Type': 'application/json'
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
def _request(self, method: str, endpoint: str, **kwargs) -> Tuple[bool, Any]:
|
|
108
|
+
"""
|
|
109
|
+
Make an API request.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
(success, data_or_error)
|
|
113
|
+
"""
|
|
114
|
+
url = f"{self.api_url}/{endpoint.lstrip('/')}"
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
response = self.session.request(method, url, **kwargs)
|
|
118
|
+
|
|
119
|
+
if response.status_code == 204:
|
|
120
|
+
return True, None
|
|
121
|
+
|
|
122
|
+
if response.status_code >= 400:
|
|
123
|
+
error_data = response.json() if response.content else {}
|
|
124
|
+
error_msg = self._parse_error(error_data, response.status_code)
|
|
125
|
+
return False, error_msg
|
|
126
|
+
|
|
127
|
+
return True, response.json() if response.content else None
|
|
128
|
+
|
|
129
|
+
except requests.exceptions.RequestException as e:
|
|
130
|
+
return False, f"Request failed: {str(e)}"
|
|
131
|
+
except json.JSONDecodeError:
|
|
132
|
+
return False, f"Invalid JSON response: {response.text[:200]}"
|
|
133
|
+
|
|
134
|
+
def _parse_error(self, error_data: Dict, status_code: int) -> str:
|
|
135
|
+
"""Parse Jira error response into readable message."""
|
|
136
|
+
if 'errorMessages' in error_data and error_data['errorMessages']:
|
|
137
|
+
return '; '.join(error_data['errorMessages'])
|
|
138
|
+
if 'errors' in error_data:
|
|
139
|
+
return '; '.join(f"{k}: {v}" for k, v in error_data['errors'].items())
|
|
140
|
+
return f"HTTP {status_code} error"
|
|
141
|
+
|
|
142
|
+
# ========== Issue Operations ==========
|
|
143
|
+
|
|
144
|
+
def get_issue(self, key: str, fields: List[str] = None, expand: List[str] = None) -> Tuple[bool, Any]:
|
|
145
|
+
"""Get issue by key."""
|
|
146
|
+
params = {}
|
|
147
|
+
if fields:
|
|
148
|
+
params['fields'] = ','.join(fields)
|
|
149
|
+
if expand:
|
|
150
|
+
params['expand'] = ','.join(expand)
|
|
151
|
+
|
|
152
|
+
return self._request('GET', f'issue/{key}', params=params)
|
|
153
|
+
|
|
154
|
+
def create_issue(self, fields: Dict) -> Tuple[bool, Any]:
|
|
155
|
+
"""Create a new issue."""
|
|
156
|
+
return self._request('POST', 'issue', json={'fields': fields})
|
|
157
|
+
|
|
158
|
+
def update_issue(self, key: str, fields: Dict = None, update: Dict = None) -> Tuple[bool, Any]:
|
|
159
|
+
"""Update an existing issue."""
|
|
160
|
+
data = {}
|
|
161
|
+
if fields:
|
|
162
|
+
data['fields'] = fields
|
|
163
|
+
if update:
|
|
164
|
+
data['update'] = update
|
|
165
|
+
|
|
166
|
+
return self._request('PUT', f'issue/{key}', json=data)
|
|
167
|
+
|
|
168
|
+
def delete_issue(self, key: str) -> Tuple[bool, Any]:
|
|
169
|
+
"""Delete an issue."""
|
|
170
|
+
return self._request('DELETE', f'issue/{key}')
|
|
171
|
+
|
|
172
|
+
def search_issues(self, jql: str, fields: List[str] = None,
|
|
173
|
+
max_results: int = 50, start_at: int = 0) -> Tuple[bool, Any]:
|
|
174
|
+
"""Search issues using JQL."""
|
|
175
|
+
data = {
|
|
176
|
+
'jql': jql,
|
|
177
|
+
'maxResults': max_results,
|
|
178
|
+
'startAt': start_at
|
|
179
|
+
}
|
|
180
|
+
if fields:
|
|
181
|
+
data['fields'] = fields
|
|
182
|
+
|
|
183
|
+
return self._request('POST', 'search', json=data)
|
|
184
|
+
|
|
185
|
+
# ========== Transitions ==========
|
|
186
|
+
|
|
187
|
+
def get_transitions(self, key: str) -> Tuple[bool, Any]:
|
|
188
|
+
"""Get available transitions for an issue."""
|
|
189
|
+
return self._request('GET', f'issue/{key}/transitions')
|
|
190
|
+
|
|
191
|
+
def transition_issue(self, key: str, transition_id: str,
|
|
192
|
+
fields: Dict = None, comment: str = None) -> Tuple[bool, Any]:
|
|
193
|
+
"""Transition an issue to a new status."""
|
|
194
|
+
data = {
|
|
195
|
+
'transition': {'id': transition_id}
|
|
196
|
+
}
|
|
197
|
+
if fields:
|
|
198
|
+
data['fields'] = fields
|
|
199
|
+
if comment:
|
|
200
|
+
data['update'] = {
|
|
201
|
+
'comment': [{
|
|
202
|
+
'add': {'body': self._format_body(comment)}
|
|
203
|
+
}]
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return self._request('POST', f'issue/{key}/transitions', json=data)
|
|
207
|
+
|
|
208
|
+
def find_transition_by_name(self, key: str, status_name: str) -> Optional[str]:
|
|
209
|
+
"""Find transition ID by target status name."""
|
|
210
|
+
success, transitions = self.get_transitions(key)
|
|
211
|
+
if not success:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
status_lower = status_name.lower()
|
|
215
|
+
for t in transitions.get('transitions', []):
|
|
216
|
+
if t.get('to', {}).get('name', '').lower() == status_lower:
|
|
217
|
+
return t['id']
|
|
218
|
+
if t.get('name', '').lower() == status_lower:
|
|
219
|
+
return t['id']
|
|
220
|
+
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
# ========== Comments ==========
|
|
224
|
+
|
|
225
|
+
def get_comments(self, key: str) -> Tuple[bool, Any]:
|
|
226
|
+
"""Get all comments for an issue."""
|
|
227
|
+
return self._request('GET', f'issue/{key}/comment')
|
|
228
|
+
|
|
229
|
+
def add_comment(self, key: str, body: str, visibility: Dict = None) -> Tuple[bool, Any]:
|
|
230
|
+
"""Add a comment to an issue."""
|
|
231
|
+
data = {
|
|
232
|
+
'body': self._format_body(body)
|
|
233
|
+
}
|
|
234
|
+
if visibility:
|
|
235
|
+
data['visibility'] = visibility
|
|
236
|
+
|
|
237
|
+
return self._request('POST', f'issue/{key}/comment', json=data)
|
|
238
|
+
|
|
239
|
+
def update_comment(self, key: str, comment_id: str, body: str) -> Tuple[bool, Any]:
|
|
240
|
+
"""Update an existing comment."""
|
|
241
|
+
data = {
|
|
242
|
+
'body': self._format_body(body)
|
|
243
|
+
}
|
|
244
|
+
return self._request('PUT', f'issue/{key}/comment/{comment_id}', json=data)
|
|
245
|
+
|
|
246
|
+
def delete_comment(self, key: str, comment_id: str) -> Tuple[bool, Any]:
|
|
247
|
+
"""Delete a comment."""
|
|
248
|
+
return self._request('DELETE', f'issue/{key}/comment/{comment_id}')
|
|
249
|
+
|
|
250
|
+
# ========== Worklog ==========
|
|
251
|
+
|
|
252
|
+
def get_worklogs(self, key: str) -> Tuple[bool, Any]:
|
|
253
|
+
"""Get all worklogs for an issue."""
|
|
254
|
+
return self._request('GET', f'issue/{key}/worklog')
|
|
255
|
+
|
|
256
|
+
def add_worklog(self, key: str, time_spent: str,
|
|
257
|
+
comment: str = None, started: str = None,
|
|
258
|
+
adjust_estimate: str = None, new_estimate: str = None) -> Tuple[bool, Any]:
|
|
259
|
+
"""
|
|
260
|
+
Add a worklog entry.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
key: Issue key
|
|
264
|
+
time_spent: Time in Jira format (e.g., "2h", "1d", "30m")
|
|
265
|
+
comment: Work description
|
|
266
|
+
started: Start datetime in ISO format
|
|
267
|
+
adjust_estimate: How to adjust estimate (new, manual, leave, auto)
|
|
268
|
+
new_estimate: New estimate if adjust_estimate is 'new'
|
|
269
|
+
"""
|
|
270
|
+
data = {
|
|
271
|
+
'timeSpent': time_spent
|
|
272
|
+
}
|
|
273
|
+
if comment:
|
|
274
|
+
data['comment'] = self._format_body(comment)
|
|
275
|
+
if started:
|
|
276
|
+
data['started'] = started
|
|
277
|
+
|
|
278
|
+
params = {}
|
|
279
|
+
if adjust_estimate:
|
|
280
|
+
params['adjustEstimate'] = adjust_estimate
|
|
281
|
+
if new_estimate:
|
|
282
|
+
params['newEstimate'] = new_estimate
|
|
283
|
+
|
|
284
|
+
return self._request('POST', f'issue/{key}/worklog', json=data, params=params)
|
|
285
|
+
|
|
286
|
+
# ========== Projects ==========
|
|
287
|
+
|
|
288
|
+
def get_projects(self) -> Tuple[bool, Any]:
|
|
289
|
+
"""Get all accessible projects."""
|
|
290
|
+
return self._request('GET', 'project')
|
|
291
|
+
|
|
292
|
+
def get_project(self, key: str) -> Tuple[bool, Any]:
|
|
293
|
+
"""Get project details."""
|
|
294
|
+
return self._request('GET', f'project/{key}')
|
|
295
|
+
|
|
296
|
+
# ========== Users ==========
|
|
297
|
+
|
|
298
|
+
def search_users(self, query: str, max_results: int = 50) -> Tuple[bool, Any]:
|
|
299
|
+
"""Search for users."""
|
|
300
|
+
return self._request('GET', 'user/search', params={
|
|
301
|
+
'query': query,
|
|
302
|
+
'maxResults': max_results
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
def get_myself(self) -> Tuple[bool, Any]:
|
|
306
|
+
"""Get current user info."""
|
|
307
|
+
return self._request('GET', 'myself')
|
|
308
|
+
|
|
309
|
+
# ========== Helpers ==========
|
|
310
|
+
|
|
311
|
+
def _format_body(self, text: str) -> Any:
|
|
312
|
+
"""
|
|
313
|
+
Format text for the appropriate Jira version.
|
|
314
|
+
Jira Server (v2) expects plain string.
|
|
315
|
+
Jira Cloud (v3) expects ADF (Atlassian Document Format).
|
|
316
|
+
"""
|
|
317
|
+
if self.is_server:
|
|
318
|
+
return text
|
|
319
|
+
|
|
320
|
+
# Atlassian Document Format (ADF) for Jira Cloud
|
|
321
|
+
paragraphs = []
|
|
322
|
+
for para in text.split('\n\n'):
|
|
323
|
+
if para.strip():
|
|
324
|
+
paragraphs.append({
|
|
325
|
+
'type': 'paragraph',
|
|
326
|
+
'content': [{'type': 'text', 'text': para.strip()}]
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
'type': 'doc',
|
|
331
|
+
'version': 1,
|
|
332
|
+
'content': paragraphs if paragraphs else [
|
|
333
|
+
{'type': 'paragraph', 'content': [{'type': 'text', 'text': text}]}
|
|
334
|
+
]
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
def parse_time_to_seconds(self, time_str: str) -> int:
|
|
338
|
+
"""
|
|
339
|
+
Parse Jira time format to seconds.
|
|
340
|
+
|
|
341
|
+
Supports: 1w, 2d, 3h, 30m (and combinations like "2h 30m")
|
|
342
|
+
"""
|
|
343
|
+
total = 0
|
|
344
|
+
patterns = {
|
|
345
|
+
'w': 5 * 8 * 60 * 60, # 1 week = 5 days
|
|
346
|
+
'd': 8 * 60 * 60, # 1 day = 8 hours
|
|
347
|
+
'h': 60 * 60,
|
|
348
|
+
'm': 60,
|
|
349
|
+
's': 1
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for match in re.finditer(r'(\d+)\s*([wdhms])', time_str.lower()):
|
|
353
|
+
value, unit = int(match.group(1)), match.group(2)
|
|
354
|
+
total += value * patterns.get(unit, 0)
|
|
355
|
+
|
|
356
|
+
return total
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def get_client() -> JiraClient:
|
|
360
|
+
"""Get a configured Jira client instance."""
|
|
361
|
+
try:
|
|
362
|
+
return JiraClient()
|
|
363
|
+
except ValueError as e:
|
|
364
|
+
print(f"❌ Configuration Error: {e}", file=sys.stderr)
|
|
365
|
+
print("\nEnsure these environment variables are set:", file=sys.stderr)
|
|
366
|
+
print(" JIRA_URL=https://your-domain.atlassian.net", file=sys.stderr)
|
|
367
|
+
print(" JIRA_EMAIL=your-email@example.com", file=sys.stderr)
|
|
368
|
+
print(" JIRA_API_TOKEN=your-api-token", file=sys.stderr)
|
|
369
|
+
sys.exit(1)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if __name__ == '__main__':
|
|
373
|
+
# Test connection when run directly
|
|
374
|
+
print("Testing Jira connection...")
|
|
375
|
+
client = get_client()
|
|
376
|
+
|
|
377
|
+
success, result = client.get_myself()
|
|
378
|
+
if success:
|
|
379
|
+
print(f"✅ Connected as: {result.get('displayName', 'Unknown')}")
|
|
380
|
+
print(f" Email: {result.get('emailAddress', 'N/A')}")
|
|
381
|
+
else:
|
|
382
|
+
print(f"❌ Connection failed: {result}")
|
|
383
|
+
sys.exit(1)
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Log Work Time to Jira Ticket
|
|
4
|
+
|
|
5
|
+
Logs work time against an existing Jira issue.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python log_work.py --ticket <key> --time <duration> [options]
|
|
9
|
+
|
|
10
|
+
Arguments:
|
|
11
|
+
--ticket Ticket key (required)
|
|
12
|
+
--time Time spent: "2h", "30m", "1d", "2h 30m" (required)
|
|
13
|
+
--comment Work description (optional)
|
|
14
|
+
--started Start time in ISO format (optional)
|
|
15
|
+
--remaining New remaining estimate (optional)
|
|
16
|
+
|
|
17
|
+
Exit Codes:
|
|
18
|
+
0 - Success
|
|
19
|
+
1 - Invalid arguments
|
|
20
|
+
2 - Ticket not found
|
|
21
|
+
3 - Worklog error
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
32
|
+
from jira_client import get_client
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def round_to_15min_blocks(time_str: str) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Round time to 15-minute blocks (EC Jira requirement).
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
time_str: Time string like "2h", "25m", "1h 20m"
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Rounded time string using 15m/30m/45m/1h increments
|
|
44
|
+
"""
|
|
45
|
+
# Parse time to total minutes
|
|
46
|
+
total_minutes = 0
|
|
47
|
+
patterns = {
|
|
48
|
+
'w': 5 * 8 * 60, # 1 week = 5 days = 40 hours
|
|
49
|
+
'd': 8 * 60, # 1 day = 8 hours
|
|
50
|
+
'h': 60,
|
|
51
|
+
'm': 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for match in re.finditer(r'(\d+)\s*([wdhm])', time_str.lower()):
|
|
55
|
+
value, unit = int(match.group(1)), match.group(2)
|
|
56
|
+
total_minutes += value * patterns.get(unit, 0)
|
|
57
|
+
|
|
58
|
+
if total_minutes == 0:
|
|
59
|
+
return time_str # Return original if parsing failed
|
|
60
|
+
|
|
61
|
+
# Round to nearest 15 minutes (minimum 15 min)
|
|
62
|
+
rounded_minutes = max(15, round(total_minutes / 15) * 15)
|
|
63
|
+
|
|
64
|
+
# Convert back to Jira format
|
|
65
|
+
hours = rounded_minutes // 60
|
|
66
|
+
mins = rounded_minutes % 60
|
|
67
|
+
|
|
68
|
+
if hours > 0 and mins > 0:
|
|
69
|
+
return f"{hours}h {mins}m"
|
|
70
|
+
elif hours > 0:
|
|
71
|
+
return f"{hours}h"
|
|
72
|
+
else:
|
|
73
|
+
return f"{mins}m"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main():
|
|
77
|
+
parser = argparse.ArgumentParser(
|
|
78
|
+
description='Log work time to a Jira ticket',
|
|
79
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
80
|
+
epilog=__doc__
|
|
81
|
+
)
|
|
82
|
+
parser.add_argument('--ticket', required=True, help='Ticket key')
|
|
83
|
+
parser.add_argument('--time', required=True, help='Time spent (e.g., "2h", "1d")')
|
|
84
|
+
parser.add_argument('--comment', help='Work description')
|
|
85
|
+
parser.add_argument('--started', help='Start time (ISO format)')
|
|
86
|
+
parser.add_argument('--remaining', help='New remaining estimate')
|
|
87
|
+
args = parser.parse_args()
|
|
88
|
+
|
|
89
|
+
client = get_client()
|
|
90
|
+
ticket = args.ticket.upper()
|
|
91
|
+
|
|
92
|
+
# Round to 15-minute blocks (EC requirement)
|
|
93
|
+
original_time = args.time
|
|
94
|
+
rounded_time = round_to_15min_blocks(original_time)
|
|
95
|
+
|
|
96
|
+
if original_time != rounded_time:
|
|
97
|
+
print(f"⏱️ Rounding {original_time} → {rounded_time} (15-min blocks)", file=sys.stderr)
|
|
98
|
+
|
|
99
|
+
print(f"⏱️ Logging {rounded_time} to {ticket}...", file=sys.stderr)
|
|
100
|
+
|
|
101
|
+
# Verify ticket exists
|
|
102
|
+
success, issue = client.get_issue(ticket)
|
|
103
|
+
if not success:
|
|
104
|
+
print(f"❌ Error: Could not find ticket {ticket}: {issue}", file=sys.stderr)
|
|
105
|
+
sys.exit(2)
|
|
106
|
+
|
|
107
|
+
# Parse started time or use now
|
|
108
|
+
started = args.started
|
|
109
|
+
if not started:
|
|
110
|
+
# Jira expects format: 2026-01-23T14:00:00.000+0100
|
|
111
|
+
started = datetime.now().strftime('%Y-%m-%dT%H:%M:%S.000%z')
|
|
112
|
+
if not started.endswith('+') and len(started) < 30:
|
|
113
|
+
# Add timezone if missing
|
|
114
|
+
started = datetime.now().astimezone().strftime('%Y-%m-%dT%H:%M:%S.000%z')
|
|
115
|
+
|
|
116
|
+
# Determine estimate adjustment
|
|
117
|
+
adjust_estimate = None
|
|
118
|
+
new_estimate = None
|
|
119
|
+
if args.remaining:
|
|
120
|
+
adjust_estimate = 'new'
|
|
121
|
+
new_estimate = args.remaining
|
|
122
|
+
|
|
123
|
+
# Add worklog with rounded time
|
|
124
|
+
success, result = client.add_worklog(
|
|
125
|
+
ticket,
|
|
126
|
+
time_spent=rounded_time,
|
|
127
|
+
comment=args.comment,
|
|
128
|
+
started=started,
|
|
129
|
+
adjust_estimate=adjust_estimate,
|
|
130
|
+
new_estimate=new_estimate
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if not success:
|
|
134
|
+
print(f"❌ Error logging work: {result}", file=sys.stderr)
|
|
135
|
+
sys.exit(3)
|
|
136
|
+
|
|
137
|
+
worklog_id = result.get('id', 'Unknown')
|
|
138
|
+
time_logged = result.get('timeSpent', args.time)
|
|
139
|
+
|
|
140
|
+
output = {
|
|
141
|
+
'success': True,
|
|
142
|
+
'ticket': ticket,
|
|
143
|
+
'worklog_id': worklog_id,
|
|
144
|
+
'time_logged': time_logged,
|
|
145
|
+
'started': started
|
|
146
|
+
}
|
|
147
|
+
print(json.dumps(output, indent=2))
|
|
148
|
+
|
|
149
|
+
print(f"✅ Logged {time_logged} to {ticket}", file=sys.stderr)
|
|
150
|
+
sys.exit(0)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == '__main__':
|
|
154
|
+
main()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Search Jira Tickets
|
|
4
|
+
|
|
5
|
+
Search and filter Jira issues using JQL.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python search_tickets.py --jql <query> [options]
|
|
9
|
+
|
|
10
|
+
Arguments:
|
|
11
|
+
--jql JQL query string (required)
|
|
12
|
+
--fields Comma-separated fields to return
|
|
13
|
+
--max-results Maximum results (default: 50)
|
|
14
|
+
--output Output format: json, table, keys (default: table)
|
|
15
|
+
|
|
16
|
+
Exit Codes:
|
|
17
|
+
0 - Success
|
|
18
|
+
1 - Invalid arguments
|
|
19
|
+
2 - Search error
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
28
|
+
from jira_client import get_client
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def format_table(issues: list) -> str:
|
|
32
|
+
"""Format issues as a simple table."""
|
|
33
|
+
if not issues:
|
|
34
|
+
return "No issues found."
|
|
35
|
+
|
|
36
|
+
lines = []
|
|
37
|
+
lines.append(f"{'Key':<12} {'Type':<10} {'Priority':<10} {'Status':<15} {'Summary'}")
|
|
38
|
+
lines.append("-" * 80)
|
|
39
|
+
|
|
40
|
+
for issue in issues:
|
|
41
|
+
key = issue.get('key', '')
|
|
42
|
+
fields = issue.get('fields', {})
|
|
43
|
+
|
|
44
|
+
issue_type = fields.get('issuetype', {}).get('name', '-')[:10]
|
|
45
|
+
priority = fields.get('priority', {}).get('name', '-')[:10] if fields.get('priority') else '-'
|
|
46
|
+
status = fields.get('status', {}).get('name', '-')[:15]
|
|
47
|
+
summary = fields.get('summary', '')[:50]
|
|
48
|
+
|
|
49
|
+
lines.append(f"{key:<12} {issue_type:<10} {priority:<10} {status:<15} {summary}")
|
|
50
|
+
|
|
51
|
+
return '\n'.join(lines)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main():
|
|
55
|
+
parser = argparse.ArgumentParser(
|
|
56
|
+
description='Search Jira tickets with JQL',
|
|
57
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
58
|
+
epilog=__doc__
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument('--jql', required=True, help='JQL query')
|
|
61
|
+
parser.add_argument('--fields', help='Comma-separated fields')
|
|
62
|
+
parser.add_argument('--max-results', type=int, default=50, help='Max results')
|
|
63
|
+
parser.add_argument('--output', choices=['json', 'table', 'keys'], default='table',
|
|
64
|
+
help='Output format')
|
|
65
|
+
args = parser.parse_args()
|
|
66
|
+
|
|
67
|
+
client = get_client()
|
|
68
|
+
|
|
69
|
+
print(f"🔍 Searching: {args.jql}", file=sys.stderr)
|
|
70
|
+
|
|
71
|
+
# Parse fields
|
|
72
|
+
fields = None
|
|
73
|
+
if args.fields:
|
|
74
|
+
fields = [f.strip() for f in args.fields.split(',')]
|
|
75
|
+
else:
|
|
76
|
+
# Default useful fields
|
|
77
|
+
fields = ['summary', 'status', 'priority', 'issuetype', 'assignee', 'created', 'updated']
|
|
78
|
+
|
|
79
|
+
# Search
|
|
80
|
+
success, result = client.search_issues(args.jql, fields=fields, max_results=args.max_results)
|
|
81
|
+
|
|
82
|
+
if not success:
|
|
83
|
+
print(f"❌ Search error: {result}", file=sys.stderr)
|
|
84
|
+
sys.exit(2)
|
|
85
|
+
|
|
86
|
+
issues = result.get('issues', [])
|
|
87
|
+
total = result.get('total', len(issues))
|
|
88
|
+
|
|
89
|
+
print(f" Found {total} issue(s) (showing {len(issues)})", file=sys.stderr)
|
|
90
|
+
|
|
91
|
+
# Output based on format
|
|
92
|
+
if args.output == 'keys':
|
|
93
|
+
for issue in issues:
|
|
94
|
+
print(issue.get('key'))
|
|
95
|
+
elif args.output == 'json':
|
|
96
|
+
print(json.dumps(issues, indent=2))
|
|
97
|
+
else:
|
|
98
|
+
print(format_table(issues))
|
|
99
|
+
|
|
100
|
+
sys.exit(0)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
if __name__ == '__main__':
|
|
104
|
+
main()
|