create-ironclaws 1.0.3 → 1.1.0

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.
@@ -1,433 +0,0 @@
1
- """
2
- Jira tool for Argus agents.
3
-
4
- Usage:
5
- # Get next ticket to work on (highest priority, oldest first)
6
- python3 jira_tool.py next
7
-
8
- # Get specific ticket
9
- python3 jira_tool.py get --issue IAI-42
10
-
11
- # Search tickets
12
- python3 jira_tool.py search --status "To Do"
13
- python3 jira_tool.py search --status "In Progress" --assignee "user@example.com"
14
-
15
- # Create a ticket
16
- python3 jira_tool.py create --summary "Bug in BobAIA" --description "..." --type Bug
17
- python3 jira_tool.py create --summary "Fix login" --type Bug --epic IAI-10
18
-
19
- # Create an epic
20
- python3 jira_tool.py create-epic --summary "Salesforce Integration" --description "..."
21
- python3 jira_tool.py create-epic --summary "Salesforce Integration" --start-date 2026-04-15 --due-date 2026-06-30
22
-
23
- # Search epics
24
- python3 jira_tool.py search-epics
25
- python3 jira_tool.py search-epics --query "Salesforce"
26
-
27
- # Update a ticket
28
- python3 jira_tool.py update --issue IAI-42 --status "In Progress"
29
- python3 jira_tool.py update --issue IAI-42 --description "Updated description"
30
- python3 jira_tool.py update --issue IAI-42 --comment "Working on this now"
31
- python3 jira_tool.py update --issue IAI-42 --assignee-email "user@yourcompany.com"
32
-
33
- # Find Jira account ID by email
34
- python3 jira_tool.py user --email "user@yourcompany.com"
35
-
36
- Environment variables (required):
37
- JIRA_BASE_URL e.g. https://yourcompany.atlassian.net
38
- JIRA_EMAIL Atlassian account email
39
- JIRA_API_TOKEN Atlassian API token
40
- JIRA_PROJECT_KEY e.g. IAI
41
- """
42
-
43
- import argparse
44
- import json
45
- import os
46
- import sys
47
-
48
- try:
49
- import requests
50
- from requests.auth import HTTPBasicAuth
51
- except ImportError:
52
- print("ERROR: 'requests' is not installed. Run: pip3 install requests", file=sys.stderr)
53
- sys.exit(1)
54
-
55
-
56
- PRIORITY_ORDER = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3, "Lowest": 4}
57
-
58
-
59
- def get_client():
60
- base_url = os.environ.get("JIRA_BASE_URL", "").rstrip("/")
61
- project = os.environ.get("JIRA_PROJECT_KEY", "")
62
-
63
- if not base_url or not project:
64
- print(
65
- "ERROR: JIRA_BASE_URL and JIRA_PROJECT_KEY must be set.",
66
- file=sys.stderr,
67
- )
68
- sys.exit(1)
69
-
70
- email = os.environ.get("JIRA_EMAIL", "")
71
- token = os.environ.get("JIRA_API_TOKEN", "")
72
- # auth is optional — if absent, OneCLI HTTPS proxy injects Authorization header.
73
- auth = HTTPBasicAuth(email, token) if email and token else None
74
- return base_url, auth, project
75
-
76
-
77
- def jira_get(base_url, auth, path, params=None):
78
- resp = requests.get(
79
- f"{base_url}/rest/api/3{path}",
80
- auth=auth,
81
- headers={"Accept": "application/json"},
82
- params=params,
83
- timeout=30,
84
- )
85
- resp.raise_for_status()
86
- return resp.json()
87
-
88
-
89
- def jira_search(base_url, auth, jql, max_results=50, fields=None):
90
- """POST-based search — returns the issues list directly."""
91
- if fields is None:
92
- fields = ["summary", "status", "priority", "issuetype",
93
- "assignee", "reporter", "description", "created"]
94
- body = {"jql": jql, "maxResults": max_results, "fields": fields}
95
- resp = requests.post(
96
- f"{base_url}/rest/api/3/search/jql",
97
- auth=auth,
98
- headers={"Accept": "application/json", "Content-Type": "application/json"},
99
- json=body,
100
- timeout=30,
101
- )
102
- resp.raise_for_status()
103
- return resp.json().get("issues", [])
104
-
105
-
106
- def jira_post(base_url, auth, path, body):
107
- resp = requests.post(
108
- f"{base_url}/rest/api/3{path}",
109
- auth=auth,
110
- headers={"Accept": "application/json", "Content-Type": "application/json"},
111
- json=body,
112
- timeout=30,
113
- )
114
- resp.raise_for_status()
115
- return resp.json()
116
-
117
-
118
- def jira_put(base_url, auth, path, body):
119
- resp = requests.put(
120
- f"{base_url}/rest/api/3{path}",
121
- auth=auth,
122
- headers={"Accept": "application/json", "Content-Type": "application/json"},
123
- json=body,
124
- timeout=30,
125
- )
126
- resp.raise_for_status()
127
- return resp
128
-
129
-
130
- def jira_transition(base_url, auth, issue_key, status_name):
131
- """Move an issue to a new status by name."""
132
- transitions = jira_get(base_url, auth, f"/issue/{issue_key}/transitions")
133
- match = next(
134
- (t for t in transitions["transitions"] if t["name"].lower() == status_name.lower()),
135
- None,
136
- )
137
- if not match:
138
- available = [t["name"] for t in transitions["transitions"]]
139
- print(
140
- f"ERROR: Status '{status_name}' not found. Available: {available}",
141
- file=sys.stderr,
142
- )
143
- sys.exit(1)
144
- jira_post(base_url, auth, f"/issue/{issue_key}/transitions", {"transition": {"id": match["id"]}})
145
-
146
-
147
- def format_issue(issue):
148
- fields = issue.get("fields", {})
149
- priority = (fields.get("priority") or {}).get("name", "None")
150
- status = (fields.get("status") or {}).get("name", "?")
151
- assignee = (fields.get("assignee") or {}).get("emailAddress", "unassigned")
152
- reporter = (fields.get("reporter") or {}).get("emailAddress", "?")
153
- issue_type = (fields.get("issuetype") or {}).get("name", "?")
154
- summary = fields.get("summary", "?")
155
- key = issue.get("key", "?")
156
-
157
- # Extract plain text from description (Atlassian Document Format)
158
- desc_raw = fields.get("description") or {}
159
- description = extract_adf_text(desc_raw)
160
-
161
- return {
162
- "key": key,
163
- "type": issue_type,
164
- "status": status,
165
- "priority": priority,
166
- "summary": summary,
167
- "description": description,
168
- "reporter": reporter,
169
- "assignee": assignee,
170
- "url": f"{os.environ.get('JIRA_BASE_URL', '').rstrip('/')}/browse/{key}",
171
- }
172
-
173
-
174
- def extract_adf_text(node):
175
- """Extract plain text from Atlassian Document Format (ADF) JSON."""
176
- if not node:
177
- return ""
178
- if isinstance(node, str):
179
- return node
180
- if node.get("type") == "text":
181
- return node.get("text", "")
182
- parts = []
183
- for child in node.get("content", []):
184
- parts.append(extract_adf_text(child))
185
- return " ".join(p for p in parts if p).strip()
186
-
187
-
188
- def cmd_get(args):
189
- base_url, auth, _ = get_client()
190
- issue = jira_get(base_url, auth, f"/issue/{args.issue}")
191
- print(json.dumps(format_issue(issue), indent=2))
192
-
193
-
194
- def cmd_next(args):
195
- base_url, auth, project = get_client()
196
- jql = f'project = {project} AND status = "To Do" ORDER BY priority ASC, created ASC'
197
- issues = jira_search(base_url, auth, jql, max_results=10)
198
- if not issues:
199
- print("No tickets in To Do.")
200
- return
201
- def sort_key(i):
202
- p = (i.get("fields", {}).get("priority") or {}).get("name", "Low")
203
- return (PRIORITY_ORDER.get(p, 99), i.get("fields", {}).get("created", ""))
204
- issues.sort(key=sort_key)
205
- print(json.dumps(format_issue(issues[0]), indent=2))
206
-
207
-
208
- def cmd_search(args):
209
- base_url, auth, project = get_client()
210
- conditions = [f"project = {project}"]
211
- if args.status:
212
- conditions.append(f'status = "{args.status}"')
213
- if args.assignee:
214
- conditions.append(f'assignee = "{args.assignee}"')
215
- jql = " AND ".join(conditions) + " ORDER BY priority ASC, created ASC"
216
- issues = jira_search(base_url, auth, jql)
217
- print(json.dumps([format_issue(i) for i in issues], indent=2))
218
-
219
-
220
- def cmd_create(args):
221
- base_url, auth, project = get_client()
222
-
223
- # Build description in ADF format
224
- description_adf = {
225
- "type": "doc",
226
- "version": 1,
227
- "content": [
228
- {
229
- "type": "paragraph",
230
- "content": [{"type": "text", "text": args.description or ""}],
231
- }
232
- ],
233
- }
234
-
235
- body = {
236
- "fields": {
237
- "project": {"key": project},
238
- "summary": args.summary,
239
- "description": description_adf,
240
- "issuetype": {"name": args.type or "Task"},
241
- }
242
- }
243
-
244
- # Set reporter by account ID if provided
245
- if args.reporter_account_id:
246
- body["fields"]["reporter"] = {"id": args.reporter_account_id}
247
-
248
- # Link to parent epic if provided
249
- if args.epic:
250
- body["fields"]["parent"] = {"key": args.epic}
251
-
252
- result = jira_post(base_url, auth, "/issue", body)
253
- key = result.get("key")
254
- url = f"{base_url}/browse/{key}"
255
- print(json.dumps({"key": key, "url": url, "epic": args.epic or None}))
256
-
257
-
258
- def cmd_create_epic(args):
259
- base_url, auth, project = get_client()
260
-
261
- description_adf = {
262
- "type": "doc",
263
- "version": 1,
264
- "content": [
265
- {
266
- "type": "paragraph",
267
- "content": [{"type": "text", "text": args.description or ""}],
268
- }
269
- ],
270
- }
271
-
272
- body = {
273
- "fields": {
274
- "project": {"key": project},
275
- "summary": args.summary,
276
- "description": description_adf,
277
- "issuetype": {"name": "Epic"},
278
- }
279
- }
280
-
281
- if args.reporter_account_id:
282
- body["fields"]["reporter"] = {"id": args.reporter_account_id}
283
-
284
- if args.start_date:
285
- # customfield_10015 is the standard Epic start date in Jira Cloud
286
- body["fields"]["customfield_10015"] = args.start_date
287
-
288
- if args.due_date:
289
- body["fields"]["duedate"] = args.due_date
290
-
291
- result = jira_post(base_url, auth, "/issue", body)
292
- key = result.get("key")
293
- url = f"{base_url}/browse/{key}"
294
- print(json.dumps({"key": key, "url": url, "type": "Epic"}))
295
-
296
-
297
- def cmd_search_epics(args):
298
- base_url, auth, project = get_client()
299
- conditions = [f"project = {project}", "issuetype = Epic"]
300
- if args.query:
301
- # Escape quotes in query for JQL
302
- safe_query = args.query.replace('"', '\\"')
303
- conditions.append(f'summary ~ "{safe_query}"')
304
- jql = " AND ".join(conditions) + " ORDER BY created DESC"
305
- issues = jira_search(base_url, auth, jql)
306
- print(json.dumps([format_issue(i) for i in issues], indent=2))
307
-
308
-
309
- def cmd_update(args):
310
- base_url, auth, _ = get_client()
311
-
312
- if args.status:
313
- jira_transition(base_url, auth, args.issue, args.status)
314
- print(f"Moved {args.issue} to '{args.status}'")
315
-
316
- if args.description:
317
- description_adf = {
318
- "type": "doc",
319
- "version": 1,
320
- "content": [
321
- {
322
- "type": "paragraph",
323
- "content": [{"type": "text", "text": args.description}],
324
- }
325
- ],
326
- }
327
- jira_put(base_url, auth, f"/issue/{args.issue}", {"fields": {"description": description_adf}})
328
- print(f"Updated description on {args.issue}")
329
-
330
- if args.comment:
331
- comment_adf = {
332
- "type": "doc",
333
- "version": 1,
334
- "content": [
335
- {
336
- "type": "paragraph",
337
- "content": [{"type": "text", "text": args.comment}],
338
- }
339
- ],
340
- }
341
- jira_post(base_url, auth, f"/issue/{args.issue}/comment", {"body": comment_adf})
342
- print(f"Added comment to {args.issue}")
343
-
344
- if args.assignee_email:
345
- result = jira_get(base_url, auth, "/user/search", params={"query": args.assignee_email})
346
- matches = [u for u in result if u.get("emailAddress", "").lower() == args.assignee_email.lower()]
347
- if not matches:
348
- print(f"ERROR: No Jira user found for email '{args.assignee_email}'", file=sys.stderr)
349
- sys.exit(1)
350
- account_id = matches[0]["accountId"]
351
- jira_put(base_url, auth, f"/issue/{args.issue}", {"fields": {"assignee": {"id": account_id}}})
352
- print(f"Assigned {args.issue} to {args.assignee_email}")
353
-
354
-
355
- def cmd_user(args):
356
- """Find Jira account ID by email — used to set reporter/assignee."""
357
- base_url, auth, _ = get_client()
358
- result = jira_get(base_url, auth, "/user/search", params={"query": args.email})
359
- matches = [u for u in result if u.get("emailAddress", "").lower() == args.email.lower()]
360
- if not matches:
361
- print(f"ERROR: No Jira user found for email '{args.email}'", file=sys.stderr)
362
- sys.exit(1)
363
- user = matches[0]
364
- print(json.dumps({
365
- "account_id": user.get("accountId"),
366
- "display_name": user.get("displayName"),
367
- "email": user.get("emailAddress"),
368
- }))
369
-
370
-
371
- def parse_args():
372
- p = argparse.ArgumentParser(description="Jira tool for Argus agents")
373
- sub = p.add_subparsers(dest="command")
374
-
375
- sub.add_parser("next", help="Get next To Do ticket by priority")
376
-
377
- g = sub.add_parser("get", help="Get a specific ticket")
378
- g.add_argument("--issue", required=True)
379
-
380
- s = sub.add_parser("search", help="Search tickets")
381
- s.add_argument("--status")
382
- s.add_argument("--assignee")
383
-
384
- c = sub.add_parser("create", help="Create a ticket")
385
- c.add_argument("--summary", required=True)
386
- c.add_argument("--description", default="")
387
- c.add_argument("--type", default="Task", help="Bug, Task, Story, etc.")
388
- c.add_argument("--reporter-account-id", dest="reporter_account_id", default=None)
389
- c.add_argument("--epic", default=None, help="Parent epic key (e.g. IAI-10) to link this ticket under")
390
-
391
- ce = sub.add_parser("create-epic", help="Create an epic")
392
- ce.add_argument("--summary", required=True)
393
- ce.add_argument("--description", default="")
394
- ce.add_argument("--reporter-account-id", dest="reporter_account_id", default=None)
395
- ce.add_argument("--start-date", dest="start_date", default=None, help="Start date (YYYY-MM-DD)")
396
- ce.add_argument("--due-date", dest="due_date", default=None, help="Due date (YYYY-MM-DD)")
397
-
398
- se = sub.add_parser("search-epics", help="Search epics in the project")
399
- se.add_argument("--query", default=None, help="Search term to filter epic summaries")
400
-
401
- u = sub.add_parser("update", help="Update a ticket")
402
- u.add_argument("--issue", required=True)
403
- u.add_argument("--status", help="Transition to this status")
404
- u.add_argument("--description", help="Update description")
405
- u.add_argument("--comment", help="Add a comment")
406
- u.add_argument("--assignee-email", dest="assignee_email", help="Assign to this email address")
407
-
408
- usr = sub.add_parser("user", help="Find Jira account ID by email")
409
- usr.add_argument("--email", required=True)
410
-
411
- return p.parse_args()
412
-
413
-
414
- def main():
415
- args = parse_args()
416
- if not args.command:
417
- print("ERROR: specify a command. Use --help for usage.", file=sys.stderr)
418
- sys.exit(1)
419
- try:
420
- {"next": cmd_next, "get": cmd_get, "search": cmd_search,
421
- "create": cmd_create, "create-epic": cmd_create_epic,
422
- "search-epics": cmd_search_epics,
423
- "update": cmd_update, "user": cmd_user}[args.command](args)
424
- except requests.HTTPError as e:
425
- print(f"ERROR: Jira API error: {e}\n{e.response.text}", file=sys.stderr)
426
- sys.exit(1)
427
- except Exception as e:
428
- print(f"ERROR: {e}", file=sys.stderr)
429
- sys.exit(1)
430
-
431
-
432
- if __name__ == "__main__":
433
- main()
@@ -1,144 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Slack channel history tool for Byte agent.
4
-
5
- Usage:
6
- # Fetch last 180 days of messages
7
- python3 slack_history_tool.py history --channel C05DV1DU58R --days 180
8
-
9
- # Fetch messages since a specific date
10
- python3 slack_history_tool.py history --channel C05DV1DU58R --since 2026-01-01
11
-
12
- # Fetch messages since a Unix timestamp (for incremental reads)
13
- python3 slack_history_tool.py history --channel C05DV1DU58R --oldest 1700000000
14
-
15
- Environment variables (required):
16
- SLACK_BOT_TOKEN Bot token with channels:history or groups:history scope
17
- """
18
-
19
- import argparse
20
- import json
21
- import os
22
- import sys
23
- from datetime import datetime, timedelta
24
- import urllib.request
25
- import urllib.parse
26
-
27
- SLACK_BOT_TOKEN = os.environ.get('SLACK_BOT_TOKEN', '')
28
-
29
- _user_cache: dict = {}
30
-
31
-
32
- def slack_api(method: str, params: dict) -> dict:
33
- url = f'https://slack.com/api/{method}'
34
- data = urllib.parse.urlencode(params).encode()
35
- req = urllib.request.Request(url, data=data, method='POST')
36
- # Authorization is optional — if token absent, OneCLI HTTPS proxy injects it.
37
- if SLACK_BOT_TOKEN:
38
- req.add_header('Authorization', f'Bearer {SLACK_BOT_TOKEN}')
39
- req.add_header('Content-Type', 'application/x-www-form-urlencoded')
40
- with urllib.request.urlopen(req, timeout=30) as response:
41
- return json.loads(response.read())
42
-
43
-
44
- def resolve_user(user_id: str) -> str:
45
- if not user_id:
46
- return 'Unknown'
47
- if user_id in _user_cache:
48
- return _user_cache[user_id]
49
- try:
50
- result = slack_api('users.info', {'user': user_id})
51
- name = (
52
- result.get('user', {}).get('real_name')
53
- or result.get('user', {}).get('name')
54
- or user_id
55
- )
56
- _user_cache[user_id] = name
57
- return name
58
- except Exception:
59
- _user_cache[user_id] = user_id
60
- return user_id
61
-
62
-
63
- def fetch_history(channel: str, oldest: str) -> list:
64
- messages = []
65
- cursor = None
66
-
67
- while True:
68
- params: dict = {
69
- 'channel': channel,
70
- 'limit': 200,
71
- 'oldest': oldest,
72
- }
73
- if cursor:
74
- params['cursor'] = cursor
75
-
76
- result = slack_api('conversations.history', params)
77
-
78
- if not result.get('ok'):
79
- print(f"Error: {result.get('error', 'unknown')}", file=sys.stderr)
80
- break
81
-
82
- messages.extend(result.get('messages', []))
83
-
84
- if not result.get('has_more'):
85
- break
86
-
87
- cursor = result.get('response_metadata', {}).get('next_cursor')
88
- if not cursor:
89
- break
90
-
91
- return messages
92
-
93
-
94
- def format_messages(messages: list) -> str:
95
- lines = []
96
- # API returns newest first — reverse for chronological order
97
- for msg in reversed(messages):
98
- # Skip system messages (joins, topic changes, etc.)
99
- if msg.get('subtype'):
100
- continue
101
- text = msg.get('text', '').strip()
102
- if not text:
103
- continue
104
-
105
- ts = float(msg.get('ts', 0))
106
- dt = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M')
107
- user_name = resolve_user(msg.get('user', ''))
108
-
109
- lines.append(f'[{dt}] {user_name}: {text}')
110
-
111
- return '\n'.join(lines)
112
-
113
-
114
- def main():
115
- # SLACK_BOT_TOKEN is optional — if absent, OneCLI HTTPS proxy injects Authorization.
116
- parser = argparse.ArgumentParser(description='Fetch Slack channel history')
117
- subparsers = parser.add_subparsers(dest='command')
118
-
119
- history_parser = subparsers.add_parser('history', help='Fetch channel message history')
120
- history_parser.add_argument('--channel', required=True, help='Slack channel ID')
121
- history_parser.add_argument('--days', type=int, default=180, help='Number of days back (default: 180)')
122
- history_parser.add_argument('--since', help='Fetch since this date (ISO format: 2026-01-01)')
123
- history_parser.add_argument('--oldest', help='Fetch since this Unix timestamp')
124
-
125
- args = parser.parse_args()
126
-
127
- if args.command == 'history':
128
- if args.oldest:
129
- oldest = args.oldest
130
- elif args.since:
131
- dt = datetime.fromisoformat(args.since)
132
- oldest = str(dt.timestamp())
133
- else:
134
- dt = datetime.now() - timedelta(days=args.days)
135
- oldest = str(dt.timestamp())
136
-
137
- messages = fetch_history(args.channel, oldest=oldest)
138
- print(format_messages(messages))
139
- else:
140
- parser.print_help()
141
-
142
-
143
- if __name__ == '__main__':
144
- main()