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.
- mcp_ticketer/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {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
|