mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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 (109) 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 +796 -46
  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 +879 -129
  11. mcp_ticketer/adapters/hybrid.py +11 -11
  12. mcp_ticketer/adapters/jira.py +973 -73
  13. mcp_ticketer/adapters/linear/__init__.py +24 -0
  14. mcp_ticketer/adapters/linear/adapter.py +2732 -0
  15. mcp_ticketer/adapters/linear/client.py +344 -0
  16. mcp_ticketer/adapters/linear/mappers.py +420 -0
  17. mcp_ticketer/adapters/linear/queries.py +479 -0
  18. mcp_ticketer/adapters/linear/types.py +360 -0
  19. mcp_ticketer/adapters/linear.py +10 -2315
  20. mcp_ticketer/analysis/__init__.py +23 -0
  21. mcp_ticketer/analysis/orphaned.py +218 -0
  22. mcp_ticketer/analysis/similarity.py +224 -0
  23. mcp_ticketer/analysis/staleness.py +266 -0
  24. mcp_ticketer/cache/memory.py +9 -8
  25. mcp_ticketer/cli/adapter_diagnostics.py +421 -0
  26. mcp_ticketer/cli/auggie_configure.py +116 -15
  27. mcp_ticketer/cli/codex_configure.py +274 -82
  28. mcp_ticketer/cli/configure.py +888 -151
  29. mcp_ticketer/cli/diagnostics.py +400 -157
  30. mcp_ticketer/cli/discover.py +297 -26
  31. mcp_ticketer/cli/gemini_configure.py +119 -26
  32. mcp_ticketer/cli/init_command.py +880 -0
  33. mcp_ticketer/cli/instruction_commands.py +435 -0
  34. mcp_ticketer/cli/linear_commands.py +616 -0
  35. mcp_ticketer/cli/main.py +203 -1165
  36. mcp_ticketer/cli/mcp_configure.py +474 -90
  37. mcp_ticketer/cli/mcp_server_commands.py +415 -0
  38. mcp_ticketer/cli/migrate_config.py +12 -8
  39. mcp_ticketer/cli/platform_commands.py +123 -0
  40. mcp_ticketer/cli/platform_detection.py +418 -0
  41. mcp_ticketer/cli/platform_installer.py +513 -0
  42. mcp_ticketer/cli/python_detection.py +126 -0
  43. mcp_ticketer/cli/queue_commands.py +15 -15
  44. mcp_ticketer/cli/setup_command.py +639 -0
  45. mcp_ticketer/cli/simple_health.py +90 -65
  46. mcp_ticketer/cli/ticket_commands.py +1013 -0
  47. mcp_ticketer/cli/update_checker.py +313 -0
  48. mcp_ticketer/cli/utils.py +114 -66
  49. mcp_ticketer/core/__init__.py +24 -1
  50. mcp_ticketer/core/adapter.py +250 -16
  51. mcp_ticketer/core/config.py +145 -37
  52. mcp_ticketer/core/env_discovery.py +101 -22
  53. mcp_ticketer/core/env_loader.py +349 -0
  54. mcp_ticketer/core/exceptions.py +160 -0
  55. mcp_ticketer/core/http_client.py +26 -26
  56. mcp_ticketer/core/instructions.py +405 -0
  57. mcp_ticketer/core/label_manager.py +732 -0
  58. mcp_ticketer/core/mappers.py +42 -30
  59. mcp_ticketer/core/models.py +280 -28
  60. mcp_ticketer/core/onepassword_secrets.py +379 -0
  61. mcp_ticketer/core/project_config.py +183 -49
  62. mcp_ticketer/core/registry.py +3 -3
  63. mcp_ticketer/core/session_state.py +171 -0
  64. mcp_ticketer/core/state_matcher.py +592 -0
  65. mcp_ticketer/core/url_parser.py +425 -0
  66. mcp_ticketer/core/validators.py +69 -0
  67. mcp_ticketer/defaults/ticket_instructions.md +644 -0
  68. mcp_ticketer/mcp/__init__.py +29 -1
  69. mcp_ticketer/mcp/__main__.py +60 -0
  70. mcp_ticketer/mcp/server/__init__.py +25 -0
  71. mcp_ticketer/mcp/server/__main__.py +60 -0
  72. mcp_ticketer/mcp/server/constants.py +58 -0
  73. mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
  74. mcp_ticketer/mcp/server/dto.py +195 -0
  75. mcp_ticketer/mcp/server/main.py +1343 -0
  76. mcp_ticketer/mcp/server/response_builder.py +206 -0
  77. mcp_ticketer/mcp/server/routing.py +655 -0
  78. mcp_ticketer/mcp/server/server_sdk.py +151 -0
  79. mcp_ticketer/mcp/server/tools/__init__.py +56 -0
  80. mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
  81. mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
  82. mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
  83. mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
  84. mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
  85. mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
  86. mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
  87. mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
  88. mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
  89. mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
  90. mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
  91. mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
  92. mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
  93. mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
  94. mcp_ticketer/queue/__init__.py +1 -0
  95. mcp_ticketer/queue/health_monitor.py +168 -136
  96. mcp_ticketer/queue/manager.py +95 -25
  97. mcp_ticketer/queue/queue.py +40 -21
  98. mcp_ticketer/queue/run_worker.py +6 -1
  99. mcp_ticketer/queue/ticket_registry.py +213 -155
  100. mcp_ticketer/queue/worker.py +109 -49
  101. mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
  102. mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
  103. mcp_ticketer/mcp/server.py +0 -1895
  104. mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
  105. mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
  106. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
  107. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
  108. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
  109. {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,616 @@
1
+ """Linear-specific CLI commands for workspace and team management."""
2
+
3
+ import os
4
+ import re
5
+
6
+ import typer
7
+ from gql import Client, gql
8
+ from gql.transport.httpx import HTTPXTransport
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+
12
+ app = typer.Typer(name="linear", help="Linear workspace and team management")
13
+ console = Console()
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
+
99
+ def _create_linear_client() -> Client:
100
+ """Create a Linear GraphQL client."""
101
+ api_key = os.getenv("LINEAR_API_KEY")
102
+ if not api_key:
103
+ console.print("[red]❌ LINEAR_API_KEY not found in environment[/red]")
104
+ console.print("Set it in .env.local or environment variables")
105
+ raise typer.Exit(1) from None
106
+
107
+ transport = HTTPXTransport(
108
+ url="https://api.linear.app/graphql", headers={"Authorization": api_key}
109
+ )
110
+ return Client(transport=transport, fetch_schema_from_transport=False)
111
+
112
+
113
+ @app.command("workspaces")
114
+ def list_workspaces() -> None:
115
+ """List all accessible Linear workspaces."""
116
+ console.print("🔍 Discovering Linear workspaces...")
117
+
118
+ # Query for current organization and user info
119
+ query = gql(
120
+ """
121
+ query GetWorkspaceInfo {
122
+ viewer {
123
+ id
124
+ name
125
+ email
126
+ organization {
127
+ id
128
+ name
129
+ urlKey
130
+ createdAt
131
+ }
132
+ }
133
+ }
134
+ """
135
+ )
136
+
137
+ try:
138
+ client = _create_linear_client()
139
+ result = client.execute(query)
140
+
141
+ viewer = result.get("viewer", {})
142
+ organization = viewer.get("organization", {})
143
+
144
+ console.print(f"\n👤 User: {viewer.get('name')} ({viewer.get('email')})")
145
+ console.print("🏢 Current Workspace:")
146
+ console.print(f" Name: {organization.get('name')}")
147
+ console.print(f" URL Key: {organization.get('urlKey')}")
148
+ console.print(f" ID: {organization.get('id')}")
149
+ if organization.get("createdAt"):
150
+ console.print(f" Created: {organization.get('createdAt')}")
151
+
152
+ console.print(
153
+ f"\n✅ API key has access to: {organization.get('name')} workspace"
154
+ )
155
+ console.print(
156
+ f"🌐 Workspace URL: https://linear.app/{organization.get('urlKey')}"
157
+ )
158
+
159
+ except Exception as e:
160
+ console.print(f"[red]❌ Error fetching workspace info: {e}[/red]")
161
+ raise typer.Exit(1) from e
162
+
163
+
164
+ @app.command("teams")
165
+ def list_teams(
166
+ workspace: str | None = typer.Option(
167
+ None, "--workspace", "-w", help="Workspace URL key (optional)"
168
+ ),
169
+ all_teams: bool = typer.Option(
170
+ False, "--all", "-a", help="Show all teams across all workspaces"
171
+ ),
172
+ ) -> None:
173
+ """List all teams in the current workspace or all accessible teams."""
174
+ if all_teams:
175
+ console.print("🔍 Discovering ALL accessible Linear teams across workspaces...")
176
+ else:
177
+ console.print("🔍 Discovering Linear teams...")
178
+
179
+ # Query for all teams with pagination
180
+ query = gql(
181
+ """
182
+ query GetTeams($first: Int, $after: String) {
183
+ viewer {
184
+ organization {
185
+ name
186
+ urlKey
187
+ }
188
+ }
189
+ teams(first: $first, after: $after) {
190
+ nodes {
191
+ id
192
+ key
193
+ name
194
+ description
195
+ private
196
+ createdAt
197
+ organization {
198
+ name
199
+ urlKey
200
+ }
201
+ members {
202
+ nodes {
203
+ id
204
+ name
205
+ }
206
+ }
207
+ issues(first: 1) {
208
+ nodes {
209
+ id
210
+ }
211
+ }
212
+ projects(first: 1) {
213
+ nodes {
214
+ id
215
+ }
216
+ }
217
+ }
218
+ pageInfo {
219
+ hasNextPage
220
+ endCursor
221
+ }
222
+ }
223
+ }
224
+ """
225
+ )
226
+
227
+ try:
228
+ client = _create_linear_client()
229
+
230
+ # Fetch all teams with pagination
231
+ all_teams = []
232
+ has_next_page = True
233
+ after_cursor = None
234
+ current_workspace = None
235
+
236
+ while has_next_page:
237
+ variables = {"first": 50}
238
+ if after_cursor:
239
+ variables["after"] = after_cursor
240
+
241
+ result = client.execute(query, variable_values=variables)
242
+
243
+ # Get workspace info from first page
244
+ if current_workspace is None:
245
+ viewer = result.get("viewer", {})
246
+ current_workspace = viewer.get("organization", {})
247
+
248
+ teams_data = result.get("teams", {})
249
+ page_teams = teams_data.get("nodes", [])
250
+ page_info = teams_data.get("pageInfo", {})
251
+
252
+ all_teams.extend(page_teams)
253
+ has_next_page = page_info.get("hasNextPage", False)
254
+ after_cursor = page_info.get("endCursor")
255
+
256
+ # Display workspace info
257
+ console.print(
258
+ f"\n🏢 Workspace: {current_workspace.get('name')} ({current_workspace.get('urlKey')})"
259
+ )
260
+
261
+ # Filter teams by workspace if specified
262
+ if workspace:
263
+ filtered_teams = [
264
+ team
265
+ for team in all_teams
266
+ if team.get("organization", {}).get("urlKey") == workspace
267
+ ]
268
+ if not filtered_teams:
269
+ console.print(
270
+ f"[yellow]No teams found in workspace '{workspace}'[/yellow]"
271
+ )
272
+ return
273
+ all_teams = filtered_teams
274
+ elif not all_teams and current_workspace:
275
+ # If not showing all teams, filter to current workspace only
276
+ filtered_teams = [
277
+ team
278
+ for team in all_teams
279
+ if team.get("organization", {}).get("urlKey")
280
+ == current_workspace.get("urlKey")
281
+ ]
282
+ all_teams = filtered_teams
283
+
284
+ if not all_teams:
285
+ console.print("[yellow]No teams found[/yellow]")
286
+ return
287
+
288
+ # Create table
289
+ title_suffix = " (all workspaces)" if all_teams else ""
290
+ table = Table(title=f"Linear Teams ({len(all_teams)} found){title_suffix}")
291
+ table.add_column("Key", style="cyan", no_wrap=True)
292
+ table.add_column("Name", style="bold")
293
+ table.add_column("Workspace", style="dim")
294
+ table.add_column("ID", style="dim")
295
+ table.add_column("Members", justify="center")
296
+ table.add_column("Issues", justify="center")
297
+ table.add_column("Projects", justify="center")
298
+ table.add_column("Private", justify="center")
299
+
300
+ for team in all_teams:
301
+ member_count = len(team.get("members", {}).get("nodes", []))
302
+ issue_count = len(team.get("issues", {}).get("nodes", []))
303
+ project_count = len(team.get("projects", {}).get("nodes", []))
304
+ is_private = "🔒" if team.get("private") else "🌐"
305
+ workspace_key = team.get("organization", {}).get("urlKey", "")
306
+
307
+ table.add_row(
308
+ team.get("key", ""),
309
+ team.get("name", ""),
310
+ workspace_key,
311
+ team.get("id", ""),
312
+ str(member_count),
313
+ str(issue_count),
314
+ str(project_count),
315
+ is_private,
316
+ )
317
+
318
+ console.print(table)
319
+
320
+ # Show configuration suggestions
321
+ if all_teams:
322
+ console.print("\n💡 Configuration suggestions:")
323
+ for team in all_teams[:3]: # Show first 3 teams
324
+ console.print(f" Team '{team.get('name')}':")
325
+ console.print(f" team_key: '{team.get('key')}'")
326
+ console.print(f" team_id: '{team.get('id')}'")
327
+ console.print()
328
+
329
+ except Exception as e:
330
+ console.print(f"[red]❌ Error fetching teams: {e}[/red]")
331
+ raise typer.Exit(1) from e
332
+
333
+
334
+ @app.command("configure")
335
+ def configure_team(
336
+ team_key: str | None = typer.Option(
337
+ None, "--team-key", "-k", help="Team key (e.g., '1M')"
338
+ ),
339
+ team_id: str | None = typer.Option(None, "--team-id", "-i", help="Team UUID"),
340
+ workspace: str | None = typer.Option(
341
+ None, "--workspace", "-w", help="Workspace URL key"
342
+ ),
343
+ ) -> None:
344
+ """Configure Linear adapter with a specific team."""
345
+ from ..cli.main import load_config, save_config
346
+
347
+ if not team_key and not team_id:
348
+ console.print("[red]❌ Either --team-key or --team-id is required[/red]")
349
+ raise typer.Exit(1) from None
350
+
351
+ console.print("🔧 Configuring Linear adapter...")
352
+
353
+ # Validate team exists
354
+ if team_id:
355
+ # Validate team by ID
356
+ query = gql(
357
+ """
358
+ query GetTeamById($id: String!) {
359
+ team(id: $id) {
360
+ id
361
+ key
362
+ name
363
+ organization {
364
+ name
365
+ urlKey
366
+ }
367
+ }
368
+ }
369
+ """
370
+ )
371
+
372
+ try:
373
+ client = _create_linear_client()
374
+ result = client.execute(query, variable_values={"id": team_id})
375
+ team = result.get("team")
376
+
377
+ if not team:
378
+ console.print(f"[red]❌ Team with ID '{team_id}' not found[/red]")
379
+ raise typer.Exit(1) from None
380
+
381
+ except Exception as e:
382
+ console.print(f"[red]❌ Error validating team: {e}[/red]")
383
+ raise typer.Exit(1) from e
384
+
385
+ elif team_key:
386
+ # Validate team by key
387
+ query = gql(
388
+ """
389
+ query GetTeamByKey($key: String!) {
390
+ teams(filter: { key: { eq: $key } }) {
391
+ nodes {
392
+ id
393
+ key
394
+ name
395
+ organization {
396
+ name
397
+ urlKey
398
+ }
399
+ }
400
+ }
401
+ }
402
+ """
403
+ )
404
+
405
+ try:
406
+ client = _create_linear_client()
407
+ result = client.execute(query, variable_values={"key": team_key})
408
+ teams = result.get("teams", {}).get("nodes", [])
409
+
410
+ if not teams:
411
+ console.print(f"[red]❌ Team with key '{team_key}' not found[/red]")
412
+ raise typer.Exit(1) from None
413
+
414
+ team = teams[0]
415
+ team_id = team["id"] # Use the found team ID
416
+
417
+ except Exception as e:
418
+ console.print(f"[red]❌ Error validating team: {e}[/red]")
419
+ raise typer.Exit(1) from e
420
+
421
+ # Update configuration
422
+ config = load_config()
423
+
424
+ # Ensure adapters section exists
425
+ if "adapters" not in config:
426
+ config["adapters"] = {}
427
+
428
+ # Update Linear adapter configuration
429
+ linear_config = {"type": "linear", "team_id": team_id}
430
+
431
+ if team_key:
432
+ linear_config["team_key"] = team_key
433
+ if workspace:
434
+ linear_config["workspace"] = workspace
435
+
436
+ config["adapters"]["linear"] = linear_config
437
+
438
+ # Save configuration
439
+ save_config(config)
440
+
441
+ console.print("✅ Linear adapter configured successfully!")
442
+ console.print(f" Team: {team.get('name')} ({team.get('key')})")
443
+ console.print(f" Team ID: {team_id}")
444
+ console.print(f" Workspace: {team.get('organization', {}).get('name')}")
445
+
446
+ # Test the configuration
447
+ console.print("\n🧪 Testing configuration...")
448
+ try:
449
+ from ..adapters.linear import LinearAdapter
450
+
451
+ adapter = LinearAdapter(linear_config)
452
+
453
+ # Test by listing a few tickets
454
+ import asyncio
455
+
456
+ tickets = asyncio.run(adapter.list(limit=1))
457
+ console.print(
458
+ f"✅ Configuration test successful! Found {len(tickets)} ticket(s)"
459
+ )
460
+
461
+ except Exception as e:
462
+ console.print(f"[yellow]⚠️ Configuration saved but test failed: {e}[/yellow]")
463
+ console.print("You may need to check your API key or team permissions")
464
+
465
+
466
+ @app.command("info")
467
+ def show_info(
468
+ team_key: str | None = typer.Option(
469
+ None, "--team-key", "-k", help="Team key to show info for"
470
+ ),
471
+ team_id: str | None = typer.Option(
472
+ None, "--team-id", "-i", help="Team UUID to show info for"
473
+ ),
474
+ ) -> None:
475
+ """Show detailed information about a specific team."""
476
+ if not team_key and not team_id:
477
+ console.print("[red]❌ Either --team-key or --team-id is required[/red]")
478
+ raise typer.Exit(1) from None
479
+
480
+ # Query for detailed team information
481
+ if team_id:
482
+ query = gql(
483
+ """
484
+ query GetTeamInfo($id: String!) {
485
+ team(id: $id) {
486
+ id
487
+ key
488
+ name
489
+ description
490
+ private
491
+ createdAt
492
+ updatedAt
493
+ organization {
494
+ name
495
+ urlKey
496
+ }
497
+ members(first: 10) {
498
+ nodes {
499
+ id
500
+ name
501
+ active
502
+ }
503
+ }
504
+ states(first: 20) {
505
+ nodes {
506
+ id
507
+ name
508
+ type
509
+ position
510
+ }
511
+ }
512
+ }
513
+ }
514
+ """
515
+ )
516
+ variables = {"id": team_id}
517
+ else:
518
+ query = gql(
519
+ """
520
+ query GetTeamInfoByKey($key: String!) {
521
+ teams(filter: { key: { eq: $key } }) {
522
+ nodes {
523
+ id
524
+ key
525
+ name
526
+ description
527
+ private
528
+ createdAt
529
+ updatedAt
530
+ organization {
531
+ name
532
+ urlKey
533
+ }
534
+ members(first: 10) {
535
+ nodes {
536
+ id
537
+ name
538
+ active
539
+ }
540
+ }
541
+ states(first: 20) {
542
+ nodes {
543
+ id
544
+ name
545
+ type
546
+ position
547
+ }
548
+ }
549
+ }
550
+ }
551
+ }
552
+ """
553
+ )
554
+ variables = {"key": team_key}
555
+
556
+ try:
557
+ client = _create_linear_client()
558
+ result = client.execute(query, variable_values=variables)
559
+
560
+ if team_id:
561
+ team = result.get("team")
562
+ else:
563
+ teams = result.get("teams", {}).get("nodes", [])
564
+ team = teams[0] if teams else None
565
+
566
+ if not team:
567
+ identifier = team_id or team_key
568
+ console.print(f"[red]❌ Team '{identifier}' not found[/red]")
569
+ raise typer.Exit(1) from None
570
+
571
+ # Display team information
572
+ console.print(f"\n🏷️ Team: {team.get('name')}")
573
+ console.print(f" Key: {team.get('key')}")
574
+ console.print(f" ID: {team.get('id')}")
575
+ console.print(
576
+ f" Workspace: {team.get('organization', {}).get('name')} ({team.get('organization', {}).get('urlKey')})"
577
+ )
578
+ console.print(
579
+ f" Privacy: {'🔒 Private' if team.get('private') else '🌐 Public'}"
580
+ )
581
+
582
+ if team.get("description"):
583
+ console.print(f" Description: {team.get('description')}")
584
+
585
+ console.print(f" Created: {team.get('createdAt')}")
586
+
587
+ # Statistics
588
+ member_count = len(team.get("members", {}).get("nodes", []))
589
+ state_count = len(team.get("states", {}).get("nodes", []))
590
+
591
+ console.print("\n📊 Statistics:")
592
+ console.print(f" Members: {member_count}")
593
+ console.print(f" Workflow States: {state_count}")
594
+
595
+ # Show members
596
+ members = team.get("members", {}).get("nodes", [])
597
+ if members:
598
+ console.print(f"\n👥 Members ({len(members)}):")
599
+ for member in members:
600
+ status = "✅" if member.get("active") else "❌"
601
+ console.print(f" {status} {member.get('name')}")
602
+ if len(members) == 10:
603
+ console.print(" ... (showing first 10 members)")
604
+
605
+ # Show workflow states
606
+ states = team.get("states", {}).get("nodes", [])
607
+ if states:
608
+ console.print(f"\n🔄 Workflow States ({len(states)}):")
609
+ for state in sorted(states, key=lambda s: s.get("position", 0)):
610
+ console.print(f" {state.get('name')} ({state.get('type')})")
611
+ if len(states) == 20:
612
+ console.print(" ... (showing first 20 states)")
613
+
614
+ except Exception as e:
615
+ console.print(f"[red]❌ Error fetching team info: {e}[/red]")
616
+ raise typer.Exit(1) from e