mcp-ticketer 0.4.11__py3-none-any.whl → 2.0.1__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 mcp-ticketer might be problematic. Click here for more details.

Files changed (111) hide show
  1. mcp_ticketer/__init__.py +10 -10
  2. mcp_ticketer/__version__.py +3 -3
  3. mcp_ticketer/adapters/__init__.py +2 -0
  4. mcp_ticketer/adapters/aitrackdown.py +394 -9
  5. mcp_ticketer/adapters/asana/__init__.py +15 -0
  6. mcp_ticketer/adapters/asana/adapter.py +1416 -0
  7. mcp_ticketer/adapters/asana/client.py +292 -0
  8. mcp_ticketer/adapters/asana/mappers.py +348 -0
  9. mcp_ticketer/adapters/asana/types.py +146 -0
  10. mcp_ticketer/adapters/github.py +836 -105
  11. mcp_ticketer/adapters/hybrid.py +47 -5
  12. mcp_ticketer/adapters/jira.py +772 -1
  13. mcp_ticketer/adapters/linear/adapter.py +2293 -108
  14. mcp_ticketer/adapters/linear/client.py +146 -12
  15. mcp_ticketer/adapters/linear/mappers.py +105 -11
  16. mcp_ticketer/adapters/linear/queries.py +168 -1
  17. mcp_ticketer/adapters/linear/types.py +80 -4
  18. mcp_ticketer/analysis/__init__.py +56 -0
  19. mcp_ticketer/analysis/dependency_graph.py +255 -0
  20. mcp_ticketer/analysis/health_assessment.py +304 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/project_status.py +594 -0
  23. mcp_ticketer/analysis/similarity.py +224 -0
  24. mcp_ticketer/analysis/staleness.py +266 -0
  25. mcp_ticketer/automation/__init__.py +11 -0
  26. mcp_ticketer/automation/project_updates.py +378 -0
  27. mcp_ticketer/cache/memory.py +3 -3
  28. mcp_ticketer/cli/adapter_diagnostics.py +4 -2
  29. mcp_ticketer/cli/auggie_configure.py +18 -6
  30. mcp_ticketer/cli/codex_configure.py +175 -60
  31. mcp_ticketer/cli/configure.py +884 -146
  32. mcp_ticketer/cli/cursor_configure.py +314 -0
  33. mcp_ticketer/cli/diagnostics.py +31 -28
  34. mcp_ticketer/cli/discover.py +293 -21
  35. mcp_ticketer/cli/gemini_configure.py +18 -6
  36. mcp_ticketer/cli/init_command.py +880 -0
  37. mcp_ticketer/cli/instruction_commands.py +435 -0
  38. mcp_ticketer/cli/linear_commands.py +99 -15
  39. mcp_ticketer/cli/main.py +109 -2055
  40. mcp_ticketer/cli/mcp_configure.py +673 -99
  41. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  42. mcp_ticketer/cli/migrate_config.py +12 -8
  43. mcp_ticketer/cli/platform_commands.py +6 -6
  44. mcp_ticketer/cli/platform_detection.py +477 -0
  45. mcp_ticketer/cli/platform_installer.py +536 -0
  46. mcp_ticketer/cli/project_update_commands.py +350 -0
  47. mcp_ticketer/cli/queue_commands.py +15 -15
  48. mcp_ticketer/cli/setup_command.py +639 -0
  49. mcp_ticketer/cli/simple_health.py +13 -11
  50. mcp_ticketer/cli/ticket_commands.py +277 -36
  51. mcp_ticketer/cli/update_checker.py +313 -0
  52. mcp_ticketer/cli/utils.py +45 -41
  53. mcp_ticketer/core/__init__.py +35 -1
  54. mcp_ticketer/core/adapter.py +170 -5
  55. mcp_ticketer/core/config.py +38 -31
  56. mcp_ticketer/core/env_discovery.py +33 -3
  57. mcp_ticketer/core/env_loader.py +7 -6
  58. mcp_ticketer/core/exceptions.py +10 -4
  59. mcp_ticketer/core/http_client.py +10 -10
  60. mcp_ticketer/core/instructions.py +405 -0
  61. mcp_ticketer/core/label_manager.py +732 -0
  62. mcp_ticketer/core/mappers.py +32 -20
  63. mcp_ticketer/core/models.py +136 -1
  64. mcp_ticketer/core/onepassword_secrets.py +379 -0
  65. mcp_ticketer/core/priority_matcher.py +463 -0
  66. mcp_ticketer/core/project_config.py +148 -14
  67. mcp_ticketer/core/registry.py +1 -1
  68. mcp_ticketer/core/session_state.py +171 -0
  69. mcp_ticketer/core/state_matcher.py +592 -0
  70. mcp_ticketer/core/url_parser.py +425 -0
  71. mcp_ticketer/core/validators.py +69 -0
  72. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  73. mcp_ticketer/mcp/__init__.py +2 -2
  74. mcp_ticketer/mcp/server/__init__.py +2 -2
  75. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  76. mcp_ticketer/mcp/server/main.py +187 -93
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +58 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +37 -9
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +854 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +65 -20
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +259 -202
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +74 -12
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1429 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +878 -319
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +295 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +942 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +3 -7
  90. mcp_ticketer/mcp/server/tools/project_status_tools.py +158 -0
  91. mcp_ticketer/mcp/server/tools/project_update_tools.py +473 -0
  92. mcp_ticketer/mcp/server/tools/search_tools.py +180 -97
  93. mcp_ticketer/mcp/server/tools/session_tools.py +308 -0
  94. mcp_ticketer/mcp/server/tools/ticket_tools.py +1182 -82
  95. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +364 -0
  96. mcp_ticketer/queue/health_monitor.py +1 -0
  97. mcp_ticketer/queue/manager.py +4 -4
  98. mcp_ticketer/queue/queue.py +3 -3
  99. mcp_ticketer/queue/run_worker.py +1 -1
  100. mcp_ticketer/queue/ticket_registry.py +2 -2
  101. mcp_ticketer/queue/worker.py +15 -13
  102. mcp_ticketer/utils/__init__.py +5 -0
  103. mcp_ticketer/utils/token_utils.py +246 -0
  104. mcp_ticketer-2.0.1.dist-info/METADATA +1366 -0
  105. mcp_ticketer-2.0.1.dist-info/RECORD +122 -0
  106. mcp_ticketer-0.4.11.dist-info/METADATA +0 -496
  107. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  108. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/WHEEL +0 -0
  109. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/entry_points.txt +0 -0
  110. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,350 @@
