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