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

Files changed (70) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +9 -3
  4. mcp_ticketer/adapters/asana/__init__.py +15 -0
  5. mcp_ticketer/adapters/asana/adapter.py +1308 -0
  6. mcp_ticketer/adapters/asana/client.py +292 -0
  7. mcp_ticketer/adapters/asana/mappers.py +334 -0
  8. mcp_ticketer/adapters/asana/types.py +146 -0
  9. mcp_ticketer/adapters/github.py +313 -96
  10. mcp_ticketer/adapters/jira.py +251 -1
  11. mcp_ticketer/adapters/linear/adapter.py +524 -22
  12. mcp_ticketer/adapters/linear/client.py +61 -9
  13. mcp_ticketer/adapters/linear/mappers.py +9 -3
  14. mcp_ticketer/cache/memory.py +3 -3
  15. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  16. mcp_ticketer/cli/auggie_configure.py +1 -1
  17. mcp_ticketer/cli/codex_configure.py +80 -1
  18. mcp_ticketer/cli/configure.py +33 -43
  19. mcp_ticketer/cli/diagnostics.py +18 -16
  20. mcp_ticketer/cli/discover.py +288 -21
  21. mcp_ticketer/cli/gemini_configure.py +1 -1
  22. mcp_ticketer/cli/instruction_commands.py +429 -0
  23. mcp_ticketer/cli/linear_commands.py +99 -15
  24. mcp_ticketer/cli/main.py +1199 -227
  25. mcp_ticketer/cli/mcp_configure.py +1 -1
  26. mcp_ticketer/cli/migrate_config.py +12 -8
  27. mcp_ticketer/cli/platform_commands.py +6 -6
  28. mcp_ticketer/cli/platform_detection.py +412 -0
  29. mcp_ticketer/cli/queue_commands.py +15 -15
  30. mcp_ticketer/cli/simple_health.py +1 -1
  31. mcp_ticketer/cli/ticket_commands.py +14 -13
  32. mcp_ticketer/cli/update_checker.py +313 -0
  33. mcp_ticketer/cli/utils.py +45 -41
  34. mcp_ticketer/core/__init__.py +12 -0
  35. mcp_ticketer/core/adapter.py +4 -4
  36. mcp_ticketer/core/config.py +17 -10
  37. mcp_ticketer/core/env_discovery.py +33 -3
  38. mcp_ticketer/core/env_loader.py +7 -6
  39. mcp_ticketer/core/exceptions.py +3 -3
  40. mcp_ticketer/core/http_client.py +10 -10
  41. mcp_ticketer/core/instructions.py +405 -0
  42. mcp_ticketer/core/mappers.py +1 -1
  43. mcp_ticketer/core/models.py +1 -1
  44. mcp_ticketer/core/onepassword_secrets.py +379 -0
  45. mcp_ticketer/core/project_config.py +17 -1
  46. mcp_ticketer/core/registry.py +1 -1
  47. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  48. mcp_ticketer/mcp/__init__.py +2 -2
  49. mcp_ticketer/mcp/server/__init__.py +2 -2
  50. mcp_ticketer/mcp/server/main.py +82 -69
  51. mcp_ticketer/mcp/server/tools/__init__.py +9 -0
  52. mcp_ticketer/mcp/server/tools/attachment_tools.py +63 -16
  53. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  54. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +154 -5
  55. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  56. mcp_ticketer/mcp/server/tools/ticket_tools.py +157 -4
  57. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  58. mcp_ticketer/queue/health_monitor.py +1 -0
  59. mcp_ticketer/queue/manager.py +4 -4
  60. mcp_ticketer/queue/queue.py +3 -3
  61. mcp_ticketer/queue/run_worker.py +1 -1
  62. mcp_ticketer/queue/ticket_registry.py +2 -2
  63. mcp_ticketer/queue/worker.py +14 -12
  64. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +106 -52
  65. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  66. mcp_ticketer-0.4.11.dist-info/RECORD +0 -77
  67. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  68. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  69. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  70. {mcp_ticketer-0.4.11.dist-info → mcp_ticketer-0.12.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,429 @@
1
+ """Ticket instructions management commands.
2
+
3
+ This module implements CLI commands for managing ticket writing instructions,
4
+ allowing users to customize and view the guidelines that help create
5
+ well-structured, consistent tickets.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.markdown import Markdown
14
+ from rich.panel import Panel
15
+
16
+ from ..core.instructions import (
17
+ InstructionsError,
18
+ InstructionsValidationError,
19
+ TicketInstructionsManager,
20
+ )
21
+
22
+ app = typer.Typer(
23
+ name="instructions",
24
+ help="Manage ticket writing instructions for your project",
25
+ )
26
+ console = Console()
27
+
28
+
29
+ @app.command()
30
+ def show(
31
+ default: bool = typer.Option(
32
+ False,
33
+ "--default",
34
+ help="Show default instructions instead of custom",
35
+ ),
36
+ raw: bool = typer.Option(
37
+ False,
38
+ "--raw",
39
+ help="Output raw markdown without formatting",
40
+ ),
41
+ ) -> None:
42
+ """Display current ticket writing instructions.
43
+
44
+ By default, shows custom instructions if they exist, otherwise shows defaults.
45
+ Use --default to always show the default instructions.
46
+ Use --raw to output raw markdown without Rich formatting (useful for piping).
47
+
48
+ Examples:
49
+ # Show current instructions (custom or default)
50
+ mcp-ticketer instructions show
51
+
52
+ # Always show default instructions
53
+ mcp-ticketer instructions show --default
54
+
55
+ # Output raw markdown for piping
56
+ mcp-ticketer instructions show --raw > team_guide.md
57
+
58
+ """
59
+ try:
60
+ manager = TicketInstructionsManager()
61
+
62
+ if default:
63
+ instructions = manager.get_default_instructions()
64
+ source = "default"
65
+ else:
66
+ instructions = manager.get_instructions()
67
+ source = "custom" if manager.has_custom_instructions() else "default"
68
+
69
+ if raw:
70
+ # Raw output for piping
71
+ console.print(instructions)
72
+ else:
73
+ # Rich formatted output
74
+ if source == "custom":
75
+ title = f"[green]Custom Instructions[/green] ({manager.get_instructions_path()})"
76
+ else:
77
+ title = "[blue]Default Instructions[/blue]"
78
+
79
+ panel = Panel(
80
+ Markdown(instructions),
81
+ title=title,
82
+ border_style="cyan",
83
+ )
84
+ console.print(panel)
85
+
86
+ except InstructionsError as e:
87
+ console.print(f"[red]Error:[/red] {e}")
88
+ raise typer.Exit(1) from None
89
+
90
+
91
+ @app.command()
92
+ def add(
93
+ file_path: str | None = typer.Argument(
94
+ None,
95
+ help="Path to markdown file with custom instructions",
96
+ ),
97
+ stdin: bool = typer.Option(
98
+ False,
99
+ "--stdin",
100
+ help="Read instructions from stdin instead of file",
101
+ ),
102
+ force: bool = typer.Option(
103
+ False,
104
+ "--force",
105
+ "-f",
106
+ help="Overwrite existing custom instructions without confirmation",
107
+ ),
108
+ ) -> None:
109
+ """Add custom ticket writing instructions for your project.
110
+
111
+ You can provide instructions from a file or via stdin. If custom instructions
112
+ already exist, you'll be prompted for confirmation unless --force is used.
113
+
114
+ Examples:
115
+ # Add from file
116
+ mcp-ticketer instructions add team_guidelines.md
117
+
118
+ # Add from stdin
119
+ cat guidelines.md | mcp-ticketer instructions add --stdin
120
+
121
+ # Force overwrite existing
122
+ mcp-ticketer instructions add new_guide.md --force
123
+
124
+ """
125
+ try:
126
+ manager = TicketInstructionsManager()
127
+
128
+ # Check for existing custom instructions
129
+ if manager.has_custom_instructions() and not force:
130
+ path = manager.get_instructions_path()
131
+ console.print(
132
+ f"[yellow]Warning:[/yellow] Custom instructions already exist at {path}"
133
+ )
134
+
135
+ confirm = typer.confirm("Do you want to overwrite them?")
136
+ if not confirm:
137
+ console.print("[yellow]Operation cancelled[/yellow]")
138
+ raise typer.Exit(0) from None
139
+
140
+ # Get content from stdin or file
141
+ if stdin:
142
+ console.print("[dim]Reading from stdin... (Press Ctrl+D when done)[/dim]")
143
+ content = sys.stdin.read()
144
+ if not content.strip():
145
+ console.print("[red]Error:[/red] No content provided on stdin")
146
+ raise typer.Exit(1) from None
147
+ elif file_path:
148
+ source_path = Path(file_path)
149
+ if not source_path.exists():
150
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
151
+ raise typer.Exit(1) from None
152
+
153
+ try:
154
+ content = source_path.read_text(encoding="utf-8")
155
+ except Exception as e:
156
+ console.print(f"[red]Error:[/red] Failed to read file: {e}")
157
+ raise typer.Exit(1) from None
158
+ else:
159
+ console.print("[red]Error:[/red] Either provide a file path or use --stdin")
160
+ console.print("Example: mcp-ticketer instructions add guidelines.md")
161
+ raise typer.Exit(1) from None
162
+
163
+ # Set instructions
164
+ manager.set_instructions(content)
165
+
166
+ path = manager.get_instructions_path()
167
+ console.print(f"[green]✓[/green] Custom instructions saved to: {path}")
168
+ console.print("[dim]Use 'mcp-ticketer instructions show' to view them[/dim]")
169
+
170
+ except InstructionsValidationError as e:
171
+ console.print(f"[red]Validation Error:[/red] {e}")
172
+ raise typer.Exit(1) from None
173
+ except InstructionsError as e:
174
+ console.print(f"[red]Error:[/red] {e}")
175
+ raise typer.Exit(1) from None
176
+
177
+
178
+ @app.command()
179
+ def update(
180
+ file_path: str | None = typer.Argument(
181
+ None,
182
+ help="Path to markdown file with updated instructions",
183
+ ),
184
+ stdin: bool = typer.Option(
185
+ False,
186
+ "--stdin",
187
+ help="Read instructions from stdin instead of file",
188
+ ),
189
+ ) -> None:
190
+ """Update existing custom instructions (alias for 'add --force').
191
+
192
+ This is a convenience command that overwrites existing custom instructions
193
+ without prompting for confirmation.
194
+
195
+ Examples:
196
+ # Update from file
197
+ mcp-ticketer instructions update new_guidelines.md
198
+
199
+ # Update from stdin
200
+ cat updated.md | mcp-ticketer instructions update --stdin
201
+
202
+ """
203
+ try:
204
+ manager = TicketInstructionsManager()
205
+
206
+ if not manager.has_custom_instructions():
207
+ console.print("[yellow]Warning:[/yellow] No custom instructions exist yet")
208
+ console.print("Use 'mcp-ticketer instructions add' to create them first")
209
+ raise typer.Exit(1) from None
210
+
211
+ # Get content from stdin or file
212
+ if stdin:
213
+ console.print("[dim]Reading from stdin... (Press Ctrl+D when done)[/dim]")
214
+ content = sys.stdin.read()
215
+ if not content.strip():
216
+ console.print("[red]Error:[/red] No content provided on stdin")
217
+ raise typer.Exit(1) from None
218
+ elif file_path:
219
+ source_path = Path(file_path)
220
+ if not source_path.exists():
221
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
222
+ raise typer.Exit(1) from None
223
+
224
+ try:
225
+ content = source_path.read_text(encoding="utf-8")
226
+ except Exception as e:
227
+ console.print(f"[red]Error:[/red] Failed to read file: {e}")
228
+ raise typer.Exit(1) from None
229
+ else:
230
+ console.print("[red]Error:[/red] Either provide a file path or use --stdin")
231
+ console.print("Example: mcp-ticketer instructions update guidelines.md")
232
+ raise typer.Exit(1) from None
233
+
234
+ # Update instructions (force overwrite)
235
+ manager.set_instructions(content)
236
+
237
+ path = manager.get_instructions_path()
238
+ console.print(f"[green]✓[/green] Custom instructions updated: {path}")
239
+ console.print("[dim]Use 'mcp-ticketer instructions show' to view them[/dim]")
240
+
241
+ except InstructionsValidationError as e:
242
+ console.print(f"[red]Validation Error:[/red] {e}")
243
+ raise typer.Exit(1) from None
244
+ except InstructionsError as e:
245
+ console.print(f"[red]Error:[/red] {e}")
246
+ raise typer.Exit(1) from None
247
+
248
+
249
+ @app.command()
250
+ def delete(
251
+ yes: bool = typer.Option(
252
+ False,
253
+ "--yes",
254
+ "-y",
255
+ help="Skip confirmation prompt",
256
+ ),
257
+ ) -> None:
258
+ """Delete custom instructions and revert to defaults.
259
+
260
+ This removes your project-specific instructions file. After deletion,
261
+ the default instructions will be used.
262
+
263
+ Examples:
264
+ # Delete with confirmation prompt
265
+ mcp-ticketer instructions delete
266
+
267
+ # Skip confirmation
268
+ mcp-ticketer instructions delete --yes
269
+
270
+ """
271
+ try:
272
+ manager = TicketInstructionsManager()
273
+
274
+ if not manager.has_custom_instructions():
275
+ console.print("[yellow]No custom instructions to delete[/yellow]")
276
+ console.print("[dim]Already using default instructions[/dim]")
277
+ raise typer.Exit(0) from None
278
+
279
+ path = manager.get_instructions_path()
280
+
281
+ if not yes:
282
+ console.print(f"[yellow]Warning:[/yellow] This will delete: {path}")
283
+ console.print("After deletion, default instructions will be used.")
284
+
285
+ confirm = typer.confirm("Are you sure?")
286
+ if not confirm:
287
+ console.print("[yellow]Operation cancelled[/yellow]")
288
+ raise typer.Exit(0) from None
289
+
290
+ # Delete instructions
291
+ manager.delete_instructions()
292
+
293
+ console.print("[green]✓[/green] Custom instructions deleted")
294
+ console.print("[dim]Now using default instructions[/dim]")
295
+
296
+ except InstructionsError as e:
297
+ console.print(f"[red]Error:[/red] {e}")
298
+ raise typer.Exit(1) from None
299
+
300
+
301
+ @app.command()
302
+ def path() -> None:
303
+ """Show path to custom instructions file.
304
+
305
+ Displays the path where custom instructions are (or would be) stored
306
+ for this project, along with status information.
307
+
308
+ Examples:
309
+ # Show instructions file path
310
+ mcp-ticketer instructions path
311
+
312
+ # Use in scripts
313
+ INST_PATH=$(mcp-ticketer instructions path --quiet)
314
+
315
+ """
316
+ try:
317
+ manager = TicketInstructionsManager()
318
+ inst_path = manager.get_instructions_path()
319
+ exists = manager.has_custom_instructions()
320
+
321
+ console.print(f"[cyan]Instructions file:[/cyan] {inst_path}")
322
+
323
+ if exists:
324
+ console.print("[green]Status:[/green] Custom instructions exist")
325
+
326
+ # Show file size
327
+ try:
328
+ size = inst_path.stat().st_size
329
+ console.print(f"[dim]Size: {size} bytes[/dim]")
330
+ except Exception:
331
+ pass
332
+ else:
333
+ console.print(
334
+ "[yellow]Status:[/yellow] No custom instructions (using defaults)"
335
+ )
336
+ console.print(
337
+ "[dim]Create with: mcp-ticketer instructions add <file>[/dim]"
338
+ )
339
+
340
+ except InstructionsError as e:
341
+ console.print(f"[red]Error:[/red] {e}")
342
+ raise typer.Exit(1) from None
343
+
344
+
345
+ @app.command()
346
+ def edit() -> None:
347
+ """Open instructions in default editor.
348
+
349
+ Opens the custom instructions file in your system's default text editor.
350
+ If custom instructions don't exist yet, creates them with default content
351
+ first.
352
+
353
+ The editor is determined by the EDITOR environment variable, or falls back
354
+ to sensible defaults (vim on Unix, notepad on Windows).
355
+
356
+ Examples:
357
+ # Edit instructions
358
+ mcp-ticketer instructions edit
359
+
360
+ # Use specific editor
361
+ EDITOR=nano mcp-ticketer instructions edit
362
+
363
+ """
364
+ import os
365
+ import platform
366
+ import subprocess
367
+
368
+ try:
369
+ manager = TicketInstructionsManager()
370
+
371
+ # If no custom instructions exist, create them with defaults
372
+ if not manager.has_custom_instructions():
373
+ console.print("[yellow]No custom instructions yet[/yellow]")
374
+ console.print("[dim]Creating from defaults...[/dim]")
375
+
376
+ # Copy defaults to custom location
377
+ default_content = manager.get_default_instructions()
378
+ manager.set_instructions(default_content)
379
+
380
+ console.print(
381
+ f"[green]✓[/green] Created custom instructions at: {manager.get_instructions_path()}"
382
+ )
383
+
384
+ inst_path = manager.get_instructions_path()
385
+
386
+ # Determine editor
387
+ editor = os.environ.get("EDITOR")
388
+
389
+ if not editor:
390
+ # Platform-specific defaults
391
+ system = platform.system()
392
+ if system == "Windows":
393
+ editor = "notepad"
394
+ else:
395
+ # Unix-like: try common editors
396
+ for candidate in ["vim", "vi", "nano", "emacs"]:
397
+ try:
398
+ result = subprocess.run(
399
+ ["which", candidate],
400
+ capture_output=True,
401
+ text=True,
402
+ timeout=1,
403
+ )
404
+ if result.returncode == 0:
405
+ editor = candidate
406
+ break
407
+ except Exception:
408
+ continue
409
+
410
+ if not editor:
411
+ editor = "vi" # Ultimate fallback
412
+
413
+ console.print(f"[dim]Opening with {editor}...[/dim]")
414
+
415
+ # Open editor
416
+ try:
417
+ subprocess.run([editor, str(inst_path)], check=True)
418
+ console.print(f"[green]✓[/green] Finished editing: {inst_path}")
419
+ except subprocess.CalledProcessError as e:
420
+ console.print(f"[red]Error:[/red] Editor exited with code {e.returncode}")
421
+ raise typer.Exit(1) from None
422
+ except FileNotFoundError:
423
+ console.print(f"[red]Error:[/red] Editor not found: {editor}")
424
+ console.print("Set EDITOR environment variable to your preferred editor")
425
+ raise typer.Exit(1) from None
426
+
427
+ except InstructionsError as e:
428
+ console.print(f"[red]Error:[/red] {e}")
429
+ raise typer.Exit(1) from None
@@ -1,6 +1,7 @@
1
1
  """Linear-specific CLI commands for workspace and team management."""
2
2
 
3
3
  import os
4
+ import re
4
5
 
5
6
  import typer
6
7
  from gql import Client, gql
@@ -12,13 +13,96 @@ app = typer.Typer(name="linear", help="Linear workspace and team management")
12
13
  console = Console()
13
14
 
14
15
 
16
+ async def derive_team_from_url(
17
+ api_key: str, team_url: str
18
+ ) -> tuple[str | None, str | None]:
19
+ """Derive team ID from Linear team issues URL.
20
+
21
+ Accepts URLs like:
22
+ - https://linear.app/1m-hyperdev/team/1M/active
23
+ - https://linear.app/1m-hyperdev/team/1M/
24
+ - https://linear.app/1m-hyperdev/team/1M
25
+
26
+ Args:
27
+ api_key: Linear API key
28
+ team_url: URL to Linear team issues page
29
+
30
+ Returns:
31
+ Tuple of (team_id, error_message). If successful, team_id is set and error_message is None.
32
+ If failed, team_id is None and error_message contains the error.
33
+
34
+ """
35
+ # Extract team key from URL using regex
36
+ # Pattern: https://linear.app/<workspace>/team/<TEAM_KEY>/...
37
+ pattern = r"https://linear\.app/[\w-]+/team/([\w-]+)"
38
+ match = re.search(pattern, team_url)
39
+
40
+ if not match:
41
+ return (
42
+ None,
43
+ "Invalid Linear team URL format. Expected: https://linear.app/<workspace>/team/<TEAM_KEY>",
44
+ )
45
+
46
+ team_key = match.group(1)
47
+ console.print(f"[dim]Extracted team key: {team_key}[/dim]")
48
+
49
+ # Query Linear API to resolve team key to team ID
50
+ query = gql(
51
+ """
52
+ query GetTeamByKey($key: String!) {
53
+ teams(filter: { key: { eq: $key } }) {
54
+ nodes {
55
+ id
56
+ key
57
+ name
58
+ organization {
59
+ name
60
+ urlKey
61
+ }
62
+ }
63
+ }
64
+ }
65
+ """
66
+ )
67
+
68
+ try:
69
+ # Create client
70
+ transport = HTTPXTransport(
71
+ url="https://api.linear.app/graphql", headers={"Authorization": api_key}
72
+ )
73
+ client = Client(transport=transport, fetch_schema_from_transport=False)
74
+
75
+ # Execute query
76
+ result = client.execute(query, variable_values={"key": team_key})
77
+ teams = result.get("teams", {}).get("nodes", [])
78
+
79
+ if not teams:
80
+ return (
81
+ None,
82
+ f"Team with key '{team_key}' not found. Please check your team URL and API key.",
83
+ )
84
+
85
+ team = teams[0]
86
+ team_id = team["id"]
87
+ team_name = team["name"]
88
+
89
+ console.print(
90
+ f"[green]✓[/green] Resolved team: {team_name} (Key: {team_key}, ID: {team_id})"
91
+ )
92
+
93
+ return team_id, None
94
+
95
+ except Exception as e:
96
+ return None, f"Failed to query Linear API: {str(e)}"
97
+
98
+
15
99
  def _create_linear_client() -> Client:
16
100
  """Create a Linear GraphQL client."""
17
101
  api_key = os.getenv("LINEAR_API_KEY")
18
102
  if not api_key:
19
103
  console.print("[red]❌ LINEAR_API_KEY not found in environment[/red]")
20
104
  console.print("Set it in .env.local or environment variables")
21
- raise typer.Exit(1)
105
+ raise typer.Exit(1) from None
22
106
 
23
107
  transport = HTTPXTransport(
24
108
  url="https://api.linear.app/graphql", headers={"Authorization": api_key}
@@ -27,7 +111,7 @@ def _create_linear_client() -> Client:
27
111
 
28
112
 
29
113
  @app.command("workspaces")
30
- def list_workspaces():
114
+ def list_workspaces() -> None:
31
115
  """List all accessible Linear workspaces."""
32
116
  console.print("🔍 Discovering Linear workspaces...")
33
117
 
@@ -74,7 +158,7 @@ def list_workspaces():
74
158
 
75
159
  except Exception as e:
76
160
  console.print(f"[red]❌ Error fetching workspace info: {e}[/red]")
77
- raise typer.Exit(1)
161
+ raise typer.Exit(1) from e
78
162
 
79
163
 
80
164
  @app.command("teams")
@@ -85,7 +169,7 @@ def list_teams(
85
169
  all_teams: bool = typer.Option(
86
170
  False, "--all", "-a", help="Show all teams across all workspaces"
87
171
  ),
88
- ):
172
+ ) -> None:
89
173
  """List all teams in the current workspace or all accessible teams."""
90
174
  if all_teams:
91
175
  console.print("🔍 Discovering ALL accessible Linear teams across workspaces...")
@@ -244,7 +328,7 @@ def list_teams(
244
328
 
245
329
  except Exception as e:
246
330
  console.print(f"[red]❌ Error fetching teams: {e}[/red]")
247
- raise typer.Exit(1)
331
+ raise typer.Exit(1) from e
248
332
 
249
333
 
250
334
  @app.command("configure")
@@ -256,13 +340,13 @@ def configure_team(
256
340
  workspace: str | None = typer.Option(
257
341
  None, "--workspace", "-w", help="Workspace URL key"
258
342
  ),
259
- ):
343
+ ) -> None:
260
344
  """Configure Linear adapter with a specific team."""
261
345
  from ..cli.main import load_config, save_config
262
346
 
263
347
  if not team_key and not team_id:
264
348
  console.print("[red]❌ Either --team-key or --team-id is required[/red]")
265
- raise typer.Exit(1)
349
+ raise typer.Exit(1) from None
266
350
 
267
351
  console.print("🔧 Configuring Linear adapter...")
268
352
 
@@ -292,11 +376,11 @@ def configure_team(
292
376
 
293
377
  if not team:
294
378
  console.print(f"[red]❌ Team with ID '{team_id}' not found[/red]")
295
- raise typer.Exit(1)
379
+ raise typer.Exit(1) from None
296
380
 
297
381
  except Exception as e:
298
382
  console.print(f"[red]❌ Error validating team: {e}[/red]")
299
- raise typer.Exit(1)
383
+ raise typer.Exit(1) from e
300
384
 
301
385
  elif team_key:
302
386
  # Validate team by key
@@ -325,14 +409,14 @@ def configure_team(
325
409
 
326
410
  if not teams:
327
411
  console.print(f"[red]❌ Team with key '{team_key}' not found[/red]")
328
- raise typer.Exit(1)
412
+ raise typer.Exit(1) from None
329
413
 
330
414
  team = teams[0]
331
415
  team_id = team["id"] # Use the found team ID
332
416
 
333
417
  except Exception as e:
334
418
  console.print(f"[red]❌ Error validating team: {e}[/red]")
335
- raise typer.Exit(1)
419
+ raise typer.Exit(1) from e
336
420
 
337
421
  # Update configuration
338
422
  config = load_config()
@@ -387,11 +471,11 @@ def show_info(
387
471
  team_id: str | None = typer.Option(
388
472
  None, "--team-id", "-i", help="Team UUID to show info for"
389
473
  ),
390
- ):
474
+ ) -> None:
391
475
  """Show detailed information about a specific team."""
392
476
  if not team_key and not team_id:
393
477
  console.print("[red]❌ Either --team-key or --team-id is required[/red]")
394
- raise typer.Exit(1)
478
+ raise typer.Exit(1) from None
395
479
 
396
480
  # Query for detailed team information
397
481
  if team_id:
@@ -482,7 +566,7 @@ def show_info(
482
566
  if not team:
483
567
  identifier = team_id or team_key
484
568
  console.print(f"[red]❌ Team '{identifier}' not found[/red]")
485
- raise typer.Exit(1)
569
+ raise typer.Exit(1) from None
486
570
 
487
571
  # Display team information
488
572
  console.print(f"\n🏷️ Team: {team.get('name')}")
@@ -529,4 +613,4 @@ def show_info(
529
613
 
530
614
  except Exception as e:
531
615
  console.print(f"[red]❌ Error fetching team info: {e}[/red]")
532
- raise typer.Exit(1)
616
+ raise typer.Exit(1) from e