mcp-ticketer 0.1.38__py3-none-any.whl → 0.1.39__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/__version__.py +1 -1
- mcp_ticketer/adapters/github.py +27 -11
- mcp_ticketer/adapters/jira.py +185 -53
- mcp_ticketer/adapters/linear.py +78 -9
- mcp_ticketer/cli/linear_commands.py +490 -0
- mcp_ticketer/cli/main.py +102 -9
- mcp_ticketer/cli/utils.py +6 -2
- mcp_ticketer/core/env_loader.py +325 -0
- mcp_ticketer/core/models.py +163 -10
- mcp_ticketer/queue/manager.py +57 -2
- mcp_ticketer/queue/run_worker.py +5 -0
- mcp_ticketer/queue/worker.py +24 -2
- {mcp_ticketer-0.1.38.dist-info → mcp_ticketer-0.1.39.dist-info}/METADATA +1 -1
- {mcp_ticketer-0.1.38.dist-info → mcp_ticketer-0.1.39.dist-info}/RECORD +18 -16
- {mcp_ticketer-0.1.38.dist-info → mcp_ticketer-0.1.39.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.38.dist-info → mcp_ticketer-0.1.39.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.38.dist-info → mcp_ticketer-0.1.39.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.38.dist-info → mcp_ticketer-0.1.39.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
"""Linear-specific CLI commands for workspace and team management."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from gql import gql, Client
|
|
10
|
+
from gql.transport.httpx import HTTPXTransport
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(name="linear", help="Linear workspace and team management")
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _create_linear_client() -> Client:
|
|
17
|
+
"""Create a Linear GraphQL client."""
|
|
18
|
+
api_key = os.getenv("LINEAR_API_KEY")
|
|
19
|
+
if not api_key:
|
|
20
|
+
console.print("[red]❌ LINEAR_API_KEY not found in environment[/red]")
|
|
21
|
+
console.print("Set it in .env.local or environment variables")
|
|
22
|
+
raise typer.Exit(1)
|
|
23
|
+
|
|
24
|
+
transport = HTTPXTransport(
|
|
25
|
+
url="https://api.linear.app/graphql",
|
|
26
|
+
headers={"Authorization": api_key}
|
|
27
|
+
)
|
|
28
|
+
return Client(transport=transport, fetch_schema_from_transport=False)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command("workspaces")
|
|
32
|
+
def list_workspaces():
|
|
33
|
+
"""List all accessible Linear workspaces."""
|
|
34
|
+
console.print("🔍 Discovering Linear workspaces...")
|
|
35
|
+
|
|
36
|
+
# Query for current organization and user info
|
|
37
|
+
query = gql("""
|
|
38
|
+
query GetWorkspaceInfo {
|
|
39
|
+
viewer {
|
|
40
|
+
id
|
|
41
|
+
name
|
|
42
|
+
email
|
|
43
|
+
organization {
|
|
44
|
+
id
|
|
45
|
+
name
|
|
46
|
+
urlKey
|
|
47
|
+
createdAt
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
""")
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
client = _create_linear_client()
|
|
55
|
+
result = client.execute(query)
|
|
56
|
+
|
|
57
|
+
viewer = result.get('viewer', {})
|
|
58
|
+
organization = viewer.get('organization', {})
|
|
59
|
+
|
|
60
|
+
console.print(f"\n👤 User: {viewer.get('name')} ({viewer.get('email')})")
|
|
61
|
+
console.print(f"🏢 Current Workspace:")
|
|
62
|
+
console.print(f" Name: {organization.get('name')}")
|
|
63
|
+
console.print(f" URL Key: {organization.get('urlKey')}")
|
|
64
|
+
console.print(f" ID: {organization.get('id')}")
|
|
65
|
+
if organization.get('createdAt'):
|
|
66
|
+
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
|
+
|
|
71
|
+
except Exception as e:
|
|
72
|
+
console.print(f"[red]❌ Error fetching workspace info: {e}[/red]")
|
|
73
|
+
raise typer.Exit(1)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("teams")
|
|
77
|
+
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
|
+
):
|
|
81
|
+
"""List all teams in the current workspace or all accessible teams."""
|
|
82
|
+
if all_teams:
|
|
83
|
+
console.print("🔍 Discovering ALL accessible Linear teams across workspaces...")
|
|
84
|
+
else:
|
|
85
|
+
console.print("🔍 Discovering Linear teams...")
|
|
86
|
+
|
|
87
|
+
# Query for all teams with pagination
|
|
88
|
+
query = gql("""
|
|
89
|
+
query GetTeams($first: Int, $after: String) {
|
|
90
|
+
viewer {
|
|
91
|
+
organization {
|
|
92
|
+
name
|
|
93
|
+
urlKey
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
teams(first: $first, after: $after) {
|
|
97
|
+
nodes {
|
|
98
|
+
id
|
|
99
|
+
key
|
|
100
|
+
name
|
|
101
|
+
description
|
|
102
|
+
private
|
|
103
|
+
createdAt
|
|
104
|
+
organization {
|
|
105
|
+
name
|
|
106
|
+
urlKey
|
|
107
|
+
}
|
|
108
|
+
members {
|
|
109
|
+
nodes {
|
|
110
|
+
id
|
|
111
|
+
name
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
issues(first: 1) {
|
|
115
|
+
nodes {
|
|
116
|
+
id
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
projects(first: 1) {
|
|
120
|
+
nodes {
|
|
121
|
+
id
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
pageInfo {
|
|
126
|
+
hasNextPage
|
|
127
|
+
endCursor
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
""")
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
client = _create_linear_client()
|
|
135
|
+
|
|
136
|
+
# Fetch all teams with pagination
|
|
137
|
+
all_teams = []
|
|
138
|
+
has_next_page = True
|
|
139
|
+
after_cursor = None
|
|
140
|
+
current_workspace = None
|
|
141
|
+
|
|
142
|
+
while has_next_page:
|
|
143
|
+
variables = {"first": 50}
|
|
144
|
+
if after_cursor:
|
|
145
|
+
variables["after"] = after_cursor
|
|
146
|
+
|
|
147
|
+
result = client.execute(query, variable_values=variables)
|
|
148
|
+
|
|
149
|
+
# Get workspace info from first page
|
|
150
|
+
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
|
+
|
|
158
|
+
all_teams.extend(page_teams)
|
|
159
|
+
has_next_page = page_info.get('hasNextPage', False)
|
|
160
|
+
after_cursor = page_info.get('endCursor')
|
|
161
|
+
|
|
162
|
+
# Display workspace info
|
|
163
|
+
console.print(f"\n🏢 Workspace: {current_workspace.get('name')} ({current_workspace.get('urlKey')})")
|
|
164
|
+
|
|
165
|
+
# Filter teams by workspace if specified
|
|
166
|
+
if workspace:
|
|
167
|
+
filtered_teams = [team for team in all_teams
|
|
168
|
+
if team.get('organization', {}).get('urlKey') == workspace]
|
|
169
|
+
if not filtered_teams:
|
|
170
|
+
console.print(f"[yellow]No teams found in workspace '{workspace}'[/yellow]")
|
|
171
|
+
return
|
|
172
|
+
all_teams = filtered_teams
|
|
173
|
+
elif not all_teams and current_workspace:
|
|
174
|
+
# 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')]
|
|
177
|
+
all_teams = filtered_teams
|
|
178
|
+
|
|
179
|
+
if not all_teams:
|
|
180
|
+
console.print("[yellow]No teams found[/yellow]")
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
# Create table
|
|
184
|
+
title_suffix = " (all workspaces)" if all_teams else ""
|
|
185
|
+
table = Table(title=f"Linear Teams ({len(all_teams)} found){title_suffix}")
|
|
186
|
+
table.add_column("Key", style="cyan", no_wrap=True)
|
|
187
|
+
table.add_column("Name", style="bold")
|
|
188
|
+
table.add_column("Workspace", style="dim")
|
|
189
|
+
table.add_column("ID", style="dim")
|
|
190
|
+
table.add_column("Members", justify="center")
|
|
191
|
+
table.add_column("Issues", justify="center")
|
|
192
|
+
table.add_column("Projects", justify="center")
|
|
193
|
+
table.add_column("Private", justify="center")
|
|
194
|
+
|
|
195
|
+
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', '')
|
|
201
|
+
|
|
202
|
+
table.add_row(
|
|
203
|
+
team.get('key', ''),
|
|
204
|
+
team.get('name', ''),
|
|
205
|
+
workspace_key,
|
|
206
|
+
team.get('id', ''),
|
|
207
|
+
str(member_count),
|
|
208
|
+
str(issue_count),
|
|
209
|
+
str(project_count),
|
|
210
|
+
is_private
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
console.print(table)
|
|
214
|
+
|
|
215
|
+
# Show configuration suggestions
|
|
216
|
+
if all_teams:
|
|
217
|
+
console.print(f"\n💡 Configuration suggestions:")
|
|
218
|
+
for team in all_teams[:3]: # Show first 3 teams
|
|
219
|
+
console.print(f" Team '{team.get('name')}':")
|
|
220
|
+
console.print(f" team_key: '{team.get('key')}'")
|
|
221
|
+
console.print(f" team_id: '{team.get('id')}'")
|
|
222
|
+
console.print()
|
|
223
|
+
|
|
224
|
+
except Exception as e:
|
|
225
|
+
console.print(f"[red]❌ Error fetching teams: {e}[/red]")
|
|
226
|
+
raise typer.Exit(1)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@app.command("configure")
|
|
230
|
+
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
|
+
):
|
|
235
|
+
"""Configure Linear adapter with a specific team."""
|
|
236
|
+
from ..cli.main import load_config, save_config
|
|
237
|
+
|
|
238
|
+
if not team_key and not team_id:
|
|
239
|
+
console.print("[red]❌ Either --team-key or --team-id is required[/red]")
|
|
240
|
+
raise typer.Exit(1)
|
|
241
|
+
|
|
242
|
+
console.print("🔧 Configuring Linear adapter...")
|
|
243
|
+
|
|
244
|
+
# Validate team exists
|
|
245
|
+
if team_id:
|
|
246
|
+
# Validate team by ID
|
|
247
|
+
query = gql("""
|
|
248
|
+
query GetTeamById($id: String!) {
|
|
249
|
+
team(id: $id) {
|
|
250
|
+
id
|
|
251
|
+
key
|
|
252
|
+
name
|
|
253
|
+
organization {
|
|
254
|
+
name
|
|
255
|
+
urlKey
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
""")
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
client = _create_linear_client()
|
|
263
|
+
result = client.execute(query, variable_values={"id": team_id})
|
|
264
|
+
team = result.get('team')
|
|
265
|
+
|
|
266
|
+
if not team:
|
|
267
|
+
console.print(f"[red]❌ Team with ID '{team_id}' not found[/red]")
|
|
268
|
+
raise typer.Exit(1)
|
|
269
|
+
|
|
270
|
+
except Exception as e:
|
|
271
|
+
console.print(f"[red]❌ Error validating team: {e}[/red]")
|
|
272
|
+
raise typer.Exit(1)
|
|
273
|
+
|
|
274
|
+
elif team_key:
|
|
275
|
+
# Validate team by key
|
|
276
|
+
query = gql("""
|
|
277
|
+
query GetTeamByKey($key: String!) {
|
|
278
|
+
teams(filter: { key: { eq: $key } }) {
|
|
279
|
+
nodes {
|
|
280
|
+
id
|
|
281
|
+
key
|
|
282
|
+
name
|
|
283
|
+
organization {
|
|
284
|
+
name
|
|
285
|
+
urlKey
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
""")
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
client = _create_linear_client()
|
|
294
|
+
result = client.execute(query, variable_values={"key": team_key})
|
|
295
|
+
teams = result.get('teams', {}).get('nodes', [])
|
|
296
|
+
|
|
297
|
+
if not teams:
|
|
298
|
+
console.print(f"[red]❌ Team with key '{team_key}' not found[/red]")
|
|
299
|
+
raise typer.Exit(1)
|
|
300
|
+
|
|
301
|
+
team = teams[0]
|
|
302
|
+
team_id = team['id'] # Use the found team ID
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
console.print(f"[red]❌ Error validating team: {e}[/red]")
|
|
306
|
+
raise typer.Exit(1)
|
|
307
|
+
|
|
308
|
+
# Update configuration
|
|
309
|
+
config = load_config()
|
|
310
|
+
|
|
311
|
+
# Ensure adapters section exists
|
|
312
|
+
if "adapters" not in config:
|
|
313
|
+
config["adapters"] = {}
|
|
314
|
+
|
|
315
|
+
# Update Linear adapter configuration
|
|
316
|
+
linear_config = {
|
|
317
|
+
"type": "linear",
|
|
318
|
+
"team_id": team_id
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if team_key:
|
|
322
|
+
linear_config["team_key"] = team_key
|
|
323
|
+
if workspace:
|
|
324
|
+
linear_config["workspace"] = workspace
|
|
325
|
+
|
|
326
|
+
config["adapters"]["linear"] = linear_config
|
|
327
|
+
|
|
328
|
+
# Save configuration
|
|
329
|
+
save_config(config)
|
|
330
|
+
|
|
331
|
+
console.print(f"✅ Linear adapter configured successfully!")
|
|
332
|
+
console.print(f" Team: {team.get('name')} ({team.get('key')})")
|
|
333
|
+
console.print(f" Team ID: {team_id}")
|
|
334
|
+
console.print(f" Workspace: {team.get('organization', {}).get('name')}")
|
|
335
|
+
|
|
336
|
+
# Test the configuration
|
|
337
|
+
console.print("\n🧪 Testing configuration...")
|
|
338
|
+
try:
|
|
339
|
+
from ..adapters.linear import LinearAdapter
|
|
340
|
+
adapter = LinearAdapter(linear_config)
|
|
341
|
+
|
|
342
|
+
# Test by listing a few tickets
|
|
343
|
+
import asyncio
|
|
344
|
+
tickets = asyncio.run(adapter.list(limit=1))
|
|
345
|
+
console.print(f"✅ Configuration test successful! Found {len(tickets)} ticket(s)")
|
|
346
|
+
|
|
347
|
+
except Exception as e:
|
|
348
|
+
console.print(f"[yellow]⚠️ Configuration saved but test failed: {e}[/yellow]")
|
|
349
|
+
console.print("You may need to check your API key or team permissions")
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
@app.command("info")
|
|
353
|
+
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
|
+
):
|
|
357
|
+
"""Show detailed information about a specific team."""
|
|
358
|
+
if not team_key and not team_id:
|
|
359
|
+
console.print("[red]❌ Either --team-key or --team-id is required[/red]")
|
|
360
|
+
raise typer.Exit(1)
|
|
361
|
+
|
|
362
|
+
# Query for detailed team information
|
|
363
|
+
if team_id:
|
|
364
|
+
query = gql("""
|
|
365
|
+
query GetTeamInfo($id: String!) {
|
|
366
|
+
team(id: $id) {
|
|
367
|
+
id
|
|
368
|
+
key
|
|
369
|
+
name
|
|
370
|
+
description
|
|
371
|
+
private
|
|
372
|
+
createdAt
|
|
373
|
+
updatedAt
|
|
374
|
+
organization {
|
|
375
|
+
name
|
|
376
|
+
urlKey
|
|
377
|
+
}
|
|
378
|
+
members(first: 10) {
|
|
379
|
+
nodes {
|
|
380
|
+
id
|
|
381
|
+
name
|
|
382
|
+
active
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
states(first: 20) {
|
|
386
|
+
nodes {
|
|
387
|
+
id
|
|
388
|
+
name
|
|
389
|
+
type
|
|
390
|
+
position
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
""")
|
|
396
|
+
variables = {"id": team_id}
|
|
397
|
+
else:
|
|
398
|
+
query = gql("""
|
|
399
|
+
query GetTeamInfoByKey($key: String!) {
|
|
400
|
+
teams(filter: { key: { eq: $key } }) {
|
|
401
|
+
nodes {
|
|
402
|
+
id
|
|
403
|
+
key
|
|
404
|
+
name
|
|
405
|
+
description
|
|
406
|
+
private
|
|
407
|
+
createdAt
|
|
408
|
+
updatedAt
|
|
409
|
+
organization {
|
|
410
|
+
name
|
|
411
|
+
urlKey
|
|
412
|
+
}
|
|
413
|
+
members(first: 10) {
|
|
414
|
+
nodes {
|
|
415
|
+
id
|
|
416
|
+
name
|
|
417
|
+
active
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
states(first: 20) {
|
|
421
|
+
nodes {
|
|
422
|
+
id
|
|
423
|
+
name
|
|
424
|
+
type
|
|
425
|
+
position
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
""")
|
|
432
|
+
variables = {"key": team_key}
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
client = _create_linear_client()
|
|
436
|
+
result = client.execute(query, variable_values=variables)
|
|
437
|
+
|
|
438
|
+
if team_id:
|
|
439
|
+
team = result.get('team')
|
|
440
|
+
else:
|
|
441
|
+
teams = result.get('teams', {}).get('nodes', [])
|
|
442
|
+
team = teams[0] if teams else None
|
|
443
|
+
|
|
444
|
+
if not team:
|
|
445
|
+
identifier = team_id or team_key
|
|
446
|
+
console.print(f"[red]❌ Team '{identifier}' not found[/red]")
|
|
447
|
+
raise typer.Exit(1)
|
|
448
|
+
|
|
449
|
+
# Display team information
|
|
450
|
+
console.print(f"\n🏷️ Team: {team.get('name')}")
|
|
451
|
+
console.print(f" Key: {team.get('key')}")
|
|
452
|
+
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'):
|
|
457
|
+
console.print(f" Description: {team.get('description')}")
|
|
458
|
+
|
|
459
|
+
console.print(f" Created: {team.get('createdAt')}")
|
|
460
|
+
|
|
461
|
+
# 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:")
|
|
466
|
+
console.print(f" Members: {member_count}")
|
|
467
|
+
console.print(f" Workflow States: {state_count}")
|
|
468
|
+
|
|
469
|
+
# Show members
|
|
470
|
+
members = team.get('members', {}).get('nodes', [])
|
|
471
|
+
if members:
|
|
472
|
+
console.print(f"\n👥 Members ({len(members)}):")
|
|
473
|
+
for member in members:
|
|
474
|
+
status = "✅" if member.get('active') else "❌"
|
|
475
|
+
console.print(f" {status} {member.get('name')}")
|
|
476
|
+
if len(members) == 10:
|
|
477
|
+
console.print(f" ... (showing first 10 members)")
|
|
478
|
+
|
|
479
|
+
# Show workflow states
|
|
480
|
+
states = team.get('states', {}).get('nodes', [])
|
|
481
|
+
if states:
|
|
482
|
+
console.print(f"\n🔄 Workflow States ({len(states)}):")
|
|
483
|
+
for state in sorted(states, key=lambda s: s.get('position', 0)):
|
|
484
|
+
console.print(f" {state.get('name')} ({state.get('type')})")
|
|
485
|
+
if len(states) == 20:
|
|
486
|
+
console.print(f" ... (showing first 20 states)")
|
|
487
|
+
|
|
488
|
+
except Exception as e:
|
|
489
|
+
console.print(f"[red]❌ Error fetching team info: {e}[/red]")
|
|
490
|
+
raise typer.Exit(1)
|
mcp_ticketer/cli/main.py
CHANGED
|
@@ -14,7 +14,7 @@ from rich.table import Table
|
|
|
14
14
|
|
|
15
15
|
from ..__version__ import __version__
|
|
16
16
|
from ..core import AdapterRegistry, Priority, TicketState
|
|
17
|
-
from ..core.models import SearchQuery
|
|
17
|
+
from ..core.models import SearchQuery, Comment
|
|
18
18
|
from ..queue import Queue, QueueStatus, WorkerManager
|
|
19
19
|
from ..queue.health_monitor import QueueHealthMonitor, HealthStatus
|
|
20
20
|
from ..queue.ticket_registry import TicketRegistry
|
|
@@ -24,6 +24,7 @@ import mcp_ticketer.adapters # noqa: F401
|
|
|
24
24
|
from .configure import configure_wizard, set_adapter_config, show_current_config
|
|
25
25
|
from .diagnostics import run_diagnostics
|
|
26
26
|
from .discover import app as discover_app
|
|
27
|
+
from .linear_commands import app as linear_app
|
|
27
28
|
from .migrate_config import migrate_config_command
|
|
28
29
|
from .queue_commands import app as queue_app
|
|
29
30
|
|
|
@@ -1033,18 +1034,64 @@ def create(
|
|
|
1033
1034
|
adapter_name = "aitrackdown"
|
|
1034
1035
|
|
|
1035
1036
|
# Create task data
|
|
1037
|
+
# Import Priority for type checking
|
|
1038
|
+
from ..core.models import Priority as PriorityEnum
|
|
1039
|
+
|
|
1036
1040
|
task_data = {
|
|
1037
1041
|
"title": title,
|
|
1038
1042
|
"description": description,
|
|
1039
|
-
"priority": priority.value if isinstance(priority,
|
|
1043
|
+
"priority": priority.value if isinstance(priority, PriorityEnum) else priority,
|
|
1040
1044
|
"tags": tags or [],
|
|
1041
1045
|
"assignee": assignee,
|
|
1042
1046
|
}
|
|
1043
1047
|
|
|
1044
|
-
#
|
|
1048
|
+
# WORKAROUND: Use direct operation for Linear adapter to bypass worker subprocess issue
|
|
1049
|
+
if adapter_name == "linear":
|
|
1050
|
+
console.print("[yellow]⚠️[/yellow] Using direct operation for Linear adapter (bypassing queue)")
|
|
1051
|
+
try:
|
|
1052
|
+
# Load configuration and create adapter directly
|
|
1053
|
+
config = load_config()
|
|
1054
|
+
adapter_config = config.get("adapters", {}).get(adapter_name, {})
|
|
1055
|
+
|
|
1056
|
+
# Import and create adapter
|
|
1057
|
+
from ..core.registry import AdapterRegistry
|
|
1058
|
+
adapter = AdapterRegistry.get_adapter(adapter_name, adapter_config)
|
|
1059
|
+
|
|
1060
|
+
# Create task directly
|
|
1061
|
+
from ..core.models import Task, Priority
|
|
1062
|
+
task = Task(
|
|
1063
|
+
title=task_data["title"],
|
|
1064
|
+
description=task_data.get("description"),
|
|
1065
|
+
priority=Priority(task_data["priority"]) if task_data.get("priority") else Priority.MEDIUM,
|
|
1066
|
+
tags=task_data.get("tags", []),
|
|
1067
|
+
assignee=task_data.get("assignee")
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
# Create ticket synchronously
|
|
1071
|
+
import asyncio
|
|
1072
|
+
result = asyncio.run(adapter.create(task))
|
|
1073
|
+
|
|
1074
|
+
console.print(f"[green]✓[/green] Ticket created successfully: {result.id}")
|
|
1075
|
+
console.print(f" Title: {result.title}")
|
|
1076
|
+
console.print(f" Priority: {result.priority}")
|
|
1077
|
+
console.print(f" State: {result.state}")
|
|
1078
|
+
# Get URL from metadata if available
|
|
1079
|
+
if result.metadata and 'linear' in result.metadata and 'url' in result.metadata['linear']:
|
|
1080
|
+
console.print(f" URL: {result.metadata['linear']['url']}")
|
|
1081
|
+
|
|
1082
|
+
return result.id
|
|
1083
|
+
|
|
1084
|
+
except Exception as e:
|
|
1085
|
+
console.print(f"[red]❌[/red] Failed to create ticket: {e}")
|
|
1086
|
+
raise
|
|
1087
|
+
|
|
1088
|
+
# Use queue for other adapters
|
|
1045
1089
|
queue = Queue()
|
|
1046
1090
|
queue_id = queue.add(
|
|
1047
|
-
ticket_data=task_data,
|
|
1091
|
+
ticket_data=task_data,
|
|
1092
|
+
adapter=adapter_name,
|
|
1093
|
+
operation="create",
|
|
1094
|
+
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
1048
1095
|
)
|
|
1049
1096
|
|
|
1050
1097
|
# Register in ticket registry for tracking
|
|
@@ -1127,12 +1174,15 @@ def list_tickets(
|
|
|
1127
1174
|
table.add_column("Assignee", style="blue")
|
|
1128
1175
|
|
|
1129
1176
|
for ticket in tickets:
|
|
1177
|
+
# Handle assignee field - Epic doesn't have assignee, Task does
|
|
1178
|
+
assignee = getattr(ticket, 'assignee', None) or "-"
|
|
1179
|
+
|
|
1130
1180
|
table.add_row(
|
|
1131
1181
|
ticket.id or "N/A",
|
|
1132
1182
|
ticket.title,
|
|
1133
1183
|
ticket.state,
|
|
1134
1184
|
ticket.priority,
|
|
1135
|
-
|
|
1185
|
+
assignee,
|
|
1136
1186
|
)
|
|
1137
1187
|
|
|
1138
1188
|
console.print(table)
|
|
@@ -1188,6 +1238,42 @@ def show(
|
|
|
1188
1238
|
console.print(comment.content)
|
|
1189
1239
|
|
|
1190
1240
|
|
|
1241
|
+
@app.command()
|
|
1242
|
+
def comment(
|
|
1243
|
+
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
1244
|
+
content: str = typer.Argument(..., help="Comment content"),
|
|
1245
|
+
adapter: Optional[AdapterType] = typer.Option(
|
|
1246
|
+
None, "--adapter", help="Override default adapter"
|
|
1247
|
+
),
|
|
1248
|
+
) -> None:
|
|
1249
|
+
"""Add a comment to a ticket."""
|
|
1250
|
+
|
|
1251
|
+
async def _comment():
|
|
1252
|
+
adapter_instance = get_adapter(
|
|
1253
|
+
override_adapter=adapter.value if adapter else None
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
# Create comment
|
|
1257
|
+
comment = Comment(
|
|
1258
|
+
ticket_id=ticket_id,
|
|
1259
|
+
content=content,
|
|
1260
|
+
author="cli-user" # Could be made configurable
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
result = await adapter_instance.add_comment(comment)
|
|
1264
|
+
return result
|
|
1265
|
+
|
|
1266
|
+
try:
|
|
1267
|
+
result = asyncio.run(_comment())
|
|
1268
|
+
console.print(f"[green]✓[/green] Comment added successfully")
|
|
1269
|
+
if result.id:
|
|
1270
|
+
console.print(f"Comment ID: {result.id}")
|
|
1271
|
+
console.print(f"Content: {content}")
|
|
1272
|
+
except Exception as e:
|
|
1273
|
+
console.print(f"[red]✗[/red] Failed to add comment: {e}")
|
|
1274
|
+
raise typer.Exit(1)
|
|
1275
|
+
|
|
1276
|
+
|
|
1191
1277
|
@app.command()
|
|
1192
1278
|
def update(
|
|
1193
1279
|
ticket_id: str = typer.Argument(..., help="Ticket ID"),
|
|
@@ -1231,9 +1317,14 @@ def update(
|
|
|
1231
1317
|
# Add ticket_id to updates
|
|
1232
1318
|
updates["ticket_id"] = ticket_id
|
|
1233
1319
|
|
|
1234
|
-
# Add to queue
|
|
1320
|
+
# Add to queue with explicit project directory
|
|
1235
1321
|
queue = Queue()
|
|
1236
|
-
queue_id = queue.add(
|
|
1322
|
+
queue_id = queue.add(
|
|
1323
|
+
ticket_data=updates,
|
|
1324
|
+
adapter=adapter_name,
|
|
1325
|
+
operation="update",
|
|
1326
|
+
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
1327
|
+
)
|
|
1237
1328
|
|
|
1238
1329
|
console.print(f"[green]✓[/green] Queued ticket update: {queue_id}")
|
|
1239
1330
|
for key, value in updates.items():
|
|
@@ -1289,7 +1380,7 @@ def transition(
|
|
|
1289
1380
|
adapter.value if adapter else config.get("default_adapter", "aitrackdown")
|
|
1290
1381
|
)
|
|
1291
1382
|
|
|
1292
|
-
# Add to queue
|
|
1383
|
+
# Add to queue with explicit project directory
|
|
1293
1384
|
queue = Queue()
|
|
1294
1385
|
queue_id = queue.add(
|
|
1295
1386
|
ticket_data={
|
|
@@ -1300,6 +1391,7 @@ def transition(
|
|
|
1300
1391
|
},
|
|
1301
1392
|
adapter=adapter_name,
|
|
1302
1393
|
operation="transition",
|
|
1394
|
+
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
1303
1395
|
)
|
|
1304
1396
|
|
|
1305
1397
|
console.print(f"[green]✓[/green] Queued state transition: {queue_id}")
|
|
@@ -1679,7 +1771,8 @@ def mcp_auggie(
|
|
|
1679
1771
|
raise typer.Exit(1)
|
|
1680
1772
|
|
|
1681
1773
|
|
|
1682
|
-
# Add
|
|
1774
|
+
# Add command groups to main app (must be after all subcommands are defined)
|
|
1775
|
+
app.add_typer(linear_app, name="linear")
|
|
1683
1776
|
app.add_typer(mcp_app, name="mcp")
|
|
1684
1777
|
|
|
1685
1778
|
|
mcp_ticketer/cli/utils.py
CHANGED
|
@@ -210,10 +210,14 @@ class CommonPatterns:
|
|
|
210
210
|
config = CommonPatterns.load_config()
|
|
211
211
|
adapter_name = config.get("default_adapter", "aitrackdown")
|
|
212
212
|
|
|
213
|
-
# Add to queue
|
|
213
|
+
# Add to queue with explicit project directory
|
|
214
|
+
from pathlib import Path
|
|
214
215
|
queue = Queue()
|
|
215
216
|
queue_id = queue.add(
|
|
216
|
-
ticket_data=ticket_data,
|
|
217
|
+
ticket_data=ticket_data,
|
|
218
|
+
adapter=adapter_name,
|
|
219
|
+
operation=operation,
|
|
220
|
+
project_dir=str(Path.cwd()) # Explicitly pass current project directory
|
|
217
221
|
)
|
|
218
222
|
|
|
219
223
|
if show_progress:
|