mcp-ticketer 0.3.5__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 (84) hide show
  1. mcp_ticketer/__version__.py +3 -3
  2. mcp_ticketer/adapters/__init__.py +2 -0
  3. mcp_ticketer/adapters/aitrackdown.py +263 -14
  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 +326 -109
  10. mcp_ticketer/adapters/hybrid.py +11 -11
  11. mcp_ticketer/adapters/jira.py +271 -25
  12. mcp_ticketer/adapters/linear/adapter.py +693 -39
  13. mcp_ticketer/adapters/linear/client.py +61 -9
  14. mcp_ticketer/adapters/linear/mappers.py +9 -3
  15. mcp_ticketer/adapters/linear/queries.py +9 -7
  16. mcp_ticketer/cache/memory.py +9 -8
  17. mcp_ticketer/cli/adapter_diagnostics.py +1 -1
  18. mcp_ticketer/cli/auggie_configure.py +104 -15
  19. mcp_ticketer/cli/codex_configure.py +188 -32
  20. mcp_ticketer/cli/configure.py +37 -48
  21. mcp_ticketer/cli/diagnostics.py +20 -18
  22. mcp_ticketer/cli/discover.py +292 -26
  23. mcp_ticketer/cli/gemini_configure.py +107 -26
  24. mcp_ticketer/cli/instruction_commands.py +429 -0
  25. mcp_ticketer/cli/linear_commands.py +105 -22
  26. mcp_ticketer/cli/main.py +1830 -435
  27. mcp_ticketer/cli/mcp_configure.py +296 -89
  28. mcp_ticketer/cli/migrate_config.py +12 -8
  29. mcp_ticketer/cli/platform_commands.py +123 -0
  30. mcp_ticketer/cli/platform_detection.py +412 -0
  31. mcp_ticketer/cli/python_detection.py +126 -0
  32. mcp_ticketer/cli/queue_commands.py +15 -15
  33. mcp_ticketer/cli/simple_health.py +1 -1
  34. mcp_ticketer/cli/ticket_commands.py +773 -0
  35. mcp_ticketer/cli/update_checker.py +313 -0
  36. mcp_ticketer/cli/utils.py +67 -62
  37. mcp_ticketer/core/__init__.py +14 -1
  38. mcp_ticketer/core/adapter.py +84 -15
  39. mcp_ticketer/core/config.py +44 -39
  40. mcp_ticketer/core/env_discovery.py +42 -12
  41. mcp_ticketer/core/env_loader.py +15 -14
  42. mcp_ticketer/core/exceptions.py +3 -3
  43. mcp_ticketer/core/http_client.py +26 -26
  44. mcp_ticketer/core/instructions.py +405 -0
  45. mcp_ticketer/core/mappers.py +11 -11
  46. mcp_ticketer/core/models.py +50 -20
  47. mcp_ticketer/core/onepassword_secrets.py +379 -0
  48. mcp_ticketer/core/project_config.py +57 -35
  49. mcp_ticketer/core/registry.py +3 -3
  50. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  51. mcp_ticketer/mcp/__init__.py +29 -1
  52. mcp_ticketer/mcp/__main__.py +60 -0
  53. mcp_ticketer/mcp/server/__init__.py +25 -0
  54. mcp_ticketer/mcp/server/__main__.py +60 -0
  55. mcp_ticketer/mcp/{dto.py → server/dto.py} +32 -32
  56. mcp_ticketer/mcp/{server.py → server/main.py} +127 -74
  57. mcp_ticketer/mcp/{response_builder.py → server/response_builder.py} +2 -2
  58. mcp_ticketer/mcp/server/server_sdk.py +93 -0
  59. mcp_ticketer/mcp/server/tools/__init__.py +47 -0
  60. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  61. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  62. mcp_ticketer/mcp/server/tools/comment_tools.py +90 -0
  63. mcp_ticketer/mcp/server/tools/config_tools.py +381 -0
  64. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +532 -0
  65. mcp_ticketer/mcp/server/tools/instruction_tools.py +293 -0
  66. mcp_ticketer/mcp/server/tools/pr_tools.py +154 -0
  67. mcp_ticketer/mcp/server/tools/search_tools.py +206 -0
  68. mcp_ticketer/mcp/server/tools/ticket_tools.py +430 -0
  69. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +382 -0
  70. mcp_ticketer/queue/__init__.py +1 -0
  71. mcp_ticketer/queue/health_monitor.py +5 -4
  72. mcp_ticketer/queue/manager.py +15 -51
  73. mcp_ticketer/queue/queue.py +19 -19
  74. mcp_ticketer/queue/run_worker.py +1 -1
  75. mcp_ticketer/queue/ticket_registry.py +14 -14
  76. mcp_ticketer/queue/worker.py +16 -14
  77. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/METADATA +168 -32
  78. mcp_ticketer-0.12.0.dist-info/RECORD +91 -0
  79. mcp_ticketer-0.3.5.dist-info/RECORD +0 -62
  80. /mcp_ticketer/mcp/{constants.py → server/constants.py} +0 -0
  81. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/WHEEL +0 -0
  82. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/entry_points.txt +0 -0
  83. {mcp_ticketer-0.3.5.dist-info → mcp_ticketer-0.12.0.dist-info}/licenses/LICENSE +0 -0
  84. {mcp_ticketer-0.3.5.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,7 +1,7 @@
1
1
  """Linear-specific CLI commands for workspace and team management."""
2
2
 
3
3
  import os
4
- from typing import Optional
4
+ import re
5
5
 
6
6
  import typer
7
7
  from gql import Client, gql
@@ -13,13 +13,96 @@ app = typer.Typer(name="linear", help="Linear workspace and team management")
13
13
  console = Console()
14
14
 
15
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
+
16
99
  def _create_linear_client() -> Client:
17
100
  """Create a Linear GraphQL client."""
18
101
  api_key = os.getenv("LINEAR_API_KEY")
19
102
  if not api_key:
20
103
  console.print("[red]❌ LINEAR_API_KEY not found in environment[/red]")
21
104
  console.print("Set it in .env.local or environment variables")
22
- raise typer.Exit(1)
105
+ raise typer.Exit(1) from None
23
106
 
24
107
  transport = HTTPXTransport(
25
108
  url="https://api.linear.app/graphql", headers={"Authorization": api_key}
@@ -28,7 +111,7 @@ def _create_linear_client() -> Client:
28
111
 
29
112
 
30
113
  @app.command("workspaces")
31
- def list_workspaces():
114
+ def list_workspaces() -> None:
32
115
  """List all accessible Linear workspaces."""
33
116
  console.print("🔍 Discovering Linear workspaces...")
34
117
 
@@ -75,18 +158,18 @@ def list_workspaces():
75
158
 
76
159
  except Exception as e:
77
160
  console.print(f"[red]❌ Error fetching workspace info: {e}[/red]")
78
- raise typer.Exit(1)
161
+ raise typer.Exit(1) from e
79
162
 
80
163
 
81
164
  @app.command("teams")
82
165
  def list_teams(
83
- workspace: Optional[str] = typer.Option(
166
+ workspace: str | None = typer.Option(
84
167
  None, "--workspace", "-w", help="Workspace URL key (optional)"
85
168
  ),
86
169
  all_teams: bool = typer.Option(
87
170
  False, "--all", "-a", help="Show all teams across all workspaces"
88
171
  ),
89
- ):
172
+ ) -> None:
90
173
  """List all teams in the current workspace or all accessible teams."""
91
174
  if all_teams:
92
175
  console.print("🔍 Discovering ALL accessible Linear teams across workspaces...")
@@ -245,25 +328,25 @@ def list_teams(
245
328
 
246
329
  except Exception as e:
247
330
  console.print(f"[red]❌ Error fetching teams: {e}[/red]")
248
- raise typer.Exit(1)
331
+ raise typer.Exit(1) from e
249
332
 
250
333
 
251
334
  @app.command("configure")
252
335
  def configure_team(
253
- team_key: Optional[str] = typer.Option(
336
+ team_key: str | None = typer.Option(
254
337
  None, "--team-key", "-k", help="Team key (e.g., '1M')"
255
338
  ),
256
- team_id: Optional[str] = typer.Option(None, "--team-id", "-i", help="Team UUID"),
257
- workspace: Optional[str] = typer.Option(
339
+ team_id: str | None = typer.Option(None, "--team-id", "-i", help="Team UUID"),
340
+ workspace: str | None = typer.Option(
258
341
  None, "--workspace", "-w", help="Workspace URL key"
259
342
  ),
260
- ):
343
+ ) -> None:
261
344
  """Configure Linear adapter with a specific team."""
262
345
  from ..cli.main import load_config, save_config
263
346
 
264
347
  if not team_key and not team_id:
265
348
  console.print("[red]❌ Either --team-key or --team-id is required[/red]")
266
- raise typer.Exit(1)
349
+ raise typer.Exit(1) from None
267
350
 
268
351
  console.print("🔧 Configuring Linear adapter...")
269
352
 
@@ -293,11 +376,11 @@ def configure_team(
293
376
 
294
377
  if not team:
295
378
  console.print(f"[red]❌ Team with ID '{team_id}' not found[/red]")
296
- raise typer.Exit(1)
379
+ raise typer.Exit(1) from None
297
380
 
298
381
  except Exception as e:
299
382
  console.print(f"[red]❌ Error validating team: {e}[/red]")
300
- raise typer.Exit(1)
383
+ raise typer.Exit(1) from e
301
384
 
302
385
  elif team_key:
303
386
  # Validate team by key
@@ -326,14 +409,14 @@ def configure_team(
326
409
 
327
410
  if not teams:
328
411
  console.print(f"[red]❌ Team with key '{team_key}' not found[/red]")
329
- raise typer.Exit(1)
412
+ raise typer.Exit(1) from None
330
413
 
331
414
  team = teams[0]
332
415
  team_id = team["id"] # Use the found team ID
333
416
 
334
417
  except Exception as e:
335
418
  console.print(f"[red]❌ Error validating team: {e}[/red]")
336
- raise typer.Exit(1)
419
+ raise typer.Exit(1) from e
337
420
 
338
421
  # Update configuration
339
422
  config = load_config()
@@ -382,17 +465,17 @@ def configure_team(
382
465
 
383
466
  @app.command("info")
384
467
  def show_info(
385
- team_key: Optional[str] = typer.Option(
468
+ team_key: str | None = typer.Option(
386
469
  None, "--team-key", "-k", help="Team key to show info for"
387
470
  ),
388
- team_id: Optional[str] = typer.Option(
471
+ team_id: str | None = typer.Option(
389
472
  None, "--team-id", "-i", help="Team UUID to show info for"
390
473
  ),
391
- ):
474
+ ) -> None:
392
475
  """Show detailed information about a specific team."""
393
476
  if not team_key and not team_id:
394
477
  console.print("[red]❌ Either --team-key or --team-id is required[/red]")
395
- raise typer.Exit(1)
478
+ raise typer.Exit(1) from None
396
479
 
397
480
  # Query for detailed team information
398
481
  if team_id:
@@ -483,7 +566,7 @@ def show_info(
483
566
  if not team:
484
567
  identifier = team_id or team_key
485
568
  console.print(f"[red]❌ Team '{identifier}' not found[/red]")
486
- raise typer.Exit(1)
569
+ raise typer.Exit(1) from None
487
570
 
488
571
  # Display team information
489
572
  console.print(f"\n🏷️ Team: {team.get('name')}")
@@ -530,4 +613,4 @@ def show_info(
530
613
 
531
614
  except Exception as e:
532
615
  console.print(f"[red]❌ Error fetching team info: {e}[/red]")
533
- raise typer.Exit(1)
616
+ raise typer.Exit(1) from e