1
+ """CLI commands for project updates (Linear project status updates)."""
2
+
3
+ import asyncio
4
+ from datetime import datetime
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from ..core.models import ProjectUpdateHealth
12
+
13
+ app = typer.Typer(name="project-update", help="Project status update management")
14
+ console = Console()
15
+
16
+
17
+ def _format_health_indicator(health: ProjectUpdateHealth | str | None) -> str:
18
+ """Format health status with color-coded indicators.
19
+
20
+ Args:
21
+ health: Health status enum or string value
22
+
23
+ Returns:
24
+ Formatted health indicator with color and icon
25
+ """
26
+ if not health:
27
+ return "[dim]○ Not Set[/dim]"
28
+
29
+ # Handle both enum and string values
30
+ health_str = health.value if isinstance(health, ProjectUpdateHealth) else health
31
+
32
+ health_indicators = {
33
+ "on_track": "[green]✓ On Track[/green]",
34
+ "at_risk": "[yellow]⚠ At Risk[/yellow]",
35
+ "off_track": "[red]✗ Off Track[/red]",
36
+ "complete": "[blue]✓ Complete[/blue]",
37
+ "inactive": "[dim]○ Inactive[/dim]",
38
+ }
39
+
40
+ return health_indicators.get(health_str, f"[dim]{health_str}[/dim]")
41
+
42
+
43
+ def _format_relative_time(dt: datetime) -> str:
44
+ """Format datetime as relative time string.
45
+
46
+ Args:
47
+ dt: Datetime to format
48
+
49
+ Returns:
50
+ Human-readable relative time (e.g., "2 hours ago")
51
+ """
52
+ try:
53
+ from humanize import naturaltime
54
+
55
+ return naturaltime(dt)
56
+ except ImportError:
57
+ # Fallback to ISO format if humanize not available
58
+ return dt.strftime("%Y-%m-%d %H:%M:%S")
59
+
60
+
61
+ async def _create_update_async(
62
+ project_id: str,
63
+ body: str,
64
+ health: ProjectUpdateHealth | None = None,
65
+ ) -> None:
66
+ """Async implementation of create command."""
67
+ from .main import get_adapter
68
+
69
+ try:
70
+ adapter = get_adapter()
71
+
72
+ # Check if adapter supports project updates
73
+ if not hasattr(adapter, "create_project_update"):
74
+ adapter_name = getattr(adapter, "adapter_name", type(adapter).__name__)
75
+ console.print(
76
+ f"[red]✗[/red] Adapter '{adapter_name}' does not support project updates"
77
+ )
78
+ console.print(
79
+ "[dim]Project updates are supported by: Linear, GitHub V2, Asana[/dim]"
80
+ )
81
+ raise typer.Exit(1) from None
82
+
83
+ # Validate body is not empty
84
+ if not body or not body.strip():
85
+ console.print("[red]✗[/red] Update body cannot be empty")
86
+ raise typer.Exit(1) from None
87
+
88
+ # Create the project update
89
+ console.print(f"[dim]Creating project update for '{project_id}'...[/dim]")
90
+ update = await adapter.create_project_update(
91
+ project_id=project_id,
92
+ body=body,
93
+ health=health,
94
+ )
95
+
96
+ # Display success message with update details
97
+ console.print("\n[green]✓[/green] Project update created successfully!")
98
+ console.print(f" Update ID: [cyan]{update.id}[/cyan]")
99
+ console.print(
100
+ f" Project: [bold]{update.project_name or update.project_id}[/bold]"
101
+ )
102
+ console.print(f" Health: {_format_health_indicator(update.health)}")
103
+ console.print(f" Created: {update.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
104
+
105
+ if update.url:
106
+ console.print(f" URL: [link]{update.url}[/link]")
107
+
108
+ # Show preview of body
109
+ preview = body[:100] + "..." if len(body) > 100 else body
110
+ console.print(f" Body Preview: {preview}")
111
+
112
+ except ValueError as e:
113
+ console.print(f"[red]✗[/red] {str(e)}")
114
+ raise typer.Exit(1) from e
115
+ except Exception as e:
116
+ console.print(f"[red]✗[/red] Unexpected error: {str(e)}")
117
+ raise typer.Exit(1) from e
118
+
119
+
120
+ async def _list_updates_async(
121
+ project_id: str,
122
+ limit: int = 10,
123
+ ) -> None:
124
+ """Async implementation of list command."""
125
+ from .main import get_adapter
126
+
127
+ try:
128
+ adapter = get_adapter()
129
+
130
+ # Check if adapter supports project updates
131
+ if not hasattr(adapter, "list_project_updates"):
132
+ adapter_name = getattr(adapter, "adapter_name", type(adapter).__name__)
133
+ console.print(
134
+ f"[red]✗[/red] Adapter '{adapter_name}' does not support project updates"
135
+ )
136
+ raise typer.Exit(1) from None
137
+
138
+ # List project updates
139
+ console.print(f"[dim]Fetching updates for project '{project_id}'...[/dim]")
140
+ updates = await adapter.list_project_updates(
141
+ project_id=project_id,
142
+ limit=limit,
143
+ )
144
+
145
+ if not updates:
146
+ console.print(
147
+ f"\n[yellow]No updates found for project '{project_id}'[/yellow]"
148
+ )
149
+ return
150
+
151
+ # Create Rich table
152
+ table = Table(title=f"Project Updates ({len(updates)} total)")
153
+ table.add_column("ID", style="cyan", no_wrap=True)
154
+ table.add_column("Date", style="dim")
155
+ table.add_column("Health", no_wrap=True)
156
+ table.add_column("Author", style="blue")
157
+ table.add_column("Preview", style="white")
158
+
159
+ for update in updates:
160
+ # Format date
161
+ date_str = (
162
+ _format_relative_time(update.created_at)
163
+ if update.created_at
164
+ else "Unknown"
165
+ )
166
+
167
+ # Format health
168
+ health_str = _format_health_indicator(update.health)
169
+
170
+ # Format author
171
+ author = update.author_name or update.author_id or "Unknown"
172
+
173
+ # Preview (first 50 chars of body)
174
+ preview = update.body[:50] + "..." if len(update.body) > 50 else update.body
175
+
176
+ table.add_row(
177
+ update.id[:8] + "..." if len(update.id) > 8 else update.id,
178
+ date_str,
179
+ health_str,
180
+ author,
181
+ preview,
182
+ )
183
+
184
+ console.print(table)
185
+
186
+ except ValueError as e:
187
+ console.print(f"[red]✗[/red] {str(e)}")
188
+ raise typer.Exit(1) from e
189
+ except Exception as e:
190
+ console.print(f"[red]✗[/red] Unexpected error: {str(e)}")
191
+ raise typer.Exit(1) from e
192
+
193
+
194
+ async def _get_update_async(update_id: str) -> None:
195
+ """Async implementation of get command."""
196
+ from .main import get_adapter
197
+
198
+ try:
199
+ adapter = get_adapter()
200
+
201
+ # Check if adapter supports project updates
202
+ if not hasattr(adapter, "get_project_update"):
203
+ adapter_name = getattr(adapter, "adapter_name", type(adapter).__name__)
204
+ console.print(
205
+ f"[red]✗[/red] Adapter '{adapter_name}' does not support project updates"
206
+ )
207
+ raise typer.Exit(1) from None
208
+
209
+ # Get project update
210
+ console.print(f"[dim]Fetching update '{update_id}'...[/dim]")
211
+ update = await adapter.get_project_update(update_id=update_id)
212
+
213
+ # Build detailed display content
214
+ content_lines = [
215
+ f"[bold]Update ID:[/bold] {update.id}",
216
+ f"[bold]Project:[/bold] {update.project_name or update.project_id}",
217
+ f"[bold]Health:[/bold] {_format_health_indicator(update.health)}",
218
+ f"[bold]Created:[/bold] {update.created_at.strftime('%Y-%m-%d %H:%M:%S')}",
219
+ ]
220
+
221
+ if update.author_name or update.author_id:
222
+ author = update.author_name or update.author_id
223
+ content_lines.append(f"[bold]Author:[/bold] {author}")
224
+
225
+ if update.url:
226
+ content_lines.append(f"[bold]URL:[/bold] [link]{update.url}[/link]")
227
+
228
+ # Add body
229
+ content_lines.append("")
230
+ content_lines.append("[bold]Body:[/bold]")
231
+ content_lines.append(update.body)
232
+
233
+ # Add Linear-specific fields if present
234
+ if update.diff_markdown:
235
+ content_lines.append("")
236
+ content_lines.append("[bold]Diff (Changes Since Last Update):[/bold]")
237
+ content_lines.append(update.diff_markdown)
238
+
239
+ if update.is_stale is not None:
240
+ stale_indicator = (
241
+ "[red]⚠ Stale[/red]" if update.is_stale else "[green]✓ Current[/green]"
242
+ )
243
+ content_lines.append("")
244
+ content_lines.append(f"[bold]Freshness:[/bold] {stale_indicator}")
245
+
246
+ # Display as Rich panel
247
+ panel = Panel(
248
+ "\n".join(content_lines),
249
+ title="[bold]Project Update Details[/bold]",
250
+ border_style="cyan",
251
+ expand=False,
252
+ )
253
+
254
+ console.print(panel)
255
+
256
+ except ValueError as e:
257
+ console.print(f"[red]✗[/red] {str(e)}")
258
+ raise typer.Exit(1) from e
259
+ except Exception as e:
260
+ console.print(f"[red]✗[/red] Unexpected error: {str(e)}")
261
+ raise typer.Exit(1) from e
262
+
263
+
264
+ @app.command("create")
265
+ def create_update(
266
+ project_id: str = typer.Argument(
267
+ ...,
268
+ help="Project identifier (UUID, slugId, short ID, or URL)",
269
+ ),
270
+ body: str = typer.Argument(
271
+ ...,
272
+ help="Update content (markdown formatted)",
273
+ ),
274
+ health: ProjectUpdateHealth | None = typer.Option(
275
+ None,
276
+ "--health",
277
+ "-h",
278
+ help="Project health status (on_track, at_risk, off_track, complete, inactive)",
279
+ ),
280
+ ) -> None:
281
+ """Create a new project status update.
282
+
283
+ Creates a status update for a project with optional health indicator.
284
+ Linear projects will automatically generate a diff showing changes.
285
+
286
+ Examples:
287
+ # Create update with health status
288
+ mcp-ticket project-update create "PROJ-123" "Sprint completed successfully" --health on_track
289
+
290
+ # Create update using project URL
291
+ mcp-ticket project-update create "https://linear.app/team/project/proj-slug" "Weekly update"
292
+
293
+ # Create update without health indicator
294
+ mcp-ticket project-update create "mcp-ticketer-eac28953c267" "Development ongoing"
295
+ """
296
+ asyncio.run(_create_update_async(project_id, body, health))
297
+
298
+
299
+ @app.command("list")
300
+ def list_updates(
301
+ project_id: str = typer.Argument(
302
+ ...,
303
+ help="Project identifier (UUID, slugId, short ID, or URL)",
304
+ ),
305
+ limit: int = typer.Option(
306
+ 10,
307
+ "--limit",
308
+ "-l",
309
+ help="Maximum number of updates to return",
310
+ min=1,
311
+ max=100,
312
+ ),
313
+ ) -> None:
314
+ """List recent project updates.
315
+
316
+ Retrieves recent status updates for a project, ordered by creation date (newest first).
317
+
318
+ Examples:
319
+ # List last 10 updates (default)
320
+ mcp-ticket project-update list "PROJ-123"
321
+
322
+ # List last 20 updates
323
+ mcp-ticket project-update list "PROJ-123" --limit 20
324
+
325
+ # List updates using project URL
326
+ mcp-ticket project-update list "https://linear.app/team/project/proj-slug"
327
+ """
328
+ asyncio.run(_list_updates_async(project_id, limit))
329
+
330
+
331
+ @app.command("get")
332
+ def get_update(
333
+ update_id: str = typer.Argument(
334
+ ...,
335
+ help="Project update identifier (UUID)",
336
+ ),
337
+ ) -> None:
338
+ """Get detailed information about a specific project update.
339
+
340
+ Retrieves full details including body, health status, author, and Linear-specific
341
+ fields like auto-generated diffs and staleness indicators.
342
+
343
+ Examples:
344
+ # Get update details
345
+ mcp-ticket project-update get "update-uuid-here"
346
+
347
+ # View update with full diff
348
+ mcp-ticket project-update get "abc123def456"
349
+ """
350
+ asyncio.run(_get_update_async(update_id))
@@ -16,7 +16,7 @@ console = Console()
16
16
  def list_queue(
17
17
  status: QueueStatus = typer.Option(None, "--status", "-s", help="Filter by status"),
18
18
  limit: int = typer.Option(25, "--limit", "-l", help="Maximum items to show"),
19
- ):
19
+ ) -> None:
20
20
  """List queue items."""
