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,435 @@
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
+ --------
50
+ # Show current instructions (custom or default)
51
+ mcp-ticketer instructions show
52
+
53
+ # Always show default instructions
54
+ mcp-ticketer instructions show --default
55
+
56
+ # Output raw markdown for piping
57
+ mcp-ticketer instructions show --raw > team_guide.md
58
+
59
+ """
60
+ try:
61
+ manager = TicketInstructionsManager()
62
+
63
+ if default:
64
+ instructions = manager.get_default_instructions()
65
+ source = "default"
66
+ else:
67
+ instructions = manager.get_instructions()
68
+ source = "custom" if manager.has_custom_instructions() else "default"
69
+
70
+ if raw:
71
+ # Raw output for piping
72
+ console.print(instructions)
73
+ else:
74
+ # Rich formatted output
75
+ if source == "custom":
76
+ title = f"[green]Custom Instructions[/green] ({manager.get_instructions_path()})"
77
+ else:
78
+ title = "[blue]Default Instructions[/blue]"
79
+
80
+ panel = Panel(
81
+ Markdown(instructions),
82
+ title=title,
83
+ border_style="cyan",
84
+ )
85
+ console.print(panel)
86
+
87
+ except InstructionsError as e:
88
+ console.print(f"[red]Error:[/red] {e}")
89
+ raise typer.Exit(1) from None
90
+
91
+
92
+ @app.command()
93
+ def add(
94
+ file_path: str | None = typer.Argument(
95
+ None,
96
+ help="Path to markdown file with custom instructions",
97
+ ),
98
+ stdin: bool = typer.Option(
99
+ False,
100
+ "--stdin",
101
+ help="Read instructions from stdin instead of file",
102
+ ),
103
+ force: bool = typer.Option(
104
+ False,
105
+ "--force",
106
+ "-f",
107
+ help="Overwrite existing custom instructions without confirmation",
108
+ ),
109
+ ) -> None:
110
+ """Add custom ticket writing instructions for your project.
111
+
112
+ You can provide instructions from a file or via stdin. If custom instructions
113
+ already exist, you'll be prompted for confirmation unless --force is used.
114
+
115
+ Examples:
116
+ --------
117
+ # Add from file
118
+ mcp-ticketer instructions add team_guidelines.md
119
+
120
+ # Add from stdin
121
+ cat guidelines.md | mcp-ticketer instructions add --stdin
122
+
123
+ # Force overwrite existing
124
+ mcp-ticketer instructions add new_guide.md --force
125
+
126
+ """
127
+ try:
128
+ manager = TicketInstructionsManager()
129
+
130
+ # Check for existing custom instructions
131
+ if manager.has_custom_instructions() and not force:
132
+ path = manager.get_instructions_path()
133
+ console.print(
134
+ f"[yellow]Warning:[/yellow] Custom instructions already exist at {path}"
135
+ )
136
+
137
+ confirm = typer.confirm("Do you want to overwrite them?")
138
+ if not confirm:
139
+ console.print("[yellow]Operation cancelled[/yellow]")
140
+ raise typer.Exit(0) from None
141
+
142
+ # Get content from stdin or file
143
+ if stdin:
144
+ console.print("[dim]Reading from stdin... (Press Ctrl+D when done)[/dim]")
145
+ content = sys.stdin.read()
146
+ if not content.strip():
147
+ console.print("[red]Error:[/red] No content provided on stdin")
148
+ raise typer.Exit(1) from None
149
+ elif file_path:
150
+ source_path = Path(file_path)
151
+ if not source_path.exists():
152
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
153
+ raise typer.Exit(1) from None
154
+
155
+ try:
156
+ content = source_path.read_text(encoding="utf-8")
157
+ except Exception as e:
158
+ console.print(f"[red]Error:[/red] Failed to read file: {e}")
159
+ raise typer.Exit(1) from None
160
+ else:
161
+ console.print("[red]Error:[/red] Either provide a file path or use --stdin")
162
+ console.print("Example: mcp-ticketer instructions add guidelines.md")
163
+ raise typer.Exit(1) from None
164
+
165
+ # Set instructions
166
+ manager.set_instructions(content)
167
+
168
+ path = manager.get_instructions_path()
169
+ console.print(f"[green]✓[/green] Custom instructions saved to: {path}")
170
+ console.print("[dim]Use 'mcp-ticketer instructions show' to view them[/dim]")
171
+
172
+ except InstructionsValidationError as e:
173
+ console.print(f"[red]Validation Error:[/red] {e}")
174
+ raise typer.Exit(1) from None
175
+ except InstructionsError as e:
176
+ console.print(f"[red]Error:[/red] {e}")
177
+ raise typer.Exit(1) from None
178
+
179
+
180
+ @app.command()
181
+ def update(
182
+ file_path: str | None = typer.Argument(
183
+ None,
184
+ help="Path to markdown file with updated instructions",
185
+ ),
186
+ stdin: bool = typer.Option(
187
+ False,
188
+ "--stdin",
189
+ help="Read instructions from stdin instead of file",
190
+ ),
191
+ ) -> None:
192
+ """Update existing custom instructions (alias for 'add --force').
193
+
194
+ This is a convenience command that overwrites existing custom instructions
195
+ without prompting for confirmation.
196
+
197
+ Examples:
198
+ --------
199
+ # Update from file
200
+ mcp-ticketer instructions update new_guidelines.md
201
+
202
+ # Update from stdin
203
+ cat updated.md | mcp-ticketer instructions update --stdin
204
+
205
+ """
206
+ try:
207
+ manager = TicketInstructionsManager()
208
+
209
+ if not manager.has_custom_instructions():
210
+ console.print("[yellow]Warning:[/yellow] No custom instructions exist yet")
211
+ console.print("Use 'mcp-ticketer instructions add' to create them first")
212
+ raise typer.Exit(1) from None
213
+
214
+ # Get content from stdin or file
215
+ if stdin:
216
+ console.print("[dim]Reading from stdin... (Press Ctrl+D when done)[/dim]")
217
+ content = sys.stdin.read()
218
+ if not content.strip():
219
+ console.print("[red]Error:[/red] No content provided on stdin")
220
+ raise typer.Exit(1) from None
221
+ elif file_path:
222
+ source_path = Path(file_path)
223
+ if not source_path.exists():
224
+ console.print(f"[red]Error:[/red] File not found: {file_path}")
225
+ raise typer.Exit(1) from None
226
+
227
+ try:
228
+ content = source_path.read_text(encoding="utf-8")
229
+ except Exception as e:
230
+ console.print(f"[red]Error:[/red] Failed to read file: {e}")
231
+ raise typer.Exit(1) from None
232
+ else:
233
+ console.print("[red]Error:[/red] Either provide a file path or use --stdin")
234
+ console.print("Example: mcp-ticketer instructions update guidelines.md")
235
+ raise typer.Exit(1) from None
236
+
237
+ # Update instructions (force overwrite)
238
+ manager.set_instructions(content)
239
+
240
+ path = manager.get_instructions_path()
241
+ console.print(f"[green]✓[/green] Custom instructions updated: {path}")
242
+ console.print("[dim]Use 'mcp-ticketer instructions show' to view them[/dim]")
243
+
244
+ except InstructionsValidationError as e:
245
+ console.print(f"[red]Validation Error:[/red] {e}")
246
+ raise typer.Exit(1) from None
247
+ except InstructionsError as e:
248
+ console.print(f"[red]Error:[/red] {e}")
249
+ raise typer.Exit(1) from None
250
+
251
+
252
+ @app.command()
253
+ def delete(
254
+ yes: bool = typer.Option(
255
+ False,
256
+ "--yes",
257
+ "-y",
258
+ help="Skip confirmation prompt",
259
+ ),
260
+ ) -> None:
261
+ """Delete custom instructions and revert to defaults.
262
+
263
+ This removes your project-specific instructions file. After deletion,
264
+ the default instructions will be used.
265
+
266
+ Examples:
267
+ --------
268
+ # Delete with confirmation prompt
269
+ mcp-ticketer instructions delete
270
+
271
+ # Skip confirmation
272
+ mcp-ticketer instructions delete --yes
273
+
274
+ """
275
+ try:
276
+ manager = TicketInstructionsManager()
277
+
278
+ if not manager.has_custom_instructions():
279
+ console.print("[yellow]No custom instructions to delete[/yellow]")
280
+ console.print("[dim]Already using default instructions[/dim]")
281
+ raise typer.Exit(0) from None
282
+
283
+ path = manager.get_instructions_path()
284
+
285
+ if not yes:
286
+ console.print(f"[yellow]Warning:[/yellow] This will delete: {path}")
287
+ console.print("After deletion, default instructions will be used.")
288
+
289
+ confirm = typer.confirm("Are you sure?")
290
+ if not confirm:
291
+ console.print("[yellow]Operation cancelled[/yellow]")
292
+ raise typer.Exit(0) from None
293
+
294
+ # Delete instructions
295
+ manager.delete_instructions()
296
+
297
+ console.print("[green]✓[/green] Custom instructions deleted")
298
+ console.print("[dim]Now using default instructions[/dim]")
299
+
300
+ except InstructionsError as e:
301
+ console.print(f"[red]Error:[/red] {e}")
302
+ raise typer.Exit(1) from None
303
+
304
+
305
+ @app.command()
306
+ def path() -> None:
307
+ """Show path to custom instructions file.
308
+
309
+ Displays the path where custom instructions are (or would be) stored
310
+ for this project, along with status information.
311
+
312
+ Examples:
313
+ --------
314
+ # Show instructions file path
315
+ mcp-ticketer instructions path
316
+
317
+ # Use in scripts
318
+ INST_PATH=$(mcp-ticketer instructions path --quiet)
319
+
320
+ """
321
+ try:
322
+ manager = TicketInstructionsManager()
323
+ inst_path = manager.get_instructions_path()
324
+ exists = manager.has_custom_instructions()
325
+
326
+ console.print(f"[cyan]Instructions file:[/cyan] {inst_path}")
327
+
328
+ if exists:
329
+ console.print("[green]Status:[/green] Custom instructions exist")
330
+
331
+ # Show file size
332
+ try:
333
+ size = inst_path.stat().st_size
334
+ console.print(f"[dim]Size: {size} bytes[/dim]")
335
+ except Exception:
336
+ pass
337
+ else:
338
+ console.print(
339
+ "[yellow]Status:[/yellow] No custom instructions (using defaults)"
340
+ )
341
+ console.print(
342
+ "[dim]Create with: mcp-ticketer instructions add <file>[/dim]"
343
+ )
344
+
345
+ except InstructionsError as e:
346
+ console.print(f"[red]Error:[/red] {e}")
347
+ raise typer.Exit(1) from None
348
+
349
+
350
+ @app.command()
351
+ def edit() -> None:
352
+ """Open instructions in default editor.
353
+
354
+ Opens the custom instructions file in your system's default text editor.
355
+ If custom instructions don't exist yet, creates them with default content
356
+ first.
357
+
358
+ The editor is determined by the EDITOR environment variable, or falls back
359
+ to sensible defaults (vim on Unix, notepad on Windows).
360
+
361
+ Examples:
362
+ --------
363
+ # Edit instructions
364
+ mcp-ticketer instructions edit
365
+
366
+ # Use specific editor
367
+ EDITOR=nano mcp-ticketer instructions edit
368
+
369
+ """
370
+ import os
371
+ import platform
372
+ import subprocess
373
+
374
+ try:
375
+ manager = TicketInstructionsManager()
376
+
377
+ # If no custom instructions exist, create them with defaults
378
+ if not manager.has_custom_instructions():
379
+ console.print("[yellow]No custom instructions yet[/yellow]")
380
+ console.print("[dim]Creating from defaults...[/dim]")
381
+
382
+ # Copy defaults to custom location
383
+ default_content = manager.get_default_instructions()
384
+ manager.set_instructions(default_content)
385
+
386
+ console.print(
387
+ f"[green]✓[/green] Created custom instructions at: {manager.get_instructions_path()}"
388
+ )
389
+
390
+ inst_path = manager.get_instructions_path()
391
+
392
+ # Determine editor
393
+ editor = os.environ.get("EDITOR")
394
+
395
+ if not editor:
396
+ # Platform-specific defaults
397
+ system = platform.system()
398
+ if system == "Windows":
399
+ editor = "notepad"
400
+ else:
401
+ # Unix-like: try common editors
402
+ for candidate in ["vim", "vi", "nano", "emacs"]:
403
+ try:
404
+ result = subprocess.run(
405
+ ["which", candidate],
406
+ capture_output=True,
407
+ text=True,
408
+ timeout=1,
409
+ )
410
+ if result.returncode == 0:
411
+ editor = candidate
412
+ break
413
+ except Exception:
414
+ continue
415
+
416
+ if not editor:
417
+ editor = "vi" # Ultimate fallback
418
+
419
+ console.print(f"[dim]Opening with {editor}...[/dim]")
420
+
421
+ # Open editor
422
+ try:
423
+ subprocess.run([editor, str(inst_path)], check=True)
424
+ console.print(f"[green]✓[/green] Finished editing: {inst_path}")
425
+ except subprocess.CalledProcessError as e:
426
+ console.print(f"[red]Error:[/red] Editor exited with code {e.returncode}")
427
+ raise typer.Exit(1) from None
428
+ except FileNotFoundError:
429
+ console.print(f"[red]Error:[/red] Editor not found: {editor}")
430
+ console.print("Set EDITOR environment variable to your preferred editor")
431
+ raise typer.Exit(1) from None
432
+
433
+ except InstructionsError as e:
434
+ console.print(f"[red]Error:[/red] {e}")
435
+ 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