claude-dev-cli 0.16.2__py3-none-any.whl → 0.18.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.

Potentially problematic release.


This version of claude-dev-cli might be problematic. Click here for more details.

@@ -0,0 +1,69 @@
1
+ """Abstract notification interface for claude-dev-cli."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional, Dict, Any
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+
8
+
9
+ class NotificationPriority(Enum):
10
+ """Notification priority levels."""
11
+ LOW = "low"
12
+ NORMAL = "normal"
13
+ HIGH = "high"
14
+ URGENT = "urgent"
15
+
16
+
17
+ @dataclass
18
+ class NotificationConfig:
19
+ """Configuration for notification backend."""
20
+ backend: str # ntfy, telegram, discord, slack, email
21
+ enabled: bool = True
22
+ settings: Dict[str, Any] = None
23
+
24
+ def __post_init__(self):
25
+ """Initialize settings."""
26
+ if self.settings is None:
27
+ self.settings = {}
28
+
29
+
30
+ class Notifier(ABC):
31
+ """Abstract base class for notification backends."""
32
+
33
+ @abstractmethod
34
+ def send(
35
+ self,
36
+ title: str,
37
+ message: str,
38
+ priority: NotificationPriority = NotificationPriority.NORMAL,
39
+ tags: Optional[list] = None
40
+ ) -> bool:
41
+ """Send a notification.
42
+
43
+ Args:
44
+ title: Notification title
45
+ message: Notification message
46
+ priority: Priority level
47
+ tags: Optional tags/labels
48
+
49
+ Returns:
50
+ True if sent successfully
51
+ """
52
+ pass
53
+
54
+ @abstractmethod
55
+ def test_connection(self) -> bool:
56
+ """Test if notification backend is properly configured.
57
+
58
+ Returns:
59
+ True if backend is accessible
60
+ """
61
+ pass
62
+
63
+ def get_backend_name(self) -> str:
64
+ """Get the name of this notification backend.
65
+
66
+ Returns:
67
+ Backend name
68
+ """
69
+ return self.__class__.__name__.replace('Notifier', '').lower()
@@ -0,0 +1,87 @@
1
+ """ntfy.sh notification backend.
2
+
3
+ No signup required - works out of the box with public ntfy.sh server.
4
+ Users can self-host if desired.
5
+ """
6
+
7
+ from typing import Optional
8
+ import requests
9
+
10
+ from claude_dev_cli.notifications.notifier import Notifier, NotificationPriority
11
+
12
+
13
+ class NtfyNotifier(Notifier):
14
+ """ntfy.sh notification backend.
15
+
16
+ Simple, free, no-signup push notifications via HTTP.
17
+ https://ntfy.sh
18
+ """
19
+
20
+ def __init__(self, topic: str, server: str = "https://ntfy.sh"):
21
+ """Initialize ntfy notifier.
22
+
23
+ Args:
24
+ topic: ntfy topic name (choose any unique name)
25
+ server: ntfy server URL (default: public ntfy.sh)
26
+ """
27
+ self.topic = topic
28
+ self.server = server.rstrip('/')
29
+ self.url = f"{self.server}/{topic}"
30
+
31
+ def send(
32
+ self,
33
+ title: str,
34
+ message: str,
35
+ priority: NotificationPriority = NotificationPriority.NORMAL,
36
+ tags: Optional[list] = None
37
+ ) -> bool:
38
+ """Send notification via ntfy."""
39
+ try:
40
+ headers = {
41
+ "Title": title,
42
+ "Priority": self._map_priority(priority)
43
+ }
44
+
45
+ if tags:
46
+ # ntfy supports emoji tags
47
+ headers["Tags"] = ",".join(tags)
48
+
49
+ response = requests.post(
50
+ self.url,
51
+ data=message.encode('utf-8'),
52
+ headers=headers,
53
+ timeout=10
54
+ )
55
+
56
+ return response.status_code == 200
57
+
58
+ except (requests.RequestException, Exception):
59
+ return False
60
+
61
+ def test_connection(self) -> bool:
62
+ """Test ntfy server connectivity."""
63
+ try:
64
+ # Send a test message
65
+ response = requests.post(
66
+ self.url,
67
+ data="Test notification from claude-dev-cli".encode('utf-8'),
68
+ headers={"Title": "Test"},
69
+ timeout=5
70
+ )
71
+ return response.status_code == 200
72
+ except requests.RequestException:
73
+ return False
74
+
75
+ def _map_priority(self, priority: NotificationPriority) -> str:
76
+ """Map NotificationPriority to ntfy priority levels."""
77
+ mapping = {
78
+ NotificationPriority.LOW: "2",
79
+ NotificationPriority.NORMAL: "3",
80
+ NotificationPriority.HIGH: "4",
81
+ NotificationPriority.URGENT: "5"
82
+ }
83
+ return mapping.get(priority, "3")
84
+
85
+ def get_backend_name(self) -> str:
86
+ """Return backend name."""
87
+ return "ntfy"
@@ -0,0 +1,10 @@
1
+ """Project management and automation for claude-dev-cli.
2
+
3
+ Note: This project will be renamed in the future to reflect its broader
4
+ automation capabilities beyond Claude AI integration.
5
+ """
6
+
7
+ from claude_dev_cli.project.executor import TicketExecutor
8
+ from claude_dev_cli.project.bug_tracker import BugTriageSystem, BugReport, BugSeverity, BugCategory
9
+
10
+ __all__ = ["TicketExecutor", "BugTriageSystem", "BugReport", "BugSeverity", "BugCategory"]
@@ -0,0 +1,458 @@
1
+ """Bug reporting and triage system for claude-dev-cli.
2
+
3
+ Handles bug reports independently from main project workflow,
4
+ with automatic triage, priority assignment, and categorization.
5
+ """
6
+
7
+ from typing import Optional, List, Dict, Any
8
+ from dataclasses import dataclass
9
+ from datetime import datetime
10
+ from enum import Enum
11
+
12
+ from claude_dev_cli.tickets.backend import TicketBackend, Ticket
13
+ from claude_dev_cli.core import ClaudeClient
14
+ from claude_dev_cli.notifications.notifier import Notifier, NotificationPriority
15
+
16
+
17
+ class BugSeverity(Enum):
18
+ """Bug severity levels."""
19
+ CRITICAL = "critical" # System down, data loss, security breach
20
+ HIGH = "high" # Major functionality broken
21
+ MEDIUM = "medium" # Functionality impaired but workaround exists
22
+ LOW = "low" # Minor issue, cosmetic
23
+ TRIVIAL = "trivial" # Typos, UI inconsistencies
24
+
25
+
26
+ class BugCategory(Enum):
27
+ """Bug categories for classification."""
28
+ CRASH = "crash" # Application crashes
29
+ DATA_LOSS = "data-loss" # Data corruption or loss
30
+ SECURITY = "security" # Security vulnerabilities
31
+ PERFORMANCE = "performance" # Performance issues
32
+ UI_UX = "ui-ux" # User interface/experience issues
33
+ INTEGRATION = "integration" # Third-party integration issues
34
+ FUNCTIONALITY = "functionality" # Feature not working as expected
35
+ DOCUMENTATION = "documentation" # Documentation errors
36
+ OTHER = "other"
37
+
38
+
39
+ @dataclass
40
+ class BugReport:
41
+ """Structured bug report."""
42
+ id: Optional[str]
43
+ title: str
44
+ description: str
45
+
46
+ # Reproduction information
47
+ steps_to_reproduce: List[str]
48
+ expected_behavior: str
49
+ actual_behavior: str
50
+
51
+ # Environment
52
+ environment: Optional[str] = None # production, staging, development
53
+ version: Optional[str] = None
54
+ platform: Optional[str] = None # OS, browser, etc.
55
+
56
+ # Classification (can be auto-assigned by triage)
57
+ severity: Optional[BugSeverity] = None
58
+ category: Optional[BugCategory] = None
59
+ priority: Optional[str] = None # critical, high, medium, low
60
+
61
+ # Additional context
62
+ stack_trace: Optional[str] = None
63
+ logs: Optional[str] = None
64
+ screenshots: List[str] = None
65
+ related_tickets: List[str] = None
66
+
67
+ # Metadata
68
+ reporter: Optional[str] = None
69
+ reported_at: Optional[datetime] = None
70
+ triaged: bool = False
71
+ assigned_to: Optional[str] = None
72
+
73
+ def __post_init__(self):
74
+ """Initialize default values."""
75
+ if self.screenshots is None:
76
+ self.screenshots = []
77
+ if self.related_tickets is None:
78
+ self.related_tickets = []
79
+ if self.reported_at is None:
80
+ self.reported_at = datetime.now()
81
+
82
+
83
+ class BugTriageSystem:
84
+ """AI-powered bug triage and classification system.
85
+
86
+ Automatically:
87
+ - Classifies bug severity based on description
88
+ - Categorizes bugs
89
+ - Suggests priority
90
+ - Identifies duplicate bugs
91
+ - Recommends assignment
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ ticket_backend: TicketBackend,
97
+ ai_client: Optional[ClaudeClient] = None,
98
+ notifier: Optional[Notifier] = None
99
+ ):
100
+ """Initialize bug triage system.
101
+
102
+ Args:
103
+ ticket_backend: Ticket management backend
104
+ ai_client: AI client for triage analysis
105
+ notifier: Notification system for critical bugs
106
+ """
107
+ self.ticket_backend = ticket_backend
108
+ self.ai_client = ai_client or ClaudeClient()
109
+ self.notifier = notifier
110
+
111
+ def submit_bug(self, bug_report: BugReport, auto_triage: bool = True) -> Ticket:
112
+ """Submit a bug report and optionally auto-triage.
113
+
114
+ Args:
115
+ bug_report: Bug report to submit
116
+ auto_triage: Whether to automatically triage
117
+
118
+ Returns:
119
+ Created ticket
120
+ """
121
+ # Auto-triage if requested
122
+ if auto_triage and not bug_report.triaged:
123
+ bug_report = self.triage_bug(bug_report)
124
+
125
+ # Create ticket in backend
126
+ ticket = self.ticket_backend.create_task(
127
+ story_id=None, # Bugs are independent
128
+ title=f"[BUG] {bug_report.title}",
129
+ description=self._format_bug_description(bug_report),
130
+ ticket_type="bug",
131
+ priority=bug_report.priority or "medium",
132
+ labels=self._get_bug_labels(bug_report),
133
+ assignee=bug_report.assigned_to,
134
+ requirements=bug_report.steps_to_reproduce,
135
+ acceptance_criteria=[
136
+ f"Expected: {bug_report.expected_behavior}",
137
+ f"Actual: {bug_report.actual_behavior}"
138
+ ]
139
+ )
140
+
141
+ bug_report.id = ticket.id
142
+
143
+ # Add detailed comment with stack trace/logs
144
+ if bug_report.stack_trace or bug_report.logs:
145
+ self._add_technical_details(ticket.id, bug_report)
146
+
147
+ # Notify if critical/high severity
148
+ if bug_report.severity in [BugSeverity.CRITICAL, BugSeverity.HIGH]:
149
+ self._notify_critical_bug(bug_report)
150
+
151
+ return ticket
152
+
153
+ def triage_bug(self, bug_report: BugReport) -> BugReport:
154
+ """Use AI to automatically triage a bug report.
155
+
156
+ Analyzes bug description and assigns:
157
+ - Severity level
158
+ - Category
159
+ - Priority
160
+ - Potential duplicates
161
+
162
+ Args:
163
+ bug_report: Bug report to triage
164
+
165
+ Returns:
166
+ Bug report with triage information added
167
+ """
168
+ triage_prompt = self._build_triage_prompt(bug_report)
169
+
170
+ try:
171
+ triage_response = self.ai_client.call(
172
+ triage_prompt,
173
+ system_prompt="You are an expert bug triage specialist. Analyze bugs and provide structured classification."
174
+ )
175
+
176
+ # Parse triage response and update bug report
177
+ triage_data = self._parse_triage_response(triage_response)
178
+
179
+ bug_report.severity = triage_data.get('severity')
180
+ bug_report.category = triage_data.get('category')
181
+ bug_report.priority = triage_data.get('priority')
182
+ bug_report.triaged = True
183
+
184
+ except Exception as e:
185
+ # Fallback to manual triage
186
+ print(f"Auto-triage failed: {e}. Using fallback classification.")
187
+ bug_report = self._fallback_triage(bug_report)
188
+
189
+ return bug_report
190
+
191
+ def find_duplicates(self, bug_report: BugReport, threshold: float = 0.7) -> List[str]:
192
+ """Find potential duplicate bug reports.
193
+
194
+ Args:
195
+ bug_report: Bug to check for duplicates
196
+ threshold: Similarity threshold (0-1)
197
+
198
+ Returns:
199
+ List of ticket IDs that might be duplicates
200
+ """
201
+ # Get all existing bug tickets
202
+ existing_bugs = self.ticket_backend.list_tickets(
203
+ status="open"
204
+ )
205
+
206
+ duplicates = []
207
+
208
+ # Simple keyword-based duplicate detection
209
+ # Could be enhanced with semantic similarity
210
+ bug_keywords = set(bug_report.title.lower().split())
211
+
212
+ for ticket in existing_bugs:
213
+ if ticket.ticket_type != "bug":
214
+ continue
215
+
216
+ ticket_keywords = set(ticket.title.lower().split())
217
+ similarity = len(bug_keywords & ticket_keywords) / len(bug_keywords | ticket_keywords)
218
+
219
+ if similarity >= threshold:
220
+ duplicates.append(ticket.id)
221
+
222
+ return duplicates
223
+
224
+ def assign_bug(self, ticket_id: str, assignee: str, reason: Optional[str] = None) -> bool:
225
+ """Assign a bug to a developer.
226
+
227
+ Args:
228
+ ticket_id: Bug ticket ID
229
+ assignee: Developer to assign to
230
+ reason: Optional reason for assignment
231
+
232
+ Returns:
233
+ True if successful
234
+ """
235
+ try:
236
+ self.ticket_backend.update_ticket(ticket_id, assignee=assignee)
237
+
238
+ if reason:
239
+ self.ticket_backend.add_comment(
240
+ ticket_id,
241
+ f"Assigned to {assignee}\nReason: {reason}",
242
+ author="bug-triage-system"
243
+ )
244
+
245
+ return True
246
+ except Exception:
247
+ return False
248
+
249
+ def escalate_bug(self, ticket_id: str, reason: str) -> bool:
250
+ """Escalate a bug to higher priority.
251
+
252
+ Args:
253
+ ticket_id: Bug ticket ID
254
+ reason: Escalation reason
255
+
256
+ Returns:
257
+ True if successful
258
+ """
259
+ try:
260
+ # Increase priority
261
+ ticket = self.ticket_backend.fetch_ticket(ticket_id)
262
+
263
+ priority_escalation = {
264
+ "low": "medium",
265
+ "medium": "high",
266
+ "high": "critical"
267
+ }
268
+
269
+ new_priority = priority_escalation.get(ticket.priority, "critical")
270
+
271
+ self.ticket_backend.update_ticket(ticket_id, priority=new_priority)
272
+ self.ticket_backend.add_comment(
273
+ ticket_id,
274
+ f"🚨 Bug escalated to {new_priority} priority\nReason: {reason}",
275
+ author="bug-triage-system"
276
+ )
277
+
278
+ # Notify about escalation
279
+ if self.notifier:
280
+ self.notifier.send(
281
+ f"Bug Escalated: {ticket_id}",
282
+ f"Priority: {new_priority}\nReason: {reason}",
283
+ priority=NotificationPriority.URGENT
284
+ )
285
+
286
+ return True
287
+ except Exception:
288
+ return False
289
+
290
+ def _build_triage_prompt(self, bug_report: BugReport) -> str:
291
+ """Build prompt for AI triage analysis."""
292
+ prompt = f"""Analyze this bug report and provide triage classification.
293
+
294
+ **Bug Title:** {bug_report.title}
295
+
296
+ **Description:**
297
+ {bug_report.description}
298
+
299
+ **Steps to Reproduce:**
300
+ {chr(10).join(f'{i+1}. {step}' for i, step in enumerate(bug_report.steps_to_reproduce))}
301
+
302
+ **Expected Behavior:** {bug_report.expected_behavior}
303
+ **Actual Behavior:** {bug_report.actual_behavior}
304
+
305
+ """
306
+
307
+ if bug_report.stack_trace:
308
+ prompt += f"\n**Stack Trace:**\n```\n{bug_report.stack_trace[:500]}\n```\n"
309
+
310
+ if bug_report.environment:
311
+ prompt += f"\n**Environment:** {bug_report.environment}"
312
+
313
+ prompt += """
314
+
315
+ Provide triage analysis in this format:
316
+
317
+ SEVERITY: [critical/high/medium/low/trivial]
318
+ CATEGORY: [crash/data-loss/security/performance/ui-ux/integration/functionality/documentation/other]
319
+ PRIORITY: [critical/high/medium/low]
320
+ REASONING: [Brief explanation of classification]
321
+ SUGGESTED_ACTION: [Immediate action needed, if any]
322
+ """
323
+
324
+ return prompt
325
+
326
+ def _parse_triage_response(self, response: str) -> Dict[str, Any]:
327
+ """Parse AI triage response."""
328
+ import re
329
+
330
+ result = {}
331
+
332
+ # Extract severity
333
+ severity_match = re.search(r'SEVERITY:\s*(\w+)', response, re.IGNORECASE)
334
+ if severity_match:
335
+ try:
336
+ result['severity'] = BugSeverity(severity_match.group(1).lower())
337
+ except ValueError:
338
+ result['severity'] = BugSeverity.MEDIUM
339
+
340
+ # Extract category
341
+ category_match = re.search(r'CATEGORY:\s*([\w-]+)', response, re.IGNORECASE)
342
+ if category_match:
343
+ try:
344
+ result['category'] = BugCategory(category_match.group(1).lower())
345
+ except ValueError:
346
+ result['category'] = BugCategory.OTHER
347
+
348
+ # Extract priority
349
+ priority_match = re.search(r'PRIORITY:\s*(\w+)', response, re.IGNORECASE)
350
+ if priority_match:
351
+ result['priority'] = priority_match.group(1).lower()
352
+
353
+ return result
354
+
355
+ def _fallback_triage(self, bug_report: BugReport) -> BugReport:
356
+ """Fallback triage based on keywords."""
357
+ title_lower = bug_report.title.lower()
358
+ desc_lower = bug_report.description.lower()
359
+
360
+ # Check for critical keywords
361
+ critical_keywords = ['crash', 'data loss', 'security', 'breach', 'exploit', 'down']
362
+ high_keywords = ['broken', 'error', 'fail', 'cannot', "doesn't work"]
363
+
364
+ if any(kw in title_lower or kw in desc_lower for kw in critical_keywords):
365
+ bug_report.severity = BugSeverity.CRITICAL
366
+ bug_report.priority = "critical"
367
+ elif any(kw in title_lower or kw in desc_lower for kw in high_keywords):
368
+ bug_report.severity = BugSeverity.HIGH
369
+ bug_report.priority = "high"
370
+ else:
371
+ bug_report.severity = BugSeverity.MEDIUM
372
+ bug_report.priority = "medium"
373
+
374
+ # Categorize
375
+ if 'crash' in title_lower or 'crash' in desc_lower:
376
+ bug_report.category = BugCategory.CRASH
377
+ elif 'security' in title_lower or 'security' in desc_lower:
378
+ bug_report.category = BugCategory.SECURITY
379
+ elif 'slow' in title_lower or 'performance' in desc_lower:
380
+ bug_report.category = BugCategory.PERFORMANCE
381
+ else:
382
+ bug_report.category = BugCategory.FUNCTIONALITY
383
+
384
+ bug_report.triaged = True
385
+ return bug_report
386
+
387
+ def _format_bug_description(self, bug_report: BugReport) -> str:
388
+ """Format bug report into ticket description."""
389
+ desc = f"{bug_report.description}\n\n"
390
+ desc += "## Reproduction Steps\n"
391
+ for i, step in enumerate(bug_report.steps_to_reproduce, 1):
392
+ desc += f"{i}. {step}\n"
393
+
394
+ desc += f"\n## Expected Behavior\n{bug_report.expected_behavior}\n"
395
+ desc += f"\n## Actual Behavior\n{bug_report.actual_behavior}\n"
396
+
397
+ if bug_report.environment:
398
+ desc += f"\n## Environment\n{bug_report.environment}"
399
+
400
+ if bug_report.version:
401
+ desc += f"\nVersion: {bug_report.version}"
402
+
403
+ if bug_report.platform:
404
+ desc += f"\nPlatform: {bug_report.platform}"
405
+
406
+ return desc
407
+
408
+ def _get_bug_labels(self, bug_report: BugReport) -> List[str]:
409
+ """Generate labels for bug ticket."""
410
+ labels = ["bug"]
411
+
412
+ if bug_report.severity:
413
+ labels.append(f"severity-{bug_report.severity.value}")
414
+
415
+ if bug_report.category:
416
+ labels.append(f"category-{bug_report.category.value}")
417
+
418
+ if bug_report.environment:
419
+ labels.append(f"env-{bug_report.environment}")
420
+
421
+ return labels
422
+
423
+ def _add_technical_details(self, ticket_id: str, bug_report: BugReport) -> None:
424
+ """Add technical details as comment."""
425
+ details = "## Technical Details\n\n"
426
+
427
+ if bug_report.stack_trace:
428
+ details += "### Stack Trace\n```\n"
429
+ details += bug_report.stack_trace
430
+ details += "\n```\n\n"
431
+
432
+ if bug_report.logs:
433
+ details += "### Logs\n```\n"
434
+ details += bug_report.logs
435
+ details += "\n```\n"
436
+
437
+ self.ticket_backend.add_comment(ticket_id, details, author="bug-reporter")
438
+
439
+ def _notify_critical_bug(self, bug_report: BugReport) -> None:
440
+ """Send notification for critical/high severity bugs."""
441
+ if not self.notifier:
442
+ return
443
+
444
+ severity_emoji = {
445
+ BugSeverity.CRITICAL: "🚨",
446
+ BugSeverity.HIGH: "⚠️"
447
+ }
448
+
449
+ emoji = severity_emoji.get(bug_report.severity, "🐛")
450
+
451
+ self.notifier.send(
452
+ f"{emoji} {bug_report.severity.value.upper()} Bug Reported",
453
+ f"Title: {bug_report.title}\n"
454
+ f"Category: {bug_report.category.value if bug_report.category else 'unknown'}\n"
455
+ f"Environment: {bug_report.environment or 'unknown'}",
456
+ priority=NotificationPriority.URGENT if bug_report.severity == BugSeverity.CRITICAL else NotificationPriority.HIGH,
457
+ tags=["bug", "alert"]
458
+ )