21
21
  queue = Queue()
22
22
  items = queue.list_items(status=status, limit=limit)
@@ -69,20 +69,20 @@ def list_queue(
69
69
 
70
70
 
71
71
  @app.command("retry")
72
- def retry_item(queue_id: str = typer.Argument(..., help="Queue ID to retry")):
72
+ def retry_item(queue_id: str = typer.Argument(..., help="Queue ID to retry")) -> None:
73
73
  """Retry a failed queue item."""
74
74
  queue = Queue()
75
75
  item = queue.get_item(queue_id)
76
76
 
77
77
  if not item:
78
78
  console.print(f"[red]Queue item not found: {queue_id}[/red]")
79
- raise typer.Exit(1)
79
+ raise typer.Exit(1) from None
80
80
 
81
81
  if item.status != QueueStatus.FAILED:
82
82
  console.print(
83
83
  f"[yellow]Item {queue_id} is not failed (status: {item.status})[/yellow]"
84
84
  )
85
- raise typer.Exit(1)
85
+ raise typer.Exit(1) from None
86
86
 
87
87
  # Reset to pending
88
88
  queue.update_status(queue_id, QueueStatus.PENDING, error_message=None)
@@ -103,7 +103,7 @@ def clear_queue(
103
103
  7, "--days", "-d", help="Clear items older than this many days"
104
104
  ),
105
105
  confirm: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
106
- ):
106
+ ) -> None:
107
107
  """Clear old queue items."""
108
108
  queue = Queue()
109
109
 
@@ -115,7 +115,7 @@ def clear_queue(
115
115
 
116
116
  if not typer.confirm(msg):
117
117
  console.print("[yellow]Cancelled[/yellow]")
118
- raise typer.Exit(0)
118
+ raise typer.Exit(0) from None
119
119
 
120
120
  queue.cleanup_old(days=days)
121
121
  console.print("[green]✓[/green] Cleared old queue items")
@@ -126,7 +126,7 @@ worker_app = typer.Typer(name="worker", help="Worker management commands")
126
126
 
127
127
 
128
128
  @worker_app.command("start")
129
- def start_worker():
129
+ def start_worker() -> None:
130
130
  """Start the background worker."""
131
131
  manager = WorkerManager()
132
132
 
@@ -142,11 +142,11 @@ def start_worker():
142
142
  console.print(f"PID: {status.get('pid')}")
143
143
  else:
144
144
  console.print("[red]✗[/red] Failed to start worker")
145
- raise typer.Exit(1)
145
+ raise typer.Exit(1) from None
146
146
 
147
147
 
148
148
  @worker_app.command("stop")
149
- def stop_worker():
149
+ def stop_worker() -> None:
150
150
  """Stop the background worker."""
151
151
  manager = WorkerManager()
152
152
 
@@ -158,11 +158,11 @@ def stop_worker():
158
158
  console.print("[green]✓[/green] Worker stopped successfully")
159
159
  else:
160
160
  console.print("[red]✗[/red] Failed to stop worker")
161
- raise typer.Exit(1)
161
+ raise typer.Exit(1) from None
162
162
 
163
163
 
164
164
  @worker_app.command("restart")
165
- def restart_worker():
165
+ def restart_worker() -> None:
166
166
  """Restart the background worker."""
167
167
  manager = WorkerManager()
168
168
 
@@ -172,11 +172,11 @@ def restart_worker():
172
172
  console.print(f"PID: {status.get('pid')}")
173
173
  else:
174
174
  console.print("[red]✗[/red] Failed to restart worker")
175
- raise typer.Exit(1)
175
+ raise typer.Exit(1) from None
176
176
 
177
177
 
178
178
  @worker_app.command("status")
179
- def worker_status():
179
+ def worker_status() -> None:
180
180
  """Check worker status."""
181
181
  manager = WorkerManager()
182
182
  status = manager.get_status()
@@ -212,7 +212,7 @@ def worker_status():
212
212
  def worker_logs(
213
213
  lines: int = typer.Option(50, "--lines", "-n", help="Number of lines to show"),
214
214
  follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output"),
215
- ):
215
+ ) -> None:
216
216
  """View worker logs."""
217
217
  import time
218
218
  from pathlib import Path
@@ -221,7 +221,7 @@ def worker_logs(
221
221
 
222
222
  if not log_file.exists():
223
223
  console.print("[yellow]No log file found[/yellow]")
224
- raise typer.Exit(1)
224
+ raise typer.Exit(1) from None
225
225
 
226
226
  if follow:
227
227
  # Follow mode - like tail -f